Created
April 21, 2014 08:16
-
-
Save xnzac/11135948 to your computer and use it in GitHub Desktop.
leaflet-0-8-dev.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Leaflet 0.8-dev (c5091ee), a JS library for interactive maps. http://leafletjs.com | |
(c) 2010-2014 Vladimir Agafonkin, (c) 2010-2011 CloudMade | |
*/ | |
(function (window, document, undefined) { | |
var L = { | |
version: '0.8-dev' | |
}; | |
function expose() { | |
var oldL = window.L; | |
L.noConflict = function () { | |
window.L = oldL; | |
return this; | |
}; | |
window.L = L; | |
} | |
// define Leaflet for Node module pattern loaders, including Browserify | |
if (typeof module === 'object' && typeof module.exports === 'object') { | |
module.exports = L; | |
// define Leaflet as an AMD module | |
} else if (typeof define === 'function' && define.amd) { | |
define(L); | |
// define Leaflet as a global L variable, saving the original L to restore later if needed | |
} else { | |
expose(); | |
} | |
/* | |
* L.Util contains various utility functions used throughout Leaflet code. | |
*/ | |
L.Util = { | |
// extend an object with properties of one or more other objects | |
extend: function (dest) { | |
var sources = Array.prototype.slice.call(arguments, 1), | |
i, j, len, src; | |
for (j = 0, len = sources.length; j < len; j++) { | |
src = sources[j]; | |
for (i in src) { | |
dest[i] = src[i]; | |
} | |
} | |
return dest; | |
}, | |
// create an object from a given prototype | |
create: Object.create || (function () { | |
function F() {} | |
return function (proto) { | |
F.prototype = proto; | |
return new F(); | |
}; | |
})(), | |
// bind a function to be called with a given context | |
bind: function (fn, obj) { | |
var slice = Array.prototype.slice; | |
if (fn.bind) { | |
return fn.bind.apply(fn, slice.call(arguments, 1)); | |
} | |
var args = slice.call(arguments, 2); | |
return function () { | |
return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); | |
}; | |
}, | |
// return unique ID of an object | |
stamp: function (obj) { | |
// jshint camelcase: false | |
obj._leaflet_id = obj._leaflet_id || ++L.Util.lastId; | |
return obj._leaflet_id; | |
}, | |
lastId: 0, | |
// return a function that won't be called more often than the given interval | |
throttle: function (fn, time, context) { | |
var lock, args, wrapperFn, later; | |
later = function () { | |
// reset lock and call if queued | |
lock = false; | |
if (args) { | |
wrapperFn.apply(context, args); | |
args = false; | |
} | |
}; | |
wrapperFn = function () { | |
if (lock) { | |
// called too soon, queue to call later | |
args = arguments; | |
} else { | |
// call and lock until later | |
fn.apply(context, arguments); | |
setTimeout(later, time); | |
lock = true; | |
} | |
}; | |
return wrapperFn; | |
}, | |
// wrap the given number to lie within a certain range (used for wrapping longitude) | |
wrapNum: function (x, range, includeMax) { | |
var max = range[1], | |
min = range[0], | |
d = max - min; | |
return x === max && includeMax ? x : ((x - min) % d + d) % d + min; | |
}, | |
// do nothing (used as a noop throughout the code) | |
falseFn: function () { return false; }, | |
// round a given number to a given precision | |
formatNum: function (num, digits) { | |
var pow = Math.pow(10, digits || 5); | |
return Math.round(num * pow) / pow; | |
}, | |
// trim whitespace from both sides of a string | |
trim: function (str) { | |
return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); | |
}, | |
// split a string into words | |
splitWords: function (str) { | |
return L.Util.trim(str).split(/\s+/); | |
}, | |
// set options to an object, inheriting parent's options as well | |
setOptions: function (obj, options) { | |
if (!obj.hasOwnProperty('options')) { | |
obj.options = obj.options ? L.Util.create(obj.options) : {}; | |
} | |
for (var i in options) { | |
obj.options[i] = options[i]; | |
} | |
return obj.options; | |
}, | |
// make an URL with GET parameters out of a set of properties/values | |
getParamString: function (obj, existingUrl, uppercase) { | |
var params = []; | |
for (var i in obj) { | |
params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i])); | |
} | |
return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); | |
}, | |
// super-simple templating facility, used for TileLayer URLs | |
template: function (str, data) { | |
return str.replace(L.Util.templateRe, function (str, key) { | |
var value = data[key]; | |
if (value === undefined) { | |
throw new Error('No value provided for variable ' + str); | |
} else if (typeof value === 'function') { | |
value = value(data); | |
} | |
return value; | |
}); | |
}, | |
templateRe: /\{ *([\w_]+) *\}/g, | |
isArray: Array.isArray || function (obj) { | |
return (Object.prototype.toString.call(obj) === '[object Array]'); | |
}, | |
// minimal image URI, set to an image when disposing to flush memory | |
emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' | |
}; | |
(function () { | |
// inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/ | |
function getPrefixed(name) { | |
return window['webkit' + name] || window['moz' + name] || window['ms' + name]; | |
} | |
var lastTime = 0; | |
// fallback for IE 7-8 | |
function timeoutDefer(fn) { | |
var time = +new Date(), | |
timeToCall = Math.max(0, 16 - (time - lastTime)); | |
lastTime = time + timeToCall; | |
return window.setTimeout(fn, timeToCall); | |
} | |
var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer, | |
cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || | |
getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; | |
L.Util.requestAnimFrame = function (fn, context, immediate, element) { | |
if (immediate && requestFn === timeoutDefer) { | |
fn.call(context); | |
} else { | |
return requestFn.call(window, L.bind(fn, context), element); | |
} | |
}; | |
L.Util.cancelAnimFrame = function (id) { | |
if (id) { | |
cancelFn.call(window, id); | |
} | |
}; | |
})(); | |
// shortcuts for most used utility functions | |
L.extend = L.Util.extend; | |
L.bind = L.Util.bind; | |
L.stamp = L.Util.stamp; | |
L.setOptions = L.Util.setOptions; | |
/* | |
* L.Class powers the OOP facilities of the library. | |
* Thanks to John Resig and Dean Edwards for inspiration! | |
*/ | |
L.Class = function () {}; | |
L.Class.extend = function (props) { | |
// extended class with the new prototype | |
var NewClass = function () { | |
// call the constructor | |
if (this.initialize) { | |
this.initialize.apply(this, arguments); | |
} | |
// call all constructor hooks | |
if (this._initHooks.length) { | |
this.callInitHooks(); | |
} | |
}; | |
// jshint camelcase: false | |
var parentProto = NewClass.__super__ = this.prototype; | |
var proto = L.Util.create(parentProto); | |
proto.constructor = NewClass; | |
NewClass.prototype = proto; | |
//inherit parent's statics | |
for (var i in this) { | |
if (this.hasOwnProperty(i) && i !== 'prototype') { | |
NewClass[i] = this[i]; | |
} | |
} | |
// mix static properties into the class | |
if (props.statics) { | |
L.extend(NewClass, props.statics); | |
delete props.statics; | |
} | |
// mix includes into the prototype | |
if (props.includes) { | |
L.Util.extend.apply(null, [proto].concat(props.includes)); | |
delete props.includes; | |
} | |
// merge options | |
if (proto.options) { | |
props.options = L.Util.extend(L.Util.create(proto.options), props.options); | |
} | |
// mix given properties into the prototype | |
L.extend(proto, props); | |
proto._initHooks = []; | |
// add method for calling all hooks | |
proto.callInitHooks = function () { | |
if (this._initHooksCalled) { return; } | |
if (parentProto.callInitHooks) { | |
parentProto.callInitHooks.call(this); | |
} | |
this._initHooksCalled = true; | |
for (var i = 0, len = proto._initHooks.length; i < len; i++) { | |
proto._initHooks[i].call(this); | |
} | |
}; | |
return NewClass; | |
}; | |
// method for adding properties to prototype | |
L.Class.include = function (props) { | |
L.extend(this.prototype, props); | |
}; | |
// merge new default options to the Class | |
L.Class.mergeOptions = function (options) { | |
L.extend(this.prototype.options, options); | |
}; | |
// add a constructor hook | |
L.Class.addInitHook = function (fn) { // (Function) || (String, args...) | |
var args = Array.prototype.slice.call(arguments, 1); | |
var init = typeof fn === 'function' ? fn : function () { | |
this[fn].apply(this, args); | |
}; | |
this.prototype._initHooks = this.prototype._initHooks || []; | |
this.prototype._initHooks.push(init); | |
}; | |
/* | |
* L.Evented is a base class that Leaflet classes inherit from to handle custom events. | |
*/ | |
L.Evented = L.Class.extend({ | |
on: function (types, fn, context) { | |
// types can be a map of types/handlers | |
if (typeof types === 'object') { | |
for (var type in types) { | |
// we don't process space-separated events here for performance; | |
// it's a hot path since Layer uses the on(obj) syntax | |
this._on(type, types[type], fn); | |
} | |
} else { | |
// types can be a string of space-separated words | |
types = L.Util.splitWords(types); | |
for (var i = 0, len = types.length; i < len; i++) { | |
this._on(types[i], fn, context); | |
} | |
} | |
return this; | |
}, | |
off: function (types, fn, context) { | |
if (!types) { | |
// clear all listeners if called without arguments | |
delete this._events; | |
} else if (typeof types === 'object') { | |
for (var type in types) { | |
this._off(type, types[type], fn); | |
} | |
} else { | |
types = L.Util.splitWords(types); | |
for (var i = 0, len = types.length; i < len; i++) { | |
this._off(types[i], fn, context); | |
} | |
} | |
return this; | |
}, | |
// attach listener (without syntactic sugar now) | |
_on: function (type, fn, context) { | |
var events = this._events = this._events || {}, | |
contextId = context && context !== this && L.stamp(context); | |
if (contextId) { | |
// store listeners with custom context in a separate hash (if it has an id); | |
// gives a major performance boost when firing and removing events (e.g. on map object) | |
var indexKey = type + '_idx', | |
indexLenKey = type + '_len', | |
typeIndex = events[indexKey] = events[indexKey] || {}, | |
id = L.stamp(fn) + '_' + contextId; | |
if (!typeIndex[id]) { | |
typeIndex[id] = {fn: fn, ctx: context}; | |
// keep track of the number of keys in the index to quickly check if it's empty | |
events[indexLenKey] = (events[indexLenKey] || 0) + 1; | |
} | |
} else { | |
// individual layers mostly use "this" for context and don't fire listeners too often | |
// so simple array makes the memory footprint better while not degrading performance | |
events[type] = events[type] || []; | |
events[type].push({fn: fn}); | |
} | |
}, | |
_off: function (type, fn, context) { | |
var events = this._events, | |
indexKey = type + '_idx', | |
indexLenKey = type + '_len'; | |
if (!events) { return; } | |
if (!fn) { | |
// clear all listeners for a type if function isn't specified | |
delete events[type]; | |
delete events[indexKey]; | |
delete events[indexLenKey]; | |
return; | |
} | |
var contextId = context && context !== this && L.stamp(context), | |
listeners, i, len, listener, id; | |
if (contextId) { | |
id = L.stamp(fn) + '_' + contextId; | |
listeners = events[indexKey]; | |
if (listeners && listeners[id]) { | |
listener = listeners[id]; | |
delete listeners[id]; | |
events[indexLenKey]--; | |
} | |
} else { | |
listeners = events[type]; | |
if (listeners) { | |
for (i = 0, len = listeners.length; i < len; i++) { | |
if (listeners[i].fn === fn) { | |
listener = listeners[i]; | |
listeners.splice(i, 1); | |
break; | |
} | |
} | |
} | |
} | |
// set the removed listener to noop so that's not called if remove happens in fire | |
if (listener) { | |
listener.fn = L.Util.falseFn; | |
} | |
}, | |
fire: function (type, data, propagate) { | |
if (!this.listens(type, propagate)) { return this; } | |
var event = L.Util.extend({}, data, {type: type, target: this}), | |
events = this._events; | |
if (events) { | |
var typeIndex = events[type + '_idx'], | |
i, len, listeners, id; | |
if (events[type]) { | |
// make sure adding/removing listeners inside other listeners won't cause infinite loop | |
listeners = events[type].slice(); | |
for (i = 0, len = listeners.length; i < len; i++) { | |
listeners[i].fn.call(this, event); | |
} | |
} | |
// fire event for the context-indexed listeners as well | |
for (id in typeIndex) { | |
typeIndex[id].fn.call(typeIndex[id].ctx, event); | |
} | |
} | |
if (propagate) { | |
// propagate the event to parents (set with addEventParent) | |
this._propagateEvent(event); | |
} | |
return this; | |
}, | |
listens: function (type, propagate) { | |
var events = this._events; | |
if (events && (events[type] || events[type + '_len'])) { return true; } | |
if (propagate) { | |
// also check parents for listeners if event propagates | |
for (var id in this._eventParents) { | |
if (this._eventParents[id].listens(type, propagate)) { return true; } | |
} | |
} | |
return false; | |
}, | |
once: function (types, fn, context) { | |
if (typeof types === 'object') { | |
for (var type in types) { | |
this.once(type, types[type], fn); | |
} | |
return this; | |
} | |
var handler = L.bind(function () { | |
this | |
.off(types, fn, context) | |
.off(types, handler, context); | |
}, this); | |
// add a listener that's executed once and removed after that | |
return this | |
.on(types, fn, context) | |
.on(types, handler, context); | |
}, | |
// adds a parent to propagate events to (when you fire with true as a 3rd argument) | |
addEventParent: function (obj) { | |
this._eventParents = this._eventParents || {}; | |
this._eventParents[L.stamp(obj)] = obj; | |
return this; | |
}, | |
removeEventParent: function (obj) { | |
if (this._eventParents) { | |
delete this._eventParents[L.stamp(obj)]; | |
} | |
return this; | |
}, | |
_propagateEvent: function (e) { | |
for (var id in this._eventParents) { | |
this._eventParents[id].fire(e.type, L.extend({layer: e.target}, e), true); | |
} | |
} | |
}); | |
var proto = L.Evented.prototype; | |
// aliases; we should ditch those eventually | |
proto.addEventListener = proto.on; | |
proto.removeEventListener = proto.clearAllEventListeners = proto.off; | |
proto.addOneTimeEventListener = proto.once; | |
proto.fireEvent = proto.fire; | |
proto.hasEventListeners = proto.listens; | |
L.Mixin = {Events: proto}; | |
/* | |
* L.Browser handles different browser and feature detections for internal Leaflet use. | |
*/ | |
(function () { | |
var ua = navigator.userAgent.toLowerCase(), | |
doc = document.documentElement, | |
ie = 'ActiveXObject' in window, | |
webkit = ua.indexOf('webkit') !== -1, | |
phantomjs = ua.indexOf('phantom') !== -1, | |
android23 = ua.search('android [23]') !== -1, | |
chrome = ua.indexOf('chrome') !== -1, | |
mobile = typeof orientation !== 'undefined', | |
msPointer = navigator.msPointerEnabled && navigator.msMaxTouchPoints && !window.PointerEvent, | |
pointer = (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) || msPointer, | |
ie3d = ie && ('transition' in doc.style), | |
webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23, | |
gecko3d = 'MozPerspective' in doc.style, | |
opera3d = 'OTransition' in doc.style; | |
var retina = 'devicePixelRatio' in window && window.devicePixelRatio > 1; | |
if (!retina && 'matchMedia' in window) { | |
var matches = window.matchMedia('(min-resolution:144dpi)'); | |
retina = matches && matches.matches; | |
} | |
var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window || | |
(window.DocumentTouch && document instanceof window.DocumentTouch)); | |
L.Browser = { | |
ie: ie, | |
ielt9: ie && !document.addEventListener, | |
webkit: webkit, | |
gecko: (ua.indexOf('gecko') !== -1) && !webkit && !window.opera && !ie, | |
android: ua.indexOf('android') !== -1, | |
android23: android23, | |
chrome: chrome, | |
safari: !chrome && ua.indexOf('safari') !== -1, | |
ie3d: ie3d, | |
webkit3d: webkit3d, | |
gecko3d: gecko3d, | |
opera3d: opera3d, | |
any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs, | |
mobile: mobile, | |
mobileWebkit: mobile && webkit, | |
mobileWebkit3d: mobile && webkit3d, | |
mobileOpera: mobile && window.opera, | |
touch: !!touch, | |
msPointer: !!msPointer, | |
pointer: !!pointer, | |
retina: !!retina | |
}; | |
}()); | |
/* | |
* L.Point represents a point with x and y coordinates. | |
*/ | |
L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) { | |
this.x = (round ? Math.round(x) : x); | |
this.y = (round ? Math.round(y) : y); | |
}; | |
L.Point.prototype = { | |
clone: function () { | |
return new L.Point(this.x, this.y); | |
}, | |
// non-destructive, returns a new point | |
add: function (point) { | |
return this.clone()._add(L.point(point)); | |
}, | |
// destructive, used directly for performance in situations where it's safe to modify existing point | |
_add: function (point) { | |
this.x += point.x; | |
this.y += point.y; | |
return this; | |
}, | |
subtract: function (point) { | |
return this.clone()._subtract(L.point(point)); | |
}, | |
_subtract: function (point) { | |
this.x -= point.x; | |
this.y -= point.y; | |
return this; | |
}, | |
divideBy: function (num) { | |
return this.clone()._divideBy(num); | |
}, | |
_divideBy: function (num) { | |
this.x /= num; | |
this.y /= num; | |
return this; | |
}, | |
multiplyBy: function (num) { | |
return this.clone()._multiplyBy(num); | |
}, | |
_multiplyBy: function (num) { | |
this.x *= num; | |
this.y *= num; | |
return this; | |
}, | |
round: function () { | |
return this.clone()._round(); | |
}, | |
_round: function () { | |
this.x = Math.round(this.x); | |
this.y = Math.round(this.y); | |
return this; | |
}, | |
floor: function () { | |
return this.clone()._floor(); | |
}, | |
_floor: function () { | |
this.x = Math.floor(this.x); | |
this.y = Math.floor(this.y); | |
return this; | |
}, | |
ceil: function () { | |
return this.clone()._ceil(); | |
}, | |
_ceil: function () { | |
this.x = Math.ceil(this.x); | |
this.y = Math.ceil(this.y); | |
return this; | |
}, | |
distanceTo: function (point) { | |
point = L.point(point); | |
var x = point.x - this.x, | |
y = point.y - this.y; | |
return Math.sqrt(x * x + y * y); | |
}, | |
equals: function (point) { | |
point = L.point(point); | |
return point.x === this.x && | |
point.y === this.y; | |
}, | |
contains: function (point) { | |
point = L.point(point); | |
return Math.abs(point.x) <= Math.abs(this.x) && | |
Math.abs(point.y) <= Math.abs(this.y); | |
}, | |
toString: function () { | |
return 'Point(' + | |
L.Util.formatNum(this.x) + ', ' + | |
L.Util.formatNum(this.y) + ')'; | |
} | |
}; | |
L.point = function (x, y, round) { | |
if (x instanceof L.Point) { | |
return x; | |
} | |
if (L.Util.isArray(x)) { | |
return new L.Point(x[0], x[1]); | |
} | |
if (x === undefined || x === null) { | |
return x; | |
} | |
return new L.Point(x, y, round); | |
}; | |
/* | |
* L.Bounds represents a rectangular area on the screen in pixel coordinates. | |
*/ | |
L.Bounds = function (a, b) { //(Point, Point) or Point[] | |
if (!a) { return; } | |
var points = b ? [a, b] : a; | |
for (var i = 0, len = points.length; i < len; i++) { | |
this.extend(points[i]); | |
} | |
}; | |
L.Bounds.prototype = { | |
// extend the bounds to contain the given point | |
extend: function (point) { // (Point) | |
point = L.point(point); | |
if (!this.min && !this.max) { | |
this.min = point.clone(); | |
this.max = point.clone(); | |
} else { | |
this.min.x = Math.min(point.x, this.min.x); | |
this.max.x = Math.max(point.x, this.max.x); | |
this.min.y = Math.min(point.y, this.min.y); | |
this.max.y = Math.max(point.y, this.max.y); | |
} | |
return this; | |
}, | |
getCenter: function (round) { // (Boolean) -> Point | |
return new L.Point( | |
(this.min.x + this.max.x) / 2, | |
(this.min.y + this.max.y) / 2, round); | |
}, | |
getBottomLeft: function () { // -> Point | |
return new L.Point(this.min.x, this.max.y); | |
}, | |
getTopRight: function () { // -> Point | |
return new L.Point(this.max.x, this.min.y); | |
}, | |
getSize: function () { | |
return this.max.subtract(this.min); | |
}, | |
contains: function (obj) { // (Bounds) or (Point) -> Boolean | |
var min, max; | |
if (typeof obj[0] === 'number' || obj instanceof L.Point) { | |
obj = L.point(obj); | |
} else { | |
obj = L.bounds(obj); | |
} | |
if (obj instanceof L.Bounds) { | |
min = obj.min; | |
max = obj.max; | |
} else { | |
min = max = obj; | |
} | |
return (min.x >= this.min.x) && | |
(max.x <= this.max.x) && | |
(min.y >= this.min.y) && | |
(max.y <= this.max.y); | |
}, | |
intersects: function (bounds) { // (Bounds) -> Boolean | |
bounds = L.bounds(bounds); | |
var min = this.min, | |
max = this.max, | |
min2 = bounds.min, | |
max2 = bounds.max, | |
xIntersects = (max2.x >= min.x) && (min2.x <= max.x), | |
yIntersects = (max2.y >= min.y) && (min2.y <= max.y); | |
return xIntersects && yIntersects; | |
}, | |
isValid: function () { | |
return !!(this.min && this.max); | |
} | |
}; | |
L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[]) | |
if (!a || a instanceof L.Bounds) { | |
return a; | |
} | |
return new L.Bounds(a, b); | |
}; | |
/* | |
* L.Transformation is an utility class to perform simple point transformations through a 2d-matrix. | |
*/ | |
L.Transformation = function (a, b, c, d) { | |
this._a = a; | |
this._b = b; | |
this._c = c; | |
this._d = d; | |
}; | |
L.Transformation.prototype = { | |
transform: function (point, scale) { // (Point, Number) -> Point | |
return this._transform(point.clone(), scale); | |
}, | |
// destructive transform (faster) | |
_transform: function (point, scale) { | |
scale = scale || 1; | |
point.x = scale * (this._a * point.x + this._b); | |
point.y = scale * (this._c * point.y + this._d); | |
return point; | |
}, | |
untransform: function (point, scale) { | |
scale = scale || 1; | |
return new L.Point( | |
(point.x / scale - this._b) / this._a, | |
(point.y / scale - this._d) / this._c); | |
} | |
}; | |
/* | |
* L.DomUtil contains various utility functions for working with DOM. | |
*/ | |
L.DomUtil = { | |
get: function (id) { | |
return typeof id === 'string' ? document.getElementById(id) : id; | |
}, | |
getStyle: function (el, style) { | |
var value = el.style[style] || (el.currentStyle && el.currentStyle[style]); | |
if ((!value || value === 'auto') && document.defaultView) { | |
var css = document.defaultView.getComputedStyle(el, null); | |
value = css ? css[style] : null; | |
} | |
return value === 'auto' ? null : value; | |
}, | |
create: function (tagName, className, container) { | |
var el = document.createElement(tagName); | |
el.className = className; | |
if (container) { | |
container.appendChild(el); | |
} | |
return el; | |
}, | |
remove: function (el) { | |
var parent = el.parentNode; | |
if (parent) { | |
parent.removeChild(el); | |
} | |
}, | |
toFront: function (el) { | |
el.parentNode.appendChild(el); | |
}, | |
toBack: function (el) { | |
var parent = el.parentNode; | |
parent.insertBefore(el, parent.firstChild); | |
}, | |
hasClass: function (el, name) { | |
if (el.classList !== undefined) { | |
return el.classList.contains(name); | |
} | |
var className = L.DomUtil.getClass(el); | |
return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); | |
}, | |
addClass: function (el, name) { | |
if (el.classList !== undefined) { | |
var classes = L.Util.splitWords(name); | |
for (var i = 0, len = classes.length; i < len; i++) { | |
el.classList.add(classes[i]); | |
} | |
} else if (!L.DomUtil.hasClass(el, name)) { | |
var className = L.DomUtil.getClass(el); | |
L.DomUtil.setClass(el, (className ? className + ' ' : '') + name); | |
} | |
}, | |
removeClass: function (el, name) { | |
if (el.classList !== undefined) { | |
el.classList.remove(name); | |
} else { | |
L.DomUtil.setClass(el, L.Util.trim((' ' + L.DomUtil.getClass(el) + ' ').replace(' ' + name + ' ', ' '))); | |
} | |
}, | |
setClass: function (el, name) { | |
if (el.className.baseVal === undefined) { | |
el.className = name; | |
} else { | |
// in case of SVG element | |
el.className.baseVal = name; | |
} | |
}, | |
getClass: function (el) { | |
return el.className.baseVal === undefined ? el.className : el.className.baseVal; | |
}, | |
setOpacity: function (el, value) { | |
if ('opacity' in el.style) { | |
el.style.opacity = value; | |
} else if ('filter' in el.style) { | |
var filter = false, | |
filterName = 'DXImageTransform.Microsoft.Alpha'; | |
// filters collection throws an error if we try to retrieve a filter that doesn't exist | |
try { | |
filter = el.filters.item(filterName); | |
} catch (e) { | |
// don't set opacity to 1 if we haven't already set an opacity, | |
// it isn't needed and breaks transparent pngs. | |
if (value === 1) { return; } | |
} | |
value = Math.round(value * 100); | |
if (filter) { | |
filter.Enabled = (value !== 100); | |
filter.Opacity = value; | |
} else { | |
el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')'; | |
} | |
} | |
}, | |
testProp: function (props) { | |
var style = document.documentElement.style; | |
for (var i = 0; i < props.length; i++) { | |
if (props[i] in style) { | |
return props[i]; | |
} | |
} | |
return false; | |
}, | |
setTransform: function (el, offset, scale) { | |
var pos = offset || new L.Point(0, 0); | |
el.style[L.DomUtil.TRANSFORM] = | |
'translate3d(' + pos.x + 'px,' + pos.y + 'px' + ',0)' + (scale ? ' scale(' + scale + ')' : ''); | |
}, | |
setPosition: function (el, point, no3d) { // (HTMLElement, Point[, Boolean]) | |
// jshint camelcase: false | |
el._leaflet_pos = point; | |
if (L.Browser.any3d && !no3d) { | |
L.DomUtil.setTransform(el, point); | |
} else { | |
el.style.left = point.x + 'px'; | |
el.style.top = point.y + 'px'; | |
} | |
}, | |
getPosition: function (el) { | |
// this method is only used for elements previously positioned using setPosition, | |
// so it's safe to cache the position for performance | |
// jshint camelcase: false | |
return el._leaflet_pos; | |
} | |
}; | |
(function () { | |
// prefix style property names | |
L.DomUtil.TRANSFORM = L.DomUtil.testProp( | |
['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); | |
// webkitTransition comes first because some browser versions that drop vendor prefix don't do | |
// the same for the transitionend event, in particular the Android 4.1 stock browser | |
var transition = L.DomUtil.TRANSITION = L.DomUtil.testProp( | |
['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); | |
L.DomUtil.TRANSITION_END = | |
transition === 'webkitTransition' || transition === 'OTransition' ? transition + 'End' : 'transitionend'; | |
if ('onselectstart' in document) { | |
L.DomUtil.disableTextSelection = function () { | |
L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault); | |
}; | |
L.DomUtil.enableTextSelection = function () { | |
L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault); | |
}; | |
} else { | |
var userSelectProperty = L.DomUtil.testProp( | |
['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); | |
L.DomUtil.disableTextSelection = function () { | |
if (userSelectProperty) { | |
var style = document.documentElement.style; | |
this._userSelect = style[userSelectProperty]; | |
style[userSelectProperty] = 'none'; | |
} | |
}; | |
L.DomUtil.enableTextSelection = function () { | |
if (userSelectProperty) { | |
document.documentElement.style[userSelectProperty] = this._userSelect; | |
delete this._userSelect; | |
} | |
}; | |
} | |
L.DomUtil.disableImageDrag = function () { | |
L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault); | |
}; | |
L.DomUtil.enableImageDrag = function () { | |
L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault); | |
}; | |
})(); | |
/* | |
* L.LatLng represents a geographical point with latitude and longitude coordinates. | |
*/ | |
L.LatLng = function (lat, lng, alt) { | |
if (isNaN(lat) || isNaN(lng)) { | |
throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); | |
} | |
this.lat = +lat; | |
this.lng = +lng; | |
if (alt !== undefined) { | |
this.alt = +alt; | |
} | |
}; | |
L.LatLng.prototype = { | |
equals: function (obj, maxMargin) { | |
if (!obj) { return false; } | |
obj = L.latLng(obj); | |
var margin = Math.max( | |
Math.abs(this.lat - obj.lat), | |
Math.abs(this.lng - obj.lng)); | |
return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); | |
}, | |
toString: function (precision) { | |
return 'LatLng(' + | |
L.Util.formatNum(this.lat, precision) + ', ' + | |
L.Util.formatNum(this.lng, precision) + ')'; | |
}, | |
distanceTo: function (other) { | |
return L.CRS.Earth.distance(this, L.latLng(other)); | |
}, | |
wrap: function () { | |
return L.CRS.Earth.wrapLatLng(this); | |
} | |
}; | |
// constructs LatLng with different signatures | |
// (LatLng) or ([Number, Number]) or (Number, Number) or (Object) | |
L.latLng = function (a, b) { | |
if (a instanceof L.LatLng) { | |
return a; | |
} | |
if (L.Util.isArray(a) && typeof a[0] !== 'object') { | |
if (a.length === 3) { | |
return new L.LatLng(a[0], a[1], a[2]); | |
} | |
return new L.LatLng(a[0], a[1]); | |
} | |
if (a === undefined || a === null) { | |
return a; | |
} | |
if (typeof a === 'object' && 'lat' in a) { | |
return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon); | |
} | |
if (b === undefined) { | |
return null; | |
} | |
return new L.LatLng(a, b); | |
}; | |
/* | |
* L.LatLngBounds represents a rectangular area on the map in geographical coordinates. | |
*/ | |
L.LatLngBounds = function (southWest, northEast) { // (LatLng, LatLng) or (LatLng[]) | |
if (!southWest) { return; } | |
var latlngs = northEast ? [southWest, northEast] : southWest; | |
for (var i = 0, len = latlngs.length; i < len; i++) { | |
this.extend(latlngs[i]); | |
} | |
}; | |
L.LatLngBounds.prototype = { | |
// extend the bounds to contain the given point or bounds | |
extend: function (obj) { // (LatLng) or (LatLngBounds) | |
var sw = this._southWest, | |
ne = this._northEast, | |
sw2, ne2; | |
if (obj instanceof L.LatLng) { | |
sw2 = obj; | |
ne2 = obj; | |
} else if (obj instanceof L.LatLngBounds) { | |
sw2 = obj._southWest; | |
ne2 = obj._northEast; | |
if (!sw2 || !ne2) { return this; } | |
} else { | |
return obj ? this.extend(L.latLng(obj) || L.latLngBounds(obj)) : this; | |
} | |
if (!sw && !ne) { | |
this._southWest = new L.LatLng(sw2.lat, sw2.lng); | |
this._northEast = new L.LatLng(ne2.lat, ne2.lng); | |
} else { | |
sw.lat = Math.min(sw2.lat, sw.lat); | |
sw.lng = Math.min(sw2.lng, sw.lng); | |
ne.lat = Math.max(ne2.lat, ne.lat); | |
ne.lng = Math.max(ne2.lng, ne.lng); | |
} | |
return this; | |
}, | |
// extend the bounds by a percentage | |
pad: function (bufferRatio) { // (Number) -> LatLngBounds | |
var sw = this._southWest, | |
ne = this._northEast, | |
heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, | |
widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; | |
return new L.LatLngBounds( | |
new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), | |
new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); | |
}, | |
getCenter: function () { // -> LatLng | |
return new L.LatLng( | |
(this._southWest.lat + this._northEast.lat) / 2, | |
(this._southWest.lng + this._northEast.lng) / 2); | |
}, | |
getSouthWest: function () { | |
return this._southWest; | |
}, | |
getNorthEast: function () { | |
return this._northEast; | |
}, | |
getNorthWest: function () { | |
return new L.LatLng(this.getNorth(), this.getWest()); | |
}, | |
getSouthEast: function () { | |
return new L.LatLng(this.getSouth(), this.getEast()); | |
}, | |
getWest: function () { | |
return this._southWest.lng; | |
}, | |
getSouth: function () { | |
return this._southWest.lat; | |
}, | |
getEast: function () { | |
return this._northEast.lng; | |
}, | |
getNorth: function () { | |
return this._northEast.lat; | |
}, | |
contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean | |
if (typeof obj[0] === 'number' || obj instanceof L.LatLng) { | |
obj = L.latLng(obj); | |
} else { | |
obj = L.latLngBounds(obj); | |
} | |
var sw = this._southWest, | |
ne = this._northEast, | |
sw2, ne2; | |
if (obj instanceof L.LatLngBounds) { | |
sw2 = obj.getSouthWest(); | |
ne2 = obj.getNorthEast(); | |
} else { | |
sw2 = ne2 = obj; | |
} | |
return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && | |
(sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); | |
}, | |
intersects: function (bounds) { // (LatLngBounds) | |
bounds = L.latLngBounds(bounds); | |
var sw = this._southWest, | |
ne = this._northEast, | |
sw2 = bounds.getSouthWest(), | |
ne2 = bounds.getNorthEast(), | |
latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), | |
lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); | |
return latIntersects && lngIntersects; | |
}, | |
toBBoxString: function () { | |
return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); | |
}, | |
equals: function (bounds) { // (LatLngBounds) | |
if (!bounds) { return false; } | |
bounds = L.latLngBounds(bounds); | |
return this._southWest.equals(bounds.getSouthWest()) && | |
this._northEast.equals(bounds.getNorthEast()); | |
}, | |
isValid: function () { | |
return !!(this._southWest && this._northEast); | |
} | |
}; | |
//TODO International date line? | |
L.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng) | |
if (!a || a instanceof L.LatLngBounds) { | |
return a; | |
} | |
return new L.LatLngBounds(a, b); | |
}; | |
/* | |
* Simple equirectangular (Plate Carree) projection, used by CRS like EPSG:4326 and Simple. | |
*/ | |
L.Projection = {}; | |
L.Projection.LonLat = { | |
project: function (latlng) { | |
return new L.Point(latlng.lng, latlng.lat); | |
}, | |
unproject: function (point) { | |
return new L.LatLng(point.y, point.x); | |
}, | |
bounds: L.bounds([-180, -90], [180, 90]) | |
}; | |
/* | |
* Spherical Mercator is the most popular map projection, used by EPSG:3857 CRS used by default. | |
*/ | |
L.Projection.SphericalMercator = { | |
R: 6378137, | |
project: function (latlng) { | |
var d = Math.PI / 180, | |
max = 1 - 1E-15, | |
sin = Math.max(Math.min(Math.sin(latlng.lat * d), max), -max); | |
return new L.Point( | |
this.R * latlng.lng * d, | |
this.R * Math.log((1 + sin) / (1 - sin)) / 2); | |
}, | |
unproject: function (point) { | |
var d = 180 / Math.PI; | |
return new L.LatLng( | |
(2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, | |
point.x * d / this.R); | |
}, | |
bounds: (function () { | |
var d = 6378137 * Math.PI; | |
return L.bounds([-d, -d], [d, d]); | |
})() | |
}; | |
/* | |
* L.CRS is the base object for all defined CRS (Coordinate Reference Systems) in Leaflet. | |
*/ | |
L.CRS = { | |
// converts geo coords to pixel ones | |
latLngToPoint: function (latlng, zoom) { | |
var projectedPoint = this.projection.project(latlng), | |
scale = this.scale(zoom); | |
return this.transformation._transform(projectedPoint, scale); | |
}, | |
// converts pixel coords to geo coords | |
pointToLatLng: function (point, zoom) { | |
var scale = this.scale(zoom), | |
untransformedPoint = this.transformation.untransform(point, scale); | |
return this.projection.unproject(untransformedPoint); | |
}, | |
// converts geo coords to projection-specific coords (e.g. in meters) | |
project: function (latlng) { | |
return this.projection.project(latlng); | |
}, | |
// converts projected coords to geo coords | |
unproject: function (point) { | |
return this.projection.unproject(point); | |
}, | |
// defines how the world scales with zoom | |
scale: function (zoom) { | |
return 256 * Math.pow(2, zoom); | |
}, | |
// returns the bounds of the world in projected coords if applicable | |
getProjectedBounds: function (zoom) { | |
if (this.infinite) { return null; } | |
var b = this.projection.bounds, | |
s = this.scale(zoom), | |
min = this.transformation.transform(b.min, s), | |
max = this.transformation.transform(b.max, s); | |
return L.bounds(min, max); | |
}, | |
// whether a coordinate axis wraps in a given range (e.g. longitude from -180 to 180); depends on CRS | |
// wrapLng: [min, max], | |
// wrapLat: [min, max], | |
// if true, the coordinate space will be unbounded (infinite in all directions) | |
// infinite: false, | |
// wraps geo coords in certain ranges if applicable | |
wrapLatLng: function (latlng) { | |
var lng = this.wrapLng ? L.Util.wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, | |
lat = this.wrapLat ? L.Util.wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat; | |
return L.latLng(lat, lng); | |
} | |
}; | |
/* | |
* A simple CRS that can be used for flat non-Earth maps like panoramas or game maps. | |
*/ | |
L.CRS.Simple = L.extend({}, L.CRS, { | |
projection: L.Projection.LonLat, | |
transformation: new L.Transformation(1, 0, -1, 0), | |
scale: function (zoom) { | |
return Math.pow(2, zoom); | |
}, | |
distance: function (latlng1, latlng2) { | |
var dx = latlng2.lng - latlng1.lng, | |
dy = latlng2.lat - latlng1.lat; | |
return Math.sqrt(dx * dx + dy * dy); | |
}, | |
infinite: true | |
}); | |
/* | |
* L.CRS.Earth is the base class for all CRS representing Earth. | |
*/ | |
L.CRS.Earth = L.extend({}, L.CRS, { | |
wrapLng: [-180, 180], | |
R: 6378137, | |
// distane between two geographical points using spherical law of cosines approximation | |
distance: function (latlng1, latlng2) { | |
var rad = Math.PI / 180, | |
lat1 = latlng1.lat * rad, | |
lat2 = latlng2.lat * rad; | |
return this.R * Math.acos(Math.sin(lat1) * Math.sin(lat2) + | |
Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad)); | |
} | |
}); | |
/* | |
* L.CRS.EPSG3857 (Spherical Mercator) is the most common CRS for web mapping and is used by Leaflet by default. | |
*/ | |
L.CRS.EPSG3857 = L.extend({}, L.CRS.Earth, { | |
code: 'EPSG:3857', | |
projection: L.Projection.SphericalMercator, | |
transformation: (function () { | |
var scale = 0.5 / (Math.PI * L.Projection.SphericalMercator.R); | |
return new L.Transformation(scale, 0.5, -scale, 0.5); | |
}()) | |
}); | |
L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, { | |
code: 'EPSG:900913' | |
}); | |
/* | |
* L.CRS.EPSG4326 is a CRS popular among advanced GIS specialists. | |
*/ | |
L.CRS.EPSG4326 = L.extend({}, L.CRS.Earth, { | |
code: 'EPSG:4326', | |
projection: L.Projection.LonLat, | |
transformation: new L.Transformation(1 / 180, 1, -1 / 180, 0.5) | |
}); | |
/* | |
* L.Map is the central class of the API - it is used to create a map. | |
*/ | |
L.Map = L.Evented.extend({ | |
options: { | |
crs: L.CRS.EPSG3857, | |
/* | |
center: LatLng, | |
zoom: Number, | |
layers: Array, | |
*/ | |
fadeAnimation: true, | |
trackResize: true, | |
markerZoomAnimation: true | |
}, | |
initialize: function (id, options) { // (HTMLElement or String, Object) | |
options = L.setOptions(this, options); | |
this._initContainer(id); | |
this._initLayout(); | |
// hack for https://github.com/Leaflet/Leaflet/issues/1980 | |
this._onResize = L.bind(this._onResize, this); | |
this._initEvents(); | |
if (options.maxBounds) { | |
this.setMaxBounds(options.maxBounds); | |
} | |
if (options.center && options.zoom !== undefined) { | |
this.setView(L.latLng(options.center), options.zoom, {reset: true}); | |
} | |
this._handlers = []; | |
this._layers = {}; | |
this._zoomBoundLayers = {}; | |
this.callInitHooks(); | |
this._addLayers(this.options.layers); | |
}, | |
// public methods that modify map state | |
// replaced by animation-powered implementation in Map.PanAnimation.js | |
setView: function (center, zoom) { | |
zoom = zoom === undefined ? this.getZoom() : zoom; | |
this._resetView(L.latLng(center), this._limitZoom(zoom)); | |
return this; | |
}, | |
setZoom: function (zoom, options) { | |
if (!this._loaded) { | |
this._zoom = this._limitZoom(zoom); | |
return this; | |
} | |
return this.setView(this.getCenter(), zoom, {zoom: options}); | |
}, | |
zoomIn: function (delta, options) { | |
return this.setZoom(this._zoom + (delta || 1), options); | |
}, | |
zoomOut: function (delta, options) { | |
return this.setZoom(this._zoom - (delta || 1), options); | |
}, | |
setZoomAround: function (latlng, zoom, options) { | |
var scale = this.getZoomScale(zoom), | |
viewHalf = this.getSize().divideBy(2), | |
containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng), | |
centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale), | |
newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset)); | |
return this.setView(newCenter, zoom, {zoom: options}); | |
}, | |
fitBounds: function (bounds, options) { | |
options = options || {}; | |
bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds); | |
var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), | |
paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), | |
zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); | |
zoom = options.maxZoom ? Math.min(options.maxZoom, zoom) : zoom; | |
var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), | |
swPoint = this.project(bounds.getSouthWest(), zoom), | |
nePoint = this.project(bounds.getNorthEast(), zoom), | |
center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); | |
return this.setView(center, zoom, options); | |
}, | |
fitWorld: function (options) { | |
return this.fitBounds([[-90, -180], [90, 180]], options); | |
}, | |
panTo: function (center, options) { // (LatLng) | |
return this.setView(center, this._zoom, {pan: options}); | |
}, | |
panBy: function (offset) { // (Point) | |
// replaced with animated panBy in Map.PanAnimation.js | |
this.fire('movestart'); | |
this._rawPanBy(L.point(offset)); | |
this.fire('move'); | |
return this.fire('moveend'); | |
}, | |
setMaxBounds: function (bounds) { | |
bounds = L.latLngBounds(bounds); | |
this.options.maxBounds = bounds; | |
if (!bounds) { | |
return this.off('moveend', this._panInsideMaxBounds); | |
} | |
if (this._loaded) { | |
this._panInsideMaxBounds(); | |
} | |
return this.on('moveend', this._panInsideMaxBounds); | |
}, | |
panInsideBounds: function (bounds, options) { | |
var center = this.getCenter(), | |
newCenter = this._limitCenter(center, this._zoom, bounds); | |
if (center.equals(newCenter)) { return this; } | |
return this.panTo(newCenter, options); | |
}, | |
invalidateSize: function (options) { | |
if (!this._loaded) { return this; } | |
options = L.extend({ | |
animate: false, | |
pan: true | |
}, options === true ? {animate: true} : options); | |
var oldSize = this.getSize(); | |
this._sizeChanged = true; | |
this._initialCenter = null; | |
var newSize = this.getSize(), | |
oldCenter = oldSize.divideBy(2).round(), | |
newCenter = newSize.divideBy(2).round(), | |
offset = oldCenter.subtract(newCenter); | |
if (!offset.x && !offset.y) { return this; } | |
if (options.animate && options.pan) { | |
this.panBy(offset); | |
} else { | |
if (options.pan) { | |
this._rawPanBy(offset); | |
} | |
this.fire('move'); | |
if (options.debounceMoveend) { | |
clearTimeout(this._sizeTimer); | |
this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); | |
} else { | |
this.fire('moveend'); | |
} | |
} | |
return this.fire('resize', { | |
oldSize: oldSize, | |
newSize: newSize | |
}); | |
}, | |
// TODO handler.addTo | |
addHandler: function (name, HandlerClass) { | |
if (!HandlerClass) { return this; } | |
var handler = this[name] = new HandlerClass(this); | |
this._handlers.push(handler); | |
if (this.options[name]) { | |
handler.enable(); | |
} | |
return this; | |
}, | |
remove: function () { | |
this._initEvents('off'); | |
try { | |
// throws error in IE6-8 | |
delete this._container._leaflet; | |
} catch (e) { | |
this._container._leaflet = undefined; | |
} | |
L.DomUtil.remove(this._mapPane); | |
if (this._clearControlPos) { | |
this._clearControlPos(); | |
} | |
this._clearHandlers(); | |
if (this._loaded) { | |
this.fire('unload'); | |
} | |
return this; | |
}, | |
createPane: function (name, container) { | |
var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''), | |
pane = L.DomUtil.create('div', className, container || this._mapPane); | |
if (name) { | |
this._panes[name] = pane; | |
} | |
return pane; | |
}, | |
// public methods for getting map state | |
getCenter: function () { // (Boolean) -> LatLng | |
this._checkIfLoaded(); | |
if (this._initialCenter && !this._moved()) { | |
return this._initialCenter; | |
} | |
return this.layerPointToLatLng(this._getCenterLayerPoint()); | |
}, | |
getZoom: function () { | |
return this._zoom; | |
}, | |
getBounds: function () { | |
var bounds = this.getPixelBounds(), | |
sw = this.unproject(bounds.getBottomLeft()), | |
ne = this.unproject(bounds.getTopRight()); | |
return new L.LatLngBounds(sw, ne); | |
}, | |
getMinZoom: function () { | |
return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom; | |
}, | |
getMaxZoom: function () { | |
return this.options.maxZoom === undefined ? | |
(this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : | |
this.options.maxZoom; | |
}, | |
getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number | |
bounds = L.latLngBounds(bounds); | |
var zoom = this.getMinZoom() - (inside ? 1 : 0), | |
maxZoom = this.getMaxZoom(), | |
size = this.getSize(), | |
nw = bounds.getNorthWest(), | |
se = bounds.getSouthEast(), | |
zoomNotFound = true, | |
boundsSize; | |
padding = L.point(padding || [0, 0]); | |
do { | |
zoom++; | |
boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding); | |
zoomNotFound = !inside ? size.contains(boundsSize) : boundsSize.x < size.x || boundsSize.y < size.y; | |
} while (zoomNotFound && zoom <= maxZoom); | |
if (zoomNotFound && inside) { | |
return null; | |
} | |
return inside ? zoom : zoom - 1; | |
}, | |
getSize: function () { | |
if (!this._size || this._sizeChanged) { | |
this._size = new L.Point( | |
this._container.clientWidth, | |
this._container.clientHeight); | |
this._sizeChanged = false; | |
} | |
return this._size.clone(); | |
}, | |
getPixelBounds: function () { | |
var topLeftPoint = this._getTopLeftPoint(); | |
return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); | |
}, | |
getPixelOrigin: function () { | |
this._checkIfLoaded(); | |
return this._initialTopLeftPoint; | |
}, | |
getPixelWorldBounds: function () { | |
return this.options.crs.getProjectedBounds(this.getZoom()); | |
}, | |
getPane: function (pane) { | |
return typeof pane === 'string' ? this._panes[pane] : pane; | |
}, | |
getPanes: function () { | |
return this._panes; | |
}, | |
getContainer: function () { | |
return this._container; | |
}, | |
// TODO replace with universal implementation after refactoring projections | |
getZoomScale: function (toZoom) { | |
var crs = this.options.crs; | |
return crs.scale(toZoom) / crs.scale(this._zoom); | |
}, | |
getScaleZoom: function (scale) { | |
return this._zoom + (Math.log(scale) / Math.LN2); | |
}, | |
// conversion methods | |
project: function (latlng, zoom) { // (LatLng[, Number]) -> Point | |
zoom = zoom === undefined ? this._zoom : zoom; | |
return this.options.crs.latLngToPoint(L.latLng(latlng), zoom); | |
}, | |
unproject: function (point, zoom) { // (Point[, Number]) -> LatLng | |
zoom = zoom === undefined ? this._zoom : zoom; | |
return this.options.crs.pointToLatLng(L.point(point), zoom); | |
}, | |
layerPointToLatLng: function (point) { // (Point) | |
var projectedPoint = L.point(point).add(this.getPixelOrigin()); | |
return this.unproject(projectedPoint); | |
}, | |
latLngToLayerPoint: function (latlng) { // (LatLng) | |
var projectedPoint = this.project(L.latLng(latlng))._round(); | |
return projectedPoint._subtract(this.getPixelOrigin()); | |
}, | |
wrapLatLng: function (latlng) { | |
return this.options.crs.wrapLatLng(L.latLng(latlng)); | |
}, | |
distance: function (latlng1, latlng2) { | |
return this.options.crs.distance(L.latLng(latlng1), L.latLng(latlng2)); | |
}, | |
containerPointToLayerPoint: function (point) { // (Point) | |
return L.point(point).subtract(this._getMapPanePos()); | |
}, | |
layerPointToContainerPoint: function (point) { // (Point) | |
return L.point(point).add(this._getMapPanePos()); | |
}, | |
containerPointToLatLng: function (point) { | |
var layerPoint = this.containerPointToLayerPoint(L.point(point)); | |
return this.layerPointToLatLng(layerPoint); | |
}, | |
latLngToContainerPoint: function (latlng) { | |
return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng))); | |
}, | |
mouseEventToContainerPoint: function (e) { // (MouseEvent) | |
return L.DomEvent.getMousePosition(e, this._container); | |
}, | |
mouseEventToLayerPoint: function (e) { // (MouseEvent) | |
return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); | |
}, | |
mouseEventToLatLng: function (e) { // (MouseEvent) | |
return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); | |
}, | |
// map initialization methods | |
_initContainer: function (id) { | |
var container = this._container = L.DomUtil.get(id); | |
if (!container) { | |
throw new Error('Map container not found.'); | |
} else if (container._leaflet) { | |
throw new Error('Map container is already initialized.'); | |
} | |
container._leaflet = true; | |
}, | |
_initLayout: function () { | |
var container = this._container; | |
this._fadeAnimated = this.options.fadeAnimation && L.Browser.any3d; | |
L.DomUtil.addClass(container, 'leaflet-container' + | |
(L.Browser.touch ? ' leaflet-touch' : '') + | |
(L.Browser.retina ? ' leaflet-retina' : '') + | |
(L.Browser.ielt9 ? ' leaflet-oldie' : '') + | |
(L.Browser.safari ? ' leaflet-safari' : '') + | |
(this._fadeAnimated ? ' leaflet-fade-anim' : '')); | |
var position = L.DomUtil.getStyle(container, 'position'); | |
if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') { | |
container.style.position = 'relative'; | |
} | |
this._initPanes(); | |
if (this._initControlPos) { | |
this._initControlPos(); | |
} | |
}, | |
_initPanes: function () { | |
var panes = this._panes = {}; | |
this._mapPane = this.createPane('mapPane', this._container); | |
this.createPane('tilePane'); | |
this.createPane('shadowPane'); | |
this.createPane('overlayPane'); | |
this.createPane('markerPane'); | |
this.createPane('popupPane'); | |
if (!this.options.markerZoomAnimation) { | |
L.DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide'); | |
L.DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide'); | |
} | |
}, | |
// private methods that modify map state | |
_resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) { | |
var zoomChanged = (this._zoom !== zoom); | |
if (!afterZoomAnim) { | |
this.fire('movestart'); | |
if (zoomChanged) { | |
this.fire('zoomstart'); | |
} | |
} | |
this._zoom = zoom; | |
this._initialCenter = center; | |
this._initialTopLeftPoint = this._getNewTopLeftPoint(center); | |
if (!preserveMapOffset) { | |
L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); | |
} else { | |
this._initialTopLeftPoint._add(this._getMapPanePos()); | |
} | |
var loading = !this._loaded; | |
this._loaded = true; | |
this.fire('viewreset', {hard: !preserveMapOffset}); | |
if (loading) { | |
this.fire('load'); | |
} | |
this.fire('move'); | |
if (zoomChanged || afterZoomAnim) { | |
this.fire('zoomend'); | |
} | |
this.fire('moveend', {hard: !preserveMapOffset}); | |
}, | |
_rawPanBy: function (offset) { | |
L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); | |
}, | |
_getZoomSpan: function () { | |
return this.getMaxZoom() - this.getMinZoom(); | |
}, | |
_panInsideMaxBounds: function () { | |
this.panInsideBounds(this.options.maxBounds); | |
}, | |
_checkIfLoaded: function () { | |
if (!this._loaded) { | |
throw new Error('Set map center and zoom first.'); | |
} | |
}, | |
// map events | |
_initEvents: function (onOff) { | |
if (!L.DomEvent) { return; } | |
onOff = onOff || 'on'; | |
L.DomEvent[onOff](this._container, | |
'click dblclick mousedown mouseup mouseenter mouseleave mousemove contextmenu', | |
this._handleMouseEvent, this); | |
if (this.options.trackResize) { | |
L.DomEvent[onOff](window, 'resize', this._onResize, this); | |
} | |
}, | |
_onResize: function () { | |
L.Util.cancelAnimFrame(this._resizeRequest); | |
this._resizeRequest = L.Util.requestAnimFrame( | |
function () { this.invalidateSize({debounceMoveend: true}); }, this, false, this._container); | |
}, | |
_handleMouseEvent: function (e) { | |
if (!this._loaded) { return; } | |
this._fireMouseEvent(this, e, | |
e.type === 'mouseenter' ? 'mouseover' : | |
e.type === 'mouseleave' ? 'mouseout' : e.type); | |
}, | |
_fireMouseEvent: function (obj, e, type, propagate, latlng) { | |
type = type || e.type; | |
if (L.DomEvent._skipped(e)) { return; } | |
if (type === 'click') { | |
if (!e._simulated && ((this.dragging && this.dragging.moved()) || | |
(this.boxZoom && this.boxZoom.moved()))) { return; } | |
obj.fire('preclick'); | |
} | |
if (!obj.listens(type, propagate)) { return; } | |
if (type === 'contextmenu') { | |
L.DomEvent.preventDefault(e); | |
} | |
if (type === 'click' || type === 'dblclick' || type === 'contextmenu') { | |
L.DomEvent.stopPropagation(e); | |
} | |
var data = { | |
originalEvent: e, | |
containerPoint: this.mouseEventToContainerPoint(e) | |
}; | |
data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); | |
data.latlng = latlng || this.layerPointToLatLng(data.layerPoint); | |
obj.fire(type, data, propagate); | |
}, | |
_clearHandlers: function () { | |
for (var i = 0, len = this._handlers.length; i < len; i++) { | |
this._handlers[i].disable(); | |
} | |
}, | |
whenReady: function (callback, context) { | |
if (this._loaded) { | |
callback.call(context || this, {target: this}); | |
} else { | |
this.on('load', callback, context); | |
} | |
return this; | |
}, | |
// private methods for getting map state | |
_getMapPanePos: function () { | |
return L.DomUtil.getPosition(this._mapPane); | |
}, | |
_moved: function () { | |
var pos = this._getMapPanePos(); | |
return pos && !pos.equals([0, 0]); | |
}, | |
_getTopLeftPoint: function () { | |
return this.getPixelOrigin().subtract(this._getMapPanePos()); | |
}, | |
_getNewTopLeftPoint: function (center, zoom) { | |
var viewHalf = this.getSize()._divideBy(2); | |
// TODO round on display, not calculation to increase precision? | |
return this.project(center, zoom)._subtract(viewHalf)._round(); | |
}, | |
_latLngToNewLayerPoint: function (latlng, newZoom, newCenter) { | |
var topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(this._getMapPanePos()); | |
return this.project(latlng, newZoom)._subtract(topLeft); | |
}, | |
// layer point of the current center | |
_getCenterLayerPoint: function () { | |
return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); | |
}, | |
// offset of the specified place to the current center in pixels | |
_getCenterOffset: function (latlng) { | |
return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint()); | |
}, | |
// adjust center for view to get inside bounds | |
_limitCenter: function (center, zoom, bounds) { | |
if (!bounds) { return center; } | |
var centerPoint = this.project(center, zoom), | |
viewHalf = this.getSize().divideBy(2), | |
viewBounds = new L.Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), | |
offset = this._getBoundsOffset(viewBounds, bounds, zoom); | |
return this.unproject(centerPoint.add(offset), zoom); | |
}, | |
// adjust offset for view to get inside bounds | |
_limitOffset: function (offset, bounds) { | |
if (!bounds) { return offset; } | |
var viewBounds = this.getPixelBounds(), | |
newBounds = new L.Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset)); | |
return offset.add(this._getBoundsOffset(newBounds, bounds)); | |
}, | |
// returns offset needed for pxBounds to get inside maxBounds at a specified zoom | |
_getBoundsOffset: function (pxBounds, maxBounds, zoom) { | |
var nwOffset = this.project(maxBounds.getNorthWest(), zoom).subtract(pxBounds.min), | |
seOffset = this.project(maxBounds.getSouthEast(), zoom).subtract(pxBounds.max), | |
dx = this._rebound(nwOffset.x, -seOffset.x), | |
dy = this._rebound(nwOffset.y, -seOffset.y); | |
return new L.Point(dx, dy); | |
}, | |
_rebound: function (left, right) { | |
return left + right > 0 ? | |
Math.round(left - right) / 2 : | |
Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right)); | |
}, | |
_limitZoom: function (zoom) { | |
var min = this.getMinZoom(), | |
max = this.getMaxZoom(); | |
return Math.max(min, Math.min(max, zoom)); | |
} | |
}); | |
L.map = function (id, options) { | |
return new L.Map(id, options); | |
}; | |
L.Layer = L.Evented.extend({ | |
options: { | |
pane: 'overlayPane' | |
}, | |
addTo: function (map) { | |
map.addLayer(this); | |
return this; | |
}, | |
remove: function () { | |
return this.removeFrom(this._map || this._mapToAdd); | |
}, | |
removeFrom: function (obj) { | |
if (obj) { | |
obj.removeLayer(this); | |
} | |
return this; | |
}, | |
getPane: function (name) { | |
return this._map.getPane(name ? (this.options[name] || name) : this.options.pane); | |
}, | |
_layerAdd: function (e) { | |
var map = e.target; | |
// check in case layer gets added and then removed before the map is ready | |
if (!map.hasLayer(this)) { return; } | |
this._map = map; | |
this._zoomAnimated = map._zoomAnimated; | |
if (this.getEvents) { | |
map.on(this.getEvents(), this); | |
} | |
if (this.getAttribution && this._map.attributionControl) { | |
this._map.attributionControl.addAttribution(this.getAttribution()); | |
} | |
this.onAdd(map); | |
this.fire('add'); | |
map.fire('layeradd', {layer: this}); | |
} | |
}); | |
L.Map.include({ | |
addLayer: function (layer) { | |
var id = L.stamp(layer); | |
if (this._layers[id]) { return layer; } | |
this._layers[id] = layer; | |
layer._mapToAdd = this; | |
if (layer.beforeAdd) { | |
layer.beforeAdd(this); | |
} | |
this.whenReady(layer._layerAdd, layer); | |
return this; | |
}, | |
removeLayer: function (layer) { | |
var id = L.stamp(layer); | |
if (!this._layers[id]) { return this; } | |
if (this._loaded) { | |
layer.onRemove(this); | |
} | |
if (layer.getAttribution && this.attributionControl) { | |
this.attributionControl.removeAttribution(layer.getAttribution()); | |
} | |
if (layer.getEvents) { | |
this.off(layer.getEvents(), layer); | |
} | |
delete this._layers[id]; | |
if (this._loaded) { | |
this.fire('layerremove', {layer: layer}); | |
layer.fire('remove'); | |
} | |
layer._map = layer._mapToAdd = null; | |
return this; | |
}, | |
hasLayer: function (layer) { | |
return !!layer && (L.stamp(layer) in this._layers); | |
}, | |
eachLayer: function (method, context) { | |
for (var i in this._layers) { | |
method.call(context, this._layers[i]); | |
} | |
return this; | |
}, | |
_addLayers: function (layers) { | |
layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : []; | |
for (var i = 0, len = layers.length; i < len; i++) { | |
this.addLayer(layers[i]); | |
} | |
}, | |
_addZoomLimit: function (layer) { | |
if (isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) { | |
this._zoomBoundLayers[L.stamp(layer)] = layer; | |
this._updateZoomLevels(); | |
} | |
}, | |
_removeZoomLimit: function (layer) { | |
var id = L.stamp(layer); | |
if (this._zoomBoundLayers[id]) { | |
delete this._zoomBoundLayers[id]; | |
this._updateZoomLevels(); | |
} | |
}, | |
_updateZoomLevels: function () { | |
var minZoom = Infinity, | |
maxZoom = -Infinity, | |
oldZoomSpan = this._getZoomSpan(); | |
for (var i in this._zoomBoundLayers) { | |
var options = this._zoomBoundLayers[i].options; | |
minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom); | |
maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom); | |
} | |
this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom; | |
this._layersMinZoom = minZoom === Infinity ? undefined : minZoom; | |
if (oldZoomSpan !== this._getZoomSpan()) { | |
this.fire('zoomlevelschange'); | |
} | |
} | |
}); | |
/* | |
* Mercator projection that takes into account that the Earth is not a perfect sphere. | |
* Less popular than spherical mercator; used by projections like EPSG:3395. | |
*/ | |
L.Projection.Mercator = { | |
R: 6378137, | |
R_MINOR: 6356752.314245179, | |
bounds: L.bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]), | |
project: function (latlng) { | |
var d = Math.PI / 180, | |
r = this.R, | |
y = latlng.lat * d, | |
tmp = this.R_MINOR / r, | |
e = Math.sqrt(1 - tmp * tmp), | |
con = e * Math.sin(y); | |
var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2); | |
y = -r * Math.log(Math.max(ts, 1E-10)); | |
return new L.Point(latlng.lng * d * r, y); | |
}, | |
unproject: function (point) { | |
var d = 180 / Math.PI, | |
r = this.R, | |
tmp = this.R_MINOR / r, | |
e = Math.sqrt(1 - tmp * tmp), | |
ts = Math.exp(-point.y / r), | |
phi = Math.PI / 2 - 2 * Math.atan(ts); | |
for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) { | |
con = e * Math.sin(phi); | |
con = Math.pow((1 - con) / (1 + con), e / 2); | |
dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi; | |
phi += dphi; | |
} | |
return new L.LatLng(phi * d, point.x * d / r); | |
} | |
}; | |
/* | |
* L.CRS.EPSG3857 (World Mercator) CRS implementation. | |
*/ | |
L.CRS.EPSG3395 = L.extend({}, L.CRS.Earth, { | |
code: 'EPSG:3395', | |
projection: L.Projection.Mercator, | |
transformation: (function () { | |
var scale = 0.5 / (Math.PI * L.Projection.Mercator.R); | |
return new L.Transformation(scale, 0.5, -scale, 0.5); | |
}()) | |
}); | |
/* | |
* L.GridLayer is used as base class for grid-like layers like TileLayer. | |
*/ | |
L.GridLayer = L.Layer.extend({ | |
options: { | |
pane: 'tilePane', | |
tileSize: 256, | |
opacity: 1, | |
unloadInvisibleTiles: L.Browser.mobile, | |
updateWhenIdle: L.Browser.mobile, | |
updateInterval: 150 | |
/* | |
minZoom: <Number>, | |
maxZoom: <Number>, | |
attribution: <String>, | |
zIndex: <Number>, | |
bounds: <LatLngBounds> | |
*/ | |
}, | |
initialize: function (options) { | |
options = L.setOptions(this, options); | |
}, | |
onAdd: function () { | |
this._initContainer(); | |
if (!this.options.updateWhenIdle) { | |
// update tiles on move, but not more often than once per given interval | |
this._update = L.Util.throttle(this._update, this.options.updateInterval, this); | |
} | |
this._reset(); | |
this._update(); | |
}, | |
beforeAdd: function (map) { | |
map._addZoomLimit(this); | |
}, | |
onRemove: function (map) { | |
this._clearBgBuffer(); | |
L.DomUtil.remove(this._container); | |
map._removeZoomLimit(this); | |
this._container = null; | |
}, | |
bringToFront: function () { | |
if (this._map) { | |
L.DomUtil.toFront(this._container); | |
this._setAutoZIndex(Math.max); | |
} | |
return this; | |
}, | |
bringToBack: function () { | |
if (this._map) { | |
L.DomUtil.toBack(this._container); | |
this._setAutoZIndex(Math.min); | |
} | |
return this; | |
}, | |
getAttribution: function () { | |
return this.options.attribution; | |
}, | |
getContainer: function () { | |
return this._container; | |
}, | |
setOpacity: function (opacity) { | |
this.options.opacity = opacity; | |
if (this._map) { | |
this._updateOpacity(); | |
} | |
return this; | |
}, | |
setZIndex: function (zIndex) { | |
this.options.zIndex = zIndex; | |
this._updateZIndex(); | |
return this; | |
}, | |
redraw: function () { | |
if (this._map) { | |
this._reset({hard: true}); | |
this._update(); | |
} | |
return this; | |
}, | |
getEvents: function () { | |
var events = { | |
viewreset: this._reset, | |
moveend: this._update | |
}; | |
if (!this.options.updateWhenIdle) { | |
events.move = this._update; | |
} | |
if (this._zoomAnimated) { | |
events.zoomstart = this._startZoomAnim; | |
events.zoomanim = this._animateZoom; | |
events.zoomend = this._endZoomAnim; | |
} | |
return events; | |
}, | |
_updateZIndex: function () { | |
if (this._container && this.options.zIndex !== undefined) { | |
this._container.style.zIndex = this.options.zIndex; | |
} | |
}, | |
_setAutoZIndex: function (compare) { | |
// go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back) | |
var layers = this.getPane().children, | |
edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min | |
for (var i = 0, len = layers.length, zIndex; i < len; i++) { | |
zIndex = layers[i].style.zIndex; | |
if (layers[i] !== this._container && zIndex) { | |
edgeZIndex = compare(edgeZIndex, +zIndex); | |
} | |
} | |
if (isFinite(edgeZIndex)) { | |
this.options.zIndex = edgeZIndex + compare(-1, 1); | |
this._updateZIndex(); | |
} | |
}, | |
_updateOpacity: function () { | |
var opacity = this.options.opacity; | |
if (L.Browser.ielt9) { | |
// IE doesn't inherit filter opacity properly, so we're forced to set it on tiles | |
for (var i in this._tiles) { | |
L.DomUtil.setOpacity(this._tiles[i], opacity); | |
} | |
} else { | |
L.DomUtil.setOpacity(this._container, opacity); | |
} | |
}, | |
_initContainer: function () { | |
if (this._container) { return; } | |
this._container = L.DomUtil.create('div', 'leaflet-layer'); | |
this._updateZIndex(); | |
if (this._zoomAnimated) { | |
var className = 'leaflet-tile-container leaflet-zoom-animated'; | |
this._bgBuffer = L.DomUtil.create('div', className, this._container); | |
this._tileContainer = L.DomUtil.create('div', className, this._container); | |
L.DomUtil.setTransform(this._tileContainer); | |
} else { | |
this._tileContainer = this._container; | |
} | |
if (this.options.opacity < 1) { | |
this._updateOpacity(); | |
} | |
this.getPane().appendChild(this._container); | |
}, | |
_reset: function (e) { | |
for (var key in this._tiles) { | |
this.fire('tileunload', { | |
tile: this._tiles[key] | |
}); | |
} | |
this._tiles = {}; | |
this._tilesToLoad = 0; | |
this._tilesTotal = 0; | |
this._tileContainer.innerHTML = ''; | |
if (this._zoomAnimated && e && e.hard) { | |
this._clearBgBuffer(); | |
} | |
this._tileNumBounds = this._getTileNumBounds(); | |
this._resetWrap(); | |
}, | |
_resetWrap: function () { | |
var map = this._map, | |
crs = map.options.crs; | |
if (crs.infinite) { return; } | |
var tileSize = this._getTileSize(); | |
if (crs.wrapLng) { | |
this._wrapLng = [ | |
Math.floor(map.project([0, crs.wrapLng[0]]).x / tileSize), | |
Math.ceil(map.project([0, crs.wrapLng[1]]).x / tileSize) | |
]; | |
} | |
if (crs.wrapLat) { | |
this._wrapLat = [ | |
Math.floor(map.project([crs.wrapLat[0], 0]).y / tileSize), | |
Math.ceil(map.project([crs.wrapLat[1], 0]).y / tileSize) | |
]; | |
} | |
}, | |
_getTileSize: function () { | |
return this.options.tileSize; | |
}, | |
_update: function () { | |
if (!this._map) { return; } | |
var bounds = this._map.getPixelBounds(), | |
zoom = this._map.getZoom(), | |
tileSize = this._getTileSize(); | |
if (zoom > this.options.maxZoom || | |
zoom < this.options.minZoom) { return; } | |
// tile coordinates range for the current view | |
var tileBounds = L.bounds( | |
bounds.min.divideBy(tileSize).floor(), | |
bounds.max.divideBy(tileSize).floor()); | |
this._addTiles(tileBounds); | |
if (this.options.unloadInvisibleTiles) { | |
this._removeOtherTiles(tileBounds); | |
} | |
}, | |
_addTiles: function (bounds) { | |
var queue = [], | |
center = bounds.getCenter(), | |
zoom = this._map.getZoom(); | |
var j, i, coords; | |
// create a queue of coordinates to load tiles from | |
for (j = bounds.min.y; j <= bounds.max.y; j++) { | |
for (i = bounds.min.x; i <= bounds.max.x; i++) { | |
coords = new L.Point(i, j); | |
coords.z = zoom; | |
// add tile to queue if it's not in cache or out of bounds | |
if (!(this._tileCoordsToKey(coords) in this._tiles) && this._isValidTile(coords)) { | |
queue.push(coords); | |
} | |
} | |
} | |
var tilesToLoad = queue.length; | |
if (tilesToLoad === 0) { return; } | |
// if its the first batch of tiles to load | |
if (!this._tilesToLoad) { | |
this.fire('loading'); | |
} | |
this._tilesToLoad += tilesToLoad; | |
this._tilesTotal += tilesToLoad; | |
// sort tile queue to load tiles in order of their distance to center | |
queue.sort(function (a, b) { | |
return a.distanceTo(center) - b.distanceTo(center); | |
}); | |
// create DOM fragment to append tiles in one batch | |
var fragment = document.createDocumentFragment(); | |
for (i = 0; i < tilesToLoad; i++) { | |
this._addTile(queue[i], fragment); | |
} | |
this._tileContainer.appendChild(fragment); | |
}, | |
_isValidTile: function (coords) { | |
var crs = this._map.options.crs; | |
if (!crs.infinite) { | |
// don't load tile if it's out of bounds and not wrapped | |
var bounds = this._tileNumBounds; | |
if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) || | |
(!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; } | |
} | |
if (!this.options.bounds) { return true; } | |
// don't load tile if it doesn't intersect the bounds in options | |
var tileBounds = this._tileCoordsToBounds(coords); | |
return L.latLngBounds(this.options.bounds).intersects(tileBounds); | |
}, | |
// converts tile coordinates to its geographical bounds | |
_tileCoordsToBounds: function (coords) { | |
var map = this._map, | |
tileSize = this.options.tileSize, | |
nwPoint = coords.multiplyBy(tileSize), | |
sePoint = nwPoint.add([tileSize, tileSize]), | |
nw = map.wrapLatLng(map.unproject(nwPoint, coords.z)), | |
se = map.wrapLatLng(map.unproject(sePoint, coords.z)); | |
return new L.LatLngBounds(nw, se); | |
}, | |
// converts tile coordinates to key for the tile cache | |
_tileCoordsToKey: function (coords) { | |
return coords.x + ':' + coords.y; | |
}, | |
// converts tile cache key to coordinates | |
_keyToTileCoords: function (key) { | |
var kArr = key.split(':'), | |
x = parseInt(kArr[0], 10), | |
y = parseInt(kArr[1], 10); | |
return new L.Point(x, y); | |
}, | |
// remove any present tiles that are off the specified bounds | |
_removeOtherTiles: function (bounds) { | |
for (var key in this._tiles) { | |
if (!bounds.contains(this._keyToTileCoords(key))) { | |
this._removeTile(key); | |
} | |
} | |
}, | |
_removeTile: function (key) { | |
var tile = this._tiles[key]; | |
L.DomUtil.remove(tile); | |
delete this._tiles[key]; | |
this.fire('tileunload', {tile: tile}); | |
}, | |
_initTile: function (tile) { | |
var size = this._getTileSize(); | |
L.DomUtil.addClass(tile, 'leaflet-tile'); | |
tile.style.width = size + 'px'; | |
tile.style.height = size + 'px'; | |
tile.onselectstart = L.Util.falseFn; | |
tile.onmousemove = L.Util.falseFn; | |
// update opacity on tiles in IE7-8 because of filter inheritance problems | |
if (L.Browser.ielt9 && this.options.opacity < 1) { | |
L.DomUtil.setOpacity(tile, this.options.opacity); | |
} | |
// without this hack, tiles disappear after zoom on Chrome for Android | |
// https://github.com/Leaflet/Leaflet/issues/2078 | |
if (L.Browser.android && !L.Browser.android23) { | |
tile.style.WebkitBackfaceVisibility = 'hidden'; | |
} | |
}, | |
_addTile: function (coords, container) { | |
var tilePos = this._getTilePos(coords); | |
// wrap tile coords if necessary (depending on CRS) | |
this._wrapCoords(coords); | |
var tile = this.createTile(coords, L.bind(this._tileReady, this)); | |
this._initTile(tile); | |
// if createTile is defined with a second argument ("done" callback), | |
// we know that tile is async and will be ready later; otherwise | |
if (this.createTile.length < 2) { | |
// mark tile as ready, but delay one frame for opacity animation to happen | |
setTimeout(L.bind(this._tileReady, this, null, tile), 0); | |
} | |
// we prefer top/left over translate3d so that we don't create a HW-accelerated layer from each tile | |
// which is slow, and it also fixes gaps between tiles in Safari | |
L.DomUtil.setPosition(tile, tilePos, true); | |
// save tile in cache | |
this._tiles[this._tileCoordsToKey(coords)] = tile; | |
container.appendChild(tile); | |
this.fire('tileloadstart', {tile: tile}); | |
}, | |
_tileReady: function (err, tile) { | |
if (err) { | |
this.fire('tileerror', { | |
error: err, | |
tile: tile | |
}); | |
} | |
L.DomUtil.addClass(tile, 'leaflet-tile-loaded'); | |
this.fire('tileload', {tile: tile}); | |
this._tilesToLoad--; | |
if (this._tilesToLoad === 0) { | |
this._visibleTilesReady(); | |
} | |
}, | |
_visibleTilesReady: function () { | |
this.fire('load'); | |
if (this._zoomAnimated) { | |
// clear scaled tiles after all new tiles are loaded (for performance) | |
clearTimeout(this._clearBgBufferTimer); | |
this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 300); | |
} | |
}, | |
_getTilePos: function (coords) { | |
return coords | |
.multiplyBy(this._getTileSize()) | |
.subtract(this._map.getPixelOrigin()); | |
}, | |
_wrapCoords: function (coords) { | |
coords.x = this._wrapLng ? L.Util.wrapNum(coords.x, this._wrapLng) : coords.x; | |
coords.y = this._wrapLat ? L.Util.wrapNum(coords.y, this._wrapLat) : coords.y; | |
}, | |
// get the global tile coordinates range for the current zoom | |
_getTileNumBounds: function () { | |
var bounds = this._map.getPixelWorldBounds(), | |
size = this._getTileSize(); | |
return bounds ? L.bounds( | |
bounds.min.divideBy(size).floor(), | |
bounds.max.divideBy(size).ceil().subtract([1, 1])) : null; | |
}, | |
_startZoomAnim: function () { | |
this._prepareBgBuffer(); | |
this._prevTranslate = this._translate || new L.Point(0, 0); | |
this._prevScale = this._scale; | |
}, | |
_animateZoom: function (e) { | |
// avoid stacking transforms by calculating cumulating translate/scale sequence | |
this._translate = this._prevTranslate.multiplyBy(e.scale).add(e.origin.multiplyBy(1 - e.scale)); | |
this._scale = this._prevScale * e.scale; | |
L.DomUtil.setTransform(this._bgBuffer, this._translate, this._scale); | |
}, | |
_endZoomAnim: function () { | |
var front = this._tileContainer; | |
front.style.visibility = ''; | |
L.DomUtil.toFront(front); // bring to front | |
}, | |
_clearBgBuffer: function () { | |
var map = this._map, | |
bg = this._bgBuffer; | |
if (map && !map._animatingZoom && !map.touchZoom._zooming && bg) { | |
bg.innerHTML = ''; | |
L.DomUtil.setTransform(bg); | |
} | |
}, | |
_prepareBgBuffer: function () { | |
var front = this._tileContainer, | |
bg = this._bgBuffer; | |
if (this._abortLoading) { | |
this._abortLoading(); | |
} | |
if (this._tilesToLoad / this._tilesTotal > 0.5) { | |
// if foreground layer doesn't have many tiles loaded, | |
// keep the existing bg layer and just zoom it some more | |
front.style.visibility = 'hidden'; | |
return; | |
} | |
// prepare the buffer to become the front tile pane | |
bg.style.visibility = 'hidden'; | |
L.DomUtil.setTransform(bg); | |
// switch out the current layer to be the new bg layer (and vice-versa) | |
this._tileContainer = bg; | |
this._bgBuffer = front; | |
// reset bg layer transform info | |
this._translate = new L.Point(0, 0); | |
this._scale = 1; | |
// prevent bg buffer from clearing right after zoom | |
clearTimeout(this._clearBgBufferTimer); | |
} | |
}); | |
L.gridLayer = function (options) { | |
return new L.GridLayer(options); | |
}; | |
/* | |
* L.TileLayer is used for standard xyz-numbered tile layers. | |
*/ | |
L.TileLayer = L.GridLayer.extend({ | |
options: { | |
minZoom: 0, | |
maxZoom: 18, | |
subdomains: 'abc', | |
// errorTileUrl: '', | |
zoomOffset: 0 | |
/* | |
maxNativeZoom: <Number>, | |
tms: <Boolean>, | |
zoomReverse: <Number>, | |
detectRetina: <Number>, | |
*/ | |
}, | |
initialize: function (url, options) { | |
this._url = url; | |
options = L.setOptions(this, options); | |
// detecting retina displays, adjusting tileSize and zoom levels | |
if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) { | |
options.tileSize = Math.floor(options.tileSize / 2); | |
options.zoomOffset++; | |
options.minZoom = Math.max(0, options.minZoom); | |
options.maxZoom--; | |
} | |
if (typeof options.subdomains === 'string') { | |
options.subdomains = options.subdomains.split(''); | |
} | |
}, | |
setUrl: function (url, noRedraw) { | |
this._url = url; | |
if (!noRedraw) { | |
this.redraw(); | |
} | |
return this; | |
}, | |
createTile: function (coords, done) { | |
var tile = document.createElement('img'); | |
tile.onload = L.bind(this._tileOnLoad, this, done, tile); | |
tile.onerror = L.bind(this._tileOnError, this, done, tile); | |
/* | |
Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons | |
http://www.w3.org/TR/WCAG20-TECHS/H67 | |
*/ | |
tile.alt = ''; | |
tile.src = this.getTileUrl(coords); | |
return tile; | |
}, | |
getTileUrl: function (coords) { | |
return L.Util.template(this._url, L.extend({ | |
r: this.options.detectRetina && L.Browser.retina && this.options.maxZoom > 0 ? '@2x' : '', | |
s: this._getSubdomain(coords), | |
x: coords.x, | |
y: this.options.tms ? this._tileNumBounds.max.y - coords.y : coords.y, | |
z: this._getZoomForUrl() | |
}, this.options)); | |
}, | |
_tileOnLoad: function (done, tile) { | |
done(null, tile); | |
}, | |
_tileOnError: function (done, tile, e) { | |
var errorUrl = this.options.errorTileUrl; | |
if (errorUrl) { | |
tile.src = errorUrl; | |
} | |
done(e, tile); | |
}, | |
_getTileSize: function () { | |
var map = this._map, | |
options = this.options, | |
zoom = map.getZoom() + options.zoomOffset, | |
zoomN = options.maxNativeZoom; | |
// increase tile size when overscaling | |
return zoomN && zoom > zoomN ? | |
Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * options.tileSize) : | |
options.tileSize; | |
}, | |
_removeTile: function (key) { | |
var tile = this._tiles[key]; | |
L.GridLayer.prototype._removeTile.call(this, key); | |
// for https://github.com/Leaflet/Leaflet/issues/137 | |
if (!L.Browser.android) { | |
tile.onload = null; | |
tile.src = L.Util.emptyImageUrl; | |
} | |
}, | |
_getZoomForUrl: function () { | |
var options = this.options, | |
zoom = this._map.getZoom(); | |
if (options.zoomReverse) { | |
zoom = options.maxZoom - zoom; | |
} | |
zoom += options.zoomOffset; | |
return options.maxNativeZoom ? Math.min(zoom, options.maxNativeZoom) : zoom; | |
}, | |
_getSubdomain: function (tilePoint) { | |
var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length; | |
return this.options.subdomains[index]; | |
}, | |
// stops loading all tiles in the background layer | |
_abortLoading: function () { | |
var i, tile; | |
for (i in this._tiles) { | |
tile = this._tiles[i]; | |
if (!tile.complete) { | |
tile.onload = L.Util.falseFn; | |
tile.onerror = L.Util.falseFn; | |
tile.src = L.Util.emptyImageUrl; | |
L.DomUtil.remove(tile); | |
} | |
} | |
} | |
}); | |
L.tileLayer = function (url, options) { | |
return new L.TileLayer(url, options); | |
}; | |
/* | |
* L.TileLayer.WMS is used for WMS tile layers. | |
*/ | |
L.TileLayer.WMS = L.TileLayer.extend({ | |
defaultWmsParams: { | |
service: 'WMS', | |
request: 'GetMap', | |
version: '1.1.1', | |
layers: '', | |
styles: '', | |
format: 'image/jpeg', | |
transparent: false | |
}, | |
initialize: function (url, options) { | |
this._url = url; | |
var wmsParams = L.extend({}, this.defaultWmsParams); | |
// all keys that are not TileLayer options go to WMS params | |
for (var i in options) { | |
if (!this.options.hasOwnProperty(i) && i !== 'crs') { | |
wmsParams[i] = options[i]; | |
} | |
} | |
options = L.setOptions(this, options); | |
wmsParams.width = wmsParams.height = | |
options.tileSize * (options.detectRetina && L.Browser.retina ? 2 : 1); | |
this.wmsParams = wmsParams; | |
}, | |
onAdd: function (map) { | |
this._crs = this.options.crs || map.options.crs; | |
this._wmsVersion = parseFloat(this.wmsParams.version); | |
var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs'; | |
this.wmsParams[projectionKey] = this._crs.code; | |
L.TileLayer.prototype.onAdd.call(this, map); | |
}, | |
getTileUrl: function (coords) { | |
var tileBounds = this._tileCoordsToBounds(coords), | |
nw = this._crs.project(tileBounds.getNorthWest()), | |
se = this._crs.project(tileBounds.getSouthEast()), | |
bbox = (this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ? | |
[se.y, nw.x, nw.y, se.x] : | |
[nw.x, se.y, se.x, nw.y]).join(','), | |
url = L.Util.template(this._url, {s: this._getSubdomain(coords)}); | |
return url + L.Util.getParamString(this.wmsParams, url, true) + '&BBOX=' + bbox; | |
}, | |
setParams: function (params, noRedraw) { | |
L.extend(this.wmsParams, params); | |
if (!noRedraw) { | |
this.redraw(); | |
} | |
return this; | |
} | |
}); | |
L.tileLayer.wms = function (url, options) { | |
return new L.TileLayer.WMS(url, options); | |
}; | |
/* | |
* L.ImageOverlay is used to overlay images over the map (to specific geographical bounds). | |
*/ | |
L.ImageOverlay = L.Layer.extend({ | |
options: { | |
opacity: 1 | |
}, | |
initialize: function (url, bounds, options) { // (String, LatLngBounds, Object) | |
this._url = url; | |
this._bounds = L.latLngBounds(bounds); | |
L.setOptions(this, options); | |
}, | |
onAdd: function () { | |
if (!this._image) { | |
this._initImage(); | |
if (this.options.opacity < 1) { | |
this._updateOpacity(); | |
} | |
} | |
this.getPane().appendChild(this._image); | |
this._reset(); | |
}, | |
onRemove: function () { | |
L.DomUtil.remove(this._image); | |
}, | |
setOpacity: function (opacity) { | |
this.options.opacity = opacity; | |
if (this._image) { | |
this._updateOpacity(); | |
} | |
return this; | |
}, | |
bringToFront: function () { | |
if (this._map) { | |
L.DomUtil.toFront(this._image); | |
} | |
return this; | |
}, | |
bringToBack: function () { | |
if (this._map) { | |
L.DomUtil.toBack(this._image); | |
} | |
return this; | |
}, | |
setUrl: function (url) { | |
this._url = url; | |
if (this._image) { | |
this._image.src = url; | |
} | |
return this; | |
}, | |
getAttribution: function () { | |
return this.options.attribution; | |
}, | |
getEvents: function () { | |
var events = { | |
viewreset: this._reset | |
}; | |
if (this._zoomAnimated) { | |
events.zoomanim = this._animateZoom; | |
} | |
return events; | |
}, | |
_initImage: function () { | |
var img = this._image = L.DomUtil.create('img', | |
'leaflet-image-layer ' + (this._zoomAnimated ? 'leaflet-zoom-animated' : '')); | |
img.onselectstart = L.Util.falseFn; | |
img.onmousemove = L.Util.falseFn; | |
img.onload = L.bind(this.fire, this, 'load'); | |
img.src = this._url; | |
}, | |
_animateZoom: function (e) { | |
var topLeft = this._map._latLngToNewLayerPoint(this._bounds.getNorthWest(), e.zoom, e.center), | |
size = this._map._latLngToNewLayerPoint(this._bounds.getSouthEast(), e.zoom, e.center).subtract(topLeft), | |
offset = topLeft.add(size._multiplyBy((1 - 1 / e.scale) / 2)); | |
L.DomUtil.setTransform(this._image, offset, e.scale); | |
}, | |
_reset: function () { | |
var image = this._image, | |
bounds = new L.Bounds( | |
this._map.latLngToLayerPoint(this._bounds.getNorthWest()), | |
this._map.latLngToLayerPoint(this._bounds.getSouthEast())), | |
size = bounds.getSize(); | |
L.DomUtil.setPosition(image, bounds.min); | |
image.style.width = size.x + 'px'; | |
image.style.height = size.y + 'px'; | |
}, | |
_updateOpacity: function () { | |
L.DomUtil.setOpacity(this._image, this.options.opacity); | |
} | |
}); | |
L.imageOverlay = function (url, bounds, options) { | |
return new L.ImageOverlay(url, bounds, options); | |
}; | |
/* | |
* L.Icon is an image-based icon class that you can use with L.Marker for custom markers. | |
*/ | |
L.Icon = L.Class.extend({ | |
/* | |
options: { | |
iconUrl: (String) (required) | |
iconRetinaUrl: (String) (optional, used for retina devices if detected) | |
iconSize: (Point) (can be set through CSS) | |
iconAnchor: (Point) (centered by default, can be set in CSS with negative margins) | |
popupAnchor: (Point) (if not specified, popup opens in the anchor point) | |
shadowUrl: (String) (no shadow by default) | |
shadowRetinaUrl: (String) (optional, used for retina devices if detected) | |
shadowSize: (Point) | |
shadowAnchor: (Point) | |
className: (String) | |
}, | |
*/ | |
initialize: function (options) { | |
L.setOptions(this, options); | |
}, | |
createIcon: function (oldIcon) { | |
return this._createIcon('icon', oldIcon); | |
}, | |
createShadow: function (oldIcon) { | |
return this._createIcon('shadow', oldIcon); | |
}, | |
_createIcon: function (name, oldIcon) { | |
var src = this._getIconUrl(name); | |
if (!src) { | |
if (name === 'icon') { | |
throw new Error('iconUrl not set in Icon options (see the docs).'); | |
} | |
return null; | |
} | |
var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null); | |
this._setIconStyles(img, name); | |
return img; | |
}, | |
_setIconStyles: function (img, name) { | |
var options = this.options, | |
size = L.point(options[name + 'Size']), | |
anchor = L.point(name === 'shadow' && options.shadowAnchor || options.iconAnchor || | |
size && size.divideBy(2, true)); | |
img.className = 'leaflet-marker-' + name + ' ' + (options.className || ''); | |
if (anchor) { | |
img.style.marginLeft = (-anchor.x) + 'px'; | |
img.style.marginTop = (-anchor.y) + 'px'; | |
} | |
if (size) { | |
img.style.width = size.x + 'px'; | |
img.style.height = size.y + 'px'; | |
} | |
}, | |
_createImg: function (src, el) { | |
el = el || document.createElement('img'); | |
el.src = src; | |
return el; | |
}, | |
_getIconUrl: function (name) { | |
return L.Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url']; | |
} | |
}); | |
L.icon = function (options) { | |
return new L.Icon(options); | |
}; | |
/* | |
* L.Icon.Default is the blue marker icon used by default in Leaflet. | |
*/ | |
L.Icon.Default = L.Icon.extend({ | |
options: { | |
iconSize: [25, 41], | |
iconAnchor: [12, 41], | |
popupAnchor: [1, -34], | |
shadowSize: [41, 41] | |
}, | |
_getIconUrl: function (name) { | |
var key = name + 'Url'; | |
if (this.options[key]) { | |
return this.options[key]; | |
} | |
var path = L.Icon.Default.imagePath; | |
if (!path) { | |
throw new Error('Couldn\'t autodetect L.Icon.Default.imagePath, set it manually.'); | |
} | |
return path + '/marker-' + name + (L.Browser.retina && name === 'icon' ? '-2x' : '') + '.png'; | |
} | |
}); | |
L.Icon.Default.imagePath = (function () { | |
var scripts = document.getElementsByTagName('script'), | |
leafletRe = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/; | |
var i, len, src, path; | |
for (i = 0, len = scripts.length; i < len; i++) { | |
src = scripts[i].src; | |
if (src.match(leafletRe)) { | |
path = src.split(leafletRe)[0]; | |
return (path ? path + '/' : '') + 'images'; | |
} | |
} | |
}()); | |
/* | |
* L.Marker is used to display clickable/draggable icons on the map. | |
*/ | |
L.Marker = L.Layer.extend({ | |
options: { | |
pane: 'markerPane', | |
icon: new L.Icon.Default(), | |
// title: '', | |
// alt: '', | |
clickable: true, | |
// draggable: false, | |
keyboard: true, | |
zIndexOffset: 0, | |
opacity: 1, | |
// riseOnHover: false, | |
riseOffset: 250 | |
}, | |
initialize: function (latlng, options) { | |
L.setOptions(this, options); | |
this._latlng = L.latLng(latlng); | |
}, | |
onAdd: function (map) { | |
this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation; | |
this._initIcon(); | |
this.update(); | |
}, | |
onRemove: function () { | |
if (this.dragging) { | |
this.dragging.disable(); | |
} | |
this._removeIcon(); | |
this._removeShadow(); | |
}, | |
getEvents: function () { | |
var events = {viewreset: this.update}; | |
if (this._zoomAnimated) { | |
events.zoomanim = this._animateZoom; | |
} | |
return events; | |
}, | |
getLatLng: function () { | |
return this._latlng; | |
}, | |
setLatLng: function (latlng) { | |
var oldLatLng = this._latlng; | |
this._latlng = L.latLng(latlng); | |
this.update(); | |
return this.fire('move', { oldLatLng: oldLatLng, latlng: this._latlng }); | |
}, | |
setZIndexOffset: function (offset) { | |
this.options.zIndexOffset = offset; | |
return this.update(); | |
}, | |
setIcon: function (icon) { | |
this.options.icon = icon; | |
if (this._map) { | |
this._initIcon(); | |
this.update(); | |
} | |
if (this._popup) { | |
this.bindPopup(this._popup); | |
} | |
return this; | |
}, | |
update: function () { | |
if (this._icon) { | |
var pos = this._map.latLngToLayerPoint(this._latlng).round(); | |
this._setPos(pos); | |
} | |
return this; | |
}, | |
_initIcon: function () { | |
var options = this.options, | |
classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide'); | |
var icon = options.icon.createIcon(this._icon), | |
addIcon = false; | |
// if we're not reusing the icon, remove the old one and init new one | |
if (icon !== this._icon) { | |
if (this._icon) { | |
this._removeIcon(); | |
} | |
addIcon = true; | |
if (options.title) { | |
icon.title = options.title; | |
} | |
if (options.alt) { | |
icon.alt = options.alt; | |
} | |
} | |
L.DomUtil.addClass(icon, classToAdd); | |
if (options.keyboard) { | |
icon.tabIndex = '0'; | |
} | |
this._icon = icon; | |
this._initInteraction(); | |
if (options.riseOnHover) { | |
L.DomEvent.on(icon, { | |
mouseover: this._bringToFront, | |
mouseout: this._resetZIndex | |
}, this); | |
} | |
var newShadow = options.icon.createShadow(this._shadow), | |
addShadow = false; | |
if (newShadow !== this._shadow) { | |
this._removeShadow(); | |
addShadow = true; | |
} | |
if (newShadow) { | |
L.DomUtil.addClass(newShadow, classToAdd); | |
} | |
this._shadow = newShadow; | |
if (options.opacity < 1) { | |
this._updateOpacity(); | |
} | |
if (addIcon) { | |
this.getPane().appendChild(this._icon); | |
} | |
if (newShadow && addShadow) { | |
this.getPane('shadowPane').appendChild(this._shadow); | |
} | |
}, | |
_removeIcon: function () { | |
if (this.options.riseOnHover) { | |
L.DomEvent.off(this._icon, { | |
mouseover: this._bringToFront, | |
mouseout: this._resetZIndex | |
}, this); | |
} | |
L.DomUtil.remove(this._icon); | |
this._icon = null; | |
}, | |
_removeShadow: function () { | |
if (this._shadow) { | |
L.DomUtil.remove(this._shadow); | |
} | |
this._shadow = null; | |
}, | |
_setPos: function (pos) { | |
L.DomUtil.setPosition(this._icon, pos); | |
if (this._shadow) { | |
L.DomUtil.setPosition(this._shadow, pos); | |
} | |
this._zIndex = pos.y + this.options.zIndexOffset; | |
this._resetZIndex(); | |
}, | |
_updateZIndex: function (offset) { | |
this._icon.style.zIndex = this._zIndex + offset; | |
}, | |
_animateZoom: function (opt) { | |
var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round(); | |
this._setPos(pos); | |
}, | |
_initInteraction: function () { | |
if (!this.options.clickable) { return; } | |
L.DomUtil.addClass(this._icon, 'leaflet-clickable'); | |
L.DomEvent.on(this._icon, 'click dblclick mousedown mouseup mouseover mouseout contextmenu keypress', | |
this._fireMouseEvent, this); | |
if (L.Handler.MarkerDrag) { | |
this.dragging = new L.Handler.MarkerDrag(this); | |
if (this.options.draggable) { | |
this.dragging.enable(); | |
} | |
} | |
}, | |
_fireMouseEvent: function (e, type) { | |
// to prevent outline when clicking on keyboard-focusable marker | |
if (e.type === 'mousedown') { | |
L.DomEvent.preventDefault(e); | |
} | |
if (e.type === 'click' && this.dragging && this.dragging.moved()) { return; } | |
if (e.type === 'keypress' && e.keyCode === 13) { | |
type = 'click'; | |
} | |
if (this._map) { | |
this._map._fireMouseEvent(this, e, type, true, this._latlng); | |
} | |
}, | |
setOpacity: function (opacity) { | |
this.options.opacity = opacity; | |
if (this._map) { | |
this._updateOpacity(); | |
} | |
return this; | |
}, | |
_updateOpacity: function () { | |
var opacity = this.options.opacity; | |
L.DomUtil.setOpacity(this._icon, opacity); | |
if (this._shadow) { | |
L.DomUtil.setOpacity(this._shadow, opacity); | |
} | |
}, | |
_bringToFront: function () { | |
this._updateZIndex(this.options.riseOffset); | |
}, | |
_resetZIndex: function () { | |
this._updateZIndex(0); | |
} | |
}); | |
L.marker = function (latlng, options) { | |
return new L.Marker(latlng, options); | |
}; | |
/* | |
* L.DivIcon is a lightweight HTML-based icon class (as opposed to the image-based L.Icon) | |
* to use with L.Marker. | |
*/ | |
L.DivIcon = L.Icon.extend({ | |
options: { | |
iconSize: [12, 12], // also can be set through CSS | |
/* | |
iconAnchor: (Point) | |
popupAnchor: (Point) | |
html: (String) | |
bgPos: (Point) | |
*/ | |
className: 'leaflet-div-icon', | |
html: false | |
}, | |
createIcon: function (oldIcon) { | |
var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'), | |
options = this.options; | |
div.innerHTML = options.html !== false ? options.html : ''; | |
if (options.bgPos) { | |
div.style.backgroundPosition = (-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px'; | |
} | |
this._setIconStyles(div, 'icon'); | |
return div; | |
}, | |
createShadow: function () { | |
return null; | |
} | |
}); | |
L.divIcon = function (options) { | |
return new L.DivIcon(options); | |
}; | |
/* | |
* L.Popup is used for displaying popups on the map. | |
*/ | |
L.Map.mergeOptions({ | |
closePopupOnClick: true | |
}); | |
L.Popup = L.Layer.extend({ | |
options: { | |
pane: 'popupPane', | |
minWidth: 50, | |
maxWidth: 300, | |
// maxHeight: <Number>, | |
offset: [0, 7], | |
autoPan: true, | |
autoPanPadding: [5, 5], | |
// autoPanPaddingTopLeft: <Point>, | |
// autoPanPaddingBottomRight: <Point>, | |
closeButton: true, | |
// keepInView: false, | |
// className: '', | |
zoomAnimation: true | |
}, | |
initialize: function (options, source) { | |
L.setOptions(this, options); | |
this._source = source; | |
}, | |
onAdd: function (map) { | |
this._zoomAnimated = this._zoomAnimated && this.options.zoomAnimation; | |
if (!this._container) { | |
this._initLayout(); | |
} | |
if (map._fadeAnimated) { | |
L.DomUtil.setOpacity(this._container, 0); | |
} | |
clearTimeout(this._removeTimeout); | |
this.getPane().appendChild(this._container); | |
this.update(); | |
if (map._fadeAnimated) { | |
L.DomUtil.setOpacity(this._container, 1); | |
} | |
map.fire('popupopen', {popup: this}); | |
if (this._source) { | |
this._source.fire('popupopen', {popup: this}, true); | |
} | |
}, | |
openOn: function (map) { | |
map.openPopup(this); | |
return this; | |
}, | |
onRemove: function (map) { | |
if (map._fadeAnimated) { | |
L.DomUtil.setOpacity(this._container, 0); | |
this._removeTimeout = setTimeout(L.bind(L.DomUtil.remove, L.DomUtil, this._container), 200); | |
} else { | |
L.DomUtil.remove(this._container); | |
} | |
map.fire('popupclose', {popup: this}); | |
if (this._source) { | |
this._source.fire('popupclose', {popup: this}, true); | |
} | |
}, | |
getLatLng: function () { | |
return this._latlng; | |
}, | |
setLatLng: function (latlng) { | |
this._latlng = L.latLng(latlng); | |
if (this._map) { | |
this._updatePosition(); | |
this._adjustPan(); | |
} | |
return this; | |
}, | |
getContent: function () { | |
return this._content; | |
}, | |
setContent: function (content) { | |
this._content = content; | |
this.update(); | |
return this; | |
}, | |
update: function () { | |
if (!this._map) { return; } | |
this._container.style.visibility = 'hidden'; | |
this._updateContent(); | |
this._updateLayout(); | |
this._updatePosition(); | |
this._container.style.visibility = ''; | |
this._adjustPan(); | |
}, | |
getEvents: function () { | |
var events = {viewreset: this._updatePosition}, | |
options = this.options; | |
if (this._zoomAnimated) { | |
events.zoomanim = this._animateZoom; | |
} | |
if ('closeOnClick' in options ? options.closeOnClick : this._map.options.closePopupOnClick) { | |
events.preclick = this._close; | |
} | |
if (options.keepInView) { | |
events.moveend = this._adjustPan; | |
} | |
return events; | |
}, | |
isOpen: function () { | |
return !!this._map && this._map.hasLayer(this); | |
}, | |
_close: function () { | |
if (this._map) { | |
this._map.closePopup(this); | |
} | |
}, | |
_initLayout: function () { | |
var prefix = 'leaflet-popup', | |
container = this._container = L.DomUtil.create('div', | |
prefix + ' ' + (this.options.className || '') + | |
' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide')); | |
if (this.options.closeButton) { | |
var closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container); | |
closeButton.href = '#close'; | |
closeButton.innerHTML = '×'; | |
L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this); | |
} | |
var wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container); | |
this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper); | |
L.DomEvent | |
.disableClickPropagation(wrapper) | |
.disableScrollPropagation(this._contentNode) | |
.on(wrapper, 'contextmenu', L.DomEvent.stopPropagation); | |
this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container); | |
this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer); | |
}, | |
_updateContent: function () { | |
if (!this._content) { return; } | |
var node = this._contentNode; | |
if (typeof this._content === 'string') { | |
node.innerHTML = this._content; | |
} else { | |
while (node.hasChildNodes()) { | |
node.removeChild(node.firstChild); | |
} | |
node.appendChild(this._content); | |
} | |
this.fire('contentupdate'); | |
}, | |
_updateLayout: function () { | |
var container = this._contentNode, | |
style = container.style; | |
style.width = ''; | |
style.whiteSpace = 'nowrap'; | |
var width = container.offsetWidth; | |
width = Math.min(width, this.options.maxWidth); | |
width = Math.max(width, this.options.minWidth); | |
style.width = (width + 1) + 'px'; | |
style.whiteSpace = ''; | |
style.height = ''; | |
var height = container.offsetHeight, | |
maxHeight = this.options.maxHeight, | |
scrolledClass = 'leaflet-popup-scrolled'; | |
if (maxHeight && height > maxHeight) { | |
style.height = maxHeight + 'px'; | |
L.DomUtil.addClass(container, scrolledClass); | |
} else { | |
L.DomUtil.removeClass(container, scrolledClass); | |
} | |
this._containerWidth = this._container.offsetWidth; | |
}, | |
_updatePosition: function () { | |
if (!this._map) { return; } | |
var pos = this._map.latLngToLayerPoint(this._latlng), | |
offset = L.point(this.options.offset); | |
if (this._zoomAnimated) { | |
L.DomUtil.setPosition(this._container, pos); | |
} else { | |
offset = offset.add(pos); | |
} | |
var bottom = this._containerBottom = -offset.y, | |
left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x; | |
// bottom position the popup in case the height of the popup changes (images loading etc) | |
this._container.style.bottom = bottom + 'px'; | |
this._container.style.left = left + 'px'; | |
}, | |
_animateZoom: function (e) { | |
var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center); | |
L.DomUtil.setPosition(this._container, pos); | |
}, | |
_adjustPan: function () { | |
if (!this.options.autoPan) { return; } | |
var map = this._map, | |
containerHeight = this._container.offsetHeight, | |
containerWidth = this._containerWidth, | |
layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom); | |
if (this._zoomAnimated) { | |
layerPos._add(L.DomUtil.getPosition(this._container)); | |
} | |
var containerPos = map.layerPointToContainerPoint(layerPos), | |
padding = L.point(this.options.autoPanPadding), | |
paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding), | |
paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding), | |
size = map.getSize(), | |
dx = 0, | |
dy = 0; | |
if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right | |
dx = containerPos.x + containerWidth - size.x + paddingBR.x; | |
} | |
if (containerPos.x - dx - paddingTL.x < 0) { // left | |
dx = containerPos.x - paddingTL.x; | |
} | |
if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom | |
dy = containerPos.y + containerHeight - size.y + paddingBR.y; | |
} | |
if (containerPos.y - dy - paddingTL.y < 0) { // top | |
dy = containerPos.y - paddingTL.y; | |
} | |
if (dx || dy) { | |
map | |
.fire('autopanstart') | |
.panBy([dx, dy]); | |
} | |
}, | |
_onCloseButtonClick: function (e) { | |
this._close(); | |
L.DomEvent.stop(e); | |
} | |
}); | |
L.popup = function (options, source) { | |
return new L.Popup(options, source); | |
}; | |
L.Map.include({ | |
openPopup: function (popup, latlng, options) { // (Popup) or (String || HTMLElement, LatLng[, Object]) | |
if (!(popup instanceof L.Popup)) { | |
var content = popup; | |
popup = new L.Popup(options).setContent(content); | |
} | |
if (latlng) { | |
popup.setLatLng(latlng); | |
} | |
if (this.hasLayer(popup)) { | |
return this; | |
} | |
this.closePopup(); | |
this._popup = popup; | |
return this.addLayer(popup); | |
}, | |
closePopup: function (popup) { | |
if (!popup || popup === this._popup) { | |
popup = this._popup; | |
this._popup = null; | |
} | |
if (popup) { | |
this.removeLayer(popup); | |
} | |
return this; | |
} | |
}); | |
/* | |
* Adds popup-related methods to all layers. | |
*/ | |
L.Layer.include({ | |
bindPopup: function (content, options) { | |
if (content instanceof L.Popup) { | |
this._popup = content; | |
content._source = this; | |
} else { | |
if (!this._popup || options) { | |
this._popup = new L.Popup(options, this); | |
} | |
this._popup.setContent(content); | |
} | |
if (!this._popupHandlersAdded) { | |
this.on({ | |
click: this._openPopup, | |
remove: this.closePopup, | |
move: this._movePopup | |
}); | |
this._popupHandlersAdded = true; | |
} | |
return this; | |
}, | |
unbindPopup: function () { | |
if (this._popup) { | |
this.on({ | |
click: this._openPopup, | |
remove: this.closePopup, | |
move: this._movePopup | |
}); | |
this._popupHandlersAdded = false; | |
this._popup = null; | |
} | |
return this; | |
}, | |
openPopup: function (latlng) { | |
if (this._popup && this._map) { | |
this._map.openPopup(this._popup, latlng || this._latlng || this.getCenter()); | |
} | |
return this; | |
}, | |
closePopup: function () { | |
if (this._popup) { | |
this._popup._close(); | |
} | |
return this; | |
}, | |
togglePopup: function () { | |
if (this._popup) { | |
if (this._popup._map) { | |
this.closePopup(); | |
} else { | |
this.openPopup(); | |
} | |
} | |
return this; | |
}, | |
setPopupContent: function (content) { | |
if (this._popup) { | |
this._popup.setContent(content); | |
} | |
return this; | |
}, | |
getPopup: function () { | |
return this._popup; | |
}, | |
_openPopup: function (e) { | |
this._map.openPopup(this._popup, e.latlng); | |
}, | |
_movePopup: function (e) { | |
this._popup.setLatLng(e.latlng); | |
} | |
}); | |
/* | |
* Popup extension to L.Marker, adding popup-related methods. | |
*/ | |
L.Marker.include({ | |
bindPopup: function (content, options) { | |
var anchor = L.point(this.options.icon.options.popupAnchor || [0, 0]) | |
.add(L.Popup.prototype.options.offset); | |
options = L.extend({offset: anchor}, options); | |
return L.Layer.prototype.bindPopup.call(this, content, options); | |
}, | |
_openPopup: L.Layer.prototype.togglePopup | |
}); | |
/* | |
* L.LayerGroup is a class to combine several layers into one so that | |
* you can manipulate the group (e.g. add/remove it) as one layer. | |
*/ | |
L.LayerGroup = L.Layer.extend({ | |
initialize: function (layers) { | |
this._layers = {}; | |
var i, len; | |
if (layers) { | |
for (i = 0, len = layers.length; i < len; i++) { | |
this.addLayer(layers[i]); | |
} | |
} | |
}, | |
addLayer: function (layer) { | |
var id = this.getLayerId(layer); | |
this._layers[id] = layer; | |
if (this._map) { | |
this._map.addLayer(layer); | |
} | |
return this; | |
}, | |
removeLayer: function (layer) { | |
var id = layer in this._layers ? layer : this.getLayerId(layer); | |
if (this._map && this._layers[id]) { | |
this._map.removeLayer(this._layers[id]); | |
} | |
delete this._layers[id]; | |
return this; | |
}, | |
hasLayer: function (layer) { | |
return !!layer && (layer in this._layers || this.getLayerId(layer) in this._layers); | |
}, | |
clearLayers: function () { | |
for (var i in this._layers) { | |
this.removeLayer(this._layers[i]); | |
} | |
return this; | |
}, | |
invoke: function (methodName) { | |
var args = Array.prototype.slice.call(arguments, 1), | |
i, layer; | |
for (i in this._layers) { | |
layer = this._layers[i]; | |
if (layer[methodName]) { | |
layer[methodName].apply(layer, args); | |
} | |
} | |
return this; | |
}, | |
onAdd: function (map) { | |
for (var i in this._layers) { | |
map.addLayer(this._layers[i]); | |
} | |
}, | |
onRemove: function (map) { | |
for (var i in this._layers) { | |
map.removeLayer(this._layers[i]); | |
} | |
}, | |
eachLayer: function (method, context) { | |
for (var i in this._layers) { | |
method.call(context, this._layers[i]); | |
} | |
return this; | |
}, | |
getLayer: function (id) { | |
return this._layers[id]; | |
}, | |
getLayers: function () { | |
var layers = []; | |
for (var i in this._layers) { | |
layers.push(this._layers[i]); | |
} | |
return layers; | |
}, | |
setZIndex: function (zIndex) { | |
return this.invoke('setZIndex', zIndex); | |
}, | |
getLayerId: function (layer) { | |
return L.stamp(layer); | |
} | |
}); | |
L.layerGroup = function (layers) { | |
return new L.LayerGroup(layers); | |
}; | |
/* | |
* L.FeatureGroup extends L.LayerGroup by introducing mouse events and additional methods | |
* shared between a group of interactive layers (like vectors or markers). | |
*/ | |
L.FeatureGroup = L.LayerGroup.extend({ | |
addLayer: function (layer) { | |
if (this.hasLayer(layer)) { | |
return this; | |
} | |
layer.addEventParent(this); | |
L.LayerGroup.prototype.addLayer.call(this, layer); | |
if (this._popupContent && layer.bindPopup) { | |
layer.bindPopup(this._popupContent, this._popupOptions); | |
} | |
return this.fire('layeradd', {layer: layer}); | |
}, | |
removeLayer: function (layer) { | |
if (!this.hasLayer(layer)) { | |
return this; | |
} | |
if (layer in this._layers) { | |
layer = this._layers[layer]; | |
} | |
layer.removeEventParent(this); | |
L.LayerGroup.prototype.removeLayer.call(this, layer); | |
if (this._popupContent) { | |
this.invoke('unbindPopup'); | |
} | |
return this.fire('layerremove', {layer: layer}); | |
}, | |
bindPopup: function (content, options) { | |
this._popupContent = content; | |
this._popupOptions = options; | |
return this.invoke('bindPopup', content, options); | |
}, | |
openPopup: function (latlng) { | |
// open popup on the first layer | |
for (var id in this._layers) { | |
this._layers[id].openPopup(latlng); | |
break; | |
} | |
return this; | |
}, | |
setStyle: function (style) { | |
return this.invoke('setStyle', style); | |
}, | |
bringToFront: function () { | |
return this.invoke('bringToFront'); | |
}, | |
bringToBack: function () { | |
return this.invoke('bringToBack'); | |
}, | |
getBounds: function () { | |
var bounds = new L.LatLngBounds(); | |
this.eachLayer(function (layer) { | |
bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng()); | |
}); | |
return bounds; | |
} | |
}); | |
L.featureGroup = function (layers) { | |
return new L.FeatureGroup(layers); | |
}; | |
/* | |
* L.Renderer is a base class for renderer implementations (SVG, Canvas); | |
* handles renderer container, bounds and zoom animation. | |
*/ | |
L.Renderer = L.Layer.extend({ | |
options: { | |
// how much to extend the clip area around the map view (relative to its size) | |
// e.g. 0.1 would be 10% of map view in each direction; defaults to clip with the map view | |
padding: 0 | |
}, | |
initialize: function (options) { | |
L.setOptions(this, options); | |
L.stamp(this); | |
}, | |
onAdd: function () { | |
if (!this._container) { | |
this._initContainer(); // defined by renderer implementations | |
if (this._zoomAnimated) { | |
L.DomUtil.addClass(this._container, 'leaflet-zoom-animated'); | |
} | |
} | |
this.getPane().appendChild(this._container); | |
this._update(); | |
}, | |
onRemove: function () { | |
L.DomUtil.remove(this._container); | |
}, | |
getEvents: function () { | |
var events = { | |
moveend: this._update | |
}; | |
if (this._zoomAnimated) { | |
events.zoomanim = this._animateZoom; | |
} | |
return events; | |
}, | |
_animateZoom: function (e) { | |
var origin = e.origin.subtract(this._map._getCenterLayerPoint()), | |
offset = this._bounds.min.add(origin.multiplyBy(1 - e.scale)); | |
L.DomUtil.setTransform(this._container, offset, e.scale); | |
}, | |
_update: function () { | |
// update pixel bounds of renderer container (for positioning/sizing/clipping later) | |
var p = this.options.padding, | |
size = this._map.getSize(), | |
min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round(); | |
this._bounds = new L.Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round()); | |
} | |
}); | |
L.Map.include({ | |
// used by each vector layer to decide which renderer to use | |
getRenderer: function (layer) { | |
var renderer = layer.options.renderer || this.options.renderer || this._renderer; | |
if (!renderer) { | |
renderer = this._renderer = (L.SVG && L.svg()) || (L.Canvas && L.canvas()); | |
} | |
if (!this.hasLayer(renderer)) { | |
this.addLayer(renderer); | |
} | |
return renderer; | |
} | |
}); | |
/* | |
* L.Path is the base class for all Leaflet vector layers like polygons and circles. | |
*/ | |
L.Path = L.Layer.extend({ | |
options: { | |
stroke: true, | |
color: '#3388ff', | |
weight: 3, | |
opacity: 1, | |
lineCap: 'round', | |
lineJoin: 'round', | |
// dashArray: null | |
// dashOffset: null | |
// fill: false | |
// fillColor: same as color by default | |
fillOpacity: 0.2, | |
// className: '' | |
clickable: true | |
}, | |
onAdd: function () { | |
this._renderer = this._map.getRenderer(this); | |
this._renderer._initPath(this); | |
// defined in children classes | |
this._project(); | |
this._update(); | |
this._renderer._addPath(this); | |
}, | |
onRemove: function () { | |
this._renderer._removePath(this); | |
}, | |
getEvents: function () { | |
return { | |
viewreset: this._project, | |
moveend: this._update | |
}; | |
}, | |
redraw: function () { | |
if (this._map) { | |
this._renderer._updatePath(this); | |
} | |
return this; | |
}, | |
setStyle: function (style) { | |
L.setOptions(this, style); | |
if (this._renderer) { | |
this._renderer._updateStyle(this); | |
} | |
return this; | |
}, | |
bringToFront: function () { | |
this._renderer._bringToFront(this); | |
return this; | |
}, | |
bringToBack: function () { | |
this._renderer._bringToBack(this); | |
return this; | |
}, | |
_fireMouseEvent: function (e, type) { | |
this._map._fireMouseEvent(this, e, type, true); | |
}, | |
_clickTolerance: function () { | |
// used when doing hit detection for Canvas layers | |
return (this.options.stroke ? this.options.weight / 2 : 0) + (L.Browser.touch ? 10 : 0); | |
} | |
}); | |
/* | |
* L.LineUtil contains different utility functions for line segments | |
* and polylines (clipping, simplification, distances, etc.) | |
*/ | |
/*jshint bitwise:false */ // allow bitwise operations for this file | |
L.LineUtil = { | |
// Simplify polyline with vertex reduction and Douglas-Peucker simplification. | |
// Improves rendering performance dramatically by lessening the number of points to draw. | |
simplify: function (/*Point[]*/ points, /*Number*/ tolerance) { | |
if (!tolerance || !points.length) { | |
return points.slice(); | |
} | |
var sqTolerance = tolerance * tolerance; | |
// stage 1: vertex reduction | |
points = this._reducePoints(points, sqTolerance); | |
// stage 2: Douglas-Peucker simplification | |
points = this._simplifyDP(points, sqTolerance); | |
return points; | |
}, | |
// distance from a point to a segment between two points | |
pointToSegmentDistance: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { | |
return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true)); | |
}, | |
closestPointOnSegment: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { | |
return this._sqClosestPointOnSegment(p, p1, p2); | |
}, | |
// Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm | |
_simplifyDP: function (points, sqTolerance) { | |
var len = points.length, | |
ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array, | |
markers = new ArrayConstructor(len); | |
markers[0] = markers[len - 1] = 1; | |
this._simplifyDPStep(points, markers, sqTolerance, 0, len - 1); | |
var i, | |
newPoints = []; | |
for (i = 0; i < len; i++) { | |
if (markers[i]) { | |
newPoints.push(points[i]); | |
} | |
} | |
return newPoints; | |
}, | |
_simplifyDPStep: function (points, markers, sqTolerance, first, last) { | |
var maxSqDist = 0, | |
index, i, sqDist; | |
for (i = first + 1; i <= last - 1; i++) { | |
sqDist = this._sqClosestPointOnSegment(points[i], points[first], points[last], true); | |
if (sqDist > maxSqDist) { | |
index = i; | |
maxSqDist = sqDist; | |
} | |
} | |
if (maxSqDist > sqTolerance) { | |
markers[index] = 1; | |
this._simplifyDPStep(points, markers, sqTolerance, first, index); | |
this._simplifyDPStep(points, markers, sqTolerance, index, last); | |
} | |
}, | |
// reduce points that are too close to each other to a single point | |
_reducePoints: function (points, sqTolerance) { | |
var reducedPoints = [points[0]]; | |
for (var i = 1, prev = 0, len = points.length; i < len; i++) { | |
if (this._sqDist(points[i], points[prev]) > sqTolerance) { | |
reducedPoints.push(points[i]); | |
prev = i; | |
} | |
} | |
if (prev < len - 1) { | |
reducedPoints.push(points[len - 1]); | |
} | |
return reducedPoints; | |
}, | |
// Cohen-Sutherland line clipping algorithm. | |
// Used to avoid rendering parts of a polyline that are not currently visible. | |
clipSegment: function (a, b, bounds, useLastCode) { | |
var codeA = useLastCode ? this._lastCode : this._getBitCode(a, bounds), | |
codeB = this._getBitCode(b, bounds), | |
codeOut, p, newCode; | |
// save 2nd code to avoid calculating it on the next segment | |
this._lastCode = codeB; | |
while (true) { | |
// if a,b is inside the clip window (trivial accept) | |
if (!(codeA | codeB)) { | |
return [a, b]; | |
// if a,b is outside the clip window (trivial reject) | |
} else if (codeA & codeB) { | |
return false; | |
// other cases | |
} else { | |
codeOut = codeA || codeB; | |
p = this._getEdgeIntersection(a, b, codeOut, bounds); | |
newCode = this._getBitCode(p, bounds); | |
if (codeOut === codeA) { | |
a = p; | |
codeA = newCode; | |
} else { | |
b = p; | |
codeB = newCode; | |
} | |
} | |
} | |
}, | |
_getEdgeIntersection: function (a, b, code, bounds) { | |
var dx = b.x - a.x, | |
dy = b.y - a.y, | |
min = bounds.min, | |
max = bounds.max, | |
x, y; | |
if (code & 8) { // top | |
x = a.x + dx * (max.y - a.y) / dy; | |
y = max.y; | |
} else if (code & 4) { // bottom | |
x = a.x + dx * (min.y - a.y) / dy; | |
y = min.y; | |
} else if (code & 2) { // right | |
x = max.x; | |
y = a.y + dy * (max.x - a.x) / dx; | |
} else if (code & 1) { // left | |
x = min.x; | |
y = a.y + dy * (min.x - a.x) / dx; | |
} | |
return new L.Point(x, y, true); | |
}, | |
_getBitCode: function (/*Point*/ p, bounds) { | |
var code = 0; | |
if (p.x < bounds.min.x) { // left | |
code |= 1; | |
} else if (p.x > bounds.max.x) { // right | |
code |= 2; | |
} | |
if (p.y < bounds.min.y) { // bottom | |
code |= 4; | |
} else if (p.y > bounds.max.y) { // top | |
code |= 8; | |
} | |
return code; | |
}, | |
// square distance (to avoid unnecessary Math.sqrt calls) | |
_sqDist: function (p1, p2) { | |
var dx = p2.x - p1.x, | |
dy = p2.y - p1.y; | |
return dx * dx + dy * dy; | |
}, | |
// return closest point on segment or distance to that point | |
_sqClosestPointOnSegment: function (p, p1, p2, sqDist) { | |
var x = p1.x, | |
y = p1.y, | |
dx = p2.x - x, | |
dy = p2.y - y, | |
dot = dx * dx + dy * dy, | |
t; | |
if (dot > 0) { | |
t = ((p.x - x) * dx + (p.y - y) * dy) / dot; | |
if (t > 1) { | |
x = p2.x; | |
y = p2.y; | |
} else if (t > 0) { | |
x += dx * t; | |
y += dy * t; | |
} | |
} | |
dx = p.x - x; | |
dy = p.y - y; | |
return sqDist ? dx * dx + dy * dy : new L.Point(x, y); | |
} | |
}; | |
/* | |
* L.Polyline implements polyline vector layer (a set of points connected with lines) | |
*/ | |
L.Polyline = L.Path.extend({ | |
options: { | |
// how much to simplify the polyline on each zoom level | |
// more = better performance and smoother look, less = more accurate | |
smoothFactor: 1.0 | |
// noClip: false | |
}, | |
initialize: function (latlngs, options) { | |
L.setOptions(this, options); | |
this._setLatLngs(latlngs); | |
}, | |
getLatLngs: function () { | |
// TODO rings | |
return this._latlngs; | |
}, | |
setLatLngs: function (latlngs) { | |
this._setLatLngs(latlngs); | |
return this.redraw(); | |
}, | |
addLatLng: function (latlng) { | |
// TODO rings | |
latlng = L.latLng(latlng); | |
this._latlngs.push(latlng); | |
this._bounds.extend(latlng); | |
return this.redraw(); | |
}, | |
spliceLatLngs: function () { | |
// TODO rings | |
var removed = [].splice.apply(this._latlngs, arguments); | |
this._setLatLngs(this._latlngs); | |
this.redraw(); | |
return removed; | |
}, | |
closestLayerPoint: function (p) { | |
var minDistance = Infinity, | |
minPoint = null, | |
closest = L.LineUtil._sqClosestPointOnSegment, | |
p1, p2; | |
for (var j = 0, jLen = this._parts.length; j < jLen; j++) { | |
var points = this._parts[j]; | |
for (var i = 1, len = points.length; i < len; i++) { | |
p1 = points[i - 1]; | |
p2 = points[i]; | |
var sqDist = closest(p, p1, p2, true); | |
if (sqDist < minDistance) { | |
minDistance = sqDist; | |
minPoint = closest(p, p1, p2); | |
} | |
} | |
} | |
if (minPoint) { | |
minPoint.distance = Math.sqrt(minDistance); | |
} | |
return minPoint; | |
}, | |
getCenter: function () { | |
var i, halfDist, segDist, dist, p1, p2, ratio, | |
points = this._rings[0], | |
len = points.length; | |
// polyline centroid algorithm; only uses the first ring if there are multiple | |
for (i = 0, halfDist = 0; i < len - 1; i++) { | |
halfDist += points[i].distanceTo(points[i + 1]) / 2; | |
} | |
for (i = 0, dist = 0; i < len - 1; i++) { | |
p1 = points[i]; | |
p2 = points[i + 1]; | |
segDist = p1.distanceTo(p2); | |
dist += segDist; | |
if (dist > halfDist) { | |
ratio = (dist - halfDist) / segDist; | |
return this._map.layerPointToLatLng([ | |
p2.x - ratio * (p2.x - p1.x), | |
p2.y - ratio * (p2.y - p1.y) | |
]); | |
} | |
} | |
}, | |
getBounds: function () { | |
return this._bounds; | |
}, | |
_setLatLngs: function (latlngs) { | |
this._bounds = new L.LatLngBounds(); | |
this._latlngs = this._convertLatLngs(latlngs); | |
}, | |
// recursively convert latlngs input into actual LatLng instances; calculate bounds along the way | |
_convertLatLngs: function (latlngs) { | |
var result = [], | |
flat = this._flat(latlngs); | |
for (var i = 0, len = latlngs.length; i < len; i++) { | |
if (flat) { | |
result[i] = L.latLng(latlngs[i]); | |
this._bounds.extend(result[i]); | |
} else { | |
result[i] = this._convertLatLngs(latlngs[i]); | |
} | |
} | |
return result; | |
}, | |
_flat: function (latlngs) { | |
// true if it's a flat array of latlngs; false if nested | |
return !L.Util.isArray(latlngs[0]) || typeof latlngs[0][0] !== 'object'; | |
}, | |
_project: function () { | |
this._rings = []; | |
this._projectLatlngs(this._latlngs, this._rings); | |
// project bounds as well to use later for Canvas hit detection/etc. | |
var w = this._clickTolerance(), | |
p = new L.Point(w, -w); | |
if (this._latlngs.length) { | |
this._pxBounds = new L.Bounds( | |
this._map.latLngToLayerPoint(this._bounds.getSouthWest())._subtract(p), | |
this._map.latLngToLayerPoint(this._bounds.getNorthEast())._add(p)); | |
} | |
}, | |
// recursively turns latlngs into a set of rings with projected coordinates | |
_projectLatlngs: function (latlngs, result) { | |
var flat = latlngs[0] instanceof L.LatLng, | |
len = latlngs.length, | |
i, ring; | |
if (flat) { | |
ring = []; | |
for (i = 0; i < len; i++) { | |
ring[i] = this._map.latLngToLayerPoint(latlngs[i]); | |
} | |
result.push(ring); | |
} else { | |
for (i = 0; i < len; i++) { | |
this._projectLatlngs(latlngs[i], result); | |
} | |
} | |
}, | |
// clip polyline by renderer bounds so that we have less to render for performance | |
_clipPoints: function () { | |
if (this.options.noClip) { | |
this._parts = this._rings; | |
return; | |
} | |
this._parts = []; | |
var parts = this._parts, | |
bounds = this._renderer._bounds, | |
i, j, k, len, len2, segment, points; | |
for (i = 0, k = 0, len = this._rings.length; i < len; i++) { | |
points = this._rings[i]; | |
for (j = 0, len2 = points.length; j < len2 - 1; j++) { | |
segment = L.LineUtil.clipSegment(points[j], points[j + 1], bounds, j); | |
if (!segment) { continue; } | |
parts[k] = parts[k] || []; | |
parts[k].push(segment[0]); | |
// if segment goes out of screen, or it's the last one, it's the end of the line part | |
if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) { | |
parts[k].push(segment[1]); | |
k++; | |
} | |
} | |
} | |
}, | |
// simplify each clipped part of the polyline for performance | |
_simplifyPoints: function () { | |
var parts = this._parts, | |
tolerance = this.options.smoothFactor; | |
for (var i = 0, len = parts.length; i < len; i++) { | |
parts[i] = L.LineUtil.simplify(parts[i], tolerance); | |
} | |
}, | |
_update: function () { | |
if (!this._map) { return; } | |
this._clipPoints(); | |
this._simplifyPoints(); | |
this._updatePath(); | |
}, | |
_updatePath: function () { | |
this._renderer._updatePoly(this); | |
} | |
}); | |
L.polyline = function (latlngs, options) { | |
return new L.Polyline(latlngs, options); | |
}; | |
/* | |
* L.PolyUtil contains utility functions for polygons (clipping, etc.). | |
*/ | |
/*jshint bitwise:false */ // allow bitwise operations here | |
L.PolyUtil = {}; | |
/* | |
* Sutherland-Hodgeman polygon clipping algorithm. | |
* Used to avoid rendering parts of a polygon that are not currently visible. | |
*/ | |
L.PolyUtil.clipPolygon = function (points, bounds) { | |
var clippedPoints, | |
edges = [1, 4, 2, 8], | |
i, j, k, | |
a, b, | |
len, edge, p, | |
lu = L.LineUtil; | |
for (i = 0, len = points.length; i < len; i++) { | |
points[i]._code = lu._getBitCode(points[i], bounds); | |
} | |
// for each edge (left, bottom, right, top) | |
for (k = 0; k < 4; k++) { | |
edge = edges[k]; | |
clippedPoints = []; | |
for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { | |
a = points[i]; | |
b = points[j]; | |
// if a is inside the clip window | |
if (!(a._code & edge)) { | |
// if b is outside the clip window (a->b goes out of screen) | |
if (b._code & edge) { | |
p = lu._getEdgeIntersection(b, a, edge, bounds); | |
p._code = lu._getBitCode(p, bounds); | |
clippedPoints.push(p); | |
} | |
clippedPoints.push(a); | |
// else if b is inside the clip window (a->b enters the screen) | |
} else if (!(b._code & edge)) { | |
p = lu._getEdgeIntersection(b, a, edge, bounds); | |
p._code = lu._getBitCode(p, bounds); | |
clippedPoints.push(p); | |
} | |
} | |
points = clippedPoints; | |
} | |
return points; | |
}; | |
/* | |
* L.Polygon implements polygon vector layer (closed polyline with a fill inside). | |
*/ | |
L.Polygon = L.Polyline.extend({ | |
options: { | |
fill: true | |
}, | |
getCenter: function () { | |
var i, j, len, p1, p2, f, area, x, y, | |
points = this._rings[0]; | |
// polygon centroid algorithm; only uses the first ring if there are multiple | |
area = x = y = 0; | |
for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { | |
p1 = points[i]; | |
p2 = points[j]; | |
f = p1.y * p2.x - p2.y * p1.x; | |
x += (p1.x + p2.x) * f; | |
y += (p1.y + p2.y) * f; | |
area += f * 3; | |
} | |
return this._map.layerPointToLatLng([x / area, y / area]); | |
}, | |
_convertLatLngs: function (latlngs) { | |
var result = L.Polyline.prototype._convertLatLngs.call(this, latlngs), | |
len = result.length; | |
// remove last point if it equals first one | |
if (len >= 2 && result[0] instanceof L.LatLng && result[0].equals(result[len - 1])) { | |
result.pop(); | |
} | |
return result; | |
}, | |
_clipPoints: function () { | |
if (this.options.noClip) { | |
this._parts = this._rings; | |
return; | |
} | |
// polygons need a different clipping algorithm so we redefine that | |
var bounds = this._renderer._bounds, | |
w = this.options.weight, | |
p = new L.Point(w, w); | |
// increase clip padding by stroke width to avoid stroke on clip edges | |
bounds = new L.Bounds(bounds.min.subtract(p), bounds.max.add(p)); | |
this._parts = []; | |
for (var i = 0, len = this._rings.length, clipped; i < len; i++) { | |
clipped = L.PolyUtil.clipPolygon(this._rings[i], bounds); | |
if (clipped.length) { | |
this._parts.push(clipped); | |
} | |
} | |
}, | |
_updatePath: function () { | |
this._renderer._updatePoly(this, true); | |
} | |
}); | |
L.polygon = function (latlngs, options) { | |
return new L.Polygon(latlngs, options); | |
}; | |
/* | |
* L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object. | |
*/ | |
L.Rectangle = L.Polygon.extend({ | |
initialize: function (latLngBounds, options) { | |
L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options); | |
}, | |
setBounds: function (latLngBounds) { | |
this.setLatLngs(this._boundsToLatLngs(latLngBounds)); | |
}, | |
_boundsToLatLngs: function (latLngBounds) { | |
latLngBounds = L.latLngBounds(latLngBounds); | |
return [ | |
latLngBounds.getSouthWest(), | |
latLngBounds.getNorthWest(), | |
latLngBounds.getNorthEast(), | |
latLngBounds.getSouthEast() | |
]; | |
} | |
}); | |
L.rectangle = function (latLngBounds, options) { | |
return new L.Rectangle(latLngBounds, options); | |
}; | |
/* | |
* L.CircleMarker is a circle overlay with a permanent pixel radius. | |
*/ | |
L.CircleMarker = L.Path.extend({ | |
options: { | |
fill: true, | |
radius: 10 | |
}, | |
initialize: function (latlng, options) { | |
L.setOptions(this, options); | |
this._latlng = L.latLng(latlng); | |
this._radius = this.options.radius; | |
}, | |
setLatLng: function (latlng) { | |
this._latlng = L.latLng(latlng); | |
this.redraw(); | |
return this.fire('move', {latlng: this._latlng}); | |
}, | |
getLatLng: function () { | |
return this._latlng; | |
}, | |
setRadius: function (radius) { | |
this.options.radius = this._radius = radius; | |
return this.redraw(); | |
}, | |
getRadius: function () { | |
return this._radius; | |
}, | |
setStyle : function (options) { | |
var radius = options && options.radius || this._radius; | |
L.Path.prototype.setStyle.call(this, options); | |
this.setRadius(radius); | |
return this; | |
}, | |
_project: function () { | |
this._point = this._map.latLngToLayerPoint(this._latlng); | |
this._updateBounds(); | |
}, | |
_updateBounds: function () { | |
var r = this._radius, | |
r2 = this._radiusY || r, | |
w = this._clickTolerance(), | |
p = [r + w, r2 + w]; | |
this._pxBounds = new L.Bounds(this._point.subtract(p), this._point.add(p)); | |
}, | |
_update: function () { | |
if (this._map) { | |
this._updatePath(); | |
} | |
}, | |
_updatePath: function () { | |
this._renderer._updateCircle(this); | |
}, | |
_empty: function () { | |
return this._radius && !this._renderer._bounds.intersects(this._pxBounds); | |
} | |
}); | |
L.circleMarker = function (latlng, options) { | |
return new L.CircleMarker(latlng, options); | |
}; | |
/* | |
* L.Circle is a circle overlay (with a certain radius in meters). | |
* It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion) | |
*/ | |
L.Circle = L.CircleMarker.extend({ | |
initialize: function (latlng, radius, options) { | |
L.setOptions(this, options); | |
this._latlng = L.latLng(latlng); | |
this._mRadius = radius; | |
}, | |
setRadius: function (radius) { | |
this._mRadius = radius; | |
return this.redraw(); | |
}, | |
getRadius: function () { | |
return this._mRadius; | |
}, | |
getBounds: function () { | |
var half = [this._radius, this._radiusY]; | |
return new L.LatLngBounds( | |
this._map.layerPointToLatLng(this._point.subtract(half)), | |
this._map.layerPointToLatLng(this._point.add(half))); | |
}, | |
setStyle: L.Path.prototype.setStyle, | |
_project: function () { | |
var lng = this._latlng.lng, | |
lat = this._latlng.lat, | |
map = this._map, | |
crs = map.options.crs; | |
if (crs.distance === L.CRS.Earth.distance) { | |
var d = Math.PI / 180, | |
latR = (this._mRadius / L.CRS.Earth.R) / d, | |
top = map.project([lat + latR, lng]), | |
bottom = map.project([lat - latR, lng]), | |
p = top.add(bottom).divideBy(2), | |
lat2 = map.unproject(p).lat, | |
lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) / | |
(Math.cos(lat * d) * Math.cos(lat2 * d))) / d; | |
this._point = p.subtract(map.getPixelOrigin()); | |
this._radius = isNaN(lngR) ? 0 : Math.max(Math.round(p.x - map.project([lat2, lng - lngR]).x), 1); | |
this._radiusY = Math.max(Math.round(p.y - top.y), 1); | |
} else { | |
var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0])); | |
this._point = map.latLngToLayerPoint(this._latlng); | |
this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x; | |
} | |
this._updateBounds(); | |
} | |
}); | |
L.circle = function (latlng, radius, options) { | |
return new L.Circle(latlng, radius, options); | |
}; | |
/* | |
* L.SVG renders vector layers with SVG. All SVG-specific code goes here. | |
*/ | |
L.SVG = L.Renderer.extend({ | |
_initContainer: function () { | |
this._container = L.SVG.create('svg'); | |
this._paths = {}; | |
this._initEvents(); | |
// makes it possible to click through svg root; we'll reset it back in individual paths | |
this._container.setAttribute('pointer-events', 'none'); | |
}, | |
_update: function () { | |
if (this._map._animatingZoom && this._bounds) { return; } | |
L.Renderer.prototype._update.call(this); | |
var b = this._bounds, | |
size = b.getSize(), | |
container = this._container, | |
pane = this.getPane(); | |
// hack to make flicker on drag end on mobile webkit less irritating | |
if (L.Browser.mobileWebkit) { | |
pane.removeChild(container); | |
} | |
L.DomUtil.setPosition(container, b.min); | |
// update container viewBox so that we don't have to change coordinates of individual layers | |
container.setAttribute('width', size.x); | |
container.setAttribute('height', size.y); | |
container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' ')); | |
if (L.Browser.mobileWebkit) { | |
pane.appendChild(container); | |
} | |
}, | |
// methods below are called by vector layers implementations | |
_initPath: function (layer) { | |
var path = layer._path = L.SVG.create('path'); | |
if (layer.options.className) { | |
L.DomUtil.addClass(path, layer.options.className); | |
} | |
if (layer.options.clickable) { | |
L.DomUtil.addClass(path, 'leaflet-clickable'); | |
} | |
this._updateStyle(layer); | |
}, | |
_addPath: function (layer) { | |
var path = layer._path; | |
this._container.appendChild(path); | |
this._paths[L.stamp(path)] = layer; | |
}, | |
_removePath: function (layer) { | |
var path = layer._path; | |
L.DomUtil.remove(path); | |
delete this._paths[L.stamp(path)]; | |
}, | |
_updatePath: function (layer) { | |
layer._project(); | |
layer._update(); | |
}, | |
_updateStyle: function (layer) { | |
var path = layer._path, | |
options = layer.options; | |
if (!path) { return; } | |
if (options.stroke) { | |
path.setAttribute('stroke', options.color); | |
path.setAttribute('stroke-opacity', options.opacity); | |
path.setAttribute('stroke-width', options.weight); | |
path.setAttribute('stroke-linecap', options.lineCap); | |
path.setAttribute('stroke-linejoin', options.lineJoin); | |
if (options.dashArray) { | |
path.setAttribute('stroke-dasharray', options.dashArray); | |
} else { | |
path.removeAttribute('stroke-dasharray'); | |
} | |
if (options.dashOffset) { | |
path.setAttribute('stroke-dashoffset', options.dashOffset); | |
} else { | |
path.removeAttribute('stroke-dashoffset'); | |
} | |
} else { | |
path.setAttribute('stroke', 'none'); | |
} | |
if (options.fill) { | |
path.setAttribute('fill', options.fillColor || options.color); | |
path.setAttribute('fill-opacity', options.fillOpacity); | |
path.setAttribute('fill-rule', 'evenodd'); | |
} else { | |
path.setAttribute('fill', 'none'); | |
} | |
path.setAttribute('pointer-events', options.pointerEvents || (options.clickable ? 'visiblePainted' : 'none')); | |
}, | |
_updatePoly: function (layer, closed) { | |
this._setPath(layer, L.SVG.pointsToPath(layer._parts, closed)); | |
}, | |
_updateCircle: function (layer) { | |
var p = layer._point, | |
r = layer._radius, | |
r2 = layer._radiusY || r, | |
arc = 'a' + r + ',' + r2 + ' 0 1,0 '; | |
// drawing a circle with two half-arcs | |
var d = layer._empty() ? 'M0 0' : | |
'M' + (p.x - r) + ',' + p.y + | |
arc + (r * 2) + ',0 ' + | |
arc + (-r * 2) + ',0 '; | |
this._setPath(layer, d); | |
}, | |
_setPath: function (layer, path) { | |
layer._path.setAttribute('d', path); | |
}, | |
// SVG does not have the concept of zIndex so we resort to changing the DOM order of elements | |
_bringToFront: function (layer) { | |
L.DomUtil.toFront(layer._path); | |
}, | |
_bringToBack: function (layer) { | |
L.DomUtil.toBack(layer._path); | |
}, | |
// TODO remove duplication with L.Map | |
_initEvents: function () { | |
L.DomEvent.on(this._container, 'click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu', | |
this._fireMouseEvent, this); | |
}, | |
_fireMouseEvent: function (e) { | |
this._paths[L.stamp(e.target || e.srcElement)]._fireMouseEvent(e); | |
} | |
}); | |
L.extend(L.SVG, { | |
create: function (name) { | |
return document.createElementNS('http://www.w3.org/2000/svg', name); | |
}, | |
// generates SVG path string for multiple rings, with each ring turning into "M..L..L.." instructions | |
pointsToPath: function (rings, closed) { | |
var str = '', | |
i, j, len, len2, points, p; | |
for (i = 0, len = rings.length; i < len; i++) { | |
points = rings[i]; | |
for (j = 0, len2 = points.length; j < len2; j++) { | |
p = points[j]; | |
str += (j ? 'L' : 'M') + p.x + ' ' + p.y; | |
} | |
// closes the ring for polygons; "x" is VML syntax | |
str += closed ? (L.Browser.svg ? 'z' : 'x') : ''; | |
} | |
// SVG complains about empty path strings | |
return str || 'M0 0'; | |
} | |
}); | |
L.Browser.svg = !!(document.createElementNS && L.SVG.create('svg').createSVGRect); | |
L.svg = function (options) { | |
return L.Browser.svg || L.Browser.vml ? new L.SVG(options) : null; | |
}; | |
/* | |
* Vector rendering for IE7-8 through VML. | |
* Thanks to Dmitry Baranovsky and his Raphael library for inspiration! | |
*/ | |
L.Browser.vml = !L.Browser.svg && (function () { | |
try { | |
var div = document.createElement('div'); | |
div.innerHTML = '<v:shape adj="1"/>'; | |
var shape = div.firstChild; | |
shape.style.behavior = 'url(#default#VML)'; | |
return shape && (typeof shape.adj === 'object'); | |
} catch (e) { | |
return false; | |
} | |
}()); | |
// redefine some SVG methods to handle VML syntax which is similar but with some differences | |
L.SVG.include(!L.Browser.vml ? {} : { | |
_initContainer: function () { | |
this._container = L.DomUtil.create('div', 'leaflet-vml-container'); | |
this._paths = {}; | |
this._initEvents(); | |
}, | |
_update: function () { | |
if (this._map._animatingZoom) { return; } | |
L.Renderer.prototype._update.call(this); | |
}, | |
_initPath: function (layer) { | |
var container = layer._container = L.SVG.create('shape'); | |
L.DomUtil.addClass(container, 'leaflet-vml-shape ' + (this.options.className || '')); | |
container.coordsize = '1 1'; | |
layer._path = L.SVG.create('path'); | |
container.appendChild(layer._path); | |
this._updateStyle(layer); | |
}, | |
_addPath: function (layer) { | |
var container = layer._container; | |
this._container.appendChild(container); | |
this._paths[L.stamp(container)] = layer; | |
}, | |
_removePath: function (layer) { | |
var container = layer._container; | |
L.DomUtil.remove(container); | |
delete this._paths[L.stamp(container)]; | |
}, | |
_updateStyle: function (layer) { | |
var stroke = layer._stroke, | |
fill = layer._fill, | |
options = layer.options, | |
container = layer._container; | |
container.stroked = !!options.stroke; | |
container.filled = !!options.fill; | |
if (options.stroke) { | |
if (!stroke) { | |
stroke = layer._stroke = L.SVG.create('stroke'); | |
container.appendChild(stroke); | |
} | |
stroke.weight = options.weight + 'px'; | |
stroke.color = options.color; | |
stroke.opacity = options.opacity; | |
if (options.dashArray) { | |
stroke.dashStyle = L.Util.isArray(options.dashArray) ? | |
options.dashArray.join(' ') : | |
options.dashArray.replace(/( *, *)/g, ' '); | |
} else { | |
stroke.dashStyle = ''; | |
} | |
stroke.endcap = options.lineCap.replace('butt', 'flat'); | |
stroke.joinstyle = options.lineJoin; | |
} else if (stroke) { | |
container.removeChild(stroke); | |
layer._stroke = null; | |
} | |
if (options.fill) { | |
if (!fill) { | |
fill = layer._fill = L.SVG.create('fill'); | |
container.appendChild(fill); | |
} | |
fill.color = options.fillColor || options.color; | |
fill.opacity = options.fillOpacity; | |
} else if (fill) { | |
container.removeChild(fill); | |
layer._fill = null; | |
} | |
}, | |
_updateCircle: function (layer) { | |
var p = layer._point, | |
r = Math.round(layer._radius), | |
r2 = Math.round(layer._radiusY || r); | |
this._setPath(layer, layer._empty() ? 'M0 0' : | |
'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r2 + ' 0,' + (65535 * 360)); | |
}, | |
_setPath: function (layer, path) { | |
layer._path.v = path; | |
} | |
}); | |
if (L.Browser.vml) { | |
L.SVG.create = (function () { | |
try { | |
document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml'); | |
return function (name) { | |
return document.createElement('<lvml:' + name + ' class="lvml">'); | |
}; | |
} catch (e) { | |
return function (name) { | |
return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">'); | |
}; | |
} | |
})(); | |
} | |
/* | |
* L.Canvas handles Canvas vector layers rendering and mouse events handling. All Canvas-specific code goes here. | |
*/ | |
L.Canvas = L.Renderer.extend({ | |
onAdd: function () { | |
L.Renderer.prototype.onAdd.call(this); | |
this._layers = this._layers || {}; | |
// redraw vectors since canvas is cleared upon removal | |
this._draw(); | |
}, | |
_initContainer: function () { | |
var container = this._container = document.createElement('canvas'); | |
L.DomEvent | |
.on(container, 'mousemove', this._onMouseMove, this) | |
.on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this); | |
this._ctx = container.getContext('2d'); | |
}, | |
_update: function () { | |
if (this._map._animatingZoom && this._bounds) { return; } | |
L.Renderer.prototype._update.call(this); | |
var b = this._bounds, | |
container = this._container, | |
size = b.getSize(), | |
m = L.Browser.retina ? 2 : 1; | |
L.DomUtil.setPosition(container, b.min); | |
// set canvas size (also clearing it); use double size on retina | |
container.width = m * size.x; | |
container.height = m * size.y; | |
container.style.width = size.x + 'px'; | |
container.style.height = size.y + 'px'; | |
if (L.Browser.retina) { | |
this._ctx.scale(2, 2); | |
} | |
// translate so we use the same path coordinates after canvas element moves | |
this._ctx.translate(-b.min.x, -b.min.y); | |
}, | |
_initPath: function (layer) { | |
this._layers[L.stamp(layer)] = layer; | |
}, | |
_addPath: L.Util.falseFn, | |
_removePath: function (layer) { | |
layer._removed = true; | |
this._requestRedraw(layer); | |
}, | |
_updatePath: function (layer) { | |
this._redrawBounds = layer._pxBounds; | |
this._draw(true); | |
layer._project(); | |
layer._update(); | |
this._draw(); | |
this._redrawBounds = null; | |
}, | |
_updateStyle: function (layer) { | |
this._requestRedraw(layer); | |
}, | |
_requestRedraw: function (layer) { | |
if (!this._map) { return; } | |
this._redrawBounds = this._redrawBounds || new L.Bounds(); | |
this._redrawBounds.extend(layer._pxBounds.min).extend(layer._pxBounds.max); | |
this._redrawRequest = this._redrawRequest || L.Util.requestAnimFrame(this._redraw, this); | |
}, | |
_redraw: function () { | |
this._redrawRequest = null; | |
this._draw(true); // clear layers in redraw bounds | |
this._draw(); // draw layers | |
this._redrawBounds = null; | |
}, | |
_draw: function (clear) { | |
this._clear = clear; | |
var layer; | |
for (var id in this._layers) { | |
layer = this._layers[id]; | |
if (!this._redrawBounds || layer._pxBounds.intersects(this._redrawBounds)) { | |
layer._updatePath(); | |
} | |
if (clear && layer._removed) { | |
delete layer._removed; | |
delete this._layers[id]; | |
} | |
} | |
}, | |
_updatePoly: function (layer, closed) { | |
var i, j, len2, p, | |
parts = layer._parts, | |
len = parts.length, | |
ctx = this._ctx; | |
if (!len) { return; } | |
ctx.beginPath(); | |
for (i = 0; i < len; i++) { | |
for (j = 0, len2 = parts[i].length; j < len2; j++) { | |
p = parts[i][j]; | |
ctx[j ? 'lineTo' : 'moveTo'](p.x, p.y); | |
} | |
if (closed) { | |
ctx.closePath(); | |
} | |
} | |
this._fillStroke(ctx, layer); | |
// TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature | |
}, | |
_updateCircle: function (layer) { | |
if (layer._empty()) { return; } | |
var p = layer._point, | |
ctx = this._ctx, | |
r = layer._radius, | |
s = (layer._radiusY || r) / r; | |
if (s !== 1) { | |
ctx.save(); | |
ctx.scale(1, s); | |
} | |
ctx.beginPath(); | |
ctx.arc(p.x, p.y / s, r, 0, Math.PI * 2, false); | |
if (s !== 1) { | |
ctx.restore(); | |
} | |
this._fillStroke(ctx, layer); | |
}, | |
_fillStroke: function (ctx, layer) { | |
var clear = this._clear, | |
options = layer.options; | |
ctx.globalCompositeOperation = clear ? 'destination-out' : 'source-over'; | |
if (options.fill) { | |
ctx.globalAlpha = clear ? 1 : options.fillOpacity; | |
ctx.fillStyle = options.fillColor || options.color; | |
ctx.fill('evenodd'); | |
} | |
if (options.stroke) { | |
ctx.globalAlpha = clear ? 1 : options.opacity; | |
// if clearing shape, do it with the previously drawn line width | |
layer._prevWeight = ctx.lineWidth = clear ? layer._prevWeight + 1 : options.weight; | |
ctx.strokeStyle = options.color; | |
ctx.lineCap = options.lineCap; | |
ctx.lineJoin = options.lineJoin; | |
ctx.stroke(); | |
} | |
}, | |
// Canvas obviously doesn't have mouse events for individual drawn objects, | |
// so we emulate that by calculating what's under the mouse on mousemove/click manually | |
_onClick: function (e) { | |
var point = this._map.mouseEventToLayerPoint(e); | |
for (var id in this._layers) { | |
if (this._layers[id]._containsPoint(point)) { | |
this._layers[id]._fireMouseEvent(e); | |
} | |
} | |
}, | |
_onMouseMove: function (e) { | |
if (!this._map || this._map._animatingZoom) { return; } | |
var point = this._map.mouseEventToLayerPoint(e); | |
// TODO don't do on each move event, throttle since it's expensive | |
for (var id in this._layers) { | |
this._handleHover(this._layers[id], e, point); | |
} | |
}, | |
_handleHover: function (layer, e, point) { | |
if (!layer.options.clickable) { return; } | |
if (layer._containsPoint(point)) { | |
// if we just got inside the layer, fire mouseover | |
if (!layer._mouseInside) { | |
L.DomUtil.addClass(this._container, 'leaflet-clickable'); // change cursor | |
layer._fireMouseEvent(e, 'mouseover'); | |
layer._mouseInside = true; | |
} | |
// fire mousemove | |
layer._fireMouseEvent(e); | |
} else if (layer._mouseInside) { | |
// if we're leaving the layer, fire mouseout | |
L.DomUtil.removeClass(this._container, 'leaflet-clickable'); | |
layer._fireMouseEvent(e, 'mouseout'); | |
layer._mouseInside = false; | |
} | |
}, | |
// TODO _bringToFront & _bringToBack, pretty tricky | |
_bringToFront: L.Util.falseFn, | |
_bringToBack: L.Util.falseFn | |
}); | |
L.Browser.canvas = (function () { | |
return !!document.createElement('canvas').getContext; | |
}()); | |
L.canvas = function (options) { | |
return L.Browser.canvas ? new L.Canvas(options) : null; | |
}; | |
L.Polyline.prototype._containsPoint = function (p, closed) { | |
var i, j, k, len, len2, part, | |
w = this._clickTolerance(); | |
if (!this._pxBounds.contains(p)) { return false; } | |
// hit detection for polylines | |
for (i = 0, len = this._parts.length; i < len; i++) { | |
part = this._parts[i]; | |
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { | |
if (!closed && (j === 0)) { continue; } | |
if (L.LineUtil.pointToSegmentDistance(p, part[k], part[j]) <= w) { | |
return true; | |
} | |
} | |
} | |
return false; | |
}; | |
L.Polygon.prototype._containsPoint = function (p) { | |
var inside = false, | |
part, p1, p2, i, j, k, len, len2; | |
if (!this._pxBounds.contains(p)) { return false; } | |
// ray casting algorithm for detecting if point is in polygon | |
for (i = 0, len = this._parts.length; i < len; i++) { | |
part = this._parts[i]; | |
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { | |
p1 = part[j]; | |
p2 = part[k]; | |
if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { | |
inside = !inside; | |
} | |
} | |
} | |
// also check if it's on polygon stroke | |
return inside || L.Polyline.prototype._containsPoint.call(this, p, true); | |
}; | |
L.CircleMarker.prototype._containsPoint = function (p) { | |
return p.distanceTo(this._point) <= this._radius + this._clickTolerance(); | |
}; | |
/* | |
* L.GeoJSON turns any GeoJSON data into a Leaflet layer. | |
*/ | |
L.GeoJSON = L.FeatureGroup.extend({ | |
initialize: function (geojson, options) { | |
L.setOptions(this, options); | |
this._layers = {}; | |
if (geojson) { | |
this.addData(geojson); | |
} | |
}, | |
addData: function (geojson) { | |
var features = L.Util.isArray(geojson) ? geojson : geojson.features, | |
i, len, feature; | |
if (features) { | |
for (i = 0, len = features.length; i < len; i++) { | |
// Only add this if geometry or geometries are set and not null | |
feature = features[i]; | |
if (feature.geometries || feature.geometry || feature.features || feature.coordinates) { | |
this.addData(feature); | |
} | |
} | |
return this; | |
} | |
var options = this.options; | |
if (options.filter && !options.filter(geojson)) { return; } | |
var layer = L.GeoJSON.geometryToLayer(geojson, options); | |
layer.feature = L.GeoJSON.asFeature(geojson); | |
layer.defaultOptions = layer.options; | |
this.resetStyle(layer); | |
if (options.onEachFeature) { | |
options.onEachFeature(geojson, layer); | |
} | |
return this.addLayer(layer); | |
}, | |
resetStyle: function (layer) { | |
// reset any custom styles | |
layer.options = layer.defaultOptions; | |
this._setLayerStyle(layer, this.options.style); | |
return this; | |
}, | |
setStyle: function (style) { | |
return this.eachLayer(function (layer) { | |
this._setLayerStyle(layer, style); | |
}, this); | |
}, | |
_setLayerStyle: function (layer, style) { | |
if (typeof style === 'function') { | |
style = style(layer.feature); | |
} | |
if (layer.setStyle) { | |
layer.setStyle(style); | |
} | |
} | |
}); | |
L.extend(L.GeoJSON, { | |
geometryToLayer: function (geojson, options) { | |
var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson, | |
coords = geometry.coordinates, | |
layers = [], | |
pointToLayer = options && options.pointToLayer, | |
coordsToLatLng = options && options.coordsToLatLng || this.coordsToLatLng, | |
latlng, latlngs, i, len; | |
switch (geometry.type) { | |
case 'Point': | |
latlng = coordsToLatLng(coords); | |
return pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng); | |
case 'MultiPoint': | |
for (i = 0, len = coords.length; i < len; i++) { | |
latlng = coordsToLatLng(coords[i]); | |
layers.push(pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng)); | |
} | |
return new L.FeatureGroup(layers); | |
case 'LineString': | |
case 'MultiLineString': | |
latlngs = this.coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, coordsToLatLng); | |
return new L.Polyline(latlngs, options); | |
case 'Polygon': | |
case 'MultiPolygon': | |
latlngs = this.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, coordsToLatLng); | |
return new L.Polygon(latlngs, options); | |
case 'GeometryCollection': | |
for (i = 0, len = geometry.geometries.length; i < len; i++) { | |
layers.push(this.geometryToLayer({ | |
geometry: geometry.geometries[i], | |
type: 'Feature', | |
properties: geojson.properties | |
}, options)); | |
} | |
return new L.FeatureGroup(layers); | |
default: | |
throw new Error('Invalid GeoJSON object.'); | |
} | |
}, | |
coordsToLatLng: function (coords) { | |
return new L.LatLng(coords[1], coords[0], coords[2]); | |
}, | |
coordsToLatLngs: function (coords, levelsDeep, coordsToLatLng) { | |
var latlngs = []; | |
for (var i = 0, len = coords.length, latlng; i < len; i++) { | |
latlng = levelsDeep ? | |
this.coordsToLatLngs(coords[i], levelsDeep - 1, coordsToLatLng) : | |
(coordsToLatLng || this.coordsToLatLng)(coords[i]); | |
latlngs.push(latlng); | |
} | |
return latlngs; | |
}, | |
latLngToCoords: function (latlng) { | |
return latlng.alt !== undefined ? | |
[latlng.lng, latlng.lat, latlng.alt] : | |
[latlng.lng, latlng.lat]; | |
}, | |
latLngsToCoords: function (latlngs, levelsDeep, closed) { | |
var coords = []; | |
for (var i = 0, len = latlngs.length; i < len; i++) { | |
coords.push(levelsDeep ? | |
L.GeoJSON.latLngsToCoords(latlngs[i], levelsDeep - 1, closed): | |
L.GeoJSON.latLngToCoords(latlngs[i])); | |
} | |
if (!levelsDeep && closed) { | |
coords.push(coords[0]); | |
} | |
return coords; | |
}, | |
getFeature: function (layer, newGeometry) { | |
return layer.feature ? | |
L.extend({}, layer.feature, {geometry: newGeometry}) : | |
L.GeoJSON.asFeature(newGeometry); | |
}, | |
asFeature: function (geoJSON) { | |
if (geoJSON.type === 'Feature') { | |
return geoJSON; | |
} | |
return { | |
type: 'Feature', | |
properties: {}, | |
geometry: geoJSON | |
}; | |
} | |
}); | |
var PointToGeoJSON = { | |
toGeoJSON: function () { | |
return L.GeoJSON.getFeature(this, { | |
type: 'Point', | |
coordinates: L.GeoJSON.latLngToCoords(this.getLatLng()) | |
}); | |
} | |
}; | |
L.Marker.include(PointToGeoJSON); | |
L.Circle.include(PointToGeoJSON); | |
L.CircleMarker.include(PointToGeoJSON); | |
L.Polyline.prototype.toGeoJSON = function () { | |
var multi = !this._flat(this._latlngs); | |
var coords = L.GeoJSON.latLngsToCoords(this._latlngs, multi ? 1 : 0); | |
return L.GeoJSON.getFeature(this, { | |
type: (multi ? 'Multi' : '') + 'LineString', | |
coordinates: coords | |
}); | |
}; | |
L.Polygon.prototype.toGeoJSON = function () { | |
var holes = !this._flat(this._latlngs), | |
multi = holes && !this._flat(this._latlngs[0]); | |
var coords = L.GeoJSON.latLngsToCoords(this._latlngs, multi ? 2 : holes ? 1 : 0, true); | |
if (holes && this._latlngs.length === 1) { | |
multi = true; | |
coords = [coords]; | |
} | |
if (!holes) { | |
coords = [coords]; | |
} | |
return L.GeoJSON.getFeature(this, { | |
type: (multi ? 'Multi' : '') + 'Polygon', | |
coordinates: coords | |
}); | |
}; | |
L.LayerGroup.include({ | |
toMultiPoint: function () { | |
var coords = []; | |
this.eachLayer(function (layer) { | |
coords.push(layer.toGeoJSON().geometry.coordinates); | |
}); | |
return L.GeoJSON.getFeature(this, { | |
type: 'MultiPoint', | |
coordinates: coords | |
}); | |
}, | |
toGeoJSON: function () { | |
var type = this.feature && this.feature.geometry && this.feature.geometry.type; | |
if (type === 'MultiPoint') { | |
return this.toMultiPoint(); | |
} | |
var isGeometryCollection = type === 'GeometryCollection', | |
jsons = []; | |
this.eachLayer(function (layer) { | |
if (layer.toGeoJSON) { | |
var json = layer.toGeoJSON(); | |
jsons.push(isGeometryCollection ? json.geometry : L.GeoJSON.asFeature(json)); | |
} | |
}); | |
if (isGeometryCollection) { | |
return L.GeoJSON.getFeature(this, { | |
geometries: jsons, | |
type: 'GeometryCollection' | |
}); | |
} | |
return { | |
type: 'FeatureCollection', | |
features: jsons | |
}; | |
} | |
}); | |
L.geoJson = function (geojson, options) { | |
return new L.GeoJSON(geojson, options); | |
}; | |
/* | |
* L.DomEvent contains functions for working with DOM events. | |
* Inspired by John Resig, Dean Edwards and YUI addEvent implementations. | |
*/ | |
var eventsKey = '_leaflet_events'; | |
L.DomEvent = { | |
on: function (obj, types, fn, context) { | |
if (typeof types === 'object') { | |
for (var type in types) { | |
this._on(obj, type, types[type], fn); | |
} | |
} else { | |
types = L.Util.splitWords(types); | |
for (var i = 0, len = types.length; i < len; i++) { | |
this._on(obj, types[i], fn, context); | |
} | |
} | |
return this; | |
}, | |
off: function (obj, types, fn, context) { | |
if (typeof types === 'object') { | |
for (var type in types) { | |
this._off(obj, type, types[type], fn); | |
} | |
} else { | |
types = L.Util.splitWords(types); | |
for (var i = 0, len = types.length; i < len; i++) { | |
this._off(obj, types[i], fn, context); | |
} | |
} | |
return this; | |
}, | |
_on: function (obj, type, fn, context) { | |
var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : ''); | |
if (obj[eventsKey] && obj[eventsKey][id]) { return this; } | |
var handler = function (e) { | |
return fn.call(context || obj, e || window.event); | |
}; | |
var originalHandler = handler; | |
if (L.Browser.pointer && type.indexOf('touch') === 0) { | |
return this.addPointerListener(obj, type, handler, id); | |
} | |
if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) { | |
this.addDoubleTapListener(obj, handler, id); | |
} | |
if ('addEventListener' in obj) { | |
if (type === 'mousewheel') { | |
obj.addEventListener('DOMMouseScroll', handler, false); | |
obj.addEventListener(type, handler, false); | |
} else if ((type === 'mouseenter') || (type === 'mouseleave')) { | |
handler = function (e) { | |
e = e || window.event; | |
if (!L.DomEvent._checkMouse(obj, e)) { return; } | |
return originalHandler(e); | |
}; | |
obj.addEventListener(type === 'mouseenter' ? 'mouseover' : 'mouseout', handler, false); | |
} else { | |
if (type === 'click' && L.Browser.android) { | |
handler = function (e) { | |
return L.DomEvent._filterClick(e, originalHandler); | |
}; | |
} | |
obj.addEventListener(type, handler, false); | |
} | |
} else if ('attachEvent' in obj) { | |
obj.attachEvent('on' + type, handler); | |
} | |
obj[eventsKey] = obj[eventsKey] || {}; | |
obj[eventsKey][id] = handler; | |
return this; | |
}, | |
_off: function (obj, type, fn, context) { | |
var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : ''), | |
handler = obj[eventsKey] && obj[eventsKey][id]; | |
if (!handler) { return this; } | |
if (L.Browser.pointer && type.indexOf('touch') === 0) { | |
this.removePointerListener(obj, type, id); | |
} else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) { | |
this.removeDoubleTapListener(obj, id); | |
} else if ('removeEventListener' in obj) { | |
if (type === 'mousewheel') { | |
obj.removeEventListener('DOMMouseScroll', handler, false); | |
obj.removeEventListener(type, handler, false); | |
} else { | |
obj.removeEventListener( | |
type === 'mouseenter' ? 'mouseover' : | |
type === 'mouseleave' ? 'mouseout' : type, handler, false); | |
} | |
} else if ('detachEvent' in obj) { | |
obj.detachEvent('on' + type, handler); | |
} | |
obj[eventsKey][id] = null; | |
return this; | |
}, | |
stopPropagation: function (e) { | |
if (e.stopPropagation) { | |
e.stopPropagation(); | |
} else { | |
e.cancelBubble = true; | |
} | |
L.DomEvent._skipped(e); | |
return this; | |
}, | |
disableScrollPropagation: function (el) { | |
return L.DomEvent.on(el, 'mousewheel MozMousePixelScroll', L.DomEvent.stopPropagation); | |
}, | |
disableClickPropagation: function (el) { | |
var stop = L.DomEvent.stopPropagation; | |
L.DomEvent.on(el, L.Draggable.START.join(' '), stop); | |
return L.DomEvent.on(el, { | |
click: L.DomEvent._fakeStop, | |
dblclick: stop | |
}); | |
}, | |
preventDefault: function (e) { | |
if (e.preventDefault) { | |
e.preventDefault(); | |
} else { | |
e.returnValue = false; | |
} | |
return this; | |
}, | |
stop: function (e) { | |
return L.DomEvent | |
.preventDefault(e) | |
.stopPropagation(e); | |
}, | |
getMousePosition: function (e, container) { | |
if (!container) { | |
return new L.Point(e.clientX, e.clientY); | |
} | |
var rect = container.getBoundingClientRect(); | |
return new L.Point( | |
e.clientX - rect.left - container.clientLeft, | |
e.clientY - rect.top - container.clientTop); | |
}, | |
getWheelDelta: function (e) { | |
var delta = 0; | |
if (e.wheelDelta) { | |
delta = e.wheelDelta / 120; | |
} | |
if (e.detail) { | |
delta = -e.detail / 3; | |
} | |
return delta; | |
}, | |
_skipEvents: {}, | |
_fakeStop: function (e) { | |
// fakes stopPropagation by setting a special event flag, checked/reset with L.DomEvent._skipped(e) | |
L.DomEvent._skipEvents[e.type] = true; | |
}, | |
_skipped: function (e) { | |
var skipped = this._skipEvents[e.type]; | |
// reset when checking, as it's only used in map container and propagates outside of the map | |
this._skipEvents[e.type] = false; | |
return skipped; | |
}, | |
// check if element really left/entered the event target (for mouseenter/mouseleave) | |
_checkMouse: function (el, e) { | |
var related = e.relatedTarget; | |
if (!related) { return true; } | |
try { | |
while (related && (related !== el)) { | |
related = related.parentNode; | |
} | |
} catch (err) { | |
return false; | |
} | |
return (related !== el); | |
}, | |
// this is a horrible workaround for a bug in Android where a single touch triggers two click events | |
_filterClick: function (e, handler) { | |
var timeStamp = (e.timeStamp || e.originalEvent.timeStamp), | |
elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick); | |
// are they closer together than 500ms yet more than 100ms? | |
// Android typically triggers them ~300ms apart while multiple listeners | |
// on the same event should be triggered far faster; | |
// or check if click is simulated on the element, and if it is, reject any non-simulated events | |
if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) { | |
L.DomEvent.stop(e); | |
return; | |
} | |
L.DomEvent._lastClick = timeStamp; | |
return handler(e); | |
} | |
}; | |
L.DomEvent.addListener = L.DomEvent.on; | |
L.DomEvent.removeListener = L.DomEvent.off; | |
/* | |
* L.Draggable allows you to add dragging capabilities to any element. Supports mobile devices too. | |
*/ | |
L.Draggable = L.Evented.extend({ | |
statics: { | |
START: L.Browser.touch ? ['touchstart', 'mousedown'] : ['mousedown'], | |
END: { | |
mousedown: 'mouseup', | |
touchstart: 'touchend', | |
pointerdown: 'touchend', | |
MSPointerDown: 'touchend' | |
}, | |
MOVE: { | |
mousedown: 'mousemove', | |
touchstart: 'touchmove', | |
pointerdown: 'touchmove', | |
MSPointerDown: 'touchmove' | |
} | |
}, | |
initialize: function (element, dragStartTarget) { | |
this._element = element; | |
this._dragStartTarget = dragStartTarget || element; | |
}, | |
enable: function () { | |
if (this._enabled) { return; } | |
L.DomEvent.on(this._dragStartTarget, L.Draggable.START.join(' '), this._onDown, this); | |
this._enabled = true; | |
}, | |
disable: function () { | |
if (!this._enabled) { return; } | |
L.DomEvent.off(this._dragStartTarget, L.Draggable.START.join(' '), this._onDown, this); | |
this._enabled = false; | |
this._moved = false; | |
}, | |
_onDown: function (e) { | |
this._moved = false; | |
if (e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; } | |
L.DomEvent.stopPropagation(e); | |
if (L.Draggable._disabled) { return; } | |
L.DomUtil.disableImageDrag(); | |
L.DomUtil.disableTextSelection(); | |
if (this._moving) { return; } | |
this.fire('down'); | |
var first = e.touches ? e.touches[0] : e; | |
this._startPoint = new L.Point(first.clientX, first.clientY); | |
this._startPos = this._newPos = L.DomUtil.getPosition(this._element); | |
L.DomEvent | |
.on(document, L.Draggable.MOVE[e.type], this._onMove, this) | |
.on(document, L.Draggable.END[e.type], this._onUp, this); | |
}, | |
_onMove: function (e) { | |
if (e.touches && e.touches.length > 1) { | |
this._moved = true; | |
return; | |
} | |
var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e), | |
newPoint = new L.Point(first.clientX, first.clientY), | |
offset = newPoint.subtract(this._startPoint); | |
if (!offset.x && !offset.y) { return; } | |
if (L.Browser.touch && Math.abs(offset.x) + Math.abs(offset.y) < 3) { return; } | |
L.DomEvent.preventDefault(e); | |
if (!this._moved) { | |
this.fire('dragstart'); | |
this._moved = true; | |
this._startPos = L.DomUtil.getPosition(this._element).subtract(offset); | |
L.DomUtil.addClass(document.body, 'leaflet-dragging'); | |
L.DomUtil.addClass(e.target || e.srcElement, 'leaflet-drag-target'); | |
} | |
this._newPos = this._startPos.add(offset); | |
this._moving = true; | |
L.Util.cancelAnimFrame(this._animRequest); | |
this._animRequest = L.Util.requestAnimFrame(this._updatePosition, this, true, this._dragStartTarget); | |
}, | |
_updatePosition: function () { | |
this.fire('predrag'); | |
L.DomUtil.setPosition(this._element, this._newPos); | |
this.fire('drag'); | |
}, | |
_onUp: function (e) { | |
L.DomUtil.removeClass(document.body, 'leaflet-dragging'); | |
L.DomUtil.removeClass(e.target || e.srcElement, 'leaflet-drag-target'); | |
for (var i in L.Draggable.MOVE) { | |
L.DomEvent | |
.off(document, L.Draggable.MOVE[i], this._onMove, this) | |
.off(document, L.Draggable.END[i], this._onUp, this); | |
} | |
L.DomUtil.enableImageDrag(); | |
L.DomUtil.enableTextSelection(); | |
if (this._moved && this._moving) { | |
// ensure drag is not fired after dragend | |
L.Util.cancelAnimFrame(this._animRequest); | |
this.fire('dragend', { | |
distance: this._newPos.distanceTo(this._startPos) | |
}); | |
} | |
this._moving = false; | |
} | |
}); | |
/* | |
L.Handler is a base class for handler classes that are used internally to inject | |
interaction features like dragging to classes like Map and Marker. | |
*/ | |
L.Handler = L.Class.extend({ | |
initialize: function (map) { | |
this._map = map; | |
}, | |
enable: function () { | |
if (this._enabled) { return; } | |
this._enabled = true; | |
this.addHooks(); | |
}, | |
disable: function () { | |
if (!this._enabled) { return; } | |
this._enabled = false; | |
this.removeHooks(); | |
}, | |
enabled: function () { | |
return !!this._enabled; | |
} | |
}); | |
/* | |
* L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default. | |
*/ | |
L.Map.mergeOptions({ | |
dragging: true, | |
inertia: !L.Browser.android23, | |
inertiaDeceleration: 3400, // px/s^2 | |
inertiaMaxSpeed: Infinity, // px/s | |
inertiaThreshold: L.Browser.touch ? 32 : 18, // ms | |
easeLinearity: 0.25, | |
// TODO refactor, move to CRS | |
worldCopyJump: false | |
}); | |
L.Map.Drag = L.Handler.extend({ | |
addHooks: function () { | |
if (!this._draggable) { | |
var map = this._map; | |
this._draggable = new L.Draggable(map._mapPane, map._container); | |
this._draggable.on({ | |
down: this._onDown, | |
dragstart: this._onDragStart, | |
drag: this._onDrag, | |
dragend: this._onDragEnd | |
}, this); | |
if (map.options.worldCopyJump) { | |
this._draggable.on('predrag', this._onPreDrag, this); | |
map.on('viewreset', this._onViewReset, this); | |
map.whenReady(this._onViewReset, this); | |
} | |
} | |
this._draggable.enable(); | |
}, | |
removeHooks: function () { | |
this._draggable.disable(); | |
}, | |
moved: function () { | |
return this._draggable && this._draggable._moved; | |
}, | |
_onDown: function () { | |
if (this._map._panAnim) { | |
this._map._panAnim.stop(); | |
} | |
}, | |
_onDragStart: function () { | |
var map = this._map; | |
map | |
.fire('movestart') | |
.fire('dragstart'); | |
if (map.options.inertia) { | |
this._positions = []; | |
this._times = []; | |
} | |
}, | |
_onDrag: function () { | |
if (this._map.options.inertia) { | |
var time = this._lastTime = +new Date(), | |
pos = this._lastPos = this._draggable._newPos; | |
this._positions.push(pos); | |
this._times.push(time); | |
if (time - this._times[0] > 200) { | |
this._positions.shift(); | |
this._times.shift(); | |
} | |
} | |
this._map | |
.fire('move') | |
.fire('drag'); | |
}, | |
_onViewReset: function () { | |
var pxCenter = this._map.getSize().divideBy(2), | |
pxWorldCenter = this._map.latLngToLayerPoint([0, 0]); | |
this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x; | |
this._worldWidth = this._map.getPixelWorldBounds().getSize().x; | |
}, | |
_onPreDrag: function () { | |
// TODO refactor to be able to adjust map pane position after zoom | |
var worldWidth = this._worldWidth, | |
halfWidth = Math.round(worldWidth / 2), | |
dx = this._initialWorldOffset, | |
x = this._draggable._newPos.x, | |
newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx, | |
newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx, | |
newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2; | |
this._draggable._newPos.x = newX; | |
}, | |
_onDragEnd: function (e) { | |
var map = this._map, | |
options = map.options, | |
delay = +new Date() - this._lastTime, | |
noInertia = !options.inertia || delay > options.inertiaThreshold || !this._positions[0]; | |
map.fire('dragend', e); | |
if (noInertia) { | |
map.fire('moveend'); | |
} else { | |
var direction = this._lastPos.subtract(this._positions[0]), | |
duration = (this._lastTime + delay - this._times[0]) / 1000, | |
ease = options.easeLinearity, | |
speedVector = direction.multiplyBy(ease / duration), | |
speed = speedVector.distanceTo([0, 0]), | |
limitedSpeed = Math.min(options.inertiaMaxSpeed, speed), | |
limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed), | |
decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease), | |
offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round(); | |
if (!offset.x || !offset.y) { | |
map.fire('moveend'); | |
} else { | |
offset = map._limitOffset(offset, map.options.maxBounds); | |
L.Util.requestAnimFrame(function () { | |
map.panBy(offset, { | |
duration: decelerationDuration, | |
easeLinearity: ease, | |
noMoveStart: true | |
}); | |
}); | |
} | |
} | |
} | |
}); | |
L.Map.addInitHook('addHandler', 'dragging', L.Map.Drag); | |
/* | |
* L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default. | |
*/ | |
L.Map.mergeOptions({ | |
doubleClickZoom: true | |
}); | |
L.Map.DoubleClickZoom = L.Handler.extend({ | |
addHooks: function () { | |
this._map.on('dblclick', this._onDoubleClick, this); | |
}, | |
removeHooks: function () { | |
this._map.off('dblclick', this._onDoubleClick, this); | |
}, | |
_onDoubleClick: function (e) { | |
var map = this._map, | |
zoom = map.getZoom() + (e.originalEvent.shiftKey ? -1 : 1); | |
if (map.options.doubleClickZoom === 'center') { | |
map.setZoom(zoom); | |
} else { | |
map.setZoomAround(e.containerPoint, zoom); | |
} | |
} | |
}); | |
L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom); | |
/* | |
* L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map. | |
*/ | |
L.Map.mergeOptions({ | |
scrollWheelZoom: true | |
}); | |
L.Map.ScrollWheelZoom = L.Handler.extend({ | |
addHooks: function () { | |
L.DomEvent.on(this._map._container, { | |
mousewheel: this._onWheelScroll, | |
MozMousePixelScroll: L.DomEvent.preventDefault | |
}, this); | |
this._delta = 0; | |
}, | |
removeHooks: function () { | |
L.DomEvent.off(this._map._container, { | |
mousewheel: this._onWheelScroll, | |
MozMousePixelScroll: L.DomEvent.preventDefault | |
}, this); | |
}, | |
_onWheelScroll: function (e) { | |
var delta = L.DomEvent.getWheelDelta(e); | |
this._delta += delta; | |
this._lastMousePos = this._map.mouseEventToContainerPoint(e); | |
if (!this._startTime) { | |
this._startTime = +new Date(); | |
} | |
var left = Math.max(40 - (+new Date() - this._startTime), 0); | |
clearTimeout(this._timer); | |
this._timer = setTimeout(L.bind(this._performZoom, this), left); | |
L.DomEvent.stop(e); | |
}, | |
_performZoom: function () { | |
var map = this._map, | |
delta = this._delta, | |
zoom = map.getZoom(); | |
delta = delta > 0 ? Math.ceil(delta) : Math.floor(delta); | |
delta = Math.max(Math.min(delta, 4), -4); | |
delta = map._limitZoom(zoom + delta) - zoom; | |
this._delta = 0; | |
this._startTime = null; | |
if (!delta) { return; } | |
if (map.options.scrollWheelZoom === 'center') { | |
map.setZoom(zoom + delta); | |
} else { | |
map.setZoomAround(this._lastMousePos, zoom + delta); | |
} | |
} | |
}); | |
L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom); | |
/* | |
* Extends the event handling code with double tap support for mobile browsers. | |
*/ | |
L.extend(L.DomEvent, { | |
_touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart', | |
_touchend: L.Browser.msPointer ? 'MSPointerUp' : L.Browser.pointer ? 'pointerup' : 'touchend', | |
// inspired by Zepto touch code by Thomas Fuchs | |
addDoubleTapListener: function (obj, handler, id) { | |
var last, touch, | |
doubleTap = false, | |
delay = 250, | |
trackedTouches = []; | |
function onTouchStart(e) { | |
var count; | |
if (L.Browser.pointer) { | |
trackedTouches.push(e.pointerId); | |
count = trackedTouches.length; | |
} else { | |
count = e.touches.length; | |
} | |
if (count > 1) { return; } | |
var now = Date.now(), | |
delta = now - (last || now); | |
touch = e.touches ? e.touches[0] : e; | |
doubleTap = (delta > 0 && delta <= delay); | |
last = now; | |
} | |
function onTouchEnd(e) { | |
if (L.Browser.pointer) { | |
var idx = trackedTouches.indexOf(e.pointerId); | |
if (idx === -1) { return; } | |
trackedTouches.splice(idx, 1); | |
} | |
if (doubleTap) { | |
if (L.Browser.pointer) { | |
// work around .type being readonly with MSPointer* events | |
var newTouch = {}, | |
prop, i; | |
for (i in touch) { | |
prop = touch[i]; | |
newTouch[i] = prop && prop.bind ? prop.bind(touch) : prop; | |
} | |
touch = newTouch; | |
} | |
touch.type = 'dblclick'; | |
handler(touch); | |
last = null; | |
} | |
} | |
var pre = '_leaflet_', | |
touchstart = this._touchstart, | |
touchend = this._touchend; | |
obj[pre + touchstart + id] = onTouchStart; | |
obj[pre + touchend + id] = onTouchEnd; | |
// on pointer we need to listen on the document, otherwise a drag starting on the map and moving off screen | |
// will not come through to us, so we will lose track of how many touches are ongoing | |
var endElement = L.Browser.pointer ? document.documentElement : obj; | |
obj.addEventListener(touchstart, onTouchStart, false); | |
endElement.addEventListener(touchend, onTouchEnd, false); | |
if (L.Browser.pointer) { | |
endElement.addEventListener(L.DomEvent.POINTER_CANCEL, onTouchEnd, false); | |
} | |
return this; | |
}, | |
removeDoubleTapListener: function (obj, id) { | |
var pre = '_leaflet_', | |
endElement = L.Browser.pointer ? document.documentElement : obj, | |
touchend = obj[pre + this._touchend + id]; | |
obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false); | |
endElement.removeEventListener(this._touchend, touchend, false); | |
if (L.Browser.pointer) { | |
endElement.removeEventListener(L.DomEvent.POINTER_CANCEL, touchend, false); | |
} | |
return this; | |
} | |
}); | |
/* | |
* Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices. | |
*/ | |
L.extend(L.DomEvent, { | |
POINTER_DOWN: L.Browser.msPointer ? 'MSPointerDown' : 'pointerdown', | |
POINTER_MOVE: L.Browser.msPointer ? 'MSPointerMove' : 'pointermove', | |
POINTER_UP: L.Browser.msPointer ? 'MSPointerUp' : 'pointerup', | |
POINTER_CANCEL: L.Browser.msPointer ? 'MSPointerCancel' : 'pointercancel', | |
_pointers: {}, | |
// Provides a touch events wrapper for (ms)pointer events. | |
// ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 | |
addPointerListener: function (obj, type, handler, id) { | |
if (type === 'touchstart') { | |
this._addPointerStart(obj, handler, id); | |
} else if (type === 'touchmove') { | |
this._addPointerMove(obj, handler, id); | |
} else if (type === 'touchend') { | |
this._addPointerEnd(obj, handler, id); | |
} | |
return this; | |
}, | |
removePointerListener: function (obj, type, id) { | |
var handler = obj['_leaflet_' + type + id]; | |
if (type === 'touchstart') { | |
obj.removeEventListener(this.POINTER_DOWN, handler, false); | |
} else if (type === 'touchmove') { | |
obj.removeEventListener(this.POINTER_MOVE, handler, false); | |
} else if (type === 'touchend') { | |
obj.removeEventListener(this.POINTER_UP, handler, false); | |
obj.removeEventListener(this.POINTER_CANCEL, handler, false); | |
} | |
return this; | |
}, | |
_addPointerStart: function (obj, handler, id) { | |
var onDown = L.bind(function (e) { | |
L.DomEvent.preventDefault(e); | |
this._pointers[e.pointerId] = e; | |
this._handlePointer(e, handler); | |
}, this); | |
obj['_leaflet_touchstart' + id] = onDown; | |
obj.addEventListener(this.POINTER_DOWN, onDown, false); | |
// need to also listen for end events to keep the _pointers object accurate | |
if (!this._pointerDocListener) { | |
var removePointer = L.bind(function (e) { | |
delete this._pointers[e.pointerId]; | |
}, this); | |
// we listen documentElement as any drags that end by moving the touch off the screen get fired there | |
document.documentElement.addEventListener(this.POINTER_UP, removePointer, false); | |
document.documentElement.addEventListener(this.POINTER_CANCEL, removePointer, false); | |
this._pointerDocListener = true; | |
} | |
}, | |
_handlePointer: function (e, handler) { | |
e.touches = []; | |
for (var i in this._pointers) { | |
e.touches.push(this._pointers[i]); | |
} | |
e.changedTouches = [e]; | |
handler(e); | |
}, | |
_addPointerMove: function (obj, handler, id) { | |
var onMove = L.bind(function (e) { | |
// don't fire touch moves when mouse isn't down | |
if ((e.pointerType === e.MSPOINTER_TYPE_MOUSE || e.pointerType === 'mouse') && e.buttons === 0) { return; } | |
this._pointers[e.pointerId] = e; | |
this._handlePointer(e, handler); | |
}, this); | |
obj['_leaflet_touchmove' + id] = onMove; | |
obj.addEventListener(this.POINTER_MOVE, onMove, false); | |
}, | |
_addPointerEnd: function (obj, handler, id) { | |
var onUp = L.bind(function (e) { | |
delete this._pointers[e.pointerId]; | |
this._handlePointer(e, handler); | |
}, this); | |
obj['_leaflet_touchend' + id] = onUp; | |
obj.addEventListener(this.POINTER_UP, onUp, false); | |
obj.addEventListener(this.POINTER_CANCEL, onUp, false); | |
} | |
}); | |
/* | |
* L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers. | |
*/ | |
L.Map.mergeOptions({ | |
touchZoom: L.Browser.touch && !L.Browser.android23, | |
bounceAtZoomLimits: true | |
}); | |
L.Map.TouchZoom = L.Handler.extend({ | |
addHooks: function () { | |
L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this); | |
}, | |
removeHooks: function () { | |
L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this); | |
}, | |
_onTouchStart: function (e) { | |
var map = this._map; | |
if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; } | |
var p1 = map.mouseEventToLayerPoint(e.touches[0]), | |
p2 = map.mouseEventToLayerPoint(e.touches[1]), | |
viewCenter = map._getCenterLayerPoint(); | |
this._startCenter = p1.add(p2)._divideBy(2); | |
this._startDist = p1.distanceTo(p2); | |
this._moved = false; | |
this._zooming = true; | |
this._centerOffset = viewCenter.subtract(this._startCenter); | |
if (map._panAnim) { | |
map._panAnim.stop(); | |
} | |
L.DomEvent | |
.on(document, 'touchmove', this._onTouchMove, this) | |
.on(document, 'touchend', this._onTouchEnd, this); | |
L.DomEvent.preventDefault(e); | |
}, | |
_onTouchMove: function (e) { | |
if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; } | |
var map = this._map, | |
p1 = map.mouseEventToLayerPoint(e.touches[0]), | |
p2 = map.mouseEventToLayerPoint(e.touches[1]); | |
this._scale = p1.distanceTo(p2) / this._startDist; | |
this._delta = p1._add(p2)._divideBy(2)._subtract(this._startCenter); | |
if (!map.options.bounceAtZoomLimits && | |
((map.getZoom() === map.getMinZoom() && this._scale < 1) || | |
(map.getZoom() === map.getMaxZoom() && this._scale > 1))) { return; } | |
if (!this._moved) { | |
map | |
.fire('movestart') | |
.fire('zoomstart'); | |
this._moved = true; | |
} | |
L.Util.cancelAnimFrame(this._animRequest); | |
this._animRequest = L.Util.requestAnimFrame(this._updateOnMove, this, true, this._map._container); | |
L.DomEvent.preventDefault(e); | |
}, | |
_updateOnMove: function () { | |
var map = this._map; | |
if (map.options.touchZoom === 'center') { | |
this._center = map.getCenter(); | |
} else { | |
this._center = map.layerPointToLatLng(this._getTargetCenter()); | |
} | |
this._zoom = map.getScaleZoom(this._scale); | |
map._animateZoom(this._center, this._zoom); | |
}, | |
_onTouchEnd: function () { | |
if (!this._moved || !this._zooming) { | |
this._zooming = false; | |
return; | |
} | |
this._zooming = false; | |
L.Util.cancelAnimFrame(this._animRequest); | |
L.DomEvent | |
.off(document, 'touchmove', this._onTouchMove) | |
.off(document, 'touchend', this._onTouchEnd); | |
var map = this._map, | |
oldZoom = map.getZoom(), | |
zoomDelta = this._zoom - oldZoom, | |
finalZoom = map._limitZoom(oldZoom + (zoomDelta > 0 ? Math.ceil(zoomDelta) : Math.floor(zoomDelta))); | |
map._animateZoom(this._center, finalZoom, true); | |
}, | |
_getTargetCenter: function () { | |
var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale); | |
return this._startCenter.add(centerOffset); | |
} | |
}); | |
L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom); | |
/* | |
* L.Map.Tap is used to enable mobile hacks like quick taps and long hold. | |
*/ | |
L.Map.mergeOptions({ | |
tap: true, | |
tapTolerance: 15 | |
}); | |
L.Map.Tap = L.Handler.extend({ | |
addHooks: function () { | |
L.DomEvent.on(this._map._container, 'touchstart', this._onDown, this); | |
}, | |
removeHooks: function () { | |
L.DomEvent.off(this._map._container, 'touchstart', this._onDown, this); | |
}, | |
_onDown: function (e) { | |
if (!e.touches) { return; } | |
L.DomEvent.preventDefault(e); | |
this._fireClick = true; | |
// don't simulate click or track longpress if more than 1 touch | |
if (e.touches.length > 1) { | |
this._fireClick = false; | |
clearTimeout(this._holdTimeout); | |
return; | |
} | |
var first = e.touches[0], | |
el = first.target; | |
this._startPos = this._newPos = new L.Point(first.clientX, first.clientY); | |
// if touching a link, highlight it | |
if (el.tagName && el.tagName.toLowerCase() === 'a') { | |
L.DomUtil.addClass(el, 'leaflet-active'); | |
} | |
// simulate long hold but setting a timeout | |
this._holdTimeout = setTimeout(L.bind(function () { | |
if (this._isTapValid()) { | |
this._fireClick = false; | |
this._onUp(); | |
this._simulateEvent('contextmenu', first); | |
} | |
}, this), 1000); | |
this._simulateEvent('mousedown', first); | |
L.DomEvent.on(document, { | |
touchmove: this._onMove, | |
touchend: this._onUp | |
}, this); | |
}, | |
_onUp: function (e) { | |
clearTimeout(this._holdTimeout); | |
L.DomEvent.off(document, { | |
touchmove: this._onMove, | |
touchend: this._onUp | |
}, this); | |
if (this._fireClick && e && e.changedTouches) { | |
var first = e.changedTouches[0], | |
el = first.target; | |
if (el && el.tagName && el.tagName.toLowerCase() === 'a') { | |
L.DomUtil.removeClass(el, 'leaflet-active'); | |
} | |
this._simulateEvent('mouseup', first); | |
// simulate click if the touch didn't move too much | |
if (this._isTapValid()) { | |
this._simulateEvent('click', first); | |
} | |
} | |
}, | |
_isTapValid: function () { | |
return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance; | |
}, | |
_onMove: function (e) { | |
var first = e.touches[0]; | |
this._newPos = new L.Point(first.clientX, first.clientY); | |
}, | |
_simulateEvent: function (type, e) { | |
var simulatedEvent = document.createEvent('MouseEvents'); | |
simulatedEvent._simulated = true; | |
e.target._simulatedClick = true; | |
simulatedEvent.initMouseEvent( | |
type, true, true, window, 1, | |
e.screenX, e.screenY, | |
e.clientX, e.clientY, | |
false, false, false, false, 0, null); | |
e.target.dispatchEvent(simulatedEvent); | |
} | |
}); | |
if (L.Browser.touch && !L.Browser.pointer) { | |
L.Map.addInitHook('addHandler', 'tap', L.Map.Tap); | |
} | |
/* | |
* L.Handler.ShiftDragZoom is used to add shift-drag zoom interaction to the map | |
* (zoom to a selected bounding box), enabled by default. | |
*/ | |
L.Map.mergeOptions({ | |
boxZoom: true | |
}); | |
L.Map.BoxZoom = L.Handler.extend({ | |
initialize: function (map) { | |
this._map = map; | |
this._container = map._container; | |
this._pane = map._panes.overlayPane; | |
}, | |
addHooks: function () { | |
L.DomEvent.on(this._container, 'mousedown', this._onMouseDown, this); | |
}, | |
removeHooks: function () { | |
L.DomEvent.off(this._container, 'mousedown', this._onMouseDown, this); | |
}, | |
moved: function () { | |
return this._moved; | |
}, | |
_onMouseDown: function (e) { | |
this._moved = false; | |
if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; } | |
L.DomUtil.disableTextSelection(); | |
L.DomUtil.disableImageDrag(); | |
this._startPoint = this._map.mouseEventToContainerPoint(e); | |
L.DomEvent.on(document, { | |
mousemove: this._onMouseMove, | |
mouseup: this._onMouseUp, | |
keydown: this._onKeyDown | |
}, this); | |
}, | |
_onMouseMove: function (e) { | |
if (!this._moved) { | |
this._moved = true; | |
this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._container); | |
L.DomUtil.addClass(this._container, 'leaflet-crosshair'); | |
this._map.fire('boxzoomstart'); | |
} | |
this._point = this._map.mouseEventToContainerPoint(e); | |
var bounds = new L.Bounds(this._point, this._startPoint), | |
size = bounds.getSize(); | |
L.DomUtil.setPosition(this._box, bounds.min); | |
this._box.style.width = size.x + 'px'; | |
this._box.style.height = size.y + 'px'; | |
}, | |
_finish: function () { | |
if (this._moved) { | |
L.DomUtil.remove(this._box); | |
L.DomUtil.removeClass(this._container, 'leaflet-crosshair'); | |
} | |
L.DomUtil.enableTextSelection(); | |
L.DomUtil.enableImageDrag(); | |
L.DomEvent.off(document, { | |
mousemove: this._onMouseMove, | |
mouseup: this._onMouseUp, | |
keydown: this._onKeyDown | |
}, this); | |
}, | |
_onMouseUp: function () { | |
this._finish(); | |
if (!this._moved) { return; } | |
var bounds = new L.LatLngBounds( | |
this._map.containerPointToLatLng(this._startPoint), | |
this._map.containerPointToLatLng(this._point)); | |
this._map | |
.fitBounds(bounds) | |
.fire('boxzoomend', {boxZoomBounds: bounds}); | |
}, | |
_onKeyDown: function (e) { | |
if (e.keyCode === 27) { | |
this._finish(); | |
} | |
} | |
}); | |
L.Map.addInitHook('addHandler', 'boxZoom', L.Map.BoxZoom); | |
/* | |
* L.Map.Keyboard is handling keyboard interaction with the map, enabled by default. | |
*/ | |
L.Map.mergeOptions({ | |
keyboard: true, | |
keyboardPanOffset: 80, | |
keyboardZoomOffset: 1 | |
}); | |
L.Map.Keyboard = L.Handler.extend({ | |
keyCodes: { | |
left: [37], | |
right: [39], | |
down: [40], | |
up: [38], | |
zoomIn: [187, 107, 61, 171], | |
zoomOut: [189, 109, 173] | |
}, | |
initialize: function (map) { | |
this._map = map; | |
this._setPanOffset(map.options.keyboardPanOffset); | |
this._setZoomOffset(map.options.keyboardZoomOffset); | |
}, | |
addHooks: function () { | |
var container = this._map._container; | |
// make the container focusable by tabbing | |
if (container.tabIndex === -1) { | |
container.tabIndex = '0'; | |
} | |
L.DomEvent.on(container, { | |
focus: this._onFocus, | |
blur: this._onBlur, | |
mousedown: this._onMouseDown | |
}, this); | |
this._map.on({ | |
focus: this._addHooks, | |
blur: this._removeHooks | |
}, this); | |
}, | |
removeHooks: function () { | |
this._removeHooks(); | |
L.DomEvent.off(this._map._container, { | |
focus: this._onFocus, | |
blur: this._onBlur, | |
mousedown: this._onMouseDown | |
}, this); | |
this._map.off({ | |
focus: this._addHooks, | |
blur: this._removeHooks | |
}, this); | |
}, | |
_onMouseDown: function () { | |
if (this._focused) { return; } | |
var body = document.body, | |
docEl = document.documentElement, | |
top = body.scrollTop || docEl.scrollTop, | |
left = body.scrollLeft || docEl.scrollLeft; | |
this._map._container.focus(); | |
window.scrollTo(left, top); | |
}, | |
_onFocus: function () { | |
this._focused = true; | |
this._map.fire('focus'); | |
}, | |
_onBlur: function () { | |
this._focused = false; | |
this._map.fire('blur'); | |
}, | |
_setPanOffset: function (pan) { | |
var keys = this._panKeys = {}, | |
codes = this.keyCodes, | |
i, len; | |
for (i = 0, len = codes.left.length; i < len; i++) { | |
keys[codes.left[i]] = [-1 * pan, 0]; | |
} | |
for (i = 0, len = codes.right.length; i < len; i++) { | |
keys[codes.right[i]] = [pan, 0]; | |
} | |
for (i = 0, len = codes.down.length; i < len; i++) { | |
keys[codes.down[i]] = [0, pan]; | |
} | |
for (i = 0, len = codes.up.length; i < len; i++) { | |
keys[codes.up[i]] = [0, -1 * pan]; | |
} | |
}, | |
_setZoomOffset: function (zoom) { | |
var keys = this._zoomKeys = {}, | |
codes = this.keyCodes, | |
i, len; | |
for (i = 0, len = codes.zoomIn.length; i < len; i++) { | |
keys[codes.zoomIn[i]] = zoom; | |
} | |
for (i = 0, len = codes.zoomOut.length; i < len; i++) { | |
keys[codes.zoomOut[i]] = -zoom; | |
} | |
}, | |
_addHooks: function () { | |
L.DomEvent.on(document, 'keydown', this._onKeyDown, this); | |
}, | |
_removeHooks: function () { | |
L.DomEvent.off(document, 'keydown', this._onKeyDown, this); | |
}, | |
_onKeyDown: function (e) { | |
if (e.altKey || e.ctrlKey || e.metaKey) { return; } | |
var key = e.keyCode, | |
map = this._map; | |
if (key in this._panKeys) { | |
if (map._panAnim && map._panAnim._inProgress) { return; } | |
map.panBy(this._panKeys[key]); | |
if (map.options.maxBounds) { | |
map.panInsideBounds(map.options.maxBounds); | |
} | |
} else if (key in this._zoomKeys) { | |
map.setZoom(map.getZoom() + this._zoomKeys[key]); | |
} else { | |
return; | |
} | |
L.DomEvent.stop(e); | |
} | |
}); | |
L.Map.addInitHook('addHandler', 'keyboard', L.Map.Keyboard); | |
/* | |
* L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable. | |
*/ | |
L.Handler.MarkerDrag = L.Handler.extend({ | |
initialize: function (marker) { | |
this._marker = marker; | |
}, | |
addHooks: function () { | |
var icon = this._marker._icon; | |
if (!this._draggable) { | |
this._draggable = new L.Draggable(icon, icon); | |
} | |
this._draggable.on({ | |
dragstart: this._onDragStart, | |
drag: this._onDrag, | |
dragend: this._onDragEnd | |
}, this).enable(); | |
L.DomUtil.addClass(icon, 'leaflet-marker-draggable'); | |
}, | |
removeHooks: function () { | |
this._draggable.off({ | |
dragstart: this._onDragStart, | |
drag: this._onDrag, | |
dragend: this._onDragEnd | |
}, this).disable(); | |
L.DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable'); | |
}, | |
moved: function () { | |
return this._draggable && this._draggable._moved; | |
}, | |
_onDragStart: function () { | |
this._marker | |
.closePopup() | |
.fire('movestart') | |
.fire('dragstart'); | |
}, | |
_onDrag: function () { | |
var marker = this._marker, | |
shadow = marker._shadow, | |
iconPos = L.DomUtil.getPosition(marker._icon), | |
latlng = marker._map.layerPointToLatLng(iconPos); | |
// update shadow position | |
if (shadow) { | |
L.DomUtil.setPosition(shadow, iconPos); | |
} | |
marker._latlng = latlng; | |
marker | |
.fire('move', {latlng: latlng}) | |
.fire('drag'); | |
}, | |
_onDragEnd: function (e) { | |
this._marker | |
.fire('moveend') | |
.fire('dragend', e); | |
} | |
}); | |
/* | |
* L.Control is a base class for implementing map controls. Handles positioning. | |
* All other controls extend from this class. | |
*/ | |
L.Control = L.Class.extend({ | |
options: { | |
position: 'topright' | |
}, | |
initialize: function (options) { | |
L.setOptions(this, options); | |
}, | |
getPosition: function () { | |
return this.options.position; | |
}, | |
setPosition: function (position) { | |
var map = this._map; | |
if (map) { | |
map.removeControl(this); | |
} | |
this.options.position = position; | |
if (map) { | |
map.addControl(this); | |
} | |
return this; | |
}, | |
getContainer: function () { | |
return this._container; | |
}, | |
addTo: function (map) { | |
this._map = map; | |
var container = this._container = this.onAdd(map), | |
pos = this.getPosition(), | |
corner = map._controlCorners[pos]; | |
L.DomUtil.addClass(container, 'leaflet-control'); | |
if (pos.indexOf('bottom') !== -1) { | |
corner.insertBefore(container, corner.firstChild); | |
} else { | |
corner.appendChild(container); | |
} | |
return this; | |
}, | |
remove: function () { | |
L.DomUtil.remove(this._container); | |
if (this.onRemove) { | |
this.onRemove(this._map); | |
} | |
this._map = null; | |
return this; | |
}, | |
_refocusOnMap: function () { | |
if (this._map) { | |
this._map.getContainer().focus(); | |
} | |
} | |
}); | |
L.control = function (options) { | |
return new L.Control(options); | |
}; | |
// adds control-related methods to L.Map | |
L.Map.include({ | |
addControl: function (control) { | |
control.addTo(this); | |
return this; | |
}, | |
removeControl: function (control) { | |
control.remove(); | |
return this; | |
}, | |
_initControlPos: function () { | |
var corners = this._controlCorners = {}, | |
l = 'leaflet-', | |
container = this._controlContainer = | |
L.DomUtil.create('div', l + 'control-container', this._container); | |
function createCorner(vSide, hSide) { | |
var className = l + vSide + ' ' + l + hSide; | |
corners[vSide + hSide] = L.DomUtil.create('div', className, container); | |
} | |
createCorner('top', 'left'); | |
createCorner('top', 'right'); | |
createCorner('bottom', 'left'); | |
createCorner('bottom', 'right'); | |
}, | |
_clearControlPos: function () { | |
L.DomUtil.remove(this._controlContainer); | |
} | |
}); | |
/* | |
* L.Control.Zoom is used for the default zoom buttons on the map. | |
*/ | |
L.Control.Zoom = L.Control.extend({ | |
options: { | |
position: 'topleft', | |
zoomInText: '+', | |
zoomInTitle: 'Zoom in', | |
zoomOutText: '-', | |
zoomOutTitle: 'Zoom out' | |
}, | |
onAdd: function (map) { | |
var zoomName = 'leaflet-control-zoom', | |
container = L.DomUtil.create('div', zoomName + ' leaflet-bar'), | |
options = this.options; | |
this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle, | |
zoomName + '-in', container, this._zoomIn); | |
this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle, | |
zoomName + '-out', container, this._zoomOut); | |
this._updateDisabled(); | |
map.on('zoomend zoomlevelschange', this._updateDisabled, this); | |
return container; | |
}, | |
onRemove: function (map) { | |
map.off('zoomend zoomlevelschange', this._updateDisabled, this); | |
}, | |
_zoomIn: function (e) { | |
this._map.zoomIn(e.shiftKey ? 3 : 1); | |
}, | |
_zoomOut: function (e) { | |
this._map.zoomOut(e.shiftKey ? 3 : 1); | |
}, | |
_createButton: function (html, title, className, container, fn) { | |
var link = L.DomUtil.create('a', className, container); | |
link.innerHTML = html; | |
link.href = '#'; | |
link.title = title; | |
L.DomEvent | |
.on(link, 'mousedown dblclick', L.DomEvent.stopPropagation) | |
.on(link, 'click', L.DomEvent.stop) | |
.on(link, 'click', fn, this) | |
.on(link, 'click', this._refocusOnMap, this); | |
return link; | |
}, | |
_updateDisabled: function () { | |
var map = this._map, | |
className = 'leaflet-disabled'; | |
L.DomUtil.removeClass(this._zoomInButton, className); | |
L.DomUtil.removeClass(this._zoomOutButton, className); | |
if (map._zoom === map.getMinZoom()) { | |
L.DomUtil.addClass(this._zoomOutButton, className); | |
} | |
if (map._zoom === map.getMaxZoom()) { | |
L.DomUtil.addClass(this._zoomInButton, className); | |
} | |
} | |
}); | |
L.Map.mergeOptions({ | |
zoomControl: true | |
}); | |
L.Map.addInitHook(function () { | |
if (this.options.zoomControl) { | |
this.zoomControl = new L.Control.Zoom(); | |
this.addControl(this.zoomControl); | |
} | |
}); | |
L.control.zoom = function (options) { | |
return new L.Control.Zoom(options); | |
}; | |
/* | |
* L.Control.Attribution is used for displaying attribution on the map (added by default). | |
*/ | |
L.Control.Attribution = L.Control.extend({ | |
options: { | |
position: 'bottomright', | |
prefix: '<a href="http://leafletjs.com" title="A JS library for interactive maps">Leaflet</a>' | |
}, | |
initialize: function (options) { | |
L.setOptions(this, options); | |
this._attributions = {}; | |
}, | |
onAdd: function (map) { | |
this._container = L.DomUtil.create('div', 'leaflet-control-attribution'); | |
L.DomEvent.disableClickPropagation(this._container); | |
// TODO ugly, refactor | |
for (var i in map._layers) { | |
if (map._layers[i].getAttribution) { | |
this.addAttribution(map._layers[i].getAttribution()); | |
} | |
} | |
this._update(); | |
return this._container; | |
}, | |
setPrefix: function (prefix) { | |
this.options.prefix = prefix; | |
this._update(); | |
return this; | |
}, | |
addAttribution: function (text) { | |
if (!text) { return; } | |
if (!this._attributions[text]) { | |
this._attributions[text] = 0; | |
} | |
this._attributions[text]++; | |
this._update(); | |
return this; | |
}, | |
removeAttribution: function (text) { | |
if (!text) { return; } | |
if (this._attributions[text]) { | |
this._attributions[text]--; | |
this._update(); | |
} | |
return this; | |
}, | |
_update: function () { | |
if (!this._map) { return; } | |
var attribs = []; | |
for (var i in this._attributions) { | |
if (this._attributions[i]) { | |
attribs.push(i); | |
} | |
} | |
var prefixAndAttribs = []; | |
if (this.options.prefix) { | |
prefixAndAttribs.push(this.options.prefix); | |
} | |
if (attribs.length) { | |
prefixAndAttribs.push(attribs.join(', ')); | |
} | |
this._container.innerHTML = prefixAndAttribs.join(' | '); | |
} | |
}); | |
L.Map.mergeOptions({ | |
attributionControl: true | |
}); | |
L.Map.addInitHook(function () { | |
if (this.options.attributionControl) { | |
this.attributionControl = (new L.Control.Attribution()).addTo(this); | |
} | |
}); | |
L.control.attribution = function (options) { | |
return new L.Control.Attribution(options); | |
}; | |
/* | |
* L.Control.Scale is used for displaying metric/imperial scale on the map. | |
*/ | |
L.Control.Scale = L.Control.extend({ | |
options: { | |
position: 'bottomleft', | |
maxWidth: 100, | |
metric: true, | |
imperial: true | |
// updateWhenIdle: false | |
}, | |
onAdd: function (map) { | |
var className = 'leaflet-control-scale', | |
container = L.DomUtil.create('div', className), | |
options = this.options; | |
this._addScales(options, className + '-line', container); | |
map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this); | |
map.whenReady(this._update, this); | |
return container; | |
}, | |
onRemove: function (map) { | |
map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this); | |
}, | |
_addScales: function (options, className, container) { | |
if (options.metric) { | |
this._mScale = L.DomUtil.create('div', className, container); | |
} | |
if (options.imperial) { | |
this._iScale = L.DomUtil.create('div', className, container); | |
} | |
}, | |
_update: function () { | |
var map = this._map, | |
y = map.getSize().y / 2; | |
var maxMeters = L.CRS.Earth.distance( | |
map.containerPointToLatLng([0, y]), | |
map.containerPointToLatLng([this.options.maxWidth, y])); | |
this._updateScales(maxMeters); | |
}, | |
_updateScales: function (maxMeters) { | |
if (this.options.metric && maxMeters) { | |
this._updateMetric(maxMeters); | |
} | |
if (this.options.imperial && maxMeters) { | |
this._updateImperial(maxMeters); | |
} | |
}, | |
_updateMetric: function (maxMeters) { | |
var meters = this._getRoundNum(maxMeters), | |
label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km'; | |
this._updateScale(this._mScale, label, meters / maxMeters); | |
}, | |
_updateImperial: function (maxMeters) { | |
var maxFeet = maxMeters * 3.2808399, | |
maxMiles, miles, feet; | |
if (maxFeet > 5280) { | |
maxMiles = maxFeet / 5280; | |
miles = this._getRoundNum(maxMiles); | |
this._updateScale(this._iScale, miles + ' mi', miles / maxMiles); | |
} else { | |
feet = this._getRoundNum(maxFeet); | |
this._updateScale(this._iScale, feet + ' ft', feet / maxFeet); | |
} | |
}, | |
_updateScale: function (scale, text, ratio) { | |
scale.style.width = (Math.round(this.options.maxWidth * ratio) - 10) + 'px'; | |
scale.innerHTML = text; | |
}, | |
_getRoundNum: function (num) { | |
var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1), | |
d = num / pow10; | |
d = d >= 10 ? 10 : | |
d >= 5 ? 5 : | |
d >= 3 ? 3 : | |
d >= 2 ? 2 : 1; | |
return pow10 * d; | |
} | |
}); | |
L.control.scale = function (options) { | |
return new L.Control.Scale(options); | |
}; | |
/* | |
* L.Control.Layers is a control to allow users to switch between different layers on the map. | |
*/ | |
L.Control.Layers = L.Control.extend({ | |
options: { | |
collapsed: true, | |
position: 'topright', | |
autoZIndex: true | |
}, | |
initialize: function (baseLayers, overlays, options) { | |
L.setOptions(this, options); | |
this._layers = {}; | |
this._lastZIndex = 0; | |
this._handlingClick = false; | |
for (var i in baseLayers) { | |
this._addLayer(baseLayers[i], i); | |
} | |
for (i in overlays) { | |
this._addLayer(overlays[i], i, true); | |
} | |
}, | |
onAdd: function () { | |
this._initLayout(); | |
this._update(); | |
return this._container; | |
}, | |
addBaseLayer: function (layer, name) { | |
this._addLayer(layer, name); | |
return this._update(); | |
}, | |
addOverlay: function (layer, name) { | |
this._addLayer(layer, name, true); | |
return this._update(); | |
}, | |
removeLayer: function (layer) { | |
layer.off('add remove', this._onLayerChange, this); | |
delete this._layers[L.stamp(layer)]; | |
return this._update(); | |
}, | |
_initLayout: function () { | |
var className = 'leaflet-control-layers', | |
container = this._container = L.DomUtil.create('div', className); | |
// makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released | |
container.setAttribute('aria-haspopup', true); | |
if (!L.Browser.touch) { | |
L.DomEvent | |
.disableClickPropagation(container) | |
.disableScrollPropagation(container); | |
} else { | |
L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation); | |
} | |
var form = this._form = L.DomUtil.create('form', className + '-list'); | |
if (this.options.collapsed) { | |
if (!L.Browser.android) { | |
L.DomEvent.on(container, { | |
mouseenter: this._expand, | |
mouseleave: this._collapse | |
}, this); | |
} | |
var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container); | |
link.href = '#'; | |
link.title = 'Layers'; | |
if (L.Browser.touch) { | |
L.DomEvent | |
.on(link, 'click', L.DomEvent.stop) | |
.on(link, 'click', this._expand, this); | |
} else { | |
L.DomEvent.on(link, 'focus', this._expand, this); | |
} | |
// work around for Firefox Android issue https://github.com/Leaflet/Leaflet/issues/2033 | |
L.DomEvent.on(form, 'click', function () { | |
setTimeout(L.bind(this._onInputClick, this), 0); | |
}, this); | |
this._map.on('click', this._collapse, this); | |
// TODO keyboard accessibility | |
} else { | |
this._expand(); | |
} | |
this._baseLayersList = L.DomUtil.create('div', className + '-base', form); | |
this._separator = L.DomUtil.create('div', className + '-separator', form); | |
this._overlaysList = L.DomUtil.create('div', className + '-overlays', form); | |
container.appendChild(form); | |
}, | |
_addLayer: function (layer, name, overlay) { | |
layer.on('add remove', this._onLayerChange, this); | |
var id = L.stamp(layer); | |
this._layers[id] = { | |
layer: layer, | |
name: name, | |
overlay: overlay | |
}; | |
if (this.options.autoZIndex && layer.setZIndex) { | |
this._lastZIndex++; | |
layer.setZIndex(this._lastZIndex); | |
} | |
}, | |
_update: function () { | |
if (!this._container) { return; } | |
this._baseLayersList.innerHTML = ''; | |
this._overlaysList.innerHTML = ''; | |
var baseLayersPresent, overlaysPresent, i, obj; | |
for (i in this._layers) { | |
obj = this._layers[i]; | |
this._addItem(obj); | |
overlaysPresent = overlaysPresent || obj.overlay; | |
baseLayersPresent = baseLayersPresent || !obj.overlay; | |
} | |
this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none'; | |
return this; | |
}, | |
_onLayerChange: function (e) { | |
if (!this._handlingClick) { | |
this._update(); | |
} | |
var overlay = this._layers[L.stamp(e.target)].overlay; | |
var type = overlay ? | |
(e.type === 'add' ? 'overlayadd' : 'overlayremove') : | |
(e.type === 'add' ? 'baselayerchange' : null); | |
if (type) { | |
this._map.fire(type, e.target); | |
} | |
}, | |
// IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe) | |
_createRadioElement: function (name, checked) { | |
var radioHtml = '<input type="radio" class="leaflet-control-layers-selector" name="' + | |
name + '"' + (checked ? ' checked="checked"' : '') + '/>'; | |
var radioFragment = document.createElement('div'); | |
radioFragment.innerHTML = radioHtml; | |
return radioFragment.firstChild; | |
}, | |
_addItem: function (obj) { | |
var label = document.createElement('label'), | |
checked = this._map.hasLayer(obj.layer), | |
input; | |
if (obj.overlay) { | |
input = document.createElement('input'); | |
input.type = 'checkbox'; | |
input.className = 'leaflet-control-layers-selector'; | |
input.defaultChecked = checked; | |
} else { | |
input = this._createRadioElement('leaflet-base-layers', checked); | |
} | |
input.layerId = L.stamp(obj.layer); | |
L.DomEvent.on(input, 'click', this._onInputClick, this); | |
var name = document.createElement('span'); | |
name.innerHTML = ' ' + obj.name; | |
label.appendChild(input); | |
label.appendChild(name); | |
var container = obj.overlay ? this._overlaysList : this._baseLayersList; | |
container.appendChild(label); | |
return label; | |
}, | |
_onInputClick: function () { | |
var inputs = this._form.getElementsByTagName('input'), | |
input, layer, hasLayer; | |
this._handlingClick = true; | |
for (var i = 0, len = inputs.length; i < len; i++) { | |
input = inputs[i]; | |
layer = this._layers[input.layerId].layer; | |
hasLayer = this._map.hasLayer(layer); | |
if (input.checked && !hasLayer) { | |
this._map.addLayer(layer); | |
} else if (!input.checked && hasLayer) { | |
this._map.removeLayer(layer); | |
} | |
} | |
this._handlingClick = false; | |
this._refocusOnMap(); | |
}, | |
_expand: function () { | |
L.DomUtil.addClass(this._container, 'leaflet-control-layers-expanded'); | |
}, | |
_collapse: function () { | |
L.DomUtil.removeClass(this._container, 'leaflet-control-layers-expanded'); | |
} | |
}); | |
L.control.layers = function (baseLayers, overlays, options) { | |
return new L.Control.Layers(baseLayers, overlays, options); | |
}; | |
/* | |
* L.PosAnimation is used by Leaflet internally for pan animations. | |
*/ | |
L.PosAnimation = L.Evented.extend({ | |
run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number]) | |
this.stop(); | |
this._el = el; | |
this._inProgress = true; | |
this._newPos = newPos; | |
this.fire('start'); | |
el.style[L.DomUtil.TRANSITION] = 'all ' + (duration || 0.25) + | |
's cubic-bezier(0,0,' + (easeLinearity || 0.5) + ',1)'; | |
L.DomEvent.on(el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this); | |
L.DomUtil.setPosition(el, newPos); | |
// toggle reflow, Chrome flickers for some reason if you don't do this | |
L.Util.falseFn(el.offsetWidth); | |
// there's no native way to track value updates of transitioned properties, so we imitate this | |
this._stepTimer = setInterval(L.bind(this._onStep, this), 50); | |
}, | |
stop: function () { | |
if (!this._inProgress) { return; } | |
// if we just removed the transition property, the element would jump to its final position, | |
// so we need to make it stay at the current position | |
// Only setPosition if _getPos actually returns a valid position. | |
this._newPos = this._getPos(); | |
if (this._newPos) { | |
L.DomUtil.setPosition(this._el, this._newPos); | |
} | |
this._onTransitionEnd(); | |
L.Util.falseFn(this._el.offsetWidth); // force reflow in case we are about to start a new animation | |
}, | |
_onStep: function () { | |
var stepPos = this._getPos(); | |
if (!stepPos) { | |
this._onTransitionEnd(); | |
return; | |
} | |
// jshint camelcase: false | |
// make L.DomUtil.getPosition return intermediate position value during animation | |
this._el._leaflet_pos = stepPos; | |
this.fire('step'); | |
}, | |
// you can't easily get intermediate values of properties animated with CSS3 Transitions, | |
// we need to parse computed style (in case of transform it returns matrix string) | |
_transformRe: /([-+]?(?:\d*\.)?\d+)\D*, ([-+]?(?:\d*\.)?\d+)\D*\)/, | |
_getPos: function () { | |
var left, top, matches, | |
el = this._el, | |
style = window.getComputedStyle(el); | |
if (L.Browser.any3d) { | |
matches = style[L.DomUtil.TRANSFORM].match(this._transformRe); | |
if (!matches) { return; } | |
left = parseFloat(matches[1]); | |
top = parseFloat(matches[2]); | |
} else { | |
left = parseFloat(style.left); | |
top = parseFloat(style.top); | |
} | |
return new L.Point(left, top, true); | |
}, | |
_onTransitionEnd: function () { | |
L.DomEvent.off(this._el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this); | |
if (!this._inProgress) { return; } | |
this._inProgress = false; | |
this._el.style[L.DomUtil.TRANSITION] = ''; | |
// jshint camelcase: false | |
// make sure L.DomUtil.getPosition returns the final position value after animation | |
this._el._leaflet_pos = this._newPos; | |
clearInterval(this._stepTimer); | |
this.fire('step').fire('end'); | |
} | |
}); | |
/* | |
* Extends L.Map to handle panning animations. | |
*/ | |
L.Map.include({ | |
setView: function (center, zoom, options) { | |
zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom); | |
center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds); | |
options = options || {}; | |
if (this._panAnim) { | |
this._panAnim.stop(); | |
} | |
if (this._loaded && !options.reset && options !== true) { | |
if (options.animate !== undefined) { | |
options.zoom = L.extend({animate: options.animate}, options.zoom); | |
options.pan = L.extend({animate: options.animate}, options.pan); | |
} | |
// try animating pan or zoom | |
var animated = (this._zoom !== zoom) ? | |
this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) : | |
this._tryAnimatedPan(center, options.pan); | |
if (animated) { | |
// prevent resize handler call, the view will refresh after animation anyway | |
clearTimeout(this._sizeTimer); | |
return this; | |
} | |
} | |
// animation didn't start, just reset the map view | |
this._resetView(center, zoom); | |
return this; | |
}, | |
panBy: function (offset, options) { | |
offset = L.point(offset).round(); | |
options = options || {}; | |
if (!offset.x && !offset.y) { | |
return this; | |
} | |
//If we pan too far then chrome gets issues with tiles | |
// and makes them disappear or appear in the wrong place (slightly offset) #2602 | |
if (options.animate !== true && !this.getSize().contains(offset)) { | |
return this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom()); | |
} | |
if (!this._panAnim) { | |
this._panAnim = new L.PosAnimation(); | |
this._panAnim.on({ | |
'step': this._onPanTransitionStep, | |
'end': this._onPanTransitionEnd | |
}, this); | |
} | |
// don't fire movestart if animating inertia | |
if (!options.noMoveStart) { | |
this.fire('movestart'); | |
} | |
// animate pan unless animate: false specified | |
if (options.animate !== false) { | |
L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim'); | |
var newPos = this._getMapPanePos().subtract(offset); | |
this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity); | |
} else { | |
this._rawPanBy(offset); | |
this.fire('move').fire('moveend'); | |
} | |
return this; | |
}, | |
_onPanTransitionStep: function () { | |
this.fire('move'); | |
}, | |
_onPanTransitionEnd: function () { | |
L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim'); | |
this.fire('moveend'); | |
}, | |
_tryAnimatedPan: function (center, options) { | |
// difference between the new and current centers in pixels | |
var offset = this._getCenterOffset(center)._floor(); | |
// don't animate too far unless animate: true specified in options | |
if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; } | |
this.panBy(offset, options); | |
return true; | |
} | |
}); | |
/* | |
* L.PosAnimation fallback implementation that powers Leaflet pan animations | |
* in browsers that don't support CSS3 Transitions. | |
*/ | |
L.PosAnimation = L.DomUtil.TRANSITION ? L.PosAnimation : L.PosAnimation.extend({ | |
run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number]) | |
this.stop(); | |
this._el = el; | |
this._inProgress = true; | |
this._duration = duration || 0.25; | |
this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2); | |
this._startPos = L.DomUtil.getPosition(el); | |
this._offset = newPos.subtract(this._startPos); | |
this._startTime = +new Date(); | |
this.fire('start'); | |
this._animate(); | |
}, | |
stop: function () { | |
if (!this._inProgress) { return; } | |
this._step(); | |
this._complete(); | |
}, | |
_animate: function () { | |
// animation loop | |
this._animId = L.Util.requestAnimFrame(this._animate, this); | |
this._step(); | |
}, | |
_step: function () { | |
var elapsed = (+new Date()) - this._startTime, | |
duration = this._duration * 1000; | |
if (elapsed < duration) { | |
this._runFrame(this._easeOut(elapsed / duration)); | |
} else { | |
this._runFrame(1); | |
this._complete(); | |
} | |
}, | |
_runFrame: function (progress) { | |
var pos = this._startPos.add(this._offset.multiplyBy(progress)); | |
L.DomUtil.setPosition(this._el, pos); | |
this.fire('step'); | |
}, | |
_complete: function () { | |
L.Util.cancelAnimFrame(this._animId); | |
this._inProgress = false; | |
this.fire('end'); | |
}, | |
_easeOut: function (t) { | |
return 1 - Math.pow(1 - t, this._easeOutPower); | |
} | |
}); | |
/* | |
* Extends L.Map to handle zoom animations. | |
*/ | |
L.Map.mergeOptions({ | |
zoomAnimation: true, | |
zoomAnimationThreshold: 4 | |
}); | |
var zoomAnimated = L.DomUtil.TRANSITION && L.Browser.any3d && !L.Browser.mobileOpera; | |
if (zoomAnimated) { | |
L.Map.addInitHook(function () { | |
// don't animate on browsers without hardware-accelerated transitions or old Android/Opera | |
this._zoomAnimated = this.options.zoomAnimation; | |
// zoom transitions run with the same duration for all layers, so if one of transitionend events | |
// happens after starting zoom animation (propagating to the map pane), we know that it ended globally | |
if (this._zoomAnimated) { | |
L.DomEvent.on(this._mapPane, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this); | |
} | |
}); | |
} | |
L.Map.include(!zoomAnimated ? {} : { | |
_catchTransitionEnd: function (e) { | |
if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) { | |
this._onZoomTransitionEnd(); | |
} | |
}, | |
_nothingToAnimate: function () { | |
return !this._container.getElementsByClassName('leaflet-zoom-animated').length; | |
}, | |
_tryAnimatedZoom: function (center, zoom, options) { | |
if (this._animatingZoom) { return true; } | |
options = options || {}; | |
// don't animate if disabled, not supported or zoom difference is too large | |
if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() || | |
Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; } | |
// offset is the pixel coords of the zoom origin relative to the current center | |
var scale = this.getZoomScale(zoom), | |
offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale); | |
// don't animate if the zoom origin isn't within one screen from the current center, unless forced | |
if (options.animate !== true && !this.getSize().contains(offset)) { return false; } | |
L.Util.requestAnimFrame(function () { | |
this | |
.fire('movestart') | |
.fire('zoomstart') | |
._animateZoom(center, zoom, true); | |
}, this); | |
return true; | |
}, | |
_animateZoom: function (center, zoom, startAnim) { | |
if (startAnim) { | |
this._animatingZoom = true; | |
// remember what center/zoom to set after animation | |
this._animateToCenter = center; | |
this._animateToZoom = zoom; | |
// disable any dragging during animation | |
if (L.Draggable) { | |
L.Draggable._disabled = true; | |
} | |
L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim'); | |
} | |
var scale = this.getZoomScale(zoom), | |
origin = this._getCenterLayerPoint().add(this._getCenterOffset(center)._divideBy(1 - 1 / scale)); | |
this.fire('zoomanim', { | |
center: center, | |
zoom: zoom, | |
origin: origin, | |
scale: scale | |
}); | |
}, | |
_onZoomTransitionEnd: function () { | |
this._animatingZoom = false; | |
L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim'); | |
this._resetView(this._animateToCenter, this._animateToZoom, true, true); | |
if (L.Draggable) { | |
L.Draggable._disabled = false; | |
} | |
} | |
}); | |
/* | |
* Provides L.Map with convenient shortcuts for using browser geolocation features. | |
*/ | |
L.Map.include({ | |
_defaultLocateOptions: { | |
timeout: 10000, | |
watch: false | |
// setView: false | |
// maxZoom: <Number> | |
// maximumAge: 0 | |
// enableHighAccuracy: false | |
}, | |
locate: function (/*Object*/ options) { | |
options = this._locateOptions = L.extend(this._defaultLocateOptions, options); | |
if (!navigator.geolocation) { | |
this._handleGeolocationError({ | |
code: 0, | |
message: 'Geolocation not supported.' | |
}); | |
return this; | |
} | |
var onResponse = L.bind(this._handleGeolocationResponse, this), | |
onError = L.bind(this._handleGeolocationError, this); | |
if (options.watch) { | |
this._locationWatchId = | |
navigator.geolocation.watchPosition(onResponse, onError, options); | |
} else { | |
navigator.geolocation.getCurrentPosition(onResponse, onError, options); | |
} | |
return this; | |
}, | |
stopLocate: function () { | |
if (navigator.geolocation) { | |
navigator.geolocation.clearWatch(this._locationWatchId); | |
} | |
if (this._locateOptions) { | |
this._locateOptions.setView = false; | |
} | |
return this; | |
}, | |
_handleGeolocationError: function (error) { | |
var c = error.code, | |
message = error.message || | |
(c === 1 ? 'permission denied' : | |
(c === 2 ? 'position unavailable' : 'timeout')); | |
if (this._locateOptions.setView && !this._loaded) { | |
this.fitWorld(); | |
} | |
this.fire('locationerror', { | |
code: c, | |
message: 'Geolocation error: ' + message + '.' | |
}); | |
}, | |
_handleGeolocationResponse: function (pos) { | |
var lat = pos.coords.latitude, | |
lng = pos.coords.longitude, | |
latlng = new L.LatLng(lat, lng), | |
latAccuracy = 180 * pos.coords.accuracy / 40075017, | |
lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * lat), | |
bounds = L.latLngBounds( | |
[lat - latAccuracy, lng - lngAccuracy], | |
[lat + latAccuracy, lng + lngAccuracy]), | |
options = this._locateOptions; | |
if (options.setView) { | |
var zoom = this.getBoundsZoom(bounds); | |
this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom); | |
} | |
var data = { | |
latlng: latlng, | |
bounds: bounds, | |
timestamp: pos.timestamp | |
}; | |
for (var i in pos.coords) { | |
if (typeof pos.coords[i] === 'number') { | |
data[i] = pos.coords[i]; | |
} | |
} | |
this.fire('locationfound', data); | |
} | |
}); | |
}(window, document)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment