@map-zero/cesium 0.1.0 → 0.2.2

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/src/index.js CHANGED
@@ -1,7 +1,16 @@
1
1
  import {
2
+ Cesium3DTileColorBlendMode,
2
3
  Cesium3DTileStyle,
3
- Cesium3DTileset
4
+ Cesium3DTileset,
5
+ ImageryLayer
4
6
  } from 'cesium';
7
+ import {
8
+ contextOverlayConfig,
9
+ hasMapZeroContextOverlay,
10
+ MapZeroCesiumImageryProvider
11
+ } from './imagery.js';
12
+
13
+ export { MapZeroCesiumImageryProvider } from './imagery.js';
5
14
 
6
15
  let autoInstanceCounter = 0;
7
16
 
@@ -14,8 +23,7 @@ let autoInstanceCounter = 0;
14
23
  * bbox?: [number, number, number, number],
15
24
  * styles?: Record<string, string>,
16
25
  * 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 }>
26
+ * layers?: string[]
19
27
  * }} MapZeroManifest
20
28
  */
21
29
 
@@ -79,7 +87,13 @@ export async function loadMapZeroStyle(input, options = {}) {
79
87
  * manifest?: MapZeroManifest,
80
88
  * style?: string | Record<string, unknown>,
81
89
  * styleJson?: Record<string, unknown> | null,
82
- * opacity?: number
90
+ * opacity?: number,
91
+ * tilesetOpacity?: number,
92
+ * buildingsOpacity?: number,
93
+ * buildings3d?: boolean,
94
+ * tilesetMaximumScreenSpaceError?: number,
95
+ * tilesetCacheBytes?: number,
96
+ * tilesetMaximumCacheOverflowBytes?: number
83
97
  * }} options
84
98
  * @returns {Promise<{ id: string, manifest: MapZeroManifest, style: Record<string, unknown> | null, tilesets: Record<string, Cesium3DTileset> }>}
85
99
  */
@@ -95,7 +109,9 @@ export async function createMapZeroCesiumTilesets(options) {
95
109
  })
96
110
  );
97
111
 
