Simplify vector tile code

This commit is contained in:
ahocevar
2018-12-31 00:34:56 +01:00
parent ab797b7160
commit 32696638d2
10 changed files with 396 additions and 462 deletions

View File

@@ -5,6 +5,7 @@ import TileState from './TileState.js';
import {easeIn} from './easing.js';
import EventTarget from './events/Target.js';
import EventType from './events/EventType.js';
import {abstract} from './util.js';
/**
@@ -105,6 +106,14 @@ 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
@@ -240,7 +249,9 @@ class Tile extends EventTarget {
* @abstract
* @api
*/
load() {}
load() {
abstract();
}
/**
* Get the alpha value for rendering.

View File

@@ -82,7 +82,7 @@ class TileQueue extends PriorityQueue {
handleTileChange(event) {
const tile = /** @type {import("./Tile.js").default} */ (event.target);
const state = tile.getState();
if (state === TileState.LOADED || state === TileState.ERROR ||
if (tile.hifi && state === TileState.LOADED || state === TileState.ERROR ||
state === TileState.EMPTY || state === TileState.ABORT) {
unlisten(tile, EventType.CHANGE, this.handleTileChange, this);
const tileKey = tile.getKey();

View File

@@ -5,9 +5,6 @@ import {getUid} from './util.js';
import Tile from './Tile.js';
import TileState from './TileState.js';
import {createCanvasContext2D} from './dom.js';
import {listen, unlistenByKey} from './events.js';
import EventType from './events/EventType.js';
import {loadFeaturesXhr} from './featureloader.js';
/**
@@ -16,6 +13,8 @@ import {loadFeaturesXhr} from './featureloader.js';
* @property {null|import("./render.js").OrderFunction} renderedRenderOrder
* @property {number} renderedTileRevision
* @property {number} renderedRevision
* @property {number} renderedZ
* @property {number} renderedTileZ
*/
@@ -26,9 +25,12 @@ class VectorImageTile extends Tile {
* @param {TileState} state State.
* @param {import("./tilecoord.js").TileCoord} urlTileCoord Wrapped tile coordinate for source urls.
* @param {import("./tilegrid/TileGrid.js").default} sourceTileGrid Tile grid of the source.
* @param {Object<string, import("./VectorTile.js").default>} sourceTiles Source tiles.
* @param {function(VectorImageTile):Array<import("./VectorTile").default>} getSourceTiles Function
* to get an source tiles for this tile.
* @param {function(VectorImageTile):void} removeSourceTiles Function to remove this tile from its
* source tiles's consumer count.
*/
constructor(tileCoord, state, urlTileCoord, sourceTileGrid, sourceTiles) {
constructor(tileCoord, state, urlTileCoord, sourceTileGrid, getSourceTiles, removeSourceTiles) {
super(tileCoord, state, {transition: 0});
@@ -39,10 +41,22 @@ class VectorImageTile extends Tile {
this.context_ = {};
/**
* @private
* @type {import("./featureloader.js").FeatureLoader}
* Executor groups by layer uid. Entries are read/written by the renderer.
* @type {Object<string, Array<import("./render/canvas/ExecutorGroup.js").default>>}
*/
this.loader_;
this.executorGroups = {};
/**
* Number of loading source tiles. Read/written by the source.
* @type {number}
*/
this.loadingSourceTiles = 0;
/**
* Tile keys of error source tiles. Read/written by the source.
* @type {Object<string, boolean>}
*/
this.errorSourceTileKeys = {};
/**
* @private
@@ -51,10 +65,14 @@ class VectorImageTile extends Tile {
this.replayState_ = {};
/**
* @private
* @type {Object<string, import("./VectorTile.js").default>}
* @type {!function(import("./VectorImageTile.js").default):Array<import("./VectorTile.js").default>}
*/
this.sourceTiles_ = sourceTiles;
this.getSourceTiles_ = getSourceTiles;
/**
* @type {!function(import("./VectorImageTile.js").default):void}
*/
this.removeSourceTiles_ = removeSourceTiles;
/**
* @private
@@ -63,69 +81,29 @@ class VectorImageTile extends Tile {
this.sourceTileGrid_ = sourceTileGrid;
/**
* @private
* 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.sourceTilesLoaded = false;
/**
* Keys of source tiles used by this tile. Use with {@link #getTile}.
* @type {Array<string>}
*/
this.tileKeys = [];
/**
* @type {import("./extent.js").Extent}
*/
this.extent = null;
this.hifi = false;
/**
* @type {import("./tilecoord.js").TileCoord}
*/
this.wrappedTileCoord = urlTileCoord;
/**
* @type {Array<import("./events.js").EventsKey>}
*/
this.loadListenerKeys_ = [];
/**
* @type {boolean}
*/
this.isInterimTile = !sourceTileGrid;
/**
* @type {Array<import("./events.js").EventsKey>}
*/
this.sourceTileListenerKeys_ = [];
}
/**
* @inheritDoc
*/
disposeInternal() {
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);
sourceTile.consumers--;
if (sourceTile.consumers == 0) {
delete this.sourceTiles_[sourceTileKey];
sourceTile.dispose();
}
}
this.tileKeys.length = 0;
this.sourceTiles_ = null;
this.loadListenerKeys_.forEach(unlistenByKey);
this.loadListenerKeys_.length = 0;
this.sourceTileListenerKeys_.forEach(unlistenByKey);
this.sourceTileListenerKeys_.length = 0;
this.removeSourceTiles_(this);
this.setState(TileState.ABORT);
super.disposeInternal();
}
@@ -158,50 +136,6 @@ class VectorImageTile extends Tile {
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,
this.wrappedTileCoord, null, this.sourceTiles_);
tile.extent = this.extent;
tile.key = this.key;
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);
}
/**
* @param {import("./layer/Layer.js").default} layer Layer.
* @return {ReplayState} The replay state.
@@ -213,103 +147,22 @@ class VectorImageTile extends Tile {
dirty: false,
renderedRenderOrder: null,
renderedRevision: -1,
renderedTileRevision: -1
renderedTileRevision: -1,
renderedZ: -1,
renderedTileZ: -1
};
}
return this.replayState_[key];
}
/**
* @param {string} tileKey Key (tileCoord) of the source tile.
* @return {import("./VectorTile.js").default} Source tile.
*/
getTile(tileKey) {
return this.sourceTiles_[tileKey];
}
/**
* @inheritDoc
* @return {Array<import("./VectorTile.js").default>} Source tiles for this tile.
*/
load() {
// Source tiles with LOADED state - we just count them because once they are
// loaded, we're no longer listening to state changes.
let leftToLoad = 0;
// Source tiles with ERROR state - we track them because they can still have
// an ERROR state after another load attempt.
const errorSourceTiles = {};
if (this.state == TileState.IDLE) {
this.setState(TileState.LOADING);
}
if (this.state == TileState.LOADING) {
this.tileKeys.forEach(function(sourceTileKey) {
const sourceTile = this.getTile(sourceTileKey);
if (sourceTile.state == TileState.IDLE) {
sourceTile.setLoader(this.loader_);
sourceTile.load();
}
if (sourceTile.state == TileState.LOADING) {
const key = listen(sourceTile, EventType.CHANGE, function(e) {
const state = sourceTile.getState();
if (state == TileState.LOADED ||
state == TileState.ERROR) {
const uid = getUid(sourceTile);
if (state == TileState.ERROR) {
errorSourceTiles[uid] = true;
} else {
--leftToLoad;
delete errorSourceTiles[uid];
}
if (leftToLoad - Object.keys(errorSourceTiles).length == 0) {
this.finishLoading_();
}
}
}.bind(this));
this.loadListenerKeys_.push(key);
++leftToLoad;
}
}.bind(this));
}
if (leftToLoad - Object.keys(errorSourceTiles).length == 0) {
setTimeout(this.finishLoading_.bind(this), 16);
}
}
/**
* @private
*/
finishLoading_() {
let loaded = this.tileKeys.length;
let empty = 0;
for (let i = loaded - 1; i >= 0; --i) {
const state = this.getTile(this.tileKeys[i]).getState();
if (state != TileState.LOADED) {
--loaded;
}
if (state == TileState.EMPTY) {
++empty;
}
}
if (loaded == this.tileKeys.length) {
this.loadListenerKeys_.forEach(unlistenByKey);
this.loadListenerKeys_.length = 0;
this.sourceTilesLoaded = true;
this.changed();
} else {
this.setState(empty == this.tileKeys.length ? TileState.EMPTY : TileState.ERROR);
}
return this.getSourceTiles_(this);
}
}
export default VectorImageTile;
/**
* Sets the loader for a tile.
* @param {import("./VectorTile.js").default} tile Vector tile.
* @param {string} url URL.
*/
export function defaultLoadFunction(tile, url) {
const loader = loadFeaturesXhr(url, tile.getFormat(), tile.onLoad.bind(tile), tile.onError.bind(tile));
tile.setLoader(loader);
}

