@honua/react 0.0.20-alpha.0 → 0.1.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/contract/discovery.d.ts +231 -0
  2. package/contract/discovery.d.ts.map +1 -0
  3. package/contract/discovery.js +554 -0
  4. package/contract/edit-sketch.d.ts +8 -0
  5. package/contract/edit-sketch.d.ts.map +1 -1
  6. package/contract/edit-sketch.js +15 -0
  7. package/contract/edit-snapping.d.ts +160 -0
  8. package/contract/edit-snapping.d.ts.map +1 -0
  9. package/contract/edit-snapping.js +675 -0
  10. package/contract/generated/projjson-v0.7-crs-validator.d.ts +35 -0
  11. package/contract/generated/projjson-v0.7-crs-validator.d.ts.map +1 -0
  12. package/contract/generated/projjson-v0.7-crs-validator.js +3941 -0
  13. package/contract/index.d.ts +12 -1
  14. package/contract/index.d.ts.map +1 -1
  15. package/contract/index.js +4 -0
  16. package/contract/pmtiles.d.ts +193 -0
  17. package/contract/pmtiles.d.ts.map +1 -0
  18. package/contract/pmtiles.js +195 -0
  19. package/contract/protocol-module.d.ts +70 -0
  20. package/contract/protocol-module.d.ts.map +1 -0
  21. package/contract/protocol-module.js +1 -0
  22. package/contract/schema-envelope.d.ts +26 -0
  23. package/contract/schema-envelope.d.ts.map +1 -0
  24. package/contract/schema-envelope.js +25 -0
  25. package/contract/schema.d.ts +582 -0
  26. package/contract/schema.d.ts.map +1 -0
  27. package/contract/schema.js +2483 -0
  28. package/contract/source-capability-support.d.ts +6 -0
  29. package/contract/source-capability-support.d.ts.map +1 -0
  30. package/contract/source-capability-support.js +154 -0
  31. package/contract/source.d.ts +57 -15
  32. package/contract/source.d.ts.map +1 -1
  33. package/contract/source.js +368 -65
  34. package/contract/types.d.ts +171 -6
  35. package/contract/types.d.ts.map +1 -1
  36. package/contract/types.js +14 -14
  37. package/core/auth/client-credentials-provider.d.ts +51 -0
  38. package/core/auth/client-credentials-provider.d.ts.map +1 -0
  39. package/core/auth/client-credentials-provider.js +115 -0
  40. package/core/auth/credential-store.d.ts +52 -0
  41. package/core/auth/credential-store.d.ts.map +1 -0
  42. package/core/auth/credential-store.js +82 -0
  43. package/core/auth/index.d.ts +23 -0
  44. package/core/auth/index.d.ts.map +1 -0
  45. package/core/auth/index.js +20 -0
  46. package/core/auth/oauth2-provider.d.ts +114 -0
  47. package/core/auth/oauth2-provider.d.ts.map +1 -0
  48. package/core/auth/oauth2-provider.js +408 -0
  49. package/core/auth/pkce.d.ts +25 -0
  50. package/core/auth/pkce.d.ts.map +1 -0
  51. package/core/auth/pkce.js +46 -0
  52. package/core/auth/static-providers.d.ts +20 -0
  53. package/core/auth/static-providers.d.ts.map +1 -0
  54. package/core/auth/static-providers.js +26 -0
  55. package/core/auth/token-endpoint.d.ts +40 -0
  56. package/core/auth/token-endpoint.d.ts.map +1 -0
  57. package/core/auth/token-endpoint.js +133 -0
  58. package/core/auth/types.d.ts +144 -0
  59. package/core/auth/types.d.ts.map +1 -0
  60. package/core/auth/types.js +8 -0
  61. package/core/bounded-json.d.ts +4 -0
  62. package/core/bounded-json.d.ts.map +1 -0
  63. package/core/bounded-json.js +38 -0
  64. package/core/cache-state.d.ts +9 -0
  65. package/core/cache-state.d.ts.map +1 -1
  66. package/core/cache-state.js +10 -0
  67. package/core/capabilities-xml.d.ts +33 -0
  68. package/core/capabilities-xml.d.ts.map +1 -0
  69. package/core/capabilities-xml.js +331 -0
  70. package/core/client.d.ts +35 -2
  71. package/core/client.d.ts.map +1 -1
  72. package/core/client.js +167 -29
  73. package/core/error-classifications.d.ts +153 -0
  74. package/core/error-classifications.d.ts.map +1 -0
  75. package/core/error-classifications.js +140 -0
  76. package/core/error-code-registry.d.ts +158 -0
  77. package/core/error-code-registry.d.ts.map +1 -0
  78. package/core/error-code-registry.js +143 -0
  79. package/core/error-envelope.d.ts +68 -0
  80. package/core/error-envelope.d.ts.map +1 -0
  81. package/core/error-envelope.js +357 -0
  82. package/core/errors.d.ts +73 -25
  83. package/core/errors.d.ts.map +1 -1
  84. package/core/errors.js +113 -46
  85. package/core/geoparquet-sql.d.ts +190 -0
  86. package/core/geoparquet-sql.d.ts.map +1 -0
  87. package/core/geoparquet-sql.js +368 -0
  88. package/core/odata-key-path.d.ts +12 -0
  89. package/core/odata-key-path.d.ts.map +1 -0
  90. package/core/odata-key-path.js +28 -0
  91. package/core/odata-write-codec.d.ts +66 -0
  92. package/core/odata-write-codec.d.ts.map +1 -0
  93. package/core/odata-write-codec.js +1083 -0
  94. package/core/odata.d.ts +59 -7
  95. package/core/odata.d.ts.map +1 -1
  96. package/core/odata.js +431 -99
  97. package/core/ogc-endpoint-layout.d.ts +70 -0
  98. package/core/ogc-endpoint-layout.d.ts.map +1 -0
  99. package/core/ogc-endpoint-layout.js +173 -0
  100. package/core/ogc-features.d.ts.map +1 -1
  101. package/core/ogc-features.js +35 -17
  102. package/core/ogc-maps.d.ts +7 -0
  103. package/core/ogc-maps.d.ts.map +1 -1
  104. package/core/ogc-maps.js +46 -7
  105. package/core/ogc-processes.d.ts +2 -1
  106. package/core/ogc-processes.d.ts.map +1 -1
  107. package/core/ogc-processes.js +33 -5
  108. package/core/ogc-records.d.ts +9 -0
  109. package/core/ogc-records.d.ts.map +1 -1
  110. package/core/ogc-records.js +78 -29
  111. package/core/ogc-tiles.d.ts +10 -1
  112. package/core/ogc-tiles.d.ts.map +1 -1
  113. package/core/ogc-tiles.js +56 -17
  114. package/core/path-utils.d.ts +9 -0
  115. package/core/path-utils.d.ts.map +1 -1
  116. package/core/path-utils.js +19 -0
  117. package/core/protocol-transport.d.ts +1 -0
  118. package/core/protocol-transport.d.ts.map +1 -1
  119. package/core/request-pipeline.d.ts.map +1 -1
  120. package/core/request-pipeline.js +1 -1
  121. package/core/spatial-filter.d.ts +8 -4
  122. package/core/spatial-filter.d.ts.map +1 -1
  123. package/core/spatial-filter.js +206 -16
  124. package/core/stac-static.d.ts +55 -0
  125. package/core/stac-static.d.ts.map +1 -0
  126. package/core/stac-static.js +386 -0
  127. package/core/stac.d.ts +12 -0
  128. package/core/stac.d.ts.map +1 -1
  129. package/core/stac.js +45 -15
  130. package/core/surfaces.d.ts +13 -0
  131. package/core/surfaces.d.ts.map +1 -1
  132. package/core/surfaces.js +127 -16
  133. package/core/types.d.ts +174 -7
  134. package/core/types.d.ts.map +1 -1
  135. package/core/wfs-capabilities.d.ts +12 -0
  136. package/core/wfs-capabilities.d.ts.map +1 -1
  137. package/core/wfs-capabilities.js +22 -3
  138. package/core/wfs.d.ts +13 -0
  139. package/core/wfs.d.ts.map +1 -1
  140. package/core/wfs.js +35 -5
  141. package/core/wms-capabilities.d.ts +56 -84
  142. package/core/wms-capabilities.d.ts.map +1 -1
  143. package/core/wms-capabilities.js +212 -509
  144. package/core/wmts-capabilities.d.ts +62 -62
  145. package/core/wmts-capabilities.d.ts.map +1 -1
  146. package/core/wmts-capabilities.js +224 -371
  147. package/esri-compat/editor.d.ts +8 -0
  148. package/esri-compat/editor.d.ts.map +1 -1
  149. package/esri-compat/editor.js +13 -0
  150. package/esri-compat/feature-layer.d.ts +2 -0
  151. package/esri-compat/feature-layer.d.ts.map +1 -1
  152. package/esri-compat/feature-layer.js +1 -0
  153. package/esri-compat/identity-manager.d.ts +43 -0
  154. package/esri-compat/identity-manager.d.ts.map +1 -1
  155. package/esri-compat/identity-manager.js +131 -1
  156. package/esri-compat/layer-list.d.ts +11 -0
  157. package/esri-compat/layer-list.d.ts.map +1 -1
  158. package/esri-compat/layer-list.js +49 -0
  159. package/esri-compat/legend.d.ts +10 -0
  160. package/esri-compat/legend.d.ts.map +1 -1
  161. package/esri-compat/legend.js +27 -0
  162. package/esri-compat/oauth-info.d.ts +15 -0
  163. package/esri-compat/oauth-info.d.ts.map +1 -1
  164. package/esri-compat/oauth-info.js +27 -0
  165. package/esri-compat/portal.d.ts +197 -0
  166. package/esri-compat/portal.d.ts.map +1 -0
  167. package/esri-compat/portal.js +251 -0
  168. package/esri-compat/renderer-objects.d.ts +40 -0
  169. package/esri-compat/renderer-objects.d.ts.map +1 -0
  170. package/esri-compat/renderer-objects.js +62 -0
  171. package/esri-compat/sketch.d.ts +43 -0
  172. package/esri-compat/sketch.d.ts.map +1 -1
  173. package/esri-compat/sketch.js +46 -0
  174. package/esri-compat/snapping.d.ts +43 -0
  175. package/esri-compat/snapping.d.ts.map +1 -0
  176. package/esri-compat/snapping.js +45 -0
  177. package/esri-compat/url.d.ts.map +1 -1
  178. package/esri-compat/url.js +1 -1
  179. package/esri-compat/widget-host.d.ts +84 -0
  180. package/esri-compat/widget-host.d.ts.map +1 -0
  181. package/esri-compat/widget-host.js +159 -0
  182. package/filter-registry/index.d.ts +145 -0
  183. package/filter-registry/index.d.ts.map +1 -0
  184. package/filter-registry/index.js +531 -0
  185. package/generated-app/errors.d.ts +18 -2
  186. package/generated-app/errors.d.ts.map +1 -1
  187. package/generated-app/errors.js +44 -11
  188. package/map/automatic-mount-integration.d.ts +134 -0
  189. package/map/automatic-mount-integration.d.ts.map +1 -0
  190. package/map/automatic-mount-integration.js +305 -0
  191. package/map/automatic-source-strategy.d.ts +125 -0
  192. package/map/automatic-source-strategy.d.ts.map +1 -0
  193. package/map/automatic-source-strategy.js +494 -0
  194. package/map/data-to-map-bridge.d.ts +293 -0
  195. package/map/data-to-map-bridge.d.ts.map +1 -0
  196. package/map/data-to-map-bridge.js +817 -0
  197. package/map/geojson-projection.d.ts +33 -0
  198. package/map/geojson-projection.d.ts.map +1 -0
  199. package/map/geojson-projection.js +118 -0
  200. package/map/index.d.ts +14 -0
  201. package/map/index.d.ts.map +1 -1
  202. package/map/index.js +8 -0
  203. package/map/raster-source-spec.d.ts +30 -0
  204. package/map/raster-source-spec.d.ts.map +1 -0
  205. package/map/raster-source-spec.js +190 -0
  206. package/map/raster-source-strategy.d.ts +65 -0
  207. package/map/raster-source-strategy.d.ts.map +1 -0
  208. package/map/raster-source-strategy.js +232 -0
  209. package/map/source-to-maplibre.d.ts +91 -0
  210. package/map/source-to-maplibre.d.ts.map +1 -0
  211. package/map/source-to-maplibre.js +454 -0
  212. package/map/temporal-playback.d.ts +139 -0
  213. package/map/temporal-playback.d.ts.map +1 -0
  214. package/map/temporal-playback.js +247 -0
  215. package/package.json +19 -1
  216. package/query-planner/canonical.d.ts +7 -0
  217. package/query-planner/canonical.d.ts.map +1 -0
  218. package/query-planner/canonical.js +145 -0
  219. package/query-planner/cql2-json.d.ts +17 -0
  220. package/query-planner/cql2-json.d.ts.map +1 -0
  221. package/query-planner/cql2-json.js +646 -0
  222. package/query-planner/diagnostics.d.ts +35 -0
  223. package/query-planner/diagnostics.d.ts.map +1 -0
  224. package/query-planner/diagnostics.js +373 -0
  225. package/query-planner/duckdb.d.ts +79 -0
  226. package/query-planner/duckdb.d.ts.map +1 -0
  227. package/query-planner/duckdb.js +758 -0
  228. package/query-planner/execution-context.d.ts +5 -0
  229. package/query-planner/execution-context.d.ts.map +1 -0
  230. package/query-planner/execution-context.js +50 -0
  231. package/query-planner/executor.d.ts +5 -0
  232. package/query-planner/executor.d.ts.map +1 -0
  233. package/query-planner/executor.js +223 -0
  234. package/query-planner/geoservices.d.ts +91 -0
  235. package/query-planner/geoservices.d.ts.map +1 -0
  236. package/query-planner/geoservices.js +667 -0
  237. package/query-planner/grpc.d.ts +100 -0
  238. package/query-planner/grpc.d.ts.map +1 -0
  239. package/query-planner/grpc.js +736 -0
  240. package/query-planner/index.d.ts +44 -0
  241. package/query-planner/index.d.ts.map +1 -0
  242. package/query-planner/index.js +30 -0
  243. package/query-planner/ir.d.ts +20 -0
  244. package/query-planner/ir.d.ts.map +1 -0
  245. package/query-planner/ir.js +417 -0
  246. package/query-planner/local-aggregate.d.ts +5 -0
  247. package/query-planner/local-aggregate.d.ts.map +1 -0
  248. package/query-planner/local-aggregate.js +126 -0
  249. package/query-planner/odata.d.ts +48 -0
  250. package/query-planner/odata.d.ts.map +1 -0
  251. package/query-planner/odata.js +662 -0
  252. package/query-planner/ogc-compiler.d.ts +55 -0
  253. package/query-planner/ogc-compiler.d.ts.map +1 -0
  254. package/query-planner/ogc-compiler.js +480 -0
  255. package/query-planner/ogc-features.d.ts +53 -0
  256. package/query-planner/ogc-features.d.ts.map +1 -0
  257. package/query-planner/ogc-features.js +617 -0
  258. package/query-planner/planner.d.ts +17 -0
  259. package/query-planner/planner.d.ts.map +1 -0
  260. package/query-planner/planner.js +818 -0
  261. package/query-planner/resource.d.ts +94 -0
  262. package/query-planner/resource.d.ts.map +1 -0
  263. package/query-planner/resource.js +526 -0
  264. package/query-planner/semantic-canonical.d.ts +15 -0
  265. package/query-planner/semantic-canonical.d.ts.map +1 -0
  266. package/query-planner/semantic-canonical.js +62 -0
  267. package/query-planner/semantic-compat.d.ts +10 -0
  268. package/query-planner/semantic-compat.d.ts.map +1 -0
  269. package/query-planner/semantic-compat.js +35 -0
  270. package/query-planner/semantic-compiler.d.ts +64 -0
  271. package/query-planner/semantic-compiler.d.ts.map +1 -0
  272. package/query-planner/semantic-compiler.js +81 -0
  273. package/query-planner/semantic-literals.d.ts +30 -0
  274. package/query-planner/semantic-literals.d.ts.map +1 -0
  275. package/query-planner/semantic-literals.js +133 -0
  276. package/query-planner/semantic-types.d.ts +265 -0
  277. package/query-planner/semantic-types.d.ts.map +1 -0
  278. package/query-planner/semantic-types.js +1 -0
  279. package/query-planner/semantic.d.ts +54 -0
  280. package/query-planner/semantic.d.ts.map +1 -0
  281. package/query-planner/semantic.js +1421 -0
  282. package/query-planner/types.d.ts +560 -0
  283. package/query-planner/types.d.ts.map +1 -0
  284. package/query-planner/types.js +49 -0
  285. package/query-planner/wfs-semantic.d.ts +58 -0
  286. package/query-planner/wfs-semantic.d.ts.map +1 -0
  287. package/query-planner/wfs-semantic.js +547 -0
  288. package/query-planner/wfs.d.ts +6 -0
  289. package/query-planner/wfs.d.ts.map +1 -0
  290. package/query-planner/wfs.js +71 -0
  291. package/react/external-map.d.ts +67 -0
  292. package/react/external-map.d.ts.map +1 -0
  293. package/react/external-map.js +63 -0
  294. package/react/honua-map.d.ts.map +1 -1
  295. package/react/honua-map.js +3 -2
  296. package/react/honua-source-layer.d.ts +110 -0
  297. package/react/honua-source-layer.d.ts.map +1 -0
  298. package/react/honua-source-layer.js +101 -0
  299. package/react/hooks.d.ts +28 -0
  300. package/react/hooks.d.ts.map +1 -1
  301. package/react/hooks.js +26 -4
  302. package/react/index.d.ts +8 -0
  303. package/react/index.d.ts.map +1 -1
  304. package/react/index.js +5 -0
  305. package/react/query-cache.d.ts +26 -4
  306. package/react/query-cache.d.ts.map +1 -1
  307. package/react/query-cache.js +51 -8
  308. package/react/selection.d.ts +177 -0
  309. package/react/selection.d.ts.map +1 -0
  310. package/react/selection.js +0 -0
  311. package/react/use-mounted-source.d.ts +69 -0
  312. package/react/use-mounted-source.d.ts.map +1 -0
  313. package/react/use-mounted-source.js +346 -0
  314. package/runtime/edit-snapping.d.ts +107 -0
  315. package/runtime/edit-snapping.d.ts.map +1 -0
  316. package/runtime/edit-snapping.js +190 -0
  317. package/runtime/errors.d.ts +3 -2
  318. package/runtime/errors.d.ts.map +1 -1
  319. package/runtime/errors.js +19 -2
  320. package/runtime/index.d.ts +8 -2
  321. package/runtime/index.d.ts.map +1 -1
  322. package/runtime/index.js +4 -4
  323. package/runtime/load-package.d.ts.map +1 -1
  324. package/runtime/load-package.js +8 -0
  325. package/runtime/map-package-validation.d.ts +0 -13
  326. package/runtime/map-package-validation.d.ts.map +1 -1
  327. package/runtime/map-package-validation.js +0 -12
  328. package/runtime/map-package.d.ts +1 -1
  329. package/runtime/map-package.d.ts.map +1 -1
  330. package/runtime/maplibre-renderer.d.ts +41 -0
  331. package/runtime/maplibre-renderer.d.ts.map +1 -0
  332. package/runtime/maplibre-renderer.js +322 -0
  333. package/runtime/pmtiles-protocol.d.ts +77 -0
  334. package/runtime/pmtiles-protocol.d.ts.map +1 -0
  335. package/runtime/pmtiles-protocol.js +120 -0
  336. package/runtime/query-tiles.d.ts +2 -1
  337. package/runtime/query-tiles.d.ts.map +1 -1
  338. package/runtime/query-tiles.js +6 -2
  339. package/runtime/source-bridge.d.ts +4 -4
  340. package/runtime/source-bridge.d.ts.map +1 -1
  341. package/runtime/source-bridge.js +36 -56
  342. package/runtime/style-interactions.d.ts +3 -2
  343. package/runtime/style-interactions.d.ts.map +1 -1
  344. package/runtime/style-interactions.js +11 -3
  345. package/runtime/terra-draw-sketch.d.ts +275 -0
  346. package/runtime/terra-draw-sketch.d.ts.map +1 -0
  347. package/runtime/terra-draw-sketch.js +404 -0
  348. package/source-capability-registry.d.ts +1 -0
  349. package/source-capability-registry.js +1 -0
  350. package/source-capability-types.d.ts +186 -0
  351. package/source-capability-types.js +7 -0
  352. package/style/index.d.ts +2 -0
  353. package/style/index.d.ts.map +1 -1
  354. package/style/index.js +2 -0
  355. package/style/renderers.d.ts +326 -0
  356. package/style/renderers.d.ts.map +1 -0
  357. package/style/renderers.js +517 -0
  358. package/webmap/convert-renderer.d.ts +24 -3
  359. package/webmap/convert-renderer.d.ts.map +1 -1
  360. package/webmap/convert-renderer.js +93 -112
  361. package/webmap/index.d.ts +1 -1
  362. package/webmap/index.d.ts.map +1 -1
  363. package/webmap/index.js +1 -1
