@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,312 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ // Point labels on polygons sit at the polygon's pole of inaccessibility: the interior point
10
+ // farthest from the outline. MapLibre places them there too. This file finds that point with the
11
+ // grid refinement of Garcia-Castellanos and Lombardo, "Poles of inaccessibility: a calculation
12
+ // algorithm for the remotest places on Earth" (Scottish Geographical Journal 123(3), 2007),
13
+ // in its best-first form: candidate squares wait in a priority queue, ordered by the largest
14
+ // distance any point in them could have, and a square is split into four only while it could
15
+ // still beat the best point found by more than the precision.
16
+ //
17
+ // Tile geometry is untrusted input, so the search is bounded by the work it does. Scoring one
18
+ // candidate costs one point-to-segment distance test per polygon vertex. The search counts those
19
+ // tests against a budget, and returns the best point found when the budget runs out. A caller
20
+ // can share one budget across all polygons of a tile, so a tile with many large polygons cannot
21
+ // hold the main thread either. The first grid also has at most MAX_GRID_CELLS_PER_AXIS squares
22
+ // along the longer side, and the precision has a floor relative to the polygon's size.
23
+
24
+ const MAX_GRID_CELLS_PER_AXIS = 64;
25
+ const MIN_RELATIVE_PRECISION = 1e-6;
26
+ /** Default budget for one search: point-to-segment distance tests. */
27
+ export const DEFAULT_POLE_SEARCH_SEGMENT_TESTS = 5e6;
28
+
29
+ /** Remaining work, in point-to-segment distance tests, shared by the searches that use it. */
30
+ export type PoleSearchBudget = {
31
+ segmentTests: number;
32
+ };
33
+
34
+ /**
35
+ * Returns the point inside a polygon farthest from its outline, to within `precision` in the
36
+ * polygon's units. `polygon` is GeoJSON-style: an outer ring followed by hole rings. Coordinates
37
+ * must be planar.
38
+ *
39
+ * The search spends at most `budget.segmentTests` point-to-segment distance tests and deducts
40
+ * what it spends, so one budget can bound several searches. When the budget runs out the best
41
+ * point found so far is returned. Returns null for a polygon without vertices, with a non-finite
42
+ * coordinate, or when the budget cannot pay for the first scored point.
43
+ */
44
+ export function getPoleOfInaccessibility(
45
+ polygon: number[][][],
46
+ precision: number,
47
+ budget: PoleSearchBudget = {segmentTests: DEFAULT_POLE_SEARCH_SEGMENT_TESTS}
48
+ ): [number, number] | null {
49
+ const outline = polygon[0];
50
+ if (!outline || outline.length === 0) {
51
+ return null;
52
+ }
53
+ const vertexCount = countFiniteVertices(polygon);
54
+ if (vertexCount < 0) {
55
+ return null;
56
+ }
57
+
58
+ const box = getRingBox(outline);
59
+ const width = box.right - box.left;
60
+ const height = box.top - box.bottom;
61
+ if (width === 0 || height === 0) {
62
+ return [box.left, box.bottom];
63
+ }
64
+ const longSide = Math.max(width, height);
65
+ const tolerance = Math.max(
66
+ Number.isFinite(precision) && precision > 0 ? precision : 0,
67
+ longSide * MIN_RELATIVE_PRECISION
68
+ );
69
+
70
+ const search = new PoleSearch(polygon, vertexCount, budget);
71
+
72
+ // Two cheap first guesses: the area centroid, which is inside most convex-ish shapes, and the
73
+ // middle of the bounding box.
74
+ const [centroidX, centroidY] = getAreaCentroid(polygon[0]);
75
+ if (!search.score(centroidX, centroidY, 0)) {
76
+ return null;
77
+ }
78
+ if (!search.score(box.left + width / 2, box.bottom + height / 2, 0)) {
79
+ return search.bestPoint();
80
+ }
81
+
82
+ // The first grid uses squares as wide as the polygon's short side, so a long, thin polygon is
83
+ // covered by squares that fit it. The cap keeps a sliver from starting with millions of them.
84
+ const side = Math.max(Math.min(width, height), longSide / MAX_GRID_CELLS_PER_AXIS);
85
+ const columns = Math.ceil(width / side);
86
+ const rows = Math.ceil(height / side);
87
+ for (let column = 0; column < columns; column++) {
88
+ for (let row = 0; row < rows; row++) {
89
+ const x = box.left + (column + 0.5) * side;
90
+ const y = box.bottom + (row + 0.5) * side;
91
+ if (!search.score(x, y, side / 2)) {
92
+ return search.bestPoint();
93
+ }
94
+ }
95
+ }
96
+
97
+ search.refine(tolerance);
98
+ return search.bestPoint();
99
+ }
100
+
101
+ /** Returns the number of vertices in all rings, or -1 if any coordinate is not finite. */
102
+ function countFiniteVertices(polygon: number[][][]): number {
103
+ let count = 0;
104
+ for (const ring of polygon) {
105
+ for (const point of ring) {
106
+ if (!Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
107
+ return -1;
108
+ }
109
+ }
110
+ count += ring.length;
111
+ }
112
+ return count;
113
+ }
114
+
115
+ function getRingBox(ring: number[][]): {left: number; bottom: number; right: number; top: number} {
116
+ let left = Infinity;
117
+ let bottom = Infinity;
118
+ let right = -Infinity;
119
+ let top = -Infinity;
120
+ for (const point of ring) {
121
+ left = Math.min(left, point[0]);
122
+ right = Math.max(right, point[0]);
123
+ bottom = Math.min(bottom, point[1]);
124
+ top = Math.max(top, point[1]);
125
+ }
126
+ return {left, bottom, right, top};
127
+ }
128
+
129
+ /** The centroid of the area a ring encloses, or its first vertex when that area is zero. */
130
+ function getAreaCentroid(ring: number[][]): [number, number] {
131
+ let twiceArea = 0;
132
+ let sumX = 0;
133
+ let sumY = 0;
134
+ let previous = ring[ring.length - 1];
135
+ for (const current of ring) {
136
+ const cross = previous[0] * current[1] - current[0] * previous[1];
137
+ twiceArea += cross;
138
+ sumX += (previous[0] + current[0]) * cross;
139
+ sumY += (previous[1] + current[1]) * cross;
140
+ previous = current;
141
+ }
142
+ if (twiceArea === 0) {
143
+ return [ring[0][0], ring[0][1]];
144
+ }
145
+ return [sumX / (3 * twiceArea), sumY / (3 * twiceArea)];
146
+ }
147
+
148
+ /**
149
+ * The state of one search: the best point so far and a max-heap of candidate squares. The
150
+ * squares are kept in parallel arrays, and the heap holds their indices, ordered by `reach`.
151
+ */
152
+ class PoleSearch {
153
+ private readonly polygon: number[][][];
154
+ private readonly vertexCount: number;
155
+ private readonly budget: PoleSearchBudget;
156
+
157
+ private readonly centerX: number[] = [];
158
+ private readonly centerY: number[] = [];
159
+ private readonly halfSide: number[] = [];
160
+ /** The largest distance to the outline that any point of the square can have. */
161
+ private readonly reach: number[] = [];
162
+ private readonly heap: number[] = [];
163
+
164
+ private bestX = NaN;
165
+ private bestY = NaN;
166
+ private bestDistance = -Infinity;
167
+
168
+ constructor(polygon: number[][][], vertexCount: number, budget: PoleSearchBudget) {
169
+ this.polygon = polygon;
170
+ this.vertexCount = vertexCount;
171
+ this.budget = budget;
172
+ }
173
+
174
+ bestPoint(): [number, number] {
175
+ return [this.bestX, this.bestY];
176
+ }
177
+
178
+ /**
179
+ * Scores the square centered at (x, y), keeps its center if it is the best so far, and queues
180
+ * the square if it has area. Returns false, without scoring, when the budget cannot pay.
181
+ */
182
+ score(x: number, y: number, half: number): boolean {
183
+ if (this.budget.segmentTests < this.vertexCount) {
184
+ return false;
185
+ }
186
+ this.budget.segmentTests -= this.vertexCount;
187
+ const distance = getSignedOutlineDistance(this.polygon, x, y);
188
+ if (distance > this.bestDistance) {
189
+ this.bestX = x;
190
+ this.bestY = y;
191
+ this.bestDistance = distance;
192
+ }
193
+ if (half > 0) {
194
+ // No point in the square is farther from the center than half its diagonal.
195
+ this.enqueue(x, y, half, distance + half * Math.SQRT2);
196
+ }
197
+ return true;
198
+ }
199
+
200
+ /** Splits the most promising squares until none can beat the best point by `tolerance`. */
201
+ refine(tolerance: number): void {
202
+ while (this.heap.length > 0) {
203
+ const index = this.dequeue();
204
+ if (this.reach[index] - this.bestDistance <= tolerance) {
205
+ // The heap is ordered by reach, so no remaining square can do better either.
206
+ return;
207
+ }
208
+ const quarter = this.halfSide[index] / 2;
209
+ const x = this.centerX[index];
210
+ const y = this.centerY[index];
211
+ if (
212
+ !this.score(x - quarter, y - quarter, quarter) ||
213
+ !this.score(x + quarter, y - quarter, quarter) ||
214
+ !this.score(x - quarter, y + quarter, quarter) ||
215
+ !this.score(x + quarter, y + quarter, quarter)
216
+ ) {
217
+ return;
218
+ }
219
+ }
220
+ }
221
+
222
+ private enqueue(x: number, y: number, half: number, reach: number): void {
223
+ const index = this.reach.length;
224
+ this.centerX.push(x);
225
+ this.centerY.push(y);
226
+ this.halfSide.push(half);
227
+ this.reach.push(reach);
228
+
229
+ const {heap} = this;
230
+ let slot = heap.length;
231
+ heap.push(index);
232
+ while (slot > 0) {
233
+ const parentSlot = (slot - 1) >> 1;
234
+ if (this.reach[heap[parentSlot]] >= reach) {
235
+ break;
236
+ }
237
+ heap[slot] = heap[parentSlot];
238
+ slot = parentSlot;
239
+ }
240
+ heap[slot] = index;
241
+ }
242
+
243
+ private dequeue(): number {
244
+ const {heap, reach} = this;
245
+ const top = heap[0];
246
+ const moved = heap.pop() as number;
247
+ const size = heap.length;
248
+ if (size === 0) {
249
+ return top;
250
+ }
251
+ let slot = 0;
252
+ for (;;) {
253
+ let next = slot * 2 + 1;
254
+ if (next >= size) {
255
+ break;
256
+ }
257
+ if (next + 1 < size && reach[heap[next + 1]] > reach[heap[next]]) {
258
+ next += 1;
259
+ }
260
+ if (reach[heap[next]] <= reach[moved]) {
261
+ break;
262
+ }
263
+ heap[slot] = heap[next];
264
+ slot = next;
265
+ }
266
+ heap[slot] = moved;
267
+ return top;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Distance from (x, y) to the nearest edge of any ring: positive inside the polygon, negative
273
+ * outside, and 0 on an edge. Inside is decided by the even-odd rule, so holes count as outside.
274
+ */
275
+ function getSignedOutlineDistance(polygon: number[][][], x: number, y: number): number {
276
+ let nearestSquared = Infinity;
277
+ let crossings = 0;
278
+ for (const ring of polygon) {
279
+ let start = ring[ring.length - 1];
280
+ for (const end of ring) {
281
+ // Count edges that cross the horizontal ray from (x, y) toward +x.
282
+ if (start[1] <= y !== end[1] <= y) {
283
+ const t = (y - start[1]) / (end[1] - start[1]);
284
+ if (start[0] + t * (end[0] - start[0]) > x) {
285
+ crossings++;
286
+ }
287
+ }
288
+ nearestSquared = Math.min(nearestSquared, getSquaredSegmentDistance(x, y, start, end));
289
+ start = end;
290
+ }
291
+ }
292
+ if (nearestSquared === 0) {
293
+ return 0;
294
+ }
295
+ const distance = Math.sqrt(nearestSquared);
296
+ return crossings % 2 === 1 ? distance : -distance;
297
+ }
298
+
299
+ /** Squared distance from (x, y) to the segment from `start` to `end`. */
300
+ function getSquaredSegmentDistance(x: number, y: number, start: number[], end: number[]): number {
301
+ const edgeX = end[0] - start[0];
302
+ const edgeY = end[1] - start[1];
303
+ const lengthSquared = edgeX * edgeX + edgeY * edgeY;
304
+ // Where the perpendicular from (x, y) meets the edge's line, as a fraction of the edge.
305
+ const along =
306
+ lengthSquared === 0
307
+ ? 0
308
+ : Math.max(0, Math.min(1, ((x - start[0]) * edgeX + (y - start[1]) * edgeY) / lengthSquared));
309
+ const offsetX = start[0] + along * edgeX - x;
310
+ const offsetY = start[1] + along * edgeY - y;
311
+ return offsetX * offsetX + offsetY * offsetY;
312
+ }
@@ -0,0 +1,245 @@
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/raster.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 {LayerExtension} from '@deck.gl/core';
75
+ import type {CompositeLayer, Layer} from '@deck.gl/core';
76
+
77
+ /** The style values of a raster layer's color adjustments, as evaluated from its paint. */
78
+ export type RasterColorAdjustments = {
79
+ /** `raster-brightness-min`, 0 to 1. */
80
+ brightnessMin: number;
81
+ /** `raster-brightness-max`, 0 to 1. */
82
+ brightnessMax: number;
83
+ /** `raster-saturation`, -1 to 1. */
84
+ saturation: number;
85
+ /** `raster-contrast`, -1 to 1. */
86
+ contrast: number;
87
+ /** `raster-hue-rotate`, in degrees. */
88
+ hueRotate: number;
89
+ };
90
+
91
+ /** The style specification's defaults, which leave colors unchanged. */
92
+ export const DEFAULT_RASTER_COLOR_ADJUSTMENTS: RasterColorAdjustments = {
93
+ brightnessMin: 0,
94
+ brightnessMax: 1,
95
+ saturation: 0,
96
+ contrast: 0,
97
+ hueRotate: 0
98
+ };
99
+
100
+ /** The shader uniforms of a set of color adjustments, computed as MapLibre GL JS does. */
101
+ export type RasterColorUniforms = {
102
+ brightnessLow: number;
103
+ brightnessHigh: number;
104
+ saturationFactor: number;
105
+ contrastFactor: number;
106
+ spinWeights: [number, number, number];
107
+ };
108
+
109
+ /** Weights that rotate an RGB color about the gray axis by `angle` degrees. */
110
+ function getSpinWeights(angle: number): [number, number, number] {
111
+ const radians = (angle * Math.PI) / 180;
112
+ const s = Math.sin(radians);
113
+ const c = Math.cos(radians);
114
+ return [(2 * c + 1) / 3, (-Math.sqrt(3) * s - c + 1) / 3, (Math.sqrt(3) * s - c + 1) / 3];
115
+ }
116
+
117
+ function getContrastFactor(contrast: number): number {
118
+ return contrast > 0 ? 1 / (1 - contrast) : 1 + contrast;
119
+ }
120
+
121
+ function getSaturationFactor(saturation: number): number {
122
+ return saturation > 0 ? 1 - 1 / (1.001 - saturation) : -saturation;
123
+ }
124
+
125
+ /** Converts color adjustments to shader uniforms, with MapLibre GL JS's `raster` program math. */
126
+ export function getRasterColorUniforms(adjustments: RasterColorAdjustments): RasterColorUniforms {
127
+ return {
128
+ brightnessLow: adjustments.brightnessMin,
129
+ brightnessHigh: adjustments.brightnessMax,
130
+ saturationFactor: getSaturationFactor(adjustments.saturation),
131
+ contrastFactor: getContrastFactor(adjustments.contrast),
132
+ spinWeights: getSpinWeights(adjustments.hueRotate)
133
+ };
134
+ }
135
+
136
+ /** Whether a set of color adjustments changes any color. */
137
+ export function hasRasterColorAdjustments(adjustments: RasterColorAdjustments): boolean {
138
+ return (Object.keys(DEFAULT_RASTER_COLOR_ADJUSTMENTS) as (keyof RasterColorAdjustments)[]).some(
139
+ key => adjustments[key] !== DEFAULT_RASTER_COLOR_ADJUSTMENTS[key]
140
+ );
141
+ }
142
+
143
+ const rasterColorModule = {
144
+ name: 'rasterColor',
145
+ fs: /* glsl */ `\
146
+ layout(std140) uniform rasterColorUniforms {
147
+ float brightnessLow;
148
+ float brightnessHigh;
149
+ float saturationFactor;
150
+ float contrastFactor;
151
+ vec3 spinWeights;
152
+ } rasterColor;
153
+
154
+ // MapLibre GL JS's raster fragment shader, applied to an unpremultiplied color.
155
+ vec3 rasterColor_adjust(vec3 color) {
156
+ vec3 rgb = vec3(
157
+ dot(color, rasterColor.spinWeights.xyz),
158
+ dot(color, rasterColor.spinWeights.zxy),
159
+ dot(color, rasterColor.spinWeights.yzx)
160
+ );
161
+ float average = (color.r + color.g + color.b) / 3.0;
162
+ rgb += (average - rgb) * rasterColor.saturationFactor;
163
+ rgb = (rgb - 0.5) * rasterColor.contrastFactor + 0.5;
164
+ return mix(vec3(rasterColor.brightnessLow), vec3(rasterColor.brightnessHigh), rgb);
165
+ }
166
+ `,
167
+ uniformTypes: {
168
+ brightnessLow: 'f32',
169
+ brightnessHigh: 'f32',
170
+ saturationFactor: 'f32',
171
+ contrastFactor: 'f32',
172
+ spinWeights: 'vec3<f32>'
173
+ }
174
+ } as const;
175
+
176
+ const injection = {
177
+ // MapLibre writes the adjusted color premultiplied, so a value pushed above 1 is clamped only
178
+ // after the multiplication by alpha. Layers using this extension blend premultiplied colors
179
+ // (`RASTER_COLOR_BLEND_PARAMETERS`) to draw the same.
180
+ 'fs:DECKGL_FILTER_COLOR': /* glsl */ `
181
+ color.rgb = rasterColor_adjust(color.rgb) * color.a;
182
+ `
183
+ };
184
+
185
+ /** The blending of layers using `RasterColorExtension`, whose shader outputs premultiplied colors. */
186
+ export const RASTER_COLOR_BLEND_PARAMETERS = {
187
+ blendColorOperation: 'add',
188
+ blendColorSrcFactor: 'one',
189
+ blendColorDstFactor: 'one-minus-src-alpha',
190
+ // WebGL sets both factors together; these are deck.gl's alpha factors.
191
+ blendAlphaOperation: 'add',
192
+ blendAlphaSrcFactor: 'one',
193
+ blendAlphaDstFactor: 'one-minus-src-alpha'
194
+ } as const;
195
+
196
+ type RasterColorExtensionProps = {
197
+ /** The color adjustments of the raster style layer. */
198
+ rasterColorAdjustments?: RasterColorAdjustments;
199
+ };
200
+
201
+ /** Whether the extension's shader runs on a layer's device. It is written in GLSL only. */
202
+ function isRasterColorSupported(layer: Layer): boolean {
203
+ return layer.context.device.type !== 'webgpu';
204
+ }
205
+
206
+ /**
207
+ * Applies a raster style layer's `raster-brightness-min`, `raster-brightness-max`,
208
+ * `raster-saturation`, `raster-contrast` and `raster-hue-rotate` to a `BitmapLayer`, with the
209
+ * formulas of MapLibre GL JS's raster shader. WebGL only.
210
+ *
211
+ * The adjusted colors are premultiplied, so the layer must blend with
212
+ * `RASTER_COLOR_BLEND_PARAMETERS` exactly where the shader runs. Added to a composite layer, the
213
+ * extension passes them to its sublayers as `rasterColorBlendParameters`, decided by the same
214
+ * device check as the shader; on WebGPU they are `null` and the sublayers keep their blending.
215
+ */
216
+ export class RasterColorExtension extends LayerExtension {
217
+ static extensionName = 'RasterColorExtension';
218
+ static defaultProps = {
219
+ rasterColorAdjustments: {type: 'object', value: DEFAULT_RASTER_COLOR_ADJUSTMENTS, compare: 1}
220
+ };
221
+
222
+ getShaders(this: Layer<RasterColorExtensionProps>) {
223
+ if (!isRasterColorSupported(this)) {
224
+ return {};
225
+ }
226
+ return {modules: [rasterColorModule], inject: injection};
227
+ }
228
+
229
+ getSubLayerProps(this: CompositeLayer<RasterColorExtensionProps>, extension: this) {
230
+ return {
231
+ ...LayerExtension.prototype.getSubLayerProps.call(this, extension),
232
+ rasterColorBlendParameters: isRasterColorSupported(this)
233
+ ? RASTER_COLOR_BLEND_PARAMETERS
234
+ : null
235
+ };
236
+ }
237
+
238
+ draw(this: Layer<RasterColorExtensionProps>) {
239
+ if (!isRasterColorSupported(this)) {
240
+ return;
241
+ }
242
+ const adjustments = this.props.rasterColorAdjustments ?? DEFAULT_RASTER_COLOR_ADJUSTMENTS;
243
+ this.setShaderModuleProps({rasterColor: getRasterColorUniforms(adjustments)});
244
+ }
245
+ }