View File

@@ -61,12 +61,6 @@ class VectorTile extends Tile {
*/
this.projection_ = null;
/**
* @private
* @type {Object<string, import("./render/canvas/ExecutorGroup.js").default>}
*/
this.executorGroups_ = {};
/**
* @private
* @type {import("./Tile.js").LoadFunction}
@@ -85,10 +79,7 @@ class VectorTile extends Tile {
* @inheritDoc
*/
disposeInternal() {
this.features_ = null;
this.executorGroups_ = {};
this.state = TileState.ABORT;
this.changed();
this.setState(TileState.ABORT);
super.disposeInternal();
}
@@ -137,15 +128,6 @@ class VectorTile extends Tile {
return this.projection_;
}
/**
* @param {string} layerId UID of the layer.
* @param {string} key Key.
* @return {import("./render/canvas/ExecutorGroup.js").default} Executor group.
*/
getExecutorGroup(layerId, key) {
return this.executorGroups_[layerId + ',' + key];
}
/**
* @inheritDoc
*/
@@ -214,15 +196,6 @@ class VectorTile extends Tile {
this.projection_ = projection;
}
/**
* @param {string} layerId UID of the layer.
* @param {string} key Key.
* @param {import("./render/canvas/ExecutorGroup.js").default} executorGroup Executor group.
*/
setExecutorGroup(layerId, key, executorGroup) {
this.executorGroups_[layerId + ',' + key] = executorGroup;
}
/**
* Set the feature loader for reading this tile's features.
* @param {import("./featureloader.js").FeatureLoader} loader Feature loader.

View File

@@ -3,7 +3,6 @@
*/
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';
@@ -30,6 +29,7 @@ import {
makeInverse
} from '../../transform.js';
import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
import {isEmpty} from '../../obj.js';
/**
@@ -125,15 +125,14 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
/**
* @private
* @type {Array<import("../../VectorImageTile.js").default>}
* @type {!Object<string, import("../../VectorImageTile.js").default>}
*/
this.tilesWithoutImage_ = null;
this.renderTileImageQueue_ = {};
/**
* @private
* @type {Object<string, import("../../events").EventsKey>}
* @type {Object<string, import("../../events.js").EventsKey>}
*/
this.tileChangeKeys_ = {};
this.tileListenerKeys_ = {};
/**
* @private
@@ -153,34 +152,23 @@ 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 {import("../../VectorImageTile.js").default} tile Tile.
* @param {number} pixelRatio Pixel ratio.
* @param {number} projection Projection.
* @private
* @param {import("../../proj/Projection").default} projection 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() {
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));
prepareTile(tile, pixelRatio, projection) {
const tileUid = getUid(tile);
if (tile.getState() === TileState.LOADED || tile.getState() === TileState.ERROR) {
unlistenByKey(this.tileListenerKeys_[tileUid]);
delete this.tileListenerKeys_[tileUid];
this.updateExecutorGroup_(tile, pixelRatio, projection);
if (this.tileImageNeedsRender_(tile, pixelRatio, projection)) {
this.renderTileImageQueue_[tileUid] = tile;
}
}
}
@@ -189,39 +177,17 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
*/
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);
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);
this.prepareTile(tile, pixelRatio, projection);
if (tile.getState() < TileState.LOADED) {
const tileUid = getUid(tile);
if (!(tileUid in this.tileListenerKeys_)) {
const listenerKey = listen(tile, EventType.CHANGE, this.prepareTile.bind(this, tile, pixelRatio, projection));
this.tileListenerKeys_[tileUid] = listenerKey;
}
}
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
*/
@@ -263,7 +229,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const builderState = tile.getReplayState(layer);
if (!builderState.dirty && builderState.renderedRevision == revision &&
builderState.renderedRenderOrder == renderOrder) {
builderState.renderedRenderOrder == renderOrder && builderState.renderedZ === tile.sourceZ) {
return;
}
@@ -272,10 +238,13 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const tileGrid = source.getTileGridForProjection(projection);
const zoom = tile.tileCoord[0];
const resolution = tileGrid.getResolution(zoom);
const tileExtent = tile.extent;
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
const sourceTile = tile.getTile(tile.tileKeys[t]);
const sourceTiles = tile.load();
const layerUid = getUid(layer);
tile.executorGroups[layerUid] = [];
for (let t = 0, tt = sourceTiles.length; t < tt; ++t) {
const sourceTile = sourceTiles[t];
if (sourceTile.getState() != TileState.LOADED) {
continue;
}
@@ -334,9 +303,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const executorGroupInstructions = builderGroup.finish();
const renderingReplayGroup = new CanvasExecutorGroup(sharedExtent, resolution,
pixelRatio, source.getOverlaps(), this.declutterTree_, executorGroupInstructions, layer.getRenderBuffer());
sourceTile.setExecutorGroup(getUid(layer), tile.tileCoord.toString(), renderingReplayGroup);
tile.executorGroups[layerUid].push(renderingReplayGroup);
}
builderState.renderedRevision = revision;
builderState.renderedZ = tile.sourceZ;
builderState.renderedRenderOrder = renderOrder;
}
@@ -348,6 +318,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const rotation = frameState.viewState.rotation;
hitTolerance = hitTolerance == undefined ? 0 : hitTolerance;
const layer = this.getLayer();
const source = /** @type {import("../../source/VectorTile").default} */ (layer.getSource());
const tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
/** @type {!Object<string, boolean>} */
const features = {};
@@ -357,17 +329,14 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
let i, ii;
for (i = 0, ii = renderedTiles.length; i < ii; ++i) {
const tile = renderedTiles[i];
bufferedExtent = buffer(tile.extent, hitTolerance * resolution, bufferedExtent);
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
bufferedExtent = buffer(tileExtent, hitTolerance * resolution, bufferedExtent);
if (!containsCoordinate(bufferedExtent, coordinate)) {
continue;
}
for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
const sourceTile = tile.getTile(tile.tileKeys[t]);
if (sourceTile.getState() != TileState.LOADED) {
continue;
}
const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(getUid(layer),
tile.tileCoord.toString()));
const executorGroups = tile.executorGroups[getUid(layer)];
for (let t = 0, tt = executorGroups.length; t < tt; ++t) {
const executorGroup = executorGroups[t];
found = found || executorGroup.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {},
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
@@ -437,7 +406,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
* @inheritDoc
*/
renderFrame(frameState, layerState) {
this.tilesWithoutImage_ = [];
super.renderFrame(frameState, layerState);
const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer());
@@ -445,7 +413,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const hifi = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);
const renderMode = layer.getRenderMode();
if (renderMode === VectorTileRenderType.IMAGE) {
this.renderMissingTileImages_(hifi, frameState);
this.renderTileImages_(hifi, frameState);
return this.container_;
}
@@ -489,20 +457,18 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
continue;
}
const tileCoord = tile.tileCoord;
const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tile.extent[0];
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tileExtent[0];
const transform = this.getRenderTransform(frameState, width, height, worldOffset);
for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
const sourceTile = tile.getTile(tile.tileKeys[t]);
if (sourceTile.getState() != TileState.LOADED) {
continue;
}
const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(getUid(layer), tileCoord.toString()));
if (!executorGroup || !executorGroup.hasExecutors(replayTypes)) {
const executorGroups = tile.executorGroups[getUid(layer)];
for (let t = 0, tt = executorGroups.length; t < tt; ++t) {
const executorGroup = executorGroups[t];
if (!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
continue;
}
const currentZ = sourceTile.tileCoord[0];
const currentZ = tile.tileCoord[0];
const currentClip = executorGroup.getClipCoords(transform);
context.save();
@@ -540,9 +506,9 @@ 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);
// Now that we have rendered the tiles we have already, let's prepare new tile images
// for the next frame
this.renderTileImages_(hifi, frameState);
return this.container_;
}
@@ -551,19 +517,22 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
* @param {boolean} hifi We have time to render a high fidelity map image.
* @param {import('../../PluggableMap.js').FrameState} frameState Frame state.
*/
renderMissingTileImages_(hifi, frameState) {
renderTileImages_(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);
// When we don't have time to render hifi, only render lowres tiles until we have used up
// half of the frame budget of 16 ms
for (const uid in this.renderTileImageQueue_) {
if (Date.now() - frameState.time > (hifi ? 100 : 8)) {
break;
}
const tile = this.renderTileImageQueue_[uid];
frameState.animate = true;
delete this.renderTileImageQueue_[uid];
this.renderTileImage_(tile, frameState.pixelRatio, frameState.viewState.projection);
}
if (this.tilesWithoutImage_.length) {
if (!isEmpty(this.renderTileImageQueue_)) {
// If there's items left in the queue, render them in another frame
frameState.animate = true;
}
}
@@ -594,6 +563,21 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
return loading;
}
/**
* @param {import("../../VectorImageTile.js").default} tile Tile.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../../proj/Projection.js").default} projection Projection.
* @return {boolean} A new tile image was rendered.
* @private
*/
tileImageNeedsRender_(tile, pixelRatio, projection) {
const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer());
const replayState = tile.getReplayState(layer);
const revision = layer.getRevision();
const sourceZ = tile.sourceZ;
return replayState.renderedTileRevision !== revision || replayState.renderedTileZ !== sourceZ;
}
/**
* @param {import("../../VectorImageTile.js").default} tile Tile.
* @param {number} pixelRatio Pixel ratio.
@@ -604,32 +588,26 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer());
const replayState = tile.getReplayState(layer);
const revision = layer.getRevision();
const replays = IMAGE_REPLAYS[layer.getRenderMode()];
if (replays && replayState.renderedTileRevision !== revision) {
replayState.renderedTileRevision = revision;
const tileCoord = tile.wrappedTileCoord;
const z = tileCoord[0];
const source = /** @type {import("../../source/VectorTile.js").default} */ (layer.getSource());
const tileGrid = source.getTileGridForProjection(projection);
const resolution = tileGrid.getResolution(z);
const context = tile.getContext(layer);
const size = source.getTilePixelSize(z, pixelRatio, projection);
context.canvas.width = size[0];
context.canvas.height = size[1];
const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent);
for (let i = 0, ii = tile.tileKeys.length; i < ii; ++i) {
const sourceTile = tile.getTile(tile.tileKeys[i]);
if (sourceTile.getState() != TileState.LOADED) {
continue;
}
const pixelScale = pixelRatio / resolution;
const transform = resetTransform(this.tmpTransform_);
scaleTransform(transform, pixelScale, -pixelScale);
translateTransform(transform, -tileExtent[0], -tileExtent[3]);
const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(getUid(layer),
tile.tileCoord.toString()));
executorGroup.execute(context, transform, 0, {}, true, replays);
}
const executorGroups = tile.executorGroups[getUid(layer)];
replayState.renderedTileRevision = revision;
replayState.renderedTileZ = tile.sourceZ;
const tileCoord = tile.wrappedTileCoord;
const z = tileCoord[0];
const source = /** @type {import("../../source/VectorTile.js").default} */ (layer.getSource());
const tileGrid = source.getTileGridForProjection(projection);
const resolution = tileGrid.getResolution(z);
const context = tile.getContext(layer);
const size = source.getTilePixelSize(z, pixelRatio, projection);
context.canvas.width = size[0];
context.canvas.height = size[1];
const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent);
for (let i = 0, ii = executorGroups.length; i < ii; ++i) {
const executorGroup = executorGroups[i];
const pixelScale = pixelRatio / resolution;
const transform = resetTransform(this.tmpTransform_);
scaleTransform(transform, pixelScale, -pixelScale);
translateTransform(transform, -tileExtent[0], -tileExtent[3]);
executorGroup.execute(context, transform, 0, {}, true, IMAGE_REPLAYS[layer.getRenderMode()]);
}
}

