From 5ab7cbf905444daefeda9f55873d694437d2dd0f Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 30 May 2021 21:22:03 +0200 Subject: [PATCH] Simpler and faster VectorTile loading --- src/ol/Tile.js | 8 - src/ol/TileQueue.js | 2 +- src/ol/VectorRenderTile.js | 27 +-- src/ol/renderer/canvas/VectorTileLayer.js | 14 +- src/ol/source/VectorTile.js | 223 +++++++----------- .../browser/spec/ol/source/vectortile.test.js | 2 +- test/browser/spec/ol/vectorrendertile.test.js | 10 +- 7 files changed, 98 insertions(+), 188 deletions(-) diff --git a/src/ol/Tile.js b/src/ol/Tile.js index cbdfb70703..82859d44e1 100644 --- a/src/ol/Tile.js +++ b/src/ol/Tile.js @@ -102,14 +102,6 @@ class Tile extends EventTarget { */ this.interimTile = null; - /** - * The tile is available at the highest possible resolution. Subclasses can - * set this to `false` initially. Tile load listeners will not be - * unregistered before this is set to `true` and a `#changed()` is called. - * @type {boolean} - */ - this.hifi = true; - /** * 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 diff --git a/src/ol/TileQueue.js b/src/ol/TileQueue.js index 349e884f84..ab34b428fc 100644 --- a/src/ol/TileQueue.js +++ b/src/ol/TileQueue.js @@ -82,7 +82,7 @@ class TileQueue extends PriorityQueue { const tile = /** @type {import("./Tile.js").default} */ (event.target); const state = tile.getState(); if ( - (tile.hifi && state === TileState.LOADED) || + state === TileState.LOADED || state === TileState.ERROR || state === TileState.EMPTY ) { diff --git a/src/ol/VectorRenderTile.js b/src/ol/VectorRenderTile.js index 7f771fcdde..de6ac25d2e 100644 --- a/src/ol/VectorRenderTile.js +++ b/src/ol/VectorRenderTile.js @@ -12,7 +12,6 @@ import {getUid} from './util.js'; * @property {number} renderedTileRevision RenderedTileRevision. * @property {number} renderedResolution RenderedResolution. * @property {number} renderedRevision RenderedRevision. - * @property {number} renderedZ RenderedZ. * @property {number} renderedTileResolution RenderedTileResolution. * @property {number} renderedTileZ RenderedTileZ. */ @@ -57,12 +56,6 @@ class VectorRenderTile extends Tile { */ this.loadingSourceTiles = 0; - /** - * Tile keys of error source tiles. Read/written by the source. - * @type {Object} - */ - this.errorSourceTileKeys = {}; - /** * @type {Object} */ @@ -77,7 +70,12 @@ class VectorRenderTile extends Tile { /** * @type {Array} */ - this.sourceTiles = null; + this.sourceTiles = []; + + /** + * @type {Object} + */ + this.errorTileKeys = {}; /** * @type {number} @@ -89,18 +87,6 @@ class VectorRenderTile extends Tile { */ this.getSourceTiles = getSourceTiles.bind(undefined, this); - /** - * z of the source tiles of the last getSourceTiles call. - * @type {number} - */ - this.sourceZ = -1; - - /** - * True when all tiles for this tile's nominal resolution are available. - * @type {boolean} - */ - this.hifi = false; - /** * @type {import("./tilecoord.js").TileCoord} */ @@ -150,7 +136,6 @@ class VectorRenderTile extends Tile { renderedRevision: -1, renderedTileResolution: NaN, renderedTileRevision: -1, - renderedZ: -1, renderedTileZ: -1, }; } diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 1bd92a5a85..ef9c6d065a 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -141,8 +141,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const tileUid = getUid(tile); const state = tile.getState(); if ( - ((state === TileState.LOADED && tile.hifi) || - state === TileState.ERROR) && + (state === TileState.LOADED || state === TileState.ERROR) && tileUid in this.tileListenerKeys_ ) { unlistenByKey(this.tileListenerKeys_[tileUid]); @@ -253,8 +252,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { !builderState.dirty && builderState.renderedResolution === resolution && builderState.renderedRevision == revision && - builderState.renderedRenderOrder == renderOrder && - builderState.renderedZ === tile.sourceZ + builderState.renderedRenderOrder == renderOrder ) { return; } @@ -372,7 +370,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } } builderState.renderedRevision = revision; - builderState.renderedZ = tile.sourceZ; builderState.renderedRenderOrder = renderOrder; builderState.renderedResolution = resolution; } @@ -516,7 +513,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { tileCoord.toString() === this.renderedTiles[i].tileCoord.toString() ) { tile = this.renderedTiles[i]; - if (tile.getState() === TileState.LOADED && tile.hifi) { + if (tile.getState() === TileState.LOADED) { const extent = tileGrid.getTileCoordExtent(tile.tileCoord); if ( source.getWrapX() && @@ -843,12 +840,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } const replayState = tile.getReplayState(layer); const revision = layer.getRevision(); - const sourceZ = tile.sourceZ; const resolution = tile.wantedResolution; return ( replayState.renderedTileResolution !== resolution || - replayState.renderedTileRevision !== revision || - replayState.renderedTileZ !== sourceZ + replayState.renderedTileRevision !== revision ); } @@ -863,7 +858,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const revision = layer.getRevision(); const executorGroups = tile.executorGroups[getUid(layer)]; replayState.renderedTileRevision = revision; - replayState.renderedTileZ = tile.sourceZ; const tileCoord = tile.wrappedTileCoord; const z = tileCoord[0]; diff --git a/src/ol/source/VectorTile.js b/src/ol/source/VectorTile.js index f9e9ed77bc..957ca34f34 100644 --- a/src/ol/source/VectorTile.js +++ b/src/ol/source/VectorTile.js @@ -18,8 +18,8 @@ import { createXYZ, extentFromProjection, } from '../tilegrid.js'; -import {equals} from '../array.js'; import {fromKey, getKeyZXY} from '../tilecoord.js'; +import {isEmpty} from '../obj.js'; import {loadFeaturesXhr} from '../featureloader.js'; import {toSize} from '../size.js'; @@ -146,11 +146,6 @@ class VectorTile extends UrlTile { */ this.format_ = options.format ? options.format : null; - /** - * @type {Object} - */ - this.loadingTiles_ = {}; - /** * @private * @type {TileCache} @@ -253,145 +248,91 @@ class VectorTile extends UrlTile { * @return {Array} Tile keys. */ getSourceTiles(pixelRatio, projection, tile) { - const urlTileCoord = tile.wrappedTileCoord; - const tileGrid = this.getTileGridForProjection(projection); - const extent = tileGrid.getTileCoordExtent(urlTileCoord); - const z = urlTileCoord[0]; - const resolution = tileGrid.getResolution(z); - // make extent 1 pixel smaller so we don't load tiles for < 0.5 pixel render space - bufferExtent(extent, -resolution, extent); - const sourceTileGrid = this.tileGrid; - const sourceExtent = sourceTileGrid.getExtent(); - if (sourceExtent) { - getIntersection(extent, sourceExtent, extent); - } - const sourceZ = sourceTileGrid.getZForResolution(resolution, 1); - const minZoom = sourceTileGrid.getMinZoom(); - - const previousSourceTiles = tile.sourceTiles; - let sourceTiles, covered, loadedZ; - if ( - previousSourceTiles && - previousSourceTiles.length > 0 && - previousSourceTiles[0].tileCoord[0] === sourceZ - ) { - sourceTiles = previousSourceTiles; - covered = true; - loadedZ = sourceZ; - } else { - sourceTiles = []; - loadedZ = sourceZ + 1; - do { - --loadedZ; - covered = true; - sourceTileGrid.forEachTileCoord( - extent, - loadedZ, - function (sourceTileCoord) { - const tileUrl = this.tileUrlFunction( - sourceTileCoord, - pixelRatio, - projection - ); - let sourceTile; - if (tileUrl !== undefined) { - if (this.sourceTileCache.containsKey(tileUrl)) { - sourceTile = this.sourceTileCache.get(tileUrl); - const state = sourceTile.getState(); - if ( - state === TileState.LOADED || - state === TileState.ERROR || - state === TileState.EMPTY - ) { - sourceTiles.push(sourceTile); - return; - } - } else if (loadedZ === sourceZ) { - sourceTile = new this.tileClass( - sourceTileCoord, - TileState.IDLE, - tileUrl, - this.format_, - this.tileLoadFunction - ); - sourceTile.extent = sourceTileGrid.getTileCoordExtent( - sourceTileCoord - ); - sourceTile.projection = projection; - sourceTile.resolution = sourceTileGrid.getResolution( - sourceTileCoord[0] - ); - this.sourceTileCache.set(tileUrl, sourceTile); - sourceTile.addEventListener( - EventType.CHANGE, - this.handleTileChange.bind(this) - ); - sourceTile.load(); - } - } - covered = - covered && - sourceTile && - sourceTile.getState() === TileState.LOADED; - if (!sourceTile) { - return; - } - if ( - sourceTile.getState() !== TileState.EMPTY && - tile.getState() === TileState.IDLE - ) { - tile.loadingSourceTiles++; - sourceTile.addEventListener( - EventType.CHANGE, - function listenChange() { - const state = sourceTile.getState(); - const sourceTileKey = sourceTile.getKey(); - if (state === TileState.LOADED || state === TileState.ERROR) { - if (state === TileState.LOADED) { - sourceTile.removeEventListener( - EventType.CHANGE, - listenChange - ); - tile.loadingSourceTiles--; - delete tile.errorSourceTileKeys[sourceTileKey]; - } else if (state === TileState.ERROR) { - tile.errorSourceTileKeys[sourceTileKey] = true; - } - const errorTileCount = Object.keys(tile.errorSourceTileKeys) - .length; - if (tile.loadingSourceTiles - errorTileCount === 0) { - tile.hifi = errorTileCount === 0; - tile.sourceZ = sourceZ; - tile.setState(TileState.LOADED); - } - } - } - ); - } - }.bind(this) - ); - if (!covered) { - sourceTiles.length = 0; - } - } while (!covered && loadedZ > minZoom); - } - if (tile.getState() === TileState.IDLE) { tile.setState(TileState.LOADING); - } - if (covered) { - tile.hifi = sourceZ === loadedZ; - tile.sourceZ = loadedZ; - if (tile.getState() < TileState.LOADED) { - tile.setState(TileState.LOADED); - } else if ( - !previousSourceTiles || - !equals(sourceTiles, previousSourceTiles) - ) { - tile.sourceTiles = sourceTiles; + const urlTileCoord = tile.wrappedTileCoord; + const tileGrid = this.getTileGridForProjection(projection); + const extent = tileGrid.getTileCoordExtent(urlTileCoord); + const z = urlTileCoord[0]; + const resolution = tileGrid.getResolution(z); + // make extent 1 pixel smaller so we don't load tiles for < 0.5 pixel render space + bufferExtent(extent, -resolution, extent); + const sourceTileGrid = this.tileGrid; + const sourceExtent = sourceTileGrid.getExtent(); + if (sourceExtent) { + getIntersection(extent, sourceExtent, extent); + } + const sourceZ = sourceTileGrid.getZForResolution(resolution, 1); + + sourceTileGrid.forEachTileCoord(extent, sourceZ, (sourceTileCoord) => { + const tileUrl = this.tileUrlFunction( + sourceTileCoord, + pixelRatio, + projection + ); + const sourceTile = this.sourceTileCache.containsKey(tileUrl) + ? this.sourceTileCache.get(tileUrl) + : new this.tileClass( + sourceTileCoord, + tileUrl ? TileState.IDLE : TileState.EMPTY, + tileUrl, + this.format_, + this.tileLoadFunction + ); + tile.sourceTiles.push(sourceTile); + const sourceTileState = sourceTile.getState(); + if (sourceTileState === TileState.IDLE) { + sourceTile.extent = sourceTileGrid.getTileCoordExtent( + sourceTileCoord + ); + sourceTile.projection = projection; + sourceTile.resolution = sourceTileGrid.getResolution( + sourceTileCoord[0] + ); + this.sourceTileCache.set(tileUrl, sourceTile); + const listenChange = (event) => { + this.handleTileChange(event); + const state = sourceTile.getState(); + if (state === TileState.LOADED || state === TileState.ERROR) { + const sourceTileKey = sourceTile.getKey(); + if (sourceTileKey in tile.errorTileKeys) { + if (sourceTile.getState() === TileState.LOADED) { + delete tile.errorTileKeys[sourceTileKey]; + } + } else { + tile.loadingSourceTiles--; + } + if (state === TileState.ERROR) { + tile.errorTileKeys[sourceTileKey] = true; + } else { + sourceTile.removeEventListener(EventType.CHANGE, listenChange); + } + if (tile.loadingSourceTiles === 0) { + tile.setState( + isEmpty(tile.errorTileKeys) + ? TileState.LOADED + : TileState.ERROR + ); + } + } + }; + sourceTile.addEventListener(EventType.CHANGE, listenChange); + tile.loadingSourceTiles++; + sourceTile.load(); + } + }); + if (!tile.loadingSourceTiles) { + tile.setState( + tile.sourceTiles.some( + (sourceTile) => sourceTile.getState() === TileState.ERROR + ) + ? TileState.ERROR + : TileState.LOADED + ); } } - return sourceTiles; + + return tile.sourceTiles; } /** diff --git a/test/browser/spec/ol/source/vectortile.test.js b/test/browser/spec/ol/source/vectortile.test.js index acb62b715f..290938c3bb 100644 --- a/test/browser/spec/ol/source/vectortile.test.js +++ b/test/browser/spec/ol/source/vectortile.test.js @@ -305,7 +305,7 @@ describe('ol.source.VectorTile', function () { let count = 0; let tile = source.getTile(0, 0, 0, 1, map.getView().getProjection()); tile.addEventListener('change', function onTileChange(e) { - if (e.target.getState() !== TileState.LOADED && !e.target.hifi) { + if (e.target.getState() !== TileState.LOADED) { return; } e.target.removeEventListener('change', onTileChange); diff --git a/test/browser/spec/ol/vectorrendertile.test.js b/test/browser/spec/ol/vectorrendertile.test.js index 01481fde6b..26bc4eb067 100644 --- a/test/browser/spec/ol/vectorrendertile.test.js +++ b/test/browser/spec/ol/vectorrendertile.test.js @@ -24,19 +24,18 @@ describe('ol.VectorRenderTile', function () { listen(tile, 'change', function (e) { ++calls; if (calls === 1) { - expect(tile.getState()).to.be(TileState.LOADED); - expect(tile.hifi).to.be(false); + expect(tile.getState()).to.be(TileState.ERROR); setTimeout(function () { sourceTile.setState(TileState.LOADED); - expect(tile.hifi).to.be(true); }, 0); } else if (calls === 2) { + expect(tile.getState()).to.be(TileState.LOADED); done(); } }); }); - it('sets LOADED state and hifi==false when source tiles fail to load', function (done) { + it('sets ERROR state when source tiles fail to load', function (done) { const source = new VectorTileSource({ format: new GeoJSON(), url: 'spec/ol/data/unavailable.json', @@ -46,8 +45,7 @@ describe('ol.VectorRenderTile', function () { tile.load(); listen(tile, 'change', function (e) { - expect(tile.getState()).to.be(TileState.LOADED); - expect(tile.hifi).to.be(false); + expect(tile.getState()).to.be(TileState.ERROR); done(); }); });