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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -2
  2. package/THIRD-PARTY-NOTICES.md +205 -0
  3. package/dist/geojson-worker.js +2160 -0
  4. package/dist/geojson-worker.js.map +6 -0
  5. package/dist/index.cjs +11388 -0
  6. package/dist/index.cjs.map +6 -0
  7. package/dist/index.js +11369 -0
  8. package/dist/index.js.map +6 -0
  9. package/dist/map-style.cjs +445 -0
  10. package/dist/map-style.cjs.map +6 -0
  11. package/dist/map-style.js +431 -0
  12. package/dist/map-style.js.map +6 -0
  13. package/dist/types/arabic-shaping.d.ts +7 -0
  14. package/dist/types/atmosphere-layer.d.ts +10 -0
  15. package/dist/types/basemap-layer.d.ts +150 -0
  16. package/dist/types/bidi.d.ts +8 -0
  17. package/dist/types/circle-layer.d.ts +49 -0
  18. package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
  19. package/dist/types/fill-outline-extension.d.ts +22 -0
  20. package/dist/types/fill-pattern.d.ts +99 -0
  21. package/dist/types/fill-style-extensions.d.ts +100 -0
  22. package/dist/types/geojson-source.d.ts +29 -0
  23. package/dist/types/geojson-tiler.d.ts +48 -0
  24. package/dist/types/geojson-worker.d.ts +40 -0
  25. package/dist/types/globe-horizon-extension.d.ts +61 -0
  26. package/dist/types/globe-layers.d.ts +239 -0
  27. package/dist/types/globe-line-angle-extension.d.ts +37 -0
  28. package/dist/types/glyph-font-renderer.d.ts +37 -0
  29. package/dist/types/glyph-pbf.d.ts +37 -0
  30. package/dist/types/glyph-store.d.ts +103 -0
  31. package/dist/types/grid-index.d.ts +47 -0
  32. package/dist/types/hillshade-layer.d.ts +154 -0
  33. package/dist/types/index.d.ts +9 -0
  34. package/dist/types/is-globe-viewport.d.ts +2 -0
  35. package/dist/types/label-font-renderer.d.ts +18 -0
  36. package/dist/types/label-placement.d.ts +247 -0
  37. package/dist/types/label-text-layer.d.ts +231 -0
  38. package/dist/types/line-blur-extension.d.ts +73 -0
  39. package/dist/types/line-glyphs.d.ts +60 -0
  40. package/dist/types/line-placement.d.ts +93 -0
  41. package/dist/types/map-style-loader.d.ts +48 -0
  42. package/dist/types/map-style-schema.d.ts +72 -0
  43. package/dist/types/map-style.d.ts +6 -0
  44. package/dist/types/mapbox-style.d.ts +40 -0
  45. package/dist/types/mvt-label-layer.d.ts +923 -0
  46. package/dist/types/polylabel.d.ts +17 -0
  47. package/dist/types/raster-color-extension.d.ts +146 -0
  48. package/dist/types/shared-texture.d.ts +34 -0
  49. package/dist/types/sort-key.d.ts +10 -0
  50. package/dist/types/sprite.d.ts +82 -0
  51. package/dist/types/style-accessor.d.ts +45 -0
  52. package/dist/types/style-expression.d.ts +83 -0
  53. package/dist/types/style-resolver.d.ts +94 -0
  54. package/dist/types/text-font.d.ts +36 -0
  55. package/dist/types/text-halo-blur-extension.d.ts +55 -0
  56. package/dist/types/text-layout.d.ts +71 -0
  57. package/dist/types/util.d.ts +22 -0
  58. package/dist/types/zoom-paint-extension.d.ts +72 -0
  59. package/package.json +80 -3
  60. package/src/arabic-shaping.ts +366 -0
  61. package/src/atmosphere-layer.ts +19 -0
  62. package/src/basemap-layer.ts +533 -0
  63. package/src/bidi.ts +199 -0
  64. package/src/circle-layer.ts +191 -0
  65. package/src/fill-extrusion-lighting-extension.ts +206 -0
  66. package/src/fill-outline-extension.ts +70 -0
  67. package/src/fill-pattern.ts +400 -0
  68. package/src/fill-style-extensions.ts +177 -0
  69. package/src/geojson-source.ts +318 -0
  70. package/src/geojson-tiler.ts +185 -0
  71. package/src/geojson-worker.ts +98 -0
  72. package/src/globe-horizon-extension.ts +64 -0
  73. package/src/globe-layers.ts +3487 -0
  74. package/src/globe-line-angle-extension.ts +131 -0
  75. package/src/glyph-font-renderer.ts +173 -0
  76. package/src/glyph-pbf.ts +164 -0
  77. package/src/glyph-store.ts +326 -0
  78. package/src/grid-index.ts +177 -0
  79. package/src/hillshade-layer.ts +775 -0
  80. package/src/index.ts +18 -0
  81. package/src/is-globe-viewport.ts +24 -0
  82. package/src/label-font-renderer.ts +159 -0
  83. package/src/label-placement.ts +727 -0
  84. package/src/label-text-layer.ts +913 -0
  85. package/src/line-blur-extension.ts +237 -0
  86. package/src/line-glyphs.ts +364 -0
  87. package/src/line-placement.ts +446 -0
  88. package/src/map-style-loader.ts +56 -0
  89. package/src/map-style-schema.ts +83 -0
  90. package/src/map-style.ts +23 -0
  91. package/src/mapbox-style.ts +168 -0
  92. package/src/mvt-label-layer.ts +3744 -0
  93. package/src/polylabel.ts +312 -0
  94. package/src/raster-color-extension.ts +245 -0
  95. package/src/shared-texture.ts +153 -0
  96. package/src/sort-key.ts +63 -0
  97. package/src/sprite.ts +244 -0
  98. package/src/style-accessor.ts +95 -0
  99. package/src/style-expression.ts +327 -0
  100. package/src/style-resolver.ts +301 -0
  101. package/src/text-font.ts +150 -0
  102. package/src/text-halo-blur-extension.ts +115 -0
  103. package/src/text-layout.ts +441 -0
  104. package/src/util.ts +47 -0
  105. package/src/zoom-paint-extension.ts +207 -0
