Leaflet/src/map/Map.js

744 lines
18 KiB
JavaScript
Raw Normal View History

/*
* 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();
this.callInitHooks();
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);
},
2012-08-11 04:11:28 +08:00
zoomIn: function (delta) {
return this.setZoom(this._zoom + (delta || 1));
},
2012-08-11 04:11:28 +08:00
zoomOut: function (delta) {
return this.setZoom(this._zoom - (delta || 1));
},
setZoomAround: function (latlng, zoom) {
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);
},
fitBounds: function (bounds, paddingTopLeft, paddingBottomRight) { // (LatLngBounds || ILayer[, Point, Point])
bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds);
paddingTopLeft = L.point(paddingTopLeft || [0, 0]);
paddingBottomRight = L.point(paddingBottomRight || paddingTopLeft);
var zoom = this.getBoundsZoom(bounds, false, paddingTopLeft.add(paddingBottomRight)),
2013-04-22 22:20:34 +08:00
paddingOffset = paddingBottomRight.subtract(paddingTopLeft).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 this.setView(center, zoom);
},
fitWorld: function () {
return this.fitBounds([[-90, -180], [90, 180]]);
},
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);
}
}
this.on('moveend', this._panInsideMaxBounds, this);
return this;
},
panInsideBounds: function (bounds) {
bounds = L.latLngBounds(bounds);
var viewBounds = this.getPixelBounds(),
viewSw = viewBounds.getBottomLeft(),
viewNe = viewBounds.getTopRight(),
sw = this.project(bounds.getSouthWest()),
ne = this.project(bounds.getNorthEast()),
dx = 0,
dy = 0;
if (viewNe.y < ne.y) { // north
dy = Math.ceil(ne.y - viewNe.y);
}
if (viewNe.x > ne.x) { // east
dx = Math.floor(ne.x - viewNe.x);
}
if (viewSw.y > sw.y) { // south
dy = Math.floor(sw.y - viewSw.y);
}
if (viewSw.x < sw.x) { // west
dx = Math.ceil(sw.x - viewSw.x);
}
if (dx || dy) {
2013-04-22 22:20:34 +08:00
return this.panBy([dx, dy]);
}
return this;
},
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);
}
2012-10-13 03:49:12 +08:00
this.whenReady(function () {
layer.onAdd(this);
this.fire('layeradd', {layer: layer});
2012-10-13 03:49:12 +08:00
}, this);
return this;
},
removeLayer: function (layer) {
var id = L.stamp(layer);
if (!this._layers[id]) { return; }
layer.onRemove(this);
delete this._layers[id];
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.fire('layerremove', {layer: layer});
},
hasLayer: function (layer) {
if (!layer) { return false; }
2013-04-23 00:20:19 +08:00
return (L.stamp(layer) in this._layers);
},
2013-02-27 03:39:00 +08:00
eachLayer: function (method, context) {
for (var i in this._layers) {
method.call(context, this._layers[i]);
2013-02-27 03:39:00 +08:00
}
return this;
},
invalidateSize: function (animate) {
var oldSize = this.getSize();
this._sizeChanged = true;
if (this.options.maxBounds) {
this.setMaxBounds(this.options.maxBounds);
}
if (!this._loaded) { return this; }
2013-04-18 20:26:45 +08:00
var newSize = this.getSize(),
offset = oldSize.subtract(newSize).divideBy(2).round();
if ((offset.x !== 0) || (offset.y !== 0)) {
if (animate === true) {
this.panBy(offset);
} else {
this._rawPanBy(offset);
this.fire('move');
clearTimeout(this._sizeTimer);
this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200);
}
2013-04-18 20:26:45 +08:00
this.fire('resize', {
oldSize: oldSize,
newSize: newSize
});
}
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;
},
remove: function () {
if (this._loaded) {
this.fire('unload');
}
this._initEvents('off');
delete this._container._leaflet;
return this;
},
// public methods for getting map state
getCenter: function () { // (Boolean) -> LatLng
this._checkIfLoaded();
if (!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 () {
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, 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');
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);
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);
},
_initLayers: function (layers) {
layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : [];
this._layers = {};
this._zoomBoundLayers = {};
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._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;
2013-04-20 17:07:00 +08:00
if (loading) {
this.fire('load');
}
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));
},
2013-02-14 19:00:08 +08:00
_getZoomSpan: function () {
return this.getMaxZoom() - this.getMinZoom();
},
_updateZoomLevels: function () {
var i,
minZoom = Infinity,
maxZoom = -Infinity,
2013-02-14 19:00:08 +08:00
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;
}
2013-02-14 19:00:08 +08:00
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(
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 () {
this._tileLayersToLoad--;
if (this._tileLayersNum && !this._tileLayersToLoad) {
this.fire('tilelayersload');
}
},
2012-10-13 03:49:12 +08:00
whenReady: function (callback, context) {
if (this._loaded) {
callback.call(context || this, this);
} else {
this.on('load', callback, context);
}
return this;
},
// private methods for getting map state
_getMapPanePos: function () {
return L.DomUtil.getPosition(this._mapPane);
},
_moved: function () {
var pos = this._getMapPanePos();
2013-04-22 22:20:34 +08:00
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());
},
_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);
};