/* * 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, unloadInvisibleTiles: L.Browser.mobile, updateWhenIdle: L.Browser.mobile, updateInterval: 150 /* minZoom: , maxZoom: , attribution: , zIndex: , bounds: */ }, initialize: function (options) { options = L.setOptions(this, options); }, onAdd: function () { this._initContainer(); if (!this.options.updateWhenIdle) { // update tiles on move, but not more often than once per given interval this._update = L.Util.throttle(this._update, this.options.updateInterval, this); } this._reset(); this._update(); }, beforeAdd: function (map) { map._addZoomLimit(this); }, onRemove: function (map) { this._clearBgBuffer(); L.DomUtil.remove(this._container); map._removeZoomLimit(this); this._container = 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; if (this._map) { this._updateOpacity(); } return this; }, setZIndex: function (zIndex) { this.options.zIndex = zIndex; this._updateZIndex(); return this; }, redraw: function () { if (this._map) { this._reset({hard: true}); this._update(); } return this; }, getEvents: function () { var events = { viewreset: this._reset, moveend: this._update }; if (!this.options.updateWhenIdle) { events.move = this._update; } if (this._zoomAnimated) { events.zoomstart = this._startZoomAnim; events.zoomanim = this._animateZoom; events.zoomend = this._endZoomAnim; } return events; }, _updateZIndex: function () { if (this._container && this.options.zIndex !== undefined) { 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 () { var opacity = this.options.opacity; if (L.Browser.ielt9) { // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles for (var i in this._tiles) { L.DomUtil.setOpacity(this._tiles[i], opacity); } } else { L.DomUtil.setOpacity(this._container, opacity); } }, _initContainer: function () { if (this._container) { return; } this._container = L.DomUtil.create('div', 'leaflet-layer'); this._updateZIndex(); if (this._zoomAnimated) { var className = 'leaflet-tile-container leaflet-zoom-animated'; this._bgBuffer = L.DomUtil.create('div', className, this._container); this._tileContainer = L.DomUtil.create('div', className, this._container); L.DomUtil.setTransform(this._tileContainer); } else { this._tileContainer = this._container; } if (this.options.opacity < 1) { this._updateOpacity(); } this.getPane().appendChild(this._container); }, _reset: function (e) { for (var key in this._tiles) { this.fire('tileunload', { tile: this._tiles[key] }); } this._tiles = {}; this._tilesToLoad = 0; this._tilesTotal = 0; this._tileContainer.innerHTML = ''; if (this._zoomAnimated && e && e.hard) { this._clearBgBuffer(); } this._tileNumBounds = this._getTileNumBounds(); this._resetWrap(); }, _resetWrap: function () { var map = this._map, crs = map.options.crs; if (crs.infinite) { return; } var tileSize = this._getTileSize(); if (crs.wrapLng) { this._wrapLng = [ Math.floor(map.project([0, crs.wrapLng[0]]).x / tileSize), Math.ceil(map.project([0, crs.wrapLng[1]]).x / tileSize) ]; } if (crs.wrapLat) { this._wrapLat = [ Math.floor(map.project([crs.wrapLat[0], 0]).y / tileSize), Math.ceil(map.project([crs.wrapLat[1], 0]).y / tileSize) ]; } }, _getTileSize: function () { return this.options.tileSize; }, _update: function () { if (!this._map) { return; } var bounds = this._map.getPixelBounds(), zoom = this._map.getZoom(), tileSize = this._getTileSize(); if (zoom > this.options.maxZoom || zoom < this.options.minZoom) { return; } // tile coordinates range for the current view var tileBounds = L.bounds( bounds.min.divideBy(tileSize).floor(), bounds.max.divideBy(tileSize).floor()); this._addTiles(tileBounds); if (this.options.unloadInvisibleTiles) { this._removeOtherTiles(tileBounds); } }, _addTiles: function (bounds) { var queue = [], center = bounds.getCenter(), zoom = this._map.getZoom(); var j, i, coords; // create a queue of coordinates to load tiles from for (j = bounds.min.y; j <= bounds.max.y; j++) { for (i = bounds.min.x; i <= bounds.max.x; i++) { coords = new L.Point(i, j); coords.z = zoom; // add tile to queue if it's not in cache or out of bounds if (!(this._tileCoordsToKey(coords) in this._tiles) && this._isValidTile(coords)) { queue.push(coords); } } } var tilesToLoad = queue.length; if (tilesToLoad === 0) { return; } // if its the first batch of tiles to load if (!this._tilesToLoad) { this.fire('loading'); } this._tilesToLoad += tilesToLoad; this._tilesTotal += tilesToLoad; // sort tile queue to load tiles in order of their distance to center queue.sort(function (a, b) { return a.distanceTo(center) - b.distanceTo(center); }); // create DOM fragment to append tiles in one batch var fragment = document.createDocumentFragment(); for (i = 0; i < tilesToLoad; i++) { this._addTile(queue[i], fragment); } this._tileContainer.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._tileNumBounds; 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).intersects(tileBounds); }, // converts tile coordinates to its geographical bounds _tileCoordsToBounds: function (coords) { var map = this._map, tileSize = this.options.tileSize, nwPoint = coords.multiplyBy(tileSize), sePoint = nwPoint.add([tileSize, 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; }, // converts tile cache key to coordiantes _keyToTileCoords: function (key) { var kArr = key.split(':'), x = parseInt(kArr[0], 10), y = parseInt(kArr[1], 10); return new L.Point(x, y); }, // remove any present tiles that are off the specified bounds _removeOtherTiles: function (bounds) { for (var key in this._tiles) { if (!bounds.contains(this._keyToTileCoords(key))) { this._removeTile(key); } } }, _removeTile: function (key) { var tile = this._tiles[key]; L.DomUtil.remove(tile); delete this._tiles[key]; this.fire('tileunload', {tile: tile}); }, _initTile: function (tile) { var size = this._getTileSize(); L.DomUtil.addClass(tile, 'leaflet-tile'); tile.style.width = size + 'px'; tile.style.height = size + '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); // wrap tile coords if necessary (depending on CRS) this._wrapCoords(coords); var tile = this.createTile(coords, L.bind(this._tileReady, this)); 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 setTimeout(L.bind(this._tileReady, this, null, tile), 0); } // we prefer top/left over translate3d so that we don't create a HW-accelerated layer from each tile // which is slow, and it also fixes gaps between tiles in Safari L.DomUtil.setPosition(tile, tilePos, true); // save tile in cache this._tiles[this._tileCoordsToKey(coords)] = tile; container.appendChild(tile); this.fire('tileloadstart', {tile: tile}); }, _tileReady: function (err, tile) { if (err) { this.fire('tileerror', { error: err, tile: tile }); } L.DomUtil.addClass(tile, 'leaflet-tile-loaded'); this.fire('tileload', {tile: tile}); this._tilesToLoad--; if (this._tilesToLoad === 0) { this._visibleTilesReady(); } }, _visibleTilesReady: function () { this.fire('load'); if (this._zoomAnimated) { // clear scaled tiles after all new tiles are loaded (for performance) clearTimeout(this._clearBgBufferTimer); this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 300); } }, _getTilePos: function (coords) { return coords .multiplyBy(this._getTileSize()) .subtract(this._map.getPixelOrigin()); }, _wrapCoords: function (coords) { coords.x = this._wrapLng ? L.Util.wrapNum(coords.x, this._wrapLng) : coords.x; coords.y = this._wrapLat ? L.Util.wrapNum(coords.y, this._wrapLat) : coords.y; }, // get the global tile coordinates range for the current zoom _getTileNumBounds: function () { var bounds = this._map.getPixelWorldBounds(), size = this._getTileSize(); return bounds ? L.bounds( bounds.min.divideBy(size).floor(), bounds.max.divideBy(size).ceil().subtract([1, 1])) : null; }, _startZoomAnim: function () { this._prepareBgBuffer(); this._prevTranslate = this._translate; this._prevScale = this._scale; }, _animateZoom: function (e) { // avoid stacking transforms by calculating cumulating translate/scale sequence this._translate = this._prevTranslate.multiplyBy(e.scale).add(e.origin.multiplyBy(1 - e.scale)); this._scale = this._prevScale * e.scale; L.DomUtil.setTransform(this._bgBuffer, this._translate, this._scale); }, _endZoomAnim: function () { var front = this._tileContainer; front.style.visibility = ''; L.DomUtil.toFront(front); // bring to front }, _clearBgBuffer: function () { var map = this._map; if (map && !map._animatingZoom && !map.touchZoom._zooming) { this._bgBuffer.innerHTML = ''; L.DomUtil.setTransform(this._bgBuffer); } }, _prepareBgBuffer: function () { var front = this._tileContainer, bg = this._bgBuffer; if (this._abortLoading) { this._abortLoading(); } if (this._tilesToLoad / this._tilesTotal > 0.5) { // if foreground layer doesn't have many tiles loaded, // keep the existing bg layer and just zoom it some more front.style.visibility = 'hidden'; return; } // prepare the buffer to become the front tile pane bg.style.visibility = 'hidden'; L.DomUtil.setTransform(bg); // switch out the current layer to be the new bg layer (and vice-versa) this._tileContainer = bg; this._bgBuffer = front; // reset bg layer transform info this._translate = new L.Point(0, 0); this._scale = 1; // prevent bg buffer from clearing right after zoom clearTimeout(this._clearBgBufferTimer); } }); L.gridLayer = function (options) { return new L.GridLayer(options); };