@tangram.gl/basemap-layers 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/THIRD-PARTY-NOTICES.md +205 -0
- package/dist/geojson-worker.js +2160 -0
- package/dist/geojson-worker.js.map +6 -0
- package/dist/index.cjs +11388 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.js +11369 -0
- package/dist/index.js.map +6 -0
- package/dist/map-style.cjs +445 -0
- package/dist/map-style.cjs.map +6 -0
- package/dist/map-style.js +431 -0
- package/dist/map-style.js.map +6 -0
- package/dist/types/arabic-shaping.d.ts +7 -0
- package/dist/types/atmosphere-layer.d.ts +10 -0
- package/dist/types/basemap-layer.d.ts +150 -0
- package/dist/types/bidi.d.ts +8 -0
- package/dist/types/circle-layer.d.ts +49 -0
- package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
- package/dist/types/fill-outline-extension.d.ts +22 -0
- package/dist/types/fill-pattern.d.ts +99 -0
- package/dist/types/fill-style-extensions.d.ts +100 -0
- package/dist/types/geojson-source.d.ts +29 -0
- package/dist/types/geojson-tiler.d.ts +48 -0
- package/dist/types/geojson-worker.d.ts +40 -0
- package/dist/types/globe-horizon-extension.d.ts +61 -0
- package/dist/types/globe-layers.d.ts +239 -0
- package/dist/types/globe-line-angle-extension.d.ts +37 -0
- package/dist/types/glyph-font-renderer.d.ts +37 -0
- package/dist/types/glyph-pbf.d.ts +37 -0
- package/dist/types/glyph-store.d.ts +103 -0
- package/dist/types/grid-index.d.ts +47 -0
- package/dist/types/hillshade-layer.d.ts +154 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/is-globe-viewport.d.ts +2 -0
- package/dist/types/label-font-renderer.d.ts +18 -0
- package/dist/types/label-placement.d.ts +247 -0
- package/dist/types/label-text-layer.d.ts +231 -0
- package/dist/types/line-blur-extension.d.ts +73 -0
- package/dist/types/line-glyphs.d.ts +60 -0
- package/dist/types/line-placement.d.ts +93 -0
- package/dist/types/map-style-loader.d.ts +48 -0
- package/dist/types/map-style-schema.d.ts +72 -0
- package/dist/types/map-style.d.ts +6 -0
- package/dist/types/mapbox-style.d.ts +40 -0
- package/dist/types/mvt-label-layer.d.ts +923 -0
- package/dist/types/polylabel.d.ts +17 -0
- package/dist/types/raster-color-extension.d.ts +146 -0
- package/dist/types/shared-texture.d.ts +34 -0
- package/dist/types/sort-key.d.ts +10 -0
- package/dist/types/sprite.d.ts +82 -0
- package/dist/types/style-accessor.d.ts +45 -0
- package/dist/types/style-expression.d.ts +83 -0
- package/dist/types/style-resolver.d.ts +94 -0
- package/dist/types/text-font.d.ts +36 -0
- package/dist/types/text-halo-blur-extension.d.ts +55 -0
- package/dist/types/text-layout.d.ts +71 -0
- package/dist/types/util.d.ts +22 -0
- package/dist/types/zoom-paint-extension.d.ts +72 -0
- package/package.json +80 -3
- package/src/arabic-shaping.ts +366 -0
- package/src/atmosphere-layer.ts +19 -0
- package/src/basemap-layer.ts +533 -0
- package/src/bidi.ts +199 -0
- package/src/circle-layer.ts +191 -0
- package/src/fill-extrusion-lighting-extension.ts +206 -0
- package/src/fill-outline-extension.ts +70 -0
- package/src/fill-pattern.ts +400 -0
- package/src/fill-style-extensions.ts +177 -0
- package/src/geojson-source.ts +318 -0
- package/src/geojson-tiler.ts +185 -0
- package/src/geojson-worker.ts +98 -0
- package/src/globe-horizon-extension.ts +64 -0
- package/src/globe-layers.ts +3487 -0
- package/src/globe-line-angle-extension.ts +131 -0
- package/src/glyph-font-renderer.ts +173 -0
- package/src/glyph-pbf.ts +164 -0
- package/src/glyph-store.ts +326 -0
- package/src/grid-index.ts +177 -0
- package/src/hillshade-layer.ts +775 -0
- package/src/index.ts +18 -0
- package/src/is-globe-viewport.ts +24 -0
- package/src/label-font-renderer.ts +159 -0
- package/src/label-placement.ts +727 -0
- package/src/label-text-layer.ts +913 -0
- package/src/line-blur-extension.ts +237 -0
- package/src/line-glyphs.ts +364 -0
- package/src/line-placement.ts +446 -0
- package/src/map-style-loader.ts +56 -0
- package/src/map-style-schema.ts +83 -0
- package/src/map-style.ts +23 -0
- package/src/mapbox-style.ts +168 -0
- package/src/mvt-label-layer.ts +3744 -0
- package/src/polylabel.ts +312 -0
- package/src/raster-color-extension.ts +245 -0
- package/src/shared-texture.ts +153 -0
- package/src/sort-key.ts +63 -0
- package/src/sprite.ts +244 -0
- package/src/style-accessor.ts +95 -0
- package/src/style-expression.ts +327 -0
- package/src/style-resolver.ts +301 -0
- package/src/text-font.ts +150 -0
- package/src/text-halo-blur-extension.ts +115 -0
- package/src/text-layout.ts +441 -0
- package/src/util.ts +47 -0
- package/src/zoom-paint-extension.ts +207 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {GeoJSONVT, GeoJSONVTOptions} from '@maplibre/geojson-vt';
|
|
6
|
+
import {createTileIndex, cutTile, decodeTile, GEOJSON_TILE_EXTENT} from './geojson-tiler';
|
|
7
|
+
import type {GeoJsonWorkerRequest, GeoJsonWorkerResponse} from './geojson-worker';
|
|
8
|
+
import type {BasemapSource} from './style-resolver';
|
|
9
|
+
|
|
10
|
+
/** The size of a GeoJSON source's tiles in pixels, fixed in MapLibre GL JS. */
|
|
11
|
+
const GEOJSON_TILE_SIZE = 512;
|
|
12
|
+
|
|
13
|
+
/** The default `maxzoom` of a GeoJSON source, as in the style specification. */
|
|
14
|
+
const GEOJSON_DEFAULT_MAXZOOM = 18;
|
|
15
|
+
|
|
16
|
+
/** The highest zoom geojson-vt tiles at. */
|
|
17
|
+
const GEOJSON_VT_MAX_ZOOM = 24;
|
|
18
|
+
|
|
19
|
+
/** Tile units per pixel of a GeoJSON tile. */
|
|
20
|
+
const TILE_UNITS_PER_PIXEL = GEOJSON_TILE_EXTENT / GEOJSON_TILE_SIZE;
|
|
21
|
+
|
|
22
|
+
/** Whether a source is a GeoJSON source whose data has been resolved to a GeoJSON object. */
|
|
23
|
+
export function isGeoJsonSource(source: BasemapSource | undefined): boolean {
|
|
24
|
+
return (
|
|
25
|
+
source?.type === 'geojson' &&
|
|
26
|
+
Boolean(source.data) &&
|
|
27
|
+
typeof source.data === 'object' &&
|
|
28
|
+
!Array.isArray(source.data)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The deepest zoom a GeoJSON source is tiled at; deeper views overzoom its tiles. */
|
|
33
|
+
export function getGeoJsonMaxZoom(source: BasemapSource): number {
|
|
34
|
+
const maxzoom = getNumber(source.maxzoom, GEOJSON_DEFAULT_MAXZOOM);
|
|
35
|
+
return Math.max(0, Math.min(Math.round(maxzoom), GEOJSON_VT_MAX_ZOOM));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A finite number, or `fallback`. */
|
|
39
|
+
function getNumber(value: unknown, fallback: number): number {
|
|
40
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* geojson-vt options for a GeoJSON source, as MapLibre GL JS 6 sets them: `buffer` and
|
|
45
|
+
* `tolerance` are given in pixels of a 512 pixel tile, and the source's `maxzoom` is the deepest
|
|
46
|
+
* zoom tiled. Clustering is supported without `clusterProperties`.
|
|
47
|
+
*/
|
|
48
|
+
export function getGeoJsonVtOptions(source: BasemapSource): GeoJSONVTOptions {
|
|
49
|
+
const maxZoom = getGeoJsonMaxZoom(source);
|
|
50
|
+
const generateId = Boolean(source.generateId);
|
|
51
|
+
const promoteId = typeof source.promoteId === 'string' ? source.promoteId : undefined;
|
|
52
|
+
const options: GeoJSONVTOptions = {
|
|
53
|
+
maxZoom,
|
|
54
|
+
extent: GEOJSON_TILE_EXTENT,
|
|
55
|
+
buffer: getNumber(source.buffer, 128) * TILE_UNITS_PER_PIXEL,
|
|
56
|
+
tolerance: getNumber(source.tolerance, 0.375) * TILE_UNITS_PER_PIXEL,
|
|
57
|
+
generateId,
|
|
58
|
+
// geojson-vt rejects `promoteId` together with `generateId`, which then wins, as in MapLibre.
|
|
59
|
+
promoteId: generateId ? undefined : promoteId
|
|
60
|
+
};
|
|
61
|
+
if (source.cluster) {
|
|
62
|
+
const clusterMaxZoom = getNumber(source.clusterMaxZoom, maxZoom - 1);
|
|
63
|
+
options.cluster = true;
|
|
64
|
+
options.clusterOptions = {
|
|
65
|
+
// The cluster index holds a level per zoom: bounded by the source's tiled zooms, which
|
|
66
|
+
// MapLibre GL JS only warns about, so that a style value cannot make it build millions.
|
|
67
|
+
maxZoom: Math.max(0, Math.min(Math.round(clusterMaxZoom), maxZoom)),
|
|
68
|
+
minPoints: Math.max(2, getNumber(source.clusterMinPoints, 2)),
|
|
69
|
+
extent: GEOJSON_TILE_EXTENT,
|
|
70
|
+
radius: getNumber(source.clusterRadius, 50) * TILE_UNITS_PER_PIXEL,
|
|
71
|
+
log: false,
|
|
72
|
+
generateId
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return options;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* How many coordinate positions of a FeatureCollection go to the worker in one message. Posting
|
|
80
|
+
* copies the features on the main thread, so a large source goes in batches, with the main thread
|
|
81
|
+
* free to draw between them.
|
|
82
|
+
*/
|
|
83
|
+
const POSITIONS_PER_BATCH = 100_000;
|
|
84
|
+
|
|
85
|
+
/** Whether GeoJSON sources are tiled in a worker where workers are available. For tests. */
|
|
86
|
+
let workerEnabled = true;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether tiles requested from now on are cut in a worker, where one can start (`true`, the
|
|
90
|
+
* default), or on the main thread. For tests.
|
|
91
|
+
*/
|
|
92
|
+
export function setGeoJsonWorkerEnabled(enabled: boolean): void {
|
|
93
|
+
workerEnabled = enabled;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A tile request sent to the worker, and how to cut the tile here if the worker fails. */
|
|
97
|
+
type PendingTile = {
|
|
98
|
+
source: BasemapSource;
|
|
99
|
+
index: {x: number; y: number; z: number};
|
|
100
|
+
resolve: (features: Record<string, unknown>[]) => void;
|
|
101
|
+
reject: (error: unknown) => void;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The worker that indexes GeoJSON sources and cuts their tiles, shared by all of them. If it
|
|
106
|
+
* cannot start (a bundle that does not emit it, a content security policy without `worker-src`)
|
|
107
|
+
* or fails, its requests, and every later one, are cut on the main thread instead.
|
|
108
|
+
*/
|
|
109
|
+
class GeoJsonWorker {
|
|
110
|
+
failed = false;
|
|
111
|
+
private worker: Worker;
|
|
112
|
+
private nextId = 0;
|
|
113
|
+
private sources = new WeakMap<BasemapSource, {id: number; sent: Promise<void>}>();
|
|
114
|
+
private requests = new Map<number, PendingTile>();
|
|
115
|
+
private registry = new FinalizationRegistry<number>(id =>
|
|
116
|
+
this.post({type: 'remove', source: id})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
constructor(worker: Worker) {
|
|
120
|
+
this.worker = worker;
|
|
121
|
+
worker.onmessage = ({data}: MessageEvent<GeoJsonWorkerResponse>) => {
|
|
122
|
+
const request = this.requests.get(data.request);
|
|
123
|
+
this.requests.delete(data.request);
|
|
124
|
+
if (!request) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if ('error' in data) {
|
|
128
|
+
request.reject(new Error(data.error));
|
|
129
|
+
} else {
|
|
130
|
+
request.resolve(decodeTile(data.tile));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
worker.onerror = event => {
|
|
134
|
+
event.preventDefault?.();
|
|
135
|
+
this.fail();
|
|
136
|
+
};
|
|
137
|
+
worker.onmessageerror = () => this.fail();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Cuts a tile of `source` in the worker, sending the source's data first if it is new. */
|
|
141
|
+
async getTileFeatures(
|
|
142
|
+
source: BasemapSource,
|
|
143
|
+
index: {x: number; y: number; z: number}
|
|
144
|
+
): Promise<Record<string, unknown>[]> {
|
|
145
|
+
let entry = this.sources.get(source);
|
|
146
|
+
if (!entry) {
|
|
147
|
+
const id = this.nextId++;
|
|
148
|
+
entry = {id, sent: this.sendSource(id, source)};
|
|
149
|
+
this.sources.set(source, entry);
|
|
150
|
+
this.registry.register(source, id);
|
|
151
|
+
}
|
|
152
|
+
await entry.sent;
|
|
153
|
+
if (this.failed) {
|
|
154
|
+
return getMainThreadTileFeatures(source, index);
|
|
155
|
+
}
|
|
156
|
+
const request = this.nextId++;
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
this.requests.set(request, {source, index, resolve, reject});
|
|
159
|
+
this.post({type: 'tile', source: entry.id, request, ...index});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sends a source's data and options. A FeatureCollection's features go in batches, yielding to
|
|
165
|
+
* the main thread between them; the worker builds the index when the last one arrives.
|
|
166
|
+
*/
|
|
167
|
+
private async sendSource(id: number, source: BasemapSource): Promise<void> {
|
|
168
|
+
const options = getGeoJsonVtOptions(source);
|
|
169
|
+
const data = source.data as {type?: string; features?: unknown};
|
|
170
|
+
if (data.type !== 'FeatureCollection' || !Array.isArray(data.features)) {
|
|
171
|
+
this.post({type: 'index', source: id, data, options});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
let batch: unknown[] = [];
|
|
175
|
+
let positions = 0;
|
|
176
|
+
for (const feature of data.features) {
|
|
177
|
+
batch.push(feature);
|
|
178
|
+
positions += countPositions((feature as {geometry?: unknown})?.geometry);
|
|
179
|
+
if (positions >= POSITIONS_PER_BATCH) {
|
|
180
|
+
this.post({type: 'features', source: id, features: batch});
|
|
181
|
+
batch = [];
|
|
182
|
+
positions = 0;
|
|
183
|
+
await yieldToMainThread();
|
|
184
|
+
if (this.failed) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
this.post({type: 'features', source: id, features: batch});
|
|
190
|
+
this.post({type: 'index', source: id, options});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private post(message: GeoJsonWorkerRequest): void {
|
|
194
|
+
if (this.failed) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
this.worker.postMessage(message);
|
|
199
|
+
} catch {
|
|
200
|
+
// Data that cannot be cloned (a function in a property, say) is tiled on the main thread.
|
|
201
|
+
this.fail();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Stops using the worker, and cuts the tiles it still owes on the main thread. */
|
|
206
|
+
private fail(): void {
|
|
207
|
+
if (this.failed) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.failed = true;
|
|
211
|
+
this.worker.terminate();
|
|
212
|
+
const requests = [...this.requests.values()];
|
|
213
|
+
this.requests.clear();
|
|
214
|
+
for (const {source, index, resolve, reject} of requests) {
|
|
215
|
+
try {
|
|
216
|
+
resolve(getMainThreadTileFeatures(source, index));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
reject(error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The number of positions in a GeoJSON geometry's coordinates, to size the batches sent. */
|
|
225
|
+
function countPositions(geometry: unknown): number {
|
|
226
|
+
const {type, coordinates, geometries} = (geometry || {}) as {
|
|
227
|
+
type?: string;
|
|
228
|
+
coordinates?: unknown;
|
|
229
|
+
geometries?: unknown[];
|
|
230
|
+
};
|
|
231
|
+
if (type === 'GeometryCollection') {
|
|
232
|
+
return (geometries || []).reduce<number>((sum, child) => sum + countPositions(child), 0);
|
|
233
|
+
}
|
|
234
|
+
const count = (value: unknown): number => {
|
|
235
|
+
if (!Array.isArray(value)) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
if (typeof value[0] === 'number') {
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
let sum = 0;
|
|
242
|
+
for (const child of value) {
|
|
243
|
+
sum += count(child);
|
|
244
|
+
}
|
|
245
|
+
return sum;
|
|
246
|
+
};
|
|
247
|
+
return count(coordinates);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Resolves after the browser has had a turn to run other tasks, without timer throttling. */
|
|
251
|
+
function yieldToMainThread(): Promise<void> {
|
|
252
|
+
return new Promise(resolve => {
|
|
253
|
+
const channel = new MessageChannel();
|
|
254
|
+
channel.port1.onmessage = () => {
|
|
255
|
+
channel.port1.close();
|
|
256
|
+
resolve();
|
|
257
|
+
};
|
|
258
|
+
channel.port2.postMessage(null);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The shared worker, once started; `null` where none can start. */
|
|
263
|
+
let geoJsonWorker: GeoJsonWorker | null | undefined;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The shared worker, or `null` where GeoJSON is tiled on the main thread: where the platform has
|
|
267
|
+
* no `Worker` (Node), where the worker could not be created or has failed, or when tests turned
|
|
268
|
+
* workers off.
|
|
269
|
+
*/
|
|
270
|
+
function getGeoJsonWorker(): GeoJsonWorker | null {
|
|
271
|
+
if (!workerEnabled || typeof Worker === 'undefined') {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (geoJsonWorker === undefined) {
|
|
275
|
+
try {
|
|
276
|
+
// Bundlers (Vite, webpack 5, esbuild with a plugin, Parcel) emit the worker and its imports
|
|
277
|
+
// from this expression, which must stay literal. The CommonJS build has no `import.meta.url`
|
|
278
|
+
// and throws here.
|
|
279
|
+
geoJsonWorker = new GeoJsonWorker(
|
|
280
|
+
new Worker(new URL('./geojson-worker.js', import.meta.url), {type: 'module'})
|
|
281
|
+
);
|
|
282
|
+
} catch {
|
|
283
|
+
geoJsonWorker = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return geoJsonWorker?.failed ? null : geoJsonWorker;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Each source's tile index on the main thread, built when its first tile is requested there. */
|
|
290
|
+
const tileIndexes = new WeakMap<BasemapSource, GeoJSONVT>();
|
|
291
|
+
|
|
292
|
+
/** Cuts a tile of a GeoJSON source on the main thread, building its index first if needed. */
|
|
293
|
+
function getMainThreadTileFeatures(
|
|
294
|
+
source: BasemapSource,
|
|
295
|
+
index: {x: number; y: number; z: number}
|
|
296
|
+
): Record<string, unknown>[] {
|
|
297
|
+
let tileIndex = tileIndexes.get(source);
|
|
298
|
+
if (!tileIndex) {
|
|
299
|
+
tileIndex = createTileIndex(source.data, getGeoJsonVtOptions(source));
|
|
300
|
+
tileIndexes.set(source, tileIndex);
|
|
301
|
+
}
|
|
302
|
+
return decodeTile(cutTile(tileIndex, index.z, index.x, index.y));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The features of one tile of a GeoJSON source, cut by geojson-vt as MapLibre GL JS cuts them, in
|
|
307
|
+
* the normalized tile-local coordinates of decoded vector tiles: the existing vector tile path
|
|
308
|
+
* then draws them as it draws vector tiles. The source is indexed, and its tiles cut, in a worker
|
|
309
|
+
* where one can start, and on the main thread otherwise. Rejects if the source's data is not
|
|
310
|
+
* valid GeoJSON.
|
|
311
|
+
*/
|
|
312
|
+
export async function getGeoJsonTileFeatures(
|
|
313
|
+
source: BasemapSource,
|
|
314
|
+
index: {x: number; y: number; z: number}
|
|
315
|
+
): Promise<Record<string, unknown>[]> {
|
|
316
|
+
const worker = getGeoJsonWorker();
|
|
317
|
+
return worker ? worker.getTileFeatures(source, index) : getMainThreadTileFeatures(source, index);
|
|
318
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Cutting GeoJSON into vector tiles with `@maplibre/geojson-vt`, and the transferable form a
|
|
11
|
+
* tile's features take between the worker that cuts them and the main thread that draws them.
|
|
12
|
+
* This module runs in both: it holds no state and imports nothing from the rest of the module.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {GeoJSONVT, type GeoJSONVTFeature, type GeoJSONVTOptions} from '@maplibre/geojson-vt';
|
|
16
|
+
|
|
17
|
+
/** The extent of MapLibre GL JS's tiles, in tile units: GeoJSON is tiled at this precision. */
|
|
18
|
+
export const GEOJSON_TILE_EXTENT = 8192;
|
|
19
|
+
|
|
20
|
+
/** The geometry types of a tile's features, by how deep their coordinates nest. */
|
|
21
|
+
const GEOMETRY_DEPTHS = {
|
|
22
|
+
Point: 0,
|
|
23
|
+
MultiPoint: 1,
|
|
24
|
+
LineString: 1,
|
|
25
|
+
MultiLineString: 2,
|
|
26
|
+
Polygon: 2,
|
|
27
|
+
MultiPolygon: 3
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
type GeometryType = keyof typeof GEOMETRY_DEPTHS;
|
|
31
|
+
|
|
32
|
+
/** One feature of an encoded tile: its coordinates are in the tile's typed arrays. */
|
|
33
|
+
type EncodedFeature = {
|
|
34
|
+
type: GeometryType;
|
|
35
|
+
properties: Record<string, unknown>;
|
|
36
|
+
id?: unknown;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A tile's features in a form whose bulk can be transferred between threads: each feature's
|
|
41
|
+
* coordinates, in tile units, follow one another in `positions` (x, y pairs), and `lengths` holds,
|
|
42
|
+
* depth first, the length of each array above a position (the polygons of a multipolygon, the
|
|
43
|
+
* rings of a polygon, the points of a ring). Tile units are integers: geojson-vt rounds them.
|
|
44
|
+
*/
|
|
45
|
+
export type EncodedTile = {
|
|
46
|
+
features: EncodedFeature[];
|
|
47
|
+
positions: Int32Array;
|
|
48
|
+
lengths: Uint32Array;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Builds the tile index of a GeoJSON source's data. Throws if the data is not valid GeoJSON. */
|
|
52
|
+
export function createTileIndex(data: unknown, options: GeoJSONVTOptions): GeoJSONVT {
|
|
53
|
+
return new GeoJSONVT(data as GeoJSON.GeoJSON, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A ring's signed area in tile units, as `@mapbox/vector-tile` computes it. */
|
|
57
|
+
function getSignedArea(ring: [number, number][]): number {
|
|
58
|
+
let sum = 0;
|
|
59
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
60
|
+
sum += (ring[j][0] - ring[i][0]) * (ring[i][1] + ring[j][1]);
|
|
61
|
+
}
|
|
62
|
+
return sum;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Groups a polygon feature's rings into polygons, as MapLibre GL JS does for vector tiles: a ring
|
|
67
|
+
* wound as the first one starts a polygon, and a ring wound the other way is a hole of the
|
|
68
|
+
* polygon before it. Rings without area are dropped.
|
|
69
|
+
*/
|
|
70
|
+
function classifyRings(rings: [number, number][][]): [number, number][][][] {
|
|
71
|
+
const polygons: [number, number][][][] = [];
|
|
72
|
+
let polygon: [number, number][][] | undefined;
|
|
73
|
+
let clockwise: boolean | undefined;
|
|
74
|
+
for (const ring of rings) {
|
|
75
|
+
const area = getSignedArea(ring);
|
|
76
|
+
if (area === 0) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
clockwise ??= area < 0;
|
|
80
|
+
if (clockwise === area < 0) {
|
|
81
|
+
polygon = [ring];
|
|
82
|
+
polygons.push(polygon);
|
|
83
|
+
} else {
|
|
84
|
+
polygon?.push(ring);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return polygons;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A geojson-vt tile feature's GeoJSON geometry, in tile units; `null` for a polygon without area. */
|
|
91
|
+
function getGeometry(feature: GeoJSONVTFeature): {type: GeometryType; coordinates: unknown} | null {
|
|
92
|
+
if (feature.type === 1) {
|
|
93
|
+
const points = feature.geometry;
|
|
94
|
+
return points.length === 1
|
|
95
|
+
? {type: 'Point', coordinates: points[0]}
|
|
96
|
+
: {type: 'MultiPoint', coordinates: points};
|
|
97
|
+
}
|
|
98
|
+
if (feature.type === 2) {
|
|
99
|
+
const lines = feature.geometry;
|
|
100
|
+
return lines.length === 1
|
|
101
|
+
? {type: 'LineString', coordinates: lines[0]}
|
|
102
|
+
: {type: 'MultiLineString', coordinates: lines};
|
|
103
|
+
}
|
|
104
|
+
const polygons = classifyRings(feature.geometry);
|
|
105
|
+
if (polygons.length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return polygons.length === 1
|
|
109
|
+
? {type: 'Polygon', coordinates: polygons[0]}
|
|
110
|
+
: {type: 'MultiPolygon', coordinates: polygons};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Cuts one tile from a tile index, as MapLibre GL JS cuts it, and encodes its features. Its typed
|
|
115
|
+
* arrays' buffers can be transferred.
|
|
116
|
+
*/
|
|
117
|
+
export function cutTile(index: GeoJSONVT, z: number, x: number, y: number): EncodedTile {
|
|
118
|
+
const features: EncodedFeature[] = [];
|
|
119
|
+
const positions: number[] = [];
|
|
120
|
+
const lengths: number[] = [];
|
|
121
|
+
|
|
122
|
+
const writeCoordinates = (coordinates: any, depth: number) => {
|
|
123
|
+
if (depth === 0) {
|
|
124
|
+
positions.push(coordinates[0], coordinates[1]);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
lengths.push(coordinates.length);
|
|
128
|
+
for (const child of coordinates) {
|
|
129
|
+
writeCoordinates(child, depth - 1);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const feature of index.getTile(z, x, y)?.features || []) {
|
|
134
|
+
const geometry = getGeometry(feature);
|
|
135
|
+
if (!geometry) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
writeCoordinates(geometry.coordinates, GEOMETRY_DEPTHS[geometry.type]);
|
|
139
|
+
const encoded: EncodedFeature = {type: geometry.type, properties: feature.tags || {}};
|
|
140
|
+
if (feature.id !== undefined) {
|
|
141
|
+
encoded.id = feature.id;
|
|
142
|
+
}
|
|
143
|
+
features.push(encoded);
|
|
144
|
+
}
|
|
145
|
+
return {features, positions: Int32Array.from(positions), lengths: Uint32Array.from(lengths)};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The GeoJSON features of an encoded tile, in the normalized tile-local coordinates of decoded
|
|
150
|
+
* vector tiles: the vector tile path then draws them as it draws vector tiles.
|
|
151
|
+
*/
|
|
152
|
+
export function decodeTile(tile: EncodedTile): Record<string, unknown>[] {
|
|
153
|
+
const {positions, lengths} = tile;
|
|
154
|
+
let position = 0;
|
|
155
|
+
let length = 0;
|
|
156
|
+
|
|
157
|
+
const readCoordinates = (depth: number): unknown => {
|
|
158
|
+
if (depth === 0) {
|
|
159
|
+
const point = [
|
|
160
|
+
positions[position] / GEOJSON_TILE_EXTENT,
|
|
161
|
+
positions[position + 1] / GEOJSON_TILE_EXTENT
|
|
162
|
+
];
|
|
163
|
+
position += 2;
|
|
164
|
+
return point;
|
|
165
|
+
}
|
|
166
|
+
const count = lengths[length++];
|
|
167
|
+
const children = new Array(count);
|
|
168
|
+
for (let i = 0; i < count; i++) {
|
|
169
|
+
children[i] = readCoordinates(depth - 1);
|
|
170
|
+
}
|
|
171
|
+
return children;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return tile.features.map(({type, properties, id}) => {
|
|
175
|
+
const feature: Record<string, unknown> = {
|
|
176
|
+
type: 'Feature',
|
|
177
|
+
geometry: {type, coordinates: readCoordinates(GEOMETRY_DEPTHS[type])},
|
|
178
|
+
properties
|
|
179
|
+
};
|
|
180
|
+
if (id !== undefined) {
|
|
181
|
+
feature.id = id;
|
|
182
|
+
}
|
|
183
|
+
return feature;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* The worker that indexes GeoJSON sources and cuts their tiles, as MapLibre GL JS's
|
|
11
|
+
* `GeoJSONWorkerSource` does, so that a large source does not hold the main thread. See
|
|
12
|
+
* `geojson-source.ts` for the other end of each message.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {GeoJSONVT, GeoJSONVTOptions} from '@maplibre/geojson-vt';
|
|
16
|
+
import {createTileIndex, cutTile} from './geojson-tiler';
|
|
17
|
+
|
|
18
|
+
/** A message from the main thread. Messages of one source arrive in the order they are sent. */
|
|
19
|
+
export type GeoJsonWorkerRequest =
|
|
20
|
+
/** Features of a FeatureCollection, sent in batches before its `index` message. */
|
|
21
|
+
| {type: 'features'; source: number; features: unknown[]}
|
|
22
|
+
/** Builds a source's index from `data`, or from the features sent before it. */
|
|
23
|
+
| {type: 'index'; source: number; data?: unknown; options: GeoJSONVTOptions}
|
|
24
|
+
| {type: 'tile'; source: number; request: number; z: number; x: number; y: number}
|
|
25
|
+
| {type: 'remove'; source: number};
|
|
26
|
+
|
|
27
|
+
/** The answer to a `tile` request: the encoded tile, or why the source cannot be tiled. */
|
|
28
|
+
export type GeoJsonWorkerResponse = {request: number} & (
|
|
29
|
+
| {tile: ReturnType<typeof cutTile>}
|
|
30
|
+
| {error: string}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** The features of each source whose index is not built yet. */
|
|
34
|
+
const pendingFeatures = new Map<number, unknown[]>();
|
|
35
|
+
|
|
36
|
+
/** Each source's index, or the error building it threw. */
|
|
37
|
+
const indexes = new Map<number, GeoJSONVT | Error>();
|
|
38
|
+
|
|
39
|
+
const scope = globalThis as unknown as {
|
|
40
|
+
onmessage: ((event: MessageEvent<GeoJsonWorkerRequest>) => void) | null;
|
|
41
|
+
postMessage: (message: GeoJsonWorkerResponse, transfer: Transferable[]) => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
scope.onmessage = ({data: message}) => {
|
|
45
|
+
switch (message.type) {
|
|
46
|
+
case 'features': {
|
|
47
|
+
const features = pendingFeatures.get(message.source);
|
|
48
|
+
if (features) {
|
|
49
|
+
for (const feature of message.features) {
|
|
50
|
+
features.push(feature);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
pendingFeatures.set(message.source, message.features);
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case 'index': {
|
|
58
|
+
const data = message.data ?? {
|
|
59
|
+
type: 'FeatureCollection',
|
|
60
|
+
features: pendingFeatures.get(message.source) || []
|
|
61
|
+
};
|
|
62
|
+
pendingFeatures.delete(message.source);
|
|
63
|
+
try {
|
|
64
|
+
indexes.set(message.source, createTileIndex(data, message.options));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
indexes.set(message.source, error instanceof Error ? error : new Error(String(error)));
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case 'tile': {
|
|
71
|
+
const index = indexes.get(message.source);
|
|
72
|
+
try {
|
|
73
|
+
if (!index) {
|
|
74
|
+
throw new Error('GeoJSON source has no index');
|
|
75
|
+
}
|
|
76
|
+
if (index instanceof Error) {
|
|
77
|
+
throw index;
|
|
78
|
+
}
|
|
79
|
+
const tile = cutTile(index, message.z, message.x, message.y);
|
|
80
|
+
scope.postMessage({request: message.request, tile}, [
|
|
81
|
+
tile.positions.buffer,
|
|
82
|
+
tile.lengths.buffer
|
|
83
|
+
]);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
scope.postMessage(
|
|
86
|
+
{request: message.request, error: error instanceof Error ? error.message : String(error)},
|
|
87
|
+
[]
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case 'remove':
|
|
93
|
+
pendingFeatures.delete(message.source);
|
|
94
|
+
indexes.delete(message.source);
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
}
|
|
98
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {LayerExtension, project} from '@deck.gl/core';
|
|
6
|
+
import type {Layer} from '@deck.gl/core';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hides every vertex of an instance whose anchor (`geometry.worldPosition`) lies beyond the
|
|
10
|
+
* globe's horizon. Runs after the collision filter's `DECKGL_FILTER_GL_POSITION` injection
|
|
11
|
+
* (`order: 1`), so its sort depth cannot move a hidden quad back into clip space.
|
|
12
|
+
*
|
|
13
|
+
* In `GlobeView`'s common space the globe is centred at the origin with radius `GLOBE_RADIUS`. As
|
|
14
|
+
* in MapLibre GL JS, the test uses the point of the surface under the anchor, with unit normal
|
|
15
|
+
* `n`:
|
|
16
|
+
* - with a perspective camera at `C`, the point is visible when `dot(n, C) >= GLOBE_RADIUS`, the
|
|
17
|
+
* camera's side of the plane through the points where lines from the camera touch the globe;
|
|
18
|
+
* - with an orthographic camera, whose rays are parallel, it is visible when the normal does not
|
|
19
|
+
* point away from the viewer. The fourth row of an orthographic view-projection matrix is
|
|
20
|
+
* `(0, 0, 0, 1)`, and its third row is the direction in which depth grows.
|
|
21
|
+
*/
|
|
22
|
+
const globeHorizonModule = {
|
|
23
|
+
name: 'globeHorizon',
|
|
24
|
+
dependencies: [project],
|
|
25
|
+
inject: {
|
|
26
|
+
'vs:DECKGL_FILTER_GL_POSITION': {
|
|
27
|
+
order: 1,
|
|
28
|
+
injection: /* glsl */ `
|
|
29
|
+
if (project.projectionMode == PROJECTION_MODE_GLOBE) {
|
|
30
|
+
vec3 globeHorizon_normal = normalize(project_position(geometry.worldPosition));
|
|
31
|
+
mat4 globeHorizon_m = project.viewProjectionMatrix;
|
|
32
|
+
vec3 globeHorizon_wRow = vec3(globeHorizon_m[0][3], globeHorizon_m[1][3], globeHorizon_m[2][3]);
|
|
33
|
+
bool globeHorizon_hidden = dot(globeHorizon_wRow, globeHorizon_wRow) == 0.0
|
|
34
|
+
? dot(globeHorizon_normal, vec3(globeHorizon_m[0][2], globeHorizon_m[1][2], globeHorizon_m[2][2])) > 0.0
|
|
35
|
+
: dot(globeHorizon_normal, project.cameraPosition) < GLOBE_RADIUS;
|
|
36
|
+
if (globeHorizon_hidden) {
|
|
37
|
+
// Every vertex of the quad lands beyond the far plane, so it is clipped. In the collision
|
|
38
|
+
// pass this keeps a hidden label out of the collision map.
|
|
39
|
+
position = vec4(0.0, 0.0, 2.0, 1.0);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hides labels and icons whose anchor is on the far side of the globe in a `GlobeView`, in the
|
|
49
|
+
* screen pass and in the collision filter's pass. In other views the shader does nothing. WebGL
|
|
50
|
+
* only: on WebGPU the extension adds no shader code.
|
|
51
|
+
*
|
|
52
|
+
* Added to a composite layer such as `TextLayer`, it reaches the character and background
|
|
53
|
+
* sublayers.
|
|
54
|
+
*/
|
|
55
|
+
export class GlobeHorizonExtension extends LayerExtension {
|
|
56
|
+
static extensionName = 'GlobeHorizonExtension';
|
|
57
|
+
|
|
58
|
+
getShaders(this: Layer) {
|
|
59
|
+
if (this.context.device.type === 'webgpu') {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
return {modules: [globeHorizonModule]};
|
|
63
|
+
}
|
|
64
|
+
}
|