@honua/app-platform 0.0.20-alpha.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 (840) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/app/index.d.ts +164 -0
  4. package/app/index.d.ts.map +1 -0
  5. package/app/index.js +424 -0
  6. package/app-controller/controller.d.ts +425 -0
  7. package/app-controller/controller.d.ts.map +1 -0
  8. package/app-controller/controller.js +1173 -0
  9. package/app-controller/index.d.ts +14 -0
  10. package/app-controller/index.d.ts.map +1 -0
  11. package/app-controller/index.js +12 -0
  12. package/app-workspace/index.d.ts +421 -0
  13. package/app-workspace/index.d.ts.map +1 -0
  14. package/app-workspace/index.js +722 -0
  15. package/collaboration/client.d.ts +40 -0
  16. package/collaboration/client.d.ts.map +1 -0
  17. package/collaboration/client.js +299 -0
  18. package/collaboration/errors.d.ts +15 -0
  19. package/collaboration/errors.d.ts.map +1 -0
  20. package/collaboration/errors.js +17 -0
  21. package/collaboration/fixture.d.ts +41 -0
  22. package/collaboration/fixture.d.ts.map +1 -0
  23. package/collaboration/fixture.js +362 -0
  24. package/collaboration/index.d.ts +18 -0
  25. package/collaboration/index.d.ts.map +1 -0
  26. package/collaboration/index.js +14 -0
  27. package/collaboration/types.d.ts +240 -0
  28. package/collaboration/types.d.ts.map +1 -0
  29. package/collaboration/types.js +1 -0
  30. package/contract/analytics-sources.d.ts +149 -0
  31. package/contract/analytics-sources.d.ts.map +1 -0
  32. package/contract/analytics-sources.js +235 -0
  33. package/contract/edit-session.d.ts +203 -0
  34. package/contract/edit-session.d.ts.map +1 -0
  35. package/contract/edit-session.js +760 -0
  36. package/contract/edit-sketch.d.ts +91 -0
  37. package/contract/edit-sketch.d.ts.map +1 -0
  38. package/contract/edit-sketch.js +281 -0
  39. package/contract/index.d.ts +63 -0
  40. package/contract/index.d.ts.map +1 -0
  41. package/contract/index.js +52 -0
  42. package/contract/jobs.d.ts +149 -0
  43. package/contract/jobs.d.ts.map +1 -0
  44. package/contract/jobs.js +40 -0
  45. package/contract/pmtiles.d.ts +162 -0
  46. package/contract/pmtiles.d.ts.map +1 -0
  47. package/contract/pmtiles.js +155 -0
  48. package/contract/source.d.ts +447 -0
  49. package/contract/source.d.ts.map +1 -0
  50. package/contract/source.js +3909 -0
  51. package/contract/spatial-aggregation.d.ts +336 -0
  52. package/contract/spatial-aggregation.d.ts.map +1 -0
  53. package/contract/spatial-aggregation.js +298 -0
  54. package/contract/temporal.d.ts +455 -0
  55. package/contract/temporal.d.ts.map +1 -0
  56. package/contract/temporal.js +146 -0
  57. package/contract/tiles.d.ts +370 -0
  58. package/contract/tiles.d.ts.map +1 -0
  59. package/contract/tiles.js +519 -0
  60. package/contract/types.d.ts +694 -0
  61. package/contract/types.d.ts.map +1 -0
  62. package/contract/types.js +183 -0
  63. package/contract/widget-source.d.ts +205 -0
  64. package/contract/widget-source.d.ts.map +1 -0
  65. package/contract/widget-source.js +1473 -0
  66. package/control-plane/client.d.ts +95 -0
  67. package/control-plane/client.d.ts.map +1 -0
  68. package/control-plane/client.js +374 -0
  69. package/control-plane/index.d.ts +18 -0
  70. package/control-plane/index.d.ts.map +1 -0
  71. package/control-plane/index.js +14 -0
  72. package/control-plane/types.d.ts +233 -0
  73. package/control-plane/types.d.ts.map +1 -0
  74. package/control-plane/types.js +7 -0
  75. package/controls/basemap-style-binding.d.ts +54 -0
  76. package/controls/basemap-style-binding.d.ts.map +1 -0
  77. package/controls/basemap-style-binding.js +199 -0
  78. package/controls/basemap-switcher.d.ts +95 -0
  79. package/controls/basemap-switcher.d.ts.map +1 -0
  80. package/controls/basemap-switcher.js +379 -0
  81. package/controls/element-utils.d.ts +48 -0
  82. package/controls/element-utils.d.ts.map +1 -0
  83. package/controls/element-utils.js +78 -0
  84. package/controls/index.d.ts +33 -0
  85. package/controls/index.d.ts.map +1 -0
  86. package/controls/index.js +30 -0
  87. package/controls/layer-list.d.ts +98 -0
  88. package/controls/layer-list.d.ts.map +1 -0
  89. package/controls/layer-list.js +375 -0
  90. package/controls/legend-derive.d.ts +53 -0
  91. package/controls/legend-derive.d.ts.map +1 -0
  92. package/controls/legend-derive.js +196 -0
  93. package/controls/legend.d.ts +104 -0
  94. package/controls/legend.d.ts.map +1 -0
  95. package/controls/legend.js +381 -0
  96. package/controls/swipe-control.d.ts +80 -0
  97. package/controls/swipe-control.d.ts.map +1 -0
  98. package/controls/swipe-control.js +368 -0
  99. package/controls/types.d.ts +227 -0
  100. package/controls/types.d.ts.map +1 -0
  101. package/controls/types.js +11 -0
  102. package/core/auth/client-credentials-provider.d.ts +51 -0
  103. package/core/auth/client-credentials-provider.d.ts.map +1 -0
  104. package/core/auth/client-credentials-provider.js +115 -0
  105. package/core/auth/credential-store.d.ts +52 -0
  106. package/core/auth/credential-store.d.ts.map +1 -0
  107. package/core/auth/credential-store.js +82 -0
  108. package/core/auth/index.d.ts +23 -0
  109. package/core/auth/index.d.ts.map +1 -0
  110. package/core/auth/index.js +20 -0
  111. package/core/auth/oauth2-provider.d.ts +114 -0
  112. package/core/auth/oauth2-provider.d.ts.map +1 -0
  113. package/core/auth/oauth2-provider.js +408 -0
  114. package/core/auth/pkce.d.ts +25 -0
  115. package/core/auth/pkce.d.ts.map +1 -0
  116. package/core/auth/pkce.js +46 -0
  117. package/core/auth/static-providers.d.ts +20 -0
  118. package/core/auth/static-providers.d.ts.map +1 -0
  119. package/core/auth/static-providers.js +26 -0
  120. package/core/auth/token-endpoint.d.ts +40 -0
  121. package/core/auth/token-endpoint.d.ts.map +1 -0
  122. package/core/auth/token-endpoint.js +133 -0
  123. package/core/auth/types.d.ts +144 -0
  124. package/core/auth/types.d.ts.map +1 -0
  125. package/core/auth/types.js +8 -0
  126. package/core/batch.d.ts +37 -0
  127. package/core/batch.d.ts.map +1 -0
  128. package/core/batch.js +79 -0
  129. package/core/cache-state.d.ts +79 -0
  130. package/core/cache-state.d.ts.map +1 -0
  131. package/core/cache-state.js +87 -0
  132. package/core/client.d.ts +607 -0
  133. package/core/client.d.ts.map +1 -0
  134. package/core/client.js +1716 -0
  135. package/core/compatibility.d.ts +18 -0
  136. package/core/compatibility.d.ts.map +1 -0
  137. package/core/compatibility.js +245 -0
  138. package/core/errors.d.ts +139 -0
  139. package/core/errors.d.ts.map +1 -0
  140. package/core/errors.js +176 -0
  141. package/core/esri-geojson.d.ts +66 -0
  142. package/core/esri-geojson.d.ts.map +1 -0
  143. package/core/esri-geojson.js +363 -0
  144. package/core/geoparquet-sql.d.ts +162 -0
  145. package/core/geoparquet-sql.d.ts.map +1 -0
  146. package/core/geoparquet-sql.js +333 -0
  147. package/core/geoservices.d.ts +34 -0
  148. package/core/geoservices.d.ts.map +1 -0
  149. package/core/geoservices.js +434 -0
  150. package/core/grpc-adapter.d.ts +23 -0
  151. package/core/grpc-adapter.d.ts.map +1 -0
  152. package/core/grpc-adapter.js +688 -0
  153. package/core/odata.d.ts +347 -0
  154. package/core/odata.d.ts.map +1 -0
  155. package/core/odata.js +913 -0
  156. package/core/ogc-conformance.d.ts +35 -0
  157. package/core/ogc-conformance.d.ts.map +1 -0
  158. package/core/ogc-conformance.js +134 -0
  159. package/core/ogc-endpoint-layout.d.ts +70 -0
  160. package/core/ogc-endpoint-layout.d.ts.map +1 -0
  161. package/core/ogc-endpoint-layout.js +173 -0
  162. package/core/ogc-features.d.ts +23 -0
  163. package/core/ogc-features.d.ts.map +1 -0
  164. package/core/ogc-features.js +129 -0
  165. package/core/ogc-maps.d.ts +49 -0
  166. package/core/ogc-maps.d.ts.map +1 -0
  167. package/core/ogc-maps.js +126 -0
  168. package/core/ogc-processes.d.ts +118 -0
  169. package/core/ogc-processes.d.ts.map +1 -0
  170. package/core/ogc-processes.js +423 -0
  171. package/core/ogc-records.d.ts +75 -0
  172. package/core/ogc-records.d.ts.map +1 -0
  173. package/core/ogc-records.js +251 -0
  174. package/core/ogc-tiles.d.ts +125 -0
  175. package/core/ogc-tiles.d.ts.map +1 -0
  176. package/core/ogc-tiles.js +183 -0
  177. package/core/path-utils.d.ts +69 -0
  178. package/core/path-utils.d.ts.map +1 -0
  179. package/core/path-utils.js +123 -0
  180. package/core/pbf-decoder.d.ts +39 -0
  181. package/core/pbf-decoder.d.ts.map +1 -0
  182. package/core/pbf-decoder.js +702 -0
  183. package/core/process-runner.d.ts +104 -0
  184. package/core/process-runner.d.ts.map +1 -0
  185. package/core/process-runner.js +315 -0
  186. package/core/protocol-transport.d.ts +76 -0
  187. package/core/protocol-transport.d.ts.map +1 -0
  188. package/core/protocol-transport.js +15 -0
  189. package/core/query-builder.d.ts +261 -0
  190. package/core/query-builder.d.ts.map +1 -0
  191. package/core/query-builder.js +538 -0
  192. package/core/request-pipeline.d.ts +88 -0
  193. package/core/request-pipeline.d.ts.map +1 -0
  194. package/core/request-pipeline.js +273 -0
  195. package/core/spatial-filter.d.ts +93 -0
  196. package/core/spatial-filter.d.ts.map +1 -0
  197. package/core/spatial-filter.js +153 -0
  198. package/core/stac-static.d.ts +51 -0
  199. package/core/stac-static.d.ts.map +1 -0
  200. package/core/stac-static.js +195 -0
  201. package/core/stac.d.ts +67 -0
  202. package/core/stac.d.ts.map +1 -0
  203. package/core/stac.js +259 -0
  204. package/core/surfaces.d.ts +540 -0
  205. package/core/surfaces.d.ts.map +1 -0
  206. package/core/surfaces.js +1964 -0
  207. package/core/terrain-elevation.d.ts +114 -0
  208. package/core/terrain-elevation.d.ts.map +1 -0
  209. package/core/terrain-elevation.js +150 -0
  210. package/core/types.d.ts +1404 -0
  211. package/core/types.d.ts.map +1 -0
  212. package/core/types.js +13 -0
  213. package/core/wfs-capabilities.d.ts +153 -0
  214. package/core/wfs-capabilities.d.ts.map +1 -0
  215. package/core/wfs-capabilities.js +490 -0
  216. package/core/wfs-filter.d.ts +118 -0
  217. package/core/wfs-filter.d.ts.map +1 -0
  218. package/core/wfs-filter.js +634 -0
  219. package/core/wfs.d.ts +232 -0
  220. package/core/wfs.d.ts.map +1 -0
  221. package/core/wfs.js +426 -0
  222. package/core/wire-shared.d.ts +32 -0
  223. package/core/wire-shared.d.ts.map +1 -0
  224. package/core/wire-shared.js +76 -0
  225. package/core/wms-axis.d.ts +36 -0
  226. package/core/wms-axis.d.ts.map +1 -0
  227. package/core/wms-axis.js +73 -0
  228. package/core/wms-capabilities.d.ts +110 -0
  229. package/core/wms-capabilities.d.ts.map +1 -0
  230. package/core/wms-capabilities.js +559 -0
  231. package/core/wms-types.d.ts +128 -0
  232. package/core/wms-types.d.ts.map +1 -0
  233. package/core/wms-types.js +32 -0
  234. package/core/wms.d.ts +144 -0
  235. package/core/wms.d.ts.map +1 -0
  236. package/core/wms.js +338 -0
  237. package/core/wmts-capabilities.d.ts +82 -0
  238. package/core/wmts-capabilities.d.ts.map +1 -0
  239. package/core/wmts-capabilities.js +402 -0
  240. package/core/wmts.d.ts +103 -0
  241. package/core/wmts.d.ts.map +1 -0
  242. package/core/wmts.js +332 -0
  243. package/core/xml-text.d.ts +20 -0
  244. package/core/xml-text.d.ts.map +1 -0
  245. package/core/xml-text.js +85 -0
  246. package/esri-compat/basemap-gallery.d.ts +38 -0
  247. package/esri-compat/basemap-gallery.d.ts.map +1 -0
  248. package/esri-compat/basemap-gallery.js +159 -0
  249. package/esri-compat/basemap-layer-list.d.ts +35 -0
  250. package/esri-compat/basemap-layer-list.d.ts.map +1 -0
  251. package/esri-compat/basemap-layer-list.js +139 -0
  252. package/esri-compat/basemap.d.ts +32 -0
  253. package/esri-compat/basemap.d.ts.map +1 -0
  254. package/esri-compat/basemap.js +84 -0
  255. package/esri-compat/bookmarks.d.ts +36 -0
  256. package/esri-compat/bookmarks.d.ts.map +1 -0
  257. package/esri-compat/bookmarks.js +108 -0
  258. package/esri-compat/class-breaks-renderer.d.ts +50 -0
  259. package/esri-compat/class-breaks-renderer.d.ts.map +1 -0
  260. package/esri-compat/class-breaks-renderer.js +164 -0
  261. package/esri-compat/color.d.ts +22 -0
  262. package/esri-compat/color.d.ts.map +1 -0
  263. package/esri-compat/color.js +106 -0
  264. package/esri-compat/controls.d.ts +174 -0
  265. package/esri-compat/controls.d.ts.map +1 -0
  266. package/esri-compat/controls.js +479 -0
  267. package/esri-compat/coordinate-conversion.d.ts +41 -0
  268. package/esri-compat/coordinate-conversion.d.ts.map +1 -0
  269. package/esri-compat/coordinate-conversion.js +125 -0
  270. package/esri-compat/directions.d.ts +45 -0
  271. package/esri-compat/directions.d.ts.map +1 -0
  272. package/esri-compat/directions.js +123 -0
  273. package/esri-compat/editor.d.ts +47 -0
  274. package/esri-compat/editor.d.ts.map +1 -0
  275. package/esri-compat/editor.js +170 -0
  276. package/esri-compat/esri-config.d.ts +16 -0
  277. package/esri-compat/esri-config.d.ts.map +1 -0
  278. package/esri-compat/esri-config.js +23 -0
  279. package/esri-compat/esri-request.d.ts +17 -0
  280. package/esri-compat/esri-request.d.ts.map +1 -0
  281. package/esri-compat/esri-request.js +127 -0
  282. package/esri-compat/event-bus.d.ts +106 -0
  283. package/esri-compat/event-bus.d.ts.map +1 -0
  284. package/esri-compat/event-bus.js +90 -0
  285. package/esri-compat/expand.d.ts +36 -0
  286. package/esri-compat/expand.d.ts.map +1 -0
  287. package/esri-compat/expand.js +97 -0
  288. package/esri-compat/extent.d.ts +43 -0
  289. package/esri-compat/extent.d.ts.map +1 -0
  290. package/esri-compat/extent.js +140 -0
  291. package/esri-compat/feature-filter.d.ts +55 -0
  292. package/esri-compat/feature-filter.d.ts.map +1 -0
  293. package/esri-compat/feature-filter.js +109 -0
  294. package/esri-compat/feature-form.d.ts +64 -0
  295. package/esri-compat/feature-form.d.ts.map +1 -0
  296. package/esri-compat/feature-form.js +137 -0
  297. package/esri-compat/feature-layer.d.ts +173 -0
  298. package/esri-compat/feature-layer.d.ts.map +1 -0
  299. package/esri-compat/feature-layer.js +661 -0
  300. package/esri-compat/feature-set.d.ts +32 -0
  301. package/esri-compat/feature-set.d.ts.map +1 -0
  302. package/esri-compat/feature-set.js +80 -0
  303. package/esri-compat/feature-table.d.ts +141 -0
  304. package/esri-compat/feature-table.d.ts.map +1 -0
  305. package/esri-compat/feature-table.js +500 -0
  306. package/esri-compat/feature-templates.d.ts +41 -0
  307. package/esri-compat/feature-templates.d.ts.map +1 -0
  308. package/esri-compat/feature-templates.js +97 -0
  309. package/esri-compat/feature.d.ts +33 -0
  310. package/esri-compat/feature.d.ts.map +1 -0
  311. package/esri-compat/feature.js +83 -0
  312. package/esri-compat/geojson-layer.d.ts +86 -0
  313. package/esri-compat/geojson-layer.d.ts.map +1 -0
  314. package/esri-compat/geojson-layer.js +191 -0
  315. package/esri-compat/geometry-engine.d.ts +107 -0
  316. package/esri-compat/geometry-engine.d.ts.map +1 -0
  317. package/esri-compat/geometry-engine.js +266 -0
  318. package/esri-compat/graphic.d.ts +46 -0
  319. package/esri-compat/graphic.d.ts.map +1 -0
  320. package/esri-compat/graphic.js +103 -0
  321. package/esri-compat/graphics-layer.d.ts +49 -0
  322. package/esri-compat/graphics-layer.d.ts.map +1 -0
  323. package/esri-compat/graphics-layer.js +185 -0
  324. package/esri-compat/group-layer.d.ts +48 -0
  325. package/esri-compat/group-layer.d.ts.map +1 -0
  326. package/esri-compat/group-layer.js +212 -0
  327. package/esri-compat/identify.d.ts +71 -0
  328. package/esri-compat/identify.d.ts.map +1 -0
  329. package/esri-compat/identify.js +299 -0
  330. package/esri-compat/identity-manager.d.ts +74 -0
  331. package/esri-compat/identity-manager.d.ts.map +1 -0
  332. package/esri-compat/identity-manager.js +245 -0
  333. package/esri-compat/imagery-layer.d.ts +78 -0
  334. package/esri-compat/imagery-layer.d.ts.map +1 -0
  335. package/esri-compat/imagery-layer.js +167 -0
  336. package/esri-compat/label-class.d.ts +31 -0
  337. package/esri-compat/label-class.d.ts.map +1 -0
  338. package/esri-compat/label-class.js +102 -0
  339. package/esri-compat/layer-list.d.ts +77 -0
  340. package/esri-compat/layer-list.d.ts.map +1 -0
  341. package/esri-compat/layer-list.js +342 -0
  342. package/esri-compat/legend.d.ts +53 -0
  343. package/esri-compat/legend.d.ts.map +1 -0
  344. package/esri-compat/legend.js +254 -0
  345. package/esri-compat/map-image-layer.d.ts +162 -0
  346. package/esri-compat/map-image-layer.d.ts.map +1 -0
  347. package/esri-compat/map-image-layer.js +709 -0
  348. package/esri-compat/map-view.d.ts +323 -0
  349. package/esri-compat/map-view.d.ts.map +1 -0
  350. package/esri-compat/map-view.js +1175 -0
  351. package/esri-compat/map.d.ts +47 -0
  352. package/esri-compat/map.d.ts.map +1 -0
  353. package/esri-compat/map.js +209 -0
  354. package/esri-compat/measurement-2d.d.ts +61 -0
  355. package/esri-compat/measurement-2d.d.ts.map +1 -0
  356. package/esri-compat/measurement-2d.js +184 -0
  357. package/esri-compat/measurement.d.ts +45 -0
  358. package/esri-compat/measurement.d.ts.map +1 -0
  359. package/esri-compat/measurement.js +193 -0
  360. package/esri-compat/oauth-info.d.ts +50 -0
  361. package/esri-compat/oauth-info.d.ts.map +1 -0
  362. package/esri-compat/oauth-info.js +142 -0
  363. package/esri-compat/picture-marker-symbol.d.ts +35 -0
  364. package/esri-compat/picture-marker-symbol.d.ts.map +1 -0
  365. package/esri-compat/picture-marker-symbol.js +126 -0
  366. package/esri-compat/point.d.ts +31 -0
  367. package/esri-compat/point.d.ts.map +1 -0
  368. package/esri-compat/point.js +102 -0
  369. package/esri-compat/polygon.d.ts +31 -0
  370. package/esri-compat/polygon.d.ts.map +1 -0
  371. package/esri-compat/polygon.js +107 -0
  372. package/esri-compat/polyline.d.ts +31 -0
  373. package/esri-compat/polyline.d.ts.map +1 -0
  374. package/esri-compat/polyline.js +107 -0
  375. package/esri-compat/popup-template.d.ts +55 -0
  376. package/esri-compat/popup-template.d.ts.map +1 -0
  377. package/esri-compat/popup-template.js +128 -0
  378. package/esri-compat/popup.d.ts +54 -0
  379. package/esri-compat/popup.d.ts.map +1 -0
  380. package/esri-compat/popup.js +312 -0
  381. package/esri-compat/print.d.ts +47 -0
  382. package/esri-compat/print.d.ts.map +1 -0
  383. package/esri-compat/print.js +105 -0
  384. package/esri-compat/query.d.ts +58 -0
  385. package/esri-compat/query.d.ts.map +1 -0
  386. package/esri-compat/query.js +128 -0
  387. package/esri-compat/reactive-utils.d.ts +24 -0
  388. package/esri-compat/reactive-utils.d.ts.map +1 -0
  389. package/esri-compat/reactive-utils.js +193 -0
  390. package/esri-compat/request.d.ts +41 -0
  391. package/esri-compat/request.d.ts.map +1 -0
  392. package/esri-compat/request.js +253 -0
  393. package/esri-compat/route-layer.d.ts +59 -0
  394. package/esri-compat/route-layer.d.ts.map +1 -0
  395. package/esri-compat/route-layer.js +223 -0
  396. package/esri-compat/route-task.d.ts +88 -0
  397. package/esri-compat/route-task.d.ts.map +1 -0
  398. package/esri-compat/route-task.js +225 -0
  399. package/esri-compat/scene-view.d.ts +41 -0
  400. package/esri-compat/scene-view.d.ts.map +1 -0
  401. package/esri-compat/scene-view.js +68 -0
  402. package/esri-compat/search.d.ts +112 -0
  403. package/esri-compat/search.d.ts.map +1 -0
  404. package/esri-compat/search.js +820 -0
  405. package/esri-compat/simple-fill-symbol.d.ts +27 -0
  406. package/esri-compat/simple-fill-symbol.d.ts.map +1 -0
  407. package/esri-compat/simple-fill-symbol.js +85 -0
  408. package/esri-compat/simple-line-symbol.d.ts +27 -0
  409. package/esri-compat/simple-line-symbol.d.ts.map +1 -0
  410. package/esri-compat/simple-line-symbol.js +85 -0
  411. package/esri-compat/simple-marker-symbol.d.ts +29 -0
  412. package/esri-compat/simple-marker-symbol.d.ts.map +1 -0
  413. package/esri-compat/simple-marker-symbol.js +92 -0
  414. package/esri-compat/simple-renderer.d.ts +29 -0
  415. package/esri-compat/simple-renderer.d.ts.map +1 -0
  416. package/esri-compat/simple-renderer.js +92 -0
  417. package/esri-compat/sketch.d.ts +64 -0
  418. package/esri-compat/sketch.d.ts.map +1 -0
  419. package/esri-compat/sketch.js +227 -0
  420. package/esri-compat/spatial-reference.d.ts +31 -0
  421. package/esri-compat/spatial-reference.d.ts.map +1 -0
  422. package/esri-compat/spatial-reference.js +102 -0
  423. package/esri-compat/swipe.d.ts +34 -0
  424. package/esri-compat/swipe.d.ts.map +1 -0
  425. package/esri-compat/swipe.js +93 -0
  426. package/esri-compat/table-list.d.ts +36 -0
  427. package/esri-compat/table-list.d.ts.map +1 -0
  428. package/esri-compat/table-list.js +126 -0
  429. package/esri-compat/text-symbol.d.ts +37 -0
  430. package/esri-compat/text-symbol.d.ts.map +1 -0
  431. package/esri-compat/text-symbol.js +123 -0
  432. package/esri-compat/tile-layer.d.ts +52 -0
  433. package/esri-compat/tile-layer.d.ts.map +1 -0
  434. package/esri-compat/tile-layer.js +168 -0
  435. package/esri-compat/time-slider.d.ts +70 -0
  436. package/esri-compat/time-slider.d.ts.map +1 -0
  437. package/esri-compat/time-slider.js +208 -0
  438. package/esri-compat/track.d.ts +51 -0
  439. package/esri-compat/track.d.ts.map +1 -0
  440. package/esri-compat/track.js +174 -0
  441. package/esri-compat/unique-value-renderer.d.ts +41 -0
  442. package/esri-compat/unique-value-renderer.d.ts.map +1 -0
  443. package/esri-compat/unique-value-renderer.js +127 -0
  444. package/esri-compat/url.d.ts +13 -0
  445. package/esri-compat/url.d.ts.map +1 -0
  446. package/esri-compat/url.js +68 -0
  447. package/esri-compat/vector-tile-layer.d.ts +47 -0
  448. package/esri-compat/vector-tile-layer.d.ts.map +1 -0
  449. package/esri-compat/vector-tile-layer.js +130 -0
  450. package/esri-compat/web-map.d.ts +14 -0
  451. package/esri-compat/web-map.d.ts.map +1 -0
  452. package/esri-compat/web-map.js +24 -0
  453. package/esri-compat/wfs-layer.d.ts +64 -0
  454. package/esri-compat/wfs-layer.d.ts.map +1 -0
  455. package/esri-compat/wfs-layer.js +145 -0
  456. package/esri-compat/wms-layer.d.ts +76 -0
  457. package/esri-compat/wms-layer.d.ts.map +1 -0
  458. package/esri-compat/wms-layer.js +157 -0
  459. package/exploration/context.d.ts +25 -0
  460. package/exploration/context.d.ts.map +1 -0
  461. package/exploration/context.js +396 -0
  462. package/exploration/index.d.ts +27 -0
  463. package/exploration/index.d.ts.map +1 -0
  464. package/exploration/index.js +24 -0
  465. package/exploration/presets.d.ts +27 -0
  466. package/exploration/presets.d.ts.map +1 -0
  467. package/exploration/presets.js +114 -0
  468. package/exploration/reducer.d.ts +23 -0
  469. package/exploration/reducer.d.ts.map +1 -0
  470. package/exploration/reducer.js +332 -0
  471. package/exploration/selection.d.ts +25 -0
  472. package/exploration/selection.d.ts.map +1 -0
  473. package/exploration/selection.js +34 -0
  474. package/exploration/selectors.d.ts +56 -0
  475. package/exploration/selectors.d.ts.map +1 -0
  476. package/exploration/selectors.js +110 -0
  477. package/exploration/types.d.ts +294 -0
  478. package/exploration/types.d.ts.map +1 -0
  479. package/exploration/types.js +33 -0
  480. package/expr/expression.d.ts +457 -0
  481. package/expr/expression.d.ts.map +1 -0
  482. package/expr/expression.js +663 -0
  483. package/expr/index.d.ts +32 -0
  484. package/expr/index.d.ts.map +1 -0
  485. package/expr/index.js +30 -0
  486. package/filter-registry/index.d.ts +145 -0
  487. package/filter-registry/index.d.ts.map +1 -0
  488. package/filter-registry/index.js +531 -0
  489. package/gen/honua/v1/feature_service_pb.d.ts +918 -0
  490. package/gen/honua/v1/feature_service_pb.d.ts.map +1 -0
  491. package/gen/honua/v1/feature_service_pb.js +375 -0
  492. package/generated-app/errors.d.ts +41 -0
  493. package/generated-app/errors.d.ts.map +1 -0
  494. package/generated-app/errors.js +41 -0
  495. package/generated-app/index.d.ts +17 -0
  496. package/generated-app/index.d.ts.map +1 -0
  497. package/generated-app/index.js +12 -0
  498. package/generated-app/manifest.d.ts +183 -0
  499. package/generated-app/manifest.d.ts.map +1 -0
  500. package/generated-app/manifest.js +14 -0
  501. package/generated-app/projection.d.ts +44 -0
  502. package/generated-app/projection.d.ts.map +1 -0
  503. package/generated-app/projection.js +234 -0
  504. package/generated-app/runtime.d.ts +215 -0
  505. package/generated-app/runtime.d.ts.map +1 -0
  506. package/generated-app/runtime.js +1196 -0
  507. package/geocoding/index.d.ts +106 -0
  508. package/geocoding/index.d.ts.map +1 -0
  509. package/geocoding/index.js +278 -0
  510. package/interactions/detail-model.d.ts +54 -0
  511. package/interactions/detail-model.d.ts.map +1 -0
  512. package/interactions/detail-model.js +105 -0
  513. package/interactions/exploration-bindings.d.ts +120 -0
  514. package/interactions/exploration-bindings.d.ts.map +1 -0
  515. package/interactions/exploration-bindings.js +218 -0
  516. package/interactions/feature-state.d.ts +144 -0
  517. package/interactions/feature-state.d.ts.map +1 -0
  518. package/interactions/feature-state.js +169 -0
  519. package/interactions/hit-test.d.ts +112 -0
  520. package/interactions/hit-test.d.ts.map +1 -0
  521. package/interactions/hit-test.js +291 -0
  522. package/interactions/index.d.ts +31 -0
  523. package/interactions/index.d.ts.map +1 -0
  524. package/interactions/index.js +24 -0
  525. package/interactions/share-state.d.ts +36 -0
  526. package/interactions/share-state.d.ts.map +1 -0
  527. package/interactions/share-state.js +123 -0
  528. package/map/feature-service-adapter.d.ts +138 -0
  529. package/map/feature-service-adapter.d.ts.map +1 -0
  530. package/map/feature-service-adapter.js +279 -0
  531. package/map/honua-map.d.ts +203 -0
  532. package/map/honua-map.d.ts.map +1 -0
  533. package/map/honua-map.js +347 -0
  534. package/map/index.d.ts +27 -0
  535. package/map/index.d.ts.map +1 -0
  536. package/map/index.js +22 -0
  537. package/map/maplibre-target.d.ts +103 -0
  538. package/map/maplibre-target.d.ts.map +1 -0
  539. package/map/maplibre-target.js +164 -0
  540. package/map/webmap-maplibre.d.ts +60 -0
  541. package/map/webmap-maplibre.d.ts.map +1 -0
  542. package/map/webmap-maplibre.js +212 -0
  543. package/operate/client.d.ts +94 -0
  544. package/operate/client.d.ts.map +1 -0
  545. package/operate/client.js +361 -0
  546. package/operate/index.d.ts +22 -0
  547. package/operate/index.d.ts.map +1 -0
  548. package/operate/index.js +19 -0
  549. package/operate/types.d.ts +397 -0
  550. package/operate/types.d.ts.map +1 -0
  551. package/operate/types.js +17 -0
  552. package/operator/client.d.ts +60 -0
  553. package/operator/client.d.ts.map +1 -0
  554. package/operator/client.js +20 -0
  555. package/operator/controllers/approval.d.ts +52 -0
  556. package/operator/controllers/approval.d.ts.map +1 -0
  557. package/operator/controllers/approval.js +123 -0
  558. package/operator/controllers/base.d.ts +27 -0
  559. package/operator/controllers/base.d.ts.map +1 -0
  560. package/operator/controllers/base.js +53 -0
  561. package/operator/controllers/builder-workspace.d.ts +55 -0
  562. package/operator/controllers/builder-workspace.d.ts.map +1 -0
  563. package/operator/controllers/builder-workspace.js +108 -0
  564. package/operator/controllers/chat.d.ts +61 -0
  565. package/operator/controllers/chat.d.ts.map +1 -0
  566. package/operator/controllers/chat.js +154 -0
  567. package/operator/controllers/clarification.d.ts +52 -0
  568. package/operator/controllers/clarification.d.ts.map +1 -0
  569. package/operator/controllers/clarification.js +121 -0
  570. package/operator/controllers/execution.d.ts +60 -0
  571. package/operator/controllers/execution.d.ts.map +1 -0
  572. package/operator/controllers/execution.js +286 -0
  573. package/operator/controllers/index.d.ts +23 -0
  574. package/operator/controllers/index.d.ts.map +1 -0
  575. package/operator/controllers/index.js +15 -0
  576. package/operator/controllers/map-workspace.d.ts +85 -0
  577. package/operator/controllers/map-workspace.d.ts.map +1 -0
  578. package/operator/controllers/map-workspace.js +296 -0
  579. package/operator/controllers/plan-review.d.ts +66 -0
  580. package/operator/controllers/plan-review.d.ts.map +1 -0
  581. package/operator/controllers/plan-review.js +124 -0
  582. package/operator/errors.d.ts +62 -0
  583. package/operator/errors.d.ts.map +1 -0
  584. package/operator/errors.js +75 -0
  585. package/operator/i18n/index.d.ts +10 -0
  586. package/operator/i18n/index.d.ts.map +1 -0
  587. package/operator/i18n/index.js +8 -0
  588. package/operator/i18n/messages.d.ts +19 -0
  589. package/operator/i18n/messages.d.ts.map +1 -0
  590. package/operator/i18n/messages.js +54 -0
  591. package/operator/index.d.ts +19 -0
  592. package/operator/index.d.ts.map +1 -0
  593. package/operator/index.js +15 -0
  594. package/operator/telemetry.d.ts +26 -0
  595. package/operator/telemetry.d.ts.map +1 -0
  596. package/operator/telemetry.js +9 -0
  597. package/operator/theming/index.d.ts +12 -0
  598. package/operator/theming/index.d.ts.map +1 -0
  599. package/operator/theming/index.js +9 -0
  600. package/operator/theming/provider.d.ts +27 -0
  601. package/operator/theming/provider.d.ts.map +1 -0
  602. package/operator/theming/provider.js +26 -0
  603. package/operator/theming/tokens.d.ts +28 -0
  604. package/operator/theming/tokens.d.ts.map +1 -0
  605. package/operator/theming/tokens.js +65 -0
  606. package/operator/workspace/events.d.ts +70 -0
  607. package/operator/workspace/events.d.ts.map +1 -0
  608. package/operator/workspace/events.js +9 -0
  609. package/operator/workspace/index.d.ts +12 -0
  610. package/operator/workspace/index.d.ts.map +1 -0
  611. package/operator/workspace/index.js +8 -0
  612. package/operator/workspace/types.d.ts +178 -0
  613. package/operator/workspace/types.d.ts.map +1 -0
  614. package/operator/workspace/types.js +47 -0
  615. package/operator/workspace/workspace.d.ts +58 -0
  616. package/operator/workspace/workspace.d.ts.map +1 -0
  617. package/operator/workspace/workspace.js +339 -0
  618. package/package.json +105 -0
  619. package/realtime/exploration.d.ts +16 -0
  620. package/realtime/exploration.d.ts.map +1 -0
  621. package/realtime/exploration.js +32 -0
  622. package/realtime/honua-server.d.ts +123 -0
  623. package/realtime/honua-server.d.ts.map +1 -0
  624. package/realtime/honua-server.js +188 -0
  625. package/realtime/index.d.ts +20 -0
  626. package/realtime/index.d.ts.map +1 -0
  627. package/realtime/index.js +14 -0
  628. package/realtime/projections.d.ts +30 -0
  629. package/realtime/projections.d.ts.map +1 -0
  630. package/realtime/projections.js +51 -0
  631. package/realtime/reducer.d.ts +14 -0
  632. package/realtime/reducer.d.ts.map +1 -0
  633. package/realtime/reducer.js +351 -0
  634. package/realtime/sse.d.ts +23 -0
  635. package/realtime/sse.d.ts.map +1 -0
  636. package/realtime/sse.js +143 -0
  637. package/realtime/store.d.ts +18 -0
  638. package/realtime/store.d.ts.map +1 -0
  639. package/realtime/store.js +63 -0
  640. package/realtime/types.d.ts +190 -0
  641. package/realtime/types.d.ts.map +1 -0
  642. package/realtime/types.js +10 -0
  643. package/replica-sync/client.d.ts +32 -0
  644. package/replica-sync/client.d.ts.map +1 -0
  645. package/replica-sync/client.js +52 -0
  646. package/replica-sync/errors.d.ts +22 -0
  647. package/replica-sync/errors.d.ts.map +1 -0
  648. package/replica-sync/errors.js +29 -0
  649. package/replica-sync/fixture.d.ts +40 -0
  650. package/replica-sync/fixture.d.ts.map +1 -0
  651. package/replica-sync/fixture.js +364 -0
  652. package/replica-sync/index.d.ts +22 -0
  653. package/replica-sync/index.d.ts.map +1 -0
  654. package/replica-sync/index.js +17 -0
  655. package/replica-sync/types.d.ts +270 -0
  656. package/replica-sync/types.d.ts.map +1 -0
  657. package/replica-sync/types.js +13 -0
  658. package/runtime/diff.d.ts +31 -0
  659. package/runtime/diff.d.ts.map +1 -0
  660. package/runtime/diff.js +143 -0
  661. package/runtime/errors.d.ts +40 -0
  662. package/runtime/errors.d.ts.map +1 -0
  663. package/runtime/errors.js +35 -0
  664. package/runtime/index.d.ts +59 -0
  665. package/runtime/index.d.ts.map +1 -0
  666. package/runtime/index.js +40 -0
  667. package/runtime/legend.d.ts +23 -0
  668. package/runtime/legend.d.ts.map +1 -0
  669. package/runtime/legend.js +38 -0
  670. package/runtime/load-package.d.ts +125 -0
  671. package/runtime/load-package.d.ts.map +1 -0
  672. package/runtime/load-package.js +272 -0
  673. package/runtime/map-package-fetch.d.ts +107 -0
  674. package/runtime/map-package-fetch.d.ts.map +1 -0
  675. package/runtime/map-package-fetch.js +336 -0
  676. package/runtime/map-package-realtime.d.ts +169 -0
  677. package/runtime/map-package-realtime.d.ts.map +1 -0
  678. package/runtime/map-package-realtime.js +350 -0
  679. package/runtime/map-package-validation.d.ts +30 -0
  680. package/runtime/map-package-validation.d.ts.map +1 -0
  681. package/runtime/map-package-validation.js +257 -0
  682. package/runtime/map-package-watch.d.ts +78 -0
  683. package/runtime/map-package-watch.d.ts.map +1 -0
  684. package/runtime/map-package-watch.js +695 -0
  685. package/runtime/map-package.d.ts +167 -0
  686. package/runtime/map-package.d.ts.map +1 -0
  687. package/runtime/map-package.js +15 -0
  688. package/runtime/ogc-styles.d.ts +110 -0
  689. package/runtime/ogc-styles.d.ts.map +1 -0
  690. package/runtime/ogc-styles.js +195 -0
  691. package/runtime/pmtiles-protocol.d.ts +77 -0
  692. package/runtime/pmtiles-protocol.d.ts.map +1 -0
  693. package/runtime/pmtiles-protocol.js +120 -0
  694. package/runtime/popups.d.ts +62 -0
  695. package/runtime/popups.d.ts.map +1 -0
  696. package/runtime/popups.js +95 -0
  697. package/runtime/query-tiles.d.ts +217 -0
  698. package/runtime/query-tiles.d.ts.map +1 -0
  699. package/runtime/query-tiles.js +801 -0
  700. package/runtime/runtime.d.ts +243 -0
  701. package/runtime/runtime.d.ts.map +1 -0
  702. package/runtime/runtime.js +1147 -0
  703. package/runtime/source-bridge.d.ts +99 -0
  704. package/runtime/source-bridge.d.ts.map +1 -0
  705. package/runtime/source-bridge.js +428 -0
  706. package/runtime/style-compose.d.ts +46 -0
  707. package/runtime/style-compose.d.ts.map +1 -0
  708. package/runtime/style-compose.js +144 -0
  709. package/runtime/style-interactions.d.ts +141 -0
  710. package/runtime/style-interactions.d.ts.map +1 -0
  711. package/runtime/style-interactions.js +574 -0
  712. package/runtime/style-spec-validation.d.ts +23 -0
  713. package/runtime/style-spec-validation.d.ts.map +1 -0
  714. package/runtime/style-spec-validation.js +357 -0
  715. package/scene-workspace/analysis-widgets.d.ts +334 -0
  716. package/scene-workspace/analysis-widgets.d.ts.map +1 -0
  717. package/scene-workspace/analysis-widgets.js +424 -0
  718. package/scene-workspace/cesium-adapter.d.ts +438 -0
  719. package/scene-workspace/cesium-adapter.d.ts.map +1 -0
  720. package/scene-workspace/cesium-adapter.js +493 -0
  721. package/scene-workspace/index.d.ts +21 -0
  722. package/scene-workspace/index.d.ts.map +1 -0
  723. package/scene-workspace/index.js +14 -0
  724. package/scene-workspace/primitives.d.ts +162 -0
  725. package/scene-workspace/primitives.d.ts.map +1 -0
  726. package/scene-workspace/primitives.js +255 -0
  727. package/scene-workspace/scene-discovery.d.ts +196 -0
  728. package/scene-workspace/scene-discovery.d.ts.map +1 -0
  729. package/scene-workspace/scene-discovery.js +228 -0
  730. package/scene-workspace/scene-view.d.ts +209 -0
  731. package/scene-workspace/scene-view.d.ts.map +1 -0
  732. package/scene-workspace/scene-view.js +267 -0
  733. package/scene-workspace/types.d.ts +229 -0
  734. package/scene-workspace/types.d.ts.map +1 -0
  735. package/scene-workspace/types.js +23 -0
  736. package/scene-workspace/workspace.d.ts +13 -0
  737. package/scene-workspace/workspace.d.ts.map +1 -0
  738. package/scene-workspace/workspace.js +391 -0
  739. package/share/denial.d.ts +78 -0
  740. package/share/denial.d.ts.map +1 -0
  741. package/share/denial.js +61 -0
  742. package/share/embed-token.d.ts +71 -0
  743. package/share/embed-token.d.ts.map +1 -0
  744. package/share/embed-token.js +99 -0
  745. package/share/index.d.ts +35 -0
  746. package/share/index.d.ts.map +1 -0
  747. package/share/index.js +30 -0
  748. package/share/open-data.d.ts +114 -0
  749. package/share/open-data.d.ts.map +1 -0
  750. package/share/open-data.js +81 -0
  751. package/share/types.d.ts +163 -0
  752. package/share/types.d.ts.map +1 -0
  753. package/share/types.js +17 -0
  754. package/studio/capability-manifest.d.ts +285 -0
  755. package/studio/capability-manifest.d.ts.map +1 -0
  756. package/studio/capability-manifest.js +59 -0
  757. package/studio/chart-spec.d.ts +85 -0
  758. package/studio/chart-spec.d.ts.map +1 -0
  759. package/studio/chart-spec.js +106 -0
  760. package/studio/index.d.ts +34 -0
  761. package/studio/index.d.ts.map +1 -0
  762. package/studio/index.js +27 -0
  763. package/studio/provenance.d.ts +131 -0
  764. package/studio/provenance.d.ts.map +1 -0
  765. package/studio/provenance.js +39 -0
  766. package/studio/publish.d.ts +64 -0
  767. package/studio/publish.d.ts.map +1 -0
  768. package/studio/publish.js +13 -0
  769. package/studio/types.d.ts +278 -0
  770. package/studio/types.d.ts.map +1 -0
  771. package/studio/types.js +71 -0
  772. package/studio/validate.d.ts +45 -0
  773. package/studio/validate.d.ts.map +1 -0
  774. package/studio/validate.js +255 -0
  775. package/studio/validation.d.ts +112 -0
  776. package/studio/validation.d.ts.map +1 -0
  777. package/studio/validation.js +42 -0
  778. package/style/factory.d.ts +30 -0
  779. package/style/factory.d.ts.map +1 -0
  780. package/style/factory.js +61 -0
  781. package/style/index.d.ts +25 -0
  782. package/style/index.d.ts.map +1 -0
  783. package/style/index.js +24 -0
  784. package/style/specification.d.ts +203 -0
  785. package/style/specification.d.ts.map +1 -0
  786. package/style/specification.js +178 -0
  787. package/style/wms-wmts-resolvers.d.ts +38 -0
  788. package/style/wms-wmts-resolvers.d.ts.map +1 -0
  789. package/style/wms-wmts-resolvers.js +92 -0
  790. package/web-components/controller.d.ts +39 -0
  791. package/web-components/controller.d.ts.map +1 -0
  792. package/web-components/controller.js +640 -0
  793. package/web-components/elements.d.ts +210 -0
  794. package/web-components/elements.d.ts.map +1 -0
  795. package/web-components/elements.js +1651 -0
  796. package/web-components/index.d.ts +24 -0
  797. package/web-components/index.d.ts.map +1 -0
  798. package/web-components/index.js +22 -0
  799. package/web-components/maplibre-renderer.d.ts +22 -0
  800. package/web-components/maplibre-renderer.d.ts.map +1 -0
  801. package/web-components/maplibre-renderer.js +474 -0
  802. package/web-components/types.d.ts +397 -0
  803. package/web-components/types.d.ts.map +1 -0
  804. package/web-components/types.js +1 -0
  805. package/webmap/contract.d.ts +28 -0
  806. package/webmap/contract.d.ts.map +1 -0
  807. package/webmap/contract.js +130 -0
  808. package/webmap/convert-basemap.d.ts +17 -0
  809. package/webmap/convert-basemap.d.ts.map +1 -0
  810. package/webmap/convert-basemap.js +78 -0
  811. package/webmap/convert-extent.d.ts +17 -0
  812. package/webmap/convert-extent.d.ts.map +1 -0
  813. package/webmap/convert-extent.js +114 -0
  814. package/webmap/convert-label.d.ts +13 -0
  815. package/webmap/convert-label.d.ts.map +1 -0
  816. package/webmap/convert-label.js +125 -0
  817. package/webmap/convert-layer.d.ts +22 -0
  818. package/webmap/convert-layer.d.ts.map +1 -0
  819. package/webmap/convert-layer.js +181 -0
  820. package/webmap/convert-popup.d.ts +34 -0
  821. package/webmap/convert-popup.d.ts.map +1 -0
  822. package/webmap/convert-popup.js +68 -0
  823. package/webmap/convert-renderer.d.ts +17 -0
  824. package/webmap/convert-renderer.d.ts.map +1 -0
  825. package/webmap/convert-renderer.js +161 -0
  826. package/webmap/convert-symbol.d.ts +17 -0
  827. package/webmap/convert-symbol.d.ts.map +1 -0
  828. package/webmap/convert-symbol.js +122 -0
  829. package/webmap/index.d.ts +44 -0
  830. package/webmap/index.d.ts.map +1 -0
  831. package/webmap/index.js +33 -0
  832. package/webmap/parse.d.ts +25 -0
  833. package/webmap/parse.d.ts.map +1 -0
  834. package/webmap/parse.js +83 -0
  835. package/webmap/types.d.ts +264 -0
  836. package/webmap/types.d.ts.map +1 -0
  837. package/webmap/types.js +13 -0
  838. package/webmap/warnings.d.ts +30 -0
  839. package/webmap/warnings.d.ts.map +1 -0
  840. package/webmap/warnings.js +37 -0
