@honua/react 0.1.0-beta.0 → 0.1.2-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 (315) 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 +11 -2
  14. package/contract/index.d.ts.map +1 -1
  15. package/contract/index.js +4 -1
  16. package/contract/pmtiles.d.ts +31 -0
  17. package/contract/pmtiles.d.ts.map +1 -1
  18. package/contract/pmtiles.js +60 -20
  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 +35 -17
  32. package/contract/source.d.ts.map +1 -1
  33. package/contract/source.js +267 -71
  34. package/contract/types.d.ts +140 -4
  35. package/contract/types.d.ts.map +1 -1
  36. package/contract/types.js +6 -14
  37. package/core/bounded-json.d.ts +4 -0
  38. package/core/bounded-json.d.ts.map +1 -0
  39. package/core/bounded-json.js +38 -0
  40. package/core/cache-state.d.ts +9 -0
  41. package/core/cache-state.d.ts.map +1 -1
  42. package/core/cache-state.js +10 -0
  43. package/core/capabilities-xml.d.ts +33 -0
  44. package/core/capabilities-xml.d.ts.map +1 -0
  45. package/core/capabilities-xml.js +331 -0
  46. package/core/client.d.ts +4 -1
  47. package/core/client.d.ts.map +1 -1
  48. package/core/client.js +115 -29
  49. package/core/error-classifications.d.ts +153 -0
  50. package/core/error-classifications.d.ts.map +1 -0
  51. package/core/error-classifications.js +140 -0
  52. package/core/error-code-registry.d.ts +158 -0
  53. package/core/error-code-registry.d.ts.map +1 -0
  54. package/core/error-code-registry.js +143 -0
  55. package/core/error-envelope.d.ts +68 -0
  56. package/core/error-envelope.d.ts.map +1 -0
  57. package/core/error-envelope.js +357 -0
  58. package/core/errors.d.ts +45 -29
  59. package/core/errors.d.ts.map +1 -1
  60. package/core/errors.js +97 -50
  61. package/core/geoparquet-sql.d.ts +30 -2
  62. package/core/geoparquet-sql.d.ts.map +1 -1
  63. package/core/geoparquet-sql.js +38 -3
  64. package/core/odata-key-path.d.ts +12 -0
  65. package/core/odata-key-path.d.ts.map +1 -0
  66. package/core/odata-key-path.js +28 -0
  67. package/core/odata-write-codec.d.ts +66 -0
  68. package/core/odata-write-codec.d.ts.map +1 -0
  69. package/core/odata-write-codec.js +1083 -0
  70. package/core/odata.d.ts +59 -7
  71. package/core/odata.d.ts.map +1 -1
  72. package/core/odata.js +431 -99
  73. package/core/ogc-features.d.ts.map +1 -1
  74. package/core/ogc-features.js +3 -0
  75. package/core/ogc-maps.d.ts +7 -0
  76. package/core/ogc-maps.d.ts.map +1 -1
  77. package/core/ogc-maps.js +46 -7
  78. package/core/ogc-processes.d.ts +2 -1
  79. package/core/ogc-processes.d.ts.map +1 -1
  80. package/core/ogc-processes.js +33 -5
  81. package/core/ogc-records.d.ts +9 -0
  82. package/core/ogc-records.d.ts.map +1 -1
  83. package/core/ogc-records.js +78 -29
  84. package/core/ogc-tiles.d.ts +10 -1
  85. package/core/ogc-tiles.d.ts.map +1 -1
  86. package/core/ogc-tiles.js +56 -17
  87. package/core/path-utils.d.ts +9 -0
  88. package/core/path-utils.d.ts.map +1 -1
  89. package/core/path-utils.js +19 -0
  90. package/core/protocol-transport.d.ts +1 -0
  91. package/core/protocol-transport.d.ts.map +1 -1
  92. package/core/request-pipeline.d.ts.map +1 -1
  93. package/core/request-pipeline.js +1 -1
  94. package/core/spatial-filter.d.ts +8 -4
  95. package/core/spatial-filter.d.ts.map +1 -1
  96. package/core/spatial-filter.js +206 -16
  97. package/core/stac-static.d.ts +6 -2
  98. package/core/stac-static.d.ts.map +1 -1
  99. package/core/stac-static.js +260 -69
  100. package/core/surfaces.d.ts.map +1 -1
  101. package/core/surfaces.js +69 -5
  102. package/core/types.d.ts +125 -7
  103. package/core/types.d.ts.map +1 -1
  104. package/core/wfs-capabilities.d.ts +2 -0
  105. package/core/wfs-capabilities.d.ts.map +1 -1
  106. package/core/wfs-capabilities.js +8 -1
  107. package/core/wfs.d.ts.map +1 -1
  108. package/core/wfs.js +10 -0
  109. package/core/wms-capabilities.d.ts +56 -84
  110. package/core/wms-capabilities.d.ts.map +1 -1
  111. package/core/wms-capabilities.js +212 -509
  112. package/core/wmts-capabilities.d.ts +62 -62
  113. package/core/wmts-capabilities.d.ts.map +1 -1
  114. package/core/wmts-capabilities.js +224 -371
  115. package/esri-compat/editor.d.ts +8 -0
  116. package/esri-compat/editor.d.ts.map +1 -1
  117. package/esri-compat/editor.js +13 -0
  118. package/esri-compat/feature-layer.d.ts +2 -0
  119. package/esri-compat/feature-layer.d.ts.map +1 -1
  120. package/esri-compat/feature-layer.js +1 -0
  121. package/esri-compat/layer-list.d.ts +11 -0
  122. package/esri-compat/layer-list.d.ts.map +1 -1
  123. package/esri-compat/layer-list.js +49 -0
  124. package/esri-compat/legend.d.ts +10 -0
  125. package/esri-compat/legend.d.ts.map +1 -1
  126. package/esri-compat/legend.js +27 -0
  127. package/esri-compat/portal.d.ts +197 -0
  128. package/esri-compat/portal.d.ts.map +1 -0
  129. package/esri-compat/portal.js +251 -0
  130. package/esri-compat/renderer-objects.d.ts +40 -0
  131. package/esri-compat/renderer-objects.d.ts.map +1 -0
  132. package/esri-compat/renderer-objects.js +62 -0
  133. package/esri-compat/sketch.d.ts +43 -0
  134. package/esri-compat/sketch.d.ts.map +1 -1
  135. package/esri-compat/sketch.js +46 -0
  136. package/esri-compat/snapping.d.ts +43 -0
  137. package/esri-compat/snapping.d.ts.map +1 -0
  138. package/esri-compat/snapping.js +45 -0
  139. package/esri-compat/url.d.ts.map +1 -1
  140. package/esri-compat/url.js +1 -1
  141. package/esri-compat/widget-host.d.ts +84 -0
  142. package/esri-compat/widget-host.d.ts.map +1 -0
  143. package/esri-compat/widget-host.js +159 -0
  144. package/filter-registry/index.d.ts +145 -0
  145. package/filter-registry/index.d.ts.map +1 -0
  146. package/filter-registry/index.js +531 -0
  147. package/generated-app/errors.d.ts +18 -2
  148. package/generated-app/errors.d.ts.map +1 -1
  149. package/generated-app/errors.js +44 -11
  150. package/map/automatic-mount-integration.d.ts +134 -0
  151. package/map/automatic-mount-integration.d.ts.map +1 -0
  152. package/map/automatic-mount-integration.js +305 -0
  153. package/map/automatic-source-strategy.d.ts +125 -0
  154. package/map/automatic-source-strategy.d.ts.map +1 -0
  155. package/map/automatic-source-strategy.js +494 -0
  156. package/map/data-to-map-bridge.d.ts +293 -0
  157. package/map/data-to-map-bridge.d.ts.map +1 -0
  158. package/map/data-to-map-bridge.js +817 -0
  159. package/map/geojson-projection.d.ts +33 -0
  160. package/map/geojson-projection.d.ts.map +1 -0
  161. package/map/geojson-projection.js +118 -0
  162. package/map/index.d.ts +14 -0
  163. package/map/index.d.ts.map +1 -1
  164. package/map/index.js +8 -0
  165. package/map/raster-source-spec.d.ts +30 -0
  166. package/map/raster-source-spec.d.ts.map +1 -0
  167. package/map/raster-source-spec.js +190 -0
  168. package/map/raster-source-strategy.d.ts +65 -0
  169. package/map/raster-source-strategy.d.ts.map +1 -0
  170. package/map/raster-source-strategy.js +232 -0
  171. package/map/source-to-maplibre.d.ts +91 -0
  172. package/map/source-to-maplibre.d.ts.map +1 -0
  173. package/map/source-to-maplibre.js +454 -0
  174. package/map/temporal-playback.d.ts +139 -0
  175. package/map/temporal-playback.d.ts.map +1 -0
  176. package/map/temporal-playback.js +247 -0
  177. package/package.json +14 -1
  178. package/query-planner/canonical.d.ts +7 -0
  179. package/query-planner/canonical.d.ts.map +1 -0
  180. package/query-planner/canonical.js +145 -0
  181. package/query-planner/cql2-json.d.ts +17 -0
  182. package/query-planner/cql2-json.d.ts.map +1 -0
  183. package/query-planner/cql2-json.js +646 -0
  184. package/query-planner/diagnostics.d.ts +35 -0
  185. package/query-planner/diagnostics.d.ts.map +1 -0
  186. package/query-planner/diagnostics.js +373 -0
  187. package/query-planner/duckdb.d.ts +79 -0
  188. package/query-planner/duckdb.d.ts.map +1 -0
  189. package/query-planner/duckdb.js +758 -0
  190. package/query-planner/execution-context.d.ts +5 -0
  191. package/query-planner/execution-context.d.ts.map +1 -0
  192. package/query-planner/execution-context.js +50 -0
  193. package/query-planner/executor.d.ts +5 -0
  194. package/query-planner/executor.d.ts.map +1 -0
  195. package/query-planner/executor.js +223 -0
  196. package/query-planner/geoservices.d.ts +91 -0
  197. package/query-planner/geoservices.d.ts.map +1 -0
  198. package/query-planner/geoservices.js +667 -0
  199. package/query-planner/grpc.d.ts +100 -0
  200. package/query-planner/grpc.d.ts.map +1 -0
  201. package/query-planner/grpc.js +736 -0
  202. package/query-planner/index.d.ts +44 -0
  203. package/query-planner/index.d.ts.map +1 -0
  204. package/query-planner/index.js +30 -0
  205. package/query-planner/ir.d.ts +20 -0
  206. package/query-planner/ir.d.ts.map +1 -0
  207. package/query-planner/ir.js +417 -0
  208. package/query-planner/local-aggregate.d.ts +5 -0
  209. package/query-planner/local-aggregate.d.ts.map +1 -0
  210. package/query-planner/local-aggregate.js +126 -0
  211. package/query-planner/odata.d.ts +48 -0
  212. package/query-planner/odata.d.ts.map +1 -0
  213. package/query-planner/odata.js +662 -0
  214. package/query-planner/ogc-compiler.d.ts +55 -0
  215. package/query-planner/ogc-compiler.d.ts.map +1 -0
  216. package/query-planner/ogc-compiler.js +480 -0
  217. package/query-planner/ogc-features.d.ts +53 -0
  218. package/query-planner/ogc-features.d.ts.map +1 -0
  219. package/query-planner/ogc-features.js +617 -0
  220. package/query-planner/planner.d.ts +17 -0
  221. package/query-planner/planner.d.ts.map +1 -0
  222. package/query-planner/planner.js +818 -0
  223. package/query-planner/resource.d.ts +94 -0
  224. package/query-planner/resource.d.ts.map +1 -0
  225. package/query-planner/resource.js +526 -0
  226. package/query-planner/semantic-canonical.d.ts +15 -0
  227. package/query-planner/semantic-canonical.d.ts.map +1 -0
  228. package/query-planner/semantic-canonical.js +62 -0
  229. package/query-planner/semantic-compat.d.ts +10 -0
  230. package/query-planner/semantic-compat.d.ts.map +1 -0
  231. package/query-planner/semantic-compat.js +35 -0
  232. package/query-planner/semantic-compiler.d.ts +64 -0
  233. package/query-planner/semantic-compiler.d.ts.map +1 -0
  234. package/query-planner/semantic-compiler.js +81 -0
  235. package/query-planner/semantic-literals.d.ts +30 -0
  236. package/query-planner/semantic-literals.d.ts.map +1 -0
  237. package/query-planner/semantic-literals.js +133 -0
  238. package/query-planner/semantic-types.d.ts +265 -0
  239. package/query-planner/semantic-types.d.ts.map +1 -0
  240. package/query-planner/semantic-types.js +1 -0
  241. package/query-planner/semantic.d.ts +54 -0
  242. package/query-planner/semantic.d.ts.map +1 -0
  243. package/query-planner/semantic.js +1421 -0
  244. package/query-planner/types.d.ts +560 -0
  245. package/query-planner/types.d.ts.map +1 -0
  246. package/query-planner/types.js +49 -0
  247. package/query-planner/wfs-semantic.d.ts +58 -0
  248. package/query-planner/wfs-semantic.d.ts.map +1 -0
  249. package/query-planner/wfs-semantic.js +547 -0
  250. package/query-planner/wfs.d.ts +6 -0
  251. package/query-planner/wfs.d.ts.map +1 -0
  252. package/query-planner/wfs.js +71 -0
  253. package/react/external-map.d.ts +67 -0
  254. package/react/external-map.d.ts.map +1 -0
  255. package/react/external-map.js +63 -0
  256. package/react/honua-map.d.ts.map +1 -1
  257. package/react/honua-map.js +3 -2
  258. package/react/honua-source-layer.d.ts +110 -0
  259. package/react/honua-source-layer.d.ts.map +1 -0
  260. package/react/honua-source-layer.js +101 -0
  261. package/react/hooks.d.ts +28 -0
  262. package/react/hooks.d.ts.map +1 -1
  263. package/react/hooks.js +26 -4
  264. package/react/index.d.ts +8 -0
  265. package/react/index.d.ts.map +1 -1
  266. package/react/index.js +5 -0
  267. package/react/query-cache.d.ts +26 -4
  268. package/react/query-cache.d.ts.map +1 -1
  269. package/react/query-cache.js +51 -8
  270. package/react/selection.d.ts +177 -0
  271. package/react/selection.d.ts.map +1 -0
  272. package/react/selection.js +0 -0
  273. package/react/use-mounted-source.d.ts +69 -0
  274. package/react/use-mounted-source.d.ts.map +1 -0
  275. package/react/use-mounted-source.js +346 -0
  276. package/runtime/edit-snapping.d.ts +107 -0
  277. package/runtime/edit-snapping.d.ts.map +1 -0
  278. package/runtime/edit-snapping.js +190 -0
  279. package/runtime/errors.d.ts +3 -2
  280. package/runtime/errors.d.ts.map +1 -1
  281. package/runtime/errors.js +19 -2
  282. package/runtime/index.d.ts +6 -0
  283. package/runtime/index.d.ts.map +1 -1
  284. package/runtime/index.js +3 -0
  285. package/runtime/maplibre-renderer.d.ts +41 -0
  286. package/runtime/maplibre-renderer.d.ts.map +1 -0
  287. package/runtime/maplibre-renderer.js +322 -0
  288. package/runtime/query-tiles.d.ts +2 -1
  289. package/runtime/query-tiles.d.ts.map +1 -1
  290. package/runtime/query-tiles.js +6 -2
  291. package/runtime/source-bridge.d.ts +4 -4
  292. package/runtime/source-bridge.d.ts.map +1 -1
  293. package/runtime/source-bridge.js +9 -56
  294. package/runtime/style-interactions.d.ts +3 -2
  295. package/runtime/style-interactions.d.ts.map +1 -1
  296. package/runtime/style-interactions.js +11 -3
  297. package/runtime/terra-draw-sketch.d.ts +275 -0
  298. package/runtime/terra-draw-sketch.d.ts.map +1 -0
  299. package/runtime/terra-draw-sketch.js +404 -0
  300. package/source-capability-registry.d.ts +1 -0
  301. package/source-capability-registry.js +1 -0
  302. package/source-capability-types.d.ts +186 -0
  303. package/source-capability-types.js +7 -0
  304. package/style/index.d.ts +2 -0
  305. package/style/index.d.ts.map +1 -1
  306. package/style/index.js +2 -0
  307. package/style/renderers.d.ts +326 -0
  308. package/style/renderers.d.ts.map +1 -0
  309. package/style/renderers.js +517 -0
  310. package/webmap/convert-renderer.d.ts +24 -3
  311. package/webmap/convert-renderer.d.ts.map +1 -1
  312. package/webmap/convert-renderer.js +93 -112
  313. package/webmap/index.d.ts +1 -1
  314. package/webmap/index.d.ts.map +1 -1
  315. package/webmap/index.js +1 -1
