map-zero 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +220 -0
  4. package/docs/api.md +66 -0
  5. package/docs/architecture.md +87 -0
  6. package/docs/cartography.md +77 -0
  7. package/docs/cesium.md +107 -0
  8. package/docs/openlayers.md +98 -0
  9. package/docs/styles.md +103 -0
  10. package/package.json +51 -0
  11. package/packages/cesium/package.json +13 -0
  12. package/packages/cesium/src/index.js +405 -0
  13. package/packages/ol/package.json +14 -0
  14. package/packages/ol/src/index.js +1705 -0
  15. package/packages/ol/src/labels.js +977 -0
  16. package/src/3dtiles/b3dm.js +38 -0
  17. package/src/3dtiles/clipper-surfaces.js +317 -0
  18. package/src/3dtiles/export.js +768 -0
  19. package/src/3dtiles/extrude.js +301 -0
  20. package/src/3dtiles/flat.js +531 -0
  21. package/src/3dtiles/glb.js +178 -0
  22. package/src/3dtiles/gpkg-buildings.js +240 -0
  23. package/src/3dtiles/gpkg-features.js +157 -0
  24. package/src/3dtiles/tileset.js +75 -0
  25. package/src/build.js +134 -0
  26. package/src/cli.js +656 -0
  27. package/src/export-pmtiles.js +962 -0
  28. package/src/geometry-read.js +50 -0
  29. package/src/gpkg-read.js +460 -0
  30. package/src/gpkg.js +567 -0
  31. package/src/html.js +593 -0
  32. package/src/layers.js +357 -0
  33. package/src/manifest.js +29 -0
  34. package/src/mvt.js +2593 -0
  35. package/src/ol.js +5 -0
  36. package/src/osm.js +2110 -0
  37. package/src/pmtiles-worker.js +70 -0
  38. package/src/pmtiles.js +260 -0
  39. package/src/server.js +720 -0
  40. package/src/style-command.js +78 -0
  41. package/src/style-filters.js +76 -0
  42. package/src/style-presets.js +93 -0
  43. package/src/style-themes.js +235 -0
  44. package/src/style.js +13 -0
  45. package/src/tile-cache.js +59 -0
  46. package/src/utils.js +222 -0
  47. package/styles/presets/light.json +4655 -0
  48. package/styles/presets/monochrome.json +4655 -0
  49. package/styles/presets/neon-dark-3d.json +90 -0
  50. package/styles/presets/neon-dark.json +4690 -0
  51. package/styles/presets/tactical.json +4690 -0
  52. package/styles/themes/neon-dark.theme.json +20 -0
