@mapwhit/tilerenderer 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/min/package.json +1 -1
- package/package.json +1 -1
- package/src/data/array_types.js +115 -64
- package/src/data/bucket/circle_bucket.js +42 -5
- package/src/data/bucket/fill_bucket.js +31 -13
- package/src/data/bucket/fill_extrusion_bucket.js +8 -6
- package/src/data/bucket/line_bucket.js +38 -14
- package/src/data/bucket/symbol_attributes.js +13 -5
- package/src/data/bucket/symbol_bucket.js +87 -33
- package/src/data/bucket/symbol_collision_buffers.js +1 -1
- package/src/data/bucket.js +3 -1
- package/src/data/feature_index.js +24 -11
- package/src/data/segment.js +15 -7
- package/src/render/draw_circle.js +45 -4
- package/src/render/draw_symbol.js +190 -22
- package/src/render/painter.js +1 -1
- package/src/source/geojson_source.js +118 -21
- package/src/source/geojson_source_diff.js +148 -0
- package/src/source/geojson_tiler.js +89 -0
- package/src/source/source.js +16 -5
- package/src/source/source_cache.js +6 -6
- package/src/source/source_state.js +4 -2
- package/src/source/tile.js +5 -3
- package/src/source/vector_tile_source.js +2 -0
- package/src/source/worker_tile.js +4 -2
- package/src/style/pauseable_placement.js +39 -7
- package/src/style/style.js +86 -34
- package/src/style/style_layer/circle_style_layer_properties.js +8 -1
- package/src/style/style_layer/fill_style_layer_properties.js +8 -1
- package/src/style/style_layer/line_style_layer_properties.js +4 -0
- package/src/style/style_layer/symbol_style_layer_properties.js +17 -2
- package/src/style-spec/reference/v8.json +161 -4
- package/src/symbol/one_em.js +4 -0
- package/src/symbol/placement.js +406 -173
- package/src/symbol/projection.js +3 -3
- package/src/symbol/quads.js +1 -6
- package/src/symbol/shaping.js +16 -27
- package/src/symbol/symbol_layout.js +243 -81
- package/src/util/vectortile_to_geojson.js +3 -4
- package/src/source/geojson_worker_source.js +0 -97
package/src/symbol/placement.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { rotate } from '@mapwhit/point-geometry';
|
|
1
2
|
import assert from 'assert';
|
|
2
3
|
import EXTENT from '../data/extent.js';
|
|
3
4
|
import pixelsToTileUnits from '../source/pixels_to_tile_units.js';
|
|
4
5
|
import properties from '../style/style_layer/symbol_style_layer_properties.js';
|
|
5
6
|
import CollisionIndex from './collision_index.js';
|
|
6
7
|
import * as projection from './projection.js';
|
|
8
|
+
import { getAnchorAlignment } from './shaping.js';
|
|
9
|
+
import { evaluateRadialOffset, getAnchorJustification } from './symbol_layout.js';
|
|
7
10
|
import * as symbolSize from './symbol_size.js';
|
|
8
11
|
|
|
9
12
|
const symbolLayoutProperties = properties.layout;
|
|
@@ -81,19 +84,50 @@ class CollisionGroups {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
function calculateVariableLayoutOffset(anchor, width, height, radialOffset, textBoxScale) {
|
|
88
|
+
const { horizontalAlign, verticalAlign } = getAnchorAlignment(anchor);
|
|
89
|
+
const shiftX = -(horizontalAlign - 0.5) * width;
|
|
90
|
+
const shiftY = -(verticalAlign - 0.5) * height;
|
|
91
|
+
const offset = evaluateRadialOffset(anchor, radialOffset);
|
|
92
|
+
return { x: shiftX + offset[0] * textBoxScale, y: shiftY + offset[1] * textBoxScale };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shiftVariableCollisionBox(collisionBox, shiftX, shiftY, rotateWithMap, pitchWithMap, angle) {
|
|
96
|
+
const { x1, x2, y1, y2, anchorPointX, anchorPointY } = collisionBox;
|
|
97
|
+
let rotatedOffset = { x: shiftX, y: shiftY };
|
|
98
|
+
if (rotateWithMap) {
|
|
99
|
+
rotatedOffset = rotate(rotatedOffset, pitchWithMap ? angle : -angle);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
x1: x1 + rotatedOffset.x,
|
|
103
|
+
y1: y1 + rotatedOffset.y,
|
|
104
|
+
x2: x2 + rotatedOffset.x,
|
|
105
|
+
y2: y2 + rotatedOffset.y,
|
|
106
|
+
// symbol anchor point stays the same regardless of text-anchor
|
|
107
|
+
anchorPointX,
|
|
108
|
+
anchorPointY
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
84
112
|
export class Placement {
|
|
85
|
-
constructor(transform, fadeDuration, crossSourceCollisions) {
|
|
113
|
+
constructor(transform, fadeDuration, crossSourceCollisions, prevPlacement) {
|
|
86
114
|
this.transform = transform.clone();
|
|
87
115
|
this.collisionIndex = new CollisionIndex(this.transform);
|
|
88
116
|
this.placements = {};
|
|
89
117
|
this.opacities = {};
|
|
118
|
+
this.variableOffsets = {};
|
|
90
119
|
this.stale = false;
|
|
91
120
|
this.fadeDuration = fadeDuration;
|
|
92
121
|
this.retainedQueryData = {};
|
|
93
122
|
this.collisionGroups = new CollisionGroups(crossSourceCollisions);
|
|
123
|
+
|
|
124
|
+
this.prevPlacement = prevPlacement;
|
|
125
|
+
if (prevPlacement) {
|
|
126
|
+
prevPlacement.prevPlacement = undefined; // Only hold on to one placement back
|
|
127
|
+
}
|
|
94
128
|
}
|
|
95
129
|
|
|
96
|
-
|
|
130
|
+
getBucketParts(results, styleLayer, tile, sortAcrossTiles) {
|
|
97
131
|
const symbolBucket = tile.getBucket(styleLayer);
|
|
98
132
|
const bucketFeatureIndex = tile.latestFeatureIndex;
|
|
99
133
|
if (!symbolBucket || !bucketFeatureIndex || styleLayer.id !== symbolBucket.layers[0].id) {
|
|
@@ -117,14 +151,6 @@ export class Placement {
|
|
|
117
151
|
pixelsToTileUnits(tile, 1, this.transform.zoom)
|
|
118
152
|
);
|
|
119
153
|
|
|
120
|
-
const iconLabelPlaneMatrix = projection.getLabelPlaneMatrix(
|
|
121
|
-
posMatrix,
|
|
122
|
-
layout.get('icon-pitch-alignment') === 'map',
|
|
123
|
-
layout.get('icon-rotation-alignment') === 'map',
|
|
124
|
-
this.transform,
|
|
125
|
-
pixelsToTileUnits(tile, 1, this.transform.zoom)
|
|
126
|
-
);
|
|
127
|
-
|
|
128
154
|
// As long as this placement lives, we have to hold onto this bucket's
|
|
129
155
|
// matching FeatureIndex/data for querying purposes
|
|
130
156
|
this.retainedQueryData[symbolBucket.bucketInstanceId] = new RetainedQueryData(
|
|
@@ -135,45 +161,110 @@ export class Placement {
|
|
|
135
161
|
tile.tileID
|
|
136
162
|
);
|
|
137
163
|
|
|
138
|
-
|
|
139
|
-
symbolBucket,
|
|
164
|
+
const parameters = {
|
|
165
|
+
bucket: symbolBucket,
|
|
166
|
+
layout,
|
|
140
167
|
posMatrix,
|
|
141
168
|
textLabelPlaneMatrix,
|
|
142
|
-
iconLabelPlaneMatrix,
|
|
143
169
|
scale,
|
|
144
170
|
textPixelRatio,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
holdingForFade: tile.holdingForFade(),
|
|
172
|
+
collisionBoxArray,
|
|
173
|
+
partiallyEvaluatedTextSize: symbolSize.evaluateSizeForZoom(
|
|
174
|
+
symbolBucket.textSizeData,
|
|
175
|
+
this.transform.zoom,
|
|
176
|
+
symbolLayoutProperties.properties['text-size']
|
|
177
|
+
),
|
|
178
|
+
collisionGroup: this.collisionGroups.get(symbolBucket.sourceID)
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (sortAcrossTiles) {
|
|
182
|
+
for (const range of symbolBucket.sortKeyRanges) {
|
|
183
|
+
const { sortKey, symbolInstanceStart, symbolInstanceEnd } = range;
|
|
184
|
+
results.push({ sortKey, symbolInstanceStart, symbolInstanceEnd, parameters });
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
results.push({
|
|
188
|
+
symbolInstanceStart: 0,
|
|
189
|
+
symbolInstanceEnd: symbolBucket.symbolInstances.length,
|
|
190
|
+
parameters
|
|
191
|
+
});
|
|
192
|
+
}
|
|
150
193
|
}
|
|
151
194
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
195
|
+
attemptAnchorPlacement(
|
|
196
|
+
anchor,
|
|
197
|
+
textBox,
|
|
198
|
+
width,
|
|
199
|
+
height,
|
|
200
|
+
radialTextOffset,
|
|
201
|
+
textBoxScale,
|
|
202
|
+
rotateWithMap,
|
|
203
|
+
pitchWithMap,
|
|
158
204
|
textPixelRatio,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
205
|
+
posMatrix,
|
|
206
|
+
collisionGroup,
|
|
207
|
+
textAllowOverlap,
|
|
208
|
+
symbolInstance,
|
|
209
|
+
bucket
|
|
163
210
|
) {
|
|
164
|
-
const
|
|
211
|
+
const shift = calculateVariableLayoutOffset(anchor, width, height, radialTextOffset, textBoxScale);
|
|
165
212
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
const placedGlyphBoxes = this.collisionIndex.placeCollisionBox(
|
|
214
|
+
shiftVariableCollisionBox(textBox, shift.x, shift.y, rotateWithMap, pitchWithMap, this.transform.angle),
|
|
215
|
+
textAllowOverlap,
|
|
216
|
+
textPixelRatio,
|
|
217
|
+
posMatrix,
|
|
218
|
+
collisionGroup.predicate
|
|
170
219
|
);
|
|
171
220
|
|
|
221
|
+
if (placedGlyphBoxes.box.length > 0) {
|
|
222
|
+
let prevAnchor;
|
|
223
|
+
// If this label was placed in the previous placement, record the anchor position
|
|
224
|
+
// to allow us to animate the transition
|
|
225
|
+
if (
|
|
226
|
+
this.prevPlacement?.variableOffsets[symbolInstance.crossTileID] &&
|
|
227
|
+
this.prevPlacement?.placements[symbolInstance.crossTileID] &&
|
|
228
|
+
this.prevPlacement?.placements[symbolInstance.crossTileID].text
|
|
229
|
+
) {
|
|
230
|
+
prevAnchor = this.prevPlacement.variableOffsets[symbolInstance.crossTileID].anchor;
|
|
231
|
+
}
|
|
232
|
+
assert(symbolInstance.crossTileID !== 0);
|
|
233
|
+
this.variableOffsets[symbolInstance.crossTileID] = {
|
|
234
|
+
radialOffset: radialTextOffset,
|
|
235
|
+
width,
|
|
236
|
+
height,
|
|
237
|
+
anchor,
|
|
238
|
+
textBoxScale,
|
|
239
|
+
prevAnchor
|
|
240
|
+
};
|
|
241
|
+
this.markUsedJustification(bucket, anchor, symbolInstance);
|
|
242
|
+
return placedGlyphBoxes;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
placeLayerBucketPart(bucketPart, seenCrossTileIDs, showCollisionBoxes) {
|
|
247
|
+
const {
|
|
248
|
+
bucket,
|
|
249
|
+
layout,
|
|
250
|
+
posMatrix,
|
|
251
|
+
textLabelPlaneMatrix,
|
|
252
|
+
scale,
|
|
253
|
+
textPixelRatio,
|
|
254
|
+
holdingForFade,
|
|
255
|
+
collisionBoxArray,
|
|
256
|
+
partiallyEvaluatedTextSize,
|
|
257
|
+
collisionGroup
|
|
258
|
+
} = bucketPart.parameters;
|
|
259
|
+
|
|
172
260
|
const textOptional = layout.get('text-optional');
|
|
173
261
|
const iconOptional = layout.get('icon-optional');
|
|
174
|
-
|
|
175
262
|
const textAllowOverlap = layout.get('text-allow-overlap');
|
|
176
263
|
const iconAllowOverlap = layout.get('icon-allow-overlap');
|
|
264
|
+
const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
|
|
265
|
+
const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
|
|
266
|
+
const zOrderByViewportY = layout.get('symbol-z-order') === 'viewport-y';
|
|
267
|
+
|
|
177
268
|
// This logic is similar to the "defaultOpacityState" logic below in updateBucketOpacities
|
|
178
269
|
// If we know a symbol is always supposed to show, force it to be marked visible even if
|
|
179
270
|
// it wasn't placed into the collision index (because some or all of it was outside the range
|
|
@@ -191,161 +282,246 @@ export class Placement {
|
|
|
191
282
|
const alwaysShowText = textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || iconOptional);
|
|
192
283
|
const alwaysShowIcon = iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || textOptional);
|
|
193
284
|
|
|
194
|
-
const collisionGroup = this.collisionGroups.get(bucket.sourceID);
|
|
195
|
-
|
|
196
285
|
if (!bucket.collisionArrays && collisionBoxArray) {
|
|
197
286
|
bucket.deserializeCollisionBoxes(collisionBoxArray);
|
|
198
287
|
}
|
|
199
288
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
let placeText = false;
|
|
211
|
-
let placeIcon = false;
|
|
212
|
-
let offscreen = true;
|
|
289
|
+
const placeSymbol = (symbolInstance, collisionArrays) => {
|
|
290
|
+
if (seenCrossTileIDs[symbolInstance.crossTileID]) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (holdingForFade) {
|
|
294
|
+
// Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
|
|
295
|
+
// know yet if we have a duplicate in a parent tile that _should_ be placed.
|
|
296
|
+
this.placements[symbolInstance.crossTileID] = new JointPlacement(false, false, false);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
213
299
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
300
|
+
let placeText = false;
|
|
301
|
+
let placeIcon = false;
|
|
302
|
+
let offscreen = true;
|
|
217
303
|
|
|
218
|
-
|
|
219
|
-
|
|
304
|
+
let placedGlyphBoxes = null;
|
|
305
|
+
let placedGlyphCircles = null;
|
|
306
|
+
let placedIconBoxes = null;
|
|
307
|
+
let textFeatureIndex = 0;
|
|
308
|
+
let iconFeatureIndex = 0;
|
|
220
309
|
|
|
221
|
-
|
|
310
|
+
if (collisionArrays.textFeatureIndex) {
|
|
311
|
+
textFeatureIndex = collisionArrays.textFeatureIndex;
|
|
312
|
+
}
|
|
222
313
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (collisionArrays.textBox) {
|
|
314
|
+
const textBox = collisionArrays.textBox;
|
|
315
|
+
if (textBox) {
|
|
316
|
+
if (!layout.get('text-variable-anchor')) {
|
|
227
317
|
placedGlyphBoxes = this.collisionIndex.placeCollisionBox(
|
|
228
|
-
|
|
229
|
-
|
|
318
|
+
textBox,
|
|
319
|
+
textAllowOverlap,
|
|
230
320
|
textPixelRatio,
|
|
231
321
|
posMatrix,
|
|
232
322
|
collisionGroup.predicate
|
|
233
323
|
);
|
|
234
324
|
placeText = placedGlyphBoxes.box.length > 0;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
placedSymbol,
|
|
251
|
-
bucket.lineVertexArray,
|
|
252
|
-
bucket.glyphOffsetArray,
|
|
253
|
-
fontSize,
|
|
254
|
-
posMatrix,
|
|
255
|
-
textLabelPlaneMatrix,
|
|
256
|
-
showCollisionBoxes,
|
|
257
|
-
layout.get('text-pitch-alignment') === 'map',
|
|
258
|
-
collisionGroup.predicate
|
|
259
|
-
);
|
|
260
|
-
// If text-allow-overlap is set, force "placedCircles" to true
|
|
261
|
-
// In theory there should always be at least one circle placed
|
|
262
|
-
// in this case, but for now quirks in text-anchor
|
|
263
|
-
// and text-offset may prevent that from being true.
|
|
264
|
-
placeText = layout.get('text-allow-overlap') || placedGlyphCircles.circles.length > 0;
|
|
265
|
-
offscreen = offscreen && placedGlyphCircles.offscreen;
|
|
266
|
-
}
|
|
325
|
+
} else {
|
|
326
|
+
const width = textBox.x2 - textBox.x1;
|
|
327
|
+
const height = textBox.y2 - textBox.y1;
|
|
328
|
+
const textBoxScale = symbolInstance.textBoxScale;
|
|
329
|
+
let anchors = layout.get('text-variable-anchor');
|
|
330
|
+
|
|
331
|
+
// If we this symbol was in the last placement, shift the previously used
|
|
332
|
+
// anchor to the front of the anchor list.
|
|
333
|
+
if (this.prevPlacement?.variableOffsets[symbolInstance.crossTileID]) {
|
|
334
|
+
const prevOffsets = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
|
|
335
|
+
if (anchors[0] !== prevOffsets.anchor) {
|
|
336
|
+
anchors = anchors.filter(anchor => anchor !== prevOffsets.anchor);
|
|
337
|
+
anchors.unshift(prevOffsets.anchor);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
267
340
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
341
|
+
for (const anchor of anchors) {
|
|
342
|
+
placedGlyphBoxes = this.attemptAnchorPlacement(
|
|
343
|
+
anchor,
|
|
344
|
+
textBox,
|
|
345
|
+
width,
|
|
346
|
+
height,
|
|
347
|
+
symbolInstance.radialTextOffset,
|
|
348
|
+
textBoxScale,
|
|
349
|
+
rotateWithMap,
|
|
350
|
+
pitchWithMap,
|
|
351
|
+
textPixelRatio,
|
|
352
|
+
posMatrix,
|
|
353
|
+
collisionGroup,
|
|
354
|
+
textAllowOverlap,
|
|
355
|
+
symbolInstance,
|
|
356
|
+
bucket
|
|
357
|
+
);
|
|
358
|
+
if (placedGlyphBoxes) {
|
|
359
|
+
placeText = true;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
282
363
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
} else if (!iconWithoutText) {
|
|
293
|
-
placeIcon = placeIcon && placeText;
|
|
364
|
+
// If we didn't get placed, we still need to copy our position from the last placement for
|
|
365
|
+
// fade animations
|
|
366
|
+
if (!this.variableOffsets[symbolInstance.crossTileID] && this.prevPlacement) {
|
|
367
|
+
const prevOffset = this.prevPlacement.variableOffsets[symbolInstance.crossTileID];
|
|
368
|
+
if (prevOffset) {
|
|
369
|
+
this.variableOffsets[symbolInstance.crossTileID] = prevOffset;
|
|
370
|
+
this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
294
373
|
}
|
|
374
|
+
}
|
|
295
375
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
376
|
+
offscreen = placedGlyphBoxes?.offscreen;
|
|
377
|
+
const textCircles = collisionArrays.textCircles;
|
|
378
|
+
if (textCircles) {
|
|
379
|
+
const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.centerJustifiedTextSymbolIndex);
|
|
380
|
+
const fontSize = symbolSize.evaluateSizeForFeature(
|
|
381
|
+
bucket.textSizeData,
|
|
382
|
+
partiallyEvaluatedTextSize,
|
|
383
|
+
placedSymbol
|
|
384
|
+
);
|
|
385
|
+
placedGlyphCircles = this.collisionIndex.placeCollisionCircles(
|
|
386
|
+
textCircles,
|
|
387
|
+
textAllowOverlap,
|
|
388
|
+
scale,
|
|
389
|
+
textPixelRatio,
|
|
390
|
+
placedSymbol,
|
|
391
|
+
bucket.lineVertexArray,
|
|
392
|
+
bucket.glyphOffsetArray,
|
|
393
|
+
fontSize,
|
|
394
|
+
posMatrix,
|
|
395
|
+
textLabelPlaneMatrix,
|
|
396
|
+
showCollisionBoxes,
|
|
397
|
+
pitchWithMap,
|
|
398
|
+
collisionGroup.predicate
|
|
399
|
+
);
|
|
400
|
+
// If text-allow-overlap is set, force "placedCircles" to true
|
|
401
|
+
// In theory there should always be at least one circle placed
|
|
402
|
+
// in this case, but for now quirks in text-anchor
|
|
403
|
+
// and text-offset may prevent that from being true.
|
|
404
|
+
placeText = textAllowOverlap || placedGlyphCircles.circles.length > 0;
|
|
405
|
+
offscreen = offscreen && placedGlyphCircles.offscreen;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (collisionArrays.iconFeatureIndex) {
|
|
409
|
+
iconFeatureIndex = collisionArrays.iconFeatureIndex;
|
|
410
|
+
}
|
|
411
|
+
if (collisionArrays.iconBox) {
|
|
412
|
+
placedIconBoxes = this.collisionIndex.placeCollisionBox(
|
|
413
|
+
collisionArrays.iconBox,
|
|
414
|
+
iconAllowOverlap,
|
|
415
|
+
textPixelRatio,
|
|
416
|
+
posMatrix,
|
|
417
|
+
collisionGroup.predicate
|
|
418
|
+
);
|
|
419
|
+
placeIcon = placedIconBoxes.box.length > 0;
|
|
420
|
+
offscreen = offscreen && placedIconBoxes.offscreen;
|
|
421
|
+
}
|
|
323
422
|
|
|
324
|
-
|
|
325
|
-
|
|
423
|
+
const iconWithoutText =
|
|
424
|
+
textOptional ||
|
|
425
|
+
(symbolInstance.numHorizontalGlyphVertices === 0 && symbolInstance.numVerticalGlyphVertices === 0);
|
|
426
|
+
const textWithoutIcon = iconOptional || symbolInstance.numIconVertices === 0;
|
|
427
|
+
|
|
428
|
+
// Combine the scales for icons and text.
|
|
429
|
+
if (!iconWithoutText && !textWithoutIcon) {
|
|
430
|
+
placeIcon = placeText = placeIcon && placeText;
|
|
431
|
+
} else if (!textWithoutIcon) {
|
|
432
|
+
placeText = placeIcon && placeText;
|
|
433
|
+
} else if (!iconWithoutText) {
|
|
434
|
+
placeIcon = placeIcon && placeText;
|
|
435
|
+
}
|
|
326
436
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
437
|
+
if (placeText && placedGlyphBoxes) {
|
|
438
|
+
this.collisionIndex.insertCollisionBox(
|
|
439
|
+
placedGlyphBoxes.box,
|
|
440
|
+
layout.get('text-ignore-placement'),
|
|
441
|
+
bucket.bucketInstanceId,
|
|
442
|
+
textFeatureIndex,
|
|
443
|
+
collisionGroup.ID
|
|
331
444
|
);
|
|
332
|
-
|
|
445
|
+
}
|
|
446
|
+
if (placeIcon && placedIconBoxes) {
|
|
447
|
+
this.collisionIndex.insertCollisionBox(
|
|
448
|
+
placedIconBoxes.box,
|
|
449
|
+
layout.get('icon-ignore-placement'),
|
|
450
|
+
bucket.bucketInstanceId,
|
|
451
|
+
iconFeatureIndex,
|
|
452
|
+
collisionGroup.ID
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (placeText && placedGlyphCircles) {
|
|
456
|
+
this.collisionIndex.insertCollisionCircles(
|
|
457
|
+
placedGlyphCircles.circles,
|
|
458
|
+
layout.get('text-ignore-placement'),
|
|
459
|
+
bucket.bucketInstanceId,
|
|
460
|
+
textFeatureIndex,
|
|
461
|
+
collisionGroup.ID
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
assert(symbolInstance.crossTileID !== 0);
|
|
466
|
+
assert(bucket.bucketInstanceId !== 0);
|
|
467
|
+
|
|
468
|
+
this.placements[symbolInstance.crossTileID] = new JointPlacement(
|
|
469
|
+
placeText || (alwaysShowText && placedGlyphBoxes),
|
|
470
|
+
placeIcon || alwaysShowIcon,
|
|
471
|
+
offscreen || bucket.justReloaded
|
|
472
|
+
);
|
|
473
|
+
seenCrossTileIDs[symbolInstance.crossTileID] = true;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
if (zOrderByViewportY) {
|
|
477
|
+
assert(bucketPart.symbolInstanceStart === 0);
|
|
478
|
+
const symbolIndexes = bucket.getSortedSymbolIndexes(this.transform.angle);
|
|
479
|
+
for (let i = symbolIndexes.length - 1; i >= 0; --i) {
|
|
480
|
+
const symbolIndex = symbolIndexes[i];
|
|
481
|
+
placeSymbol(bucket.symbolInstances.get(symbolIndex), bucket.collisionArrays[symbolIndex]);
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
for (let i = bucketPart.symbolInstanceStart; i < bucketPart.symbolInstanceEnd; i++) {
|
|
485
|
+
placeSymbol(bucket.symbolInstances.get(i), bucket.collisionArrays[i]);
|
|
333
486
|
}
|
|
334
487
|
}
|
|
335
488
|
|
|
336
489
|
bucket.justReloaded = false;
|
|
337
490
|
}
|
|
338
491
|
|
|
339
|
-
|
|
492
|
+
markUsedJustification(bucket, placedAnchor, symbolInstance) {
|
|
493
|
+
const justifications = {
|
|
494
|
+
left: symbolInstance.leftJustifiedTextSymbolIndex,
|
|
495
|
+
center: symbolInstance.centerJustifiedTextSymbolIndex,
|
|
496
|
+
right: symbolInstance.rightJustifiedTextSymbolIndex
|
|
497
|
+
};
|
|
498
|
+
const autoIndex = justifications[getAnchorJustification(placedAnchor)];
|
|
499
|
+
|
|
500
|
+
for (const justification in justifications) {
|
|
501
|
+
const index = justifications[justification];
|
|
502
|
+
if (index >= 0) {
|
|
503
|
+
if (autoIndex >= 0 && index !== autoIndex) {
|
|
504
|
+
// There are multiple justifications and this one isn't it: shift offscreen
|
|
505
|
+
bucket.text.placedSymbolArray.get(index).crossTileID = 0;
|
|
506
|
+
} else {
|
|
507
|
+
// Either this is the chosen justification or the justification is hardwired: use this one
|
|
508
|
+
bucket.text.placedSymbolArray.get(index).crossTileID = symbolInstance.crossTileID;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
commit(now) {
|
|
340
515
|
this.commitTime = now;
|
|
341
516
|
|
|
517
|
+
const prevPlacement = this.prevPlacement;
|
|
342
518
|
let placementChanged = false;
|
|
343
519
|
|
|
344
520
|
const increment =
|
|
345
521
|
prevPlacement && this.fadeDuration !== 0 ? (this.commitTime - prevPlacement.commitTime) / this.fadeDuration : 1;
|
|
346
522
|
|
|
347
523
|
const prevOpacities = prevPlacement ? prevPlacement.opacities : {};
|
|
348
|
-
|
|
524
|
+
const prevOffsets = prevPlacement ? prevPlacement.variableOffsets : {};
|
|
349
525
|
// add the opacities from the current placement, and copy their current values from the previous placement
|
|
350
526
|
for (const crossTileID in this.placements) {
|
|
351
527
|
const jointPlacement = this.placements[crossTileID];
|
|
@@ -384,6 +560,15 @@ export class Placement {
|
|
|
384
560
|
}
|
|
385
561
|
}
|
|
386
562
|
}
|
|
563
|
+
for (const crossTileID in prevOffsets) {
|
|
564
|
+
if (
|
|
565
|
+
!this.variableOffsets[crossTileID] &&
|
|
566
|
+
this.opacities[crossTileID] &&
|
|
567
|
+
!this.opacities[crossTileID].isHidden()
|
|
568
|
+
) {
|
|
569
|
+
this.variableOffsets[crossTileID] = prevOffsets[crossTileID];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
387
572
|
|
|
388
573
|
// this.lastPlacementChangeTime is the time of the last commit() that
|
|
389
574
|
// resulted in a placement change -- in other words, the start time of
|
|
@@ -398,7 +583,6 @@ export class Placement {
|
|
|
398
583
|
|
|
399
584
|
updateLayerOpacities(styleLayer, tiles) {
|
|
400
585
|
const seenCrossTileIDs = {};
|
|
401
|
-
|
|
402
586
|
for (const tile of tiles) {
|
|
403
587
|
const symbolBucket = tile.getBucket(styleLayer);
|
|
404
588
|
if (symbolBucket && tile.latestFeatureIndex && styleLayer.id === symbolBucket.layers[0].id) {
|
|
@@ -425,6 +609,9 @@ export class Placement {
|
|
|
425
609
|
const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true);
|
|
426
610
|
const textAllowOverlap = layout.get('text-allow-overlap');
|
|
427
611
|
const iconAllowOverlap = layout.get('icon-allow-overlap');
|
|
612
|
+
const variablePlacement = layout.get('text-variable-anchor');
|
|
613
|
+
const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
|
|
614
|
+
const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
|
|
428
615
|
// If allow-overlap is true, we can show symbols before placement runs on them
|
|
429
616
|
// But we have to wait for placement if we potentially depend on a paired icon/text
|
|
430
617
|
// with allow-overlap: false.
|
|
@@ -447,38 +634,51 @@ export class Placement {
|
|
|
447
634
|
|
|
448
635
|
for (let s = 0; s < bucket.symbolInstances.length; s++) {
|
|
449
636
|
const symbolInstance = bucket.symbolInstances.get(s);
|
|
450
|
-
const
|
|
637
|
+
const { numHorizontalGlyphVertices, numVerticalGlyphVertices, crossTileID } = symbolInstance;
|
|
638
|
+
|
|
639
|
+
const isDuplicate = seenCrossTileIDs[crossTileID];
|
|
451
640
|
|
|
452
|
-
let opacityState = this.opacities[
|
|
641
|
+
let opacityState = this.opacities[crossTileID];
|
|
453
642
|
if (isDuplicate) {
|
|
454
643
|
opacityState = duplicateOpacityState;
|
|
455
644
|
} else if (!opacityState) {
|
|
456
645
|
opacityState = defaultOpacityState;
|
|
457
646
|
// store the state so that future placements use it as a starting point
|
|
458
|
-
this.opacities[
|
|
647
|
+
this.opacities[crossTileID] = opacityState;
|
|
459
648
|
}
|
|
460
649
|
|
|
461
|
-
seenCrossTileIDs[
|
|
650
|
+
seenCrossTileIDs[crossTileID] = true;
|
|
462
651
|
|
|
463
|
-
const hasText =
|
|
652
|
+
const hasText = numHorizontalGlyphVertices > 0 || numVerticalGlyphVertices > 0;
|
|
464
653
|
const hasIcon = symbolInstance.numIconVertices > 0;
|
|
465
654
|
|
|
466
655
|
if (hasText) {
|
|
467
656
|
const packedOpacity = packOpacity(opacityState.text);
|
|
468
657
|
// Vertical text fades in/out on collision the same way as corresponding
|
|
469
658
|
// horizontal text. Switch between vertical/horizontal should be instantaneous
|
|
470
|
-
const opacityEntryCount = (
|
|
659
|
+
const opacityEntryCount = (numHorizontalGlyphVertices + numVerticalGlyphVertices) / 4;
|
|
471
660
|
for (let i = 0; i < opacityEntryCount; i++) {
|
|
472
661
|
bucket.text.opacityVertexArray.emplaceBack(packedOpacity);
|
|
473
662
|
}
|
|
474
663
|
// If this label is completely faded, mark it so that we don't have to calculate
|
|
475
|
-
// its position at render time
|
|
476
|
-
|
|
477
|
-
|
|
664
|
+
// its position at render time. If this layer has variable placement, shift the various
|
|
665
|
+
// symbol instances appropriately so that symbols from buckets that have yet to be placed
|
|
666
|
+
// offset appropriately.
|
|
667
|
+
const hidden = opacityState.text.isHidden() ? 1 : 0;
|
|
668
|
+
[
|
|
669
|
+
symbolInstance.rightJustifiedTextSymbolIndex,
|
|
670
|
+
symbolInstance.centerJustifiedTextSymbolIndex,
|
|
671
|
+
symbolInstance.leftJustifiedTextSymbolIndex,
|
|
672
|
+
symbolInstance.verticalPlacedTextSymbolIndex
|
|
673
|
+
].forEach(index => {
|
|
674
|
+
if (index >= 0) {
|
|
675
|
+
bucket.text.placedSymbolArray.get(index).hidden = hidden;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
478
678
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
679
|
+
const prevOffset = this.variableOffsets[symbolInstance.crossTileID];
|
|
680
|
+
if (prevOffset) {
|
|
681
|
+
this.markUsedJustification(bucket, prevOffset.anchor, symbolInstance);
|
|
482
682
|
}
|
|
483
683
|
}
|
|
484
684
|
|
|
@@ -494,7 +694,40 @@ export class Placement {
|
|
|
494
694
|
const collisionArrays = bucket.collisionArrays[s];
|
|
495
695
|
if (collisionArrays) {
|
|
496
696
|
if (collisionArrays.textBox) {
|
|
497
|
-
|
|
697
|
+
let shift = { x: 0, y: 0 };
|
|
698
|
+
let used = true;
|
|
699
|
+
if (variablePlacement) {
|
|
700
|
+
const variableOffset = this.variableOffsets[crossTileID];
|
|
701
|
+
if (variableOffset) {
|
|
702
|
+
// This will show either the currently placed position or the last
|
|
703
|
+
// successfully placed position (so you can visualize what collision
|
|
704
|
+
// just made the symbol disappear, and the most likely place for the
|
|
705
|
+
// symbol to come back)
|
|
706
|
+
shift = calculateVariableLayoutOffset(
|
|
707
|
+
variableOffset.anchor,
|
|
708
|
+
variableOffset.width,
|
|
709
|
+
variableOffset.height,
|
|
710
|
+
variableOffset.radialOffset,
|
|
711
|
+
variableOffset.textBoxScale
|
|
712
|
+
);
|
|
713
|
+
if (rotateWithMap) {
|
|
714
|
+
shift = rotate(shift, pitchWithMap ? this.transform.angle : -this.transform.angle);
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
// No offset -> this symbol hasn't been placed since coming on-screen
|
|
718
|
+
// No single box is particularly meaningful and all of them would be too noisy
|
|
719
|
+
// Use the center box just to show something's there, but mark it "not used"
|
|
720
|
+
used = false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
updateCollisionVertices(
|
|
725
|
+
bucket.collisionBox.collisionVertexArray,
|
|
726
|
+
opacityState.text.placed,
|
|
727
|
+
!used,
|
|
728
|
+
shift.x,
|
|
729
|
+
shift.y
|
|
730
|
+
);
|
|
498
731
|
}
|
|
499
732
|
|
|
500
733
|
if (collisionArrays.iconBox) {
|
|
@@ -551,11 +784,11 @@ export class Placement {
|
|
|
551
784
|
}
|
|
552
785
|
}
|
|
553
786
|
|
|
554
|
-
function updateCollisionVertices(collisionVertexArray, placed, notUsed) {
|
|
555
|
-
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0);
|
|
556
|
-
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0);
|
|
557
|
-
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0);
|
|
558
|
-
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0);
|
|
787
|
+
function updateCollisionVertices(collisionVertexArray, placed, notUsed, shiftX, shiftY) {
|
|
788
|
+
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
|
|
789
|
+
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
|
|
790
|
+
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
|
|
791
|
+
collisionVertexArray.emplaceBack(placed ? 1 : 0, notUsed ? 1 : 0, shiftX || 0, shiftY || 0);
|
|
559
792
|
}
|
|
560
793
|
|
|
561
794
|
// All four vertices for a glyph will have the same opacity state
|