9262 lines
296 KiB
JavaScript
9262 lines
296 KiB
JavaScript
/* mapbox.js 0.6.7 */
|
|
!function() {
|
|
var define; // Undefine define (require.js)
|
|
/*!
|
|
* bean.js - copyright Jacob Thornton 2011
|
|
* https://github.com/fat/bean
|
|
* MIT License
|
|
* special thanks to:
|
|
* dean edwards: http://dean.edwards.name/
|
|
* dperini: https://github.com/dperini/nwevents
|
|
* the entire mootools team: github.com/mootools/mootools-core
|
|
*/
|
|
!function (name, context, definition) {
|
|
if (typeof module !== 'undefined') module.exports = definition(name, context);
|
|
else if (typeof define === 'function' && typeof define.amd === 'object') define(definition);
|
|
else context[name] = definition(name, context);
|
|
}('bean', this, function (name, context) {
|
|
var win = window
|
|
, old = context[name]
|
|
, overOut = /over|out/
|
|
, namespaceRegex = /[^\.]*(?=\..*)\.|.*/
|
|
, nameRegex = /\..*/
|
|
, addEvent = 'addEventListener'
|
|
, attachEvent = 'attachEvent'
|
|
, removeEvent = 'removeEventListener'
|
|
, detachEvent = 'detachEvent'
|
|
, ownerDocument = 'ownerDocument'
|
|
, targetS = 'target'
|
|
, qSA = 'querySelectorAll'
|
|
, doc = document || {}
|
|
, root = doc.documentElement || {}
|
|
, W3C_MODEL = root[addEvent]
|
|
, eventSupport = W3C_MODEL ? addEvent : attachEvent
|
|
, slice = Array.prototype.slice
|
|
, mouseTypeRegex = /click|mouse(?!(.*wheel|scroll))|menu|drag|drop/i
|
|
, mouseWheelTypeRegex = /mouse.*(wheel|scroll)/i
|
|
, textTypeRegex = /^text/i
|
|
, touchTypeRegex = /^touch|^gesture/i
|
|
, ONE = {} // singleton for quick matching making add() do one()
|
|
|
|
, nativeEvents = (function (hash, events, i) {
|
|
for (i = 0; i < events.length; i++)
|
|
hash[events[i]] = 1
|
|
return hash
|
|
}({}, (
|
|
'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
|
|
'mousewheel mousemultiwheel DOMMouseScroll ' + // mouse wheel
|
|
'mouseover mouseout mousemove selectstart selectend ' + // mouse movement
|
|
'keydown keypress keyup ' + // keyboard
|
|
'orientationchange ' + // mobile
|
|
'focus blur change reset select submit ' + // form elements
|
|
'load unload beforeunload resize move DOMContentLoaded '+ // window
|
|
'readystatechange message ' + // window
|
|
'error abort scroll ' + // misc
|
|
(W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event
|
|
// that doesn't actually exist, so make sure we only do these on newer browsers
|
|
'show ' + // mouse buttons
|
|
'input invalid ' + // form elements
|
|
'touchstart touchmove touchend touchcancel ' + // touch
|
|
'gesturestart gesturechange gestureend ' + // gesture
|
|
'readystatechange pageshow pagehide popstate ' + // window
|
|
'hashchange offline online ' + // window
|
|
'afterprint beforeprint ' + // printing
|
|
'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd
|
|
'loadstart progress suspend emptied stalled loadmetadata ' + // media
|
|
'loadeddata canplay canplaythrough playing waiting seeking ' + // media
|
|
'seeked ended durationchange timeupdate play pause ratechange ' + // media
|
|
'volumechange cuechange ' + // media
|
|
'checking noupdate downloading cached updateready obsolete ' + // appcache
|
|
'' : '')
|
|
).split(' ')
|
|
))
|
|
|
|
, customEvents = (function () {
|
|
var cdp = 'compareDocumentPosition'
|
|
, isAncestor = cdp in root
|
|
? function (element, container) {
|
|
return container[cdp] && (container[cdp](element) & 16) === 16
|
|
}
|
|
: 'contains' in root
|
|
? function (element, container) {
|
|
container = container.nodeType === 9 || container === window ? root : container
|
|
return container !== element && container.contains(element)
|
|
}
|
|
: function (element, container) {
|
|
while (element = element.parentNode) if (element === container) return 1
|
|
return 0
|
|
}
|
|
|
|
function check(event) {
|
|
var related = event.relatedTarget
|
|
return !related
|
|
? related === null
|
|
: (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isAncestor(related, this))
|
|
}
|
|
|
|
return {
|
|
mouseenter: { base: 'mouseover', condition: check }
|
|
, mouseleave: { base: 'mouseout', condition: check }
|
|
, mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' }
|
|
}
|
|
}())
|
|
|
|
, fixEvent = (function () {
|
|
var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ')
|
|
, mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' '))
|
|
, mouseWheelProps = mouseProps.concat('wheelDelta wheelDeltaX wheelDeltaY wheelDeltaZ axis'.split(' ')) // 'axis' is FF specific
|
|
, keyProps = commonProps.concat('char charCode key keyCode keyIdentifier keyLocation'.split(' '))
|
|
, textProps = commonProps.concat(['data'])
|
|
, touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' '))
|
|
, messageProps = commonProps.concat(['data', 'origin', 'source'])
|
|
, preventDefault = 'preventDefault'
|
|
, createPreventDefault = function (event) {
|
|
return function () {
|
|
if (event[preventDefault])
|
|
event[preventDefault]()
|
|
else
|
|
event.returnValue = false
|
|
}
|
|
}
|
|
, stopPropagation = 'stopPropagation'
|
|
, createStopPropagation = function (event) {
|
|
return function () {
|
|
if (event[stopPropagation])
|
|
event[stopPropagation]()
|
|
else
|
|
event.cancelBubble = true
|
|
}
|
|
}
|
|
, createStop = function (synEvent) {
|
|
return function () {
|
|
synEvent[preventDefault]()
|
|
synEvent[stopPropagation]()
|
|
synEvent.stopped = true
|
|
}
|
|
}
|
|
, copyProps = function (event, result, props) {
|
|
var i, p
|
|
for (i = props.length; i--;) {
|
|
p = props[i]
|
|
if (!(p in result) && p in event) result[p] = event[p]
|
|
}
|
|
}
|
|
|
|
return function (event, isNative) {
|
|
var result = { originalEvent: event, isNative: isNative }
|
|
if (!event)
|
|
return result
|
|
|
|
var props
|
|
, type = event.type
|
|
, target = event[targetS] || event.srcElement
|
|
|
|
result[preventDefault] = createPreventDefault(event)
|
|
result[stopPropagation] = createStopPropagation(event)
|
|
result.stop = createStop(result)
|
|
result[targetS] = target && target.nodeType === 3 ? target.parentNode : target
|
|
|
|
if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive
|
|
if (type.indexOf('key') !== -1) {
|
|
props = keyProps
|
|
result.keyCode = event.keyCode || event.which
|
|
} else if (mouseTypeRegex.test(type)) {
|
|
props = mouseProps
|
|
result.rightClick = event.which === 3 || event.button === 2
|
|
result.pos = { x: 0, y: 0 }
|
|
if (event.pageX || event.pageY) {
|
|
result.clientX = event.pageX
|
|
result.clientY = event.pageY
|
|
} else if (event.clientX || event.clientY) {
|
|
result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft
|
|
result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop
|
|
}
|
|
if (overOut.test(type))
|
|
result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element']
|
|
} else if (touchTypeRegex.test(type)) {
|
|
props = touchProps
|
|
} else if (mouseWheelTypeRegex.test(type)) {
|
|
props = mouseWheelProps
|
|
} else if (textTypeRegex.test(type)) {
|
|
props = textProps
|
|
} else if (type === 'message') {
|
|
props = messageProps
|
|
}
|
|
copyProps(event, result, props || commonProps)
|
|
}
|
|
return result
|
|
}
|
|
}())
|
|
|
|
// if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both
|
|
, targetElement = function (element, isNative) {
|
|
return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element
|
|
}
|
|
|
|
// we use one of these per listener, of any type
|
|
, RegEntry = (function () {
|
|
function entry(element, type, handler, original, namespaces) {
|
|
var isNative = this.isNative = nativeEvents[type] && element[eventSupport]
|
|
this.element = element
|
|
this.type = type
|
|
this.handler = handler
|
|
this.original = original
|
|
this.namespaces = namespaces
|
|
this.custom = customEvents[type]
|
|
this.eventType = W3C_MODEL || isNative ? type : 'propertychange'
|
|
this.customType = !W3C_MODEL && !isNative && type
|
|
this[targetS] = targetElement(element, isNative)
|
|
this[eventSupport] = this[targetS][eventSupport]
|
|
}
|
|
|
|
entry.prototype = {
|
|
// given a list of namespaces, is our entry in any of them?
|
|
inNamespaces: function (checkNamespaces) {
|
|
var i, j
|
|
if (!checkNamespaces)
|
|
return true
|
|
if (!this.namespaces)
|
|
return false
|
|
for (i = checkNamespaces.length; i--;) {
|
|
for (j = this.namespaces.length; j--;) {
|
|
if (checkNamespaces[i] === this.namespaces[j])
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// match by element, original fn (opt), handler fn (opt)
|
|
, matches: function (checkElement, checkOriginal, checkHandler) {
|
|
return this.element === checkElement &&
|
|
(!checkOriginal || this.original === checkOriginal) &&
|
|
(!checkHandler || this.handler === checkHandler)
|
|
}
|
|
}
|
|
|
|
return entry
|
|
}())
|
|
|
|
, registry = (function () {
|
|
// our map stores arrays by event type, just because it's better than storing
|
|
// everything in a single array. uses '$' as a prefix for the keys for safety
|
|
var map = {}
|
|
|
|
// generic functional search of our registry for matching listeners,
|
|
// `fn` returns false to break out of the loop
|
|
, forAll = function (element, type, original, handler, fn) {
|
|
if (!type || type === '*') {
|
|
// search the whole registry
|
|
for (var t in map) {
|
|
if (t.charAt(0) === '$')
|
|
forAll(element, t.substr(1), original, handler, fn)
|
|
}
|
|
} else {
|
|
var i = 0, l, list = map['$' + type], all = element === '*'
|
|
if (!list)
|
|
return
|
|
for (l = list.length; i < l; i++) {
|
|
if (all || list[i].matches(element, original, handler))
|
|
if (!fn(list[i], list, i, type))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
, has = function (element, type, original) {
|
|
// we're not using forAll here simply because it's a bit slower and this
|
|
// needs to be fast
|
|
var i, list = map['$' + type]
|
|
if (list) {
|
|
for (i = list.length; i--;) {
|
|
if (list[i].matches(element, original, null))
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
, get = function (element, type, original) {
|
|
var entries = []
|
|
forAll(element, type, original, null, function (entry) { return entries.push(entry) })
|
|
return entries
|
|
}
|
|
|
|
, put = function (entry) {
|
|
(map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry)
|
|
return entry
|
|
}
|
|
|
|
, del = function (entry) {
|
|
forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) {
|
|
list.splice(i, 1)
|
|
if (list.length === 0)
|
|
delete map['$' + entry.type]
|
|
return false
|
|
})
|
|
}
|
|
|
|
// dump all entries, used for onunload
|
|
, entries = function () {
|
|
var t, entries = []
|
|
for (t in map) {
|
|
if (t.charAt(0) === '$')
|
|
entries = entries.concat(map[t])
|
|
}
|
|
return entries
|
|
}
|
|
|
|
return { has: has, get: get, put: put, del: del, entries: entries }
|
|
}())
|
|
|
|
, selectorEngine = doc[qSA]
|
|
? function (s, r) {
|
|
return r[qSA](s)
|
|
}
|
|
: function () {
|
|
throw new Error('Bean: No selector engine installed') // eeek
|
|
}
|
|
|
|
, setSelectorEngine = function (e) {
|
|
selectorEngine = e
|
|
}
|
|
|
|
// add and remove listeners to DOM elements
|
|
, listener = W3C_MODEL ? function (element, type, fn, add) {
|
|
element[add ? addEvent : removeEvent](type, fn, false)
|
|
} : function (element, type, fn, add, custom) {
|
|
if (custom && add && element['_on' + custom] === null)
|
|
element['_on' + custom] = 0
|
|
element[add ? attachEvent : detachEvent]('on' + type, fn)
|
|
}
|
|
|
|
, nativeHandler = function (element, fn, args) {
|
|
var beanDel = fn.__beanDel
|
|
, handler = function (event) {
|
|
event = fixEvent(event || ((this[ownerDocument] || this.document || this).parentWindow || win).event, true)
|
|
if (beanDel) // delegated event, fix the fix
|
|
event.currentTarget = beanDel.ft(event[targetS], element)
|
|
return fn.apply(element, [event].concat(args))
|
|
}
|
|
handler.__beanDel = beanDel
|
|
return handler
|
|
}
|
|
|
|
, customHandler = function (element, fn, type, condition, args, isNative) {
|
|
var beanDel = fn.__beanDel
|
|
, handler = function (event) {
|
|
var target = beanDel ? beanDel.ft(event[targetS], element) : this // deleated event
|
|
if (condition ? condition.apply(target, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) {
|
|
if (event) {
|
|
event = fixEvent(event || ((this[ownerDocument] || this.document || this).parentWindow || win).event, isNative)
|
|
event.currentTarget = target
|
|
}
|
|
fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args))
|
|
}
|
|
}
|
|
handler.__beanDel = beanDel
|
|
return handler
|
|
}
|
|
|
|
, once = function (rm, element, type, fn, originalFn) {
|
|
// wrap the handler in a handler that does a remove as well
|
|
return function () {
|
|
rm(element, type, originalFn)
|
|
fn.apply(this, arguments)
|
|
}
|
|
}
|
|
|
|
, removeListener = function (element, orgType, handler, namespaces) {
|
|
var i, l, entry
|
|
, type = (orgType && orgType.replace(nameRegex, ''))
|
|
, handlers = registry.get(element, type, handler)
|
|
|
|
for (i = 0, l = handlers.length; i < l; i++) {
|
|
if (handlers[i].inNamespaces(namespaces)) {
|
|
if ((entry = handlers[i])[eventSupport])
|
|
listener(entry[targetS], entry.eventType, entry.handler, false, entry.type)
|
|
// TODO: this is problematic, we have a registry.get() and registry.del() that
|
|
// both do registry searches so we waste cycles doing this. Needs to be rolled into
|
|
// a single registry.forAll(fn) that removes while finding, but the catch is that
|
|
// we'll be splicing the arrays that we're iterating over. Needs extra tests to
|
|
// make sure we don't screw it up. @rvagg
|
|
registry.del(entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
, addListener = function (element, orgType, fn, originalFn, args) {
|
|
var entry
|
|
, type = orgType.replace(nameRegex, '')
|
|
, namespaces = orgType.replace(namespaceRegex, '').split('.')
|
|
|
|
if (registry.has(element, type, fn))
|
|
return element // no dupe
|
|
if (type === 'unload')
|
|
fn = once(removeListener, element, type, fn, originalFn) // self clean-up
|
|
if (customEvents[type]) {
|
|
if (customEvents[type].condition)
|
|
fn = customHandler(element, fn, type, customEvents[type].condition, args, true)
|
|
type = customEvents[type].base || type
|
|
}
|
|
entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces))
|
|
entry.handler = entry.isNative ?
|
|
nativeHandler(element, entry.handler, args) :
|
|
customHandler(element, entry.handler, type, false, args, false)
|
|
if (entry[eventSupport])
|
|
listener(entry[targetS], entry.eventType, entry.handler, true, entry.customType)
|
|
}
|
|
|
|
, del = function (selector, fn, $) {
|
|
//TODO: findTarget (therefore $) is called twice, once for match and once for
|
|
// setting e.currentTarget, fix this so it's only needed once
|
|
var findTarget = function (target, root) {
|
|
var i, array = typeof selector === 'string' ? $(selector, root) : selector
|
|
for (; target && target !== root; target = target.parentNode) {
|
|
for (i = array.length; i--;) {
|
|
if (array[i] === target)
|
|
return target
|
|
}
|
|
}
|
|
}
|
|
, handler = function (e) {
|
|
var match = findTarget(e[targetS], this)
|
|
match && fn.apply(match, arguments)
|
|
}
|
|
|
|
handler.__beanDel = {
|
|
ft: findTarget // attach it here for customEvents to use too
|
|
, selector: selector
|
|
, $: $
|
|
}
|
|
return handler
|
|
}
|
|
|
|
, remove = function (element, typeSpec, fn) {
|
|
var k, type, namespaces, i
|
|
, rm = removeListener
|
|
, isString = typeSpec && typeof typeSpec === 'string'
|
|
|
|
if (isString && typeSpec.indexOf(' ') > 0) {
|
|
// remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3')
|
|
typeSpec = typeSpec.split(' ')
|
|
for (i = typeSpec.length; i--;)
|
|
remove(element, typeSpec[i], fn)
|
|
return element
|
|
}
|
|
type = isString && typeSpec.replace(nameRegex, '')
|
|
if (type && customEvents[type])
|
|
type = customEvents[type].type
|
|
if (!typeSpec || isString) {
|
|
// remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3)
|
|
if (namespaces = isString && typeSpec.replace(namespaceRegex, ''))
|
|
namespaces = namespaces.split('.')
|
|
rm(element, type, fn, namespaces)
|
|
} else if (typeof typeSpec === 'function') {
|
|
// remove(el, fn)
|
|
rm(element, null, typeSpec)
|
|
} else {
|
|
// remove(el, { t1: fn1, t2, fn2 })
|
|
for (k in typeSpec) {
|
|
if (typeSpec.hasOwnProperty(k))
|
|
remove(element, k, typeSpec[k])
|
|
}
|
|
}
|
|
return element
|
|
}
|
|
|
|
// 5th argument, $=selector engine, is deprecated and will be removed
|
|
, add = function (element, events, fn, delfn, $) {
|
|
var type, types, i, args
|
|
, originalFn = fn
|
|
, isDel = fn && typeof fn === 'string'
|
|
|
|
if (events && !fn && typeof events === 'object') {
|
|
for (type in events) {
|
|
if (events.hasOwnProperty(type))
|
|
add.apply(this, [ element, type, events[type] ])
|
|
}
|
|
} else {
|
|
args = arguments.length > 3 ? slice.call(arguments, 3) : []
|
|
types = (isDel ? fn : events).split(' ')
|
|
isDel && (fn = del(events, (originalFn = delfn), $ || selectorEngine)) && (args = slice.call(args, 1))
|
|
// special case for one()
|
|
this === ONE && (fn = once(remove, element, events, fn, originalFn))
|
|
for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args)
|
|
}
|
|
return element
|
|
}
|
|
|
|
, one = function () {
|
|
return add.apply(ONE, arguments)
|
|
}
|
|
|
|
, fireListener = W3C_MODEL ? function (isNative, type, element) {
|
|
var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents')
|
|
evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1)
|
|
element.dispatchEvent(evt)
|
|
} : function (isNative, type, element) {
|
|
element = targetElement(element, isNative)
|
|
// if not-native then we're using onpropertychange so we just increment a custom property
|
|
isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++
|
|
}
|
|
|
|
, fire = function (element, type, args) {
|
|
var i, j, l, names, handlers
|
|
, types = type.split(' ')
|
|
|
|
for (i = types.length; i--;) {
|
|
type = types[i].replace(nameRegex, '')
|
|
if (names = types[i].replace(namespaceRegex, ''))
|
|
names = names.split('.')
|
|
if (!names && !args && element[eventSupport]) {
|
|
fireListener(nativeEvents[type], type, element)
|
|
} else {
|
|
// non-native event, either because of a namespace, arguments or a non DOM element
|
|
// iterate over all listeners and manually 'fire'
|
|
handlers = registry.get(element, type)
|
|
args = [false].concat(args)
|
|
for (j = 0, l = handlers.length; j < l; j++) {
|
|
if (handlers[j].inNamespaces(names))
|
|
handlers[j].handler.apply(element, args)
|
|
}
|
|
}
|
|
}
|
|
return element
|
|
}
|
|
|
|
, clone = function (element, from, type) {
|
|
var i = 0
|
|
, handlers = registry.get(from, type)
|
|
, l = handlers.length
|
|
, args, beanDel
|
|
|
|
for (;i < l; i++) {
|
|
if (handlers[i].original) {
|
|
beanDel = handlers[i].handler.__beanDel
|
|
if (beanDel) {
|
|
args = [ element, beanDel.selector, handlers[i].type, handlers[i].original, beanDel.$]
|
|
} else
|
|
args = [ element, handlers[i].type, handlers[i].original ]
|
|
add.apply(null, args)
|
|
}
|
|
}
|
|
return element
|
|
}
|
|
|
|
, bean = {
|
|
add: add
|
|
, one: one
|
|
, remove: remove
|
|
, clone: clone
|
|
, fire: fire
|
|
, setSelectorEngine: setSelectorEngine
|
|
, noConflict: function () {
|
|
context[name] = old
|
|
return this
|
|
}
|
|
}
|
|
|
|
if (win[attachEvent]) {
|
|
// for IE, clean up on unload to avoid leaks
|
|
var cleanup = function () {
|
|
var i, entries = registry.entries()
|
|
for (i in entries) {
|
|
if (entries[i].type && entries[i].type !== 'unload')
|
|
remove(entries[i].element, entries[i].type)
|
|
}
|
|
win[detachEvent]('onunload', cleanup)
|
|
win.CollectGarbage && win.CollectGarbage()
|
|
}
|
|
win[attachEvent]('onunload', cleanup)
|
|
}
|
|
|
|
return bean
|
|
})
|
|
/*!
|
|
* Reqwest! A general purpose XHR connection manager
|
|
* (c) Dustin Diaz 2012
|
|
* https://github.com/ded/reqwest
|
|
* license MIT
|
|
*/
|
|
!function (name, definition) {
|
|
if (typeof module != 'undefined') module.exports = definition()
|
|
else if (typeof define == 'function' && define.amd) define(definition)
|
|
else this[name] = definition()
|
|
}('reqwest', function () {
|
|
|
|
var win = window
|
|
, doc = document
|
|
, twoHundo = /^20\d$/
|
|
, byTag = 'getElementsByTagName'
|
|
, readyState = 'readyState'
|
|
, contentType = 'Content-Type'
|
|
, requestedWith = 'X-Requested-With'
|
|
, head = doc[byTag]('head')[0]
|
|
, uniqid = 0
|
|
, callbackPrefix = 'reqwest_' + (+new Date())
|
|
, lastValue // data stored by the most recent JSONP callback
|
|
, xmlHttpRequest = 'XMLHttpRequest'
|
|
|
|
var isArray = typeof Array.isArray == 'function' ? Array.isArray : function (a) {
|
|
return a instanceof Array
|
|
}
|
|
var defaultHeaders = {
|
|
contentType: 'application/x-www-form-urlencoded'
|
|
, requestedWith: xmlHttpRequest
|
|
, accept: {
|
|
'*': 'text/javascript, text/html, application/xml, text/xml, */*'
|
|
, xml: 'application/xml, text/xml'
|
|
, html: 'text/html'
|
|
, text: 'text/plain'
|
|
, json: 'application/json, text/javascript'
|
|
, js: 'application/javascript, text/javascript'
|
|
}
|
|
}
|
|
var xhr = win[xmlHttpRequest] ?
|
|
function () {
|
|
return new XMLHttpRequest()
|
|
} :
|
|
function () {
|
|
return new ActiveXObject('Microsoft.XMLHTTP')
|
|
}
|
|
|
|
function handleReadyState(o, success, error) {
|
|
return function () {
|
|
if (o && o[readyState] == 4) {
|
|
if (twoHundo.test(o.status)) {
|
|
success(o)
|
|
} else {
|
|
error(o)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function setHeaders(http, o) {
|
|
var headers = o.headers || {}, h
|
|
headers.Accept = headers.Accept || defaultHeaders.accept[o.type] || defaultHeaders.accept['*']
|
|
// breaks cross-origin requests with legacy browsers
|
|
if (!o.crossOrigin && !headers[requestedWith]) headers[requestedWith] = defaultHeaders.requestedWith
|
|
if (!headers[contentType]) headers[contentType] = o.contentType || defaultHeaders.contentType
|
|
for (h in headers) {
|
|
headers.hasOwnProperty(h) && http.setRequestHeader(h, headers[h])
|
|
}
|
|
}
|
|
|
|
function setCredentials(http, o) {
|
|
if (typeof o.withCredentials !== "undefined" && typeof http.withCredentials !== "undefined") {
|
|
http.withCredentials = !!o.withCredentials
|
|
}
|
|
}
|
|
|
|
function generalCallback(data) {
|
|
lastValue = data
|
|
}
|
|
|
|
function urlappend(url, s) {
|
|
return url + (/\?/.test(url) ? '&' : '?') + s
|
|
}
|
|
|
|
function handleJsonp(o, fn, err, url) {
|
|
var reqId = uniqid++
|
|
, cbkey = o.jsonpCallback || 'callback' // the 'callback' key
|
|
, cbval = o.jsonpCallbackName || reqwest.getcallbackPrefix(reqId)
|
|
// , cbval = o.jsonpCallbackName || ('reqwest_' + reqId) // the 'callback' value
|
|
, cbreg = new RegExp('((^|\\?|&)' + cbkey + ')=([^&]+)')
|
|
, match = url.match(cbreg)
|
|
, script = doc.createElement('script')
|
|
, loaded = 0
|
|
, isIE10 = navigator.userAgent.indexOf('MSIE 10.0') !== -1
|
|
|
|
if (match) {
|
|
if (match[3] === '?') {
|
|
url = url.replace(cbreg, '$1=' + cbval) // wildcard callback func name
|
|
} else {
|
|
cbval = match[3] // provided callback func name
|
|
}
|
|
} else {
|
|
url = urlappend(url, cbkey + '=' + cbval) // no callback details, add 'em
|
|
}
|
|
|
|
win[cbval] = generalCallback
|
|
|
|
script.type = 'text/javascript'
|
|
script.src = url
|
|
script.async = true
|
|
if (typeof script.onreadystatechange !== 'undefined' && !isIE10) {
|
|
// need this for IE due to out-of-order onreadystatechange(), binding script
|
|
// execution to an event listener gives us control over when the script
|
|
// is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
|
|
//
|
|
// if this hack is used in IE10 jsonp callback are never called
|
|
script.event = 'onclick'
|
|
script.htmlFor = script.id = '_reqwest_' + reqId
|
|
}
|
|
|
|
script.onload = script.onreadystatechange = function () {
|
|
if ((script[readyState] && script[readyState] !== 'complete' && script[readyState] !== 'loaded') || loaded) {
|
|
return false;
|
|
}
|
|
script.onload = script.onreadystatechange = null
|
|
script.onclick && script.onclick()
|
|
// Call the user callback with the last value stored and clean up values and scripts.
|
|
o.success && o.success(lastValue)
|
|
lastValue = undefined
|
|
head.removeChild(script)
|
|
loaded = 1
|
|
}
|
|
|
|
// Add the script to the DOM head
|
|
head.appendChild(script)
|
|
}
|
|
|
|
function getRequest(o, fn, err) {
|
|
var method = (o.method || 'GET').toUpperCase()
|
|
, url = typeof o === 'string' ? o : o.url
|
|
// convert non-string objects to query-string form unless o.processData is false
|
|
, data = (o.processData !== false && o.data && typeof o.data !== 'string')
|
|
? reqwest.toQueryString(o.data)
|
|
: (o.data || null)
|
|
, http
|
|
|
|
// if we're working on a GET request and we have data then we should append
|
|
// query string to end of URL and not post data
|
|
if ((o.type == 'jsonp' || method == 'GET') && data) {
|
|
url = urlappend(url, data)
|
|
data = null
|
|
}
|
|
|
|
if (o.type == 'jsonp') return handleJsonp(o, fn, err, url)
|
|
|
|
http = xhr()
|
|
http.open(method, url, true)
|
|
setHeaders(http, o)
|
|
setCredentials(http, o)
|
|
http.onreadystatechange = handleReadyState(http, fn, err)
|
|
o.before && o.before(http)
|
|
http.send(data)
|
|
return http
|
|
}
|
|
|
|
function Reqwest(o, fn) {
|
|
this.o = o
|
|
this.fn = fn
|
|
|
|
init.apply(this, arguments)
|
|
}
|
|
|
|
function setType(url) {
|
|
var m = url.match(/\.(json|jsonp|html|xml)(\?|$)/)
|
|
return m ? m[1] : 'js'
|
|
}
|
|
|
|
function init(o, fn) {
|
|
|
|
this.url = typeof o == 'string' ? o : o.url
|
|
this.timeout = null
|
|
|
|
// whether request has been fulfilled for purpose
|
|
// of tracking the Promises
|
|
this._fulfilled = false
|
|
// success handlers
|
|
this._fulfillmentHandlers = []
|
|
// error handlers
|
|
this._errorHandlers = []
|
|
// complete (both success and fail) handlers
|
|
this._completeHandlers = []
|
|
this._erred = false
|
|
this._responseArgs = {}
|
|
|
|
var self = this
|
|
, type = o.type || setType(this.url)
|
|
|
|
fn = fn || function () {}
|
|
|
|
if (o.timeout) {
|
|
this.timeout = setTimeout(function () {
|
|
self.abort()
|
|
}, o.timeout)
|
|
}
|
|
|
|
if (o.success) {
|
|
this._fulfillmentHandlers.push(function () {
|
|
o.success.apply(o, arguments)
|
|
})
|
|
}
|
|
|
|
if (o.error) {
|
|
this._errorHandlers.push(function () {
|
|
o.error.apply(o, arguments)
|
|
})
|
|
}
|
|
|
|
if (o.complete) {
|
|
this._completeHandlers.push(function () {
|
|
o.complete.apply(o, arguments)
|
|
})
|
|
}
|
|
|
|
function complete(resp) {
|
|
o.timeout && clearTimeout(self.timeout)
|
|
self.timeout = null
|
|
while (self._completeHandlers.length > 0) {
|
|
self._completeHandlers.shift()(resp)
|
|
}
|
|
}
|
|
|
|
function success(resp) {
|
|
var r = resp.responseText
|
|
if (r) {
|
|
switch (type) {
|
|
case 'json':
|
|
try {
|
|
resp = win.JSON ? win.JSON.parse(r) : eval('(' + r + ')')
|
|
} catch (err) {
|
|
return error(resp, 'Could not parse JSON in response', err)
|
|
}
|
|
break;
|
|
case 'js':
|
|
resp = eval(r)
|
|
break;
|
|
case 'html':
|
|
resp = r
|
|
break;
|
|
case 'xml':
|
|
resp = resp.responseXML;
|
|
break;
|
|
}
|
|
}
|
|
|
|
self._responseArgs.resp = resp
|
|
self._fulfilled = true
|
|
fn(resp)
|
|
while (self._fulfillmentHandlers.length > 0) {
|
|
self._fulfillmentHandlers.shift()(resp)
|
|
}
|
|
|
|
complete(resp)
|
|
}
|
|
|
|
function error(resp, msg, t) {
|
|
self._responseArgs.resp = resp
|
|
self._responseArgs.msg = msg
|
|
self._responseArgs.t = t
|
|
self._erred = true
|
|
while (self._errorHandlers.length > 0) {
|
|
self._errorHandlers.shift()(resp, msg, t)
|
|
}
|
|
complete(resp)
|
|
}
|
|
|
|
this.request = getRequest(o, success, error)
|
|
}
|
|
|
|
Reqwest.prototype = {
|
|
abort: function () {
|
|
this.request.abort()
|
|
}
|
|
|
|
, retry: function () {
|
|
init.call(this, this.o, this.fn)
|
|
}
|
|
|
|
/**
|
|
* Small deviation from the Promises A CommonJs specification
|
|
* http://wiki.commonjs.org/wiki/Promises/A
|
|
*/
|
|
|
|
/**
|
|
* `then` will execute upon successful requests
|
|
*/
|
|
, then: function (success, fail) {
|
|
if (this._fulfilled) {
|
|
success(this._responseArgs.resp)
|
|
} else if (this._erred) {
|
|
fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
|
|
} else {
|
|
this._fulfillmentHandlers.push(success)
|
|
this._errorHandlers.push(fail)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* `always` will execute whether the request succeeds or fails
|
|
*/
|
|
, always: function (fn) {
|
|
if (this._fulfilled || this._erred) {
|
|
fn(this._responseArgs.resp)
|
|
} else {
|
|
this._completeHandlers.push(fn)
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* `fail` will execute when the request fails
|
|
*/
|
|
, fail: function (fn) {
|
|
if (this._erred) {
|
|
fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
|
|
} else {
|
|
this._errorHandlers.push(fn)
|
|
}
|
|
return this
|
|
}
|
|
}
|
|
|
|
function reqwest(o, fn) {
|
|
return new Reqwest(o, fn)
|
|
}
|
|
|
|
// normalize newline variants according to spec -> CRLF
|
|
function normalize(s) {
|
|
return s ? s.replace(/\r?\n/g, '\r\n') : ''
|
|
}
|
|
|
|
function serial(el, cb) {
|
|
var n = el.name
|
|
, t = el.tagName.toLowerCase()
|
|
, optCb = function (o) {
|
|
// IE gives value="" even where there is no value attribute
|
|
// 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273
|
|
if (o && !o.disabled)
|
|
cb(n, normalize(o.attributes.value && o.attributes.value.specified ? o.value : o.text))
|
|
}
|
|
|
|
// don't serialize elements that are disabled or without a name
|
|
if (el.disabled || !n) return;
|
|
|
|
switch (t) {
|
|
case 'input':
|
|
if (!/reset|button|image|file/i.test(el.type)) {
|
|
var ch = /checkbox/i.test(el.type)
|
|
, ra = /radio/i.test(el.type)
|
|
, val = el.value;
|
|
// WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here
|
|
(!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val))
|
|
}
|
|
break;
|
|
case 'textarea':
|
|
cb(n, normalize(el.value))
|
|
break;
|
|
case 'select':
|
|
if (el.type.toLowerCase() === 'select-one') {
|
|
optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null)
|
|
} else {
|
|
for (var i = 0; el.length && i < el.length; i++) {
|
|
el.options[i].selected && optCb(el.options[i])
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// collect up all form elements found from the passed argument elements all
|
|
// the way down to child elements; pass a '<form>' or form fields.
|
|
// called with 'this'=callback to use for serial() on each element
|
|
function eachFormElement() {
|
|
var cb = this
|
|
, e, i, j
|
|
, serializeSubtags = function (e, tags) {
|
|
for (var i = 0; i < tags.length; i++) {
|
|
var fa = e[byTag](tags[i])
|
|
for (j = 0; j < fa.length; j++) serial(fa[j], cb)
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < arguments.length; i++) {
|
|
e = arguments[i]
|
|
if (/input|select|textarea/i.test(e.tagName)) serial(e, cb)
|
|
serializeSubtags(e, [ 'input', 'select', 'textarea' ])
|
|
}
|
|
}
|
|
|
|
// standard query string style serialization
|
|
function serializeQueryString() {
|
|
return reqwest.toQueryString(reqwest.serializeArray.apply(null, arguments))
|
|
}
|
|
|
|
// { 'name': 'value', ... } style serialization
|
|
function serializeHash() {
|
|
var hash = {}
|
|
eachFormElement.apply(function (name, value) {
|
|
if (name in hash) {
|
|
hash[name] && !isArray(hash[name]) && (hash[name] = [hash[name]])
|
|
hash[name].push(value)
|
|
} else hash[name] = value
|
|
}, arguments)
|
|
return hash
|
|
}
|
|
|
|
// [ { name: 'name', value: 'value' }, ... ] style serialization
|
|
reqwest.serializeArray = function () {
|
|
var arr = []
|
|
eachFormElement.apply(function (name, value) {
|
|
arr.push({name: name, value: value})
|
|
}, arguments)
|
|
return arr
|
|
}
|
|
|
|
reqwest.serialize = function () {
|
|
if (arguments.length === 0) return ''
|
|
var opt, fn
|
|
, args = Array.prototype.slice.call(arguments, 0)
|
|
|
|
opt = args.pop()
|
|
opt && opt.nodeType && args.push(opt) && (opt = null)
|
|
opt && (opt = opt.type)
|
|
|
|
if (opt == 'map') fn = serializeHash
|
|
else if (opt == 'array') fn = reqwest.serializeArray
|
|
else fn = serializeQueryString
|
|
|
|
return fn.apply(null, args)
|
|
}
|
|
|
|
reqwest.toQueryString = function (o) {
|
|
var qs = '', i
|
|
, enc = encodeURIComponent
|
|
, push = function (k, v) {
|
|
qs += enc(k) + '=' + enc(v) + '&'
|
|
}
|
|
|
|
if (isArray(o)) {
|
|
for (i = 0; o && i < o.length; i++) push(o[i].name, o[i].value)
|
|
} else {
|
|
for (var k in o) {
|
|
if (!Object.hasOwnProperty.call(o, k)) continue;
|
|
var v = o[k]
|
|
if (isArray(v)) {
|
|
for (i = 0; i < v.length; i++) push(k, v[i])
|
|
} else push(k, o[k])
|
|
}
|
|
}
|
|
|
|
// spaces should be + according to spec
|
|
return qs.replace(/&$/, '').replace(/%20/g, '+')
|
|
}
|
|
|
|
reqwest.getcallbackPrefix = function (reqId) {
|
|
return callbackPrefix
|
|
}
|
|
|
|
// jQuery and Zepto compatibility, differences can be remapped here so you can call
|
|
// .ajax.compat(options, callback)
|
|
reqwest.compat = function (o, fn) {
|
|
if (o) {
|
|
o.type && (o.method = o.type) && delete o.type
|
|
o.dataType && (o.type = o.dataType)
|
|
o.jsonpCallback && (o.jsonpCallbackName = o.jsonpCallback) && delete o.jsonpCallback
|
|
o.jsonp && (o.jsonpCallback = o.jsonp)
|
|
}
|
|
return new Reqwest(o, fn)
|
|
}
|
|
|
|
return reqwest
|
|
});
|
|
}()
|
|
/*
|
|
* CommonJS-compatible mustache.js module
|
|
*
|
|
* See http://github.com/janl/mustache.js for more info.
|
|
*/
|
|
|
|
/*
|
|
mustache.js — Logic-less templates in JavaScript
|
|
|
|
See http://mustache.github.com/ for more info.
|
|
*/
|
|
|
|
var Mustache = function () {
|
|
var _toString = Object.prototype.toString;
|
|
|
|
Array.isArray = Array.isArray || function (obj) {
|
|
return _toString.call(obj) == "[object Array]";
|
|
}
|
|
|
|
var _trim = String.prototype.trim, trim;
|
|
|
|
if (_trim) {
|
|
trim = function (text) {
|
|
return text == null ? "" : _trim.call(text);
|
|
}
|
|
} else {
|
|
var trimLeft, trimRight;
|
|
|
|
// IE doesn't match non-breaking spaces with \s.
|
|
if ((/\S/).test("\xA0")) {
|
|
trimLeft = /^[\s\xA0]+/;
|
|
trimRight = /[\s\xA0]+$/;
|
|
} else {
|
|
trimLeft = /^\s+/;
|
|
trimRight = /\s+$/;
|
|
}
|
|
|
|
trim = function (text) {
|
|
return text == null ? "" :
|
|
text.toString().replace(trimLeft, "").replace(trimRight, "");
|
|
}
|
|
}
|
|
|
|
var escapeMap = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
|
|
function escapeHTML(string) {
|
|
return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) {
|
|
return escapeMap[s] || s;
|
|
});
|
|
}
|
|
|
|
var regexCache = {};
|
|
var Renderer = function () {};
|
|
|
|
Renderer.prototype = {
|
|
otag: "{{",
|
|
ctag: "}}",
|
|
pragmas: {},
|
|
buffer: [],
|
|
pragmas_implemented: {
|
|
"IMPLICIT-ITERATOR": true
|
|
},
|
|
context: {},
|
|
|
|
render: function (template, context, partials, in_recursion) {
|
|
// reset buffer & set context
|
|
if (!in_recursion) {
|
|
this.context = context;
|
|
this.buffer = []; // TODO: make this non-lazy
|
|
}
|
|
|
|
// fail fast
|
|
if (!this.includes("", template)) {
|
|
if (in_recursion) {
|
|
return template;
|
|
} else {
|
|
this.send(template);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// get the pragmas together
|
|
template = this.render_pragmas(template);
|
|
|
|
// render the template
|
|
var html = this.render_section(template, context, partials);
|
|
|
|
// render_section did not find any sections, we still need to render the tags
|
|
if (html === false) {
|
|
html = this.render_tags(template, context, partials, in_recursion);
|
|
}
|
|
|
|
if (in_recursion) {
|
|
return html;
|
|
} else {
|
|
this.sendLines(html);
|
|
}
|
|
},
|
|
|
|
/*
|
|
Sends parsed lines
|
|
*/
|
|
send: function (line) {
|
|
if (line !== "") {
|
|
this.buffer.push(line);
|
|
}
|
|
},
|
|
|
|
sendLines: function (text) {
|
|
if (text) {
|
|
var lines = text.split("\n");
|
|
for (var i = 0; i < lines.length; i++) {
|
|
this.send(lines[i]);
|
|
}
|
|
}
|
|
},
|
|
|
|
/*
|
|
Looks for %PRAGMAS
|
|
*/
|
|
render_pragmas: function (template) {
|
|
// no pragmas
|
|
if (!this.includes("%", template)) {
|
|
return template;
|
|
}
|
|
|
|
var that = this;
|
|
var regex = this.getCachedRegex("render_pragmas", function (otag, ctag) {
|
|
return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g");
|
|
});
|
|
|
|
return template.replace(regex, function (match, pragma, options) {
|
|
if (!that.pragmas_implemented[pragma]) {
|
|
throw({message:
|
|
"This implementation of mustache doesn't understand the '" +
|
|
pragma + "' pragma"});
|
|
}
|
|
that.pragmas[pragma] = {};
|
|
if (options) {
|
|
var opts = options.split("=");
|
|
that.pragmas[pragma][opts[0]] = opts[1];
|
|
}
|
|
return "";
|
|
// ignore unknown pragmas silently
|
|
});
|
|
},
|
|
|
|
/*
|
|
Tries to find a partial in the curent scope and render it
|
|
*/
|
|
render_partial: function (name, context, partials) {
|
|
name = trim(name);
|
|
if (!partials || partials[name] === undefined) {
|
|
throw({message: "unknown_partial '" + name + "'"});
|
|
}
|
|
if (!context || typeof context[name] != "object") {
|
|
return this.render(partials[name], context, partials, true);
|
|
}
|
|
return this.render(partials[name], context[name], partials, true);
|
|
},
|
|
|
|
/*
|
|
Renders inverted (^) and normal (#) sections
|
|
*/
|
|
render_section: function (template, context, partials) {
|
|
if (!this.includes("#", template) && !this.includes("^", template)) {
|
|
// did not render anything, there were no sections
|
|
return false;
|
|
}
|
|
|
|
var that = this;
|
|
|
|
var regex = this.getCachedRegex("render_section", function (otag, ctag) {
|
|
// This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder
|
|
return new RegExp(
|
|
"^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1)
|
|
|
|
otag + // {{
|
|
"(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3)
|
|
ctag + // }}
|
|
|
|
"\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped
|
|
|
|
otag + // {{
|
|
"\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag).
|
|
ctag + // }}
|
|
|
|
"\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped.
|
|
|
|
"g");
|
|
});
|
|
|
|
|
|
// for each {{#foo}}{{/foo}} section do...
|
|
return template.replace(regex, function (match, before, type, name, content, after) {
|
|
// before contains only tags, no sections
|
|
var renderedBefore = before ? that.render_tags(before, context, partials, true) : "",
|
|
|
|
// after may contain both sections and tags, so use full rendering function
|
|
renderedAfter = after ? that.render(after, context, partials, true) : "",
|
|
|
|
// will be computed below
|
|
renderedContent,
|
|
|
|
value = that.find(name, context);
|
|
|
|
if (type === "^") { // inverted section
|
|
if (!value || Array.isArray(value) && value.length === 0) {
|
|
// false or empty list, render it
|
|
renderedContent = that.render(content, context, partials, true);
|
|
} else {
|
|
renderedContent = "";
|
|
}
|
|
} else if (type === "#") { // normal section
|
|
if (Array.isArray(value)) { // Enumerable, Let's loop!
|
|
renderedContent = that.map(value, function (row) {
|
|
return that.render(content, that.create_context(row), partials, true);
|
|
}).join("");
|
|
} else if (that.is_object(value)) { // Object, Use it as subcontext!
|
|
renderedContent = that.render(content, that.create_context(value),
|
|
partials, true);
|
|
} else if (typeof value == "function") {
|
|
// higher order section
|
|
renderedContent = value.call(context, content, function (text) {
|
|
return that.render(text, context, partials, true);
|
|
});
|
|
} else if (value) { // boolean section
|
|
renderedContent = that.render(content, context, partials, true);
|
|
} else {
|
|
renderedContent = "";
|
|
}
|
|
}
|
|
|
|
return renderedBefore + renderedContent + renderedAfter;
|
|
});
|
|
},
|
|
|
|
/*
|
|
Replace {{foo}} and friends with values from our view
|
|
*/
|
|
render_tags: function (template, context, partials, in_recursion) {
|
|
// tit for tat
|
|
var that = this;
|
|
|
|
var new_regex = function () {
|
|
return that.getCachedRegex("render_tags", function (otag, ctag) {
|
|
return new RegExp(otag + "(=|!|>|&|\\{|%)?([^#\\^]+?)\\1?" + ctag + "+", "g");
|
|
});
|
|
};
|
|
|
|
var regex = new_regex();
|
|
var tag_replace_callback = function (match, operator, name) {
|
|
switch(operator) {
|
|
case "!": // ignore comments
|
|
return "";
|
|
case "=": // set new delimiters, rebuild the replace regexp
|
|
that.set_delimiters(name);
|
|
regex = new_regex();
|
|
return "";
|
|
case ">": // render partial
|
|
return that.render_partial(name, context, partials);
|
|
case "{": // the triple mustache is unescaped
|
|
case "&": // & operator is an alternative unescape method
|
|
return that.find(name, context);
|
|
default: // escape the value
|
|
return escapeHTML(that.find(name, context));
|
|
}
|
|
};
|
|
var lines = template.split("\n");
|
|
for(var i = 0; i < lines.length; i++) {
|
|
lines[i] = lines[i].replace(regex, tag_replace_callback, this);
|
|
if (!in_recursion) {
|
|
this.send(lines[i]);
|
|
}
|
|
}
|
|
|
|
if (in_recursion) {
|
|
return lines.join("\n");
|
|
}
|
|
},
|
|
|
|
set_delimiters: function (delimiters) {
|
|
var dels = delimiters.split(" ");
|
|
this.otag = this.escape_regex(dels[0]);
|
|
this.ctag = this.escape_regex(dels[1]);
|
|
},
|
|
|
|
escape_regex: function (text) {
|
|
// thank you Simon Willison
|
|
if (!arguments.callee.sRE) {
|
|
var specials = [
|
|
'/', '.', '*', '+', '?', '|',
|
|
'(', ')', '[', ']', '{', '}', '\\'
|
|
];
|
|
arguments.callee.sRE = new RegExp(
|
|
'(\\' + specials.join('|\\') + ')', 'g'
|
|
);
|
|
}
|
|
return text.replace(arguments.callee.sRE, '\\$1');
|
|
},
|
|
|
|
/*
|
|
find `name` in current `context`. That is find me a value
|
|
from the view object
|
|
*/
|
|
find: function (name, context) {
|
|
name = trim(name);
|
|
|
|
// Checks whether a value is thruthy or false or 0
|
|
function is_kinda_truthy(bool) {
|
|
return bool === false || bool === 0 || bool;
|
|
}
|
|
|
|
var value;
|
|
|
|
// check for dot notation eg. foo.bar
|
|
if (name.match(/([a-z_]+)\./ig)) {
|
|
var childValue = this.walk_context(name, context);
|
|
if (is_kinda_truthy(childValue)) {
|
|
value = childValue;
|
|
}
|
|
} else {
|
|
if (is_kinda_truthy(context[name])) {
|
|
value = context[name];
|
|
} else if (is_kinda_truthy(this.context[name])) {
|
|
value = this.context[name];
|
|
}
|
|
}
|
|
|
|
if (typeof value == "function") {
|
|
return value.apply(context);
|
|
}
|
|
if (value !== undefined) {
|
|
return value;
|
|
}
|
|
// silently ignore unkown variables
|
|
return "";
|
|
},
|
|
|
|
walk_context: function (name, context) {
|
|
var path = name.split('.');
|
|
// if the var doesn't exist in current context, check the top level context
|
|
var value_context = (context[path[0]] != undefined) ? context : this.context;
|
|
var value = value_context[path.shift()];
|
|
while (value != undefined && path.length > 0) {
|
|
value_context = value;
|
|
value = value[path.shift()];
|
|
}
|
|
// if the value is a function, call it, binding the correct context
|
|
if (typeof value == "function") {
|
|
return value.apply(value_context);
|
|
}
|
|
return value;
|
|
},
|
|
|
|
// Utility methods
|
|
|
|
/* includes tag */
|
|
includes: function (needle, haystack) {
|
|
return haystack.indexOf(this.otag + needle) != -1;
|
|
},
|
|
|
|
// by @langalex, support for arrays of strings
|
|
create_context: function (_context) {
|
|
if (this.is_object(_context)) {
|
|
return _context;
|
|
} else {
|
|
var iterator = ".";
|
|
if (this.pragmas["IMPLICIT-ITERATOR"]) {
|
|
iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
|
|
}
|
|
var ctx = {};
|
|
ctx[iterator] = _context;
|
|
return ctx;
|
|
}
|
|
},
|
|
|
|
is_object: function (a) {
|
|
return a && typeof a == "object";
|
|
},
|
|
|
|
/*
|
|
Why, why, why? Because IE. Cry, cry cry.
|
|
*/
|
|
map: function (array, fn) {
|
|
if (typeof array.map == "function") {
|
|
return array.map(fn);
|
|
} else {
|
|
var r = [];
|
|
var l = array.length;
|
|
for(var i = 0; i < l; i++) {
|
|
r.push(fn(array[i]));
|
|
}
|
|
return r;
|
|
}
|
|
},
|
|
|
|
getCachedRegex: function (name, generator) {
|
|
var byOtag = regexCache[this.otag];
|
|
if (!byOtag) {
|
|
byOtag = regexCache[this.otag] = {};
|
|
}
|
|
|
|
var byCtag = byOtag[this.ctag];
|
|
if (!byCtag) {
|
|
byCtag = byOtag[this.ctag] = {};
|
|
}
|
|
|
|
var regex = byCtag[name];
|
|
if (!regex) {
|
|
regex = byCtag[name] = generator(this.otag, this.ctag);
|
|
}
|
|
|
|
return regex;
|
|
}
|
|
};
|
|
|
|
return({
|
|
name: "mustache.js",
|
|
version: "0.4.0",
|
|
|
|
/*
|
|
Turns a template and view into HTML
|
|
*/
|
|
to_html: function (template, view, partials, send_fun) {
|
|
var renderer = new Renderer();
|
|
if (send_fun) {
|
|
renderer.send = send_fun;
|
|
}
|
|
renderer.render(template, view || {}, partials);
|
|
if (!send_fun) {
|
|
return renderer.buffer.join("\n");
|
|
}
|
|
}
|
|
});
|
|
}();
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
exports.name = Mustache.name;
|
|
exports.version = Mustache.version;
|
|
|
|
exports.to_html = function() {
|
|
return Mustache.to_html.apply(this, arguments);
|
|
};
|
|
}
|
|
/*!
|
|
* Modest Maps JS v3.3.5
|
|
* http://modestmaps.com/
|
|
*
|
|
* Copyright (c) 2011 Stamen Design, All Rights Reserved.
|
|
*
|
|
* Open source under the BSD License.
|
|
* http://creativecommons.org/licenses/BSD/
|
|
*
|
|
* Versioned using Semantic Versioning (v.major.minor.patch)
|
|
* See CHANGELOG and http://semver.org/ for more details.
|
|
*
|
|
*/
|
|
|
|
var previousMM = MM;
|
|
|
|
// namespacing for backwards-compatibility
|
|
if (!com) {
|
|
var com = {};
|
|
if (!com.modestmaps) com.modestmaps = {};
|
|
}
|
|
|
|
var MM = com.modestmaps = {
|
|
noConflict: function() {
|
|
MM = previousMM;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
(function(MM) {
|
|
// Make inheritance bearable: clone one level of properties
|
|
MM.extend = function(child, parent) {
|
|
for (var property in parent.prototype) {
|
|
if (typeof child.prototype[property] == "undefined") {
|
|
child.prototype[property] = parent.prototype[property];
|
|
}
|
|
}
|
|
return child;
|
|
};
|
|
|
|
MM.getFrame = function () {
|
|
// native animation frames
|
|
// http://webstuff.nfshost.com/anim-timing/Overview.html
|
|
// http://dev.chromium.org/developers/design-documents/requestanimationframe-implementation
|
|
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
|
|
// can't apply these directly to MM because Chrome needs window
|
|
// to own webkitRequestAnimationFrame (for example)
|
|
// perhaps we should namespace an alias onto window instead?
|
|
// e.g. window.mmRequestAnimationFrame?
|
|
return function(callback) {
|
|
(window.requestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame ||
|
|
window.mozRequestAnimationFrame ||
|
|
window.oRequestAnimationFrame ||
|
|
window.msRequestAnimationFrame ||
|
|
function (callback) {
|
|
window.setTimeout(function () {
|
|
callback(+new Date());
|
|
}, 10);
|
|
})(callback);
|
|
};
|
|
}();
|
|
|
|
// Inspired by LeafletJS
|
|
MM.transformProperty = (function(props) {
|
|
if (!this.document) return; // node.js safety
|
|
var style = document.documentElement.style;
|
|
for (var i = 0; i < props.length; i++) {
|
|
if (props[i] in style) {
|
|
return props[i];
|
|
}
|
|
}
|
|
return false;
|
|
})(['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']);
|
|
|
|
MM.matrixString = function(point) {
|
|
// Make the result of point.scale * point.width a whole number.
|
|
if (point.scale * point.width % 1) {
|
|
point.scale += (1 - point.scale * point.width % 1) / point.width;
|
|
}
|
|
|
|
var scale = point.scale || 1;
|
|
if (MM._browser.webkit3d) {
|
|
return 'translate3d(' +
|
|
point.x.toFixed(0) + 'px,' + point.y.toFixed(0) + 'px, 0px)' +
|
|
'scale3d(' + scale + ',' + scale + ', 1)';
|
|
} else {
|
|
return 'translate(' +
|
|
point.x.toFixed(6) + 'px,' + point.y.toFixed(6) + 'px)' +
|
|
'scale(' + scale + ',' + scale + ')';
|
|
}
|
|
};
|
|
|
|
MM._browser = (function(window) {
|
|
return {
|
|
webkit: ('WebKitCSSMatrix' in window),
|
|
webkit3d: ('WebKitCSSMatrix' in window) && ('m11' in new WebKitCSSMatrix())
|
|
};
|
|
})(this); // use this for node.js global
|
|
|
|
MM.moveElement = function(el, point) {
|
|
if (MM.transformProperty) {
|
|
// Optimize for identity transforms, where you don't actually
|
|
// need to change this element's string. Browsers can optimize for
|
|
// the .style.left case but not for this CSS case.
|
|
if (!point.scale) point.scale = 1;
|
|
if (!point.width) point.width = 0;
|
|
if (!point.height) point.height = 0;
|
|
var ms = MM.matrixString(point);
|
|
if (el[MM.transformProperty] !== ms) {
|
|
el.style[MM.transformProperty] =
|
|
el[MM.transformProperty] = ms;
|
|
}
|
|
} else {
|
|
el.style.left = point.x + 'px';
|
|
el.style.top = point.y + 'px';
|
|
// Don't set width unless asked to: this is performance-intensive
|
|
// and not always necessary
|
|
if (point.width && point.height && point.scale) {
|
|
el.style.width = Math.ceil(point.width * point.scale) + 'px';
|
|
el.style.height = Math.ceil(point.height * point.scale) + 'px';
|
|
}
|
|
}
|
|
};
|
|
|
|
// Events
|
|
// Cancel an event: prevent it from bubbling
|
|
MM.cancelEvent = function(e) {
|
|
// there's more than one way to skin this cat
|
|
e.cancelBubble = true;
|
|
e.cancel = true;
|
|
e.returnValue = false;
|
|
if (e.stopPropagation) { e.stopPropagation(); }
|
|
if (e.preventDefault) { e.preventDefault(); }
|
|
return false;
|
|
};
|
|
|
|
MM.coerceLayer = function(layerish) {
|
|
if (typeof layerish == 'string') {
|
|
// Probably a template string
|
|
return new MM.Layer(new MM.TemplatedLayer(layerish));
|
|
} else if ('draw' in layerish && typeof layerish.draw == 'function') {
|
|
// good enough, though we should probably enforce .parent and .destroy() too
|
|
return layerish;
|
|
} else {
|
|
// probably a MapProvider
|
|
return new MM.Layer(layerish);
|
|
}
|
|
};
|
|
|
|
// see http://ejohn.org/apps/jselect/event.html for the originals
|
|
MM.addEvent = function(obj, type, fn) {
|
|
if (obj.addEventListener) {
|
|
obj.addEventListener(type, fn, false);
|
|
if (type == 'mousewheel') {
|
|
obj.addEventListener('DOMMouseScroll', fn, false);
|
|
}
|
|
} else if (obj.attachEvent) {
|
|
obj['e'+type+fn] = fn;
|
|
obj[type+fn] = function(){ obj['e'+type+fn](window.event); };
|
|
obj.attachEvent('on'+type, obj[type+fn]);
|
|
}
|
|
};
|
|
|
|
MM.removeEvent = function( obj, type, fn ) {
|
|
if (obj.removeEventListener) {
|
|
obj.removeEventListener(type, fn, false);
|
|
if (type == 'mousewheel') {
|
|
obj.removeEventListener('DOMMouseScroll', fn, false);
|
|
}
|
|
} else if (obj.detachEvent) {
|
|
obj.detachEvent('on'+type, obj[type+fn]);
|
|
obj[type+fn] = null;
|
|
}
|
|
};
|
|
|
|
// Cross-browser function to get current element style property
|
|
MM.getStyle = function(el,styleProp) {
|
|
if (el.currentStyle)
|
|
return el.currentStyle[styleProp];
|
|
else if (window.getComputedStyle)
|
|
return document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp);
|
|
};
|
|
// Point
|
|
MM.Point = function(x, y) {
|
|
this.x = parseFloat(x);
|
|
this.y = parseFloat(y);
|
|
};
|
|
|
|
MM.Point.prototype = {
|
|
x: 0,
|
|
y: 0,
|
|
toString: function() {
|
|
return "(" + this.x.toFixed(3) + ", " + this.y.toFixed(3) + ")";
|
|
},
|
|
copy: function() {
|
|
return new MM.Point(this.x, this.y);
|
|
}
|
|
};
|
|
|
|
// Get the euclidean distance between two points
|
|
MM.Point.distance = function(p1, p2) {
|
|
return Math.sqrt(
|
|
Math.pow(p2.x - p1.x, 2) +
|
|
Math.pow(p2.y - p1.y, 2));
|
|
};
|
|
|
|
// Get a point between two other points, biased by `t`.
|
|
MM.Point.interpolate = function(p1, p2, t) {
|
|
return new MM.Point(
|
|
p1.x + (p2.x - p1.x) * t,
|
|
p1.y + (p2.y - p1.y) * t);
|
|
};
|
|
// Coordinate
|
|
// ----------
|
|
// An object representing a tile position, at as specified zoom level.
|
|
// This is not necessarily a precise tile - `row`, `column`, and
|
|
// `zoom` can be floating-point numbers, and the `container()` function
|
|
// can be used to find the actual tile that contains the point.
|
|
MM.Coordinate = function(row, column, zoom) {
|
|
this.row = row;
|
|
this.column = column;
|
|
this.zoom = zoom;
|
|
};
|
|
|
|
MM.Coordinate.prototype = {
|
|
|
|
row: 0,
|
|
column: 0,
|
|
zoom: 0,
|
|
|
|
toString: function() {
|
|
return "(" + this.row.toFixed(3) +
|
|
", " + this.column.toFixed(3) +
|
|
" @" + this.zoom.toFixed(3) + ")";
|
|
},
|
|
// Quickly generate a string representation of this coordinate to
|
|
// index it in hashes.
|
|
toKey: function() {
|
|
// We've tried to use efficient hash functions here before but we took
|
|
// them out. Contributions welcome but watch out for collisions when the
|
|
// row or column are negative and check thoroughly (exhaustively) before
|
|
// committing.
|
|
return this.zoom + ',' + this.row + ',' + this.column;
|
|
},
|
|
// Clone this object.
|
|
copy: function() {
|
|
return new MM.Coordinate(this.row, this.column, this.zoom);
|
|
},
|
|
// Get the actual, rounded-number tile that contains this point.
|
|
container: function() {
|
|
// using floor here (not parseInt, ~~) because we want -0.56 --> -1
|
|
return new MM.Coordinate(Math.floor(this.row),
|
|
Math.floor(this.column),
|
|
Math.floor(this.zoom));
|
|
},
|
|
// Recalculate this Coordinate at a different zoom level and return the
|
|
// new object.
|
|
zoomTo: function(destination) {
|
|
var power = Math.pow(2, destination - this.zoom);
|
|
return new MM.Coordinate(this.row * power,
|
|
this.column * power,
|
|
destination);
|
|
},
|
|
// Recalculate this Coordinate at a different relative zoom level and return the
|
|
// new object.
|
|
zoomBy: function(distance) {
|
|
var power = Math.pow(2, distance);
|
|
return new MM.Coordinate(this.row * power,
|
|
this.column * power,
|
|
this.zoom + distance);
|
|
},
|
|
// Move this coordinate up by `dist` coordinates
|
|
up: function(dist) {
|
|
if (dist === undefined) dist = 1;
|
|
return new MM.Coordinate(this.row - dist, this.column, this.zoom);
|
|
},
|
|
// Move this coordinate right by `dist` coordinates
|
|
right: function(dist) {
|
|
if (dist === undefined) dist = 1;
|
|
return new MM.Coordinate(this.row, this.column + dist, this.zoom);
|
|
},
|
|
// Move this coordinate down by `dist` coordinates
|
|
down: function(dist) {
|
|
if (dist === undefined) dist = 1;
|
|
return new MM.Coordinate(this.row + dist, this.column, this.zoom);
|
|
},
|
|
// Move this coordinate left by `dist` coordinates
|
|
left: function(dist) {
|
|
if (dist === undefined) dist = 1;
|
|
return new MM.Coordinate(this.row, this.column - dist, this.zoom);
|
|
}
|
|
};
|
|
// Location
|
|
// --------
|
|
MM.Location = function(lat, lon) {
|
|
this.lat = parseFloat(lat);
|
|
this.lon = parseFloat(lon);
|
|
};
|
|
|
|
MM.Location.prototype = {
|
|
lat: 0,
|
|
lon: 0,
|
|
toString: function() {
|
|
return "(" + this.lat.toFixed(3) + ", " + this.lon.toFixed(3) + ")";
|
|
},
|
|
copy: function() {
|
|
return new MM.Location(this.lat, this.lon);
|
|
}
|
|
};
|
|
|
|
// returns approximate distance between start and end locations
|
|
//
|
|
// default unit is meters
|
|
//
|
|
// you can specify different units by optionally providing the
|
|
// earth's radius in the units you desire
|
|
//
|
|
// Default is 6,378,000 metres, suggested values are:
|
|
//
|
|
// * 3963.1 statute miles
|
|
// * 3443.9 nautical miles
|
|
// * 6378 km
|
|
//
|
|
// see [Formula and code for calculating distance based on two lat/lon locations](http://jan.ucc.nau.edu/~cvm/latlon_formula.html)
|
|
MM.Location.distance = function(l1, l2, r) {
|
|
if (!r) {
|
|
// default to meters
|
|
r = 6378000;
|
|
}
|
|
var deg2rad = Math.PI / 180.0,
|
|
a1 = l1.lat * deg2rad,
|
|
b1 = l1.lon * deg2rad,
|
|
a2 = l2.lat * deg2rad,
|
|
b2 = l2.lon * deg2rad,
|
|
c = Math.cos(a1) * Math.cos(b1) * Math.cos(a2) * Math.cos(b2),
|
|
d = Math.cos(a1) * Math.sin(b1) * Math.cos(a2) * Math.sin(b2),
|
|
e = Math.sin(a1) * Math.sin(a2);
|
|
return Math.acos(c + d + e) * r;
|
|
};
|
|
|
|
// Interpolates along a great circle, f between 0 and 1
|
|
//
|
|
// * FIXME: could be heavily optimized (lots of trig calls to cache)
|
|
// * FIXME: could be inmproved for calculating a full path
|
|
MM.Location.interpolate = function(l1, l2, f) {
|
|
if (l1.lat === l2.lat && l1.lon === l2.lon) {
|
|
return new MM.Location(l1.lat, l1.lon);
|
|
}
|
|
var deg2rad = Math.PI / 180.0,
|
|
lat1 = l1.lat * deg2rad,
|
|
lon1 = l1.lon * deg2rad,
|
|
lat2 = l2.lat * deg2rad,
|
|
lon2 = l2.lon * deg2rad;
|
|
|
|
var d = 2 * Math.asin(
|
|
Math.sqrt(
|
|
Math.pow(Math.sin((lat1 - lat2) / 2), 2) +
|
|
Math.cos(lat1) * Math.cos(lat2) *
|
|
Math.pow(Math.sin((lon1 - lon2) / 2), 2)));
|
|
|
|
var A = Math.sin((1-f)*d)/Math.sin(d);
|
|
var B = Math.sin(f*d)/Math.sin(d);
|
|
var x = A * Math.cos(lat1) * Math.cos(lon1) +
|
|
B * Math.cos(lat2) * Math.cos(lon2);
|
|
var y = A * Math.cos(lat1) * Math.sin(lon1) +
|
|
B * Math.cos(lat2) * Math.sin(lon2);
|
|
var z = A * Math.sin(lat1) + B * Math.sin(lat2);
|
|
|
|
var latN = Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)));
|
|
var lonN = Math.atan2(y,x);
|
|
|
|
return new MM.Location(latN / deg2rad, lonN / deg2rad);
|
|
};
|
|
|
|
// Returns bearing from one point to another
|
|
//
|
|
// * FIXME: bearing is not constant along significant great circle arcs.
|
|
MM.Location.bearing = function(l1, l2) {
|
|
var deg2rad = Math.PI / 180.0,
|
|
lat1 = l1.lat * deg2rad,
|
|
lon1 = l1.lon * deg2rad,
|
|
lat2 = l2.lat * deg2rad,
|
|
lon2 = l2.lon * deg2rad;
|
|
var result = Math.atan2(
|
|
Math.sin(lon1 - lon2) *
|
|
Math.cos(lat2),
|
|
Math.cos(lat1) *
|
|
Math.sin(lat2) -
|
|
Math.sin(lat1) *
|
|
Math.cos(lat2) *
|
|
Math.cos(lon1 - lon2)
|
|
) / -(Math.PI / 180);
|
|
|
|
// map it into 0-360 range
|
|
return (result < 0) ? result + 360 : result;
|
|
};
|
|
// Extent
|
|
// ----------
|
|
// An object representing a map's rectangular extent, defined by its north,
|
|
// south, east and west bounds.
|
|
|
|
MM.Extent = function(north, west, south, east) {
|
|
if (north instanceof MM.Location &&
|
|
west instanceof MM.Location) {
|
|
var northwest = north,
|
|
southeast = west;
|
|
|
|
north = northwest.lat;
|
|
west = northwest.lon;
|
|
south = southeast.lat;
|
|
east = southeast.lon;
|
|
}
|
|
if (isNaN(south)) south = north;
|
|
if (isNaN(east)) east = west;
|
|
this.north = Math.max(north, south);
|
|
this.south = Math.min(north, south);
|
|
this.east = Math.max(east, west);
|
|
this.west = Math.min(east, west);
|
|
};
|
|
|
|
MM.Extent.prototype = {
|
|
// boundary attributes
|
|
north: 0,
|
|
south: 0,
|
|
east: 0,
|
|
west: 0,
|
|
|
|
copy: function() {
|
|
return new MM.Extent(this.north, this.west, this.south, this.east);
|
|
},
|
|
|
|
toString: function(precision) {
|
|
if (isNaN(precision)) precision = 3;
|
|
return [
|
|
this.north.toFixed(precision),
|
|
this.west.toFixed(precision),
|
|
this.south.toFixed(precision),
|
|
this.east.toFixed(precision)
|
|
].join(", ");
|
|
},
|
|
|
|
// getters for the corner locations
|
|
northWest: function() {
|
|
return new MM.Location(this.north, this.west);
|
|
},
|
|
southEast: function() {
|
|
return new MM.Location(this.south, this.east);
|
|
},
|
|
northEast: function() {
|
|
return new MM.Location(this.north, this.east);
|
|
},
|
|
southWest: function() {
|
|
return new MM.Location(this.south, this.west);
|
|
},
|
|
// getter for the center location
|
|
center: function() {
|
|
return new MM.Location(
|
|
this.south + (this.north - this.south) / 2,
|
|
this.east + (this.west - this.east) / 2
|
|
);
|
|
},
|
|
|
|
// extend the bounds to include a location's latitude and longitude
|
|
encloseLocation: function(loc) {
|
|
if (loc.lat > this.north) this.north = loc.lat;
|
|
if (loc.lat < this.south) this.south = loc.lat;
|
|
if (loc.lon > this.east) this.east = loc.lon;
|
|
if (loc.lon < this.west) this.west = loc.lon;
|
|
},
|
|
|
|
// extend the bounds to include multiple locations
|
|
encloseLocations: function(locations) {
|
|
var len = locations.length;
|
|
for (var i = 0; i < len; i++) {
|
|
this.encloseLocation(locations[i]);
|
|
}
|
|
},
|
|
|
|
// reset bounds from a list of locations
|
|
setFromLocations: function(locations) {
|
|
var len = locations.length,
|
|
first = locations[0];
|
|
this.north = this.south = first.lat;
|
|
this.east = this.west = first.lon;
|
|
for (var i = 1; i < len; i++) {
|
|
this.encloseLocation(locations[i]);
|
|
}
|
|
},
|
|
|
|
// extend the bounds to include another extent
|
|
encloseExtent: function(extent) {
|
|
if (extent.north > this.north) this.north = extent.north;
|
|
if (extent.south < this.south) this.south = extent.south;
|
|
if (extent.east > this.east) this.east = extent.east;
|
|
if (extent.west < this.west) this.west = extent.west;
|
|
},
|
|
|
|
// determine if a location is within this extent
|
|
containsLocation: function(loc) {
|
|
return loc.lat >= this.south &&
|
|
loc.lat <= this.north &&
|
|
loc.lon >= this.west &&
|
|
loc.lon <= this.east;
|
|
},
|
|
|
|
// turn an extent into an array of locations containing its northwest
|
|
// and southeast corners (used in MM.Map.setExtent())
|
|
toArray: function() {
|
|
return [this.northWest(), this.southEast()];
|
|
}
|
|
};
|
|
|
|
MM.Extent.fromString = function(str) {
|
|
var parts = str.split(/\s*,\s*/);
|
|
if (parts.length != 4) {
|
|
throw "Invalid extent string (expecting 4 comma-separated numbers)";
|
|
}
|
|
return new MM.Extent(
|
|
parseFloat(parts[0]),
|
|
parseFloat(parts[1]),
|
|
parseFloat(parts[2]),
|
|
parseFloat(parts[3])
|
|
);
|
|
};
|
|
|
|
MM.Extent.fromArray = function(locations) {
|
|
var extent = new MM.Extent();
|
|
extent.setFromLocations(locations);
|
|
return extent;
|
|
};
|
|
|
|
// Transformation
|
|
// --------------
|
|
MM.Transformation = function(ax, bx, cx, ay, by, cy) {
|
|
this.ax = ax;
|
|
this.bx = bx;
|
|
this.cx = cx;
|
|
this.ay = ay;
|
|
this.by = by;
|
|
this.cy = cy;
|
|
};
|
|
|
|
MM.Transformation.prototype = {
|
|
|
|
ax: 0,
|
|
bx: 0,
|
|
cx: 0,
|
|
ay: 0,
|
|
by: 0,
|
|
cy: 0,
|
|
|
|
transform: function(point) {
|
|
return new MM.Point(this.ax * point.x + this.bx * point.y + this.cx,
|
|
this.ay * point.x + this.by * point.y + this.cy);
|
|
},
|
|
|
|
untransform: function(point) {
|
|
return new MM.Point((point.x * this.by - point.y * this.bx -
|
|
this.cx * this.by + this.cy * this.bx) /
|
|
(this.ax * this.by - this.ay * this.bx),
|
|
(point.x * this.ay - point.y * this.ax -
|
|
this.cx * this.ay + this.cy * this.ax) /
|
|
(this.bx * this.ay - this.by * this.ax));
|
|
}
|
|
|
|
};
|
|
|
|
|
|
// Generates a transform based on three pairs of points,
|
|
// a1 -> a2, b1 -> b2, c1 -> c2.
|
|
MM.deriveTransformation = function(a1x, a1y, a2x, a2y,
|
|
b1x, b1y, b2x, b2y,
|
|
c1x, c1y, c2x, c2y) {
|
|
var x = MM.linearSolution(a1x, a1y, a2x,
|
|
b1x, b1y, b2x,
|
|
c1x, c1y, c2x);
|
|
var y = MM.linearSolution(a1x, a1y, a2y,
|
|
b1x, b1y, b2y,
|
|
c1x, c1y, c2y);
|
|
return new MM.Transformation(x[0], x[1], x[2], y[0], y[1], y[2]);
|
|
};
|
|
|
|
// Solves a system of linear equations.
|
|
//
|
|
// t1 = (a * r1) + (b + s1) + c
|
|
// t2 = (a * r2) + (b + s2) + c
|
|
// t3 = (a * r3) + (b + s3) + c
|
|
//
|
|
// r1 - t3 are the known values.
|
|
// a, b, c are the unknowns to be solved.
|
|
// returns the a, b, c coefficients.
|
|
MM.linearSolution = function(r1, s1, t1, r2, s2, t2, r3, s3, t3) {
|
|
// make them all floats
|
|
r1 = parseFloat(r1);
|
|
s1 = parseFloat(s1);
|
|
t1 = parseFloat(t1);
|
|
r2 = parseFloat(r2);
|
|
s2 = parseFloat(s2);
|
|
t2 = parseFloat(t2);
|
|
r3 = parseFloat(r3);
|
|
s3 = parseFloat(s3);
|
|
t3 = parseFloat(t3);
|
|
|
|
var a = (((t2 - t3) * (s1 - s2)) - ((t1 - t2) * (s2 - s3))) /
|
|
(((r2 - r3) * (s1 - s2)) - ((r1 - r2) * (s2 - s3)));
|
|
|
|
var b = (((t2 - t3) * (r1 - r2)) - ((t1 - t2) * (r2 - r3))) /
|
|
(((s2 - s3) * (r1 - r2)) - ((s1 - s2) * (r2 - r3)));
|
|
|
|
var c = t1 - (r1 * a) - (s1 * b);
|
|
return [ a, b, c ];
|
|
};
|
|
// Projection
|
|
// ----------
|
|
|
|
// An abstract class / interface for projections
|
|
MM.Projection = function(zoom, transformation) {
|
|
if (!transformation) {
|
|
transformation = new MM.Transformation(1, 0, 0, 0, 1, 0);
|
|
}
|
|
this.zoom = zoom;
|
|
this.transformation = transformation;
|
|
};
|
|
|
|
MM.Projection.prototype = {
|
|
|
|
zoom: 0,
|
|
transformation: null,
|
|
|
|
rawProject: function(point) {
|
|
throw "Abstract method not implemented by subclass.";
|
|
},
|
|
|
|
rawUnproject: function(point) {
|
|
throw "Abstract method not implemented by subclass.";
|
|
},
|
|
|
|
project: function(point) {
|
|
point = this.rawProject(point);
|
|
if(this.transformation) {
|
|
point = this.transformation.transform(point);
|
|
}
|
|
return point;
|
|
},
|
|
|
|
unproject: function(point) {
|
|
if(this.transformation) {
|
|
point = this.transformation.untransform(point);
|
|
}
|
|
point = this.rawUnproject(point);
|
|
return point;
|
|
},
|
|
|
|
locationCoordinate: function(location) {
|
|
var point = new MM.Point(Math.PI * location.lon / 180.0,
|
|
Math.PI * location.lat / 180.0);
|
|
point = this.project(point);
|
|
return new MM.Coordinate(point.y, point.x, this.zoom);
|
|
},
|
|
|
|
coordinateLocation: function(coordinate) {
|
|
coordinate = coordinate.zoomTo(this.zoom);
|
|
var point = new MM.Point(coordinate.column, coordinate.row);
|
|
point = this.unproject(point);
|
|
return new MM.Location(180.0 * point.y / Math.PI,
|
|
180.0 * point.x / Math.PI);
|
|
}
|
|
};
|
|
|
|
// A projection for equilateral maps, based on longitude and latitude
|
|
MM.LinearProjection = function(zoom, transformation) {
|
|
MM.Projection.call(this, zoom, transformation);
|
|
};
|
|
|
|
// The Linear projection doesn't reproject points
|
|
MM.LinearProjection.prototype = {
|
|
rawProject: function(point) {
|
|
return new MM.Point(point.x, point.y);
|
|
},
|
|
rawUnproject: function(point) {
|
|
return new MM.Point(point.x, point.y);
|
|
}
|
|
};
|
|
|
|
MM.extend(MM.LinearProjection, MM.Projection);
|
|
|
|
MM.MercatorProjection = function(zoom, transformation) {
|
|
// super!
|
|
MM.Projection.call(this, zoom, transformation);
|
|
};
|
|
|
|
// Project lon/lat points into meters required for Mercator
|
|
MM.MercatorProjection.prototype = {
|
|
rawProject: function(point) {
|
|
return new MM.Point(point.x,
|
|
Math.log(Math.tan(0.25 * Math.PI + 0.5 * point.y)));
|
|
},
|
|
|
|
rawUnproject: function(point) {
|
|
return new MM.Point(point.x,
|
|
2 * Math.atan(Math.pow(Math.E, point.y)) - 0.5 * Math.PI);
|
|
}
|
|
};
|
|
|
|
MM.extend(MM.MercatorProjection, MM.Projection);
|
|
// Providers
|
|
// ---------
|
|
// Providers provide tile URLs and possibly elements for layers.
|
|
//
|
|
// MapProvider ->
|
|
// Template
|
|
//
|
|
MM.MapProvider = function(getTile) {
|
|
if (getTile) {
|
|
this.getTile = getTile;
|
|
}
|
|
};
|
|
|
|
MM.MapProvider.prototype = {
|
|
|
|
// these are limits for available *tiles*
|
|
// panning limits will be different (since you can wrap around columns)
|
|
// but if you put Infinity in here it will screw up sourceCoordinate
|
|
tileLimits: [
|
|
new MM.Coordinate(0,0,0), // top left outer
|
|
new MM.Coordinate(1,1,0).zoomTo(18) // bottom right inner
|
|
],
|
|
|
|
getTileUrl: function(coordinate) {
|
|
throw "Abstract method not implemented by subclass.";
|
|
},
|
|
|
|
getTile: function(coordinate) {
|
|
throw "Abstract method not implemented by subclass.";
|
|
},
|
|
|
|
// releaseTile is not required
|
|
releaseTile: function(element) { },
|
|
|
|
// use this to tell MapProvider that tiles only exist between certain zoom levels.
|
|
// should be set separately on Map to restrict interactive zoom/pan ranges
|
|
setZoomRange: function(minZoom, maxZoom) {
|
|
this.tileLimits[0] = this.tileLimits[0].zoomTo(minZoom);
|
|
this.tileLimits[1] = this.tileLimits[1].zoomTo(maxZoom);
|
|
},
|
|
|
|
// wrap column around the world if necessary
|
|
// return null if wrapped coordinate is outside of the tile limits
|
|
sourceCoordinate: function(coord) {
|
|
var TL = this.tileLimits[0].zoomTo(coord.zoom).container(),
|
|
BR = this.tileLimits[1].zoomTo(coord.zoom),
|
|
columnSize = Math.pow(2, coord.zoom),
|
|
wrappedColumn;
|
|
|
|
BR = new MM.Coordinate(Math.ceil(BR.row), Math.ceil(BR.column), Math.floor(BR.zoom));
|
|
|
|
if (coord.column < 0) {
|
|
wrappedColumn = ((coord.column % columnSize) + columnSize) % columnSize;
|
|
} else {
|
|
wrappedColumn = coord.column % columnSize;
|
|
}
|
|
|
|
if (coord.row < TL.row || coord.row >= BR.row) {
|
|
return null;
|
|
} else if (wrappedColumn < TL.column || wrappedColumn >= BR.column) {
|
|
return null;
|
|
} else {
|
|
return new MM.Coordinate(coord.row, wrappedColumn, coord.zoom);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* FIXME: need a better explanation here! This is a pretty crucial part of
|
|
* understanding how to use ModestMaps.
|
|
*
|
|
* TemplatedMapProvider is a tile provider that generates tile URLs from a
|
|
* template string by replacing the following bits for each tile
|
|
* coordinate:
|
|
*
|
|
* {Z}: the tile's zoom level (from 1 to ~20)
|
|
* {X}: the tile's X, or column (from 0 to a very large number at higher
|
|
* zooms)
|
|
* {Y}: the tile's Y, or row (from 0 to a very large number at higher
|
|
* zooms)
|
|
*
|
|
* E.g.:
|
|
*
|
|
* var osm = new MM.TemplatedMapProvider("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png");
|
|
*
|
|
* Or:
|
|
*
|
|
* var placeholder = new MM.TemplatedMapProvider("http://placehold.it/256/f0f/fff.png&text={Z}/{X}/{Y}");
|
|
*
|
|
*/
|
|
MM.Template = function(template, subdomains) {
|
|
var isQuadKey = template.match(/{(Q|quadkey)}/);
|
|
// replace Microsoft style substitution strings
|
|
if (isQuadKey) template = template
|
|
.replace('{subdomains}', '{S}')
|
|
.replace('{zoom}', '{Z}')
|
|
.replace('{quadkey}', '{Q}');
|
|
|
|
var hasSubdomains = (subdomains &&
|
|
subdomains.length && template.indexOf("{S}") >= 0);
|
|
|
|
function quadKey (row, column, zoom) {
|
|
var key = '';
|
|
for (var i = 1; i <= zoom; i++) {
|
|
key += (((row >> zoom - i) & 1) << 1) | ((column >> zoom - i) & 1);
|
|
}
|
|
return key || '0';
|
|
}
|
|
|
|
var getTileUrl = function(coordinate) {
|
|
var coord = this.sourceCoordinate(coordinate);
|
|
if (!coord) {
|
|
return null;
|
|
}
|
|
var base = template;
|
|
if (hasSubdomains) {
|
|
var index = parseInt(coord.zoom + coord.row + coord.column, 10) %
|
|
subdomains.length;
|
|
base = base.replace('{S}', subdomains[index]);
|
|
}
|
|
if (isQuadKey) {
|
|
return base
|
|
.replace('{Z}', coord.zoom.toFixed(0))
|
|
.replace('{Q}', quadKey(coord.row,
|
|
coord.column,
|
|
coord.zoom));
|
|
} else {
|
|
return base
|
|
.replace('{Z}', coord.zoom.toFixed(0))
|
|
.replace('{X}', coord.column.toFixed(0))
|
|
.replace('{Y}', coord.row.toFixed(0));
|
|
}
|
|
};
|
|
|
|
MM.MapProvider.call(this, getTileUrl);
|
|
};
|
|
|
|
MM.Template.prototype = {
|
|
// quadKey generator
|
|
getTile: function(coord) {
|
|
return this.getTileUrl(coord);
|
|
}
|
|
};
|
|
|
|
MM.extend(MM.Template, MM.MapProvider);
|
|
|
|
MM.TemplatedLayer = function(template, subdomains, name) {
|
|
return new MM.Layer(new MM.Template(template, subdomains), null, name);
|
|
};
|
|
// Event Handlers
|
|
// --------------
|
|
|
|
// A utility function for finding the offset of the
|
|
// mouse from the top-left of the page
|
|
MM.getMousePoint = function(e, map) {
|
|
// start with just the mouse (x, y)
|
|
var point = new MM.Point(e.clientX, e.clientY);
|
|
|
|
// correct for scrolled document
|
|
point.x += document.body.scrollLeft + document.documentElement.scrollLeft;
|
|
point.y += document.body.scrollTop + document.documentElement.scrollTop;
|
|
|
|
// correct for nested offsets in DOM
|
|
for (var node = map.parent; node; node = node.offsetParent) {
|
|
point.x -= node.offsetLeft;
|
|
point.y -= node.offsetTop;
|
|
}
|
|
return point;
|
|
};
|
|
|
|
MM.MouseWheelHandler = function() {
|
|
var handler = {},
|
|
map,
|
|
_zoomDiv,
|
|
prevTime,
|
|
precise = false;
|
|
|
|
function mouseWheel(e) {
|
|
var delta = 0;
|
|
prevTime = prevTime || new Date().getTime();
|
|
|
|
try {
|
|
_zoomDiv.scrollTop = 1000;
|
|
_zoomDiv.dispatchEvent(e);
|
|
delta = 1000 - _zoomDiv.scrollTop;
|
|
} catch (error) {
|
|
delta = e.wheelDelta || (-e.detail * 5);
|
|
}
|
|
|
|
// limit mousewheeling to once every 200ms
|
|
var timeSince = new Date().getTime() - prevTime;
|
|
var point = MM.getMousePoint(e, map);
|
|
|
|
if (Math.abs(delta) > 0 && (timeSince > 200) && !precise) {
|
|
map.zoomByAbout(delta > 0 ? 1 : -1, point);
|
|
prevTime = new Date().getTime();
|
|
} else if (precise) {
|
|
map.zoomByAbout(delta * 0.001, point);
|
|
}
|
|
|
|
// Cancel the event so that the page doesn't scroll
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
_zoomDiv = document.body.appendChild(document.createElement('div'));
|
|
_zoomDiv.style.cssText = 'visibility:hidden;top:0;height:0;width:0;overflow-y:scroll';
|
|
var innerDiv = _zoomDiv.appendChild(document.createElement('div'));
|
|
innerDiv.style.height = '2000px';
|
|
MM.addEvent(map.parent, 'mousewheel', mouseWheel);
|
|
return handler;
|
|
};
|
|
|
|
handler.precise = function(x) {
|
|
if (!arguments.length) return precise;
|
|
precise = x;
|
|
return handler;
|
|
};
|
|
|
|
handler.remove = function() {
|
|
MM.removeEvent(map.parent, 'mousewheel', mouseWheel);
|
|
_zoomDiv.parentNode.removeChild(_zoomDiv);
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
|
|
MM.DoubleClickHandler = function() {
|
|
var handler = {},
|
|
map;
|
|
|
|
function doubleClick(e) {
|
|
// Ensure that this handler is attached once.
|
|
// Get the point on the map that was double-clicked
|
|
var point = MM.getMousePoint(e, map);
|
|
// use shift-double-click to zoom out
|
|
map.zoomByAbout(e.shiftKey ? -1 : 1, point);
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
MM.addEvent(map.parent, 'dblclick', doubleClick);
|
|
return handler;
|
|
};
|
|
|
|
handler.remove = function() {
|
|
MM.removeEvent(map.parent, 'dblclick', doubleClick);
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
|
|
// Handle the use of mouse dragging to pan the map.
|
|
MM.DragHandler = function() {
|
|
var handler = {},
|
|
prevMouse,
|
|
map;
|
|
|
|
function mouseDown(e) {
|
|
if (e.shiftKey || e.button == 2) return;
|
|
MM.addEvent(document, 'mouseup', mouseUp);
|
|
MM.addEvent(document, 'mousemove', mouseMove);
|
|
|
|
prevMouse = new MM.Point(e.clientX, e.clientY);
|
|
map.parent.style.cursor = 'move';
|
|
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function mouseUp(e) {
|
|
MM.removeEvent(document, 'mouseup', mouseUp);
|
|
MM.removeEvent(document, 'mousemove', mouseMove);
|
|
|
|
prevMouse = null;
|
|
map.parent.style.cursor = '';
|
|
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function mouseMove(e) {
|
|
if (prevMouse) {
|
|
map.panBy(
|
|
e.clientX - prevMouse.x,
|
|
e.clientY - prevMouse.y);
|
|
prevMouse.x = e.clientX;
|
|
prevMouse.y = e.clientY;
|
|
prevMouse.t = +new Date();
|
|
}
|
|
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
MM.addEvent(map.parent, 'mousedown', mouseDown);
|
|
return handler;
|
|
};
|
|
|
|
handler.remove = function() {
|
|
MM.removeEvent(map.parent, 'mousedown', mouseDown);
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
|
|
MM.MouseHandler = function() {
|
|
var handler = {},
|
|
map,
|
|
handlers;
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
handlers = [
|
|
MM.DragHandler().init(map),
|
|
MM.DoubleClickHandler().init(map),
|
|
MM.MouseWheelHandler().init(map)
|
|
];
|
|
return handler;
|
|
};
|
|
|
|
handler.remove = function() {
|
|
for (var i = 0; i < handlers.length; i++) {
|
|
handlers[i].remove();
|
|
}
|
|
return handler;
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
MM.TouchHandler = function() {
|
|
var handler = {},
|
|
map,
|
|
maxTapTime = 250,
|
|
maxTapDistance = 30,
|
|
maxDoubleTapDelay = 350,
|
|
locations = {},
|
|
taps = [],
|
|
snapToZoom = true,
|
|
wasPinching = false,
|
|
lastPinchCenter = null;
|
|
|
|
function isTouchable () {
|
|
var el = document.createElement('div');
|
|
el.setAttribute('ongesturestart', 'return;');
|
|
return (typeof el.ongesturestart === 'function');
|
|
}
|
|
|
|
function updateTouches(e) {
|
|
for (var i = 0; i < e.touches.length; i += 1) {
|
|
var t = e.touches[i];
|
|
if (t.identifier in locations) {
|
|
var l = locations[t.identifier];
|
|
l.x = t.clientX;
|
|
l.y = t.clientY;
|
|
l.scale = e.scale;
|
|
}
|
|
else {
|
|
locations[t.identifier] = {
|
|
scale: e.scale,
|
|
startPos: { x: t.clientX, y: t.clientY },
|
|
x: t.clientX,
|
|
y: t.clientY,
|
|
time: new Date().getTime()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test whether touches are from the same source -
|
|
// whether this is the same touchmove event.
|
|
function sameTouch (event, touch) {
|
|
return (event && event.touch) &&
|
|
(touch.identifier == event.touch.identifier);
|
|
}
|
|
|
|
function touchStart(e) {
|
|
updateTouches(e);
|
|
}
|
|
|
|
function touchMove(e) {
|
|
switch (e.touches.length) {
|
|
case 1:
|
|
onPanning(e.touches[0]);
|
|
break;
|
|
case 2:
|
|
onPinching(e);
|
|
break;
|
|
}
|
|
updateTouches(e);
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function touchEnd(e) {
|
|
var now = new Date().getTime();
|
|
// round zoom if we're done pinching
|
|
if (e.touches.length === 0 && wasPinching) {
|
|
onPinched(lastPinchCenter);
|
|
}
|
|
|
|
// Look at each changed touch in turn.
|
|
for (var i = 0; i < e.changedTouches.length; i += 1) {
|
|
var t = e.changedTouches[i],
|
|
loc = locations[t.identifier];
|
|
// if we didn't see this one (bug?)
|
|
// or if it was consumed by pinching already
|
|
// just skip to the next one
|
|
if (!loc || loc.wasPinch) {
|
|
continue;
|
|
}
|
|
|
|
// we now know we have an event object and a
|
|
// matching touch that's just ended. Let's see
|
|
// what kind of event it is based on how long it
|
|
// lasted and how far it moved.
|
|
var pos = { x: t.clientX, y: t.clientY },
|
|
time = now - loc.time,
|
|
travel = MM.Point.distance(pos, loc.startPos);
|
|
if (travel > maxTapDistance) {
|
|
// we will to assume that the drag has been handled separately
|
|
} else if (time > maxTapTime) {
|
|
// close in space, but not in time: a hold
|
|
pos.end = now;
|
|
pos.duration = time;
|
|
onHold(pos);
|
|
} else {
|
|
// close in both time and space: a tap
|
|
pos.time = now;
|
|
onTap(pos);
|
|
}
|
|
}
|
|
|
|
// Weird, sometimes an end event doesn't get thrown
|
|
// for a touch that nevertheless has disappeared.
|
|
// Still, this will eventually catch those ids:
|
|
|
|
var validTouchIds = {};
|
|
for (var j = 0; j < e.touches.length; j++) {
|
|
validTouchIds[e.touches[j].identifier] = true;
|
|
}
|
|
for (var id in locations) {
|
|
if (!(id in validTouchIds)) {
|
|
delete validTouchIds[id];
|
|
}
|
|
}
|
|
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function onHold (hold) {
|
|
// TODO
|
|
}
|
|
|
|
// Handle a tap event - mainly watch for a doubleTap
|
|
function onTap(tap) {
|
|
if (taps.length &&
|
|
(tap.time - taps[0].time) < maxDoubleTapDelay) {
|
|
onDoubleTap(tap);
|
|
taps = [];
|
|
return;
|
|
}
|
|
taps = [tap];
|
|
}
|
|
|
|
// Handle a double tap by zooming in a single zoom level to a
|
|
// round zoom.
|
|
function onDoubleTap(tap) {
|
|
var z = map.getZoom(), // current zoom
|
|
tz = Math.round(z) + 1, // target zoom
|
|
dz = tz - z; // desired delate
|
|
|
|
// zoom in to a round number
|
|
var p = new MM.Point(tap.x, tap.y);
|
|
map.zoomByAbout(dz, p);
|
|
}
|
|
|
|
// Re-transform the actual map parent's CSS transformation
|
|
function onPanning (touch) {
|
|
var pos = { x: touch.clientX, y: touch.clientY },
|
|
prev = locations[touch.identifier];
|
|
map.panBy(pos.x - prev.x, pos.y - prev.y);
|
|
}
|
|
|
|
function onPinching(e) {
|
|
// use the first two touches and their previous positions
|
|
var t0 = e.touches[0],
|
|
t1 = e.touches[1],
|
|
p0 = new MM.Point(t0.clientX, t0.clientY),
|
|
p1 = new MM.Point(t1.clientX, t1.clientY),
|
|
l0 = locations[t0.identifier],
|
|
l1 = locations[t1.identifier];
|
|
|
|
// mark these touches so they aren't used as taps/holds
|
|
l0.wasPinch = true;
|
|
l1.wasPinch = true;
|
|
|
|
// scale about the center of these touches
|
|
var center = MM.Point.interpolate(p0, p1, 0.5);
|
|
|
|
map.zoomByAbout(
|
|
Math.log(e.scale) / Math.LN2 -
|
|
Math.log(l0.scale) / Math.LN2,
|
|
center );
|
|
|
|
// pan from the previous center of these touches
|
|
var prevCenter = MM.Point.interpolate(l0, l1, 0.5);
|
|
|
|
map.panBy(center.x - prevCenter.x,
|
|
center.y - prevCenter.y);
|
|
wasPinching = true;
|
|
lastPinchCenter = center;
|
|
}
|
|
|
|
// When a pinch event ends, round the zoom of the map.
|
|
function onPinched(p) {
|
|
// TODO: easing
|
|
if (snapToZoom) {
|
|
var z = map.getZoom(), // current zoom
|
|
tz =Math.round(z); // target zoom
|
|
map.zoomByAbout(tz - z, p);
|
|
}
|
|
wasPinching = false;
|
|
}
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
|
|
// Fail early if this isn't a touch device.
|
|
if (!isTouchable()) return handler;
|
|
|
|
MM.addEvent(map.parent, 'touchstart', touchStart);
|
|
MM.addEvent(map.parent, 'touchmove', touchMove);
|
|
MM.addEvent(map.parent, 'touchend', touchEnd);
|
|
return handler;
|
|
};
|
|
|
|
handler.remove = function() {
|
|
// Fail early if this isn't a touch device.
|
|
if (!isTouchable()) return handler;
|
|
|
|
MM.removeEvent(map.parent, 'touchstart', touchStart);
|
|
MM.removeEvent(map.parent, 'touchmove', touchMove);
|
|
MM.removeEvent(map.parent, 'touchend', touchEnd);
|
|
return handler;
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
// CallbackManager
|
|
// ---------------
|
|
// A general-purpose event binding manager used by `Map`
|
|
// and `RequestManager`
|
|
|
|
// Construct a new CallbackManager, with an list of
|
|
// supported events.
|
|
MM.CallbackManager = function(owner, events) {
|
|
this.owner = owner;
|
|
this.callbacks = {};
|
|
for (var i = 0; i < events.length; i++) {
|
|
this.callbacks[events[i]] = [];
|
|
}
|
|
};
|
|
|
|
// CallbackManager does simple event management for modestmaps
|
|
MM.CallbackManager.prototype = {
|
|
// The element on which callbacks will be triggered.
|
|
owner: null,
|
|
|
|
// An object of callbacks in the form
|
|
//
|
|
// { event: function }
|
|
callbacks: null,
|
|
|
|
// Add a callback to this object - where the `event` is a string of
|
|
// the event name and `callback` is a function.
|
|
addCallback: function(event, callback) {
|
|
if (typeof(callback) == 'function' && this.callbacks[event]) {
|
|
this.callbacks[event].push(callback);
|
|
}
|
|
},
|
|
|
|
// Remove a callback. The given function needs to be equal (`===`) to
|
|
// the callback added in `addCallback`, so named functions should be
|
|
// used as callbacks.
|
|
removeCallback: function(event, callback) {
|
|
if (typeof(callback) == 'function' && this.callbacks[event]) {
|
|
var cbs = this.callbacks[event],
|
|
len = cbs.length;
|
|
for (var i = 0; i < len; i++) {
|
|
if (cbs[i] === callback) {
|
|
cbs.splice(i,1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Trigger a callback, passing it an object or string from the second
|
|
// argument.
|
|
dispatchCallback: function(event, message) {
|
|
if(this.callbacks[event]) {
|
|
for (var i = 0; i < this.callbacks[event].length; i += 1) {
|
|
try {
|
|
this.callbacks[event][i](this.owner, message);
|
|
} catch(e) {
|
|
//console.log(e);
|
|
// meh
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
// RequestManager
|
|
// --------------
|
|
// an image loading queue
|
|
MM.RequestManager = function() {
|
|
|
|
// The loading bay is a document fragment to optimize appending, since
|
|
// the elements within are invisible. See
|
|
// [this blog post](http://ejohn.org/blog/dom-documentfragments/).
|
|
this.loadingBay = document.createDocumentFragment();
|
|
|
|
this.requestsById = {};
|
|
this.openRequestCount = 0;
|
|
|
|
this.maxOpenRequests = 4;
|
|
this.requestQueue = [];
|
|
|
|
this.callbackManager = new MM.CallbackManager(this, [
|
|
'requestcomplete', 'requesterror']);
|
|
};
|
|
|
|
MM.RequestManager.prototype = {
|
|
|
|
// DOM element, hidden, for making sure images dispatch complete events
|
|
loadingBay: null,
|
|
|
|
// all known requests, by ID
|
|
requestsById: null,
|
|
|
|
// current pending requests
|
|
requestQueue: null,
|
|
|
|
// current open requests (children of loadingBay)
|
|
openRequestCount: null,
|
|
|
|
// the number of open requests permitted at one time, clamped down
|
|
// because of domain-connection limits.
|
|
maxOpenRequests: null,
|
|
|
|
// for dispatching 'requestcomplete'
|
|
callbackManager: null,
|
|
|
|
addCallback: function(event, callback) {
|
|
this.callbackManager.addCallback(event,callback);
|
|
},
|
|
|
|
removeCallback: function(event, callback) {
|
|
this.callbackManager.removeCallback(event,callback);
|
|
},
|
|
|
|
dispatchCallback: function(event, message) {
|
|
this.callbackManager.dispatchCallback(event,message);
|
|
},
|
|
|
|
// Clear everything in the queue by excluding nothing
|
|
clear: function() {
|
|
this.clearExcept({});
|
|
},
|
|
|
|
clearRequest: function(id) {
|
|
if(id in this.requestsById) {
|
|
delete this.requestsById[id];
|
|
}
|
|
|
|
for(var i = 0; i < this.requestQueue.length; i++) {
|
|
var request = this.requestQueue[i];
|
|
if(request && request.id == id) {
|
|
this.requestQueue[i] = null;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Clear everything in the queue except for certain keys, specified
|
|
// by an object of the form
|
|
//
|
|
// { key: throwawayvalue }
|
|
clearExcept: function(validIds) {
|
|
|
|
// clear things from the queue first...
|
|
for (var i = 0; i < this.requestQueue.length; i++) {
|
|
var request = this.requestQueue[i];
|
|
if (request && !(request.id in validIds)) {
|
|
this.requestQueue[i] = null;
|
|
}
|
|
}
|
|
|
|
// then check the loadingBay...
|
|
var openRequests = this.loadingBay.childNodes;
|
|
for (var j = openRequests.length-1; j >= 0; j--) {
|
|
var img = openRequests[j];
|
|
if (!(img.id in validIds)) {
|
|
this.loadingBay.removeChild(img);
|
|
this.openRequestCount--;
|
|
/* console.log(this.openRequestCount + " open requests"); */
|
|
img.src = img.coord = img.onload = img.onerror = null;
|
|
}
|
|
}
|
|
|
|
// hasOwnProperty protects against prototype additions
|
|
// > "The standard describes an augmentable Object.prototype.
|
|
// Ignore standards at your own peril."
|
|
// -- http://www.yuiblog.com/blog/2006/09/26/for-in-intrigue/
|
|
for (var id in this.requestsById) {
|
|
if (!(id in validIds)) {
|
|
if (this.requestsById.hasOwnProperty(id)) {
|
|
var requestToRemove = this.requestsById[id];
|
|
// whether we've done the request or not...
|
|
delete this.requestsById[id];
|
|
if (requestToRemove !== null) {
|
|
requestToRemove =
|
|
requestToRemove.id =
|
|
requestToRemove.coord =
|
|
requestToRemove.url = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Given a tile id, check whether the RequestManager is currently
|
|
// requesting it and waiting for the result.
|
|
hasRequest: function(id) {
|
|
return (id in this.requestsById);
|
|
},
|
|
|
|
// * TODO: remove dependency on coord (it's for sorting, maybe call it data?)
|
|
// * TODO: rename to requestImage once it's not tile specific
|
|
requestTile: function(id, coord, url) {
|
|
if (!(id in this.requestsById)) {
|
|
var request = { id: id, coord: coord.copy(), url: url };
|
|
// if there's no url just make sure we don't request this image again
|
|
this.requestsById[id] = request;
|
|
if (url) {
|
|
this.requestQueue.push(request);
|
|
/* console.log(this.requestQueue.length + ' pending requests'); */
|
|
}
|
|
}
|
|
},
|
|
|
|
getProcessQueue: function() {
|
|
// let's only create this closure once...
|
|
if (!this._processQueue) {
|
|
var theManager = this;
|
|
this._processQueue = function() {
|
|
theManager.processQueue();
|
|
};
|
|
}
|
|
return this._processQueue;
|
|
},
|
|
|
|
// Select images from the `requestQueue` and create image elements for
|
|
// them, attaching their load events to the function returned by
|
|
// `this.getLoadComplete()` so that they can be added to the map.
|
|
processQueue: function(sortFunc) {
|
|
// When the request queue fills up beyond 8, start sorting the
|
|
// requests so that spiral-loading or another pattern can be used.
|
|
if (sortFunc && this.requestQueue.length > 8) {
|
|
this.requestQueue.sort(sortFunc);
|
|
}
|
|
while (this.openRequestCount < this.maxOpenRequests && this.requestQueue.length > 0) {
|
|
var request = this.requestQueue.pop();
|
|
if (request) {
|
|
this.openRequestCount++;
|
|
/* console.log(this.openRequestCount + ' open requests'); */
|
|
|
|
// JSLitmus benchmark shows createElement is a little faster than
|
|
// new Image() in Firefox and roughly the same in Safari:
|
|
// http://tinyurl.com/y9wz2jj http://tinyurl.com/yes6rrt
|
|
var img = document.createElement('img');
|
|
|
|
// FIXME: id is technically not unique in document if there
|
|
// are two Maps but toKey is supposed to be fast so we're trying
|
|
// to avoid a prefix ... hence we can't use any calls to
|
|
// `document.getElementById()` to retrieve images
|
|
img.id = request.id;
|
|
img.style.position = 'absolute';
|
|
// * FIXME: store this elsewhere to avoid scary memory leaks?
|
|
// * FIXME: call this 'data' not 'coord' so that RequestManager is less Tile-centric?
|
|
img.coord = request.coord;
|
|
// add it to the DOM in a hidden layer, this is a bit of a hack, but it's
|
|
// so that the event we get in image.onload has srcElement assigned in IE6
|
|
this.loadingBay.appendChild(img);
|
|
// set these before img.src to avoid missing an img that's already cached
|
|
img.onload = img.onerror = this.getLoadComplete();
|
|
img.src = request.url;
|
|
|
|
// keep things tidy
|
|
request = request.id = request.coord = request.url = null;
|
|
}
|
|
}
|
|
},
|
|
|
|
_loadComplete: null,
|
|
|
|
// Get the singleton `_loadComplete` function that is called on image
|
|
// load events, either removing them from the queue and dispatching an
|
|
// event to add them to the map, or deleting them if the image failed
|
|
// to load.
|
|
getLoadComplete: function() {
|
|
// let's only create this closure once...
|
|
if (!this._loadComplete) {
|
|
var theManager = this;
|
|
this._loadComplete = function(e) {
|
|
// this is needed because we don't use MM.addEvent for images
|
|
e = e || window.event;
|
|
|
|
// srcElement for IE, target for FF, Safari etc.
|
|
var img = e.srcElement || e.target;
|
|
|
|
// unset these straight away so we don't call this twice
|
|
img.onload = img.onerror = null;
|
|
|
|
// pull it back out of the (hidden) DOM
|
|
// so that draw will add it correctly later
|
|
theManager.loadingBay.removeChild(img);
|
|
theManager.openRequestCount--;
|
|
delete theManager.requestsById[img.id];
|
|
|
|
/* console.log(theManager.openRequestCount + ' open requests'); */
|
|
|
|
// NB:- complete is also true onerror if we got a 404
|
|
if (e.type === 'load' && (img.complete ||
|
|
(img.readyState && img.readyState == 'complete'))) {
|
|
theManager.dispatchCallback('requestcomplete', img);
|
|
} else {
|
|
// if it didn't finish clear its src to make sure it
|
|
// really stops loading
|
|
// FIXME: we'll never retry because this id is still
|
|
// in requestsById - is that right?
|
|
theManager.dispatchCallback('requesterror', {
|
|
element: img,
|
|
url: ('' + img.src)
|
|
});
|
|
img.src = null;
|
|
}
|
|
|
|
// keep going in the same order
|
|
// use `setTimeout()` to avoid the IE recursion limit, see
|
|
// http://cappuccino.org/discuss/2010/03/01/internet-explorer-global-variables-and-stack-overflows/
|
|
// and https://github.com/stamen/modestmaps-js/issues/12
|
|
setTimeout(theManager.getProcessQueue(), 0);
|
|
|
|
};
|
|
}
|
|
return this._loadComplete;
|
|
}
|
|
|
|
};
|
|
|
|
// Layer
|
|
MM.Layer = function(provider, parent, name) {
|
|
this.parent = parent || document.createElement('div');
|
|
this.parent.style.cssText = 'position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0';
|
|
this.name = name;
|
|
this.levels = {};
|
|
this.requestManager = new MM.RequestManager();
|
|
this.requestManager.addCallback('requestcomplete', this.getTileComplete());
|
|
this.requestManager.addCallback('requesterror', this.getTileError());
|
|
if (provider) this.setProvider(provider);
|
|
};
|
|
|
|
MM.Layer.prototype = {
|
|
|
|
map: null, // TODO: remove
|
|
parent: null,
|
|
name: null,
|
|
enabled: true,
|
|
tiles: null,
|
|
levels: null,
|
|
requestManager: null,
|
|
provider: null,
|
|
_tileComplete: null,
|
|
|
|
getTileComplete: function() {
|
|
if (!this._tileComplete) {
|
|
var theLayer = this;
|
|
this._tileComplete = function(manager, tile) {
|
|
theLayer.tiles[tile.id] = tile;
|
|
theLayer.positionTile(tile);
|
|
};
|
|
}
|
|
return this._tileComplete;
|
|
},
|
|
|
|
getTileError: function() {
|
|
if (!this._tileError) {
|
|
var theLayer = this;
|
|
this._tileError = function(manager, tile) {
|
|
tile.element.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
theLayer.tiles[tile.element.id] = tile.element;
|
|
theLayer.positionTile(tile.element);
|
|
};
|
|
}
|
|
return this._tileError;
|
|
},
|
|
|
|
draw: function() {
|
|
if (!this.enabled || !this.map) return;
|
|
// compares manhattan distance from center of
|
|
// requested tiles to current map center
|
|
// NB:- requested tiles are *popped* from queue, so we do a descending sort
|
|
var theCoord = this.map.coordinate.zoomTo(Math.round(this.map.coordinate.zoom));
|
|
|
|
function centerDistanceCompare(r1, r2) {
|
|
if (r1 && r2) {
|
|
var c1 = r1.coord;
|
|
var c2 = r2.coord;
|
|
if (c1.zoom == c2.zoom) {
|
|
var ds1 = Math.abs(theCoord.row - c1.row - 0.5) +
|
|
Math.abs(theCoord.column - c1.column - 0.5);
|
|
var ds2 = Math.abs(theCoord.row - c2.row - 0.5) +
|
|
Math.abs(theCoord.column - c2.column - 0.5);
|
|
return ds1 < ds2 ? 1 : ds1 > ds2 ? -1 : 0;
|
|
} else {
|
|
return c1.zoom < c2.zoom ? 1 : c1.zoom > c2.zoom ? -1 : 0;
|
|
}
|
|
}
|
|
return r1 ? 1 : r2 ? -1 : 0;
|
|
}
|
|
|
|
// if we're in between zoom levels, we need to choose the nearest:
|
|
var baseZoom = Math.round(this.map.coordinate.zoom);
|
|
|
|
// these are the top left and bottom right tile coordinates
|
|
// we'll be loading everything in between:
|
|
var startCoord = this.map.pointCoordinate(new MM.Point(0,0))
|
|
.zoomTo(baseZoom).container();
|
|
var endCoord = this.map.pointCoordinate(this.map.dimensions)
|
|
.zoomTo(baseZoom).container().right().down();
|
|
|
|
// tiles with invalid keys will be removed from visible levels
|
|
// requests for tiles with invalid keys will be canceled
|
|
// (this object maps from a tile key to a boolean)
|
|
var validTileKeys = { };
|
|
|
|
// make sure we have a container for tiles in the current level
|
|
var levelElement = this.createOrGetLevel(startCoord.zoom);
|
|
|
|
// use this coordinate for generating keys, parents and children:
|
|
var tileCoord = startCoord.copy();
|
|
|
|
for (tileCoord.column = startCoord.column;
|
|
tileCoord.column <= endCoord.column; tileCoord.column++) {
|
|
for (tileCoord.row = startCoord.row;
|
|
tileCoord.row <= endCoord.row; tileCoord.row++) {
|
|
var validKeys = this.inventoryVisibleTile(levelElement, tileCoord);
|
|
|
|
while (validKeys.length) {
|
|
validTileKeys[validKeys.pop()] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// i from i to zoom-5 are levels that would be scaled too big,
|
|
// i from zoom + 2 to levels. length are levels that would be
|
|
// scaled too small (and tiles would be too numerous)
|
|
for (var name in this.levels) {
|
|
if (this.levels.hasOwnProperty(name)) {
|
|
var zoom = parseInt(name,10);
|
|
|
|
if (zoom >= startCoord.zoom - 5 && zoom < startCoord.zoom + 2) {
|
|
continue;
|
|
}
|
|
|
|
var level = this.levels[name];
|
|
level.style.display = 'none';
|
|
var visibleTiles = this.tileElementsInLevel(level);
|
|
|
|
while (visibleTiles.length) {
|
|
this.provider.releaseTile(visibleTiles[0].coord);
|
|
this.requestManager.clearRequest(visibleTiles[0].coord.toKey());
|
|
level.removeChild(visibleTiles[0]);
|
|
visibleTiles.shift();
|
|
}
|
|
}
|
|
}
|
|
|
|
// levels we want to see, if they have tiles in validTileKeys
|
|
var minLevel = startCoord.zoom - 5;
|
|
var maxLevel = startCoord.zoom + 2;
|
|
|
|
for (var z = minLevel; z < maxLevel; z++) {
|
|
this.adjustVisibleLevel(this.levels[z], z, validTileKeys);
|
|
}
|
|
|
|
// cancel requests that aren't visible:
|
|
this.requestManager.clearExcept(validTileKeys);
|
|
|
|
// get newly requested tiles, sort according to current view:
|
|
this.requestManager.processQueue(centerDistanceCompare);
|
|
},
|
|
|
|
// For a given tile coordinate in a given level element, ensure that it's
|
|
// correctly represented in the DOM including potentially-overlapping
|
|
// parent and child tiles for pyramid loading.
|
|
//
|
|
// Return a list of valid (i.e. loadable?) tile keys.
|
|
inventoryVisibleTile: function(layer_element, tile_coord) {
|
|
var tile_key = tile_coord.toKey(),
|
|
valid_tile_keys = [tile_key];
|
|
|
|
// Check that the needed tile already exists someplace - add it to the DOM if it does.
|
|
if (tile_key in this.tiles) {
|
|
var tile = this.tiles[tile_key];
|
|
|
|
// ensure it's in the DOM:
|
|
if (tile.parentNode != layer_element) {
|
|
layer_element.appendChild(tile);
|
|
// if the provider implements reAddTile(), call it
|
|
if ("reAddTile" in this.provider) {
|
|
this.provider.reAddTile(tile_key, tile_coord, tile);
|
|
}
|
|
}
|
|
|
|
return valid_tile_keys;
|
|
}
|
|
|
|
// Check that the needed tile has even been requested at all.
|
|
if (!this.requestManager.hasRequest(tile_key)) {
|
|
var tileToRequest = this.provider.getTile(tile_coord);
|
|
if (typeof tileToRequest == 'string') {
|
|
this.addTileImage(tile_key, tile_coord, tileToRequest);
|
|
// tile must be truish
|
|
} else if (tileToRequest) {
|
|
this.addTileElement(tile_key, tile_coord, tileToRequest);
|
|
}
|
|
}
|
|
|
|
// look for a parent tile in our image cache
|
|
var tileCovered = false;
|
|
var maxStepsOut = tile_coord.zoom;
|
|
|
|
for (var pz = 1; pz <= maxStepsOut; pz++) {
|
|
var parent_coord = tile_coord.zoomBy(-pz).container();
|
|
var parent_key = parent_coord.toKey();
|
|
|
|
// only mark it valid if we have it already
|
|
if (parent_key in this.tiles) {
|
|
valid_tile_keys.push(parent_key);
|
|
tileCovered = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// if we didn't find a parent, look at the children:
|
|
if (!tileCovered) {
|
|
var child_coord = tile_coord.zoomBy(1);
|
|
|
|
// mark everything valid whether or not we have it:
|
|
valid_tile_keys.push(child_coord.toKey());
|
|
child_coord.column += 1;
|
|
valid_tile_keys.push(child_coord.toKey());
|
|
child_coord.row += 1;
|
|
valid_tile_keys.push(child_coord.toKey());
|
|
child_coord.column -= 1;
|
|
valid_tile_keys.push(child_coord.toKey());
|
|
}
|
|
|
|
return valid_tile_keys;
|
|
},
|
|
|
|
tileElementsInLevel: function(level) {
|
|
// this is somewhat future proof, we're looking for DOM elements
|
|
// not necessarily <img> elements
|
|
var tiles = [];
|
|
for (var tile = level.firstChild; tile; tile = tile.nextSibling) {
|
|
if (tile.nodeType == 1) {
|
|
tiles.push(tile);
|
|
}
|
|
}
|
|
return tiles;
|
|
},
|
|
|
|
/**
|
|
* For a given level, adjust visibility as a whole and discard individual
|
|
* tiles based on values in valid_tile_keys from inventoryVisibleTile().
|
|
*/
|
|
adjustVisibleLevel: function(level, zoom, valid_tile_keys) {
|
|
// no tiles for this level yet
|
|
if (!level) return;
|
|
|
|
var scale = 1;
|
|
var theCoord = this.map.coordinate.copy();
|
|
|
|
if (level.childNodes.length > 0) {
|
|
level.style.display = 'block';
|
|
scale = Math.pow(2, this.map.coordinate.zoom - zoom);
|
|
theCoord = theCoord.zoomTo(zoom);
|
|
} else {
|
|
level.style.display = 'none';
|
|
return false;
|
|
}
|
|
|
|
var tileWidth = this.map.tileSize.x * scale;
|
|
var tileHeight = this.map.tileSize.y * scale;
|
|
var center = new MM.Point(this.map.dimensions.x/2, this.map.dimensions.y/2);
|
|
var tiles = this.tileElementsInLevel(level);
|
|
|
|
while (tiles.length) {
|
|
var tile = tiles.pop();
|
|
|
|
if (!valid_tile_keys[tile.id]) {
|
|
this.provider.releaseTile(tile.coord);
|
|
this.requestManager.clearRequest(tile.coord.toKey());
|
|
level.removeChild(tile);
|
|
} else {
|
|
// position tiles
|
|
MM.moveElement(tile, {
|
|
x: Math.round(center.x +
|
|
(tile.coord.column - theCoord.column) * tileWidth),
|
|
y: Math.round(center.y +
|
|
(tile.coord.row - theCoord.row) * tileHeight),
|
|
scale: scale,
|
|
// TODO: pass only scale or only w/h
|
|
width: this.map.tileSize.x,
|
|
height: this.map.tileSize.y
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
createOrGetLevel: function(zoom) {
|
|
if (zoom in this.levels) {
|
|
return this.levels[zoom];
|
|
}
|
|
|
|
var level = document.createElement('div');
|
|
level.id = this.parent.id + '-zoom-' + zoom;
|
|
level.style.cssText = 'position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0; padding: 0;';
|
|
level.style.zIndex = zoom;
|
|
|
|
this.parent.appendChild(level);
|
|
this.levels[zoom] = level;
|
|
|
|
return level;
|
|
},
|
|
|
|
addTileImage: function(key, coord, url) {
|
|
this.requestManager.requestTile(key, coord, url);
|
|
},
|
|
|
|
addTileElement: function(key, coordinate, element) {
|
|
// Expected in draw()
|
|
element.id = key;
|
|
element.coord = coordinate.copy();
|
|
this.positionTile(element);
|
|
},
|
|
|
|
positionTile: function(tile) {
|
|
// position this tile (avoids a full draw() call):
|
|
var theCoord = this.map.coordinate.zoomTo(tile.coord.zoom);
|
|
|
|
// Start tile positioning and prevent drag for modern browsers
|
|
tile.style.cssText = 'position:absolute;-webkit-user-select:none;' +
|
|
'-webkit-user-drag:none;-moz-user-drag:none;-webkit-transform-origin:0 0;' +
|
|
'-moz-transform-origin:0 0;-o-transform-origin:0 0;-ms-transform-origin:0 0;' +
|
|
'width:' + this.map.tileSize.x + 'px; height: ' + this.map.tileSize.y + 'px;';
|
|
|
|
// Prevent drag for IE
|
|
tile.ondragstart = function() { return false; };
|
|
|
|
var scale = Math.pow(2, this.map.coordinate.zoom - tile.coord.zoom);
|
|
|
|
MM.moveElement(tile, {
|
|
x: Math.round((this.map.dimensions.x/2) +
|
|
(tile.coord.column - theCoord.column) * this.map.tileSize.x),
|
|
y: Math.round((this.map.dimensions.y/2) +
|
|
(tile.coord.row - theCoord.row) * this.map.tileSize.y),
|
|
scale: scale,
|
|
// TODO: pass only scale or only w/h
|
|
width: this.map.tileSize.x,
|
|
height: this.map.tileSize.y
|
|
});
|
|
|
|
// add tile to its level
|
|
var theLevel = this.levels[tile.coord.zoom];
|
|
theLevel.appendChild(tile);
|
|
|
|
// Support style transition if available.
|
|
tile.className = 'map-tile-loaded';
|
|
|
|
// ensure the level is visible if it's still the current level
|
|
if (Math.round(this.map.coordinate.zoom) == tile.coord.zoom) {
|
|
theLevel.style.display = 'block';
|
|
}
|
|
|
|
// request a lazy redraw of all levels
|
|
// this will remove tiles that were only visible
|
|
// to cover this tile while it loaded:
|
|
this.requestRedraw();
|
|
},
|
|
|
|
_redrawTimer: undefined,
|
|
|
|
requestRedraw: function() {
|
|
// we'll always draw within 1 second of this request,
|
|
// sometimes faster if there's already a pending redraw
|
|
// this is used when a new tile arrives so that we clear
|
|
// any parent/child tiles that were only being displayed
|
|
// until the tile loads at the right zoom level
|
|
if (!this._redrawTimer) {
|
|
this._redrawTimer = setTimeout(this.getRedraw(), 1000);
|
|
}
|
|
},
|
|
|
|
_redraw: null,
|
|
|
|
getRedraw: function() {
|
|
// let's only create this closure once...
|
|
if (!this._redraw) {
|
|
var theLayer = this;
|
|
this._redraw = function() {
|
|
theLayer.draw();
|
|
theLayer._redrawTimer = 0;
|
|
};
|
|
}
|
|
return this._redraw;
|
|
},
|
|
|
|
setProvider: function(newProvider) {
|
|
var firstProvider = (this.provider === null);
|
|
|
|
// if we already have a provider the we'll need to
|
|
// clear the DOM, cancel requests and redraw
|
|
if (!firstProvider) {
|
|
this.requestManager.clear();
|
|
|
|
for (var name in this.levels) {
|
|
if (this.levels.hasOwnProperty(name)) {
|
|
var level = this.levels[name];
|
|
|
|
while (level.firstChild) {
|
|
this.provider.releaseTile(level.firstChild.coord);
|
|
level.removeChild(level.firstChild);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// first provider or not we'll init/reset some values...
|
|
this.tiles = {};
|
|
|
|
// for later: check geometry of old provider and set a new coordinate center
|
|
// if needed (now? or when?)
|
|
this.provider = newProvider;
|
|
|
|
if (!firstProvider) {
|
|
this.draw();
|
|
}
|
|
},
|
|
|
|
// Enable a layer and show its dom element
|
|
enable: function() {
|
|
this.enabled = true;
|
|
this.parent.style.display = '';
|
|
this.draw();
|
|
},
|
|
|
|
// Disable a layer, don't display in DOM, clear all requests
|
|
disable: function() {
|
|
this.enabled = false;
|
|
this.requestManager.clear();
|
|
this.parent.style.display = 'none';
|
|
},
|
|
|
|
// Remove this layer from the DOM, cancel all of its requests
|
|
// and unbind any callbacks that are bound to it.
|
|
destroy: function() {
|
|
this.requestManager.clear();
|
|
this.requestManager.removeCallback('requestcomplete', this.getTileComplete());
|
|
this.requestManager.removeCallback('requesterror', this.getTileError());
|
|
// TODO: does requestManager need a destroy function too?
|
|
this.provider = null;
|
|
// If this layer was ever attached to the DOM, detach it.
|
|
if (this.parent.parentNode) {
|
|
this.parent.parentNode.removeChild(this.parent);
|
|
}
|
|
this.map = null;
|
|
}
|
|
};
|
|
|
|
// Map
|
|
|
|
// Instance of a map intended for drawing to a div.
|
|
//
|
|
// * `parent` (required DOM element)
|
|
// Can also be an ID of a DOM element
|
|
// * `layerOrLayers` (required MM.Layer or Array of MM.Layers)
|
|
// each one must implement draw(), destroy(), have a .parent DOM element and a .map property
|
|
// (an array of URL templates or MM.MapProviders is also acceptable)
|
|
// * `dimensions` (optional Point)
|
|
// Size of map to create
|
|
// * `eventHandlers` (optional Array)
|
|
// If empty or null MouseHandler will be used
|
|
// Otherwise, each handler will be called with init(map)
|
|
MM.Map = function(parent, layerOrLayers, dimensions, eventHandlers) {
|
|
|
|
if (typeof parent == 'string') {
|
|
parent = document.getElementById(parent);
|
|
if (!parent) {
|
|
throw 'The ID provided to modest maps could not be found.';
|
|
}
|
|
}
|
|
this.parent = parent;
|
|
|
|
// we're no longer adding width and height to parent.style but we still
|
|
// need to enforce padding, overflow and position otherwise everything screws up
|
|
// TODO: maybe console.warn if the current values are bad?
|
|
this.parent.style.padding = '0';
|
|
this.parent.style.overflow = 'hidden';
|
|
|
|
var position = MM.getStyle(this.parent, 'position');
|
|
if (position != 'relative' && position != 'absolute') {
|
|
this.parent.style.position = 'relative';
|
|
}
|
|
|
|
this.layers = [];
|
|
|
|
if (!layerOrLayers) {
|
|
layerOrLayers = [];
|
|
}
|
|
|
|
if (!(layerOrLayers instanceof Array)) {
|
|
layerOrLayers = [ layerOrLayers ];
|
|
}
|
|
|
|
for (var i = 0; i < layerOrLayers.length; i++) {
|
|
this.addLayer(layerOrLayers[i]);
|
|
}
|
|
|
|
// default to Google-y Mercator style maps
|
|
this.projection = new MM.MercatorProjection(0,
|
|
MM.deriveTransformation(-Math.PI, Math.PI, 0, 0,
|
|
Math.PI, Math.PI, 1, 0,
|
|
-Math.PI, -Math.PI, 0, 1));
|
|
this.tileSize = new MM.Point(256, 256);
|
|
|
|
// default 0-18 zoom level
|
|
// with infinite horizontal pan and clamped vertical pan
|
|
this.coordLimits = [
|
|
new MM.Coordinate(0,-Infinity,0), // top left outer
|
|
new MM.Coordinate(1,Infinity,0).zoomTo(18) // bottom right inner
|
|
];
|
|
|
|
// eyes towards null island
|
|
this.coordinate = new MM.Coordinate(0.5, 0.5, 0);
|
|
|
|
// if you don't specify dimensions we assume you want to fill the parent
|
|
// unless the parent has no w/h, in which case we'll still use a default
|
|
if (!dimensions) {
|
|
dimensions = new MM.Point(this.parent.offsetWidth,
|
|
this.parent.offsetHeight);
|
|
this.autoSize = true;
|
|
// use destroy to get rid of this handler from the DOM
|
|
MM.addEvent(window, 'resize', this.windowResize());
|
|
} else {
|
|
this.autoSize = false;
|
|
// don't call setSize here because it calls draw()
|
|
this.parent.style.width = Math.round(dimensions.x) + 'px';
|
|
this.parent.style.height = Math.round(dimensions.y) + 'px';
|
|
}
|
|
this.dimensions = dimensions;
|
|
|
|
this.callbackManager = new MM.CallbackManager(this, [
|
|
'zoomed',
|
|
'panned',
|
|
'centered',
|
|
'extentset',
|
|
'resized',
|
|
'drawn'
|
|
]);
|
|
|
|
// set up handlers last so that all required attributes/functions are in place if needed
|
|
if (eventHandlers === undefined) {
|
|
this.eventHandlers = [
|
|
MM.MouseHandler().init(this),
|
|
MM.TouchHandler().init(this)
|
|
];
|
|
} else {
|
|
this.eventHandlers = eventHandlers;
|
|
if (eventHandlers instanceof Array) {
|
|
for (var j = 0; j < eventHandlers.length; j++) {
|
|
eventHandlers[j].init(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
MM.Map.prototype = {
|
|
|
|
parent: null, // DOM Element
|
|
dimensions: null, // MM.Point with x/y size of parent element
|
|
|
|
projection: null, // MM.Projection of first known layer
|
|
coordinate: null, // Center of map MM.Coordinate with row/column/zoom
|
|
tileSize: null, // MM.Point with x/y size of tiles
|
|
|
|
coordLimits: null, // Array of [ topLeftOuter, bottomLeftInner ] MM.Coordinates
|
|
|
|
layers: null, // Array of MM.Layer (interface = .draw(), .destroy(), .parent and .map)
|
|
|
|
callbackManager: null, // MM.CallbackManager, handles map events
|
|
|
|
eventHandlers: null, // Array of interaction handlers, just a MM.MouseHandler by default
|
|
|
|
autoSize: null, // Boolean, true if we have a window resize listener
|
|
|
|
toString: function() {
|
|
return 'Map(#' + this.parent.id + ')';
|
|
},
|
|
|
|
// callbacks...
|
|
|
|
addCallback: function(event, callback) {
|
|
this.callbackManager.addCallback(event, callback);
|
|
return this;
|
|
},
|
|
|
|
removeCallback: function(event, callback) {
|
|
this.callbackManager.removeCallback(event, callback);
|
|
return this;
|
|
},
|
|
|
|
dispatchCallback: function(event, message) {
|
|
this.callbackManager.dispatchCallback(event, message);
|
|
return this;
|
|
},
|
|
|
|
windowResize: function() {
|
|
if (!this._windowResize) {
|
|
var theMap = this;
|
|
this._windowResize = function(event) {
|
|
// don't call setSize here because it sets parent.style.width/height
|
|
// and setting the height breaks percentages and default styles
|
|
theMap.dimensions = new MM.Point(theMap.parent.offsetWidth, theMap.parent.offsetHeight);
|
|
theMap.draw();
|
|
theMap.dispatchCallback('resized', [theMap.dimensions]);
|
|
};
|
|
}
|
|
return this._windowResize;
|
|
},
|
|
|
|
// A convenience function to restrict interactive zoom ranges.
|
|
// (you should also adjust map provider to restrict which tiles get loaded,
|
|
// or modify map.coordLimits and provider.tileLimits for finer control)
|
|
setZoomRange: function(minZoom, maxZoom) {
|
|
this.coordLimits[0] = this.coordLimits[0].zoomTo(minZoom);
|
|
this.coordLimits[1] = this.coordLimits[1].zoomTo(maxZoom);
|
|
return this;
|
|
},
|
|
|
|
// zooming
|
|
zoomBy: function(zoomOffset) {
|
|
this.coordinate = this.enforceLimits(this.coordinate.zoomBy(zoomOffset));
|
|
MM.getFrame(this.getRedraw());
|
|
this.dispatchCallback('zoomed', zoomOffset);
|
|
return this;
|
|
},
|
|
|
|
zoomIn: function() { return this.zoomBy(1); },
|
|
zoomOut: function() { return this.zoomBy(-1); },
|
|
setZoom: function(z) { return this.zoomBy(z - this.coordinate.zoom); },
|
|
|
|
zoomByAbout: function(zoomOffset, point) {
|
|
var location = this.pointLocation(point);
|
|
|
|
this.coordinate = this.enforceLimits(this.coordinate.zoomBy(zoomOffset));
|
|
var newPoint = this.locationPoint(location);
|
|
|
|
this.dispatchCallback('zoomed', zoomOffset);
|
|
return this.panBy(point.x - newPoint.x, point.y - newPoint.y);
|
|
},
|
|
|
|
// panning
|
|
panBy: function(dx, dy) {
|
|
this.coordinate.column -= dx / this.tileSize.x;
|
|
this.coordinate.row -= dy / this.tileSize.y;
|
|
|
|
this.coordinate = this.enforceLimits(this.coordinate);
|
|
|
|
// Defer until the browser is ready to draw.
|
|
MM.getFrame(this.getRedraw());
|
|
this.dispatchCallback('panned', [dx, dy]);
|
|
return this;
|
|
},
|
|
|
|
panLeft: function() { return this.panBy(100, 0); },
|
|
panRight: function() { return this.panBy(-100, 0); },
|
|
panDown: function() { return this.panBy(0, -100); },
|
|
panUp: function() { return this.panBy(0, 100); },
|
|
|
|
// positioning
|
|
setCenter: function(location) {
|
|
return this.setCenterZoom(location, this.coordinate.zoom);
|
|
},
|
|
|
|
setCenterZoom: function(location, zoom) {
|
|
this.coordinate = this.projection.locationCoordinate(location).zoomTo(parseFloat(zoom) || 0);
|
|
MM.getFrame(this.getRedraw());
|
|
this.dispatchCallback('centered', [location, zoom]);
|
|
return this;
|
|
},
|
|
|
|
extentCoordinate: function(locations, precise) {
|
|
// coerce locations to an array if it's a Extent instance
|
|
if (locations instanceof MM.Extent) {
|
|
locations = locations.toArray();
|
|
}
|
|
|
|
var TL, BR;
|
|
for (var i = 0; i < locations.length; i++) {
|
|
var coordinate = this.projection.locationCoordinate(locations[i]);
|
|
if (TL) {
|
|
TL.row = Math.min(TL.row, coordinate.row);
|
|
TL.column = Math.min(TL.column, coordinate.column);
|
|
TL.zoom = Math.min(TL.zoom, coordinate.zoom);
|
|
BR.row = Math.max(BR.row, coordinate.row);
|
|
BR.column = Math.max(BR.column, coordinate.column);
|
|
BR.zoom = Math.max(BR.zoom, coordinate.zoom);
|
|
}
|
|
else {
|
|
TL = coordinate.copy();
|
|
BR = coordinate.copy();
|
|
}
|
|
}
|
|
|
|
var width = this.dimensions.x + 1;
|
|
var height = this.dimensions.y + 1;
|
|
|
|
// multiplication factor between horizontal span and map width
|
|
var hFactor = (BR.column - TL.column) / (width / this.tileSize.x);
|
|
|
|
// multiplication factor expressed as base-2 logarithm, for zoom difference
|
|
var hZoomDiff = Math.log(hFactor) / Math.log(2);
|
|
|
|
// possible horizontal zoom to fit geographical extent in map width
|
|
var hPossibleZoom = TL.zoom - (precise ? hZoomDiff : Math.ceil(hZoomDiff));
|
|
|
|
// multiplication factor between vertical span and map height
|
|
var vFactor = (BR.row - TL.row) / (height / this.tileSize.y);
|
|
|
|
// multiplication factor expressed as base-2 logarithm, for zoom difference
|
|
var vZoomDiff = Math.log(vFactor) / Math.log(2);
|
|
|
|
// possible vertical zoom to fit geographical extent in map height
|
|
var vPossibleZoom = TL.zoom - (precise ? vZoomDiff : Math.ceil(vZoomDiff));
|
|
|
|
// initial zoom to fit extent vertically and horizontally
|
|
var initZoom = Math.min(hPossibleZoom, vPossibleZoom);
|
|
|
|
// additionally, make sure it's not outside the boundaries set by map limits
|
|
initZoom = Math.min(initZoom, this.coordLimits[1].zoom);
|
|
initZoom = Math.max(initZoom, this.coordLimits[0].zoom);
|
|
|
|
// coordinate of extent center
|
|
var centerRow = (TL.row + BR.row) / 2;
|
|
var centerColumn = (TL.column + BR.column) / 2;
|
|
var centerZoom = TL.zoom;
|
|
return new MM.Coordinate(centerRow, centerColumn, centerZoom).zoomTo(initZoom);
|
|
},
|
|
|
|
setExtent: function(locations, precise) {
|
|
this.coordinate = this.extentCoordinate(locations, precise);
|
|
this.draw(); // draw calls enforceLimits
|
|
// (if you switch to getFrame, call enforceLimits first)
|
|
|
|
this.dispatchCallback('extentset', locations);
|
|
return this;
|
|
},
|
|
|
|
// Resize the map's container `<div>`, redrawing the map and triggering
|
|
// `resized` to make sure that the map's presentation is still correct.
|
|
setSize: function(dimensions) {
|
|
// Ensure that, whether a raw object or a Point object is passed,
|
|
// this.dimensions will be a Point.
|
|
this.dimensions = new MM.Point(dimensions.x, dimensions.y);
|
|
this.parent.style.width = Math.round(this.dimensions.x) + 'px';
|
|
this.parent.style.height = Math.round(this.dimensions.y) + 'px';
|
|
if (this.autoSize) {
|
|
MM.removeEvent(window, 'resize', this.windowResize());
|
|
this.autoSize = false;
|
|
}
|
|
this.draw(); // draw calls enforceLimits
|
|
// (if you switch to getFrame, call enforceLimits first)
|
|
this.dispatchCallback('resized', this.dimensions);
|
|
return this;
|
|
},
|
|
|
|
// projecting points on and off screen
|
|
coordinatePoint: function(coord) {
|
|
// Return an x, y point on the map image for a given coordinate.
|
|
if (coord.zoom != this.coordinate.zoom) {
|
|
coord = coord.zoomTo(this.coordinate.zoom);
|
|
}
|
|
|
|
// distance from the center of the map
|
|
var point = new MM.Point(this.dimensions.x / 2, this.dimensions.y / 2);
|
|
point.x += this.tileSize.x * (coord.column - this.coordinate.column);
|
|
point.y += this.tileSize.y * (coord.row - this.coordinate.row);
|
|
|
|
return point;
|
|
},
|
|
|
|
// Get a `MM.Coordinate` from an `MM.Point` - returns a new tile-like object
|
|
// from a screen point.
|
|
pointCoordinate: function(point) {
|
|
// new point coordinate reflecting distance from map center, in tile widths
|
|
var coord = this.coordinate.copy();
|
|
coord.column += (point.x - this.dimensions.x / 2) / this.tileSize.x;
|
|
coord.row += (point.y - this.dimensions.y / 2) / this.tileSize.y;
|
|
|
|
return coord;
|
|
},
|
|
|
|
// Return an MM.Coordinate (row,col,zoom) for an MM.Location (lat,lon).
|
|
locationCoordinate: function(location) {
|
|
return this.projection.locationCoordinate(location);
|
|
},
|
|
|
|
// Return an MM.Location (lat,lon) for an MM.Coordinate (row,col,zoom).
|
|
coordinateLocation: function(coordinate) {
|
|
return this.projection.coordinateLocation(coordinate);
|
|
},
|
|
|
|
// Return an x, y point on the map image for a given geographical location.
|
|
locationPoint: function(location) {
|
|
return this.coordinatePoint(this.locationCoordinate(location));
|
|
},
|
|
|
|
// Return a geographical location on the map image for a given x, y point.
|
|
pointLocation: function(point) {
|
|
return this.coordinateLocation(this.pointCoordinate(point));
|
|
},
|
|
|
|
// inspecting
|
|
getExtent: function() {
|
|
return new MM.Extent(
|
|
this.pointLocation(new MM.Point(0, 0)),
|
|
this.pointLocation(this.dimensions)
|
|
);
|
|
},
|
|
|
|
extent: function(locations, precise) {
|
|
if (locations) {
|
|
return this.setExtent(locations, precise);
|
|
} else {
|
|
return this.getExtent();
|
|
}
|
|
},
|
|
|
|
// Get the current centerpoint of the map, returning a `Location`
|
|
getCenter: function() {
|
|
return this.projection.coordinateLocation(this.coordinate);
|
|
},
|
|
|
|
center: function(location) {
|
|
if (location) {
|
|
return this.setCenter(location);
|
|
} else {
|
|
return this.getCenter();
|
|
}
|
|
},
|
|
|
|
// Get the current zoom level of the map, returning a number
|
|
getZoom: function() {
|
|
return this.coordinate.zoom;
|
|
},
|
|
|
|
zoom: function(zoom) {
|
|
if (zoom !== undefined) {
|
|
return this.setZoom(zoom);
|
|
} else {
|
|
return this.getZoom();
|
|
}
|
|
},
|
|
|
|
// return a copy of the layers array
|
|
getLayers: function() {
|
|
return this.layers.slice();
|
|
},
|
|
|
|
// return the first layer with given name
|
|
getLayer: function(name) {
|
|
for (var i = 0; i < this.layers.length; i++) {
|
|
if (name == this.layers[i].name)
|
|
return this.layers[i];
|
|
}
|
|
},
|
|
|
|
// return the layer at the given index
|
|
getLayerAt: function(index) {
|
|
return this.layers[index];
|
|
},
|
|
|
|
// put the given layer on top of all the others
|
|
// Since this is called for the first layer, which is by definition
|
|
// added before the map has a valid `coordinate`, we request
|
|
// a redraw only if the map has a center coordinate.
|
|
addLayer: function(layer) {
|
|
this.layers.push(layer);
|
|
this.parent.appendChild(layer.parent);
|
|
layer.map = this; // TODO: remove map property from MM.Layer?
|
|
if (this.coordinate) {
|
|
MM.getFrame(this.getRedraw());
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// find the given layer and remove it
|
|
removeLayer: function(layer) {
|
|
for (var i = 0; i < this.layers.length; i++) {
|
|
if (layer == this.layers[i] || layer == this.layers[i].name) {
|
|
this.removeLayerAt(i);
|
|
break;
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// replace the current layer at the given index with the given layer
|
|
setLayerAt: function(index, layer) {
|
|
|
|
if (index < 0 || index >= this.layers.length) {
|
|
throw new Error('invalid index in setLayerAt(): ' + index);
|
|
}
|
|
|
|
if (this.layers[index] != layer) {
|
|
|
|
// clear existing layer at this index
|
|
if (index < this.layers.length) {
|
|
var other = this.layers[index];
|
|
this.parent.insertBefore(layer.parent, other.parent);
|
|
other.destroy();
|
|
} else {
|
|
// Or if this will be the last layer, it can be simply appended
|
|
this.parent.appendChild(layer.parent);
|
|
}
|
|
|
|
this.layers[index] = layer;
|
|
layer.map = this; // TODO: remove map property from MM.Layer
|
|
|
|
MM.getFrame(this.getRedraw());
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
// put the given layer at the given index, moving others if necessary
|
|
insertLayerAt: function(index, layer) {
|
|
|
|
if (index < 0 || index > this.layers.length) {
|
|
throw new Error('invalid index in insertLayerAt(): ' + index);
|
|
}
|
|
|
|
if (index == this.layers.length) {
|
|
// it just gets tacked on to the end
|
|
this.layers.push(layer);
|
|
this.parent.appendChild(layer.parent);
|
|
} else {
|
|
// it needs to get slipped in amongst the others
|
|
var other = this.layers[index];
|
|
this.parent.insertBefore(layer.parent, other.parent);
|
|
this.layers.splice(index, 0, layer);
|
|
}
|
|
|
|
layer.map = this; // TODO: remove map property from MM.Layer
|
|
|
|
MM.getFrame(this.getRedraw());
|
|
|
|
return this;
|
|
},
|
|
|
|
// remove the layer at the given index, call .destroy() on the layer
|
|
removeLayerAt: function(index) {
|
|
if (index < 0 || index >= this.layers.length) {
|
|
throw new Error('invalid index in removeLayer(): ' + index);
|
|
}
|
|
|
|
// gone baby gone.
|
|
var old = this.layers[index];
|
|
this.layers.splice(index, 1);
|
|
old.destroy();
|
|
|
|
return this;
|
|
},
|
|
|
|
// switch the stacking order of two layers, by index
|
|
swapLayersAt: function(i, j) {
|
|
|
|
if (i < 0 || i >= this.layers.length || j < 0 || j >= this.layers.length) {
|
|
throw new Error('invalid index in swapLayersAt(): ' + index);
|
|
}
|
|
|
|
var layer1 = this.layers[i],
|
|
layer2 = this.layers[j],
|
|
dummy = document.createElement('div');
|
|
|
|
// kick layer2 out, replace it with the dummy.
|
|
this.parent.replaceChild(dummy, layer2.parent);
|
|
|
|
// put layer2 back in and kick layer1 out
|
|
this.parent.replaceChild(layer2.parent, layer1.parent);
|
|
|
|
// put layer1 back in and ditch the dummy
|
|
this.parent.replaceChild(layer1.parent, dummy);
|
|
|
|
// now do it to the layers array
|
|
this.layers[i] = layer2;
|
|
this.layers[j] = layer1;
|
|
|
|
return this;
|
|
},
|
|
|
|
// Enable and disable layers.
|
|
// Disabled layers are not displayed, are not drawn, and do not request
|
|
// tiles. They do maintain their layer index on the map.
|
|
enableLayer: function(name) {
|
|
var l = this.getLayer(name);
|
|
if (l) l.enable();
|
|
return this;
|
|
},
|
|
|
|
enableLayerAt: function(index) {
|
|
var l = this.getLayerAt(index);
|
|
if (l) l.enable();
|
|
return this;
|
|
},
|
|
|
|
disableLayer: function(name) {
|
|
var l = this.getLayer(name);
|
|
if (l) l.disable();
|
|
return this;
|
|
},
|
|
|
|
disableLayerAt: function(index) {
|
|
var l = this.getLayerAt(index);
|
|
if (l) l.disable();
|
|
return this;
|
|
},
|
|
|
|
|
|
// limits
|
|
|
|
enforceZoomLimits: function(coord) {
|
|
var limits = this.coordLimits;
|
|
if (limits) {
|
|
// clamp zoom level:
|
|
var minZoom = limits[0].zoom;
|
|
var maxZoom = limits[1].zoom;
|
|
if (coord.zoom < minZoom) {
|
|
coord = coord.zoomTo(minZoom);
|
|
}
|
|
else if (coord.zoom > maxZoom) {
|
|
coord = coord.zoomTo(maxZoom);
|
|
}
|
|
}
|
|
return coord;
|
|
},
|
|
|
|
enforcePanLimits: function(coord) {
|
|
|
|
if (this.coordLimits) {
|
|
|
|
coord = coord.copy();
|
|
|
|
// clamp pan:
|
|
var topLeftLimit = this.coordLimits[0].zoomTo(coord.zoom);
|
|
var bottomRightLimit = this.coordLimits[1].zoomTo(coord.zoom);
|
|
var currentTopLeft = this.pointCoordinate(new MM.Point(0, 0))
|
|
.zoomTo(coord.zoom);
|
|
var currentBottomRight = this.pointCoordinate(this.dimensions)
|
|
.zoomTo(coord.zoom);
|
|
|
|
// this handles infinite limits:
|
|
// (Infinity - Infinity) is Nan
|
|
// NaN is never less than anything
|
|
if (bottomRightLimit.row - topLeftLimit.row <
|
|
currentBottomRight.row - currentTopLeft.row) {
|
|
// if the limit is smaller than the current view center it
|
|
coord.row = (bottomRightLimit.row + topLeftLimit.row) / 2;
|
|
} else {
|
|
if (currentTopLeft.row < topLeftLimit.row) {
|
|
coord.row += topLeftLimit.row - currentTopLeft.row;
|
|
} else if (currentBottomRight.row > bottomRightLimit.row) {
|
|
coord.row -= currentBottomRight.row - bottomRightLimit.row;
|
|
}
|
|
}
|
|
if (bottomRightLimit.column - topLeftLimit.column <
|
|
currentBottomRight.column - currentTopLeft.column) {
|
|
// if the limit is smaller than the current view, center it
|
|
coord.column = (bottomRightLimit.column + topLeftLimit.column) / 2;
|
|
} else {
|
|
if (currentTopLeft.column < topLeftLimit.column) {
|
|
coord.column += topLeftLimit.column - currentTopLeft.column;
|
|
} else if (currentBottomRight.column > bottomRightLimit.column) {
|
|
coord.column -= currentBottomRight.column - bottomRightLimit.column;
|
|
}
|
|
}
|
|
}
|
|
|
|
return coord;
|
|
},
|
|
|
|
// Prevent accidentally navigating outside the `coordLimits` of the map.
|
|
enforceLimits: function(coord) {
|
|
return this.enforcePanLimits(this.enforceZoomLimits(coord));
|
|
},
|
|
|
|
// rendering
|
|
|
|
// Redraw the tiles on the map, reusing existing tiles.
|
|
draw: function() {
|
|
// make sure we're not too far in or out:
|
|
this.coordinate = this.enforceLimits(this.coordinate);
|
|
|
|
// if we don't have dimensions, check the parent size
|
|
if (this.dimensions.x <= 0 || this.dimensions.y <= 0) {
|
|
if (this.autoSize) {
|
|
// maybe the parent size has changed?
|
|
var w = this.parent.offsetWidth,
|
|
h = this.parent.offsetHeight;
|
|
this.dimensions = new MM.Point(w,h);
|
|
if (w <= 0 || h <= 0) {
|
|
return;
|
|
}
|
|
} else {
|
|
// the issue can only be corrected with setSize
|
|
return;
|
|
}
|
|
}
|
|
|
|
// draw layers one by one
|
|
for(var i = 0; i < this.layers.length; i++) {
|
|
this.layers[i].draw();
|
|
}
|
|
|
|
this.dispatchCallback('drawn');
|
|
},
|
|
|
|
_redrawTimer: undefined,
|
|
|
|
requestRedraw: function() {
|
|
// we'll always draw within 1 second of this request,
|
|
// sometimes faster if there's already a pending redraw
|
|
// this is used when a new tile arrives so that we clear
|
|
// any parent/child tiles that were only being displayed
|
|
// until the tile loads at the right zoom level
|
|
if (!this._redrawTimer) {
|
|
this._redrawTimer = setTimeout(this.getRedraw(), 1000);
|
|
}
|
|
},
|
|
|
|
_redraw: null,
|
|
|
|
getRedraw: function() {
|
|
// let's only create this closure once...
|
|
if (!this._redraw) {
|
|
var theMap = this;
|
|
this._redraw = function() {
|
|
theMap.draw();
|
|
theMap._redrawTimer = 0;
|
|
};
|
|
}
|
|
return this._redraw;
|
|
},
|
|
|
|
// Attempts to destroy all attachment a map has to a page
|
|
// and clear its memory usage.
|
|
destroy: function() {
|
|
for (var j = 0; j < this.layers.length; j++) {
|
|
this.layers[j].destroy();
|
|
}
|
|
this.layers = [];
|
|
this.projection = null;
|
|
for (var i = 0; i < this.eventHandlers.length; i++) {
|
|
this.eventHandlers[i].remove();
|
|
}
|
|
if (this.autoSize) {
|
|
MM.removeEvent(window, 'resize', this.windowResize());
|
|
}
|
|
}
|
|
};
|
|
// Instance of a map intended for drawing to a div.
|
|
//
|
|
// * `parent` (required DOM element)
|
|
// Can also be an ID of a DOM element
|
|
// * `provider` (required MM.MapProvider or URL template)
|
|
// * `location` (required MM.Location)
|
|
// Location for map to show
|
|
// * `zoom` (required number)
|
|
MM.mapByCenterZoom = function(parent, layerish, location, zoom) {
|
|
var layer = MM.coerceLayer(layerish),
|
|
map = new MM.Map(parent, layer, false);
|
|
map.setCenterZoom(location, zoom).draw();
|
|
return map;
|
|
};
|
|
|
|
// Instance of a map intended for drawing to a div.
|
|
//
|
|
// * `parent` (required DOM element)
|
|
// Can also be an ID of a DOM element
|
|
// * `provider` (required MM.MapProvider or URL template)
|
|
// * `locationA` (required MM.Location)
|
|
// Location of one map corner
|
|
// * `locationB` (required MM.Location)
|
|
// Location of other map corner
|
|
MM.mapByExtent = function(parent, layerish, locationA, locationB) {
|
|
var layer = MM.coerceLayer(layerish),
|
|
map = new MM.Map(parent, layer, false);
|
|
map.setExtent([locationA, locationB]).draw();
|
|
return map;
|
|
};
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
Point: MM.Point,
|
|
Projection: MM.Projection,
|
|
MercatorProjection: MM.MercatorProjection,
|
|
LinearProjection: MM.LinearProjection,
|
|
Transformation: MM.Transformation,
|
|
Location: MM.Location,
|
|
MapProvider: MM.MapProvider,
|
|
Template: MM.Template,
|
|
Coordinate: MM.Coordinate,
|
|
deriveTransformation: MM.deriveTransformation
|
|
};
|
|
}
|
|
})(MM);
|
|
// Copyright Google Inc.
|
|
// Licensed under the Apache Licence Version 2.0
|
|
// Autogenerated at Tue Oct 11 13:36:46 EDT 2011
|
|
// @provides html4
|
|
var html4 = {};
|
|
html4.atype = {
|
|
NONE: 0,
|
|
URI: 1,
|
|
URI_FRAGMENT: 11,
|
|
SCRIPT: 2,
|
|
STYLE: 3,
|
|
ID: 4,
|
|
IDREF: 5,
|
|
IDREFS: 6,
|
|
GLOBAL_NAME: 7,
|
|
LOCAL_NAME: 8,
|
|
CLASSES: 9,
|
|
FRAME_TARGET: 10
|
|
};
|
|
html4.ATTRIBS = {
|
|
'*::class': 9,
|
|
'*::dir': 0,
|
|
'*::id': 4,
|
|
'*::lang': 0,
|
|
'*::onclick': 2,
|
|
'*::ondblclick': 2,
|
|
'*::onkeydown': 2,
|
|
'*::onkeypress': 2,
|
|
'*::onkeyup': 2,
|
|
'*::onload': 2,
|
|
'*::onmousedown': 2,
|
|
'*::onmousemove': 2,
|
|
'*::onmouseout': 2,
|
|
'*::onmouseover': 2,
|
|
'*::onmouseup': 2,
|
|
'*::style': 3,
|
|
'*::title': 0,
|
|
'a::accesskey': 0,
|
|
'a::coords': 0,
|
|
'a::href': 1,
|
|
'a::hreflang': 0,
|
|
'a::name': 7,
|
|
'a::onblur': 2,
|
|
'a::onfocus': 2,
|
|
'a::rel': 0,
|
|
'a::rev': 0,
|
|
'a::shape': 0,
|
|
'a::tabindex': 0,
|
|
'a::target': 10,
|
|
'a::type': 0,
|
|
'area::accesskey': 0,
|
|
'area::alt': 0,
|
|
'area::coords': 0,
|
|
'area::href': 1,
|
|
'area::nohref': 0,
|
|
'area::onblur': 2,
|
|
'area::onfocus': 2,
|
|
'area::shape': 0,
|
|
'area::tabindex': 0,
|
|
'area::target': 10,
|
|
'bdo::dir': 0,
|
|
'blockquote::cite': 1,
|
|
'br::clear': 0,
|
|
'button::accesskey': 0,
|
|
'button::disabled': 0,
|
|
'button::name': 8,
|
|
'button::onblur': 2,
|
|
'button::onfocus': 2,
|
|
'button::tabindex': 0,
|
|
'button::type': 0,
|
|
'button::value': 0,
|
|
'canvas::height': 0,
|
|
'canvas::width': 0,
|
|
'caption::align': 0,
|
|
'col::align': 0,
|
|
'col::char': 0,
|
|
'col::charoff': 0,
|
|
'col::span': 0,
|
|
'col::valign': 0,
|
|
'col::width': 0,
|
|
'colgroup::align': 0,
|
|
'colgroup::char': 0,
|
|
'colgroup::charoff': 0,
|
|
'colgroup::span': 0,
|
|
'colgroup::valign': 0,
|
|
'colgroup::width': 0,
|
|
'del::cite': 1,
|
|
'del::datetime': 0,
|
|
'dir::compact': 0,
|
|
'div::align': 0,
|
|
'dl::compact': 0,
|
|
'font::color': 0,
|
|
'font::face': 0,
|
|
'font::size': 0,
|
|
'form::accept': 0,
|
|
'form::action': 1,
|
|
'form::autocomplete': 0,
|
|
'form::enctype': 0,
|
|
'form::method': 0,
|
|
'form::name': 7,
|
|
'form::onreset': 2,
|
|
'form::onsubmit': 2,
|
|
'form::target': 10,
|
|
'h1::align': 0,
|
|
'h2::align': 0,
|
|
'h3::align': 0,
|
|
'h4::align': 0,
|
|
'h5::align': 0,
|
|
'h6::align': 0,
|
|
'hr::align': 0,
|
|
'hr::noshade': 0,
|
|
'hr::size': 0,
|
|
'hr::width': 0,
|
|
'iframe::align': 0,
|
|
'iframe::frameborder': 0,
|
|
'iframe::height': 0,
|
|
'iframe::marginheight': 0,
|
|
'iframe::marginwidth': 0,
|
|
'iframe::width': 0,
|
|
'img::align': 0,
|
|
'img::alt': 0,
|
|
'img::border': 0,
|
|
'img::height': 0,
|
|
'img::hspace': 0,
|
|
'img::ismap': 0,
|
|
'img::name': 7,
|
|
'img::src': 1,
|
|
'img::usemap': 11,
|
|
'img::vspace': 0,
|
|
'img::width': 0,
|
|
'input::accept': 0,
|
|
'input::accesskey': 0,
|
|
'input::align': 0,
|
|
'input::alt': 0,
|
|
'input::autocomplete': 0,
|
|
'input::checked': 0,
|
|
'input::disabled': 0,
|
|
'input::ismap': 0,
|
|
'input::maxlength': 0,
|
|
'input::name': 8,
|
|
'input::onblur': 2,
|
|
'input::onchange': 2,
|
|
'input::onfocus': 2,
|
|
'input::onselect': 2,
|
|
'input::readonly': 0,
|
|
'input::size': 0,
|
|
'input::src': 1,
|
|
'input::tabindex': 0,
|
|
'input::type': 0,
|
|
'input::usemap': 11,
|
|
'input::value': 0,
|
|
'ins::cite': 1,
|
|
'ins::datetime': 0,
|
|
'label::accesskey': 0,
|
|
'label::for': 5,
|
|
'label::onblur': 2,
|
|
'label::onfocus': 2,
|
|
'legend::accesskey': 0,
|
|
'legend::align': 0,
|
|
'li::type': 0,
|
|
'li::value': 0,
|
|
'map::name': 7,
|
|
'menu::compact': 0,
|
|
'ol::compact': 0,
|
|
'ol::start': 0,
|
|
'ol::type': 0,
|
|
'optgroup::disabled': 0,
|
|
'optgroup::label': 0,
|
|
'option::disabled': 0,
|
|
'option::label': 0,
|
|
'option::selected': 0,
|
|
'option::value': 0,
|
|
'p::align': 0,
|
|
'pre::width': 0,
|
|
'q::cite': 1,
|
|
'select::disabled': 0,
|
|
'select::multiple': 0,
|
|
'select::name': 8,
|
|
'select::onblur': 2,
|
|
'select::onchange': 2,
|
|
'select::onfocus': 2,
|
|
'select::size': 0,
|
|
'select::tabindex': 0,
|
|
'table::align': 0,
|
|
'table::bgcolor': 0,
|
|
'table::border': 0,
|
|
'table::cellpadding': 0,
|
|
'table::cellspacing': 0,
|
|
'table::frame': 0,
|
|
'table::rules': 0,
|
|
'table::summary': 0,
|
|
'table::width': 0,
|
|
'tbody::align': 0,
|
|
'tbody::char': 0,
|
|
'tbody::charoff': 0,
|
|
'tbody::valign': 0,
|
|
'td::abbr': 0,
|
|
'td::align': 0,
|
|
'td::axis': 0,
|
|
'td::bgcolor': 0,
|
|
'td::char': 0,
|
|
'td::charoff': 0,
|
|
'td::colspan': 0,
|
|
'td::headers': 6,
|
|
'td::height': 0,
|
|
'td::nowrap': 0,
|
|
'td::rowspan': 0,
|
|
'td::scope': 0,
|
|
'td::valign': 0,
|
|
'td::width': 0,
|
|
'textarea::accesskey': 0,
|
|
'textarea::cols': 0,
|
|
'textarea::disabled': 0,
|
|
'textarea::name': 8,
|
|
'textarea::onblur': 2,
|
|
'textarea::onchange': 2,
|
|
'textarea::onfocus': 2,
|
|
'textarea::onselect': 2,
|
|
'textarea::readonly': 0,
|
|
'textarea::rows': 0,
|
|
'textarea::tabindex': 0,
|
|
'tfoot::align': 0,
|
|
'tfoot::char': 0,
|
|
'tfoot::charoff': 0,
|
|
'tfoot::valign': 0,
|
|
'th::abbr': 0,
|
|
'th::align': 0,
|
|
'th::axis': 0,
|
|
'th::bgcolor': 0,
|
|
'th::char': 0,
|
|
'th::charoff': 0,
|
|
'th::colspan': 0,
|
|
'th::headers': 6,
|
|
'th::height': 0,
|
|
'th::nowrap': 0,
|
|
'th::rowspan': 0,
|
|
'th::scope': 0,
|
|
'th::valign': 0,
|
|
'th::width': 0,
|
|
'thead::align': 0,
|
|
'thead::char': 0,
|
|
'thead::charoff': 0,
|
|
'thead::valign': 0,
|
|
'tr::align': 0,
|
|
'tr::bgcolor': 0,
|
|
'tr::char': 0,
|
|
'tr::charoff': 0,
|
|
'tr::valign': 0,
|
|
'ul::compact': 0,
|
|
'ul::type': 0
|
|
};
|
|
html4.eflags = {
|
|
OPTIONAL_ENDTAG: 1,
|
|
EMPTY: 2,
|
|
CDATA: 4,
|
|
RCDATA: 8,
|
|
UNSAFE: 16,
|
|
FOLDABLE: 32,
|
|
SCRIPT: 64,
|
|
STYLE: 128
|
|
};
|
|
html4.ELEMENTS = {
|
|
'a': 0,
|
|
'abbr': 0,
|
|
'acronym': 0,
|
|
'address': 0,
|
|
'applet': 16,
|
|
'area': 2,
|
|
'b': 0,
|
|
'base': 18,
|
|
'basefont': 18,
|
|
'bdo': 0,
|
|
'big': 0,
|
|
'blockquote': 0,
|
|
'body': 49,
|
|
'br': 2,
|
|
'button': 0,
|
|
'canvas': 0,
|
|
'caption': 0,
|
|
'center': 0,
|
|
'cite': 0,
|
|
'code': 0,
|
|
'col': 2,
|
|
'colgroup': 1,
|
|
'dd': 1,
|
|
'del': 0,
|
|
'dfn': 0,
|
|
'dir': 0,
|
|
'div': 0,
|
|
'dl': 0,
|
|
'dt': 1,
|
|
'em': 0,
|
|
'fieldset': 0,
|
|
'font': 0,
|
|
'form': 0,
|
|
'frame': 18,
|
|
'frameset': 16,
|
|
'h1': 0,
|
|
'h2': 0,
|
|
'h3': 0,
|
|
'h4': 0,
|
|
'h5': 0,
|
|
'h6': 0,
|
|
'head': 49,
|
|
'hr': 2,
|
|
'html': 49,
|
|
'i': 0,
|
|
'iframe': 4,
|
|
'img': 2,
|
|
'input': 2,
|
|
'ins': 0,
|
|
'isindex': 18,
|
|
'kbd': 0,
|
|
'label': 0,
|
|
'legend': 0,
|
|
'li': 1,
|
|
'link': 18,
|
|
'map': 0,
|
|
'menu': 0,
|
|
'meta': 18,
|
|
'nobr': 0,
|
|
'noembed': 4,
|
|
'noframes': 20,
|
|
'noscript': 20,
|
|
'object': 16,
|
|
'ol': 0,
|
|
'optgroup': 0,
|
|
'option': 1,
|
|
'p': 1,
|
|
'param': 18,
|
|
'pre': 0,
|
|
'q': 0,
|
|
's': 0,
|
|
'samp': 0,
|
|
'script': 84,
|
|
'select': 0,
|
|
'small': 0,
|
|
'span': 0,
|
|
'strike': 0,
|
|
'strong': 0,
|
|
'style': 148,
|
|
'sub': 0,
|
|
'sup': 0,
|
|
'table': 0,
|
|
'tbody': 1,
|
|
'td': 1,
|
|
'textarea': 8,
|
|
'tfoot': 1,
|
|
'th': 1,
|
|
'thead': 1,
|
|
'title': 24,
|
|
'tr': 1,
|
|
'tt': 0,
|
|
'u': 0,
|
|
'ul': 0,
|
|
'var': 0
|
|
};
|
|
html4.ueffects = {
|
|
NOT_LOADED: 0,
|
|
SAME_DOCUMENT: 1,
|
|
NEW_DOCUMENT: 2
|
|
};
|
|
html4.URIEFFECTS = {
|
|
'a::href': 2,
|
|
'area::href': 2,
|
|
'blockquote::cite': 0,
|
|
'body::background': 1,
|
|
'del::cite': 0,
|
|
'form::action': 2,
|
|
'img::src': 1,
|
|
'input::src': 1,
|
|
'ins::cite': 0,
|
|
'q::cite': 0
|
|
};
|
|
html4.ltypes = {
|
|
UNSANDBOXED: 2,
|
|
SANDBOXED: 1,
|
|
DATA: 0
|
|
};
|
|
html4.LOADERTYPES = {
|
|
'a::href': 2,
|
|
'area::href': 2,
|
|
'blockquote::cite': 2,
|
|
'body::background': 1,
|
|
'del::cite': 2,
|
|
'form::action': 2,
|
|
'img::src': 1,
|
|
'input::src': 1,
|
|
'ins::cite': 2,
|
|
'q::cite': 2
|
|
};;
|
|
// Copyright (C) 2006 Google Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/**
|
|
* @fileoverview
|
|
* An HTML sanitizer that can satisfy a variety of security policies.
|
|
*
|
|
* <p>
|
|
* The HTML sanitizer is built around a SAX parser and HTML element and
|
|
* attributes schemas.
|
|
*
|
|
* @author mikesamuel@gmail.com
|
|
* @requires html4
|
|
* @overrides window
|
|
* @provides html, html_sanitize
|
|
*/
|
|
|
|
/**
|
|
* @namespace
|
|
*/
|
|
var html = (function (html4) {
|
|
var lcase;
|
|
// The below may not be true on browsers in the Turkish locale.
|
|
if ('script' === 'SCRIPT'.toLowerCase()) {
|
|
lcase = function (s) { return s.toLowerCase(); };
|
|
} else {
|
|
/**
|
|
* {@updoc
|
|
* $ lcase('SCRIPT')
|
|
* # 'script'
|
|
* $ lcase('script')
|
|
* # 'script'
|
|
* }
|
|
*/
|
|
lcase = function (s) {
|
|
return s.replace(
|
|
/[A-Z]/g,
|
|
function (ch) {
|
|
return String.fromCharCode(ch.charCodeAt(0) | 32);
|
|
});
|
|
};
|
|
}
|
|
|
|
var ENTITIES = {
|
|
lt : '<',
|
|
gt : '>',
|
|
amp : '&',
|
|
nbsp : '\240',
|
|
quot : '"',
|
|
apos : '\''
|
|
};
|
|
|
|
// Schemes on which to defer to uripolicy. Urls with other schemes are denied
|
|
var WHITELISTED_SCHEMES = /^(?:https?|mailto|data)$/i;
|
|
|
|
var decimalEscapeRe = /^#(\d+)$/;
|
|
var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/;
|
|
/**
|
|
* Decodes an HTML entity.
|
|
*
|
|
* {@updoc
|
|
* $ lookupEntity('lt')
|
|
* # '<'
|
|
* $ lookupEntity('GT')
|
|
* # '>'
|
|
* $ lookupEntity('amp')
|
|
* # '&'
|
|
* $ lookupEntity('nbsp')
|
|
* # '\xA0'
|
|
* $ lookupEntity('apos')
|
|
* # "'"
|
|
* $ lookupEntity('quot')
|
|
* # '"'
|
|
* $ lookupEntity('#xa')
|
|
* # '\n'
|
|
* $ lookupEntity('#10')
|
|
* # '\n'
|
|
* $ lookupEntity('#x0a')
|
|
* # '\n'
|
|
* $ lookupEntity('#010')
|
|
* # '\n'
|
|
* $ lookupEntity('#x00A')
|
|
* # '\n'
|
|
* $ lookupEntity('Pi') // Known failure
|
|
* # '\u03A0'
|
|
* $ lookupEntity('pi') // Known failure
|
|
* # '\u03C0'
|
|
* }
|
|
*
|
|
* @param name the content between the '&' and the ';'.
|
|
* @return a single unicode code-point as a string.
|
|
*/
|
|
function lookupEntity(name) {
|
|
name = lcase(name); // TODO: π is different from Π
|
|
if (ENTITIES.hasOwnProperty(name)) { return ENTITIES[name]; }
|
|
var m = name.match(decimalEscapeRe);
|
|
if (m) {
|
|
return String.fromCharCode(parseInt(m[1], 10));
|
|
} else if (!!(m = name.match(hexEscapeRe))) {
|
|
return String.fromCharCode(parseInt(m[1], 16));
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function decodeOneEntity(_, name) {
|
|
return lookupEntity(name);
|
|
}
|
|
|
|
var nulRe = /\0/g;
|
|
function stripNULs(s) {
|
|
return s.replace(nulRe, '');
|
|
}
|
|
|
|
var entityRe = /&(#\d+|#x[0-9A-Fa-f]+|\w+);/g;
|
|
/**
|
|
* The plain text of a chunk of HTML CDATA which possibly containing.
|
|
*
|
|
* {@updoc
|
|
* $ unescapeEntities('')
|
|
* # ''
|
|
* $ unescapeEntities('hello World!')
|
|
* # 'hello World!'
|
|
* $ unescapeEntities('1 < 2 && 4 > 3 ')
|
|
* # '1 < 2 && 4 > 3\n'
|
|
* $ unescapeEntities('<< <- unfinished entity>')
|
|
* # '<< <- unfinished entity>'
|
|
* $ unescapeEntities('/foo?bar=baz©=true') // & often unescaped in URLS
|
|
* # '/foo?bar=baz©=true'
|
|
* $ unescapeEntities('pi=ππ, Pi=Π\u03A0') // FIXME: known failure
|
|
* # 'pi=\u03C0\u03c0, Pi=\u03A0\u03A0'
|
|
* }
|
|
*
|
|
* @param s a chunk of HTML CDATA. It must not start or end inside an HTML
|
|
* entity.
|
|
*/
|
|
function unescapeEntities(s) {
|
|
return s.replace(entityRe, decodeOneEntity);
|
|
}
|
|
|
|
var ampRe = /&/g;
|
|
var looseAmpRe = /&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi;
|
|
var ltRe = /</g;
|
|
var gtRe = />/g;
|
|
var quotRe = /\"/g;
|
|
var eqRe = /\=/g; // Backslash required on JScript.net
|
|
|
|
/**
|
|
* Escapes HTML special characters in attribute values as HTML entities.
|
|
*
|
|
* {@updoc
|
|
* $ escapeAttrib('')
|
|
* # ''
|
|
* $ escapeAttrib('"<<&==&>>"') // Do not just escape the first occurrence.
|
|
* # '"<<&==&>>"'
|
|
* $ escapeAttrib('Hello <World>!')
|
|
* # 'Hello <World>!'
|
|
* }
|
|
*/
|
|
function escapeAttrib(s) {
|
|
// Escaping '=' defangs many UTF-7 and SGML short-tag attacks.
|
|
return s.replace(ampRe, '&').replace(ltRe, '<').replace(gtRe, '>')
|
|
.replace(quotRe, '"').replace(eqRe, '=');
|
|
}
|
|
|
|
/**
|
|
* Escape entities in RCDATA that can be escaped without changing the meaning.
|
|
* {@updoc
|
|
* $ normalizeRCData('1 < 2 && 3 > 4 && 5 < 7&8')
|
|
* # '1 < 2 && 3 > 4 && 5 < 7&8'
|
|
* }
|
|
*/
|
|
function normalizeRCData(rcdata) {
|
|
return rcdata
|
|
.replace(looseAmpRe, '&$1')
|
|
.replace(ltRe, '<')
|
|
.replace(gtRe, '>');
|
|
}
|
|
|
|
|
|
// TODO(mikesamuel): validate sanitizer regexs against the HTML5 grammar at
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html
|
|
|
|
/** token definitions. */
|
|
var INSIDE_TAG_TOKEN = new RegExp(
|
|
// Don't capture space.
|
|
'^\\s*(?:'
|
|
// Capture an attribute name in group 1, and value in group 3.
|
|
// We capture the fact that there was an attribute in group 2, since
|
|
// interpreters are inconsistent in whether a group that matches nothing
|
|
// is null, undefined, or the empty string.
|
|
+ ('(?:'
|
|
+ '([a-z][a-z-]*)' // attribute name
|
|
+ ('(' // optionally followed
|
|
+ '\\s*=\\s*'
|
|
+ ('('
|
|
// A double quoted string.
|
|
+ '\"[^\"]*\"'
|
|
// A single quoted string.
|
|
+ '|\'[^\']*\''
|
|
// The positive lookahead is used to make sure that in
|
|
// <foo bar= baz=boo>, the value for bar is blank, not "baz=boo".
|
|
+ '|(?=[a-z][a-z-]*\\s*=)'
|
|
// An unquoted value that is not an attribute name.
|
|
// We know it is not an attribute name because the previous
|
|
// zero-width match would've eliminated that possibility.
|
|
+ '|[^>\"\'\\s]*'
|
|
+ ')'
|
|
)
|
|
+ ')'
|
|
) + '?'
|
|
+ ')'
|
|
)
|
|
// End of tag captured in group 3.
|
|
+ '|(\/?>)'
|
|
// Don't capture cruft
|
|
+ '|[\\s\\S][^a-z\\s>]*)',
|
|
'i');
|
|
|
|
var OUTSIDE_TAG_TOKEN = new RegExp(
|
|
'^(?:'
|
|
// Entity captured in group 1.
|
|
+ '&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);'
|
|
// Comment, doctypes, and processing instructions not captured.
|
|
+ '|<\!--[\\s\\S]*?--\>|<!\\w[^>]*>|<\\?[^>*]*>'
|
|
// '/' captured in group 2 for close tags, and name captured in group 3.
|
|
+ '|<(\/)?([a-z][a-z0-9]*)'
|
|
// Text captured in group 4.
|
|
+ '|([^<&>]+)'
|
|
// Cruft captured in group 5.
|
|
+ '|([<&>]))',
|
|
'i');
|
|
|
|
/**
|
|
* Given a SAX-like event handler, produce a function that feeds those
|
|
* events and a parameter to the event handler.
|
|
*
|
|
* The event handler has the form:{@code
|
|
* {
|
|
* // Name is an upper-case HTML tag name. Attribs is an array of
|
|
* // alternating upper-case attribute names, and attribute values. The
|
|
* // attribs array is reused by the parser. Param is the value passed to
|
|
* // the saxParser.
|
|
* startTag: function (name, attribs, param) { ... },
|
|
* endTag: function (name, param) { ... },
|
|
* pcdata: function (text, param) { ... },
|
|
* rcdata: function (text, param) { ... },
|
|
* cdata: function (text, param) { ... },
|
|
* startDoc: function (param) { ... },
|
|
* endDoc: function (param) { ... }
|
|
* }}
|
|
*
|
|
* @param {Object} handler a record containing event handlers.
|
|
* @return {Function} that takes a chunk of html and a parameter.
|
|
* The parameter is passed on to the handler methods.
|
|
*/
|
|
function makeSaxParser(handler) {
|
|
return function parse(htmlText, param) {
|
|
htmlText = String(htmlText);
|
|
var htmlLower = null;
|
|
|
|
var inTag = false; // True iff we're currently processing a tag.
|
|
var attribs = []; // Accumulates attribute names and values.
|
|
var tagName = void 0; // The name of the tag currently being processed.
|
|
var eflags = void 0; // The element flags for the current tag.
|
|
var openTag = void 0; // True if the current tag is an open tag.
|
|
|
|
if (handler.startDoc) { handler.startDoc(param); }
|
|
|
|
while (htmlText) {
|
|
var m = htmlText.match(inTag ? INSIDE_TAG_TOKEN : OUTSIDE_TAG_TOKEN);
|
|
htmlText = htmlText.substring(m[0].length);
|
|
|
|
if (inTag) {
|
|
if (m[1]) { // attribute
|
|
// setAttribute with uppercase names doesn't work on IE6.
|
|
var attribName = lcase(m[1]);
|
|
var decodedValue;
|
|
if (m[2]) {
|
|
var encodedValue = m[3];
|
|
switch (encodedValue.charCodeAt(0)) { // Strip quotes
|
|
case 34: case 39:
|
|
encodedValue = encodedValue.substring(
|
|
1, encodedValue.length - 1);
|
|
break;
|
|
}
|
|
decodedValue = unescapeEntities(stripNULs(encodedValue));
|
|
} else {
|
|
// Use name as value for valueless attribs, so
|
|
// <input type=checkbox checked>
|
|
// gets attributes ['type', 'checkbox', 'checked', 'checked']
|
|
decodedValue = attribName;
|
|
}
|
|
attribs.push(attribName, decodedValue);
|
|
} else if (m[4]) {
|
|
if (eflags !== void 0) { // False if not in whitelist.
|
|
if (openTag) {
|
|
if (handler.startTag) {
|
|
handler.startTag(tagName, attribs, param);
|
|
}
|
|
} else {
|
|
if (handler.endTag) {
|
|
handler.endTag(tagName, param);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (openTag
|
|
&& (eflags & (html4.eflags.CDATA | html4.eflags.RCDATA))) {
|
|
if (htmlLower === null) {
|
|
htmlLower = lcase(htmlText);
|
|
} else {
|
|
htmlLower = htmlLower.substring(
|
|
htmlLower.length - htmlText.length);
|
|
}
|
|
var dataEnd = htmlLower.indexOf('</' + tagName);
|
|
if (dataEnd < 0) { dataEnd = htmlText.length; }
|
|
if (dataEnd) {
|
|
if (eflags & html4.eflags.CDATA) {
|
|
if (handler.cdata) {
|
|
handler.cdata(htmlText.substring(0, dataEnd), param);
|
|
}
|
|
} else if (handler.rcdata) {
|
|
handler.rcdata(
|
|
normalizeRCData(htmlText.substring(0, dataEnd)), param);
|
|
}
|
|
htmlText = htmlText.substring(dataEnd);
|
|
}
|
|
}
|
|
|
|
tagName = eflags = openTag = void 0;
|
|
attribs.length = 0;
|
|
inTag = false;
|
|
}
|
|
} else {
|
|
if (m[1]) { // Entity
|
|
if (handler.pcdata) { handler.pcdata(m[0], param); }
|
|
} else if (m[3]) { // Tag
|
|
openTag = !m[2];
|
|
inTag = true;
|
|
tagName = lcase(m[3]);
|
|
eflags = html4.ELEMENTS.hasOwnProperty(tagName)
|
|
? html4.ELEMENTS[tagName] : void 0;
|
|
} else if (m[4]) { // Text
|
|
if (handler.pcdata) { handler.pcdata(m[4], param); }
|
|
} else if (m[5]) { // Cruft
|
|
if (handler.pcdata) {
|
|
var ch = m[5];
|
|
handler.pcdata(
|
|
ch === '<' ? '<' : ch === '>' ? '>' : '&',
|
|
param);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (handler.endDoc) { handler.endDoc(param); }
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a function that strips unsafe tags and attributes from html.
|
|
* @param {Function} sanitizeAttributes
|
|
* maps from (tagName, attribs[]) to null or a sanitized attribute array.
|
|
* The attribs array can be arbitrarily modified, but the same array
|
|
* instance is reused, so should not be held.
|
|
* @return {Function} from html to sanitized html
|
|
*/
|
|
function makeHtmlSanitizer(sanitizeAttributes) {
|
|
var stack;
|
|
var ignoring;
|
|
return makeSaxParser({
|
|
startDoc: function (_) {
|
|
stack = [];
|
|
ignoring = false;
|
|
},
|
|
startTag: function (tagName, attribs, out) {
|
|
if (ignoring) { return; }
|
|
if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
|
|
var eflags = html4.ELEMENTS[tagName];
|
|
if (eflags & html4.eflags.FOLDABLE) {
|
|
return;
|
|
} else if (eflags & html4.eflags.UNSAFE) {
|
|
ignoring = !(eflags & html4.eflags.EMPTY);
|
|
return;
|
|
}
|
|
attribs = sanitizeAttributes(tagName, attribs);
|
|
// TODO(mikesamuel): relying on sanitizeAttributes not to
|
|
// insert unsafe attribute names.
|
|
if (attribs) {
|
|
if (!(eflags & html4.eflags.EMPTY)) {
|
|
stack.push(tagName);
|
|
}
|
|
|
|
out.push('<', tagName);
|
|
for (var i = 0, n = attribs.length; i < n; i += 2) {
|
|
var attribName = attribs[i],
|
|
value = attribs[i + 1];
|
|
if (value !== null && value !== void 0) {
|
|
out.push(' ', attribName, '="', escapeAttrib(value), '"');
|
|
}
|
|
}
|
|
out.push('>');
|
|
}
|
|
},
|
|
endTag: function (tagName, out) {
|
|
if (ignoring) {
|
|
ignoring = false;
|
|
return;
|
|
}
|
|
if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
|
|
var eflags = html4.ELEMENTS[tagName];
|
|
if (!(eflags & (html4.eflags.UNSAFE | html4.eflags.EMPTY
|
|
| html4.eflags.FOLDABLE))) {
|
|
var index;
|
|
if (eflags & html4.eflags.OPTIONAL_ENDTAG) {
|
|
for (index = stack.length; --index >= 0;) {
|
|
var stackEl = stack[index];
|
|
if (stackEl === tagName) { break; }
|
|
if (!(html4.ELEMENTS[stackEl]
|
|
& html4.eflags.OPTIONAL_ENDTAG)) {
|
|
// Don't pop non optional end tags looking for a match.
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
for (index = stack.length; --index >= 0;) {
|
|
if (stack[index] === tagName) { break; }
|
|
}
|
|
}
|
|
if (index < 0) { return; } // Not opened.
|
|
for (var i = stack.length; --i > index;) {
|
|
var stackEl = stack[i];
|
|
if (!(html4.ELEMENTS[stackEl]
|
|
& html4.eflags.OPTIONAL_ENDTAG)) {
|
|
out.push('</', stackEl, '>');
|
|
}
|
|
}
|
|
stack.length = index;
|
|
out.push('</', tagName, '>');
|
|
}
|
|
},
|
|
pcdata: function (text, out) {
|
|
if (!ignoring) { out.push(text); }
|
|
},
|
|
rcdata: function (text, out) {
|
|
if (!ignoring) { out.push(text); }
|
|
},
|
|
cdata: function (text, out) {
|
|
if (!ignoring) { out.push(text); }
|
|
},
|
|
endDoc: function (out) {
|
|
for (var i = stack.length; --i >= 0;) {
|
|
out.push('</', stack[i], '>');
|
|
}
|
|
stack.length = 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
// From RFC3986
|
|
var URI_SCHEME_RE = new RegExp(
|
|
"^" +
|
|
"(?:" +
|
|
"([^:\/?#]+)" + // scheme
|
|
":)?"
|
|
);
|
|
|
|
/**
|
|
* Strips unsafe tags and attributes from html.
|
|
* @param {string} htmlText to sanitize
|
|
* @param {Function} opt_uriPolicy -- a transform to apply to uri/url
|
|
* attribute values. If no opt_uriPolicy is provided, no uris
|
|
* are allowed ie. the default uriPolicy rewrites all uris to null
|
|
* @param {Function} opt_nmTokenPolicy : string -> string? -- a transform to
|
|
* apply to names, ids, and classes. If no opt_nmTokenPolicy is provided,
|
|
* all names, ids and classes are passed through ie. the default
|
|
* nmTokenPolicy is an identity transform
|
|
* @return {string} html
|
|
*/
|
|
function sanitize(htmlText, opt_uriPolicy, opt_nmTokenPolicy) {
|
|
var out = [];
|
|
makeHtmlSanitizer(
|
|
function sanitizeAttribs(tagName, attribs) {
|
|
for (var i = 0; i < attribs.length; i += 2) {
|
|
var attribName = attribs[i];
|
|
var value = attribs[i + 1];
|
|
var atype = null, attribKey;
|
|
if ((attribKey = tagName + '::' + attribName,
|
|
html4.ATTRIBS.hasOwnProperty(attribKey))
|
|
|| (attribKey = '*::' + attribName,
|
|
html4.ATTRIBS.hasOwnProperty(attribKey))) {
|
|
atype = html4.ATTRIBS[attribKey];
|
|
}
|
|
if (atype !== null) {
|
|
switch (atype) {
|
|
case html4.atype.NONE: break;
|
|
case html4.atype.SCRIPT:
|
|
case html4.atype.STYLE:
|
|
value = null;
|
|
break;
|
|
case html4.atype.ID:
|
|
case html4.atype.IDREF:
|
|
case html4.atype.IDREFS:
|
|
case html4.atype.GLOBAL_NAME:
|
|
case html4.atype.LOCAL_NAME:
|
|
case html4.atype.CLASSES:
|
|
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
|
|
break;
|
|
case html4.atype.URI:
|
|
var parsedUri = ('' + value).match(URI_SCHEME_RE);
|
|
if (!parsedUri) {
|
|
value = null;
|
|
} else if (!parsedUri[1] ||
|
|
WHITELISTED_SCHEMES.test(parsedUri[1])) {
|
|
value = opt_uriPolicy && opt_uriPolicy(value);
|
|
} else {
|
|
value = null;
|
|
}
|
|
break;
|
|
case html4.atype.URI_FRAGMENT:
|
|
if (value && '#' === value.charAt(0)) {
|
|
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
|
|
if (value) { value = '#' + value; }
|
|
} else {
|
|
value = null;
|
|
}
|
|
break;
|
|
default:
|
|
value = null;
|
|
break;
|
|
}
|
|
} else {
|
|
value = null;
|
|
}
|
|
attribs[i + 1] = value;
|
|
}
|
|
return attribs;
|
|
})(htmlText, out);
|
|
return out.join('');
|
|
}
|
|
|
|
return {
|
|
escapeAttrib: escapeAttrib,
|
|
makeHtmlSanitizer: makeHtmlSanitizer,
|
|
makeSaxParser: makeSaxParser,
|
|
normalizeRCData: normalizeRCData,
|
|
sanitize: sanitize,
|
|
unescapeEntities: unescapeEntities
|
|
};
|
|
})(html4);
|
|
|
|
var html_sanitize = html.sanitize;
|
|
|
|
// Exports for closure compiler. Note this file is also cajoled
|
|
// for domado and run in an environment without 'window'
|
|
if (typeof window !== 'undefined') {
|
|
window['html'] = html;
|
|
window['html_sanitize'] = html_sanitize;
|
|
}
|
|
// Loosen restrictions of Caja's
|
|
// html-sanitizer to allow for styling
|
|
html4.ATTRIBS['*::style'] = 0;
|
|
html4.ELEMENTS['style'] = 0;
|
|
|
|
html4.ATTRIBS['a::target'] = 0;
|
|
|
|
html4.ELEMENTS['video'] = 0;
|
|
html4.ATTRIBS['video::src'] = 0;
|
|
html4.ATTRIBS['video::poster'] = 0;
|
|
html4.ATTRIBS['video::controls'] = 0;
|
|
|
|
html4.ELEMENTS['audio'] = 0;
|
|
html4.ATTRIBS['audio::src'] = 0;
|
|
html4.ATTRIBS['video::autoplay'] = 0;
|
|
html4.ATTRIBS['video::controls'] = 0;
|
|
;wax = wax || {};
|
|
|
|
// Attribution
|
|
// -----------
|
|
wax.attribution = function() {
|
|
var a = {};
|
|
|
|
var container = document.createElement('div');
|
|
container.className = 'map-attribution';
|
|
|
|
a.content = function(x) {
|
|
if (typeof x === 'undefined') return container.innerHTML;
|
|
container.innerHTML = wax.u.sanitize(x);
|
|
return this;
|
|
};
|
|
|
|
a.element = function() {
|
|
return container;
|
|
};
|
|
|
|
a.init = function() {
|
|
return this;
|
|
};
|
|
|
|
return a;
|
|
};
|
|
wax = wax || {};
|
|
|
|
// Attribution
|
|
// -----------
|
|
wax.bwdetect = function(options, callback) {
|
|
var detector = {},
|
|
threshold = options.threshold || 400,
|
|
// test image: 30.29KB
|
|
testImage = 'http://a.tiles.mapbox.com/mapbox/1.0.0/blue-marble-topo-bathy-jul/0/0/0.png?preventcache=' + (+new Date()),
|
|
// High-bandwidth assumed
|
|
// 1: high bandwidth (.png, .jpg)
|
|
// 0: low bandwidth (.png128, .jpg70)
|
|
bw = 1,
|
|
// Alternative versions
|
|
auto = options.auto === undefined ? true : options.auto;
|
|
|
|
function bwTest() {
|
|
wax.bw = -1;
|
|
var im = new Image();
|
|
im.src = testImage;
|
|
var first = true;
|
|
var timeout = setTimeout(function() {
|
|
if (first && wax.bw == -1) {
|
|
detector.bw(0);
|
|
first = false;
|
|
}
|
|
}, threshold);
|
|
im.onload = function() {
|
|
if (first && wax.bw == -1) {
|
|
clearTimeout(timeout);
|
|
detector.bw(1);
|
|
first = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
detector.bw = function(x) {
|
|
if (!arguments.length) return bw;
|
|
var oldBw = bw;
|
|
if (wax.bwlisteners && wax.bwlisteners.length) (function () {
|
|
listeners = wax.bwlisteners;
|
|
wax.bwlisteners = [];
|
|
for (i = 0; i < listeners; i++) {
|
|
listeners[i](x);
|
|
}
|
|
})();
|
|
wax.bw = x;
|
|
|
|
if (bw != (bw = x)) callback(x);
|
|
};
|
|
|
|
detector.add = function() {
|
|
if (auto) bwTest();
|
|
return this;
|
|
};
|
|
|
|
if (wax.bw == -1) {
|
|
wax.bwlisteners = wax.bwlisteners || [];
|
|
wax.bwlisteners.push(detector.bw);
|
|
} else if (wax.bw !== undefined) {
|
|
detector.bw(wax.bw);
|
|
} else {
|
|
detector.add();
|
|
}
|
|
return detector;
|
|
};
|
|
// Formatter
|
|
// ---------
|
|
//
|
|
// This code is no longer the recommended code path for Wax -
|
|
// see `template.js`, a safe implementation of Mustache templates.
|
|
wax.formatter = function(x) {
|
|
var formatter = {},
|
|
f;
|
|
|
|
// Prevent against just any input being used.
|
|
if (x && typeof x === 'string') {
|
|
try {
|
|
// Ugly, dangerous use of eval.
|
|
eval('f = ' + x);
|
|
} catch (e) {
|
|
if (console) console.log(e);
|
|
}
|
|
} else if (x && typeof x === 'function') {
|
|
f = x;
|
|
} else {
|
|
f = function() {};
|
|
}
|
|
|
|
// Wrap the given formatter function in order to
|
|
// catch exceptions that it may throw.
|
|
formatter.format = function(options, data) {
|
|
try {
|
|
return wax.u.sanitize(f(options, data));
|
|
} catch (e) {
|
|
if (console) console.log(e);
|
|
}
|
|
};
|
|
|
|
return formatter;
|
|
};
|
|
// GridInstance
|
|
// ------------
|
|
// GridInstances are queryable, fully-formed
|
|
// objects for acquiring features from events.
|
|
//
|
|
// This code ignores format of 1.1-1.2
|
|
wax.gi = function(grid_tile, options) {
|
|
options = options || {};
|
|
// resolution is the grid-elements-per-pixel ratio of gridded data.
|
|
// The size of a tile element. For now we expect tiles to be squares.
|
|
var instance = {},
|
|
resolution = options.resolution || 4,
|
|
tileSize = options.tileSize || 256;
|
|
|
|
// Resolve the UTF-8 encoding stored in grids to simple
|
|
// number values.
|
|
// See the [utfgrid spec](https://github.com/mapbox/utfgrid-spec)
|
|
// for details.
|
|
function resolveCode(key) {
|
|
if (key >= 93) key--;
|
|
if (key >= 35) key--;
|
|
key -= 32;
|
|
return key;
|
|
}
|
|
|
|
instance.grid_tile = function() {
|
|
return grid_tile;
|
|
};
|
|
|
|
instance.getKey = function(x, y) {
|
|
if (!(grid_tile && grid_tile.grid)) return;
|
|
if ((y < 0) || (x < 0)) return;
|
|
if ((Math.floor(y) >= tileSize) ||
|
|
(Math.floor(x) >= tileSize)) return;
|
|
// Find the key in the grid. The above calls should ensure that
|
|
// the grid's array is large enough to make this work.
|
|
return resolveCode(grid_tile.grid[
|
|
Math.floor((y) / resolution)
|
|
].charCodeAt(
|
|
Math.floor((x) / resolution)
|
|
));
|
|
};
|
|
|
|
// Lower-level than tileFeature - has nothing to do
|
|
// with the DOM. Takes a px offset from 0, 0 of a grid.
|
|
instance.gridFeature = function(x, y) {
|
|
// Find the key in the grid. The above calls should ensure that
|
|
// the grid's array is large enough to make this work.
|
|
var key = this.getKey(x, y),
|
|
keys = grid_tile.keys;
|
|
|
|
if (keys &&
|
|
keys[key] &&
|
|
grid_tile.data[keys[key]]) {
|
|
return grid_tile.data[keys[key]];
|
|
}
|
|
};
|
|
|
|
// Get a feature:
|
|
// * `x` and `y`: the screen coordinates of an event
|
|
// * `tile_element`: a DOM element of a tile, from which we can get an offset.
|
|
instance.tileFeature = function(x, y, tile_element) {
|
|
if (!grid_tile) return;
|
|
// IE problem here - though recoverable, for whatever reason
|
|
var offset = wax.u.offset(tile_element);
|
|
feature = this.gridFeature(x - offset.left, y - offset.top);
|
|
return feature;
|
|
};
|
|
|
|
return instance;
|
|
};
|
|
// GridManager
|
|
// -----------
|
|
// Generally one GridManager will be used per map.
|
|
//
|
|
// It takes one options object, which current accepts a single option:
|
|
// `resolution` determines the number of pixels per grid element in the grid.
|
|
// The default is 4.
|
|
wax.gm = function() {
|
|
|
|
var resolution = 4,
|
|
grid_tiles = {},
|
|
manager = {},
|
|
tilejson,
|
|
formatter;
|
|
|
|
var gridUrl = function(url) {
|
|
if (url) {
|
|
return url.replace(/(\.png|\.jpg|\.jpeg)(\d*)/, '.grid.json');
|
|
}
|
|
};
|
|
|
|
function templatedGridUrl(template) {
|
|
if (typeof template === 'string') template = [template];
|
|
return function templatedGridFinder(url) {
|
|
if (!url) return;
|
|
var rx = new RegExp('/(\\d+)\\/(\\d+)\\/(\\d+)\\.[\\w\\._]+');
|
|
var xyz = rx.exec(url);
|
|
if (!xyz) return;
|
|
return template[parseInt(xyz[2], 10) % template.length]
|
|
.replace(/\{z\}/g, xyz[1])
|
|
.replace(/\{x\}/g, xyz[2])
|
|
.replace(/\{y\}/g, xyz[3]);
|
|
};
|
|
}
|
|
|
|
manager.formatter = function(x) {
|
|
if (!arguments.length) return formatter;
|
|
formatter = wax.formatter(x);
|
|
return manager;
|
|
};
|
|
|
|
manager.template = function(x) {
|
|
if (!arguments.length) return formatter;
|
|
formatter = wax.template(x);
|
|
return manager;
|
|
};
|
|
|
|
manager.gridUrl = function(x) {
|
|
// Getter-setter
|
|
if (!arguments.length) return gridUrl;
|
|
|
|
// Handle tilesets that don't support grids
|
|
if (!x) {
|
|
gridUrl = function() { return null; };
|
|
} else {
|
|
gridUrl = typeof x === 'function' ?
|
|
x : templatedGridUrl(x);
|
|
}
|
|
return manager;
|
|
};
|
|
|
|
manager.getGrid = function(url, callback) {
|
|
var gurl = gridUrl(url);
|
|
if (!formatter || !gurl) return callback(null, null);
|
|
|
|
wax.request.get(gurl, function(err, t) {
|
|
if (err) return callback(err, null);
|
|
callback(null, wax.gi(t, {
|
|
formatter: formatter,
|
|
resolution: resolution
|
|
}));
|
|
});
|
|
return manager;
|
|
};
|
|
|
|
manager.tilejson = function(x) {
|
|
if (!arguments.length) return tilejson;
|
|
// prefer templates over formatters
|
|
if (x.template) {
|
|
manager.template(x.template);
|
|
} else if (x.formatter) {
|
|
manager.formatter(x.formatter);
|
|
} else {
|
|
// In this case, we cannot support grids
|
|
formatter = undefined;
|
|
}
|
|
manager.gridUrl(x.grids);
|
|
if (x.resolution) resolution = x.resolution;
|
|
tilejson = x;
|
|
return manager;
|
|
};
|
|
|
|
return manager;
|
|
};
|
|
wax = wax || {};
|
|
|
|
// Hash
|
|
// ----
|
|
wax.hash = function(options) {
|
|
options = options || {};
|
|
|
|
var s0, // old hash
|
|
hash = {},
|
|
lat = 90 - 1e-8; // allowable latitude range
|
|
|
|
function getState() {
|
|
return location.hash.substring(1);
|
|
}
|
|
|
|
function pushState(state) {
|
|
var l = window.location;
|
|
l.replace(l.toString().replace((l.hash || /$/), '#' + state));
|
|
}
|
|
|
|
function parseHash(s) {
|
|
var args = s.split('/');
|
|
for (var i = 0; i < args.length; i++) {
|
|
args[i] = Number(args[i]);
|
|
if (isNaN(args[i])) return true;
|
|
}
|
|
if (args.length < 3) {
|
|
// replace bogus hash
|
|
return true;
|
|
} else if (args.length == 3) {
|
|
options.setCenterZoom(args);
|
|
}
|
|
}
|
|
|
|
function move() {
|
|
var s1 = options.getCenterZoom();
|
|
if (s0 !== s1) {
|
|
s0 = s1;
|
|
// don't recenter the map!
|
|
pushState(s0);
|
|
}
|
|
}
|
|
|
|
function stateChange(state) {
|
|
// ignore spurious hashchange events
|
|
if (state === s0) return;
|
|
if (parseHash(s0 = state)) {
|
|
// replace bogus hash
|
|
move();
|
|
}
|
|
}
|
|
|
|
var _move = wax.u.throttle(move, 500);
|
|
|
|
hash.add = function() {
|
|
stateChange(getState());
|
|
options.bindChange(_move);
|
|
return hash;
|
|
};
|
|
|
|
hash.remove = function() {
|
|
options.unbindChange(_move);
|
|
return hash;
|
|
};
|
|
|
|
return hash;
|
|
};
|
|
wax = wax || {};
|
|
|
|
wax.interaction = function() {
|
|
var gm = wax.gm(),
|
|
interaction = {},
|
|
_downLock = false,
|
|
_clickTimeout = null,
|
|
// Active feature
|
|
// Down event
|
|
_d,
|
|
// Touch tolerance
|
|
tol = 4,
|
|
grid,
|
|
attach,
|
|
detach,
|
|
parent,
|
|
map,
|
|
tileGrid;
|
|
|
|
var defaultEvents = {
|
|
mousemove: onMove,
|
|
touchstart: onDown,
|
|
mousedown: onDown
|
|
};
|
|
|
|
var touchEnds = {
|
|
touchend: onUp,
|
|
touchmove: onUp,
|
|
touchcancel: touchCancel
|
|
};
|
|
|
|
// Abstract getTile method. Depends on a tilegrid with
|
|
// grid[ [x, y, tile] ] structure.
|
|
function getTile(e) {
|
|
var g = grid();
|
|
for (var i = 0; i < g.length; i++) {
|
|
if ((g[i][0] < e.y) &&
|
|
((g[i][0] + 256) > e.y) &&
|
|
(g[i][1] < e.x) &&
|
|
((g[i][1] + 256) > e.x)) return g[i][2];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Clear the double-click timeout to prevent double-clicks from
|
|
// triggering popups.
|
|
function killTimeout() {
|
|
if (_clickTimeout) {
|
|
window.clearTimeout(_clickTimeout);
|
|
_clickTimeout = null;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function onMove(e) {
|
|
// If the user is actually dragging the map, exit early
|
|
// to avoid performance hits.
|
|
if (_downLock) return;
|
|
|
|
var pos = wax.u.eventoffset(e);
|
|
|
|
interaction.screen_feature(pos, function(feature) {
|
|
if (feature) {
|
|
bean.fire(interaction, 'on', {
|
|
parent: parent(),
|
|
data: feature,
|
|
formatter: gm.formatter().format,
|
|
e: e
|
|
});
|
|
} else {
|
|
bean.fire(interaction, 'off');
|
|
}
|
|
});
|
|
}
|
|
|
|
function dragEnd() {
|
|
_downLock = false;
|
|
}
|
|
|
|
// A handler for 'down' events - which means `mousedown` and `touchstart`
|
|
function onDown(e) {
|
|
|
|
// Prevent interaction offset calculations happening while
|
|
// the user is dragging the map.
|
|
//
|
|
// Store this event so that we can compare it to the
|
|
// up event
|
|
_downLock = true;
|
|
_d = wax.u.eventoffset(e);
|
|
if (e.type === 'mousedown') {
|
|
bean.add(document.body, 'click', onUp);
|
|
// track mouse up to remove lockDown when the drags end
|
|
bean.add(document.body, 'mouseup', dragEnd);
|
|
|
|
// Only track single-touches. Double-touches will not affect this
|
|
// control
|
|
} else if (e.type === 'touchstart' && e.touches.length === 1) {
|
|
// Don't make the user click close if they hit another tooltip
|
|
bean.fire(interaction, 'off');
|
|
// Touch moves invalidate touches
|
|
bean.add(e.srcElement, touchEnds);
|
|
}
|
|
}
|
|
|
|
function touchCancel(e) {
|
|
bean.remove(e.srcElement, touchEnds);
|
|
_downLock = false;
|
|
}
|
|
|
|
function onUp(e) {
|
|
var evt = {},
|
|
pos = wax.u.eventoffset(e);
|
|
_downLock = false;
|
|
|
|
// TODO: refine
|
|
for (var key in e) {
|
|
evt[key] = e[key];
|
|
}
|
|
|
|
bean.remove(document.body, 'mouseup', onUp);
|
|
bean.remove(e.srcElement, touchEnds);
|
|
|
|
if (e.type === 'touchend') {
|
|
// If this was a touch and it survived, there's no need to avoid a double-tap
|
|
// but also wax.u.eventoffset will have failed, since this touch
|
|
// event doesn't have coordinates
|
|
interaction.click(e, _d);
|
|
} else if (Math.round(pos.y / tol) === Math.round(_d.y / tol) &&
|
|
Math.round(pos.x / tol) === Math.round(_d.x / tol)) {
|
|
// Contain the event data in a closure.
|
|
// Ignore double-clicks by ignoring clicks within 300ms of
|
|
// each other.
|
|
if(!_clickTimeout) {
|
|
_clickTimeout = window.setTimeout(function() {
|
|
_clickTimeout = null;
|
|
interaction.click(evt, pos);
|
|
}, 300);
|
|
} else {
|
|
killTimeout();
|
|
}
|
|
}
|
|
return onUp;
|
|
}
|
|
|
|
// Handle a click event. Takes a second
|
|
interaction.click = function(e, pos) {
|
|
interaction.screen_feature(pos, function(feature) {
|
|
if (feature) bean.fire(interaction, 'on', {
|
|
parent: parent(),
|
|
data: feature,
|
|
formatter: gm.formatter().format,
|
|
e: e
|
|
});
|
|
});
|
|
};
|
|
|
|
interaction.screen_feature = function(pos, callback) {
|
|
var tile = getTile(pos);
|
|
if (!tile) callback(null);
|
|
gm.getGrid(tile.src, function(err, g) {
|
|
if (err || !g) return callback(null);
|
|
var feature = g.tileFeature(pos.x, pos.y, tile);
|
|
callback(feature);
|
|
});
|
|
};
|
|
|
|
// set an attach function that should be
|
|
// called when maps are set
|
|
interaction.attach = function(x) {
|
|
if (!arguments.length) return attach;
|
|
attach = x;
|
|
return interaction;
|
|
};
|
|
|
|
interaction.detach = function(x) {
|
|
if (!arguments.length) return detach;
|
|
detach = x;
|
|
return interaction;
|
|
};
|
|
|
|
// Attach listeners to the map
|
|
interaction.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
if (attach) attach(map);
|
|
bean.add(parent(), defaultEvents);
|
|
bean.add(parent(), 'touchstart', onDown);
|
|
return interaction;
|
|
};
|
|
|
|
// set a grid getter for this control
|
|
interaction.grid = function(x) {
|
|
if (!arguments.length) return grid;
|
|
grid = x;
|
|
return interaction;
|
|
};
|
|
|
|
// detach this and its events from the map cleanly
|
|
interaction.remove = function(x) {
|
|
if (detach) detach(map);
|
|
bean.remove(parent(), defaultEvents);
|
|
bean.fire(interaction, 'remove');
|
|
return interaction;
|
|
};
|
|
|
|
// get or set a tilejson chunk of json
|
|
interaction.tilejson = function(x) {
|
|
if (!arguments.length) return gm.tilejson();
|
|
gm.tilejson(x);
|
|
return interaction;
|
|
};
|
|
|
|
// return the formatter, which has an exposed .format
|
|
// function
|
|
interaction.formatter = function() {
|
|
return gm.formatter();
|
|
};
|
|
|
|
// ev can be 'on', 'off', fn is the handler
|
|
interaction.on = function(ev, fn) {
|
|
bean.add(interaction, ev, fn);
|
|
return interaction;
|
|
};
|
|
|
|
// ev can be 'on', 'off', fn is the handler
|
|
interaction.off = function(ev, fn) {
|
|
bean.remove(interaction, ev, fn);
|
|
return interaction;
|
|
};
|
|
|
|
// Return or set the gridmanager implementation
|
|
interaction.gridmanager = function(x) {
|
|
if (!arguments.length) return gm;
|
|
gm = x;
|
|
return interaction;
|
|
};
|
|
|
|
// parent should be a function that returns
|
|
// the parent element of the map
|
|
interaction.parent = function(x) {
|
|
parent = x;
|
|
return interaction;
|
|
};
|
|
|
|
return interaction;
|
|
};
|
|
// Wax Legend
|
|
// ----------
|
|
|
|
// Wax header
|
|
var wax = wax || {};
|
|
|
|
wax.legend = function() {
|
|
var element,
|
|
legend = {},
|
|
container;
|
|
|
|
legend.element = function() {
|
|
return container;
|
|
};
|
|
|
|
legend.content = function(content) {
|
|
if (!arguments.length) return element.innerHTML;
|
|
|
|
element.innerHTML = wax.u.sanitize(content);
|
|
element.style.display = 'block';
|
|
if (element.innerHTML === '') {
|
|
element.style.display = 'none';
|
|
}
|
|
|
|
return legend;
|
|
};
|
|
|
|
legend.add = function() {
|
|
container = document.createElement('div');
|
|
container.className = 'map-legends wax-legends';
|
|
|
|
element = container.appendChild(document.createElement('div'));
|
|
element.className = 'map-legend wax-legend';
|
|
element.style.display = 'none';
|
|
return legend;
|
|
};
|
|
|
|
return legend.add();
|
|
};
|
|
var wax = wax || {};
|
|
|
|
wax.location = function() {
|
|
|
|
var t = {};
|
|
|
|
function on(o) {
|
|
if ((o.e.type === 'mousemove' || !o.e.type)) {
|
|
return;
|
|
} else {
|
|
var loc = o.formatter({ format: 'location' }, o.data);
|
|
if (loc) {
|
|
window.top.location.href = loc;
|
|
}
|
|
}
|
|
}
|
|
|
|
t.events = function() {
|
|
return {
|
|
on: on
|
|
};
|
|
};
|
|
|
|
return t;
|
|
|
|
};
|
|
var wax = wax || {};
|
|
wax.movetip = {};
|
|
|
|
wax.movetip = function() {
|
|
var popped = false,
|
|
t = {},
|
|
_tooltipOffset,
|
|
_contextOffset,
|
|
tooltip,
|
|
parent;
|
|
|
|
function moveTooltip(e) {
|
|
var eo = wax.u.eventoffset(e);
|
|
// faux-positioning
|
|
if ((_tooltipOffset.height + eo.y) >
|
|
(_contextOffset.top + _contextOffset.height) &&
|
|
(_contextOffset.height > _tooltipOffset.height)) {
|
|
eo.y -= _tooltipOffset.height;
|
|
tooltip.className += ' flip-y';
|
|
}
|
|
|
|
// faux-positioning
|
|
if ((_tooltipOffset.width + eo.x) >
|
|
(_contextOffset.left + _contextOffset.width)) {
|
|
eo.x -= _tooltipOffset.width;
|
|
tooltip.className += ' flip-x';
|
|
}
|
|
|
|
tooltip.style.left = eo.x + 'px';
|
|
tooltip.style.top = eo.y + 'px';
|
|
}
|
|
|
|
// Get the active tooltip for a layer or create a new one if no tooltip exists.
|
|
// Hide any tooltips on layers underneath this one.
|
|
function getTooltip(feature) {
|
|
var tooltip = document.createElement('div');
|
|
tooltip.className = 'map-tooltip map-tooltip-0';
|
|
tooltip.innerHTML = feature;
|
|
return tooltip;
|
|
}
|
|
|
|
// Hide a given tooltip.
|
|
function hide() {
|
|
if (tooltip) {
|
|
tooltip.parentNode.removeChild(tooltip);
|
|
tooltip = null;
|
|
}
|
|
}
|
|
|
|
function on(o) {
|
|
var content;
|
|
if (popped) return;
|
|
if ((o.e.type === 'mousemove' || !o.e.type)) {
|
|
content = o.formatter({ format: 'teaser' }, o.data);
|
|
if (!content) return;
|
|
hide();
|
|
parent.style.cursor = 'pointer';
|
|
tooltip = document.body.appendChild(getTooltip(content));
|
|
} else {
|
|
content = o.formatter({ format: 'teaser' }, o.data);
|
|
if (!content) return;
|
|
hide();
|
|
var tt = document.body.appendChild(getTooltip(content));
|
|
tt.className += ' map-popup';
|
|
|
|
var close = tt.appendChild(document.createElement('a'));
|
|
close.href = '#close';
|
|
close.className = 'close';
|
|
close.innerHTML = 'Close';
|
|
|
|
popped = true;
|
|
|
|
tooltip = tt;
|
|
|
|
_tooltipOffset = wax.u.offset(tooltip);
|
|
_contextOffset = wax.u.offset(parent);
|
|
moveTooltip(o.e);
|
|
|
|
bean.add(close, 'click touchend', function closeClick(e) {
|
|
e.stop();
|
|
hide();
|
|
popped = false;
|
|
});
|
|
}
|
|
if (tooltip) {
|
|
_tooltipOffset = wax.u.offset(tooltip);
|
|
_contextOffset = wax.u.offset(parent);
|
|
moveTooltip(o.e);
|
|
}
|
|
|
|
}
|
|
|
|
function off() {
|
|
parent.style.cursor = 'default';
|
|
if (!popped) hide();
|
|
}
|
|
|
|
t.parent = function(x) {
|
|
if (!arguments.length) return parent;
|
|
parent = x;
|
|
return t;
|
|
};
|
|
|
|
t.events = function() {
|
|
return {
|
|
on: on,
|
|
off: off
|
|
};
|
|
};
|
|
|
|
return t;
|
|
};
|
|
|
|
// Wax GridUtil
|
|
// ------------
|
|
|
|
// Wax header
|
|
var wax = wax || {};
|
|
|
|
// Request
|
|
// -------
|
|
// Request data cache. `callback(data)` where `data` is the response data.
|
|
wax.request = {
|
|
cache: {},
|
|
locks: {},
|
|
promises: {},
|
|
get: function(url, callback) {
|
|
// Cache hit.
|
|
if (this.cache[url]) {
|
|
return callback(this.cache[url][0], this.cache[url][1]);
|
|
// Cache miss.
|
|
} else {
|
|
this.promises[url] = this.promises[url] || [];
|
|
this.promises[url].push(callback);
|
|
// Lock hit.
|
|
if (this.locks[url]) return;
|
|
// Request.
|
|
var that = this;
|
|
this.locks[url] = true;
|
|
reqwest({
|
|
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=?',
|
|
type: 'jsonp',
|
|
success: function(data) {
|
|
that.locks[url] = false;
|
|
that.cache[url] = [null, data];
|
|
for (var i = 0; i < that.promises[url].length; i++) {
|
|
that.promises[url][i](that.cache[url][0], that.cache[url][1]);
|
|
}
|
|
},
|
|
error: function(err) {
|
|
that.locks[url] = false;
|
|
that.cache[url] = [err, null];
|
|
for (var i = 0; i < that.promises[url].length; i++) {
|
|
that.promises[url][i](that.cache[url][0], that.cache[url][1]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
// Templating
|
|
// ---------
|
|
wax.template = function(x) {
|
|
var template = {};
|
|
|
|
// Clone the data object such that the '__[format]__' key is only
|
|
// set for this instance of templating.
|
|
template.format = function(options, data) {
|
|
var clone = {};
|
|
for (var key in data) {
|
|
clone[key] = data[key];
|
|
}
|
|
if (options.format) {
|
|
clone['__' + options.format + '__'] = true;
|
|
}
|
|
return wax.u.sanitize(Mustache.to_html(x, clone));
|
|
};
|
|
|
|
return template;
|
|
};
|
|
if (!wax) var wax = {};
|
|
|
|
// A wrapper for reqwest jsonp to easily load TileJSON from a URL.
|
|
wax.tilejson = function(url, callback) {
|
|
reqwest({
|
|
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=?',
|
|
type: 'jsonp',
|
|
success: callback,
|
|
error: callback
|
|
});
|
|
};
|
|
var wax = wax || {};
|
|
wax.tooltip = {};
|
|
|
|
wax.tooltip = function() {
|
|
var popped = false,
|
|
animate = false,
|
|
t = {},
|
|
tooltips = [],
|
|
_currentContent,
|
|
transitionEvent,
|
|
parent;
|
|
|
|
if (document.body.style['-webkit-transition'] !== undefined) {
|
|
transitionEvent = 'webkitTransitionEnd';
|
|
} else if (document.body.style.MozTransition !== undefined) {
|
|
transitionEvent = 'transitionend';
|
|
}
|
|
|
|
// Get the active tooltip for a layer or create a new one if no tooltip exists.
|
|
// Hide any tooltips on layers underneath this one.
|
|
function getTooltip(feature) {
|
|
var tooltip = document.createElement('div');
|
|
tooltip.className = 'map-tooltip map-tooltip-0 wax-tooltip';
|
|
tooltip.innerHTML = feature;
|
|
return tooltip;
|
|
}
|
|
|
|
function remove() {
|
|
if (this.parentNode) this.parentNode.removeChild(this);
|
|
}
|
|
|
|
// Hide a given tooltip.
|
|
function hide() {
|
|
var _ct;
|
|
while (_ct = tooltips.pop()) {
|
|
if (animate && transitionEvent) {
|
|
// This code assumes that transform-supporting browsers
|
|
// also support proper events. IE9 does both.
|
|
bean.add(_ct, transitionEvent, remove);
|
|
_ct.className += ' map-fade';
|
|
} else {
|
|
if (_ct.parentNode) _ct.parentNode.removeChild(_ct);
|
|
}
|
|
}
|
|
}
|
|
|
|
function on(o) {
|
|
var content;
|
|
if (o.e.type === 'mousemove' || !o.e.type) {
|
|
if (!popped) {
|
|
content = o.content || o.formatter({ format: 'teaser' }, o.data);
|
|
if (!content || content == _currentContent) return;
|
|
hide();
|
|
parent.style.cursor = 'pointer';
|
|
tooltips.push(parent.appendChild(getTooltip(content)));
|
|
_currentContent = content;
|
|
}
|
|
} else {
|
|
content = o.content || o.formatter({ format: 'full' }, o.data);
|
|
if (!content) {
|
|
if (o.e.type && o.e.type.match(/touch/)) {
|
|
// fallback possible
|
|
content = o.content || o.formatter({ format: 'teaser' }, o.data);
|
|
}
|
|
// but if that fails, return just the same.
|
|
if (!content) return;
|
|
}
|
|
hide();
|
|
parent.style.cursor = 'pointer';
|
|
var tt = parent.appendChild(getTooltip(content));
|
|
tt.className += ' map-popup wax-popup';
|
|
|
|
var close = tt.appendChild(document.createElement('a'));
|
|
close.href = '#close';
|
|
close.className = 'close';
|
|
close.innerHTML = 'Close';
|
|
popped = true;
|
|
|
|
tooltips.push(tt);
|
|
|
|
bean.add(close, 'touchstart mousedown', function(e) {
|
|
e.stop();
|
|
});
|
|
|
|
bean.add(close, 'click touchend', function closeClick(e) {
|
|
e.stop();
|
|
hide();
|
|
popped = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
function off() {
|
|
parent.style.cursor = 'default';
|
|
_currentContent = null;
|
|
if (!popped) hide();
|
|
}
|
|
|
|
t.parent = function(x) {
|
|
if (!arguments.length) return parent;
|
|
parent = x;
|
|
return t;
|
|
};
|
|
|
|
t.animate = function(x) {
|
|
if (!arguments.length) return animate;
|
|
animate = x;
|
|
return t;
|
|
};
|
|
|
|
t.events = function() {
|
|
return {
|
|
on: on,
|
|
off: off
|
|
};
|
|
};
|
|
|
|
return t;
|
|
};
|
|
var wax = wax || {};
|
|
|
|
// Utils are extracted from other libraries or
|
|
// written from scratch to plug holes in browser compatibility.
|
|
wax.u = {
|
|
// From Bonzo
|
|
offset: function(el) {
|
|
// TODO: window margins
|
|
//
|
|
// Okay, so fall back to styles if offsetWidth and height are botched
|
|
// by Firefox.
|
|
var width = el.offsetWidth || parseInt(el.style.width, 10),
|
|
height = el.offsetHeight || parseInt(el.style.height, 10),
|
|
doc_body = document.body,
|
|
top = 0,
|
|
left = 0;
|
|
|
|
var calculateOffset = function(el) {
|
|
if (el === doc_body || el === document.documentElement) return;
|
|
top += el.offsetTop;
|
|
left += el.offsetLeft;
|
|
|
|
var style = el.style.transform ||
|
|
el.style.WebkitTransform ||
|
|
el.style.OTransform ||
|
|
el.style.MozTransform ||
|
|
el.style.msTransform;
|
|
|
|
if (style) {
|
|
var match;
|
|
if (match = style.match(/translate\((.+)[px]?, (.+)[px]?\)/)) {
|
|
top += parseInt(match[2], 10);
|
|
left += parseInt(match[1], 10);
|
|
} else if (match = style.match(/translate3d\((.+)[px]?, (.+)[px]?, (.+)[px]?\)/)) {
|
|
top += parseInt(match[2], 10);
|
|
left += parseInt(match[1], 10);
|
|
} else if (match = style.match(/matrix3d\(([\-\d,\s]+)\)/)) {
|
|
var pts = match[1].split(',');
|
|
top += parseInt(pts[13], 10);
|
|
left += parseInt(pts[12], 10);
|
|
} else if (match = style.match(/matrix\(.+, .+, .+, .+, (.+), (.+)\)/)) {
|
|
top += parseInt(match[2], 10);
|
|
left += parseInt(match[1], 10);
|
|
}
|
|
}
|
|
};
|
|
|
|
// from jquery, offset.js
|
|
if ( typeof el.getBoundingClientRect !== "undefined" ) {
|
|
var body = document.body;
|
|
var doc = el.ownerDocument.documentElement;
|
|
var clientTop = document.clientTop || body.clientTop || 0;
|
|
var clientLeft = document.clientLeft || body.clientLeft || 0;
|
|
var scrollTop = window.pageYOffset || doc.scrollTop;
|
|
var scrollLeft = window.pageXOffset || doc.scrollLeft;
|
|
|
|
var box = el.getBoundingClientRect();
|
|
top = box.top + scrollTop - clientTop;
|
|
left = box.left + scrollLeft - clientLeft;
|
|
|
|
} else {
|
|
calculateOffset(el);
|
|
try {
|
|
while (el = el.offsetParent) { calculateOffset(el); }
|
|
} catch(e) {
|
|
// Hello, internet explorer.
|
|
}
|
|
}
|
|
|
|
// Offsets from the body
|
|
top += doc_body.offsetTop;
|
|
left += doc_body.offsetLeft;
|
|
// Offsets from the HTML element
|
|
top += doc_body.parentNode.offsetTop;
|
|
left += doc_body.parentNode.offsetLeft;
|
|
|
|
// Firefox and other weirdos. Similar technique to jQuery's
|
|
// `doesNotIncludeMarginInBodyOffset`.
|
|
var htmlComputed = document.defaultView ?
|
|
window.getComputedStyle(doc_body.parentNode, null) :
|
|
doc_body.parentNode.currentStyle;
|
|
if (doc_body.parentNode.offsetTop !==
|
|
parseInt(htmlComputed.marginTop, 10) &&
|
|
!isNaN(parseInt(htmlComputed.marginTop, 10))) {
|
|
top += parseInt(htmlComputed.marginTop, 10);
|
|
left += parseInt(htmlComputed.marginLeft, 10);
|
|
}
|
|
|
|
return {
|
|
top: top,
|
|
left: left,
|
|
height: height,
|
|
width: width
|
|
};
|
|
},
|
|
|
|
'$': function(x) {
|
|
return (typeof x === 'string') ?
|
|
document.getElementById(x) :
|
|
x;
|
|
},
|
|
|
|
// From quirksmode: normalize the offset of an event from the top-left
|
|
// of the page.
|
|
eventoffset: function(e) {
|
|
var posx = 0;
|
|
var posy = 0;
|
|
if (!e) { e = window.event; }
|
|
if (e.pageX || e.pageY) {
|
|
// Good browsers
|
|
return {
|
|
x: e.pageX,
|
|
y: e.pageY
|
|
};
|
|
} else if (e.clientX || e.clientY) {
|
|
// Internet Explorer
|
|
return {
|
|
x: e.clientX,
|
|
y: e.clientY
|
|
};
|
|
} else if (e.touches && e.touches.length === 1) {
|
|
// Touch browsers
|
|
return {
|
|
x: e.touches[0].pageX,
|
|
y: e.touches[0].pageY
|
|
};
|
|
}
|
|
},
|
|
|
|
// Ripped from underscore.js
|
|
// Internal function used to implement `_.throttle` and `_.debounce`.
|
|
limit: function(func, wait, debounce) {
|
|
var timeout;
|
|
return function() {
|
|
var context = this, args = arguments;
|
|
var throttler = function() {
|
|
timeout = null;
|
|
func.apply(context, args);
|
|
};
|
|
if (debounce) clearTimeout(timeout);
|
|
if (debounce || !timeout) timeout = setTimeout(throttler, wait);
|
|
};
|
|
},
|
|
|
|
// Returns a function, that, when invoked, will only be triggered at most once
|
|
// during a given window of time.
|
|
throttle: function(func, wait) {
|
|
return this.limit(func, wait, false);
|
|
},
|
|
|
|
sanitize: function(content) {
|
|
if (!content) return '';
|
|
|
|
function urlX(url) {
|
|
// Data URIs are subject to a bug in Firefox
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107
|
|
// which let them be a vector. But WebKit does 'the right thing'
|
|
// or at least 'something' about this situation, so we'll tolerate
|
|
// them.
|
|
if (/^(https?:\/\/|data:image)/.test(url)) {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
function idX(id) { return id; }
|
|
|
|
return html_sanitize(content, urlX, idX);
|
|
}
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
wax.mm.attribution = function() {
|
|
var map,
|
|
a = {},
|
|
container = document.createElement('div');
|
|
|
|
container.className = 'map-attribution map-mm';
|
|
|
|
a.content = function(x) {
|
|
if (typeof x === 'undefined') return container.innerHTML;
|
|
container.innerHTML = wax.u.sanitize(x);
|
|
return a;
|
|
};
|
|
|
|
a.element = function() {
|
|
return container;
|
|
};
|
|
|
|
a.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return a;
|
|
};
|
|
|
|
a.add = function() {
|
|
if (!map) return false;
|
|
map.parent.appendChild(container);
|
|
return a;
|
|
};
|
|
|
|
a.remove = function() {
|
|
if (!map) return false;
|
|
if (container.parentNode) container.parentNode.removeChild(container);
|
|
return a;
|
|
};
|
|
|
|
a.appendTo = function(elem) {
|
|
wax.u.$(elem).appendChild(container);
|
|
return a;
|
|
};
|
|
|
|
return a;
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
wax.mm.boxselector = function() {
|
|
var corner,
|
|
nearCorner,
|
|
boxDiv,
|
|
style,
|
|
borderWidth = 0,
|
|
horizontal = false, // Whether the resize is horizontal
|
|
vertical = false,
|
|
edge = 5, // Distance from border sensitive to resizing
|
|
addEvent = MM.addEvent,
|
|
removeEvent = MM.removeEvent,
|
|
box,
|
|
boxselector = {},
|
|
map,
|
|
callbackManager = new MM.CallbackManager(boxselector, ['change']);
|
|
|
|
function getMousePoint(e) {
|
|
// start with just the mouse (x, y)
|
|
var point = new MM.Point(e.clientX, e.clientY);
|
|
// correct for scrolled document
|
|
point.x += document.body.scrollLeft +
|
|
document.documentElement.scrollLeft;
|
|
point.y += document.body.scrollTop +
|
|
document.documentElement.scrollTop;
|
|
|
|
// correct for nested offsets in DOM
|
|
for (var node = map.parent; node; node = node.offsetParent) {
|
|
point.x -= node.offsetLeft;
|
|
point.y -= node.offsetTop;
|
|
}
|
|
return point;
|
|
}
|
|
|
|
function mouseDown(e) {
|
|
if (!e.shiftKey) return;
|
|
|
|
corner = nearCorner = getMousePoint(e);
|
|
horizontal = vertical = true;
|
|
|
|
style.left = corner.x + 'px';
|
|
style.top = corner.y + 'px';
|
|
style.width = style.height = 0;
|
|
|
|
addEvent(document, 'mousemove', mouseMove);
|
|
addEvent(document, 'mouseup', mouseUp);
|
|
|
|
map.parent.style.cursor = 'crosshair';
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
// Resize existing box
|
|
function mouseDownResize(e) {
|
|
var point = getMousePoint(e),
|
|
TL = {
|
|
x: parseInt(boxDiv.offsetLeft, 10),
|
|
y: parseInt(boxDiv.offsetTop, 10)
|
|
},
|
|
BR = {
|
|
x: TL.x + parseInt(boxDiv.offsetWidth, 10),
|
|
y: TL.y + parseInt(boxDiv.offsetHeight, 10)
|
|
};
|
|
|
|
// Determine whether resize is horizontal, vertical or both
|
|
horizontal = point.x - TL.x <= edge || BR.x - point.x <= edge;
|
|
vertical = point.y - TL.y <= edge || BR.y - point.y <= edge;
|
|
|
|
if (vertical || horizontal) {
|
|
corner = {
|
|
x: (point.x - TL.x < BR.x - point.x) ? BR.x : TL.x,
|
|
y: (point.y - TL.y < BR.y - point.y) ? BR.y : TL.y
|
|
};
|
|
nearCorner = {
|
|
x: (point.x - TL.x < BR.x - point.x) ? TL.x : BR.x,
|
|
y: (point.y - TL.y < BR.y - point.y) ? TL.y : BR.y
|
|
};
|
|
addEvent(document, 'mousemove', mouseMove);
|
|
addEvent(document, 'mouseup', mouseUp);
|
|
return MM.cancelEvent(e);
|
|
}
|
|
}
|
|
|
|
function mouseMove(e) {
|
|
var point = getMousePoint(e);
|
|
style.display = 'block';
|
|
if (horizontal) {
|
|
style.left = (point.x < corner.x ? point.x : corner.x) + 'px';
|
|
style.width = Math.abs(point.x - corner.x) - 2 * borderWidth + 'px';
|
|
}
|
|
if (vertical) {
|
|
style.top = (point.y < corner.y ? point.y : corner.y) + 'px';
|
|
style.height = Math.abs(point.y - corner.y) - 2 * borderWidth + 'px';
|
|
}
|
|
changeCursor(point, map.parent);
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function mouseUp(e) {
|
|
var point = getMousePoint(e),
|
|
l1 = map.pointLocation( new MM.Point(
|
|
horizontal ? point.x : nearCorner.x,
|
|
vertical? point.y : nearCorner.y
|
|
));
|
|
l2 = map.pointLocation(corner);
|
|
|
|
// Format coordinates like mm.map.getExtent().
|
|
boxselector.extent([
|
|
new MM.Location(
|
|
Math.max(l1.lat, l2.lat),
|
|
Math.min(l1.lon, l2.lon)),
|
|
new MM.Location(
|
|
Math.min(l1.lat, l2.lat),
|
|
Math.max(l1.lon, l2.lon))
|
|
]);
|
|
|
|
removeEvent(document, 'mousemove', mouseMove);
|
|
removeEvent(document, 'mouseup', mouseUp);
|
|
|
|
map.parent.style.cursor = 'auto';
|
|
}
|
|
|
|
function mouseMoveCursor(e) {
|
|
changeCursor(getMousePoint(e), boxDiv);
|
|
}
|
|
|
|
// Set resize cursor if mouse is on edge
|
|
function changeCursor(point, elem) {
|
|
var TL = {
|
|
x: parseInt(boxDiv.offsetLeft, 10),
|
|
y: parseInt(boxDiv.offsetTop, 10)
|
|
},
|
|
BR = {
|
|
x: TL.x + parseInt(boxDiv.offsetWidth, 10),
|
|
y: TL.y + parseInt(boxDiv.offsetHeight, 10)
|
|
};
|
|
// Build cursor style string
|
|
var prefix = '';
|
|
if (point.y - TL.y <= edge) prefix = 'n';
|
|
else if (BR.y - point.y <= edge) prefix = 's';
|
|
if (point.x - TL.x <= edge) prefix += 'w';
|
|
else if (BR.x - point.x <= edge) prefix += 'e';
|
|
if (prefix !== '') prefix += '-resize';
|
|
elem.style.cursor = prefix;
|
|
}
|
|
|
|
function drawbox(map, e) {
|
|
if (!boxDiv || !box) return;
|
|
var br = map.locationPoint(box[1]),
|
|
tl = map.locationPoint(box[0]),
|
|
style = boxDiv.style;
|
|
|
|
style.display = 'block';
|
|
style.height = 'auto';
|
|
style.width = 'auto';
|
|
style.left = Math.max(0, tl.x) + 'px';
|
|
style.top = Math.max(0, tl.y) + 'px';
|
|
style.right = Math.max(0, map.dimensions.x - br.x) + 'px';
|
|
style.bottom = Math.max(0, map.dimensions.y - br.y) + 'px';
|
|
}
|
|
|
|
boxselector.addCallback = function(event, callback) {
|
|
callbackManager.addCallback(event, callback);
|
|
return boxselector;
|
|
};
|
|
|
|
boxselector.removeCallback = function(event, callback) {
|
|
callbackManager.removeCallback(event, callback);
|
|
return boxselector;
|
|
};
|
|
|
|
boxselector.extent = function(x, silent) {
|
|
if (!x) return box;
|
|
|
|
box = [
|
|
new MM.Location(
|
|
Math.max(x[0].lat, x[1].lat),
|
|
Math.min(x[0].lon, x[1].lon)),
|
|
new MM.Location(
|
|
Math.min(x[0].lat, x[1].lat),
|
|
Math.max(x[0].lon, x[1].lon))
|
|
];
|
|
|
|
drawbox(map);
|
|
|
|
if (!silent) callbackManager.dispatchCallback('change', box);
|
|
};
|
|
boxDiv = document.createElement('div');
|
|
boxDiv.className = 'boxselector-box';
|
|
style = boxDiv.style;
|
|
|
|
boxselector.add = function() {
|
|
boxDiv.id = map.parent.id + '-boxselector-box';
|
|
map.parent.appendChild(boxDiv);
|
|
borderWidth = parseInt(window.getComputedStyle(boxDiv).borderWidth, 10);
|
|
|
|
addEvent(map.parent, 'mousedown', mouseDown);
|
|
addEvent(boxDiv, 'mousedown', mouseDownResize);
|
|
addEvent(map.parent, 'mousemove', mouseMoveCursor);
|
|
map.addCallback('drawn', drawbox);
|
|
return boxselector;
|
|
};
|
|
|
|
boxselector.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return boxselector;
|
|
};
|
|
|
|
boxselector.remove = function() {
|
|
map.parent.removeChild(boxDiv);
|
|
|
|
removeEvent(map.parent, 'mousedown', mouseDown);
|
|
removeEvent(boxDiv, 'mousedown', mouseDownResize);
|
|
removeEvent(map.parent, 'mousemove', mouseMoveCursor);
|
|
|
|
map.removeCallback('drawn', drawbox);
|
|
return boxselector;
|
|
};
|
|
|
|
return boxselector;
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
wax._ = {};
|
|
|
|
wax.mm.bwdetect = function(map, options) {
|
|
options = options || {};
|
|
var lowpng = options.png || '.png128',
|
|
lowjpg = options.jpg || '.jpg70',
|
|
bw = false;
|
|
|
|
wax._.bw_png = lowpng;
|
|
wax._.bw_jpg = lowjpg;
|
|
|
|
return wax.bwdetect(options, function(x) {
|
|
wax._.bw = !x;
|
|
for (var i = 0; i < map.layers.length; i++) {
|
|
if (map.getLayerAt(i).provider instanceof wax.mm.connector) {
|
|
map.getLayerAt(i).setProvider(map.getLayerAt(i).provider);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
// Add zoom links, which can be styled as buttons, to a `modestmaps.Map`
|
|
// control. This function can be used chaining-style with other
|
|
// chaining-style controls.
|
|
wax.mm.fullscreen = function() {
|
|
// true: fullscreen
|
|
// false: minimized
|
|
var fullscreened = false,
|
|
fullscreen = {},
|
|
a = document.createElement('a'),
|
|
map,
|
|
body = document.body,
|
|
dimensions;
|
|
|
|
a.className = 'map-fullscreen';
|
|
a.href = '#fullscreen';
|
|
// a.innerHTML = 'fullscreen';
|
|
|
|
function click(e) {
|
|
if (e) e.stop();
|
|
if (fullscreened) {
|
|
fullscreen.original();
|
|
} else {
|
|
fullscreen.full();
|
|
}
|
|
}
|
|
|
|
fullscreen.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return fullscreen;
|
|
};
|
|
|
|
// Modest Maps demands an absolute height & width, and doesn't auto-correct
|
|
// for changes, so here we save the original size of the element and
|
|
// restore to that size on exit from fullscreen.
|
|
fullscreen.add = function() {
|
|
bean.add(a, 'click', click);
|
|
map.parent.appendChild(a);
|
|
return fullscreen;
|
|
};
|
|
|
|
fullscreen.remove = function() {
|
|
bean.remove(a, 'click', click);
|
|
if (a.parentNode) a.parentNode.removeChild(a);
|
|
return fullscreen;
|
|
};
|
|
|
|
fullscreen.full = function() {
|
|
if (fullscreened) { return; } else { fullscreened = true; }
|
|
dimensions = map.dimensions;
|
|
map.parent.className += ' map-fullscreen-map';
|
|
body.className += ' map-fullscreen-view';
|
|
map.dimensions = { x: map.parent.offsetWidth, y: map.parent.offsetHeight };
|
|
map.draw();
|
|
return fullscreen;
|
|
};
|
|
|
|
fullscreen.original = function() {
|
|
if (!fullscreened) { return; } else { fullscreened = false; }
|
|
map.parent.className = map.parent.className.replace(' map-fullscreen-map', '');
|
|
body.className = body.className.replace(' map-fullscreen-view', '');
|
|
map.dimensions = dimensions;
|
|
map.draw();
|
|
return fullscreen;
|
|
};
|
|
|
|
fullscreen.fullscreen = function(x) {
|
|
if (!arguments.length) {
|
|
return fullscreened;
|
|
} else {
|
|
if (x && !fullscreened) {
|
|
fullscreen.full();
|
|
} else if (!x && fullscreened) {
|
|
fullscreen.original();
|
|
}
|
|
return fullscreen;
|
|
}
|
|
};
|
|
|
|
fullscreen.element = function() {
|
|
return a;
|
|
};
|
|
|
|
fullscreen.appendTo = function(elem) {
|
|
wax.u.$(elem).appendChild(a);
|
|
return fullscreen;
|
|
};
|
|
|
|
return fullscreen;
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
wax.mm.hash = function() {
|
|
var map;
|
|
var hash = wax.hash({
|
|
getCenterZoom: function() {
|
|
var center = map.getCenter(),
|
|
zoom = map.getZoom(),
|
|
precision = Math.max(
|
|
0,
|
|
Math.ceil(Math.log(zoom) / Math.LN2));
|
|
|
|
return [zoom.toFixed(2),
|
|
center.lat.toFixed(precision),
|
|
center.lon.toFixed(precision)
|
|
].join('/');
|
|
},
|
|
setCenterZoom: function setCenterZoom(args) {
|
|
map.setCenterZoom(
|
|
new MM.Location(args[1], args[2]),
|
|
args[0]);
|
|
},
|
|
bindChange: function(fn) {
|
|
map.addCallback('drawn', fn);
|
|
},
|
|
unbindChange: function(fn) {
|
|
map.removeCallback('drawn', fn);
|
|
}
|
|
});
|
|
|
|
hash.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return hash;
|
|
};
|
|
|
|
return hash;
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
wax.mm.interaction = function() {
|
|
var dirty = false,
|
|
_grid,
|
|
map,
|
|
clearingEvents = ['zoomed', 'panned', 'centered',
|
|
'extentset', 'resized', 'drawn'];
|
|
|
|
function grid() {
|
|
if (!dirty && _grid !== undefined && _grid.length) {
|
|
return _grid;
|
|
} else {
|
|
var tiles;
|
|
for (var i = 0; i < map.getLayers().length; i++) {
|
|
var levels = map.getLayerAt(i).levels;
|
|
var zoomLayer = levels && levels[Math.round(map.zoom())];
|
|
if (zoomLayer !== undefined) {
|
|
tiles = map.getLayerAt(i).tileElementsInLevel(zoomLayer);
|
|
if (tiles.length) break;
|
|
}
|
|
}
|
|
_grid = (function(t) {
|
|
var o = [];
|
|
for (var key in t) {
|
|
if (t[key].parentNode === zoomLayer) {
|
|
var offset = wax.u.offset(t[key]);
|
|
o.push([
|
|
offset.top,
|
|
offset.left,
|
|
t[key]
|
|
]);
|
|
}
|
|
}
|
|
return o;
|
|
})(tiles);
|
|
return _grid;
|
|
}
|
|
}
|
|
|
|
function setdirty() { dirty = true; }
|
|
|
|
function attach(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
for (var i = 0; i < clearingEvents.length; i++) {
|
|
map.addCallback(clearingEvents[i], setdirty);
|
|
}
|
|
}
|
|
|
|
function detach(x) {
|
|
for (var i = 0; i < clearingEvents.length; i++) {
|
|
map.removeCallback(clearingEvents[i], setdirty);
|
|
}
|
|
}
|
|
|
|
return wax.interaction()
|
|
.attach(attach)
|
|
.detach(detach)
|
|
.parent(function() {
|
|
return map.parent;
|
|
})
|
|
.grid(grid);
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
wax.mm.legend = function() {
|
|
var map,
|
|
l = {};
|
|
|
|
var container = document.createElement('div');
|
|
container.className = 'wax-legends map-legends';
|
|
|
|
var element = container.appendChild(document.createElement('div'));
|
|
element.className = 'wax-legend map-legend';
|
|
element.style.display = 'none';
|
|
|
|
l.content = function(x) {
|
|
if (!arguments.length) return element.innerHTML;
|
|
|
|
element.innerHTML = wax.u.sanitize(x);
|
|
element.style.display = 'block';
|
|
if (element.innerHTML === '') {
|
|
element.style.display = 'none';
|
|
}
|
|
return l;
|
|
};
|
|
|
|
l.element = function() {
|
|
return container;
|
|
};
|
|
|
|
l.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return l;
|
|
};
|
|
|
|
l.add = function() {
|
|
if (!map) return false;
|
|
l.appendTo(map.parent);
|
|
return l;
|
|
};
|
|
|
|
l.remove = function() {
|
|
if (container.parentNode) {
|
|
container.parentNode.removeChild(container);
|
|
}
|
|
return l;
|
|
};
|
|
|
|
l.appendTo = function(elem) {
|
|
wax.u.$(elem).appendChild(container);
|
|
return l;
|
|
};
|
|
|
|
return l;
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
// This takes an object of options:
|
|
//
|
|
// * `callback`: a function called with an array of `com.modestmaps.Location`
|
|
// objects when the map is edited
|
|
//
|
|
// It also exposes a public API function: `addLocation`, which adds a point
|
|
// to the map as if added by the user.
|
|
wax.mm.pointselector = function() {
|
|
var map,
|
|
mouseDownPoint = null,
|
|
mouseUpPoint = null,
|
|
callback = null,
|
|
tolerance = 5,
|
|
overlayDiv,
|
|
pointselector = {},
|
|
callbackManager = new MM.CallbackManager(pointselector, ['change']),
|
|
locations = [];
|
|
|
|
// Create a `MM.Point` from a screen event, like a click.
|
|
function makePoint(e) {
|
|
var coords = wax.u.eventoffset(e);
|
|
var point = new MM.Point(coords.x, coords.y);
|
|
// correct for scrolled document
|
|
|
|
// and for the document
|
|
var body = {
|
|
x: parseFloat(MM.getStyle(document.documentElement, 'margin-left')),
|
|
y: parseFloat(MM.getStyle(document.documentElement, 'margin-top'))
|
|
};
|
|
|
|
if (!isNaN(body.x)) point.x -= body.x;
|
|
if (!isNaN(body.y)) point.y -= body.y;
|
|
|
|
// TODO: use wax.util.offset
|
|
// correct for nested offsets in DOM
|
|
for (var node = map.parent; node; node = node.offsetParent) {
|
|
point.x -= node.offsetLeft;
|
|
point.y -= node.offsetTop;
|
|
}
|
|
return point;
|
|
}
|
|
|
|
// Currently locations in this control contain circular references to elements.
|
|
// These can't be JSON encoded, so here's a utility to clean the data that's
|
|
// spit back.
|
|
function cleanLocations(locations) {
|
|
var o = [];
|
|
for (var i = 0; i < locations.length; i++) {
|
|
o.push(new MM.Location(locations[i].lat, locations[i].lon));
|
|
}
|
|
return o;
|
|
}
|
|
|
|
// Attach this control to a map by registering callbacks
|
|
// and adding the overlay
|
|
|
|
// Redraw the points when the map is moved, so that they stay in the
|
|
// correct geographic locations.
|
|
function drawPoints() {
|
|
var offset = new MM.Point(0, 0);
|
|
for (var i = 0; i < locations.length; i++) {
|
|
var point = map.locationPoint(locations[i]);
|
|
if (!locations[i].pointDiv) {
|
|
locations[i].pointDiv = document.createElement('div');
|
|
locations[i].pointDiv.className = 'map-point-div';
|
|
locations[i].pointDiv.style.position = 'absolute';
|
|
locations[i].pointDiv.style.display = 'block';
|
|
// TODO: avoid circular reference
|
|
locations[i].pointDiv.location = locations[i];
|
|
// Create this closure once per point
|
|
bean.add(locations[i].pointDiv, 'mouseup',
|
|
(function selectPointWrap(e) {
|
|
var l = locations[i];
|
|
return function(e) {
|
|
MM.removeEvent(map.parent, 'mouseup', mouseUp);
|
|
pointselector.deleteLocation(l, e);
|
|
};
|
|
})());
|
|
map.parent.appendChild(locations[i].pointDiv);
|
|
}
|
|
locations[i].pointDiv.style.left = point.x + 'px';
|
|
locations[i].pointDiv.style.top = point.y + 'px';
|
|
}
|
|
}
|
|
|
|
function mouseDown(e) {
|
|
mouseDownPoint = makePoint(e);
|
|
bean.add(map.parent, 'mouseup', mouseUp);
|
|
}
|
|
|
|
// Remove the awful circular reference from locations.
|
|
// TODO: This function should be made unnecessary by not having it.
|
|
function mouseUp(e) {
|
|
if (!mouseDownPoint) return;
|
|
mouseUpPoint = makePoint(e);
|
|
if (MM.Point.distance(mouseDownPoint, mouseUpPoint) < tolerance) {
|
|
pointselector.addLocation(map.pointLocation(mouseDownPoint));
|
|
callbackManager.dispatchCallback('change', cleanLocations(locations));
|
|
}
|
|
mouseDownPoint = null;
|
|
}
|
|
|
|
// API for programmatically adding points to the map - this
|
|
// calls the callback for ever point added, so it can be symmetrical.
|
|
// Useful for initializing the map when it's a part of a form.
|
|
pointselector.addLocation = function(location) {
|
|
locations.push(location);
|
|
drawPoints();
|
|
callbackManager.dispatchCallback('change', cleanLocations(locations));
|
|
return pointselector;
|
|
};
|
|
|
|
// TODO set locations
|
|
pointselector.locations = function() {
|
|
if (!arguments.length) return locations;
|
|
};
|
|
|
|
pointselector.addCallback = function(event, callback) {
|
|
callbackManager.addCallback(event, callback);
|
|
return pointselector;
|
|
};
|
|
|
|
pointselector.removeCallback = function(event, callback) {
|
|
callbackManager.removeCallback(event, callback);
|
|
return pointselector;
|
|
};
|
|
|
|
pointselector.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return pointselector;
|
|
};
|
|
|
|
pointselector.add = function() {
|
|
bean.add(map.parent, 'mousedown', mouseDown);
|
|
map.addCallback('drawn', drawPoints);
|
|
return pointselector;
|
|
};
|
|
|
|
pointselector.remove = function() {
|
|
bean.remove(map.parent, 'mousedown', mouseDown);
|
|
map.removeCallback('drawn', drawPoints);
|
|
for (var i = locations.length - 1; i > -1; i--) {
|
|
pointselector.deleteLocation(locations[i]);
|
|
}
|
|
return pointselector;
|
|
};
|
|
|
|
pointselector.deleteLocation = function(location, e) {
|
|
if (!e || confirm('Delete this point?')) {
|
|
location.pointDiv.parentNode.removeChild(location.pointDiv);
|
|
for (var i = 0; i < locations.length; i++) {
|
|
if (locations[i] === location) {
|
|
locations.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
callbackManager.dispatchCallback('change', cleanLocations(locations));
|
|
}
|
|
};
|
|
|
|
return pointselector;
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
wax.mm.zoombox = function() {
|
|
// TODO: respond to resize
|
|
var zoombox = {},
|
|
map,
|
|
drawing = false,
|
|
box = document.createElement('div'),
|
|
mouseDownPoint = null;
|
|
|
|
function getMousePoint(e) {
|
|
// start with just the mouse (x, y)
|
|
var point = new MM.Point(e.clientX, e.clientY);
|
|
// correct for scrolled document
|
|
point.x += document.body.scrollLeft + document.documentElement.scrollLeft;
|
|
point.y += document.body.scrollTop + document.documentElement.scrollTop;
|
|
|
|
// correct for nested offsets in DOM
|
|
for (var node = map.parent; node; node = node.offsetParent) {
|
|
point.x -= node.offsetLeft;
|
|
point.y -= node.offsetTop;
|
|
}
|
|
return point;
|
|
}
|
|
|
|
function mouseUp(e) {
|
|
if (!drawing) return;
|
|
|
|
drawing = false;
|
|
var point = getMousePoint(e);
|
|
|
|
var l1 = map.pointLocation(point),
|
|
l2 = map.pointLocation(mouseDownPoint);
|
|
|
|
map.setExtent([l1, l2]);
|
|
|
|
box.style.display = 'none';
|
|
MM.removeEvent(map.parent, 'mousemove', mouseMove);
|
|
MM.removeEvent(map.parent, 'mouseup', mouseUp);
|
|
|
|
map.parent.style.cursor = 'auto';
|
|
}
|
|
|
|
function mouseDown(e) {
|
|
if (!(e.shiftKey && !this.drawing)) return;
|
|
|
|
drawing = true;
|
|
mouseDownPoint = getMousePoint(e);
|
|
|
|
box.style.left = mouseDownPoint.x + 'px';
|
|
box.style.top = mouseDownPoint.y + 'px';
|
|
|
|
MM.addEvent(map.parent, 'mousemove', mouseMove);
|
|
MM.addEvent(map.parent, 'mouseup', mouseUp);
|
|
|
|
map.parent.style.cursor = 'crosshair';
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function mouseMove(e) {
|
|
if (!drawing) return;
|
|
|
|
var point = getMousePoint(e);
|
|
box.style.display = 'block';
|
|
if (point.x < mouseDownPoint.x) {
|
|
box.style.left = point.x + 'px';
|
|
} else {
|
|
box.style.left = mouseDownPoint.x + 'px';
|
|
}
|
|
box.style.width = Math.abs(point.x - mouseDownPoint.x) + 'px';
|
|
if (point.y < mouseDownPoint.y) {
|
|
box.style.top = point.y + 'px';
|
|
} else {
|
|
box.style.top = mouseDownPoint.y + 'px';
|
|
}
|
|
box.style.height = Math.abs(point.y - mouseDownPoint.y) + 'px';
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
zoombox.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return zoombox;
|
|
};
|
|
|
|
zoombox.add = function() {
|
|
if (!map) return false;
|
|
// Use a flag to determine whether the zoombox is currently being
|
|
// drawn. Necessary only for IE because `mousedown` is triggered
|
|
// twice.
|
|
box.id = map.parent.id + '-zoombox-box';
|
|
box.className = 'zoombox-box';
|
|
map.parent.appendChild(box);
|
|
MM.addEvent(map.parent, 'mousedown', mouseDown);
|
|
return this;
|
|
};
|
|
|
|
zoombox.remove = function() {
|
|
if (!map) return false;
|
|
if (box.parentNode) box.parentNode.removeChild(box);
|
|
MM.removeEvent(map.parent, 'mousedown', mouseDown);
|
|
return zoombox;
|
|
};
|
|
|
|
return zoombox;
|
|
};
|
|
wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
wax.mm.zoomer = function() {
|
|
var zoomer = {},
|
|
smooth = true,
|
|
map;
|
|
|
|
var zoomin = document.createElement('a'),
|
|
zoomout = document.createElement('a');
|
|
|
|
function stopEvents(e) {
|
|
e.stop();
|
|
}
|
|
|
|
function zIn(e) {
|
|
e.stop();
|
|
if (smooth && map.ease) {
|
|
map.ease.zoom(map.zoom() + 1).run(50);
|
|
} else {
|
|
map.zoomIn();
|
|
}
|
|
}
|
|
|
|
function zOut(e) {
|
|
e.stop();
|
|
if (smooth && map.ease) {
|
|
map.ease.zoom(map.zoom() - 1).run(50);
|
|
} else {
|
|
map.zoomOut();
|
|
}
|
|
}
|
|
|
|
zoomin.innerHTML = '+';
|
|
zoomin.href = '#';
|
|
zoomin.className = 'zoomer zoomin';
|
|
zoomout.innerHTML = '-';
|
|
zoomout.href = '#';
|
|
zoomout.className = 'zoomer zoomout';
|
|
|
|
function updateButtons(map, e) {
|
|
if (map.coordinate.zoom === map.coordLimits[0].zoom) {
|
|
zoomout.className = 'zoomer zoomout zoomdisabled';
|
|
} else if (map.coordinate.zoom === map.coordLimits[1].zoom) {
|
|
zoomin.className = 'zoomer zoomin zoomdisabled';
|
|
} else {
|
|
zoomin.className = 'zoomer zoomin';
|
|
zoomout.className = 'zoomer zoomout';
|
|
}
|
|
}
|
|
|
|
zoomer.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return zoomer;
|
|
};
|
|
|
|
zoomer.add = function() {
|
|
if (!map) return false;
|
|
map.addCallback('drawn', updateButtons);
|
|
zoomer.appendTo(map.parent);
|
|
bean.add(zoomin, 'mousedown dblclick', stopEvents);
|
|
bean.add(zoomout, 'mousedown dblclick', stopEvents);
|
|
bean.add(zoomout, 'touchstart click', zOut);
|
|
bean.add(zoomin, 'touchstart click', zIn);
|
|
return zoomer;
|
|
};
|
|
|
|
zoomer.remove = function() {
|
|
if (!map) return false;
|
|
map.removeCallback('drawn', updateButtons);
|
|
if (zoomin.parentNode) zoomin.parentNode.removeChild(zoomin);
|
|
if (zoomout.parentNode) zoomout.parentNode.removeChild(zoomout);
|
|
bean.remove(zoomin, 'mousedown dblclick', stopEvents);
|
|
bean.remove(zoomout, 'mousedown dblclick', stopEvents);
|
|
bean.remove(zoomout, 'touchstart click', zOut);
|
|
bean.remove(zoomin, 'touchstart click', zIn);
|
|
return zoomer;
|
|
};
|
|
|
|
zoomer.appendTo = function(elem) {
|
|
wax.u.$(elem).appendChild(zoomin);
|
|
wax.u.$(elem).appendChild(zoomout);
|
|
return zoomer;
|
|
};
|
|
|
|
zoomer.smooth = function(x) {
|
|
if (!arguments.length) return smooth;
|
|
smooth = x;
|
|
return zoomer;
|
|
};
|
|
|
|
return zoomer;
|
|
};
|
|
var wax = wax || {};
|
|
wax.mm = wax.mm || {};
|
|
|
|
// A layer connector for Modest Maps conformant to TileJSON
|
|
// https://github.com/mapbox/tilejson
|
|
wax.mm._provider = function(options) {
|
|
this.options = {
|
|
tiles: options.tiles,
|
|
scheme: options.scheme || 'xyz',
|
|
minzoom: options.minzoom || 0,
|
|
maxzoom: options.maxzoom || 22,
|
|
bounds: options.bounds || [-180, -90, 180, 90]
|
|
};
|
|
};
|
|
|
|
wax.mm._provider.prototype = {
|
|
outerLimits: function() {
|
|
return [
|
|
this.locationCoordinate(
|
|
new MM.Location(
|
|
this.options.bounds[0],
|
|
this.options.bounds[1])).zoomTo(this.options.minzoom),
|
|
this.locationCoordinate(
|
|
new MM.Location(
|
|
this.options.bounds[2],
|
|
this.options.bounds[3])).zoomTo(this.options.maxzoom)
|
|
];
|
|
},
|
|
getTile: function(c) {
|
|
var coord;
|
|
if (!(coord = this.sourceCoordinate(c))) return null;
|
|
if (coord.zoom < this.options.minzoom || coord.zoom > this.options.maxzoom) return null;
|
|
|
|
coord.row = (this.options.scheme === 'tms') ?
|
|
Math.pow(2, coord.zoom) - coord.row - 1 :
|
|
coord.row;
|
|
|
|
var u = this.options.tiles[parseInt(Math.pow(2, coord.zoom) * coord.row + coord.column, 10) %
|
|
this.options.tiles.length]
|
|
.replace('{z}', coord.zoom.toFixed(0))
|
|
.replace('{x}', coord.column.toFixed(0))
|
|
.replace('{y}', coord.row.toFixed(0));
|
|
|
|
if (wax._ && wax._.bw) {
|
|
u = u.replace('.png', wax._.bw_png)
|
|
.replace('.jpg', wax._.bw_jpg);
|
|
}
|
|
|
|
return u;
|
|
}
|
|
};
|
|
|
|
if (MM) {
|
|
MM.extend(wax.mm._provider, MM.MapProvider);
|
|
}
|
|
|
|
wax.mm.connector = function(options) {
|
|
var x = new wax.mm._provider(options);
|
|
return new MM.Layer(x);
|
|
};
|
|
;(function(context, MM) {
|
|
var easey = function() {
|
|
var easey = {},
|
|
running = false,
|
|
abort = false, // killswitch for transitions
|
|
abortCallback; // callback called when aborted
|
|
|
|
var easings = {
|
|
easeIn: function(t) { return t * t; },
|
|
easeOut: function(t) { return Math.sin(t * Math.PI / 2); },
|
|
easeInOut: function(t) { return (1 - Math.cos(Math.PI * t)) / 2; },
|
|
linear: function(t) { return t; }
|
|
};
|
|
var easing = easings.easeOut;
|
|
|
|
// to is the singular coordinate that any transition is based off
|
|
// three dimensions:
|
|
//
|
|
// * to
|
|
// * time
|
|
// * path
|
|
var from, to, map;
|
|
|
|
easey.stop = function(callback) {
|
|
abort = true;
|
|
from = undefined;
|
|
abortCallback = callback;
|
|
};
|
|
|
|
easey.running = function() {
|
|
return running;
|
|
};
|
|
|
|
easey.point = function(x) {
|
|
to = map.pointCoordinate(x);
|
|
return easey;
|
|
};
|
|
|
|
easey.zoom = function(x) {
|
|
if (!to) to = map.coordinate.copy();
|
|
to = map.enforceZoomLimits(to.zoomTo(x));
|
|
return easey;
|
|
};
|
|
|
|
easey.location = function(x) {
|
|
to = map.locationCoordinate(x);
|
|
return easey;
|
|
};
|
|
|
|
easey.from = function(x) {
|
|
if (!arguments.length) return from ? from.copy() : from;
|
|
from = x.copy();
|
|
return easey;
|
|
};
|
|
|
|
easey.to = function(x) {
|
|
if (!arguments.length) return to.copy();
|
|
to = map.enforceZoomLimits(x.copy());
|
|
return easey;
|
|
};
|
|
|
|
easey.path = function(x) {
|
|
path = paths[x];
|
|
return easey;
|
|
};
|
|
|
|
easey.easing = function(x) {
|
|
easing = easings[x];
|
|
return easey;
|
|
};
|
|
|
|
easey.map = function(x) {
|
|
if (!arguments.length) return map;
|
|
map = x;
|
|
return easey;
|
|
};
|
|
|
|
function interp(a, b, p) {
|
|
if (p === 0) return a;
|
|
if (p === 1) return b;
|
|
return a + ((b - a) * p);
|
|
}
|
|
|
|
var paths = {},
|
|
static_coord = new MM.Coordinate(0, 0, 0);
|
|
|
|
// The screen path simply moves between
|
|
// coordinates in a non-geographical way
|
|
paths.screen = function(a, b, t, static_coord) {
|
|
var zoom_lerp = interp(a.zoom, b.zoom, t);
|
|
if (static_coord) {
|
|
static_coord.row = interp(
|
|
a.row,
|
|
b.row * Math.pow(2, a.zoom - b.zoom),
|
|
t) * Math.pow(2, zoom_lerp - a.zoom);
|
|
static_coord.column = interp(
|
|
a.column,
|
|
b.column * Math.pow(2, a.zoom - b.zoom),
|
|
t) * Math.pow(2, zoom_lerp - a.zoom);
|
|
static_coord.zoom = zoom_lerp;
|
|
} else {
|
|
return new MM.Coordinate(
|
|
interp(a.row,
|
|
b.row * Math.pow(2, a.zoom - b.zoom),
|
|
t) * Math.pow(2, zoom_lerp - a.zoom),
|
|
interp(a.column,
|
|
b.column * Math.pow(2, a.zoom - b.zoom),
|
|
t) * Math.pow(2, zoom_lerp - a.zoom),
|
|
zoom_lerp);
|
|
}
|
|
};
|
|
|
|
// The screen path means that the b
|
|
// coordinate should maintain its point on screen
|
|
// throughout the transition, but the map
|
|
// should move to its zoom level
|
|
paths.about = function(a, b, t, static_coord) {
|
|
var zoom_lerp = interp(a.zoom, b.zoom, t);
|
|
|
|
// center x, center y
|
|
var cx = map.dimensions.x / 2,
|
|
cy = map.dimensions.y / 2,
|
|
// tilesize
|
|
tx = map.tileSize.x,
|
|
ty = map.tileSize.y;
|
|
|
|
var startx = cx + tx * ((b.column * Math.pow(2, a.zoom - b.zoom)) - a.column);
|
|
var starty = cy + ty * ((b.row * Math.pow(2, a.zoom - b.zoom)) - a.row);
|
|
|
|
var endx = cx + tx * ((b.column * Math.pow(2, zoom_lerp - b.zoom)) -
|
|
(a.column * Math.pow(2, zoom_lerp - a.zoom)));
|
|
var endy = cy + ty * ((b.row * Math.pow(2, zoom_lerp - b.zoom)) - (a.row *
|
|
Math.pow(2, zoom_lerp - a.zoom)));
|
|
|
|
if (static_coord) {
|
|
static_coord.column = (a.column * Math.pow(2, zoom_lerp - a.zoom)) - ((startx - endx) / tx);
|
|
static_coord.row = (a.row * Math.pow(2, zoom_lerp - a.zoom)) - ((starty - endy) / ty);
|
|
static_coord.zoom = zoom_lerp;
|
|
} else {
|
|
return new MM.Coordinate(
|
|
(a.column * Math.pow(2, zoom_lerp - a.zoom)) - ((startx - endx) / tx),
|
|
(a.row * Math.pow(2, zoom_lerp - a.zoom)) - ((starty - endy) / ty),
|
|
zoom_lerp);
|
|
}
|
|
};
|
|
|
|
var path = paths.screen;
|
|
|
|
easey.t = function(t) {
|
|
path(from, to, easing(t), static_coord);
|
|
map.coordinate = static_coord;
|
|
map.draw();
|
|
return easey;
|
|
};
|
|
|
|
easey.future = function(parts) {
|
|
var futures = [];
|
|
for (var t = 0; t < parts; t++) {
|
|
futures.push(path(from, to, t / (parts - 1)));
|
|
}
|
|
return futures;
|
|
};
|
|
|
|
var start;
|
|
easey.resetRun = function () {
|
|
start = (+ new Date());
|
|
return easey;
|
|
};
|
|
|
|
easey.run = function(time, callback) {
|
|
|
|
if (running) return easey.stop(function() {
|
|
easey.run(time, callback);
|
|
});
|
|
|
|
if (!from) from = map.coordinate.copy();
|
|
if (!to) to = map.coordinate.copy();
|
|
time = time || 1000;
|
|
start = (+new Date());
|
|
running = true;
|
|
|
|
function tick() {
|
|
var delta = (+new Date()) - start;
|
|
if (abort) {
|
|
abort = running = false;
|
|
abortCallback();
|
|
return (abortCallback = undefined);
|
|
} else if (delta > time) {
|
|
if (to.zoom != from.zoom) map.dispatchCallback('zoomed', to.zoom - from.zoom);
|
|
running = false;
|
|
path(from, to, 1, static_coord);
|
|
map.coordinate = static_coord;
|
|
to = from = undefined;
|
|
map.draw();
|
|
if (callback) return callback(map);
|
|
} else {
|
|
path(from, to, easing(delta / time), static_coord);
|
|
map.coordinate = static_coord;
|
|
map.draw();
|
|
MM.getFrame(tick);
|
|
}
|
|
}
|
|
|
|
MM.getFrame(tick);
|
|
};
|
|
|
|
// Optimally smooth (constant perceived velocity) and
|
|
// efficient (minimal path distance) zooming and panning.
|
|
//
|
|
// Based on "Smooth and efficient zooming and panning"
|
|
// by Jarke J. van Wijk and Wim A.A. Nuij
|
|
//
|
|
// Model described in section 3, equations 1 through 5
|
|
// Derived equation (9) of optimal path implemented below
|
|
easey.optimal = function(V, rho, callback) {
|
|
|
|
if (running) return easey.stop(function() {
|
|
easey.optimal(V, rho, callback);
|
|
});
|
|
|
|
// Section 6 describes user testing of these tunable values
|
|
V = V || 0.9;
|
|
rho = rho || 1.42;
|
|
|
|
function sqr(n) { return n*n; }
|
|
function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
|
|
function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
|
|
function tanh(n) { return sinh(n) / cosh(n); }
|
|
|
|
if (from) map.coordinate = from; // For when `from` not current coordinate
|
|
else from = map.coordinate.copy();
|
|
|
|
// Width is measured in coordinate units at zoom 0
|
|
var TL = map.pointCoordinate(new MM.Point(0, 0)).zoomTo(0),
|
|
BR = map.pointCoordinate(map.dimensions).zoomTo(0),
|
|
w0 = Math.max(BR.column - TL.column, BR.row - TL.row),
|
|
w1 = w0 * Math.pow(2, from.zoom - to.zoom),
|
|
start = from.zoomTo(0),
|
|
end = to.zoomTo(0),
|
|
c0 = {x: start.column, y: start.row},
|
|
c1 = {x: end.column, y: end.row},
|
|
u0 = 0,
|
|
u1 = Math.sqrt(sqr(c1.x - c0.x) + sqr(c1.y - c0.y));
|
|
|
|
function b(i) {
|
|
var n = sqr(w1) - sqr(w0) + (i ? -1: 1) * Math.pow(rho, 4) * sqr(u1 - u0),
|
|
d = 2 * (i ? w1 : w0) * sqr(rho) * (u1 - u0);
|
|
return n/d;
|
|
}
|
|
|
|
function r(i) {
|
|
return Math.log(-b(i) + Math.sqrt(sqr(b(i)) + 1));
|
|
}
|
|
|
|
var r0 = r(0),
|
|
r1 = r(1),
|
|
S = (r1 - r0) / rho;
|
|
|
|
// Width
|
|
var w = function(s) {
|
|
return w0 * cosh(r0) / cosh (rho * s + r0);
|
|
};
|
|
|
|
// Zoom
|
|
var u = function(s) {
|
|
return (w0 / sqr(rho)) * cosh(r0) * tanh(rho * s + r0) - (w0 / sqr(rho)) * sinh(r0) + u0;
|
|
};
|
|
|
|
// Special case, when no panning necessary
|
|
if (Math.abs(u1) < 0.000001) {
|
|
if (Math.abs(w0 - w1) < 0.000001) return;
|
|
|
|
// Based on section 4
|
|
var k = w1 < w0 ? -1 : 1;
|
|
S = Math.abs(Math.log(w1/w0)) / rho;
|
|
u = function(s) {
|
|
return u0;
|
|
};
|
|
w = function(s) {
|
|
return w0 * Math.exp(k * rho * s);
|
|
};
|
|
}
|
|
|
|
var oldpath = path;
|
|
path = function (a, b, t, static_coord) {
|
|
if (t == 1) {
|
|
if (static_coord) {
|
|
static_coord.row = to.row;
|
|
static_coord.column = to.column;
|
|
static_coord.zoom = to.zoom;
|
|
}
|
|
return to;
|
|
}
|
|
var s = t * S,
|
|
us = u(s),
|
|
z = a.zoom + (Math.log(w0/w(s)) / Math.LN2),
|
|
x = interp(c0.x, c1.x, us/u1 || 1),
|
|
y = interp(c0.y, c1.y, us/u1 || 1);
|
|
|
|
var power = Math.pow(2, z);
|
|
if (static_coord) {
|
|
static_coord.row = y * power;
|
|
static_coord.column = x * power;
|
|
static_coord.zoom = z;
|
|
} else {
|
|
return new MM.Coordinate(y * power, x * power, z);
|
|
}
|
|
};
|
|
|
|
easey.run(S / V * 1000, function(m) {
|
|
path = oldpath;
|
|
if (callback) callback(m);
|
|
});
|
|
};
|
|
|
|
return easey;
|
|
};
|
|
|
|
this.easey = easey;
|
|
if (typeof this.mapbox == 'undefined') this.mapbox = {};
|
|
this.mapbox.ease = easey;
|
|
})(this, MM);
|
|
;(function(context, MM) {
|
|
|
|
var easey_handlers = {};
|
|
|
|
easey_handlers.TouchHandler = function() {
|
|
var handler = {},
|
|
map,
|
|
panner,
|
|
maxTapTime = 250,
|
|
maxTapDistance = 30,
|
|
maxDoubleTapDelay = 350,
|
|
locations = {},
|
|
taps = [],
|
|
wasPinching = false,
|
|
lastPinchCenter = null,
|
|
p0 = new MM.Point(0, 0),
|
|
p1 = new MM.Point(0, 0);
|
|
|
|
function focusMap(e) {
|
|
map.parent.focus();
|
|
}
|
|
|
|
function clearLocations() {
|
|
for (var loc in locations) {
|
|
if (locations.hasOwnProperty(loc)) {
|
|
delete locations[loc];
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateTouches (e) {
|
|
for (var i = 0; i < e.touches.length; i += 1) {
|
|
var t = e.touches[i];
|
|
if (t.identifier in locations) {
|
|
var l = locations[t.identifier];
|
|
l.x = t.clientX;
|
|
l.y = t.clientY;
|
|
l.scale = e.scale;
|
|
} else {
|
|
locations[t.identifier] = {
|
|
scale: e.scale,
|
|
startPos: { x: t.clientX, y: t.screenY },
|
|
startZoom: map.zoom(),
|
|
x: t.clientX,
|
|
y: t.clientY,
|
|
time: new Date().getTime()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
function touchStartMachine(e) {
|
|
if (!panner) panner = panning(map, 0.10);
|
|
MM.addEvent(e.touches[0].target, 'touchmove',
|
|
touchMoveMachine);
|
|
MM.addEvent(e.touches[0].target, 'touchend',
|
|
touchEndMachine);
|
|
if (e.touches[1]) {
|
|
MM.addEvent(e.touches[1].target, 'touchmove',
|
|
touchMoveMachine);
|
|
MM.addEvent(e.touches[1].target, 'touchend',
|
|
touchEndMachine);
|
|
}
|
|
updateTouches(e);
|
|
panner.down(e.touches[0]);
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function touchMoveMachine(e) {
|
|
switch (e.touches.length) {
|
|
case 1:
|
|
panner.move(e.touches[0]);
|
|
break;
|
|
case 2:
|
|
onPinching(e);
|
|
break;
|
|
}
|
|
updateTouches(e);
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
// Handle a tap event - mainly watch for a doubleTap
|
|
function onTap(tap) {
|
|
if (taps.length &&
|
|
(tap.time - taps[0].time) < maxDoubleTapDelay) {
|
|
onDoubleTap(tap);
|
|
taps = [];
|
|
return;
|
|
}
|
|
taps = [tap];
|
|
}
|
|
|
|
// Handle a double tap by zooming in a single zoom level to a
|
|
// round zoom.
|
|
function onDoubleTap(tap) {
|
|
// zoom in to a round number
|
|
easey().map(map)
|
|
.to(map.pointCoordinate(tap).zoomTo(map.getZoom() + 1))
|
|
.path('about').run(200, function() {
|
|
map.dispatchCallback('zoomed');
|
|
clearLocations();
|
|
});
|
|
}
|
|
|
|
function onPinching(e) {
|
|
// use the first two touches and their previous positions
|
|
var t0 = e.touches[0],
|
|
t1 = e.touches[1];
|
|
p0.x = t0.clientX;
|
|
p0.y = t0.clientY;
|
|
p1.x = t1.clientX;
|
|
p1.y = t1.clientY;
|
|
l0 = locations[t0.identifier],
|
|
l1 = locations[t1.identifier];
|
|
|
|
// mark these touches so they aren't used as taps/holds
|
|
l0.wasPinch = true;
|
|
l1.wasPinch = true;
|
|
|
|
// scale about the center of these touches
|
|
var center = MM.Point.interpolate(p0, p1, 0.5);
|
|
|
|
map.zoomByAbout(
|
|
Math.log(e.scale) / Math.LN2 - Math.log(l0.scale) / Math.LN2,
|
|
center);
|
|
|
|
// pan from the previous center of these touches
|
|
prevX = l0.x + (l1.x - l0.x) * 0.5;
|
|
prevY = l0.y + (l1.y - l0.y) * 0.5;
|
|
map.panBy(center.x - prevX,
|
|
center.y - prevY);
|
|
wasPinching = true;
|
|
lastPinchCenter = center;
|
|
}
|
|
|
|
// When a pinch event ends, round the zoom of the map.
|
|
function onPinched(touch) {
|
|
var z = map.getZoom(), // current zoom
|
|
tz = locations[touch.identifier].startZoom > z ? Math.floor(z) : Math.ceil(z);
|
|
easey().map(map).point(lastPinchCenter).zoom(tz)
|
|
.path('about').run(300);
|
|
clearLocations();
|
|
wasPinching = false;
|
|
}
|
|
|
|
function touchEndMachine(e) {
|
|
MM.removeEvent(e.target, 'touchmove',
|
|
touchMoveMachine);
|
|
MM.removeEvent(e.target, 'touchend',
|
|
touchEndMachine);
|
|
var now = new Date().getTime();
|
|
|
|
// round zoom if we're done pinching
|
|
if (e.touches.length === 0 && wasPinching) {
|
|
onPinched(e.changedTouches[0]);
|
|
}
|
|
|
|
panner.up();
|
|
|
|
// Look at each changed touch in turn.
|
|
for (var i = 0; i < e.changedTouches.length; i += 1) {
|
|
var t = e.changedTouches[i],
|
|
loc = locations[t.identifier];
|
|
// if we didn't see this one (bug?)
|
|
// or if it was consumed by pinching already
|
|
// just skip to the next one
|
|
if (!loc || loc.wasPinch) {
|
|
continue;
|
|
}
|
|
|
|
// we now know we have an event object and a
|
|
// matching touch that's just ended. Let's see
|
|
// what kind of event it is based on how long it
|
|
// lasted and how far it moved.
|
|
var pos = { x: t.clientX, y: t.clientY },
|
|
time = now - loc.time,
|
|
travel = MM.Point.distance(pos, loc.startPos);
|
|
if (travel > maxTapDistance) {
|
|
// we will to assume that the drag has been handled separately
|
|
} else if (time > maxTapTime) {
|
|
// close in space, but not in time: a hold
|
|
pos.end = now;
|
|
pos.duration = time;
|
|
} else {
|
|
// close in both time and space: a tap
|
|
pos.time = now;
|
|
onTap(pos);
|
|
}
|
|
}
|
|
|
|
// Weird, sometimes an end event doesn't get thrown
|
|
// for a touch that nevertheless has disappeared.
|
|
// Still, this will eventually catch those ids:
|
|
|
|
var validTouchIds = {};
|
|
for (var j = 0; j < e.touches.length; j++) {
|
|
validTouchIds[e.touches[j].identifier] = true;
|
|
}
|
|
for (var id in locations) {
|
|
if (!(id in validTouchIds)) {
|
|
delete validTouchIds[id];
|
|
}
|
|
}
|
|
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
|
|
MM.addEvent(map.parent, 'touchstart',
|
|
touchStartMachine);
|
|
};
|
|
|
|
handler.remove = function() {
|
|
if (!panner) return;
|
|
MM.removeEvent(map.parent, 'touchstart',
|
|
touchStartMachine);
|
|
panner.remove();
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
|
|
easey_handlers.DoubleClickHandler = function() {
|
|
var handler = {},
|
|
map;
|
|
|
|
function doubleClick(e) {
|
|
// Ensure that this handler is attached once.
|
|
// Get the point on the map that was double-clicked
|
|
var point = MM.getMousePoint(e, map);
|
|
z = map.getZoom() + (e.shiftKey ? -1 : 1);
|
|
// use shift-double-click to zoom out
|
|
easey().map(map)
|
|
.to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z))
|
|
.path('about').run(100, function() {
|
|
map.dispatchCallback('zoomed');
|
|
});
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
MM.addEvent(map.parent, 'dblclick', doubleClick);
|
|
return handler;
|
|
};
|
|
|
|
handler.remove = function() {
|
|
MM.removeEvent(map.parent, 'dblclick', doubleClick);
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
|
|
easey_handlers.MouseWheelHandler = function() {
|
|
var handler = {},
|
|
map,
|
|
_zoomDiv,
|
|
ea = easey(),
|
|
prevTime,
|
|
precise = false;
|
|
|
|
function mouseWheel(e) {
|
|
var delta = 0;
|
|
prevTime = prevTime || new Date().getTime();
|
|
|
|
try {
|
|
_zoomDiv.scrollTop = 1000;
|
|
_zoomDiv.dispatchEvent(e);
|
|
delta = 1000 - _zoomDiv.scrollTop;
|
|
} catch (error) {
|
|
delta = e.wheelDelta || (-e.detail * 5);
|
|
}
|
|
|
|
// limit mousewheeling to once every 200ms
|
|
var timeSince = new Date().getTime() - prevTime;
|
|
|
|
function dispatchZoomed() {
|
|
map.dispatchCallback('zoomed');
|
|
}
|
|
|
|
if (!ea.running()) {
|
|
var point = MM.getMousePoint(e, map),
|
|
z = map.getZoom();
|
|
ea.map(map)
|
|
.easing('easeOut')
|
|
.to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z + (delta > 0 ? 1 : -1)))
|
|
.path('about').run(100, dispatchZoomed);
|
|
prevTime = new Date().getTime();
|
|
} else if (timeSince > 150){
|
|
ea.zoom(ea.to().zoom + (delta > 0 ? 1 : -1)).from(map.coordinate).resetRun();
|
|
prevTime = new Date().getTime();
|
|
}
|
|
|
|
// Cancel the event so that the page doesn't scroll
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
_zoomDiv = document.body.appendChild(document.createElement('div'));
|
|
_zoomDiv.style.cssText = 'visibility:hidden;top:0;height:0;width:0;overflow-y:scroll';
|
|
var innerDiv = _zoomDiv.appendChild(document.createElement('div'));
|
|
innerDiv.style.height = '2000px';
|
|
MM.addEvent(map.parent, 'mousewheel', mouseWheel);
|
|
return handler;
|
|
};
|
|
|
|
handler.precise = function(x) {
|
|
if (!arguments.length) return precise;
|
|
precise = x;
|
|
return handler;
|
|
};
|
|
|
|
handler.remove = function() {
|
|
MM.removeEvent(map.parent, 'mousewheel', mouseWheel);
|
|
_zoomDiv.parentNode.removeChild(_zoomDiv);
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
|
|
easey_handlers.DragHandler = function() {
|
|
var handler = {},
|
|
map,
|
|
panner;
|
|
|
|
function focusMap(e) {
|
|
map.parent.focus();
|
|
}
|
|
|
|
function mouseDown(e) {
|
|
if (e.shiftKey || e.button == 2) return;
|
|
MM.addEvent(document, 'mousemove', mouseMove);
|
|
MM.addEvent(document, 'mouseup', mouseUp);
|
|
panner.down(e);
|
|
map.parent.style.cursor = 'move';
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function mouseMove(e) {
|
|
panner.move(e);
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
function mouseUp(e) {
|
|
MM.removeEvent(document, 'mousemove', mouseMove);
|
|
MM.removeEvent(document, 'mouseup', mouseUp);
|
|
panner.up();
|
|
map.parent.style.cursor = '';
|
|
return MM.cancelEvent(e);
|
|
}
|
|
|
|
handler.init = function(x) {
|
|
map = x;
|
|
MM.addEvent(map.parent, 'click', focusMap);
|
|
MM.addEvent(map.parent, 'mousedown', mouseDown);
|
|
panner = panning(map);
|
|
};
|
|
|
|
handler.remove = function() {
|
|
MM.removeEvent(map.parent, 'click', focusMap);
|
|
MM.removeEvent(map.parent, 'mousedown', mouseDown);
|
|
panner.up();
|
|
panner.remove();
|
|
};
|
|
|
|
return handler;
|
|
};
|
|
|
|
|
|
function panning(map, drag) {
|
|
|
|
var p = {};
|
|
drag = drag || 0.15;
|
|
|
|
var speed = { x: 0, y: 0 },
|
|
dir = { x: 0, y: 0 },
|
|
removed = false,
|
|
nowPoint = null,
|
|
oldPoint = null,
|
|
moveTime = null,
|
|
prevMoveTime = null,
|
|
animatedLastPoint = true,
|
|
t,
|
|
prevT = new Date().getTime();
|
|
|
|
p.down = function(e) {
|
|
nowPoint = oldPoint = MM.getMousePoint(e, map);
|
|
moveTime = prevMoveTime = +new Date();
|
|
};
|
|
|
|
p.move = function(e) {
|
|
if (nowPoint) {
|
|
if (animatedLastPoint) {
|
|
oldPoint = nowPoint;
|
|
prevMoveTime = moveTime;
|
|
animatedLastPoint = false;
|
|
}
|
|
nowPoint = MM.getMousePoint(e, map);
|
|
moveTime = +new Date();
|
|
}
|
|
};
|
|
|
|
p.up = function() {
|
|
if (+new Date() - prevMoveTime < 50) {
|
|
dt = Math.max(1, moveTime - prevMoveTime);
|
|
dir.x = nowPoint.x - oldPoint.x;
|
|
dir.y = nowPoint.y - oldPoint.y;
|
|
speed.x = dir.x / dt;
|
|
speed.y = dir.y / dt;
|
|
} else {
|
|
speed.x = 0;
|
|
speed.y = 0;
|
|
}
|
|
nowPoint = oldPoint = null;
|
|
moveTime = null;
|
|
};
|
|
|
|
p.remove = function() {
|
|
removed = true;
|
|
};
|
|
|
|
function animate(t) {
|
|
var dt = Math.max(1, t - prevT);
|
|
if (nowPoint && oldPoint) {
|
|
if (!animatedLastPoint) {
|
|
dir.x = nowPoint.x - oldPoint.x;
|
|
dir.y = nowPoint.y - oldPoint.y;
|
|
map.panBy(dir.x, dir.y);
|
|
animatedLastPoint = true;
|
|
}
|
|
} else {
|
|
// Rough time based animation accuracy
|
|
// using a linear approximation approach
|
|
speed.x *= Math.pow(1 - drag, dt * 60 / 1000);
|
|
speed.y *= Math.pow(1 - drag, dt * 60 / 1000);
|
|
if (Math.abs(speed.x) < 0.001) {
|
|
speed.x = 0;
|
|
}
|
|
if (Math.abs(speed.y) < 0.001) {
|
|
speed.y = 0;
|
|
}
|
|
if (speed.x || speed.y) {
|
|
map.panBy(speed.x * dt, speed.y * dt);
|
|
}
|
|
}
|
|
prevT = t;
|
|
if (!removed) MM.getFrame(animate);
|
|
}
|
|
|
|
MM.getFrame(animate);
|
|
return p;
|
|
}
|
|
|
|
|
|
this.easey_handlers = easey_handlers;
|
|
|
|
})(this, MM);
|
|
if (typeof mapbox == 'undefined') mapbox = {};
|
|
if (typeof mapbox.markers == 'undefined') mapbox.markers = {};
|
|
mapbox.markers.layer = function() {
|
|
|
|
var m = {},
|
|
// external list of geojson features
|
|
features = [],
|
|
// internal list of markers
|
|
markers = [],
|
|
// internal list of callbacks
|
|
callbackManager = new MM.CallbackManager(m, ['drawn', 'markeradded']),
|
|
// the absolute position of the parent element
|
|
position = null,
|
|
// a factory function for creating DOM elements out of
|
|
// GeoJSON objects
|
|
factory = mapbox.markers.simplestyle_factory,
|
|
// a sorter function for sorting GeoJSON objects
|
|
// in the DOM
|
|
sorter = function(a, b) {
|
|
return b.geometry.coordinates[1] -
|
|
a.geometry.coordinates[1];
|
|
},
|
|
// a list of urls from which features can be loaded.
|
|
// these can be templated with {z}, {x}, and {y}
|
|
urls,
|
|
// map bounds
|
|
left = null,
|
|
right = null,
|
|
// a function that filters points
|
|
filter = function() {
|
|
return true;
|
|
},
|
|
_seq = 0,
|
|
keyfn = function() {
|
|
return ++_seq;
|
|
},
|
|
index = {};
|
|
|
|
// The parent DOM element
|
|
m.parent = document.createElement('div');
|
|
m.parent.style.cssText = 'position: absolute; top: 0px;' +
|
|
'left:0px; width:100%; height:100%; margin:0; padding:0; z-index:0;pointer-events:none;';
|
|
m.name = 'markers';
|
|
|
|
// reposition a single marker element
|
|
function reposition(marker) {
|
|
// remember the tile coordinate so we don't have to reproject every time
|
|
if (!marker.coord) marker.coord = m.map.locationCoordinate(marker.location);
|
|
var pos = m.map.coordinatePoint(marker.coord);
|
|
var pos_loc, new_pos;
|
|
|
|
// If this point has wound around the world, adjust its position
|
|
// to the new, onscreen location
|
|
if (pos.x < 0) {
|
|
pos_loc = new MM.Location(marker.location.lat, marker.location.lon);
|
|
pos_loc.lon += Math.ceil((left.lon - marker.location.lon) / 360) * 360;
|
|
new_pos = m.map.locationPoint(pos_loc);
|
|
if (new_pos.x < m.map.dimensions.x) {
|
|
pos = new_pos;
|
|
marker.coord = m.map.locationCoordinate(pos_loc);
|
|
}
|
|
} else if (pos.x > m.map.dimensions.x) {
|
|
pos_loc = new MM.Location(marker.location.lat, marker.location.lon);
|
|
pos_loc.lon -= Math.ceil((marker.location.lon - right.lon) / 360) * 360;
|
|
new_pos = m.map.locationPoint(pos_loc);
|
|
if (new_pos.x > 0) {
|
|
pos = new_pos;
|
|
marker.coord = m.map.locationCoordinate(pos_loc);
|
|
}
|
|
}
|
|
|
|
pos.scale = 1;
|
|
pos.width = pos.height = 0;
|
|
MM.moveElement(marker.element, pos);
|
|
}
|
|
|
|
// Adding and removing callbacks is mainly a way to enable mmg_interaction to operate.
|
|
// I think there are better ways to do this, by, for instance, having mmg be able to
|
|
// register 'binders' to markers, but this is backwards-compatible and equivalent
|
|
// externally.
|
|
m.addCallback = function(event, callback) {
|
|
callbackManager.addCallback(event, callback);
|
|
return m;
|
|
};
|
|
|
|
m.removeCallback = function(event, callback) {
|
|
callbackManager.removeCallback(event, callback);
|
|
return m;
|
|
};
|
|
|
|
// Draw this layer - reposition all markers on the div. This requires
|
|
// the markers library to be attached to a map, and will noop otherwise.
|
|
m.draw = function() {
|
|
if (!m.map) return;
|
|
left = m.map.pointLocation(new MM.Point(0, 0));
|
|
right = m.map.pointLocation(new MM.Point(m.map.dimensions.x, 0));
|
|
callbackManager.dispatchCallback('drawn', m);
|
|
for (var i = 0; i < markers.length; i++) {
|
|
reposition(markers[i]);
|
|
}
|
|
};
|
|
|
|
// Add a fully-formed marker to the layer. This fires a `markeradded` event.
|
|
// This does not require the map element t be attached.
|
|
m.add = function(marker) {
|
|
if (!marker || !marker.element) return null;
|
|
m.parent.appendChild(marker.element);
|
|
markers.push(marker);
|
|
callbackManager.dispatchCallback('markeradded', marker);
|
|
return marker;
|
|
};
|
|
|
|
// Remove a fully-formed marker - which must be the same exact marker
|
|
// object as in the markers array - from the layer.
|
|
m.remove = function(marker) {
|
|
if (!marker) return null;
|
|
m.parent.removeChild(marker.element);
|
|
for (var i = 0; i < markers.length; i++) {
|
|
if (markers[i] === marker) {
|
|
markers.splice(i, 1);
|
|
return marker;
|
|
}
|
|
}
|
|
return marker;
|
|
};
|
|
|
|
m.markers = function(x) {
|
|
if (!arguments.length) return markers;
|
|
};
|
|
|
|
// Add a GeoJSON feature to the markers layer.
|
|
m.add_feature = function(x) {
|
|
return m.features(m.features().concat([x]));
|
|
};
|
|
|
|
m.sort = function(x) {
|
|
if (!arguments.length) return sorter;
|
|
sorter = x;
|
|
return m;
|
|
};
|
|
|
|
// Public data interface
|
|
m.features = function(x) {
|
|
// Return features
|
|
if (!arguments.length) return features;
|
|
|
|
// Set features
|
|
if (!x) x = [];
|
|
features = x.slice();
|
|
|
|
features.sort(sorter);
|
|
|
|
for (var j = 0; j < markers.length; j++) {
|
|
markers[j].touch = false;
|
|
}
|
|
|
|
for (var i = 0; i < features.length; i++) {
|
|
if (filter(features[i])) {
|
|
var id = keyfn(features[i]);
|
|
if (index[id]) {
|
|
// marker is already on the map, needs to be moved or rebuilt
|
|
index[id].location = new MM.Location(
|
|
features[i].geometry.coordinates[1],
|
|
features[i].geometry.coordinates[0]);
|
|
index[id].coord = null;
|
|
reposition(index[id]);
|
|
} else {
|
|
// marker needs to be added to the map
|
|
index[id] = m.add({
|
|
element: factory(features[i]),
|
|
location: new MM.Location(
|
|
features[i].geometry.coordinates[1],
|
|
features[i].geometry.coordinates[0]),
|
|
data: features[i]
|
|
});
|
|
}
|
|
if (index[id]) index[id].touch = true;
|
|
}
|
|
}
|
|
|
|
for (var k = markers.length - 1; k >= 0; k--) {
|
|
if (markers[k].touch === false) {
|
|
m.remove(markers[k]);
|
|
}
|
|
}
|
|
|
|
if (m.map && m.map.coordinate) m.map.draw();
|
|
|
|
return m;
|
|
};
|
|
|
|
// Request features from a URL - either a local URL or a JSONP call.
|
|
// Expects GeoJSON-formatted features.
|
|
m.url = function(x, callback) {
|
|
if (!arguments.length) return urls;
|
|
if (typeof reqwest === 'undefined') throw 'reqwest is required for url loading';
|
|
if (typeof x === 'string') x = [x];
|
|
|
|
urls = x;
|
|
function add_features(err, x) {
|
|
if (err && callback) return callback(err);
|
|
var features = typeof x !== 'undefined' && x.features ? x.features : null;
|
|
if (features) m.features(features);
|
|
if (callback) callback(err, features, m);
|
|
}
|
|
|
|
reqwest((urls[0].match(/geojsonp$/)) ? {
|
|
url: urls[0] + (~urls[0].indexOf('?') ? '&' : '?') + 'callback=?',
|
|
type: 'jsonp',
|
|
success: function(resp) { add_features(null, resp); },
|
|
error: add_features
|
|
} : {
|
|
url: urls[0],
|
|
type: 'json',
|
|
success: function(resp) { add_features(null, resp); },
|
|
error: add_features
|
|
});
|
|
return m;
|
|
};
|
|
|
|
m.id = function(x, callback) {
|
|
return m.url('http://a.tiles.mapbox.com/v3/' + x + '/markers.geojsonp', callback);
|
|
};
|
|
|
|
m.csv = function(x) {
|
|
return m.features(mapbox.markers.csv_to_geojson(x));
|
|
};
|
|
|
|
m.extent = function() {
|
|
var ext = [{
|
|
lat: Infinity,
|
|
lon: Infinity
|
|
}, {
|
|
lat: -Infinity,
|
|
lon: -Infinity
|
|
}];
|
|
var ft = m.features();
|
|
for (var i = 0; i < ft.length; i++) {
|
|
var coords = ft[i].geometry.coordinates;
|
|
if (coords[0] < ext[0].lon) ext[0].lon = coords[0];
|
|
if (coords[1] < ext[0].lat) ext[0].lat = coords[1];
|
|
if (coords[0] > ext[1].lon) ext[1].lon = coords[0];
|
|
if (coords[1] > ext[1].lat) ext[1].lat = coords[1];
|
|
}
|
|
return ext;
|
|
};
|
|
|
|
m.key = function(x) {
|
|
if (!arguments.length) return keyfn;
|
|
if (x === null) {
|
|
keyfn = function() { return ++_seq; };
|
|
} else {
|
|
keyfn = x;
|
|
}
|
|
return m;
|
|
};
|
|
|
|
// Factory interface
|
|
m.factory = function(x) {
|
|
if (!arguments.length) return factory;
|
|
factory = x;
|
|
// re-render all features
|
|
m.features(m.features());
|
|
return m;
|
|
};
|
|
|
|
m.filter = function(x) {
|
|
if (!arguments.length) return filter;
|
|
filter = x;
|
|
// Setting a filter re-sets the features into a new array.
|
|
// This does _not_ change the actual output of .features()
|
|
m.features(m.features());
|
|
return m;
|
|
};
|
|
|
|
m.destroy = function() {
|
|
if (m.parent.parentNode) {
|
|
m.parent.parentNode.removeChild(m.parent);
|
|
}
|
|
};
|
|
|
|
// Get or set this layer's name
|
|
m.named = function(x) {
|
|
if (!arguments.length) return m.name;
|
|
m.name = x;
|
|
return m;
|
|
};
|
|
|
|
m.enabled = true;
|
|
|
|
m.enable = function() {
|
|
this.enabled = true;
|
|
this.parent.style.display = '';
|
|
return m;
|
|
};
|
|
|
|
m.disable = function() {
|
|
this.enabled = false;
|
|
this.parent.style.display = 'none';
|
|
return m;
|
|
};
|
|
|
|
return m;
|
|
};
|
|
|
|
mmg = mapbox.markers.layer; // Backwards compatibility
|
|
mapbox.markers.interaction = function(mmg) {
|
|
// Make markersLayer.interaction a singleton and this an accessor.
|
|
if (mmg && mmg.interaction) return mmg.interaction;
|
|
|
|
var mi = {},
|
|
tooltips = [],
|
|
exclusive = true,
|
|
hideOnMove = true,
|
|
showOnHover = true,
|
|
close_timer = null,
|
|
on = true,
|
|
formatter;
|
|
|
|
mi.formatter = function(x) {
|
|
if (!arguments.length) return formatter;
|
|
formatter = x;
|
|
return mi;
|
|
};
|
|
mi.formatter(function(feature) {
|
|
var o = '',
|
|
props = feature.properties;
|
|
|
|
// Tolerate markers without properties at all.
|
|
if (!props) return null;
|
|
|
|
if (props.title) {
|
|
o += '<div class="marker-title">' + props.title + '</div>';
|
|
}
|
|
if (props.description) {
|
|
o += '<div class="marker-description">' + props.description + '</div>';
|
|
}
|
|
|
|
if (typeof html_sanitize !== undefined) {
|
|
o = html_sanitize(o,
|
|
function(url) {
|
|
if (/^(https?:\/\/|data:image)/.test(url)) return url;
|
|
},
|
|
function(x) { return x; });
|
|
}
|
|
|
|
return o;
|
|
});
|
|
|
|
mi.hideOnMove = function(x) {
|
|
if (!arguments.length) return hideOnMove;
|
|
hideOnMove = x;
|
|
return mi;
|
|
};
|
|
|
|
mi.exclusive = function(x) {
|
|
if (!arguments.length) return exclusive;
|
|
exclusive = x;
|
|
return mi;
|
|
};
|
|
|
|
mi.showOnHover = function(x) {
|
|
if (!arguments.length) return showOnHover;
|
|
showOnHover = x;
|
|
return mi;
|
|
};
|
|
|
|
mi.hideTooltips = function() {
|
|
while (tooltips.length) mmg.remove(tooltips.pop());
|
|
for (var i = 0; i < markers.length; i++) {
|
|
delete markers[i].clicked;
|
|
}
|
|
};
|
|
|
|
mi.add = function() {
|
|
on = true;
|
|
return mi;
|
|
};
|
|
|
|
mi.remove = function() {
|
|
on = false;
|
|
return mi;
|
|
};
|
|
|
|
mi.bindMarker = function(marker) {
|
|
var delayed_close = function() {
|
|
if (showOnHover === false) return;
|
|
if (!marker.clicked) close_timer = window.setTimeout(function() {
|
|
mi.hideTooltips();
|
|
}, 200);
|
|
};
|
|
|
|
var show = function(e) {
|
|
if (e && e.type == 'mouseover' && showOnHover === false) return;
|
|
if (!on) return;
|
|
var content = formatter(marker.data);
|
|
// Don't show a popup if the formatter returns an
|
|
// empty string. This does not do any magic around DOM elements.
|
|
if (!content) return;
|
|
|
|
if (exclusive && tooltips.length > 0) {
|
|
mi.hideTooltips();
|
|
// We've hidden all of the tooltips, so let's not close
|
|
// the one that we're creating as soon as it is created.
|
|
if (close_timer) window.clearTimeout(close_timer);
|
|
}
|
|
|
|
var tooltip = document.createElement('div');
|
|
tooltip.className = 'marker-tooltip';
|
|
tooltip.style.width = '100%';
|
|
|
|
var wrapper = tooltip.appendChild(document.createElement('div'));
|
|
wrapper.style.cssText = 'position: absolute; pointer-events: none;';
|
|
|
|
var popup = wrapper.appendChild(document.createElement('div'));
|
|
popup.className = 'marker-popup';
|
|
popup.style.cssText = 'pointer-events: auto;';
|
|
|
|
if (typeof content == 'string') {
|
|
popup.innerHTML = content;
|
|
} else {
|
|
popup.appendChild(content);
|
|
}
|
|
|
|
// Align the bottom of the tooltip with the top of its marker
|
|
wrapper.style.bottom = marker.element.offsetHeight / 2 + 20 + 'px';
|
|
|
|
// Block mouse and touch events
|
|
function stopPropagation(e) {
|
|
e.cancelBubble = true;
|
|
if (e.stopPropagation) { e.stopPropagation(); }
|
|
return false;
|
|
}
|
|
MM.addEvent(popup, 'mousedown', stopPropagation);
|
|
MM.addEvent(popup, 'touchstart', stopPropagation);
|
|
|
|
if (showOnHover) {
|
|
tooltip.onmouseover = function() {
|
|
if (close_timer) window.clearTimeout(close_timer);
|
|
};
|
|
tooltip.onmouseout = delayed_close;
|
|
}
|
|
|
|
var t = {
|
|
element: tooltip,
|
|
data: {},
|
|
interactive: false,
|
|
location: marker.location.copy()
|
|
};
|
|
tooltips.push(t);
|
|
marker.tooltip = t;
|
|
mmg.add(t);
|
|
mmg.draw();
|
|
};
|
|
|
|
marker.showTooltip = show;
|
|
|
|
marker.element.onclick = marker.element.ontouchstart = function() {
|
|
show();
|
|
marker.clicked = true;
|
|
};
|
|
|
|
marker.element.onmouseover = show;
|
|
marker.element.onmouseout = delayed_close;
|
|
};
|
|
|
|
function bindPanned() {
|
|
mmg.map.addCallback('panned', function() {
|
|
if (hideOnMove) {
|
|
while (tooltips.length) {
|
|
mmg.remove(tooltips.pop());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (mmg) {
|
|
// Remove tooltips on panning
|
|
mmg.addCallback('drawn', bindPanned);
|
|
|
|
// Bind present markers
|
|
var markers = mmg.markers();
|
|
for (var i = 0; i < markers.length; i++) {
|
|
mi.bindMarker(markers[i]);
|
|
}
|
|
|
|
// Bind future markers
|
|
mmg.addCallback('markeradded', function(_, marker) {
|
|
// Markers can choose to be not-interactive. The main example
|
|
// of this currently is marker bubbles, which should not recursively
|
|
// give marker bubbles.
|
|
if (marker.interactive !== false) mi.bindMarker(marker);
|
|
});
|
|
|
|
// Save reference to self on the markers instance.
|
|
mmg.interaction = mi;
|
|
}
|
|
|
|
return mi;
|
|
};
|
|
|
|
mmg_interaction = mapbox.markers.interaction;
|
|
mapbox.markers.csv_to_geojson = function(x) {
|
|
// Extracted from d3
|
|
function csv_parse(text) {
|
|
var header;
|
|
return csv_parseRows(text, function(row, i) {
|
|
if (i) {
|
|
var o = {}, j = -1, m = header.length;
|
|
while (++j < m) o[header[j]] = row[j];
|
|
return o;
|
|
} else {
|
|
header = row;
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
function csv_parseRows (text, f) {
|
|
var EOL = {}, // sentinel value for end-of-line
|
|
EOF = {}, // sentinel value for end-of-file
|
|
rows = [], // output rows
|
|
re = /\r\n|[,\r\n]/g, // field separator regex
|
|
n = 0, // the current line number
|
|
t, // the current token
|
|
eol; // is the current token followed by EOL?
|
|
|
|
re.lastIndex = 0; // work-around bug in FF 3.6
|
|
|
|
/** @private Returns the next token. */
|
|
function token() {
|
|
if (re.lastIndex >= text.length) return EOF; // special case: end of file
|
|
if (eol) { eol = false; return EOL; } // special case: end of line
|
|
|
|
// special case: quotes
|
|
var j = re.lastIndex;
|
|
if (text.charCodeAt(j) === 34) {
|
|
var i = j;
|
|
while (i++ < text.length) {
|
|
if (text.charCodeAt(i) === 34) {
|
|
if (text.charCodeAt(i + 1) !== 34) break;
|
|
i++;
|
|
}
|
|
}
|
|
re.lastIndex = i + 2;
|
|
var c = text.charCodeAt(i + 1);
|
|
if (c === 13) {
|
|
eol = true;
|
|
if (text.charCodeAt(i + 2) === 10) re.lastIndex++;
|
|
} else if (c === 10) {
|
|
eol = true;
|
|
}
|
|
return text.substring(j + 1, i).replace(/""/g, "\"");
|
|
}
|
|
|
|
// common case
|
|
var m = re.exec(text);
|
|
if (m) {
|
|
eol = m[0].charCodeAt(0) !== 44;
|
|
return text.substring(j, m.index);
|
|
}
|
|
re.lastIndex = text.length;
|
|
return text.substring(j);
|
|
}
|
|
|
|
while ((t = token()) !== EOF) {
|
|
var a = [];
|
|
while ((t !== EOL) && (t !== EOF)) {
|
|
a.push(t);
|
|
t = token();
|
|
}
|
|
if (f && !(a = f(a, n++))) continue;
|
|
rows.push(a);
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
var features = [];
|
|
var parsed = csv_parse(x);
|
|
if (!parsed.length) return features;
|
|
|
|
var latfield = '',
|
|
lonfield = '';
|
|
|
|
for (var f in parsed[0]) {
|
|
if (f.match(/^Lat/i)) latfield = f;
|
|
if (f.match(/^Lon/i)) lonfield = f;
|
|
}
|
|
|
|
if (!latfield || !lonfield) {
|
|
throw 'CSV: Could not find latitude or longitude field';
|
|
}
|
|
|
|
for (var i = 0; i < parsed.length; i++) {
|
|
if (parsed[i][lonfield] !== undefined &&
|
|
parsed[i][lonfield] !== undefined) {
|
|
features.push({
|
|
type: 'Feature',
|
|
properties: parsed[i],
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [
|
|
parseFloat(parsed[i][lonfield]),
|
|
parseFloat(parsed[i][latfield])]
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return features;
|
|
};
|
|
mapbox.markers.simplestyle_factory = function(feature) {
|
|
|
|
var sizes = {
|
|
small: [20, 50],
|
|
medium: [30, 70],
|
|
large: [35, 90]
|
|
};
|
|
|
|
var fp = feature.properties || {};
|
|
|
|
var size = fp['marker-size'] || 'medium';
|
|
var symbol = (fp['marker-symbol']) ? '-' + fp['marker-symbol'] : '';
|
|
var color = fp['marker-color'] || '7e7e7e';
|
|
color = color.replace('#', '');
|
|
|
|
var d = document.createElement('img');
|
|
d.width = sizes[size][0];
|
|
d.height = sizes[size][1];
|
|
d.className = 'simplestyle-marker';
|
|
d.alt = fp.title || '';
|
|
d.src = (mapbox.markers.marker_baseurl || 'http://a.tiles.mapbox.com/v3/marker/') +
|
|
'pin-' +
|
|
// Internet Explorer does not support the `size[0]` syntax.
|
|
size.charAt(0) + symbol + '+' + color +
|
|
((window.devicePixelRatio === 2) ? '@2x' : '') +
|
|
'.png';
|
|
// Support retina markers for 2x devices
|
|
|
|
var ds = d.style;
|
|
ds.position = 'absolute';
|
|
ds.clip = 'rect(auto auto ' + (sizes[size][1] * 0.75) + 'px auto)';
|
|
ds.marginTop = -((sizes[size][1]) / 2) + 'px';
|
|
ds.marginLeft = -(sizes[size][0] / 2) + 'px';
|
|
ds.cursor = 'pointer';
|
|
ds.pointerEvents = 'all';
|
|
|
|
return d;
|
|
};
|
|
if (typeof mapbox === 'undefined') mapbox = {};
|
|
|
|
mapbox.MAPBOX_URL = 'http://a.tiles.mapbox.com/v3/';
|
|
|
|
// a `mapbox.map` is a modestmaps object with the
|
|
// easey handlers as defaults
|
|
mapbox.map = function(el, layer, dimensions, eventhandlers) {
|
|
var m = new MM.Map(el, layer, dimensions,
|
|
eventhandlers || [
|
|
easey_handlers.TouchHandler(),
|
|
easey_handlers.DragHandler(),
|
|
easey_handlers.DoubleClickHandler(),
|
|
easey_handlers.MouseWheelHandler()
|
|
]);
|
|
|
|
// Set maxzoom to 17, highest zoom level supported by MapBox streets
|
|
m.setZoomRange(0, 17);
|
|
|
|
// Attach easey, ui, and interaction
|
|
m.ease = easey().map(m);
|
|
m.ui = mapbox.ui(m);
|
|
m.interaction = mapbox.interaction().map(m);
|
|
|
|
// Autoconfigure map with sensible defaults
|
|
m.auto = function() {
|
|
this.ui.zoomer.add();
|
|
this.ui.zoombox.add();
|
|
this.ui.legend.add();
|
|
this.ui.attribution.add();
|
|
this.ui.refresh();
|
|
this.interaction.auto();
|
|
|
|
for (var i = 0; i < this.layers.length; i++) {
|
|
if (this.layers[i].tilejson) {
|
|
var tj = this.layers[i].tilejson(),
|
|
center = tj.center || new MM.Location(0, 0),
|
|
zoom = tj.zoom || 0;
|
|
this.setCenterZoom(center, zoom);
|
|
break;
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
m.refresh = function() {
|
|
this.ui.refresh();
|
|
this.interaction.refresh();
|
|
return this;
|
|
};
|
|
|
|
var smooth_handlers = [
|
|
easey_handlers.TouchHandler,
|
|
easey_handlers.DragHandler,
|
|
easey_handlers.DoubleClickHandler,
|
|
easey_handlers.MouseWheelHandler
|
|
];
|
|
|
|
var default_handlers = [
|
|
MM.TouchHandler,
|
|
MM.DragHandler,
|
|
MM.DoubleClickHandler,
|
|
MM.MouseWheelHandler
|
|
];
|
|
|
|
MM.Map.prototype.smooth = function(_) {
|
|
while (this.eventHandlers.length) {
|
|
this.eventHandlers.pop().remove();
|
|
}
|
|
|
|
var handlers = _ ? smooth_handlers : default_handlers;
|
|
for (var j = 0; j < handlers.length; j++) {
|
|
var h = handlers[j]();
|
|
this.eventHandlers.push(h);
|
|
h.init(this);
|
|
}
|
|
return m;
|
|
};
|
|
|
|
m.setPanLimits = function(locations) {
|
|
if (!(locations instanceof MM.Extent)) {
|
|
locations = new MM.Extent(
|
|
new MM.Location(
|
|
locations[0].lat,
|
|
locations[0].lon),
|
|
new MM.Location(
|
|
locations[1].lat,
|
|
locations[1].lon));
|
|
}
|
|
locations = locations.toArray();
|
|
this.coordLimits = [
|
|
this.locationCoordinate(locations[0]).zoomTo(this.coordLimits[0].zoom),
|
|
this.locationCoordinate(locations[1]).zoomTo(this.coordLimits[1].zoom)
|
|
];
|
|
return m;
|
|
};
|
|
|
|
m.center = function(location, animate) {
|
|
if (location && animate) {
|
|
this.ease.location(location).zoom(this.zoom())
|
|
.optimal(null, null, animate.callback);
|
|
} else {
|
|
return MM.Map.prototype.center.call(this, location);
|
|
}
|
|
};
|
|
|
|
m.zoom = function(zoom, animate) {
|
|
if (zoom !== undefined && animate) {
|
|
this.ease.to(this.coordinate).zoom(zoom).run(600);
|
|
} else {
|
|
return MM.Map.prototype.zoom.call(this, zoom);
|
|
}
|
|
};
|
|
|
|
m.centerzoom = function(location, zoom, animate) {
|
|
if (location && zoom !== undefined && animate) {
|
|
this.ease.location(location).zoom(zoom).optimal(null, null, animate.callback);
|
|
} else if (location && zoom !== undefined) {
|
|
return this.setCenterZoom(location, zoom);
|
|
}
|
|
};
|
|
|
|
// Insert a tile layer below marker layers
|
|
m.addTileLayer = function(layer) {
|
|
for (var i = m.layers.length; i > 0; i--) {
|
|
if (!m.layers[i - 1].features) {
|
|
return this.insertLayerAt(i, layer);
|
|
}
|
|
}
|
|
return this.insertLayerAt(0, layer);
|
|
};
|
|
|
|
// We need to redraw after removing due to compositing
|
|
m.removeLayerAt = function(index) {
|
|
MM.Map.prototype.removeLayerAt.call(this, index);
|
|
MM.getFrame(this.getRedraw());
|
|
return this;
|
|
};
|
|
|
|
// We need to redraw after removing due to compositing
|
|
m.swapLayersAt = function(a, b) {
|
|
MM.Map.prototype.swapLayersAt.call(this, a, b);
|
|
MM.getFrame(this.getRedraw());
|
|
return this;
|
|
};
|
|
|
|
return m;
|
|
};
|
|
|
|
this.mapbox = mapbox;
|
|
if (typeof mapbox === 'undefined') mapbox = {};
|
|
|
|
// Simplest way to create a map. Just provide an element id and
|
|
// a tilejson url (or an array of many) and an optional callback
|
|
// that takes one argument, the map.
|
|
mapbox.auto = function(elem, url, callback) {
|
|
mapbox.load(url, function(tj) {
|
|
|
|
var opts = tj instanceof Array ? tj : [tj];
|
|
|
|
var tileLayers = [],
|
|
markerLayers = [];
|
|
for (var i = 0; i < opts.length; i++) {
|
|
if (opts[i].layer) tileLayers.push(opts[i].layer);
|
|
if (opts[i].markers) markerLayers.push(opts[i].markers);
|
|
}
|
|
|
|
var map = mapbox.map(elem, tileLayers.concat(markerLayers)).auto();
|
|
if (callback) callback(map, tj);
|
|
});
|
|
};
|
|
|
|
|
|
// mapbox.load pulls a [TileJSON](http://mapbox.com/wax/tilejson.html)
|
|
// object from a server and uses it to configure a map and various map-related
|
|
// objects
|
|
mapbox.load = function(url, callback) {
|
|
|
|
// Support multiple urls
|
|
if (url instanceof Array) {
|
|
return mapbox.util.asyncMap(url, mapbox.load, callback);
|
|
}
|
|
|
|
// Support bare IDs as well as fully-formed URLs
|
|
if (url.indexOf('http') !== 0) {
|
|
url = mapbox.MAPBOX_URL + url + '.jsonp';
|
|
}
|
|
|
|
wax.tilejson(url, function(tj) {
|
|
// Pull zoom level out of center
|
|
tj.zoom = tj.center[2];
|
|
|
|
// Instantiate center as a Modest Maps-compatible object
|
|
tj.center = {
|
|
lat: tj.center[1],
|
|
lon: tj.center[0]
|
|
};
|
|
|
|
tj.thumbnail = mapbox.MAPBOX_URL + tj.id + '/thumb.png';
|
|
|
|
// Instantiate tile layer
|
|
tj.layer = mapbox.layer().tilejson(tj);
|
|
|
|
// Instantiate markers layer
|
|
if (tj.data) {
|
|
tj.markers = mapbox.markers.layer();
|
|
tj.markers.url(tj.data, function() {
|
|
mapbox.markers.interaction(tj.markers);
|
|
callback(tj);
|
|
});
|
|
} else {
|
|
callback(tj);
|
|
}
|
|
});
|
|
};
|
|
if (typeof mapbox === 'undefined') mapbox = {};
|
|
|
|
mapbox.ui = function(map) {
|
|
var ui = {
|
|
zoomer: wax.mm.zoomer().map(map).smooth(true),
|
|
pointselector: wax.mm.pointselector().map(map),
|
|
hash: wax.mm.hash().map(map),
|
|
zoombox: wax.mm.zoombox().map(map),
|
|
fullscreen: wax.mm.fullscreen().map(map),
|
|
legend: wax.mm.legend().map(map),
|
|
attribution: wax.mm.attribution().map(map)
|
|
};
|
|
|
|
function unique(x) {
|
|
var u = {}, l = [];
|
|
for (var i = 0; i < x.length; i++) u[x[i]] = true;
|
|
for (var a in u) { if (a) l.push(a); }
|
|
return l;
|
|
}
|
|
|
|
ui.refresh = function() {
|
|
if (!map) return console && console.error('ui not attached to map');
|
|
|
|
var attributions = [], legends = [];
|
|
for (var i = 0; i < map.layers.length; i++) {
|
|
if (map.layers[i].enabled && map.layers[i].tilejson) {
|
|
var attribution = map.layers[i].tilejson().attribution;
|
|
if (attribution) attributions.push(attribution);
|
|
var legend = map.layers[i].tilejson().legend;
|
|
if (legend) legends.push(legend);
|
|
}
|
|
}
|
|
|
|
var unique_attributions = unique(attributions);
|
|
var unique_legends = unique(legends);
|
|
|
|
ui.attribution.content(unique_attributions.length ? unique_attributions.join('<br />') : '');
|
|
ui.legend.content(unique_legends.length ? unique_legends.join('<br />') : '');
|
|
|
|
ui.attribution.element().style.display = unique_attributions.length ? '' : 'none';
|
|
ui.legend.element().style.display = unique_legends.length ? '' : 'none';
|
|
};
|
|
|
|
return ui;
|
|
};
|
|
if (typeof mapbox === 'undefined') mapbox = {};
|
|
|
|
|
|
mapbox.util = {
|
|
|
|
// Asynchronous map that groups results maintaining order
|
|
asyncMap: function(values, func, callback) {
|
|
var remaining = values.length,
|
|
results = [];
|
|
|
|
function next(index) {
|
|
return function(result) {
|
|
results[index] = result;
|
|
remaining--;
|
|
if (!remaining) callback(results);
|
|
};
|
|
}
|
|
|
|
for (var i = 0; i < values.length; i++) {
|
|
func(values[i], next(i));
|
|
}
|
|
}
|
|
};
|
|
if (typeof mapbox === 'undefined') mapbox = {};
|
|
|
|
mapbox.interaction = function() {
|
|
|
|
var interaction = wax.mm.interaction(),
|
|
auto = false;
|
|
|
|
interaction.refresh = function() {
|
|
var map = interaction.map();
|
|
if (!auto || !map) return interaction;
|
|
for (var i = map.layers.length - 1; i >= 0; i --) {
|
|
if (map.layers[i].enabled) {
|
|
var tj = map.layers[i].tilejson && map.layers[i].tilejson();
|
|
if (tj && tj.template) return interaction.tilejson(tj);
|
|
}
|
|
}
|
|
return interaction.tilejson({});
|
|
};
|
|
|
|
interaction.auto = function() {
|
|
auto = true;
|
|
interaction.on(wax.tooltip()
|
|
.animate(true)
|
|
.parent(interaction.map().parent)
|
|
.events()).on(wax.location().events());
|
|
return interaction.refresh();
|
|
};
|
|
|
|
return interaction;
|
|
};
|
|
if (typeof mapbox === 'undefined') mapbox = {};
|
|
|
|
mapbox.layer = function() {
|
|
if (!(this instanceof mapbox.layer)) {
|
|
return new mapbox.layer();
|
|
}
|
|
// instance variables
|
|
this._tilejson = {};
|
|
this._url = '';
|
|
this._id = '';
|
|
this._composite = true;
|
|
|
|
this.name = '';
|
|
this.parent = document.createElement('div');
|
|
this.parent.style.cssText = 'position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0';
|
|
this.levels = {};
|
|
this.requestManager = new MM.RequestManager();
|
|
this.requestManager.addCallback('requestcomplete', this.getTileComplete());
|
|
this.requestManager.addCallback('requesterror', this.getTileError());
|
|
this.setProvider(new wax.mm._provider({
|
|
tiles: ['data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7']
|
|
}));
|
|
};
|
|
|
|
mapbox.layer.prototype.refresh = function(callback) {
|
|
var that = this;
|
|
// When the async request for a TileJSON blob comes back,
|
|
// this resets its own tilejson and calls setProvider on itself.
|
|
wax.tilejson(this._url, function(o) {
|
|
that.tilejson(o);
|
|
if (callback) callback(that);
|
|
});
|
|
return this;
|
|
};
|
|
|
|
mapbox.layer.prototype.url = function(x, callback) {
|
|
if (!arguments.length) return this._url;
|
|
this._mapboxhosting = x.indexOf(mapbox.MAPBOX_URL) == 0;
|
|
this._url = x;
|
|
return this.refresh(callback);
|
|
};
|
|
|
|
mapbox.layer.prototype.id = function(x, callback) {
|
|
if (!arguments.length) return this._id;
|
|
this.named(x);
|
|
this._id = x;
|
|
return this.url(mapbox.MAPBOX_URL + x + '.jsonp', callback);
|
|
};
|
|
|
|
mapbox.layer.prototype.named = function(x) {
|
|
if (!arguments.length) return this.name;
|
|
this.name = x;
|
|
return this;
|
|
};
|
|
|
|
mapbox.layer.prototype.tilejson = function(x) {
|
|
if (!arguments.length) return this._tilejson;
|
|
|
|
if (!this._composite || !this._mapboxhosting) this.setProvider(new wax.mm._provider(x));
|
|
|
|
this._tilejson = x;
|
|
|
|
this.name = this.name || x.id;
|
|
this._id = this._id || x.id;
|
|
|
|
if (x.bounds) {
|
|
var proj = new MM.MercatorProjection(0,
|
|
MM.deriveTransformation(
|
|
-Math.PI, Math.PI, 0, 0,
|
|
Math.PI, Math.PI, 1, 0,
|
|
-Math.PI, -Math.PI, 0, 1));
|
|
|
|
this.provider.tileLimits = [
|
|
proj.locationCoordinate(new MM.Location(x.bounds[3], x.bounds[0]))
|
|
.zoomTo(x.minzoom ? x.minzoom : 0),
|
|
proj.locationCoordinate(new MM.Location(x.bounds[1], x.bounds[2]))
|
|
.zoomTo(x.maxzoom ? x.maxzoom : 18)
|
|
];
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
mapbox.layer.prototype.draw = function() {
|
|
if (!this.enabled || !this.map) return;
|
|
|
|
if (this._composite && this._mapboxhosting) {
|
|
|
|
// Get index of current layer
|
|
var i = 0;
|
|
for (i; i < this.map.layers.length; i++) {
|
|
if (this.map.layers[i] == this) break;
|
|
}
|
|
|
|
// If layer is composited by layer below it, don't draw
|
|
for (var j = i - 1; j >= 0; j--) {
|
|
if (this.map.getLayerAt(j).enabled) {
|
|
if (this.map.getLayerAt(j)._composite) {
|
|
this.parent.style.display = 'none';
|
|
this.compositeLayer = false;
|
|
return this;
|
|
}
|
|
else break;
|
|
}
|
|
}
|
|
|
|
// Get map IDs for all consecutive composited layers
|
|
var ids = [];
|
|
for (var k = i; k < this.map.layers.length; k++) {
|
|
var l = this.map.getLayerAt(k);
|
|
if (l.enabled) {
|
|
if (l._composite && l._mapboxhosting) ids.push(l.id());
|
|
else break;
|
|
}
|
|
}
|
|
ids = ids.join(',');
|
|
|
|
if (this.compositeLayer !== ids) {
|
|
this.compositeLayer = ids;
|
|
var that = this;
|
|
wax.tilejson(mapbox.MAPBOX_URL + ids + '.jsonp', function(tiledata) {
|
|
that.setProvider(new wax.mm._provider(tiledata));
|
|
// setProvider calls .draw()
|
|
});
|
|
this.parent.style.display = '';
|
|
return this;
|
|
}
|
|
|
|
} else {
|
|
this.parent.style.display = '';
|
|
// Set back to regular provider
|
|
if (this.compositeLayer) {
|
|
this.compositeLayer = false;
|
|
this.setProvider(new wax.mm._provider(this.tilejson()));
|
|
// .draw() called by .tilejson()
|
|
}
|
|
}
|
|
|
|
return MM.Layer.prototype.draw.call(this);
|
|
};
|
|
|
|
mapbox.layer.prototype.composite = function(x) {
|
|
if (!arguments.length) return this._composite;
|
|
if (x) this._composite = true;
|
|
else this._composite = false;
|
|
return this;
|
|
};
|
|
|
|
// we need to redraw map due to compositing
|
|
mapbox.layer.prototype.enable = function(x) {
|
|
MM.Layer.prototype.enable.call(this, x);
|
|
if (this.map) this.map.draw();
|
|
return this;
|
|
};
|
|
|
|
// we need to redraw map due to compositing
|
|
mapbox.layer.prototype.disable = function(x) {
|
|
MM.Layer.prototype.disable.call(this, x);
|
|
if (this.map) this.map.draw();
|
|
return this;
|
|
};
|
|
|
|
MM.extend(mapbox.layer, MM.Layer);
|