isard 0.0.0-stage → 0.2.1

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 (84) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -2
  4. package/dist/basemap.worker.d.ts +76 -0
  5. package/dist/basemap.worker.js +4 -0
  6. package/dist/basemap.worker.js.map +13 -0
  7. package/dist/basemaps.d.ts +48 -0
  8. package/dist/basemaps.js +97 -0
  9. package/dist/basemaps.js.map +1 -0
  10. package/dist/color.d.ts +8 -0
  11. package/dist/color.js +98 -0
  12. package/dist/color.js.map +1 -0
  13. package/dist/controls.d.ts +43 -0
  14. package/dist/controls.js +115 -0
  15. package/dist/controls.js.map +1 -0
  16. package/dist/decoder.worker.js +6 -0
  17. package/dist/decoder.worker.js.map +12 -0
  18. package/dist/decoders.d.ts +27 -0
  19. package/dist/decoders.js +109 -0
  20. package/dist/decoders.js.map +1 -0
  21. package/dist/equirectangular.d.ts +35 -0
  22. package/dist/equirectangular.js +143 -0
  23. package/dist/equirectangular.js.map +1 -0
  24. package/dist/extrusions.d.ts +79 -0
  25. package/dist/extrusions.js +224 -0
  26. package/dist/extrusions.js.map +1 -0
  27. package/dist/features.d.ts +159 -0
  28. package/dist/features.js +387 -0
  29. package/dist/features.js.map +1 -0
  30. package/dist/geoplateforme.d.ts +35 -0
  31. package/dist/geoplateforme.js +56 -0
  32. package/dist/geoplateforme.js.map +1 -0
  33. package/dist/glyphs.d.ts +104 -0
  34. package/dist/glyphs.js +383 -0
  35. package/dist/glyphs.js.map +1 -0
  36. package/dist/index.d.ts +34 -0
  37. package/dist/index.js +25 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/interaction.d.ts +53 -0
  40. package/dist/interaction.js +259 -0
  41. package/dist/interaction.js.map +1 -0
  42. package/dist/isard_bg.wasm +0 -0
  43. package/dist/isard_core_bg.wasm +0 -0
  44. package/dist/layers.d.ts +103 -0
  45. package/dist/layers.js +290 -0
  46. package/dist/layers.js.map +1 -0
  47. package/dist/loader.d.ts +30 -0
  48. package/dist/loader.js +102 -0
  49. package/dist/loader.js.map +1 -0
  50. package/dist/map.d.ts +211 -0
  51. package/dist/map.js +1222 -0
  52. package/dist/map.js.map +1 -0
  53. package/dist/mvt.d.ts +19 -0
  54. package/dist/mvt.js +231 -0
  55. package/dist/mvt.js.map +1 -0
  56. package/dist/overlay.d.ts +118 -0
  57. package/dist/overlay.js +334 -0
  58. package/dist/overlay.js.map +1 -0
  59. package/dist/protocol.d.ts +30 -0
  60. package/dist/protocol.js +2 -0
  61. package/dist/protocol.js.map +1 -0
  62. package/dist/style.d.ts +7 -0
  63. package/dist/style.js +130 -0
  64. package/dist/style.js.map +1 -0
  65. package/dist/symbols.d.ts +61 -0
  66. package/dist/symbols.js +238 -0
  67. package/dist/symbols.js.map +1 -0
  68. package/dist/terrain.d.ts +14 -0
  69. package/dist/terrain.js +56 -0
  70. package/dist/terrain.js.map +1 -0
  71. package/dist/types.d.ts +312 -0
  72. package/dist/types.js +2 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/vectorstyle.d.ts +83 -0
  75. package/dist/vectorstyle.js +518 -0
  76. package/dist/vectorstyle.js.map +1 -0
  77. package/dist/vectortiles.d.ts +124 -0
  78. package/dist/vectortiles.js +523 -0
  79. package/dist/vectortiles.js.map +1 -0
  80. package/dist/wasm/isard.d.ts +612 -0
  81. package/dist/wasm/isard.js +2623 -0
  82. package/dist/wasm/isard_core.d.ts +469 -0
  83. package/dist/wasm/isard_core.js +2385 -0
  84. package/package.json +49 -4
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Vector basemaps from a MapLibre style (OpenFreeMap's Positron by default): the fills, lines, circles and raster
3
+ * layers drawn tile by tile in workers into the raster pool, so that they lie on the relief like any basemap and composite
4
+ * with the hillshade in one pass; and the labels of the style's symbol layers in a symbol layer of their own, kept
5
+ * above the application's layers (as MapLibre's `beforeId: firstSymbol` does), shaped and placed by the engine.
6
+ *
7
+ * Any subset of the style can be drawn on its own raster layer (`raster({ layers })`), at its own place in the stack:
8
+ * rivers from the basemap's data over a flood layer, for instance, restyled with `extraLayers`.
9
+ */
10
+ import type { Priority } from './basemap.worker.ts';
11
+ import type { RasterLayer } from './layers.ts';
12
+ import type { GeoMap } from './map.ts';
13
+ import type { SymbolLayer } from './symbols.ts';
14
+ import { type LabelCandidate, type RasterSource, type Style, type StyleLayer } from './vectorstyle.ts';
15
+ import type { Camera } from './wasm/isard.js';
16
+ /** OpenFreeMap's styles: OpenStreetMap data in the OpenMapTiles schema, worldwide, free and without a key */
17
+ export declare const OPENFREEMAP_STYLES: {
18
+ readonly positron: "https://tiles.openfreemap.org/styles/positron";
19
+ readonly liberty: "https://tiles.openfreemap.org/styles/liberty";
20
+ readonly bright: "https://tiles.openfreemap.org/styles/bright";
21
+ };
22
+ /** PLAN.IGN vector styles of the Géoplateforme (France) */
23
+ export declare const PLAN_STYLES: {
24
+ readonly standard: "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json";
25
+ readonly grey: "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/gris.json";
26
+ };
27
+ /** Vector basemaps by name (see `BasemapName`) */
28
+ export declare const VECTOR_BASEMAPS: {
29
+ readonly 'osm-positron': "https://tiles.openfreemap.org/styles/positron";
30
+ readonly 'osm-liberty': "https://tiles.openfreemap.org/styles/liberty";
31
+ readonly 'osm-bright': "https://tiles.openfreemap.org/styles/bright";
32
+ readonly 'ign-plan-vector': "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json";
33
+ readonly 'ign-plan-grey-vector': "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/gris.json";
34
+ };
35
+ export interface VectorTilesOptions {
36
+ /** A style URL or object (default OpenFreeMap's Positron) */
37
+ style?: string | Style;
38
+ /** Workers drawing the tiles (default cores - 1, 1 to 4). The vector tiles of a map share its workers (and their
39
+ * decoded tiles); another count gives these tiles workers of their own */
40
+ workers?: number;
41
+ /** Where to load the worker from, when a bundler does not resolve the default URL (default the map's
42
+ * `basemapWorkerUrl`); another URL than the map's gives these tiles workers of their own */
43
+ workerUrl?: string | URL;
44
+ /** Credits of the tiles, shown while its raster layers are on the map (default: OpenFreeMap's for its styles, the
45
+ * IGN's for PLAN.IGN's) */
46
+ attribution?: string;
47
+ }
48
+ /** The credits of a known style: OpenFreeMap's, PLAN.IGN's */
49
+ export declare function styleAttribution(style: string | Style): string | undefined;
50
+ /** Default size of a basemap worker pool: cores - 1, 1 to 4 */
51
+ export declare function defaultWorkerCount(): number;
52
+ /** Default URL of the basemap worker, next to the module */
53
+ export declare function defaultWorkerUrl(): URL;
54
+ /** Fetch priority of a raster tile drawn by the workers: 'high' near the center of the view, at the current zoom or
55
+ * coarser; 'low' far from it or finer (prefetch); 'auto' between */
56
+ export declare function tilePriority(z: number, x: number, y: number, camera: Pick<Camera, 'center' | 'zoom'>): Priority;
57
+ /** The worker of a vector tile among `count`: always the same one */
58
+ export declare function workerIndex(vz: number, vx: number, vy: number, count: number): number;
59
+ /** The worker-side key of a renderer: unique across the `VectorTiles` sharing a pool */
60
+ export declare function rendererKey(namespace: string, n: number): string;
61
+ /** What the pool needs of a worker (a `Worker`, or a stand-in in tests) */
62
+ export type PoolWorker = Pick<Worker, 'postMessage' | 'terminate' | 'onmessage' | 'onerror' | 'onmessageerror'>;
63
+ export declare class VectorTiles {
64
+ private readonly map;
65
+ private readonly pool;
66
+ /** The pool is this instance's own (a worker count or URL other than the map's): stopped with it */
67
+ private readonly ownPool;
68
+ /** Prefix of its renderer keys in the pool's workers */
69
+ private readonly namespace;
70
+ private destroyed;
71
+ private keys;
72
+ /** Renderers sent to the workers, dropped on destroy */
73
+ private readonly renderers;
74
+ private readonly cleanup;
75
+ /** Credits of its raster layers */
76
+ private readonly attribution;
77
+ /** Its raster and label layers, removed with it */
78
+ private readonly layers;
79
+ /** The style once loaded, with its source resolved */
80
+ readonly ready: Promise<{
81
+ style: Style;
82
+ tiles: string;
83
+ maxzoom: number;
84
+ rasters: Record<string, RasterSource>;
85
+ }>;
86
+ /**
87
+ * Its workers are the map's (shared by every `VectorTiles` of the map, a vector tile decoded once for all of them),
88
+ * unless `workers` or `workerUrl` asks for others than the map's: then it has workers of its own
89
+ */
90
+ constructor(map: GeoMap, options?: VectorTilesOptions);
91
+ private load;
92
+ /** The fonts of the style's labels, for its glyphs (the first of each `text-font`, as the workers take it) */
93
+ private static fonts;
94
+ /** The style's sprite (its JSON and its image), loaded once, for the renderers that draw fill patterns */
95
+ private sprite;
96
+ private loadSprite;
97
+ private request;
98
+ /** Sends a renderer of these layers to every worker; returns its key */
99
+ private renderer;
100
+ /**
101
+ * A raster layer drawing the style's fills, lines and circles (or those `layers` keeps, then `extraLayers`, of the
102
+ * same source); `size`: texture size of its tiles (512 by default: sharp lines)
103
+ */
104
+ raster(options?: {
105
+ layers?: (layer: StyleLayer) => boolean;
106
+ extraLayers?: StyleLayer[];
107
+ size?: number;
108
+ opacity?: number;
109
+ minZoom?: number;
110
+ maxZoom?: number;
111
+ before?: Parameters<GeoMap['addRaster']>[0]['before'];
112
+ }): RasterLayer;
113
+ /**
114
+ * A symbol layer with the labels of the style's symbol layers (or those `layers` keeps), updated as the view
115
+ * moves: the labels of the vector tiles in view, the higher style layers placed first. It stays above the layers
116
+ * added after it unless one is moved over it.
117
+ */
118
+ labels(options?: {
119
+ layers?: (layer: StyleLayer) => boolean;
120
+ maxTiles?: number;
121
+ }): SymbolLayer<LabelCandidate>;
122
+ /** Stops its workers' work and removes its raster and label layers from the map (those still there) */
123
+ destroy(): void;
124
+ }
@@ -0,0 +1,523 @@
1
+ import { OPENFREEMAP_ATTRIBUTION } from "./basemaps.js";
2
+ import { IGN_ATTRIBUTION } from "./geoplateforme.js";
3
+ import { labelFont, } from "./vectorstyle.js";
4
+ /** OpenFreeMap's styles: OpenStreetMap data in the OpenMapTiles schema, worldwide, free and without a key */
5
+ export const OPENFREEMAP_STYLES = {
6
+ positron: 'https://tiles.openfreemap.org/styles/positron',
7
+ liberty: 'https://tiles.openfreemap.org/styles/liberty',
8
+ bright: 'https://tiles.openfreemap.org/styles/bright',
9
+ };
10
+ /** PLAN.IGN vector styles of the Géoplateforme (France) */
11
+ export const PLAN_STYLES = {
12
+ standard: 'https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json',
13
+ grey: 'https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/gris.json',
14
+ };
15
+ /** Vector basemaps by name (see `BasemapName`) */
16
+ export const VECTOR_BASEMAPS = {
17
+ 'osm-positron': OPENFREEMAP_STYLES.positron,
18
+ 'osm-liberty': OPENFREEMAP_STYLES.liberty,
19
+ 'osm-bright': OPENFREEMAP_STYLES.bright,
20
+ 'ign-plan-vector': PLAN_STYLES.standard,
21
+ 'ign-plan-grey-vector': PLAN_STYLES.grey,
22
+ };
23
+ /** The credits of a known style: OpenFreeMap's, PLAN.IGN's */
24
+ export function styleAttribution(style) {
25
+ if (typeof style !== 'string')
26
+ return undefined;
27
+ if (Object.values(OPENFREEMAP_STYLES).includes(style))
28
+ return OPENFREEMAP_ATTRIBUTION;
29
+ if (Object.values(PLAN_STYLES).includes(style))
30
+ return IGN_ATTRIBUTION;
31
+ return undefined;
32
+ }
33
+ /** Text size of a style (a number, legacy stops, or the stops of a zoom expression made by the worker) as a size for
34
+ * the symbol layer */
35
+ function size(value) {
36
+ if (typeof value === 'number')
37
+ return value;
38
+ if (Array.isArray(value) && value.length > 0 && value.every(Array.isArray)) {
39
+ return value.map((s) => [Number(s[0]), Number(s[1])]);
40
+ }
41
+ if (value && typeof value === 'object' && !Array.isArray(value) && Array.isArray(value.stops)) {
42
+ return value.stops.map((s) => {
43
+ const [z, v] = s;
44
+ return [Number(z), Number(v)];
45
+ });
46
+ }
47
+ return 16;
48
+ }
49
+ /** Fetches a JSON document (a style, a TileJSON) */
50
+ async function json(url) {
51
+ const response = await fetch(url);
52
+ if (!response.ok)
53
+ throw new Error(`${url}: ${response.status}`);
54
+ return (await response.json());
55
+ }
56
+ /** Default size of a basemap worker pool: cores - 1, 1 to 4 */
57
+ export function defaultWorkerCount() {
58
+ return Math.min(4, Math.max(1, (typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 2 : 2) - 1));
59
+ }
60
+ /** Default URL of the basemap worker, next to the module */
61
+ export function defaultWorkerUrl() {
62
+ return new URL('./basemap.worker.js', import.meta.url);
63
+ }
64
+ /** Fetch priority of a raster tile drawn by the workers: 'high' near the center of the view, at the current zoom or
65
+ * coarser; 'low' far from it or finer (prefetch); 'auto' between */
66
+ export function tilePriority(z, x, y, camera) {
67
+ // A raster tile of level z covers the map at zoom z - 1
68
+ if (z - 1 > camera.zoom + 1)
69
+ return 'low';
70
+ const n = 2 ** z;
71
+ const [lon, lat] = camera.center;
72
+ const cx = ((lon + 180) / 360) * n;
73
+ const phi = (Math.max(-85.05, Math.min(85.05, lat)) * Math.PI) / 180;
74
+ const cy = ((1 - Math.log(Math.tan(phi) + 1 / Math.cos(phi)) / Math.PI) / 2) * n;
75
+ // Across the antimeridian, the short way
76
+ let dx = Math.abs(x + 0.5 - cx) % n;
77
+ dx = Math.min(dx, n - dx);
78
+ const d = Math.hypot(dx, y + 0.5 - cy);
79
+ if (d <= 1.5)
80
+ return 'high';
81
+ return d >= 4 ? 'low' : 'auto';
82
+ }
83
+ /** The worker of a vector tile among `count`: always the same one */
84
+ export function workerIndex(vz, vx, vy, count) {
85
+ // Unsigned: every worker gets its share
86
+ return ((Math.imul(vx, 73856093) ^ Math.imul(vy, 19349663) ^ vz) >>> 0) % Math.max(1, count);
87
+ }
88
+ /** The worker-side key of a renderer: unique across the `VectorTiles` sharing a pool */
89
+ export function rendererKey(namespace, n) {
90
+ return `${namespace}:r${n}`;
91
+ }
92
+ /**
93
+ * @internal Workers drawing vector tiles, shared by the `VectorTiles` of a map: each vector tile always goes to the
94
+ * same worker, which keeps it decoded for every style drawn from it. Requests are tracked one by one, with their
95
+ * owner, so that one `VectorTiles` going away cancels its own requests only.
96
+ */
97
+ export class BasemapWorkers {
98
+ count;
99
+ url;
100
+ workers;
101
+ pending = new Map();
102
+ next = 1;
103
+ namespaces = 0;
104
+ terminated = false;
105
+ constructor(count, url, create = (u) => new Worker(u, { type: 'module' })) {
106
+ this.count = count;
107
+ this.url = url;
108
+ this.workers = Array.from({ length: Math.max(1, count) }, () => {
109
+ const worker = create(url);
110
+ worker.onmessage = ({ data }) => {
111
+ const p = this.pending.get(data.id);
112
+ if (!p)
113
+ return;
114
+ this.pending.delete(data.id);
115
+ if ('error' in data)
116
+ p.reject(new Error(data.error));
117
+ else
118
+ p.resolve(data);
119
+ };
120
+ // A worker that fails to load (a bundler serving it from elsewhere) or to read a message fails its requests
121
+ worker.onerror = (event) => {
122
+ event.preventDefault();
123
+ this.fail(worker, `the vector basemap worker ${url} failed${event.message ? `: ${event.message}` : ''}`);
124
+ };
125
+ worker.onmessageerror = () => this.fail(worker, `the vector basemap worker ${url} sent an unreadable message`);
126
+ return worker;
127
+ });
128
+ }
129
+ /** A prefix for the renderer keys of one `VectorTiles` */
130
+ namespace() {
131
+ return `v${this.namespaces++}`;
132
+ }
133
+ /** Requests in progress */
134
+ get size() {
135
+ return this.pending.size;
136
+ }
137
+ /** Rejects the requests of a failed worker */
138
+ fail(worker, message) {
139
+ const error = new Error(`isard: ${message}. If a bundler moves the worker, give its URL in the \`basemapWorkerUrl\` option (see "With a bundler" in the guide)`);
140
+ for (const [id, p] of this.pending) {
141
+ if (p.worker !== worker)
142
+ continue;
143
+ this.pending.delete(id);
144
+ p.reject(error);
145
+ }
146
+ }
147
+ /** The worker of a vector tile */
148
+ worker(vz, vx, vy) {
149
+ return this.workers[workerIndex(vz, vx, vy, this.workers.length)] ?? this.workers[0];
150
+ }
151
+ /** Sends a message to every worker (a renderer, or its removal) */
152
+ broadcast(message) {
153
+ if (this.terminated)
154
+ return;
155
+ // A renderer tells the workers how many they are: they share the decoded tiles' budget
156
+ const sent = message.type === 'init' ? { ...message, workers: this.workers.length } : message;
157
+ // A sprite image is copied to each worker (structured clone)
158
+ for (const w of this.workers)
159
+ w.postMessage(sent);
160
+ }
161
+ /** Asks a worker for a tile's image or labels, on behalf of `owner` */
162
+ request(worker, work, owner, signal) {
163
+ return new Promise((resolve, reject) => {
164
+ if (this.terminated)
165
+ return reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
166
+ if (signal?.aborted)
167
+ return reject(new DOMException('aborted', 'AbortError'));
168
+ const id = this.next++;
169
+ const abort = () => {
170
+ if (!this.pending.delete(id))
171
+ return;
172
+ worker.postMessage({ type: 'abort', id });
173
+ reject(new DOMException('aborted', 'AbortError'));
174
+ };
175
+ // The listener goes once the request is settled
176
+ const settle = () => signal?.removeEventListener('abort', abort);
177
+ this.pending.set(id, {
178
+ resolve: (r) => {
179
+ settle();
180
+ resolve(r);
181
+ },
182
+ reject: (e) => {
183
+ settle();
184
+ reject(e);
185
+ },
186
+ worker,
187
+ owner,
188
+ });
189
+ signal?.addEventListener('abort', abort, { once: true });
190
+ worker.postMessage({ ...work, id });
191
+ });
192
+ }
193
+ /** Cancels the requests of an owner (the workers stop them) */
194
+ cancel(owner) {
195
+ for (const [id, p] of this.pending) {
196
+ if (p.owner !== owner)
197
+ continue;
198
+ this.pending.delete(id);
199
+ p.worker.postMessage({ type: 'abort', id });
200
+ p.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
201
+ }
202
+ }
203
+ /** Stops the workers; their requests fail */
204
+ terminate() {
205
+ if (this.terminated)
206
+ return;
207
+ this.terminated = true;
208
+ for (const w of this.workers)
209
+ w.terminate();
210
+ const pending = [...this.pending.values()];
211
+ this.pending.clear();
212
+ for (const p of pending)
213
+ p.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
214
+ }
215
+ }
216
+ export class VectorTiles {
217
+ map;
218
+ pool;
219
+ /** The pool is this instance's own (a worker count or URL other than the map's): stopped with it */
220
+ ownPool;
221
+ /** Prefix of its renderer keys in the pool's workers */
222
+ namespace;
223
+ destroyed = false;
224
+ keys = 0;
225
+ /** Renderers sent to the workers, dropped on destroy */
226
+ renderers = [];
227
+ cleanup = [];
228
+ /** Credits of its raster layers */
229
+ attribution;
230
+ /** Its raster and label layers, removed with it */
231
+ layers = [];
232
+ /** The style once loaded, with its source resolved */
233
+ ready;
234
+ /**
235
+ * Its workers are the map's (shared by every `VectorTiles` of the map, a vector tile decoded once for all of them),
236
+ * unless `workers` or `workerUrl` asks for others than the map's: then it has workers of its own
237
+ */
238
+ constructor(map, options = {}) {
239
+ this.map = map;
240
+ const { pool, own } = map.basemapWorkers(options.workers, options.workerUrl);
241
+ this.pool = pool;
242
+ this.ownPool = own;
243
+ this.namespace = pool.namespace();
244
+ const style = options.style ?? OPENFREEMAP_STYLES.positron;
245
+ this.attribution = options.attribution ?? styleAttribution(style);
246
+ this.ready = this.load(style);
247
+ // Reported once here; `ready`, the raster tiles and the labels fail with it
248
+ this.ready.catch((error) => {
249
+ if (!this.destroyed)
250
+ console.warn('isard: vector tiles', error);
251
+ });
252
+ }
253
+ async load(input) {
254
+ const style = typeof input === 'string' ? await json(input) : input;
255
+ const [name, source] = Object.entries(style.sources).find(([, s]) => s.type === 'vector') ?? [];
256
+ if (!name || !source)
257
+ throw new Error('the style has no vector source');
258
+ let { tiles, maxzoom } = source;
259
+ if (!tiles && source.url) {
260
+ const tilejson = await json(source.url);
261
+ tiles = tilejson.tiles;
262
+ maxzoom ??= tilejson.maxzoom;
263
+ }
264
+ const template = tiles?.[0];
265
+ if (!template)
266
+ throw new Error(`source ${name} has no tiles`);
267
+ // Its raster sources of tiles, drawn into the same images (the shaded relief of a style at low zoom)
268
+ const rasters = {};
269
+ for (const [key, s] of Object.entries(style.sources)) {
270
+ const url = s.type === 'raster' ? s.tiles?.[0] : undefined;
271
+ if (url)
272
+ rasters[key] = { tiles: url, maxzoom: s.maxzoom ?? 22, tileSize: s.tileSize ?? 512 };
273
+ }
274
+ // Only the layers of those sources (and the background); other types (hillshade, heatmap…) are left out. The
275
+ // application's style object stays as it is
276
+ const layers = style.layers.filter((l) => l.type === 'background' ||
277
+ (l.source === name && l.type !== 'raster') ||
278
+ (l.type === 'raster' && l.source !== undefined && rasters[l.source] !== undefined));
279
+ return { style: { ...style, layers }, tiles: template, maxzoom: maxzoom ?? 14, rasters };
280
+ }
281
+ /** The fonts of the style's labels, for its glyphs (the first of each `text-font`, as the workers take it) */
282
+ static fonts(style) {
283
+ const fonts = new Set();
284
+ for (const layer of style.layers) {
285
+ const font = layer.layout?.['text-font'];
286
+ if (layer.type === 'symbol' && Array.isArray(font) && typeof font[0] === 'string')
287
+ fonts.add(labelFont(font[0]));
288
+ }
289
+ return [...fonts];
290
+ }
291
+ /** The style's sprite (its JSON and its image), loaded once, for the renderers that draw fill patterns */
292
+ sprite;
293
+ loadSprite(url) {
294
+ this.sprite ??= (async () => {
295
+ try {
296
+ const [json, png] = await Promise.all([fetch(`${url}.json`), fetch(`${url}.png`)]);
297
+ if (!json.ok || !png.ok)
298
+ return undefined;
299
+ return { icons: await json.json(), image: await createImageBitmap(await png.blob()) };
300
+ }
301
+ catch {
302
+ return undefined;
303
+ }
304
+ })();
305
+ return this.sprite;
306
+ }
307
+ request(vz, vx, vy, work, signal) {
308
+ if (this.destroyed)
309
+ return Promise.reject(new DOMException('isard: vector tiles destroyed', 'AbortError'));
310
+ return this.pool.request(this.pool.worker(vz, vx, vy), work, this, signal);
311
+ }
312
+ /** Sends a renderer of these layers to every worker; returns its key */
313
+ async renderer(select, extra) {
314
+ const { style, tiles, maxzoom, rasters } = await this.ready;
315
+ const key = rendererKey(this.namespace, this.keys++);
316
+ const layers = [...style.layers.filter(select), ...extra];
317
+ const patterns = layers.some((l) => l.paint?.['fill-pattern'] !== undefined);
318
+ // The image is downloaded once, copied to each worker
319
+ const sprite = patterns && style.sprite ? await this.loadSprite(style.sprite) : undefined;
320
+ if (this.destroyed)
321
+ throw new DOMException('isard: vector tiles destroyed', 'AbortError');
322
+ this.pool.broadcast({ type: 'init', key, tiles, maxzoom, layers, rasters, ...(sprite ? { sprite } : {}) });
323
+ this.renderers.push(key);
324
+ return { key, maxzoom };
325
+ }
326
+ /**
327
+ * A raster layer drawing the style's fills, lines and circles (or those `layers` keeps, then `extraLayers`, of the
328
+ * same source); `size`: texture size of its tiles (512 by default: sharp lines)
329
+ */
330
+ raster(options = {}) {
331
+ const { layers = () => true, extraLayers = [], size: texture = 512, opacity = 1, before } = options;
332
+ const drawn = (l) => l.type !== 'symbol' && layers(l);
333
+ const renderer = this.renderer(drawn, extraLayers);
334
+ // Its failure is reported by `ready` and each tile
335
+ renderer.catch(() => undefined);
336
+ const loader = async (z, x, y, signal) => {
337
+ const { key, maxzoom } = await renderer;
338
+ // The vector tile it is drawn from (see the worker)
339
+ const vz = Math.max(0, Math.min(z - 1, maxzoom));
340
+ const shift = z - vz;
341
+ const priority = tilePriority(z, x, y, this.map.getCamera());
342
+ const response = await this.request(vz, x >> shift, y >> shift, { type: 'render', key, z, x, y, size: texture, priority }, signal);
343
+ return 'image' in response ? response.image : null;
344
+ };
345
+ const layer = this.map.addRaster({
346
+ url: '',
347
+ size: texture,
348
+ opacity,
349
+ minZoom: options.minZoom ?? 0,
350
+ maxZoom: options.maxZoom ?? 20,
351
+ loader,
352
+ ...(this.attribution ? { attribution: this.attribution } : {}),
353
+ ...(before ? { before } : {}),
354
+ });
355
+ this.layers.push(layer);
356
+ return layer;
357
+ }
358
+ /**
359
+ * A symbol layer with the labels of the style's symbol layers (or those `layers` keeps), updated as the view
360
+ * moves: the labels of the vector tiles in view, the higher style layers placed first. It stays above the layers
361
+ * added after it unless one is moved over it.
362
+ */
363
+ labels(options = {}) {
364
+ const { layers = () => true, maxTiles = 36 } = options;
365
+ const symbol = this.map.addSymbols({
366
+ data: [],
367
+ text: (p) => p.text,
368
+ font: (p) => p.font,
369
+ size: (p) => size(p.size),
370
+ color: (p) => p.color,
371
+ haloColor: (p) => p.haloColor,
372
+ haloWidth: (p) => p.haloWidth,
373
+ anchor: (p) => p.anchor,
374
+ offset: (p) => p.offset,
375
+ maxWidth: (p) => p.maxWidth,
376
+ lineHeight: (p) => p.lineHeight,
377
+ overlap: (p) => p.overlap,
378
+ placement: 'line',
379
+ });
380
+ this.map.keepOnTop(symbol);
381
+ this.layers.push(symbol);
382
+ const renderer = this.ready.then(({ style }) => {
383
+ // The style's fonts from its own glyphs
384
+ if (style.glyphs)
385
+ this.map.glyphs.register(style.glyphs, VectorTiles.fonts(style));
386
+ return this.renderer((l) => l.type === 'symbol' && layers(l), []);
387
+ });
388
+ renderer.catch(() => undefined);
389
+ /** The labels of the tiles, the most recently used last */
390
+ const tiles = new Map();
391
+ let generation = 0;
392
+ /** The tiles whose labels are shown */
393
+ let shown = '';
394
+ const update = async () => {
395
+ if (this.destroyed)
396
+ return;
397
+ const { key, maxzoom } = await renderer;
398
+ const run = ++generation;
399
+ const camera = this.map.getCamera();
400
+ const z = Math.max(0, Math.min(Math.floor(camera.zoom), maxzoom));
401
+ const n = 2 ** z;
402
+ const [west, south, east, north] = this.map.getBounds();
403
+ const tx = (lon) => Math.floor(((lon + 180) / 360) * n);
404
+ const ty = (lat) => {
405
+ const phi = (Math.max(-85.05, Math.min(85.05, lat)) * Math.PI) / 180;
406
+ return Math.floor(((1 - Math.log(Math.tan(phi) + 1 / Math.cos(phi)) / Math.PI) / 2) * n);
407
+ };
408
+ const [cx, cy] = [((camera.center[0] + 180) / 360) * n, ty(camera.center[1])];
409
+ // The tiles of the bounds, within a window around the center: a tilted view's bounds reach far, and only the
410
+ // nearest `maxTiles` are kept anyway
411
+ const reach = Math.ceil(Math.sqrt(maxTiles)) + 2;
412
+ const [x0, x1] = [
413
+ Math.max(0, tx(west), Math.floor(cx) - reach),
414
+ Math.min(n - 1, tx(east), Math.floor(cx) + reach),
415
+ ];
416
+ const [y0, y1] = [
417
+ Math.max(0, ty(north), Math.floor(cy) - reach),
418
+ Math.min(n - 1, ty(south), Math.floor(cy) + reach),
419
+ ];
420
+ const wanted = [];
421
+ for (let y = y0; y <= y1; y += 1)
422
+ for (let x = x0; x <= x1; x += 1)
423
+ wanted.push([x, y]);
424
+ // Nearest to the center first, at most `maxTiles` (tilted views reach far)
425
+ wanted.sort((a, b) => Math.hypot(a[0] + 0.5 - cx, a[1] + 0.5 - cy) - Math.hypot(b[0] + 0.5 - cx, b[1] + 0.5 - cy));
426
+ const keys = wanted.slice(0, maxTiles).map(([x, y]) => `${z}/${x}/${y}`);
427
+ // The same tiles as the labels shown: nothing to do
428
+ const set = keys.join(' ');
429
+ if (set === shown)
430
+ return;
431
+ const lists = await Promise.all(keys.map((k, index) => {
432
+ let p = tiles.get(k);
433
+ if (p) {
434
+ // Used again: the most recent
435
+ tiles.delete(k);
436
+ tiles.set(k, p);
437
+ return p;
438
+ }
439
+ const [tz, x, y] = k.split('/').map(Number);
440
+ // The nearest tiles first: the others wait for the raster tiles
441
+ const priority = index < 4 ? 'auto' : 'low';
442
+ p = this.request(tz, x, y, { type: 'labels', key, z: tz, x, y, priority }).then((r) => ('labels' in r ? r.labels : []), () => {
443
+ // A network error is not kept: the tile is asked for again on the next update
444
+ tiles.delete(k);
445
+ return [];
446
+ });
447
+ tiles.set(k, p);
448
+ // A bounded cache of tiles' labels, the least recently used dropped
449
+ if (tiles.size > 256) {
450
+ const oldest = tiles.keys().next().value;
451
+ if (oldest !== undefined)
452
+ tiles.delete(oldest);
453
+ }
454
+ return p;
455
+ }));
456
+ if (run !== generation || this.destroyed)
457
+ return;
458
+ // A tile that failed is missing: the next update asks for it again
459
+ shown = keys.every((k) => tiles.has(k)) ? set : '';
460
+ const features = lists
461
+ .flat()
462
+ // The labels of the higher style layers first: they win the collisions
463
+ .sort((a, b) => b.layer - a.layer)
464
+ .map((p) => ({
465
+ type: 'Feature',
466
+ properties: p,
467
+ geometry: {
468
+ type: p.along ? 'LineString' : 'Point',
469
+ coordinates: p.along
470
+ ? [
471
+ [p.lon - p.along[0] / 2, p.lat - p.along[1] / 2],
472
+ [p.lon + p.along[0] / 2, p.lat + p.along[1] / 2],
473
+ ]
474
+ : [p.lon, p.lat],
475
+ },
476
+ }));
477
+ symbol.setData(features);
478
+ };
479
+ // The style failed to load: reported by `ready`
480
+ const refresh = () => {
481
+ update().catch(() => undefined);
482
+ };
483
+ let timer = 0;
484
+ const schedule = () => {
485
+ if (timer)
486
+ return;
487
+ timer = window.setTimeout(() => {
488
+ timer = 0;
489
+ refresh();
490
+ }, 250);
491
+ };
492
+ this.cleanup.push(this.map.on('move', schedule), this.map.on('moveend', refresh), () => clearTimeout(timer));
493
+ refresh();
494
+ return symbol;
495
+ }
496
+ /** Stops its workers' work and removes its raster and label layers from the map (those still there) */
497
+ destroy() {
498
+ if (this.destroyed)
499
+ return;
500
+ this.destroyed = true;
501
+ for (const off of this.cleanup)
502
+ off();
503
+ this.cleanup.length = 0;
504
+ for (const layer of this.layers)
505
+ if (this.map.hasLayer(layer))
506
+ layer.remove();
507
+ this.layers.length = 0;
508
+ this.map.dropVectorTiles(this);
509
+ // The page's copy of the sprite (the workers have theirs)
510
+ this.sprite?.then((sprite) => sprite?.image.close()).catch(() => undefined);
511
+ if (this.ownPool) {
512
+ this.pool.terminate();
513
+ }
514
+ else {
515
+ // Shared workers: only its requests stop and its renderers go
516
+ this.pool.cancel(this);
517
+ for (const key of this.renderers)
518
+ this.pool.broadcast({ type: 'drop', key });
519
+ }
520
+ this.renderers.length = 0;
521
+ }
522
+ }
523
+ //# sourceMappingURL=vectortiles.js.map