View File

@@ -3,15 +3,18 @@
*/
import TileState from '../TileState.js';
import VectorImageTile, {defaultLoadFunction} from '../VectorImageTile.js';
import VectorImageTile from '../VectorImageTile.js';
import Tile from '../VectorTile.js';
import {toSize} from '../size.js';
import UrlTile from './UrlTile.js';
import {getKeyZXY} from '../tilecoord.js';
import {getKeyZXY, getKey} from '../tilecoord.js';
import {createXYZ, extentFromProjection, createForProjection} from '../tilegrid.js';
import {getIntersection, getWidth, getHeight} from '../extent.js';
import {listen} from '../events.js';
import {buffer as bufferExtent, getIntersection} from '../extent.js';
import {listen, unlistenByKey} from '../events.js';
import EventType from '../events/EventType.js';
import {loadFeaturesXhr} from '../featureloader.js';
import {isEmpty} from '../obj.js';
import {equals} from '../array.js';
/**
* @typedef {Object} Options
@@ -113,11 +116,22 @@ class VectorTile extends UrlTile {
this.format_ = options.format ? options.format : null;
/**
* @private
* @type {Object<string, Tile>}
*/
* @type {Object<string, import("./VectorTile").default>}
*/
this.loadingTiles_ = {};
/**
* @private
* @type {Object<string, import("../VectorTile.js").default>}
*/
this.sourceTiles_ = {};
/**
* @private
* @type {Object<string, Array<import("../VectorTile.js").default>>}
*/
this.sourceTilesByTileKey_ = {};
/**
* @private
* @type {boolean}
@@ -152,49 +166,139 @@ class VectorTile extends UrlTile {
clear() {
this.tileCache.clear();
this.sourceTiles_ = {};
this.sourceTilesByTileKey_ = {};
}
/**
* Finds and assigns source tiles for a vector image tile.
* @param {VectorImageTile} tile Tile.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../proj/Projection").default} projection Projection.
* @param {VectorImageTile} tile Vector image tile.
* @return {Array<import("../VectorTile").default>} Tile keys.
*/
assignTiles(tile, pixelRatio, projection) {
if (!tile.wrappedTileCoord) {
return;
}
const sourceTileGrid = this.tileGrid;
const tileGrid = this.getTileGridForProjection(projection);
getSourceTiles(pixelRatio, projection, tile) {
const sourceTiles = [];
const urlTileCoord = tile.wrappedTileCoord;
const extent = tile.extent = tileGrid.getTileCoordExtent(urlTileCoord);
const resolution = this.resolution_ = tileGrid.getResolution(urlTileCoord[0]);
const sourceZ = sourceTileGrid.getZForResolution(resolution);
sourceTileGrid.forEachTileCoord(extent, sourceZ, function(sourceTileCoord) {
let sharedExtent = getIntersection(extent,
sourceTileGrid.getTileCoordExtent(sourceTileCoord));
if (urlTileCoord) {
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, -1 / resolution, extent);
const sourceTileGrid = this.tileGrid;
const sourceExtent = sourceTileGrid.getExtent();
if (sourceExtent) {
sharedExtent = getIntersection(sharedExtent, sourceExtent, sharedExtent);
getIntersection(extent, sourceTileGrid.getExtent(), extent);
}
if (getWidth(sharedExtent) / resolution >= 0.5 &&
getHeight(sharedExtent) / resolution >= 0.5) {
// only include source tile if overlap is at least 1 pixel
const sourceTileKey = sourceTileCoord.toString();
let sourceTile = this.sourceTiles_[sourceTileKey];
if (!sourceTile) {
const tileUrl = this.tileUrlFunction(sourceTileCoord, pixelRatio, projection);
sourceTile = this.sourceTiles_[sourceTileKey] = new this.tileClass(sourceTileCoord,
tileUrl == undefined ? TileState.EMPTY : TileState.IDLE,
tileUrl == undefined ? '' : tileUrl,
this.format_, this.tileLoadFunction);
tile.sourceTileListenerKeys_.push(
listen(sourceTile, EventType.CHANGE, this.handleTileChange.bind(this)));
const sourceZ = sourceTileGrid.getZForResolution(resolution);
const minZoom = sourceTileGrid.getMinZoom();
let loadedZ = sourceZ + 1;
let covered, empty;
do {
--loadedZ;
covered = true;
empty = true;
sourceTileGrid.forEachTileCoord(extent, loadedZ, function(sourceTileCoord) {
const tileKey = getKey(sourceTileCoord);
let sourceTile;
if (tileKey in this.sourceTiles_) {
sourceTile = this.sourceTiles_[tileKey];
const state = sourceTile.getState();
if (state === TileState.LOADED || state === TileState.ERROR || state === TileState.EMPTY) {
empty = empty && state === TileState.EMPTY;
sourceTiles.push(sourceTile);
return;
}
} else if (loadedZ === sourceZ) {
const tileUrl = this.tileUrlFunction(sourceTileCoord, pixelRatio, projection);
sourceTile = new this.tileClass(sourceTileCoord,
tileUrl == undefined ? TileState.EMPTY : TileState.IDLE,
tileUrl == undefined ? '' : tileUrl,
this.format_, this.tileLoadFunction);
this.sourceTiles_[tileKey] = sourceTile;
empty = empty && sourceTile.getState() === TileState.EMPTY;
listen(sourceTile, EventType.CHANGE, this.handleTileChange, this);
sourceTile.load();
} else {
empty = false;
}
covered = false;
if (!sourceTile) {
return;
}
if (sourceTile.getState() !== TileState.EMPTY && tile.getState() === TileState.IDLE) {
tile.loadingSourceTiles++;
const key = listen(sourceTile, EventType.CHANGE, function() {
const state = sourceTile.getState();
const sourceTileKey = getKey(sourceTile.tileCoord);
if (state === TileState.LOADED || state === TileState.ERROR) {
if (state === TileState.LOADED) {
unlistenByKey(key);
tile.loadingSourceTiles--;
delete tile.errorSourceTileKeys[sourceTileKey];
} else if (state === TileState.ERROR) {
tile.errorSourceTileKeys[sourceTileKey] = true;
}
if (tile.loadingSourceTiles - Object.keys(tile.errorSourceTileKeys).length === 0) {
tile.hifi = true;
tile.sourceZ = sourceZ;
tile.setState(isEmpty(tile.errorSourceTileKeys) ? TileState.LOADED : TileState.ERROR);
}
}
});
}
}.bind(this));
if (!covered) {
sourceTiles.length = 0;
}
sourceTile.consumers++;
tile.tileKeys.push(sourceTileKey);
} while (!covered && loadedZ > minZoom);
if (!empty && tile.getState() === TileState.IDLE) {
tile.setState(TileState.LOADING);
}
}.bind(this));
if (covered || empty) {
tile.hifi = sourceZ === loadedZ;
tile.sourceZ = loadedZ;
const previousSourceTiles = this.sourceTilesByTileKey_[getKey(tile.tileCoord)];
if (tile.getState() < TileState.LOADED) {
tile.setState(empty ? TileState.EMPTY : TileState.LOADED);
} else if (!previousSourceTiles || !equals(sourceTiles, previousSourceTiles)) {
this.removeSourceTiles(tile);
this.addSourceTiles(tile, sourceTiles);
}
}
}
return sourceTiles;
}
/**
* @param {VectorImageTile} tile Tile.
* @param {Array<import("../VectorTile").default>} sourceTiles Source tiles.
*/
addSourceTiles(tile, sourceTiles) {
this.sourceTilesByTileKey_[getKey(tile.tileCoord)] = sourceTiles;
for (let i = 0, ii = sourceTiles.length; i < ii; ++i) {
sourceTiles[i].consumers++;
}
}
/**
* @param {VectorImageTile} tile Tile.
*/
removeSourceTiles(tile) {
const tileKey = getKey(tile.tileCoord);
if (tileKey in this.sourceTilesByTileKey_) {
const sourceTiles = this.sourceTilesByTileKey_[tileKey];
for (let i = 0, ii = sourceTiles.length; i < ii; ++i) {
const sourceTile = sourceTiles[i];
sourceTile.consumers--;
if (sourceTile.consumers === 0) {
sourceTile.dispose();
delete this.sourceTiles_[getKey(sourceTile.tileCoord)];
}
}
}
delete this.sourceTilesByTileKey_[tileKey];
}
/**
@@ -215,16 +319,15 @@ class VectorTile extends UrlTile {
urlTileCoord !== null ? TileState.IDLE : TileState.EMPTY,
urlTileCoord,
this.tileGrid,
this.sourceTiles_);
this.getSourceTiles.bind(this, pixelRatio, projection),
this.removeSourceTiles.bind(this));
tile.key = this.getRevision().toString();
this.assignTiles(tile, pixelRatio, projection);
this.tileCache.set(tileCoordKey, tile);
return tile;
}
}
/**
* @inheritDoc
*/
@@ -241,7 +344,6 @@ class VectorTile extends UrlTile {
return tileGrid;
}
/**
* @inheritDoc
*/
@@ -249,7 +351,6 @@ class VectorTile extends UrlTile {
return pixelRatio;
}
/**
* @inheritDoc
*/
@@ -262,3 +363,14 @@ class VectorTile extends UrlTile {
export default VectorTile;
/**
* Sets the loader for a tile.
* @param {import("../VectorTile.js").default} tile Vector tile.
* @param {string} url URL.
*/
export function defaultLoadFunction(tile, url) {
const loader = loadFeaturesXhr(url, tile.getFormat(), tile.onLoad.bind(tile), tile.onError.bind(tile));
tile.setLoader(loader);
}