Simpler and faster VectorTile loading

This commit is contained in:
Andreas Hocevar
2021-05-30 21:22:03 +02:00
parent 04e323d69e
commit 5ab7cbf905
7 changed files with 98 additions and 188 deletions
-8
View File
@@ -102,14 +102,6 @@ class Tile extends EventTarget {
*/ */
this.interimTile = null; 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 * 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 * if this tile can effectively be used, or if a new tile should be created
+1 -1
View File
@@ -82,7 +82,7 @@ class TileQueue extends PriorityQueue {
const tile = /** @type {import("./Tile.js").default} */ (event.target); const tile = /** @type {import("./Tile.js").default} */ (event.target);
const state = tile.getState(); const state = tile.getState();
if ( if (
(tile.hifi && state === TileState.LOADED) || state === TileState.LOADED ||
state === TileState.ERROR || state === TileState.ERROR ||
state === TileState.EMPTY state === TileState.EMPTY
) { ) {
+6 -21
View File
@@ -12,7 +12,6 @@ import {getUid} from './util.js';
* @property {number} renderedTileRevision RenderedTileRevision. * @property {number} renderedTileRevision RenderedTileRevision.
* @property {number} renderedResolution RenderedResolution. * @property {number} renderedResolution RenderedResolution.
* @property {number} renderedRevision RenderedRevision. * @property {number} renderedRevision RenderedRevision.
* @property {number} renderedZ RenderedZ.
* @property {number} renderedTileResolution RenderedTileResolution. * @property {number} renderedTileResolution RenderedTileResolution.
* @property {number} renderedTileZ RenderedTileZ. * @property {number} renderedTileZ RenderedTileZ.
*/ */
@@ -57,12 +56,6 @@ class VectorRenderTile extends Tile {
*/ */
this.loadingSourceTiles = 0; this.loadingSourceTiles = 0;
/**
* Tile keys of error source tiles. Read/written by the source.
* @type {Object<string, boolean>}
*/
this.errorSourceTileKeys = {};
/** /**
* @type {Object<number, ImageData>} * @type {Object<number, ImageData>}
*/ */
@@ -77,7 +70,12 @@ class VectorRenderTile extends Tile {
/** /**
* @type {Array<import("./VectorTile.js").default>} * @type {Array<import("./VectorTile.js").default>}
*/ */
this.sourceTiles = null; this.sourceTiles = [];
/**
* @type {Object<string, boolean>}
*/
this.errorTileKeys = {};
/** /**
* @type {number} * @type {number}
@@ -89,18 +87,6 @@ class VectorRenderTile extends Tile {
*/ */
this.getSourceTiles = getSourceTiles.bind(undefined, this); this.getSourceTiles = getSourceTiles.bind(undefined, this);
/**
* 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} * @type {import("./tilecoord.js").TileCoord}
*/ */
@@ -150,7 +136,6 @@ class VectorRenderTile extends Tile {
renderedRevision: -1, renderedRevision: -1,
renderedTileResolution: NaN, renderedTileResolution: NaN,
renderedTileRevision: -1, renderedTileRevision: -1,
renderedZ: -1,
renderedTileZ: -1, renderedTileZ: -1,
}; };
} }
+4 -10
View File
@@ -141,8 +141,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const tileUid = getUid(tile); const tileUid = getUid(tile);
const state = tile.getState(); const state = tile.getState();
if ( if (
((state === TileState.LOADED && tile.hifi) || (state === TileState.LOADED || state === TileState.ERROR) &&
state === TileState.ERROR) &&
tileUid in this.tileListenerKeys_ tileUid in this.tileListenerKeys_
) { ) {
unlistenByKey(this.tileListenerKeys_[tileUid]); unlistenByKey(this.tileListenerKeys_[tileUid]);
@@ -253,8 +252,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
!builderState.dirty && !builderState.dirty &&
builderState.renderedResolution === resolution && builderState.renderedResolution === resolution &&
builderState.renderedRevision == revision && builderState.renderedRevision == revision &&
builderState.renderedRenderOrder == renderOrder && builderState.renderedRenderOrder == renderOrder
builderState.renderedZ === tile.sourceZ
) { ) {
return; return;
} }
@@ -372,7 +370,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
} }
} }
builderState.renderedRevision = revision; builderState.renderedRevision = revision;
builderState.renderedZ = tile.sourceZ;
builderState.renderedRenderOrder = renderOrder; builderState.renderedRenderOrder = renderOrder;
builderState.renderedResolution = resolution; builderState.renderedResolution = resolution;
} }
@@ -516,7 +513,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
tileCoord.toString() === this.renderedTiles[i].tileCoord.toString() tileCoord.toString() === this.renderedTiles[i].tileCoord.toString()
) { ) {
tile = this.renderedTiles[i]; tile = this.renderedTiles[i];
if (tile.getState() === TileState.LOADED && tile.hifi) { if (tile.getState() === TileState.LOADED) {
const extent = tileGrid.getTileCoordExtent(tile.tileCoord); const extent = tileGrid.getTileCoordExtent(tile.tileCoord);
if ( if (
source.getWrapX() && source.getWrapX() &&
@@ -843,12 +840,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
} }
const replayState = tile.getReplayState(layer); const replayState = tile.getReplayState(layer);
const revision = layer.getRevision(); const revision = layer.getRevision();
const sourceZ = tile.sourceZ;
const resolution = tile.wantedResolution; const resolution = tile.wantedResolution;
return ( return (
replayState.renderedTileResolution !== resolution || replayState.renderedTileResolution !== resolution ||
replayState.renderedTileRevision !== revision || replayState.renderedTileRevision !== revision
replayState.renderedTileZ !== sourceZ
); );
} }
@@ -863,7 +858,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const revision = layer.getRevision(); const revision = layer.getRevision();
const executorGroups = tile.executorGroups[getUid(layer)]; const executorGroups = tile.executorGroups[getUid(layer)];
replayState.renderedTileRevision = revision; replayState.renderedTileRevision = revision;
replayState.renderedTileZ = tile.sourceZ;
const tileCoord = tile.wrappedTileCoord; const tileCoord = tile.wrappedTileCoord;
const z = tileCoord[0]; const z = tileCoord[0];
+82 -141
View File
@@ -18,8 +18,8 @@ import {
createXYZ, createXYZ,
extentFromProjection, extentFromProjection,
} from '../tilegrid.js'; } from '../tilegrid.js';
import {equals} from '../array.js';
import {fromKey, getKeyZXY} from '../tilecoord.js'; import {fromKey, getKeyZXY} from '../tilecoord.js';
import {isEmpty} from '../obj.js';
import {loadFeaturesXhr} from '../featureloader.js'; import {loadFeaturesXhr} from '../featureloader.js';
import {toSize} from '../size.js'; import {toSize} from '../size.js';
@@ -146,11 +146,6 @@ class VectorTile extends UrlTile {
*/ */
this.format_ = options.format ? options.format : null; this.format_ = options.format ? options.format : null;
/**
* @type {Object<string, import("./VectorTile").default>}
*/
this.loadingTiles_ = {};
/** /**
* @private * @private
* @type {TileCache} * @type {TileCache}
@@ -253,145 +248,91 @@ class VectorTile extends UrlTile {
* @return {Array<import("../VectorTile").default>} Tile keys. * @return {Array<import("../VectorTile").default>} Tile keys.
*/ */
getSourceTiles(pixelRatio, projection, tile) { getSourceTiles(pixelRatio, projection, tile) {
const urlTileCoord = tile.wrappedTileCoord;
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, -resolution, extent);
const sourceTileGrid = this.tileGrid;
const sourceExtent = sourceTileGrid.getExtent();
if (sourceExtent) {
getIntersection(extent, sourceExtent, extent);
}
const sourceZ = sourceTileGrid.getZForResolution(resolution, 1);
const minZoom = sourceTileGrid.getMinZoom();
const previousSourceTiles = tile.sourceTiles;
let sourceTiles, covered, loadedZ;
if (
previousSourceTiles &&
previousSourceTiles.length > 0 &&
previousSourceTiles[0].tileCoord[0] === sourceZ
) {
sourceTiles = previousSourceTiles;
covered = true;
loadedZ = sourceZ;
} else {
sourceTiles = [];
loadedZ = sourceZ + 1;
do {
--loadedZ;
covered = true;
sourceTileGrid.forEachTileCoord(
extent,
loadedZ,
function (sourceTileCoord) {
const tileUrl = this.tileUrlFunction(
sourceTileCoord,
pixelRatio,
projection
);
let sourceTile;
if (tileUrl !== undefined) {
if (this.sourceTileCache.containsKey(tileUrl)) {
sourceTile = this.sourceTileCache.get(tileUrl);
const state = sourceTile.getState();
if (
state === TileState.LOADED ||
state === TileState.ERROR ||
state === TileState.EMPTY
) {
sourceTiles.push(sourceTile);
return;
}
} else if (loadedZ === sourceZ) {
sourceTile = new this.tileClass(
sourceTileCoord,
TileState.IDLE,
tileUrl,
this.format_,
this.tileLoadFunction
);
sourceTile.extent = sourceTileGrid.getTileCoordExtent(
sourceTileCoord
);
sourceTile.projection = projection;
sourceTile.resolution = sourceTileGrid.getResolution(
sourceTileCoord[0]
);
this.sourceTileCache.set(tileUrl, sourceTile);
sourceTile.addEventListener(
EventType.CHANGE,
this.handleTileChange.bind(this)
);
sourceTile.load();
}
}
covered =
covered &&
sourceTile &&
sourceTile.getState() === TileState.LOADED;
if (!sourceTile) {
return;
}
if (
sourceTile.getState() !== TileState.EMPTY &&
tile.getState() === TileState.IDLE
) {
tile.loadingSourceTiles++;
sourceTile.addEventListener(
EventType.CHANGE,
function listenChange() {
const state = sourceTile.getState();
const sourceTileKey = sourceTile.getKey();
if (state === TileState.LOADED || state === TileState.ERROR) {
if (state === TileState.LOADED) {
sourceTile.removeEventListener(
EventType.CHANGE,
listenChange
);
tile.loadingSourceTiles--;
delete tile.errorSourceTileKeys[sourceTileKey];
} else if (state === TileState.ERROR) {
tile.errorSourceTileKeys[sourceTileKey] = true;
}
const errorTileCount = Object.keys(tile.errorSourceTileKeys)
.length;
if (tile.loadingSourceTiles - errorTileCount === 0) {
tile.hifi = errorTileCount === 0;
tile.sourceZ = sourceZ;
tile.setState(TileState.LOADED);
}
}
}
);
}
}.bind(this)
);
if (!covered) {
sourceTiles.length = 0;
}
} while (!covered && loadedZ > minZoom);
}
if (tile.getState() === TileState.IDLE) { if (tile.getState() === TileState.IDLE) {
tile.setState(TileState.LOADING); tile.setState(TileState.LOADING);
} const urlTileCoord = tile.wrappedTileCoord;
if (covered) { const tileGrid = this.getTileGridForProjection(projection);
tile.hifi = sourceZ === loadedZ; const extent = tileGrid.getTileCoordExtent(urlTileCoord);
tile.sourceZ = loadedZ; const z = urlTileCoord[0];
if (tile.getState() < TileState.LOADED) { const resolution = tileGrid.getResolution(z);
tile.setState(TileState.LOADED); // make extent 1 pixel smaller so we don't load tiles for < 0.5 pixel render space
} else if ( bufferExtent(extent, -resolution, extent);
!previousSourceTiles || const sourceTileGrid = this.tileGrid;
!equals(sourceTiles, previousSourceTiles) const sourceExtent = sourceTileGrid.getExtent();
) { if (sourceExtent) {
tile.sourceTiles = sourceTiles; getIntersection(extent, sourceExtent, extent);
}
const sourceZ = sourceTileGrid.getZForResolution(resolution, 1);
sourceTileGrid.forEachTileCoord(extent, sourceZ, (sourceTileCoord) => {
const tileUrl = this.tileUrlFunction(
sourceTileCoord,
pixelRatio,
projection
);
const sourceTile = this.sourceTileCache.containsKey(tileUrl)
? this.sourceTileCache.get(tileUrl)
: new this.tileClass(
sourceTileCoord,
tileUrl ? TileState.IDLE : TileState.EMPTY,
tileUrl,
this.format_,
this.tileLoadFunction
);
tile.sourceTiles.push(sourceTile);
const sourceTileState = sourceTile.getState();
if (sourceTileState === TileState.IDLE) {
sourceTile.extent = sourceTileGrid.getTileCoordExtent(
sourceTileCoord
);
sourceTile.projection = projection;
sourceTile.resolution = sourceTileGrid.getResolution(
sourceTileCoord[0]
);
this.sourceTileCache.set(tileUrl, sourceTile);
const listenChange = (event) => {
this.handleTileChange(event);
const state = sourceTile.getState();
if (state === TileState.LOADED || state === TileState.ERROR) {
const sourceTileKey = sourceTile.getKey();
if (sourceTileKey in tile.errorTileKeys) {
if (sourceTile.getState() === TileState.LOADED) {
delete tile.errorTileKeys[sourceTileKey];
}
} else {
tile.loadingSourceTiles--;
}
if (state === TileState.ERROR) {
tile.errorTileKeys[sourceTileKey] = true;
} else {
sourceTile.removeEventListener(EventType.CHANGE, listenChange);
}
if (tile.loadingSourceTiles === 0) {
tile.setState(
isEmpty(tile.errorTileKeys)
? TileState.LOADED
: TileState.ERROR
);
}
}
};
sourceTile.addEventListener(EventType.CHANGE, listenChange);
tile.loadingSourceTiles++;
sourceTile.load();
}
});
if (!tile.loadingSourceTiles) {
tile.setState(
tile.sourceTiles.some(
(sourceTile) => sourceTile.getState() === TileState.ERROR
)
? TileState.ERROR
: TileState.LOADED
);
} }
} }
return sourceTiles;
return tile.sourceTiles;
} }
/** /**
@@ -305,7 +305,7 @@ describe('ol.source.VectorTile', function () {
let count = 0; let count = 0;
let tile = source.getTile(0, 0, 0, 1, map.getView().getProjection()); let tile = source.getTile(0, 0, 0, 1, map.getView().getProjection());
tile.addEventListener('change', function onTileChange(e) { tile.addEventListener('change', function onTileChange(e) {
if (e.target.getState() !== TileState.LOADED && !e.target.hifi) { if (e.target.getState() !== TileState.LOADED) {
return; return;
} }
e.target.removeEventListener('change', onTileChange); e.target.removeEventListener('change', onTileChange);
@@ -24,19 +24,18 @@ describe('ol.VectorRenderTile', function () {
listen(tile, 'change', function (e) { listen(tile, 'change', function (e) {
++calls; ++calls;
if (calls === 1) { if (calls === 1) {
expect(tile.getState()).to.be(TileState.LOADED); expect(tile.getState()).to.be(TileState.ERROR);
expect(tile.hifi).to.be(false);
setTimeout(function () { setTimeout(function () {
sourceTile.setState(TileState.LOADED); sourceTile.setState(TileState.LOADED);
expect(tile.hifi).to.be(true);
}, 0); }, 0);
} else if (calls === 2) { } else if (calls === 2) {
expect(tile.getState()).to.be(TileState.LOADED);
done(); done();
} }
}); });
}); });
it('sets LOADED state and hifi==false when source tiles fail to load', function (done) { it('sets ERROR state when source tiles fail to load', function (done) {
const source = new VectorTileSource({ const source = new VectorTileSource({
format: new GeoJSON(), format: new GeoJSON(),
url: 'spec/ol/data/unavailable.json', url: 'spec/ol/data/unavailable.json',
@@ -46,8 +45,7 @@ describe('ol.VectorRenderTile', function () {
tile.load(); tile.load();
listen(tile, 'change', function (e) { listen(tile, 'change', function (e) {
expect(tile.getState()).to.be(TileState.LOADED); expect(tile.getState()).to.be(TileState.ERROR);
expect(tile.hifi).to.be(false);
done(); done();
}); });
}); });