Skip to content

Commit

Permalink
avoid flickering when longitude is wrapped while panning
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ansis committed Apr 2, 2018
1 parent 06ea9c3 commit ff393e1
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 17 deletions.
37 changes: 36 additions & 1 deletion src/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class SourceCache extends Evented {
_sourceLoaded: boolean;
_sourceErrored: boolean;
_tiles: {[any]: Tile};
_prevLng: number;
_cache: Cache<Tile>;
_timers: {[any]: TimeoutID};
_cacheTimers: {[any]: TimeoutID};
Expand Down Expand Up @@ -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.
Expand All @@ -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 = {};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]);
Expand Down
8 changes: 8 additions & 0 deletions src/source/tile_id.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
29 changes: 28 additions & 1 deletion src/symbol/cross_tile_symbol_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -219,7 +244,7 @@ class CrossTileSymbolIndex {
this.maxBucketInstanceId = 0;
}

addLayer(styleLayer: StyleLayer, tiles: Array<Tile>) {
addLayer(styleLayer: StyleLayer, tiles: Array<Tile>, lng: number) {
let layerIndex = this.layerIndexes[styleLayer.id];
if (layerIndex === undefined) {
layerIndex = this.layerIndexes[styleLayer.id] = new CrossTileSymbolLayerIndex();
Expand All @@ -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;
Expand Down
28 changes: 14 additions & 14 deletions test/unit/symbol/cross_tile_symbol_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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);

Expand All @@ -136,15 +136,15 @@ 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);

const layerIndex = index.layerIndexes[styleLayer.id];
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
Expand Down Expand Up @@ -174,15 +174,15 @@ 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);

const layerIndex = index.layerIndexes[styleLayer.id];
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
Expand All @@ -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]);
Expand Down

0 comments on commit ff393e1

Please sign in to comment.