UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

835 lines (627 loc) 22.5 kB
/* * L.Map is the central class of the API - it is used to create a map. */ L.Map = L.Evented.extend({ options: { crs: L.CRS.EPSG3857, /* center: LatLng, zoom: Number, layers: Array, */ fadeAnimation: true, trackResize: true, markerZoomAnimation: true, maxBoundsViscosity: 0.0, transform3DLimit: 8388608 // Precision limit of a 32-bit float }, initialize: function (id, options) { // (HTMLElement or String, Object) options = L.setOptions(this, options); this._initContainer(id); this._initLayout(); // hack for https://github.com/Leaflet/Leaflet/issues/1980 this._onResize = L.bind(this._onResize, this); this._initEvents(); if (options.maxBounds) { this.setMaxBounds(options.maxBounds); } if (options.zoom !== undefined) { this._zoom = this._limitZoom(options.zoom); } if (options.center && options.zoom !== undefined) { this.setView(L.latLng(options.center), options.zoom, {reset: true}); } this._handlers = []; this._layers = {}; this._zoomBoundLayers = {}; this._sizeChanged = true; this.callInitHooks(); this._addLayers(this.options.layers); }, // public methods that modify map state // replaced by animation-powered implementation in Map.PanAnimation.js setView: function (center, zoom) { zoom = zoom === undefined ? this.getZoom() : zoom; this._resetView(L.latLng(center), zoom); return this; }, setZoom: function (zoom, options) { if (!this._loaded) { this._zoom = 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}); }, _getBoundsCenterZoom: function (bounds, options) { options = options || {}; bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds); var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); zoom = options.maxZoom ? Math.min(options.maxZoom, zoom) : zoom; var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), swPoint = this.project(bounds.getSouthWest(), zoom), nePoint = this.project(bounds.getNorthEast(), zoom), center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); return { center: center, zoom: zoom }; }, fitBounds: function (bounds, options) { var target = this._getBoundsCenterZoom(bounds, options); return this.setView(target.center, target.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); if (!bounds) { return this.off('moveend', this._panInsideMaxBounds); } else if (this.options.maxBounds) { this.off('moveend', this._panInsideMaxBounds); } this.options.maxBounds = bounds; if (this._loaded) { this._panInsideMaxBounds(); } return this.on('moveend', this._panInsideMaxBounds); }, setMinZoom: function (zoom) { this.options.minZoom = zoom; if (this._loaded && this.getZoom() < this.options.minZoom) { return this.setZoom(zoom); } return this; }, setMaxZoom: function (zoom) { this.options.maxZoom = zoom; if (this._loaded && (this.getZoom() > this.options.maxZoom)) { return this.setZoom(zoom); } return this; }, panInsideBounds: function (bounds, options) { this._enforcingBounds = true; var center = this.getCenter(), newCenter = this._limitCenter(center, this._zoom, L.latLngBounds(bounds)); if (center.equals(newCenter)) { return this; } this.panTo(newCenter, options); this._enforcingBounds = false; return this; }, invalidateSize: function (options) { if (!this._loaded) { return this; } options = L.extend({ animate: false, pan: true }, options === true ? {animate: true} : options); var oldSize = this.getSize(); this._sizeChanged = true; this._lastCenter = null; var newSize = this.getSize(), oldCenter = oldSize.divideBy(2).round(), newCenter = newSize.divideBy(2).round(), offset = oldCenter.subtract(newCenter); if (!offset.x && !offset.y) { return this; } if (options.animate && options.pan) { this.panBy(offset); } else { if (options.pan) { this._rawPanBy(offset); } this.fire('move'); if (options.debounceMoveend) { clearTimeout(this._sizeTimer); this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); } else { this.fire('moveend'); } } return this.fire('resize', { oldSize: oldSize, newSize: newSize }); }, stop: function () { L.Util.cancelAnimFrame(this._flyToFrame); if (this._panAnim) { this._panAnim.stop(); } return this; }, // TODO handler.addTo addHandler: function (name, HandlerClass) { if (!HandlerClass) { return this; } var handler = this[name] = new HandlerClass(this); this._handlers.push(handler); if (this.options[name]) { handler.enable(); } return this; }, remove: function () { this._initEvents(true); try { // throws error in IE6-8 delete this._container._leaflet; } catch (e) { this._container._leaflet = undefined; } L.DomUtil.remove(this._mapPane); if (this._clearControlPos) { this._clearControlPos(); } this._clearHandlers(); if (this._loaded) { this.fire('unload'); } for (var i in this._layers) { this._layers[i].remove(); } return this; }, createPane: function (name, container) { var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''), pane = L.DomUtil.create('div', className, container || this._mapPane); if (name) { this._panes[name] = pane; } return pane; }, // public methods for getting map state getCenter: function () { // (Boolean) -> LatLng this._checkIfLoaded(); if (this._lastCenter && !this._moved()) { return this._lastCenter; } return this.layerPointToLatLng(this._getCenterLayerPoint()); }, getZoom: function () { return this._zoom; }, getBounds: function () { var bounds = this.getPixelBounds(), sw = this.unproject(bounds.getBottomLeft()), ne = this.unproject(bounds.getTopRight()); return new L.LatLngBounds(sw, ne); }, getMinZoom: function () { return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom; }, getMaxZoom: function () { return this.options.maxZoom === undefined ? (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : this.options.maxZoom; }, getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number bounds = L.latLngBounds(bounds); var zoom = this.getMinZoom() - (inside ? 1 : 0), maxZoom = this.getMaxZoom(), size = this.getSize(), nw = bounds.getNorthWest(), se = bounds.getSouthEast(), zoomNotFound = true, boundsSize; padding = L.point(padding || [0, 0]); do { zoom++; boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding).floor(); 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 (center, zoom) { var topLeftPoint = this._getTopLeftPoint(center, zoom); return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); }, getPixelOrigin: function () { this._checkIfLoaded(); return this._pixelOrigin; }, getPixelWorldBounds: function (zoom) { return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom); }, getPane: function (pane) { return typeof pane === 'string' ? this._panes[pane] : pane; }, getPanes: function () { return this._panes; }, getContainer: function () { return this._container; }, // TODO replace with universal implementation after refactoring projections getZoomScale: function (toZoom, fromZoom) { var crs = this.options.crs; fromZoom = fromZoom === undefined ? this._zoom : fromZoom; return crs.scale(toZoom) / crs.scale(fromZoom); }, getScaleZoom: function (scale, fromZoom) { var crs = this.options.crs; fromZoom = fromZoom === undefined ? this._zoom : fromZoom; return crs.zoom(scale * crs.scale(fromZoom)); }, // conversion methods project: function (latlng, zoom) { // (LatLng[, Number]) -> Point zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.latLngToPoint(L.latLng(latlng), zoom); }, unproject: function (point, zoom) { // (Point[, Number]) -> LatLng zoom = zoom === undefined ? this._zoom : zoom; return this.options.crs.pointToLatLng(L.point(point), zoom); }, layerPointToLatLng: function (point) { // (Point) var projectedPoint = L.point(point).add(this.getPixelOrigin()); return this.unproject(projectedPoint); }, latLngToLayerPoint: function (latlng) { // (LatLng) var projectedPoint = this.project(L.latLng(latlng))._round(); return projectedPoint._subtract(this.getPixelOrigin()); }, wrapLatLng: function (latlng) { return this.options.crs.wrapLatLng(L.latLng(latlng)); }, distance: function (latlng1, latlng2) { return this.options.crs.distance(L.latLng(latlng1), L.latLng(latlng2)); }, containerPointToLayerPoint: function (point) { // (Point) return L.point(point).subtract(this._getMapPanePos()); }, layerPointToContainerPoint: function (point) { // (Point) return L.point(point).add(this._getMapPanePos()); }, containerPointToLatLng: function (point) { var layerPoint = this.containerPointToLayerPoint(L.point(point)); return this.layerPointToLatLng(layerPoint); }, latLngToContainerPoint: function (latlng) { return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng))); }, mouseEventToContainerPoint: function (e) { // (MouseEvent) return L.DomEvent.getMousePosition(e, this._container); }, mouseEventToLayerPoint: function (e) { // (MouseEvent) return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); }, mouseEventToLatLng: function (e) { // (MouseEvent) return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); }, // map initialization methods _initContainer: function (id) { var container = this._container = L.DomUtil.get(id); if (!container) { throw new Error('Map container not found.'); } else if (container._leaflet) { throw new Error('Map container is already initialized.'); } L.DomEvent.addListener(container, 'scroll', this._onScroll, this); container._leaflet = true; }, _initLayout: function () { var container = this._container; this._fadeAnimated = this.options.fadeAnimation && L.Browser.any3d; L.DomUtil.addClass(container, 'leaflet-container' + (L.Browser.touch ? ' leaflet-touch' : '') + (L.Browser.retina ? ' leaflet-retina' : '') + (L.Browser.ielt9 ? ' leaflet-oldie' : '') + (L.Browser.safari ? ' leaflet-safari' : '') + (this._fadeAnimated ? ' leaflet-fade-anim' : '')); var position = L.DomUtil.getStyle(container, 'position'); if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') { container.style.position = 'relative'; } this._initPanes(); if (this._initControlPos) { this._initControlPos(); } }, _initPanes: function () { var panes = this._panes = {}; this._paneRenderers = {}; this._mapPane = this.createPane('mapPane', this._container); L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); this.createPane('tilePane'); this.createPane('shadowPane'); this.createPane('overlayPane'); this.createPane('markerPane'); this.createPane('popupPane'); if (!this.options.markerZoomAnimation) { L.DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide'); L.DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide'); } }, // private methods that modify map state _resetView: function (center, zoom) { L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); var loading = !this._loaded; this._loaded = true; zoom = this._limitZoom(zoom); var zoomChanged = this._zoom !== zoom; this ._moveStart(zoomChanged) ._move(center, zoom) ._moveEnd(zoomChanged); this.fire('viewreset'); if (loading) { this.fire('load'); } }, _moveStart: function (zoomChanged) { if (zoomChanged) { this.fire('zoomstart'); } return this.fire('movestart'); }, _move: function (center, zoom, data) { if (zoom === undefined) { zoom = this._zoom; } var zoomChanged = this._zoom !== zoom; this._zoom = zoom; this._lastCenter = center; this._pixelOrigin = this._getNewPixelOrigin(center); if (zoomChanged) { this.fire('zoom', data); } return this.fire('move', data); }, _moveEnd: function (zoomChanged) { if (zoomChanged) { this.fire('zoomend'); } return this.fire('moveend'); }, _rawPanBy: function (offset) { L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); }, _getZoomSpan: function () { return this.getMaxZoom() - this.getMinZoom(); }, _panInsideMaxBounds: function () { if (!this._enforcingBounds) { this.panInsideBounds(this.options.maxBounds); } }, _checkIfLoaded: function () { if (!this._loaded) { throw new Error('Set map center and zoom first.'); } }, // DOM event handling _initEvents: function (remove) { if (!L.DomEvent) { return; } this._targets = {}; this._targets[L.stamp(this._container)] = this; var onOff = remove ? 'off' : 'on'; L.DomEvent[onOff](this._container, 'click dblclick mousedown mouseup ' + 'mouseover mouseout mousemove contextmenu keypress', this._handleDOMEvent, this); if (this.options.trackResize) { L.DomEvent[onOff](window, 'resize', this._onResize, this); } if (L.Browser.any3d && this.options.transform3DLimit) { this[onOff]('moveend', this._onMoveEnd); } }, _onResize: function () { L.Util.cancelAnimFrame(this._resizeRequest); this._resizeRequest = L.Util.requestAnimFrame( function () { this.invalidateSize({debounceMoveend: true}); }, this); }, _onScroll: function () { this._container.scrollTop = 0; this._container.scrollLeft = 0; }, _onMoveEnd: function () { var pos = this._getMapPanePos(); if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) { // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have // a pixel offset on very high values, see: http://jsfiddle.net/dg6r5hhb/ this._resetView(this.getCenter(), this.getZoom()); } }, _findEventTargets: function (e, type) { var targets = [], target, isHover = type === 'mouseout' || type === 'mouseover', src = e.target || e.srcElement; while (src) { target = this._targets[L.stamp(src)]; if (target && target.listens(type, true)) { if (isHover && !L.DomEvent._isExternalTarget(src, e)) { break; } targets.push(target); if (isHover) { break; } } if (src === this._container) { break; } src = src.parentNode; } if (!targets.length && !isHover && L.DomEvent._isExternalTarget(src, e)) { targets = [this]; } return targets; }, _handleDOMEvent: function (e) { if (!this._loaded || L.DomEvent._skipped(e)) { return; } // find the layer the event is propagating from and its parents var type = e.type === 'keypress' && e.keyCode === 13 ? 'click' : e.type; if (e.type === 'click') { // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups). var synth = L.Util.extend({}, e); synth.type = 'preclick'; this._handleDOMEvent(synth); } if (type === 'mousedown') { // prevents outline when clicking on keyboard-focusable element L.DomUtil.preventOutline(e.target || e.srcElement); } this._fireDOMEvent(e, type); }, _fireDOMEvent: function (e, type, targets) { if (e._stopped) { return; } targets = (targets || []).concat(this._findEventTargets(e, type)); if (!targets.length) { return; } var target = targets[0]; if (type === 'contextmenu' && target.listens(type, true)) { L.DomEvent.preventDefault(e); } // prevents firing click after you just dragged an object if ((e.type === 'click' || e.type === 'preclick') && !e._simulated && this._draggableMoved(target)) { return; } var data = { originalEvent: e }; if (e.type !== 'keypress') { var isMarker = target instanceof L.Marker; data.containerPoint = isMarker ? this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e); data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint); } for (var i = 0; i < targets.length; i++) { targets[i].fire(type, data, true); if (data.originalEvent._stopped || (targets[i].options.nonBubblingEvents && L.Util.indexOf(targets[i].options.nonBubblingEvents, type) !== -1)) { return; } } }, _draggableMoved: function (obj) { obj = obj.options.draggable ? obj : this; return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved()); }, _clearHandlers: function () { for (var i = 0, len = this._handlers.length; i < len; i++) { this._handlers[i].disable(); } }, whenReady: function (callback, context) { if (this._loaded) { callback.call(context || this, {target: this}); } else { this.on('load', callback, context); } return this; }, // private methods for getting map state _getMapPanePos: function () { return L.DomUtil.getPosition(this._mapPane) || new L.Point(0, 0); }, _moved: function () { var pos = this._getMapPanePos(); return pos && !pos.equals([0, 0]); }, _getTopLeftPoint: function (center, zoom) { var pixelOrigin = center && zoom !== undefined ? this._getNewPixelOrigin(center, zoom) : this.getPixelOrigin(); return pixelOrigin.subtract(this._getMapPanePos()); }, _getNewPixelOrigin: function (center, zoom) { var viewHalf = this.getSize()._divideBy(2); return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round(); }, _latLngToNewLayerPoint: function (latlng, zoom, center) { var topLeft = this._getNewPixelOrigin(center, zoom); return this.project(latlng, zoom)._subtract(topLeft); }, // 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(); if (!L.Browser.any3d) { zoom = Math.round(zoom); } return Math.max(min, Math.min(max, zoom)); } }); L.map = function (id, options) { return new L.Map(id, options); };