@@ -0,0 +1,3909 @@
1
+ /**
2
+ * Built-in `Source` adapters that wrap the runtime classes in
3
+ * `src/core/surfaces.ts` (`HonuaFeatureLayer`, `HonuaMapLayer`,
4
+ * `HonuaOgcFeatureCollection`) and the OGC / STAC / WFS / WMS / OData
5
+ * adapters in `src/core/`. Consumers register adapters for any other
6
+ * protocol (MapLibre-native sources, etc.) via
7
+ * `CreateDatasetOptions.resolveSource`.
8
+ *
9
+ * These adapters do not reimplement query logic — they translate the
10
+ * canonical `Query` / `Result` envelope to and from the existing per-class
11
+ * request and response shapes.
12
+ *
13
+ * @module
14
+ */
15
+ import { HonuaCapabilityNotSupportedError, HonuaHttpError } from "../core/errors.js";
16
+ import { HonuaOdataEntitySet, buildOdataSpatialFilter, odataFieldSchema, rewriteWhereToOdataFilter, } from "../core/odata.js";
17
+ import { HonuaOgcCollectionMap, HonuaOgcMaps } from "../core/ogc-maps.js";
18
+ import { HonuaOgcRecordCollection } from "../core/ogc-records.js";
19
+ import { HonuaOgcTiles, HonuaOgcTileset } from "../core/ogc-tiles.js";
20
+ import { trimTrailingSlashes } from "../core/path-utils.js";
21
+ import { HonuaStacStaticCatalog } from "../core/stac-static.js";
22
+ import { HonuaStacSearch } from "../core/stac.js";
23
+ import { HonuaFeatureLayer, HonuaGeometryService, HonuaGeoprocessingService, HonuaImageService, HonuaMapLayer, HonuaMapService, HonuaOgcFeatureCollection, HonuaOgcFeatures, } from "../core/surfaces.js";
24
+ import { UNSUPPORTED_FES, compileSpatialFilter, compileWhere, geoJsonGeometryToGml, serializeFes, } from "../core/wfs-filter.js";
25
+ import { HonuaWfs, HonuaWfsFeatureType } from "../core/wfs.js";
26
+ import { HonuaWms, HonuaWmsLayer, parseWmsLayerNames } from "../core/wms.js";
27
+ import { HonuaWmts, HonuaWmtsLayer, HonuaWmtsTileset } from "../core/wmts.js";
28
+ import { HonuaPmtilesArchive, stripPmtilesScheme } from "./pmtiles.js";
29
+ import { CAPABILITIES, PROTOCOL_DEFAULT_CAPABILITIES, } from "./types.js";
30
+ /**
31
+ * Construct a protocol-neutral `Dataset` over a `HonuaClient`.
32
+ *
33
+ * A `Dataset` groups one or more `Source`s. Each `Source` accepts a protocol-neutral
34
+ * `Query` and returns a protocol-neutral `Result`. The dataset is the cross-protocol
35
+ * unit of work: a single `Dataset` can mix GeoServices FeatureServer, OGC API Features,
36
+ * WFS, STAC, and OData sources under one capability policy.
37
+ *
38
+ * `Source` handles are constructed lazily, server compatibility is checked once per
39
+ * dataset (and cached on the underlying client), and capability gaps surface as
40
+ * {@link HonuaCapabilityNotSupportedError} under the default `"strict"` policy rather
41
+ * than silently returning empty results.
42
+ *
43
+ * Operations the canonical surface does not cover stay reachable through the typed
44
+ * `source.protocol(...)` escape hatch (e.g. ImageServer `exportImage`, GeometryServer
45
+ * `project`, GPServer `submitJob`, WFS `LockFeature`).
46
+ *
47
+ * @param options - The dataset id, the `HonuaClient` to talk to, the list of
48
+ * `SourceDescriptor`s, and (optionally) `capabilityPolicy` and a custom
49
+ * `resolveSource` factory.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { createDataset, PROTOCOL_DEFAULT_CAPABILITIES } from "@honua/sdk-js/contract";
54
+ * import { HonuaClient } from "@honua/sdk-js/honua";
55
+ *
56
+ * const client = new HonuaClient({ baseUrl: "https://your-honua-server.example" });
57
+ *
58
+ * const dataset = createDataset({
59
+ * id: "parcels",
60
+ * client,
61
+ * sources: [
62
+ * {
63
+ * id: "parcels-fs",
64
+ * protocol: "geoservices-feature-service",
65
+ * locator: { url: "https://your-honua-server.example", serviceId: "parcels", layerId: 0 },
66
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"],
67
+ * },
68
+ * ],
69
+ * });
70
+ *
71
+ * const parcels = dataset.source("parcels-fs")!;
72
+ * const result = await parcels.queryAll({
73
+ * where: "STATUS = 'ACTIVE'",
74
+ * outFields: ["OBJECTID", "NAME"],
75
+ * returnGeometry: true,
76
+ * pagination: { limit: 500 },
77
+ * });
78
+ *
79
+ * console.log(`Loaded ${result.features.length} features`);
80
+ * ```
81
+ */
82
+ export function createDataset(options) {
83
+ const { id, client, sources, resolveSource } = options;
84
+ const policy = options.capabilityPolicy ?? "strict";
85
+ const descriptors = new Map();
86
+ const handles = new Map();
87
+ let compatibilityPromise;
88
+ for (const descriptor of sources) {
89
+ if (descriptors.has(descriptor.id)) {
90
+ throw new Error(`createDataset: duplicate source id "${descriptor.id}"`);
91
+ }
92
+ descriptors.set(descriptor.id, descriptor);
93
+ }
94
+ function resolve(descriptor) {
95
+ const cached = handles.get(descriptor.id);
96
+ if (cached)
97
+ return cached;
98
+ const built = buildBuiltInSource(descriptor, client, policy) ??
99
+ resolveSource?.(descriptor, { client, capabilityPolicy: policy });
100
+ if (!built) {
101
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
102
+ }
103
+ handles.set(descriptor.id, built);
104
+ return built;
105
+ }
106
+ return {
107
+ id,
108
+ client,
109
+ sourceDescriptors: [...descriptors.values()],
110
+ source(sourceId) {
111
+ const descriptor = descriptors.get(sourceId);
112
+ if (!descriptor)
113
+ return undefined;
114
+ return resolve(descriptor);
115
+ },
116
+ sourceIds() {
117
+ return [...descriptors.keys()];
118
+ },
119
+ isCompatible() {
120
+ if (options.skipCompatibilityCheck)
121
+ return Promise.resolve(true);
122
+ if (!compatibilityPromise) {
123
+ compatibilityPromise = client
124
+ .checkCompatibility()
125
+ .then((status) => status.supported)
126
+ .catch(() => false);
127
+ }
128
+ return compatibilityPromise;
129
+ },
130
+ supportsFeature(feature) {
131
+ return client.supportsFeature(feature).catch(() => false);
132
+ },
133
+ };
134
+ }
135
+ // ── Built-in resolver ─────────────────────────────────────────
136
+ function buildBuiltInSource(descriptor, client, policy) {
137
+ switch (descriptor.protocol) {
138
+ case "geoservices-feature-service":
139
+ return geoServicesFeatureSource(descriptor, client, policy);
140
+ case "geoservices-map-service":
141
+ return geoServicesMapServiceSource(descriptor, client, policy);
142
+ case "geoservices-image-service":
143
+ return geoServicesImageSource(descriptor, client, policy);
144
+ case "geoservices-geometry-service":
145
+ return geoServicesGeometryServiceSource(descriptor, client, policy);
146
+ case "geoservices-gp-service":
147
+ return geoServicesGPServiceSource(descriptor, client, policy);
148
+ case "ogc-features":
149
+ return ogcFeaturesSource(descriptor, client, policy);
150
+ case "ogc-tiles":
151
+ return ogcTilesSource(descriptor, client, policy);
152
+ case "ogc-maps":
153
+ return ogcMapsSource(descriptor, client, policy);
154
+ case "ogc-records":
155
+ return ogcRecordsSource(descriptor, client, policy);
156
+ case "wms":
157
+ return wmsSource(descriptor, client, policy);
158
+ case "wmts":
159
+ return wmtsSource(descriptor, client, policy);
160
+ case "stac":
161
+ return stacSearchSource(descriptor, client, policy);
162
+ case "wfs":
163
+ return wfsSource(descriptor, client, policy);
164
+ case "odata":
165
+ return odataSource(descriptor, client, policy);
166
+ case "pmtiles":
167
+ return pmtilesSource(descriptor, client, policy);
168
+ default:
169
+ return undefined;
170
+ }
171
+ }
172
+ // ── GeoServices Feature Service ───────────────────────────────
173
+ /**
174
+ * Adapter factory for a GeoServices FeatureServer layer.
175
+ *
176
+ * Used internally by {@link createDataset} when it sees a descriptor with
177
+ * `protocol: "geoservices-feature-service"`. Calling it directly is rarely
178
+ * necessary; pass the descriptor to `createDataset` instead.
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * const dataset = createDataset({
183
+ * id: "parcels",
184
+ * client,
185
+ * sources: [{
186
+ * id: "parcels-fs",
187
+ * protocol: "geoservices-feature-service",
188
+ * locator: { url: "https://your-honua-server.example", serviceId: "parcels", layerId: 0 },
189
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"],
190
+ * }],
191
+ * });
192
+ * const result = await dataset.source("parcels-fs")!.queryAll({
193
+ * where: "STATUS = 'ACTIVE'",
194
+ * outFields: ["OBJECTID", "NAME"],
195
+ * });
196
+ * ```
197
+ */
198
+ export function geoServicesFeatureSource(descriptor, client, policy) {
199
+ const { serviceId, layerId } = requireFeatureServiceLocator(descriptor);
200
+ const layer = new HonuaFeatureLayer({ client, serviceId, layerId });
201
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"];
202
+ const adapterRegistry = {
203
+ "geoservices-feature-service": layer,
204
+ };
205
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
206
+ async query(request) {
207
+ ensureCapability(descriptor, caps, "query");
208
+ const requestParams = toFeatureLayerRequest(request);
209
+ if (request?.aggregation) {
210
+ ensureCapability(descriptor, caps, "queryAggregate");
211
+ const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
212
+ return aggregateResultFromFeatureLayer(await layer.queryFeatures({ ...requestParams, ...aggFields }), aggregateAlias);
213
+ }
214
+ const response = await layer.queryFeatures(requestParams);
215
+ return featureLayerResultFromTyped(response);
216
+ },
217
+ async queryAll(request) {
218
+ ensureCapability(descriptor, caps, "query");
219
+ const params = withPagingBounds(toFeatureLayerRequest(request), request?.pagination?.limit);
220
+ const features = await layer.queryFeaturesAll(params);
221
+ const { features: limited, exceededTransferLimit } = applyQueryAllLimit(features, request?.pagination?.limit);
222
+ return {
223
+ features: limited,
224
+ exceededTransferLimit,
225
+ totalCount: limited.length,
226
+ };
227
+ },
228
+ async queryAggregate(request) {
229
+ ensureCapability(descriptor, caps, "queryAggregate");
230
+ const requestParams = toFeatureLayerRequest(request);
231
+ const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
232
+ const response = await layer.queryFeatures({ ...requestParams, ...aggFields });
233
+ return aggregateResultFromFeatureLayer(response, aggregateAlias);
234
+ },
235
+ async queryExtent(request) {
236
+ ensureCapability(descriptor, caps, "queryExtent");
237
+ const response = await layer.queryFeatures(toExtentOnlyRequest(toFeatureLayerRequest(request)));
238
+ return extractExtentEnvelope(response);
239
+ },
240
+ async *stream(request) {
241
+ ensureCapability(descriptor, caps, "stream");
242
+ const stream = layer.queryFeaturesStream(withStreamPageSize(toFeatureLayerRequest(request), request?.pagination?.limit));
243
+ for await (const page of stream) {
244
+ yield {
245
+ features: page,
246
+ exceededTransferLimit: false,
247
+ };
248
+ }
249
+ },
250
+ async queryObjectIds(request) {
251
+ ensureCapability(descriptor, caps, "queryObjectIds");
252
+ return layer.queryObjectIds(toFeatureLayerRequest(request));
253
+ },
254
+ async applyEdits(envelope) {
255
+ ensureCapability(descriptor, caps, "applyEdits");
256
+ const response = await layer.applyEdits(toApplyEditsRequest(envelope));
257
+ return canonicalEditResult(response);
258
+ },
259
+ async queryRelated(request) {
260
+ ensureCapability(descriptor, caps, "queryRelated");
261
+ const response = await layer.queryRelatedRecords(toRelatedRecordsRequest(request));
262
+ return canonicalRelatedResult(response);
263
+ },
264
+ attachments: featureLayerAttachmentApi(descriptor, caps, layer),
265
+ });
266
+ }
267
+ // ── GeoServices Map Service / Map Layer ───────────────────────
268
+ /**
269
+ * Adapter factory for a GeoServices MapServer layer.
270
+ *
271
+ * Reach `Source.protocol("geoservices-map-service")` for raw `exportImage` /
272
+ * `identify` / `find` access; the canonical `query()` runs against a single
273
+ * sublayer via the MapServer query endpoint.
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * const dataset = createDataset({
278
+ * id: "basemap",
279
+ * client,
280
+ * sources: [{
281
+ * id: "states-mapserver",
282
+ * protocol: "geoservices-map-service",
283
+ * locator: { url: "https://your-honua-server.example", serviceId: "states", layerId: 0 },
284
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-map-service"],
285
+ * }],
286
+ * });
287
+ * ```
288
+ */
289
+ export function geoServicesMapServiceSource(descriptor, client, policy) {
290
+ const { serviceId, layerId } = requireMapServiceLocator(descriptor);
291
+ const service = new HonuaMapService({ client, serviceId });
292
+ const layer = new HonuaMapLayer({ client, serviceId, layerId });
293
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-map-service"];
294
+ const adapterRegistry = {
295
+ "geoservices-map-service": service,
296
+ "geoservices-map-layer": layer,
297
+ };
298
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
299
+ async query(request) {
300
+ ensureCapability(descriptor, caps, "query");
301
+ const params = toFeatureLayerRequest(request);
302
+ if (request?.aggregation) {
303
+ ensureCapability(descriptor, caps, "queryAggregate");
304
+ const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
305
+ return aggregateResultFromUntyped(await layer.queryFeatures({ ...params, ...aggFields }), aggregateAlias);
306
+ }
307
+ const response = await layer.queryFeatures(params);
308
+ return featureLayerResultFromUntyped(response);
309
+ },
310
+ async queryAll(request) {
311
+ ensureCapability(descriptor, caps, "query");
312
+ const params = withPagingBounds(toFeatureLayerRequest(request), request?.pagination?.limit);
313
+ const features = await layer.queryFeaturesAll(params);
314
+ const typed = features.map((toTypedFeature));
315
+ const { features: limited, exceededTransferLimit } = applyQueryAllLimit(typed, request?.pagination?.limit);
316
+ return {
317
+ features: limited,
318
+ exceededTransferLimit,
319
+ totalCount: limited.length,
320
+ };
321
+ },
322
+ async queryAggregate(request) {
323
+ ensureCapability(descriptor, caps, "queryAggregate");
324
+ const params = toFeatureLayerRequest(request);
325
+ const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
326
+ const response = await layer.queryFeatures({ ...params, ...aggFields });
327
+ return aggregateResultFromUntyped(response, aggregateAlias);
328
+ },
329
+ async queryExtent(request) {
330
+ ensureCapability(descriptor, caps, "queryExtent");
331
+ const response = await layer.queryFeatures(toExtentOnlyRequest(toFeatureLayerRequest(request)));
332
+ return extractExtentEnvelope(response);
333
+ },
334
+ async *stream(request) {
335
+ ensureCapability(descriptor, caps, "stream");
336
+ const stream = layer.queryFeaturesStream(withStreamPageSize(toFeatureLayerRequest(request), request?.pagination?.limit));
337
+ for await (const page of stream) {
338
+ yield {
339
+ features: page.map((toTypedFeature)),
340
+ exceededTransferLimit: false,
341
+ };
342
+ }
343
+ },
344
+ async queryObjectIds(request) {
345
+ ensureCapability(descriptor, caps, "queryObjectIds");
346
+ return layer.queryObjectIds(toFeatureLayerRequest(request));
347
+ },
348
+ async applyEdits() {
349
+ // MapServer is read-only; applyEdits exists only on the FeatureServer
350
+ // surface. Refuse rather than silently no-op so callers can branch.
351
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
352
+ },
353
+ async queryRelated(request) {
354
+ ensureCapability(descriptor, caps, "queryRelated");
355
+ const response = await layer.queryRelatedRecords(toRelatedRecordsRequest(request));
356
+ return canonicalRelatedResult(response);
357
+ },
358
+ attachments: unsupportedAttachmentApi(descriptor),
359
+ });
360
+ }
361
+ // ── OGC API Features ──────────────────────────────────────────
362
+ /**
363
+ * Adapter factory for an OGC API Features collection.
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * const dataset = createDataset({
368
+ * id: "addresses",
369
+ * client,
370
+ * sources: [{
371
+ * id: "addresses-ogc",
372
+ * protocol: "ogc-features",
373
+ * locator: { url: "https://your-honua-server.example", collectionId: "addresses" },
374
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["ogc-features"],
375
+ * }],
376
+ * });
377
+ * const result = await dataset.source("addresses-ogc")!.queryAll({
378
+ * where: "city = 'Honolulu'",
379
+ * pagination: { limit: 200 },
380
+ * });
381
+ * ```
382
+ */
383
+ export function ogcFeaturesSource(descriptor, client, policy) {
384
+ const { collectionId } = requireOgcLocator(descriptor);
385
+ const layoutMode = ogcFeaturesLayoutMode(descriptor.locator.layout);
386
+ const collection = new HonuaOgcFeatureCollection({
387
+ client,
388
+ collectionId,
389
+ ...(layoutMode ? { layout: layoutMode } : {}),
390
+ });
391
+ const root = new HonuaOgcFeatures({ client, ...(layoutMode ? { layout: layoutMode } : {}) });
392
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-features"];
393
+ const adapterRegistry = {
394
+ "ogc-features": collection,
395
+ };
396
+ void root;
397
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
398
+ async query(request) {
399
+ ensureCapability(descriptor, caps, "query");
400
+ if (request?.aggregation) {
401
+ // OGC has no server-side aggregation; the adapter always aggregates
402
+ // client-side over the returned page. `queryAggregate` is therefore a
403
+ // degradable capability here.
404
+ ensureCapabilityOrFallback(descriptor, caps, "queryAggregate", policy);
405
+ }
406
+ const response = await collection.items(toOgcRequest(request));
407
+ const features = response.features.map((toTypedFeatureFromOgc));
408
+ const totalCount = response.numberMatched;
409
+ const degraded = [];
410
+ const exceededTransferLimit = totalCount !== undefined && features.length < totalCount;
411
+ if (request?.aggregation) {
412
+ const aggregateRows = clientSideAggregate(features, request.aggregation);
413
+ degraded.push({
414
+ capability: "queryAggregate",
415
+ reason: "OGC API Features does not expose server-side aggregation; aggregating client-side over the returned page.",
416
+ protocol: "ogc-features",
417
+ sourceId: descriptor.id,
418
+ });
419
+ return {
420
+ features,
421
+ exceededTransferLimit,
422
+ totalCount,
423
+ aggregateRows,
424
+ degraded,
425
+ };
426
+ }
427
+ return {
428
+ features,
429
+ exceededTransferLimit,
430
+ totalCount,
431
+ };
432
+ },
433
+ async queryAll(request) {
434
+ ensureCapability(descriptor, caps, "query");
435
+ const limit = request?.pagination?.limit;
436
+ // When the caller caps the result with `pagination.limit`, request
437
+ // `limit + 1` rows from the OGC helper (the lookahead row) so the
438
+ // adapter can stamp `exceededTransferLimit: true` when more records
439
+ // exist. Mirrors the GeoServices `withPagingBounds` + `applyQueryAllLimit`
440
+ // pattern so `queryAll({ pagination: { limit } })` has the same
441
+ // contract across protocols.
442
+ const ogcRequest = toOgcRequest(request);
443
+ if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
444
+ ogcRequest.limit = limit + 1;
445
+ }
446
+ const all = await collection.itemsAll(withUnboundedMaxPages(ogcRequest));
447
+ const typed = all.map((toTypedFeatureFromOgc));
448
+ const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
449
+ return {
450
+ features,
451
+ exceededTransferLimit,
452
+ totalCount: features.length,
453
+ };
454
+ },
455
+ async queryAggregate(request) {
456
+ // Always client-side (OGC has no server-side aggregation). Under `strict`
457
+ // the descriptor must advertise `queryAggregate`; under `degraded` the
458
+ // fallback runs unconditionally.
459
+ ensureCapabilityOrFallback(descriptor, caps, "queryAggregate", policy);
460
+ const all = await collection.itemsAll(withUnboundedMaxPages(toOgcMaterializedRequest(request)));
461
+ const features = all.map((toTypedFeatureFromOgc));
462
+ return {
463
+ features,
464
+ exceededTransferLimit: false,
465
+ totalCount: features.length,
466
+ aggregateRows: clientSideAggregate(features, request.aggregation),
467
+ degraded: [
468
+ {
469
+ capability: "queryAggregate",
470
+ reason: "OGC API Features aggregation evaluated client-side over the materialized result set.",
471
+ protocol: "ogc-features",
472
+ sourceId: descriptor.id,
473
+ },
474
+ ],
475
+ };
476
+ },
477
+ async queryExtent(request) {
478
+ ensureCapabilityOrFallback(descriptor, caps, "queryExtent", policy);
479
+ // Collection-wide extent (no filters): the metadata bbox is accurate
480
+ // and avoids draining items. A filtered request must be computed
481
+ // client-side — the collection bbox would over-report the matching
482
+ // subset's extent. `outSr` also forces the items path so the extent
483
+ // is expressed in the caller's requested CRS; the metadata bbox is
484
+ // frozen in the collection's native CRS (typically CRS84) and would
485
+ // silently mislead `queryExtent({ outSr: 3857 })` callers.
486
+ if (!hasExtentFilter(request) && request?.outSr === undefined) {
487
+ const meta = await collection.metadata();
488
+ const bbox = meta.extent?.spatial?.bbox?.[0];
489
+ if (!bbox || bbox.length < 4)
490
+ return { extent: null };
491
+ const [xmin, ymin, xmax, ymax] = bbox;
492
+ return { extent: { xmin, ymin, xmax, ymax } };
493
+ }
494
+ const all = await collection.itemsAll(withUnboundedMaxPages(toOgcMaterializedRequest(request)));
495
+ return computeExtentFromOgcFeatures(all);
496
+ },
497
+ async *stream(request) {
498
+ ensureCapability(descriptor, caps, "stream");
499
+ const stream = collection.itemsStream(withUnboundedMaxPages(toOgcRequest(request)));
500
+ for await (const page of stream) {
501
+ yield {
502
+ features: page.map((toTypedFeatureFromOgc)),
503
+ exceededTransferLimit: false,
504
+ };
505
+ }
506
+ },
507
+ async queryObjectIds(request) {
508
+ ensureCapability(descriptor, caps, "queryObjectIds");
509
+ // OGC `/items` does not expose a server-side ids-only mode; drain the
510
+ // matching set and project the GeoJSON `id`. Callers that need a
511
+ // bounded scan should pass `pagination.limit`.
512
+ const all = await collection.itemsAll(withUnboundedMaxPages(toOgcRequest(request)));
513
+ const ids = [];
514
+ for (const feature of all) {
515
+ if (feature.id !== undefined && feature.id !== null) {
516
+ ids.push(feature.id);
517
+ }
518
+ }
519
+ return ids;
520
+ },
521
+ async applyEdits(envelope) {
522
+ ensureCapability(descriptor, caps, "applyEdits");
523
+ const added = [];
524
+ const updated = [];
525
+ const deleted = [];
526
+ // OGC has no batch edit endpoint; edits fan out to per-item
527
+ // createItem / replaceItem / deleteItem. The canonical envelope's
528
+ // signal aborts every remaining request as soon as the caller
529
+ // cancels — each item call passes the same signal through.
530
+ const { signal } = envelope;
531
+ for (const add of envelope.adds ?? []) {
532
+ try {
533
+ const created = await collection.createItem({
534
+ feature: featureToGeoJsonFeature(add),
535
+ ...(signal ? { signal } : {}),
536
+ });
537
+ added.push({ id: created.id, success: true });
538
+ }
539
+ catch (err) {
540
+ added.push({ success: false, error: editErrorFromCatch(err) });
541
+ }
542
+ }
543
+ for (const update of envelope.updates ?? []) {
544
+ if (update.id === undefined || update.id === null) {
545
+ updated.push({ success: false, error: { code: 400, description: "update.id is required" } });
546
+ continue;
547
+ }
548
+ try {
549
+ await collection.replaceItem({
550
+ featureId: update.id,
551
+ feature: featureToGeoJsonFeature(update),
552
+ ...(signal ? { signal } : {}),
553
+ });
554
+ updated.push({ id: update.id, success: true });
555
+ }
556
+ catch (err) {
557
+ updated.push({ id: update.id, success: false, error: editErrorFromCatch(err) });
558
+ }
559
+ }
560
+ for (const id of envelope.deletes ?? []) {
561
+ try {
562
+ await collection.deleteItem({ featureId: id, ...(signal ? { signal } : {}) });
563
+ deleted.push({ id, success: true });
564
+ }
565
+ catch (err) {
566
+ deleted.push({ id, success: false, error: editErrorFromCatch(err) });
567
+ }
568
+ }
569
+ return { added, updated, deleted };
570
+ },
571
+ async queryRelated() {
572
+ // OGC API Features has no related-records concept; refuse rather than
573
+ // silently return empty groups.
574
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
575
+ },
576
+ attachments: unsupportedAttachmentApi(descriptor),
577
+ });
578
+ }
579
+ // ── GeoServices Image Service ────────────────────────────────
580
+ /**
581
+ * Build a `Source` over an Esri-style ImageServer endpoint. The query
582
+ * surface drives the raster catalog (each row is a raster with footprint
583
+ * geometry); image export, identify, and tile operations live behind
584
+ * `Source.protocol("geoservices-image-service")`.
585
+ *
586
+ * @example
587
+ * ```ts
588
+ * const dataset = createDataset({
589
+ * id: "imagery",
590
+ * client,
591
+ * sources: [{
592
+ * id: "naip",
593
+ * protocol: "geoservices-image-service",
594
+ * locator: { url: "https://your-honua-server.example", serviceId: "naip" },
595
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES["geoservices-image-service"],
596
+ * }],
597
+ * });
598
+ * const img = dataset.source("naip")!.protocol("geoservices-image-service");
599
+ * const png = await img?.exportImage({ bbox: [-158.5, 21.2, -157.6, 21.7], size: [512, 512] });
600
+ * ```
601
+ */
602
+ export function geoServicesImageSource(descriptor, client, policy) {
603
+ const { serviceId } = requireImageServiceLocator(descriptor);
604
+ const service = new HonuaImageService({ client, serviceId });
605
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-image-service"];
606
+ const adapterRegistry = {
607
+ "geoservices-image-service": service,
608
+ };
609
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
610
+ async query(request) {
611
+ ensureCapability(descriptor, caps, "query");
612
+ requireImageServerCompatibleQuery(request);
613
+ // ImageServer query returns the raster catalog. The GeoServices
614
+ // request shape is identical to FeatureServer query so we reuse
615
+ // `toFeatureLayerRequest` and dispatch through the service.
616
+ const response = await service.queryRasterCatalog(toFeatureLayerRequest(request));
617
+ return featureLayerResultFromUntyped(response);
618
+ },
619
+ async queryAll(request) {
620
+ ensureCapability(descriptor, caps, "query");
621
+ requireImageServerCompatibleQuery(request);
622
+ const baseParams = toFeatureLayerRequest(request);
623
+ const limit = request?.pagination?.limit;
624
+ // ImageServer's catalog endpoint supports `resultOffset`/`resultRecordCount`
625
+ // but the wrapper does not expose a `queryAllRasterCatalog` helper, so
626
+ // drain pages here. Mirror `withPagingBounds` + `applyQueryAllLimit` so
627
+ // an empty/short page or a `limit + 1` lookahead row stops the loop and
628
+ // stamps `exceededTransferLimit` correctly.
629
+ const startingOffset = typeof baseParams.resultOffset === "number" ? baseParams.resultOffset : 0;
630
+ const pageSize = typeof limit === "number" && limit > 0
631
+ ? Math.max(1, Math.min(limit, 2000))
632
+ : typeof baseParams.resultRecordCount === "number" && baseParams.resultRecordCount > 0
633
+ ? baseParams.resultRecordCount
634
+ : 2000;
635
+ const collected = [];
636
+ let lastFields;
637
+ const target = typeof limit === "number" && limit >= 0 ? limit + 1 : Number.POSITIVE_INFINITY;
638
+ for (let page = 0;; page += 1) {
639
+ const response = await service.queryRasterCatalog({
640
+ ...baseParams,
641
+ resultOffset: startingOffset + page * pageSize,
642
+ resultRecordCount: pageSize,
643
+ });
644
+ if (response.fields)
645
+ lastFields = response.fields;
646
+ const pageFeatures = response.features ?? [];
647
+ for (const feature of pageFeatures) {
648
+ collected.push(feature);
649
+ if (collected.length >= target)
650
+ break;
651
+ }
652
+ if (collected.length >= target)
653
+ break;
654
+ if (pageFeatures.length < pageSize)
655
+ break;
656
+ }
657
+ const typed = collected.map((toTypedFeature));
658
+ const { features: bounded, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
659
+ return {
660
+ features: bounded,
661
+ exceededTransferLimit,
662
+ totalCount: bounded.length,
663
+ ...(lastFields ? { fields: lastFields } : {}),
664
+ };
665
+ },
666
+ async queryAggregate() {
667
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
668
+ },
669
+ async queryExtent(request) {
670
+ ensureCapability(descriptor, caps, "queryExtent");
671
+ requireImageServerCompatibleQuery(request);
672
+ const response = await service.queryRasterCatalog(toExtentOnlyRequest(toFeatureLayerRequest(request)));
673
+ return extractExtentEnvelope(response);
674
+ },
675
+ // biome-ignore lint/correctness/useYield: ImageServer has no streaming raster-catalog mode; this generator refuses iteration rather than silently emit a single page
676
+ async *stream() {
677
+ throw new HonuaCapabilityNotSupportedError("stream", descriptor.protocol, descriptor.id);
678
+ },
679
+ async queryObjectIds(request) {
680
+ ensureCapability(descriptor, caps, "queryObjectIds");
681
+ requireImageServerCompatibleQuery(request);
682
+ return service.queryRasterCatalogObjectIds(toFeatureLayerRequest(request));
683
+ },
684
+ async applyEdits() {
685
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
686
+ },
687
+ async queryRelated() {
688
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
689
+ },
690
+ attachments: unsupportedAttachmentApi(descriptor),
691
+ });
692
+ }
693
+ // ── GeoServices Geometry Service ─────────────────────────────
694
+ /**
695
+ * Build a `Source` over an Esri-style Geometry Service endpoint. Geometry
696
+ * Service is a stateless utility — it does not host features — so the
697
+ * query family throws `HonuaCapabilityNotSupportedError` and operations
698
+ * (buffer, project, simplify, etc.) live behind
699
+ * `Source.protocol("geoservices-geometry-service")`.
700
+ *
701
+ * @example
702
+ * ```ts
703
+ * const geom = dataset.source("geom")!.protocol("geoservices-geometry-service");
704
+ * const projected = await geom?.project({
705
+ * inSr: 4326,
706
+ * outSr: 3857,
707
+ * geometries: [{ x: -158, y: 21 }],
708
+ * });
709
+ * ```
710
+ */
711
+ export function geoServicesGeometryServiceSource(descriptor, client, policy) {
712
+ const service = new HonuaGeometryService({ client });
713
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-geometry-service"];
714
+ const adapterRegistry = {
715
+ "geoservices-geometry-service": service,
716
+ };
717
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
718
+ }
719
+ // ── GeoServices GP Service ───────────────────────────────────
720
+ /**
721
+ * Build a `Source` over an Esri-style GP (geoprocessing) service. GP
722
+ * services run async tasks rather than hosting features; the canonical
723
+ * feature surface throws and task submission / status / result lookup
724
+ * live behind `Source.protocol("geoservices-gp-service")`.
725
+ *
726
+ * @example
727
+ * ```ts
728
+ * const gp = dataset.source("buffer-task")!.protocol("geoservices-gp-service");
729
+ * const job = await gp!.submitJob({ inputs: { distance: 500 } });
730
+ * const result = await gp!.awaitJobResult(job.jobId);
731
+ * ```
732
+ */
733
+ export function geoServicesGPServiceSource(descriptor, client, policy) {
734
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-gp-service"];
735
+ const { serviceId, taskName } = requireGPServiceLocator(descriptor, caps);
736
+ const service = new HonuaGeoprocessingService({
737
+ client,
738
+ serviceId,
739
+ taskName,
740
+ });
741
+ const adapterRegistry = {
742
+ "geoservices-gp-service": service,
743
+ };
744
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
745
+ }
746
+ // ── OGC API Tiles ─────────────────────────────────────────────
747
+ /**
748
+ * Render-only Source adapter for OGC API Tiles. The query family throws
749
+ * `HonuaCapabilityNotSupportedError` (the conformance class is
750
+ * tile-fetch, not feature-query); rendering integrations consume the
751
+ * tileset through `Source.protocol("ogc-tiles")` to reach the underlying
752
+ * runtime class.
753
+ *
754
+ * @example
755
+ * ```ts
756
+ * const tiles = dataset.source("ogc-tileset")!.protocol("ogc-tiles");
757
+ * const bytes = await tiles!.tile({ tileMatrix: "WebMercatorQuad", tileRow: 5, tileCol: 8 });
758
+ * ```
759
+ */
760
+ export function ogcTilesSource(descriptor, client, policy) {
761
+ const { collectionId, tileMatrixSetId } = requireOgcTilesLocator(descriptor);
762
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-tiles"];
763
+ // Descriptors without a tileMatrixSetId cannot construct a usable
764
+ // HonuaOgcTileset (every tile route requires `tileMatrixSetId`). Expose
765
+ // the root HonuaOgcTiles adapter instead so callers can discover the
766
+ // tilesets the server advertises for the collection before binding one.
767
+ const adapter = tileMatrixSetId !== undefined && tileMatrixSetId !== ""
768
+ ? new HonuaOgcTileset({ client, collectionId, tileMatrixSetId })
769
+ : new HonuaOgcTiles({ client });
770
+ const adapterRegistry = {
771
+ "ogc-tiles": adapter,
772
+ };
773
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
774
+ }
775
+ // ── PMTiles archive ───────────────────────────────────────────
776
+ /**
777
+ * Tiles-only Source adapter for a PMTiles archive (`protocol: "pmtiles"`).
778
+ *
779
+ * PMTiles archives are immutable single-file tile stores, so the canonical
780
+ * query family throws `HonuaCapabilityNotSupportedError` (there is no
781
+ * feature-query surface on an archive). Archive metadata — bounds, min/max
782
+ * zoom, and vector layer names — is inspected through the typed escape hatch:
783
+ * `Source.protocol("pmtiles").describe()`. Rendering integrations register the
784
+ * `pmtiles://` protocol on the map (see `@honua/sdk-js/runtime`) and reference
785
+ * the archive as a MapLibre source `url`.
786
+ *
787
+ * `locator.url` may carry a leading `pmtiles://` scheme (the MapLibre form) or
788
+ * be a bare archive URL; both resolve to the same archive.
789
+ *
790
+ * @example
791
+ * ```ts
792
+ * const archive = dataset.source("basemap")!.protocol("pmtiles");
793
+ * const info = await archive!.describe();
794
+ * console.log(info.bounds, info.vectorLayers.map((layer) => layer.id));
795
+ * ```
796
+ */
797
+ export function pmtilesSource(descriptor, _client, policy) {
798
+ const url = stripPmtilesScheme(requirePmtilesLocator(descriptor));
799
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.pmtiles;
800
+ const archive = new HonuaPmtilesArchive(url);
801
+ const adapterRegistry = {
802
+ pmtiles: archive,
803
+ };
804
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
805
+ }
806
+ // ── OGC API Maps ──────────────────────────────────────────────
807
+ /**
808
+ * Render-only Source adapter for OGC API Maps. Same shape as the Tiles
809
+ * adapter — `Source.protocol("ogc-maps")` exposes the runtime class for
810
+ * server-rendered map images; the canonical query family throws.
811
+ *
812
+ * @example
813
+ * ```ts
814
+ * const maps = dataset.source("ogc-map")!.protocol("ogc-maps");
815
+ * const png = await maps!.map({ bbox: [-158, 21, -157, 22], width: 512, height: 512 });
816
+ * ```
817
+ */
818
+ export function ogcMapsSource(descriptor, client, policy) {
819
+ const root = new HonuaOgcMaps({ client });
820
+ const adapterRegistry = {};
821
+ if (descriptor.locator.collectionId !== undefined) {
822
+ adapterRegistry["ogc-maps"] = new HonuaOgcCollectionMap({
823
+ client,
824
+ collectionId: descriptor.locator.collectionId,
825
+ styleId: descriptor.locator.styleId,
826
+ });
827
+ }
828
+ else {
829
+ adapterRegistry["ogc-maps"] = root;
830
+ }
831
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-maps"];
832
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
833
+ }
834
+ // ── OGC API Records ───────────────────────────────────────────
835
+ /**
836
+ * Metadata-catalog Source adapter for OGC API Records. The canonical query
837
+ * family searches one catalog (`locator.collectionId`, the Records
838
+ * collection/catalog id) and returns record documents as typed features.
839
+ * Records-specific search affordances (`q`, `type`, `externalIds`,
840
+ * `profile`, raw HTML/JSON access) live on `Source.protocol("ogc-records")`.
841
+ *
842
+ * @example
843
+ * ```ts
844
+ * const records = dataset.source("catalog")!;
845
+ * const result = await records.query({ where: "type = 'dataset' AND q = 'parcels'" });
846
+ * ```
847
+ */
848
+ export function ogcRecordsSource(descriptor, client, policy) {
849
+ const { collectionId } = requireOgcRecordsLocator(descriptor);
850
+ const collection = new HonuaOgcRecordCollection({ client, collectionId });
851
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-records"];
852
+ const adapterRegistry = {
853
+ "ogc-records": collection,
854
+ };
855
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
856
+ async query(request) {
857
+ ensureCapability(descriptor, caps, "query");
858
+ if (request?.aggregation) {
859
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
860
+ }
861
+ const response = await collection.search(toOgcRecordsRequest(request));
862
+ const features = (response.features ?? []).map((toTypedFeatureFromOgcRecord));
863
+ const totalCount = response.numberMatched;
864
+ return {
865
+ features,
866
+ exceededTransferLimit: totalCount !== undefined && features.length < totalCount,
867
+ ...(totalCount !== undefined ? { totalCount } : {}),
868
+ };
869
+ },
870
+ async queryAll(request) {
871
+ ensureCapability(descriptor, caps, "query");
872
+ const limit = request?.pagination?.limit;
873
+ const records = await collection.searchAll({
874
+ ...toOgcRecordsRequest(request),
875
+ ...withPagingBounds({}, limit),
876
+ });
877
+ const typed = records.map((toTypedFeatureFromOgcRecord));
878
+ const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
879
+ return {
880
+ features,
881
+ exceededTransferLimit,
882
+ totalCount: features.length,
883
+ };
884
+ },
885
+ async queryAggregate() {
886
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
887
+ },
888
+ async queryExtent() {
889
+ throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
890
+ },
891
+ async *stream(request) {
892
+ ensureCapability(descriptor, caps, "stream");
893
+ const limit = request?.pagination?.limit;
894
+ const stream = collection.searchStream({
895
+ ...toOgcRecordsRequest(request),
896
+ pageSize: limit !== undefined ? Math.max(1, limit) : undefined,
897
+ maxPages: Number.MAX_SAFE_INTEGER,
898
+ });
899
+ for await (const page of stream) {
900
+ yield {
901
+ features: page.map((toTypedFeatureFromOgcRecord)),
902
+ exceededTransferLimit: false,
903
+ };
904
+ }
905
+ },
906
+ async queryObjectIds(request) {
907
+ ensureCapability(descriptor, caps, "queryObjectIds");
908
+ const limit = request?.pagination?.limit;
909
+ const records = await collection.searchAll({
910
+ ...toOgcRecordsRequest(request),
911
+ ...withPagingBounds({}, limit),
912
+ });
913
+ const ids = [];
914
+ for (const record of records) {
915
+ if (record.id !== undefined && record.id !== null) {
916
+ ids.push(record.id);
917
+ }
918
+ }
919
+ if (typeof limit === "number" && limit >= 0 && ids.length > limit) {
920
+ return ids.slice(0, limit);
921
+ }
922
+ return ids;
923
+ },
924
+ async applyEdits() {
925
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
926
+ },
927
+ async queryRelated() {
928
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
929
+ },
930
+ attachments: unsupportedAttachmentApi(descriptor),
931
+ });
932
+ }
933
+ // ── WMS 1.3 ───────────────────────────────────────────────────
934
+ /**
935
+ * `Source` adapter for first-party WMS 1.3.0 services. The render path
936
+ * routes through `HonuaWms` / `HonuaWmsLayer` (both of which sit
937
+ * behind the canonical `Source.protocol("wms" | "wms-layer")` escape
938
+ * hatch); `Source.query()` translates a point spatial filter into a
939
+ * `GetFeatureInfo` call. Non-point queries throw
940
+ * `HonuaCapabilityNotSupportedError` so the canonical query envelope
941
+ * stays honest — multi-pixel feature info lives on
942
+ * `Source.protocol("wms").featureInfo()`.
943
+ */
944
+ /**
945
+ * Adapter factory for a WMS 1.3.0 endpoint.
946
+ *
947
+ * The canonical `Source.query()` is implemented through point-only
948
+ * `GetFeatureInfo`. Reach `Source.protocol("wms")` for raw `GetMap` /
949
+ * `GetLegendGraphic` access (with TIME / ELEVATION dimension handling and
950
+ * CRS axis-order swap per WMS 1.3 §6.7.3.2).
951
+ *
952
+ * @example
953
+ * ```ts
954
+ * const wms = dataset.source("usgs-imagery")!.protocol("wms");
955
+ * const png = await wms!.getMap({
956
+ * layers: ["topo"], bbox: [-158, 21, -157, 22], crs: "EPSG:4326",
957
+ * width: 512, height: 512,
958
+ * });
959
+ * ```
960
+ */
961
+ export function wmsSource(descriptor, client, policy) {
962
+ const { serviceId } = requireWmsLocator(descriptor);
963
+ const layerName = descriptor.locator.typeName;
964
+ const styleId = descriptor.locator.styleId;
965
+ const root = new HonuaWms({ client, serviceId });
966
+ const adapterRegistry = { wms: root };
967
+ // `HonuaWmsLayer` is a single-layer handle (its `describe()` resolves
968
+ // exactly one `<Layer>` from the parsed Capabilities). Multi-layer
969
+ // composites (`LAYERS=a,b`) must stay on the service-level `wms`
970
+ // handle and use `featureInfo()` directly; registering them as a
971
+ // `wms-layer` would silently mis-route `describe()` / `stylesIn()` /
972
+ // `legend()` to a single layer name that does not exist on the wire.
973
+ const parsedLayers = parseWmsLayerNames(layerName);
974
+ if (parsedLayers.length === 1) {
975
+ const layerOpts = {
976
+ client,
977
+ serviceId,
978
+ layerName: parsedLayers[0],
979
+ };
980
+ if (typeof styleId === "string")
981
+ layerOpts.defaultStyleId = styleId;
982
+ adapterRegistry["wms-layer"] = new HonuaWmsLayer(layerOpts);
983
+ }
984
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wms;
985
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
986
+ async query(request) {
987
+ ensureCapability(descriptor, caps, "query");
988
+ requireWmsCompatibleQuery(descriptor, request);
989
+ const layers = wmsRequireLayers(descriptor, layerName);
990
+ const { x: px, y: py, crs } = wmsExtractPointFromQuery(request, descriptor);
991
+ const bboxRadius = 0.0001; // tiny envelope around the point keeps the request a 1×1 image.
992
+ const widthHeight = 1;
993
+ const featureCount = request?.pagination?.limit;
994
+ const response = await client.getWmsFeatureInfo({
995
+ serviceId,
996
+ layers,
997
+ ...(styleId !== undefined ? { styles: [styleId] } : {}),
998
+ queryLayers: layers,
999
+ crs,
1000
+ bbox: [px - bboxRadius, py - bboxRadius, px + bboxRadius, py + bboxRadius],
1001
+ width: widthHeight,
1002
+ height: widthHeight,
1003
+ i: 0,
1004
+ j: 0,
1005
+ infoFormat: "application/json",
1006
+ ...(featureCount !== undefined ? { featureCount } : {}),
1007
+ ...(request?.signal ? { signal: request.signal } : {}),
1008
+ });
1009
+ return {
1010
+ features: response.features ?? [],
1011
+ exceededTransferLimit: false,
1012
+ };
1013
+ },
1014
+ async queryAll(request) {
1015
+ ensureCapability(descriptor, caps, "query");
1016
+ // GetFeatureInfo returns a single response set; queryAll degenerates
1017
+ // to query() because there is no paging on the wire.
1018
+ const result = await this.query(request);
1019
+ return result;
1020
+ },
1021
+ async queryAggregate() {
1022
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1023
+ },
1024
+ async queryExtent() {
1025
+ throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
1026
+ },
1027
+ // biome-ignore lint/correctness/useYield: WMS exposes only single-shot GetFeatureInfo, which is delivered through query()
1028
+ async *stream() {
1029
+ throw new HonuaCapabilityNotSupportedError("stream", descriptor.protocol, descriptor.id);
1030
+ },
1031
+ async queryObjectIds() {
1032
+ throw new HonuaCapabilityNotSupportedError("queryObjectIds", descriptor.protocol, descriptor.id);
1033
+ },
1034
+ async applyEdits() {
1035
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
1036
+ },
1037
+ async queryRelated() {
1038
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
1039
+ },
1040
+ attachments: unsupportedAttachmentApi(descriptor),
1041
+ });
1042
+ }
1043
+ // ── WMTS 1.0 ──────────────────────────────────────────────────
1044
+ /**
1045
+ * Render-only `Source` adapter for first-party WMTS 1.0.0 services.
1046
+ * `Source.query()` throws because WMTS GetFeatureInfo is keyed on tile
1047
+ * pixels (not a canonical spatial filter); raw access lives on
1048
+ * `Source.protocol("wmts" | "wmts-layer" | "wmts-tileset")`.
1049
+ */
1050
+ /**
1051
+ * Adapter factory for a WMTS 1.0.0 endpoint. Render-only; `Source.query()`
1052
+ * throws `HonuaCapabilityNotSupportedError`. Reach `Source.protocol("wmts")`
1053
+ * for RESTful + KVP tile fetch and capabilities.
1054
+ *
1055
+ * @example
1056
+ * ```ts
1057
+ * const wmts = dataset.source("basemap-tiles")!.protocol("wmts");
1058
+ * const tile = await wmts!.tile({ tileMatrixSet: "WebMercatorQuad", tileMatrix: "8", tileRow: 5, tileCol: 8 });
1059
+ * ```
1060
+ */
1061
+ export function wmtsSource(descriptor, client, policy) {
1062
+ const { serviceId } = requireWmtsLocator(descriptor);
1063
+ const layerName = descriptor.locator.typeName;
1064
+ const tileMatrixSetId = descriptor.locator.tileMatrixSetId ?? "WebMercatorQuad";
1065
+ const styleId = descriptor.locator.styleId ?? "default";
1066
+ const root = new HonuaWmts({ client, serviceId });
1067
+ const adapterRegistry = { wmts: root };
1068
+ if (typeof layerName === "string" && layerName.length > 0) {
1069
+ adapterRegistry["wmts-layer"] = new HonuaWmtsLayer({
1070
+ client,
1071
+ serviceId,
1072
+ layerName,
1073
+ defaultStyleId: styleId,
1074
+ defaultTileMatrixSetId: tileMatrixSetId,
1075
+ });
1076
+ adapterRegistry["wmts-tileset"] = new HonuaWmtsTileset({
1077
+ client,
1078
+ serviceId,
1079
+ layerName,
1080
+ styleId,
1081
+ tileMatrixSetId,
1082
+ });
1083
+ }
1084
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wmts;
1085
+ return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
1086
+ }
1087
+ // ── STAC API ──────────────────────────────────────────────────
1088
+ /**
1089
+ * Adapter factory for a STAC API search endpoint.
1090
+ *
1091
+ * The canonical `Source.query()` runs a `POST /search` against the STAC root,
1092
+ * with `Query.spatialFilter` (e.g. an `envelope(...)` bounding box) and
1093
+ * `Query.where` translated into STAC's `bbox` / `datetime` / `filter`
1094
+ * parameters.
1095
+ *
1096
+ * @example
1097
+ * ```ts
1098
+ * import { envelope } from "@honua/sdk-js";
1099
+ *
1100
+ * const dataset = createDataset({
1101
+ * id: "imagery",
1102
+ * client,
1103
+ * sources: [{
1104
+ * id: "stac-search",
1105
+ * protocol: "stac",
1106
+ * locator: { url: "https://your-honua-server.example/stac" },
1107
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES.stac,
1108
+ * }],
1109
+ * });
1110
+ * const result = await dataset.source("stac-search")!.query({
1111
+ * where: "collections IN ('landsat-c2-l2')",
1112
+ * spatialFilter: envelope(-158.5, 21.2, -157.6, 21.7),
1113
+ * });
1114
+ * ```
1115
+ */
1116
+ export function stacSearchSource(descriptor, client, policy) {
1117
+ // Endpoint-layout selection. `stac-api` treats `locator.url` as a raw STAC
1118
+ // API root (search / collections mounted directly under it) rather than the
1119
+ // Honua `/stac` facade. `stac-static` reads a static catalog.json tree with
1120
+ // no search endpoint. Omitted / `honua-facade` keeps the `/stac` facade.
1121
+ const layout = descriptor.locator.layout;
1122
+ const stacBasePath = layout === "stac-api" ? "" : undefined;
1123
+ const stac = new HonuaStacSearch({ client, ...(stacBasePath !== undefined ? { basePath: stacBasePath } : {}) });
1124
+ const staticCatalog = layout === "stac-static" ? new HonuaStacStaticCatalog(client, client.serverBaseUrl) : undefined;
1125
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.stac;
1126
+ const collectionScope = descriptor.locator.collectionId;
1127
+ const adapterRegistry = {
1128
+ stac,
1129
+ };
1130
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
1131
+ async query(request) {
1132
+ ensureCapability(descriptor, caps, "query");
1133
+ if (request?.aggregation) {
1134
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1135
+ }
1136
+ const stacRequest = toStacRequest(request, collectionScope);
1137
+ const response = staticCatalog
1138
+ ? await staticCatalog.search(toStacStaticParams(stacRequest))
1139
+ : await stac.search(stacRequest);
1140
+ const features = (response.features ?? []).map((toTypedFeatureFromStac));
1141
+ const totalCount = response.numberMatched ?? response.context?.matched;
1142
+ const exceededTransferLimit = totalCount !== undefined && features.length < totalCount;
1143
+ return {
1144
+ features,
1145
+ exceededTransferLimit,
1146
+ totalCount,
1147
+ };
1148
+ },
1149
+ async queryAll(request) {
1150
+ ensureCapability(descriptor, caps, "query");
1151
+ const limit = request?.pagination?.limit;
1152
+ if (staticCatalog) {
1153
+ const response = await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope)));
1154
+ const typed = (response.features ?? []).map((toTypedFeatureFromStac));
1155
+ const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
1156
+ return { features, exceededTransferLimit, totalCount: features.length };
1157
+ }
1158
+ const items = await stac.searchAll({
1159
+ ...toStacRequest(request, collectionScope),
1160
+ ...withPagingBounds({}, limit),
1161
+ });
1162
+ const typed = items.map((toTypedFeatureFromStac));
1163
+ const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
1164
+ return {
1165
+ features,
1166
+ exceededTransferLimit,
1167
+ totalCount: features.length,
1168
+ };
1169
+ },
1170
+ async queryAggregate() {
1171
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1172
+ },
1173
+ async queryExtent() {
1174
+ throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
1175
+ },
1176
+ async *stream(request) {
1177
+ ensureCapability(descriptor, caps, "stream");
1178
+ const limit = request?.pagination?.limit;
1179
+ if (staticCatalog) {
1180
+ // A static catalog has no server-side paging; yield the whole
1181
+ // filtered set as one page.
1182
+ const response = await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope)));
1183
+ yield {
1184
+ features: (response.features ?? []).map((toTypedFeatureFromStac)),
1185
+ exceededTransferLimit: false,
1186
+ };
1187
+ return;
1188
+ }
1189
+ const stream = stac.searchStream({
1190
+ ...toStacRequest(request, collectionScope),
1191
+ pageSize: limit !== undefined ? Math.max(1, limit) : undefined,
1192
+ maxPages: Number.MAX_SAFE_INTEGER,
1193
+ });
1194
+ for await (const page of stream) {
1195
+ yield {
1196
+ features: page.map((toTypedFeatureFromStac)),
1197
+ exceededTransferLimit: false,
1198
+ };
1199
+ }
1200
+ },
1201
+ async queryObjectIds(request) {
1202
+ ensureCapability(descriptor, caps, "queryObjectIds");
1203
+ // STAC `/search` does not expose a server-side ids-only mode; drain
1204
+ // the matching items and project the GeoJSON `id`. `pagination.limit`
1205
+ // routes through `withPagingBounds` so the underlying searchAll
1206
+ // fetches at most `limit + 1` rows, never a full-catalog scan.
1207
+ const limit = request?.pagination?.limit;
1208
+ const items = staticCatalog
1209
+ ? ((await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope)))).features ?? [])
1210
+ : await stac.searchAll({
1211
+ ...toStacRequest(request, collectionScope),
1212
+ ...withPagingBounds({}, limit),
1213
+ });
1214
+ const ids = [];
1215
+ for (const item of items) {
1216
+ if (item.id !== undefined && item.id !== null) {
1217
+ ids.push(item.id);
1218
+ }
1219
+ }
1220
+ if (typeof limit === "number" && limit >= 0 && ids.length > limit) {
1221
+ return ids.slice(0, limit);
1222
+ }
1223
+ return ids;
1224
+ },
1225
+ async applyEdits() {
1226
+ throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
1227
+ },
1228
+ async queryRelated() {
1229
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
1230
+ },
1231
+ attachments: unsupportedAttachmentApi(descriptor),
1232
+ });
1233
+ }
1234
+ // ── WFS 2.0 ───────────────────────────────────────────────────
1235
+ /**
1236
+ * Threshold above which the WFS adapter switches to POST GetFeature with a
1237
+ * `<fes:Filter>` body. URL budget under 7000 chars stays GET-friendly; longer
1238
+ * filters are routed through POST so we do not stress middleboxes that
1239
+ * trim long query strings. The threshold is intentionally a single
1240
+ * constant we can revise after telemetry lands.
1241
+ */
1242
+ const WFS_GET_FILTER_BUDGET = 7000;
1243
+ const DEFAULT_WFS_GEOMETRY_PROPERTY = "the_geom";
1244
+ /**
1245
+ * Adapter factory for a WFS 2.0 endpoint.
1246
+ *
1247
+ * `Query.where` and `Query.spatialFilter` compile to FES 2.0; GeoJSON is
1248
+ * preferred over GML via `OperationsMetadata` negotiation. `applyEdits()`
1249
+ * builds `<wfs:Transaction>` bodies. Reach `Source.protocol("wfs")` for raw
1250
+ * GML / `LockFeature` / stored-query access.
1251
+ *
1252
+ * @example
1253
+ * ```ts
1254
+ * const dataset = createDataset({
1255
+ * id: "wfs-parcels",
1256
+ * client,
1257
+ * sources: [{
1258
+ * id: "parcels",
1259
+ * protocol: "wfs",
1260
+ * locator: { url: "https://your-honua-server.example/wfs", typeName: "ns:Parcels" },
1261
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES.wfs,
1262
+ * }],
1263
+ * });
1264
+ * const result = await dataset.source("parcels")!.queryAll({
1265
+ * where: "STATUS = 'ACTIVE'",
1266
+ * pagination: { limit: 500 },
1267
+ * });
1268
+ * ```
1269
+ */
1270
+ export function wfsSource(descriptor, client, policy) {
1271
+ const { url, typeName, featureNamespace, srsName: wfsSrsName } = requireWfsLocator(descriptor);
1272
+ const root = new HonuaWfs({ client, endpointUrl: url });
1273
+ const featureType = new HonuaWfsFeatureType({ root, typeName });
1274
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wfs;
1275
+ void policy;
1276
+ const adapterRegistry = {
1277
+ wfs: featureType,
1278
+ };
1279
+ /**
1280
+ * Negotiated output format cache. Populated lazily on the first
1281
+ * capability-gated call so a no-op `createDataset({ sources: [...wfs] })`
1282
+ * does not issue network traffic.
1283
+ */
1284
+ let cachedFormat;
1285
+ async function negotiateJsonOrThrow() {
1286
+ if (cachedFormat)
1287
+ return cachedFormat;
1288
+ const snapshot = await root.capabilities();
1289
+ const choice = root.negotiateOutputFormat(snapshot);
1290
+ if (!choice) {
1291
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
1292
+ }
1293
+ if (choice.kind !== "json") {
1294
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, `${descriptor.id} (server advertises only ${choice.format}; reach raw GML through Source.protocol("wfs"))`);
1295
+ }
1296
+ cachedFormat = choice;
1297
+ return choice;
1298
+ }
1299
+ return makeSource(descriptor, caps, policy, adapterRegistry, {
1300
+ async query(request) {
1301
+ ensureCapability(descriptor, caps, "query");
1302
+ if (request?.aggregation) {
1303
+ // WFS 2.0 has no server-side aggregation; refuse rather than ship a
1304
+ // silent partial result.
1305
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1306
+ }
1307
+ // `pagination.limit === 0` is an explicit zero cap, not "unbounded";
1308
+ // short-circuit before the wire call so we do not silently widen to the
1309
+ // server's default page size.
1310
+ if (request?.pagination?.limit === 0) {
1311
+ return { features: [], exceededTransferLimit: false };
1312
+ }
1313
+ const choice = await negotiateJsonOrThrow();
1314
+ const json = await runGetFeatureJson(featureType, typeName, choice, request, request?.pagination?.limit);
1315
+ return resultFromGeoJson(json);
1316
+ },
1317
+ async queryAll(request) {
1318
+ ensureCapability(descriptor, caps, "query");
1319
+ const choice = await negotiateJsonOrThrow();
1320
+ const limit = request?.pagination?.limit;
1321
+ const target = typeof limit === "number" && limit >= 0 ? limit + 1 : Number.POSITIVE_INFINITY;
1322
+ const collected = [];
1323
+ let totalCount;
1324
+ let offset = request?.pagination?.offset ?? 0;
1325
+ // Mirror `withPagingBounds`: a finite limit (including 0) sets the
1326
+ // lookahead page size to `limit + 1` so we can stamp
1327
+ // `exceededTransferLimit` without overfetching a full default page.
1328
+ const pageSize = typeof limit === "number" && limit >= 0 ? Math.max(1, Math.min(limit + 1, 2000)) : 2000;
1329
+ while (collected.length < target) {
1330
+ const pageRequest = {
1331
+ ...(request ?? {}),
1332
+ pagination: { offset, limit: pageSize },
1333
+ };
1334
+ const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, pageSize);
1335
+ const result = resultFromGeoJson(json);
1336
+ if (result.totalCount !== undefined)
1337
+ totalCount = result.totalCount;
1338
+ collected.push(...result.features);
1339
+ if (result.features.length < pageSize)
1340
+ break;
1341
+ offset += result.features.length;
1342
+ }
1343
+ const { features, exceededTransferLimit } = applyQueryAllLimit(collected, limit);
1344
+ return {
1345
+ features,
1346
+ exceededTransferLimit,
1347
+ totalCount: totalCount ?? features.length,
1348
+ };
1349
+ },
1350
+ async queryAggregate() {
1351
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1352
+ },
1353
+ async queryExtent(request) {
1354
+ ensureCapability(descriptor, caps, "queryExtent");
1355
+ // Unfiltered queryExtent: prefer the per-feature-type WGS84BoundingBox
1356
+ // from GetCapabilities so we avoid an extra HTTP request entirely.
1357
+ if (!hasExtentFilter(request) && request?.outSr === undefined) {
1358
+ const snapshot = await root.capabilities();
1359
+ const ft = snapshot.featureTypes.find((entry) => entry.name === typeName);
1360
+ if (ft?.wgs84BoundingBox) {
1361
+ return { extent: { ...ft.wgs84BoundingBox } };
1362
+ }
1363
+ }
1364
+ // Filtered request: drain every matching page so the returned extent
1365
+ // covers all features the filter resolves to, not just the first
1366
+ // server-default page. Caller pagination is intentionally ignored —
1367
+ // queryExtent is a "what bbox holds the matching set" question.
1368
+ const choice = await negotiateJsonOrThrow();
1369
+ const drainPageSize = 2000;
1370
+ const extentRequest = toWfsExtentDrainRequest(request);
1371
+ let xmin = Number.POSITIVE_INFINITY;
1372
+ let ymin = Number.POSITIVE_INFINITY;
1373
+ let xmax = Number.NEGATIVE_INFINITY;
1374
+ let ymax = Number.NEGATIVE_INFINITY;
1375
+ let saw = false;
1376
+ let count = 0;
1377
+ let offset = 0;
1378
+ while (true) {
1379
+ const pageRequest = {
1380
+ ...extentRequest,
1381
+ pagination: { offset, limit: drainPageSize },
1382
+ };
1383
+ const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, drainPageSize);
1384
+ const page = computeExtentFromFeatureCollection(json);
1385
+ count += page.count;
1386
+ if (page.extent) {
1387
+ if (page.extent.xmin < xmin)
1388
+ xmin = page.extent.xmin;
1389
+ if (page.extent.ymin < ymin)
1390
+ ymin = page.extent.ymin;
1391
+ if (page.extent.xmax > xmax)
1392
+ xmax = page.extent.xmax;
1393
+ if (page.extent.ymax > ymax)
1394
+ ymax = page.extent.ymax;
1395
+ saw = true;
1396
+ }
1397
+ if (page.count < drainPageSize)
1398
+ break;
1399
+ offset += page.count;
1400
+ }
1401
+ return saw ? { extent: { xmin, ymin, xmax, ymax }, count } : { extent: null, count };
1402
+ },
1403
+ async *stream(request) {
1404
+ ensureCapability(descriptor, caps, "stream");
1405
+ // WFS 2.0 has no native streaming verb; iterate pages over GetFeature
1406
+ // and yield each as a Result. Reuses the same negotiation as `query`.
1407
+ const limit = request?.pagination?.limit;
1408
+ // `pagination.limit === 0` is an explicit zero cap; yield nothing
1409
+ // rather than silently fall back to the default 2000-row page size.
1410
+ if (limit === 0)
1411
+ return;
1412
+ const choice = await negotiateJsonOrThrow();
1413
+ const pageSize = typeof limit === "number" && limit > 0 ? Math.max(1, limit) : 2000;
1414
+ let offset = request?.pagination?.offset ?? 0;
1415
+ while (true) {
1416
+ const pageRequest = {
1417
+ ...(request ?? {}),
1418
+ pagination: { offset, limit: pageSize },
1419
+ };
1420
+ const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, pageSize);
1421
+ const result = resultFromGeoJson(json);
1422
+ if (result.features.length === 0)
1423
+ break;
1424
+ yield {
1425
+ features: result.features,
1426
+ exceededTransferLimit: false,
1427
+ };
1428
+ if (result.features.length < pageSize)
1429
+ break;
1430
+ offset += result.features.length;
1431
+ }
1432
+ },
1433
+ async queryObjectIds(request) {
1434
+ ensureCapability(descriptor, caps, "queryObjectIds");
1435
+ // WFS does not expose a server-side ids-only mode that interoperates
1436
+ // across implementations; drain the matching set across pages and
1437
+ // project the GeoJSON `id`. `Query.pagination.limit` bounds the scan
1438
+ // as a global cap (not a per-page count) so callers can stop the drain
1439
+ // without learning the server's default page size. A finite limit
1440
+ // (including 0) is honored as a real cap; only `undefined` / negative
1441
+ // means "drain everything".
1442
+ const requestedLimit = request?.pagination?.limit;
1443
+ if (requestedLimit === 0)
1444
+ return [];
1445
+ const choice = await negotiateJsonOrThrow();
1446
+ const limitCap = typeof requestedLimit === "number" && requestedLimit > 0 ? requestedLimit : undefined;
1447
+ const drainPageSize = 2000;
1448
+ const ids = [];
1449
+ let offset = request?.pagination?.offset ?? 0;
1450
+ const idsRequest = toWfsObjectIdsDrainRequest(request);
1451
+ while (true) {
1452
+ const remainingCap = limitCap !== undefined ? Math.max(0, limitCap - ids.length) : undefined;
1453
+ if (remainingCap === 0)
1454
+ break;
1455
+ const pageSize = remainingCap !== undefined ? Math.min(drainPageSize, remainingCap) : drainPageSize;
1456
+ const pageRequest = {
1457
+ ...idsRequest,
1458
+ pagination: { offset, limit: pageSize },
1459
+ };
1460
+ const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, pageSize);
1461
+ const collection = json;
1462
+ const features = collection.features ?? [];
1463
+ for (const feature of features) {
1464
+ if (feature.id !== undefined && feature.id !== null) {
1465
+ ids.push(feature.id);
1466
+ if (limitCap !== undefined && ids.length >= limitCap)
1467
+ break;
1468
+ }
1469
+ }
1470
+ if (limitCap !== undefined && ids.length >= limitCap)
1471
+ break;
1472
+ if (features.length < pageSize)
1473
+ break;
1474
+ offset += features.length;
1475
+ }
1476
+ return ids;
1477
+ },
1478
+ async applyEdits(envelope) {
1479
+ ensureCapability(descriptor, caps, "applyEdits");
1480
+ // CanonicalFeature.id is required for updates because each <wfs:Update>
1481
+ // is filtered by `<fes:ResourceId>`; without an id the block would
1482
+ // mass-update every feature in the type. Mirror the OGC adapter's
1483
+ // per-item guard: push a failure outcome for each malformed update and
1484
+ // build the transaction with only the valid ones.
1485
+ const malformedUpdateIndices = new Set();
1486
+ const validUpdates = [];
1487
+ for (const [idx, update] of (envelope.updates ?? []).entries()) {
1488
+ if (update.id === undefined || update.id === null) {
1489
+ malformedUpdateIndices.add(idx);
1490
+ }
1491
+ else {
1492
+ validUpdates.push(update);
1493
+ }
1494
+ }
1495
+ const adds = envelope.adds ?? [];
1496
+ const deletes = envelope.deletes ?? [];
1497
+ let summary;
1498
+ if (adds.length === 0 && validUpdates.length === 0 && deletes.length === 0) {
1499
+ // Nothing to send — every operation was either absent or malformed.
1500
+ // Skip the wire round-trip entirely so the server never sees an
1501
+ // unaddressed transaction.
1502
+ summary = { totalInserted: 0, totalUpdated: 0, totalDeleted: 0, insertResults: [] };
1503
+ }
1504
+ else {
1505
+ const filtered = { ...envelope, updates: validUpdates };
1506
+ const body = buildTransactionBody(typeName, filtered, featureNamespace, wfsSrsName);
1507
+ const transactionOptions = { body };
1508
+ if (envelope.signal)
1509
+ transactionOptions.signal = envelope.signal;
1510
+ summary = await featureType.transaction(transactionOptions);
1511
+ }
1512
+ return canonicalEditResultFromTransaction(envelope, summary, malformedUpdateIndices);
1513
+ },
1514
+ async queryRelated() {
1515
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
1516
+ },
1517
+ attachments: unsupportedAttachmentApi(descriptor),
1518
+ });
1519
+ }
1520
+ function requireWfsLocator(descriptor) {
1521
+ const { url, typeName, featureNamespace, srsName } = descriptor.locator;
1522
+ if (typeof url !== "string" || url.length === 0) {
1523
+ throw new Error(`createDataset: source "${descriptor.id}" (wfs) requires locator.url`);
1524
+ }
1525
+ if (typeof typeName !== "string" || typeName.length === 0) {
1526
+ throw new Error(`createDataset: source "${descriptor.id}" (wfs) requires locator.typeName`);
1527
+ }
1528
+ if (featureNamespace !== undefined && (typeof featureNamespace !== "string" || featureNamespace.length === 0)) {
1529
+ throw new Error(`createDataset: source "${descriptor.id}" (wfs) locator.featureNamespace must be a non-empty string`);
1530
+ }
1531
+ return {
1532
+ url,
1533
+ typeName,
1534
+ featureNamespace: typeof featureNamespace === "string" ? featureNamespace : undefined,
1535
+ srsName: wfsSrsNameFromOutSr(srsName),
1536
+ };
1537
+ }
1538
+ function toWfsExtentDrainRequest(request) {
1539
+ // The drain computes a bbox over geometries, so a caller-supplied
1540
+ // `returnGeometry: false` would suppress geometry on every page and
1541
+ // leave the extent empty. Strip it alongside `outFields` and
1542
+ // `pagination` so the drain always sees the full geometry-bearing
1543
+ // response regardless of the caller's projection / paging / geometry
1544
+ // intent.
1545
+ const { outFields: _outFields, pagination: _pagination, returnGeometry: _returnGeometry, ...extentRequest } = request ?? {};
1546
+ void _outFields;
1547
+ void _pagination;
1548
+ void _returnGeometry;
1549
+ return extentRequest;
1550
+ }
1551
+ /**
1552
+ * `queryObjectIds` reads the GeoJSON `id` from each drained feature, so
1553
+ * neither `outFields` nor `returnGeometry` affects the result. Strip
1554
+ * both before issuing each `GetFeature` page so a caller-supplied
1555
+ * `returnGeometry: false` cannot throw the runGetFeatureJson guard
1556
+ * (which refuses to suppress geometry without an explicit `outFields`)
1557
+ * and so the drain's `propertyName=` does not balloon with the
1558
+ * geometry property the caller never asked for.
1559
+ */
1560
+ function toWfsObjectIdsDrainRequest(request) {
1561
+ const { outFields: _outFields, returnGeometry: _returnGeometry, pagination: _pagination, ...idsRequest } = request ?? {};
1562
+ void _outFields;
1563
+ void _returnGeometry;
1564
+ void _pagination;
1565
+ return idsRequest;
1566
+ }
1567
+ /**
1568
+ * Compile `Query.where` and `Query.spatialFilter` into a (possibly empty)
1569
+ * FES filter and route the request through GET or POST GetFeature based on
1570
+ * the URL budget. Returns the decoded GeoJSON FeatureCollection.
1571
+ */
1572
+ async function runGetFeatureJson(featureType, typeName, choice, request, pageSize) {
1573
+ const filterNodes = [];
1574
+ let needsPostBody = false;
1575
+ let bbox;
1576
+ if (request?.where !== undefined && request.where !== "") {
1577
+ const compiled = compileWhere(request.where);
1578
+ if (compiled === UNSUPPORTED_FES) {
1579
+ throw new HonuaCapabilityNotSupportedError("query", "wfs", `where clause is not expressible in FES; route the request through Source.protocol("wfs") to emit a custom filter`);
1580
+ }
1581
+ if (compiled.kind !== "and" || compiled.operands.length > 0) {
1582
+ filterNodes.push(compiled);
1583
+ }
1584
+ }
1585
+ if (request?.spatialFilter) {
1586
+ const spatialRel = request.spatialFilter.spatialRel;
1587
+ // The `bbox=` KVP and `<fes:BBOX>` both encode envelope-intersects
1588
+ // semantics (OGC 09-026r2). Only take the bbox shortcut when the caller
1589
+ // wants intersects (default), envelope-intersects, or did not specify.
1590
+ // Anything else (Contains, Within, Crosses, Overlaps, Touches) needs an
1591
+ // FES predicate that preserves the requested relation, which means
1592
+ // routing the envelope through compileSpatialFilter so the geometry is
1593
+ // serialized as a polygon under the correct spatial op.
1594
+ const isEnvelopeIntersects = spatialRel === undefined ||
1595
+ spatialRel === "esriSpatialRelIntersects" ||
1596
+ spatialRel === "esriSpatialRelEnvelopeIntersects";
1597
+ if (request.spatialFilter.geometryType === "esriGeometryEnvelope" &&
1598
+ isEnvelopeIntersects &&
1599
+ filterNodes.length === 0 &&
1600
+ request.where === undefined) {
1601
+ // BBox-only requests are short and travel safely as a `bbox=` KVP, no
1602
+ // FES emission required. Saves a round-trip through XML serialization
1603
+ // and keeps the URL human-debuggable.
1604
+ const env = request.spatialFilter.geometry;
1605
+ if (typeof env.xmin === "number" &&
1606
+ typeof env.ymin === "number" &&
1607
+ typeof env.xmax === "number" &&
1608
+ typeof env.ymax === "number") {
1609
+ bbox = `${env.xmin},${env.ymin},${env.xmax},${env.ymax}`;
1610
+ }
1611
+ }
1612
+ else {
1613
+ const filterSrsName = wfsSrsNameFromOutSr(request.outSr);
1614
+ const compiled = compileSpatialFilter(request.spatialFilter, {
1615
+ geometryProperty: DEFAULT_WFS_GEOMETRY_PROPERTY,
1616
+ ...(filterSrsName !== undefined ? { srsName: filterSrsName } : {}),
1617
+ });
1618
+ if (compiled === UNSUPPORTED_FES) {
1619
+ throw new HonuaCapabilityNotSupportedError("query", "wfs", `spatial filter (geometryType=${request.spatialFilter.geometryType}, spatialRel=${request.spatialFilter.spatialRel ?? "default"}) is not expressible in FES; reach the wire through Source.protocol("wfs")`);
1620
+ }
1621
+ filterNodes.push(compiled);
1622
+ }
1623
+ }
1624
+ const filterXml = filterNodes.length > 0 ? serializeFes(filterNodes, { typeName }) : undefined;
1625
+ if (filterXml !== undefined) {
1626
+ const encoded = encodeURIComponent(filterXml);
1627
+ if (encoded.length > WFS_GET_FILTER_BUDGET)
1628
+ needsPostBody = true;
1629
+ }
1630
+ const params = {};
1631
+ if (bbox !== undefined)
1632
+ params.bbox = bbox;
1633
+ if (filterXml !== undefined && !needsPostBody)
1634
+ params.filter = filterXml;
1635
+ // WFS `propertyName=` drops every property the caller does not list,
1636
+ // including the geometry column. Honor the canonical contract: when
1637
+ // `outFields` is set and `returnGeometry !== false`, append the
1638
+ // geometry property so geometry is preserved; when
1639
+ // `returnGeometry === false`, omit the geometry property; when
1640
+ // `returnGeometry === false` is asked without an `outFields` list,
1641
+ // refuse the request because WFS cannot suppress geometry without
1642
+ // enumerating every non-geometry property — silently widening to
1643
+ // "geometry included" would break the canonical contract.
1644
+ const callerOutFields = request?.outFields && request.outFields.length > 0 ? request.outFields : undefined;
1645
+ const wantsGeometry = request?.returnGeometry !== false;
1646
+ let propertyNames;
1647
+ if (callerOutFields !== undefined) {
1648
+ if (wantsGeometry) {
1649
+ const merged = [...callerOutFields];
1650
+ if (!merged.includes(DEFAULT_WFS_GEOMETRY_PROPERTY))
1651
+ merged.push(DEFAULT_WFS_GEOMETRY_PROPERTY);
1652
+ propertyNames = merged;
1653
+ }
1654
+ else {
1655
+ propertyNames = [...callerOutFields];
1656
+ }
1657
+ }
1658
+ else if (request?.returnGeometry === false) {
1659
+ throw new HonuaCapabilityNotSupportedError("query", "wfs", `returnGeometry=false requires an explicit outFields list (WFS propertyName cannot suppress geometry without enumerating non-geometry properties); set Query.outFields or reach the wire through Source.protocol("wfs")`);
1660
+ }
1661
+ if (propertyNames)
1662
+ params.propertyName = [...propertyNames];
1663
+ const sortBy = request?.orderBy && request.orderBy.length > 0
1664
+ ? request.orderBy.map((s) => `${s.field}${s.direction === "desc" ? " D" : " A"}`).join(",")
1665
+ : undefined;
1666
+ if (sortBy !== undefined)
1667
+ params.sortBy = sortBy;
1668
+ if (typeof pageSize === "number" && pageSize > 0) {
1669
+ params.count = pageSize;
1670
+ }
1671
+ else if (typeof request?.pagination?.limit === "number" && request.pagination.limit > 0) {
1672
+ params.count = request.pagination.limit;
1673
+ }
1674
+ if (typeof request?.pagination?.offset === "number" && request.pagination.offset > 0) {
1675
+ params.startIndex = request.pagination.offset;
1676
+ }
1677
+ const srsName = wfsSrsNameFromOutSr(request?.outSr);
1678
+ if (srsName !== undefined)
1679
+ params.srsName = srsName;
1680
+ params.outputFormat = choice.format;
1681
+ if (request?.signal)
1682
+ params.signal = request.signal;
1683
+ if (needsPostBody && filterXml !== undefined) {
1684
+ params.method = "POST";
1685
+ const postOptions = {
1686
+ typeName,
1687
+ filter: filterXml,
1688
+ count: params.count,
1689
+ startIndex: params.startIndex,
1690
+ outputFormat: choice.format,
1691
+ };
1692
+ if (propertyNames)
1693
+ postOptions.propertyNames = propertyNames;
1694
+ if (sortBy !== undefined)
1695
+ postOptions.sortBy = sortBy;
1696
+ if (srsName !== undefined)
1697
+ postOptions.srsName = srsName;
1698
+ params.body = buildPostGetFeatureBody(postOptions);
1699
+ }
1700
+ const response = await featureType.getFeature(params);
1701
+ if (response.kind !== "json") {
1702
+ throw new HonuaCapabilityNotSupportedError("query", "wfs", `WFS GetFeature returned ${response.contentType}; canonical surface only carries JSON. Reach raw output through Source.protocol("wfs")`);
1703
+ }
1704
+ return response.data;
1705
+ }
1706
+ function buildPostGetFeatureBody(options) {
1707
+ const countAttr = typeof options.count === "number" ? ` count="${options.count}"` : "";
1708
+ const startAttr = typeof options.startIndex === "number" ? ` startIndex="${options.startIndex}"` : "";
1709
+ const outputAttr = ` outputFormat="${escapeXmlAttr(options.outputFormat)}"`;
1710
+ const queryAttrs = options.srsName !== undefined ? ` srsName="${escapeXmlAttr(options.srsName)}"` : "";
1711
+ const propertyXml = options.propertyNames && options.propertyNames.length > 0
1712
+ ? options.propertyNames.map((name) => `<wfs:PropertyName>${escapeXmlText(name)}</wfs:PropertyName>`).join("")
1713
+ : "";
1714
+ const sortByXml = options.sortBy !== undefined ? buildSortByXml(options.sortBy) : "";
1715
+ return `<wfs:GetFeature xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" service="WFS" version="2.0.0"${outputAttr}${countAttr}${startAttr}><wfs:Query typeNames="${escapeXmlAttr(options.typeName)}"${queryAttrs}>${propertyXml}${options.filter}${sortByXml}</wfs:Query></wfs:GetFeature>`;
1716
+ }
1717
+ /**
1718
+ * Build a `<fes:SortBy>` block from the same comma-separated `sortBy` string
1719
+ * the GET path emits (`FIELD A,OTHER D`). Empty entries are skipped.
1720
+ */
1721
+ function buildSortByXml(sortBy) {
1722
+ const entries = sortBy
1723
+ .split(",")
1724
+ .map((entry) => entry.trim())
1725
+ .filter((entry) => entry.length > 0);
1726
+ if (entries.length === 0)
1727
+ return "";
1728
+ const properties = entries.map((entry) => {
1729
+ const parts = entry.split(/\s+/);
1730
+ const field = parts[0];
1731
+ const direction = parts[1]?.toUpperCase() === "D" ? "DESC" : "ASC";
1732
+ return `<fes:SortProperty><fes:ValueReference>${escapeXmlText(field)}</fes:ValueReference><fes:SortOrder>${direction}</fes:SortOrder></fes:SortProperty>`;
1733
+ });
1734
+ return `<fes:SortBy>${properties.join("")}</fes:SortBy>`;
1735
+ }
1736
+ /**
1737
+ * Translate a canonical `Query.outSr` (string CRS URI / EPSG token, or numeric
1738
+ * WKID) into the WFS `srsName` form. Numeric WKIDs become the OGC URN form
1739
+ * `urn:ogc:def:crs:EPSG::<wkid>` so cross-server interop matches the format
1740
+ * advertised in `OperationsMetadata` / `Filter_Capabilities`.
1741
+ */
1742
+ function wfsSrsNameFromOutSr(outSr) {
1743
+ if (typeof outSr === "string")
1744
+ return outSr.length > 0 ? outSr : undefined;
1745
+ if (typeof outSr === "number" && Number.isFinite(outSr))
1746
+ return `urn:ogc:def:crs:EPSG::${outSr}`;
1747
+ return undefined;
1748
+ }
1749
+ function escapeXmlAttr(value) {
1750
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1751
+ }
1752
+ function resultFromGeoJson(data) {
1753
+ if (typeof data !== "object" || data === null) {
1754
+ return { features: [], exceededTransferLimit: false };
1755
+ }
1756
+ const collection = data;
1757
+ const features = (collection.features ?? []).map((f) => ({
1758
+ attributes: (f.properties ?? {}),
1759
+ geometry: f.geometry,
1760
+ }));
1761
+ const totalCount = typeof collection.numberMatched === "number" && Number.isFinite(collection.numberMatched)
1762
+ ? collection.numberMatched
1763
+ : undefined;
1764
+ const exceededTransferLimit = totalCount !== undefined && features.length < totalCount;
1765
+ const out = {
1766
+ features,
1767
+ exceededTransferLimit,
1768
+ };
1769
+ if (totalCount !== undefined)
1770
+ out.totalCount = totalCount;
1771
+ return out;
1772
+ }
1773
+ function computeExtentFromFeatureCollection(data) {
1774
+ if (typeof data !== "object" || data === null)
1775
+ return { extent: null, count: 0 };
1776
+ const collection = data;
1777
+ let xmin = Number.POSITIVE_INFINITY;
1778
+ let ymin = Number.POSITIVE_INFINITY;
1779
+ let xmax = Number.NEGATIVE_INFINITY;
1780
+ let ymax = Number.NEGATIVE_INFINITY;
1781
+ let saw = false;
1782
+ let count = 0;
1783
+ for (const feature of collection.features ?? []) {
1784
+ count += 1;
1785
+ visitGeometryCoords(feature.geometry, (x, y) => {
1786
+ if (x < xmin)
1787
+ xmin = x;
1788
+ if (y < ymin)
1789
+ ymin = y;
1790
+ if (x > xmax)
1791
+ xmax = x;
1792
+ if (y > ymax)
1793
+ ymax = y;
1794
+ saw = true;
1795
+ });
1796
+ }
1797
+ if (!saw)
1798
+ return { extent: null, count };
1799
+ return { extent: { xmin, ymin, xmax, ymax }, count };
1800
+ }
1801
+ function buildTransactionBody(typeName, envelope, featureNamespace, srsName) {
1802
+ const releaseAction = envelope.rollbackOnFailure ? "ALL" : "SOME";
1803
+ const { prefix } = splitTypeName(typeName);
1804
+ // Bind the feature-namespace prefix on the Transaction root so prefixed
1805
+ // feature elements inside <wfs:Insert> and prefixed `typeName=` attribute
1806
+ // references on <wfs:Update>/<wfs:Delete> resolve. When the descriptor's
1807
+ // locator does not advertise `featureNamespace`, fall back to a synthetic
1808
+ // URN so the document is at least well-formed XML; strict servers will
1809
+ // reject the unknown URI with a diagnostic ExceptionReport that the caller
1810
+ // can resolve by setting `locator.featureNamespace`.
1811
+ const featureNs = prefix ? (featureNamespace ?? syntheticFeatureNamespace(prefix)) : undefined;
1812
+ const featureNsAttr = prefix && featureNs ? ` xmlns:${prefix}="${escapeXmlAttr(featureNs)}"` : "";
1813
+ const blocks = [];
1814
+ // Insert handles are read back in `canonicalEditResultFromTransaction`
1815
+ // to map server-echoed `<wfs:Feature handle="…">` entries inside
1816
+ // `<wfs:InsertResults>` onto the originating `envelope.adds[i]`. The
1817
+ // shared scheme lives in `wfsInsertHandle` so the build side and the
1818
+ // result-mapping side cannot drift.
1819
+ const adds = envelope.adds ?? [];
1820
+ for (let i = 0; i < adds.length; i += 1) {
1821
+ blocks.push(buildInsertBlock(typeName, adds[i], wfsInsertHandle(i), srsName));
1822
+ }
1823
+ let handleCounter = adds.length;
1824
+ for (const update of envelope.updates ?? []) {
1825
+ handleCounter += 1;
1826
+ blocks.push(buildUpdateBlock(typeName, update, `upd-${handleCounter}`, srsName));
1827
+ }
1828
+ for (const id of envelope.deletes ?? []) {
1829
+ handleCounter += 1;
1830
+ blocks.push(buildDeleteBlock(typeName, id, `del-${handleCounter}`));
1831
+ }
1832
+ return `<wfs:Transaction xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:gml="http://www.opengis.net/gml/3.2"${featureNsAttr} service="WFS" version="2.0.0" releaseAction="${releaseAction}">${blocks.join("")}</wfs:Transaction>`;
1833
+ }
1834
+ /**
1835
+ * Split a namespace-qualified WFS type name like `parcels:lot` into its
1836
+ * prefix and local components. Returns `{ prefix: undefined, local: typeName }`
1837
+ * for unprefixed names.
1838
+ */
1839
+ function splitTypeName(typeName) {
1840
+ const colon = typeName.indexOf(":");
1841
+ if (colon < 0)
1842
+ return { prefix: undefined, local: typeName };
1843
+ return { prefix: typeName.slice(0, colon), local: typeName.slice(colon + 1) };
1844
+ }
1845
+ /**
1846
+ * Deterministic stub URI used when the locator does not declare a
1847
+ * `featureNamespace`. Servers that perform strict schema validation will
1848
+ * reject this URI with an `<ows:ExceptionReport>` rather than silently
1849
+ * accept the body — the message names the prefix so callers know which
1850
+ * `featureNamespace` to set on the descriptor.
1851
+ */
1852
+ function syntheticFeatureNamespace(prefix) {
1853
+ return `urn:honua:wfs:feature-namespace:${prefix}`;
1854
+ }
1855
+ function buildInsertBlock(typeName, feature, handle, srsName) {
1856
+ const attributes = feature.attributes;
1857
+ const propertyXml = Object.entries(attributes)
1858
+ .filter(([, value]) => value !== undefined)
1859
+ .map(([key, value]) => `<${escapeXmlElement(key)}>${escapeXmlText(formatLiteral(value))}</${escapeXmlElement(key)}>`)
1860
+ .join("");
1861
+ const geometryXml = feature.geometry ? (geoJsonGeometryToGml(feature.geometry, srsName) ?? "") : "";
1862
+ const geometryProperty = escapeXmlElement(DEFAULT_WFS_GEOMETRY_PROPERTY);
1863
+ const featureXml = `<${escapeXmlElement(typeName)}>${propertyXml}${geometryXml ? `<${geometryProperty}>${geometryXml}</${geometryProperty}>` : ""}</${escapeXmlElement(typeName)}>`;
1864
+ return `<wfs:Insert handle="${handle}">${featureXml}</wfs:Insert>`;
1865
+ }
1866
+ function buildUpdateBlock(typeName, feature, handle, srsName) {
1867
+ const attributes = feature.attributes;
1868
+ const properties = Object.entries(attributes)
1869
+ .filter(([, value]) => value !== undefined)
1870
+ .map(([key, value]) => `<wfs:Property><wfs:ValueReference>${escapeXmlText(key)}</wfs:ValueReference><wfs:Value>${escapeXmlText(formatLiteral(value))}</wfs:Value></wfs:Property>`)
1871
+ .join("");
1872
+ const geometryProperty = feature.geometry
1873
+ ? `<wfs:Property><wfs:ValueReference>${escapeXmlText(DEFAULT_WFS_GEOMETRY_PROPERTY)}</wfs:ValueReference><wfs:Value>${geoJsonGeometryToGml(feature.geometry, srsName) ?? ""}</wfs:Value></wfs:Property>`
1874
+ : "";
1875
+ const filter = feature.id !== undefined
1876
+ ? `<fes:Filter><fes:ResourceId rid="${escapeXmlAttr(String(feature.id))}"/></fes:Filter>`
1877
+ : "";
1878
+ return `<wfs:Update handle="${handle}" typeName="${escapeXmlAttr(typeName)}">${properties}${geometryProperty}${filter}</wfs:Update>`;
1879
+ }
1880
+ function buildDeleteBlock(typeName, id, handle) {
1881
+ return `<wfs:Delete handle="${handle}" typeName="${escapeXmlAttr(typeName)}"><fes:Filter><fes:ResourceId rid="${escapeXmlAttr(String(id))}"/></fes:Filter></wfs:Delete>`;
1882
+ }
1883
+ function formatLiteral(value) {
1884
+ if (value === null)
1885
+ return "";
1886
+ if (typeof value === "number" || typeof value === "boolean")
1887
+ return String(value);
1888
+ return String(value);
1889
+ }
1890
+ function escapeXmlElement(name) {
1891
+ // Element names in WFS are namespace-qualified, but we treat the typeName
1892
+ // as a single token so callers may include `:` or `_`. Disallow anything
1893
+ // that could break the document.
1894
+ return name.replace(/[^A-Za-z0-9_:.-]/g, "_");
1895
+ }
1896
+ function escapeXmlText(value) {
1897
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1898
+ }
1899
+ /**
1900
+ * Stable handle scheme for `<wfs:Insert>` blocks. The server is expected
1901
+ * (but not required by WFS 2.0) to echo this on the matching
1902
+ * `<wfs:Feature handle="…">` element inside `<wfs:InsertResults>`, which
1903
+ * lets `canonicalEditResultFromTransaction` map returned ResourceIds back
1904
+ * onto `envelope.adds[i]` even when the server reorders the buckets or
1905
+ * omits failed inserts under `releaseAction="SOME"`.
1906
+ */
1907
+ function wfsInsertHandle(idx) {
1908
+ return `add-${idx + 1}`;
1909
+ }
1910
+ function canonicalEditResultFromTransaction(envelope, summary, malformedUpdateIndices = new Set()) {
1911
+ // Index `<wfs:InsertResults>` by the handle the server echoed so that
1912
+ // server-side reordering or `releaseAction="SOME"` partial failures do
1913
+ // not misassign ResourceIds to the wrong `envelope.adds[i]`. The
1914
+ // handle attribute is informational in the WFS 2.0 spec, so when the
1915
+ // server omits it on every `<wfs:Feature>` we fall back to the legacy
1916
+ // positional pairing rather than dropping every insert id silently.
1917
+ let anyHandleEchoed = false;
1918
+ const insertByHandle = new Map();
1919
+ for (const bucket of summary.insertResults) {
1920
+ if (bucket.handle !== undefined) {
1921
+ anyHandleEchoed = true;
1922
+ insertByHandle.set(bucket.handle, bucket.ids);
1923
+ }
1924
+ }
1925
+ const added = (envelope.adds ?? []).map((_, idx) => {
1926
+ const matchedIds = anyHandleEchoed ? insertByHandle.get(wfsInsertHandle(idx)) : summary.insertResults[idx]?.ids;
1927
+ const insertedId = matchedIds?.[0];
1928
+ // When handles are echoed, presence in `InsertResults` is the
1929
+ // authoritative per-insert success signal (the server only emits a
1930
+ // bucket for the inserts that actually committed). Without handles
1931
+ // we fall back to the legacy "first N succeeded" heuristic anchored
1932
+ // on `totalInserted`.
1933
+ const ok = anyHandleEchoed ? matchedIds !== undefined : idx < summary.totalInserted;
1934
+ const out = { success: ok };
1935
+ if (insertedId !== undefined)
1936
+ out.id = insertedId;
1937
+ return out;
1938
+ });
1939
+ // Updates that were skipped because they had no id never went on the wire;
1940
+ // their outcome is a deterministic 400. The remaining indices align with
1941
+ // the transaction summary in the order the body sent them.
1942
+ const updated = [];
1943
+ let validUpdateIdx = 0;
1944
+ for (const [idx, update] of (envelope.updates ?? []).entries()) {
1945
+ if (malformedUpdateIndices.has(idx)) {
1946
+ updated.push({ success: false, error: { code: 400, description: "update.id is required" } });
1947
+ continue;
1948
+ }
1949
+ const out = { success: validUpdateIdx < summary.totalUpdated };
1950
+ if (update.id !== undefined)
1951
+ out.id = update.id;
1952
+ updated.push(out);
1953
+ validUpdateIdx += 1;
1954
+ }
1955
+ const deleted = (envelope.deletes ?? []).map((id, idx) => ({
1956
+ id,
1957
+ success: idx < summary.totalDeleted,
1958
+ }));
1959
+ return { added, updated, deleted };
1960
+ }
1961
+ // ── OData entity set ──────────────────────────────────────────
1962
+ /**
1963
+ * Build a `Source` over an OData v4 entity set. The `Query` translation
1964
+ * lowers `where` / `outFields` / `orderBy` / pagination / spatial filter
1965
+ * onto OData's `$`-prefixed query options. Dialect-specific operations
1966
+ * (`$batch`, `$apply`, `$search`, `$deltatoken`) live behind
1967
+ * `Source.protocol("odata")` on the returned `HonuaOdataEntitySet`.
1968
+ *
1969
+ * `$metadata` is fetched lazily on the first call that needs it and
1970
+ * cached on the entity-set instance. The fetched capability annotations
1971
+ * are intersected with the descriptor's declared `Capabilities` set the
1972
+ * first time a capability-gated method is invoked — this is the
1973
+ * precedent for the metadata-driven downgrade pattern referenced in
1974
+ * `docs/shared-client-contract.md`.
1975
+ */
1976
+ /**
1977
+ * Adapter factory for an OData v4 entity set.
1978
+ *
1979
+ * `Query.where` compiles to OData `$filter`, `Query.outFields` becomes
1980
+ * `$select`, `Query.orderBy` becomes `$orderby`, and `Query.pagination`
1981
+ * becomes `$top`/`$skip`. Geospatial filters translate to OData's
1982
+ * `geo.intersects` / `geo.distance` family.
1983
+ *
1984
+ * @example
1985
+ * ```ts
1986
+ * const dataset = createDataset({
1987
+ * id: "incidents",
1988
+ * client,
1989
+ * sources: [{
1990
+ * id: "incidents",
1991
+ * protocol: "odata",
1992
+ * locator: { url: "https://your-honua-server.example/odata", entitySet: "Incidents" },
1993
+ * capabilities: PROTOCOL_DEFAULT_CAPABILITIES.odata,
1994
+ * }],
1995
+ * });
1996
+ * const result = await dataset.source("incidents")!.queryAll({
1997
+ * where: "Severity ge 3",
1998
+ * orderBy: [{ field: "ReportedAt", direction: "desc" }],
1999
+ * pagination: { limit: 100 },
2000
+ * });
2001
+ * ```
2002
+ */
2003
+ export function odataSource(descriptor, client, policy) {
2004
+ const { entitySet, basePath } = requireOdataLocator(descriptor);
2005
+ const entity = new HonuaOdataEntitySet({ client, entitySet, basePath });
2006
+ const declaredCaps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.odata;
2007
+ const negotiation = new OdataCapabilityNegotiator(entity, declaredCaps);
2008
+ const adapterRegistry = {
2009
+ odata: entity,
2010
+ };
2011
+ return makeSource(descriptor, declaredCaps, policy, adapterRegistry, {
2012
+ async query(request) {
2013
+ ensureCapability(descriptor, declaredCaps, "query");
2014
+ await negotiation.ensureAdvertised(descriptor, "query");
2015
+ const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
2016
+ const params = await buildOdataParams(entity, descriptor, request, { count: true, geomColumn });
2017
+ const page = await entity.query(params);
2018
+ return odataResultFromPage(descriptor, page, await negotiation.fieldsFor(descriptor.id), request?.returnGeometry, geomColumn);
2019
+ },
2020
+ async queryAll(request) {
2021
+ ensureCapability(descriptor, declaredCaps, "query");
2022
+ await negotiation.ensureAdvertised(descriptor, "query");
2023
+ const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
2024
+ const params = await buildOdataParams(entity, descriptor, request, { count: true, geomColumn });
2025
+ const limit = request?.pagination?.limit;
2026
+ // Lookahead row: ask the server for `limit + 1` so we can prove
2027
+ // truncation by collecting more than `limit` rows even when the
2028
+ // response carries no `@odata.nextLink`. Mirrors the GeoServices
2029
+ // and OGC `queryAll` truncation pattern.
2030
+ if (typeof limit === "number" && limit >= 0) {
2031
+ params.top = limit + 1;
2032
+ }
2033
+ const drained = await entity.queryAll(params);
2034
+ const allFeatures = drained.rows.map((row) => odataRowToFeature(row, descriptor, request?.returnGeometry, geomColumn));
2035
+ const { features: limited, exceededTransferLimit: collectedExceeded } = applyQueryAllLimit(allFeatures, limit);
2036
+ // Belt-and-braces: if the server respected `$top` exactly but
2037
+ // reports `@odata.count > limited.length`, that also proves
2038
+ // truncation. Either signal sets the flag.
2039
+ const countExceeded = typeof drained.totalCount === "number" && typeof limit === "number" && drained.totalCount > limited.length;
2040
+ const exceededTransferLimit = collectedExceeded || countExceeded;
2041
+ const fields = await negotiation.fieldsFor(descriptor.id);
2042
+ return {
2043
+ features: limited,
2044
+ exceededTransferLimit,
2045
+ ...(typeof drained.totalCount === "number"
2046
+ ? { totalCount: drained.totalCount }
2047
+ : { totalCount: limited.length }),
2048
+ ...(fields.length > 0 ? { fields } : {}),
2049
+ };
2050
+ },
2051
+ async queryAggregate() {
2052
+ // OData aggregation is dialect-specific (`$apply` pipeline). The
2053
+ // canonical surface refuses; callers reach the typed escape hatch
2054
+ // (`Source.protocol("odata").apply(...)`) for the dialect surface.
2055
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
2056
+ },
2057
+ async queryExtent() {
2058
+ // OData does not expose an extent endpoint; computing one client-side
2059
+ // would require draining every matching row, which the canonical
2060
+ // surface refuses to do silently. Mirrors the OGC `attachments` /
2061
+ // `queryRelated` posture: refuse rather than ship a degraded path.
2062
+ throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
2063
+ },
2064
+ async *stream(request) {
2065
+ ensureCapability(descriptor, declaredCaps, "stream");
2066
+ await negotiation.ensureAdvertised(descriptor, "query");
2067
+ const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
2068
+ const params = await buildOdataParams(entity, descriptor, request, { count: false, geomColumn });
2069
+ const fields = await negotiation.fieldsFor(descriptor.id);
2070
+ const stream = entity.queryStream(params);
2071
+ for await (const page of stream) {
2072
+ yield odataResultFromPage(descriptor, page, fields, request?.returnGeometry, geomColumn);
2073
+ }
2074
+ },
2075
+ async queryObjectIds(request) {
2076
+ ensureCapability(descriptor, declaredCaps, "queryObjectIds");
2077
+ await negotiation.ensureAdvertised(descriptor, "query");
2078
+ const keyField = await negotiation.keyField(descriptor.id);
2079
+ const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
2080
+ const params = await buildOdataParams(entity, descriptor, request, { count: false, geomColumn });
2081
+ params.select = [keyField];
2082
+ // `queryObjectIds` projects only the key field, so `out.expand`
2083
+ // built from dotted `outFields` is irrelevant; clear it so a
2084
+ // caller-supplied projection cannot inflate the wire request.
2085
+ delete params.expand;
2086
+ // `entity.queryAll` respects `top` exactly. The canonical
2087
+ // `queryObjectIds` honors `Query.pagination.limit` as a hard cap
2088
+ // and slices the projection to it (mirrors the STAC adapter at
2089
+ // line 825). No lookahead row is needed because the result is
2090
+ // ids, not features — there is no `exceededTransferLimit` flag
2091
+ // to stamp.
2092
+ const limit = request?.pagination?.limit;
2093
+ const drained = await entity.queryAll(params);
2094
+ const ids = [];
2095
+ for (const row of drained.rows) {
2096
+ const value = row[keyField];
2097
+ if (value === undefined || value === null)
2098
+ continue;
2099
+ ids.push(typeof value === "number" ? value : String(value));
2100
+ }
2101
+ if (typeof limit === "number" && limit >= 0 && ids.length > limit) {
2102
+ return ids.slice(0, limit);
2103
+ }
2104
+ return ids;
2105
+ },
2106
+ async applyEdits(envelope) {
2107
+ ensureCapability(descriptor, declaredCaps, "applyEdits");
2108
+ await negotiation.ensureAdvertised(descriptor, "applyEdits");
2109
+ const keyFields = await negotiation.keyFields(descriptor.id);
2110
+ // Atomic path: when the caller asks for rollback-on-failure AND the
2111
+ // service advertises `$batch`, collapse the envelope into a single
2112
+ // OData batch with a shared `atomicityGroup` so a later failure
2113
+ // tears down earlier edits server-side. When `$batch` is not
2114
+ // advertised the rollback contract cannot be honored — degrade to
2115
+ // the per-call path and stamp `degraded[]` so downstream views can
2116
+ // flag the result.
2117
+ if (envelope.rollbackOnFailure === true) {
2118
+ const batchAdvertised = await negotiation.batchAdvertised();
2119
+ if (batchAdvertised) {
2120
+ return atomicOdataApplyEdits(entity, envelope, keyFields, descriptor);
2121
+ }
2122
+ const result = await perCallOdataApplyEdits(entity, envelope, keyFields);
2123
+ return {
2124
+ ...result,
2125
+ degraded: [
2126
+ ...(result.degraded ?? []),
2127
+ {
2128
+ capability: "applyEdits",
2129
+ protocol: descriptor.protocol,
2130
+ reason: "rollbackOnFailure was requested but the OData service does not advertise $batch; edits ran per-call without atomicity.",
2131
+ sourceId: descriptor.id,
2132
+ },
2133
+ ],
2134
+ };
2135
+ }
2136
+ return perCallOdataApplyEdits(entity, envelope, keyFields);
2137
+ },
2138
+ async queryRelated() {
2139
+ // OData has no canonical related-records surface; navigation
2140
+ // properties live behind the typed escape hatch (`$expand` via
2141
+ // `protocol("odata").raw(...)` or `apply()` for cross-set joins).
2142
+ throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
2143
+ },
2144
+ attachments: unsupportedAttachmentApi(descriptor),
2145
+ });
2146
+ }
2147
+ /**
2148
+ * Per-call OData edits — issues independent POST/PATCH/DELETE requests
2149
+ * and collects per-row outcomes. This is the non-atomic fallback used
2150
+ * when `rollbackOnFailure` is unset or the server does not advertise
2151
+ * `$batch`.
2152
+ */
2153
+ async function perCallOdataApplyEdits(entity, envelope, keyFields) {
2154
+ const added = [];
2155
+ const updated = [];
2156
+ const deleted = [];
2157
+ const { signal } = envelope;
2158
+ for (const add of envelope.adds ?? []) {
2159
+ try {
2160
+ const created = await entity.add(featureToOdataBody(add), {
2161
+ ...(signal ? { signal } : {}),
2162
+ });
2163
+ const id = readKey(created, keyFields, add.id);
2164
+ added.push(id !== undefined ? { id, success: true } : { success: true });
2165
+ }
2166
+ catch (err) {
2167
+ added.push({ success: false, error: editErrorFromCatch(err) });
2168
+ }
2169
+ }
2170
+ // Layer-scoped paths like `Layers(<n>)/Features` carry the parent key
2171
+ // (`LayerId`) in the URL itself, so the entity-set key parens only
2172
+ // need the non-parent components. `readKey` / `readKeyFromBody` keep
2173
+ // using the full `keyFields` because the response body still carries
2174
+ // every key.
2175
+ const urlKeys = urlKeyFields(entity.entitySet, keyFields);
2176
+ for (const update of envelope.updates ?? []) {
2177
+ const key = canonicalKeyToOdata(update, urlKeys);
2178
+ if (key === undefined) {
2179
+ updated.push({ success: false, error: { code: 400, description: "update.id is required" } });
2180
+ continue;
2181
+ }
2182
+ try {
2183
+ await entity.update(key, featureToOdataBody(update), { ...(signal ? { signal } : {}) });
2184
+ updated.push({ id: update.id ?? readKeyFromBody(update.attributes, keyFields), success: true });
2185
+ }
2186
+ catch (err) {
2187
+ updated.push({ id: update.id, success: false, error: editErrorFromCatch(err) });
2188
+ }
2189
+ }
2190
+ for (const id of envelope.deletes ?? []) {
2191
+ const key = canonicalKeyToOdata({ id, attributes: {} }, urlKeys);
2192
+ if (key === undefined) {
2193
+ deleted.push({ success: false, error: { code: 400, description: "delete id is required" } });
2194
+ continue;
2195
+ }
2196
+ try {
2197
+ await entity.delete(key, { ...(signal ? { signal } : {}) });
2198
+ deleted.push({ id, success: true });
2199
+ }
2200
+ catch (err) {
2201
+ deleted.push({ id, success: false, error: editErrorFromCatch(err) });
2202
+ }
2203
+ }
2204
+ return { added, updated, deleted };
2205
+ }
2206
+ /**
2207
+ * Atomic OData edits — collapses the envelope into a single
2208
+ * `$batch` request whose change-set wraps every operation in the same
2209
+ * `atomicityGroup`. Honua Server's `ODataBatchHandler` rolls back the
2210
+ * entire group when any operation fails. The batch responses are
2211
+ * threaded back into per-row `EditOutcome` entries in the original
2212
+ * order so callers see the same shape regardless of transport.
2213
+ */
2214
+ async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor) {
2215
+ const operations = [];
2216
+ const plan = [];
2217
+ const bucketIndices = { add: [], update: [], delete: [] };
2218
+ // Layer-scoped paths like `Layers(<n>)/Features` carry the parent key
2219
+ // (`LayerId`) in the URL itself, so the entity-set key parens only
2220
+ // need the non-parent components.
2221
+ const urlKeys = urlKeyFields(entity.entitySet, keyFields);
2222
+ // Sequence ids deterministically so the response shape is predictable
2223
+ // and the test surface stays small.
2224
+ let seq = 1;
2225
+ const adds = envelope.adds ?? [];
2226
+ for (const add of adds) {
2227
+ const id = String(seq++);
2228
+ operations.push({
2229
+ id,
2230
+ method: "POST",
2231
+ url: stripLeadingSlashLocal(entity.entitySet),
2232
+ body: featureToOdataBody(add),
2233
+ });
2234
+ bucketIndices.add.push(plan.length);
2235
+ plan.push({ kind: "add", id: add.id });
2236
+ }
2237
+ const updates = envelope.updates ?? [];
2238
+ for (const update of updates) {
2239
+ const id = String(seq++);
2240
+ const key = canonicalKeyToOdata(update, urlKeys);
2241
+ if (key === undefined) {
2242
+ bucketIndices.update.push(plan.length);
2243
+ plan.push({ kind: "update", id: update.id });
2244
+ continue;
2245
+ }
2246
+ operations.push({
2247
+ id,
2248
+ method: "PATCH",
2249
+ url: `${entity.entitySet}(${key})`,
2250
+ body: featureToOdataBody(update),
2251
+ });
2252
+ bucketIndices.update.push(plan.length);
2253
+ plan.push({ kind: "update", id: update.id, key });
2254
+ }
2255
+ const deletes = envelope.deletes ?? [];
2256
+ for (const rawId of deletes) {
2257
+ const id = String(seq++);
2258
+ const key = canonicalKeyToOdata({ id: rawId, attributes: {} }, urlKeys);
2259
+ if (key === undefined) {
2260
+ bucketIndices.delete.push(plan.length);
2261
+ plan.push({ kind: "delete", id: rawId });
2262
+ continue;
2263
+ }
2264
+ operations.push({ id, method: "DELETE", url: `${entity.entitySet}(${key})` });
2265
+ bucketIndices.delete.push(plan.length);
2266
+ plan.push({ kind: "delete", id: rawId, key });
2267
+ }
2268
+ // Items with `key === undefined` were never added to operations; mark
2269
+ // their outcome here without consulting the response.
2270
+ const added = new Array(adds.length);
2271
+ const updated = new Array(updates.length);
2272
+ const deleted = new Array(deletes.length);
2273
+ for (let i = 0; i < updates.length; i += 1) {
2274
+ const planIdx = bucketIndices.update[i];
2275
+ if (plan[planIdx].key === undefined) {
2276
+ updated[i] = { success: false, error: { code: 400, description: "update.id is required" } };
2277
+ }
2278
+ }
2279
+ for (let i = 0; i < deletes.length; i += 1) {
2280
+ const planIdx = bucketIndices.delete[i];
2281
+ if (plan[planIdx].key === undefined) {
2282
+ deleted[i] = { success: false, error: { code: 400, description: "delete id is required" } };
2283
+ }
2284
+ }
2285
+ if (operations.length === 0) {
2286
+ // Every requested edit was malformed — return the validation outcomes
2287
+ // rather than send an empty batch.
2288
+ return { added, updated, deleted };
2289
+ }
2290
+ let batchOutcomes;
2291
+ try {
2292
+ const batchResult = await entity.batch(operations, {
2293
+ atomicity: "all",
2294
+ ...(envelope.signal ? { signal: envelope.signal } : {}),
2295
+ });
2296
+ batchOutcomes = batchResult.responses;
2297
+ }
2298
+ catch (err) {
2299
+ // Whole-batch failure (network/auth/etc.). Apply the same error to
2300
+ // every requested row so the caller sees a uniform rollback signal.
2301
+ const error = editErrorFromCatch(err);
2302
+ for (let i = 0; i < adds.length; i += 1)
2303
+ added[i] = { success: false, error };
2304
+ for (let i = 0; i < updates.length; i += 1) {
2305
+ if (!updated[i])
2306
+ updated[i] = { id: updates[i].id, success: false, error };
2307
+ }
2308
+ for (let i = 0; i < deletes.length; i += 1) {
2309
+ if (!deleted[i])
2310
+ deleted[i] = { id: deletes[i], success: false, error };
2311
+ }
2312
+ return { added, updated, deleted };
2313
+ }
2314
+ // Index outcomes by id for O(1) lookup.
2315
+ const byId = new Map();
2316
+ for (const r of batchOutcomes)
2317
+ byId.set(String(r.id), r);
2318
+ // Walk the operations in submission order so outcome indices line up
2319
+ // with adds/updates/deletes positions.
2320
+ let opCursor = 0;
2321
+ for (let i = 0; i < adds.length; i += 1) {
2322
+ const id = operations[opCursor].id ?? String(opCursor + 1);
2323
+ opCursor += 1;
2324
+ const outcome = byId.get(String(id));
2325
+ added[i] = batchOutcomeToEdit(outcome, adds[i].id, keyFields);
2326
+ }
2327
+ for (let i = 0; i < updates.length; i += 1) {
2328
+ if (updated[i])
2329
+ continue; // skipped (no key)
2330
+ const id = operations[opCursor].id ?? String(opCursor + 1);
2331
+ opCursor += 1;
2332
+ const outcome = byId.get(String(id));
2333
+ updated[i] = batchOutcomeToEdit(outcome, updates[i].id, keyFields);
2334
+ }
2335
+ for (let i = 0; i < deletes.length; i += 1) {
2336
+ if (deleted[i])
2337
+ continue;
2338
+ const id = operations[opCursor].id ?? String(opCursor + 1);
2339
+ opCursor += 1;
2340
+ const outcome = byId.get(String(id));
2341
+ deleted[i] = batchOutcomeToEdit(outcome, deletes[i], keyFields);
2342
+ }
2343
+ void descriptor;
2344
+ return { added, updated, deleted };
2345
+ }
2346
+ function batchOutcomeToEdit(outcome, fallbackId, keyFields) {
2347
+ if (!outcome) {
2348
+ return {
2349
+ ...(fallbackId !== undefined ? { id: fallbackId } : {}),
2350
+ success: false,
2351
+ error: { code: 0, description: "missing batch response" },
2352
+ };
2353
+ }
2354
+ if (outcome.status >= 200 && outcome.status < 300) {
2355
+ const body = outcome.body && typeof outcome.body === "object" ? outcome.body : undefined;
2356
+ const id = body ? readKey(body, keyFields, fallbackId) : fallbackId;
2357
+ return id !== undefined ? { id, success: true } : { success: true };
2358
+ }
2359
+ const description = describeBatchError(outcome.body, outcome.status);
2360
+ return {
2361
+ ...(fallbackId !== undefined ? { id: fallbackId } : {}),
2362
+ success: false,
2363
+ error: { code: outcome.status, description },
2364
+ };
2365
+ }
2366
+ function describeBatchError(body, status) {
2367
+ if (body && typeof body === "object") {
2368
+ const err = body.error;
2369
+ if (err && typeof err.message === "string")
2370
+ return err.message;
2371
+ }
2372
+ return `HTTP ${status}`;
2373
+ }
2374
+ function stripLeadingSlashLocal(url) {
2375
+ return url.startsWith("/") ? url.slice(1) : url;
2376
+ }
2377
+ function requireOdataLocator(descriptor) {
2378
+ const { entitySet, url, layerId } = descriptor.locator;
2379
+ // Resolve the entity-set token. Honua Server's `SourceLocator` only
2380
+ // carries `url`, `serviceId`, and `layerId`, so server-produced OData
2381
+ // bindings arrive without `entitySet`. The canonical server route is
2382
+ // layer-scoped — `/odata/Layers(<layerId>)/Features` — so derive that
2383
+ // path when only `layerId` is provided. SDK callers that already pass
2384
+ // `entitySet` (the historical path) continue to win.
2385
+ let resolvedEntitySet;
2386
+ if (typeof entitySet === "string" && entitySet !== "") {
2387
+ resolvedEntitySet = entitySet;
2388
+ }
2389
+ else if (typeof layerId === "number" && Number.isFinite(layerId)) {
2390
+ resolvedEntitySet = `Layers(${layerId})/Features`;
2391
+ }
2392
+ if (resolvedEntitySet === undefined) {
2393
+ throw new Error(`createDataset: source "${descriptor.id}" (odata) requires locator.entitySet or locator.layerId`);
2394
+ }
2395
+ // `locator.url` is informational on the canonical surface; the request
2396
+ // path is built from `basePath/<entitySet>`. When `locator.url` carries
2397
+ // a path, treat its trailing path component as the basePath so a
2398
+ // descriptor pointing at `https://srv/odata/v4` still resolves correctly.
2399
+ const basePath = url ? extractOdataBasePath(url) : "/odata";
2400
+ return { entitySet: resolvedEntitySet, basePath };
2401
+ }
2402
+ function extractOdataBasePath(url) {
2403
+ try {
2404
+ const parsed = new URL(url);
2405
+ const pathname = trimTrailingSlashes(parsed.pathname);
2406
+ return pathname === "" ? "/odata" : pathname;
2407
+ }
2408
+ catch {
2409
+ // Relative URL: take it as the basePath verbatim.
2410
+ return url.startsWith("/") ? trimTrailingSlashes(url) || "/odata" : `/${trimTrailingSlashes(url)}`;
2411
+ }
2412
+ }
2413
+ /**
2414
+ * Resolve the OData geometry column for a descriptor. Prefers
2415
+ * `descriptor.schema.fields` (where a field of type
2416
+ * `esriFieldTypeGeometry` names the column), then falls back to the
2417
+ * lazy `$metadata` probe (the first property typed `Edm.Geography` /
2418
+ * `Edm.Geometry` per CSDL parsing). Returns `undefined` when neither
2419
+ * declares one — callers fall back to the canonical name guesses
2420
+ * (`Geometry` / `Geography` / `Shape`) where the column name is
2421
+ * needed for client-side filtering only (never for emitting a
2422
+ * spatial filter, which throws when the column cannot be resolved).
2423
+ */
2424
+ async function resolveOdataGeometryColumn(entity, descriptor) {
2425
+ const schemaCol = descriptor.schema?.fields?.find((f) => f.type === "esriFieldTypeGeometry")?.name;
2426
+ if (schemaCol)
2427
+ return schemaCol;
2428
+ try {
2429
+ const meta = await entity.metadata();
2430
+ const typeName = meta.entitySets[entity.entitySetName];
2431
+ const fields = (typeName ? meta.fields[typeName] : undefined) ?? [];
2432
+ return fields.find((f) => f.isSpatial)?.name;
2433
+ }
2434
+ catch {
2435
+ return undefined;
2436
+ }
2437
+ }
2438
+ /**
2439
+ * Split a list of canonical `outFields` into a root `$select` list and
2440
+ * an OData `$expand` argument. Plain field names go into `$select`.
2441
+ * Dotted field paths (e.g. `Owner.name`) are translated to OData
2442
+ * navigation expands such as `Owner($select=name)`. Multi-level paths
2443
+ * (e.g. `Owner.address.street`) nest as
2444
+ * `Owner($expand=address($select=street))`. Multiple fields under the
2445
+ * same navigation share one expand entry
2446
+ * (`Owner($select=name,email)`) so the URL stays compact.
2447
+ */
2448
+ function splitOdataOutFields(outFields) {
2449
+ const root = { select: [], children: new Map() };
2450
+ for (const field of outFields) {
2451
+ const segments = field.split(".");
2452
+ if (segments.length === 1) {
2453
+ root.select.push(segments[0]);
2454
+ continue;
2455
+ }
2456
+ let cursor = root;
2457
+ for (let i = 0; i < segments.length - 1; i += 1) {
2458
+ const seg = segments[i];
2459
+ let child = cursor.children.get(seg);
2460
+ if (!child) {
2461
+ child = { select: [], children: new Map() };
2462
+ cursor.children.set(seg, child);
2463
+ }
2464
+ cursor = child;
2465
+ }
2466
+ cursor.select.push(segments[segments.length - 1]);
2467
+ }
2468
+ function serialize(node) {
2469
+ const parts = [];
2470
+ if (node.select.length > 0)
2471
+ parts.push(`$select=${node.select.join(",")}`);
2472
+ if (node.children.size > 0) {
2473
+ const segs = [];
2474
+ for (const [name, child] of node.children) {
2475
+ const inner = serialize(child);
2476
+ segs.push(inner === "" ? name : `${name}(${inner})`);
2477
+ }
2478
+ parts.push(`$expand=${segs.join(",")}`);
2479
+ }
2480
+ return parts.join(";");
2481
+ }
2482
+ const expand = [];
2483
+ for (const [name, child] of root.children) {
2484
+ const inner = serialize(child);
2485
+ expand.push(inner === "" ? name : `${name}(${inner})`);
2486
+ }
2487
+ return { select: root.select, expand };
2488
+ }
2489
+ /**
2490
+ * Decide whether a candidate root `outFields` entry refers to the
2491
+ * geometry column. When the resolver supplied a column name (from
2492
+ * descriptor schema or metadata), match against it directly. The
2493
+ * canonical name guesses (`geometry`, `geography`, `shape`) are
2494
+ * preserved as a fallback so a server that emits a hard-coded geometry
2495
+ * field still drops out of `$select` even when neither schema nor
2496
+ * metadata declares one.
2497
+ */
2498
+ function isOdataGeometryFieldName(field, resolved) {
2499
+ if (resolved && field === resolved)
2500
+ return true;
2501
+ if (resolved)
2502
+ return false;
2503
+ const lower = field.toLowerCase();
2504
+ return lower === "geometry" || lower === "geography" || lower === "shape";
2505
+ }
2506
+ /**
2507
+ * Build the OData query params for a canonical `Query`. Materializes
2508
+ * `$metadata` through the negotiator only when a translation rule needs
2509
+ * a geometry column or the key field — otherwise the request stays
2510
+ * metadata-free.
2511
+ */
2512
+ async function buildOdataParams(entity, descriptor, request, options) {
2513
+ const out = {};
2514
+ if (options.count)
2515
+ out.count = true;
2516
+ if (!request)
2517
+ return out;
2518
+ const filterParts = [];
2519
+ if (request.where !== undefined && request.where !== "") {
2520
+ const rewritten = rewriteWhereToOdataFilter(request.where);
2521
+ if (rewritten !== "")
2522
+ filterParts.push(rewritten);
2523
+ }
2524
+ if (request.spatialFilter) {
2525
+ // `$metadata` is advisory here — it only refines the geometry column
2526
+ // and SRID context. When it is missing or failing, fall back to the
2527
+ // descriptor-derived `geomColumn` and column defaults instead of
2528
+ // rejecting the query (same degradation as `resolveOdataGeometryColumn`).
2529
+ const meta = await entity.metadata().catch(() => undefined);
2530
+ const typeName = meta ? meta.entitySets[entity.entitySetName] : undefined;
2531
+ const spatialFields = (typeName && meta ? meta.fields[typeName] : undefined) ?? [];
2532
+ // The WKT SRID stamps the **input** literal's coordinate system, not
2533
+ // the desired output SR (`Query.outSr` controls the response geometry
2534
+ // SR via column-side projection on the server). Derive the input SRID
2535
+ // from `spatialFilter.geometry.spatialReference` and fall back to the
2536
+ // metadata-declared column SRID; if neither is available, omit the
2537
+ // SRID prefix and let the column default apply. The geometry column
2538
+ // itself is preferred from `options.geomColumn` (descriptor schema
2539
+ // first, then metadata isSpatial) so a schema-declared column named
2540
+ // anything other than `Geometry`/`Geography`/`Shape` is honored.
2541
+ const ctx = {
2542
+ ...(options.geomColumn ? { geometryColumn: options.geomColumn } : {}),
2543
+ ...(spatialFields.length > 0 ? { geometryFields: spatialFields } : {}),
2544
+ };
2545
+ filterParts.push(buildOdataSpatialFilter(request.spatialFilter, ctx));
2546
+ }
2547
+ if (filterParts.length > 0)
2548
+ out.filter = filterParts.join(" and ");
2549
+ if (request.outFields && request.outFields.length > 0) {
2550
+ const split = splitOdataOutFields(request.outFields);
2551
+ let rootSelect = split.select;
2552
+ if (request.returnGeometry === false) {
2553
+ rootSelect = rootSelect.filter((f) => !isOdataGeometryFieldName(f, options.geomColumn));
2554
+ }
2555
+ if (rootSelect.length > 0)
2556
+ out.select = rootSelect;
2557
+ if (split.expand.length > 0)
2558
+ out.expand = split.expand;
2559
+ }
2560
+ else if (request.returnGeometry === false) {
2561
+ // No outFields supplied — derive `$select` from metadata so the
2562
+ // geometry column never reaches the wire. Falls through silently
2563
+ // when metadata is unavailable; the result-side dropper still
2564
+ // strips the geometry from the canonical Result.
2565
+ const meta = await entity.metadata().catch(() => undefined);
2566
+ if (meta) {
2567
+ const typeName = meta.entitySets[entity.entitySetName];
2568
+ const allFields = (typeName ? meta.fields[typeName] : undefined) ?? [];
2569
+ const nonSpatial = allFields
2570
+ .filter((f) => !f.isSpatial && (options.geomColumn === undefined || f.name !== options.geomColumn))
2571
+ .map((f) => f.name);
2572
+ if (nonSpatial.length > 0)
2573
+ out.select = nonSpatial;
2574
+ }
2575
+ }
2576
+ if (request.orderBy && request.orderBy.length > 0) {
2577
+ out.orderBy = request.orderBy.map((s) => (s.direction === "desc" ? `${s.field} desc` : s.field));
2578
+ }
2579
+ if (request.pagination?.limit !== undefined)
2580
+ out.top = request.pagination.limit;
2581
+ if (request.pagination?.offset !== undefined && request.pagination.offset > 0) {
2582
+ out.skip = request.pagination.offset;
2583
+ }
2584
+ if (request.signal)
2585
+ out.signal = request.signal;
2586
+ void descriptor;
2587
+ return out;
2588
+ }
2589
+ function odataResultFromPage(descriptor, page, fields, returnGeometry, geomColumn) {
2590
+ const features = page.rows.map((row) => odataRowToFeature(row, descriptor, returnGeometry, geomColumn));
2591
+ const exceededTransferLimit = typeof page.totalCount === "number" ? features.length < page.totalCount : Boolean(page.nextLink);
2592
+ return {
2593
+ features,
2594
+ exceededTransferLimit,
2595
+ ...(typeof page.totalCount === "number" ? { totalCount: page.totalCount } : {}),
2596
+ ...(fields.length > 0 ? { fields } : {}),
2597
+ };
2598
+ }
2599
+ /**
2600
+ * Convert one OData JSON row into a canonical `HonuaTypedFeature`. The
2601
+ * geometry column (when the row carries one as GeoJSON via Honua Server's
2602
+ * spatial encoding) is split out from the attributes envelope. When
2603
+ * `returnGeometry === false`, the geometry column is dropped from both
2604
+ * the attributes envelope and the `geometry` field — defensive against
2605
+ * servers that ignore the `$select` exclusion (or callers that bypassed
2606
+ * metadata-derived selects). The geometry column is sourced from the
2607
+ * caller-resolved `geomColumn` (descriptor schema → metadata
2608
+ * `isSpatial`) and falls back to the canonical name guesses on the
2609
+ * row keys when neither declares one.
2610
+ */
2611
+ function odataRowToFeature(row, descriptor, returnGeometry, geomColumn) {
2612
+ const geometryKey = geomColumn ??
2613
+ descriptor.schema?.fields?.find((f) => f.type === "esriFieldTypeGeometry")?.name ??
2614
+ findGeometryKey(row);
2615
+ if (geometryKey && row[geometryKey] !== undefined) {
2616
+ const { [geometryKey]: geometry, ...rest } = row;
2617
+ return {
2618
+ attributes: rest,
2619
+ geometry: returnGeometry === false ? null : (geometry ?? null),
2620
+ };
2621
+ }
2622
+ return { attributes: row, geometry: null };
2623
+ }
2624
+ function findGeometryKey(row) {
2625
+ for (const key of Object.keys(row)) {
2626
+ const lower = key.toLowerCase();
2627
+ if (lower === "geometry" || lower === "geography" || lower === "shape")
2628
+ return key;
2629
+ }
2630
+ return undefined;
2631
+ }
2632
+ function featureToOdataBody(feature) {
2633
+ const body = { ...feature.attributes };
2634
+ if (feature.geometry !== undefined && feature.geometry !== null) {
2635
+ if (body.Geometry === undefined && body.geometry === undefined) {
2636
+ body.Geometry = feature.geometry;
2637
+ }
2638
+ }
2639
+ return body;
2640
+ }
2641
+ function readKey(body, keyFields, fallback) {
2642
+ if (keyFields.length === 1) {
2643
+ const value = body[keyFields[0]];
2644
+ if (typeof value === "number" || typeof value === "string")
2645
+ return value;
2646
+ }
2647
+ if (keyFields.length > 1) {
2648
+ // Composite key: surface as `Field=value,Field=value`. Numeric fallback
2649
+ // is preferred when only one of the key parts is OBJECTID-like to keep
2650
+ // the EditOutcome.id readable.
2651
+ const objectId = body.ObjectId ?? body.objectId ?? body.OBJECTID;
2652
+ if (typeof objectId === "number" || typeof objectId === "string")
2653
+ return objectId;
2654
+ }
2655
+ return fallback;
2656
+ }
2657
+ function readKeyFromBody(attributes, keyFields) {
2658
+ if (typeof attributes !== "object" || attributes === null)
2659
+ return undefined;
2660
+ return readKey(attributes, keyFields, undefined);
2661
+ }
2662
+ /**
2663
+ * For OData navigation paths like `Layers(<n>)/Features`, the parent key
2664
+ * (`LayerId`) is already encoded in the URL path — the entity-set key
2665
+ * parens should only carry the non-parent components of the composite
2666
+ * key. Returns the metadata key fields with the parent-key field removed
2667
+ * when the path is layer-scoped, so a caller addressing a row by its
2668
+ * bare ObjectId still produces a valid `Features(<objectId>)` URL.
2669
+ *
2670
+ * Direct paths (e.g. `Parcels`) return the input key fields unchanged.
2671
+ */
2672
+ function urlKeyFields(entitySetPath, keyFields) {
2673
+ if (!/^Layers\(\d+\)\/Features$/i.test(entitySetPath))
2674
+ return keyFields;
2675
+ const filtered = keyFields.filter((f) => f.toLowerCase() !== "layerid");
2676
+ // Defense in depth: if filtering eliminates every key field (the
2677
+ // metadata declared LayerId-only) fall back to the original list so
2678
+ // the formatter at least has something to work with.
2679
+ return filtered.length === 0 ? keyFields : filtered;
2680
+ }
2681
+ function canonicalKeyToOdata(feature, keyFields) {
2682
+ if (keyFields.length === 0) {
2683
+ if (feature.id === undefined || feature.id === null)
2684
+ return undefined;
2685
+ return formatOdataKeyValue(feature.id);
2686
+ }
2687
+ if (keyFields.length === 1) {
2688
+ const fieldName = keyFields[0];
2689
+ const value = feature.attributes?.[fieldName] ?? feature.id;
2690
+ if (value === undefined || value === null)
2691
+ return undefined;
2692
+ return formatOdataKeyValue(value);
2693
+ }
2694
+ // Composite key — prefer attribute-derived components. When `feature.id`
2695
+ // is a pre-formatted key expression (`LayerId=1,ObjectId=3` or the bare
2696
+ // `1,3` tuple), use it verbatim so callers that address composite-key
2697
+ // rows on the canonical `deletes: FeatureId[]` envelope still work.
2698
+ if (typeof feature.id === "string" && (feature.id.includes("=") || feature.id.includes(","))) {
2699
+ return feature.id;
2700
+ }
2701
+ const parts = [];
2702
+ for (const fieldName of keyFields) {
2703
+ const value = feature.attributes?.[fieldName];
2704
+ if (value === undefined || value === null) {
2705
+ // Allow a single-value `feature.id` to fill the ObjectId component
2706
+ // when the key is the conventional Honua (LayerId, ObjectId) pair
2707
+ // and the caller addressed the row by its bare ObjectId.
2708
+ if (fieldName === "ObjectId" && (typeof feature.id === "number" || typeof feature.id === "string")) {
2709
+ parts.push(`${fieldName}=${formatOdataKeyValue(feature.id)}`);
2710
+ continue;
2711
+ }
2712
+ return undefined;
2713
+ }
2714
+ parts.push(`${fieldName}=${formatOdataKeyValue(value)}`);
2715
+ }
2716
+ return parts.join(",");
2717
+ }
2718
+ function formatOdataKeyValue(value) {
2719
+ if (typeof value === "number")
2720
+ return String(value);
2721
+ // Strings are quoted; numeric strings stay as numbers because OData's
2722
+ // key syntax accepts both for Edm.Int* keys.
2723
+ const numeric = Number(value);
2724
+ if (Number.isFinite(numeric) && String(numeric) === value)
2725
+ return String(numeric);
2726
+ return `'${String(value).replace(/'/g, "''")}'`;
2727
+ }
2728
+ /**
2729
+ * Lazy, per-source negotiator that fetches `$metadata` exactly once and
2730
+ * intersects the declared capability set with what the service actually
2731
+ * advertises through `Capabilities.*` annotations. The intersection is
2732
+ * surfaced as `HonuaCapabilityNotSupportedError` on the first call that
2733
+ * needs the missing capability.
2734
+ */
2735
+ class OdataCapabilityNegotiator {
2736
+ entity;
2737
+ declared;
2738
+ metaPromise;
2739
+ fieldsCache;
2740
+ constructor(entity, declared) {
2741
+ this.entity = entity;
2742
+ this.declared = [...declared];
2743
+ }
2744
+ async ensureAdvertised(descriptor, capability) {
2745
+ // Only check capabilities the descriptor declares — caller-passed
2746
+ // narrower sets short-circuit through `ensureCapability` before this
2747
+ // is reached.
2748
+ if (!this.declared.includes(capability))
2749
+ return;
2750
+ // A missing or failing `$metadata` endpoint is allowed at runtime —
2751
+ // degrade to the descriptor's declared capability set instead of
2752
+ // rejecting the canonical call, mirroring `fieldsFor` / `keyFields` /
2753
+ // `batchAdvertised`. Errors from the data request itself still
2754
+ // surface from `query`.
2755
+ const meta = await this.materialize().catch(() => undefined);
2756
+ if (!meta)
2757
+ return;
2758
+ const advertised = meta.capabilities[this.entity.entitySetName] ?? {};
2759
+ const flag = advertisedFlag(advertised, capability);
2760
+ if (flag === false) {
2761
+ throw new HonuaCapabilityNotSupportedError(capability, descriptor.protocol, descriptor.id);
2762
+ }
2763
+ }
2764
+ async fieldsFor(_sourceId) {
2765
+ void _sourceId;
2766
+ if (this.fieldsCache)
2767
+ return this.fieldsCache;
2768
+ try {
2769
+ const meta = await this.materialize();
2770
+ const fields = odataFieldSchema(meta, this.entity.entitySetName);
2771
+ this.fieldsCache = fields;
2772
+ return fields;
2773
+ }
2774
+ catch {
2775
+ // A missing `$metadata` endpoint is allowed at runtime — the
2776
+ // canonical surface still works, just without the schema in the
2777
+ // result envelope. Errors are surfaced from `query` itself so the
2778
+ // adapter does not double-throw.
2779
+ this.fieldsCache = [];
2780
+ return this.fieldsCache;
2781
+ }
2782
+ }
2783
+ async keyFields(_sourceId) {
2784
+ void _sourceId;
2785
+ const meta = await this.materialize().catch(() => undefined);
2786
+ if (!meta)
2787
+ return [];
2788
+ const typeName = meta.entitySets[this.entity.entitySetName];
2789
+ if (!typeName)
2790
+ return [];
2791
+ return meta.keys[typeName] ?? [];
2792
+ }
2793
+ async keyField(sourceId) {
2794
+ const keys = await this.keyFields(sourceId);
2795
+ if (keys.length === 1)
2796
+ return keys[0];
2797
+ // Composite key: prefer the conventional Honua `ObjectId` field for
2798
+ // `queryObjectIds()` so the canonical surface returns a flat
2799
+ // `FeatureId[]` instead of composite-key strings.
2800
+ if (keys.includes("ObjectId"))
2801
+ return "ObjectId";
2802
+ if (keys.length > 0)
2803
+ return keys[0];
2804
+ return "ObjectId";
2805
+ }
2806
+ /**
2807
+ * Returns true when the entity-set's `Capabilities.BatchSupported`
2808
+ * annotation is `true`, false when explicitly `false`, and true by
2809
+ * default when the metadata is unavailable (the OData spec defaults
2810
+ * `$batch` to supported when the annotation is absent).
2811
+ */
2812
+ async batchAdvertised() {
2813
+ const meta = await this.materialize().catch(() => undefined);
2814
+ if (!meta)
2815
+ return true;
2816
+ const flag = meta.capabilities[this.entity.entitySetName]?.batch;
2817
+ return flag !== false;
2818
+ }
2819
+ materialize() {
2820
+ if (!this.metaPromise) {
2821
+ this.metaPromise = this.entity.metadata();
2822
+ }
2823
+ return this.metaPromise;
2824
+ }
2825
+ }
2826
+ function advertisedFlag(advertised, capability) {
2827
+ switch (capability) {
2828
+ case "query":
2829
+ return advertised.query;
2830
+ case "stream":
2831
+ return advertised.query;
2832
+ case "queryObjectIds":
2833
+ return advertised.query;
2834
+ case "applyEdits":
2835
+ // Treat the union as the canonical "applyEdits" gate — adapters
2836
+ // refuse only when every relevant flag is explicitly false.
2837
+ if (advertised.insert === false && advertised.update === false && advertised.delete === false) {
2838
+ return false;
2839
+ }
2840
+ return undefined;
2841
+ default:
2842
+ return undefined;
2843
+ }
2844
+ }
2845
+ function makeSource(descriptor, caps, _policy, adapters, impl) {
2846
+ function lookupAdapter(kind) {
2847
+ return adapters[kind];
2848
+ }
2849
+ return {
2850
+ descriptor: { ...descriptor, capabilities: caps },
2851
+ capabilities: caps,
2852
+ query: impl.query.bind(impl),
2853
+ queryAll: impl.queryAll.bind(impl),
2854
+ queryAggregate: impl.queryAggregate.bind(impl),
2855
+ queryExtent: impl.queryExtent.bind(impl),
2856
+ stream: impl.stream.bind(impl),
2857
+ queryObjectIds: impl.queryObjectIds.bind(impl),
2858
+ applyEdits: impl.applyEdits.bind(impl),
2859
+ queryRelated: impl.queryRelated.bind(impl),
2860
+ attachments: impl.attachments,
2861
+ protocol: lookupAdapter,
2862
+ adapter: lookupAdapter,
2863
+ };
2864
+ }
2865
+ /**
2866
+ * Helper: build a no-attachment-support `AttachmentApi` whose every method
2867
+ * throws `HonuaCapabilityNotSupportedError` for the descriptor. Used by
2868
+ * adapters that do not advertise `attachments` so the namespace property
2869
+ * is present (callers can always read `source.attachments`) but the calls
2870
+ * themselves participate in capability negotiation.
2871
+ */
2872
+ function unsupportedAttachmentApi(descriptor) {
2873
+ const fail = () => {
2874
+ throw new HonuaCapabilityNotSupportedError("attachments", descriptor.protocol, descriptor.id);
2875
+ };
2876
+ return {
2877
+ async query() {
2878
+ return fail();
2879
+ },
2880
+ async list() {
2881
+ return fail();
2882
+ },
2883
+ async add() {
2884
+ return fail();
2885
+ },
2886
+ async update() {
2887
+ return fail();
2888
+ },
2889
+ async delete() {
2890
+ return fail();
2891
+ },
2892
+ };
2893
+ }
2894
+ /**
2895
+ * Build a `SourceImplementation` skeleton whose every method throws
2896
+ * `HonuaCapabilityNotSupportedError`. Used by the geometry- and gp-service
2897
+ * sources, which do not host features and only surface protocol-specific
2898
+ * operations through `Source.protocol()`.
2899
+ */
2900
+ function unsupportedFeatureSurface(descriptor) {
2901
+ const fail = (capability) => {
2902
+ throw new HonuaCapabilityNotSupportedError(capability, descriptor.protocol, descriptor.id);
2903
+ };
2904
+ return {
2905
+ async query() {
2906
+ return fail("query");
2907
+ },
2908
+ async queryAll() {
2909
+ return fail("query");
2910
+ },
2911
+ async queryAggregate() {
2912
+ return fail("queryAggregate");
2913
+ },
2914
+ async queryExtent() {
2915
+ return fail("queryExtent");
2916
+ },
2917
+ // biome-ignore lint/correctness/useYield: utility-only services (Geometry, GP) do not host features; this generator refuses iteration rather than silently complete with no pages
2918
+ async *stream() {
2919
+ fail("stream");
2920
+ },
2921
+ async queryObjectIds() {
2922
+ return fail("queryObjectIds");
2923
+ },
2924
+ async applyEdits() {
2925
+ return fail("applyEdits");
2926
+ },
2927
+ async queryRelated() {
2928
+ return fail("queryRelated");
2929
+ },
2930
+ attachments: unsupportedAttachmentApi(descriptor),
2931
+ };
2932
+ }
2933
+ function ensureCapability(descriptor, caps, capability) {
2934
+ if (caps.has(capability))
2935
+ return;
2936
+ throw new HonuaCapabilityNotSupportedError(capability, descriptor.protocol, descriptor.id);
2937
+ }
2938
+ // `degraded` may only bypass a missing capability when the call site immediately
2939
+ // takes a defined fallback path. Returns true when the descriptor advertises the
2940
+ // capability (use the native path), false when policy is `degraded` and a
2941
+ // fallback will execute, and throws under `strict` when missing.
2942
+ function ensureCapabilityOrFallback(descriptor, caps, capability, policy) {
2943
+ if (caps.has(capability))
2944
+ return true;
2945
+ if (policy === "strict") {
2946
+ throw new HonuaCapabilityNotSupportedError(capability, descriptor.protocol, descriptor.id);
2947
+ }
2948
+ return false;
2949
+ }
2950
+ function requireFeatureServiceLocator(descriptor) {
2951
+ const { serviceId, layerId } = descriptor.locator;
2952
+ if (typeof serviceId !== "string" || typeof layerId !== "number") {
2953
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-feature-service) requires locator.serviceId and locator.layerId`);
2954
+ }
2955
+ return { serviceId, layerId };
2956
+ }
2957
+ function requireMapServiceLocator(descriptor) {
2958
+ const { serviceId, layerId } = descriptor.locator;
2959
+ if (typeof serviceId !== "string" || typeof layerId !== "number") {
2960
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-map-service) requires locator.serviceId and locator.layerId`);
2961
+ }
2962
+ return { serviceId, layerId };
2963
+ }
2964
+ function requireOgcLocator(descriptor) {
2965
+ const { collectionId } = descriptor.locator;
2966
+ if (collectionId === undefined || collectionId === null || collectionId === "") {
2967
+ throw new Error(`createDataset: source "${descriptor.id}" (ogc-features) requires locator.collectionId`);
2968
+ }
2969
+ return { collectionId };
2970
+ }
2971
+ /**
2972
+ * Map a `SourceLocator.layout` hint onto the OGC API Features endpoint
2973
+ * discovery mode. `undefined` / `honua-facade` keep the fixed facade fast
2974
+ * path; `ogc-api` and `auto` enable spec-driven landing-page discovery.
2975
+ * The STAC-specific modes are not applicable to Features and fall back to
2976
+ * the facade.
2977
+ */
2978
+ function ogcFeaturesLayoutMode(layout) {
2979
+ if (layout === "ogc-api")
2980
+ return "ogc-api";
2981
+ if (layout === "auto")
2982
+ return "auto";
2983
+ return undefined;
2984
+ }
2985
+ function requireOgcRecordsLocator(descriptor) {
2986
+ const { collectionId } = descriptor.locator;
2987
+ if (collectionId === undefined || collectionId === null || collectionId === "") {
2988
+ throw new Error(`createDataset: source "${descriptor.id}" (ogc-records) requires locator.collectionId`);
2989
+ }
2990
+ return { collectionId };
2991
+ }
2992
+ function requirePmtilesLocator(descriptor) {
2993
+ const { url } = descriptor.locator;
2994
+ if (typeof url !== "string" || url === "") {
2995
+ throw new Error(`createDataset: source "${descriptor.id}" (pmtiles) requires locator.url`);
2996
+ }
2997
+ return url;
2998
+ }
2999
+ function requireImageServiceLocator(descriptor) {
3000
+ const { serviceId } = descriptor.locator;
3001
+ if (typeof serviceId !== "string") {
3002
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-image-service) requires locator.serviceId`);
3003
+ }
3004
+ return { serviceId };
3005
+ }
3006
+ /**
3007
+ * Reject canonical `Query` fields the ImageServer catalog endpoint cannot
3008
+ * honor. The Honua Server catalog handler reads `where` / `objectIds` /
3009
+ * `outSR` / `resultOffset` / `resultRecordCount` / `returnGeometry` and
3010
+ * silently ignores `outFields`, `geometry` / `geometryType` / `spatialRel`,
3011
+ * and `orderByFields`. Refusing the request explicitly prevents callers
3012
+ * from receiving an unfiltered, unsorted, or wider result that looks like
3013
+ * a silent data quality bug. Mirrors the OGC adapter's spatialFilter /
3014
+ * spatialRel guards.
3015
+ */
3016
+ function requireImageServerCompatibleQuery(request) {
3017
+ if (!request)
3018
+ return;
3019
+ if (request.spatialFilter) {
3020
+ throw new Error("geoservices-image-service: Query.spatialFilter is not supported on the raster catalog; the ImageServer catalog endpoint does not accept geometry / geometryType / spatialRel filters. Use Query.where to constrain the catalog or call protocol().identify() / exportImage() on the typed escape hatch.");
3021
+ }
3022
+ if (request.orderBy && request.orderBy.length > 0) {
3023
+ throw new Error("geoservices-image-service: Query.orderBy is not supported on the raster catalog; the ImageServer catalog endpoint does not honor orderByFields. Drop orderBy or sort client-side.");
3024
+ }
3025
+ if (request.outFields && request.outFields.length > 0) {
3026
+ throw new Error("geoservices-image-service: Query.outFields is not supported on the raster catalog; the ImageServer catalog endpoint always returns the full catalog row schema. Drop outFields or project client-side.");
3027
+ }
3028
+ }
3029
+ function requireGPServiceLocator(descriptor, caps) {
3030
+ const { serviceId, taskName } = descriptor.locator;
3031
+ if (typeof serviceId !== "string") {
3032
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-gp-service) requires locator.serviceId`);
3033
+ }
3034
+ // Honua Server publishes submitJob / jobs / cancel / results only under
3035
+ // /rest/services/<serviceId>/GPServer/<taskName>/..., so descriptors that
3036
+ // advertise the `geoprocess` capability must carry a task name. Without
3037
+ // one the lifecycle routes resolve to non-existent paths on the server.
3038
+ // Descriptors with only `connect` (service-root metadata probe) may omit
3039
+ // taskName.
3040
+ if (caps.has("geoprocess") && (typeof taskName !== "string" || taskName.length === 0)) {
3041
+ throw new Error(`createDataset: source "${descriptor.id}" (geoservices-gp-service) advertises "geoprocess" but locator.taskName is missing; GP lifecycle routes require /GPServer/<taskName>/...`);
3042
+ }
3043
+ return { serviceId, taskName };
3044
+ }
3045
+ function requireOgcTilesLocator(descriptor) {
3046
+ const { collectionId, tileMatrixSetId } = descriptor.locator;
3047
+ if (collectionId === undefined || collectionId === null || collectionId === "") {
3048
+ throw new Error(`createDataset: source "${descriptor.id}" (ogc-tiles) requires locator.collectionId`);
3049
+ }
3050
+ return { collectionId, tileMatrixSetId };
3051
+ }
3052
+ function requireWmsLocator(descriptor) {
3053
+ const { serviceId } = descriptor.locator;
3054
+ if (typeof serviceId !== "string" || serviceId.length === 0) {
3055
+ throw new Error(`createDataset: source "${descriptor.id}" (wms) requires locator.serviceId`);
3056
+ }
3057
+ return { serviceId };
3058
+ }
3059
+ function requireWmtsLocator(descriptor) {
3060
+ const { serviceId } = descriptor.locator;
3061
+ if (typeof serviceId !== "string" || serviceId.length === 0) {
3062
+ throw new Error(`createDataset: source "${descriptor.id}" (wmts) requires locator.serviceId`);
3063
+ }
3064
+ return { serviceId };
3065
+ }
3066
+ function wmsRequireLayers(descriptor, locatorTypeName) {
3067
+ const parsed = parseWmsLayerNames(locatorTypeName);
3068
+ if (parsed.length === 0) {
3069
+ throw new Error(`createDataset: source "${descriptor.id}" (wms) requires locator.typeName (the WMS LAYER name) for canonical query()`);
3070
+ }
3071
+ return parsed;
3072
+ }
3073
+ /**
3074
+ * Reject canonical `Query` fields that WMS GetFeatureInfo cannot honor so a
3075
+ * mixed-source caller does not silently receive an unfiltered, reprojected,
3076
+ * or differently-shaped result. WMS GetFeatureInfo only consumes the
3077
+ * (i, j) pixel pair plus the rendered envelope; there is no SQL/CQL filter,
3078
+ * no field projection, no order, no offset paging, no geometry toggle, and
3079
+ * no separate output-SR knob on the wire — honua-server projects the
3080
+ * response in the request CRS itself. `aggregation` is mapped to
3081
+ * `queryAggregate` so the error carries the same capability vocabulary as
3082
+ * the rest of the contract. `pagination.limit` is honored (it maps to
3083
+ * `FEATURE_COUNT`).
3084
+ */
3085
+ function requireWmsCompatibleQuery(descriptor, request) {
3086
+ if (!request)
3087
+ return;
3088
+ if (request.aggregation) {
3089
+ throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
3090
+ }
3091
+ if (typeof request.where === "string" && request.where.length > 0) {
3092
+ throw new Error(`wms: Query.where is not supported on GetFeatureInfo for source "${descriptor.id}"; WMS has no SQL/CQL filter on the wire. Pre-filter via Query.spatialFilter (point) or use a tabular protocol.`);
3093
+ }
3094
+ if (request.outFields && request.outFields.length > 0) {
3095
+ throw new Error(`wms: Query.outFields is not supported on GetFeatureInfo for source "${descriptor.id}"; the server returns the layer's full attribute schema. Project client-side after the result lands.`);
3096
+ }
3097
+ if (request.orderBy && request.orderBy.length > 0) {
3098
+ throw new Error(`wms: Query.orderBy is not supported on GetFeatureInfo for source "${descriptor.id}"; the server has no sort surface. Sort client-side after the result lands.`);
3099
+ }
3100
+ if (typeof request.pagination?.offset === "number" && request.pagination.offset > 0) {
3101
+ throw new Error(`wms: Query.pagination.offset is not supported on GetFeatureInfo for source "${descriptor.id}"; the request returns at most FEATURE_COUNT records and does not paginate. Drop pagination.offset.`);
3102
+ }
3103
+ if (request.returnGeometry === false) {
3104
+ throw new Error(`wms: Query.returnGeometry=false is not supported on GetFeatureInfo for source "${descriptor.id}"; the server controls geometry inclusion via INFO_FORMAT and the layer template. Drop returnGeometry or strip geometry client-side.`);
3105
+ }
3106
+ if (request.outSr !== undefined) {
3107
+ throw new Error(`wms: Query.outSr is not supported on GetFeatureInfo for source "${descriptor.id}"; honua-server projects the response in the request CRS and has no separate output-SR knob. Stamp the spatial filter geometry's spatialReference with the desired CRS instead, or reproject the result client-side.`);
3108
+ }
3109
+ }
3110
+ /**
3111
+ * Extract `(x, y)` and the WMS CRS from `Query.spatialFilter`. WMS only
3112
+ * exposes feature-info on a pixel coordinate, so the canonical
3113
+ * `Source.query()` is restricted to point spatial filters. The CRS is
3114
+ * derived from the geometry's `spatialReference` (`wkid` / `latestWkid` /
3115
+ * `wkt`) and falls back to `CRS:84` — the WMS 1.3.0 longitude/latitude
3116
+ * code that preserves the canonical `(x, y)` axis order — when the
3117
+ * caller did not stamp the geometry with a spatial reference. Non-point
3118
+ * geometries throw `HonuaCapabilityNotSupportedError`. `Query.outSr` is
3119
+ * intentionally not consulted here: it is the **output** spatial
3120
+ * reference for projected results, not the input CRS for the
3121
+ * GetFeatureInfo wire request.
3122
+ */
3123
+ function wmsExtractPointFromQuery(request, descriptor) {
3124
+ const filter = request?.spatialFilter;
3125
+ if (!filter) {
3126
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
3127
+ }
3128
+ const geom = filter.geometry;
3129
+ if (filter.geometryType !== "esriGeometryPoint") {
3130
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
3131
+ }
3132
+ const crs = wmsCrsFromGeometrySpatialReference(geom?.spatialReference);
3133
+ if (geom && typeof geom.x === "number" && typeof geom.y === "number") {
3134
+ return { x: geom.x, y: geom.y, crs };
3135
+ }
3136
+ if (geom && Array.isArray(geom.coordinates) && geom.coordinates.length >= 2) {
3137
+ const [x, y] = geom.coordinates;
3138
+ if (typeof x === "number" && typeof y === "number")
3139
+ return { x, y, crs };
3140
+ }
3141
+ throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
3142
+ }
3143
+ /**
3144
+ * Translate a geometry's `spatialReference` (Esri-style `{ wkid?,
3145
+ * latestWkid?, wkt? }`) into a WMS-compatible CRS code. `wkid` /
3146
+ * `latestWkid` map to `EPSG:N`. A non-empty `wkt` is passed through
3147
+ * verbatim — the server validates it. When no recognizable hint is
3148
+ * present the function returns `CRS:84` so the canonical (x, y) axis
3149
+ * order is preserved on the wire.
3150
+ */
3151
+ function wmsCrsFromGeometrySpatialReference(spatialReference) {
3152
+ if (spatialReference && typeof spatialReference === "object") {
3153
+ const sr = spatialReference;
3154
+ if (typeof sr.latestWkid === "number" && Number.isFinite(sr.latestWkid)) {
3155
+ return `EPSG:${sr.latestWkid}`;
3156
+ }
3157
+ if (typeof sr.wkid === "number" && Number.isFinite(sr.wkid)) {
3158
+ return `EPSG:${sr.wkid}`;
3159
+ }
3160
+ if (typeof sr.wkt === "string" && sr.wkt.trim().length > 0) {
3161
+ return sr.wkt.trim();
3162
+ }
3163
+ }
3164
+ return "CRS:84";
3165
+ }
3166
+ function toStacRequest(request, collectionScope) {
3167
+ const out = {};
3168
+ if (request?.where !== undefined) {
3169
+ out.filter = request.where;
3170
+ out.filterLang = "cql2-text";
3171
+ }
3172
+ if (request?.outFields && request.outFields.length > 0) {
3173
+ out.fields = { include: [...request.outFields] };
3174
+ }
3175
+ if (request?.pagination?.limit !== undefined)
3176
+ out.limit = request.pagination.limit;
3177
+ if (request?.pagination?.offset !== undefined)
3178
+ out.offset = request.pagination.offset;
3179
+ if (request?.orderBy && request.orderBy.length > 0) {
3180
+ out.sortby = request.orderBy.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
3181
+ }
3182
+ if (request?.spatialFilter) {
3183
+ if (request.spatialFilter.geometryType !== "esriGeometryEnvelope") {
3184
+ throw new Error(`stac: spatialFilter.geometryType "${request.spatialFilter.geometryType}" is not supported; only "esriGeometryEnvelope" translates to STAC bbox.`);
3185
+ }
3186
+ const env = request.spatialFilter.geometry;
3187
+ if (typeof env.xmin === "number" &&
3188
+ typeof env.ymin === "number" &&
3189
+ typeof env.xmax === "number" &&
3190
+ typeof env.ymax === "number") {
3191
+ out.bbox = [env.xmin, env.ymin, env.xmax, env.ymax];
3192
+ }
3193
+ }
3194
+ if (collectionScope !== undefined && collectionScope !== "") {
3195
+ out.collections = [String(collectionScope)];
3196
+ }
3197
+ if (request?.signal)
3198
+ out.signal = request.signal;
3199
+ return out;
3200
+ }
3201
+ /**
3202
+ * Project a STAC API search request onto the client-side filter params a
3203
+ * static catalog traversal applies (it has no server-side query grammar).
3204
+ */
3205
+ function toStacStaticParams(request) {
3206
+ const out = {};
3207
+ if (request.collections && request.collections.length > 0)
3208
+ out.collections = request.collections;
3209
+ if (request.bbox)
3210
+ out.bbox = request.bbox;
3211
+ if (request.datetime !== undefined)
3212
+ out.datetime = request.datetime;
3213
+ if (request.limit !== undefined)
3214
+ out.limit = request.limit;
3215
+ if (request.signal)
3216
+ out.signal = request.signal;
3217
+ return out;
3218
+ }
3219
+ function toTypedFeatureFromStac(feature) {
3220
+ return {
3221
+ attributes: (feature.properties ?? {}),
3222
+ geometry: feature.geometry,
3223
+ };
3224
+ }
3225
+ function toOgcRecordsRequest(request) {
3226
+ if (!request)
3227
+ return {};
3228
+ const out = {};
3229
+ if (request.where !== undefined) {
3230
+ out.filter = request.where;
3231
+ out.filterLang = "cql2-text";
3232
+ }
3233
+ if (request.outFields && request.outFields.length > 0)
3234
+ out.properties = [...request.outFields];
3235
+ if (request.pagination) {
3236
+ if (request.pagination.limit !== undefined)
3237
+ out.limit = request.pagination.limit;
3238
+ if (request.pagination.offset !== undefined)
3239
+ out.offset = request.pagination.offset;
3240
+ }
3241
+ if (request.orderBy && request.orderBy.length > 0) {
3242
+ out.sortby = request.orderBy.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
3243
+ }
3244
+ if (request.spatialFilter) {
3245
+ if (request.spatialFilter.geometryType !== "esriGeometryEnvelope") {
3246
+ throw new Error(`ogc-records: spatialFilter.geometryType "${request.spatialFilter.geometryType}" is not supported; only "esriGeometryEnvelope" translates to Records bbox.`);
3247
+ }
3248
+ const rel = request.spatialFilter.spatialRel;
3249
+ if (rel !== undefined && rel !== "esriSpatialRelIntersects" && rel !== "esriSpatialRelEnvelopeIntersects") {
3250
+ throw new Error(`ogc-records: spatialFilter.spatialRel "${rel}" is not supported; the Records bbox parameter only expresses envelope-intersects.`);
3251
+ }
3252
+ const env = request.spatialFilter.geometry;
3253
+ if (typeof env.xmin === "number" &&
3254
+ typeof env.ymin === "number" &&
3255
+ typeof env.xmax === "number" &&
3256
+ typeof env.ymax === "number") {
3257
+ out.bbox = [env.xmin, env.ymin, env.xmax, env.ymax];
3258
+ }
3259
+ }
3260
+ if (request.signal)
3261
+ out.signal = request.signal;
3262
+ return out;
3263
+ }
3264
+ function toTypedFeatureFromOgcRecord(feature) {
3265
+ return {
3266
+ attributes: (feature.properties ?? {}),
3267
+ geometry: feature.geometry,
3268
+ };
3269
+ }
3270
+ /**
3271
+ * The core paging helpers default `maxPages` to 100, which would silently
3272
+ * truncate the canonical `queryAll()` / `stream()` whose contract is to
3273
+ * drain until the source is exhausted. Override with
3274
+ * `Number.MAX_SAFE_INTEGER` so the helper loop only terminates on an empty
3275
+ * page or short page.
3276
+ *
3277
+ * `normalizeMaxPages` / the inline GeoServices checks require a finite
3278
+ * integer, so `Infinity` cannot be used as the sentinel.
3279
+ */
3280
+ function withUnboundedMaxPages(params) {
3281
+ return { ...params, maxPages: Number.MAX_SAFE_INTEGER };
3282
+ }
3283
+ /**
3284
+ * `HonuaFeatureLayer.queryFeaturesAll` / `HonuaMapLayer.queryFeaturesAll` only
3285
+ * stop on an empty or short page — they have no total-row cap. When the
3286
+ * canonical `Query.pagination.limit` supplies a cap, derive the per-page size
3287
+ * *and* `maxPages` from it so the paging loop fetches at most `limit + 1`
3288
+ * rows (the extra row lets `applyQueryAllLimit` stamp `exceededTransferLimit`
3289
+ * when more records exist). Without this, `queryAll({ pagination: { limit: 1 } })`
3290
+ * would keep issuing `resultOffset=0,1,2,…` until the source is drained.
3291
+ */
3292
+ function withPagingBounds(params, limit) {
3293
+ if (typeof limit !== "number" || limit < 0) {
3294
+ return { ...params, maxPages: Number.MAX_SAFE_INTEGER };
3295
+ }
3296
+ const pageSize = Math.max(1, Math.min(Math.max(limit, 1), 2000));
3297
+ const target = limit + 1;
3298
+ const maxPages = Math.max(1, Math.ceil(target / pageSize));
3299
+ return { ...params, pageSize, maxPages };
3300
+ }
3301
+ /**
3302
+ * `HonuaFeatureLayer.queryFeaturesStream` / `HonuaMapLayer.queryFeaturesStream`
3303
+ * derive per-page size from `pageSize` and default to 2000 when it is
3304
+ * omitted. `toFeatureLayerRequest` maps `pagination.limit` to
3305
+ * `resultRecordCount`, which the stream helper then overwrites with its own
3306
+ * `pageSize`. Without this bridge `source.stream({ pagination: { limit: 10 } })`
3307
+ * would still fetch 2000-row pages. Treat `pagination.limit` as the caller's
3308
+ * per-batch budget in streaming mode (it carries the same meaning as
3309
+ * `resultRecordCount` for a single-page `query()`).
3310
+ */
3311
+ function withStreamPageSize(params, limit) {
3312
+ const base = { ...params, maxPages: Number.MAX_SAFE_INTEGER };
3313
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1) {
3314
+ return base;
3315
+ }
3316
+ return { ...base, pageSize: Math.max(1, Math.trunc(limit)) };
3317
+ }
3318
+ function applyQueryAllLimit(features, limit) {
3319
+ if (typeof limit !== "number" || limit < 0) {
3320
+ return { features, exceededTransferLimit: false };
3321
+ }
3322
+ if (features.length <= limit) {
3323
+ return { features, exceededTransferLimit: false };
3324
+ }
3325
+ return { features: features.slice(0, limit), exceededTransferLimit: true };
3326
+ }
3327
+ function toFeatureLayerRequest(request) {
3328
+ if (!request)
3329
+ return {};
3330
+ const out = {};
3331
+ if (request.where !== undefined)
3332
+ out.where = request.where;
3333
+ if (request.outFields && request.outFields.length > 0)
3334
+ out.outFields = [...request.outFields];
3335
+ if (request.returnGeometry !== undefined)
3336
+ out.returnGeometry = request.returnGeometry;
3337
+ if (request.outSr !== undefined)
3338
+ out.outSr = request.outSr;
3339
+ if (request.orderBy && request.orderBy.length > 0) {
3340
+ out.orderByFields = request.orderBy.map((s) => `${s.field}${s.direction === "desc" ? " DESC" : ""}`).join(",");
3341
+ }
3342
+ if (request.spatialFilter) {
3343
+ out.geometry = request.spatialFilter.geometry;
3344
+ out.geometryType = request.spatialFilter.geometryType;
3345
+ if (request.spatialFilter.spatialRel)
3346
+ out.spatialRel = request.spatialFilter.spatialRel;
3347
+ }
3348
+ if (request.pagination) {
3349
+ if (request.pagination.offset !== undefined)
3350
+ out.resultOffset = request.pagination.offset;
3351
+ if (request.pagination.limit !== undefined)
3352
+ out.resultRecordCount = request.pagination.limit;
3353
+ }
3354
+ if (request.signal)
3355
+ out.signal = request.signal;
3356
+ return out;
3357
+ }
3358
+ /**
3359
+ * Convert a translated GeoServices request into an extent-only variant. The
3360
+ * caller's spatial filter / outSr / signal / orderByFields must survive; only
3361
+ * paging and geometry-return are dropped, and `returnExtentOnly=true` is
3362
+ * stamped via `extraParams`. The returnCountOnly/ExtentOnly/IdsOnly side
3363
+ * channel lives in the untyped `extraParams` bag because it sits outside
3364
+ * `QueryFeaturesRequest`.
3365
+ */
3366
+ function toExtentOnlyRequest(params) {
3367
+ const { resultOffset: _offset, resultRecordCount: _count, outFields: _outFields, ...rest } = params;
3368
+ void _offset;
3369
+ void _count;
3370
+ void _outFields;
3371
+ return {
3372
+ ...rest,
3373
+ returnGeometry: false,
3374
+ extraParams: {
3375
+ ...(rest.extraParams ?? {}),
3376
+ returnExtentOnly: true,
3377
+ },
3378
+ };
3379
+ }
3380
+ /**
3381
+ * True when the canonical `Query` carries constraints that restrict which
3382
+ * records count towards the extent. Used by the degraded OGC fallback to
3383
+ * decide whether the collection metadata bbox (unfiltered) is a safe
3384
+ * shortcut or whether the extent must be computed client-side over the
3385
+ * matching records.
3386
+ */
3387
+ function hasExtentFilter(request) {
3388
+ if (!request)
3389
+ return false;
3390
+ if (request.where !== undefined && request.where !== "")
3391
+ return true;
3392
+ if (request.spatialFilter)
3393
+ return true;
3394
+ return false;
3395
+ }
3396
+ function computeExtentFromOgcFeatures(features) {
3397
+ let xmin = Number.POSITIVE_INFINITY;
3398
+ let ymin = Number.POSITIVE_INFINITY;
3399
+ let xmax = Number.NEGATIVE_INFINITY;
3400
+ let ymax = Number.NEGATIVE_INFINITY;
3401
+ let saw = false;
3402
+ for (const feature of features) {
3403
+ visitGeometryCoords(feature.geometry, (x, y) => {
3404
+ if (x < xmin)
3405
+ xmin = x;
3406
+ if (y < ymin)
3407
+ ymin = y;
3408
+ if (x > xmax)
3409
+ xmax = x;
3410
+ if (y > ymax)
3411
+ ymax = y;
3412
+ saw = true;
3413
+ });
3414
+ }
3415
+ if (!saw)
3416
+ return { extent: null, count: features.length };
3417
+ return { extent: { xmin, ymin, xmax, ymax }, count: features.length };
3418
+ }
3419
+ function visitGeometryCoords(geometry, visit) {
3420
+ if (typeof geometry !== "object" || geometry === null)
3421
+ return;
3422
+ const geom = geometry;
3423
+ if (Array.isArray(geom.geometries)) {
3424
+ for (const inner of geom.geometries)
3425
+ visitGeometryCoords(inner, visit);
3426
+ return;
3427
+ }
3428
+ visitCoordinates(geom.coordinates, visit);
3429
+ }
3430
+ function visitCoordinates(coords, visit) {
3431
+ if (!Array.isArray(coords))
3432
+ return;
3433
+ if (coords.length >= 2 && typeof coords[0] === "number" && typeof coords[1] === "number") {
3434
+ visit(coords[0], coords[1]);
3435
+ return;
3436
+ }
3437
+ for (const inner of coords)
3438
+ visitCoordinates(inner, visit);
3439
+ }
3440
+ function extractExtentEnvelope(response) {
3441
+ if (typeof response !== "object" || response === null)
3442
+ return { extent: null };
3443
+ const obj = response;
3444
+ const extent = typeof obj.extent === "object" && obj.extent !== null
3445
+ ? obj.extent
3446
+ : null;
3447
+ const count = typeof obj.count === "number" && Number.isFinite(obj.count) ? obj.count : undefined;
3448
+ return { extent, count };
3449
+ }
3450
+ function toOgcRequest(request) {
3451
+ if (!request)
3452
+ return {};
3453
+ const out = {};
3454
+ if (request.where !== undefined)
3455
+ out.filter = request.where;
3456
+ if (request.outFields && request.outFields.length > 0)
3457
+ out.properties = [...request.outFields];
3458
+ if (request.outSr !== undefined)
3459
+ out.crs = String(request.outSr);
3460
+ if (request.pagination) {
3461
+ if (request.pagination.limit !== undefined)
3462
+ out.limit = request.pagination.limit;
3463
+ if (request.pagination.offset !== undefined)
3464
+ out.offset = request.pagination.offset;
3465
+ }
3466
+ if (request.orderBy && request.orderBy.length > 0) {
3467
+ out.sortby = request.orderBy.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
3468
+ }
3469
+ if (request.spatialFilter) {
3470
+ // OGC API Features only exposes bbox at /items; arbitrary geometry types
3471
+ // would need CQL2 (which this adapter does not yet emit). Refuse the
3472
+ // request explicitly instead of silently dropping the constraint and
3473
+ // returning unfiltered features, which would look like a silent data
3474
+ // quality bug to the caller.
3475
+ if (request.spatialFilter.geometryType !== "esriGeometryEnvelope") {
3476
+ throw new Error(`ogc-features: spatialFilter.geometryType "${request.spatialFilter.geometryType}" is not supported; only "esriGeometryEnvelope" translates to OGC bbox. Convert the geometry to an envelope or use a GeoServices source.`);
3477
+ }
3478
+ // The OGC bbox parameter is defined as an envelope-intersects predicate
3479
+ // (OGC 17-069r4 §7.15.3). `contains`, `within`, `crosses`, etc. would
3480
+ // require CQL2 which this adapter does not yet emit. Refuse the request
3481
+ // rather than fall back to bbox semantics — a "within" request that
3482
+ // silently returns every intersecting feature is a correctness bug.
3483
+ const rel = request.spatialFilter.spatialRel;
3484
+ if (rel !== undefined && rel !== "esriSpatialRelIntersects" && rel !== "esriSpatialRelEnvelopeIntersects") {
3485
+ throw new Error(`ogc-features: spatialFilter.spatialRel "${rel}" is not supported; the OGC bbox parameter only expresses envelope-intersects. Use a GeoServices source or convert to an intersects predicate.`);
3486
+ }
3487
+ const env = request.spatialFilter.geometry;
3488
+ if (typeof env.xmin === "number" &&
3489
+ typeof env.ymin === "number" &&
3490
+ typeof env.xmax === "number" &&
3491
+ typeof env.ymax === "number") {
3492
+ out.bbox = `${env.xmin},${env.ymin},${env.xmax},${env.ymax}`;
3493
+ }
3494
+ }
3495
+ if (request.signal)
3496
+ out.signal = request.signal;
3497
+ return out;
3498
+ }
3499
+ /**
3500
+ * OGC operations that must inspect the full matching record set (`queryAggregate`
3501
+ * and degraded `queryExtent`) cannot inherit caller pagination. `limit` /
3502
+ * `offset` would silently materialize only a window of the match set and break
3503
+ * the whole-result contract those methods promise.
3504
+ */
3505
+ function toOgcMaterializedRequest(request) {
3506
+ const { limit: _limit, offset: _offset, ...out } = toOgcRequest(request);
3507
+ void _limit;
3508
+ void _offset;
3509
+ return out;
3510
+ }
3511
+ function featureLayerResultFromTyped(response) {
3512
+ return {
3513
+ features: response.features ?? [],
3514
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
3515
+ fields: response.fields,
3516
+ };
3517
+ }
3518
+ function featureLayerResultFromUntyped(response) {
3519
+ const features = (response.features ?? []).map((toTypedFeature));
3520
+ return {
3521
+ features,
3522
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
3523
+ fields: response.fields,
3524
+ };
3525
+ }
3526
+ function toTypedFeature(feature) {
3527
+ return {
3528
+ attributes: feature.attributes,
3529
+ geometry: feature.geometry,
3530
+ };
3531
+ }
3532
+ function toTypedFeatureFromOgc(feature) {
3533
+ return {
3534
+ attributes: (feature.properties ?? {}),
3535
+ geometry: feature.geometry,
3536
+ };
3537
+ }
3538
+ /**
3539
+ * Build the GeoServices aggregation fields that must live on the top-level
3540
+ * `QueryFeaturesRequest` / `MapLayerQueryRequest`. Both the REST serializer
3541
+ * (`client.ts`) and the gRPC adapter (`grpc-adapter.ts`) read `outStatistics`
3542
+ * and `groupByFieldsForStatistics` from the request root; stashing them in
3543
+ * `extraParams` would be silently dropped by the gRPC path.
3544
+ */
3545
+ function buildAggregationFields(aggregation) {
3546
+ const stats = aggregation.metrics.map((m) => ({
3547
+ statisticType: toGeoServicesStatisticType(m.fn),
3548
+ onStatisticField: m.field,
3549
+ outStatisticFieldName: m.alias ?? `${m.fn}_${m.field}`,
3550
+ }));
3551
+ const out = {
3552
+ outStatistics: stats,
3553
+ returnGeometry: false,
3554
+ aggregateAlias: stats[0]?.outStatisticFieldName,
3555
+ };
3556
+ if (aggregation.groupBy && aggregation.groupBy.length > 0) {
3557
+ out.groupByFieldsForStatistics = aggregation.groupBy.join(",");
3558
+ }
3559
+ return out;
3560
+ }
3561
+ function toGeoServicesStatisticType(fn) {
3562
+ switch (fn) {
3563
+ case "stddev":
3564
+ return "stddev";
3565
+ case "var":
3566
+ return "var";
3567
+ default:
3568
+ return fn;
3569
+ }
3570
+ }
3571
+ function aggregateResultFromFeatureLayer(response, _aggregateAlias) {
3572
+ const aggregateRows = (response.features ?? []).map((f) => f.attributes);
3573
+ return {
3574
+ features: [],
3575
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
3576
+ aggregateRows,
3577
+ fields: response.fields,
3578
+ };
3579
+ }
3580
+ function aggregateResultFromUntyped(response, _alias) {
3581
+ const aggregateRows = (response.features ?? []).map((f) => f.attributes);
3582
+ return {
3583
+ features: [],
3584
+ exceededTransferLimit: response.exceededTransferLimit ?? false,
3585
+ aggregateRows,
3586
+ fields: response.fields,
3587
+ };
3588
+ }
3589
+ function clientSideAggregate(features, aggregation) {
3590
+ const groupKeys = aggregation.groupBy ?? [];
3591
+ if (groupKeys.length === 0) {
3592
+ return [computeMetrics(features, aggregation.metrics)];
3593
+ }
3594
+ const groups = new Map();
3595
+ for (const feature of features) {
3596
+ const key = groupKeys.map((field) => String(readField(feature.attributes, field) ?? "")).join("\u0000");
3597
+ let bucket = groups.get(key);
3598
+ if (!bucket) {
3599
+ bucket = [];
3600
+ groups.set(key, bucket);
3601
+ }
3602
+ bucket.push(feature);
3603
+ }
3604
+ const rows = [];
3605
+ for (const bucket of groups.values()) {
3606
+ const head = bucket[0];
3607
+ const row = {};
3608
+ for (const field of groupKeys) {
3609
+ row[field] = readField(head.attributes, field);
3610
+ }
3611
+ Object.assign(row, computeMetrics(bucket, aggregation.metrics));
3612
+ rows.push(row);
3613
+ }
3614
+ return rows;
3615
+ }
3616
+ function computeMetrics(features, metrics) {
3617
+ const out = {};
3618
+ for (const metric of metrics) {
3619
+ const alias = metric.alias ?? `${metric.fn}_${metric.field}`;
3620
+ const values = features
3621
+ .map((f) => readField(f.attributes, metric.field))
3622
+ .filter((v) => typeof v === "number" && Number.isFinite(v));
3623
+ out[alias] = applyAggregation(metric.fn, values, features.length);
3624
+ }
3625
+ return out;
3626
+ }
3627
+ function applyAggregation(fn, values, totalLength) {
3628
+ if (fn === "count")
3629
+ return totalLength;
3630
+ if (values.length === 0)
3631
+ return 0;
3632
+ switch (fn) {
3633
+ case "sum":
3634
+ return values.reduce((a, b) => a + b, 0);
3635
+ case "avg":
3636
+ return values.reduce((a, b) => a + b, 0) / values.length;
3637
+ case "min":
3638
+ return Math.min(...values);
3639
+ case "max":
3640
+ return Math.max(...values);
3641
+ case "stddev":
3642
+ case "var": {
3643
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
3644
+ const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;
3645
+ return fn === "var" ? variance : Math.sqrt(variance);
3646
+ }
3647
+ default:
3648
+ return 0;
3649
+ }
3650
+ }
3651
+ function readField(attributes, field) {
3652
+ if (typeof attributes !== "object" || attributes === null)
3653
+ return undefined;
3654
+ return attributes[field];
3655
+ }
3656
+ // ── Edit / related / attachment translators ───────────────────
3657
+ function toApplyEditsRequest(envelope) {
3658
+ const out = {};
3659
+ if (envelope.adds && envelope.adds.length > 0) {
3660
+ out.adds = envelope.adds.map((f) => canonicalToHonuaFeature(f));
3661
+ }
3662
+ if (envelope.updates && envelope.updates.length > 0) {
3663
+ out.updates = envelope.updates.map((f) => canonicalToHonuaFeature(f));
3664
+ }
3665
+ if (envelope.deletes && envelope.deletes.length > 0) {
3666
+ const numericIds = [];
3667
+ let allNumeric = true;
3668
+ for (const id of envelope.deletes) {
3669
+ const parsed = Number(id);
3670
+ if (Number.isFinite(parsed)) {
3671
+ numericIds.push(parsed);
3672
+ }
3673
+ else {
3674
+ allNumeric = false;
3675
+ break;
3676
+ }
3677
+ }
3678
+ out.deletes = allNumeric ? numericIds : envelope.deletes.map(String).join(",");
3679
+ }
3680
+ if (envelope.rollbackOnFailure !== undefined) {
3681
+ out.rollbackOnFailure = envelope.rollbackOnFailure;
3682
+ }
3683
+ if (envelope.signal) {
3684
+ out.signal = envelope.signal;
3685
+ }
3686
+ return out;
3687
+ }
3688
+ function canonicalToHonuaFeature(feature) {
3689
+ const attributes = { ...feature.attributes };
3690
+ // GeoServices addresses updates by `OBJECTID` inside attributes; populate it
3691
+ // from the canonical `id` when the caller supplied one but did not embed it
3692
+ // in attributes themselves. Skip when an OBJECTID is already present so a
3693
+ // mismatch surfaces as a server-side validation error rather than being
3694
+ // silently overwritten.
3695
+ if (feature.id !== undefined && feature.id !== null && attributes.OBJECTID === undefined) {
3696
+ attributes.OBJECTID = feature.id;
3697
+ }
3698
+ const out = { attributes };
3699
+ if (feature.geometry !== undefined) {
3700
+ out.geometry = feature.geometry;
3701
+ }
3702
+ return out;
3703
+ }
3704
+ function canonicalEditResult(response) {
3705
+ return {
3706
+ added: (response.addResults ?? []).map(toEditOutcome),
3707
+ updated: (response.updateResults ?? []).map(toEditOutcome),
3708
+ deleted: (response.deleteResults ?? []).map(toEditOutcome),
3709
+ };
3710
+ }
3711
+ function toEditOutcome(result) {
3712
+ const out = { success: result.success };
3713
+ if (typeof result.objectId === "number" && Number.isFinite(result.objectId)) {
3714
+ out.id = result.objectId;
3715
+ }
3716
+ if (result.error) {
3717
+ out.error = { code: result.error.code, description: result.error.description };
3718
+ }
3719
+ return out;
3720
+ }
3721
+ function toRelatedRecordsRequest(request) {
3722
+ const out = { relationshipId: request.relationshipId };
3723
+ if (request.sourceIds.length > 0) {
3724
+ const numeric = [];
3725
+ let allNumeric = true;
3726
+ for (const id of request.sourceIds) {
3727
+ const parsed = Number(id);
3728
+ if (Number.isFinite(parsed))
3729
+ numeric.push(parsed);
3730
+ else {
3731
+ allNumeric = false;
3732
+ break;
3733
+ }
3734
+ }
3735
+ out.objectIds = allNumeric ? numeric : request.sourceIds.map(String).join(",");
3736
+ }
3737
+ if (request.where !== undefined)
3738
+ out.where = request.where;
3739
+ if (request.outFields && request.outFields.length > 0)
3740
+ out.outFields = [...request.outFields];
3741
+ if (request.returnGeometry !== undefined)
3742
+ out.returnGeometry = request.returnGeometry;
3743
+ if (request.signal)
3744
+ out.signal = request.signal;
3745
+ return out;
3746
+ }
3747
+ function canonicalRelatedResult(response) {
3748
+ const groups = (response.relatedRecordGroups ?? []).map((g) => ({
3749
+ sourceId: g.objectId,
3750
+ features: (g.relatedRecords ?? []).map((f) => ({
3751
+ attributes: f.attributes,
3752
+ geometry: f.geometry ?? null,
3753
+ })),
3754
+ }));
3755
+ const out = { groups };
3756
+ if (response.fields)
3757
+ out.fields = response.fields;
3758
+ return out;
3759
+ }
3760
+ function featureToGeoJsonFeature(feature) {
3761
+ const out = {
3762
+ type: "Feature",
3763
+ geometry: feature.geometry ?? null,
3764
+ properties: { ...feature.attributes },
3765
+ };
3766
+ if (feature.id !== undefined && feature.id !== null) {
3767
+ out.id = feature.id;
3768
+ }
3769
+ return out;
3770
+ }
3771
+ function editErrorFromCatch(err) {
3772
+ if (err instanceof HonuaHttpError) {
3773
+ return { code: err.statusCode, description: err.message };
3774
+ }
3775
+ if (err instanceof Error) {
3776
+ // Fall back to ad-hoc `status` / `code` / `statusCode` shapes that
3777
+ // third-party errors might carry; default to 500 when nothing fits so
3778
+ // the per-item EditOutcome still surfaces a numeric code.
3779
+ const candidate = err;
3780
+ const code = typeof candidate.statusCode === "number"
3781
+ ? candidate.statusCode
3782
+ : typeof candidate.status === "number"
3783
+ ? candidate.status
3784
+ : typeof candidate.code === "number"
3785
+ ? candidate.code
3786
+ : 500;
3787
+ return { code, description: err.message };
3788
+ }
3789
+ return { code: 500, description: String(err) };
3790
+ }
3791
+ function featureLayerAttachmentApi(descriptor, caps, layer) {
3792
+ function ensureAttachments() {
3793
+ if (!caps.has("attachments")) {
3794
+ throw new HonuaCapabilityNotSupportedError("attachments", descriptor.protocol, descriptor.id);
3795
+ }
3796
+ }
3797
+ return {
3798
+ async query(request) {
3799
+ ensureAttachments();
3800
+ const response = await layer.queryAttachments({
3801
+ ...(request?.parentIds
3802
+ ? { objectIds: request.parentIds.map(toAttachmentNumericId).filter(isFiniteNumberStrict) }
3803
+ : {}),
3804
+ ...(request?.where !== undefined ? { where: request.where } : {}),
3805
+ ...(request?.signal ? { signal: request.signal } : {}),
3806
+ });
3807
+ return (response.attachmentGroups ?? []).map((group) => ({
3808
+ parentId: group.parentObjectId,
3809
+ attachments: (group.attachmentInfos ?? []).map(toAttachmentInfo(group.parentObjectId)),
3810
+ }));
3811
+ },
3812
+ async list(parentId, options) {
3813
+ ensureAttachments();
3814
+ const response = await layer.listAttachments({
3815
+ objectId: parentId,
3816
+ ...(options?.signal ? { signal: options.signal } : {}),
3817
+ });
3818
+ const numericParent = Number(parentId);
3819
+ return (response.attachmentInfos ?? []).map(toAttachmentInfo(Number.isFinite(numericParent) ? numericParent : parentId));
3820
+ },
3821
+ async add(request) {
3822
+ ensureAttachments();
3823
+ const response = await layer.addAttachment({
3824
+ objectId: request.parentId,
3825
+ attachment: request.attachment,
3826
+ ...(request.name ? { name: request.name } : {}),
3827
+ ...(request.contentType ? { contentType: request.contentType } : {}),
3828
+ ...(request.signal ? { signal: request.signal } : {}),
3829
+ });
3830
+ return toAttachmentEditOutcome(response.addAttachmentResult, request.parentId);
3831
+ },
3832
+ async update(request) {
3833
+ ensureAttachments();
3834
+ const response = await layer.updateAttachment({
3835
+ objectId: request.parentId,
3836
+ attachmentId: request.attachmentId,
3837
+ attachment: request.attachment,
3838
+ ...(request.name ? { name: request.name } : {}),
3839
+ ...(request.contentType ? { contentType: request.contentType } : {}),
3840
+ ...(request.signal ? { signal: request.signal } : {}),
3841
+ });
3842
+ return toAttachmentEditOutcome(response.updateAttachmentResult, request.parentId);
3843
+ },
3844
+ async delete(request) {
3845
+ ensureAttachments();
3846
+ const numericIds = request.attachmentIds.map(toAttachmentNumericId).filter(isFiniteNumberStrict);
3847
+ const response = await layer.deleteAttachments({
3848
+ objectId: request.parentId,
3849
+ attachmentIds: numericIds.length === request.attachmentIds.length ? numericIds : request.attachmentIds.map(String).join(","),
3850
+ ...(request.signal ? { signal: request.signal } : {}),
3851
+ });
3852
+ return (response.deleteAttachmentResults ?? []).map((r) => toAttachmentEditOutcome(r, request.parentId));
3853
+ },
3854
+ };
3855
+ }
3856
+ function toAttachmentInfo(parentId) {
3857
+ return (info) => {
3858
+ const out = {
3859
+ id: info.id,
3860
+ parentId: info.parentObjectId ?? parentId,
3861
+ };
3862
+ if (info.name !== undefined)
3863
+ out.name = info.name;
3864
+ if (info.contentType !== undefined)
3865
+ out.contentType = info.contentType;
3866
+ if (info.size !== undefined)
3867
+ out.size = info.size;
3868
+ return out;
3869
+ };
3870
+ }
3871
+ function toAttachmentEditOutcome(result, parentId) {
3872
+ const out = { success: result.success };
3873
+ if (parentId !== undefined)
3874
+ out.parentId = parentId;
3875
+ if (typeof result.objectId === "number" && Number.isFinite(result.objectId)) {
3876
+ out.attachmentId = result.objectId;
3877
+ }
3878
+ if (result.error) {
3879
+ out.error = { code: result.error.code, description: result.error.description };
3880
+ }
3881
+ return out;
3882
+ }
3883
+ function toAttachmentNumericId(id) {
3884
+ const parsed = Number(id);
3885
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
3886
+ }
3887
+ function isFiniteNumberStrict(n) {
3888
+ return Number.isFinite(n);
3889
+ }
3890
+ // ── Re-exports for downstream tickets ─────────────────────────
3891
+ /** Sentinel: `Capabilities` set that contains every defined capability. */
3892
+ export const ALL_CAPABILITIES = new Set(CAPABILITIES);
3893
+ /** Tag a `Protocol` as "first-party-supported by this build". */
3894
+ export const FIRST_PARTY_PROTOCOLS = new Set([
3895
+ "geoservices-feature-service",
3896
+ "geoservices-map-service",
3897
+ "geoservices-image-service",
3898
+ "geoservices-geometry-service",
3899
+ "geoservices-gp-service",
3900
+ "ogc-features",
3901
+ "ogc-tiles",
3902
+ "ogc-maps",
3903
+ "ogc-records",
3904
+ "stac",
3905
+ "wfs",
3906
+ "wms",
3907
+ "wmts",
3908
+ "pmtiles",
3909
+ ]);