diff --git a/src/ol/ImageTile.js b/src/ol/ImageTile.js index a2fce7f973..621d45ac16 100644 --- a/src/ol/ImageTile.js +++ b/src/ol/ImageTile.js @@ -112,7 +112,40 @@ class ImageTile extends Tile { } /** - * Load not yet loaded URI. + * Load the image or retry if loading previously failed. + * Loading is taken care of by the tile queue, and calling this method is + * only needed for preloading or for reloading in case of an error. + * + * To retry loading tiles on failed requests, use a custom `tileLoadFunction` + * that checks for error status codes and reloads only when the status code is + * 408, 429, 500, 502, 503 and 504, and only when not too many retries have been + * made already: + * + * ```js + * const retryCodes = [408, 429, 500, 502, 503, 504]; + * const retries = {}; + * source.setTileLoadFunction((tile, src) => { + * const image = tile.getImage(); + * fetch(src) + * .then((response) => { + * if (retryCodes.includes(response.status)) { + * retries[src] = (retries[src] || 0) + 1; + * if (retries[src] <= 3) { + * setTimeout(() => tile.load(), retries[src] * 1000); + * } + * return Promise.reject(); + * } + * return response.blob(); + * }) + * .then((blob) => { + * const imageUrl = URL.createObjectURL(blob); + * image.src = imageUrl; + * setTimeout(() => URL.revokeObjectURL(imageUrl), 5000); + * }) + * .catch(() => tile.setState(3)); // error + * }); + * ``` + * * @api */ load() { diff --git a/src/ol/Tile.js b/src/ol/Tile.js index 22ee857a53..71510ee7fe 100644 --- a/src/ol/Tile.js +++ b/src/ol/Tile.js @@ -142,7 +142,12 @@ class Tile extends EventTarget { /** * Called by the tile cache when the tile is removed from the cache due to expiry */ - release() {} + release() { + if (this.state === TileState.ERROR) { + // to remove the `change` listener on this tile in `ol/TileQueue#handleTileChange` + this.setState(TileState.EMPTY); + } + } /** * @return {string} Key. diff --git a/src/ol/TileCache.js b/src/ol/TileCache.js index 5a00af4534..d6ac1f02d5 100644 --- a/src/ol/TileCache.js +++ b/src/ol/TileCache.js @@ -5,6 +5,13 @@ import LRUCache from './structs/LRUCache.js'; import {fromKey, getKey} from './tilecoord.js'; class TileCache extends LRUCache { + clear() { + while (this.getCount() > 0) { + this.pop().release(); + } + super.clear(); + } + /** * @param {!Object} usedTiles Used tiles. */ diff --git a/src/ol/TileQueue.js b/src/ol/TileQueue.js index ab34b428fc..523c63e58e 100644 --- a/src/ol/TileQueue.js +++ b/src/ol/TileQueue.js @@ -86,7 +86,9 @@ class TileQueue extends PriorityQueue { state === TileState.ERROR || state === TileState.EMPTY ) { - tile.removeEventListener(EventType.CHANGE, this.boundHandleTileChange_); + if (state !== TileState.ERROR) { + tile.removeEventListener(EventType.CHANGE, this.boundHandleTileChange_); + } const tileKey = tile.getKey(); if (tileKey in this.tilesLoadingKeys_) { delete this.tilesLoadingKeys_[tileKey]; diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index 5d0695aaaa..671cffb1ac 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -129,10 +129,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { const tileSource = tileLayer.getSource(); let tile = tileSource.getTile(z, x, y, pixelRatio, projection); if (tile.getState() == TileState.ERROR) { - if (!tileLayer.getUseInterimTilesOnError()) { - // When useInterimTilesOnError is false, we consider the error tile as loaded. - tile.setState(TileState.LOADED); - } else if (tileLayer.getPreload() > 0) { + if (tileLayer.getUseInterimTilesOnError() && tileLayer.getPreload() > 0) { // Preloaded tiles for lower resolutions might have finished loading. this.newTiles_ = true; } diff --git a/src/ol/source/TileEventType.js b/src/ol/source/TileEventType.js index b98d18a555..a649a3202c 100644 --- a/src/ol/source/TileEventType.js +++ b/src/ol/source/TileEventType.js @@ -22,7 +22,9 @@ export default { TILELOADEND: 'tileloadend', /** - * Triggered if tile loading results in an error. + * Triggered if tile loading results in an error. Note that this is not the + * right place to re-fetch tiles. See {@link module:ol/ImageTile~ImageTile#load} + * for details. * @event module:ol/source/Tile.TileSourceEvent#tileloaderror * @api */ diff --git a/test/browser/spec/ol/tilequeue.test.js b/test/browser/spec/ol/tilequeue.test.js index 5644c98513..47fee436dc 100644 --- a/test/browser/spec/ol/tilequeue.test.js +++ b/test/browser/spec/ol/tilequeue.test.js @@ -1,5 +1,6 @@ import ImageTile from '../../../../src/ol/ImageTile.js'; import Tile from '../../../../src/ol/Tile.js'; +import TileCache from '../../../../src/ol/TileCache.js'; import TileQueue from '../../../../src/ol/TileQueue.js'; import TileState from '../../../../src/ol/TileState.js'; import {DROP} from '../../../../src/ol/structs/PriorityQueue.js'; @@ -149,7 +150,7 @@ describe('ol.TileQueue', function () { expect(tile.hasListener('change')).to.be(false); }); - it('error tiles', function () { + it('error tiles - with retry', function (done) { const tq = new TileQueue(noop, noop); const tile = createImageTile(noop); @@ -160,7 +161,47 @@ describe('ol.TileQueue', function () { tile.setState(TileState.ERROR); expect(tq.getTilesLoading()).to.eql(0); - expect(tile.hasListener('change')).to.be(false); + expect(tile.hasListener('change')).to.be(true); + + tile.setState(TileState.IDLE); + setTimeout(() => tile.setState(TileState.LOADING), 100); + setTimeout(() => tile.setState(TileState.LOADED), 200); + setTimeout(() => { + try { + expect(tq.getTilesLoading()).to.eql(0); + expect(tile.hasListener('change')).to.be(false); + done(); + } catch (e) { + done(e); + } + }, 300); + }); + + it('error tiles - without retry', function (done) { + const tq = new TileQueue(noop, noop); + const tile = createImageTile(noop); + const tileCache = new TileCache(); + tileCache.set(tile.getTileCoord().toString(), tile); + + tq.enqueue([tile]); + tq.loadMoreTiles(Infinity, Infinity); + expect(tq.getTilesLoading()).to.eql(1); + expect(tile.getState()).to.eql(1); // LOADING + + tile.setState(TileState.ERROR); + expect(tq.getTilesLoading()).to.eql(0); + expect(tile.hasListener('change')).to.be(true); + + setTimeout(() => tileCache.clear(), 100); + setTimeout(() => { + try { + expect(tq.getTilesLoading()).to.eql(0); + expect(tile.hasListener('change')).to.be(false); + done(); + } catch (e) { + done(e); + } + }, 200); }); }); });