UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

683 lines (527 loc) 16.4 kB
/* * 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); };