From 43759fd8464fa8660324c1a9d0c266ec6b5a8bda Mon Sep 17 00:00:00 2001 From: ahocevar Date: Tue, 20 Nov 2018 20:02:34 +0100 Subject: [PATCH 1/7] Decouple executor group creation from rendering --- src/ol/Tile.js | 3 +- src/ol/VectorImageTile.js | 38 +++++++++++++-------- src/ol/VectorTile.js | 18 +++++----- src/ol/renderer/canvas/TileLayer.js | 2 +- src/ol/renderer/canvas/VectorTileLayer.js | 40 ++++++++++++++--------- 5 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/ol/Tile.js b/src/ol/Tile.js index f7a9dc0e78..91c4ca3f69 100644 --- a/src/ol/Tile.js +++ b/src/ol/Tile.js @@ -146,9 +146,10 @@ class Tile extends EventTarget { * Get the interim tile most suitable for rendering using the chain of interim * tiles. This corresponds to the most recent tile that has been loaded, if no * such tile exists, the original tile is returned. + * @param {import("./layer/Layer").default} layer Layer. * @return {!Tile} Best tile for rendering. */ - getInterimTile() { + getInterimTile(layer) { if (!this.interimTile) { //empty chain return this; diff --git a/src/ol/VectorImageTile.js b/src/ol/VectorImageTile.js index ce1459f3bc..829d7734d3 100644 --- a/src/ol/VectorImageTile.js +++ b/src/ol/VectorImageTile.js @@ -72,6 +72,12 @@ class VectorImageTile extends Tile { */ this.sourceTiles_ = sourceTiles; + /** + * @private + * @type {boolean} + */ + this.sourceTilesLoaded = false; + /** * Keys of source tiles used by this tile. Use with {@link #getTile}. * @type {Array} @@ -107,13 +113,13 @@ class VectorImageTile extends Tile { * Use only source tiles that are loaded already * @type {boolean} */ - this.useLoadedOnly = zoom != tileCoord[0]; + this.isInterimTile = zoom != tileCoord[0]; if (urlTileCoord) { const extent = this.extent = tileGrid.getTileCoordExtent(urlTileCoord); const resolution = tileGrid.getResolution(zoom); const sourceZ = sourceTileGrid.getZForResolution(resolution); - const useLoadedOnly = this.useLoadedOnly; + const isInterimTile = this.isInterimTile; let loadCount = 0; sourceTileGrid.forEachTileCoord(extent, sourceZ, function(sourceTileCoord) { let sharedExtent = getIntersection(extent, @@ -128,7 +134,7 @@ class VectorImageTile extends Tile { ++loadCount; const sourceTileKey = sourceTileCoord.toString(); let sourceTile = sourceTiles[sourceTileKey]; - if (!sourceTile && !useLoadedOnly) { + if (!sourceTile && !isInterimTile) { const tileUrl = tileUrlFunction(sourceTileCoord, pixelRatio, projection); sourceTile = sourceTiles[sourceTileKey] = new tileClass(sourceTileCoord, tileUrl == undefined ? TileState.EMPTY : TileState.IDLE, @@ -137,19 +143,19 @@ class VectorImageTile extends Tile { this.sourceTileListenerKeys_.push( listen(sourceTile, EventType.CHANGE, handleTileChange)); } - if (sourceTile && (!useLoadedOnly || sourceTile.getState() == TileState.LOADED)) { + if (sourceTile && (!isInterimTile || sourceTile.getState() == TileState.LOADED)) { sourceTile.consumers++; this.tileKeys.push(sourceTileKey); } } }.bind(this)); - if (useLoadedOnly && loadCount == this.tileKeys.length) { + if (isInterimTile && loadCount == this.tileKeys.length) { this.finishLoading_(); } - this.createInterimTile_ = function() { - if (this.getState() !== TileState.LOADED && !useLoadedOnly) { + this.createInterimTile_ = function(layerId) { + if (this.getState() !== TileState.LOADED && !isInterimTile) { let bestZoom = -1; for (const key in sourceTiles) { const sourceTile = sourceTiles[key]; @@ -157,16 +163,20 @@ class VectorImageTile extends Tile { const sourceTileCoord = sourceTile.tileCoord; const sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord); if (containsExtent(sourceTileExtent, extent) && sourceTileCoord[0] > bestZoom) { - bestZoom = sourceTileCoord[0]; + const lowResExecutorGroup = sourceTile.getLowResExecutorGroup(layerId, zoom, extent); + if (lowResExecutorGroup) { + // reuse existing replay if we're rendering an interim tile + sourceTile.setExecutorGroup(layerId, this.tileCoord.toString(), lowResExecutorGroup); + bestZoom = sourceTileCoord[0]; + } } } } if (bestZoom !== -1) { - const tile = new VectorImageTile(tileCoord, state, sourceRevision, + this.interimTile = new VectorImageTile(tileCoord, state, sourceRevision, format, tileLoadFunction, urlTileCoord, tileUrlFunction, sourceTileGrid, tileGrid, sourceTiles, pixelRatio, projection, tileClass, VOID, bestZoom); - this.interimTile = tile; } } }; @@ -174,11 +184,11 @@ class VectorImageTile extends Tile { } - getInterimTile() { + getInterimTile(layer) { if (!this.interimTile) { - this.createInterimTile_(); + this.createInterimTile_(getUid(layer)); } - return super.getInterimTile(); + return super.getInterimTile(layer); } /** @@ -331,7 +341,7 @@ class VectorImageTile extends Tile { this.loadListenerKeys_.forEach(unlistenByKey); this.loadListenerKeys_.length = 0; this.sourceTilesLoaded = true; - this.setState(TileState.LOADED); + this.changed(); } else { this.setState(empty == this.tileKeys.length ? TileState.EMPTY : TileState.ERROR); } diff --git a/src/ol/VectorTile.js b/src/ol/VectorTile.js index b6e1c5373e..3e64403891 100644 --- a/src/ol/VectorTile.js +++ b/src/ol/VectorTile.js @@ -2,7 +2,6 @@ * @module ol/VectorTile */ import {containsExtent} from './extent.js'; -import {getUid} from './util.js'; import Tile from './Tile.js'; import TileState from './TileState.js'; @@ -140,23 +139,22 @@ class VectorTile extends Tile { } /** - * @param {import("./layer/Layer.js").default} layer Layer. + * @param {string} layerId UID of the layer. * @param {string} key Key. * @return {import("./render/canvas/ExecutorGroup.js").default} Executor group. */ - getExecutorGroup(layer, key) { - return this.executorGroups_[getUid(layer) + ',' + key]; + getExecutorGroup(layerId, key) { + return this.executorGroups_[layerId + ',' + key]; } /** * Get the best matching lower resolution replay group for a given zoom and extent. - * @param {import("./layer/Layer").default} layer Layer. + * @param {string} layerId UID of the layer. * @param {number} zoom Zoom. * @param {import("./extent").Extent} extent Extent. * @return {import("./render/canvas/ExecutorGroup.js").default} Executor groups. */ - getLowResExecutorGroup(layer, zoom, extent) { - const layerId = getUid(layer); + getLowResExecutorGroup(layerId, zoom, extent) { let bestZoom = 0; let replayGroup = null; for (const key in this.executorGroups_) { @@ -242,12 +240,12 @@ class VectorTile extends Tile { } /** - * @param {import("./layer/Layer.js").default} layer Layer. + * @param {string} layerId UID of the layer. * @param {string} key Key. * @param {import("./render/canvas/ExecutorGroup.js").default} executorGroup Executor group. */ - setExecutorGroup(layer, key, executorGroup) { - this.executorGroups_[getUid(layer) + ',' + key] = executorGroup; + setExecutorGroup(layerId, key, executorGroup) { + this.executorGroups_[layerId + ',' + key] = executorGroup; } /** diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index ef9b0059a6..eca296b5c8 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -100,7 +100,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { } } if (!this.isDrawableTile_(tile)) { - tile = tile.getInterimTile(); + tile = tile.getInterimTile(tileLayer); } return tile; } diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index e80e18abdb..029403edf3 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -5,7 +5,7 @@ import {getUid} from '../../util.js'; import {createCanvasContext2D} from '../../dom.js'; import TileState from '../../TileState.js'; import ViewHint from '../../ViewHint.js'; -import {listen, unlisten} from '../../events.js'; +import {listen, unlisten, unlistenByKey} from '../../events.js'; import EventType from '../../events/EventType.js'; import rbush from 'rbush'; import {buffer, containsCoordinate, equals, getIntersection, getTopLeft, intersects} from '../../extent.js'; @@ -147,11 +147,20 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @inheritDoc */ getTile(z, x, y, pixelRatio, projection) { - const tile = super.getTile(z, x, y, pixelRatio, projection); + const tile = /** @type {import("../../VectorImageTile.js").default} */ (super.getTile(z, x, y, pixelRatio, projection)); + if (tile.getState() === TileState.IDLE) { + const key = listen(tile, EventType.CHANGE, function() { + if (tile.sourceTilesLoaded) { + this.createExecutorGroup_(tile, pixelRatio, projection); + unlistenByKey(key); + tile.setState(TileState.LOADED); + } + }.bind(this)); + } if (tile.getState() === TileState.LOADED) { - this.createExecutorGroup_(/** @type {import("../../VectorImageTile.js").default} */ (tile), pixelRatio, projection); + this.createExecutorGroup_(tile, pixelRatio, projection); if (this.context) { - this.renderTileImage_(/** @type {import("../../VectorImageTile.js").default} */ (tile), pixelRatio, projection); + this.renderTileImage_(tile, pixelRatio, projection); } } return tile; @@ -162,7 +171,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ getTileImage(tile) { const tileLayer = /** @type {import("../../layer/Tile.js").default} */ (this.getLayer()); - return /** @type {import("../../VectorImageTile.js").default} */ (tile).getImage(tileLayer); + return tile.getImage(tileLayer); } /** @@ -186,6 +195,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ createExecutorGroup_(tile, pixelRatio, projection) { const layer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()); + const layerId = getUid(layer); const revision = layer.getRevision(); const renderOrder = /** @type {import("../../render.js").OrderFunction} */ (layer.getRenderOrder()) || null; @@ -207,13 +217,11 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (sourceTile.getState() != TileState.LOADED) { continue; } - if (tile.useLoadedOnly) { - const lowResExecutorGroup = sourceTile.getLowResExecutorGroup(layer, zoom, tileExtent); - if (lowResExecutorGroup) { - // reuse existing replay if we're rendering an interim tile - sourceTile.setExecutorGroup(layer, tile.tileCoord.toString(), lowResExecutorGroup); - continue; - } + if (tile.isInterimTile) { + // reuse existing replay if we're rendering an interim tile + sourceTile.setExecutorGroup(layerId, tile.tileCoord.toString(), + sourceTile.getLowResExecutorGroup(layerId, zoom, tileExtent)); + continue; } const sourceTileCoord = sourceTile.tileCoord; @@ -271,7 +279,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const executorGroupInstructions = builderGroup.finish(); const renderingReplayGroup = new CanvasExecutorGroup(sharedExtent, resolution, pixelRatio, source.getOverlaps(), this.declutterTree_, executorGroupInstructions, layer.getRenderBuffer()); - sourceTile.setExecutorGroup(layer, tile.tileCoord.toString(), renderingReplayGroup); + sourceTile.setExecutorGroup(getUid(layer), tile.tileCoord.toString(), renderingReplayGroup); } builderState.renderedRevision = revision; builderState.renderedRenderOrder = renderOrder; @@ -303,7 +311,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (sourceTile.getState() != TileState.LOADED) { continue; } - const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, + const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(getUid(layer), tile.tileCoord.toString())); found = found || executorGroup.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, /** @@ -431,7 +439,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (sourceTile.getState() != TileState.LOADED) { continue; } - const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, tileCoord.toString())); + const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(getUid(layer), tileCoord.toString())); if (!executorGroup || !executorGroup.hasExecutors(replayTypes)) { // sourceTile was not yet loaded when this.createReplayGroup_() was // called, or it has no replays of the types we want to render @@ -536,7 +544,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const transform = resetTransform(this.tmpTransform_); scaleTransform(transform, pixelScale, -pixelScale); translateTransform(transform, -tileExtent[0], -tileExtent[3]); - const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, + const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(getUid(layer), tile.tileCoord.toString())); executorGroup.execute(context, transform, 0, {}, true, replays); } From 82e2a848629299eba1f57d5f58c73b72376c0e2f Mon Sep 17 00:00:00 2001 From: ahocevar Date: Wed, 21 Nov 2018 06:06:33 +0100 Subject: [PATCH 2/7] Remove interim tile handling for now --- src/ol/Tile.js | 3 +- src/ol/VectorImageTile.js | 68 ++----------------- src/ol/renderer/canvas/TileLayer.js | 2 +- src/ol/renderer/canvas/VectorTileLayer.js | 8 --- src/ol/source/VectorTile.js | 2 +- .../renderer/canvas/vectortilelayer.test.js | 4 +- test/spec/ol/vectorimagetile.test.js | 14 ++-- 7 files changed, 18 insertions(+), 83 deletions(-) diff --git a/src/ol/Tile.js b/src/ol/Tile.js index 91c4ca3f69..f7a9dc0e78 100644 --- a/src/ol/Tile.js +++ b/src/ol/Tile.js @@ -146,10 +146,9 @@ class Tile extends EventTarget { * Get the interim tile most suitable for rendering using the chain of interim * tiles. This corresponds to the most recent tile that has been loaded, if no * such tile exists, the original tile is returned. - * @param {import("./layer/Layer").default} layer Layer. * @return {!Tile} Best tile for rendering. */ - getInterimTile(layer) { + getInterimTile() { if (!this.interimTile) { //empty chain return this; diff --git a/src/ol/VectorImageTile.js b/src/ol/VectorImageTile.js index 829d7734d3..85ad648f50 100644 --- a/src/ol/VectorImageTile.js +++ b/src/ol/VectorImageTile.js @@ -6,10 +6,9 @@ import Tile from './Tile.js'; import TileState from './TileState.js'; import {createCanvasContext2D} from './dom.js'; import {listen, unlistenByKey} from './events.js'; -import {containsExtent, getHeight, getIntersection, getWidth} from './extent.js'; +import {getHeight, getIntersection, getWidth} from './extent.js'; import EventType from './events/EventType.js'; import {loadFeaturesXhr} from './featureloader.js'; -import {VOID} from './functions.js'; /** @@ -40,11 +39,10 @@ class VectorImageTile extends Tile { * instantiate for source tiles. * @param {function(this: import("./source/VectorTile.js").default, import("./events/Event.js").default)} handleTileChange * Function to call when a source tile's state changes. - * @param {number} zoom Integer zoom to render the tile for. */ constructor(tileCoord, state, sourceRevision, format, tileLoadFunction, urlTileCoord, tileUrlFunction, sourceTileGrid, tileGrid, sourceTiles, - pixelRatio, projection, tileClass, handleTileChange, zoom) { + pixelRatio, projection, tileClass, handleTileChange) { super(tileCoord, state, {transition: 0}); @@ -109,18 +107,10 @@ class VectorImageTile extends Tile { */ this.sourceTileListenerKeys_ = []; - /** - * Use only source tiles that are loaded already - * @type {boolean} - */ - this.isInterimTile = zoom != tileCoord[0]; - if (urlTileCoord) { const extent = this.extent = tileGrid.getTileCoordExtent(urlTileCoord); - const resolution = tileGrid.getResolution(zoom); + const resolution = tileGrid.getResolution(urlTileCoord[0]); const sourceZ = sourceTileGrid.getZForResolution(resolution); - const isInterimTile = this.isInterimTile; - let loadCount = 0; sourceTileGrid.forEachTileCoord(extent, sourceZ, function(sourceTileCoord) { let sharedExtent = getIntersection(extent, sourceTileGrid.getTileCoordExtent(sourceTileCoord)); @@ -131,10 +121,9 @@ class VectorImageTile extends Tile { if (getWidth(sharedExtent) / resolution >= 0.5 && getHeight(sharedExtent) / resolution >= 0.5) { // only include source tile if overlap is at least 1 pixel - ++loadCount; const sourceTileKey = sourceTileCoord.toString(); let sourceTile = sourceTiles[sourceTileKey]; - if (!sourceTile && !isInterimTile) { + if (!sourceTile) { const tileUrl = tileUrlFunction(sourceTileCoord, pixelRatio, projection); sourceTile = sourceTiles[sourceTileKey] = new tileClass(sourceTileCoord, tileUrl == undefined ? TileState.EMPTY : TileState.IDLE, @@ -143,64 +132,19 @@ class VectorImageTile extends Tile { this.sourceTileListenerKeys_.push( listen(sourceTile, EventType.CHANGE, handleTileChange)); } - if (sourceTile && (!isInterimTile || sourceTile.getState() == TileState.LOADED)) { - sourceTile.consumers++; - this.tileKeys.push(sourceTileKey); - } + sourceTile.consumers++; + this.tileKeys.push(sourceTileKey); } }.bind(this)); - - if (isInterimTile && loadCount == this.tileKeys.length) { - this.finishLoading_(); - } - - this.createInterimTile_ = function(layerId) { - if (this.getState() !== TileState.LOADED && !isInterimTile) { - let bestZoom = -1; - for (const key in sourceTiles) { - const sourceTile = sourceTiles[key]; - if (sourceTile.getState() === TileState.LOADED) { - const sourceTileCoord = sourceTile.tileCoord; - const sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord); - if (containsExtent(sourceTileExtent, extent) && sourceTileCoord[0] > bestZoom) { - const lowResExecutorGroup = sourceTile.getLowResExecutorGroup(layerId, zoom, extent); - if (lowResExecutorGroup) { - // reuse existing replay if we're rendering an interim tile - sourceTile.setExecutorGroup(layerId, this.tileCoord.toString(), lowResExecutorGroup); - bestZoom = sourceTileCoord[0]; - } - } - } - } - if (bestZoom !== -1) { - this.interimTile = new VectorImageTile(tileCoord, state, sourceRevision, - format, tileLoadFunction, urlTileCoord, tileUrlFunction, - sourceTileGrid, tileGrid, sourceTiles, pixelRatio, projection, - tileClass, VOID, bestZoom); - } - } - }; } - - } - - getInterimTile(layer) { - if (!this.interimTile) { - this.createInterimTile_(getUid(layer)); - } - return super.getInterimTile(layer); } /** * @inheritDoc */ disposeInternal() { - delete this.createInterimTile_; this.state = TileState.ABORT; this.changed(); - if (this.interimTile) { - this.interimTile.dispose(); - } for (let i = 0, ii = this.tileKeys.length; i < ii; ++i) { const sourceTileKey = this.tileKeys[i]; diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index eca296b5c8..ef9b0059a6 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -100,7 +100,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { } } if (!this.isDrawableTile_(tile)) { - tile = tile.getInterimTile(tileLayer); + tile = tile.getInterimTile(); } return tile; } diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 029403edf3..5331a24fd7 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -195,7 +195,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ createExecutorGroup_(tile, pixelRatio, projection) { const layer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()); - const layerId = getUid(layer); const revision = layer.getRevision(); const renderOrder = /** @type {import("../../render.js").OrderFunction} */ (layer.getRenderOrder()) || null; @@ -217,13 +216,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (sourceTile.getState() != TileState.LOADED) { continue; } - if (tile.isInterimTile) { - // reuse existing replay if we're rendering an interim tile - sourceTile.setExecutorGroup(layerId, tile.tileCoord.toString(), - sourceTile.getLowResExecutorGroup(layerId, zoom, tileExtent)); - continue; - } - const sourceTileCoord = sourceTile.tileCoord; const sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord); const sharedExtent = getIntersection(tileExtent, sourceTileExtent); diff --git a/src/ol/source/VectorTile.js b/src/ol/source/VectorTile.js index 63a266215a..5be126a92f 100644 --- a/src/ol/source/VectorTile.js +++ b/src/ol/source/VectorTile.js @@ -171,7 +171,7 @@ class VectorTile extends UrlTile { this.format_, this.tileLoadFunction, urlTileCoord, this.tileUrlFunction, this.tileGrid, this.getTileGridForProjection(projection), this.sourceTiles_, pixelRatio, projection, this.tileClass, - this.handleTileChange.bind(this), tileCoord[0]); + this.handleTileChange.bind(this)); this.tileCache.set(tileCoordKey, tile); return tile; diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 2dd459d0ca..6801a21231 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -244,8 +244,8 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { }; const tileUrlFunction = function() {}; const tile = new VectorImageTile([0, 0, 0], undefined, undefined, undefined, - undefined, [0, 0, 0], tileUrlFunction, createXYZ(), createXYZ(), {}, undefined, - undefined, VectorTile, undefined, 0); + undefined, [0, 0, 0], undefined, createXYZ(), createXYZ(), undefined, undefined, + undefined, undefined, undefined); tile.transition_ = 0; tile.wrappedTileCoord = [0, 0, 0]; tile.setState(TileState.LOADED); diff --git a/test/spec/ol/vectorimagetile.test.js b/test/spec/ol/vectorimagetile.test.js index e5f7809f68..63a1073b4b 100644 --- a/test/spec/ol/vectorimagetile.test.js +++ b/test/spec/ol/vectorimagetile.test.js @@ -17,7 +17,7 @@ describe('ol.VectorImageTile', function() { defaultLoadFunction, [0, 0, 0], function() { return url; }, createXYZ(), createXYZ(), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}, 0); + 1, getProjection('EPSG:3857'), VectorTile, function() {}); tile.load(); const sourceTile = tile.getTile(tile.tileKeys[0]); @@ -41,7 +41,7 @@ describe('ol.VectorImageTile', function() { }, [0, 0, 0], function() { return url; }, createXYZ(), createXYZ(), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}, 0); + 1, getProjection('EPSG:3857'), VectorTile, function() {}); tile.load(); let calls = 0; @@ -65,7 +65,7 @@ describe('ol.VectorImageTile', function() { defaultLoadFunction, [0, 0, 0], function() { return url; }, createXYZ(), createXYZ(), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}, 0); + 1, getProjection('EPSG:3857'), VectorTile, function() {}); tile.load(); @@ -81,7 +81,7 @@ describe('ol.VectorImageTile', function() { const tile = new VectorImageTile([0, 0, 0], 0, url, format, defaultLoadFunction, [0, 0, 0], function() {}, createXYZ(), createXYZ(), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}, 0); + 1, getProjection('EPSG:3857'), VectorTile, function() {}); tile.load(); @@ -105,7 +105,7 @@ describe('ol.VectorImageTile', function() { return url; }, tileGrid, createXYZ({extent: [-180, -90, 180, 90], tileSize: 512}), - sourceTiles, 1, getProjection('EPSG:4326'), VectorTile, function() {}, 1); + sourceTiles, 1, getProjection('EPSG:4326'), VectorTile, function() {}); tile.load(); expect(tile.tileKeys.length).to.be(1); expect(tile.getTile(tile.tileKeys[0]).tileCoord).to.eql([0, 16, 9]); @@ -118,7 +118,7 @@ describe('ol.VectorImageTile', function() { defaultLoadFunction, [0, 0, 0], function() { return url; }, createXYZ(), createXYZ({tileSize: 512}), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}, 0); + 1, getProjection('EPSG:3857'), VectorTile, function() {}); tile.load(); expect(tile.loadListenerKeys_.length).to.be(4); @@ -138,7 +138,7 @@ describe('ol.VectorImageTile', function() { defaultLoadFunction, [0, 0, 0], function() { return url; }, createXYZ(), createXYZ({tileSize: 512}), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}, 0); + 1, getProjection('EPSG:3857'), VectorTile, function() {}); tile.load(); listenOnce(tile, 'change', function() { From 2ce8fa6f10e55164a762692014856858c26b7051 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Wed, 21 Nov 2018 15:30:00 +0100 Subject: [PATCH 3/7] Render only when we have time, and not during interaction/animation --- src/ol/Disposable.js | 6 +-- src/ol/renderer/canvas/VectorTileLayer.js | 53 ++++++++++++++++++----- test/spec/ol/disposable.test.js | 6 +-- test/spec/ol/vectorimagetile.test.js | 13 ++++-- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/ol/Disposable.js b/src/ol/Disposable.js index d7800fa2b8..da4cb6b60f 100644 --- a/src/ol/Disposable.js +++ b/src/ol/Disposable.js @@ -14,15 +14,15 @@ class Disposable { * @type {boolean} * @private */ - this.disposed_ = false; + this.disposed = false; } /** * Clean up. */ dispose() { - if (!this.disposed_) { - this.disposed_ = true; + if (!this.disposed) { + this.disposed = true; this.disposeInternal(); } } diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 5331a24fd7..4390579207 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -122,6 +122,12 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ this.renderedLayerRevision_; + /** + * @private + * @type {Object} + */ + this.tilesToPrepare_ = null; + /** * @private * @type {import("../../transform.js").Transform} @@ -151,17 +157,23 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (tile.getState() === TileState.IDLE) { const key = listen(tile, EventType.CHANGE, function() { if (tile.sourceTilesLoaded) { - this.createExecutorGroup_(tile, pixelRatio, projection); + this.updateExecutorGroup_(tile, pixelRatio, projection); + if (!this.tilesToPrepare_) { + this.tilesToPrepare_ = {}; + tile.setState(TileState.LOADED); + } else { + const tileId = getUid(tile); + if (!(tileId in this.tilesToPrepare_)) { + this.tilesToPrepare_[tileId] = [tile, pixelRatio, projection]; + } + } unlistenByKey(key); - tile.setState(TileState.LOADED); } }.bind(this)); } if (tile.getState() === TileState.LOADED) { - this.createExecutorGroup_(tile, pixelRatio, projection); - if (this.context) { - this.renderTileImage_(tile, pixelRatio, projection); - } + this.updateExecutorGroup_(tile, pixelRatio, projection); + this.renderTileImage_(tile, pixelRatio, projection); } return tile; } @@ -193,7 +205,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @param {import("../../proj/Projection.js").default} projection Projection. * @private */ - createExecutorGroup_(tile, pixelRatio, projection) { + updateExecutorGroup_(tile, pixelRatio, projection) { const layer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()); const revision = layer.getRevision(); const renderOrder = /** @type {import("../../render.js").OrderFunction} */ (layer.getRenderOrder()) || null; @@ -413,7 +425,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { this.declutterTree_.clear(); } const viewHints = frameState.viewHints; - const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); + const hifi = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); const tiles = this.renderedTiles; const tileGrid = source.getTileGridForProjection(frameState.viewState.projection); const clips = []; @@ -460,14 +472,14 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { context.clip(); } } - executorGroup.execute(context, transform, rotation, {}, snapToPixel, replayTypes, declutterReplays); + executorGroup.execute(context, transform, rotation, {}, hifi, replayTypes, declutterReplays); context.restore(); clips.push(currentClip); zs.push(currentZ); } } if (declutterReplays) { - replayDeclutter(declutterReplays, context, rotation, snapToPixel); + replayDeclutter(declutterReplays, context, rotation, hifi); } const opacity = layerState.opacity; @@ -475,6 +487,27 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { canvas.style.opacity = opacity; } + if (this.tilesToPrepare_) { + for (const key in this.tilesToPrepare_) { + if (!hifi || Date.now() - frameState.time >= 16) { + break; + } + const args = this.tilesToPrepare_[key]; + delete this.tilesToPrepare_[key]; + const tile = args[0]; + if (!tile.disposed) { + frameState.animate = true; + this.renderTileImage_.apply(this, args); + tile.setState(TileState.LOADED); + } + } + if (Object.keys(this.tilesToPrepare_).length > 0) { + frameState.animate = true; + } else { + this.tilesToPrepare_ = null; + } + } + return this.container_; } diff --git a/test/spec/ol/disposable.test.js b/test/spec/ol/disposable.test.js index 21907b9bbd..168446f186 100644 --- a/test/spec/ol/disposable.test.js +++ b/test/spec/ol/disposable.test.js @@ -12,17 +12,17 @@ describe('ol.Disposable', function() { }); - describe('#disposed_', function() { + describe('#disposed', function() { it('is initially false', function() { const disposable = new Disposable(); - expect(disposable.disposed_).to.be(false); + expect(disposable.disposed).to.be(false); }); it('is true after a call to dispose', function() { const disposable = new Disposable(); disposable.dispose(); - expect(disposable.disposed_).to.be(true); + expect(disposable.disposed).to.be(true); }); }); diff --git a/test/spec/ol/vectorimagetile.test.js b/test/spec/ol/vectorimagetile.test.js index 63a1073b4b..b1b093a68b 100644 --- a/test/spec/ol/vectorimagetile.test.js +++ b/test/spec/ol/vectorimagetile.test.js @@ -30,7 +30,7 @@ describe('ol.VectorImageTile', function() { }); }); - it('sets LOADED state when previously failed source tiles are loaded', function(done) { + it('sets sourceTilesLoaded when previously failed source tiles are loaded', function(done) { const format = new GeoJSON(); const url = 'spec/ol/data/unavailable.json'; let sourceTile; @@ -47,7 +47,11 @@ describe('ol.VectorImageTile', function() { let calls = 0; listen(tile, 'change', function(e) { ++calls; - expect(tile.getState()).to.be(calls == 2 ? TileState.LOADED : TileState.ERROR); + if (calls === 1) { + expect(tile.sourceTilesLoaded).to.be(false); + } else if (calls === 2) { + expect(tile.sourceTilesLoaded).to.be(true); + } if (calls == 2) { done(); } else { @@ -131,7 +135,7 @@ describe('ol.VectorImageTile', function() { expect(tile.getState()).to.be(TileState.ABORT); }); - it('#dispose() when loaded', function(done) { + it('#dispose() when source tiles are loaded', function(done) { const format = new GeoJSON(); const url = 'spec/ol/data/point.json'; const tile = new VectorImageTile([0, 0, 0], 0, url, format, @@ -142,7 +146,8 @@ describe('ol.VectorImageTile', function() { tile.load(); listenOnce(tile, 'change', function() { - expect(tile.getState()).to.be(TileState.LOADED); + expect(tile.getState()).to.be(TileState.LOADING); + expect(tile.sourceTilesLoaded).to.be.ok(); expect(tile.loadListenerKeys_.length).to.be(0); expect(tile.tileKeys.length).to.be(4); tile.dispose(); From fbf98a44ea0ce12570b6edc311a536912ce16345 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Wed, 21 Nov 2018 23:59:48 +0100 Subject: [PATCH 4/7] Streamline tile preparation and remove unused code --- src/ol/Disposable.js | 6 +- src/ol/VectorImageTile.js | 13 ++- src/ol/VectorTile.js | 25 ----- src/ol/render/canvas/ExecutorGroup.js | 7 -- src/ol/renderer/Layer.js | 25 +++-- src/ol/renderer/canvas/TileLayer.js | 8 +- src/ol/renderer/canvas/VectorTileLayer.js | 98 ++++++++++++------- test/spec/ol/disposable.test.js | 6 +- .../renderer/canvas/vectortilelayer.test.js | 11 ++- 9 files changed, 104 insertions(+), 95 deletions(-) diff --git a/src/ol/Disposable.js b/src/ol/Disposable.js index da4cb6b60f..d7800fa2b8 100644 --- a/src/ol/Disposable.js +++ b/src/ol/Disposable.js @@ -14,15 +14,15 @@ class Disposable { * @type {boolean} * @private */ - this.disposed = false; + this.disposed_ = false; } /** * Clean up. */ dispose() { - if (!this.disposed) { - this.disposed = true; + if (!this.disposed_) { + this.disposed_ = true; this.disposeInternal(); } } diff --git a/src/ol/VectorImageTile.js b/src/ol/VectorImageTile.js index 85ad648f50..a075fa0699 100644 --- a/src/ol/VectorImageTile.js +++ b/src/ol/VectorImageTile.js @@ -143,8 +143,7 @@ class VectorImageTile extends Tile { * @inheritDoc */ disposeInternal() { - this.state = TileState.ABORT; - this.changed(); + this.setState(TileState.ABORT); for (let i = 0, ii = this.tileKeys.length; i < ii; ++i) { const sourceTileKey = this.tileKeys[i]; @@ -176,6 +175,14 @@ class VectorImageTile extends Tile { return this.context_[key]; } + /** + * @param {import("./layer/Layer.js").default} layer Layer. + * @return {boolean} Tile has a rendering context for the given layer. + */ + hasContext(layer) { + return getUid(layer) in this.context_; + } + /** * Get the Canvas for this tile. * @param {import("./layer/Layer.js").default} layer Layer. @@ -262,7 +269,7 @@ class VectorImageTile extends Tile { }.bind(this)); } if (leftToLoad - Object.keys(errorSourceTiles).length == 0) { - setTimeout(this.finishLoading_.bind(this), 0); + setTimeout(this.finishLoading_.bind(this), 16); } } diff --git a/src/ol/VectorTile.js b/src/ol/VectorTile.js index 3e64403891..e7ae5f0acf 100644 --- a/src/ol/VectorTile.js +++ b/src/ol/VectorTile.js @@ -1,7 +1,6 @@ /** * @module ol/VectorTile */ -import {containsExtent} from './extent.js'; import Tile from './Tile.js'; import TileState from './TileState.js'; @@ -147,30 +146,6 @@ class VectorTile extends Tile { return this.executorGroups_[layerId + ',' + key]; } - /** - * Get the best matching lower resolution replay group for a given zoom and extent. - * @param {string} layerId UID of the layer. - * @param {number} zoom Zoom. - * @param {import("./extent").Extent} extent Extent. - * @return {import("./render/canvas/ExecutorGroup.js").default} Executor groups. - */ - getLowResExecutorGroup(layerId, zoom, extent) { - let bestZoom = 0; - let replayGroup = null; - for (const key in this.executorGroups_) { - const keyData = key.split(','); - const candidateZoom = Number(keyData[1]); - if (keyData[0] === layerId && candidateZoom <= zoom) { - const candidate = this.executorGroups_[key]; - if (containsExtent(candidate.getMaxExtent(), extent) && candidateZoom > bestZoom) { - replayGroup = candidate; - bestZoom = candidateZoom; - } - } - } - return replayGroup; - } - /** * @inheritDoc */ diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index 36d921921d..94b8022adc 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -280,13 +280,6 @@ class ExecutorGroup { return flatClipCoords; } - /** - * @return {import("../../extent.js").Extent} The extent of the replay group. - */ - getMaxExtent() { - return this.maxExtent_; - } - /** * @param {number|undefined} zIndex Z index. * @param {import("./BuilderType.js").default} builderType Builder type. diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index 3d20b415ac..3dd5682626 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -48,6 +48,18 @@ class LayerRenderer extends Observable { return abstract(); } + /** + * @param {Object>} tiles Lookup of loaded tiles by zoom level. + * @param {number} zoom Zoom level. + * @param {import("../Tile.js").default} tile Tile. + */ + loadedTileCallback(tiles, zoom, tile) { + if (!tiles[zoom]) { + tiles[zoom] = {}; + } + tiles[zoom][tile.tileCoord.toString()] = tile; + } + /** * Create a function that adds loaded tiles to the tile lookup. * @param {import("../source/Tile.js").default} source Tile source. @@ -63,20 +75,13 @@ class LayerRenderer extends Observable { * @param {number} zoom Zoom level. * @param {import("../TileRange.js").default} tileRange Tile range. * @return {boolean} The tile range is fully loaded. + * @this {LayerRenderer} */ function(zoom, tileRange) { - /** - * @param {import("../Tile.js").default} tile Tile. - */ - function callback(tile) { - if (!tiles[zoom]) { - tiles[zoom] = {}; - } - tiles[zoom][tile.tileCoord.toString()] = tile; - } + const callback = this.loadedTileCallback.bind(this, tiles, zoom); return source.forEachLoadedTile(projection, zoom, tileRange, callback); } - ); + ).bind(this); } /** diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index ef9b0059a6..104ae39ad1 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -65,11 +65,11 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { } /** - * @private + * @protected * @param {import("../../Tile.js").default} tile Tile. * @return {boolean} Tile is drawable. */ - isDrawableTile_(tile) { + isDrawableTile(tile) { const tileLayer = /** @type {import("../../layer/Tile.js").default} */ (this.getLayer()); const tileState = tile.getState(); const useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); @@ -99,7 +99,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { this.newTiles_ = true; } } - if (!this.isDrawableTile_(tile)) { + if (!this.isDrawableTile(tile)) { tile = tile.getInterimTile(); } return tile; @@ -177,7 +177,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { for (let x = tileRange.minX; x <= tileRange.maxX; ++x) { for (let y = tileRange.minY; y <= tileRange.maxY; ++y) { const tile = this.getTile(z, x, y, pixelRatio, projection); - if (this.isDrawableTile_(tile)) { + if (this.isDrawableTile(tile)) { const uid = getUid(this); if (tile.getState() == TileState.LOADED) { tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 4390579207..c788a83788 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -3,6 +3,7 @@ */ import {getUid} from '../../util.js'; import {createCanvasContext2D} from '../../dom.js'; +import {getValues} from '../../obj.js'; import TileState from '../../TileState.js'; import ViewHint from '../../ViewHint.js'; import {listen, unlisten, unlistenByKey} from '../../events.js'; @@ -124,9 +125,15 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { /** * @private - * @type {Object} + * @type {Array} */ - this.tilesToPrepare_ = null; + this.tilesWithoutImage_ = null; + + /** + * @private + * @type {Object= 16) { - break; - } - const args = this.tilesToPrepare_[key]; - delete this.tilesToPrepare_[key]; - const tile = args[0]; - if (!tile.disposed) { - frameState.animate = true; - this.renderTileImage_.apply(this, args); - tile.setState(TileState.LOADED); - } - } - if (Object.keys(this.tilesToPrepare_).length > 0) { - frameState.animate = true; - } else { - this.tilesToPrepare_ = null; - } - } + this.renderMissingTileImages_(hifi, frameState); return this.container_; } + renderMissingTileImages_(hifi, frameState) { + if (hifi) { + while (this.tilesWithoutImage_.length && Date.now() - frameState.time < 100) { + const tile = this.tilesWithoutImage_.pop(); + frameState.animate = true; + this.renderTileImage_(tile, frameState.pixelRatio, frameState.viewState.projection); + } + } + if (this.tilesWithoutImage_.length) { + frameState.animate = true; + } + } + /** * @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {number} squaredTolerance Squared tolerance. diff --git a/test/spec/ol/disposable.test.js b/test/spec/ol/disposable.test.js index 168446f186..21907b9bbd 100644 --- a/test/spec/ol/disposable.test.js +++ b/test/spec/ol/disposable.test.js @@ -12,17 +12,17 @@ describe('ol.Disposable', function() { }); - describe('#disposed', function() { + describe('#disposed_', function() { it('is initially false', function() { const disposable = new Disposable(); - expect(disposable.disposed).to.be(false); + expect(disposable.disposed_).to.be(false); }); it('is true after a call to dispose', function() { const disposable = new Disposable(); disposable.dispose(); - expect(disposable.disposed).to.be(true); + expect(disposable.disposed_).to.be(true); }); }); diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 6801a21231..526be8d3e0 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -75,8 +75,11 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { tileGrid: createXYZ() }); source.getTile = function() { - arguments[1] = TileState.LOADED; const tile = VectorTileSource.prototype.getTile.apply(source, arguments); + tile.hasContext = function() { + return true; + }; + tile.sourceTilesLoaded = true; tile.setState(TileState.LOADED); return tile; }; @@ -242,9 +245,8 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { sourceTile.getImage = function() { return document.createElement('canvas'); }; - const tileUrlFunction = function() {}; const tile = new VectorImageTile([0, 0, 0], undefined, undefined, undefined, - undefined, [0, 0, 0], undefined, createXYZ(), createXYZ(), undefined, undefined, + undefined, [0, 0, 0], undefined, createXYZ(), createXYZ(), {'0,0,0': sourceTile}, undefined, undefined, undefined, undefined); tile.transition_ = 0; tile.wrappedTileCoord = [0, 0, 0]; @@ -256,6 +258,9 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { return tile; }; const renderer = new CanvasVectorTileLayerRenderer(layer); + renderer.isDrawableTile = function() { + return true; + }; const proj = getProjection('EPSG:3857'); const frameState = { extent: proj.getExtent(), From 1d243a7f37f343e153ec73a099c352d01bb27316 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Wed, 28 Nov 2018 21:38:05 +0100 Subject: [PATCH 5/7] Add comments and TODOs --- src/ol/VectorImageTile.js | 1 - src/ol/renderer/canvas/VectorTileLayer.js | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ol/VectorImageTile.js b/src/ol/VectorImageTile.js index a075fa0699..df0d10b4d8 100644 --- a/src/ol/VectorImageTile.js +++ b/src/ol/VectorImageTile.js @@ -144,7 +144,6 @@ class VectorImageTile extends Tile { */ disposeInternal() { this.setState(TileState.ABORT); - for (let i = 0, ii = this.tileKeys.length; i < ii; ++i) { const sourceTileKey = this.tileKeys[i]; const sourceTile = this.getTile(sourceTileKey); diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index c788a83788..73e27b0942 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -170,6 +170,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { unlistenByKey(this.tileChangeKeys_[uid]); delete this.tileChangeKeys_[uid]; if (tile.sourceTilesLoaded) { + // Create render instructions immediately when all source tiles are available. + //TODO Make sure no canvas operations are involved in instruction creation. this.updateExecutorGroup_(tile, pixelRatio, projection); //FIXME This should be done by the tile, and VectorImage tiles should be layer specific tile.setState(TileState.LOADED); @@ -182,6 +184,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (tile.hasContext(this.getLayer())) { this.renderTileImage_(tile, pixelRatio, projection); } else { + // Render new tile images after existing tiles have been drawn to the target canvas. this.tilesWithoutImage_.push(tile); } } @@ -517,13 +520,21 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { canvas.style.opacity = opacity; } + // Now that we have rendered the tiles we have already, let's prepare new tiles for the + // next frame this.renderMissingTileImages_(hifi, frameState); return this.container_; } + /** + * @param {boolean} hifi We have time to render a high fidelity map image. + * @param {import('../../PluggableMap.js').FrameState} frameState Frame state. + */ renderMissingTileImages_(hifi, frameState) { if (hifi) { + // Do not spend more than 100 ms in this render frame, to avoid delays when the user starts + // interacting again with the map. while (this.tilesWithoutImage_.length && Date.now() - frameState.time < 100) { const tile = this.tilesWithoutImage_.pop(); frameState.animate = true; From 6202a0cf057c4633dcfb3135793af35925dc89fa Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 29 Nov 2018 22:25:20 +0100 Subject: [PATCH 6/7] Bring back interim tiles, but don't block user interaction --- src/ol/VectorImageTile.js | 70 +++++++++++++++++++++-- src/ol/renderer/canvas/VectorTileLayer.js | 44 ++++++++++---- 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/ol/VectorImageTile.js b/src/ol/VectorImageTile.js index df0d10b4d8..8e794da597 100644 --- a/src/ol/VectorImageTile.js +++ b/src/ol/VectorImageTile.js @@ -70,6 +70,12 @@ class VectorImageTile extends Tile { */ this.sourceTiles_ = sourceTiles; + /** + * @private + * @type {import("./tilegrid/TileGrid.js").default} + */ + this.sourceTileGrid_ = sourceTileGrid; + /** * @private * @type {boolean} @@ -102,14 +108,19 @@ class VectorImageTile extends Tile { */ this.loadListenerKeys_ = []; + /** + * @type {boolean} + */ + this.isInterimTile = !sourceTileGrid; + /** * @type {Array} */ this.sourceTileListenerKeys_ = []; - if (urlTileCoord) { + if (urlTileCoord && sourceTileGrid) { const extent = this.extent = tileGrid.getTileCoordExtent(urlTileCoord); - const resolution = tileGrid.getResolution(urlTileCoord[0]); + const resolution = this.resolution_ = tileGrid.getResolution(urlTileCoord[0]); const sourceZ = sourceTileGrid.getZForResolution(resolution); sourceTileGrid.forEachTileCoord(extent, sourceZ, function(sourceTileCoord) { let sharedExtent = getIntersection(extent, @@ -143,7 +154,13 @@ class VectorImageTile extends Tile { * @inheritDoc */ disposeInternal() { - this.setState(TileState.ABORT); + if (!this.isInterimTile) { + this.setState(TileState.ABORT); + } + if (this.interimTile) { + this.interimTile.dispose(); + this.interimTile = null; + } for (let i = 0, ii = this.tileKeys.length; i < ii; ++i) { const sourceTileKey = this.tileKeys[i]; const sourceTile = this.getTile(sourceTileKey); @@ -188,8 +205,51 @@ class VectorImageTile extends Tile { * @return {HTMLCanvasElement} Canvas. */ getImage(layer) { - return this.getReplayState(layer).renderedTileRevision == -1 ? - null : this.getContext(layer).canvas; + return this.hasContext(layer) ? this.getContext(layer).canvas : null; + } + + /** + * @override + * @return {VectorImageTile} Interim tile. + */ + getInterimTile() { + const sourceTileGrid = this.sourceTileGrid_; + const state = this.getState(); + if (state < TileState.LOADED && !this.interimTile) { + let z = this.tileCoord[0]; + const minZoom = sourceTileGrid.getMinZoom(); + while (--z > minZoom) { + let covered = true; + const tileKeys = []; + sourceTileGrid.forEachTileCoord(this.extent, z, function(tileCoord) { + const key = tileCoord.toString(); + if (key in this.sourceTiles_ && this.sourceTiles_[key].getState() === TileState.LOADED) { + tileKeys.push(key); + } else { + covered = false; + } + }.bind(this)); + if (covered && tileKeys.length) { + for (let i = 0, ii = tileKeys.length; i < ii; ++i) { + this.sourceTiles_[tileKeys[i]].consumers++; + } + const tile = new VectorImageTile(this.tileCoord, TileState.IDLE, undefined, null, null, + this.wrappedTileCoord, null, null, null, this.sourceTiles_, + undefined, null, null, null); + tile.extent = this.extent; + tile.tileKeys = tileKeys; + tile.context_ = this.context_; + setTimeout(function() { + tile.sourceTilesLoaded = true; + tile.changed(); + }, 16); + this.interimTile = tile; + break; + } + } + } + const interimTile = /** @type {VectorImageTile} */ (this.interimTile); + return state === TileState.LOADED ? this : (interimTile || this); } /** diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 73e27b0942..c97cdc5e47 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -131,7 +131,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { /** * @private - * @type {Object} */ this.tileChangeKeys_ = {}; @@ -158,10 +158,13 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } /** - * @inheritDoc + * Listen to tile changes and mark tile as loaded when source tiles are loaded. + * @param {import("../../VectorImageTile").default} tile Tile to listen on. + * @param {number} pixelRatio Pixel ratio. + * @param {number} projection Projection. + * @private */ - getTile(z, x, y, pixelRatio, projection) { - const tile = /** @type {import("../../VectorImageTile.js").default} */ (super.getTile(z, x, y, pixelRatio, projection)); + listenTileChange_(tile, pixelRatio, projection) { const uid = getUid(tile); if (!(uid in this.tileChangeKeys_) && tile.getState() === TileState.IDLE) { this.tileChangeKeys_[uid] = listen(tile, EventType.CHANGE, function() { @@ -179,9 +182,26 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } }.bind(this)); } + } + + /** + * @inheritDoc + */ + getTile(z, x, y, pixelRatio, projection) { + const tile = /** @type {import("../../VectorImageTile.js").default} */ (super.getTile(z, x, y, pixelRatio, projection)); + this.listenTileChange_(tile, pixelRatio, projection); + if (tile.isInterimTile) { + // Register change listener also on the original tile + const source = /** @type {import("../../source/VectorTile").default} */ (this.getLayer().getSource()); + const originalTile = /** @type {import("../../VectorImageTile").default} */ (source.getTile(z, x, y, pixelRatio, projection)); + this.listenTileChange_(originalTile, pixelRatio, projection); + } if (tile.getState() === TileState.LOADED) { + // Update existing instructions if necessary (e.g. when the style has changed) this.updateExecutorGroup_(tile, pixelRatio, projection); - if (tile.hasContext(this.getLayer())) { + const layer = this.getLayer(); + if (tile.getReplayState(layer).renderedTileRevision !== -1) { + // Update existing tile image if necessary (e.g. when the style has changed) this.renderTileImage_(tile, pixelRatio, projection); } else { // Render new tile images after existing tiles have been drawn to the target canvas. @@ -532,12 +552,14 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @param {import('../../PluggableMap.js').FrameState} frameState Frame state. */ renderMissingTileImages_(hifi, frameState) { - if (hifi) { - // Do not spend more than 100 ms in this render frame, to avoid delays when the user starts - // interacting again with the map. - while (this.tilesWithoutImage_.length && Date.now() - frameState.time < 100) { - const tile = this.tilesWithoutImage_.pop(); - frameState.animate = true; + // Even when we have time to render hifi, do not spend more than 100 ms in this render frame, + // to avoid delays when the user starts interacting again with the map. + while (this.tilesWithoutImage_.length && Date.now() - frameState.time < 100) { + frameState.animate = true; + const tile = this.tilesWithoutImage_.pop(); + // When we don't have time to render hifi, only render interim tiles until we have used up + // half of the frame budget of 16 ms + if (hifi || (tile.isInterimTile && Date.now() - frameState.time < 8)) { this.renderTileImage_(tile, frameState.pixelRatio, frameState.viewState.projection); } } From df59b894b13fcd7919841ec2acf493a72b4f0b8a Mon Sep 17 00:00:00 2001 From: ahocevar Date: Tue, 4 Dec 2018 00:18:14 +0100 Subject: [PATCH 7/7] Make tile keys stable to avoid TileQueue confusion --- src/ol/VectorImageTile.js | 16 +++------------- .../ol/renderer/canvas/vectortilelayer.test.js | 4 ++-- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/ol/VectorImageTile.js b/src/ol/VectorImageTile.js index 8e794da597..da9ebd1655 100644 --- a/src/ol/VectorImageTile.js +++ b/src/ol/VectorImageTile.js @@ -93,11 +93,6 @@ class VectorImageTile extends Tile { */ this.extent = null; - /** - * @type {number} - */ - this.sourceRevision_ = sourceRevision; - /** * @type {import("./tilecoord.js").TileCoord} */ @@ -118,6 +113,8 @@ class VectorImageTile extends Tile { */ this.sourceTileListenerKeys_ = []; + this.key = sourceRevision.toString(); + if (urlTileCoord && sourceTileGrid) { const extent = this.extent = tileGrid.getTileCoordExtent(urlTileCoord); const resolution = this.resolution_ = tileGrid.getResolution(urlTileCoord[0]); @@ -233,7 +230,7 @@ class VectorImageTile extends Tile { for (let i = 0, ii = tileKeys.length; i < ii; ++i) { this.sourceTiles_[tileKeys[i]].consumers++; } - const tile = new VectorImageTile(this.tileCoord, TileState.IDLE, undefined, null, null, + const tile = new VectorImageTile(this.tileCoord, TileState.IDLE, Number(this.key), null, null, this.wrappedTileCoord, null, null, null, this.sourceTiles_, undefined, null, null, null); tile.extent = this.extent; @@ -269,13 +266,6 @@ class VectorImageTile extends Tile { return this.replayState_[key]; } - /** - * @inheritDoc - */ - getKey() { - return this.tileKeys.join('/') + '-' + this.sourceRevision_; - } - /** * @param {string} tileKey Key (tileCoord) of the source tile. * @return {import("./VectorTile.js").default} Source tile. diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 526be8d3e0..5521b586af 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -245,7 +245,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { sourceTile.getImage = function() { return document.createElement('canvas'); }; - const tile = new VectorImageTile([0, 0, 0], undefined, undefined, undefined, + const tile = new VectorImageTile([0, 0, 0], undefined, 1, undefined, undefined, [0, 0, 0], undefined, createXYZ(), createXYZ(), {'0,0,0': sourceTile}, undefined, undefined, undefined, undefined); tile.transition_ = 0; @@ -338,7 +338,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { } }; frameState.layerStates[getUid(layer)] = {}; - renderer.renderedTiles = [new TileClass([0, 0, -1])]; + renderer.renderedTiles = [new TileClass([0, 0, -1], undefined, 1)]; renderer.forEachFeatureAtCoordinate( coordinate, frameState, 0, spy, undefined); expect(spy.callCount).to.be(1);