@@ -0,0 +1,775 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT AND BSD-3-Clause
7
+ // Copyright (c) vis.gl contributors
8
+ // Copyright (c) 2023, MapLibre contributors
9
+ // Copyright (c) 2020, Mapbox
10
+
11
+ /**
12
+ * @license
13
+ * Portions of this file are adapted from MapLibre GL JS v6.13.0,
14
+ * src/shaders/glsl/hillshade.fragment.glsl and hillshade_prepare.fragment.glsl,
15
+ * under these licenses (see also THIRD-PARTY-NOTICES.md):
16
+ *
17
+ * Copyright (c) 2023, MapLibre contributors
18
+ *
19
+ * All rights reserved.
20
+ *
21
+ * Redistribution and use in source and binary forms, with or without modification,
22
+ * are permitted provided that the following conditions are met:
23
+ *
24
+ * * Redistributions of source code must retain the above copyright notice,
25
+ * this list of conditions and the following disclaimer.
26
+ * * Redistributions in binary form must reproduce the above copyright notice,
27
+ * this list of conditions and the following disclaimer in the documentation
28
+ * and/or other materials provided with the distribution.
29
+ * * Neither the name of MapLibre GL JS nor the names of its contributors
30
+ * may be used to endorse or promote products derived from this software
31
+ * without specific prior written permission.
32
+ *
33
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
34
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
35
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
36
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
37
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
38
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
39
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
40
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
41
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
42
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
43
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44
+ *
45
+ * MapLibre GL JS contains code from mapbox-gl-js v1.13 and earlier, licensed under a
46
+ * BSD-3-Clause license:
47
+ *
48
+ * Copyright (c) 2020, Mapbox
49
+ * Redistribution and use in source and binary forms, with or without modification,
50
+ * are permitted provided that the following conditions are met:
51
+ *
52
+ * * Redistributions of source code must retain the above copyright notice,
53
+ * this list of conditions and the following disclaimer.
54
+ * * Redistributions in binary form must reproduce the above copyright notice,
55
+ * this list of conditions and the following disclaimer in the documentation
56
+ * and/or other materials provided with the distribution.
57
+ * * Neither the name of Mapbox GL JS nor the names of its contributors
58
+ * may be used to endorse or promote products derived from this software
59
+ * without specific prior written permission.
60
+ *
61
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
62
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
63
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
64
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
65
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
66
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
67
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
68
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
69
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
70
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
71
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
72
+ */
73
+
74
+ import {COORDINATE_SYSTEM, log} from '@deck.gl/core';
75
+ import type {Layer, LayersList, UpdateParameters} from '@deck.gl/core';
76
+ import {SharedTile2DLayer, SharedTileset2D} from '@deck.gl-community/geo-layers';
77
+ import type {SharedTile2DLayerProps, SharedTileset2DProps} from '@deck.gl-community/geo-layers';
78
+ import {BitmapLayer} from '@deck.gl/layers';
79
+ import type {BasemapLoadOptions, BasemapSource} from './style-resolver';
80
+
81
+ /**
82
+ * `hillshade` style layers over `raster-dem` sources, drawn as MapLibre GL JS draws them.
83
+ *
84
+ * MapLibre draws a hillshade in two passes. The first ("prepare") decodes each DEM tile into
85
+ * elevations and stores the slope of every pixel, scaled for the tile's zoom, in an 8-bit
86
+ * texture one pixel larger than the tile on every side. The second shades that texture with the
87
+ * layer's paint. Here the first pass runs on the CPU, with the same arithmetic and the same 8-bit
88
+ * rounding, and the second in the fragment shader of a `BitmapLayer`, ported from MapLibre's.
89
+ */
90
+
91
+ /** The encodings of a `raster-dem` source. */
92
+ export type DemEncoding = 'mapbox' | 'terrarium' | 'custom';
93
+
94
+ /** `[redFactor, greenFactor, blueFactor, baseShift]`: elevation = r·R + g·G + b·B − shift. */
95
+ export type DemUnpackVector = [number, number, number, number];
96
+
97
+ /** The elevations of a DEM tile, in meters, row by row from the north-west corner. */
98
+ export type DemTile = {
99
+ /** Width and height in pixels. */
100
+ dim: number;
101
+ elevations: Float32Array;
102
+ };
103
+
104
+ /** The unpack vector of a `raster-dem` source, as MapLibre GL JS's `DEMData` chooses it. */
105
+ export function getDemUnpackVector(source: BasemapSource): DemUnpackVector {
106
+ switch (source.encoding) {
107
+ case 'terrarium':
108
+ return [256, 1, 1 / 256, 32768];
109
+ case 'custom':
110
+ return [
111
+ (source.redFactor as number | undefined) ?? 1,
112
+ (source.greenFactor as number | undefined) ?? 1,
113
+ (source.blueFactor as number | undefined) ?? 1,
114
+ (source.baseShift as number | undefined) ?? 0
115
+ ];
116
+ default:
117
+ // `mapbox` is the default encoding.
118
+ return [6553.6, 25.6, 0.1, 10000];
119
+ }
120
+ }
121
+
122
+ /** Decodes the RGBA pixels of a square DEM tile into elevations. */
123
+ export function decodeDemTile(
124
+ pixels: ArrayLike<number>,
125
+ width: number,
126
+ height: number,
127
+ [redFactor, greenFactor, blueFactor, baseShift]: DemUnpackVector
128
+ ): DemTile {
129
+ if (width !== height) {
130
+ throw new RangeError('DEM tiles must be square');
131
+ }
132
+ const elevations = new Float32Array(width * height);
133
+ for (let i = 0; i < elevations.length; i++) {
134
+ elevations[i] =
135
+ pixels[i * 4] * redFactor +
136
+ pixels[i * 4 + 1] * greenFactor +
137
+ pixels[i * 4 + 2] * blueFactor -
138
+ baseShift;
139
+ }
140
+ return {dim: width, elevations};
141
+ }
142
+
143
+ /**
144
+ * The DEM tile next to a tile, `dx` and `dy` (each -1, 0 or 1, not both 0) tiles away, or `null`
145
+ * where it is not loaded.
146
+ */
147
+ export type DemNeighbors = (dx: number, dy: number) => DemTile | null;
148
+
149
+ /**
150
+ * The slope texture of MapLibre GL JS's hillshade "prepare" pass, `dim + 2` pixels square: the
151
+ * tile and a one-pixel border, RGBA, with the scaled east-west and north-south slopes in red and
152
+ * green (128 is flat). Pixels past the tile's edges come from its loaded neighbors, and where a
153
+ * neighbor is missing, from the tile's nearest edge pixel, as MapLibre fills them.
154
+ *
155
+ * `zoom` is the tile's zoom, also where it is scaled up past its source's `maxzoom`. MapLibre
156
+ * scales the slopes by it, and exaggerates them below zoom 15.
157
+ */
158
+ export function prepareHillshadeTexture(
159
+ dem: DemTile,
160
+ zoom: number,
161
+ getNeighbor: DemNeighbors = () => null
162
+ ): Uint8ClampedArray<ArrayBuffer> {
163
+ const {dim} = dem;
164
+ const neighbors: (DemTile | null)[] = [];
165
+ for (let dy = -1; dy <= 1; dy++) {
166
+ for (let dx = -1; dx <= 1; dx++) {
167
+ const neighbor = dx || dy ? getNeighbor(dx, dy) : dem;
168
+ neighbors.push(neighbor && neighbor.dim === dim ? neighbor : null);
169
+ }
170
+ }
171
+ // The elevation at a pixel of the tile or of its two-pixel border.
172
+ const getElevation = (x: number, y: number): number => {
173
+ const dx = x < 0 ? -1 : x >= dim ? 1 : 0;
174
+ const dy = y < 0 ? -1 : y >= dim ? 1 : 0;
175
+ const neighbor = neighbors[(dy + 1) * 3 + dx + 1];
176
+ if (neighbor) {
177
+ return neighbor.elevations[(y - dy * dim) * dim + (x - dx * dim)];
178
+ }
179
+ const clampedX = Math.min(Math.max(x, 0), dim - 1);
180
+ const clampedY = Math.min(Math.max(y, 0), dim - 1);
181
+ return dem.elevations[clampedY * dim + clampedX];
182
+ };
183
+
184
+ const exaggerationFactor = zoom < 2 ? 0.4 : zoom < 4.5 ? 0.35 : 0.3;
185
+ const exaggeration = zoom < 15 ? (zoom - 15) * exaggerationFactor : 0;
186
+ const scale = dim / 2 ** (exaggeration + (28.2562 - zoom)) / 8;
187
+ const flat = 128 / 255;
188
+
189
+ const size = dim + 2;
190
+ const texture = new Uint8ClampedArray(size * size * 4);
191
+ for (let row = 0; row < size; row++) {
192
+ // Texture pixel (column, row) is tile pixel (column - 1, row - 1).
193
+ const y = row - 1;
194
+ for (let column = 0; column < size; column++) {
195
+ const x = column - 1;
196
+ const a = getElevation(x - 1, y - 1);
197
+ const b = getElevation(x, y - 1);
198
+ const c = getElevation(x + 1, y - 1);
199
+ const d = getElevation(x - 1, y);
200
+ const f = getElevation(x + 1, y);
201
+ const g = getElevation(x - 1, y + 1);
202
+ const h = getElevation(x, y + 1);
203
+ const i = getElevation(x + 1, y + 1);
204
+ const index = (row * size + column) * 4;
205
+ // Rounded to 8 bits as the GPU stores MapLibre's framebuffer.
206
+ texture[index] = Math.round(
207
+ Math.min(Math.max((c + f + f + i - (a + d + d + g)) * scale + flat, 0), 1) * 255
208
+ );
209
+ texture[index + 1] = Math.round(
210
+ Math.min(Math.max((g + h + h + i - (a + b + b + c)) * scale + flat, 0), 1) * 255
211
+ );
212
+ texture[index + 2] = 255;
213
+ texture[index + 3] = 255;
214
+ }
215
+ }
216
+ return texture;
217
+ }
218
+
219
+ /** `hillshade-method`, as the shader's `method` uniform. */
220
+ const HILLSHADE_METHODS: Record<string, number> = {
221
+ standard: 0,
222
+ combined: 1,
223
+ igor: 2,
224
+ multidirectional: 3,
225
+ basic: 4
226
+ };
227
+
228
+ /** The most light sources the shader takes. MapLibre takes any number. */
229
+ export const MAX_HILLSHADE_LIGHTS = 4;
230
+
231
+ /** A hillshade layer's paint, evaluated and ready for the shader (but for the map's bearing). */
232
+ export type HillshadeParameters = {
233
+ /** `hillshade-method`. */
234
+ method: number;
235
+ /** `hillshade-exaggeration`. */
236
+ exaggeration: number;
237
+ /** `hillshade-accent-color`, premultiplied RGBA in 0-1. */
238
+ accent: number[];
239
+ /** Whether the light turns with the map (`hillshade-illumination-anchor: viewport`). */
240
+ viewportAnchored: boolean;
241
+ /** One per light source, at most `MAX_HILLSHADE_LIGHTS`. Directions clockwise from north. */
242
+ directions: number[];
243
+ altitudes: number[];
244
+ /** Premultiplied RGBA in 0-1. */
245
+ shadows: number[][];
246
+ highlights: number[][];
247
+ };
248
+
249
+ function toNumbers(value: unknown, fallback: number): number[] {
250
+ const values = (Array.isArray(value) ? value : [value]).filter(
251
+ (item): item is number => typeof item === 'number'
252
+ );
253
+ return values.length ? values : [fallback];
254
+ }
255
+
256
+ /** `[r, g, b, a]`, RGB in 0-255 and alpha in 0-1, to premultiplied RGBA in 0-1. */
257
+ function toPremultiplied(color: unknown): number[] {
258
+ if (!Array.isArray(color) || color.length < 3) {
259
+ return [0, 0, 0, 0];
260
+ }
261
+ const alpha = color[3] ?? 1;
262
+ return [(color[0] / 255) * alpha, (color[1] / 255) * alpha, (color[2] / 255) * alpha, alpha];
263
+ }
264
+
265
+ function toColors(value: unknown, fallback: number[]): number[][] {
266
+ if (!Array.isArray(value) || value.length === 0) {
267
+ return [fallback];
268
+ }
269
+ // A color array is a list of colors; a single color is a list of numbers.
270
+ return (Array.isArray(value[0]) ? value : [value]).map(toPremultiplied);
271
+ }
272
+
273
+ /** Lengthens a list to `length` by repeating its last item, as MapLibre does. */
274
+ function padList<T>(list: T[], length: number): T[] {
275
+ return list.concat(Array(length - list.length).fill(list[list.length - 1]));
276
+ }
277
+
278
+ /**
279
+ * The shader parameters of a hillshade layer's evaluated paint (see `getPaint`), with the
280
+ * specification's defaults for unset properties. As in MapLibre GL JS, the number of light sources
281
+ * is the number of `hillshade-highlight-color`s, and the other lists are lengthened to match by
282
+ * repeating their last value; only `multidirectional` uses more than the first.
283
+ */
284
+ export function getHillshadeParameters(paint: Record<string, unknown>): HillshadeParameters {
285
+ let directions = toNumbers(paint['hillshade-illumination-direction'], 335);
286
+ let altitudes = toNumbers(paint['hillshade-illumination-altitude'], 45);
287
+ let shadows = toColors(paint['hillshade-shadow-color'], [0, 0, 0, 1]);
288
+ let highlights = toColors(paint['hillshade-highlight-color'], [1, 1, 1, 1]);
289
+ const lights = Math.min(highlights.length, MAX_HILLSHADE_LIGHTS);
290
+ const count = Math.max(directions.length, altitudes.length, shadows.length, highlights.length);
291
+ directions = padList(directions, count);
292
+ altitudes = padList(altitudes, count);
293
+ shadows = padList(shadows, count);
294
+ highlights = padList(highlights, count);
295
+ return {
296
+ method: HILLSHADE_METHODS[paint['hillshade-method'] as string] ?? 0,
297
+ exaggeration: (paint['hillshade-exaggeration'] as number | undefined) ?? 0.5,
298
+ accent: paint['hillshade-accent-color']
299
+ ? toPremultiplied(paint['hillshade-accent-color'])
300
+ : [0, 0, 0, 1],
301
+ viewportAnchored: paint['hillshade-illumination-anchor'] !== 'map',
302
+ directions: directions.slice(0, lights),
303
+ altitudes: altitudes.slice(0, lights),
304
+ shadows: shadows.slice(0, lights),
305
+ highlights: highlights.slice(0, lights)
306
+ };
307
+ }
308
+
309
+ /** The `hillshade` shader module's uniforms. */
310
+ export type HillshadeUniforms = {
311
+ accent: number[];
312
+ altitudes: number[];
313
+ azimuths: number[];
314
+ shadows: number[];
315
+ highlights: number[];
316
+ latRange: [number, number];
317
+ exaggeration: number;
318
+ method: number;
319
+ lightCount: number;
320
+ };
321
+
322
+ /**
323
+ * The shader uniforms of a tile: its parameters, at the map's `bearing` (degrees) for a light
324
+ * anchored to the viewport, and the tile's latitude range `[north, south]`.
325
+ */
326
+ export function getHillshadeUniforms(
327
+ parameters: HillshadeParameters,
328
+ bearing: number,
329
+ latRange: [number, number]
330
+ ): HillshadeUniforms {
331
+ const toRadians = (degrees: number) => (degrees * Math.PI) / 180;
332
+ // MapLibre wraps its bearing to [-180, 180].
333
+ const wrappedBearing = ((((bearing + 180) % 360) + 360) % 360) - 180;
334
+ const rotation = parameters.viewportAnchored ? toRadians(wrappedBearing) : 0;
335
+ const lightCount = parameters.highlights.length;
336
+ const toVec4 = (values: number[]) => padList(values, 4).slice(0, 4);
337
+ // Columns of a mat4: one color per light.
338
+ const toMat4 = (colors: number[][]) => padList(colors, 4).slice(0, 4).flat();
339
+ return {
340
+ accent: parameters.accent,
341
+ altitudes: toVec4(parameters.altitudes.map(toRadians)),
342
+ azimuths: toVec4(parameters.directions.map(direction => toRadians(direction) + rotation)),
343
+ shadows: toMat4(parameters.shadows),
344
+ highlights: toMat4(parameters.highlights),
345
+ latRange,
346
+ exaggeration: parameters.exaggeration,
347
+ method: parameters.method,
348
+ lightCount
349
+ };
350
+ }
351
+
352
+ const hillshadeModule = {
353
+ name: 'hillshade',
354
+ fs: /* glsl */ `\
355
+ layout(std140) uniform hillshadeUniforms {
356
+ vec4 accent;
357
+ vec4 altitudes;
358
+ vec4 azimuths;
359
+ mat4 shadows;
360
+ mat4 highlights;
361
+ vec2 latRange;
362
+ float exaggeration;
363
+ float method;
364
+ float lightCount;
365
+ } hillshade;
366
+
367
+ // MapLibre GL JS's hillshade fragment shader, for up to four light sources.
368
+ const float HILLSHADE_PI = 3.141592653589793;
369
+
370
+ float hillshade_getAspect(vec2 deriv) {
371
+ return deriv.x != 0.0 ? atan(deriv.y, -deriv.x) : HILLSHADE_PI / 2.0 * (deriv.y > 0.0 ? 1.0 : -1.0);
372
+ }
373
+
374
+ vec4 hillshade_igor(vec2 deriv) {
375
+ deriv = deriv * hillshade.exaggeration * 2.0;
376
+ float aspect = hillshade_getAspect(deriv);
377
+ float azimuth = hillshade.azimuths[0] + HILLSHADE_PI;
378
+ float slopeStrength = atan(length(deriv)) * 2.0 / HILLSHADE_PI;
379
+ float aspectStrength = 1.0 - abs(mod((aspect + azimuth) / HILLSHADE_PI + 0.5, 2.0) - 1.0);
380
+ float shadowStrength = slopeStrength * aspectStrength;
381
+ float highlightStrength = slopeStrength * (1.0 - aspectStrength);
382
+ return hillshade.shadows[0] * shadowStrength + hillshade.highlights[0] * highlightStrength;
383
+ }
384
+
385
+ vec4 hillshade_standard(vec2 deriv) {
386
+ float azimuth = hillshade.azimuths[0] + HILLSHADE_PI;
387
+ float slope = atan(0.625 * length(deriv));
388
+ float aspect = hillshade_getAspect(deriv);
389
+ float intensity = hillshade.exaggeration;
390
+ float base = 1.875 - intensity * 1.75;
391
+ float maxValue = 0.5 * HILLSHADE_PI;
392
+ float scaledSlope = intensity != 0.5
393
+ ? ((pow(base, slope) - 1.0) / (pow(base, maxValue) - 1.0)) * maxValue
394
+ : slope;
395
+ float accent = cos(scaledSlope);
396
+ vec4 accentColor = (1.0 - accent) * hillshade.accent * clamp(intensity * 2.0, 0.0, 1.0);
397
+ float shade = abs(mod((aspect + azimuth) / HILLSHADE_PI + 0.5, 2.0) - 1.0);
398
+ vec4 shadeColor = mix(hillshade.shadows[0], hillshade.highlights[0], shade) *
399
+ sin(scaledSlope) * clamp(intensity * 2.0, 0.0, 1.0);
400
+ return accentColor * (1.0 - shadeColor.a) + shadeColor;
401
+ }
402
+
403
+ vec4 hillshade_basic(vec2 deriv) {
404
+ deriv = deriv * hillshade.exaggeration * 2.0;
405
+ float azimuth = hillshade.azimuths[0] + HILLSHADE_PI;
406
+ float cosAz = cos(azimuth);
407
+ float sinAz = sin(azimuth);
408
+ float cosAlt = cos(hillshade.altitudes[0]);
409
+ float sinAlt = sin(hillshade.altitudes[0]);
410
+ float cang = (sinAlt - (deriv.y * cosAz * cosAlt - deriv.x * sinAz * cosAlt)) /
411
+ sqrt(1.0 + dot(deriv, deriv));
412
+ float shade = clamp(cang, 0.0, 1.0);
413
+ return shade > 0.5
414
+ ? hillshade.highlights[0] * (2.0 * shade - 1.0)
415
+ : hillshade.shadows[0] * (1.0 - 2.0 * shade);
416
+ }
417
+
418
+ vec4 hillshade_multidirectional(vec2 deriv) {
419
+ deriv = deriv * hillshade.exaggeration * 2.0;
420
+ vec4 color = vec4(0.0);
421
+ for (int i = 0; i < 4; i++) {
422
+ if (float(i) >= hillshade.lightCount) {
423
+ break;
424
+ }
425
+ float cosAlt = cos(hillshade.altitudes[i]);
426
+ float sinAlt = sin(hillshade.altitudes[i]);
427
+ float cosAz = -cos(hillshade.azimuths[i]);
428
+ float sinAz = -sin(hillshade.azimuths[i]);
429
+ float cang = (sinAlt - (deriv.y * cosAz * cosAlt - deriv.x * sinAz * cosAlt)) /
430
+ sqrt(1.0 + dot(deriv, deriv));
431
+ float shade = clamp(cang, 0.0, 1.0);
432
+ color += (shade > 0.5
433
+ ? hillshade.highlights[i] * (2.0 * shade - 1.0)
434
+ : hillshade.shadows[i] * (1.0 - 2.0 * shade)) / hillshade.lightCount;
435
+ }
436
+ return color;
437
+ }
438
+
439
+ vec4 hillshade_combined(vec2 deriv) {
440
+ deriv = deriv * hillshade.exaggeration * 2.0;
441
+ float azimuth = hillshade.azimuths[0] + HILLSHADE_PI;
442
+ float cosAz = cos(azimuth);
443
+ float sinAz = sin(azimuth);
444
+ float cosAlt = cos(hillshade.altitudes[0]);
445
+ float sinAlt = sin(hillshade.altitudes[0]);
446
+ float cang = acos((sinAlt - (deriv.y * cosAz * cosAlt - deriv.x * sinAz * cosAlt)) /
447
+ sqrt(1.0 + dot(deriv, deriv)));
448
+ cang = clamp(cang, 0.0, HILLSHADE_PI / 2.0);
449
+ float shade = cang * atan(length(deriv)) * 4.0 / HILLSHADE_PI / HILLSHADE_PI;
450
+ float highlight = (HILLSHADE_PI / 2.0 - cang) * atan(length(deriv)) * 4.0 / HILLSHADE_PI / HILLSHADE_PI;
451
+ return hillshade.shadows[0] * shade + hillshade.highlights[0] * highlight;
452
+ }
453
+
454
+ // The premultiplied color of a pixel of the slope texture, at \`uv\` in the tile.
455
+ vec4 hillshade_getColor(sampler2D slopes, vec2 uv) {
456
+ // The texture has a one-pixel border around the tile.
457
+ vec2 size = vec2(textureSize(slopes, 0));
458
+ vec4 pixel = texture(slopes, (uv * (size - 2.0) + 1.0) / size);
459
+ // Slopes are scaled by the cosine of the latitude, interpolated across the tile.
460
+ float scaleFactor = cos(radians(
461
+ (hillshade.latRange[0] - hillshade.latRange[1]) * (1.0 - uv.y) + hillshade.latRange[1]
462
+ ));
463
+ vec2 deriv = (pixel.rg - 128.0 / 255.0) * 8.0 / scaleFactor;
464
+ if (hillshade.method > 3.5) {
465
+ return hillshade_basic(deriv);
466
+ }
467
+ if (hillshade.method > 2.5) {
468
+ return hillshade_multidirectional(deriv);
469
+ }
470
+ if (hillshade.method > 1.5) {
471
+ return hillshade_igor(deriv);
472
+ }
473
+ if (hillshade.method > 0.5) {
474
+ return hillshade_combined(deriv);
475
+ }
476
+ return hillshade_standard(deriv);
477
+ }
478
+ `,
479
+ uniformTypes: {
480
+ accent: 'vec4<f32>',
481
+ altitudes: 'vec4<f32>',
482
+ azimuths: 'vec4<f32>',
483
+ shadows: 'mat4x4<f32>',
484
+ highlights: 'mat4x4<f32>',
485
+ latRange: 'vec2<f32>',
486
+ exaggeration: 'f32',
487
+ method: 'f32',
488
+ lightCount: 'f32'
489
+ }
490
+ } as const;
491
+
492
+ /**
493
+ * The blending of the hillshade's premultiplied colors, as MapLibre blends them (and as
494
+ * `RASTER_COLOR_BLEND_PARAMETERS`).
495
+ */
496
+ const PREMULTIPLIED_BLEND_PARAMETERS = {
497
+ blendColorOperation: 'add',
498
+ blendColorSrcFactor: 'one',
499
+ blendColorDstFactor: 'one-minus-src-alpha',
500
+ blendAlphaOperation: 'add',
501
+ blendAlphaSrcFactor: 'one',
502
+ blendAlphaDstFactor: 'one-minus-src-alpha'
503
+ } as const;
504
+
505
+ type HillshadeBitmapLayerProps = {
506
+ hillshadeParameters: HillshadeParameters;
507
+ /** The tile's latitude range, `[north, south]`. */
508
+ hillshadeLatRange: [number, number];
509
+ };
510
+
511
+ /**
512
+ * A `BitmapLayer` whose image is a tile's slope texture (see `prepareHillshadeTexture`), drawn as
513
+ * MapLibre GL JS shades it. WebGL only.
514
+ */
515
+ class HillshadeBitmapLayer extends BitmapLayer<HillshadeBitmapLayerProps> {
516
+ static layerName = 'HillshadeBitmapLayer';
517
+
518
+ getShaders() {
519
+ const shaders = super.getShaders();
520
+ return {
521
+ ...shaders,
522
+ modules: [...shaders.modules, hillshadeModule],
523
+ inject: {
524
+ ...shaders.inject,
525
+ // At the end of main(), where the bitmap's sampler is in scope.
526
+ 'fs:#main-end': /* glsl */ `
527
+ if (!bool(picking.isActive)) {
528
+ fragColor = hillshade_getColor(bitmapTexture, geometry.uv) * layer.opacity;
529
+ }
530
+ `
531
+ }
532
+ };
533
+ }
534
+
535
+ draw(options: any) {
536
+ const {hillshadeParameters, hillshadeLatRange} = this.props;
537
+ const bearing = (this.context.viewport as {bearing?: number}).bearing ?? 0;
538
+ for (const model of this.getModels()) {
539
+ model.shaderInputs.setProps({
540
+ hillshade: getHillshadeUniforms(hillshadeParameters, bearing, hillshadeLatRange)
541
+ });
542
+ }
543
+ super.draw(options);
544
+ }
545
+ }
546
+
547
+ /** Reads the pixels of a decoded image. */
548
+ function readImagePixels(image: ImageBitmap): Uint8ClampedArray {
549
+ const {width, height} = image;
550
+ const canvas =
551
+ typeof OffscreenCanvas === 'undefined'
552
+ ? Object.assign(document.createElement('canvas'), {width, height})
553
+ : new OffscreenCanvas(width, height);
554
+ const context = canvas.getContext('2d', {willReadFrequently: true}) as
555
+ | CanvasRenderingContext2D
556
+ | OffscreenCanvasRenderingContext2D;
557
+ context.drawImage(image, 0, 0);
558
+ return context.getImageData(0, 0, width, height).data;
559
+ }
560
+
561
+ /**
562
+ * Fetches and decodes a DEM tile. Colors are read as stored, without color space conversion or
563
+ * premultiplication, which would change the elevations. An empty response is a tile without data.
564
+ */
565
+ export async function loadDemTile(
566
+ url: string,
567
+ unpack: DemUnpackVector,
568
+ {signal, loadOptions}: {signal?: AbortSignal; loadOptions?: BasemapLoadOptions} = {}
569
+ ): Promise<DemTile | null> {
570
+ const fetchFn = loadOptions?.fetch || fetch;
571
+ const response = await fetchFn(url, {...loadOptions?.fetchOptions, signal});
572
+ if (!response.ok) {
573
+ throw new Error(`Failed to load DEM tile: ${url} (${response.status})`);
574
+ }
575
+ const blob = await response.blob();
576
+ if (blob.size === 0) {
577
+ return null;
578
+ }
579
+ const image = await createImageBitmap(blob, {
580
+ premultiplyAlpha: 'none',
581
+ colorSpaceConversion: 'none'
582
+ });
583
+ try {
584
+ return decodeDemTile(readImagePixels(image), image.width, image.height, unpack);
585
+ } finally {
586
+ image.close();
587
+ }
588
+ }
589
+
590
+ /** The slope texture of a DEM tile, with the neighbors it was prepared with. */
591
+ const preparedTextures = new WeakMap<DemTile, {key: string; image: ImageData}>();
592
+
593
+ type TileIndex = {x: number; y: number; z: number};
594
+ type DemTileHeader = {
595
+ id: string;
596
+ index: TileIndex;
597
+ bbox: unknown;
598
+ content: DemTile | null;
599
+ isLoaded: boolean;
600
+ };
601
+
602
+ /**
603
+ * Creates the tileset of a `raster-dem` source: each tile is fetched and decoded once, and shared
604
+ * by every `HillshadeTileLayer` drawn from it, as the tiles of a vector source are shared by its
605
+ * style runs. `tilesetProps` are the source's tile pyramid (see `getTilePyramidProps`).
606
+ */
607
+ export function createSharedDemTileset(
608
+ source: BasemapSource,
609
+ loadOptions: BasemapLoadOptions | undefined,
610
+ tilesetProps: Partial<SharedTileset2DProps<DemTile | null>> = {}
611
+ ): SharedTileset2D<DemTile | null> {
612
+ const unpack = getDemUnpackVector(source);
613
+ // Expands the source's URL template for a tile, as the layer does for its own tileset.
614
+ const loader = new SharedTile2DLayer<DemTile | null>({
615
+ data: source.tiles as string[],
616
+ getTileData: ({url, signal}) => (url ? loadDemTile(url, unpack, {signal, loadOptions}) : null)
617
+ });
618
+ return new SharedTileset2D<DemTile | null>({
619
+ ...loader._getTilesetOptions(),
620
+ ...tilesetProps,
621
+ tileSize: source.tileSize || 512,
622
+ getTileData: tile => loader.getTileData(tile)
623
+ } as SharedTileset2DProps<DemTile | null>);
624
+ }
625
+
626
+ /**
627
+ * A tileset shared by the `HillshadeTileLayer`s of one `raster-dem` source, finalized when the
628
+ * last of them is. `BasemapLayer` owns its sources' tilesets itself; this is for the layers that
629
+ * `getBasemapLayers` creates without it.
630
+ */
631
+ export type DemTilesetOwner = {tileset: SharedTileset2D; consumers: Set<object>};
632
+
633
+ /** Props of a `HillshadeTileLayer`. */
634
+ export type HillshadeTileLayerProps = SharedTile2DLayerProps<DemTile | null> & {
635
+ hillshadeParameters: HillshadeParameters;
636
+ /** Extra props of every tile's bitmap layer, such as `parameters`. */
637
+ hillshadeSubLayerProps?: Record<string, unknown>;
638
+ /** The owner of a shared `data` tileset that the layer releases when finalized. */
639
+ demTilesetOwner?: DemTilesetOwner | null;
640
+ };
641
+
642
+ /**
643
+ * The tiles of a `raster-dem` source, shaded by one `hillshade` style layer. The decoded tiles
644
+ * come from the source's tileset, shared with the source's other hillshade layers (see
645
+ * `createSharedDemTileset`); the slopes and the shading are the layer's own. Each tile's slopes are
646
+ * prepared from its elevations and those of its loaded neighbors, so that tiles join without
647
+ * seams. Draws nothing on WebGPU: the shader is GLSL.
648
+ */
649
+ export class HillshadeTileLayer extends SharedTile2DLayer<DemTile | null, HillshadeTileLayerProps> {
650
+ static layerName = 'HillshadeTileLayer';
651
+ static defaultProps = {
652
+ ...SharedTile2DLayer.defaultProps,
653
+ hillshadeParameters: {type: 'object', value: null, compare: false},
654
+ hillshadeSubLayerProps: {type: 'object', value: null, compare: false},
655
+ demTilesetOwner: {type: 'object', value: null, compare: false}
656
+ };
657
+
658
+ declare state: SharedTile2DLayer<DemTile | null>['state'] & {
659
+ demTilesetOwner?: DemTilesetOwner;
660
+ /** Stands for this layer among an owner's consumers. */
661
+ demTilesetConsumer?: object;
662
+ };
663
+
664
+ updateState(params: UpdateParameters<this>): void {
665
+ const owner = this.props.demTilesetOwner || undefined;
666
+ if (owner !== this.state.demTilesetOwner) {
667
+ this.releaseDemTileset();
668
+ this.state.demTilesetConsumer ??= {};
669
+ owner?.consumers.add(this.state.demTilesetConsumer);
670
+ this.state.demTilesetOwner = owner;
671
+ }
672
+ super.updateState(params);
673
+ }
674
+
675
+ finalizeState(): void {
676
+ super.finalizeState();
677
+ this.releaseDemTileset();
678
+ }
679
+
680
+ private releaseDemTileset(): void {
681
+ const owner = this.state.demTilesetOwner;
682
+ if (owner) {
683
+ owner.consumers.delete(this.state.demTilesetConsumer!);
684
+ if (!owner.consumers.size) owner.tileset.finalize();
685
+ this.state.demTilesetOwner = undefined;
686
+ }
687
+ }
688
+
689
+ _onTileLoad(tile: any): void {
690
+ // The tiles around it can now take its edge pixels: their sublayers are prepared again.
691
+ const {x, y, z} = tile.index as TileIndex;
692
+ const count = 2 ** z;
693
+ for (const other of (this.state.tileset?.tiles || []) as DemTileHeader[]) {
694
+ const index = other.index;
695
+ const dx = Math.abs(index.x - x);
696
+ if (
697
+ index.z === z &&
698
+ Math.abs(index.y - y) <= 1 &&
699
+ (dx <= 1 || dx === count - 1) &&
700
+ other !== tile
701
+ ) {
702
+ this.state.tileLayers.delete(other.id);
703
+ }
704
+ }
705
+ super._onTileLoad(tile);
706
+ }
707
+
708
+ renderLayers(): Layer | null | LayersList {
709
+ if (this.context.device.type === 'webgpu') {
710
+ log.once(1, '[BasemapLayer] hillshade layers are not drawn on WebGPU')();
711
+ return null;
712
+ }
713
+ return super.renderLayers();
714
+ }
715
+
716
+ renderSubLayers(props: any): Layer | null | LayersList {
717
+ const dem = props.data as DemTile | null;
718
+ const tile = props.tile as DemTileHeader;
719
+ if (!dem || typeof ImageData === 'undefined') {
720
+ return null;
721
+ }
722
+ const {x, y, z} = tile.index;
723
+
724
+ const loaded = new Map<string, DemTile>();
725
+ for (const other of (this.state.tileset?.tiles || []) as DemTileHeader[]) {
726
+ if (other.index.z === z && other.isLoaded && other.content) {
727
+ loaded.set(`${other.index.x}/${other.index.y}`, other.content);
728
+ }
729
+ }
730
+ const count = 2 ** z;
731
+ const getNeighbor: DemNeighbors = (dx, dy) =>
732
+ loaded.get(`${(x + dx + count) % count}/${y + dy}`) ?? null;
733
+ // Which neighbors are loaded decides the texture's border.
734
+ const present = [-1, 0, 1]
735
+ .flatMap(dy => [-1, 0, 1].map(dx => ((dx || dy) && getNeighbor(dx, dy) ? 1 : 0)))
736
+ .join('');
737
+ const key = present;
738
+ // Kept with the decoded tile, so the source's hillshade layers prepare its slopes once.
739
+ let prepared = preparedTextures.get(dem);
740
+ if (prepared?.key !== key) {
741
+ const size = dem.dim + 2;
742
+ const pixels = prepareHillshadeTexture(dem, z, getNeighbor);
743
+ prepared = {key, image: new ImageData(pixels, size, size)};
744
+ preparedTextures.set(dem, prepared);
745
+ }
746
+
747
+ const {west, south, east, north} = tile.bbox as {
748
+ west: number;
749
+ south: number;
750
+ east: number;
751
+ north: number;
752
+ };
753
+ const {hillshadeParameters, hillshadeSubLayerProps} = this.props;
754
+ const textureParameters = hillshadeSubLayerProps?.textureParameters as
755
+ | {magFilter?: string}
756
+ | undefined;
757
+ return new HillshadeBitmapLayer({
758
+ ...props,
759
+ ...hillshadeSubLayerProps,
760
+ // A texture keeps the filter it was created with, so a filter change needs a new layer.
761
+ id: textureParameters?.magFilter === 'nearest' ? `${props.id}-nearest` : props.id,
762
+ parameters: {
763
+ ...(hillshadeSubLayerProps?.parameters as object),
764
+ ...PREMULTIPLIED_BLEND_PARAMETERS
765
+ },
766
+ data: null,
767
+ // Positions in the tile are Web Mercator, also on a globe.
768
+ _imageCoordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
769
+ image: prepared.image,
770
+ bounds: [west, south, east, north],
771
+ hillshadeParameters,
772
+ hillshadeLatRange: [north, south]
773
+ } as any);
774
+ }
775
+ }