@@ -1,559 +1,262 @@
1
1
  /**
2
- * WMS 1.3.0 Capabilities parser. Hand-rolled, narrow-scope text-walker
3
- * over a `WMS_Capabilities` document. Returns a typed shape (layers,
4
- * styles, bbox-per-CRS, dimensions, advertised formats) that the SDK
5
- * can route through the canonical `Source` surface without leaking XML
6
- * nodes into its public API.
2
+ * Bounded WMS capabilities projection.
7
3
  *
8
- * The parser is closed-schema (WMS 1.3 + the ISO 19128 service-metadata
9
- * subset honua-server emits) and tolerates missing optional nodes
10
- * gracefully. Vendor-extension elements outside the named set are
11
- * ignored. A `HonuaWmsCapabilitiesParseError` (extends `HonuaError`) is
12
- * thrown when a required root or top-level node is missing.
4
+ * The parser accepts the metadata subset needed by discovery and the WMS
5
+ * runtime, rejects malformed/active XML, and returns plain immutable-friendly
6
+ * records. Unknown vendor extensions are ignored; required structural drift is
7
+ * surfaced through {@link HonuaWmsCapabilitiesParseError}.
13
8
  *
14
9
  * @module
15
10
  */
16
- import { decodeXmlText as decodeXmlEntities } from "./xml-text.js";
17
- /**
18
- * Thrown when WMS Capabilities XML is missing required root structure or
19
- * is otherwise malformed in a way that prevents extracting the typed
20
- * shape above. Recoverable per-node gaps (missing `<Title>`,
21
- * `<Abstract>`, etc.) are tolerated and return defaults.
22
- */
23
- export class HonuaWmsCapabilitiesParseError extends Error {
24
- constructor(message) {
25
- super(message);
11
+ import { parseCapabilitiesXml, xmlAttribute, xmlChild, xmlChildren, xmlText, } from "./capabilities-xml.js";
12
+ import { HonuaSdkError } from "./error-envelope.js";
13
+ export class HonuaWmsCapabilitiesParseError extends HonuaSdkError {
14
+ constructor(message, options = {}) {
15
+ super("core.wms-capabilities-parse", message, options);
26
16
  this.name = "HonuaWmsCapabilitiesParseError";
27
17
  }
28
18
  }
29
- /**
30
- * Parse a `WMS_Capabilities` text body into the typed `WmsCapabilities`
31
- * shape. The input is the raw XML string returned by `requestText`; the
32
- * parser walks it once with a small named-element matcher and tolerates
33
- * vendor extensions (unknown elements are skipped).
34
- */
35
19
  export function parseWmsCapabilities(xml) {
36
- if (typeof xml !== "string" || xml.length === 0) {
37
- throw new HonuaWmsCapabilitiesParseError("WMS Capabilities body is empty");
38
- }
39
- const root = findElement(xml, 0, "WMS_Capabilities") ?? findElement(xml, 0, "WMT_MS_Capabilities");
40
- if (!root) {
41
- throw new HonuaWmsCapabilitiesParseError("missing <WMS_Capabilities> root element (received WMS Capabilities XML?)");
42
- }
43
- const version = readAttribute(root.openTag, "version") ?? "1.3.0";
44
- const service = parseServiceMetadata(root.inner);
45
- const capabilityNode = findElement(root.inner, 0, "Capability");
46
- const requestNode = capabilityNode ? findElement(capabilityNode.inner, 0, "Request") : undefined;
47
- const formats = parseFormats(requestNode?.inner ?? "");
48
- const requestSupport = {
49
- getFeatureInfo: requestNode ? findElement(requestNode.inner, 0, "GetFeatureInfo") !== undefined : false,
50
- getLegendGraphic: requestNode ? findElement(requestNode.inner, 0, "GetLegendGraphic") !== undefined : false,
51
- };
52
- const layers = [];
53
- if (capabilityNode) {
54
- for (const layer of iterChildLayers(capabilityNode.inner, [])) {
55
- layers.push(layer);
20
+ try {
21
+ const root = parseCapabilitiesXml(xml, "WMS");
22
+ if (root.localName !== "WMS_Capabilities" && root.localName !== "WMT_MS_Capabilities") {
23
+ throw new Error("missing <WMS_Capabilities> root element (received WMS Capabilities XML?)");
56
24
  }
25
+ const version = xmlAttribute(root, "version") ?? "";
26
+ const warnings = [];
27
+ const serviceNode = xmlChild(root, "Service");
28
+ const service = compactObject({
29
+ title: xmlText(xmlChild(serviceNode, "Title")),
30
+ abstract: xmlText(xmlChild(serviceNode, "Abstract")),
31
+ });
32
+ const capability = xmlChild(root, "Capability");
33
+ const requestNode = xmlChild(capability, "Request");
34
+ const operations = parseOperations(requestNode);
35
+ const operation = (name) => operations.find((candidate) => candidate.name === name);
36
+ const formats = Object.freeze({
37
+ map: operation("GetMap")?.formats ?? Object.freeze([]),
38
+ featureInfo: operation("GetFeatureInfo")?.formats ?? Object.freeze([]),
39
+ legend: operation("GetLegendGraphic")?.formats ?? Object.freeze([]),
40
+ });
41
+ const request = Object.freeze({
42
+ getMap: operation("GetMap") !== undefined,
43
+ getFeatureInfo: operation("GetFeatureInfo") !== undefined,
44
+ getLegendGraphic: operation("GetLegendGraphic") !== undefined,
45
+ operations,
46
+ });
47
+ const layers = Object.freeze(xmlChildren(capability, "Layer").map((layer) => parseLayer(layer, emptyInheritance(), 0, warnings)));
48
+ return Object.freeze({ version, service, layers, formats, request, warnings: Object.freeze(unique(warnings)) });
57
49
  }
58
- return {
59
- version,
60
- service,
61
- layers,
62
- formats,
63
- request: requestSupport,
64
- };
65
- }
66
- function parseServiceMetadata(capabilityXml) {
67
- const node = findElement(capabilityXml, 0, "Service");
68
- if (!node)
69
- return {};
70
- const out = {};
71
- const title = readTextElement(node.inner, "Title");
72
- if (title !== undefined)
73
- out.title = title;
74
- const abstract = readTextElement(node.inner, "Abstract");
75
- if (abstract !== undefined)
76
- out.abstract = abstract;
77
- return out;
78
- }
79
- function parseFormats(requestInnerXml) {
80
- const map = collectFormats(requestInnerXml, "GetMap");
81
- const featureInfo = collectFormats(requestInnerXml, "GetFeatureInfo");
82
- const legend = collectFormats(requestInnerXml, "GetLegendGraphic");
83
- return { map, featureInfo, legend };
84
- }
85
- function collectFormats(requestInnerXml, requestName) {
86
- const node = findElement(requestInnerXml, 0, requestName);
87
- if (!node)
88
- return [];
89
- const formats = [];
90
- let cursor = 0;
91
- while (cursor < node.inner.length) {
92
- const fmt = findElement(node.inner, cursor, "Format");
93
- if (!fmt)
94
- break;
95
- const text = decodeXmlText(fmt.inner.trim());
96
- if (text.length > 0)
97
- formats.push(text);
98
- cursor = fmt.endIndex;
50
+ catch (cause) {
51
+ if (cause instanceof HonuaWmsCapabilitiesParseError)
52
+ throw cause;
53
+ const message = cause instanceof Error ? cause.message : "WMS Capabilities XML is malformed";
54
+ throw new HonuaWmsCapabilitiesParseError(message, { cause });
99
55
  }
100
- return formats;
101
56
  }
102
- /**
103
- * Walk every `<Layer>` element directly inside `parentInner`, applying
104
- * inherited CRS / dimension / style fields from ancestors per WMS 1.3
105
- * §7.2.4.6 inheritance rules. Iterative (no recursion through closures)
106
- * to keep the parse hot path small.
107
- */
108
- function* iterChildLayers(parentInner, ancestors) {
109
- let cursor = 0;
110
- while (cursor < parentInner.length) {
111
- const layer = findElement(parentInner, cursor, "Layer");
112
- if (!layer)
113
- break;
114
- const queryableAttr = readAttribute(layer.openTag, "queryable");
115
- const inherited = mergeAncestors(ancestors);
116
- // Strip nested <Layer>...</Layer> subtrees from the layer's own inner so
117
- // descendant Name / Title / CRS / BoundingBox / Style / Dimension nodes
118
- // do not leak upward into the parent (and from there into siblings via
119
- // `mergeAncestors`). WMS 1.3 §7.2.4.6 inheritance is ancestor → descendant
120
- // only; without this guard, an unnamed group layer takes its first child's
121
- // name and bleeds the child's style across siblings.
122
- const ownInner = stripNestedLayers(layer.inner);
123
- const name = readTextElement(ownInner, "Name") ?? "";
124
- const title = readTextElement(ownInner, "Title");
125
- const abstract = readTextElement(ownInner, "Abstract");
126
- const localCrs = collectChildText(ownInner, "CRS");
127
- const localBbox = collectBoundingBoxes(ownInner);
128
- const localStyles = collectStyles(ownInner);
129
- const localDimensions = collectDimensions(ownInner);
130
- const mergedCrs = uniqueOrder([...inherited.crs, ...localCrs]);
131
- const mergedBbox = mergeBoundingBoxes(inherited.bbox, localBbox);
132
- const mergedStyles = mergeStyles(inherited.styles, localStyles);
133
- const mergedDimensions = mergeDimensions(inherited.dimensions, localDimensions);
134
- const queryable = queryableAttr === "1" || /^true$/i.test(queryableAttr ?? "")
135
- ? true
136
- : queryableAttr === "0"
137
- ? false
138
- : inherited.queryable;
139
- const node = {
140
- name,
141
- ...(title !== undefined ? { title } : {}),
142
- ...(abstract !== undefined ? { abstract } : {}),
143
- crs: mergedCrs,
144
- bbox: mergedBbox,
145
- styles: mergedStyles,
146
- dimensions: mergedDimensions,
147
- queryable,
148
- children: [],
149
- };
150
- const ancestorsForChildren = [...ancestors, node];
151
- const children = [];
152
- for (const child of iterChildLayers(layer.inner, ancestorsForChildren)) {
153
- children.push(child);
57
+ function parseOperations(request) {
58
+ const names = ["GetCapabilities", "GetMap", "GetFeatureInfo", "GetLegendGraphic"];
59
+ return Object.freeze(names.flatMap((name) => {
60
+ const nodes = xmlChildren(request, name);
61
+ if (nodes.length > 1)
62
+ throw new Error(`WMS Request repeats ${name} operation metadata.`);
63
+ const node = nodes[0];
64
+ if (!node)
65
+ return [];
66
+ const formats = unique(xmlChildren(node, "Format").flatMap((entry) => xmlText(entry) ?? []));
67
+ const methods = [];
68
+ const getUrls = [];
69
+ const postUrls = [];
70
+ for (const dcp of xmlChildren(node, "DCPType")) {
71
+ const http = xmlChild(dcp, "HTTP");
72
+ for (const method of http?.children ?? []) {
73
+ if (method.localName !== "Get" && method.localName !== "Post")
74
+ continue;
75
+ const online = xmlChild(method, "OnlineResource") ?? method;
76
+ const href = xmlAttribute(online, "href");
77
+ const normalizedMethod = method.localName === "Get" ? "GET" : "POST";
78
+ if (!methods.includes(normalizedMethod))
79
+ methods.push(normalizedMethod);
80
+ if (href)
81
+ (normalizedMethod === "GET" ? getUrls : postUrls).push(href);
82
+ }
154
83
  }
155
- yield { ...node, children };
156
- cursor = layer.endIndex;
157
- }
84
+ return [
85
+ Object.freeze({
86
+ name,
87
+ formats: Object.freeze(formats),
88
+ methods: Object.freeze(methods),
89
+ getUrls: Object.freeze(unique(getUrls)),
90
+ postUrls: Object.freeze(unique(postUrls)),
91
+ }),
92
+ ];
93
+ }));
158
94
  }
159
- function mergeAncestors(ancestors) {
160
- if (ancestors.length === 0) {
161
- return { crs: [], bbox: [], styles: [], dimensions: [], queryable: false };
162
- }
163
- let crs = [];
164
- let bbox = [];
165
- let styles = [];
166
- let dimensions = [];
167
- let queryable = false;
168
- for (const ancestor of ancestors) {
169
- crs = uniqueOrder([...crs, ...ancestor.crs]);
170
- bbox = mergeBoundingBoxes(bbox, ancestor.bbox);
171
- styles = mergeStyles(styles, ancestor.styles);
172
- dimensions = mergeDimensions(dimensions, ancestor.dimensions);
173
- queryable = queryable || ancestor.queryable;
174
- }
175
- return { crs, bbox, styles, dimensions, queryable };
95
+ function emptyInheritance() {
96
+ return Object.freeze({
97
+ crs: Object.freeze([]),
98
+ bbox: Object.freeze([]),
99
+ styles: Object.freeze([]),
100
+ dimensions: Object.freeze([]),
101
+ queryable: false,
102
+ });
176
103
  }
177
- function uniqueOrder(values) {
178
- const seen = new Set();
179
- const out = [];
180
- for (const v of values) {
181
- if (seen.has(v))
182
- continue;
183
- seen.add(v);
184
- out.push(v);
104
+ function parseLayer(node, inherited, depth, warnings) {
105
+ // The shared XML reader already enforces 32 document levels. This separate
106
+ // semantic limit keeps a capabilities document from constructing a deeper
107
+ // recursive public layer object through wrapper elements.
108
+ if (depth >= 24)
109
+ throw new Error("WMS Capabilities exceeds the 24-level layer hierarchy limit");
110
+ const localCrs = xmlChildren(node, "CRS")
111
+ .concat(xmlChildren(node, "SRS"))
112
+ .flatMap((candidate) => xmlText(candidate)?.split(/\s+/).filter(Boolean) ?? []);
113
+ const crs = Object.freeze(unique([...inherited.crs, ...localCrs]));
114
+ const bbox = Object.freeze(mergeByKey(inherited.bbox, parseBoundingBoxes(node, warnings), (entry) => entry.crs));
115
+ const styles = Object.freeze(mergeByKey(inherited.styles, parseStyles(node, warnings), (entry) => entry.name));
116
+ const dimensions = Object.freeze(mergeByKey(inherited.dimensions, parseDimensions(node, warnings), (entry) => entry.name));
117
+ const queryableValue = xmlAttribute(node, "queryable")?.toLowerCase();
118
+ if (queryableValue !== undefined && !["0", "1", "false", "true"].includes(queryableValue)) {
119
+ warnings.push("WMS layer queryable metadata was malformed and inherited conservatively.");
185
120
  }
186
- return out;
121
+ const queryable = queryableValue === "1" || queryableValue === "true"
122
+ ? true
123
+ : queryableValue === "0" || queryableValue === "false"
124
+ ? false
125
+ : inherited.queryable;
126
+ const next = { crs, bbox, styles, dimensions, queryable };
127
+ const children = Object.freeze(xmlChildren(node, "Layer").map((child) => parseLayer(child, next, depth + 1, warnings)));
128
+ return Object.freeze({
129
+ name: xmlText(xmlChild(node, "Name")) ?? "",
130
+ ...optional("title", xmlText(xmlChild(node, "Title"))),
131
+ ...optional("abstract", xmlText(xmlChild(node, "Abstract"))),
132
+ crs,
133
+ bbox,
134
+ styles,
135
+ dimensions,
136
+ queryable,
137
+ children,
138
+ });
187
139
  }
188
- function mergeBoundingBoxes(parent, local) {
189
- if (local.length === 0)
190
- return parent;
140
+ function parseBoundingBoxes(node, warnings) {
191
141
  const out = [];
192
- const seen = new Set();
193
- for (const bb of local) {
194
- seen.add(bb.crs);
195
- out.push(bb);
196
- }
197
- for (const bb of parent) {
198
- if (!seen.has(bb.crs))
199
- out.push(bb);
200
- }
201
- return out;
202
- }
203
- function mergeStyles(parent, local) {
204
- if (parent.length === 0)
205
- return local;
206
- const seen = new Set(local.map((s) => s.name));
207
- const out = [...local];
208
- for (const s of parent) {
209
- if (!seen.has(s.name))
210
- out.push(s);
211
- }
212
- return out;
213
- }
214
- function mergeDimensions(parent, local) {
215
- if (parent.length === 0)
216
- return local;
217
- const seen = new Set(local.map((d) => d.name));
218
- const out = [...local];
219
- for (const d of parent) {
220
- if (!seen.has(d.name))
221
- out.push(d);
222
- }
223
- return out;
224
- }
225
- function collectBoundingBoxes(inner) {
226
- const out = [];
227
- let cursor = 0;
228
- while (cursor < inner.length) {
229
- const node = findElement(inner, cursor, "BoundingBox");
230
- if (!node)
231
- break;
232
- const crs = readAttribute(node.openTag, "CRS") ?? readAttribute(node.openTag, "SRS");
233
- const minx = parseFloatAttr(node.openTag, "minx");
234
- const miny = parseFloatAttr(node.openTag, "miny");
235
- const maxx = parseFloatAttr(node.openTag, "maxx");
236
- const maxy = parseFloatAttr(node.openTag, "maxy");
237
- if (crs && Number.isFinite(minx) && Number.isFinite(miny) && Number.isFinite(maxx) && Number.isFinite(maxy)) {
238
- out.push({ crs, minx, miny, maxx, maxy });
142
+ for (const bbox of xmlChildren(node, "BoundingBox")) {
143
+ const crs = xmlAttribute(bbox, "CRS") ?? xmlAttribute(bbox, "SRS");
144
+ const minx = finiteNumber(xmlAttribute(bbox, "minx"));
145
+ const miny = finiteNumber(xmlAttribute(bbox, "miny"));
146
+ const maxx = finiteNumber(xmlAttribute(bbox, "maxx"));
147
+ const maxy = finiteNumber(xmlAttribute(bbox, "maxy"));
148
+ if (crs &&
149
+ minx !== undefined &&
150
+ miny !== undefined &&
151
+ maxx !== undefined &&
152
+ maxy !== undefined &&
153
+ minx <= maxx &&
154
+ miny <= maxy) {
155
+ out.push(Object.freeze({ crs, minx, miny, maxx, maxy }));
239
156
  }
240
- cursor = node.endIndex;
241
- }
242
- // EX_GeographicBoundingBox is the WMS 1.3 alternative geographic bbox node
243
- // (always WGS84). honua-server emits both; we pull it as a CRS:84 entry
244
- // when the layer has not advertised one already.
245
- const geo = findElement(inner, 0, "EX_GeographicBoundingBox");
246
- if (geo) {
247
- const w = parseFloatText(readTextElement(geo.inner, "westBoundLongitude"));
248
- const e = parseFloatText(readTextElement(geo.inner, "eastBoundLongitude"));
249
- const s = parseFloatText(readTextElement(geo.inner, "southBoundLatitude"));
250
- const n = parseFloatText(readTextElement(geo.inner, "northBoundLatitude"));
251
- if (Number.isFinite(w) &&
252
- Number.isFinite(e) &&
253
- Number.isFinite(s) &&
254
- Number.isFinite(n) &&
255
- !out.some((bb) => bb.crs === "CRS:84")) {
256
- out.push({ crs: "CRS:84", minx: w, miny: s, maxx: e, maxy: n });
157
+ else {
158
+ warnings.push("WMS BoundingBox metadata was malformed and ignored.");
257
159
  }
258
160
  }
259
- return out;
260
- }
261
- function collectStyles(inner) {
262
- const out = [];
263
- let cursor = 0;
264
- while (cursor < inner.length) {
265
- const node = findElement(inner, cursor, "Style");
266
- if (!node)
267
- break;
268
- const name = readTextElement(node.inner, "Name") ?? "";
269
- const title = readTextElement(node.inner, "Title");
270
- const legend = findElement(node.inner, 0, "LegendURL");
271
- const onlineResource = legend ? findElement(legend.inner, 0, "OnlineResource") : undefined;
272
- const legendUrl = onlineResource ? readAttribute(onlineResource.openTag, "xlink:href") : undefined;
273
- if (name.length > 0) {
274
- const entry = { name };
275
- if (title !== undefined)
276
- entry.title = title;
277
- if (legendUrl !== undefined)
278
- entry.legendUrl = legendUrl;
279
- out.push(entry);
161
+ const geographic = xmlChild(node, "EX_GeographicBoundingBox");
162
+ if (geographic && !out.some((entry) => entry.crs === "CRS:84")) {
163
+ const minx = finiteNumber(xmlText(xmlChild(geographic, "westBoundLongitude")));
164
+ const miny = finiteNumber(xmlText(xmlChild(geographic, "southBoundLatitude")));
165
+ const maxx = finiteNumber(xmlText(xmlChild(geographic, "eastBoundLongitude")));
166
+ const maxy = finiteNumber(xmlText(xmlChild(geographic, "northBoundLatitude")));
167
+ if (minx !== undefined &&
168
+ miny !== undefined &&
169
+ maxx !== undefined &&
170
+ maxy !== undefined &&
171
+ minx <= maxx &&
172
+ miny <= maxy &&
173
+ minx >= -180 &&
174
+ maxx <= 180 &&
175
+ miny >= -90 &&
176
+ maxy <= 90) {
177
+ out.push(Object.freeze({ crs: "CRS:84", minx, miny, maxx, maxy }));
280
178
  }
281
- cursor = node.endIndex;
282
- }
283
- return out;
284
- }
285
- function collectDimensions(inner) {
286
- const out = [];
287
- let cursor = 0;
288
- while (cursor < inner.length) {
289
- const node = findElement(inner, cursor, "Dimension");
290
- if (!node)
291
- break;
292
- const name = readAttribute(node.openTag, "name");
293
- if (typeof name === "string" && name.length > 0) {
294
- const units = readAttribute(node.openTag, "units");
295
- const def = readAttribute(node.openTag, "default");
296
- const text = decodeXmlText(node.inner.trim());
297
- const values = text.length > 0
298
- ? text
299
- .split(",")
300
- .map((v) => v.trim())
301
- .filter((v) => v.length > 0)
302
- : [];
303
- const dim = { name, values };
304
- if (units !== undefined)
305
- dim.units = units;
306
- if (def !== undefined)
307
- dim.default = def;
308
- out.push(dim);
179
+ else {
180
+ warnings.push("WMS EX_GeographicBoundingBox metadata was malformed and ignored.");
309
181
  }
310
- cursor = node.endIndex;
311
182
  }
312
183
  return out;
313
184
  }
314
- function collectChildText(inner, tag) {
315
- const out = [];
316
- let cursor = 0;
317
- while (cursor < inner.length) {
318
- const node = findElement(inner, cursor, tag);
319
- if (!node)
320
- break;
321
- const text = decodeXmlText(node.inner.trim());
322
- if (text.length > 0)
323
- out.push(text);
324
- cursor = node.endIndex;
325
- }
326
- return out;
327
- }
328
- /**
329
- * Return `inner` with every nested `<Layer>...</Layer>` (and self-closed
330
- * `<Layer .../>`) subtree elided. Used by `iterChildLayers` to read the
331
- * layer's *own* metadata without walking into descendant layers.
332
- */
333
- function stripNestedLayers(inner) {
334
- if (inner.indexOf("<Layer") < 0)
335
- return inner;
336
- let cursor = 0;
337
- let out = "";
338
- while (cursor < inner.length) {
339
- const node = findElement(inner, cursor, "Layer");
340
- if (!node) {
341
- out += inner.slice(cursor);
342
- break;
185
+ function parseStyles(node, warnings) {
186
+ return xmlChildren(node, "Style").flatMap((style) => {
187
+ const name = xmlText(xmlChild(style, "Name"));
188
+ if (!name) {
189
+ warnings.push("WMS Style metadata without a name was ignored.");
190
+ return [];
343
191
  }
344
- out += inner.slice(cursor, node.startIndex);
345
- cursor = node.endIndex;
346
- }
347
- return out;
192
+ const legend = xmlChild(style, "LegendURL");
193
+ const resource = xmlChild(legend, "OnlineResource");
194
+ return [
195
+ Object.freeze({
196
+ name,
197
+ ...optional("title", xmlText(xmlChild(style, "Title"))),
198
+ ...optional("legendUrl", resource ? xmlAttribute(resource, "href") : undefined),
199
+ ...optional("legendFormat", xmlText(xmlChild(legend, "Format"))),
200
+ }),
201
+ ];
202
+ });
348
203
  }
349
- /**
350
- * Find the next `<tag …>…</tag>` (or `<tag …/>`) starting at `from`. The
351
- * search ignores element nesting other than the requested tag and walks
352
- * a balanced depth counter for the same tag so nested same-name elements
353
- * (`<Layer>` inside `<Layer>`) terminate against the correct close. Skips
354
- * XML comments (`<!-- … -->`) and CDATA sections (`<![CDATA[ … ]]>`).
355
- */
356
- function findElement(xml, from, tag) {
357
- let i = from;
358
- while (i < xml.length) {
359
- if (xml[i] !== "<") {
360
- i += 1;
361
- continue;
362
- }
363
- const next = xml[i + 1];
364
- if (next === "!" && xml.startsWith("<!--", i)) {
365
- const end = xml.indexOf("-->", i + 4);
366
- if (end < 0)
367
- return undefined;
368
- i = end + 3;
369
- continue;
370
- }
371
- if (next === "!" && xml.startsWith("<![CDATA[", i)) {
372
- const end = xml.indexOf("]]>", i + 9);
373
- if (end < 0)
374
- return undefined;
375
- i = end + 3;
376
- continue;
377
- }
378
- if (next === "?" || next === "!") {
379
- const end = xml.indexOf(">", i);
380
- if (end < 0)
381
- return undefined;
382
- i = end + 1;
383
- continue;
384
- }
385
- const isClosing = next === "/";
386
- const nameStart = isClosing ? i + 2 : i + 1;
387
- const matchesTag = xml.startsWith(tag, nameStart) && isTagBoundary(xml.charCodeAt(nameStart + tag.length));
388
- if (!matchesTag) {
389
- const close = xml.indexOf(">", i);
390
- if (close < 0)
391
- return undefined;
392
- i = close + 1;
393
- continue;
394
- }
395
- const close = xml.indexOf(">", i);
396
- if (close < 0)
397
- return undefined;
398
- const openTag = xml.slice(i, close + 1);
399
- const selfClosing = !isClosing && xml.charCodeAt(close - 1) === 47; // '/'
400
- if (selfClosing) {
401
- return { openTag, inner: "", startIndex: i, endIndex: close + 1 };
402
- }
403
- if (isClosing) {
404
- // unmatched closing tag at this level — caller advanced past the
405
- // opener. Skip past and keep searching.
406
- i = close + 1;
407
- continue;
408
- }
409
- // Walk forward looking for the matching close tag, accounting for
410
- // nested same-name openings.
411
- let depth = 1;
412
- let cursor = close + 1;
413
- while (cursor < xml.length && depth > 0) {
414
- const angle = xml.indexOf("<", cursor);
415
- if (angle < 0)
416
- break;
417
- const next2 = xml[angle + 1];
418
- if (next2 === "!" && xml.startsWith("<!--", angle)) {
419
- const end = xml.indexOf("-->", angle + 4);
420
- if (end < 0)
421
- return undefined;
422
- cursor = end + 3;
423
- continue;
424
- }
425
- if (next2 === "!" && xml.startsWith("<![CDATA[", angle)) {
426
- const end = xml.indexOf("]]>", angle + 9);
427
- if (end < 0)
428
- return undefined;
429
- cursor = end + 3;
430
- continue;
431
- }
432
- const isCloser = next2 === "/";
433
- const inspectStart = isCloser ? angle + 2 : angle + 1;
434
- const matchesSame = xml.startsWith(tag, inspectStart) && isTagBoundary(xml.charCodeAt(inspectStart + tag.length));
435
- const angleClose = xml.indexOf(">", angle);
436
- if (angleClose < 0)
437
- return undefined;
438
- if (matchesSame) {
439
- if (isCloser) {
440
- depth -= 1;
441
- if (depth === 0) {
442
- return {
443
- openTag,
444
- inner: xml.slice(close + 1, angle),
445
- startIndex: i,
446
- endIndex: angleClose + 1,
447
- };
448
- }
449
- }
450
- else {
451
- const innerSelfClose = xml.charCodeAt(angleClose - 1) === 47;
452
- if (!innerSelfClose)
453
- depth += 1;
454
- }
455
- }
456
- cursor = angleClose + 1;
204
+ function parseDimensions(node, warnings) {
205
+ const candidates = [...xmlChildren(node, "Dimension"), ...xmlChildren(node, "Extent")];
206
+ return candidates.flatMap((dimension) => {
207
+ const name = xmlAttribute(dimension, "name");
208
+ if (!name) {
209
+ warnings.push("WMS Dimension metadata without a name was ignored.");
210
+ return [];
457
211
  }
458
- return undefined;
459
- }
460
- return undefined;
212
+ const raw = xmlText(dimension);
213
+ const values = raw
214
+ ? raw
215
+ .split(",")
216
+ .map((value) => value.trim())
217
+ .filter(Boolean)
218
+ : [];
219
+ return [
220
+ Object.freeze({
221
+ name,
222
+ ...optional("units", xmlAttribute(dimension, "units")),
223
+ ...optional("default", xmlAttribute(dimension, "default")),
224
+ values: Object.freeze(unique(values)),
225
+ }),
226
+ ];
227
+ });
461
228
  }
462
- function isTagBoundary(code) {
463
- // Whitespace, '/', or '>' terminate a tag name.
464
- return code === 32 || code === 9 || code === 10 || code === 13 || code === 47 || code === 62;
465
- }
466
- function readAttribute(openTag, name) {
467
- // Match `name="value"` or `name='value'`. Tolerates leading whitespace
468
- // and case-sensitive name match (WMS attribute names are case-sensitive).
469
- const idx = findAttributeIndex(openTag, name);
470
- if (idx < 0)
471
- return undefined;
472
- const after = openTag.slice(idx + name.length);
473
- const eq = after.indexOf("=");
474
- if (eq < 0)
229
+ function finiteNumber(value) {
230
+ if (value === undefined || value.trim() === "")
475
231
  return undefined;
476
- let cursor = eq + 1;
477
- while (cursor < after.length && /\s/.test(after[cursor]))
478
- cursor += 1;
479
- const quote = after[cursor];
480
- if (quote !== '"' && quote !== "'")
481
- return undefined;
482
- const end = after.indexOf(quote, cursor + 1);
483
- if (end < 0)
484
- return undefined;
485
- return decodeXmlText(after.slice(cursor + 1, end));
486
- }
487
- function findAttributeIndex(openTag, name) {
488
- let i = 0;
489
- while (i < openTag.length) {
490
- const found = openTag.indexOf(name, i);
491
- if (found < 0)
492
- return -1;
493
- const before = openTag.charCodeAt(found - 1);
494
- const isBoundaryStart = found === 0 || before === 32 || before === 9 || before === 10 || before === 13;
495
- const after = openTag.charCodeAt(found + name.length);
496
- const isBoundaryEnd = after === 61 || after === 32 || after === 9 || after === 10 || after === 13;
497
- if (isBoundaryStart && isBoundaryEnd) {
498
- return found;
499
- }
500
- i = found + 1;
501
- }
502
- return -1;
232
+ const parsed = Number(value);
233
+ return Number.isFinite(parsed) ? parsed : undefined;
503
234
  }
504
- function readTextElement(xml, tag) {
505
- const node = findElement(xml, 0, tag);
506
- if (!node)
507
- return undefined;
508
- const decoded = decodeXmlText(node.inner.trim());
509
- return decoded;
235
+ function unique(values) {
236
+ return [...new Set(values)];
510
237
  }
511
- function parseFloatAttr(openTag, name) {
512
- const value = readAttribute(openTag, name);
513
- if (value === undefined)
514
- return Number.NaN;
515
- const parsed = Number(value);
516
- return Number.isFinite(parsed) ? parsed : Number.NaN;
238
+ function mergeByKey(parent, local, key) {
239
+ const localKeys = new Set(local.map(key));
240
+ return [...local, ...parent.filter((entry) => !localKeys.has(key(entry)))];
517
241
  }
518
- function parseFloatText(value) {
519
- if (value === undefined)
520
- return Number.NaN;
521
- const parsed = Number(value);
522
- return Number.isFinite(parsed) ? parsed : Number.NaN;
242
+ function optional(key, value) {
243
+ return value === undefined ? {} : { [key]: value };
523
244
  }
524
- function decodeXmlText(text) {
525
- if (text.indexOf("&") < 0 && text.indexOf("<![CDATA[") < 0)
526
- return text;
527
- return decodeXmlEntities(text);
245
+ function compactObject(value) {
246
+ return Object.freeze(Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)));
528
247
  }
529
- /**
530
- * Walk a parsed `WmsCapabilities` tree and yield every named layer
531
- * (skipping container layers without a `name`). Useful for descriptor
532
- * resolution: callers want to bind a `LAYER` name without re-walking
533
- * the parent hierarchy.
534
- */
535
248
  export function* iterateWmsLayers(capabilities) {
536
249
  const stack = [...capabilities.layers];
537
250
  while (stack.length > 0) {
538
251
  const next = stack.shift();
539
- if (!next)
540
- break;
541
252
  if (next.name.length > 0)
542
253
  yield next;
543
- for (const child of next.children) {
544
- stack.push(child);
545
- }
254
+ stack.unshift(...next.children);
546
255
  }
547
256
  }
548
- /**
549
- * Find the first named layer with a matching name, or `undefined` if the
550
- * capabilities document does not advertise it. Walks both root layers and
551
- * children. Intended for the `HonuaWms.layer(name)` resolver path.
552
- */
553
257
  export function findWmsLayer(capabilities, name) {
554
- for (const layer of iterateWmsLayers(capabilities)) {
258
+ for (const layer of iterateWmsLayers(capabilities))
555
259
  if (layer.name === name)
556
260
  return layer;
557
- }
558
261
  return undefined;
559
262
  }