-/*\r
- * L.Mixin.Events adds custom events functionality to Leaflet classes\r
- */\r
-\r
-var key = '_leaflet_events';\r
-\r
-L.Mixin = {};\r
-\r
-L.Mixin.Events = {\r
- \r
- addEventListener: function (types, fn, context) { // (String, Function[, Object]) or (Object[, Object])\r
- var events = this[key] = this[key] || {},\r
- type, i, len;\r
- \r
- // Types can be a map of types/handlers\r
- if (typeof types === 'object') {\r
- for (type in types) {\r
- if (types.hasOwnProperty(type)) {\r
- this.addEventListener(type, types[type], fn);\r
- }\r
- }\r
- \r
- return this;\r
- }\r
- \r
- types = L.Util.splitWords(types);\r
- \r
- for (i = 0, len = types.length; i < len; i++) {\r
- events[types[i]] = events[types[i]] || [];\r
- events[types[i]].push({\r
- action: fn,\r
- context: context || this\r
- });\r
- }\r
- \r
- return this;\r
- },\r
-\r
- hasEventListeners: function (type) { // (String) -> Boolean\r
- return (key in this) && (type in this[key]) && (this[key][type].length > 0);\r
- },\r
-\r
- removeEventListener: function (types, fn, context) { // (String[, Function, Object]) or (Object[, Object])\r
- var events = this[key],\r
- type, i, len, listeners, j;\r
- \r
- if (typeof types === 'object') {\r
- for (type in types) {\r
- if (types.hasOwnProperty(type)) {\r
- this.removeEventListener(type, types[type], fn);\r
- }\r
- }\r
- \r
- return this;\r
- }\r
- \r
- types = L.Util.splitWords(types);\r
-\r
- for (i = 0, len = types.length; i < len; i++) {\r
-\r
- if (this.hasEventListeners(types[i])) {\r
- listeners = events[types[i]];\r
- \r
- for (j = listeners.length - 1; j >= 0; j--) {\r
- if (\r
- (!fn || listeners[j].action === fn) &&\r
- (!context || (listeners[j].context === context))\r
- ) {\r
- listeners.splice(j, 1);\r
- }\r
- }\r
- }\r
- }\r
- \r
- return this;\r
- },\r
-\r
- fireEvent: function (type, data) { // (String[, Object])\r
- if (!this.hasEventListeners(type)) {\r
- return this;\r
- }\r
-\r
- var event = L.Util.extend({\r
- type: type,\r
- target: this\r
- }, data);\r
-\r
- var listeners = this[key][type].slice();\r
-\r
- for (var i = 0, len = listeners.length; i < len; i++) {\r
- listeners[i].action.call(listeners[i].context || this, event);\r
- }\r
-\r
- return this;\r
- }\r
-};\r
-\r
-L.Mixin.Events.on = L.Mixin.Events.addEventListener;\r
-L.Mixin.Events.off = L.Mixin.Events.removeEventListener;\r
-L.Mixin.Events.fire = L.Mixin.Events.fireEvent;\r
-
-
-(function () {\r
- var ua = navigator.userAgent.toLowerCase(),\r
- ie = !!window.ActiveXObject,\r
- ie6 = ie && !window.XMLHttpRequest,\r
- webkit = ua.indexOf("webkit") !== -1,\r
- gecko = ua.indexOf("gecko") !== -1,\r
- //Terrible browser detection to work around a safari / iOS / android browser bug. See TileLayer._addTile and debug/hacks/jitter.html\r
- chrome = ua.indexOf("chrome") !== -1,\r
- opera = window.opera,\r
- android = ua.indexOf("android") !== -1,\r
- android23 = ua.search("android [23]") !== -1,\r
- mobile = typeof orientation !== undefined + '' ? true : false,\r
- doc = document.documentElement,\r
- ie3d = ie && ('transition' in doc.style),\r
- webkit3d = webkit && ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()),\r
- gecko3d = gecko && ('MozPerspective' in doc.style),\r
- opera3d = opera && ('OTransition' in doc.style);\r
-\r
- var touch = !window.L_NO_TOUCH && (function () {\r
- var startName = 'ontouchstart';\r
-\r
- // WebKit, etc\r
- if (startName in doc) {\r
- return true;\r
- }\r
-\r
- // Firefox/Gecko\r
- var div = document.createElement('div'),\r
- supported = false;\r
-\r
- if (!div.setAttribute) {\r
- return false;\r
- }\r
- div.setAttribute(startName, 'return;');\r
-\r
- if (typeof div[startName] === 'function') {\r
- supported = true;\r
- }\r
-\r
- div.removeAttribute(startName);\r
- div = null;\r
-\r
- return supported;\r
- }());\r
-\r
- var retina = (('devicePixelRatio' in window && window.devicePixelRatio > 1) || ('matchMedia' in window && window.matchMedia("(min-resolution:144dpi)").matches));\r
-\r
- L.Browser = {\r
- ua: ua,\r
- ie: ie,\r
- ie6: ie6,\r
- webkit: webkit,\r
- gecko: gecko,\r
- opera: opera,\r
- android: android,\r
- android23: android23,\r
-\r
- chrome: chrome,\r
-\r
- ie3d: ie3d,\r
- webkit3d: webkit3d,\r
- gecko3d: gecko3d,\r
- opera3d: opera3d,\r
- any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d),\r
-\r
- mobile: mobile,\r
- mobileWebkit: mobile && webkit,\r
- mobileWebkit3d: mobile && webkit3d,\r
- mobileOpera: mobile && opera,\r
-\r
- touch: touch,\r
-\r
- retina: retina\r
- };\r
-}());\r
-
-
-/*\r
- * L.Point represents a point with x and y coordinates.\r
- */\r
-\r
-L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {\r
- this.x = (round ? Math.round(x) : x);\r
- this.y = (round ? Math.round(y) : y);\r
-};\r
-\r
-L.Point.prototype = {\r
-\r
- clone: function () {\r
- return new L.Point(this.x, this.y);\r
- },\r
-\r
- // non-destructive, returns a new point\r
- add: function (point) {\r
- return this.clone()._add(L.point(point));\r
- },\r
-\r
- // destructive, used directly for performance in situations where it's safe to modify existing point\r
- _add: function (point) {\r
- this.x += point.x;\r
- this.y += point.y;\r
- return this;\r
- },\r
-\r
- subtract: function (point) {\r
- return this.clone()._subtract(L.point(point));\r
- },\r
-\r
- _subtract: function (point) {\r
- this.x -= point.x;\r
- this.y -= point.y;\r
- return this;\r
- },\r
-\r
- divideBy: function (num) {\r
- return this.clone()._divideBy(num);\r
- },\r
-\r
- _divideBy: function (num) {\r
- this.x /= num;\r
- this.y /= num;\r
- return this;\r
- },\r
-\r
- multiplyBy: function (num) {\r
- return this.clone()._multiplyBy(num);\r
- },\r
-\r
- _multiplyBy: function (num) {\r
- this.x *= num;\r
- this.y *= num;\r
- return this;\r
- },\r
-\r
- round: function () {\r
- return this.clone()._round();\r
- },\r
-\r
- _round: function () {\r
- this.x = Math.round(this.x);\r
- this.y = Math.round(this.y);\r
- return this;\r
- },\r
-\r
- floor: function () {\r
- return this.clone()._floor();\r
- },\r
-\r
- _floor: function () {\r
- this.x = Math.floor(this.x);\r
- this.y = Math.floor(this.y);\r
- return this;\r
- },\r
-\r
- distanceTo: function (point) {\r
- point = L.point(point);\r
-\r
- var x = point.x - this.x,\r
- y = point.y - this.y;\r
-\r
- return Math.sqrt(x * x + y * y);\r
- },\r
-\r
- toString: function () {\r
- return 'Point(' +\r
- L.Util.formatNum(this.x) + ', ' +\r
- L.Util.formatNum(this.y) + ')';\r
- }\r
-};\r
-\r
-L.point = function (x, y, round) {\r
- if (x instanceof L.Point) {\r
- return x;\r
- }\r
- if (x instanceof Array) {\r
- return new L.Point(x[0], x[1]);\r
- }\r
- if (isNaN(x)) {\r
- return x;\r
- }\r
- return new L.Point(x, y, round);\r
-};\r
-
-
-/*\r
- * L.Bounds represents a rectangular area on the screen in pixel coordinates.\r
- */\r
-\r
-L.Bounds = L.Class.extend({\r
-\r
- initialize: function (a, b) { //(Point, Point) or Point[]\r
- if (!a) { return; }\r
-\r
- var points = b ? [a, b] : a;\r
-\r
- for (var i = 0, len = points.length; i < len; i++) {\r
- this.extend(points[i]);\r
- }\r
- },\r
-\r
- // extend the bounds to contain the given point\r
- extend: function (point) { // (Point)\r
- point = L.point(point);\r
-\r
- if (!this.min && !this.max) {\r
- this.min = point.clone();\r
- this.max = point.clone();\r
- } else {\r
- this.min.x = Math.min(point.x, this.min.x);\r
- this.max.x = Math.max(point.x, this.max.x);\r
- this.min.y = Math.min(point.y, this.min.y);\r
- this.max.y = Math.max(point.y, this.max.y);\r
- }\r
- return this;\r
- },\r
-\r
- getCenter: function (round) { // (Boolean) -> Point\r
- return new L.Point(\r
- (this.min.x + this.max.x) / 2,\r
- (this.min.y + this.max.y) / 2, round);\r
- },\r
-\r
- getBottomLeft: function () { // -> Point\r
- return new L.Point(this.min.x, this.max.y);\r
- },\r
-\r
- getTopRight: function () { // -> Point\r
- return new L.Point(this.max.x, this.min.y);\r
- },\r
-\r
- contains: function (obj) { // (Bounds) or (Point) -> Boolean\r
- var min, max;\r
-\r
- if (typeof obj[0] === 'number' || obj instanceof L.Point) {\r
- obj = L.point(obj);\r
- } else {\r
- obj = L.bounds(obj);\r
- }\r
-\r
- if (obj instanceof L.Bounds) {\r
- min = obj.min;\r
- max = obj.max;\r
- } else {\r
- min = max = obj;\r
- }\r
-\r
- return (min.x >= this.min.x) &&\r
- (max.x <= this.max.x) &&\r
- (min.y >= this.min.y) &&\r
- (max.y <= this.max.y);\r
- },\r
-\r
- intersects: function (bounds) { // (Bounds) -> Boolean\r
- bounds = L.bounds(bounds);\r
-\r
- var min = this.min,\r
- max = this.max,\r
- min2 = bounds.min,\r
- max2 = bounds.max;\r
-\r
- var xIntersects = (max2.x >= min.x) && (min2.x <= max.x),\r
- yIntersects = (max2.y >= min.y) && (min2.y <= max.y);\r
-\r
- return xIntersects && yIntersects;\r
- },\r
-\r
- isValid: function () {\r
- return !!(this.min && this.max);\r
- }\r
-});\r
-\r
-L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[])\r
- if (!a || a instanceof L.Bounds) {\r
- return a;\r
- }\r
- return new L.Bounds(a, b);\r
-};\r
-
-
-/*\r
- * L.Transformation is an utility class to perform simple point transformations through a 2d-matrix.\r
- */\r
-\r
-L.Transformation = L.Class.extend({\r
- initialize: function (/*Number*/ a, /*Number*/ b, /*Number*/ c, /*Number*/ d) {\r
- this._a = a;\r
- this._b = b;\r
- this._c = c;\r
- this._d = d;\r
- },\r
-\r
- transform: function (point, scale) {\r
- return this._transform(point.clone(), scale);\r
- },\r
-\r
- // destructive transform (faster)\r
- _transform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ {\r
- scale = scale || 1;\r
- point.x = scale * (this._a * point.x + this._b);\r
- point.y = scale * (this._c * point.y + this._d);\r
- return point;\r
- },\r
-\r
- untransform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ {\r
- scale = scale || 1;\r
- return new L.Point(\r
- (point.x / scale - this._b) / this._a,\r
- (point.y / scale - this._d) / this._c);\r
- }\r
-});\r
-
-
-/*\r
- * L.DomUtil contains various utility functions for working with DOM.\r
- */\r
-\r
-L.DomUtil = {\r
- get: function (id) {\r
- return (typeof id === 'string' ? document.getElementById(id) : id);\r
- },\r
-\r
- getStyle: function (el, style) {\r
-\r
- var value = el.style[style];\r
-\r
- if (!value && el.currentStyle) {\r
- value = el.currentStyle[style];\r
- }\r
-\r
- if (!value || value === 'auto') {\r
- var css = document.defaultView.getComputedStyle(el, null);\r
- value = css ? css[style] : null;\r
- }\r
-\r
- return value === 'auto' ? null : value;\r
- },\r
-\r
- getViewportOffset: function (element) {\r
-\r
- var top = 0,\r
- left = 0,\r
- el = element,\r
- docBody = document.body,\r
- pos;\r
-\r
- do {\r
- top += el.offsetTop || 0;\r
- left += el.offsetLeft || 0;\r
- pos = L.DomUtil.getStyle(el, 'position');\r
-\r
- if (el.offsetParent === docBody && pos === 'absolute') { break; }\r
-\r
- if (pos === 'fixed') {\r
- top += docBody.scrollTop || 0;\r
- left += docBody.scrollLeft || 0;\r
- break;\r
- }\r
- el = el.offsetParent;\r
-\r
- } while (el);\r
-\r
- el = element;\r
-\r
- do {\r
- if (el === docBody) { break; }\r
-\r
- top -= el.scrollTop || 0;\r
- left -= el.scrollLeft || 0;\r
-\r
- el = el.parentNode;\r
- } while (el);\r
-\r
- return new L.Point(left, top);\r
- },\r
-\r
- create: function (tagName, className, container) {\r
-\r
- var el = document.createElement(tagName);\r
- el.className = className;\r
-\r
- if (container) {\r
- container.appendChild(el);\r
- }\r
-\r
- return el;\r
- },\r
-\r
- disableTextSelection: function () {\r
- if (document.selection && document.selection.empty) {\r
- document.selection.empty();\r
- }\r
- if (!this._onselectstart) {\r
- this._onselectstart = document.onselectstart;\r
- document.onselectstart = L.Util.falseFn;\r
- }\r
- },\r
-\r
- enableTextSelection: function () {\r
- if (document.onselectstart === L.Util.falseFn) {\r
- document.onselectstart = this._onselectstart;\r
- this._onselectstart = null;\r
- }\r
- },\r
-\r
- hasClass: function (el, name) {\r
- return (el.className.length > 0) &&\r
- new RegExp("(^|\\s)" + name + "(\\s|$)").test(el.className);\r
- },\r
-\r
- addClass: function (el, name) {\r
- if (!L.DomUtil.hasClass(el, name)) {\r
- el.className += (el.className ? ' ' : '') + name;\r
- }\r
- },\r
-\r
- removeClass: function (el, name) {\r
-\r
- function replaceFn(w, match) {\r
- if (match === name) { return ''; }\r
- return w;\r
- }\r
-\r
- el.className = el.className\r
- .replace(/(\S+)\s*/g, replaceFn)\r
- .replace(/(^\s+|\s+$)/, '');\r
- },\r
-\r
- setOpacity: function (el, value) {\r
-\r
- if ('opacity' in el.style) {\r
- el.style.opacity = value;\r
-\r
- } else if (L.Browser.ie) {\r
-\r
- var filter = false,\r
- filterName = 'DXImageTransform.Microsoft.Alpha';\r
-\r
- // filters collection throws an error if we try to retrieve a filter that doesn't exist\r
- try { filter = el.filters.item(filterName); } catch (e) {}\r
-\r
- value = Math.round(value * 100);\r
-\r
- if (filter) {\r
- filter.Enabled = (value !== 100);\r
- filter.Opacity = value;\r
- } else {\r
- el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';\r
- }\r
- }\r
- },\r
-\r
- testProp: function (props) {\r
-\r
- var style = document.documentElement.style;\r
-\r
- for (var i = 0; i < props.length; i++) {\r
- if (props[i] in style) {\r
- return props[i];\r
- }\r
- }\r
- return false;\r
- },\r
-\r
- getTranslateString: function (point) {\r
- // on WebKit browsers (Chrome/Safari/iOS Safari/Android) using translate3d instead of translate\r
- // makes animation smoother as it ensures HW accel is used. Firefox 13 doesn't care\r
- // (same speed either way), Opera 12 doesn't support translate3d\r
-\r
- var is3d = L.Browser.webkit3d,\r
- open = 'translate' + (is3d ? '3d' : '') + '(',\r
- close = (is3d ? ',0' : '') + ')';\r
-\r
- return open + point.x + 'px,' + point.y + 'px' + close;\r
- },\r
-\r
- getScaleString: function (scale, origin) {\r
-\r
- var preTranslateStr = L.DomUtil.getTranslateString(origin.add(origin.multiplyBy(-1 * scale))),\r
- scaleStr = ' scale(' + scale + ') ';\r
-\r
- return preTranslateStr + scaleStr;\r
- },\r
-\r
- setPosition: function (el, point, disable3D) { // (HTMLElement, Point[, Boolean])\r
-\r
- el._leaflet_pos = point;\r
-\r
- if (!disable3D && L.Browser.any3d) {\r
- el.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(point);\r
-\r
- // workaround for Android 2/3 stability (https://github.com/CloudMade/Leaflet/issues/69)\r
- if (L.Browser.mobileWebkit3d) {\r
- el.style.WebkitBackfaceVisibility = 'hidden';\r
- }\r
- } else {\r
- el.style.left = point.x + 'px';\r
- el.style.top = point.y + 'px';\r
- }\r
- },\r
-\r
- getPosition: function (el) {\r
- // this method is only used for elements previously positioned using setPosition,\r
- // so it's safe to cache the position for performance\r
- return el._leaflet_pos;\r
- }\r
-};\r
-\r
-\r
-// prefix style property names\r
-\r
-L.DomUtil.TRANSFORM = L.DomUtil.testProp(\r
- ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']);\r
-\r
-L.DomUtil.TRANSITION = L.DomUtil.testProp(\r
- ['transition', 'webkitTransition', 'OTransition', 'MozTransition', 'msTransition']);\r
-\r
-L.DomUtil.TRANSITION_END =\r
- L.DomUtil.TRANSITION === 'webkitTransition' || L.DomUtil.TRANSITION === 'OTransition' ?\r
- L.DomUtil.TRANSITION + 'End' : 'transitionend';\r
-
-
-/*\r
- CM.LatLng represents a geographical point with latitude and longtitude coordinates.\r
-*/\r
-\r
-L.LatLng = function (rawLat, rawLng, noWrap) { // (Number, Number[, Boolean])\r
- var lat = parseFloat(rawLat),\r
- lng = parseFloat(rawLng);\r
-\r
- if (isNaN(lat) || isNaN(lng)) {\r
- throw new Error('Invalid LatLng object: (' + rawLat + ', ' + rawLng + ')');\r
- }\r
-\r
- if (noWrap !== true) {\r
- lat = Math.max(Math.min(lat, 90), -90); // clamp latitude into -90..90\r
- lng = (lng + 180) % 360 + ((lng < -180 || lng === 180) ? 180 : -180); // wrap longtitude into -180..180\r
- }\r
-\r
- this.lat = lat;\r
- this.lng = lng;\r
-};\r
-\r
-L.Util.extend(L.LatLng, {\r
- DEG_TO_RAD: Math.PI / 180,\r
- RAD_TO_DEG: 180 / Math.PI,\r
- MAX_MARGIN: 1.0E-9 // max margin of error for the "equals" check\r
-});\r
-\r
-L.LatLng.prototype = {\r
- equals: function (obj) { // (LatLng) -> Boolean\r
- if (!obj) { return false; }\r
-\r
- obj = L.latLng(obj);\r
-\r
- var margin = Math.max(Math.abs(this.lat - obj.lat), Math.abs(this.lng - obj.lng));\r
- return margin <= L.LatLng.MAX_MARGIN;\r
- },\r
-\r
- toString: function () { // -> String\r
- return 'LatLng(' +\r
- L.Util.formatNum(this.lat) + ', ' +\r
- L.Util.formatNum(this.lng) + ')';\r
- },\r
-\r
- // Haversine distance formula, see http://en.wikipedia.org/wiki/Haversine_formula\r
- distanceTo: function (other) { // (LatLng) -> Number\r
- other = L.latLng(other);\r
-\r
- var R = 6378137, // earth radius in meters\r
- d2r = L.LatLng.DEG_TO_RAD,\r
- dLat = (other.lat - this.lat) * d2r,\r
- dLon = (other.lng - this.lng) * d2r,\r
- lat1 = this.lat * d2r,\r
- lat2 = other.lat * d2r,\r
- sin1 = Math.sin(dLat / 2),\r
- sin2 = Math.sin(dLon / 2);\r
-\r
- var a = sin1 * sin1 + sin2 * sin2 * Math.cos(lat1) * Math.cos(lat2);\r
-\r
- return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\r
- }\r
-};\r
-\r
-L.latLng = function (a, b, c) { // (LatLng) or ([Number, Number]) or (Number, Number, Boolean)\r
- if (a instanceof L.LatLng) {\r
- return a;\r
- }\r
- if (a instanceof Array) {\r
- return new L.LatLng(a[0], a[1]);\r
- }\r
- if (isNaN(a)) {\r
- return a;\r
- }\r
- return new L.LatLng(a, b, c);\r
-};\r
-
-
-/*\r
- * L.LatLngBounds represents a rectangular area on the map in geographical coordinates.\r
- */\r
-\r
-L.LatLngBounds = L.Class.extend({\r
- initialize: function (southWest, northEast) { // (LatLng, LatLng) or (LatLng[])\r
- if (!southWest) { return; }\r
-\r
- var latlngs = northEast ? [southWest, northEast] : southWest;\r
-\r
- for (var i = 0, len = latlngs.length; i < len; i++) {\r
- this.extend(latlngs[i]);\r
- }\r
- },\r
-\r
- // extend the bounds to contain the given point or bounds\r
- extend: function (obj) { // (LatLng) or (LatLngBounds)\r
- if (typeof obj[0] === 'number' || obj instanceof L.LatLng) {\r
- obj = L.latLng(obj);\r
- } else {\r
- obj = L.latLngBounds(obj);\r
- }\r
-\r
- if (obj instanceof L.LatLng) {\r
- if (!this._southWest && !this._northEast) {\r
- this._southWest = new L.LatLng(obj.lat, obj.lng, true);\r
- this._northEast = new L.LatLng(obj.lat, obj.lng, true);\r
- } else {\r
- this._southWest.lat = Math.min(obj.lat, this._southWest.lat);\r
- this._southWest.lng = Math.min(obj.lng, this._southWest.lng);\r
-\r
- this._northEast.lat = Math.max(obj.lat, this._northEast.lat);\r
- this._northEast.lng = Math.max(obj.lng, this._northEast.lng);\r
- }\r
- } else if (obj instanceof L.LatLngBounds) {\r
- this.extend(obj._southWest);\r
- this.extend(obj._northEast);\r
- }\r
- return this;\r
- },\r
-\r
- // extend the bounds by a percentage\r
- pad: function (bufferRatio) { // (Number) -> LatLngBounds\r
- var sw = this._southWest,\r
- ne = this._northEast,\r
- heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,\r
- widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;\r
-\r
- return new L.LatLngBounds(\r
- new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),\r
- new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));\r
- },\r
-\r
- getCenter: function () { // -> LatLng\r
- return new L.LatLng(\r
- (this._southWest.lat + this._northEast.lat) / 2,\r
- (this._southWest.lng + this._northEast.lng) / 2);\r
- },\r
-\r
- getSouthWest: function () {\r
- return this._southWest;\r
- },\r
-\r
- getNorthEast: function () {\r
- return this._northEast;\r
- },\r
-\r
- getNorthWest: function () {\r
- return new L.LatLng(this._northEast.lat, this._southWest.lng, true);\r
- },\r
-\r
- getSouthEast: function () {\r
- return new L.LatLng(this._southWest.lat, this._northEast.lng, true);\r
- },\r
-\r
- contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean\r
- if (typeof obj[0] === 'number' || obj instanceof L.LatLng) {\r
- obj = L.latLng(obj);\r
- } else {\r
- obj = L.latLngBounds(obj);\r
- }\r
-\r
- var sw = this._southWest,\r
- ne = this._northEast,\r
- sw2, ne2;\r
-\r
- if (obj instanceof L.LatLngBounds) {\r
- sw2 = obj.getSouthWest();\r
- ne2 = obj.getNorthEast();\r
- } else {\r
- sw2 = ne2 = obj;\r
- }\r
-\r
- return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&\r
- (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);\r
- },\r
-\r
- intersects: function (bounds) { // (LatLngBounds)\r
- bounds = L.latLngBounds(bounds);\r
-\r
- var sw = this._southWest,\r
- ne = this._northEast,\r
- sw2 = bounds.getSouthWest(),\r
- ne2 = bounds.getNorthEast();\r
-\r
- var latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),\r
- lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);\r
-\r
- return latIntersects && lngIntersects;\r
- },\r
-\r
- toBBoxString: function () {\r
- var sw = this._southWest,\r
- ne = this._northEast;\r
- return [sw.lng, sw.lat, ne.lng, ne.lat].join(',');\r
- },\r
-\r
- equals: function (bounds) { // (LatLngBounds)\r
- if (!bounds) { return false; }\r
-\r
- bounds = L.latLngBounds(bounds);\r
-\r
- return this._southWest.equals(bounds.getSouthWest()) &&\r
- this._northEast.equals(bounds.getNorthEast());\r
- },\r
-\r
- isValid: function () {\r
- return !!(this._southWest && this._northEast);\r
- }\r
-});\r
-\r
-//TODO International date line?\r
-\r
-L.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng)\r
- if (!a || a instanceof L.LatLngBounds) {\r
- return a;\r
- }\r
- return new L.LatLngBounds(a, b);\r
-};\r
-
-
-/*\r
- * L.Projection contains various geographical projections used by CRS classes.\r
- */\r
-\r
-L.Projection = {};\r
-
-
-\r
-L.Projection.SphericalMercator = {\r
- MAX_LATITUDE: 85.0511287798,\r
-\r
- project: function (latlng) { // (LatLng) -> Point\r
- var d = L.LatLng.DEG_TO_RAD,\r
- max = this.MAX_LATITUDE,\r
- lat = Math.max(Math.min(max, latlng.lat), -max),\r
- x = latlng.lng * d,\r
- y = lat * d;\r
- y = Math.log(Math.tan((Math.PI / 4) + (y / 2)));\r
-\r
- return new L.Point(x, y);\r
- },\r
-\r
- unproject: function (point) { // (Point, Boolean) -> LatLng\r
- var d = L.LatLng.RAD_TO_DEG,\r
- lng = point.x * d,\r
- lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d;\r
-\r
- // TODO refactor LatLng wrapping\r
- return new L.LatLng(lat, lng, true);\r
- }\r
-};\r
-
-
-\r
-L.Projection.LonLat = {\r
- project: function (latlng) {\r
- return new L.Point(latlng.lng, latlng.lat);\r
- },\r
-\r
- unproject: function (point) {\r
- return new L.LatLng(point.y, point.x, true);\r
- }\r
-};\r
-
-
-\r
-L.CRS = {\r
- latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point\r
- var projectedPoint = this.projection.project(latlng),\r
- scale = this.scale(zoom);\r
-\r
- return this.transformation._transform(projectedPoint, scale);\r
- },\r
-\r
- pointToLatLng: function (point, zoom) { // (Point, Number[, Boolean]) -> LatLng\r
- var scale = this.scale(zoom),\r
- untransformedPoint = this.transformation.untransform(point, scale);\r
-\r
- return this.projection.unproject(untransformedPoint);\r
- },\r
-\r
- project: function (latlng) {\r
- return this.projection.project(latlng);\r
- },\r
-\r
- scale: function (zoom) {\r
- return 256 * Math.pow(2, zoom);\r
- }\r
-};\r
-
-
-\r
-L.CRS.EPSG3857 = L.Util.extend({}, L.CRS, {\r
- code: 'EPSG:3857',\r
-\r
- projection: L.Projection.SphericalMercator,\r
- transformation: new L.Transformation(0.5 / Math.PI, 0.5, -0.5 / Math.PI, 0.5),\r
-\r
- project: function (latlng) { // (LatLng) -> Point\r
- var projectedPoint = this.projection.project(latlng),\r
- earthRadius = 6378137;\r
- return projectedPoint.multiplyBy(earthRadius);\r
- }\r
-});\r
-\r
-L.CRS.EPSG900913 = L.Util.extend({}, L.CRS.EPSG3857, {\r
- code: 'EPSG:900913'\r
-});\r
-
-
-\r
-L.CRS.EPSG4326 = L.Util.extend({}, L.CRS, {\r
- code: 'EPSG:4326',\r
-\r
- projection: L.Projection.LonLat,\r
- transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5)\r
-});\r
-
-
-/*\r
- * L.Map is the central class of the API - it is used to create a map.\r
- */\r
-\r
-L.Map = L.Class.extend({\r
-\r
- includes: L.Mixin.Events,\r
-\r
- options: {\r
- crs: L.CRS.EPSG3857,\r
-\r
- /*\r
- center: LatLng,\r
- zoom: Number,\r
- layers: Array,\r
- */\r
-\r
- fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android23,\r
- trackResize: true,\r
- markerZoomAnimation: L.DomUtil.TRANSITION && L.Browser.any3d\r
- },\r
-\r
- initialize: function (id, options) { // (HTMLElement or String, Object)\r
- options = L.Util.setOptions(this, options);\r
-\r
- this._initContainer(id);\r
- this._initLayout();\r
- this._initHooks();\r
- this._initEvents();\r
-\r
- if (options.maxBounds) {\r
- this.setMaxBounds(options.maxBounds);\r
- }\r
-\r
- if (options.center && options.zoom !== undefined) {\r
- this.setView(L.latLng(options.center), options.zoom, true);\r
- }\r
-\r
- this._initLayers(options.layers);\r
- },\r
-\r
-\r
- // public methods that modify map state\r
-\r
- // replaced by animation-powered implementation in Map.PanAnimation.js\r
- setView: function (center, zoom) {\r
- this._resetView(L.latLng(center), this._limitZoom(zoom));\r
- return this;\r
- },\r
-\r
- setZoom: function (zoom) { // (Number)\r
- return this.setView(this.getCenter(), zoom);\r
- },\r
-\r
- zoomIn: function (delta) {\r
- return this.setZoom(this._zoom + (delta || 1));\r
- },\r
-\r
- zoomOut: function (delta) {\r
- return this.setZoom(this._zoom - (delta || 1));\r
- },\r
-\r
- fitBounds: function (bounds) { // (LatLngBounds)\r
- var zoom = this.getBoundsZoom(bounds);\r
- return this.setView(L.latLngBounds(bounds).getCenter(), zoom);\r
- },\r
-\r
- fitWorld: function () {\r
- var sw = new L.LatLng(-60, -170),\r
- ne = new L.LatLng(85, 179);\r
-\r
- return this.fitBounds(new L.LatLngBounds(sw, ne));\r
- },\r
-\r
- panTo: function (center) { // (LatLng)\r
- return this.setView(center, this._zoom);\r
- },\r
-\r
- panBy: function (offset) { // (Point)\r
- // replaced with animated panBy in Map.Animation.js\r
- this.fire('movestart');\r
-\r
- this._rawPanBy(L.point(offset));\r
-\r
- this.fire('move');\r
- return this.fire('moveend');\r
- },\r
-\r
- setMaxBounds: function (bounds) {\r
- bounds = L.latLngBounds(bounds);\r
-\r
- this.options.maxBounds = bounds;\r
-\r
- if (!bounds) {\r
- this._boundsMinZoom = null;\r
- return this;\r
- }\r
-\r
- var minZoom = this.getBoundsZoom(bounds, true);\r
-\r
- this._boundsMinZoom = minZoom;\r
-\r
- if (this._loaded) {\r
- if (this._zoom < minZoom) {\r
- this.setView(bounds.getCenter(), minZoom);\r
- } else {\r
- this.panInsideBounds(bounds);\r
- }\r
- }\r
-\r
- return this;\r
- },\r
-\r
- panInsideBounds: function (bounds) {\r
- bounds = L.latLngBounds(bounds);\r
-\r
- var viewBounds = this.getBounds(),\r
- viewSw = this.project(viewBounds.getSouthWest()),\r
- viewNe = this.project(viewBounds.getNorthEast()),\r
- sw = this.project(bounds.getSouthWest()),\r
- ne = this.project(bounds.getNorthEast()),\r
- dx = 0,\r
- dy = 0;\r
-\r
- if (viewNe.y < ne.y) { // north\r
- dy = ne.y - viewNe.y;\r
- }\r
- if (viewNe.x > ne.x) { // east\r
- dx = ne.x - viewNe.x;\r
- }\r
- if (viewSw.y > sw.y) { // south\r
- dy = sw.y - viewSw.y;\r
- }\r
- if (viewSw.x < sw.x) { // west\r
- dx = sw.x - viewSw.x;\r
- }\r
-\r
- return this.panBy(new L.Point(dx, dy, true));\r
- },\r
-\r
- addLayer: function (layer) {\r
- // TODO method is too big, refactor\r
-\r
- var id = L.Util.stamp(layer);\r
-\r
- if (this._layers[id]) { return this; }\r
-\r
- this._layers[id] = layer;\r
-\r
- // TODO getMaxZoom, getMinZoom in ILayer (instead of options)\r
- if (layer.options && !isNaN(layer.options.maxZoom)) {\r
- this._layersMaxZoom = Math.max(this._layersMaxZoom || 0, layer.options.maxZoom);\r
- }\r
- if (layer.options && !isNaN(layer.options.minZoom)) {\r
- this._layersMinZoom = Math.min(this._layersMinZoom || Infinity, layer.options.minZoom);\r
- }\r
-\r
- // TODO looks ugly, refactor!!!\r
- if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {\r
- this._tileLayersNum++;\r
- this._tileLayersToLoad++;\r
- layer.on('load', this._onTileLayerLoad, this);\r
- }\r
-\r
- this.whenReady(function () {\r
- layer.onAdd(this);\r
- this.fire('layeradd', {layer: layer});\r
- }, this);\r
-\r
- return this;\r
- },\r
-\r
- removeLayer: function (layer) {\r
- var id = L.Util.stamp(layer);\r
-\r
- if (!this._layers[id]) { return; }\r
-\r
- layer.onRemove(this);\r
-\r
- delete this._layers[id];\r
-\r
- // TODO looks ugly, refactor\r
- if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {\r
- this._tileLayersNum--;\r
- this._tileLayersToLoad--;\r
- layer.off('load', this._onTileLayerLoad, this);\r
- }\r
-\r
- return this.fire('layerremove', {layer: layer});\r
- },\r
-\r
- hasLayer: function (layer) {\r
- var id = L.Util.stamp(layer);\r
- return this._layers.hasOwnProperty(id);\r
- },\r
-\r
- invalidateSize: function (animate) {\r
- var oldSize = this.getSize();\r
-\r
- this._sizeChanged = true;\r
-\r
- if (this.options.maxBounds) {\r
- this.setMaxBounds(this.options.maxBounds);\r
- }\r
-\r
- if (!this._loaded) { return this; }\r
-\r
- var offset = oldSize._subtract(this.getSize())._divideBy(2)._round();\r
-\r
- if (animate === true) {\r
- this.panBy(offset);\r
- } else {\r
- this._rawPanBy(offset);\r
-\r
- this.fire('move');\r
-\r
- clearTimeout(this._sizeTimer);\r
- this._sizeTimer = setTimeout(L.Util.bind(this.fire, this, 'moveend'), 200);\r
- }\r
- return this;\r
- },\r
-\r
- // TODO handler.addTo\r
- addHandler: function (name, HandlerClass) {\r
- if (!HandlerClass) { return; }\r
-\r
- this[name] = new HandlerClass(this);\r
-\r
- if (this.options[name]) {\r
- this[name].enable();\r
- }\r
-\r
- return this;\r
- },\r
-\r
-\r
- // public methods for getting map state\r
-\r
- getCenter: function () { // (Boolean) -> LatLng\r
- return this.layerPointToLatLng(this._getCenterLayerPoint());\r
- },\r
-\r
- getZoom: function () {\r
- return this._zoom;\r
- },\r
-\r
- getBounds: function () {\r
- var bounds = this.getPixelBounds(),\r
- sw = this.unproject(bounds.getBottomLeft()),\r
- ne = this.unproject(bounds.getTopRight());\r
-\r
- return new L.LatLngBounds(sw, ne);\r
- },\r
-\r
- getMinZoom: function () {\r
- var z1 = this.options.minZoom || 0,\r
- z2 = this._layersMinZoom || 0,\r
- z3 = this._boundsMinZoom || 0;\r
-\r
- return Math.max(z1, z2, z3);\r
- },\r
-\r
- getMaxZoom: function () {\r
- var z1 = this.options.maxZoom === undefined ? Infinity : this.options.maxZoom,\r
- z2 = this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom;\r
-\r
- return Math.min(z1, z2);\r
- },\r
-\r
- getBoundsZoom: function (bounds, inside) { // (LatLngBounds, Boolean) -> Number\r
- bounds = L.latLngBounds(bounds);\r
-\r
- var size = this.getSize(),\r
- zoom = this.options.minZoom || 0,\r
- maxZoom = this.getMaxZoom(),\r
- ne = bounds.getNorthEast(),\r
- sw = bounds.getSouthWest(),\r
- boundsSize,\r
- nePoint,\r
- swPoint,\r
- zoomNotFound = true;\r
-\r
- if (inside) {\r
- zoom--;\r
- }\r
-\r
- do {\r
- zoom++;\r
- nePoint = this.project(ne, zoom);\r
- swPoint = this.project(sw, zoom);\r
- boundsSize = new L.Point(Math.abs(nePoint.x - swPoint.x), Math.abs(swPoint.y - nePoint.y));\r
-\r
- if (!inside) {\r
- zoomNotFound = boundsSize.x <= size.x && boundsSize.y <= size.y;\r
- } else {\r
- zoomNotFound = boundsSize.x < size.x || boundsSize.y < size.y;\r
- }\r
- } while (zoomNotFound && zoom <= maxZoom);\r
-\r
- if (zoomNotFound && inside) {\r
- return null;\r
- }\r
-\r
- return inside ? zoom : zoom - 1;\r
- },\r
-\r
- getSize: function () {\r
- if (!this._size || this._sizeChanged) {\r
- this._size = new L.Point(\r
- this._container.clientWidth,\r
- this._container.clientHeight);\r
-\r
- this._sizeChanged = false;\r
- }\r
- return this._size.clone();\r
- },\r
-\r
- getPixelBounds: function () {\r
- var topLeftPoint = this._getTopLeftPoint();\r
- return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));\r
- },\r
-\r
- getPixelOrigin: function () {\r
- return this._initialTopLeftPoint;\r
- },\r
-\r
- getPanes: function () {\r
- return this._panes;\r
- },\r
-\r
- getContainer: function () {\r
- return this._container;\r
- },\r
-\r
-\r
- // TODO replace with universal implementation after refactoring projections\r
-\r
- getZoomScale: function (toZoom) {\r
- var crs = this.options.crs;\r
- return crs.scale(toZoom) / crs.scale(this._zoom);\r
- },\r
-\r
- getScaleZoom: function (scale) {\r
- return this._zoom + (Math.log(scale) / Math.LN2);\r
- },\r
-\r
-\r
- // conversion methods\r
-\r
- project: function (latlng, zoom) { // (LatLng[, Number]) -> Point\r
- zoom = zoom === undefined ? this._zoom : zoom;\r
- return this.options.crs.latLngToPoint(L.latLng(latlng), zoom);\r
- },\r
-\r
- unproject: function (point, zoom) { // (Point[, Number]) -> LatLng\r
- zoom = zoom === undefined ? this._zoom : zoom;\r
- return this.options.crs.pointToLatLng(L.point(point), zoom);\r
- },\r
-\r
- layerPointToLatLng: function (point) { // (Point)\r
- var projectedPoint = L.point(point).add(this._initialTopLeftPoint);\r
- return this.unproject(projectedPoint);\r
- },\r
-\r
- latLngToLayerPoint: function (latlng) { // (LatLng)\r
- var projectedPoint = this.project(L.latLng(latlng))._round();\r
- return projectedPoint._subtract(this._initialTopLeftPoint);\r
- },\r
-\r
- containerPointToLayerPoint: function (point) { // (Point)\r
- return L.point(point).subtract(this._getMapPanePos());\r
- },\r
-\r
- layerPointToContainerPoint: function (point) { // (Point)\r
- return L.point(point).add(this._getMapPanePos());\r
- },\r
-\r
- containerPointToLatLng: function (point) {\r
- var layerPoint = this.containerPointToLayerPoint(L.point(point));\r
- return this.layerPointToLatLng(layerPoint);\r
- },\r
-\r
- latLngToContainerPoint: function (latlng) {\r
- return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng)));\r
- },\r
-\r
- mouseEventToContainerPoint: function (e) { // (MouseEvent)\r
- return L.DomEvent.getMousePosition(e, this._container);\r
- },\r
-\r
- mouseEventToLayerPoint: function (e) { // (MouseEvent)\r
- return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));\r
- },\r
-\r
- mouseEventToLatLng: function (e) { // (MouseEvent)\r
- return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));\r
- },\r
-\r
-\r
- // map initialization methods\r
-\r
- _initContainer: function (id) {\r
- var container = this._container = L.DomUtil.get(id);\r
-\r
- if (container._leaflet) {\r
- throw new Error("Map container is already initialized.");\r
- }\r
-\r
- container._leaflet = true;\r
- },\r
-\r
- _initLayout: function () {\r
- var container = this._container;\r
-\r
- container.innerHTML = '';\r
- L.DomUtil.addClass(container, 'leaflet-container');\r
-\r
- if (L.Browser.touch) {\r
- L.DomUtil.addClass(container, 'leaflet-touch');\r
- }\r
-\r
- if (this.options.fadeAnimation) {\r
- L.DomUtil.addClass(container, 'leaflet-fade-anim');\r
- }\r
-\r
- var position = L.DomUtil.getStyle(container, 'position');\r
-\r
- if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {\r
- container.style.position = 'relative';\r
- }\r
-\r
- this._initPanes();\r
-\r
- if (this._initControlPos) {\r
- this._initControlPos();\r
- }\r
- },\r
-\r
- _initPanes: function () {\r
- var panes = this._panes = {};\r
-\r
- this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container);\r
-\r
- this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane);\r
- this._objectsPane = panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane);\r
-\r
- panes.shadowPane = this._createPane('leaflet-shadow-pane');\r
- panes.overlayPane = this._createPane('leaflet-overlay-pane');\r
- panes.markerPane = this._createPane('leaflet-marker-pane');\r
- panes.popupPane = this._createPane('leaflet-popup-pane');\r
-\r
- var zoomHide = ' leaflet-zoom-hide';\r
-\r
- if (!this.options.markerZoomAnimation) {\r
- L.DomUtil.addClass(panes.markerPane, zoomHide);\r
- L.DomUtil.addClass(panes.shadowPane, zoomHide);\r
- L.DomUtil.addClass(panes.popupPane, zoomHide);\r
- }\r
- },\r
-\r
- _createPane: function (className, container) {\r
- return L.DomUtil.create('div', className, container || this._objectsPane);\r
- },\r
-\r
- _initializers: [],\r
-\r
- _initHooks: function () {\r
- var i, len;\r
- for (i = 0, len = this._initializers.length; i < len; i++) {\r
- this._initializers[i].call(this);\r
- }\r
- },\r
-\r
- _initLayers: function (layers) {\r
- layers = layers ? (layers instanceof Array ? layers : [layers]) : [];\r
-\r
- this._layers = {};\r
- this._tileLayersNum = 0;\r
-\r
- var i, len;\r
-\r
- for (i = 0, len = layers.length; i < len; i++) {\r
- this.addLayer(layers[i]);\r
- }\r
- },\r
-\r
-\r
- // private methods that modify map state\r
-\r
- _resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) {\r
-\r
- var zoomChanged = (this._zoom !== zoom);\r
-\r
- if (!afterZoomAnim) {\r
- this.fire('movestart');\r
-\r
- if (zoomChanged) {\r
- this.fire('zoomstart');\r
- }\r
- }\r
-\r
- this._zoom = zoom;\r
-\r
- this._initialTopLeftPoint = this._getNewTopLeftPoint(center);\r
-\r
- if (!preserveMapOffset) {\r
- L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));\r
- } else {\r
- this._initialTopLeftPoint._add(this._getMapPanePos());\r
- }\r
-\r
- this._tileLayersToLoad = this._tileLayersNum;\r
-\r
- var loading = !this._loaded;\r
- this._loaded = true;\r
-\r
- this.fire('viewreset', {hard: !preserveMapOffset});\r
-\r
- this.fire('move');\r
-\r
- if (zoomChanged || afterZoomAnim) {\r
- this.fire('zoomend');\r
- }\r
-\r
- this.fire('moveend', {hard: !preserveMapOffset});\r
-\r
- if (loading) {\r
- this.fire('load');\r
- }\r
- },\r
-\r
- _rawPanBy: function (offset) {\r
- L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));\r
- },\r
-\r
-\r
- // map events\r
-\r
- _initEvents: function () {\r
- if (!L.DomEvent) { return; }\r
-\r
- L.DomEvent.on(this._container, 'click', this._onMouseClick, this);\r
-\r
- var events = ['dblclick', 'mousedown', 'mouseup', 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu'],\r
- i, len;\r
-\r
- for (i = 0, len = events.length; i < len; i++) {\r
- L.DomEvent.on(this._container, events[i], this._fireMouseEvent, this);\r
- }\r
-\r
- if (this.options.trackResize) {\r
- L.DomEvent.on(window, 'resize', this._onResize, this);\r
- }\r
- },\r
-\r
- _onResize: function () {\r
- L.Util.cancelAnimFrame(this._resizeRequest);\r
- this._resizeRequest = L.Util.requestAnimFrame(this.invalidateSize, this, false, this._container);\r
- },\r
-\r
- _onMouseClick: function (e) {\r
- if (!this._loaded || (this.dragging && this.dragging.moved())) { return; }\r
-\r
- this.fire('preclick');\r
- this._fireMouseEvent(e);\r
- },\r
-\r
- _fireMouseEvent: function (e) {\r
- if (!this._loaded) { return; }\r
-\r
- var type = e.type;\r
-\r
- type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type));\r
-\r
- if (!this.hasEventListeners(type)) { return; }\r
-\r
- if (type === 'contextmenu') {\r
- L.DomEvent.preventDefault(e);\r
- }\r
-\r
- var containerPoint = this.mouseEventToContainerPoint(e),\r
- layerPoint = this.containerPointToLayerPoint(containerPoint),\r
- latlng = this.layerPointToLatLng(layerPoint);\r
-\r
- this.fire(type, {\r
- latlng: latlng,\r
- layerPoint: layerPoint,\r
- containerPoint: containerPoint,\r
- originalEvent: e\r
- });\r
- },\r
-\r
- _onTileLayerLoad: function () {\r
- // TODO super-ugly, refactor!!!\r
- // clear scaled tiles after all new tiles are loaded (for performance)\r
- this._tileLayersToLoad--;\r
- if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) {\r
- clearTimeout(this._clearTileBgTimer);\r
- this._clearTileBgTimer = setTimeout(L.Util.bind(this._clearTileBg, this), 500);\r
- }\r
- },\r
-\r
- whenReady: function (callback, context) {\r
- if (this._loaded) {\r
- callback.call(context || this, this);\r
- } else {\r
- this.on('load', callback, context);\r
- }\r
- return this;\r
- },\r
-\r
-\r
- // private methods for getting map state\r
-\r
- _getMapPanePos: function () {\r
- return L.DomUtil.getPosition(this._mapPane);\r
- },\r
-\r
- _getTopLeftPoint: function () {\r
- if (!this._loaded) {\r
- throw new Error('Set map center and zoom first.');\r
- }\r
-\r
- return this._initialTopLeftPoint.subtract(this._getMapPanePos());\r
- },\r
-\r
- _getNewTopLeftPoint: function (center, zoom) {\r
- var viewHalf = this.getSize()._divideBy(2);\r
- // TODO round on display, not calculation to increase precision?\r
- return this.project(center, zoom)._subtract(viewHalf)._round();\r
- },\r
-\r
- _latLngToNewLayerPoint: function (latlng, newZoom, newCenter) {\r
- var topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(this._getMapPanePos());\r
- return this.project(latlng, newZoom)._subtract(topLeft);\r
- },\r
-\r
- _getCenterLayerPoint: function () {\r
- return this.containerPointToLayerPoint(this.getSize()._divideBy(2));\r
- },\r
-\r
- _getCenterOffset: function (center) {\r
- return this.latLngToLayerPoint(center).subtract(this._getCenterLayerPoint());\r
- },\r
-\r
- _limitZoom: function (zoom) {\r
- var min = this.getMinZoom(),\r
- max = this.getMaxZoom();\r
-\r
- return Math.max(min, Math.min(max, zoom));\r
- }\r
-});\r
-\r
-L.Map.addInitHook = function (fn) {\r
- var args = Array.prototype.slice.call(arguments, 1);\r
-\r
- var init = typeof fn === 'function' ? fn : function () {\r
- this[fn].apply(this, args);\r
- };\r
-\r
- this.prototype._initializers.push(init);\r
-};\r
-\r
-L.map = function (id, options) {\r
- return new L.Map(id, options);\r
-};\r
-
-
-\r
-L.Projection.Mercator = {\r
- MAX_LATITUDE: 85.0840591556,\r
-\r
- R_MINOR: 6356752.3142,\r
- R_MAJOR: 6378137,\r
-\r
- project: function (latlng) { // (LatLng) -> Point\r
- var d = L.LatLng.DEG_TO_RAD,\r
- max = this.MAX_LATITUDE,\r
- lat = Math.max(Math.min(max, latlng.lat), -max),\r
- r = this.R_MAJOR,\r
- r2 = this.R_MINOR,\r
- x = latlng.lng * d * r,\r
- y = lat * d,\r
- tmp = r2 / r,\r
- eccent = Math.sqrt(1.0 - tmp * tmp),\r
- con = eccent * Math.sin(y);\r
-\r
- con = Math.pow((1 - con) / (1 + con), eccent * 0.5);\r
-\r
- var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con;\r
- y = -r2 * Math.log(ts);\r
-\r
- return new L.Point(x, y);\r
- },\r
-\r
- unproject: function (point) { // (Point, Boolean) -> LatLng\r
- var d = L.LatLng.RAD_TO_DEG,\r
- r = this.R_MAJOR,\r
- r2 = this.R_MINOR,\r
- lng = point.x * d / r,\r
- tmp = r2 / r,\r
- eccent = Math.sqrt(1 - (tmp * tmp)),\r
- ts = Math.exp(- point.y / r2),\r
- phi = (Math.PI / 2) - 2 * Math.atan(ts),\r
- numIter = 15,\r
- tol = 1e-7,\r
- i = numIter,\r
- dphi = 0.1,\r
- con;\r
-\r
- while ((Math.abs(dphi) > tol) && (--i > 0)) {\r
- con = eccent * Math.sin(phi);\r
- dphi = (Math.PI / 2) - 2 * Math.atan(ts * Math.pow((1.0 - con) / (1.0 + con), 0.5 * eccent)) - phi;\r
- phi += dphi;\r
- }\r
-\r
- return new L.LatLng(phi * d, lng, true);\r
- }\r
-};\r
-
-
-\r
-L.CRS.EPSG3395 = L.Util.extend({}, L.CRS, {\r
- code: 'EPSG:3395',\r
-\r
- projection: L.Projection.Mercator,\r
-\r
- transformation: (function () {\r
- var m = L.Projection.Mercator,\r
- r = m.R_MAJOR,\r
- r2 = m.R_MINOR;\r
-\r
- return new L.Transformation(0.5 / (Math.PI * r), 0.5, -0.5 / (Math.PI * r2), 0.5);\r
- }())\r
-});\r
-
-
-/*\r
- * L.TileLayer is used for standard xyz-numbered tile layers.\r
- */\r
-\r
-L.TileLayer = L.Class.extend({\r
- includes: L.Mixin.Events,\r
-\r
- options: {\r
- minZoom: 0,\r
- maxZoom: 18,\r
- tileSize: 256,\r
- subdomains: 'abc',\r
- errorTileUrl: '',\r
- attribution: '',\r
- zoomOffset: 0,\r
- opacity: 1,\r
- /* (undefined works too)\r
- zIndex: null,\r
- tms: false,\r
- continuousWorld: false,\r
- noWrap: false,\r
- zoomReverse: false,\r
- detectRetina: false,\r
- reuseTiles: false,\r
- */\r
- unloadInvisibleTiles: L.Browser.mobile,\r
- updateWhenIdle: L.Browser.mobile\r
- },\r
-\r
- initialize: function (url, options) {\r
- options = L.Util.setOptions(this, options);\r
-\r
- // detecting retina displays, adjusting tileSize and zoom levels\r
- if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) {\r
-\r
- options.tileSize = Math.floor(options.tileSize / 2);\r
- options.zoomOffset++;\r
-\r
- if (options.minZoom > 0) {\r
- options.minZoom--;\r
- }\r
- this.options.maxZoom--;\r
- }\r
-\r
- this._url = url;\r
-\r
- var subdomains = this.options.subdomains;\r
-\r
- if (typeof subdomains === 'string') {\r
- this.options.subdomains = subdomains.split('');\r
- }\r
- },\r
-\r
- onAdd: function (map) {\r
- this._map = map;\r
-\r
- // create a container div for tiles\r
- this._initContainer();\r
-\r
- // create an image to clone for tiles\r
- this._createTileProto();\r
-\r
- // set up events\r
- map.on({\r
- 'viewreset': this._resetCallback,\r
- 'moveend': this._update\r
- }, this);\r
-\r
- if (!this.options.updateWhenIdle) {\r
- this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this);\r
- map.on('move', this._limitedUpdate, this);\r
- }\r
-\r
- this._reset();\r
- this._update();\r
- },\r
-\r
- addTo: function (map) {\r
- map.addLayer(this);\r
- return this;\r
- },\r
-\r
- onRemove: function (map) {\r
- map._panes.tilePane.removeChild(this._container);\r
-\r
- map.off({\r
- 'viewreset': this._resetCallback,\r
- 'moveend': this._update\r
- }, this);\r
-\r
- if (!this.options.updateWhenIdle) {\r
- map.off('move', this._limitedUpdate, this);\r
- }\r
-\r
- this._container = null;\r
- this._map = null;\r
- },\r
-\r
- bringToFront: function () {\r
- var pane = this._map._panes.tilePane;\r
-\r
- if (this._container) {\r
- pane.appendChild(this._container);\r
- this._setAutoZIndex(pane, Math.max);\r
- }\r
-\r
- return this;\r
- },\r
-\r
- bringToBack: function () {\r
- var pane = this._map._panes.tilePane;\r
-\r
- if (this._container) {\r
- pane.insertBefore(this._container, pane.firstChild);\r
- this._setAutoZIndex(pane, Math.min);\r
- }\r
-\r
- return this;\r
- },\r
-\r
- getAttribution: function () {\r
- return this.options.attribution;\r
- },\r
-\r
- setOpacity: function (opacity) {\r
- this.options.opacity = opacity;\r
-\r
- if (this._map) {\r
- this._updateOpacity();\r
- }\r
-\r
- return this;\r
- },\r
-\r
- setZIndex: function (zIndex) {\r
- this.options.zIndex = zIndex;\r
- this._updateZIndex();\r
-\r
- return this;\r
- },\r
-\r
- setUrl: function (url, noRedraw) {\r
- this._url = url;\r
-\r
- if (!noRedraw) {\r
- this.redraw();\r
- }\r
-\r
- return this;\r
- },\r
-\r
- redraw: function () {\r
- if (this._map) {\r
- this._map._panes.tilePane.empty = false;\r
- this._reset(true);\r
- this._update();\r
- }\r
- return this;\r
- },\r
-\r
- _updateZIndex: function () {\r
- if (this._container && this.options.zIndex !== undefined) {\r
- this._container.style.zIndex = this.options.zIndex;\r
- }\r
- },\r
-\r
- _setAutoZIndex: function (pane, compare) {\r
-\r
- var layers = pane.getElementsByClassName('leaflet-layer'),\r
- edgeZIndex = -compare(Infinity, -Infinity), // -Ifinity for max, Infinity for min\r
- zIndex;\r
-\r
- for (var i = 0, len = layers.length; i < len; i++) {\r
-\r
- if (layers[i] !== this._container) {\r
- zIndex = parseInt(layers[i].style.zIndex, 10);\r
-\r
- if (!isNaN(zIndex)) {\r
- edgeZIndex = compare(edgeZIndex, zIndex);\r
- }\r
- }\r
- }\r
-\r
- this.options.zIndex = this._container.style.zIndex = (isFinite(edgeZIndex) ? edgeZIndex : 0) + compare(1, -1);\r
- },\r
-\r
- _updateOpacity: function () {\r
- L.DomUtil.setOpacity(this._container, this.options.opacity);\r
-\r
- // stupid webkit hack to force redrawing of tiles\r
- var i,\r
- tiles = this._tiles;\r
-\r
- if (L.Browser.webkit) {\r
- for (i in tiles) {\r
- if (tiles.hasOwnProperty(i)) {\r
- tiles[i].style.webkitTransform += ' translate(0,0)';\r
- }\r
- }\r
- }\r
- },\r
-\r
- _initContainer: function () {\r
- var tilePane = this._map._panes.tilePane;\r
-\r
- if (!this._container || tilePane.empty) {\r
- this._container = L.DomUtil.create('div', 'leaflet-layer');\r
-\r
- this._updateZIndex();\r
-\r
- tilePane.appendChild(this._container);\r
-\r
- if (this.options.opacity < 1) {\r
- this._updateOpacity();\r
- }\r
- }\r
- },\r
-\r
- _resetCallback: function (e) {\r
- this._reset(e.hard);\r
- },\r
-\r
- _reset: function (clearOldContainer) {\r
- var key,\r
- tiles = this._tiles;\r
-\r
- for (key in tiles) {\r
- if (tiles.hasOwnProperty(key)) {\r
- this.fire('tileunload', {tile: tiles[key]});\r
- }\r
- }\r
-\r
- this._tiles = {};\r
- this._tilesToLoad = 0;\r
-\r
- if (this.options.reuseTiles) {\r
- this._unusedTiles = [];\r
- }\r
-\r
- if (clearOldContainer && this._container) {\r
- this._container.innerHTML = "";\r
- }\r
-\r
- this._initContainer();\r
- },\r
-\r
- _update: function (e) {\r
-\r
- if (!this._map) { return; }\r
-\r
- var bounds = this._map.getPixelBounds(),\r
- zoom = this._map.getZoom(),\r
- tileSize = this.options.tileSize;\r
-\r
- if (zoom > this.options.maxZoom || zoom < this.options.minZoom) {\r
- return;\r
- }\r
-\r
- var nwTilePoint = new L.Point(\r
- Math.floor(bounds.min.x / tileSize),\r
- Math.floor(bounds.min.y / tileSize)),\r
- seTilePoint = new L.Point(\r
- Math.floor(bounds.max.x / tileSize),\r
- Math.floor(bounds.max.y / tileSize)),\r
- tileBounds = new L.Bounds(nwTilePoint, seTilePoint);\r
-\r
- this._addTilesFromCenterOut(tileBounds);\r
-\r
- if (this.options.unloadInvisibleTiles || this.options.reuseTiles) {\r
- this._removeOtherTiles(tileBounds);\r
- }\r
- },\r
-\r
- _addTilesFromCenterOut: function (bounds) {\r
- var queue = [],\r
- center = bounds.getCenter();\r
-\r
- var j, i, point;\r
-\r
- for (j = bounds.min.y; j <= bounds.max.y; j++) {\r
- for (i = bounds.min.x; i <= bounds.max.x; i++) {\r
- point = new L.Point(i, j);\r
-\r
- if (this._tileShouldBeLoaded(point)) {\r
- queue.push(point);\r
- }\r
- }\r
- }\r
-\r
- var tilesToLoad = queue.length;\r
-\r
- if (tilesToLoad === 0) { return; }\r
-\r
- // load tiles in order of their distance to center\r
- queue.sort(function (a, b) {\r
- return a.distanceTo(center) - b.distanceTo(center);\r
- });\r
-\r
- var fragment = document.createDocumentFragment();\r
-\r
- // if its the first batch of tiles to load\r
- if (!this._tilesToLoad) {\r
- this.fire('loading');\r
- }\r
-\r
- this._tilesToLoad += tilesToLoad;\r
-\r
- for (i = 0; i < tilesToLoad; i++) {\r
- this._addTile(queue[i], fragment);\r
- }\r
-\r
- this._container.appendChild(fragment);\r
- },\r
-\r
- _tileShouldBeLoaded: function (tilePoint) {\r
- if ((tilePoint.x + ':' + tilePoint.y) in this._tiles) {\r
- return false; // already loaded\r
- }\r
-\r
- if (!this.options.continuousWorld) {\r
- var limit = this._getWrapTileNum();\r
-\r
- if (this.options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit) ||\r
- tilePoint.y < 0 || tilePoint.y >= limit) {\r
- return false; // exceeds world bounds\r
- }\r
- }\r
-\r
- return true;\r
- },\r
-\r
- _removeOtherTiles: function (bounds) {\r
- var kArr, x, y, key;\r
-\r
- for (key in this._tiles) {\r
- if (this._tiles.hasOwnProperty(key)) {\r
- kArr = key.split(':');\r
- x = parseInt(kArr[0], 10);\r
- y = parseInt(kArr[1], 10);\r
-\r
- // remove tile if it's out of bounds\r
- if (x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) {\r
- this._removeTile(key);\r
- }\r
- }\r
- }\r
- },\r
-\r
- _removeTile: function (key) {\r
- var tile = this._tiles[key];\r
-\r
- this.fire("tileunload", {tile: tile, url: tile.src});\r
-\r
- if (this.options.reuseTiles) {\r
- L.DomUtil.removeClass(tile, 'leaflet-tile-loaded');\r
- this._unusedTiles.push(tile);\r
- } else if (tile.parentNode === this._container) {\r
- this._container.removeChild(tile);\r
- }\r
-\r
- if (!L.Browser.android) { //For https://github.com/CloudMade/Leaflet/issues/137\r
- tile.src = L.Util.emptyImageUrl;\r
- }\r
-\r
- delete this._tiles[key];\r
- },\r
-\r
- _addTile: function (tilePoint, container) {\r
- var tilePos = this._getTilePos(tilePoint);\r
-\r
- // get unused tile - or create a new tile\r
- var tile = this._getTile();\r
-\r
- // Chrome 20 layouts much faster with top/left (Verify with timeline, frames)\r
- // android 4 browser has display issues with top/left and requires transform instead\r
- // android 3 browser not tested\r
- // android 2 browser requires top/left or tiles disappear on load or first drag (reappear after zoom) https://github.com/CloudMade/Leaflet/issues/866\r
- // (other browsers don't currently care) - see debug/hacks/jitter.html for an example\r
- L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome || L.Browser.android23);\r
-\r
- this._tiles[tilePoint.x + ':' + tilePoint.y] = tile;\r
-\r
- this._loadTile(tile, tilePoint);\r
-\r
- if (tile.parentNode !== this._container) {\r
- container.appendChild(tile);\r
- }\r
- },\r
-\r
- _getZoomForUrl: function () {\r
-\r
- var options = this.options,\r
- zoom = this._map.getZoom();\r
-\r
- if (options.zoomReverse) {\r
- zoom = options.maxZoom - zoom;\r
- }\r
-\r
- return zoom + options.zoomOffset;\r
- },\r
-\r
- _getTilePos: function (tilePoint) {\r
- var origin = this._map.getPixelOrigin(),\r
- tileSize = this.options.tileSize;\r
-\r
- return tilePoint.multiplyBy(tileSize).subtract(origin);\r
- },\r
-\r
- // image-specific code (override to implement e.g. Canvas or SVG tile layer)\r
-\r
- getTileUrl: function (tilePoint) {\r
- this._adjustTilePoint(tilePoint);\r
-\r
- return L.Util.template(this._url, L.Util.extend({\r
- s: this._getSubdomain(tilePoint),\r
- z: this._getZoomForUrl(),\r
- x: tilePoint.x,\r
- y: tilePoint.y\r
- }, this.options));\r
- },\r
-\r
- _getWrapTileNum: function () {\r
- // TODO refactor, limit is not valid for non-standard projections\r
- return Math.pow(2, this._getZoomForUrl());\r
- },\r
-\r
- _adjustTilePoint: function (tilePoint) {\r
-\r
- var limit = this._getWrapTileNum();\r
-\r
- // wrap tile coordinates\r
- if (!this.options.continuousWorld && !this.options.noWrap) {\r
- tilePoint.x = ((tilePoint.x % limit) + limit) % limit;\r
- }\r
-\r
- if (this.options.tms) {\r
- tilePoint.y = limit - tilePoint.y - 1;\r
- }\r
- },\r
-\r
- _getSubdomain: function (tilePoint) {\r
- var index = (tilePoint.x + tilePoint.y) % this.options.subdomains.length;\r
- return this.options.subdomains[index];\r
- },\r
-\r
- _createTileProto: function () {\r
- var img = this._tileImg = L.DomUtil.create('img', 'leaflet-tile');\r
- img.galleryimg = 'no';\r
-\r
- var tileSize = this.options.tileSize;\r
- img.style.width = tileSize + 'px';\r
- img.style.height = tileSize + 'px';\r
- },\r
-\r
- _getTile: function () {\r
- if (this.options.reuseTiles && this._unusedTiles.length > 0) {\r
- var tile = this._unusedTiles.pop();\r
- this._resetTile(tile);\r
- return tile;\r
- }\r
- return this._createTile();\r
- },\r
-\r
- _resetTile: function (tile) {\r
- // Override if data stored on a tile needs to be cleaned up before reuse\r
- },\r
-\r
- _createTile: function () {\r
- var tile = this._tileImg.cloneNode(false);\r
- tile.onselectstart = tile.onmousemove = L.Util.falseFn;\r
- return tile;\r
- },\r
-\r
- _loadTile: function (tile, tilePoint) {\r
- tile._layer = this;\r
- tile.onload = this._tileOnLoad;\r
- tile.onerror = this._tileOnError;\r
-\r
- tile.src = this.getTileUrl(tilePoint);\r
- },\r
-\r
- _tileLoaded: function () {\r
- this._tilesToLoad--;\r
- if (!this._tilesToLoad) {\r
- this.fire('load');\r
- }\r
- },\r
-\r
- _tileOnLoad: function (e) {\r
- var layer = this._layer;\r
-\r
- //Only if we are loading an actual image\r
- if (this.src !== L.Util.emptyImageUrl) {\r
- L.DomUtil.addClass(this, 'leaflet-tile-loaded');\r
-\r
- layer.fire('tileload', {\r
- tile: this,\r
- url: this.src\r
- });\r
- }\r
-\r
- layer._tileLoaded();\r
- },\r
-\r
- _tileOnError: function (e) {\r
- var layer = this._layer;\r
-\r
- layer.fire('tileerror', {\r
- tile: this,\r
- url: this.src\r
- });\r
-\r
- var newUrl = layer.options.errorTileUrl;\r
- if (newUrl) {\r
- this.src = newUrl;\r
- }\r
-\r
- layer._tileLoaded();\r
- }\r
-});\r
-\r
-L.tileLayer = function (url, options) {\r
- return new L.TileLayer(url, options);\r
-};\r
-
-
-L.TileLayer.WMS = L.TileLayer.extend({\r
-\r
- defaultWmsParams: {\r
- service: 'WMS',\r
- request: 'GetMap',\r
- version: '1.1.1',\r
- layers: '',\r
- styles: '',\r
- format: 'image/jpeg',\r
- transparent: false\r
- },\r
-\r
- initialize: function (url, options) { // (String, Object)\r
-\r
- this._url = url;\r
-\r
- var wmsParams = L.Util.extend({}, this.defaultWmsParams);\r
-\r
- if (options.detectRetina && L.Browser.retina) {\r
- wmsParams.width = wmsParams.height = this.options.tileSize * 2;\r
- } else {\r
- wmsParams.width = wmsParams.height = this.options.tileSize;\r
- }\r
-\r
- for (var i in options) {\r
- // all keys that are not TileLayer options go to WMS params\r
- if (!this.options.hasOwnProperty(i)) {\r
- wmsParams[i] = options[i];\r
- }\r
- }\r
-\r
- this.wmsParams = wmsParams;\r
-\r
- L.Util.setOptions(this, options);\r
- },\r
-\r
- onAdd: function (map) {\r
-\r
- var projectionKey = parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs';\r
- this.wmsParams[projectionKey] = map.options.crs.code;\r
-\r
- L.TileLayer.prototype.onAdd.call(this, map);\r
- },\r
-\r
- getTileUrl: function (tilePoint, zoom) { // (Point, Number) -> String\r
-\r
- var map = this._map,\r
- crs = map.options.crs,\r
- tileSize = this.options.tileSize,\r
-\r
- nwPoint = tilePoint.multiplyBy(tileSize),\r
- sePoint = nwPoint.add(new L.Point(tileSize, tileSize)),\r
-\r
- nw = crs.project(map.unproject(nwPoint, zoom)),\r
- se = crs.project(map.unproject(sePoint, zoom)),\r
-\r
- bbox = [nw.x, se.y, se.x, nw.y].join(','),\r
-\r
- url = L.Util.template(this._url, {s: this._getSubdomain(tilePoint)});\r
-\r
- return url + L.Util.getParamString(this.wmsParams) + "&bbox=" + bbox;\r
- },\r
-\r
- setParams: function (params, noRedraw) {\r
-\r
- L.Util.extend(this.wmsParams, params);\r
-\r
- if (!noRedraw) {\r
- this.redraw();\r
- }\r
-\r
- return this;\r
- }\r
-});\r
-\r
-L.tileLayer.wms = function (url, options) {\r
- return new L.TileLayer.WMS(url, options);\r
-};\r
-
-
-L.TileLayer.Canvas = L.TileLayer.extend({\r
- options: {\r
- async: false\r
- },\r
-\r
- initialize: function (options) {\r
- L.Util.setOptions(this, options);\r
- },\r
-\r
- redraw: function () {\r
- var i,\r
- tiles = this._tiles;\r
-\r
- for (i in tiles) {\r
- if (tiles.hasOwnProperty(i)) {\r
- this._redrawTile(tiles[i]);\r
- }\r
- }\r
- },\r
-\r
- _redrawTile: function (tile) {\r
- this.drawTile(tile, tile._tilePoint, tile._zoom);\r
- },\r
-\r
- _createTileProto: function () {\r
- var proto = this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile');\r
-\r
- var tileSize = this.options.tileSize;\r
- proto.width = tileSize;\r
- proto.height = tileSize;\r
- },\r
-\r
- _createTile: function () {\r
- var tile = this._canvasProto.cloneNode(false);\r
- tile.onselectstart = tile.onmousemove = L.Util.falseFn;\r
- return tile;\r
- },\r
-\r
- _loadTile: function (tile, tilePoint, zoom) {\r
- tile._layer = this;\r
- tile._tilePoint = tilePoint;\r
- tile._zoom = zoom;\r
-\r
- this.drawTile(tile, tilePoint, zoom);\r
-\r
- if (!this.options.async) {\r
- this.tileDrawn(tile);\r
- }\r
- },\r
-\r
- drawTile: function (tile, tilePoint, zoom) {\r
- // override with rendering code\r
- },\r
-\r
- tileDrawn: function (tile) {\r
- this._tileOnLoad.call(tile);\r
- }\r
-});\r
-\r
-\r
-L.tileLayer.canvas = function (options) {\r
- return new L.TileLayer.Canvas(options);\r
+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
+if (typeof window !== 'undefined') {
+ expose();
+}
+
+
+
+/*
+ * @namespace Util
+ *
+ * Various utility functions, used by Leaflet internally.
+ */
+
+L.Util = {
+
+ // @function extend(dest: Object, src?: Object): Object
+ // Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut.
+ extend: function (dest) {
+ var i, j, len, src;
+
+ for (j = 1, len = arguments.length; j < len; j++) {
+ src = arguments[j];
+ for (i in src) {
+ dest[i] = src[i];
+ }
+ }
+ return dest;
+ },
+
+ // @function create(proto: Object, properties?: Object): Object
+ // Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create)
+ create: Object.create || (function () {
+ function F() {}
+ return function (proto) {
+ F.prototype = proto;
+ return new F();
+ };
+ })(),
+
+ // @function bind(fn: Function, …): Function
+ // Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind).
+ // Has a `L.bind()` shortcut.
+ 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);
+ };
+ },
+
+ // @function stamp(obj: Object): Number
+ // Returns the unique ID of an object, assiging it one if it doesn't have it.
+ stamp: function (obj) {
+ /*eslint-disable */
+ obj._leaflet_id = obj._leaflet_id || ++L.Util.lastId;
+ return obj._leaflet_id;
+ /*eslint-enable */
+ },
+
+ // @property lastId: Number
+ // Last unique ID used by [`stamp()`](#util-stamp)
+ lastId: 0,
+
+ // @function throttle(fn: Function, time: Number, context: Object): Function
+ // Returns a function which executes function `fn` with the given scope `context`
+ // (so that the `this` keyword refers to `context` inside `fn`'s code). The function
+ // `fn` will be called no more than one time per given amount of `time`. The arguments
+ // received by the bound function will be any arguments passed when binding the
+ // function, followed by any arguments passed when invoking the bound function.
+ // Has an `L.bind` shortcut.
+ 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;
+ },
+
+ // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number
+ // Returns the number `num` modulo `range` in such a way so it lies within
+ // `range[0]` and `range[1]`. The returned value will be always smaller than
+ // `range[1]` unless `includeMax` is set to `true`.
+ 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;
+ },
+
+ // @function falseFn(): Function
+ // Returns a function which always returns `false`.
+ falseFn: function () { return false; },
+
+ // @function formatNum(num: Number, digits?: Number): Number
+ // Returns the number `num` rounded to `digits` decimals, or to 5 decimals by default.
+ formatNum: function (num, digits) {
+ var pow = Math.pow(10, digits || 5);
+ return Math.round(num * pow) / pow;
+ },
+
+ // @function trim(str: String): String
+ // Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)
+ trim: function (str) {
+ return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
+ },
+
+ // @function splitWords(str: String): String[]
+ // Trims and splits the string on whitespace and returns the array of parts.
+ splitWords: function (str) {
+ return L.Util.trim(str).split(/\s+/);
+ },
+
+ // @function setOptions(obj: Object, options: Object): Object
+ // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut.
+ 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;
+ },
+
+ // @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String
+ // Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}`
+ // translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will
+ // be appended at the end. If `uppercase` is `true`, the parameter names will
+ // be uppercased (e.g. `'?A=foo&B=bar'`)
+ 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('&');
+ },
+
+ // @function template(str: String, data: Object): String
+ // Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'`
+ // and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string
+ // `('Hello foo, bar')`. You can also specify functions instead of strings for
+ // data values — they will be evaluated passing `data` as an argument.
+ 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,
+
+ // @function isArray(obj): Boolean
+ // Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)
+ isArray: Array.isArray || function (obj) {
+ return (Object.prototype.toString.call(obj) === '[object Array]');
+ },
+
+ // @function indexOf(array: Array, el: Object): Number
+ // Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf)
+ indexOf: function (array, el) {
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] === el) { return i; }
+ }
+ return -1;
+ },
+
+ // @property emptyImageUrl: String
+ // Data URI string containing a base64-encoded empty GIF image.
+ // Used as a hack to free memory from unused images on WebKit-powered
+ // mobile devices (by setting image `src` to this string).
+ 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); };
+
+
+ // @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number
+ // Schedules `fn` to be executed when the browser repaints. `fn` is bound to
+ // `context` if given. When `immediate` is set, `fn` is called immediately if
+ // the browser doesn't have native support for
+ // [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame),
+ // otherwise it's delayed. Returns a request ID that can be used to cancel the request.
+ L.Util.requestAnimFrame = function (fn, context, immediate) {
+ if (immediate && requestFn === timeoutDefer) {
+ fn.call(context);
+ } else {
+ return requestFn.call(window, L.bind(fn, context));
+ }
+ };
+
+ // @function cancelAnimFrame(id: Number): undefined
+ // Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame).
+ 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;
+
+
+
+
+// @class Class
+// @aka L.Class
+
+// @section
+// @uninheritable
+
+// Thanks to John Resig and Dean Edwards for inspiration!
+
+L.Class = function () {};
+
+L.Class.extend = function (props) {
+
+ // @function extend(props: Object): Function
+ // [Extends the current class](#class-inheritance) given the properties to be included.
+ // Returns a Javascript function that is a class constructor (to be called with `new`).
+ var NewClass = function () {
+
+ // call the constructor
+ if (this.initialize) {
+ this.initialize.apply(this, arguments);
+ }
+
+ // call all constructor hooks
+ this.callInitHooks();
+ };
+
+ 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;
+};
+
+
+// @function include(properties: Object): this
+// [Includes a mixin](#class-includes) into the current class.
+L.Class.include = function (props) {
+ L.extend(this.prototype, props);
+ return this;
+};
+
+// @function mergeOptions(options: Object): this
+// [Merges `options`](#class-options) into the defaults of the class.
+L.Class.mergeOptions = function (options) {
+ L.extend(this.prototype.options, options);
+ return this;
+};
+
+// @function addInitHook(fn: Function): this
+// Adds a [constructor hook](#class-constructor-hooks) to the class.
+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);
+ return this;
+};
+
+
+
+/*
+ * @class Evented
+ * @aka L.Evented
+ * @inherits Class
+ *
+ * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event).
+ *
+ * @example
+ *
+ * ```js
+ * map.on('click', function(e) {
+ * alert(e.latlng);
+ * } );
+ * ```
+ *
+ * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function:
+ *
+ * ```js
+ * function onClick(e) { ... }
+ *
+ * map.on('click', onClick);
+ * map.off('click', onClick);
+ * ```
+ */
+
+
+L.Evented = L.Class.extend({
+
+ /* @method on(type: String, fn: Function, context?: Object): this
+ * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`).
+ *
+ * @alternative
+ * @method on(eventMap: Object): this
+ * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+ */
+ 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;
+ },
+
+ /* @method off(type: String, fn?: Function, context?: Object): this
+ * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener.
+ *
+ * @alternative
+ * @method off(eventMap: Object): this
+ * Removes a set of type/listener pairs.
+ *
+ * @alternative
+ * @method off: this
+ * Removes all listeners to all events on the object.
+ */
+ 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) {
+ this._events = this._events || {};
+
+ /* get/init listeners for type */
+ var typeListeners = this._events[type];
+ if (!typeListeners) {
+ typeListeners = [];
+ this._events[type] = typeListeners;
+ }
+
+ if (context === this) {
+ // Less memory footprint.
+ context = undefined;
+ }
+ var newListener = {fn: fn, ctx: context},
+ listeners = typeListeners;
+
+ // check if fn already there
+ for (var i = 0, len = listeners.length; i < len; i++) {
+ if (listeners[i].fn === fn && listeners[i].ctx === context) {
+ return;
+ }
+ }
+
+ listeners.push(newListener);
+ },
+
+ _off: function (type, fn, context) {
+ var listeners,
+ i,
+ len;
+
+ if (!this._events) { return; }
+
+ listeners = this._events[type];
+
+ if (!listeners) {
+ return;
+ }
+
+ if (!fn) {
+ // Set all removed listeners to noop so they are not called if remove happens in fire
+ for (i = 0, len = listeners.length; i < len; i++) {
+ listeners[i].fn = L.Util.falseFn;
+ }
+ // clear all listeners for a type if function isn't specified
+ delete this._events[type];
+ return;
+ }
+
+ if (context === this) {
+ context = undefined;
+ }
+
+ if (listeners) {
+
+ // find fn and remove it
+ for (i = 0, len = listeners.length; i < len; i++) {
+ var l = listeners[i];
+ if (l.ctx !== context) { continue; }
+ if (l.fn === fn) {
+
+ // set the removed listener to noop so that's not called if remove happens in fire
+ l.fn = L.Util.falseFn;
+
+ if (this._firingCount) {
+ /* copy array in case events are being fired */
+ this._events[type] = listeners = listeners.slice();
+ }
+ listeners.splice(i, 1);
+
+ return;
+ }
+ }
+ }
+ },
+
+ // @method fire(type: String, data?: Object, propagate?: Boolean): this
+ // Fires an event of the specified type. You can optionally provide an data
+ // object — the first argument of the listener function will contain its
+ // properties. The event can optionally be propagated to event parents.
+ fire: function (type, data, propagate) {
+ if (!this.listens(type, propagate)) { return this; }
+
+ var event = L.Util.extend({}, data, {type: type, target: this});
+
+ if (this._events) {
+ var listeners = this._events[type];
+
+ if (listeners) {
+ this._firingCount = (this._firingCount + 1) || 1;
+ for (var i = 0, len = listeners.length; i < len; i++) {
+ var l = listeners[i];
+ l.fn.call(l.ctx || this, event);
+ }
+
+ this._firingCount--;
+ }
+ }
+
+ if (propagate) {
+ // propagate the event to parents (set with addEventParent)
+ this._propagateEvent(event);
+ }
+
+ return this;
+ },
+
+ // @method listens(type: String): Boolean
+ // Returns `true` if a particular event type has any listeners attached to it.
+ listens: function (type, propagate) {
+ var listeners = this._events && this._events[type];
+ if (listeners && listeners.length) { 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;
+ },
+
+ // @method once(…): this
+ // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed.
+ 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);
+ },
+
+ // @method addEventParent(obj: Evented): this
+ // Adds an event parent - an `Evented` that will receive propagated events
+ addEventParent: function (obj) {
+ this._eventParents = this._eventParents || {};
+ this._eventParents[L.stamp(obj)] = obj;
+ return this;
+ },
+
+ // @method removeEventParent(obj: Evented): this
+ // Removes an event parent, so it will stop receiving propagated events
+ 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
+
+// @method addEventListener(…): this
+// Alias to [`on(…)`](#evented-on)
+proto.addEventListener = proto.on;
+
+// @method removeEventListener(…): this
+// Alias to [`off(…)`](#evented-off)
+
+// @method clearAllEventListeners(…): this
+// Alias to [`off()`](#evented-off)
+proto.removeEventListener = proto.clearAllEventListeners = proto.off;
+
+// @method addOneTimeEventListener(…): this
+// Alias to [`once(…)`](#evented-once)
+proto.addOneTimeEventListener = proto.once;
+
+// @method fireEvent(…): this
+// Alias to [`fire(…)`](#evented-fire)
+proto.fireEvent = proto.fire;
+
+// @method hasEventListeners(…): Boolean
+// Alias to [`listens(…)`](#evented-listens)
+proto.hasEventListeners = proto.listens;
+
+L.Mixin = {Events: proto};
+
+
+
+/*
+ * @namespace Browser
+ * @aka L.Browser
+ *
+ * A namespace with static properties for browser/feature detection used by Leaflet internally.
+ *
+ * @example
+ *
+ * ```js
+ * if (L.Browser.ielt9) {
+ * alert('Upgrade your browser, dude!');
+ * }
+ * ```
+ */
+
+(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,
+ gecko = ua.indexOf('gecko') !== -1 && !webkit && !window.opera && !ie,
+
+ win = navigator.platform.indexOf('Win') === 0,
+
+ mobile = typeof orientation !== 'undefined' || ua.indexOf('mobile') !== -1,
+ msPointer = !window.PointerEvent && window.MSPointerEvent,
+ pointer = window.PointerEvent || msPointer,
+
+ ie3d = ie && ('transition' in doc.style),
+ webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23,
+ gecko3d = 'MozPerspective' in doc.style,
+ opera12 = 'OTransition' in doc.style;
+
+
+ var touch = !window.L_NO_TOUCH && (pointer || 'ontouchstart' in window ||
+ (window.DocumentTouch && document instanceof window.DocumentTouch));
+
+ L.Browser = {
+
+ // @property ie: Boolean
+ // `true` for all Internet Explorer versions (not Edge).
+ ie: ie,
+
+ // @property ielt9: Boolean
+ // `true` for Internet Explorer versions less than 9.
+ ielt9: ie && !document.addEventListener,
+
+ // @property edge: Boolean
+ // `true` for the Edge web browser.
+ edge: 'msLaunchUri' in navigator && !('documentMode' in document),
+
+ // @property webkit: Boolean
+ // `true` for webkit-based browsers like Chrome and Safari (including mobile versions).
+ webkit: webkit,
+
+ // @property gecko: Boolean
+ // `true` for gecko-based browsers like Firefox.
+ gecko: gecko,
+
+ // @property android: Boolean
+ // `true` for any browser running on an Android platform.
+ android: ua.indexOf('android') !== -1,
+
+ // @property android23: Boolean
+ // `true` for browsers running on Android 2 or Android 3.
+ android23: android23,
+
+ // @property chrome: Boolean
+ // `true` for the Chrome browser.
+ chrome: chrome,
+
+ // @property safari: Boolean
+ // `true` for the Safari browser.
+ safari: !chrome && ua.indexOf('safari') !== -1,
+
+
+ // @property win: Boolean
+ // `true` when the browser is running in a Windows platform
+ win: win,
+
+
+ // @property ie3d: Boolean
+ // `true` for all Internet Explorer versions supporting CSS transforms.
+ ie3d: ie3d,
+
+ // @property webkit3d: Boolean
+ // `true` for webkit-based browsers supporting CSS transforms.
+ webkit3d: webkit3d,
+
+ // @property gecko3d: Boolean
+ // `true` for gecko-based browsers supporting CSS transforms.
+ gecko3d: gecko3d,
+
+ // @property opera12: Boolean
+ // `true` for the Opera browser supporting CSS transforms (version 12 or later).
+ opera12: opera12,
+
+ // @property any3d: Boolean
+ // `true` for all browsers supporting CSS transforms.
+ any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantomjs,
+
+
+ // @property mobile: Boolean
+ // `true` for all browsers running in a mobile device.
+ mobile: mobile,
+
+ // @property mobileWebkit: Boolean
+ // `true` for all webkit-based browsers in a mobile device.
+ mobileWebkit: mobile && webkit,
+
+ // @property mobileWebkit3d: Boolean
+ // `true` for all webkit-based browsers in a mobile device supporting CSS transforms.
+ mobileWebkit3d: mobile && webkit3d,
+
+ // @property mobileOpera: Boolean
+ // `true` for the Opera browser in a mobile device.
+ mobileOpera: mobile && window.opera,
+
+ // @property mobileGecko: Boolean
+ // `true` for gecko-based browsers running in a mobile device.
+ mobileGecko: mobile && gecko,
+
+
+ // @property touch: Boolean
+ // `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events).
+ // This does not necessarily mean that the browser is running in a computer with
+ // a touchscreen, it only means that the browser is capable of understanding
+ // touch events.
+ touch: !!touch,
+
+ // @property msPointer: Boolean
+ // `true` for browsers implementing the Microsoft touch events model (notably IE10).
+ msPointer: !!msPointer,
+
+ // @property pointer: Boolean
+ // `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx).
+ pointer: !!pointer,
+
+
+ // @property retina: Boolean
+ // `true` for browsers on a high-resolution "retina" screen.
+ retina: (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1
+ };
+
+}());
+
+
+
+/*
+ * @class Point
+ * @aka L.Point
+ *
+ * Represents a point with `x` and `y` coordinates in pixels.
+ *
+ * @example
+ *
+ * ```js
+ * var point = L.point(200, 300);
+ * ```
+ *
+ * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent:
+ *
+ * ```js
+ * map.panBy([200, 300]);
+ * map.panBy(L.point(200, 300));
+ * ```
+ */
+
+L.Point = function (x, y, round) {
+ // @property x: Number; The `x` coordinate of the point
+ this.x = (round ? Math.round(x) : x);
+ // @property y: Number; The `y` coordinate of the point
+ this.y = (round ? Math.round(y) : y);
+};
+
+L.Point.prototype = {
+
+ // @method clone(): Point
+ // Returns a copy of the current point.
+ clone: function () {
+ return new L.Point(this.x, this.y);
+ },
+
+ // @method add(otherPoint: Point): Point
+ // Returns the result of addition of the current and the given points.
+ add: function (point) {
+ // non-destructive, returns a new point
+ return this.clone()._add(L.point(point));
+ },
+
+ _add: function (point) {
+ // destructive, used directly for performance in situations where it's safe to modify existing point
+ this.x += point.x;
+ this.y += point.y;
+ return this;
+ },
+
+ // @method subtract(otherPoint: Point): Point
+ // Returns the result of subtraction of the given point from the current.
+ subtract: function (point) {
+ return this.clone()._subtract(L.point(point));
+ },
+
+ _subtract: function (point) {
+ this.x -= point.x;
+ this.y -= point.y;
+ return this;
+ },
+
+ // @method divideBy(num: Number): Point
+ // Returns the result of division of the current point by the given number.
+ divideBy: function (num) {
+ return this.clone()._divideBy(num);
+ },
+
+ _divideBy: function (num) {
+ this.x /= num;
+ this.y /= num;
+ return this;
+ },
+
+ // @method multiplyBy(num: Number): Point
+ // Returns the result of multiplication of the current point by the given number.
+ multiplyBy: function (num) {
+ return this.clone()._multiplyBy(num);
+ },
+
+ _multiplyBy: function (num) {
+ this.x *= num;
+ this.y *= num;
+ return this;
+ },
+
+ // @method scaleBy(scale: Point): Point
+ // Multiply each coordinate of the current point by each coordinate of
+ // `scale`. In linear algebra terms, multiply the point by the
+ // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation)
+ // defined by `scale`.
+ scaleBy: function (point) {
+ return new L.Point(this.x * point.x, this.y * point.y);
+ },
+
+ // @method unscaleBy(scale: Point): Point
+ // Inverse of `scaleBy`. Divide each coordinate of the current point by
+ // each coordinate of `scale`.
+ unscaleBy: function (point) {
+ return new L.Point(this.x / point.x, this.y / point.y);
+ },
+
+ // @method round(): Point
+ // Returns a copy of the current point with rounded coordinates.
+ round: function () {
+ return this.clone()._round();
+ },
+
+ _round: function () {
+ this.x = Math.round(this.x);
+ this.y = Math.round(this.y);
+ return this;
+ },
+
+ // @method floor(): Point
+ // Returns a copy of the current point with floored coordinates (rounded down).
+ floor: function () {
+ return this.clone()._floor();
+ },
+
+ _floor: function () {
+ this.x = Math.floor(this.x);
+ this.y = Math.floor(this.y);
+ return this;
+ },
+
+ // @method ceil(): Point
+ // Returns a copy of the current point with ceiled coordinates (rounded up).
+ ceil: function () {
+ return this.clone()._ceil();
+ },
+
+ _ceil: function () {
+ this.x = Math.ceil(this.x);
+ this.y = Math.ceil(this.y);
+ return this;
+ },
+
+ // @method distanceTo(otherPoint: Point): Number
+ // Returns the cartesian distance between the current and the given points.
+ 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);
+ },
+
+ // @method equals(otherPoint: Point): Boolean
+ // Returns `true` if the given point has the same coordinates.
+ equals: function (point) {
+ point = L.point(point);
+
+ return point.x === this.x &&
+ point.y === this.y;
+ },
+
+ // @method contains(otherPoint: Point): Boolean
+ // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values).
+ contains: function (point) {
+ point = L.point(point);
+
+ return Math.abs(point.x) <= Math.abs(this.x) &&
+ Math.abs(point.y) <= Math.abs(this.y);
+ },
+
+ // @method toString(): String
+ // Returns a string representation of the point for debugging purposes.
+ toString: function () {
+ return 'Point(' +
+ L.Util.formatNum(this.x) + ', ' +
+ L.Util.formatNum(this.y) + ')';
+ }
+};
+
+// @factory L.point(x: Number, y: Number, round?: Boolean)
+// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values.
+
+// @alternative
+// @factory L.point(coords: Number[])
+// Expects an array of the form `[x, y]` instead.
+
+// @alternative
+// @factory L.point(coords: Object)
+// Expects a plain object of the form `{x: Number, y: Number}` instead.
+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;
+ }
+ if (typeof x === 'object' && 'x' in x && 'y' in x) {
+ return new L.Point(x.x, x.y);
+ }
+ return new L.Point(x, y, round);
+};
+
+
+
+/*
+ * @class Bounds
+ * @aka L.Bounds
+ *
+ * Represents a rectangular area in pixel coordinates.
+ *
+ * @example
+ *
+ * ```js
+ * var p1 = L.point(10, 10),
+ * p2 = L.point(40, 60),
+ * bounds = L.bounds(p1, p2);
+ * ```
+ *
+ * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
+ *
+ * ```js
+ * otherBounds.intersects([[10, 10], [40, 60]]);
+ * ```
+ */
+
+L.Bounds = function (a, b) {
+ 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 = {
+ // @method extend(point: Point): this
+ // Extends the bounds to contain the given point.
+ extend: function (point) { // (Point)
+ point = L.point(point);
+
+ // @property min: Point
+ // The top left corner of the rectangle.
+ // @property max: Point
+ // The bottom right corner of the rectangle.
+ 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;
+ },
+
+ // @method getCenter(round?: Boolean): Point
+ // Returns the center point of the bounds.
+ getCenter: function (round) {
+ return new L.Point(
+ (this.min.x + this.max.x) / 2,
+ (this.min.y + this.max.y) / 2, round);
+ },
+
+ // @method getBottomLeft(): Point
+ // Returns the bottom-left point of the bounds.
+ getBottomLeft: function () {
+ return new L.Point(this.min.x, this.max.y);
+ },
+
+ // @method getTopRight(): Point
+ // Returns the top-right point of the bounds.
+ getTopRight: function () { // -> Point
+ return new L.Point(this.max.x, this.min.y);
+ },
+
+ // @method getSize(): Point
+ // Returns the size of the given bounds
+ getSize: function () {
+ return this.max.subtract(this.min);
+ },
+
+ // @method contains(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle contains the given one.
+ // @alternative
+ // @method contains(point: Point): Boolean
+ // Returns `true` if the rectangle contains the given point.
+ contains: function (obj) {
+ 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);
+ },
+
+ // @method intersects(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle intersects the given bounds. Two bounds
+ // intersect if they have at least one point in common.
+ 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;
+ },
+
+ // @method overlaps(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle overlaps the given bounds. Two bounds
+ // overlap if their intersection is an area.
+ overlaps: function (bounds) { // (Bounds) -> Boolean
+ bounds = L.bounds(bounds);
+
+ var min = this.min,
+ max = this.max,
+ min2 = bounds.min,
+ max2 = bounds.max,
+ xOverlaps = (max2.x > min.x) && (min2.x < max.x),
+ yOverlaps = (max2.y > min.y) && (min2.y < max.y);
+
+ return xOverlaps && yOverlaps;
+ },
+
+ isValid: function () {
+ return !!(this.min && this.max);
+ }
+};
+
+
+// @factory L.bounds(topLeft: Point, bottomRight: Point)
+// Creates a Bounds object from two coordinates (usually top-left and bottom-right corners).
+// @alternative
+// @factory L.bounds(points: Point[])
+// Creates a Bounds object from the points it contains
+L.bounds = function (a, b) {
+ if (!a || a instanceof L.Bounds) {
+ return a;
+ }
+ return new L.Bounds(a, b);
+};
+
+
+
+/*
+ * @class Transformation
+ * @aka L.Transformation
+ *
+ * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d`
+ * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing
+ * the reverse. Used by Leaflet in its projections code.
+ *
+ * @example
+ *
+ * ```js
+ * var transformation = new L.Transformation(2, 5, -1, 10),
+ * p = L.point(1, 2),
+ * p2 = transformation.transform(p), // L.point(7, 8)
+ * p3 = transformation.untransform(p2); // L.point(1, 2)
+ * ```
+ */
+
+
+// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number)
+// Creates a `Transformation` object with the given coefficients.
+L.Transformation = function (a, b, c, d) {
+ this._a = a;
+ this._b = b;
+ this._c = c;
+ this._d = d;
+};
+
+L.Transformation.prototype = {
+ // @method transform(point: Point, scale?: Number): Point
+ // Returns a transformed point, optionally multiplied by the given scale.
+ // Only accepts actual `L.Point` instances, not arrays.
+ 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;
+ },
+
+ // @method untransform(point: Point, scale?: Number): Point
+ // Returns the reverse transformation of the given point, optionally divided
+ // by the given scale. Only accepts actual `L.Point` instances, not arrays.
+ 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);
+ }
+};
+
+
+
+/*
+ * @namespace DomUtil
+ *
+ * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model)
+ * tree, used by Leaflet internally.
+ *
+ * Most functions expecting or returning a `HTMLElement` also work for
+ * SVG elements. The only difference is that classes refer to CSS classes
+ * in HTML and SVG classes in SVG.
+ */
+
+L.DomUtil = {
+
+ // @function get(id: String|HTMLElement): HTMLElement
+ // Returns an element given its DOM id, or returns the element itself
+ // if it was passed directly.
+ get: function (id) {
+ return typeof id === 'string' ? document.getElementById(id) : id;
+ },
+
+ // @function getStyle(el: HTMLElement, styleAttrib: String): String
+ // Returns the value for a certain style attribute on an element,
+ // including computed values or values set through CSS.
+ 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;
+ },
+
+ // @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement
+ // Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element.
+ create: function (tagName, className, container) {
+
+ var el = document.createElement(tagName);
+ el.className = className || '';
+
+ if (container) {
+ container.appendChild(el);
+ }
+
+ return el;
+ },
+
+ // @function remove(el: HTMLElement)
+ // Removes `el` from its parent element
+ remove: function (el) {
+ var parent = el.parentNode;
+ if (parent) {
+ parent.removeChild(el);
+ }
+ },
+
+ // @function empty(el: HTMLElement)
+ // Removes all of `el`'s children elements from `el`
+ empty: function (el) {
+ while (el.firstChild) {
+ el.removeChild(el.firstChild);
+ }
+ },
+
+ // @function toFront(el: HTMLElement)
+ // Makes `el` the last children of its parent, so it renders in front of the other children.
+ toFront: function (el) {
+ el.parentNode.appendChild(el);
+ },
+
+ // @function toBack(el: HTMLElement)
+ // Makes `el` the first children of its parent, so it renders back from the other children.
+ toBack: function (el) {
+ var parent = el.parentNode;
+ parent.insertBefore(el, parent.firstChild);
+ },
+
+ // @function hasClass(el: HTMLElement, name: String): Boolean
+ // Returns `true` if the element's class attribute contains `name`.
+ 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);
+ },
+
+ // @function addClass(el: HTMLElement, name: String)
+ // Adds `name` to the element's class attribute.
+ 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);
+ }
+ },
+
+ // @function removeClass(el: HTMLElement, name: String)
+ // Removes `name` from the element's class attribute.
+ 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 + ' ', ' ')));
+ }
+ },
+
+ // @function setClass(el: HTMLElement, name: String)
+ // Sets the element's class.
+ setClass: function (el, name) {
+ if (el.className.baseVal === undefined) {
+ el.className = name;
+ } else {
+ // in case of SVG element
+ el.className.baseVal = name;
+ }
+ },
+
+ // @function getClass(el: HTMLElement): String
+ // Returns the element's class.
+ getClass: function (el) {
+ return el.className.baseVal === undefined ? el.className : el.className.baseVal;
+ },
+
+ // @function setOpacity(el: HTMLElement, opacity: Number)
+ // Set the opacity of an element (including old IE support).
+ // `opacity` must be a number from `0` to `1`.
+ setOpacity: function (el, value) {
+
+ if ('opacity' in el.style) {
+ el.style.opacity = value;
+
+ } else if ('filter' in el.style) {
+ L.DomUtil._setOpacityIE(el, value);
+ }
+ },
+
+ _setOpacityIE: function (el, value) {
+ 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 + ')';
+ }
+ },
+
+ // @function testProp(props: String[]): String|false
+ // Goes through the array of style names and returns the first name
+ // that is a valid style name for an element. If no such name is found,
+ // it returns false. Useful for vendor-prefixed styles like `transform`.
+ 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;
+ },
+
+ // @function setTransform(el: HTMLElement, offset: Point, scale?: Number)
+ // Resets the 3D CSS transform of `el` so it is translated by `offset` pixels
+ // and optionally scaled by `scale`. Does not have an effect if the
+ // browser doesn't support 3D CSS transforms.
+ setTransform: function (el, offset, scale) {
+ var pos = offset || new L.Point(0, 0);
+
+ el.style[L.DomUtil.TRANSFORM] =
+ (L.Browser.ie3d ?
+ 'translate(' + pos.x + 'px,' + pos.y + 'px)' :
+ 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') +
+ (scale ? ' scale(' + scale + ')' : '');
+ },
+
+ // @function setPosition(el: HTMLElement, position: Point)
+ // Sets the position of `el` to coordinates specified by `position`,
+ // using CSS translate or top/left positioning depending on the browser
+ // (used by Leaflet internally to position its layers).
+ setPosition: function (el, point) { // (HTMLElement, Point[, Boolean])
+
+ /*eslint-disable */
+ el._leaflet_pos = point;
+ /*eslint-enable */
+
+ if (L.Browser.any3d) {
+ L.DomUtil.setTransform(el, point);
+ } else {
+ el.style.left = point.x + 'px';
+ el.style.top = point.y + 'px';
+ }
+ },
+
+ // @function getPosition(el: HTMLElement): Point
+ // Returns the coordinates of an element previously positioned with setPosition.
+ getPosition: function (el) {
+ // this method is only used for elements previously positioned using setPosition,
+ // so it's safe to cache the position for performance
+
+ return el._leaflet_pos || new L.Point(0, 0);
+ }
+};
+
+
+(function () {
+ // prefix style property names
+
+ // @property TRANSFORM: String
+ // Vendor-prefixed fransform style name (e.g. `'webkitTransform'` for WebKit).
+ 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
+
+ // @property TRANSITION: String
+ // Vendor-prefixed transform style name.
+ var transition = L.DomUtil.TRANSITION = L.DomUtil.testProp(
+ ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']);
+
+ L.DomUtil.TRANSITION_END =
+ transition === 'webkitTransition' || transition === 'OTransition' ? transition + 'End' : 'transitionend';
+
+ // @function disableTextSelection()
+ // Prevents the user from generating `selectstart` DOM events, usually generated
+ // when the user drags the mouse through a page with text. Used internally
+ // by Leaflet to override the behaviour of any click-and-drag interaction on
+ // the map. Affects drag interactions on the whole document.
+
+ // @function enableTextSelection()
+ // Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection).
+ 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;
+ }
+ };
+ }
+
+ // @function disableImageDrag()
+ // As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but
+ // for `dragstart` DOM events, usually generated when the user drags an image.
+ L.DomUtil.disableImageDrag = function () {
+ L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault);
+ };
+
+ // @function enableImageDrag()
+ // Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection).
+ L.DomUtil.enableImageDrag = function () {
+ L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault);
+ };
+
+ // @function preventOutline(el: HTMLElement)
+ // Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline)
+ // of the element `el` invisible. Used internally by Leaflet to prevent
+ // focusable elements from displaying an outline when the user performs a
+ // drag interaction on them.
+ L.DomUtil.preventOutline = function (element) {
+ while (element.tabIndex === -1) {
+ element = element.parentNode;
+ }
+ if (!element || !element.style) { return; }
+ L.DomUtil.restoreOutline();
+ this._outlineElement = element;
+ this._outlineStyle = element.style.outline;
+ element.style.outline = 'none';
+ L.DomEvent.on(window, 'keydown', L.DomUtil.restoreOutline, this);
+ };
+
+ // @function restoreOutline()
+ // Cancels the effects of a previous [`L.DomUtil.preventOutline`]().
+ L.DomUtil.restoreOutline = function () {
+ if (!this._outlineElement) { return; }
+ this._outlineElement.style.outline = this._outlineStyle;
+ delete this._outlineElement;
+ delete this._outlineStyle;
+ L.DomEvent.off(window, 'keydown', L.DomUtil.restoreOutline, this);
+ };
+})();
+
+
+
+/* @class LatLng
+ * @aka L.LatLng
+ *
+ * Represents a geographical point with a certain latitude and longitude.
+ *
+ * @example
+ *
+ * ```
+ * var latlng = L.latLng(50.5, 30.5);
+ * ```
+ *
+ * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent:
+ *
+ * ```
+ * map.panTo([50, 30]);
+ * map.panTo({lon: 30, lat: 50});
+ * map.panTo({lat: 50, lng: 30});
+ * map.panTo(L.latLng(50, 30));
+ * ```
+ */
+
+L.LatLng = function (lat, lng, alt) {
+ if (isNaN(lat) || isNaN(lng)) {
+ throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
+ }
+
+ // @property lat: Number
+ // Latitude in degrees
+ this.lat = +lat;
+
+ // @property lng: Number
+ // Longitude in degrees
+ this.lng = +lng;
+
+ // @property alt: Number
+ // Altitude in meters (optional)
+ if (alt !== undefined) {
+ this.alt = +alt;
+ }
+};
+
+L.LatLng.prototype = {
+ // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean
+ // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overriden by setting `maxMargin` to a small number.
+ 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);
+ },
+
+ // @method toString(): String
+ // Returns a string representation of the point (for debugging purposes).
+ toString: function (precision) {
+ return 'LatLng(' +
+ L.Util.formatNum(this.lat, precision) + ', ' +
+ L.Util.formatNum(this.lng, precision) + ')';
+ },
+
+ // @method distanceTo(otherLatLng: LatLng): Number
+ // Returns the distance (in meters) to the given `LatLng` calculated using the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula).
+ distanceTo: function (other) {
+ return L.CRS.Earth.distance(this, L.latLng(other));
+ },
+
+ // @method wrap(): LatLng
+ // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees.
+ wrap: function () {
+ return L.CRS.Earth.wrapLatLng(this);
+ },
+
+ // @method toBounds(sizeInMeters: Number): LatLngBounds
+ // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`.
+ toBounds: function (sizeInMeters) {
+ var latAccuracy = 180 * sizeInMeters / 40075017,
+ lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
+
+ return L.latLngBounds(
+ [this.lat - latAccuracy, this.lng - lngAccuracy],
+ [this.lat + latAccuracy, this.lng + lngAccuracy]);
+ },
+
+ clone: function () {
+ return new L.LatLng(this.lat, this.lng, this.alt);
+ }
+};
+
+
+
+// @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng
+// Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude).
+
+// @alternative
+// @factory L.latLng(coords: Array): LatLng
+// Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead.
+
+// @alternative
+// @factory L.latLng(coords: Object): LatLng
+// Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead.
+
+L.latLng = function (a, b, c) {
+ 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]);
+ }
+ if (a.length === 2) {
+ return new L.LatLng(a[0], a[1]);
+ }
+ return null;
+ }
+ 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, a.alt);
+ }
+ if (b === undefined) {
+ return null;
+ }
+ return new L.LatLng(a, b, c);
+};
+
+
+
+/*
+ * @class LatLngBounds
+ * @aka L.LatLngBounds
+ *
+ * Represents a rectangular geographical area on a map.
+ *
+ * @example
+ *
+ * ```js
+ * var corner1 = L.latLng(40.712, -74.227),
+ * corner2 = L.latLng(40.774, -74.125),
+ * bounds = L.latLngBounds(corner1, corner2);
+ * ```
+ *
+ * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
+ *
+ * ```js
+ * map.fitBounds([
+ * [40.712, -74.227],
+ * [40.774, -74.125]
+ * ]);
+ * ```
+ *
+ * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range.
+ */
+
+L.LatLngBounds = function (corner1, corner2) { // (LatLng, LatLng) or (LatLng[])
+ if (!corner1) { return; }
+
+ var latlngs = corner2 ? [corner1, corner2] : corner1;
+
+ for (var i = 0, len = latlngs.length; i < len; i++) {
+ this.extend(latlngs[i]);
+ }
+};
+
+L.LatLngBounds.prototype = {
+
+ // @method extend(latlng: LatLng): this
+ // Extend the bounds to contain the given point
+
+ // @alternative
+ // @method extend(otherBounds: LatLngBounds): this
+ // Extend the bounds to contain the given bounds
+ extend: function (obj) {
+ 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;
+ },
+
+ // @method pad(bufferRatio: Number): LatLngBounds
+ // Returns bigger bounds created by extending the current bounds by a given percentage in each direction.
+ pad: function (bufferRatio) {
+ 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));
+ },
+
+ // @method getCenter(): LatLng
+ // Returns the center point of the bounds.
+ getCenter: function () {
+ return new L.LatLng(
+ (this._southWest.lat + this._northEast.lat) / 2,
+ (this._southWest.lng + this._northEast.lng) / 2);
+ },
+
+ // @method getSouthWest(): LatLng
+ // Returns the south-west point of the bounds.
+ getSouthWest: function () {
+ return this._southWest;
+ },
+
+ // @method getNorthEast(): LatLng
+ // Returns the north-east point of the bounds.
+ getNorthEast: function () {
+ return this._northEast;
+ },
+
+ // @method getNorthWest(): LatLng
+ // Returns the north-west point of the bounds.
+ getNorthWest: function () {
+ return new L.LatLng(this.getNorth(), this.getWest());
+ },
+
+ // @method getSouthEast(): LatLng
+ // Returns the south-east point of the bounds.
+ getSouthEast: function () {
+ return new L.LatLng(this.getSouth(), this.getEast());
+ },
+
+ // @method getWest(): Number
+ // Returns the west longitude of the bounds
+ getWest: function () {
+ return this._southWest.lng;
+ },
+
+ // @method getSouth(): Number
+ // Returns the south latitude of the bounds
+ getSouth: function () {
+ return this._southWest.lat;
+ },
+
+ // @method getEast(): Number
+ // Returns the east longitude of the bounds
+ getEast: function () {
+ return this._northEast.lng;
+ },
+
+ // @method getNorth(): Number
+ // Returns the north latitude of the bounds
+ getNorth: function () {
+ return this._northEast.lat;
+ },
+
+ // @method contains(otherBounds: LatLngBounds): Boolean
+ // Returns `true` if the rectangle contains the given one.
+
+ // @alternative
+ // @method contains (latlng: LatLng): Boolean
+ // Returns `true` if the rectangle contains the given point.
+ contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
+ if (typeof obj[0] === 'number' || obj instanceof L.LatLng || 'lat' in obj) {
+ 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);
+ },
+
+ // @method intersects(otherBounds: LatLngBounds): Boolean
+ // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common.
+ intersects: function (bounds) {
+ 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;
+ },
+
+ // @method overlaps(otherBounds: Bounds): Boolean
+ // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area.
+ overlaps: function (bounds) {
+ bounds = L.latLngBounds(bounds);
+
+ var sw = this._southWest,
+ ne = this._northEast,
+ sw2 = bounds.getSouthWest(),
+ ne2 = bounds.getNorthEast(),
+
+ latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat),
+ lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng);
+
+ return latOverlaps && lngOverlaps;
+ },
+
+ // @method toBBoxString(): String
+ // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data.
+ toBBoxString: function () {
+ return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');
+ },
+
+ // @method equals(otherBounds: LatLngBounds): Boolean
+ // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds.
+ equals: function (bounds) {
+ if (!bounds) { return false; }
+
+ bounds = L.latLngBounds(bounds);
+
+ return this._southWest.equals(bounds.getSouthWest()) &&
+ this._northEast.equals(bounds.getNorthEast());
+ },
+
+ // @method isValid(): Boolean
+ // Returns `true` if the bounds are properly initialized.
+ isValid: function () {
+ return !!(this._southWest && this._northEast);
+ }
+};
+
+// TODO International date line?
+
+// @factory L.latLngBounds(corner1: LatLng, corner2: LatLng)
+// Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle.
+
+// @alternative
+// @factory L.latLngBounds(latlngs: LatLng[])
+// Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds).
+L.latLngBounds = function (a, b) {
+ if (a instanceof L.LatLngBounds) {
+ return a;
+ }
+ return new L.LatLngBounds(a, b);
+};
+
+
+
+/*
+ * @namespace Projection
+ * @section
+ * Leaflet comes with a set of already defined Projections out of the box:
+ *
+ * @projection L.Projection.LonLat
+ *
+ * Equirectangular, or Plate Carree projection — the most simple projection,
+ * mostly used by GIS enthusiasts. Directly maps `x` as longitude, and `y` as
+ * latitude. Also suitable for flat worlds, e.g. game maps. Used by the
+ * `EPSG:3395` and `Simple` CRS.
+ */
+
+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])
+};
+
+
+
+/*
+ * @namespace Projection
+ * @projection L.Projection.SphericalMercator
+ *
+ * Spherical Mercator projection — the most common projection for online maps,
+ * used by almost all free and commercial tile providers. Assumes that Earth is
+ * a sphere. Used by the `EPSG:3857` CRS.
+ */
+
+L.Projection.SphericalMercator = {
+
+ R: 6378137,
+ MAX_LATITUDE: 85.0511287798,
+
+ project: function (latlng) {
+ var d = Math.PI / 180,
+ max = this.MAX_LATITUDE,
+ lat = Math.max(Math.min(max, latlng.lat), -max),
+ sin = Math.sin(lat * d);
+
+ 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]);
+ })()
+};
+
+
+
+/*
+ * @class CRS
+ * @aka L.CRS
+ * Abstract class that defines coordinate reference systems for projecting
+ * geographical points into pixel (screen) coordinates and back (and to
+ * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See
+ * [spatial reference system](http://en.wikipedia.org/wiki/Coordinate_reference_system).
+ *
+ * Leaflet defines the most usual CRSs by default. If you want to use a
+ * CRS not defined by default, take a look at the
+ * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin.
+ */
+
+L.CRS = {
+ // @method latLngToPoint(latlng: LatLng, zoom: Number): Point
+ // Projects geographical coordinates into pixel coordinates for a given zoom.
+ latLngToPoint: function (latlng, zoom) {
+ var projectedPoint = this.projection.project(latlng),
+ scale = this.scale(zoom);
+
+ return this.transformation._transform(projectedPoint, scale);
+ },
+
+ // @method pointToLatLng(point: Point, zoom: Number): LatLng
+ // The inverse of `latLngToPoint`. Projects pixel coordinates on a given
+ // zoom into geographical coordinates.
+ pointToLatLng: function (point, zoom) {
+ var scale = this.scale(zoom),
+ untransformedPoint = this.transformation.untransform(point, scale);
+
+ return this.projection.unproject(untransformedPoint);
+ },
+
+ // @method project(latlng: LatLng): Point
+ // Projects geographical coordinates into coordinates in units accepted for
+ // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services).
+ project: function (latlng) {
+ return this.projection.project(latlng);
+ },
+
+ // @method unproject(point: Point): LatLng
+ // Given a projected coordinate returns the corresponding LatLng.
+ // The inverse of `project`.
+ unproject: function (point) {
+ return this.projection.unproject(point);
+ },
+
+ // @method scale(zoom: Number): Number
+ // Returns the scale used when transforming projected coordinates into
+ // pixel coordinates for a particular zoom. For example, it returns
+ // `256 * 2^zoom` for Mercator-based CRS.
+ scale: function (zoom) {
+ return 256 * Math.pow(2, zoom);
+ },
+
+ // @method zoom(scale: Number): Number
+ // Inverse of `scale()`, returns the zoom level corresponding to a scale
+ // factor of `scale`.
+ zoom: function (scale) {
+ return Math.log(scale / 256) / Math.LN2;
+ },
+
+ // @method getProjectedBounds(zoom: Number): Bounds
+ // Returns the projection's bounds scaled and transformed for the provided `zoom`.
+ 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);
+ },
+
+ // @method distance(latlng1: LatLng, latlng2: LatLng): Number
+ // Returns the distance between two geographical coordinates.
+
+ // @property code: String
+ // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`)
+ //
+ // @property wrapLng: Number[]
+ // An array of two numbers defining whether the longitude (horizontal) coordinate
+ // axis wraps around a given range and how. Defaults to `[-180, 180]` in most
+ // geographical CRSs. If `undefined`, the longitude axis does not wrap around.
+ //
+ // @property wrapLat: Number[]
+ // Like `wrapLng`, but for the latitude (vertical) axis.
+
+ // wrapLng: [min, max],
+ // wrapLat: [min, max],
+
+ // @property infinite: Boolean
+ // If true, the coordinate space will be unbounded (infinite in both axes)
+ infinite: false,
+
+ // @method wrapLatLng(latlng: LatLng): LatLng
+ // Returns a `LatLng` where lat and lng has been wrapped according to the
+ // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds.
+ // Only accepts actual `L.LatLng` instances, not arrays.
+ 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,
+ alt = latlng.alt;
+
+ return L.latLng(lat, lng, alt);
+ },
+
+ // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
+ // Returns a `LatLngBounds` with the same size as the given one, ensuring
+ // that its center is within the CRS's bounds.
+ // Only accepts actual `L.LatLngBounds` instances, not arrays.
+ wrapLatLngBounds: function (bounds) {
+ var center = bounds.getCenter(),
+ newCenter = this.wrapLatLng(center),
+ latShift = center.lat - newCenter.lat,
+ lngShift = center.lng - newCenter.lng;
+
+ if (latShift === 0 && lngShift === 0) {
+ return bounds;
+ }
+
+ var sw = bounds.getSouthWest(),
+ ne = bounds.getNorthEast(),
+ newSw = L.latLng({lat: sw.lat - latShift, lng: sw.lng - lngShift}),
+ newNe = L.latLng({lat: ne.lat - latShift, lng: ne.lng - lngShift});
+
+ return new L.LatLngBounds(newSw, newNe);
+ }
+};
+
+
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.Simple
+ *
+ * A simple CRS that maps longitude and latitude into `x` and `y` directly.
+ * May be used for maps of flat surfaces (e.g. game maps). Note that the `y`
+ * axis should still be inverted (going from bottom to top). `distance()` returns
+ * simple euclidean distance.
+ */
+
+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);
+ },
+
+ zoom: function (scale) {
+ return Math.log(scale) / Math.LN2;
+ },
+
+ distance: function (latlng1, latlng2) {
+ var dx = latlng2.lng - latlng1.lng,
+ dy = latlng2.lat - latlng1.lat;
+
+ return Math.sqrt(dx * dx + dy * dy);
+ },
+
+ infinite: true
+});
+
+
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.Earth
+ *
+ * Serves as the base for CRS that are global such that they cover the earth.
+ * Can only be used as the base for other CRS and cannot be used directly,
+ * since it does not have a `code`, `projection` or `transformation`. `distance()` returns
+ * meters.
+ */
+
+L.CRS.Earth = L.extend({}, L.CRS, {
+ wrapLng: [-180, 180],
+
+ // Mean Earth Radius, as recommended for use by
+ // the International Union of Geodesy and Geophysics,
+ // see http://rosettacode.org/wiki/Haversine_formula
+ R: 6371000,
+
+ // distance 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,
+ a = Math.sin(lat1) * Math.sin(lat2) +
+ Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad);
+
+ return this.R * Math.acos(Math.min(a, 1));
+ }
+});
+
+
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.EPSG3857
+ *
+ * The most common CRS for online maps, used by almost all free and commercial
+ * tile providers. Uses Spherical Mercator projection. Set in by default in
+ * Map's `crs` option.
+ */
+
+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'
+});
+
+
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.EPSG4326
+ *
+ * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection.
+ *
+ * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic),
+ * which is a breaking change from 0.7.x behaviour. If you are using a `TileLayer`
+ * with this CRS, ensure that there are two 256x256 pixel tiles covering the
+ * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90),
+ * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set.
+ */
+
+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)
+});
+
+
+
+/*
+ * @class Map
+ * @aka L.Map
+ * @inherits Evented
+ *
+ * The central class of the API — it is used to create a map on a page and manipulate it.
+ *
+ * @example
+ *
+ * ```js
+ * // initialize the map on the "map" div with a given center and zoom
+ * var map = L.map('map', {
+ * center: [51.505, -0.09],
+ * zoom: 13
+ * });
+ * ```
+ *
+ */
+
+L.Map = L.Evented.extend({
+
+ options: {
+ // @section Map State Options
+ // @option crs: CRS = L.CRS.EPSG3857
+ // The [Coordinate Reference System](#crs) to use. Don't change this if you're not
+ // sure what it means.
+ crs: L.CRS.EPSG3857,
+
+ // @option center: LatLng = undefined
+ // Initial geographic center of the map
+ center: undefined,
+
+ // @option zoom: Number = undefined
+ // Initial map zoom level
+ zoom: undefined,
+
+ // @option minZoom: Number = undefined
+ // Minimum zoom level of the map. Overrides any `minZoom` option set on map layers.
+ minZoom: undefined,
+
+ // @option maxZoom: Number = undefined
+ // Maximum zoom level of the map. Overrides any `maxZoom` option set on map layers.
+ maxZoom: undefined,
+
+ // @option layers: Layer[] = []
+ // Array of layers that will be added to the map initially
+ layers: [],
+
+ // @option maxBounds: LatLngBounds = null
+ // When this option is set, the map restricts the view to the given
+ // geographical bounds, bouncing the user back if the user tries to pan
+ // outside the view. To set the restriction dynamically, use
+ // [`setMaxBounds`](#map-setmaxbounds) method.
+ maxBounds: undefined,
+
+ // @option renderer: Renderer = *
+ // The default method for drawing vector layers on the map. `L.SVG`
+ // or `L.Canvas` by default depending on browser support.
+ renderer: undefined,
+
+
+ // @section Animation Options
+ // @option zoomAnimation: Boolean = true
+ // Whether the map zoom animation is enabled. By default it's enabled
+ // in all browsers that support CSS3 Transitions except Android.
+ zoomAnimation: true,
+
+ // @option zoomAnimationThreshold: Number = 4
+ // Won't animate zoom if the zoom difference exceeds this value.
+ zoomAnimationThreshold: 4,
+
+ // @option fadeAnimation: Boolean = true
+ // Whether the tile fade animation is enabled. By default it's enabled
+ // in all browsers that support CSS3 Transitions except Android.
+ fadeAnimation: true,
+
+ // @option markerZoomAnimation: Boolean = true
+ // Whether markers animate their zoom with the zoom animation, if disabled
+ // they will disappear for the length of the animation. By default it's
+ // enabled in all browsers that support CSS3 Transitions except Android.
+ markerZoomAnimation: true,
+
+ // @option transform3DLimit: Number = 2^23
+ // Defines the maximum size of a CSS translation transform. The default
+ // value should not be changed unless a web browser positions layers in
+ // the wrong place after doing a large `panBy`.
+ transform3DLimit: 8388608, // Precision limit of a 32-bit float
+
+ // @section Interaction Options
+ // @option zoomSnap: Number = 1
+ // Forces the map's zoom level to always be a multiple of this, particularly
+ // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom.
+ // By default, the zoom level snaps to the nearest integer; lower values
+ // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0`
+ // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom.
+ zoomSnap: 1,
+
+ // @option zoomDelta: Number = 1
+ // Controls how much the map's zoom level will change after a
+ // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+`
+ // or `-` on the keyboard, or using the [zoom controls](#control-zoom).
+ // Values smaller than `1` (e.g. `0.5`) allow for greater granularity.
+ zoomDelta: 1,
+
+ // @option trackResize: Boolean = true
+ // Whether the map automatically handles browser window resize to update itself.
+ trackResize: 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.zoom !== undefined) {
+ this._zoom = this._limitZoom(options.zoom);
+ }
+
+ if (options.center && options.zoom !== undefined) {
+ this.setView(L.latLng(options.center), options.zoom, {reset: true});
+ }
+
+ this._handlers = [];
+ this._layers = {};
+ this._zoomBoundLayers = {};
+ this._sizeChanged = true;
+
+ this.callInitHooks();
+
+ // don't animate on browsers without hardware-accelerated transitions or old Android/Opera
+ this._zoomAnimated = L.DomUtil.TRANSITION && L.Browser.any3d && !L.Browser.mobileOpera &&
+ 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) {
+ this._createAnimProxy();
+ L.DomEvent.on(this._proxy, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
+ }
+
+ this._addLayers(this.options.layers);
+ },
+
+
+ // @section Methods for modifying map state
+
+ // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this
+ // Sets the view of the map (geographical center and zoom) with the given
+ // animation options.
+ 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 || {};
+
+ this._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, duration: options.duration}, options.pan);
+ }
+
+ // try animating pan or zoom
+ var moved = (this._zoom !== zoom) ?
+ this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
+ this._tryAnimatedPan(center, options.pan);
+
+ if (moved) {
+ // 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;
+ },
+
+ // @method setZoom(zoom: Number, options: Zoom/pan options): this
+ // Sets the zoom of the map.
+ setZoom: function (zoom, options) {
+ if (!this._loaded) {
+ this._zoom = zoom;
+ return this;
+ }
+ return this.setView(this.getCenter(), zoom, {zoom: options});
+ },
+
+ // @method zoomIn(delta?: Number, options?: Zoom options): this
+ // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
+ zoomIn: function (delta, options) {
+ delta = delta || (L.Browser.any3d ? this.options.zoomDelta : 1);
+ return this.setZoom(this._zoom + delta, options);
+ },
+
+ // @method zoomOut(delta?: Number, options?: Zoom options): this
+ // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default).
+ zoomOut: function (delta, options) {
+ delta = delta || (L.Browser.any3d ? this.options.zoomDelta : 1);
+ return this.setZoom(this._zoom - delta, options);
+ },
+
+ // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this
+ // Zooms the map while keeping a specified geographical point on the map
+ // stationary (e.g. used internally for scroll zoom and double-click zoom).
+ // @alternative
+ // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this
+ // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary.
+ 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});
+ },
+
+ _getBoundsCenterZoom: 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 = (typeof options.maxZoom === 'number') ? 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 {
+ center: center,
+ zoom: zoom
+ };
+ },
+
+ // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this
+ // Sets a map view that contains the given geographical bounds with the
+ // maximum zoom level possible.
+ fitBounds: function (bounds, options) {
+
+ bounds = L.latLngBounds(bounds);
+
+ if (!bounds.isValid()) {
+ throw new Error('Bounds are not valid.');
+ }
+
+ var target = this._getBoundsCenterZoom(bounds, options);
+ return this.setView(target.center, target.zoom, options);
+ },
+
+ // @method fitWorld(options?: fitBounds options): this
+ // Sets a map view that mostly contains the whole world with the maximum
+ // zoom level possible.
+ fitWorld: function (options) {
+ return this.fitBounds([[-90, -180], [90, 180]], options);
+ },
+
+ // @method panTo(latlng: LatLng, options?: Pan options): this
+ // Pans the map to a given center.
+ panTo: function (center, options) { // (LatLng)
+ return this.setView(center, this._zoom, {pan: options});
+ },
+
+ // @method panBy(offset: Point): this
+ // Pans the map by a given number of pixels (animated).
+ panBy: function (offset, options) {
+ offset = L.point(offset).round();
+ options = options || {};
+
+ if (!offset.x && !offset.y) {
+ return this.fire('moveend');
+ }
+ // If we pan too far, 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)) {
+ this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom());
+ return this;
+ }
+
+ 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).round();
+ this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
+ } else {
+ this._rawPanBy(offset);
+ this.fire('move').fire('moveend');
+ }
+
+ return this;
+ },
+
+ // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this
+ // Sets the view of the map (geographical center and zoom) performing a smooth
+ // pan-zoom animation.
+ flyTo: function (targetCenter, targetZoom, options) {
+
+ options = options || {};
+ if (options.animate === false || !L.Browser.any3d) {
+ return this.setView(targetCenter, targetZoom, options);
+ }
+
+ this._stop();
+
+ var from = this.project(this.getCenter()),
+ to = this.project(targetCenter),
+ size = this.getSize(),
+ startZoom = this._zoom;
+
+ targetCenter = L.latLng(targetCenter);
+ targetZoom = targetZoom === undefined ? startZoom : targetZoom;
+
+ var w0 = Math.max(size.x, size.y),
+ w1 = w0 * this.getZoomScale(startZoom, targetZoom),
+ u1 = (to.distanceTo(from)) || 1,
+ rho = 1.42,
+ rho2 = rho * rho;
+
+ function r(i) {
+ var s1 = i ? -1 : 1,
+ s2 = i ? w1 : w0,
+ t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1,
+ b1 = 2 * s2 * rho2 * u1,
+ b = t1 / b1,
+ sq = Math.sqrt(b * b + 1) - b;
+
+ // workaround for floating point precision bug when sq = 0, log = -Infinite,
+ // thus triggering an infinite loop in flyTo
+ var log = sq < 0.000000001 ? -18 : Math.log(sq);
+
+ return log;
+ }
+
+ function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
+ function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
+ function tanh(n) { return sinh(n) / cosh(n); }
+
+ var r0 = r(0);
+
+ function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); }
+ function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; }
+
+ function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); }
+
+ var start = Date.now(),
+ S = (r(1) - r0) / rho,
+ duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8;
+
+ function frame() {
+ var t = (Date.now() - start) / duration,
+ s = easeOut(t) * S;
+
+ if (t <= 1) {
+ this._flyToFrame = L.Util.requestAnimFrame(frame, this);
+
+ this._move(
+ this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom),
+ this.getScaleZoom(w0 / w(s), startZoom),
+ {flyTo: true});
+
+ } else {
+ this
+ ._move(targetCenter, targetZoom)
+ ._moveEnd(true);
+ }
+ }
+
+ this._moveStart(true);
+
+ frame.call(this);
+ return this;
+ },
+
+ // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this
+ // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto),
+ // but takes a bounds parameter like [`fitBounds`](#map-fitbounds).
+ flyToBounds: function (bounds, options) {
+ var target = this._getBoundsCenterZoom(bounds, options);
+ return this.flyTo(target.center, target.zoom, options);
+ },
+
+ // @method setMaxBounds(bounds: Bounds): this
+ // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option).
+ setMaxBounds: function (bounds) {
+ bounds = L.latLngBounds(bounds);
+
+ if (!bounds.isValid()) {
+ this.options.maxBounds = null;
+ return this.off('moveend', this._panInsideMaxBounds);
+ } else if (this.options.maxBounds) {
+ this.off('moveend', this._panInsideMaxBounds);
+ }
+
+ this.options.maxBounds = bounds;
+
+ if (this._loaded) {
+ this._panInsideMaxBounds();
+ }
+
+ return this.on('moveend', this._panInsideMaxBounds);
+ },
+
+ // @method setMinZoom(zoom: Number): this
+ // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option).
+ setMinZoom: function (zoom) {
+ this.options.minZoom = zoom;
+
+ if (this._loaded && this.getZoom() < this.options.minZoom) {
+ return this.setZoom(zoom);
+ }
+
+ return this;
+ },
+
+ // @method setMaxZoom(zoom: Number): this
+ // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option).
+ setMaxZoom: function (zoom) {
+ this.options.maxZoom = zoom;
+
+ if (this._loaded && (this.getZoom() > this.options.maxZoom)) {
+ return this.setZoom(zoom);
+ }
+
+ return this;
+ },
+
+ // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this
+ // Pans the map to the closest view that would lie inside the given bounds (if it's not already), controlling the animation using the options specific, if any.
+ panInsideBounds: function (bounds, options) {
+ this._enforcingBounds = true;
+ var center = this.getCenter(),
+ newCenter = this._limitCenter(center, this._zoom, L.latLngBounds(bounds));
+
+ if (!center.equals(newCenter)) {
+ this.panTo(newCenter, options);
+ }
+
+ this._enforcingBounds = false;
+ return this;
+ },
+
+ // @method invalidateSize(options: Zoom/Pan options): this
+ // Checks if the map container size changed and updates the map if so —
+ // call it after you've changed the map size dynamically, also animating
+ // pan by default. If `options.pan` is `false`, panning will not occur.
+ // If `options.debounceMoveend` is `true`, it will delay `moveend` event so
+ // that it doesn't happen often even if the method is called many
+ // times in a row.
+
+ // @alternative
+ // @method invalidateSize(animate: Boolean): this
+ // Checks if the map container size changed and updates the map if so —
+ // call it after you've changed the map size dynamically, also animating
+ // pan by default.
+ 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._lastCenter = 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');
+ }
+ }
+
+ // @section Map state change events
+ // @event resize: ResizeEvent
+ // Fired when the map is resized.
+ return this.fire('resize', {
+ oldSize: oldSize,
+ newSize: newSize
+ });
+ },
+
+ // @section Methods for modifying map state
+ // @method stop(): this
+ // Stops the currently running `panTo` or `flyTo` animation, if any.
+ stop: function () {
+ this.setZoom(this._limitZoom(this._zoom));
+ if (!this.options.zoomSnap) {
+ this.fire('viewreset');
+ }
+ return this._stop();
+ },
+
+ // @section Geolocation methods
+ // @method locate(options?: Locate options): this
+ // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound)
+ // event with location data on success or a [`locationerror`](#map-locationerror) event on failure,
+ // and optionally sets the map view to the user's location with respect to
+ // detection accuracy (or to the world view if geolocation failed).
+ // Note that, if your page doesn't use HTTPS, this method will fail in
+ // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins))
+ // See `Locate options` for more details.
+ locate: function (options) {
+
+ options = this._locateOptions = L.extend({
+ timeout: 10000,
+ watch: false
+ // setView: false
+ // maxZoom: <Number>
+ // maximumAge: 0
+ // enableHighAccuracy: false
+ }, options);
+
+ if (!('geolocation' in navigator)) {
+ 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;
+ },
+
+ // @method stopLocate(): this
+ // Stops watching location previously initiated by `map.locate({watch: true})`
+ // and aborts resetting the map view if map.locate was called with
+ // `{setView: true}`.
+ stopLocate: function () {
+ if (navigator.geolocation && navigator.geolocation.clearWatch) {
+ 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();
+ }
+
+ // @section Location events
+ // @event locationerror: ErrorEvent
+ // Fired when geolocation (using the [`locate`](#map-locate) method) failed.
+ 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),
+ bounds = latlng.toBounds(pos.coords.accuracy),
+ 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];
+ }
+ }
+
+ // @event locationfound: LocationEvent
+ // Fired when geolocation (using the [`locate`](#map-locate) method)
+ // went successfully.
+ this.fire('locationfound', data);
+ },
+
+ // TODO handler.addTo
+ // TODO Appropiate docs section?
+ // @section Other Methods
+ // @method addHandler(name: String, HandlerClass: Function): this
+ // Adds a new `Handler` to the map, given its name and constructor function.
+ 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;
+ },
+
+ // @method remove(): this
+ // Destroys the map and clears all related event listeners.
+ remove: function () {
+
+ this._initEvents(true);
+
+ if (this._containerId !== this._container._leaflet_id) {
+ throw new Error('Map container is being reused by another instance');
+ }
+
+ try {
+ // throws error in IE6-8
+ delete this._container._leaflet_id;
+ delete this._containerId;
+ } catch (e) {
+ /*eslint-disable */
+ this._container._leaflet_id = undefined;
+ /*eslint-enable */
+ this._containerId = undefined;
+ }
+
+ L.DomUtil.remove(this._mapPane);
+
+ if (this._clearControlPos) {
+ this._clearControlPos();
+ }
+
+ this._clearHandlers();
+
+ if (this._loaded) {
+ // @section Map state change events
+ // @event unload: Event
+ // Fired when the map is destroyed with [remove](#map-remove) method.
+ this.fire('unload');
+ }
+
+ for (var i in this._layers) {
+ this._layers[i].remove();
+ }
+
+ return this;
+ },
+
+ // @section Other Methods
+ // @method createPane(name: String, container?: HTMLElement): HTMLElement
+ // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already,
+ // then returns it. The pane is created as a children of `container`, or
+ // as a children of the main map pane if not set.
+ 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;
+ },
+
+ // @section Methods for Getting Map State
+
+ // @method getCenter(): LatLng
+ // Returns the geographical center of the map view
+ getCenter: function () {
+ this._checkIfLoaded();
+
+ if (this._lastCenter && !this._moved()) {
+ return this._lastCenter;
+ }
+ return this.layerPointToLatLng(this._getCenterLayerPoint());
+ },
+
+ // @method getZoom(): Number
+ // Returns the current zoom level of the map view
+ getZoom: function () {
+ return this._zoom;
+ },
+
+ // @method getBounds(): LatLngBounds
+ // Returns the geographical bounds visible in the current map view
+ getBounds: function () {
+ var bounds = this.getPixelBounds(),
+ sw = this.unproject(bounds.getBottomLeft()),
+ ne = this.unproject(bounds.getTopRight());
+
+ return new L.LatLngBounds(sw, ne);
+ },
+
+ // @method getMinZoom(): Number
+ // Returns the minimum zoom level of the map (if set in the `minZoom` option of the map or of any layers), or `0` by default.
+ getMinZoom: function () {
+ return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom;
+ },
+
+ // @method getMaxZoom(): Number
+ // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers).
+ getMaxZoom: function () {
+ return this.options.maxZoom === undefined ?
+ (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) :
+ this.options.maxZoom;
+ },
+
+ // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean): Number
+ // Returns the maximum zoom level on which the given bounds fit to the map
+ // view in its entirety. If `inside` (optional) is set to `true`, the method
+ // instead returns the minimum zoom level on which the map view fits into
+ // the given bounds in its entirety.
+ getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
+ bounds = L.latLngBounds(bounds);
+ padding = L.point(padding || [0, 0]);
+
+ var zoom = this.getZoom() || 0,
+ min = this.getMinZoom(),
+ max = this.getMaxZoom(),
+ nw = bounds.getNorthWest(),
+ se = bounds.getSouthEast(),
+ size = this.getSize().subtract(padding),
+ boundsSize = L.bounds(this.project(se, zoom), this.project(nw, zoom)).getSize(),
+ snap = L.Browser.any3d ? this.options.zoomSnap : 1;
+
+ var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y);
+ zoom = this.getScaleZoom(scale, zoom);
+
+ if (snap) {
+ zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
+ zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
+ }
+
+ return Math.max(min, Math.min(max, zoom));
+ },
+
+ // @method getSize(): Point
+ // Returns the current size of the map container (in pixels).
+ getSize: function () {
+ if (!this._size || this._sizeChanged) {
+ this._size = new L.Point(
+ this._container.clientWidth || 0,
+ this._container.clientHeight || 0);
+
+ this._sizeChanged = false;
+ }
+ return this._size.clone();
+ },
+
+ // @method getPixelBounds(): Bounds
+ // Returns the bounds of the current map view in projected pixel
+ // coordinates (sometimes useful in layer and overlay implementations).
+ getPixelBounds: function (center, zoom) {
+ var topLeftPoint = this._getTopLeftPoint(center, zoom);
+ return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
+ },
+
+ // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to
+ // the map pane? "left point of the map layer" can be confusing, specially
+ // since there can be negative offsets.
+ // @method getPixelOrigin(): Point
+ // Returns the projected pixel coordinates of the top left point of
+ // the map layer (useful in custom layer and overlay implementations).
+ getPixelOrigin: function () {
+ this._checkIfLoaded();
+ return this._pixelOrigin;
+ },
+
+ // @method getPixelWorldBounds(zoom?: Number): Bounds
+ // Returns the world's bounds in pixel coordinates for zoom level `zoom`.
+ // If `zoom` is omitted, the map's current zoom level is used.
+ getPixelWorldBounds: function (zoom) {
+ return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom);
+ },
+
+ // @section Other Methods
+
+ // @method getPane(pane: String|HTMLElement): HTMLElement
+ // Returns a [map pane](#map-pane), given its name or its HTML element (its identity).
+ getPane: function (pane) {
+ return typeof pane === 'string' ? this._panes[pane] : pane;
+ },
+
+ // @method getPanes(): Object
+ // Returns a plain object containing the names of all [panes](#map-pane) as keys and
+ // the panes as values.
+ getPanes: function () {
+ return this._panes;
+ },
+
+ // @method getContainer: HTMLElement
+ // Returns the HTML element that contains the map.
+ getContainer: function () {
+ return this._container;
+ },
+
+
+ // @section Conversion Methods
+
+ // @method getZoomScale(toZoom: Number, fromZoom: Number): Number
+ // Returns the scale factor to be applied to a map transition from zoom level
+ // `fromZoom` to `toZoom`. Used internally to help with zoom animations.
+ getZoomScale: function (toZoom, fromZoom) {
+ // TODO replace with universal implementation after refactoring projections
+ var crs = this.options.crs;
+ fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
+ return crs.scale(toZoom) / crs.scale(fromZoom);
+ },
+
+ // @method getScaleZoom(scale: Number, fromZoom: Number): Number
+ // Returns the zoom level that the map would end up at, if it is at `fromZoom`
+ // level and everything is scaled by a factor of `scale`. Inverse of
+ // [`getZoomScale`](#map-getZoomScale).
+ getScaleZoom: function (scale, fromZoom) {
+ var crs = this.options.crs;
+ fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
+ var zoom = crs.zoom(scale * crs.scale(fromZoom));
+ return isNaN(zoom) ? Infinity : zoom;
+ },
+
+ // @method project(latlng: LatLng, zoom: Number): Point
+ // Projects a geographical coordinate `LatLng` according to the projection
+ // of the map's CRS, then scales it according to `zoom` and the CRS's
+ // `Transformation`. The result is pixel coordinate relative to
+ // the CRS origin.
+ project: function (latlng, zoom) {
+ zoom = zoom === undefined ? this._zoom : zoom;
+ return this.options.crs.latLngToPoint(L.latLng(latlng), zoom);
+ },
+
+ // @method unproject(point: Point, zoom: Number): LatLng
+ // Inverse of [`project`](#map-project).
+ unproject: function (point, zoom) {
+ zoom = zoom === undefined ? this._zoom : zoom;
+ return this.options.crs.pointToLatLng(L.point(point), zoom);
+ },
+
+ // @method layerPointToLatLng(point: Point): LatLng
+ // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
+ // returns the corresponding geographical coordinate (for the current zoom level).
+ layerPointToLatLng: function (point) {
+ var projectedPoint = L.point(point).add(this.getPixelOrigin());
+ return this.unproject(projectedPoint);
+ },
+
+ // @method latLngToLayerPoint(latlng: LatLng): Point
+ // Given a geographical coordinate, returns the corresponding pixel coordinate
+ // relative to the [origin pixel](#map-getpixelorigin).
+ latLngToLayerPoint: function (latlng) {
+ var projectedPoint = this.project(L.latLng(latlng))._round();
+ return projectedPoint._subtract(this.getPixelOrigin());
+ },
+
+ // @method wrapLatLng(latlng: LatLng): LatLng
+ // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the
+ // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the
+ // CRS's bounds.
+ // By default this means longitude is wrapped around the dateline so its
+ // value is between -180 and +180 degrees.
+ wrapLatLng: function (latlng) {
+ return this.options.crs.wrapLatLng(L.latLng(latlng));
+ },
+
+ // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds
+ // Returns a `LatLngBounds` with the same size as the given one, ensuring that
+ // its center is within the CRS's bounds.
+ // By default this means the center longitude is wrapped around the dateline so its
+ // value is between -180 and +180 degrees, and the majority of the bounds
+ // overlaps the CRS's bounds.
+ wrapLatLngBounds: function (latlng) {
+ return this.options.crs.wrapLatLngBounds(L.latLngBounds(latlng));
+ },
+
+ // @method distance(latlng1: LatLng, latlng2: LatLng): Number
+ // Returns the distance between two geographical coordinates according to
+ // the map's CRS. By default this measures distance in meters.
+ distance: function (latlng1, latlng2) {
+ return this.options.crs.distance(L.latLng(latlng1), L.latLng(latlng2));
+ },
+
+ // @method containerPointToLayerPoint(point: Point): Point
+ // Given a pixel coordinate relative to the map container, returns the corresponding
+ // pixel coordinate relative to the [origin pixel](#map-getpixelorigin).
+ containerPointToLayerPoint: function (point) { // (Point)
+ return L.point(point).subtract(this._getMapPanePos());
+ },
+
+ // @method layerPointToContainerPoint(point: Point): Point
+ // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
+ // returns the corresponding pixel coordinate relative to the map container.
+ layerPointToContainerPoint: function (point) { // (Point)
+ return L.point(point).add(this._getMapPanePos());
+ },
+
+ // @method containerPointToLatLng(point: Point): LatLng
+ // Given a pixel coordinate relative to the map container, returns
+ // the corresponding geographical coordinate (for the current zoom level).
+ containerPointToLatLng: function (point) {
+ var layerPoint = this.containerPointToLayerPoint(L.point(point));
+ return this.layerPointToLatLng(layerPoint);
+ },
+
+ // @method latLngToContainerPoint(latlng: LatLng): Point
+ // Given a geographical coordinate, returns the corresponding pixel coordinate
+ // relative to the map container.
+ latLngToContainerPoint: function (latlng) {
+ return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng)));
+ },
+
+ // @method mouseEventToContainerPoint(ev: MouseEvent): Point
+ // Given a MouseEvent object, returns the pixel coordinate relative to the
+ // map container where the event took place.
+ mouseEventToContainerPoint: function (e) {
+ return L.DomEvent.getMousePosition(e, this._container);
+ },
+
+ // @method mouseEventToLayerPoint(ev: MouseEvent): Point
+ // Given a MouseEvent object, returns the pixel coordinate relative to
+ // the [origin pixel](#map-getpixelorigin) where the event took place.
+ mouseEventToLayerPoint: function (e) {
+ return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
+ },
+
+ // @method mouseEventToLatLng(ev: MouseEvent): LatLng
+ // Given a MouseEvent object, returns geographical coordinate where the
+ // event took place.
+ 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_id) {
+ throw new Error('Map container is already initialized.');
+ }
+
+ L.DomEvent.addListener(container, 'scroll', this._onScroll, this);
+ this._containerId = L.Util.stamp(container);
+ },
+
+ _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._paneRenderers = {};
+
+ // @section
+ //
+ // Panes are DOM elements used to control the ordering of layers on the map. You
+ // can access panes with [`map.getPane`](#map-getpane) or
+ // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the
+ // [`map.createPane`](#map-createpane) method.
+ //
+ // Every map has the following default panes that differ only in zIndex.
+ //
+ // @pane mapPane: HTMLElement = 'auto'
+ // Pane that contains all other map panes
+
+ this._mapPane = this.createPane('mapPane', this._container);
+ L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
+
+ // @pane tilePane: HTMLElement = 200
+ // Pane for `GridLayer`s and `TileLayer`s
+ this.createPane('tilePane');
+ // @pane overlayPane: HTMLElement = 400
+ // Pane for vector overlays (`Path`s), like `Polyline`s and `Polygon`s
+ this.createPane('shadowPane');
+ // @pane shadowPane: HTMLElement = 500
+ // Pane for overlay shadows (e.g. `Marker` shadows)
+ this.createPane('overlayPane');
+ // @pane markerPane: HTMLElement = 600
+ // Pane for `Icon`s of `Marker`s
+ this.createPane('markerPane');
+ // @pane tooltipPane: HTMLElement = 650
+ // Pane for tooltip.
+ this.createPane('tooltipPane');
+ // @pane popupPane: HTMLElement = 700
+ // Pane for `Popup`s.
+ 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
+
+ // @section Map state change events
+ _resetView: function (center, zoom) {
+ L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
+
+ var loading = !this._loaded;
+ this._loaded = true;
+ zoom = this._limitZoom(zoom);
+
+ this.fire('viewprereset');
+
+ var zoomChanged = this._zoom !== zoom;
+ this
+ ._moveStart(zoomChanged)
+ ._move(center, zoom)
+ ._moveEnd(zoomChanged);
+
+ // @event viewreset: Event
+ // Fired when the map needs to redraw its content (this usually happens
+ // on map zoom or load). Very useful for creating custom overlays.
+ this.fire('viewreset');
+
+ // @event load: Event
+ // Fired when the map is initialized (when its center and zoom are set
+ // for the first time).
+ if (loading) {
+ this.fire('load');
+ }
+ },
+
+ _moveStart: function (zoomChanged) {
+ // @event zoomstart: Event
+ // Fired when the map zoom is about to change (e.g. before zoom animation).
+ // @event movestart: Event
+ // Fired when the view of the map starts changing (e.g. user starts dragging the map).
+ if (zoomChanged) {
+ this.fire('zoomstart');
+ }
+ return this.fire('movestart');
+ },
+
+ _move: function (center, zoom, data) {
+ if (zoom === undefined) {
+ zoom = this._zoom;
+ }
+ var zoomChanged = this._zoom !== zoom;
+
+ this._zoom = zoom;
+ this._lastCenter = center;
+ this._pixelOrigin = this._getNewPixelOrigin(center);
+
+ // @event zoom: Event
+ // Fired repeatedly during any change in zoom level, including zoom
+ // and fly animations.
+ if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530
+ this.fire('zoom', data);
+ }
+
+ // @event move: Event
+ // Fired repeatedly during any movement of the map, including pan and
+ // fly animations.
+ return this.fire('move', data);
+ },
+
+ _moveEnd: function (zoomChanged) {
+ // @event zoomend: Event
+ // Fired when the map has changed, after any animations.
+ if (zoomChanged) {
+ this.fire('zoomend');
+ }
+
+ // @event moveend: Event
+ // Fired when the center of the map stops changing (e.g. user stopped
+ // dragging the map).
+ return this.fire('moveend');
+ },
+
+ _stop: function () {
+ L.Util.cancelAnimFrame(this._flyToFrame);
+ if (this._panAnim) {
+ this._panAnim.stop();
+ }
+ return this;
+ },
+
+ _rawPanBy: function (offset) {
+ L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
+ },
+
+ _getZoomSpan: function () {
+ return this.getMaxZoom() - this.getMinZoom();
+ },
+
+ _panInsideMaxBounds: function () {
+ if (!this._enforcingBounds) {
+ this.panInsideBounds(this.options.maxBounds);
+ }
+ },
+
+ _checkIfLoaded: function () {
+ if (!this._loaded) {
+ throw new Error('Set map center and zoom first.');
+ }
+ },
+
+ // DOM event handling
+
+ // @section Interaction events
+ _initEvents: function (remove) {
+ if (!L.DomEvent) { return; }
+
+ this._targets = {};
+ this._targets[L.stamp(this._container)] = this;
+
+ var onOff = remove ? 'off' : 'on';
+
+ // @event click: MouseEvent
+ // Fired when the user clicks (or taps) the map.
+ // @event dblclick: MouseEvent
+ // Fired when the user double-clicks (or double-taps) the map.
+ // @event mousedown: MouseEvent
+ // Fired when the user pushes the mouse button on the map.
+ // @event mouseup: MouseEvent
+ // Fired when the user releases the mouse button on the map.
+ // @event mouseover: MouseEvent
+ // Fired when the mouse enters the map.
+ // @event mouseout: MouseEvent
+ // Fired when the mouse leaves the map.
+ // @event mousemove: MouseEvent
+ // Fired while the mouse moves over the map.
+ // @event contextmenu: MouseEvent
+ // Fired when the user pushes the right mouse button on the map, prevents
+ // default browser context menu from showing if there are listeners on
+ // this event. Also fired on mobile when the user holds a single touch
+ // for a second (also called long press).
+ // @event keypress: KeyboardEvent
+ // Fired when the user presses a key from the keyboard while the map is focused.
+ L.DomEvent[onOff](this._container, 'click dblclick mousedown mouseup ' +
+ 'mouseover mouseout mousemove contextmenu keypress', this._handleDOMEvent, this);
+
+ if (this.options.trackResize) {
+ L.DomEvent[onOff](window, 'resize', this._onResize, this);
+ }
+
+ if (L.Browser.any3d && this.options.transform3DLimit) {
+ this[onOff]('moveend', this._onMoveEnd);
+ }
+ },
+
+ _onResize: function () {
+ L.Util.cancelAnimFrame(this._resizeRequest);
+ this._resizeRequest = L.Util.requestAnimFrame(
+ function () { this.invalidateSize({debounceMoveend: true}); }, this);
+ },
+
+ _onScroll: function () {
+ this._container.scrollTop = 0;
+ this._container.scrollLeft = 0;
+ },
+
+ _onMoveEnd: function () {
+ var pos = this._getMapPanePos();
+ if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have
+ // a pixel offset on very high values, see: http://jsfiddle.net/dg6r5hhb/
+ this._resetView(this.getCenter(), this.getZoom());
+ }
+ },
+
+ _findEventTargets: function (e, type) {
+ var targets = [],
+ target,
+ isHover = type === 'mouseout' || type === 'mouseover',
+ src = e.target || e.srcElement,
+ dragging = false;
+
+ while (src) {
+ target = this._targets[L.stamp(src)];
+ if (target && (type === 'click' || type === 'preclick') && !e._simulated && this._draggableMoved(target)) {
+ // Prevent firing click after you just dragged an object.
+ dragging = true;
+ break;
+ }
+ if (target && target.listens(type, true)) {
+ if (isHover && !L.DomEvent._isExternalTarget(src, e)) { break; }
+ targets.push(target);
+ if (isHover) { break; }
+ }
+ if (src === this._container) { break; }
+ src = src.parentNode;
+ }
+ if (!targets.length && !dragging && !isHover && L.DomEvent._isExternalTarget(src, e)) {
+ targets = [this];
+ }
+ return targets;
+ },
+
+ _handleDOMEvent: function (e) {
+ if (!this._loaded || L.DomEvent._skipped(e)) { return; }
+
+ var type = e.type === 'keypress' && e.keyCode === 13 ? 'click' : e.type;
+
+ if (type === 'mousedown') {
+ // prevents outline when clicking on keyboard-focusable element
+ L.DomUtil.preventOutline(e.target || e.srcElement);
+ }
+
+ this._fireDOMEvent(e, type);
+ },
+
+ _fireDOMEvent: function (e, type, targets) {
+
+ if (e.type === 'click') {
+ // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups).
+ // @event preclick: MouseEvent
+ // Fired before mouse click on the map (sometimes useful when you
+ // want something to happen on click before any existing click
+ // handlers start running).
+ var synth = L.Util.extend({}, e);
+ synth.type = 'preclick';
+ this._fireDOMEvent(synth, synth.type, targets);
+ }
+
+ if (e._stopped) { return; }
+
+ // Find the layer the event is propagating from and its parents.
+ targets = (targets || []).concat(this._findEventTargets(e, type));
+
+ if (!targets.length) { return; }
+
+ var target = targets[0];
+ if (type === 'contextmenu' && target.listens(type, true)) {
+ L.DomEvent.preventDefault(e);
+ }
+
+ var data = {
+ originalEvent: e
+ };
+
+ if (e.type !== 'keypress') {
+ var isMarker = target instanceof L.Marker;
+ data.containerPoint = isMarker ?
+ this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e);
+ data.layerPoint = this.containerPointToLayerPoint(data.containerPoint);
+ data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint);
+ }
+
+ for (var i = 0; i < targets.length; i++) {
+ targets[i].fire(type, data, true);
+ if (data.originalEvent._stopped ||
+ (targets[i].options.nonBubblingEvents && L.Util.indexOf(targets[i].options.nonBubblingEvents, type) !== -1)) { return; }
+ }
+ },
+
+ _draggableMoved: function (obj) {
+ obj = obj.dragging && obj.dragging.enabled() ? obj : this;
+ return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved());
+ },
+
+ _clearHandlers: function () {
+ for (var i = 0, len = this._handlers.length; i < len; i++) {
+ this._handlers[i].disable();
+ }
+ },
+
+ // @section Other Methods
+
+ // @method whenReady(fn: Function, context?: Object): this
+ // Runs the given function `fn` when the map gets initialized with
+ // a view (center and zoom) and at least one layer, or immediately
+ // if it's already initialized, optionally passing a function context.
+ 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) || new L.Point(0, 0);
+ },
+
+ _moved: function () {
+ var pos = this._getMapPanePos();
+ return pos && !pos.equals([0, 0]);
+ },
+
+ _getTopLeftPoint: function (center, zoom) {
+ var pixelOrigin = center && zoom !== undefined ?
+ this._getNewPixelOrigin(center, zoom) :
+ this.getPixelOrigin();
+ return pixelOrigin.subtract(this._getMapPanePos());
+ },
+
+ _getNewPixelOrigin: function (center, zoom) {
+ var viewHalf = this.getSize()._divideBy(2);
+ return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round();
+ },
+
+ _latLngToNewLayerPoint: function (latlng, zoom, center) {
+ var topLeft = this._getNewPixelOrigin(center, zoom);
+ return this.project(latlng, zoom)._subtract(topLeft);
+ },
+
+ _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) {
+ var topLeft = this._getNewPixelOrigin(center, zoom);
+ return L.bounds([
+ this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft),
+ this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft),
+ this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft),
+ this.project(latLngBounds.getNorthEast(), zoom)._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);
+
+ // If offset is less than a pixel, ignore.
+ // This prevents unstable projections from getting into
+ // an infinite loop of tiny offsets.
+ if (offset.round().equals([0, 0])) {
+ return center;
+ }
+
+ 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 projectedMaxBounds = L.bounds(
+ this.project(maxBounds.getNorthEast(), zoom),
+ this.project(maxBounds.getSouthWest(), zoom)
+ ),
+ minOffset = projectedMaxBounds.min.subtract(pxBounds.min),
+ maxOffset = projectedMaxBounds.max.subtract(pxBounds.max),
+
+ dx = this._rebound(minOffset.x, -maxOffset.x),
+ dy = this._rebound(minOffset.y, -maxOffset.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(),
+ snap = L.Browser.any3d ? this.options.zoomSnap : 1;
+ if (snap) {
+ zoom = Math.round(zoom / snap) * snap;
+ }
+ return Math.max(min, Math.min(max, zoom));
+ },
+
+ _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;
+ },
+
+ _createAnimProxy: function () {
+
+ var proxy = this._proxy = L.DomUtil.create('div', 'leaflet-proxy leaflet-zoom-animated');
+ this._panes.mapPane.appendChild(proxy);
+
+ this.on('zoomanim', function (e) {
+ var prop = L.DomUtil.TRANSFORM,
+ transform = proxy.style[prop];
+
+ L.DomUtil.setTransform(proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1));
+
+ // workaround for case when transform is the same and so transitionend event is not fired
+ if (transform === proxy.style[prop] && this._animatingZoom) {
+ this._onZoomTransitionEnd();
+ }
+ }, this);
+
+ this.on('load moveend', function () {
+ var c = this.getCenter(),
+ z = this.getZoom();
+ L.DomUtil.setTransform(proxy, this.project(c, z), this.getZoomScale(z, 1));
+ }, this);
+ },
+
+ _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
+ ._moveStart(true)
+ ._animateZoom(center, zoom, true);
+ }, this);
+
+ return true;
+ },
+
+ _animateZoom: function (center, zoom, startAnim, noUpdate) {
+ if (startAnim) {
+ this._animatingZoom = true;
+
+ // remember what center/zoom to set after animation
+ this._animateToCenter = center;
+ this._animateToZoom = zoom;
+
+ L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
+ }
+
+ // @event zoomanim: ZoomAnimEvent
+ // Fired on every frame of a zoom animation
+ this.fire('zoomanim', {
+ center: center,
+ zoom: zoom,
+ noUpdate: noUpdate
+ });
+
+ // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693
+ setTimeout(L.bind(this._onZoomTransitionEnd, this), 250);
+ },
+
+ _onZoomTransitionEnd: function () {
+ if (!this._animatingZoom) { return; }
+
+ L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
+
+ this._animatingZoom = false;
+
+ this._move(this._animateToCenter, this._animateToZoom);
+
+ // This anim frame should prevent an obscure iOS webkit tile loading race condition.
+ L.Util.requestAnimFrame(function () {
+ this._moveEnd(true);
+ }, this);
+ }
+});
+
+// @section
+
+// @factory L.map(id: String, options?: Map options)
+// Instantiates a map object given the DOM ID of a `<div>` element
+// and optionally an object literal with `Map options`.
+//
+// @alternative
+// @factory L.map(el: HTMLElement, options?: Map options)
+// Instantiates a map object given an instance of a `<div>` HTML element
+// and optionally an object literal with `Map options`.
+L.map = function (id, options) {
+ return new L.Map(id, options);
+};
+
+
+
+
+/*
+ * @class Layer
+ * @inherits Evented
+ * @aka L.Layer
+ * @aka ILayer
+ *
+ * A set of methods from the Layer base class that all Leaflet layers use.
+ * Inherits all methods, options and events from `L.Evented`.
+ *
+ * @example
+ *
+ * ```js
+ * var layer = L.Marker(latlng).addTo(map);
+ * layer.addTo(map);
+ * layer.remove();
+ * ```
+ *
+ * @event add: Event
+ * Fired after the layer is added to a map
+ *
+ * @event remove: Event
+ * Fired after the layer is removed from a map
+ */
+
+
+L.Layer = L.Evented.extend({
+
+ // Classes extending `L.Layer` will inherit the following options:
+ options: {
+ // @option pane: String = 'overlayPane'
+ // By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default.
+ pane: 'overlayPane',
+ nonBubblingEvents: [], // Array of events that should not be bubbled to DOM parents (like the map),
+
+ // @option attribution: String = null
+ // String to be shown in the attribution control, describes the layer data, e.g. "© Mapbox".
+ attribution: null
+ },
+
+ /* @section
+ * Classes extending `L.Layer` will inherit the following methods:
+ *
+ * @method addTo(map: Map): this
+ * Adds the layer to the given map
+ */
+ addTo: function (map) {
+ map.addLayer(this);
+ return this;
+ },
+
+ // @method remove: this
+ // Removes the layer from the map it is currently active on.
+ remove: function () {
+ return this.removeFrom(this._map || this._mapToAdd);
+ },
+
+ // @method removeFrom(map: Map): this
+ // Removes the layer from the given map
+ removeFrom: function (obj) {
+ if (obj) {
+ obj.removeLayer(this);
+ }
+ return this;
+ },
+
+ // @method getPane(name? : String): HTMLElement
+ // Returns the `HTMLElement` representing the named pane on the map. If `name` is omitted, returns the pane for this layer.
+ getPane: function (name) {
+ return this._map.getPane(name ? (this.options[name] || name) : this.options.pane);
+ },
+
+ addInteractiveTarget: function (targetEl) {
+ this._map._targets[L.stamp(targetEl)] = this;
+ return this;
+ },
+
+ removeInteractiveTarget: function (targetEl) {
+ delete this._map._targets[L.stamp(targetEl)];
+ return this;
+ },
+
+ // @method getAttribution: String
+ // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution).
+ getAttribution: function () {
+ return this.options.attribution;
+ },
+
+ _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) {
+ var events = this.getEvents();
+ map.on(events, this);
+ this.once('remove', function () {
+ map.off(events, this);
+ }, this);
+ }
+
+ this.onAdd(map);
+
+ if (this.getAttribution && map.attributionControl) {
+ map.attributionControl.addAttribution(this.getAttribution());
+ }
+
+ this.fire('add');
+ map.fire('layeradd', {layer: this});
+ }
+});
+
+/* @section Extension methods
+ * @uninheritable
+ *
+ * Every layer should extend from `L.Layer` and (re-)implement the following methods.
+ *
+ * @method onAdd(map: Map): this
+ * Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer).
+ *
+ * @method onRemove(map: Map): this
+ * Should contain all clean up code that removes the layer's elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer).
+ *
+ * @method getEvents(): Object
+ * This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer.
+ *
+ * @method getAttribution(): String
+ * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible.
+ *
+ * @method beforeAdd(map: Map): this
+ * Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only.
+ */
+
+
+/* @namespace Map
+ * @section Layer events
+ *
+ * @event layeradd: LayerEvent
+ * Fired when a new layer is added to the map.
+ *
+ * @event layerremove: LayerEvent
+ * Fired when some layer is removed from the map
+ *
+ * @section Methods for Layers and Controls
+ */
+L.Map.include({
+ // @method addLayer(layer: Layer): this
+ // Adds the given layer to the map
+ addLayer: function (layer) {
+ var id = L.stamp(layer);
+ if (this._layers[id]) { return this; }
+ this._layers[id] = layer;
+
+ layer._mapToAdd = this;
+
+ if (layer.beforeAdd) {
+ layer.beforeAdd(this);
+ }
+
+ this.whenReady(layer._layerAdd, layer);
+
+ return this;
+ },
+
+ // @method removeLayer(layer: Layer): this
+ // Removes the given layer from the map.
+ 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());
+ }
+
+ delete this._layers[id];
+
+ if (this._loaded) {
+ this.fire('layerremove', {layer: layer});
+ layer.fire('remove');
+ }
+
+ layer._map = layer._mapToAdd = null;
+
+ return this;
+ },
+
+ // @method hasLayer(layer: Layer): Boolean
+ // Returns `true` if the given layer is currently added to the map
+ hasLayer: function (layer) {
+ return !!layer && (L.stamp(layer) in this._layers);
+ },
+
+ /* @method eachLayer(fn: Function, context?: Object): this
+ * Iterates over the layers of the map, optionally specifying context of the iterator function.
+ * ```
+ * map.eachLayer(function(layer){
+ * layer.bindPopup('Hello');
+ * });
+ * ```
+ */
+ 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;
+
+ // @section Map state change events
+ // @event zoomlevelschange: Event
+ // Fired when the number of zoomlevels on the map is changed due
+ // to adding or removing a layer.
+ if (oldZoomSpan !== this._getZoomSpan()) {
+ this.fire('zoomlevelschange');
+ }
+
+ if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) {
+ this.setZoom(this._layersMaxZoom);
+ }
+ if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) {
+ this.setZoom(this._layersMinZoom);
+ }
+ }
+});
+
+
+
+/*
+ * @namespace DomEvent
+ * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally.
+ */
+
+// Inspired by John Resig, Dean Edwards and YUI addEvent implementations.
+
+
+
+var eventsKey = '_leaflet_events';
+
+L.DomEvent = {
+
+ // @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this
+ // Adds a listener function (`fn`) to a particular DOM event type of the
+ // element `el`. You can optionally specify the context of the listener
+ // (object the `this` keyword will point to). You can also pass several
+ // space-separated types (e.g. `'click dblclick'`).
+
+ // @alternative
+ // @function on(el: HTMLElement, eventMap: Object, context?: Object): this
+ // Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+ 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;
+ },
+
+ // @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this
+ // Removes a previously added listener function. If no function is specified,
+ // it will remove all the listeners of that particular DOM event from the element.
+ // Note that if you passed a custom context to on, you must pass the same
+ // context to `off` in order to remove the listener.
+
+ // @alternative
+ // @function off(el: HTMLElement, eventMap: Object, context?: Object): this
+ // Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
+ 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) {
+ this.addPointerListener(obj, type, handler, id);
+
+ } else if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener &&
+ !(L.Browser.pointer && L.Browser.chrome)) {
+ // Chrome >55 does not need the synthetic dblclicks from addDoubleTapListener
+ // See #5180
+ this.addDoubleTapListener(obj, handler, id);
+
+ } else if ('addEventListener' in obj) {
+
+ if (type === 'mousewheel') {
+ obj.addEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false);
+
+ } else if ((type === 'mouseenter') || (type === 'mouseleave')) {
+ handler = function (e) {
+ e = e || window.event;
+ if (L.DomEvent._isExternalTarget(obj, e)) {
+ 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('onwheel' in obj ? 'wheel' : 'mousewheel', 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;
+ },
+
+ // @function stopPropagation(ev: DOMEvent): this
+ // Stop the given event from propagation to parent elements. Used inside the listener functions:
+ // ```js
+ // L.DomEvent.on(div, 'click', function (ev) {
+ // L.DomEvent.stopPropagation(ev);
+ // });
+ // ```
+ stopPropagation: function (e) {
+
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ } else if (e.originalEvent) { // In case of Leaflet event.
+ e.originalEvent._stopped = true;
+ } else {
+ e.cancelBubble = true;
+ }
+ L.DomEvent._skipped(e);
+
+ return this;
+ },
+
+ // @function disableScrollPropagation(el: HTMLElement): this
+ // Adds `stopPropagation` to the element's `'mousewheel'` events (plus browser variants).
+ disableScrollPropagation: function (el) {
+ return L.DomEvent.on(el, 'mousewheel', L.DomEvent.stopPropagation);
+ },
+
+ // @function disableClickPropagation(el: HTMLElement): this
+ // Adds `stopPropagation` to the element's `'click'`, `'doubleclick'`,
+ // `'mousedown'` and `'touchstart'` events (plus browser variants).
+ 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
+ });
+ },
+
+ // @function preventDefault(ev: DOMEvent): this
+ // Prevents the default action of the DOM Event `ev` from happening (such as
+ // following a link in the href of the a element, or doing a POST request
+ // with page reload when a `<form>` is submitted).
+ // Use it inside listener functions.
+ preventDefault: function (e) {
+
+ if (e.preventDefault) {
+ e.preventDefault();
+ } else {
+ e.returnValue = false;
+ }
+ return this;
+ },
+
+ // @function stop(ev): this
+ // Does `stopPropagation` and `preventDefault` at the same time.
+ stop: function (e) {
+ return L.DomEvent
+ .preventDefault(e)
+ .stopPropagation(e);
+ },
+
+ // @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point
+ // Gets normalized mouse position from a DOM event relative to the
+ // `container` or to the whole page if not specified.
+ 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);
+ },
+
+ // Chrome on Win scrolls double the pixels as in other platforms (see #4538),
+ // and Firefox scrolls device pixels, not CSS pixels
+ _wheelPxFactor: (L.Browser.win && L.Browser.chrome) ? 2 :
+ L.Browser.gecko ? window.devicePixelRatio :
+ 1,
+
+ // @function getWheelDelta(ev: DOMEvent): Number
+ // Gets normalized wheel delta from a mousewheel DOM event, in vertical
+ // pixels scrolled (negative if scrolling down).
+ // Events from pointing devices without precise scrolling are mapped to
+ // a best guess of 60 pixels.
+ getWheelDelta: function (e) {
+ return (L.Browser.edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta
+ (e.deltaY && e.deltaMode === 0) ? -e.deltaY / L.DomEvent._wheelPxFactor : // Pixels
+ (e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines
+ (e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages
+ (e.deltaX || e.deltaZ) ? 0 : // Skip horizontal/depth wheel events
+ e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels
+ (e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines
+ e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages
+ 0;
+ },
+
+ _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)
+ _isExternalTarget: 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 && 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;
+
+ handler(e);
+ }
+};
+
+// @function addListener(…): this
+// Alias to [`L.DomEvent.on`](#domevent-on)
+L.DomEvent.addListener = L.DomEvent.on;
+
+// @function removeListener(…): this
+// Alias to [`L.DomEvent.off`](#domevent-off)
+L.DomEvent.removeListener = L.DomEvent.off;
+
+
+
+/*
+ * @class PosAnimation
+ * @aka L.PosAnimation
+ * @inherits Evented
+ * Used internally for panning animations, utilizing CSS3 Transitions for modern browsers and a timer fallback for IE6-9.
+ *
+ * @example
+ * ```js
+ * var fx = new L.PosAnimation();
+ * fx.run(el, [300, 500], 0.5);
+ * ```
+ *
+ * @constructor L.PosAnimation()
+ * Creates a `PosAnimation` object.
+ *
+ */
+
+L.PosAnimation = L.Evented.extend({
+
+ // @method run(el: HTMLElement, newPos: Point, duration?: Number, easeLinearity?: Number)
+ // Run an animation of a given element to a new position, optionally setting
+ // duration in seconds (`0.25` by default) and easing linearity factor (3rd
+ // argument of the [cubic bezier curve](http://cubic-bezier.com/#0,0,.5,1),
+ // `0.5` by default).
+ run: function (el, newPos, duration, easeLinearity) {
+ 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();
+
+ // @event start: Event
+ // Fired when the animation starts
+ this.fire('start');
+
+ this._animate();
+ },
+
+ // @method stop()
+ // Stops the animation (if currently running).
+ stop: function () {
+ if (!this._inProgress) { return; }
+
+ this._step(true);
+ this._complete();
+ },
+
+ _animate: function () {
+ // animation loop
+ this._animId = L.Util.requestAnimFrame(this._animate, this);
+ this._step();
+ },
+
+ _step: function (round) {
+ var elapsed = (+new Date()) - this._startTime,
+ duration = this._duration * 1000;
+
+ if (elapsed < duration) {
+ this._runFrame(this._easeOut(elapsed / duration), round);
+ } else {
+ this._runFrame(1);
+ this._complete();
+ }
+ },
+
+ _runFrame: function (progress, round) {
+ var pos = this._startPos.add(this._offset.multiplyBy(progress));
+ if (round) {
+ pos._round();
+ }
+ L.DomUtil.setPosition(this._el, pos);
+
+ // @event step: Event
+ // Fired continuously during the animation.
+ this.fire('step');
+ },
+
+ _complete: function () {
+ L.Util.cancelAnimFrame(this._animId);
+
+ this._inProgress = false;
+ // @event end: Event
+ // Fired when the animation ends.
+ this.fire('end');
+ },
+
+ _easeOut: function (t) {
+ return 1 - Math.pow(1 - t, this._easeOutPower);
+ }
+});
+
+
+
+/*
+ * @namespace Projection
+ * @projection L.Projection.Mercator
+ *
+ * Elliptical Mercator projection — more complex than Spherical Mercator. Takes into account that Earth is a geoid, not a perfect sphere. Used by the EPSG:3395 CRS.
+ */
+
+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);
+ }
+};
+
+
+
+/*
+ * @namespace CRS
+ * @crs L.CRS.EPSG3395
+ *
+ * Rarely used by some commercial tile providers. Uses Elliptical Mercator projection.
+ */
+
+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);
+ }())
+});
+
+
+
+/*
+ * @class GridLayer
+ * @inherits Layer
+ * @aka L.GridLayer
+ *
+ * Generic class for handling a tiled grid of HTML elements. This is the base class for all tile layers and replaces `TileLayer.Canvas`.
+ * GridLayer can be extended to create a tiled grid of HTML elements like `<canvas>`, `<img>` or `<div>`. GridLayer will handle creating and animating these DOM elements for you.
+ *
+ *
+ * @section Synchronous usage
+ * @example
+ *
+ * To create a custom layer, extend GridLayer and implement the `createTile()` method, which will be passed a `Point` object with the `x`, `y`, and `z` (zoom level) coordinates to draw your tile.
+ *
+ * ```js
+ * var CanvasLayer = L.GridLayer.extend({
+ * createTile: function(coords){
+ * // create a <canvas> element for drawing
+ * var tile = L.DomUtil.create('canvas', 'leaflet-tile');
+ *
+ * // setup tile width and height according to the options
+ * var size = this.getTileSize();
+ * tile.width = size.x;
+ * tile.height = size.y;
+ *
+ * // get a canvas context and draw something on it using coords.x, coords.y and coords.z
+ * var ctx = tile.getContext('2d');
+ *
+ * // return the tile so it can be rendered on screen
+ * return tile;
+ * }
+ * });
+ * ```
+ *
+ * @section Asynchronous usage
+ * @example
+ *
+ * Tile creation can also be asynchronous, this is useful when using a third-party drawing library. Once the tile is finished drawing it can be passed to the `done()` callback.
+ *
+ * ```js
+ * var CanvasLayer = L.GridLayer.extend({
+ * createTile: function(coords, done){
+ * var error;
+ *
+ * // create a <canvas> element for drawing
+ * var tile = L.DomUtil.create('canvas', 'leaflet-tile');
+ *
+ * // setup tile width and height according to the options
+ * var size = this.getTileSize();
+ * tile.width = size.x;
+ * tile.height = size.y;
+ *
+ * // draw something asynchronously and pass the tile to the done() callback
+ * setTimeout(function() {
+ * done(error, tile);
+ * }, 1000);
+ *
+ * return tile;
+ * }
+ * });
+ * ```
+ *
+ * @section
+ */
+
+
+L.GridLayer = L.Layer.extend({
+
+ // @section
+ // @aka GridLayer options
+ options: {
+ // @option tileSize: Number|Point = 256
+ // Width and height of tiles in the grid. Use a number if width and height are equal, or `L.point(width, height)` otherwise.
+ tileSize: 256,
+
+ // @option opacity: Number = 1.0
+ // Opacity of the tiles. Can be used in the `createTile()` function.
+ opacity: 1,
+
+ // @option updateWhenIdle: Boolean = depends
+ // If `false`, new tiles are loaded during panning, otherwise only after it (for better performance). `true` by default on mobile browsers, otherwise `false`.
+ updateWhenIdle: L.Browser.mobile,
+
+ // @option updateWhenZooming: Boolean = true
+ // By default, a smooth zoom animation (during a [touch zoom](#map-touchzoom) or a [`flyTo()`](#map-flyto)) will update grid layers every integer zoom level. Setting this option to `false` will update the grid layer only when the smooth animation ends.
+ updateWhenZooming: true,
+
+ // @option updateInterval: Number = 200
+ // Tiles will not update more than once every `updateInterval` milliseconds when panning.
+ updateInterval: 200,
+
+ // @option zIndex: Number = 1
+ // The explicit zIndex of the tile layer.
+ zIndex: 1,
+
+ // @option bounds: LatLngBounds = undefined
+ // If set, tiles will only be loaded inside the set `LatLngBounds`.
+ bounds: null,
+
+ // @option minZoom: Number = 0
+ // The minimum zoom level that tiles will be loaded at. By default the entire map.
+ minZoom: 0,
+
+ // @option maxZoom: Number = undefined
+ // The maximum zoom level that tiles will be loaded at.
+ maxZoom: undefined,
+
+ // @option noWrap: Boolean = false
+ // Whether the layer is wrapped around the antimeridian. If `true`, the
+ // GridLayer will only be displayed once at low zoom levels. Has no
+ // effect when the [map CRS](#map-crs) doesn't wrap around. Can be used
+ // in combination with [`bounds`](#gridlayer-bounds) to prevent requesting
+ // tiles outside the CRS limits.
+ noWrap: false,
+
+ // @option pane: String = 'tilePane'
+ // `Map pane` where the grid layer will be added.
+ pane: 'tilePane',
+
+ // @option className: String = ''
+ // A custom class name to assign to the tile layer. Empty by default.
+ className: '',
+
+ // @option keepBuffer: Number = 2
+ // When panning the map, keep this many rows and columns of tiles before unloading them.
+ keepBuffer: 2
+ },
+
+ initialize: function (options) {
+ L.setOptions(this, options);
+ },
+
+ onAdd: function () {
+ this._initContainer();
+
+ this._levels = {};
+ this._tiles = {};
+
+ this._resetView();
+ this._update();
+ },
+
+ beforeAdd: function (map) {
+ map._addZoomLimit(this);
+ },
+
+ onRemove: function (map) {
+ this._removeAllTiles();
+ L.DomUtil.remove(this._container);
+ map._removeZoomLimit(this);
+ this._container = null;
+ this._tileZoom = null;
+ },
+
+ // @method bringToFront: this
+ // Brings the tile layer to the top of all tile layers.
+ bringToFront: function () {
+ if (this._map) {
+ L.DomUtil.toFront(this._container);
+ this._setAutoZIndex(Math.max);
+ }
+ return this;
+ },
+
+ // @method bringToBack: this
+ // Brings the tile layer to the bottom of all tile layers.
+ bringToBack: function () {
+ if (this._map) {
+ L.DomUtil.toBack(this._container);
+ this._setAutoZIndex(Math.min);
+ }
+ return this;
+ },
+
+ // @method getContainer: HTMLElement
+ // Returns the HTML element that contains the tiles for this layer.
+ getContainer: function () {
+ return this._container;
+ },
+
+ // @method setOpacity(opacity: Number): this
+ // Changes the [opacity](#gridlayer-opacity) of the grid layer.
+ setOpacity: function (opacity) {
+ this.options.opacity = opacity;
+ this._updateOpacity();
+ return this;
+ },
+
+ // @method setZIndex(zIndex: Number): this
+ // Changes the [zIndex](#gridlayer-zindex) of the grid layer.
+ setZIndex: function (zIndex) {
+ this.options.zIndex = zIndex;
+ this._updateZIndex();
+
+ return this;
+ },
+
+ // @method isLoading: Boolean
+ // Returns `true` if any tile in the grid layer has not finished loading.
+ isLoading: function () {
+ return this._loading;
+ },
+
+ // @method redraw: this
+ // Causes the layer to clear all the tiles and request them again.
+ redraw: function () {
+ if (this._map) {
+ this._removeAllTiles();
+ this._update();
+ }
+ return this;
+ },
+
+ getEvents: function () {
+ var events = {
+ viewprereset: this._invalidateAll,
+ viewreset: this._resetView,
+ zoom: this._resetView,
+ moveend: this._onMoveEnd
+ };
+
+ if (!this.options.updateWhenIdle) {
+ // update tiles on move, but not more often than once per given interval
+ if (!this._onMove) {
+ this._onMove = L.Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
+ }
+
+ events.move = this._onMove;
+ }
+
+ if (this._zoomAnimated) {
+ events.zoomanim = this._animateZoom;
+ }
+
+ return events;
+ },
+
+ // @section Extension methods
+ // Layers extending `GridLayer` shall reimplement the following method.
+ // @method createTile(coords: Object, done?: Function): HTMLElement
+ // Called only internally, must be overriden by classes extending `GridLayer`.
+ // Returns the `HTMLElement` corresponding to the given `coords`. If the `done` callback
+ // is specified, it must be called when the tile has finished loading and drawing.
+ createTile: function () {
+ return document.createElement('div');
+ },
+
+ // @section
+ // @method getTileSize: Point
+ // Normalizes the [tileSize option](#gridlayer-tilesize) into a point. Used by the `createTile()` method.
+ getTileSize: function () {
+ var s = this.options.tileSize;
+ return s instanceof L.Point ? s : new L.Point(s, s);
+ },
+
+ _updateZIndex: function () {
+ if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
+ 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 () {
+ if (!this._map) { return; }
+
+ // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
+ if (L.Browser.ielt9) { return; }
+
+ L.DomUtil.setOpacity(this._container, this.options.opacity);
+
+ var now = +new Date(),
+ nextFrame = false,
+ willPrune = false;
+
+ for (var key in this._tiles) {
+ var tile = this._tiles[key];
+ if (!tile.current || !tile.loaded) { continue; }
+
+ var fade = Math.min(1, (now - tile.loaded) / 200);
+
+ L.DomUtil.setOpacity(tile.el, fade);
+ if (fade < 1) {
+ nextFrame = true;
+ } else {
+ if (tile.active) { willPrune = true; }
+ tile.active = true;
+ }
+ }
+
+ if (willPrune && !this._noPrune) { this._pruneTiles(); }
+
+ if (nextFrame) {
+ L.Util.cancelAnimFrame(this._fadeFrame);
+ this._fadeFrame = L.Util.requestAnimFrame(this._updateOpacity, this);
+ }
+ },
+
+ _initContainer: function () {
+ if (this._container) { return; }
+
+ this._container = L.DomUtil.create('div', 'leaflet-layer ' + (this.options.className || ''));
+ this._updateZIndex();
+
+ if (this.options.opacity < 1) {
+ this._updateOpacity();
+ }
+
+ this.getPane().appendChild(this._container);
+ },
+
+ _updateLevels: function () {
+
+ var zoom = this._tileZoom,
+ maxZoom = this.options.maxZoom;
+
+ if (zoom === undefined) { return undefined; }
+
+ for (var z in this._levels) {
+ if (this._levels[z].el.children.length || z === zoom) {
+ this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
+ } else {
+ L.DomUtil.remove(this._levels[z].el);
+ this._removeTilesAtZoom(z);
+ delete this._levels[z];
+ }
+ }
+
+ var level = this._levels[zoom],
+ map = this._map;
+
+ if (!level) {
+ level = this._levels[zoom] = {};
+
+ level.el = L.DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
+ level.el.style.zIndex = maxZoom;
+
+ level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
+ level.zoom = zoom;
+
+ this._setZoomTransform(level, map.getCenter(), map.getZoom());
+
+ // force the browser to consider the newly added element for transition
+ L.Util.falseFn(level.el.offsetWidth);
+ }
+
+ this._level = level;
+
+ return level;
+ },
+
+ _pruneTiles: function () {
+ if (!this._map) {
+ return;
+ }
+
+ var key, tile;
+
+ var zoom = this._map.getZoom();
+ if (zoom > this.options.maxZoom ||
+ zoom < this.options.minZoom) {
+ this._removeAllTiles();
+ return;
+ }
+
+ for (key in this._tiles) {
+ tile = this._tiles[key];
+ tile.retain = tile.current;
+ }
+
+ for (key in this._tiles) {
+ tile = this._tiles[key];
+ if (tile.current && !tile.active) {
+ var coords = tile.coords;
+ if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) {
+ this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2);
+ }
+ }
+ }
+
+ for (key in this._tiles) {
+ if (!this._tiles[key].retain) {
+ this._removeTile(key);
+ }
+ }
+ },
+
+ _removeTilesAtZoom: function (zoom) {
+ for (var key in this._tiles) {
+ if (this._tiles[key].coords.z !== zoom) {
+ continue;
+ }
+ this._removeTile(key);
+ }
+ },
+
+ _removeAllTiles: function () {
+ for (var key in this._tiles) {
+ this._removeTile(key);
+ }
+ },
+
+ _invalidateAll: function () {
+ for (var z in this._levels) {
+ L.DomUtil.remove(this._levels[z].el);
+ delete this._levels[z];
+ }
+ this._removeAllTiles();
+
+ this._tileZoom = null;
+ },
+
+ _retainParent: function (x, y, z, minZoom) {
+ var x2 = Math.floor(x / 2),
+ y2 = Math.floor(y / 2),
+ z2 = z - 1,
+ coords2 = new L.Point(+x2, +y2);
+ coords2.z = +z2;
+
+ var key = this._tileCoordsToKey(coords2),
+ tile = this._tiles[key];
+
+ if (tile && tile.active) {
+ tile.retain = true;
+ return true;
+
+ } else if (tile && tile.loaded) {
+ tile.retain = true;
+ }
+
+ if (z2 > minZoom) {
+ return this._retainParent(x2, y2, z2, minZoom);
+ }
+
+ return false;
+ },
+
+ _retainChildren: function (x, y, z, maxZoom) {
+
+ for (var i = 2 * x; i < 2 * x + 2; i++) {
+ for (var j = 2 * y; j < 2 * y + 2; j++) {
+
+ var coords = new L.Point(i, j);
+ coords.z = z + 1;
+
+ var key = this._tileCoordsToKey(coords),
+ tile = this._tiles[key];
+
+ if (tile && tile.active) {
+ tile.retain = true;
+ continue;
+
+ } else if (tile && tile.loaded) {
+ tile.retain = true;
+ }
+
+ if (z + 1 < maxZoom) {
+ this._retainChildren(i, j, z + 1, maxZoom);
+ }
+ }
+ }
+ },
+
+ _resetView: function (e) {
+ var animating = e && (e.pinch || e.flyTo);
+ this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
+ },
+
+ _animateZoom: function (e) {
+ this._setView(e.center, e.zoom, true, e.noUpdate);
+ },
+
+ _setView: function (center, zoom, noPrune, noUpdate) {
+ var tileZoom = Math.round(zoom);
+ if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
+ (this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
+ tileZoom = undefined;
+ }
+
+ var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom);
+
+ if (!noUpdate || tileZoomChanged) {
+
+ this._tileZoom = tileZoom;
+
+ if (this._abortLoading) {
+ this._abortLoading();
+ }
+
+ this._updateLevels();
+ this._resetGrid();
+
+ if (tileZoom !== undefined) {
+ this._update(center);
+ }
+
+ if (!noPrune) {
+ this._pruneTiles();
+ }
+
+ // Flag to prevent _updateOpacity from pruning tiles during
+ // a zoom anim or a pinch gesture
+ this._noPrune = !!noPrune;
+ }
+
+ this._setZoomTransforms(center, zoom);
+ },
+
+ _setZoomTransforms: function (center, zoom) {
+ for (var i in this._levels) {
+ this._setZoomTransform(this._levels[i], center, zoom);
+ }
+ },
+
+ _setZoomTransform: function (level, center, zoom) {
+ var scale = this._map.getZoomScale(zoom, level.zoom),
+ translate = level.origin.multiplyBy(scale)
+ .subtract(this._map._getNewPixelOrigin(center, zoom)).round();
+
+ if (L.Browser.any3d) {
+ L.DomUtil.setTransform(level.el, translate, scale);
+ } else {
+ L.DomUtil.setPosition(level.el, translate);
+ }
+ },
+
+ _resetGrid: function () {
+ var map = this._map,
+ crs = map.options.crs,
+ tileSize = this._tileSize = this.getTileSize(),
+ tileZoom = this._tileZoom;
+
+ var bounds = this._map.getPixelWorldBounds(this._tileZoom);
+ if (bounds) {
+ this._globalTileRange = this._pxBoundsToTileRange(bounds);
+ }
+
+ this._wrapX = crs.wrapLng && !this.options.noWrap && [
+ Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x),
+ Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)
+ ];
+ this._wrapY = crs.wrapLat && !this.options.noWrap && [
+ Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x),
+ Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)
+ ];
+ },
+
+ _onMoveEnd: function () {
+ if (!this._map || this._map._animatingZoom) { return; }
+
+ this._update();
+ },
+
+ _getTiledPixelBounds: function (center) {
+ var map = this._map,
+ mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
+ scale = map.getZoomScale(mapZoom, this._tileZoom),
+ pixelCenter = map.project(center, this._tileZoom).floor(),
+ halfSize = map.getSize().divideBy(scale * 2);
+
+ return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
+ },
+
+ // Private method to load tiles in the grid's active zoom level according to map bounds
+ _update: function (center) {
+ var map = this._map;
+ if (!map) { return; }
+ var zoom = map.getZoom();
+
+ if (center === undefined) { center = map.getCenter(); }
+ if (this._tileZoom === undefined) { return; } // if out of minzoom/maxzoom
+
+ var pixelBounds = this._getTiledPixelBounds(center),
+ tileRange = this._pxBoundsToTileRange(pixelBounds),
+ tileCenter = tileRange.getCenter(),
+ queue = [],
+ margin = this.options.keepBuffer,
+ noPruneRange = new L.Bounds(tileRange.getBottomLeft().subtract([margin, -margin]),
+ tileRange.getTopRight().add([margin, -margin]));
+
+ for (var key in this._tiles) {
+ var c = this._tiles[key].coords;
+ if (c.z !== this._tileZoom || !noPruneRange.contains(L.point(c.x, c.y))) {
+ this._tiles[key].current = false;
+ }
+ }
+
+ // _update just loads more tiles. If the tile zoom level differs too much
+ // from the map's, let _setView reset levels and prune old tiles.
+ if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; }
+
+ // create a queue of coordinates to load tiles from
+ for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
+ for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
+ var coords = new L.Point(i, j);
+ coords.z = this._tileZoom;
+
+ if (!this._isValidTile(coords)) { continue; }
+
+ var tile = this._tiles[this._tileCoordsToKey(coords)];
+ if (tile) {
+ tile.current = true;
+ } else {
+ queue.push(coords);
+ }
+ }
+ }
+
+ // sort tile queue to load tiles in order of their distance to center
+ queue.sort(function (a, b) {
+ return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
+ });
+
+ if (queue.length !== 0) {
+ // if it's the first batch of tiles to load
+ if (!this._loading) {
+ this._loading = true;
+ // @event loading: Event
+ // Fired when the grid layer starts loading tiles.
+ this.fire('loading');
+ }
+
+ // create DOM fragment to append tiles in one batch
+ var fragment = document.createDocumentFragment();
+
+ for (i = 0; i < queue.length; i++) {
+ this._addTile(queue[i], fragment);
+ }
+
+ this._level.el.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._globalTileRange;
+ 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).overlaps(tileBounds);
+ },
+
+ _keyToBounds: function (key) {
+ return this._tileCoordsToBounds(this._keyToTileCoords(key));
+ },
+
+ // converts tile coordinates to its geographical bounds
+ _tileCoordsToBounds: function (coords) {
+
+ var map = this._map,
+ tileSize = this.getTileSize(),
+
+ nwPoint = coords.scaleBy(tileSize),
+ sePoint = nwPoint.add(tileSize),
+
+ nw = map.unproject(nwPoint, coords.z),
+ se = map.unproject(sePoint, coords.z),
+ bounds = new L.LatLngBounds(nw, se);
+
+ if (!this.options.noWrap) {
+ map.wrapLatLngBounds(bounds);
+ }
+
+ return bounds;
+ },
+
+ // converts tile coordinates to key for the tile cache
+ _tileCoordsToKey: function (coords) {
+ return coords.x + ':' + coords.y + ':' + coords.z;
+ },
+
+ // converts tile cache key to coordinates
+ _keyToTileCoords: function (key) {
+ var k = key.split(':'),
+ coords = new L.Point(+k[0], +k[1]);
+ coords.z = +k[2];
+ return coords;
+ },
+
+ _removeTile: function (key) {
+ var tile = this._tiles[key];
+ if (!tile) { return; }
+
+ L.DomUtil.remove(tile.el);
+
+ delete this._tiles[key];
+
+ // @event tileunload: TileEvent
+ // Fired when a tile is removed (e.g. when a tile goes off the screen).
+ this.fire('tileunload', {
+ tile: tile.el,
+ coords: this._keyToTileCoords(key)
+ });
+ },
+
+ _initTile: function (tile) {
+ L.DomUtil.addClass(tile, 'leaflet-tile');
+
+ var tileSize = this.getTileSize();
+ tile.style.width = tileSize.x + 'px';
+ tile.style.height = tileSize.y + '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),
+ key = this._tileCoordsToKey(coords);
+
+ var tile = this.createTile(this._wrapCoords(coords), L.bind(this._tileReady, this, coords));
+
+ 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
+ L.Util.requestAnimFrame(L.bind(this._tileReady, this, coords, null, tile));
+ }
+
+ L.DomUtil.setPosition(tile, tilePos);
+
+ // save tile in cache
+ this._tiles[key] = {
+ el: tile,
+ coords: coords,
+ current: true
+ };
+
+ container.appendChild(tile);
+ // @event tileloadstart: TileEvent
+ // Fired when a tile is requested and starts loading.
+ this.fire('tileloadstart', {
+ tile: tile,
+ coords: coords
+ });
+ },
+
+ _tileReady: function (coords, err, tile) {
+ if (!this._map) { return; }
+
+ if (err) {
+ // @event tileerror: TileErrorEvent
+ // Fired when there is an error loading a tile.
+ this.fire('tileerror', {
+ error: err,
+ tile: tile,
+ coords: coords
+ });
+ }
+
+ var key = this._tileCoordsToKey(coords);
+
+ tile = this._tiles[key];
+ if (!tile) { return; }
+
+ tile.loaded = +new Date();
+ if (this._map._fadeAnimated) {
+ L.DomUtil.setOpacity(tile.el, 0);
+ L.Util.cancelAnimFrame(this._fadeFrame);
+ this._fadeFrame = L.Util.requestAnimFrame(this._updateOpacity, this);
+ } else {
+ tile.active = true;
+ this._pruneTiles();
+ }
+
+ if (!err) {
+ L.DomUtil.addClass(tile.el, 'leaflet-tile-loaded');
+
+ // @event tileload: TileEvent
+ // Fired when a tile loads.
+ this.fire('tileload', {
+ tile: tile.el,
+ coords: coords
+ });
+ }
+
+ if (this._noTilesToLoad()) {
+ this._loading = false;
+ // @event load: Event
+ // Fired when the grid layer loaded all visible tiles.
+ this.fire('load');
+
+ if (L.Browser.ielt9 || !this._map._fadeAnimated) {
+ L.Util.requestAnimFrame(this._pruneTiles, this);
+ } else {
+ // Wait a bit more than 0.2 secs (the duration of the tile fade-in)
+ // to trigger a pruning.
+ setTimeout(L.bind(this._pruneTiles, this), 250);
+ }
+ }
+ },
+
+ _getTilePos: function (coords) {
+ return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
+ },
+
+ _wrapCoords: function (coords) {
+ var newCoords = new L.Point(
+ this._wrapX ? L.Util.wrapNum(coords.x, this._wrapX) : coords.x,
+ this._wrapY ? L.Util.wrapNum(coords.y, this._wrapY) : coords.y);
+ newCoords.z = coords.z;
+ return newCoords;
+ },
+
+ _pxBoundsToTileRange: function (bounds) {
+ var tileSize = this.getTileSize();
+ return new L.Bounds(
+ bounds.min.unscaleBy(tileSize).floor(),
+ bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
+ },
+
+ _noTilesToLoad: function () {
+ for (var key in this._tiles) {
+ if (!this._tiles[key].loaded) { return false; }
+ }
+ return true;
+ }
+});
+
+// @factory L.gridLayer(options?: GridLayer options)
+// Creates a new instance of GridLayer with the supplied options.
+L.gridLayer = function (options) {
+ return new L.GridLayer(options);
+};
+
+
+
+/*
+ * @class TileLayer
+ * @inherits GridLayer
+ * @aka L.TileLayer
+ * Used to load and display tile layers on the map. Extends `GridLayer`.
+ *
+ * @example
+ *
+ * ```js
+ * L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?{foo}', {foo: 'bar'}).addTo(map);
+ * ```
+ *
+ * @section URL template
+ * @example
+ *
+ * A string of the following form:
+ *
+ * ```
+ * 'http://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png'
+ * ```
+ *
+ * `{s}` means one of the available subdomains (used sequentially to help with browser parallel requests per domain limitation; subdomain values are specified in options; `a`, `b` or `c` by default, can be omitted), `{z}` — zoom level, `{x}` and `{y}` — tile coordinates. `{r}` can be used to add @2x to the URL to load retina tiles.
+ *
+ * You can use custom keys in the template, which will be [evaluated](#util-template) from TileLayer options, like this:
+ *
+ * ```
+ * L.tileLayer('http://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png', {foo: 'bar'});
+ * ```
+ */
+
+
+L.TileLayer = L.GridLayer.extend({
+
+ // @section
+ // @aka TileLayer options
+ options: {
+ // @option minZoom: Number = 0
+ // Minimum zoom number.
+ minZoom: 0,
+
+ // @option maxZoom: Number = 18
+ // Maximum zoom number.
+ maxZoom: 18,
+
+ // @option maxNativeZoom: Number = null
+ // Maximum zoom number the tile source has available. If it is specified,
+ // the tiles on all zoom levels higher than `maxNativeZoom` will be loaded
+ // from `maxNativeZoom` level and auto-scaled.
+ maxNativeZoom: null,
+
+ // @option minNativeZoom: Number = null
+ // Minimum zoom number the tile source has available. If it is specified,
+ // the tiles on all zoom levels lower than `minNativeZoom` will be loaded
+ // from `minNativeZoom` level and auto-scaled.
+ minNativeZoom: null,
+
+ // @option subdomains: String|String[] = 'abc'
+ // Subdomains of the tile service. Can be passed in the form of one string (where each letter is a subdomain name) or an array of strings.
+ subdomains: 'abc',
+
+ // @option errorTileUrl: String = ''
+ // URL to the tile image to show in place of the tile that failed to load.
+ errorTileUrl: '',
+
+ // @option zoomOffset: Number = 0
+ // The zoom number used in tile URLs will be offset with this value.
+ zoomOffset: 0,
+
+ // @option tms: Boolean = false
+ // If `true`, inverses Y axis numbering for tiles (turn this on for [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services).
+ tms: false,
+
+ // @option zoomReverse: Boolean = false
+ // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`)
+ zoomReverse: false,
+
+ // @option detectRetina: Boolean = false
+ // If `true` and user is on a retina display, it will request four tiles of half the specified size and a bigger zoom level in place of one to utilize the high resolution.
+ detectRetina: false,
+
+ // @option crossOrigin: Boolean = false
+ // If true, all tiles will have their crossOrigin attribute set to ''. This is needed if you want to access tile pixel data.
+ crossOrigin: false
+ },
+
+ 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);
+
+ if (!options.zoomReverse) {
+ options.zoomOffset++;
+ options.maxZoom--;
+ } else {
+ options.zoomOffset--;
+ options.minZoom++;
+ }
+
+ options.minZoom = Math.max(0, options.minZoom);
+ }
+
+ if (typeof options.subdomains === 'string') {
+ options.subdomains = options.subdomains.split('');
+ }
+
+ // for https://github.com/Leaflet/Leaflet/issues/137
+ if (!L.Browser.android) {
+ this.on('tileunload', this._onTileRemove);
+ }
+ },
+
+ // @method setUrl(url: String, noRedraw?: Boolean): this
+ // Updates the layer's URL template and redraws it (unless `noRedraw` is set to `true`).
+ setUrl: function (url, noRedraw) {
+ this._url = url;
+
+ if (!noRedraw) {
+ this.redraw();
+ }
+ return this;
+ },
+
+ // @method createTile(coords: Object, done?: Function): HTMLElement
+ // Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile)
+ // to return an `<img>` HTML element with the appropiate image URL given `coords`. The `done`
+ // callback is called when the tile has been loaded.
+ createTile: function (coords, done) {
+ var tile = document.createElement('img');
+
+ L.DomEvent.on(tile, 'load', L.bind(this._tileOnLoad, this, done, tile));
+ L.DomEvent.on(tile, 'error', L.bind(this._tileOnError, this, done, tile));
+
+ if (this.options.crossOrigin) {
+ tile.crossOrigin = '';
+ }
+
+ /*
+ 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 = '';
+
+ /*
+ Set role="presentation" to force screen readers to ignore this
+ https://www.w3.org/TR/wai-aria/roles#textalternativecomputation
+ */
+ tile.setAttribute('role', 'presentation');
+
+ tile.src = this.getTileUrl(coords);
+
+ return tile;
+ },
+
+ // @section Extension methods
+ // @uninheritable
+ // Layers extending `TileLayer` might reimplement the following method.
+ // @method getTileUrl(coords: Object): String
+ // Called only internally, returns the URL for a tile given its coordinates.
+ // Classes extending `TileLayer` can override this function to provide custom tile URL naming schemes.
+ getTileUrl: function (coords) {
+ var data = {
+ r: L.Browser.retina ? '@2x' : '',
+ s: this._getSubdomain(coords),
+ x: coords.x,
+ y: coords.y,
+ z: this._getZoomForUrl()
+ };
+ if (this._map && !this._map.options.crs.infinite) {
+ var invertedY = this._globalTileRange.max.y - coords.y;
+ if (this.options.tms) {
+ data['y'] = invertedY;
+ }
+ data['-y'] = invertedY;
+ }
+
+ return L.Util.template(this._url, L.extend(data, this.options));
+ },
+
+ _tileOnLoad: function (done, tile) {
+ // For https://github.com/Leaflet/Leaflet/issues/3332
+ if (L.Browser.ielt9) {
+ setTimeout(L.bind(done, this, null, tile), 0);
+ } else {
+ done(null, tile);
+ }
+ },
+
+ _tileOnError: function (done, tile, e) {
+ var errorUrl = this.options.errorTileUrl;
+ if (errorUrl && tile.src !== errorUrl) {
+ tile.src = errorUrl;
+ }
+ done(e, tile);
+ },
+
+ getTileSize: function () {
+ var map = this._map,
+ tileSize = L.GridLayer.prototype.getTileSize.call(this),
+ zoom = this._tileZoom + this.options.zoomOffset,
+ minNativeZoom = this.options.minNativeZoom,
+ maxNativeZoom = this.options.maxNativeZoom;
+
+ // decrease tile size when scaling below minNativeZoom
+ if (minNativeZoom !== null && zoom < minNativeZoom) {
+ return tileSize.divideBy(map.getZoomScale(minNativeZoom, zoom)).round();
+ }
+
+ // increase tile size when scaling above maxNativeZoom
+ if (maxNativeZoom !== null && zoom > maxNativeZoom) {
+ return tileSize.divideBy(map.getZoomScale(maxNativeZoom, zoom)).round();
+ }
+
+ return tileSize;
+ },
+
+ _onTileRemove: function (e) {
+ e.tile.onload = null;
+ },
+
+ _getZoomForUrl: function () {
+ var zoom = this._tileZoom,
+ maxZoom = this.options.maxZoom,
+ zoomReverse = this.options.zoomReverse,
+ zoomOffset = this.options.zoomOffset,
+ minNativeZoom = this.options.minNativeZoom,
+ maxNativeZoom = this.options.maxNativeZoom;
+
+ if (zoomReverse) {
+ zoom = maxZoom - zoom;
+ }
+
+ zoom += zoomOffset;
+
+ if (minNativeZoom !== null && zoom < minNativeZoom) {
+ return minNativeZoom;
+ }
+
+ if (maxNativeZoom !== null && zoom > maxNativeZoom) {
+ return maxNativeZoom;
+ }
+
+ return 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) {
+ if (this._tiles[i].coords.z !== this._tileZoom) {
+ tile = this._tiles[i].el;
+
+ tile.onload = L.Util.falseFn;
+ tile.onerror = L.Util.falseFn;
+
+ if (!tile.complete) {
+ tile.src = L.Util.emptyImageUrl;
+ L.DomUtil.remove(tile);
+ }
+ }
+ }
+ }
+});
+
+
+// @factory L.tilelayer(urlTemplate: String, options?: TileLayer options)
+// Instantiates a tile layer object given a `URL template` and optionally an options object.
+
+L.tileLayer = function (url, options) {
+ return new L.TileLayer(url, options);
+};
+
+
+
+/*
+ * @class TileLayer.WMS
+ * @inherits TileLayer
+ * @aka L.TileLayer.WMS
+ * Used to display [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services as tile layers on the map. Extends `TileLayer`.
+ *
+ * @example
+ *
+ * ```js
+ * var nexrad = L.tileLayer.wms("http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", {
+ * layers: 'nexrad-n0r-900913',
+ * format: 'image/png',
+ * transparent: true,
+ * attribution: "Weather data © 2012 IEM Nexrad"
+ * });
+ * ```
+ */
+
+L.TileLayer.WMS = L.TileLayer.extend({
+
+ // @section
+ // @aka TileLayer.WMS options
+ // If any custom options not documented here are used, they will be sent to the
+ // WMS server as extra parameters in each request URL. This can be useful for
+ // [non-standard vendor WMS parameters](http://docs.geoserver.org/stable/en/user/services/wms/vendor.html).
+ defaultWmsParams: {
+ service: 'WMS',
+ request: 'GetMap',
+
+ // @option layers: String = ''
+ // **(required)** Comma-separated list of WMS layers to show.
+ layers: '',
+
+ // @option styles: String = ''
+ // Comma-separated list of WMS styles.
+ styles: '',
+
+ // @option format: String = 'image/jpeg'
+ // WMS image format (use `'image/png'` for layers with transparency).
+ format: 'image/jpeg',
+
+ // @option transparent: Boolean = false
+ // If `true`, the WMS service will return images with transparency.
+ transparent: false,
+
+ // @option version: String = '1.1.1'
+ // Version of the WMS service to use
+ version: '1.1.1'
+ },
+
+ options: {
+ // @option crs: CRS = null
+ // Coordinate Reference System to use for the WMS requests, defaults to
+ // map CRS. Don't change this if you're not sure what it means.
+ crs: null,
+
+ // @option uppercase: Boolean = false
+ // If `true`, WMS request parameter keys will be uppercase.
+ uppercase: 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 (!(i in this.options)) {
+ 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.TileLayer.prototype.getTileUrl.call(this, coords);
+
+ return url +
+ L.Util.getParamString(this.wmsParams, url, this.options.uppercase) +
+ (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox;
+ },
+
+ // @method setParams(params: Object, noRedraw?: Boolean): this
+ // Merges an object with the new parameters and re-requests tiles on the current screen (unless `noRedraw` was set to true).
+ setParams: function (params, noRedraw) {
+
+ L.extend(this.wmsParams, params);
+
+ if (!noRedraw) {
+ this.redraw();
+ }
+
+ return this;
+ }
+});
+
+
+// @factory L.tileLayer.wms(baseUrl: String, options: TileLayer.WMS options)
+// Instantiates a WMS tile layer object given a base URL of the WMS service and a WMS parameters/options object.
+L.tileLayer.wms = function (url, options) {
+ return new L.TileLayer.WMS(url, options);
+};
+
+
+
+/*
+ * @class ImageOverlay
+ * @aka L.ImageOverlay
+ * @inherits Interactive layer
+ *
+ * Used to load and display a single image over specific bounds of the map. Extends `Layer`.
+ *
+ * @example
+ *
+ * ```js
+ * var imageUrl = 'http://www.lib.utexas.edu/maps/historical/newark_nj_1922.jpg',
+ * imageBounds = [[40.712216, -74.22655], [40.773941, -74.12544]];
+ * L.imageOverlay(imageUrl, imageBounds).addTo(map);
+ * ```
+ */
+
+L.ImageOverlay = L.Layer.extend({
+
+ // @section
+ // @aka ImageOverlay options
+ options: {
+ // @option opacity: Number = 1.0
+ // The opacity of the image overlay.
+ opacity: 1,
+
+ // @option alt: String = ''
+ // Text for the `alt` attribute of the image (useful for accessibility).
+ alt: '',
+
+ // @option interactive: Boolean = false
+ // If `true`, the image overlay will emit [mouse events](#interactive-layer) when clicked or hovered.
+ interactive: false,
+
+ // @option crossOrigin: Boolean = false
+ // If true, the image will have its crossOrigin attribute set to ''. This is needed if you want to access image pixel data.
+ crossOrigin: false
+ },
+
+ 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();
+ }
+ }
+
+ if (this.options.interactive) {
+ L.DomUtil.addClass(this._image, 'leaflet-interactive');
+ this.addInteractiveTarget(this._image);
+ }
+
+ this.getPane().appendChild(this._image);
+ this._reset();
+ },
+
+ onRemove: function () {
+ L.DomUtil.remove(this._image);
+ if (this.options.interactive) {
+ this.removeInteractiveTarget(this._image);
+ }
+ },
+
+ // @method setOpacity(opacity: Number): this
+ // Sets the opacity of the overlay.
+ setOpacity: function (opacity) {
+ this.options.opacity = opacity;
+
+ if (this._image) {
+ this._updateOpacity();
+ }
+ return this;
+ },
+
+ setStyle: function (styleOpts) {
+ if (styleOpts.opacity) {
+ this.setOpacity(styleOpts.opacity);
+ }
+ return this;
+ },
+
+ // @method bringToFront(): this
+ // Brings the layer to the top of all overlays.
+ bringToFront: function () {
+ if (this._map) {
+ L.DomUtil.toFront(this._image);
+ }
+ return this;
+ },
+
+ // @method bringToBack(): this
+ // Brings the layer to the bottom of all overlays.
+ bringToBack: function () {
+ if (this._map) {
+ L.DomUtil.toBack(this._image);
+ }
+ return this;
+ },
+
+ // @method setUrl(url: String): this
+ // Changes the URL of the image.
+ setUrl: function (url) {
+ this._url = url;
+
+ if (this._image) {
+ this._image.src = url;
+ }
+ return this;
+ },
+
+ // @method setBounds(bounds: LatLngBounds): this
+ // Update the bounds that this ImageOverlay covers
+ setBounds: function (bounds) {
+ this._bounds = bounds;
+
+ if (this._map) {
+ this._reset();
+ }
+ return this;
+ },
+
+ getEvents: function () {
+ var events = {
+ zoom: this._reset,
+ viewreset: this._reset
+ };
+
+ if (this._zoomAnimated) {
+ events.zoomanim = this._animateZoom;
+ }
+
+ return events;
+ },
+
+ // @method getBounds(): LatLngBounds
+ // Get the bounds that this ImageOverlay covers
+ getBounds: function () {
+ return this._bounds;
+ },
+
+ // @method getElement(): HTMLElement
+ // Get the img element that represents the ImageOverlay on the map
+ getElement: function () {
+ return this._image;
+ },
+
+ _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');
+
+ if (this.options.crossOrigin) {
+ img.crossOrigin = '';
+ }
+
+ img.src = this._url;
+ img.alt = this.options.alt;
+ },
+
+ _animateZoom: function (e) {
+ var scale = this._map.getZoomScale(e.zoom),
+ offset = this._map._latLngBoundsToNewLayerBounds(this._bounds, e.zoom, e.center).min;
+
+ L.DomUtil.setTransform(this._image, offset, 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);
+ }
+});
+
+// @factory L.imageOverlay(imageUrl: String, bounds: LatLngBounds, options?: ImageOverlay options)
+// Instantiates an image overlay object given the URL of the image and the
+// geographical bounds it is tied to.
+L.imageOverlay = function (url, bounds, options) {
+ return new L.ImageOverlay(url, bounds, options);
+};
+
+
+
+/*
+ * @class Icon
+ * @aka L.Icon
+ * @inherits Layer
+ *
+ * Represents an icon to provide when creating a marker.
+ *
+ * @example
+ *
+ * ```js
+ * var myIcon = L.icon({
+ * iconUrl: 'my-icon.png',
+ * iconRetinaUrl: 'my-icon@2x.png',
+ * iconSize: [38, 95],
+ * iconAnchor: [22, 94],
+ * popupAnchor: [-3, -76],
+ * shadowUrl: 'my-icon-shadow.png',
+ * shadowRetinaUrl: 'my-icon-shadow@2x.png',
+ * shadowSize: [68, 95],
+ * shadowAnchor: [22, 94]
+ * });
+ *
+ * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
+ * ```
+ *
+ * `L.Icon.Default` extends `L.Icon` and is the blue icon Leaflet uses for markers by default.
+ *
+ */
+
+L.Icon = L.Class.extend({
+
+ /* @section
+ * @aka Icon options
+ *
+ * @option iconUrl: String = null
+ * **(required)** The URL to the icon image (absolute or relative to your script path).
+ *
+ * @option iconRetinaUrl: String = null
+ * The URL to a retina sized version of the icon image (absolute or relative to your
+ * script path). Used for Retina screen devices.
+ *
+ * @option iconSize: Point = null
+ * Size of the icon image in pixels.
+ *
+ * @option iconAnchor: Point = null
+ * The coordinates of the "tip" of the icon (relative to its top left corner). The icon
+ * will be aligned so that this point is at the marker's geographical location. Centered
+ * by default if size is specified, also can be set in CSS with negative margins.
+ *
+ * @option popupAnchor: Point = null
+ * The coordinates of the point from which popups will "open", relative to the icon anchor.
+ *
+ * @option shadowUrl: String = null
+ * The URL to the icon shadow image. If not specified, no shadow image will be created.
+ *
+ * @option shadowRetinaUrl: String = null
+ *
+ * @option shadowSize: Point = null
+ * Size of the shadow image in pixels.
+ *
+ * @option shadowAnchor: Point = null
+ * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same
+ * as iconAnchor if not specified).
+ *
+ * @option className: String = ''
+ * A custom class name to assign to both icon and shadow images. Empty by default.
+ */
+
+ initialize: function (options) {
+ L.setOptions(this, options);
+ },
+
+ // @method createIcon(oldIcon?: HTMLElement): HTMLElement
+ // Called internally when the icon has to be shown, returns a `<img>` HTML element
+ // styled according to the options.
+ createIcon: function (oldIcon) {
+ return this._createIcon('icon', oldIcon);
+ },
+
+ // @method createShadow(oldIcon?: HTMLElement): HTMLElement
+ // As `createIcon`, but for the shadow beneath it.
+ 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;
+ var sizeOption = options[name + 'Size'];
+
+ if (typeof sizeOption === 'number') {
+ sizeOption = [sizeOption, sizeOption];
+ }
+
+ var size = L.point(sizeOption),
+ 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'];
+ }
+});
+
+
+// @factory L.icon(options: Icon options)
+// Creates an icon instance with the given options.
+L.icon = function (options) {
+ return new L.Icon(options);
+};
+
+
+
+/*
+ * @miniclass Icon.Default (Icon)
+ * @aka L.Icon.Default
+ * @section
+ *
+ * A trivial subclass of `Icon`, represents the icon to use in `Marker`s when
+ * no icon is specified. Points to the blue marker image distributed with Leaflet
+ * releases.
+ *
+ * In order to customize the default icon, just change the properties of `L.Icon.Default.prototype.options`
+ * (which is a set of `Icon options`).
+ *
+ * If you want to _completely_ replace the default icon, override the
+ * `L.Marker.prototype.options.icon` with your own icon instead.
+ */
+
+L.Icon.Default = L.Icon.extend({
+
+ options: {
+ iconUrl: 'marker-icon.png',
+ iconRetinaUrl: 'marker-icon-2x.png',
+ shadowUrl: 'marker-shadow.png',
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+ tooltipAnchor: [16, -28],
+ shadowSize: [41, 41]
+ },
+
+ _getIconUrl: function (name) {
+ if (!L.Icon.Default.imagePath) { // Deprecated, backwards-compatibility only
+ L.Icon.Default.imagePath = this._detectIconPath();
+ }
+
+ // @option imagePath: String
+ // `L.Icon.Default` will try to auto-detect the absolute location of the
+ // blue icon images. If you are placing these images in a non-standard
+ // way, set this option to point to the right absolute path.
+ return (this.options.imagePath || L.Icon.Default.imagePath) + L.Icon.prototype._getIconUrl.call(this, name);
+ },
+
+ _detectIconPath: function () {
+ var el = L.DomUtil.create('div', 'leaflet-default-icon-path', document.body);
+ var path = L.DomUtil.getStyle(el, 'background-image') ||
+ L.DomUtil.getStyle(el, 'backgroundImage'); // IE8
+
+ document.body.removeChild(el);
+
+ return path.indexOf('url') === 0 ?
+ path.replace(/^url\([\"\']?/, '').replace(/marker-icon\.png[\"\']?\)$/, '') : '';
+ }
+});
+
+
+
+/*
+ * @class Marker
+ * @inherits Interactive layer
+ * @aka L.Marker
+ * L.Marker is used to display clickable/draggable icons on the map. Extends `Layer`.
+ *
+ * @example
+ *
+ * ```js
+ * L.marker([50.5, 30.5]).addTo(map);
+ * ```
+ */
+
+L.Marker = L.Layer.extend({
+
+ // @section
+ // @aka Marker options
+ options: {
+ // @option icon: Icon = *
+ // Icon class to use for rendering the marker. See [Icon documentation](#L.Icon) for details on how to customize the marker icon. If not specified, a new `L.Icon.Default` is used.
+ icon: new L.Icon.Default(),
+
+ // Option inherited from "Interactive layer" abstract class
+ interactive: true,
+
+ // @option draggable: Boolean = false
+ // Whether the marker is draggable with mouse/touch or not.
+ draggable: false,
+
+ // @option keyboard: Boolean = true
+ // Whether the marker can be tabbed to with a keyboard and clicked by pressing enter.
+ keyboard: true,
+
+ // @option title: String = ''
+ // Text for the browser tooltip that appear on marker hover (no tooltip by default).
+ title: '',
+
+ // @option alt: String = ''
+ // Text for the `alt` attribute of the icon image (useful for accessibility).
+ alt: '',
+
+ // @option zIndexOffset: Number = 0
+ // By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively).
+ zIndexOffset: 0,
+
+ // @option opacity: Number = 1.0
+ // The opacity of the marker.
+ opacity: 1,
+
+ // @option riseOnHover: Boolean = false
+ // If `true`, the marker will get on top of others when you hover the mouse over it.
+ riseOnHover: false,
+
+ // @option riseOffset: Number = 250
+ // The z-index offset used for the `riseOnHover` feature.
+ riseOffset: 250,
+
+ // @option pane: String = 'markerPane'
+ // `Map pane` where the markers icon will be added.
+ pane: 'markerPane',
+
+ // FIXME: shadowPane is no longer a valid option
+ nonBubblingEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu']
+ },
+
+ /* @section
+ *
+ * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods:
+ */
+
+ initialize: function (latlng, options) {
+ L.setOptions(this, options);
+ this._latlng = L.latLng(latlng);
+ },
+
+ onAdd: function (map) {
+ this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;
+
+ if (this._zoomAnimated) {
+ map.on('zoomanim', this._animateZoom, this);
+ }
+
+ this._initIcon();
+ this.update();
+ },
+
+ onRemove: function (map) {
+ if (this.dragging && this.dragging.enabled()) {
+ this.options.draggable = true;
+ this.dragging.removeHooks();
+ }
+
+ if (this._zoomAnimated) {
+ map.off('zoomanim', this._animateZoom, this);
+ }
+
+ this._removeIcon();
+ this._removeShadow();
+ },
+
+ getEvents: function () {
+ return {
+ zoom: this.update,
+ viewreset: this.update
+ };
+ },
+
+ // @method getLatLng: LatLng
+ // Returns the current geographical position of the marker.
+ getLatLng: function () {
+ return this._latlng;
+ },
+
+ // @method setLatLng(latlng: LatLng): this
+ // Changes the marker position to the given point.
+ setLatLng: function (latlng) {
+ var oldLatLng = this._latlng;
+ this._latlng = L.latLng(latlng);
+ this.update();
+
+ // @event move: Event
+ // Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
+ return this.fire('move', {oldLatLng: oldLatLng, latlng: this._latlng});
+ },
+
+ // @method setZIndexOffset(offset: Number): this
+ // Changes the [zIndex offset](#marker-zindexoffset) of the marker.
+ setZIndexOffset: function (offset) {
+ this.options.zIndexOffset = offset;
+ return this.update();
+ },
+
+ // @method setIcon(icon: Icon): this
+ // Changes the marker icon.
+ setIcon: function (icon) {
+
+ this.options.icon = icon;
+
+ if (this._map) {
+ this._initIcon();
+ this.update();
+ }
+
+ if (this._popup) {
+ this.bindPopup(this._popup, this._popup.options);
+ }
+
+ return this;
+ },
+
+ getElement: function () {
+ return this._icon;
+ },
+
+ 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;
+
+ if (options.riseOnHover) {
+ this.on({
+ mouseover: this._bringToFront,
+ mouseout: this._resetZIndex
+ });
+ }
+
+ var newShadow = options.icon.createShadow(this._shadow),
+ addShadow = false;
+
+ if (newShadow !== this._shadow) {
+ this._removeShadow();
+ addShadow = true;
+ }
+
+ if (newShadow) {
+ L.DomUtil.addClass(newShadow, classToAdd);
+ newShadow.alt = '';
+ }
+ this._shadow = newShadow;
+
+
+ if (options.opacity < 1) {
+ this._updateOpacity();
+ }
+
+
+ if (addIcon) {
+ this.getPane().appendChild(this._icon);
+ }
+ this._initInteraction();
+ if (newShadow && addShadow) {
+ this.getPane('shadowPane').appendChild(this._shadow);
+ }
+ },
+
+ _removeIcon: function () {
+ if (this.options.riseOnHover) {
+ this.off({
+ mouseover: this._bringToFront,
+ mouseout: this._resetZIndex
+ });
+ }
+
+ L.DomUtil.remove(this._icon);
+ this.removeInteractiveTarget(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.interactive) { return; }
+
+ L.DomUtil.addClass(this._icon, 'leaflet-interactive');
+
+ this.addInteractiveTarget(this._icon);
+
+ if (L.Handler.MarkerDrag) {
+ var draggable = this.options.draggable;
+ if (this.dragging) {
+ draggable = this.dragging.enabled();
+ this.dragging.disable();
+ }
+
+ this.dragging = new L.Handler.MarkerDrag(this);
+
+ if (draggable) {
+ this.dragging.enable();
+ }
+ }
+ },
+
+ // @method setOpacity(opacity: Number): this
+ // Changes the opacity of the marker.
+ 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);
+ },
+
+ _getPopupAnchor: function () {
+ return this.options.icon.options.popupAnchor || [0, 0];
+ },
+
+ _getTooltipAnchor: function () {
+ return this.options.icon.options.tooltipAnchor || [0, 0];
+ }
+});
+
+
+// factory L.marker(latlng: LatLng, options? : Marker options)
+
+// @factory L.marker(latlng: LatLng, options? : Marker options)
+// Instantiates a Marker object given a geographical point and optionally an options object.
+L.marker = function (latlng, options) {
+ return new L.Marker(latlng, options);
+};
+
+
+
+/*
+ * @class DivIcon
+ * @aka L.DivIcon
+ * @inherits Icon
+ *
+ * Represents a lightweight icon for markers that uses a simple `<div>`
+ * element instead of an image. Inherits from `Icon` but ignores the `iconUrl` and shadow options.
+ *
+ * @example
+ * ```js
+ * var myIcon = L.divIcon({className: 'my-div-icon'});
+ * // you can set .my-div-icon styles in CSS
+ *
+ * L.marker([50.505, 30.57], {icon: myIcon}).addTo(map);
+ * ```
+ *
+ * By default, it has a 'leaflet-div-icon' CSS class and is styled as a little white square with a shadow.
+ */
+
+L.DivIcon = L.Icon.extend({
+ options: {
+ // @section
+ // @aka DivIcon options
+ iconSize: [12, 12], // also can be set through CSS
+
+ // iconAnchor: (Point),
+ // popupAnchor: (Point),
+
+ // @option html: String = ''
+ // Custom HTML code to put inside the div element, empty by default.
+ html: false,
+
+ // @option bgPos: Point = [0, 0]
+ // Optional relative position of the background, in pixels
+ bgPos: null,
+
+ className: 'leaflet-div-icon'
+ },
+
+ 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) {
+ var bgPos = L.point(options.bgPos);
+ div.style.backgroundPosition = (-bgPos.x) + 'px ' + (-bgPos.y) + 'px';
+ }
+ this._setIconStyles(div, 'icon');
+
+ return div;
+ },
+
+ createShadow: function () {
+ return null;
+ }
+});
+
+// @factory L.divIcon(options: DivIcon options)
+// Creates a `DivIcon` instance with the given options.
+L.divIcon = function (options) {
+ return new L.DivIcon(options);
+};
+
+
+
+/*
+ * @class DivOverlay
+ * @inherits Layer
+ * @aka L.DivOverlay
+ * Base model for L.Popup and L.Tooltip. Inherit from it for custom popup like plugins.
+ */
+
+// @namespace DivOverlay
+L.DivOverlay = L.Layer.extend({
+
+ // @section
+ // @aka DivOverlay options
+ options: {
+ // @option offset: Point = Point(0, 7)
+ // The offset of the popup position. Useful to control the anchor
+ // of the popup when opening it on some overlays.
+ offset: [0, 7],
+
+ // @option className: String = ''
+ // A custom CSS class name to assign to the popup.
+ className: '',
+
+ // @option pane: String = 'popupPane'
+ // `Map pane` where the popup will be added.
+ pane: 'popupPane'
+ },
+
+ initialize: function (options, source) {
+ L.setOptions(this, options);
+
+ this._source = source;
+ },
+
+ onAdd: function (map) {
+ this._zoomAnimated = map._zoomAnimated;
+
+ 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);
+ }
+
+ this.bringToFront();
+ },
+
+ 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);
+ }
+ },
+
+ // @namespace Popup
+ // @method getLatLng: LatLng
+ // Returns the geographical point of popup.
+ getLatLng: function () {
+ return this._latlng;
+ },
+
+ // @method setLatLng(latlng: LatLng): this
+ // Sets the geographical point where the popup will open.
+ setLatLng: function (latlng) {
+ this._latlng = L.latLng(latlng);
+ if (this._map) {
+ this._updatePosition();
+ this._adjustPan();
+ }
+ return this;
+ },
+
+ // @method getContent: String|HTMLElement
+ // Returns the content of the popup.
+ getContent: function () {
+ return this._content;
+ },
+
+ // @method setContent(htmlContent: String|HTMLElement|Function): this
+ // Sets the HTML content of the popup. If a function is passed the source layer will be passed to the function. The function should return a `String` or `HTMLElement` to be used in the popup.
+ setContent: function (content) {
+ this._content = content;
+ this.update();
+ return this;
+ },
+
+ // @method getElement: String|HTMLElement
+ // Alias for [getContent()](#popup-getcontent)
+ getElement: function () {
+ return this._container;
+ },
+
+ // @method update: null
+ // Updates the popup content, layout and position. Useful for updating the popup after something inside changed, e.g. image loaded.
+ 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 = {
+ zoom: this._updatePosition,
+ viewreset: this._updatePosition
+ };
+
+ if (this._zoomAnimated) {
+ events.zoomanim = this._animateZoom;
+ }
+ return events;
+ },
+
+ // @method isOpen: Boolean
+ // Returns `true` when the popup is visible on the map.
+ isOpen: function () {
+ return !!this._map && this._map.hasLayer(this);
+ },
+
+ // @method bringToFront: this
+ // Brings this popup in front of other popups (in the same map pane).
+ bringToFront: function () {
+ if (this._map) {
+ L.DomUtil.toFront(this._container);
+ }
+ return this;
+ },
+
+ // @method bringToBack: this
+ // Brings this popup to the back of other popups (in the same map pane).
+ bringToBack: function () {
+ if (this._map) {
+ L.DomUtil.toBack(this._container);
+ }
+ return this;
+ },
+
+ _updateContent: function () {
+ if (!this._content) { return; }
+
+ var node = this._contentNode;
+ var content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content;
+
+ if (typeof content === 'string') {
+ node.innerHTML = content;
+ } else {
+ while (node.hasChildNodes()) {
+ node.removeChild(node.firstChild);
+ }
+ node.appendChild(content);
+ }
+ this.fire('contentupdate');
+ },
+
+ _updatePosition: function () {
+ if (!this._map) { return; }
+
+ var pos = this._map.latLngToLayerPoint(this._latlng),
+ offset = L.point(this.options.offset),
+ anchor = this._getAnchor();
+
+ if (this._zoomAnimated) {
+ L.DomUtil.setPosition(this._container, pos.add(anchor));
+ } else {
+ offset = offset.add(pos).add(anchor);
+ }
+
+ 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';
+ },
+
+ _getAnchor: function () {
+ return [0, 0];
+ }
+
+});
+
+
+
+/*
+ * @class Popup
+ * @inherits DivOverlay
+ * @aka L.Popup
+ * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to
+ * open popups while making sure that only one popup is open at one time
+ * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want.
+ *
+ * @example
+ *
+ * If you want to just bind a popup to marker click and then open it, it's really easy:
+ *
+ * ```js
+ * marker.bindPopup(popupContent).openPopup();
+ * ```
+ * Path overlays like polylines also have a `bindPopup` method.
+ * Here's a more complicated way to open a popup on a map:
+ *
+ * ```js
+ * var popup = L.popup()
+ * .setLatLng(latlng)
+ * .setContent('<p>Hello world!<br />This is a nice popup.</p>')
+ * .openOn(map);
+ * ```
+ */
+
+
+// @namespace Popup
+L.Popup = L.DivOverlay.extend({
+
+ // @section
+ // @aka Popup options
+ options: {
+ // @option maxWidth: Number = 300
+ // Max width of the popup, in pixels.
+ maxWidth: 300,
+
+ // @option minWidth: Number = 50
+ // Min width of the popup, in pixels.
+ minWidth: 50,
+
+ // @option maxHeight: Number = null
+ // If set, creates a scrollable container of the given height
+ // inside a popup if its content exceeds it.
+ maxHeight: null,
+
+ // @option autoPan: Boolean = true
+ // Set it to `false` if you don't want the map to do panning animation
+ // to fit the opened popup.
+ autoPan: true,
+
+ // @option autoPanPaddingTopLeft: Point = null
+ // The margin between the popup and the top left corner of the map
+ // view after autopanning was performed.
+ autoPanPaddingTopLeft: null,
+
+ // @option autoPanPaddingBottomRight: Point = null
+ // The margin between the popup and the bottom right corner of the map
+ // view after autopanning was performed.
+ autoPanPaddingBottomRight: null,
+
+ // @option autoPanPadding: Point = Point(5, 5)
+ // Equivalent of setting both top left and bottom right autopan padding to the same value.
+ autoPanPadding: [5, 5],
+
+ // @option keepInView: Boolean = false
+ // Set it to `true` if you want to prevent users from panning the popup
+ // off of the screen while it is open.
+ keepInView: false,
+
+ // @option closeButton: Boolean = true
+ // Controls the presence of a close button in the popup.
+ closeButton: true,
+
+ // @option autoClose: Boolean = true
+ // Set it to `false` if you want to override the default behavior of
+ // the popup closing when user clicks the map (set globally by
+ // the Map's [closePopupOnClick](#map-closepopuponclick) option).
+ autoClose: true,
+
+ // @option className: String = ''
+ // A custom CSS class name to assign to the popup.
+ className: ''
+ },
+
+ // @namespace Popup
+ // @method openOn(map: Map): this
+ // Adds the popup to the map and closes the previous one. The same as `map.openPopup(popup)`.
+ openOn: function (map) {
+ map.openPopup(this);
+ return this;
+ },
+
+ onAdd: function (map) {
+ L.DivOverlay.prototype.onAdd.call(this, map);
+
+ // @namespace Map
+ // @section Popup events
+ // @event popupopen: PopupEvent
+ // Fired when a popup is opened in the map
+ map.fire('popupopen', {popup: this});
+
+ if (this._source) {
+ // @namespace Layer
+ // @section Popup events
+ // @event popupopen: PopupEvent
+ // Fired when a popup bound to this layer is opened
+ this._source.fire('popupopen', {popup: this}, true);
+ // For non-path layers, we toggle the popup when clicking
+ // again the layer, so prevent the map to reopen it.
+ if (!(this._source instanceof L.Path)) {
+ this._source.on('preclick', L.DomEvent.stopPropagation);
+ }
+ }
+ },
+
+ onRemove: function (map) {
+ L.DivOverlay.prototype.onRemove.call(this, map);
+
+ // @namespace Map
+ // @section Popup events
+ // @event popupclose: PopupEvent
+ // Fired when a popup in the map is closed
+ map.fire('popupclose', {popup: this});
+
+ if (this._source) {
+ // @namespace Layer
+ // @section Popup events
+ // @event popupclose: PopupEvent
+ // Fired when a popup bound to this layer is closed
+ this._source.fire('popupclose', {popup: this}, true);
+ if (!(this._source instanceof L.Path)) {
+ this._source.off('preclick', L.DomEvent.stopPropagation);
+ }
+ }
+ },
+
+ getEvents: function () {
+ var events = L.DivOverlay.prototype.getEvents.call(this);
+
+ if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
+ events.preclick = this._close;
+ }
+
+ if (this.options.keepInView) {
+ events.moveend = this._adjustPan;
+ }
+
+ return events;
+ },
+
+ _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-animated');
+
+ 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);
+ },
+
+ _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;
+ },
+
+ _animateZoom: function (e) {
+ var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center),
+ anchor = this._getAnchor();
+ L.DomUtil.setPosition(this._container, pos.add(anchor));
+ },
+
+ _adjustPan: function () {
+ if (!this.options.autoPan || (this._map._panAnim && this._map._panAnim._inProgress)) { return; }
+
+ var map = this._map,
+ marginBottom = parseInt(L.DomUtil.getStyle(this._container, 'marginBottom'), 10) || 0,
+ containerHeight = this._container.offsetHeight + marginBottom,
+ containerWidth = this._containerWidth,
+ layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom);
+
+ 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;
+ }
+
+ // @namespace Map
+ // @section Popup events
+ // @event autopanstart: Event
+ // Fired when the map starts autopanning when opening a popup.
+ if (dx || dy) {
+ map
+ .fire('autopanstart')
+ .panBy([dx, dy]);
+ }
+ },
+
+ _onCloseButtonClick: function (e) {
+ this._close();
+ L.DomEvent.stop(e);
+ },
+
+ _getAnchor: function () {
+ // Where should we anchor the popup on the source layer?
+ return L.point(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]);
+ }
+
+});
+
+// @namespace Popup
+// @factory L.popup(options?: Popup options, source?: Layer)
+// Instantiates a `Popup` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the popup with a reference to the Layer to which it refers.
+L.popup = function (options, source) {
+ return new L.Popup(options, source);
+};
+
+
+/* @namespace Map
+ * @section Interaction Options
+ * @option closePopupOnClick: Boolean = true
+ * Set it to `false` if you don't want popups to close when user clicks the map.
+ */
+L.Map.mergeOptions({
+ closePopupOnClick: true
+});
+
+
+// @namespace Map
+// @section Methods for Layers and Controls
+L.Map.include({
+ // @method openPopup(popup: Popup): this
+ // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability).
+ // @alternative
+ // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this
+ // Creates a popup with the specified content and options and opens it in the given point on a map.
+ openPopup: function (popup, latlng, options) {
+ if (!(popup instanceof L.Popup)) {
+ popup = new L.Popup(options).setContent(popup);
+ }
+
+ if (latlng) {
+ popup.setLatLng(latlng);
+ }
+
+ if (this.hasLayer(popup)) {
+ return this;
+ }
+
+ if (this._popup && this._popup.options.autoClose) {
+ this.closePopup();
+ }
+
+ this._popup = popup;
+ return this.addLayer(popup);
+ },
+
+ // @method closePopup(popup?: Popup): this
+ // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one).
+ closePopup: function (popup) {
+ if (!popup || popup === this._popup) {
+ popup = this._popup;
+ this._popup = null;
+ }
+ if (popup) {
+ this.removeLayer(popup);
+ }
+ return this;
+ }
+});
+
+/*
+ * @namespace Layer
+ * @section Popup methods example
+ *
+ * All layers share a set of methods convenient for binding popups to it.
+ *
+ * ```js
+ * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map);
+ * layer.openPopup();
+ * layer.closePopup();
+ * ```
+ *
+ * Popups will also be automatically opened when the layer is clicked on and closed when the layer is removed from the map or another popup is opened.
+ */
+
+// @section Popup methods
+L.Layer.include({
+
+ // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this
+ // Binds a popup to the layer with the passed `content` and sets up the
+ // neccessary event listeners. If a `Function` is passed it will receive
+ // the layer as the first argument and should return a `String` or `HTMLElement`.
+ bindPopup: function (content, options) {
+
+ if (content instanceof L.Popup) {
+ L.setOptions(content, options);
+ 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;
+ },
+
+ // @method unbindPopup(): this
+ // Removes the popup previously bound with `bindPopup`.
+ unbindPopup: function () {
+ if (this._popup) {
+ this.off({
+ click: this._openPopup,
+ remove: this.closePopup,
+ move: this._movePopup
+ });
+ this._popupHandlersAdded = false;
+ this._popup = null;
+ }
+ return this;
+ },
+
+ // @method openPopup(latlng?: LatLng): this
+ // Opens the bound popup at the specificed `latlng` or at the default popup anchor if no `latlng` is passed.
+ openPopup: function (layer, latlng) {
+ if (!(layer instanceof L.Layer)) {
+ latlng = layer;
+ layer = this;
+ }
+
+ if (layer instanceof L.FeatureGroup) {
+ for (var id in this._layers) {
+ layer = this._layers[id];
+ break;
+ }
+ }
+
+ if (!latlng) {
+ latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng();
+ }
+
+ if (this._popup && this._map) {
+ // set popup source to this layer
+ this._popup._source = layer;
+
+ // update the popup (content, layout, ect...)
+ this._popup.update();
+
+ // open the popup on the map
+ this._map.openPopup(this._popup, latlng);
+ }
+
+ return this;
+ },
+
+ // @method closePopup(): this
+ // Closes the popup bound to this layer if it is open.
+ closePopup: function () {
+ if (this._popup) {
+ this._popup._close();
+ }
+ return this;
+ },
+
+ // @method togglePopup(): this
+ // Opens or closes the popup bound to this layer depending on its current state.
+ togglePopup: function (target) {
+ if (this._popup) {
+ if (this._popup._map) {
+ this.closePopup();
+ } else {
+ this.openPopup(target);
+ }
+ }
+ return this;
+ },
+
+ // @method isPopupOpen(): boolean
+ // Returns `true` if the popup bound to this layer is currently open.
+ isPopupOpen: function () {
+ return (this._popup ? this._popup.isOpen() : false);
+ },
+
+ // @method setPopupContent(content: String|HTMLElement|Popup): this
+ // Sets the content of the popup bound to this layer.
+ setPopupContent: function (content) {
+ if (this._popup) {
+ this._popup.setContent(content);
+ }
+ return this;
+ },
+
+ // @method getPopup(): Popup
+ // Returns the popup bound to this layer.
+ getPopup: function () {
+ return this._popup;
+ },
+
+ _openPopup: function (e) {
+ var layer = e.layer || e.target;
+
+ if (!this._popup) {
+ return;
+ }
+
+ if (!this._map) {
+ return;
+ }
+
+ // prevent map click
+ L.DomEvent.stop(e);
+
+ // if this inherits from Path its a vector and we can just
+ // open the popup at the new location
+ if (layer instanceof L.Path) {
+ this.openPopup(e.layer || e.target, e.latlng);
+ return;
+ }
+
+ // otherwise treat it like a marker and figure out
+ // if we should toggle it open/closed
+ if (this._map.hasLayer(this._popup) && this._popup._source === layer) {
+ this.closePopup();
+ } else {
+ this.openPopup(layer, e.latlng);
+ }
+ },
+
+ _movePopup: function (e) {
+ this._popup.setLatLng(e.latlng);
+ }
+});
+
+
+
+/*
+ * @class Tooltip
+ * @inherits DivOverlay
+ * @aka L.Tooltip
+ * Used to display small texts on top of map layers.
+ *
+ * @example
+ *
+ * ```js
+ * marker.bindTooltip("my tooltip text").openTooltip();
+ * ```
+ * Note about tooltip offset. Leaflet takes two options in consideration
+ * for computing tooltip offseting:
+ * - the `offset` Tooltip option: it defaults to [0, 0], and it's specific to one tooltip.
+ * Add a positive x offset to move the tooltip to the right, and a positive y offset to
+ * move it to the bottom. Negatives will move to the left and top.
+ * - the `tooltipAnchor` Icon option: this will only be considered for Marker. You
+ * should adapt this value if you use a custom icon.
+ */
+
+
+// @namespace Tooltip
+L.Tooltip = L.DivOverlay.extend({
+
+ // @section
+ // @aka Tooltip options
+ options: {
+ // @option pane: String = 'tooltipPane'
+ // `Map pane` where the tooltip will be added.
+ pane: 'tooltipPane',
+
+ // @option offset: Point = Point(0, 0)
+ // Optional offset of the tooltip position.
+ offset: [0, 0],
+
+ // @option direction: String = 'auto'
+ // Direction where to open the tooltip. Possible values are: `right`, `left`,
+ // `top`, `bottom`, `center`, `auto`.
+ // `auto` will dynamicaly switch between `right` and `left` according to the tooltip
+ // position on the map.
+ direction: 'auto',
+
+ // @option permanent: Boolean = false
+ // Whether to open the tooltip permanently or only on mouseover.
+ permanent: false,
+
+ // @option sticky: Boolean = false
+ // If true, the tooltip will follow the mouse instead of being fixed at the feature center.
+ sticky: false,
+
+ // @option interactive: Boolean = false
+ // If true, the tooltip will listen to the feature events.
+ interactive: false,
+
+ // @option opacity: Number = 0.9
+ // Tooltip container opacity.
+ opacity: 0.9
+ },
+
+ onAdd: function (map) {
+ L.DivOverlay.prototype.onAdd.call(this, map);
+ this.setOpacity(this.options.opacity);
+
+ // @namespace Map
+ // @section Tooltip events
+ // @event tooltipopen: TooltipEvent
+ // Fired when a tooltip is opened in the map.
+ map.fire('tooltipopen', {tooltip: this});
+
+ if (this._source) {
+ // @namespace Layer
+ // @section Tooltip events
+ // @event tooltipopen: TooltipEvent
+ // Fired when a tooltip bound to this layer is opened.
+ this._source.fire('tooltipopen', {tooltip: this}, true);
+ }
+ },
+
+ onRemove: function (map) {
+ L.DivOverlay.prototype.onRemove.call(this, map);
+
+ // @namespace Map
+ // @section Tooltip events
+ // @event tooltipclose: TooltipEvent
+ // Fired when a tooltip in the map is closed.
+ map.fire('tooltipclose', {tooltip: this});
+
+ if (this._source) {
+ // @namespace Layer
+ // @section Tooltip events
+ // @event tooltipclose: TooltipEvent
+ // Fired when a tooltip bound to this layer is closed.
+ this._source.fire('tooltipclose', {tooltip: this}, true);
+ }
+ },
+
+ getEvents: function () {
+ var events = L.DivOverlay.prototype.getEvents.call(this);
+
+ if (L.Browser.touch && !this.options.permanent) {
+ events.preclick = this._close;
+ }
+
+ return events;
+ },
+
+ _close: function () {
+ if (this._map) {
+ this._map.closeTooltip(this);
+ }
+ },
+
+ _initLayout: function () {
+ var prefix = 'leaflet-tooltip',
+ className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');
+
+ this._contentNode = this._container = L.DomUtil.create('div', className);
+ },
+
+ _updateLayout: function () {},
+
+ _adjustPan: function () {},
+
+ _setPosition: function (pos) {
+ var map = this._map,
+ container = this._container,
+ centerPoint = map.latLngToContainerPoint(map.getCenter()),
+ tooltipPoint = map.layerPointToContainerPoint(pos),
+ direction = this.options.direction,
+ tooltipWidth = container.offsetWidth,
+ tooltipHeight = container.offsetHeight,
+ offset = L.point(this.options.offset),
+ anchor = this._getAnchor();
+
+ if (direction === 'top') {
+ pos = pos.add(L.point(-tooltipWidth / 2 + offset.x, -tooltipHeight + offset.y + anchor.y, true));
+ } else if (direction === 'bottom') {
+ pos = pos.subtract(L.point(tooltipWidth / 2 - offset.x, -offset.y, true));
+ } else if (direction === 'center') {
+ pos = pos.subtract(L.point(tooltipWidth / 2 + offset.x, tooltipHeight / 2 - anchor.y + offset.y, true));
+ } else if (direction === 'right' || direction === 'auto' && tooltipPoint.x < centerPoint.x) {
+ direction = 'right';
+ pos = pos.add(L.point(offset.x + anchor.x, anchor.y - tooltipHeight / 2 + offset.y, true));
+ } else {
+ direction = 'left';
+ pos = pos.subtract(L.point(tooltipWidth + anchor.x - offset.x, tooltipHeight / 2 - anchor.y - offset.y, true));
+ }
+
+ L.DomUtil.removeClass(container, 'leaflet-tooltip-right');
+ L.DomUtil.removeClass(container, 'leaflet-tooltip-left');
+ L.DomUtil.removeClass(container, 'leaflet-tooltip-top');
+ L.DomUtil.removeClass(container, 'leaflet-tooltip-bottom');
+ L.DomUtil.addClass(container, 'leaflet-tooltip-' + direction);
+ L.DomUtil.setPosition(container, pos);
+ },
+
+ _updatePosition: function () {
+ var pos = this._map.latLngToLayerPoint(this._latlng);
+ this._setPosition(pos);
+ },
+
+ setOpacity: function (opacity) {
+ this.options.opacity = opacity;
+
+ if (this._container) {
+ L.DomUtil.setOpacity(this._container, opacity);
+ }
+ },
+
+ _animateZoom: function (e) {
+ var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
+ this._setPosition(pos);
+ },
+
+ _getAnchor: function () {
+ // Where should we anchor the tooltip on the source layer?
+ return L.point(this._source && this._source._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]);
+ }
+
+});
+
+// @namespace Tooltip
+// @factory L.tooltip(options?: Tooltip options, source?: Layer)
+// Instantiates a Tooltip object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the tooltip with a reference to the Layer to which it refers.
+L.tooltip = function (options, source) {
+ return new L.Tooltip(options, source);
+};
+
+// @namespace Map
+// @section Methods for Layers and Controls
+L.Map.include({
+
+ // @method openTooltip(tooltip: Tooltip): this
+ // Opens the specified tooltip.
+ // @alternative
+ // @method openTooltip(content: String|HTMLElement, latlng: LatLng, options?: Tooltip options): this
+ // Creates a tooltip with the specified content and options and open it.
+ openTooltip: function (tooltip, latlng, options) {
+ if (!(tooltip instanceof L.Tooltip)) {
+ tooltip = new L.Tooltip(options).setContent(tooltip);
+ }
+
+ if (latlng) {
+ tooltip.setLatLng(latlng);
+ }
+
+ if (this.hasLayer(tooltip)) {
+ return this;
+ }
+
+ return this.addLayer(tooltip);
+ },
+
+ // @method closeTooltip(tooltip?: Tooltip): this
+ // Closes the tooltip given as parameter.
+ closeTooltip: function (tooltip) {
+ if (tooltip) {
+ this.removeLayer(tooltip);
+ }
+ return this;
+ }
+
+});
+
+/*
+ * @namespace Layer
+ * @section Tooltip methods example
+ *
+ * All layers share a set of methods convenient for binding tooltips to it.
+ *
+ * ```js
+ * var layer = L.Polygon(latlngs).bindTooltip('Hi There!').addTo(map);
+ * layer.openTooltip();
+ * layer.closeTooltip();
+ * ```
+ */
+
+// @section Tooltip methods
+L.Layer.include({
+
+ // @method bindTooltip(content: String|HTMLElement|Function|Tooltip, options?: Tooltip options): this
+ // Binds a tooltip to the layer with the passed `content` and sets up the
+ // neccessary event listeners. If a `Function` is passed it will receive
+ // the layer as the first argument and should return a `String` or `HTMLElement`.
+ bindTooltip: function (content, options) {
+
+ if (content instanceof L.Tooltip) {
+ L.setOptions(content, options);
+ this._tooltip = content;
+ content._source = this;
+ } else {
+ if (!this._tooltip || options) {
+ this._tooltip = L.tooltip(options, this);
+ }
+ this._tooltip.setContent(content);
+
+ }
+
+ this._initTooltipInteractions();
+
+ if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) {
+ this.openTooltip();
+ }
+
+ return this;
+ },
+
+ // @method unbindTooltip(): this
+ // Removes the tooltip previously bound with `bindTooltip`.
+ unbindTooltip: function () {
+ if (this._tooltip) {
+ this._initTooltipInteractions(true);
+ this.closeTooltip();
+ this._tooltip = null;
+ }
+ return this;
+ },
+
+ _initTooltipInteractions: function (remove) {
+ if (!remove && this._tooltipHandlersAdded) { return; }
+ var onOff = remove ? 'off' : 'on',
+ events = {
+ remove: this.closeTooltip,
+ move: this._moveTooltip
+ };
+ if (!this._tooltip.options.permanent) {
+ events.mouseover = this._openTooltip;
+ events.mouseout = this.closeTooltip;
+ if (this._tooltip.options.sticky) {
+ events.mousemove = this._moveTooltip;
+ }
+ if (L.Browser.touch) {
+ events.click = this._openTooltip;
+ }
+ } else {
+ events.add = this._openTooltip;
+ }
+ this[onOff](events);
+ this._tooltipHandlersAdded = !remove;
+ },
+
+ // @method openTooltip(latlng?: LatLng): this
+ // Opens the bound tooltip at the specificed `latlng` or at the default tooltip anchor if no `latlng` is passed.
+ openTooltip: function (layer, latlng) {
+ if (!(layer instanceof L.Layer)) {
+ latlng = layer;
+ layer = this;
+ }
+
+ if (layer instanceof L.FeatureGroup) {
+ for (var id in this._layers) {
+ layer = this._layers[id];
+ break;
+ }
+ }
+
+ if (!latlng) {
+ latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng();
+ }
+
+ if (this._tooltip && this._map) {
+
+ // set tooltip source to this layer
+ this._tooltip._source = layer;
+
+ // update the tooltip (content, layout, ect...)
+ this._tooltip.update();
+
+ // open the tooltip on the map
+ this._map.openTooltip(this._tooltip, latlng);
+
+ // Tooltip container may not be defined if not permanent and never
+ // opened.
+ if (this._tooltip.options.interactive && this._tooltip._container) {
+ L.DomUtil.addClass(this._tooltip._container, 'leaflet-clickable');
+ this.addInteractiveTarget(this._tooltip._container);
+ }
+ }
+
+ return this;
+ },
+
+ // @method closeTooltip(): this
+ // Closes the tooltip bound to this layer if it is open.
+ closeTooltip: function () {
+ if (this._tooltip) {
+ this._tooltip._close();
+ if (this._tooltip.options.interactive && this._tooltip._container) {
+ L.DomUtil.removeClass(this._tooltip._container, 'leaflet-clickable');
+ this.removeInteractiveTarget(this._tooltip._container);
+ }
+ }
+ return this;
+ },
+
+ // @method toggleTooltip(): this
+ // Opens or closes the tooltip bound to this layer depending on its current state.
+ toggleTooltip: function (target) {
+ if (this._tooltip) {
+ if (this._tooltip._map) {
+ this.closeTooltip();
+ } else {
+ this.openTooltip(target);
+ }
+ }
+ return this;
+ },
+
+ // @method isTooltipOpen(): boolean
+ // Returns `true` if the tooltip bound to this layer is currently open.
+ isTooltipOpen: function () {
+ return this._tooltip.isOpen();
+ },
+
+ // @method setTooltipContent(content: String|HTMLElement|Tooltip): this
+ // Sets the content of the tooltip bound to this layer.
+ setTooltipContent: function (content) {
+ if (this._tooltip) {
+ this._tooltip.setContent(content);
+ }
+ return this;
+ },
+
+ // @method getTooltip(): Tooltip
+ // Returns the tooltip bound to this layer.
+ getTooltip: function () {
+ return this._tooltip;
+ },
+
+ _openTooltip: function (e) {
+ var layer = e.layer || e.target;
+
+ if (!this._tooltip || !this._map) {
+ return;
+ }
+ this.openTooltip(layer, this._tooltip.options.sticky ? e.latlng : undefined);
+ },
+
+ _moveTooltip: function (e) {
+ var latlng = e.latlng, containerPoint, layerPoint;
+ if (this._tooltip.options.sticky && e.originalEvent) {
+ containerPoint = this._map.mouseEventToContainerPoint(e.originalEvent);
+ layerPoint = this._map.containerPointToLayerPoint(containerPoint);
+ latlng = this._map.layerPointToLatLng(layerPoint);
+ }
+ this._tooltip.setLatLng(latlng);
+ }
+});
+
+
+
+/*
+ * @class LayerGroup
+ * @aka L.LayerGroup
+ * @inherits Layer
+ *
+ * Used to group several layers and handle them as one. If you add it to the map,
+ * any layers added or removed from the group will be added/removed on the map as
+ * well. Extends `Layer`.
+ *
+ * @example
+ *
+ * ```js
+ * L.layerGroup([marker1, marker2])
+ * .addLayer(polyline)
+ * .addTo(map);
+ * ```
+ */
+
+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]);
+ }
+ }
+ },
+
+ // @method addLayer(layer: Layer): this
+ // Adds the given layer to the group.
+ addLayer: function (layer) {
+ var id = this.getLayerId(layer);
+
+ this._layers[id] = layer;
+
+ if (this._map) {
+ this._map.addLayer(layer);
+ }
+
+ return this;
+ },
+
+ // @method removeLayer(layer: Layer): this
+ // Removes the given layer from the group.
+ // @alternative
+ // @method removeLayer(id: Number): this
+ // Removes the layer with the given internal ID from the group.
+ 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;
+ },
+
+ // @method hasLayer(layer: Layer): Boolean
+ // Returns `true` if the given layer is currently added to the group.
+ hasLayer: function (layer) {
+ return !!layer && (layer in this._layers || this.getLayerId(layer) in this._layers);
+ },
+
+ // @method clearLayers(): this
+ // Removes all the layers from the group.
+ clearLayers: function () {
+ for (var i in this._layers) {
+ this.removeLayer(this._layers[i]);
+ }
+ return this;
+ },
+
+ // @method invoke(methodName: String, …): this
+ // Calls `methodName` on every layer contained in this group, passing any
+ // additional parameters. Has no effect if the layers contained do not
+ // implement `methodName`.
+ 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]);
+ }
+ },
+
+ // @method eachLayer(fn: Function, context?: Object): this
+ // Iterates over the layers of the group, optionally specifying context of the iterator function.
+ // ```js
+ // group.eachLayer(function (layer) {
+ // layer.bindPopup('Hello');
+ // });
+ // ```
+ eachLayer: function (method, context) {
+ for (var i in this._layers) {
+ method.call(context, this._layers[i]);
+ }
+ return this;
+ },
+
+ // @method getLayer(id: Number): Layer
+ // Returns the layer with the given internal ID.
+ getLayer: function (id) {
+ return this._layers[id];
+ },
+
+ // @method getLayers(): Layer[]
+ // Returns an array of all the layers added to the group.
+ getLayers: function () {
+ var layers = [];
+
+ for (var i in this._layers) {
+ layers.push(this._layers[i]);
+ }
+ return layers;
+ },
+
+ // @method setZIndex(zIndex: Number): this
+ // Calls `setZIndex` on every layer contained in this group, passing the z-index.
+ setZIndex: function (zIndex) {
+ return this.invoke('setZIndex', zIndex);
+ },
+
+ // @method getLayerId(layer: Layer): Number
+ // Returns the internal ID for a layer
+ getLayerId: function (layer) {
+ return L.stamp(layer);
+ }
+});
+
+
+// @factory L.layerGroup(layers: Layer[])
+// Create a layer group, optionally given an initial set of layers.
+L.layerGroup = function (layers) {
+ return new L.LayerGroup(layers);
+};
+
+
+
+/*
+ * @class FeatureGroup
+ * @aka L.FeatureGroup
+ * @inherits LayerGroup
+ *
+ * Extended `LayerGroup` that makes it easier to do the same thing to all its member layers:
+ * * [`bindPopup`](#layer-bindpopup) binds a popup to all of the layers at once (likewise with [`bindTooltip`](#layer-bindtooltip))
+ * * Events are propagated to the `FeatureGroup`, so if the group has an event
+ * handler, it will handle events from any of the layers. This includes mouse events
+ * and custom events.
+ * * Has `layeradd` and `layerremove` events
+ *
+ * @example
+ *
+ * ```js
+ * L.featureGroup([marker1, marker2, polyline])
+ * .bindPopup('Hello world!')
+ * .on('click', function() { alert('Clicked on a member of the group!'); })
+ * .addTo(map);
+ * ```
+ */
+
+L.FeatureGroup = L.LayerGroup.extend({
+
+ addLayer: function (layer) {
+ if (this.hasLayer(layer)) {
+ return this;
+ }
+
+ layer.addEventParent(this);
+
+ L.LayerGroup.prototype.addLayer.call(this, layer);
+
+ // @event layeradd: LayerEvent
+ // Fired when a layer is added to this `FeatureGroup`
+ 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);
+
+ // @event layerremove: LayerEvent
+ // Fired when a layer is removed from this `FeatureGroup`
+ return this.fire('layerremove', {layer: layer});
+ },
+
+ // @method setStyle(style: Path options): this
+ // Sets the given path options to each layer of the group that has a `setStyle` method.
+ setStyle: function (style) {
+ return this.invoke('setStyle', style);
+ },
+
+ // @method bringToFront(): this
+ // Brings the layer group to the top of all other layers
+ bringToFront: function () {
+ return this.invoke('bringToFront');
+ },
+
+ // @method bringToBack(): this
+ // Brings the layer group to the top of all other layers
+ bringToBack: function () {
+ return this.invoke('bringToBack');
+ },
+
+ // @method getBounds(): LatLngBounds
+ // Returns the LatLngBounds of the Feature Group (created from bounds and coordinates of its children).
+ getBounds: function () {
+ var bounds = new L.LatLngBounds();
+
+ for (var id in this._layers) {
+ var layer = this._layers[id];
+ bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng());
+ }
+ return bounds;
+ }
+});
+
+// @factory L.featureGroup(layers: Layer[])
+// Create a feature group, optionally given an initial set of layers.
+L.featureGroup = function (layers) {
+ return new L.FeatureGroup(layers);
+};
+
+
+
+/*
+ * @class Renderer
+ * @inherits Layer
+ * @aka L.Renderer
+ *
+ * Base class for vector renderer implementations (`SVG`, `Canvas`). Handles the
+ * DOM container of the renderer, its bounds, and its zoom animation.
+ *
+ * A `Renderer` works as an implicit layer group for all `Path`s - the renderer
+ * itself can be added or removed to the map. All paths use a renderer, which can
+ * be implicit (the map will decide the type of renderer and use it automatically)
+ * or explicit (using the [`renderer`](#path-renderer) option of the path).
+ *
+ * Do not use this class directly, use `SVG` and `Canvas` instead.
+ *
+ * @event update: Event
+ * Fired when the renderer updates its bounds, center and zoom, for example when
+ * its map has moved
+ */
+
+L.Renderer = L.Layer.extend({
+
+ // @section
+ // @aka Renderer options
+ options: {
+ // @option padding: Number = 0.1
+ // 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
+ padding: 0.1
+ },
+
+ initialize: function (options) {
+ L.setOptions(this, options);
+ L.stamp(this);
+ this._layers = this._layers || {};
+ },
+
+ 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();
+ this.on('update', this._updatePaths, this);
+ },
+
+ onRemove: function () {
+ L.DomUtil.remove(this._container);
+ this.off('update', this._updatePaths, this);
+ },
+
+ getEvents: function () {
+ var events = {
+ viewreset: this._reset,
+ zoom: this._onZoom,
+ moveend: this._update,
+ zoomend: this._onZoomEnd
+ };
+ if (this._zoomAnimated) {
+ events.zoomanim = this._onAnimZoom;
+ }
+ return events;
+ },
+
+ _onAnimZoom: function (ev) {
+ this._updateTransform(ev.center, ev.zoom);
+ },
+
+ _onZoom: function () {
+ this._updateTransform(this._map.getCenter(), this._map.getZoom());
+ },
+
+ _updateTransform: function (center, zoom) {
+ var scale = this._map.getZoomScale(zoom, this._zoom),
+ position = L.DomUtil.getPosition(this._container),
+ viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding),
+ currentCenterPoint = this._map.project(this._center, zoom),
+ destCenterPoint = this._map.project(center, zoom),
+ centerOffset = destCenterPoint.subtract(currentCenterPoint),
+
+ topLeftOffset = viewHalf.multiplyBy(-scale).add(position).add(viewHalf).subtract(centerOffset);
+
+ if (L.Browser.any3d) {
+ L.DomUtil.setTransform(this._container, topLeftOffset, scale);
+ } else {
+ L.DomUtil.setPosition(this._container, topLeftOffset);
+ }
+ },
+
+ _reset: function () {
+ this._update();
+ this._updateTransform(this._center, this._zoom);
+
+ for (var id in this._layers) {
+ this._layers[id]._reset();
+ }
+ },
+
+ _onZoomEnd: function () {
+ for (var id in this._layers) {
+ this._layers[id]._project();
+ }
+ },
+
+ _updatePaths: function () {
+ for (var id in this._layers) {
+ this._layers[id]._update();
+ }
+ },
+
+ _update: function () {
+ // Update pixel bounds of renderer container (for positioning/sizing/clipping later)
+ // Subclasses are responsible of firing the 'update' event.
+ 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());
+
+ this._center = this._map.getCenter();
+ this._zoom = this._map.getZoom();
+ }
+});
+
+
+L.Map.include({
+ // @namespace Map; @method getRenderer(layer: Path): Renderer
+ // Returns the instance of `Renderer` that should be used to render the given
+ // `Path`. It will ensure that the `renderer` options of the map and paths
+ // are respected, and that the renderers do exist on the map.
+ getRenderer: function (layer) {
+ // @namespace Path; @option renderer: Renderer
+ // Use this specific instance of `Renderer` for this path. Takes
+ // precedence over the map's [default renderer](#map-renderer).
+ var renderer = layer.options.renderer || this._getPaneRenderer(layer.options.pane) || this.options.renderer || this._renderer;
+
+ if (!renderer) {
+ // @namespace Map; @option preferCanvas: Boolean = false
+ // Whether `Path`s should be rendered on a `Canvas` renderer.
+ // By default, all `Path`s are rendered in a `SVG` renderer.
+ renderer = this._renderer = (this.options.preferCanvas && L.canvas()) || L.svg();
+ }
+
+ if (!this.hasLayer(renderer)) {
+ this.addLayer(renderer);
+ }
+ return renderer;
+ },
+
+ _getPaneRenderer: function (name) {
+ if (name === 'overlayPane' || name === undefined) {
+ return false;
+ }
+
+ var renderer = this._paneRenderers[name];
+ if (renderer === undefined) {
+ renderer = (L.SVG && L.svg({pane: name})) || (L.Canvas && L.canvas({pane: name}));
+ this._paneRenderers[name] = renderer;
+ }
+ return renderer;
+ }
+});
+
+
+
+/*
+ * @class Path
+ * @aka L.Path
+ * @inherits Interactive layer
+ *
+ * An abstract class that contains options and constants shared between vector
+ * overlays (Polygon, Polyline, Circle). Do not use it directly. Extends `Layer`.
+ */
+
+L.Path = L.Layer.extend({
+
+ // @section
+ // @aka Path options
+ options: {
+ // @option stroke: Boolean = true
+ // Whether to draw stroke along the path. Set it to `false` to disable borders on polygons or circles.
+ stroke: true,
+
+ // @option color: String = '#3388ff'
+ // Stroke color
+ color: '#3388ff',
+
+ // @option weight: Number = 3
+ // Stroke width in pixels
+ weight: 3,
+
+ // @option opacity: Number = 1.0
+ // Stroke opacity
+ opacity: 1,
+
+ // @option lineCap: String= 'round'
+ // A string that defines [shape to be used at the end](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linecap) of the stroke.
+ lineCap: 'round',
+
+ // @option lineJoin: String = 'round'
+ // A string that defines [shape to be used at the corners](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-linejoin) of the stroke.
+ lineJoin: 'round',
+
+ // @option dashArray: String = null
+ // A string that defines the stroke [dash pattern](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dasharray). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility).
+ dashArray: null,
+
+ // @option dashOffset: String = null
+ // A string that defines the [distance into the dash pattern to start the dash](https://developer.mozilla.org/docs/Web/SVG/Attribute/stroke-dashoffset). Doesn't work on `Canvas`-powered layers in [some old browsers](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility).
+ dashOffset: null,
+
+ // @option fill: Boolean = depends
+ // Whether to fill the path with color. Set it to `false` to disable filling on polygons or circles.
+ fill: false,
+
+ // @option fillColor: String = *
+ // Fill color. Defaults to the value of the [`color`](#path-color) option
+ fillColor: null,
+
+ // @option fillOpacity: Number = 0.2
+ // Fill opacity.
+ fillOpacity: 0.2,
+
+ // @option fillRule: String = 'evenodd'
+ // A string that defines [how the inside of a shape](https://developer.mozilla.org/docs/Web/SVG/Attribute/fill-rule) is determined.
+ fillRule: 'evenodd',
+
+ // className: '',
+
+ // Option inherited from "Interactive layer" abstract class
+ interactive: true
+ },
+
+ beforeAdd: function (map) {
+ // Renderer is set here because we need to call renderer.getEvents
+ // before this.getEvents.
+ this._renderer = map.getRenderer(this);
+ },
+
+ onAdd: function () {
+ this._renderer._initPath(this);
+ this._reset();
+ this._renderer._addPath(this);
+ },
+
+ onRemove: function () {
+ this._renderer._removePath(this);
+ },
+
+ // @method redraw(): this
+ // Redraws the layer. Sometimes useful after you changed the coordinates that the path uses.
+ redraw: function () {
+ if (this._map) {
+ this._renderer._updatePath(this);
+ }
+ return this;
+ },
+
+ // @method setStyle(style: Path options): this
+ // Changes the appearance of a Path based on the options in the `Path options` object.
+ setStyle: function (style) {
+ L.setOptions(this, style);
+ if (this._renderer) {
+ this._renderer._updateStyle(this);
+ }
+ return this;
+ },
+
+ // @method bringToFront(): this
+ // Brings the layer to the top of all path layers.
+ bringToFront: function () {
+ if (this._renderer) {
+ this._renderer._bringToFront(this);
+ }
+ return this;
+ },
+
+ // @method bringToBack(): this
+ // Brings the layer to the bottom of all path layers.
+ bringToBack: function () {
+ if (this._renderer) {
+ this._renderer._bringToBack(this);
+ }
+ return this;
+ },
+
+ getElement: function () {
+ return this._path;
+ },
+
+ _reset: function () {
+ // defined in children classes
+ this._project();
+ this._update();
+ },
+
+ _clickTolerance: function () {
+ // used when doing hit detection for Canvas layers
+ return (this.options.stroke ? this.options.weight / 2 : 0) + (L.Browser.touch ? 10 : 0);
+ }
+});
+
+
+
+/*
+ * @namespace LineUtil
+ *
+ * Various utility functions for polyine points processing, used by Leaflet internally to make polylines lightning-fast.
+ */
+
+L.LineUtil = {
+
+ // Simplify polyline with vertex reduction and Douglas-Peucker simplification.
+ // Improves rendering performance dramatically by lessening the number of points to draw.
+
+ // @function simplify(points: Point[], tolerance: Number): Point[]
+ // Dramatically reduces the number of points in a polyline while retaining
+ // its shape and returns a new array of simplified points, using the
+ // [Douglas-Peucker algorithm](http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm).
+ // Used for a huge performance boost when processing/displaying Leaflet polylines for
+ // each zoom level and also reducing visual noise. tolerance affects the amount of
+ // simplification (lesser value means higher quality but slower and with more points).
+ // Also released as a separated micro-library [Simplify.js](http://mourner.github.com/simplify-js/).
+ simplify: function (points, 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;
+ },
+
+ // @function pointToSegmentDistance(p: Point, p1: Point, p2: Point): Number
+ // Returns the distance between point `p` and segment `p1` to `p2`.
+ pointToSegmentDistance: function (p, p1, p2) {
+ return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true));
+ },
+
+ // @function closestPointOnSegment(p: Point, p1: Point, p2: Point): Number
+ // Returns the closest point from a point `p` on a segment `p1` to `p2`.
+ closestPointOnSegment: function (p, p1, 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;
+ },
+
+
+ // @function clipSegment(a: Point, b: Point, bounds: Bounds, useLastCode?: Boolean, round?: Boolean): Point[]|Boolean
+ // Clips the segment a to b by rectangular bounds with the
+ // [Cohen-Sutherland algorithm](https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm)
+ // (modifying the segment points directly!). Used by Leaflet to only show polyline
+ // points that are on the screen or near, increasing performance.
+ clipSegment: function (a, b, bounds, useLastCode, round) {
+ 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)
+ if (codeA & codeB) {
+ return false;
+ }
+
+ // other cases
+ codeOut = codeA || codeB;
+ p = this._getEdgeIntersection(a, b, codeOut, bounds, round);
+ newCode = this._getBitCode(p, bounds);
+
+ if (codeOut === codeA) {
+ a = p;
+ codeA = newCode;
+ } else {
+ b = p;
+ codeB = newCode;
+ }
+ }
+ },
+
+ _getEdgeIntersection: function (a, b, code, bounds, round) {
+ 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, round);
+ },
+
+ _getBitCode: function (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);
+ }
+};
+
+
+
+/*
+ * @class Polyline
+ * @aka L.Polyline
+ * @inherits Path
+ *
+ * A class for drawing polyline overlays on a map. Extends `Path`.
+ *
+ * @example
+ *
+ * ```js
+ * // create a red polyline from an array of LatLng points
+ * var latlngs = [
+ * [45.51, -122.68],
+ * [37.77, -122.43],
+ * [34.04, -118.2]
+ * ];
+ *
+ * var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map);
+ *
+ * // zoom the map to the polyline
+ * map.fitBounds(polyline.getBounds());
+ * ```
+ *
+ * You can also pass a multi-dimensional array to represent a `MultiPolyline` shape:
+ *
+ * ```js
+ * // create a red polyline from an array of arrays of LatLng points
+ * var latlngs = [
+ * [[45.51, -122.68],
+ * [37.77, -122.43],
+ * [34.04, -118.2]],
+ * [[40.78, -73.91],
+ * [41.83, -87.62],
+ * [32.76, -96.72]]
+ * ];
+ * ```
+ */
+
+L.Polyline = L.Path.extend({
+
+ // @section
+ // @aka Polyline options
+ options: {
+ // @option smoothFactor: Number = 1.0
+ // How much to simplify the polyline on each zoom level. More means
+ // better performance and smoother look, and less means more accurate representation.
+ smoothFactor: 1.0,
+
+ // @option noClip: Boolean = false
+ // Disable polyline clipping.
+ noClip: false
+ },
+
+ initialize: function (latlngs, options) {
+ L.setOptions(this, options);
+ this._setLatLngs(latlngs);
+ },
+
+ // @method getLatLngs(): LatLng[]
+ // Returns an array of the points in the path, or nested arrays of points in case of multi-polyline.
+ getLatLngs: function () {
+ return this._latlngs;
+ },
+
+ // @method setLatLngs(latlngs: LatLng[]): this
+ // Replaces all the points in the polyline with the given array of geographical points.
+ setLatLngs: function (latlngs) {
+ this._setLatLngs(latlngs);
+ return this.redraw();
+ },
+
+ // @method isEmpty(): Boolean
+ // Returns `true` if the Polyline has no LatLngs.
+ isEmpty: function () {
+ return !this._latlngs.length;
+ },
+
+ 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;
+ },
+
+ // @method getCenter(): LatLng
+ // Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the polyline.
+ getCenter: function () {
+ // throws error when not yet added to map as this center calculation requires projected coordinates
+ if (!this._map) {
+ throw new Error('Must add layer to map before using getCenter()');
+ }
+
+ var i, halfDist, segDist, dist, p1, p2, ratio,
+ points = this._rings[0],
+ len = points.length;
+
+ if (!len) { return null; }
+
+ // 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;
+ }
+
+ // The line is so small in the current view that all points are on the same pixel.
+ if (halfDist === 0) {
+ return this._map.layerPointToLatLng(points[0]);
+ }
+
+ 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)
+ ]);
+ }
+ }
+ },
+
+ // @method getBounds(): LatLngBounds
+ // Returns the `LatLngBounds` of the path.
+ getBounds: function () {
+ return this._bounds;
+ },
+
+ // @method addLatLng(latlng: LatLng, latlngs? LatLng[]): this
+ // Adds a given point to the polyline. By default, adds to the first ring of
+ // the polyline in case of a multi-polyline, but can be overridden by passing
+ // a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)).
+ addLatLng: function (latlng, latlngs) {
+ latlngs = latlngs || this._defaultShape();
+ latlng = L.latLng(latlng);
+ latlngs.push(latlng);
+ this._bounds.extend(latlng);
+ return this.redraw();
+ },
+
+ _setLatLngs: function (latlngs) {
+ this._bounds = new L.LatLngBounds();
+ this._latlngs = this._convertLatLngs(latlngs);
+ },
+
+ _defaultShape: function () {
+ return L.Polyline._flat(this._latlngs) ? this._latlngs : this._latlngs[0];
+ },
+
+ // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way
+ _convertLatLngs: function (latlngs) {
+ var result = [],
+ flat = L.Polyline._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;
+ },
+
+ _project: function () {
+ var pxBounds = new L.Bounds();
+ this._rings = [];
+ this._projectLatlngs(this._latlngs, this._rings, pxBounds);
+
+ var w = this._clickTolerance(),
+ p = new L.Point(w, w);
+
+ if (this._bounds.isValid() && pxBounds.isValid()) {
+ pxBounds.min._subtract(p);
+ pxBounds.max._add(p);
+ this._pxBounds = pxBounds;
+ }
+ },
+
+ // recursively turns latlngs into a set of rings with projected coordinates
+ _projectLatlngs: function (latlngs, result, projectedBounds) {
+ 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]);
+ projectedBounds.extend(ring[i]);
+ }
+ result.push(ring);
+ } else {
+ for (i = 0; i < len; i++) {
+ this._projectLatlngs(latlngs[i], result, projectedBounds);
+ }
+ }
+ },
+
+ // clip polyline by renderer bounds so that we have less to render for performance
+ _clipPoints: function () {
+ var bounds = this._renderer._bounds;
+
+ this._parts = [];
+ if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
+ return;
+ }
+
+ if (this.options.noClip) {
+ this._parts = this._rings;
+ return;
+ }
+
+ var parts = this._parts,
+ 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, true);
+
+ 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);
+ }
+});
+
+// @factory L.polyline(latlngs: LatLng[], options?: Polyline options)
+// Instantiates a polyline object given an array of geographical points and
+// optionally an options object. You can create a `Polyline` object with
+// multiple separate lines (`MultiPolyline`) by passing an array of arrays
+// of geographic points.
+L.polyline = function (latlngs, options) {
+ return new L.Polyline(latlngs, options);
+};
+
+L.Polyline._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' && typeof latlngs[0][0] !== 'undefined');
+};
+
+
+
+/*
+ * @namespace PolyUtil
+ * Various utility functions for polygon geometries.
+ */
+
+L.PolyUtil = {};
+
+/* @function clipPolygon(points: Point[], bounds: Bounds, round?: Boolean): Point[]
+ * Clips the polygon geometry defined by the given `points` by the given bounds (using the [Sutherland-Hodgeman algorithm](https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm)).
+ * Used by Leaflet to only show polygon points that are on the screen or near, increasing
+ * performance. Note that polygon points needs different algorithm for clipping
+ * than polyline, so there's a seperate method for it.
+ */
+L.PolyUtil.clipPolygon = function (points, bounds, round) {
+ 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, round);
+ 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, round);
+ p._code = lu._getBitCode(p, bounds);
+ clippedPoints.push(p);
+ }
+ }
+ points = clippedPoints;
+ }
+
+ return points;
+};
+
+
+
+/*
+ * @class Polygon
+ * @aka L.Polygon
+ * @inherits Polyline
+ *
+ * A class for drawing polygon overlays on a map. Extends `Polyline`.
+ *
+ * Note that points you pass when creating a polygon shouldn't have an additional last point equal to the first one — it's better to filter out such points.
+ *
+ *
+ * @example
+ *
+ * ```js
+ * // create a red polygon from an array of LatLng points
+ * var latlngs = [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]];
+ *
+ * var polygon = L.polygon(latlngs, {color: 'red'}).addTo(map);
+ *
+ * // zoom the map to the polygon
+ * map.fitBounds(polygon.getBounds());
+ * ```
+ *
+ * You can also pass an array of arrays of latlngs, with the first array representing the outer shape and the other arrays representing holes in the outer shape:
+ *
+ * ```js
+ * var latlngs = [
+ * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
+ * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
+ * ];
+ * ```
+ *
+ * Additionally, you can pass a multi-dimensional array to represent a MultiPolygon shape.
+ *
+ * ```js
+ * var latlngs = [
+ * [ // first polygon
+ * [[37, -109.05],[41, -109.03],[41, -102.05],[37, -102.04]], // outer ring
+ * [[37.29, -108.58],[40.71, -108.58],[40.71, -102.50],[37.29, -102.50]] // hole
+ * ],
+ * [ // second polygon
+ * [[41, -111.03],[45, -111.04],[45, -104.05],[41, -104.05]]
+ * ]
+ * ];
+ * ```
+ */
+
+L.Polygon = L.Polyline.extend({
+
+ options: {
+ fill: true
+ },
+
+ isEmpty: function () {
+ return !this._latlngs.length || !this._latlngs[0].length;
+ },
+
+ getCenter: function () {
+ // throws error when not yet added to map as this center calculation requires projected coordinates
+ if (!this._map) {
+ throw new Error('Must add layer to map before using getCenter()');
+ }
+
+ var i, j, p1, p2, f, area, x, y, center,
+ points = this._rings[0],
+ len = points.length;
+
+ if (!len) { return null; }
+
+ // polygon centroid algorithm; only uses the first ring if there are multiple
+
+ area = x = y = 0;
+
+ for (i = 0, 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;
+ }
+
+ if (area === 0) {
+ // Polygon is so small that all points are on same pixel.
+ center = points[0];
+ } else {
+ center = [x / area, y / area];
+ }
+ return this._map.layerPointToLatLng(center);
+ },
+
+ _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;
+ },
+
+ _setLatLngs: function (latlngs) {
+ L.Polyline.prototype._setLatLngs.call(this, latlngs);
+ if (L.Polyline._flat(this._latlngs)) {
+ this._latlngs = [this._latlngs];
+ }
+ },
+
+ _defaultShape: function () {
+ return L.Polyline._flat(this._latlngs[0]) ? this._latlngs[0] : this._latlngs[0][0];
+ },
+
+ _clipPoints: function () {
+ // 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 = [];
+ if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
+ return;
+ }
+
+ if (this.options.noClip) {
+ this._parts = this._rings;
+ return;
+ }
+
+ for (var i = 0, len = this._rings.length, clipped; i < len; i++) {
+ clipped = L.PolyUtil.clipPolygon(this._rings[i], bounds, true);
+ if (clipped.length) {
+ this._parts.push(clipped);
+ }
+ }
+ },
+
+ _updatePath: function () {
+ this._renderer._updatePoly(this, true);
+ }
+});
+
+
+// @factory L.polygon(latlngs: LatLng[], options?: Polyline options)
+L.polygon = function (latlngs, options) {
+ return new L.Polygon(latlngs, options);
+};
+
+
+
+/*
+ * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object.
+ */
+
+/*
+ * @class Rectangle
+ * @aka L.Retangle
+ * @inherits Polygon
+ *
+ * A class for drawing rectangle overlays on a map. Extends `Polygon`.
+ *
+ * @example
+ *
+ * ```js
+ * // define rectangle geographical bounds
+ * var bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]];
+ *
+ * // create an orange rectangle
+ * L.rectangle(bounds, {color: "#ff7800", weight: 1}).addTo(map);
+ *
+ * // zoom the map to the rectangle bounds
+ * map.fitBounds(bounds);
+ * ```
+ *
+ */
+
+
+L.Rectangle = L.Polygon.extend({
+ initialize: function (latLngBounds, options) {
+ L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options);
+ },
+
+ // @method setBounds(latLngBounds: LatLngBounds): this
+ // Redraws the rectangle with the passed bounds.
+ setBounds: function (latLngBounds) {
+ return this.setLatLngs(this._boundsToLatLngs(latLngBounds));
+ },
+
+ _boundsToLatLngs: function (latLngBounds) {
+ latLngBounds = L.latLngBounds(latLngBounds);
+ return [
+ latLngBounds.getSouthWest(),
+ latLngBounds.getNorthWest(),
+ latLngBounds.getNorthEast(),
+ latLngBounds.getSouthEast()
+ ];
+ }
+});
+
+
+// @factory L.rectangle(latLngBounds: LatLngBounds, options?: Polyline options)
+L.rectangle = function (latLngBounds, options) {
+ return new L.Rectangle(latLngBounds, options);
+};
+
+
+
+/*
+ * @class CircleMarker
+ * @aka L.CircleMarker
+ * @inherits Path
+ *
+ * A circle of a fixed size with radius specified in pixels. Extends `Path`.
+ */
+
+L.CircleMarker = L.Path.extend({
+
+ // @section
+ // @aka CircleMarker options
+ options: {
+ fill: true,
+
+ // @option radius: Number = 10
+ // Radius of the circle marker, in pixels
+ radius: 10
+ },
+
+ initialize: function (latlng, options) {
+ L.setOptions(this, options);
+ this._latlng = L.latLng(latlng);
+ this._radius = this.options.radius;
+ },
+
+ // @method setLatLng(latLng: LatLng): this
+ // Sets the position of a circle marker to a new location.
+ setLatLng: function (latlng) {
+ this._latlng = L.latLng(latlng);
+ this.redraw();
+ return this.fire('move', {latlng: this._latlng});
+ },
+
+ // @method getLatLng(): LatLng
+ // Returns the current geographical position of the circle marker
+ getLatLng: function () {
+ return this._latlng;
+ },
+
+ // @method setRadius(radius: Number): this
+ // Sets the radius of a circle marker. Units are in pixels.
+ setRadius: function (radius) {
+ this.options.radius = this._radius = radius;
+ return this.redraw();
+ },
+
+ // @method getRadius(): Number
+ // Returns the current radius of the circle
+ 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);
+ }
+});
+
+
+// @factory L.circleMarker(latlng: LatLng, options?: CircleMarker options)
+// Instantiates a circle marker object given a geographical point, and an optional options object.
+L.circleMarker = function (latlng, options) {
+ return new L.CircleMarker(latlng, options);
+};
+
+
+
+/*
+ * @class Circle
+ * @aka L.Circle
+ * @inherits CircleMarker
+ *
+ * A class for drawing circle overlays on a map. Extends `CircleMarker`.
+ *
+ * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion).
+ *
+ * @example
+ *
+ * ```js
+ * L.circle([50.5, 30.5], {radius: 200}).addTo(map);
+ * ```
+ */
+
+L.Circle = L.CircleMarker.extend({
+
+ initialize: function (latlng, options, legacyOptions) {
+ if (typeof options === 'number') {
+ // Backwards compatibility with 0.7.x factory (latlng, radius, options?)
+ options = L.extend({}, legacyOptions, {radius: options});
+ }
+ L.setOptions(this, options);
+ this._latlng = L.latLng(latlng);
+
+ if (isNaN(this.options.radius)) { throw new Error('Circle radius cannot be NaN'); }
+
+ // @section
+ // @aka Circle options
+ // @option radius: Number; Radius of the circle, in meters.
+ this._mRadius = this.options.radius;
+ },
+
+ // @method setRadius(radius: Number): this
+ // Sets the radius of a circle. Units are in meters.
+ setRadius: function (radius) {
+ this._mRadius = radius;
+ return this.redraw();
+ },
+
+ // @method getRadius(): Number
+ // Returns the current radius of a circle. Units are in meters.
+ getRadius: function () {
+ return this._mRadius;
+ },
+
+ // @method getBounds(): LatLngBounds
+ // Returns the `LatLngBounds` of the path.
+ getBounds: function () {
+ var half = [this._radius, this._radiusY || this._radius];
+
+ 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;
+
+ if (isNaN(lngR) || lngR === 0) {
+ lngR = latR / Math.cos(Math.PI / 180 * lat); // Fallback for edge case, #2425
+ }
+
+ 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();
+ }
+});
+
+// @factory L.circle(latlng: LatLng, options?: Circle options)
+// Instantiates a circle object given a geographical point, and an options object
+// which contains the circle radius.
+// @alternative
+// @factory L.circle(latlng: LatLng, radius: Number, options?: Circle options)
+// Obsolete way of instantiating a circle, for compatibility with 0.7.x code.
+// Do not use in new applications or plugins.
+L.circle = function (latlng, options, legacyOptions) {
+ return new L.Circle(latlng, options, legacyOptions);
+};
+
+
+
+/*
+ * @class SVG
+ * @inherits Renderer
+ * @aka L.SVG
+ *
+ * Allows vector layers to be displayed with [SVG](https://developer.mozilla.org/docs/Web/SVG).
+ * Inherits `Renderer`.
+ *
+ * Due to [technical limitations](http://caniuse.com/#search=svg), SVG is not
+ * available in all web browsers, notably Android 2.x and 3.x.
+ *
+ * Although SVG is not available on IE7 and IE8, these browsers support
+ * [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language)
+ * (a now deprecated technology), and the SVG renderer will fall back to VML in
+ * this case.
+ *
+ * @example
+ *
+ * Use SVG by default for all paths in the map:
+ *
+ * ```js
+ * var map = L.map('map', {
+ * renderer: L.svg()
+ * });
+ * ```
+ *
+ * Use a SVG renderer with extra padding for specific vector geometries:
+ *
+ * ```js
+ * var map = L.map('map');
+ * var myRenderer = L.svg({ padding: 0.5 });
+ * var line = L.polyline( coordinates, { renderer: myRenderer } );
+ * var circle = L.circle( center, { renderer: myRenderer } );
+ * ```
+ */
+
+L.SVG = L.Renderer.extend({
+
+ getEvents: function () {
+ var events = L.Renderer.prototype.getEvents.call(this);
+ events.zoomstart = this._onZoomStart;
+ return events;
+ },
+
+ _initContainer: function () {
+ this._container = L.SVG.create('svg');
+
+ // makes it possible to click through svg root; we'll reset it back in individual paths
+ this._container.setAttribute('pointer-events', 'none');
+
+ this._rootGroup = L.SVG.create('g');
+ this._container.appendChild(this._rootGroup);
+ },
+
+ _onZoomStart: function () {
+ // Drag-then-pinch interactions might mess up the center and zoom.
+ // In this case, the easiest way to prevent this is re-do the renderer
+ // bounds and padding when the zooming starts.
+ this._update();
+ },
+
+ _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;
+
+ // set size of svg-container if changed
+ if (!this._svgSize || !this._svgSize.equals(size)) {
+ this._svgSize = size;
+ container.setAttribute('width', size.x);
+ container.setAttribute('height', size.y);
+ }
+
+ // movement: update container viewBox so that we don't have to change coordinates of individual layers
+ L.DomUtil.setPosition(container, b.min);
+ container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' '));
+
+ this.fire('update');
+ },
+
+ // methods below are called by vector layers implementations
+
+ _initPath: function (layer) {
+ var path = layer._path = L.SVG.create('path');
+
+ // @namespace Path
+ // @option className: String = null
+ // Custom class name set on an element. Only for SVG renderer.
+ if (layer.options.className) {
+ L.DomUtil.addClass(path, layer.options.className);
+ }
+
+ if (layer.options.interactive) {
+ L.DomUtil.addClass(path, 'leaflet-interactive');
+ }
+
+ this._updateStyle(layer);
+ this._layers[L.stamp(layer)] = layer;
+ },
+
+ _addPath: function (layer) {
+ this._rootGroup.appendChild(layer._path);
+ layer.addInteractiveTarget(layer._path);
+ },
+
+ _removePath: function (layer) {
+ L.DomUtil.remove(layer._path);
+ layer.removeInteractiveTarget(layer._path);
+ delete this._layers[L.stamp(layer)];
+ },
+
+ _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', options.fillRule || 'evenodd');
+ } else {
+ path.setAttribute('fill', '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);
+ }
+});
+
+
+// @namespace SVG; @section
+// There are several static functions which can be called without instantiating L.SVG:
+L.extend(L.SVG, {
+ // @function create(name: String): SVGElement
+ // Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement),
+ // corresponding to the class name passed. For example, using 'line' will return
+ // an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement).
+ create: function (name) {
+ return document.createElementNS('http://www.w3.org/2000/svg', name);
+ },
+
+ // @function pointsToPath(rings: Point[], closed: Boolean): String
+ // Generates a 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';
+ }
+});
+
+// @namespace Browser; @property svg: Boolean
+// `true` when the browser supports [SVG](https://developer.mozilla.org/docs/Web/SVG).
+L.Browser.svg = !!(document.createElementNS && L.SVG.create('svg').createSVGRect);
+
+
+// @namespace SVG
+// @factory L.svg(options?: Renderer options)
+// Creates a SVG renderer with the given options.
+L.svg = function (options) {
+ return L.Browser.svg || L.Browser.vml ? new L.SVG(options) : null;
+};
+
+
+
+/*
+ * Thanks to Dmitry Baranovsky and his Raphael library for inspiration!
+ */
+
+/*
+ * @class SVG
+ *
+ * Although SVG is not available on IE7 and IE8, these browsers support [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language), and the SVG renderer will fall back to VML in this case.
+ *
+ * VML was deprecated in 2012, which means VML functionality exists only for backwards compatibility
+ * with old versions of Internet Explorer.
+ */
+
+// @namespace Browser; @property vml: Boolean
+// `true` if the browser supports [VML](https://en.wikipedia.org/wiki/Vector_Markup_Language).
+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');
+ },
+
+ _update: function () {
+ if (this._map._animatingZoom) { return; }
+ L.Renderer.prototype._update.call(this);
+ this.fire('update');
+ },
+
+ _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);
+ this._layers[L.stamp(layer)] = layer;
+ },
+
+ _addPath: function (layer) {
+ var container = layer._container;
+ this._container.appendChild(container);
+
+ if (layer.options.interactive) {
+ layer.addInteractiveTarget(container);
+ }
+ },
+
+ _removePath: function (layer) {
+ var container = layer._container;
+ L.DomUtil.remove(container);
+ layer.removeInteractiveTarget(container);
+ delete this._layers[L.stamp(layer)];
+ },
+
+ _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.round(),
+ 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;
+ },
+
+ _bringToFront: function (layer) {
+ L.DomUtil.toFront(layer._container);
+ },
+
+ _bringToBack: function (layer) {
+ L.DomUtil.toBack(layer._container);
+ }
+});
+
+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">');
+ };
+ }
+ })();
+}
+
+
+
+/*
+ * @class Canvas
+ * @inherits Renderer
+ * @aka L.Canvas
+ *
+ * Allows vector layers to be displayed with [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
+ * Inherits `Renderer`.
+ *
+ * Due to [technical limitations](http://caniuse.com/#search=canvas), Canvas is not
+ * available in all web browsers, notably IE8, and overlapping geometries might
+ * not display properly in some edge cases.
+ *
+ * @example
+ *
+ * Use Canvas by default for all paths in the map:
+ *
+ * ```js
+ * var map = L.map('map', {
+ * renderer: L.canvas()
+ * });
+ * ```
+ *
+ * Use a Canvas renderer with extra padding for specific vector geometries:
+ *
+ * ```js
+ * var map = L.map('map');
+ * var myRenderer = L.canvas({ padding: 0.5 });
+ * var line = L.polyline( coordinates, { renderer: myRenderer } );
+ * var circle = L.circle( center, { renderer: myRenderer } );
+ * ```
+ */
+
+L.Canvas = L.Renderer.extend({
+ getEvents: function () {
+ var events = L.Renderer.prototype.getEvents.call(this);
+ events.viewprereset = this._onViewPreReset;
+ return events;
+ },
+
+ _onViewPreReset: function () {
+ // Set a flag so that a viewprereset+moveend+viewreset only updates&redraws once
+ this._postponeUpdatePaths = true;
+ },
+
+ onAdd: function () {
+ L.Renderer.prototype.onAdd.call(this);
+
+ // Redraw vectors since canvas is cleared upon removal,
+ // in case of removing the renderer itself from the map.
+ this._draw();
+ },
+
+ _initContainer: function () {
+ var container = this._container = document.createElement('canvas');
+
+ L.DomEvent
+ .on(container, 'mousemove', L.Util.throttle(this._onMouseMove, 32, this), this)
+ .on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this)
+ .on(container, 'mouseout', this._handleMouseOut, this);
+
+ this._ctx = container.getContext('2d');
+ },
+
+ _updatePaths: function () {
+ if (this._postponeUpdatePaths) { return; }
+
+ var layer;
+ this._redrawBounds = null;
+ for (var id in this._layers) {
+ layer = this._layers[id];
+ layer._update();
+ }
+ this._redraw();
+ },
+
+ _update: function () {
+ if (this._map._animatingZoom && this._bounds) { return; }
+
+ this._drawnLayers = {};
+
+ 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);
+
+ // Tell paths to redraw themselves
+ this.fire('update');
+ },
+
+ _reset: function () {
+ L.Renderer.prototype._reset.call(this);
+
+ if (this._postponeUpdatePaths) {
+ this._postponeUpdatePaths = false;
+ this._updatePaths();
+ }
+ },
+
+ _initPath: function (layer) {
+ this._updateDashArray(layer);
+ this._layers[L.stamp(layer)] = layer;
+
+ var order = layer._order = {
+ layer: layer,
+ prev: this._drawLast,
+ next: null
+ };
+ if (this._drawLast) { this._drawLast.next = order; }
+ this._drawLast = order;
+ this._drawFirst = this._drawFirst || this._drawLast;
+ },
+
+ _addPath: function (layer) {
+ this._requestRedraw(layer);
+ },
+
+ _removePath: function (layer) {
+ var order = layer._order;
+ var next = order.next;
+ var prev = order.prev;
+
+ if (next) {
+ next.prev = prev;
+ } else {
+ this._drawLast = prev;
+ }
+ if (prev) {
+ prev.next = next;
+ } else {
+ this._drawFirst = next;
+ }
+
+ delete layer._order;
+
+ delete this._layers[L.stamp(layer)];
+
+ this._requestRedraw(layer);
+ },
+
+ _updatePath: function (layer) {
+ // Redraw the union of the layer's old pixel
+ // bounds and the new pixel bounds.
+ this._extendRedrawBounds(layer);
+ layer._project();
+ layer._update();
+ // The redraw will extend the redraw bounds
+ // with the new pixel bounds.
+ this._requestRedraw(layer);
+ },
+
+ _updateStyle: function (layer) {
+ this._updateDashArray(layer);
+ this._requestRedraw(layer);
+ },
+
+ _updateDashArray: function (layer) {
+ if (layer.options.dashArray) {
+ var parts = layer.options.dashArray.split(','),
+ dashArray = [],
+ i;
+ for (i = 0; i < parts.length; i++) {
+ dashArray.push(Number(parts[i]));
+ }
+ layer.options._dashArray = dashArray;
+ }
+ },
+
+ _requestRedraw: function (layer) {
+ if (!this._map) { return; }
+
+ this._extendRedrawBounds(layer);
+ this._redrawRequest = this._redrawRequest || L.Util.requestAnimFrame(this._redraw, this);
+ },
+
+ _extendRedrawBounds: function (layer) {
+ var padding = (layer.options.weight || 0) + 1;
+ this._redrawBounds = this._redrawBounds || new L.Bounds();
+ this._redrawBounds.extend(layer._pxBounds.min.subtract([padding, padding]));
+ this._redrawBounds.extend(layer._pxBounds.max.add([padding, padding]));
+ },
+
+ _redraw: function () {
+ this._redrawRequest = null;
+
+ if (this._redrawBounds) {
+ this._redrawBounds.min._floor();
+ this._redrawBounds.max._ceil();
+ }
+
+ this._clear(); // clear layers in redraw bounds
+ this._draw(); // draw layers
+
+ this._redrawBounds = null;
+ },
+
+ _clear: function () {
+ var bounds = this._redrawBounds;
+ if (bounds) {
+ var size = bounds.getSize();
+ this._ctx.clearRect(bounds.min.x, bounds.min.y, size.x, size.y);
+ } else {
+ this._ctx.clearRect(0, 0, this._container.width, this._container.height);
+ }
+ },
+
+ _draw: function () {
+ var layer, bounds = this._redrawBounds;
+ this._ctx.save();
+ if (bounds) {
+ var size = bounds.getSize();
+ this._ctx.beginPath();
+ this._ctx.rect(bounds.min.x, bounds.min.y, size.x, size.y);
+ this._ctx.clip();
+ }
+
+ this._drawing = true;
+
+ for (var order = this._drawFirst; order; order = order.next) {
+ layer = order.layer;
+ if (!bounds || (layer._pxBounds && layer._pxBounds.intersects(bounds))) {
+ layer._updatePath();
+ }
+ }
+
+ this._drawing = false;
+
+ this._ctx.restore(); // Restore state before clipping.
+ },
+
+ _updatePoly: function (layer, closed) {
+ if (!this._drawing) { return; }
+
+ var i, j, len2, p,
+ parts = layer._parts,
+ len = parts.length,
+ ctx = this._ctx;
+
+ if (!len) { return; }
+
+ this._drawnLayers[layer._leaflet_id] = layer;
+
+ ctx.beginPath();
+
+ if (ctx.setLineDash) {
+ ctx.setLineDash(layer.options && layer.options._dashArray || []);
+ }
+
+ 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 (!this._drawing || layer._empty()) { return; }
+
+ var p = layer._point,
+ ctx = this._ctx,
+ r = layer._radius,
+ s = (layer._radiusY || r) / r;
+
+ this._drawnLayers[layer._leaflet_id] = layer;
+
+ 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 options = layer.options;
+
+ if (options.fill) {
+ ctx.globalAlpha = options.fillOpacity;
+ ctx.fillStyle = options.fillColor || options.color;
+ ctx.fill(options.fillRule || 'evenodd');
+ }
+
+ if (options.stroke && options.weight !== 0) {
+ ctx.globalAlpha = options.opacity;
+ ctx.lineWidth = 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), layer, clickedLayer;
+
+ for (var order = this._drawFirst; order; order = order.next) {
+ layer = order.layer;
+ if (layer.options.interactive && layer._containsPoint(point) && !this._map._draggableMoved(layer)) {
+ clickedLayer = layer;
+ }
+ }
+ if (clickedLayer) {
+ L.DomEvent._fakeStop(e);
+ this._fireEvent([clickedLayer], e);
+ }
+ },
+
+ _onMouseMove: function (e) {
+ if (!this._map || this._map.dragging.moving() || this._map._animatingZoom) { return; }
+
+ var point = this._map.mouseEventToLayerPoint(e);
+ this._handleMouseHover(e, point);
+ },
+
+
+ _handleMouseOut: function (e) {
+ var layer = this._hoveredLayer;
+ if (layer) {
+ // if we're leaving the layer, fire mouseout
+ L.DomUtil.removeClass(this._container, 'leaflet-interactive');
+ this._fireEvent([layer], e, 'mouseout');
+ this._hoveredLayer = null;
+ }
+ },
+
+ _handleMouseHover: function (e, point) {
+ var layer, candidateHoveredLayer;
+
+ for (var order = this._drawFirst; order; order = order.next) {
+ layer = order.layer;
+ if (layer.options.interactive && layer._containsPoint(point)) {
+ candidateHoveredLayer = layer;
+ }
+ }
+
+ if (candidateHoveredLayer !== this._hoveredLayer) {
+ this._handleMouseOut(e);
+
+ if (candidateHoveredLayer) {
+ L.DomUtil.addClass(this._container, 'leaflet-interactive'); // change cursor
+ this._fireEvent([candidateHoveredLayer], e, 'mouseover');
+ this._hoveredLayer = candidateHoveredLayer;
+ }
+ }
+
+ if (this._hoveredLayer) {
+ this._fireEvent([this._hoveredLayer], e);
+ }
+ },
+
+ _fireEvent: function (layers, e, type) {
+ this._map._fireDOMEvent(e, type || e.type, layers);
+ },
+
+ _bringToFront: function (layer) {
+ var order = layer._order;
+ var next = order.next;
+ var prev = order.prev;
+
+ if (next) {
+ next.prev = prev;
+ } else {
+ // Already last
+ return;
+ }
+ if (prev) {
+ prev.next = next;
+ } else if (next) {
+ // Update first entry unless this is the
+ // signle entry
+ this._drawFirst = next;
+ }
+
+ order.prev = this._drawLast;
+ this._drawLast.next = order;
+
+ order.next = null;
+ this._drawLast = order;
+
+ this._requestRedraw(layer);
+ },
+
+ _bringToBack: function (layer) {
+ var order = layer._order;
+ var next = order.next;
+ var prev = order.prev;
+
+ if (prev) {
+ prev.next = next;
+ } else {
+ // Already first
+ return;
+ }
+ if (next) {
+ next.prev = prev;
+ } else if (prev) {
+ // Update last entry unless this is the
+ // signle entry
+ this._drawLast = prev;
+ }
+
+ order.prev = null;
+
+ order.next = this._drawFirst;
+ this._drawFirst.prev = order;
+ this._drawFirst = order;
+
+ this._requestRedraw(layer);
+ }
+});
+
+// @namespace Browser; @property canvas: Boolean
+// `true` when the browser supports [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
+L.Browser.canvas = (function () {
+ return !!document.createElement('canvas').getContext;
+}());
+
+// @namespace Canvas
+// @factory L.canvas(options?: Renderer options)
+// Creates a Canvas renderer with the given options.
+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();
+};
+
+
+
+/*
+ * @class GeoJSON
+ * @aka L.GeoJSON
+ * @inherits FeatureGroup
+ *
+ * Represents a GeoJSON object or an array of GeoJSON objects. Allows you to parse
+ * GeoJSON data and display it on the map. Extends `FeatureGroup`.
+ *
+ * @example
+ *
+ * ```js
+ * L.geoJSON(data, {
+ * style: function (feature) {
+ * return {color: feature.properties.color};
+ * }
+ * }).bindPopup(function (layer) {
+ * return layer.feature.properties.description;
+ * }).addTo(map);
+ * ```
+ */
+
+L.GeoJSON = L.FeatureGroup.extend({
+
+ /* @section
+ * @aka GeoJSON options
+ *
+ * @option pointToLayer: Function = *
+ * A `Function` defining how GeoJSON points spawn Leaflet layers. It is internally
+ * called when data is added, passing the GeoJSON point feature and its `LatLng`.
+ * The default is to spawn a default `Marker`:
+ * ```js
+ * function(geoJsonPoint, latlng) {
+ * return L.marker(latlng);
+ * }
+ * ```
+ *
+ * @option style: Function = *
+ * A `Function` defining the `Path options` for styling GeoJSON lines and polygons,
+ * called internally when data is added.
+ * The default value is to not override any defaults:
+ * ```js
+ * function (geoJsonFeature) {
+ * return {}
+ * }
+ * ```
+ *
+ * @option onEachFeature: Function = *
+ * A `Function` that will be called once for each created `Feature`, after it has
+ * been created and styled. Useful for attaching events and popups to features.
+ * The default is to do nothing with the newly created layers:
+ * ```js
+ * function (feature, layer) {}
+ * ```
+ *
+ * @option filter: Function = *
+ * A `Function` that will be used to decide whether to include a feature or not.
+ * The default is to include all features:
+ * ```js
+ * function (geoJsonFeature) {
+ * return true;
+ * }
+ * ```
+ * Note: dynamically changing the `filter` option will have effect only on newly
+ * added data. It will _not_ re-evaluate already included features.
+ *
+ * @option coordsToLatLng: Function = *
+ * A `Function` that will be used for converting GeoJSON coordinates to `LatLng`s.
+ * The default is the `coordsToLatLng` static method.
+ */
+
+ initialize: function (geojson, options) {
+ L.setOptions(this, options);
+
+ this._layers = {};
+
+ if (geojson) {
+ this.addData(geojson);
+ }
+ },
+
+ // @method addData( <GeoJSON> data ): this
+ // Adds a GeoJSON object to the layer.
+ 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 this; }
+
+ var layer = L.GeoJSON.geometryToLayer(geojson, options);
+ if (!layer) {
+ return this;
+ }
+ layer.feature = L.GeoJSON.asFeature(geojson);
+
+ layer.defaultOptions = layer.options;
+ this.resetStyle(layer);
+
+ if (options.onEachFeature) {
+ options.onEachFeature(geojson, layer);
+ }
+
+ return this.addLayer(layer);
+ },
+
+ // @method resetStyle( <Path> layer ): this
+ // Resets the given vector layer's style to the original GeoJSON style, useful for resetting style after hover events.
+ resetStyle: function (layer) {
+ // reset any custom styles
+ layer.options = L.Util.extend({}, layer.defaultOptions);
+ this._setLayerStyle(layer, this.options.style);
+ return this;
+ },
+
+ // @method setStyle( <Function> style ): this
+ // Changes styles of GeoJSON vector layers with the given style function.
+ 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);
+ }
+ }
+});
+
+// @section
+// There are several static functions which can be called without instantiating L.GeoJSON:
+L.extend(L.GeoJSON, {
+ // @function geometryToLayer(featureData: Object, options?: GeoJSON options): Layer
+ // Creates a `Layer` from a given GeoJSON feature. Can use a custom
+ // [`pointToLayer`](#geojson-pointtolayer) and/or [`coordsToLatLng`](#geojson-coordstolatlng)
+ // functions if provided as options.
+ geometryToLayer: function (geojson, options) {
+
+ var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
+ coords = geometry ? geometry.coordinates : null,
+ layers = [],
+ pointToLayer = options && options.pointToLayer,
+ coordsToLatLng = options && options.coordsToLatLng || this.coordsToLatLng,
+ latlng, latlngs, i, len;
+
+ if (!coords && !geometry) {
+ return null;
+ }
+
+ 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++) {
+ var layer = this.geometryToLayer({
+ geometry: geometry.geometries[i],
+ type: 'Feature',
+ properties: geojson.properties
+ }, options);
+
+ if (layer) {
+ layers.push(layer);
+ }
+ }
+ return new L.FeatureGroup(layers);
+
+ default:
+ throw new Error('Invalid GeoJSON object.');
+ }
+ },
+
+ // @function coordsToLatLng(coords: Array): LatLng
+ // Creates a `LatLng` object from an array of 2 numbers (longitude, latitude)
+ // or 3 numbers (longitude, latitude, altitude) used in GeoJSON for points.
+ coordsToLatLng: function (coords) {
+ return new L.LatLng(coords[1], coords[0], coords[2]);
+ },
+
+ // @function coordsToLatLngs(coords: Array, levelsDeep?: Number, coordsToLatLng?: Function): Array
+ // Creates a multidimensional array of `LatLng`s from a GeoJSON coordinates array.
+ // `levelsDeep` specifies the nesting level (0 is for an array of points, 1 for an array of arrays of points, etc., 0 by default).
+ // Can use a custom [`coordsToLatLng`](#geojson-coordstolatlng) function.
+ 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;
+ },
+
+ // @function latLngToCoords(latlng: LatLng): Array
+ // Reverse of [`coordsToLatLng`](#geojson-coordstolatlng)
+ latLngToCoords: function (latlng) {
+ return latlng.alt !== undefined ?
+ [latlng.lng, latlng.lat, latlng.alt] :
+ [latlng.lng, latlng.lat];
+ },
+
+ // @function latLngsToCoords(latlngs: Array, levelsDeep?: Number, closed?: Boolean): Array
+ // Reverse of [`coordsToLatLngs`](#geojson-coordstolatlngs)
+ // `closed` determines whether the first point should be appended to the end of the array to close the feature, only used when `levelsDeep` is 0. False by default.
+ 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);
+ },
+
+ // @function asFeature(geojson: Object): Object
+ // Normalize GeoJSON geometries/features into GeoJSON features.
+ asFeature: function (geojson) {
+ if (geojson.type === 'Feature' || geojson.type === 'FeatureCollection') {
+ 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())
+ });
+ }