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