@honua/app-platform 0.1.2-beta.0 → 0.1.4-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 (383) hide show
  1. package/collaboration/client.d.ts.map +1 -1
  2. package/connect-url-safety.d.ts +53 -0
  3. package/connect-url-safety.js +189 -0
  4. package/contract/discovery.d.ts +34 -0
  5. package/contract/discovery.d.ts.map +1 -1
  6. package/contract/index.d.ts +7 -5
  7. package/contract/index.d.ts.map +1 -1
  8. package/contract/index.js +3 -2
  9. package/contract/pmtiles.d.ts +10 -1
  10. package/contract/pmtiles.d.ts.map +1 -1
  11. package/contract/pmtiles.js +10 -2
  12. package/contract/protocol-module.d.ts +84 -9
  13. package/contract/protocol-module.d.ts.map +1 -1
  14. package/contract/query-filter-fes.d.ts +18 -0
  15. package/contract/query-filter-fes.d.ts.map +1 -0
  16. package/contract/query-filter-fes.js +122 -0
  17. package/contract/query-filter.d.ts +300 -0
  18. package/contract/query-filter.d.ts.map +1 -0
  19. package/contract/query-filter.js +883 -0
  20. package/contract/schema.d.ts +12 -0
  21. package/contract/schema.d.ts.map +1 -1
  22. package/contract/schema.js +34 -0
  23. package/contract/source-capability-support.d.ts.map +1 -1
  24. package/contract/source-capability-support.js +15 -3
  25. package/contract/source.d.ts +45 -25
  26. package/contract/source.d.ts.map +1 -1
  27. package/contract/source.js +719 -479
  28. package/contract/types.d.ts +87 -18
  29. package/contract/types.d.ts.map +1 -1
  30. package/contract/widget-source.d.ts.map +1 -1
  31. package/controls/basemap-switcher.d.ts +19 -11
  32. package/controls/basemap-switcher.d.ts.map +1 -1
  33. package/controls/basemap-switcher.js +58 -25
  34. package/controls/catalog.d.ts +105 -0
  35. package/controls/catalog.d.ts.map +1 -0
  36. package/controls/catalog.js +341 -0
  37. package/controls/element-utils.d.ts +9 -0
  38. package/controls/element-utils.d.ts.map +1 -1
  39. package/controls/element-utils.js +17 -0
  40. package/controls/index.d.ts +29 -8
  41. package/controls/index.d.ts.map +1 -1
  42. package/controls/index.js +26 -8
  43. package/controls/layer-list.d.ts +13 -5
  44. package/controls/layer-list.d.ts.map +1 -1
  45. package/controls/layer-list.js +72 -15
  46. package/controls/legend.d.ts +32 -5
  47. package/controls/legend.d.ts.map +1 -1
  48. package/controls/legend.js +95 -25
  49. package/controls/qualification.d.ts +152 -0
  50. package/controls/qualification.d.ts.map +1 -0
  51. package/controls/qualification.js +1265 -0
  52. package/controls/registry.d.ts +104 -0
  53. package/controls/registry.d.ts.map +1 -0
  54. package/controls/registry.js +150 -0
  55. package/controls/swipe-control.d.ts +6 -1
  56. package/controls/swipe-control.d.ts.map +1 -1
  57. package/controls/swipe-control.js +82 -20
  58. package/core/client.d.ts +45 -21
  59. package/core/client.d.ts.map +1 -1
  60. package/core/client.js +224 -46
  61. package/core/error-classifications.d.ts +6 -0
  62. package/core/error-classifications.d.ts.map +1 -1
  63. package/core/error-classifications.js +6 -0
  64. package/core/error-code-registry.d.ts +6 -0
  65. package/core/error-code-registry.d.ts.map +1 -1
  66. package/core/error-code-registry.js +6 -0
  67. package/core/error-envelope.js +2 -0
  68. package/core/errors.d.ts +1 -1
  69. package/core/errors.js +1 -1
  70. package/core/geoparquet-sql.d.ts +68 -4
  71. package/core/geoparquet-sql.d.ts.map +1 -1
  72. package/core/geoparquet-sql.js +325 -8
  73. package/core/odata-write-codec.d.ts.map +1 -1
  74. package/core/odata-write-codec.js +0 -1
  75. package/core/odata.d.ts +6 -0
  76. package/core/odata.d.ts.map +1 -1
  77. package/core/odata.js +6 -1
  78. package/core/ogc-processes.d.ts +182 -25
  79. package/core/ogc-processes.d.ts.map +1 -1
  80. package/core/ogc-processes.js +602 -30
  81. package/core/process-runner.d.ts +8 -1
  82. package/core/process-runner.d.ts.map +1 -1
  83. package/core/process-runner.js +1 -0
  84. package/core/protocol-transport.d.ts +2 -0
  85. package/core/protocol-transport.d.ts.map +1 -1
  86. package/core/stac.d.ts +31 -0
  87. package/core/stac.d.ts.map +1 -1
  88. package/core/stac.js +136 -19
  89. package/core/surfaces.d.ts.map +1 -1
  90. package/core/surfaces.js +273 -68
  91. package/core/types.d.ts +123 -9
  92. package/core/types.d.ts.map +1 -1
  93. package/core/wfs-axis-order.d.ts +18 -0
  94. package/core/wfs-axis-order.d.ts.map +1 -0
  95. package/core/wfs-axis-order.js +108 -0
  96. package/core/wfs-capabilities.d.ts +65 -24
  97. package/core/wfs-capabilities.d.ts.map +1 -1
  98. package/core/wfs-capabilities.js +237 -27
  99. package/core/wfs-filter.d.ts +26 -3
  100. package/core/wfs-filter.d.ts.map +1 -1
  101. package/core/wfs-filter.js +48 -53
  102. package/core/wfs-protocol-error.d.ts +12 -0
  103. package/core/wfs-protocol-error.d.ts.map +1 -0
  104. package/core/wfs-protocol-error.js +13 -0
  105. package/core/wfs.d.ts +53 -1
  106. package/core/wfs.d.ts.map +1 -1
  107. package/core/wfs.js +251 -29
  108. package/core/wms-feature-info.d.ts +92 -0
  109. package/core/wms-feature-info.d.ts.map +1 -0
  110. package/core/wms-feature-info.js +285 -0
  111. package/core/wms.d.ts +9 -0
  112. package/core/wms.d.ts.map +1 -1
  113. package/core/wms.js +17 -5
  114. package/esri-compat/event-bus.d.ts +20 -0
  115. package/esri-compat/event-bus.d.ts.map +1 -1
  116. package/esri-compat/feature-filter.d.ts +46 -1
  117. package/esri-compat/feature-filter.d.ts.map +1 -1
  118. package/esri-compat/feature-filter.js +49 -0
  119. package/esri-compat/geojson-layer.d.ts +4 -2
  120. package/esri-compat/geojson-layer.d.ts.map +1 -1
  121. package/esri-compat/geojson-layer.js +4 -2
  122. package/esri-compat/identity-manager.js +2 -2
  123. package/esri-compat/imagery-layer.d.ts +2 -1
  124. package/esri-compat/imagery-layer.d.ts.map +1 -1
  125. package/esri-compat/imagery-layer.js +2 -1
  126. package/esri-compat/layer-list.d.ts +8 -1
  127. package/esri-compat/layer-list.d.ts.map +1 -1
  128. package/esri-compat/layer-list.js +11 -4
  129. package/esri-compat/legend.d.ts.map +1 -1
  130. package/esri-compat/legend.js +1 -1
  131. package/esri-compat/locator.d.ts +297 -0
  132. package/esri-compat/locator.d.ts.map +1 -0
  133. package/esri-compat/locator.js +579 -0
  134. package/esri-compat/map-image-layer.d.ts.map +1 -1
  135. package/esri-compat/map-view.d.ts.map +1 -1
  136. package/esri-compat/map-view.js +1 -1
  137. package/esri-compat/scene-view.d.ts.map +1 -1
  138. package/esri-compat/time-slider.d.ts +29 -0
  139. package/esri-compat/time-slider.d.ts.map +1 -1
  140. package/esri-compat/time-slider.js +117 -0
  141. package/esri-compat/wfs-layer.d.ts +2 -1
  142. package/esri-compat/wfs-layer.d.ts.map +1 -1
  143. package/esri-compat/wfs-layer.js +2 -1
  144. package/esri-compat/widget-host.d.ts +42 -6
  145. package/esri-compat/widget-host.d.ts.map +1 -1
  146. package/esri-compat/widget-host.js +99 -10
  147. package/generated-app/projection.d.ts.map +1 -1
  148. package/generated-app/runtime.d.ts.map +1 -1
  149. package/map/data-to-map-bridge.d.ts +1 -1
  150. package/map/data-to-map-bridge.d.ts.map +1 -1
  151. package/map/data-to-map-bridge.js +1 -2
  152. package/map/honua-map.d.ts.map +1 -1
  153. package/map/honua-map.js +1 -1
  154. package/map/index.d.ts +2 -0
  155. package/map/index.d.ts.map +1 -1
  156. package/map/index.js +2 -0
  157. package/map/realtime-reconciliation-adapter.d.ts +98 -0
  158. package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
  159. package/map/realtime-reconciliation-adapter.js +84 -0
  160. package/map/temporal-playback.d.ts +25 -0
  161. package/map/temporal-playback.d.ts.map +1 -1
  162. package/map/temporal-playback.js +29 -1
  163. package/operator/workspace/workspace.d.ts.map +1 -1
  164. package/operator/workspace/workspace.js +0 -2
  165. package/package.json +4 -3
  166. package/query-planner/canonical-filter.d.ts +30 -0
  167. package/query-planner/canonical-filter.d.ts.map +1 -0
  168. package/query-planner/canonical-filter.js +30 -0
  169. package/query-planner/columnar-plan.d.ts +63 -0
  170. package/query-planner/columnar-plan.d.ts.map +1 -0
  171. package/query-planner/columnar-plan.js +94 -0
  172. package/query-planner/diagnostics.d.ts +3 -3
  173. package/query-planner/diagnostics.d.ts.map +1 -1
  174. package/query-planner/diagnostics.js +17 -4
  175. package/query-planner/duckdb.d.ts.map +1 -1
  176. package/query-planner/duckdb.js +16 -2
  177. package/query-planner/execution-context.d.ts.map +1 -1
  178. package/query-planner/execution-context.js +3 -1
  179. package/query-planner/executor.d.ts.map +1 -1
  180. package/query-planner/executor.js +7 -1
  181. package/query-planner/geoservices.d.ts.map +1 -1
  182. package/query-planner/geoservices.js +24 -5
  183. package/query-planner/grpc.d.ts.map +1 -1
  184. package/query-planner/grpc.js +29 -5
  185. package/query-planner/index.d.ts +20 -5
  186. package/query-planner/index.d.ts.map +1 -1
  187. package/query-planner/index.js +16 -3
  188. package/query-planner/ir.d.ts +6 -1
  189. package/query-planner/ir.d.ts.map +1 -1
  190. package/query-planner/ir.js +63 -1
  191. package/query-planner/odata-protocol-module.d.ts +54 -0
  192. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  193. package/query-planner/odata-protocol-module.js +199 -0
  194. package/query-planner/odata-v1.d.ts +23 -0
  195. package/query-planner/odata-v1.d.ts.map +1 -0
  196. package/query-planner/odata-v1.js +202 -0
  197. package/query-planner/odata.d.ts +0 -3
  198. package/query-planner/odata.d.ts.map +1 -1
  199. package/query-planner/odata.js +0 -143
  200. package/query-planner/ogc-features.d.ts.map +1 -1
  201. package/query-planner/ogc-features.js +23 -1
  202. package/query-planner/planner.d.ts +2 -2
  203. package/query-planner/planner.d.ts.map +1 -1
  204. package/query-planner/planner.js +146 -27
  205. package/query-planner/representation.d.ts +59 -0
  206. package/query-planner/representation.d.ts.map +1 -0
  207. package/query-planner/representation.js +140 -0
  208. package/query-planner/types.d.ts +153 -3
  209. package/query-planner/types.d.ts.map +1 -1
  210. package/query-planner/wfs-protocol-module.d.ts +57 -0
  211. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  212. package/query-planner/wfs-protocol-module.js +731 -0
  213. package/query-planner/wfs-v1.d.ts +41 -0
  214. package/query-planner/wfs-v1.d.ts.map +1 -0
  215. package/query-planner/wfs-v1.js +366 -0
  216. package/query-planner/wfs.d.ts +1 -3
  217. package/query-planner/wfs.d.ts.map +1 -1
  218. package/query-planner/wfs.js +1 -70
  219. package/realtime/checkpoint-store.d.ts +206 -0
  220. package/realtime/checkpoint-store.d.ts.map +1 -0
  221. package/realtime/checkpoint-store.js +504 -0
  222. package/realtime/honua-server.d.ts +8 -0
  223. package/realtime/honua-server.d.ts.map +1 -1
  224. package/realtime/honua-server.js +123 -14
  225. package/realtime/index.d.ts +4 -0
  226. package/realtime/index.d.ts.map +1 -1
  227. package/realtime/index.js +2 -0
  228. package/realtime/reconciliation.d.ts +245 -0
  229. package/realtime/reconciliation.d.ts.map +1 -0
  230. package/realtime/reconciliation.js +377 -0
  231. package/realtime/resumable-transport.d.ts.map +1 -1
  232. package/realtime/sse.d.ts.map +1 -1
  233. package/realtime/websocket.d.ts.map +1 -1
  234. package/realtime/wire.d.ts +2 -1
  235. package/realtime/wire.d.ts.map +1 -1
  236. package/realtime/wire.js +24 -8
  237. package/replica-sync/errors.d.ts +19 -1
  238. package/replica-sync/errors.d.ts.map +1 -1
  239. package/replica-sync/errors.js +16 -0
  240. package/replica-sync/geoservices.d.ts +260 -0
  241. package/replica-sync/geoservices.d.ts.map +1 -0
  242. package/replica-sync/geoservices.js +1093 -0
  243. package/replica-sync/index.d.ts +5 -1
  244. package/replica-sync/index.d.ts.map +1 -1
  245. package/replica-sync/index.js +3 -1
  246. package/replica-sync/transport-conformance.d.ts +58 -0
  247. package/replica-sync/transport-conformance.d.ts.map +1 -0
  248. package/replica-sync/transport-conformance.js +265 -0
  249. package/replica-sync/types.d.ts +22 -0
  250. package/replica-sync/types.d.ts.map +1 -1
  251. package/runtime/index.d.ts +6 -2
  252. package/runtime/index.d.ts.map +1 -1
  253. package/runtime/index.js +3 -1
  254. package/runtime/load-package.d.ts +24 -0
  255. package/runtime/load-package.d.ts.map +1 -1
  256. package/runtime/load-package.js +45 -1
  257. package/runtime/maplibre-renderer.d.ts +11 -0
  258. package/runtime/maplibre-renderer.d.ts.map +1 -1
  259. package/runtime/maplibre-renderer.js +32 -7
  260. package/runtime/offline-region-protocol.d.ts +73 -0
  261. package/runtime/offline-region-protocol.d.ts.map +1 -0
  262. package/runtime/offline-region-protocol.js +63 -0
  263. package/runtime/offline-region-style.d.ts +78 -0
  264. package/runtime/offline-region-style.d.ts.map +1 -0
  265. package/runtime/offline-region-style.js +147 -0
  266. package/runtime/pmtiles-protocol.d.ts +7 -8
  267. package/runtime/pmtiles-protocol.d.ts.map +1 -1
  268. package/runtime/pmtiles-protocol.js +19 -66
  269. package/runtime/protocol-registry.d.ts +55 -0
  270. package/runtime/protocol-registry.d.ts.map +1 -0
  271. package/runtime/protocol-registry.js +103 -0
  272. package/runtime/runtime.d.ts +2 -0
  273. package/runtime/runtime.d.ts.map +1 -1
  274. package/runtime/runtime.js +6 -1
  275. package/scene-workspace/analysis-widgets.d.ts +4 -2
  276. package/scene-workspace/analysis-widgets.d.ts.map +1 -1
  277. package/scene-workspace/analysis-widgets.js +4 -2
  278. package/scene-workspace/camera-correspondence.d.ts +151 -0
  279. package/scene-workspace/camera-correspondence.d.ts.map +1 -0
  280. package/scene-workspace/camera-correspondence.js +220 -0
  281. package/scene-workspace/cesium-adapter.d.ts +230 -29
  282. package/scene-workspace/cesium-adapter.d.ts.map +1 -1
  283. package/scene-workspace/cesium-adapter.js +741 -56
  284. package/scene-workspace/cesium-mount.d.ts +205 -0
  285. package/scene-workspace/cesium-mount.d.ts.map +1 -0
  286. package/scene-workspace/cesium-mount.js +392 -0
  287. package/scene-workspace/cesium-scene-owner.d.ts +160 -0
  288. package/scene-workspace/cesium-scene-owner.d.ts.map +1 -0
  289. package/scene-workspace/cesium-scene-owner.js +205 -0
  290. package/scene-workspace/cesium-state-sync.d.ts +101 -0
  291. package/scene-workspace/cesium-state-sync.d.ts.map +1 -0
  292. package/scene-workspace/cesium-state-sync.js +432 -0
  293. package/scene-workspace/cesium-time.d.ts +346 -0
  294. package/scene-workspace/cesium-time.d.ts.map +1 -0
  295. package/scene-workspace/cesium-time.js +403 -0
  296. package/scene-workspace/index.d.ts +42 -10
  297. package/scene-workspace/index.d.ts.map +1 -1
  298. package/scene-workspace/index.js +32 -7
  299. package/scene-workspace/maplibre-state-sync.d.ts +112 -0
  300. package/scene-workspace/maplibre-state-sync.d.ts.map +1 -0
  301. package/scene-workspace/maplibre-state-sync.js +400 -0
  302. package/scene-workspace/primitives.d.ts +237 -3
  303. package/scene-workspace/primitives.d.ts.map +1 -1
  304. package/scene-workspace/primitives.js +1087 -17
  305. package/scene-workspace/scene-discovery.d.ts +5 -2
  306. package/scene-workspace/scene-discovery.d.ts.map +1 -1
  307. package/scene-workspace/scene-discovery.js +5 -2
  308. package/scene-workspace/scene-view.d.ts +5 -2
  309. package/scene-workspace/scene-view.d.ts.map +1 -1
  310. package/scene-workspace/scene-view.js +5 -2
  311. package/scene-workspace/source-to-cesium.d.ts +141 -8
  312. package/scene-workspace/source-to-cesium.d.ts.map +1 -1
  313. package/scene-workspace/source-to-cesium.js +497 -72
  314. package/scene-workspace/state-sync-port-kit.d.ts +156 -0
  315. package/scene-workspace/state-sync-port-kit.d.ts.map +1 -0
  316. package/scene-workspace/state-sync-port-kit.js +319 -0
  317. package/scene-workspace/state-sync.d.ts +34 -1
  318. package/scene-workspace/state-sync.d.ts.map +1 -1
  319. package/scene-workspace/state-sync.js +48 -0
  320. package/scene-workspace/types.d.ts +8 -0
  321. package/scene-workspace/types.d.ts.map +1 -1
  322. package/scene-workspace/types.js +2 -0
  323. package/scene-workspace/workspace.d.ts +12 -1
  324. package/scene-workspace/workspace.d.ts.map +1 -1
  325. package/scene-workspace/workspace.js +13 -0
  326. package/studio/index.d.ts +24 -7
  327. package/studio/index.d.ts.map +1 -1
  328. package/studio/index.js +21 -7
  329. package/studio/lifecycle-client.d.ts +164 -0
  330. package/studio/lifecycle-client.d.ts.map +1 -0
  331. package/studio/lifecycle-client.js +299 -0
  332. package/studio/lifecycle-errors.d.ts +91 -0
  333. package/studio/lifecycle-errors.d.ts.map +1 -0
  334. package/studio/lifecycle-errors.js +77 -0
  335. package/studio/lifecycle-types.d.ts +321 -0
  336. package/studio/lifecycle-types.d.ts.map +1 -0
  337. package/studio/lifecycle-types.js +22 -0
  338. package/web-components/controller.d.ts.map +1 -1
  339. package/web-components/csp-styles.d.ts +6 -0
  340. package/web-components/csp-styles.d.ts.map +1 -0
  341. package/web-components/csp-styles.js +53 -0
  342. package/web-components/elements.d.ts +158 -2
  343. package/web-components/elements.d.ts.map +1 -1
  344. package/web-components/elements.js +1302 -215
  345. package/web-components/export-redaction.d.ts +252 -0
  346. package/web-components/export-redaction.d.ts.map +1 -0
  347. package/web-components/export-redaction.js +588 -0
  348. package/web-components/export.d.ts +417 -0
  349. package/web-components/export.d.ts.map +1 -0
  350. package/web-components/export.js +719 -0
  351. package/web-components/feature-editor-model.d.ts +265 -0
  352. package/web-components/feature-editor-model.d.ts.map +1 -0
  353. package/web-components/feature-editor-model.js +438 -0
  354. package/web-components/feature-editor-sketch.d.ts +125 -0
  355. package/web-components/feature-editor-sketch.d.ts.map +1 -0
  356. package/web-components/feature-editor-sketch.js +194 -0
  357. package/web-components/feature-editor-workflow.d.ts +286 -0
  358. package/web-components/feature-editor-workflow.d.ts.map +1 -0
  359. package/web-components/feature-editor-workflow.js +889 -0
  360. package/web-components/feature-editor.d.ts +83 -0
  361. package/web-components/feature-editor.d.ts.map +1 -0
  362. package/web-components/feature-editor.js +829 -0
  363. package/web-components/feature-table-engine.d.ts +465 -0
  364. package/web-components/feature-table-engine.d.ts.map +1 -0
  365. package/web-components/feature-table-engine.js +1360 -0
  366. package/web-components/feature-table-view.d.ts +95 -0
  367. package/web-components/feature-table-view.d.ts.map +1 -0
  368. package/web-components/feature-table-view.js +343 -0
  369. package/web-components/index.d.ts +34 -3
  370. package/web-components/index.d.ts.map +1 -1
  371. package/web-components/index.js +35 -2
  372. package/web-components/maplibre-renderer.d.ts.map +1 -1
  373. package/web-components/maplibre-renderer.js +13 -4
  374. package/web-components/measurement.d.ts +3 -1
  375. package/web-components/measurement.d.ts.map +1 -1
  376. package/web-components/measurement.js +62 -18
  377. package/web-components/time-slider.d.ts +122 -0
  378. package/web-components/time-slider.d.ts.map +1 -0
  379. package/web-components/time-slider.js +740 -0
  380. package/web-components/types.d.ts +310 -0
  381. package/web-components/types.d.ts.map +1 -1
  382. package/webmap/convert-label.js +0 -1
  383. package/webmap/convert-symbol.d.ts.map +1 -1
