@map-zero/cesium 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/package.json +13 -0
- package/src/index.js +405 -0
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@map-zero/cesium",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Cesium integration helper for map-zero 3D Tiles packages.",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"cesium": "^1.141.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Cesium3DTileStyle,
|
|
3
|
+
Cesium3DTileset
|
|
4
|
+
} from 'cesium';
|
|
5
|
+
|
|
6
|
+
let autoInstanceCounter = 0;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{
|
|
10
|
+
* id?: string,
|
|
11
|
+
* format?: string,
|
|
12
|
+
* version?: number,
|
|
13
|
+
* name?: string,
|
|
14
|
+
* bbox?: [number, number, number, number],
|
|
15
|
+
* styles?: Record<string, string>,
|
|
16
|
+
* tiles3d?: { format?: string, url?: string, layers?: string[] },
|
|
17
|
+
* cesium?: { tilesets?: Record<string, string>, bbox?: [number, number, number, number], focusBbox?: [number, number, number, number] },
|
|
18
|
+
* layers?: Array<{ id: string, table?: string, style?: string }>
|
|
19
|
+
* }} MapZeroManifest
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load a map-zero manifest.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} manifestUrl
|
|
26
|
+
* @returns {Promise<MapZeroManifest>}
|
|
27
|
+
*/
|
|
28
|
+
export async function loadMapZeroManifest(manifestUrl) {
|
|
29
|
+
const response = await fetch(manifestUrl);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`failed to load map-zero manifest: ${response.status}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return response.json();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load a style document.
|
|
39
|
+
*
|
|
40
|
+
* Supported forms:
|
|
41
|
+
* - loadMapZeroStyle('./styles/neon-dark.json')
|
|
42
|
+
* - loadMapZeroStyle(manifest, { manifestUrl, style: 'default' })
|
|
43
|
+
*
|
|
44
|
+
* @param {string | MapZeroManifest} input
|
|
45
|
+
* @param {{ manifestUrl?: string, style?: string }} [options]
|
|
46
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
47
|
+
*/
|
|
48
|
+
export async function loadMapZeroStyle(input, options = {}) {
|
|
49
|
+
if (typeof input === 'string') {
|
|
50
|
+
const response = await fetch(resolveRelativeUrl(input, globalThis.location?.href ?? 'http://localhost/'));
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`failed to load map-zero style: ${response.status}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return response.json();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const manifest = input;
|
|
59
|
+
const key = options.style ?? 'default';
|
|
60
|
+
const stylePath = manifest.styles?.[key] ?? manifest.styles?.default;
|
|
61
|
+
if (!stylePath) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const response = await fetch(resolveRelativeUrl(stylePath, options.manifestUrl ?? globalThis.location?.href ?? 'http://localhost/'));
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`failed to load map-zero style: ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return response.json();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create Cesium 3D Tiles primitives for a map-zero package.
|
|
75
|
+
*
|
|
76
|
+
* @param {{
|
|
77
|
+
* id?: string,
|
|
78
|
+
* manifestUrl: string,
|
|
79
|
+
* manifest?: MapZeroManifest,
|
|
80
|
+
* style?: string | Record<string, unknown>,
|
|
81
|
+
* styleJson?: Record<string, unknown> | null,
|
|
82
|
+
* opacity?: number
|
|
83
|
+
* }} options
|
|
84
|
+
* @returns {Promise<{ id: string, manifest: MapZeroManifest, style: Record<string, unknown> | null, tilesets: Record<string, Cesium3DTileset> }>}
|
|
85
|
+
*/
|
|
86
|
+
export async function createMapZeroCesiumTilesets(options) {
|
|
87
|
+
const manifest = options.manifest ?? await loadMapZeroManifest(options.manifestUrl);
|
|
88
|
+
const instanceId = createInstanceId(options.id, manifest, options.manifestUrl);
|
|
89
|
+
const styleJson = options.styleJson ?? (
|
|
90
|
+
options.style && typeof options.style === 'object'
|
|
91
|
+
? options.style
|
|
92
|
+
: await loadMapZeroStyle(manifest, {
|
|
93
|
+
manifestUrl: options.manifestUrl,
|
|
94
|
+
style: typeof options.style === 'string' ? options.style : undefined
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const tilesetEntries = manifestTilesetEntries(manifest);
|
|
99
|
+
if (tilesetEntries.length === 0) {
|
|
100
|
+
return {
|
|
101
|
+
id: instanceId,
|
|
102
|
+
manifest,
|
|
103
|
+
style: styleJson,
|
|
104
|
+
tilesets: {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @type {Record<string, Cesium3DTileset>} */
|
|
109
|
+
const tilesets = {};
|
|
110
|
+
for (const entry of tilesetEntries) {
|
|
111
|
+
const url = resolveRelativeUrl(entry.url, options.manifestUrl);
|
|
112
|
+
const tileset = await Cesium3DTileset.fromUrl(url);
|
|
113
|
+
tagCesiumTileset(tileset, instanceId, entry.layerId);
|
|
114
|
+
tileset.style = createMapZeroCesiumStyle(styleJson, {
|
|
115
|
+
layerId: entry.layerId,
|
|
116
|
+
visibleLayers: new Set([entry.layerId]),
|
|
117
|
+
opacity: options.opacity ?? 1
|
|
118
|
+
});
|
|
119
|
+
tilesets[entry.layerId] = tileset;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id: instanceId,
|
|
124
|
+
manifest,
|
|
125
|
+
style: styleJson,
|
|
126
|
+
tilesets
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Add map-zero 3D Tiles to an existing Cesium Viewer.
|
|
132
|
+
*
|
|
133
|
+
* The helper does not create or own the viewer. It only adds map-zero
|
|
134
|
+
* primitives and returns a small controller.
|
|
135
|
+
*
|
|
136
|
+
* @param {{ scene: { primitives: { add: (primitive: unknown) => unknown, remove: (primitive: unknown) => boolean } } }} viewer
|
|
137
|
+
* @param {{
|
|
138
|
+
* id?: string,
|
|
139
|
+
* manifestUrl: string,
|
|
140
|
+
* style?: string | Record<string, unknown>,
|
|
141
|
+
* opacity?: number,
|
|
142
|
+
* zoomTo?: boolean,
|
|
143
|
+
* applyDefaultSceneStyle?: boolean,
|
|
144
|
+
* configureScene?: (viewer: unknown) => void
|
|
145
|
+
* }} options
|
|
146
|
+
* @returns {Promise<{
|
|
147
|
+
* id: string,
|
|
148
|
+
* manifest: MapZeroManifest,
|
|
149
|
+
* style: Record<string, unknown> | null,
|
|
150
|
+
* tilesets: Record<string, Cesium3DTileset>,
|
|
151
|
+
* setVisible: (layerId: string, visible: boolean) => void,
|
|
152
|
+
* setOpacity: (layerId: string, opacity: number) => void,
|
|
153
|
+
* destroy: () => void
|
|
154
|
+
* }>}
|
|
155
|
+
*/
|
|
156
|
+
export async function addMapZeroToCesium(viewer, options) {
|
|
157
|
+
if (options.applyDefaultSceneStyle) {
|
|
158
|
+
applyMapZeroCesiumSceneStyle(viewer);
|
|
159
|
+
}
|
|
160
|
+
if (typeof options.configureScene === 'function') {
|
|
161
|
+
options.configureScene(viewer);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = await createMapZeroCesiumTilesets(options);
|
|
165
|
+
const uniqueTilesets = [...new Set(Object.values(result.tilesets))];
|
|
166
|
+
const visibleLayers = new Set(Object.keys(result.tilesets));
|
|
167
|
+
let opacity = options.opacity ?? 1;
|
|
168
|
+
|
|
169
|
+
for (const tileset of uniqueTilesets) {
|
|
170
|
+
viewer.scene.primitives.add(tileset);
|
|
171
|
+
}
|
|
172
|
+
if (options.zoomTo !== false && typeof viewer.zoomTo === 'function') {
|
|
173
|
+
const firstTileset = uniqueTilesets[0];
|
|
174
|
+
if (firstTileset) {
|
|
175
|
+
await viewer.zoomTo(firstTileset);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
manifest: result.manifest,
|
|
181
|
+
id: result.id,
|
|
182
|
+
style: result.style,
|
|
183
|
+
tilesets: result.tilesets,
|
|
184
|
+
setVisible(layerId, visible) {
|
|
185
|
+
const tileset = result.tilesets[layerId];
|
|
186
|
+
if (tileset) {
|
|
187
|
+
if (visible) {
|
|
188
|
+
visibleLayers.add(layerId);
|
|
189
|
+
} else {
|
|
190
|
+
visibleLayers.delete(layerId);
|
|
191
|
+
}
|
|
192
|
+
applyStyleToTilesetMap(result.tilesets, result.style, {
|
|
193
|
+
opacity,
|
|
194
|
+
visibleLayers
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
setOpacity(layerId, nextOpacity) {
|
|
199
|
+
if (!result.tilesets[layerId]) return;
|
|
200
|
+
opacity = clamp01(Number(nextOpacity));
|
|
201
|
+
applyStyleToTilesetMap(result.tilesets, result.style, {
|
|
202
|
+
opacity,
|
|
203
|
+
visibleLayers
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
destroy() {
|
|
207
|
+
for (const tileset of uniqueTilesets) {
|
|
208
|
+
viewer.scene.primitives.remove(tileset);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Apply optional tactical scene defaults for the built-in map-zero viewer.
|
|
216
|
+
*
|
|
217
|
+
* This is intentionally opt-in. External applications should keep full control
|
|
218
|
+
* of their Cesium Viewer and call this helper only when they want the map-zero
|
|
219
|
+
* black-background tactical look.
|
|
220
|
+
*
|
|
221
|
+
* @param {any} viewer
|
|
222
|
+
*/
|
|
223
|
+
export function applyMapZeroCesiumSceneStyle(viewer) {
|
|
224
|
+
const Cesium = globalThis.Cesium;
|
|
225
|
+
const scene = viewer?.scene;
|
|
226
|
+
if (!scene || !Cesium) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
scene.backgroundColor = Cesium.Color.BLACK;
|
|
231
|
+
if (scene.globe) {
|
|
232
|
+
scene.globe.baseColor = Cesium.Color.BLACK;
|
|
233
|
+
scene.globe.enableLighting = false;
|
|
234
|
+
scene.globe.depthTestAgainstTerrain = false;
|
|
235
|
+
}
|
|
236
|
+
if (scene.fog) scene.fog.enabled = false;
|
|
237
|
+
if (scene.skyBox) scene.skyBox.show = false;
|
|
238
|
+
if (scene.sun) scene.sun.show = false;
|
|
239
|
+
if (scene.moon) scene.moon.show = false;
|
|
240
|
+
if (scene.skyAtmosphere) scene.skyAtmosphere.show = false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Convert a map-zero layer style to a Cesium 3D Tiles style.
|
|
245
|
+
*
|
|
246
|
+
* @param {Record<string, unknown> | null} styleJson
|
|
247
|
+
* @param {{ layerId: string, opacity?: number, visibleLayers?: Set<string> }} options
|
|
248
|
+
* @returns {Cesium3DTileStyle}
|
|
249
|
+
*/
|
|
250
|
+
export function createMapZeroCesiumStyle(styleJson, options) {
|
|
251
|
+
const visibleLayers = options.visibleLayers ?? new Set([options.layerId]);
|
|
252
|
+
if (!visibleLayers.has(options.layerId)) {
|
|
253
|
+
return new Cesium3DTileStyle({
|
|
254
|
+
show: false
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const rule = layerStyle(styleJson, options.layerId);
|
|
259
|
+
const { color, opacity } = cesiumLayerMaterial(rule, options.layerId);
|
|
260
|
+
return new Cesium3DTileStyle({
|
|
261
|
+
color: `color('${safeCssColor(color)}', ${clamp01(Number(options.opacity ?? 1) * opacity).toFixed(3)})`,
|
|
262
|
+
show: true
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Pick a single material color from a map-zero style rule.
|
|
268
|
+
*
|
|
269
|
+
* In 2D, buildings commonly use a dark fill plus a bright stroke. A single
|
|
270
|
+
* Cesium material cannot show that outline, so building solids use the body or
|
|
271
|
+
* stroke color instead of the dark fill.
|
|
272
|
+
*
|
|
273
|
+
* @param {Record<string, any> | null} rule
|
|
274
|
+
* @param {string} layerId
|
|
275
|
+
* @returns {{ color: string, opacity: number }}
|
|
276
|
+
*/
|
|
277
|
+
function cesiumLayerMaterial(rule, layerId) {
|
|
278
|
+
if (layerId === 'buildings') {
|
|
279
|
+
return {
|
|
280
|
+
color: String(rule?.body?.color ?? rule?.stroke ?? rule?.fill ?? '#ff00ff'),
|
|
281
|
+
opacity: clamp01(Number(rule?.body?.opacity ?? rule?.strokeOpacity ?? rule?.fillOpacity ?? 0.8))
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
color: String(rule?.fill ?? rule?.body?.color ?? rule?.stroke ?? '#00ffff'),
|
|
287
|
+
opacity: clamp01(Number(rule?.fillOpacity ?? rule?.body?.opacity ?? rule?.strokeOpacity ?? 0.8))
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @param {Record<string, Cesium3DTileset>} tilesets
|
|
293
|
+
* @param {Record<string, unknown> | null} style
|
|
294
|
+
* @param {{ opacity: number, visibleLayers: Set<string> }} options
|
|
295
|
+
*/
|
|
296
|
+
function applyStyleToTilesetMap(tilesets, style, options) {
|
|
297
|
+
for (const [layerId, tileset] of Object.entries(tilesets)) {
|
|
298
|
+
tileset.style = createMapZeroCesiumStyle(style, {
|
|
299
|
+
layerId,
|
|
300
|
+
opacity: options.opacity,
|
|
301
|
+
visibleLayers: options.visibleLayers
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {Record<string, unknown> | null} styleJson
|
|
308
|
+
* @param {string} layerId
|
|
309
|
+
* @returns {Record<string, any> | null}
|
|
310
|
+
*/
|
|
311
|
+
function layerStyle(styleJson, layerId) {
|
|
312
|
+
const layers = /** @type {{ layers?: Record<string, unknown> } | null} */ (styleJson)?.layers;
|
|
313
|
+
return /** @type {Record<string, any> | null} */ (layers?.[layerId] ?? null);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {MapZeroManifest} manifest
|
|
318
|
+
* @returns {Array<{ layerId: string, url: string }>}
|
|
319
|
+
*/
|
|
320
|
+
function manifestTilesetEntries(manifest) {
|
|
321
|
+
const cesiumTilesets = manifest.cesium?.tilesets;
|
|
322
|
+
if (cesiumTilesets && typeof cesiumTilesets === 'object') {
|
|
323
|
+
return Object.entries(cesiumTilesets)
|
|
324
|
+
.filter(([, url]) => typeof url === 'string' && url.length > 0)
|
|
325
|
+
.map(([layerId, url]) => ({ layerId, url }));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (manifest.tiles3d?.format === '3dtiles' && typeof manifest.tiles3d.url === 'string') {
|
|
329
|
+
const layers = Array.isArray(manifest.tiles3d.layers) && manifest.tiles3d.layers.length > 0
|
|
330
|
+
? manifest.tiles3d.layers.map(String)
|
|
331
|
+
: ['buildings'];
|
|
332
|
+
return layers.map((layerId) => ({
|
|
333
|
+
layerId,
|
|
334
|
+
url: /** @type {string} */ (manifest.tiles3d?.url)
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @param {string | undefined} id
|
|
343
|
+
* @param {MapZeroManifest} manifest
|
|
344
|
+
* @param {string} manifestUrl
|
|
345
|
+
* @returns {string}
|
|
346
|
+
*/
|
|
347
|
+
function createInstanceId(id, manifest, manifestUrl) {
|
|
348
|
+
if (id) {
|
|
349
|
+
return safeInstanceId(id);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const name = typeof manifest.name === 'string' && manifest.name.trim()
|
|
353
|
+
? manifest.name
|
|
354
|
+
: new URL(manifestUrl, globalThis.location?.href ?? 'http://localhost/').pathname.split('/').filter(Boolean).at(-2) ?? 'mapzero';
|
|
355
|
+
autoInstanceCounter += 1;
|
|
356
|
+
return `${safeInstanceId(name)}-${autoInstanceCounter}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {string} id
|
|
361
|
+
* @returns {string}
|
|
362
|
+
*/
|
|
363
|
+
function safeInstanceId(id) {
|
|
364
|
+
return id.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'mapzero';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @param {Cesium3DTileset} tileset
|
|
369
|
+
* @param {string} instanceId
|
|
370
|
+
* @param {string} layerId
|
|
371
|
+
*/
|
|
372
|
+
function tagCesiumTileset(tileset, instanceId, layerId) {
|
|
373
|
+
tileset.mapZero = {
|
|
374
|
+
id: instanceId,
|
|
375
|
+
layerId,
|
|
376
|
+
namespacedLayerId: `${instanceId}:${layerId}`
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @param {string} path
|
|
382
|
+
* @param {string} baseUrl
|
|
383
|
+
* @returns {string}
|
|
384
|
+
*/
|
|
385
|
+
function resolveRelativeUrl(path, baseUrl) {
|
|
386
|
+
const absoluteBase = new URL(baseUrl, globalThis.location?.href ?? 'http://localhost/').href;
|
|
387
|
+
return new URL(path, absoluteBase).toString();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {string} color
|
|
392
|
+
* @returns {string}
|
|
393
|
+
*/
|
|
394
|
+
function safeCssColor(color) {
|
|
395
|
+
return /^#[0-9a-f]{6}$/i.test(color) ? color : '#ff00ff';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* @param {number} value
|
|
401
|
+
* @returns {number}
|
|
402
|
+
*/
|
|
403
|
+
function clamp01(value) {
|
|
404
|
+
return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 1));
|
|
405
|
+
}
|