Skip to content

Commit

Permalink
Introduce symbol cross fading when crossing integer zoom levels.
Browse files Browse the repository at this point in the history
Fixes issue #934.
Rough port of logic in mapbox/mapbox-gl-native#10468
  • Loading branch information
ChrisLoer committed Jul 16, 2018
1 parent 34ab52a commit 2bd10a4
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 15 deletions.
12 changes: 10 additions & 2 deletions src/render/painter.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ class Painter {
{
let sourceCache;
let coords = [];
let symbolCoords = [];

this.currentLayer = 0;

Expand All @@ -382,19 +383,26 @@ class Painter {
if (layer.source !== (sourceCache && sourceCache.id)) {
sourceCache = this.style.sourceCaches[layer.source];
coords = [];
symbolCoords = [];

if (sourceCache) {
this.clearStencil();
coords = sourceCache.getVisibleCoordinates();
coords = sourceCache.getVisibleCoordinates(false);
// For symbol layers in the translucent pass, we add extra tiles to
// the renderable set for cross-tile symbol fading.
// Symbol layers don't use tile clipping, so no need to render
// separate clipping masks
symbolCoords = sourceCache.getVisibleCoordinates(true);
if (sourceCache.getSource().isTileClipped) {
this._renderTileClippingMasks(coords);
}
}

coords.reverse();
symbolCoords.reverse();
}

this.renderLayer(this, (sourceCache: any), layer, coords);
this.renderLayer(this, (sourceCache: any), layer, layer.type === 'symbol' ? symbolCoords : coords);
}
}

Expand Down
42 changes: 36 additions & 6 deletions src/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class SourceCache extends Evented {
_coveredTiles: {[any]: boolean};
transform: Transform;
_isIdRenderable: (id: number) => boolean;
_isIdRenderableForSymbols: (id: number) => boolean;
used: boolean;
_state: SourceFeatureState

Expand Down Expand Up @@ -94,6 +95,7 @@ class SourceCache extends Evented {
this._maxTileCacheSize = null;

this._isIdRenderable = this._isIdRenderable.bind(this);
this._isIdRenderableForSymbols = this._isIdRenderableForSymbols.bind(this);

this._coveredTiles = {};
this._state = new SourceFeatureState();
Expand Down Expand Up @@ -190,8 +192,10 @@ class SourceCache extends Evented {
return Object.keys(this._tiles).map(Number).sort(compareKeyZoom);
}

getRenderableIds() {
return this.getIds().filter(this._isIdRenderable);
getRenderableIds(symbolLayer?: boolean) {
return symbolLayer ?
this.getIds().filter(this._isIdRenderableForSymbols) :
this.getIds().filter(this._isIdRenderable);
}

hasRenderableParent(tileID: OverscaledTileID) {
Expand All @@ -203,6 +207,10 @@ class SourceCache extends Evented {
}

_isIdRenderable(id: number) {
return this._tiles[id] && this._tiles[id].hasData() && !this._coveredTiles[id] && !this._tiles[id].holdingForFade();
}

_isIdRenderableForSymbols(id: number) {
return this._tiles[id] && this._tiles[id].hasData() && !this._coveredTiles[id];
}

Expand Down Expand Up @@ -524,10 +532,29 @@ class SourceCache extends Evented {
for (fadedParent in parentsForFading) {
retain[fadedParent] = parentsForFading[fadedParent];
}
for (const retainedId in retain) {
// Make sure retained tiles always clear any existing fade holds
// so that if they're removed again their fade timer starts fresh.
this._tiles[retainedId].clearFadeHold();
}
// Remove the tiles we don't need anymore.
const remove = keysDifference(this._tiles, retain);
for (let i = 0; i < remove.length; i++) {
this._removeTile(remove[i]);
const tileID = remove[i];
const tile = this._tiles[tileID];
if (tile.hasSymbolBuckets && !tile.holdingForFade()) {
tile.setHoldDuration(this.map._fadeDuration);
} else if (!tile.hasSymbolBuckets || tile.symbolFadeFinished()) {
this._removeTile(tileID);
}
}
}

releaseSymbolFadeTiles() {
for (const id in this._tiles) {
if (this._tiles[id].holdingForFade()) {
this._removeTile(id);
}
}
}

Expand Down Expand Up @@ -734,6 +761,10 @@ class SourceCache extends Evented {

for (let i = 0; i < ids.length; i++) {
const tile = this._tiles[ids[i]];
if (tile.holdingForFade()) {
// Tiles held for fading are covered by tiles that are closer to ideal
continue;
}
const tileID = tile.tileID;
const scale = Math.pow(2, this.transform.zoom - tile.tileID.overscaledZ);
const queryPadding = maxPitchScaleFactor * tile.queryPadding * EXTENT / tile.tileSize / scale;
Expand Down Expand Up @@ -763,8 +794,8 @@ class SourceCache extends Evented {
return tileResults;
}

getVisibleCoordinates() {
const coords = this.getRenderableIds().map((id) => this._tiles[id].tileID);
getVisibleCoordinates(symbolLayer?: boolean) {
const coords = this.getRenderableIds(symbolLayer).map((id) => this._tiles[id].tileID);
for (const coord of coords) {
coord.posMatrix = this.transform.calculatePosMatrix(coord.toUnwrapped());
}
Expand Down Expand Up @@ -827,4 +858,3 @@ function isRasterType(type) {
}

export default SourceCache;

32 changes: 28 additions & 4 deletions src/source/tile.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class Tile {
resourceTiming: ?Array<PerformanceResourceTiming>;
queryPadding: number;

symbolFadeHoldUntil: ?number;
hasSymbolBuckets: boolean;

/**
* @param {OverscaledTileID} tileID
* @param size
Expand All @@ -103,6 +106,7 @@ class Tile {
this.buckets = {};
this.expirationTime = null;
this.queryPadding = 0;
this.hasSymbolBuckets = false;

// Counts the number of times a response was already expired when
// received. We're using this to add a delay when making a new request
Expand Down Expand Up @@ -164,11 +168,15 @@ class Tile {
this.collisionBoxArray = data.collisionBoxArray;
this.buckets = deserializeBucket(data.buckets, painter.style);

if (justReloaded) {
for (const id in this.buckets) {
const bucket = this.buckets[id];
if (bucket instanceof SymbolBucket) {
this.hasSymbolBuckets = false;
for (const id in this.buckets) {
const bucket = this.buckets[id];
if (bucket instanceof SymbolBucket) {
this.hasSymbolBuckets = true;
if (justReloaded) {
bucket.justReloaded = true;
} else {
break;
}
}
}
Expand Down Expand Up @@ -438,6 +446,22 @@ class Tile {
}
}
}

holdingForFade(): boolean {
return this.symbolFadeHoldUntil !== undefined;
}

symbolFadeFinished(): boolean {
return !this.symbolFadeHoldUntil || this.symbolFadeHoldUntil < browser.now();
}

clearFadeHold() {
this.symbolFadeHoldUntil = undefined;
}

setHoldDuration(duration: number) {
this.symbolFadeHoldUntil = browser.now() + duration;
}
}

export default Tile;
8 changes: 7 additions & 1 deletion src/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,7 @@ class Style extends Evented {

if (!layerTiles[styleLayer.source]) {
const sourceCache = this.sourceCaches[styleLayer.source];
layerTiles[styleLayer.source] = sourceCache.getRenderableIds()
layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true)
.map((id) => sourceCache.getTileByID(id))
.sort((a, b) => (b.tileID.overscaledZ - a.tileID.overscaledZ) || (a.tileID.isLessThan(b.tileID) ? -1 : 1));
}
Expand Down Expand Up @@ -1086,6 +1086,12 @@ class Style extends Evented {
return needsRerender;
}

_releaseSymbolFadeTiles() {
for (const id in this.sourceCaches) {
this.sourceCaches[id].releaseSymbolFadeTiles();
}
}

// Callbacks from web workers

getImages(mapId: string, params: {icons: Array<string>}, callback: Callback<{[string]: StyleImage}>) {
Expand Down
10 changes: 8 additions & 2 deletions src/symbol/placement.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,11 @@ export class Placement {
);

this.placeLayerBucket(symbolBucket, posMatrix, textLabelPlaneMatrix, iconLabelPlaneMatrix, scale, textPixelRatio,
showCollisionBoxes, seenCrossTileIDs, collisionBoxArray);
showCollisionBoxes, tile.holdingForFade(), seenCrossTileIDs, collisionBoxArray);
}

placeLayerBucket(bucket: SymbolBucket, posMatrix: mat4, textLabelPlaneMatrix: mat4, iconLabelPlaneMatrix: mat4,
scale: number, textPixelRatio: number, showCollisionBoxes: boolean, seenCrossTileIDs: { [string | number]: boolean },
scale: number, textPixelRatio: number, showCollisionBoxes: boolean, holdingForFade: boolean, seenCrossTileIDs: { [string | number]: boolean },
collisionBoxArray: ?CollisionBoxArray) {
const layout = bucket.layers[0].layout;

Expand All @@ -193,6 +193,12 @@ export class Placement {

for (const symbolInstance of bucket.symbolInstances) {
if (!seenCrossTileIDs[symbolInstance.crossTileID]) {
if (holdingForFade) {
// Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
// know yet if we have a duplicate in a parent tile that _should_ be placed.
this.placements[symbolInstance.crossTileID] = new JointPlacement(false, false, false);
continue;
}

let placeText = false;
let placeIcon = false;
Expand Down
7 changes: 7 additions & 0 deletions src/ui/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,13 @@ class Map extends Camera {
this._styleDirty = true;
}

if (this.style && !this._placementDirty) {
// Since no fade operations are in progress, we can release
// all tiles held for fading. If we didn't do this, the tiles
// would just sit in the SourceCaches until the next render
this.style._releaseSymbolFadeTiles();
}

// Schedule another render frame if it's needed.
//
// Even though `_styleDirty` and `_sourcesDirty` are reset in this
Expand Down

0 comments on commit 2bd10a4

Please sign in to comment.