@@ -7,20 +7,30 @@
7
7
  * when a live {@link CesiumSceneRuntimeTarget} is actually driven.
8
8
  *
9
9
  * As of honua-server#1197 the adapter renders Honua 3D content end-to-end:
10
- * 3D-Tiles tilesets (add/remove/visibility), terrain providers (quantized-mesh
11
- * via `CesiumTerrainProvider`, with vertical exaggeration), and glTF/GLB model
12
- * layers placed with heading/pitch/roll + scale. It also resolves a picked
13
- * 3D-Tiles feature's attributes (batch properties / `EXT_structural_metadata`).
10
+ * 3D-Tiles tilesets (add/remove/visibility, including point clouds with bounded
11
+ * shading), terrain providers (quantized-mesh via `CesiumTerrainProvider`, with
12
+ * vertical exaggeration), imagery providers, and glTF/GLB model layers placed
13
+ * with heading/pitch/roll + scale. It also resolves a picked 3D-Tiles feature's
14
+ * attributes (batch properties / `EXT_structural_metadata`).
15
+ *
16
+ * Every model/imagery binding is validated before the peer loads, so an
17
+ * unrenderable primitive fails closed with a stable diagnostic instead of
18
+ * reaching a Cesium factory or silently rendering nothing.
14
19
  *
