/* * 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.setOptions(this, options); this._initContainer(id); this._initLayout(); // hack for https://github.com/Leaflet/Leaflet/issues/1980 this._onResize = L.bind(this._onResize, this); this._initEvents(); if (options.maxBounds) { this.setMaxBounds(options.maxBounds); } if (options.center && options.zoom !== undefined) { this.setView(L.latLng(options.center), options.zoom, {reset: true}); } this._handlers = []; this._layers = {}; this._zoomBoundLayers = {}; this._tileLayersNum = 0; this.callInitHooks(); this._addLayers(options.layers); }, // public methods that modify map state // replaced by animation-powered implementation in Map.PanAnimation.js setView: function (center, zoom) { zoom = zoom === undefined ? this.getZoom() : zoom; this._resetView(L.latLng(center), this._limitZoom(zoom)); return this; }, setZoom: function (zoom, options) { if (!this._loaded) { this._zoom = this._limitZoom(zoom); return this; } return this.setView(this.getCenter(), zoom, {zoom: options}); }, zoomIn: function (delta, options) { return this.setZoom(this._zoom + (delta || 1), options); }, zoomOut: function (delta, options) { return this.setZoom(this._zoom - (delta || 1), options); }, setZoomAround: function (latlng, zoom, options) { var scale = this.getZoomScale(zoom), viewHalf = this.getSize().divideBy(2), containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng), centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale), newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset)); return this.setView(newCenter, zoom, {zoom: options}); }, fitBounds: function (bounds, options) { options = options || {}; bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds); var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)), paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), swPoint = this.project(bounds.getSouthWest(), zoom), nePoint = this.project(bounds.getNorthEast(), zoom), center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); zoom = options && options.maxZoom ? Math.min(options.maxZoom, zoom) : zoom; return this.setView(center, zoom, options); }, fitWorld: function (options) { return this.fitBounds([[-90, -180], [90, 180]], options); }, panTo: function (center, options) { // (LatLng) return this.setView(center, this._zoom, {pan: options}); }, panBy: function (offset) { // (Point) // replaced with animated panBy in Map.PanAnimation.js this.fire('movestart'); this._rawPanBy(L.point(offset)); this.fire('move'); return this.fire('moveend'); }, setMaxBounds: function (bounds) { bounds = L.latLngBounds(bounds); this.options.maxBounds = bounds; if (!bounds) { return this.off('moveend', this._panInsideMaxBounds, this); } if (this._loaded) { this._panInsideMaxBounds(); } return this.on('moveend', this._panInsideMaxBounds, this); }, panInsideBounds: function (bounds, options) { var center = this.getCenter(), newCenter = this._limitCenter(center, this._zoom, bounds); if (center.equals(newCenter)) { return this; } return this.panTo(newCenter, options); }, addLayer: function (layer) { // TODO method is too big, refactor var id = L.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) || !isNaN(layer.options.minZoom))) { this._zoomBoundLayers[id] = layer; this._updateZoomLevels(); } // TODO looks ugly, refactor!!! if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) { this._tileLayersNum++; this._tileLayersToLoad++; layer.on('load', this._onTileLayerLoad, this); } if (this._loaded) { this._layerAdd(layer); } return this; }, removeLayer: function (layer) { var id = L.stamp(layer); if (!this._layers[id]) { return this; } if (this._loaded) { layer.onRemove(this); } delete this._layers[id]; if (this._loaded) { this.fire('layerremove', {layer: layer}); } if (this._zoomBoundLayers[id]) { delete this._zoomBoundLayers[id]; this._updateZoomLevels(); } // 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; }, hasLayer: function (layer) { if (!layer) { return false; } return (L.stamp(layer) in this._layers); }, eachLayer: function (method, context) { for (var i in this._layers) { method.call(context, this._layers[i]); } return this; }, invalidateSize: function (options) { options = L.extend({ animate: false, pan: true }, options === true ? {animate: true} : options); var oldSize = this.getSize(); this._sizeChanged = true; this._initialCenter = null; if (!this._loaded) { return this; } var newSize = this.getSize(), oldCenter = oldSize.divideBy(2).round(), newCenter = newSize.divideBy(2).round(), offset = oldCenter.subtract(newCenter); if (!offset.x && !offset.y) { return this; } if (options.animate && options.pan) { this.panBy(offset); } else { if (options.pan) { this._rawPanBy(offset); } this.fire('move'); if (options.debounceMoveend) { clearTimeout(this._sizeTimer); this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); } else { this.fire('moveend'); } } return this.fire('resize', { oldSize: oldSize, newSize: newSize }); }, // TODO handler.addTo addHandler: function (name, HandlerClass) { if (!HandlerClass) { return this; } var handler = this[name] = new HandlerClass(this); this._handlers.push(handler); if (this.options[name]) { handler.enable(); } return this; }, remove: function () { if (this._loaded) { this.fire('unload'); } this._initEvents('off'); try { // throws error in IE6-8 delete this._container._leaflet; } catch (e) { this._container._leaflet = undefined; } this._clearPanes(); if (this._clearControlPos) { this._clearControlPos(); } this._clearHandlers(); return this; }, // public methods for getting map state getCenter: function () { // (Boolean) -> LatLng this._checkIfLoaded(); if (this._initialCenter && !this._moved()) { return this._initialCenter; } return this.layerPointToLatLng(this._getCenterLayerPoint()); }, getZoom: function () { return this._zoom; }, getBounds: function () { var bounds = this.getPixelBounds(), sw = this.unproject(bounds.getBottomLeft()), ne = this.unproject(bounds.getTopRight()); return new L.LatLngBounds(sw, ne); }, getMinZoom: function () { return this.options.minZoom === undefined ? (this._layersMinZoom === undefined ? 0 : this._layersMinZoom) : this.options.minZoom; }, getMaxZoom: function () { return this.options.maxZoom === undefined ? (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : this.options.maxZoom; }, getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number bounds = L.latLngBounds(bounds); var zoom = this.getMinZoom() - (inside ? 1 : 0), maxZoom = this.getMaxZoom(), size = this.getSize(), nw = bounds.getNorthWest(), se = bounds.getSouthEast(), zoomNotFound = true, boundsSize; padding = L.point(padding || [0, 0]); do { zoom++; boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding); zoomNotFound = !inside ? size.contains(boundsSize) : boundsSize.x < size.x || boundsSize.y < size.y; } while (zoomNotFound && zoom <= maxZoom); if (zoomNotFound && inside) { return null; } return inside ? zoom : zoom - 1; }, getSize: function () { if (!this._size || this._sizeChanged) { this._size = new L.Point( this._container.clientWidth, this._container.clientHeight); this._sizeChanged = false; } return this._size.clone(); }, getPixelBounds: function () { var topLeftPoint = this._getTopLeftPoint(); return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); }, getPixelOrigin: function () { this._checkIfLoaded(); return this._initialTopLeftPoint; }, getPanes: function () { return this._panes; }, getContainer: function () { return this._container; }, // TODO replace with universal implementation after refactoring projections getZoomScale: function (toZoom) { var crs = this.options.crs; return crs.scale(toZoom) / crs.scale(this._zoom); }, getScaleZoom: function (scale) { return this._zoom + (Math.log(scale) / Math.LN2); }, // conversion methods project: function (latlng, zoom) { // (LatLng[, Number]) -> Point zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.latLngToPoint(L.latLng(latlng), zoom); }, unproject: function (point, zoom) { // (Point[, Number]) -> LatLng zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.pointToLatLng(L.point(point), zoom); }, layerPointToLatLng: function (point) { // (Point) var projectedPoint = L.point(point).add(this.getPixelOrigin()); return this.unproject(projectedPoint); }, latLngToLayerPoint: function (latlng) { // (LatLng) var projectedPoint = this.project(L.latLng(latlng))._round(); return projectedPoint._subtract(this.getPixelOrigin()); }, containerPointToLayerPoint: function (point) { // (Point) return L.point(point).subtract(this._getMapPanePos()); }, layerPointToContainerPoint: function (point) { // (Point) return L.point(point).add(this._getMapPanePos()); }, containerPointToLatLng: function (point) { var layerPoint = this.containerPointToLayerPoint(L.point(point)); return this.layerPointToLatLng(layerPoint); }, latLngToContainerPoint: function (latlng) { return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng))); }, mouseEventToContainerPoint: function (e) { // (MouseEvent) return L.DomEvent.getMousePosition(e, this._container); }, mouseEventToLayerPoint: function (e) { // (MouseEvent) return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); }, mouseEventToLatLng: function (e) { // (MouseEvent) return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); }, // map initialization methods _initContainer: function (id) { var container = this._container = L.DomUtil.get(id); if (!container) { throw new Error('Map container not found.'); } else if (container._leaflet) { throw new Error('Map container is already initialized.'); } container._leaflet = true; }, _initLayout: function () { var container = this._container; L.DomUtil.addClass(container, 'leaflet-container' + (L.Browser.touch ? ' leaflet-touch' : '') + (L.Browser.retina ? ' leaflet-retina' : '') + (L.Browser.ielt9 ? ' leaflet-oldie' : '') + (this.options.fadeAnimation ? ' 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); 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._panes.objectsPane); }, _clearPanes: function () { this._container.removeChild(this._mapPane); }, _addLayers: function (layers) { layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : []; for (var i = 0, len = layers.length; i < len; i++) { this.addLayer(layers[i]); } }, // private methods that modify map state _resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) { var zoomChanged = (this._zoom !== zoom); if (!afterZoomAnim) { this.fire('movestart'); if (zoomChanged) { this.fire('zoomstart'); } } this._zoom = zoom; this._initialCenter = center; this._initialTopLeftPoint = this._getNewTopLeftPoint(center); if (!preserveMapOffset) { L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); } else { this._initialTopLeftPoint._add(this._getMapPanePos()); } this._tileLayersToLoad = this._tileLayersNum; var loading = !this._loaded; this._loaded = true; if (loading) { this.fire('load'); this.eachLayer(this._layerAdd, this); } this.fire('viewreset', {hard: !preserveMapOffset}); this.fire('move'); if (zoomChanged || afterZoomAnim) { this.fire('zoomend'); } this.fire('moveend', {hard: !preserveMapOffset}); }, _rawPanBy: function (offset) { L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); }, _getZoomSpan: function () { return this.getMaxZoom() - this.getMinZoom(); }, _updateZoomLevels: function () { var i, minZoom = Infinity, maxZoom = -Infinity, oldZoomSpan = this._getZoomSpan(); for (i in this._zoomBoundLayers) { var layer = this._zoomBoundLayers[i]; if (!isNaN(layer.options.minZoom)) { minZoom = Math.min(minZoom, layer.options.minZoom); } if (!isNaN(layer.options.maxZoom)) { maxZoom = Math.max(maxZoom, layer.options.maxZoom); } } if (i === undefined) { // we have no tilelayers this._layersMaxZoom = this._layersMinZoom = undefined; } else { this._layersMaxZoom = maxZoom; this._layersMinZoom = minZoom; } if (oldZoomSpan !== this._getZoomSpan()) { this.fire('zoomlevelschange'); } }, _panInsideMaxBounds: function () { this.panInsideBounds(this.options.maxBounds); }, _checkIfLoaded: function () { if (!this._loaded) { throw new Error('Set map center and zoom first.'); } }, // map events _initEvents: function (onOff) { if (!L.DomEvent) { return; } onOff = onOff || 'on'; L.DomEvent[onOff](this._container, 'click', 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[onOff](this._container, events[i], this._fireMouseEvent, this); } if (this.options.trackResize) { L.DomEvent[onOff](window, 'resize', this._onResize, this); } }, _onResize: function () { L.Util.cancelAnimFrame(this._resizeRequest); this._resizeRequest = L.Util.requestAnimFrame( function () { this.invalidateSize({debounceMoveend: true}); }, this, false, this._container); }, _onMouseClick: function (e) { if (!this._loaded || (!e._simulated && ((this.dragging && this.dragging.moved()) || (this.boxZoom && this.boxZoom.moved()))) || L.DomEvent._skipped(e)) { return; } this.fire('preclick'); this._fireMouseEvent(e); }, _fireMouseEvent: function (e) { if (!this._loaded || L.DomEvent._skipped(e)) { 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 () { this._tileLayersToLoad--; if (this._tileLayersNum && !this._tileLayersToLoad) { this.fire('tilelayersload'); } }, _clearHandlers: function () { for (var i = 0, len = this._handlers.length; i < len; i++) { this._handlers[i].disable(); } }, whenReady: function (callback, context) { if (this._loaded) { callback.call(context || this, this); } else { this.on('load', callback, context); } return this; }, _layerAdd: function (layer) { layer.onAdd(this); this.fire('layeradd', {layer: layer}); }, // private methods for getting map state _getMapPanePos: function () { return L.DomUtil.getPosition(this._mapPane); }, _moved: function () { var pos = this._getMapPanePos(); return pos && !pos.equals([0, 0]); }, _getTopLeftPoint: function () { return this.getPixelOrigin().subtract(this._getMapPanePos()); }, _getNewTopLeftPoint: function (center, zoom) { var viewHalf = this.getSize()._divideBy(2); // TODO round on display, not calculation to increase precision? return this.project(center, zoom)._subtract(viewHalf)._round(); }, _latLngToNewLayerPoint: function (latlng, newZoom, newCenter) { var topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(this._getMapPanePos()); return this.project(latlng, newZoom)._subtract(topLeft); }, // layer point of the current center _getCenterLayerPoint: function () { return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); }, // offset of the specified place to the current center in pixels _getCenterOffset: function (latlng) { return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint()); }, // adjust center for view to get inside bounds _limitCenter: function (center, zoom, bounds) { if (!bounds) { return center; } var centerPoint = this.project(center, zoom), viewHalf = this.getSize().divideBy(2), viewBounds = new L.Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), offset = this._getBoundsOffset(viewBounds, bounds, zoom); return this.unproject(centerPoint.add(offset), zoom); }, // adjust offset for view to get inside bounds _limitOffset: function (offset, bounds) { if (!bounds) { return offset; } var viewBounds = this.getPixelBounds(), newBounds = new L.Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset)); return offset.add(this._getBoundsOffset(newBounds, bounds)); }, // returns offset needed for pxBounds to get inside maxBounds at a specified zoom _getBoundsOffset: function (pxBounds, maxBounds, zoom) { var nwOffset = this.project(maxBounds.getNorthWest(), zoom).subtract(pxBounds.min), seOffset = this.project(maxBounds.getSouthEast(), zoom).subtract(pxBounds.max), dx = this._rebound(nwOffset.x, -seOffset.x), dy = this._rebound(nwOffset.y, -seOffset.y); return new L.Point(dx, dy); }, _rebound: function (left, right) { return left + right > 0 ? Math.round(left - right) / 2 : Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right)); }, _limitZoom: function (zoom) { var min = this.getMinZoom(), max = this.getMaxZoom(); return Math.max(min, Math.min(max, zoom)); } }); L.map = function (id, options) { return new L.Map(id, options); };