UNPKG

55.7 kBPlain TextView Raw
1import CollisionIndex from './collision_index';
2import type {FeatureKey} from './collision_index';
3import EXTENT from '../data/extent';
4import * as symbolSize from './symbol_size';
5import * as projection from './projection';
6import {getAnchorJustification, evaluateVariableOffset} from './symbol_layout';
7import {getAnchorAlignment, WritingMode} from './shaping';
8import {mat4} from 'gl-matrix';
9import assert from 'assert';
10import pixelsToTileUnits from '../source/pixels_to_tile_units';
11import Point from '@mapbox/point-geometry';
12import type Transform from '../geo/transform';
13import type StyleLayer from '../style/style_layer';
14import {PossiblyEvaluated} from '../style/properties';
15import type {SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated} from '../style/style_layer/symbol_style_layer_properties.g';
16import {getOverlapMode, OverlapMode} from '../style/style_layer/symbol_style_layer';
17
18import type Tile from '../source/tile';
19import SymbolBucket, {CollisionArrays, SingleCollisionBox} from '../data/bucket/symbol_bucket';
20
21import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../data/array_types.g';
22import type FeatureIndex from '../data/feature_index';
23import type {OverscaledTileID} from '../source/tile_id';
24import type {TextAnchor} from './symbol_layout';
25
26class OpacityState {
27 opacity: number;
28 placed: boolean;
29 constructor(prevState: OpacityState, increment: number, placed: boolean, skipFade?: boolean | null) {
30 if (prevState) {
31 this.opacity = Math.max(0, Math.min(1, prevState.opacity + (prevState.placed ? increment : -increment)));
32 } else {
33 this.opacity = (skipFade && placed) ? 1 : 0;
34 }
35 this.placed = placed;
36 }
37 isHidden() {
38 return this.opacity === 0 && !this.placed;
39 }
40}
41
42class JointOpacityState {
43 text: OpacityState;
44 icon: OpacityState;
45 constructor(prevState: JointOpacityState, increment: number, placedText: boolean, placedIcon: boolean, skipFade?: boolean | null) {
46 this.text = new OpacityState(prevState ? prevState.text : null, increment, placedText, skipFade);
47 this.icon = new OpacityState(prevState ? prevState.icon : null, increment, placedIcon, skipFade);
48 }
49 isHidden() {
50 return this.text.isHidden() && this.icon.isHidden();
51 }
52}
53
54class JointPlacement {
55 text: boolean;
56 icon: boolean;
57 // skipFade = outside viewport, but within CollisionIndex::viewportPadding px of the edge
58 // Because these symbols aren't onscreen yet, we can skip the "fade in" animation,
59 // and if a subsequent viewport change brings them into view, they'll be fully
60 // visible right away.
61 skipFade: boolean;
62 constructor(text: boolean, icon: boolean, skipFade: boolean) {
63 this.text = text;
64 this.icon = icon;
65 this.skipFade = skipFade;
66 }
67}
68
69class CollisionCircleArray {
70 // Stores collision circles and placement matrices of a bucket for debug rendering.
71 invProjMatrix: mat4;
72 viewportMatrix: mat4;
73 circles: Array<number>;
74
75 constructor() {
76 this.invProjMatrix = mat4.create();
77 this.viewportMatrix = mat4.create();
78 this.circles = [];
79 }
80}
81
82export class RetainedQueryData {
83 bucketInstanceId: number;
84 featureIndex: FeatureIndex;
85 sourceLayerIndex: number;
86 bucketIndex: number;
87 tileID: OverscaledTileID;
88 featureSortOrder: Array<number>;
89 constructor(bucketInstanceId: number,
90 featureIndex: FeatureIndex,
91 sourceLayerIndex: number,
92 bucketIndex: number,
93 tileID: OverscaledTileID) {
94 this.bucketInstanceId = bucketInstanceId;
95 this.featureIndex = featureIndex;
96 this.sourceLayerIndex = sourceLayerIndex;
97 this.bucketIndex = bucketIndex;
98 this.tileID = tileID;
99 }
100}
101
102type CollisionGroup = {
103 ID: number;
104 predicate?: (key: FeatureKey) => boolean;
105};
106
107class CollisionGroups {
108 collisionGroups: {[groupName: string]: CollisionGroup};
109 maxGroupID: number;
110 crossSourceCollisions: boolean;
111
112 constructor(crossSourceCollisions: boolean) {
113 this.crossSourceCollisions = crossSourceCollisions;
114 this.maxGroupID = 0;
115 this.collisionGroups = {};
116 }
117
118 get(sourceID: string) {
119 // The predicate/groupID mechanism allows for arbitrary grouping,
120 // but the current interface defines one source == one group when
121 // crossSourceCollisions == true.
122 if (!this.crossSourceCollisions) {
123 if (!this.collisionGroups[sourceID]) {
124 const nextGroupID = ++this.maxGroupID;
125 this.collisionGroups[sourceID] = {
126 ID: nextGroupID,
127 predicate: (key) => {
128 return key.collisionGroupID === nextGroupID;
129 }
130 };
131 }
132 return this.collisionGroups[sourceID];
133 } else {
134 return {ID: 0, predicate: null};
135 }
136 }
137}
138
139function calculateVariableLayoutShift(
140 anchor: TextAnchor,
141 width: number,
142 height: number,
143 textOffset: [number, number],
144 textBoxScale: number
145): Point {
146 const {horizontalAlign, verticalAlign} = getAnchorAlignment(anchor);
147 const shiftX = -(horizontalAlign - 0.5) * width;
148 const shiftY = -(verticalAlign - 0.5) * height;
149 const offset = evaluateVariableOffset(anchor, textOffset);
150 return new Point(
151 shiftX + offset[0] * textBoxScale,
152 shiftY + offset[1] * textBoxScale
153 );
154}
155
156function shiftVariableCollisionBox(collisionBox: SingleCollisionBox,
157 shiftX: number, shiftY: number,
158 rotateWithMap: boolean, pitchWithMap: boolean,
159 angle: number) {
160 const {x1, x2, y1, y2, anchorPointX, anchorPointY} = collisionBox;
161 const rotatedOffset = new Point(shiftX, shiftY);
162 if (rotateWithMap) {
163 rotatedOffset._rotate(pitchWithMap ? angle : -angle);
164 }
165 return {
166 x1: x1 + rotatedOffset.x,
167 y1: y1 + rotatedOffset.y,
168 x2: x2 + rotatedOffset.x,
169 y2: y2 + rotatedOffset.y,
170 // symbol anchor point stays the same regardless of text-anchor
171 anchorPointX,
172 anchorPointY
173 };
174}
175
176export type VariableOffset = {
177 textOffset: [number, number];
178 width: number;
179 height: number;
180 anchor: TextAnchor;
181 textBoxScale: number;
182 prevAnchor?: TextAnchor;
183};
184
185type TileLayerParameters = {
186 bucket: SymbolBucket;
187 layout: PossiblyEvaluated<SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated>;
188 posMatrix: mat4;
189 textLabelPlaneMatrix: mat4;
190 labelToScreenMatrix: mat4;
191 scale: number;
192 textPixelRatio: number;
193 holdingForFade: boolean;
194 collisionBoxArray: CollisionBoxArray;
195 partiallyEvaluatedTextSize: {
196 uSize: number;
197 uSizeT: number;
198 };
199 collisionGroup: CollisionGroup;
200};
201
202export type BucketPart = {
203 sortKey?: number | void;
204 symbolInstanceStart: number;
205 symbolInstanceEnd: number;
206 parameters: TileLayerParameters;
207};
208
209export type CrossTileID = string | number;
210
211export class Placement {
212 transform: Transform;
213 collisionIndex: CollisionIndex;
214 placements: {
215 [_ in CrossTileID]: JointPlacement;
216 };
217 opacities: {
218 [_ in CrossTileID]: JointOpacityState;
219 };
220 variableOffsets: {
221 [_ in CrossTileID]: VariableOffset;
222 };
223 placedOrientations: {
224 [_ in CrossTileID]: number;
225 };
226 commitTime: number;
227 prevZoomAdjustment: number;
228 lastPlacementChangeTime: number;
229 stale: boolean;
230 fadeDuration: number;
231 retainedQueryData: {
232 [_: number]: RetainedQueryData;
233 };
234 collisionGroups: CollisionGroups;
235 prevPlacement: Placement;
236 zoomAtLastRecencyCheck: number;
237 collisionCircleArrays: {
238 [k in any]: CollisionCircleArray;
239 };
240
241 constructor(transform: Transform, fadeDuration: number, crossSourceCollisions: boolean, prevPlacement?: Placement) {
242 this.transform = transform.clone();
243 this.collisionIndex = new CollisionIndex(this.transform);
244 this.placements = {};
245 this.opacities = {};
246 this.variableOffsets = {};
247 this.stale = false;
248 this.commitTime = 0;
249 this.fadeDuration = fadeDuration;
250 this.retainedQueryData = {};
251 this.collisionGroups = new CollisionGroups(crossSourceCollisions);
252 this.collisionCircleArrays = {};
253
254 this.prevPlacement = prevPlacement;
255 if (prevPlacement) {
256 prevPlacement.prevPlacement = undefined; // Only hold on to one placement back
257 }
258
259 this.placedOrientations = {};
260 }
261
262 getBucketParts(results: Array<BucketPart>, styleLayer: StyleLayer, tile: Tile, sortAcrossTiles: boolean) {
263 const symbolBucket = (tile.getBucket(styleLayer) as SymbolBucket);
264 const bucketFeatureIndex = tile.latestFeatureIndex;
265 if (!symbolBucket || !bucketFeatureIndex || styleLayer.id !== symbolBucket.layerIds[0])
266 return;
267
268 const collisionBoxArray = tile.collisionBoxArray;
269
270 const layout = symbolBucket.layers[0].layout;
271
272 const scale = Math.pow(2, this.transform.zoom - tile.tileID.overscaledZ);
273 const textPixelRatio = tile.tileSize / EXTENT;
274
275 const posMatrix = this.transform.calculatePosMatrix(tile.tileID.toUnwrapped());
276
277 const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
278 const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
279 const pixelsToTiles = pixelsToTileUnits(tile, 1, this.transform.zoom);
280
281 const textLabelPlaneMatrix = projection.getLabelPlaneMatrix(posMatrix,
282 pitchWithMap,
283 rotateWithMap,
284 this.transform,
285 pixelsToTiles);
286
287 let labelToScreenMatrix = null;
288
289 if (pitchWithMap) {
290 const glMatrix = projection.getGlCoordMatrix(
291 posMatrix,
292 pitchWithMap,
293 rotateWithMap,
294 this.transform,
295 pixelsToTiles);
296
297 labelToScreenMatrix = mat4.multiply([] as any, this.transform.labelPlaneMatrix, glMatrix);
298 }
299
300 // As long as this placement lives, we have to hold onto this bucket's
301 // matching FeatureIndex/data for querying purposes
302 this.retainedQueryData[symbolBucket.bucketInstanceId] = new RetainedQueryData(
303 symbolBucket.bucketInstanceId,
304 bucketFeatureIndex,
305 symbolBucket.sourceLayerIndex,
306 symbolBucket.index,
307 tile.tileID
308 );
309
310 const parameters = {
311 bucket: symbolBucket,
312 layout,
313 posMatrix,
314 textLabelPlaneMatrix,
315 labelToScreenMatrix,
316 scale,
317 textPixelRatio,
318 holdingForFade: tile.holdingForFade(),
319 collisionBoxArray,
320 partiallyEvaluatedTextSize: symbolSize.evaluateSizeForZoom(symbolBucket.textSizeData, this.transform.zoom),
321 collisionGroup: this.collisionGroups.get(symbolBucket.sourceID)
322 };
323
324 if (sortAcrossTiles) {
325 for (const range of symbolBucket.sortKeyRanges) {
326 const {sortKey, symbolInstanceStart, symbolInstanceEnd} = range;
327 results.push({sortKey, symbolInstanceStart, symbolInstanceEnd, parameters});
328 }
329 } else {
330 results.push({
331 symbolInstanceStart: 0,
332 symbolInstanceEnd: symbolBucket.symbolInstances.length,
333 parameters
334 });
335 }
336 }
337
338 attemptAnchorPlacement(
339 anchor: TextAnchor,
340 textBox: SingleCollisionBox,
341 width: number,
342 height: number,
343 textBoxScale: number,
344 rotateWithMap: boolean,
345 pitchWithMap: boolean,
346 textPixelRatio: number,
347 posMatrix: mat4,
348 collisionGroup: CollisionGroup,
349 textOverlapMode: OverlapMode,
350 symbolInstance: SymbolInstance,
351 bucket: SymbolBucket,
352 orientation: number,
353 iconBox?: SingleCollisionBox | null
354 ): {
355 shift: Point;
356 placedGlyphBoxes: {
357 box: Array<number>;
358 offscreen: boolean;
359 };
360 } {
361
362 const textOffset = [symbolInstance.textOffset0, symbolInstance.textOffset1] as [number, number];
363 const shift = calculateVariableLayoutShift(anchor, width, height, textOffset, textBoxScale);
364
365 const placedGlyphBoxes = this.collisionIndex.placeCollisionBox(
366 shiftVariableCollisionBox(
367 textBox, shift.x, shift.y,
368 rotateWithMap, pitchWithMap, this.transform.angle),
369 textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
370
371 if (iconBox) {
372 const placedIconBoxes = this.collisionIndex.placeCollisionBox(
373 shiftVariableCollisionBox(
374 iconBox, shift.x, shift.y,
375 rotateWithMap, pitchWithMap, this.transform.angle),
376 textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
377 if (placedIconBoxes.box.length === 0) return;
378 }
379
380 if (placedGlyphBoxes.box.length > 0) {
381 let prevAnchor;
382 // If this label was placed in the previous placement, record the anchor position
383 // to allow us to animate the transition
384 if (this.prevPlacement &&
385 this.prevPlacement.variableOffsets[symbolInstance.crossTileID] &&
386 this.prevPlacement.placements[symbolInstance.crossTileID] &&
387 this.prevPlacement.placements[symbolInstance.crossTileID].text) {
388 prevAnchor = this.prevPlacement.variableOffsets[symbolInstance.crossTileID].anchor;
389 }
390 assert(symbolInstance.crossTileID !== 0);
391 this.variableOffsets[symbolInstance.crossTileID] = {
392 textOffset,
393 width,
394 height,
395 anchor,
396 textBoxScale,
397 prevAnchor
398 };
399 this.markUsedJustification(bucket, anchor, symbolInstance, orientation);
400
401 if (bucket.allowVerticalPlacement) {
402 this.markUsedOrientation(bucket, orientation, symbolInstance);
403 this.placedOrientations[symbolInstance.crossTileID] = orientation;
404 }
405
406 return {shift, placedGlyphBoxes};
407 }
408 }
409
410 placeLayerBucketPart(bucketPart: BucketPart, seenCrossTileIDs: {
411 [k in string | number]: boolean;
412 }, showCollisionBoxes: boolean) {
413
414 const {
415 bucket,
416 layout,
417 posMatrix,
418 textLabelPlaneMatrix,
419 labelToScreenMatrix,
420 textPixelRatio,
421 holdingForFade,
422 collisionBoxArray,
423 partiallyEvaluatedTextSize,
424 collisionGroup
425 } = bucketPart.parameters;
426
427 const textOptional = layout.get('text-optional');
428 const iconOptional = layout.get('icon-optional');
429 const textOverlapMode = getOverlapMode(layout, 'text-overlap', 'text-allow-overlap');
430 const textAlwaysOverlap = textOverlapMode === 'always';
431 const iconOverlapMode = getOverlapMode(layout, 'icon-overlap', 'icon-allow-overlap');
432 const iconAlwaysOverlap = iconOverlapMode === 'always';
433 const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
434 const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
435 const hasIconTextFit = layout.get('icon-text-fit') !== 'none';
436 const zOrderByViewportY = layout.get('symbol-z-order') === 'viewport-y';
437
438 // This logic is similar to the "defaultOpacityState" logic below in updateBucketOpacities
439 // If we know a symbol is always supposed to show, force it to be marked visible even if
440 // it wasn't placed into the collision index (because some or all of it was outside the range
441 // of the collision grid).
442 // There is a subtle edge case here we're accepting:
443 // Symbol A has text-allow-overlap: true, icon-allow-overlap: true, icon-optional: false
444 // A's icon is outside the grid, so doesn't get placed
445 // A's text would be inside grid, but doesn't get placed because of icon-optional: false
446 // We still show A because of the allow-overlap settings.
447 // Symbol B has allow-overlap: false, and gets placed where A's text would be
448 // On panning in, there is a short period when Symbol B and Symbol A will overlap
449 // This is the reverse of our normal policy of "fade in on pan", but should look like any other
450 // collision and hopefully not be too noticeable.
451 // See https://github.com/mapbox/mapbox-gl-js/issues/7172
452 const alwaysShowText = textAlwaysOverlap && (iconAlwaysOverlap || !bucket.hasIconData() || iconOptional);
453 const alwaysShowIcon = iconAlwaysOverlap && (textAlwaysOverlap || !bucket.hasTextData() || textOptional);
454
455 if (!bucket.collisionArrays && collisionBoxArray) {
456 bucket.deserializeCollisionBoxes(collisionBoxArray);
457 }
458
459 const placeSymbol = (symbolInstance: SymbolInstance, collisionArrays: CollisionArrays) => {
460 if (seenCrossTileIDs[symbolInstance.crossTileID]) return;
461 if (holdingForFade) {
462 // Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
463 // know yet if we have a duplicate in a parent tile that _should_ be placed.
464 this.placements[symbolInstance.crossTileID] = new JointPlacement(false, false, false);
465 return;
466 }
467
468 let placeText = false;
469 let placeIcon = false;
470 let offscreen = true;
471 let shift = null;
472
473 let placed = {box: null, offscreen: null};
474 let placedVerticalText = {box: null, offscreen: null};
475
476 let placedGlyphBoxes = null;
477 let placedGlyphCircles = null;
478 let placedIconBoxes = null;
479 let textFeatureIndex = 0;
480 let verticalTextFeatureIndex = 0;
481 let iconFeatureIndex = 0;
482
483 if (collisionArrays.textFeatureIndex) {
484 textFeatureIndex = collisionArrays.textFeatureIndex;
485 } else if (symbolInstance.useRuntimeCollisionCircles) {
486 textFeatureIndex = symbolInstance.featureIndex;
487 }
488 if (collisionArrays.verticalTextFeatureIndex) {
489 verticalTextFeatureIndex = collisionArrays.verticalTextFeatureIndex;
490 }
491
492 const textBox = collisionArrays.textBox;
493 if (textBox) {
494
495 const updatePreviousOrientationIfNotPlaced = (isPlaced) => {
496 let previousOrientation = WritingMode.horizontal;
497 if (bucket.allowVerticalPlacement && !isPlaced && this.prevPlacement) {
498 const prevPlacedOrientation = this.prevPlacement.placedOrientations[symbolInstance.crossTileID];
499 if (prevPlacedOrientation) {
500 this.placedOrientations[symbolInstance.crossTileID] = prevPlacedOrientation;
501 previousOrientation = prevPlacedOrientation;
502 this.markUsedOrientation(bucket, previousOrientation, symbolInstance);
503 }
504 }
505 return previousOrientation;
506 };
507
508 const placeTextForPlacementModes = (placeHorizontalFn, placeVerticalFn) => {
509 if (bucket.allowVerticalPlacement && symbolInstance.numVerticalGlyphVertices > 0 && collisionArrays.verticalTextBox) {
510 for (const placementMode of bucket.writingModes) {
511 if (placementMode === WritingMode.vertical) {
512 placed = placeVerticalFn();
513 placedVerticalText = placed;
514 } else {
515 placed = placeHorizontalFn();
516 }
517 if (placed && placed.box && placed.box.length) break;
518 }
519 } else {
520 placed = placeHorizontalFn();
521 }
522 };
523
524 if (!layout.get('text-variable-anchor')) {
525 const placeBox = (collisionTextBox, orientation) => {
526 const placedFeature = this.collisionIndex.placeCollisionBox(
527 collisionTextBox,
528 textOverlapMode,
529 textPixelRatio,
530 posMatrix,
531 collisionGroup.predicate);
532 if (placedFeature && placedFeature.box && placedFeature.box.length) {
533 this.markUsedOrientation(bucket, orientation, symbolInstance);
534 this.placedOrientations[symbolInstance.crossTileID] = orientation;
535 }
536 return placedFeature;
537 };
538
539 const placeHorizontal = () => {
540 return placeBox(textBox, WritingMode.horizontal);
541 };
542
543 const placeVertical = () => {
544 const verticalTextBox = collisionArrays.verticalTextBox;
545 if (bucket.allowVerticalPlacement && symbolInstance.numVerticalGlyphVertices > 0 && verticalTextBox) {
546 return placeBox(verticalTextBox, WritingMode.vertical);
547 }
548 return {box: null, offscreen: null};
549 };
550
551 placeTextForPlacementModes(placeHorizontal, placeVertical);
552 updatePreviousOrientationIfNotPlaced(placed && placed.box && placed.box.length);
553
554 } else {
555 let anchors = layout.get('text-variable-anchor');
556
557 // If this symbol was in the last placement, shift the previously used
558 // anchor to the front of the anchor list, only if the previous anchor
559 // is still in the anchor list
560 if (this.prevPlacement && this.prevPlacement.variableOffsets[symbolInstance.crossTileID]) {
561 const prevOffsets = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
562 if (anchors.indexOf(prevOffsets.anchor) > 0) {
563 anchors = anchors.filter(anchor => anchor !== prevOffsets.anchor);
564 anchors.unshift(prevOffsets.anchor);
565 }
566 }
567
568 const placeBoxForVariableAnchors = (collisionTextBox, collisionIconBox, orientation) => {
569 const width = collisionTextBox.x2 - collisionTextBox.x1;
570 const height = collisionTextBox.y2 - collisionTextBox.y1;
571 const textBoxScale = symbolInstance.textBoxScale;
572
573 const variableIconBox = hasIconTextFit && (iconOverlapMode === 'never') ? collisionIconBox : null;
574
575 let placedBox: {
576 box: Array<number>;
577 offscreen: boolean;
578 } = {box: [], offscreen: false};
579 const placementAttempts = (textOverlapMode !== 'never') ? anchors.length * 2 : anchors.length;
580 for (let i = 0; i < placementAttempts; ++i) {
581 const anchor = anchors[i % anchors.length];
582 const overlapMode = (i >= anchors.length) ? textOverlapMode : 'never';
583 const result = this.attemptAnchorPlacement(
584 anchor, collisionTextBox, width, height,
585 textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix,
586 collisionGroup, overlapMode, symbolInstance, bucket, orientation, variableIconBox);
587
588 if (result) {
589 placedBox = result.placedGlyphBoxes;
590 if (placedBox && placedBox.box && placedBox.box.length) {
591 placeText = true;
592 shift = result.shift;
593 break;
594 }
595 }
596 }
597
598 return placedBox;
599 };
600
601 const placeHorizontal = () => {
602 return placeBoxForVariableAnchors(textBox, collisionArrays.iconBox, WritingMode.horizontal);
603 };
604
605 const placeVertical = () => {
606 const verticalTextBox = collisionArrays.verticalTextBox;
607 const wasPlaced = placed && placed.box && placed.box.length;
608 if (bucket.allowVerticalPlacement && !wasPlaced && symbolInstance.numVerticalGlyphVertices > 0 && verticalTextBox) {
609 return placeBoxForVariableAnchors(verticalTextBox, collisionArrays.verticalIconBox, WritingMode.vertical);
610 }
611 return {box: null, offscreen: null};
612 };
613
614 placeTextForPlacementModes(placeHorizontal, placeVertical);
615
616 if (placed) {
617 placeText = placed.box;
618 offscreen = placed.offscreen;
619 }
620
621 const prevOrientation = updatePreviousOrientationIfNotPlaced(placed && placed.box);
622
623 // If we didn't get placed, we still need to copy our position from the last placement for
624 // fade animations
625 if (!placeText && this.prevPlacement) {
626 const prevOffset = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
627 if (prevOffset) {
628 this.variableOffsets[symbolInstance.crossTileID] = prevOffset;
629 this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance, prevOrientation);
630 }
631 }
632
633 }
634 }
635
636 placedGlyphBoxes = placed;
637 placeText = placedGlyphBoxes && placedGlyphBoxes.box && placedGlyphBoxes.box.length > 0;
638
639 offscreen = placedGlyphBoxes && placedGlyphBoxes.offscreen;
640
641 if (symbolInstance.useRuntimeCollisionCircles) {
642 const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex);
643 const fontSize = symbolSize.evaluateSizeForFeature(bucket.textSizeData, partiallyEvaluatedTextSize, placedSymbol);
644
645 const textPixelPadding = layout.get('text-padding');
646 const circlePixelDiameter = symbolInstance.collisionCircleDiameter;
647
648 placedGlyphCircles = this.collisionIndex.placeCollisionCircles(
649 textOverlapMode,
650 placedSymbol,
651 bucket.lineVertexArray,
652 bucket.glyphOffsetArray,
653 fontSize,
654 posMatrix,
655 textLabelPlaneMatrix,
656 labelToScreenMatrix,
657 showCollisionBoxes,
658 pitchWithMap,
659 collisionGroup.predicate,
660 circlePixelDiameter,
661 textPixelPadding);
662
663 assert(!placedGlyphCircles.circles.length || (!placedGlyphCircles.collisionDetected || showCollisionBoxes));
664 // If text-overlap is set to 'always', force "placedCircles" to true
665 // In theory there should always be at least one circle placed
666 // in this case, but for now quirks in text-anchor
667 // and text-offset may prevent that from being true.
668 placeText = textAlwaysOverlap || (placedGlyphCircles.circles.length > 0 && !placedGlyphCircles.collisionDetected);
669 offscreen = offscreen && placedGlyphCircles.offscreen;
670 }
671
672 if (collisionArrays.iconFeatureIndex) {
673 iconFeatureIndex = collisionArrays.iconFeatureIndex;
674 }
675
676 if (collisionArrays.iconBox) {
677
678 const placeIconFeature = iconBox => {
679 const shiftedIconBox = hasIconTextFit && shift ?
680 shiftVariableCollisionBox(
681 iconBox, shift.x, shift.y,
682 rotateWithMap, pitchWithMap, this.transform.angle) :
683 iconBox;
684 return this.collisionIndex.placeCollisionBox(shiftedIconBox,
685 iconOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
686 };
687
688 if (placedVerticalText && placedVerticalText.box && placedVerticalText.box.length && collisionArrays.verticalIconBox) {
689 placedIconBoxes = placeIconFeature(collisionArrays.verticalIconBox);
690 placeIcon = placedIconBoxes.box.length > 0;
691 } else {
692 placedIconBoxes = placeIconFeature(collisionArrays.iconBox);
693 placeIcon = placedIconBoxes.box.length > 0;
694 }
695 offscreen = offscreen && placedIconBoxes.offscreen;
696 }
697
698 const iconWithoutText = textOptional ||
699 (symbolInstance.numHorizontalGlyphVertices === 0 && symbolInstance.numVerticalGlyphVertices === 0);
700 const textWithoutIcon = iconOptional || symbolInstance.numIconVertices === 0;
701
702 // Combine the scales for icons and text.
703 if (!iconWithoutText && !textWithoutIcon) {
704 placeIcon = placeText = placeIcon && placeText;
705 } else if (!textWithoutIcon) {
706 placeText = placeIcon && placeText;
707 } else if (!iconWithoutText) {
708 placeIcon = placeIcon && placeText;
709 }
710
711 if (placeText && placedGlyphBoxes && placedGlyphBoxes.box) {
712 if (placedVerticalText && placedVerticalText.box && verticalTextFeatureIndex) {
713 this.collisionIndex.insertCollisionBox(
714 placedGlyphBoxes.box,
715 textOverlapMode,
716 layout.get('text-ignore-placement'),
717 bucket.bucketInstanceId,
718 verticalTextFeatureIndex,
719 collisionGroup.ID);
720 } else {
721 this.collisionIndex.insertCollisionBox(
722 placedGlyphBoxes.box,
723 textOverlapMode,
724 layout.get('text-ignore-placement'),
725 bucket.bucketInstanceId,
726 textFeatureIndex,
727 collisionGroup.ID);
728 }
729
730 }
731 if (placeIcon && placedIconBoxes) {
732 this.collisionIndex.insertCollisionBox(
733 placedIconBoxes.box,
734 iconOverlapMode,
735 layout.get('icon-ignore-placement'),
736 bucket.bucketInstanceId,
737 iconFeatureIndex,
738 collisionGroup.ID);
739 }
740 if (placedGlyphCircles) {
741 if (placeText) {
742 this.collisionIndex.insertCollisionCircles(
743 placedGlyphCircles.circles,
744 textOverlapMode,
745 layout.get('text-ignore-placement'),
746 bucket.bucketInstanceId,
747 textFeatureIndex,
748 collisionGroup.ID);
749 }
750
751 if (showCollisionBoxes) {
752 const id = bucket.bucketInstanceId;
753 let circleArray = this.collisionCircleArrays[id];
754
755 // Group collision circles together by bucket. Circles can't be pushed forward for rendering yet as the symbol placement
756 // for a bucket is not guaranteed to be complete before the commit-function has been called
757 if (circleArray === undefined)
758 circleArray = this.collisionCircleArrays[id] = new CollisionCircleArray();
759
760 for (let i = 0; i < placedGlyphCircles.circles.length; i += 4) {
761 circleArray.circles.push(placedGlyphCircles.circles[i + 0]); // x
762 circleArray.circles.push(placedGlyphCircles.circles[i + 1]); // y
763 circleArray.circles.push(placedGlyphCircles.circles[i + 2]); // radius
764 circleArray.circles.push(placedGlyphCircles.collisionDetected ? 1 : 0); // collisionDetected-flag
765 }
766 }
767 }
768
769 assert(symbolInstance.crossTileID !== 0);
770 assert(bucket.bucketInstanceId !== 0);
771
772 this.placements[symbolInstance.crossTileID] = new JointPlacement(placeText || alwaysShowText, placeIcon || alwaysShowIcon, offscreen || bucket.justReloaded);
773 seenCrossTileIDs[symbolInstance.crossTileID] = true;
774 };
775
776 if (zOrderByViewportY) {
777 assert(bucketPart.symbolInstanceStart === 0);
778 const symbolIndexes = bucket.getSortedSymbolIndexes(this.transform.angle);
779 for (let i = symbolIndexes.length - 1; i >= 0; --i) {
780 const symbolIndex = symbolIndexes[i];
781 placeSymbol(bucket.symbolInstances.get(symbolIndex), bucket.collisionArrays[symbolIndex]);
782 }
783 } else {
784 for (let i = bucketPart.symbolInstanceStart; i < bucketPart.symbolInstanceEnd; i++) {
785 placeSymbol(bucket.symbolInstances.get(i), bucket.collisionArrays[i]);
786 }
787 }
788
789 if (showCollisionBoxes && bucket.bucketInstanceId in this.collisionCircleArrays) {
790 const circleArray = this.collisionCircleArrays[bucket.bucketInstanceId];
791
792 // Store viewport and inverse projection matrices per bucket
793 mat4.invert(circleArray.invProjMatrix, posMatrix);
794 circleArray.viewportMatrix = this.collisionIndex.getViewportMatrix();
795 }
796
797 bucket.justReloaded = false;
798 }
799
800 markUsedJustification(bucket: SymbolBucket, placedAnchor: TextAnchor, symbolInstance: SymbolInstance, orientation: number) {
801 const justifications = {
802 'left': symbolInstance.leftJustifiedTextSymbolIndex,
803 'center': symbolInstance.centerJustifiedTextSymbolIndex,
804 'right': symbolInstance.rightJustifiedTextSymbolIndex
805 };
806
807 let autoIndex;
808 if (orientation === WritingMode.vertical) {
809 autoIndex = symbolInstance.verticalPlacedTextSymbolIndex;
810 } else {
811 autoIndex = justifications[getAnchorJustification(placedAnchor)];
812 }
813
814 const indexes = [
815 symbolInstance.leftJustifiedTextSymbolIndex,
816 symbolInstance.centerJustifiedTextSymbolIndex,
817 symbolInstance.rightJustifiedTextSymbolIndex,
818 symbolInstance.verticalPlacedTextSymbolIndex
819 ];
820
821 for (const index of indexes) {
822 if (index >= 0) {
823 if (autoIndex >= 0 && index !== autoIndex) {
824 // There are multiple justifications and this one isn't it: shift offscreen
825 bucket.text.placedSymbolArray.get(index).crossTileID = 0;
826 } else {
827 // Either this is the chosen justification or the justification is hardwired: use this one
828 bucket.text.placedSymbolArray.get(index).crossTileID = symbolInstance.crossTileID;
829 }
830 }
831 }
832 }
833
834 markUsedOrientation(bucket: SymbolBucket, orientation: number, symbolInstance: SymbolInstance) {
835 const horizontal = (orientation === WritingMode.horizontal || orientation === WritingMode.horizontalOnly) ? orientation : 0;
836 const vertical = orientation === WritingMode.vertical ? orientation : 0;
837
838 const horizontalIndexes = [
839 symbolInstance.leftJustifiedTextSymbolIndex,
840 symbolInstance.centerJustifiedTextSymbolIndex,
841 symbolInstance.rightJustifiedTextSymbolIndex
842 ];
843
844 for (const index of horizontalIndexes) {
845 bucket.text.placedSymbolArray.get(index).placedOrientation = horizontal;
846 }
847
848 if (symbolInstance.verticalPlacedTextSymbolIndex) {
849 bucket.text.placedSymbolArray.get(symbolInstance.verticalPlacedTextSymbolIndex).placedOrientation = vertical;
850 }
851 }
852
853 commit(now: number): void {
854 this.commitTime = now;
855 this.zoomAtLastRecencyCheck = this.transform.zoom;
856
857 const prevPlacement = this.prevPlacement;
858 let placementChanged = false;
859
860 this.prevZoomAdjustment = prevPlacement ? prevPlacement.zoomAdjustment(this.transform.zoom) : 0;
861 const increment = prevPlacement ? prevPlacement.symbolFadeChange(now) : 1;
862
863 const prevOpacities = prevPlacement ? prevPlacement.opacities : {};
864 const prevOffsets = prevPlacement ? prevPlacement.variableOffsets : {};
865 const prevOrientations = prevPlacement ? prevPlacement.placedOrientations : {};
866
867 // add the opacities from the current placement, and copy their current values from the previous placement
868 for (const crossTileID in this.placements) {
869 const jointPlacement = this.placements[crossTileID];
870 const prevOpacity = prevOpacities[crossTileID];
871 if (prevOpacity) {
872 this.opacities[crossTileID] = new JointOpacityState(prevOpacity, increment, jointPlacement.text, jointPlacement.icon);
873 placementChanged = placementChanged ||
874 jointPlacement.text !== prevOpacity.text.placed ||
875 jointPlacement.icon !== prevOpacity.icon.placed;
876 } else {
877 this.opacities[crossTileID] = new JointOpacityState(null, increment, jointPlacement.text, jointPlacement.icon, jointPlacement.skipFade);
878 placementChanged = placementChanged || jointPlacement.text || jointPlacement.icon;
879 }
880 }
881
882 // copy and update values from the previous placement that aren't in the current placement but haven't finished fading
883 for (const crossTileID in prevOpacities) {
884 const prevOpacity = prevOpacities[crossTileID];
885 if (!this.opacities[crossTileID]) {
886 const jointOpacity = new JointOpacityState(prevOpacity, increment, false, false);
887 if (!jointOpacity.isHidden()) {
888 this.opacities[crossTileID] = jointOpacity;
889 placementChanged = placementChanged || prevOpacity.text.placed || prevOpacity.icon.placed;
890 }
891 }
892 }
893 for (const crossTileID in prevOffsets) {
894 if (!this.variableOffsets[crossTileID] && this.opacities[crossTileID] && !this.opacities[crossTileID].isHidden()) {
895 this.variableOffsets[crossTileID] = prevOffsets[crossTileID];
896 }
897 }
898
899 for (const crossTileID in prevOrientations) {
900 if (!this.placedOrientations[crossTileID] && this.opacities[crossTileID] && !this.opacities[crossTileID].isHidden()) {
901 this.placedOrientations[crossTileID] = prevOrientations[crossTileID];
902 }
903 }
904
905 // this.lastPlacementChangeTime is the time of the last commit() that
906 // resulted in a placement change -- in other words, the start time of
907 // the last symbol fade animation
908 assert(!prevPlacement || prevPlacement.lastPlacementChangeTime !== undefined);
909 if (placementChanged) {
910 this.lastPlacementChangeTime = now;
911 } else if (typeof this.lastPlacementChangeTime !== 'number') {
912 this.lastPlacementChangeTime = prevPlacement ? prevPlacement.lastPlacementChangeTime : now;
913 }
914 }
915
916 updateLayerOpacities(styleLayer: StyleLayer, tiles: Array<Tile>) {
917 const seenCrossTileIDs = {};
918 for (const tile of tiles) {
919 const symbolBucket = tile.getBucket(styleLayer) as SymbolBucket;
920 if (symbolBucket && tile.latestFeatureIndex && styleLayer.id === symbolBucket.layerIds[0]) {
921 this.updateBucketOpacities(symbolBucket, seenCrossTileIDs, tile.collisionBoxArray);
922 }
923 }
924 }
925
926 updateBucketOpacities(bucket: SymbolBucket, seenCrossTileIDs: {
927 [k in string | number]: boolean;
928 }, collisionBoxArray?: CollisionBoxArray | null) {
929 if (bucket.hasTextData()) bucket.text.opacityVertexArray.clear();
930 if (bucket.hasIconData()) bucket.icon.opacityVertexArray.clear();
931 if (bucket.hasIconCollisionBoxData()) bucket.iconCollisionBox.collisionVertexArray.clear();
932 if (bucket.hasTextCollisionBoxData()) bucket.textCollisionBox.collisionVertexArray.clear();
933
934 const layout = bucket.layers[0].layout;
935 const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true);
936 const textAllowOverlap = layout.get('text-allow-overlap');
937 const iconAllowOverlap = layout.get('icon-allow-overlap');
938 const variablePlacement = layout.get('text-variable-anchor');
939 const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
940 const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
941 const hasIconTextFit = layout.get('icon-text-fit') !== 'none';
942 // If allow-overlap is true, we can show symbols before placement runs on them
943 // But we have to wait for placement if we potentially depend on a paired icon/text
944 // with allow-overlap: false.
945 // See https://github.com/mapbox/mapbox-gl-js/issues/7032
946 const defaultOpacityState = new JointOpacityState(null, 0,
947 textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || layout.get('icon-optional')),
948 iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || layout.get('text-optional')),
949 true);
950
951 if (!bucket.collisionArrays && collisionBoxArray && ((bucket.hasIconCollisionBoxData() || bucket.hasTextCollisionBoxData()))) {
952 bucket.deserializeCollisionBoxes(collisionBoxArray);
953 }
954
955 const addOpacities = (iconOrText, numVertices: number, opacity: number) => {
956 for (let i = 0; i < numVertices / 4; i++) {
957 iconOrText.opacityVertexArray.emplaceBack(opacity);
958 }
959 };
960
961 for (let s = 0; s < bucket.symbolInstances.length; s++) {
962 const symbolInstance = bucket.symbolInstances.get(s);
963 const {
964 numHorizontalGlyphVertices,
965 numVerticalGlyphVertices,
966 crossTileID
967 } = symbolInstance;
968
969 const isDuplicate = seenCrossTileIDs[crossTileID];
970
971 let opacityState = this.opacities[crossTileID];
972 if (isDuplicate) {
973 opacityState = duplicateOpacityState;
974 } else if (!opacityState) {
975 opacityState = defaultOpacityState;
976 // store the state so that future placements use it as a starting point
977 this.opacities[crossTileID] = opacityState;
978 }
979
980 seenCrossTileIDs[crossTileID] = true;
981
982 const hasText = numHorizontalGlyphVertices > 0 || numVerticalGlyphVertices > 0;
983 const hasIcon = symbolInstance.numIconVertices > 0;
984
985 const placedOrientation = this.placedOrientations[symbolInstance.crossTileID];
986 const horizontalHidden = placedOrientation === WritingMode.vertical;
987 const verticalHidden = placedOrientation === WritingMode.horizontal || placedOrientation === WritingMode.horizontalOnly;
988
989 if (hasText) {
990 const packedOpacity = packOpacity(opacityState.text);
991 // Vertical text fades in/out on collision the same way as corresponding
992 // horizontal text. Switch between vertical/horizontal should be instantaneous
993 const horizontalOpacity = horizontalHidden ? PACKED_HIDDEN_OPACITY : packedOpacity;
994 addOpacities(bucket.text, numHorizontalGlyphVertices, horizontalOpacity);
995 const verticalOpacity = verticalHidden ? PACKED_HIDDEN_OPACITY : packedOpacity;
996 addOpacities(bucket.text, numVerticalGlyphVertices, verticalOpacity);
997
998 // If this label is completely faded, mark it so that we don't have to calculate
999 // its position at render time. If this layer has variable placement, shift the various
1000 // symbol instances appropriately so that symbols from buckets that have yet to be placed
1001 // offset appropriately.
1002 const symbolHidden = opacityState.text.isHidden();
1003 [
1004 symbolInstance.rightJustifiedTextSymbolIndex,
1005 symbolInstance.centerJustifiedTextSymbolIndex,
1006 symbolInstance.leftJustifiedTextSymbolIndex
1007 ].forEach(index => {
1008 if (index >= 0) {
1009 bucket.text.placedSymbolArray.get(index).hidden = symbolHidden || horizontalHidden ? 1 : 0;
1010 }
1011 });
1012
1013 if (symbolInstance.verticalPlacedTextSymbolIndex >= 0) {
1014 bucket.text.placedSymbolArray.get(symbolInstance.verticalPlacedTextSymbolIndex).hidden = symbolHidden || verticalHidden ? 1 : 0;
1015 }
1016
1017 const prevOffset = this.variableOffsets[symbolInstance.crossTileID];
1018 if (prevOffset) {
1019 this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance, placedOrientation);
1020 }
1021
1022 const prevOrientation = this.placedOrientations[symbolInstance.crossTileID];
1023 if (prevOrientation) {
1024 this.markUsedJustification(bucket, 'left', symbolInstance, prevOrientation);
1025 this.markUsedOrientation(bucket, prevOrientation, symbolInstance);
1026 }
1027 }
1028
1029 if (hasIcon) {
1030 const packedOpacity = packOpacity(opacityState.icon);
1031
1032 const useHorizontal = !(hasIconTextFit && symbolInstance.verticalPlacedIconSymbolIndex && horizontalHidden);
1033
1034 if (symbolInstance.placedIconSymbolIndex >= 0) {
1035 const horizontalOpacity = useHorizontal ? packedOpacity : PACKED_HIDDEN_OPACITY;
1036 addOpacities(bucket.icon, symbolInstance.numIconVertices, horizontalOpacity);
1037 bucket.icon.placedSymbolArray.get(symbolInstance.placedIconSymbolIndex).hidden =
1038 (opacityState.icon.isHidden() as any);
1039 }
1040
1041 if (symbolInstance.verticalPlacedIconSymbolIndex >= 0) {
1042 const verticalOpacity = !useHorizontal ? packedOpacity : PACKED_HIDDEN_OPACITY;
1043 addOpacities(bucket.icon, symbolInstance.numVerticalIconVertices, verticalOpacity);
1044 bucket.icon.placedSymbolArray.get(symbolInstance.verticalPlacedIconSymbolIndex).hidden =
1045 (opacityState.icon.isHidden() as any);
1046 }
1047 }
1048
1049 if (bucket.hasIconCollisionBoxData() || bucket.hasTextCollisionBoxData()) {
1050 const collisionArrays = bucket.collisionArrays[s];
1051 if (collisionArrays) {
1052 let shift = new Point(0, 0);
1053 if (collisionArrays.textBox || collisionArrays.verticalTextBox) {
1054 let used = true;
1055 if (variablePlacement) {
1056 const variableOffset = this.variableOffsets[crossTileID];
1057 if (variableOffset) {
1058 // This will show either the currently placed position or the last
1059 // successfully placed position (so you can visualize what collision
1060 // just made the symbol disappear, and the most likely place for the
1061 // symbol to come back)
1062 shift = calculateVariableLayoutShift(variableOffset.anchor,
1063 variableOffset.width,
1064 variableOffset.height,
1065 variableOffset.textOffset,
1066 variableOffset.textBoxScale);
1067 if (rotateWithMap) {
1068 shift._rotate(pitchWithMap ? this.transform.angle : -this.transform.angle);
1069 }
1070 } else {
1071 // No offset -> this symbol hasn't been placed since coming on-screen
1072 // No single box is particularly meaningful and all of them would be too noisy
1073 // Use the center box just to show something's there, but mark it "not used"
1074 used = false;
1075 }
1076 }
1077
1078 if (collisionArrays.textBox) {
1079 updateCollisionVertices(bucket.textCollisionBox.collisionVertexArray, opacityState.text.placed, !used || horizontalHidden, shift.x, shift.y);
1080 }
1081 if (collisionArrays.verticalTextBox) {
1082 updateCollisionVertices(bucket.textCollisionBox.collisionVertexArray, opacityState.text.placed, !used || verticalHidden, shift.x, shift.y);
1083 }
1084 }
1085
1086 const verticalIconUsed = Boolean(!verticalHidden && collisionArrays.verticalIconBox);
1087
1088 if (collisionArrays.iconBox) {
1089 updateCollisionVertices(bucket.iconCollisionBox.collisionVertexArray, opacityState.icon.placed, verticalIconUsed,
1090 hasIconTextFit ? shift.x : 0,
1091 hasIconTextFit ? shift.y : 0);
1092 }
1093
1094 if (collisionArrays.verticalIconBox) {
1095 updateCollisionVertices(bucket.iconCollisionBox.collisionVertexArray, opacityState.icon.placed, !verticalIconUsed,
1096 hasIconTextFit ? shift.x : 0,
1097 hasIconTextFit ? shift.y : 0);
1098 }
1099 }
1100 }
1101 }
1102
1103 bucket.sortFeatures(this.transform.angle);
1104 if (this.retainedQueryData[bucket.bucketInstanceId]) {
1105 this.retainedQueryData[bucket.bucketInstanceId].featureSortOrder = bucket.featureSortOrder;
1106 }
1107
1108 if (bucket.hasTextData() && bucket.text.opacityVertexBuffer) {
1109 bucket.text.opacityVertexBuffer.updateData(bucket.text.opacityVertexArray);
1110 }
1111 if (bucket.hasIconData() && bucket.icon.opacityVertexBuffer) {
1112 bucket.icon.opacityVertexBuffer.updateData(bucket.icon.opacityVertexArray);
1113 }
1114 if (bucket.hasIconCollisionBoxData() && bucket.iconCollisionBox.collisionVertexBuffer) {
1115 bucket.iconCollisionBox.collisionVertexBuffer.updateData(bucket.iconCollisionBox.collisionVertexArray);
1116 }
1117 if (bucket.hasTextCollisionBoxData() && bucket.textCollisionBox.collisionVertexBuffer) {
1118 bucket.textCollisionBox.collisionVertexBuffer.updateData(bucket.textCollisionBox.collisionVertexArray);
1119 }
1120
1121 assert(bucket.text.opacityVertexArray.length === bucket.text.layoutVertexArray.length / 4);
1122 assert(bucket.icon.opacityVertexArray.length === bucket.icon.layoutVertexArray.length / 4);
1123
1124 // Push generated collision circles to the bucket for debug rendering
1125 if (bucket.bucketInstanceId in this.collisionCircleArrays) {
1126 const instance = this.collisionCircleArrays[bucket.bucketInstanceId];
1127
1128 bucket.placementInvProjMatrix = instance.invProjMatrix;
1129 bucket.placementViewportMatrix = instance.viewportMatrix;
1130 bucket.collisionCircleArray = instance.circles;
1131
1132 delete this.collisionCircleArrays[bucket.bucketInstanceId];
1133 }
1134 }
1135
1136 symbolFadeChange(now: number) {
1137 return this.fadeDuration === 0 ?
1138 1 :
1139 ((now - this.commitTime) / this.fadeDuration + this.prevZoomAdjustment);
1140 }
1141
1142 zoomAdjustment(zoom: number) {
1143 // When zooming out quickly, labels can overlap each other. This
1144 // adjustment is used to reduce the interval between placement calculations
1145 // and to reduce the fade duration when zooming out quickly. Discovering the
1146 // collisions more quickly and fading them more quickly reduces the unwanted effect.
1147 return Math.max(0, (this.transform.zoom - zoom) / 1.5);
1148 }
1149
1150 hasTransitions(now: number) {
1151 return this.stale ||
1152 now - this.lastPlacementChangeTime < this.fadeDuration;
1153 }
1154
1155 stillRecent(now: number, zoom: number) {
1156 // The adjustment makes placement more frequent when zooming.
1157 // This condition applies the adjustment only after the map has
1158 // stopped zooming. This avoids adding extra jank while zooming.
1159 const durationAdjustment = this.zoomAtLastRecencyCheck === zoom ?
1160 (1 - this.zoomAdjustment(zoom)) :
1161 1;
1162 this.zoomAtLastRecencyCheck = zoom;
1163
1164 return this.commitTime + this.fadeDuration * durationAdjustment > now;
1165 }
1166
1167 setStale() {
1168 this.stale = true;
1169 }
1170}
1171
1172function updateCollisionVertices(collisionVertexArray: CollisionVertexArray, placed: boolean, notUsed: boolean | number, shiftX?: number, shiftY?: number) {
1173 collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
1174 collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
1175 collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
1176 collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
1177}
1178
1179// All four vertices for a glyph will have the same opacity state
1180// So we pack the opacity into a uint8, and then repeat it four times
1181// to make a single uint32 that we can upload for each glyph in the
1182// label.
1183const shift25 = Math.pow(2, 25);
1184const shift24 = Math.pow(2, 24);
1185const shift17 = Math.pow(2, 17);
1186const shift16 = Math.pow(2, 16);
1187const shift9 = Math.pow(2, 9);
1188const shift8 = Math.pow(2, 8);
1189const shift1 = Math.pow(2, 1);
1190function packOpacity(opacityState: OpacityState): number {
1191 if (opacityState.opacity === 0 && !opacityState.placed) {
1192 return 0;
1193 } else if (opacityState.opacity === 1 && opacityState.placed) {
1194 return 4294967295;
1195 }
1196 const targetBit = opacityState.placed ? 1 : 0;
1197 const opacityBits = Math.floor(opacityState.opacity * 127);
1198 return opacityBits * shift25 + targetBit * shift24 +
1199 opacityBits * shift17 + targetBit * shift16 +
1200 opacityBits * shift9 + targetBit * shift8 +
1201 opacityBits * shift1 + targetBit;
1202}
1203
1204const PACKED_HIDDEN_OPACITY = 0;