UNPKG

leaflet-engine

Version:

Leaflet is an open-source JavaScript library for mobile-friendly interactive maps.

2,174 lines (1,646 loc) 175 kB
/* Copyright (c) 2010-2012, CloudMade, Vladimir Agafonkin Leaflet is an open-source JavaScript library for mobile-friendly interactive maps. http://leaflet.cloudmade.com */ (function (window, undefined) { var L, originalL; if (typeof exports !== undefined + '') { L = exports; } else { originalL = window.L; L = {}; L.noConflict = function () { window.L = originalL; return this; }; window.L = L; } L.version = '0.4.5'; /* * L.Util is a namespace for various utility functions. */ L.Util = { extend: function (/*Object*/ dest) /*-> Object*/ { // merge src properties into dest var sources = Array.prototype.slice.call(arguments, 1); for (var j = 0, len = sources.length, src; j < len; j++) { src = sources[j] || {}; for (var i in src) { if (src.hasOwnProperty(i)) { dest[i] = src[i]; } } } return dest; }, bind: function (fn, obj) { // (Function, Object) -> Function var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null; return function () { return fn.apply(obj, args || arguments); }; }, stamp: (function () { var lastId = 0, key = '_leaflet_id'; return function (/*Object*/ obj) { obj[key] = obj[key] || ++lastId; return obj[key]; }; }()), limitExecByInterval: function (fn, time, context) { var lock, execOnUnlock; return function wrapperFn() { var args = arguments; if (lock) { execOnUnlock = true; return; } lock = true; setTimeout(function () { lock = false; if (execOnUnlock) { wrapperFn.apply(context, args); execOnUnlock = false; } }, time); fn.apply(context, args); }; }, falseFn: function () { return false; }, formatNum: function (num, digits) { var pow = Math.pow(10, digits || 5); return Math.round(num * pow) / pow; }, splitWords: function (str) { return str.replace(/^\s+|\s+$/g, '').split(/\s+/); }, setOptions: function (obj, options) { obj.options = L.Util.extend({}, obj.options, options); return obj.options; }, getParamString: function (obj) { var params = []; for (var i in obj) { if (obj.hasOwnProperty(i)) { params.push(i + '=' + obj[i]); } } return '?' + params.join('&'); }, template: function (str, data) { return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) { var value = data[key]; if (!data.hasOwnProperty(key)) { throw new Error('No value provided for variable ' + str); } return value; }); }, emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' }; (function () { function getPrefixed(name) { var i, fn, prefixes = ['webkit', 'moz', 'o', 'ms']; for (i = 0; i < prefixes.length && !fn; i++) { fn = window[prefixes[i] + name]; } return fn; } function timeoutDefer(fn) { return window.setTimeout(fn, 1000 / 60); } var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer; var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; L.Util.requestAnimFrame = function (fn, context, immediate, element) { fn = L.Util.bind(fn, context); if (immediate && requestFn === timeoutDefer) { fn(); } else { return requestFn.call(window, fn, element); } }; L.Util.cancelAnimFrame = function (id) { if (id) { cancelFn.call(window, id); } }; }()); /* * Class powers the OOP facilities of the library. Thanks to John Resig and Dean Edwards for inspiration! */ L.Class = function () {}; L.Class.extend = function (/*Object*/ props) /*-> Class*/ { // extended class with the new prototype var NewClass = function () { if (this.initialize) { this.initialize.apply(this, arguments); } }; // instantiate class without calling constructor var F = function () {}; F.prototype = this.prototype; var proto = new F(); 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.Util.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 (props.options && proto.options) { props.options = L.Util.extend({}, proto.options, props.options); } // mix given properties into the prototype L.Util.extend(proto, props); return NewClass; }; // method for adding properties to prototype L.Class.include = function (props) { L.Util.extend(this.prototype, props); }; L.Class.mergeOptions = function (options) { L.Util.extend(this.prototype.options, options); }; /* * L.Mixin.Events adds custom events functionality to Leaflet classes */ var key = '_leaflet_events'; L.Mixin = {}; L.Mixin.Events = { addEventListener: function (types, fn, context) { // (String, Function[, Object]) or (Object[, Object]) var events = this[key] = this[key] || {}, type, i, len; // Types can be a map of types/handlers if (typeof types === 'object') { for (type in types) { if (types.hasOwnProperty(type)) { this.addEventListener(type, types[type], fn); } } return this; } types = L.Util.splitWords(types); for (i = 0, len = types.length; i < len; i++) { events[types[i]] = events[types[i]] || []; events[types[i]].push({ action: fn, context: context || this }); } return this; }, hasEventListeners: function (type) { // (String) -> Boolean return (key in this) && (type in this[key]) && (this[key][type].length > 0); }, removeEventListener: function (types, fn, context) { // (String[, Function, Object]) or (Object[, Object]) var events = this[key], type, i, len, listeners, j; if (typeof types === 'object') { for (type in types) { if (types.hasOwnProperty(type)) { this.removeEventListener(type, types[type], fn); } } return this; } types = L.Util.splitWords(types); for (i = 0, len = types.length; i < len; i++) { if (this.hasEventListeners(types[i])) { listeners = events[types[i]]; for (j = listeners.length - 1; j >= 0; j--) { if ( (!fn || listeners[j].action === fn) && (!context || (listeners[j].context === context)) ) { listeners.splice(j, 1); } } } } return this; }, fireEvent: function (type, data) { // (String[, Object]) if (!this.hasEventListeners(type)) { return this; } var event = L.Util.extend({ type: type, target: this }, data); var listeners = this[key][type].slice(); for (var i = 0, len = listeners.length; i < len; i++) { listeners[i].action.call(listeners[i].context || this, event); } return this; } }; L.Mixin.Events.on = L.Mixin.Events.addEventListener; L.Mixin.Events.off = L.Mixin.Events.removeEventListener; L.Mixin.Events.fire = L.Mixin.Events.fireEvent; (function () { var ua = navigator.userAgent.toLowerCase(), ie = !!window.ActiveXObject, ie6 = ie && !window.XMLHttpRequest, webkit = ua.indexOf("webkit") !== -1, gecko = ua.indexOf("gecko") !== -1, //Terrible browser detection to work around a safari / iOS / android browser bug. See TileLayer._addTile and debug/hacks/jitter.html chrome = ua.indexOf("chrome") !== -1, opera = window.opera, android = ua.indexOf("android") !== -1, android23 = ua.search("android [23]") !== -1, mobile = typeof orientation !== undefined + '' ? true : false, doc = document.documentElement, ie3d = ie && ('transition' in doc.style), webkit3d = webkit && ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()), gecko3d = gecko && ('MozPerspective' in doc.style), opera3d = opera && ('OTransition' in doc.style); var touch = !window.L_NO_TOUCH && (function () { var startName = 'ontouchstart'; // WebKit, etc if (startName in doc) { return true; } // Firefox/Gecko var div = document.createElement('div'), supported = false; if (!div.setAttribute) { return false; } div.setAttribute(startName, 'return;'); if (typeof div[startName] === 'function') { supported = true; } div.removeAttribute(startName); div = null; return supported; }()); var retina = (('devicePixelRatio' in window && window.devicePixelRatio > 1) || ('matchMedia' in window && window.matchMedia("(min-resolution:144dpi)").matches)); L.Browser = { ua: ua, ie: ie, ie6: ie6, webkit: webkit, gecko: gecko, opera: opera, android: android, android23: android23, chrome: chrome, ie3d: ie3d, webkit3d: webkit3d, gecko3d: gecko3d, opera3d: opera3d, any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d), mobile: mobile, mobileWebkit: mobile && webkit, mobileWebkit3d: mobile && webkit3d, mobileOpera: mobile && opera, touch: touch, retina: retina }; }()); /* * L.Point represents a point with x and y coordinates. */ L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) { this.x = (round ? Math.round(x) : x); this.y = (round ? Math.round(y) : y); }; L.Point.prototype = { add: function (point) { return this.clone()._add(L.point(point)); }, _add: function (point) { this.x += point.x; this.y += point.y; return this; }, subtract: function (point) { return this.clone()._subtract(L.point(point)); }, // destructive subtract (faster) _subtract: function (point) { this.x -= point.x; this.y -= point.y; return this; }, divideBy: function (num, round) { return new L.Point(this.x / num, this.y / num, round); }, multiplyBy: function (num, round) { return new L.Point(this.x * num, this.y * num, round); }, 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); }, round: function () { return this.clone()._round(); }, // destructive round _round: function () { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; }, floor: function () { return this.clone()._floor(); }, _floor: function () { this.x = Math.floor(this.x); this.y = Math.floor(this.y); return this; }, clone: function () { return new L.Point(this.x, this.y); }, toString: function () { return 'Point(' + L.Util.formatNum(this.x) + ', ' + L.Util.formatNum(this.y) + ')'; } }; L.point = function (x, y, round) { if (x instanceof L.Point) { return x; } if (x instanceof Array) { return new L.Point(x[0], x[1]); } if (isNaN(x)) { return x; } return new L.Point(x, y, round); }; /* * L.Bounds represents a rectangular area on the screen in pixel coordinates. */ L.Bounds = L.Class.extend({ initialize: function (a, b) { //(Point, Point) or Point[] if (!a) { return; } var points = b ? [a, b] : a; for (var i = 0, len = points.length; i < len; i++) { this.extend(points[i]); } }, // extend the bounds to contain the given point extend: function (point) { // (Point) point = L.point(point); if (!this.min && !this.max) { this.min = point.clone(); this.max = point.clone(); } else { this.min.x = Math.min(point.x, this.min.x); this.max.x = Math.max(point.x, this.max.x); this.min.y = Math.min(point.y, this.min.y); this.max.y = Math.max(point.y, this.max.y); } return this; }, getCenter: function (round) { // (Boolean) -> Point return new L.Point( (this.min.x + this.max.x) / 2, (this.min.y + this.max.y) / 2, round); }, getBottomLeft: function () { // -> Point return new L.Point(this.min.x, this.max.y); }, getTopRight: function () { // -> Point return new L.Point(this.max.x, this.min.y); }, contains: function (obj) { // (Bounds) or (Point) -> Boolean var min, max; if (typeof obj[0] === 'number' || obj instanceof L.Point) { obj = L.point(obj); } else { obj = L.bounds(obj); } if (obj instanceof L.Bounds) { min = obj.min; max = obj.max; } else { min = max = obj; } return (min.x >= this.min.x) && (max.x <= this.max.x) && (min.y >= this.min.y) && (max.y <= this.max.y); }, intersects: function (bounds) { // (Bounds) -> Boolean bounds = L.bounds(bounds); var min = this.min, max = this.max, min2 = bounds.min, max2 = bounds.max; var xIntersects = (max2.x >= min.x) && (min2.x <= max.x), yIntersects = (max2.y >= min.y) && (min2.y <= max.y); return xIntersects && yIntersects; } }); L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[]) if (!a || a instanceof L.Bounds) { return a; } return new L.Bounds(a, b); }; /* * L.Transformation is an utility class to perform simple point transformations through a 2d-matrix. */ L.Transformation = L.Class.extend({ initialize: function (/*Number*/ a, /*Number*/ b, /*Number*/ c, /*Number*/ d) { this._a = a; this._b = b; this._c = c; this._d = d; }, transform: function (point, scale) { return this._transform(point.clone(), scale); }, // destructive transform (faster) _transform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ { scale = scale || 1; point.x = scale * (this._a * point.x + this._b); point.y = scale * (this._c * point.y + this._d); return point; }, untransform: function (/*Point*/ point, /*Number*/ scale) /*-> Point*/ { scale = scale || 1; return new L.Point( (point.x / scale - this._b) / this._a, (point.y / scale - this._d) / this._c); } }); /* * L.DomUtil contains various utility functions for working with DOM */ L.DomUtil = { get: function (id) { return (typeof id === 'string' ? document.getElementById(id) : id); }, getStyle: function (el, style) { var value = el.style[style]; if (!value && el.currentStyle) { value = el.currentStyle[style]; } if (!value || value === 'auto') { var css = document.defaultView.getComputedStyle(el, null); value = css ? css[style] : null; } return (value === 'auto' ? null : value); }, getViewportOffset: function (element) { var top = 0, left = 0, el = element, docBody = document.body; do { top += el.offsetTop || 0; left += el.offsetLeft || 0; if (el.offsetParent === docBody && L.DomUtil.getStyle(el, 'position') === 'absolute') { break; } if (L.DomUtil.getStyle(el, 'position') === 'fixed') { top += docBody.scrollTop || 0; left += docBody.scrollLeft || 0; break; } el = el.offsetParent; } while (el); el = element; do { if (el === docBody) { break; } top -= el.scrollTop || 0; left -= el.scrollLeft || 0; el = el.parentNode; } while (el); return new L.Point(left, top); }, create: function (tagName, className, container) { var el = document.createElement(tagName); el.className = className; if (container) { container.appendChild(el); } return el; }, disableTextSelection: function () { if (document.selection && document.selection.empty) { document.selection.empty(); } if (!this._onselectstart) { this._onselectstart = document.onselectstart; document.onselectstart = L.Util.falseFn; } }, enableTextSelection: function () { document.onselectstart = this._onselectstart; this._onselectstart = null; }, hasClass: function (el, name) { return (el.className.length > 0) && new RegExp("(^|\\s)" + name + "(\\s|$)").test(el.className); }, addClass: function (el, name) { if (!L.DomUtil.hasClass(el, name)) { el.className += (el.className ? ' ' : '') + name; } }, removeClass: function (el, name) { function replaceFn(w, match) { if (match === name) { return ''; } return w; } el.className = el.className .replace(/(\S+)\s*/g, replaceFn) .replace(/(^\s+|\s+$)/, ''); }, setOpacity: function (el, value) { if ('opacity' in el.style) { el.style.opacity = value; } else if (L.Browser.ie) { 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) {} value = Math.round(value * 100); if (filter) { filter.Enabled = (value !== 100); filter.Opacity = value; } else { el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')'; } } }, testProp: function (props) { var style = document.documentElement.style; for (var i = 0; i < props.length; i++) { if (props[i] in style) { return props[i]; } } return false; }, getTranslateString: function (point) { // On webkit browsers (Chrome/Safari/MobileSafari/Android) using translate3d instead of translate // makes animation smoother as it ensures HW accel is used. Firefox 13 doesn't care // (same speed either way), Opera 12 doesn't support translate3d var is3d = L.Browser.webkit3d, open = 'translate' + (is3d ? '3d' : '') + '(', close = (is3d ? ',0' : '') + ')'; return open + point.x + 'px,' + point.y + 'px' + close; }, getScaleString: function (scale, origin) { var preTranslateStr = L.DomUtil.getTranslateString(origin.add(origin.multiplyBy(-1 * scale))), scaleStr = ' scale(' + scale + ') '; return preTranslateStr + scaleStr; }, setPosition: function (el, point, disable3D) { el._leaflet_pos = point; if (!disable3D && L.Browser.any3d) { el.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(point); // workaround for Android 2/3 stability (https://github.com/CloudMade/Leaflet/issues/69) if (L.Browser.mobileWebkit3d) { el.style.WebkitBackfaceVisibility = 'hidden'; } } else { el.style.left = point.x + 'px'; el.style.top = point.y + 'px'; } }, getPosition: function (el) { return el._leaflet_pos; } }; L.Util.extend(L.DomUtil, { TRANSITION: L.DomUtil.testProp(['transition', 'webkitTransition', 'OTransition', 'MozTransition', 'msTransition']), TRANSFORM: L.DomUtil.testProp(['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']) }); /* CM.LatLng represents a geographical point with latitude and longtitude coordinates. */ L.LatLng = function (rawLat, rawLng, noWrap) { // (Number, Number[, Boolean]) var lat = parseFloat(rawLat), lng = parseFloat(rawLng); if (isNaN(lat) || isNaN(lng)) { throw new Error('Invalid LatLng object: (' + rawLat + ', ' + rawLng + ')'); } if (noWrap !== true) { lat = Math.max(Math.min(lat, 90), -90); // clamp latitude into -90..90 lng = (lng + 180) % 360 + ((lng < -180 || lng === 180) ? 180 : -180); // wrap longtitude into -180..180 } this.lat = lat; this.lng = lng; }; L.Util.extend(L.LatLng, { DEG_TO_RAD: Math.PI / 180, RAD_TO_DEG: 180 / Math.PI, MAX_MARGIN: 1.0E-9 // max margin of error for the "equals" check }); L.LatLng.prototype = { equals: function (obj) { // (LatLng) -> Boolean 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 <= L.LatLng.MAX_MARGIN; }, toString: function () { // -> String return 'LatLng(' + L.Util.formatNum(this.lat) + ', ' + L.Util.formatNum(this.lng) + ')'; }, // Haversine distance formula, see http://en.wikipedia.org/wiki/Haversine_formula distanceTo: function (other) { // (LatLng) -> Number other = L.latLng(other); var R = 6378137, // earth radius in meters d2r = L.LatLng.DEG_TO_RAD, dLat = (other.lat - this.lat) * d2r, dLon = (other.lng - this.lng) * d2r, lat1 = this.lat * d2r, lat2 = other.lat * d2r, sin1 = Math.sin(dLat / 2), sin2 = Math.sin(dLon / 2); var a = sin1 * sin1 + sin2 * sin2 * Math.cos(lat1) * Math.cos(lat2); return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } }; L.latLng = function (a, b, c) { // (LatLng) or ([Number, Number]) or (Number, Number, Boolean) if (a instanceof L.LatLng) { return a; } if (a instanceof Array) { return new L.LatLng(a[0], a[1]); } if (isNaN(a)) { return a; } return new L.LatLng(a, b, c); }; /* * L.LatLngBounds represents a rectangular area on the map in geographical coordinates. */ L.LatLngBounds = L.Class.extend({ initialize: function (southWest, northEast) { // (LatLng, LatLng) or (LatLng[]) if (!southWest) { return; } var latlngs = northEast ? [southWest, northEast] : southWest; for (var i = 0, len = latlngs.length; i < len; i++) { this.extend(latlngs[i]); } }, // extend the bounds to contain the given point or bounds extend: function (obj) { // (LatLng) or (LatLngBounds) if (typeof obj[0] === 'number' || obj instanceof L.LatLng) { obj = L.latLng(obj); } else { obj = L.latLngBounds(obj); } if (obj instanceof L.LatLng) { if (!this._southWest && !this._northEast) { this._southWest = new L.LatLng(obj.lat, obj.lng, true); this._northEast = new L.LatLng(obj.lat, obj.lng, true); } else { this._southWest.lat = Math.min(obj.lat, this._southWest.lat); this._southWest.lng = Math.min(obj.lng, this._southWest.lng); this._northEast.lat = Math.max(obj.lat, this._northEast.lat); this._northEast.lng = Math.max(obj.lng, this._northEast.lng); } } else if (obj instanceof L.LatLngBounds) { this.extend(obj._southWest); this.extend(obj._northEast); } return this; }, // extend the bounds by a percentage pad: function (bufferRatio) { // (Number) -> LatLngBounds var sw = this._southWest, ne = this._northEast, heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; return new L.LatLngBounds( new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); }, getCenter: function () { // -> LatLng return new L.LatLng( (this._southWest.lat + this._northEast.lat) / 2, (this._southWest.lng + this._northEast.lng) / 2); }, getSouthWest: function () { return this._southWest; }, getNorthEast: function () { return this._northEast; }, getNorthWest: function () { return new L.LatLng(this._northEast.lat, this._southWest.lng, true); }, getSouthEast: function () { return new L.LatLng(this._southWest.lat, this._northEast.lng, true); }, contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean if (typeof obj[0] === 'number' || obj instanceof L.LatLng) { obj = L.latLng(obj); } else { obj = L.latLngBounds(obj); } var sw = this._southWest, ne = this._northEast, sw2, ne2; if (obj instanceof L.LatLngBounds) { sw2 = obj.getSouthWest(); ne2 = obj.getNorthEast(); } else { sw2 = ne2 = obj; } return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); }, intersects: function (bounds) { // (LatLngBounds) bounds = L.latLngBounds(bounds); var sw = this._southWest, ne = this._northEast, sw2 = bounds.getSouthWest(), ne2 = bounds.getNorthEast(); var latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); return latIntersects && lngIntersects; }, toBBoxString: function () { var sw = this._southWest, ne = this._northEast; return [sw.lng, sw.lat, ne.lng, ne.lat].join(','); }, equals: function (bounds) { // (LatLngBounds) if (!bounds) { return false; } bounds = L.latLngBounds(bounds); return this._southWest.equals(bounds.getSouthWest()) && this._northEast.equals(bounds.getNorthEast()); } }); //TODO International date line? L.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng) if (!a || a instanceof L.LatLngBounds) { return a; } return new L.LatLngBounds(a, b); }; /* * L.Projection contains various geographical projections used by CRS classes. */ L.Projection = {}; L.Projection.SphericalMercator = { MAX_LATITUDE: 85.0511287798, project: function (latlng) { // (LatLng) -> Point var d = L.LatLng.DEG_TO_RAD, max = this.MAX_LATITUDE, lat = Math.max(Math.min(max, latlng.lat), -max), x = latlng.lng * d, y = lat * d; y = Math.log(Math.tan((Math.PI / 4) + (y / 2))); return new L.Point(x, y); }, unproject: function (point) { // (Point, Boolean) -> LatLng var d = L.LatLng.RAD_TO_DEG, lng = point.x * d, lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d; // TODO refactor LatLng wrapping return new L.LatLng(lat, lng, true); } }; 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, true); } }; L.CRS = { latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point var projectedPoint = this.projection.project(latlng), scale = this.scale(zoom); return this.transformation._transform(projectedPoint, scale); }, pointToLatLng: function (point, zoom) { // (Point, Number[, Boolean]) -> LatLng var scale = this.scale(zoom), untransformedPoint = this.transformation.untransform(point, scale); return this.projection.unproject(untransformedPoint); }, project: function (latlng) { return this.projection.project(latlng); }, scale: function (zoom) { return 256 * Math.pow(2, zoom); } }; L.CRS.EPSG3857 = L.Util.extend({}, L.CRS, { code: 'EPSG:3857', projection: L.Projection.SphericalMercator, transformation: new L.Transformation(0.5 / Math.PI, 0.5, -0.5 / Math.PI, 0.5), project: function (latlng) { // (LatLng) -> Point var projectedPoint = this.projection.project(latlng), earthRadius = 6378137; return projectedPoint.multiplyBy(earthRadius); } }); L.CRS.EPSG900913 = L.Util.extend({}, L.CRS.EPSG3857, { code: 'EPSG:900913' }); L.CRS.EPSG4326 = L.Util.extend({}, L.CRS, { code: 'EPSG:4326', projection: L.Projection.LonLat, transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5) }); /* * L.Map is the central class of the API - it is used to create a map. */ L.Map = L.Class.extend({ includes: L.Mixin.Events, options: { crs: L.CRS.EPSG3857, /* center: LatLng, zoom: Number, layers: Array, */ fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android23, trackResize: true, markerZoomAnimation: L.DomUtil.TRANSITION && L.Browser.any3d }, initialize: function (id, options) { // (HTMLElement or String, Object) options = L.Util.setOptions(this, options); this._initContainer(id); this._initLayout(); this._initHooks(); this._initEvents(); if (options.maxBounds) { this.setMaxBounds(options.maxBounds); } if (options.center && options.zoom !== undefined) { this.setView(L.latLng(options.center), options.zoom, true); } this._initLayers(options.layers); }, // public methods that modify map state // replaced by animation-powered implementation in Map.PanAnimation.js setView: function (center, zoom) { this._resetView(L.latLng(center), this._limitZoom(zoom)); return this; }, setZoom: function (zoom) { // (Number) return this.setView(this.getCenter(), zoom); }, zoomIn: function () { return this.setZoom(this._zoom + 1); }, zoomOut: function () { return this.setZoom(this._zoom - 1); }, fitBounds: function (bounds) { // (LatLngBounds) var zoom = this.getBoundsZoom(bounds); return this.setView(L.latLngBounds(bounds).getCenter(), zoom); }, fitWorld: function () { var sw = new L.LatLng(-60, -170), ne = new L.LatLng(85, 179); return this.fitBounds(new L.LatLngBounds(sw, ne)); }, panTo: function (center) { // (LatLng) return this.setView(center, this._zoom); }, panBy: function (offset) { // (Point) // replaced with animated panBy in Map.Animation.js this.fire('movestart'); this._rawPanBy(L.point(offset)); this.fire('move'); return this.fire('moveend'); }, setMaxBounds: function (bounds) { bounds = L.latLngBounds(bounds); this.options.maxBounds = bounds; if (!bounds) { this._boundsMinZoom = null; return this; } var minZoom = this.getBoundsZoom(bounds, true); this._boundsMinZoom = minZoom; if (this._loaded) { if (this._zoom < minZoom) { this.setView(bounds.getCenter(), minZoom); } else { this.panInsideBounds(bounds); } } return this; }, panInsideBounds: function (bounds) { bounds = L.latLngBounds(bounds); var viewBounds = this.getBounds(), viewSw = this.project(viewBounds.getSouthWest()), viewNe = this.project(viewBounds.getNorthEast()), sw = this.project(bounds.getSouthWest()), ne = this.project(bounds.getNorthEast()), dx = 0, dy = 0; if (viewNe.y < ne.y) { // north dy = ne.y - viewNe.y; } if (viewNe.x > ne.x) { // east dx = ne.x - viewNe.x; } if (viewSw.y > sw.y) { // south dy = sw.y - viewSw.y; } if (viewSw.x < sw.x) { // west dx = sw.x - viewSw.x; } return this.panBy(new L.Point(dx, dy, true)); }, addLayer: function (layer) { // TODO method is too big, refactor var id = L.Util.stamp(layer); if (this._layers[id]) { return this; } this._layers[id] = layer; // TODO getMaxZoom, getMinZoom in ILayer (instead of options) if (layer.options && !isNaN(layer.options.maxZoom)) { this._layersMaxZoom = Math.max(this._layersMaxZoom || 0, layer.options.maxZoom); } if (layer.options && !isNaN(layer.options.minZoom)) { this._layersMinZoom = Math.min(this._layersMinZoom || Infinity, layer.options.minZoom); } // TODO looks ugly, refactor!!! if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) { this._tileLayersNum++; this._tileLayersToLoad++; layer.on('load', this._onTileLayerLoad, this); } var onMapLoad = function () { layer.onAdd(this); this.fire('layeradd', {layer: layer}); }; if (this._loaded) { onMapLoad.call(this); } else { this.on('load', onMapLoad, this); } return this; }, removeLayer: function (layer) { var id = L.Util.stamp(layer); if (!this._layers[id]) { return; } layer.onRemove(this); delete this._layers[id]; // TODO looks ugly, refactor if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) { this._tileLayersNum--; this._tileLayersToLoad--; layer.off('load', this._onTileLayerLoad, this); } return this.fire('layerremove', {layer: layer}); }, hasLayer: function (layer) { var id = L.Util.stamp(layer); return this._layers.hasOwnProperty(id); }, invalidateSize: function (animate) { var oldSize = this.getSize(); this._sizeChanged = true; if (this.options.maxBounds) { this.setMaxBounds(this.options.maxBounds); } if (!this._loaded) { return this; } var offset = oldSize.subtract(this.getSize()).divideBy(2, true); if (animate === true) { this.panBy(offset); } else { this._rawPanBy(offset); this.fire('move'); clearTimeout(this._sizeTimer); this._sizeTimer = setTimeout(L.Util.bind(this.fire, this, 'moveend'), 200); } return this; }, // TODO handler.addTo addHandler: function (name, HandlerClass) { if (!HandlerClass) { return; } this[name] = new HandlerClass(this); if (this.options[name]) { this[name].enable(); } return this; }, // public methods for getting map state getCenter: function () { // (Boolean) -> LatLng return this.layerPointToLatLng(this._getCenterLayerPoint()); }, getZoom: function () { return this._zoom; }, getBounds: function () { var bounds = this.getPixelBounds(), sw = this.unproject(bounds.getBottomLeft()), ne = this.unproject(bounds.getTopRight()); return new L.LatLngBounds(sw, ne); }, getMinZoom: function () { var z1 = this.options.minZoom || 0, z2 = this._layersMinZoom || 0, z3 = this._boundsMinZoom || 0; return Math.max(z1, z2, z3); }, getMaxZoom: function () { var z1 = this.options.maxZoom === undefined ? Infinity : this.options.maxZoom, z2 = this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom; return Math.min(z1, z2); }, getBoundsZoom: function (bounds, inside) { // (LatLngBounds, Boolean) -> Number bounds = L.latLngBounds(bounds); var size = this.getSize(), zoom = this.options.minZoom || 0, maxZoom = this.getMaxZoom(), ne = bounds.getNorthEast(), sw = bounds.getSouthWest(), boundsSize, nePoint, swPoint, zoomNotFound = true; if (inside) { zoom--; } do { zoom++; nePoint = this.project(ne, zoom); swPoint = this.project(sw, zoom); boundsSize = new L.Point(Math.abs(nePoint.x - swPoint.x), Math.abs(swPoint.y - nePoint.y)); if (!inside) { zoomNotFound = boundsSize.x <= size.x && boundsSize.y <= size.y; } else { zoomNotFound = boundsSize.x < size.x || boundsSize.y < size.y; } } while (zoomNotFound && zoom <= maxZoom); if (zoomNotFound && inside) { return null; } return inside ? zoom : zoom - 1; }, getSize: function () { if (!this._size || this._sizeChanged) { this._size = new L.Point( this._container.clientWidth, this._container.clientHeight); this._sizeChanged = false; } return this._size; }, getPixelBounds: function () { var topLeftPoint = this._getTopLeftPoint(); return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); }, getPixelOrigin: function () { return this._initialTopLeftPoint; }, getPanes: function () { return this._panes; }, getContainer: function () { return this._container; }, // TODO replace with universal implementation after refactoring projections getZoomScale: function (toZoom) { var crs = this.options.crs; return crs.scale(toZoom) / crs.scale(this._zoom); }, getScaleZoom: function (scale) { return this._zoom + (Math.log(scale) / Math.LN2); }, // conversion methods project: function (latlng, zoom) { // (LatLng[, Number]) -> Point zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.latLngToPoint(L.latLng(latlng), zoom); }, unproject: function (point, zoom) { // (Point[, Number]) -> LatLng zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.pointToLatLng(L.point(point), zoom); }, layerPointToLatLng: function (point) { // (Point) var projectedPoint = L.point(point).add(this._initialTopLeftPoint); return this.unproject(projectedPoint); }, latLngToLayerPoint: function (latlng) { // (LatLng) var projectedPoint = this.project(L.latLng(latlng))._round(); return projectedPoint._subtract(this._initialTopLeftPoint); }, containerPointToLayerPoint: function (point) { // (Point) return L.point(point).subtract(this._getMapPanePos()); }, layerPointToContainerPoint: function (point) { // (Point) return L.point(point).add(this._getMapPanePos()); }, containerPointToLatLng: function (point) { var layerPoint = this.containerPointToLayerPoint(L.point(point)); return this.layerPointToLatLng(layerPoint); }, latLngToContainerPoint: function (latlng) { return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng))); }, mouseEventToContainerPoint: function (e) { // (MouseEvent) return L.DomEvent.getMousePosition(e, this._container); }, mouseEventToLayerPoint: function (e) { // (MouseEvent) return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); }, mouseEventToLatLng: function (e) { // (MouseEvent) return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); }, // map initialization methods _initContainer: function (id) { var container = this._container = L.DomUtil.get(id); if (container._leaflet) { throw new Error("Map container is already initialized."); } container._leaflet = true; }, _initLayout: function () { var container = this._container; container.innerHTML = ''; L.DomUtil.addClass(container, 'leaflet-container'); if (L.Browser.touch) { L.DomUtil.addClass(container, 'leaflet-touch'); } if (this.options.fadeAnimation) { L.DomUtil.addClass(container, 'leaflet-fade-anim'); } var position = L.DomUtil.getStyle(container, 'position'); if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') { container.style.position = 'relative'; } this._initPanes(); if (this._initControlPos) { this._initControlPos(); } }, _initPanes: function () { var panes = this._panes = {}; this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container); this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane); this._objectsPane = panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane); panes.shadowPane = this._createPane('leaflet-shadow-pane'); panes.overlayPane = this._createPane('leaflet-overlay-pane'); panes.markerPane = this._createPane('leaflet-marker-pane'); panes.popupPane = this._createPane('leaflet-popup-pane'); var zoomHide = ' leaflet-zoom-hide'; if (!this.options.markerZoomAnimation) { L.DomUtil.addClass(panes.markerPane, zoomHide); L.DomUtil.addClass(panes.shadowPane, zoomHide); L.DomUtil.addClass(panes.popupPane, zoomHide); } }, _createPane: function (className, container) { return L.DomUtil.create('div', className, container || this._objectsPane); }, _initializers: [], _initHooks: function () { var i, len; for (i = 0, len = this._initializers.length; i < len; i++) { this._initializers[i].call(this); } }, _initLayers: function (layers) { layers = layers ? (layers instanceof Array ? layers : [layers]) : []; this._layers = {}; this._tileLayersNum = 0; var i, len; for (i = 0, len = layers.length; i < len; i++) { this.addLayer(layers[i]); } }, // private methods that modify map state _resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) { var zoomChanged = (this._zoom !== zoom); if (!afterZoomAnim) { this.fire('movestart'); if (zoomChanged) { this.fire('zoomstart'); } } this._zoom = zoom; this._initialTopLeftPoint = this._getNewTopLeftPoint(center); if (!preserveMapOffset) { L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); } else { this._initialTopLeftPoint._add(this._getMapPanePos()); } this._tileLayersToLoad = this._tileLayersNum; this.fire('viewreset', {hard: !preserveMapOffset}); this.fire('move'); if (zoomChanged || afterZoomAnim) { this.fire('zoomend'); } this.fire('moveend', {hard: !preserveMapOffset}); if (!this._loaded) { this._loaded = true; this.fire('load'); } }, _rawPanBy: function (offset) { L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); }, // map events _initEvents: function () { if (!L.DomEvent) { return; } L.DomEvent.on(this._container, 'click', this._onMouseClick, this); var events = ['dblclick', 'mousedown', 'mouseup', 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu'], i, len; for (i = 0, len = events.length; i < len; i++) { L.DomEvent.on(this._container, events[i], this._fireMouseEvent, this); } if (this.options.trackResize) { L.DomEvent.on(window, 'resize', this._onResize, this); } }, _onResize: function () { L.Util.cancelAnimFrame(this._resizeRequest); this._resizeRequest = L.Util.requestAnimFrame(this.invalidateSize, this, false, this._container); }, _onMouseClick: function (e) { if (!this._loaded || (this.dragging && this.dragging.moved())) { return; } this.fire('preclick'); this._fireMouseEvent(e); }, _fireMouseEvent: function (e) { if (!this._loaded) { return; } var type = e.type; type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type)); if (!this.hasEventListeners(type)) { return; } if (type === 'contextmenu') { L.DomEvent.preventDefault(e); } var containerPoint = this.mouseEventToContainerPoint(e), layerPoint = this.containerPointToLayerPoint(containerPoint), latlng = this.layerPointToLatLng(layerPoint); this.fire(type, { latlng: latlng, layerPoint: layerPoint, containerPoint: containerPoint, originalEvent: e }); }, _onTileLayerLoad: function () { // TODO super-ugly, refactor!!! // clear scaled tiles after all new tiles are loaded (for performance) this._tileLayersToLoad--; if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) { clearTimeout(this._clearTileBgTimer); this._clearTileBgTimer = setTimeout(L.Util.bind(this._clearTileBg, this), 500); } }, // private methods for getting map state _getMapPanePos: function () { return L.DomUtil.getPosition(this._mapPane); }, _getTopLeftPoint: function () { if (!this._loaded) { throw new Error('Set map center and zoom first.'); } return this._initialTopLeftPoint.subtract(this._getMapPanePos()); }, _getNewTopLeftPoint: function (center, zoom) { var viewHalf = this.getSize().divideBy(2); // TODO round on display, not calculation to increase precision? return this.project(center, zoom)._subtract(viewHalf)._round(); }, _latLngToNewLayerPoint: function (latlng, newZoom, newCenter) { var topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(this._getMapPanePos()); return this.project(latlng, newZoom)._subtract(topLeft); }, _getCenterLayerPoint: function () { return this.containerPointToLayerPoint(this.getSize().divideBy(2)); }, _getCenterOffset: function (center) { return this.latLngToLayerPoint(center).subtract(this._getCenterLayerPoint()); }, _limitZoom: function (zoom) { var min = this.getMinZoom(), max = this.getMaxZoom(); return Math.max(min, Math.min(max, zoom)); } }); L.Map.addInitHook = function (fn) { var args = Array.prototype.slice.call(arguments, 1); var init = typeof fn === 'function' ? fn : function () { this[fn].apply(this, args); }; this.prototype._initializers.push(init); }; L.map = function (id, options) { return new L.Map(id, options); }; L.Projection.Mercator = { MAX_LATITUDE: 85.0840591556, R_MINOR: 6356752.3142, R_MAJOR: 6378137, project: function (latlng) { // (LatLng) -> Point var d = L.LatLng.DEG_TO_RAD, max = this.MAX_LATITUDE, lat = Math.max(Math.min(max, latlng.lat), -max), r = this.R_MAJOR, r2 = this.R_MINOR, x = latlng.lng * d * r, y = lat * d, tmp = r2 / r, eccent = Math.sqrt(1.0 - tmp * tmp), con = eccent * Math.sin(y); con = Math.pow((1 - con) / (1 + con), eccent * 0.5); var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con; y = -r2 * Math.log(ts); return new L.Point(x, y); }, unproject: function (point) { // (Point, Boolean) -> LatLng var d = L.LatLng.RAD_TO_DEG, r = this.R_MAJOR, r2 = this.R_MINOR, lng = point.x * d / r, tmp = r2 / r, eccent = Math.sqrt(1 - (tmp * tmp)), ts = Math.exp(- point.y / r2), phi = (Math.PI / 2) - 2 * Math.atan(ts), numIter = 15, tol = 1e-7, i = numIter, dphi = 0.1, con; while ((Math.abs(dphi) > tol) && (--i > 0)) { con = eccent * Math.sin(phi); dphi = (Math.PI / 2) - 2 * Math.atan(ts * Math.pow((1.0 - con) / (1.0 + con), 0.5 * eccent)) - phi; phi += dphi; } return new L.LatLng(phi * d, lng, true); } }; L.CRS.EPSG3395 = L.Util.extend({}, L.CRS, { code: 'EPSG:3395', projection: L.Projection.Mercator, transformation: (function () { var m = L.Projection.Mercator, r = m.R_MAJOR, r2 = m.R_MINOR; return new L.Transformation(0.5 / (Math.PI * r), 0.5, -0.5 / (Math.PI * r2), 0.5); }()) }); /* * L.TileLayer is used for standard xyz-numbered tile layers. */ L.TileLayer = L.Class.extend({ includes: L.Mixin.Events, options: { minZoom: 0, maxZoom: 18, tileSize: 256, subdomains: 'abc', errorTileUrl: '', attribution: '', zoomOffset: 0, opacity: 1, /* (undefined works too) zIndex: null, tms: false, continuousWorld: false, noWrap: false, zoomReverse: false, detectRetina: false, reuseTiles: false, */ unloadInvisibleTiles: L.Browser.mobile, updateWhenIdle: L.Browser.mobile }, initialize: function (url, options) { options = L.Util.setOptions(this, options); // detecting retina displays, adjusting tileSize and zoom levels if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) { options.tileSize = Math.floor(options.tileSize / 2); options.zoomOffset++; if (options.minZoom > 0) { options.minZoom--; } this.options.maxZoom--; } this._url = url; var subdomains = this.options.subdomains; if (typeof subdomains === 'string') { this.options.subdomains = subdomains.split(''); } }, onAdd: function (map) { this._map = map; // create a container div for tiles this._initContainer(); // create an image to clone for tiles this._createTileProto(); // set up events map.on({ 'viewreset': this._resetCallback, 'moveend': this._update }, this); if (!this.options.updateWhenIdle) { this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this); map.on('move', this._limitedUpdate, this); } this._reset(); this._update(); }, addTo: function (map) { map.addLayer(this); return this; }, onRemove: function (map) { map._panes.tilePane.removeChild(this._container); map.off({ 'viewreset': this._resetCallback, 'moveend': this._update }, this); if (!this.options.updateWhenIdle) { map.off('move', this._limitedUpdate, this); } this._container = null; this._map = null; }, bringToFront: function () { var pane = this._map._panes.tilePane; if (this._container) { pane.appendChild(this._container); this._setAutoZIndex(pane, Math.max); } return this; }, bringToBack: function () { var pane = this._map._panes.tilePane; if (this._container) { pane.insertBefore(this._container, pane.firstChild); this._setAutoZIndex(pane, Math.min); } return this; }, getAttribution: function () { return this.options.attribution; }, setOpacity: function (opacity) { this.options.opacity = opacity; if (this._map) { this._updateOpacity(); } return this; }, setZIndex: function (zIndex) { this.options.zIndex = zIndex; this._updateZIndex(); return this; }, setUrl: function (url, noRedraw) { this._url = url; if (!noRedraw) { this.redraw(); } return this; }, redraw: function () { if (this._map) { this._map._panes.tilePane.empty = false; this._reset(true); this._update(); } return this; }, _updateZIndex: function () { if (this._container && this.options.zIndex !== undefined) { this._container.style.zIndex = this.options.zIndex; } }, _setAutoZIndex: function (pane, compare) { var layers = pane.getElementsByClassName('leaflet-layer'), edgeZIndex = -compare(Infinity, -Infinity), // -Ifinity for max, Infinity for min zIndex; for (var i = 0, len = layers.length; i < len; i++) { if (layers[i] !== this._container) { zIndex = parseInt(layers[i].style.zIndex, 10); if (!isNaN(zIndex)) { edgeZIndex = compare(edgeZIndex, zIndex); } } } this._container.style.zIndex = isFinite(edgeZIndex) ? edgeZIndex + compare(1, -1) : ''; }, _updateOpacity: function () { L.DomUtil.setOpacity(this._container, this.options.opacity); // stupid webkit hack to force redrawing of tiles var i, tiles = this._tiles; if (L.Browser.webkit) { for (i in tiles) { if (tiles.hasOwnProperty(i)) { tiles[i].style.webkitTransform += ' translate(0,0)'; } } } }, _initContainer: function () { var tilePane = this._map._panes.tilePane; if (!this._container || tilePane.empty) { this._container = L.DomUtil.create('div', 'leaflet-layer'); this._updateZIndex(); tilePane.appendChild(this._container); if (this.options.opacity < 1) { this._updateOpacity(); } } }, _resetCallback: function (e) { this._reset(e.hard); }, _reset: function (clearOldContainer) { var key, tiles = this._tiles; for (key in tiles) { if (tiles.hasOwnProperty(key)) { this.fire('tileunload', {tile: tiles[key]}); } } this._tiles = {}; this._tilesToLoad = 0; if (this.options.reuseTiles) { this._unusedTiles = []; } if (clearOldContainer && this._container) { this._container.innerHTML = ""; } this._initContainer(); }, _update: function (e) { if (this._map._panTransition && this._map._panTransition._inProgress) { return; } var bounds = this._map.getPixelBounds(), zoom = this._map.getZoom(), tileSize = this.options.tileSize; if (zoom > this.options.maxZoom || zoom < this.options.minZoom) { return; } var nwTilePoint = new L.Point( Math.floor(bounds.min.x / tileSize), Math.floor(bounds.min.y / tileSize)), seTilePoint = new L.Point( Math.floor(bounds.max.x / tileSize), Math.floor(bounds.max.y / tileSize)), tileBounds = new L.Bounds(nwTilePoint, seTilePoint); this._addTilesFromCenterOut(tileBounds); if (this.options.unloadInvisibleTiles || this.options.reuseTiles) { this._removeOtherTiles(tileBounds); } }, _addTilesFromCenterOut: function (bounds) { var queue = [], center = bounds.getCenter(); var j, i, point; for (j = bounds.min.y; j <= bounds.max.y; j++) { for (i = bounds.min.x; i <= bounds.max.x; i++) { point = new L.Point(i, j); if (this._tileShouldBeLoaded(point)) { queue.push(point); } } } var tilesToLoad = queue.length; if (tilesToLoad === 0) { return; } // load tiles in order of their distance to center queue.sort(function (a, b) { return a.distanceTo(center) - b.distanceTo(center); }); var fragment = document.createDocumentFragment(); // if its the first batch of tiles to load if (!this._tilesToLoad) { this.fire('loading'); } this._tilesToLoad += tilesToLoad; for (i = 0; i < tile