diff --git a/src/ol/VectorImageTile.js b/src/ol/VectorImageTile.js index 1e16db4599..8e0ea79d23 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): void} 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}); @@ -72,6 +70,18 @@ class VectorImageTile extends Tile { */ this.sourceTiles_ = sourceTiles; + /** + * @private + * @type {import("./tilegrid/TileGrid.js").default} + */ + this.sourceTileGrid_ = sourceTileGrid; + + /** + * @private + * @type {boolean} + */ + this.sourceTilesLoaded = false; + /** * Keys of source tiles used by this tile. Use with {@link #getTile}. * @type {Array} @@ -83,11 +93,6 @@ class VectorImageTile extends Tile { */ this.extent = null; - /** - * @type {number} - */ - this.sourceRevision_ = sourceRevision; - /** * @type {import("./tilecoord.js").TileCoord} */ @@ -98,23 +103,22 @@ class VectorImageTile extends Tile { */ this.loadListenerKeys_ = []; + /** + * @type {boolean} + */ + this.isInterimTile = !sourceTileGrid; + /** * @type {Array} */ this.sourceTileListenerKeys_ = []; - /** - * Use only source tiles that are loaded already - * @type {boolean} - */ - this.useLoadedOnly = zoom != tileCoord[0]; + this.key = sourceRevision.toString(); - if (urlTileCoord) { + if (urlTileCoord && sourceTileGrid) { const extent = this.extent = tileGrid.getTileCoordExtent(urlTileCoord); - const resolution = tileGrid.getResolution(zoom); + const resolution = this.resolution_ = tileGrid.getResolution(urlTileCoord[0]); const sourceZ = sourceTileGrid.getZForResolution(resolution); - const useLoadedOnly = this.useLoadedOnly; - let loadCount = 0; sourceTileGrid.forEachTileCoord(extent, sourceZ, function(sourceTileCoord) { let sharedExtent = getIntersection(extent, sourceTileGrid.getTileCoordExtent(sourceTileCoord)); @@ -125,10 +129,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 && !useLoadedOnly) { + if (!sourceTile) { const tileUrl = tileUrlFunction(sourceTileCoord, pixelRatio, projection); sourceTile = sourceTiles[sourceTileKey] = new tileClass(sourceTileCoord, tileUrl == undefined ? TileState.EMPTY : TileState.IDLE, @@ -137,61 +140,24 @@ class VectorImageTile extends Tile { this.sourceTileListenerKeys_.push( listen(sourceTile, EventType.CHANGE, handleTileChange)); } - if (sourceTile && (!useLoadedOnly || sourceTile.getState() == TileState.LOADED)) { - sourceTile.consumers++; - this.tileKeys.push(sourceTileKey); - } + sourceTile.consumers++; + this.tileKeys.push(sourceTileKey); } }.bind(this)); - - if (useLoadedOnly && loadCount == this.tileKeys.length) { - this.finishLoading_(); - } - - this.createInterimTile_ = function() { - if (this.getState() !== TileState.LOADED && !useLoadedOnly) { - 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) { - bestZoom = sourceTileCoord[0]; - } - } - } - if (bestZoom !== -1) { - const tile = new VectorImageTile(tileCoord, state, sourceRevision, - format, tileLoadFunction, urlTileCoord, tileUrlFunction, - sourceTileGrid, tileGrid, sourceTiles, pixelRatio, projection, - tileClass, VOID, bestZoom); - this.interimTile = tile; - } - } - }; } - - } - - getInterimTile() { - if (!this.interimTile) { - this.createInterimTile_(); - } - return super.getInterimTile(); } /** * @inheritDoc */ disposeInternal() { - delete this.createInterimTile_; - this.state = TileState.ABORT; - this.changed(); + 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); @@ -222,14 +188,65 @@ 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. * @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, Number(this.key), 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); } /** @@ -249,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. @@ -308,7 +318,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); } } @@ -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..e7ae5f0acf 100644 --- a/src/ol/VectorTile.js +++ b/src/ol/VectorTile.js @@ -1,8 +1,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,37 +138,12 @@ 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]; - } - - /** - * Get the best matching lower resolution replay group for a given zoom and extent. - * @param {import("./layer/Layer").default} layer 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); - 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; + getExecutorGroup(layerId, key) { + return this.executorGroups_[layerId + ',' + key]; } /** @@ -242,12 +215,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/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 08eb3a27de..571aac80de 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -3,9 +3,10 @@ */ 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} 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'; @@ -122,6 +123,18 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ this.renderedLayerRevision_; + /** + * @private + * @type {Array} + */ + this.tilesWithoutImage_ = null; + + /** + * @private + * @type {Object} + */ + this.tileChangeKeys_ = {}; + /** * @private * @type {import("../../transform.js").Transform} @@ -140,29 +153,88 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ disposeInternal() { unlisten(labelCache, EventType.CLEAR, this.handleFontsChanged_, this); + getValues(this.tileChangeKeys_).forEach(unlistenByKey); super.disposeInternal(); } + /** + * 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 + */ + 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() { + const state = tile.getState(); + if (state === TileState.ABORT || tile.sourceTilesLoaded) { + 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); + } + } + }.bind(this)); + } + } + /** * @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)); + 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) { - this.createExecutorGroup_(/** @type {import("../../VectorImageTile.js").default} */ (tile), pixelRatio, projection); - if (this.context) { - this.renderTileImage_(/** @type {import("../../VectorImageTile.js").default} */ (tile), pixelRatio, projection); + // Update existing instructions if necessary (e.g. when the style has changed) + this.updateExecutorGroup_(tile, pixelRatio, projection); + 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. + this.tilesWithoutImage_.push(tile); } } return tile; } + /** + * @inheritDoc + */ + loadedTileCallback(tiles, zoom, tile) { + if (!tile.hasContext(this.getLayer())) { + this.tilesWithoutImage_.push(tile); + return false; + } + return super.loadedTileCallback(tiles, zoom, tile); + } + + /** + * @inheritdoc + */ + isDrawableTile(tile) { + return super.isDrawableTile(tile) && tile.hasContext(this.getLayer()); + } + /** * @inheritDoc */ 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); } /** @@ -184,7 +256,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; @@ -207,15 +279,6 @@ 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; - } - } - const sourceTileCoord = sourceTile.tileCoord; const sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord); const sharedExtent = getIntersection(tileExtent, sourceTileExtent); @@ -271,7 +334,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 +366,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, {}, /** @@ -374,11 +437,15 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @inheritDoc */ renderFrame(frameState, layerState) { + this.tilesWithoutImage_ = []; super.renderFrame(frameState, layerState); const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); + const viewHints = frameState.viewHints; + const hifi = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); const renderMode = layer.getRenderMode(); if (renderMode === VectorTileRenderType.IMAGE) { + this.renderMissingTileImages_(hifi, frameState); return this.container_; } @@ -412,8 +479,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (declutterReplays) { this.declutterTree_.clear(); } - const viewHints = frameState.viewHints; - const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); const tiles = this.renderedTiles; const tileGrid = source.getTileGridForProjection(frameState.viewState.projection); const clips = []; @@ -431,7 +496,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 @@ -460,14 +525,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,9 +540,34 @@ 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) { + // 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); + } + } + if (this.tilesWithoutImage_.length) { + frameState.animate = true; + } + } + /** * @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {number} squaredTolerance Squared tolerance. @@ -536,7 +626,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); } 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..5521b586af 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,10 +245,9 @@ 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], tileUrlFunction, createXYZ(), createXYZ(), {}, undefined, - undefined, VectorTile, undefined, 0); + 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; tile.wrappedTileCoord = [0, 0, 0]; tile.setState(TileState.LOADED); @@ -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(), @@ -333,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); diff --git a/test/spec/ol/vectorimagetile.test.js b/test/spec/ol/vectorimagetile.test.js index e5f7809f68..b1b093a68b 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]); @@ -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; @@ -41,13 +41,17 @@ 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; 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 { @@ -65,7 +69,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 +85,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 +109,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 +122,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); @@ -131,18 +135,19 @@ 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, 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() { - 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();