@map-zero/cesium 0.3.2 → 0.4.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/LICENSE +21 -0
- package/README.md +10 -0
- package/package.json +6 -5
- package/src/cesium-labels.js +150 -0
- package/src/index.js +45 -82
- package/src/vector.js +138 -0
- package/src/imagery.js +0 -305
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcos Pérez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -6,3 +6,13 @@ See the main repository for documentation, examples, and release notes:
|
|
|
6
6
|
|
|
7
7
|
https://github.com/mploscos/map-zero
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
## Cesium 1.145 native vectors
|
|
11
|
+
|
|
12
|
+
Version 0.4.0 renders native MVT with an XYZ `vectorTilesUrl`, using Cesium 1.145 `MVTDataProvider` and surface draping. The built-in server exposes PMTiles data at `/api/vector-tiles/{z}/{x}/{y}.mvt`.
|
|
13
|
+
|
|
14
|
+
Native labels share the OpenLayers theme’s selection and priority rules, with deduplication and screen decluttering. Set `labels: false` or `maxLabels` (default 150), and toggle them with `controller.setLabelsVisible()`.
|
|
15
|
+
|
|
16
|
+
The raster worker has been removed. Cesium no longer requires OpenLayers or PMTiles as peer dependencies. Provide an MVT endpoint for context, or `contextOverlay: false` for a static 3D Tiles-only scene. Re-export old PMTiles for label anchor metadata. See [integration details](https://github.com/mploscos/map-zero/blob/v0.4.0/docs/cesium.md).
|
|
17
|
+
|
|
18
|
+
Requires Cesium 1.145.0 and Node 22+ for package consumers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@map-zero/cesium",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cesium integration helper for map-zero 3D Tiles packages.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@map-zero/
|
|
25
|
+
"@map-zero/core": "0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"cesium": "1.
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
"cesium": "1.145.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Cartesian2, Cartesian3, Color, EllipsoidalOccluder, HeightReference,
|
|
3
|
+
HorizontalOrigin, LabelCollection, LabelStyle, SceneMode, SceneTransforms, VerticalOrigin
|
|
4
|
+
} from 'cesium';
|
|
5
|
+
import { LruCache } from '../../core/src/shared/cache.js';
|
|
6
|
+
import { describeLabel } from '../../core/src/labels.js';
|
|
7
|
+
import { getLayerRule, zoomMatchesRule } from '../../core/src/style.js';
|
|
8
|
+
|
|
9
|
+
/** Deterministic priority placement; bounds the visible label count and overlap checks. */
|
|
10
|
+
export function selectLabels(candidates, maxLabels) {
|
|
11
|
+
const selected = [];
|
|
12
|
+
const ids = new Set();
|
|
13
|
+
candidates.sort((a, b) => b.priority - a.priority || a.key.localeCompare(b.key));
|
|
14
|
+
for (const candidate of candidates) {
|
|
15
|
+
if (ids.has(candidate.key)) continue;
|
|
16
|
+
ids.add(candidate.key);
|
|
17
|
+
const a = candidate.box;
|
|
18
|
+
if (selected.some(({ box: b }) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1])) continue;
|
|
19
|
+
selected.push(candidate);
|
|
20
|
+
if (selected.length >= maxLabels) break;
|
|
21
|
+
}
|
|
22
|
+
return selected;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Labels share the native provider's loaded tiles; no extra requests or MVT decode. */
|
|
26
|
+
export function createCesiumLabels(viewer, tileset, options) {
|
|
27
|
+
const maxLabels = options.maxLabels ?? 150;
|
|
28
|
+
if (!Number.isInteger(maxLabels) || maxLabels < 1 || maxLabels > 1000) {
|
|
29
|
+
throw new Error('maxLabels must be an integer between 1 and 1000');
|
|
30
|
+
}
|
|
31
|
+
const scene = viewer.scene;
|
|
32
|
+
const collection = scene.primitives.add(new LabelCollection({ scene, show: options.labels !== false }));
|
|
33
|
+
const cache = new WeakMap();
|
|
34
|
+
const visible = new Set();
|
|
35
|
+
const active = new Map();
|
|
36
|
+
const widths = new LruCache(1024);
|
|
37
|
+
const measure = document.createElement('canvas').getContext('2d');
|
|
38
|
+
const occluder = new EllipsoidalOccluder(scene.globe?.ellipsoid);
|
|
39
|
+
let destroyed = false;
|
|
40
|
+
|
|
41
|
+
function candidatesFor(content) {
|
|
42
|
+
if (cache.has(content)) return cache.get(content);
|
|
43
|
+
const candidates = [];
|
|
44
|
+
// Cesium 1.145 vector content has one feature table per MVT source layer.
|
|
45
|
+
const tables = content.batchTables ?? [];
|
|
46
|
+
for (let table = 0; table < tables.length; table++) {
|
|
47
|
+
for (let i = 0; i < tables[table].featuresLength; i++) {
|
|
48
|
+
const feature = content.getFeature(i, table);
|
|
49
|
+
if (!feature) continue;
|
|
50
|
+
const lon = feature.getProperty('mapzero_label_lon');
|
|
51
|
+
const lat = feature.getProperty('mapzero_label_lat');
|
|
52
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat) || Math.abs(lon) > 180 || Math.abs(lat) > 90) continue;
|
|
53
|
+
const source = String(feature.getProperty('_layer') ?? feature.getProperty('layer') ?? '');
|
|
54
|
+
const layer = source === 'aviation' ? 'aip' : source;
|
|
55
|
+
const properties = Object.fromEntries(feature.getPropertyIds().map((key) => [key, feature.getProperty(key)]));
|
|
56
|
+
const adapter = { get: (key) => properties[key] };
|
|
57
|
+
// Keep data independent of the feature lifetime. Eligibility is evaluated
|
|
58
|
+
// again at the current zoom, so zoom limits in custom themes still apply.
|
|
59
|
+
const id = properties.id ?? properties.fid ?? `${lon},${lat}`;
|
|
60
|
+
candidates.push({ key: `${layer}:${id}`, layer, adapter, position: Cartesian3.fromDegrees(lon, lat) });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
cache.set(content, candidates);
|
|
64
|
+
return candidates;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function update() {
|
|
68
|
+
if (destroyed || !collection.show) return;
|
|
69
|
+
const candidates = [];
|
|
70
|
+
const zoom = options.getZoom();
|
|
71
|
+
const width = scene.canvas.clientWidth, height = scene.canvas.clientHeight;
|
|
72
|
+
const layerRules = new Map();
|
|
73
|
+
occluder.cameraPosition = viewer.camera.positionWC;
|
|
74
|
+
for (const content of visible) {
|
|
75
|
+
for (const candidate of candidatesFor(content)) {
|
|
76
|
+
const { layer, adapter, position } = candidate;
|
|
77
|
+
if (!layerRules.has(layer)) layerRules.set(layer, getLayerRule(options.styleDocument, { id: layer }));
|
|
78
|
+
if (options.visibility.get(layer) === false || !zoomMatchesRule(zoom, layerRules.get(layer))) continue;
|
|
79
|
+
const opacity = options.opacities.get(layer) ?? options.opacity ?? 1;
|
|
80
|
+
if (opacity <= 0) continue;
|
|
81
|
+
if (candidate.zoom !== zoom) {
|
|
82
|
+
candidate.zoom = zoom;
|
|
83
|
+
candidate.descriptor = describeLabel(adapter, layer, zoom, options.styleDocument);
|
|
84
|
+
if (candidate.descriptor?.text.length > 64) {
|
|
85
|
+
candidate.descriptor.text = candidate.descriptor.text.slice(0, 61).trimEnd() + '…';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const descriptor = candidate.descriptor;
|
|
89
|
+
if (!descriptor) continue;
|
|
90
|
+
if (scene.mode === SceneMode.SCENE3D && !occluder.isPointVisible(position)) continue;
|
|
91
|
+
const point = SceneTransforms.worldToWindowCoordinates(scene, position);
|
|
92
|
+
if (!point || point.x < 0 || point.x > width || point.y < 0 || point.y > height) continue;
|
|
93
|
+
const measureKey = `${descriptor.font}:${descriptor.text}`;
|
|
94
|
+
let textWidth = widths.get(measureKey);
|
|
95
|
+
if (textWidth === undefined) {
|
|
96
|
+
measure.font = descriptor.font;
|
|
97
|
+
textWidth = measure.measureText(descriptor.text).width;
|
|
98
|
+
widths.set(measureKey, textWidth);
|
|
99
|
+
}
|
|
100
|
+
const halfWidth = textWidth / 2 + descriptor.haloWidth + 6;
|
|
101
|
+
const labelHeight = (Number(/([\d.]+)px/.exec(descriptor.font)?.[1]) || 12) + 2 * descriptor.haloWidth + 6;
|
|
102
|
+
candidates.push({ ...candidate, ...descriptor, opacity,
|
|
103
|
+
box: [point.x - halfWidth, point.y - labelHeight - 6, point.x + halfWidth, point.y - 6] });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const selected = selectLabels(candidates, maxLabels);
|
|
107
|
+
const keys = new Set(selected.map((label) => label.key));
|
|
108
|
+
let changed = false;
|
|
109
|
+
for (const [key, value] of active) {
|
|
110
|
+
if (!keys.has(key)) { collection.remove(value.label); active.delete(key); changed = true; }
|
|
111
|
+
}
|
|
112
|
+
for (const candidate of selected) {
|
|
113
|
+
const signature = JSON.stringify([candidate.text, candidate.font, candidate.fill, candidate.halo, candidate.haloWidth, candidate.opacity]);
|
|
114
|
+
const previous = active.get(candidate.key);
|
|
115
|
+
if (previous?.signature === signature) continue;
|
|
116
|
+
const fillColor = Color.fromCssColorString(candidate.fill);
|
|
117
|
+
const outlineColor = Color.fromCssColorString(candidate.halo);
|
|
118
|
+
fillColor.alpha *= candidate.opacity; outlineColor.alpha *= candidate.opacity;
|
|
119
|
+
const properties = {
|
|
120
|
+
id: { mapZeroLayer: candidate.layer, mapZeroLabel: candidate.key },
|
|
121
|
+
position: candidate.position, text: candidate.text, font: candidate.font,
|
|
122
|
+
fillColor, outlineColor, outlineWidth: candidate.haloWidth,
|
|
123
|
+
style: LabelStyle.FILL_AND_OUTLINE, horizontalOrigin: HorizontalOrigin.CENTER,
|
|
124
|
+
verticalOrigin: VerticalOrigin.BOTTOM, pixelOffset: new Cartesian2(0, -6),
|
|
125
|
+
heightReference: HeightReference.CLAMP_TO_TERRAIN, disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
126
|
+
};
|
|
127
|
+
const label = previous?.label ?? collection.add(properties);
|
|
128
|
+
if (previous) Object.assign(label, properties);
|
|
129
|
+
active.set(candidate.key, { signature, label }); changed = true;
|
|
130
|
+
}
|
|
131
|
+
if (changed) scene.requestRender();
|
|
132
|
+
}
|
|
133
|
+
const removers = [
|
|
134
|
+
scene.preRender.addEventListener(() => visible.clear()),
|
|
135
|
+
tileset.tileVisible.addEventListener((tile) => visible.add(tile.content)),
|
|
136
|
+
// Mutate primitives only after traversal; an unchanged layout requests no frame.
|
|
137
|
+
scene.postRender.addEventListener(() => queueMicrotask(update))
|
|
138
|
+
];
|
|
139
|
+
return {
|
|
140
|
+
collection,
|
|
141
|
+
setVisible(visible) { collection.show = Boolean(visible); scene.requestRender(); },
|
|
142
|
+
destroy() {
|
|
143
|
+
if (destroyed) return;
|
|
144
|
+
destroyed = true;
|
|
145
|
+
removers.forEach((remove) => remove());
|
|
146
|
+
visible.clear(); active.clear(); widths.clear();
|
|
147
|
+
scene.primitives.remove(collection);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
+
import { createMapZeroVectorContext } from './vector.js';
|
|
1
2
|
import {
|
|
2
3
|
Cesium3DTileColorBlendMode,
|
|
3
4
|
Cesium3DTileStyle,
|
|
4
|
-
Cesium3DTileset
|
|
5
|
-
ImageryLayer
|
|
5
|
+
Cesium3DTileset
|
|
6
6
|
} from 'cesium';
|
|
7
|
-
import {
|
|
8
|
-
contextOverlayConfig,
|
|
9
|
-
hasMapZeroContextOverlay,
|
|
10
|
-
MapZeroCesiumImageryProvider
|
|
11
|
-
} from './imagery.js';
|
|
12
7
|
|
|
13
|
-
|
|
8
|
+
|
|
9
|
+
export { createNativeVectorStyle, vectorZoomRange } from './vector.js';
|
|
14
10
|
|
|
15
11
|
let autoInstanceCounter = 0;
|
|
16
12
|
|
|
@@ -160,10 +156,12 @@ export async function createMapZeroCesiumTilesets(options) {
|
|
|
160
156
|
* tilesetOpacity?: number,
|
|
161
157
|
* buildingsOpacity?: number,
|
|
162
158
|
* contextOverlay?: boolean,
|
|
159
|
+
* vectorTilesUrl?: string,
|
|
160
|
+
* vectorMaxZoom?: number,
|
|
161
|
+
* labels?: boolean,
|
|
162
|
+
* maxLabels?: number,
|
|
163
|
+
* vectorHeightReference?: import('cesium').HeightReference,
|
|
163
164
|
* contextOpacity?: number,
|
|
164
|
-
* contextOverzoomLevels?: number,
|
|
165
|
-
* contextEdgeGuardPixels?: number,
|
|
166
|
-
* workerUrl?: string | URL,
|
|
167
165
|
* buildings3d?: boolean,
|
|
168
166
|
* tilesetMaximumScreenSpaceError?: number,
|
|
169
167
|
* tilesetCacheBytes?: number,
|
|
@@ -178,8 +176,9 @@ export async function createMapZeroCesiumTilesets(options) {
|
|
|
178
176
|
* manifest: MapZeroManifest,
|
|
179
177
|
* style: Record<string, unknown> | null,
|
|
180
178
|
* tilesets: Record<string, Cesium3DTileset>,
|
|
181
|
-
*
|
|
182
|
-
*
|
|
179
|
+
* vectorProvider?: import('cesium').MVTDataProvider,
|
|
180
|
+
* labelCollection?: import('cesium').LabelCollection,
|
|
181
|
+
* setLabelsVisible: (visible: boolean) => void,
|
|
183
182
|
* setVisible: (layerId: string, visible: boolean) => void,
|
|
184
183
|
* setOpacity: (layerId: string, opacity: number) => void,
|
|
185
184
|
* destroy: () => void
|
|
@@ -196,27 +195,18 @@ export async function addMapZeroToCesium(viewer, options) {
|
|
|
196
195
|
const result = await createMapZeroCesiumTilesets(options);
|
|
197
196
|
const uniqueTilesets = [...new Set(Object.values(result.tilesets))];
|
|
198
197
|
const visibleLayers = new Set(Object.keys(result.tilesets));
|
|
199
|
-
let
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const imageryLayer = imageryProvider
|
|
212
|
-
? new ImageryLayer(imageryProvider, {
|
|
213
|
-
alpha: clamp01(Number(options.contextOpacity ?? options.opacity ?? 1)),
|
|
214
|
-
show: true
|
|
215
|
-
})
|
|
216
|
-
: undefined;
|
|
217
|
-
|
|
218
|
-
if (imageryLayer) {
|
|
219
|
-
viewer.imageryLayers?.add(imageryLayer);
|
|
198
|
+
let vectorContext;
|
|
199
|
+
try {
|
|
200
|
+
if (options.contextOverlay !== false) {
|
|
201
|
+
vectorContext = await createMapZeroVectorContext(viewer, {
|
|
202
|
+
...options, manifest: result.manifest, styleDocument: result.style,
|
|
203
|
+
excludedLayers: Object.keys(result.tilesets)
|
|
204
|
+
});
|
|
205
|
+
viewer.scene.primitives.add(vectorContext.provider);
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
for (const tileset of uniqueTilesets) tileset.destroy();
|
|
209
|
+
throw error;
|
|
220
210
|
}
|
|
221
211
|
for (const tileset of uniqueTilesets) {
|
|
222
212
|
viewer.scene.primitives.add(tileset);
|
|
@@ -233,40 +223,34 @@ export async function addMapZeroToCesium(viewer, options) {
|
|
|
233
223
|
id: result.id,
|
|
234
224
|
style: result.style,
|
|
235
225
|
tilesets: result.tilesets,
|
|
236
|
-
|
|
237
|
-
|
|
226
|
+
vectorProvider: vectorContext?.provider,
|
|
227
|
+
vectorRange: vectorContext?.range,
|
|
228
|
+
labelCollection: vectorContext?.labels?.collection,
|
|
229
|
+
setLabelsVisible(visible) { vectorContext?.labels?.setVisible(visible); },
|
|
238
230
|
setVisible(layerId, visible) {
|
|
231
|
+
vectorContext?.setVisible(layerId, visible);
|
|
239
232
|
const tileset = result.tilesets[layerId];
|
|
240
|
-
if (tileset)
|
|
241
|
-
if (visible) {
|
|
242
|
-
visibleLayers.add(layerId);
|
|
243
|
-
} else {
|
|
244
|
-
visibleLayers.delete(layerId);
|
|
245
|
-
}
|
|
246
|
-
applyStyleToTilesetMap(result.tilesets, result.style, {
|
|
247
|
-
opacity,
|
|
248
|
-
visibleLayers
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
imageryProvider?.setLayerVisible(layerId, visible);
|
|
252
|
-
imageryProvider?.setLayerVisible(layerId === 'aviation' ? 'aip' : layerId, visible);
|
|
233
|
+
if (tileset) tileset.show = Boolean(visible);
|
|
253
234
|
viewer.scene?.requestRender?.();
|
|
254
235
|
},
|
|
255
236
|
setOpacity(layerId, nextOpacity) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
237
|
+
vectorContext?.setOpacity(layerId, nextOpacity);
|
|
238
|
+
const tileset = result.tilesets[layerId];
|
|
239
|
+
if (!tileset) return;
|
|
240
|
+
tileset.style = createMapZeroCesiumStyle(result.style, {
|
|
241
|
+
layerId,
|
|
242
|
+
opacity: clamp01(Number(nextOpacity)),
|
|
260
243
|
visibleLayers
|
|
261
244
|
});
|
|
245
|
+
viewer.scene?.requestRender?.();
|
|
262
246
|
},
|
|
263
247
|
destroy() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
248
|
+
vectorContext?.destroy();
|
|
249
|
+
if (vectorContext) viewer.scene.primitives.remove(vectorContext.provider);
|
|
267
250
|
for (const tileset of uniqueTilesets) {
|
|
268
251
|
viewer.scene.primitives.remove(tileset);
|
|
269
252
|
}
|
|
253
|
+
viewer.scene?.requestRender?.();
|
|
270
254
|
}
|
|
271
255
|
};
|
|
272
256
|
}
|
|
@@ -352,21 +336,6 @@ function cesiumLayerMaterial(rule, layerId) {
|
|
|
352
336
|
};
|
|
353
337
|
}
|
|
354
338
|
|
|
355
|
-
/**
|
|
356
|
-
* @param {Record<string, Cesium3DTileset>} tilesets
|
|
357
|
-
* @param {Record<string, unknown> | null} style
|
|
358
|
-
* @param {{ opacity: number, visibleLayers: Set<string> }} options
|
|
359
|
-
*/
|
|
360
|
-
function applyStyleToTilesetMap(tilesets, style, options) {
|
|
361
|
-
for (const [layerId, tileset] of Object.entries(tilesets)) {
|
|
362
|
-
tileset.style = createMapZeroCesiumStyle(style, {
|
|
363
|
-
layerId,
|
|
364
|
-
opacity: options.opacity,
|
|
365
|
-
visibleLayers: options.visibleLayers
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
339
|
/**
|
|
371
340
|
* @param {string} layerId
|
|
372
341
|
* @param {{ opacity?: number, tilesetOpacity?: number, buildingsOpacity?: number }} options
|
|
@@ -398,6 +367,11 @@ function layerStyle(styleJson, layerId) {
|
|
|
398
367
|
* @returns {Array<{ layerId: string, url: string }>}
|
|
399
368
|
*/
|
|
400
369
|
function manifestTilesetEntries(manifest, options = {}) {
|
|
370
|
+
if (manifest.tiles3d?.format === '3dtiles' && manifest.tiles3d.tilesets) {
|
|
371
|
+
return Object.entries(manifest.tiles3d.tilesets)
|
|
372
|
+
.filter(([layerId, url]) => typeof url === 'string' && isAllowedCesiumTilesetLayer(layerId, options))
|
|
373
|
+
.map(([layerId, url]) => ({ layerId, url }));
|
|
374
|
+
}
|
|
401
375
|
if (manifest.tiles3d?.format === '3dtiles' && typeof manifest.tiles3d.url === 'string') {
|
|
402
376
|
const layers = Array.isArray(manifest.tiles3d.layers) && manifest.tiles3d.layers.length > 0
|
|
403
377
|
? manifest.tiles3d.layers.map(String)
|
|
@@ -422,17 +396,6 @@ function isAllowedCesiumTilesetLayer(layerId, options) {
|
|
|
422
396
|
return layerId !== 'buildings' || options.buildings3d !== false;
|
|
423
397
|
}
|
|
424
398
|
|
|
425
|
-
/**
|
|
426
|
-
* @param {MapZeroManifest} manifest
|
|
427
|
-
* @param {{ contextOverlay?: boolean }} options
|
|
428
|
-
* @returns {boolean}
|
|
429
|
-
*/
|
|
430
|
-
function shouldCreateContextOverlay(manifest, options) {
|
|
431
|
-
if (options.contextOverlay === false) {
|
|
432
|
-
return false;
|
|
433
|
-
}
|
|
434
|
-
return hasMapZeroContextOverlay(manifest);
|
|
435
|
-
}
|
|
436
399
|
|
|
437
400
|
/**
|
|
438
401
|
* @param {Cesium3DTileset} tileset
|
package/src/vector.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createCesiumLabels } from './cesium-labels.js';
|
|
2
|
+
import { BoundingSphere, Cartesian3, Cesium3DTileStyle, Color, HeightReference, MVTDataProvider, Rectangle } from 'cesium';
|
|
3
|
+
import { getLayerRule, mergeFeatureRule, styleWidth, zoomMatchesRule } from '../../core/src/style.js';
|
|
4
|
+
|
|
5
|
+
const POLYGON_LAYERS = new Set(['landuse', 'terrain', 'water', 'buildings']);
|
|
6
|
+
|
|
7
|
+
/** Keep Cesium's eager runtime hierarchy bounded before allocating tile nodes. */
|
|
8
|
+
export function vectorZoomRange(bbox, minZoom = 8, maxZoom = 16, maxNodes = 20000) {
|
|
9
|
+
if (!Array.isArray(bbox) || bbox.length !== 4 || !bbox.every(Number.isFinite)
|
|
10
|
+
|| bbox[0] >= bbox[2] || bbox[1] >= bbox[3] || bbox[0] < -180 || bbox[2] > 180
|
|
11
|
+
|| bbox[1] < -85.05112878 || bbox[3] > 85.05112878) {
|
|
12
|
+
throw new Error('Native vector rendering requires a valid Web Mercator manifest bbox');
|
|
13
|
+
}
|
|
14
|
+
if (!Number.isInteger(minZoom) || !Number.isInteger(maxZoom) || minZoom < 0 || maxZoom > 22 || minZoom > maxZoom) {
|
|
15
|
+
throw new Error('Native vector zoom range must satisfy 0 <= minZoom <= maxZoom <= 22');
|
|
16
|
+
}
|
|
17
|
+
const tileY = (lat, n) => (1 - Math.asinh(Math.tan(lat * Math.PI / 180)) / Math.PI) / 2 * n;
|
|
18
|
+
let nodes = 0;
|
|
19
|
+
let effectiveMaxZoom = minZoom;
|
|
20
|
+
for (let z = minZoom; z <= maxZoom; z++) {
|
|
21
|
+
const n = 2 ** z;
|
|
22
|
+
const columns = Math.floor((bbox[2] + 180) / 360 * n) - Math.floor((bbox[0] + 180) / 360 * n) + 1;
|
|
23
|
+
const rows = Math.floor(tileY(bbox[1], n)) - Math.floor(tileY(bbox[3], n)) + 1;
|
|
24
|
+
if (nodes + columns * rows > maxNodes) {
|
|
25
|
+
if (z === minZoom) throw new Error('Native vector extent is too large at minZoom; use a smaller package or lower minZoom');
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
nodes += columns * rows;
|
|
29
|
+
effectiveMaxZoom = z;
|
|
30
|
+
}
|
|
31
|
+
return { minZoom, maxZoom: effectiveMaxZoom, estimatedNodes: nodes };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Translate the shared map-zero theme into native per-feature Cesium styling.
|
|
35
|
+
* Labels, glow, dashes and multi-pass road casings are not supported by MVTDataProvider.
|
|
36
|
+
*/
|
|
37
|
+
export function createNativeVectorStyle(styleDocument, options = {}) {
|
|
38
|
+
const zoom = options.zoom ?? 16;
|
|
39
|
+
const visibility = options.visibility ?? new Map();
|
|
40
|
+
const opacities = options.opacities ?? new Map();
|
|
41
|
+
const excludedLayers = options.excludedLayers ?? new Set();
|
|
42
|
+
const rules = new WeakMap();
|
|
43
|
+
const describe = (feature) => {
|
|
44
|
+
if (rules.has(feature)) return rules.get(feature);
|
|
45
|
+
const layer = String(feature.getProperty('_layer') ?? feature.getProperty('layer') ?? '');
|
|
46
|
+
const id = layer === 'aviation' ? 'aip' : layer;
|
|
47
|
+
const rule = mergeFeatureRule(getLayerRule(styleDocument, { id }), { get: (key) => feature.getProperty(key) });
|
|
48
|
+
const geometry = String(feature.getProperty('mapzero_geometry') ?? '');
|
|
49
|
+
const polygon = geometry ? geometry.includes('Polygon') : POLYGON_LAYERS.has(id) || id === 'boundaries';
|
|
50
|
+
const point = geometry ? geometry.includes('Point') : id === 'pois';
|
|
51
|
+
const info = { id, rule, polygon, point };
|
|
52
|
+
rules.set(feature, info);
|
|
53
|
+
return info;
|
|
54
|
+
};
|
|
55
|
+
const evaluateColor = (feature, result) => {
|
|
56
|
+
const { id, rule, polygon, point } = describe(feature);
|
|
57
|
+
const css = polygon || point ? rule.fill ?? rule.stroke : rule.stroke ?? rule.fill;
|
|
58
|
+
const color = Color.fromCssColorString(String(css ?? '#00ffff'), result) ?? Color.clone(Color.CYAN, result);
|
|
59
|
+
const alpha = polygon ? (rule.fill ? rule.fillOpacity ?? 1 : 0) : point ? rule.fillOpacity ?? 1 : rule.strokeOpacity ?? 1;
|
|
60
|
+
color.alpha *= Math.max(0, Math.min(1, Number(alpha) * (opacities.get(id) ?? options.opacity ?? 1)));
|
|
61
|
+
return color;
|
|
62
|
+
};
|
|
63
|
+
return new Cesium3DTileStyle({
|
|
64
|
+
show: { evaluate(feature) {
|
|
65
|
+
const { id, rule } = describe(feature);
|
|
66
|
+
return !excludedLayers.has(id) && visibility.get(id) !== false && zoomMatchesRule(zoom, rule);
|
|
67
|
+
} },
|
|
68
|
+
color: { evaluate: evaluateColor, evaluateColor },
|
|
69
|
+
lineWidth: { evaluate(feature) {
|
|
70
|
+
const { id, rule } = describe(feature);
|
|
71
|
+
return styleWidth(rule.strokeWidth, 1, id, zoom);
|
|
72
|
+
} },
|
|
73
|
+
pointSize: { evaluate(feature) {
|
|
74
|
+
const { id, rule } = describe(feature);
|
|
75
|
+
return 2 * styleWidth(rule.radius ?? rule.circleRadius, 3, id, zoom);
|
|
76
|
+
} }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Native MVT rendering, using the public Cesium 1.145 provider API. */
|
|
81
|
+
export async function createMapZeroVectorContext(viewer, options) {
|
|
82
|
+
if (!options.vectorTilesUrl) throw new Error('Native vector rendering requires vectorTilesUrl with /{z}/{x}/{y}.mvt');
|
|
83
|
+
const template = new URL(options.vectorTilesUrl, new URL(options.manifestUrl, globalThis.location?.href ?? 'http://localhost/')).href
|
|
84
|
+
.replaceAll('%7B', '{').replaceAll('%7D', '}');
|
|
85
|
+
if (!['{z}', '{x}', '{y}'].every((token) => template.includes(token))) throw new Error('vectorTilesUrl must include {z}, {x}, and {y}');
|
|
86
|
+
const range = vectorZoomRange(options.manifest.bbox,
|
|
87
|
+
options.manifest.tiles?.minZoom ?? 8, Math.min(options.vectorMaxZoom ?? options.manifest.tiles?.maxZoom ?? 16, options.manifest.tiles?.maxZoom ?? 16));
|
|
88
|
+
const provider = await MVTDataProvider.fromUrl(template, {
|
|
89
|
+
minZoom: range.minZoom,
|
|
90
|
+
maxZoom: range.maxZoom,
|
|
91
|
+
extent: Rectangle.fromDegrees(...options.manifest.bbox),
|
|
92
|
+
heightReference: options.vectorHeightReference ?? HeightReference.CLAMP_TO_TERRAIN,
|
|
93
|
+
scene: viewer.scene
|
|
94
|
+
});
|
|
95
|
+
const visibility = new Map();
|
|
96
|
+
const opacities = new Map();
|
|
97
|
+
const excludedLayers = new Set(options.excludedLayers ?? []);
|
|
98
|
+
let zoom;
|
|
99
|
+
const bbox = options.manifest.bbox;
|
|
100
|
+
const latitude = (bbox[1] + bbox[3]) / 2;
|
|
101
|
+
const center = new BoundingSphere(Cartesian3.fromDegrees((bbox[0] + bbox[2]) / 2, latitude), 1);
|
|
102
|
+
const getZoom = () => {
|
|
103
|
+
const resolution = viewer.camera.getPixelSize(center, viewer.scene.canvas.clientWidth, viewer.scene.canvas.clientHeight);
|
|
104
|
+
return Math.max(0, Math.min(22, Math.round(Math.log2(156543.033928 * Math.cos(latitude * Math.PI / 180) / Math.max(0.001, resolution)))));
|
|
105
|
+
};
|
|
106
|
+
const refresh = () => {
|
|
107
|
+
const nextZoom = getZoom();
|
|
108
|
+
zoom = nextZoom;
|
|
109
|
+
provider.tileset.style = createNativeVectorStyle(options.styleDocument ?? {}, {
|
|
110
|
+
zoom, visibility, opacities, excludedLayers, opacity: options.contextOpacity ?? options.opacity ?? 1
|
|
111
|
+
});
|
|
112
|
+
viewer.scene.requestRender?.();
|
|
113
|
+
};
|
|
114
|
+
provider.tileset.maximumScreenSpaceError = 8;
|
|
115
|
+
provider.tileset.cacheBytes = 128 * 1024 * 1024;
|
|
116
|
+
provider.tileset.maximumCacheOverflowBytes = 64 * 1024 * 1024;
|
|
117
|
+
provider.tileset.preloadWhenHidden = false;
|
|
118
|
+
let labels;
|
|
119
|
+
try {
|
|
120
|
+
labels = createCesiumLabels(viewer, provider.tileset, {
|
|
121
|
+
...options, styleDocument: options.styleDocument ?? {}, visibility, opacities, getZoom,
|
|
122
|
+
opacity: options.contextOpacity ?? options.opacity ?? 1
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
provider.destroy();
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
refresh();
|
|
129
|
+
const removeCameraListener = viewer.camera?.moveEnd?.addEventListener(() => refresh());
|
|
130
|
+
return {
|
|
131
|
+
provider,
|
|
132
|
+
range,
|
|
133
|
+
labels,
|
|
134
|
+
setVisible(id, visible) { visibility.set(id === 'aviation' ? 'aip' : id, Boolean(visible)); refresh(); },
|
|
135
|
+
setOpacity(id, opacity) { opacities.set(id === 'aviation' ? 'aip' : id, Math.max(0, Math.min(1, Number(opacity)))); refresh(); },
|
|
136
|
+
destroy() { removeCameraListener?.(); labels.destroy(); }
|
|
137
|
+
};
|
|
138
|
+
}
|
package/src/imagery.js
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Event,
|
|
3
|
-
Rectangle,
|
|
4
|
-
WebMercatorTilingScheme
|
|
5
|
-
} from 'cesium';
|
|
6
|
-
|
|
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);
|
|
18
|
-
const TILE_SIZE = 512;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Cesium ImageryProvider that rasterizes map-zero PMTiles/MVT tiles in a
|
|
22
|
-
* dedicated worker. OffscreenCanvas is required by design so MVT decoding and
|
|
23
|
-
* canvas drawing never block Cesium's main render thread.
|
|
24
|
-
*/
|
|
25
|
-
export class MapZeroCesiumImageryProvider {
|
|
26
|
-
/**
|
|
27
|
-
* @param {{
|
|
28
|
-
* manifest: Record<string, unknown>,
|
|
29
|
-
* manifestUrl: string,
|
|
30
|
-
* styleDocument?: Record<string, unknown> | null,
|
|
31
|
-
* layers?: string[],
|
|
32
|
-
* tileSize?: number,
|
|
33
|
-
* minimumLevel?: number,
|
|
34
|
-
* maximumLevel?: number,
|
|
35
|
-
* overzoomLevels?: number,
|
|
36
|
-
* edgeGuardPixels?: number,
|
|
37
|
-
* workerUrl?: string | URL
|
|
38
|
-
* }} options
|
|
39
|
-
*/
|
|
40
|
-
constructor(options) {
|
|
41
|
-
assertWorkerRasterSupport();
|
|
42
|
-
this.manifest = options.manifest;
|
|
43
|
-
this.manifestUrl = resolveWorkerBaseUrl(options.manifestUrl);
|
|
44
|
-
this.styleDocument = options.styleDocument ?? {};
|
|
45
|
-
this.tileWidth = Number(options.tileSize ?? TILE_SIZE);
|
|
46
|
-
this.tileHeight = Number(options.tileSize ?? TILE_SIZE);
|
|
47
|
-
this.tilingScheme = new WebMercatorTilingScheme();
|
|
48
|
-
this.rectangle = rectangleFromManifestBbox(
|
|
49
|
-
this.manifest,
|
|
50
|
-
/** @type {any} */ (this.manifest).bbox
|
|
51
|
-
);
|
|
52
|
-
this.minimumLevel = Number.isFinite(options.minimumLevel) ? Number(options.minimumLevel) : 0;
|
|
53
|
-
this.sourceMaximumLevel = Number(pmtilesInfo(this.manifest).maxZoom ?? 18);
|
|
54
|
-
const contextOverlay = contextOverlayConfig(this.manifest);
|
|
55
|
-
this.overzoomLevels = clampInteger(options.overzoomLevels ?? contextOverlay?.overzoomLevels ?? 0, 0, 4);
|
|
56
|
-
this.edgeGuardPixels = clampInteger(options.edgeGuardPixels ?? contextOverlay?.edgeGuardPixels ?? 0, 0, 8);
|
|
57
|
-
this.maximumLevel = Number.isFinite(options.maximumLevel)
|
|
58
|
-
? Number(options.maximumLevel)
|
|
59
|
-
: this.sourceMaximumLevel + this.overzoomLevels;
|
|
60
|
-
this.ready = true;
|
|
61
|
-
this.readyPromise = Promise.resolve(true);
|
|
62
|
-
this.hasAlphaChannel = true;
|
|
63
|
-
this.errorEvent = new Event();
|
|
64
|
-
this.credit = undefined;
|
|
65
|
-
this.proxy = undefined;
|
|
66
|
-
|
|
67
|
-
this.layerIds = normalizeContextLayers(options.layers ?? contextOverlay?.layers ?? DEFAULT_CONTEXT_LAYERS);
|
|
68
|
-
this.layerVisibility = new Map(this.layerIds.map((layerId) => [layerId, true]));
|
|
69
|
-
this.cache = new Map();
|
|
70
|
-
this.pending = new Map();
|
|
71
|
-
this.nextRequestId = 1;
|
|
72
|
-
this.metrics = createImageryMetrics();
|
|
73
|
-
this.worker = new Worker(
|
|
74
|
-
options.workerUrl ?? DEFAULT_WORKER_URL,
|
|
75
|
-
{ type: 'module' }
|
|
76
|
-
);
|
|
77
|
-
this.worker.addEventListener('message', (event) => this.#handleWorkerMessage(event.data));
|
|
78
|
-
this.worker.addEventListener('error', (event) => {
|
|
79
|
-
const error = new Error(event.message || 'map-zero imagery worker failed');
|
|
80
|
-
this.errorEvent.raiseEvent(error);
|
|
81
|
-
this.#rejectPending(error);
|
|
82
|
-
});
|
|
83
|
-
this.worker.postMessage({
|
|
84
|
-
type: 'init',
|
|
85
|
-
options: {
|
|
86
|
-
manifest: this.manifest,
|
|
87
|
-
manifestUrl: this.manifestUrl,
|
|
88
|
-
styleDocument: this.styleDocument,
|
|
89
|
-
layers: this.layerIds,
|
|
90
|
-
tileSize: this.tileWidth,
|
|
91
|
-
sourceMaximumLevel: this.sourceMaximumLevel,
|
|
92
|
-
overzoomLevels: this.overzoomLevels,
|
|
93
|
-
edgeGuardPixels: this.edgeGuardPixels,
|
|
94
|
-
source: String(contextOverlay?.source ?? pmtilesInfo(this.manifest).url ?? 'tiles.pmtiles')
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @param {string} layerId
|
|
101
|
-
* @param {boolean} visible
|
|
102
|
-
*/
|
|
103
|
-
setLayerVisible(layerId, visible) {
|
|
104
|
-
this.layerVisibility.set(sourceLayerFor(layerId), visible);
|
|
105
|
-
this.layerVisibility.set(layerAlias(layerId), visible);
|
|
106
|
-
this.cache.clear();
|
|
107
|
-
this.worker.postMessage({
|
|
108
|
-
type: 'visibility',
|
|
109
|
-
layerId,
|
|
110
|
-
visible
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* @param {number} x
|
|
116
|
-
* @param {number} y
|
|
117
|
-
* @param {number} level
|
|
118
|
-
* @returns {Promise<HTMLCanvasElement>}
|
|
119
|
-
*/
|
|
120
|
-
async requestImage(x, y, level) {
|
|
121
|
-
const key = `${level}/${x}/${y}`;
|
|
122
|
-
const cached = this.cache.get(key);
|
|
123
|
-
if (cached) {
|
|
124
|
-
this.metrics.cacheHits++;
|
|
125
|
-
return cached;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const id = this.nextRequestId++;
|
|
129
|
-
const promise = new Promise((resolve, reject) => {
|
|
130
|
-
this.pending.set(id, { resolve, reject });
|
|
131
|
-
this.worker.postMessage({
|
|
132
|
-
type: 'render',
|
|
133
|
-
id,
|
|
134
|
-
x,
|
|
135
|
-
y,
|
|
136
|
-
z: level
|
|
137
|
-
});
|
|
138
|
-
}).catch((error) => {
|
|
139
|
-
this.errorEvent.raiseEvent(error);
|
|
140
|
-
return emptyCanvas(this.tileWidth, this.tileHeight);
|
|
141
|
-
});
|
|
142
|
-
this.cache.set(key, promise);
|
|
143
|
-
return promise;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
getTileCredits() {
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
pickFeatures() {
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
destroy() {
|
|
155
|
-
this.#rejectPending(new Error('map-zero imagery provider destroyed'));
|
|
156
|
-
this.worker.terminate();
|
|
157
|
-
this.cache.clear();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
#handleWorkerMessage(message) {
|
|
161
|
-
if (message?.type === 'metrics') {
|
|
162
|
-
mergeWorkerMetrics(this.metrics, message.metrics);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (message?.type !== 'tile') {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (message.metrics) {
|
|
171
|
-
mergeWorkerMetrics(this.metrics, message.metrics);
|
|
172
|
-
}
|
|
173
|
-
const pending = this.pending.get(message.id);
|
|
174
|
-
if (!pending) {
|
|
175
|
-
message.image?.close?.();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
this.pending.delete(message.id);
|
|
179
|
-
if (message.error) {
|
|
180
|
-
pending.reject(new Error(message.error));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
pending.resolve(imageToCanvas(message.image, this.tileWidth, this.tileHeight));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
#rejectPending(error) {
|
|
187
|
-
for (const pending of this.pending.values()) {
|
|
188
|
-
pending.reject(error);
|
|
189
|
-
}
|
|
190
|
-
this.pending.clear();
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function assertWorkerRasterSupport() {
|
|
195
|
-
if (typeof Worker !== 'function' || typeof OffscreenCanvas !== 'function' || typeof createImageBitmap !== 'function') {
|
|
196
|
-
throw new Error('map-zero Cesium context overlay requires Worker, OffscreenCanvas, and createImageBitmap');
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function createImageryMetrics() {
|
|
201
|
-
const metrics = {
|
|
202
|
-
requested: 0,
|
|
203
|
-
cacheHits: 0,
|
|
204
|
-
decoded: 0,
|
|
205
|
-
features: 0,
|
|
206
|
-
overzoomed: 0,
|
|
207
|
-
requestLevels: {},
|
|
208
|
-
sourceLevels: {},
|
|
209
|
-
renderMs: { total: 0, max: 0 },
|
|
210
|
-
decodeMs: { total: 0, max: 0 }
|
|
211
|
-
};
|
|
212
|
-
const root = globalThis.__mapZeroCesiumMetrics ??= { imageryProviders: [] };
|
|
213
|
-
root.imageryProviders.push(metrics);
|
|
214
|
-
return metrics;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function mergeWorkerMetrics(target, patch) {
|
|
218
|
-
if (!patch) return;
|
|
219
|
-
for (const key of ['requested', 'cacheHits', 'decoded', 'features', 'overzoomed']) {
|
|
220
|
-
target[key] = Number(patch[key] ?? target[key] ?? 0);
|
|
221
|
-
}
|
|
222
|
-
target.requestLevels = { ...patch.requestLevels };
|
|
223
|
-
target.sourceLevels = { ...patch.sourceLevels };
|
|
224
|
-
target.renderMs = { ...patch.renderMs };
|
|
225
|
-
target.decodeMs = { ...patch.decodeMs };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* @param {Record<string, unknown>} manifest
|
|
230
|
-
* @returns {{ type?: string, source?: string, layers?: string[], backend?: string, overzoomLevels?: number, edgeGuardPixels?: number } | null}
|
|
231
|
-
*/
|
|
232
|
-
export function contextOverlayConfig(manifest) {
|
|
233
|
-
const pmtiles = pmtilesInfo(manifest);
|
|
234
|
-
if (!pmtiles.url) return null;
|
|
235
|
-
return {
|
|
236
|
-
type: 'client-rasterized-pmtiles',
|
|
237
|
-
source: pmtiles.url,
|
|
238
|
-
layers: contextLayerIds(manifest)
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* @param {Record<string, unknown>} manifest
|
|
244
|
-
* @returns {boolean}
|
|
245
|
-
*/
|
|
246
|
-
export function hasMapZeroContextOverlay(manifest) {
|
|
247
|
-
const overlay = contextOverlayConfig(manifest);
|
|
248
|
-
return Boolean(overlay?.source || pmtilesInfo(manifest).url);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function contextLayerIds(manifest) {
|
|
252
|
-
const layers = Array.isArray(manifest.layers)
|
|
253
|
-
? manifest.layers.map(String).filter((layerId) => layerId !== 'buildings')
|
|
254
|
-
: [];
|
|
255
|
-
return layers.length > 0 ? layers : DEFAULT_CONTEXT_LAYERS;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function rectangleFromManifestBbox(manifest, bbox) {
|
|
259
|
-
const padded = expandBboxByTileMargin(bbox, pmtilesInfo(manifest).minZoom);
|
|
260
|
-
return rectangleFromBbox(padded);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function rectangleFromBbox(bbox) {
|
|
264
|
-
if (Array.isArray(bbox) && bbox.length === 4) {
|
|
265
|
-
return Rectangle.fromDegrees(Number(bbox[0]), Number(bbox[1]), Number(bbox[2]), Number(bbox[3]));
|
|
266
|
-
}
|
|
267
|
-
return Rectangle.MAX_VALUE;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function expandBboxByTileMargin(bbox, minZoom) {
|
|
271
|
-
if (!Array.isArray(bbox) || bbox.length !== 4) return bbox;
|
|
272
|
-
const z = clampInteger(minZoom ?? 8, 0, 22);
|
|
273
|
-
const margin = 360 / 2 ** z;
|
|
274
|
-
return [
|
|
275
|
-
Math.max(-180, Number(bbox[0]) - margin),
|
|
276
|
-
Math.max(-WEB_MERCATOR_MAX_LAT, Number(bbox[1]) - margin),
|
|
277
|
-
Math.min(180, Number(bbox[2]) + margin),
|
|
278
|
-
Math.min(WEB_MERCATOR_MAX_LAT, Number(bbox[3]) + margin)
|
|
279
|
-
];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function emptyCanvas(width, height) {
|
|
283
|
-
const canvas = document.createElement('canvas');
|
|
284
|
-
canvas.width = width;
|
|
285
|
-
canvas.height = height;
|
|
286
|
-
return canvas;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function imageToCanvas(image, width, height) {
|
|
290
|
-
const canvas = emptyCanvas(width, height);
|
|
291
|
-
if (!image) {
|
|
292
|
-
return canvas;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const ctx = canvas.getContext('2d');
|
|
296
|
-
if (ctx) {
|
|
297
|
-
ctx.drawImage(image, 0, 0, width, height);
|
|
298
|
-
}
|
|
299
|
-
image.close?.();
|
|
300
|
-
return canvas;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function resolveWorkerBaseUrl(url) {
|
|
304
|
-
return new URL(url, globalThis.location?.href ?? 'http://localhost/').toString();
|
|
305
|
-
}
|