@honua/app-platform 0.1.2-beta.0 → 0.1.7-beta.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/app/index.d.ts.map +1 -1
- package/app/index.js +4 -0
- package/collaboration/client.d.ts.map +1 -1
- package/connect-url-safety.d.ts +53 -0
- package/connect-url-safety.js +189 -0
- package/contract/discovery.d.ts +34 -0
- package/contract/discovery.d.ts.map +1 -1
- package/contract/index.d.ts +7 -5
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +3 -2
- package/contract/pmtiles.d.ts +10 -1
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +10 -2
- package/contract/protocol-module.d.ts +84 -9
- package/contract/protocol-module.d.ts.map +1 -1
- package/contract/query-filter-fes.d.ts +18 -0
- package/contract/query-filter-fes.d.ts.map +1 -0
- package/contract/query-filter-fes.js +122 -0
- package/contract/query-filter.d.ts +300 -0
- package/contract/query-filter.d.ts.map +1 -0
- package/contract/query-filter.js +883 -0
- package/contract/schema.d.ts +12 -0
- package/contract/schema.d.ts.map +1 -1
- package/contract/schema.js +34 -0
- package/contract/source-capability-support.d.ts.map +1 -1
- package/contract/source-capability-support.js +15 -3
- package/contract/source.d.ts +45 -25
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +719 -479
- package/contract/types.d.ts +88 -19
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +4 -0
- package/contract/widget-source.d.ts.map +1 -1
- package/controls/basemap-switcher.d.ts +19 -11
- package/controls/basemap-switcher.d.ts.map +1 -1
- package/controls/basemap-switcher.js +58 -25
- package/controls/catalog.d.ts +105 -0
- package/controls/catalog.d.ts.map +1 -0
- package/controls/catalog.js +341 -0
- package/controls/element-utils.d.ts +9 -0
- package/controls/element-utils.d.ts.map +1 -1
- package/controls/element-utils.js +17 -0
- package/controls/index.d.ts +29 -8
- package/controls/index.d.ts.map +1 -1
- package/controls/index.js +26 -8
- package/controls/layer-list.d.ts +13 -5
- package/controls/layer-list.d.ts.map +1 -1
- package/controls/layer-list.js +72 -15
- package/controls/legend.d.ts +32 -5
- package/controls/legend.d.ts.map +1 -1
- package/controls/legend.js +95 -25
- package/controls/qualification.d.ts +152 -0
- package/controls/qualification.d.ts.map +1 -0
- package/controls/qualification.js +1265 -0
- package/controls/registry.d.ts +104 -0
- package/controls/registry.d.ts.map +1 -0
- package/controls/registry.js +150 -0
- package/controls/swipe-control.d.ts +6 -1
- package/controls/swipe-control.d.ts.map +1 -1
- package/controls/swipe-control.js +82 -20
- package/core/client.d.ts +51 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +234 -54
- package/core/error-classifications.d.ts +23 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +23 -0
- package/core/error-code-registry.d.ts +24 -1
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +23 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +2 -2
- package/core/errors.d.ts.map +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +69 -5
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +327 -10
- package/core/odata-write-codec.d.ts.map +1 -1
- package/core/odata-write-codec.js +0 -1
- package/core/odata.d.ts +6 -0
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +6 -1
- package/core/ogc-processes.d.ts +182 -25
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +602 -30
- package/core/process-runner.d.ts +8 -1
- package/core/process-runner.d.ts.map +1 -1
- package/core/process-runner.js +1 -0
- package/core/protocol-transport.d.ts +2 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/stac.d.ts +32 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +158 -23
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +138 -11
- package/core/types.d.ts.map +1 -1
- package/core/wfs-axis-order.d.ts +18 -0
- package/core/wfs-axis-order.d.ts.map +1 -0
- package/core/wfs-axis-order.js +108 -0
- package/core/wfs-capabilities.d.ts +65 -24
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +237 -27
- package/core/wfs-filter.d.ts +26 -3
- package/core/wfs-filter.d.ts.map +1 -1
- package/core/wfs-filter.js +48 -53
- package/core/wfs-protocol-error.d.ts +12 -0
- package/core/wfs-protocol-error.d.ts.map +1 -0
- package/core/wfs-protocol-error.js +13 -0
- package/core/wfs.d.ts +53 -1
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +251 -29
- package/core/wms-feature-info.d.ts +92 -0
- package/core/wms-feature-info.d.ts.map +1 -0
- package/core/wms-feature-info.js +285 -0
- package/core/wms.d.ts +9 -0
- package/core/wms.d.ts.map +1 -1
- package/core/wms.js +17 -5
- package/esri-compat/event-bus.d.ts +20 -0
- package/esri-compat/event-bus.d.ts.map +1 -1
- package/esri-compat/feature-filter.d.ts +46 -1
- package/esri-compat/feature-filter.d.ts.map +1 -1
- package/esri-compat/feature-filter.js +49 -0
- package/esri-compat/geojson-layer.d.ts +4 -2
- package/esri-compat/geojson-layer.d.ts.map +1 -1
- package/esri-compat/geojson-layer.js +4 -2
- package/esri-compat/identity-manager.js +2 -2
- package/esri-compat/imagery-layer.d.ts +2 -1
- package/esri-compat/imagery-layer.d.ts.map +1 -1
- package/esri-compat/imagery-layer.js +2 -1
- package/esri-compat/layer-list.d.ts +8 -1
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +11 -4
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +1 -1
- package/esri-compat/locator.d.ts +297 -0
- package/esri-compat/locator.d.ts.map +1 -0
- package/esri-compat/locator.js +579 -0
- package/esri-compat/map-image-layer.d.ts.map +1 -1
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +1 -1
- package/esri-compat/scene-view.d.ts.map +1 -1
- package/esri-compat/time-slider.d.ts +29 -0
- package/esri-compat/time-slider.d.ts.map +1 -1
- package/esri-compat/time-slider.js +117 -0
- package/esri-compat/wfs-layer.d.ts +2 -1
- package/esri-compat/wfs-layer.d.ts.map +1 -1
- package/esri-compat/wfs-layer.js +2 -1
- package/esri-compat/widget-host.d.ts +42 -6
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +99 -10
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/runtime.d.ts.map +1 -1
- package/interactions/declarative-contract.d.ts +149 -0
- package/interactions/declarative-contract.d.ts.map +1 -0
- package/interactions/declarative-contract.js +293 -0
- package/interactions/declarative.d.ts +198 -0
- package/interactions/declarative.d.ts.map +1 -0
- package/interactions/declarative.js +470 -0
- package/interactions/feature-state.d.ts +7 -0
- package/interactions/feature-state.d.ts.map +1 -1
- package/interactions/feature-state.js +10 -2
- package/map/data-to-map-bridge.d.ts +1 -1
- package/map/data-to-map-bridge.d.ts.map +1 -1
- package/map/data-to-map-bridge.js +1 -2
- package/map/honua-map.d.ts.map +1 -1
- package/map/honua-map.js +1 -1
- package/map/index.d.ts +2 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +2 -0
- package/map/maplibre-target.js +1 -1
- package/map/realtime-reconciliation-adapter.d.ts +98 -0
- package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
- package/map/realtime-reconciliation-adapter.js +84 -0
- package/map/temporal-playback.d.ts +25 -0
- package/map/temporal-playback.d.ts.map +1 -1
- package/map/temporal-playback.js +29 -1
- package/operator/workspace/workspace.d.ts.map +1 -1
- package/operator/workspace/workspace.js +0 -2
- package/package.json +4 -3
- package/query-planner/canonical-filter.d.ts +30 -0
- package/query-planner/canonical-filter.d.ts.map +1 -0
- package/query-planner/canonical-filter.js +30 -0
- package/query-planner/columnar-plan.d.ts +63 -0
- package/query-planner/columnar-plan.d.ts.map +1 -0
- package/query-planner/columnar-plan.js +94 -0
- package/query-planner/diagnostics.d.ts +3 -3
- package/query-planner/diagnostics.d.ts.map +1 -1
- package/query-planner/diagnostics.js +17 -4
- package/query-planner/duckdb.d.ts.map +1 -1
- package/query-planner/duckdb.js +16 -2
- package/query-planner/execution-context.d.ts.map +1 -1
- package/query-planner/execution-context.js +3 -1
- package/query-planner/executor.d.ts.map +1 -1
- package/query-planner/executor.js +7 -1
- package/query-planner/geoservices.d.ts.map +1 -1
- package/query-planner/geoservices.js +24 -5
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +29 -5
- package/query-planner/index.d.ts +20 -5
- package/query-planner/index.d.ts.map +1 -1
- package/query-planner/index.js +16 -3
- package/query-planner/ir.d.ts +6 -1
- package/query-planner/ir.d.ts.map +1 -1
- package/query-planner/ir.js +63 -1
- package/query-planner/odata-protocol-module.d.ts +54 -0
- package/query-planner/odata-protocol-module.d.ts.map +1 -0
- package/query-planner/odata-protocol-module.js +199 -0
- package/query-planner/odata-v1.d.ts +23 -0
- package/query-planner/odata-v1.d.ts.map +1 -0
- package/query-planner/odata-v1.js +202 -0
- package/query-planner/odata.d.ts +0 -3
- package/query-planner/odata.d.ts.map +1 -1
- package/query-planner/odata.js +0 -143
- package/query-planner/ogc-features.d.ts.map +1 -1
- package/query-planner/ogc-features.js +23 -1
- package/query-planner/planner.d.ts +2 -2
- package/query-planner/planner.d.ts.map +1 -1
- package/query-planner/planner.js +146 -27
- package/query-planner/representation.d.ts +59 -0
- package/query-planner/representation.d.ts.map +1 -0
- package/query-planner/representation.js +140 -0
- package/query-planner/types.d.ts +153 -3
- package/query-planner/types.d.ts.map +1 -1
- package/query-planner/wfs-protocol-module.d.ts +57 -0
- package/query-planner/wfs-protocol-module.d.ts.map +1 -0
- package/query-planner/wfs-protocol-module.js +731 -0
- package/query-planner/wfs-v1.d.ts +41 -0
- package/query-planner/wfs-v1.d.ts.map +1 -0
- package/query-planner/wfs-v1.js +366 -0
- package/query-planner/wfs.d.ts +1 -3
- package/query-planner/wfs.d.ts.map +1 -1
- package/query-planner/wfs.js +1 -70
- package/realtime/checkpoint-store.d.ts +206 -0
- package/realtime/checkpoint-store.d.ts.map +1 -0
- package/realtime/checkpoint-store.js +504 -0
- package/realtime/honua-server.d.ts +8 -0
- package/realtime/honua-server.d.ts.map +1 -1
- package/realtime/honua-server.js +123 -14
- package/realtime/index.d.ts +4 -0
- package/realtime/index.d.ts.map +1 -1
- package/realtime/index.js +2 -0
- package/realtime/reconciliation.d.ts +245 -0
- package/realtime/reconciliation.d.ts.map +1 -0
- package/realtime/reconciliation.js +377 -0
- package/realtime/resumable-transport.d.ts.map +1 -1
- package/realtime/sse.d.ts.map +1 -1
- package/realtime/sse.js +7 -1
- package/realtime/websocket.d.ts.map +1 -1
- package/realtime/wire.d.ts +2 -1
- package/realtime/wire.d.ts.map +1 -1
- package/realtime/wire.js +24 -8
- package/replica-sync/errors.d.ts +19 -1
- package/replica-sync/errors.d.ts.map +1 -1
- package/replica-sync/errors.js +16 -0
- package/replica-sync/geoservices.d.ts +260 -0
- package/replica-sync/geoservices.d.ts.map +1 -0
- package/replica-sync/geoservices.js +1093 -0
- package/replica-sync/index.d.ts +5 -1
- package/replica-sync/index.d.ts.map +1 -1
- package/replica-sync/index.js +3 -1
- package/replica-sync/transport-conformance.d.ts +58 -0
- package/replica-sync/transport-conformance.d.ts.map +1 -0
- package/replica-sync/transport-conformance.js +265 -0
- package/replica-sync/types.d.ts +22 -0
- package/replica-sync/types.d.ts.map +1 -1
- package/runtime/index.d.ts +6 -2
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +3 -1
- package/runtime/load-package.d.ts +24 -0
- package/runtime/load-package.d.ts.map +1 -1
- package/runtime/load-package.js +45 -1
- package/runtime/maplibre-renderer.d.ts +11 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -1
- package/runtime/maplibre-renderer.js +32 -7
- package/runtime/offline-region-protocol.d.ts +73 -0
- package/runtime/offline-region-protocol.d.ts.map +1 -0
- package/runtime/offline-region-protocol.js +63 -0
- package/runtime/offline-region-style.d.ts +78 -0
- package/runtime/offline-region-style.d.ts.map +1 -0
- package/runtime/offline-region-style.js +147 -0
- package/runtime/pmtiles-protocol.d.ts +7 -8
- package/runtime/pmtiles-protocol.d.ts.map +1 -1
- package/runtime/pmtiles-protocol.js +19 -66
- package/runtime/protocol-registry.d.ts +55 -0
- package/runtime/protocol-registry.d.ts.map +1 -0
- package/runtime/protocol-registry.js +103 -0
- package/runtime/runtime.d.ts +2 -0
- package/runtime/runtime.d.ts.map +1 -1
- package/runtime/runtime.js +6 -1
- package/runtime/style-compose.d.ts.map +1 -1
- package/runtime/style-compose.js +78 -9
- package/scene-workspace/analysis-widgets.d.ts +4 -2
- package/scene-workspace/analysis-widgets.d.ts.map +1 -1
- package/scene-workspace/analysis-widgets.js +4 -2
- package/scene-workspace/camera-correspondence.d.ts +151 -0
- package/scene-workspace/camera-correspondence.d.ts.map +1 -0
- package/scene-workspace/camera-correspondence.js +220 -0
- package/scene-workspace/cesium-adapter.d.ts +230 -29
- package/scene-workspace/cesium-adapter.d.ts.map +1 -1
- package/scene-workspace/cesium-adapter.js +741 -56
- package/scene-workspace/cesium-mount.d.ts +205 -0
- package/scene-workspace/cesium-mount.d.ts.map +1 -0
- package/scene-workspace/cesium-mount.js +392 -0
- package/scene-workspace/cesium-scene-owner.d.ts +160 -0
- package/scene-workspace/cesium-scene-owner.d.ts.map +1 -0
- package/scene-workspace/cesium-scene-owner.js +205 -0
- package/scene-workspace/cesium-state-sync.d.ts +101 -0
- package/scene-workspace/cesium-state-sync.d.ts.map +1 -0
- package/scene-workspace/cesium-state-sync.js +432 -0
- package/scene-workspace/cesium-time.d.ts +346 -0
- package/scene-workspace/cesium-time.d.ts.map +1 -0
- package/scene-workspace/cesium-time.js +403 -0
- package/scene-workspace/index.d.ts +42 -10
- package/scene-workspace/index.d.ts.map +1 -1
- package/scene-workspace/index.js +32 -7
- package/scene-workspace/maplibre-state-sync.d.ts +112 -0
- package/scene-workspace/maplibre-state-sync.d.ts.map +1 -0
- package/scene-workspace/maplibre-state-sync.js +400 -0
- package/scene-workspace/primitives.d.ts +237 -3
- package/scene-workspace/primitives.d.ts.map +1 -1
- package/scene-workspace/primitives.js +1087 -17
- package/scene-workspace/scene-discovery.d.ts +5 -2
- package/scene-workspace/scene-discovery.d.ts.map +1 -1
- package/scene-workspace/scene-discovery.js +5 -2
- package/scene-workspace/scene-view.d.ts +5 -2
- package/scene-workspace/scene-view.d.ts.map +1 -1
- package/scene-workspace/scene-view.js +5 -2
- package/scene-workspace/source-to-cesium.d.ts +141 -8
- package/scene-workspace/source-to-cesium.d.ts.map +1 -1
- package/scene-workspace/source-to-cesium.js +497 -72
- package/scene-workspace/state-sync-port-kit.d.ts +156 -0
- package/scene-workspace/state-sync-port-kit.d.ts.map +1 -0
- package/scene-workspace/state-sync-port-kit.js +319 -0
- package/scene-workspace/state-sync.d.ts +34 -1
- package/scene-workspace/state-sync.d.ts.map +1 -1
- package/scene-workspace/state-sync.js +48 -0
- package/scene-workspace/types.d.ts +8 -0
- package/scene-workspace/types.d.ts.map +1 -1
- package/scene-workspace/types.js +2 -0
- package/scene-workspace/workspace.d.ts +12 -1
- package/scene-workspace/workspace.d.ts.map +1 -1
- package/scene-workspace/workspace.js +13 -0
- package/studio/index.d.ts +24 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +21 -7
- package/studio/lifecycle-client.d.ts +164 -0
- package/studio/lifecycle-client.d.ts.map +1 -0
- package/studio/lifecycle-client.js +299 -0
- package/studio/lifecycle-errors.d.ts +91 -0
- package/studio/lifecycle-errors.d.ts.map +1 -0
- package/studio/lifecycle-errors.js +77 -0
- package/studio/lifecycle-types.d.ts +321 -0
- package/studio/lifecycle-types.d.ts.map +1 -0
- package/studio/lifecycle-types.js +22 -0
- package/web-components/controller.d.ts.map +1 -1
- package/web-components/csp-styles.d.ts +6 -0
- package/web-components/csp-styles.d.ts.map +1 -0
- package/web-components/csp-styles.js +53 -0
- package/web-components/elements.d.ts +158 -2
- package/web-components/elements.d.ts.map +1 -1
- package/web-components/elements.js +1302 -215
- package/web-components/export-redaction.d.ts +252 -0
- package/web-components/export-redaction.d.ts.map +1 -0
- package/web-components/export-redaction.js +588 -0
- package/web-components/export.d.ts +417 -0
- package/web-components/export.d.ts.map +1 -0
- package/web-components/export.js +719 -0
- package/web-components/feature-editor-model.d.ts +265 -0
- package/web-components/feature-editor-model.d.ts.map +1 -0
- package/web-components/feature-editor-model.js +438 -0
- package/web-components/feature-editor-sketch.d.ts +125 -0
- package/web-components/feature-editor-sketch.d.ts.map +1 -0
- package/web-components/feature-editor-sketch.js +194 -0
- package/web-components/feature-editor-workflow.d.ts +286 -0
- package/web-components/feature-editor-workflow.d.ts.map +1 -0
- package/web-components/feature-editor-workflow.js +889 -0
- package/web-components/feature-editor.d.ts +83 -0
- package/web-components/feature-editor.d.ts.map +1 -0
- package/web-components/feature-editor.js +829 -0
- package/web-components/feature-table-engine.d.ts +465 -0
- package/web-components/feature-table-engine.d.ts.map +1 -0
- package/web-components/feature-table-engine.js +1360 -0
- package/web-components/feature-table-view.d.ts +95 -0
- package/web-components/feature-table-view.d.ts.map +1 -0
- package/web-components/feature-table-view.js +343 -0
- package/web-components/index.d.ts +34 -3
- package/web-components/index.d.ts.map +1 -1
- package/web-components/index.js +35 -2
- package/web-components/maplibre-renderer.d.ts.map +1 -1
- package/web-components/maplibre-renderer.js +13 -4
- package/web-components/measurement.d.ts +3 -1
- package/web-components/measurement.d.ts.map +1 -1
- package/web-components/measurement.js +62 -18
- package/web-components/time-slider.d.ts +122 -0
- package/web-components/time-slider.d.ts.map +1 -0
- package/web-components/time-slider.js +740 -0
- package/web-components/types.d.ts +310 -0
- package/web-components/types.d.ts.map +1 -1
- package/webmap/convert-label.js +0 -1
- package/webmap/convert-symbol.d.ts.map +1 -1
|
@@ -1,3 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer-neutral scene primitive contract: terrain, imagery, 3D Tiles, model
|
|
3
|
+
* layers, extrusions, ground, camera, and layer metadata, plus the diagnostics
|
|
4
|
+
* that say — before a viewer exists — whether a renderer can honor each one.
|
|
5
|
+
*
|
|
6
|
+
* The MapLibre adapter lives here because it needs no optional peer; the Cesium
|
|
7
|
+
* adapter implements the same {@link SceneRuntimeAdapter} contract.
|
|
8
|
+
*
|
|
9
|
+
* @beta Part of the beta `@honua/app-platform/scene-workspace` surface; primitive
|
|
10
|
+
* kinds, diagnostic codes, and renderer capability flags grow additively and
|
|
11
|
+
* no export is renamed or removed through 0.1.x.
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { isCredentialQueryName } from "../connect-url-safety.js";
|
|
15
|
+
const SCENE_ASSET_URL_BASE = "https://scene.honua.invalid/";
|
|
16
|
+
const WMS_RESERVED_PARAMETER_KEYS = new Set(["bbox", "crs", "height", "request", "service", "srs", "width"]);
|
|
17
|
+
const ARCGIS_IMAGE_SERVER_RESERVED_PARAMETER_KEYS = new Set([
|
|
18
|
+
"bbox",
|
|
19
|
+
"bboxsr",
|
|
20
|
+
"f",
|
|
21
|
+
"format",
|
|
22
|
+
"imagesr",
|
|
23
|
+
"size",
|
|
24
|
+
"transparent",
|
|
25
|
+
]);
|
|
26
|
+
const WMTS_RESERVED_DIMENSION_KEYS = new Set([
|
|
27
|
+
"format",
|
|
28
|
+
"layer",
|
|
29
|
+
"request",
|
|
30
|
+
"service",
|
|
31
|
+
"style",
|
|
32
|
+
"tilecol",
|
|
33
|
+
"tilematrix",
|
|
34
|
+
"tilematrixset",
|
|
35
|
+
"tilematrixsetid",
|
|
36
|
+
"tilerow",
|
|
37
|
+
"version",
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Bounded point-cloud rendering controls for a tiled point-cloud asset.
|
|
41
|
+
*
|
|
42
|
+
* Point clouds ride the same `3d-tiles` model-layer path as any other tileset,
|
|
43
|
+
* but they need splat sizing and eye-dome lighting to be legible. These fields
|
|
44
|
+
* are the renderer-neutral subset every 3D engine can honor; each is validated
|
|
45
|
+
* before the renderer peer is loaded, so an out-of-range value fails closed
|
|
46
|
+
* rather than silently reverting to a renderer default.
|
|
47
|
+
*/
|
|
48
|
+
const POINT_CLOUD_SHADING_BOOLEAN_KEYS = ["attenuation", "eyeDomeLighting"];
|
|
49
|
+
const POINT_CLOUD_SHADING_MAGNITUDE_KEYS = [
|
|
50
|
+
"maximumAttenuation",
|
|
51
|
+
"geometricErrorScale",
|
|
52
|
+
"eyeDomeLightingStrength",
|
|
53
|
+
"eyeDomeLightingRadius",
|
|
54
|
+
];
|
|
55
|
+
const POINT_CLOUD_SHADING_KEYS = new Set([
|
|
56
|
+
...POINT_CLOUD_SHADING_BOOLEAN_KEYS,
|
|
57
|
+
...POINT_CLOUD_SHADING_MAGNITUDE_KEYS,
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* Both shipped adapters address the world in geographic WGS84 and consume the
|
|
61
|
+
* Web Mercator family through their own tiling/reprojection, so they share one
|
|
62
|
+
* spatial capability record. Nothing else is honorable here: this slice
|
|
63
|
+
* performs no reprojection, so a projected or local CRS would render in the
|
|
64
|
+
* wrong place, and a non-ellipsoidal datum would render at the wrong height.
|
|
65
|
+
*/
|
|
66
|
+
export const RENDERER_WGS84_SPATIAL_CAPABILITIES = {
|
|
67
|
+
exactHorizontalCrs: ["EPSG:4326", "OGC:CRS84"],
|
|
68
|
+
equivalentHorizontalCrs: ["EPSG:3857"],
|
|
69
|
+
verticalDatums: ["EPSG:4979"],
|
|
70
|
+
};
|
|
1
71
|
export const MAPLIBRE_SCENE_CAPABILITIES = {
|
|
2
72
|
renderer: "maplibre",
|
|
3
73
|
camera: true,
|
|
@@ -6,6 +76,7 @@ export const MAPLIBRE_SCENE_CAPABILITIES = {
|
|
|
6
76
|
extrusion: true,
|
|
7
77
|
modelLayer: { formats: [] },
|
|
8
78
|
sceneLayerMetadata: true,
|
|
79
|
+
spatial: RENDERER_WGS84_SPATIAL_CAPABILITIES,
|
|
9
80
|
};
|
|
10
81
|
export function createSceneRuntimeAdapter(options) {
|
|
11
82
|
return {
|
|
@@ -26,7 +97,38 @@ export function createMapLibreSceneAdapter(id = "maplibre-scene") {
|
|
|
26
97
|
export function diagnoseScenePrimitives(primitives, capabilities) {
|
|
27
98
|
return primitives.flatMap((primitive) => diagnoseScenePrimitive(primitive, capabilities));
|
|
28
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Diagnose one primitive against a renderer capability record.
|
|
102
|
+
*
|
|
103
|
+
* Three independent questions are answered: whether the renderer can
|
|
104
|
+
* *materialize* the binding at all (structure), whether it can place it where
|
|
105
|
+
* the author meant and resolve it at the detail the author claimed (spatial
|
|
106
|
+
* reference and precision), and what the plan says about the material behind it
|
|
107
|
+
* (cache freshness, source version). All are pure — nothing here loads a
|
|
108
|
+
* renderer peer, so a migration analysis can run before a viewer exists.
|
|
109
|
+
*
|
|
110
|
+
* A spatial finding — CRS, vertical datum, or precision — replaces the generic
|
|
111
|
+
* `scene-primitive-supported` summary, mirroring how a renderability finding
|
|
112
|
+
* already does: the caller should read the fidelity, not a bare "supported".
|
|
113
|
+
* Descriptive asset findings (cache freshness, unreadable metadata) are additive
|
|
114
|
+
* instead, because they say nothing about whether the renderer can draw the
|
|
115
|
+
* binding. A primitive that declares no CRS, vertical datum, precision, cache,
|
|
116
|
+
* or source version diagnoses exactly as it did before those contracts existed.
|
|
117
|
+
*/
|
|
29
118
|
export function diagnoseScenePrimitive(primitive, capabilities) {
|
|
119
|
+
const unreadableFields = unreadableAssetMetadataFields(primitive);
|
|
120
|
+
const spatial = [
|
|
121
|
+
...diagnoseSceneSpatialReference(primitive, capabilities),
|
|
122
|
+
...diagnoseScenePrecision(primitive, capabilities, unreadableFields),
|
|
123
|
+
];
|
|
124
|
+
const structural = diagnoseScenePrimitiveStructure(primitive, capabilities);
|
|
125
|
+
const asset = diagnoseSceneAssetMetadata(primitive, capabilities, unreadableFields);
|
|
126
|
+
const findings = spatial.length === 0
|
|
127
|
+
? [...structural, ...asset]
|
|
128
|
+
: [...spatial, ...structural.filter((entry) => entry.code !== "scene-primitive-supported"), ...asset];
|
|
129
|
+
return withSourceVersionContext(primitive, findings);
|
|
130
|
+
}
|
|
131
|
+
function diagnoseScenePrimitiveStructure(primitive, capabilities) {
|
|
30
132
|
switch (primitive.kind) {
|
|
31
133
|
case "camera":
|
|
32
134
|
return capabilities.camera
|
|
@@ -56,13 +158,28 @@ export function diagnoseScenePrimitive(primitive, capabilities) {
|
|
|
56
158
|
degraded(primitive, capabilities, "Elevation source is supported but exaggeration will be ignored.", "Apply source at native scale."),
|
|
57
159
|
];
|
|
58
160
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return terrainDiagnostics;
|
|
63
|
-
}
|
|
161
|
+
const terrainDiagnostics = diagnoseRenderableTerrain(primitive, capabilities);
|
|
162
|
+
if (terrainDiagnostics.length > 0)
|
|
163
|
+
return terrainDiagnostics;
|
|
64
164
|
return [supported(primitive, capabilities, "Elevation source can be applied as terrain.")];
|
|
65
165
|
}
|
|
166
|
+
case "imagery-layer": {
|
|
167
|
+
const imagery = capabilities.imagery;
|
|
168
|
+
if (!imagery) {
|
|
169
|
+
return [
|
|
170
|
+
unsupported(primitive, capabilities, "Renderer adapter does not support imagery layers.", "Keep the imagery binding in workspace diagnostics or route it to an imagery-capable renderer."),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
if (!imagery.protocols.includes(primitive.protocol)) {
|
|
174
|
+
return [
|
|
175
|
+
unsupported(primitive, capabilities, `Imagery protocol '${primitive.protocol}' is not supported by ${capabilities.renderer}.`, "Use a supported imagery protocol or keep the source binding for another renderer."),
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
const imageryDiagnostics = diagnoseRenderableImagery(primitive, capabilities);
|
|
179
|
+
return imageryDiagnostics.length > 0
|
|
180
|
+
? imageryDiagnostics
|
|
181
|
+
: [supported(primitive, capabilities, "Imagery layer can be materialized by the renderer.")];
|
|
182
|
+
}
|
|
66
183
|
case "extrusion":
|
|
67
184
|
return capabilities.extrusion
|
|
68
185
|
? [supported(primitive, capabilities, "Extrusion can be applied as a fill-extrusion layer.")]
|
|
@@ -70,12 +187,24 @@ export function diagnoseScenePrimitive(primitive, capabilities) {
|
|
|
70
187
|
degraded(primitive, capabilities, "Renderer adapter does not support extrusions.", "Render the source as a 2D fill/line layer."),
|
|
71
188
|
];
|
|
72
189
|
case "model-layer": {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
190
|
+
const modelLayer = capabilities.modelLayer;
|
|
191
|
+
const formats = modelLayer?.formats ?? [];
|
|
192
|
+
if (!formats.includes(primitive.format)) {
|
|
193
|
+
return [
|
|
194
|
+
unsupported(primitive, capabilities, `Model format '${primitive.format}' is not supported by ${capabilities.renderer}.`, "Preserve model metadata and route to a 3D renderer adapter."),
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
const modelDiagnostics = diagnoseRenderableModelLayer(primitive, capabilities);
|
|
198
|
+
const materializedFormats = modelLayer?.materializedFormats;
|
|
199
|
+
if (materializedFormats !== undefined && !materializedFormats.includes(primitive.format)) {
|
|
200
|
+
modelDiagnostics.push({
|
|
201
|
+
...diagnostic("scene-primitive-model-format-not-materialized", "error", "unsupported", primitive, capabilities, `Model format '${primitive.format}' is consumable by ${capabilities.renderer} but this adapter does not materialize it.`, "Publish the asset as a 3D-Tiles or glTF/GLB model layer, or keep the binding for an adapter that renders it."),
|
|
202
|
+
context: { format: primitive.format, materializedFormats: [...materializedFormats] },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return modelDiagnostics.length > 0
|
|
206
|
+
? modelDiagnostics
|
|
207
|
+
: [supported(primitive, capabilities, "Model layer format is supported.")];
|
|
79
208
|
}
|
|
80
209
|
case "scene-layer-metadata":
|
|
81
210
|
return capabilities.sceneLayerMetadata
|
|
@@ -130,8 +259,12 @@ export function applyMapLibreScenePrimitives(target, primitives) {
|
|
|
130
259
|
const diagnostics = diagnoseScenePrimitives(primitives, MAPLIBRE_SCENE_CAPABILITIES);
|
|
131
260
|
for (const primitive of primitives) {
|
|
132
261
|
if (primitive.kind === "elevation-source") {
|
|
133
|
-
|
|
134
|
-
|
|
262
|
+
// A primitive can carry several findings (spatial reference *and*
|
|
263
|
+
// renderability). Any one of them being fail-closed must skip the apply,
|
|
264
|
+
// so this asks whether *any* is unsupported rather than reading whichever
|
|
265
|
+
// diagnostic happens to come first.
|
|
266
|
+
const blocked = diagnostics.some((diagnostic) => diagnostic.primitiveId === primitive.id && diagnostic.status === "unsupported");
|
|
267
|
+
if (blocked)
|
|
135
268
|
continue;
|
|
136
269
|
const patch = toMapLibreTerrainPatch(primitive);
|
|
137
270
|
if (!target.getSource(patch.sourceId))
|
|
@@ -177,10 +310,27 @@ function diagnostic(code, severity, status, primitive, capabilities, message, fa
|
|
|
177
310
|
...(fallback ? { fallback } : {}),
|
|
178
311
|
};
|
|
179
312
|
}
|
|
180
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Endpoint and range validation for an elevation source, applied to every
|
|
315
|
+
* terrain protocol rather than `terrain-rgb` alone.
|
|
316
|
+
*
|
|
317
|
+
* `quantized-mesh`, `raster-dem`, `image-service`, `i3s`, and `custom` all reach
|
|
318
|
+
* a renderer through the same door — `CesiumTerrainProvider.fromUrl(url)` or a
|
|
319
|
+
* MapLibre `raster-dem` source — and every one of them fails opaquely on an
|
|
320
|
+
* absent or malformed endpoint. Failing closed here keeps that failure legible
|
|
321
|
+
* and keeps it off the live scene.
|
|
322
|
+
*/
|
|
323
|
+
function diagnoseRenderableTerrain(primitive, capabilities) {
|
|
181
324
|
const diagnostics = [];
|
|
182
325
|
if (!hasRenderableTerrainUrl(primitive)) {
|
|
183
|
-
diagnostics.push(diagnostic("scene-primitive-terrain-source-missing-url", "error", "unsupported", primitive, capabilities,
|
|
326
|
+
diagnostics.push(diagnostic("scene-primitive-terrain-source-missing-url", "error", "unsupported", primitive, capabilities, `${primitive.protocol} elevation source requires a renderable url or tile template.`, "Provide a non-empty url or at least one non-empty tiles entry before applying terrain."));
|
|
327
|
+
}
|
|
328
|
+
const invalidEndpointFields = invalidTerrainEndpointFields(primitive);
|
|
329
|
+
if (invalidEndpointFields.length > 0) {
|
|
330
|
+
diagnostics.push({
|
|
331
|
+
...diagnostic("scene-primitive-terrain-source-url-invalid", "error", "unsupported", primitive, capabilities, `${primitive.protocol} elevation source endpoint is invalid.`, "Use a relative, HTTP, or HTTPS endpoint for the url and every tiles entry."),
|
|
332
|
+
context: { invalidFields: invalidEndpointFields },
|
|
333
|
+
});
|
|
184
334
|
}
|
|
185
335
|
const invalidRanges = {};
|
|
186
336
|
if (primitive.tileSize !== undefined && !isPositiveFiniteNumber(primitive.tileSize)) {
|
|
@@ -202,24 +352,944 @@ function diagnoseRenderableTerrainRgb(primitive, capabilities) {
|
|
|
202
352
|
}
|
|
203
353
|
if (Object.keys(invalidRanges).length > 0) {
|
|
204
354
|
diagnostics.push({
|
|
205
|
-
...diagnostic("scene-primitive-terrain-range-invalid", "error", "unsupported", primitive, capabilities,
|
|
355
|
+
...diagnostic("scene-primitive-terrain-range-invalid", "error", "unsupported", primitive, capabilities, `${primitive.protocol} elevation source has invalid numeric rendering ranges.`, "Use finite positive tileSize/exaggeration values and ordered zoom values between 0 and 24."),
|
|
206
356
|
context: invalidRanges,
|
|
207
357
|
});
|
|
208
358
|
}
|
|
209
359
|
return diagnostics;
|
|
210
360
|
}
|
|
361
|
+
function diagnoseRenderableImagery(primitive, capabilities) {
|
|
362
|
+
const diagnostics = [];
|
|
363
|
+
let urlProblem;
|
|
364
|
+
if (!isNonEmptyString(primitive.url)) {
|
|
365
|
+
diagnostics.push(diagnostic("scene-primitive-imagery-source-missing-url", "error", "unsupported", primitive, capabilities, "Imagery requires a provider URL.", "Provide a credential-free URL."));
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
urlProblem = sceneAssetUrlProblem(primitive.url);
|
|
369
|
+
if (urlProblem === "invalid") {
|
|
370
|
+
diagnostics.push(diagnostic("scene-primitive-imagery-source-url-invalid", "error", "unsupported", primitive, capabilities, "Imagery provider URL is invalid.", "Use a relative, HTTP, or HTTPS URL."));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (urlProblem === "credentials" || hasCredentialParameter(primitive.parameters)) {
|
|
374
|
+
diagnostics.push(diagnostic("scene-primitive-imagery-credentials-forbidden", "error", "unsupported", primitive, capabilities, "Imagery bindings cannot contain credentials or signed URLs.", "Resolve authorization at the host boundary."));
|
|
375
|
+
}
|
|
376
|
+
const missingServiceFields = [];
|
|
377
|
+
if ((primitive.protocol === "wms" || primitive.protocol === "wmts") && !isNonEmptyString(primitive.layer)) {
|
|
378
|
+
missingServiceFields.push("layer");
|
|
379
|
+
}
|
|
380
|
+
if (primitive.protocol === "wmts") {
|
|
381
|
+
if (!isNonEmptyString(primitive.style))
|
|
382
|
+
missingServiceFields.push("style");
|
|
383
|
+
if (!isNonEmptyString(primitive.tileMatrixSetId))
|
|
384
|
+
missingServiceFields.push("tileMatrixSetId");
|
|
385
|
+
}
|
|
386
|
+
if ((primitive.protocol === "single-tile" || primitive.protocol === "arcgis-imagery") &&
|
|
387
|
+
typeof primitive.url === "string" &&
|
|
388
|
+
primitive.url.includes("{s}") &&
|
|
389
|
+
primitive.subdomains === undefined) {
|
|
390
|
+
missingServiceFields.push("subdomains");
|
|
391
|
+
}
|
|
392
|
+
if (missingServiceFields.length > 0) {
|
|
393
|
+
diagnostics.push({
|
|
394
|
+
...diagnostic("scene-primitive-imagery-service-config-missing", "error", "unsupported", primitive, capabilities, `Imagery configuration for '${primitive.protocol}' is incomplete.`, "Provide required service fields."),
|
|
395
|
+
context: { missingFields: missingServiceFields },
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const invalidServiceFields = [];
|
|
399
|
+
const isWebMapService = primitive.protocol === "wms" || primitive.protocol === "wmts";
|
|
400
|
+
const arcGisEndpoint = primitive.protocol === "arcgis-imagery" && typeof primitive.url === "string"
|
|
401
|
+
? arcGisImageryEndpoint(primitive.url)
|
|
402
|
+
: undefined;
|
|
403
|
+
if (primitive.protocol === "arcgis-imagery" && isNonEmptyString(primitive.url) && !arcGisEndpoint) {
|
|
404
|
+
invalidServiceFields.push("url");
|
|
405
|
+
}
|
|
406
|
+
if (primitive.layer !== undefined && !isWebMapService)
|
|
407
|
+
invalidServiceFields.push("layer");
|
|
408
|
+
if (primitive.style !== undefined &&
|
|
409
|
+
(!isWebMapService || (primitive.protocol === "wms" && !isNonEmptyString(primitive.style)))) {
|
|
410
|
+
invalidServiceFields.push("style");
|
|
411
|
+
}
|
|
412
|
+
if (primitive.format !== undefined) {
|
|
413
|
+
if (!isWebMapService || !isNonEmptyString(primitive.format))
|
|
414
|
+
invalidServiceFields.push("format");
|
|
415
|
+
}
|
|
416
|
+
if (primitive.tileMatrixSetId !== undefined && primitive.protocol !== "wmts") {
|
|
417
|
+
invalidServiceFields.push("tileMatrixSetId");
|
|
418
|
+
}
|
|
419
|
+
if (primitive.parameters !== undefined && !isValidImageryParameters(primitive.parameters)) {
|
|
420
|
+
invalidServiceFields.push("parameters");
|
|
421
|
+
}
|
|
422
|
+
if ((primitive.protocol === "single-tile" || arcGisEndpoint === "map") && primitive.minimumLevel !== undefined) {
|
|
423
|
+
invalidServiceFields.push("minimumLevel");
|
|
424
|
+
}
|
|
425
|
+
if (primitive.protocol === "single-tile" && primitive.maximumLevel !== undefined) {
|
|
426
|
+
invalidServiceFields.push("maximumLevel");
|
|
427
|
+
}
|
|
428
|
+
const invalidParameterKeys = invalidImageryParameterKeys(primitive);
|
|
429
|
+
if (invalidParameterKeys.length > 0 && !invalidServiceFields.includes("parameters")) {
|
|
430
|
+
invalidServiceFields.push("parameters");
|
|
431
|
+
}
|
|
432
|
+
if (invalidServiceFields.length > 0) {
|
|
433
|
+
diagnostics.push({
|
|
434
|
+
...diagnostic("scene-primitive-imagery-service-config-invalid", "error", "unsupported", primitive, capabilities, `Imagery configuration for '${primitive.protocol}' is invalid.`, "Omit unsupported fields."),
|
|
435
|
+
context: {
|
|
436
|
+
invalidFields: invalidServiceFields,
|
|
437
|
+
...(invalidParameterKeys.length > 0 ? { invalidParameterKeys } : {}),
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (primitive.opacity !== undefined &&
|
|
442
|
+
(!Number.isFinite(primitive.opacity) || primitive.opacity < 0 || primitive.opacity > 1)) {
|
|
443
|
+
diagnostics.push({
|
|
444
|
+
...diagnostic("scene-primitive-imagery-opacity-invalid", "error", "unsupported", primitive, capabilities, "Imagery opacity must be finite and between 0 and 1.", "Use opacity in [0, 1]."),
|
|
445
|
+
context: { opacity: primitive.opacity },
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const invalidLevels = {};
|
|
449
|
+
if (primitive.minimumLevel !== undefined && !isNonNegativeInteger(primitive.minimumLevel)) {
|
|
450
|
+
invalidLevels.minimumLevel = primitive.minimumLevel;
|
|
451
|
+
}
|
|
452
|
+
if (primitive.maximumLevel !== undefined && !isNonNegativeInteger(primitive.maximumLevel)) {
|
|
453
|
+
invalidLevels.maximumLevel = primitive.maximumLevel;
|
|
454
|
+
}
|
|
455
|
+
if (primitive.minimumLevel !== undefined &&
|
|
456
|
+
primitive.maximumLevel !== undefined &&
|
|
457
|
+
Number.isFinite(primitive.minimumLevel) &&
|
|
458
|
+
Number.isFinite(primitive.maximumLevel) &&
|
|
459
|
+
primitive.minimumLevel > primitive.maximumLevel) {
|
|
460
|
+
invalidLevels.levelRange = [primitive.minimumLevel, primitive.maximumLevel];
|
|
461
|
+
}
|
|
462
|
+
if (Object.keys(invalidLevels).length > 0) {
|
|
463
|
+
diagnostics.push({
|
|
464
|
+
...diagnostic("scene-primitive-imagery-level-range-invalid", "error", "unsupported", primitive, capabilities, "Imagery level bounds must be ordered non-negative integers.", "Use integer levels with minimumLevel <= maximumLevel."),
|
|
465
|
+
context: invalidLevels,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (primitive.subdomains !== undefined &&
|
|
469
|
+
(!isValidImagerySubdomains(primitive.subdomains) ||
|
|
470
|
+
typeof primitive.url !== "string" ||
|
|
471
|
+
!primitive.url.includes("{s}"))) {
|
|
472
|
+
diagnostics.push(diagnostic("scene-primitive-imagery-subdomains-invalid", "error", "unsupported", primitive, capabilities, "Subdomains require DNS labels and a {s} URL.", "Omit subdomains or add {s}."));
|
|
473
|
+
}
|
|
474
|
+
return diagnostics;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Renderability checks for a model layer, mirroring the imagery contract: the
|
|
478
|
+
* asset URI must be a credential-free relative/HTTP/HTTPS URL, the placement
|
|
479
|
+
* must be expressible as a real position on the globe, and point-cloud shading
|
|
480
|
+
* must be in range. Every failure is fail-closed (`unsupported`), so nothing is
|
|
481
|
+
* handed to a renderer factory that would either throw opaquely or, worse,
|
|
482
|
+
* produce a silently wrong model matrix.
|
|
483
|
+
*/
|
|
484
|
+
function diagnoseRenderableModelLayer(primitive, capabilities) {
|
|
485
|
+
const diagnostics = [];
|
|
486
|
+
if (!isNonEmptyString(primitive.uri)) {
|
|
487
|
+
diagnostics.push(diagnostic("scene-primitive-model-source-missing-uri", "error", "unsupported", primitive, capabilities, "Model layer requires an asset URI.", "Provide a credential-free URI."));
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
const uriProblem = sceneAssetUrlProblem(primitive.uri);
|
|
491
|
+
if (uriProblem === "invalid") {
|
|
492
|
+
diagnostics.push(diagnostic("scene-primitive-model-source-uri-invalid", "error", "unsupported", primitive, capabilities, "Model layer asset URI is invalid.", "Use a relative, HTTP, or HTTPS URI."));
|
|
493
|
+
}
|
|
494
|
+
else if (uriProblem === "credentials") {
|
|
495
|
+
diagnostics.push(diagnostic("scene-primitive-model-credentials-forbidden", "error", "unsupported", primitive, capabilities, "Model bindings cannot contain credentials or signed URIs.", "Resolve authorization at the host boundary."));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const invalidPlacementFields = invalidModelPlacementFields(primitive);
|
|
499
|
+
if (invalidPlacementFields.length > 0) {
|
|
500
|
+
diagnostics.push({
|
|
501
|
+
...diagnostic("scene-primitive-model-placement-invalid", "error", "unsupported", primitive, capabilities, "Model placement must use finite longitude/latitude/height, rotation, and positive scale values.", "Correct the placement or omit it to place the asset at its authored origin."),
|
|
502
|
+
context: { invalidFields: invalidPlacementFields },
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const invalidShadingFields = invalidPointCloudShadingFields(primitive);
|
|
506
|
+
if (invalidShadingFields.length > 0) {
|
|
507
|
+
diagnostics.push({
|
|
508
|
+
...diagnostic("scene-primitive-model-point-cloud-shading-invalid", "error", "unsupported", primitive, capabilities, "Point-cloud shading requires boolean toggles and finite positive magnitudes on a tiled asset.", "Correct the shading values or omit pointCloudShading."),
|
|
509
|
+
context: { invalidFields: invalidShadingFields },
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return diagnostics;
|
|
513
|
+
}
|
|
514
|
+
function invalidModelPlacementFields(primitive) {
|
|
515
|
+
const invalidFields = [];
|
|
516
|
+
const { position, rotation, scale } = primitive;
|
|
517
|
+
if (position !== undefined) {
|
|
518
|
+
if (!Array.isArray(position) || position.length < 2 || position.length > 3)
|
|
519
|
+
invalidFields.push("position");
|
|
520
|
+
else {
|
|
521
|
+
const [longitude, latitude, height] = position;
|
|
522
|
+
const validLongitude = Number.isFinite(longitude) && longitude >= -180 && longitude <= 180;
|
|
523
|
+
const validLatitude = Number.isFinite(latitude) && latitude >= -90 && latitude <= 90;
|
|
524
|
+
const validHeight = height === undefined || Number.isFinite(height);
|
|
525
|
+
if (!validLongitude || !validLatitude || !validHeight)
|
|
526
|
+
invalidFields.push("position");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (rotation !== undefined) {
|
|
530
|
+
if (!Array.isArray(rotation) || rotation.length !== 3 || !rotation.every((angle) => Number.isFinite(angle))) {
|
|
531
|
+
invalidFields.push("rotation");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (scale !== undefined) {
|
|
535
|
+
const validScale = Array.isArray(scale)
|
|
536
|
+
? scale.length === 3 && scale.every((axis) => typeof axis === "number" && isPositiveFiniteNumber(axis))
|
|
537
|
+
: typeof scale === "number" && isPositiveFiniteNumber(scale);
|
|
538
|
+
if (!validScale)
|
|
539
|
+
invalidFields.push("scale");
|
|
540
|
+
}
|
|
541
|
+
return invalidFields;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Point-cloud shading is validated as a closed record: an unknown or misspelled
|
|
545
|
+
* own key (`maximumAttenutation`) is itself a failure, because
|
|
546
|
+
* `normalizePointCloudShading` would otherwise drop it and the layer would
|
|
547
|
+
* render with Cesium's defaults while still reporting `supported`. The `typeof`
|
|
548
|
+
* guards are load-bearing at runtime even though the declared type narrows
|
|
549
|
+
* them — deserialized plan JSON is not type-checked.
|
|
550
|
+
*/
|
|
551
|
+
function invalidPointCloudShadingFields(primitive) {
|
|
552
|
+
const shading = primitive.pointCloudShading;
|
|
553
|
+
if (shading === undefined)
|
|
554
|
+
return [];
|
|
555
|
+
// Only a tiled asset has points to shade, and only a plain data record can be
|
|
556
|
+
// checked key-by-key.
|
|
557
|
+
if (primitive.format !== "3d-tiles" || !isPlainRecord(shading))
|
|
558
|
+
return ["pointCloudShading"];
|
|
559
|
+
const invalidFields = [];
|
|
560
|
+
for (const key of Object.keys(shading)) {
|
|
561
|
+
if (!POINT_CLOUD_SHADING_KEYS.has(key))
|
|
562
|
+
invalidFields.push(key);
|
|
563
|
+
}
|
|
564
|
+
for (const field of POINT_CLOUD_SHADING_BOOLEAN_KEYS) {
|
|
565
|
+
const value = shading[field];
|
|
566
|
+
if (value !== undefined && typeof value !== "boolean")
|
|
567
|
+
invalidFields.push(field);
|
|
568
|
+
}
|
|
569
|
+
for (const field of POINT_CLOUD_SHADING_MAGNITUDE_KEYS) {
|
|
570
|
+
const value = shading[field];
|
|
571
|
+
if (value !== undefined && (typeof value !== "number" || !isPositiveFiniteNumber(value)))
|
|
572
|
+
invalidFields.push(field);
|
|
573
|
+
}
|
|
574
|
+
return invalidFields;
|
|
575
|
+
}
|
|
211
576
|
function hasRenderableTerrainUrl(primitive) {
|
|
212
577
|
if (typeof primitive.url === "string" && primitive.url.trim() !== "")
|
|
213
578
|
return true;
|
|
214
579
|
return primitive.tiles?.some((tile) => typeof tile === "string" && tile.trim() !== "") === true;
|
|
215
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Name the declared terrain endpoints that cannot be resolved. A blank or absent
|
|
583
|
+
* value is the *missing*-endpoint case and is reported separately, so only
|
|
584
|
+
* present, non-blank material is judged here.
|
|
585
|
+
*/
|
|
586
|
+
function invalidTerrainEndpointFields(primitive) {
|
|
587
|
+
const invalidFields = [];
|
|
588
|
+
if (isNonEmptyString(primitive.url) && sceneAssetUrlProblem(primitive.url) === "invalid")
|
|
589
|
+
invalidFields.push("url");
|
|
590
|
+
if (primitive.tiles !== undefined) {
|
|
591
|
+
if (!Array.isArray(primitive.tiles))
|
|
592
|
+
invalidFields.push("tiles");
|
|
593
|
+
else {
|
|
594
|
+
primitive.tiles.forEach((tile, index) => {
|
|
595
|
+
if (isNonEmptyString(tile) && sceneAssetUrlProblem(tile) === "invalid")
|
|
596
|
+
invalidFields.push(`tiles[${index}]`);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return invalidFields;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Classify a primitive's declared spatial reference against the renderer.
|
|
604
|
+
*
|
|
605
|
+
* Pure and peer-free (NFR-001), and silent for a primitive that declares
|
|
606
|
+
* nothing or a renderer that declares no spatial capability (NFR-002) — there is
|
|
607
|
+
* no honest classification to make in either case, and inventing one would turn
|
|
608
|
+
* an unannotated plan into a wall of warnings.
|
|
609
|
+
*/
|
|
610
|
+
function diagnoseSceneSpatialReference(primitive, capabilities) {
|
|
611
|
+
if (!primitiveDeclaresSpatialReference(primitive))
|
|
612
|
+
return [];
|
|
613
|
+
const { crs, verticalDatum } = primitive;
|
|
614
|
+
if (crs === undefined && verticalDatum === undefined)
|
|
615
|
+
return [];
|
|
616
|
+
const spatial = capabilities.spatial;
|
|
617
|
+
if (!spatial)
|
|
618
|
+
return [];
|
|
619
|
+
const diagnostics = [];
|
|
620
|
+
if (crs !== undefined) {
|
|
621
|
+
const exact = spatial.exactHorizontalCrs ?? [];
|
|
622
|
+
const equivalent = spatial.equivalentHorizontalCrs ?? [];
|
|
623
|
+
const normalized = normalizeSceneCrs(crs);
|
|
624
|
+
const context = { crs, ...(normalized !== undefined ? { normalizedCrs: normalized } : {}) };
|
|
625
|
+
if (normalized !== undefined && containsNormalizedCrs(exact, normalized)) {
|
|
626
|
+
diagnostics.push({
|
|
627
|
+
...diagnostic("scene-primitive-crs-exact", "info", "supported", primitive, capabilities, `Horizontal CRS '${crs}' is addressed natively by ${capabilities.renderer}.`),
|
|
628
|
+
fidelity: "exact",
|
|
629
|
+
context,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
else if (normalized !== undefined && containsNormalizedCrs(equivalent, normalized)) {
|
|
633
|
+
diagnostics.push({
|
|
634
|
+
...diagnostic("scene-primitive-crs-equivalent", "warning", "degraded", primitive, capabilities, `Horizontal CRS '${crs}' is honored by ${capabilities.renderer} through its own reprojection, not as authored.`, "Accept the renderer's resampling, or republish the binding in WGS84 to keep exact fidelity."),
|
|
635
|
+
fidelity: "equivalent",
|
|
636
|
+
context: { ...context, exactHorizontalCrs: [...exact] },
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
diagnostics.push({
|
|
641
|
+
...diagnostic("scene-primitive-crs-unsupported", "error", "unsupported", primitive, capabilities, `Horizontal CRS '${crs}' cannot be honored by ${capabilities.renderer}; coordinates are never reinterpreted.`, "Republish the binding in a supported CRS, or reproject it before it reaches the scene."),
|
|
642
|
+
fidelity: "unsupported",
|
|
643
|
+
context: { ...context, exactHorizontalCrs: [...exact], equivalentHorizontalCrs: [...equivalent] },
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (verticalDatum !== undefined) {
|
|
648
|
+
const supportedDatums = spatial.verticalDatums ?? [];
|
|
649
|
+
const normalized = normalizeSceneCrs(verticalDatum);
|
|
650
|
+
if (normalized === undefined || !containsNormalizedCrs(supportedDatums, normalized)) {
|
|
651
|
+
diagnostics.push({
|
|
652
|
+
...diagnostic("scene-primitive-vertical-datum-unsupported", "error", "unsupported", primitive, capabilities, `Vertical datum '${verticalDatum}' cannot be honored by ${capabilities.renderer}; heights are never transformed.`, "Republish heights against an ellipsoidal WGS84 datum, or drop the height component."),
|
|
653
|
+
fidelity: "unsupported",
|
|
654
|
+
context: {
|
|
655
|
+
verticalDatum,
|
|
656
|
+
...(normalized !== undefined ? { normalizedVerticalDatum: normalized } : {}),
|
|
657
|
+
supportedVerticalDatums: [...supportedDatums],
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return diagnostics;
|
|
663
|
+
}
|
|
664
|
+
function primitiveDeclaresSpatialReference(primitive) {
|
|
665
|
+
return (primitive.kind === "elevation-source" || primitive.kind === "imagery-layer" || primitive.kind === "model-layer");
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Height quantum, in metres, of the RGB DEM encodings both shipped renderers
|
|
669
|
+
* decode. Each is an exact consequence of the published decode formula, not an
|
|
670
|
+
* estimate: Mapbox Terrain-RGB is `-10000 + (R * 65536 + G * 256 + B) * 0.1`, so
|
|
671
|
+
* the smallest height step it can express is 0.1 m; Terrarium is
|
|
672
|
+
* `(R * 256 + G + B / 256) - 32768`, so its step is 1/256 m. An encoding whose
|
|
673
|
+
* quantum is not knowable from the plan — `custom`, or none declared — appears
|
|
674
|
+
* nowhere here and contributes no limit.
|
|
675
|
+
*/
|
|
676
|
+
const DEM_ENCODING_HEIGHT_QUANTUM_METERS = new Map([
|
|
677
|
+
["mapbox", 0.1],
|
|
678
|
+
["terrarium", 1 / 256],
|
|
679
|
+
]);
|
|
680
|
+
/** WGS84 semi-major axis, in metres: the magnitude a surface-level ECEF coordinate carries. */
|
|
681
|
+
const WGS84_SEMI_MAJOR_AXIS_METERS = 6_378_137;
|
|
682
|
+
/**
|
|
683
|
+
* Spacing between adjacent float32 values at the ellipsoid's surface, in metres.
|
|
684
|
+
*
|
|
685
|
+
* A float32 carries a 24-bit significand, so consecutive representable values
|
|
686
|
+
* near a magnitude `x` are `2 ** (floor(log2 x) - 23)` apart — 0.5 m at the
|
|
687
|
+
* semi-major axis. That is the precision floor of any binding that stores
|
|
688
|
+
* earth-centred coordinates at float32 width, whatever the renderer does with
|
|
689
|
+
* them afterwards, and it is the reason 3D assets are published in a local frame
|
|
690
|
+
* anchored by a relative-to-centre origin rather than in raw ECEF.
|
|
691
|
+
*/
|
|
692
|
+
const GEOCENTRIC_FLOAT32_QUANTUM_METERS = 2 ** (Math.floor(Math.log2(WGS84_SEMI_MAJOR_AXIS_METERS)) - 23);
|
|
693
|
+
const SCENE_PRECISION_AXES = ["horizontal", "vertical"];
|
|
694
|
+
const SCENE_PRECISION_LIMIT_FALLBACKS = {
|
|
695
|
+
"dem-encoding": "Publish the DEM in an encoding whose height quantum carries the declared precision, or declare the precision the encoding actually resolves.",
|
|
696
|
+
"geocentric-float32-coordinates": "Republish the asset in a local frame anchored by its own origin, or store coordinates at float64 width.",
|
|
697
|
+
"renderer-floor": "Accept the renderer's sampling, or route the binding to an adapter whose declared precision floor carries it.",
|
|
698
|
+
};
|
|
699
|
+
/**
|
|
700
|
+
* Classify a binding's declared precision against the limits the plan and the
|
|
701
|
+
* renderer declare.
|
|
702
|
+
*
|
|
703
|
+
* Pure and peer-free, and silent wherever a comparison would be invented: a
|
|
704
|
+
* claim with no limit, a limit with no claim, an encoding whose quantum is not
|
|
705
|
+
* published, and a renderer that declares no floor all produce nothing.
|
|
706
|
+
*
|
|
707
|
+
* Only two fidelities are honest here. A binding whose detail exceeds a limit
|
|
708
|
+
* still renders — coarser than authored, which is exactly `equivalent` — so no
|
|
709
|
+
* precision finding is ever minted at `unsupported`. Precision that cannot be
|
|
710
|
+
* *read* is a validity failure and is reported separately as
|
|
711
|
+
* `scene-primitive-asset-metadata-invalid`.
|
|
712
|
+
*/
|
|
713
|
+
function diagnoseScenePrecision(primitive, capabilities, unreadableFields) {
|
|
714
|
+
if (!primitiveDeclaresSpatialReference(primitive))
|
|
715
|
+
return [];
|
|
716
|
+
const precision = primitive.precision;
|
|
717
|
+
if (precision === undefined)
|
|
718
|
+
return [];
|
|
719
|
+
// A record we could not read is never classified: a dropped or misspelled key
|
|
720
|
+
// would otherwise read as agreement with whatever survived.
|
|
721
|
+
if (unreadableFields.some((field) => field === "precision" || field.startsWith("precision.")))
|
|
722
|
+
return [];
|
|
723
|
+
const diagnostics = [];
|
|
724
|
+
for (const axis of SCENE_PRECISION_AXES) {
|
|
725
|
+
const claimedMeters = axis === "horizontal" ? precision.horizontalMeters : precision.verticalMeters;
|
|
726
|
+
if (claimedMeters === undefined)
|
|
727
|
+
continue;
|
|
728
|
+
const limit = coarsestScenePrecisionLimit(primitive, capabilities, axis);
|
|
729
|
+
if (limit === undefined)
|
|
730
|
+
continue;
|
|
731
|
+
const context = {
|
|
732
|
+
axis,
|
|
733
|
+
claimedMeters,
|
|
734
|
+
limitMeters: limit.quantumMeters,
|
|
735
|
+
limitSource: limit.source,
|
|
736
|
+
...(limit.detail !== undefined ? { limitDetail: limit.detail } : {}),
|
|
737
|
+
};
|
|
738
|
+
if (claimedMeters >= limit.quantumMeters) {
|
|
739
|
+
diagnostics.push({
|
|
740
|
+
...diagnostic("scene-primitive-precision-exact", "info", "supported", primitive, capabilities, `Declared ${axis} precision of ${claimedMeters} m is carried as authored; the coarsest declared limit is ${limit.quantumMeters} m.`),
|
|
741
|
+
fidelity: "exact",
|
|
742
|
+
context,
|
|
743
|
+
});
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
diagnostics.push({
|
|
747
|
+
...diagnostic("scene-primitive-precision-equivalent", "warning", "degraded", primitive, capabilities, `Declared ${axis} precision of ${claimedMeters} m is finer than the ${limit.source} limit of ${limit.quantumMeters} m; ${capabilities.renderer} carries this binding at the coarser quantum, not as authored.`, SCENE_PRECISION_LIMIT_FALLBACKS[limit.source]),
|
|
748
|
+
fidelity: "equivalent",
|
|
749
|
+
context,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return diagnostics;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* The limit that actually decides an axis: the coarsest one declared, because a
|
|
756
|
+
* binding resolves no finer than its bluntest instrument. Ties keep the
|
|
757
|
+
* first-collected limit so the finding is deterministic.
|
|
758
|
+
*/
|
|
759
|
+
function coarsestScenePrecisionLimit(primitive, capabilities, axis) {
|
|
760
|
+
const limits = [];
|
|
761
|
+
if (axis === "vertical" &&
|
|
762
|
+
primitive.kind === "elevation-source" &&
|
|
763
|
+
(primitive.protocol === "terrain-rgb" || primitive.protocol === "raster-dem") &&
|
|
764
|
+
primitive.encoding !== undefined) {
|
|
765
|
+
const quantumMeters = DEM_ENCODING_HEIGHT_QUANTUM_METERS.get(primitive.encoding);
|
|
766
|
+
if (quantumMeters !== undefined) {
|
|
767
|
+
limits.push({ source: "dem-encoding", quantumMeters, detail: primitive.encoding });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (primitive.precision?.coordinateFrame === "geocentric" && primitive.precision.coordinateStorage === "float32") {
|
|
771
|
+
limits.push({
|
|
772
|
+
source: "geocentric-float32-coordinates",
|
|
773
|
+
quantumMeters: GEOCENTRIC_FLOAT32_QUANTUM_METERS,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
const floor = capabilities.spatial?.precision;
|
|
777
|
+
const floorMeters = axis === "horizontal" ? floor?.horizontalMeters : floor?.verticalMeters;
|
|
778
|
+
if (floorMeters !== undefined && isPositiveFiniteNumber(floorMeters)) {
|
|
779
|
+
limits.push({ source: "renderer-floor", quantumMeters: floorMeters });
|
|
780
|
+
}
|
|
781
|
+
let coarsest;
|
|
782
|
+
for (const limit of limits) {
|
|
783
|
+
if (coarsest === undefined || limit.quantumMeters > coarsest.quantumMeters)
|
|
784
|
+
coarsest = limit;
|
|
785
|
+
}
|
|
786
|
+
return coarsest;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Report the descriptive asset metadata a binding carries: cache freshness the
|
|
790
|
+
* plan observed, and any metadata that could not be read.
|
|
791
|
+
*
|
|
792
|
+
* Neither finding is fail-closed. Precision, cache state, and source version
|
|
793
|
+
* describe the material behind a binding; none of them can make the renderer
|
|
794
|
+
* draw it in the wrong place, so a malformed one degrades the plan's claims
|
|
795
|
+
* rather than refusing a scene that is otherwise renderable. Staying silent is
|
|
796
|
+
* not an option either — a dropped `cache.staus` typo would read as `ready`.
|
|
797
|
+
*/
|
|
798
|
+
function diagnoseSceneAssetMetadata(primitive, capabilities, unreadableFields) {
|
|
799
|
+
const diagnostics = [];
|
|
800
|
+
if (unreadableFields.length > 0) {
|
|
801
|
+
diagnostics.push({
|
|
802
|
+
...diagnostic("scene-primitive-asset-metadata-invalid", "warning", "degraded", primitive, capabilities, "Descriptive asset metadata could not be read, so it is not classified.", "Correct the named fields or omit them; the binding still renders, but its precision, cache, and source-version claims are ignored."),
|
|
803
|
+
context: { invalidFields: [...unreadableFields] },
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
diagnostics.push(...diagnoseSceneCacheStatus(primitive, capabilities, unreadableFields));
|
|
807
|
+
return diagnostics;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Surface a declared cache status that the developer would otherwise never see.
|
|
811
|
+
*
|
|
812
|
+
* `stale` degrades the plan — the scene draws material the author already knew
|
|
813
|
+
* was out of date — while `bypass` is a supported, informational statement that
|
|
814
|
+
* every request reaches the origin. `ready` and `unknown` say nothing worth
|
|
815
|
+
* repeating, and `unknown` is genuinely the unknown state, so both are silent.
|
|
816
|
+
* Nothing here revalidates or refetches: the SDK reports the plan's cache
|
|
817
|
+
* metadata, it does not own the cache.
|
|
818
|
+
*/
|
|
819
|
+
function diagnoseSceneCacheStatus(primitive, capabilities, unreadableFields) {
|
|
820
|
+
const cache = primitive.cache;
|
|
821
|
+
if (cache === undefined)
|
|
822
|
+
return [];
|
|
823
|
+
if (unreadableFields.some((field) => field === "cache" || field.startsWith("cache.")))
|
|
824
|
+
return [];
|
|
825
|
+
const scope = cache.scope ?? "asset";
|
|
826
|
+
const context = {
|
|
827
|
+
cacheStatus: cache.status,
|
|
828
|
+
...(cache.scope !== undefined ? { cacheScope: cache.scope } : {}),
|
|
829
|
+
...(cache.updatedAt !== undefined ? { cacheUpdatedAt: cache.updatedAt } : {}),
|
|
830
|
+
...(cache.ttlMs !== undefined ? { cacheTtlMs: cache.ttlMs } : {}),
|
|
831
|
+
...(cache.validator !== undefined ? { cacheValidator: cache.validator } : {}),
|
|
832
|
+
};
|
|
833
|
+
if (cache.status === "stale") {
|
|
834
|
+
return [
|
|
835
|
+
{
|
|
836
|
+
...diagnostic("scene-primitive-cache-stale", "warning", "degraded", primitive, capabilities, `Binding declares a stale ${scope} cache; the scene draws the cached material and the SDK never revalidates it.`, "Refresh the cached material at the host boundary before applying the plan, or update the binding's cache metadata."),
|
|
837
|
+
context,
|
|
838
|
+
},
|
|
839
|
+
];
|
|
840
|
+
}
|
|
841
|
+
if (cache.status === "bypass") {
|
|
842
|
+
return [
|
|
843
|
+
{
|
|
844
|
+
...diagnostic("scene-primitive-cache-bypass", "info", "supported", primitive, capabilities, `Binding declares that its ${scope} cache is bypassed; every request for this material reaches the origin.`),
|
|
845
|
+
context,
|
|
846
|
+
},
|
|
847
|
+
];
|
|
848
|
+
}
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
const SCENE_PRECISION_KEYS = new Set([
|
|
852
|
+
"horizontalMeters",
|
|
853
|
+
"verticalMeters",
|
|
854
|
+
"coordinateFrame",
|
|
855
|
+
"coordinateStorage",
|
|
856
|
+
]);
|
|
857
|
+
const SCENE_PRECISION_MAGNITUDE_KEYS = ["horizontalMeters", "verticalMeters"];
|
|
858
|
+
const SCENE_PRECISION_COORDINATE_FRAMES = new Set(["geocentric", "local"]);
|
|
859
|
+
const SCENE_PRECISION_COORDINATE_STORAGE = new Set(["float32", "float64"]);
|
|
860
|
+
const SCENE_CACHE_KEYS = new Set(["status", "scope", "updatedAt", "ttlMs", "validator"]);
|
|
861
|
+
const SCENE_CACHE_STATUSES = new Set(["ready", "stale", "bypass", "unknown"]);
|
|
862
|
+
const SCENE_CACHE_SCOPES = new Set(["metadata", "tiles", "asset", "interaction"]);
|
|
863
|
+
/**
|
|
864
|
+
* Name the descriptive asset metadata that cannot be trusted, dotted from the
|
|
865
|
+
* primitive root (`precision.horizontalMeters`, `cache.status`,
|
|
866
|
+
* `sourceVersion`).
|
|
867
|
+
*
|
|
868
|
+
* Both records are validated as closed records, for the reason
|
|
869
|
+
* `pointCloudShading` is: an unknown own key is itself a failure, because
|
|
870
|
+
* dropping it silently would leave the developer believing a claim was checked.
|
|
871
|
+
* The `typeof` guards are load-bearing at runtime even where the declared type
|
|
872
|
+
* narrows them — deserialized plan JSON is not type-checked.
|
|
873
|
+
*/
|
|
874
|
+
function unreadableAssetMetadataFields(primitive) {
|
|
875
|
+
const invalidFields = [];
|
|
876
|
+
if (primitive.sourceVersion !== undefined && !isNonEmptyString(primitive.sourceVersion)) {
|
|
877
|
+
invalidFields.push("sourceVersion");
|
|
878
|
+
}
|
|
879
|
+
invalidFields.push(...unreadablePrecisionFields(primitive));
|
|
880
|
+
invalidFields.push(...unreadableCacheFields(primitive));
|
|
881
|
+
return invalidFields;
|
|
882
|
+
}
|
|
883
|
+
function unreadablePrecisionFields(primitive) {
|
|
884
|
+
if (!primitiveDeclaresSpatialReference(primitive)) {
|
|
885
|
+
// `precision` lives on the spatial-reference mixin, so a kind that cannot
|
|
886
|
+
// carry it has nothing to classify. A stray value on deserialized plan JSON
|
|
887
|
+
// is named rather than silently ignored.
|
|
888
|
+
const stray = primitive.precision;
|
|
889
|
+
return stray === undefined ? [] : ["precision"];
|
|
890
|
+
}
|
|
891
|
+
const precision = primitive.precision;
|
|
892
|
+
if (precision === undefined)
|
|
893
|
+
return [];
|
|
894
|
+
if (!isPlainRecord(precision))
|
|
895
|
+
return ["precision"];
|
|
896
|
+
const invalidFields = [];
|
|
897
|
+
for (const key of Object.keys(precision)) {
|
|
898
|
+
if (!SCENE_PRECISION_KEYS.has(key))
|
|
899
|
+
invalidFields.push(`precision.${key}`);
|
|
900
|
+
}
|
|
901
|
+
for (const key of SCENE_PRECISION_MAGNITUDE_KEYS) {
|
|
902
|
+
const value = precision[key];
|
|
903
|
+
if (value !== undefined && (typeof value !== "number" || !isPositiveFiniteNumber(value))) {
|
|
904
|
+
invalidFields.push(`precision.${key}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const coordinateFrame = precision.coordinateFrame;
|
|
908
|
+
if (coordinateFrame !== undefined &&
|
|
909
|
+
(typeof coordinateFrame !== "string" || !SCENE_PRECISION_COORDINATE_FRAMES.has(coordinateFrame))) {
|
|
910
|
+
invalidFields.push("precision.coordinateFrame");
|
|
911
|
+
}
|
|
912
|
+
const coordinateStorage = precision.coordinateStorage;
|
|
913
|
+
if (coordinateStorage !== undefined &&
|
|
914
|
+
(typeof coordinateStorage !== "string" || !SCENE_PRECISION_COORDINATE_STORAGE.has(coordinateStorage))) {
|
|
915
|
+
invalidFields.push("precision.coordinateStorage");
|
|
916
|
+
}
|
|
917
|
+
return invalidFields;
|
|
918
|
+
}
|
|
919
|
+
function unreadableCacheFields(primitive) {
|
|
920
|
+
const cache = primitive.cache;
|
|
921
|
+
if (cache === undefined)
|
|
922
|
+
return [];
|
|
923
|
+
if (!isPlainRecord(cache))
|
|
924
|
+
return ["cache"];
|
|
925
|
+
const invalidFields = [];
|
|
926
|
+
for (const key of Object.keys(cache)) {
|
|
927
|
+
if (!SCENE_CACHE_KEYS.has(key))
|
|
928
|
+
invalidFields.push(`cache.${key}`);
|
|
929
|
+
}
|
|
930
|
+
if (typeof cache.status !== "string" || !SCENE_CACHE_STATUSES.has(cache.status))
|
|
931
|
+
invalidFields.push("cache.status");
|
|
932
|
+
if (cache.scope !== undefined && (typeof cache.scope !== "string" || !SCENE_CACHE_SCOPES.has(cache.scope))) {
|
|
933
|
+
invalidFields.push("cache.scope");
|
|
934
|
+
}
|
|
935
|
+
if (cache.updatedAt !== undefined &&
|
|
936
|
+
(!isNonEmptyString(cache.updatedAt) || Number.isNaN(Date.parse(cache.updatedAt)))) {
|
|
937
|
+
invalidFields.push("cache.updatedAt");
|
|
938
|
+
}
|
|
939
|
+
if (cache.ttlMs !== undefined &&
|
|
940
|
+
(typeof cache.ttlMs !== "number" || !Number.isFinite(cache.ttlMs) || cache.ttlMs < 0)) {
|
|
941
|
+
invalidFields.push("cache.ttlMs");
|
|
942
|
+
}
|
|
943
|
+
if (cache.validator !== undefined && !isNonEmptyString(cache.validator))
|
|
944
|
+
invalidFields.push("cache.validator");
|
|
945
|
+
return invalidFields;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Carry the version the plan was authored against onto every finding the
|
|
949
|
+
* primitive raises, so a diagnostic can be traced back to the material it was
|
|
950
|
+
* computed from. A blank or non-string version is reported by
|
|
951
|
+
* {@link unreadableAssetMetadataFields} instead of being propagated.
|
|
952
|
+
*/
|
|
953
|
+
function withSourceVersionContext(primitive, diagnostics) {
|
|
954
|
+
if (!isNonEmptyString(primitive.sourceVersion))
|
|
955
|
+
return [...diagnostics];
|
|
956
|
+
const sourceVersion = primitive.sourceVersion.trim();
|
|
957
|
+
return diagnostics.map((entry) => ({ ...entry, context: { ...entry.context, sourceVersion } }));
|
|
958
|
+
}
|
|
959
|
+
function containsNormalizedCrs(candidates, normalized) {
|
|
960
|
+
return candidates.some((candidate) => normalizeSceneCrs(candidate) === normalized);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Aliases that cannot be derived from an authority/code pair. Keys are compared
|
|
964
|
+
* lowercase against the trimmed identifier.
|
|
965
|
+
*/
|
|
966
|
+
const SCENE_CRS_ALIASES = new Map([
|
|
967
|
+
["crs84", "OGC:CRS84"],
|
|
968
|
+
["ogc:crs84", "OGC:CRS84"],
|
|
969
|
+
["wgs84", "EPSG:4326"],
|
|
970
|
+
["wgs 84", "EPSG:4326"],
|
|
971
|
+
["wgs-84", "EPSG:4326"],
|
|
972
|
+
["ellipsoidal-wgs84", "EPSG:4979"],
|
|
973
|
+
["wgs84-ellipsoidal", "EPSG:4979"],
|
|
974
|
+
["web mercator", "EPSG:3857"],
|
|
975
|
+
["web-mercator", "EPSG:3857"],
|
|
976
|
+
["esri:102100", "EPSG:3857"],
|
|
977
|
+
["esri:102113", "EPSG:3857"],
|
|
978
|
+
["epsg:102100", "EPSG:3857"],
|
|
979
|
+
["epsg:102113", "EPSG:3857"],
|
|
980
|
+
["epsg:900913", "EPSG:3857"],
|
|
981
|
+
["epsg:3785", "EPSG:3857"],
|
|
982
|
+
]);
|
|
983
|
+
const SCENE_CRS_AUTHORITIES = new Set(["epsg", "esri", "ogc"]);
|
|
984
|
+
/**
|
|
985
|
+
* Normalize a CRS or vertical-datum identifier to a canonical `AUTHORITY:CODE`
|
|
986
|
+
* token, or `undefined` when it cannot be identified at all.
|
|
987
|
+
*
|
|
988
|
+
* Splitting is done with a single linear scan rather than a pattern so an
|
|
989
|
+
* attacker-supplied plan value cannot drive backtracking.
|
|
990
|
+
*/
|
|
991
|
+
function normalizeSceneCrs(value) {
|
|
992
|
+
if (typeof value !== "string")
|
|
993
|
+
return undefined;
|
|
994
|
+
const trimmed = value.trim();
|
|
995
|
+
if (trimmed === "")
|
|
996
|
+
return undefined;
|
|
997
|
+
const lower = trimmed.toLowerCase();
|
|
998
|
+
const alias = SCENE_CRS_ALIASES.get(lower);
|
|
999
|
+
if (alias)
|
|
1000
|
+
return alias;
|
|
1001
|
+
const segments = splitSceneCrsSegments(lower);
|
|
1002
|
+
const code = segments[segments.length - 1];
|
|
1003
|
+
if (code === undefined)
|
|
1004
|
+
return undefined;
|
|
1005
|
+
if (code === "crs84")
|
|
1006
|
+
return "OGC:CRS84";
|
|
1007
|
+
if (!isDigitString(code))
|
|
1008
|
+
return undefined;
|
|
1009
|
+
let authority;
|
|
1010
|
+
for (let index = segments.length - 2; index >= 0; index -= 1) {
|
|
1011
|
+
const segment = segments[index];
|
|
1012
|
+
if (segment !== undefined && SCENE_CRS_AUTHORITIES.has(segment)) {
|
|
1013
|
+
authority = segment;
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
// A bare numeric identifier is an EPSG code by convention; an OGC authority
|
|
1018
|
+
// does not mint numeric CRS codes, so that pairing stays unidentified.
|
|
1019
|
+
if (authority === undefined)
|
|
1020
|
+
return segments.length === 1 ? resolveSceneCrsToken("epsg", code) : undefined;
|
|
1021
|
+
if (authority === "ogc")
|
|
1022
|
+
return undefined;
|
|
1023
|
+
return resolveSceneCrsToken(authority, code);
|
|
1024
|
+
}
|
|
1025
|
+
function resolveSceneCrsToken(authority, code) {
|
|
1026
|
+
const canonical = `${authority.toUpperCase()}:${stripLeadingZeros(code)}`;
|
|
1027
|
+
return SCENE_CRS_ALIASES.get(canonical.toLowerCase()) ?? canonical;
|
|
1028
|
+
}
|
|
1029
|
+
function splitSceneCrsSegments(value) {
|
|
1030
|
+
const segments = [];
|
|
1031
|
+
let current = "";
|
|
1032
|
+
for (const character of value) {
|
|
1033
|
+
if (character !== ":" && character !== "/") {
|
|
1034
|
+
current += character;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (current !== "")
|
|
1038
|
+
segments.push(current);
|
|
1039
|
+
current = "";
|
|
1040
|
+
}
|
|
1041
|
+
if (current !== "")
|
|
1042
|
+
segments.push(current);
|
|
1043
|
+
return segments;
|
|
1044
|
+
}
|
|
1045
|
+
function isDigitString(value) {
|
|
1046
|
+
if (value === "")
|
|
1047
|
+
return false;
|
|
1048
|
+
for (const character of value) {
|
|
1049
|
+
if (character < "0" || character > "9")
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
function stripLeadingZeros(value) {
|
|
1055
|
+
let index = 0;
|
|
1056
|
+
while (index < value.length - 1 && value[index] === "0")
|
|
1057
|
+
index += 1;
|
|
1058
|
+
return value.slice(index);
|
|
1059
|
+
}
|
|
216
1060
|
function isPositiveFiniteNumber(value) {
|
|
217
1061
|
return Number.isFinite(value) && value > 0;
|
|
218
1062
|
}
|
|
219
1063
|
function isZoom(value) {
|
|
220
1064
|
return Number.isFinite(value) && value >= 0 && value <= 24;
|
|
221
1065
|
}
|
|
222
|
-
function
|
|
1066
|
+
function isNonNegativeInteger(value) {
|
|
1067
|
+
return Number.isInteger(value) && value >= 0;
|
|
1068
|
+
}
|
|
1069
|
+
function isNonEmptyString(value) {
|
|
1070
|
+
return typeof value === "string" && value.trim() !== "";
|
|
1071
|
+
}
|
|
1072
|
+
function sceneAssetUrlProblem(value) {
|
|
1073
|
+
let parsed;
|
|
1074
|
+
try {
|
|
1075
|
+
parsed = new URL(value, SCENE_ASSET_URL_BASE);
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
return "invalid";
|
|
1079
|
+
}
|
|
1080
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
1081
|
+
return "invalid";
|
|
1082
|
+
if (parsed.username !== "" || parsed.password !== "")
|
|
1083
|
+
return "credentials";
|
|
1084
|
+
for (const key of parsed.searchParams.keys()) {
|
|
1085
|
+
if (isCredentialQueryName(key))
|
|
1086
|
+
return "credentials";
|
|
1087
|
+
}
|
|
1088
|
+
if (parsed.hash !== "") {
|
|
1089
|
+
const fragmentParameters = new URLSearchParams(parsed.hash.slice(1).replaceAll("?", "&"));
|
|
1090
|
+
for (const key of fragmentParameters.keys()) {
|
|
1091
|
+
if (isCredentialQueryName(key))
|
|
1092
|
+
return "credentials";
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return undefined;
|
|
1096
|
+
}
|
|
1097
|
+
function hasCredentialParameter(parameters) {
|
|
1098
|
+
if (!isValidImageryParameters(parameters))
|
|
1099
|
+
return false;
|
|
1100
|
+
return Object.keys(parameters).some(isCredentialQueryName);
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* A plain, inspectable data record: object literal (or null-prototype), not an
|
|
1104
|
+
* array, no symbol keys, and no accessors. Anything else cannot be validated
|
|
1105
|
+
* key-by-key without invoking foreign code, so it fails closed.
|
|
1106
|
+
*/
|
|
1107
|
+
function isPlainRecord(value) {
|
|
1108
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1109
|
+
return false;
|
|
1110
|
+
try {
|
|
1111
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1112
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1113
|
+
return false;
|
|
1114
|
+
if (Object.getOwnPropertySymbols(value).length > 0)
|
|
1115
|
+
return false;
|
|
1116
|
+
return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => "value" in descriptor);
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function isValidImageryParameters(value) {
|
|
1123
|
+
if (!isPlainRecord(value))
|
|
1124
|
+
return false;
|
|
1125
|
+
return Object.values(value).every((entry) => typeof entry === "string" || typeof entry === "boolean" || (typeof entry === "number" && Number.isFinite(entry)));
|
|
1126
|
+
}
|
|
1127
|
+
function isValidImagerySubdomains(value) {
|
|
1128
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 32)
|
|
1129
|
+
return false;
|
|
1130
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1131
|
+
if (!Object.hasOwn(value, index) || !isSafeDnsLabel(value[index]))
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
function invalidImageryParameterKeys(primitive) {
|
|
1137
|
+
if (primitive.protocol === "wms" &&
|
|
1138
|
+
primitive.parameters !== undefined &&
|
|
1139
|
+
isValidImageryParameters(primitive.parameters)) {
|
|
1140
|
+
return invalidCaseInsensitiveParameterKeys(primitive.parameters, WMS_RESERVED_PARAMETER_KEYS);
|
|
1141
|
+
}
|
|
1142
|
+
if (primitive.protocol === "wmts" &&
|
|
1143
|
+
primitive.parameters !== undefined &&
|
|
1144
|
+
isValidImageryParameters(primitive.parameters)) {
|
|
1145
|
+
return invalidCaseInsensitiveParameterKeys(primitive.parameters, WMTS_RESERVED_DIMENSION_KEYS);
|
|
1146
|
+
}
|
|
1147
|
+
if (primitive.protocol !== "arcgis-imagery" ||
|
|
1148
|
+
typeof primitive.url !== "string" ||
|
|
1149
|
+
primitive.parameters === undefined ||
|
|
1150
|
+
!isValidImageryParameters(primitive.parameters)) {
|
|
1151
|
+
return [];
|
|
1152
|
+
}
|
|
1153
|
+
if (arcGisImageryEndpoint(primitive.url) === "image") {
|
|
1154
|
+
return invalidCaseInsensitiveParameterKeys(primitive.parameters, ARCGIS_IMAGE_SERVER_RESERVED_PARAMETER_KEYS);
|
|
1155
|
+
}
|
|
1156
|
+
const invalidKeys = [];
|
|
1157
|
+
const seenKeys = new Set();
|
|
1158
|
+
for (const [key, value] of Object.entries(primitive.parameters)) {
|
|
1159
|
+
const canonical = canonicalParameterKey(key);
|
|
1160
|
+
if (seenKeys.has(canonical)) {
|
|
1161
|
+
invalidKeys.push(key);
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
seenKeys.add(canonical);
|
|
1165
|
+
const valid = (canonical === "layers" && normalizeArcGisMapServerLayers(value) !== undefined) ||
|
|
1166
|
+
((canonical === "enablepickfeatures" || canonical === "useprecachedtilesifavailable") &&
|
|
1167
|
+
typeof value === "boolean") ||
|
|
1168
|
+
((canonical === "tilewidth" || canonical === "tileheight") &&
|
|
1169
|
+
typeof value === "number" &&
|
|
1170
|
+
Number.isInteger(value) &&
|
|
1171
|
+
value > 0 &&
|
|
1172
|
+
value <= 8192);
|
|
1173
|
+
if (!valid)
|
|
1174
|
+
invalidKeys.push(key);
|
|
1175
|
+
}
|
|
1176
|
+
return invalidKeys;
|
|
1177
|
+
}
|
|
1178
|
+
function invalidCaseInsensitiveParameterKeys(parameters, reservedKeys) {
|
|
1179
|
+
const invalidKeys = [];
|
|
1180
|
+
const seenKeys = new Set();
|
|
1181
|
+
for (const key of Object.keys(parameters)) {
|
|
1182
|
+
const caseInsensitiveKey = key.toLowerCase();
|
|
1183
|
+
if (seenKeys.has(caseInsensitiveKey) || reservedKeys.has(canonicalParameterKey(key)))
|
|
1184
|
+
invalidKeys.push(key);
|
|
1185
|
+
seenKeys.add(caseInsensitiveKey);
|
|
1186
|
+
}
|
|
1187
|
+
return invalidKeys;
|
|
1188
|
+
}
|
|
1189
|
+
function normalizeArcGisMapServerLayers(value) {
|
|
1190
|
+
if (!isNonEmptyString(value))
|
|
1191
|
+
return undefined;
|
|
1192
|
+
const trimmed = value.trim();
|
|
1193
|
+
const layerList = trimmed.toLowerCase().startsWith("show:") ? trimmed.slice(5) : trimmed;
|
|
1194
|
+
const layerIds = layerList.split(",").map((layerId) => layerId.trim());
|
|
1195
|
+
if (layerIds.length === 0 || layerIds.some((layerId) => !/^\d+$/.test(layerId)))
|
|
1196
|
+
return undefined;
|
|
1197
|
+
return layerIds.join(",");
|
|
1198
|
+
}
|
|
1199
|
+
function arcGisImageryEndpoint(url) {
|
|
1200
|
+
const endpoint = url.split(/[?#]/, 1)[0] ?? "";
|
|
1201
|
+
let end = endpoint.length;
|
|
1202
|
+
while (end > 0 && endpoint.charCodeAt(end - 1) === 47)
|
|
1203
|
+
end -= 1;
|
|
1204
|
+
const normalized = endpoint.slice(0, end).toLowerCase();
|
|
1205
|
+
if (normalized.endsWith("/imageserver"))
|
|
1206
|
+
return "image";
|
|
1207
|
+
return normalized.endsWith("/mapserver") ? "map" : undefined;
|
|
1208
|
+
}
|
|
1209
|
+
function isSafeDnsLabel(value) {
|
|
1210
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 63)
|
|
1211
|
+
return false;
|
|
1212
|
+
if (value[0] === "-" || value.at(-1) === "-")
|
|
1213
|
+
return false;
|
|
1214
|
+
for (const character of value) {
|
|
1215
|
+
const lower = character.toLowerCase();
|
|
1216
|
+
const isLetter = lower >= "a" && lower <= "z";
|
|
1217
|
+
const isDigit = character >= "0" && character <= "9";
|
|
1218
|
+
if (!isLetter && !isDigit && character !== "-")
|
|
1219
|
+
return false;
|
|
1220
|
+
}
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
function canonicalParameterKey(key) {
|
|
1224
|
+
let canonical = "";
|
|
1225
|
+
for (const character of key.toLowerCase()) {
|
|
1226
|
+
if (character >= "a" && character <= "z")
|
|
1227
|
+
canonical += character;
|
|
1228
|
+
else if (character >= "0" && character <= "9")
|
|
1229
|
+
canonical += character;
|
|
1230
|
+
}
|
|
1231
|
+
return canonical;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Enforces the credential-free serialization boundary used by workspace state.
|
|
1235
|
+
* Renderability diagnostics remain separate so incomplete-but-safe bindings can
|
|
1236
|
+
* still be inspected, while unsafe or malformed URL material is never retained.
|
|
1237
|
+
* @internal
|
|
1238
|
+
*/
|
|
1239
|
+
export function assertScenePrimitiveSerializable(primitive) {
|
|
1240
|
+
if (primitive.kind === "model-layer") {
|
|
1241
|
+
assertSceneModelPrimitiveSerializable(primitive);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (primitive.kind !== "imagery-layer")
|
|
1245
|
+
return;
|
|
1246
|
+
if (typeof primitive.url !== "string") {
|
|
1247
|
+
throw new TypeError(`Scene imagery primitive '${primitive.id}' has an invalid provider URL.`);
|
|
1248
|
+
}
|
|
1249
|
+
const urlProblem = isNonEmptyString(primitive.url) ? sceneAssetUrlProblem(primitive.url) : undefined;
|
|
1250
|
+
if (urlProblem === "invalid") {
|
|
1251
|
+
throw new TypeError(`Scene imagery primitive '${primitive.id}' has an invalid provider URL.`);
|
|
1252
|
+
}
|
|
1253
|
+
if (urlProblem === "credentials" || hasCredentialParameter(primitive.parameters)) {
|
|
1254
|
+
throw new TypeError(`Scene imagery primitive '${primitive.id}' must be credential-free.`);
|
|
1255
|
+
}
|
|
1256
|
+
if (primitive.parameters !== undefined && !isValidImageryParameters(primitive.parameters)) {
|
|
1257
|
+
throw new TypeError(`Scene imagery primitive '${primitive.id}' has invalid service parameters.`);
|
|
1258
|
+
}
|
|
1259
|
+
if (primitive.subdomains !== undefined && !isValidImagerySubdomains(primitive.subdomains)) {
|
|
1260
|
+
throw new TypeError(`Scene imagery primitive '${primitive.id}' has invalid subdomains.`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Model layers cross the same serialization boundary as imagery: a persisted
|
|
1265
|
+
* plan must never carry a signed tileset/model URI. Placement and shading stay
|
|
1266
|
+
* on the diagnostic path so an incomplete-but-safe binding is still inspectable.
|
|
1267
|
+
*/
|
|
1268
|
+
function assertSceneModelPrimitiveSerializable(primitive) {
|
|
1269
|
+
if (typeof primitive.uri !== "string") {
|
|
1270
|
+
throw new TypeError(`Scene model primitive '${primitive.id}' has an invalid asset URI.`);
|
|
1271
|
+
}
|
|
1272
|
+
const uriProblem = isNonEmptyString(primitive.uri) ? sceneAssetUrlProblem(primitive.uri) : undefined;
|
|
1273
|
+
if (uriProblem === "invalid") {
|
|
1274
|
+
throw new TypeError(`Scene model primitive '${primitive.id}' has an invalid asset URI.`);
|
|
1275
|
+
}
|
|
1276
|
+
if (uriProblem === "credentials") {
|
|
1277
|
+
throw new TypeError(`Scene model primitive '${primitive.id}' must be credential-free.`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Compile protocol-neutral filter clauses into a legacy-style 2D layer filter
|
|
1282
|
+
* scoped to one source.
|
|
1283
|
+
*
|
|
1284
|
+
* Clauses with an `appliesTo` list that excludes `sourceId` are skipped, and a
|
|
1285
|
+
* clause the 2D filter language cannot express (`like`) is dropped rather than
|
|
1286
|
+
* emitted as something weaker. The result always starts with `"all"`, so an
|
|
1287
|
+
* empty clause set compiles to a filter that keeps everything.
|
|
1288
|
+
*
|
|
1289
|
+
* Exported so the shipped state-sync port applies the same compiler the
|
|
1290
|
+
* extrusion binding uses instead of re-deriving it (issue #1049).
|
|
1291
|
+
*/
|
|
1292
|
+
export function compileMapLibreFilters(filters, sourceId) {
|
|
223
1293
|
const compiled = Object.values(filters)
|
|
224
1294
|
.filter((clause) => !clause.appliesTo || clause.appliesTo.length === 0 || clause.appliesTo.includes(sourceId))
|
|
225
1295
|
.map(clauseToMapLibreFilter)
|