@tangram.gl/basemap-layers 0.0.0-stage → 0.0.1

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 (105) hide show
  1. package/README.md +34 -2
  2. package/THIRD-PARTY-NOTICES.md +205 -0
  3. package/dist/geojson-worker.js +2160 -0
  4. package/dist/geojson-worker.js.map +6 -0
  5. package/dist/index.cjs +11388 -0
  6. package/dist/index.cjs.map +6 -0
  7. package/dist/index.js +11369 -0
  8. package/dist/index.js.map +6 -0
  9. package/dist/map-style.cjs +445 -0
  10. package/dist/map-style.cjs.map +6 -0
  11. package/dist/map-style.js +431 -0
  12. package/dist/map-style.js.map +6 -0
  13. package/dist/types/arabic-shaping.d.ts +7 -0
  14. package/dist/types/atmosphere-layer.d.ts +10 -0
  15. package/dist/types/basemap-layer.d.ts +150 -0
  16. package/dist/types/bidi.d.ts +8 -0
  17. package/dist/types/circle-layer.d.ts +49 -0
  18. package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
  19. package/dist/types/fill-outline-extension.d.ts +22 -0
  20. package/dist/types/fill-pattern.d.ts +99 -0
  21. package/dist/types/fill-style-extensions.d.ts +100 -0
  22. package/dist/types/geojson-source.d.ts +29 -0
  23. package/dist/types/geojson-tiler.d.ts +48 -0
  24. package/dist/types/geojson-worker.d.ts +40 -0
  25. package/dist/types/globe-horizon-extension.d.ts +61 -0
  26. package/dist/types/globe-layers.d.ts +239 -0
  27. package/dist/types/globe-line-angle-extension.d.ts +37 -0
  28. package/dist/types/glyph-font-renderer.d.ts +37 -0
  29. package/dist/types/glyph-pbf.d.ts +37 -0
  30. package/dist/types/glyph-store.d.ts +103 -0
  31. package/dist/types/grid-index.d.ts +47 -0
  32. package/dist/types/hillshade-layer.d.ts +154 -0
  33. package/dist/types/index.d.ts +9 -0
  34. package/dist/types/is-globe-viewport.d.ts +2 -0
  35. package/dist/types/label-font-renderer.d.ts +18 -0
  36. package/dist/types/label-placement.d.ts +247 -0
  37. package/dist/types/label-text-layer.d.ts +231 -0
  38. package/dist/types/line-blur-extension.d.ts +73 -0
  39. package/dist/types/line-glyphs.d.ts +60 -0
  40. package/dist/types/line-placement.d.ts +93 -0
  41. package/dist/types/map-style-loader.d.ts +48 -0
  42. package/dist/types/map-style-schema.d.ts +72 -0
  43. package/dist/types/map-style.d.ts +6 -0
  44. package/dist/types/mapbox-style.d.ts +40 -0
  45. package/dist/types/mvt-label-layer.d.ts +923 -0
  46. package/dist/types/polylabel.d.ts +17 -0
  47. package/dist/types/raster-color-extension.d.ts +146 -0
  48. package/dist/types/shared-texture.d.ts +34 -0
  49. package/dist/types/sort-key.d.ts +10 -0
  50. package/dist/types/sprite.d.ts +82 -0
  51. package/dist/types/style-accessor.d.ts +45 -0
  52. package/dist/types/style-expression.d.ts +83 -0
  53. package/dist/types/style-resolver.d.ts +94 -0
  54. package/dist/types/text-font.d.ts +36 -0
  55. package/dist/types/text-halo-blur-extension.d.ts +55 -0
  56. package/dist/types/text-layout.d.ts +71 -0
  57. package/dist/types/util.d.ts +22 -0
  58. package/dist/types/zoom-paint-extension.d.ts +72 -0
  59. package/package.json +80 -3
  60. package/src/arabic-shaping.ts +366 -0
  61. package/src/atmosphere-layer.ts +19 -0
  62. package/src/basemap-layer.ts +533 -0
  63. package/src/bidi.ts +199 -0
  64. package/src/circle-layer.ts +191 -0
  65. package/src/fill-extrusion-lighting-extension.ts +206 -0
  66. package/src/fill-outline-extension.ts +70 -0
  67. package/src/fill-pattern.ts +400 -0
  68. package/src/fill-style-extensions.ts +177 -0
  69. package/src/geojson-source.ts +318 -0
  70. package/src/geojson-tiler.ts +185 -0
  71. package/src/geojson-worker.ts +98 -0
  72. package/src/globe-horizon-extension.ts +64 -0
  73. package/src/globe-layers.ts +3487 -0
  74. package/src/globe-line-angle-extension.ts +131 -0
  75. package/src/glyph-font-renderer.ts +173 -0
  76. package/src/glyph-pbf.ts +164 -0
  77. package/src/glyph-store.ts +326 -0
  78. package/src/grid-index.ts +177 -0
  79. package/src/hillshade-layer.ts +775 -0
  80. package/src/index.ts +18 -0
  81. package/src/is-globe-viewport.ts +24 -0
  82. package/src/label-font-renderer.ts +159 -0
  83. package/src/label-placement.ts +727 -0
  84. package/src/label-text-layer.ts +913 -0
  85. package/src/line-blur-extension.ts +237 -0
  86. package/src/line-glyphs.ts +364 -0
  87. package/src/line-placement.ts +446 -0
  88. package/src/map-style-loader.ts +56 -0
  89. package/src/map-style-schema.ts +83 -0
  90. package/src/map-style.ts +23 -0
  91. package/src/mapbox-style.ts +168 -0
  92. package/src/mvt-label-layer.ts +3744 -0
  93. package/src/polylabel.ts +312 -0
  94. package/src/raster-color-extension.ts +245 -0
  95. package/src/shared-texture.ts +153 -0
  96. package/src/sort-key.ts +63 -0
  97. package/src/sprite.ts +244 -0
  98. package/src/style-accessor.ts +95 -0
  99. package/src/style-expression.ts +327 -0
  100. package/src/style-resolver.ts +301 -0
  101. package/src/text-font.ts +150 -0
  102. package/src/text-halo-blur-extension.ts +115 -0
  103. package/src/text-layout.ts +441 -0
  104. package/src/util.ts +47 -0
  105. package/src/zoom-paint-extension.ts +207 -0
