@mapwhit/tilerenderer 1.3.0 → 1.5.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.
Files changed (46) hide show
  1. package/build/min/package.json +1 -1
  2. package/package.json +1 -1
  3. package/src/data/array_types.js +115 -64
  4. package/src/data/bucket/circle_bucket.js +42 -5
  5. package/src/data/bucket/fill_bucket.js +31 -13
  6. package/src/data/bucket/fill_extrusion_bucket.js +8 -6
  7. package/src/data/bucket/line_bucket.js +38 -14
  8. package/src/data/bucket/symbol_attributes.js +13 -5
  9. package/src/data/bucket/symbol_bucket.js +112 -40
  10. package/src/data/bucket/symbol_collision_buffers.js +1 -1
  11. package/src/data/bucket.js +3 -1
  12. package/src/data/feature_index.js +24 -11
  13. package/src/data/segment.js +15 -7
  14. package/src/index.js +23 -23
  15. package/src/render/draw_circle.js +45 -4
  16. package/src/render/draw_symbol.js +190 -22
  17. package/src/render/painter.js +1 -1
  18. package/src/source/geojson_source.js +118 -21
  19. package/src/source/geojson_source_diff.js +148 -0
  20. package/src/source/geojson_tiler.js +89 -0
  21. package/src/source/rtl_text_plugin.js +133 -66
  22. package/src/source/source.js +16 -5
  23. package/src/source/source_cache.js +6 -6
  24. package/src/source/source_state.js +4 -2
  25. package/src/source/tile.js +5 -3
  26. package/src/source/vector_tile_source.js +2 -0
  27. package/src/source/worker_tile.js +6 -2
  28. package/src/style/evaluation_parameters.js +2 -3
  29. package/src/style/pauseable_placement.js +39 -7
  30. package/src/style/style.js +34 -23
  31. package/src/style/style_layer/circle_style_layer_properties.js +8 -1
  32. package/src/style/style_layer/fill_style_layer_properties.js +8 -1
  33. package/src/style/style_layer/line_style_layer_properties.js +4 -0
  34. package/src/style/style_layer/symbol_style_layer_properties.js +17 -2
  35. package/src/style-spec/reference/v8.json +161 -4
  36. package/src/symbol/one_em.js +4 -0
  37. package/src/symbol/placement.js +406 -173
  38. package/src/symbol/projection.js +3 -3
  39. package/src/symbol/quads.js +1 -6
  40. package/src/symbol/shaping.js +18 -29
  41. package/src/symbol/symbol_layout.js +243 -81
  42. package/src/symbol/transform_text.js +3 -4
  43. package/src/util/config.js +1 -9
  44. package/src/util/script_detection.js +19 -7
  45. package/src/util/vectortile_to_geojson.js +3 -4
  46. package/src/source/geojson_worker_source.js +0 -97
