Leaflet/src/map/Map.js
2011-12-08 17:53:17 +02:00

586 lines
15 KiB
JavaScript

/*
* 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: {
// projection
crs: L.CRS.EPSG3857 || L.CRS.EPSG4326,
scale: function (zoom) {
return 256 * Math.pow(2, zoom);
},
// state
center: null,
zoom: null,
layers: [],
// interaction
dragging: true,
touchZoom: L.Browser.touch && !L.Browser.android,
scrollWheelZoom: !L.Browser.touch,
doubleClickZoom: true,
shiftDragZoom: true,
// controls
zoomControl: true,
attributionControl: true,
// animation
fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android,
zoomAnimation: L.DomUtil.TRANSITION && !L.Browser.android && !L.Browser.mobileOpera,
// misc
trackResize: true,
closePopupOnClick: true,
worldCopyJump: true
},
// constructor
initialize: function (id, options) { // (HTMLElement or String, Object)
L.Util.setOptions(this, options);
this._container = L.DomUtil.get(id);
if (this._container._leaflet) {
throw new Error("Map container is already initialized.");
}
this._container._leaflet = true;
this._initLayout();
if (L.DomEvent) {
this._initEvents();
if (L.Handler) {
this._initInteraction();
}
if (L.Control) {
this._initControls();
}
}
if (this.options.maxBounds) {
this.setMaxBounds(this.options.maxBounds);
}
var center = this.options.center,
zoom = this.options.zoom;
if (center !== null && zoom !== null) {
this.setView(center, zoom, true);
}
var layers = this.options.layers;
layers = (layers instanceof Array ? layers : [layers]);
this._tileLayersNum = 0;
this._initLayers(layers);
},
// public methods that modify map state
// replaced by animation-powered implementation in Map.PanAnimation.js
setView: function (center, zoom) {
// reset the map view
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');
this.fire('moveend');
return this;
},
setMaxBounds: function (bounds) {
this.options.maxBounds = bounds;
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, insertAtTheTop) {
var id = L.Util.stamp(layer);
if (this._layers[id]) {
return this;
}
this._layers[id] = layer;
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 getMaxZoom, getMinZoom in ILayer (instead of options)
if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
this._tileLayersNum++;
layer.on('load', this._onTileLayerLoad, this);
}
if (this.attributionControl && layer.getAttribution) {
this.attributionControl.addAttribution(layer.getAttribution());
}
var onMapLoad = function () {
layer.onAdd(this, insertAtTheTop);
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]) {
layer.onRemove(this);
delete this._layers[id];
if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) {
this._tileLayersNum--;
layer.off('load', this._onTileLayerLoad, this);
}
if (this.attributionControl && layer.getAttribution) {
this.attributionControl.removeAttribution(layer.getAttribution());
}
this.fire('layerremove', {layer: layer});
}
return this;
},
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;
}
this._rawPanBy(oldSize.subtract(this.getSize()).divideBy(2));
this.fire('move');
clearTimeout(this._sizeTimer);
this._sizeTimer = setTimeout(L.Util.bind(function () {
this.fire('moveend');
}, this), 200);
return this;
},
// public methods for getting map state
getCenter: function (unbounded) { // (Boolean)
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)),
ne = this.unproject(new L.Point(bounds.max.x, bounds.min.y));
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 = isNaN(this.options.maxZoom) ? Infinity : this.options.maxZoom,
z2 = this._layersMaxZoom || Infinity;
return Math.min(z1, z2);
},
getBoundsZoom: function (bounds, inside) { // (LatLngBounds)
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) {
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(),
size = this.getSize();
return new L.Bounds(topLeftPoint, topLeftPoint.add(size));
},
getPixelOrigin: function () {
return this._initialTopLeftPoint;
},
getPanes: function () {
return this._panes;
},
// 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);
},
project: function (latlng, zoom) { // (LatLng[, Number]) -> Point
zoom = (typeof zoom === 'undefined' ? this._zoom : zoom);
return this.options.crs.latLngToPoint(latlng, this.options.scale(zoom));
},
unproject: function (point, zoom, unbounded) { // (Point[, Number, Boolean]) -> LatLng
zoom = (typeof zoom === 'undefined' ? this._zoom : zoom);
return this.options.crs.pointToLatLng(point, this.options.scale(zoom), unbounded);
},
// private methods that modify map state
_initLayout: function () {
var container = this._container;
container.innerHTML = '';
container.className += ' leaflet-container';
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);
},
_resetView: function (center, zoom, preserveMapOffset) {
var zoomChanged = (this._zoom !== zoom);
this.fire('movestart');
this._zoom = zoom;
this._initialTopLeftPoint = this._getNewTopLeftPoint(center);
if (!preserveMapOffset) {
L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
} else {
var offset = L.DomUtil.getPosition(this._mapPane);
this._initialTopLeftPoint._add(offset);
}
this._tileLayersToLoad = this._tileLayersNum;
this.fire('viewreset', {hard: !preserveMapOffset});
this.fire('move');
if (zoomChanged) { this.fire('zoomend'); }
this.fire('moveend');
if (!this._loaded) {
this._loaded = true;
this.fire('load');
}
},
_initLayers: function (layers) {
this._layers = {};
var i, len;
for (i = 0, len = layers.length; i < len; i++) {
this.addLayer(layers[i]);
}
},
_initControls: function () {
if (this.options.zoomControl) {
this.addControl(new L.Control.Zoom());
}
if (this.options.attributionControl) {
this.attributionControl = new L.Control.Attribution();
this.addControl(this.attributionControl);
}
},
_rawPanBy: function (offset) {
var mapPaneOffset = L.DomUtil.getPosition(this._mapPane);
L.DomUtil.setPosition(this._mapPane, mapPaneOffset.subtract(offset));
},
// map events
_initEvents: function () {
L.DomEvent.addListener(this._container, 'click', this._onMouseClick, this);
var events = ['dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove'];
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; }
this.fire(type, {
latlng: this.mouseEventToLatLng(e),
layerPoint: this.mouseEventToLayerPoint(e)
});
},
_initInteraction: function () {
var handlers = {
dragging: L.Handler.MapDrag,
touchZoom: L.Handler.TouchZoom,
doubleClickZoom: L.Handler.DoubleClickZoom,
scrollWheelZoom: L.Handler.ScrollWheelZoom,
shiftDragZoom: L.Handler.ShiftDragZoom
};
var i;
for (i in handlers) {
if (handlers.hasOwnProperty(i) && handlers[i]) {
this[i] = new handlers[i](this);
if (this.options[i]) { this[i].enable(); }
}
}
},
_onTileLayerLoad: function () {
// 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 offset = L.DomUtil.getPosition(this._mapPane);
return this._initialTopLeftPoint.subtract(offset);
},
_getNewTopLeftPoint: function (center) {
var viewHalf = this.getSize().divideBy(2);
return this.project(center).subtract(viewHalf).round();
},
_limitZoom: function (zoom) {
var min = this.getMinZoom();
var max = this.getMaxZoom();
return Math.max(min, Math.min(max, zoom));
}
});