@@ -18,12 +18,15 @@ import { HonuaOgcCollectionMap, HonuaOgcMaps } from "../core/ogc-maps.js";
18
18
  import { HonuaOgcRecordCollection } from "../core/ogc-records.js";
19
19
  import { HonuaOgcTiles, HonuaOgcTileset } from "../core/ogc-tiles.js";
20
20
  import { trimTrailingSlashes } from "../core/path-utils.js";
21
+ import { HonuaStacStaticCatalog } from "../core/stac-static.js";
21
22
  import { HonuaStacSearch } from "../core/stac.js";
22
23
  import { HonuaFeatureLayer, HonuaGeometryService, HonuaGeoprocessingService, HonuaImageService, HonuaMapLayer, HonuaMapService, HonuaOgcFeatureCollection, HonuaOgcFeatures, } from "../core/surfaces.js";
23
24
  import { UNSUPPORTED_FES, compileSpatialFilter, compileWhere, geoJsonGeometryToGml, serializeFes, } from "../core/wfs-filter.js";
24
- import { HonuaWfs, HonuaWfsFeatureType } from "../core/wfs.js";
25
+ import { HonuaWfsFeatureType } from "../core/wfs.js";
25
26
  import { HonuaWms, HonuaWmsLayer, parseWmsLayerNames } from "../core/wms.js";
