@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.
- package/README.md +34 -2
- package/THIRD-PARTY-NOTICES.md +205 -0
- package/dist/geojson-worker.js +2160 -0
- package/dist/geojson-worker.js.map +6 -0
- package/dist/index.cjs +11388 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.js +11369 -0
- package/dist/index.js.map +6 -0
- package/dist/map-style.cjs +445 -0
- package/dist/map-style.cjs.map +6 -0
- package/dist/map-style.js +431 -0
- package/dist/map-style.js.map +6 -0
- package/dist/types/arabic-shaping.d.ts +7 -0
- package/dist/types/atmosphere-layer.d.ts +10 -0
- package/dist/types/basemap-layer.d.ts +150 -0
- package/dist/types/bidi.d.ts +8 -0
- package/dist/types/circle-layer.d.ts +49 -0
- package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
- package/dist/types/fill-outline-extension.d.ts +22 -0
- package/dist/types/fill-pattern.d.ts +99 -0
- package/dist/types/fill-style-extensions.d.ts +100 -0
- package/dist/types/geojson-source.d.ts +29 -0
- package/dist/types/geojson-tiler.d.ts +48 -0
- package/dist/types/geojson-worker.d.ts +40 -0
- package/dist/types/globe-horizon-extension.d.ts +61 -0
- package/dist/types/globe-layers.d.ts +239 -0
- package/dist/types/globe-line-angle-extension.d.ts +37 -0
- package/dist/types/glyph-font-renderer.d.ts +37 -0
- package/dist/types/glyph-pbf.d.ts +37 -0
- package/dist/types/glyph-store.d.ts +103 -0
- package/dist/types/grid-index.d.ts +47 -0
- package/dist/types/hillshade-layer.d.ts +154 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/is-globe-viewport.d.ts +2 -0
- package/dist/types/label-font-renderer.d.ts +18 -0
- package/dist/types/label-placement.d.ts +247 -0
- package/dist/types/label-text-layer.d.ts +231 -0
- package/dist/types/line-blur-extension.d.ts +73 -0
- package/dist/types/line-glyphs.d.ts +60 -0
- package/dist/types/line-placement.d.ts +93 -0
- package/dist/types/map-style-loader.d.ts +48 -0
- package/dist/types/map-style-schema.d.ts +72 -0
- package/dist/types/map-style.d.ts +6 -0
- package/dist/types/mapbox-style.d.ts +40 -0
- package/dist/types/mvt-label-layer.d.ts +923 -0
- package/dist/types/polylabel.d.ts +17 -0
- package/dist/types/raster-color-extension.d.ts +146 -0
- package/dist/types/shared-texture.d.ts +34 -0
- package/dist/types/sort-key.d.ts +10 -0
- package/dist/types/sprite.d.ts +82 -0
- package/dist/types/style-accessor.d.ts +45 -0
- package/dist/types/style-expression.d.ts +83 -0
- package/dist/types/style-resolver.d.ts +94 -0
- package/dist/types/text-font.d.ts +36 -0
- package/dist/types/text-halo-blur-extension.d.ts +55 -0
- package/dist/types/text-layout.d.ts +71 -0
- package/dist/types/util.d.ts +22 -0
- package/dist/types/zoom-paint-extension.d.ts +72 -0
- package/package.json +80 -3
- package/src/arabic-shaping.ts +366 -0
- package/src/atmosphere-layer.ts +19 -0
- package/src/basemap-layer.ts +533 -0
- package/src/bidi.ts +199 -0
- package/src/circle-layer.ts +191 -0
- package/src/fill-extrusion-lighting-extension.ts +206 -0
- package/src/fill-outline-extension.ts +70 -0
- package/src/fill-pattern.ts +400 -0
- package/src/fill-style-extensions.ts +177 -0
- package/src/geojson-source.ts +318 -0
- package/src/geojson-tiler.ts +185 -0
- package/src/geojson-worker.ts +98 -0
- package/src/globe-horizon-extension.ts +64 -0
- package/src/globe-layers.ts +3487 -0
- package/src/globe-line-angle-extension.ts +131 -0
- package/src/glyph-font-renderer.ts +173 -0
- package/src/glyph-pbf.ts +164 -0
- package/src/glyph-store.ts +326 -0
- package/src/grid-index.ts +177 -0
- package/src/hillshade-layer.ts +775 -0
- package/src/index.ts +18 -0
- package/src/is-globe-viewport.ts +24 -0
- package/src/label-font-renderer.ts +159 -0
- package/src/label-placement.ts +727 -0
- package/src/label-text-layer.ts +913 -0
- package/src/line-blur-extension.ts +237 -0
- package/src/line-glyphs.ts +364 -0
- package/src/line-placement.ts +446 -0
- package/src/map-style-loader.ts +56 -0
- package/src/map-style-schema.ts +83 -0
- package/src/map-style.ts +23 -0
- package/src/mapbox-style.ts +168 -0
- package/src/mvt-label-layer.ts +3744 -0
- package/src/polylabel.ts +312 -0
- package/src/raster-color-extension.ts +245 -0
- package/src/shared-texture.ts +153 -0
- package/src/sort-key.ts +63 -0
- package/src/sprite.ts +244 -0
- package/src/style-accessor.ts +95 -0
- package/src/style-expression.ts +327 -0
- package/src/style-resolver.ts +301 -0
- package/src/text-font.ts +150 -0
- package/src/text-halo-blur-extension.ts +115 -0
- package/src/text-layout.ts +441 -0
- package/src/util.ts +47 -0
- 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
|
+
}
|