98
- const tilesetEntries = manifestTilesetEntries(manifest);
112
+ const tilesetEntries = manifestTilesetEntries(manifest, {
113
+ buildings3d: options.buildings3d
114
+ });
99
115
  if (tilesetEntries.length === 0) {
100
116
  return {
101
117
  id: instanceId,
@@ -111,10 +127,12 @@ export async function createMapZeroCesiumTilesets(options) {
111
127
  const url = resolveRelativeUrl(entry.url, options.manifestUrl);
112
128
  const tileset = await Cesium3DTileset.fromUrl(url);
113
129
  tagCesiumTileset(tileset, instanceId, entry.layerId);
130
+ configureCesiumTilesetStreaming(tileset, entry.layerId, options);
131
+ configureCesiumTilesetColor(tileset, entry.layerId);
114
132
  tileset.style = createMapZeroCesiumStyle(styleJson, {
115
133
  layerId: entry.layerId,
116
134
  visibleLayers: new Set([entry.layerId]),
117
- opacity: options.opacity ?? 1
135
+ opacity: tilesetOpacityForLayer(entry.layerId, options)
118
136
  });
119
137
  tilesets[entry.layerId] = tileset;
120
138
  }
@@ -139,8 +157,20 @@ export async function createMapZeroCesiumTilesets(options) {
139
157
  * manifestUrl: string,
140
158
  * style?: string | Record<string, unknown>,
141
159
  * opacity?: number,
160
+ * tilesetOpacity?: number,
161
+ * buildingsOpacity?: number,
162
+ * contextOverlay?: boolean,
163
+ * contextOpacity?: number,
164
+ * contextOverzoomLevels?: number,
165
+ * contextEdgeGuardPixels?: number,
166
+ * contextWorkerUrl?: string | URL,
167
+ * buildings3d?: boolean,
168
+ * tilesetMaximumScreenSpaceError?: number,
169
+ * tilesetCacheBytes?: number,
170
+ * tilesetMaximumCacheOverflowBytes?: number,
142
171
  * zoomTo?: boolean,
143
172
  * applyDefaultSceneStyle?: boolean,
173
+ * sceneStyle?: Record<string, unknown>,
144
174
  * configureScene?: (viewer: unknown) => void
145
175
  * }} options
146
176
  * @returns {Promise<{
@@ -148,6 +178,8 @@ export async function createMapZeroCesiumTilesets(options) {
148
178
  * manifest: MapZeroManifest,
149
179
  * style: Record<string, unknown> | null,
150
180
  * tilesets: Record<string, Cesium3DTileset>,
181
+ * imageryProvider?: MapZeroCesiumImageryProvider,
182
+ * imageryLayer?: ImageryLayer,
151
183
  * setVisible: (layerId: string, visible: boolean) => void,
152
184
  * setOpacity: (layerId: string, opacity: number) => void,
153
185
  * destroy: () => void
@@ -155,7 +187,7 @@ export async function createMapZeroCesiumTilesets(options) {
155
187
  */
156
188
  export async function addMapZeroToCesium(viewer, options) {
157
189
  if (options.applyDefaultSceneStyle) {
158
- applyMapZeroCesiumSceneStyle(viewer);
190
+ applyMapZeroCesiumSceneStyle(viewer, options.sceneStyle);
159
191
  }
160
192
  if (typeof options.configureScene === 'function') {
161
193
  options.configureScene(viewer);
@@ -164,8 +196,28 @@ export async function addMapZeroToCesium(viewer, options) {
164
196
  const result = await createMapZeroCesiumTilesets(options);
165
197
  const uniqueTilesets = [...new Set(Object.values(result.tilesets))];
166
198
  const visibleLayers = new Set(Object.keys(result.tilesets));
167
- let opacity = options.opacity ?? 1;
168
-
199
+ let opacity = options.tilesetOpacity ?? options.opacity ?? 1;
200
+ const imageryProvider = shouldCreateContextOverlay(result.manifest, options)
201
+ ? new MapZeroCesiumImageryProvider({
202
+ manifest: result.manifest,
203
+ manifestUrl: options.manifestUrl,
204
+ styleDocument: result.style,
205
+ layers: contextOverlayConfig(result.manifest)?.layers,
206
+ overzoomLevels: options.contextOverzoomLevels,
207
+ edgeGuardPixels: options.contextEdgeGuardPixels,
208
+ workerUrl: options.contextWorkerUrl
209
+ })
210
+ : undefined;
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);
220
+ }
169
221
  for (const tileset of uniqueTilesets) {
170
222
  viewer.scene.primitives.add(tileset);
171
223
  }
@@ -181,6 +233,8 @@ export async function addMapZeroToCesium(viewer, options) {
181
233
  id: result.id,
182
234
  style: result.style,
183
235
  tilesets: result.tilesets,
236
+ imageryProvider,
237
+ imageryLayer,
184
238
  setVisible(layerId, visible) {
185
239
  const tileset = result.tilesets[layerId];
186
240
  if (tileset) {
@@ -194,6 +248,9 @@ export async function addMapZeroToCesium(viewer, options) {
194
248
  visibleLayers
195
249
  });
196
250
  }
251
+ imageryProvider?.setLayerVisible(layerId, visible);
252
+ imageryProvider?.setLayerVisible(layerId === 'aviation' ? 'aip' : layerId, visible);
253
+ viewer.scene?.requestRender?.();
197
254
  },
198
255
  setOpacity(layerId, nextOpacity) {
199
256
  if (!result.tilesets[layerId]) return;
@@ -204,6 +261,9 @@ export async function addMapZeroToCesium(viewer, options) {
204
261
  });
205
262
  },
206
263
  destroy() {
264
+ if (imageryLayer) {
265
+ viewer.imageryLayers?.remove(imageryLayer, true);
266
+ }
207
267
  for (const tileset of uniqueTilesets) {
208
268
  viewer.scene.primitives.remove(tileset);
209
269
  }
@@ -219,25 +279,29 @@ export async function addMapZeroToCesium(viewer, options) {
219
279
  * black-background tactical look.
220
280
  *
221
281
  * @param {any} viewer
282
+ * @param {Record<string, unknown>} [options]
222
283
  */
223
- export function applyMapZeroCesiumSceneStyle(viewer) {
284
+ export function applyMapZeroCesiumSceneStyle(viewer, options = {}) {
224
285
  const Cesium = globalThis.Cesium;
225
286
  const scene = viewer?.scene;
226
287
  if (!scene || !Cesium) {
227
288
  return;
228
289
  }
229
290
 
230
- scene.backgroundColor = Cesium.Color.BLACK;
291
+ const backgroundColor = colorFromOption(Cesium, options.backgroundColor, Cesium.Color.BLACK);
292
+ const globeBaseColor = colorFromOption(Cesium, options.globeBaseColor, backgroundColor);
293
+ scene.backgroundColor = backgroundColor;
231
294
  if (scene.globe) {
232
- scene.globe.baseColor = Cesium.Color.BLACK;
233
- scene.globe.enableLighting = false;
234
- scene.globe.depthTestAgainstTerrain = false;
295
+ scene.globe.baseColor = globeBaseColor;
296
+ scene.globe.enableLighting = Boolean(options.enableLighting ?? false);
297
+ scene.globe.depthTestAgainstTerrain = Boolean(options.depthTestAgainstTerrain ?? false);
235
298
  }
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;
299
+ if (scene.fog) scene.fog.enabled = Boolean(options.fog ?? false);
300
+ if (scene.skyBox) scene.skyBox.show = Boolean(options.skyBox ?? false);
301
+ if (scene.sun) scene.sun.show = Boolean(options.sun ?? false);
302
+ if (scene.moon) scene.moon.show = Boolean(options.moon ?? false);
303
+ if (scene.skyAtmosphere) scene.skyAtmosphere.show = Boolean(options.skyAtmosphere ?? false);
304
+ scene.requestRender?.();
241
305
  }
242
306
 
243
307
  /**
@@ -267,8 +331,8 @@ export function createMapZeroCesiumStyle(styleJson, options) {
267
331
  * Pick a single material color from a map-zero style rule.
268
332
  *
269
333
  * 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.
334
+ * Cesium material cannot show that outline, so building solids use the fill:
335
+ * it keeps the mass quiet while avoiding translucent sorting artifacts.
272
336
  *
273
337
  * @param {Record<string, any> | null} rule
274
338
  * @param {string} layerId
@@ -277,8 +341,8 @@ export function createMapZeroCesiumStyle(styleJson, options) {
277
341
  function cesiumLayerMaterial(rule, layerId) {
278
342
  if (layerId === 'buildings') {
279
343
  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))
344
+ color: buildingSolidColor(rule),
345
+ opacity: 1
282
346
  };
283
347
  }
284
348
 
@@ -303,6 +367,21 @@ function applyStyleToTilesetMap(tilesets, style, options) {
303
367
  }
304
368
  }
305
369
 
370
+ /**
371
+ * @param {string} layerId
372
+ * @param {{ opacity?: number, tilesetOpacity?: number, buildingsOpacity?: number }} options
373
+ * @returns {number}
374
+ */
375
+ function tilesetOpacityForLayer(layerId, options) {
376
+ if (layerId === 'buildings' && Number.isFinite(Number(options.buildingsOpacity))) {
377
+ return Number(options.buildingsOpacity);
378
+ }
379
+ if (Number.isFinite(Number(options.tilesetOpacity))) {
380
+ return Number(options.tilesetOpacity);
381
+ }
382
+ return Number(options.opacity ?? 1);
383
+ }
384
+
306
385
  /**
307
386
  * @param {Record<string, unknown> | null} styleJson
308
387
  * @param {string} layerId
@@ -315,29 +394,88 @@ function layerStyle(styleJson, layerId) {
315
394
 
316
395
  /**
317
396
  * @param {MapZeroManifest} manifest
397
+ * @param {{ buildings3d?: boolean }} [options]
318
398
  * @returns {Array<{ layerId: string, url: string }>}
319
399
  */
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
-
400
+ function manifestTilesetEntries(manifest, options = {}) {
328
401
  if (manifest.tiles3d?.format === '3dtiles' && typeof manifest.tiles3d.url === 'string') {
329
402
  const layers = Array.isArray(manifest.tiles3d.layers) && manifest.tiles3d.layers.length > 0
330
403
  ? manifest.tiles3d.layers.map(String)
331
404
  : ['buildings'];
332
- return layers.map((layerId) => ({
333
- layerId,
334
- url: /** @type {string} */ (manifest.tiles3d?.url)
335
- }));
405
+ return layers
406
+ .filter((layerId) => isAllowedCesiumTilesetLayer(layerId, options))
407
+ .map((layerId) => ({
408
+ layerId,
409
+ url: /** @type {string} */ (manifest.tiles3d?.url)
410
+ }));
336
411
  }
337
412
 
338
413
  return [];
339
414
  }
340
415
 
416
+ /**
417
+ * @param {string} layerId
418
+ * @param {{ buildings3d?: boolean }} options
419
+ * @returns {boolean}
420
+ */
421
+ function isAllowedCesiumTilesetLayer(layerId, options) {
422
+ return layerId !== 'buildings' || options.buildings3d !== false;
423
+ }
424
+
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
+
437
+ /**
438
+ * @param {Cesium3DTileset} tileset
439
+ * @param {string} layerId
440
+ * @param {{ tilesetMaximumScreenSpaceError?: number, tilesetCacheBytes?: number, tilesetMaximumCacheOverflowBytes?: number }} options
441
+ */
442
+ function configureCesiumTilesetStreaming(tileset, layerId, options = {}) {
443
+ if (layerId !== 'buildings') {
444
+ return;
445
+ }
446
+
447
+ tileset.maximumScreenSpaceError = finiteNumber(options.tilesetMaximumScreenSpaceError, 24);
448
+ tileset.skipLevelOfDetail = true;
449
+ tileset.baseScreenSpaceError = 1024;
450
+ tileset.skipScreenSpaceErrorFactor = 16;
451
+ tileset.skipLevels = 1;
452
+ tileset.immediatelyLoadDesiredLevelOfDetail = false;
453
+ tileset.loadSiblings = false;
454
+ tileset.cullWithChildrenBounds = true;
455
+ tileset.dynamicScreenSpaceError = true;
456
+ tileset.dynamicScreenSpaceErrorDensity = 0.00278;
457
+ tileset.dynamicScreenSpaceErrorFactor = 4;
458
+ tileset.preloadWhenHidden = false;
459
+ tileset.preloadFlightDestinations = false;
460
+ tileset.cacheBytes = finiteNumber(options.tilesetCacheBytes, 768 * 1024 * 1024);
461
+ tileset.maximumCacheOverflowBytes = finiteNumber(options.tilesetMaximumCacheOverflowBytes, 512 * 1024 * 1024);
462
+ }
463
+
464
+ /**
465
+ * @param {Cesium3DTileset} tileset
466
+ * @param {string} layerId
467
+ */
468
+ function configureCesiumTilesetColor(tileset, layerId) {
469
+ if (layerId === 'buildings') {
470
+ tileset.backFaceCulling = false;
471
+ tileset.colorBlendMode = Cesium3DTileColorBlendMode.MIX;
472
+ tileset.colorBlendAmount = 0.45;
473
+ return;
474
+ }
475
+ tileset.colorBlendMode = Cesium3DTileColorBlendMode.REPLACE;
476
+ tileset.colorBlendAmount = 1;
477
+ }
478
+
341
479
  /**
342
480
  * @param {string | undefined} id
343
481
  * @param {MapZeroManifest} manifest
@@ -395,6 +533,39 @@ function safeCssColor(color) {
395
533
  return /^#[0-9a-f]{6}$/i.test(color) ? color : '#ff00ff';
396
534
  }
397
535
 
536
+ /**
537
+ * @param {Record<string, any> | null} rule
538
+ * @returns {string}
539
+ */
540
+ function buildingSolidColor(rule) {
541
+ const explicit = rule?.cesium?.color ?? rule?.tiles3d?.color ?? rule?.material?.color;
542
+ if (typeof explicit === 'string' && isHexColor(explicit)) {
543
+ return explicit;
544
+ }
545
+ return '#8a3f82';
546
+ }
547
+
548
+ /**
549
+ * @param {string} value
550
+ * @returns {boolean}
551
+ */
552
+ function isHexColor(value) {
553
+ return /^#[0-9a-f]{6}$/i.test(value);
554
+ }
555
+
556
+ /**
557
+ * @param {any} Cesium
558
+ * @param {unknown} value
559
+ * @param {any} fallback
560
+ * @returns {any}
561
+ */
562
+ function colorFromOption(Cesium, value, fallback) {
563
+ if (typeof value !== 'string') {
564
+ return fallback;
565
+ }
566
+ return Cesium.Color.fromCssColorString(value) ?? fallback;
567
+ }
568
+
398
569
 
399
570
  /**
400
571
  * @param {number} value
@@ -403,3 +574,13 @@ function safeCssColor(color) {
403
574
  function clamp01(value) {
404
575
  return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 1));
405
576
  }
577
+
578
+ /**
579
+ * @param {unknown} value
580
+ * @param {number} fallback
581
+ * @returns {number}
582
+ */
583
+ function finiteNumber(value, fallback) {
584
+ const number = Number(value);
585
+ return Number.isFinite(number) ? number : fallback;
586
+ }