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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/docs/api.md +66 -0
- package/docs/architecture.md +87 -0
- package/docs/cartography.md +77 -0
- package/docs/cesium.md +107 -0
- package/docs/openlayers.md +98 -0
- package/docs/styles.md +103 -0
- package/package.json +51 -0
- package/packages/cesium/package.json +13 -0
- package/packages/cesium/src/index.js +405 -0
- package/packages/ol/package.json +14 -0
- package/packages/ol/src/index.js +1705 -0
- package/packages/ol/src/labels.js +977 -0
- package/src/3dtiles/b3dm.js +38 -0
- package/src/3dtiles/clipper-surfaces.js +317 -0
- package/src/3dtiles/export.js +768 -0
- package/src/3dtiles/extrude.js +301 -0
- package/src/3dtiles/flat.js +531 -0
- package/src/3dtiles/glb.js +178 -0
- package/src/3dtiles/gpkg-buildings.js +240 -0
- package/src/3dtiles/gpkg-features.js +157 -0
- package/src/3dtiles/tileset.js +75 -0
- package/src/build.js +134 -0
- package/src/cli.js +656 -0
- package/src/export-pmtiles.js +962 -0
- package/src/geometry-read.js +50 -0
- package/src/gpkg-read.js +460 -0
- package/src/gpkg.js +567 -0
- package/src/html.js +593 -0
- package/src/layers.js +357 -0
- package/src/manifest.js +29 -0
- package/src/mvt.js +2593 -0
- package/src/ol.js +5 -0
- package/src/osm.js +2110 -0
- package/src/pmtiles-worker.js +70 -0
- package/src/pmtiles.js +260 -0
- package/src/server.js +720 -0
- package/src/style-command.js +78 -0
- package/src/style-filters.js +76 -0
- package/src/style-presets.js +93 -0
- package/src/style-themes.js +235 -0
- package/src/style.js +13 -0
- package/src/tile-cache.js +59 -0
- package/src/utils.js +222 -0
- package/styles/presets/light.json +4655 -0
- package/styles/presets/monochrome.json +4655 -0
- package/styles/presets/neon-dark-3d.json +90 -0
- package/styles/presets/neon-dark.json +4690 -0
- package/styles/presets/tactical.json +4690 -0
- 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
|
+
}
|