Files
openlayers/lib/OpenLayers/Layer/Grid.js
crschmidt cde39e98ae Improve the handling of tile events with regard to tiles being unloaded before
their load events fire by adding an 'unload' event to the tile, and calling
it from the places where we're about to stop listening to events. In the longer
term, it might make sense to have this be automatic, but this resolves issues
with map resizes screwing with tile events, and reverts a previous,
incomplete solution to solve a problem with untiled tiles not resizing when
the map size changes. r=ahocevar, checked out by bartvde (Closes #1417) 


git-svn-id: http://svn.openlayers.org/trunk/openlayers@6495 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
2008-03-12 12:53:34 +00:00

742 lines
24 KiB
JavaScript

/* Copyright (c) 2006-2008 MetaCarta, Inc., 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
*/
/**
* 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: 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.
*/
buffer: 2,
/**
* APIProperty: numLoadingTiles
* {Integer} How many tiles are still loading?
*/
numLoadingTiles: 0,
/**
* 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);
//grid layers will trigger 'tileloaded' 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
//
this.events.addEventType("tileloaded");
this.grid = [];
},
/**
* APIMethod: destroy
* Deconstruct the layer and clear the grid.
*/
destroy: function() {
this.clearGrid();
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; iRow < this.grid.length; iRow++) {
var row = this.grid[iRow];
for(var iCol=0; iCol < row.length; iCol++) {
var tile = row[iCol];
this.removeTileMonitoringHooks(tile);
tile.destroy();
}
}
this.grid = [];
}
},
/**
* 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.options);
}
//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 = [];
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();
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))) {
this.initSingleTile(bounds);
}
} else {
// if the bounds have changed such that they are not even
// *partially* contained by our tiles (IE user has
// programmatically panned to the other side of the earth)
// then we want to reTile (thus, partial true).
//
if (forceReTile || !tilesBounds.containsBounds(bounds, true)) {
this.initGriddedTiles(bounds);
} else {
//we might have to shift our buffer tiles
this.moveGriddedTiles(bounds);
}
}
}
},
/**
* 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().clone();
size.h = parseInt(size.h * this.ratio);
size.w = parseInt(size.w * this.ratio);
}
OpenLayers.Layer.HTTPRequest.prototype.setTileSize.apply(this, [size]);
},
/**
* Method: getGridBounds
* Deprecated. This function will be removed in 3.0. Please use
* getTilesBounds() instead.
*
* Returns:
* {<OpenLayers.Bounds>} A Bounds object representing the bounds of all the
* currently loaded tiles (including those partially or not at all seen
* onscreen)
*/
getGridBounds: function() {
var msg = "The getGridBounds() function is deprecated. It will be " +
"removed in 3.0. Please use getTilesBounds() instead.";
OpenLayers.Console.warn(msg);
return this.getTilesBounds();
},
/**
* 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;
if (this.grid.length) {
var bottom = this.grid.length - 1;
var bottomLeftTile = this.grid[bottom][0];
var right = this.grid[0].length - 1;
var topRightTile = this.grid[0][right];
bounds = new OpenLayers.Bounds(bottomLeftTile.bounds.left,
bottomLeftTile.bounds.bottom,
topRightTile.bounds.right,
topRightTile.bounds.top);
}
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);
},
/**
* Method: calculateGridLayout
* Generate parameters for the grid layout. This
*
* Parameters:
* bounds - {<OpenLayers.Bound>}
* extent - {<OpenLayers.Bounds>}
* resolution - {Number}
*
* Returns:
* Object containing properties tilelon, tilelat, tileoffsetlat,
* tileoffsetlat, tileoffsetx, tileoffsety
*/
calculateGridLayout: function(bounds, extent, resolution) {
var tilelon = resolution * this.tileSize.w;
var tilelat = resolution * this.tileSize.h;
var offsetlon = bounds.left - extent.left;
var tilecol = Math.floor(offsetlon/tilelon) - this.buffer;
var tilecolremain = offsetlon/tilelon - tilecol;
var tileoffsetx = -tilecolremain * this.tileSize.w;
var tileoffsetlon = extent.left + tilecol * tilelon;
var offsetlat = bounds.top - (extent.bottom + tilelat);
var tilerow = Math.ceil(offsetlat/tilelat) + this.buffer;
var tilerowremain = tilerow - offsetlat/tilelat;
var tileoffsety = -tilerowremain * this.tileSize.h;
var tileoffsetlat = extent.bottom + tilerow * tilelat;
return {
tilelon: tilelon, tilelat: tilelat,
tileoffsetlon: tileoffsetlon, tileoffsetlat: tileoffsetlat,
tileoffsetx: tileoffsetx, tileoffsety: tileoffsety
}
},
/**
* 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 extent = this.map.getMaxExtent();
var resolution = this.map.getResolution();
var tileLayout = this.calculateGridLayout(bounds, extent, 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);
//now actually draw the tiles
this.spiralTileLoad();
},
/**
* 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; i < tileQueue.length; i++) {
var tile = tileQueue[i];
tile.draw();
//mark tile as unqueued for the next time (since tiles are reused)
tile.queued = false;
}
},
/**
* APIMethod: addTile
* Gives subclasses of Grid the opportunity to create an
* OpenLayer.Tile of their choosing. The implementer should initialize
* the new tile and take whatever steps necessary to display it.
*
* Parameters
* bounds - {<OpenLayers.Bounds>}
* position - {<OpenLayers.Pixel>}
*
* Returns:
* {<OpenLayers.Tile>} The added OpenLayers.Tile
*/
addTile:function(bounds, position) {
// Should be implemented by subclasses
},
/**
* 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");
}
};
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
*
* Parameters:
* bounds - {<OpenLayers.Bounds>}
*/
moveGriddedTiles: function(bounds) {
var buffer = this.buffer || 1;
while (true) {
var tlLayer = this.grid[0][0].position;
var tlViewPort =
this.map.getViewPortPxFromLayerPx(tlLayer);
if (tlViewPort.x > -this.tileSize.w * (buffer - 1)) {
this.shiftColumn(true);
} else if (tlViewPort.x < -this.tileSize.w * buffer) {
this.shiftColumn(false);
} else if (tlViewPort.y > -this.tileSize.h * (buffer - 1)) {
this.shiftRow(true);
} else if (tlViewPort.y < -this.tileSize.h * buffer) {
this.shiftRow(false);
} else {
break;
}
};
},
/**
* 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.map.getResolution();
var deltaY = (prepend) ? -this.tileSize.h : this.tileSize.h;
var deltaLat = resolution * -deltaY;
var row = (prepend) ? grid.pop() : grid.shift();
for (var i=0; i < modelRow.length; 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.map.getResolution();
var deltaLon = resolution * deltaX;
for (var i=0; i<this.grid.length; 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.
* colums - {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.map.getMaxExtent();
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"
});