1 /* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD
2 * license. See http://svn.openlayers.org/trunk/openlayers/license.txt for the
3 * full text of the license. */
7 * @requires OpenLayers/Layer/HTTPRequest.js
8 * @requires OpenLayers/Console.js
12 * Class: OpenLayers.Layer.Grid
13 * Base class for layers that use a lattice of tiles. Create a new grid
14 * layer with the <OpenLayers.Layer.Grid> constructor.
17 * - <OpenLayers.Layer.HTTPRequest>
19 OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, {
22 * APIProperty: tileSize
29 * {Array(Array(<OpenLayers.Tile>))} This is an array of rows, each row is
35 * APIProperty: singleTile
36 * {Boolean} Moves the layer into single-tile mode, meaning that one tile
37 * will be loaded. The tile's size will be determined by the 'ratio'
38 * property. When the tile is dragged such that it does not cover the
39 * entire viewport, it is reloaded.
43 /** APIProperty: ratio
44 * {Float} Used only when in single-tile mode, this specifies the
45 * ratio of the size of the single tile to the size of the map.
51 * {Integer} Used only when in gridded mode, this specifies the number of
52 * extra rows and colums of tiles on each side which will
53 * surround the minimum grid tiles to cover the map.
58 * APIProperty: numLoadingTiles
59 * {Integer} How many tiles are still loading?
64 * Constructor: OpenLayers.Layer.Grid
65 * Create a new grid layer
71 * options - {Object} Hashtable of extra options to tag onto the layer
73 initialize: function(name, url, params, options) {
74 OpenLayers.Layer.HTTPRequest.prototype.initialize.apply(this,
77 //grid layers will trigger 'tileloaded' when each new tile is
78 // loaded, as a means of progress update to listeners.
79 // listeners can access 'numLoadingTiles' if they wish to keep track
80 // of the loading progress
82 this.events.addEventType("tileloaded");
89 * Deconstruct the layer and clear the grid.
95 OpenLayers.Layer.HTTPRequest.prototype.destroy.apply(this, arguments);
100 * Go through and remove all tiles from the grid, calling
101 * destroy() on each of them to kill circular references
103 clearGrid:function() {
105 for(var iRow=0, len=this.grid.length; iRow<len; iRow++) {
106 var row = this.grid[iRow];
107 for(var iCol=0, clen=row.length; iCol<clen; iCol++) {
108 var tile = row[iCol];
109 this.removeTileMonitoringHooks(tile);
119 * Create a clone of this layer
122 * obj - {Object} Is this ever used?
125 * {<OpenLayers.Layer.Grid>} An exact clone of this OpenLayers.Layer.Grid
127 clone: function (obj) {
130 obj = new OpenLayers.Layer.Grid(this.name,
136 //get all additions from superclasses
137 obj = OpenLayers.Layer.HTTPRequest.prototype.clone.apply(this, [obj]);
139 // copy/set any non-init, non-simple values here
140 if (this.tileSize != null) {
141 obj.tileSize = this.tileSize.clone();
144 // we do not want to copy reference to grid, so we make a new array
152 * This function is called whenever the map is moved. All the moving
153 * of actual 'tiles' is done by the map, but moveTo's role is to accept
154 * a bounds and make sure the data that that bounds requires is pre-loaded.
157 * bounds - {<OpenLayers.Bounds>}
158 * zoomChanged - {Boolean}
159 * dragging - {Boolean}
161 moveTo:function(bounds, zoomChanged, dragging) {
162 OpenLayers.Layer.HTTPRequest.prototype.moveTo.apply(this, arguments);
164 bounds = bounds || this.map.getExtent();
166 if (bounds != null) {
168 // if grid is empty or zoom has changed, we *must* re-tile
169 var forceReTile = !this.grid.length || zoomChanged;
171 // total bounds of the tiles
172 var tilesBounds = this.getTilesBounds();
174 if (this.singleTile) {
176 // We want to redraw whenever even the slightest part of the
177 // current bounds is not contained by our tile.
178 // (thus, we do not specify partial -- its default is false)
180 (!dragging && !tilesBounds.containsBounds(bounds))) {
181 this.initSingleTile(bounds);
185 // if the bounds have changed such that they are not even
186 // *partially* contained by our tiles (IE user has
187 // programmatically panned to the other side of the earth)
188 // then we want to reTile (thus, partial true).
190 if (forceReTile || !tilesBounds.containsBounds(bounds, true)) {
191 this.initGriddedTiles(bounds);
193 //we might have to shift our buffer tiles
194 this.moveGriddedTiles(bounds);
201 * APIMethod: setTileSize
202 * Check if we are in singleTile mode and if so, set the size as a ratio
203 * of the map size (as specified by the layer's 'ratio' property).
206 * size - {<OpenLayers.Size>}
208 setTileSize: function(size) {
209 if (this.singleTile) {
210 size = this.map.getSize().clone();
211 size.h = parseInt(size.h * this.ratio);
212 size.w = parseInt(size.w * this.ratio);
214 OpenLayers.Layer.HTTPRequest.prototype.setTileSize.apply(this, [size]);
218 * Method: getGridBounds
219 * Deprecated. This function will be removed in 3.0. Please use
220 * getTilesBounds() instead.
223 * {<OpenLayers.Bounds>} A Bounds object representing the bounds of all the
224 * currently loaded tiles (including those partially or not at all seen
227 getGridBounds: function() {
228 var msg = "The getGridBounds() function is deprecated. It will be " +
229 "removed in 3.0. Please use getTilesBounds() instead.";
230 OpenLayers.Console.warn(msg);
231 return this.getTilesBounds();
235 * APIMethod: getTilesBounds
236 * Return the bounds of the tile grid.
239 * {<OpenLayers.Bounds>} A Bounds object representing the bounds of all the
240 * currently loaded tiles (including those partially or not at all seen
243 getTilesBounds: function() {
246 if (this.grid.length) {
247 var bottom = this.grid.length - 1;
248 var bottomLeftTile = this.grid[bottom][0];
250 var right = this.grid[0].length - 1;
251 var topRightTile = this.grid[0][right];
253 bounds = new OpenLayers.Bounds(bottomLeftTile.bounds.left,
254 bottomLeftTile.bounds.bottom,
255 topRightTile.bounds.right,
256 topRightTile.bounds.top);
263 * Method: initSingleTile
266 * bounds - {<OpenLayers.Bounds>}
268 initSingleTile: function(bounds) {
270 //determine new tile bounds
271 var center = bounds.getCenterLonLat();
272 var tileWidth = bounds.getWidth() * this.ratio;
273 var tileHeight = bounds.getHeight() * this.ratio;
276 new OpenLayers.Bounds(center.lon - (tileWidth/2),
277 center.lat - (tileHeight/2),
278 center.lon + (tileWidth/2),
279 center.lat + (tileHeight/2));
281 var ul = new OpenLayers.LonLat(tileBounds.left, tileBounds.top);
282 var px = this.map.getLayerPxFromLonLat(ul);
284 if (!this.grid.length) {
288 var tile = this.grid[0][0];
290 tile = this.addTile(tileBounds, px);
292 this.addTileMonitoringHooks(tile);
294 this.grid[0][0] = tile;
296 tile.moveTo(tileBounds, px);
299 //remove all but our single tile
300 this.removeExcessTiles(1,1);
304 * Method: calculateGridLayout
305 * Generate parameters for the grid layout. This
308 * bounds - {<OpenLayers.Bound>}
309 * extent - {<OpenLayers.Bounds>}
310 * resolution - {Number}
313 * Object containing properties tilelon, tilelat, tileoffsetlat,
314 * tileoffsetlat, tileoffsetx, tileoffsety
316 calculateGridLayout: function(bounds, extent, resolution) {
317 var tilelon = resolution * this.tileSize.w;
318 var tilelat = resolution * this.tileSize.h;
320 var offsetlon = bounds.left - extent.left;
321 var tilecol = Math.floor(offsetlon/tilelon) - this.buffer;
322 var tilecolremain = offsetlon/tilelon - tilecol;
323 var tileoffsetx = -tilecolremain * this.tileSize.w;
324 var tileoffsetlon = extent.left + tilecol * tilelon;
326 var offsetlat = bounds.top - (extent.bottom + tilelat);
327 var tilerow = Math.ceil(offsetlat/tilelat) + this.buffer;
328 var tilerowremain = tilerow - offsetlat/tilelat;
329 var tileoffsety = -tilerowremain * this.tileSize.h;
330 var tileoffsetlat = extent.bottom + tilerow * tilelat;
333 tilelon: tilelon, tilelat: tilelat,
334 tileoffsetlon: tileoffsetlon, tileoffsetlat: tileoffsetlat,
335 tileoffsetx: tileoffsetx, tileoffsety: tileoffsety
341 * Method: initGriddedTiles
344 * bounds - {<OpenLayers.Bounds>}
346 initGriddedTiles:function(bounds) {
348 // work out mininum number of rows and columns; this is the number of
349 // tiles required to cover the viewport plus at least one for panning
351 var viewSize = this.map.getSize();
352 var minRows = Math.ceil(viewSize.h/this.tileSize.h) +
353 Math.max(1, 2 * this.buffer);
354 var minCols = Math.ceil(viewSize.w/this.tileSize.w) +
355 Math.max(1, 2 * this.buffer);
357 var extent = this.maxExtent;
358 var resolution = this.map.getResolution();
360 var tileLayout = this.calculateGridLayout(bounds, extent, resolution);
362 var tileoffsetx = Math.round(tileLayout.tileoffsetx); // heaven help us
363 var tileoffsety = Math.round(tileLayout.tileoffsety);
365 var tileoffsetlon = tileLayout.tileoffsetlon;
366 var tileoffsetlat = tileLayout.tileoffsetlat;
368 var tilelon = tileLayout.tilelon;
369 var tilelat = tileLayout.tilelat;
371 this.origin = new OpenLayers.Pixel(tileoffsetx, tileoffsety);
373 var startX = tileoffsetx;
374 var startLon = tileoffsetlon;
378 var layerContainerDivLeft = parseInt(this.map.layerContainerDiv.style.left);
379 var layerContainerDivTop = parseInt(this.map.layerContainerDiv.style.top);
383 var row = this.grid[rowidx++];
389 tileoffsetlon = startLon;
390 tileoffsetx = startX;
395 new OpenLayers.Bounds(tileoffsetlon,
397 tileoffsetlon + tilelon,
398 tileoffsetlat + tilelat);
401 x -= layerContainerDivLeft;
404 y -= layerContainerDivTop;
406 var px = new OpenLayers.Pixel(x, y);
407 var tile = row[colidx++];
409 tile = this.addTile(tileBounds, px);
410 this.addTileMonitoringHooks(tile);
413 tile.moveTo(tileBounds, px, false);
416 tileoffsetlon += tilelon;
417 tileoffsetx += this.tileSize.w;
418 } while ((tileoffsetlon <= bounds.right + tilelon * this.buffer)
419 || colidx < minCols);
421 tileoffsetlat -= tilelat;
422 tileoffsety += this.tileSize.h;
423 } while((tileoffsetlat >= bounds.bottom - tilelat * this.buffer)
424 || rowidx < minRows);
426 //shave off exceess rows and colums
427 this.removeExcessTiles(rowidx, colidx);
429 //now actually draw the tiles
430 this.spiralTileLoad();
434 * Method: spiralTileLoad
435 * Starts at the top right corner of the grid and proceeds in a spiral
436 * towards the center, adding tiles one at a time to the beginning of a
439 * Once all the grid's tiles have been added to the queue, we go back
440 * and iterate through the queue (thus reversing the spiral order from
441 * outside-in to inside-out), calling draw() on each tile.
443 spiralTileLoad: function() {
446 var directions = ["right", "down", "left", "up"];
450 var direction = OpenLayers.Util.indexOf(directions, "right");
451 var directionsTried = 0;
453 while( directionsTried < directions.length) {
456 var testCell = iCell;
458 switch (directions[direction]) {
473 // if the test grid coordinates are within the bounds of the
474 // grid, get a reference to the tile.
476 if ((testRow < this.grid.length) && (testRow >= 0) &&
477 (testCell < this.grid[0].length) && (testCell >= 0)) {
478 tile = this.grid[testRow][testCell];
481 if ((tile != null) && (!tile.queued)) {
482 //add tile to beginning of queue, mark it as queued.
483 tileQueue.unshift(tile);
486 //restart the directions counter and take on the new coords
491 //need to try to load a tile in a different direction
492 direction = (direction + 1) % 4;
497 // now we go through and draw the tiles in forward order
498 for(var i=0, len=tileQueue.length; i<len; i++) {
499 var tile = tileQueue[i];
501 //mark tile as unqueued for the next time (since tiles are reused)
508 * Gives subclasses of Grid the opportunity to create an
509 * OpenLayer.Tile of their choosing. The implementer should initialize
510 * the new tile and take whatever steps necessary to display it.
513 * bounds - {<OpenLayers.Bounds>}
514 * position - {<OpenLayers.Pixel>}
517 * {<OpenLayers.Tile>} The added OpenLayers.Tile
519 addTile:function(bounds, position) {
520 // Should be implemented by subclasses
524 * Method: addTileMonitoringHooks
525 * This function takes a tile as input and adds the appropriate hooks to
526 * the tile so that the layer can keep track of the loading tiles.
529 * tile - {<OpenLayers.Tile>}
531 addTileMonitoringHooks: function(tile) {
533 tile.onLoadStart = function() {
534 //if that was first tile then trigger a 'loadstart' on the layer
535 if (this.numLoadingTiles == 0) {
536 this.events.triggerEvent("loadstart");
538 this.numLoadingTiles++;
540 tile.events.register("loadstart", this, tile.onLoadStart);
542 tile.onLoadEnd = function() {
543 this.numLoadingTiles--;
544 this.events.triggerEvent("tileloaded");
545 //if that was the last tile, then trigger a 'loadend' on the layer
546 if (this.numLoadingTiles == 0) {
547 this.events.triggerEvent("loadend");
550 tile.events.register("loadend", this, tile.onLoadEnd);
551 tile.events.register("unload", this, tile.onLoadEnd);
555 * Method: removeTileMonitoringHooks
556 * This function takes a tile as input and removes the tile hooks
557 * that were added in addTileMonitoringHooks()
560 * tile - {<OpenLayers.Tile>}
562 removeTileMonitoringHooks: function(tile) {
565 "loadstart": tile.onLoadStart,
566 "loadend": tile.onLoadEnd,
567 "unload": tile.onLoadEnd,
573 * Method: moveGriddedTiles
576 * bounds - {<OpenLayers.Bounds>}
578 moveGriddedTiles: function(bounds) {
579 var buffer = this.buffer || 1;
581 var tlLayer = this.grid[0][0].position;
583 this.map.getViewPortPxFromLayerPx(tlLayer);
584 if (tlViewPort.x > -this.tileSize.w * (buffer - 1)) {
585 this.shiftColumn(true);
586 } else if (tlViewPort.x < -this.tileSize.w * buffer) {
587 this.shiftColumn(false);
588 } else if (tlViewPort.y > -this.tileSize.h * (buffer - 1)) {
590 } else if (tlViewPort.y < -this.tileSize.h * buffer) {
591 this.shiftRow(false);
603 * prepend - {Boolean} if true, prepend to beginning.
604 * if false, then append to end
606 shiftRow:function(prepend) {
607 var modelRowIndex = (prepend) ? 0 : (this.grid.length - 1);
608 var grid = this.grid;
609 var modelRow = grid[modelRowIndex];
611 var resolution = this.map.getResolution();
612 var deltaY = (prepend) ? -this.tileSize.h : this.tileSize.h;
613 var deltaLat = resolution * -deltaY;
615 var row = (prepend) ? grid.pop() : grid.shift();
617 for (var i=0, len=modelRow.length; i<len; i++) {
618 var modelTile = modelRow[i];
619 var bounds = modelTile.bounds.clone();
620 var position = modelTile.position.clone();
621 bounds.bottom = bounds.bottom + deltaLat;
622 bounds.top = bounds.top + deltaLat;
623 position.y = position.y + deltaY;
624 row[i].moveTo(bounds, position);
635 * Method: shiftColumn
636 * Shift grid work in the other dimension
639 * prepend - {Boolean} if true, prepend to beginning.
640 * if false, then append to end
642 shiftColumn: function(prepend) {
643 var deltaX = (prepend) ? -this.tileSize.w : this.tileSize.w;
644 var resolution = this.map.getResolution();
645 var deltaLon = resolution * deltaX;
647 for (var i=0, len=this.grid.length; i<len; i++) {
648 var row = this.grid[i];
649 var modelTileIndex = (prepend) ? 0 : (row.length - 1);
650 var modelTile = row[modelTileIndex];
652 var bounds = modelTile.bounds.clone();
653 var position = modelTile.position.clone();
654 bounds.left = bounds.left + deltaLon;
655 bounds.right = bounds.right + deltaLon;
656 position.x = position.x + deltaX;
658 var tile = prepend ? this.grid[i].pop() : this.grid[i].shift();
659 tile.moveTo(bounds, position);
669 * Method: removeExcessTiles
670 * When the size of the map or the buffer changes, we may need to
671 * remove some excess rows and columns.
674 * rows - {Integer} Maximum number of rows we want our grid to have.
675 * colums - {Integer} Maximum number of columns we want our grid to have.
677 removeExcessTiles: function(rows, columns) {
680 while (this.grid.length > rows) {
681 var row = this.grid.pop();
682 for (var i=0, l=row.length; i<l; i++) {
684 this.removeTileMonitoringHooks(tile);
689 // remove extra columns
690 while (this.grid[0].length > columns) {
691 for (var i=0, l=this.grid.length; i<l; i++) {
692 var row = this.grid[i];
693 var tile = row.pop();
694 this.removeTileMonitoringHooks(tile);
701 * Method: onMapResize
702 * For singleTile layers, this will set a new tile size according to the
703 * dimensions of the map pane.
705 onMapResize: function() {
706 if (this.singleTile) {
713 * APIMethod: getTileBounds
714 * Returns The tile bounds for a layer given a pixel location.
717 * viewPortPx - {<OpenLayers.Pixel>} The location in the viewport.
720 * {<OpenLayers.Bounds>} Bounds of the tile at the given pixel location.
722 getTileBounds: function(viewPortPx) {
723 var maxExtent = this.maxExtent;
724 var resolution = this.getResolution();
725 var tileMapWidth = resolution * this.tileSize.w;
726 var tileMapHeight = resolution * this.tileSize.h;
727 var mapPoint = this.getLonLatFromViewPortPx(viewPortPx);
728 var tileLeft = maxExtent.left + (tileMapWidth *
729 Math.floor((mapPoint.lon -
732 var tileBottom = maxExtent.bottom + (tileMapHeight *
733 Math.floor((mapPoint.lat -
736 return new OpenLayers.Bounds(tileLeft, tileBottom,
737 tileLeft + tileMapWidth,
738 tileBottom + tileMapHeight);
741 CLASS_NAME: "OpenLayers.Layer.Grid"