map-zero 0.1.0 → 0.2.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.
@@ -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,10 @@ 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
83
94
  * }} options
84
95
  * @returns {Promise<{ id: string, manifest: MapZeroManifest, style: Record<string, unknown> | null, tilesets: Record<string, Cesium3DTileset> }>}
85
96
  */
@@ -95,7 +106,9 @@ export async function createMapZeroCesiumTilesets(options) {
95
106
  })
96
107
  );
97
108
 
98
- const tilesetEntries = manifestTilesetEntries(manifest);
109
+ const tilesetEntries = manifestTilesetEntries(manifest, {
110
+ buildings3d: options.buildings3d
111
+ });
99
112
  if (tilesetEntries.length === 0) {
100
113
  return {
101
114
  id: instanceId,
@@ -111,10 +124,12 @@ export async function createMapZeroCesiumTilesets(options) {
111
124
  const url = resolveRelativeUrl(entry.url, options.manifestUrl);
112
125
  const tileset = await Cesium3DTileset.fromUrl(url);
113
126
  tagCesiumTileset(tileset, instanceId, entry.layerId);
127
+ configureCesiumTilesetStreaming(tileset, entry.layerId);
128
+ configureCesiumTilesetColor(tileset, entry.layerId);
114
129
  tileset.style = createMapZeroCesiumStyle(styleJson, {
115
130
  layerId: entry.layerId,
116
131
  visibleLayers: new Set([entry.layerId]),
117
- opacity: options.opacity ?? 1
132
+ opacity: tilesetOpacityForLayer(entry.layerId, options)
118
133
  });
119
134
  tilesets[entry.layerId] = tileset;
120
135
  }
@@ -139,8 +154,17 @@ export async function createMapZeroCesiumTilesets(options) {
139
154
  * manifestUrl: string,
140
155
  * style?: string | Record<string, unknown>,
141
156
  * opacity?: number,
157
+ * tilesetOpacity?: number,
158
+ * buildingsOpacity?: number,
159
+ * contextOverlay?: boolean,
160
+ * contextOpacity?: number,
161
+ * contextOverzoomLevels?: number,
162
+ * contextEdgeGuardPixels?: number,
163
+ * contextWorkerUrl?: string | URL,
164
+ * buildings3d?: boolean,
142
165
  * zoomTo?: boolean,
143
166
  * applyDefaultSceneStyle?: boolean,
167
+ * sceneStyle?: Record<string, unknown>,
144
168
  * configureScene?: (viewer: unknown) => void
145
169
  * }} options
146
170
  * @returns {Promise<{
@@ -148,6 +172,8 @@ export async function createMapZeroCesiumTilesets(options) {
148
172
  * manifest: MapZeroManifest,
149
173
  * style: Record<string, unknown> | null,
150
174
  * tilesets: Record<string, Cesium3DTileset>,
175
+ * imageryProvider?: MapZeroCesiumImageryProvider,
176
+ * imageryLayer?: ImageryLayer,
151
177
  * setVisible: (layerId: string, visible: boolean) => void,
152
178
  * setOpacity: (layerId: string, opacity: number) => void,
153
179
  * destroy: () => void
@@ -155,7 +181,7 @@ export async function createMapZeroCesiumTilesets(options) {
155
181
  */
156
182
  export async function addMapZeroToCesium(viewer, options) {
157
183
  if (options.applyDefaultSceneStyle) {
158
- applyMapZeroCesiumSceneStyle(viewer);
184
+ applyMapZeroCesiumSceneStyle(viewer, options.sceneStyle);
159
185
  }
160
186
  if (typeof options.configureScene === 'function') {
161
187
  options.configureScene(viewer);
@@ -164,8 +190,28 @@ export async function addMapZeroToCesium(viewer, options) {
164
190
  const result = await createMapZeroCesiumTilesets(options);
165
191
  const uniqueTilesets = [...new Set(Object.values(result.tilesets))];
166
192
  const visibleLayers = new Set(Object.keys(result.tilesets));
167
- let opacity = options.opacity ?? 1;
168
-
193
+ let opacity = options.tilesetOpacity ?? options.opacity ?? 1;
194
+ const imageryProvider = shouldCreateContextOverlay(result.manifest, options)
195
+ ? new MapZeroCesiumImageryProvider({
196
+ manifest: result.manifest,
197
+ manifestUrl: options.manifestUrl,
198
+ styleDocument: result.style,
199
+ layers: contextOverlayConfig(result.manifest)?.layers,
200
+ overzoomLevels: options.contextOverzoomLevels,
201
+ edgeGuardPixels: options.contextEdgeGuardPixels,
202
+ workerUrl: options.contextWorkerUrl
203
+ })
204
+ : undefined;
205
+ const imageryLayer = imageryProvider
206
+ ? new ImageryLayer(imageryProvider, {
207
+ alpha: clamp01(Number(options.contextOpacity ?? options.opacity ?? 1)),
208
+ show: true
209
+ })
210
+ : undefined;
211
+
212
+ if (imageryLayer) {
213
+ viewer.imageryLayers?.add(imageryLayer);
214
+ }
169
215
  for (const tileset of uniqueTilesets) {
170
216
  viewer.scene.primitives.add(tileset);
171
217
  }
@@ -181,6 +227,8 @@ export async function addMapZeroToCesium(viewer, options) {
181
227
  id: result.id,
182
228
  style: result.style,
183
229
  tilesets: result.tilesets,
230
+ imageryProvider,
231
+ imageryLayer,
184
232
  setVisible(layerId, visible) {
185
233
  const tileset = result.tilesets[layerId];
186
234
  if (tileset) {
@@ -194,6 +242,9 @@ export async function addMapZeroToCesium(viewer, options) {
194
242
  visibleLayers
195
243
  });
196
244
  }
245
+ imageryProvider?.setLayerVisible(layerId, visible);
246
+ imageryProvider?.setLayerVisible(layerId === 'aviation' ? 'aip' : layerId, visible);
247
+ viewer.scene?.requestRender?.();
197
248
  },
198
249
  setOpacity(layerId, nextOpacity) {
199
250
  if (!result.tilesets[layerId]) return;
@@ -204,6 +255,9 @@ export async function addMapZeroToCesium(viewer, options) {
204
255
  });
205
256
  },
206
257
  destroy() {
258
+ if (imageryLayer) {
259
+ viewer.imageryLayers?.remove(imageryLayer, true);
260
+ }
207
261
  for (const tileset of uniqueTilesets) {
208
262
  viewer.scene.primitives.remove(tileset);
209
263
  }
@@ -219,25 +273,29 @@ export async function addMapZeroToCesium(viewer, options) {
219
273
  * black-background tactical look.
220
274
  *
221
275
  * @param {any} viewer
276
+ * @param {Record<string, unknown>} [options]
222
277
  */
223
- export function applyMapZeroCesiumSceneStyle(viewer) {
278
+ export function applyMapZeroCesiumSceneStyle(viewer, options = {}) {
224
279
  const Cesium = globalThis.Cesium;
225
280
  const scene = viewer?.scene;
226
281
  if (!scene || !Cesium) {
227
282
  return;
228
283
  }
229
284
 
230
- scene.backgroundColor = Cesium.Color.BLACK;
285
+ const backgroundColor = colorFromOption(Cesium, options.backgroundColor, Cesium.Color.BLACK);
286
+ const globeBaseColor = colorFromOption(Cesium, options.globeBaseColor, backgroundColor);
287
+ scene.backgroundColor = backgroundColor;
231
288
  if (scene.globe) {
232
- scene.globe.baseColor = Cesium.Color.BLACK;
233
- scene.globe.enableLighting = false;
234
- scene.globe.depthTestAgainstTerrain = false;
289
+ scene.globe.baseColor = globeBaseColor;
290
+ scene.globe.enableLighting = Boolean(options.enableLighting ?? false);
291
+ scene.globe.depthTestAgainstTerrain = Boolean(options.depthTestAgainstTerrain ?? false);
235
292
  }
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;
293
+ if (scene.fog) scene.fog.enabled = Boolean(options.fog ?? false);
294
+ if (scene.skyBox) scene.skyBox.show = Boolean(options.skyBox ?? false);
295
+ if (scene.sun) scene.sun.show = Boolean(options.sun ?? false);
296
+ if (scene.moon) scene.moon.show = Boolean(options.moon ?? false);
297
+ if (scene.skyAtmosphere) scene.skyAtmosphere.show = Boolean(options.skyAtmosphere ?? false);
298
+ scene.requestRender?.();
241
299
  }
242
300
 
243
301
  /**
@@ -267,8 +325,8 @@ export function createMapZeroCesiumStyle(styleJson, options) {
267
325
  * Pick a single material color from a map-zero style rule.
268
326
  *
269
327
  * 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.
328
+ * Cesium material cannot show that outline, so building solids use the fill:
329
+ * it keeps the mass quiet while avoiding translucent sorting artifacts.
272
330
  *
273
331
  * @param {Record<string, any> | null} rule
274
332
  * @param {string} layerId
@@ -277,8 +335,8 @@ export function createMapZeroCesiumStyle(styleJson, options) {
277
335
  function cesiumLayerMaterial(rule, layerId) {
278
336
  if (layerId === 'buildings') {
279
337
  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))
338
+ color: buildingSolidColor(rule),
339
+ opacity: 1
282
340
  };
283
341
  }
284
342
 
@@ -303,6 +361,21 @@ function applyStyleToTilesetMap(tilesets, style, options) {
303
361
  }
304
362
  }
305
363
 
364
+ /**
365
+ * @param {string} layerId
366
+ * @param {{ opacity?: number, tilesetOpacity?: number, buildingsOpacity?: number }} options
367
+ * @returns {number}
368
+ */
369
+ function tilesetOpacityForLayer(layerId, options) {
370
+ if (layerId === 'buildings' && Number.isFinite(Number(options.buildingsOpacity))) {
371
+ return Number(options.buildingsOpacity);
372
+ }
373
+ if (Number.isFinite(Number(options.tilesetOpacity))) {
374
+ return Number(options.tilesetOpacity);
375
+ }
376
+ return Number(options.opacity ?? 1);
377
+ }
378
+
306
379
  /**
307
380
  * @param {Record<string, unknown> | null} styleJson
308
381
  * @param {string} layerId
@@ -315,29 +388,87 @@ function layerStyle(styleJson, layerId) {
315
388
 
316
389
  /**
317
390
  * @param {MapZeroManifest} manifest
391
+ * @param {{ buildings3d?: boolean }} [options]
318
392
  * @returns {Array<{ layerId: string, url: string }>}
319
393
  */
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
-
394
+ function manifestTilesetEntries(manifest, options = {}) {
328
395
  if (manifest.tiles3d?.format === '3dtiles' && typeof manifest.tiles3d.url === 'string') {
329
396
  const layers = Array.isArray(manifest.tiles3d.layers) && manifest.tiles3d.layers.length > 0
330
397
  ? manifest.tiles3d.layers.map(String)
331
398
  : ['buildings'];
332
- return layers.map((layerId) => ({
333
- layerId,
334
- url: /** @type {string} */ (manifest.tiles3d?.url)
335
- }));
399
+ return layers
400
+ .filter((layerId) => isAllowedCesiumTilesetLayer(layerId, options))
401
+ .map((layerId) => ({
402
+ layerId,
403
+ url: /** @type {string} */ (manifest.tiles3d?.url)
404
+ }));
336
405
  }
337
406
 
338
407
  return [];
339
408
  }
340
409
 
410
+ /**
411
+ * @param {string} layerId
412
+ * @param {{ buildings3d?: boolean }} options
413
+ * @returns {boolean}
414
+ */
415
+ function isAllowedCesiumTilesetLayer(layerId, options) {
416
+ return layerId !== 'buildings' || options.buildings3d !== false;
417
+ }
418
+
419
+ /**
420
+ * @param {MapZeroManifest} manifest
421
+ * @param {{ contextOverlay?: boolean }} options
422
+ * @returns {boolean}
423
+ */
424
+ function shouldCreateContextOverlay(manifest, options) {
425
+ if (options.contextOverlay === false) {
426
+ return false;
427
+ }
428
+ return hasMapZeroContextOverlay(manifest);
429
+ }
430
+
431
+ /**
432
+ * @param {Cesium3DTileset} tileset
433
+ * @param {string} layerId
434
+ */
435
+ function configureCesiumTilesetStreaming(tileset, layerId) {
436
+ if (layerId !== 'buildings') {
437
+ return;
438
+ }
439
+
440
+ tileset.maximumScreenSpaceError = 24;
441
+ tileset.skipLevelOfDetail = true;
442
+ tileset.baseScreenSpaceError = 1024;
443
+ tileset.skipScreenSpaceErrorFactor = 16;
444
+ tileset.skipLevels = 1;
445
+ tileset.immediatelyLoadDesiredLevelOfDetail = false;
446
+ tileset.loadSiblings = false;
447
+ tileset.cullWithChildrenBounds = true;
448
+ tileset.dynamicScreenSpaceError = true;
449
+ tileset.dynamicScreenSpaceErrorDensity = 0.00278;
450
+ tileset.dynamicScreenSpaceErrorFactor = 4;
451
+ tileset.preloadWhenHidden = false;
452
+ tileset.preloadFlightDestinations = false;
453
+ tileset.cacheBytes = 256 * 1024 * 1024;
454
+ tileset.maximumCacheOverflowBytes = 128 * 1024 * 1024;
455
+ }
456
+
457
+ /**
458
+ * @param {Cesium3DTileset} tileset
459
+ * @param {string} layerId
460
+ */
461
+ function configureCesiumTilesetColor(tileset, layerId) {
462
+ if (layerId === 'buildings') {
463
+ tileset.backFaceCulling = false;
464
+ tileset.colorBlendMode = Cesium3DTileColorBlendMode.MIX;
465
+ tileset.colorBlendAmount = 0.45;
466
+ return;
467
+ }
468
+ tileset.colorBlendMode = Cesium3DTileColorBlendMode.REPLACE;
469
+ tileset.colorBlendAmount = 1;
470
+ }
471
+
341
472
  /**
342
473
  * @param {string | undefined} id
343
474
  * @param {MapZeroManifest} manifest
@@ -395,6 +526,39 @@ function safeCssColor(color) {
395
526
  return /^#[0-9a-f]{6}$/i.test(color) ? color : '#ff00ff';
396
527
  }
397
528
 
529
+ /**
530
+ * @param {Record<string, any> | null} rule
531
+ * @returns {string}
532
+ */
533
+ function buildingSolidColor(rule) {
534
+ const explicit = rule?.cesium?.color ?? rule?.tiles3d?.color ?? rule?.material?.color;
535
+ if (typeof explicit === 'string' && isHexColor(explicit)) {
536
+ return explicit;
537
+ }
538
+ return '#8a3f82';
539
+ }
540
+
541
+ /**
542
+ * @param {string} value
543
+ * @returns {boolean}
544
+ */
545
+ function isHexColor(value) {
546
+ return /^#[0-9a-f]{6}$/i.test(value);
547
+ }
548
+
549
+ /**
550
+ * @param {any} Cesium
551
+ * @param {unknown} value
552
+ * @param {any} fallback
553
+ * @returns {any}
554
+ */
555
+ function colorFromOption(Cesium, value, fallback) {
556
+ if (typeof value !== 'string') {
557
+ return fallback;
558
+ }
559
+ return Cesium.Color.fromCssColorString(value) ?? fallback;
560
+ }
561
+
398
562
 
399
563
  /**
400
564
  * @param {number} value
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@map-zero/ol",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "OpenLayers integration helper for map-zero packages.",
6
6
  "main": "./src/index.js",