Files
openlayers/lib/OpenLayers/Layer/Grid.js
ahocevar b6df3d871b It turns out we really want to retile sometimes.
The reason is that we want to avoid moveGriddedTiles to run through hundreds of cycles to shift tiles until we reach the new bounds. But containsBounds does not work if extents that cross the date line start on different worlds, so we use intersectsBounds where we can pass the world bounds to handle this case.
2011-12-07 15:55:22 +01:00

1142 lines
38 KiB
JavaScript

/* Copyright (c) 2006-2011 by OpenLayers Contributors (see authors.txt for
* full list of contributors). Published under the Clear BSD license.
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the
* full text of the license. */
/**
* @requires OpenLayers/Layer/HTTPRequest.js
* @requires OpenLayers/Tile/Image.js
*/
/**
* Class: OpenLayers.Layer.Grid
* Base class for layers that use a lattice of tiles. Create a new grid
* layer with the <OpenLayers.Layer.Grid> constructor.
*
* Inherits from:
* - <OpenLayers.Layer.HTTPRequest>
*/
OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, {
/**
* APIProperty: tileSize
* {<OpenLayers.Size>}
*/
tileSize: null,
/**
* Property: tileOriginCorner
* {String} If the <tileOrigin> property is not provided, the tile origin
* will be derived from the layer's <maxExtent>. The corner of the
* <maxExtent> used is determined by this property. Acceptable values
* are "tl" (top left), "tr" (top right), "bl" (bottom left), and "br"
* (bottom right). Default is "bl".
*/
tileOriginCorner: "bl",
/**
* APIProperty: tileOrigin
* {<OpenLayers.LonLat>} Optional origin for aligning the grid of tiles.
* If provided, requests for tiles at all resolutions will be aligned
* with this location (no tiles shall overlap this location). If
* not provided, the grid of tiles will be aligned with the layer's
* <maxExtent>. Default is ``null``.
*/
tileOrigin: null,
/** APIProperty: tileOptions
* {Object} optional configuration options for <OpenLayers.Tile> instances
* created by this Layer, if supported by the tile class.
*/
tileOptions: null,
/**
* Property: grid
* {Array(Array(<OpenLayers.Tile>))} This is an array of rows, each row is
* an array of tiles.
*/
grid: null,
/**
* APIProperty: singleTile
* {Boolean} Moves the layer into single-tile mode, meaning that one tile
* will be loaded. The tile's size will be determined by the 'ratio'
* property. When the tile is dragged such that it does not cover the
* entire viewport, it is reloaded.
*/
singleTile: false,
/** APIProperty: ratio
* {Float} Used only when in single-tile mode, this specifies the
* ratio of the size of the single tile to the size of the map.
*/
ratio: 1.5,
/**
* APIProperty: buffer
* {Integer} Used only when in gridded mode, this specifies the number of
* extra rows and colums of tiles on each side which will
* surround the minimum grid tiles to cover the map.
* For very slow loading layers, a larger value may increase
* performance somewhat when dragging, but will increase bandwidth
* use significantly.
*/
buffer: 0,
/**
* APIProperty: numLoadingTiles
* {Integer} How many tiles are still loading?
*/
numLoadingTiles: 0,
/**
* APIProperty: tileLoadingDelay
* {Integer} - Number of milliseconds before we shift and load
* tiles. Default is 100.
*/
tileLoadingDelay: 100,
/**
* Property: serverResolutions
* {Array(Number}} This property is documented in subclasses as
* an API property.
*/
serverResolutions: null,
/**
* Property: timerId
* {Number} - The id of the tileLoadingDelay timer.
*/
timerId: null,
/**
* Property: backBuffer
* {DOMElement} The back buffer.
*/
backBuffer: null,
/**
* Property: gridResolution
* {Number} The resolution of the current grid. Used for backbuffering.
* This property is updated each the grid is initialized.
*/
gridResolution: null,
/**
* Property: backBufferResolution
* {Number} The resolution of the current back buffer. This property is
* updated each time a back buffer is created.
*/
backBufferResolution: null,
/**
* Property: backBufferLonLat
* {Object} The top-left corner of the current back buffer. Includes lon
* and lat properties. This object is updated each time a back buffer
* is created.
*/
backBufferLonLat: null,
/**
* Register a listener for a particular event with the following syntax:
* (code)
* layer.events.register(type, obj, listener);
* (end)
*
* Listeners will be called with a reference to an event object. The
* properties of this event depends on exactly what happened.
*
* All event objects have at least the following properties:
* object - {Object} A reference to layer.events.object.
* element - {DOMElement} A reference to layer.events.element.
*
* Supported event types:
* tileloaded - Triggered when each new tile is
* loaded, as a means of progress update to listeners.
* listeners can access 'numLoadingTiles' if they wish to keep
* track of the loading progress.
*/
/**
* Constructor: OpenLayers.Layer.Grid
* Create a new grid layer
*
* Parameters:
* name - {String}
* url - {String}
* params - {Object}
* options - {Object} Hashtable of extra options to tag onto the layer
*/
initialize: function(name, url, params, options) {
OpenLayers.Layer.HTTPRequest.prototype.initialize.apply(this,
arguments);
this.grid = [];
this._moveGriddedTiles = OpenLayers.Function.bind(
this.moveGriddedTiles, this
);
},
/**
* Method: removeMap
* Called when the layer is removed from the map.
*
* Parameters:
* map - {<OpenLayers.Map>} The map.
*/
removeMap: function(map) {
if(this.timerId != null) {
window.clearTimeout(this.timerId);
this.timerId = null;
}
},
/**
* APIMethod: destroy
* Deconstruct the layer and clear the grid.
*/
destroy: function() {
this.clearGrid();
// clearGrid should remove any back buffer from the layer,
// so no need to call removeBackBuffer here
this.grid = null;
this.tileSize = null;
OpenLayers.Layer.HTTPRequest.prototype.destroy.apply(this, arguments);
},
/**
* Method: clearGrid
* Go through and remove all tiles from the grid, calling
* destroy() on each of them to kill circular references
*/
clearGrid:function() {
if (this.grid) {
for(var iRow=0, len=this.grid.length; iRow<len; iRow++) {
var row = this.grid[iRow];
for(var iCol=0, clen=row.length; iCol<clen; iCol++) {
var tile = row[iCol];
this.removeTileMonitoringHooks(tile);
tile.destroy();
}
}
this.grid = [];
this.gridResolution = null;
}
},
/**
* APIMethod: clone
* Create a clone of this layer
*
* Parameters:
* obj - {Object} Is this ever used?
*
* Returns:
* {<OpenLayers.Layer.Grid>} An exact clone of this OpenLayers.Layer.Grid
*/
clone: function (obj) {
if (obj == null) {
obj = new OpenLayers.Layer.Grid(this.name,
this.url,
this.params,
this.getOptions());
}
//get all additions from superclasses
obj = OpenLayers.Layer.HTTPRequest.prototype.clone.apply(this, [obj]);
// copy/set any non-init, non-simple values here
if (this.tileSize != null) {
obj.tileSize = this.tileSize.clone();
}
// we do not want to copy reference to grid, so we make a new array
obj.grid = [];
obj.gridResolution = null;
return obj;
},
/**
* Method: moveTo
* This function is called whenever the map is moved. All the moving
* of actual 'tiles' is done by the map, but moveTo's role is to accept
* a bounds and make sure the data that that bounds requires is pre-loaded.
*
* Parameters:
* bounds - {<OpenLayers.Bounds>}
* zoomChanged - {Boolean}
* dragging - {Boolean}
*/
moveTo:function(bounds, zoomChanged, dragging) {
OpenLayers.Layer.HTTPRequest.prototype.moveTo.apply(this, arguments);
bounds = bounds || this.map.getExtent();
if (bounds != null) {
// if grid is empty or zoom has changed, we *must* re-tile
var forceReTile = !this.grid.length || zoomChanged;
// total bounds of the tiles
var tilesBounds = this.getTilesBounds();
// the new map resolution
var resolution = this.map.getResolution();
// the server-supported resolution for the new map resolution
var serverResolution = this.getServerResolution(resolution);
if (this.singleTile) {
// We want to redraw whenever even the slightest part of the
// current bounds is not contained by our tile.
// (thus, we do not specify partial -- its default is false)
if ( forceReTile ||
(!dragging && !tilesBounds.containsBounds(bounds))) {
// In single tile mode with no transition effect, we insert
// a non-scaled backbuffer when the layer is moved. But if
// a zoom occurs right after a move, i.e. before the new
// image is received, we need to remove the backbuffer, or
// an ill-positioned image will be visible during the zoom
// transition.
if(zoomChanged && this.transitionEffect !== 'resize') {
this.removeBackBuffer();
}
if(!zoomChanged || this.transitionEffect === 'resize') {
this.applyBackBuffer(serverResolution);
}
this.initSingleTile(bounds);
}
} else {
// if the bounds have changed such that they are not even
// *partially* contained by our tiles (e.g. when user has
// programmatically panned to the other side of the earth on
// zoom level 18), then moveGriddedTime could potentially have
// to run through thousands of cycles, so we want to reTile
// instead (thus, partial true).
forceReTile = forceReTile ||
!tilesBounds.intersectsBounds(bounds, {
worldBounds: this.map.baseLayer.wrapDateLine &&
this.map.getMaxExtent()
});
if(resolution !== serverResolution) {
bounds = this.map.calculateBounds(null, serverResolution);
if(forceReTile) {
// stretch the layer div
var scale = serverResolution / resolution;
this.transformDiv(scale);
}
} else {
// reset the layer width, height, left, top, to deal with
// the case where the layer was previously transformed
this.div.style.width = '100%';
this.div.style.height = '100%';
this.div.style.left = '0%';
this.div.style.top = '0%';
}
if(forceReTile) {
if(zoomChanged && this.transitionEffect === 'resize') {
this.applyBackBuffer(serverResolution);
}
this.initGriddedTiles(bounds);
} else {
this.scheduleMoveGriddedTiles();
}
}
}
},
/**
* Method: getServerResolution
* Return the closest highest server-supported resolution. Throw an
* exception if none is found in the serverResolutions array.
*
* Parameters:
* resolution - {Number} The base resolution. If undefined the
* map resolution is used.
*
* Returns:
* {Number} The closest highest server resolution value.
*/
getServerResolution: function(resolution) {
resolution = resolution || this.map.getResolution();
if(this.serverResolutions &&
OpenLayers.Util.indexOf(this.serverResolutions, resolution) === -1) {
var i, serverResolution;
for(i=this.serverResolutions.length-1; i>= 0; i--) {
serverResolution = this.serverResolutions[i];
if(serverResolution > resolution) {
resolution = serverResolution;
break;
}
}
if(i === -1) {
throw 'no appropriate resolution in serverResolutions';
}
}
return resolution;
},
/**
* Method: getServerZoom
* Return the zoom value corresponding to the best zoom supported by the server
* resolution.
*
* Returns:
* {Number} The closest server supported zoom.
*/
getServerZoom: function() {
return this.map.getZoomForResolution(this.getServerResolution());
},
/**
* Method: transformDiv
* Transform the layer div.
*
* Parameters:
* scale - {Number} The value by which the layer div is to
* be scaled.
*/
transformDiv: function(scale) {
// scale the layer div
this.div.style.width = 100 * scale + '%';
this.div.style.height = 100 * scale + '%';
// and translate the layer div as necessary
var size = this.map.getSize();
var lcX = parseInt(this.map.layerContainerDiv.style.left, 10);
var lcY = parseInt(this.map.layerContainerDiv.style.top, 10);
var x = (lcX - (size.w / 2.0)) * (scale - 1);
var y = (lcY - (size.h / 2.0)) * (scale - 1);
this.div.style.left = x + '%';
this.div.style.top = y + '%';
},
/**
* Method: getResolutionScale
* Return the value by which the layer is currently scaled.
*
* Returns:
* {Number} The resolution scale.
*/
getResolutionScale: function() {
return parseInt(this.div.style.width, 10) / 100;
},
/**
* Method: applyBackBuffer
* Create, insert, scale and position a back buffer for the layer.
*
* Parameters:
* resolution - {Number} The resolution to transition to.
*/
applyBackBuffer: function(resolution) {
var backBuffer = this.backBuffer;
if(!backBuffer) {
backBuffer = this.createBackBuffer();
if(!backBuffer) {
return;
}
this.div.insertBefore(backBuffer, this.div.firstChild);
this.backBuffer = backBuffer;
// set some information in the instance for subsequent
// calls to applyBackBuffer where the same back buffer
// is reused
var topLeftTileBounds = this.grid[0][0].bounds;
this.backBufferLonLat = {
lon: topLeftTileBounds.left,
lat: topLeftTileBounds.top
};
this.backBufferResolution = this.gridResolution;
}
var style = backBuffer.style;
// scale the back buffer
var ratio = this.backBufferResolution / resolution;
style.width = 100 * ratio + '%';
style.height = 100 * ratio + '%';
// and position it (based on the grid's top-left corner)
var position = this.getViewPortPxFromLonLat(
this.backBufferLonLat, resolution);
var leftOffset = parseInt(this.map.layerContainerDiv.style.left, 10);
var topOffset = parseInt(this.map.layerContainerDiv.style.top, 10);
backBuffer.style.left = (position.x - leftOffset) + '%';
backBuffer.style.top = (position.y - topOffset) + '%';
},
/**
* Method: createBackBuffer
* Create a back buffer.
*
* Returns:
* {DOMElement} The DOM element for the back buffer, undefined if the
* grid isn't initialized yet.
*/
createBackBuffer: function() {
var backBuffer;
if(this.grid.length > 0) {
backBuffer = document.createElement('div');
backBuffer.id = this.div.id + '_bb';
backBuffer.className = 'olBackBuffer';
backBuffer.style.position = 'absolute';
backBuffer.style.width = '100%';
backBuffer.style.height = '100%';
for(var i=0, lenI=this.grid.length; i<lenI; i++) {
for(var j=0, lenJ=this.grid[i].length; j<lenJ; j++) {
var tile = this.grid[i][j].createBackBuffer();
if(!tile) {
continue;
}
// to be able to correctly position the back buffer we
// place the tiles grid at (0, 0) in the back buffer
tile.style.top = (i * this.tileSize.h) + '%';
tile.style.left = (j * this.tileSize.w) + '%';
backBuffer.appendChild(tile);
}
}
}
return backBuffer;
},
/**
* Method: removeBackBuffer
* Remove back buffer from DOM.
*/
removeBackBuffer: function() {
if(this.backBuffer && this.backBuffer.parentNode) {
this.div.removeChild(this.backBuffer);
this.backBuffer = null;
this.backBufferResolution = null;
}
},
/**
* Method: moveByPx
* Move the layer based on pixel vector.
*
* Parameters:
* dx - {Number}
* dy - {Number}
*/
moveByPx: function(dx, dy) {
if (!this.singleTile) {
this.scheduleMoveGriddedTiles();
}
},
/**
* Method: scheduleMoveGriddedTiles
* Schedule the move of tiles.
*/
scheduleMoveGriddedTiles: function() {
if (this.timerId != null) {
window.clearTimeout(this.timerId);
}
this.timerId = window.setTimeout(
this._moveGriddedTiles,
this.tileLoadingDelay
);
},
/**
* APIMethod: setTileSize
* Check if we are in singleTile mode and if so, set the size as a ratio
* of the map size (as specified by the layer's 'ratio' property).
*
* Parameters:
* size - {<OpenLayers.Size>}
*/
setTileSize: function(size) {
if (this.singleTile) {
size = this.map.getSize();
size.h = parseInt(size.h * this.ratio);
size.w = parseInt(size.w * this.ratio);
}
OpenLayers.Layer.HTTPRequest.prototype.setTileSize.apply(this, [size]);
},
/**
* APIMethod: getTilesBounds
* Return the bounds of the tile grid.
*
* Returns:
* {<OpenLayers.Bounds>} A Bounds object representing the bounds of all the
* currently loaded tiles (including those partially or not at all seen
* onscreen).
*/
getTilesBounds: function() {
var bounds = null;
var length = this.grid.length;
if (length) {
var bottomLeftTileBounds = this.grid[length - 1][0].bounds,
width = this.grid[0].length * bottomLeftTileBounds.getWidth(),
height = this.grid.length * bottomLeftTileBounds.getHeight();
bounds = new OpenLayers.Bounds(bottomLeftTileBounds.left,
bottomLeftTileBounds.bottom,
bottomLeftTileBounds.left + width,
bottomLeftTileBounds.bottom + height);
}
return bounds;
},
/**
* Method: initSingleTile
*
* Parameters:
* bounds - {<OpenLayers.Bounds>}
*/
initSingleTile: function(bounds) {
//determine new tile bounds
var center = bounds.getCenterLonLat();
var tileWidth = bounds.getWidth() * this.ratio;
var tileHeight = bounds.getHeight() * this.ratio;
var tileBounds =
new OpenLayers.Bounds(center.lon - (tileWidth/2),
center.lat - (tileHeight/2),
center.lon + (tileWidth/2),
center.lat + (tileHeight/2));
var ul = new OpenLayers.LonLat(tileBounds.left, tileBounds.top);
var px = this.map.getLayerPxFromLonLat(ul);
if (!this.grid.length) {
this.grid[0] = [];
}
var tile = this.grid[0][0];
if (!tile) {
tile = this.addTile(tileBounds, px);
this.addTileMonitoringHooks(tile);
tile.draw();
this.grid[0][0] = tile;
} else {
tile.moveTo(tileBounds, px);
}
//remove all but our single tile
this.removeExcessTiles(1,1);
// store the resolution of the grid
this.gridResolution = this.getServerResolution();
},
/**
* Method: calculateGridLayout
* Generate parameters for the grid layout.
*
* Parameters:
* bounds - {<OpenLayers.Bound>}
* origin - {<OpenLayers.LonLat>}
* resolution - {Number}
*
* Returns:
* Object containing properties tilelon, tilelat, tileoffsetlat,
* tileoffsetlat, tileoffsetx, tileoffsety
*/
calculateGridLayout: function(bounds, origin, resolution) {
bounds = bounds.clone();
var tilelon = resolution * this.tileSize.w;
var tilelat = resolution * this.tileSize.h;
var offsetlon = bounds.left - origin.lon;
var tilecol = Math.floor(offsetlon/tilelon) - this.buffer;
var tilecolremain = offsetlon/tilelon - tilecol;
var tileoffsetx = -tilecolremain * this.tileSize.w;
var tileoffsetlon = origin.lon + tilecol * tilelon;
var offsetlat = bounds.top - (origin.lat + tilelat);
var tilerow = Math.ceil(offsetlat/tilelat) + this.buffer;
var tilerowremain = tilerow - offsetlat/tilelat;
var tileoffsety = -tilerowremain * this.tileSize.h;
var tileoffsetlat = origin.lat + tilerow * tilelat;
return {
tilelon: tilelon, tilelat: tilelat,
tileoffsetlon: tileoffsetlon, tileoffsetlat: tileoffsetlat,
tileoffsetx: tileoffsetx, tileoffsety: tileoffsety
};
},
/**
* Method: getTileOrigin
* Determine the origin for aligning the grid of tiles. If a <tileOrigin>
* property is supplied, that will be returned. Otherwise, the origin
* will be derived from the layer's <maxExtent> property. In this case,
* the tile origin will be the corner of the <maxExtent> given by the
* <tileOriginCorner> property.
*
* Returns:
* {<OpenLayers.LonLat>} The tile origin.
*/
getTileOrigin: function() {
var origin = this.tileOrigin;
if (!origin) {
var extent = this.getMaxExtent();
var edges = ({
"tl": ["left", "top"],
"tr": ["right", "top"],
"bl": ["left", "bottom"],
"br": ["right", "bottom"]
})[this.tileOriginCorner];
origin = new OpenLayers.LonLat(extent[edges[0]], extent[edges[1]]);
}
return origin;
},
/**
* Method: initGriddedTiles
*
* Parameters:
* bounds - {<OpenLayers.Bounds>}
*/
initGriddedTiles:function(bounds) {
// work out mininum number of rows and columns; this is the number of
// tiles required to cover the viewport plus at least one for panning
var viewSize = this.map.getSize();
var minRows = Math.ceil(viewSize.h/this.tileSize.h) +
Math.max(1, 2 * this.buffer);
var minCols = Math.ceil(viewSize.w/this.tileSize.w) +
Math.max(1, 2 * this.buffer);
var origin = this.getTileOrigin();
var resolution = this.getServerResolution();
var tileLayout = this.calculateGridLayout(bounds, origin, resolution);
var tileoffsetx = Math.round(tileLayout.tileoffsetx); // heaven help us
var tileoffsety = Math.round(tileLayout.tileoffsety);
var tileoffsetlon = tileLayout.tileoffsetlon;
var tileoffsetlat = tileLayout.tileoffsetlat;
var tilelon = tileLayout.tilelon;
var tilelat = tileLayout.tilelat;
this.origin = new OpenLayers.Pixel(tileoffsetx, tileoffsety);
var startX = tileoffsetx;
var startLon = tileoffsetlon;
var rowidx = 0;
var layerContainerDivLeft = parseInt(this.map.layerContainerDiv.style.left);
var layerContainerDivTop = parseInt(this.map.layerContainerDiv.style.top);
do {
var row = this.grid[rowidx++];
if (!row) {
row = [];
this.grid.push(row);
}
tileoffsetlon = startLon;
tileoffsetx = startX;
var colidx = 0;
do {
var tileBounds =
new OpenLayers.Bounds(tileoffsetlon,
tileoffsetlat,
tileoffsetlon + tilelon,
tileoffsetlat + tilelat);
var x = tileoffsetx;
x -= layerContainerDivLeft;
var y = tileoffsety;
y -= layerContainerDivTop;
var px = new OpenLayers.Pixel(x, y);
var tile = row[colidx++];
if (!tile) {
tile = this.addTile(tileBounds, px);
this.addTileMonitoringHooks(tile);
row.push(tile);
} else {
tile.moveTo(tileBounds, px, false);
}
tileoffsetlon += tilelon;
tileoffsetx += this.tileSize.w;
} while ((tileoffsetlon <= bounds.right + tilelon * this.buffer)
|| colidx < minCols);
tileoffsetlat -= tilelat;
tileoffsety += this.tileSize.h;
} while((tileoffsetlat >= bounds.bottom - tilelat * this.buffer)
|| rowidx < minRows);
//shave off exceess rows and colums
this.removeExcessTiles(rowidx, colidx);
// store the resolution of the grid
this.gridResolution = this.getServerResolution();
//now actually draw the tiles
this.spiralTileLoad();
},
/**
* Method: getMaxExtent
* Get this layer's maximum extent. (Implemented as a getter for
* potential specific implementations in sub-classes.)
*
* Returns:
* {OpenLayers.Bounds}
*/
getMaxExtent: function() {
return this.maxExtent;
},
/**
* Method: spiralTileLoad
* Starts at the top right corner of the grid and proceeds in a spiral
* towards the center, adding tiles one at a time to the beginning of a
* queue.
*
* Once all the grid's tiles have been added to the queue, we go back
* and iterate through the queue (thus reversing the spiral order from
* outside-in to inside-out), calling draw() on each tile.
*/
spiralTileLoad: function() {
var tileQueue = [];
var directions = ["right", "down", "left", "up"];
var iRow = 0;
var iCell = -1;
var direction = OpenLayers.Util.indexOf(directions, "right");
var directionsTried = 0;
while( directionsTried < directions.length) {
var testRow = iRow;
var testCell = iCell;
switch (directions[direction]) {
case "right":
testCell++;
break;
case "down":
testRow++;
break;
case "left":
testCell--;
break;
case "up":
testRow--;
break;
}
// if the test grid coordinates are within the bounds of the
// grid, get a reference to the tile.
var tile = null;
if ((testRow < this.grid.length) && (testRow >= 0) &&
(testCell < this.grid[0].length) && (testCell >= 0)) {
tile = this.grid[testRow][testCell];
}
if ((tile != null) && (!tile.queued)) {
//add tile to beginning of queue, mark it as queued.
tileQueue.unshift(tile);
tile.queued = true;
//restart the directions counter and take on the new coords
directionsTried = 0;
iRow = testRow;
iCell = testCell;
} else {
//need to try to load a tile in a different direction
direction = (direction + 1) % 4;
directionsTried++;
}
}
// now we go through and draw the tiles in forward order
for(var i=0, len=tileQueue.length; i<len; i++) {
var tile = tileQueue[i];
tile.draw();
//mark tile as unqueued for the next time (since tiles are reused)
tile.queued = false;
}
},
/**
* APIMethod: addTile
* Create a tile, initialize it, and add it to the layer div.
*
* Parameters
* bounds - {<OpenLayers.Bounds>}
* position - {<OpenLayers.Pixel>}
*
* Returns:
* {<OpenLayers.Tile>} The added OpenLayers.Tile
*/
addTile:function(bounds, position) {
return new OpenLayers.Tile.Image(this, position, bounds, null,
this.tileSize, this.tileOptions);
},
/**
* Method: addTileMonitoringHooks
* This function takes a tile as input and adds the appropriate hooks to
* the tile so that the layer can keep track of the loading tiles.
*
* Parameters:
* tile - {<OpenLayers.Tile>}
*/
addTileMonitoringHooks: function(tile) {
tile.onLoadStart = function() {
//if that was first tile then trigger a 'loadstart' on the layer
if (this.numLoadingTiles == 0) {
this.events.triggerEvent("loadstart");
}
this.numLoadingTiles++;
};
tile.events.register("loadstart", this, tile.onLoadStart);
tile.onLoadEnd = function() {
this.numLoadingTiles--;
this.events.triggerEvent("tileloaded");
//if that was the last tile, then trigger a 'loadend' on the layer
if (this.numLoadingTiles == 0) {
this.events.triggerEvent("loadend");
this.removeBackBuffer();
}
};
tile.events.register("loadend", this, tile.onLoadEnd);
tile.events.register("unload", this, tile.onLoadEnd);
},
/**
* Method: removeTileMonitoringHooks
* This function takes a tile as input and removes the tile hooks
* that were added in addTileMonitoringHooks()
*
* Parameters:
* tile - {<OpenLayers.Tile>}
*/
removeTileMonitoringHooks: function(tile) {
tile.unload();
tile.events.un({
"loadstart": tile.onLoadStart,
"loadend": tile.onLoadEnd,
"unload": tile.onLoadEnd,
scope: this
});
},
/**
* Method: moveGriddedTiles
*/
moveGriddedTiles: function() {
var shifted = true;
var buffer = this.buffer || 1;
var scale = this.getResolutionScale();
var tlLayer = this.grid[0][0].position.clone();
tlLayer.x *= scale;
tlLayer.y *= scale;
tlLayer = tlLayer.add(parseInt(this.div.style.left, 10),
parseInt(this.div.style.top, 10));
var offsetX = parseInt(this.map.layerContainerDiv.style.left);
var offsetY = parseInt(this.map.layerContainerDiv.style.top);
var tlViewPort = tlLayer.add(offsetX, offsetY);
var tileSize = this.tileSize.clone();
tileSize.w *= scale;
tileSize.h *= scale;
if (tlViewPort.x > -tileSize.w * (buffer - 1)) {
this.shiftColumn(true);
} else if (tlViewPort.x < -tileSize.w * buffer) {
this.shiftColumn(false);
} else if (tlViewPort.y > -tileSize.h * (buffer - 1)) {
this.shiftRow(true);
} else if (tlViewPort.y < -tileSize.h * buffer) {
this.shiftRow(false);
} else {
shifted = false;
}
if (shifted) {
// we may have other row or columns to shift, schedule it
// with a setTimeout, to give the user a chance to sneak
// in moveTo's
this.timerId = window.setTimeout(this._moveGriddedTiles, 0);
}
},
/**
* Method: shiftRow
* Shifty grid work
*
* Parameters:
* prepend - {Boolean} if true, prepend to beginning.
* if false, then append to end
*/
shiftRow:function(prepend) {
var modelRowIndex = (prepend) ? 0 : (this.grid.length - 1);
var grid = this.grid;
var modelRow = grid[modelRowIndex];
var resolution = this.getServerResolution();
var deltaY = (prepend) ? -this.tileSize.h : this.tileSize.h;
var deltaLat = resolution * -deltaY;
var row = (prepend) ? grid.pop() : grid.shift();
for (var i=0, len=modelRow.length; i<len; i++) {
var modelTile = modelRow[i];
var bounds = modelTile.bounds.clone();
var position = modelTile.position.clone();
bounds.bottom = bounds.bottom + deltaLat;
bounds.top = bounds.top + deltaLat;
position.y = position.y + deltaY;
row[i].moveTo(bounds, position);
}
if (prepend) {
grid.unshift(row);
} else {
grid.push(row);
}
},
/**
* Method: shiftColumn
* Shift grid work in the other dimension
*
* Parameters:
* prepend - {Boolean} if true, prepend to beginning.
* if false, then append to end
*/
shiftColumn: function(prepend) {
var deltaX = (prepend) ? -this.tileSize.w : this.tileSize.w;
var resolution = this.getServerResolution();
var deltaLon = resolution * deltaX;
for (var i=0, len=this.grid.length; i<len; i++) {
var row = this.grid[i];
var modelTileIndex = (prepend) ? 0 : (row.length - 1);
var modelTile = row[modelTileIndex];
var bounds = modelTile.bounds.clone();
var position = modelTile.position.clone();
bounds.left = bounds.left + deltaLon;
bounds.right = bounds.right + deltaLon;
position.x = position.x + deltaX;
var tile = prepend ? this.grid[i].pop() : this.grid[i].shift();
tile.moveTo(bounds, position);
if (prepend) {
row.unshift(tile);
} else {
row.push(tile);
}
}
},
/**
* Method: removeExcessTiles
* When the size of the map or the buffer changes, we may need to
* remove some excess rows and columns.
*
* Parameters:
* rows - {Integer} Maximum number of rows we want our grid to have.
* columns - {Integer} Maximum number of columns we want our grid to have.
*/
removeExcessTiles: function(rows, columns) {
// remove extra rows
while (this.grid.length > rows) {
var row = this.grid.pop();
for (var i=0, l=row.length; i<l; i++) {
var tile = row[i];
this.removeTileMonitoringHooks(tile);
tile.destroy();
}
}
// remove extra columns
while (this.grid[0].length > columns) {
for (var i=0, l=this.grid.length; i<l; i++) {
var row = this.grid[i];
var tile = row.pop();
this.removeTileMonitoringHooks(tile);
tile.destroy();
}
}
},
/**
* Method: onMapResize
* For singleTile layers, this will set a new tile size according to the
* dimensions of the map pane.
*/
onMapResize: function() {
if (this.singleTile) {
this.clearGrid();
this.setTileSize();
}
},
/**
* APIMethod: getTileBounds
* Returns The tile bounds for a layer given a pixel location.
*
* Parameters:
* viewPortPx - {<OpenLayers.Pixel>} The location in the viewport.
*
* Returns:
* {<OpenLayers.Bounds>} Bounds of the tile at the given pixel location.
*/
getTileBounds: function(viewPortPx) {
var maxExtent = this.maxExtent;
var resolution = this.getResolution();
var tileMapWidth = resolution * this.tileSize.w;
var tileMapHeight = resolution * this.tileSize.h;
var mapPoint = this.getLonLatFromViewPortPx(viewPortPx);
var tileLeft = maxExtent.left + (tileMapWidth *
Math.floor((mapPoint.lon -
maxExtent.left) /
tileMapWidth));
var tileBottom = maxExtent.bottom + (tileMapHeight *
Math.floor((mapPoint.lat -
maxExtent.bottom) /
tileMapHeight));
return new OpenLayers.Bounds(tileLeft, tileBottom,
tileLeft + tileMapWidth,
tileBottom + tileMapHeight);
},
CLASS_NAME: "OpenLayers.Layer.Grid"
});