leaflet
Version:
JavaScript library for mobile-friendly interactive maps
683 lines (527 loc) • 16.4 kB
JavaScript
/*
* L.GridLayer is used as base class for grid-like layers like TileLayer.
*/
L.GridLayer = L.Layer.extend({
options: {
pane: 'tilePane',
tileSize: 256,
opacity: 1,
zIndex: 1,
updateWhenIdle: L.Browser.mobile,
updateInterval: 200,
attribution: null,
bounds: null,
minZoom: 0
// maxZoom: <Number>
// noWrap: false
},
initialize: function (options) {
options = L.setOptions(this, options);
},
onAdd: function () {
this._initContainer();
this._levels = {};
this._tiles = {};
this._resetView();
this._update();
},
beforeAdd: function (map) {
map._addZoomLimit(this);
},
onRemove: function (map) {
L.DomUtil.remove(this._container);
map._removeZoomLimit(this);
this._container = null;
this._tileZoom = null;
},
bringToFront: function () {
if (this._map) {
L.DomUtil.toFront(this._container);
this._setAutoZIndex(Math.max);
}
return this;
},
bringToBack: function () {
if (this._map) {
L.DomUtil.toBack(this._container);
this._setAutoZIndex(Math.min);
}
return this;
},
getAttribution: function () {
return this.options.attribution;
},
getContainer: function () {
return this._container;
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
this._updateOpacity();
return this;
},
setZIndex: function (zIndex) {
this.options.zIndex = zIndex;
this._updateZIndex();
return this;
},
isLoading: function () {
return this._loading;
},
redraw: function () {
if (this._map) {
this._removeAllTiles();
this._update();
}
return this;
},
getEvents: function () {
var events = {
viewreset: this._resetAll,
zoom: this._resetView,
moveend: this._onMoveEnd
};
if (!this.options.updateWhenIdle) {
// update tiles on move, but not more often than once per given interval
if (!this._onMove) {
this._onMove = L.Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
}
events.move = this._onMove;
}
if (this._zoomAnimated) {
events.zoomanim = this._animateZoom;
}
return events;
},
createTile: function () {
return document.createElement('div');
},
getTileSize: function () {
var s = this.options.tileSize;
return s instanceof L.Point ? s : new L.Point(s, s);
},
_updateZIndex: function () {
if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
this._container.style.zIndex = this.options.zIndex;
}
},
_setAutoZIndex: function (compare) {
// go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)
var layers = this.getPane().children,
edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min
for (var i = 0, len = layers.length, zIndex; i < len; i++) {
zIndex = layers[i].style.zIndex;
if (layers[i] !== this._container && zIndex) {
edgeZIndex = compare(edgeZIndex, +zIndex);
}
}
if (isFinite(edgeZIndex)) {
this.options.zIndex = edgeZIndex + compare(-1, 1);
this._updateZIndex();
}
},
_updateOpacity: function () {
if (!this._map) { return; }
// IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
if (L.Browser.ielt9 || !this._map._fadeAnimated) {
return;
}
L.DomUtil.setOpacity(this._container, this.options.opacity);
var now = +new Date(),
nextFrame = false,
willPrune = false;
for (var key in this._tiles) {
var tile = this._tiles[key];
if (!tile.current || !tile.loaded) { continue; }
var fade = Math.min(1, (now - tile.loaded) / 200);
L.DomUtil.setOpacity(tile.el, fade);
if (fade < 1) {
nextFrame = true;
} else {
if (tile.active) { willPrune = true; }
tile.active = true;
}
}
if (willPrune && !this._noPrune) { this._pruneTiles(); }
if (nextFrame) {
L.Util.cancelAnimFrame(this._fadeFrame);
this._fadeFrame = L.Util.requestAnimFrame(this._updateOpacity, this);
}
},
_initContainer: function () {
if (this._container) { return; }
this._container = L.DomUtil.create('div', 'leaflet-layer');
this._updateZIndex();
if (this.options.opacity < 1) {
this._updateOpacity();
}
this.getPane().appendChild(this._container);
},
_updateLevels: function () {
var zoom = this._tileZoom,
maxZoom = this.options.maxZoom;
for (var z in this._levels) {
if (this._levels[z].el.children.length || z === zoom) {
this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
} else {
L.DomUtil.remove(this._levels[z].el);
delete this._levels[z];
}
}
var level = this._levels[zoom],
map = this._map;
if (!level) {
level = this._levels[zoom] = {};
level.el = L.DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
level.el.style.zIndex = maxZoom;
level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
level.zoom = zoom;
this._setZoomTransform(level, map.getCenter(), map.getZoom());
// force the browser to consider the newly added element for transition
L.Util.falseFn(level.el.offsetWidth);
}
this._level = level;
return level;
},
_pruneTiles: function () {
var key, tile;
var zoom = this._map.getZoom();
if (zoom > this.options.maxZoom ||
zoom < this.options.minZoom) { return this._removeAllTiles(); }
for (key in this._tiles) {
tile = this._tiles[key];
tile.retain = tile.current;
}
for (key in this._tiles) {
tile = this._tiles[key];
if (tile.current && !tile.active) {
var coords = tile.coords;
if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) {
this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2);
}
}
}
for (key in this._tiles) {
if (!this._tiles[key].retain) {
this._removeTile(key);
}
}
},
_removeAllTiles: function () {
for (var key in this._tiles) {
this._removeTile(key);
}
},
_resetAll: function () {
for (var z in this._levels) {
L.DomUtil.remove(this._levels[z].el);
delete this._levels[z];
}
this._removeAllTiles();
this._tileZoom = null;
this._resetView();
},
_retainParent: function (x, y, z, minZoom) {
var x2 = Math.floor(x / 2),
y2 = Math.floor(y / 2),
z2 = z - 1;
var key = x2 + ':' + y2 + ':' + z2,
tile = this._tiles[key];
if (tile && tile.active) {
tile.retain = true;
return true;
} else if (tile && tile.loaded) {
tile.retain = true;
}
if (z2 > minZoom) {
return this._retainParent(x2, y2, z2, minZoom);
}
return false;
},
_retainChildren: function (x, y, z, maxZoom) {
for (var i = 2 * x; i < 2 * x + 2; i++) {
for (var j = 2 * y; j < 2 * y + 2; j++) {
var key = i + ':' + j + ':' + (z + 1),
tile = this._tiles[key];
if (tile && tile.active) {
tile.retain = true;
continue;
} else if (tile && tile.loaded) {
tile.retain = true;
}
if (z + 1 < maxZoom) {
this._retainChildren(i, j, z + 1, maxZoom);
}
}
}
},
_resetView: function (e) {
var animating = e && (e.pinch || e.flyTo);
this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating);
},
_animateZoom: function (e) {
this._setView(e.center, e.zoom, true, e.noUpdate);
},
_setView: function (center, zoom, noPrune, noUpdate) {
var tileZoom = Math.round(zoom);
if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
(this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
tileZoom = undefined;
}
var tileZoomChanged = (tileZoom !== this._tileZoom);
if (!noUpdate || tileZoomChanged) {
this._tileZoom = tileZoom;
if (this._abortLoading) {
this._abortLoading();
}
this._updateLevels();
this._resetGrid();
if (tileZoom !== undefined) {
this._update(center);
}
if (!noPrune) {
this._pruneTiles();
}
// Flag to prevent _updateOpacity from pruning tiles during
// a zoom anim or a pinch gesture
this._noPrune = !!noPrune;
}
this._setZoomTransforms(center, zoom);
},
_setZoomTransforms: function (center, zoom) {
for (var i in this._levels) {
this._setZoomTransform(this._levels[i], center, zoom);
}
},
_setZoomTransform: function (level, center, zoom) {
var scale = this._map.getZoomScale(zoom, level.zoom),
translate = level.origin.multiplyBy(scale)
.subtract(this._map._getNewPixelOrigin(center, zoom)).round();
if (L.Browser.any3d) {
L.DomUtil.setTransform(level.el, translate, scale);
} else {
L.DomUtil.setPosition(level.el, translate);
}
},
_resetGrid: function () {
var map = this._map,
crs = map.options.crs,
tileSize = this._tileSize = this.getTileSize(),
tileZoom = this._tileZoom;
var bounds = this._map.getPixelWorldBounds(this._tileZoom);
if (bounds) {
this._globalTileRange = this._pxBoundsToTileRange(bounds);
}
this._wrapX = crs.wrapLng && !this.options.noWrap && [
Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x),
Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)
];
this._wrapY = crs.wrapLat && !this.options.noWrap && [
Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x),
Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)
];
},
_onMoveEnd: function () {
if (!this._map || this._map._animatingZoom) { return; }
this._resetView();
},
_getTiledPixelBounds: function (center, zoom, tileZoom) {
var map = this._map,
scale = map.getZoomScale(zoom, tileZoom),
pixelCenter = map.project(center, tileZoom).floor(),
halfSize = map.getSize().divideBy(scale * 2);
return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
},
// Private method to load tiles in the grid's active zoom level according to map bounds
_update: function (center) {
var map = this._map;
if (!map) { return; }
var zoom = map.getZoom();
if (center === undefined) { center = map.getCenter(); }
if (this._tileZoom === undefined) { return; } // if out of minzoom/maxzoom
var pixelBounds = this._getTiledPixelBounds(center, zoom, this._tileZoom),
tileRange = this._pxBoundsToTileRange(pixelBounds),
tileCenter = tileRange.getCenter(),
queue = [];
for (var key in this._tiles) {
this._tiles[key].current = false;
}
// _update just loads more tiles. If the tile zoom level differs too much
// from the map's, let _setView reset levels and prune old tiles.
if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; }
// create a queue of coordinates to load tiles from
for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
var coords = new L.Point(i, j);
coords.z = this._tileZoom;
if (!this._isValidTile(coords)) { continue; }
var tile = this._tiles[this._tileCoordsToKey(coords)];
if (tile) {
tile.current = true;
} else {
queue.push(coords);
}
}
}
// sort tile queue to load tiles in order of their distance to center
queue.sort(function (a, b) {
return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
});
if (queue.length !== 0) {
// if its the first batch of tiles to load
if (!this._loading) {
this._loading = true;
this.fire('loading');
}
// create DOM fragment to append tiles in one batch
var fragment = document.createDocumentFragment();
for (i = 0; i < queue.length; i++) {
this._addTile(queue[i], fragment);
}
this._level.el.appendChild(fragment);
}
},
_isValidTile: function (coords) {
var crs = this._map.options.crs;
if (!crs.infinite) {
// don't load tile if it's out of bounds and not wrapped
var bounds = this._globalTileRange;
if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
(!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
}
if (!this.options.bounds) { return true; }
// don't load tile if it doesn't intersect the bounds in options
var tileBounds = this._tileCoordsToBounds(coords);
return L.latLngBounds(this.options.bounds).overlaps(tileBounds);
},
_keyToBounds: function (key) {
return this._tileCoordsToBounds(this._keyToTileCoords(key));
},
// converts tile coordinates to its geographical bounds
_tileCoordsToBounds: function (coords) {
var map = this._map,
tileSize = this.getTileSize(),
nwPoint = coords.scaleBy(tileSize),
sePoint = nwPoint.add(tileSize),
nw = map.wrapLatLng(map.unproject(nwPoint, coords.z)),
se = map.wrapLatLng(map.unproject(sePoint, coords.z));
return new L.LatLngBounds(nw, se);
},
// converts tile coordinates to key for the tile cache
_tileCoordsToKey: function (coords) {
return coords.x + ':' + coords.y + ':' + coords.z;
},
// converts tile cache key to coordinates
_keyToTileCoords: function (key) {
var k = key.split(':'),
coords = new L.Point(+k[0], +k[1]);
coords.z = +k[2];
return coords;
},
_removeTile: function (key) {
var tile = this._tiles[key];
if (!tile) { return; }
L.DomUtil.remove(tile.el);
delete this._tiles[key];
this.fire('tileunload', {
tile: tile.el,
coords: this._keyToTileCoords(key)
});
},
_initTile: function (tile) {
L.DomUtil.addClass(tile, 'leaflet-tile');
var tileSize = this.getTileSize();
tile.style.width = tileSize.x + 'px';
tile.style.height = tileSize.y + 'px';
tile.onselectstart = L.Util.falseFn;
tile.onmousemove = L.Util.falseFn;
// update opacity on tiles in IE7-8 because of filter inheritance problems
if (L.Browser.ielt9 && this.options.opacity < 1) {
L.DomUtil.setOpacity(tile, this.options.opacity);
}
// without this hack, tiles disappear after zoom on Chrome for Android
// https://github.com/Leaflet/Leaflet/issues/2078
if (L.Browser.android && !L.Browser.android23) {
tile.style.WebkitBackfaceVisibility = 'hidden';
}
},
_addTile: function (coords, container) {
var tilePos = this._getTilePos(coords),
key = this._tileCoordsToKey(coords);
var tile = this.createTile(this._wrapCoords(coords), L.bind(this._tileReady, this, coords));
this._initTile(tile);
// if createTile is defined with a second argument ("done" callback),
// we know that tile is async and will be ready later; otherwise
if (this.createTile.length < 2) {
// mark tile as ready, but delay one frame for opacity animation to happen
L.Util.requestAnimFrame(L.bind(this._tileReady, this, coords, null, tile));
}
L.DomUtil.setPosition(tile, tilePos);
// save tile in cache
this._tiles[key] = {
el: tile,
coords: coords,
current: true
};
container.appendChild(tile);
this.fire('tileloadstart', {
tile: tile,
coords: coords
});
},
_tileReady: function (coords, err, tile) {
if (!this._map) { return; }
if (err) {
this.fire('tileerror', {
error: err,
tile: tile,
coords: coords
});
}
var key = this._tileCoordsToKey(coords);
tile = this._tiles[key];
if (!tile) { return; }
tile.loaded = +new Date();
if (this._map._fadeAnimated) {
L.DomUtil.setOpacity(tile.el, 0);
L.Util.cancelAnimFrame(this._fadeFrame);
this._fadeFrame = L.Util.requestAnimFrame(this._updateOpacity, this);
} else {
tile.active = true;
this._pruneTiles();
}
L.DomUtil.addClass(tile.el, 'leaflet-tile-loaded');
this.fire('tileload', {
tile: tile.el,
coords: coords
});
if (this._noTilesToLoad()) {
this._loading = false;
this.fire('load');
}
},
_getTilePos: function (coords) {
return coords.scaleBy(this.getTileSize()).subtract(this._level.origin);
},
_wrapCoords: function (coords) {
var newCoords = new L.Point(
this._wrapX ? L.Util.wrapNum(coords.x, this._wrapX) : coords.x,
this._wrapY ? L.Util.wrapNum(coords.y, this._wrapY) : coords.y);
newCoords.z = coords.z;
return newCoords;
},
_pxBoundsToTileRange: function (bounds) {
var tileSize = this.getTileSize();
return new L.Bounds(
bounds.min.unscaleBy(tileSize).floor(),
bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
},
_noTilesToLoad: function () {
for (var key in this._tiles) {
if (!this._tiles[key].loaded) { return false; }
}
return true;
}
});
L.gridLayer = function (options) {
return new L.GridLayer(options);
};