UNPKG

mmg

Version:

Simple markers for Modest Maps

2,135 lines (1,632 loc) 151 kB
/* Copyright (c) 2010-2012, CloudMade, Vladimir Agafonkin Leaflet is a modern open-source JavaScript library for interactive maps. http://leaflet.cloudmade.com */ (function () { var L, originalL; if (typeof exports !== 'undefined') { L = exports; } else { L = {}; originalL = window.L; L.noConflict = function () { window.L = originalL; return L; }; window.L = L; } L.version = '0.4'; /* * 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]; }; }()), // TODO refactor: remove repetition requestAnimFrame: (function () { function timeoutDefer(callback) { window.setTimeout(callback, 1000 / 60); } var requestFn = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || timeoutDefer; return function (callback, context, immediate, contextEl) { callback = context ? L.Util.bind(callback, context) : callback; if (immediate && requestFn === timeoutDefer) { callback(); } else { return requestFn.call(window, callback, contextEl); } }; }()), cancelAnimFrame: (function () { var requestFn = window.cancelAnimationFrame || window.webkitCancelRequestAnimationFrame || window.mozCancelRequestAnimationFrame || window.oCancelRequestAnimationFrame || window.msCancelRequestAnimationFrame || clearTimeout; return function (handle) { if (!handle) { return; } return requestFn.call(window, handle); }; }()), 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; }, 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=' }; /* * 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 */ L.Mixin = {}; L.Mixin.Events = { addEventListener: function (/*String*/ type, /*Function*/ fn, /*(optional) Object*/ context) { var events = this._leaflet_events = this._leaflet_events || {}; events[type] = events[type] || []; events[type].push({ action: fn, context: context || this }); return this; }, hasEventListeners: function (/*String*/ type) /*-> Boolean*/ { var k = '_leaflet_events'; return (k in this) && (type in this[k]) && (this[k][type].length > 0); }, removeEventListener: function (/*String*/ type, /*Function*/ fn, /*(optional) Object*/ context) { if (!this.hasEventListeners(type)) { return this; } for (var i = 0, events = this._leaflet_events, len = events[type].length; i < len; i++) { if ( (events[type][i].action === fn) && (!context || (events[type][i].context === context)) ) { events[type].splice(i, 1); return this; } } return this; }, fireEvent: function (/*String*/ type, /*(optional) Object*/ data) { if (!this.hasEventListeners(type)) { return this; } var event = L.Util.extend({ type: type, target: this }, data); var listeners = this._leaflet_events[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, webkit = ua.indexOf("webkit") !== -1, mobile = typeof orientation !== 'undefined' ? true : false, android = ua.indexOf("android") !== -1, opera = window.opera; L.Browser = { ie: ie, ie6: ie && !window.XMLHttpRequest, webkit: webkit, webkit3d: webkit && ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()), gecko: ua.indexOf("gecko") !== -1, opera: opera, android: android, mobileWebkit: mobile && webkit, mobileOpera: mobile && opera, mobile: mobile, touch: (function () { var touchSupported = false, startName = 'ontouchstart'; // WebKit, etc if (startName in document.documentElement) { return true; } // Firefox/Gecko var e = document.createElement('div'); // If no support for basic event stuff, unlikely to have touch support if (!e.setAttribute || !e.removeAttribute) { return false; } e.setAttribute(startName, 'return;'); if (typeof e[startName] === 'function') { touchSupported = true; } e.removeAttribute(startName); e = null; return touchSupported; }()) }; }()); /* * 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(point); }, _add: function (point) { this.x += point.x; this.y += point.y; return this; }, subtract: function (point) { return this.clone()._subtract(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) { return new L.Point(this.x * num, this.y * num); }, distanceTo: function (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; }, 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.Bounds represents a rectangular area on the screen in pixel coordinates. */ L.Bounds = L.Class.extend({ initialize: function (min, max) { //(Point, Point) or Point[] if (!min) { return; } var points = (min instanceof Array ? min : [min, max]); 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) { if (!this.min && !this.max) { this.min = new L.Point(point.x, point.y); this.max = new L.Point(point.x, point.y); } 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); } }, getCenter: function (round)/*->Point*/ { return new L.Point( (this.min.x + this.max.x) / 2, (this.min.y + this.max.y) / 2, round); }, contains: function (/*Bounds or Point*/ obj)/*->Boolean*/ { var min, max; 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) { 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.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) { el.className = el.className.replace(/(\S+)\s*/g, function (w, match) { if (match === name) { return ''; } return w; }).replace(/^\s+/, ''); }, setOpacity: function (el, value) { if (L.Browser.ie) { el.style.filter += value !== 1 ? 'alpha(opacity=' + Math.round(value * 100) + ')' : ''; } else { el.style.opacity = value; } }, //TODO refactor away this ugly translate/position mess 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) { return L.DomUtil.TRANSLATE_OPEN + point.x + 'px,' + point.y + 'px' + L.DomUtil.TRANSLATE_CLOSE; }, getScaleString: function (scale, origin) { var preTranslateStr = L.DomUtil.getTranslateString(origin), scaleStr = ' scale(' + scale + ') ', postTranslateStr = L.DomUtil.getTranslateString(origin.multiplyBy(-1)); return preTranslateStr + scaleStr + postTranslateStr; }, setPosition: function (el, point) { el._leaflet_pos = point; if (L.Browser.webkit3d) { el.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(point); el.style['-webkit-backface-visibility'] = '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(['transformProperty', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']), TRANSLATE_OPEN: 'translate' + (L.Browser.webkit3d ? '3d(' : '('), TRANSLATE_CLOSE: L.Browser.webkit3d ? ',0)' : ')' }); /* CM.LatLng represents a geographical point with latitude and longtitude coordinates. */ L.LatLng = function (/*Number*/ rawLat, /*Number*/ rawLng, /*Boolean*/ noWrap) { 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 } //TODO change to lat() & lng() 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 (/*LatLng*/ obj) { if (!(obj instanceof L.LatLng)) { return false; } var margin = Math.max(Math.abs(this.lat - obj.lat), Math.abs(this.lng - obj.lng)); return margin <= L.LatLng.MAX_MARGIN; }, toString: function () { 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 (/*LatLng*/ other)/*->Double*/ { 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.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 = (southWest instanceof Array ? southWest : [southWest, northEast]); 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 (/*LatLng or 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 (/*LatLngBounds or LatLng*/ obj) /*-> Boolean*/ { 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 (/*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 (/*LatLngBounds*/ bounds) { return bounds ? this._southWest.equals(bounds.getSouthWest()) && this._northEast.equals(bounds.getNorthEast()) : false; } }); //TODO International date line? /* * 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, unbounded) { // (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; return new L.LatLng(lat, lng, unbounded); } }; L.Projection.LonLat = { project: function (latlng) { return new L.Point(latlng.lng, latlng.lat); }, unproject: function (point, unbounded) { return new L.LatLng(point.y, point.x, unbounded); } }; 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, unbounded) { // (Point, Number[, Boolean]) -> LatLng var scale = this.scale(zoom), untransformedPoint = this.transformation.untransform(point, scale); return this.projection.unproject(untransformedPoint, unbounded); //TODO get rid of 'unbounded' everywhere }, 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.android, trackResize: true }, 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 && typeof options.zoom !== 'undefined') { this.setView(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(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(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(offset); this.fire('move'); return this.fire('moveend'); }, setMaxBounds: function (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) { 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, insertAtTheBottom) { // 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, insertAtTheBottom); 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 () { 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); 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 (unbounded) { // (Boolean) -> LatLng var viewHalf = this.getSize().divideBy(2), centerPoint = this._getTopLeftPoint().add(viewHalf); return this.unproject(centerPoint, this._zoom, unbounded); }, getZoom: function () { return this._zoom; }, getBounds: function () { var bounds = this.getPixelBounds(), sw = this.unproject(new L.Point(bounds.min.x, bounds.max.y), this._zoom, true), ne = this.unproject(new L.Point(bounds.max.x, bounds.min.y), this._zoom, true); 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 = typeof this.options.maxZoom === 'undefined' ? Infinity : this.options.maxZoom, z2 = typeof this._layersMaxZoom === 'undefined' ? Infinity : this._layersMaxZoom; return Math.min(z1, z2); }, getBoundsZoom: function (bounds, inside) { // (LatLngBounds, Boolean) -> Number 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(nePoint.x - swPoint.x, 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; }, // conversion methods 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)); }, containerPointToLayerPoint: function (point) { // (Point) return point.subtract(L.DomUtil.getPosition(this._mapPane)); }, layerPointToContainerPoint: function (point) { // (Point) return point.add(L.DomUtil.getPosition(this._mapPane)); }, layerPointToLatLng: function (point) { // (Point) return this.unproject(point.add(this._initialTopLeftPoint)); }, latLngToLayerPoint: function (latlng) { // (LatLng) return this.project(latlng)._round()._subtract(this._initialTopLeftPoint); }, containerPointToLatLng: function (point) { return this.layerPointToLatLng(this.containerPointToLayerPoint(point)); }, latLngToContainerPoint: function (latlng) { return this.layerPointToContainerPoint(this.latLngToLayerPoint(latlng)); }, project: function (latlng, zoom) { // (LatLng[, Number]) -> Point zoom = typeof zoom === 'undefined' ? this._zoom : zoom; return this.options.crs.latLngToPoint(latlng, zoom); }, unproject: function (point, zoom, unbounded) { // (Point[, Number, Boolean]) -> LatLng // TODO remove unbounded, making it true all the time? zoom = typeof zoom === 'undefined' ? this._zoom : zoom; return this.options.crs.pointToLatLng(point, zoom, unbounded); }, // private methods that modify map state _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 = ''; container.className += ' leaflet-container'; if (L.Browser.touch) { container.className += ' leaflet-touch'; } if (this.options.fadeAnimation) { container.className += ' leaflet-fade-anim'; } var position = L.DomUtil.getStyle(container, 'position'); if (position !== 'absolute' && position !== 'relative') { 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'); }, _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); } }, _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(L.DomUtil.getPosition(this._mapPane)); } this._tileLayersToLoad = this._tileLayersNum; this.fire('viewreset', {hard: !preserveMapOffset}); this.fire('move'); if (zoomChanged || afterZoomAnim) { this.fire('zoomend'); } this.fire('moveend'); if (!this._loaded) { this._loaded = true; this.fire('load'); } }, _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]); } }, _rawPanBy: function (offset) { var newPos = L.DomUtil.getPosition(this._mapPane).subtract(offset); L.DomUtil.setPosition(this._mapPane, newPos); }, // map events _initEvents: function () { if (!L.DomEvent) { return; } L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this); var events = ['dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu']; var i, len; for (i = 0, len = events.length; i < len; i++) { L.DomEvent.addListener(this._container, events[i], this._fireMouseEvent, this); } if (this.options.trackResize) { L.DomEvent.addListener(window, 'resize', this._onResize, this); } }, _onResize: function () { L.Util.requestAnimFrame(this.invalidateSize, this, false, this._container); }, _onMouseClick: function (e) { if (!this._loaded || (this.dragging && this.dragging.moved())) { return; } this.fire('pre' + e.type); 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 _getTopLeftPoint: function () { if (!this._loaded) { throw new Error('Set map center and zoom first.'); } var mapPanePos = L.DomUtil.getPosition(this._mapPane); return this._initialTopLeftPoint.subtract(mapPanePos); }, _getNewTopLeftPoint: function (center) { var viewHalf = this.getSize().divideBy(2); // TODO round on display, not calculation to increase precision? return this.project(center)._subtract(viewHalf)._round(); }, _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.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, unbounded) { // (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, unbounded); } }; 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: '', opacity: 1, scheme: 'xyz', continuousWorld: false, noWrap: false, zoomOffset: 0, zoomReverse: false, detectRetina: false, unloadInvisibleTiles: L.Browser.mobile, updateWhenIdle: L.Browser.mobile, reuseTiles: false }, initialize: function (url, options) { options = L.Util.setOptions(this, options); // detecting retina displays, adjusting tileSize and zoom levels if (options.detectRetina && window.devicePixelRatio > 1 && 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, insertAtTheBottom) { this._map = map; this._insertAtTheBottom = insertAtTheBottom; // 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, this); map.on('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(); }, onRemove: function (map) { map._panes.tilePane.removeChild(this._container); map.off('viewreset', this._resetCallback, this); map.off('moveend', this._update, this); if (!this.options.updateWhenIdle) { map.off('move', this._limitedUpdate, this); } this._container = null; this._map = null; }, getAttribution: function () { return this.options.attribution; }, setOpacity: function (opacity) { this.options.opacity = opacity; if (this._map) { this._updateOpacity(); } // 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)'; } } } }, _updateOpacity: function () { L.DomUtil.setOpacity(this._container, this.options.opacity); }, _initContainer: function () { var tilePane = this._map._panes.tilePane, first = tilePane.firstChild; if (!this._container || tilePane.empty) { this._container = L.DomUtil.create('div', 'leaflet-layer'); if (this._insertAtTheBottom && first) { tilePane.insertBefore(this._container, first); } else { 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 = {}; 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; for (j = bounds.min.y; j <= bounds.max.y; j++) { for (i = bounds.min.x; i <= bounds.max.x; i++) { if (!((i + ':' + j) in this._tiles)) { queue.push(new L.Point(i, j)); } } } // 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(); this._tilesToLoad = queue.length; var k, len; for (k = 0, len = this._tilesToLoad; k < len; k++) { this._addTile(queue[k], fragment); } this._container.appendChild(fragment); }, _removeOtherTiles: function (bounds) { var kArr, x, y, key, tile; for (key in this._tiles) { if (this._tiles.hasOwnProperty(key)) { kArr = key.split(':'); x = parseInt(kArr[0], 10); y = parseInt(kArr[1], 10); // remove tile if it's out of bounds if (x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) { this._removeTile(key); } } } }, _removeTile: function (key) { var tile = this._tiles[key]; this.fire("tileunload", {tile: tile, url: tile.src}); if (tile.parentNode === this._container) { this._container.removeChild(tile); } if (this.options.reuseTiles) { this._unusedTiles.push(tile); } tile.src = L.Util.emptyImageUrl; delete this._tiles[key]; }, _addTile: function (tilePoint, container) { var tilePos = this._getTilePos(tilePoint), zoom = this._map.getZoom(), key = tilePoint.x + ':' + tilePoint.y, limit = Math.pow(2, this._getOffsetZoom(zoom)); // wrap tile coordinates if (!this.options.continuousWorld) { if (!this.options.noWrap) { tilePoint.x = ((tilePoint.x % limit) + limit) % limit; } else if (tilePoint.x < 0 || tilePoint.x >= limit) { this._tilesToLoad--; return; } if (tilePoint.y < 0 || tilePoint.y >= limit) { this._tilesToLoad--; return; } } // get unused tile - or create a new tile var tile = this._getTile(); L.DomUtil.setPosition(tile, tilePos); this._tiles[key] = tile; if (this.options.scheme === 'tms') { tilePoint.y = limit - tilePoint.y - 1; } this._loadTile(tile, tilePoint, zoom); container.appendChild(tile); }, _getOffsetZoom: function (zoom) { var options = this.options; zoom = options.zoomReverse ? options.maxZoom - zoom : zoom; return zoom + options.zoomOffset; }, _getTilePos: function (tilePoint) { var origin = this._map.getPixelOrigin(), tileSize = this.options.tileSize; return tilePoint.multiplyBy(tileSize).subtract(origin); }, // image-specific code (override to implement e.g. Canvas or SVG tile layer) getTileUrl: function (tilePoint, zoom) { var subdomains = this.options.subdomains, index = (tilePoint.x + tilePoint.y) % subdomains.length, s = this.options.subdomains[index]; return L.Util.template(this._url, L.Util.extend({ s: s, z: this._getOffsetZoom(zoom), x: tilePoint.x, y: tilePoint.y }, this.options)); }, _createTileProto: function () { var img = this._tileImg = L.DomUtil.create('img', 'leaflet-tile'); img.galleryimg = 'no'; var tileSize = this.options.tileSize; img.style.width = tileSize + 'px'; img.style.height = tileSize + 'px'; }, _getTile: function () { if (this.options.reuseTiles && this._unusedTiles.length > 0) { var tile = this._unusedTiles.pop(); this._resetTile(tile); return tile; } return this._createTile(); }, _resetTile: function (tile) { // Override if data stored on a tile needs to be cleaned up before reuse }, _createTile: function () { var tile = this._tileImg.cloneNode(false); tile.onselectstart = tile.onmousemove = L.Util.falseFn; return tile; }, _loadTile: function (tile, tilePoint, zoom) { tile._layer = this; tile.onload = this._tileOnLoad; tile.onerror = this._tileOnError; tile.src = this.getTileUrl(tilePoint, zoom); }, _tileLoaded: function () { this._tilesToLoad--; if (!this._tilesToLoad) { this.fire('load'); } }, _tileOnLoad: function (e) { var layer = this._layer; this.className += ' leaflet-tile-loaded'; layer.fire('tileload', { tile: this, url: this.src }); layer._tileLoaded(); }, _tileOnError: function (e) { var layer = this._layer; layer.fire('tileerror', { tile: this, url: this.src }); var newUrl = layer.options.errorTileUrl; if (newUrl) { this.src = newUrl; } layer._tileLoaded(); } }); L.TileLayer.WMS = L.TileLayer.extend({ defaultWmsParams: { service: 'WMS', request: 'GetMap', version: '1.1.1', layers: '', styles: '', format: 'image/jpeg', transparent: false }, initialize: function (url, options) { // (String, Object) this._url = url; var wmsParams = L.Util.extend({}, this.defaultWmsParams); wmsParams.width = wmsParams.height = this.options.tileSize; for (var i in options) { // all keys that are not TileLayer options go to WMS params if (!this.options.hasOwnProperty(i)) { wmsParams[i] = options[i]; } } this.wmsParams = wmsParams; L.Util.setOptions(this, options); }, onAdd: function (map, insertAtTheBottom) { var projectionKey = parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs'; this.wmsParams[projectionKey] = map.options.crs.code; L.TileLayer.prototype.onAdd.call(this, map, insertAtTheBottom); }, getTileUrl: function (tilePoint, zoom) { // (Point, Number) -> String var map = this._map, crs = map.options.crs, tileSize = this.options.tileSize, nwPoint = tilePoint.multiplyBy(tileSize), sePoint = nwPoint.add(new L.Point(tileSize, tileSize)), nwMap = map.unproject(nwPoint, zoom, true), seMap = map.unproject(sePoint, zoom, true), nw = crs.project(nwMap), se = crs.project(seMap), bbox = [nw.x, se.y, se.x, nw.y].join(','); return this._url + L.Util.getParamString(this.wmsParams) + "&bbox=" + bbox; } }); L.TileLayer.Canvas = L.TileLayer.extend({ options: { async: false }, initialize: function (options) { L.Util.setOptions(this, options); }, redraw: function () { var i, tiles = this._tiles; for (i in tiles) { if (tiles.hasOwnProperty(i)) { this._redrawTile(tiles[i]); } } }, _redrawTile: function (tile) { this.drawTile(tile, tile._tilePoint, tile._zoom); }, _createTileProto: function () { var proto = this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile'); var tileSize = this.options.tileSize; proto.width = tileSize; proto.height = tileSize; }, _createTile: function () { var tile = this._canvasProto.cloneNode(false); tile.onselectstart = tile.onmousemove = L.Util.falseFn; return tile; }, _loadTile: function (tile, tilePoint, zoom) { tile._layer = this; tile._tilePoint = tilePoint; tile._zoom = zoom; this.drawTile(tile, tilePoint, zoom); if (!this.options.async) { this.tileDrawn(tile); } }, drawTile: function (tile, tilePoint, zoom) { // override with rendering code }, tileDrawn: function (tile) { this._tileOnLoad.call(tile); } }); L.ImageOverlay