@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
package/src/polylabel.ts
ADDED
|
@@ -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
|
+
}
|