@@ -0,0 +1,727 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ import {LayerExtension} from '@deck.gl/core';
10
+ import type {Layer, Viewport} from '@deck.gl/core';
11
+ import type {Device, Texture} from '@luma.gl/core';
12
+ import {GridIndex, type OverlapMode} from './grid-index';
13
+
14
+ /**
15
+ * Pixels around the viewport in which symbols are placed, so that a symbol just outside it still
16
+ * hides those it would overlap when it comes in: MapLibre GL JS's `viewportPadding`.
17
+ */
18
+ export const PLACEMENT_VIEWPORT_PADDING = 100;
19
+
20
+ /** The grid's cell size in pixels, as MapLibre GL JS's collision index. */
21
+ const PLACEMENT_CELL_SIZE = 25;
22
+
23
+ /**
24
+ * Milliseconds between two placements while the view keeps changing, as MapLibre GL JS's default
25
+ * `fadeDuration`. The view the map comes to rest in is always placed.
26
+ */
27
+ export const DEFAULT_PLACEMENT_INTERVAL = 300;
28
+
29
+ /**
30
+ * A symbol part's collision shape in grid pixels (screen pixels moved by
31
+ * `PLACEMENT_VIEWPORT_PADDING`): a box `[x1, y1, x2, y2]`, or circles `[x, y, radius, ...]` for
32
+ * text along a line.
33
+ */
34
+ export type PlacementShape =
35
+ | {box: number[]; circles?: undefined}
36
+ | {circles: number[]; box?: undefined};
37
+
38
+ /** One symbol to place: the slot its visibility is written to, and its parts' shapes. */
39
+ export type PlacementSymbol = {
40
+ slot: number;
41
+ /** `null` when the symbol draws no text. */
42
+ text: PlacementShape | null;
43
+ /** `null` when the symbol draws no icon. */
44
+ icon: PlacementShape | null;
45
+ /** Hidden whatever its collisions: beyond a globe's horizon. */
46
+ occluded?: boolean;
47
+ /**
48
+ * Its icon is still loading. Unless the icon is optional, the symbol is not placed until it has
49
+ * loaded, as MapLibre GL JS lays out no symbol until its images have loaded: its text, placed
50
+ * alone, would hide when the icon arrives and collides.
51
+ */
52
+ iconPending?: boolean;
53
+ /**
54
+ * With `text-variable-anchor`: the text's shape at each of its anchors, in the order they are
55
+ * tried, and the slot whose text is drawn at each. `text` is the first one's shape, and `key`
56
+ * names the symbol across placements, so that it keeps the anchor it was placed at while that
57
+ * still fits, as in MapLibre GL JS.
58
+ */
59
+ textAnchors?: {shapes: PlacementShape[]; slots: number[]; key: object};
60
+ };
61
+
62
+ /** A symbol style layer's placement rules, from its layout, as MapLibre GL JS reads them. */
63
+ export type SymbolPlacementRules = {
64
+ textOverlap: OverlapMode;
65
+ iconOverlap: OverlapMode;
66
+ textIgnorePlacement: boolean;
67
+ iconIgnorePlacement: boolean;
68
+ textOptional: boolean;
69
+ iconOptional: boolean;
70
+ /** Whether the layer draws any text, and any icon (MapLibre's `hasTextData`/`hasIconData`). */
71
+ hasTextData: boolean;
72
+ hasIconData: boolean;
73
+ };
74
+
75
+ /** Visibility bits of a placed symbol. */
76
+ export const TEXT_VISIBLE = 1;
77
+ export const ICON_VISIBLE = 2;
78
+ /** `placeSymbol` returns the index of the anchor its text is placed at above the visibility bits. */
79
+ export const ANCHOR_SHIFT = 2;
80
+
81
+ function isPlaceable(grid: GridIndex, shape: PlacementShape, mode: OverlapMode): boolean {
82
+ if (shape.box) {
83
+ const [x1, y1, x2, y2] = shape.box;
84
+ return (
85
+ grid.isInside(x1, y1, x2, y2) && (mode === 'always' || !grid.hitTest(x1, y1, x2, y2, mode))
86
+ );
87
+ }
88
+ const {circles} = shape;
89
+ let inside = false;
90
+ for (let i = 0; i < circles.length; i += 3) {
91
+ const [x, y, r] = [circles[i], circles[i + 1], circles[i + 2]];
92
+ inside ||= grid.isInside(x - r, y - r, x + r, y + r);
93
+ if (mode !== 'always' && grid.hitTestCircle(x, y, r, mode)) {
94
+ return false;
95
+ }
96
+ }
97
+ return inside;
98
+ }
99
+
100
+ function insertShape(grid: GridIndex, shape: PlacementShape, mode: OverlapMode): void {
101
+ if (shape.box) {
102
+ const [x1, y1, x2, y2] = shape.box;
103
+ grid.insert(x1, y1, x2, y2, mode);
104
+ return;
105
+ }
106
+ const {circles} = shape;
107
+ for (let i = 0; i < circles.length; i += 3) {
108
+ grid.insertCircle(circles[i], circles[i + 1], circles[i + 2], mode);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * The anchor that text with `text-variable-anchor` is placed at, as MapLibre GL JS's
114
+ * `placeBoxForVariableAnchors`: the anchor it was placed at before while it still fits, else the
115
+ * first anchor where it overlaps nothing, else, when the text may overlap, the first anchor where
116
+ * it may be placed so. Returns the anchor's index, or -1 when it fits at none.
117
+ */
118
+ function getTextAnchor(
119
+ grid: GridIndex,
120
+ shapes: PlacementShape[],
121
+ overlap: OverlapMode,
122
+ previous: number
123
+ ): number {
124
+ if (previous >= 0 && previous < shapes.length && isPlaceable(grid, shapes[previous], 'never')) {
125
+ return previous;
126
+ }
127
+ const modes: OverlapMode[] = overlap === 'never' ? ['never'] : ['never', overlap];
128
+ for (const mode of modes) {
129
+ for (let index = 0; index < shapes.length; index++) {
130
+ if (isPlaceable(grid, shapes[index], mode)) {
131
+ return index;
132
+ }
133
+ }
134
+ }
135
+ return -1;
136
+ }
137
+
138
+ /**
139
+ * Places one symbol in `grid`, as MapLibre GL JS's `Placement.placeLayerBucketPart` places a
140
+ * symbol instance: each part is placeable where it lies in the grid and overlaps nothing it may not
141
+ * overlap; a part that is not optional is placed only with the other part; placed parts are added
142
+ * to the grid unless they ignore placement. Text with variable anchors is placed at the first of
143
+ * them where it fits (see `getTextAnchor`); its icon stays where it is. Returns the visibility bits
144
+ * (`TEXT_VISIBLE`, `ICON_VISIBLE`), and the index of the text's anchor shifted by `ANCHOR_SHIFT`.
145
+ * A part that always overlaps, of a symbol whose other part does too or is optional or absent, is
146
+ * shown even outside the grid (MapLibre's `alwaysShowText`/`Icon`).
147
+ */
148
+ export function placeSymbol(
149
+ grid: GridIndex,
150
+ symbol: PlacementSymbol,
151
+ rules: SymbolPlacementRules,
152
+ previousAnchor = -1
153
+ ): number {
154
+ // Beyond the horizon, or waiting for its icon: neither drawn nor in the way of the symbols placed
155
+ // after it.
156
+ if (symbol.occluded || (symbol.iconPending && !rules.iconOptional)) {
157
+ return 0;
158
+ }
159
+ const {icon, textAnchors} = symbol;
160
+ let {text} = symbol;
161
+ let anchor = 0;
162
+ let placeText: boolean;
163
+ if (text && textAnchors && textAnchors.shapes.length > 1) {
164
+ const index = getTextAnchor(grid, textAnchors.shapes, rules.textOverlap, previousAnchor);
165
+ placeText = index >= 0;
166
+ anchor = Math.max(index, 0);
167
+ text = textAnchors.shapes[anchor];
168
+ } else {
169
+ placeText = Boolean(text) && isPlaceable(grid, text!, rules.textOverlap);
170
+ }
171
+ let placeIcon = Boolean(icon) && isPlaceable(grid, icon!, rules.iconOverlap);
172
+
173
+ const iconWithoutText = rules.textOptional || !text;
174
+ const textWithoutIcon = rules.iconOptional || !icon;
175
+ if (!iconWithoutText && !textWithoutIcon) {
176
+ placeIcon = placeText = placeIcon && placeText;
177
+ } else if (!textWithoutIcon) {
178
+ placeText = placeIcon && placeText;
179
+ } else if (!iconWithoutText) {
180
+ placeIcon = placeIcon && placeText;
181
+ }
182
+
183
+ if (placeText && !rules.textIgnorePlacement) {
184
+ insertShape(grid, text!, rules.textOverlap);
185
+ }
186
+ if (placeIcon && !rules.iconIgnorePlacement) {
187
+ insertShape(grid, icon!, rules.iconOverlap);
188
+ }
189
+
190
+ const textAlways = rules.textOverlap === 'always';
191
+ const iconAlways = rules.iconOverlap === 'always';
192
+ const alwaysShowText = textAlways && (iconAlways || !rules.hasIconData || rules.iconOptional);
193
+ const alwaysShowIcon = iconAlways && (textAlways || !rules.hasTextData || rules.textOptional);
194
+ return (
195
+ (placeText || (alwaysShowText && Boolean(text)) ? TEXT_VISIBLE : 0) |
196
+ (placeIcon || (alwaysShowIcon && Boolean(icon)) ? ICON_VISIBLE : 0) |
197
+ (anchor << ANCHOR_SHIFT)
198
+ );
199
+ }
200
+
201
+ /** A label layer that takes part in placement (`MVTLabelLayer`). */
202
+ export interface PlacementLayer {
203
+ /**
204
+ * The layer's place in the style: layers with a higher order are placed first, as MapLibre GL
205
+ * JS places style layers from the top down.
206
+ */
207
+ getPlacementOrder(): number;
208
+ /** A value that changes whenever what the layer would place changes, other than the view. */
209
+ getPlacementRevision(): number;
210
+ /**
211
+ * A value that changes whenever the layer's symbols take other slots from its slot base, as when
212
+ * its rows change: the placement made before then holds the visibility of other symbols in
213
+ * those slots, or of none, so the layer is placed again at once, whether or not the view moves.
214
+ */
215
+ getPlacementSlotRevision(): number;
216
+ /** The number of slots the layer's symbols use, `0..count-1` from its slot base. */
217
+ getPlacementSlotCount(): number;
218
+ /**
219
+ * Whether the layer is drawn in `viewport`: a hidden layer, or one of another view, has no say
220
+ * in what is placed there.
221
+ */
222
+ isDrawnIn(viewport: Viewport): boolean;
223
+ /** Appends the layer's symbols in its placement order, for `viewport`, to `out`. */
224
+ getPlacementSymbols(
225
+ viewport: Viewport,
226
+ slotBase: number,
227
+ out: PlacementSymbol[]
228
+ ): SymbolPlacementRules | null;
229
+ /** Requests another frame, while a placement waits for the throttle. */
230
+ setNeedsRedraw(): void;
231
+ /**
232
+ * The deck layer the layer is drawn under (its topmost parent, or itself), and the deck's layers
233
+ * in draw order. Of two layers under different deck layers, the one under the later deck layer
234
+ * is placed first, as MapLibre GL JS places the layers drawn over others first; the placement
235
+ * order decides only between layers under the same deck layer. Without it, the layer is placed
236
+ * by its placement order alone, after the layers that give one. Deck layers are told apart by
237
+ * their ids, which deck.gl keeps when it makes a new instance of a layer.
238
+ */
239
+ getPlacementRoot?(): {root: {id: string}; layers: readonly PlacementDeckLayer[]} | null;
240
+ }
241
+
242
+ /** What placement reads of a deck's layers (see `PlacementLayer.getPlacementRoot`). */
243
+ export type PlacementDeckLayer = {id: string; parent?: unknown};
244
+
245
+ /** What a placement reads of a registered layer at a frame, for one viewport. */
246
+ type PlacementLayerState = {
247
+ layer: PlacementLayer;
248
+ base: number;
249
+ /** Whether the layer is drawn in the viewport. */
250
+ drawn: boolean;
251
+ /** The index of its deck layer among the deck's layers, or -1 (see `getPlacementRoot`). */
252
+ rootIndex: number;
253
+ };
254
+
255
+ /** The placement of one viewport (one view of the deck). */
256
+ type ViewPlacement = {
257
+ /** Four bytes per slot (an RGBA texel): text, then icon visibility (0 or 255). */
258
+ values: Uint8Array;
259
+ texture: Texture | null;
260
+ /** Whether `values` changed since they were written to the texture. */
261
+ dirty: boolean;
262
+ viewport: Viewport | null;
263
+ revision: string;
264
+ /** The slots the placement was made for (see `getSlotLayout`). */
265
+ slotLayout: string;
266
+ placedAt: number;
267
+ /** The frame being drawn, and the viewports of it and of the frame before. */
268
+ frame: number;
269
+ frameViewport: Viewport | null;
270
+ lastViewport: Viewport | null;
271
+ /** The anchor each symbol with variable anchors had its text placed at (see `textAnchors`). */
272
+ anchors: WeakMap<object, number>;
273
+ };
274
+
275
+ /** The width of the visibility textures, in slots. */
276
+ const TEXTURE_WIDTH = 1024;
277
+
278
+ /**
279
+ * Places the symbols of every registered label layer together, as MapLibre GL JS's `Placement`:
280
+ * greedily, style layers from the top down, each against the symbols placed before it in a grid
281
+ * over the viewport. Each symbol's text and icon visibility is written to a texture that the label
282
+ * sublayers read (`LabelPlacementExtension`), one per viewport id. Placement runs from the label
283
+ * sublayers' draw: at most every `interval` ms while the view keeps moving, and at once when the
284
+ * view is at rest and the labels or the view changed since the last placement.
285
+ */
286
+ export class LabelPlacement {
287
+ interval = DEFAULT_PLACEMENT_INTERVAL;
288
+ /** Each layer's slots, and the allocation that gave them (see `getSlotLayout`). */
289
+ private readonly layers = new Map<
290
+ PlacementLayer,
291
+ {base: number; count: number; allocation: number}
292
+ >();
293
+ private readonly views = new Map<string, ViewPlacement>();
294
+ private capacity = 0;
295
+ /** Counts the slot allocations made. */
296
+ private allocations = 0;
297
+ /** Changes when a layer joins, leaves or moves its slots. */
298
+ private version = 0;
299
+ private readonly symbols: PlacementSymbol[] = [];
300
+ /** Each layer's state at the frame being placed, in registration order (see `getRevision`). */
301
+ private readonly states: PlacementLayerState[] = [];
302
+ /** The index of each top-level deck layer, by id, in the deck's layers last read. */
303
+ private rootIndices: {layers: readonly PlacementDeckLayer[] | null; byId: Map<string, number>} = {
304
+ layers: null,
305
+ byId: new Map()
306
+ };
307
+
308
+ /**
309
+ * Adds a layer, or updates its slot count; returns its slot base. A layer that needs more slots
310
+ * than it has takes the first free ones that are enough, and gives up its own. No other layer
311
+ * moves: each draws from the slots it read at its last update, until it updates again.
312
+ */
313
+ register(layer: PlacementLayer): number {
314
+ const count = layer.getPlacementSlotCount();
315
+ const entry = this.layers.get(layer);
316
+ if (entry && entry.count >= count) {
317
+ entry.count = count;
318
+ return entry.base;
319
+ }
320
+ const base = this.allocate(count, layer);
321
+ const allocation = ++this.allocations;
322
+ if (entry) {
323
+ // Updated in place, so that the layer keeps its place among layers of the same order.
324
+ entry.base = base;
325
+ entry.count = count;
326
+ entry.allocation = allocation;
327
+ } else {
328
+ this.layers.set(layer, {base, count, allocation});
329
+ }
330
+ this.version++;
331
+ return base;
332
+ }
333
+
334
+ unregister(layer: PlacementLayer): void {
335
+ if (this.layers.delete(layer)) {
336
+ this.version++;
337
+ }
338
+ }
339
+
340
+ /** The slot base of a registered layer, or -1. */
341
+ getSlotBase(layer: PlacementLayer): number {
342
+ return this.layers.get(layer)?.base ?? -1;
343
+ }
344
+
345
+ /**
346
+ * The first `count` slots that no layer but `growing` uses, past the last layer's slots when no
347
+ * gap between them is enough. The textures grow to hold them, keeping every slot's visibility.
348
+ */
349
+ private allocate(count: number, growing: PlacementLayer): number {
350
+ const used = [...this.layers]
351
+ .filter(([layer]) => layer !== growing)
352
+ .map(([, {base, count: length}]) => [base, base + length])
353
+ .sort((a, b) => a[0] - b[0]);
354
+ let base = 0;
355
+ for (const [start, end] of used) {
356
+ if (start - base >= count) {
357
+ break;
358
+ }
359
+ base = Math.max(base, end);
360
+ }
361
+ let capacity = Math.max(this.capacity, TEXTURE_WIDTH);
362
+ while (capacity < base + count) {
363
+ capacity *= 2;
364
+ }
365
+ if (capacity !== this.capacity) {
366
+ this.capacity = capacity;
367
+ for (const view of this.views.values()) {
368
+ const values = new Uint8Array(capacity * 4);
369
+ values.set(view.values);
370
+ view.values = values;
371
+ view.dirty = true;
372
+ }
373
+ }
374
+ return base;
375
+ }
376
+
377
+ private getView(viewport: Viewport): ViewPlacement {
378
+ let view = this.views.get(viewport.id);
379
+ if (!view) {
380
+ view = {
381
+ values: new Uint8Array(Math.max(this.capacity, TEXTURE_WIDTH) * 4),
382
+ texture: null,
383
+ dirty: true,
384
+ viewport: null,
385
+ revision: '',
386
+ slotLayout: '',
387
+ placedAt: Number.NEGATIVE_INFINITY,
388
+ frame: Number.NaN,
389
+ frameViewport: null,
390
+ lastViewport: null,
391
+ anchors: new WeakMap()
392
+ };
393
+ this.views.set(viewport.id, view);
394
+ }
395
+ return view;
396
+ }
397
+
398
+ /**
399
+ * The visibility texture for `viewport`, placing first when a placement is due. `frame` names the
400
+ * frame being drawn (deck.gl's timeline time; each call is a frame of its own without it): the
401
+ * label sublayers of a frame all draw with the placement decided at its first one. Picking passes
402
+ * read the last placement and never place.
403
+ */
404
+ prepare(
405
+ device: Device,
406
+ viewport: Viewport,
407
+ isPicking: boolean,
408
+ now: number,
409
+ frame: number = now
410
+ ): Texture {
411
+ const view = this.getView(viewport);
412
+ if (!isPicking && frame !== view.frame) {
413
+ view.frame = frame;
414
+ view.lastViewport = view.frameViewport;
415
+ view.frameViewport = viewport;
416
+ const revision = this.getRevision(viewport);
417
+ const slotLayout = this.getSlotLayout();
418
+ const changed =
419
+ revision !== view.revision || !view.viewport || !view.viewport.equals(viewport);
420
+ // A view at rest (the same as at the frame before) is placed at once; a moving one at most
421
+ // every `interval`, as MapLibre places while the map moves. Symbols in other slots than at
422
+ // the last placement are placed at once, whether the view moves or not: their slots hold
423
+ // the visibility of other symbols, or of none.
424
+ const resting = Boolean(view.lastViewport?.equals(viewport));
425
+ if (changed) {
426
+ if (resting || slotLayout !== view.slotLayout || now - view.placedAt >= this.interval) {
427
+ this.place(view, viewport, revision, slotLayout, now);
428
+ } else {
429
+ // Another frame, so that the view is placed once it rests.
430
+ for (const layer of this.layers.keys()) {
431
+ layer.setNeedsRedraw();
432
+ break;
433
+ }
434
+ }
435
+ }
436
+ }
437
+ return this.getTexture(device, view);
438
+ }
439
+
440
+ /**
441
+ * A value that changes whenever what would be placed in `viewport` changes, other than the view:
442
+ * a layer joins or leaves, its symbols change, it is drawn in the viewport or no longer is, or
443
+ * its deck layer moves among the deck's layers. Reads each layer's state for `place`.
444
+ */
445
+ private getRevision(viewport: Viewport): string {
446
+ let revision = `${this.version}`;
447
+ const states = this.states;
448
+ states.length = 0;
449
+ for (const [layer, {base}] of this.layers) {
450
+ const drawn = layer.isDrawnIn(viewport);
451
+ const rootIndex = this.getRootIndex(layer);
452
+ states.push({layer, base, drawn, rootIndex});
453
+ revision += `|${layer.getPlacementRevision()}:${drawn ? rootIndex : 'hidden'}`;
454
+ }
455
+ return revision;
456
+ }
457
+
458
+ /** The index of a layer's deck layer among the deck's layers, or -1. */
459
+ private getRootIndex(layer: PlacementLayer): number {
460
+ const placementRoot = layer.getPlacementRoot?.();
461
+ if (!placementRoot) {
462
+ return -1;
463
+ }
464
+ // The deck's layers are indexed again only when deck.gl gives it new ones.
465
+ if (placementRoot.layers !== this.rootIndices.layers) {
466
+ const byId = new Map<string, number>();
467
+ placementRoot.layers.forEach((deckLayer, index) => {
468
+ if (!deckLayer.parent) {
469
+ byId.set(deckLayer.id, index);
470
+ }
471
+ });
472
+ this.rootIndices = {layers: placementRoot.layers, byId};
473
+ }
474
+ return this.rootIndices.byId.get(placementRoot.root.id) ?? -1;
475
+ }
476
+
477
+ /**
478
+ * A value that changes whenever a layer's symbols take other slots: the layer is given new ones,
479
+ * by an allocation of its own (a layer that takes the slots another left has another), or its
480
+ * rows change.
481
+ */
482
+ private getSlotLayout(): string {
483
+ let layout = '';
484
+ for (const [layer, {allocation}] of this.layers) {
485
+ layout += `|${allocation}:${layer.getPlacementSlotRevision()}`;
486
+ }
487
+ return layout;
488
+ }
489
+
490
+ private getTexture(device: Device, view: ViewPlacement): Texture {
491
+ const height = view.values.length / 4 / TEXTURE_WIDTH;
492
+ if (view.texture?.device !== device || view.texture.height !== height) {
493
+ view.texture?.destroy();
494
+ view.texture = device.createTexture({
495
+ format: 'rgba8unorm',
496
+ width: TEXTURE_WIDTH,
497
+ height,
498
+ mipLevels: 1,
499
+ sampler: {minFilter: 'nearest', magFilter: 'nearest'}
500
+ });
501
+ view.dirty = true;
502
+ }
503
+ if (view.dirty) {
504
+ view.texture.writeData(view.values);
505
+ view.dirty = false;
506
+ }
507
+ return view.texture;
508
+ }
509
+
510
+ /** Places every registered layer's symbols for `viewport`. */
511
+ private place(
512
+ view: ViewPlacement,
513
+ viewport: Viewport,
514
+ revision: string,
515
+ slotLayout: string,
516
+ now: number
517
+ ): void {
518
+ const padding = PLACEMENT_VIEWPORT_PADDING;
519
+ const grid = new GridIndex(
520
+ viewport.width + 2 * padding,
521
+ viewport.height + 2 * padding,
522
+ PLACEMENT_CELL_SIZE
523
+ );
524
+ view.values.fill(0);
525
+ const symbols = this.symbols;
526
+ const previousAnchors = view.anchors;
527
+ const anchors = new WeakMap<object, number>();
528
+ for (const {layer, base} of this.getPlacementOrder()) {
529
+ symbols.length = 0;
530
+ const rules = layer.getPlacementSymbols(viewport, base, symbols);
531
+ if (!rules) {
532
+ continue;
533
+ }
534
+ for (const symbol of symbols) {
535
+ const {textAnchors} = symbol;
536
+ const previous = textAnchors ? (previousAnchors.get(textAnchors.key) ?? -1) : -1;
537
+ const bits = placeSymbol(grid, symbol, rules, previous);
538
+ // Text with variable anchors is drawn from the slot of the anchor it is placed at; the
539
+ // slots of its other anchors stay hidden.
540
+ const anchor = bits >> ANCHOR_SHIFT;
541
+ const textSlot = textAnchors ? textAnchors.slots[anchor] : symbol.slot;
542
+ view.values[textSlot * 4] = bits & TEXT_VISIBLE ? 255 : 0;
543
+ view.values[symbol.slot * 4 + 1] = bits & ICON_VISIBLE ? 255 : 0;
544
+ if (textAnchors && bits & TEXT_VISIBLE) {
545
+ anchors.set(textAnchors.key, anchor);
546
+ }
547
+ }
548
+ }
549
+ view.anchors = anchors;
550
+ symbols.length = 0;
551
+ view.viewport = viewport;
552
+ view.revision = revision;
553
+ view.slotLayout = slotLayout;
554
+ view.placedAt = now;
555
+ view.dirty = true;
556
+ }
557
+
558
+ /**
559
+ * The layers drawn in the viewport of the last `getRevision`, in the order they are placed: under
560
+ * the later deck layer first, then by placement order, highest first (see
561
+ * `PlacementLayer.getPlacementRoot`). Layers that tie keep the order they registered in.
562
+ */
563
+ private getPlacementOrder(): PlacementLayerState[] {
564
+ return this.states
565
+ .filter(state => state.drawn)
566
+ .map(state => ({state, order: state.layer.getPlacementOrder()}))
567
+ .sort((a, b) => b.state.rootIndex - a.state.rootIndex || b.order - a.order)
568
+ .map(({state}) => state);
569
+ }
570
+
571
+ /** Releases the textures. */
572
+ destroy(): void {
573
+ for (const view of this.views.values()) {
574
+ view.texture?.destroy();
575
+ }
576
+ this.views.clear();
577
+ this.states.length = 0;
578
+ this.rootIndices = {layers: null, byId: new Map()};
579
+ }
580
+ }
581
+
582
+ /** The placement shared by the label layers of each deck, and how many users it has. */
583
+ const sharedPlacements = new WeakMap<object, {placement: LabelPlacement; users: number}>();
584
+
585
+ /**
586
+ * Takes a use of the placement shared by the label layers of the deck that `key` names (its layer
587
+ * manager), made at the first use. Each use is given back with `releaseSharedLabelPlacement`.
588
+ */
589
+ export function acquireSharedLabelPlacement(key: object): LabelPlacement {
590
+ let shared = sharedPlacements.get(key);
591
+ if (!shared) {
592
+ shared = {placement: new LabelPlacement(), users: 0};
593
+ sharedPlacements.set(key, shared);
594
+ }
595
+ shared.users++;
596
+ return shared.placement;
597
+ }
598
+
599
+ /** Gives back a use of a deck's shared placement, which is released with its last use. */
600
+ export function releaseSharedLabelPlacement(key: object | null | undefined): void {
601
+ const shared = key && sharedPlacements.get(key);
602
+ if (shared && --shared.users === 0) {
603
+ shared.placement.destroy();
604
+ sharedPlacements.delete(key);
605
+ }
606
+ }
607
+
608
+ /** The key of the shared placement of a layer's deck: its layer manager. */
609
+ export function getSharedLabelPlacementKey(layer: {context?: unknown}): object {
610
+ const context = layer.context as {layerManager?: object} | undefined;
611
+ return context?.layerManager || context || layer;
612
+ }
613
+
614
+ /** Props that `LabelPlacementExtension` adds to a label sublayer. */
615
+ export type LabelPlacementExtensionProps = {
616
+ /** The placement that decides which symbols are drawn; all are drawn without it. */
617
+ labelPlacement?: LabelPlacement | null;
618
+ /** The slot of each object; a negative slot is always drawn. */
619
+ getPlacementSlot?: (object: any, info: {index: number}) => number;
620
+ /** Which visibility the sublayer draws by: its symbols' text (0) or icons (1). */
621
+ placementPart?: 0 | 1;
622
+ };
623
+
624
+ const placementModule = {
625
+ name: 'labelPlacement',
626
+ vs: /* glsl */ `\
627
+ uniform highp sampler2D labelPlacementTexture;
628
+ layout(std140) uniform labelPlacementUniforms {
629
+ float part;
630
+ } labelPlacement;
631
+ `,
632
+ uniformTypes: {part: 'f32'}
633
+ } as const;
634
+
635
+ const PLACEMENT_INJECTION = {
636
+ 'vs:#decl': /* glsl */ `
637
+ in float instancePlacementSlots;
638
+ `,
639
+ 'vs:DECKGL_FILTER_GL_POSITION': /* glsl */ `
640
+ if (instancePlacementSlots >= 0.0) {
641
+ int labelPlacement_slot = int(instancePlacementSlots + 0.5);
642
+ vec2 labelPlacement_value = texelFetch(
643
+ labelPlacementTexture,
644
+ ivec2(labelPlacement_slot % ${TEXTURE_WIDTH}, labelPlacement_slot / ${TEXTURE_WIDTH}),
645
+ 0
646
+ ).rg;
647
+ float labelPlacement_visible = labelPlacement.part < 0.5
648
+ ? labelPlacement_value.r
649
+ : labelPlacement_value.g;
650
+ if (labelPlacement_visible < 0.5) {
651
+ // Every vertex of the quad lands beyond the far plane, so it is clipped.
652
+ position = vec4(0.0, 0.0, 2.0, 1.0);
653
+ }
654
+ }
655
+ `
656
+ };
657
+
658
+ /** The texture last handed to each model's shaders. */
659
+ const boundTextures = new WeakMap<object, Texture>();
660
+
661
+ /**
662
+ * Draws a label sublayer's objects only where `labelPlacement` placed their part (text or icon):
663
+ * each object's slot is an attribute (`getPlacementSlot`), and the vertex shader reads its
664
+ * visibility from the placement's texture for the viewport being drawn. Added to a composite layer
665
+ * such as `TextLayer`, it reaches the character and background sublayers, so a hidden label is
666
+ * neither drawn nor picked. WebGL only.
667
+ */
668
+ export class LabelPlacementExtension extends LayerExtension {
669
+ static extensionName = 'LabelPlacementExtension';
670
+ static defaultProps = {
671
+ labelPlacement: {type: 'object', value: null, compare: false},
672
+ getPlacementSlot: {type: 'accessor', value: -1},
673
+ placementPart: 0
674
+ };
675
+
676
+ isEnabled(layer: Layer): boolean {
677
+ return layer.context.device.type !== 'webgpu' && layer.getAttributeManager() !== null;
678
+ }
679
+
680
+ getShaders(this: Layer<LabelPlacementExtensionProps>, extension: this): any {
681
+ if (!extension.isEnabled(this)) {
682
+ return null;
683
+ }
684
+ return {modules: [placementModule], inject: PLACEMENT_INJECTION};
685
+ }
686
+
687
+ initializeState(this: Layer<LabelPlacementExtensionProps>, context: any, extension: this) {
688
+ if (!extension.isEnabled(this)) {
689
+ return;
690
+ }
691
+ this.getAttributeManager()!.add({
692
+ instancePlacementSlots: {
693
+ size: 1,
694
+ stepMode: 'dynamic',
695
+ accessor: 'getPlacementSlot',
696
+ defaultValue: -1
697
+ }
698
+ });
699
+ }
700
+
701
+ updateState(this: Layer<LabelPlacementExtensionProps>, params: any, extension: this) {
702
+ if (!extension.isEnabled(this)) {
703
+ return;
704
+ }
705
+ this.setShaderModuleProps({labelPlacement: {part: this.props.placementPart || 0}} as any);
706
+ }
707
+
708
+ draw(this: Layer<LabelPlacementExtensionProps>, params: any, extension: this) {
709
+ const {labelPlacement} = this.props;
710
+ const {device, viewport} = this.context;
711
+ if (!labelPlacement || !viewport || !extension.isEnabled(this)) {
712
+ return;
713
+ }
714
+ const isPicking = Boolean(params?.shaderModuleProps?.picking?.isActive);
715
+ const now = performance.now();
716
+ // The timeline's time is set once per frame by deck.gl's animation loop.
717
+ const frame = this.context.timeline?.getTime();
718
+ const texture = labelPlacement.prepare(device, viewport, isPicking, now, frame ?? now);
719
+ const models = this.getModels();
720
+ if (models.some(model => boundTextures.get(model) !== texture)) {
721
+ this.setShaderModuleProps({labelPlacement: {labelPlacementTexture: texture}} as any);
722
+ for (const model of models) {
723
+ boundTextures.set(model, texture);
724
+ }
725
+ }
726
+ }
727
+ }