15
20
  * The provider wiring is still kept Cesium-mockable: every CesiumJS symbol the
16
21
  * adapter touches is read off the lazily-imported module, so the wiring is
17
22
  * exercised against a `vi.mock("cesium")` stub without a live WebGL `Viewer`.
18
23
  *
19
- * @experimental Part of the experimental `scene-workspace` surface; not yet
20
- * covered by the SDK's semver contract prior to `1.0.0`.
24
+ * @beta Part of the beta `@honua/app-platform/scene-workspace` surface: the
25
+ * adapter's exports are not renamed or removed through 0.1.x, and diagnostic
26
+ * codes and renderer capability flags grow additively. The `cesium` peer floor
27
+ * may rise within 0.1.x, and the server-authored 3D style types track the
28
+ * Honua Server styling contract. See the surface tiers table in
29
+ * `docs/standalone-capability-matrix.md`.
21
30
  * @module
22
31
  */
23
- import { createSceneRuntimeAdapter, diagnoseScenePrimitives, summarizeDiagnosticStatus, } from "./primitives.js";
32
+ import { applyCesiumSceneTime, restoreCesiumClock, } from "./cesium-time.js";
33
+ import { RENDERER_WGS84_SPATIAL_CAPABILITIES, createSceneRuntimeAdapter, diagnoseScenePrimitives, summarizeDiagnosticStatus, } from "./primitives.js";
24
34
  /**
25
35
  * Capabilities advertised by the Cesium adapter. Unlike the MapLibre 2.5D
26
36
  * adapter, Cesium is a true globe renderer: full 3D camera with
@@ -35,15 +45,25 @@ export const CESIUM_SCENE_CAPABILITIES = {
35
45
  protocols: ["quantized-mesh", "terrain-rgb", "raster-dem", "image-service", "custom"],
36
46
  supportsExaggeration: true,
37
47
  },
48
+ imagery: {
49
+ protocols: ["url-template", "wms", "wmts", "single-tile", "arcgis-imagery"],
50
+ },
38
51
  extrusion: true,
39
52
  modelLayer: {
40
- // glTF/glb single models and 3D Tiles tilesets (incl. point clouds) are
41
- // rendered live by this adapter as of #1197. I3S scene layers remain
42
- // declared-capable (Cesium can consume them) but Honua's server does not yet
43
- // emit i3s model-layer primitives, so they fall through to diagnostics only.
53
+ // CesiumJS itself can consume all four formats. `materializedFormats` is the
54
+ // narrower set this adapter actually attaches to a live scene: glTF/GLB
55
+ // single models and 3D-Tiles tilesets (point clouds ride the tileset path).
56
+ // I3S stays declared-capable but unmaterialized, so an i3s primitive fails
57
+ // closed with `scene-primitive-model-format-not-materialized` instead of
58
+ // reporting `supported` and then rendering nothing.
44
59
  formats: ["gltf", "glb", "3d-tiles", "i3s"],
60
+ materializedFormats: ["gltf", "glb", "3d-tiles"],
45
61
  },
46
62
  sceneLayerMetadata: true,
63
+ // The globe is the WGS84 ellipsoid. Web Mercator bindings are accepted
64
+ // because Cesium reprojects them onto that globe itself; nothing else is,
65
+ // because this adapter never reinterprets coordinates or heights.
66
+ spatial: RENDERER_WGS84_SPATIAL_CAPABILITIES,
47
67
  };
48
68
  const DEG2RAD = Math.PI / 180;
49
69
  const RAD2DEG = 180 / Math.PI;
@@ -147,6 +167,16 @@ export function modelLayerToCesiumPlacement(primitive) {
147
167
  scale: resolveCesiumModelScale(primitive.scale),
148
168
  };
149
169
  }
170
+ const ownedCesiumTerrainProviders = new WeakSet();
171
+ function disposeOwnedCesiumTerrainProvider(resource) {
172
+ if (resource && typeof resource === "object" && ownedCesiumTerrainProviders.has(resource)) {
173
+ const destroy = resource.destroy;
174
+ if (typeof destroy === "function") {
175
+ destroy.call(resource);
176
+ ownedCesiumTerrainProviders.delete(resource);
177
+ }
178
+ }
179
+ }
150
180
  /**
151
181
  * Lazily import the CesiumJS runtime. Kept in its own function so the dynamic
152
182
  * `import("cesium")` is the *only* reference to the package in `src/`, which is
@@ -302,23 +332,53 @@ export async function applyTilesetServerStyle(tileset, tilesetUrl, cesium, fetch
302
332
  * (no fetch), so existing callers are unaffected.
303
333
  */