@@ -9,7 +9,10 @@ export const dynamicLayoutAttributes = createLayout([{ name: 'a_projected_pos',
9
9
 
10
10
  export const placementOpacityAttributes = createLayout([{ name: 'a_fade_opacity', components: 1, type: 'Uint32' }], 4);
11
11
 
12
- export const collisionVertexAttributes = createLayout([{ name: 'a_placed', components: 2, type: 'Uint8' }], 4);
12
+ export const collisionVertexAttributes = createLayout([
13
+ { name: 'a_placed', components: 2, type: 'Uint8' },
14
+ { name: 'a_shift', components: 2, type: 'Float32' }
15
+ ]);
13
16
 
14
17
  export const collisionBox = createLayout([
15
18
  // the box is centered around the anchor point
@@ -70,13 +73,16 @@ export const placement = createLayout([
70
73
  { type: 'Float32', name: 'lineOffsetX' },
71
74
  { type: 'Float32', name: 'lineOffsetY' },
72
75
  { type: 'Uint8', name: 'writingMode' },
73
- { type: 'Uint8', name: 'hidden' }
76
+ { type: 'Uint8', name: 'hidden' },
77
+ { type: 'Uint32', name: 'crossTileID' }
74
78
  ]);
75
79
 
76
80
  export const symbolInstance = createLayout([
77
81
  { type: 'Int16', name: 'anchorX' },
78
82
  { type: 'Int16', name: 'anchorY' },
79
- { type: 'Int16', name: 'horizontalPlacedTextSymbolIndex' },
83
+ { type: 'Int16', name: 'rightJustifiedTextSymbolIndex' },
84
+ { type: 'Int16', name: 'centerJustifiedTextSymbolIndex' },
85
+ { type: 'Int16', name: 'leftJustifiedTextSymbolIndex' },
80
86
  { type: 'Int16', name: 'verticalPlacedTextSymbolIndex' },
81
87
  { type: 'Uint16', name: 'key' },
82
88
  { type: 'Uint16', name: 'textBoxStartIndex' },
@@ -84,10 +90,12 @@ export const symbolInstance = createLayout([
84
90
  { type: 'Uint16', name: 'iconBoxStartIndex' },
85
91
  { type: 'Uint16', name: 'iconBoxEndIndex' },
86
92
  { type: 'Uint16', name: 'featureIndex' },
87
- { type: 'Uint16', name: 'numGlyphVertices' },
93
+ { type: 'Uint16', name: 'numHorizontalGlyphVertices' },
88
94
  { type: 'Uint16', name: 'numVerticalGlyphVertices' },
89
95
  { type: 'Uint16', name: 'numIconVertices' },
90
- { type: 'Uint32', name: 'crossTileID' }
96
+ { type: 'Uint32', name: 'crossTileID' },
97
+ { type: 'Float32', name: 'textBoxScale' },
98
+ { type: 'Float32', name: 'radialTextOffset' }
91
99
  ]);
92
100
 
93
101
  export const glyphOffset = createLayout([{ type: 'Float32', name: 'offsetX' }]);
@@ -1,11 +1,12 @@
1
1
  import { dist } from '@mapwhit/point-geometry';
2
2
  import { Formatted } from '@mapwhit/style-expressions';
3
3
  import { VectorTileFeature } from '@mapwhit/vector-tile';
4
+ import { rtlPlugin } from '../../source/rtl_text_plugin.js';
4
5
  import EvaluationParameters from '../../style/evaluation_parameters.js';
5
6
  import mergeLines from '../../symbol/mergelines.js';
6
7
  import { getSizeData } from '../../symbol/symbol_size.js';
7
8
  import transformText from '../../symbol/transform_text.js';
8
- import { allowsVerticalWritingMode } from '../../util/script_detection.js';
9
+ import { allowsVerticalWritingMode, stringContainsRTLText } from '../../util/script_detection.js';
9
10
  import { verticalizedCharacterMap } from '../../util/verticalize_punctuation.js';
10
11
  import {
11
12
  CollisionBoxLayoutArray,
@@ -44,6 +45,15 @@ export function addDynamicAttributes(dynamicLayoutVertexArray, p, angle) {
44
45
  dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle);
45
46
  }
46
47
 
48
+ function containsRTLText(formattedText) {
49
+ for (const section of formattedText.sections) {
50
+ if (stringContainsRTLText(section.text)) {
51
+ return true;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+
47
57
  /**
48
58
  * Unlike other buckets, which simply implement #addFeature with type-specific
49
59
  * logic for (essentially) triangulating feature geometries, SymbolBucket
@@ -86,6 +96,8 @@ export default class SymbolBucket {
86
96
  this.pixelRatio = options.pixelRatio;
87
97
  this.sourceLayerIndex = options.sourceLayerIndex;
88
98
  this.hasPattern = false;
99
+ this.hasRTLText = false;
100
+ this.sortKeyRanges = [];
89
101
 
90
102
  const layer = this.layers[0];
91
103
  const unevaluatedLayoutValues = layer._unevaluatedLayout._values;
@@ -94,7 +106,10 @@ export default class SymbolBucket {
94
106
  this.iconSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['icon-size']);
95
107
 
96
108
  const layout = this.layers[0]._layout;
97
- const zOrderByViewportY = layout.get('symbol-z-order') === 'viewport-y';
109
+ const sortKey = layout.get('symbol-sort-key');
110
+ const zOrder = layout.get('symbol-z-order');
111
+ this.sortFeaturesByKey = zOrder !== 'viewport-y' && sortKey.constantOr(1) !== undefined;
112
+ const zOrderByViewportY = zOrder === 'viewport-y' || (zOrder === 'auto' && !this.sortFeaturesByKey);
98
113
  this.sortFeaturesByY =
99
114
  zOrderByViewportY &&
100
115
  (layout.get('text-allow-overlap') ||
@@ -102,6 +117,8 @@ export default class SymbolBucket {
102
117
  layout.get('text-ignore-placement') ||
103
118
  layout.get('icon-ignore-placement'));
104
119
 
120
+ this.stateDependentLayerIds = this.layers.filter(l => l.isStateDependent()).map(l => l.id);
121
+
105
122
  this.sourceID = options.sourceID;
106
123
  }
107
124
 
@@ -140,6 +157,7 @@ export default class SymbolBucket {
140
157
  (textField.value.kind !== 'constant' || textField.value.value.toString().length > 0) &&
141
158
  (textFont.value.kind !== 'constant' || textFont.value.value.length > 0);
142
159
  const hasIcon = iconImage.value.kind !== 'constant' || iconImage.value.value?.length > 0;
160
+ const symbolSortKey = layout.get('symbol-sort-key');
143
161
 
144
162
  this.features = [];
145
163
 
@@ -151,7 +169,7 @@ export default class SymbolBucket {
151
169
  const stacks = options.glyphDependencies;
152
170
  const globalProperties = new EvaluationParameters(this.zoom);
153
171
 
154
- for (const { feature, index, sourceLayerIndex } of features) {
172
+ for (const { feature, id, index, sourceLayerIndex } of features) {
155
173
  if (!layer._featureFilter(globalProperties, feature)) {
156
174
  continue;
157
175
  }
@@ -162,11 +180,16 @@ export default class SymbolBucket {
162
180
  // but plain string token evaluation skips that pathway so do the
163
181
  // conversion here.
164
182
  const resolvedTokens = layer.getValueAndResolveTokens('text-field', feature);
165
- text = transformText(
166
- resolvedTokens instanceof Formatted ? resolvedTokens : Formatted.fromString(resolvedTokens),
167
- layer,
168
- feature
169
- );
183
+ const formattedText =
184
+ resolvedTokens instanceof Formatted ? resolvedTokens : Formatted.fromString(resolvedTokens);
185
+ // on this instance: if hasRTLText is already true, all future calls to containsRTLText can be skipped.
186
+ const bucketHasRTLText = (this.hasRTLText = this.hasRTLText || containsRTLText(formattedText));
187
+ if (
188
+ !bucketHasRTLText || // non-rtl text so can proceed safely
189
+ rtlPlugin.isRTLSupported(true) // Use the rtlText plugin to shape text if available or We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping
190
+ ) {
191
+ text = transformText(formattedText, layer, feature);
192
+ }
170
193
  }
171
194
 
172
195
  let icon;
@@ -178,18 +201,19 @@ export default class SymbolBucket {
178
201
  continue;
179
202
  }
180
203
 
204
+ const sortKey = this.sortFeaturesByKey ? symbolSortKey.evaluate(feature, {}) : undefined;
205
+
181
206
  const symbolFeature = {
207
+ id,
182
208
  text,
183
209
  icon,
184
210
  index,
185
211
  sourceLayerIndex,
186
212
  geometry: loadGeometry(feature),
187
213
  properties: feature.properties,
188
- type: VectorTileFeature.types[feature.type]
214
+ type: VectorTileFeature.types[feature.type],
215
+ sortKey
189
216
  };
190
- if (typeof feature.id !== 'undefined') {
191
- symbolFeature.id = feature.id;
192
- }
193
217
  this.features.push(symbolFeature);
194
218
 
195
219
  if (icon) {
@@ -214,6 +238,13 @@ export default class SymbolBucket {
214
238
  // It's better to place labels on one long line than on many short segments.
215
239
  this.features = mergeLines(this.features);
216
240
  }
241
+
242
+ if (this.sortFeaturesByKey) {
243
+ this.features.sort((a, b) => {
244
+ // a.sortKey is always a number when sortFeaturesByKey is true
245
+ return a.sortKey - b.sortKey;
246
+ });
247
+ }
217
248
  }
218
249
 
219
250
  update(states, vtLayer, imagePositions) {
@@ -229,7 +260,9 @@ export default class SymbolBucket {
229
260
  }
230
261
 
231
262
  isEmpty() {
232
- return this.symbolInstances.length === 0;
263
+ // When the bucket encounters only rtl-text but the plugin isnt loaded, no symbol instances will be created.
264
+ // In order for the bucket to be serialized, and not discarded as an empty bucket both checks are necessary.
265
+ return this.symbolInstances.length === 0 && !this.hasRTLText;
233
266
  }
234
267
 
235
268
  uploadPending() {
@@ -295,7 +328,7 @@ export default class SymbolBucket {
295
328
  lineLength
296
329
  ) {
297
330
  const { indexArray, layoutVertexArray, dynamicLayoutVertexArray, segments } = arrays;
298
- const segment = segments.prepareSegment(4 * quads.length, layoutVertexArray, indexArray);
331
+ const segment = segments.prepareSegment(4 * quads.length, layoutVertexArray, indexArray, feature.sortKey);
299
332
  const glyphOffsetArrayStart = this.glyphOffsetArray.length;
300
333
  const vertexStartIndex = segment.vertexLength;
301
334
  const { x: lax, y: lay } = labelAnchor;
@@ -335,7 +368,9 @@ export default class SymbolBucket {
335
368
  lineOffset[0],
336
369
  lineOffset[1],
337
370
  writingMode,
338
- false
371
+ false,
372
+ // The crossTileID is only filled/used on the foreground for dynamic text anchors
373
+ 0
339
374
  );
340
375
 
341
376
  arrays.programConfigurations.populatePaintArrays(arrays.layoutVertexArray.length, feature, feature.index, {
@@ -439,6 +474,43 @@ export default class SymbolBucket {
439
474
  }
440
475
  }
441
476
 
477
+ getSortedSymbolIndexes(angle) {
478
+ if (this.sortedAngle === angle && this.symbolInstanceIndexes !== undefined) {
479
+ return this.symbolInstanceIndexes;
480
+ }
481
+ const sin = Math.sin(angle);
482
+ const cos = Math.cos(angle);
483
+ const rotatedYs = [];
484
+ const featureIndexes = [];
485
+ const result = [];
486
+
487
+ for (let i = 0; i < this.symbolInstances.length; ++i) {
488
+ result.push(i);
489
+ const symbolInstance = this.symbolInstances.get(i);
490
+ rotatedYs.push(Math.round(sin * symbolInstance.anchorX + cos * symbolInstance.anchorY) | 0);
491
+ featureIndexes.push(symbolInstance.featureIndex);
492
+ }
493
+
494
+ result.sort((aIndex, bIndex) => {
495
+ return rotatedYs[aIndex] - rotatedYs[bIndex] || featureIndexes[bIndex] - featureIndexes[aIndex];
496
+ });
497
+
498
+ return result;
499
+ }
500
+
501
+ addToSortKeyRanges(symbolInstanceIndex, sortKey) {
502
+ const last = this.sortKeyRanges[this.sortKeyRanges.length - 1];
503
+ if (last && last.sortKey === sortKey) {
504
+ last.symbolInstanceEnd = symbolInstanceIndex + 1;
505
+ } else {
506
+ this.sortKeyRanges.push({
507
+ sortKey,
508
+ symbolInstanceStart: symbolInstanceIndex,
509
+ symbolInstanceEnd: symbolInstanceIndex + 1
510
+ });
511
+ }
512
+ }
513
+
442
514
  sortFeatures(angle) {
443
515
  if (!this.sortFeaturesByY) {
444
516
  return;
@@ -447,7 +519,6 @@ export default class SymbolBucket {
447
519
  if (this.sortedAngle === angle) {
448
520
  return;
449
521
  }
450
- this.sortedAngle = angle;
451
522
 
452
523
  // The current approach to sorting doesn't sort across segments so don't try.
453
524
  // Sorting within segments separately seemed not to be worth the complexity.
@@ -460,35 +531,36 @@ export default class SymbolBucket {
460
531
  // sorted order.
461
532
 
462
533
  // To avoid sorting the actual symbolInstance array we sort an array of indexes.
463
- const slen = this.symbolInstances.length;
464
- const symbolInstanceIndexes = new Array(slen);
465
- const rotatedYs = new Array(slen);
466
- const featureIndexes = new Array(slen);
467
- const sin = Math.sin(angle);
468
- const cos = Math.cos(angle);
469
- for (let i = 0; i < slen; i++) {
470
- symbolInstanceIndexes[i] = i;
471
- const { anchorX, anchorY, featureIndex } = this.symbolInstances.get(i);
472
- rotatedYs[i] = Math.round(sin * anchorX + cos * anchorY) | 0;
473
- featureIndexes[i] = featureIndex;
474
- }
475
-
476
- symbolInstanceIndexes.sort((a, b) => rotatedYs[a] - rotatedYs[b] || featureIndexes[b] - featureIndexes[a]);
534
+ this.symbolInstanceIndexes = this.getSortedSymbolIndexes(angle);
535
+ this.sortedAngle = angle;
477
536
 
478
537
  this.text.indexArray.clear();
479
538
  this.icon.indexArray.clear();
480
539
 
481
- this.featureSortOrder = new Array(slen);
482
-
483
- for (let i = 0; i < slen; i++) {
484
- const index = symbolInstanceIndexes[i];
485
- const { featureIndex, horizontalPlacedTextSymbolIndex, verticalPlacedTextSymbolIndex } =
486
- this.symbolInstances.get(index);
487
- this.featureSortOrder[i] = featureIndex;
540
+ this.featureSortOrder = new Array(this.symbolInstanceIndexes.length);
541
+
542
+ for (let i = 0; i < this.symbolInstanceIndexes.length; i++) {
543
+ const index = this.symbolInstanceIndexes[i];
544
+ const {
545
+ centerJustifiedTextSymbolIndex,
546
+ featureIndex,
547
+ leftJustifiedTextSymbolIndex,
548
+ rightJustifiedTextSymbolIndex,
549
+ verticalPlacedTextSymbolIndex
550
+ } = this.symbolInstances.get(index);
551
+ this.featureSortOrder.push(featureIndex);
552
+
553
+ [rightJustifiedTextSymbolIndex, centerJustifiedTextSymbolIndex, leftJustifiedTextSymbolIndex].forEach(
554
+ (index, i, array) => {
555
+ // Only add a given index the first time it shows up,
556
+ // to avoid duplicate opacity entries when multiple justifications
557
+ // share the same glyphs.
558
+ if (index >= 0 && array.indexOf(index) === i) {
559
+ this.addIndicesForPlacedTextSymbol(index);
560
+ }
561
+ }
562
+ );
488
563
 
489
- if (horizontalPlacedTextSymbolIndex >= 0) {
490
- this.addIndicesForPlacedTextSymbol(horizontalPlacedTextSymbolIndex);
491
- }
492
564
  if (verticalPlacedTextSymbolIndex >= 0) {
493
565
  this.addIndicesForPlacedTextSymbol(verticalPlacedTextSymbolIndex);
494
566
  }
@@ -1,4 +1,4 @@
1
- import { StructArrayLayout2ub4 as CollisionVertexArray } from '../array_types.js';
1
+ import { CollisionVertexArray } from '../array_types.js';
2
2
  import SegmentVector from '../segment.js';
3
3
  import { collisionVertexAttributes } from './symbol_attributes.js';
4
4
 
@@ -49,6 +49,8 @@ export function updateBuckets(buckets, style) {
49
49
 
50
50
  // swap out the layers in the bucket with the current style layers
51
51
  bucket.layers = layers;
52
- bucket.stateDependentLayers = layers.filter(layer => layer.isStateDependent());
52
+ if (bucket.stateDependentLayerIds?.length) {
53
+ bucket.stateDependentLayers = layers.filter(layer => layer.isStateDependent());
54
+ }
53
55
  }
54
56
  }
@@ -9,12 +9,13 @@ import { FeatureIndexArray } from './array_types.js';
9
9
  import EXTENT from './extent.js';
10
10
  import loadGeometry from './load_geometry.js';
11
11
 
12
- class FeatureIndex {
13
- constructor(tileID, grid = new Grid(EXTENT, 16, 0), featureIndexArray = new FeatureIndexArray()) {
12
+ export default class FeatureIndex {
13
+ constructor(tileID, promoteId) {
14
14
  this.tileID = tileID;
15
- this.grid = grid;
15
+ this.grid = new Grid(EXTENT, 16, 0);
16
16
  this.grid3D = new Grid(EXTENT, 16, 0);
17
- this.featureIndexArray = featureIndexArray;
17
+ this.featureIndexArray = new FeatureIndexArray();
18
+ this.promoteId = promoteId;
18
19
  }
19
20
 
20
21
  insert(feature, geometry, featureIndex, sourceLayerIndex, bucketIndex, is3D) {
@@ -91,14 +92,14 @@ class FeatureIndex {
91
92
 
92
93
  const match = this.featureIndexArray.get(index);
93
94
  let featureGeometry = null;
94
- const intersectionTest = (feature, styleLayer) => {
95
+ const intersectionTest = (feature, styleLayer, id) => {
95
96
  if (!featureGeometry) {
96
97
  featureGeometry = loadGeometry(feature);
97
98
  }
98
99
  let featureState = {};
99
- if (feature.id) {
100
+ if (id !== undefined) {
100
101
  // `feature-state` expression evaluation requires feature state to be available
101
- featureState = sourceFeatureState.getState(styleLayer.sourceLayer || '_geojsonTileLayer', String(feature.id));
102
+ featureState = sourceFeatureState.getState(styleLayer.sourceLayer || '_geojsonTileLayer', id);
102
103
  }
103
104
  return styleLayer.queryIntersectsFeature(
104
105
  queryGeometry,
@@ -149,6 +150,8 @@ class FeatureIndex {
149
150
  return;
150
151
  }
151
152
 
153
+ const id = this.getId(feature, sourceLayerName);
154
+
152
155
  const { x, y, z } = this.tileID.canonical;
153
156
  for (const layerID of layerIDs) {
154
157
  if (filterLayerIDs && !filterLayerIDs.includes(layerID)) {
@@ -160,13 +163,13 @@ class FeatureIndex {
160
163
  continue;
161
164
  }
162
165
 
163
- const intersectionZ = !intersectionTest || intersectionTest(feature, styleLayer);
166
+ const intersectionZ = !intersectionTest || intersectionTest(feature, styleLayer, id);
164
167
  if (!intersectionZ) {
165
168
  // Only applied for non-symbol features
166
169
  continue;
167
170
  }
168
171
 
169
- const geojsonFeature = new GeoJSONFeature(feature, z, x, y);
172
+ const geojsonFeature = new GeoJSONFeature(feature, z, x, y, id);
170
173
  geojsonFeature.layer = styleLayer;
171
174
  const layerResult = (result[layerID] ??= []);
172
175
  layerResult.push({ featureIndex, feature: geojsonFeature, intersectionZ });
@@ -201,9 +204,19 @@ class FeatureIndex {
201
204
  }
202
205
  return result;
203
206
  }
204
- }
205
207
 
206
- export default FeatureIndex;
208
+ getId(feature, sourceLayerId) {
209
+ let id = feature.id;
210
+ if (this.promoteId) {
211
+ const propName = typeof this.promoteId === 'string' ? this.promoteId : this.promoteId[sourceLayerId];
212
+ id = feature.properties[propName];
213
+ if (typeof id === 'boolean') {
214
+ id = Number(id);
215
+ }
216
+ }
217
+ return id;
218
+ }
219
+ }
207
220
 
208
221
  function getBounds(geometry) {
209
222
  let minX = Number.POSITIVE_INFINITY;
@@ -5,20 +5,27 @@ class SegmentVector {
5
5
  this.segments = segments;
6
6
  }
7
7
 
8
- prepareSegment(numVertices, layoutVertexArray, indexArray) {
8
+ prepareSegment(numVertices, layoutVertexArray, indexArray, sortKey) {
9
9
  if (numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
10
10
  warn.once(
11
11
  `Max vertices per segment is ${SegmentVector.MAX_VERTEX_ARRAY_LENGTH}: bucket requested ${numVertices}`
12
12
  );
13
13
  }
14
14
  let segment = this.segments.at(-1);
15
- if (!segment || segment.vertexLength + numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
15
+ if (
16
+ !segment ||
17
+ segment.vertexLength + numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH ||
18
+ segment.sortKey !== sortKey
19
+ ) {
16
20
  segment = {
17
21
  vertexOffset: layoutVertexArray.length,
18
22
  primitiveOffset: indexArray.length,
19
23
  vertexLength: 0,
20
24
  primitiveLength: 0
21
25
  };
26
+ if (sortKey !== undefined) {
27
+ segment.sortKey = sortKey;
28
+ }
22
29
  this.segments.push(segment);
23
30
  }
24
31
  return segment;
@@ -39,11 +46,12 @@ class SegmentVector {
39
46
  static simpleSegment(vertexOffset, primitiveOffset, vertexLength, primitiveLength) {
40
47
  return new SegmentVector([
41
48
  {
42
- vertexOffset: vertexOffset,
43
- primitiveOffset: primitiveOffset,
44
- vertexLength: vertexLength,
45
- primitiveLength: primitiveLength,
46
- vaos: {}
49
+ vertexOffset,
50
+ primitiveOffset,
51
+ vertexLength,
52
+ primitiveLength,
53
+ vaos: {},
54
+ sortKey: 0
47
55
  }
48
56
  ]);
49
57
  }
package/src/index.js CHANGED
@@ -5,20 +5,21 @@ import { Point } from '@mapwhit/point-geometry';
5
5
  import packageJSON from '../package.json' with { type: 'json' };
6
6
  import { default as LngLat } from './geo/lng_lat.js';
7
7
  import { default as LngLatBounds } from './geo/lng_lat_bounds.js';
8
- import { setRTLTextPlugin } from './source/rtl_text_plugin.js';
8
+ import { rtlPluginLoader } from './source/rtl_text_plugin.js';
9
9
  import { default as Style } from './style/style.js';
10
10
  import { default as Map } from './ui/map.js';
11
11
  import config from './util/config.js';
12
12
 
13
13
  const { version } = packageJSON;
14
14
 
15
- export { version, config, setRTLTextPlugin, Point, LngLat, LngLatBounds, Style, Map, Evented };
15
+ export { version, config, setRTLTextPlugin, getRTLTextPluginStatus, Point, LngLat, LngLatBounds, Style, Map, Evented };
16
16
 
17
17
  // for commonjs backward compatibility
18
18
  const mapwhit = {
19
19
  version,
20
20
  config,
21
21
  setRTLTextPlugin,
22
+ getRTLTextPluginStatus,
22
23
  Point,
23
24
  LngLat,
24
25
  LngLatBounds,
@@ -27,24 +28,7 @@ const mapwhit = {
27
28
  Evented
28
29
  };
29
30
 
30
- const properties = {
31
- workerCount: {
32
- get() {
33
- return config.WORKER_COUNT;
34
- },
35
- set(count) {
36
- config.WORKER_COUNT = count;
37
- }
38
- },
39
- workerUrl: {
40
- get() {
41
- return config.WORKER_URL;
42
- },
43
- set(url) {
44
- config.WORKER_URL = url;
45
- }
46
- }
47
- };
31
+ const properties = {};
48
32
 
49
33
  Object.defineProperties(mapwhit, properties);
50
34
  Object.defineProperties(config, properties);
@@ -64,11 +48,27 @@ export default mapwhit;
64
48
  *
65
49
  * @function setRTLTextPlugin
66
50
  * @param {string} pluginURL URL pointing to the Mapbox RTL text plugin source.
67
- * @param {Function} callback Called with an error argument if there is an error.
51
+ * @param {boolean} lazy If set to `true`, mapboxgl will defer loading the plugin until rtl text is encountered,
52
+ * rtl text will then be rendered only after the plugin finishes loading.
53
+ * @example
54
+ * setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.3.0/dist/mapbox-gl-rtl-text.js', false);
55
+ * @see [Add support for right-to-left scripts](https://maplibre.org/maplibre-gl-js/docs/examples/mapbox-gl-rtl-text/)
56
+ */
57
+ function setRTLTextPlugin(pluginURL, lazy) {
58
+ return rtlPluginLoader.setRTLTextPlugin(pluginURL, lazy);
59
+ }
60
+ /**
61
+ * Gets the map's [RTL text plugin](https://www.mapbox.com/mapbox-gl-js/plugins/#mapbox-gl-rtl-text) status.
62
+ * The status can be `unavailable` (i.e. not requested or removed), `loading`, `loaded` or `error`.
63
+ * If the status is `loaded` and the plugin is requested again, an error will be thrown.
64
+ *
65
+ * @function getRTLTextPluginStatus
68
66
  * @example
69
- * mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');
70
- * @see [Add support for right-to-left scripts](https://www.mapbox.com/mapbox-gl-js/example/mapbox-gl-rtl-text/)
67
+ * const pluginStatus = getRTLTextPluginStatus();
71
68
  */
69
+ function getRTLTextPluginStatus() {
70
+ return rtlPluginLoader.getRTLTextPluginStatus();
71
+ }
72
72
 
73
73
  // canary assert: used to confirm that asserts have been removed from production build
74
74
  import assert from 'assert';
@@ -1,3 +1,4 @@
1
+ import SegmentVector from '../data/segment.js';
1
2
  import CullFaceMode from '../gl/cull_face_mode.js';
2
3
  import DepthMode from '../gl/depth_mode.js';
3
4
  import StencilMode from '../gl/stencil_mode.js';
@@ -12,6 +13,7 @@ function drawCircles(painter, sourceCache, layer, coords) {
12
13
  const opacity = layer._paint.get('circle-opacity');
13
14
  const strokeWidth = layer._paint.get('circle-stroke-width');
14
15
  const strokeOpacity = layer._paint.get('circle-stroke-opacity');
16
+ const sortFeaturesByKey = layer._layout.get('circle-sort-key').constantOr(1) !== undefined;
15
17
 
16
18
  if (opacity.constantOr(1) === 0 && (strokeWidth.constantOr(1) === 0 || strokeOpacity.constantOr(1) === 0)) {
17
19
  return;
@@ -26,6 +28,8 @@ function drawCircles(painter, sourceCache, layer, coords) {
26
28
  const stencilMode = StencilMode.disabled;
27
29
  const colorMode = painter.colorModeForRenderPass();
28
30
 
31
+ const segmentsRenderStates = [];
32
+
29
33
  for (let i = 0; i < coords.length; i++) {
30
34
  const coord = coords[i];
31
35
 
@@ -37,6 +41,43 @@ function drawCircles(painter, sourceCache, layer, coords) {
37
41
 
38
42
  const programConfiguration = bucket.programConfigurations.get(layer.id);
39
43
  const program = painter.useProgram('circle', programConfiguration);
44
+ const layoutVertexBuffer = bucket.layoutVertexBuffer;
45
+ const indexBuffer = bucket.indexBuffer;
46
+ const uniformValues = circleUniformValues(painter, coord, tile, layer);
47
+
48
+ const state = {
49
+ programConfiguration,
50
+ program,
51
+ layoutVertexBuffer,
52
+ indexBuffer,
53
+ uniformValues
54
+ };
55
+
56
+ if (sortFeaturesByKey) {
57
+ const oldSegments = bucket.segments.get();
58
+ for (const segment of oldSegments) {
59
+ segmentsRenderStates.push({
60
+ segments: new SegmentVector([segment]),
61
+ sortKey: segment.sortKey,
62
+ state
63
+ });
64
+ }
65
+ } else {
66
+ segmentsRenderStates.push({
67
+ segments: bucket.segments,
68
+ sortKey: 0,
69
+ state
70
+ });
71
+ }
72
+ }
73
+
74
+ if (sortFeaturesByKey) {
75
+ segmentsRenderStates.sort((a, b) => a.sortKey - b.sortKey);
76
+ }
77
+
78
+ for (const segmentsState of segmentsRenderStates) {
79
+ const { programConfiguration, program, layoutVertexBuffer, indexBuffer, uniformValues } = segmentsState.state;
80
+ const segments = segmentsState.segments;
40
81
 
41
82
  program.draw(
42
83
  context,
@@ -45,11 +86,11 @@ function drawCircles(painter, sourceCache, layer, coords) {
45
86
  stencilMode,
46
87
  colorMode,
47
88
  CullFaceMode.disabled,
48
- circleUniformValues(painter, coord, tile, layer),
89
+ uniformValues,
49
90
  layer.id,
50
- bucket.layoutVertexBuffer,
51
- bucket.indexBuffer,
52
- bucket.segments,
91
+ layoutVertexBuffer,
92
+ indexBuffer,
93
+ segments,
53
94
  layer._paint,
54
95
  painter.transform.zoom,
55
96
  programConfiguration