From 132861e1751902aff43a591d6f3dc883c64b8575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 6 Nov 2015 16:50:57 +0100 Subject: [PATCH 1/3] Use interim tiles when changing dynamic parameters --- src/ol/imagetile.js | 3 + .../canvas/canvastilelayerrenderer.js | 26 ++++-- src/ol/renderer/dom/domtilelayerrenderer.js | 12 ++- .../renderer/webgl/webgltilelayerrenderer.js | 11 ++- src/ol/source/tileimagesource.js | 85 ++++++++++++++----- src/ol/source/tilesource.js | 11 +++ src/ol/structs/lrucache.js | 10 +++ src/ol/tile.js | 16 ++++ test/spec/ol/source/tileimagesource.test.js | 69 +++++++++++++++ test/spec/ol/structs/lrucache.test.js | 10 +++ 10 files changed, 222 insertions(+), 31 deletions(-) diff --git a/src/ol/imagetile.js b/src/ol/imagetile.js index f1e267f2b0..131be58b27 100644 --- a/src/ol/imagetile.js +++ b/src/ol/imagetile.js @@ -70,6 +70,9 @@ ol.ImageTile.prototype.disposeInternal = function() { if (this.state == ol.TileState.LOADING) { this.unlistenImage_(); } + if (this.interimTile) { + goog.dispose(this.interimTile); + } goog.base(this, 'disposeInternal'); }; diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js index 14ada48772..94ab39f092 100644 --- a/src/ol/renderer/canvas/canvastilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -316,19 +316,29 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = var tmpExtent = ol.extent.createEmpty(); var tmpTileRange = new ol.TileRange(0, 0, 0, 0); - var childTileRange, fullyLoaded, tile, tileState, x, y; + var childTileRange, fullyLoaded, tile, x, y; + var drawableTile = ( + /** + * @param {!ol.Tile} tile Tile. + * @return {boolean} Tile is selected. + */ + function(tile) { + var tileState = tile.getState(); + return tileState == ol.TileState.LOADED || + tileState == ol.TileState.EMPTY || + tileState == ol.TileState.ERROR && !useInterimTilesOnError; + }); for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) { - tile = tileSource.getTile(z, x, y, pixelRatio, projection); - tileState = tile.getState(); - if (tileState == ol.TileState.LOADED || - tileState == ol.TileState.EMPTY || - (tileState == ol.TileState.ERROR && !useInterimTilesOnError)) { + if (!drawableTile(tile) && tile.interimTile) { + tile = tile.interimTile; + } + goog.asserts.assert(tile); + if (drawableTile(tile)) { tilesToDrawByZ[z][ol.tilecoord.toString(tile.tileCoord)] = tile; continue; } - fullyLoaded = tileGrid.forEachTileCoordParentTileRange( tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); if (!fullyLoaded) { @@ -360,7 +370,7 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = var origin = ol.extent.getTopLeft(tileGrid.getTileCoordExtent( [z, canvasTileRange.minX, canvasTileRange.maxY], tmpExtent)); - var currentZ, index, scale, tileCoordKey, tileExtent, tilesToDraw; + var currentZ, index, scale, tileCoordKey, tileExtent, tileState, tilesToDraw; var ix, iy, interimTileRange, maxX, maxY; var height, width; for (i = 0, ii = zs.length; i < ii; ++i) { diff --git a/src/ol/renderer/dom/domtilelayerrenderer.js b/src/ol/renderer/dom/domtilelayerrenderer.js index d9a66b5f7e..c5429a3bb8 100644 --- a/src/ol/renderer/dom/domtilelayerrenderer.js +++ b/src/ol/renderer/dom/domtilelayerrenderer.js @@ -128,12 +128,19 @@ ol.renderer.dom.TileLayer.prototype.prepareFrame = var tmpExtent = ol.extent.createEmpty(); var tmpTileRange = new ol.TileRange(0, 0, 0, 0); - var childTileRange, fullyLoaded, tile, tileState, x, y; + var childTileRange, drawable, fullyLoaded, tile, tileState, x, y; for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) { - tile = tileSource.getTile(z, x, y, pixelRatio, projection); tileState = tile.getState(); + drawable = tileState == ol.TileState.LOADED || + tileState == ol.TileState.EMPTY || + tileState == ol.TileState.ERROR && !useInterimTilesOnError; + if (!drawable && tile.interimTile) { + tile = tile.interimTile; + } + goog.asserts.assert(tile); + tileState = tile.getState(); if (tileState == ol.TileState.LOADED) { tilesToDrawByZ[z][ol.tilecoord.toString(tile.tileCoord)] = tile; continue; @@ -142,7 +149,6 @@ ol.renderer.dom.TileLayer.prototype.prepareFrame = !useInterimTilesOnError)) { continue; } - fullyLoaded = tileGrid.forEachTileCoordParentTileRange( tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); if (!fullyLoaded) { diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index 675aac21ae..84f7f8c530 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -247,7 +247,8 @@ ol.renderer.webgl.TileLayer.prototype.prepareFrame = var allTilesLoaded = true; var tmpExtent = ol.extent.createEmpty(); var tmpTileRange = new ol.TileRange(0, 0, 0, 0); - var childTileRange, fullyLoaded, tile, tileState, x, y, tileExtent; + var childTileRange, drawable, fullyLoaded, tile, tileState; + var x, y, tileExtent; for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) { @@ -260,6 +261,14 @@ ol.renderer.webgl.TileLayer.prototype.prepareFrame = } } tileState = tile.getState(); + drawable = tileState == ol.TileState.LOADED || + tileState == ol.TileState.EMPTY || + tileState == ol.TileState.ERROR && !useInterimTilesOnError; + if (!drawable && tile.interimTile) { + tile = tile.interimTile; + } + goog.asserts.assert(tile); + tileState = tile.getState(); if (tileState == ol.TileState.LOADED) { if (mapRenderer.isTileTextureLoaded(tile)) { tilesToDrawByZ[z][ol.tilecoord.toString(tile.tileCoord)] = tile; diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 6622a0b762..3b77c6b542 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -162,6 +162,36 @@ ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { }; +/** + * @param {number} z Tile coordinate z. + * @param {number} x Tile coordinate x. + * @param {number} y Tile coordinate y. + * @param {number} pixelRatio Pixel ratio. + * @param {ol.proj.Projection} projection Projection. + * @param {string} key The key set on the tile. + * @return {ol.Tile} Tile. + * @private + */ +ol.source.TileImage.prototype.createTile_ = + function(z, x, y, pixelRatio, projection, key) { + var tileCoord = [z, x, y]; + var urlTileCoord = this.getTileCoordForTileUrlFunction( + tileCoord, projection); + var tileUrl = urlTileCoord ? + this.tileUrlFunction(urlTileCoord, pixelRatio, projection) : undefined; + var tile = new this.tileClass( + tileCoord, + tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY, + tileUrl !== undefined ? tileUrl : '', + this.crossOrigin, + this.tileLoadFunction); + tile.key = key; + goog.events.listen(tile, goog.events.EventType.CHANGE, + this.handleTileChange, false, this); + return tile; +}; + + /** * @inheritDoc */ @@ -176,7 +206,7 @@ ol.source.TileImage.prototype.getTile = var cache = this.getTileCacheForProjection(projection); var tileCoordKey = this.getKeyZXY(z, x, y); if (cache.containsKey(tileCoordKey)) { - return /** @type {!ol.Tile} */(cache.get(tileCoordKey)); + return /** @type {!ol.Tile} */ (cache.get(tileCoordKey)); } else { var sourceProjection = this.getProjection(); var sourceTileGrid = this.getTileGridForProjection(sourceProjection); @@ -208,28 +238,45 @@ ol.source.TileImage.prototype.getTile = */ ol.source.TileImage.prototype.getTileInternal = function(z, x, y, pixelRatio, projection) { + var /** @type {ol.Tile} */ tile = null; var tileCoordKey = this.getKeyZXY(z, x, y); - if (this.tileCache.containsKey(tileCoordKey)) { - return /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey)); - } else { + var paramsKey = this.getKeyParams(); + if (!this.tileCache.containsKey(tileCoordKey)) { goog.asserts.assert(projection, 'argument projection is truthy'); - var tileCoord = [z, x, y]; - var urlTileCoord = this.getTileCoordForTileUrlFunction( - tileCoord, projection); - var tileUrl = !urlTileCoord ? undefined : - this.tileUrlFunction(urlTileCoord, pixelRatio, projection); - var tile = new this.tileClass( - tileCoord, - tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY, - tileUrl !== undefined ? tileUrl : '', - this.crossOrigin, - this.tileLoadFunction); - goog.events.listen(tile, goog.events.EventType.CHANGE, - this.handleTileChange, false, this); - + tile = this.createTile_(z, x, y, pixelRatio, projection, paramsKey); this.tileCache.set(tileCoordKey, tile); - return tile; + } else { + tile = /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey)); + if (tile.key != paramsKey) { + // The source's params changed. If the tile has an interim tile and if we + // can use it then we use it. Otherwise we create a new tile. In both + // cases we attempt to assign an interim tile to the new tile. + var /** @type {ol.Tile} */ interimTile = tile; + if (tile.interimTile && tile.interimTile.key == paramsKey) { + goog.asserts.assert(tile.interimTile.getState() == ol.TileState.LOADED); + goog.asserts.assert(tile.interimTile.interimTile === null); + tile = tile.interimTile; + if (interimTile.getState() == ol.TileState.LOADED) { + tile.interimTile = interimTile; + } + } else { + tile = this.createTile_(z, x, y, pixelRatio, projection, paramsKey); + if (interimTile.getState() == ol.TileState.LOADED) { + tile.interimTile = interimTile; + } else if (interimTile.interimTile && + interimTile.interimTile.getState() == ol.TileState.LOADED) { + tile.interimTile = interimTile.interimTile; + interimTile.interimTile = null; + } + } + if (tile.interimTile) { + tile.interimTile.interimTile = null; + } + this.tileCache.replace(tileCoordKey, tile); + } } + goog.asserts.assert(tile); + return tile; }; diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index 648a77b564..ad32b743ce 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -156,6 +156,17 @@ ol.source.Tile.prototype.getGutter = function() { }; +/** + * Return the "parameters" key, a string composed of the source's + * parameters/dimensions. + * @return {string} The parameters key. + * @protected + */ +ol.source.Tile.prototype.getKeyParams = function() { + return ''; +}; + + /** * @param {number} z Z. * @param {number} x X. diff --git a/src/ol/structs/lrucache.js b/src/ol/structs/lrucache.js index cf02b66ed7..0034c9c1d0 100644 --- a/src/ol/structs/lrucache.js +++ b/src/ol/structs/lrucache.js @@ -226,6 +226,16 @@ ol.structs.LRUCache.prototype.pop = function() { }; +/** + * @param {string} key Key. + * @param {T} value Value. + */ +ol.structs.LRUCache.prototype.replace = function(key, value) { + this.get(key); // update `newest_` + this.entries_[key].value_ = value; +}; + + /** * @param {string} key Key. * @param {T} value Value. diff --git a/src/ol/tile.js b/src/ol/tile.js index cb3c8528df..7f5d02759b 100644 --- a/src/ol/tile.js +++ b/src/ol/tile.js @@ -44,6 +44,22 @@ ol.Tile = function(tileCoord, state) { */ this.state = state; + /** + * An "interim" tile for this tile. The interim tile may be used while this + * one is loading, for "smooth" transitions when changing params/dimensions + * on the source. + * @type {ol.Tile} + */ + this.interimTile = null; + + /** + * A key assigned to the tile. This is used by the tile source to determine + * if this tile can effectively be used, or if a new tile should be created + * and this one be used as an interim tile for this new tile. + * @type {string} + */ + this.key = ''; + }; goog.inherits(ol.Tile, goog.events.EventTarget); diff --git a/test/spec/ol/source/tileimagesource.test.js b/test/spec/ol/source/tileimagesource.test.js index 2b90a23717..75e4bb1601 100644 --- a/test/spec/ol/source/tileimagesource.test.js +++ b/test/spec/ol/source/tileimagesource.test.js @@ -22,6 +22,75 @@ describe('ol.source.TileImage', function() { }); }); + describe('#getTileInternal', function() { + var source, tile; + + beforeEach(function() { + source = createSource(); + expect(source.getKeyParams()).to.be(''); + source.getTileInternal(0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(source.tileCache.getCount()).to.be(1); + tile = source.tileCache.get(source.getKeyZXY(0, 0, -1)); + }); + + it('gets the tile from the cache', function() { + var returnedTile = source.getTileInternal( + 0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(returnedTile).to.be(tile); + }); + + describe('change a dynamic param', function() { + + describe('tile is not loaded', function() { + it('returns a tile with no interim tile', function() { + source.getKeyParams = function() { + return 'key0'; + }; + var returnedTile = source.getTileInternal( + 0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(returnedTile).not.to.be(tile); + expect(returnedTile.key).to.be('key0'); + expect(returnedTile.interimTile).to.be(null); + }); + }); + + describe('tile is loaded', function() { + it('returns a tile with interim tile', function() { + source.getKeyParams = function() { + return 'key0'; + }; + tile.state = ol.TileState.LOADED; + var returnedTile = source.getTileInternal( + 0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(returnedTile).not.to.be(tile); + expect(returnedTile.key).to.be('key0'); + expect(returnedTile.interimTile).to.be(tile); + }); + }); + + describe('tile is not loaded but interim tile is', function() { + it('returns a tile with interim tile', function() { + var dynamicParamsKey, returnedTile; + source.getKeyParams = function() { + return dynamicParamsKey; + }; + dynamicParamsKey = 'key0'; + tile.state = ol.TileState.LOADED; + returnedTile = source.getTileInternal( + 0, 0, -1, 1, ol.proj.get('EPSG:3857')); + dynamicParamsKey = 'key1'; + returnedTile = source.getTileInternal( + 0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(returnedTile).not.to.be(tile); + expect(returnedTile.key).to.be('key1'); + expect(returnedTile.interimTile).to.be(tile); + }); + }); + + }); + + }); + describe('#getTile', function() { it('does not do reprojection for identity', function() { var source3857 = createSource('EPSG:3857'); diff --git a/test/spec/ol/structs/lrucache.test.js b/test/spec/ol/structs/lrucache.test.js index 7c5e29a2b8..36f6cc3dca 100644 --- a/test/spec/ol/structs/lrucache.test.js +++ b/test/spec/ol/structs/lrucache.test.js @@ -81,6 +81,16 @@ describe('ol.structs.LRUCache', function() { }); }); + describe('replacing value of a key', function() { + it('moves the key to newest position', function() { + fillLRUCache(lruCache); + lruCache.replace('b', 4); + expect(lruCache.getCount()).to.eql(4); + expect(lruCache.getKeys()).to.eql(['b', 'd', 'c', 'a']); + expect(lruCache.getValues()).to.eql([4, 3, 2, 0]); + }); + }); + describe('setting a new value', function() { it('adds it as the newest value', function() { fillLRUCache(lruCache); From 0f70a21ba672a81f75b11d09d6aa9fd6a51a237f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Thu, 12 Nov 2015 10:54:45 +0100 Subject: [PATCH 2/3] Implement getKeyParams in ol.source.WMTS --- src/ol/source/wmtssource.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ol/source/wmtssource.js b/src/ol/source/wmtssource.js index 14483b02d5..26a8628022 100644 --- a/src/ol/source/wmtssource.js +++ b/src/ol/source/wmtssource.js @@ -61,8 +61,8 @@ ol.source.WMTS = function(options) { * @private * @type {string} */ - this.coordKeyPrefix_ = ''; - this.resetCoordKeyPrefix_(); + this.dimensionsKey_ = ''; + this.resetDimensionsKey_(); /** * @private @@ -218,8 +218,8 @@ ol.source.WMTS.prototype.getFormat = function() { /** * @inheritDoc */ -ol.source.WMTS.prototype.getKeyZXY = function(z, x, y) { - return this.coordKeyPrefix_ + goog.base(this, 'getKeyZXY', z, x, y); +ol.source.WMTS.prototype.getKeyParams = function() { + return this.dimensionsKey_; }; @@ -276,13 +276,13 @@ ol.source.WMTS.prototype.getVersion = function() { /** * @private */ -ol.source.WMTS.prototype.resetCoordKeyPrefix_ = function() { +ol.source.WMTS.prototype.resetDimensionsKey_ = function() { var i = 0; var res = []; for (var key in this.dimensions_) { res[i++] = key + '-' + this.dimensions_[key]; } - this.coordKeyPrefix_ = res.join('/'); + this.dimensionsKey_ = res.join('/'); }; @@ -293,7 +293,7 @@ ol.source.WMTS.prototype.resetCoordKeyPrefix_ = function() { */ ol.source.WMTS.prototype.updateDimensions = function(dimensions) { goog.object.extend(this.dimensions_, dimensions); - this.resetCoordKeyPrefix_(); + this.resetDimensionsKey_(); this.changed(); }; From c3c8c4fa73db94c15ab905bf4ad711dff495c649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Fri, 6 Nov 2015 16:53:03 +0100 Subject: [PATCH 3/3] Add an wmts-dimensions example --- examples/wmts-dimensions.html | 18 ++++++++ examples/wmts-dimensions.js | 85 +++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 examples/wmts-dimensions.html create mode 100644 examples/wmts-dimensions.js diff --git a/examples/wmts-dimensions.html b/examples/wmts-dimensions.html new file mode 100644 index 0000000000..abe64bc378 --- /dev/null +++ b/examples/wmts-dimensions.html @@ -0,0 +1,18 @@ +--- +layout: example.html +title: Smooth WMTS tile transition example +shortdesc: Example of smooth tile transitions when changing the dimension of a WMTS layer. +docs: > + Demonstrates smooth reloading of layers when changing a dimension continously. The demonstration layer is a global sea-level computation (flooding computation from SCALGO, underlying data from CGIAR-CSI SRTM) where cells that are flooded if the sea-level rises to more than x m are colored blue. The user selects the sea-level dimension using a slider. +tags: "wmts, parameter, transition" +--- +
+
+
+
+
+ Sea-level: + + +
+
diff --git a/examples/wmts-dimensions.js b/examples/wmts-dimensions.js new file mode 100644 index 0000000000..042f19a469 --- /dev/null +++ b/examples/wmts-dimensions.js @@ -0,0 +1,85 @@ +goog.require('ol.Attribution'); +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.control'); +goog.require('ol.control.Attribution'); +goog.require('ol.control.Zoom'); +goog.require('ol.extent'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.source.OSM'); +goog.require('ol.source.WMTS'); +goog.require('ol.tilegrid.WMTS'); + + +// create the WMTS tile grid in the google projection +var projection = ol.proj.get('EPSG:3857'); +var tileSizePixels = 256; +var tileSizeMtrs = ol.extent.getWidth(projection.getExtent()) / tileSizePixels; +var matrixIds = []; +var resolutions = []; +for (var i = 0; i <= 14; i++) { + matrixIds[i] = i; + resolutions[i] = tileSizeMtrs / Math.pow(2, i); +} +var tileGrid = new ol.tilegrid.WMTS({ + origin: ol.extent.getTopLeft(projection.getExtent()), + resolutions: resolutions, + matrixIds: matrixIds +}); + +var scalgoToken = 'CC5BF28A7D96B320C7DFBFD1236B5BEB'; + +var wmtsSource = new ol.source.WMTS({ + url: 'http://ts2.scalgo.com/global/wmts?token=' + scalgoToken, + layer: 'hydrosheds:sea-levels', + format: 'image/png', + matrixSet: 'EPSG:3857', + attributions: [ + new ol.Attribution({ + html: 'SCALGO' + }), + new ol.Attribution({ + html: 'CGIAR-CSI SRTM' + }) + ], + tileGrid: tileGrid, + style: 'default', + dimensions: { + 'threshold': 100 + } +}); + +var map = new ol.Map({ + target: 'map', + view: new ol.View({ + projection: projection, + center: [-3052589, 3541786], + zoom: 3 + }), + controls: [ + new ol.control.Zoom(), + new ol.control.Attribution() + ], + layers: [ + new ol.layer.Tile({ + source: new ol.source.OSM() + }), + new ol.layer.Tile({ + opacity: 0.5, + source: wmtsSource + }) + ] +}); + +var updateSourceDimension = function(source, sliderVal) { + source.updateDimensions({'threshold': sliderVal}); + document.getElementById('theinfo').innerHTML = sliderVal + ' meters.'; +}; + +updateSourceDimension(wmtsSource, 10); + +document.getElementById('slider').addEventListener('input', function() { + updateSourceDimension(wmtsSource, this.value); +});