`, 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('' + tagName);
if (dataEnd < 0) { dataEnd = htmlText.length; }
if (dataEnd) {
if (eflags & html4.eflags.CDATA) {
if (handler.cdata) {
handler.cdata(htmlText.substring(0, dataEnd), param);
}
} else if (handler.rcdata) {
handler.rcdata(
normalizeRCData(htmlText.substring(0, dataEnd)), param);
}
htmlText = htmlText.substring(dataEnd);
}
}
tagName = eflags = openTag = void 0;
attribs.length = 0;
inTag = false;
}
} else {
if (m[1]) { // Entity
if (handler.pcdata) { handler.pcdata(m[0], param); }
} else if (m[3]) { // Tag
openTag = !m[2];
inTag = true;
tagName = lcase(m[3]);
eflags = html4.ELEMENTS.hasOwnProperty(tagName)
? html4.ELEMENTS[tagName] : void 0;
} else if (m[4]) { // Text
if (handler.pcdata) { handler.pcdata(m[4], param); }
} else if (m[5]) { // Cruft
if (handler.pcdata) {
var ch = m[5];
handler.pcdata(
ch === '<' ? '<' : ch === '>' ? '>' : '&',
param);
}
}
}
}
if (handler.endDoc) { handler.endDoc(param); }
};
}
/**
* Returns a function that strips unsafe tags and attributes from html.
* @param {Function} sanitizeAttributes
* maps from (tagName, attribs[]) to null or a sanitized attribute array.
* The attribs array can be arbitrarily modified, but the same array
* instance is reused, so should not be held.
* @return {Function} from html to sanitized html
*/
function makeHtmlSanitizer(sanitizeAttributes) {
var stack;
var ignoring;
return makeSaxParser({
startDoc: function (_) {
stack = [];
ignoring = false;
},
startTag: function (tagName, attribs, out) {
if (ignoring) { return; }
if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
var eflags = html4.ELEMENTS[tagName];
if (eflags & html4.eflags.FOLDABLE) {
return;
} else if (eflags & html4.eflags.UNSAFE) {
ignoring = !(eflags & html4.eflags.EMPTY);
return;
}
attribs = sanitizeAttributes(tagName, attribs);
// TODO(mikesamuel): relying on sanitizeAttributes not to
// insert unsafe attribute names.
if (attribs) {
if (!(eflags & html4.eflags.EMPTY)) {
stack.push(tagName);
}
out.push('<', tagName);
for (var i = 0, n = attribs.length; i < n; i += 2) {
var attribName = attribs[i],
value = attribs[i + 1];
if (value !== null && value !== void 0) {
out.push(' ', attribName, '="', escapeAttrib(value), '"');
}
}
out.push('>');
}
},
endTag: function (tagName, out) {
if (ignoring) {
ignoring = false;
return;
}
if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; }
var eflags = html4.ELEMENTS[tagName];
if (!(eflags & (html4.eflags.UNSAFE | html4.eflags.EMPTY
| html4.eflags.FOLDABLE))) {
var index;
if (eflags & html4.eflags.OPTIONAL_ENDTAG) {
for (index = stack.length; --index >= 0;) {
var stackEl = stack[index];
if (stackEl === tagName) { break; }
if (!(html4.ELEMENTS[stackEl]
& html4.eflags.OPTIONAL_ENDTAG)) {
// Don't pop non optional end tags looking for a match.
return;
}
}
} else {
for (index = stack.length; --index >= 0;) {
if (stack[index] === tagName) { break; }
}
}
if (index < 0) { return; } // Not opened.
for (var i = stack.length; --i > index;) {
var stackEl = stack[i];
if (!(html4.ELEMENTS[stackEl]
& html4.eflags.OPTIONAL_ENDTAG)) {
out.push('', stackEl, '>');
}
}
stack.length = index;
out.push('', tagName, '>');
}
},
pcdata: function (text, out) {
if (!ignoring) { out.push(text); }
},
rcdata: function (text, out) {
if (!ignoring) { out.push(text); }
},
cdata: function (text, out) {
if (!ignoring) { out.push(text); }
},
endDoc: function (out) {
for (var i = stack.length; --i >= 0;) {
out.push('', stack[i], '>');
}
stack.length = 0;
}
});
}
// From RFC3986
var URI_SCHEME_RE = new RegExp(
"^" +
"(?:" +
"([^:\/?#]+)" + // scheme
":)?"
);
/**
* Strips unsafe tags and attributes from html.
* @param {string} htmlText to sanitize
* @param {Function} opt_uriPolicy -- a transform to apply to uri/url
* attribute values. If no opt_uriPolicy is provided, no uris
* are allowed ie. the default uriPolicy rewrites all uris to null
* @param {Function} opt_nmTokenPolicy : string -> string? -- a transform to
* apply to names, ids, and classes. If no opt_nmTokenPolicy is provided,
* all names, ids and classes are passed through ie. the default
* nmTokenPolicy is an identity transform
* @return {string} html
*/
function sanitize(htmlText, opt_uriPolicy, opt_nmTokenPolicy) {
var out = [];
makeHtmlSanitizer(
function sanitizeAttribs(tagName, attribs) {
for (var i = 0; i < attribs.length; i += 2) {
var attribName = attribs[i];
var value = attribs[i + 1];
var atype = null, attribKey;
if ((attribKey = tagName + '::' + attribName,
html4.ATTRIBS.hasOwnProperty(attribKey))
|| (attribKey = '*::' + attribName,
html4.ATTRIBS.hasOwnProperty(attribKey))) {
atype = html4.ATTRIBS[attribKey];
}
if (atype !== null) {
switch (atype) {
case html4.atype.NONE: break;
case html4.atype.SCRIPT:
case html4.atype.STYLE:
value = null;
break;
case html4.atype.ID:
case html4.atype.IDREF:
case html4.atype.IDREFS:
case html4.atype.GLOBAL_NAME:
case html4.atype.LOCAL_NAME:
case html4.atype.CLASSES:
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
break;
case html4.atype.URI:
var parsedUri = ('' + value).match(URI_SCHEME_RE);
if (!parsedUri) {
value = null;
} else if (!parsedUri[1] ||
WHITELISTED_SCHEMES.test(parsedUri[1])) {
value = opt_uriPolicy && opt_uriPolicy(value);
} else {
value = null;
}
break;
case html4.atype.URI_FRAGMENT:
if (value && '#' === value.charAt(0)) {
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
if (value) { value = '#' + value; }
} else {
value = null;
}
break;
default:
value = null;
break;
}
} else {
value = null;
}
attribs[i + 1] = value;
}
return attribs;
})(htmlText, out);
return out.join('');
}
return {
escapeAttrib: escapeAttrib,
makeHtmlSanitizer: makeHtmlSanitizer,
makeSaxParser: makeSaxParser,
normalizeRCData: normalizeRCData,
sanitize: sanitize,
unescapeEntities: unescapeEntities
};
})(html4);
var html_sanitize = html.sanitize;
// Exports for closure compiler. Note this file is also cajoled
// for domado and run in an environment without 'window'
if (typeof window !== 'undefined') {
window['html'] = html;
window['html_sanitize'] = html_sanitize;
}
// Loosen restrictions of Caja's
// html-sanitizer to allow for styling
html4.ATTRIBS['*::style'] = 0;
html4.ELEMENTS['style'] = 0;
html4.ATTRIBS['a::target'] = 0;
html4.ELEMENTS['video'] = 0;
html4.ATTRIBS['video::src'] = 0;
html4.ATTRIBS['video::poster'] = 0;
html4.ATTRIBS['video::controls'] = 0;
html4.ELEMENTS['audio'] = 0;
html4.ATTRIBS['audio::src'] = 0;
html4.ATTRIBS['video::autoplay'] = 0;
html4.ATTRIBS['video::controls'] = 0;
;wax = wax || {};
// Attribution
// -----------
wax.attribution = function() {
var a = {};
var container = document.createElement('div');
container.className = 'map-attribution';
a.content = function(x) {
if (typeof x === 'undefined') return container.innerHTML;
container.innerHTML = wax.u.sanitize(x);
return this;
};
a.element = function() {
return container;
};
a.init = function() {
return this;
};
return a;
};
wax = wax || {};
// Attribution
// -----------
wax.bwdetect = function(options, callback) {
var detector = {},
threshold = options.threshold || 400,
// test image: 30.29KB
testImage = 'http://a.tiles.mapbox.com/mapbox/1.0.0/blue-marble-topo-bathy-jul/0/0/0.png?preventcache=' + (+new Date()),
// High-bandwidth assumed
// 1: high bandwidth (.png, .jpg)
// 0: low bandwidth (.png128, .jpg70)
bw = 1,
// Alternative versions
auto = options.auto === undefined ? true : options.auto;
function bwTest() {
wax.bw = -1;
var im = new Image();
im.src = testImage;
var first = true;
var timeout = setTimeout(function() {
if (first && wax.bw == -1) {
detector.bw(0);
first = false;
}
}, threshold);
im.onload = function() {
if (first && wax.bw == -1) {
clearTimeout(timeout);
detector.bw(1);
first = false;
}
};
}
detector.bw = function(x) {
if (!arguments.length) return bw;
var oldBw = bw;
if (wax.bwlisteners && wax.bwlisteners.length) (function () {
listeners = wax.bwlisteners;
wax.bwlisteners = [];
for (i = 0; i < listeners; i++) {
listeners[i](x);
}
})();
wax.bw = x;
if (bw != (bw = x)) callback(x);
};
detector.add = function() {
if (auto) bwTest();
return this;
};
if (wax.bw == -1) {
wax.bwlisteners = wax.bwlisteners || [];
wax.bwlisteners.push(detector.bw);
} else if (wax.bw !== undefined) {
detector.bw(wax.bw);
} else {
detector.add();
}
return detector;
};
// Formatter
// ---------
//
// This code is no longer the recommended code path for Wax -
// see `template.js`, a safe implementation of Mustache templates.
wax.formatter = function(x) {
var formatter = {},
f;
// Prevent against just any input being used.
if (x && typeof x === 'string') {
try {
// Ugly, dangerous use of eval.
eval('f = ' + x);
} catch (e) {
if (console) console.log(e);
}
} else if (x && typeof x === 'function') {
f = x;
} else {
f = function() {};
}
// Wrap the given formatter function in order to
// catch exceptions that it may throw.
formatter.format = function(options, data) {
try {
return wax.u.sanitize(f(options, data));
} catch (e) {
if (console) console.log(e);
}
};
return formatter;
};
// GridInstance
// ------------
// GridInstances are queryable, fully-formed
// objects for acquiring features from events.
//
// This code ignores format of 1.1-1.2
wax.gi = function(grid_tile, options) {
options = options || {};
// resolution is the grid-elements-per-pixel ratio of gridded data.
// The size of a tile element. For now we expect tiles to be squares.
var instance = {},
resolution = options.resolution || 4,
tileSize = options.tileSize || 256;
// Resolve the UTF-8 encoding stored in grids to simple
// number values.
// See the [utfgrid spec](https://github.com/mapbox/utfgrid-spec)
// for details.
function resolveCode(key) {
if (key >= 93) key--;
if (key >= 35) key--;
key -= 32;
return key;
}
instance.grid_tile = function() {
return grid_tile;
};
instance.getKey = function(x, y) {
if (!(grid_tile && grid_tile.grid)) return;
if ((y < 0) || (x < 0)) return;
if ((Math.floor(y) >= tileSize) ||
(Math.floor(x) >= tileSize)) return;
// Find the key in the grid. The above calls should ensure that
// the grid's array is large enough to make this work.
return resolveCode(grid_tile.grid[
Math.floor((y) / resolution)
].charCodeAt(
Math.floor((x) / resolution)
));
};
// Lower-level than tileFeature - has nothing to do
// with the DOM. Takes a px offset from 0, 0 of a grid.
instance.gridFeature = function(x, y) {
// Find the key in the grid. The above calls should ensure that
// the grid's array is large enough to make this work.
var key = this.getKey(x, y),
keys = grid_tile.keys;
if (keys &&
keys[key] &&
grid_tile.data[keys[key]]) {
return grid_tile.data[keys[key]];
}
};
// Get a feature:
// * `x` and `y`: the screen coordinates of an event
// * `tile_element`: a DOM element of a tile, from which we can get an offset.
instance.tileFeature = function(x, y, tile_element) {
if (!grid_tile) return;
// IE problem here - though recoverable, for whatever reason
var offset = wax.u.offset(tile_element);
feature = this.gridFeature(x - offset.left, y - offset.top);
return feature;
};
return instance;
};
// GridManager
// -----------
// Generally one GridManager will be used per map.
//
// It takes one options object, which current accepts a single option:
// `resolution` determines the number of pixels per grid element in the grid.
// The default is 4.
wax.gm = function() {
var resolution = 4,
grid_tiles = {},
manager = {},
tilejson,
formatter;
var gridUrl = function(url) {
if (url) {
return url.replace(/(\.png|\.jpg|\.jpeg)(\d*)/, '.grid.json');
}
};
function templatedGridUrl(template) {
if (typeof template === 'string') template = [template];
return function templatedGridFinder(url) {
if (!url) return;
var rx = new RegExp('/(\\d+)\\/(\\d+)\\/(\\d+)\\.[\\w\\._]+');
var xyz = rx.exec(url);
if (!xyz) return;
return template[parseInt(xyz[2], 10) % template.length]
.replace(/\{z\}/g, xyz[1])
.replace(/\{x\}/g, xyz[2])
.replace(/\{y\}/g, xyz[3]);
};
}
manager.formatter = function(x) {
if (!arguments.length) return formatter;
formatter = wax.formatter(x);
return manager;
};
manager.template = function(x) {
if (!arguments.length) return formatter;
formatter = wax.template(x);
return manager;
};
manager.gridUrl = function(x) {
// Getter-setter
if (!arguments.length) return gridUrl;
// Handle tilesets that don't support grids
if (!x) {
gridUrl = function() { return null; };
} else {
gridUrl = typeof x === 'function' ?
x : templatedGridUrl(x);
}
return manager;
};
manager.getGrid = function(url, callback) {
var gurl = gridUrl(url);
if (!formatter || !gurl) return callback(null, null);
wax.request.get(gurl, function(err, t) {
if (err) return callback(err, null);
callback(null, wax.gi(t, {
formatter: formatter,
resolution: resolution
}));
});
return manager;
};
manager.tilejson = function(x) {
if (!arguments.length) return tilejson;
// prefer templates over formatters
if (x.template) {
manager.template(x.template);
} else if (x.formatter) {
manager.formatter(x.formatter);
} else {
// In this case, we cannot support grids
formatter = undefined;
}
manager.gridUrl(x.grids);
if (x.resolution) resolution = x.resolution;
tilejson = x;
return manager;
};
return manager;
};
wax = wax || {};
// Hash
// ----
wax.hash = function(options) {
options = options || {};
var s0, // old hash
hash = {},
lat = 90 - 1e-8; // allowable latitude range
function getState() {
return location.hash.substring(1);
}
function pushState(state) {
var l = window.location;
l.replace(l.toString().replace((l.hash || /$/), '#' + state));
}
function parseHash(s) {
var args = s.split('/');
for (var i = 0; i < args.length; i++) {
args[i] = Number(args[i]);
if (isNaN(args[i])) return true;
}
if (args.length < 3) {
// replace bogus hash
return true;
} else if (args.length == 3) {
options.setCenterZoom(args);
}
}
function move() {
var s1 = options.getCenterZoom();
if (s0 !== s1) {
s0 = s1;
// don't recenter the map!
pushState(s0);
}
}
function stateChange(state) {
// ignore spurious hashchange events
if (state === s0) return;
if (parseHash(s0 = state)) {
// replace bogus hash
move();
}
}
var _move = wax.u.throttle(move, 500);
hash.add = function() {
stateChange(getState());
options.bindChange(_move);
return hash;
};
hash.remove = function() {
options.unbindChange(_move);
return hash;
};
return hash;
};
wax = wax || {};
wax.interaction = function() {
var gm = wax.gm(),
interaction = {},
_downLock = false,
_clickTimeout = null,
// Active feature
// Down event
_d,
// Touch tolerance
tol = 4,
grid,
attach,
detach,
parent,
map,
tileGrid;
var defaultEvents = {
mousemove: onMove,
touchstart: onDown,
mousedown: onDown
};
var touchEnds = {
touchend: onUp,
touchmove: onUp,
touchcancel: touchCancel
};
// Abstract getTile method. Depends on a tilegrid with
// grid[ [x, y, tile] ] structure.
function getTile(e) {
var g = grid();
for (var i = 0; i < g.length; i++) {
if ((g[i][0] < e.y) &&
((g[i][0] + 256) > e.y) &&
(g[i][1] < e.x) &&
((g[i][1] + 256) > e.x)) return g[i][2];
}
return false;
}
// Clear the double-click timeout to prevent double-clicks from
// triggering popups.
function killTimeout() {
if (_clickTimeout) {
window.clearTimeout(_clickTimeout);
_clickTimeout = null;
return true;
} else {
return false;
}
}
function onMove(e) {
// If the user is actually dragging the map, exit early
// to avoid performance hits.
if (_downLock) return;
var pos = wax.u.eventoffset(e);
interaction.screen_feature(pos, function(feature) {
if (feature) {
bean.fire(interaction, 'on', {
parent: parent(),
data: feature,
formatter: gm.formatter().format,
e: e
});
} else {
bean.fire(interaction, 'off');
}
});
}
function dragEnd() {
_downLock = false;
}
// A handler for 'down' events - which means `mousedown` and `touchstart`
function onDown(e) {
// Prevent interaction offset calculations happening while
// the user is dragging the map.
//
// Store this event so that we can compare it to the
// up event
_downLock = true;
_d = wax.u.eventoffset(e);
if (e.type === 'mousedown') {
bean.add(document.body, 'click', onUp);
// track mouse up to remove lockDown when the drags end
bean.add(document.body, 'mouseup', dragEnd);
// Only track single-touches. Double-touches will not affect this
// control
} else if (e.type === 'touchstart' && e.touches.length === 1) {
// Don't make the user click close if they hit another tooltip
bean.fire(interaction, 'off');
// Touch moves invalidate touches
bean.add(e.srcElement, touchEnds);
}
}
function touchCancel(e) {
bean.remove(e.srcElement, touchEnds);
_downLock = false;
}
function onUp(e) {
var evt = {},
pos = wax.u.eventoffset(e);
_downLock = false;
// TODO: refine
for (var key in e) {
evt[key] = e[key];
}
bean.remove(document.body, 'mouseup', onUp);
bean.remove(e.srcElement, touchEnds);
if (e.type === 'touchend') {
// If this was a touch and it survived, there's no need to avoid a double-tap
// but also wax.u.eventoffset will have failed, since this touch
// event doesn't have coordinates
interaction.click(e, _d);
} else if (Math.round(pos.y / tol) === Math.round(_d.y / tol) &&
Math.round(pos.x / tol) === Math.round(_d.x / tol)) {
// Contain the event data in a closure.
// Ignore double-clicks by ignoring clicks within 300ms of
// each other.
if(!_clickTimeout) {
_clickTimeout = window.setTimeout(function() {
_clickTimeout = null;
interaction.click(evt, pos);
}, 300);
} else {
killTimeout();
}
}
return onUp;
}
// Handle a click event. Takes a second
interaction.click = function(e, pos) {
interaction.screen_feature(pos, function(feature) {
if (feature) bean.fire(interaction, 'on', {
parent: parent(),
data: feature,
formatter: gm.formatter().format,
e: e
});
});
};
interaction.screen_feature = function(pos, callback) {
var tile = getTile(pos);
if (!tile) callback(null);
gm.getGrid(tile.src, function(err, g) {
if (err || !g) return callback(null);
var feature = g.tileFeature(pos.x, pos.y, tile);
callback(feature);
});
};
// set an attach function that should be
// called when maps are set
interaction.attach = function(x) {
if (!arguments.length) return attach;
attach = x;
return interaction;
};
interaction.detach = function(x) {
if (!arguments.length) return detach;
detach = x;
return interaction;
};
// Attach listeners to the map
interaction.map = function(x) {
if (!arguments.length) return map;
map = x;
if (attach) attach(map);
bean.add(parent(), defaultEvents);
bean.add(parent(), 'touchstart', onDown);
return interaction;
};
// set a grid getter for this control
interaction.grid = function(x) {
if (!arguments.length) return grid;
grid = x;
return interaction;
};
// detach this and its events from the map cleanly
interaction.remove = function(x) {
if (detach) detach(map);
bean.remove(parent(), defaultEvents);
bean.fire(interaction, 'remove');
return interaction;
};
// get or set a tilejson chunk of json
interaction.tilejson = function(x) {
if (!arguments.length) return gm.tilejson();
gm.tilejson(x);
return interaction;
};
// return the formatter, which has an exposed .format
// function
interaction.formatter = function() {
return gm.formatter();
};
// ev can be 'on', 'off', fn is the handler
interaction.on = function(ev, fn) {
bean.add(interaction, ev, fn);
return interaction;
};
// ev can be 'on', 'off', fn is the handler
interaction.off = function(ev, fn) {
bean.remove(interaction, ev, fn);
return interaction;
};
// Return or set the gridmanager implementation
interaction.gridmanager = function(x) {
if (!arguments.length) return gm;
gm = x;
return interaction;
};
// parent should be a function that returns
// the parent element of the map
interaction.parent = function(x) {
parent = x;
return interaction;
};
return interaction;
};
// Wax Legend
// ----------
// Wax header
var wax = wax || {};
wax.legend = function() {
var element,
legend = {},
container;
legend.element = function() {
return container;
};
legend.content = function(content) {
if (!arguments.length) return element.innerHTML;
element.innerHTML = wax.u.sanitize(content);
element.style.display = 'block';
if (element.innerHTML === '') {
element.style.display = 'none';
}
return legend;
};
legend.add = function() {
container = document.createElement('div');
container.className = 'map-legends wax-legends';
element = container.appendChild(document.createElement('div'));
element.className = 'map-legend wax-legend';
element.style.display = 'none';
return legend;
};
return legend.add();
};
var wax = wax || {};
wax.location = function() {
var t = {};
function on(o) {
if ((o.e.type === 'mousemove' || !o.e.type)) {
return;
} else {
var loc = o.formatter({ format: 'location' }, o.data);
if (loc) {
window.top.location.href = loc;
}
}
}
t.events = function() {
return {
on: on
};
};
return t;
};
var wax = wax || {};
wax.movetip = {};
wax.movetip = function() {
var popped = false,
t = {},
_tooltipOffset,
_contextOffset,
tooltip,
parent;
function moveTooltip(e) {
var eo = wax.u.eventoffset(e);
// faux-positioning
if ((_tooltipOffset.height + eo.y) >
(_contextOffset.top + _contextOffset.height) &&
(_contextOffset.height > _tooltipOffset.height)) {
eo.y -= _tooltipOffset.height;
tooltip.className += ' flip-y';
}
// faux-positioning
if ((_tooltipOffset.width + eo.x) >
(_contextOffset.left + _contextOffset.width)) {
eo.x -= _tooltipOffset.width;
tooltip.className += ' flip-x';
}
tooltip.style.left = eo.x + 'px';
tooltip.style.top = eo.y + 'px';
}
// Get the active tooltip for a layer or create a new one if no tooltip exists.
// Hide any tooltips on layers underneath this one.
function getTooltip(feature) {
var tooltip = document.createElement('div');
tooltip.className = 'map-tooltip map-tooltip-0';
tooltip.innerHTML = feature;
return tooltip;
}
// Hide a given tooltip.
function hide() {
if (tooltip) {
tooltip.parentNode.removeChild(tooltip);
tooltip = null;
}
}
function on(o) {
var content;
if (popped) return;
if ((o.e.type === 'mousemove' || !o.e.type)) {
content = o.formatter({ format: 'teaser' }, o.data);
if (!content) return;
hide();
parent.style.cursor = 'pointer';
tooltip = document.body.appendChild(getTooltip(content));
} else {
content = o.formatter({ format: 'teaser' }, o.data);
if (!content) return;
hide();
var tt = document.body.appendChild(getTooltip(content));
tt.className += ' map-popup';
var close = tt.appendChild(document.createElement('a'));
close.href = '#close';
close.className = 'close';
close.innerHTML = 'Close';
popped = true;
tooltip = tt;
_tooltipOffset = wax.u.offset(tooltip);
_contextOffset = wax.u.offset(parent);
moveTooltip(o.e);
bean.add(close, 'click touchend', function closeClick(e) {
e.stop();
hide();
popped = false;
});
}
if (tooltip) {
_tooltipOffset = wax.u.offset(tooltip);
_contextOffset = wax.u.offset(parent);
moveTooltip(o.e);
}
}
function off() {
parent.style.cursor = 'default';
if (!popped) hide();
}
t.parent = function(x) {
if (!arguments.length) return parent;
parent = x;
return t;
};
t.events = function() {
return {
on: on,
off: off
};
};
return t;
};
// Wax GridUtil
// ------------
// Wax header
var wax = wax || {};
// Request
// -------
// Request data cache. `callback(data)` where `data` is the response data.
wax.request = {
cache: {},
locks: {},
promises: {},
get: function(url, callback) {
// Cache hit.
if (this.cache[url]) {
return callback(this.cache[url][0], this.cache[url][1]);
// Cache miss.
} else {
this.promises[url] = this.promises[url] || [];
this.promises[url].push(callback);
// Lock hit.
if (this.locks[url]) return;
// Request.
var that = this;
this.locks[url] = true;
reqwest({
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=?',
type: 'jsonp',
success: function(data) {
that.locks[url] = false;
that.cache[url] = [null, data];
for (var i = 0; i < that.promises[url].length; i++) {
that.promises[url][i](that.cache[url][0], that.cache[url][1]);
}
},
error: function(err) {
that.locks[url] = false;
that.cache[url] = [err, null];
for (var i = 0; i < that.promises[url].length; i++) {
that.promises[url][i](that.cache[url][0], that.cache[url][1]);
}
}
});
}
}
};
// Templating
// ---------
wax.template = function(x) {
var template = {};
// Clone the data object such that the '__[format]__' key is only
// set for this instance of templating.
template.format = function(options, data) {
var clone = {};
for (var key in data) {
clone[key] = data[key];
}
if (options.format) {
clone['__' + options.format + '__'] = true;
}
return wax.u.sanitize(Mustache.to_html(x, clone));
};
return template;
};
if (!wax) var wax = {};
// A wrapper for reqwest jsonp to easily load TileJSON from a URL.
wax.tilejson = function(url, callback) {
reqwest({
url: url + (~url.indexOf('?') ? '&' : '?') + 'callback=?',
type: 'jsonp',
success: callback,
error: callback
});
};
var wax = wax || {};
wax.tooltip = {};
wax.tooltip = function() {
var popped = false,
animate = false,
t = {},
tooltips = [],
_currentContent,
transitionEvent,
parent;
if (document.body.style['-webkit-transition'] !== undefined) {
transitionEvent = 'webkitTransitionEnd';
} else if (document.body.style.MozTransition !== undefined) {
transitionEvent = 'transitionend';
}
// Get the active tooltip for a layer or create a new one if no tooltip exists.
// Hide any tooltips on layers underneath this one.
function getTooltip(feature) {
var tooltip = document.createElement('div');
tooltip.className = 'map-tooltip map-tooltip-0 wax-tooltip';
tooltip.innerHTML = feature;
return tooltip;
}
function remove() {
if (this.parentNode) this.parentNode.removeChild(this);
}
// Hide a given tooltip.
function hide() {
var _ct;
while (_ct = tooltips.pop()) {
if (animate && transitionEvent) {
// This code assumes that transform-supporting browsers
// also support proper events. IE9 does both.
bean.add(_ct, transitionEvent, remove);
_ct.className += ' map-fade';
} else {
if (_ct.parentNode) _ct.parentNode.removeChild(_ct);
}
}
}
function on(o) {
var content;
if (o.e.type === 'mousemove' || !o.e.type) {
if (!popped) {
content = o.content || o.formatter({ format: 'teaser' }, o.data);
if (!content || content == _currentContent) return;
hide();
parent.style.cursor = 'pointer';
tooltips.push(parent.appendChild(getTooltip(content)));
_currentContent = content;
}
} else {
content = o.content || o.formatter({ format: 'full' }, o.data);
if (!content) {
if (o.e.type && o.e.type.match(/touch/)) {
// fallback possible
content = o.content || o.formatter({ format: 'teaser' }, o.data);
}
// but if that fails, return just the same.
if (!content) return;
}
hide();
parent.style.cursor = 'pointer';
var tt = parent.appendChild(getTooltip(content));
tt.className += ' map-popup wax-popup';
var close = tt.appendChild(document.createElement('a'));
close.href = '#close';
close.className = 'close';
close.innerHTML = 'Close';
popped = true;
tooltips.push(tt);
bean.add(close, 'touchstart mousedown', function(e) {
e.stop();
});
bean.add(close, 'click touchend', function closeClick(e) {
e.stop();
hide();
popped = false;
});
}
}
function off() {
parent.style.cursor = 'default';
_currentContent = null;
if (!popped) hide();
}
t.parent = function(x) {
if (!arguments.length) return parent;
parent = x;
return t;
};
t.animate = function(x) {
if (!arguments.length) return animate;
animate = x;
return t;
};
t.events = function() {
return {
on: on,
off: off
};
};
return t;
};
var wax = wax || {};
// Utils are extracted from other libraries or
// written from scratch to plug holes in browser compatibility.
wax.u = {
// From Bonzo
offset: function(el) {
// TODO: window margins
//
// Okay, so fall back to styles if offsetWidth and height are botched
// by Firefox.
var width = el.offsetWidth || parseInt(el.style.width, 10),
height = el.offsetHeight || parseInt(el.style.height, 10),
doc_body = document.body,
top = 0,
left = 0;
var calculateOffset = function(el) {
if (el === doc_body || el === document.documentElement) return;
top += el.offsetTop;
left += el.offsetLeft;
var style = el.style.transform ||
el.style.WebkitTransform ||
el.style.OTransform ||
el.style.MozTransform ||
el.style.msTransform;
if (style) {
var match;
if (match = style.match(/translate\((.+)[px]?, (.+)[px]?\)/)) {
top += parseInt(match[2], 10);
left += parseInt(match[1], 10);
} else if (match = style.match(/translate3d\((.+)[px]?, (.+)[px]?, (.+)[px]?\)/)) {
top += parseInt(match[2], 10);
left += parseInt(match[1], 10);
} else if (match = style.match(/matrix3d\(([\-\d,\s]+)\)/)) {
var pts = match[1].split(',');
top += parseInt(pts[13], 10);
left += parseInt(pts[12], 10);
} else if (match = style.match(/matrix\(.+, .+, .+, .+, (.+), (.+)\)/)) {
top += parseInt(match[2], 10);
left += parseInt(match[1], 10);
}
}
};
// from jquery, offset.js
if ( typeof el.getBoundingClientRect !== "undefined" ) {
var body = document.body;
var doc = el.ownerDocument.documentElement;
var clientTop = document.clientTop || body.clientTop || 0;
var clientLeft = document.clientLeft || body.clientLeft || 0;
var scrollTop = window.pageYOffset || doc.scrollTop;
var scrollLeft = window.pageXOffset || doc.scrollLeft;
var box = el.getBoundingClientRect();
top = box.top + scrollTop - clientTop;
left = box.left + scrollLeft - clientLeft;
} else {
calculateOffset(el);
try {
while (el = el.offsetParent) { calculateOffset(el); }
} catch(e) {
// Hello, internet explorer.
}
}
// Offsets from the body
top += doc_body.offsetTop;
left += doc_body.offsetLeft;
// Offsets from the HTML element
top += doc_body.parentNode.offsetTop;
left += doc_body.parentNode.offsetLeft;
// Firefox and other weirdos. Similar technique to jQuery's
// `doesNotIncludeMarginInBodyOffset`.
var htmlComputed = document.defaultView ?
window.getComputedStyle(doc_body.parentNode, null) :
doc_body.parentNode.currentStyle;
if (doc_body.parentNode.offsetTop !==
parseInt(htmlComputed.marginTop, 10) &&
!isNaN(parseInt(htmlComputed.marginTop, 10))) {
top += parseInt(htmlComputed.marginTop, 10);
left += parseInt(htmlComputed.marginLeft, 10);
}
return {
top: top,
left: left,
height: height,
width: width
};
},
'$': function(x) {
return (typeof x === 'string') ?
document.getElementById(x) :
x;
},
// From quirksmode: normalize the offset of an event from the top-left
// of the page.
eventoffset: function(e) {
var posx = 0;
var posy = 0;
if (!e) { e = window.event; }
if (e.pageX || e.pageY) {
// Good browsers
return {
x: e.pageX,
y: e.pageY
};
} else if (e.clientX || e.clientY) {
// Internet Explorer
return {
x: e.clientX,
y: e.clientY
};
} else if (e.touches && e.touches.length === 1) {
// Touch browsers
return {
x: e.touches[0].pageX,
y: e.touches[0].pageY
};
}
},
// Ripped from underscore.js
// Internal function used to implement `_.throttle` and `_.debounce`.
limit: function(func, wait, debounce) {
var timeout;
return function() {
var context = this, args = arguments;
var throttler = function() {
timeout = null;
func.apply(context, args);
};
if (debounce) clearTimeout(timeout);
if (debounce || !timeout) timeout = setTimeout(throttler, wait);
};
},
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
throttle: function(func, wait) {
return this.limit(func, wait, false);
},
sanitize: function(content) {
if (!content) return '';
function urlX(url) {
// Data URIs are subject to a bug in Firefox
// https://bugzilla.mozilla.org/show_bug.cgi?id=255107
// which let them be a vector. But WebKit does 'the right thing'
// or at least 'something' about this situation, so we'll tolerate
// them.
if (/^(https?:\/\/|data:image)/.test(url)) {
return url;
}
}
function idX(id) { return id; }
return html_sanitize(content, urlX, idX);
}
};
wax = wax || {};
wax.mm = wax.mm || {};
wax.mm.attribution = function() {
var map,
a = {},
container = document.createElement('div');
container.className = 'map-attribution map-mm';
a.content = function(x) {
if (typeof x === 'undefined') return container.innerHTML;
container.innerHTML = wax.u.sanitize(x);
return a;
};
a.element = function() {
return container;
};
a.map = function(x) {
if (!arguments.length) return map;
map = x;
return a;
};
a.add = function() {
if (!map) return false;
map.parent.appendChild(container);
return a;
};
a.remove = function() {
if (!map) return false;
if (container.parentNode) container.parentNode.removeChild(container);
return a;
};
a.appendTo = function(elem) {
wax.u.$(elem).appendChild(container);
return a;
};
return a;
};
wax = wax || {};
wax.mm = wax.mm || {};
wax.mm.boxselector = function() {
var corner,
nearCorner,
boxDiv,
style,
borderWidth = 0,
horizontal = false, // Whether the resize is horizontal
vertical = false,
edge = 5, // Distance from border sensitive to resizing
addEvent = MM.addEvent,
removeEvent = MM.removeEvent,
box,
boxselector = {},
map,
callbackManager = new MM.CallbackManager(boxselector, ['change']);
function getMousePoint(e) {
// start with just the mouse (x, y)
var point = new MM.Point(e.clientX, e.clientY);
// correct for scrolled document
point.x += document.body.scrollLeft +
document.documentElement.scrollLeft;
point.y += document.body.scrollTop +
document.documentElement.scrollTop;
// correct for nested offsets in DOM
for (var node = map.parent; node; node = node.offsetParent) {
point.x -= node.offsetLeft;
point.y -= node.offsetTop;
}
return point;
}
function mouseDown(e) {
if (!e.shiftKey) return;
corner = nearCorner = getMousePoint(e);
horizontal = vertical = true;
style.left = corner.x + 'px';
style.top = corner.y + 'px';
style.width = style.height = 0;
addEvent(document, 'mousemove', mouseMove);
addEvent(document, 'mouseup', mouseUp);
map.parent.style.cursor = 'crosshair';
return MM.cancelEvent(e);
}
// Resize existing box
function mouseDownResize(e) {
var point = getMousePoint(e),
TL = {
x: parseInt(boxDiv.offsetLeft, 10),
y: parseInt(boxDiv.offsetTop, 10)
},
BR = {
x: TL.x + parseInt(boxDiv.offsetWidth, 10),
y: TL.y + parseInt(boxDiv.offsetHeight, 10)
};
// Determine whether resize is horizontal, vertical or both
horizontal = point.x - TL.x <= edge || BR.x - point.x <= edge;
vertical = point.y - TL.y <= edge || BR.y - point.y <= edge;
if (vertical || horizontal) {
corner = {
x: (point.x - TL.x < BR.x - point.x) ? BR.x : TL.x,
y: (point.y - TL.y < BR.y - point.y) ? BR.y : TL.y
};
nearCorner = {
x: (point.x - TL.x < BR.x - point.x) ? TL.x : BR.x,
y: (point.y - TL.y < BR.y - point.y) ? TL.y : BR.y
};
addEvent(document, 'mousemove', mouseMove);
addEvent(document, 'mouseup', mouseUp);
return MM.cancelEvent(e);
}
}
function mouseMove(e) {
var point = getMousePoint(e);
style.display = 'block';
if (horizontal) {
style.left = (point.x < corner.x ? point.x : corner.x) + 'px';
style.width = Math.abs(point.x - corner.x) - 2 * borderWidth + 'px';
}
if (vertical) {
style.top = (point.y < corner.y ? point.y : corner.y) + 'px';
style.height = Math.abs(point.y - corner.y) - 2 * borderWidth + 'px';
}
changeCursor(point, map.parent);
return MM.cancelEvent(e);
}
function mouseUp(e) {
var point = getMousePoint(e),
l1 = map.pointLocation( new MM.Point(
horizontal ? point.x : nearCorner.x,
vertical? point.y : nearCorner.y
));
l2 = map.pointLocation(corner);
// Format coordinates like mm.map.getExtent().
boxselector.extent([
new MM.Location(
Math.max(l1.lat, l2.lat),
Math.min(l1.lon, l2.lon)),
new MM.Location(
Math.min(l1.lat, l2.lat),
Math.max(l1.lon, l2.lon))
]);
removeEvent(document, 'mousemove', mouseMove);
removeEvent(document, 'mouseup', mouseUp);
map.parent.style.cursor = 'auto';
}
function mouseMoveCursor(e) {
changeCursor(getMousePoint(e), boxDiv);
}
// Set resize cursor if mouse is on edge
function changeCursor(point, elem) {
var TL = {
x: parseInt(boxDiv.offsetLeft, 10),
y: parseInt(boxDiv.offsetTop, 10)
},
BR = {
x: TL.x + parseInt(boxDiv.offsetWidth, 10),
y: TL.y + parseInt(boxDiv.offsetHeight, 10)
};
// Build cursor style string
var prefix = '';
if (point.y - TL.y <= edge) prefix = 'n';
else if (BR.y - point.y <= edge) prefix = 's';
if (point.x - TL.x <= edge) prefix += 'w';
else if (BR.x - point.x <= edge) prefix += 'e';
if (prefix !== '') prefix += '-resize';
elem.style.cursor = prefix;
}
function drawbox(map, e) {
if (!boxDiv || !box) return;
var br = map.locationPoint(box[1]),
tl = map.locationPoint(box[0]),
style = boxDiv.style;
style.display = 'block';
style.height = 'auto';
style.width = 'auto';
style.left = Math.max(0, tl.x) + 'px';
style.top = Math.max(0, tl.y) + 'px';
style.right = Math.max(0, map.dimensions.x - br.x) + 'px';
style.bottom = Math.max(0, map.dimensions.y - br.y) + 'px';
}
boxselector.addCallback = function(event, callback) {
callbackManager.addCallback(event, callback);
return boxselector;
};
boxselector.removeCallback = function(event, callback) {
callbackManager.removeCallback(event, callback);
return boxselector;
};
boxselector.extent = function(x, silent) {
if (!x) return box;
box = [
new MM.Location(
Math.max(x[0].lat, x[1].lat),
Math.min(x[0].lon, x[1].lon)),
new MM.Location(
Math.min(x[0].lat, x[1].lat),
Math.max(x[0].lon, x[1].lon))
];
drawbox(map);
if (!silent) callbackManager.dispatchCallback('change', box);
};
boxDiv = document.createElement('div');
boxDiv.className = 'boxselector-box';
style = boxDiv.style;
boxselector.add = function() {
boxDiv.id = map.parent.id + '-boxselector-box';
map.parent.appendChild(boxDiv);
borderWidth = parseInt(window.getComputedStyle(boxDiv).borderWidth, 10);
addEvent(map.parent, 'mousedown', mouseDown);
addEvent(boxDiv, 'mousedown', mouseDownResize);
addEvent(map.parent, 'mousemove', mouseMoveCursor);
map.addCallback('drawn', drawbox);
return boxselector;
};
boxselector.map = function(x) {
if (!arguments.length) return map;
map = x;
return boxselector;
};
boxselector.remove = function() {
map.parent.removeChild(boxDiv);
removeEvent(map.parent, 'mousedown', mouseDown);
removeEvent(boxDiv, 'mousedown', mouseDownResize);
removeEvent(map.parent, 'mousemove', mouseMoveCursor);
map.removeCallback('drawn', drawbox);
return boxselector;
};
return boxselector;
};
wax = wax || {};
wax.mm = wax.mm || {};
wax._ = {};
wax.mm.bwdetect = function(map, options) {
options = options || {};
var lowpng = options.png || '.png128',
lowjpg = options.jpg || '.jpg70',
bw = false;
wax._.bw_png = lowpng;
wax._.bw_jpg = lowjpg;
return wax.bwdetect(options, function(x) {
wax._.bw = !x;
for (var i = 0; i < map.layers.length; i++) {
if (map.getLayerAt(i).provider instanceof wax.mm.connector) {
map.getLayerAt(i).setProvider(map.getLayerAt(i).provider);
}
}
});
};
wax = wax || {};
wax.mm = wax.mm || {};
// Add zoom links, which can be styled as buttons, to a `modestmaps.Map`
// control. This function can be used chaining-style with other
// chaining-style controls.
wax.mm.fullscreen = function() {
// true: fullscreen
// false: minimized
var fullscreened = false,
fullscreen = {},
a = document.createElement('a'),
map,
body = document.body,
dimensions;
a.className = 'map-fullscreen';
a.href = '#fullscreen';
// a.innerHTML = 'fullscreen';
function click(e) {
if (e) e.stop();
if (fullscreened) {
fullscreen.original();
} else {
fullscreen.full();
}
}
fullscreen.map = function(x) {
if (!arguments.length) return map;
map = x;
return fullscreen;
};
// Modest Maps demands an absolute height & width, and doesn't auto-correct
// for changes, so here we save the original size of the element and
// restore to that size on exit from fullscreen.
fullscreen.add = function() {
bean.add(a, 'click', click);
map.parent.appendChild(a);
return fullscreen;
};
fullscreen.remove = function() {
bean.remove(a, 'click', click);
if (a.parentNode) a.parentNode.removeChild(a);
return fullscreen;
};
fullscreen.full = function() {
if (fullscreened) { return; } else { fullscreened = true; }
dimensions = map.dimensions;
map.parent.className += ' map-fullscreen-map';
body.className += ' map-fullscreen-view';
map.dimensions = { x: map.parent.offsetWidth, y: map.parent.offsetHeight };
map.draw();
return fullscreen;
};
fullscreen.original = function() {
if (!fullscreened) { return; } else { fullscreened = false; }
map.parent.className = map.parent.className.replace(' map-fullscreen-map', '');
body.className = body.className.replace(' map-fullscreen-view', '');
map.dimensions = dimensions;
map.draw();
return fullscreen;
};
fullscreen.fullscreen = function(x) {
if (!arguments.length) {
return fullscreened;
} else {
if (x && !fullscreened) {
fullscreen.full();
} else if (!x && fullscreened) {
fullscreen.original();
}
return fullscreen;
}
};
fullscreen.element = function() {
return a;
};
fullscreen.appendTo = function(elem) {
wax.u.$(elem).appendChild(a);
return fullscreen;
};
return fullscreen;
};
wax = wax || {};
wax.mm = wax.mm || {};
wax.mm.hash = function() {
var map;
var hash = wax.hash({
getCenterZoom: function() {
var center = map.getCenter(),
zoom = map.getZoom(),
precision = Math.max(
0,
Math.ceil(Math.log(zoom) / Math.LN2));
return [zoom.toFixed(2),
center.lat.toFixed(precision),
center.lon.toFixed(precision)
].join('/');
},
setCenterZoom: function setCenterZoom(args) {
map.setCenterZoom(
new MM.Location(args[1], args[2]),
args[0]);
},
bindChange: function(fn) {
map.addCallback('drawn', fn);
},
unbindChange: function(fn) {
map.removeCallback('drawn', fn);
}
});
hash.map = function(x) {
if (!arguments.length) return map;
map = x;
return hash;
};
return hash;
};
wax = wax || {};
wax.mm = wax.mm || {};
wax.mm.interaction = function() {
var dirty = false,
_grid,
map,
clearingEvents = ['zoomed', 'panned', 'centered',
'extentset', 'resized', 'drawn'];
function grid() {
if (!dirty && _grid !== undefined && _grid.length) {
return _grid;
} else {
var tiles;
for (var i = 0; i < map.getLayers().length; i++) {
var levels = map.getLayerAt(i).levels;
var zoomLayer = levels && levels[Math.round(map.zoom())];
if (zoomLayer !== undefined) {
tiles = map.getLayerAt(i).tileElementsInLevel(zoomLayer);
if (tiles.length) break;
}
}
_grid = (function(t) {
var o = [];
for (var key in t) {
if (t[key].parentNode === zoomLayer) {
var offset = wax.u.offset(t[key]);
o.push([
offset.top,
offset.left,
t[key]
]);
}
}
return o;
})(tiles);
return _grid;
}
}
function setdirty() { dirty = true; }
function attach(x) {
if (!arguments.length) return map;
map = x;
for (var i = 0; i < clearingEvents.length; i++) {
map.addCallback(clearingEvents[i], setdirty);
}
}
function detach(x) {
for (var i = 0; i < clearingEvents.length; i++) {
map.removeCallback(clearingEvents[i], setdirty);
}
}
return wax.interaction()
.attach(attach)
.detach(detach)
.parent(function() {
return map.parent;
})
.grid(grid);
};
wax = wax || {};
wax.mm = wax.mm || {};
wax.mm.legend = function() {
var map,
l = {};
var container = document.createElement('div');
container.className = 'wax-legends map-legends';
var element = container.appendChild(document.createElement('div'));
element.className = 'wax-legend map-legend';
element.style.display = 'none';
l.content = function(x) {
if (!arguments.length) return element.innerHTML;
element.innerHTML = wax.u.sanitize(x);
element.style.display = 'block';
if (element.innerHTML === '') {
element.style.display = 'none';
}
return l;
};
l.element = function() {
return container;
};
l.map = function(x) {
if (!arguments.length) return map;
map = x;
return l;
};
l.add = function() {
if (!map) return false;
l.appendTo(map.parent);
return l;
};
l.remove = function() {
if (container.parentNode) {
container.parentNode.removeChild(container);
}
return l;
};
l.appendTo = function(elem) {
wax.u.$(elem).appendChild(container);
return l;
};
return l;
};
wax = wax || {};
wax.mm = wax.mm || {};
// This takes an object of options:
//
// * `callback`: a function called with an array of `com.modestmaps.Location`
// objects when the map is edited
//
// It also exposes a public API function: `addLocation`, which adds a point
// to the map as if added by the user.
wax.mm.pointselector = function() {
var map,
mouseDownPoint = null,
mouseUpPoint = null,
callback = null,
tolerance = 5,
overlayDiv,
pointselector = {},
callbackManager = new MM.CallbackManager(pointselector, ['change']),
locations = [];
// Create a `MM.Point` from a screen event, like a click.
function makePoint(e) {
var coords = wax.u.eventoffset(e);
var point = new MM.Point(coords.x, coords.y);
// correct for scrolled document
// and for the document
var body = {
x: parseFloat(MM.getStyle(document.documentElement, 'margin-left')),
y: parseFloat(MM.getStyle(document.documentElement, 'margin-top'))
};
if (!isNaN(body.x)) point.x -= body.x;
if (!isNaN(body.y)) point.y -= body.y;
// TODO: use wax.util.offset
// correct for nested offsets in DOM
for (var node = map.parent; node; node = node.offsetParent) {
point.x -= node.offsetLeft;
point.y -= node.offsetTop;
}
return point;
}
// Currently locations in this control contain circular references to elements.
// These can't be JSON encoded, so here's a utility to clean the data that's
// spit back.
function cleanLocations(locations) {
var o = [];
for (var i = 0; i < locations.length; i++) {
o.push(new MM.Location(locations[i].lat, locations[i].lon));
}
return o;
}
// Attach this control to a map by registering callbacks
// and adding the overlay
// Redraw the points when the map is moved, so that they stay in the
// correct geographic locations.
function drawPoints() {
var offset = new MM.Point(0, 0);
for (var i = 0; i < locations.length; i++) {
var point = map.locationPoint(locations[i]);
if (!locations[i].pointDiv) {
locations[i].pointDiv = document.createElement('div');
locations[i].pointDiv.className = 'map-point-div';
locations[i].pointDiv.style.position = 'absolute';
locations[i].pointDiv.style.display = 'block';
// TODO: avoid circular reference
locations[i].pointDiv.location = locations[i];
// Create this closure once per point
bean.add(locations[i].pointDiv, 'mouseup',
(function selectPointWrap(e) {
var l = locations[i];
return function(e) {
MM.removeEvent(map.parent, 'mouseup', mouseUp);
pointselector.deleteLocation(l, e);
};
})());
map.parent.appendChild(locations[i].pointDiv);
}
locations[i].pointDiv.style.left = point.x + 'px';
locations[i].pointDiv.style.top = point.y + 'px';
}
}
function mouseDown(e) {
mouseDownPoint = makePoint(e);
bean.add(map.parent, 'mouseup', mouseUp);
}
// Remove the awful circular reference from locations.
// TODO: This function should be made unnecessary by not having it.
function mouseUp(e) {
if (!mouseDownPoint) return;
mouseUpPoint = makePoint(e);
if (MM.Point.distance(mouseDownPoint, mouseUpPoint) < tolerance) {
pointselector.addLocation(map.pointLocation(mouseDownPoint));
callbackManager.dispatchCallback('change', cleanLocations(locations));
}
mouseDownPoint = null;
}
// API for programmatically adding points to the map - this
// calls the callback for ever point added, so it can be symmetrical.
// Useful for initializing the map when it's a part of a form.
pointselector.addLocation = function(location) {
locations.push(location);
drawPoints();
callbackManager.dispatchCallback('change', cleanLocations(locations));
return pointselector;
};
// TODO set locations
pointselector.locations = function() {
if (!arguments.length) return locations;
};
pointselector.addCallback = function(event, callback) {
callbackManager.addCallback(event, callback);
return pointselector;
};
pointselector.removeCallback = function(event, callback) {
callbackManager.removeCallback(event, callback);
return pointselector;
};
pointselector.map = function(x) {
if (!arguments.length) return map;
map = x;
return pointselector;
};
pointselector.add = function() {
bean.add(map.parent, 'mousedown', mouseDown);
map.addCallback('drawn', drawPoints);
return pointselector;
};
pointselector.remove = function() {
bean.remove(map.parent, 'mousedown', mouseDown);
map.removeCallback('drawn', drawPoints);
for (var i = locations.length - 1; i > -1; i--) {
pointselector.deleteLocation(locations[i]);
}
return pointselector;
};
pointselector.deleteLocation = function(location, e) {
if (!e || confirm('Delete this point?')) {
location.pointDiv.parentNode.removeChild(location.pointDiv);
for (var i = 0; i < locations.length; i++) {
if (locations[i] === location) {
locations.splice(i, 1);
break;
}
}
callbackManager.dispatchCallback('change', cleanLocations(locations));
}
};
return pointselector;
};
wax = wax || {};
wax.mm = wax.mm || {};
wax.mm.zoombox = function() {
// TODO: respond to resize
var zoombox = {},
map,
drawing = false,
box = document.createElement('div'),
mouseDownPoint = null;
function getMousePoint(e) {
// start with just the mouse (x, y)
var point = new MM.Point(e.clientX, e.clientY);
// correct for scrolled document
point.x += document.body.scrollLeft + document.documentElement.scrollLeft;
point.y += document.body.scrollTop + document.documentElement.scrollTop;
// correct for nested offsets in DOM
for (var node = map.parent; node; node = node.offsetParent) {
point.x -= node.offsetLeft;
point.y -= node.offsetTop;
}
return point;
}
function mouseUp(e) {
if (!drawing) return;
drawing = false;
var point = getMousePoint(e);
var l1 = map.pointLocation(point),
l2 = map.pointLocation(mouseDownPoint);
map.setExtent([l1, l2]);
box.style.display = 'none';
MM.removeEvent(map.parent, 'mousemove', mouseMove);
MM.removeEvent(map.parent, 'mouseup', mouseUp);
map.parent.style.cursor = 'auto';
}
function mouseDown(e) {
if (!(e.shiftKey && !this.drawing)) return;
drawing = true;
mouseDownPoint = getMousePoint(e);
box.style.left = mouseDownPoint.x + 'px';
box.style.top = mouseDownPoint.y + 'px';
MM.addEvent(map.parent, 'mousemove', mouseMove);
MM.addEvent(map.parent, 'mouseup', mouseUp);
map.parent.style.cursor = 'crosshair';
return MM.cancelEvent(e);
}
function mouseMove(e) {
if (!drawing) return;
var point = getMousePoint(e);
box.style.display = 'block';
if (point.x < mouseDownPoint.x) {
box.style.left = point.x + 'px';
} else {
box.style.left = mouseDownPoint.x + 'px';
}
box.style.width = Math.abs(point.x - mouseDownPoint.x) + 'px';
if (point.y < mouseDownPoint.y) {
box.style.top = point.y + 'px';
} else {
box.style.top = mouseDownPoint.y + 'px';
}
box.style.height = Math.abs(point.y - mouseDownPoint.y) + 'px';
return MM.cancelEvent(e);
}
zoombox.map = function(x) {
if (!arguments.length) return map;
map = x;
return zoombox;
};
zoombox.add = function() {
if (!map) return false;
// Use a flag to determine whether the zoombox is currently being
// drawn. Necessary only for IE because `mousedown` is triggered
// twice.
box.id = map.parent.id + '-zoombox-box';
box.className = 'zoombox-box';
map.parent.appendChild(box);
MM.addEvent(map.parent, 'mousedown', mouseDown);
return this;
};
zoombox.remove = function() {
if (!map) return false;
if (box.parentNode) box.parentNode.removeChild(box);
MM.removeEvent(map.parent, 'mousedown', mouseDown);
return zoombox;
};
return zoombox;
};
wax = wax || {};
wax.mm = wax.mm || {};
wax.mm.zoomer = function() {
var zoomer = {},
smooth = true,
map;
var zoomin = document.createElement('a'),
zoomout = document.createElement('a');
function stopEvents(e) {
e.stop();
}
function zIn(e) {
e.stop();
if (smooth && map.ease) {
map.ease.zoom(map.zoom() + 1).run(50);
} else {
map.zoomIn();
}
}
function zOut(e) {
e.stop();
if (smooth && map.ease) {
map.ease.zoom(map.zoom() - 1).run(50);
} else {
map.zoomOut();
}
}
zoomin.innerHTML = '+';
zoomin.href = '#';
zoomin.className = 'zoomer zoomin';
zoomout.innerHTML = '-';
zoomout.href = '#';
zoomout.className = 'zoomer zoomout';
function updateButtons(map, e) {
if (map.coordinate.zoom === map.coordLimits[0].zoom) {
zoomout.className = 'zoomer zoomout zoomdisabled';
} else if (map.coordinate.zoom === map.coordLimits[1].zoom) {
zoomin.className = 'zoomer zoomin zoomdisabled';
} else {
zoomin.className = 'zoomer zoomin';
zoomout.className = 'zoomer zoomout';
}
}
zoomer.map = function(x) {
if (!arguments.length) return map;
map = x;
return zoomer;
};
zoomer.add = function() {
if (!map) return false;
map.addCallback('drawn', updateButtons);
zoomer.appendTo(map.parent);
bean.add(zoomin, 'mousedown dblclick', stopEvents);
bean.add(zoomout, 'mousedown dblclick', stopEvents);
bean.add(zoomout, 'touchstart click', zOut);
bean.add(zoomin, 'touchstart click', zIn);
return zoomer;
};
zoomer.remove = function() {
if (!map) return false;
map.removeCallback('drawn', updateButtons);
if (zoomin.parentNode) zoomin.parentNode.removeChild(zoomin);
if (zoomout.parentNode) zoomout.parentNode.removeChild(zoomout);
bean.remove(zoomin, 'mousedown dblclick', stopEvents);
bean.remove(zoomout, 'mousedown dblclick', stopEvents);
bean.remove(zoomout, 'touchstart click', zOut);
bean.remove(zoomin, 'touchstart click', zIn);
return zoomer;
};
zoomer.appendTo = function(elem) {
wax.u.$(elem).appendChild(zoomin);
wax.u.$(elem).appendChild(zoomout);
return zoomer;
};
zoomer.smooth = function(x) {
if (!arguments.length) return smooth;
smooth = x;
return zoomer;
};
return zoomer;
};
var wax = wax || {};
wax.mm = wax.mm || {};
// A layer connector for Modest Maps conformant to TileJSON
// https://github.com/mapbox/tilejson
wax.mm._provider = function(options) {
this.options = {
tiles: options.tiles,
scheme: options.scheme || 'xyz',
minzoom: options.minzoom || 0,
maxzoom: options.maxzoom || 22,
bounds: options.bounds || [-180, -90, 180, 90]
};
};
wax.mm._provider.prototype = {
outerLimits: function() {
return [
this.locationCoordinate(
new MM.Location(
this.options.bounds[0],
this.options.bounds[1])).zoomTo(this.options.minzoom),
this.locationCoordinate(
new MM.Location(
this.options.bounds[2],
this.options.bounds[3])).zoomTo(this.options.maxzoom)
];
},
getTile: function(c) {
var coord;
if (!(coord = this.sourceCoordinate(c))) return null;
if (coord.zoom < this.options.minzoom || coord.zoom > this.options.maxzoom) return null;
coord.row = (this.options.scheme === 'tms') ?
Math.pow(2, coord.zoom) - coord.row - 1 :
coord.row;
var u = this.options.tiles[parseInt(Math.pow(2, coord.zoom) * coord.row + coord.column, 10) %
this.options.tiles.length]
.replace('{z}', coord.zoom.toFixed(0))
.replace('{x}', coord.column.toFixed(0))
.replace('{y}', coord.row.toFixed(0));
if (wax._ && wax._.bw) {
u = u.replace('.png', wax._.bw_png)
.replace('.jpg', wax._.bw_jpg);
}
return u;
}
};
if (MM) {
MM.extend(wax.mm._provider, MM.MapProvider);
}
wax.mm.connector = function(options) {
var x = new wax.mm._provider(options);
return new MM.Layer(x);
};
;(function(context, MM) {
var easey = function() {
var easey = {},
running = false,
abort = false, // killswitch for transitions
abortCallback; // callback called when aborted
var easings = {
easeIn: function(t) { return t * t; },
easeOut: function(t) { return Math.sin(t * Math.PI / 2); },
easeInOut: function(t) { return (1 - Math.cos(Math.PI * t)) / 2; },
linear: function(t) { return t; }
};
var easing = easings.easeOut;
// to is the singular coordinate that any transition is based off
// three dimensions:
//
// * to
// * time
// * path
var from, to, map;
easey.stop = function(callback) {
abort = true;
from = undefined;
abortCallback = callback;
};
easey.running = function() {
return running;
};
easey.point = function(x) {
to = map.pointCoordinate(x);
return easey;
};
easey.zoom = function(x) {
if (!to) to = map.coordinate.copy();
to = map.enforceZoomLimits(to.zoomTo(x));
return easey;
};
easey.location = function(x) {
to = map.locationCoordinate(x);
return easey;
};
easey.from = function(x) {
if (!arguments.length) return from ? from.copy() : from;
from = x.copy();
return easey;
};
easey.to = function(x) {
if (!arguments.length) return to.copy();
to = map.enforceZoomLimits(x.copy());
return easey;
};
easey.path = function(x) {
path = paths[x];
return easey;
};
easey.easing = function(x) {
easing = easings[x];
return easey;
};
easey.map = function(x) {
if (!arguments.length) return map;
map = x;
return easey;
};
function interp(a, b, p) {
if (p === 0) return a;
if (p === 1) return b;
return a + ((b - a) * p);
}
var paths = {},
static_coord = new MM.Coordinate(0, 0, 0);
// The screen path simply moves between
// coordinates in a non-geographical way
paths.screen = function(a, b, t, static_coord) {
var zoom_lerp = interp(a.zoom, b.zoom, t);
if (static_coord) {
static_coord.row = interp(
a.row,
b.row * Math.pow(2, a.zoom - b.zoom),
t) * Math.pow(2, zoom_lerp - a.zoom);
static_coord.column = interp(
a.column,
b.column * Math.pow(2, a.zoom - b.zoom),
t) * Math.pow(2, zoom_lerp - a.zoom);
static_coord.zoom = zoom_lerp;
} else {
return new MM.Coordinate(
interp(a.row,
b.row * Math.pow(2, a.zoom - b.zoom),
t) * Math.pow(2, zoom_lerp - a.zoom),
interp(a.column,
b.column * Math.pow(2, a.zoom - b.zoom),
t) * Math.pow(2, zoom_lerp - a.zoom),
zoom_lerp);
}
};
// The screen path means that the b
// coordinate should maintain its point on screen
// throughout the transition, but the map
// should move to its zoom level
paths.about = function(a, b, t, static_coord) {
var zoom_lerp = interp(a.zoom, b.zoom, t);
// center x, center y
var cx = map.dimensions.x / 2,
cy = map.dimensions.y / 2,
// tilesize
tx = map.tileSize.x,
ty = map.tileSize.y;
var startx = cx + tx * ((b.column * Math.pow(2, a.zoom - b.zoom)) - a.column);
var starty = cy + ty * ((b.row * Math.pow(2, a.zoom - b.zoom)) - a.row);
var endx = cx + tx * ((b.column * Math.pow(2, zoom_lerp - b.zoom)) -
(a.column * Math.pow(2, zoom_lerp - a.zoom)));
var endy = cy + ty * ((b.row * Math.pow(2, zoom_lerp - b.zoom)) - (a.row *
Math.pow(2, zoom_lerp - a.zoom)));
if (static_coord) {
static_coord.column = (a.column * Math.pow(2, zoom_lerp - a.zoom)) - ((startx - endx) / tx);
static_coord.row = (a.row * Math.pow(2, zoom_lerp - a.zoom)) - ((starty - endy) / ty);
static_coord.zoom = zoom_lerp;
} else {
return new MM.Coordinate(
(a.column * Math.pow(2, zoom_lerp - a.zoom)) - ((startx - endx) / tx),
(a.row * Math.pow(2, zoom_lerp - a.zoom)) - ((starty - endy) / ty),
zoom_lerp);
}
};
var path = paths.screen;
easey.t = function(t) {
path(from, to, easing(t), static_coord);
map.coordinate = static_coord;
map.draw();
return easey;
};
easey.future = function(parts) {
var futures = [];
for (var t = 0; t < parts; t++) {
futures.push(path(from, to, t / (parts - 1)));
}
return futures;
};
var start;
easey.resetRun = function () {
start = (+ new Date());
return easey;
};
easey.run = function(time, callback) {
if (running) return easey.stop(function() {
easey.run(time, callback);
});
if (!from) from = map.coordinate.copy();
if (!to) to = map.coordinate.copy();
time = time || 1000;
start = (+new Date());
running = true;
function tick() {
var delta = (+new Date()) - start;
if (abort) {
abort = running = false;
abortCallback();
return (abortCallback = undefined);
} else if (delta > time) {
if (to.zoom != from.zoom) map.dispatchCallback('zoomed', to.zoom - from.zoom);
running = false;
path(from, to, 1, static_coord);
map.coordinate = static_coord;
to = from = undefined;
map.draw();
if (callback) return callback(map);
} else {
path(from, to, easing(delta / time), static_coord);
map.coordinate = static_coord;
map.draw();
MM.getFrame(tick);
}
}
MM.getFrame(tick);
};
// Optimally smooth (constant perceived velocity) and
// efficient (minimal path distance) zooming and panning.
//
// Based on "Smooth and efficient zooming and panning"
// by Jarke J. van Wijk and Wim A.A. Nuij
//
// Model described in section 3, equations 1 through 5
// Derived equation (9) of optimal path implemented below
easey.optimal = function(V, rho, callback) {
if (running) return easey.stop(function() {
easey.optimal(V, rho, callback);
});
// Section 6 describes user testing of these tunable values
V = V || 0.9;
rho = rho || 1.42;
function sqr(n) { return n*n; }
function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
function tanh(n) { return sinh(n) / cosh(n); }
if (from) map.coordinate = from; // For when `from` not current coordinate
else from = map.coordinate.copy();
// Width is measured in coordinate units at zoom 0
var TL = map.pointCoordinate(new MM.Point(0, 0)).zoomTo(0),
BR = map.pointCoordinate(map.dimensions).zoomTo(0),
w0 = Math.max(BR.column - TL.column, BR.row - TL.row),
w1 = w0 * Math.pow(2, from.zoom - to.zoom),
start = from.zoomTo(0),
end = to.zoomTo(0),
c0 = {x: start.column, y: start.row},
c1 = {x: end.column, y: end.row},
u0 = 0,
u1 = Math.sqrt(sqr(c1.x - c0.x) + sqr(c1.y - c0.y));
function b(i) {
var n = sqr(w1) - sqr(w0) + (i ? -1: 1) * Math.pow(rho, 4) * sqr(u1 - u0),
d = 2 * (i ? w1 : w0) * sqr(rho) * (u1 - u0);
return n/d;
}
function r(i) {
return Math.log(-b(i) + Math.sqrt(sqr(b(i)) + 1));
}
var r0 = r(0),
r1 = r(1),
S = (r1 - r0) / rho;
// Width
var w = function(s) {
return w0 * cosh(r0) / cosh (rho * s + r0);
};
// Zoom
var u = function(s) {
return (w0 / sqr(rho)) * cosh(r0) * tanh(rho * s + r0) - (w0 / sqr(rho)) * sinh(r0) + u0;
};
// Special case, when no panning necessary
if (Math.abs(u1) < 0.000001) {
if (Math.abs(w0 - w1) < 0.000001) return;
// Based on section 4
var k = w1 < w0 ? -1 : 1;
S = Math.abs(Math.log(w1/w0)) / rho;
u = function(s) {
return u0;
};
w = function(s) {
return w0 * Math.exp(k * rho * s);
};
}
var oldpath = path;
path = function (a, b, t, static_coord) {
if (t == 1) {
if (static_coord) {
static_coord.row = to.row;
static_coord.column = to.column;
static_coord.zoom = to.zoom;
}
return to;
}
var s = t * S,
us = u(s),
z = a.zoom + (Math.log(w0/w(s)) / Math.LN2),
x = interp(c0.x, c1.x, us/u1 || 1),
y = interp(c0.y, c1.y, us/u1 || 1);
var power = Math.pow(2, z);
if (static_coord) {
static_coord.row = y * power;
static_coord.column = x * power;
static_coord.zoom = z;
} else {
return new MM.Coordinate(y * power, x * power, z);
}
};
easey.run(S / V * 1000, function(m) {
path = oldpath;
if (callback) callback(m);
});
};
return easey;
};
this.easey = easey;
if (typeof this.mapbox == 'undefined') this.mapbox = {};
this.mapbox.ease = easey;
})(this, MM);
;(function(context, MM) {
var easey_handlers = {};
easey_handlers.TouchHandler = function() {
var handler = {},
map,
panner,
maxTapTime = 250,
maxTapDistance = 30,
maxDoubleTapDelay = 350,
locations = {},
taps = [],
wasPinching = false,
lastPinchCenter = null,
p0 = new MM.Point(0, 0),
p1 = new MM.Point(0, 0);
function focusMap(e) {
map.parent.focus();
}
function clearLocations() {
for (var loc in locations) {
if (locations.hasOwnProperty(loc)) {
delete locations[loc];
}
}
}
function updateTouches (e) {
for (var i = 0; i < e.touches.length; i += 1) {
var t = e.touches[i];
if (t.identifier in locations) {
var l = locations[t.identifier];
l.x = t.clientX;
l.y = t.clientY;
l.scale = e.scale;
} else {
locations[t.identifier] = {
scale: e.scale,
startPos: { x: t.clientX, y: t.screenY },
startZoom: map.zoom(),
x: t.clientX,
y: t.clientY,
time: new Date().getTime()
};
}
}
}
function touchStartMachine(e) {
if (!panner) panner = panning(map, 0.10);
MM.addEvent(e.touches[0].target, 'touchmove',
touchMoveMachine);
MM.addEvent(e.touches[0].target, 'touchend',
touchEndMachine);
if (e.touches[1]) {
MM.addEvent(e.touches[1].target, 'touchmove',
touchMoveMachine);
MM.addEvent(e.touches[1].target, 'touchend',
touchEndMachine);
}
updateTouches(e);
panner.down(e.touches[0]);
return MM.cancelEvent(e);
}
function touchMoveMachine(e) {
switch (e.touches.length) {
case 1:
panner.move(e.touches[0]);
break;
case 2:
onPinching(e);
break;
}
updateTouches(e);
return MM.cancelEvent(e);
}
// Handle a tap event - mainly watch for a doubleTap
function onTap(tap) {
if (taps.length &&
(tap.time - taps[0].time) < maxDoubleTapDelay) {
onDoubleTap(tap);
taps = [];
return;
}
taps = [tap];
}
// Handle a double tap by zooming in a single zoom level to a
// round zoom.
function onDoubleTap(tap) {
// zoom in to a round number
easey().map(map)
.to(map.pointCoordinate(tap).zoomTo(map.getZoom() + 1))
.path('about').run(200, function() {
map.dispatchCallback('zoomed');
clearLocations();
});
}
function onPinching(e) {
// use the first two touches and their previous positions
var t0 = e.touches[0],
t1 = e.touches[1];
p0.x = t0.clientX;
p0.y = t0.clientY;
p1.x = t1.clientX;
p1.y = t1.clientY;
l0 = locations[t0.identifier],
l1 = locations[t1.identifier];
// mark these touches so they aren't used as taps/holds
l0.wasPinch = true;
l1.wasPinch = true;
// scale about the center of these touches
var center = MM.Point.interpolate(p0, p1, 0.5);
map.zoomByAbout(
Math.log(e.scale) / Math.LN2 - Math.log(l0.scale) / Math.LN2,
center);
// pan from the previous center of these touches
prevX = l0.x + (l1.x - l0.x) * 0.5;
prevY = l0.y + (l1.y - l0.y) * 0.5;
map.panBy(center.x - prevX,
center.y - prevY);
wasPinching = true;
lastPinchCenter = center;
}
// When a pinch event ends, round the zoom of the map.
function onPinched(touch) {
var z = map.getZoom(), // current zoom
tz = locations[touch.identifier].startZoom > z ? Math.floor(z) : Math.ceil(z);
easey().map(map).point(lastPinchCenter).zoom(tz)
.path('about').run(300);
clearLocations();
wasPinching = false;
}
function touchEndMachine(e) {
MM.removeEvent(e.target, 'touchmove',
touchMoveMachine);
MM.removeEvent(e.target, 'touchend',
touchEndMachine);
var now = new Date().getTime();
// round zoom if we're done pinching
if (e.touches.length === 0 && wasPinching) {
onPinched(e.changedTouches[0]);
}
panner.up();
// Look at each changed touch in turn.
for (var i = 0; i < e.changedTouches.length; i += 1) {
var t = e.changedTouches[i],
loc = locations[t.identifier];
// if we didn't see this one (bug?)
// or if it was consumed by pinching already
// just skip to the next one
if (!loc || loc.wasPinch) {
continue;
}
// we now know we have an event object and a
// matching touch that's just ended. Let's see
// what kind of event it is based on how long it
// lasted and how far it moved.
var pos = { x: t.clientX, y: t.clientY },
time = now - loc.time,
travel = MM.Point.distance(pos, loc.startPos);
if (travel > maxTapDistance) {
// we will to assume that the drag has been handled separately
} else if (time > maxTapTime) {
// close in space, but not in time: a hold
pos.end = now;
pos.duration = time;
} else {
// close in both time and space: a tap
pos.time = now;
onTap(pos);
}
}
// Weird, sometimes an end event doesn't get thrown
// for a touch that nevertheless has disappeared.
// Still, this will eventually catch those ids:
var validTouchIds = {};
for (var j = 0; j < e.touches.length; j++) {
validTouchIds[e.touches[j].identifier] = true;
}
for (var id in locations) {
if (!(id in validTouchIds)) {
delete validTouchIds[id];
}
}
return MM.cancelEvent(e);
}
handler.init = function(x) {
map = x;
MM.addEvent(map.parent, 'touchstart',
touchStartMachine);
};
handler.remove = function() {
if (!panner) return;
MM.removeEvent(map.parent, 'touchstart',
touchStartMachine);
panner.remove();
};
return handler;
};
easey_handlers.DoubleClickHandler = function() {
var handler = {},
map;
function doubleClick(e) {
// Ensure that this handler is attached once.
// Get the point on the map that was double-clicked
var point = MM.getMousePoint(e, map);
z = map.getZoom() + (e.shiftKey ? -1 : 1);
// use shift-double-click to zoom out
easey().map(map)
.to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z))
.path('about').run(100, function() {
map.dispatchCallback('zoomed');
});
return MM.cancelEvent(e);
}
handler.init = function(x) {
map = x;
MM.addEvent(map.parent, 'dblclick', doubleClick);
return handler;
};
handler.remove = function() {
MM.removeEvent(map.parent, 'dblclick', doubleClick);
};
return handler;
};
easey_handlers.MouseWheelHandler = function() {
var handler = {},
map,
_zoomDiv,
ea = easey(),
prevTime,
precise = false;
function mouseWheel(e) {
var delta = 0;
prevTime = prevTime || new Date().getTime();
try {
_zoomDiv.scrollTop = 1000;
_zoomDiv.dispatchEvent(e);
delta = 1000 - _zoomDiv.scrollTop;
} catch (error) {
delta = e.wheelDelta || (-e.detail * 5);
}
// limit mousewheeling to once every 200ms
var timeSince = new Date().getTime() - prevTime;
function dispatchZoomed() {
map.dispatchCallback('zoomed');
}
if (!ea.running()) {
var point = MM.getMousePoint(e, map),
z = map.getZoom();
ea.map(map)
.easing('easeOut')
.to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z + (delta > 0 ? 1 : -1)))
.path('about').run(100, dispatchZoomed);
prevTime = new Date().getTime();
} else if (timeSince > 150){
ea.zoom(ea.to().zoom + (delta > 0 ? 1 : -1)).from(map.coordinate).resetRun();
prevTime = new Date().getTime();
}
// Cancel the event so that the page doesn't scroll
return MM.cancelEvent(e);
}
handler.init = function(x) {
map = x;
_zoomDiv = document.body.appendChild(document.createElement('div'));
_zoomDiv.style.cssText = 'visibility:hidden;top:0;height:0;width:0;overflow-y:scroll';
var innerDiv = _zoomDiv.appendChild(document.createElement('div'));
innerDiv.style.height = '2000px';
MM.addEvent(map.parent, 'mousewheel', mouseWheel);
return handler;
};
handler.precise = function(x) {
if (!arguments.length) return precise;
precise = x;
return handler;
};
handler.remove = function() {
MM.removeEvent(map.parent, 'mousewheel', mouseWheel);
_zoomDiv.parentNode.removeChild(_zoomDiv);
};
return handler;
};
easey_handlers.DragHandler = function() {
var handler = {},
map,
panner;
function focusMap(e) {
map.parent.focus();
}
function mouseDown(e) {
if (e.shiftKey || e.button == 2) return;
MM.addEvent(document, 'mousemove', mouseMove);
MM.addEvent(document, 'mouseup', mouseUp);
panner.down(e);
map.parent.style.cursor = 'move';
return MM.cancelEvent(e);
}
function mouseMove(e) {
panner.move(e);
return MM.cancelEvent(e);
}
function mouseUp(e) {
MM.removeEvent(document, 'mousemove', mouseMove);
MM.removeEvent(document, 'mouseup', mouseUp);
panner.up();
map.parent.style.cursor = '';
return MM.cancelEvent(e);
}
handler.init = function(x) {
map = x;
MM.addEvent(map.parent, 'click', focusMap);
MM.addEvent(map.parent, 'mousedown', mouseDown);
panner = panning(map);
};
handler.remove = function() {
MM.removeEvent(map.parent, 'click', focusMap);
MM.removeEvent(map.parent, 'mousedown', mouseDown);
panner.up();
panner.remove();
};
return handler;
};
function panning(map, drag) {
var p = {};
drag = drag || 0.15;
var speed = { x: 0, y: 0 },
dir = { x: 0, y: 0 },
removed = false,
nowPoint = null,
oldPoint = null,
moveTime = null,
prevMoveTime = null,
animatedLastPoint = true,
t,
prevT = new Date().getTime();
p.down = function(e) {
nowPoint = oldPoint = MM.getMousePoint(e, map);
moveTime = prevMoveTime = +new Date();
};
p.move = function(e) {
if (nowPoint) {
if (animatedLastPoint) {
oldPoint = nowPoint;
prevMoveTime = moveTime;
animatedLastPoint = false;
}
nowPoint = MM.getMousePoint(e, map);
moveTime = +new Date();
}
};
p.up = function() {
if (+new Date() - prevMoveTime < 50) {
dt = Math.max(1, moveTime - prevMoveTime);
dir.x = nowPoint.x - oldPoint.x;
dir.y = nowPoint.y - oldPoint.y;
speed.x = dir.x / dt;
speed.y = dir.y / dt;
} else {
speed.x = 0;
speed.y = 0;
}
nowPoint = oldPoint = null;
moveTime = null;
};
p.remove = function() {
removed = true;
};
function animate(t) {
var dt = Math.max(1, t - prevT);
if (nowPoint && oldPoint) {
if (!animatedLastPoint) {
dir.x = nowPoint.x - oldPoint.x;
dir.y = nowPoint.y - oldPoint.y;
map.panBy(dir.x, dir.y);
animatedLastPoint = true;
}
} else {
// Rough time based animation accuracy
// using a linear approximation approach
speed.x *= Math.pow(1 - drag, dt * 60 / 1000);
speed.y *= Math.pow(1 - drag, dt * 60 / 1000);
if (Math.abs(speed.x) < 0.001) {
speed.x = 0;
}
if (Math.abs(speed.y) < 0.001) {
speed.y = 0;
}
if (speed.x || speed.y) {
map.panBy(speed.x * dt, speed.y * dt);
}
}
prevT = t;
if (!removed) MM.getFrame(animate);
}
MM.getFrame(animate);
return p;
}
this.easey_handlers = easey_handlers;
})(this, MM);
if (typeof mapbox == 'undefined') mapbox = {};
if (typeof mapbox.markers == 'undefined') mapbox.markers = {};
mapbox.markers.layer = function() {
var m = {},
// external list of geojson features
features = [],
// internal list of markers
markers = [],
// internal list of callbacks
callbackManager = new MM.CallbackManager(m, ['drawn', 'markeradded']),
// the absolute position of the parent element
position = null,
// a factory function for creating DOM elements out of
// GeoJSON objects
factory = mapbox.markers.simplestyle_factory,
// a sorter function for sorting GeoJSON objects
// in the DOM
sorter = function(a, b) {
return b.geometry.coordinates[1] -
a.geometry.coordinates[1];
},
// a list of urls from which features can be loaded.
// these can be templated with {z}, {x}, and {y}
urls,
// map bounds
left = null,
right = null,
// a function that filters points
filter = function() {
return true;
},
_seq = 0,
keyfn = function() {
return ++_seq;
},
index = {};
// The parent DOM element
m.parent = document.createElement('div');
m.parent.style.cssText = 'position: absolute; top: 0px;' +
'left:0px; width:100%; height:100%; margin:0; padding:0; z-index:0;pointer-events:none;';
m.name = 'markers';
// reposition a single marker element
function reposition(marker) {
// remember the tile coordinate so we don't have to reproject every time
if (!marker.coord) marker.coord = m.map.locationCoordinate(marker.location);
var pos = m.map.coordinatePoint(marker.coord);
var pos_loc, new_pos;
// If this point has wound around the world, adjust its position
// to the new, onscreen location
if (pos.x < 0) {
pos_loc = new MM.Location(marker.location.lat, marker.location.lon);
pos_loc.lon += Math.ceil((left.lon - marker.location.lon) / 360) * 360;
new_pos = m.map.locationPoint(pos_loc);
if (new_pos.x < m.map.dimensions.x) {
pos = new_pos;
marker.coord = m.map.locationCoordinate(pos_loc);
}
} else if (pos.x > m.map.dimensions.x) {
pos_loc = new MM.Location(marker.location.lat, marker.location.lon);
pos_loc.lon -= Math.ceil((marker.location.lon - right.lon) / 360) * 360;
new_pos = m.map.locationPoint(pos_loc);
if (new_pos.x > 0) {
pos = new_pos;
marker.coord = m.map.locationCoordinate(pos_loc);
}
}
pos.scale = 1;
pos.width = pos.height = 0;
MM.moveElement(marker.element, pos);
}
// Adding and removing callbacks is mainly a way to enable mmg_interaction to operate.
// I think there are better ways to do this, by, for instance, having mmg be able to
// register 'binders' to markers, but this is backwards-compatible and equivalent
// externally.
m.addCallback = function(event, callback) {
callbackManager.addCallback(event, callback);
return m;
};
m.removeCallback = function(event, callback) {
callbackManager.removeCallback(event, callback);
return m;
};
// Draw this layer - reposition all markers on the div. This requires
// the markers library to be attached to a map, and will noop otherwise.
m.draw = function() {
if (!m.map) return;
left = m.map.pointLocation(new MM.Point(0, 0));
right = m.map.pointLocation(new MM.Point(m.map.dimensions.x, 0));
callbackManager.dispatchCallback('drawn', m);
for (var i = 0; i < markers.length; i++) {
reposition(markers[i]);
}
};
// Add a fully-formed marker to the layer. This fires a `markeradded` event.
// This does not require the map element t be attached.
m.add = function(marker) {
if (!marker || !marker.element) return null;
m.parent.appendChild(marker.element);
markers.push(marker);
callbackManager.dispatchCallback('markeradded', marker);
return marker;
};
// Remove a fully-formed marker - which must be the same exact marker
// object as in the markers array - from the layer.
m.remove = function(marker) {
if (!marker) return null;
m.parent.removeChild(marker.element);
for (var i = 0; i < markers.length; i++) {
if (markers[i] === marker) {
markers.splice(i, 1);
return marker;
}
}
return marker;
};
m.markers = function(x) {
if (!arguments.length) return markers;
};
// Add a GeoJSON feature to the markers layer.
m.add_feature = function(x) {
return m.features(m.features().concat([x]));
};
m.sort = function(x) {
if (!arguments.length) return sorter;
sorter = x;
return m;
};
// Public data interface
m.features = function(x) {
// Return features
if (!arguments.length) return features;
// Set features
if (!x) x = [];
features = x.slice();
features.sort(sorter);
for (var j = 0; j < markers.length; j++) {
markers[j].touch = false;
}
for (var i = 0; i < features.length; i++) {
if (filter(features[i])) {
var id = keyfn(features[i]);
if (index[id]) {
// marker is already on the map, needs to be moved or rebuilt
index[id].location = new MM.Location(
features[i].geometry.coordinates[1],
features[i].geometry.coordinates[0]);
index[id].coord = null;
reposition(index[id]);
} else {
// marker needs to be added to the map
index[id] = m.add({
element: factory(features[i]),
location: new MM.Location(
features[i].geometry.coordinates[1],
features[i].geometry.coordinates[0]),
data: features[i]
});
}
if (index[id]) index[id].touch = true;
}
}
for (var k = markers.length - 1; k >= 0; k--) {
if (markers[k].touch === false) {
m.remove(markers[k]);
}
}
if (m.map && m.map.coordinate) m.map.draw();
return m;
};
// Request features from a URL - either a local URL or a JSONP call.
// Expects GeoJSON-formatted features.
m.url = function(x, callback) {
if (!arguments.length) return urls;
if (typeof reqwest === 'undefined') throw 'reqwest is required for url loading';
if (typeof x === 'string') x = [x];
urls = x;
function add_features(err, x) {
if (err && callback) return callback(err);
var features = typeof x !== 'undefined' && x.features ? x.features : null;
if (features) m.features(features);
if (callback) callback(err, features, m);
}
reqwest((urls[0].match(/geojsonp$/)) ? {
url: urls[0] + (~urls[0].indexOf('?') ? '&' : '?') + 'callback=?',
type: 'jsonp',
success: function(resp) { add_features(null, resp); },
error: add_features
} : {
url: urls[0],
type: 'json',
success: function(resp) { add_features(null, resp); },
error: add_features
});
return m;
};
m.id = function(x, callback) {
return m.url('http://a.tiles.mapbox.com/v3/' + x + '/markers.geojsonp', callback);
};
m.csv = function(x) {
return m.features(mapbox.markers.csv_to_geojson(x));
};
m.extent = function() {
var ext = [{
lat: Infinity,
lon: Infinity
}, {
lat: -Infinity,
lon: -Infinity
}];
var ft = m.features();
for (var i = 0; i < ft.length; i++) {
var coords = ft[i].geometry.coordinates;
if (coords[0] < ext[0].lon) ext[0].lon = coords[0];
if (coords[1] < ext[0].lat) ext[0].lat = coords[1];
if (coords[0] > ext[1].lon) ext[1].lon = coords[0];
if (coords[1] > ext[1].lat) ext[1].lat = coords[1];
}
return ext;
};
m.key = function(x) {
if (!arguments.length) return keyfn;
if (x === null) {
keyfn = function() { return ++_seq; };
} else {
keyfn = x;
}
return m;
};
// Factory interface
m.factory = function(x) {
if (!arguments.length) return factory;
factory = x;
// re-render all features
m.features(m.features());
return m;
};
m.filter = function(x) {
if (!arguments.length) return filter;
filter = x;
// Setting a filter re-sets the features into a new array.
// This does _not_ change the actual output of .features()
m.features(m.features());
return m;
};
m.destroy = function() {
if (m.parent.parentNode) {
m.parent.parentNode.removeChild(m.parent);
}
};
// Get or set this layer's name
m.named = function(x) {
if (!arguments.length) return m.name;
m.name = x;
return m;
};
m.enabled = true;
m.enable = function() {
this.enabled = true;
this.parent.style.display = '';
return m;
};
m.disable = function() {
this.enabled = false;
this.parent.style.display = 'none';
return m;
};
return m;
};
mmg = mapbox.markers.layer; // Backwards compatibility
mapbox.markers.interaction = function(mmg) {
// Make markersLayer.interaction a singleton and this an accessor.
if (mmg && mmg.interaction) return mmg.interaction;
var mi = {},
tooltips = [],
exclusive = true,
hideOnMove = true,
showOnHover = true,
close_timer = null,
on = true,
formatter;
mi.formatter = function(x) {
if (!arguments.length) return formatter;
formatter = x;
return mi;
};
mi.formatter(function(feature) {
var o = '',
props = feature.properties;
// Tolerate markers without properties at all.
if (!props) return null;
if (props.title) {
o += '' + 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);