1 | import CollisionIndex from './collision_index';
|
2 | import type {FeatureKey} from './collision_index';
|
3 | import EXTENT from '../data/extent';
|
4 | import * as symbolSize from './symbol_size';
|
5 | import * as projection from './projection';
|
6 | import {getAnchorJustification, evaluateVariableOffset} from './symbol_layout';
|
7 | import {getAnchorAlignment, WritingMode} from './shaping';
|
8 | import {mat4} from 'gl-matrix';
|
9 | import assert from 'assert';
|
10 | import pixelsToTileUnits from '../source/pixels_to_tile_units';
|
11 | import Point from '@mapbox/point-geometry';
|
12 | import type Transform from '../geo/transform';
|
13 | import type StyleLayer from '../style/style_layer';
|
14 | import {PossiblyEvaluated} from '../style/properties';
|
15 | import type {SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated} from '../style/style_layer/symbol_style_layer_properties.g';
|
16 | import {getOverlapMode, OverlapMode} from '../style/style_layer/symbol_style_layer';
|
17 |
|
18 | import type Tile from '../source/tile';
|
19 | import SymbolBucket, {CollisionArrays, SingleCollisionBox} from '../data/bucket/symbol_bucket';
|
20 |
|
21 | import type {CollisionBoxArray, CollisionVertexArray, SymbolInstance} from '../data/array_types.g';
|
22 | import type FeatureIndex from '../data/feature_index';
|
23 | import type {OverscaledTileID} from '../source/tile_id';
|
24 | import type {TextAnchor} from './symbol_layout';
|
25 |
|
26 | class 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 |
|
42 | class 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 |
|
54 | class JointPlacement {
|
55 | text: boolean;
|
56 | icon: boolean;
|
57 |
|
58 |
|
59 |
|
60 |
|
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 |
|
69 | class CollisionCircleArray {
|
70 |
|
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 |
|
82 | export 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 |
|
102 | type CollisionGroup = {
|
103 | ID: number;
|
104 | predicate?: (key: FeatureKey) => boolean;
|
105 | };
|
106 |
|
107 | class 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 |
|
120 |
|
121 |
|
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 |
|
139 | function 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 |
|
156 | function 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 |
|
171 | anchorPointX,
|
172 | anchorPointY
|
173 | };
|
174 | }
|
175 |
|
176 | export type VariableOffset = {
|
177 | textOffset: [number, number];
|
178 | width: number;
|
179 | height: number;
|
180 | anchor: TextAnchor;
|
181 | textBoxScale: number;
|
182 | prevAnchor?: TextAnchor;
|
183 | };
|
184 |
|
185 | type 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 |
|
202 | export type BucketPart = {
|
203 | sortKey?: number | void;
|
204 | symbolInstanceStart: number;
|
205 | symbolInstanceEnd: number;
|
206 | parameters: TileLayerParameters;
|
207 | };
|
208 |
|
209 | export type CrossTileID = string | number;
|
210 |
|
211 | export 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;
|
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 |
|
301 |
|
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 |
|
383 |
|
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 |
|
439 |
|
440 |
|
441 |
|
442 |
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
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 |
|
463 |
|
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 |
|
558 |
|
559 |
|
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 |
|
624 |
|
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 |
|
665 |
|
666 |
|
667 |
|
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 |
|
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 |
|
756 |
|
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]);
|
762 | circleArray.circles.push(placedGlyphCircles.circles[i + 1]);
|
763 | circleArray.circles.push(placedGlyphCircles.circles[i + 2]);
|
764 | circleArray.circles.push(placedGlyphCircles.collisionDetected ? 1 : 0);
|
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 |
|
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 |
|
825 | bucket.text.placedSymbolArray.get(index).crossTileID = 0;
|
826 | } else {
|
827 |
|
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 |
|
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 |
|
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 |
|
906 |
|
907 |
|
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 |
|
943 |
|
944 |
|
945 |
|
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 |
|
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 |
|
992 |
|
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 |
|
999 |
|
1000 |
|
1001 |
|
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 |
|
1059 |
|
1060 |
|
1061 |
|
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 |
|
1072 |
|
1073 |
|
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 |
|
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 |
|
1144 |
|
1145 |
|
1146 |
|
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 |
|
1157 |
|
1158 |
|
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 |
|
1172 | function 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 |
|
1180 |
|
1181 |
|
1182 |
|
1183 | const shift25 = Math.pow(2, 25);
|
1184 | const shift24 = Math.pow(2, 24);
|
1185 | const shift17 = Math.pow(2, 17);
|
1186 | const shift16 = Math.pow(2, 16);
|
1187 | const shift9 = Math.pow(2, 9);
|
1188 | const shift8 = Math.pow(2, 8);
|
1189 | const shift1 = Math.pow(2, 1);
|
1190 | function 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 |
|
1204 | const PACKED_HIDDEN_OPACITY = 0;
|