bower_components/leaflet/src/layer/tile/TileLayer.js
/*
* 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 = options.tileSize,
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);
};