@@ -0,0 +1,1705 @@
1
+ import MVT from 'ol/format/MVT.js';
2
+ import WebGLVectorTileLayer from 'ol/layer/WebGLVectorTile.js';
3
+ import WebGLVectorTileLayerRenderer from 'ol/renderer/webgl/VectorTileLayer.js';
4
+ import VectorTileSource from 'ol/source/VectorTile.js';
5
+ import { createXYZ } from 'ol/tilegrid.js';
6
+ import { PMTiles } from 'pmtiles';
7
+
8
+ import {
9
+ activeLabelLayerIdsForZoom,
10
+ createMapZeroLabelLayer,
11
+ hasEnabledLabels
12
+ } from './labels.js';
13
+
14
+ let autoInstanceCounter = 0;
15
+
16
+ /**
17
+ * @typedef {{
18
+ * id?: string,
19
+ * manifestUrl: string,
20
+ * manifest?: Record<string, unknown>,
21
+ * style?: string | Record<string, unknown>,
22
+ * visibleLayers?: string[] | Set<string>,
23
+ * source?: 'auto' | 'pmtiles' | 'dynamic',
24
+ * apiBaseUrl?: string,
25
+ * zIndexBase?: number,
26
+ * onTileLoadStart?: () => void,
27
+ * onTileLoadEnd?: () => void,
28
+ * onTileLoadError?: () => void
29
+ * }} MapZeroOpenLayersOptions
30
+ */
31
+
32
+ /**
33
+ * Load a map-zero manifest.
34
+ *
35
+ * @param {string} manifestUrl
36
+ * @returns {Promise<Record<string, unknown>>}
37
+ */
38
+ export async function loadMapZeroManifest(manifestUrl) {
39
+ return fetchJson(resolveUrl(manifestUrl, documentBaseUrl()));
40
+ }
41
+
42
+ /**
43
+ * Load a standalone map-zero style JSON document.
44
+ *
45
+ * The returned object can be reused across multiple package instances. Helpers
46
+ * treat style objects as readonly and keep package-specific state elsewhere.
47
+ *
48
+ * @param {string} styleUrl
49
+ * @returns {Promise<Record<string, unknown>>}
50
+ */
51
+ export async function loadMapZeroStyle(styleUrl) {
52
+ return fetchJson(resolveUrl(styleUrl, documentBaseUrl()));
53
+ }
54
+
55
+ /**
56
+ * Create OpenLayers layers for a map-zero package without adding them to a map.
57
+ *
58
+ * @param {MapZeroOpenLayersOptions} options
59
+ * @returns {Promise<{
60
+ * id: string,
61
+ * manifest: Record<string, unknown>,
62
+ * style: Record<string, unknown>,
63
+ * layers: WebGLVectorTileLayer[],
64
+ * attachMap?: (map: unknown) => void,
65
+ * detachMap?: () => void,
66
+ * setVisible: (layerId: string, visible: boolean) => void,
67
+ * setOpacity: (layerId: string, opacity: number) => void,
68
+ * destroy: () => void
69
+ * }>}
70
+ */
71
+ export async function createMapZeroOpenLayersLayers(options) {
72
+ patchWebGlVectorTileRenderer();
73
+
74
+ const manifestUrl = resolveUrl(options.manifestUrl, documentBaseUrl());
75
+ const manifestBaseUrl = new URL('.', manifestUrl).href;
76
+ const manifest = options.manifest ?? await loadMapZeroManifest(manifestUrl);
77
+ const instanceId = createInstanceId(options.id, manifest, manifestUrl);
78
+ const styleDocument = await loadStyleDocument(manifest, manifestBaseUrl, options.style ?? 'default');
79
+ const orderedLayers = orderManifestLayers(manifest, styleDocument);
80
+ const layerVisibility = createLayerVisibility(orderedLayers, styleDocument, options.visibleLayers);
81
+ const layerOpacity = new Map(orderedLayers.map((layer) => [layer.id, 1]));
82
+ const context = {
83
+ instanceId,
84
+ manifest,
85
+ manifestUrl,
86
+ manifestBaseUrl,
87
+ styleDocument,
88
+ orderedLayers,
89
+ layerVisibility,
90
+ layerOpacity,
91
+ sourceMode: resolveSourceMode(manifest, options.source ?? 'auto'),
92
+ apiBaseUrl: resolveApiBaseUrl(options.apiBaseUrl, manifestUrl),
93
+ onTileLoadStart: options.onTileLoadStart,
94
+ onTileLoadEnd: options.onTileLoadEnd,
95
+ onTileLoadError: options.onTileLoadError
96
+ };
97
+
98
+ const source = createTileSource(context);
99
+ const layer = new WebGLVectorTileLayer({
100
+ source,
101
+ preload: 0,
102
+ useInterimTilesOnError: false,
103
+ style: createWebGlStyles(context)
104
+ });
105
+ tagOpenLayersLayer(layer, instanceId, orderedLayers.map((item) => item.id), 'geometry');
106
+ const labelController = hasEnabledLabels(styleDocument)
107
+ ? createMapZeroLabelLayer({
108
+ instanceId,
109
+ tileUrlFunction: createLabelTileUrlFunction(context),
110
+ loadTileData: createLabelTileDataLoader(context),
111
+ sourceOptions: createVectorTileSourceZoomOptions(context),
112
+ styleDocument,
113
+ onTileLoadStart: options.onTileLoadStart,
114
+ onTileLoadEnd: options.onTileLoadEnd,
115
+ onTileLoadError: options.onTileLoadError
116
+ })
117
+ : null;
118
+
119
+ applyLayerZIndex(layer, orderedLayers, styleDocument, options.zIndexBase);
120
+ if (labelController) {
121
+ tagOpenLayersLayer(labelController.layer, instanceId, orderedLayers.map((item) => item.id), 'labels');
122
+ labelController.layer.setZIndex(layer.getZIndex() + 100);
123
+ }
124
+
125
+ const refresh = () => {
126
+ layer.setStyle(createWebGlStyles(context));
127
+ source.setTileUrlFunction(createTileUrlFunction(context), String(Date.now()));
128
+ labelController?.refresh();
129
+ layer.changed();
130
+ };
131
+
132
+ return {
133
+ id: instanceId,
134
+ manifest,
135
+ style: styleDocument,
136
+ layers: labelController ? [layer, labelController.layer] : [layer],
137
+ attachMap(map) {
138
+ labelController?.attachMap(/** @type {Parameters<typeof labelController.attachMap>[0]} */ (map));
139
+ },
140
+ detachMap() {
141
+ labelController?.detachMap();
142
+ },
143
+ setVisible(layerId, visible) {
144
+ if (!layerVisibility.has(layerId)) {
145
+ throw new Error(`unknown map-zero layer: ${layerId}`);
146
+ }
147
+
148
+ layerVisibility.set(layerId, Boolean(visible));
149
+ refresh();
150
+ },
151
+ setOpacity(layerId, opacity) {
152
+ if (!layerOpacity.has(layerId)) {
153
+ throw new Error(`unknown map-zero layer: ${layerId}`);
154
+ }
155
+
156
+ layerOpacity.set(layerId, clamp(Number(opacity), 0, 1));
157
+ layer.setStyle(createWebGlStyles(context));
158
+ layer.changed();
159
+ },
160
+ destroy() {
161
+ source.clear();
162
+ labelController?.destroy();
163
+ layer.dispose();
164
+ }
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Add map-zero layers to an existing OpenLayers map.
170
+ *
171
+ * The helper does not create or own the map. It only adds map-zero layers.
172
+ *
173
+ * @param {{ addLayer: (layer: WebGLVectorTileLayer) => void, removeLayer: (layer: WebGLVectorTileLayer) => void }} map
174
+ * @param {MapZeroOpenLayersOptions} options
175
+ * @returns {Promise<{
176
+ * id: string,
177
+ * manifest: Record<string, unknown>,
178
+ * style: Record<string, unknown>,
179
+ * layers: WebGLVectorTileLayer[],
180
+ * setVisible: (layerId: string, visible: boolean) => void,
181
+ * setOpacity: (layerId: string, opacity: number) => void,
182
+ * destroy: () => void
183
+ * }>}
184
+ */
185
+ export async function addMapZeroToOpenLayers(map, options) {
186
+ const controller = await createMapZeroOpenLayersLayers(options);
187
+
188
+ for (const layer of controller.layers) {
189
+ map.addLayer(layer);
190
+ }
191
+ controller.attachMap?.(map);
192
+
193
+ return {
194
+ ...controller,
195
+ destroy() {
196
+ controller.detachMap?.();
197
+ for (const layer of controller.layers) {
198
+ map.removeLayer(layer);
199
+ }
200
+
201
+ controller.destroy();
202
+ }
203
+ };
204
+ }
205
+
206
+ /**
207
+ * @param {string | undefined} id
208
+ * @param {Record<string, unknown>} manifest
209
+ * @param {string} manifestUrl
210
+ * @returns {string}
211
+ */
212
+ function createInstanceId(id, manifest, manifestUrl) {
213
+ if (id) {
214
+ return safeInstanceId(id);
215
+ }
216
+
217
+ const name = typeof manifest.name === 'string' && manifest.name.trim()
218
+ ? manifest.name
219
+ : new URL(manifestUrl).pathname.split('/').filter(Boolean).at(-2) ?? 'mapzero';
220
+ autoInstanceCounter += 1;
221
+ return `${safeInstanceId(name)}-${autoInstanceCounter}`;
222
+ }
223
+
224
+ /**
225
+ * @param {string} id
226
+ * @returns {string}
227
+ */
228
+ function safeInstanceId(id) {
229
+ return id.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'mapzero';
230
+ }
231
+
232
+ /**
233
+ * @param {Record<string, unknown>} manifest
234
+ * @param {string} manifestBaseUrl
235
+ * @param {string | Record<string, unknown>} style
236
+ * @returns {Promise<Record<string, unknown>>}
237
+ */
238
+ async function loadStyleDocument(manifest, manifestBaseUrl, style) {
239
+ if (style && typeof style === 'object') {
240
+ return style;
241
+ }
242
+
243
+ const styleName = String(style || 'default');
244
+ const styles = manifest.styles && typeof manifest.styles === 'object'
245
+ ? /** @type {Record<string, string>} */ (manifest.styles)
246
+ : {};
247
+ const styleUrl = styles[styleName] ?? styleName;
248
+ return fetchJson(resolveUrl(styleUrl, manifestBaseUrl));
249
+ }
250
+
251
+ /**
252
+ * @param {Record<string, unknown>} manifest
253
+ * @param {Record<string, unknown>} styleDocument
254
+ * @returns {Array<{ id: string, type?: string, style?: string }>}
255
+ */
256
+ function orderManifestLayers(manifest, styleDocument) {
257
+ const layers = Array.isArray(manifest.layers)
258
+ ? /** @type {Array<{ id: string, type?: string, style?: string }>} */ (manifest.layers)
259
+ : [];
260
+ const drawOrder = Array.isArray(styleDocument.drawOrder)
261
+ ? /** @type {string[]} */ (styleDocument.drawOrder)
262
+ : layers.map((layer) => layer.id);
263
+
264
+ return [...layers].sort((a, b) => {
265
+ const ai = drawOrder.indexOf(a.id);
266
+ const bi = drawOrder.indexOf(b.id);
267
+ const ao = getLayerRule(styleDocument, a).order ?? 0;
268
+ const bo = getLayerRule(styleDocument, b).order ?? 0;
269
+ return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi) || Number(ao) - Number(bo);
270
+ });
271
+ }
272
+
273
+ /**
274
+ * @param {Array<{ id: string, type?: string, style?: string }>} orderedLayers
275
+ * @param {Record<string, unknown>} styleDocument
276
+ * @param {string[] | Set<string> | undefined} visibleLayers
277
+ * @returns {Map<string, boolean>}
278
+ */
279
+ function createLayerVisibility(orderedLayers, styleDocument, visibleLayers) {
280
+ const selected = visibleLayers ? new Set(visibleLayers) : null;
281
+ const visibility = new Map();
282
+
283
+ for (const layer of orderedLayers) {
284
+ const rule = getLayerRule(styleDocument, layer);
285
+ visibility.set(layer.id, (selected ? selected.has(layer.id) : true) && rule.visible !== false);
286
+ }
287
+
288
+ return visibility;
289
+ }
290
+
291
+ /**
292
+ * @param {WebGLVectorTileLayer} layer
293
+ * @param {Array<{ id: string, type?: string, style?: string }>} orderedLayers
294
+ * @param {Record<string, unknown>} styleDocument
295
+ * @param {number | undefined} zIndexBase
296
+ */
297
+ function applyLayerZIndex(layer, orderedLayers, styleDocument, zIndexBase) {
298
+ const maxOrder = orderedLayers.reduce((max, item) => {
299
+ const order = Number(getLayerRule(styleDocument, item).order);
300
+ return Number.isFinite(order) ? Math.max(max, order) : max;
301
+ }, 0);
302
+ layer.setZIndex((Number.isFinite(Number(zIndexBase)) ? Number(zIndexBase) : 0) + maxOrder);
303
+ }
304
+
305
+ /**
306
+ * @param {unknown} layer
307
+ * @param {string} instanceId
308
+ * @param {string[]} layerIds
309
+ * @param {'geometry' | 'labels'} role
310
+ */
311
+ function tagOpenLayersLayer(layer, instanceId, layerIds, role) {
312
+ const namespacedLayerIds = layerIds.map((layerId) => namespaceLayerId(instanceId, layerId));
313
+ if (typeof layer?.set === 'function') {
314
+ layer.set('mapzero:id', instanceId);
315
+ layer.set('mapzero:role', role);
316
+ layer.set('mapzero:layerIds', namespacedLayerIds);
317
+ }
318
+ }
319
+
320
+ /**
321
+ * @param {string} instanceId
322
+ * @param {string} layerId
323
+ * @returns {string}
324
+ */
325
+ function namespaceLayerId(instanceId, layerId) {
326
+ return `${instanceId}:${layerId}`;
327
+ }
328
+
329
+ /**
330
+ * @param {Record<string, unknown>} manifest
331
+ * @param {'auto' | 'pmtiles' | 'dynamic'} source
332
+ * @returns {'pmtiles' | 'dynamic'}
333
+ */
334
+ function resolveSourceMode(manifest, source) {
335
+ if (source === 'dynamic') {
336
+ return 'dynamic';
337
+ }
338
+
339
+ const hasPmtiles = isPmtilesManifest(manifest);
340
+ if (source === 'pmtiles') {
341
+ if (!hasPmtiles) {
342
+ throw new Error('manifest does not define vector PMTiles');
343
+ }
344
+
345
+ return 'pmtiles';
346
+ }
347
+
348
+ return hasPmtiles ? 'pmtiles' : 'dynamic';
349
+ }
350
+
351
+ /**
352
+ * @param {string | undefined} apiBaseUrl
353
+ * @param {string} manifestUrl
354
+ * @returns {string}
355
+ */
356
+ function resolveApiBaseUrl(apiBaseUrl, manifestUrl) {
357
+ if (apiBaseUrl) {
358
+ return resolveUrl(apiBaseUrl, manifestUrl).replace(/\/$/, '');
359
+ }
360
+
361
+ return new URL('/api', manifestUrl).href.replace(/\/$/, '');
362
+ }
363
+
364
+ /**
365
+ * @param {Record<string, unknown>} manifest
366
+ * @returns {boolean}
367
+ */
368
+ function isPmtilesManifest(manifest) {
369
+ const tiles = manifest.tiles;
370
+ return Boolean(
371
+ tiles &&
372
+ typeof tiles === 'object' &&
373
+ tiles.format === 'pmtiles' &&
374
+ tiles.type === 'mvt' &&
375
+ typeof tiles.url === 'string'
376
+ );
377
+ }
378
+
379
+ /**
380
+ * @param {{
381
+ * instanceId: string,
382
+ * manifest: Record<string, unknown>,
383
+ * manifestBaseUrl: string,
384
+ * styleDocument: Record<string, unknown>,
385
+ * orderedLayers: Array<{ id: string, type?: string, style?: string }>,
386
+ * layerVisibility: Map<string, boolean>,
387
+ * sourceMode: 'pmtiles' | 'dynamic',
388
+ * apiBaseUrl: string,
389
+ * onTileLoadStart?: () => void,
390
+ * onTileLoadEnd?: () => void,
391
+ * onTileLoadError?: () => void
392
+ * }} context
393
+ * @returns {VectorTileSource}
394
+ */
395
+ function createTileSource(context) {
396
+ const format = new MVT();
397
+ const sourceOptions = {
398
+ format,
399
+ ...createVectorTileSourceZoomOptions(context),
400
+ cacheSize: 1024,
401
+ transition: 0,
402
+ wrapX: false,
403
+ tileUrlFunction: createTileUrlFunction(context)
404
+ };
405
+
406
+ if (context.sourceMode === 'pmtiles') {
407
+ sourceOptions.tileLoadFunction = createPmtilesTileLoadFunction(format, createPmtilesArchive(context));
408
+ }
409
+
410
+ const source = new VectorTileSource(sourceOptions);
411
+ if (context.onTileLoadStart) {
412
+ source.on('tileloadstart', context.onTileLoadStart);
413
+ }
414
+ if (context.onTileLoadEnd) {
415
+ source.on('tileloadend', context.onTileLoadEnd);
416
+ }
417
+ if (context.onTileLoadError) {
418
+ source.on('tileloaderror', context.onTileLoadError);
419
+ }
420
+
421
+ return source;
422
+ }
423
+
424
+ /**
425
+ * @param {{ sourceMode: 'pmtiles' | 'dynamic', manifest: Record<string, unknown> }} context
426
+ * @returns {{ minZoom?: number, maxZoom: number, tileGrid?: unknown }}
427
+ */
428
+ function createVectorTileSourceZoomOptions(context) {
429
+ if (context.sourceMode !== 'pmtiles') {
430
+ return { maxZoom: 22 };
431
+ }
432
+
433
+ const range = pmtilesZoomRange(context.manifest);
434
+ return {
435
+ minZoom: range.minZoom,
436
+ maxZoom: range.maxZoom,
437
+ tileGrid: createXYZ({
438
+ minZoom: range.minZoom,
439
+ maxZoom: range.maxZoom,
440
+ tileSize: 512
441
+ })
442
+ };
443
+ }
444
+
445
+ /**
446
+ * @param {{ sourceMode: 'pmtiles' | 'dynamic' }} context
447
+ * @returns {(tileCoord: number[] | null) => string | undefined}
448
+ */
449
+ function createTileUrlFunction(context) {
450
+ return context.sourceMode === 'pmtiles'
451
+ ? createPmtilesTileUrlFunction(context)
452
+ : createDynamicTileUrlFunction(context);
453
+ }
454
+
455
+ /**
456
+ * @param {{
457
+ * instanceId: string,
458
+ * manifest: Record<string, unknown>,
459
+ * manifestBaseUrl: string,
460
+ * orderedLayers: Array<{ id: string, type?: string, style?: string }>,
461
+ * styleDocument: Record<string, unknown>,
462
+ * layerVisibility: Map<string, boolean>
463
+ * }} context
464
+ * @returns {(tileCoord: number[] | null) => string | undefined}
465
+ */
466
+ function createPmtilesTileUrlFunction(context) {
467
+ const { minZoom, maxZoom } = pmtilesZoomRange(context.manifest);
468
+ const packageBbox = normalizeBbox(context.manifest.bbox);
469
+
470
+ return (tileCoord) => {
471
+ if (!tileCoord) {
472
+ return undefined;
473
+ }
474
+
475
+ const [z, x, y] = tileCoord;
476
+ if (!isValidTileCoord(z, x, y) || z < minZoom || z > maxZoom) {
477
+ return undefined;
478
+ }
479
+
480
+ if (packageBbox && !bboxIntersects(tileToBbox(z, x, y), packageBbox)) {
481
+ return undefined;
482
+ }
483
+
484
+ if (activeLayerIdsForZoom(context.orderedLayers, context.styleDocument, context.layerVisibility, z).length === 0) {
485
+ return undefined;
486
+ }
487
+
488
+ return 'pmtiles://' + encodeURIComponent(context.instanceId) + '/' + z + '/' + x + '/' + y;
489
+ };
490
+ }
491
+
492
+ /**
493
+ * @param {Record<string, unknown>} manifest
494
+ * @returns {{ minZoom: number, maxZoom: number }}
495
+ */
496
+ function pmtilesZoomRange(manifest) {
497
+ const tiles = /** @type {{ minZoom?: unknown, maxZoom?: unknown } | undefined} */ (manifest.tiles);
498
+ const minZoom = Number.isInteger(Number(tiles?.minZoom)) ? clamp(Number(tiles?.minZoom), 0, 22) : 0;
499
+ const maxZoom = Number.isInteger(Number(tiles?.maxZoom)) ? clamp(Number(tiles?.maxZoom), minZoom, 22) : 22;
500
+ return { minZoom, maxZoom };
501
+ }
502
+
503
+ /**
504
+ * @param {{
505
+ * apiBaseUrl: string,
506
+ * manifest: Record<string, unknown>,
507
+ * orderedLayers: Array<{ id: string, type?: string, style?: string }>,
508
+ * styleDocument: Record<string, unknown>,
509
+ * layerVisibility: Map<string, boolean>
510
+ * }} context
511
+ * @returns {(tileCoord: number[] | null) => string | undefined}
512
+ */
513
+ function createDynamicTileUrlFunction(context) {
514
+ return (tileCoord) => {
515
+ if (!tileCoord) {
516
+ return undefined;
517
+ }
518
+
519
+ const [z, x, y] = tileCoord;
520
+ if (!isValidTileCoord(z, x, y)) {
521
+ return undefined;
522
+ }
523
+
524
+ // Do not clip dynamic requests to manifest.bbox here. The package bbox is
525
+ // the extraction extent, but stored OSM features can cross that edge. The
526
+ // GeoPackage RTree is the authoritative source for whether a tile has data.
527
+ const layers = activeLayerIdsForZoom(context.orderedLayers, context.styleDocument, context.layerVisibility, z);
528
+ if (layers.length === 0) {
529
+ return undefined;
530
+ }
531
+
532
+ return context.apiBaseUrl + '/tiles/' + z + '/' + x + '/' + y + '.mvt?layers=' +
533
+ encodeURIComponent(layers.join(',')) + '&detail=' + detailForZoom(z);
534
+ };
535
+ }
536
+
537
+ /**
538
+ * @param {{
539
+ * sourceMode: 'pmtiles' | 'dynamic',
540
+ * apiBaseUrl: string,
541
+ * manifest: Record<string, unknown>,
542
+ * orderedLayers: Array<{ id: string, type?: string, style?: string }>,
543
+ * styleDocument: Record<string, unknown>,
544
+ * layerVisibility: Map<string, boolean>
545
+ * }} context
546
+ * @returns {(tileCoord: number[] | null) => string | undefined}
547
+ */
548
+ function createLabelTileUrlFunction(context) {
549
+ return (tileCoord) => {
550
+ if (!tileCoord) {
551
+ return undefined;
552
+ }
553
+
554
+ const [z, x, y] = tileCoord;
555
+ if (!isValidTileCoord(z, x, y)) {
556
+ return undefined;
557
+ }
558
+
559
+ // Labels use the same dynamic tile behavior as geometry: avoid client-side
560
+ // bbox clipping so labels for features crossing the extraction edge can load.
561
+ const labelLayers = activeLabelLayerIdsForZoom(
562
+ context.orderedLayers,
563
+ context.styleDocument,
564
+ context.layerVisibility,
565
+ z
566
+ );
567
+ if (labelLayers.length === 0) {
568
+ return undefined;
569
+ }
570
+
571
+ if (context.sourceMode === 'pmtiles') {
572
+ return 'pmtiles://' + encodeURIComponent(context.instanceId) + '/' + z + '/' + x + '/' + y;
573
+ }
574
+
575
+ return context.apiBaseUrl + '/tiles/' + z + '/' + x + '/' + y + '.mvt?layers=' +
576
+ encodeURIComponent(labelLayers.join(',')) + '&detail=' + detailForZoom(z);
577
+ };
578
+ }
579
+
580
+ /**
581
+ * @param {{
582
+ * sourceMode: 'pmtiles' | 'dynamic',
583
+ * manifest: Record<string, unknown>,
584
+ * manifestBaseUrl: string
585
+ * }} context
586
+ * @returns {(tileCoord: number[], url: string | undefined) => Promise<ArrayBuffer | Uint8Array | null>}
587
+ */
588
+ function createLabelTileDataLoader(context) {
589
+ if (context.sourceMode === 'pmtiles') {
590
+ const archive = createPmtilesArchive(context);
591
+ return async (tileCoord) => {
592
+ const [z, x, y] = tileCoord;
593
+ const result = await archive.getZxy(z, x, y);
594
+ return result?.data ?? null;
595
+ };
596
+ }
597
+
598
+ return async (tileCoord, url) => {
599
+ if (!url) {
600
+ return null;
601
+ }
602
+
603
+ const response = await fetch(url);
604
+ return response.ok ? response.arrayBuffer() : null;
605
+ };
606
+ }
607
+
608
+ /**
609
+ * @param {{ manifest: Record<string, unknown>, manifestBaseUrl: string }} context
610
+ * @returns {PMTiles}
611
+ */
612
+ function createPmtilesArchive(context) {
613
+ const tiles = /** @type {{ url: string }} */ (context.manifest.tiles);
614
+ return new PMTiles(resolveUrl(tiles.url, context.manifestBaseUrl));
615
+ }
616
+
617
+ /**
618
+ * @param {MVT} format
619
+ * @param {PMTiles} archive
620
+ * @returns {(tile: { getTileCoord: () => number[], setLoader: (loader: Function) => void }) => void}
621
+ */
622
+ function createPmtilesTileLoadFunction(format, archive) {
623
+ return (tile) => {
624
+ tile.setLoader((extent, resolution, projection) => {
625
+ const [z, x, y] = tile.getTileCoord();
626
+ archive.getZxy(z, x, y)
627
+ .then((result) => {
628
+ if (!result) {
629
+ tile.setFeatures([]);
630
+ return;
631
+ }
632
+
633
+ const features = format.readFeatures(result.data, {
634
+ extent,
635
+ featureProjection: projection
636
+ });
637
+ tile.setFeatures(features);
638
+ })
639
+ .catch(() => {
640
+ tile.setFeatures([]);
641
+ });
642
+ });
643
+ };
644
+ }
645
+
646
+ /**
647
+ * @param {number} zoom
648
+ * @returns {'overview' | 'normal' | 'full'}
649
+ */
650
+ function detailForZoom(zoom) {
651
+ if (zoom <= 11) {
652
+ return 'overview';
653
+ }
654
+
655
+ if (zoom <= 14) {
656
+ return 'normal';
657
+ }
658
+
659
+ return 'full';
660
+ }
661
+
662
+ /**
663
+ * @param {Array<{ id: string, type?: string, style?: string }>} orderedLayers
664
+ * @param {Record<string, unknown>} styleDocument
665
+ * @param {Map<string, boolean>} layerVisibility
666
+ * @param {number} zoom
667
+ * @returns {string[]}
668
+ */
669
+ function activeLayerIdsForZoom(orderedLayers, styleDocument, layerVisibility, zoom) {
670
+ return orderedLayers
671
+ .filter((layer) => layerVisibility.get(layer.id) && zoomMatchesRule(zoom, getLayerRule(styleDocument, layer)))
672
+ .map((layer) => layer.id);
673
+ }
674
+
675
+ /**
676
+ * @param {number} zoom
677
+ * @param {Record<string, unknown>} rule
678
+ * @returns {boolean}
679
+ */
680
+ function zoomMatchesRule(zoom, rule) {
681
+ if (Number.isFinite(rule.minZoom) && zoom < Number(rule.minZoom)) {
682
+ return false;
683
+ }
684
+
685
+ if (Number.isFinite(rule.maxZoom) && zoom > Number(rule.maxZoom)) {
686
+ return false;
687
+ }
688
+
689
+ return true;
690
+ }
691
+
692
+ /**
693
+ * @param {{
694
+ * orderedLayers: Array<{ id: string, type?: string, style?: string }>,
695
+ * styleDocument: Record<string, unknown>,
696
+ * layerVisibility: Map<string, boolean>,
697
+ * layerOpacity: Map<string, number>
698
+ * }} context
699
+ * @returns {Array<{ filter: unknown[], style: Record<string, unknown> }>}
700
+ */
701
+ function createWebGlStyles(context) {
702
+ const styles = [];
703
+
704
+ for (const layer of context.orderedLayers) {
705
+ if (!context.layerVisibility.get(layer.id)) {
706
+ continue;
707
+ }
708
+
709
+ const rule = getLayerRule(context.styleDocument, layer);
710
+ const filter = createLayerFilter(layer.id, rule);
711
+ const styleParts = layer.id === 'roads'
712
+ ? createRoadStyleRules(filter, rule, context.layerOpacity)
713
+ : layer.id === 'boundaries' || isAipLayer(layer.id)
714
+ ? createGeometryAwareStyleRules(filter, rule, layer.id, context.layerOpacity)
715
+ : createLayerStyleRules(filter, rule, layer.type || 'line', layer.id, context.layerOpacity);
716
+ for (const style of styleParts) {
717
+ styles.push({
718
+ filter: style.filter,
719
+ style: style.style
720
+ });
721
+ }
722
+ }
723
+
724
+ return styles;
725
+ }
726
+
727
+ /**
728
+ * @param {unknown[]} filter
729
+ * @param {Record<string, unknown>} rule
730
+ * @param {Map<string, number>} layerOpacity
731
+ * @returns {Array<{ filter: unknown[], style: Record<string, unknown> }>}
732
+ */
733
+ function createRoadStyleRules(filter, rule, layerOpacity) {
734
+ return [
735
+ ...createRoadSemanticUnderlayRules(filter, rule, layerOpacity),
736
+ ...createLayerStyleRules(filter, rule, 'line', 'roads', layerOpacity),
737
+ ...createRoadSemanticOverlayRules(filter, rule, layerOpacity)
738
+ ];
739
+ }
740
+
741
+ /**
742
+ * @param {unknown[]} filter
743
+ * @param {Record<string, unknown>} rule
744
+ * @param {Map<string, number>} layerOpacity
745
+ * @returns {Array<{ filter: unknown[], style: Record<string, unknown> }>}
746
+ */
747
+ function createRoadSemanticUnderlayRules(filter, rule, layerOpacity) {
748
+ const bridge = semanticRule(rule, 'bridge');
749
+ if (!semanticEnabled(bridge)) {
750
+ return [];
751
+ }
752
+
753
+ const casingWidth = widthStyleValue(rule, 'casingWidth', Number(rule.casing?.width) || 5, 'roads');
754
+ const shadowStyle = {
755
+ 'stroke-color': colorWithOpacity(String(bridge.shadow || '#000000'), layerOpacityModifier('roads', layerOpacity, 0.9)),
756
+ 'stroke-width': ['+', casingWidth, 1.4],
757
+ 'stroke-line-cap': 'round',
758
+ 'stroke-line-join': 'round'
759
+ };
760
+
761
+ return [{
762
+ filter: bridgeFilter(filter),
763
+ style: shadowStyle
764
+ }];
765
+ }
766
+
767
+ /**
768
+ * @param {unknown[]} filter
769
+ * @param {Record<string, unknown>} rule
770
+ * @param {Map<string, number>} layerOpacity
771
+ * @returns {Array<{ filter: unknown[], style: Record<string, unknown> }>}
772
+ */
773
+ function createRoadSemanticOverlayRules(filter, rule, layerOpacity) {
774
+ const styles = [];
775
+ const bodyWidth = widthStyleValue(rule, 'strokeWidth', Number(rule.strokeWidth ?? 1), 'roads');
776
+ const bodyColor = String(rule.stroke || '#ffffff');
777
+ const casingColor = String(rule.casing?.color || rule.stroke || '#ffffff');
778
+
779
+ const bridge = semanticRule(rule, 'bridge');
780
+ if (semanticEnabled(bridge)) {
781
+ styles.push({
782
+ filter: bridgeFilter(filter),
783
+ style: {
784
+ 'stroke-color': colorWithOpacity(String(bridge.casing || casingColor), layerOpacityModifier('roads', layerOpacity, 0.85)),
785
+ 'stroke-width': ['+', bodyWidth, 0.8],
786
+ 'stroke-line-cap': 'round',
787
+ 'stroke-line-join': 'round'
788
+ }
789
+ });
790
+ styles.push({
791
+ filter: bridgeFilter(filter),
792
+ style: {
793
+ 'stroke-color': colorWithOpacity(String(bridge.body || bodyColor), layerOpacityModifier('roads', layerOpacity, 0.95)),
794
+ 'stroke-width': ['*', bodyWidth, 0.85],
795
+ 'stroke-line-cap': 'round',
796
+ 'stroke-line-join': 'round'
797
+ }
798
+ });
799
+ }
800
+
801
+ const tunnel = semanticRule(rule, 'tunnel');
802
+ if (semanticEnabled(tunnel)) {
803
+ styles.push({
804
+ filter: tunnelFilter(filter),
805
+ style: dashedStrokeStyle(
806
+ String(tunnel.color || casingColor),
807
+ Number(tunnel.opacity ?? 0.72),
808
+ ['*', bodyWidth, 0.9],
809
+ Array.isArray(tunnel.dash) ? tunnel.dash : [10, 7],
810
+ layerOpacity
811
+ )
812
+ });
813
+ }
814
+
815
+ const construction = semanticRule(rule, 'construction');
816
+ if (semanticEnabled(construction)) {
817
+ styles.push({
818
+ filter: constructionFilter(filter),
819
+ style: dashedStrokeStyle(
820
+ String(construction.color || '#d9a520'),
821
+ Number(construction.opacity ?? 0.95),
822
+ ['*', bodyWidth, 0.85],
823
+ Array.isArray(construction.dash) ? construction.dash : [7, 5],
824
+ layerOpacity
825
+ )
826
+ });
827
+ }
828
+
829
+ const restrictedAccess = semanticRule(rule, 'restrictedAccess');
830
+ if (semanticEnabled(restrictedAccess)) {
831
+ styles.push({
832
+ filter: minZoomFilter(restrictedAccessFilter(filter), Number(restrictedAccess.minZoom ?? 15)),
833
+ style: dashedStrokeStyle(
834
+ String(restrictedAccess.color || casingColor),
835
+ Number(restrictedAccess.opacity ?? 0.75),
836
+ ['*', bodyWidth, 0.7],
837
+ Array.isArray(restrictedAccess.dash) ? restrictedAccess.dash : [5, 5],
838
+ layerOpacity
839
+ )
840
+ });
841
+ }
842
+
843
+ const oneway = semanticRule(rule, 'oneway');
844
+ if (semanticEnabled(oneway)) {
845
+ styles.push({
846
+ filter: minZoomFilter(onewayFilter(filter), Number(oneway.minZoom ?? 15)),
847
+ style: dashedStrokeStyle(
848
+ String(oneway.color || bodyColor),
849
+ Number(oneway.opacity ?? 0.85),
850
+ ['*', bodyWidth, 0.22],
851
+ Array.isArray(oneway.dash) ? oneway.dash : [1.2, 9],
852
+ layerOpacity
853
+ )
854
+ });
855
+ }
856
+
857
+ return styles;
858
+ }
859
+
860
+ /**
861
+ * @param {Record<string, unknown>} rule
862
+ * @param {string} name
863
+ * @returns {Record<string, unknown>}
864
+ */
865
+ function semanticRule(rule, name) {
866
+ const semantics = rule.semantics && typeof rule.semantics === 'object'
867
+ ? /** @type {Record<string, Record<string, unknown>>} */ (rule.semantics)
868
+ : {};
869
+ return semantics[name] || {};
870
+ }
871
+
872
+ /**
873
+ * @param {Record<string, unknown>} rule
874
+ * @returns {boolean}
875
+ */
876
+ function semanticEnabled(rule) {
877
+ return rule.enabled !== false;
878
+ }
879
+
880
+ /**
881
+ * @param {string} color
882
+ * @param {number} opacity
883
+ * @param {unknown} width
884
+ * @param {unknown[]} dash
885
+ * @param {Map<string, number>} layerOpacity
886
+ * @returns {Record<string, unknown>}
887
+ */
888
+ function dashedStrokeStyle(color, opacity, width, dash, layerOpacity) {
889
+ return {
890
+ 'stroke-color': colorWithOpacity(color, layerOpacityModifier('roads', layerOpacity, opacity)),
891
+ 'stroke-width': width,
892
+ 'stroke-line-cap': 'butt',
893
+ 'stroke-line-join': 'round',
894
+ 'stroke-line-dash': dash
895
+ };
896
+ }
897
+
898
+ /**
899
+ * @param {unknown[]} filter
900
+ * @returns {unknown[]}
901
+ */
902
+ function bridgeFilter(filter) {
903
+ return ['all', filter, ['any', propertyEquals('bridge', 'yes'), propertyIn('layer', ['1', '2', '3', '4', '5'])]];
904
+ }
905
+
906
+ /**
907
+ * @param {unknown[]} filter
908
+ * @returns {unknown[]}
909
+ */
910
+ function tunnelFilter(filter) {
911
+ return ['all', filter, propertyEquals('tunnel', 'yes')];
912
+ }
913
+
914
+ /**
915
+ * @param {unknown[]} filter
916
+ * @returns {unknown[]}
917
+ */
918
+ function constructionFilter(filter) {
919
+ return ['all', filter, ['any', propertyEquals('highway', 'construction'), propertyNotEmpty('construction')]];
920
+ }
921
+
922
+ /**
923
+ * @param {unknown[]} filter
924
+ * @returns {unknown[]}
925
+ */
926
+ function restrictedAccessFilter(filter) {
927
+ return ['all', filter, ['any', propertyNotEmpty('service'), propertyIn('access', ['private', 'no', 'destination', 'customers', 'delivery'])]];
928
+ }
929
+
930
+ /**
931
+ * @param {unknown[]} filter
932
+ * @returns {unknown[]}
933
+ */
934
+ function onewayFilter(filter) {
935
+ return ['all', filter, propertyIn('oneway', ['yes', 'true', '1'])];
936
+ }
937
+
938
+ /**
939
+ * @param {unknown[]} filter
940
+ * @param {number} zoom
941
+ * @returns {unknown[]}
942
+ */
943
+ function minZoomFilter(filter, zoom) {
944
+ return Number.isFinite(zoom) ? ['all', filter, ['>=', ['zoom'], zoom]] : filter;
945
+ }
946
+
947
+ /**
948
+ * @param {string} property
949
+ * @param {string} value
950
+ * @returns {unknown[]}
951
+ */
952
+ function propertyEquals(property, value) {
953
+ return ['==', ['get', property, 'string'], value];
954
+ }
955
+
956
+ /**
957
+ * @param {string} property
958
+ * @param {string[]} values
959
+ * @returns {unknown[]}
960
+ */
961
+ function propertyIn(property, values) {
962
+ if (values.length === 0) {
963
+ return false;
964
+ }
965
+
966
+ if (values.length === 1) {
967
+ return propertyEquals(property, values[0]);
968
+ }
969
+
970
+ return ['any', ...values.map((value) => propertyEquals(property, value))];
971
+ }
972
+
973
+ /**
974
+ * @param {string} property
975
+ * @returns {unknown[]}
976
+ */
977
+ function propertyNotEmpty(property) {
978
+ return ['!=', ['get', property, 'string'], ''];
979
+ }
980
+
981
+ /**
982
+ * @param {unknown[]} filter
983
+ * @param {Record<string, unknown>} rule
984
+ * @param {string} layerType
985
+ * @param {string} layerId
986
+ * @param {Map<string, number>} layerOpacity
987
+ * @returns {Array<{ filter: unknown[], style: Record<string, unknown> }>}
988
+ */
989
+ function createLayerStyleRules(filter, rule, layerType, layerId, layerOpacity) {
990
+ const visibleFilter = createPropertyVisibilityFilter(filter, rule);
991
+ return createLayerStyleParts(rule, layerType, layerId, layerOpacity).map((style) => ({
992
+ filter: visibleFilter,
993
+ style
994
+ }));
995
+ }
996
+
997
+ /**
998
+ * @param {unknown[]} filter
999
+ * @param {Record<string, unknown>} rule
1000
+ * @returns {unknown[]}
1001
+ */
1002
+ function createPropertyVisibilityFilter(filter, rule) {
1003
+ const byProperty = rule.byProperty;
1004
+ if (!byProperty || typeof byProperty !== 'object') {
1005
+ return filter;
1006
+ }
1007
+
1008
+ const hidden = [];
1009
+ for (const [propertyName, values] of Object.entries(byProperty)) {
1010
+ if (!values || typeof values !== 'object') {
1011
+ continue;
1012
+ }
1013
+
1014
+ for (const [value, overrides] of Object.entries(values)) {
1015
+ if (styleOverrideVisible(overrides) === false) {
1016
+ hidden.push(['!=', ['get', propertyName, 'string'], value]);
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ return hidden.length > 0 ? ['all', filter, ...hidden] : filter;
1022
+ }
1023
+
1024
+ /**
1025
+ * @param {unknown[]} filter
1026
+ * @param {Record<string, unknown>} rule
1027
+ * @param {string} layerId
1028
+ * @param {Map<string, number>} layerOpacity
1029
+ * @returns {Array<{ filter: unknown[], style: Record<string, unknown> }>}
1030
+ */
1031
+ function createGeometryAwareStyleRules(filter, rule, layerId, layerOpacity) {
1032
+ const polygonFilter = ['all', filter, ['==', ['geometry-type'], 'Polygon']];
1033
+ const lineFilter = ['all', filter, ['==', ['geometry-type'], 'LineString']];
1034
+ const pointFilter = ['all', filter, ['==', ['geometry-type'], 'Point']];
1035
+ const lineRule = {
1036
+ ...rule,
1037
+ fill: null,
1038
+ fillOpacity: 0
1039
+ };
1040
+
1041
+ const rules = [
1042
+ ...createLayerStyleRules(polygonFilter, rule, 'polygon', layerId, layerOpacity),
1043
+ ...createLayerStyleRules(lineFilter, lineRule, 'line', layerId, layerOpacity)
1044
+ ];
1045
+
1046
+ if (isAipLayer(layerId)) {
1047
+ rules.push(...createLayerStyleRules(pointFilter, rule, 'point', layerId, layerOpacity));
1048
+ }
1049
+
1050
+ return rules;
1051
+ }
1052
+
1053
+ /**
1054
+ * @param {Record<string, unknown>} rule
1055
+ * @param {string} layerType
1056
+ * @param {string} layerId
1057
+ * @param {Map<string, number>} layerOpacity
1058
+ * @returns {Array<Record<string, unknown>>}
1059
+ */
1060
+ function createLayerStyleParts(rule, layerType, layerId, layerOpacity) {
1061
+ const styles = [];
1062
+
1063
+ if (rule.glow?.enabled && rule.stroke) {
1064
+ const glowWidth = widthStyleValue(rule, 'glowWidth', Number(rule.glow.width) || 4, layerId);
1065
+ const glowStyle = layerType === 'point'
1066
+ ? {
1067
+ 'circle-radius': glowWidth,
1068
+ 'circle-fill-color': colorStyleValue(rule, 'glowColor', 'glowOpacity', String(rule.glow.color || rule.fill || rule.stroke), Number(rule.glow.opacity ?? 0.2), layerId, layerOpacity)
1069
+ }
1070
+ : {
1071
+ 'stroke-color': colorStyleValue(rule, 'glowColor', 'glowOpacity', String(rule.glow.color || rule.stroke), Number(rule.glow.opacity ?? 0.2), layerId, layerOpacity),
1072
+ 'stroke-width': glowWidth
1073
+ };
1074
+ applyStrokeLineOptions(glowStyle, rule);
1075
+ styles.push(glowStyle);
1076
+ }
1077
+
1078
+ if (rule.casing?.enabled && rule.stroke && layerType !== 'point') {
1079
+ const casingStyle = {
1080
+ 'stroke-color': colorStyleValue(
1081
+ rule,
1082
+ 'casingColor',
1083
+ 'casingOpacity',
1084
+ String(rule.casing.color || rule.stroke),
1085
+ Number(rule.casing.opacity ?? 0.2),
1086
+ layerId,
1087
+ layerOpacity
1088
+ ),
1089
+ 'stroke-width': widthStyleValue(rule, 'casingWidth', Number(rule.casing.width) || Math.max(1, Number(rule.strokeWidth ?? 1) + 1), layerId)
1090
+ };
1091
+ applyStrokeLineOptions(casingStyle, rule);
1092
+ styles.push(casingStyle);
1093
+ }
1094
+
1095
+ const baseStyle = {};
1096
+ if (rule.fill) {
1097
+ baseStyle['fill-color'] = colorStyleValue(rule, 'fill', 'fillOpacity', String(rule.fill), Number(rule.fillOpacity ?? 1), layerId, layerOpacity);
1098
+ }
1099
+ if (rule.stroke) {
1100
+ baseStyle['stroke-color'] = colorStyleValue(rule, 'stroke', 'strokeOpacity', String(rule.stroke), Number(rule.strokeOpacity ?? 1), layerId, layerOpacity);
1101
+ baseStyle['stroke-width'] = widthStyleValue(rule, 'strokeWidth', Number(rule.strokeWidth ?? 1), layerId);
1102
+ applyStrokeLineOptions(baseStyle, rule);
1103
+ }
1104
+ if (layerType === 'point') {
1105
+ baseStyle['circle-radius'] = 4;
1106
+ baseStyle['circle-fill-color'] = colorStyleValue(
1107
+ rule,
1108
+ 'fill',
1109
+ 'fillOpacity',
1110
+ String(rule.fill || rule.stroke || '#ffffff'),
1111
+ Number(rule.fillOpacity ?? rule.strokeOpacity ?? 1),
1112
+ layerId,
1113
+ layerOpacity
1114
+ );
1115
+ if (rule.stroke) {
1116
+ baseStyle['circle-stroke-color'] = colorStyleValue(rule, 'stroke', 'strokeOpacity', String(rule.stroke), Number(rule.strokeOpacity ?? 1), layerId, layerOpacity);
1117
+ baseStyle['circle-stroke-width'] = numberStyleValue(rule, 'strokeWidth', Math.max(0.5, Number(rule.strokeWidth ?? 1)));
1118
+ }
1119
+ }
1120
+
1121
+ styles.push(baseStyle);
1122
+
1123
+ if (rule.centerLine && rule.stroke && layerType !== 'point') {
1124
+ const centerWidth = rule.centerLine.enabled ? Number(rule.centerLine.width ?? 0.5) : 0;
1125
+ const centerOpacity = rule.centerLine.enabled ? Number(rule.centerLine.opacity ?? 0.5) : 0;
1126
+ const centerStyle = {
1127
+ 'stroke-color': colorStyleValue(
1128
+ rule,
1129
+ 'centerLineColor',
1130
+ 'centerLineOpacity',
1131
+ String(rule.centerLine.color || rule.stroke),
1132
+ centerOpacity,
1133
+ layerId,
1134
+ layerOpacity
1135
+ ),
1136
+ 'stroke-width': widthStyleValue(rule, 'centerLineWidth', centerWidth, layerId)
1137
+ };
1138
+ applyStrokeLineOptions(centerStyle, rule);
1139
+ styles.push(centerStyle);
1140
+ }
1141
+
1142
+ return styles;
1143
+ }
1144
+
1145
+ /**
1146
+ * @param {Record<string, unknown>} style
1147
+ * @param {Record<string, unknown>} rule
1148
+ */
1149
+ function applyStrokeLineOptions(style, rule) {
1150
+ if (rule.lineCap) {
1151
+ style['stroke-line-cap'] = rule.lineCap;
1152
+ }
1153
+
1154
+ if (rule.lineJoin) {
1155
+ style['stroke-line-join'] = rule.lineJoin;
1156
+ }
1157
+
1158
+ if (Number.isFinite(Number(rule.miterLimit))) {
1159
+ style['stroke-miter-limit'] = Number(rule.miterLimit);
1160
+ }
1161
+ }
1162
+
1163
+ /**
1164
+ * @param {string} layerId
1165
+ * @param {Record<string, unknown>} rule
1166
+ * @returns {unknown[]}
1167
+ */
1168
+ function createLayerFilter(layerId, rule) {
1169
+ const filters = [
1170
+ ['==', ['get', 'layer'], layerId]
1171
+ ];
1172
+
1173
+ if (Number.isFinite(rule.minZoom)) {
1174
+ filters.push(['>=', ['zoom'], Number(rule.minZoom)]);
1175
+ }
1176
+
1177
+ if (Number.isFinite(rule.maxZoom)) {
1178
+ filters.push(['<=', ['zoom'], Number(rule.maxZoom)]);
1179
+ }
1180
+
1181
+ if (layerId === 'pois') {
1182
+ filters.push(createPoiSelectionFilter(rule));
1183
+ }
1184
+
1185
+ return filters.length === 1 ? filters[0] : ['all', ...filters];
1186
+ }
1187
+
1188
+ /**
1189
+ * @param {Record<string, unknown>} rule
1190
+ * @returns {unknown[]}
1191
+ */
1192
+ function createPoiSelectionFilter(rule) {
1193
+ return ['!=', ['get', 'poi_category', 'string'], 'consumer'];
1194
+ }
1195
+
1196
+ /**
1197
+ * @param {Record<string, unknown>} styleDocument
1198
+ * @param {{ id: string, style?: string }} layer
1199
+ * @returns {Record<string, unknown>}
1200
+ */
1201
+ function getLayerRule(styleDocument, layer) {
1202
+ const layers = styleDocument.layers && typeof styleDocument.layers === 'object'
1203
+ ? /** @type {Record<string, Record<string, unknown>>} */ (styleDocument.layers)
1204
+ : {};
1205
+ const id = layer.style || layer.id;
1206
+ return normalizeStyleRule(layers[id] || layers[layerAlias(id)] || {});
1207
+ }
1208
+
1209
+ /**
1210
+ * @param {string} layerId
1211
+ * @returns {boolean}
1212
+ */
1213
+ function isAipLayer(layerId) {
1214
+ return layerId === 'aip' || layerId === 'aviation';
1215
+ }
1216
+
1217
+ /**
1218
+ * @param {string} layerId
1219
+ * @returns {string}
1220
+ */
1221
+ function layerAlias(layerId) {
1222
+ if (layerId === 'aip') return 'aviation';
1223
+ if (layerId === 'aviation') return 'aip';
1224
+ return layerId;
1225
+ }
1226
+
1227
+ /**
1228
+ * Normalize the public declarative style schema to the internal style keys.
1229
+ *
1230
+ * @param {Record<string, unknown>} rule
1231
+ * @returns {Record<string, unknown>}
1232
+ */
1233
+ function normalizeStyleRule(rule) {
1234
+ const normalized = { ...rule };
1235
+ const visibility = rule.visibility && typeof rule.visibility === 'object'
1236
+ ? /** @type {Record<string, unknown>} */ (rule.visibility)
1237
+ : null;
1238
+ const body = rule.body && typeof rule.body === 'object'
1239
+ ? /** @type {Record<string, unknown>} */ (rule.body)
1240
+ : null;
1241
+ const center = rule.center && typeof rule.center === 'object'
1242
+ ? /** @type {Record<string, unknown>} */ (rule.center)
1243
+ : null;
1244
+
1245
+ if (visibility) {
1246
+ normalized.visible = visibility.visible ?? normalized.visible;
1247
+ normalized.minZoom = visibility.minZoom ?? normalized.minZoom;
1248
+ normalized.maxZoom = visibility.maxZoom ?? normalized.maxZoom;
1249
+ }
1250
+
1251
+ if (body) {
1252
+ normalized.stroke = body.color ?? normalized.stroke;
1253
+ normalized.strokeWidth = body.width ?? normalized.strokeWidth;
1254
+ normalized.strokeOpacity = body.opacity ?? normalized.strokeOpacity;
1255
+ normalized.lineCap = body.lineCap ?? normalized.lineCap;
1256
+ normalized.lineJoin = body.lineJoin ?? normalized.lineJoin;
1257
+ normalized.widthScale = body.widthScale ?? normalized.widthScale;
1258
+ }
1259
+
1260
+ if (center) {
1261
+ const centerLine = {
1262
+ ...(normalized.centerLine && typeof normalized.centerLine === 'object' ? normalized.centerLine : {})
1263
+ };
1264
+ for (const key of ['enabled', 'color', 'width', 'opacity']) {
1265
+ if (key in center) {
1266
+ centerLine[key] = center[key];
1267
+ }
1268
+ }
1269
+ normalized.centerLine = centerLine;
1270
+ }
1271
+
1272
+ const semantics = normalized.semantics && typeof normalized.semantics === 'object'
1273
+ ? { .../** @type {Record<string, unknown>} */ (normalized.semantics) }
1274
+ : {};
1275
+ for (const key of ['bridge', 'tunnel', 'oneway', 'construction', 'restrictedAccess']) {
1276
+ if (rule[key] && typeof rule[key] === 'object' && !semantics[key]) {
1277
+ semantics[key] = rule[key];
1278
+ }
1279
+ }
1280
+ if (Object.keys(semantics).length > 0) {
1281
+ normalized.semantics = semantics;
1282
+ }
1283
+
1284
+ return normalized;
1285
+ }
1286
+
1287
+ /**
1288
+ * @param {Record<string, unknown>} rule
1289
+ * @param {string} property
1290
+ * @param {number} fallback
1291
+ * @returns {unknown}
1292
+ */
1293
+ function numberStyleValue(rule, property, fallback) {
1294
+ const byProperty = rule.byProperty;
1295
+ if (!byProperty || typeof byProperty !== 'object') {
1296
+ return fallback;
1297
+ }
1298
+
1299
+ for (const [propertyName, values] of Object.entries(byProperty)) {
1300
+ if (!values || typeof values !== 'object') {
1301
+ continue;
1302
+ }
1303
+
1304
+ const match = [['get', propertyName, 'string']];
1305
+ for (const [value, overrides] of Object.entries(values)) {
1306
+ const override = styleOverrideValue(overrides, property);
1307
+ if (Number.isFinite(Number(override))) {
1308
+ match.push(value, Number(override));
1309
+ }
1310
+ }
1311
+
1312
+ if (match.length > 1) {
1313
+ match.push(fallback);
1314
+ return ['match', ...match];
1315
+ }
1316
+ }
1317
+
1318
+ return fallback;
1319
+ }
1320
+
1321
+ /**
1322
+ * @param {Record<string, unknown>} rule
1323
+ * @param {string} property
1324
+ * @param {number} fallback
1325
+ * @param {string} layerId
1326
+ * @returns {unknown}
1327
+ */
1328
+ function widthStyleValue(rule, property, fallback, layerId) {
1329
+ if (layerId !== 'roads') {
1330
+ return numberStyleValue(rule, property, fallback);
1331
+ }
1332
+
1333
+ const byProperty = rule.byProperty;
1334
+ const defaultScale = zoomWidthScale(rule);
1335
+ const defaultWidth = defaultScale ? ['*', fallback, defaultScale] : fallback;
1336
+
1337
+ if (!byProperty || typeof byProperty !== 'object') {
1338
+ return defaultWidth;
1339
+ }
1340
+
1341
+ for (const [propertyName, values] of Object.entries(byProperty)) {
1342
+ if (!values || typeof values !== 'object') {
1343
+ continue;
1344
+ }
1345
+
1346
+ const match = [['get', propertyName, 'string']];
1347
+ for (const [value, overrides] of Object.entries(values)) {
1348
+ if (!overrides || typeof overrides !== 'object') {
1349
+ continue;
1350
+ }
1351
+
1352
+ const override = styleOverrideValue(overrides, property);
1353
+ const width = Number.isFinite(Number(override)) ? Number(override) : fallback;
1354
+ const scale = zoomWidthScale(overrides) ?? defaultScale;
1355
+ match.push(value, scale ? ['*', width, scale] : width);
1356
+ }
1357
+
1358
+ if (match.length > 1) {
1359
+ match.push(defaultWidth);
1360
+ return ['match', ...match];
1361
+ }
1362
+ }
1363
+
1364
+ return defaultWidth;
1365
+ }
1366
+
1367
+ /**
1368
+ * @param {Record<string, unknown>} rule
1369
+ * @returns {unknown[] | null}
1370
+ */
1371
+ function zoomWidthScale(rule) {
1372
+ const body = rule.body && typeof rule.body === 'object'
1373
+ ? /** @type {Record<string, unknown>} */ (rule.body)
1374
+ : null;
1375
+ const widthScale = rule.widthScale ?? body?.widthScale;
1376
+ const stops = widthScale && typeof widthScale === 'object' ? widthScale.stops : null;
1377
+ if (!Array.isArray(stops)) {
1378
+ return null;
1379
+ }
1380
+
1381
+ const expression = ['interpolate', ['linear'], ['zoom']];
1382
+ for (const stop of stops) {
1383
+ if (!Array.isArray(stop) || stop.length < 2) {
1384
+ continue;
1385
+ }
1386
+
1387
+ const zoom = Number(stop[0]);
1388
+ const scale = Number(stop[1]);
1389
+ if (Number.isFinite(zoom) && Number.isFinite(scale) && scale >= 0) {
1390
+ expression.push(zoom, scale);
1391
+ }
1392
+ }
1393
+
1394
+ return expression.length > 4 ? expression : null;
1395
+ }
1396
+
1397
+ /**
1398
+ * @param {Record<string, unknown>} rule
1399
+ * @param {string} property
1400
+ * @param {number} fallback
1401
+ * @param {string} layerId
1402
+ * @param {Map<string, number>} layerOpacity
1403
+ * @returns {unknown}
1404
+ */
1405
+ function opacityStyleValue(rule, property, fallback, layerId, layerOpacity) {
1406
+ return layerOpacityModifier(layerId, layerOpacity, roadOpacityModifiers(layerId, numberStyleValue(rule, property, fallback)));
1407
+ }
1408
+
1409
+ /**
1410
+ * @param {Record<string, unknown>} rule
1411
+ * @param {string} colorProperty
1412
+ * @param {string} opacityProperty
1413
+ * @param {string} fallbackColor
1414
+ * @param {number} fallbackOpacity
1415
+ * @param {string} layerId
1416
+ * @param {Map<string, number>} layerOpacity
1417
+ * @returns {unknown}
1418
+ */
1419
+ function colorStyleValue(rule, colorProperty, opacityProperty, fallbackColor, fallbackOpacity, layerId, layerOpacity) {
1420
+ const byProperty = rule.byProperty;
1421
+ if (!byProperty || typeof byProperty !== 'object') {
1422
+ return colorWithOpacity(fallbackColor, opacityStyleValue(rule, opacityProperty, fallbackOpacity, layerId, layerOpacity));
1423
+ }
1424
+
1425
+ for (const [propertyName, values] of Object.entries(byProperty)) {
1426
+ if (!values || typeof values !== 'object') {
1427
+ continue;
1428
+ }
1429
+
1430
+ const match = [['get', propertyName, 'string']];
1431
+ for (const [value, overrides] of Object.entries(values)) {
1432
+ if (!overrides || typeof overrides !== 'object') {
1433
+ continue;
1434
+ }
1435
+
1436
+ const color = styleOverrideValue(overrides, colorProperty);
1437
+ const opacity = styleOverrideValue(overrides, opacityProperty);
1438
+ if (color || Number.isFinite(Number(opacity))) {
1439
+ match.push(
1440
+ value,
1441
+ colorWithOpacity(
1442
+ String(color || fallbackColor),
1443
+ layerOpacityModifier(
1444
+ layerId,
1445
+ layerOpacity,
1446
+ roadOpacityModifiers(layerId, Number.isFinite(Number(opacity)) ? Number(opacity) : fallbackOpacity)
1447
+ )
1448
+ )
1449
+ );
1450
+ }
1451
+ }
1452
+
1453
+ if (match.length > 1) {
1454
+ match.push(colorWithOpacity(fallbackColor, opacityStyleValue(rule, opacityProperty, fallbackOpacity, layerId, layerOpacity)));
1455
+ return ['match', ...match];
1456
+ }
1457
+ }
1458
+
1459
+ return colorWithOpacity(fallbackColor, opacityStyleValue(rule, opacityProperty, fallbackOpacity, layerId, layerOpacity));
1460
+ }
1461
+
1462
+ /**
1463
+ * @param {unknown} overrides
1464
+ * @param {string} property
1465
+ * @returns {unknown}
1466
+ */
1467
+ function styleOverrideValue(overrides, property) {
1468
+ if (!overrides || typeof overrides !== 'object') {
1469
+ return undefined;
1470
+ }
1471
+
1472
+ const body = overrides.body && typeof overrides.body === 'object' ? overrides.body : null;
1473
+ const center = overrides.center && typeof overrides.center === 'object' ? overrides.center : null;
1474
+
1475
+ if (property === 'stroke') {
1476
+ return body?.color ?? overrides.bodyColor ?? overrides.stroke;
1477
+ }
1478
+
1479
+ if (property === 'strokeWidth') {
1480
+ return body?.width ?? overrides.bodyWidth ?? overrides.strokeWidth;
1481
+ }
1482
+
1483
+ if (property === 'strokeOpacity') {
1484
+ return body?.opacity ?? overrides.bodyOpacity ?? overrides.strokeOpacity;
1485
+ }
1486
+
1487
+ if (property === 'glowWidth') {
1488
+ return overrides.glow?.width ?? overrides.glowWidth;
1489
+ }
1490
+
1491
+ if (property === 'glowOpacity') {
1492
+ return overrides.glow?.opacity ?? overrides.glowOpacity;
1493
+ }
1494
+
1495
+ if (property === 'glowColor') {
1496
+ return overrides.glow?.color ?? overrides.glowColor;
1497
+ }
1498
+
1499
+ if (property === 'casingWidth') {
1500
+ return overrides.casing?.width ?? overrides.casingWidth;
1501
+ }
1502
+
1503
+ if (property === 'casingOpacity') {
1504
+ return overrides.casing?.opacity ?? overrides.casingOpacity;
1505
+ }
1506
+
1507
+ if (property === 'casingColor') {
1508
+ return overrides.casing?.color ?? overrides.casingColor;
1509
+ }
1510
+
1511
+ if (property === 'centerLineWidth') {
1512
+ return center?.width ?? overrides.centerLine?.width ?? overrides.centerLineWidth;
1513
+ }
1514
+
1515
+ if (property === 'centerLineOpacity') {
1516
+ return center?.opacity ?? overrides.centerLine?.opacity ?? overrides.centerLineOpacity;
1517
+ }
1518
+
1519
+ if (property === 'centerLineColor') {
1520
+ return center?.color ?? overrides.centerLine?.color ?? overrides.centerLineColor;
1521
+ }
1522
+
1523
+ return overrides[property];
1524
+ }
1525
+
1526
+ /**
1527
+ * @param {unknown} overrides
1528
+ * @returns {boolean | undefined}
1529
+ */
1530
+ function styleOverrideVisible(overrides) {
1531
+ if (!overrides || typeof overrides !== 'object') {
1532
+ return undefined;
1533
+ }
1534
+
1535
+ const visibility = overrides.visibility && typeof overrides.visibility === 'object'
1536
+ ? overrides.visibility
1537
+ : null;
1538
+ return visibility?.visible ?? overrides.visible;
1539
+ }
1540
+
1541
+ /**
1542
+ * @param {string} layerId
1543
+ * @param {unknown} opacity
1544
+ * @returns {unknown}
1545
+ */
1546
+ function roadOpacityModifiers(layerId, opacity) {
1547
+ return opacity;
1548
+ }
1549
+
1550
+ /**
1551
+ * @param {string} layerId
1552
+ * @param {Map<string, number>} layerOpacity
1553
+ * @param {unknown} opacity
1554
+ * @returns {unknown}
1555
+ */
1556
+ function layerOpacityModifier(layerId, layerOpacity, opacity) {
1557
+ const value = layerOpacity.get(layerId) ?? 1;
1558
+ return value === 1 ? opacity : ['*', opacity, value];
1559
+ }
1560
+
1561
+ /**
1562
+ * @param {string} hex
1563
+ * @param {unknown} opacity
1564
+ * @returns {unknown}
1565
+ */
1566
+ function colorWithOpacity(hex, opacity) {
1567
+ if (!hex || !hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) {
1568
+ return hex || '#ffffff';
1569
+ }
1570
+
1571
+ const expanded = hex.length === 4
1572
+ ? '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]
1573
+ : hex;
1574
+ const r = parseInt(expanded.slice(1, 3), 16);
1575
+ const g = parseInt(expanded.slice(3, 5), 16);
1576
+ const b = parseInt(expanded.slice(5, 7), 16);
1577
+ return ['color', r, g, b, opacity];
1578
+ }
1579
+
1580
+ /**
1581
+ * @param {string} path
1582
+ * @param {string} baseUrl
1583
+ * @returns {string}
1584
+ */
1585
+ function resolveUrl(path, baseUrl) {
1586
+ return new URL(path, baseUrl).href;
1587
+ }
1588
+
1589
+ /**
1590
+ * @returns {string}
1591
+ */
1592
+ function documentBaseUrl() {
1593
+ return globalThis.document?.baseURI || globalThis.location?.href || 'http://localhost/';
1594
+ }
1595
+
1596
+ /**
1597
+ * @param {string} url
1598
+ * @returns {Promise<Record<string, unknown>>}
1599
+ */
1600
+ async function fetchJson(url) {
1601
+ const response = await fetch(url);
1602
+ const body = await response.json();
1603
+ if (!response.ok) {
1604
+ throw new Error(body.error || response.statusText);
1605
+ }
1606
+ return body;
1607
+ }
1608
+
1609
+ /**
1610
+ * @param {unknown} bbox
1611
+ * @returns {[number, number, number, number] | null}
1612
+ */
1613
+ function normalizeBbox(bbox) {
1614
+ if (!Array.isArray(bbox) || bbox.length !== 4) {
1615
+ return null;
1616
+ }
1617
+
1618
+ const values = bbox.map(Number);
1619
+ if (values.some((value) => !Number.isFinite(value)) || values[0] >= values[2] || values[1] >= values[3]) {
1620
+ return null;
1621
+ }
1622
+
1623
+ return /** @type {[number, number, number, number]} */ (values);
1624
+ }
1625
+
1626
+ /**
1627
+ * @param {number} z
1628
+ * @param {number} x
1629
+ * @param {number} y
1630
+ * @returns {boolean}
1631
+ */
1632
+ function isValidTileCoord(z, x, y) {
1633
+ if (!Number.isInteger(z) || z < 0 || z > 22) {
1634
+ return false;
1635
+ }
1636
+
1637
+ const maxIndex = 2 ** z;
1638
+ return Number.isInteger(x) && Number.isInteger(y) && x >= 0 && y >= 0 && x < maxIndex && y < maxIndex;
1639
+ }
1640
+
1641
+ /**
1642
+ * @param {number} z
1643
+ * @param {number} x
1644
+ * @param {number} y
1645
+ * @returns {[number, number, number, number]}
1646
+ */
1647
+ function tileToBbox(z, x, y) {
1648
+ const n = 2 ** z;
1649
+ const minLon = (x / n) * 360 - 180;
1650
+ const maxLon = ((x + 1) / n) * 360 - 180;
1651
+ const maxLat = tileYToLat(y, n);
1652
+ const minLat = tileYToLat(y + 1, n);
1653
+ return [minLon, minLat, maxLon, maxLat];
1654
+ }
1655
+
1656
+ /**
1657
+ * @param {number} y
1658
+ * @param {number} n
1659
+ * @returns {number}
1660
+ */
1661
+ function tileYToLat(y, n) {
1662
+ const mercator = Math.PI * (1 - (2 * y) / n);
1663
+ return (Math.atan(Math.sinh(mercator)) * 180) / Math.PI;
1664
+ }
1665
+
1666
+ /**
1667
+ * @param {[number, number, number, number]} a
1668
+ * @param {[number, number, number, number]} b
1669
+ * @returns {boolean}
1670
+ */
1671
+ function bboxIntersects(a, b) {
1672
+ return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
1673
+ }
1674
+
1675
+ /**
1676
+ * @param {number} value
1677
+ * @param {number} min
1678
+ * @param {number} max
1679
+ * @returns {number}
1680
+ */
1681
+ function clamp(value, min, max) {
1682
+ if (!Number.isFinite(value)) {
1683
+ return min;
1684
+ }
1685
+
1686
+ return Math.max(min, Math.min(max, value));
1687
+ }
1688
+
1689
+ function patchWebGlVectorTileRenderer() {
1690
+ const prototype = WebGLVectorTileLayerRenderer?.prototype;
1691
+ if (!prototype || prototype.__mapZeroTileMaskPatch) {
1692
+ return;
1693
+ }
1694
+
1695
+ const renderTileMask = prototype.renderTileMask;
1696
+ prototype.renderTileMask = function (tileRepresentation, tileZ, extent, depth) {
1697
+ const buffers = tileRepresentation?.buffers;
1698
+ if (!tileRepresentation?.ready || !buffers?.invertVerticesTransform) {
1699
+ return;
1700
+ }
1701
+
1702
+ return renderTileMask.call(this, tileRepresentation, tileZ, extent, depth);
1703
+ };
1704
+ prototype.__mapZeroTileMaskPatch = true;
1705
+ }