Merge pull request #13863 from ahocevar/tile-error-handling

Fix reloading tiles in case of an error with tile.load()
This commit is contained in:
Andreas Hocevar
2022-07-22 00:43:37 +02:00
committed by GitHub
7 changed files with 97 additions and 10 deletions

View File

@@ -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() {

View File

@@ -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.

View File

@@ -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<string, boolean>} usedTiles Used tiles.
*/

View File

@@ -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];

View File

@@ -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;
}

View File

@@ -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
*/

View File

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