From d4f011d00cb9256b2b37eee44b34978cf6ea353b Mon Sep 17 00:00:00 2001 From: ahocevar Date: Wed, 10 Oct 2012 12:22:57 +0200 Subject: [PATCH 01/14] Absolute calculation of tile bounds This avoids cumulated tile bounds errors for layer types that do not use a tile row/column index in requests (e.g. WMS). --- lib/OpenLayers/Layer/Grid.js | 14 ++++++++++---- tests/Layer/WMS.html | 12 ++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index fcb2b42e64..bc1d36cb0e 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -867,7 +867,8 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * * Returns: * {Object} containing properties tilelon, tilelat, tileoffsetlat, - * tileoffsetlat, tileoffsetx, tileoffsety + * tileoffsetlat, tileoffsetx, tileoffsety and optional startrow, startcol + * for grid layouts where absolute tile bounds calculation is possible. */ calculateGridLayout: function(bounds, origin, resolution) { var tilelon = resolution * this.tileSize.w; @@ -894,7 +895,8 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { return { tilelon: tilelon, tilelat: tilelat, tileoffsetlon: tileoffsetlon, tileoffsetlat: tileoffsetlat, - tileoffsetx: tileoffsetx, tileoffsety: tileoffsety + tileoffsetx: tileoffsetx, tileoffsety: tileoffsety, + startcol: tilecol, startrow: tilerow }; }, @@ -1012,12 +1014,16 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { Math.pow(tileCenter.lat - center.lat, 2) }); - tileoffsetlon += tilelon; + tileoffsetlon = 'startcol' in tileLayout ? + origin.lon + (tileLayout.startcol + colidx) * tilelon : + tileoffsetlon + tilelon; tileoffsetx += Math.round(tileSize.w); } while ((tileoffsetlon <= bounds.right + tilelon * this.buffer) || colidx < minCols); - tileoffsetlat -= tilelat; + tileoffsetlat = 'startrow' in tileLayout ? + origin.lat + (tileLayout.startrow - rowidx) * tilelat : + tileoffsetlat - tilelat; tileoffsety += Math.round(tileSize.h); } while((tileoffsetlat >= bounds.bottom - tilelat * this.buffer) || rowidx < minRows); diff --git a/tests/Layer/WMS.html b/tests/Layer/WMS.html index 0e9fb9a8fc..fc292b809e 100644 --- a/tests/Layer/WMS.html +++ b/tests/Layer/WMS.html @@ -533,6 +533,18 @@ map.destroy(); } + + function test_tileBounds(t) { + t.plan(1); + var map = new OpenLayers.Map("map", {projection: "EPSG:3857"}); + var layer = new OpenLayers.Layer.WMS("wms", "../../img/blank.gif"); + map.addLayer(layer); + map.setCenter([0, 0], 1); + map.pan(2, -100); + map.zoomIn(); + t.eq(layer.grid[1][0].bounds, new OpenLayers.Bounds(-10018754.17, 0, 0, 10018754.17), "no floating point errors"); + map.destroy(); + } From c5bb52d93f9f4fe7221962ae994653c31da392cd Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 11 Oct 2012 21:22:52 +0200 Subject: [PATCH 02/14] No deltas for tile bounds/position calculation Now we also do not use deltas for shiftRow and shiftColumn. Some refactoring was done so we do not need different calculateGridLayout methods for layers with top-left and bottom-left tile origin. TODO: With this commit, ArcGisCache and KaMap layers are broken. --- lib/OpenLayers/Layer/Grid.js | 172 ++++++++++++++++--------------- lib/OpenLayers/Layer/KaMap.js | 3 +- lib/OpenLayers/Layer/MapGuide.js | 36 ------- lib/OpenLayers/Layer/Zoomify.js | 36 ------- tests/Layer/WMS.html | 16 ++- 5 files changed, 104 insertions(+), 159 deletions(-) diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index bc1d36cb0e..e0502fabb9 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -270,6 +270,14 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * tile that could not be loaded. */ + /** + * Property: gridLayout + * {Object} containing properties tilelon, tilelat, tileoffsetlat, + * tileoffsetlat, tileoffsetx, tileoffsety and optional startrow, startcol + * for grid layouts where absolute tile bounds calculation is possible. + */ + gridLayout: null, + /** * Constructor: OpenLayers.Layer.Grid * Create a new grid layer @@ -363,6 +371,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { } this.grid = []; this.gridResolution = null; + this.gridLayout = null; } }, @@ -866,9 +875,8 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * resolution - {Number} * * Returns: - * {Object} containing properties tilelon, tilelat, tileoffsetlat, - * tileoffsetlat, tileoffsetx, tileoffsety and optional startrow, startcol - * for grid layouts where absolute tile bounds calculation is possible. + * {Object} Object containing properties tilelon, tilelat, tileoffsetx, + * tileoffsety, startcol, startrow */ calculateGridLayout: function(bounds, origin, resolution) { var tilelon = resolution * this.tileSize.w; @@ -884,17 +892,16 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var tilecol = Math.floor(offsetlon/tilelon) - this.buffer; var tilecolremain = offsetlon/tilelon - tilecol; var tileoffsetx = -tilecolremain * tileSize.w; - var tileoffsetlon = origin.lon + tilecol * tilelon; - var offsetlat = bounds.top - (origin.lat + tilelat); - var tilerow = Math.ceil(offsetlat/tilelat) + this.buffer; + var rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; + + var offsetlat = rowSign * (origin.lat - bounds.top + tilelat); + var tilerow = Math[~rowSign ? 'floor' : 'ceil'](offsetlat/tilelat) - this.buffer * rowSign; var tilerowremain = tilerow - offsetlat/tilelat; - var tileoffsety = -tilerowremain * tileSize.h; - var tileoffsetlat = origin.lat + tilerow * tilelat; + var tileoffsety = rowSign * tilerowremain * tileSize.h; return { tilelon: tilelon, tilelat: tilelat, - tileoffsetlon: tileoffsetlon, tileoffsetlat: tileoffsetlat, tileoffsetx: tileoffsetx, tileoffsety: tileoffsety, startcol: tilecol, startrow: tilerow }; @@ -927,6 +934,30 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { return origin; }, + /** + * Method: getTileBoundsForGridIndex + * + * Parameters: + * row - {Number} The row of the grid + * col - {Number} The column of the grid + * + * Returns: + * {} The bounds for the tile at (row, col) + */ + getTileBoundsForGridIndex: function(row, col) { + var origin = this.getTileOrigin(); + var tileLayout = this.gridLayout; + var tilelon = tileLayout.tilelon; + var tilelat = tileLayout.tilelat; + var rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; + var minX = origin.lon + (tileLayout.startcol + col) * tilelon; + var minY = origin.lat - (tileLayout.startrow + row * rowSign) * tilelat * rowSign; + return new OpenLayers.Bounds( + minX, minY, + minX + tilelon, minY + tilelat + ); + }, + /** * Method: initGriddedTiles * @@ -956,50 +987,34 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { 2 * this.buffer + 1; var tileLayout = this.calculateGridLayout(bounds, origin, serverResolution); - var tileoffsetx = Math.round(tileLayout.tileoffsetx); // heaven help us - var tileoffsety = Math.round(tileLayout.tileoffsety); + this.gridLayout = tileLayout; - var tileoffsetlon = tileLayout.tileoffsetlon; - var tileoffsetlat = tileLayout.tileoffsetlat; + var startX = Math.round(tileLayout.tileoffsetx); // heaven help us + var startY = Math.round(tileLayout.tileoffsety); var tilelon = tileLayout.tilelon; var tilelat = tileLayout.tilelat; - - var startX = tileoffsetx; - var startLon = tileoffsetlon; - - var rowidx = 0; var layerContainerDivLeft = this.map.layerContainerOriginPx.x; var layerContainerDivTop = this.map.layerContainerOriginPx.y; var tileData = [], center = this.map.getCenter(); + + var rowidx = 0; do { - var row = this.grid[rowidx++]; + 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 tileBounds = this.getTileBoundsForGridIndex(rowidx, colidx); + var x = startX + colidx * tileSize.w - layerContainerDivLeft; + var y = startY + rowidx * tileSize.h - layerContainerDivTop; var px = new OpenLayers.Pixel(x, y); - var tile = row[colidx++]; + var tile = row[colidx]; if (!tile) { tile = this.addTile(tileBounds, px); this.addTileMonitoringHooks(tile); @@ -1014,18 +1029,12 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { Math.pow(tileCenter.lat - center.lat, 2) }); - tileoffsetlon = 'startcol' in tileLayout ? - origin.lon + (tileLayout.startcol + colidx) * tilelon : - tileoffsetlon + tilelon; - tileoffsetx += Math.round(tileSize.w); - } while ((tileoffsetlon <= bounds.right + tilelon * this.buffer) + colidx += 1; + } while ((tileBounds.right <= bounds.right + tilelon * this.buffer) || colidx < minCols); - tileoffsetlat = 'startrow' in tileLayout ? - origin.lat + (tileLayout.startrow - rowidx) * tilelat : - tileoffsetlat - tilelat; - tileoffsety += Math.round(tileSize.h); - } while((tileoffsetlat >= bounds.bottom - tilelat * this.buffer) + rowidx += 1; + } while((tileBounds.bottom >= bounds.bottom - tilelat * this.buffer) || rowidx < minRows); //shave off exceess rows and colums @@ -1200,31 +1209,28 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * if false, then append to end * tileSize - {Object} rendered tile size; object with w and h properties */ - shiftRow:function(prepend, tileSize) { - var modelRowIndex = (prepend) ? 0 : (this.grid.length - 1); + shiftRow: function(prepend, tileSize) { var grid = this.grid; - var modelRow = grid[modelRowIndex]; - + var rowIndex = prepend ? 0 : (grid.length - 1); var sign = prepend ? -1 : 1; - var deltaLat = this.getServerResolution() * -sign * this.tileSize.h; + var rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; + var tileLayout = this.gridLayout; + tileLayout.startrow += sign * rowSign; - var row = (prepend) ? grid.pop() : grid.shift(); + var bounds = this.getTileBoundsForGridIndex(rowIndex, 0); + var position = this.map.getViewPortPxFromLonLat( + new OpenLayers.LonLat(bounds.left, bounds.top) + ); + var y = Math.round(position.y - this.map.layerContainerOriginPx.y); - for (var i=0, len=modelRow.length; i} - * origin - {} - * resolution - {Number} - * - * Returns: - * {Object} Object containing properties tilelon, tilelat, tileoffsetlat, - * tileoffsetlat, tileoffsetx, tileoffsety - */ - calculateGridLayout: function(bounds, origin, resolution) { - 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 = origin.lat - bounds.top + tilelat; - var tilerow = Math.floor(offsetlat/tilelat) - this.buffer; - var tilerowremain = tilerow - offsetlat/tilelat; - var tileoffsety = tilerowremain * this.tileSize.h; - var tileoffsetlat = origin.lat - tilelat*tilerow; - - return { - tilelon: tilelon, tilelat: tilelat, - tileoffsetlon: tileoffsetlon, tileoffsetlat: tileoffsetlat, - tileoffsetx: tileoffsetx, tileoffsety: tileoffsety - }; - }, - CLASS_NAME: "OpenLayers.Layer.MapGuide" }); diff --git a/lib/OpenLayers/Layer/Zoomify.js b/lib/OpenLayers/Layer/Zoomify.js index d0b482ab29..8c5a8a9e4d 100644 --- a/lib/OpenLayers/Layer/Zoomify.js +++ b/lib/OpenLayers/Layer/Zoomify.js @@ -256,41 +256,5 @@ OpenLayers.Layer.Zoomify = OpenLayers.Class(OpenLayers.Layer.Grid, { this.map.maxExtent.top); }, - /** - * Method: calculateGridLayout - * Generate parameters for the grid layout. This - * - * Parameters: - * bounds - {} - * origin - {} - * resolution - {Number} - * - * Returns: - * {Object} Object containing properties tilelon, tilelat, tileoffsetlat, - * tileoffsetlat, tileoffsetx, tileoffsety - */ - calculateGridLayout: function(bounds, origin, resolution) { - 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 = origin.lat - bounds.top + tilelat; - var tilerow = Math.floor(offsetlat/tilelat) - this.buffer; - var tilerowremain = tilerow - offsetlat/tilelat; - var tileoffsety = tilerowremain * this.tileSize.h; - var tileoffsetlat = origin.lat - tilelat*tilerow; - - return { - tilelon: tilelon, tilelat: tilelat, - tileoffsetlon: tileoffsetlon, tileoffsetlat: tileoffsetlat, - tileoffsetx: tileoffsetx, tileoffsety: tileoffsety - }; - }, - CLASS_NAME: "OpenLayers.Layer.Zoomify" }); diff --git a/tests/Layer/WMS.html b/tests/Layer/WMS.html index fc292b809e..9577acea91 100644 --- a/tests/Layer/WMS.html +++ b/tests/Layer/WMS.html @@ -535,15 +535,27 @@ } function test_tileBounds(t) { - t.plan(1); + // do not defer moveGriddedTiles + var isNative = OpenLayers.Animation.isNative; + OpenLayers.Animation.isNative = true; + t.plan(2); + var map = new OpenLayers.Map("map", {projection: "EPSG:3857"}); var layer = new OpenLayers.Layer.WMS("wms", "../../img/blank.gif"); map.addLayer(layer); map.setCenter([0, 0], 1); map.pan(2, -100); map.zoomIn(); - t.eq(layer.grid[1][0].bounds, new OpenLayers.Bounds(-10018754.17, 0, 0, 10018754.17), "no floating point errors"); + t.eq(layer.grid[1][0].bounds, new OpenLayers.Bounds(-10018754.17, 0, 0, 10018754.17), "no floating point errors after zooming"); + map.zoomTo(14); + var bounds = layer.grid[0][0].bounds.clone(); + map.pan(260, 520); + map.pan(-260, -520); + t.eq(layer.grid[0][0].bounds, bounds, "no floating point errors after dragging back and forth"); + console.log(bounds.toString()) + map.destroy(); + OpenLayers.Animation.isNative = isNative; } From e7292ecbe2ecee5f5254bec7655da4fc52575ae1 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 11 Oct 2012 22:26:59 +0200 Subject: [PATCH 03/14] Fixing ArcGISCache layer This is done by caching the tileOrigin at grid creation time. --- lib/OpenLayers/Layer/ArcGISCache.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/OpenLayers/Layer/ArcGISCache.js b/lib/OpenLayers/Layer/ArcGISCache.js index 27173392c4..e52ea645e6 100644 --- a/lib/OpenLayers/Layer/ArcGISCache.js +++ b/lib/OpenLayers/Layer/ArcGISCache.js @@ -358,6 +358,17 @@ OpenLayers.Layer.ArcGISCache = OpenLayers.Class(OpenLayers.Layer.XYZ, { return OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]); }, + /** + * Method: initGriddedTiles + * + * Parameters: + * bounds - {} + */ + initGriddedTiles: function(bounds) { + delete this._tileOrigin; + OpenLayers.Layer.XYZ.prototype.initGriddedTiles.apply(this, arguments); + }, + /** * Method: getMaxExtent * Get this layer's maximum extent. @@ -379,8 +390,11 @@ OpenLayers.Layer.ArcGISCache = OpenLayers.Class(OpenLayers.Layer.XYZ, { * {} The tile origin. */ getTileOrigin: function() { - var extent = this.getMaxExtent(); - return new OpenLayers.LonLat(extent.left, extent.bottom); + if (!this._tileOrigin) { + var extent = this.getMaxExtent(); + this._tileOrigin = new OpenLayers.LonLat(extent.left, extent.bottom); + } + return this._tileOrigin; }, /** From 7df5e3ca8ebb46b52ff6477260cf6d2c7f9ead31 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 11 Oct 2012 23:54:04 +0200 Subject: [PATCH 04/14] Fixing KaMap layer This also fixes an issue that has gone unnoticed for a while: the grid did not cover the bottom of the map viewport, but instead covered an invisible area above the top of the map viewport. --- lib/OpenLayers/Layer/KaMap.js | 32 ++++++++++++++++++++++++++------ tests/Layer/WrapDateLine.html | 6 +++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/OpenLayers/Layer/KaMap.js b/lib/OpenLayers/Layer/KaMap.js index 2abd9ea65f..2a7c77322a 100644 --- a/lib/OpenLayers/Layer/KaMap.js +++ b/lib/OpenLayers/Layer/KaMap.js @@ -92,8 +92,8 @@ OpenLayers.Layer.KaMap = OpenLayers.Class(OpenLayers.Layer.Grid, { * resolution - {Number} * * Returns: - * {Object} Object containing properties tilelon, tilelat, tileoffsetlat, - * tileoffsetlat, tileoffsetx, tileoffsety + * {Object} Object containing properties tilelon, tilelat, tileoffsetx, + * tileoffsety, startcol, startrow */ calculateGridLayout: function(bounds, origin, resolution) { var tilelon = resolution*this.tileSize.w; @@ -103,22 +103,42 @@ OpenLayers.Layer.KaMap = OpenLayers.Class(OpenLayers.Layer.Grid, { var tilecol = Math.floor(offsetlon/tilelon) - this.buffer; var tilecolremain = offsetlon/tilelon - tilecol; var tileoffsetx = -tilecolremain * this.tileSize.w; - var tileoffsetlon = tilecol * tilelon; var offsetlat = bounds.top; - var tilerow = Math.ceil(offsetlat/tilelat) + this.buffer; + var tilerow = Math.floor(offsetlat/tilelat) + this.buffer; var tilerowremain = tilerow - offsetlat/tilelat; var tileoffsety = -(tilerowremain+1) * this.tileSize.h; - var tileoffsetlat = tilerow * tilelat; return { tilelon: tilelon, tilelat: tilelat, - tileoffsetlon: tileoffsetlon, tileoffsetlat: tileoffsetlat, tileoffsetx: tileoffsetx, tileoffsety: tileoffsety, startcol: tilecol, startrow: tilerow }; }, + /** + * Method: getTileBoundsForGridIndex + * + * Parameters: + * row - {Number} The row of the grid + * col - {Number} The column of the grid + * + * Returns: + * {} The bounds for the tile at (row, col) + */ + getTileBoundsForGridIndex: function(row, col) { + var origin = this.getTileOrigin(); + var tileLayout = this.gridLayout; + var tilelon = tileLayout.tilelon; + var tilelat = tileLayout.tilelat; + var minX = (tileLayout.startcol + col) * tilelon; + var minY = (tileLayout.startrow - row) * tilelat; + return new OpenLayers.Bounds( + minX, minY, + minX + tilelon, minY + tilelat + ); + }, + /** * APIMethod: clone * diff --git a/tests/Layer/WrapDateLine.html b/tests/Layer/WrapDateLine.html index 700abf3f34..6b84362d59 100644 --- a/tests/Layer/WrapDateLine.html +++ b/tests/Layer/WrapDateLine.html @@ -154,9 +154,9 @@ var m = new OpenLayers.Map('map', {adjustZoom: function(z) {return z;}}); m.addLayer(layer); m.zoomToMaxExtent(); - t.eq(layer.grid[5][7].url, "http://www.openlayers.org/world/index.php?g=satellite&map=world&i=jpeg&t=0&l=-256&s=221471921.25", "grid[5][7] kamap is okay"); - t.eq(layer.grid[5][6].url, "http://www.openlayers.org/world/index.php?g=satellite&map=world&i=jpeg&t=0&l=0&s=221471921.25", "grid[5][6] kamap is okay"); - t.eq(layer.grid[5][5].url, "http://www.openlayers.org/world/index.php?g=satellite&map=world&i=jpeg&t=0&l=-256&s=221471921.25", "grid[5][5] is okay"); + t.eq(layer.grid[4][7].url, "http://www.openlayers.org/world/index.php?g=satellite&map=world&i=jpeg&t=0&l=-256&s=221471921.25", "grid[5][7] kamap is okay"); + t.eq(layer.grid[4][6].url, "http://www.openlayers.org/world/index.php?g=satellite&map=world&i=jpeg&t=0&l=0&s=221471921.25", "grid[5][6] kamap is okay"); + t.eq(layer.grid[4][5].url, "http://www.openlayers.org/world/index.php?g=satellite&map=world&i=jpeg&t=0&l=-256&s=221471921.25", "grid[5][5] is okay"); t.ok(layer.grid[7][6].url == null, "no latitudinal wrapping - tile not loaded if outside maxExtent"); m.destroy(); } From f78d127b1c43398401af082d90d188545f350436 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 11 Oct 2012 23:59:17 +0200 Subject: [PATCH 05/14] Removing console.log line --- tests/Layer/WMS.html | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Layer/WMS.html b/tests/Layer/WMS.html index 9577acea91..7f9f020676 100644 --- a/tests/Layer/WMS.html +++ b/tests/Layer/WMS.html @@ -552,7 +552,6 @@ map.pan(260, 520); map.pan(-260, -520); t.eq(layer.grid[0][0].bounds, bounds, "no floating point errors after dragging back and forth"); - console.log(bounds.toString()) map.destroy(); OpenLayers.Animation.isNative = isNative; From 66455600c7e5a45dd5bf7dfd8266c33603d5c253 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 12 Oct 2012 03:10:42 +0200 Subject: [PATCH 06/14] Better precision for right and top corners --- lib/OpenLayers/Layer/Grid.js | 10 ++++++---- tests/Layer/WMS.html | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index e0502fabb9..637c6c163e 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -949,12 +949,14 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var tileLayout = this.gridLayout; var tilelon = tileLayout.tilelon; var tilelat = tileLayout.tilelat; + var startcol = tileLayout.startcol; + var startrow = tileLayout.startrow; var rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; - var minX = origin.lon + (tileLayout.startcol + col) * tilelon; - var minY = origin.lat - (tileLayout.startrow + row * rowSign) * tilelat * rowSign; return new OpenLayers.Bounds( - minX, minY, - minX + tilelon, minY + tilelat + origin.lon + (startcol + col) * tilelon, + origin.lat - (startrow + row * rowSign) * tilelat * rowSign, + origin.lon + (startcol + col + 1) * tilelon, + origin.lat - (startrow + (row - 1) * rowSign) * tilelat * rowSign ); }, diff --git a/tests/Layer/WMS.html b/tests/Layer/WMS.html index 7f9f020676..cdc00705a3 100644 --- a/tests/Layer/WMS.html +++ b/tests/Layer/WMS.html @@ -538,7 +538,7 @@ // do not defer moveGriddedTiles var isNative = OpenLayers.Animation.isNative; OpenLayers.Animation.isNative = true; - t.plan(2); + t.plan(3); var map = new OpenLayers.Map("map", {projection: "EPSG:3857"}); var layer = new OpenLayers.Layer.WMS("wms", "../../img/blank.gif"); @@ -547,11 +547,12 @@ map.pan(2, -100); map.zoomIn(); t.eq(layer.grid[1][0].bounds, new OpenLayers.Bounds(-10018754.17, 0, 0, 10018754.17), "no floating point errors after zooming"); - map.zoomTo(14); + map.setCenter([0, 0], 14); var bounds = layer.grid[0][0].bounds.clone(); map.pan(260, 520); map.pan(-260, -520); t.eq(layer.grid[0][0].bounds, bounds, "no floating point errors after dragging back and forth"); + t.eq(bounds.right, 0, "0 is 0, and not some super small number"); map.destroy(); OpenLayers.Animation.isNative = isNative; From ff4a1b2468edb3184b1f03ea4e9fbd2f3b2107b3 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 12 Oct 2012 03:23:56 +0200 Subject: [PATCH 07/14] Optimizing positions for rendering Calculating pixel positions from origin and grid index causes alignment issues in the grid. By going back to incremental positioning, we get a result without blank spaces between tiles again. --- lib/OpenLayers/Layer/Grid.js | 54 ++++++++++++----------------------- lib/OpenLayers/Layer/KaMap.js | 9 ++---- lib/OpenLayers/Tile/Image.js | 4 +-- 3 files changed, 22 insertions(+), 45 deletions(-) diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index 637c6c163e..db5e288ec7 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -875,34 +875,23 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * resolution - {Number} * * Returns: - * {Object} Object containing properties tilelon, tilelat, tileoffsetx, - * tileoffsety, startcol, startrow + * {Object} Object containing properties tilelon, tilelat, startcol, + * startrow */ calculateGridLayout: function(bounds, origin, resolution) { var tilelon = resolution * this.tileSize.w; var tilelat = resolution * this.tileSize.h; - var ratio = resolution / this.map.getResolution(), - tileSize = { - w: Math.round(this.tileSize.w * ratio), - h: Math.round(this.tileSize.h * ratio) - }; - var offsetlon = bounds.left - origin.lon; var tilecol = Math.floor(offsetlon/tilelon) - this.buffer; - var tilecolremain = offsetlon/tilelon - tilecol; - var tileoffsetx = -tilecolremain * tileSize.w; var rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; var offsetlat = rowSign * (origin.lat - bounds.top + tilelat); var tilerow = Math[~rowSign ? 'floor' : 'ceil'](offsetlat/tilelat) - this.buffer * rowSign; - var tilerowremain = tilerow - offsetlat/tilelat; - var tileoffsety = rowSign * tilerowremain * tileSize.h; return { tilelon: tilelon, tilelat: tilelat, - tileoffsetx: tileoffsetx, tileoffsety: tileoffsety, startcol: tilecol, startrow: tilerow }; @@ -991,15 +980,19 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var tileLayout = this.calculateGridLayout(bounds, origin, serverResolution); this.gridLayout = tileLayout; - var startX = Math.round(tileLayout.tileoffsetx); // heaven help us - var startY = Math.round(tileLayout.tileoffsety); - var tilelon = tileLayout.tilelon; var tilelat = tileLayout.tilelat; var layerContainerDivLeft = this.map.layerContainerOriginPx.x; var layerContainerDivTop = this.map.layerContainerOriginPx.y; + var tileBounds = this.getTileBoundsForGridIndex(0, 0); + var startPx = this.map.getViewPortPxFromLonLat( + new OpenLayers.LonLat(tileBounds.left, tileBounds.top) + ); + startPx.x = Math.round(startPx.x) - layerContainerDivLeft; + startPx.y = Math.round(startPx.y) - layerContainerDivTop; + var tileData = [], center = this.map.getCenter(); var rowidx = 0; @@ -1012,10 +1005,10 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var colidx = 0; do { - var tileBounds = this.getTileBoundsForGridIndex(rowidx, colidx); - var x = startX + colidx * tileSize.w - layerContainerDivLeft; - var y = startY + rowidx * tileSize.h - layerContainerDivTop; - var px = new OpenLayers.Pixel(x, y); + tileBounds = this.getTileBoundsForGridIndex(rowidx, colidx); + var px = startPx.clone(); + px.x = px.x + colidx * Math.round(tileSize.w); + px.y = px.y + rowidx * Math.round(tileSize.h); var tile = row[colidx]; if (!tile) { tile = this.addTile(tileBounds, px); @@ -1219,17 +1212,12 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var tileLayout = this.gridLayout; tileLayout.startrow += sign * rowSign; - var bounds = this.getTileBoundsForGridIndex(rowIndex, 0); - var position = this.map.getViewPortPxFromLonLat( - new OpenLayers.LonLat(bounds.left, bounds.top) - ); - var y = Math.round(position.y - this.map.layerContainerOriginPx.y); - + var modelRow = grid[rowIndex]; var row = grid[prepend ? 'pop' : 'shift'](); for (var i=0, len=row.length; i Date: Fri, 12 Oct 2012 13:41:54 +0200 Subject: [PATCH 08/14] Register application listener after the control's --- examples/cache-write.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/cache-write.js b/examples/cache-write.js index 8f4ec9e081..e9db31a326 100644 --- a/examples/cache-write.js +++ b/examples/cache-write.js @@ -8,13 +8,10 @@ function init() { div: "map", projection: "EPSG:900913", layers: [ - new OpenLayers.Layer.WMS("OSGeo", "http://vmap0.tiles.osgeo.org/wms/vmap0", { - layers: "basic" - }, { - eventListeners: { - tileloaded: updateStatus - } - }) + new OpenLayers.Layer.WMS( + "OSGeo", "http://vmap0.tiles.osgeo.org/wms/vmap0", + {layers: "basic"} + ) ], center: [0, 0], zoom: 1 @@ -38,6 +35,7 @@ function init() { }; // update the number of cached tiles and detect local storage support + map.layers[0].events.on({'tileloaded': updateStatus}); function updateStatus() { if (window.localStorage) { status.innerHTML = localStorage.length + " entries in cache."; From 6607bcc0bbc2032bc6196e5b4090a16cdfeb8837 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 12 Oct 2012 14:06:08 +0200 Subject: [PATCH 09/14] Do not cache data from aborted tile loads This also results in a simplified cache method that can more easily be overridden for use with other storage providers. --- lib/OpenLayers/Control/CacheWrite.js | 60 +++++++++++++++++----------- lib/OpenLayers/Layer/Grid.js | 12 ++++-- tests/Control/CacheWrite.html | 5 ++- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/lib/OpenLayers/Control/CacheWrite.js b/lib/OpenLayers/Control/CacheWrite.js index 8b4e787e2f..455e5674ab 100644 --- a/lib/OpenLayers/Control/CacheWrite.js +++ b/lib/OpenLayers/Control/CacheWrite.js @@ -111,7 +111,7 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { addLayer: function(evt) { evt.layer.events.on({ tileloadstart: this.makeSameOrigin, - tileloaded: this.cache, + tileloaded: this.onTileloaded, scope: this }); }, @@ -128,7 +128,7 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { removeLayer: function(evt) { evt.layer.events.un({ tileloadstart: this.makeSameOrigin, - tileloaded: this.cache, + tileloaded: this.onTileloaded, scope: this }); }, @@ -156,6 +156,22 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { } }, + /** + * Method: onTileloaded + * Decides whether a tile can be cached and calls the cache method. + * + * Parameters: + * evt - {Event} + */ + onTileloaded: function(evt) { + if (this.active && !evt.aborted && + evt.tile instanceof OpenLayers.Tile.Image && + evt.tile.url.substr(0, 5) !== 'data:') { + this.cache({tile: evt.tile}); + delete OpenLayers.Control.CacheWrite.urlMap[evt.tile.url]; + } + }, + /** * Method: cache * Adds a tile to the cache. When the cache is full, the "cachefull" event @@ -166,29 +182,25 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { * with the data to add to the cache */ cache: function(obj) { - if (this.active && window.localStorage) { + if (window.localStorage) { var tile = obj.tile; - if (tile instanceof OpenLayers.Tile.Image && - tile.url.substr(0, 5) !== 'data:') { - try { - var canvasContext = tile.getCanvasContext(); - if (canvasContext) { - var urlMap = OpenLayers.Control.CacheWrite.urlMap; - var url = urlMap[tile.url] || tile.url; - window.localStorage.setItem( - "olCache_" + url, - canvasContext.canvas.toDataURL(this.imageFormat) - ); - delete urlMap[tile.url]; - } - } catch(e) { - // local storage full or CORS violation - var reason = e.name || e.message; - if (reason && this.quotaRegEx.test(reason)) { - this.events.triggerEvent("cachefull", {tile: tile}); - } else { - OpenLayers.Console.error(e.toString()); - } + try { + var canvasContext = tile.getCanvasContext(); + if (canvasContext) { + var urlMap = OpenLayers.Control.CacheWrite.urlMap; + var url = urlMap[tile.url] || tile.url; + window.localStorage.setItem( + "olCache_" + url, + canvasContext.canvas.toDataURL(this.imageFormat) + ); + } + } catch(e) { + // local storage full or CORS violation + var reason = e.name || e.message; + if (reason && this.quotaRegEx.test(reason)) { + this.events.triggerEvent("cachefull", {tile: tile}); + } else { + OpenLayers.Console.error(e.toString()); } } } diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index fcb2b42e64..df6535427d 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -262,8 +262,9 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * loaded, as a means of progress update to listeners. * listeners can access 'numLoadingTiles' if they wish to keep * track of the loading progress. Listeners are called with an object - * with a tile property as first argument, making the loded tile - * available to the listener. + * with a 'tile' property as first argument, making the loded tile + * available to the listener, and an 'aborted' property, which will be + * true when loading was aborted and no tile data is available. * tileerror - Triggered before the tileloaded event (i.e. when the tile is * still hidden) if a tile failed to load. Listeners receive an object * as first argument, which has a tile property that references the @@ -1090,9 +1091,12 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { this.numLoadingTiles++; }; - tile.onLoadEnd = function() { + tile.onLoadEnd = function(evt) { this.numLoadingTiles--; - this.events.triggerEvent("tileloaded", {tile: tile}); + this.events.triggerEvent("tileloaded", { + tile: tile, + aborted: evt.type === "unload" + }); //if that was the last tile, then trigger a 'loadend' on the layer if (this.tileQueue.length === 0 && this.numLoadingTiles === 0) { this.loading = false; diff --git a/tests/Control/CacheWrite.html b/tests/Control/CacheWrite.html index fdb6cabeaf..9922569332 100644 --- a/tests/Control/CacheWrite.html +++ b/tests/Control/CacheWrite.html @@ -40,7 +40,7 @@ return; } - t.plan(3); + t.plan(4); OpenLayers.Control.CacheWrite.clearCache(); var length = window.localStorage.length; @@ -68,6 +68,9 @@ // content will be null for browsers that have localStorage but no canvas support var content = canvasContext ? canvasContext.canvas.toDataURL("image/png") : null; t.eq(window.localStorage.getItem("olCache_"+url), content, "localStorage contains correct image data"); + + layer.events.triggerEvent('tileloaded', {aborted: true, tile: layer.grid[1][1]}); + t.eq(window.localStorage.length, length + (canvasContext ? tiles-1 : 0), "tile aborted during load not cached"); var key = Math.random(); window.localStorage.setItem(key, "bar"); From 157dd9e1c22d1543ca226ed672f418ed87acae28 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 12 Oct 2012 14:44:48 +0200 Subject: [PATCH 10/14] Fixing typo --- lib/OpenLayers/Layer/Grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index df6535427d..de12b76396 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -262,7 +262,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * loaded, as a means of progress update to listeners. * listeners can access 'numLoadingTiles' if they wish to keep * track of the loading progress. Listeners are called with an object - * with a 'tile' property as first argument, making the loded tile + * with a 'tile' property as first argument, making the loaded tile * available to the listener, and an 'aborted' property, which will be * true when loading was aborted and no tile data is available. * tileerror - Triggered before the tileloaded event (i.e. when the tile is From e5feda7ad776f3b45d6ec54198be181c84f158b4 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 12 Oct 2012 15:03:38 +0200 Subject: [PATCH 11/14] Camel casing method name --- lib/OpenLayers/Control/CacheWrite.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/OpenLayers/Control/CacheWrite.js b/lib/OpenLayers/Control/CacheWrite.js index 455e5674ab..fdd8afce68 100644 --- a/lib/OpenLayers/Control/CacheWrite.js +++ b/lib/OpenLayers/Control/CacheWrite.js @@ -111,7 +111,7 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { addLayer: function(evt) { evt.layer.events.on({ tileloadstart: this.makeSameOrigin, - tileloaded: this.onTileloaded, + tileloaded: this.onTileLoaded, scope: this }); }, @@ -128,7 +128,7 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { removeLayer: function(evt) { evt.layer.events.un({ tileloadstart: this.makeSameOrigin, - tileloaded: this.onTileloaded, + tileloaded: this.onTileLoaded, scope: this }); }, @@ -157,13 +157,13 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { }, /** - * Method: onTileloaded + * Method: onTileLoaded * Decides whether a tile can be cached and calls the cache method. * * Parameters: * evt - {Event} */ - onTileloaded: function(evt) { + onTileLoaded: function(evt) { if (this.active && !evt.aborted && evt.tile instanceof OpenLayers.Tile.Image && evt.tile.url.substr(0, 5) !== 'data:') { From 0eb8949ad23d801af9172c3cf5b87cf0895ebb1f Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 12 Oct 2012 16:11:10 +0200 Subject: [PATCH 12/14] Updating and fixing API docs --- lib/OpenLayers/Layer/Grid.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index db5e288ec7..d77aaa643f 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -272,9 +272,8 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { /** * Property: gridLayout - * {Object} containing properties tilelon, tilelat, tileoffsetlat, - * tileoffsetlat, tileoffsetx, tileoffsety and optional startrow, startcol - * for grid layouts where absolute tile bounds calculation is possible. + * {Object} Object containing properties tilelon, tilelat, startcol, + * startrow */ gridLayout: null, From a0acf1e550287974a81e626af4cac575950eda94 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 12 Oct 2012 16:16:44 +0200 Subject: [PATCH 13/14] Calculating rowSign only once --- lib/OpenLayers/Layer/Grid.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/OpenLayers/Layer/Grid.js b/lib/OpenLayers/Layer/Grid.js index d77aaa643f..ba6af27c49 100644 --- a/lib/OpenLayers/Layer/Grid.js +++ b/lib/OpenLayers/Layer/Grid.js @@ -276,6 +276,13 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { * startrow */ gridLayout: null, + + /** + * Property: rowSign + * {Number} 1 for grids starting at the top, -1 for grids starting at the + * bottom. This is used for several grid index and offset calculations. + */ + rowSign: null, /** * Constructor: OpenLayers.Layer.Grid @@ -308,6 +315,8 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { this.moveTimerId = null; }, this); } + + this.rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; }, /** @@ -884,7 +893,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var offsetlon = bounds.left - origin.lon; var tilecol = Math.floor(offsetlon/tilelon) - this.buffer; - var rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; + var rowSign = this.rowSign; var offsetlat = rowSign * (origin.lat - bounds.top + tilelat); var tilerow = Math[~rowSign ? 'floor' : 'ceil'](offsetlat/tilelat) - this.buffer * rowSign; @@ -939,7 +948,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var tilelat = tileLayout.tilelat; var startcol = tileLayout.startcol; var startrow = tileLayout.startrow; - var rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; + var rowSign = this.rowSign; return new OpenLayers.Bounds( origin.lon + (startcol + col) * tilelon, origin.lat - (startrow + row * rowSign) * tilelat * rowSign, @@ -1207,7 +1216,7 @@ OpenLayers.Layer.Grid = OpenLayers.Class(OpenLayers.Layer.HTTPRequest, { var grid = this.grid; var rowIndex = prepend ? 0 : (grid.length - 1); var sign = prepend ? -1 : 1; - var rowSign = this.tileOriginCorner.substr(0, 1) === "t" ? 1 : -1; + var rowSign = this.rowSign; var tileLayout = this.gridLayout; tileLayout.startrow += sign * rowSign; From e38ab01752372bbff4f946dcebdba03ceb71dce1 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Sat, 13 Oct 2012 09:26:45 +0200 Subject: [PATCH 14/14] Fixing image-layer example --- examples/data/4_m_citylights_lg.gif | Bin 0 -> 19000 bytes examples/image-layer.html | 15 ++++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 examples/data/4_m_citylights_lg.gif diff --git a/examples/data/4_m_citylights_lg.gif b/examples/data/4_m_citylights_lg.gif new file mode 100644 index 0000000000000000000000000000000000000000..4bf9b87f698fb0d6105a62ba4390f93a0b932332 GIT binary patch literal 19000 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

S1c9*kAC|k_`^?Mc({=>t45BKW10U{?2nB-2~ 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