From c9f94d4ff802533ea2879d9d271b83ee14f831d2 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 | 50 ++++++++++++++++++++- src/source/tile_id.js | 8 ++++ src/style/style.js | 2 +- src/symbol/cross_tile_symbol_index.js | 29 +++++++++++- test/unit/source/source_cache.test.js | 26 +++++++++++ test/unit/symbol/cross_tile_symbol_index.js | 49 ++++++++++++++------ 6 files changed, 146 insertions(+), 18 deletions(-) diff --git a/src/source/source_cache.js b/src/source/source_cache.js index a9f97d0b8af..36a69738c75 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 | void; _cache: Cache; _timers: {[any]: TimeoutID}; _cacheTimers: {[any]: TimeoutID}; @@ -392,6 +393,49 @@ class SourceCache extends Evented { this._cache.setMaxSize(maxSize); } + handleWrapJump(lng: number) { + // On top of the regular z/x/y values, TileIDs have a `wrap` value that specify + // which cppy of the world the tile belongs to. For example, at `lng: 10` you + // might render z/x/y/0 while at `lng: 370` you would render z/x/y/1. + // + // When lng values get wrapped (going from `lng: 370` to `long: 10`) you expect + // to see the same thing on the screen (370 degrees and 10 degrees is the same + // place in the world) but all the TileIDs will have different wrap values. + // + // In order to make this transition seamless, we calculate the rounded difference of + // "worlds" between the last frame and the current frame. If the map panned by + // a world, then we can assign all the tiles new TileIDs with updated wrap values. + // For example, assign z/x/y/1 a new id: z/x/y/0. It is the same tile, just rendered + // in a different position. + // + // This enables us to reuse the tiles at more ideal locations and prevent flickering. + const prevLng = this._prevLng === undefined ? lng : this._prevLng; + const lngDifference = lng - prevLng; + const worldDifference = lngDifference / 360; + const wrapDelta = Math.round(worldDifference); + 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; + + // Reset tile reload timers + 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); + } + } + } + /** * Removes tiles that are outside the viewport and adds new tiles that * are inside the viewport. @@ -401,6 +445,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 = {}; @@ -572,13 +618,13 @@ 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); } + // set the tileID because the cached tile could have had a different wrap value + tile.tileID = tileID; } const cached = Boolean(tile); 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 327147ac4b1..e34f9d6995a 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -975,7 +975,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 4d7d3566d1c..609edcf70bb 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) { @@ -221,7 +246,7 @@ class CrossTileSymbolIndex { this.bucketsInCurrentPlacement = {}; } - 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(); @@ -230,6 +255,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/source/source_cache.test.js b/test/unit/source/source_cache.test.js index 3fe7b26beb6..3a8ff81a7d2 100644 --- a/test/unit/source/source_cache.test.js +++ b/test/unit/source/source_cache.test.js @@ -709,6 +709,32 @@ test('SourceCache#update', (t) => { sourceCache.onAdd(); }); + t.test('reassigns tiles for large jumps in longitude', (t) => { + + const transform = new Transform(); + transform.resize(511, 511); + transform.zoom = 0; + + const sourceCache = createSourceCache({}); + sourceCache.on('data', (e) => { + if (e.sourceDataType === 'metadata') { + transform.center = new LngLat(360, 0); + const tileID = new OverscaledTileID(0, 1, 0, 0, 0); + sourceCache.update(transform); + t.deepEqual(sourceCache.getIds(), [tileID.key]); + const tile = sourceCache.getTile(tileID); + + transform.center = new LngLat(0, 0); + const wrappedTileID = new OverscaledTileID(0, 0, 0, 0, 0); + sourceCache.update(transform); + t.deepEqual(sourceCache.getIds(), [wrappedTileID.key]); + t.equal(sourceCache.getTile(wrappedTileID), tile); + t.end(); + } + }); + sourceCache.onAdd(); + }); + t.end(); }); diff --git a/test/unit/symbol/cross_tile_symbol_index.js b/test/unit/symbol/cross_tile_symbol_index.js index 92d3e31dabb..298e2437113 100644 --- a/test/unit/symbol/cross_tile_symbol_index.js +++ b/test/unit/symbol/cross_tile_symbol_index.js @@ -37,7 +37,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); @@ -51,7 +51,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 @@ -67,7 +67,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); @@ -78,8 +78,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 @@ -100,18 +100,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); @@ -137,7 +137,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); @@ -145,7 +145,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 @@ -175,7 +175,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); @@ -183,7 +183,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 @@ -193,6 +193,27 @@ test('CrossTileSymbolIndex.addLayer', (t) => { t.end(); }); + t.test('reuses indexes when longitude is wrapped', (t) => { + const index = new CrossTileSymbolIndex(); + const longitude = 370; + + const tileID = new OverscaledTileID(6, 1, 6, 8, 8); + const firstInstances = [ + makeSymbolInstance(1000, 1000, ""), // A + ]; + const tile = makeTile(tileID, firstInstances); + + index.addLayer(styleLayer, [tile], longitude); + t.equal(firstInstances[0].crossTileID, 1); // A + + tile.tileID = tileID.wrapped(); + + index.addLayer(styleLayer, [tile], longitude % 360); + t.equal(firstInstances[0].crossTileID, 1); + t.end(); + + }); + t.end(); }); @@ -207,7 +228,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]);