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"
+---
+
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);
+});
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/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();
};
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);