/* * L.TileLayer is used for standard xyz-numbered tile layers. */ L.TileLayer = L.Class.extend({ includes: L.Mixin.Events, options: { minZoom: 0, maxZoom: 18, tileSize: 256, subdomains: 'abc', errorTileUrl: '', attribution: '', zoomOffset: 0, opacity: 1, /* maxNativeZoom: null, zIndex: null, tms: false, continuousWorld: false, noWrap: false, zoomReverse: false, detectRetina: false, reuseTiles: false, bounds: false, */ unloadInvisibleTiles: L.Browser.mobile, updateWhenIdle: L.Browser.mobile }, initialize: function (url, options) { options = L.setOptions(this, options); // detecting retina displays, adjusting tileSize and zoom levels if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) { options.tileSize = Math.floor(options.tileSize / 2); options.zoomOffset++; if (options.minZoom > 0) { options.minZoom--; } this.options.maxZoom--; } if (options.bounds) { options.bounds = L.latLngBounds(options.bounds); } this._url = url; var subdomains = this.options.subdomains; if (typeof subdomains === 'string') { this.options.subdomains = subdomains.split(''); } }, onAdd: function (map) { this._map = map; this._animated = map._zoomAnimated; // create a container div for tiles this._initContainer(); // set up events map.on({ 'viewreset': this._reset, 'moveend': this._update }, this); if (this._animated) { map.on({ 'zoomanim': this._animateZoom, 'zoomend': this._endZoomAnim }, this); } if (!this.options.updateWhenIdle) { this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this); map.on('move', this._limitedUpdate, this); } this._reset(); this._update(); }, addTo: function (map) { map.addLayer(this); return this; }, onRemove: function (map) { this._container.parentNode.removeChild(this._container); map.off({ 'viewreset': this._reset, 'moveend': this._update }, this); if (this._animated) { map.off({ 'zoomanim': this._animateZoom, 'zoomend': this._endZoomAnim }, this); } if (!this.options.updateWhenIdle) { map.off('move', this._limitedUpdate, this); } this._container = null; this._map = null; }, bringToFront: function () { var pane = this._map._panes.tilePane; if (this._container) { pane.appendChild(this._container); this._setAutoZIndex(pane, Math.max); } return this; }, bringToBack: function () { var pane = this._map._panes.tilePane; if (this._container) { pane.insertBefore(this._container, pane.firstChild); this._setAutoZIndex(pane, 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; }, setUrl: function (url, noRedraw) { this._url = url; if (!noRedraw) { this.redraw(); } return this; }, redraw: function () { if (this._map) { this._reset({hard: true}); this._update(); } return this; }, _updateZIndex: function () { if (this._container && this.options.zIndex !== undefined) { this._container.style.zIndex = this.options.zIndex; } }, _setAutoZIndex: function (pane, compare) { var layers = pane.children, edgeZIndex = -compare(Infinity, -Infinity), // -Infinity for max, Infinity for min zIndex, i, len; for (i = 0, len = layers.length; i < len; i++) { if (layers[i] !== this._container) { zIndex = parseInt(layers[i].style.zIndex, 10); if (!isNaN(zIndex)) { edgeZIndex = compare(edgeZIndex, zIndex); } } } this.options.zIndex = this._container.style.zIndex = (isFinite(edgeZIndex) ? edgeZIndex : 0) + compare(1, -1); }, _updateOpacity: function () { var i, tiles = this._tiles; if (L.Browser.ielt9) { for (i in tiles) { L.DomUtil.setOpacity(tiles[i], this.options.opacity); } } else { L.DomUtil.setOpacity(this._container, this.options.opacity); } }, _initContainer: function () { var tilePane = this._map._panes.tilePane; if (!this._container) { this._container = L.DomUtil.create('div', 'leaflet-layer'); this._updateZIndex(); if (this._animated) { var className = 'leaflet-tile-container'; this._bgBuffer = L.DomUtil.create('div', className, this._container); this._tileContainer = L.DomUtil.create('div', className, this._container); } else { this._tileContainer = this._container; } tilePane.appendChild(this._container); if (this.options.opacity < 1) { this._updateOpacity(); } } }, _reset: function (e) { for (var key in this._tiles) { this.fire('tileunload', {tile: this._tiles[key]}); } this._tiles = {}; this._tilesToLoad = 0; if (this.options.reuseTiles) { this._unusedTiles = []; } this._tileContainer.innerHTML = ''; if (this._animated && e && e.hard) { this._clearBgBuffer(); } this._initContainer(); }, _getTileSize: function () { var map = this._map, zoom = map.getZoom() + this.options.zoomOffset, zoomN = this.options.maxNativeZoom, tileSize = this.options.tileSize; if (zoomN && zoom > zoomN) { tileSize = Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * tileSize); } return tileSize; }, _update: function () { if (!this._map) { return; } var map = this._map, bounds = map.getPixelBounds(), zoom = map.getZoom(), tileSize = this._getTileSize(); if (zoom > this.options.maxZoom || zoom < this.options.minZoom) { return; } var tileBounds = L.bounds( bounds.min.divideBy(tileSize)._floor(), bounds.max.divideBy(tileSize)._floor()); this._addTilesFromCenterOut(tileBounds); if (this.options.unloadInvisibleTiles || this.options.reuseTiles) { this._removeOtherTiles(tileBounds); } }, _addTilesFromCenterOut: function (bounds) { var queue = [], center = bounds.getCenter(); var j, i, point; for (j = bounds.min.y; j <= bounds.max.y; j++) { for (i = bounds.min.x; i <= bounds.max.x; i++) { point = new L.Point(i, j); if (this._tileShouldBeLoaded(point)) { queue.push(point); } } } var tilesToLoad = queue.length; if (tilesToLoad === 0) { return; } // load tiles in order of their distance to center queue.sort(function (a, b) { return a.distanceTo(center) - b.distanceTo(center); }); var fragment = document.createDocumentFragment(); // if its the first batch of tiles to load if (!this._tilesToLoad) { this.fire('loading'); } this._tilesToLoad += tilesToLoad; for (i = 0; i < tilesToLoad; i++) { this._addTile(queue[i], fragment); } this._tileContainer.appendChild(fragment); }, _tileShouldBeLoaded: function (tilePoint) { if ((tilePoint.x + ':' + tilePoint.y) in this._tiles) { return false; // already loaded } var options = this.options; if (!options.continuousWorld) { var limit = this._getWrapTileNum(); // don't load if exceeds world bounds if ((options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit.x)) || tilePoint.y < 0 || tilePoint.y >= limit.y) { return false; } } if (options.bounds) { var tileSize = this._getTileSize(), nwPoint = tilePoint.multiplyBy(tileSize), sePoint = nwPoint.add([tileSize, tileSize]), nw = this._map.unproject(nwPoint), se = this._map.unproject(sePoint); // TODO temporary hack, will be removed after refactoring projections // https://github.com/Leaflet/Leaflet/issues/1618 if (!options.continuousWorld && !options.noWrap) { nw = nw.wrap(); se = se.wrap(); } if (!options.bounds.intersects([nw, se])) { return false; } } return true; }, _removeOtherTiles: function (bounds) { var kArr, x, y, key; for (key in this._tiles) { kArr = key.split(':'); x = parseInt(kArr[0], 10); y = parseInt(kArr[1], 10); // remove tile if it's out of bounds if (x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) { this._removeTile(key); } } }, _removeTile: function (key) { var tile = this._tiles[key]; this.fire('tileunload', {tile: tile, url: tile.src}); if (this.options.reuseTiles) { L.DomUtil.removeClass(tile, 'leaflet-tile-loaded'); this._unusedTiles.push(tile); } else if (tile.parentNode === this._tileContainer) { this._tileContainer.removeChild(tile); } // for https://github.com/CloudMade/Leaflet/issues/137 if (!L.Browser.android) { tile.onload = null; tile.src = L.Util.emptyImageUrl; } delete this._tiles[key]; }, _addTile: function (tilePoint, container) { var tilePos = this._getTilePos(tilePoint); // get unused tile - or create a new tile var tile = this._getTile(); /* Chrome 20 layouts much faster with top/left (verify with timeline, frames) Android 4 browser has display issues with top/left and requires transform instead (other browsers don't currently care) - see debug/hacks/jitter.html for an example */ L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome); this._tiles[tilePoint.x + ':' + tilePoint.y] = tile; this._loadTile(tile, tilePoint); if (tile.parentNode !== this._tileContainer) { container.appendChild(tile); } }, _getZoomForUrl: function () { var options = this.options, zoom = this._map.getZoom(); if (options.zoomReverse) { zoom = options.maxZoom - zoom; } zoom += options.zoomOffset; return options.maxNativeZoom ? Math.min(zoom, options.maxNativeZoom) : zoom; }, _getTilePos: function (tilePoint) { var origin = this._map.getPixelOrigin(), tileSize = this._getTileSize(); return tilePoint.multiplyBy(tileSize).subtract(origin); }, // image-specific code (override to implement e.g. Canvas or SVG tile layer) getTileUrl: function (tilePoint) { return L.Util.template(this._url, L.extend({ s: this._getSubdomain(tilePoint), z: tilePoint.z, x: tilePoint.x, y: tilePoint.y }, this.options)); }, _getWrapTileNum: function () { var crs = this._map.options.crs, size = crs.getSize(this._map.getZoom()); return size.divideBy(this._getTileSize())._floor(); }, _adjustTilePoint: function (tilePoint) { var limit = this._getWrapTileNum(); // wrap tile coordinates if (!this.options.continuousWorld && !this.options.noWrap) { tilePoint.x = ((tilePoint.x % limit.x) + limit.x) % limit.x; } if (this.options.tms) { tilePoint.y = limit.y - tilePoint.y - 1; } tilePoint.z = this._getZoomForUrl(); }, _getSubdomain: function (tilePoint) { var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length; return this.options.subdomains[index]; }, _getTile: function () { if (this.options.reuseTiles && this._unusedTiles.length > 0) { var tile = this._unusedTiles.pop(); this._resetTile(tile); return tile; } return this._createTile(); }, // Override if data stored on a tile needs to be cleaned up before reuse _resetTile: function (/*tile*/) {}, _createTile: function () { var tile = L.DomUtil.create('img', 'leaflet-tile'); tile.style.width = tile.style.height = this._getTileSize() + 'px'; tile.galleryimg = 'no'; tile.onselectstart = tile.onmousemove = L.Util.falseFn; if (L.Browser.ielt9 && this.options.opacity !== undefined) { 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.mobileWebkit3d) { tile.style.WebkitBackfaceVisibility = 'hidden'; } return tile; }, _loadTile: function (tile, tilePoint) { tile._layer = this; tile.onload = this._tileOnLoad; tile.onerror = this._tileOnError; this._adjustTilePoint(tilePoint); tile.src = this.getTileUrl(tilePoint); this.fire('tileloadstart', { tile: tile, url: tile.src }); }, _tileLoaded: function () { this._tilesToLoad--; if (this._animated) { L.DomUtil.addClass(this._tileContainer, 'leaflet-zoom-animated'); } if (!this._tilesToLoad) { this.fire('load'); if (this._animated) { // clear scaled tiles after all new tiles are loaded (for performance) clearTimeout(this._clearBgBufferTimer); this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 500); } } }, _tileOnLoad: function () { var layer = this._layer; //Only if we are loading an actual image if (this.src !== L.Util.emptyImageUrl) { L.DomUtil.addClass(this, 'leaflet-tile-loaded'); layer.fire('tileload', { tile: this, url: this.src }); } layer._tileLoaded(); }, _tileOnError: function () { var layer = this._layer; layer.fire('tileerror', { tile: this, url: this.src }); var newUrl = layer.options.errorTileUrl; if (newUrl) { this.src = newUrl; } layer._tileLoaded(); } }); L.tileLayer = function (url, options) { return new L.TileLayer(url, options); };