diff --git a/src/ol/Tile.js b/src/ol/Tile.js index aba97d71fb..c8d0738661 100644 --- a/src/ol/Tile.js +++ b/src/ol/Tile.js @@ -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. diff --git a/src/ol/TileQueue.js b/src/ol/TileQueue.js index fbce910fcd..8ad585c290 100644 --- a/src/ol/TileQueue.js +++ b/src/ol/TileQueue.js @@ -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(); diff --git a/src/ol/VectorImageTile.js b/src/ol/VectorImageTile.js deleted file mode 100644 index 8e0ea79d23..0000000000 --- a/src/ol/VectorImageTile.js +++ /dev/null @@ -1,362 +0,0 @@ -/** - * @module ol/VectorImageTile - */ -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 {getHeight, getIntersection, getWidth} from './extent.js'; -import EventType from './events/EventType.js'; -import {loadFeaturesXhr} from './featureloader.js'; - - -/** - * @typedef {Object} ReplayState - * @property {boolean} dirty - * @property {null|import("./render.js").OrderFunction} renderedRenderOrder - * @property {number} renderedTileRevision - * @property {number} renderedRevision - */ - - -class VectorImageTile extends Tile { - - /** - * @param {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate. - * @param {TileState} state State. - * @param {number} sourceRevision Source revision. - * @param {import("./format/Feature.js").default} format Feature format. - * @param {import("./Tile.js").LoadFunction} tileLoadFunction Tile load function. - * @param {import("./tilecoord.js").TileCoord} urlTileCoord Wrapped tile coordinate for source urls. - * @param {import("./Tile.js").UrlFunction} tileUrlFunction Tile url function. - * @param {import("./tilegrid/TileGrid.js").default} sourceTileGrid Tile grid of the source. - * @param {import("./tilegrid/TileGrid.js").default} tileGrid Tile grid of the renderer. - * @param {Object} sourceTiles Source tiles. - * @param {number} pixelRatio Pixel ratio. - * @param {import("./proj/Projection.js").default} projection Projection. - * @param {typeof import("./VectorTile.js").default} tileClass Class to - * 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. - */ - constructor(tileCoord, state, sourceRevision, format, tileLoadFunction, - urlTileCoord, tileUrlFunction, sourceTileGrid, tileGrid, sourceTiles, - pixelRatio, projection, tileClass, handleTileChange) { - - super(tileCoord, state, {transition: 0}); - - /** - * @private - * @type {!Object} - */ - this.context_ = {}; - - /** - * @private - * @type {import("./featureloader.js").FeatureLoader} - */ - this.loader_; - - /** - * @private - * @type {!Object} - */ - this.replayState_ = {}; - - /** - * @private - * @type {Object} - */ - 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} - */ - this.tileKeys = []; - - /** - * @type {import("./extent.js").Extent} - */ - this.extent = null; - - /** - * @type {import("./tilecoord.js").TileCoord} - */ - this.wrappedTileCoord = urlTileCoord; - - /** - * @type {Array} - */ - this.loadListenerKeys_ = []; - - /** - * @type {boolean} - */ - this.isInterimTile = !sourceTileGrid; - - /** - * @type {Array} - */ - this.sourceTileListenerKeys_ = []; - - this.key = sourceRevision.toString(); - - if (urlTileCoord && sourceTileGrid) { - const extent = this.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)); - const sourceExtent = sourceTileGrid.getExtent(); - if (sourceExtent) { - sharedExtent = getIntersection(sharedExtent, sourceExtent, sharedExtent); - } - 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 = sourceTiles[sourceTileKey]; - if (!sourceTile) { - const tileUrl = tileUrlFunction(sourceTileCoord, pixelRatio, projection); - sourceTile = sourceTiles[sourceTileKey] = new tileClass(sourceTileCoord, - tileUrl == undefined ? TileState.EMPTY : TileState.IDLE, - tileUrl == undefined ? '' : tileUrl, - format, tileLoadFunction); - this.sourceTileListenerKeys_.push( - listen(sourceTile, EventType.CHANGE, handleTileChange)); - } - sourceTile.consumers++; - this.tileKeys.push(sourceTileKey); - } - }.bind(this)); - } - } - - /** - * @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; - super.disposeInternal(); - } - - /** - * @param {import("./layer/Layer.js").default} layer Layer. - * @return {CanvasRenderingContext2D} The rendering context. - */ - getContext(layer) { - const key = getUid(layer); - if (!(key in this.context_)) { - this.context_[key] = createCanvasContext2D(); - } - 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.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); - } - - /** - * @param {import("./layer/Layer.js").default} layer Layer. - * @return {ReplayState} The replay state. - */ - getReplayState(layer) { - const key = getUid(layer); - if (!(key in this.replayState_)) { - this.replayState_[key] = { - dirty: false, - renderedRenderOrder: null, - renderedRevision: -1, - renderedTileRevision: -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 - */ - 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); - } - } -} - - -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); -} diff --git a/src/ol/VectorRenderTile.js b/src/ol/VectorRenderTile.js new file mode 100644 index 0000000000..025e76c418 --- /dev/null +++ b/src/ol/VectorRenderTile.js @@ -0,0 +1,168 @@ +/** + * @module ol/VectorRenderTile + */ +import {getUid} from './util.js'; +import Tile from './Tile.js'; +import TileState from './TileState.js'; +import {createCanvasContext2D} from './dom.js'; + + +/** + * @typedef {Object} ReplayState + * @property {boolean} dirty + * @property {null|import("./render.js").OrderFunction} renderedRenderOrder + * @property {number} renderedTileRevision + * @property {number} renderedRevision + * @property {number} renderedZ + * @property {number} renderedTileZ + */ + + +class VectorRenderTile extends Tile { + + /** + * @param {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate. + * @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 {function(VectorRenderTile):Array} getSourceTiles Function + * to get an source tiles for this tile. + * @param {function(VectorRenderTile):void} removeSourceTiles Function to remove this tile from its + * source tiles's consumer count. + */ + constructor(tileCoord, state, urlTileCoord, sourceTileGrid, getSourceTiles, removeSourceTiles) { + + super(tileCoord, state, {transition: 0}); + + /** + * @private + * @type {!Object} + */ + this.context_ = {}; + + /** + * Executor groups by layer uid. Entries are read/written by the renderer. + * @type {Object>} + */ + 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} + */ + this.errorSourceTileKeys = {}; + + /** + * @private + * @type {!Object} + */ + this.replayState_ = {}; + + /** + * @type {!function(import("./VectorRenderTile.js").default):Array} + */ + this.getSourceTiles_ = getSourceTiles; + + /** + * @type {!function(import("./VectorRenderTile.js").default):void} + */ + this.removeSourceTiles_ = removeSourceTiles; + + /** + * @private + * @type {import("./tilegrid/TileGrid.js").default} + */ + this.sourceTileGrid_ = sourceTileGrid; + + /** + * z of the source tiles of the last getSourceTiles call. + * @type {number} + */ + this.sourceZ = -1; + + /** + * True when all tiles for this tile's nominal resolution are available. + * @type {boolean} + */ + this.hifi = false; + + /** + * @type {import("./tilecoord.js").TileCoord} + */ + this.wrappedTileCoord = urlTileCoord; + } + + /** + * @inheritDoc + */ + disposeInternal() { + this.removeSourceTiles_(this); + this.setState(TileState.ABORT); + super.disposeInternal(); + } + + /** + * @param {import("./layer/Layer.js").default} layer Layer. + * @return {CanvasRenderingContext2D} The rendering context. + */ + getContext(layer) { + const key = getUid(layer); + if (!(key in this.context_)) { + this.context_[key] = createCanvasContext2D(); + } + 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.hasContext(layer) ? this.getContext(layer).canvas : null; + } + + /** + * @param {import("./layer/Layer.js").default} layer Layer. + * @return {ReplayState} The replay state. + */ + getReplayState(layer) { + const key = getUid(layer); + if (!(key in this.replayState_)) { + this.replayState_[key] = { + dirty: false, + renderedRenderOrder: null, + renderedRevision: -1, + renderedTileRevision: -1, + renderedZ: -1, + renderedTileZ: -1 + }; + } + return this.replayState_[key]; + } + + /** + * @inheritDoc + * @return {Array} Source tiles for this tile. + */ + load() { + return this.getSourceTiles_(this); + } +} + + +export default VectorRenderTile; diff --git a/src/ol/VectorTile.js b/src/ol/VectorTile.js index e7ae5f0acf..e9e0350acb 100644 --- a/src/ol/VectorTile.js +++ b/src/ol/VectorTile.js @@ -61,12 +61,6 @@ class VectorTile extends Tile { */ this.projection_ = null; - /** - * @private - * @type {Object} - */ - 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. diff --git a/src/ol/index.js b/src/ol/index.js index 2566564f17..e3e3d8607e 100644 --- a/src/ol/index.js +++ b/src/ol/index.js @@ -26,7 +26,7 @@ export {default as Tile} from './Tile.js'; export {default as TileCache} from './TileCache.js'; export {default as TileQueue} from './TileQueue.js'; export {default as TileRange} from './TileRange.js'; -export {default as VectorImageTile} from './VectorImageTile.js'; +export {default as VectorRenderTile} from './VectorRenderTile.js'; export {default as VectorTile} from './VectorTile.js'; export {default as View} from './View.js'; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 571aac80de..4bee342199 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -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} + * @type {!Object} */ - this.tilesWithoutImage_ = null; + this.renderTileImageQueue_ = {}; /** - * @private - * @type {Object} + * @type {Object} */ - 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("../../VectorRenderTile.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; + } } } @@ -188,40 +176,18 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @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); - 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); + const tile = /** @type {import("../../VectorRenderTile.js").default} */ (super.getTile(z, x, y, pixelRatio, projection)); + 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 */ @@ -251,7 +217,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } /** - * @param {import("../../VectorImageTile.js").default} tile Tile. + * @param {import("../../VectorRenderTile.js").default} tile Tile. * @param {number} pixelRatio Pixel ratio. * @param {import("../../proj/Projection.js").default} projection Projection. * @private @@ -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,26 +318,25 @@ 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} */ const features = {}; - const renderedTiles = /** @type {Array} */ (this.renderedTiles); + const renderedTiles = /** @type {Array} */ (this.renderedTiles); let bufferedExtent, found; 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_; } @@ -484,25 +452,23 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const clips = []; const zs = []; for (let i = tiles.length - 1; i >= 0; --i) { - const tile = /** @type {import("../../VectorImageTile.js").default} */ (tiles[i]); + const tile = /** @type {import("../../VectorRenderTile.js").default} */ (tiles[i]); if (tile.getState() == TileState.ABORT) { 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; } } @@ -595,7 +564,22 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } /** - * @param {import("../../VectorImageTile.js").default} tile Tile. + * @param {import("../../VectorRenderTile.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("../../VectorRenderTile.js").default} tile Tile. * @param {number} pixelRatio Pixel ratio. * @param {import("../../proj/Projection.js").default} projection Projection. * @private @@ -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()]); } } diff --git a/src/ol/source/VectorTile.js b/src/ol/source/VectorTile.js index 5be126a92f..d0d181336e 100644 --- a/src/ol/source/VectorTile.js +++ b/src/ol/source/VectorTile.js @@ -3,12 +3,18 @@ */ import TileState from '../TileState.js'; -import VectorImageTile, {defaultLoadFunction} from '../VectorImageTile.js'; +import VectorRenderTile from '../VectorRenderTile.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 {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 @@ -110,11 +116,22 @@ class VectorTile extends UrlTile { this.format_ = options.format ? options.format : null; /** - * @private - * @type {Object} - */ + * @type {Object} + */ + this.loadingTiles_ = {}; + + /** + * @private + * @type {Object} + */ this.sourceTiles_ = {}; + /** + * @private + * @type {Object>} + */ + this.sourceTilesByTileKey_ = {}; + /** * @private * @type {boolean} @@ -149,6 +166,139 @@ class VectorTile extends UrlTile { clear() { this.tileCache.clear(); this.sourceTiles_ = {}; + this.sourceTilesByTileKey_ = {}; + } + + /** + * @param {number} pixelRatio Pixel ratio. + * @param {import("../proj/Projection").default} projection Projection. + * @param {VectorRenderTile} tile Vector image tile. + * @return {Array} Tile keys. + */ + getSourceTiles(pixelRatio, projection, tile) { + const sourceTiles = []; + const urlTileCoord = tile.wrappedTileCoord; + 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) { + getIntersection(extent, sourceTileGrid.getExtent(), extent); + } + 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; + } + } while (!covered && loadedZ > minZoom); + if (!empty && tile.getState() === TileState.IDLE) { + tile.setState(TileState.LOADING); + } + 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 {VectorRenderTile} tile Tile. + * @param {Array} 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 {VectorRenderTile} 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]; } /** @@ -164,21 +314,20 @@ class VectorTile extends UrlTile { const tileCoord = [z, x, y]; const urlTileCoord = this.getTileCoordForTileUrlFunction( tileCoord, projection); - const tile = new VectorImageTile( + const tile = new VectorRenderTile( tileCoord, urlTileCoord !== null ? TileState.IDLE : TileState.EMPTY, - this.getRevision(), - this.format_, this.tileLoadFunction, urlTileCoord, this.tileUrlFunction, - this.tileGrid, this.getTileGridForProjection(projection), - this.sourceTiles_, pixelRatio, projection, this.tileClass, - this.handleTileChange.bind(this)); + urlTileCoord, + this.tileGrid, + this.getSourceTiles.bind(this, pixelRatio, projection), + this.removeSourceTiles.bind(this)); + tile.key = this.getRevision().toString(); this.tileCache.set(tileCoordKey, tile); return tile; } } - /** * @inheritDoc */ @@ -195,7 +344,6 @@ class VectorTile extends UrlTile { return tileGrid; } - /** * @inheritDoc */ @@ -203,7 +351,6 @@ class VectorTile extends UrlTile { return pixelRatio; } - /** * @inheritDoc */ @@ -216,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); +} diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 8e44634d7a..4ffb3365d8 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -2,7 +2,7 @@ import {clear} from '../../../../../src/ol/obj.js'; import Feature from '../../../../../src/ol/Feature.js'; import Map from '../../../../../src/ol/Map.js'; import TileState from '../../../../../src/ol/TileState.js'; -import VectorImageTile from '../../../../../src/ol/VectorImageTile.js'; +import VectorRenderTile from '../../../../../src/ol/VectorRenderTile.js'; import VectorTile from '../../../../../src/ol/VectorTile.js'; import View from '../../../../../src/ol/View.js'; import {getCenter} from '../../../../../src/ol/extent.js'; @@ -19,6 +19,7 @@ import Style from '../../../../../src/ol/style/Style.js'; import Text from '../../../../../src/ol/style/Text.js'; import {createXYZ} from '../../../../../src/ol/tilegrid.js'; import VectorTileRenderType from '../../../../../src/ol/layer/VectorTileRenderType.js'; +import {getUid} from '../../../../../src/ol/util.js'; describe('ol.renderer.canvas.VectorTileLayer', function() { @@ -62,9 +63,9 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { class TileClass extends VectorTile { constructor() { super(...arguments); - this.setState(TileState.LOADED); this.setFeatures([feature1, feature2, feature3]); this.setProjection(getProjection('EPSG:4326')); + this.setState(TileState.LOADED); tileCallback(this); } } @@ -73,12 +74,14 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { tileClass: TileClass, tileGrid: createXYZ() }); + source.getSourceTiles = function() { + return [new TileClass([0, 0, 0])]; + }; source.getTile = function() { const tile = VectorTileSource.prototype.getTile.apply(source, arguments); tile.hasContext = function() { return true; }; - tile.sourceTilesLoaded = true; tile.setState(TileState.LOADED); return tile; }; @@ -219,13 +222,10 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { }); map.addLayer(layer2); - const spy1 = sinon.spy(VectorTile.prototype, 'getExecutorGroup'); - const spy2 = sinon.spy(VectorTile.prototype, 'setExecutorGroup'); map.renderSync(); - expect(spy1.callCount).to.be(4); - expect(spy2.callCount).to.be(2); - spy1.restore(); - spy2.restore(); + const tile = source.getTile(0, 0, 0, 1, getProjection('EPSG:3857')); + expect(Object.keys(tile.executorGroups)[0]).to.be(getUid(layer)); + expect(Object.keys(tile.executorGroups)[1]).to.be(getUid(layer2)); }); }); @@ -244,15 +244,13 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { sourceTile.getImage = function() { return document.createElement('canvas'); }; - 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); + const tile = new VectorRenderTile([0, 0, 0], 1, [0, 0, 0], createXYZ(), + function() { + return sourceTile; + }, + function() {}); tile.transition_ = 0; - tile.wrappedTileCoord = [0, 0, 0]; tile.setState(TileState.LOADED); - tile.getSourceTile = function() { - return sourceTile; - }; layer.getSource().getTile = function() { return tile; }; @@ -287,33 +285,37 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { }); describe('#forEachFeatureAtCoordinate', function() { - let layer, renderer, executorGroup; - class TileClass extends VectorImageTile { + let layer, renderer, executorGroup, source; + class TileClass extends VectorRenderTile { constructor() { super(...arguments); - this.extent = [-Infinity, -Infinity, Infinity, Infinity]; this.setState(TileState.LOADED); - const sourceTile = new VectorTile([0, 0, 0]); - sourceTile.setState(TileState.LOADED); - sourceTile.setProjection(getProjection('EPSG:3857')); - sourceTile.getExecutorGroup = function() { - return executorGroup; - }; - const key = sourceTile.tileCoord.toString(); - this.tileKeys = [key]; - this.sourceTiles_ = {}; - this.sourceTiles_[key] = sourceTile; this.wrappedTileCoord = arguments[0]; } } beforeEach(function() { + const sourceTile = new VectorTile([0, 0, 0]); + sourceTile.setState(TileState.LOADED); + sourceTile.setProjection(getProjection('EPSG:3857')); + source = new VectorTileSource({ + tileClass: TileClass, + tileGrid: createXYZ() + }); + source.sourceTiles_ = { + '0/0/0': sourceTile + }; + source.sourceTilesByTileKey_ = { + '0/0/0': [sourceTile] + }; executorGroup = {}; + source.getTile = function() { + const tile = VectorTileSource.prototype.getTile.apply(source, arguments); + tile.executorGroups[getUid(layer)] = [executorGroup]; + return tile; + }; layer = new VectorTileLayer({ - source: new VectorTileSource({ - tileClass: TileClass, - tileGrid: createXYZ() - }) + source: source }); renderer = new CanvasVectorTileLayerRenderer(layer); executorGroup.forEachFeatureAtCoordinate = function(coordinate, @@ -336,7 +338,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { rotation: 0 } }; - renderer.renderedTiles = [new TileClass([0, 0, -1], undefined, 1)]; + renderer.renderedTiles = [source.getTile(0, 0, 0, 1, getProjection('EPSG:3857'))]; renderer.forEachFeatureAtCoordinate( coordinate, frameState, 0, spy, undefined); expect(spy.callCount).to.be(1); diff --git a/test/spec/ol/source/vectortile.test.js b/test/spec/ol/source/vectortile.test.js index 36d6c80691..cfc0462ca4 100644 --- a/test/spec/ol/source/vectortile.test.js +++ b/test/spec/ol/source/vectortile.test.js @@ -1,13 +1,16 @@ import Map from '../../../../src/ol/Map.js'; import View from '../../../../src/ol/View.js'; -import VectorImageTile from '../../../../src/ol/VectorImageTile.js'; +import VectorRenderTile from '../../../../src/ol/VectorRenderTile.js'; import VectorTile from '../../../../src/ol/VectorTile.js'; +import GeoJSON from '../../../../src/ol/format/GeoJSON.js'; import MVT from '../../../../src/ol/format/MVT.js'; import VectorTileLayer from '../../../../src/ol/layer/VectorTile.js'; import {get as getProjection} from '../../../../src/ol/proj.js'; import VectorTileSource from '../../../../src/ol/source/VectorTile.js'; import {createXYZ} from '../../../../src/ol/tilegrid.js'; import TileGrid from '../../../../src/ol/tilegrid/TileGrid.js'; +import {listen, unlistenByKey} from '../../../../src/ol/events.js'; +import TileState from '../../../../src/ol/TileState.js'; describe('ol.source.VectorTile', function() { @@ -38,7 +41,7 @@ describe('ol.source.VectorTile', function() { describe('#getTile()', function() { it('creates a tile with the correct tile class', function() { tile = source.getTile(0, 0, 0, 1, getProjection('EPSG:3857')); - expect(tile).to.be.a(VectorImageTile); + expect(tile).to.be.a(VectorRenderTile); }); it('sets the correct tileCoord on the created tile', function() { expect(tile.getTileCoord()).to.eql([0, 0, 0]); @@ -47,6 +50,24 @@ describe('ol.source.VectorTile', function() { expect(source.getTile(0, 0, 0, 1, getProjection('EPSG:3857'))) .to.equal(tile); }); + it('loads source tiles', function(done) { + const source = new VectorTileSource({ + format: new GeoJSON(), + url: 'spec/ol/data/point.json' + }); + const tile = source.getTile(0, 0, 0, 1, source.getProjection()); + + tile.load(); + const key = listen(tile, 'change', function(e) { + if (tile.getState() === TileState.LOADED) { + const sourceTile = tile.load()[0]; + expect(sourceTile.getFeatures().length).to.be.greaterThan(0); + unlistenByKey(key); + done(); + } + }); + }); + }); describe('#getTileGridForProjection', function() { diff --git a/test/spec/ol/vectorimagetile.test.js b/test/spec/ol/vectorimagetile.test.js index b1b093a68b..56621a1e8f 100644 --- a/test/spec/ol/vectorimagetile.test.js +++ b/test/spec/ol/vectorimagetile.test.js @@ -1,75 +1,49 @@ import TileState from '../../../src/ol/TileState.js'; -import VectorImageTile, {defaultLoadFunction} from '../../../src/ol/VectorImageTile.js'; -import VectorTile from '../../../src/ol/VectorTile.js'; -import {listen, listenOnce} from '../../../src/ol/events.js'; +import {defaultLoadFunction} from '../../../src/ol/source/VectorTile.js'; +import VectorTileSource from '../../../src/ol/source/VectorTile.js'; +import {listen, listenOnce, unlistenByKey} from '../../../src/ol/events.js'; import GeoJSON from '../../../src/ol/format/GeoJSON.js'; -import {get as getProjection} from '../../../src/ol/proj.js'; import {createXYZ} from '../../../src/ol/tilegrid.js'; import TileGrid from '../../../src/ol/tilegrid/TileGrid.js'; +import {getKey} from '../../../src/ol/tilecoord.js'; +import EventType from '../../../src/ol/events/EventType.js'; -describe('ol.VectorImageTile', function() { +describe('ol.VectorRenderTile', function() { - it('configures loader that sets features on the source tile', 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(), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}); - - tile.load(); - const sourceTile = tile.getTile(tile.tileKeys[0]); - const loader = sourceTile.loader_; - expect(typeof loader).to.be('function'); - - listen(sourceTile, 'change', function(e) { - expect(sourceTile.getFeatures().length).to.be.greaterThan(0); - done(); - }); - }); - - it('sets sourceTilesLoaded when previously failed source tiles are loaded', function(done) { - const format = new GeoJSON(); - const url = 'spec/ol/data/unavailable.json'; + it('triggers "change" when previously failed source tiles are loaded', function(done) { let sourceTile; - const tile = new VectorImageTile([0, 0, 0] /* one world away */, 0, url, format, - function(tile, url) { + const source = new VectorTileSource({ + format: new GeoJSON(), + url: 'spec/ol/data/unavailable.json', + tileLoadFunction: function(tile, url) { sourceTile = tile; defaultLoadFunction(tile, url); - }, [0, 0, 0], function() { - return url; - }, createXYZ(), createXYZ(), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}); + } + }); + const tile = source.getTile(0, 0, 0, 1, source.getProjection()); tile.load(); let calls = 0; listen(tile, 'change', function(e) { ++calls; if (calls === 1) { - expect(tile.sourceTilesLoaded).to.be(false); - } else if (calls === 2) { - expect(tile.sourceTilesLoaded).to.be(true); - } - if (calls == 2) { - done(); - } else { + expect(tile.getState()).to.be(TileState.ERROR); setTimeout(function() { sourceTile.setState(TileState.LOADED); }, 0); + } else if (calls === 2) { + done(); } }); }); it('sets ERROR state when source tiles fail to load', function(done) { - const format = new GeoJSON(); - const url = 'spec/ol/data/unavailable.json'; - const tile = new VectorImageTile([0, 0, 0], 0, url, format, - defaultLoadFunction, [0, 0, 0], function() { - return url; - }, createXYZ(), createXYZ(), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}); + const source = new VectorTileSource({ + format: new GeoJSON(), + url: 'spec/ol/data/unavailable.json' + }); + const tile = source.getTile(0, 0, 0, 1, source.getProjection()); tile.load(); @@ -79,81 +53,90 @@ describe('ol.VectorImageTile', function() { }); }); - it('sets EMPTY state when tile has only empty source tiles', function(done) { - const format = new GeoJSON(); - const url = ''; - const tile = new VectorImageTile([0, 0, 0], 0, url, format, - defaultLoadFunction, [0, 0, 0], function() {}, - createXYZ(), createXYZ(), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}); + it('sets EMPTY state when tile has only empty source tiles', function() { + const source = new VectorTileSource({ + format: new GeoJSON(), + url: '' + }); + const tile = source.getTile(0, 0, 0, 1, source.getProjection()); tile.load(); - - listen(tile, 'change', function() { - expect(tile.getState()).to.be(TileState.EMPTY); - done(); - }); + expect(tile.getState()).to.be(TileState.EMPTY); }); - it('only loads tiles within the source tileGrid\'s extent', function() { - const format = new GeoJSON(); + it('only loads tiles within the source tileGrid\'s extent', function(done) { const url = 'spec/ol/data/point.json'; - const tileGrid = new TileGrid({ - resolutions: [0.02197265625, 0.010986328125, 0.0054931640625], - origin: [-180, 90], - extent: [-88, 35, -87, 36] - }); - const sourceTiles = {}; - const tile = new VectorImageTile([1, 0, 0], 0, url, format, - defaultLoadFunction, [1, 0, 0], function(zxy) { + const source = new VectorTileSource({ + projection: 'EPSG:4326', + format: new GeoJSON(), + tileGrid: new TileGrid({ + resolutions: [0.02197265625, 0.010986328125, 0.0054931640625], + origin: [-180, 90], + extent: [-88, 35, -87, 36] + }), + tileUrlFunction: function(zxy) { return url; - }, tileGrid, - createXYZ({extent: [-180, -90, 180, 90], tileSize: 512}), - sourceTiles, 1, getProjection('EPSG:4326'), VectorTile, function() {}); + }, + url: url + }); + const tile = source.getTile(0, 0, 0, 1, source.getProjection()); + tile.load(); - expect(tile.tileKeys.length).to.be(1); - expect(tile.getTile(tile.tileKeys[0]).tileCoord).to.eql([0, 16, 9]); + const key = listen(tile, EventType.CHANGE, function() { + if (tile.getState() === TileState.LOADED) { + unlistenByKey(key); + const sourceTiles = tile.load(); + expect(sourceTiles.length).to.be(1); + expect(sourceTiles[0].tileCoord).to.eql([0, 16, 9]); + done(); + } + }); }); it('#dispose() while loading', function() { - const format = new GeoJSON(); - const url = 'spec/ol/data/point.json'; - const tile = new VectorImageTile([0, 0, 0] /* one world away */, 0, url, format, - defaultLoadFunction, [0, 0, 0], function() { - return url; - }, createXYZ(), createXYZ({tileSize: 512}), {}, - 1, getProjection('EPSG:3857'), VectorTile, function() {}); + const source = new VectorTileSource({ + format: new GeoJSON(), + url: 'spec/ol/data/point.json', + tileGrid: createXYZ() + }); + source.getTileGridForProjection = function() { + return createXYZ({tileSize: 512}); + }; + const tile = source.getTile(0, 0, 0, 1, source.getProjection()); tile.load(); - expect(tile.loadListenerKeys_.length).to.be(4); - expect(tile.tileKeys.length).to.be(4); expect(tile.getState()).to.be(TileState.LOADING); tile.dispose(); - expect(tile.loadListenerKeys_.length).to.be(0); - expect(tile.tileKeys.length).to.be(0); - expect(tile.sourceTiles_).to.be(null); + expect(source.sourceTilesByTileKey_[getKey(tile)]).to.be(undefined); expect(tile.getState()).to.be(TileState.ABORT); }); 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() {}); + const source = new VectorTileSource({ + format: new GeoJSON(), + url: 'spec/ol/data/point.json', + tileGrid: createXYZ() + }); + source.getTileGridForProjection = function() { + return createXYZ({tileSize: 512}); + }; + const tile = source.getTile(0, 0, 0, 1, source.getProjection()); tile.load(); listenOnce(tile, 'change', function() { - 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); + expect(tile.getState()).to.be(TileState.LOADED); + expect(tile.loadingSourceTiles).to.be(0); + const sourceTiles = tile.load(); + expect(sourceTiles.length).to.be(4); + for (let i = 0, ii = sourceTiles.length; i < ii; ++i) { + expect(sourceTiles[i].consumers).to.be(1); + } tile.dispose(); - expect(tile.tileKeys.length).to.be(0); - expect(tile.sourceTiles_).to.be(null); expect(tile.getState()).to.be(TileState.ABORT); + for (let i = 0, ii = sourceTiles.length; i < ii; ++i) { + expect(sourceTiles[i].consumers).to.be(0); + expect(sourceTiles[i].getState()).to.be(TileState.ABORT); + } done(); }); }); diff --git a/test/spec/ol/vectortile.test.js b/test/spec/ol/vectortile.test.js index b7a7d47a5f..485d397c7a 100644 --- a/test/spec/ol/vectortile.test.js +++ b/test/spec/ol/vectortile.test.js @@ -1,5 +1,5 @@ import Feature from '../../../src/ol/Feature.js'; -import {defaultLoadFunction} from '../../../src/ol/VectorImageTile.js'; +import {defaultLoadFunction} from '../../../src/ol/source/VectorTile.js'; import VectorTile from '../../../src/ol/VectorTile.js'; import {listen} from '../../../src/ol/events.js'; import TextFeature from '../../../src/ol/format/TextFeature.js';