26
27
  import { HonuaWmts, HonuaWmtsLayer, HonuaWmtsTileset } from "../core/wmts.js";
28
+ import { pmtilesProtocolModule } from "./pmtiles.js";
29
+ import { addCapabilitySupport, normalizeCapabilityDescriptor } from "./source-capability-support.js";
27
30
  import { CAPABILITIES, PROTOCOL_DEFAULT_CAPABILITIES, } from "./types.js";
28
31
  /**
29
32
  * Construct a protocol-neutral `Dataset` over a `HonuaClient`.
@@ -83,7 +86,8 @@ export function createDataset(options) {
83
86
  const descriptors = new Map();
84
87
  const handles = new Map();
85
88
  let compatibilityPromise;
86
- for (const descriptor of sources) {
89
+ for (const input of sources) {
90
+ const descriptor = normalizeCapabilityDescriptor(input);
87
91
  if (descriptors.has(descriptor.id)) {
88
92
  throw new Error(`createDataset: duplicate source id "${descriptor.id}"`);
89
93
  }
@@ -93,13 +97,14 @@ export function createDataset(options) {
93
97
  const cached = handles.get(descriptor.id);
94
98
  if (cached)
95
99
  return cached;
96
- const built = buildBuiltInSource(descriptor, client, policy) ??
97
- resolveSource?.(descriptor, { client, capabilityPolicy: policy });
100
+ const builtIn = buildBuiltInSource(descriptor, client, policy);
101
+ const built = builtIn ?? resolveSource?.(descriptor, { client, capabilityPolicy: policy });
98
102
  if (!built) {
99
103
  throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
100
104
  }
101
- handles.set(descriptor.id, built);
102
- return built;
105
+ const source = builtIn ?? addCapabilitySupport(built, descriptor);
106
+ handles.set(descriptor.id, source);
107
+ return source;
103
108
  }
104
109
  return {
105
110
  id,
@@ -133,6 +138,15 @@ export function createDataset(options) {
133
138
  // ── Built-in resolver ─────────────────────────────────────────
134
139
  function buildBuiltInSource(descriptor, client, policy) {
135
140
  switch (descriptor.protocol) {
141
+ case "grpc":
142
+ // Honua gRPC is a transport-selectable fast path over the same
143
+ // canonical FeatureServer semantics, not a distinct adapter: the
144
+ // returned `HonuaFeatureLayer` transparently routes `queryFeatures`
145
+ // over gRPC-Web when `client.transport === "grpc-web"` (see
146
+ // `docs/protocol-capability-matrix.md`, "gRPC FeatureService"). The
147
+ // discovered descriptor's narrower `capabilities` (verified through a
148
+ // live gRPC parity probe in `connect-grpc.ts`) still gate execution.
149
+ return geoServicesFeatureSource(descriptor, client, policy);
136
150
  case "geoservices-feature-service":
137
151
  return geoServicesFeatureSource(descriptor, client, policy);
138
152
  case "geoservices-map-service":
@@ -161,6 +175,8 @@ function buildBuiltInSource(descriptor, client, policy) {
161
175
  return wfsSource(descriptor, client, policy);
162
176
  case "odata":
163
177
  return odataSource(descriptor, client, policy);
178
+ case "pmtiles":
179
+ return pmtilesSource(descriptor, client, policy);
164
180
  default:
165
181
  return undefined;
166
182
  }
@@ -192,9 +208,16 @@ function buildBuiltInSource(descriptor, client, policy) {
192
208
  * ```
193
209
  */
