@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.
Files changed (400) hide show
  1. package/app/index.d.ts.map +1 -1
  2. package/app/index.js +4 -0
  3. package/collaboration/client.d.ts.map +1 -1
  4. package/connect-url-safety.d.ts +53 -0
  5. package/connect-url-safety.js +189 -0
  6. package/contract/discovery.d.ts +34 -0
  7. package/contract/discovery.d.ts.map +1 -1
  8. package/contract/index.d.ts +7 -5
  9. package/contract/index.d.ts.map +1 -1
  10. package/contract/index.js +3 -2
  11. package/contract/pmtiles.d.ts +10 -1
  12. package/contract/pmtiles.d.ts.map +1 -1
  13. package/contract/pmtiles.js +10 -2
  14. package/contract/protocol-module.d.ts +84 -9
  15. package/contract/protocol-module.d.ts.map +1 -1
  16. package/contract/query-filter-fes.d.ts +18 -0
  17. package/contract/query-filter-fes.d.ts.map +1 -0
  18. package/contract/query-filter-fes.js +122 -0
  19. package/contract/query-filter.d.ts +300 -0
  20. package/contract/query-filter.d.ts.map +1 -0
  21. package/contract/query-filter.js +883 -0
  22. package/contract/schema.d.ts +12 -0
  23. package/contract/schema.d.ts.map +1 -1
  24. package/contract/schema.js +34 -0
  25. package/contract/source-capability-support.d.ts.map +1 -1
  26. package/contract/source-capability-support.js +15 -3
  27. package/contract/source.d.ts +45 -25
  28. package/contract/source.d.ts.map +1 -1
  29. package/contract/source.js +719 -479
  30. package/contract/types.d.ts +88 -19
  31. package/contract/types.d.ts.map +1 -1
  32. package/contract/types.js +4 -0
  33. package/contract/widget-source.d.ts.map +1 -1
  34. package/controls/basemap-switcher.d.ts +19 -11
  35. package/controls/basemap-switcher.d.ts.map +1 -1
  36. package/controls/basemap-switcher.js +58 -25
  37. package/controls/catalog.d.ts +105 -0
  38. package/controls/catalog.d.ts.map +1 -0
  39. package/controls/catalog.js +341 -0
  40. package/controls/element-utils.d.ts +9 -0
  41. package/controls/element-utils.d.ts.map +1 -1
  42. package/controls/element-utils.js +17 -0
  43. package/controls/index.d.ts +29 -8
  44. package/controls/index.d.ts.map +1 -1
  45. package/controls/index.js +26 -8
  46. package/controls/layer-list.d.ts +13 -5
  47. package/controls/layer-list.d.ts.map +1 -1
  48. package/controls/layer-list.js +72 -15
  49. package/controls/legend.d.ts +32 -5
  50. package/controls/legend.d.ts.map +1 -1
  51. package/controls/legend.js +95 -25
  52. package/controls/qualification.d.ts +152 -0
  53. package/controls/qualification.d.ts.map +1 -0
  54. package/controls/qualification.js +1265 -0
  55. package/controls/registry.d.ts +104 -0
  56. package/controls/registry.d.ts.map +1 -0
  57. package/controls/registry.js +150 -0
  58. package/controls/swipe-control.d.ts +6 -1
  59. package/controls/swipe-control.d.ts.map +1 -1
  60. package/controls/swipe-control.js +82 -20
  61. package/core/client.d.ts +51 -21
  62. package/core/client.d.ts.map +1 -1
  63. package/core/client.js +234 -54
  64. package/core/error-classifications.d.ts +23 -0
  65. package/core/error-classifications.d.ts.map +1 -1
  66. package/core/error-classifications.js +23 -0
  67. package/core/error-code-registry.d.ts +24 -1
  68. package/core/error-code-registry.d.ts.map +1 -1
  69. package/core/error-code-registry.js +23 -0
  70. package/core/error-envelope.js +2 -0
  71. package/core/errors.d.ts +2 -2
  72. package/core/errors.d.ts.map +1 -1
  73. package/core/errors.js +1 -1
  74. package/core/geoparquet-sql.d.ts +69 -5
  75. package/core/geoparquet-sql.d.ts.map +1 -1
  76. package/core/geoparquet-sql.js +327 -10
  77. package/core/odata-write-codec.d.ts.map +1 -1
  78. package/core/odata-write-codec.js +0 -1
  79. package/core/odata.d.ts +6 -0
  80. package/core/odata.d.ts.map +1 -1
  81. package/core/odata.js +6 -1
  82. package/core/ogc-processes.d.ts +182 -25
  83. package/core/ogc-processes.d.ts.map +1 -1
  84. package/core/ogc-processes.js +602 -30
  85. package/core/process-runner.d.ts +8 -1
  86. package/core/process-runner.d.ts.map +1 -1
  87. package/core/process-runner.js +1 -0
  88. package/core/protocol-transport.d.ts +2 -0
  89. package/core/protocol-transport.d.ts.map +1 -1
  90. package/core/stac.d.ts +32 -0
  91. package/core/stac.d.ts.map +1 -1
  92. package/core/stac.js +158 -23
  93. package/core/surfaces.d.ts.map +1 -1
  94. package/core/surfaces.js +273 -68
  95. package/core/types.d.ts +138 -11
  96. package/core/types.d.ts.map +1 -1
  97. package/core/wfs-axis-order.d.ts +18 -0
  98. package/core/wfs-axis-order.d.ts.map +1 -0
  99. package/core/wfs-axis-order.js +108 -0
  100. package/core/wfs-capabilities.d.ts +65 -24
  101. package/core/wfs-capabilities.d.ts.map +1 -1
  102. package/core/wfs-capabilities.js +237 -27
  103. package/core/wfs-filter.d.ts +26 -3
  104. package/core/wfs-filter.d.ts.map +1 -1
  105. package/core/wfs-filter.js +48 -53
  106. package/core/wfs-protocol-error.d.ts +12 -0
  107. package/core/wfs-protocol-error.d.ts.map +1 -0
  108. package/core/wfs-protocol-error.js +13 -0
  109. package/core/wfs.d.ts +53 -1
  110. package/core/wfs.d.ts.map +1 -1
  111. package/core/wfs.js +251 -29
  112. package/core/wms-feature-info.d.ts +92 -0
  113. package/core/wms-feature-info.d.ts.map +1 -0
  114. package/core/wms-feature-info.js +285 -0
  115. package/core/wms.d.ts +9 -0
  116. package/core/wms.d.ts.map +1 -1
  117. package/core/wms.js +17 -5
  118. package/esri-compat/event-bus.d.ts +20 -0
  119. package/esri-compat/event-bus.d.ts.map +1 -1
  120. package/esri-compat/feature-filter.d.ts +46 -1
  121. package/esri-compat/feature-filter.d.ts.map +1 -1
  122. package/esri-compat/feature-filter.js +49 -0
  123. package/esri-compat/geojson-layer.d.ts +4 -2
  124. package/esri-compat/geojson-layer.d.ts.map +1 -1
  125. package/esri-compat/geojson-layer.js +4 -2
  126. package/esri-compat/identity-manager.js +2 -2
  127. package/esri-compat/imagery-layer.d.ts +2 -1
  128. package/esri-compat/imagery-layer.d.ts.map +1 -1
  129. package/esri-compat/imagery-layer.js +2 -1
  130. package/esri-compat/layer-list.d.ts +8 -1
  131. package/esri-compat/layer-list.d.ts.map +1 -1
  132. package/esri-compat/layer-list.js +11 -4
  133. package/esri-compat/legend.d.ts.map +1 -1
  134. package/esri-compat/legend.js +1 -1
  135. package/esri-compat/locator.d.ts +297 -0
  136. package/esri-compat/locator.d.ts.map +1 -0
  137. package/esri-compat/locator.js +579 -0
  138. package/esri-compat/map-image-layer.d.ts.map +1 -1
  139. package/esri-compat/map-view.d.ts.map +1 -1
  140. package/esri-compat/map-view.js +1 -1
  141. package/esri-compat/scene-view.d.ts.map +1 -1
  142. package/esri-compat/time-slider.d.ts +29 -0
  143. package/esri-compat/time-slider.d.ts.map +1 -1
  144. package/esri-compat/time-slider.js +117 -0
  145. package/esri-compat/wfs-layer.d.ts +2 -1
  146. package/esri-compat/wfs-layer.d.ts.map +1 -1
  147. package/esri-compat/wfs-layer.js +2 -1
  148. package/esri-compat/widget-host.d.ts +42 -6
  149. package/esri-compat/widget-host.d.ts.map +1 -1
  150. package/esri-compat/widget-host.js +99 -10
  151. package/generated-app/projection.d.ts.map +1 -1
  152. package/generated-app/runtime.d.ts.map +1 -1
  153. package/interactions/declarative-contract.d.ts +149 -0
  154. package/interactions/declarative-contract.d.ts.map +1 -0
  155. package/interactions/declarative-contract.js +293 -0
  156. package/interactions/declarative.d.ts +198 -0
  157. package/interactions/declarative.d.ts.map +1 -0
  158. package/interactions/declarative.js +470 -0
  159. package/interactions/feature-state.d.ts +7 -0
  160. package/interactions/feature-state.d.ts.map +1 -1
  161. package/interactions/feature-state.js +10 -2
  162. package/map/data-to-map-bridge.d.ts +1 -1
  163. package/map/data-to-map-bridge.d.ts.map +1 -1
  164. package/map/data-to-map-bridge.js +1 -2
  165. package/map/honua-map.d.ts.map +1 -1
  166. package/map/honua-map.js +1 -1
  167. package/map/index.d.ts +2 -0
  168. package/map/index.d.ts.map +1 -1
  169. package/map/index.js +2 -0
  170. package/map/maplibre-target.js +1 -1
  171. package/map/realtime-reconciliation-adapter.d.ts +98 -0
  172. package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
  173. package/map/realtime-reconciliation-adapter.js +84 -0
  174. package/map/temporal-playback.d.ts +25 -0
  175. package/map/temporal-playback.d.ts.map +1 -1
  176. package/map/temporal-playback.js +29 -1
  177. package/operator/workspace/workspace.d.ts.map +1 -1
  178. package/operator/workspace/workspace.js +0 -2
  179. package/package.json +4 -3
  180. package/query-planner/canonical-filter.d.ts +30 -0
  181. package/query-planner/canonical-filter.d.ts.map +1 -0
  182. package/query-planner/canonical-filter.js +30 -0
  183. package/query-planner/columnar-plan.d.ts +63 -0
  184. package/query-planner/columnar-plan.d.ts.map +1 -0
  185. package/query-planner/columnar-plan.js +94 -0
  186. package/query-planner/diagnostics.d.ts +3 -3
  187. package/query-planner/diagnostics.d.ts.map +1 -1
  188. package/query-planner/diagnostics.js +17 -4
  189. package/query-planner/duckdb.d.ts.map +1 -1
  190. package/query-planner/duckdb.js +16 -2
  191. package/query-planner/execution-context.d.ts.map +1 -1
  192. package/query-planner/execution-context.js +3 -1
  193. package/query-planner/executor.d.ts.map +1 -1
  194. package/query-planner/executor.js +7 -1
  195. package/query-planner/geoservices.d.ts.map +1 -1
  196. package/query-planner/geoservices.js +24 -5
  197. package/query-planner/grpc.d.ts.map +1 -1
  198. package/query-planner/grpc.js +29 -5
  199. package/query-planner/index.d.ts +20 -5
  200. package/query-planner/index.d.ts.map +1 -1
  201. package/query-planner/index.js +16 -3
  202. package/query-planner/ir.d.ts +6 -1
  203. package/query-planner/ir.d.ts.map +1 -1
  204. package/query-planner/ir.js +63 -1
  205. package/query-planner/odata-protocol-module.d.ts +54 -0
  206. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  207. package/query-planner/odata-protocol-module.js +199 -0
  208. package/query-planner/odata-v1.d.ts +23 -0
  209. package/query-planner/odata-v1.d.ts.map +1 -0
  210. package/query-planner/odata-v1.js +202 -0
  211. package/query-planner/odata.d.ts +0 -3
  212. package/query-planner/odata.d.ts.map +1 -1
  213. package/query-planner/odata.js +0 -143
  214. package/query-planner/ogc-features.d.ts.map +1 -1
  215. package/query-planner/ogc-features.js +23 -1
  216. package/query-planner/planner.d.ts +2 -2
  217. package/query-planner/planner.d.ts.map +1 -1
  218. package/query-planner/planner.js +146 -27
  219. package/query-planner/representation.d.ts +59 -0
  220. package/query-planner/representation.d.ts.map +1 -0
  221. package/query-planner/representation.js +140 -0
  222. package/query-planner/types.d.ts +153 -3
  223. package/query-planner/types.d.ts.map +1 -1
  224. package/query-planner/wfs-protocol-module.d.ts +57 -0
  225. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  226. package/query-planner/wfs-protocol-module.js +731 -0
  227. package/query-planner/wfs-v1.d.ts +41 -0
  228. package/query-planner/wfs-v1.d.ts.map +1 -0
  229. package/query-planner/wfs-v1.js +366 -0
  230. package/query-planner/wfs.d.ts +1 -3
  231. package/query-planner/wfs.d.ts.map +1 -1
  232. package/query-planner/wfs.js +1 -70
  233. package/realtime/checkpoint-store.d.ts +206 -0
  234. package/realtime/checkpoint-store.d.ts.map +1 -0
  235. package/realtime/checkpoint-store.js +504 -0
  236. package/realtime/honua-server.d.ts +8 -0
  237. package/realtime/honua-server.d.ts.map +1 -1
  238. package/realtime/honua-server.js +123 -14
  239. package/realtime/index.d.ts +4 -0
  240. package/realtime/index.d.ts.map +1 -1
  241. package/realtime/index.js +2 -0
  242. package/realtime/reconciliation.d.ts +245 -0
  243. package/realtime/reconciliation.d.ts.map +1 -0
  244. package/realtime/reconciliation.js +377 -0
  245. package/realtime/resumable-transport.d.ts.map +1 -1
  246. package/realtime/sse.d.ts.map +1 -1
  247. package/realtime/sse.js +7 -1
  248. package/realtime/websocket.d.ts.map +1 -1
  249. package/realtime/wire.d.ts +2 -1
  250. package/realtime/wire.d.ts.map +1 -1
  251. package/realtime/wire.js +24 -8
  252. package/replica-sync/errors.d.ts +19 -1
  253. package/replica-sync/errors.d.ts.map +1 -1
  254. package/replica-sync/errors.js +16 -0
  255. package/replica-sync/geoservices.d.ts +260 -0
  256. package/replica-sync/geoservices.d.ts.map +1 -0
  257. package/replica-sync/geoservices.js +1093 -0
  258. package/replica-sync/index.d.ts +5 -1
  259. package/replica-sync/index.d.ts.map +1 -1
  260. package/replica-sync/index.js +3 -1
  261. package/replica-sync/transport-conformance.d.ts +58 -0
  262. package/replica-sync/transport-conformance.d.ts.map +1 -0
  263. package/replica-sync/transport-conformance.js +265 -0
  264. package/replica-sync/types.d.ts +22 -0
  265. package/replica-sync/types.d.ts.map +1 -1
  266. package/runtime/index.d.ts +6 -2
  267. package/runtime/index.d.ts.map +1 -1
  268. package/runtime/index.js +3 -1
  269. package/runtime/load-package.d.ts +24 -0
  270. package/runtime/load-package.d.ts.map +1 -1
  271. package/runtime/load-package.js +45 -1
  272. package/runtime/maplibre-renderer.d.ts +11 -0
  273. package/runtime/maplibre-renderer.d.ts.map +1 -1
  274. package/runtime/maplibre-renderer.js +32 -7
  275. package/runtime/offline-region-protocol.d.ts +73 -0
  276. package/runtime/offline-region-protocol.d.ts.map +1 -0
  277. package/runtime/offline-region-protocol.js +63 -0
  278. package/runtime/offline-region-style.d.ts +78 -0
  279. package/runtime/offline-region-style.d.ts.map +1 -0
  280. package/runtime/offline-region-style.js +147 -0
  281. package/runtime/pmtiles-protocol.d.ts +7 -8
  282. package/runtime/pmtiles-protocol.d.ts.map +1 -1
  283. package/runtime/pmtiles-protocol.js +19 -66
  284. package/runtime/protocol-registry.d.ts +55 -0
  285. package/runtime/protocol-registry.d.ts.map +1 -0
  286. package/runtime/protocol-registry.js +103 -0
  287. package/runtime/runtime.d.ts +2 -0
  288. package/runtime/runtime.d.ts.map +1 -1
  289. package/runtime/runtime.js +6 -1
  290. package/runtime/style-compose.d.ts.map +1 -1
  291. package/runtime/style-compose.js +78 -9
  292. package/scene-workspace/analysis-widgets.d.ts +4 -2
  293. package/scene-workspace/analysis-widgets.d.ts.map +1 -1
  294. package/scene-workspace/analysis-widgets.js +4 -2
  295. package/scene-workspace/camera-correspondence.d.ts +151 -0
  296. package/scene-workspace/camera-correspondence.d.ts.map +1 -0
  297. package/scene-workspace/camera-correspondence.js +220 -0
  298. package/scene-workspace/cesium-adapter.d.ts +230 -29
  299. package/scene-workspace/cesium-adapter.d.ts.map +1 -1
  300. package/scene-workspace/cesium-adapter.js +741 -56
  301. package/scene-workspace/cesium-mount.d.ts +205 -0
  302. package/scene-workspace/cesium-mount.d.ts.map +1 -0
  303. package/scene-workspace/cesium-mount.js +392 -0
  304. package/scene-workspace/cesium-scene-owner.d.ts +160 -0
  305. package/scene-workspace/cesium-scene-owner.d.ts.map +1 -0
  306. package/scene-workspace/cesium-scene-owner.js +205 -0
  307. package/scene-workspace/cesium-state-sync.d.ts +101 -0
  308. package/scene-workspace/cesium-state-sync.d.ts.map +1 -0
  309. package/scene-workspace/cesium-state-sync.js +432 -0
  310. package/scene-workspace/cesium-time.d.ts +346 -0
  311. package/scene-workspace/cesium-time.d.ts.map +1 -0
  312. package/scene-workspace/cesium-time.js +403 -0
  313. package/scene-workspace/index.d.ts +42 -10
  314. package/scene-workspace/index.d.ts.map +1 -1
  315. package/scene-workspace/index.js +32 -7
  316. package/scene-workspace/maplibre-state-sync.d.ts +112 -0
  317. package/scene-workspace/maplibre-state-sync.d.ts.map +1 -0
  318. package/scene-workspace/maplibre-state-sync.js +400 -0
  319. package/scene-workspace/primitives.d.ts +237 -3
  320. package/scene-workspace/primitives.d.ts.map +1 -1
  321. package/scene-workspace/primitives.js +1087 -17
  322. package/scene-workspace/scene-discovery.d.ts +5 -2
  323. package/scene-workspace/scene-discovery.d.ts.map +1 -1
  324. package/scene-workspace/scene-discovery.js +5 -2
  325. package/scene-workspace/scene-view.d.ts +5 -2
  326. package/scene-workspace/scene-view.d.ts.map +1 -1
  327. package/scene-workspace/scene-view.js +5 -2
  328. package/scene-workspace/source-to-cesium.d.ts +141 -8
  329. package/scene-workspace/source-to-cesium.d.ts.map +1 -1
  330. package/scene-workspace/source-to-cesium.js +497 -72
  331. package/scene-workspace/state-sync-port-kit.d.ts +156 -0
  332. package/scene-workspace/state-sync-port-kit.d.ts.map +1 -0
  333. package/scene-workspace/state-sync-port-kit.js +319 -0
  334. package/scene-workspace/state-sync.d.ts +34 -1
  335. package/scene-workspace/state-sync.d.ts.map +1 -1
  336. package/scene-workspace/state-sync.js +48 -0
  337. package/scene-workspace/types.d.ts +8 -0
  338. package/scene-workspace/types.d.ts.map +1 -1
  339. package/scene-workspace/types.js +2 -0
  340. package/scene-workspace/workspace.d.ts +12 -1
  341. package/scene-workspace/workspace.d.ts.map +1 -1
  342. package/scene-workspace/workspace.js +13 -0
  343. package/studio/index.d.ts +24 -7
  344. package/studio/index.d.ts.map +1 -1
  345. package/studio/index.js +21 -7
  346. package/studio/lifecycle-client.d.ts +164 -0
  347. package/studio/lifecycle-client.d.ts.map +1 -0
  348. package/studio/lifecycle-client.js +299 -0
  349. package/studio/lifecycle-errors.d.ts +91 -0
  350. package/studio/lifecycle-errors.d.ts.map +1 -0
  351. package/studio/lifecycle-errors.js +77 -0
  352. package/studio/lifecycle-types.d.ts +321 -0
  353. package/studio/lifecycle-types.d.ts.map +1 -0
  354. package/studio/lifecycle-types.js +22 -0
  355. package/web-components/controller.d.ts.map +1 -1
  356. package/web-components/csp-styles.d.ts +6 -0
  357. package/web-components/csp-styles.d.ts.map +1 -0
  358. package/web-components/csp-styles.js +53 -0
  359. package/web-components/elements.d.ts +158 -2
  360. package/web-components/elements.d.ts.map +1 -1
  361. package/web-components/elements.js +1302 -215
  362. package/web-components/export-redaction.d.ts +252 -0
  363. package/web-components/export-redaction.d.ts.map +1 -0
  364. package/web-components/export-redaction.js +588 -0
  365. package/web-components/export.d.ts +417 -0
  366. package/web-components/export.d.ts.map +1 -0
  367. package/web-components/export.js +719 -0
  368. package/web-components/feature-editor-model.d.ts +265 -0
  369. package/web-components/feature-editor-model.d.ts.map +1 -0
  370. package/web-components/feature-editor-model.js +438 -0
  371. package/web-components/feature-editor-sketch.d.ts +125 -0
  372. package/web-components/feature-editor-sketch.d.ts.map +1 -0
  373. package/web-components/feature-editor-sketch.js +194 -0
  374. package/web-components/feature-editor-workflow.d.ts +286 -0
  375. package/web-components/feature-editor-workflow.d.ts.map +1 -0
  376. package/web-components/feature-editor-workflow.js +889 -0
  377. package/web-components/feature-editor.d.ts +83 -0
  378. package/web-components/feature-editor.d.ts.map +1 -0
  379. package/web-components/feature-editor.js +829 -0
  380. package/web-components/feature-table-engine.d.ts +465 -0
  381. package/web-components/feature-table-engine.d.ts.map +1 -0
  382. package/web-components/feature-table-engine.js +1360 -0
  383. package/web-components/feature-table-view.d.ts +95 -0
  384. package/web-components/feature-table-view.d.ts.map +1 -0
  385. package/web-components/feature-table-view.js +343 -0
  386. package/web-components/index.d.ts +34 -3
  387. package/web-components/index.d.ts.map +1 -1
  388. package/web-components/index.js +35 -2
  389. package/web-components/maplibre-renderer.d.ts.map +1 -1
  390. package/web-components/maplibre-renderer.js +13 -4
  391. package/web-components/measurement.d.ts +3 -1
  392. package/web-components/measurement.d.ts.map +1 -1
  393. package/web-components/measurement.js +62 -18
  394. package/web-components/time-slider.d.ts +122 -0
  395. package/web-components/time-slider.d.ts.map +1 -0
  396. package/web-components/time-slider.js +740 -0
  397. package/web-components/types.d.ts +310 -0
  398. package/web-components/types.d.ts.map +1 -1
  399. package/webmap/convert-label.js +0 -1
  400. 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
- if (primitive.protocol === "terrain-rgb") {
60
- const terrainDiagnostics = diagnoseRenderableTerrainRgb(primitive, capabilities);
61
- if (terrainDiagnostics.length > 0)
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 formats = capabilities.modelLayer?.formats ?? [];
74
- if (formats.includes(primitive.format))
75
- return [supported(primitive, capabilities, "Model layer format is supported.")];
76
- return [
77
- unsupported(primitive, capabilities, `Model format '${primitive.format}' is not supported by ${capabilities.renderer}.`, "Preserve model metadata and route to a 3D renderer adapter."),
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
- const terrainDiagnostic = diagnostics.find((diagnostic) => diagnostic.primitiveId === primitive.id);
134
- if (terrainDiagnostic?.status === "unsupported")
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
- function diagnoseRenderableTerrainRgb(primitive, capabilities) {
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, "terrain-rgb 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."));
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, "terrain-rgb elevation source has invalid numeric rendering ranges.", "Use finite positive tileSize/exaggeration values and ordered zoom values between 0 and 24."),
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 compileMapLibreFilters(filters, sourceId) {
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)