mapbox-gl
Version:
A WebGL interactive maps library
756 lines (672 loc) • 36.8 kB
JavaScript
// @flow
import CollisionIndex from './collision_index';
import EXTENT from '../data/extent';
import * as symbolSize from './symbol_size';
import * as projection from './projection';
import { getAnchorJustification, evaluateRadialOffset } from './symbol_layout';
import { getAnchorAlignment } from './shaping';
import assert from 'assert';
import pixelsToTileUnits from '../source/pixels_to_tile_units';
import Point from '@mapbox/point-geometry';
import type Transform from '../geo/transform';
import type StyleLayer from '../style/style_layer';
import type Tile from '../source/tile';
import type SymbolBucket, { CollisionArrays, SingleCollisionBox } from '../data/bucket/symbol_bucket';
import type {mat4} from 'gl-matrix';
import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../data/array_types';
import type FeatureIndex from '../data/feature_index';
import type {OverscaledTileID} from '../source/tile_id';
import type {TextAnchor} from './symbol_layout';
class OpacityState {
opacity: number;
placed: boolean;
constructor(prevState: ?OpacityState, increment: number, placed: boolean, skipFade: ?boolean) {
if (prevState) {
this.opacity = Math.max(0, Math.min(1, prevState.opacity + (prevState.placed ? increment : -increment)));
} else {
this.opacity = (skipFade && placed) ? 1 : 0;
}
this.placed = placed;
}
isHidden() {
return this.opacity === 0 && !this.placed;
}
}
class JointOpacityState {
text: OpacityState;
icon: OpacityState;
constructor(prevState: ?JointOpacityState, increment: number, placedText: boolean, placedIcon: boolean, skipFade: ?boolean) {
this.text = new OpacityState(prevState ? prevState.text : null, increment, placedText, skipFade);
this.icon = new OpacityState(prevState ? prevState.icon : null, increment, placedIcon, skipFade);
}
isHidden() {
return this.text.isHidden() && this.icon.isHidden();
}
}
class JointPlacement {
text: boolean;
icon: boolean;
// skipFade = outside viewport, but within CollisionIndex::viewportPadding px of the edge
// Because these symbols aren't onscreen yet, we can skip the "fade in" animation,
// and if a subsequent viewport change brings them into view, they'll be fully
// visible right away.
skipFade: boolean;
constructor(text: boolean, icon: boolean, skipFade: boolean) {
this.text = text;
this.icon = icon;
this.skipFade = skipFade;
}
}
export class RetainedQueryData {
bucketInstanceId: number;
featureIndex: FeatureIndex;
sourceLayerIndex: number;
bucketIndex: number;
tileID: OverscaledTileID;
featureSortOrder: ?Array<number>
constructor(bucketInstanceId: number,
featureIndex: FeatureIndex,
sourceLayerIndex: number,
bucketIndex: number,
tileID: OverscaledTileID) {
this.bucketInstanceId = bucketInstanceId;
this.featureIndex = featureIndex;
this.sourceLayerIndex = sourceLayerIndex;
this.bucketIndex = bucketIndex;
this.tileID = tileID;
}
}
type CollisionGroup = { ID: number, predicate?: any };
class CollisionGroups {
collisionGroups: {[groupName: string]: CollisionGroup};
maxGroupID: number;
crossSourceCollisions: boolean;
constructor(crossSourceCollisions: boolean) {
this.crossSourceCollisions = crossSourceCollisions;
this.maxGroupID = 0;
this.collisionGroups = {};
}
get(sourceID: string) {
// The predicate/groupID mechanism allows for arbitrary grouping,
// but the current interface defines one source == one group when
// crossSourceCollisions == true.
if (!this.crossSourceCollisions) {
if (!this.collisionGroups[sourceID]) {
const nextGroupID = ++this.maxGroupID;
this.collisionGroups[sourceID] = {
ID: nextGroupID,
predicate: (key) => {
return key.collisionGroupID === nextGroupID;
}
};
}
return this.collisionGroups[sourceID];
} else {
return { ID: 0, predicate: null };
}
}
}
function calculateVariableLayoutOffset(anchor: TextAnchor, width: number, height: number, radialOffset: number, textBoxScale: number): Point {
const {horizontalAlign, verticalAlign} = getAnchorAlignment(anchor);
const shiftX = -(horizontalAlign - 0.5) * width;
const shiftY = -(verticalAlign - 0.5) * height;
const offset = evaluateRadialOffset(anchor, radialOffset);
return new Point(
shiftX + offset[0] * textBoxScale,
shiftY + offset[1] * textBoxScale
);
}
function shiftVariableCollisionBox(collisionBox: SingleCollisionBox,
shiftX: number, shiftY: number,
rotateWithMap: boolean, pitchWithMap: boolean,
angle: number) {
const {x1, x2, y1, y2, anchorPointX, anchorPointY} = collisionBox;
const rotatedOffset = new Point(shiftX, shiftY);
if (rotateWithMap) {
rotatedOffset._rotate(pitchWithMap ? angle : -angle);
}
return {
x1: x1 + rotatedOffset.x,
y1: y1 + rotatedOffset.y,
x2: x2 + rotatedOffset.x,
y2: y2 + rotatedOffset.y,
// symbol anchor point stays the same regardless of text-anchor
anchorPointX,
anchorPointY
};
}
export type VariableOffset = {
radialOffset: number,
width: number,
height: number,
anchor: TextAnchor,
textBoxScale: number,
prevAnchor?: TextAnchor
};
export type CrossTileID = string | number;
export class Placement {
transform: Transform;
collisionIndex: CollisionIndex;
placements: { [CrossTileID]: JointPlacement };
opacities: { [CrossTileID]: JointOpacityState };
variableOffsets: {[CrossTileID]: VariableOffset };
commitTime: number;
lastPlacementChangeTime: number;
stale: boolean;
fadeDuration: number;
retainedQueryData: {[number]: RetainedQueryData};
collisionGroups: CollisionGroups;
prevPlacement: ?Placement;
constructor(transform: Transform, fadeDuration: number, crossSourceCollisions: boolean, prevPlacement?: Placement) {
this.transform = transform.clone();
this.collisionIndex = new CollisionIndex(this.transform);
this.placements = {};
this.opacities = {};
this.variableOffsets = {};
this.stale = false;
this.commitTime = 0;
this.fadeDuration = fadeDuration;
this.retainedQueryData = {};
this.collisionGroups = new CollisionGroups(crossSourceCollisions);
this.prevPlacement = prevPlacement;
if (prevPlacement) {
prevPlacement.prevPlacement = undefined; // Only hold on to one placement back
}
}
placeLayerTile(styleLayer: StyleLayer, tile: Tile, showCollisionBoxes: boolean, seenCrossTileIDs: { [string | number]: boolean }) {
const symbolBucket = ((tile.getBucket(styleLayer): any): SymbolBucket);
const bucketFeatureIndex = tile.latestFeatureIndex;
if (!symbolBucket || !bucketFeatureIndex || styleLayer.id !== symbolBucket.layerIds[0])
return;
const collisionBoxArray = tile.collisionBoxArray;
const layout = symbolBucket.layers[0].layout;
const scale = Math.pow(2, this.transform.zoom - tile.tileID.overscaledZ);
const textPixelRatio = tile.tileSize / EXTENT;
const posMatrix = this.transform.calculatePosMatrix(tile.tileID.toUnwrapped());
const textLabelPlaneMatrix = projection.getLabelPlaneMatrix(posMatrix,
layout.get('text-pitch-alignment') === 'map',
layout.get('text-rotation-alignment') === 'map',
this.transform,
pixelsToTileUnits(tile, 1, this.transform.zoom));
const iconLabelPlaneMatrix = projection.getLabelPlaneMatrix(posMatrix,
layout.get('icon-pitch-alignment') === 'map',
layout.get('icon-rotation-alignment') === 'map',
this.transform,
pixelsToTileUnits(tile, 1, this.transform.zoom));
// As long as this placement lives, we have to hold onto this bucket's
// matching FeatureIndex/data for querying purposes
this.retainedQueryData[symbolBucket.bucketInstanceId] = new RetainedQueryData(
symbolBucket.bucketInstanceId,
bucketFeatureIndex,
symbolBucket.sourceLayerIndex,
symbolBucket.index,
tile.tileID
);
this.placeLayerBucket(symbolBucket, posMatrix, textLabelPlaneMatrix, iconLabelPlaneMatrix, scale, textPixelRatio,
showCollisionBoxes, tile.holdingForFade(), seenCrossTileIDs, collisionBoxArray);
}
attemptAnchorPlacement(anchor: TextAnchor, textBox: SingleCollisionBox, width: number, height: number,
radialTextOffset: number, textBoxScale: number, rotateWithMap: boolean,
pitchWithMap: boolean, textPixelRatio: number, posMatrix: mat4, collisionGroup: CollisionGroup,
textAllowOverlap: boolean, symbolInstance: SymbolInstance, bucket: SymbolBucket) {
const shift = calculateVariableLayoutOffset(anchor, width, height, radialTextOffset, textBoxScale);
const placedGlyphBoxes = this.collisionIndex.placeCollisionBox(
shiftVariableCollisionBox(
textBox, shift.x, shift.y,
rotateWithMap, pitchWithMap, this.transform.angle),
textAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate);
if (placedGlyphBoxes.box.length > 0) {
let prevAnchor;
// If this label was placed in the previous placement, record the anchor position
// to allow us to animate the transition
if (this.prevPlacement &&
this.prevPlacement.variableOffsets[symbolInstance.crossTileID] &&
this.prevPlacement.placements[symbolInstance.crossTileID] &&
this.prevPlacement.placements[symbolInstance.crossTileID].text) {
prevAnchor = this.prevPlacement.variableOffsets[symbolInstance.crossTileID].anchor;
}
assert(symbolInstance.crossTileID !== 0);
this.variableOffsets[symbolInstance.crossTileID] = {
radialOffset: radialTextOffset,
width,
height,
anchor,
textBoxScale,
prevAnchor
};
this.markUsedJustification(bucket, anchor, symbolInstance);
return placedGlyphBoxes;
}
}
placeLayerBucket(bucket: SymbolBucket, posMatrix: mat4, textLabelPlaneMatrix: mat4, iconLabelPlaneMatrix: mat4,
scale: number, textPixelRatio: number, showCollisionBoxes: boolean, holdingForFade: boolean, seenCrossTileIDs: { [string | number]: boolean },
collisionBoxArray: ?CollisionBoxArray) {
const layout = bucket.layers[0].layout;
const partiallyEvaluatedTextSize = symbolSize.evaluateSizeForZoom(bucket.textSizeData, this.transform.zoom);
const textOptional = layout.get('text-optional');
const iconOptional = layout.get('icon-optional');
const textAllowOverlap = layout.get('text-allow-overlap');
const iconAllowOverlap = layout.get('icon-allow-overlap');
// This logic is similar to the "defaultOpacityState" logic below in updateBucketOpacities
// If we know a symbol is always supposed to show, force it to be marked visible even if
// it wasn't placed into the collision index (because some or all of it was outside the range
// of the collision grid).
// There is a subtle edge case here we're accepting:
// Symbol A has text-allow-overlap: true, icon-allow-overlap: true, icon-optional: false
// A's icon is outside the grid, so doesn't get placed
// A's text would be inside grid, but doesn't get placed because of icon-optional: false
// We still show A because of the allow-overlap settings.
// Symbol B has allow-overlap: false, and gets placed where A's text would be
// On panning in, there is a short period when Symbol B and Symbol A will overlap
// This is the reverse of our normal policy of "fade in on pan", but should look like any other
// collision and hopefully not be too noticeable.
// See https://github.com/mapbox/mapbox-gl-js/issues/7172
const alwaysShowText = textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || iconOptional);
const alwaysShowIcon = iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || textOptional);
const collisionGroup = this.collisionGroups.get(bucket.sourceID);
const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
const zOrderByViewportY = layout.get('symbol-z-order') === 'viewport-y';
if (!bucket.collisionArrays && collisionBoxArray) {
bucket.deserializeCollisionBoxes(collisionBoxArray);
}
const placeSymbol = (symbolInstance: SymbolInstance, collisionArrays: CollisionArrays) => {
if (seenCrossTileIDs[symbolInstance.crossTileID]) return;
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);
return;
}
let placeText = false;
let placeIcon = false;
let offscreen = true;
let placedGlyphBoxes = null;
let placedGlyphCircles = null;
let placedIconBoxes = null;
let textFeatureIndex = 0;
let iconFeatureIndex = 0;
if (collisionArrays.textFeatureIndex) {
textFeatureIndex = collisionArrays.textFeatureIndex;
}
const textBox = collisionArrays.textBox;
if (textBox) {
if (!layout.get('text-variable-anchor')) {
placedGlyphBoxes = this.collisionIndex.placeCollisionBox(textBox,
layout.get('text-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate);
placeText = placedGlyphBoxes.box.length > 0;
} else {
const width = textBox.x2 - textBox.x1;
const height = textBox.y2 - textBox.y1;
const textBoxScale = symbolInstance.textBoxScale;
let anchors = layout.get('text-variable-anchor');
// If this symbol was in the last placement, shift the previously used
// anchor to the front of the anchor list, only if the previous anchor
// is still in the anchor list
if (this.prevPlacement && this.prevPlacement.variableOffsets[symbolInstance.crossTileID]) {
const prevOffsets = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
if (anchors.indexOf(prevOffsets.anchor) > 0) {
anchors = anchors.filter(anchor => anchor !== prevOffsets.anchor);
anchors.unshift(prevOffsets.anchor);
}
}
for (const anchor of anchors) {
placedGlyphBoxes = this.attemptAnchorPlacement(
anchor, textBox, width, height, symbolInstance.radialTextOffset,
textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix,
collisionGroup, textAllowOverlap, symbolInstance, bucket);
if (placedGlyphBoxes) {
placeText = true;
break;
}
}
// If we didn't get placed, we still need to copy our position from the last placement for
// fade animations
if (!this.variableOffsets[symbolInstance.crossTileID] && this.prevPlacement) {
const prevOffset = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
if (prevOffset) {
this.variableOffsets[symbolInstance.crossTileID] = prevOffset;
this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance);
}
}
}
}
offscreen = placedGlyphBoxes && placedGlyphBoxes.offscreen;
const textCircles = collisionArrays.textCircles;
if (textCircles) {
const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex);
const fontSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol);
placedGlyphCircles = this.collisionIndex.placeCollisionCircles(textCircles,
layout.get('text-allow-overlap'),
scale,
textPixelRatio,
placedSymbol,
bucket.lineVertexArray,
bucket.glyphOffsetArray,
fontSize,
posMatrix,
textLabelPlaneMatrix,
showCollisionBoxes,
pitchWithMap,
collisionGroup.predicate);
// If text-allow-overlap is set, force "placedCircles" to true
// In theory there should always be at least one circle placed
// in this case, but for now quirks in text-anchor
// and text-offset may prevent that from being true.
placeText = layout.get('text-allow-overlap') || placedGlyphCircles.circles.length > 0;
offscreen = offscreen && placedGlyphCircles.offscreen;
}
if (collisionArrays.iconFeatureIndex) {
iconFeatureIndex = collisionArrays.iconFeatureIndex;
}
if (collisionArrays.iconBox) {
placedIconBoxes = this.collisionIndex.placeCollisionBox(collisionArrays.iconBox,
layout.get('icon-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate);
placeIcon = placedIconBoxes.box.length > 0;
offscreen = offscreen && placedIconBoxes.offscreen;
}
const iconWithoutText = textOptional ||
(symbolInstance.numHorizontalGlyphVertices === 0 && symbolInstance.numVerticalGlyphVertices === 0);
const textWithoutIcon = iconOptional || symbolInstance.numIconVertices === 0;
// Combine the scales for icons and text.
if (!iconWithoutText && !textWithoutIcon) {
placeIcon = placeText = placeIcon && placeText;
} else if (!textWithoutIcon) {
placeText = placeIcon && placeText;
} else if (!iconWithoutText) {
placeIcon = placeIcon && placeText;
}
if (placeText && placedGlyphBoxes) {
this.collisionIndex.insertCollisionBox(placedGlyphBoxes.box, layout.get('text-ignore-placement'),
bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID);
}
if (placeIcon && placedIconBoxes) {
this.collisionIndex.insertCollisionBox(placedIconBoxes.box, layout.get('icon-ignore-placement'),
bucket.bucketInstanceId, iconFeatureIndex, collisionGroup.ID);
}
if (placeText && placedGlyphCircles) {
this.collisionIndex.insertCollisionCircles(placedGlyphCircles.circles, layout.get('text-ignore-placement'),
bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID);
}
assert(symbolInstance.crossTileID !== 0);
assert(bucket.bucketInstanceId !== 0);
this.placements[symbolInstance.crossTileID] = new JointPlacement(placeText || alwaysShowText, placeIcon || alwaysShowIcon, offscreen || bucket.justReloaded);
seenCrossTileIDs[symbolInstance.crossTileID] = true;
};
if (zOrderByViewportY) {
const symbolIndexes = bucket.getSortedSymbolIndexes(this.transform.angle);
for (let i = symbolIndexes.length - 1; i >= 0; --i) {
const symbolIndex = symbolIndexes[i];
placeSymbol(bucket.symbolInstances.get(symbolIndex), bucket.collisionArrays[symbolIndex]);
}
} else {
for (let i = 0; i < bucket.symbolInstances.length; ++i) {
placeSymbol(bucket.symbolInstances.get(i), bucket.collisionArrays[i]);
}
}
bucket.justReloaded = false;
}
markUsedJustification(bucket: SymbolBucket, placedAnchor: TextAnchor, symbolInstance: SymbolInstance) {
const justifications = {
"left": symbolInstance.leftJustifiedTextSymbolIndex,
"center": symbolInstance.centerJustifiedTextSymbolIndex,
"right": symbolInstance.rightJustifiedTextSymbolIndex
};
const autoIndex = justifications[getAnchorJustification(placedAnchor)];
for (const justification in justifications) {
const index = justifications[justification];
if (index >= 0) {
if (autoIndex >= 0 && index !== autoIndex) {
// There are multiple justifications and this one isn't it: shift offscreen
bucket.text.placedSymbolArray.get(index).crossTileID = 0;
} else {
// Either this is the chosen justification or the justification is hardwired: use this one
bucket.text.placedSymbolArray.get(index).crossTileID = symbolInstance.crossTileID;
}
}
}
}
commit(now: number): void {
this.commitTime = now;
const prevPlacement = this.prevPlacement;
let placementChanged = false;
const increment = (prevPlacement && this.fadeDuration !== 0) ?
(this.commitTime - prevPlacement.commitTime) / this.fadeDuration :
1;
const prevOpacities = prevPlacement ? prevPlacement.opacities : {};
const prevOffsets = prevPlacement ? prevPlacement.variableOffsets : {};
// add the opacities from the current placement, and copy their current values from the previous placement
for (const crossTileID in this.placements) {
const jointPlacement = this.placements[crossTileID];
const prevOpacity = prevOpacities[crossTileID];
if (prevOpacity) {
this.opacities[crossTileID] = new JointOpacityState(prevOpacity, increment, jointPlacement.text, jointPlacement.icon);
placementChanged = placementChanged ||
jointPlacement.text !== prevOpacity.text.placed ||
jointPlacement.icon !== prevOpacity.icon.placed;
} else {
this.opacities[crossTileID] = new JointOpacityState(null, increment, jointPlacement.text, jointPlacement.icon, jointPlacement.skipFade);
placementChanged = placementChanged || jointPlacement.text || jointPlacement.icon;
}
}
// copy and update values from the previous placement that aren't in the current placement but haven't finished fading
for (const crossTileID in prevOpacities) {
const prevOpacity = prevOpacities[crossTileID];
if (!this.opacities[crossTileID]) {
const jointOpacity = new JointOpacityState(prevOpacity, increment, false, false);
if (!jointOpacity.isHidden()) {
this.opacities[crossTileID] = jointOpacity;
placementChanged = placementChanged || prevOpacity.text.placed || prevOpacity.icon.placed;
}
}
}
for (const crossTileID in prevOffsets) {
if (!this.variableOffsets[crossTileID] && this.opacities[crossTileID] && !this.opacities[crossTileID].isHidden()) {
this.variableOffsets[crossTileID] = prevOffsets[crossTileID];
}
}
// this.lastPlacementChangeTime is the time of the last commit() that
// resulted in a placement change -- in other words, the start time of
// the last symbol fade animation
assert(!prevPlacement || prevPlacement.lastPlacementChangeTime !== undefined);
if (placementChanged) {
this.lastPlacementChangeTime = now;
} else if (typeof this.lastPlacementChangeTime !== 'number') {
this.lastPlacementChangeTime = prevPlacement ? prevPlacement.lastPlacementChangeTime : now;
}
}
updateLayerOpacities(styleLayer: StyleLayer, tiles: Array<Tile>) {
const seenCrossTileIDs = {};
for (const tile of tiles) {
const symbolBucket = ((tile.getBucket(styleLayer): any): SymbolBucket);
if (symbolBucket && tile.latestFeatureIndex && styleLayer.id === symbolBucket.layerIds[0]) {
this.updateBucketOpacities(symbolBucket, seenCrossTileIDs, tile.collisionBoxArray);
}
}
}
updateBucketOpacities(bucket: SymbolBucket, seenCrossTileIDs: { [string | number]: boolean }, collisionBoxArray: ?CollisionBoxArray) {
if (bucket.hasTextData()) bucket.text.opacityVertexArray.clear();
if (bucket.hasIconData()) bucket.icon.opacityVertexArray.clear();
if (bucket.hasCollisionBoxData()) bucket.collisionBox.collisionVertexArray.clear();
if (bucket.hasCollisionCircleData()) bucket.collisionCircle.collisionVertexArray.clear();
const layout = bucket.layers[0].layout;
const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true);
const textAllowOverlap = layout.get('text-allow-overlap');
const iconAllowOverlap = layout.get('icon-allow-overlap');
const variablePlacement = layout.get('text-variable-anchor');
const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
// If allow-overlap is true, we can show symbols before placement runs on them
// But we have to wait for placement if we potentially depend on a paired icon/text
// with allow-overlap: false.
// See https://github.com/mapbox/mapbox-gl-js/issues/7032
const defaultOpacityState = new JointOpacityState(null, 0,
textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || layout.get('icon-optional')),
iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || layout.get('text-optional')),
true);
if (!bucket.collisionArrays && collisionBoxArray && (bucket.hasCollisionBoxData() || bucket.hasCollisionCircleData())) {
bucket.deserializeCollisionBoxes(collisionBoxArray);
}
for (let s = 0; s < bucket.symbolInstances.length; s++) {
const symbolInstance = bucket.symbolInstances.get(s);
const {
numHorizontalGlyphVertices,
numVerticalGlyphVertices,
crossTileID
} = symbolInstance;
const isDuplicate = seenCrossTileIDs[crossTileID];
let opacityState = this.opacities[crossTileID];
if (isDuplicate) {
opacityState = duplicateOpacityState;
} else if (!opacityState) {
opacityState = defaultOpacityState;
// store the state so that future placements use it as a starting point
this.opacities[crossTileID] = opacityState;
}
seenCrossTileIDs[crossTileID] = true;
const hasText = numHorizontalGlyphVertices > 0 || numVerticalGlyphVertices > 0;
const hasIcon = symbolInstance.numIconVertices > 0;
if (hasText) {
const packedOpacity = packOpacity(opacityState.text);
// Vertical text fades in/out on collision the same way as corresponding
// horizontal text. Switch between vertical/horizontal should be instantaneous
const opacityEntryCount = (numHorizontalGlyphVertices + numVerticalGlyphVertices) / 4;
for (let i = 0; i < opacityEntryCount; i++) {
bucket.text.opacityVertexArray.emplaceBack(packedOpacity);
}
// If this label is completely faded, mark it so that we don't have to calculate
// its position at render time. If this layer has variable placement, shift the various
// symbol instances appropriately so that symbols from buckets that have yet to be placed
// offset appropriately.
const hidden = opacityState.text.isHidden() ? 1 : 0;
[
symbolInstance.rightJustifiedTextSymbolIndex,
symbolInstance.centerJustifiedTextSymbolIndex,
symbolInstance.leftJustifiedTextSymbolIndex,
symbolInstance.verticalPlacedTextSymbolIndex
].forEach(index => {
if (index >= 0) {
bucket.text.placedSymbolArray.get(index).hidden = hidden;
}
});
const prevOffset = this.variableOffsets[symbolInstance.crossTileID];
if (prevOffset) {
this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance);
}
}
if (hasIcon) {
const packedOpacity = packOpacity(opacityState.icon);
for (let i = 0; i < symbolInstance.numIconVertices / 4; i++) {
bucket.icon.opacityVertexArray.emplaceBack(packedOpacity);
}
bucket.icon.placedSymbolArray.get(s).hidden =
(opacityState.icon.isHidden(): any);
}
if (bucket.hasCollisionBoxData() || bucket.hasCollisionCircleData()) {
const collisionArrays = bucket.collisionArrays[s];
if (collisionArrays) {
if (collisionArrays.textBox) {
let shift = new Point(0, 0);
let used = true;
if (variablePlacement) {
const variableOffset = this.variableOffsets[crossTileID];
if (variableOffset) {
// This will show either the currently placed position or the last
// successfully placed position (so you can visualize what collision
// just made the symbol disappear, and the most likely place for the
// symbol to come back)
shift = calculateVariableLayoutOffset(variableOffset.anchor,
variableOffset.width,
variableOffset.height,
variableOffset.radialOffset,
variableOffset.textBoxScale);
if (rotateWithMap) {
shift._rotate(pitchWithMap ? this.transform.angle : -this.transform.angle);
}
} else {
// No offset -> this symbol hasn't been placed since coming on-screen
// No single box is particularly meaningful and all of them would be too noisy
// Use the center box just to show something's there, but mark it "not used"
used = false;
}
}
updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.text.placed, !used, shift.x, shift.y);
}
if (collisionArrays.iconBox) {
updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.icon.placed, false);
}
const textCircles = collisionArrays.textCircles;
if (textCircles && bucket.hasCollisionCircleData()) {
for (let k = 0; k < textCircles.length; k += 5) {
const notUsed = isDuplicate || textCircles[k + 4] === 0;
updateCollisionVertices(bucket.collisionCircle.collisionVertexArray, opacityState.text.placed, notUsed);
}
}
}
}
}
bucket.sortFeatures(this.transform.angle);
if (this.retainedQueryData[bucket.bucketInstanceId]) {
this.retainedQueryData[bucket.bucketInstanceId].featureSortOrder = bucket.featureSortOrder;
}
if (bucket.hasTextData() && bucket.text.opacityVertexBuffer) {
bucket.text.opacityVertexBuffer.updateData(bucket.text.opacityVertexArray);
}
if (bucket.hasIconData() && bucket.icon.opacityVertexBuffer) {
bucket.icon.opacityVertexBuffer.updateData(bucket.icon.opacityVertexArray);
}
if (bucket.hasCollisionBoxData() && bucket.collisionBox.collisionVertexBuffer) {
bucket.collisionBox.collisionVertexBuffer.updateData(bucket.collisionBox.collisionVertexArray);
}
if (bucket.hasCollisionCircleData() && bucket.collisionCircle.collisionVertexBuffer) {
bucket.collisionCircle.collisionVertexBuffer.updateData(bucket.collisionCircle.collisionVertexArray);
}
assert(bucket.text.opacityVertexArray.length === bucket.text.layoutVertexArray.length / 4);
assert(bucket.icon.opacityVertexArray.length === bucket.icon.layoutVertexArray.length / 4);
}
symbolFadeChange(now: number) {
return this.fadeDuration === 0 ?
1 :
(now - this.commitTime) / this.fadeDuration;
}
hasTransitions(now: number) {
return this.stale ||
now - this.lastPlacementChangeTime < this.fadeDuration;
}
stillRecent(now: number) {
return this.commitTime + this.fadeDuration > now;
}
setStale() {
this.stale = true;
}
}
function updateCollisionVertices(collisionVertexArray: CollisionVertexArray, placed: boolean, notUsed: boolean, shiftX?: number, shiftY?: number) {
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
}
// All four vertices for a glyph will have the same opacity state
// So we pack the opacity into a uint8, and then repeat it four times
// to make a single uint32 that we can upload for each glyph in the
// label.
const shift25 = Math.pow(2, 25);
const shift24 = Math.pow(2, 24);
const shift17 = Math.pow(2, 17);
const shift16 = Math.pow(2, 16);
const shift9 = Math.pow(2, 9);
const shift8 = Math.pow(2, 8);
const shift1 = Math.pow(2, 1);
function packOpacity(opacityState: OpacityState): number {
if (opacityState.opacity === 0 && !opacityState.placed) {
return 0;
} else if (opacityState.opacity === 1 && opacityState.placed) {
return 4294967295;
}
const targetBit = opacityState.placed ? 1 : 0;
const opacityBits = Math.floor(opacityState.opacity * 127);
return opacityBits * shift25 + targetBit * shift24 +
opacityBits * shift17 + targetBit * shift16 +
opacityBits * shift9 + targetBit * shift8 +
opacityBits * shift1 + targetBit;
}