working GridLayer implementation, #1180

This commit is contained in:
Vladimir Agafonkin
2013-11-25 18:41:27 +02:00
parent b2acd4e26d
commit e8c504b5f9
3 changed files with 548 additions and 1 deletions

View File

@ -29,10 +29,16 @@ var deps = {
heading: 'Additional projections'
},
GridLayer: {
src: ['layer/tile/GridLayer.js'],
desc: 'Grid Layer.',
heading: 'Layers'
},
TileLayer: {
src: ['layer/tile/TileLayer.js'],
desc: 'The base class for displaying tile layers on the map.',
heading: 'Layers'
deps: ['GridLayer']
},
TileLayerWMS: {

39
debug/map/grid.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title>Leaflet debug page</title>
<link rel="stylesheet" href="../../dist/leaflet.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../css/screen.css" />
<script type="text/javascript" src="../../build/deps.js"></script>
<script src="../leaflet-include.js"></script>
</head>
<body>
<div id="map"></div>
<script type="text/javascript">
var grid = L.gridLayer({
// bounds: [[-86,-180],[86,180]],
attribution: 'Grid Layer'
});
grid.createTile = function (coords) {
var tile = document.createElement('div');
tile.innerHTML = [coords.x, coords.y, coords.z].join(',');
tile.style.outline = '1px solid red';
return tile;
};
var map = L.map('map')
.setView([50.5, 30.51], 10)
.addLayer(grid);
</script>
</body>
</html>

502
src/layer/tile/GridLayer.js Normal file
View File

@ -0,0 +1,502 @@
/*
* L.GridLayer is used as base class for grid-like layers like TileLayer.
*/
L.GridLayer = L.Class.extend({
includes: L.Mixin.Events,
options: {
tileSize: 256,
opacity: 1,
// minZoom: <Number>,
// maxZoom: <Number>,
async: false,
unloadInvisibleTiles: L.Browser.mobile,
updateWhenIdle: L.Browser.mobile,
updateInterval: 150
},
initialize: function (options) {
options = L.setOptions(this, options);
},
onAdd: function (map) {
this._map = map;
this._animated = map._zoomAnimated;
this._initContainer();
if (!this.options.updateWhenIdle) {
// update tiles on move, but not more often than once per given interval
this._update = L.Util.limitExecByInterval(this._update, this.options.updateInterval, this);
}
map.on(this._getEvents(), this);
this._reset();
this._update();
},
onRemove: function (map) {
this._getPane().removeChild(this._container);
map.on(this._getEvents(), this);
this._container = null;
this._map = null;
},
addTo: function (map) {
map.addLayer(this);
return this;
},
bringToFront: function () {
var pane = this._getPane();
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;
},
redraw: function () {
if (this._map) {
this._reset({hard: true}); //TODO wtf hard?
this._update();
}
return this;
},
isValidTile: function (coords) {
if (!this.options.bounds) { return true; }
var tileBounds = this._tileCoordsToBounds(coords),
bounds = L.latLngBounds(this.options.bounds);
return bounds.contains(tileBounds);
},
_getPane: function () {
// TODO pane in options?
return this._map._panes.tilePane;
},
_getEvents: function () {
var events = {
viewreset: this._reset,
moveend: this._update
};
if (!this.options.updateWhenIdle) {
events.move = this._update;
}
if (this._animated) {
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 (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 () {
if (this._container) { return; }
this._container = L.DomUtil.create('div', 'leaflet-layer');
this._updateZIndex();
if (this._animated) {
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);
} else {
this._tileContainer = this._container;
}
// TODO check if opacity works when setting before appendChild
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._tileContainer.innerHTML = '';
if (this._animated && e && e.hard) {
this._clearBgBuffer();
}
// this._initContainer();
// TODO OK?
},
_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 bounds = this._map.getPixelBounds(),
zoom = this._map.getZoom(),
tileSize = this._getTileSize();
if (zoom > this.options.maxZoom ||
zoom < this.options.minZoom) { return; }
// TODO clear bg buffer?
var tileBounds = L.bounds(
bounds.min.divideBy(tileSize).floor(),
bounds.max.divideBy(tileSize).floor());
this._addTilesFromCenterOut(tileBounds);
if (this.options.unloadInvisibleTiles) {
this._removeOtherTiles(tileBounds);
}
},
_addTilesFromCenterOut: function (bounds) {
var queue = [],
center = bounds.getCenter(),
zoom = this._map.getZoom();
var j, i, coords;
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;
if (!this._tileIsAdded(coords) && 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;
// load tiles to 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();
for (i = 0; i < tilesToLoad; i++) {
this._addTile(queue[i], fragment);
}
this._tileContainer.appendChild(fragment);
},
_tileIsAdded: function (coords) {
return this._tileCoordsToKey(coords) in this._tiles;
},
_tileCoordsToBounds: function (coords) {
var tileSize = this.options.tileSize,
nwPoint = coords.multiplyBy(tileSize),
sePoint = nwPoint.add([tileSize, tileSize]),
nw = this._map.unproject(nwPoint),
se = this._map.unproject(sePoint);
// TODO rectangular
return new L.LatLngBounds(nw, se);
},
_tileCoordsToKey: function (coords) {
return coords.x + ':' + coords.y;
},
_keyToTileCoords: function (key) {
var kArr = key.split(':'),
x = parseInt(kArr[0], 10),
y = parseInt(kArr[1], 10);
return new L.Point(x, y);
},
_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];
if (tile.parentNode) {
tile.parentNode.removeChild(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;
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';
}
},
_addTile: function (coords, container) {
var tile = this.createTile(coords, this._tileReady);
// TODO wrapping happens here?
this._initTile(tile);
if (this.createTile.length < 2) {
this._tileReady(tile);
// TODO pass tileReady to createTile for async
}
var tilePos = this._getTilePos(coords);
/*
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
Android 2 browser requires top/left or tiles disappear on load or first drag
(reappear after zoom) https://github.com/CloudMade/Leaflet/issues/866
(other browsers don't currently care) - see debug/hacks/jitter.html for an example
*/
L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome || L.Browser.android23);
this._tiles[this._tileCoordsToKey(coords)] = tile;
container.appendChild(tile);
},
_tileReady: function (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._animated) {
// clear scaled tiles after all new tiles are loaded (for performance)
this._clearBgBuffer();
// TODO find out why timeout was needed
// clearTimeout(this._clearBgBufferTimer);
// this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 500);
}
},
_getTilePos: function (tilePoint) {
var origin = this._map.getPixelOrigin(),
tileSize = this._getTileSize();
return tilePoint.multiplyBy(tileSize).subtract(origin);
},
_animateZoom: function (e) {
if (!this._animating) {
this._animating = true;
this._prepareBgBuffer();
}
var bg = this._bgBuffer,
transform = L.DomUtil.TRANSFORM,
initialTransform = e.delta ? L.DomUtil.getTranslateString(e.delta) : bg.style[transform],
scaleStr = L.DomUtil.getScaleString(e.scale, e.origin);
bg.style[transform] = e.backwards ?
scaleStr + ' ' + initialTransform :
initialTransform + ' ' + scaleStr;
},
_endZoomAnim: function () {
var front = this._tileContainer,
bg = this._bgBuffer;
front.style.visibility = '';
front.parentNode.appendChild(front); // Bring to fore
// force reflow
L.Util.falseFn(bg.offsetWidth);
this._animating = false;
},
_clearBgBuffer: function () {
var map = this._map;
if (map && !map._animatingZoom && !map.touchZoom._zooming) {
this._bgBuffer.innerHTML = '';
this._bgBuffer.style[L.DomUtil.TRANSFORM] = '';
}
},
_prepareBgBuffer: function () {
this._swapBgBuffer();
// TODO override in TileLayer
},
_swapBgBuffer: function () {
var front = this._tileContainer,
bg = this._bgBuffer;
// prepare the buffer to become the front tile pane
bg.style.visibility = 'hidden';
bg.style[L.DomUtil.TRANSFORM] = '';
// switch out the current layer to be the new bg layer (and vice-versa)
this._tileContainer = bg;
this._bgBuffer = front;
// prevent bg buffer from clearing right after zoom
clearTimeout(this._clearBgBufferTimer);
}
});
L.gridLayer = function (options) {
return new L.GridLayer(options);
};