--- a/labs/openlayers/lib/OpenLayers/Layer/Grid.js +++ b/labs/openlayers/lib/OpenLayers/Layer/Grid.js @@ -1,1 +1,756 @@ - +/* Copyright (c) 2006-2010 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/Console.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, 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 = []; + } + }, + + /** + * 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 = []; + + 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(); + 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.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: 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 + * 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, 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.map.getResolution(); + 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. + * 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.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" +}); +