194
210
  export function geoServicesFeatureSource(descriptor, client, policy) {
211
+ descriptor = normalizeCapabilityDescriptor(descriptor);
195
212
  const { serviceId, layerId } = requireFeatureServiceLocator(descriptor);
196
213
  const layer = new HonuaFeatureLayer({ client, serviceId, layerId });
197
- const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-feature-service"];
214
+ // Indexed by the descriptor's own protocol (not hardcoded to
215
+ // "geoservices-feature-service") so a `protocol: "grpc"` descriptor built
216
+ // without explicit capabilities falls back to the narrower gRPC default
217
+ // set rather than silently inheriting REST-only capabilities (`pbf`,
218
+ // `sql`, `attachments`, `queryRelated`) the gRPC FeatureService RPC
219
+ // surface does not support.
220
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES[descriptor.protocol];
198
221
  const adapterRegistry = {
199
222
  "geoservices-feature-service": layer,
200
223
  };
@@ -283,6 +306,7 @@ export function geoServicesFeatureSource(descriptor, client, policy) {
283
306
  * ```
284
307
  */
285
308
  export function geoServicesMapServiceSource(descriptor, client, policy) {
309
+ descriptor = normalizeCapabilityDescriptor(descriptor);
286
310
  const { serviceId, layerId } = requireMapServiceLocator(descriptor);
287
311
  const service = new HonuaMapService({ client, serviceId });
288
312
  const layer = new HonuaMapLayer({ client, serviceId, layerId });
@@ -377,9 +401,15 @@ export function geoServicesMapServiceSource(descriptor, client, policy) {
377
401
  * ```
378
402
  */
379
403
  export function ogcFeaturesSource(descriptor, client, policy) {
404
+ descriptor = normalizeCapabilityDescriptor(descriptor);
380
405
  const { collectionId } = requireOgcLocator(descriptor);
381
- const collection = new HonuaOgcFeatureCollection({ client, collectionId });
382
- const root = new HonuaOgcFeatures({ client });
406
+ const layoutMode = ogcFeaturesLayoutMode(descriptor.locator.layout);
407
+ const collection = new HonuaOgcFeatureCollection({
408
+ client,
409
+ collectionId,
410
+ ...(layoutMode ? { layout: layoutMode } : {}),
411
+ });
412
+ const root = new HonuaOgcFeatures({ client, ...(layoutMode ? { layout: layoutMode } : {}) });
383
413
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-features"];
384
414
  const adapterRegistry = {
385
415
  "ogc-features": collection,
@@ -591,6 +621,7 @@ export function ogcFeaturesSource(descriptor, client, policy) {
591
621
  * ```
592
622
  */
593
623
  export function geoServicesImageSource(descriptor, client, policy) {
624
+ descriptor = normalizeCapabilityDescriptor(descriptor);
594
625
  const { serviceId } = requireImageServiceLocator(descriptor);
595
626
  const service = new HonuaImageService({ client, serviceId });
596
627
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-image-service"];
@@ -700,6 +731,7 @@ export function geoServicesImageSource(descriptor, client, policy) {
700
731
  * ```
701
732
  */
702
733
  export function geoServicesGeometryServiceSource(descriptor, client, policy) {
734
+ descriptor = normalizeCapabilityDescriptor(descriptor);
703
735
  const service = new HonuaGeometryService({ client });
704
736
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-geometry-service"];
705
737
  const adapterRegistry = {
@@ -722,6 +754,7 @@ export function geoServicesGeometryServiceSource(descriptor, client, policy) {
722
754
  * ```
723
755
  */
724
756
  export function geoServicesGPServiceSource(descriptor, client, policy) {
757
+ descriptor = normalizeCapabilityDescriptor(descriptor);
725
758
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["geoservices-gp-service"];
726
759
  const { serviceId, taskName } = requireGPServiceLocator(descriptor, caps);
727
760
  const service = new HonuaGeoprocessingService({
@@ -749,20 +782,66 @@ export function geoServicesGPServiceSource(descriptor, client, policy) {
749
782
  * ```
750
783
  */
751
784
  export function ogcTilesSource(descriptor, client, policy) {
785
+ descriptor = normalizeCapabilityDescriptor(descriptor);
752
786
  const { collectionId, tileMatrixSetId } = requireOgcTilesLocator(descriptor);
753
787
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-tiles"];
788
+ const basePath = descriptor.locator.basePath;
754
789
  // Descriptors without a tileMatrixSetId cannot construct a usable
755
790
  // HonuaOgcTileset (every tile route requires `tileMatrixSetId`). Expose
756
791
  // the root HonuaOgcTiles adapter instead so callers can discover the
757
792
  // tilesets the server advertises for the collection before binding one.
793
+ // A `connect()`-discovered third-party root threads its basePath so tile
794
+ // routes resolve against the advertised layout, not the `/ogc/tiles` facade.
758
795
  const adapter = tileMatrixSetId !== undefined && tileMatrixSetId !== ""
759
- ? new HonuaOgcTileset({ client, collectionId, tileMatrixSetId })
760
- : new HonuaOgcTiles({ client });
796
+ ? new HonuaOgcTileset({ client, collectionId, tileMatrixSetId, ...(basePath !== undefined ? { basePath } : {}) })
797
+ : new HonuaOgcTiles({ client, ...(basePath !== undefined ? { basePath } : {}) });
761
798
  const adapterRegistry = {
762
799
  "ogc-tiles": adapter,
763
800
  };
764
801
  return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
765
802
  }
803
+ // ── PMTiles archive ───────────────────────────────────────────
804
+ /**
805
+ * Tiles-only Source adapter for a PMTiles archive (`protocol: "pmtiles"`).
806
+ *
807
+ * PMTiles archives are immutable single-file tile stores, so the canonical
808
+ * query family throws `HonuaCapabilityNotSupportedError` (there is no
809
+ * feature-query surface on an archive). Archive metadata — bounds, min/max
810
+ * zoom, and vector layer names — is inspected through the typed escape hatch:
811
+ * `Source.protocol("pmtiles").describe()`. Rendering integrations register the
812
+ * `pmtiles://` protocol on the map (see `@honua/sdk-js/runtime`) and reference
813
+ * the archive as a MapLibre source `url`.
814
+ *
815
+ * `locator.url` may carry a leading `pmtiles://` scheme (the MapLibre form) or
816
+ * be a bare archive URL; both resolve to the same archive.
817
+ *
818
+ * This builds the escape-hatch adapter through {@link pmtilesProtocolModule}
819
+ * (issue #538) rather than constructing `HonuaPmtilesArchive` directly, so
820
+ * the built-in wiring runs the same public `ProtocolModule` seam a module
821
+ * registered through `HonuaPluginRegistry` uses
822
+ * (`pmtilesProtocolPlugin`, `@honua/sdk-js/plugin`).
823
+ *
824
+ * @example
825
+ * ```ts
826
+ * const archive = dataset.source("basemap")!.protocol("pmtiles");
827
+ * const info = await archive!.describe();
828
+ * console.log(info.bounds, info.vectorLayers.map((layer) => layer.id));
829
+ * ```
830
+ */
831
+ export function pmtilesSource(descriptor, _client, policy) {
832
+ descriptor = normalizeCapabilityDescriptor(descriptor);
833
+ const discovered = pmtilesProtocolModule().discover(descriptor);
834
+ if (discovered instanceof Promise) {
835
+ // PMTiles discovery is always synchronous (the reader opens lazily on the
836
+ // handle's own first `describe()` call); a module that started returning
837
+ // a promise would be a breaking change to this built-in's own contract.
838
+ throw new Error("pmtiles: built-in source construction requires synchronous protocol-module discovery");
839
+ }
840
+ const adapterRegistry = {
841
+ pmtiles: discovered.adapter,
842
+ };
843
+ return makeSource(descriptor, discovered.capabilities, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
844
+ }
766
845
  // ── OGC API Maps ──────────────────────────────────────────────
767
846
  /**
768
847
  * Render-only Source adapter for OGC API Maps. Same shape as the Tiles
@@ -776,13 +855,18 @@ export function ogcTilesSource(descriptor, client, policy) {
776
855
  * ```
777
856
  */
778
857
  export function ogcMapsSource(descriptor, client, policy) {
779
- const root = new HonuaOgcMaps({ client });
858
+ descriptor = normalizeCapabilityDescriptor(descriptor);
859
+ const basePath = descriptor.locator.basePath;
860
+ // A `connect()`-discovered third-party root threads its basePath so render
861
+ // routes resolve against the advertised layout, not the `/ogc/maps` facade.
862
+ const root = new HonuaOgcMaps({ client, ...(basePath !== undefined ? { basePath } : {}) });
780
863
  const adapterRegistry = {};
781
864
  if (descriptor.locator.collectionId !== undefined) {
782
865
  adapterRegistry["ogc-maps"] = new HonuaOgcCollectionMap({
783
866
  client,
784
867
  collectionId: descriptor.locator.collectionId,
785
868
  styleId: descriptor.locator.styleId,
869
+ ...(basePath !== undefined ? { basePath } : {}),
786
870
  });
787
871
  }
788
872
  else {
@@ -806,8 +890,14 @@ export function ogcMapsSource(descriptor, client, policy) {
806
890
  * ```
807
891
  */
808
892
  export function ogcRecordsSource(descriptor, client, policy) {
893
+ descriptor = normalizeCapabilityDescriptor(descriptor);
809
894
  const { collectionId } = requireOgcRecordsLocator(descriptor);
810
- const collection = new HonuaOgcRecordCollection({ client, collectionId });
895
+ const basePath = descriptor.locator.basePath;
896
+ const collection = new HonuaOgcRecordCollection({
897
+ client,
898
+ collectionId,
899
+ ...(basePath !== undefined ? { basePath } : {}),
900
+ });
811
901
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES["ogc-records"];
812
902
  const adapterRegistry = {
813
903
  "ogc-records": collection,
@@ -919,6 +1009,18 @@ export function ogcRecordsSource(descriptor, client, policy) {
919
1009
  * ```
920
1010
  */
921
1011
  export function wmsSource(descriptor, client, policy) {
1012
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1013
+ const advertisedCaps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wms;
1014
+ if (typeof descriptor.locator.serviceId !== "string" || descriptor.locator.serviceId.length === 0) {
1015
+ if (descriptor.locator.raster?.kind !== "wms-kvp")
1016
+ requireWmsLocator(descriptor);
1017
+ // Raw third-party discovery is renderable through the existing descriptor
1018
+ // → MapLibre raster projection. The existing canonical FeatureInfo adapter
1019
+ // is Honua-service-id based, so keep that surface fail-closed here.
1020
+ const caps = new Set([...advertisedCaps].filter((capability) => capability !== "query"));
1021
+ descriptor = { ...descriptor, capabilities: caps };
1022
+ return makeSource(descriptor, caps, policy, {}, unsupportedFeatureSurface(descriptor));
1023
+ }
922
1024
  const { serviceId } = requireWmsLocator(descriptor);
923
1025
  const layerName = descriptor.locator.typeName;
924
1026
  const styleId = descriptor.locator.styleId;
@@ -941,7 +1043,7 @@ export function wmsSource(descriptor, client, policy) {
941
1043
  layerOpts.defaultStyleId = styleId;
942
1044
  adapterRegistry["wms-layer"] = new HonuaWmsLayer(layerOpts);
943
1045
  }
944
- const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wms;
1046
+ const caps = advertisedCaps;
945
1047
  return makeSource(descriptor, caps, policy, adapterRegistry, {
946
1048
  async query(request) {
947
1049
  ensureCapability(descriptor, caps, "query");
@@ -1019,6 +1121,16 @@ export function wmsSource(descriptor, client, policy) {
1019
1121
  * ```
1020
1122
  */
1021
1123
  export function wmtsSource(descriptor, client, policy) {
1124
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1125
+ const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wmts;
1126
+ if (typeof descriptor.locator.serviceId !== "string" || descriptor.locator.serviceId.length === 0) {
1127
+ if (descriptor.locator.raster?.kind !== "wmts-kvp" && descriptor.locator.raster?.kind !== "wmts-template") {
1128
+ requireWmtsLocator(descriptor);
1129
+ }
1130
+ // Raw WMTS sources use the validated descriptor binding in the existing
1131
+ // MapLibre projection; no second protocol wire adapter is introduced.
1132
+ return makeSource(descriptor, caps, policy, {}, unsupportedFeatureSurface(descriptor));
1133
+ }
1022
1134
  const { serviceId } = requireWmtsLocator(descriptor);
1023
1135
  const layerName = descriptor.locator.typeName;
1024
1136
  const tileMatrixSetId = descriptor.locator.tileMatrixSetId ?? "WebMercatorQuad";
@@ -1041,7 +1153,6 @@ export function wmtsSource(descriptor, client, policy) {
1041
1153
  tileMatrixSetId,
1042
1154
  });
1043
1155
  }
1044
- const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wmts;
1045
1156
  return makeSource(descriptor, caps, policy, adapterRegistry, unsupportedFeatureSurface(descriptor));
1046
1157
  }
1047
1158
  // ── STAC API ──────────────────────────────────────────────────
@@ -1074,7 +1185,17 @@ export function wmtsSource(descriptor, client, policy) {
1074
1185
  * ```
1075
1186
  */
1076
1187
  export function stacSearchSource(descriptor, client, policy) {
1077
- const stac = new HonuaStacSearch({ client });
1188
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1189
+ // Endpoint-layout selection. `stac-api` treats `locator.url` as a raw STAC
1190
+ // API root (search / collections mounted directly under it) rather than the
1191
+ // Honua `/stac` facade. `stac-static` reads a static catalog.json tree with
1192
+ // no search endpoint. Omitted / `honua-facade` keeps the `/stac` facade.
1193
+ const layout = descriptor.locator.layout;
1194
+ const stacBasePath = layout === "stac-api" ? "" : undefined;
1195
+ const stac = new HonuaStacSearch({ client, ...(stacBasePath !== undefined ? { basePath: stacBasePath } : {}) });
1196
+ const staticCatalog = layout === "stac-static"
1197
+ ? new HonuaStacStaticCatalog(client, descriptor.locator.url, descriptor.locator.stacStatic)
1198
+ : undefined;
1078
1199
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.stac;
1079
1200
  const collectionScope = descriptor.locator.collectionId;
1080
1201
  const adapterRegistry = {
@@ -1087,7 +1208,9 @@ export function stacSearchSource(descriptor, client, policy) {
1087
1208
  throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
1088
1209
  }
1089
1210
  const stacRequest = toStacRequest(request, collectionScope);
1090
- const response = await stac.search(stacRequest);
1211
+ const response = staticCatalog
1212
+ ? await staticCatalog.search(toStacStaticParams(stacRequest))
1213
+ : await stac.search(stacRequest);
1091
1214
  const features = (response.features ?? []).map((toTypedFeatureFromStac));
1092
1215
  const totalCount = response.numberMatched ?? response.context?.matched;
1093
1216
  const exceededTransferLimit = totalCount !== undefined && features.length < totalCount;
@@ -1100,6 +1223,12 @@ export function stacSearchSource(descriptor, client, policy) {
1100
1223
  async queryAll(request) {
1101
1224
  ensureCapability(descriptor, caps, "query");
1102
1225
  const limit = request?.pagination?.limit;
1226
+ if (staticCatalog) {
1227
+ const response = await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope)));
1228
+ const typed = (response.features ?? []).map((toTypedFeatureFromStac));
1229
+ const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
1230
+ return { features, exceededTransferLimit, totalCount: features.length };
1231
+ }
1103
1232
  const items = await stac.searchAll({
1104
1233
  ...toStacRequest(request, collectionScope),
1105
1234
  ...withPagingBounds({}, limit),
@@ -1121,6 +1250,16 @@ export function stacSearchSource(descriptor, client, policy) {
1121
1250
  async *stream(request) {
1122
1251
  ensureCapability(descriptor, caps, "stream");
1123
1252
  const limit = request?.pagination?.limit;
1253
+ if (staticCatalog) {
1254
+ // A static catalog has no server-side paging; yield the whole
1255
+ // filtered set as one page.
1256
+ const response = await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope)));
1257
+ yield {
1258
+ features: (response.features ?? []).map((toTypedFeatureFromStac)),
1259
+ exceededTransferLimit: false,
1260
+ };
1261
+ return;
1262
+ }
1124
1263
  const stream = stac.searchStream({
1125
1264
  ...toStacRequest(request, collectionScope),
1126
1265
  pageSize: limit !== undefined ? Math.max(1, limit) : undefined,
@@ -1140,10 +1279,12 @@ export function stacSearchSource(descriptor, client, policy) {
1140
1279
  // routes through `withPagingBounds` so the underlying searchAll
1141
1280
  // fetches at most `limit + 1` rows, never a full-catalog scan.
1142
1281
  const limit = request?.pagination?.limit;
1143
- const items = await stac.searchAll({
1144
- ...toStacRequest(request, collectionScope),
1145
- ...withPagingBounds({}, limit),
1146
- });
1282
+ const items = staticCatalog
1283
+ ? ((await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope)))).features ?? [])
1284
+ : await stac.searchAll({
1285
+ ...toStacRequest(request, collectionScope),
1286
+ ...withPagingBounds({}, limit),
1287
+ });
1147
1288
  const ids = [];
1148
1289
  for (const item of items) {
1149
1290
  if (item.id !== undefined && item.id !== null) {
@@ -1201,8 +1342,9 @@ const DEFAULT_WFS_GEOMETRY_PROPERTY = "the_geom";
1201
1342
  * ```
1202
1343
  */
1203
1344
  export function wfsSource(descriptor, client, policy) {
1345
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1204
1346
  const { url, typeName, featureNamespace, srsName: wfsSrsName } = requireWfsLocator(descriptor);
1205
- const root = new HonuaWfs({ client, endpointUrl: url });
1347
+ const root = client.wfs(url);
1206
1348
  const featureType = new HonuaWfsFeatureType({ root, typeName });
1207
1349
  const caps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wfs;
1208
1350
  void policy;
@@ -1435,6 +1577,10 @@ export function wfsSource(descriptor, client, policy) {
1435
1577
  summary = { totalInserted: 0, totalUpdated: 0, totalDeleted: 0, insertResults: [] };
1436
1578
  }
1437
1579
  else {
1580
+ // Ensure a caller-cache hit still resolves the advertised Transaction
1581
+ // POST URL before sending edits. Fresh connect discovery reuses this
1582
+ // same root, so the capabilities promise is already settled there.
1583
+ await root.capabilities(envelope.signal ? { signal: envelope.signal } : undefined);
1438
1584
  const filtered = { ...envelope, updates: validUpdates };
1439
1585
  const body = buildTransactionBody(typeName, filtered, featureNamespace, wfsSrsName);
1440
1586
  const transactionOptions = { body };
@@ -1891,21 +2037,12 @@ function canonicalEditResultFromTransaction(envelope, summary, malformedUpdateIn
1891
2037
  }));
1892
2038
  return { added, updated, deleted };
1893
2039
  }
1894
- // ── OData entity set ──────────────────────────────────────────
1895
- /**
1896
- * Build a `Source` over an OData v4 entity set. The `Query` translation
1897
- * lowers `where` / `outFields` / `orderBy` / pagination / spatial filter
1898
- * onto OData's `$`-prefixed query options. Dialect-specific operations
1899
- * (`$batch`, `$apply`, `$search`, `$deltatoken`) live behind
1900
- * `Source.protocol("odata")` on the returned `HonuaOdataEntitySet`.
1901
- *
1902
- * `$metadata` is fetched lazily on the first call that needs it and
1903
- * cached on the entity-set instance. The fetched capability annotations
1904
- * are intersected with the descriptor's declared `Capabilities` set the
1905
- * first time a capability-gated method is invoked — this is the
1906
- * precedent for the metadata-driven downgrade pattern referenced in
1907
- * `docs/shared-client-contract.md`.
1908
- */
2040
+ let odataWriteCodecPromise;
2041
+ /** Load the opt-in codec once; legacy sources never evaluate this import. */
2042
+ function loadOdataWriteCodec() {
2043
+ odataWriteCodecPromise ??= import("../core/odata-write-codec.js");
2044
+ return odataWriteCodecPromise;
2045
+ }
1909
2046
  /**
1910
2047
  * Adapter factory for an OData v4 entity set.
1911
2048
  *
@@ -1933,11 +2070,16 @@ function canonicalEditResultFromTransaction(envelope, summary, malformedUpdateIn
1933
2070
  * });
1934
2071
  * ```
1935
2072
  */
1936
- export function odataSource(descriptor, client, policy) {
2073
+ export function odataSource(descriptor, client, policy, options = {}) {
2074
+ descriptor = normalizeCapabilityDescriptor(descriptor);
1937
2075
  const { entitySet, basePath } = requireOdataLocator(descriptor);
1938
2076
  const entity = new HonuaOdataEntitySet({ client, entitySet, basePath });
1939
2077
  const declaredCaps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.odata;
1940
2078
  const negotiation = new OdataCapabilityNegotiator(entity, declaredCaps);
2079
+ const writeEncoding = options.writeEncoding ?? "legacy";
2080
+ if (writeEncoding !== "legacy" && writeEncoding !== "lossless-json") {
2081
+ throw new TypeError('odata: writeEncoding must be "legacy" or "lossless-json"');
2082
+ }
1941
2083
  const adapterRegistry = {
1942
2084
  odata: entity,
1943
2085
  };
@@ -2040,6 +2182,9 @@ export function odataSource(descriptor, client, policy) {
2040
2182
  ensureCapability(descriptor, declaredCaps, "applyEdits");
2041
2183
  await negotiation.ensureAdvertised(descriptor, "applyEdits");
2042
2184
  const keyFields = await negotiation.keyFields(descriptor.id);
2185
+ const prepared = writeEncoding === "lossless-json"
2186
+ ? prepareLosslessOdataEdits(entity, envelope, keyFields, await negotiation.metadata(), await loadOdataWriteCodec())
2187
+ : undefined;
2043
2188
  // Atomic path: when the caller asks for rollback-on-failure AND the
2044
2189
  // service advertises `$batch`, collapse the envelope into a single
2045
2190
  // OData batch with a shared `atomicityGroup` so a later failure
@@ -2050,9 +2195,9 @@ export function odataSource(descriptor, client, policy) {
2050
2195
  if (envelope.rollbackOnFailure === true) {
2051
2196
  const batchAdvertised = await negotiation.batchAdvertised();
2052
2197
  if (batchAdvertised) {
2053
- return atomicOdataApplyEdits(entity, envelope, keyFields, descriptor);
2198
+ return atomicOdataApplyEdits(entity, envelope, keyFields, descriptor, prepared);
2054
2199
  }
2055
- const result = await perCallOdataApplyEdits(entity, envelope, keyFields);
2200
+ const result = await perCallOdataApplyEdits(entity, envelope, keyFields, prepared);
2056
2201
  return {
2057
2202
  ...result,
2058
2203
  degraded: [
@@ -2066,7 +2211,7 @@ export function odataSource(descriptor, client, policy) {
2066
2211
  ],
2067
2212
  };
2068
2213
  }
2069
- return perCallOdataApplyEdits(entity, envelope, keyFields);
2214
+ return perCallOdataApplyEdits(entity, envelope, keyFields, prepared);
2070
2215
  },
2071
2216
  async queryRelated() {
2072
2217
  // OData has no canonical related-records surface; navigation
@@ -2077,21 +2222,114 @@ export function odataSource(descriptor, client, policy) {
2077
2222
  attachments: unsupportedAttachmentApi(descriptor),
2078
2223
  });
2079
2224
  }
2225
+ /**
2226
+ * Preflight the complete opted-in envelope against one cached metadata
2227
+ * snapshot. Both direct and atomic execution consume this projection, so a
2228
+ * local encoding failure cannot leak a partial request or take a different
2229
+ * branch-specific coercion path.
2230
+ */
2231
+ function prepareLosslessOdataEdits(entity, envelope, keyFields, metadata, codec) {
2232
+ const urlKeys = urlKeyFields(entity.entitySet, keyFields);
2233
+ const adds = (envelope.adds ?? []).map((add) => preparedOdataBody(metadata, entity.entitySet, add, codec));
2234
+ const updates = (envelope.updates ?? []).map((update) => {
2235
+ const prepared = preparedOdataBody(metadata, entity.entitySet, update, codec);
2236
+ const key = preparedOdataUpdateKey(metadata, entity.entitySet, update.id, prepared.body, urlKeys, codec);
2237
+ return { ...prepared, key };
2238
+ });
2239
+ const deletes = (envelope.deletes ?? []).map((id) => {
2240
+ if (urlKeys.length > 1) {
2241
+ // The generic contract carries only a scalar FeatureId. Lossless
2242
+ // composite addressing requires named components, so do not interpret
2243
+ // an ad-hoc string expression or risk targeting a different entity.
2244
+ throw new codec.HonuaOdataEdmEncodingError("missing-key", "$.key");
2245
+ }
2246
+ return {
2247
+ key: codec.encodeOdataEntityKey(metadata, entity.entitySet, id, {
2248
+ keyFields: urlKeys,
2249
+ }),
2250
+ };
2251
+ });
2252
+ return { adds, updates, deletes };
2253
+ }
2254
+ function preparedOdataBody(metadata, entitySet, feature, codec) {
2255
+ const attributes = codec.encodeOdataWriteBody(metadata, entitySet, feature.attributes);
2256
+ let encoded = attributes;
2257
+ if (feature.geometry !== undefined &&
2258
+ feature.geometry !== null &&
2259
+ !Object.hasOwn(attributes.body, "Geometry") &&
2260
+ !Object.hasOwn(attributes.body, "geometry")) {
2261
+ const geometry = codec.encodeOdataWriteBody(metadata, entitySet, { Geometry: feature.geometry });
2262
+ encoded = {
2263
+ body: { ...attributes.body, ...geometry.body },
2264
+ requiresIeee754Compatible: attributes.requiresIeee754Compatible || geometry.requiresIeee754Compatible,
2265
+ };
2266
+ }
2267
+ return {
2268
+ body: encoded.body,
2269
+ contentType: encoded.requiresIeee754Compatible ? "application/json;IEEE754Compatible=true" : "application/json",
2270
+ };
2271
+ }
2272
+ function preparedOdataUpdateKey(metadata, entitySet, id, attributes, keyFields, codec) {
2273
+ if (keyFields.length === 1) {
2274
+ const field = keyFields[0];
2275
+ const attributeValue = ownPreparedValue(attributes, field);
2276
+ const attributeKey = attributeValue === undefined || attributeValue === null
2277
+ ? undefined
2278
+ : codec.encodeOdataEntityKey(metadata, entitySet, attributeValue, { keyFields });
2279
+ const idKey = id === undefined || id === null ? undefined : codec.encodeOdataEntityKey(metadata, entitySet, id, { keyFields });
2280
+ if (attributeKey &&
2281
+ idKey &&
2282
+ (attributeKey.literal !== idKey.literal || attributeKey.pathSegment !== idKey.pathSegment)) {
2283
+ throw new codec.HonuaOdataEdmEncodingError("invalid-value", "$.key");
2284
+ }
2285
+ return attributeKey ?? idKey ?? codec.encodeOdataEntityKey(metadata, entitySet, undefined, { keyFields });
2286
+ }
2287
+ if (keyFields.length === 0) {
2288
+ return codec.encodeOdataEntityKey(metadata, entitySet, id, { keyFields });
2289
+ }
2290
+ if (id !== undefined && id !== null) {
2291
+ // A scalar generic id cannot prove equivalence to a named composite key.
2292
+ // Reject it even when the body carries all components so the adapter never
2293
+ // targets one identity while reporting another.
2294
+ throw new codec.HonuaOdataEdmEncodingError("invalid-value", "$.key");
2295
+ }
2296
+ const key = Object.create(null);
2297
+ for (const field of keyFields) {
2298
+ const value = ownPreparedValue(attributes, field);
2299
+ if (value === undefined || value === null)
2300
+ continue;
2301
+ Object.defineProperty(key, field, {
2302
+ configurable: true,
2303
+ enumerable: true,
2304
+ writable: true,
2305
+ value,
2306
+ });
2307
+ }
2308
+ return codec.encodeOdataEntityKey(metadata, entitySet, key, { keyFields });
2309
+ }
2310
+ function ownPreparedValue(attributes, field) {
2311
+ const descriptor = Object.getOwnPropertyDescriptor(attributes, field);
2312
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
2313
+ }
2080
2314
  /**
2081
2315
  * Per-call OData edits — issues independent POST/PATCH/DELETE requests
2082
2316
  * and collects per-row outcomes. This is the non-atomic fallback used
2083
2317
  * when `rollbackOnFailure` is unset or the server does not advertise
2084
2318
  * `$batch`.
2085
2319
  */
2086
- async function perCallOdataApplyEdits(entity, envelope, keyFields) {
2320
+ async function perCallOdataApplyEdits(entity, envelope, keyFields, prepared) {
2087
2321
  const added = [];
2088
2322
  const updated = [];
2089
2323
  const deleted = [];
2090
2324
  const { signal } = envelope;
2091
- for (const add of envelope.adds ?? []) {
2325
+ const adds = envelope.adds ?? [];
2326
+ for (let index = 0; index < adds.length; index += 1) {
2327
+ const add = adds[index];
2328
+ const encoded = prepared?.adds[index];
2092
2329
  try {
2093
- const created = await entity.add(featureToOdataBody(add), {
2330
+ const created = await entity.add(encoded?.body ?? featureToOdataBody(add), {
2094
2331
  ...(signal ? { signal } : {}),
2332
+ ...(encoded ? { contentType: encoded.contentType } : {}),
2095
2333
  });
2096
2334
  const id = readKey(created, keyFields, add.id);
2097
2335
  added.push(id !== undefined ? { id, success: true } : { success: true });
@@ -2106,22 +2344,30 @@ async function perCallOdataApplyEdits(entity, envelope, keyFields) {
2106
2344
  // using the full `keyFields` because the response body still carries
2107
2345
  // every key.
2108
2346
  const urlKeys = urlKeyFields(entity.entitySet, keyFields);
2109
- for (const update of envelope.updates ?? []) {
2110
- const key = canonicalKeyToOdata(update, urlKeys);
2347
+ const updates = envelope.updates ?? [];
2348
+ for (let index = 0; index < updates.length; index += 1) {
2349
+ const update = updates[index];
2350
+ const encoded = prepared?.updates[index];
2351
+ const key = encoded?.key.literal ?? canonicalKeyToOdata(update, urlKeys);
2111
2352
  if (key === undefined) {
2112
2353
  updated.push({ success: false, error: { code: 400, description: "update.id is required" } });
2113
2354
  continue;
2114
2355
  }
2115
2356
  try {
2116
- await entity.update(key, featureToOdataBody(update), { ...(signal ? { signal } : {}) });
2357
+ await entity.update(key, encoded?.body ?? featureToOdataBody(update), {
2358
+ ...(signal ? { signal } : {}),
2359
+ ...(encoded ? { contentType: encoded.contentType } : {}),
2360
+ });
2117
2361
  updated.push({ id: update.id ?? readKeyFromBody(update.attributes, keyFields), success: true });
2118
2362
  }
2119
2363
  catch (err) {
2120
2364
  updated.push({ id: update.id, success: false, error: editErrorFromCatch(err) });
2121
2365
  }
2122
2366
  }
2123
- for (const id of envelope.deletes ?? []) {
2124
- const key = canonicalKeyToOdata({ id, attributes: {} }, urlKeys);
2367
+ const deletes = envelope.deletes ?? [];
2368
+ for (let index = 0; index < deletes.length; index += 1) {
2369
+ const id = deletes[index];
2370
+ const key = prepared?.deletes[index]?.key.literal ?? canonicalKeyToOdata({ id, attributes: {} }, urlKeys);
2125
2371
  if (key === undefined) {
2126
2372
  deleted.push({ success: false, error: { code: 400, description: "delete id is required" } });
2127
2373
  continue;
@@ -2144,7 +2390,7 @@ async function perCallOdataApplyEdits(entity, envelope, keyFields) {
2144
2390
  * threaded back into per-row `EditOutcome` entries in the original
2145
2391
  * order so callers see the same shape regardless of transport.
2146
2392
  */
2147
- async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor) {
2393
+ async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor, prepared) {
2148
2394
  const operations = [];
2149
2395
  const plan = [];
2150
2396
  const bucketIndices = { add: [], update: [], delete: [] };
@@ -2156,21 +2402,26 @@ async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor) {
2156
2402
  // and the test surface stays small.
2157
2403
  let seq = 1;
2158
2404
  const adds = envelope.adds ?? [];
2159
- for (const add of adds) {
2405
+ for (let index = 0; index < adds.length; index += 1) {
2406
+ const add = adds[index];
2407
+ const encoded = prepared?.adds[index];
2160
2408
  const id = String(seq++);
2161
2409
  operations.push({
2162
2410
  id,
2163
2411
  method: "POST",
2164
2412
  url: stripLeadingSlashLocal(entity.entitySet),
2165
- body: featureToOdataBody(add),
2413
+ body: encoded?.body ?? featureToOdataBody(add),
2414
+ ...(encoded ? { headers: { "Content-Type": encoded.contentType } } : {}),
2166
2415
  });
2167
2416
  bucketIndices.add.push(plan.length);
2168
2417
  plan.push({ kind: "add", id: add.id });
2169
2418
  }
2170
2419
  const updates = envelope.updates ?? [];
2171
- for (const update of updates) {
2420
+ for (let index = 0; index < updates.length; index += 1) {
2421
+ const update = updates[index];
2422
+ const encoded = prepared?.updates[index];
2172
2423
  const id = String(seq++);
2173
- const key = canonicalKeyToOdata(update, urlKeys);
2424
+ const key = encoded?.key.literal ?? canonicalKeyToOdata(update, urlKeys);
2174
2425
  if (key === undefined) {
2175
2426
  bucketIndices.update.push(plan.length);
2176
2427
  plan.push({ kind: "update", id: update.id });
@@ -2179,22 +2430,29 @@ async function atomicOdataApplyEdits(entity, envelope, keyFields, descriptor) {
2179
2430
  operations.push({
2180
2431
  id,
2181
2432
  method: "PATCH",
2182
- url: `${entity.entitySet}(${key})`,
2183
- body: featureToOdataBody(update),
2433
+ url: `${entity.entitySet}(${encoded?.key.pathSegment ?? key})`,
2434
+ body: encoded?.body ?? featureToOdataBody(update),
2435
+ ...(encoded ? { headers: { "Content-Type": encoded.contentType } } : {}),
2184
2436
  });
2185
2437
  bucketIndices.update.push(plan.length);
2186
2438
  plan.push({ kind: "update", id: update.id, key });
2187
2439
  }
2188
2440
  const deletes = envelope.deletes ?? [];
2189
- for (const rawId of deletes) {
2441
+ for (let index = 0; index < deletes.length; index += 1) {
2442
+ const rawId = deletes[index];
2443
+ const encoded = prepared?.deletes[index];
2190
2444
  const id = String(seq++);
2191
- const key = canonicalKeyToOdata({ id: rawId, attributes: {} }, urlKeys);
2445
+ const key = encoded?.key.literal ?? canonicalKeyToOdata({ id: rawId, attributes: {} }, urlKeys);
2192
2446
  if (key === undefined) {
2193
2447
  bucketIndices.delete.push(plan.length);
2194
2448
  plan.push({ kind: "delete", id: rawId });
2195
2449
  continue;
2196
2450
  }
2197
- operations.push({ id, method: "DELETE", url: `${entity.entitySet}(${key})` });
2451
+ operations.push({
2452
+ id,
2453
+ method: "DELETE",
2454
+ url: `${entity.entitySet}(${encoded?.key.pathSegment ?? key})`,
2455
+ });
2198
2456
  bucketIndices.delete.push(plan.length);
2199
2457
  plan.push({ kind: "delete", id: rawId, key });
2200
2458
  }
@@ -2485,6 +2743,13 @@ async function buildOdataParams(entity, descriptor, request, options) {
2485
2743
  if (request.returnGeometry === false) {
2486
2744
  rootSelect = rootSelect.filter((f) => !isOdataGeometryFieldName(f, options.geomColumn));
2487
2745
  }
2746
+ else if (options.geomColumn && !rootSelect.includes(options.geomColumn)) {
2747
+ // OData `$select` is exclusive: once callers project attributes the
2748
+ // service omits every unlisted property. Preserve canonical
2749
+ // `returnGeometry` (default true) by explicitly retaining the resolved
2750
+ // geometry column.
2751
+ rootSelect = [...rootSelect, options.geomColumn];
2752
+ }
2488
2753
  if (rootSelect.length > 0)
2489
2754
  out.select = rootSelect;
2490
2755
  if (split.expand.length > 0)
@@ -2723,6 +2988,10 @@ class OdataCapabilityNegotiator {
2723
2988
  return [];
2724
2989
  return meta.keys[typeName] ?? [];
2725
2990
  }
2991
+ /** Return the same single-flight snapshot used by every capability probe. */
2992
+ metadata() {
2993
+ return this.materialize();
2994
+ }
2726
2995
  async keyField(sourceId) {
2727
2996
  const keys = await this.keyFields(sourceId);
2728
2997
  if (keys.length === 1)
@@ -2779,7 +3048,7 @@ function makeSource(descriptor, caps, _policy, adapters, impl) {
2779
3048
  function lookupAdapter(kind) {
2780
3049
  return adapters[kind];
2781
3050
  }
2782
- return {
3051
+ const source = {
2783
3052
  descriptor: { ...descriptor, capabilities: caps },
2784
3053
  capabilities: caps,
2785
3054
  query: impl.query.bind(impl),
@@ -2794,6 +3063,7 @@ function makeSource(descriptor, caps, _policy, adapters, impl) {
2794
3063
  protocol: lookupAdapter,
2795
3064
  adapter: lookupAdapter,
2796
3065
  };
3066
+ return addCapabilitySupport(source, source.descriptor);
2797
3067
  }
2798
3068
  /**
2799
3069
  * Helper: build a no-attachment-support `AttachmentApi` whose every method
@@ -2901,6 +3171,20 @@ function requireOgcLocator(descriptor) {
2901
3171
  }
2902
3172
  return { collectionId };
2903
3173
  }
3174
+ /**
3175
+ * Map a `SourceLocator.layout` hint onto the OGC API Features endpoint
3176
+ * discovery mode. `undefined` / `honua-facade` keep the fixed facade fast
3177
+ * path; `ogc-api` and `auto` enable spec-driven landing-page discovery.
3178
+ * The STAC-specific modes are not applicable to Features and fall back to
3179
+ * the facade.
3180
+ */
3181
+ function ogcFeaturesLayoutMode(layout) {
3182
+ if (layout === "ogc-api")
3183
+ return "ogc-api";
3184
+ if (layout === "auto")
3185
+ return "auto";
3186
+ return undefined;
3187
+ }
2904
3188
  function requireOgcRecordsLocator(descriptor) {
2905
3189
  const { collectionId } = descriptor.locator;
2906
3190
  if (collectionId === undefined || collectionId === null || collectionId === "") {
@@ -2947,8 +3231,6 @@ function requireGPServiceLocator(descriptor, caps) {
2947
3231
  // /rest/services/<serviceId>/GPServer/<taskName>/..., so descriptors that
2948
3232
  // advertise the `geoprocess` capability must carry a task name. Without
2949
3233
  // one the lifecycle routes resolve to non-existent paths on the server.
2950
- // Descriptors with only `connect` (service-root metadata probe) may omit
2951
- // taskName.
2952
3234
  if (caps.has("geoprocess") && (typeof taskName !== "string" || taskName.length === 0)) {
2953
3235
  throw new Error(`createDataset: source "${descriptor.id}" (geoservices-gp-service) advertises "geoprocess" but locator.taskName is missing; GP lifecycle routes require /GPServer/<taskName>/...`);
2954
3236
  }
@@ -3110,6 +3392,24 @@ function toStacRequest(request, collectionScope) {
3110
3392
  out.signal = request.signal;
3111
3393
  return out;
3112
3394
  }
3395
+ /**
3396
+ * Project a STAC API search request onto the client-side filter params a
3397
+ * static catalog traversal applies (it has no server-side query grammar).
3398
+ */
3399
+ function toStacStaticParams(request) {
3400
+ const out = {};
3401
+ if (request.collections && request.collections.length > 0)
3402
+ out.collections = request.collections;
3403
+ if (request.bbox)
3404
+ out.bbox = request.bbox;
3405
+ if (request.datetime !== undefined)
3406
+ out.datetime = request.datetime;
3407
+ if (request.limit !== undefined)
3408
+ out.limit = request.limit;
3409
+ if (request.signal)
3410
+ out.signal = request.signal;
3411
+ return out;
3412
+ }
3113
3413
  function toTypedFeatureFromStac(feature) {
3114
3414
  return {
3115
3415
  attributes: (feature.properties ?? {}),
@@ -3345,8 +3645,10 @@ function toOgcRequest(request) {
3345
3645
  if (!request)
3346
3646
  return {};
3347
3647
  const out = {};
3348
- if (request.where !== undefined)
3648
+ if (request.where !== undefined) {
3349
3649
  out.filter = request.where;
3650
+ out.filterLang = "cql2-text";
3651
+ }
3350
3652
  if (request.outFields && request.outFields.length > 0)
3351
3653
  out.properties = [...request.outFields];
3352
3654
  if (request.outSr !== undefined)
@@ -3551,10 +3853,10 @@ function readField(attributes, field) {
3551
3853
  function toApplyEditsRequest(envelope) {
3552
3854
  const out = {};
3553
3855
  if (envelope.adds && envelope.adds.length > 0) {
3554
- out.adds = envelope.adds.map((f) => canonicalToHonuaFeature(f));
3856
+ out.adds = envelope.adds.map(canonicalToHonuaFeature);
3555
3857
  }
3556
3858
  if (envelope.updates && envelope.updates.length > 0) {
3557
- out.updates = envelope.updates.map((f) => canonicalToHonuaFeature(f));
3859
+ out.updates = envelope.updates.map(canonicalToHonuaFeature);
3558
3860
  }
3559
3861
  if (envelope.deletes && envelope.deletes.length > 0) {
3560
3862
  const numericIds = [];
@@ -3799,4 +4101,5 @@ export const FIRST_PARTY_PROTOCOLS = new Set([
3799
4101
  "wfs",
3800
4102
  "wms",
3801
4103
  "wmts",
4104
+ "pmtiles",
3802
4105
  ]);