@map-zero/cesium 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/package.json +19 -3
- package/src/imagery.js +14 -143
- package/src/index.js +2 -2
- package/src/imagery-worker.js +0 -604
package/README.md
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@map-zero/cesium",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cesium integration helper for map-zero 3D Tiles packages.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/mploscos/map-zero#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/mploscos/map-zero.git",
|
|
11
|
+
"directory": "packages/cesium"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/mploscos/map-zero/issues"
|
|
15
|
+
},
|
|
6
16
|
"main": "./src/index.js",
|
|
7
17
|
"exports": {
|
|
8
|
-
".": "./src/index.js"
|
|
9
|
-
|
|
18
|
+
".": "./src/index.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@map-zero/raster": "0.3.1"
|
|
10
26
|
},
|
|
11
27
|
"peerDependencies": {
|
|
12
28
|
"cesium": "^1.141.0",
|
package/src/imagery.js
CHANGED
|
@@ -4,10 +4,18 @@ import {
|
|
|
4
4
|
WebMercatorTilingScheme
|
|
5
5
|
} from 'cesium';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { WEB_MERCATOR_MAX_LAT } from '../../raster/src/shared/geo.js';
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_CONTEXT_LAYERS,
|
|
10
|
+
layerAlias,
|
|
11
|
+
normalizeContextLayers,
|
|
12
|
+
sourceLayerFor
|
|
13
|
+
} from '../../raster/src/shared/layers.js';
|
|
14
|
+
import { pmtilesInfo } from '../../raster/src/shared/manifest.js';
|
|
15
|
+
import { clampInteger } from '../../raster/src/shared/math.js';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_WORKER_URL = new URL('@map-zero/raster/imagery-worker.js', import.meta.url);
|
|
8
18
|
const TILE_SIZE = 512;
|
|
9
|
-
const WEB_MERCATOR_MAX = 20037508.342789244;
|
|
10
|
-
const WEB_MERCATOR_MAX_LAT = 85.05112878;
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* Cesium ImageryProvider that rasterizes map-zero PMTiles/MVT tiles in a
|
|
@@ -25,7 +33,7 @@ export class MapZeroCesiumImageryProvider {
|
|
|
25
33
|
* minimumLevel?: number,
|
|
26
34
|
* maximumLevel?: number,
|
|
27
35
|
* overzoomLevels?: number,
|
|
28
|
-
* edgeGuardPixels?: number
|
|
36
|
+
* edgeGuardPixels?: number,
|
|
29
37
|
* workerUrl?: string | URL
|
|
30
38
|
* }} options
|
|
31
39
|
*/
|
|
@@ -63,7 +71,7 @@ export class MapZeroCesiumImageryProvider {
|
|
|
63
71
|
this.nextRequestId = 1;
|
|
64
72
|
this.metrics = createImageryMetrics();
|
|
65
73
|
this.worker = new Worker(
|
|
66
|
-
options.workerUrl ??
|
|
74
|
+
options.workerUrl ?? DEFAULT_WORKER_URL,
|
|
67
75
|
{ type: 'module' }
|
|
68
76
|
);
|
|
69
77
|
this.worker.addEventListener('message', (event) => this.#handleWorkerMessage(event.data));
|
|
@@ -219,7 +227,7 @@ function mergeWorkerMetrics(target, patch) {
|
|
|
219
227
|
|
|
220
228
|
/**
|
|
221
229
|
* @param {Record<string, unknown>} manifest
|
|
222
|
-
* @returns {{ type?: string, source?: string, layers?: string[], overzoomLevels?: number, edgeGuardPixels?: number } | null}
|
|
230
|
+
* @returns {{ type?: string, source?: string, layers?: string[], backend?: string, overzoomLevels?: number, edgeGuardPixels?: number } | null}
|
|
223
231
|
*/
|
|
224
232
|
export function contextOverlayConfig(manifest) {
|
|
225
233
|
const pmtiles = pmtilesInfo(manifest);
|
|
@@ -247,95 +255,6 @@ function contextLayerIds(manifest) {
|
|
|
247
255
|
return layers.length > 0 ? layers : DEFAULT_CONTEXT_LAYERS;
|
|
248
256
|
}
|
|
249
257
|
|
|
250
|
-
function getLayerRule(styleDocument, layer) {
|
|
251
|
-
const layers = styleDocument.layers && typeof styleDocument.layers === 'object' ? styleDocument.layers : {};
|
|
252
|
-
const id = layer.style || layer.id;
|
|
253
|
-
return normalizeStyleRule(layers[id] || layers[layerAlias(id)] || {});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function normalizeStyleRule(rule) {
|
|
257
|
-
const normalized = { ...rule };
|
|
258
|
-
const visibility = objectRule(rule.visibility);
|
|
259
|
-
const body = objectRule(rule.body);
|
|
260
|
-
const center = objectRule(rule.center);
|
|
261
|
-
if (visibility) {
|
|
262
|
-
normalized.visible = visibility.visible ?? normalized.visible;
|
|
263
|
-
normalized.minZoom = visibility.minZoom ?? normalized.minZoom;
|
|
264
|
-
normalized.maxZoom = visibility.maxZoom ?? normalized.maxZoom;
|
|
265
|
-
}
|
|
266
|
-
if (body) {
|
|
267
|
-
normalized.stroke = body.color ?? normalized.stroke;
|
|
268
|
-
normalized.strokeWidth = body.width ?? normalized.strokeWidth;
|
|
269
|
-
normalized.strokeOpacity = body.opacity ?? normalized.strokeOpacity;
|
|
270
|
-
normalized.lineCap = body.lineCap ?? normalized.lineCap;
|
|
271
|
-
normalized.lineJoin = body.lineJoin ?? normalized.lineJoin;
|
|
272
|
-
normalized.widthScale = body.widthScale ?? normalized.widthScale;
|
|
273
|
-
}
|
|
274
|
-
if (center) {
|
|
275
|
-
normalized.fill = center.color ?? normalized.fill;
|
|
276
|
-
normalized.fillOpacity = center.opacity ?? normalized.fillOpacity;
|
|
277
|
-
}
|
|
278
|
-
if (!normalized.lineCap) normalized.lineCap = 'round';
|
|
279
|
-
if (!normalized.lineJoin) normalized.lineJoin = 'round';
|
|
280
|
-
return normalized;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function mergeFeatureRule(rule, feature) {
|
|
284
|
-
let merged = { ...rule };
|
|
285
|
-
const byProperty = objectRule(rule.byProperty);
|
|
286
|
-
if (!byProperty) return merged;
|
|
287
|
-
for (const [property, values] of Object.entries(byProperty)) {
|
|
288
|
-
const value = String(feature.get(property) ?? '');
|
|
289
|
-
const override = objectRule(values?.[value]);
|
|
290
|
-
if (override) {
|
|
291
|
-
merged = normalizeStyleRule({ ...merged, ...override });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return merged;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function zoomMatchesRule(zoom, rule) {
|
|
298
|
-
if (Number.isFinite(rule.minZoom) && zoom < Number(rule.minZoom)) return false;
|
|
299
|
-
if (Number.isFinite(rule.maxZoom) && zoom > Number(rule.maxZoom)) return false;
|
|
300
|
-
return rule.visible !== false;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function styleWidth(value, fallback, layerId, zoom) {
|
|
304
|
-
const width = Number(Array.isArray(value) ? fallback : value);
|
|
305
|
-
const base = Number.isFinite(width) && width > 0 ? width : fallback;
|
|
306
|
-
const scale = layerId === 'roads' ? Math.max(0.65, Math.min(1.35, 0.72 + zoom * 0.035)) : 1;
|
|
307
|
-
return Math.max(0, base * scale);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function geometryTypeKind(type) {
|
|
311
|
-
if (type === 'Point' || type === 'MultiPoint') return 'point';
|
|
312
|
-
if (type === 'Polygon' || type === 'MultiPolygon') return 'polygon';
|
|
313
|
-
return 'line';
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function tileMercatorExtent(x, y, z) {
|
|
317
|
-
const tiles = 2 ** z;
|
|
318
|
-
const span = (WEB_MERCATOR_MAX * 2) / tiles;
|
|
319
|
-
const minX = -WEB_MERCATOR_MAX + x * span;
|
|
320
|
-
const maxX = minX + span;
|
|
321
|
-
const maxY = WEB_MERCATOR_MAX - y * span;
|
|
322
|
-
const minY = maxY - span;
|
|
323
|
-
return [minX, minY, maxX, maxY];
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function sourceTileForRequest(x, y, z, maxZoom) {
|
|
327
|
-
const sourceZ = Math.min(z, maxZoom);
|
|
328
|
-
if (sourceZ === z) {
|
|
329
|
-
return { x, y, z };
|
|
330
|
-
}
|
|
331
|
-
const shift = z - sourceZ;
|
|
332
|
-
return {
|
|
333
|
-
x: Math.floor(x / 2 ** shift),
|
|
334
|
-
y: Math.floor(y / 2 ** shift),
|
|
335
|
-
z: sourceZ
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
258
|
function rectangleFromManifestBbox(manifest, bbox) {
|
|
340
259
|
const padded = expandBboxByTileMargin(bbox, pmtilesInfo(manifest).minZoom);
|
|
341
260
|
return rectangleFromBbox(padded);
|
|
@@ -381,54 +300,6 @@ function imageToCanvas(image, width, height) {
|
|
|
381
300
|
return canvas;
|
|
382
301
|
}
|
|
383
302
|
|
|
384
|
-
function clearCanvasBorder(ctx, width, height, pixels) {
|
|
385
|
-
if (!(pixels > 0)) return;
|
|
386
|
-
ctx.clearRect(0, 0, width, pixels);
|
|
387
|
-
ctx.clearRect(0, height - pixels, width, pixels);
|
|
388
|
-
ctx.clearRect(0, 0, pixels, height);
|
|
389
|
-
ctx.clearRect(width - pixels, 0, pixels, height);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function pmtilesInfo(manifest) {
|
|
393
|
-
const tiles = manifest.tiles && typeof manifest.tiles === 'object' ? manifest.tiles : {};
|
|
394
|
-
if (tiles.format === 'pmtiles' || tiles.type === 'mvt') {
|
|
395
|
-
return tiles;
|
|
396
|
-
}
|
|
397
|
-
const vector = manifest.vectorTiles && typeof manifest.vectorTiles === 'object' ? manifest.vectorTiles : {};
|
|
398
|
-
const pmtiles = vector.pmtiles && typeof vector.pmtiles === 'object' ? vector.pmtiles : {};
|
|
399
|
-
return pmtiles;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function normalizeContextLayers(layers) {
|
|
403
|
-
const values = Array.isArray(layers) && layers.length > 0 ? layers : DEFAULT_CONTEXT_LAYERS;
|
|
404
|
-
return values.map((layer) => sourceLayerFor(String(layer)));
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function isLayerVisible(layerVisibility, layer) {
|
|
408
|
-
const direct = layerVisibility.get(layer);
|
|
409
|
-
if (direct != null) return direct;
|
|
410
|
-
const source = layerVisibility.get(sourceLayerFor(layer));
|
|
411
|
-
if (source != null) return source;
|
|
412
|
-
const alias = layerVisibility.get(layerAlias(layer));
|
|
413
|
-
return alias === true;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function sourceLayerFor(layer) {
|
|
417
|
-
return layer === 'aviation' ? 'aip' : layer;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function layerAlias(layer) {
|
|
421
|
-
if (layer === 'aip') return 'aviation';
|
|
422
|
-
if (layer === 'aviation') return 'aip';
|
|
423
|
-
return layer;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
303
|
function resolveWorkerBaseUrl(url) {
|
|
427
304
|
return new URL(url, globalThis.location?.href ?? 'http://localhost/').toString();
|
|
428
305
|
}
|
|
429
|
-
|
|
430
|
-
function clampInteger(value, min, max) {
|
|
431
|
-
const number = Math.trunc(Number(value));
|
|
432
|
-
if (!Number.isFinite(number)) return min;
|
|
433
|
-
return Math.max(min, Math.min(max, number));
|
|
434
|
-
}
|
package/src/index.js
CHANGED
|
@@ -163,7 +163,7 @@ export async function createMapZeroCesiumTilesets(options) {
|
|
|
163
163
|
* contextOpacity?: number,
|
|
164
164
|
* contextOverzoomLevels?: number,
|
|
165
165
|
* contextEdgeGuardPixels?: number,
|
|
166
|
-
*
|
|
166
|
+
* workerUrl?: string | URL,
|
|
167
167
|
* buildings3d?: boolean,
|
|
168
168
|
* tilesetMaximumScreenSpaceError?: number,
|
|
169
169
|
* tilesetCacheBytes?: number,
|
|
@@ -205,7 +205,7 @@ export async function addMapZeroToCesium(viewer, options) {
|
|
|
205
205
|
layers: contextOverlayConfig(result.manifest)?.layers,
|
|
206
206
|
overzoomLevels: options.contextOverzoomLevels,
|
|
207
207
|
edgeGuardPixels: options.contextEdgeGuardPixels,
|
|
208
|
-
workerUrl: options.
|
|
208
|
+
workerUrl: options.workerUrl
|
|
209
209
|
})
|
|
210
210
|
: undefined;
|
|
211
211
|
const imageryLayer = imageryProvider
|
package/src/imagery-worker.js
DELETED
|
@@ -1,604 +0,0 @@
|
|
|
1
|
-
import Feature from 'ol/Feature.js';
|
|
2
|
-
import MVT from 'ol/format/MVT.js';
|
|
3
|
-
import { PMTiles } from 'pmtiles';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_CONTEXT_LAYERS = ['roads', 'railways', 'water', 'landuse', 'boundaries', 'aviation', 'pois'];
|
|
6
|
-
const WEB_MERCATOR_MAX = 20037508.342789244;
|
|
7
|
-
const LABEL_SOURCE_LAYERS = new Set(['roads', 'aip', 'aviation', 'pois']);
|
|
8
|
-
const MAJOR_ROADS = new Set(['motorway', 'motorway_link', 'trunk', 'trunk_link', 'primary', 'primary_link']);
|
|
9
|
-
const SECONDARY_ROADS = new Set(['secondary', 'secondary_link']);
|
|
10
|
-
const LOCAL_ROADS = new Set(['residential', 'living_street', 'unclassified']);
|
|
11
|
-
const DISABLED_ROADS = new Set(['service', 'track', 'path', 'footway', 'cycleway', 'steps', 'corridor', 'platform']);
|
|
12
|
-
const GENERIC_LABEL_VALUES = new Set(['yes', 'no', 'true', 'false', 'unknown', 'none', 'station', 'airport', 'aerodrome']);
|
|
13
|
-
|
|
14
|
-
let state = null;
|
|
15
|
-
|
|
16
|
-
self.addEventListener('message', (event) => {
|
|
17
|
-
handleMessage(event.data).catch((error) => {
|
|
18
|
-
if (event.data?.id != null) {
|
|
19
|
-
self.postMessage({ type: 'tile', id: event.data.id, error: error.message, metrics: state?.metrics });
|
|
20
|
-
} else {
|
|
21
|
-
self.postMessage({ type: 'error', error: error.message, metrics: state?.metrics });
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
async function handleMessage(message) {
|
|
27
|
-
if (message?.type === 'init') {
|
|
28
|
-
state = createState(message.options);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (!state) {
|
|
33
|
-
throw new Error('map-zero imagery worker was not initialized');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (message?.type === 'visibility') {
|
|
37
|
-
state.layerVisibility.set(sourceLayerFor(message.layerId), Boolean(message.visible));
|
|
38
|
-
state.layerVisibility.set(layerAlias(message.layerId), Boolean(message.visible));
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (message?.type === 'render') {
|
|
43
|
-
const image = await renderTile(message.x, message.y, message.z);
|
|
44
|
-
self.postMessage({ type: 'tile', id: message.id, image, metrics: state.metrics }, [image]);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function createState(options) {
|
|
49
|
-
const manifest = options.manifest;
|
|
50
|
-
const styleDocument = options.styleDocument ?? {};
|
|
51
|
-
const layerIds = normalizeContextLayers(options.layers);
|
|
52
|
-
return {
|
|
53
|
-
manifest,
|
|
54
|
-
manifestUrl: options.manifestUrl,
|
|
55
|
-
styleDocument,
|
|
56
|
-
tileSize: Number(options.tileSize ?? 512),
|
|
57
|
-
pixelRatio: clampNumber(options.pixelRatio ?? 1, 1, 2),
|
|
58
|
-
sourceMaximumLevel: Number(options.sourceMaximumLevel ?? pmtilesInfo(manifest).maxZoom ?? 18),
|
|
59
|
-
edgeGuardPixels: clampInteger(options.edgeGuardPixels ?? 0, 0, 8),
|
|
60
|
-
layerIds,
|
|
61
|
-
layerVisibility: new Map(layerIds.map((layerId) => [layerId, true])),
|
|
62
|
-
orderedLayers: orderManifestLayers(manifest, styleDocument)
|
|
63
|
-
.filter((layer) => layerIds.includes(layer.id) || layerIds.includes(layerAlias(layer.id))),
|
|
64
|
-
format: new MVT({ featureClass: Feature }),
|
|
65
|
-
archive: new PMTiles(resolveRelativeUrl(String(options.source ?? pmtilesInfo(manifest).url ?? 'tiles.pmtiles'), options.manifestUrl)),
|
|
66
|
-
sourceTileCache: new Map(),
|
|
67
|
-
metrics: createMetrics()
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function renderTile(x, y, z) {
|
|
72
|
-
return renderTileBitmap(x, y, z);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function renderTileBitmap(x, y, z) {
|
|
76
|
-
const started = performance.now();
|
|
77
|
-
state.metrics.requested++;
|
|
78
|
-
addMetricCount(state.metrics.requestLevels, z);
|
|
79
|
-
|
|
80
|
-
const sourceTile = sourceTileForRequest(x, y, z, state.sourceMaximumLevel);
|
|
81
|
-
let canvas;
|
|
82
|
-
if (sourceTile.z !== z) {
|
|
83
|
-
state.metrics.overzoomed++;
|
|
84
|
-
addMetricCount(state.metrics.sourceLevels, sourceTile.z);
|
|
85
|
-
canvas = await renderOverzoomedTile(sourceTile, x, y, z);
|
|
86
|
-
} else {
|
|
87
|
-
canvas = await renderSourceTile(sourceTile, tileMercatorExtent(x, y, z), z, state.tileSize);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
recordMetricTime(state.metrics, 'renderMs', performance.now() - started);
|
|
91
|
-
return canvas.transferToImageBitmap();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function renderOverzoomedTile(sourceTile, x, y, z) {
|
|
95
|
-
const { canvas, ctx } = createRenderCanvas(state.tileSize, state.pixelRatio);
|
|
96
|
-
if (!ctx) return canvas;
|
|
97
|
-
|
|
98
|
-
const sourceData = await readSourceTile(sourceTile);
|
|
99
|
-
if (!sourceData) return canvas;
|
|
100
|
-
drawSourceData(ctx, sourceData, tileMercatorExtent(x, y, z), z, state.tileSize);
|
|
101
|
-
clearCanvasBorder(ctx, state.tileSize, state.tileSize, state.edgeGuardPixels);
|
|
102
|
-
return canvas;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function renderSourceTile(sourceTile, renderExtent, z, size) {
|
|
106
|
-
const { canvas, ctx } = createRenderCanvas(size, state.pixelRatio);
|
|
107
|
-
if (!ctx) return canvas;
|
|
108
|
-
|
|
109
|
-
const sourceData = await readSourceTile(sourceTile);
|
|
110
|
-
if (!sourceData) return canvas;
|
|
111
|
-
drawSourceData(ctx, sourceData, renderExtent, z, size);
|
|
112
|
-
clearCanvasBorder(ctx, size, size, state.edgeGuardPixels);
|
|
113
|
-
return canvas;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function createRenderCanvas(size, pixelRatio) {
|
|
117
|
-
const ratio = clampNumber(pixelRatio, 1, 2);
|
|
118
|
-
const canvas = new OffscreenCanvas(Math.round(size * ratio), Math.round(size * ratio));
|
|
119
|
-
const ctx = canvas.getContext('2d');
|
|
120
|
-
if (ctx && ratio !== 1) ctx.scale(ratio, ratio);
|
|
121
|
-
return { canvas, ctx };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function readSourceTile(tile) {
|
|
125
|
-
const key = `${tile.z}/${tile.x}/${tile.y}`;
|
|
126
|
-
const cached = state.sourceTileCache.get(key);
|
|
127
|
-
if (cached) return cached;
|
|
128
|
-
|
|
129
|
-
const promise = (async () => {
|
|
130
|
-
const started = performance.now();
|
|
131
|
-
const result = await state.archive.getZxy(tile.z, tile.x, tile.y);
|
|
132
|
-
if (!result) return null;
|
|
133
|
-
addMetricCount(state.metrics.sourceLevels, tile.z);
|
|
134
|
-
const features = state.format.readFeatures(result.data, {
|
|
135
|
-
extent: tileMercatorExtent(tile.x, tile.y, tile.z),
|
|
136
|
-
featureProjection: 'EPSG:3857'
|
|
137
|
-
});
|
|
138
|
-
state.metrics.decoded++;
|
|
139
|
-
state.metrics.features += features.length;
|
|
140
|
-
recordMetricTime(state.metrics, 'decodeMs', performance.now() - started);
|
|
141
|
-
return {
|
|
142
|
-
features,
|
|
143
|
-
byLayer: groupFeaturesByLayer(features)
|
|
144
|
-
};
|
|
145
|
-
})();
|
|
146
|
-
state.sourceTileCache.set(key, promise);
|
|
147
|
-
return promise;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function drawSourceData(ctx, sourceData, renderExtent, z, size) {
|
|
151
|
-
const { features, byLayer } = sourceData;
|
|
152
|
-
for (const layer of state.orderedLayers) {
|
|
153
|
-
if (!isLayerVisible(state.layerVisibility, layer.id) || !zoomMatchesRule(z, getLayerRule(state.styleDocument, layer))) continue;
|
|
154
|
-
const layerFeatures = byLayer.get(sourceLayerFor(layer.id)) ?? byLayer.get(layer.id) ?? [];
|
|
155
|
-
drawLayer(ctx, layerFeatures, layer, getLayerRule(state.styleDocument, layer), renderExtent, size, z);
|
|
156
|
-
}
|
|
157
|
-
drawLabels(ctx, features, state.styleDocument, renderExtent, size, z, state.layerVisibility);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function drawLayer(ctx, features, layer, rule, extent, size, zoom) {
|
|
161
|
-
for (const feature of features) {
|
|
162
|
-
const geometry = feature.getGeometry?.();
|
|
163
|
-
if (!geometry) continue;
|
|
164
|
-
const featureRule = mergeFeatureRule(rule, feature);
|
|
165
|
-
if (featureRule.visible === false) continue;
|
|
166
|
-
const type = geometry.getType();
|
|
167
|
-
if (layer.id === 'boundaries' && (type === 'Polygon' || type === 'MultiPolygon')) {
|
|
168
|
-
drawGeometry(ctx, geometry, { ...featureRule, stroke: null, strokeOpacity: 0, glow: { enabled: false }, casing: { enabled: false } }, extent, size, layer.id, zoom);
|
|
169
|
-
} else if (isAipLayer(layer.id) && (type === 'LineString' || type === 'MultiLineString')) {
|
|
170
|
-
drawGeometry(ctx, geometry, { ...featureRule, fill: null, fillOpacity: 0 }, extent, size, layer.id, zoom);
|
|
171
|
-
} else {
|
|
172
|
-
drawGeometry(ctx, geometry, featureRule, extent, size, layer.id, zoom);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function drawGeometry(ctx, geometry, rule, extent, size, layerId, zoom) {
|
|
178
|
-
const type = geometry.getType();
|
|
179
|
-
const draw = (style) => {
|
|
180
|
-
if (type === 'Polygon') return drawPolygons(ctx, [geometry.getCoordinates()], style, extent, size);
|
|
181
|
-
if (type === 'MultiPolygon') return drawPolygons(ctx, geometry.getCoordinates(), style, extent, size);
|
|
182
|
-
if (type === 'LineString') return drawLines(ctx, [geometry.getCoordinates()], style, extent, size);
|
|
183
|
-
if (type === 'MultiLineString') return drawLines(ctx, geometry.getCoordinates(), style, extent, size);
|
|
184
|
-
if (type === 'Point') return drawPoints(ctx, [geometry.getCoordinates()], style, extent, size);
|
|
185
|
-
if (type === 'MultiPoint') return drawPoints(ctx, geometry.getCoordinates(), style, extent, size);
|
|
186
|
-
return undefined;
|
|
187
|
-
};
|
|
188
|
-
for (const style of canvasStyleParts(rule, geometryTypeKind(type), layerId, zoom)) draw(style);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function canvasStyleParts(rule, kind, layerId, zoom) {
|
|
192
|
-
const parts = [];
|
|
193
|
-
if (rule.glow?.enabled && rule.stroke) parts.push({ stroke: colorWithOpacity(String(rule.glow.color || rule.stroke), opacity(rule.glow.opacity, 0.2)), width: styleWidth(rule.glow.width ?? rule.glowWidth, 4, layerId, zoom), lineCap: rule.lineCap, lineJoin: rule.lineJoin });
|
|
194
|
-
if (rule.casing?.enabled && rule.stroke && kind !== 'point') parts.push({ stroke: colorWithOpacity(String(rule.casing.color || rule.stroke), opacity(rule.casing.opacity, 0.35)), width: styleWidth(rule.casing.width ?? rule.casingWidth, Number(rule.strokeWidth ?? 1) + 1, layerId, zoom), lineCap: rule.lineCap, lineJoin: rule.lineJoin });
|
|
195
|
-
const base = {};
|
|
196
|
-
if (rule.fill && kind !== 'line') base.fill = colorWithOpacity(String(rule.fill), opacity(rule.fillOpacity, 1));
|
|
197
|
-
if (rule.stroke && kind !== 'polygon-fill-only') {
|
|
198
|
-
base.stroke = colorWithOpacity(String(rule.stroke), opacity(rule.strokeOpacity, 1));
|
|
199
|
-
base.width = styleWidth(rule.strokeWidth, 1, layerId, zoom);
|
|
200
|
-
base.lineCap = rule.lineCap;
|
|
201
|
-
base.lineJoin = rule.lineJoin;
|
|
202
|
-
}
|
|
203
|
-
if (kind === 'point') {
|
|
204
|
-
base.radius = styleWidth(rule.radius ?? rule.circleRadius, 4, layerId, zoom);
|
|
205
|
-
base.fill = colorWithOpacity(String(rule.fill || rule.stroke || '#ffffff'), opacity(rule.fillOpacity ?? rule.strokeOpacity, 1));
|
|
206
|
-
if (rule.stroke) {
|
|
207
|
-
base.stroke = colorWithOpacity(String(rule.stroke), opacity(rule.strokeOpacity, 1));
|
|
208
|
-
base.width = styleWidth(rule.strokeWidth, 1, layerId, zoom);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
parts.push(base);
|
|
212
|
-
if (rule.centerLine?.enabled && rule.stroke && kind !== 'point') parts.push({ stroke: colorWithOpacity(String(rule.centerLine.color || rule.stroke), opacity(rule.centerLine.opacity, 0.5)), width: styleWidth(rule.centerLine.width, 0.5, layerId, zoom), lineCap: rule.lineCap, lineJoin: rule.lineJoin });
|
|
213
|
-
return parts;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function drawPolygons(ctx, polygons, style, extent, size) {
|
|
217
|
-
if (!style.fill && !style.stroke) return;
|
|
218
|
-
ctx.save();
|
|
219
|
-
clipCanvas(ctx, size);
|
|
220
|
-
ctx.beginPath();
|
|
221
|
-
for (const polygon of polygons) for (const ring of polygon) traceLine(ctx, ring, extent, size, true);
|
|
222
|
-
if (style.fill) {
|
|
223
|
-
ctx.fillStyle = style.fill;
|
|
224
|
-
ctx.fill('evenodd');
|
|
225
|
-
}
|
|
226
|
-
if (style.stroke && style.width > 0) {
|
|
227
|
-
applyStroke(ctx, style);
|
|
228
|
-
ctx.stroke();
|
|
229
|
-
}
|
|
230
|
-
ctx.restore();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function drawLines(ctx, lines, style, extent, size) {
|
|
234
|
-
if (!style.stroke || !(style.width > 0)) return;
|
|
235
|
-
ctx.save();
|
|
236
|
-
clipCanvas(ctx, size);
|
|
237
|
-
applyStroke(ctx, style);
|
|
238
|
-
ctx.beginPath();
|
|
239
|
-
for (const line of lines) traceLine(ctx, line, extent, size);
|
|
240
|
-
ctx.stroke();
|
|
241
|
-
ctx.restore();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function drawPoints(ctx, points, style, extent, size) {
|
|
245
|
-
const radius = Number(style.radius ?? 4);
|
|
246
|
-
if (!style.fill && !style.stroke) return;
|
|
247
|
-
ctx.save();
|
|
248
|
-
clipCanvas(ctx, size);
|
|
249
|
-
for (const point of points) {
|
|
250
|
-
const [px, py] = projectPoint(point, extent, size);
|
|
251
|
-
if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
|
|
252
|
-
ctx.beginPath();
|
|
253
|
-
ctx.arc(px, py, radius, 0, Math.PI * 2);
|
|
254
|
-
if (style.fill) {
|
|
255
|
-
ctx.fillStyle = style.fill;
|
|
256
|
-
ctx.fill();
|
|
257
|
-
}
|
|
258
|
-
if (style.stroke && style.width > 0) {
|
|
259
|
-
applyStroke(ctx, style);
|
|
260
|
-
ctx.stroke();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
ctx.restore();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function drawLabels(ctx, features, styleDocument, extent, size, zoom, layerVisibility) {
|
|
267
|
-
if (styleDocument.labels?.enabled === false) return;
|
|
268
|
-
const labels = [];
|
|
269
|
-
for (const feature of features) {
|
|
270
|
-
const layer = sourceLayerFor(String(feature.get('layer') ?? ''));
|
|
271
|
-
if (!LABEL_SOURCE_LAYERS.has(layer) || layerVisibility.get(layer) === false) continue;
|
|
272
|
-
const label = labelForFeature(feature, layer, zoom);
|
|
273
|
-
const point = label ? labelPoint(feature.getGeometry?.()) : null;
|
|
274
|
-
if (label && point) labels.push({ ...label, point });
|
|
275
|
-
}
|
|
276
|
-
labels.sort((a, b) => a.priority - b.priority);
|
|
277
|
-
const occupied = [];
|
|
278
|
-
for (const label of labels.slice(-160)) {
|
|
279
|
-
const [x, y] = projectPoint(label.point, extent, size);
|
|
280
|
-
const fontSize = zoom >= 17 ? 12 : zoom >= 15 ? 11 : 10;
|
|
281
|
-
ctx.save();
|
|
282
|
-
ctx.font = `600 ${fontSize}px sans-serif`;
|
|
283
|
-
const width = ctx.measureText(label.text).width;
|
|
284
|
-
const box = [x - width / 2 - 3, y - fontSize / 2 - 3, x + width / 2 + 3, y + fontSize / 2 + 3];
|
|
285
|
-
if (occupied.some((other) => boxesOverlap(box, other))) {
|
|
286
|
-
ctx.restore();
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
occupied.push(box);
|
|
290
|
-
ctx.textAlign = 'center';
|
|
291
|
-
ctx.textBaseline = 'middle';
|
|
292
|
-
ctx.lineJoin = 'round';
|
|
293
|
-
ctx.strokeStyle = 'rgba(0,16,20,0.92)';
|
|
294
|
-
ctx.lineWidth = 3;
|
|
295
|
-
ctx.strokeText(label.text, x, y);
|
|
296
|
-
ctx.fillStyle = 'rgba(217,251,255,0.9)';
|
|
297
|
-
ctx.fillText(label.text, x, y);
|
|
298
|
-
ctx.restore();
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function labelForFeature(feature, layer, zoom) {
|
|
303
|
-
if (layer === 'roads') {
|
|
304
|
-
const highway = String(feature.get('highway') ?? '');
|
|
305
|
-
if (DISABLED_ROADS.has(highway)) return null;
|
|
306
|
-
const ref = cleanText(feature.get('ref'));
|
|
307
|
-
const name = cleanText(feature.get('name'));
|
|
308
|
-
if (MAJOR_ROADS.has(highway) && zoom >= 12 && isMeaningfulLabel(ref)) return { text: ref, priority: 820 };
|
|
309
|
-
if (SECONDARY_ROADS.has(highway) && zoom >= 16 && isMeaningfulLabel(ref)) return { text: ref, priority: 700 };
|
|
310
|
-
if (LOCAL_ROADS.has(highway) && zoom >= 17 && isMeaningfulLabel(name)) return { text: name, priority: 400 };
|
|
311
|
-
} else if (isAipLayer(layer)) {
|
|
312
|
-
const aeroway = String(feature.get('aeroway') ?? '');
|
|
313
|
-
const ref = cleanText(feature.get('ref'));
|
|
314
|
-
const name = cleanText(feature.get('name'));
|
|
315
|
-
if ((aeroway === 'aerodrome' || aeroway === 'heliport') && zoom >= 11 && zoom < 15) return meaningfulLabel(name || ref, 980);
|
|
316
|
-
if (aeroway === 'runway' && zoom >= 12) return meaningfulLabel(ref || name || 'RWY', 900);
|
|
317
|
-
if ((aeroway === 'terminal' || aeroway === 'apron') && zoom >= 15) return meaningfulLabel(name || ref, 720);
|
|
318
|
-
if (aeroway === 'helipad' && zoom >= 14) return meaningfulLabel(ref || name || 'H', 980);
|
|
319
|
-
} else if (layer === 'pois' && zoom >= 13) {
|
|
320
|
-
return meaningfulLabel(cleanText(feature.get('name') ?? feature.get('ref') ?? feature.get('operator')), 620);
|
|
321
|
-
}
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function getLayerRule(styleDocument, layer) {
|
|
326
|
-
const layers = styleDocument.layers && typeof styleDocument.layers === 'object' ? styleDocument.layers : {};
|
|
327
|
-
const id = layer.style || layer.id;
|
|
328
|
-
return normalizeStyleRule(layers[id] || layers[layerAlias(id)] || {});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function normalizeStyleRule(rule) {
|
|
332
|
-
const normalized = { ...rule };
|
|
333
|
-
const visibility = objectRule(rule.visibility);
|
|
334
|
-
const body = objectRule(rule.body);
|
|
335
|
-
const center = objectRule(rule.center);
|
|
336
|
-
if (visibility) {
|
|
337
|
-
normalized.visible = visibility.visible ?? normalized.visible;
|
|
338
|
-
normalized.minZoom = visibility.minZoom ?? normalized.minZoom;
|
|
339
|
-
normalized.maxZoom = visibility.maxZoom ?? normalized.maxZoom;
|
|
340
|
-
}
|
|
341
|
-
if (body) {
|
|
342
|
-
normalized.stroke = body.color ?? normalized.stroke;
|
|
343
|
-
normalized.strokeWidth = body.width ?? normalized.strokeWidth;
|
|
344
|
-
normalized.strokeOpacity = body.opacity ?? normalized.strokeOpacity;
|
|
345
|
-
normalized.lineCap = body.lineCap ?? normalized.lineCap;
|
|
346
|
-
normalized.lineJoin = body.lineJoin ?? normalized.lineJoin;
|
|
347
|
-
}
|
|
348
|
-
if (center) {
|
|
349
|
-
normalized.fill = center.color ?? normalized.fill;
|
|
350
|
-
normalized.fillOpacity = center.opacity ?? normalized.fillOpacity;
|
|
351
|
-
}
|
|
352
|
-
if (!normalized.lineCap) normalized.lineCap = 'round';
|
|
353
|
-
if (!normalized.lineJoin) normalized.lineJoin = 'round';
|
|
354
|
-
return normalized;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function mergeFeatureRule(rule, feature) {
|
|
358
|
-
let merged = { ...rule };
|
|
359
|
-
const byProperty = objectRule(rule.byProperty);
|
|
360
|
-
if (!byProperty) return merged;
|
|
361
|
-
for (const [property, values] of Object.entries(byProperty)) {
|
|
362
|
-
const override = objectRule(values?.[String(feature.get(property) ?? '')]);
|
|
363
|
-
if (override) merged = normalizeStyleRule({ ...merged, ...override });
|
|
364
|
-
}
|
|
365
|
-
return merged;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function orderManifestLayers(manifest, styleDocument) {
|
|
369
|
-
const layers = Array.isArray(manifest.layers) ? manifest.layers.map(manifestLayer) : [];
|
|
370
|
-
const drawOrder = Array.isArray(styleDocument.drawOrder) ? styleDocument.drawOrder : layers.map((layer) => layer.id);
|
|
371
|
-
return [...layers].sort((a, b) => {
|
|
372
|
-
const ai = drawOrder.indexOf(a.id);
|
|
373
|
-
const bi = drawOrder.indexOf(b.id);
|
|
374
|
-
const ao = Number(getLayerRule(styleDocument, a).order ?? 0);
|
|
375
|
-
const bo = Number(getLayerRule(styleDocument, b).order ?? 0);
|
|
376
|
-
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi) || ao - bo;
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function manifestLayer(layerId) {
|
|
381
|
-
return { id: String(layerId), style: String(layerId) };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function groupFeaturesByLayer(features) {
|
|
385
|
-
const groups = new Map();
|
|
386
|
-
for (const feature of features) {
|
|
387
|
-
const layer = sourceLayerFor(String(feature.get('layer') ?? ''));
|
|
388
|
-
if (!groups.has(layer)) groups.set(layer, []);
|
|
389
|
-
groups.get(layer).push(feature);
|
|
390
|
-
}
|
|
391
|
-
return groups;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function zoomMatchesRule(zoom, rule) {
|
|
395
|
-
if (Number.isFinite(rule.minZoom) && zoom < Number(rule.minZoom)) return false;
|
|
396
|
-
if (Number.isFinite(rule.maxZoom) && zoom > Number(rule.maxZoom)) return false;
|
|
397
|
-
return rule.visible !== false;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function styleWidth(value, fallback, layerId, zoom) {
|
|
401
|
-
const width = Number(Array.isArray(value) ? fallback : value);
|
|
402
|
-
const base = Number.isFinite(width) && width > 0 ? width : fallback;
|
|
403
|
-
const scale = layerId === 'roads' ? Math.max(0.65, Math.min(1.35, 0.72 + zoom * 0.035)) : 1;
|
|
404
|
-
return Math.max(0, base * scale);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function traceLine(ctx, line, extent, size, close = false) {
|
|
408
|
-
let first = true;
|
|
409
|
-
for (const point of line) {
|
|
410
|
-
const [px, py] = projectPoint(point, extent, size);
|
|
411
|
-
if (!Number.isFinite(px) || !Number.isFinite(py)) {
|
|
412
|
-
first = true;
|
|
413
|
-
} else if (first) {
|
|
414
|
-
ctx.moveTo(px, py);
|
|
415
|
-
first = false;
|
|
416
|
-
} else {
|
|
417
|
-
ctx.lineTo(px, py);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (close) ctx.closePath();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function labelPoint(geometry) {
|
|
424
|
-
if (!geometry) return null;
|
|
425
|
-
const type = geometry.getType();
|
|
426
|
-
if (type === 'Point') return geometry.getCoordinates();
|
|
427
|
-
if (type === 'MultiPoint') return geometry.getCoordinates()[0] ?? null;
|
|
428
|
-
if (type === 'LineString') return middlePoint(geometry.getCoordinates());
|
|
429
|
-
if (type === 'MultiLineString') return middlePoint(geometry.getCoordinates()[0] ?? []);
|
|
430
|
-
if (type === 'Polygon') return ringCenter(geometry.getCoordinates()[0] ?? []);
|
|
431
|
-
if (type === 'MultiPolygon') return ringCenter(geometry.getCoordinates()[0]?.[0] ?? []);
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function middlePoint(line) {
|
|
436
|
-
return line[Math.max(0, Math.floor(line.length / 2))] ?? null;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function ringCenter(ring) {
|
|
440
|
-
if (!ring.length) return null;
|
|
441
|
-
let x = 0;
|
|
442
|
-
let y = 0;
|
|
443
|
-
for (const point of ring) {
|
|
444
|
-
x += point[0];
|
|
445
|
-
y += point[1];
|
|
446
|
-
}
|
|
447
|
-
return [x / ring.length, y / ring.length];
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function clipCanvas(ctx, size) {
|
|
451
|
-
ctx.beginPath();
|
|
452
|
-
ctx.rect(0, 0, size, size);
|
|
453
|
-
ctx.clip();
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function applyStroke(ctx, style) {
|
|
457
|
-
ctx.strokeStyle = style.stroke;
|
|
458
|
-
ctx.lineWidth = Math.max(0, Number(style.width ?? 1));
|
|
459
|
-
ctx.lineCap = String(style.lineCap || 'round');
|
|
460
|
-
ctx.lineJoin = String(style.lineJoin || 'round');
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function clearCanvasBorder(ctx, width, height, pixels) {
|
|
464
|
-
if (!(pixels > 0)) return;
|
|
465
|
-
ctx.clearRect(0, 0, width, pixels);
|
|
466
|
-
ctx.clearRect(0, height - pixels, width, pixels);
|
|
467
|
-
ctx.clearRect(0, 0, pixels, height);
|
|
468
|
-
ctx.clearRect(width - pixels, 0, pixels, height);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function boxesOverlap(a, b) {
|
|
472
|
-
return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function projectPoint(point, extent, size) {
|
|
476
|
-
const [minX, minY, maxX, maxY] = extent;
|
|
477
|
-
return [((point[0] - minX) / (maxX - minX)) * size, ((maxY - point[1]) / (maxY - minY)) * size];
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function geometryTypeKind(type) {
|
|
481
|
-
if (type === 'Point' || type === 'MultiPoint') return 'point';
|
|
482
|
-
if (type === 'Polygon' || type === 'MultiPolygon') return 'polygon';
|
|
483
|
-
return 'line';
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function tileMercatorExtent(x, y, z) {
|
|
487
|
-
const span = (WEB_MERCATOR_MAX * 2) / 2 ** z;
|
|
488
|
-
const minX = -WEB_MERCATOR_MAX + x * span;
|
|
489
|
-
const maxX = minX + span;
|
|
490
|
-
const maxY = WEB_MERCATOR_MAX - y * span;
|
|
491
|
-
const minY = maxY - span;
|
|
492
|
-
return [minX, minY, maxX, maxY];
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function sourceTileForRequest(x, y, z, maxZoom) {
|
|
496
|
-
const sourceZ = Math.min(z, maxZoom);
|
|
497
|
-
if (sourceZ === z) return { x, y, z };
|
|
498
|
-
const shift = z - sourceZ;
|
|
499
|
-
return { x: Math.floor(x / 2 ** shift), y: Math.floor(y / 2 ** shift), z: sourceZ };
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function normalizeContextLayers(layers) {
|
|
503
|
-
const values = Array.isArray(layers) && layers.length > 0 ? layers : DEFAULT_CONTEXT_LAYERS;
|
|
504
|
-
return values.map((layer) => sourceLayerFor(String(layer)));
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function isLayerVisible(layerVisibility, layer) {
|
|
508
|
-
const direct = layerVisibility.get(layer);
|
|
509
|
-
if (direct != null) return direct;
|
|
510
|
-
const source = layerVisibility.get(sourceLayerFor(layer));
|
|
511
|
-
if (source != null) return source;
|
|
512
|
-
return layerVisibility.get(layerAlias(layer)) === true;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function sourceLayerFor(layer) {
|
|
516
|
-
return layer === 'aviation' ? 'aip' : layer;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function layerAlias(layer) {
|
|
520
|
-
if (layer === 'aip') return 'aviation';
|
|
521
|
-
if (layer === 'aviation') return 'aip';
|
|
522
|
-
return layer;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function isAipLayer(layer) {
|
|
526
|
-
return layer === 'aip' || layer === 'aviation';
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function colorWithOpacity(color, alpha) {
|
|
530
|
-
if (color.startsWith('rgba(') || color.startsWith('hsla(')) return color;
|
|
531
|
-
const rgb = parseHexColor(color);
|
|
532
|
-
return rgb ? `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})` : color;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function parseHexColor(color) {
|
|
536
|
-
const match = color.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
537
|
-
if (!match) return null;
|
|
538
|
-
const hex = match[1].length === 3 ? match[1].split('').map((char) => char + char).join('') : match[1];
|
|
539
|
-
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function opacity(value, fallback) {
|
|
543
|
-
const number = Number(value ?? fallback);
|
|
544
|
-
return Number.isFinite(number) ? Math.max(0, Math.min(1, number)) : fallback;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function cleanText(value) {
|
|
548
|
-
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function isMeaningfulLabel(text) {
|
|
552
|
-
const normalized = cleanText(text);
|
|
553
|
-
return normalized.length >= 2 && !GENERIC_LABEL_VALUES.has(normalized.toLowerCase().replace(/\s+/g, '_'));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function meaningfulLabel(text, priority) {
|
|
557
|
-
return isMeaningfulLabel(text) ? { text: cleanText(text), priority } : null;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function objectRule(value) {
|
|
561
|
-
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function pmtilesInfo(manifest) {
|
|
565
|
-
const tiles = manifest.tiles && typeof manifest.tiles === 'object' ? manifest.tiles : {};
|
|
566
|
-
return tiles.format === 'pmtiles' || tiles.type === 'mvt' ? tiles : {};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function clampInteger(value, min, max) {
|
|
570
|
-
const number = Math.trunc(Number(value));
|
|
571
|
-
return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : min;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function clampNumber(value, min, max) {
|
|
575
|
-
const number = Number(value);
|
|
576
|
-
return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : min;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function resolveRelativeUrl(url, baseUrl) {
|
|
580
|
-
return new URL(url, new URL(baseUrl, self.location.href)).toString();
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function createMetrics() {
|
|
584
|
-
return {
|
|
585
|
-
requested: 0,
|
|
586
|
-
cacheHits: 0,
|
|
587
|
-
decoded: 0,
|
|
588
|
-
features: 0,
|
|
589
|
-
overzoomed: 0,
|
|
590
|
-
requestLevels: {},
|
|
591
|
-
sourceLevels: {},
|
|
592
|
-
renderMs: { total: 0, max: 0 },
|
|
593
|
-
decodeMs: { total: 0, max: 0 }
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function recordMetricTime(metrics, key, value) {
|
|
598
|
-
metrics[key].total += value;
|
|
599
|
-
metrics[key].max = Math.max(metrics[key].max, value);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function addMetricCount(counts, key) {
|
|
603
|
-
counts[key] = (counts[key] ?? 0) + 1;
|
|
604
|
-
}
|