From ff393e1ba1e63b56117c39e94b21bc3411203df4 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 2 Apr 2018 11:24:02 -0400 Subject: [PATCH] avoid flickering when longitude is wrapped while panning When panning cross the antimeridian, the longitude value gets wrapped. This results in tileIDs getting assigned a different `wrap` value for tiles that cover roughly the same area on the screen. This pr calculates what this change in wrap values is and updates the state of both `SourceCache` and `CrossTileSymbolIndex` so that areas use the same tile and symbol state for the same screen areas even if they have a different `wrap` value. I think this is the long term fix for the CrossTileSymbolIndex. For SourceCache, it may be better to rework how tiles are retained so that you can actually use versions of tiles with a different wrap as tiles in the next frame. --- src/source/source_cache.js | 37 ++++++++++++++++++++- src/source/tile_id.js | 8 +++++ src/style/style.js | 2 +- src/symbol/cross_tile_symbol_index.js | 29 +++++++++++++++- test/unit/symbol/cross_tile_symbol_index.js | 28 ++++++++-------- 5 files changed, 87 insertions(+), 17 deletions(-) diff --git a/src/source/source_cache.js b/src/source/source_cache.js index a9f97d0b8af..e1a6320c5b5 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -43,6 +43,7 @@ class SourceCache extends Evented { _sourceLoaded: boolean; _sourceErrored: boolean; _tiles: {[any]: Tile}; + _prevLng: number; _cache: Cache; _timers: {[any]: TimeoutID}; _cacheTimers: {[any]: TimeoutID}; @@ -392,6 +393,27 @@ class SourceCache extends Evented { this._cache.setMaxSize(maxSize); } + handleWrapJump(lng: number) { + // Wrapping longitude values can cause a jump in `wrap` values. Tiles that + // cover a similar area of the screen can have different wrap values. This + // calculates this difference in wrap values so that we can match tiles + // across frames where the longitude gets wrapped. + const prevLng = this._prevLng === undefined ? lng : this._prevLng; + const wrapDelta = Math.round((lng - prevLng) / 360); + this._prevLng = lng; + + if (wrapDelta) { + const tiles = {}; + for (const key in this._tiles) { + const tile = this._tiles[key]; + tile.tileID = tile.tileID.unwrapTo(tile.tileID.wrap + wrapDelta); + tiles[tile.tileID.key] = tile; + } + this._tiles = tiles; + this._resetTileReloadTimers(); + } + } + /** * Removes tiles that are outside the viewport and adds new tiles that * are inside the viewport. @@ -401,6 +423,8 @@ class SourceCache extends Evented { if (!this._sourceLoaded || this._paused) { return; } this.updateCacheSize(transform); + this.handleWrapJump(this.transform.center.lng); + // Covered is a list of retained tiles who's areas are fully covered by other, // better, retained tiles. They are not drawn separately. this._coveredTiles = {}; @@ -573,12 +597,12 @@ class SourceCache extends Evented { tile = this._cache.getAndRemove((tileID.wrapped().key: any)); if (tile) { // set the tileID because the cached tile could have had a different wrap value - tile.tileID = tileID; if (this._cacheTimers[tileID.key]) { clearTimeout(this._cacheTimers[tileID.key]); delete this._cacheTimers[tileID.key]; this._setTileReloadTimer(tileID.key, tile); } + tile.tileID = tileID; } const cached = Boolean(tile); @@ -612,6 +636,17 @@ class SourceCache extends Evented { } } + _resetTileReloadTimers() { + for (const id in this._timers) { + clearTimeout(this._timers[id]); + delete this._timers[id]; + } + for (const id in this._tiles) { + const tile = this._tiles[id]; + this._setTileReloadTimer(id, tile); + } + } + _setCacheInvalidationTimer(id: string | number, tile: Tile) { if (id in this._cacheTimers) { clearTimeout(this._cacheTimers[id]); diff --git a/src/source/tile_id.js b/src/source/tile_id.js index 82ce0b12002..2a507526448 100644 --- a/src/source/tile_id.js +++ b/src/source/tile_id.js @@ -68,6 +68,10 @@ export class OverscaledTileID { this.key = calculateKey(wrap, overscaledZ, x, y); } + equals(id: OverscaledTileID) { + return this.overscaledZ === id.overscaledZ && this.wrap === id.wrap && this.canonical.equals(id.canonical); + } + scaledTo(targetZ: number) { assert(targetZ <= this.overscaledZ); const zDifference = this.canonical.z - targetZ; @@ -122,6 +126,10 @@ export class OverscaledTileID { return new OverscaledTileID(this.overscaledZ, 0, this.canonical.z, this.canonical.x, this.canonical.y); } + unwrapTo(wrap: number) { + return new OverscaledTileID(this.overscaledZ, wrap, this.canonical.z, this.canonical.x, this.canonical.y); + } + overscaleFactor() { return Math.pow(2, this.overscaledZ - this.canonical.z); } diff --git a/src/style/style.js b/src/style/style.js index efbc61c26a5..41777d6366a 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -950,7 +950,7 @@ class Style extends Evented { .sort((a, b) => (b.tileID.overscaledZ - a.tileID.overscaledZ) || (a.tileID.isLessThan(b.tileID) ? -1 : 1)); } - const layerBucketsChanged = this.crossTileSymbolIndex.addLayer(styleLayer, layerTiles[styleLayer.source]); + const layerBucketsChanged = this.crossTileSymbolIndex.addLayer(styleLayer, layerTiles[styleLayer.source], transform.center.lng); symbolBucketsChanged = symbolBucketsChanged || layerBucketsChanged; } this.crossTileSymbolIndex.pruneUnusedLayers(this._order); diff --git a/src/symbol/cross_tile_symbol_index.js b/src/symbol/cross_tile_symbol_index.js index 3937e305837..7c7236ccb27 100644 --- a/src/symbol/cross_tile_symbol_index.js +++ b/src/symbol/cross_tile_symbol_index.js @@ -118,10 +118,35 @@ class CrossTileIDs { class CrossTileSymbolLayerIndex { indexes: {[zoom: string | number]: {[tileId: string | number]: TileLayerIndex}}; usedCrossTileIDs: {[zoom: string | number]: {[crossTileID: number]: boolean}}; + lng: number; constructor() { this.indexes = {}; this.usedCrossTileIDs = {}; + this.lng = 0; + } + + /* + * Sometimes when a user pans across the antimeridian the longitude value gets wrapped. + * To prevent labels from flashing out and in we adjust the tileID values in the indexes + * so that they match the new wrapped version of the map. + */ + handleWrapJump(lng: number) { + const wrapDelta = Math.round((lng - this.lng) / 360); + if (wrapDelta !== 0) { + for (const zoom in this.indexes) { + const zoomIndexes = this.indexes[zoom]; + const newZoomIndex = {}; + for (const key in zoomIndexes) { + // change the tileID's wrap and add it to a new index + const index = zoomIndexes[key]; + index.tileID = index.tileID.unwrapTo(index.tileID.wrap - wrapDelta); + newZoomIndex[index.tileID.key] = index; + } + this.indexes[zoom] = newZoomIndex; + } + } + this.lng = lng; } addBucket(tileID: OverscaledTileID, bucket: SymbolBucket, crossTileIDs: CrossTileIDs) { @@ -219,7 +244,7 @@ class CrossTileSymbolIndex { this.maxBucketInstanceId = 0; } - addLayer(styleLayer: StyleLayer, tiles: Array) { + addLayer(styleLayer: StyleLayer, tiles: Array, lng: number) { let layerIndex = this.layerIndexes[styleLayer.id]; if (layerIndex === undefined) { layerIndex = this.layerIndexes[styleLayer.id] = new CrossTileSymbolLayerIndex(); @@ -228,6 +253,8 @@ class CrossTileSymbolIndex { let symbolBucketsChanged = false; const currentBucketIDs = {}; + layerIndex.handleWrapJump(lng); + for (const tile of tiles) { const symbolBucket = ((tile.getBucket(styleLayer): any): SymbolBucket); if (!symbolBucket) continue; diff --git a/test/unit/symbol/cross_tile_symbol_index.js b/test/unit/symbol/cross_tile_symbol_index.js index 3f6b64552d6..7876bdadab9 100644 --- a/test/unit/symbol/cross_tile_symbol_index.js +++ b/test/unit/symbol/cross_tile_symbol_index.js @@ -36,7 +36,7 @@ test('CrossTileSymbolIndex.addLayer', (t) => { ]; const mainTile = makeTile(mainID, mainInstances); - index.addLayer(styleLayer, [mainTile]); + index.addLayer(styleLayer, [mainTile], 0); // Assigned new IDs t.equal(mainInstances[0].crossTileID, 1); t.equal(mainInstances[1].crossTileID, 2); @@ -50,7 +50,7 @@ test('CrossTileSymbolIndex.addLayer', (t) => { ]; const childTile = makeTile(childID, childInstances); - index.addLayer(styleLayer, [mainTile, childTile]); + index.addLayer(styleLayer, [mainTile, childTile], 0); // matched parent tile t.equal(childInstances[0].crossTileID, 1); // does not match because of different key @@ -66,7 +66,7 @@ test('CrossTileSymbolIndex.addLayer', (t) => { ]; const parentTile = makeTile(parentID, parentInstances); - index.addLayer(styleLayer, [mainTile, childTile, parentTile]); + index.addLayer(styleLayer, [mainTile, childTile, parentTile], 0); // matched child tile t.equal(parentInstances[0].crossTileID, 1); @@ -77,8 +77,8 @@ test('CrossTileSymbolIndex.addLayer', (t) => { ]; const grandchildTile = makeTile(grandchildID, grandchildInstances); - index.addLayer(styleLayer, [mainTile]); - index.addLayer(styleLayer, [mainTile, grandchildTile]); + index.addLayer(styleLayer, [mainTile], 0); + index.addLayer(styleLayer, [mainTile, grandchildTile], 0); // Matches the symbol in `mainBucket` t.equal(grandchildInstances[0].crossTileID, 1); // Does not match the previous value for Windsor because that tile was removed @@ -99,18 +99,18 @@ test('CrossTileSymbolIndex.addLayer', (t) => { const childTile = makeTile(childID, childInstances); // assigns a new id - index.addLayer(styleLayer, [mainTile]); + index.addLayer(styleLayer, [mainTile], 0); t.equal(mainInstances[0].crossTileID, 1); // removes the tile - index.addLayer(styleLayer, []); + index.addLayer(styleLayer, [], 0); // assigns a new id - index.addLayer(styleLayer, [childTile]); + index.addLayer(styleLayer, [childTile], 0); t.equal(childInstances[0].crossTileID, 2); // overwrites the old id to match the already-added tile - index.addLayer(styleLayer, [mainTile, childTile]); + index.addLayer(styleLayer, [mainTile, childTile], 0); t.equal(mainInstances[0].crossTileID, 2); t.equal(childInstances[0].crossTileID, 2); @@ -136,7 +136,7 @@ test('CrossTileSymbolIndex.addLayer', (t) => { const childTile = makeTile(childID, childInstances); // assigns new ids - index.addLayer(styleLayer, [mainTile]); + index.addLayer(styleLayer, [mainTile], 0); t.equal(mainInstances[0].crossTileID, 1); t.equal(mainInstances[1].crossTileID, 2); @@ -144,7 +144,7 @@ test('CrossTileSymbolIndex.addLayer', (t) => { t.deepEqual(Object.keys(layerIndex.usedCrossTileIDs[6]), [1, 2]); // copies parent ids without duplicate ids in this tile - index.addLayer(styleLayer, [childTile]); + index.addLayer(styleLayer, [childTile], 0); t.equal(childInstances[0].crossTileID, 1); // A' copies from A t.equal(childInstances[1].crossTileID, 2); // B' copies from B t.equal(childInstances[2].crossTileID, 3); // C' gets new ID @@ -174,7 +174,7 @@ test('CrossTileSymbolIndex.addLayer', (t) => { const secondTile = makeTile(tileID, secondInstances); // assigns new ids - index.addLayer(styleLayer, [firstTile]); + index.addLayer(styleLayer, [firstTile], 0); t.equal(firstInstances[0].crossTileID, 1); t.equal(firstInstances[1].crossTileID, 2); @@ -182,7 +182,7 @@ test('CrossTileSymbolIndex.addLayer', (t) => { t.deepEqual(Object.keys(layerIndex.usedCrossTileIDs[6]), [1, 2]); // uses same ids when tile gets updated - index.addLayer(styleLayer, [secondTile]); + index.addLayer(styleLayer, [secondTile], 0); t.equal(secondInstances[0].crossTileID, 1); // A' copies from A t.equal(secondInstances[1].crossTileID, 2); // B' copies from B t.equal(secondInstances[2].crossTileID, 3); // C' gets new ID @@ -206,7 +206,7 @@ test('CrossTileSymbolIndex.pruneUnusedLayers', (t) => { const tile = makeTile(tileID, instances); // assigns new ids - index.addLayer(styleLayer, [tile]); + index.addLayer(styleLayer, [tile], 0); t.equal(instances[0].crossTileID, 1); t.equal(instances[1].crossTileID, 2); t.ok(index.layerIndexes[styleLayer.id]);