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);