@honua/react 0.1.2-beta.0 → 0.1.7-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contract/discovery.d.ts +34 -0
- package/contract/discovery.d.ts.map +1 -1
- package/contract/index.d.ts +7 -5
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +3 -2
- package/contract/pmtiles.d.ts +10 -1
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +10 -2
- package/contract/protocol-module.d.ts +84 -9
- package/contract/protocol-module.d.ts.map +1 -1
- package/contract/query-filter-fes.d.ts +18 -0
- package/contract/query-filter-fes.d.ts.map +1 -0
- package/contract/query-filter-fes.js +122 -0
- package/contract/query-filter.d.ts +300 -0
- package/contract/query-filter.d.ts.map +1 -0
- package/contract/query-filter.js +883 -0
- package/contract/schema.d.ts +12 -0
- package/contract/schema.d.ts.map +1 -1
- package/contract/schema.js +34 -0
- package/contract/source-capability-support.d.ts.map +1 -1
- package/contract/source-capability-support.js +15 -3
- package/contract/source.d.ts +45 -25
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +719 -479
- package/contract/types.d.ts +88 -19
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +4 -0
- package/contract/widget-source.d.ts.map +1 -1
- package/core/client.d.ts +51 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +234 -54
- package/core/error-classifications.d.ts +23 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +23 -0
- package/core/error-code-registry.d.ts +24 -1
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +23 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +2 -2
- package/core/errors.d.ts.map +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +69 -5
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +327 -10
- package/core/odata-write-codec.d.ts.map +1 -1
- package/core/odata-write-codec.js +0 -1
- package/core/odata.d.ts +6 -0
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +6 -1
- package/core/ogc-processes.d.ts +182 -25
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +602 -30
- package/core/process-runner.d.ts +8 -1
- package/core/process-runner.d.ts.map +1 -1
- package/core/process-runner.js +1 -0
- package/core/protocol-transport.d.ts +2 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/stac.d.ts +32 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +158 -23
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +138 -11
- package/core/types.d.ts.map +1 -1
- package/core/wfs-axis-order.d.ts +18 -0
- package/core/wfs-axis-order.d.ts.map +1 -0
- package/core/wfs-axis-order.js +108 -0
- package/core/wfs-capabilities.d.ts +65 -24
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +237 -27
- package/core/wfs-filter.d.ts +26 -3
- package/core/wfs-filter.d.ts.map +1 -1
- package/core/wfs-filter.js +48 -53
- package/core/wfs-protocol-error.d.ts +12 -0
- package/core/wfs-protocol-error.d.ts.map +1 -0
- package/core/wfs-protocol-error.js +13 -0
- package/core/wfs.d.ts +53 -1
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +251 -29
- package/core/wms-feature-info.d.ts +92 -0
- package/core/wms-feature-info.d.ts.map +1 -0
- package/core/wms-feature-info.js +285 -0
- package/core/wms.d.ts +9 -0
- package/core/wms.d.ts.map +1 -1
- package/core/wms.js +17 -5
- package/esri-compat/event-bus.d.ts +20 -0
- package/esri-compat/event-bus.d.ts.map +1 -1
- package/esri-compat/feature-filter.d.ts +46 -1
- package/esri-compat/feature-filter.d.ts.map +1 -1
- package/esri-compat/feature-filter.js +49 -0
- package/esri-compat/geojson-layer.d.ts +4 -2
- package/esri-compat/geojson-layer.d.ts.map +1 -1
- package/esri-compat/geojson-layer.js +4 -2
- package/esri-compat/identity-manager.js +2 -2
- package/esri-compat/imagery-layer.d.ts +2 -1
- package/esri-compat/imagery-layer.d.ts.map +1 -1
- package/esri-compat/imagery-layer.js +2 -1
- package/esri-compat/layer-list.d.ts +8 -1
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +11 -4
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +1 -1
- package/esri-compat/locator.d.ts +297 -0
- package/esri-compat/locator.d.ts.map +1 -0
- package/esri-compat/locator.js +579 -0
- package/esri-compat/map-image-layer.d.ts.map +1 -1
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +1 -1
- package/esri-compat/scene-view.d.ts.map +1 -1
- package/esri-compat/time-slider.d.ts +29 -0
- package/esri-compat/time-slider.d.ts.map +1 -1
- package/esri-compat/time-slider.js +117 -0
- package/esri-compat/wfs-layer.d.ts +2 -1
- package/esri-compat/wfs-layer.d.ts.map +1 -1
- package/esri-compat/wfs-layer.js +2 -1
- package/esri-compat/widget-host.d.ts +42 -6
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +99 -10
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/runtime.d.ts.map +1 -1
- package/interactions/declarative-contract.d.ts +149 -0
- package/interactions/declarative-contract.d.ts.map +1 -0
- package/interactions/declarative-contract.js +293 -0
- package/interactions/declarative.d.ts +198 -0
- package/interactions/declarative.d.ts.map +1 -0
- package/interactions/declarative.js +470 -0
- package/interactions/feature-state.d.ts +7 -0
- package/interactions/feature-state.d.ts.map +1 -1
- package/interactions/feature-state.js +10 -2
- package/map/data-to-map-bridge.d.ts +1 -1
- package/map/data-to-map-bridge.d.ts.map +1 -1
- package/map/data-to-map-bridge.js +1 -2
- package/map/honua-map.d.ts.map +1 -1
- package/map/honua-map.js +1 -1
- package/map/index.d.ts +2 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +2 -0
- package/map/maplibre-target.js +1 -1
- package/map/realtime-reconciliation-adapter.d.ts +98 -0
- package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
- package/map/realtime-reconciliation-adapter.js +84 -0
- package/map/temporal-playback.d.ts +25 -0
- package/map/temporal-playback.d.ts.map +1 -1
- package/map/temporal-playback.js +29 -1
- package/package.json +4 -3
- package/query-planner/canonical-filter.d.ts +30 -0
- package/query-planner/canonical-filter.d.ts.map +1 -0
- package/query-planner/canonical-filter.js +30 -0
- package/query-planner/columnar-plan.d.ts +63 -0
- package/query-planner/columnar-plan.d.ts.map +1 -0
- package/query-planner/columnar-plan.js +94 -0
- package/query-planner/diagnostics.d.ts +3 -3
- package/query-planner/diagnostics.d.ts.map +1 -1
- package/query-planner/diagnostics.js +17 -4
- package/query-planner/duckdb.d.ts.map +1 -1
- package/query-planner/duckdb.js +16 -2
- package/query-planner/execution-context.d.ts.map +1 -1
- package/query-planner/execution-context.js +3 -1
- package/query-planner/executor.d.ts.map +1 -1
- package/query-planner/executor.js +7 -1
- package/query-planner/geoservices.d.ts.map +1 -1
- package/query-planner/geoservices.js +24 -5
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +29 -5
- package/query-planner/index.d.ts +20 -5
- package/query-planner/index.d.ts.map +1 -1
- package/query-planner/index.js +16 -3
- package/query-planner/ir.d.ts +6 -1
- package/query-planner/ir.d.ts.map +1 -1
- package/query-planner/ir.js +63 -1
- package/query-planner/odata-protocol-module.d.ts +54 -0
- package/query-planner/odata-protocol-module.d.ts.map +1 -0
- package/query-planner/odata-protocol-module.js +199 -0
- package/query-planner/odata-v1.d.ts +23 -0
- package/query-planner/odata-v1.d.ts.map +1 -0
- package/query-planner/odata-v1.js +202 -0
- package/query-planner/odata.d.ts +0 -3
- package/query-planner/odata.d.ts.map +1 -1
- package/query-planner/odata.js +0 -143
- package/query-planner/ogc-features.d.ts.map +1 -1
- package/query-planner/ogc-features.js +23 -1
- package/query-planner/planner.d.ts +2 -2
- package/query-planner/planner.d.ts.map +1 -1
- package/query-planner/planner.js +146 -27
- package/query-planner/representation.d.ts +59 -0
- package/query-planner/representation.d.ts.map +1 -0
- package/query-planner/representation.js +140 -0
- package/query-planner/types.d.ts +153 -3
- package/query-planner/types.d.ts.map +1 -1
- package/query-planner/wfs-protocol-module.d.ts +57 -0
- package/query-planner/wfs-protocol-module.d.ts.map +1 -0
- package/query-planner/wfs-protocol-module.js +731 -0
- package/query-planner/wfs-v1.d.ts +41 -0
- package/query-planner/wfs-v1.d.ts.map +1 -0
- package/query-planner/wfs-v1.js +366 -0
- package/query-planner/wfs.d.ts +1 -3
- package/query-planner/wfs.d.ts.map +1 -1
- package/query-planner/wfs.js +1 -70
- package/react/honua-map.d.ts +3 -1
- package/react/honua-map.d.ts.map +1 -1
- package/react/honua-map.js +11 -1
- package/react/hooks.d.ts +1 -1
- package/react/hooks.js +1 -1
- package/react/selection.d.ts.map +1 -1
- package/react/selection.js +0 -0
- package/react/use-mounted-source.d.ts +1 -1
- package/react/use-mounted-source.js +1 -1
- package/realtime/checkpoint-store.d.ts +206 -0
- package/realtime/checkpoint-store.d.ts.map +1 -0
- package/realtime/checkpoint-store.js +504 -0
- package/realtime/contract.d.ts +142 -0
- package/realtime/contract.d.ts.map +1 -0
- package/realtime/contract.js +154 -0
- package/realtime/exploration.d.ts +16 -0
- package/realtime/exploration.d.ts.map +1 -0
- package/realtime/exploration.js +32 -0
- package/realtime/honua-server.d.ts +131 -0
- package/realtime/honua-server.d.ts.map +1 -0
- package/realtime/honua-server.js +302 -0
- package/realtime/index.d.ts +39 -0
- package/realtime/index.d.ts.map +1 -0
- package/realtime/index.js +26 -0
- package/realtime/odata-delta.d.ts +114 -0
- package/realtime/odata-delta.d.ts.map +1 -0
- package/realtime/odata-delta.js +483 -0
- package/realtime/projections.d.ts +30 -0
- package/realtime/projections.d.ts.map +1 -0
- package/realtime/projections.js +51 -0
- package/realtime/reconciliation.d.ts +245 -0
- package/realtime/reconciliation.d.ts.map +1 -0
- package/realtime/reconciliation.js +377 -0
- package/realtime/reducer.d.ts +14 -0
- package/realtime/reducer.d.ts.map +1 -0
- package/realtime/reducer.js +351 -0
- package/realtime/resumable-transport.d.ts +111 -0
- package/realtime/resumable-transport.d.ts.map +1 -0
- package/realtime/resumable-transport.js +406 -0
- package/realtime/resumable.d.ts +104 -0
- package/realtime/resumable.d.ts.map +1 -0
- package/realtime/resumable.js +632 -0
- package/realtime/sse.d.ts +23 -0
- package/realtime/sse.d.ts.map +1 -0
- package/realtime/sse.js +153 -0
- package/realtime/store.d.ts +18 -0
- package/realtime/store.d.ts.map +1 -0
- package/realtime/store.js +63 -0
- package/realtime/types.d.ts +190 -0
- package/realtime/types.d.ts.map +1 -0
- package/realtime/types.js +10 -0
- package/realtime/websocket.d.ts +60 -0
- package/realtime/websocket.d.ts.map +1 -0
- package/realtime/websocket.js +157 -0
- package/realtime/wire.d.ts +28 -0
- package/realtime/wire.d.ts.map +1 -0
- package/realtime/wire.js +73 -0
- package/runtime/index.d.ts +6 -2
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +3 -1
- package/runtime/load-package.d.ts +24 -0
- package/runtime/load-package.d.ts.map +1 -1
- package/runtime/load-package.js +45 -1
- package/runtime/maplibre-renderer.d.ts +11 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -1
- package/runtime/maplibre-renderer.js +32 -7
- package/runtime/offline-region-protocol.d.ts +73 -0
- package/runtime/offline-region-protocol.d.ts.map +1 -0
- package/runtime/offline-region-protocol.js +63 -0
- package/runtime/offline-region-style.d.ts +78 -0
- package/runtime/offline-region-style.d.ts.map +1 -0
- package/runtime/offline-region-style.js +147 -0
- package/runtime/pmtiles-protocol.d.ts +7 -8
- package/runtime/pmtiles-protocol.d.ts.map +1 -1
- package/runtime/pmtiles-protocol.js +19 -66
- package/runtime/protocol-registry.d.ts +55 -0
- package/runtime/protocol-registry.d.ts.map +1 -0
- package/runtime/protocol-registry.js +103 -0
- package/runtime/runtime.d.ts +2 -0
- package/runtime/runtime.d.ts.map +1 -1
- package/runtime/runtime.js +6 -1
- package/runtime/style-compose.d.ts.map +1 -1
- package/runtime/style-compose.js +78 -9
- package/studio/index.d.ts +24 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +21 -7
- package/studio/lifecycle-client.d.ts +164 -0
- package/studio/lifecycle-client.d.ts.map +1 -0
- package/studio/lifecycle-client.js +299 -0
- package/studio/lifecycle-errors.d.ts +91 -0
- package/studio/lifecycle-errors.d.ts.map +1 -0
- package/studio/lifecycle-errors.js +77 -0
- package/studio/lifecycle-types.d.ts +321 -0
- package/studio/lifecycle-types.d.ts.map +1 -0
- package/studio/lifecycle-types.js +22 -0
- package/webmap/convert-label.js +0 -1
- package/webmap/convert-symbol.d.ts.map +1 -1
package/contract/source.js
CHANGED
|
@@ -13,19 +13,25 @@
|
|
|
13
13
|
* @module
|
|
14
14
|
*/
|
|
15
15
|
import { HonuaCapabilityNotSupportedError, HonuaHttpError } from "../core/errors.js";
|
|
16
|
-
import {
|
|
16
|
+
import { esriGeometryToGeoJSON } from "../core/esri-geojson.js";
|
|
17
|
+
import { buildOdataSpatialFilter, geometryToWkt, odataFieldSchema, rewriteWhereToOdataFilter, } from "../core/odata.js";
|
|
17
18
|
import { HonuaOgcCollectionMap, HonuaOgcMaps } from "../core/ogc-maps.js";
|
|
18
19
|
import { HonuaOgcRecordCollection } from "../core/ogc-records.js";
|
|
19
20
|
import { HonuaOgcTiles, HonuaOgcTileset } from "../core/ogc-tiles.js";
|
|
20
|
-
import { trimTrailingSlashes } from "../core/path-utils.js";
|
|
21
21
|
import { HonuaStacStaticCatalog } from "../core/stac-static.js";
|
|
22
22
|
import { HonuaStacSearch } from "../core/stac.js";
|
|
23
23
|
import { HonuaFeatureLayer, HonuaGeometryService, HonuaGeoprocessingService, HonuaImageService, HonuaMapLayer, HonuaMapService, HonuaOgcFeatureCollection, HonuaOgcFeatures, } from "../core/surfaces.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { geoJsonGeometryToGml } from "../core/wfs-filter.js";
|
|
25
|
+
import { executeWmsFeatureInfo, resolveWmsQueryCrs } from "../core/wms-feature-info.js";
|
|
26
26
|
import { HonuaWms, HonuaWmsLayer, parseWmsLayerNames } from "../core/wms.js";
|
|
27
27
|
import { HonuaWmts, HonuaWmtsLayer, HonuaWmtsTileset } from "../core/wmts.js";
|
|
28
|
+
import { createQueryIr } from "../query-planner/ir.js";
|
|
29
|
+
import { createOdataProtocolCompiledQuery, odataProtocolModule, odataProtocolResultFromPage, } from "../query-planner/odata-protocol-module.js";
|
|
30
|
+
import { HonuaQueryPlanningError, } from "../query-planner/types.js";
|
|
31
|
+
import { HonuaWfsProtocolError, assertWfsPageProgress, buildWfsPostGetFeatureBody, checkedWfsPageEnd, wfsProtocolModule, } from "../query-planner/wfs-protocol-module.js";
|
|
32
|
+
import { wfsSpatialFilterNeedsGeometryProperty, wfsSrsNameFromOutSr } from "../query-planner/wfs-v1.js";
|
|
28
33
|
import { pmtilesProtocolModule } from "./pmtiles.js";
|
|
34
|
+
import { andQueryFilters, compileQueryFilterToCql2Text, compileQueryFilterToOData, compileQueryFilterToSql92, compileTemporalFilterToGeoServicesTime, compileTemporalFilterToOgcDatetime, queryFilterConstraints, queryFilterHasSpatial, temporalFilterToExpression, } from "./query-filter.js";
|
|
29
35
|
import { addCapabilitySupport, normalizeCapabilityDescriptor } from "./source-capability-support.js";
|
|
30
36
|
import { CAPABILITIES, PROTOCOL_DEFAULT_CAPABILITIES, } from "./types.js";
|
|
31
37
|
/**
|
|
@@ -43,7 +49,7 @@ import { CAPABILITIES, PROTOCOL_DEFAULT_CAPABILITIES, } from "./types.js";
|
|
|
43
49
|
*
|
|
44
50
|
* Operations the canonical surface does not cover stay reachable through the typed
|
|
45
51
|
* `source.protocol(...)` escape hatch (e.g. ImageServer `exportImage`, GeometryServer
|
|
46
|
-
* `project`, GPServer `submitJob`, WFS `
|
|
52
|
+
* `project`, GPServer `submitJob`, WFS `GetPropertyValue`).
|
|
47
53
|
*
|
|
48
54
|
* @param options - The dataset id, the `HonuaClient` to talk to, the list of
|
|
49
55
|
* `SourceDescriptor`s, and (optionally) `capabilityPolicy` and a custom
|
|
@@ -71,7 +77,6 @@ import { CAPABILITIES, PROTOCOL_DEFAULT_CAPABILITIES, } from "./types.js";
|
|
|
71
77
|
*
|
|
72
78
|
* const parcels = dataset.source("parcels-fs")!;
|
|
73
79
|
* const result = await parcels.queryAll({
|
|
74
|
-
* where: "STATUS = 'ACTIVE'",
|
|
75
80
|
* outFields: ["OBJECTID", "NAME"],
|
|
76
81
|
* returnGeometry: true,
|
|
77
82
|
* pagination: { limit: 500 },
|
|
@@ -81,6 +86,10 @@ import { CAPABILITIES, PROTOCOL_DEFAULT_CAPABILITIES, } from "./types.js";
|
|
|
81
86
|
* ```
|
|
82
87
|
*/
|
|
83
88
|
export function createDataset(options) {
|
|
89
|
+
return createDatasetWithAdapterSeeds(options);
|
|
90
|
+
}
|
|
91
|
+
/** @internal Reuse metadata already reviewed by top-level connect() adapters. */
|
|
92
|
+
export function createDatasetWithAdapterSeeds(options, seeds = {}) {
|
|
84
93
|
const { id, client, sources, resolveSource } = options;
|
|
85
94
|
const policy = options.capabilityPolicy ?? "strict";
|
|
86
95
|
const descriptors = new Map();
|
|
@@ -97,7 +106,7 @@ export function createDataset(options) {
|
|
|
97
106
|
const cached = handles.get(descriptor.id);
|
|
98
107
|
if (cached)
|
|
99
108
|
return cached;
|
|
100
|
-
const builtIn = buildBuiltInSource(descriptor, client, policy);
|
|
109
|
+
const builtIn = buildBuiltInSource(descriptor, client, policy, seeds);
|
|
101
110
|
const built = builtIn ?? resolveSource?.(descriptor, { client, capabilityPolicy: policy });
|
|
102
111
|
if (!built) {
|
|
103
112
|
throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, descriptor.id);
|
|
@@ -136,7 +145,7 @@ export function createDataset(options) {
|
|
|
136
145
|
};
|
|
137
146
|
}
|
|
138
147
|
// ── Built-in resolver ─────────────────────────────────────────
|
|
139
|
-
function buildBuiltInSource(descriptor, client, policy) {
|
|
148
|
+
function buildBuiltInSource(descriptor, client, policy, seeds) {
|
|
140
149
|
switch (descriptor.protocol) {
|
|
141
150
|
case "grpc":
|
|
142
151
|
// Honua gRPC is a transport-selectable fast path over the same
|
|
@@ -176,7 +185,11 @@ function buildBuiltInSource(descriptor, client, policy) {
|
|
|
176
185
|
case "odata":
|
|
177
186
|
return odataSource(descriptor, client, policy);
|
|
178
187
|
case "pmtiles":
|
|
179
|
-
return pmtilesSource(descriptor, client, policy
|
|
188
|
+
return pmtilesSource(descriptor, client, policy, {
|
|
189
|
+
...(seeds.pmtilesDescriptions?.get(descriptor.id)
|
|
190
|
+
? { preloadedDescription: seeds.pmtilesDescriptions.get(descriptor.id) }
|
|
191
|
+
: {}),
|
|
192
|
+
});
|
|
180
193
|
default:
|
|
181
194
|
return undefined;
|
|
182
195
|
}
|
|
@@ -202,8 +215,8 @@ function buildBuiltInSource(descriptor, client, policy) {
|
|
|
202
215
|
* }],
|
|
203
216
|
* });
|
|
204
217
|
* const result = await dataset.source("parcels-fs")!.queryAll({
|
|
205
|
-
* where: "STATUS = 'ACTIVE'",
|
|
206
218
|
* outFields: ["OBJECTID", "NAME"],
|
|
219
|
+
* pagination: { limit: 500 },
|
|
207
220
|
* });
|
|
208
221
|
* ```
|
|
209
222
|
*/
|
|
@@ -224,7 +237,7 @@ export function geoServicesFeatureSource(descriptor, client, policy) {
|
|
|
224
237
|
return makeSource(descriptor, caps, policy, adapterRegistry, {
|
|
225
238
|
async query(request) {
|
|
226
239
|
ensureCapability(descriptor, caps, "query");
|
|
227
|
-
const requestParams = toFeatureLayerRequest(request);
|
|
240
|
+
const requestParams = toFeatureLayerRequest(request, descriptor);
|
|
228
241
|
if (request?.aggregation) {
|
|
229
242
|
ensureCapability(descriptor, caps, "queryAggregate");
|
|
230
243
|
const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
|
|
@@ -235,7 +248,7 @@ export function geoServicesFeatureSource(descriptor, client, policy) {
|
|
|
235
248
|
},
|
|
236
249
|
async queryAll(request) {
|
|
237
250
|
ensureCapability(descriptor, caps, "query");
|
|
238
|
-
const params = withPagingBounds(toFeatureLayerRequest(request), request?.pagination?.limit);
|
|
251
|
+
const params = withPagingBounds(toFeatureLayerRequest(request, descriptor), request?.pagination?.limit);
|
|
239
252
|
const features = await layer.queryFeaturesAll(params);
|
|
240
253
|
const { features: limited, exceededTransferLimit } = applyQueryAllLimit(features, request?.pagination?.limit);
|
|
241
254
|
return {
|
|
@@ -246,19 +259,19 @@ export function geoServicesFeatureSource(descriptor, client, policy) {
|
|
|
246
259
|
},
|
|
247
260
|
async queryAggregate(request) {
|
|
248
261
|
ensureCapability(descriptor, caps, "queryAggregate");
|
|
249
|
-
const requestParams = toFeatureLayerRequest(request);
|
|
262
|
+
const requestParams = toFeatureLayerRequest(request, descriptor);
|
|
250
263
|
const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
|
|
251
264
|
const response = await layer.queryFeatures({ ...requestParams, ...aggFields });
|
|
252
265
|
return aggregateResultFromFeatureLayer(response, aggregateAlias);
|
|
253
266
|
},
|
|
254
267
|
async queryExtent(request) {
|
|
255
268
|
ensureCapability(descriptor, caps, "queryExtent");
|
|
256
|
-
const response = await layer.queryFeatures(toExtentOnlyRequest(toFeatureLayerRequest(request)));
|
|
269
|
+
const response = await layer.queryFeatures(toExtentOnlyRequest(toFeatureLayerRequest(request, descriptor)));
|
|
257
270
|
return extractExtentEnvelope(response);
|
|
258
271
|
},
|
|
259
272
|
async *stream(request) {
|
|
260
273
|
ensureCapability(descriptor, caps, "stream");
|
|
261
|
-
const stream = layer.queryFeaturesStream(withStreamPageSize(toFeatureLayerRequest(request), request?.pagination?.limit));
|
|
274
|
+
const stream = layer.queryFeaturesStream(withStreamPageSize(toFeatureLayerRequest(request, descriptor), request?.pagination?.limit));
|
|
262
275
|
for await (const page of stream) {
|
|
263
276
|
yield {
|
|
264
277
|
features: page,
|
|
@@ -268,7 +281,7 @@ export function geoServicesFeatureSource(descriptor, client, policy) {
|
|
|
268
281
|
},
|
|
269
282
|
async queryObjectIds(request) {
|
|
270
283
|
ensureCapability(descriptor, caps, "queryObjectIds");
|
|
271
|
-
return layer.queryObjectIds(toFeatureLayerRequest(request));
|
|
284
|
+
return layer.queryObjectIds(toFeatureLayerRequest(request, descriptor));
|
|
272
285
|
},
|
|
273
286
|
async applyEdits(envelope) {
|
|
274
287
|
ensureCapability(descriptor, caps, "applyEdits");
|
|
@@ -318,7 +331,7 @@ export function geoServicesMapServiceSource(descriptor, client, policy) {
|
|
|
318
331
|
return makeSource(descriptor, caps, policy, adapterRegistry, {
|
|
319
332
|
async query(request) {
|
|
320
333
|
ensureCapability(descriptor, caps, "query");
|
|
321
|
-
const params = toFeatureLayerRequest(request);
|
|
334
|
+
const params = toFeatureLayerRequest(request, descriptor);
|
|
322
335
|
if (request?.aggregation) {
|
|
323
336
|
ensureCapability(descriptor, caps, "queryAggregate");
|
|
324
337
|
const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
|
|
@@ -329,7 +342,7 @@ export function geoServicesMapServiceSource(descriptor, client, policy) {
|
|
|
329
342
|
},
|
|
330
343
|
async queryAll(request) {
|
|
331
344
|
ensureCapability(descriptor, caps, "query");
|
|
332
|
-
const params = withPagingBounds(toFeatureLayerRequest(request), request?.pagination?.limit);
|
|
345
|
+
const params = withPagingBounds(toFeatureLayerRequest(request, descriptor), request?.pagination?.limit);
|
|
333
346
|
const features = await layer.queryFeaturesAll(params);
|
|
334
347
|
const typed = features.map((toTypedFeature));
|
|
335
348
|
const { features: limited, exceededTransferLimit } = applyQueryAllLimit(typed, request?.pagination?.limit);
|
|
@@ -341,19 +354,19 @@ export function geoServicesMapServiceSource(descriptor, client, policy) {
|
|
|
341
354
|
},
|
|
342
355
|
async queryAggregate(request) {
|
|
343
356
|
ensureCapability(descriptor, caps, "queryAggregate");
|
|
344
|
-
const params = toFeatureLayerRequest(request);
|
|
357
|
+
const params = toFeatureLayerRequest(request, descriptor);
|
|
345
358
|
const { aggregateAlias, ...aggFields } = buildAggregationFields(request.aggregation);
|
|
346
359
|
const response = await layer.queryFeatures({ ...params, ...aggFields });
|
|
347
360
|
return aggregateResultFromUntyped(response, aggregateAlias);
|
|
348
361
|
},
|
|
349
362
|
async queryExtent(request) {
|
|
350
363
|
ensureCapability(descriptor, caps, "queryExtent");
|
|
351
|
-
const response = await layer.queryFeatures(toExtentOnlyRequest(toFeatureLayerRequest(request)));
|
|
364
|
+
const response = await layer.queryFeatures(toExtentOnlyRequest(toFeatureLayerRequest(request, descriptor)));
|
|
352
365
|
return extractExtentEnvelope(response);
|
|
353
366
|
},
|
|
354
367
|
async *stream(request) {
|
|
355
368
|
ensureCapability(descriptor, caps, "stream");
|
|
356
|
-
const stream = layer.queryFeaturesStream(withStreamPageSize(toFeatureLayerRequest(request), request?.pagination?.limit));
|
|
369
|
+
const stream = layer.queryFeaturesStream(withStreamPageSize(toFeatureLayerRequest(request, descriptor), request?.pagination?.limit));
|
|
357
370
|
for await (const page of stream) {
|
|
358
371
|
yield {
|
|
359
372
|
features: page.map((toTypedFeature)),
|
|
@@ -363,7 +376,7 @@ export function geoServicesMapServiceSource(descriptor, client, policy) {
|
|
|
363
376
|
},
|
|
364
377
|
async queryObjectIds(request) {
|
|
365
378
|
ensureCapability(descriptor, caps, "queryObjectIds");
|
|
366
|
-
return layer.queryObjectIds(toFeatureLayerRequest(request));
|
|
379
|
+
return layer.queryObjectIds(toFeatureLayerRequest(request, descriptor));
|
|
367
380
|
},
|
|
368
381
|
async applyEdits() {
|
|
369
382
|
// MapServer is read-only; applyEdits exists only on the FeatureServer
|
|
@@ -395,7 +408,6 @@ export function geoServicesMapServiceSource(descriptor, client, policy) {
|
|
|
395
408
|
* }],
|
|
396
409
|
* });
|
|
397
410
|
* const result = await dataset.source("addresses-ogc")!.queryAll({
|
|
398
|
-
* where: "city = 'Honolulu'",
|
|
399
411
|
* pagination: { limit: 200 },
|
|
400
412
|
* });
|
|
401
413
|
* ```
|
|
@@ -424,7 +436,7 @@ export function ogcFeaturesSource(descriptor, client, policy) {
|
|
|
424
436
|
// degradable capability here.
|
|
425
437
|
ensureCapabilityOrFallback(descriptor, caps, "queryAggregate", policy);
|
|
426
438
|
}
|
|
427
|
-
const response = await collection.items(toOgcRequest(request));
|
|
439
|
+
const response = await collection.items(toOgcRequest(request, descriptor));
|
|
428
440
|
const features = response.features.map((toTypedFeatureFromOgc));
|
|
429
441
|
const totalCount = response.numberMatched;
|
|
430
442
|
const degraded = [];
|
|
@@ -460,7 +472,7 @@ export function ogcFeaturesSource(descriptor, client, policy) {
|
|
|
460
472
|
// exist. Mirrors the GeoServices `withPagingBounds` + `applyQueryAllLimit`
|
|
461
473
|
// pattern so `queryAll({ pagination: { limit } })` has the same
|
|
462
474
|
// contract across protocols.
|
|
463
|
-
const ogcRequest = toOgcRequest(request);
|
|
475
|
+
const ogcRequest = toOgcRequest(request, descriptor);
|
|
464
476
|
if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
|
|
465
477
|
ogcRequest.limit = limit + 1;
|
|
466
478
|
}
|
|
@@ -478,7 +490,7 @@ export function ogcFeaturesSource(descriptor, client, policy) {
|
|
|
478
490
|
// the descriptor must advertise `queryAggregate`; under `degraded` the
|
|
479
491
|
// fallback runs unconditionally.
|
|
480
492
|
ensureCapabilityOrFallback(descriptor, caps, "queryAggregate", policy);
|
|
481
|
-
const all = await collection.itemsAll(withUnboundedMaxPages(toOgcMaterializedRequest(request)));
|
|
493
|
+
const all = await collection.itemsAll(withUnboundedMaxPages(toOgcMaterializedRequest(request, descriptor)));
|
|
482
494
|
const features = all.map((toTypedFeatureFromOgc));
|
|
483
495
|
return {
|
|
484
496
|
features,
|
|
@@ -512,12 +524,12 @@ export function ogcFeaturesSource(descriptor, client, policy) {
|
|
|
512
524
|
const [xmin, ymin, xmax, ymax] = bbox;
|
|
513
525
|
return { extent: { xmin, ymin, xmax, ymax } };
|
|
514
526
|
}
|
|
515
|
-
const all = await collection.itemsAll(withUnboundedMaxPages(toOgcMaterializedRequest(request)));
|
|
527
|
+
const all = await collection.itemsAll(withUnboundedMaxPages(toOgcMaterializedRequest(request, descriptor)));
|
|
516
528
|
return computeExtentFromOgcFeatures(all);
|
|
517
529
|
},
|
|
518
530
|
async *stream(request) {
|
|
519
531
|
ensureCapability(descriptor, caps, "stream");
|
|
520
|
-
const stream = collection.itemsStream(withUnboundedMaxPages(toOgcRequest(request)));
|
|
532
|
+
const stream = collection.itemsStream(withUnboundedMaxPages(toOgcRequest(request, descriptor)));
|
|
521
533
|
for await (const page of stream) {
|
|
522
534
|
yield {
|
|
523
535
|
features: page.map((toTypedFeatureFromOgc)),
|
|
@@ -530,7 +542,7 @@ export function ogcFeaturesSource(descriptor, client, policy) {
|
|
|
530
542
|
// OGC `/items` does not expose a server-side ids-only mode; drain the
|
|
531
543
|
// matching set and project the GeoJSON `id`. Callers that need a
|
|
532
544
|
// bounded scan should pass `pagination.limit`.
|
|
533
|
-
const all = await collection.itemsAll(withUnboundedMaxPages(toOgcRequest(request)));
|
|
545
|
+
const all = await collection.itemsAll(withUnboundedMaxPages(toOgcRequest(request, descriptor)));
|
|
534
546
|
const ids = [];
|
|
535
547
|
for (const feature of all) {
|
|
536
548
|
if (feature.id !== undefined && feature.id !== null) {
|
|
@@ -635,13 +647,13 @@ export function geoServicesImageSource(descriptor, client, policy) {
|
|
|
635
647
|
// ImageServer query returns the raster catalog. The GeoServices
|
|
636
648
|
// request shape is identical to FeatureServer query so we reuse
|
|
637
649
|
// `toFeatureLayerRequest` and dispatch through the service.
|
|
638
|
-
const response = await service.queryRasterCatalog(toFeatureLayerRequest(request));
|
|
650
|
+
const response = await service.queryRasterCatalog(toFeatureLayerRequest(request, descriptor));
|
|
639
651
|
return featureLayerResultFromUntyped(response);
|
|
640
652
|
},
|
|
641
653
|
async queryAll(request) {
|
|
642
654
|
ensureCapability(descriptor, caps, "query");
|
|
643
655
|
requireImageServerCompatibleQuery(request);
|
|
644
|
-
const baseParams = toFeatureLayerRequest(request);
|
|
656
|
+
const baseParams = toFeatureLayerRequest(request, descriptor);
|
|
645
657
|
const limit = request?.pagination?.limit;
|
|
646
658
|
// ImageServer's catalog endpoint supports `resultOffset`/`resultRecordCount`
|
|
647
659
|
// but the wrapper does not expose a `queryAllRasterCatalog` helper, so
|
|
@@ -691,7 +703,7 @@ export function geoServicesImageSource(descriptor, client, policy) {
|
|
|
691
703
|
async queryExtent(request) {
|
|
692
704
|
ensureCapability(descriptor, caps, "queryExtent");
|
|
693
705
|
requireImageServerCompatibleQuery(request);
|
|
694
|
-
const response = await service.queryRasterCatalog(toExtentOnlyRequest(toFeatureLayerRequest(request)));
|
|
706
|
+
const response = await service.queryRasterCatalog(toExtentOnlyRequest(toFeatureLayerRequest(request, descriptor)));
|
|
695
707
|
return extractExtentEnvelope(response);
|
|
696
708
|
},
|
|
697
709
|
// biome-ignore lint/correctness/useYield: ImageServer has no streaming raster-catalog mode; this generator refuses iteration rather than silently emit a single page
|
|
@@ -701,7 +713,7 @@ export function geoServicesImageSource(descriptor, client, policy) {
|
|
|
701
713
|
async queryObjectIds(request) {
|
|
702
714
|
ensureCapability(descriptor, caps, "queryObjectIds");
|
|
703
715
|
requireImageServerCompatibleQuery(request);
|
|
704
|
-
return service.queryRasterCatalogObjectIds(toFeatureLayerRequest(request));
|
|
716
|
+
return service.queryRasterCatalogObjectIds(toFeatureLayerRequest(request, descriptor));
|
|
705
717
|
},
|
|
706
718
|
async applyEdits() {
|
|
707
719
|
throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
|
|
@@ -828,9 +840,9 @@ export function ogcTilesSource(descriptor, client, policy) {
|
|
|
828
840
|
* console.log(info.bounds, info.vectorLayers.map((layer) => layer.id));
|
|
829
841
|
* ```
|
|
830
842
|
*/
|
|
831
|
-
export function pmtilesSource(descriptor, _client, policy) {
|
|
843
|
+
export function pmtilesSource(descriptor, _client, policy, deps = {}) {
|
|
832
844
|
descriptor = normalizeCapabilityDescriptor(descriptor);
|
|
833
|
-
const discovered = pmtilesProtocolModule().discover(descriptor);
|
|
845
|
+
const discovered = pmtilesProtocolModule(deps).discover(descriptor);
|
|
834
846
|
if (discovered instanceof Promise) {
|
|
835
847
|
// PMTiles discovery is always synchronous (the reader opens lazily on the
|
|
836
848
|
// handle's own first `describe()` call); a module that started returning
|
|
@@ -886,7 +898,7 @@ export function ogcMapsSource(descriptor, client, policy) {
|
|
|
886
898
|
* @example
|
|
887
899
|
* ```ts
|
|
888
900
|
* const records = dataset.source("catalog")!;
|
|
889
|
-
* const result = await records.query({
|
|
901
|
+
* const result = await records.query({ pagination: { limit: 100 } });
|
|
890
902
|
* ```
|
|
891
903
|
*/
|
|
892
904
|
export function ogcRecordsSource(descriptor, client, policy) {
|
|
@@ -908,7 +920,7 @@ export function ogcRecordsSource(descriptor, client, policy) {
|
|
|
908
920
|
if (request?.aggregation) {
|
|
909
921
|
throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
|
|
910
922
|
}
|
|
911
|
-
const response = await collection.search(toOgcRecordsRequest(request));
|
|
923
|
+
const response = await collection.search(toOgcRecordsRequest(request, descriptor));
|
|
912
924
|
const features = (response.features ?? []).map((toTypedFeatureFromOgcRecord));
|
|
913
925
|
const totalCount = response.numberMatched;
|
|
914
926
|
return {
|
|
@@ -921,7 +933,7 @@ export function ogcRecordsSource(descriptor, client, policy) {
|
|
|
921
933
|
ensureCapability(descriptor, caps, "query");
|
|
922
934
|
const limit = request?.pagination?.limit;
|
|
923
935
|
const records = await collection.searchAll({
|
|
924
|
-
...toOgcRecordsRequest(request),
|
|
936
|
+
...toOgcRecordsRequest(request, descriptor),
|
|
925
937
|
...withPagingBounds({}, limit),
|
|
926
938
|
});
|
|
927
939
|
const typed = records.map((toTypedFeatureFromOgcRecord));
|
|
@@ -942,7 +954,7 @@ export function ogcRecordsSource(descriptor, client, policy) {
|
|
|
942
954
|
ensureCapability(descriptor, caps, "stream");
|
|
943
955
|
const limit = request?.pagination?.limit;
|
|
944
956
|
const stream = collection.searchStream({
|
|
945
|
-
...toOgcRecordsRequest(request),
|
|
957
|
+
...toOgcRecordsRequest(request, descriptor),
|
|
946
958
|
pageSize: limit !== undefined ? Math.max(1, limit) : undefined,
|
|
947
959
|
maxPages: Number.MAX_SAFE_INTEGER,
|
|
948
960
|
});
|
|
@@ -957,7 +969,7 @@ export function ogcRecordsSource(descriptor, client, policy) {
|
|
|
957
969
|
ensureCapability(descriptor, caps, "queryObjectIds");
|
|
958
970
|
const limit = request?.pagination?.limit;
|
|
959
971
|
const records = await collection.searchAll({
|
|
960
|
-
...toOgcRecordsRequest(request),
|
|
972
|
+
...toOgcRecordsRequest(request, descriptor),
|
|
961
973
|
...withPagingBounds({}, limit),
|
|
962
974
|
});
|
|
963
975
|
const ids = [];
|
|
@@ -1012,14 +1024,21 @@ export function wmsSource(descriptor, client, policy) {
|
|
|
1012
1024
|
descriptor = normalizeCapabilityDescriptor(descriptor);
|
|
1013
1025
|
const advertisedCaps = descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wms;
|
|
1014
1026
|
if (typeof descriptor.locator.serviceId !== "string" || descriptor.locator.serviceId.length === 0) {
|
|
1015
|
-
|
|
1027
|
+
const featureInfoBinding = descriptor.locator.featureInfo;
|
|
1028
|
+
if (descriptor.locator.raster?.kind !== "wms-kvp" && featureInfoBinding?.kind !== "wms-kvp") {
|
|
1016
1029
|
requireWmsLocator(descriptor);
|
|
1030
|
+
}
|
|
1017
1031
|
// Raw third-party discovery is renderable through the existing descriptor
|
|
1018
|
-
// → MapLibre raster projection
|
|
1019
|
-
//
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1032
|
+
// → MapLibre raster projection, and queryable whenever discovery reviewed a
|
|
1033
|
+
// GetFeatureInfo operation (queryable layer + projectable INFO_FORMAT +
|
|
1034
|
+
// provable CRS axis order). Without that binding the query surface stays
|
|
1035
|
+
// fail-closed.
|
|
1036
|
+
if (featureInfoBinding?.kind !== "wms-kvp" || !advertisedCaps.has("query")) {
|
|
1037
|
+
const caps = new Set([...advertisedCaps].filter((capability) => capability !== "query"));
|
|
1038
|
+
descriptor = { ...descriptor, capabilities: caps };
|
|
1039
|
+
return makeSource(descriptor, caps, policy, {}, unsupportedFeatureSurface(descriptor));
|
|
1040
|
+
}
|
|
1041
|
+
return standaloneWmsSource({ ...descriptor, capabilities: advertisedCaps }, featureInfoBinding, client, policy, advertisedCaps);
|
|
1023
1042
|
}
|
|
1024
1043
|
const { serviceId } = requireWmsLocator(descriptor);
|
|
1025
1044
|
const layerName = descriptor.locator.typeName;
|
|
@@ -1049,24 +1068,11 @@ export function wmsSource(descriptor, client, policy) {
|
|
|
1049
1068
|
ensureCapability(descriptor, caps, "query");
|
|
1050
1069
|
requireWmsCompatibleQuery(descriptor, request);
|
|
1051
1070
|
const layers = wmsRequireLayers(descriptor, layerName);
|
|
1052
|
-
const
|
|
1053
|
-
const bboxRadius = 0.0001; // tiny envelope around the point keeps the request a 1×1 image.
|
|
1054
|
-
const widthHeight = 1;
|
|
1055
|
-
const featureCount = request?.pagination?.limit;
|
|
1071
|
+
const point = wmsExtractPointFromQuery(request, descriptor);
|
|
1056
1072
|
const response = await client.getWmsFeatureInfo({
|
|
1057
1073
|
serviceId,
|
|
1058
|
-
layers,
|
|
1059
|
-
...(styleId !== undefined ? { styles: [styleId] } : {}),
|
|
1060
|
-
queryLayers: layers,
|
|
1061
|
-
crs,
|
|
1062
|
-
bbox: [px - bboxRadius, py - bboxRadius, px + bboxRadius, py + bboxRadius],
|
|
1063
|
-
width: widthHeight,
|
|
1064
|
-
height: widthHeight,
|
|
1065
|
-
i: 0,
|
|
1066
|
-
j: 0,
|
|
1074
|
+
...wmsPointFeatureInfoRequest({ layers, styleId, crs: point.crs, x: point.x, y: point.y, request }),
|
|
1067
1075
|
infoFormat: "application/json",
|
|
1068
|
-
...(featureCount !== undefined ? { featureCount } : {}),
|
|
1069
|
-
...(request?.signal ? { signal: request.signal } : {}),
|
|
1070
1076
|
});
|
|
1071
1077
|
return {
|
|
1072
1078
|
features: response.features ?? [],
|
|
@@ -1102,6 +1108,97 @@ export function wmsSource(descriptor, client, policy) {
|
|
|
1102
1108
|
attachments: unsupportedAttachmentApi(descriptor),
|
|
1103
1109
|
});
|
|
1104
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Radius of the envelope drawn around the queried point. WMS has no point
|
|
1113
|
+
* request: `GetFeatureInfo` addresses a pixel inside a rendered image, so the
|
|
1114
|
+
* canonical point query renders a 1×1 image whose BBOX is this tiny envelope
|
|
1115
|
+
* and reads pixel (0, 0).
|
|
1116
|
+
*/
|
|
1117
|
+
const WMS_FEATURE_INFO_POINT_RADIUS = 0.0001;
|
|
1118
|
+
/**
|
|
1119
|
+
* Build the shared `GetFeatureInfo` envelope for a canonical point query. Both
|
|
1120
|
+
* the Honua service-id path and the capabilities-driven third-party path use
|
|
1121
|
+
* it, so LAYERS / QUERY_LAYERS / STYLES / BBOX / WIDTH / HEIGHT / I / J /
|
|
1122
|
+
* FEATURE_COUNT are derived once.
|
|
1123
|
+
*/
|
|
1124
|
+
function wmsPointFeatureInfoRequest(options) {
|
|
1125
|
+
const radius = WMS_FEATURE_INFO_POINT_RADIUS;
|
|
1126
|
+
const featureCount = options.request?.pagination?.limit;
|
|
1127
|
+
const signal = options.request?.signal;
|
|
1128
|
+
return {
|
|
1129
|
+
layers: [...options.layers],
|
|
1130
|
+
...(options.styleId !== undefined ? { styles: [options.styleId] } : {}),
|
|
1131
|
+
queryLayers: [...options.layers],
|
|
1132
|
+
crs: options.crs,
|
|
1133
|
+
bbox: [options.x - radius, options.y - radius, options.x + radius, options.y + radius],
|
|
1134
|
+
width: 1,
|
|
1135
|
+
height: 1,
|
|
1136
|
+
i: 0,
|
|
1137
|
+
j: 0,
|
|
1138
|
+
...(featureCount !== undefined ? { featureCount } : {}),
|
|
1139
|
+
...(signal ? { signal } : {}),
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* `Source` adapter for a third-party WMS 1.3.0 endpoint that `connect()`
|
|
1144
|
+
* reviewed a `GetFeatureInfo` binding for. Canonical `Source.query()` issues
|
|
1145
|
+
* the point request against the capabilities-advertised operation URL under the
|
|
1146
|
+
* same same-origin, credential-free policy discovery applied to `GetMap`.
|
|
1147
|
+
*
|
|
1148
|
+
* The raw `Source.protocol("wms")` handles stay unavailable here: they are
|
|
1149
|
+
* Honua-service-id bound. Render/tiles continue to flow through the descriptor
|
|
1150
|
+
* → MapLibre raster projection.
|
|
1151
|
+
*/
|
|
1152
|
+
function standaloneWmsSource(descriptor, binding, client, policy, caps) {
|
|
1153
|
+
const layerName = descriptor.locator.typeName;
|
|
1154
|
+
const styleId = descriptor.locator.styleId;
|
|
1155
|
+
const runQuery = async (request) => {
|
|
1156
|
+
ensureCapability(descriptor, caps, "query");
|
|
1157
|
+
requireWmsCompatibleQuery(descriptor, request);
|
|
1158
|
+
const layers = wmsRequireLayers(descriptor, layerName);
|
|
1159
|
+
const point = wmsExtractPointFromQuery(request, descriptor);
|
|
1160
|
+
const crs = resolveWmsQueryCrs(binding.crs, point.crs);
|
|
1161
|
+
if (crs === undefined) {
|
|
1162
|
+
throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, `${descriptor.id} (GetFeatureInfo CRS "${point.crs}" is not one of the layer's reviewed CRS identifiers: ${binding.crs.join(", ")})`);
|
|
1163
|
+
}
|
|
1164
|
+
const features = await executeWmsFeatureInfo(client, {
|
|
1165
|
+
binding: { url: binding.url, format: binding.format },
|
|
1166
|
+
request: wmsPointFeatureInfoRequest({ layers, styleId, crs, x: point.x, y: point.y, request }),
|
|
1167
|
+
sourceId: descriptor.id,
|
|
1168
|
+
});
|
|
1169
|
+
return { features, exceededTransferLimit: false };
|
|
1170
|
+
};
|
|
1171
|
+
return makeSource(descriptor, caps, policy, {}, {
|
|
1172
|
+
async query(request) {
|
|
1173
|
+
return runQuery(request);
|
|
1174
|
+
},
|
|
1175
|
+
async queryAll(request) {
|
|
1176
|
+
// GetFeatureInfo returns a single response set; queryAll degenerates to
|
|
1177
|
+
// query() because there is no paging on the wire.
|
|
1178
|
+
return runQuery(request);
|
|
1179
|
+
},
|
|
1180
|
+
async queryAggregate() {
|
|
1181
|
+
throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
|
|
1182
|
+
},
|
|
1183
|
+
async queryExtent() {
|
|
1184
|
+
throw new HonuaCapabilityNotSupportedError("queryExtent", descriptor.protocol, descriptor.id);
|
|
1185
|
+
},
|
|
1186
|
+
// biome-ignore lint/correctness/useYield: WMS exposes only single-shot GetFeatureInfo, which is delivered through query()
|
|
1187
|
+
async *stream() {
|
|
1188
|
+
throw new HonuaCapabilityNotSupportedError("stream", descriptor.protocol, descriptor.id);
|
|
1189
|
+
},
|
|
1190
|
+
async queryObjectIds() {
|
|
1191
|
+
throw new HonuaCapabilityNotSupportedError("queryObjectIds", descriptor.protocol, descriptor.id);
|
|
1192
|
+
},
|
|
1193
|
+
async applyEdits() {
|
|
1194
|
+
throw new HonuaCapabilityNotSupportedError("applyEdits", descriptor.protocol, descriptor.id);
|
|
1195
|
+
},
|
|
1196
|
+
async queryRelated() {
|
|
1197
|
+
throw new HonuaCapabilityNotSupportedError("queryRelated", descriptor.protocol, descriptor.id);
|
|
1198
|
+
},
|
|
1199
|
+
attachments: unsupportedAttachmentApi(descriptor),
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1105
1202
|
// ── WMTS 1.0 ──────────────────────────────────────────────────
|
|
1106
1203
|
/**
|
|
1107
1204
|
* Render-only `Source` adapter for first-party WMTS 1.0.0 services.
|
|
@@ -1159,10 +1256,14 @@ export function wmtsSource(descriptor, client, policy) {
|
|
|
1159
1256
|
/**
|
|
1160
1257
|
* Adapter factory for a STAC API search endpoint.
|
|
1161
1258
|
*
|
|
1162
|
-
* The canonical `Source.query()` runs a
|
|
1163
|
-
*
|
|
1164
|
-
* `
|
|
1165
|
-
*
|
|
1259
|
+
* The canonical `Source.query()` runs a search against the STAC root, using
|
|
1260
|
+
* `POST /search` when the landing page advertises a `rel="search"` link with
|
|
1261
|
+
* `method: "POST"` (long CQL2 filters and `intersects` geometries overflow GET
|
|
1262
|
+
* URL limits) and falling back to the universally supported `GET /search`
|
|
1263
|
+
* otherwise. `Query.spatialFilter` translates to STAC's `bbox` for an
|
|
1264
|
+
* `envelope(...)` and to `intersects` for any other GeoJSON-convertible
|
|
1265
|
+
* geometry. The deprecated source-native `Query.where` migration member maps
|
|
1266
|
+
* to STAC `datetime` / `filter` parameters.
|
|
1166
1267
|
*
|
|
1167
1268
|
* @example
|
|
1168
1269
|
* ```ts
|
|
@@ -1179,8 +1280,8 @@ export function wmtsSource(descriptor, client, policy) {
|
|
|
1179
1280
|
* }],
|
|
1180
1281
|
* });
|
|
1181
1282
|
* const result = await dataset.source("stac-search")!.query({
|
|
1182
|
-
* where: "collections IN ('landsat-c2-l2')",
|
|
1183
1283
|
* spatialFilter: envelope(-158.5, 21.2, -157.6, 21.7),
|
|
1284
|
+
* pagination: { limit: 100 },
|
|
1184
1285
|
* });
|
|
1185
1286
|
* ```
|
|
1186
1287
|
*/
|
|
@@ -1201,13 +1302,24 @@ export function stacSearchSource(descriptor, client, policy) {
|
|
|
1201
1302
|
const adapterRegistry = {
|
|
1202
1303
|
stac,
|
|
1203
1304
|
};
|
|
1305
|
+
/**
|
|
1306
|
+
* Build the wire search request, negotiating the HTTP method once per
|
|
1307
|
+
* source: `POST /search` when the API advertises it, `GET /search`
|
|
1308
|
+
* otherwise. A static catalog has no search endpoint, so it never probes.
|
|
1309
|
+
*/
|
|
1310
|
+
const toNegotiatedStacRequest = async (request) => {
|
|
1311
|
+
const stacRequest = toStacRequest(request, collectionScope, descriptor);
|
|
1312
|
+
if (!staticCatalog && (await stac.supportsPostSearch()))
|
|
1313
|
+
stacRequest.usePost = true;
|
|
1314
|
+
return stacRequest;
|
|
1315
|
+
};
|
|
1204
1316
|
return makeSource(descriptor, caps, policy, adapterRegistry, {
|
|
1205
1317
|
async query(request) {
|
|
1206
1318
|
ensureCapability(descriptor, caps, "query");
|
|
1207
1319
|
if (request?.aggregation) {
|
|
1208
1320
|
throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
|
|
1209
1321
|
}
|
|
1210
|
-
const stacRequest =
|
|
1322
|
+
const stacRequest = await toNegotiatedStacRequest(request);
|
|
1211
1323
|
const response = staticCatalog
|
|
1212
1324
|
? await staticCatalog.search(toStacStaticParams(stacRequest))
|
|
1213
1325
|
: await stac.search(stacRequest);
|
|
@@ -1224,13 +1336,13 @@ export function stacSearchSource(descriptor, client, policy) {
|
|
|
1224
1336
|
ensureCapability(descriptor, caps, "query");
|
|
1225
1337
|
const limit = request?.pagination?.limit;
|
|
1226
1338
|
if (staticCatalog) {
|
|
1227
|
-
const response = await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope)));
|
|
1339
|
+
const response = await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope, descriptor)));
|
|
1228
1340
|
const typed = (response.features ?? []).map((toTypedFeatureFromStac));
|
|
1229
1341
|
const { features, exceededTransferLimit } = applyQueryAllLimit(typed, limit);
|
|
1230
1342
|
return { features, exceededTransferLimit, totalCount: features.length };
|
|
1231
1343
|
}
|
|
1232
1344
|
const items = await stac.searchAll({
|
|
1233
|
-
...
|
|
1345
|
+
...(await toNegotiatedStacRequest(request)),
|
|
1234
1346
|
...withPagingBounds({}, limit),
|
|
1235
1347
|
});
|
|
1236
1348
|
const typed = items.map((toTypedFeatureFromStac));
|
|
@@ -1253,7 +1365,7 @@ export function stacSearchSource(descriptor, client, policy) {
|
|
|
1253
1365
|
if (staticCatalog) {
|
|
1254
1366
|
// A static catalog has no server-side paging; yield the whole
|
|
1255
1367
|
// filtered set as one page.
|
|
1256
|
-
const response = await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope)));
|
|
1368
|
+
const response = await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope, descriptor)));
|
|
1257
1369
|
yield {
|
|
1258
1370
|
features: (response.features ?? []).map((toTypedFeatureFromStac)),
|
|
1259
1371
|
exceededTransferLimit: false,
|
|
@@ -1261,7 +1373,7 @@ export function stacSearchSource(descriptor, client, policy) {
|
|
|
1261
1373
|
return;
|
|
1262
1374
|
}
|
|
1263
1375
|
const stream = stac.searchStream({
|
|
1264
|
-
...
|
|
1376
|
+
...(await toNegotiatedStacRequest(request)),
|
|
1265
1377
|
pageSize: limit !== undefined ? Math.max(1, limit) : undefined,
|
|
1266
1378
|
maxPages: Number.MAX_SAFE_INTEGER,
|
|
1267
1379
|
});
|
|
@@ -1280,9 +1392,10 @@ export function stacSearchSource(descriptor, client, policy) {
|
|
|
1280
1392
|
// fetches at most `limit + 1` rows, never a full-catalog scan.
|
|
1281
1393
|
const limit = request?.pagination?.limit;
|
|
1282
1394
|
const items = staticCatalog
|
|
1283
|
-
? ((await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope))))
|
|
1395
|
+
? ((await staticCatalog.search(toStacStaticParams(toStacRequest(request, collectionScope, descriptor))))
|
|
1396
|
+
.features ?? [])
|
|
1284
1397
|
: await stac.searchAll({
|
|
1285
|
-
...
|
|
1398
|
+
...(await toNegotiatedStacRequest(request)),
|
|
1286
1399
|
...withPagingBounds({}, limit),
|
|
1287
1400
|
});
|
|
1288
1401
|
const ids = [];
|
|
@@ -1306,22 +1419,28 @@ export function stacSearchSource(descriptor, client, policy) {
|
|
|
1306
1419
|
});
|
|
1307
1420
|
}
|
|
1308
1421
|
// ── WFS 2.0 ───────────────────────────────────────────────────
|
|
1309
|
-
/**
|
|
1310
|
-
* Threshold above which the WFS adapter switches to POST GetFeature with a
|
|
1311
|
-
* `<fes:Filter>` body. URL budget under 7000 chars stays GET-friendly; longer
|
|
1312
|
-
* filters are routed through POST so we do not stress middleboxes that
|
|
1313
|
-
* trim long query strings. The threshold is intentionally a single
|
|
1314
|
-
* constant we can revise after telemetry lands.
|
|
1315
|
-
*/
|
|
1316
|
-
const WFS_GET_FILTER_BUDGET = 7000;
|
|
1317
|
-
const DEFAULT_WFS_GEOMETRY_PROPERTY = "the_geom";
|
|
1318
1422
|
/**
|
|
1319
1423
|
* Adapter factory for a WFS 2.0 endpoint.
|
|
1320
1424
|
*
|
|
1321
|
-
* `Query.
|
|
1322
|
-
* preferred over GML via
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1425
|
+
* `Query.spatialFilter` and the deprecated source-native `Query.where`
|
|
1426
|
+
* migration member compile to FES 2.0; GeoJSON is preferred over GML via
|
|
1427
|
+
* `OperationsMetadata` negotiation. `applyEdits()` builds
|
|
1428
|
+
* `<wfs:Transaction>` bodies. Reach `Source.protocol("wfs")` for raw GML,
|
|
1429
|
+
* custom FES filters, `GetPropertyValue`, raw `<wfs:Transaction>` bodies, and
|
|
1430
|
+
* stored queries.
|
|
1431
|
+
*
|
|
1432
|
+
* Locking (`LockFeature` / `GetFeatureWithLock`) has no typed helper anywhere in
|
|
1433
|
+
* the SDK. Callers that need locks must send their own request XML through
|
|
1434
|
+
* `Source.protocol("wfs")!.root.requestText(...)` and parse the response
|
|
1435
|
+
* themselves; no lock id is extracted and no lock state is tracked.
|
|
1436
|
+
*
|
|
1437
|
+
* FES spatial predicates and transaction geometry name the feature type's
|
|
1438
|
+
* geometry property, which is server-specific (`the_geom` on
|
|
1439
|
+
* PostGIS-via-GeoServer, `msGeometry` on MapServer, arbitrary per-schema names
|
|
1440
|
+
* elsewhere). The adapter resolves it from `DescribeFeatureType` on first use
|
|
1441
|
+
* and caches it; `locator.geometryName` pins it explicitly. Resolution failure
|
|
1442
|
+
* fails closed rather than guessing a default that would silently match
|
|
1443
|
+
* nothing.
|
|
1325
1444
|
*
|
|
1326
1445
|
* @example
|
|
1327
1446
|
* ```ts
|
|
@@ -1336,17 +1455,21 @@ const DEFAULT_WFS_GEOMETRY_PROPERTY = "the_geom";
|
|
|
1336
1455
|
* }],
|
|
1337
1456
|
* });
|
|
1338
1457
|
* const result = await dataset.source("parcels")!.queryAll({
|
|
1339
|
-
* where: "STATUS = 'ACTIVE'",
|
|
1340
1458
|
* pagination: { limit: 500 },
|
|
1341
1459
|
* });
|
|
1342
1460
|
* ```
|
|
1343
1461
|
*/
|
|
1344
1462
|
export function wfsSource(descriptor, client, policy) {
|
|
1345
1463
|
descriptor = normalizeCapabilityDescriptor(descriptor);
|
|
1346
|
-
const {
|
|
1347
|
-
const
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1464
|
+
const { typeName, featureNamespace, srsName: wfsSrsName, geometryName } = requireWfsLocator(descriptor);
|
|
1465
|
+
const module = wfsProtocolModule(client);
|
|
1466
|
+
const discovered = module.discover(descriptor);
|
|
1467
|
+
if (discovered instanceof Promise) {
|
|
1468
|
+
throw new Error("wfs: built-in source construction requires synchronous protocol-module discovery");
|
|
1469
|
+
}
|
|
1470
|
+
const featureType = discovered.adapter;
|
|
1471
|
+
const root = featureType.root;
|
|
1472
|
+
const caps = discovered.capabilities;
|
|
1350
1473
|
void policy;
|
|
1351
1474
|
const adapterRegistry = {
|
|
1352
1475
|
wfs: featureType,
|
|
@@ -1371,6 +1494,65 @@ export function wfsSource(descriptor, client, policy) {
|
|
|
1371
1494
|
cachedFormat = choice;
|
|
1372
1495
|
return choice;
|
|
1373
1496
|
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Geometry property of this feature type, from `locator.geometryName` or —
|
|
1499
|
+
* once, lazily — from `DescribeFeatureType`. Throws
|
|
1500
|
+
* `HonuaWfsProtocolError("unresolved-geometry-property")` when neither
|
|
1501
|
+
* names it, so a spatial filter or transaction never targets a guessed
|
|
1502
|
+
* property that silently matches nothing.
|
|
1503
|
+
*/
|
|
1504
|
+
async function resolveGeometryProperty(signal) {
|
|
1505
|
+
if (geometryName !== undefined)
|
|
1506
|
+
return geometryName;
|
|
1507
|
+
return featureType.geometryPropertyName(signal ? { signal } : undefined);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Descriptor whose locator carries the resolved geometry property, so the
|
|
1511
|
+
* compiler emits it on every reference in the request it plans.
|
|
1512
|
+
*/
|
|
1513
|
+
let geometryResolvedDescriptor;
|
|
1514
|
+
async function geometryBoundDescriptor(signal) {
|
|
1515
|
+
if (geometryName !== undefined)
|
|
1516
|
+
return descriptor;
|
|
1517
|
+
if (!geometryResolvedDescriptor) {
|
|
1518
|
+
const resolved = await resolveGeometryProperty(signal);
|
|
1519
|
+
geometryResolvedDescriptor = { ...descriptor, locator: { ...descriptor.locator, geometryName: resolved } };
|
|
1520
|
+
}
|
|
1521
|
+
return geometryResolvedDescriptor;
|
|
1522
|
+
}
|
|
1523
|
+
async function compileSourceQuery(request, operation) {
|
|
1524
|
+
try {
|
|
1525
|
+
const normalizedRequest = normalizeWfsModuleQuery(request);
|
|
1526
|
+
const buildIr = (source) => normalizedRequest === undefined
|
|
1527
|
+
? createQueryIr({ descriptor: source })
|
|
1528
|
+
: createQueryIr({ descriptor: source, query: normalizedRequest });
|
|
1529
|
+
// The legacy WFS Source accepted an explicitly empty `where`. Preserve
|
|
1530
|
+
// its wire distinction from an omitted `where`: with a spatial filter,
|
|
1531
|
+
// the former takes the FES path while the latter may use the bbox KVP.
|
|
1532
|
+
const canonicalQuery = (ir) => request?.where === ""
|
|
1533
|
+
? Object.freeze({
|
|
1534
|
+
...ir.query,
|
|
1535
|
+
where: Object.freeze({ kind: "source-native", expression: "" }),
|
|
1536
|
+
})
|
|
1537
|
+
: ir.query;
|
|
1538
|
+
let ir = buildIr(descriptor);
|
|
1539
|
+
let query = canonicalQuery(ir);
|
|
1540
|
+
// Only an FES spatial predicate names the geometry property on the wire;
|
|
1541
|
+
// the `bbox=` shortcut addresses the default geometry positionally. Pay
|
|
1542
|
+
// for schema resolution exactly when the request depends on the name.
|
|
1543
|
+
if (ir.source.geometryProperty === undefined && wfsSpatialFilterNeedsGeometryProperty(query)) {
|
|
1544
|
+
ir = buildIr(await geometryBoundDescriptor(request?.signal));
|
|
1545
|
+
query = canonicalQuery(ir);
|
|
1546
|
+
}
|
|
1547
|
+
return module.compile({ source: ir.source, query, operation });
|
|
1548
|
+
}
|
|
1549
|
+
catch (error) {
|
|
1550
|
+
if (error instanceof HonuaQueryPlanningError && error.code === "unsupported-query") {
|
|
1551
|
+
throw new HonuaCapabilityNotSupportedError("query", descriptor.protocol, `${descriptor.id} (${error.message})`);
|
|
1552
|
+
}
|
|
1553
|
+
throw error;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1374
1556
|
return makeSource(descriptor, caps, policy, adapterRegistry, {
|
|
1375
1557
|
async query(request) {
|
|
1376
1558
|
ensureCapability(descriptor, caps, "query");
|
|
@@ -1379,48 +1561,25 @@ export function wfsSource(descriptor, client, policy) {
|
|
|
1379
1561
|
// silent partial result.
|
|
1380
1562
|
throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
|
|
1381
1563
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
}
|
|
1388
|
-
const choice = await negotiateJsonOrThrow();
|
|
1389
|
-
const json = await runGetFeatureJson(featureType, typeName, choice, request, request?.pagination?.limit);
|
|
1390
|
-
return resultFromGeoJson(json);
|
|
1564
|
+
return module.execute(discovered, {
|
|
1565
|
+
compiled: await compileSourceQuery(request, "query"),
|
|
1566
|
+
operation: "query",
|
|
1567
|
+
query: {},
|
|
1568
|
+
...(request?.signal ? { signal: request.signal } : {}),
|
|
1569
|
+
});
|
|
1391
1570
|
},
|
|
1392
1571
|
async queryAll(request) {
|
|
1393
1572
|
ensureCapability(descriptor, caps, "query");
|
|
1394
|
-
const
|
|
1395
|
-
const limit =
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
while (collected.length < target) {
|
|
1405
|
-
const pageRequest = {
|
|
1406
|
-
...(request ?? {}),
|
|
1407
|
-
pagination: { offset, limit: pageSize },
|
|
1408
|
-
};
|
|
1409
|
-
const json = await runGetFeatureJson(featureType, typeName, choice, pageRequest, pageSize);
|
|
1410
|
-
const result = resultFromGeoJson(json);
|
|
1411
|
-
if (result.totalCount !== undefined)
|
|
1412
|
-
totalCount = result.totalCount;
|
|
1413
|
-
collected.push(...result.features);
|
|
1414
|
-
if (result.features.length < pageSize)
|
|
1415
|
-
break;
|
|
1416
|
-
offset += result.features.length;
|
|
1417
|
-
}
|
|
1418
|
-
const { features, exceededTransferLimit } = applyQueryAllLimit(collected, limit);
|
|
1419
|
-
return {
|
|
1420
|
-
features,
|
|
1421
|
-
exceededTransferLimit,
|
|
1422
|
-
totalCount: totalCount ?? features.length,
|
|
1423
|
-
};
|
|
1573
|
+
const requestedLimit = request?.pagination?.limit;
|
|
1574
|
+
const limit = typeof requestedLimit === "number" && requestedLimit >= 0 ? requestedLimit : undefined;
|
|
1575
|
+
return module.execute(discovered, {
|
|
1576
|
+
compiled: await compileSourceQuery(request, "queryAll"),
|
|
1577
|
+
operation: "queryAll",
|
|
1578
|
+
query: {
|
|
1579
|
+
...(limit !== undefined ? { logicalLimit: limit } : {}),
|
|
1580
|
+
},
|
|
1581
|
+
...(request?.signal ? { signal: request.signal } : {}),
|
|
1582
|
+
});
|
|
1424
1583
|
},
|
|
1425
1584
|
async queryAggregate() {
|
|
1426
1585
|
throw new HonuaCapabilityNotSupportedError("queryAggregate", descriptor.protocol, descriptor.id);
|
|
@@ -1455,7 +1614,7 @@ export function wfsSource(descriptor, client, policy) {
|
|
|
1455
1614
|
...extentRequest,
|
|
1456
1615
|
pagination: { offset, limit: drainPageSize },
|
|
1457
1616
|
};
|
|
1458
|
-
const json = await runGetFeatureJson(featureType,
|
|
1617
|
+
const json = await runGetFeatureJson(featureType, choice, featureNamespace, await compileSourceQuery(pageRequest, "query"));
|
|
1459
1618
|
const page = computeExtentFromFeatureCollection(json);
|
|
1460
1619
|
count += page.count;
|
|
1461
1620
|
if (page.extent) {
|
|
@@ -1484,25 +1643,46 @@ export function wfsSource(descriptor, client, policy) {
|
|
|
1484
1643
|
// rather than silently fall back to the default 2000-row page size.
|
|
1485
1644
|
if (limit === 0)
|
|
1486
1645
|
return;
|
|
1487
|
-
const choice = await negotiateJsonOrThrow();
|
|
1488
1646
|
const pageSize = typeof limit === "number" && limit > 0 ? Math.max(1, limit) : 2000;
|
|
1489
1647
|
let offset = request?.pagination?.offset ?? 0;
|
|
1648
|
+
let totalCount;
|
|
1649
|
+
const seenPages = new Set();
|
|
1490
1650
|
while (true) {
|
|
1491
1651
|
const pageRequest = {
|
|
1492
1652
|
...(request ?? {}),
|
|
1493
1653
|
pagination: { offset, limit: pageSize },
|
|
1494
1654
|
};
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
|
|
1655
|
+
const result = await module.execute(discovered, {
|
|
1656
|
+
compiled: await compileSourceQuery(pageRequest, "query"),
|
|
1657
|
+
operation: "query",
|
|
1658
|
+
query: {},
|
|
1659
|
+
...(request?.signal ? { signal: request.signal } : {}),
|
|
1660
|
+
});
|
|
1661
|
+
if (result.totalCount !== undefined) {
|
|
1662
|
+
if (totalCount !== undefined && totalCount !== result.totalCount) {
|
|
1663
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature numberMatched changed while streaming one query");
|
|
1664
|
+
}
|
|
1665
|
+
totalCount = result.totalCount;
|
|
1666
|
+
}
|
|
1667
|
+
const pageEnd = checkedWfsPageEnd(offset, result.features.length);
|
|
1668
|
+
if (totalCount !== undefined && pageEnd > totalCount) {
|
|
1669
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature returned more rows than numberMatched permits");
|
|
1670
|
+
}
|
|
1671
|
+
if (result.features.length === 0) {
|
|
1672
|
+
if (totalCount !== undefined && offset < totalCount) {
|
|
1673
|
+
throw new HonuaWfsProtocolError("paging-stalled", `WFS GetFeature returned no rows at startIndex ${offset} while numberMatched=${totalCount}`);
|
|
1674
|
+
}
|
|
1498
1675
|
break;
|
|
1676
|
+
}
|
|
1677
|
+
assertWfsPageProgress(result, seenPages, offset);
|
|
1499
1678
|
yield {
|
|
1500
1679
|
features: result.features,
|
|
1501
|
-
exceededTransferLimit:
|
|
1680
|
+
exceededTransferLimit: totalCount !== undefined ? pageEnd < totalCount : result.exceededTransferLimit,
|
|
1681
|
+
...(totalCount !== undefined ? { totalCount } : {}),
|
|
1502
1682
|
};
|
|
1503
|
-
|
|
1683
|
+
offset = pageEnd;
|
|
1684
|
+
if (totalCount !== undefined && offset >= totalCount)
|
|
1504
1685
|
break;
|
|
1505
|
-
offset += result.features.length;
|
|
1506
1686
|
}
|
|
1507
1687
|
},
|
|
1508
1688
|
async queryObjectIds(request) {
|
|
@@ -1532,7 +1712,7 @@ export function wfsSource(descriptor, client, policy) {
|
|
|
1532
1712
|
...idsRequest,
|
|
1533
1713
|
pagination: { offset, limit: pageSize },
|
|
1534
1714
|
};
|
|
1535
|
-
const json = await runGetFeatureJson(featureType,
|
|
1715
|
+
const json = await runGetFeatureJson(featureType, choice, featureNamespace, await compileSourceQuery(pageRequest, "query"));
|
|
1536
1716
|
const collection = json;
|
|
1537
1717
|
const features = collection.features ?? [];
|
|
1538
1718
|
for (const feature of features) {
|
|
@@ -1582,7 +1762,13 @@ export function wfsSource(descriptor, client, policy) {
|
|
|
1582
1762
|
// same root, so the capabilities promise is already settled there.
|
|
1583
1763
|
await root.capabilities(envelope.signal ? { signal: envelope.signal } : undefined);
|
|
1584
1764
|
const filtered = { ...envelope, updates: validUpdates };
|
|
1585
|
-
|
|
1765
|
+
// Attribute-only edits and deletes never name the geometry property,
|
|
1766
|
+
// so only a geometry-bearing transaction pays for (and fails closed
|
|
1767
|
+
// on) schema resolution.
|
|
1768
|
+
const writesGeometry = adds.some((add) => add.geometry !== undefined && add.geometry !== null) ||
|
|
1769
|
+
validUpdates.some((update) => update.geometry !== undefined && update.geometry !== null);
|
|
1770
|
+
const geometryProperty = writesGeometry ? await resolveGeometryProperty(envelope.signal) : undefined;
|
|
1771
|
+
const body = buildTransactionBody(typeName, filtered, featureNamespace, wfsSrsName, geometryProperty);
|
|
1586
1772
|
const transactionOptions = { body };
|
|
1587
1773
|
if (envelope.signal)
|
|
1588
1774
|
transactionOptions.signal = envelope.signal;
|
|
@@ -1596,8 +1782,32 @@ export function wfsSource(descriptor, client, policy) {
|
|
|
1596
1782
|
attachments: unsupportedAttachmentApi(descriptor),
|
|
1597
1783
|
});
|
|
1598
1784
|
}
|
|
1785
|
+
function normalizeWfsModuleQuery(request) {
|
|
1786
|
+
if (request === undefined)
|
|
1787
|
+
return undefined;
|
|
1788
|
+
const omitWhere = request.where === "";
|
|
1789
|
+
const pagination = request.pagination;
|
|
1790
|
+
const omitLimit = pagination?.limit !== undefined && pagination.limit < 0;
|
|
1791
|
+
const omitOffset = pagination?.offset !== undefined && pagination.offset < 0;
|
|
1792
|
+
if (!omitWhere && !omitLimit && !omitOffset)
|
|
1793
|
+
return request;
|
|
1794
|
+
const { where: _where, pagination: _pagination, ...rest } = request;
|
|
1795
|
+
void _where;
|
|
1796
|
+
void _pagination;
|
|
1797
|
+
const normalizedPagination = pagination === undefined
|
|
1798
|
+
? undefined
|
|
1799
|
+
: {
|
|
1800
|
+
...(!omitOffset && pagination.offset !== undefined ? { offset: pagination.offset } : {}),
|
|
1801
|
+
...(!omitLimit && pagination.limit !== undefined ? { limit: pagination.limit } : {}),
|
|
1802
|
+
};
|
|
1803
|
+
return {
|
|
1804
|
+
...rest,
|
|
1805
|
+
...(!omitWhere && request.where !== undefined ? { where: request.where } : {}),
|
|
1806
|
+
...(normalizedPagination !== undefined ? { pagination: normalizedPagination } : {}),
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1599
1809
|
function requireWfsLocator(descriptor) {
|
|
1600
|
-
const { url, typeName, featureNamespace, srsName } = descriptor.locator;
|
|
1810
|
+
const { url, typeName, featureNamespace, srsName, geometryName } = descriptor.locator;
|
|
1601
1811
|
if (typeof url !== "string" || url.length === 0) {
|
|
1602
1812
|
throw new Error(`createDataset: source "${descriptor.id}" (wfs) requires locator.url`);
|
|
1603
1813
|
}
|
|
@@ -1607,13 +1817,23 @@ function requireWfsLocator(descriptor) {
|
|
|
1607
1817
|
if (featureNamespace !== undefined && (typeof featureNamespace !== "string" || featureNamespace.length === 0)) {
|
|
1608
1818
|
throw new Error(`createDataset: source "${descriptor.id}" (wfs) locator.featureNamespace must be a non-empty string`);
|
|
1609
1819
|
}
|
|
1820
|
+
// The geometry property is spliced into FES `<fes:ValueReference>` and
|
|
1821
|
+
// transaction element names, so an override must be a plain XML name.
|
|
1822
|
+
if (geometryName !== undefined && (typeof geometryName !== "string" || !isWfsPropertyName(geometryName))) {
|
|
1823
|
+
throw new Error(`createDataset: source "${descriptor.id}" (wfs) locator.geometryName must be a valid geometry property name`);
|
|
1824
|
+
}
|
|
1610
1825
|
return {
|
|
1611
1826
|
url,
|
|
1612
1827
|
typeName,
|
|
1613
1828
|
featureNamespace: typeof featureNamespace === "string" ? featureNamespace : undefined,
|
|
1614
1829
|
srsName: wfsSrsNameFromOutSr(srsName),
|
|
1830
|
+
geometryName: typeof geometryName === "string" ? geometryName : undefined,
|
|
1615
1831
|
};
|
|
1616
1832
|
}
|
|
1833
|
+
/** Optionally prefixed XML name, the only shape a WFS property reference takes. */
|
|
1834
|
+
function isWfsPropertyName(value) {
|
|
1835
|
+
return /^[A-Za-z_][A-Za-z0-9_.-]*(?::[A-Za-z_][A-Za-z0-9_.-]*)?$/.test(value);
|
|
1836
|
+
}
|
|
1617
1837
|
function toWfsExtentDrainRequest(request) {
|
|
1618
1838
|
// The drain computes a bbox over geometries, so a caller-supplied
|
|
1619
1839
|
// `returnGeometry: false` would suppress geometry on every page and
|
|
@@ -1644,211 +1864,51 @@ function toWfsObjectIdsDrainRequest(request) {
|
|
|
1644
1864
|
return idsRequest;
|
|
1645
1865
|
}
|
|
1646
1866
|
/**
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1867
|
+
* Execute a compiler-approved WFS GetFeature request and return its decoded
|
|
1868
|
+
* GeoJSON FeatureCollection. The protocol compiler owns filter, projection,
|
|
1869
|
+
* paging, CRS, and transport selection so legacy drains do not duplicate it.
|
|
1650
1870
|
*/
|
|
1651
|
-
async function runGetFeatureJson(featureType,
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
let bbox;
|
|
1655
|
-
if (request?.where !== undefined && request.where !== "") {
|
|
1656
|
-
const compiled = compileWhere(request.where);
|
|
1657
|
-
if (compiled === UNSUPPORTED_FES) {
|
|
1658
|
-
throw new HonuaCapabilityNotSupportedError("query", "wfs", `where clause is not expressible in FES; route the request through Source.protocol("wfs") to emit a custom filter`);
|
|
1659
|
-
}
|
|
1660
|
-
if (compiled.kind !== "and" || compiled.operands.length > 0) {
|
|
1661
|
-
filterNodes.push(compiled);
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
if (request?.spatialFilter) {
|
|
1665
|
-
const spatialRel = request.spatialFilter.spatialRel;
|
|
1666
|
-
// The `bbox=` KVP and `<fes:BBOX>` both encode envelope-intersects
|
|
1667
|
-
// semantics (OGC 09-026r2). Only take the bbox shortcut when the caller
|
|
1668
|
-
// wants intersects (default), envelope-intersects, or did not specify.
|
|
1669
|
-
// Anything else (Contains, Within, Crosses, Overlaps, Touches) needs an
|
|
1670
|
-
// FES predicate that preserves the requested relation, which means
|
|
1671
|
-
// routing the envelope through compileSpatialFilter so the geometry is
|
|
1672
|
-
// serialized as a polygon under the correct spatial op.
|
|
1673
|
-
const isEnvelopeIntersects = spatialRel === undefined ||
|
|
1674
|
-
spatialRel === "esriSpatialRelIntersects" ||
|
|
1675
|
-
spatialRel === "esriSpatialRelEnvelopeIntersects";
|
|
1676
|
-
if (request.spatialFilter.geometryType === "esriGeometryEnvelope" &&
|
|
1677
|
-
isEnvelopeIntersects &&
|
|
1678
|
-
filterNodes.length === 0 &&
|
|
1679
|
-
request.where === undefined) {
|
|
1680
|
-
// BBox-only requests are short and travel safely as a `bbox=` KVP, no
|
|
1681
|
-
// FES emission required. Saves a round-trip through XML serialization
|
|
1682
|
-
// and keeps the URL human-debuggable.
|
|
1683
|
-
const env = request.spatialFilter.geometry;
|
|
1684
|
-
if (typeof env.xmin === "number" &&
|
|
1685
|
-
typeof env.ymin === "number" &&
|
|
1686
|
-
typeof env.xmax === "number" &&
|
|
1687
|
-
typeof env.ymax === "number") {
|
|
1688
|
-
bbox = `${env.xmin},${env.ymin},${env.xmax},${env.ymax}`;
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
else {
|
|
1692
|
-
const filterSrsName = wfsSrsNameFromOutSr(request.outSr);
|
|
1693
|
-
const compiled = compileSpatialFilter(request.spatialFilter, {
|
|
1694
|
-
geometryProperty: DEFAULT_WFS_GEOMETRY_PROPERTY,
|
|
1695
|
-
...(filterSrsName !== undefined ? { srsName: filterSrsName } : {}),
|
|
1696
|
-
});
|
|
1697
|
-
if (compiled === UNSUPPORTED_FES) {
|
|
1698
|
-
throw new HonuaCapabilityNotSupportedError("query", "wfs", `spatial filter (geometryType=${request.spatialFilter.geometryType}, spatialRel=${request.spatialFilter.spatialRel ?? "default"}) is not expressible in FES; reach the wire through Source.protocol("wfs")`);
|
|
1699
|
-
}
|
|
1700
|
-
filterNodes.push(compiled);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
const filterXml = filterNodes.length > 0 ? serializeFes(filterNodes, { typeName }) : undefined;
|
|
1704
|
-
if (filterXml !== undefined) {
|
|
1705
|
-
const encoded = encodeURIComponent(filterXml);
|
|
1706
|
-
if (encoded.length > WFS_GET_FILTER_BUDGET)
|
|
1707
|
-
needsPostBody = true;
|
|
1708
|
-
}
|
|
1709
|
-
const params = {};
|
|
1710
|
-
if (bbox !== undefined)
|
|
1711
|
-
params.bbox = bbox;
|
|
1712
|
-
if (filterXml !== undefined && !needsPostBody)
|
|
1713
|
-
params.filter = filterXml;
|
|
1714
|
-
// WFS `propertyName=` drops every property the caller does not list,
|
|
1715
|
-
// including the geometry column. Honor the canonical contract: when
|
|
1716
|
-
// `outFields` is set and `returnGeometry !== false`, append the
|
|
1717
|
-
// geometry property so geometry is preserved; when
|
|
1718
|
-
// `returnGeometry === false`, omit the geometry property; when
|
|
1719
|
-
// `returnGeometry === false` is asked without an `outFields` list,
|
|
1720
|
-
// refuse the request because WFS cannot suppress geometry without
|
|
1721
|
-
// enumerating every non-geometry property — silently widening to
|
|
1722
|
-
// "geometry included" would break the canonical contract.
|
|
1723
|
-
const callerOutFields = request?.outFields && request.outFields.length > 0 ? request.outFields : undefined;
|
|
1724
|
-
const wantsGeometry = request?.returnGeometry !== false;
|
|
1725
|
-
let propertyNames;
|
|
1726
|
-
if (callerOutFields !== undefined) {
|
|
1727
|
-
if (wantsGeometry) {
|
|
1728
|
-
const merged = [...callerOutFields];
|
|
1729
|
-
if (!merged.includes(DEFAULT_WFS_GEOMETRY_PROPERTY))
|
|
1730
|
-
merged.push(DEFAULT_WFS_GEOMETRY_PROPERTY);
|
|
1731
|
-
propertyNames = merged;
|
|
1732
|
-
}
|
|
1733
|
-
else {
|
|
1734
|
-
propertyNames = [...callerOutFields];
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
else if (request?.returnGeometry === false) {
|
|
1738
|
-
throw new HonuaCapabilityNotSupportedError("query", "wfs", `returnGeometry=false requires an explicit outFields list (WFS propertyName cannot suppress geometry without enumerating non-geometry properties); set Query.outFields or reach the wire through Source.protocol("wfs")`);
|
|
1739
|
-
}
|
|
1740
|
-
if (propertyNames)
|
|
1741
|
-
params.propertyName = [...propertyNames];
|
|
1742
|
-
const sortBy = request?.orderBy && request.orderBy.length > 0
|
|
1743
|
-
? request.orderBy.map((s) => `${s.field}${s.direction === "desc" ? " D" : " A"}`).join(",")
|
|
1871
|
+
async function runGetFeatureJson(featureType, choice, featureNamespace, compiled) {
|
|
1872
|
+
const typePrefix = compiled.typeName.includes(":")
|
|
1873
|
+
? compiled.typeName.slice(0, compiled.typeName.indexOf(":"))
|
|
1744
1874
|
: undefined;
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
if (
|
|
1761
|
-
params.
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
filter: filterXml,
|
|
1767
|
-
count: params.count,
|
|
1768
|
-
startIndex: params.startIndex,
|
|
1875
|
+
const namespace = typePrefix !== undefined && featureNamespace !== undefined
|
|
1876
|
+
? { prefix: typePrefix, uri: featureNamespace }
|
|
1877
|
+
: undefined;
|
|
1878
|
+
const params = {
|
|
1879
|
+
method: compiled.method,
|
|
1880
|
+
outputFormat: choice.format,
|
|
1881
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
1882
|
+
...(compiled.method === "GET" && compiled.filter !== undefined ? { filter: compiled.filter } : {}),
|
|
1883
|
+
...(compiled.bbox !== undefined ? { bbox: compiled.bbox } : {}),
|
|
1884
|
+
...(compiled.propertyName !== undefined ? { propertyName: [...compiled.propertyName] } : {}),
|
|
1885
|
+
...(compiled.sortBy !== undefined ? { sortBy: compiled.sortBy } : {}),
|
|
1886
|
+
...(compiled.count !== undefined ? { count: compiled.count } : {}),
|
|
1887
|
+
...(compiled.startIndex !== undefined && compiled.startIndex > 0 ? { startIndex: compiled.startIndex } : {}),
|
|
1888
|
+
...(compiled.srsName !== undefined ? { srsName: compiled.srsName } : {}),
|
|
1889
|
+
};
|
|
1890
|
+
if (compiled.method === "POST" && compiled.filter !== undefined) {
|
|
1891
|
+
params.body = buildWfsPostGetFeatureBody({
|
|
1892
|
+
typeName: compiled.typeName,
|
|
1893
|
+
filter: compiled.filter,
|
|
1894
|
+
count: compiled.count,
|
|
1895
|
+
startIndex: compiled.startIndex !== undefined && compiled.startIndex > 0 ? compiled.startIndex : undefined,
|
|
1769
1896
|
outputFormat: choice.format,
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
if (srsName !== undefined)
|
|
1776
|
-
postOptions.srsName = srsName;
|
|
1777
|
-
params.body = buildPostGetFeatureBody(postOptions);
|
|
1897
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
1898
|
+
...(compiled.propertyName !== undefined ? { propertyNames: compiled.propertyName } : {}),
|
|
1899
|
+
...(compiled.sortBy !== undefined ? { sortBy: compiled.sortBy } : {}),
|
|
1900
|
+
...(compiled.srsName !== undefined ? { srsName: compiled.srsName } : {}),
|
|
1901
|
+
});
|
|
1778
1902
|
}
|
|
1779
1903
|
const response = await featureType.getFeature(params);
|
|
1780
1904
|
if (response.kind !== "json") {
|
|
1781
|
-
throw new HonuaCapabilityNotSupportedError("query", "wfs", `WFS GetFeature returned ${response.contentType}; canonical surface only carries JSON. Reach
|
|
1905
|
+
throw new HonuaCapabilityNotSupportedError("query", "wfs", `WFS GetFeature returned ${response.contentType}; canonical surface only carries JSON. Reach the wire through Source.protocol("wfs")`);
|
|
1782
1906
|
}
|
|
1783
1907
|
return response.data;
|
|
1784
1908
|
}
|
|
1785
|
-
function buildPostGetFeatureBody(options) {
|
|
1786
|
-
const countAttr = typeof options.count === "number" ? ` count="${options.count}"` : "";
|
|
1787
|
-
const startAttr = typeof options.startIndex === "number" ? ` startIndex="${options.startIndex}"` : "";
|
|
1788
|
-
const outputAttr = ` outputFormat="${escapeXmlAttr(options.outputFormat)}"`;
|
|
1789
|
-
const queryAttrs = options.srsName !== undefined ? ` srsName="${escapeXmlAttr(options.srsName)}"` : "";
|
|
1790
|
-
const propertyXml = options.propertyNames && options.propertyNames.length > 0
|
|
1791
|
-
? options.propertyNames.map((name) => `<wfs:PropertyName>${escapeXmlText(name)}</wfs:PropertyName>`).join("")
|
|
1792
|
-
: "";
|
|
1793
|
-
const sortByXml = options.sortBy !== undefined ? buildSortByXml(options.sortBy) : "";
|
|
1794
|
-
return `<wfs:GetFeature xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" service="WFS" version="2.0.0"${outputAttr}${countAttr}${startAttr}><wfs:Query typeNames="${escapeXmlAttr(options.typeName)}"${queryAttrs}>${propertyXml}${options.filter}${sortByXml}</wfs:Query></wfs:GetFeature>`;
|
|
1795
|
-
}
|
|
1796
|
-
/**
|
|
1797
|
-
* Build a `<fes:SortBy>` block from the same comma-separated `sortBy` string
|
|
1798
|
-
* the GET path emits (`FIELD A,OTHER D`). Empty entries are skipped.
|
|
1799
|
-
*/
|
|
1800
|
-
function buildSortByXml(sortBy) {
|
|
1801
|
-
const entries = sortBy
|
|
1802
|
-
.split(",")
|
|
1803
|
-
.map((entry) => entry.trim())
|
|
1804
|
-
.filter((entry) => entry.length > 0);
|
|
1805
|
-
if (entries.length === 0)
|
|
1806
|
-
return "";
|
|
1807
|
-
const properties = entries.map((entry) => {
|
|
1808
|
-
const parts = entry.split(/\s+/);
|
|
1809
|
-
const field = parts[0];
|
|
1810
|
-
const direction = parts[1]?.toUpperCase() === "D" ? "DESC" : "ASC";
|
|
1811
|
-
return `<fes:SortProperty><fes:ValueReference>${escapeXmlText(field)}</fes:ValueReference><fes:SortOrder>${direction}</fes:SortOrder></fes:SortProperty>`;
|
|
1812
|
-
});
|
|
1813
|
-
return `<fes:SortBy>${properties.join("")}</fes:SortBy>`;
|
|
1814
|
-
}
|
|
1815
|
-
/**
|
|
1816
|
-
* Translate a canonical `Query.outSr` (string CRS URI / EPSG token, or numeric
|
|
1817
|
-
* WKID) into the WFS `srsName` form. Numeric WKIDs become the OGC URN form
|
|
1818
|
-
* `urn:ogc:def:crs:EPSG::<wkid>` so cross-server interop matches the format
|
|
1819
|
-
* advertised in `OperationsMetadata` / `Filter_Capabilities`.
|
|
1820
|
-
*/
|
|
1821
|
-
function wfsSrsNameFromOutSr(outSr) {
|
|
1822
|
-
if (typeof outSr === "string")
|
|
1823
|
-
return outSr.length > 0 ? outSr : undefined;
|
|
1824
|
-
if (typeof outSr === "number" && Number.isFinite(outSr))
|
|
1825
|
-
return `urn:ogc:def:crs:EPSG::${outSr}`;
|
|
1826
|
-
return undefined;
|
|
1827
|
-
}
|
|
1828
1909
|
function escapeXmlAttr(value) {
|
|
1829
1910
|
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
1830
1911
|
}
|
|
1831
|
-
function resultFromGeoJson(data) {
|
|
1832
|
-
if (typeof data !== "object" || data === null) {
|
|
1833
|
-
return { features: [], exceededTransferLimit: false };
|
|
1834
|
-
}
|
|
1835
|
-
const collection = data;
|
|
1836
|
-
const features = (collection.features ?? []).map((f) => ({
|
|
1837
|
-
attributes: (f.properties ?? {}),
|
|
1838
|
-
geometry: f.geometry,
|
|
1839
|
-
}));
|
|
1840
|
-
const totalCount = typeof collection.numberMatched === "number" && Number.isFinite(collection.numberMatched)
|
|
1841
|
-
? collection.numberMatched
|
|
1842
|
-
: undefined;
|
|
1843
|
-
const exceededTransferLimit = totalCount !== undefined && features.length < totalCount;
|
|
1844
|
-
const out = {
|
|
1845
|
-
features,
|
|
1846
|
-
exceededTransferLimit,
|
|
1847
|
-
};
|
|
1848
|
-
if (totalCount !== undefined)
|
|
1849
|
-
out.totalCount = totalCount;
|
|
1850
|
-
return out;
|
|
1851
|
-
}
|
|
1852
1912
|
function computeExtentFromFeatureCollection(data) {
|
|
1853
1913
|
if (typeof data !== "object" || data === null)
|
|
1854
1914
|
return { extent: null, count: 0 };
|
|
@@ -1877,7 +1937,7 @@ function computeExtentFromFeatureCollection(data) {
|
|
|
1877
1937
|
return { extent: null, count };
|
|
1878
1938
|
return { extent: { xmin, ymin, xmax, ymax }, count };
|
|
1879
1939
|
}
|
|
1880
|
-
function buildTransactionBody(typeName, envelope, featureNamespace, srsName) {
|
|
1940
|
+
function buildTransactionBody(typeName, envelope, featureNamespace, srsName, geometryProperty) {
|
|
1881
1941
|
const releaseAction = envelope.rollbackOnFailure ? "ALL" : "SOME";
|
|
1882
1942
|
const { prefix } = splitTypeName(typeName);
|
|
1883
1943
|
// Bind the feature-namespace prefix on the Transaction root so prefixed
|
|
@@ -1897,12 +1957,12 @@ function buildTransactionBody(typeName, envelope, featureNamespace, srsName) {
|
|
|
1897
1957
|
// result-mapping side cannot drift.
|
|
1898
1958
|
const adds = envelope.adds ?? [];
|
|
1899
1959
|
for (let i = 0; i < adds.length; i += 1) {
|
|
1900
|
-
blocks.push(buildInsertBlock(typeName, adds[i], wfsInsertHandle(i), srsName));
|
|
1960
|
+
blocks.push(buildInsertBlock(typeName, adds[i], wfsInsertHandle(i), srsName, geometryProperty));
|
|
1901
1961
|
}
|
|
1902
1962
|
let handleCounter = adds.length;
|
|
1903
1963
|
for (const update of envelope.updates ?? []) {
|
|
1904
1964
|
handleCounter += 1;
|
|
1905
|
-
blocks.push(buildUpdateBlock(typeName, update, `upd-${handleCounter}`, srsName));
|
|
1965
|
+
blocks.push(buildUpdateBlock(typeName, update, `upd-${handleCounter}`, srsName, geometryProperty));
|
|
1906
1966
|
}
|
|
1907
1967
|
for (const id of envelope.deletes ?? []) {
|
|
1908
1968
|
handleCounter += 1;
|
|
@@ -1931,30 +1991,41 @@ function splitTypeName(typeName) {
|
|
|
1931
1991
|
function syntheticFeatureNamespace(prefix) {
|
|
1932
1992
|
return `urn:honua:wfs:feature-namespace:${prefix}`;
|
|
1933
1993
|
}
|
|
1934
|
-
function buildInsertBlock(typeName, feature, handle, srsName) {
|
|
1994
|
+
function buildInsertBlock(typeName, feature, handle, srsName, geometryProperty) {
|
|
1935
1995
|
const attributes = feature.attributes;
|
|
1936
1996
|
const propertyXml = Object.entries(attributes)
|
|
1937
1997
|
.filter(([, value]) => value !== undefined)
|
|
1938
1998
|
.map(([key, value]) => `<${escapeXmlElement(key)}>${escapeXmlText(formatLiteral(value))}</${escapeXmlElement(key)}>`)
|
|
1939
1999
|
.join("");
|
|
1940
2000
|
const geometryXml = feature.geometry ? (geoJsonGeometryToGml(feature.geometry, srsName) ?? "") : "";
|
|
1941
|
-
const
|
|
1942
|
-
const featureXml = `<${escapeXmlElement(typeName)}>${propertyXml}${geometryXml ? `<${
|
|
2001
|
+
const property = geometryXml ? escapeXmlElement(requireTransactionGeometryProperty(geometryProperty)) : "";
|
|
2002
|
+
const featureXml = `<${escapeXmlElement(typeName)}>${propertyXml}${geometryXml ? `<${property}>${geometryXml}</${property}>` : ""}</${escapeXmlElement(typeName)}>`;
|
|
1943
2003
|
return `<wfs:Insert handle="${handle}">${featureXml}</wfs:Insert>`;
|
|
1944
2004
|
}
|
|
1945
|
-
function buildUpdateBlock(typeName, feature, handle, srsName) {
|
|
2005
|
+
function buildUpdateBlock(typeName, feature, handle, srsName, geometryProperty) {
|
|
1946
2006
|
const attributes = feature.attributes;
|
|
1947
2007
|
const properties = Object.entries(attributes)
|
|
1948
2008
|
.filter(([, value]) => value !== undefined)
|
|
1949
2009
|
.map(([key, value]) => `<wfs:Property><wfs:ValueReference>${escapeXmlText(key)}</wfs:ValueReference><wfs:Value>${escapeXmlText(formatLiteral(value))}</wfs:Value></wfs:Property>`)
|
|
1950
2010
|
.join("");
|
|
1951
|
-
const
|
|
1952
|
-
? `<wfs:Property><wfs:ValueReference>${escapeXmlText(
|
|
2011
|
+
const geometryXml = feature.geometry
|
|
2012
|
+
? `<wfs:Property><wfs:ValueReference>${escapeXmlText(requireTransactionGeometryProperty(geometryProperty))}</wfs:ValueReference><wfs:Value>${geoJsonGeometryToGml(feature.geometry, srsName) ?? ""}</wfs:Value></wfs:Property>`
|
|
1953
2013
|
: "";
|
|
1954
2014
|
const filter = feature.id !== undefined
|
|
1955
2015
|
? `<fes:Filter><fes:ResourceId rid="${escapeXmlAttr(String(feature.id))}"/></fes:Filter>`
|
|
1956
2016
|
: "";
|
|
1957
|
-
return `<wfs:Update handle="${handle}" typeName="${escapeXmlAttr(typeName)}">${properties}${
|
|
2017
|
+
return `<wfs:Update handle="${handle}" typeName="${escapeXmlAttr(typeName)}">${properties}${geometryXml}${filter}</wfs:Update>`;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Fail closed rather than write geometry into a guessed property: a
|
|
2021
|
+
* transaction that targets the wrong element name is accepted by some servers
|
|
2022
|
+
* and silently drops the geometry.
|
|
2023
|
+
*/
|
|
2024
|
+
function requireTransactionGeometryProperty(geometryProperty) {
|
|
2025
|
+
if (geometryProperty === undefined) {
|
|
2026
|
+
throw new HonuaWfsProtocolError("unresolved-geometry-property", "WFS transaction geometry requires the feature type's geometry property; set locator.geometryName or make DescribeFeatureType reachable");
|
|
2027
|
+
}
|
|
2028
|
+
return geometryProperty;
|
|
1958
2029
|
}
|
|
1959
2030
|
function buildDeleteBlock(typeName, id, handle) {
|
|
1960
2031
|
return `<wfs:Delete handle="${handle}" typeName="${escapeXmlAttr(typeName)}"><fes:Filter><fes:ResourceId rid="${escapeXmlAttr(String(id))}"/></fes:Filter></wfs:Delete>`;
|
|
@@ -2046,10 +2117,11 @@ function loadOdataWriteCodec() {
|
|
|
2046
2117
|
/**
|
|
2047
2118
|
* Adapter factory for an OData v4 entity set.
|
|
2048
2119
|
*
|
|
2049
|
-
* `Query.where` compiles to
|
|
2050
|
-
* `$
|
|
2051
|
-
* becomes `$top`/`$skip`.
|
|
2052
|
-
* `geo.intersects` /
|
|
2120
|
+
* The deprecated source-native `Query.where` migration member compiles to
|
|
2121
|
+
* OData `$filter`, `Query.outFields` becomes `$select`, `Query.orderBy`
|
|
2122
|
+
* becomes `$orderby`, and `Query.pagination` becomes `$top`/`$skip`.
|
|
2123
|
+
* Geospatial filters translate to OData's `geo.intersects` /
|
|
2124
|
+
* `geo.distance` family.
|
|
2053
2125
|
*
|
|
2054
2126
|
* @example
|
|
2055
2127
|
* ```ts
|
|
@@ -2064,7 +2136,6 @@ function loadOdataWriteCodec() {
|
|
|
2064
2136
|
* }],
|
|
2065
2137
|
* });
|
|
2066
2138
|
* const result = await dataset.source("incidents")!.queryAll({
|
|
2067
|
-
* where: "Severity ge 3",
|
|
2068
2139
|
* orderBy: [{ field: "ReportedAt", direction: "desc" }],
|
|
2069
2140
|
* pagination: { limit: 100 },
|
|
2070
2141
|
* });
|
|
@@ -2072,9 +2143,16 @@ function loadOdataWriteCodec() {
|
|
|
2072
2143
|
*/
|
|
2073
2144
|
export function odataSource(descriptor, client, policy, options = {}) {
|
|
2074
2145
|
descriptor = normalizeCapabilityDescriptor(descriptor);
|
|
2075
|
-
const
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2146
|
+
const module = odataProtocolModule(client);
|
|
2147
|
+
const discovered = module.discover(descriptor);
|
|
2148
|
+
if (discovered instanceof Promise) {
|
|
2149
|
+
// OData discovery only binds an explicitly injected client to an entity
|
|
2150
|
+
// set. `$metadata` remains lazy, so built-in Source construction is
|
|
2151
|
+
// intentionally synchronous.
|
|
2152
|
+
throw new Error("odata: built-in source construction requires synchronous protocol-module discovery");
|
|
2153
|
+
}
|
|
2154
|
+
const entity = discovered.adapter;
|
|
2155
|
+
const declaredCaps = discovered.capabilities;
|
|
2078
2156
|
const negotiation = new OdataCapabilityNegotiator(entity, declaredCaps);
|
|
2079
2157
|
const writeEncoding = options.writeEncoding ?? "legacy";
|
|
2080
2158
|
if (writeEncoding !== "legacy" && writeEncoding !== "lossless-json") {
|
|
@@ -2089,8 +2167,17 @@ export function odataSource(descriptor, client, policy, options = {}) {
|
|
|
2089
2167
|
await negotiation.ensureAdvertised(descriptor, "query");
|
|
2090
2168
|
const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
|
|
2091
2169
|
const params = await buildOdataParams(entity, descriptor, request, { count: true, geomColumn });
|
|
2092
|
-
|
|
2093
|
-
|
|
2170
|
+
return module.execute(discovered, {
|
|
2171
|
+
compiled: createOdataProtocolCompiledQuery(entity.entitySet, "query", params),
|
|
2172
|
+
operation: "query",
|
|
2173
|
+
query: {
|
|
2174
|
+
count: true,
|
|
2175
|
+
fields: await negotiation.fieldsFor(descriptor.id),
|
|
2176
|
+
...(request?.returnGeometry !== undefined ? { returnGeometry: request.returnGeometry } : {}),
|
|
2177
|
+
...(geomColumn ? { geometryColumn: geomColumn } : {}),
|
|
2178
|
+
},
|
|
2179
|
+
...(request?.signal ? { signal: request.signal } : {}),
|
|
2180
|
+
});
|
|
2094
2181
|
},
|
|
2095
2182
|
async queryAll(request) {
|
|
2096
2183
|
ensureCapability(descriptor, declaredCaps, "query");
|
|
@@ -2098,30 +2185,20 @@ export function odataSource(descriptor, client, policy, options = {}) {
|
|
|
2098
2185
|
const geomColumn = await resolveOdataGeometryColumn(entity, descriptor);
|
|
2099
2186
|
const params = await buildOdataParams(entity, descriptor, request, { count: true, geomColumn });
|
|
2100
2187
|
const limit = request?.pagination?.limit;
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
const exceededTransferLimit = collectedExceeded || countExceeded;
|
|
2116
|
-
const fields = await negotiation.fieldsFor(descriptor.id);
|
|
2117
|
-
return {
|
|
2118
|
-
features: limited,
|
|
2119
|
-
exceededTransferLimit,
|
|
2120
|
-
...(typeof drained.totalCount === "number"
|
|
2121
|
-
? { totalCount: drained.totalCount }
|
|
2122
|
-
: { totalCount: limited.length }),
|
|
2123
|
-
...(fields.length > 0 ? { fields } : {}),
|
|
2124
|
-
};
|
|
2188
|
+
return module.execute(discovered, {
|
|
2189
|
+
// The operation-bound compiler adds the one-row queryAll lookahead;
|
|
2190
|
+
// the runtime query context retains the caller-visible logical limit.
|
|
2191
|
+
compiled: createOdataProtocolCompiledQuery(entity.entitySet, "queryAll", params),
|
|
2192
|
+
operation: "queryAll",
|
|
2193
|
+
query: {
|
|
2194
|
+
count: true,
|
|
2195
|
+
fields: await negotiation.fieldsFor(descriptor.id),
|
|
2196
|
+
...(request?.returnGeometry !== undefined ? { returnGeometry: request.returnGeometry } : {}),
|
|
2197
|
+
...(geomColumn ? { geometryColumn: geomColumn } : {}),
|
|
2198
|
+
...(limit !== undefined ? { limit } : {}),
|
|
2199
|
+
},
|
|
2200
|
+
...(request?.signal ? { signal: request.signal } : {}),
|
|
2201
|
+
});
|
|
2125
2202
|
},
|
|
2126
2203
|
async queryAggregate() {
|
|
2127
2204
|
// OData aggregation is dialect-specific (`$apply` pipeline). The
|
|
@@ -2144,7 +2221,7 @@ export function odataSource(descriptor, client, policy, options = {}) {
|
|
|
2144
2221
|
const fields = await negotiation.fieldsFor(descriptor.id);
|
|
2145
2222
|
const stream = entity.queryStream(params);
|
|
2146
2223
|
for await (const page of stream) {
|
|
2147
|
-
yield
|
|
2224
|
+
yield odataProtocolResultFromPage(descriptor, page, fields, request?.returnGeometry, geomColumn);
|
|
2148
2225
|
}
|
|
2149
2226
|
},
|
|
2150
2227
|
async queryObjectIds(request) {
|
|
@@ -2565,42 +2642,6 @@ function describeBatchError(body, status) {
|
|
|
2565
2642
|
function stripLeadingSlashLocal(url) {
|
|
2566
2643
|
return url.startsWith("/") ? url.slice(1) : url;
|
|
2567
2644
|
}
|
|
2568
|
-
function requireOdataLocator(descriptor) {
|
|
2569
|
-
const { entitySet, url, layerId } = descriptor.locator;
|
|
2570
|
-
// Resolve the entity-set token. Honua Server's `SourceLocator` only
|
|
2571
|
-
// carries `url`, `serviceId`, and `layerId`, so server-produced OData
|
|
2572
|
-
// bindings arrive without `entitySet`. The canonical server route is
|
|
2573
|
-
// layer-scoped — `/odata/Layers(<layerId>)/Features` — so derive that
|
|
2574
|
-
// path when only `layerId` is provided. SDK callers that already pass
|
|
2575
|
-
// `entitySet` (the historical path) continue to win.
|
|
2576
|
-
let resolvedEntitySet;
|
|
2577
|
-
if (typeof entitySet === "string" && entitySet !== "") {
|
|
2578
|
-
resolvedEntitySet = entitySet;
|
|
2579
|
-
}
|
|
2580
|
-
else if (typeof layerId === "number" && Number.isFinite(layerId)) {
|
|
2581
|
-
resolvedEntitySet = `Layers(${layerId})/Features`;
|
|
2582
|
-
}
|
|
2583
|
-
if (resolvedEntitySet === undefined) {
|
|
2584
|
-
throw new Error(`createDataset: source "${descriptor.id}" (odata) requires locator.entitySet or locator.layerId`);
|
|
2585
|
-
}
|
|
2586
|
-
// `locator.url` is informational on the canonical surface; the request
|
|
2587
|
-
// path is built from `basePath/<entitySet>`. When `locator.url` carries
|
|
2588
|
-
// a path, treat its trailing path component as the basePath so a
|
|
2589
|
-
// descriptor pointing at `https://srv/odata/v4` still resolves correctly.
|
|
2590
|
-
const basePath = url ? extractOdataBasePath(url) : "/odata";
|
|
2591
|
-
return { entitySet: resolvedEntitySet, basePath };
|
|
2592
|
-
}
|
|
2593
|
-
function extractOdataBasePath(url) {
|
|
2594
|
-
try {
|
|
2595
|
-
const parsed = new URL(url);
|
|
2596
|
-
const pathname = trimTrailingSlashes(parsed.pathname);
|
|
2597
|
-
return pathname === "" ? "/odata" : pathname;
|
|
2598
|
-
}
|
|
2599
|
-
catch {
|
|
2600
|
-
// Relative URL: take it as the basePath verbatim.
|
|
2601
|
-
return url.startsWith("/") ? trimTrailingSlashes(url) || "/odata" : `/${trimTrailingSlashes(url)}`;
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
2645
|
/**
|
|
2605
2646
|
* Resolve the OData geometry column for a descriptor. Prefers
|
|
2606
2647
|
* `descriptor.schema.fields` (where a field of type
|
|
@@ -2712,6 +2753,9 @@ async function buildOdataParams(entity, descriptor, request, options) {
|
|
|
2712
2753
|
if (rewritten !== "")
|
|
2713
2754
|
filterParts.push(rewritten);
|
|
2714
2755
|
}
|
|
2756
|
+
const typedFilter = await compileOdataQueryFilter(entity, descriptor, request, options.geomColumn);
|
|
2757
|
+
if (typedFilter !== undefined)
|
|
2758
|
+
filterParts.push(typedFilter);
|
|
2715
2759
|
if (request.spatialFilter) {
|
|
2716
2760
|
// `$metadata` is advisory here — it only refines the geometry column
|
|
2717
2761
|
// and SRID context. When it is missing or failing, fall back to the
|
|
@@ -2781,52 +2825,49 @@ async function buildOdataParams(entity, descriptor, request, options) {
|
|
|
2781
2825
|
}
|
|
2782
2826
|
if (request.signal)
|
|
2783
2827
|
out.signal = request.signal;
|
|
2784
|
-
void descriptor;
|
|
2785
2828
|
return out;
|
|
2786
2829
|
}
|
|
2787
|
-
function odataResultFromPage(descriptor, page, fields, returnGeometry, geomColumn) {
|
|
2788
|
-
const features = page.rows.map((row) => odataRowToFeature(row, descriptor, returnGeometry, geomColumn));
|
|
2789
|
-
const exceededTransferLimit = typeof page.totalCount === "number" ? features.length < page.totalCount : Boolean(page.nextLink);
|
|
2790
|
-
return {
|
|
2791
|
-
features,
|
|
2792
|
-
exceededTransferLimit,
|
|
2793
|
-
...(typeof page.totalCount === "number" ? { totalCount: page.totalCount } : {}),
|
|
2794
|
-
...(fields.length > 0 ? { fields } : {}),
|
|
2795
|
-
};
|
|
2796
|
-
}
|
|
2797
2830
|
/**
|
|
2798
|
-
*
|
|
2799
|
-
*
|
|
2800
|
-
*
|
|
2801
|
-
*
|
|
2802
|
-
* the attributes envelope and the `geometry` field — defensive against
|
|
2803
|
-
* servers that ignore the `$select` exclusion (or callers that bypassed
|
|
2804
|
-
* metadata-derived selects). The geometry column is sourced from the
|
|
2805
|
-
* caller-resolved `geomColumn` (descriptor schema → metadata
|
|
2806
|
-
* `isSpatial`) and falls back to the canonical name guesses on the
|
|
2807
|
-
* row keys when neither declares one.
|
|
2831
|
+
* Compile `Query.filter` / `Query.temporalFilter` into an OData `$filter`
|
|
2832
|
+
* fragment. OData has no protocol-level time parameter, so a temporal filter
|
|
2833
|
+
* must name its field; without one the adapter refuses rather than dropping the
|
|
2834
|
+
* constraint. Spatial nodes reuse the `$metadata`-aware `geo.*` emitter.
|
|
2808
2835
|
*/
|
|
2809
|
-
function
|
|
2810
|
-
const
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
attributes: rest,
|
|
2817
|
-
geometry: returnGeometry === false ? null : (geometry ?? null),
|
|
2818
|
-
};
|
|
2819
|
-
}
|
|
2820
|
-
return { attributes: row, geometry: null };
|
|
2821
|
-
}
|
|
2822
|
-
function findGeometryKey(row) {
|
|
2823
|
-
for (const key of Object.keys(row)) {
|
|
2824
|
-
const lower = key.toLowerCase();
|
|
2825
|
-
if (lower === "geometry" || lower === "geography" || lower === "shape")
|
|
2826
|
-
return key;
|
|
2836
|
+
async function compileOdataQueryFilter(entity, descriptor, request, geomColumn) {
|
|
2837
|
+
const ctx = queryFilterContext(descriptor, geomColumn);
|
|
2838
|
+
const temporalExpression = request.temporalFilter
|
|
2839
|
+
? temporalFilterToExpression(request.temporalFilter, ctx)
|
|
2840
|
+
: undefined;
|
|
2841
|
+
if (request.temporalFilter && temporalExpression === undefined) {
|
|
2842
|
+
throw new HonuaCapabilityNotSupportedError("temporalFilter.field", descriptor.protocol, descriptor.id);
|
|
2827
2843
|
}
|
|
2828
|
-
|
|
2844
|
+
const expression = andQueryFilters(request.filter, temporalExpression);
|
|
2845
|
+
if (!expression)
|
|
2846
|
+
return undefined;
|
|
2847
|
+
const meta = await entity.metadata().catch(() => undefined);
|
|
2848
|
+
const typeName = meta ? meta.entitySets[entity.entitySetName] : undefined;
|
|
2849
|
+
const spatialFields = (typeName && meta ? meta.fields[typeName] : undefined) ?? [];
|
|
2850
|
+
const spatialContext = {
|
|
2851
|
+
...(geomColumn ? { geometryColumn: geomColumn } : {}),
|
|
2852
|
+
...(spatialFields.length > 0 ? { geometryFields: spatialFields } : {}),
|
|
2853
|
+
};
|
|
2854
|
+
return compileQueryFilterToOData(expression, ctx, (node) => {
|
|
2855
|
+
const spatialRel = ODATA_SPATIAL_REL[node.operator];
|
|
2856
|
+
if (!spatialRel) {
|
|
2857
|
+
throw new HonuaCapabilityNotSupportedError(`filter.spatial.${node.operator}`, descriptor.protocol, descriptor.id);
|
|
2858
|
+
}
|
|
2859
|
+
return buildOdataSpatialFilter({ geometry: node.geometry.geometry, geometryType: node.geometry.geometryType, spatialRel }, spatialContext);
|
|
2860
|
+
});
|
|
2829
2861
|
}
|
|
2862
|
+
/**
|
|
2863
|
+
* OData v4 standard geo functions express only `geo.intersects` (and the
|
|
2864
|
+
* distance family). Every other topological predicate is refused by name so a
|
|
2865
|
+
* `within` never silently returns intersecting rows.
|
|
2866
|
+
*/
|
|
2867
|
+
const ODATA_SPATIAL_REL = {
|
|
2868
|
+
intersects: "esriSpatialRelIntersects",
|
|
2869
|
+
"bbox-intersects": "esriSpatialRelEnvelopeIntersects",
|
|
2870
|
+
};
|
|
2830
2871
|
function featureToOdataBody(feature) {
|
|
2831
2872
|
const body = { ...feature.attributes };
|
|
2832
2873
|
if (feature.geometry !== undefined && feature.geometry !== null) {
|
|
@@ -3212,8 +3253,11 @@ function requireImageServiceLocator(descriptor) {
|
|
|
3212
3253
|
function requireImageServerCompatibleQuery(request) {
|
|
3213
3254
|
if (!request)
|
|
3214
3255
|
return;
|
|
3256
|
+
if (request.filter !== undefined && queryFilterHasSpatial(request.filter)) {
|
|
3257
|
+
throw new Error("geoservices-image-service: a spatial filter node is not supported on the raster catalog; the ImageServer catalog endpoint does not accept geometry / geometryType / spatialRel filters. Attribute and temporal predicates are supported; for geometry, call protocol().identify() / exportImage() on the typed escape hatch.");
|
|
3258
|
+
}
|
|
3215
3259
|
if (request.spatialFilter) {
|
|
3216
|
-
throw new Error("geoservices-image-service: Query.spatialFilter is not supported on the raster catalog; the ImageServer catalog endpoint does not accept geometry / geometryType / spatialRel filters.
|
|
3260
|
+
throw new Error("geoservices-image-service: Query.spatialFilter is not supported on the raster catalog; the ImageServer catalog endpoint does not accept geometry / geometryType / spatialRel filters. During migration, the deprecated source-native Query.where member can constrain the catalog; otherwise call protocol().identify() / exportImage() on the typed escape hatch.");
|
|
3217
3261
|
}
|
|
3218
3262
|
if (request.orderBy && request.orderBy.length > 0) {
|
|
3219
3263
|
throw new Error("geoservices-image-service: Query.orderBy is not supported on the raster catalog; the ImageServer catalog endpoint does not honor orderByFields. Drop orderBy or sort client-side.");
|
|
@@ -3285,6 +3329,14 @@ function requireWmsCompatibleQuery(descriptor, request) {
|
|
|
3285
3329
|
if (typeof request.where === "string" && request.where.length > 0) {
|
|
3286
3330
|
throw new Error(`wms: Query.where is not supported on GetFeatureInfo for source "${descriptor.id}"; WMS has no SQL/CQL filter on the wire. Pre-filter via Query.spatialFilter (point) or use a tabular protocol.`);
|
|
3287
3331
|
}
|
|
3332
|
+
if (request.filter !== undefined) {
|
|
3333
|
+
throw new HonuaCapabilityNotSupportedError("filter", descriptor.protocol, descriptor.id);
|
|
3334
|
+
}
|
|
3335
|
+
if (request.temporalFilter !== undefined) {
|
|
3336
|
+
// WMS carries time as a dimension on the typed `WmsMapRequest` envelope,
|
|
3337
|
+
// not as a GetFeatureInfo attribute predicate.
|
|
3338
|
+
throw new HonuaCapabilityNotSupportedError("temporalFilter", descriptor.protocol, descriptor.id);
|
|
3339
|
+
}
|
|
3288
3340
|
if (request.outFields && request.outFields.length > 0) {
|
|
3289
3341
|
throw new Error(`wms: Query.outFields is not supported on GetFeatureInfo for source "${descriptor.id}"; the server returns the layer's full attribute schema. Project client-side after the result lands.`);
|
|
3290
3342
|
}
|
|
@@ -3357,12 +3409,24 @@ function wmsCrsFromGeometrySpatialReference(spatialReference) {
|
|
|
3357
3409
|
}
|
|
3358
3410
|
return "CRS:84";
|
|
3359
3411
|
}
|
|
3360
|
-
function toStacRequest(request, collectionScope) {
|
|
3412
|
+
function toStacRequest(request, collectionScope, descriptor) {
|
|
3361
3413
|
const out = {};
|
|
3362
|
-
|
|
3414
|
+
const cql2 = request ? compileCql2QueryFilter(request, descriptor) : {};
|
|
3415
|
+
const filterParts = [];
|
|
3416
|
+
if (request?.where !== undefined && request.where !== "")
|
|
3417
|
+
filterParts.push(request.where);
|
|
3418
|
+
if (cql2.filter !== undefined)
|
|
3419
|
+
filterParts.push(cql2.filter);
|
|
3420
|
+
if (filterParts.length > 0) {
|
|
3421
|
+
out.filter = filterParts.length === 1 ? filterParts[0] : filterParts.map((part) => `(${part})`).join(" AND ");
|
|
3422
|
+
out.filterLang = "cql2-text";
|
|
3423
|
+
}
|
|
3424
|
+
else if (request?.where !== undefined) {
|
|
3363
3425
|
out.filter = request.where;
|
|
3364
3426
|
out.filterLang = "cql2-text";
|
|
3365
3427
|
}
|
|
3428
|
+
if (cql2.datetime !== undefined)
|
|
3429
|
+
out.datetime = cql2.datetime;
|
|
3366
3430
|
if (request?.outFields && request.outFields.length > 0) {
|
|
3367
3431
|
out.fields = { include: [...request.outFields] };
|
|
3368
3432
|
}
|
|
@@ -3374,15 +3438,30 @@ function toStacRequest(request, collectionScope) {
|
|
|
3374
3438
|
out.sortby = request.orderBy.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
|
|
3375
3439
|
}
|
|
3376
3440
|
if (request?.spatialFilter) {
|
|
3377
|
-
if (request.spatialFilter.geometryType
|
|
3378
|
-
|
|
3441
|
+
if (request.spatialFilter.geometryType === "esriGeometryEnvelope") {
|
|
3442
|
+
const env = request.spatialFilter.geometry;
|
|
3443
|
+
if (typeof env.xmin === "number" &&
|
|
3444
|
+
typeof env.ymin === "number" &&
|
|
3445
|
+
typeof env.xmax === "number" &&
|
|
3446
|
+
typeof env.ymax === "number") {
|
|
3447
|
+
out.bbox = [env.xmin, env.ymin, env.xmax, env.ymax];
|
|
3448
|
+
}
|
|
3379
3449
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3450
|
+
else {
|
|
3451
|
+
// STAC Item Search takes an arbitrary GeoJSON geometry on `intersects`
|
|
3452
|
+
// (OGC 21-047 §7.2), so a polygon AOI does not have to be degraded to
|
|
3453
|
+
// its envelope. `intersects` is the only predicate the parameter
|
|
3454
|
+
// expresses; refuse other spatial relationships instead of silently
|
|
3455
|
+
// widening them.
|
|
3456
|
+
const rel = request.spatialFilter.spatialRel;
|
|
3457
|
+
if (rel !== undefined && rel !== "esriSpatialRelIntersects" && rel !== "esriSpatialRelEnvelopeIntersects") {
|
|
3458
|
+
throw new Error(`stac: spatialFilter.spatialRel "${rel}" is not supported; STAC search only expresses an intersects predicate.`);
|
|
3459
|
+
}
|
|
3460
|
+
const geometry = toStacIntersectsGeometry(request.spatialFilter.geometry);
|
|
3461
|
+
if (geometry === null) {
|
|
3462
|
+
throw new Error(`stac: spatialFilter.geometryType "${request.spatialFilter.geometryType}" could not be converted to a GeoJSON geometry for STAC intersects.`);
|
|
3463
|
+
}
|
|
3464
|
+
out.intersects = geometry;
|
|
3386
3465
|
}
|
|
3387
3466
|
}
|
|
3388
3467
|
if (collectionScope !== undefined && collectionScope !== "") {
|
|
@@ -3392,11 +3471,35 @@ function toStacRequest(request, collectionScope) {
|
|
|
3392
3471
|
out.signal = request.signal;
|
|
3393
3472
|
return out;
|
|
3394
3473
|
}
|
|
3474
|
+
/**
|
|
3475
|
+
* Normalize a canonical `spatialFilter.geometry` into the GeoJSON geometry
|
|
3476
|
+
* STAC `intersects` expects. Esri-shaped geometries (the shape the
|
|
3477
|
+
* `polygon()` / `spatialIntersects()` builders emit) are converted; a
|
|
3478
|
+
* geometry that is already GeoJSON passes through untouched.
|
|
3479
|
+
*/
|
|
3480
|
+
function toStacIntersectsGeometry(geometry) {
|
|
3481
|
+
if (typeof geometry.type === "string" && (geometry.coordinates !== undefined || geometry.geometries !== undefined)) {
|
|
3482
|
+
return geometry;
|
|
3483
|
+
}
|
|
3484
|
+
const converted = esriGeometryToGeoJSON(geometry);
|
|
3485
|
+
return converted === null ? null : converted;
|
|
3486
|
+
}
|
|
3395
3487
|
/**
|
|
3396
3488
|
* Project a STAC API search request onto the client-side filter params a
|
|
3397
3489
|
* static catalog traversal applies (it has no server-side query grammar).
|
|
3398
3490
|
*/
|
|
3399
3491
|
function toStacStaticParams(request) {
|
|
3492
|
+
if (request.intersects !== undefined) {
|
|
3493
|
+
// A static catalog has no search endpoint; traversal only applies the
|
|
3494
|
+
// bbox / datetime / collection filters below. Silently dropping the
|
|
3495
|
+
// geometry would return an unfiltered superset.
|
|
3496
|
+
throw new Error("stac-static: an `intersects` geometry filter is not supported by static catalog traversal; use an envelope spatial filter.");
|
|
3497
|
+
}
|
|
3498
|
+
if (request.filter !== undefined) {
|
|
3499
|
+
// Static traversal has no CQL2 evaluator. Dropping the predicate would
|
|
3500
|
+
// return every item in the catalog, so refuse instead.
|
|
3501
|
+
throw new Error("stac-static: a CQL2 attribute filter (Query.filter or Query.where) is not supported by static catalog traversal; use a STAC API endpoint or filter client-side after the result lands.");
|
|
3502
|
+
}
|
|
3400
3503
|
const out = {};
|
|
3401
3504
|
if (request.collections && request.collections.length > 0)
|
|
3402
3505
|
out.collections = request.collections;
|
|
@@ -3416,14 +3519,26 @@ function toTypedFeatureFromStac(feature) {
|
|
|
3416
3519
|
geometry: feature.geometry,
|
|
3417
3520
|
};
|
|
3418
3521
|
}
|
|
3419
|
-
function toOgcRecordsRequest(request) {
|
|
3522
|
+
function toOgcRecordsRequest(request, descriptor) {
|
|
3420
3523
|
if (!request)
|
|
3421
3524
|
return {};
|
|
3422
3525
|
const out = {};
|
|
3423
|
-
|
|
3526
|
+
const cql2 = compileCql2QueryFilter(request, descriptor);
|
|
3527
|
+
const filterParts = [];
|
|
3528
|
+
if (request.where !== undefined && request.where !== "")
|
|
3529
|
+
filterParts.push(request.where);
|
|
3530
|
+
if (cql2.filter !== undefined)
|
|
3531
|
+
filterParts.push(cql2.filter);
|
|
3532
|
+
if (filterParts.length > 0) {
|
|
3533
|
+
out.filter = filterParts.length === 1 ? filterParts[0] : filterParts.map((part) => `(${part})`).join(" AND ");
|
|
3534
|
+
out.filterLang = "cql2-text";
|
|
3535
|
+
}
|
|
3536
|
+
else if (request.where !== undefined) {
|
|
3424
3537
|
out.filter = request.where;
|
|
3425
3538
|
out.filterLang = "cql2-text";
|
|
3426
3539
|
}
|
|
3540
|
+
if (cql2.datetime !== undefined)
|
|
3541
|
+
out.datetime = cql2.datetime;
|
|
3427
3542
|
if (request.outFields && request.outFields.length > 0)
|
|
3428
3543
|
out.properties = [...request.outFields];
|
|
3429
3544
|
if (request.pagination) {
|
|
@@ -3518,12 +3633,27 @@ function applyQueryAllLimit(features, limit) {
|
|
|
3518
3633
|
}
|
|
3519
3634
|
return { features: features.slice(0, limit), exceededTransferLimit: true };
|
|
3520
3635
|
}
|
|
3521
|
-
function toFeatureLayerRequest(request) {
|
|
3636
|
+
function toFeatureLayerRequest(request, descriptor) {
|
|
3522
3637
|
if (!request)
|
|
3523
3638
|
return {};
|
|
3524
3639
|
const out = {};
|
|
3525
|
-
|
|
3640
|
+
const ctx = queryFilterContext(descriptor);
|
|
3641
|
+
// The typed filter, the deprecated source-native `where`, and a field-bound
|
|
3642
|
+
// temporal predicate are conjunctive: each contributes a SQL-92 fragment and
|
|
3643
|
+
// the adapter ANDs them. A spatial node inside the typed filter leaves the
|
|
3644
|
+
// SQL and becomes the request's geometry parameters.
|
|
3645
|
+
const compiled = compileGeoServicesQueryFilter(request, ctx);
|
|
3646
|
+
const whereParts = [];
|
|
3647
|
+
if (request.where !== undefined && request.where !== "")
|
|
3648
|
+
whereParts.push(request.where);
|
|
3649
|
+
if (compiled.where !== undefined)
|
|
3650
|
+
whereParts.push(compiled.where);
|
|
3651
|
+
if (whereParts.length > 0) {
|
|
3652
|
+
out.where = whereParts.length === 1 ? whereParts[0] : whereParts.map((part) => `(${part})`).join(" AND ");
|
|
3653
|
+
}
|
|
3654
|
+
else if (request.where !== undefined) {
|
|
3526
3655
|
out.where = request.where;
|
|
3656
|
+
}
|
|
3527
3657
|
if (request.outFields && request.outFields.length > 0)
|
|
3528
3658
|
out.outFields = [...request.outFields];
|
|
3529
3659
|
if (request.returnGeometry !== undefined)
|
|
@@ -3533,11 +3663,23 @@ function toFeatureLayerRequest(request) {
|
|
|
3533
3663
|
if (request.orderBy && request.orderBy.length > 0) {
|
|
3534
3664
|
out.orderByFields = request.orderBy.map((s) => `${s.field}${s.direction === "desc" ? " DESC" : ""}`).join(",");
|
|
3535
3665
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3666
|
+
const spatialFilter = request.spatialFilter ?? compiled.spatialFilter;
|
|
3667
|
+
if (request.spatialFilter && compiled.spatialFilter) {
|
|
3668
|
+
// One `query` request carries one geometry. Refuse rather than drop either
|
|
3669
|
+
// constraint or silently intersect them client-side.
|
|
3670
|
+
throw new HonuaCapabilityNotSupportedError("filter.spatial.multiple", descriptor.protocol, descriptor.id);
|
|
3671
|
+
}
|
|
3672
|
+
if (spatialFilter) {
|
|
3673
|
+
out.geometry = spatialFilter.geometry;
|
|
3674
|
+
out.geometryType = spatialFilter.geometryType;
|
|
3675
|
+
if (spatialFilter.spatialRel)
|
|
3676
|
+
out.spatialRel = spatialFilter.spatialRel;
|
|
3677
|
+
}
|
|
3678
|
+
if (compiled.time !== undefined) {
|
|
3679
|
+
out.extraParams = {
|
|
3680
|
+
...(out.extraParams ?? {}),
|
|
3681
|
+
time: compiled.time,
|
|
3682
|
+
};
|
|
3541
3683
|
}
|
|
3542
3684
|
if (request.pagination) {
|
|
3543
3685
|
if (request.pagination.offset !== undefined)
|
|
@@ -3549,6 +3691,58 @@ function toFeatureLayerRequest(request) {
|
|
|
3549
3691
|
out.signal = request.signal;
|
|
3550
3692
|
return out;
|
|
3551
3693
|
}
|
|
3694
|
+
/** Filter-compilation identity used when refusing a construct. */
|
|
3695
|
+
function queryFilterContext(descriptor, geometryProperty) {
|
|
3696
|
+
return {
|
|
3697
|
+
protocol: descriptor.protocol,
|
|
3698
|
+
sourceId: descriptor.id,
|
|
3699
|
+
...(geometryProperty !== undefined ? { geometryProperty } : {}),
|
|
3700
|
+
...descriptorQueryFilterConstraints(descriptor),
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
/**
|
|
3704
|
+
* Bind this source's evaluated capability profile into the filter gate.
|
|
3705
|
+
*
|
|
3706
|
+
* A profile is per-source evidence, so it narrows — never widens — what the
|
|
3707
|
+
* protocol's grammar allows: a GeoServices layer whose evidence advertises only
|
|
3708
|
+
* `esriSpatialRelIntersects`, or an OGC collection whose queryables list omits
|
|
3709
|
+
* `LIKE`, refuses those constructs by name even though the protocol could carry
|
|
3710
|
+
* them. Sources without a profile keep the protocol-level behaviour, because
|
|
3711
|
+
* absent evidence is not evidence of absence.
|
|
3712
|
+
*/
|
|
3713
|
+
function descriptorQueryFilterConstraints(descriptor) {
|
|
3714
|
+
const constraints = descriptor.capabilityProfile?.entries.find((entry) => entry.id === "query")?.constraints;
|
|
3715
|
+
if (!constraints)
|
|
3716
|
+
return {};
|
|
3717
|
+
const bound = queryFilterConstraints(constraints);
|
|
3718
|
+
if (bound.supportedFilterOperators === undefined &&
|
|
3719
|
+
bound.supportedSpatialPredicates === undefined &&
|
|
3720
|
+
bound.supportedTemporalPredicates === undefined) {
|
|
3721
|
+
return {};
|
|
3722
|
+
}
|
|
3723
|
+
return { ...bound, constraintSource: "capability-profile" };
|
|
3724
|
+
}
|
|
3725
|
+
/**
|
|
3726
|
+
* Lower `Query.filter` + `Query.temporalFilter` onto the GeoServices request
|
|
3727
|
+
* vocabulary: a SQL-92 `where` fragment, at most one geometry parameter set,
|
|
3728
|
+
* and the `time=` parameter when the temporal filter targets the layer's own
|
|
3729
|
+
* time dimension.
|
|
3730
|
+
*/
|
|
3731
|
+
function compileGeoServicesQueryFilter(request, ctx) {
|
|
3732
|
+
const temporalExpression = request.temporalFilter
|
|
3733
|
+
? temporalFilterToExpression(request.temporalFilter, ctx)
|
|
3734
|
+
: undefined;
|
|
3735
|
+
const expression = andQueryFilters(request.filter, temporalExpression);
|
|
3736
|
+
const compiled = expression ? compileQueryFilterToSql92(expression, ctx) : {};
|
|
3737
|
+
const time = request.temporalFilter && temporalExpression === undefined
|
|
3738
|
+
? compileTemporalFilterToGeoServicesTime(request.temporalFilter, ctx)
|
|
3739
|
+
: undefined;
|
|
3740
|
+
return {
|
|
3741
|
+
...(compiled.where !== undefined ? { where: compiled.where } : {}),
|
|
3742
|
+
...(compiled.spatialFilter ? { spatialFilter: compiled.spatialFilter } : {}),
|
|
3743
|
+
...(time !== undefined ? { time } : {}),
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3552
3746
|
/**
|
|
3553
3747
|
* Convert a translated GeoServices request into an extent-only variant. The
|
|
3554
3748
|
* caller's spatial filter / outSr / signal / orderByFields must survive; only
|
|
@@ -3585,6 +3779,10 @@ function hasExtentFilter(request) {
|
|
|
3585
3779
|
return true;
|
|
3586
3780
|
if (request.spatialFilter)
|
|
3587
3781
|
return true;
|
|
3782
|
+
if (request.filter !== undefined)
|
|
3783
|
+
return true;
|
|
3784
|
+
if (request.temporalFilter !== undefined)
|
|
3785
|
+
return true;
|
|
3588
3786
|
return false;
|
|
3589
3787
|
}
|
|
3590
3788
|
function computeExtentFromOgcFeatures(features) {
|
|
@@ -3641,14 +3839,26 @@ function extractExtentEnvelope(response) {
|
|
|
3641
3839
|
const count = typeof obj.count === "number" && Number.isFinite(obj.count) ? obj.count : undefined;
|
|
3642
3840
|
return { extent, count };
|
|
3643
3841
|
}
|
|
3644
|
-
function toOgcRequest(request) {
|
|
3842
|
+
function toOgcRequest(request, descriptor) {
|
|
3645
3843
|
if (!request)
|
|
3646
3844
|
return {};
|
|
3647
3845
|
const out = {};
|
|
3648
|
-
|
|
3846
|
+
const cql2 = compileCql2QueryFilter(request, descriptor);
|
|
3847
|
+
const filterParts = [];
|
|
3848
|
+
if (request.where !== undefined && request.where !== "")
|
|
3849
|
+
filterParts.push(request.where);
|
|
3850
|
+
if (cql2.filter !== undefined)
|
|
3851
|
+
filterParts.push(cql2.filter);
|
|
3852
|
+
if (filterParts.length > 0) {
|
|
3853
|
+
out.filter = filterParts.length === 1 ? filterParts[0] : filterParts.map((part) => `(${part})`).join(" AND ");
|
|
3854
|
+
out.filterLang = "cql2-text";
|
|
3855
|
+
}
|
|
3856
|
+
else if (request.where !== undefined) {
|
|
3649
3857
|
out.filter = request.where;
|
|
3650
3858
|
out.filterLang = "cql2-text";
|
|
3651
3859
|
}
|
|
3860
|
+
if (cql2.datetime !== undefined)
|
|
3861
|
+
out.datetime = cql2.datetime;
|
|
3652
3862
|
if (request.outFields && request.outFields.length > 0)
|
|
3653
3863
|
out.properties = [...request.outFields];
|
|
3654
3864
|
if (request.outSr !== undefined)
|
|
@@ -3698,12 +3908,42 @@ function toOgcRequest(request) {
|
|
|
3698
3908
|
* `offset` would silently materialize only a window of the match set and break
|
|
3699
3909
|
* the whole-result contract those methods promise.
|
|
3700
3910
|
*/
|
|
3701
|
-
function toOgcMaterializedRequest(request) {
|
|
3702
|
-
const { limit: _limit, offset: _offset, ...out } = toOgcRequest(request);
|
|
3911
|
+
function toOgcMaterializedRequest(request, descriptor) {
|
|
3912
|
+
const { limit: _limit, offset: _offset, ...out } = toOgcRequest(request, descriptor);
|
|
3703
3913
|
void _limit;
|
|
3704
3914
|
void _offset;
|
|
3705
3915
|
return out;
|
|
3706
3916
|
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Compile `Query.filter` / `Query.temporalFilter` for the CQL2 protocols (OGC
|
|
3919
|
+
* API Features, OGC API Records, STAC). A temporal filter without a field is
|
|
3920
|
+
* the protocol's own `datetime=` parameter; with a field it becomes a CQL2
|
|
3921
|
+
* temporal predicate so the constraint stays exact.
|
|
3922
|
+
*/
|
|
3923
|
+
function compileCql2QueryFilter(request, descriptor) {
|
|
3924
|
+
const ctx = queryFilterContext(descriptor, ogcGeometryProperty(descriptor));
|
|
3925
|
+
const temporalExpression = request.temporalFilter
|
|
3926
|
+
? temporalFilterToExpression(request.temporalFilter, ctx)
|
|
3927
|
+
: undefined;
|
|
3928
|
+
const expression = andQueryFilters(request.filter, temporalExpression);
|
|
3929
|
+
const filter = expression ? compileQueryFilterToCql2Text(expression, ctx, geometryToWkt) : undefined;
|
|
3930
|
+
const datetime = request.temporalFilter && temporalExpression === undefined
|
|
3931
|
+
? compileTemporalFilterToOgcDatetime(request.temporalFilter, ctx)
|
|
3932
|
+
: undefined;
|
|
3933
|
+
return {
|
|
3934
|
+
...(filter !== undefined ? { filter } : {}),
|
|
3935
|
+
...(datetime !== undefined ? { datetime } : {}),
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Geometry property a CQL2 spatial predicate references. OGC collections
|
|
3940
|
+
* default to the GeoJSON `geometry` member; `locator.geometryName` pins a
|
|
3941
|
+
* server that publishes a differently named queryable.
|
|
3942
|
+
*/
|
|
3943
|
+
function ogcGeometryProperty(descriptor) {
|
|
3944
|
+
const declared = descriptor.locator.geometryName;
|
|
3945
|
+
return typeof declared === "string" && declared.length > 0 ? declared : "geometry";
|
|
3946
|
+
}
|
|
3707
3947
|
function featureLayerResultFromTyped(response) {
|
|
3708
3948
|
return {
|
|
3709
3949
|
features: response.features ?? [],
|