/* 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 '
' 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 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 `
`, 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. * *

* 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 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 !') * # '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 // , 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]*?--\>|]*>|<\\?[^>*]*>' // '/' 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 // // 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('' ? '>' : '&', 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(''); } } stack.length = index; out.push(''); } }, 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.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 += '

' + props.title + '
'; } if (props.description) { o += '
' + props.description + '
'; } 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('
') : ''); ui.legend.content(unique_legends.length ? unique_legends.join('
') : ''); 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);