@tangram.gl/basemap-layers 0.0.0-stage → 0.0.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 (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,3487 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {GlyphStore} from './glyph-store';
6
+ import {SharedMVTLayer, SharedTileset2D} from '@deck.gl-community/geo-layers';
7
+ import {isGlobeViewport} from './is-globe-viewport';
8
+ import {
9
+ COORDINATE_SYSTEM,
10
+ type Layer,
11
+ type LayersList,
12
+ type UpdateParameters,
13
+ log
14
+ } from '@deck.gl/core';
15
+ import {PathStyleExtension} from '@deck.gl/extensions';
16
+ import {TileLayer} from '@deck.gl/geo-layers';
17
+ import {BitmapLayer, GeoJsonLayer, SolidPolygonLayer} from '@deck.gl/layers';
18
+ import {getGlobeAtmosphereLayer, getGlobeAtmosphereSkyLayer} from './atmosphere-layer';
19
+ import {createCircleSubLayer} from './circle-layer';
20
+ import {sortFeaturesByKey} from './sort-key';
21
+ import {getGeoJsonMaxZoom, getGeoJsonTileFeatures, isGeoJsonSource} from './geojson-source';
22
+ import {
23
+ type FillPatternGroup,
24
+ getFeaturesWithoutPattern,
25
+ getFillPatternGroups,
26
+ getFillPatternProps
27
+ } from './fill-pattern';
28
+ import {FillPatternExtension, FillTranslateExtension} from './fill-style-extensions';
29
+ import {FillOutlineExtension} from './fill-outline-extension';
30
+ import {LineBlurExtension, getBlurredLineWidth} from './line-blur-extension';
31
+ import {
32
+ HillshadeTileLayer,
33
+ createSharedDemTileset,
34
+ getHillshadeParameters,
35
+ type DemTile,
36
+ type DemTilesetOwner
37
+ } from './hillshade-layer';
38
+ import type {LabelPlacement} from './label-placement';
39
+ import {MVTLabelLayer, type LabelTile} from './mvt-label-layer';
40
+ import {isSameList} from './util';
41
+ import {
42
+ type RasterColorAdjustments,
43
+ RasterColorExtension,
44
+ hasRasterColorAdjustments
45
+ } from './raster-color-extension';
46
+ import {filterFeatures, parseProperties} from './map-style';
47
+ import {
48
+ STYLE_ZOOM_STEP,
49
+ type StyleAccessor,
50
+ getFilterZoom,
51
+ getStyleAccessor,
52
+ getStyleZoomKey,
53
+ getZoomBucket,
54
+ withOpacity
55
+ } from './style-accessor';
56
+ import {evaluateStyleLight, getStylePropertyDefault} from './style-expression';
57
+ import {
58
+ DEFAULT_FILL_EXTRUSION_LIGHT,
59
+ type FillExtrusionLight,
60
+ FillExtrusionLightingExtension
61
+ } from './fill-extrusion-lighting-extension';
62
+ import {
63
+ MAX_ZOOM_PAINT_PARTS,
64
+ ZOOM_PAINT_FILL_COLOR_ROW,
65
+ ZOOM_PAINT_LINE_COLOR_ROW,
66
+ ZOOM_PAINT_WIDTH_ROW,
67
+ ZoomPaint,
68
+ ZoomPaintExtension,
69
+ type ZoomPaintValues,
70
+ createZoomPaintValues,
71
+ getZoomPaintOffset
72
+ } from './zoom-paint-extension';
73
+ import type {BasemapGlobeConfig, BasemapLayerProps} from './basemap-layer';
74
+ import type {SpriteAtlas} from './sprite';
75
+ import type {LabelFontFamily} from './text-font';
76
+ import type {
77
+ BasemapLoadOptions,
78
+ BasemapSource,
79
+ ResolvedBasemapStyleLayer as BasemapStyleLayer,
80
+ ResolvedBasemapStyle
81
+ } from './style-resolver';
82
+
83
+ type BasemapMode = NonNullable<BasemapLayerProps['mode']>;
84
+
85
+ type BasemapLayerConfig = {
86
+ atmosphere: boolean;
87
+ basemap: boolean;
88
+ labels: boolean;
89
+ };
90
+
91
+ type BasemapLayerGroup = {
92
+ idPrefix?: string;
93
+ mode?: BasemapMode;
94
+ globe?: {config?: BasemapGlobeConfig};
95
+ styleDefinition: ResolvedBasemapStyle;
96
+ zoom?: number;
97
+ loadOptions?: BasemapLoadOptions;
98
+ /**
99
+ * Sprites loaded for the style; symbol layers draw `icon-image` from them. `null` means the
100
+ * sprites are still loading: no icons are drawn, and labels are placed as if each symbol had its
101
+ * icon. Omit it, or pass `[]`, for a style without sprites.
102
+ */
103
+ spriteAtlases?: SpriteAtlas[] | null;
104
+ /** Overrides the label font that `text-font` maps to (see `BasemapLayerProps.fontFamily`). */
105
+ fontFamily?: LabelFontFamily | null;
106
+ /** The style's glyphs, which labels are drawn with (see `MVTLabelLayerProps.glyphs`). */
107
+ glyphs?: GlyphStore | null;
108
+ /** The label placement shared by the map's label layers (see `LabelPlacement`). */
109
+ labelPlacement?: LabelPlacement | null;
110
+ /** Externally owned source caches; BasemapLayer supplies and finalizes these. */
111
+ tilesets?: Map<BasemapSource, SharedTileset2D>;
112
+ };
113
+
114
+ /**
115
+ * Style layers that draw together, in style order: a background, raster or hillshade layer on its
116
+ * own, or consecutive style layers of one vector source, which share one tile layer.
117
+ */
118
+ export type StyleLayerRun = {
119
+ type: 'background' | 'raster' | 'hillshade' | 'vector';
120
+ /** The deck.gl layer id, without the basemap's id prefix. */
121
+ id: string;
122
+ sourceId?: string;
123
+ source?: BasemapSource;
124
+ styleLayers: BasemapStyleLayer[];
125
+ };
126
+
127
+ function logBasemapRuntimeEvent(message: string, details?: unknown): void {
128
+ log.probe(1, `[BasemapLayer] ${message}`, details ?? '')();
129
+ }
130
+
131
+ function logBasemapRuntimeError(message: string, error: unknown, details?: unknown): void {
132
+ log.error(`[BasemapLayer] ${message}`, details || '', error)();
133
+ }
134
+
135
+ function getBackgroundParameters(mode: BasemapMode) {
136
+ return (
137
+ mode === 'globe'
138
+ ? {depthTest: true, depthWriteEnabled: true, depthCompare: 'less-equal', cullMode: 'back'}
139
+ : {depthTest: false, cullMode: 'none'}
140
+ ) as any;
141
+ }
142
+
143
+ /**
144
+ * Flat map layers draw in style order without writing depth, but are depth-tested so that
145
+ * extrusions hide the ground behind them. On the globe, layers tessellate the sphere differently,
146
+ * so a later layer can lie behind an earlier one and fail a depth test: globe layers draw in style
147
+ * order without depth, and back-face culling hides the far side. The globe background still
148
+ * writes depth, so the globe hides what is drawn behind it.
149
+ *
150
+ * A flat globe layer drawn after an extrusion is `depthTested`, still without writing depth, so
151
+ * that the buildings hide it. It is tested against the extrusions and the globe background only,
152
+ * never against other flat layers, and extrusions draw at high zoom, where tiles lie close enough
153
+ * to the sphere to pass the test against the background.
154
+ */
155
+ function getTileParameters(mode: BasemapMode, depthTested = false) {
156
+ if (mode === 'globe' && !depthTested) {
157
+ return {depthTest: false, depthWriteEnabled: false, cullMode: 'back'} as any;
158
+ }
159
+ return {
160
+ depthTest: true,
161
+ depthWriteEnabled: false,
162
+ depthCompare: 'less-equal',
163
+ cullMode: mode === 'globe' ? 'back' : 'none'
164
+ } as any;
165
+ }
166
+
167
+ const BACKGROUND_DATA = [
168
+ [
169
+ [-180, 90],
170
+ [0, 90],
171
+ [180, 90],
172
+ [180, -90],
173
+ [0, -90],
174
+ [-180, -90]
175
+ ]
176
+ ];
177
+
178
+ const BACKGROUND_NORTH_POLE_DATA = [
179
+ [
180
+ [-180, 90],
181
+ [0, 90],
182
+ [180, 90],
183
+ [180, 85],
184
+ [0, 85],
185
+ [-180, 85]
186
+ ]
187
+ ];
188
+
189
+ const SUPPORTED_TYPES = new Set([
190
+ 'background',
191
+ 'circle',
192
+ 'fill',
193
+ 'fill-extrusion',
194
+ 'line',
195
+ 'symbol',
196
+ 'raster',
197
+ 'hillshade'
198
+ ]);
199
+ const DEFAULT_CONFIG: BasemapLayerConfig = {atmosphere: false, basemap: true, labels: true};
200
+ const DEFAULT_TEXT_COLOR = [0, 0, 0, 1];
201
+ // The style spec's default `line-color` and `fill-extrusion-color`.
202
+ const DEFAULT_COLOR = [0, 0, 0, 1];
203
+ const TRANSPARENT_COLOR: [number, number, number, number] = [0, 0, 0, 0];
204
+
205
+ /**
206
+ * Dashes for `line-dasharray`, shared by every dashed sublayer so deck.gl does not see their
207
+ * extensions change. `dashMode: 'path'` runs the pattern continuously along each line, as MapLibre
208
+ * does, instead of restarting it at every vertex.
209
+ */
210
+ const DASH_EXTENSION = new PathStyleExtension({dash: true, dashMode: 'path'});
211
+ /** The dash array of a solid line in a dashed sublayer: no gaps. */
212
+ const SOLID_DASH_ARRAY: StyleAccessor<[number, number]> = {value: [0, 0]};
213
+ /** `line-offset` and `line-gap-width`, alone and with dashes; shared for the same reason. */
214
+ const OFFSET_EXTENSION = new PathStyleExtension({offset: true});
215
+ const DASH_OFFSET_EXTENSION = new PathStyleExtension({dash: true, dashMode: 'path', offset: true});
216
+ /** `line-blur`, shared for the same reason: by layer, and per feature. */
217
+ const LINE_BLUR_EXTENSION = new LineBlurExtension();
218
+ const LINE_BLUR_PER_FEATURE_EXTENSION = new LineBlurExtension({perFeature: true});
219
+ /** A fill's outline, drawn as a line; shared for the same reason. */
220
+ const FILL_OUTLINE_EXTENSION = new FillOutlineExtension();
221
+
222
+ /** `fill-pattern` and `fill-translate`, shared by every fill that sets them. */
223
+ const FILL_PATTERN_EXTENSION = new FillPatternExtension();
224
+ const FILL_TRANSLATE_EXTENSION = new FillTranslateExtension();
225
+
226
+ /** Applies the `raster-*` color adjustments, shared by every raster layer that sets them. */
227
+ const RASTER_COLOR_EXTENSION = new RasterColorExtension();
228
+
229
+ /**
230
+ * The texture filters of `raster-resampling: nearest`. MapLibre magnifies with the nearest texel
231
+ * and keeps a linear filter for minification.
232
+ */
233
+ const NEAREST_TEXTURE_PARAMETERS = {magFilter: 'nearest'} as const;
234
+
235
+ /** Extrusions are depth-tested against each other; flat layers draw in style order. */
236
+ const EXTRUSION_PARAMETERS = {
237
+ depthTest: true,
238
+ depthWriteEnabled: true,
239
+ depthCompare: 'less-equal',
240
+ cullMode: 'none'
241
+ } as any;
242
+
243
+ /** The latest evaluated paint per style layer, reused across tiles within one zoom step. */
244
+ const paintCache = new WeakMap<
245
+ BasemapStyleLayer,
246
+ {zoomBucket: number; paint: Record<string, any>}
247
+ >();
248
+
249
+ /**
250
+ * Evaluates a style layer's paint at the stepped zoom (see `getZoomBucket`). Every tile
251
+ * regenerated at one step gets the same result, so it is computed once per style layer and step.
252
+ */
253
+ function getPaint(layer: BasemapStyleLayer, zoom: number): Record<string, any> {
254
+ const zoomBucket = getZoomBucket(zoom);
255
+ const cached = paintCache.get(layer);
256
+ if (cached && cached.zoomBucket === zoomBucket) {
257
+ return cached.paint;
258
+ }
259
+ const properties = parseProperties(layer, {zoom: zoomBucket});
260
+ const paint = Object.fromEntries(
261
+ properties.map(entry => [Object.keys(entry)[0], Object.values(entry)[0]])
262
+ );
263
+ paintCache.set(layer, {zoomBucket, paint});
264
+ return paint;
265
+ }
266
+
267
+ type FilteredFeatures = {
268
+ /** The filter's contents when the entry was made, so an in-place edit is detected. */
269
+ filterSnapshot: string;
270
+ sourceLayer: string | undefined;
271
+ filterZoom: number | null;
272
+ /** The tile content's length when it was filtered: content edited in place is filtered again. */
273
+ length: number;
274
+ features: any[];
275
+ };
276
+
277
+ /**
278
+ * The latest filtered features per tile content and style layer. Tile sublayers regenerate at
279
+ * every style zoom step; handing them the same array keeps deck.gl from seeing a data change, so
280
+ * only zoom-dependent accessors recompute instead of every feature being re-tessellated. The
281
+ * entry is reused while the contents of the style layer's `filter`, its `source-layer` and (for
282
+ * filters that read `["zoom"]`) the integer zoom are unchanged, so a filter edited in place is
283
+ * re-applied, as `compileStyleFilter` recompiles it.
284
+ */
285
+ const filteredFeatureCache = new WeakMap<any[], WeakMap<BasemapStyleLayer, FilteredFeatures>>();
286
+
287
+ /**
288
+ * The features of each tile content by source layer, in tile order, and the content's length when
289
+ * they were split: content whose length changes, edited in place, is split again.
290
+ */
291
+ const sourceLayerFeatureCache = new WeakMap<
292
+ any[],
293
+ {length: number; bySourceLayer: Map<unknown, any[]>}
294
+ >();
295
+
296
+ function getSourceLayerFeatures(features: any[], sourceLayer: string): any[] {
297
+ const cached = sourceLayerFeatureCache.get(features);
298
+ let bySourceLayer = cached?.length === features.length ? cached.bySourceLayer : undefined;
299
+ if (!bySourceLayer) {
300
+ bySourceLayer = new Map();
301
+ for (const feature of features) {
302
+ const layerName = feature.properties?.layerName;
303
+ let group = bySourceLayer.get(layerName);
304
+ if (!group) {
305
+ group = [];
306
+ bySourceLayer.set(layerName, group);
307
+ }
308
+ group.push(feature);
309
+ }
310
+ sourceLayerFeatureCache.set(features, {length: features.length, bySourceLayer});
311
+ }
312
+ return bySourceLayer.get(sourceLayer) || [];
313
+ }
314
+
315
+ /**
316
+ * The contents of style layers' filters, as `filterTileFeatures` compares them. Taken once per
317
+ * style layer each time the tile layers are generated, rather than once per tile.
318
+ */
319
+ class FilterSnapshots {
320
+ private snapshots = new Map<BasemapStyleLayer, string>();
321
+
322
+ get(styleLayer: BasemapStyleLayer): string {
323
+ let snapshot = this.snapshots.get(styleLayer);
324
+ if (snapshot === undefined) {
325
+ snapshot = styleLayer.filter === undefined ? '' : JSON.stringify(styleLayer.filter);
326
+ this.snapshots.set(styleLayer, snapshot);
327
+ }
328
+ return snapshot;
329
+ }
330
+ }
331
+
332
+ function filterTileFeatures(
333
+ features: any[],
334
+ styleLayer: BasemapStyleLayer,
335
+ zoom: number,
336
+ filterSnapshots: FilterSnapshots = new FilterSnapshots()
337
+ ): any[] {
338
+ const sourceLayer = styleLayer['source-layer'];
339
+ const {filter} = styleLayer;
340
+ if (!sourceLayer && !filter) {
341
+ return features;
342
+ }
343
+
344
+ const filterSnapshot = filterSnapshots.get(styleLayer);
345
+ // MapLibre evaluates `["zoom"]` in filters at integer zooms.
346
+ const filterZoom = filterSnapshot.includes('["zoom"]') ? getFilterZoom(zoom) : null;
347
+ let byStyleLayer = filteredFeatureCache.get(features);
348
+ if (!byStyleLayer) {
349
+ byStyleLayer = new WeakMap();
350
+ filteredFeatureCache.set(features, byStyleLayer);
351
+ }
352
+ const cached = byStyleLayer.get(styleLayer);
353
+ if (
354
+ cached &&
355
+ cached.filterSnapshot === filterSnapshot &&
356
+ cached.sourceLayer === sourceLayer &&
357
+ cached.filterZoom === filterZoom &&
358
+ cached.length === features.length
359
+ ) {
360
+ return cached.features;
361
+ }
362
+
363
+ const sourceFeatures = sourceLayer ? getSourceLayerFeatures(features, sourceLayer) : features;
364
+ const filtered = filter
365
+ ? filterFeatures({
366
+ features: sourceFeatures,
367
+ filter: filter as unknown[],
368
+ globalProperties: {zoom: getFilterZoom(zoom)}
369
+ })
370
+ : sourceFeatures;
371
+ byStyleLayer.set(styleLayer, {
372
+ filterSnapshot,
373
+ sourceLayer,
374
+ filterZoom,
375
+ length: features.length,
376
+ features: filtered
377
+ });
378
+ return filtered;
379
+ }
380
+
381
+ /** The `minzoom`/`maxzoom` limits that `isStyleLayerVisibleAtZoom` compares against. */
382
+ export function getStyleZoomLimits(styleLayers: BasemapStyleLayer[]): (number | undefined)[] {
383
+ return styleLayers.flatMap(layer => [layer.minzoom, layer.maxzoom]);
384
+ }
385
+
386
+ /**
387
+ * A style layer is visible unless its `layout.visibility` is `"none"`, and within its own
388
+ * `minzoom`/`maxzoom` only, as in MapLibre. A source's `maxzoom` limits which tiles exist, not
389
+ * which layers draw: past it, the tile layer overzooms. `visibility` is a constant in the style
390
+ * spec, so it is read directly rather than evaluated.
391
+ */
392
+ function isStyleLayerVisibleAtZoom(styleLayer: BasemapStyleLayer, zoom: number): boolean {
393
+ if (styleLayer.layout?.visibility === 'none') {
394
+ return false;
395
+ }
396
+ const {minzoom, maxzoom} = styleLayer;
397
+ return (minzoom === undefined || zoom >= minzoom) && (maxzoom === undefined || zoom < maxzoom);
398
+ }
399
+
400
+ function getTileFeatures(data: unknown): any[] {
401
+ if (Array.isArray(data)) {
402
+ return data;
403
+ }
404
+
405
+ if (
406
+ data &&
407
+ typeof data === 'object' &&
408
+ Array.isArray((data as {features?: unknown[]}).features)
409
+ ) {
410
+ return (data as {features: unknown[]}).features as any[];
411
+ }
412
+
413
+ return [];
414
+ }
415
+
416
+ /** The prop that records which style layer of its run a tile sublayer draws. */
417
+ const STYLE_LAYER_INDEX = 'styleLayerIndex';
418
+ /** The view each label layer draws in, or `null` for every view (see `renderLabelLayers`). */
419
+ const labelLayerViews = new WeakMap<Layer, string | null>();
420
+
421
+ /** A view's key in `SharedTile2DLayer`'s per-view tileset views. */
422
+ function getViewKey(viewport: {id?: string} | null | undefined): string {
423
+ return viewport?.id || 'default';
424
+ }
425
+
426
+ /** The prop that tells a tile's `renderSubLayers` whether its vector tile layer auto-highlights. */
427
+ const AUTO_HIGHLIGHT = 'styleAutoHighlight';
428
+
429
+ /**
430
+ * The id of a style layer's sublayer in a tile. Style layer ids are arbitrary strings, and
431
+ * generated layers append `-<suffix>` to it: deck.gl's own composite sublayers, the second side of
432
+ * a `line-gap-width` and the per-feature line shape groups. Quoting the style layer id as a JSON
433
+ * string ends it at an unescaped quote, so no suffix can turn one style layer's id into another's.
434
+ * Consecutive style layers drawn in one sublayer are named by the JSON array of their ids, which
435
+ * starts with `[` where a single id starts with `"`.
436
+ */
437
+ function getStyleSubLayerId(parentId: string, styleLayerId: string | string[]): string {
438
+ return `${parentId}-${JSON.stringify(styleLayerId)}`;
439
+ }
440
+
441
+ /**
442
+ * The prop that hands a tile's `renderSubLayers` the `ZoomPaintContext` of its tile layer, or
443
+ * null where fills and lines cannot take their paint from `ZoomPaintExtension`, which needs WebGL.
444
+ */
445
+ const ZOOM_PAINT_CONTEXT = 'zoomPaintContext';
446
+
447
+ /** Shared by every fill and line sublayer, so deck.gl does not see their extensions change. */
448
+ const ZOOM_PAINT_EXTENSION = new ZoomPaintExtension();
449
+
450
+ /** The paint properties `getGeometryPaint` reads. */
451
+ const GEOMETRY_PAINT_PROPERTIES = [
452
+ 'fill-color',
453
+ 'fill-opacity',
454
+ 'fill-pattern',
455
+ 'fill-outline-color',
456
+ 'fill-antialias',
457
+ 'line-color',
458
+ 'line-opacity',
459
+ 'line-width'
460
+ ];
461
+
462
+ /**
463
+ * Writes style layers' colors for `ZoomPaintExtension`, in 0-1, with an alpha of -1 where they are
464
+ * per feature. deck.gl draws a color that is the same for a whole layer as it is, and per-object
465
+ * colors in 8 bits: style layers that would have per-object colors in one sublayer have their
466
+ * colors rounded to 8 bits, so that taking them from a texture changes no pixel.
467
+ */
468
+ function setZoomPaintColors(
469
+ values: ZoomPaintValues,
470
+ row: number,
471
+ colors: StyleAccessor<number[]>[]
472
+ ): void {
473
+ const isLayerConstant = colors.every(
474
+ ({value}) =>
475
+ typeof value !== 'function' && JSON.stringify(value) === JSON.stringify(colors[0].value)
476
+ );
477
+ colors.forEach(({value}, part) => {
478
+ // Per-feature colors keep the alpha of -1 that takes them from the attribute.
479
+ if (typeof value !== 'function') {
480
+ const offset = getZoomPaintOffset(row, part);
481
+ for (let channel = 0; channel < 4; channel++) {
482
+ values[offset + channel] =
483
+ (isLayerConstant ? value[channel] : toUnorm8(value[channel])) / 255;
484
+ }
485
+ }
486
+ });
487
+ }
488
+
489
+ /** Rounds a 0-255 value as deck.gl stores it in an 8 bit color attribute. */
490
+ const unorm8 = new Uint8ClampedArray(1);
491
+ function toUnorm8(value: number): number {
492
+ unorm8[0] = value;
493
+ return unorm8[0];
494
+ }
495
+
496
+ /**
497
+ * Evaluates the paint of style layers drawn by one sublayer for `ZoomPaintExtension`: a style
498
+ * layer's width, or 1 where it is per feature, and its colors in 0-1, or an alpha of -1 where they
499
+ * are per feature.
500
+ */
501
+ export function getZoomPaintValues(
502
+ styleLayers: BasemapStyleLayer[],
503
+ mode: BasemapMode,
504
+ zoom: number
505
+ ): ZoomPaintValues {
506
+ const values = createZoomPaintValues();
507
+ const paints = styleLayers.map(styleLayer => getGeometryPaint(styleLayer, mode, zoom));
508
+ paints.forEach(({lineWidth}, part) => {
509
+ if (typeof lineWidth.value !== 'function') {
510
+ values[getZoomPaintOffset(ZOOM_PAINT_WIDTH_ROW, part)] = lineWidth.value;
511
+ }
512
+ });
513
+ setZoomPaintColors(
514
+ values,
515
+ ZOOM_PAINT_LINE_COLOR_ROW,
516
+ paints.map(paint => paint.lineColor)
517
+ );
518
+ setZoomPaintColors(
519
+ values,
520
+ ZOOM_PAINT_FILL_COLOR_ROW,
521
+ paints.map(paint => paint.fillColor)
522
+ );
523
+ return values;
524
+ }
525
+
526
+ /**
527
+ * The paint of one tile layer's fill and line sublayers for `ZoomPaintExtension`: the zoom its
528
+ * sublayers were generated at, and a `ZoomPaint` per sequence of style layers drawn by one
529
+ * sublayer, shared by all tiles. Kept in the tile layer's state.
530
+ */
531
+ export class ZoomPaintContext {
532
+ /** The stepped zoom (see `getZoomBucket`) at which paint is evaluated. */
533
+ zoom = 0;
534
+
535
+ private zoomPaints = new Map<string, {styleLayers: BasemapStyleLayer[]; zoomPaint: ZoomPaint}>();
536
+
537
+ /** The style the zoom paints were made for (see `setStyle`). */
538
+ private style: unknown;
539
+
540
+ /** The zoom paints no sublayer has asked for since the last style change. */
541
+ private unused = new Set<string>();
542
+
543
+ /**
544
+ * Destroys the zoom paints of sequences of style layers that no sublayer drew with since the
545
+ * previous style change. A new style keeps the zoom paints of the style layers it keeps, and its
546
+ * tile sublayers, regenerated, ask for the others again, so at most one style's are left over.
547
+ */
548
+ setStyle(style: unknown): void {
549
+ if (style === this.style) {
550
+ return;
551
+ }
552
+ this.style = style;
553
+ for (const key of this.unused) {
554
+ this.zoomPaints.get(key)?.zoomPaint.destroy();
555
+ this.zoomPaints.delete(key);
556
+ }
557
+ this.unused = new Set(this.zoomPaints.keys());
558
+ }
559
+
560
+ /**
561
+ * The `ZoomPaint` of style layers drawn by one sublayer. Its values are evaluated once per zoom
562
+ * step, and again when a paint property is replaced.
563
+ */
564
+ get(styleLayers: BasemapStyleLayer[], mode: BasemapMode): ZoomPaint {
565
+ const key = `${mode} ${JSON.stringify(styleLayers.map(layer => layer.id))}`;
566
+ this.unused.delete(key);
567
+ const cached = this.zoomPaints.get(key);
568
+ if (
569
+ cached &&
570
+ cached.styleLayers.length === styleLayers.length &&
571
+ cached.styleLayers.every((layer, index) => layer === styleLayers[index])
572
+ ) {
573
+ return cached.zoomPaint;
574
+ }
575
+ // A new style: the sublayers that drew with the previous style layers are gone.
576
+ cached?.zoomPaint.destroy();
577
+
578
+ // The paint values the current values were evaluated from, compared at every draw.
579
+ const inputs: unknown[] = [];
580
+ const inputsChanged = () => {
581
+ let index = 0;
582
+ let changed = false;
583
+ for (const layer of styleLayers) {
584
+ for (const name of GEOMETRY_PAINT_PROPERTIES) {
585
+ const input = layer.paint?.[name];
586
+ if (inputs[index] !== input) {
587
+ inputs[index] = input;
588
+ changed = true;
589
+ }
590
+ index++;
591
+ }
592
+ }
593
+ return changed;
594
+ };
595
+ let last: {zoom: number; values: ZoomPaintValues} | null = null;
596
+ const zoomPaint = new ZoomPaint(() => {
597
+ const {zoom} = this;
598
+ if (inputsChanged() || !last || last.zoom !== zoom) {
599
+ last = {zoom, values: getZoomPaintValues(styleLayers, mode, zoom)};
600
+ }
601
+ return last.values;
602
+ });
603
+ this.zoomPaints.set(key, {styleLayers: [...styleLayers], zoomPaint});
604
+ return zoomPaint;
605
+ }
606
+
607
+ /** Releases the textures of every `ZoomPaint`. */
608
+ destroy(): void {
609
+ for (const {zoomPaint} of this.zoomPaints.values()) {
610
+ zoomPaint.destroy();
611
+ }
612
+ this.zoomPaints.clear();
613
+ }
614
+ }
615
+
616
+ function getSubLayerBaseProps(props: any) {
617
+ const {
618
+ pickable,
619
+ visible,
620
+ opacity,
621
+ modelMatrix,
622
+ coordinateSystem,
623
+ coordinateOrigin,
624
+ extensions,
625
+ highlightedObjectIndex,
626
+ highlightColor,
627
+ parameters,
628
+ wrapLongitude,
629
+ [STYLE_LAYER_INDEX]: styleLayerIndex
630
+ } = props;
631
+
632
+ return {
633
+ // Recorded for `sortTileSubLayers`, which orders all tiles' sublayers by style layer.
634
+ [STYLE_LAYER_INDEX]: styleLayerIndex,
635
+ pickable,
636
+ visible,
637
+ opacity,
638
+ modelMatrix,
639
+ coordinateSystem,
640
+ coordinateOrigin,
641
+ extensions,
642
+ highlightedObjectIndex,
643
+ highlightColor,
644
+ parameters,
645
+ wrapLongitude
646
+ };
647
+ }
648
+
649
+ function getGlobeFillColor(color: [number, number, number, number], mode: BasemapMode) {
650
+ if (mode !== 'globe') {
651
+ return color;
652
+ }
653
+
654
+ return [color[0], color[1], color[2], 255] as [number, number, number, number];
655
+ }
656
+
657
+ function getConfig(globe?: {config?: BasemapGlobeConfig}): BasemapLayerConfig {
658
+ return {...DEFAULT_CONFIG, ...(globe?.config || {})};
659
+ }
660
+
661
+ function createBackgroundLayer({
662
+ idPrefix,
663
+ layer,
664
+ zoom,
665
+ mode
666
+ }: {
667
+ idPrefix: string;
668
+ layer: BasemapStyleLayer;
669
+ zoom: number;
670
+ mode: BasemapMode;
671
+ }) {
672
+ const paint = getPaint(layer, zoom);
673
+
674
+ return new SolidPolygonLayer({
675
+ id: `${idPrefix}-${layer.id}`,
676
+ data: BACKGROUND_DATA,
677
+ getPolygon: d => d,
678
+ stroked: false,
679
+ filled: true,
680
+ // A `background-pattern` disables `background-color`. Patterns are not drawn, so a patterned
681
+ // background draws nothing rather than its color or the default black.
682
+ getFillColor:
683
+ paint['background-pattern'] !== undefined
684
+ ? TRANSPARENT_COLOR
685
+ : withOpacity(
686
+ (paint['background-color'] ?? getStylePropertyDefault('background-color')) as number[],
687
+ paint['background-opacity'] ?? 1
688
+ ),
689
+ parameters: getBackgroundParameters(mode)
690
+ });
691
+ }
692
+
693
+ /** A raster paint value, or the style specification's default when the layer does not set it. */
694
+ function getRasterPaintValue(paint: Record<string, any>, propertyName: string): any {
695
+ return paint[propertyName] ?? getStylePropertyDefault(propertyName);
696
+ }
697
+
698
+ function getRasterColorAdjustments(paint: Record<string, any>): RasterColorAdjustments {
699
+ return {
700
+ brightnessMin: getRasterPaintValue(paint, 'raster-brightness-min'),
701
+ brightnessMax: getRasterPaintValue(paint, 'raster-brightness-max'),
702
+ saturation: getRasterPaintValue(paint, 'raster-saturation'),
703
+ contrast: getRasterPaintValue(paint, 'raster-contrast'),
704
+ hueRotate: getRasterPaintValue(paint, 'raster-hue-rotate')
705
+ };
706
+ }
707
+
708
+ /** The bounds of the Web Mercator tile pyramid. */
709
+ const WORLD_TILE_EXTENT = [-180, -85.051129, 180, 85.051129];
710
+
711
+ /**
712
+ * The `zoomOffset` of vector sources' tiles. MapLibre GL JS draws a vector source with the tiles
713
+ * at the floor of the zoom (and a raster source with those at the rounded zoom, as deck.gl's
714
+ * `TileLayer` does): its styles and labels are laid out for that. Half a zoom down before
715
+ * rounding selects the same tiles.
716
+ */
717
+ export const VECTOR_TILE_ZOOM_OFFSET = -0.5;
718
+
719
+ /**
720
+ * The tile pyramid range of a source. `TileLayer` draws nothing at a view zoom below its `minZoom`,
721
+ * unless it has an `extent`. On a globe, the view zoom goes below 0 near the poles: `GlobeView`
722
+ * lowers the zoom with the latitude, so that the globe keeps its size on screen (zoom 0 at the
723
+ * equator is zoom -2 at latitude 76). A source whose pyramid starts at zoom 0 draws its zoom 0
724
+ * tiles at any lower zoom, as in MapLibre; the extent, the whole pyramid, selects them.
725
+ */
726
+ export function getTilePyramidProps(source: BasemapSource): {
727
+ minZoom: number;
728
+ maxZoom: number;
729
+ extent: number[] | null;
730
+ } {
731
+ const minZoom = source.minzoom ?? 0;
732
+ return {
733
+ minZoom,
734
+ maxZoom: source.type === 'geojson' ? getGeoJsonMaxZoom(source) : (source.maxzoom ?? 22),
735
+ extent: minZoom <= 0 ? WORLD_TILE_EXTENT : null
736
+ };
737
+ }
738
+
739
+ /**
740
+ * Creates the tileset of a `raster-dem` source: each tile is fetched and decoded once, and shared by
741
+ * every `hillshade` layer drawn from it. `BasemapLayer` creates and finalizes these itself. Direct
742
+ * `getBasemapLayers` callers that call it on every render pass them in `tilesets`, keyed by source,
743
+ * to keep the tiles across calls, and finalize them when done.
744
+ */
745
+ export function createDemTileset(
746
+ source: BasemapSource,
747
+ loadOptions?: BasemapLoadOptions
748
+ ): SharedTileset2D<DemTile | null> {
749
+ return createSharedDemTileset(source, loadOptions, getTilePyramidProps(source));
750
+ }
751
+
752
+ function createRasterLayer({
753
+ idPrefix,
754
+ layer,
755
+ source,
756
+ zoom,
757
+ mode,
758
+ depthTested
759
+ }: {
760
+ idPrefix: string;
761
+ layer: BasemapStyleLayer;
762
+ source: BasemapSource;
763
+ zoom: number;
764
+ mode: BasemapMode;
765
+ /** Drawn after an extrusion, see `getTileParameters`. */
766
+ depthTested?: boolean;
767
+ }) {
768
+ const paint = getPaint(layer, zoom);
769
+ const rasterColorAdjustments = getRasterColorAdjustments(paint);
770
+ // A zoom-interpolated adjustment that crosses its default value adds or removes the extension
771
+ // at that zoom step, which recompiles the tiles' shaders once.
772
+ const isColorAdjusted = hasRasterColorAdjustments(rasterColorAdjustments);
773
+ // MapLibre magnifies with nearest-neighbour sampling when either property is `nearest`.
774
+ const isNearest =
775
+ getRasterPaintValue(paint, 'raster-resampling') === 'nearest' ||
776
+ getRasterPaintValue(paint, 'resampling') === 'nearest';
777
+
778
+ return new TileLayer({
779
+ id: `${idPrefix}-${layer.id}`,
780
+ data: source.tiles,
781
+ ...getTilePyramidProps(source),
782
+ tileSize: source.tileSize || 512,
783
+ // deck.gl draws with `opacity ** (1 / 2.2)`. Undo that, so the alpha is `raster-opacity`, as
784
+ // in MapLibre.
785
+ opacity: getRasterPaintValue(paint, 'raster-opacity') ** 2.2,
786
+ // The color adjustments and texture filter are passed down to each tile's bitmap layer.
787
+ ...(isColorAdjusted ? {extensions: [RASTER_COLOR_EXTENSION], rasterColorAdjustments} : {}),
788
+ textureParameters: isNearest ? NEAREST_TEXTURE_PARAMETERS : null,
789
+ renderSubLayers: props => {
790
+ const {west, south, east, north} = (props.tile?.bbox || {}) as {
791
+ west: number;
792
+ south: number;
793
+ east: number;
794
+ north: number;
795
+ };
796
+
797
+ // Set by `RasterColorExtension` where its shader runs, which outputs premultiplied colors.
798
+ const blendParameters = (props as {rasterColorBlendParameters?: object | null})
799
+ .rasterColorBlendParameters;
800
+
801
+ return new BitmapLayer({
802
+ ...props,
803
+ // A texture keeps the filter it was created with, so a filter change needs a new layer.
804
+ id: isNearest ? `${props.id}-nearest` : props.id,
805
+ _imageCoordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
806
+ data: null,
807
+ image: props.data,
808
+ bounds: [west, south, east, north],
809
+ parameters: blendParameters
810
+ ? {...getTileParameters(mode, depthTested), ...blendParameters}
811
+ : getTileParameters(mode, depthTested)
812
+ } as any);
813
+ },
814
+ onTileError: error => {
815
+ logBasemapRuntimeError('Raster tile failed to load', error, {
816
+ layerId: layer.id,
817
+ sourceId: layer.source
818
+ });
819
+ },
820
+ parameters: getTileParameters(mode, depthTested)
821
+ });
822
+ }
823
+
824
+ /**
825
+ * A `hillshade` style layer over a `raster-dem` source. Its tiles are decoded on the main thread,
826
+ * once for all of the source's hillshade layers, which share `tileset` (see `HillshadeTileLayer`).
827
+ * MapLibre draws nothing at `hillshade-exaggeration: 0`.
828
+ */
829
+ function createHillshadeLayer({
830
+ idPrefix,
831
+ layer,
832
+ tileset,
833
+ demTilesetOwner,
834
+ source,
835
+ zoom,
836
+ mode,
837
+ depthTested
838
+ }: {
839
+ idPrefix: string;
840
+ layer: BasemapStyleLayer;
841
+ /** The source's decoded tiles (see `createDemTileset`). */
842
+ tileset: SharedTileset2D;
843
+ /** Set when the layer shares a tileset that no `BasemapLayer` owns. */
844
+ demTilesetOwner?: DemTilesetOwner;
845
+ source: BasemapSource;
846
+ zoom: number;
847
+ mode: BasemapMode;
848
+ /** Drawn after an extrusion, see `getTileParameters`. */
849
+ depthTested?: boolean;
850
+ }) {
851
+ const hillshadeParameters = getHillshadeParameters(getPaint(layer, zoom));
852
+ const isNearest = getRasterPaintValue(getPaint(layer, zoom), 'resampling') === 'nearest';
853
+ const filter = isNearest ? 'nearest' : 'linear';
854
+
855
+ return new HillshadeTileLayer({
856
+ id: `${idPrefix}-${layer.id}`,
857
+ data: tileset,
858
+ demTilesetOwner,
859
+ ...getTilePyramidProps(source),
860
+ tileSize: source.tileSize || 512,
861
+ visible: hillshadeParameters.exaggeration !== 0,
862
+ hillshadeParameters,
863
+ hillshadeSubLayerProps: {
864
+ // MapLibre samples the slopes without mipmaps.
865
+ textureParameters: {minFilter: filter, magFilter: filter, mipmapFilter: 'none'},
866
+ parameters: getTileParameters(mode, depthTested)
867
+ },
868
+ onTileError: error => {
869
+ logBasemapRuntimeError('DEM tile failed to load', error, {
870
+ layerId: layer.id,
871
+ sourceId: layer.source
872
+ });
873
+ },
874
+ parameters: getTileParameters(mode, depthTested)
875
+ });
876
+ }
877
+
878
+ /**
879
+ * Orders the sublayers of all tiles of a tile layer by style layer, as MapLibre draws each style
880
+ * layer across all tiles before the next: a tile's fills cannot paint over the labels of the tile
881
+ * drawn before it. Within a style layer, tiles keep their order, and so do the sublayers of a tile.
882
+ */
883
+ export function sortTileSubLayers<LayerT extends {props: Record<string, any>}>(
884
+ tileSubLayers: (LayerT[] | null | undefined)[]
885
+ ): LayerT[] {
886
+ // `Array.prototype.sort` is stable.
887
+ return tileSubLayers
888
+ .flatMap(layers => layers || [])
889
+ .sort((a, b) => (a.props[STYLE_LAYER_INDEX] ?? 0) - (b.props[STYLE_LAYER_INDEX] ?? 0));
890
+ }
891
+
892
+ /** The prop that hands a tile's `renderSubLayers` the map to record its label features in. */
893
+ const TILE_LABEL_FEATURES = 'tileLabelFeatures';
894
+
895
+ /** The features of a symbol style layer without any in a tile. */
896
+ const NO_FEATURES: any[] = [];
897
+
898
+ /**
899
+ * Builds the label layers of a run from the label features of its drawn tiles, by style layer
900
+ * index; `getSubLayerProps` gives a label layer the tile layer's props.
901
+ */
902
+ type RenderLabelLayers = (
903
+ tilesByStyleLayer: Map<number, LabelTile[]>,
904
+ getSubLayerProps: (id: string) => Record<string, any>,
905
+ /** The style layers whose label layers are kept hidden (see `renderViewLabelLayers`). */
906
+ hidden: Set<number>
907
+ ) => Layer[];
908
+
909
+ type SourceTilesetOwner = {tileset: SharedTileset2D; consumers: Set<StyledMVTLayer['state']>};
910
+
911
+ /** A tile of a shared tileset, as `SharedTile2DLayer` renders it. */
912
+ type SharedTile = {
913
+ id: string;
914
+ index: {x: number; y: number; z: number};
915
+ bbox?: unknown;
916
+ content?: unknown;
917
+ };
918
+
919
+ class StyledMVTLayer extends SharedMVTLayer {
920
+ declare state: SharedMVTLayer['state'] & {
921
+ sourceTilesetOwner?: SourceTilesetOwner;
922
+ /** The sublayers last generated for each tile, by tile id. */
923
+ keptTileLayers?: Map<string, Layer[]>;
924
+ /** Hidden tiles whose sublayers were kept rather than regenerated, by tile id. */
925
+ staleTiles?: Set<string>;
926
+ /** The tile object each id stood for at the last render (see `forgetRemovedTiles`). */
927
+ keptTiles?: Map<string, SharedTile>;
928
+ /** The paint textures of the fill and line sublayers (see `ZoomPaintExtension`). */
929
+ zoomPaintContext?: ZoomPaintContext;
930
+ /**
931
+ * The features of each symbol style layer in a tile, by tile id and the style layer's index in
932
+ * its run, as `renderSubLayers` last filtered them. Labels are drawn for all tiles at once, see
933
+ * `renderLabelLayers`. Kept per layer: the tiles are shared with the source's other runs.
934
+ */
935
+ tileLabelFeatures?: Map<string, Map<number, any[]>>;
936
+ /**
937
+ * The label tiles of each symbol style layer, by index, as `renderLabelLayers` last made them,
938
+ * by view (see `getLabelViewKey`).
939
+ */
940
+ labelTiles?: Map<string, Map<number, {key: unknown[]; tiles: LabelTile[]}>>;
941
+ /** The symbol style layers that have a label layer, by view (see `renderViewLabelLayers`). */
942
+ labelStyleLayers?: Map<string, Set<number>>;
943
+ /** The style layers `labelStyleLayers` indexes, and the tileset of their tiles. */
944
+ labelStyle?: unknown;
945
+ labelTileset?: unknown;
946
+ /** The label layers last rendered, and the props and tiles they were rendered for, by view. */
947
+ labelLayers?: Map<
948
+ string,
949
+ {
950
+ props: unknown;
951
+ byStyleLayer: Map<number, {tiles: LabelTile[]; isHidden: boolean; layers: Layer[]}>;
952
+ }
953
+ >;
954
+ };
955
+
956
+ updateState(params: UpdateParameters<this>): void {
957
+ const owner = (this.props as any).sourceTilesetOwner as SourceTilesetOwner | undefined;
958
+ const previous = this.state.sourceTilesetOwner;
959
+ if (owner !== previous) {
960
+ this.releaseSourceTileset();
961
+ owner?.consumers.add(this.state);
962
+ this.state.sourceTilesetOwner = owner;
963
+ }
964
+ const {geographic, tileset} = this.state;
965
+ super.updateState(params);
966
+ // A new tileset, or a switch between a map and a globe, makes every kept sublayer obsolete.
967
+ if (this.state.geographic !== geographic || this.state.tileset !== tileset) {
968
+ this.state.keptTileLayers?.clear();
969
+ this.state.staleTiles?.clear();
970
+ this.state.tileLabelFeatures?.clear();
971
+ }
972
+ }
973
+
974
+ /** Whether a tile is drawn in any view of this layer; every tile is, before the first view. */
975
+ isTileShown(tile: SharedTile): boolean {
976
+ const views = this.state.tilesetViews;
977
+ if (!views?.size) {
978
+ return true;
979
+ }
980
+ for (const view of views.values()) {
981
+ if (view.isTileVisible(tile as any)) {
982
+ return true;
983
+ }
984
+ }
985
+ return false;
986
+ }
987
+
988
+ /**
989
+ * `SharedTile2DLayer` generates sublayers for every tile of the shared tileset, shown or not,
990
+ * again whenever the update triggers change, which is at every style zoom step, and culls the
991
+ * hidden ones: several times as many tiles as are shown. A hidden tile keeps the sublayers it
992
+ * has instead, and regenerates them when it is shown again. Kept sublayers stay initialized, so
993
+ * showing the tile again updates them rather than creating them.
994
+ */
995
+ renderLayers(): Layer | null | LayersList {
996
+ const {tileset, tileLayers} = this.state;
997
+ if (!this.state.keptTileLayers) {
998
+ this.state.keptTileLayers = new Map();
999
+ this.state.staleTiles = new Set();
1000
+ }
1001
+ const kept = this.state.keptTileLayers;
1002
+ const stale = this.state.staleTiles!;
1003
+ const tiles: SharedTile[] = (tileset?.tiles as SharedTile[] | undefined) || [];
1004
+ this.forgetRemovedTiles(tiles);
1005
+ for (const tile of tiles) {
1006
+ if (this.isTileShown(tile)) {
1007
+ if (stale.delete(tile.id)) {
1008
+ tileLayers.delete(tile.id);
1009
+ }
1010
+ } else if (!tileLayers.has(tile.id) && kept.has(tile.id)) {
1011
+ tileLayers.set(tile.id, kept.get(tile.id)!);
1012
+ stale.add(tile.id);
1013
+ }
1014
+ }
1015
+ const layers = super.renderLayers();
1016
+ for (const tile of tiles) {
1017
+ const generated = tileLayers.get(tile.id);
1018
+ if (generated && !stale.has(tile.id)) {
1019
+ kept.set(tile.id, generated);
1020
+ }
1021
+ }
1022
+ if (!Array.isArray(layers)) {
1023
+ return layers;
1024
+ }
1025
+ return [...sortTileSubLayers(layers as (Layer[] | null)[]), ...this.renderLabelLayers(tiles)];
1026
+ }
1027
+
1028
+ /**
1029
+ * The labels of all drawn tiles, one label layer per symbol style layer, drawn after every
1030
+ * tile's sublayers: a run has no other style layer after its first symbol layer (see
1031
+ * `getStyleLayerRuns`). Each label layer keeps its tiles' list, and so its rows, until a tile is
1032
+ * shown or hidden, or its features change.
1033
+ */
1034
+ renderLabelLayers(tiles: SharedTile[]): Layer[] {
1035
+ const views = this.state.tilesetViews;
1036
+ // Each view draws the labels of the tiles it shows: a tile drawn in one view would otherwise
1037
+ // label, and hide the parent labels of, another.
1038
+ if (!views || views.size <= 1) {
1039
+ return this.renderViewLabelLayers(tiles, null, tile => this.isTileShown(tile));
1040
+ }
1041
+ return [...views].flatMap(([viewKey, view]) =>
1042
+ this.renderViewLabelLayers(tiles, viewKey, tile => view.isTileVisible(tile as any))
1043
+ );
1044
+ }
1045
+
1046
+ /** The label layers of the tiles `isShown` accepts, for one view, or for all with `null`. */
1047
+ private renderViewLabelLayers(
1048
+ tiles: SharedTile[],
1049
+ viewKey: string | null,
1050
+ isShown: (tile: SharedTile) => boolean
1051
+ ): Layer[] {
1052
+ const render = (this.props as {renderLabelLayers?: RenderLabelLayers}).renderLabelLayers;
1053
+ if (!render) {
1054
+ return [];
1055
+ }
1056
+ const cacheKey = viewKey ?? '';
1057
+ this.state.labelTiles ||= new Map();
1058
+ this.state.labelLayers ||= new Map();
1059
+ const previous = this.state.labelTiles.get(cacheKey) || new Map();
1060
+ const next = new Map<number, {key: unknown[]; tiles: LabelTile[]}>();
1061
+ const tileSize = (this.props as {tileSize?: number}).tileSize ?? 512;
1062
+ for (const tile of tiles) {
1063
+ const byStyleLayer =
1064
+ tile.content && isShown(tile) ? this.state.tileLabelFeatures?.get(tile.id) : null;
1065
+ for (const [styleLayerIndex, features] of byStyleLayer || []) {
1066
+ let entry = next.get(styleLayerIndex);
1067
+ if (!entry) {
1068
+ entry = {key: [], tiles: []};
1069
+ next.set(styleLayerIndex, entry);
1070
+ }
1071
+ entry.key.push(tile, features);
1072
+ entry.tiles.push({
1073
+ index: tile.index,
1074
+ features,
1075
+ tileBoundingBox: (tile.bbox as LabelTile['tileBoundingBox']) ?? null,
1076
+ tileZoom: tile.index?.z ?? null,
1077
+ tileSize,
1078
+ tile
1079
+ });
1080
+ }
1081
+ }
1082
+ // A style layer without features in any drawn tile has no label layer, unless it has had one:
1083
+ // the drawn tiles change as the view moves, and a style layer whose features are only in
1084
+ // tiles at the view's edge loses and regains them. Its label layer is then kept, hidden, with
1085
+ // the tiles it last drew, so that neither hiding nor showing it again changes its data, while
1086
+ // the style layer is drawn at this zoom and its tiles are of the zoom drawn now or next to it.
1087
+ // A drawn tile's sublayers record every symbol layer drawn at its zoom, with or without
1088
+ // features, so one that a labelled tile leaves out is outside its zoom range, and is dropped.
1089
+ // While no drawn tile is labelled yet, as when they load, the label layer is kept too.
1090
+ // Style layer indices name other style layers in another style, and the kept tiles are gone
1091
+ // with their tileset: a new style or tileset forgets the label layers kept.
1092
+ const style = (this.props as {paintStyle?: unknown}).paintStyle;
1093
+ const {tileset} = this.state;
1094
+ if (
1095
+ !this.state.labelStyleLayers ||
1096
+ this.state.labelStyle !== style ||
1097
+ this.state.labelTileset !== tileset
1098
+ ) {
1099
+ this.state.labelStyleLayers = new Map();
1100
+ this.state.labelStyle = style;
1101
+ this.state.labelTileset = tileset;
1102
+ }
1103
+ const labelled = this.state.labelStyleLayers.get(cacheKey) || new Set<number>();
1104
+ const drawnTiles = tiles.filter(tile => tile.content && isShown(tile));
1105
+ const drawnFeatures = drawnTiles
1106
+ .map(tile => this.state.tileLabelFeatures?.get(tile.id))
1107
+ .filter((byStyleLayer): byStyleLayer is Map<number, any[]> => Boolean(byStyleLayer));
1108
+ // Only tiles of the zoom drawn now, or of the next zoom in or out, as across one tile zoom
1109
+ // step, are likely to return to the view and draw the kept rows again.
1110
+ const drawnZoom = drawnTiles.length ? Math.max(...drawnTiles.map(tile => tile.index.z)) : null;
1111
+ const isAtDrawnZoom = (entry: {tiles: LabelTile[]}) =>
1112
+ drawnZoom === null ||
1113
+ entry.tiles.length === 0 ||
1114
+ Math.abs(Math.max(...entry.tiles.map(tile => tile.index.z)) - drawnZoom) <= 1;
1115
+ const hidden = new Set<number>();
1116
+ for (const styleLayerIndex of labelled) {
1117
+ const entry = next.get(styleLayerIndex);
1118
+ const last = previous.get(styleLayerIndex);
1119
+ const isDrawnAtThisZoom = entry
1120
+ ? true
1121
+ : drawnFeatures.every(byStyleLayer => byStyleLayer.has(styleLayerIndex));
1122
+ if (
1123
+ last &&
1124
+ isDrawnAtThisZoom &&
1125
+ isAtDrawnZoom(last) &&
1126
+ (!entry || entry.tiles.every(tile => tile.features.length === 0))
1127
+ ) {
1128
+ next.set(styleLayerIndex, last);
1129
+ hidden.add(styleLayerIndex);
1130
+ }
1131
+ }
1132
+ for (const [styleLayerIndex, entry] of next) {
1133
+ if (hidden.has(styleLayerIndex)) {
1134
+ continue;
1135
+ }
1136
+ if (entry.tiles.every(tile => tile.features.length === 0)) {
1137
+ next.delete(styleLayerIndex);
1138
+ continue;
1139
+ }
1140
+ const last = previous.get(styleLayerIndex);
1141
+ if (last && isSameList(last.key, entry.key)) {
1142
+ next.set(styleLayerIndex, last);
1143
+ }
1144
+ }
1145
+ this.state.labelTiles.set(cacheKey, next);
1146
+ this.state.labelStyleLayers.set(cacheKey, new Set(next.keys()));
1147
+ // Each style layer's label layers stay the same instances while its tiles, whether it is
1148
+ // hidden, and this layer's props (with `render`, new at each style zoom step) do not change:
1149
+ // new instances would compare and update the label layers at every frame. A hidden label
1150
+ // layer keeps its tiles, and its instance, also across zoom steps: it is not drawn, and the
1151
+ // instance that shows it again is compared with this one, so it then takes every change made
1152
+ // meanwhile. A new style has no hidden label layers (see above).
1153
+ const lastLayers = this.state.labelLayers.get(cacheKey);
1154
+ const byStyleLayer = new Map<
1155
+ number,
1156
+ {tiles: LabelTile[]; isHidden: boolean; layers: Layer[]}
1157
+ >();
1158
+ const layers: Layer[] = [];
1159
+ // In style order, whichever tile first had a style layer.
1160
+ for (const [styleLayerIndex, entry] of [...next].sort(([a], [b]) => a - b)) {
1161
+ const isHidden = hidden.has(styleLayerIndex);
1162
+ const kept =
1163
+ lastLayers?.props === this.props || isHidden
1164
+ ? lastLayers?.byStyleLayer.get(styleLayerIndex)
1165
+ : undefined;
1166
+ let rendered = kept;
1167
+ if (!kept || kept.tiles !== entry.tiles || kept.isHidden !== isHidden) {
1168
+ const styleLayerLayers = render(
1169
+ new Map([[styleLayerIndex, entry.tiles]]),
1170
+ id => this.getSubLayerProps({id: viewKey === null ? id : `${id}-view-${viewKey}`}),
1171
+ hidden
1172
+ );
1173
+ for (const layer of styleLayerLayers) {
1174
+ labelLayerViews.set(layer, viewKey);
1175
+ }
1176
+ rendered = {tiles: entry.tiles, isHidden, layers: styleLayerLayers};
1177
+ }
1178
+ byStyleLayer.set(styleLayerIndex, rendered!);
1179
+ layers.push(...rendered!.layers);
1180
+ }
1181
+ this.state.labelLayers.set(cacheKey, {props: this.props, byStyleLayer});
1182
+ return layers;
1183
+ }
1184
+
1185
+ /**
1186
+ * Label layers draw the labels of several tiles, and are not culled by tile; with several views,
1187
+ * each view draws its own.
1188
+ */
1189
+ filterSubLayer(context: any): boolean {
1190
+ const {layer} = context;
1191
+ if (layer.props.tile) {
1192
+ return super.filterSubLayer(context);
1193
+ }
1194
+ const viewKey = labelLayerViews.get(layer);
1195
+ return viewKey === null || viewKey === undefined || viewKey === getViewKey(context.viewport);
1196
+ }
1197
+
1198
+ /**
1199
+ * A picked label reports the tile of its feature, which `SharedMVTLayer` converts from: on a map
1200
+ * the recorded features are tile-local, on a globe in longitude/latitude.
1201
+ */
1202
+ getPickingInfo(params: any): any {
1203
+ const {sourceLayer} = params;
1204
+ if (sourceLayer && !sourceLayer.props.tile) {
1205
+ const coordinateSystem = isGlobeViewport(this.context.viewport)
1206
+ ? COORDINATE_SYSTEM.LNGLAT
1207
+ : COORDINATE_SYSTEM.CARTESIAN;
1208
+ const info = super.getPickingInfo({
1209
+ ...params,
1210
+ sourceLayer: {props: {tile: params.info.tile, coordinateSystem}}
1211
+ });
1212
+ info.sourceTileSubLayer = sourceLayer;
1213
+ return info;
1214
+ }
1215
+ return super.getPickingInfo(params);
1216
+ }
1217
+
1218
+ /**
1219
+ * A hidden tile that has no sublayers yet gets them when it is shown. A tile's new sublayers can
1220
+ * keep those it last rendered (see `getTileGeoJsonLayer`).
1221
+ */
1222
+ renderSubLayers(props: Parameters<SharedMVTLayer['renderSubLayers']>[0]) {
1223
+ if (!this.isTileShown(props.tile)) {
1224
+ this.state.staleTiles?.add(props.tile.id);
1225
+ return [];
1226
+ }
1227
+ this.state.zoomPaintContext ||= new ZoomPaintContext();
1228
+ const {zoomPaintContext} = this.state;
1229
+ zoomPaintContext.setStyle((this.props as {paintStyle?: unknown}).paintStyle);
1230
+ zoomPaintContext.zoom = getZoomBucket((this.props as {styleZoom?: number}).styleZoom ?? 0);
1231
+ // The tile's label features, recorded as `renderSubLayers` filters them.
1232
+ const labelFeatures = new Map<number, any[]>();
1233
+ this.state.tileLabelFeatures ||= new Map();
1234
+ this.state.tileLabelFeatures.set(props.tile.id, labelFeatures);
1235
+ (props as any)[TILE_LABEL_FEATURES] = labelFeatures;
1236
+ const previous = this.state.keptTileLayers?.get(props.tile.id);
1237
+ // `SharedMVTLayer` turns `autoHighlight` off for the tile sublayers, which it highlights itself.
1238
+ return super.renderSubLayers({
1239
+ ...props,
1240
+ [ZOOM_PAINT_CONTEXT]: this.context.device?.type === 'webgpu' ? null : zoomPaintContext,
1241
+ [PREVIOUS_TILE_LAYERS]: previous
1242
+ ? new Map(previous.map((layer: Layer) => [layer.id, layer]))
1243
+ : undefined,
1244
+ [AUTO_HIGHLIGHT]: Boolean(this.props.autoHighlight)
1245
+ } as typeof props);
1246
+ }
1247
+
1248
+ _onTileLoad(tile: any): void {
1249
+ this.forgetTile(tile);
1250
+ super._onTileLoad(tile);
1251
+ }
1252
+
1253
+ _onTileUnload(tile: any): void {
1254
+ this.forgetTile(tile);
1255
+ super._onTileUnload(tile);
1256
+ }
1257
+
1258
+ /**
1259
+ * Drops what this layer kept for tiles that left the shared tileset, or were replaced in it by
1260
+ * another tile of the same id, including the sublayers `SharedTile2DLayer` holds for that id.
1261
+ * `reloadAll` removes tiles without an unload event, and deck.gl then finalizes their sublayers,
1262
+ * which must not be handed back.
1263
+ */
1264
+ private forgetRemovedTiles(tiles: SharedTile[]): void {
1265
+ let {keptTiles} = this.state;
1266
+ if (!keptTiles) {
1267
+ keptTiles = new Map();
1268
+ this.state.keptTiles = keptTiles;
1269
+ }
1270
+ const live = new Map(tiles.map(tile => [tile.id, tile]));
1271
+ for (const [id, tile] of keptTiles) {
1272
+ if (live.get(id) !== tile) {
1273
+ keptTiles.delete(id);
1274
+ this.forgetTile({id} as SharedTile);
1275
+ this.state.tileLayers.delete(id);
1276
+ }
1277
+ }
1278
+ for (const tile of tiles) {
1279
+ keptTiles.set(tile.id, tile);
1280
+ }
1281
+ }
1282
+
1283
+ /** Drops a tile's kept sublayers, when its content is replaced or evicted. */
1284
+ private forgetTile(tile: SharedTile): void {
1285
+ this.state.keptTileLayers?.delete(tile.id);
1286
+ this.state.staleTiles?.delete(tile.id);
1287
+ this.state.tileLabelFeatures?.delete(tile.id);
1288
+ }
1289
+
1290
+ /** Releases the shared tileset and the textures of `ZoomPaintExtension`. */
1291
+ finalizeState(): void {
1292
+ super.finalizeState();
1293
+ this.releaseSourceTileset();
1294
+ this.state.zoomPaintContext?.destroy();
1295
+ }
1296
+
1297
+ private releaseSourceTileset(): void {
1298
+ const owner = this.state.sourceTilesetOwner;
1299
+ if (owner) {
1300
+ owner.consumers.delete(this.state);
1301
+ if (!owner.consumers.size) owner.tileset.finalize();
1302
+ this.state.sourceTilesetOwner = undefined;
1303
+ }
1304
+ }
1305
+
1306
+ static layerName = 'StyledMVTLayer';
1307
+ static defaultProps = {
1308
+ ...SharedMVTLayer.defaultProps,
1309
+ // A new function at each update, as `renderSubLayers`: compared, it would regenerate every tile.
1310
+ renderLabelLayers: {type: 'function', optional: true, value: null, compare: false}
1311
+ } as any;
1312
+ }
1313
+
1314
+ /**
1315
+ * A `GeoJsonLayer` that creates only the sublayers its style layer type draws. Fill, line and
1316
+ * fill-extrusion layers draw no points; a line layer draws polygons as their outlines only; and
1317
+ * `drawsLineStrings: false` leaves out line strings, which a fill without an outline would draw
1318
+ * zero pixels wide. Every sublayer left out would otherwise be updated and drawn at every frame.
1319
+ */
1320
+ class StyledGeoJsonLayer extends GeoJsonLayer<any, {drawsLineStrings?: boolean}> {
1321
+ static layerName = 'StyledGeoJsonLayer';
1322
+ static defaultProps = {...GeoJsonLayer.defaultProps, drawsLineStrings: true};
1323
+
1324
+ /**
1325
+ * `TileLayer` clones each tile sublayer to add the tile's own props. A sublayer kept from the
1326
+ * tile's previous sublayers has them already, and stays the same instance.
1327
+ */
1328
+ clone(newProps: Record<string, unknown>) {
1329
+ if (keptTileLayers.has(this)) {
1330
+ keptTileLayers.delete(this);
1331
+ const props = this.props as Record<string, unknown>;
1332
+ if (Object.keys(newProps).every(key => props[key] === newProps[key])) {
1333
+ return this;
1334
+ }
1335
+ }
1336
+ const layer = super.clone(newProps);
1337
+ const inputs = tileLayerInputs.get(this);
1338
+ if (inputs) {
1339
+ tileLayerInputs.set(layer, 'tile' in newProps ? inputs : {...inputs, ...newProps});
1340
+ }
1341
+ return layer;
1342
+ }
1343
+
1344
+ protected shouldRenderSubLayer(subLayerId: string, data: any): boolean {
1345
+ if (
1346
+ subLayerId.startsWith('points-') ||
1347
+ (subLayerId === 'polygons-fill' && !this.props.filled) ||
1348
+ (subLayerId === 'polygons-stroke' && !this.props.stroked) ||
1349
+ (subLayerId === 'linestrings' && !this.props.drawsLineStrings)
1350
+ ) {
1351
+ return false;
1352
+ }
1353
+ return super.shouldRenderSubLayer(subLayerId, data);
1354
+ }
1355
+ }
1356
+
1357
+ /** The prop that hands a tile's previous sublayers, by id, to `renderSubLayers`. */
1358
+ const PREVIOUS_TILE_LAYERS = 'previousTileLayers';
1359
+ /** The props each tile sublayer was created with, for `getTileGeoJsonLayer`. */
1360
+ const tileLayerInputs = new WeakMap<object, Record<string, unknown>>();
1361
+ /** Previous tile sublayers that `getTileGeoJsonLayer` has just kept, until `TileLayer` adds them. */
1362
+ const keptTileLayers = new WeakSet<object>();
1363
+
1364
+ /**
1365
+ * Whether two prop values are the same to deck.gl: equal, or arrays and plain objects of the
1366
+ * same values, or objects that `equals` each other (deck.gl's layer extensions and matrices).
1367
+ */
1368
+ function isSameValue(a: any, b: any, depth = 3): boolean {
1369
+ if (a === b) {
1370
+ return true;
1371
+ }
1372
+ if (depth === 0 || !a || !b || typeof a !== 'object' || typeof b !== 'object') {
1373
+ return false;
1374
+ }
1375
+ if (Array.isArray(a) || ArrayBuffer.isView(a)) {
1376
+ if (!(Array.isArray(b) || ArrayBuffer.isView(b))) {
1377
+ return false;
1378
+ }
1379
+ const x = a as unknown as ArrayLike<unknown>;
1380
+ const y = b as unknown as ArrayLike<unknown>;
1381
+ if (x.length !== y.length) {
1382
+ return false;
1383
+ }
1384
+ for (let i = 0; i < x.length; i++) {
1385
+ if (!isSameValue(x[i], y[i], depth - 1)) {
1386
+ return false;
1387
+ }
1388
+ }
1389
+ return true;
1390
+ }
1391
+ if (Object.getPrototypeOf(a) !== Object.prototype) {
1392
+ return a.constructor === b.constructor && typeof a.equals === 'function' && a.equals(b);
1393
+ }
1394
+ // Functions in a nested object are not accessors: they must be the same.
1395
+ return Object.getPrototypeOf(b) === Object.prototype && isSameProps(a, b, depth - 1, false);
1396
+ }
1397
+
1398
+ /**
1399
+ * Whether two sets of layer props are the same to deck.gl. Per-feature accessors (`get*`) that
1400
+ * are both functions count as the same, unless `accessors` is false: with the same data, deck.gl
1401
+ * reads them again only when their update trigger, compared with the other props, changes.
1402
+ */
1403
+ export function isSameProps(
1404
+ a: Record<string, any>,
1405
+ b: Record<string, any>,
1406
+ depth = 3,
1407
+ accessors = true
1408
+ ): boolean {
1409
+ const keys = Object.keys(a);
1410
+ if (keys.length !== Object.keys(b).length) {
1411
+ return false;
1412
+ }
1413
+ for (const key of keys) {
1414
+ if (!(key in b)) {
1415
+ return false;
1416
+ }
1417
+ const x = a[key];
1418
+ const y = b[key];
1419
+ const isAccessor =
1420
+ accessors && key.startsWith('get') && typeof x === 'function' && typeof y === 'function';
1421
+ // deck.gl compares data by identity, and recomputes every accessor for new data.
1422
+ if (key === 'data' ? x !== y : !isAccessor && !isSameValue(x, y, depth)) {
1423
+ return false;
1424
+ }
1425
+ }
1426
+ return true;
1427
+ }
1428
+
1429
+ /**
1430
+ * A fill, line or fill-extrusion sublayer of a tile. Tile sublayers regenerate at every style
1431
+ * zoom step, while many of them, a constant fill or a line whose width does not change between
1432
+ * the two steps, get the same props as before. Such a sublayer keeps the instance the tile last
1433
+ * rendered, which deck.gl then neither diffs nor updates; a new one would be created, cloned by
1434
+ * `TileLayer`, matched and diffed, only to change nothing.
1435
+ */
1436
+ function getTileGeoJsonLayer(tileProps: any, props: Record<string, any>): StyledGeoJsonLayer {
1437
+ const previous = (tileProps[PREVIOUS_TILE_LAYERS] as Map<string, any> | undefined)?.get(props.id);
1438
+ const inputs = previous && tileLayerInputs.get(previous);
1439
+ if (inputs && isSameProps(props, inputs)) {
1440
+ keptTileLayers.add(previous);
1441
+ return previous;
1442
+ }
1443
+ const layer = new StyledGeoJsonLayer(props);
1444
+ tileLayerInputs.set(layer, props);
1445
+ return layer;
1446
+ }
1447
+
1448
+ /** Whether a style accessor is a constant, fully transparent color. */
1449
+ function isTransparent(color: StyleAccessor<number[]>): boolean {
1450
+ return typeof color.value !== 'function' && !(color.value[3] > 0);
1451
+ }
1452
+
1453
+ function createStyledVectorSubLayer({
1454
+ idPrefix,
1455
+ sourceId,
1456
+ styleLayer,
1457
+ features,
1458
+ props,
1459
+ zoom,
1460
+ config,
1461
+ mode,
1462
+ depthTested,
1463
+ collisionPriorityRange,
1464
+ spriteAtlases,
1465
+ loadOptions,
1466
+ fontFamily,
1467
+ glyphs,
1468
+ labelPlacement,
1469
+ light
1470
+ }: {
1471
+ idPrefix: string;
1472
+ sourceId: string;
1473
+ styleLayer: BasemapStyleLayer;
1474
+ features: any[];
1475
+ props: any;
1476
+ zoom: number;
1477
+ config: BasemapLayerConfig;
1478
+ mode: BasemapMode;
1479
+ /** Drawn after an extrusion, see `getTileParameters`. */
1480
+ depthTested?: boolean;
1481
+ collisionPriorityRange?: [number, number];
1482
+ spriteAtlases?: SpriteAtlas[] | null;
1483
+ loadOptions?: BasemapLoadOptions;
1484
+ fontFamily?: LabelFontFamily | null;
1485
+ /** The style's glyphs, which labels are drawn with (see `MVTLabelLayerProps.glyphs`). */
1486
+ glyphs?: GlyphStore | null;
1487
+ /** The label placement shared by the map's label layers (see `LabelPlacement`). */
1488
+ labelPlacement?: LabelPlacement | null;
1489
+ /** The style's `light`, which shades `fill-extrusion` layers. */
1490
+ light?: FillExtrusionLight;
1491
+ }) {
1492
+ if (features.length === 0) {
1493
+ return null;
1494
+ }
1495
+
1496
+ if (styleLayer.type === 'symbol') {
1497
+ const paint = getPaint(styleLayer, zoom);
1498
+ return createSymbolSubLayer({
1499
+ props,
1500
+ styleLayer,
1501
+ features,
1502
+ config,
1503
+ mode,
1504
+ zoom,
1505
+ opacity: 1,
1506
+ paint,
1507
+ collisionPriorityRange,
1508
+ spriteAtlases,
1509
+ loadOptions,
1510
+ fontFamily,
1511
+ glyphs,
1512
+ labelPlacement
1513
+ });
1514
+ }
1515
+
1516
+ if (styleLayer.type === 'circle') {
1517
+ return createCircleSubLayer({
1518
+ baseProps: getSubLayerBaseProps(props),
1519
+ id: getStyleSubLayerId(props.id, styleLayer.id),
1520
+ styleLayer,
1521
+ features,
1522
+ zoom,
1523
+ globe: mode === 'globe',
1524
+ // Depth-tested against the globe without writing depth: a circle's quad, blur and
1525
+ // antialiased edge included, would otherwise cut holes in the circles drawn after it.
1526
+ // Circles face the viewer, so back-face culling does not hide those on the far side.
1527
+ parameters: {
1528
+ ...getTileParameters(mode),
1529
+ depthTest: true,
1530
+ depthCompare: 'less-equal',
1531
+ depthWriteEnabled: false
1532
+ }
1533
+ });
1534
+ }
1535
+
1536
+ if (styleLayer.type === 'fill-extrusion') {
1537
+ return createExtrusionSubLayer({props, styleLayer, features, zoom, light});
1538
+ }
1539
+
1540
+ if (styleLayer.type === 'line') {
1541
+ return createLineSubLayers({props, styleLayer, features, mode, zoom, depthTested});
1542
+ }
1543
+
1544
+ if (styleLayer.type === 'fill' && styleLayer.paint?.['fill-pattern'] !== undefined) {
1545
+ const groups = getFillPatternGroups(features, styleLayer, zoom, spriteAtlases);
1546
+ if (groups.length > 0) {
1547
+ const layers = groups.map(pattern =>
1548
+ createGeometrySubLayer({
1549
+ props,
1550
+ styles: [
1551
+ getGeometryStyle({
1552
+ styleLayer,
1553
+ features: pattern.features,
1554
+ mode,
1555
+ zoom,
1556
+ depthTested,
1557
+ pattern
1558
+ })
1559
+ ],
1560
+ mode,
1561
+ idSuffix: groups.length > 1 ? `-pattern-${pattern.atlasIndex}` : ''
1562
+ })
1563
+ );
1564
+ // Features whose image is missing draw no fill, but keep their outline, as in MapLibre.
1565
+ const unpatterned = getFeaturesWithoutPattern(features, groups);
1566
+ if (unpatterned.length > 0 && styleLayer.paint?.['fill-outline-color'] !== undefined) {
1567
+ layers.push(
1568
+ createGeometrySubLayer({
1569
+ props,
1570
+ styles: [
1571
+ getGeometryStyle({styleLayer, features: unpatterned, mode, zoom, depthTested})
1572
+ ],
1573
+ mode,
1574
+ idSuffix: '-missing-pattern'
1575
+ })
1576
+ );
1577
+ }
1578
+ return layers;
1579
+ }
1580
+ }
1581
+
1582
+ return createGeometrySubLayer({
1583
+ props,
1584
+ styles: [getGeometryStyle({styleLayer, features, mode, zoom, depthTested})],
1585
+ mode
1586
+ });
1587
+ }
1588
+
1589
+ function createVectorLayerGroup({
1590
+ idPrefix,
1591
+ id,
1592
+ sourceId,
1593
+ source,
1594
+ tileset,
1595
+ styleLayers,
1596
+ zoom,
1597
+ config,
1598
+ loadOptions,
1599
+ mode,
1600
+ sourceTilesetOwner,
1601
+ depthTestedFrom = -1,
1602
+ labelPriorityRanges,
1603
+ styleDefinition,
1604
+ spriteAtlases,
1605
+ fontFamily,
1606
+ glyphs,
1607
+ labelPlacement
1608
+ }: {
1609
+ idPrefix: string;
1610
+ id: string;
1611
+ sourceId: string;
1612
+ source: BasemapSource;
1613
+ /** Shared with the source's other runs, if it has any. */
1614
+ tileset?: SharedTileset2D | null;
1615
+ sourceTilesetOwner?: SourceTilesetOwner;
1616
+ styleLayers: BasemapStyleLayer[];
1617
+ zoom: number;
1618
+ config: BasemapLayerConfig;
1619
+ loadOptions?: BasemapLoadOptions;
1620
+ mode: BasemapMode;
1621
+ /** The first of `styleLayers` drawn after an extrusion, if any; see `getTileParameters`. */
1622
+ depthTestedFrom?: number;
1623
+ labelPriorityRanges?: Map<BasemapStyleLayer, [number, number]>;
1624
+ /** The resolved style, compared by identity to regenerate tiles when the style changes. */
1625
+ styleDefinition?: ResolvedBasemapStyle;
1626
+ /** Sprites for `icon-image`; tiles regenerate when they finish loading. */
1627
+ spriteAtlases?: SpriteAtlas[] | null;
1628
+ /** Label font override; tiles regenerate when it changes. */
1629
+ fontFamily?: LabelFontFamily | null;
1630
+ /** The style's glyphs, which labels are drawn with (see `MVTLabelLayerProps.glyphs`). */
1631
+ glyphs?: GlyphStore | null;
1632
+ /** The label placement shared by the map's label layers (see `LabelPlacement`). */
1633
+ labelPlacement?: LabelPlacement | null;
1634
+ }) {
1635
+ const filterSnapshots = new FilterSnapshots();
1636
+
1637
+ return new StyledMVTLayer({
1638
+ id: `${idPrefix}-${id}`,
1639
+ data: tileset || source.tiles,
1640
+ ...{sourceTilesetOwner},
1641
+ // The tile pyramid's range; style layers are gated by their own range in renderSubLayers.
1642
+ ...getTilePyramidProps(source),
1643
+ // The zoom at which `renderSubLayers` evaluates paint, which `ZoomPaintExtension` also uses.
1644
+ styleZoom: zoom,
1645
+ // The style layers whose paint `ZoomPaintExtension` draws (see `ZoomPaintContext.setStyle`).
1646
+ paintStyle: styleDefinition?.layers,
1647
+ tileSize: source.tileSize || 512,
1648
+ zoomOffset: VECTOR_TILE_ZOOM_OFFSET,
1649
+ loadOptions: {
1650
+ ...(loadOptions || {}),
1651
+ mvt: {
1652
+ ...((loadOptions?.mvt as Record<string, unknown> | undefined) || {}),
1653
+ shape: 'geojson-table'
1654
+ }
1655
+ },
1656
+ onTileError: (error: unknown) => {
1657
+ logBasemapRuntimeError('Vector tile layer failed', error, {
1658
+ sourceId
1659
+ });
1660
+ },
1661
+ onTileLoad: (tile: Parameters<NonNullable<SharedMVTLayer['props']['onTileLoad']>>[0]) => {
1662
+ const features = Array.isArray(tile.content) ? tile.content : [];
1663
+ if (features.length === 0) {
1664
+ logBasemapRuntimeEvent('Loaded empty vector tile', {
1665
+ sourceId,
1666
+ tileIndex: tile.index
1667
+ });
1668
+ }
1669
+ },
1670
+ parameters: getTileParameters(mode),
1671
+ // `renderSubLayers` reads `zoom` and the style: regenerate tile sublayers at each style zoom
1672
+ // step (evaluation and filters), at fractional layer limits (visibility), and when the style
1673
+ // changes. Two styles can share a source id and so this layer's id; deck.gl compares the
1674
+ // style by identity.
1675
+ updateTriggers: {
1676
+ renderSubLayers: [
1677
+ getStyleZoomKey(zoom, getStyleZoomLimits(styleLayers)),
1678
+ // Changes when an extrusion in another run crosses its zoom limits.
1679
+ depthTestedFrom,
1680
+ styleDefinition,
1681
+ spriteAtlases,
1682
+ fontFamily,
1683
+ glyphs,
1684
+ labelPlacement
1685
+ ]
1686
+ },
1687
+ renderLabelLayers: ((tilesByStyleLayer, getSubLayerProps, hidden) =>
1688
+ [...tilesByStyleLayer].flatMap(([styleLayerIndex, tiles]) => {
1689
+ const styleLayer = styleLayers[styleLayerIndex];
1690
+ // An index the tile layer kept from another style has no style layer here.
1691
+ if (!styleLayer) {
1692
+ return [];
1693
+ }
1694
+ return createSymbolSubLayer({
1695
+ props: {
1696
+ ...getSubLayerProps(`labels-${JSON.stringify(styleLayer.id)}`),
1697
+ // A label layer kept without features (see `renderViewLabelLayers`) is not drawn.
1698
+ ...(hidden.has(styleLayerIndex) ? {visible: false} : {}),
1699
+ [STYLE_LAYER_INDEX]: styleLayerIndex
1700
+ },
1701
+ styleLayer,
1702
+ features: [],
1703
+ tiles,
1704
+ config,
1705
+ mode,
1706
+ zoom,
1707
+ opacity: 1,
1708
+ paint: getPaint(styleLayer, zoom),
1709
+ collisionPriorityRange: labelPriorityRanges?.get(styleLayer),
1710
+ spriteAtlases,
1711
+ loadOptions,
1712
+ fontFamily,
1713
+ glyphs,
1714
+ labelPlacement
1715
+ });
1716
+ })) as RenderLabelLayers,
1717
+ renderSubLayers: (props: Parameters<SharedMVTLayer['renderSubLayers']>[0]) => {
1718
+ const styleProps = props as typeof props & {
1719
+ styleAutoHighlight?: boolean;
1720
+ highlightedFeatureId?: string | number | null;
1721
+ zoomPaintContext?: ZoomPaintContext | null;
1722
+ };
1723
+ const features = getTileFeatures(props.data);
1724
+ const layers: any[] = [];
1725
+ // Where the tile layer collects them, a tile's labels are drawn with the other tiles'.
1726
+ const labelFeatures = (props as any)[TILE_LABEL_FEATURES] as Map<number, any[]> | undefined;
1727
+ const highlights =
1728
+ Boolean(styleProps[AUTO_HIGHLIGHT]) ||
1729
+ (styleProps.highlightedFeatureId !== null && styleProps.highlightedFeatureId !== undefined);
1730
+ // Fills and lines take zoom-dependent paint from a texture (see `createGeometrySubLayer`).
1731
+ const zoomPaintContext = styleProps[ZOOM_PAINT_CONTEXT];
1732
+ // Consecutive fills and lines that can share a sublayer (see `getBatchKey`), and the index
1733
+ // of the first, which places the batch among all tiles' sublayers (`sortTileSubLayers`).
1734
+ let batch: GeometryStyle[] = [];
1735
+ let batchIndex = 0;
1736
+ const flush = () => {
1737
+ if (batch.length > 0) {
1738
+ layers.push(
1739
+ createGeometrySubLayer({
1740
+ props: {...props, [STYLE_LAYER_INDEX]: batchIndex},
1741
+ styles: batch,
1742
+ mode
1743
+ })
1744
+ );
1745
+ batch = [];
1746
+ }
1747
+ };
1748
+
1749
+ styleLayers.forEach((styleLayer, styleLayerIndex) => {
1750
+ if (!isStyleLayerVisibleAtZoom(styleLayer, zoom)) {
1751
+ return;
1752
+ }
1753
+
1754
+ const filteredData = filterTileFeatures(features, styleLayer, zoom, filterSnapshots);
1755
+
1756
+ if (features.length > 0 && filteredData.length === 0) {
1757
+ logBasemapRuntimeEvent('Vector tile rendered no matching features', {
1758
+ sourceId,
1759
+ layerId: styleLayer.id,
1760
+ sourceLayer: styleLayer['source-layer'],
1761
+ tileIndex: props.tile?.index
1762
+ });
1763
+ }
1764
+ if (filteredData.length === 0) {
1765
+ if (styleLayer.type === 'symbol' && labelFeatures) {
1766
+ // The tile still covers its parent's labels (see `combineTileRows`).
1767
+ labelFeatures.set(styleLayerIndex, NO_FEATURES);
1768
+ return;
1769
+ }
1770
+ // A fill or line without features in this tile does not separate its neighbours. Any
1771
+ // other style layer does, even here: it may draw in a neighbouring tile, and all tiles
1772
+ // draw it after this batch, as in MapLibre.
1773
+ if (styleLayer.type !== 'fill' && styleLayer.type !== 'line') {
1774
+ flush();
1775
+ }
1776
+ return;
1777
+ }
1778
+
1779
+ const depthTested = depthTestedFrom >= 0 && styleLayerIndex >= depthTestedFrom;
1780
+ // A sublayer draws its features in order: a line layer's, in the order of their keys.
1781
+ const layerFeatures =
1782
+ styleLayer.type === 'line'
1783
+ ? sortFeaturesByKey(filteredData, styleLayer, 'line-sort-key', zoom)
1784
+ : filteredData;
1785
+ const style = getBatchableGeometryStyle(styleLayer, layerFeatures, mode, zoom, depthTested);
1786
+ if (style) {
1787
+ // A style layer that draws nothing in this tile does not separate its neighbours. A line
1788
+ // that draws nothing only because its width is 0 at this zoom step, and takes its width
1789
+ // from `ZoomPaintExtension`, is batched as at a step that gives it a width: its sublayer
1790
+ // and batch stay the same across the step, and are not made again (see
1791
+ // `createGeometrySubLayer`, which hides a sublayer that draws nothing).
1792
+ const isKeptAtZeroWidth = style.isZeroWidth && Boolean(zoomPaintContext);
1793
+ if (style.drawsFill || style.drawsLines || isKeptAtZeroWidth) {
1794
+ // A batch draws all of its style layers in one tile before the next tile. On a map,
1795
+ // tiles are clipped to their bounds, so that draws the same pixels as each style layer
1796
+ // across all tiles in turn; on the globe, tiles are not clipped, and where their
1797
+ // buffers overlap a tile's later style layer would draw under a neighbour's earlier one.
1798
+ // A highlight colors one object of a sublayer: in a batch, a feature drawn by two
1799
+ // style layers would be highlighted only under the second one's copy.
1800
+ const key = mode === 'globe' || highlights ? null : getBatchKey(style);
1801
+ if (!canBatch(batch, style, key)) {
1802
+ flush();
1803
+ batchIndex = styleLayerIndex;
1804
+ }
1805
+ batch.push(style);
1806
+ if (key === null) {
1807
+ flush();
1808
+ }
1809
+ }
1810
+ return;
1811
+ }
1812
+
1813
+ if (styleLayer.type === 'symbol' && labelFeatures) {
1814
+ // Drawn with the labels of the other tiles, after all tiles' sublayers.
1815
+ labelFeatures.set(styleLayerIndex, filteredData);
1816
+ return;
1817
+ }
1818
+ flush();
1819
+ layers.push(
1820
+ createStyledVectorSubLayer({
1821
+ idPrefix,
1822
+ sourceId,
1823
+ styleLayer,
1824
+ features: layerFeatures,
1825
+ props: {...props, [STYLE_LAYER_INDEX]: styleLayerIndex},
1826
+ zoom,
1827
+ config,
1828
+ mode,
1829
+ depthTested,
1830
+ collisionPriorityRange: labelPriorityRanges?.get(styleLayer),
1831
+ spriteAtlases,
1832
+ loadOptions,
1833
+ fontFamily,
1834
+ glyphs,
1835
+ labelPlacement,
1836
+ light: getFillExtrusionLight(styleDefinition, zoom)
1837
+ })
1838
+ );
1839
+ });
1840
+ flush();
1841
+
1842
+ return layers.flat().filter(layer => Boolean(layer)) as any;
1843
+ }
1844
+ } as any);
1845
+ }
1846
+
1847
+ function inferWaterColor(
1848
+ styleLayers: BasemapStyleLayer[],
1849
+ zoom: number
1850
+ ): [number, number, number, number] {
1851
+ const waterLayer = styleLayers.find(
1852
+ layer => layer.type === 'fill' && `${layer['source-layer'] || ''}`.includes('water')
1853
+ );
1854
+
1855
+ if (!waterLayer) {
1856
+ return [20, 40, 68, 255];
1857
+ }
1858
+
1859
+ const paint = getPaint(waterLayer, zoom);
1860
+ return withOpacity(paint['fill-color'], paint['fill-opacity'] ?? 1);
1861
+ }
1862
+
1863
+ /** All style-layer `minzoom`/`maxzoom` limits in a style. */
1864
+ export function getStyleDefinitionZoomLimits(
1865
+ styleDefinition: BasemapLayerGroup['styleDefinition']
1866
+ ): (number | undefined)[] {
1867
+ return getStyleZoomLimits(styleDefinition.layers || []);
1868
+ }
1869
+
1870
+ export function getBasemapLayers({
1871
+ idPrefix = 'basemap',
1872
+ mode = 'map',
1873
+ globe,
1874
+ styleDefinition,
1875
+ zoom = 0,
1876
+ loadOptions,
1877
+ spriteAtlases,
1878
+ fontFamily,
1879
+ glyphs,
1880
+ labelPlacement,
1881
+ tilesets
1882
+ }: BasemapLayerGroup) {
1883
+ const config = getConfig(globe);
1884
+ const styleLayers = (styleDefinition.layers || []).filter(layer =>
1885
+ SUPPORTED_TYPES.has(layer.type)
1886
+ );
1887
+ const layers: any[] = [];
1888
+ logBasemapRuntimeEvent('Generating basemap layers', {
1889
+ mode,
1890
+ styleLayerCount: styleLayers.length,
1891
+ sourceCount: Object.keys(styleDefinition.sources || {}).length
1892
+ });
1893
+
1894
+ layers.push(...getGlobePreLayers({idPrefix, mode, config, styleLayers, zoom}));
1895
+
1896
+ if (config.basemap) {
1897
+ layers.push(
1898
+ ...getStyleLayers({
1899
+ idPrefix,
1900
+ styleLayers,
1901
+ styleDefinition,
1902
+ zoom,
1903
+ config,
1904
+ loadOptions,
1905
+ mode,
1906
+ spriteAtlases,
1907
+ fontFamily,
1908
+ glyphs,
1909
+ labelPlacement,
1910
+ tilesets
1911
+ })
1912
+ );
1913
+ }
1914
+
1915
+ layers.push(...getGlobePostLayers({idPrefix, mode, config, styleLayers, zoom}));
1916
+
1917
+ return layers.filter(layer => Boolean(layer));
1918
+ }
1919
+
1920
+ export function getGlobeBaseLayers({
1921
+ globe,
1922
+ styleDefinition,
1923
+ idPrefix = 'globe-basemap',
1924
+ zoom = 0,
1925
+ loadOptions,
1926
+ spriteAtlases,
1927
+ fontFamily,
1928
+ glyphs,
1929
+ labelPlacement
1930
+ }: Omit<BasemapLayerGroup, 'mode'>) {
1931
+ return getBasemapLayers({
1932
+ idPrefix,
1933
+ mode: 'globe',
1934
+ globe,
1935
+ styleDefinition,
1936
+ zoom,
1937
+ loadOptions,
1938
+ spriteAtlases,
1939
+ fontFamily,
1940
+ glyphs,
1941
+ labelPlacement
1942
+ });
1943
+ }
1944
+
1945
+ export function getGlobeTopLayers({globe}: {globe: {config: BasemapGlobeConfig}}) {
1946
+ const {config} = globe;
1947
+ return config.atmosphere ? [getGlobeAtmosphereLayer()] : [];
1948
+ }
1949
+
1950
+ function createSymbolSubLayer({
1951
+ props,
1952
+ styleLayer,
1953
+ features,
1954
+ config,
1955
+ mode,
1956
+ zoom,
1957
+ opacity,
1958
+ paint,
1959
+ collisionPriorityRange,
1960
+ spriteAtlases,
1961
+ loadOptions,
1962
+ fontFamily,
1963
+ glyphs,
1964
+ labelPlacement,
1965
+ tiles
1966
+ }: {
1967
+ props: any;
1968
+ styleLayer: BasemapStyleLayer;
1969
+ features: any[];
1970
+ /** The label features of several tiles, drawn together instead of `features`. */
1971
+ tiles?: LabelTile[];
1972
+ config: BasemapLayerConfig;
1973
+ mode: BasemapMode;
1974
+ zoom: number;
1975
+ opacity: number;
1976
+ paint: Record<string, any>;
1977
+ collisionPriorityRange?: [number, number];
1978
+ spriteAtlases?: SpriteAtlas[] | null;
1979
+ loadOptions?: BasemapLoadOptions;
1980
+ fontFamily?: LabelFontFamily | null;
1981
+ /** The style's glyphs, which labels are drawn with (see `MVTLabelLayerProps.glyphs`). */
1982
+ glyphs?: GlyphStore | null;
1983
+ /** The label placement shared by the map's label layers (see `LabelPlacement`). */
1984
+ labelPlacement?: LabelPlacement | null;
1985
+ }) {
1986
+ return new MVTLabelLayer({
1987
+ ...getSubLayerBaseProps(props),
1988
+ id: tiles ? props.id : getStyleSubLayerId(props.id, styleLayer.id),
1989
+ ...(tiles ? {tiles} : {data: features}),
1990
+ config,
1991
+ mode,
1992
+ styleLayer,
1993
+ collisionPriorityRange,
1994
+ spriteAtlases,
1995
+ // The sprite atlas images load through the same fetch as the style and its tiles.
1996
+ iconLoadOptions: loadOptions,
1997
+ fontFamily,
1998
+ glyphs,
1999
+ labelPlacement,
2000
+ // Polygon labels outside the tile's own extent are left to the neighbouring tile.
2001
+ tileBoundingBox: props.tile?.bbox ?? null,
2002
+ // Labels along lines are spaced in pixels at the tile's own zoom.
2003
+ tileZoom: props.tile?.index?.z ?? null,
2004
+ tileSize: props.tileSize ?? null,
2005
+ zoom: getZoomBucket(zoom),
2006
+ // The style spec's default `text-color` is black.
2007
+ textColor: withOpacity(paint['text-color'] ?? DEFAULT_TEXT_COLOR, opacity),
2008
+ billboard: true
2009
+ });
2010
+ }
2011
+
2012
+ /**
2013
+ * Converts a style `line-dasharray` into deck.gl's `[dash, gap]`. MapLibre measures dashes in line
2014
+ * widths; deck.gl's `widths` unit is half the line width, so lengths double. deck.gl draws one dash
2015
+ * and one gap per period, so a longer pattern keeps its period and total dash length, but merges
2016
+ * its dashes. In an odd-length pattern the last dash runs into the first, as in MapLibre. An empty
2017
+ * pattern, or one without gaps, draws a solid line.
2018
+ */
2019
+ export function getDashArray(dasharray: unknown): [number, number] {
2020
+ if (!Array.isArray(dasharray)) {
2021
+ return [0, 0];
2022
+ }
2023
+ let dash = 0;
2024
+ let gap = 0;
2025
+ dasharray.forEach((value, index) => {
2026
+ const length = Math.max(0, Number(value) || 0);
2027
+ if (index % 2 === 0) {
2028
+ dash += length;
2029
+ } else {
2030
+ gap += length;
2031
+ }
2032
+ });
2033
+ return [dash * 2, gap * 2];
2034
+ }
2035
+
2036
+ /** The least distance, in pixels, of a gap's line from the gap's center: see getLineOffset. */
2037
+ export const MIN_GAP_LINE_CENTER = 0.01;
2038
+
2039
+ /**
2040
+ * Converts `line-offset` and `line-gap-width` into deck.gl's `getOffset`, which is in line widths.
2041
+ * A gap draws two lines of `width` with their centers `gapWidth / 2 + width / 2` either side of
2042
+ * the offset line: `side` is 1 or -1 for them. Without a gap, `side` is ignored. Positive is to
2043
+ * the right of the line's direction in both. Dividing by `width` assumes deck.gl draws the line
2044
+ * `width` pixels wide, that is, no `lineWidthMinPixels`/`lineWidthMaxPixels` clamp applies.
2045
+ *
2046
+ * deck.gl offsets by widening the stroke and masking it across the line, not by offsetting the
2047
+ * path, so the outside of a sharp corner breaks open and the two lines of a gap end in separate
2048
+ * stubs rather than one cap. Offsetting the path belongs in deck.gl's `PathStyleExtension`.
2049
+ */
2050
+ export function getLineOffset(
2051
+ offset: number,
2052
+ gapWidth: number,
2053
+ width: number,
2054
+ side: number,
2055
+ blur = 0
2056
+ ): number {
2057
+ if (!(width > 0)) {
2058
+ return 0;
2059
+ }
2060
+ // A blurred gap's lines are drawn `blur` wider and `blur / 2` closer: see getBlurredLineWidth.
2061
+ const drawnWidth = getBlurredLineWidth(width, gapWidth, blur);
2062
+ let center = gapWidth > 0 ? gapWidth / 2 + width - drawnWidth / 2 : 0;
2063
+ // A line of a gap is never centered on the gap's center, where it would be as wide as
2064
+ // `line-blur` is `line-width` plus `line-gap-width`: LineBlurExtension clips only lines off
2065
+ // the center, since a line without a gap is centered on it.
2066
+ if (gapWidth > 0 && Math.abs(center) < MIN_GAP_LINE_CENTER) {
2067
+ center = MIN_GAP_LINE_CENTER;
2068
+ }
2069
+ return (offset + side * center) / drawnWidth;
2070
+ }
2071
+
2072
+ /**
2073
+ * Scales a blurred gap's `getDashArray` by `width / drawnWidth` (see `getBlurredLineWidth`), so
2074
+ * that its dashes keep the pixel lengths MapLibre gives them in `line-width`s. `shape` is the
2075
+ * `[offset, gap, width, blur]` accessor of `getLineOffset`.
2076
+ */
2077
+ export function getBlurredDashArray(
2078
+ dashArray: StyleAccessor<[number, number]>,
2079
+ shape: StyleAccessor<[number, number, number, number]>
2080
+ ): {value: [number, number] | ((feature: any) => [number, number]); updateTrigger?: unknown[]} {
2081
+ const scale = (
2082
+ [dash, gap]: [number, number],
2083
+ [, gapWidth, width, blur]: [number, number, number, number]
2084
+ ): [number, number] => {
2085
+ const drawnWidth = getBlurredLineWidth(width, gapWidth, blur);
2086
+ const ratio = drawnWidth > 0 ? width / drawnWidth : 1;
2087
+ return [dash * ratio, gap * ratio];
2088
+ };
2089
+ const dashValue = dashArray.value;
2090
+ const shapeValue = shape.value;
2091
+ if (typeof dashValue !== 'function' && typeof shapeValue !== 'function') {
2092
+ return {value: scale(dashValue, shapeValue)};
2093
+ }
2094
+ return {
2095
+ value: (feature: any) =>
2096
+ scale(
2097
+ typeof dashValue === 'function' ? dashValue(feature) : dashValue,
2098
+ typeof shapeValue === 'function' ? shapeValue(feature) : shapeValue
2099
+ ),
2100
+ // The parts that are constants change by value; the others have their triggers.
2101
+ updateTrigger: [
2102
+ ...(typeof dashValue === 'function' ? [dashArray.updateTrigger] : dashValue),
2103
+ ...(typeof shapeValue === 'function' ? [shape.updateTrigger] : shapeValue)
2104
+ ]
2105
+ };
2106
+ }
2107
+
2108
+ /** deck.gl path props for a line's `line-cap`, `line-join` and `line-miter-limit`. */
2109
+ export type LineShape = {
2110
+ lineCapRounded: boolean;
2111
+ lineJointRounded: boolean;
2112
+ lineMiterLimit: number;
2113
+ };
2114
+
2115
+ /**
2116
+ * Converts `line-cap`, `line-join` and `line-miter-limit` into deck.gl path props.
2117
+ *
2118
+ * - `round` caps and joins are drawn round. `butt` caps end at the endpoint. deck.gl has no square
2119
+ * cap, so `square` is drawn as `butt`: the line ends half its width short of MapLibre's.
2120
+ * - deck.gl cuts a joint flat at `miterLimit + 1` half widths from the vertex. MapLibre draws a
2121
+ * `miter` join in full while its tip is within `line-miter-limit` half widths of the vertex, so
2122
+ * passing `line-miter-limit - 1` draws the same joins in full. A sharper corner, which MapLibre
2123
+ * bevels, is cut flat at that distance instead.
2124
+ * - deck.gl has no bevel join. A `bevel` join is cut flat half the line width from the vertex,
2125
+ * which matches a bevel at shallow corners; at sharp corners a true bevel cuts closer.
2126
+ */
2127
+ export function getLineShape([cap, join, miterLimit]: unknown[]): LineShape {
2128
+ const lineJoin = join ?? getStylePropertyDefault('line-join');
2129
+ const limit = Number(miterLimit ?? getStylePropertyDefault('line-miter-limit'));
2130
+ return {
2131
+ lineCapRounded: (cap ?? getStylePropertyDefault('line-cap')) === 'round',
2132
+ lineJointRounded: lineJoin === 'round',
2133
+ lineMiterLimit: lineJoin === 'miter' && Number.isFinite(limit) ? Math.max(0, limit - 1) : 0
2134
+ };
2135
+ }
2136
+
2137
+ /** The sublayers for a `line` style layer: one, or one per shape when the shape is per feature. */
2138
+ function createLineSubLayers({
2139
+ props,
2140
+ styleLayer,
2141
+ features,
2142
+ mode,
2143
+ zoom,
2144
+ depthTested
2145
+ }: {
2146
+ props: any;
2147
+ styleLayer: BasemapStyleLayer;
2148
+ features: any[];
2149
+ mode: BasemapMode;
2150
+ zoom: number;
2151
+ depthTested?: boolean;
2152
+ }) {
2153
+ const lineShape = getLineShapeAccessor(styleLayer, zoom);
2154
+ if (typeof lineShape.value !== 'function') {
2155
+ return createGeometrySubLayer({
2156
+ props,
2157
+ styles: [
2158
+ getGeometryStyle({
2159
+ styleLayer,
2160
+ features,
2161
+ mode,
2162
+ zoom,
2163
+ depthTested,
2164
+ lineShape: lineShape.value
2165
+ })
2166
+ ],
2167
+ mode
2168
+ });
2169
+ }
2170
+
2171
+ return getLineShapeGroups(features, styleLayer, lineShape).map(group =>
2172
+ createGeometrySubLayer({
2173
+ props,
2174
+ styles: [
2175
+ getGeometryStyle({
2176
+ styleLayer,
2177
+ features: group.features,
2178
+ mode,
2179
+ zoom,
2180
+ depthTested,
2181
+ lineShape: group.shape
2182
+ })
2183
+ ],
2184
+ mode,
2185
+ idSuffix: `-${group.key}`
2186
+ })
2187
+ );
2188
+ }
2189
+
2190
+ function getLineShapeAccessor(styleLayer: BasemapStyleLayer, zoom: number) {
2191
+ return getStyleAccessor(
2192
+ styleLayer,
2193
+ ['line-cap', 'line-join', 'line-miter-limit'],
2194
+ zoom,
2195
+ getLineShape
2196
+ );
2197
+ }
2198
+
2199
+ /**
2200
+ * The style of a fill, or of a line with one shape for all features: the style layers that can
2201
+ * share a sublayer with their neighbours (see `getBatchKey`). Null for other style layers.
2202
+ */
2203
+ function getBatchableGeometryStyle(
2204
+ styleLayer: BasemapStyleLayer,
2205
+ features: any[],
2206
+ mode: BasemapMode,
2207
+ zoom: number,
2208
+ depthTested: boolean
2209
+ ): GeometryStyle | null {
2210
+ if (styleLayer.type === 'fill') {
2211
+ // A patterned fill draws a sublayer per sprite atlas of its images (see `getFillPatternGroups`).
2212
+ return styleLayer.paint?.['fill-pattern'] !== undefined
2213
+ ? null
2214
+ : getGeometryStyle({styleLayer, features, mode, zoom, depthTested});
2215
+ }
2216
+ if (styleLayer.type !== 'line') {
2217
+ return null;
2218
+ }
2219
+ const lineShape = getLineShapeAccessor(styleLayer, zoom);
2220
+ return typeof lineShape.value === 'function'
2221
+ ? null
2222
+ : getGeometryStyle({styleLayer, features, mode, zoom, depthTested, lineShape: lineShape.value});
2223
+ }
2224
+
2225
+ type LineShapeGroup = {key: string; shape: LineShape; features: any[]};
2226
+
2227
+ /** The latest line shape groups per tile content and style layer. */
2228
+ const lineShapeGroupCache = new WeakMap<
2229
+ any[],
2230
+ WeakMap<BasemapStyleLayer, {key: unknown[]; groups: LineShapeGroup[]}>
2231
+ >();
2232
+
2233
+ /**
2234
+ * Splits features by their per-feature line shape, since deck.gl applies caps and joins to a
2235
+ * whole layer. Groups are in the order each shape first appears, so features draw group by group
2236
+ * rather than in source order. The groups, and so their feature arrays, are reused while the
2237
+ * layout values (and, when they depend on zoom, the style zoom) are unchanged, so deck.gl does not
2238
+ * re-tessellate the features at every zoom step.
2239
+ */
2240
+ function getLineShapeGroups(
2241
+ features: any[],
2242
+ styleLayer: BasemapStyleLayer,
2243
+ lineShape: StyleAccessor<LineShape>
2244
+ ): LineShapeGroup[] {
2245
+ const key = [
2246
+ styleLayer.layout?.['line-cap'],
2247
+ styleLayer.layout?.['line-join'],
2248
+ styleLayer.layout?.['line-miter-limit'],
2249
+ lineShape.updateTrigger
2250
+ ];
2251
+ let byStyleLayer = lineShapeGroupCache.get(features);
2252
+ if (!byStyleLayer) {
2253
+ byStyleLayer = new WeakMap();
2254
+ lineShapeGroupCache.set(features, byStyleLayer);
2255
+ }
2256
+ const cached = byStyleLayer.get(styleLayer);
2257
+ if (cached && cached.key.every((entry, index) => entry === key[index])) {
2258
+ return cached.groups;
2259
+ }
2260
+
2261
+ const getShape = lineShape.value as (feature: any) => LineShape;
2262
+ const groups = new Map<string, LineShapeGroup>();
2263
+ for (const feature of features) {
2264
+ const shape = getShape(feature);
2265
+ const groupKey = `${shape.lineCapRounded}-${shape.lineJointRounded}-${shape.lineMiterLimit}`;
2266
+ let group = groups.get(groupKey);
2267
+ if (!group) {
2268
+ group = {key: groupKey, shape, features: []};
2269
+ groups.set(groupKey, group);
2270
+ }
2271
+ group.features.push(feature);
2272
+ }
2273
+ const result = [...groups.values()];
2274
+ byStyleLayer.set(styleLayer, {key, groups: result});
2275
+ return result;
2276
+ }
2277
+
2278
+ /**
2279
+ * The extensions and props of `fill-pattern` (with the features' images) and `fill-translate`
2280
+ * (when the layer sets it). `fill-translate` is evaluated at the stepped zoom; it cannot depend on
2281
+ * feature data.
2282
+ */
2283
+ function getFillStyleProps(
2284
+ styleLayer: BasemapStyleLayer,
2285
+ zoom: number,
2286
+ extensions: any[] | undefined,
2287
+ pattern: FillPatternGroup | undefined
2288
+ ) {
2289
+ const hasTranslate = styleLayer.paint?.['fill-translate'] !== undefined;
2290
+ if (!pattern && !hasTranslate) {
2291
+ return {};
2292
+ }
2293
+ const paint = getPaint(styleLayer, zoom);
2294
+ return {
2295
+ extensions: [
2296
+ ...(extensions || []),
2297
+ ...(pattern ? [FILL_PATTERN_EXTENSION] : []),
2298
+ ...(hasTranslate ? [FILL_TRANSLATE_EXTENSION] : [])
2299
+ ],
2300
+ ...(pattern ? getFillPatternProps(pattern, zoom) : {}),
2301
+ ...(hasTranslate
2302
+ ? {
2303
+ fillTranslate: paint['fill-translate'] ?? getStylePropertyDefault('fill-translate'),
2304
+ fillTranslateAnchor:
2305
+ paint['fill-translate-anchor'] ?? getStylePropertyDefault('fill-translate-anchor')
2306
+ }
2307
+ : {})
2308
+ };
2309
+ }
2310
+
2311
+ /** A fill or line style layer's evaluated paint for the features of one tile. */
2312
+ type GeometryStyle = {
2313
+ styleLayer: BasemapStyleLayer;
2314
+ features: any[];
2315
+ /** The stepped zoom the paint was evaluated at. */
2316
+ zoom: number;
2317
+ isLine: boolean;
2318
+ isFill: boolean;
2319
+ drawsFill: boolean;
2320
+ drawsLines: boolean;
2321
+ /**
2322
+ * Whether the line draws nothing only because its width is 0 at this zoom step. With its paint
2323
+ * from `ZoomPaintExtension`, it keeps its place in its sublayer (see `renderSubLayers`).
2324
+ */
2325
+ isZeroWidth: boolean;
2326
+ fillColor: StyleAccessor<number[]>;
2327
+ lineColor: StyleAccessor<number[]>;
2328
+ lineWidth: StyleAccessor<number>;
2329
+ dashArray: StyleAccessor<[number, number]> | null;
2330
+ lineOffset: StyleAccessor<[number, number, number, number]> | null;
2331
+ gapWidth: StyleAccessor<number> | null;
2332
+ lineBlur: StyleAccessor<number> | null;
2333
+ /** The `line-offset` at a blurred gap's center, which each side of the gap is drawn from. */
2334
+ gapOffset: StyleAccessor<number> | null;
2335
+ /** Whether the line is blurred and has a gap, so it draws wider (see `getBlurredLineWidth`). */
2336
+ isBlurredGap: boolean;
2337
+ /** Whether the fill sets `fill-pattern` or `fill-translate`, which take their own extensions. */
2338
+ hasFillStyle: boolean;
2339
+ lineShape?: LineShape;
2340
+ /** The features' `fill-pattern` images; without it, a patterned fill draws no fill. */
2341
+ pattern?: FillPatternGroup;
2342
+ /** Whether the style layer draws after an extrusion (see `getTileParameters`). */
2343
+ depthTested?: boolean;
2344
+ };
2345
+
2346
+ /** The paint of a fill or line style layer that `ZoomPaintExtension` can apply. */
2347
+ type GeometryPaint = {
2348
+ fillColor: StyleAccessor<number[]>;
2349
+ lineColor: StyleAccessor<number[]>;
2350
+ lineWidth: StyleAccessor<number>;
2351
+ /** Whether a fill draws its outline. */
2352
+ hasOutline: boolean;
2353
+ };
2354
+
2355
+ function getGeometryPaint(
2356
+ styleLayer: BasemapStyleLayer,
2357
+ mode: BasemapMode,
2358
+ zoom: number,
2359
+ pattern?: FillPatternGroup
2360
+ ): GeometryPaint {
2361
+ const isLine = styleLayer.type === 'line';
2362
+ const isFill = styleLayer.type === 'fill';
2363
+
2364
+ // A `fill-pattern` disables `fill-color`: the image supplies the colors and `fill-opacity` the
2365
+ // opacity, also on a globe. Without its images (sprites loading, or missing) the fill draws
2366
+ // nothing rather than its color or the default black.
2367
+ const hasFillPattern = isFill && styleLayer.paint?.['fill-pattern'] !== undefined;
2368
+ const fillColor = hasFillPattern
2369
+ ? pattern
2370
+ ? getStyleAccessor(styleLayer, ['fill-opacity'], zoom, ([opacity]) =>
2371
+ withOpacity([255, 255, 255, 1], opacity ?? 1)
2372
+ )
2373
+ : {value: TRANSPARENT_COLOR}
2374
+ : getStyleAccessor(styleLayer, ['fill-color', 'fill-opacity'], zoom, ([color, opacity]) =>
2375
+ getGlobeFillColor(
2376
+ withOpacity(color ?? getStylePropertyDefault('fill-color'), opacity ?? 1),
2377
+ mode
2378
+ )
2379
+ );
2380
+ // MapLibre draws a fill's outline only with `fill-antialias`; without `fill-outline-color` the
2381
+ // outline is in `fill-color` and only antialiases the edge, which deck.gl's fill does not need.
2382
+ const hasOutline =
2383
+ isFill &&
2384
+ styleLayer.paint?.['fill-outline-color'] !== undefined &&
2385
+ getPaint(styleLayer, zoom)['fill-antialias'] !== false;
2386
+ const lineColor = isLine
2387
+ ? getStyleAccessor(styleLayer, ['line-color', 'line-opacity'], zoom, ([color, opacity]) =>
2388
+ withOpacity(color ?? DEFAULT_COLOR, opacity ?? 1)
2389
+ )
2390
+ : getStyleAccessor(
2391
+ styleLayer,
2392
+ ['fill-outline-color', 'fill-opacity'],
2393
+ zoom,
2394
+ ([color, opacity]) => withOpacity(color, opacity ?? 1)
2395
+ );
2396
+ // A fill's outline is MapLibre's 1 pixel antialiasing line.
2397
+ const lineWidth: StyleAccessor<number> = isLine
2398
+ ? getStyleAccessor(styleLayer, ['line-width'], zoom, ([width]) =>
2399
+ Math.max(0, Number(width ?? 1))
2400
+ )
2401
+ : {value: hasOutline ? 1 : 0};
2402
+ return {fillColor, lineColor, lineWidth, hasOutline};
2403
+ }
2404
+
2405
+ function getGeometryStyle({
2406
+ styleLayer,
2407
+ features,
2408
+ mode,
2409
+ zoom,
2410
+ depthTested,
2411
+ lineShape,
2412
+ pattern
2413
+ }: {
2414
+ styleLayer: BasemapStyleLayer;
2415
+ features: any[];
2416
+ mode: BasemapMode;
2417
+ zoom: number;
2418
+ depthTested?: boolean;
2419
+ lineShape?: LineShape;
2420
+ pattern?: FillPatternGroup;
2421
+ }): GeometryStyle {
2422
+ const isLine = styleLayer.type === 'line';
2423
+ const isFill = styleLayer.type === 'fill';
2424
+
2425
+ const paint = getGeometryPaint(styleLayer, mode, zoom, pattern);
2426
+ const {fillColor, lineColor, hasOutline} = paint;
2427
+ const hasDashArray = isLine && styleLayer.paint?.['line-dasharray'] !== undefined;
2428
+ const hasOffset =
2429
+ isLine &&
2430
+ (styleLayer.paint?.['line-offset'] !== undefined ||
2431
+ styleLayer.paint?.['line-gap-width'] !== undefined);
2432
+ const styleBlur =
2433
+ isLine && styleLayer.paint?.['line-blur'] !== undefined
2434
+ ? getStyleAccessor(styleLayer, ['line-blur'], zoom, ([blur]) =>
2435
+ Math.max(0, Number(blur) || 0)
2436
+ )
2437
+ : null;
2438
+ // A per-feature blur takes a vertex attribute, which a path with dashes and offsets has no room
2439
+ // for (see LineBlurExtension): such lines draw sharp.
2440
+ const lineBlur =
2441
+ styleBlur && (typeof styleBlur.value !== 'function' || !(hasDashArray && hasOffset))
2442
+ ? styleBlur
2443
+ : null;
2444
+ const hasBlur = lineBlur !== null;
2445
+ // A blurred gap's lines are drawn wider: see getBlurredLineWidth.
2446
+ const isBlurredGap = hasBlur && styleLayer.paint?.['line-gap-width'] !== undefined;
2447
+ const lineWidth = isBlurredGap
2448
+ ? getStyleAccessor(
2449
+ styleLayer,
2450
+ ['line-width', 'line-gap-width', 'line-blur'],
2451
+ zoom,
2452
+ ([width, gap, blur]) =>
2453
+ getBlurredLineWidth(
2454
+ Math.max(0, Number(width ?? 1)),
2455
+ Number(gap ?? 0),
2456
+ Math.max(0, Number(blur) || 0)
2457
+ )
2458
+ )
2459
+ : paint.lineWidth;
2460
+ // The style spec evaluates zoom in `line-dasharray` at integer zooms only.
2461
+ const dashArray: StyleAccessor<[number, number]> | null = hasDashArray
2462
+ ? getStyleAccessor(styleLayer, ['line-dasharray'], Math.floor(zoom), ([dasharray]) =>
2463
+ getDashArray(dasharray)
2464
+ )
2465
+ : null;
2466
+ const lineOffset = hasOffset
2467
+ ? getStyleAccessor<[number, number, number, number]>(
2468
+ styleLayer,
2469
+ ['line-offset', 'line-gap-width', 'line-width', 'line-blur'],
2470
+ zoom,
2471
+ ([offset, gap, width, blur]) => [
2472
+ Number(offset ?? 0),
2473
+ Math.max(0, Number(gap ?? 0)),
2474
+ Math.max(0, Number(width ?? 1)),
2475
+ hasBlur ? Math.max(0, Number(blur) || 0) : 0
2476
+ ]
2477
+ )
2478
+ : null;
2479
+ // Where a wide blur makes a gap's two lines overlap, each draws only its side of the gap's
2480
+ // center. That takes the center's `line-offset` for the whole layer.
2481
+ const gapOffset = isBlurredGap
2482
+ ? getStyleAccessor(styleLayer, ['line-offset'], zoom, ([offset]) => Number(offset ?? 0))
2483
+ : null;
2484
+ const gapWidth = hasOffset
2485
+ ? getStyleAccessor(styleLayer, ['line-gap-width'], zoom, ([gap]) => Number(gap ?? 0))
2486
+ : null;
2487
+ const hasFillStyle =
2488
+ isFill &&
2489
+ (styleLayer.paint?.['fill-pattern'] !== undefined ||
2490
+ styleLayer.paint?.['fill-translate'] !== undefined);
2491
+
2492
+ // A constant transparent color or zero width draws nothing, so its sublayers are left out.
2493
+ const drawsFill = isFill && !isTransparent(fillColor);
2494
+ const drawsLines = (isLine || hasOutline) && !isTransparent(lineColor) && lineWidth.value !== 0;
2495
+ // A line whose width is 0 at this zoom step, and which draws at the next step or the previous
2496
+ // one, is kept for that step (see `renderSubLayers`). One that draws at neither, as many lines
2497
+ // do below the zoom where their width starts, or that stays transparent, is left out. Its color
2498
+ // at this step does not matter: at width 0 it draws nothing. Lines with an offset or a blur take
2499
+ // their width in attributes, not from `ZoomPaintExtension`.
2500
+ const isZeroWidth =
2501
+ isLine &&
2502
+ lineWidth.value === 0 &&
2503
+ !hasOffset &&
2504
+ !hasBlur &&
2505
+ [zoom - STYLE_ZOOM_STEP, zoom + STYLE_ZOOM_STEP].some(stepZoom => {
2506
+ const stepPaint = getGeometryPaint(styleLayer, mode, stepZoom);
2507
+ return stepPaint.lineWidth.value !== 0 && !isTransparent(stepPaint.lineColor);
2508
+ });
2509
+
2510
+ return {
2511
+ styleLayer,
2512
+ features,
2513
+ zoom,
2514
+ isLine,
2515
+ isFill,
2516
+ drawsFill,
2517
+ drawsLines,
2518
+ isZeroWidth,
2519
+ fillColor,
2520
+ lineColor,
2521
+ lineWidth,
2522
+ dashArray,
2523
+ lineOffset,
2524
+ gapWidth,
2525
+ lineBlur,
2526
+ gapOffset,
2527
+ isBlurredGap,
2528
+ hasFillStyle,
2529
+ lineShape,
2530
+ pattern,
2531
+ depthTested
2532
+ };
2533
+ }
2534
+
2535
+ /**
2536
+ * The key under which consecutive style layers can draw as one sublayer, or null when this one
2537
+ * draws alone. Style layers share a sublayer when everything deck.gl applies to a whole layer is
2538
+ * the same: a fill without an outline, or a line's cap, join and miter limit. Solid and dashed
2539
+ * lines share a sublayer: the solid ones draw with a dash array without gaps. Lines with an
2540
+ * offset, a gap or a blur, and fills with a pattern or a translation, draw alone.
2541
+ */
2542
+ function getBatchKey(style: GeometryStyle): string | null {
2543
+ if (style.isFill) {
2544
+ return style.drawsLines || style.hasFillStyle ? null : 'fill';
2545
+ }
2546
+ if (style.lineOffset || style.lineBlur || !style.lineShape) {
2547
+ return null;
2548
+ }
2549
+ const {lineCapRounded, lineJointRounded, lineMiterLimit} = style.lineShape;
2550
+ return `line-${lineCapRounded}-${lineJointRounded}-${lineMiterLimit}`;
2551
+ }
2552
+
2553
+ /** Whether features include polygons and lines, per tile content and style layer. */
2554
+ const geometryKindCache = new WeakMap<any[], {polygons: boolean; lines: boolean; length: number}>();
2555
+
2556
+ function getGeometryKinds(features: any[]): {polygons: boolean; lines: boolean} {
2557
+ let kinds = geometryKindCache.get(features);
2558
+ // Content edited in place is looked at again.
2559
+ if (!kinds || kinds.length !== features.length) {
2560
+ kinds = {polygons: false, lines: false, length: features.length};
2561
+ for (const feature of features) {
2562
+ const type = feature.geometry?.type;
2563
+ kinds.polygons ||=
2564
+ type === 'Polygon' || type === 'MultiPolygon' || type === 'GeometryCollection';
2565
+ kinds.lines ||=
2566
+ type === 'LineString' || type === 'MultiLineString' || type === 'GeometryCollection';
2567
+ }
2568
+ geometryKindCache.set(features, kinds);
2569
+ }
2570
+ return kinds;
2571
+ }
2572
+
2573
+ /**
2574
+ * Whether `style` can draw in the same sublayer as `batch`, after it. A sublayer draws its
2575
+ * features in order, so a batch draws each style layer's features after the previous one's, as
2576
+ * MapLibre draws the style layers in turn. The exception is a line sublayer, which draws polygon
2577
+ * outlines before line strings: a style layer's polygons cannot follow an earlier one's lines.
2578
+ */
2579
+ function canBatch(batch: GeometryStyle[], style: GeometryStyle, key: string | null): boolean {
2580
+ if (batch.length === 0 || key === null || getBatchKey(batch[0]) !== key) {
2581
+ return false;
2582
+ }
2583
+ return !(
2584
+ style.isLine &&
2585
+ getGeometryKinds(style.features).polygons &&
2586
+ batch.some(part => getGeometryKinds(part.features).lines)
2587
+ );
2588
+ }
2589
+
2590
+ type FeatureBatch = {
2591
+ parts: any[][];
2592
+ /** Each part's length when the batch was made: parts edited in place are batched again. */
2593
+ lengths: number[];
2594
+ data: any[];
2595
+ partIndices: Uint16Array;
2596
+ };
2597
+
2598
+ /** The latest concatenated features per tile content and batch. */
2599
+ const featureBatchCache = new WeakMap<any[], Map<string, FeatureBatch>>();
2600
+
2601
+ /**
2602
+ * The features of a batch's style layers, in order, and the index of the style layer each one
2603
+ * belongs to. The result is reused while every style layer's features are unchanged, so deck.gl
2604
+ * does not re-tessellate the batch at every zoom step.
2605
+ */
2606
+ function getFeatureBatch(tileFeatures: any[], id: string, parts: any[][]): FeatureBatch {
2607
+ let byId = featureBatchCache.get(tileFeatures);
2608
+ if (!byId) {
2609
+ byId = new Map();
2610
+ featureBatchCache.set(tileFeatures, byId);
2611
+ }
2612
+ const cached = byId.get(id);
2613
+ if (
2614
+ cached &&
2615
+ cached.parts.length === parts.length &&
2616
+ cached.parts.every(
2617
+ (features, index) => features === parts[index] && cached.lengths[index] === features.length
2618
+ )
2619
+ ) {
2620
+ return cached;
2621
+ }
2622
+ const data = parts.flat();
2623
+ const partIndices = new Uint16Array(data.length);
2624
+ let start = 0;
2625
+ parts.forEach((features, index) => {
2626
+ partIndices.fill(index, start, start + features.length);
2627
+ start += features.length;
2628
+ });
2629
+ const batch = {parts, lengths: parts.map(features => features.length), data, partIndices};
2630
+ byId.set(id, batch);
2631
+ return batch;
2632
+ }
2633
+
2634
+ /**
2635
+ * One accessor for the features of several style layers: a constant when every style layer's
2636
+ * value is the same constant, otherwise a function that applies each feature's own style layer's
2637
+ * accessor. Its update trigger changes when any style layer's value or trigger changes.
2638
+ */
2639
+ export function mergeStyleAccessors<T>(
2640
+ accessors: StyleAccessor<T>[],
2641
+ partIndices: ArrayLike<number>
2642
+ ): {value: T | ((feature: any, info: {index: number}) => T); updateTrigger?: unknown} {
2643
+ if (accessors.length === 1) {
2644
+ return accessors[0];
2645
+ }
2646
+ const values = accessors.map(accessor => accessor.value);
2647
+ // Tagged, so that a constant and a per-feature trigger of the same text differ.
2648
+ const keys = accessors.map(accessor =>
2649
+ typeof accessor.value === 'function'
2650
+ ? `f:${JSON.stringify(accessor.updateTrigger ?? null)}`
2651
+ : `c:${JSON.stringify(accessor.value)}`
2652
+ );
2653
+ if (values.every(value => typeof value !== 'function') && keys.every(key => key === keys[0])) {
2654
+ return {value: values[0] as T};
2655
+ }
2656
+ return {
2657
+ value: (feature: any, {index}: {index: number}) => {
2658
+ const value = values[partIndices[index]];
2659
+ return typeof value === 'function' ? (value as (feature: any) => T)(feature) : value;
2660
+ },
2661
+ updateTrigger: keys.join('|')
2662
+ };
2663
+ }
2664
+
2665
+ /**
2666
+ * The sublayer for one fill or line style layer, or for consecutive style layers that share a
2667
+ * batch key (see `getBatchKey`), drawn one after another. A line with a gap adds a second
2668
+ * sublayer for the other side of the gap.
2669
+ */
2670
+ function createGeometrySubLayer({
2671
+ props,
2672
+ styles: allStyles,
2673
+ mode,
2674
+ idSuffix = ''
2675
+ }: {
2676
+ props: any;
2677
+ styles: GeometryStyle[];
2678
+ mode: BasemapMode;
2679
+ idSuffix?: string;
2680
+ }) {
2681
+ // Paint that is the same for all features of a style layer is taken from a texture (see
2682
+ // `ZoomPaintExtension`), except next to an offset: it is computed from the width on the CPU, and
2683
+ // with dashes leaves no vertex attribute for the extension. Blurred lines and patterned or
2684
+ // translated fills, which draw alone with their own extensions, keep their paint in attributes.
2685
+ const zoomPaintContext = props[ZOOM_PAINT_CONTEXT] as ZoomPaintContext | null | undefined;
2686
+ const takesZoomPaint = (list: GeometryStyle[]) =>
2687
+ Boolean(zoomPaintContext) &&
2688
+ !list[0].lineOffset &&
2689
+ !list[0].lineBlur &&
2690
+ !list[0].hasFillStyle &&
2691
+ list.length <= MAX_ZOOM_PAINT_PARTS;
2692
+ // Lines kept at width 0 (see `renderSubLayers`) draw nothing because the texture gives them
2693
+ // that width. Without the texture their width would be in attributes, so they are left out, as
2694
+ // other style layers that draw nothing are.
2695
+ const styles = takesZoomPaint(allStyles)
2696
+ ? allStyles
2697
+ : allStyles.filter(style => style.drawsFill || style.drawsLines);
2698
+ if (styles.length === 0) {
2699
+ return null;
2700
+ }
2701
+ const [first] = styles;
2702
+ // A batch draws lines when any of its style layers does, or keeps lines at width 0. A sublayer
2703
+ // of lines all kept at width 0 draws nothing; it is hidden, and adds no draw.
2704
+ const drawsFill = styles.some(style => style.drawsFill);
2705
+ const drawsLines = styles.some(style => style.drawsLines || style.isZeroWidth);
2706
+ const isHidden = !styles.some(style => style.drawsFill || style.drawsLines);
2707
+ const {
2708
+ isFill,
2709
+ lineShape,
2710
+ lineOffset,
2711
+ gapWidth,
2712
+ depthTested,
2713
+ lineBlur,
2714
+ gapOffset,
2715
+ isBlurredGap,
2716
+ hasFillStyle,
2717
+ pattern,
2718
+ zoom
2719
+ } = first;
2720
+ if (!drawsFill && !drawsLines) {
2721
+ return null;
2722
+ }
2723
+ const baseProps = getSubLayerBaseProps(props);
2724
+ const tileFeatures = getTileFeatures(props.data);
2725
+ // A batch is named after all of its style layers (see `getStyleSubLayerId`).
2726
+ const styleLayerIds =
2727
+ styles.length === 1 ? first.styleLayer.id : styles.map(style => style.styleLayer.id);
2728
+ const {data, partIndices} =
2729
+ styles.length === 1
2730
+ ? {data: first.features, partIndices: []}
2731
+ : getFeatureBatch(
2732
+ tileFeatures,
2733
+ JSON.stringify(styleLayerIds),
2734
+ styles.map(style => style.features)
2735
+ );
2736
+ const merge = <T>(get: (style: GeometryStyle) => StyleAccessor<T>) =>
2737
+ mergeStyleAccessors(styles.map(get), partIndices);
2738
+ const zoomPaint =
2739
+ zoomPaintContext && takesZoomPaint(styles)
2740
+ ? zoomPaintContext.get(
2741
+ styles.map(style => style.styleLayer),
2742
+ mode
2743
+ )
2744
+ : null;
2745
+ const perFeature = <T>(accessor: StyleAccessor<T>, constant: T): StyleAccessor<T> =>
2746
+ zoomPaint && typeof accessor.value !== 'function' ? {value: constant} : accessor;
2747
+ const fillColor = merge(style => perFeature(style.fillColor, TRANSPARENT_COLOR));
2748
+ const lineColor = merge(style => perFeature(style.lineColor, TRANSPARENT_COLOR));
2749
+ // The width attribute scales the texture's width of its style layer, which is 1 where it is per
2750
+ // feature.
2751
+ const lineWidth = merge(style => perFeature(style.lineWidth, 1));
2752
+ // A dash array without gaps draws a solid line.
2753
+ const dashArray = styles.some(style => style.dashArray)
2754
+ ? merge(style => style.dashArray ?? SOLID_DASH_ARRAY)
2755
+ : null;
2756
+
2757
+ const offsetValue = lineOffset?.value;
2758
+ const getOffset = (side: number) => (feature: any) => {
2759
+ const [offset, gap, width, blur] =
2760
+ typeof offsetValue === 'function' ? offsetValue(feature) : offsetValue!;
2761
+ return getLineOffset(offset, gap, width, side, blur);
2762
+ };
2763
+ const getBlurGap = (side: number) =>
2764
+ gapOffset && typeof gapOffset.value !== 'function'
2765
+ ? {blurGapSide: side, blurGapOffset: gapOffset.value}
2766
+ : {};
2767
+ // deck.gl measures dashes in drawn widths and MapLibre in `line-width`s, so a blurred gap's
2768
+ // wider lines take dashes shortened by as much.
2769
+ const lineDashArray =
2770
+ dashArray && isBlurredGap && lineOffset
2771
+ ? getBlurredDashArray(dashArray as StyleAccessor<[number, number]>, lineOffset)
2772
+ : dashArray;
2773
+ // MapLibre fades a line's edges, over its blur and a device pixel, from half a device pixel past
2774
+ // the line: a line narrower than a pixel still draws, fainter. `LineBlurExtension` draws that
2775
+ // fade, without a blur too; deck.gl draws lines without antialiasing aliased, and drops most of
2776
+ // a line narrower than a pixel. A fill's outline is MapLibre's aliased 1 pixel line, drawn as
2777
+ // deck.gl draws lines without antialiasing and cut to a line's pixels by `FillOutlineExtension`.
2778
+ const fadesLines = drawsLines && !isFill;
2779
+ const drawsOutline = drawsLines && isFill;
2780
+ const geometryExtensions =
2781
+ dashArray || lineOffset || fadesLines || drawsOutline || zoomPaint
2782
+ ? [
2783
+ ...(baseProps.extensions || []),
2784
+ ...(dashArray ? [lineOffset ? DASH_OFFSET_EXTENSION : DASH_EXTENSION] : []),
2785
+ ...(!dashArray && lineOffset ? [OFFSET_EXTENSION] : []),
2786
+ // Before the fade, which widens the stroke once its width is final.
2787
+ ...(zoomPaint ? [ZOOM_PAINT_EXTENSION] : []),
2788
+ ...(fadesLines
2789
+ ? [
2790
+ typeof lineBlur?.value === 'function'
2791
+ ? LINE_BLUR_PER_FEATURE_EXTENSION
2792
+ : LINE_BLUR_EXTENSION
2793
+ ]
2794
+ : []),
2795
+ ...(drawsOutline ? [FILL_OUTLINE_EXTENSION] : [])
2796
+ ]
2797
+ : null;
2798
+ // `fill-pattern` and `fill-translate` add their extensions to these, so an outlined fill keeps
2799
+ // its outline's.
2800
+ const fillStyleProps =
2801
+ isFill && hasFillStyle
2802
+ ? getFillStyleProps(
2803
+ first.styleLayer,
2804
+ zoom,
2805
+ geometryExtensions ?? baseProps.extensions,
2806
+ pattern
2807
+ )
2808
+ : {};
2809
+
2810
+ const layerProps = {
2811
+ ...baseProps,
2812
+ // Not drawn, picked or highlighted while all of its lines are kept at width 0.
2813
+ ...(isHidden ? {visible: false, pickable: false} : {}),
2814
+ id: `${getStyleSubLayerId(props.id, styleLayerIds)}${idSuffix}`,
2815
+ data,
2816
+ stroked: drawsLines,
2817
+ filled: drawsFill,
2818
+ // A fill's line strings draw only as its 1 pixel outline.
2819
+ drawsLineStrings: drawsLines,
2820
+ getFillColor: isFill ? (fillColor.value as any) : [0, 0, 0, 0],
2821
+ getLineColor: lineColor.value as any,
2822
+ getLineWidth: lineWidth.value as any,
2823
+ ...(geometryExtensions ? {extensions: geometryExtensions} : {}),
2824
+ ...(zoomPaint
2825
+ ? {
2826
+ zoomPaint,
2827
+ getStylePart:
2828
+ styles.length === 1
2829
+ ? 0
2830
+ : (_feature: any, {index}: {index: number}) => partIndices[index]
2831
+ }
2832
+ : {}),
2833
+ lineAntialiasing: fadesLines,
2834
+ ...(fadesLines ? {getBlur: lineBlur ? lineBlur.value : 0} : {}),
2835
+ ...getBlurGap(1),
2836
+ ...fillStyleProps,
2837
+ ...(offsetValue ? {getOffset: getOffset(1)} : {}),
2838
+ ...(lineDashArray
2839
+ ? {
2840
+ getDashArray: lineDashArray.value as any,
2841
+ dashJustified: false
2842
+ }
2843
+ : {}),
2844
+ updateTriggers: {
2845
+ getFillColor: isFill ? fillColor.updateTrigger : undefined,
2846
+ // The group's accessors change with its features, which are new data in that case.
2847
+ getFillPatternScale: pattern ? Math.floor(zoom) : undefined,
2848
+ getLineColor: lineColor.updateTrigger,
2849
+ getLineWidth: lineWidth.updateTrigger,
2850
+ getDashArray: lineDashArray?.updateTrigger,
2851
+ getOffset: typeof offsetValue === 'function' ? lineOffset?.updateTrigger : offsetValue,
2852
+ getBlur: lineBlur?.updateTrigger
2853
+ },
2854
+ lineWidthUnits: 'pixels',
2855
+ lineWidthMinPixels: 0,
2856
+ lineCapRounded: false,
2857
+ lineJointRounded: false,
2858
+ ...lineShape,
2859
+ getPointRadius: 0,
2860
+ pointRadiusMinPixels: 0,
2861
+ parameters: getTileParameters(mode, depthTested)
2862
+ };
2863
+ const layer = getTileGeoJsonLayer(props, layerProps);
2864
+ if (!gapWidth || (typeof gapWidth.value !== 'function' && !(gapWidth.value > 0))) {
2865
+ return layer;
2866
+ }
2867
+
2868
+ // A gap draws a second line on the other side, from the same data so that tile data, picked
2869
+ // objects and indices stay those of the features. Features without a gap draw only once.
2870
+ const getGap = gapWidth.value;
2871
+ const getWidth = styles[0].lineWidth.value;
2872
+ return [
2873
+ layer,
2874
+ getTileGeoJsonLayer(props, {
2875
+ ...layerProps,
2876
+ id: `${layerProps.id}-gap`,
2877
+ getOffset: getOffset(-1),
2878
+ ...getBlurGap(-1),
2879
+ ...(typeof getGap === 'function'
2880
+ ? {
2881
+ getLineWidth: (feature: any) =>
2882
+ getGap(feature) > 0
2883
+ ? typeof getWidth === 'function'
2884
+ ? getWidth(feature)
2885
+ : getWidth
2886
+ : 0,
2887
+ updateTriggers: {
2888
+ ...layerProps.updateTriggers,
2889
+ // A width that is not per feature is a value with no trigger of its own.
2890
+ getLineWidth: [
2891
+ typeof getWidth === 'function' ? lineWidth.updateTrigger : getWidth,
2892
+ gapWidth.updateTrigger
2893
+ ]
2894
+ }
2895
+ }
2896
+ : {})
2897
+ })
2898
+ ];
2899
+ }
2900
+
2901
+ /** The latest features raised to their `fill-extrusion-base`, per tile content and style layer. */
2902
+ const raisedFeatureCache = new WeakMap<
2903
+ any[],
2904
+ WeakMap<BasemapStyleLayer, {key: unknown[]; features: any[]}>
2905
+ >();
2906
+
2907
+ function raiseCoordinates(coordinates: any, base: number): any {
2908
+ return typeof coordinates[0] === 'number'
2909
+ ? [coordinates[0], coordinates[1], base]
2910
+ : coordinates.map((child: any) => raiseCoordinates(child, base));
2911
+ }
2912
+
2913
+ /**
2914
+ * Returns the features with their polygons raised to `fill-extrusion-base`: deck.gl extrudes a
2915
+ * polygon from its own `z` by `getElevation`. Features with a base of 0 are returned as they are,
2916
+ * and so is the array when every base is 0. The result is reused while the base is unchanged.
2917
+ */
2918
+ function getRaisedFeatures(
2919
+ features: any[],
2920
+ styleLayer: BasemapStyleLayer,
2921
+ base: StyleAccessor<number>
2922
+ ): any[] {
2923
+ const {value} = base;
2924
+ if (value === 0) {
2925
+ return features;
2926
+ }
2927
+ const key = [
2928
+ styleLayer.paint?.['fill-extrusion-base'],
2929
+ styleLayer.paint?.['fill-extrusion-height'],
2930
+ typeof value === 'function' ? base.updateTrigger : value
2931
+ ];
2932
+ let byStyleLayer = raisedFeatureCache.get(features);
2933
+ if (!byStyleLayer) {
2934
+ byStyleLayer = new WeakMap();
2935
+ raisedFeatureCache.set(features, byStyleLayer);
2936
+ }
2937
+ const cached = byStyleLayer.get(styleLayer);
2938
+ if (cached && cached.key.every((entry, index) => entry === key[index])) {
2939
+ return cached.features;
2940
+ }
2941
+
2942
+ let raisedAny = false;
2943
+ const raised = features.map(feature => {
2944
+ const geometryType = feature.geometry?.type;
2945
+ const featureBase = typeof value === 'function' ? value(feature) : value;
2946
+ if (featureBase === 0 || (geometryType !== 'Polygon' && geometryType !== 'MultiPolygon')) {
2947
+ return feature;
2948
+ }
2949
+ raisedAny = true;
2950
+ return {
2951
+ ...feature,
2952
+ geometry: {
2953
+ ...feature.geometry,
2954
+ coordinates: raiseCoordinates(feature.geometry.coordinates, featureBase)
2955
+ }
2956
+ };
2957
+ });
2958
+ const result = raisedAny ? raised : features;
2959
+ byStyleLayer.set(styleLayer, {key, features: result});
2960
+ return result;
2961
+ }
2962
+
2963
+ function getExtrusionBounds(height: unknown, base: unknown): [number, number] {
2964
+ const top = Math.max(0, Number(height ?? 0) || 0);
2965
+ return [Math.min(Math.max(0, Number(base ?? 0) || 0), top), top];
2966
+ }
2967
+
2968
+ /**
2969
+ * Draws a `fill-extrusion` style layer as extruded polygons, from `fill-extrusion-base` to
2970
+ * `fill-extrusion-height`, in meters. `fill-extrusion-opacity` applies to the whole layer, as in
2971
+ * MapLibre, and the alpha of `fill-extrusion-color` is ignored.
2972
+ */
2973
+ function createExtrusionSubLayer({
2974
+ props,
2975
+ styleLayer,
2976
+ features,
2977
+ zoom,
2978
+ light = DEFAULT_FILL_EXTRUSION_LIGHT
2979
+ }: {
2980
+ props: any;
2981
+ styleLayer: BasemapStyleLayer;
2982
+ features: any[];
2983
+ zoom: number;
2984
+ light?: FillExtrusionLight;
2985
+ }) {
2986
+ const paint = getPaint(styleLayer, zoom);
2987
+ const color = getStyleAccessor(styleLayer, ['fill-extrusion-color'], zoom, ([value]) =>
2988
+ withOpacity([...(value ?? DEFAULT_COLOR).slice(0, 3), 1])
2989
+ );
2990
+ const base = getStyleAccessor(
2991
+ styleLayer,
2992
+ ['fill-extrusion-height', 'fill-extrusion-base'],
2993
+ zoom,
2994
+ ([height, baseValue]) => getExtrusionBounds(height, baseValue)[0]
2995
+ );
2996
+ const elevation = getStyleAccessor(
2997
+ styleLayer,
2998
+ ['fill-extrusion-height', 'fill-extrusion-base'],
2999
+ zoom,
3000
+ ([height, baseValue]) => {
3001
+ const [bottom, top] = getExtrusionBounds(height, baseValue);
3002
+ return top - bottom;
3003
+ }
3004
+ );
3005
+ const baseProps = getSubLayerBaseProps(props);
3006
+
3007
+ return getTileGeoJsonLayer(props, {
3008
+ ...baseProps,
3009
+ id: getStyleSubLayerId(props.id, styleLayer.id),
3010
+ data: getRaisedFeatures(features, styleLayer, base),
3011
+ opacity: (baseProps.opacity ?? 1) * (paint['fill-extrusion-opacity'] ?? 1),
3012
+ extruded: true,
3013
+ filled: true,
3014
+ stroked: false,
3015
+ drawsLineStrings: false,
3016
+ wireframe: false,
3017
+ getFillColor: color.value as any,
3018
+ getElevation: elevation.value as any,
3019
+ updateTriggers: {
3020
+ getFillColor: color.updateTrigger,
3021
+ getElevation: elevation.updateTrigger
3022
+ },
3023
+ getPointRadius: 0,
3024
+ pointRadiusMinPixels: 0,
3025
+ parameters: EXTRUSION_PARAMETERS,
3026
+ extensions: [...(baseProps.extensions || []), FILL_EXTRUSION_LIGHTING_EXTENSION],
3027
+ fillExtrusionLight: light,
3028
+ fillExtrusionVerticalGradient: paint['fill-extrusion-vertical-gradient'] ?? true
3029
+ });
3030
+ }
3031
+
3032
+ /** Shared by every extrusion sublayer, so deck.gl does not see their extensions change. */
3033
+ const FILL_EXTRUSION_LIGHTING_EXTENSION = new FillExtrusionLightingExtension();
3034
+
3035
+ /** The latest evaluated `light` per style `light` object, reused within one zoom step. */
3036
+ const fillExtrusionLightCache = new WeakMap<
3037
+ object,
3038
+ {zoomBucket: number; light: FillExtrusionLight}
3039
+ >();
3040
+
3041
+ /**
3042
+ * Returns a style's `light` at the stepped zoom, the same object while it is unchanged, so tile
3043
+ * sublayers are kept. A style without `light` takes the style spec's default.
3044
+ */
3045
+ function getFillExtrusionLight(
3046
+ styleDefinition: ResolvedBasemapStyle | undefined,
3047
+ zoom: number
3048
+ ): FillExtrusionLight {
3049
+ const styleLight = styleDefinition?.light;
3050
+ if (!styleLight || typeof styleLight !== 'object') {
3051
+ return DEFAULT_FILL_EXTRUSION_LIGHT;
3052
+ }
3053
+ const zoomBucket = getZoomBucket(zoom);
3054
+ const cached = fillExtrusionLightCache.get(styleLight);
3055
+ if (cached && cached.zoomBucket === zoomBucket) {
3056
+ return cached.light;
3057
+ }
3058
+ const {anchor, position, color, intensity} = evaluateStyleLight(styleLight, zoomBucket);
3059
+ const isNumbers = (value: unknown, length: number): value is number[] =>
3060
+ Array.isArray(value) &&
3061
+ value.length >= length &&
3062
+ value.slice(0, length).every(entry => Number.isFinite(entry));
3063
+ const defaults = DEFAULT_FILL_EXTRUSION_LIGHT;
3064
+ // Values of the wrong type (the expression engine passes a constant through) take the default.
3065
+ const light: FillExtrusionLight = {
3066
+ anchor: anchor === 'map' ? 'map' : 'viewport',
3067
+ position: isNumbers(position, 3) ? [position[0], position[1], position[2]] : defaults.position,
3068
+ color: isNumbers(color, 3) ? [color[0] / 255, color[1] / 255, color[2] / 255] : defaults.color,
3069
+ intensity: Number.isFinite(intensity)
3070
+ ? Math.min(Math.max(intensity as number, 0), 1)
3071
+ : defaults.intensity
3072
+ };
3073
+ fillExtrusionLightCache.set(styleLight, {zoomBucket, light});
3074
+ return light;
3075
+ }
3076
+
3077
+ /** deck.gl's `CollisionFilterExtension` supports priorities from -1000 to 1000. */
3078
+ const COLLISION_PRIORITY_RANGE: [number, number] = [-1000, 1000];
3079
+
3080
+ /**
3081
+ * Splits the collision priority range into one band per symbol layer, in style order. All labels
3082
+ * share one collision group, so a later style layer's band is above an earlier one's: MapLibre
3083
+ * places symbol layers from the top of the style down, and `symbol-sort-key` only orders labels
3084
+ * within a layer.
3085
+ */
3086
+ function getLabelPriorityRanges(
3087
+ styleLayers: BasemapStyleLayer[]
3088
+ ): Map<BasemapStyleLayer, [number, number]> {
3089
+ const symbolLayers = styleLayers.filter(layer => layer.type === 'symbol');
3090
+ const [min, max] = COLLISION_PRIORITY_RANGE;
3091
+ const width = (max - min) / Math.max(symbolLayers.length, 1);
3092
+ return new Map(
3093
+ symbolLayers.map((layer, index) => [layer, [min + index * width, min + (index + 1) * width]])
3094
+ );
3095
+ }
3096
+
3097
+ /** Whether a style layer is drawn from vector tiles: a `symbol` layer only with labels on. */
3098
+ function isVectorStyleLayer(layer: BasemapStyleLayer, config: BasemapLayerConfig): boolean {
3099
+ return layer.type === 'symbol'
3100
+ ? config.labels
3101
+ : layer.type === 'fill' ||
3102
+ layer.type === 'fill-extrusion' ||
3103
+ layer.type === 'line' ||
3104
+ layer.type === 'circle';
3105
+ }
3106
+
3107
+ /**
3108
+ * Whether a source's layers draw from vector tiles: a vector source with tile templates, or a
3109
+ * GeoJSON source with data, which is cut into tiles on the client (see `getGeoJsonTileFeatures`).
3110
+ */
3111
+ export function isVectorTileSource(source: BasemapSource | undefined): boolean {
3112
+ return source?.type === 'geojson'
3113
+ ? isGeoJsonSource(source)
3114
+ : Boolean(source?.tiles) && source?.type === 'vector';
3115
+ }
3116
+
3117
+ /**
3118
+ * The `getTileData` of a vector tile source's tileset: `loader` fetches and decodes a vector
3119
+ * source's tiles, and a GeoJSON source's tiles are cut from its data, in the same tile-local
3120
+ * coordinates.
3121
+ */
3122
+ export function getVectorTileLoader(
3123
+ source: BasemapSource,
3124
+ loader: SharedMVTLayer
3125
+ ): (tile: Parameters<SharedMVTLayer['getTileData']>[0]) => Promise<any> {
3126
+ return source.type === 'geojson'
3127
+ ? tile => getGeoJsonTileFeatures(source, tile.index)
3128
+ : tile => loader.getTileData(tile);
3129
+ }
3130
+
3131
+ /**
3132
+ * The source layers drawn from each vector source, over all of its runs, sorted; `null` for a
3133
+ * source with a style layer that names none. Tiles decode only these: OpenMapTiles tiles, for
3134
+ * one, carry house numbers and peaks that few styles draw, and points of interest, most of a
3135
+ * street-level tile, that a style without them, or with labels off, does not draw.
3136
+ */
3137
+ export function getRunSourceLayers(runs: StyleLayerRun[]): Map<BasemapSource, string[] | null> {
3138
+ const names = new Map<BasemapSource, Set<string> | null>();
3139
+ for (const run of runs) {
3140
+ if (run.type !== 'vector') {
3141
+ continue;
3142
+ }
3143
+ let set = names.has(run.source!) ? names.get(run.source!) : new Set<string>();
3144
+ for (const layer of run.styleLayers) {
3145
+ const sourceLayer = layer['source-layer'];
3146
+ if (typeof sourceLayer !== 'string') {
3147
+ set = null;
3148
+ }
3149
+ set?.add(sourceLayer as string);
3150
+ }
3151
+ names.set(run.source!, set ?? null);
3152
+ }
3153
+ return new Map([...names].map(([source, set]) => [source, set ? [...set].sort() : null]));
3154
+ }
3155
+
3156
+ /**
3157
+ * The source layers each vector source of a style decodes (see `getRunSourceLayers`). With the
3158
+ * basemap off they are those it would draw, so that turning it off and on keeps the tiles.
3159
+ */
3160
+ export function getStyleSourceLayers(
3161
+ styleDefinition: ResolvedBasemapStyle,
3162
+ globe?: {config?: BasemapGlobeConfig}
3163
+ ): Map<BasemapSource, string[] | null> {
3164
+ const config = getConfig(globe);
3165
+ const styleLayers = (styleDefinition.layers || []).filter(layer =>
3166
+ SUPPORTED_TYPES.has(layer.type)
3167
+ );
3168
+ return getRunSourceLayers(getStyleLayerRuns(styleLayers, styleDefinition, config));
3169
+ }
3170
+
3171
+ /**
3172
+ * `loadOptions` for a vector source's tiles, decoding only `sourceLayers` when given. Source
3173
+ * layers set in `loadOptions.mvt.layers` are left as they are.
3174
+ */
3175
+ export function getSourceLoadOptions(
3176
+ loadOptions: BasemapLoadOptions | undefined,
3177
+ sourceLayers: string[] | null | undefined
3178
+ ): BasemapLoadOptions | undefined {
3179
+ const mvt = (loadOptions as {mvt?: Record<string, unknown>} | undefined)?.mvt;
3180
+ if (!sourceLayers || mvt?.layers) {
3181
+ return loadOptions;
3182
+ }
3183
+ return {...loadOptions, mvt: {...mvt, layers: sourceLayers}} as BasemapLoadOptions;
3184
+ }
3185
+
3186
+ /**
3187
+ * Splits the style into runs that draw in style order, as in MapLibre: each background, raster
3188
+ * and hillshade layer on its own, and consecutive layers of one vector source together, so that a
3189
+ * raster layer between two vector layers draws between them. Layers that cannot draw (a source
3190
+ * without tiles, labels turned off) do not split a run. Runs do not depend on zoom, so the tile
3191
+ * layers keep their ids and tiles when a layer leaves its zoom range. Background, raster and
3192
+ * hillshade runs have their style layer's id. The first run of a source has the source id as its
3193
+ * id, unless a background, raster or hillshade run already has that id; later runs, and a first run whose source id is
3194
+ * taken, add the quoted id of their first style layer.
3195
+ */
3196
+ export function getStyleLayerRuns(
3197
+ styleLayers: BasemapStyleLayer[],
3198
+ styleDefinition: ResolvedBasemapStyle,
3199
+ config: BasemapLayerConfig = DEFAULT_CONFIG
3200
+ ): StyleLayerRun[] {
3201
+ const runs: StyleLayerRun[] = [];
3202
+ const sourceIds = new Set<string>();
3203
+ const reservedIds = new Set([
3204
+ ...Object.keys(styleDefinition.sources || {}),
3205
+ ...styleLayers.map(layer => layer.id)
3206
+ ]);
3207
+ const getSource = (layer: BasemapStyleLayer) =>
3208
+ layer.source ? styleDefinition.sources?.[layer.source as string] : undefined;
3209
+ // deck.gl matches layers by id, so a vector run must not take a background or raster run's id.
3210
+ const singleLayerRunIds = new Set(
3211
+ styleLayers
3212
+ .filter(
3213
+ layer =>
3214
+ layer.type === 'background' ||
3215
+ ((layer.type === 'raster' || layer.type === 'hillshade') && getSource(layer)?.tiles)
3216
+ )
3217
+ .map(layer => layer.id)
3218
+ );
3219
+ for (const layer of styleLayers) {
3220
+ const sourceId = layer.source as string | undefined;
3221
+ const source = getSource(layer);
3222
+ if (layer.type === 'background') {
3223
+ runs.push({type: 'background', id: layer.id, styleLayers: [layer]});
3224
+ } else if (layer.type === 'raster') {
3225
+ if (source?.tiles) {
3226
+ runs.push({type: 'raster', id: layer.id, sourceId, source, styleLayers: [layer]});
3227
+ } else {
3228
+ logBasemapRuntimeEvent('Skipping style layer without resolved tiles', {
3229
+ layerId: layer.id,
3230
+ sourceId
3231
+ });
3232
+ }
3233
+ } else if (layer.type === 'hillshade') {
3234
+ if (source?.tiles && source.type === 'raster-dem') {
3235
+ runs.push({type: 'hillshade', id: layer.id, sourceId, source, styleLayers: [layer]});
3236
+ } else {
3237
+ logBasemapRuntimeEvent('Skipping hillshade layer without a raster-dem source', {
3238
+ layerId: layer.id,
3239
+ sourceId
3240
+ });
3241
+ }
3242
+ } else if (isVectorStyleLayer(layer, config) && sourceId && isVectorTileSource(source)) {
3243
+ const last = runs[runs.length - 1];
3244
+ // Labels draw after all of their run's other layers, so a layer after a label starts a run.
3245
+ const followsLabel =
3246
+ layer.type !== 'symbol' &&
3247
+ last?.styleLayers.some(styleLayer => styleLayer.type === 'symbol');
3248
+ if (last?.type === 'vector' && last.sourceId === sourceId && !followsLabel) {
3249
+ last.styleLayers.push(layer);
3250
+ } else {
3251
+ let id = sourceId;
3252
+ if (sourceIds.has(sourceId) || singleLayerRunIds.has(sourceId)) {
3253
+ id = `${sourceId}-${JSON.stringify(layer.id)}`;
3254
+ while (reservedIds.has(id)) id += '-run';
3255
+ reservedIds.add(id);
3256
+ }
3257
+ runs.push({
3258
+ type: 'vector',
3259
+ id,
3260
+ sourceId,
3261
+ source,
3262
+ styleLayers: [layer]
3263
+ });
3264
+ sourceIds.add(sourceId);
3265
+ }
3266
+ }
3267
+ }
3268
+ return runs;
3269
+ }
3270
+
3271
+ /**
3272
+ * The deck.gl layers of the style's background, raster and vector layers, in style order. A run
3273
+ * draws only while one of its layers is visible at `zoom`. A vector run keeps all of its style
3274
+ * layers, visible or not: its tile regeneration key must change when any of them crosses its own
3275
+ * limits, and `renderSubLayers` gates each layer by zoom.
3276
+ */
3277
+ function getStyleLayers({
3278
+ idPrefix,
3279
+ styleLayers,
3280
+ styleDefinition,
3281
+ zoom,
3282
+ config,
3283
+ loadOptions,
3284
+ mode,
3285
+ spriteAtlases,
3286
+ fontFamily,
3287
+ glyphs,
3288
+ labelPlacement,
3289
+ tilesets
3290
+ }: {
3291
+ idPrefix: string;
3292
+ styleLayers: BasemapStyleLayer[];
3293
+ styleDefinition: ResolvedBasemapStyle;
3294
+ zoom: number;
3295
+ config: BasemapLayerConfig;
3296
+ loadOptions?: BasemapLoadOptions;
3297
+ mode: BasemapMode;
3298
+ spriteAtlases?: SpriteAtlas[] | null;
3299
+ fontFamily?: LabelFontFamily | null;
3300
+ /** The style's glyphs, which labels are drawn with (see `MVTLabelLayerProps.glyphs`). */
3301
+ glyphs?: GlyphStore | null;
3302
+ /** The label placement shared by the map's label layers (see `LabelPlacement`). */
3303
+ labelPlacement?: LabelPlacement | null;
3304
+ tilesets?: Map<BasemapSource, SharedTileset2D>;
3305
+ }) {
3306
+ const labelPriorityRanges = getLabelPriorityRanges(
3307
+ styleLayers.filter(layer => isVectorStyleLayer(layer, config))
3308
+ );
3309
+ const allRuns = getStyleLayerRuns(styleLayers, styleDefinition, config);
3310
+ const runs = allRuns.filter(run =>
3311
+ run.styleLayers.some(layer => isStyleLayerVisibleAtZoom(layer, zoom))
3312
+ );
3313
+
3314
+ // On a globe, flat layers drawn after an extrusion are depth-tested against the buildings.
3315
+ const extrusionIndex =
3316
+ mode === 'globe'
3317
+ ? styleLayers.findIndex(
3318
+ layer => layer.type === 'fill-extrusion' && isStyleLayerVisibleAtZoom(layer, zoom)
3319
+ )
3320
+ : -1;
3321
+ const depthTested = new Set(extrusionIndex >= 0 ? styleLayers.slice(extrusionIndex + 1) : []);
3322
+
3323
+ const ownedTilesets = new Map<BasemapSource, SourceTilesetOwner>();
3324
+ const ownedDemTilesets = new Map<BasemapSource, DemTilesetOwner>();
3325
+ let sourceLayers: Map<BasemapSource, string[] | null> | undefined;
3326
+ return runs.map(run => {
3327
+ const [layer] = run.styleLayers;
3328
+ if (run.type === 'background') {
3329
+ return createBackgroundLayer({idPrefix, layer, zoom, mode});
3330
+ }
3331
+ if (run.type === 'raster') {
3332
+ return createRasterLayer({
3333
+ idPrefix,
3334
+ layer,
3335
+ source: run.source!,
3336
+ zoom,
3337
+ mode,
3338
+ depthTested: depthTested.has(layer)
3339
+ });
3340
+ }
3341
+ if (run.type === 'hillshade') {
3342
+ // One tileset per raster-dem source, shared by its hillshade layers.
3343
+ let owner: DemTilesetOwner | undefined;
3344
+ let tileset = tilesets?.get(run.source!);
3345
+ if (!tileset) {
3346
+ owner = ownedDemTilesets.get(run.source!);
3347
+ if (!owner) {
3348
+ owner = {
3349
+ tileset: createDemTileset(run.source!, loadOptions),
3350
+ consumers: new Set()
3351
+ };
3352
+ ownedDemTilesets.set(run.source!, owner);
3353
+ }
3354
+ tileset = owner.tileset;
3355
+ }
3356
+ return createHillshadeLayer({
3357
+ idPrefix,
3358
+ layer,
3359
+ tileset,
3360
+ demTilesetOwner: owner,
3361
+ source: run.source!,
3362
+ zoom,
3363
+ mode,
3364
+ depthTested: depthTested.has(layer)
3365
+ });
3366
+ }
3367
+ let tileset = tilesets?.get(run.source!);
3368
+ let owner: SourceTilesetOwner | undefined;
3369
+ if (!tileset) {
3370
+ owner = ownedTilesets.get(run.source!);
3371
+ if (!owner) {
3372
+ // Tiles decode the source layers of all of the source's runs, at every zoom.
3373
+ sourceLayers ??= getRunSourceLayers(allRuns);
3374
+ const loader = new SharedMVTLayer({
3375
+ data: run.source!.tiles!,
3376
+ loadOptions: getSourceLoadOptions(loadOptions, sourceLayers.get(run.source!))
3377
+ });
3378
+ tileset = new SharedTileset2D({
3379
+ ...loader._getTilesetOptions(),
3380
+ ...getTilePyramidProps(run.source!),
3381
+ tileSize: run.source!.tileSize || 512,
3382
+ zoomOffset: VECTOR_TILE_ZOOM_OFFSET,
3383
+ getTileData: getVectorTileLoader(run.source!, loader)
3384
+ });
3385
+ owner = {tileset, consumers: new Set()};
3386
+ ownedTilesets.set(run.source!, owner);
3387
+ }
3388
+ tileset = owner.tileset;
3389
+ }
3390
+ return createVectorLayerGroup({
3391
+ idPrefix,
3392
+ id: run.id,
3393
+ sourceId: run.sourceId!,
3394
+ source: run.source!,
3395
+ tileset,
3396
+ sourceTilesetOwner: owner,
3397
+ styleLayers: run.styleLayers,
3398
+ depthTestedFrom: run.styleLayers.findIndex(styleLayer => depthTested.has(styleLayer)),
3399
+ zoom,
3400
+ config,
3401
+ loadOptions,
3402
+ mode,
3403
+ labelPriorityRanges,
3404
+ styleDefinition,
3405
+ spriteAtlases,
3406
+ fontFamily,
3407
+ glyphs,
3408
+ labelPlacement
3409
+ });
3410
+ });
3411
+ }
3412
+
3413
+ function getGlobePreLayers({
3414
+ idPrefix,
3415
+ mode,
3416
+ config,
3417
+ styleLayers,
3418
+ zoom
3419
+ }: {
3420
+ idPrefix: string;
3421
+ mode: BasemapMode;
3422
+ config: BasemapLayerConfig;
3423
+ styleLayers: BasemapStyleLayer[];
3424
+ zoom: number;
3425
+ }) {
3426
+ const layers = [];
3427
+
3428
+ if (mode === 'globe' && config.atmosphere) {
3429
+ layers.push(getGlobeAtmosphereSkyLayer());
3430
+ }
3431
+
3432
+ // A hidden or out-of-range background draws nothing, so it does not replace the fallback.
3433
+ const hasBackground = styleLayers.some(
3434
+ layer => layer.type === 'background' && isStyleLayerVisibleAtZoom(layer, zoom)
3435
+ );
3436
+ if (mode === 'globe' && !hasBackground) {
3437
+ layers.push(
3438
+ new SolidPolygonLayer({
3439
+ id: `${idPrefix}-background-fallback`,
3440
+ data: BACKGROUND_DATA,
3441
+ getPolygon: d => d,
3442
+ stroked: false,
3443
+ filled: true,
3444
+ getFillColor: [10, 24, 46, 255],
3445
+ parameters: getBackgroundParameters(mode)
3446
+ })
3447
+ );
3448
+ }
3449
+
3450
+ return layers;
3451
+ }
3452
+
3453
+ function getGlobePostLayers({
3454
+ idPrefix,
3455
+ mode,
3456
+ config,
3457
+ styleLayers,
3458
+ zoom
3459
+ }: {
3460
+ idPrefix: string;
3461
+ mode: BasemapMode;
3462
+ config: BasemapLayerConfig;
3463
+ styleLayers: BasemapStyleLayer[];
3464
+ zoom: number;
3465
+ }) {
3466
+ const layers = [];
3467
+
3468
+ if (mode === 'globe' && config.basemap) {
3469
+ layers.push(
3470
+ new SolidPolygonLayer({
3471
+ id: `${idPrefix}-background-north-pole`,
3472
+ data: BACKGROUND_NORTH_POLE_DATA,
3473
+ getPolygon: d => d,
3474
+ stroked: false,
3475
+ filled: true,
3476
+ getFillColor: inferWaterColor(styleLayers, zoom),
3477
+ parameters: getBackgroundParameters(mode)
3478
+ })
3479
+ );
3480
+ }
3481
+
3482
+ if (mode === 'globe' && config.atmosphere) {
3483
+ layers.push(getGlobeAtmosphereLayer());
3484
+ }
3485
+
3486
+ return layers;
3487
+ }