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 18, 2018
1 parent 912e7e1 commit c9f94d4
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 18 deletions.
50 changes: 48 additions & 2 deletions 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 | void;
_cache: Cache<Tile>;
_timers: {[any]: TimeoutID};
_cacheTimers: {[any]: TimeoutID};
Expand Down Expand Up @@ -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.
Expand All @@ -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 = {};
Expand Down Expand Up @@ -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);
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 @@ -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);
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 @@ -221,7 +246,7 @@ class CrossTileSymbolIndex {
this.bucketsInCurrentPlacement = {};
}

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 @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions test/unit/source/source_cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
49 changes: 35 additions & 14 deletions test/unit/symbol/cross_tile_symbol_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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);

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

Expand All @@ -137,15 +137,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 @@ -175,15 +175,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 @@ -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();
});

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

0 comments on commit c9f94d4

Please sign in to comment.