304
334
  export async function addCesium3DTileset(scene, primitive, cesium, options = {}) {
335
+ assertRenderableModelLayer(primitive);
336
+ options.signal?.throwIfAborted();
305
337
  const mod = cesium ?? (await loadCesium());
306
- const tileset = await mod.Cesium3DTileset.fromUrl(primitive.uri);
307
- if (primitive.position) {
308
- tileset.modelMatrix = placementToModelMatrix(mod, modelLayerToCesiumPlacement(primitive));
338
+ const pointCloudShading = normalizePointCloudShading(primitive.pointCloudShading);
339
+ // Attribution reaches every materialized asset, not just imagery (#1049
340
+ // REQ-005): a tileset drawn without its credit is an attribution gap the user
341
+ // cannot see, and Cesium's `CreditDisplay` is the only place it can land.
342
+ const tilesetOptions = {
343
+ ...(pointCloudShading ? { pointCloudShading } : {}),
344
+ ...(primitive.attribution ? { credit: primitive.attribution } : {}),
345
+ };
346
+ const tileset = await (Object.keys(tilesetOptions).length > 0
347
+ ? mod.Cesium3DTileset.fromUrl(primitive.uri, tilesetOptions)
348
+ : mod.Cesium3DTileset.fromUrl(primitive.uri));
349
+ // From here on the tileset is adapter-owned. A placement, style-sidecar, or
350
+ // collection failure must not strand it: nothing else holds a reference, so
351
+ // it would otherwise retain its GPU/worker resources for the process lifetime.
352
+ // An abort settled while the load was in flight lands here too, which is what
353
+ // keeps a cancelled tileset out of the scene entirely rather than attached and
354
+ // then detached.
355
+ try {
356
+ options.signal?.throwIfAborted();
357
+ if (primitive.position) {
358
+ tileset.modelMatrix = placementToModelMatrix(mod, modelLayerToCesiumPlacement(primitive));
359
+ }
360
+ if (options.applyServerStyle !== false) {
361
+ await applyTilesetServerStyle(tileset, primitive.uri, mod, options.fetchImpl ?? fetch.bind(globalThis));
362
+ options.signal?.throwIfAborted();
363
+ }
364
+ scene.primitives.add(tileset);
309
365
  }
310
- if (options.applyServerStyle !== false) {
311
- await applyTilesetServerStyle(tileset, primitive.uri, mod, options.fetchImpl ?? fetch.bind(globalThis));
366
+ catch (error) {
367
+ if (scene.primitives.contains(tileset))
368
+ scene.primitives.remove(tileset);
369
+ destroyOwnedCesiumPrimitive(tileset);
370
+ throw error;
312
371
  }
313
- scene.primitives.add(tileset);
314
- return makeLayerHandle(scene, primitive.id, tileset, "model-layer", primitive.format);
372
+ return makeLayerHandle(scene, primitive.id, tileset, primitive.format);
315
373
  }
316
374
  /**
317
375
  * Materialize a glTF/GLB model from a model-layer primitive, placing it at the
318
376
  * primitive's position/rotation/scale, and add it to the scene. Returns a
319
- * handle for visibility / teardown.
377
+ * handle for visibility / opacity / teardown.
320
378
  */
321
- export async function addCesiumModel(scene, primitive, cesium) {
379
+ export async function addCesiumModel(scene, primitive, cesium, options = {}) {
380
+ assertRenderableModelLayer(primitive);
381
+ options.signal?.throwIfAborted();
322
382
  const mod = cesium ?? (await loadCesium());
323
383
  const placement = modelLayerToCesiumPlacement(primitive);
324
384
  // `placementToModelMatrix` already folds the uniform scale into the model
@@ -328,9 +388,363 @@ export async function addCesiumModel(scene, primitive, cesium) {
328
388
  const model = await mod.Model.fromGltfAsync({
329
389
  url: primitive.uri,
330
390
  modelMatrix: placementToModelMatrix(mod, placement),
391
+ ...(primitive.attribution ? { credit: primitive.attribution } : {}),
331
392
  });
332
- scene.primitives.add(model);
333
- return makeLayerHandle(scene, primitive.id, model, "model-layer", primitive.format);
393
+ try {
394
+ options.signal?.throwIfAborted();
395
+ scene.primitives.add(model);
396
+ }
397
+ catch (error) {
398
+ destroyOwnedCesiumPrimitive(model);
399
+ throw error;
400
+ }
401
+ return makeLayerHandle(scene, primitive.id, model, primitive.format, mod);
402
+ }
403
+ /**
404
+ * Destroy an adapter-owned tileset/model exactly once. Cesium's
405
+ * `PrimitiveCollection.remove` normally destroys what it removes, but a
406
+ * collection configured with `destroyPrimitives = false` — and any failure
407
+ * before the primitive reaches a collection at all — would otherwise leak it.
408
+ */
409
+ function destroyOwnedCesiumPrimitive(primitive) {
410
+ if (typeof primitive.destroy === "function" && primitive.isDestroyed?.() !== true)
411
+ primitive.destroy();
412
+ }
413
+ /**
414
+ * Fail closed on an unrenderable model layer *before* the optional Cesium peer
415
+ * is loaded, mirroring {@link addCesiumImageryLayer}. A missing/invalid/signed
416
+ * asset URI, an impossible placement, or out-of-range point-cloud shading must
417
+ * never reach a Cesium factory: `Cesium3DTileset.fromUrl("")` throws opaquely
418
+ * and `Cartesian3.fromDegrees(NaN, …)` produces a silently broken model matrix.
419
+ */
420
+ function assertRenderableModelLayer(primitive) {
421
+ const validation = diagnoseScenePrimitives([primitive], CESIUM_SCENE_CAPABILITIES).find((diagnostic) => diagnostic.status === "unsupported");
422
+ if (validation)
423
+ throw new Error(`${validation.code}: ${validation.message}`);
424
+ }
425
+ /**
426
+ * Fail closed on an unrenderable elevation source *before* the scene's terrain
427
+ * provider or vertical exaggeration is touched, mirroring
428
+ * {@link assertRenderableModelLayer}.
429
+ *
430
+ * Every terrain protocol reaches Cesium through `CesiumTerrainProvider.fromUrl`,
431
+ * which rejects opaquely on a missing or malformed endpoint — and an elevation
432
+ * source in an unhonorable CRS or vertical datum would place the whole globe's
433
+ * heights wrong while reporting success. Neither may reach a live scene.
434
+ */
435
+ function assertRenderableTerrain(primitive) {
436
+ const validation = diagnoseScenePrimitives([primitive], CESIUM_SCENE_CAPABILITIES).find((diagnostic) => diagnostic.status === "unsupported");
437
+ if (validation)
438
+ throw new Error(`${validation.code}: ${validation.message}`);
439
+ }
440
+ /**
441
+ * Project validated point-cloud shading onto Cesium's `PointCloudShading`
442
+ * option bag, dropping absent fields so Cesium keeps its own defaults instead
443
+ * of receiving explicit `undefined`s. Returns `undefined` when nothing is set.
444
+ */
445
+ function normalizePointCloudShading(shading) {
446
+ if (!shading)
447
+ return undefined;
448
+ const options = {};
449
+ if (shading.attenuation !== undefined)
450
+ options.attenuation = shading.attenuation;
451
+ if (shading.maximumAttenuation !== undefined)
452
+ options.maximumAttenuation = shading.maximumAttenuation;
453
+ if (shading.geometricErrorScale !== undefined)
454
+ options.geometricErrorScale = shading.geometricErrorScale;
455
+ if (shading.eyeDomeLighting !== undefined)
456
+ options.eyeDomeLighting = shading.eyeDomeLighting;
457
+ if (shading.eyeDomeLightingStrength !== undefined) {
458
+ options.eyeDomeLightingStrength = shading.eyeDomeLightingStrength;
459
+ }
460
+ if (shading.eyeDomeLightingRadius !== undefined)
461
+ options.eyeDomeLightingRadius = shading.eyeDomeLightingRadius;
462
+ return Object.keys(options).length > 0 ? options : undefined;
463
+ }
464
+ /**
465
+ * Materialize one renderer-neutral imagery primitive through Cesium's matching
466
+ * provider and add it to the scene's imagery collection.
467
+ *
468
+ * The returned handle owns the provider and layer it creates. Removal is
469
+ * idempotent, releases the layer's GPU resources through Cesium, and calls an
470
+ * optional provider `destroy()` exactly once.
471
+ */
472
+ export async function addCesiumImageryLayer(scene, primitive, cesium, options = {}) {
473
+ const validation = diagnoseScenePrimitives([primitive], CESIUM_SCENE_CAPABILITIES).find((diagnostic) => diagnostic.status === "unsupported");
474
+ if (validation)
475
+ throw new Error(`${validation.code}: ${validation.message}`);
476
+ if (!scene.imageryLayers) {
477
+ throw new Error("scene-primitive-imagery-target-missing: Cesium imageryLayers unavailable.");
478
+ }
479
+ options.signal?.throwIfAborted();
480
+ const mod = cesium ?? (await loadCesium());
481
+ const provider = await createCesiumImageryProvider(primitive, mod);
482
+ let layer;
483
+ try {
484
+ options.signal?.throwIfAborted();
485
+ layer = scene.imageryLayers.addImageryProvider(provider);
486
+ layer.alpha = primitive.opacity ?? 1;
487
+ }
488
+ catch (error) {
489
+ if (layer && scene.imageryLayers.contains(layer))
490
+ scene.imageryLayers.remove(layer, true);
491
+ disposeCesiumImageryProvider(provider);
492
+ throw error;
493
+ }
494
+ let removed = false;
495
+ return {
496
+ id: primitive.id,
497
+ kind: "imagery-layer",
498
+ protocol: primitive.protocol,
499
+ setVisible(visible) {
500
+ if (!removed)
501
+ layer.show = visible;
502
+ },
503
+ setOpacity(opacity) {
504
+ if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
505
+ throw new RangeError("Imagery opacity must be a finite number between 0 and 1.");
506
+ }
507
+ if (!removed)
508
+ layer.alpha = opacity;
509
+ },
510
+ remove() {
511
+ if (removed)
512
+ return;
513
+ removed = true;
514
+ try {
515
+ if (scene.imageryLayers?.contains(layer))
516
+ scene.imageryLayers.remove(layer, true);
517
+ }
518
+ finally {
519
+ disposeCesiumImageryProvider(provider);
520
+ }
521
+ },
522
+ };
523
+ }
524
+ function disposeCesiumImageryProvider(provider) {
525
+ if (typeof provider.destroy === "function" && provider.isDestroyed?.() !== true)
526
+ provider.destroy();
527
+ }
528
+ async function createCesiumImageryProvider(primitive, cesium) {
529
+ const commonOptions = {
530
+ ...(primitive.attribution ? { credit: primitive.attribution } : {}),
531
+ ...(primitive.minimumLevel !== undefined ? { minimumLevel: primitive.minimumLevel } : {}),
532
+ ...(primitive.maximumLevel !== undefined ? { maximumLevel: primitive.maximumLevel } : {}),
533
+ };
534
+ switch (primitive.protocol) {
535
+ case "url-template":
536
+ return new cesium.UrlTemplateImageryProvider({
537
+ url: urlWithServiceParameters(primitive.url, primitive.parameters),
538
+ ...commonOptions,
539
+ ...(primitive.subdomains ? { subdomains: primitive.subdomains } : {}),
540
+ });
541
+ case "wms":
542
+ return new cesium.WebMapServiceImageryProvider({
543
+ url: normalizedWmsUrl(primitive.url, primitive.parameters),
544
+ layers: primitive.layer,
545
+ ...commonOptions,
546
+ ...(primitive.subdomains ? { subdomains: primitive.subdomains } : {}),
547
+ ...(primitive.parameters || primitive.format || primitive.style
548
+ ? {
549
+ parameters: normalizedWmsParameters(primitive.parameters, primitive.format, primitive.style),
550
+ }
551
+ : {}),
552
+ });
553
+ case "wmts":
554
+ return new cesium.WebMapTileServiceImageryProvider({
555
+ url: normalizedWmtsUrl(primitive.url, primitive.parameters),
556
+ layer: primitive.layer,
557
+ style: primitive.style,
558
+ tileMatrixSetID: primitive.tileMatrixSetId,
559
+ format: primitive.format ?? "image/png",
560
+ ...commonOptions,
561
+ ...(primitive.subdomains ? { subdomains: primitive.subdomains } : {}),
562
+ ...(primitive.parameters ? { dimensions: primitive.parameters } : {}),
563
+ });
564
+ case "single-tile":
565
+ return cesium.SingleTileImageryProvider.fromUrl(urlWithServiceParameters(resolveConfiguredSubdomainUrl(primitive), primitive.parameters), {
566
+ ...(primitive.attribution ? { credit: primitive.attribution } : {}),
567
+ });
568
+ case "arcgis-imagery":
569
+ if (isArcGisImageServerUrl(primitive.url)) {
570
+ return new cesium.UrlTemplateImageryProvider({
571
+ url: arcGisImageServerExportUrl(primitive.url, primitive.parameters),
572
+ ...commonOptions,
573
+ ...(primitive.subdomains ? { subdomains: primitive.subdomains } : {}),
574
+ });
575
+ }
576
+ return cesium.ArcGisMapServerImageryProvider.fromUrl(urlWithoutQueryKeys(resolveConfiguredSubdomainUrl(primitive), [...ARCGIS_IMAGE_SERVER_RESERVED_PARAMETERS, "layers", ...Object.keys(primitive.parameters ?? {})], true), {
577
+ ...commonOptions,
578
+ ...arcGisMapServerOptions(primitive.parameters),
579
+ });
580
+ }
581
+ }
582
+ function resolveConfiguredSubdomainUrl(primitive) {
583
+ const subdomain = primitive.subdomains?.[0];
584
+ return subdomain === undefined ? primitive.url : primitive.url.replaceAll("{s}", subdomain);
585
+ }
586
+ function normalizedWmsUrl(url, parameters) {
587
+ return urlWithoutQueryKeys(url, [...WMS_PROVIDER_QUERY_KEYS, ...Object.keys(parameters ?? {})]);
588
+ }
589
+ function normalizedWmtsUrl(url, parameters) {
590
+ return urlWithoutQueryKeys(url, [...WMTS_PROVIDER_QUERY_KEYS, ...Object.keys(parameters ?? {})]);
591
+ }
592
+ function urlWithServiceParameters(url, parameters) {
593
+ const entries = Object.entries(parameters ?? {});
594
+ if (entries.length === 0)
595
+ return url;
596
+ const normalizedUrl = urlWithoutQueryKeys(url, entries.map(([key]) => key));
597
+ const hashIndex = normalizedUrl.indexOf("#");
598
+ const withoutFragment = hashIndex === -1 ? normalizedUrl : normalizedUrl.slice(0, hashIndex);
599
+ const fragment = hashIndex === -1 ? "" : normalizedUrl.slice(hashIndex);
600
+ const encodedParameters = entries
601
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
602
+ .join("&");
603
+ return `${withoutFragment}${withoutFragment.includes("?") ? "&" : "?"}${encodedParameters}${fragment}`;
604
+ }
605
+ function urlWithoutQueryKeys(url, keys, canonical = false) {
606
+ const overriddenKeys = new Set(keys.map((key) => (canonical ? key.toLowerCase().replaceAll(/[^a-z0-9]/g, "") : key.toLowerCase())));
607
+ const hashIndex = url.indexOf("#");
608
+ const withoutFragment = hashIndex === -1 ? url : url.slice(0, hashIndex);
609
+ const fragment = hashIndex === -1 ? "" : url.slice(hashIndex);
610
+ const queryIndex = withoutFragment.indexOf("?");
611
+ if (queryIndex === -1)
612
+ return url;
613
+ const endpoint = withoutFragment.slice(0, queryIndex);
614
+ const query = withoutFragment.slice(queryIndex + 1);
615
+ const preserved = query.split("&").filter((entry) => {
616
+ if (entry === "")
617
+ return false;
618
+ const separatorIndex = entry.indexOf("=");
619
+ const rawKey = separatorIndex === -1 ? entry : entry.slice(0, separatorIndex);
620
+ try {
621
+ const decodedKey = decodeURIComponent(rawKey.replaceAll("+", " ")).toLowerCase();
622
+ return !overriddenKeys.has(canonical ? decodedKey.replaceAll(/[^a-z0-9]/g, "") : decodedKey);
623
+ }
624
+ catch {
625
+ return true;
626
+ }
627
+ });
628
+ return `${endpoint}${preserved.length > 0 ? `?${preserved.join("&")}` : ""}${fragment}`;
629
+ }
630
+ function arcGisMapServerOptions(parameters) {
631
+ const options = {};
632
+ for (const [key, value] of Object.entries(parameters ?? {})) {
633
+ switch (key.toLowerCase().replaceAll(/[^a-z0-9]/g, "")) {
634
+ case "layers":
635
+ options.layers = normalizeArcGisMapServerLayers(value);
636
+ break;
637
+ case "enablepickfeatures":
638
+ options.enablePickFeatures = value;
639
+ break;
640
+ case "useprecachedtilesifavailable":
641
+ options.usePreCachedTilesIfAvailable = value;
642
+ break;
643
+ case "tilewidth":
644
+ options.tileWidth = value;
645
+ break;
646
+ case "tileheight":
647
+ options.tileHeight = value;
648
+ break;
649
+ }
650
+ }
651
+ return options;
652
+ }
653
+ function normalizeArcGisMapServerLayers(value) {
654
+ const trimmed = String(value).trim();
655
+ const layerList = trimmed.toLowerCase().startsWith("show:") ? trimmed.slice(5) : trimmed;
656
+ return layerList
657
+ .split(",")
658
+ .map((layerId) => layerId.trim())
659
+ .join(",");
660
+ }
661
+ function normalizedWmsParameters(parameters, format, style) {
662
+ const entries = Object.entries(parameters ?? {}).filter(([key]) => key.toLowerCase() !== "layers" &&
663
+ (format === undefined || key.toLowerCase() !== "format") &&
664
+ (style === undefined || key.toLowerCase() !== "styles"));
665
+ return {
666
+ ...Object.fromEntries(entries),
667
+ ...(format !== undefined ? { format } : {}),
668
+ ...(style !== undefined ? { styles: style } : {}),
669
+ };
670
+ }
671
+ function isArcGisImageServerUrl(url) {
672
+ const endpoint = url.split(/[?#]/, 1)[0] ?? "";
673
+ return trimTrailingSlashes(endpoint).toLowerCase().endsWith("/imageserver");
674
+ }
675
+ function arcGisImageServerExportUrl(url, parameters) {
676
+ const [withoutFragment] = url.split("#", 1);
677
+ const queryIndex = withoutFragment.indexOf("?");
678
+ const endpoint = trimTrailingSlashes(queryIndex === -1 ? withoutFragment : withoutFragment.slice(0, queryIndex));
679
+ const existingQuery = queryIndex === -1 ? "" : withoutFragment.slice(queryIndex + 1);
680
+ const primitiveParameters = new Map();
681
+ for (const [key, value] of Object.entries(parameters ?? {})) {
682
+ if (!ARCGIS_IMAGE_SERVER_RESERVED_PARAMETERS.has(key.toLowerCase())) {
683
+ primitiveParameters.set(key.toLowerCase(), [key, String(value)]);
684
+ }
685
+ }
686
+ const preservedParameters = new URLSearchParams();
687
+ for (const [key, value] of new URLSearchParams(existingQuery)) {
688
+ const normalizedKey = key.toLowerCase();
689
+ if (!ARCGIS_IMAGE_SERVER_RESERVED_PARAMETERS.has(normalizedKey) && !primitiveParameters.has(normalizedKey)) {
690
+ preservedParameters.append(key, value);
691
+ }
692
+ }
693
+ for (const [key, value] of primitiveParameters.values())
694
+ preservedParameters.set(key, value);
695
+ const requestParameters = [
696
+ "f=image",
697
+ "bbox={westProjected}%2C{southProjected}%2C{eastProjected}%2C{northProjected}",
698
+ "bboxSR=3857",
699
+ "imageSR=3857",
700
+ "size={width}%2C{height}",
701
+ "format=png32",
702
+ "transparent=true",
703
+ ];
704
+ const preservedQuery = preservedParameters.toString();
705
+ return `${endpoint}/exportImage?${preservedQuery ? `${preservedQuery}&` : ""}${requestParameters.join("&")}`;
706
+ }
707
+ const ARCGIS_IMAGE_SERVER_RESERVED_PARAMETERS = new Set([
708
+ "f",
709
+ "bbox",
710
+ "bboxsr",
711
+ "imagesr",
712
+ "size",
713
+ "format",
714
+ "transparent",
715
+ ]);
716
+ const WMS_PROVIDER_QUERY_KEYS = [
717
+ "bbox",
718
+ "crs",
719
+ "format",
720
+ "height",
721
+ "layers",
722
+ "request",
723
+ "service",
724
+ "srs",
725
+ "styles",
726
+ "transparent",
727
+ "version",
728
+ "width",
729
+ ];
730
+ const WMTS_PROVIDER_QUERY_KEYS = [
731
+ "format",
732
+ "layer",
733
+ "request",
734
+ "service",
735
+ "style",
736
+ "tilecol",
737
+ "tilematrix",
738
+ "tilematrixset",
739
+ "tilematrixsetid",
740
+ "tilerow",
741
+ "version",
742
+ ];
743
+ function trimTrailingSlashes(value) {
744
+ let end = value.length;
745
+ while (end > 0 && value.charCodeAt(end - 1) === 47)
746
+ end -= 1;
747
+ return value.slice(0, end);
334
748
  }
335
749
  /**
336
750
  * Wire a terrain provider from an elevation-source primitive onto the scene.
@@ -343,6 +757,11 @@ export async function addCesiumModel(scene, primitive, cesium) {
343
757
  * is applied via the scene's `verticalExaggeration`.
344
758
  */
345
759
  export async function applyCesiumTerrain(scene, primitive, cesium) {
760
+ return applyCesiumTerrainInternal(scene, primitive, cesium, true);
761
+ }
762
+ async function applyCesiumTerrainInternal(scene, primitive, cesium, disposePreviousProvider, signal) {
763
+ signal?.throwIfAborted();
764
+ assertRenderableTerrain(primitive);
346
765
  const url = typeof primitive.url === "string" && primitive.url.trim() !== "" ? primitive.url : undefined;
347
766
  if (primitive.exaggeration !== undefined && Number.isFinite(primitive.exaggeration)) {
348
767
  scene.verticalExaggeration = primitive.exaggeration;
@@ -350,8 +769,21 @@ export async function applyCesiumTerrain(scene, primitive, cesium) {
350
769
  if (!url)
351
770
  return undefined;
352
771
  const mod = cesium ?? (await loadCesium());
353
- const provider = await mod.CesiumTerrainProvider.fromUrl(url);
772
+ const provider = await mod.CesiumTerrainProvider.fromUrl(url, primitive.attribution ? { credit: primitive.attribution } : {});
773
+ if (provider && typeof provider === "object")
774
+ ownedCesiumTerrainProviders.add(provider);
775
+ if (signal?.aborted) {
776
+ // The provider resolved into a scene the host has already abandoned. Release
777
+ // it here rather than installing it and relying on a rollback to notice.
778
+ disposeOwnedCesiumTerrainProvider(provider);
779
+ signal.throwIfAborted();
780
+ }
781
+ const previousProvider = scene.terrainProvider;
354
782
  scene.terrainProvider = provider;
783
+ if (disposePreviousProvider && previousProvider && previousProvider !== provider) {
784
+ disposeOwnedCesiumTerrainProvider(previousProvider);
785
+ }
786
+ let removed = false;
355
787
  return {
356
788
  id: primitive.id,
357
789
  kind: "elevation-source",
@@ -359,28 +791,69 @@ export async function applyCesiumTerrain(scene, primitive, cesium) {
359
791
  /* terrain is always on once set; visibility is a no-op for the provider. */
360
792
  },
361
793
  remove() {
794
+ if (removed)
795
+ return;
362
796
  // Only tear down if *this* handle's provider is still the active one. If a
363
797
  // newer elevation source has since replaced `scene.terrainProvider`,
364
798
  // removing this (now-stale) handle must be a no-op so we don't clobber the
365
799
  // active terrain/exaggeration.
366
- if (scene.terrainProvider !== provider)
800
+ if (scene.terrainProvider !== provider) {
801
+ disposeOwnedCesiumTerrainProvider(provider);
802
+ removed = true;
367
803
  return;
804
+ }
368
805
  scene.terrainProvider = undefined;
369
806
  scene.verticalExaggeration = 1;
807
+ disposeOwnedCesiumTerrainProvider(provider);
808
+ removed = true;
370
809
  },
371
810
  };
372
811
  }
373
- function makeLayerHandle(scene, id, primitive, kind, format) {
812
+ /**
813
+ * Wrap an adapter-owned tileset/model in a bounded handle.
814
+ *
815
+ * Ownership rules mirror {@link addCesiumImageryLayer}: `remove()` is idempotent
816
+ * and destroys the object exactly once (see
817
+ * {@link destroyOwnedCesiumPrimitive}), and post-removal control calls are
818
+ * no-ops rather than mutations of a destroyed object.
819
+ *
820
+ * `setOpacity` is only wired for glTF/GLB models, where `Model.color`'s alpha is
821
+ * the adapter-owned opacity channel. Tilesets omit it — see
822
+ * {@link CesiumLayerHandle}.
823
+ */
824
+ function makeLayerHandle(scene, id, primitive, format, cesium) {
825
+ let removed = false;
826
+ const model = cesium && (format === "gltf" || format === "glb") ? primitive : undefined;
374
827
  return {
375
828
  id,
376
- kind,
829
+ kind: "model-layer",
377
830
  format,
378
831
  setVisible(visible) {
379
- primitive.show = visible;
832
+ if (!removed)
833
+ primitive.show = visible;
380
834
  },
835
+ ...(model && cesium
836
+ ? {
837
+ setOpacity(opacity) {
838
+ if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
839
+ throw new RangeError("Model opacity must be a finite number between 0 and 1.");
840
+ }
841
+ if (!removed)
842
+ model.color = cesium.Color.WHITE.withAlpha(opacity);
843
+ },
844
+ }
845
+ : {}),
381
846
  remove() {
382
- if (scene.primitives.contains(primitive))
383
- scene.primitives.remove(primitive);
847
+ if (removed)
848
+ return;
849
+ removed = true;
850
+ try {
851
+ if (scene.primitives.contains(primitive))
852
+ scene.primitives.remove(primitive);
853
+ }
854
+ finally {
855
+ destroyOwnedCesiumPrimitive(primitive);
856
+ }
384
857
  },
385
858
  };
386
859
  }
@@ -421,60 +894,272 @@ export function pickCesiumFeatureAttributes(scene, windowPosition) {
421
894
  * As of #1197 this lands the full 3D stack against `target.scene`:
422
895
  * - `camera` → `Camera.setView` (via the lazily-loaded `Cartesian3`).
423
896
  * - `elevation-source` → `scene.terrainProvider` + `verticalExaggeration`.
897
+ * - `imagery-layer` → a protocol-specific provider added to `scene.imageryLayers`.
424
898
  * - `model-layer` (`3d-tiles`) → `Cesium3DTileset.fromUrl` added to
425
899
  * `scene.primitives`; (`gltf`/`glb`) → `Model.fromGltfAsync` placed by
426
900
  * position/rotation/scale.
427
901
  *
428
902
  * Returns the diagnostics plus the live layer handles keyed by primitive id so
429
903
  * callers can toggle visibility / tear layers down. Layer rendering is skipped
430
- * (gracefully) when no `scene` is attached or when a primitive diagnoses as
431
- * unsupported. I3S model layers and extrusions are declared-capable but not yet
432
- * materialized here (no Honua server primitives emit them); point clouds ride
433
- * the 3D-Tiles path and are gated on server ingest (honua-server#1201).
904
+ * when no `scene` is attached or when a primitive diagnoses as unsupported —
905
+ * and every skip is accounted for by a diagnostic, so nothing is silently
906
+ * dropped. Point clouds ride the 3D-Tiles path with optional
907
+ * `pointCloudShading`. I3S is declared-capable but unmaterialized, so it
908
+ * diagnoses `scene-primitive-model-format-not-materialized`; extrusions remain
909
+ * a MapLibre-path concern.
910
+ *
911
+ * As of #1048 `state` is load-bearing: its `timeline` slice is bound to the
912
+ * target's clock when the host opted into one (see
913
+ * {@link CesiumSceneRuntimeTarget}), and every arm — applied, host-owned,
914
+ * unbound, uninterpretable — reports itself as a `scene-time-*` diagnostic.
915
+ * Binding time constructs no renderer resource, so it never displaces a
916
+ * primitive: the mount's reuse diff is untouched by a moving clock.
434
917
  */
435
- export async function applyCesiumScenePrimitives(target, primitives, _state) {
436
- const diagnostics = diagnoseScenePrimitives(primitives, CESIUM_SCENE_CAPABILITIES);
918
+ export async function applyCesiumScenePrimitives(target, primitives, state) {
919
+ const application = await applyCesiumScenePrimitivesInternal(target, primitives, {
920
+ ...(state ? { state } : {}),
921
+ });
922
+ return {
923
+ status: application.status,
924
+ diagnostics: application.diagnostics,
925
+ layers: application.layers,
926
+ };
927
+ }
928
+ /**
929
+ * Stable identity of a scene primitive within one mount: its kind and id.
930
+ *
931
+ * Identity is deliberately kind-qualified. Two primitives of different kinds may
932
+ * legitimately share an id (a terrain source and the imagery draped over it, for
933
+ * instance), and they own different renderer resources, so they must never be
934
+ * treated as revisions of one another.
935
+ *
936
+ * @internal
937
+ */
938
+ export function scenePrimitiveMountKey(primitive) {
939
+ return `${primitive.kind}${primitive.id}`;
940
+ }
941
+ /**
942
+ * A stable, order-independent fingerprint of a primitive's configuration, used
943
+ * to decide whether a revised plan's primitive is the *same* binding as the one
944
+ * already mounted.
945
+ *
946
+ * Returns `undefined` when the primitive cannot be fingerprinted deterministically
947
+ * (a cycle, or a non-serializable value smuggled through `metadata`). An absent
948
+ * fingerprint means "assume changed": the binding is rebuilt rather than reused,
949
+ * which costs a rebuild but can never leave a stale renderer resource claiming to
950
+ * satisfy a plan it no longer matches.
951
+ *
952
+ * @internal
953
+ */
954
+ export function scenePrimitiveFingerprint(primitive) {
955
+ try {
956
+ return stableStringify(primitive, new Set());
957
+ }
958
+ catch {
959
+ return undefined;
960
+ }
961
+ }
962
+ function stableStringify(value, seen) {
963
+ if (value === undefined)
964
+ return "undefined";
965
+ if (value === null)
966
+ return "null";
967
+ if (typeof value === "function" || typeof value === "symbol") {
968
+ throw new TypeError("Scene primitives must be serializable to be fingerprinted.");
969
+ }
970
+ if (typeof value !== "object")
971
+ return JSON.stringify(value) ?? "undefined";
972
+ const container = value;
973
+ if (seen.has(container))
974
+ throw new TypeError("Scene primitives must be acyclic to be fingerprinted.");
975
+ seen.add(container);
976
+ try {
977
+ if (Array.isArray(value))
978
+ return `[${value.map((entry) => stableStringify(entry, seen)).join(",")}]`;
979
+ const entries = Object.entries(value)
980
+ .filter(([, entryValue]) => entryValue !== undefined)
981
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
982
+ return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue, seen)}`).join(",")}}`;
983
+ }
984
+ finally {
985
+ seen.delete(container);
986
+ }
987
+ }
988
+ /**
989
+ * The application engine behind both the one-shot
990
+ * {@link applyCesiumScenePrimitives} and the {@link CesiumLayerHandle}-owning
991
+ * mount lifecycle.
992
+ *
993
+ * Two properties hold regardless of how it is driven:
994
+ *
995
+ * - **Transactional.** Any failure — including an abort — removes every handle
996
+ * this application created, in reverse construction order, and restores the
997
+ * terrain provider, vertical exaggeration, and clock the scene had on entry.
998
+ * Handles carried in through `reuse` are never touched, so a failed revision
999
+ * leaves the previously applied plan intact.
1000
+ * - **Ownership-complete.** Every renderer resource it constructs is either
1001
+ * returned in `mounted` or destroyed before the call returns. Nothing is
1002
+ * reachable only from a discarded local.
1003
+ *
1004
+ * @internal
1005
+ */
1006
+ export async function applyCesiumScenePrimitivesInternal(target, primitives, options = {}) {
1007
+ const { signal, reuse, state } = options;
1008
+ signal?.throwIfAborted();
1009
+ const diagnostics = [...diagnoseScenePrimitives(primitives, CESIUM_SCENE_CAPABILITIES)];
437
1010
  const cesium = await loadCesium();
1011
+ signal?.throwIfAborted();
1012
+ // Time first: entity availability, time-dynamic tileset content, and clock-
1013
+ // driven properties are all evaluated against the clock, so a resource that
1014
+ // attaches below should attach under the application's own time rather than
1015
+ // under whatever instant the viewer happened to be showing.
1016
+ const time = applyCesiumSceneTime(target, state?.timeline, cesium);
1017
+ diagnostics.push(...time.diagnostics);
438
1018
  const toCartesian = (longitude, latitude, height) => cesium.Cartesian3.fromDegrees(longitude, latitude, height);
439
- const unsupportedIds = new Set(diagnostics.filter((diagnostic) => diagnostic.status === "unsupported").map((diagnostic) => diagnostic.primitiveId));
1019
+ const unsupportedPrimitives = new Set(primitives.filter((primitive) => diagnoseScenePrimitives([primitive], CESIUM_SCENE_CAPABILITIES).some((diagnostic) => diagnostic.status === "unsupported")));
440
1020
  const layers = new Map();
1021
+ const mounted = new Map();
1022
+ const created = [];
1023
+ const reused = [];
1024
+ const appliedHandles = [];
1025
+ const deferredTerrainProviders = [];
441
1026
  const scene = target.scene;
442
- for (const primitive of primitives) {
443
- if (primitive.kind === "camera") {
444
- applyCameraStateToCesiumCamera(target.camera, primitive.camera, toCartesian);
445
- continue;
1027
+ const originalTerrainProvider = scene?.terrainProvider;
1028
+ const originalVerticalExaggeration = scene?.verticalExaggeration;
1029
+ let terrainWasApplied = false;
1030
+ const registerHandle = (key, handle) => {
1031
+ appliedHandles.push(handle);
1032
+ layers.get(handle.id)?.remove();
1033
+ layers.set(handle.id, handle);
1034
+ mounted.set(key, handle);
1035
+ created.push(handle.id);
1036
+ };
1037
+ try {
1038
+ for (const primitive of primitives) {
1039
+ signal?.throwIfAborted();
1040
+ const key = scenePrimitiveMountKey(primitive);
1041
+ if (reuse?.has(key)) {
1042
+ // Unchanged since the last application: skip materialization entirely so
1043
+ // the live resource — and any visibility/opacity the host set on it —
1044
+ // survives the revision.
1045
+ const carried = reuse.get(key);
1046
+ mounted.set(key, carried);
1047
+ if (carried) {
1048
+ layers.set(carried.id, carried);
1049
+ reused.push(carried.id);
1050
+ }
1051
+ continue;
1052
+ }
1053
+ if (primitive.kind === "camera") {
1054
+ applyCameraStateToCesiumCamera(target.camera, primitive.camera, toCartesian);
1055
+ mounted.set(key, undefined);
1056
+ continue;
1057
+ }
1058
+ if (!scene || unsupportedPrimitives.has(primitive)) {
1059
+ mounted.set(key, undefined);
1060
+ continue;
1061
+ }
1062
+ if (primitive.kind === "elevation-source") {
1063
+ const previousProvider = scene.terrainProvider;
1064
+ terrainWasApplied = true;
1065
+ const handle = await applyCesiumTerrainInternal(scene, primitive, cesium, false, signal);
1066
+ if (handle) {
1067
+ if (previousProvider && previousProvider !== scene.terrainProvider) {
1068
+ deferredTerrainProviders.push(previousProvider);
1069
+ }
1070
+ registerHandle(key, handle);
1071
+ }
1072
+ else {
1073
+ mounted.set(key, undefined);
1074
+ }
1075
+ continue;
1076
+ }
1077
+ if (primitive.kind === "imagery-layer") {
1078
+ if (!scene.imageryLayers) {
1079
+ diagnostics.push({
1080
+ code: "scene-primitive-imagery-target-missing",
1081
+ severity: "error",
1082
+ status: "unsupported",
1083
+ primitiveId: primitive.id,
1084
+ primitiveKind: primitive.kind,
1085
+ renderer: "cesium",
1086
+ message: "Cesium imageryLayers unavailable.",
1087
+ fallback: "Attach a complete Cesium scene target before applying imagery.",
1088
+ });
1089
+ mounted.set(key, undefined);
1090
+ continue;
1091
+ }
1092
+ registerHandle(key, await addCesiumImageryLayer(scene, primitive, cesium, { ...(signal ? { signal } : {}) }));
1093
+ continue;
1094
+ }
1095
+ if (primitive.kind === "model-layer") {
1096
+ // Only materialized formats reach here: anything else already diagnosed
1097
+ // as unsupported above and was filtered into `unsupportedPrimitives`.
1098
+ const materializationOptions = { ...(signal ? { signal } : {}) };
1099
+ if (primitive.format === "3d-tiles") {
1100
+ registerHandle(key, await addCesium3DTileset(scene, primitive, cesium, materializationOptions));
1101
+ }
1102
+ else {
1103
+ registerHandle(key, await addCesiumModel(scene, primitive, cesium, materializationOptions));
1104
+ }
1105
+ continue;
1106
+ }
1107
+ mounted.set(key, undefined);
1108
+ }
1109
+ signal?.throwIfAborted();
1110
+ for (const provider of [...deferredTerrainProviders].reverse())
1111
+ disposeOwnedCesiumTerrainProvider(provider);
1112
+ }
1113
+ catch (cause) {
1114
+ const rollbackErrors = [];
1115
+ for (const handle of appliedHandles.reverse()) {
1116
+ try {
1117
+ handle.remove();
1118
+ }
1119
+ catch (error) {
1120
+ rollbackErrors.push(error);
1121
+ }
446
1122
  }
447
- if (!scene || unsupportedIds.has(primitive.id))
448
- continue;
449
- if (primitive.kind === "elevation-source") {
450
- const handle = await applyCesiumTerrain(scene, primitive, cesium);
451
- if (handle)
452
- layers.set(handle.id, handle);
453
- continue;
1123
+ if (scene && terrainWasApplied) {
1124
+ try {
1125
+ scene.terrainProvider = originalTerrainProvider;
1126
+ scene.verticalExaggeration = originalVerticalExaggeration;
1127
+ }
1128
+ catch (error) {
1129
+ rollbackErrors.push(error);
1130
+ }
454
1131
  }
455
- if (primitive.kind === "model-layer") {
456
- if (primitive.format === "3d-tiles") {
457
- layers.set(primitive.id, await addCesium3DTileset(scene, primitive, cesium));
1132
+ if (target.clock && time.previous) {
1133
+ try {
1134
+ restoreCesiumClock(target.clock, time.previous);
458
1135
  }
459
- else if (primitive.format === "gltf" || primitive.format === "glb") {
460
- layers.set(primitive.id, await addCesiumModel(scene, primitive, cesium));
1136
+ catch (error) {
1137
+ rollbackErrors.push(error);
461
1138
  }
462
- // i3s / obj / custom: declared-capable but not materialized here.
463
1139
  }
1140
+ if (rollbackErrors.length > 0) {
1141
+ throw new AggregateError([cause, ...rollbackErrors], "Cesium scene primitive application failed and layer rollback was incomplete.", { cause });
1142
+ }
1143
+ throw cause;
464
1144
  }
465
1145
  return {
466
1146
  status: summarizeDiagnosticStatus(diagnostics),
467
1147
  diagnostics,
468
1148
  layers,
1149
+ mounted,
1150
+ created,
1151
+ reused,
1152
+ time,
469
1153
  };
470
1154
  }
471
1155
  /**
472
1156
  * Create a {@link SceneRuntimeAdapter} backed by CesiumJS.
473
1157
  *
474
1158
  * When `options.target` (a live Cesium `Viewer`/`Scene` surface — a `camera`
475
- * plus an optional `scene`) is supplied, `apply` drives the live globe: camera
476
- * sync plus, when a `scene` is attached, terrain providers, 3D-Tiles tilesets,
477
- * and glTF/GLB model layers (#1197). Without a target the adapter still
1159
+ * plus an optional `scene` and an optional `clock`) is supplied, `apply` drives
1160
+ * the live globe: camera sync plus, when a `scene` is attached, terrain and
1161
+ * imagery providers, 3D-Tiles tilesets, and glTF/GLB model layers, and, when a
1162
+ * `clock` is attached, the workspace `timeline` slice. Without a target the adapter still
478
1163
  * diagnoses primitives against Cesium's true-3D capabilities — useful for
479
1164
  * migration analysis before a viewer exists. CesiumJS itself is only imported
480
1165
  * lazily when `apply` runs.