@honua/react 0.1.1-beta.0 → 0.1.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +87 -18
- package/contract/types.d.ts.map +1 -1
- package/contract/widget-source.d.ts.map +1 -1
- package/core/client.d.ts +45 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +224 -46
- package/core/error-classifications.d.ts +6 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +6 -0
- package/core/error-code-registry.d.ts +6 -0
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +6 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +68 -4
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +325 -8
- 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 +31 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +136 -19
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +123 -9
- 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/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/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 +147 -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/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
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { PROTOCOL_DEFAULT_CAPABILITIES } from "../contract/types.js";
|
|
2
|
+
import { isHonuaSdkError } from "../core/error-envelope.js";
|
|
3
|
+
import { HonuaAbortError, HonuaCapabilityNotSupportedError } from "../core/errors.js";
|
|
4
|
+
import { HonuaWfsProtocolError } from "../core/wfs-protocol-error.js";
|
|
5
|
+
import { canonicalStringify, sha256, toJsonValue } from "./canonical.js";
|
|
6
|
+
import { queryIrAuthorityFingerprint } from "./ir.js";
|
|
7
|
+
import { wfsProtocolMethodForFilter, wfsProtocolQueryCompiler, wfsRuntimeEndpointIdentity } from "./wfs-v1.js";
|
|
8
|
+
export { HonuaWfsProtocolError };
|
|
9
|
+
const WFS_PAGE_FINGERPRINTS = new WeakMap();
|
|
10
|
+
/** Construct the first-party query-capable WFS protocol module. */
|
|
11
|
+
export function wfsProtocolModule(client) {
|
|
12
|
+
const handles = new WeakMap();
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
kind: "wfs",
|
|
15
|
+
environments: Object.freeze(["browser", "node", "worker"]),
|
|
16
|
+
capabilities(descriptor) {
|
|
17
|
+
return descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wfs;
|
|
18
|
+
},
|
|
19
|
+
discover(descriptor, options) {
|
|
20
|
+
const { endpointUrl, endpoint, typeName } = requireWfsLocator(descriptor);
|
|
21
|
+
const root = client.wfs(endpointUrl);
|
|
22
|
+
const adapter = root.featureType(typeName);
|
|
23
|
+
const state = {
|
|
24
|
+
sourceId: descriptor.id,
|
|
25
|
+
endpoint,
|
|
26
|
+
authorityFingerprint: queryIrAuthorityFingerprint(endpointUrl, options?.authorizationScope),
|
|
27
|
+
typeName,
|
|
28
|
+
adapter,
|
|
29
|
+
descriptor,
|
|
30
|
+
lifecycleController: new AbortController(),
|
|
31
|
+
disposed: false,
|
|
32
|
+
};
|
|
33
|
+
const handle = Object.freeze({
|
|
34
|
+
descriptor,
|
|
35
|
+
capabilities: descriptor.capabilities ?? PROTOCOL_DEFAULT_CAPABILITIES.wfs,
|
|
36
|
+
adapter,
|
|
37
|
+
diagnostics: Object.freeze([]),
|
|
38
|
+
dispose() {
|
|
39
|
+
if (state.disposed)
|
|
40
|
+
return;
|
|
41
|
+
state.disposed = true;
|
|
42
|
+
state.evidence = undefined;
|
|
43
|
+
state.lifecycleController.abort();
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
handles.set(handle, state);
|
|
47
|
+
return handle;
|
|
48
|
+
},
|
|
49
|
+
compile: wfsProtocolQueryCompiler.compile,
|
|
50
|
+
async execute(handle, input) {
|
|
51
|
+
const operation = input.operation;
|
|
52
|
+
const state = handles.get(handle);
|
|
53
|
+
if (!state) {
|
|
54
|
+
throw new TypeError("wfs: protocol handle was not discovered by this module instance");
|
|
55
|
+
}
|
|
56
|
+
if (state.disposed) {
|
|
57
|
+
throw new TypeError("wfs: protocol handle has been disposed");
|
|
58
|
+
}
|
|
59
|
+
const signal = input.signal
|
|
60
|
+
? AbortSignal.any([input.signal, state.lifecycleController.signal])
|
|
61
|
+
: state.lifecycleController.signal;
|
|
62
|
+
const compiled = executableWfsArtifactSnapshot(state, input.compiled, operation);
|
|
63
|
+
if (operation === "queryAggregate") {
|
|
64
|
+
throw new HonuaCapabilityNotSupportedError("queryAggregate", handle.descriptor.protocol, handle.descriptor.id);
|
|
65
|
+
}
|
|
66
|
+
if (operation === "query" && compiled.count === 0) {
|
|
67
|
+
return { features: [], exceededTransferLimit: false };
|
|
68
|
+
}
|
|
69
|
+
const logicalLimit = operation === "queryAll" ? resolveWfsQueryAllLogicalLimit(compiled, input.query.logicalLimit) : undefined;
|
|
70
|
+
const evidence = await resolveWfsExecutionEvidence(state, compiled, signal);
|
|
71
|
+
if (operation === "query") {
|
|
72
|
+
return executeWfsPage(state.adapter, compiled, evidence, signal);
|
|
73
|
+
}
|
|
74
|
+
const target = typeof logicalLimit === "number" ? logicalLimit + 1 : Number.POSITIVE_INFINITY;
|
|
75
|
+
const pageSize = typeof logicalLimit === "number" ? Math.max(1, Math.min(logicalLimit + 1, 2000)) : 2000;
|
|
76
|
+
const collected = [];
|
|
77
|
+
const seenPages = new Set();
|
|
78
|
+
let totalCount;
|
|
79
|
+
let offset = compiled.startIndex ?? 0;
|
|
80
|
+
while (collected.length < target) {
|
|
81
|
+
const requestedPageSize = Number.isFinite(target) ? Math.min(pageSize, target - collected.length) : pageSize;
|
|
82
|
+
const page = await executeWfsPage(state.adapter, compiled, evidence, signal, {
|
|
83
|
+
count: requestedPageSize,
|
|
84
|
+
startIndex: offset,
|
|
85
|
+
});
|
|
86
|
+
totalCount = consistentWfsTotalCount(totalCount, page.totalCount);
|
|
87
|
+
const pageEnd = checkedWfsPageEnd(offset, page.features.length);
|
|
88
|
+
if (totalCount !== undefined && pageEnd > totalCount) {
|
|
89
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature returned more rows than numberMatched permits");
|
|
90
|
+
}
|
|
91
|
+
if (page.features.length === 0) {
|
|
92
|
+
if (totalCount !== undefined && offset < totalCount) {
|
|
93
|
+
throw new HonuaWfsProtocolError("paging-stalled", `WFS GetFeature returned no rows at startIndex ${offset} while numberMatched=${totalCount}`);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
assertWfsPageProgress(page, seenPages, offset);
|
|
98
|
+
collected.push(...page.features);
|
|
99
|
+
offset = pageEnd;
|
|
100
|
+
if (totalCount !== undefined && offset >= totalCount)
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const { features, exceededTransferLimit } = applyLimit(collected, logicalLimit);
|
|
104
|
+
const returnedEnd = checkedWfsPageEnd(compiled.startIndex ?? 0, features.length);
|
|
105
|
+
return {
|
|
106
|
+
features,
|
|
107
|
+
exceededTransferLimit: exceededTransferLimit ||
|
|
108
|
+
(totalCount !== undefined && typeof logicalLimit === "number" && totalCount > returnedEnd),
|
|
109
|
+
...(totalCount !== undefined ? { totalCount } : {}),
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function requireWfsLocator(descriptor) {
|
|
115
|
+
if (descriptor.protocol !== "wfs") {
|
|
116
|
+
throw new TypeError(`wfs: cannot discover protocol "${descriptor.protocol}"`);
|
|
117
|
+
}
|
|
118
|
+
const { url, typeName } = descriptor.locator;
|
|
119
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
120
|
+
throw new Error(`createDataset: source "${descriptor.id}" (wfs) requires locator.url`);
|
|
121
|
+
}
|
|
122
|
+
if (typeof typeName !== "string" || typeName.length === 0) {
|
|
123
|
+
throw new Error(`createDataset: source "${descriptor.id}" (wfs) requires locator.typeName`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
endpointUrl: url,
|
|
127
|
+
endpoint: wfsRuntimeEndpointIdentity(url),
|
|
128
|
+
typeName,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const WFS_PROTOCOL_ARTIFACT_KEYS = new Set([
|
|
132
|
+
"compiler",
|
|
133
|
+
"operation",
|
|
134
|
+
"sourceId",
|
|
135
|
+
"endpoint",
|
|
136
|
+
"authorityFingerprint",
|
|
137
|
+
"typeName",
|
|
138
|
+
"method",
|
|
139
|
+
"filter",
|
|
140
|
+
"bbox",
|
|
141
|
+
"propertyName",
|
|
142
|
+
"sortBy",
|
|
143
|
+
"startIndex",
|
|
144
|
+
"count",
|
|
145
|
+
"filterSrsName",
|
|
146
|
+
"srsName",
|
|
147
|
+
]);
|
|
148
|
+
function executableWfsArtifactSnapshot(state, value, operation) {
|
|
149
|
+
if (operation !== "query" && operation !== "queryAll" && operation !== "queryAggregate") {
|
|
150
|
+
throw new TypeError("wfs: requested protocol operation is invalid");
|
|
151
|
+
}
|
|
152
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
153
|
+
throw new TypeError("wfs: compiled GetFeature artifact is invalid");
|
|
154
|
+
}
|
|
155
|
+
if (Object.keys(value).some((key) => !WFS_PROTOCOL_ARTIFACT_KEYS.has(key))) {
|
|
156
|
+
throw new TypeError("wfs: compiled GetFeature artifact contains unsupported fields");
|
|
157
|
+
}
|
|
158
|
+
const compiler = value.compiler;
|
|
159
|
+
const compiledOperation = value.operation;
|
|
160
|
+
const sourceId = value.sourceId;
|
|
161
|
+
const endpoint = value.endpoint;
|
|
162
|
+
const authorityFingerprint = value.authorityFingerprint;
|
|
163
|
+
const typeName = value.typeName;
|
|
164
|
+
const method = value.method;
|
|
165
|
+
const filter = value.filter;
|
|
166
|
+
const bbox = value.bbox;
|
|
167
|
+
const propertyName = value.propertyName;
|
|
168
|
+
const sortBy = value.sortBy;
|
|
169
|
+
const startIndex = value.startIndex;
|
|
170
|
+
const count = value.count;
|
|
171
|
+
const filterSrsName = value.filterSrsName;
|
|
172
|
+
const srsName = value.srsName;
|
|
173
|
+
if (propertyName !== undefined &&
|
|
174
|
+
(!Array.isArray(propertyName) ||
|
|
175
|
+
propertyName.length === 0 ||
|
|
176
|
+
propertyName.some((name) => typeof name !== "string" || name.length === 0))) {
|
|
177
|
+
throw new TypeError("wfs: compiled propertyName is invalid");
|
|
178
|
+
}
|
|
179
|
+
const compiled = Object.freeze({
|
|
180
|
+
compiler,
|
|
181
|
+
operation: compiledOperation,
|
|
182
|
+
sourceId,
|
|
183
|
+
endpoint,
|
|
184
|
+
authorityFingerprint,
|
|
185
|
+
typeName,
|
|
186
|
+
method,
|
|
187
|
+
...(filter !== undefined ? { filter } : {}),
|
|
188
|
+
...(bbox !== undefined ? { bbox } : {}),
|
|
189
|
+
...(propertyName !== undefined ? { propertyName: Object.freeze([...propertyName]) } : {}),
|
|
190
|
+
...(sortBy !== undefined ? { sortBy } : {}),
|
|
191
|
+
...(startIndex !== undefined ? { startIndex } : {}),
|
|
192
|
+
...(count !== undefined ? { count } : {}),
|
|
193
|
+
...(filterSrsName !== undefined ? { filterSrsName } : {}),
|
|
194
|
+
...(srsName !== undefined ? { srsName } : {}),
|
|
195
|
+
});
|
|
196
|
+
if (compiled.compiler !== "wfs-2.0-protocol-query-v1") {
|
|
197
|
+
throw new TypeError("wfs: protocol module requires a wfs-2.0-protocol-query-v1 artifact");
|
|
198
|
+
}
|
|
199
|
+
if (compiled.operation !== operation) {
|
|
200
|
+
throw new TypeError("wfs: compiled operation does not match the requested protocol operation");
|
|
201
|
+
}
|
|
202
|
+
if (typeof compiled.sourceId !== "string" || compiled.sourceId.length === 0 || compiled.sourceId !== state.sourceId) {
|
|
203
|
+
throw new TypeError("wfs: compiled source id does not match the discovered protocol handle");
|
|
204
|
+
}
|
|
205
|
+
if (typeof compiled.authorityFingerprint !== "string" ||
|
|
206
|
+
!/^sha256:[0-9a-f]{64}$/.test(compiled.authorityFingerprint) ||
|
|
207
|
+
compiled.authorityFingerprint !== state.authorityFingerprint) {
|
|
208
|
+
throw new TypeError("wfs: compiled authority does not match the discovered protocol handle");
|
|
209
|
+
}
|
|
210
|
+
let normalizedEndpoint;
|
|
211
|
+
try {
|
|
212
|
+
normalizedEndpoint = wfsRuntimeEndpointIdentity(compiled.endpoint);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
throw new TypeError("wfs: compiled endpoint identity is invalid");
|
|
216
|
+
}
|
|
217
|
+
if (normalizedEndpoint !== compiled.endpoint ||
|
|
218
|
+
normalizedEndpoint !== state.endpoint ||
|
|
219
|
+
compiled.typeName !== state.typeName) {
|
|
220
|
+
throw new TypeError("wfs: compiled identity does not match the discovered protocol handle");
|
|
221
|
+
}
|
|
222
|
+
if (compiled.method !== "GET" && compiled.method !== "POST") {
|
|
223
|
+
throw new TypeError("wfs: compiled GetFeature method is invalid");
|
|
224
|
+
}
|
|
225
|
+
assertOptionalNonEmptyString(compiled.filter, "filter");
|
|
226
|
+
assertOptionalNonEmptyString(compiled.bbox, "bbox");
|
|
227
|
+
assertOptionalNonEmptyString(compiled.sortBy, "sortBy");
|
|
228
|
+
assertOptionalNonEmptyString(compiled.filterSrsName, "filterSrsName");
|
|
229
|
+
assertOptionalNonEmptyString(compiled.srsName, "srsName");
|
|
230
|
+
if (compiled.filter !== undefined && compiled.bbox !== undefined) {
|
|
231
|
+
throw new TypeError("wfs: compiled GetFeature request cannot contain both filter and bbox");
|
|
232
|
+
}
|
|
233
|
+
const expectedMethod = wfsProtocolMethodForFilter(compiled.filter);
|
|
234
|
+
if (compiled.method !== expectedMethod) {
|
|
235
|
+
throw new TypeError("wfs: compiled GetFeature method does not match the filter budget");
|
|
236
|
+
}
|
|
237
|
+
if (compiled.method === "POST" && compiled.filter === undefined) {
|
|
238
|
+
throw new TypeError("wfs: compiled POST GetFeature request requires a filter");
|
|
239
|
+
}
|
|
240
|
+
assertCompiledSpatialCrsBinding(compiled);
|
|
241
|
+
assertOptionalNonNegativeInteger(compiled.startIndex, "startIndex");
|
|
242
|
+
assertOptionalNonNegativeInteger(compiled.count, "count");
|
|
243
|
+
return compiled;
|
|
244
|
+
}
|
|
245
|
+
function assertOptionalNonNegativeInteger(value, name) {
|
|
246
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
247
|
+
throw new TypeError(`wfs: compiled ${name} must be a non-negative safe integer`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function assertOptionalNonEmptyString(value, name) {
|
|
251
|
+
if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
|
|
252
|
+
throw new TypeError(`wfs: compiled ${name} must be a non-empty string`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function assertCompiledSpatialCrsBinding(compiled) {
|
|
256
|
+
if (compiled.bbox !== undefined) {
|
|
257
|
+
const suffix = compiled.filterSrsName ? `,${compiled.filterSrsName}` : "";
|
|
258
|
+
if (suffix && !compiled.bbox.endsWith(suffix)) {
|
|
259
|
+
throw new TypeError("wfs: compiled bbox does not match the filter geometry CRS");
|
|
260
|
+
}
|
|
261
|
+
const coordinates = (suffix ? compiled.bbox.slice(0, -suffix.length) : compiled.bbox).split(",");
|
|
262
|
+
if (coordinates.length !== 4 || coordinates.some((value) => !Number.isFinite(Number(value)))) {
|
|
263
|
+
throw new TypeError("wfs: compiled bbox is invalid");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (compiled.filter !== undefined) {
|
|
267
|
+
const advertised = [...compiled.filter.matchAll(/\ssrsName="([^"]*)"/g)].map((match) => match[1]);
|
|
268
|
+
const filterSrsName = compiled.filterSrsName;
|
|
269
|
+
if (filterSrsName === undefined && advertised.length > 0) {
|
|
270
|
+
throw new TypeError("wfs: compiled filter carries an unbound geometry CRS");
|
|
271
|
+
}
|
|
272
|
+
if (filterSrsName !== undefined &&
|
|
273
|
+
(advertised.length === 0 || advertised.some((value) => value !== escapeXmlAttr(filterSrsName)))) {
|
|
274
|
+
throw new TypeError("wfs: compiled filter does not match the filter geometry CRS");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else if (compiled.filterSrsName !== undefined && compiled.bbox === undefined) {
|
|
278
|
+
throw new TypeError("wfs: compiled filter geometry CRS has no spatial predicate");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function resolveWfsExecutionEvidence(state, compiled, signal) {
|
|
282
|
+
if (signal?.aborted)
|
|
283
|
+
throw new HonuaAbortError();
|
|
284
|
+
let snapshot;
|
|
285
|
+
try {
|
|
286
|
+
snapshot = await state.adapter.root.capabilities(signal ? { signal } : undefined);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (isHonuaSdkError(error))
|
|
290
|
+
throw error;
|
|
291
|
+
throw invalidCapabilities("WFS GetCapabilities returned a malformed or invalid document", error);
|
|
292
|
+
}
|
|
293
|
+
if (signal?.aborted)
|
|
294
|
+
throw new HonuaAbortError();
|
|
295
|
+
if (state.disposed)
|
|
296
|
+
throw new TypeError("wfs: protocol handle has been disposed");
|
|
297
|
+
const cached = state.evidence;
|
|
298
|
+
const evidence = cached?.snapshot === snapshot ? cached.value : validateWfsCapabilities(state, snapshot);
|
|
299
|
+
if (cached?.snapshot !== snapshot) {
|
|
300
|
+
state.evidence = { snapshot, value: evidence };
|
|
301
|
+
}
|
|
302
|
+
if (!evidence.methods.includes(compiled.method)) {
|
|
303
|
+
throw new HonuaCapabilityNotSupportedError("query", state.descriptor.protocol, `${state.descriptor.id} (GetFeature does not advertise ${compiled.method})`);
|
|
304
|
+
}
|
|
305
|
+
assertAdvertisedCrs(evidence, compiled.filterSrsName, "filter geometry");
|
|
306
|
+
assertAdvertisedCrs(evidence, compiled.srsName, "response");
|
|
307
|
+
return evidence;
|
|
308
|
+
}
|
|
309
|
+
function validateWfsCapabilities(state, snapshot) {
|
|
310
|
+
if (snapshot.version !== "2.0.0") {
|
|
311
|
+
throw invalidCapabilities(`WFS GetCapabilities must advertise exact version 2.0.0, got ${snapshot.version ?? "none"}`);
|
|
312
|
+
}
|
|
313
|
+
const operation = snapshot.operations.get("GetFeature");
|
|
314
|
+
if (!operation ||
|
|
315
|
+
!Array.isArray(operation.methods) ||
|
|
316
|
+
operation.methods.length === 0 ||
|
|
317
|
+
operation.methods.some((method) => method !== "GET" && method !== "POST")) {
|
|
318
|
+
throw invalidCapabilities("WFS GetCapabilities does not advertise a GetFeature HTTP binding");
|
|
319
|
+
}
|
|
320
|
+
const methods = Object.freeze([...new Set(operation.methods)]);
|
|
321
|
+
for (const method of methods) {
|
|
322
|
+
assertSameOriginDcp(state, operation, method);
|
|
323
|
+
}
|
|
324
|
+
const featureType = snapshot.featureTypes.find((candidate) => candidate.name === state.typeName);
|
|
325
|
+
if (!featureType) {
|
|
326
|
+
throw invalidCapabilities(`WFS GetCapabilities does not advertise feature type "${state.typeName}"`);
|
|
327
|
+
}
|
|
328
|
+
if (!featureType.defaultCrs) {
|
|
329
|
+
throw invalidCapabilities(`WFS feature type "${state.typeName}" does not advertise DefaultCRS`);
|
|
330
|
+
}
|
|
331
|
+
const defaultCrs = canonicalWfsCrs(featureType.defaultCrs);
|
|
332
|
+
if (!defaultCrs) {
|
|
333
|
+
throw invalidCapabilities(`WFS feature type "${state.typeName}" advertises an unsupported DefaultCRS`);
|
|
334
|
+
}
|
|
335
|
+
const supportedCrs = Object.freeze([
|
|
336
|
+
...new Set([defaultCrs, ...(featureType.otherCrs ?? []).map((value) => canonicalWfsCrs(value))].filter((value) => value !== undefined)),
|
|
337
|
+
]);
|
|
338
|
+
const namespace = namespaceBinding(state, snapshot, featureType);
|
|
339
|
+
const advertisedFormats = snapshot.outputFormatsByOp.get("GetFeature") ?? [];
|
|
340
|
+
if (!Array.isArray(advertisedFormats) ||
|
|
341
|
+
advertisedFormats.some((value) => typeof value !== "string" || value.length === 0)) {
|
|
342
|
+
throw invalidCapabilities("WFS GetFeature does not advertise an outputFormat");
|
|
343
|
+
}
|
|
344
|
+
const negotiatedFormat = state.adapter.root.negotiateOutputFormat(snapshot);
|
|
345
|
+
if (!negotiatedFormat ||
|
|
346
|
+
negotiatedFormat.kind !== "json" ||
|
|
347
|
+
(advertisedFormats.length > 0 && !advertisedFormats.includes(negotiatedFormat.format))) {
|
|
348
|
+
throw new HonuaCapabilityNotSupportedError("query", state.descriptor.protocol, `${state.descriptor.id} (GetFeature does not advertise a recognized GeoJSON output format)`);
|
|
349
|
+
}
|
|
350
|
+
const format = Object.freeze({ kind: "json", format: negotiatedFormat.format });
|
|
351
|
+
return Object.freeze({
|
|
352
|
+
methods,
|
|
353
|
+
featureTypeName: featureType.name,
|
|
354
|
+
format,
|
|
355
|
+
...(namespace ? { namespace } : {}),
|
|
356
|
+
supportedCrs,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function namespaceBinding(state, snapshot, featureType) {
|
|
360
|
+
const separator = state.typeName.indexOf(":");
|
|
361
|
+
if (separator < 0)
|
|
362
|
+
return undefined;
|
|
363
|
+
const prefix = state.typeName.slice(0, separator);
|
|
364
|
+
const elementBinding = featureType.namespace;
|
|
365
|
+
const uri = elementBinding === undefined
|
|
366
|
+
? snapshot.namespaces.get(prefix)
|
|
367
|
+
: elementBinding.prefix === prefix
|
|
368
|
+
? elementBinding.uri
|
|
369
|
+
: undefined;
|
|
370
|
+
if (!uri) {
|
|
371
|
+
throw invalidCapabilities(`WFS GetCapabilities does not bind namespace prefix "${prefix}" for feature type "${state.typeName}"`);
|
|
372
|
+
}
|
|
373
|
+
const descriptorNamespace = state.descriptor.locator.featureNamespace;
|
|
374
|
+
if (descriptorNamespace !== undefined && descriptorNamespace !== uri) {
|
|
375
|
+
throw invalidCapabilities(`WFS descriptor namespace for prefix "${prefix}" does not match GetCapabilities evidence`);
|
|
376
|
+
}
|
|
377
|
+
return Object.freeze({ prefix, uri });
|
|
378
|
+
}
|
|
379
|
+
function assertSameOriginDcp(state, operation, method) {
|
|
380
|
+
const root = state.adapter.root;
|
|
381
|
+
const configured = new URL(root.endpointUrl, root.client.serverBaseUrl);
|
|
382
|
+
const advertised = new URL(method === "GET" ? (operation.getUrl ?? root.endpointUrl) : (operation.postUrl ?? root.endpointUrl), root.client.serverBaseUrl);
|
|
383
|
+
if (advertised.username || advertised.password || advertised.origin !== configured.origin) {
|
|
384
|
+
throw invalidCapabilities(`WFS GetFeature ${method} DCP authority is not bound to the configured endpoint`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function assertAdvertisedCrs(evidence, value, role) {
|
|
388
|
+
if (value === undefined)
|
|
389
|
+
return;
|
|
390
|
+
const canonical = canonicalWfsCrs(value);
|
|
391
|
+
if (!canonical || !evidence.supportedCrs.includes(canonical)) {
|
|
392
|
+
throw new HonuaCapabilityNotSupportedError("query", "wfs", `${evidence.featureTypeName} (${role} CRS is not advertised)`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function canonicalWfsCrs(value) {
|
|
396
|
+
const trimmed = value.trim();
|
|
397
|
+
if (/^(?:OGC:)?CRS:?84$/i.test(trimmed))
|
|
398
|
+
return "ogc:crs84";
|
|
399
|
+
const urn = trimmed.match(/^urn:(?:ogc|x-ogc):def:crs:(EPSG|OGC):[^:]*:([^:\s]+)$/i);
|
|
400
|
+
const http = trimmed.match(/^https?:\/\/www\.opengis\.net\/def\/crs\/(EPSG|OGC)\/[^/]+\/([^/?#]+)\/?$/i);
|
|
401
|
+
const legacyEpsgHttp = trimmed.match(/^https?:\/\/www\.opengis\.net\/gml\/srs\/epsg\.xml#(\d+)$/i);
|
|
402
|
+
const short = trimmed.match(/^EPSG:(\d+)$/i);
|
|
403
|
+
const numeric = trimmed.match(/^(\d+)$/);
|
|
404
|
+
const shorthandCode = short?.[1] ?? numeric?.[1] ?? legacyEpsgHttp?.[1];
|
|
405
|
+
if (shorthandCode)
|
|
406
|
+
return canonicalEpsgCode(shorthandCode);
|
|
407
|
+
const authority = (urn?.[1] ?? http?.[1])?.toUpperCase();
|
|
408
|
+
const code = urn?.[2] ?? http?.[2];
|
|
409
|
+
if (authority === "EPSG" && code && /^\d+$/.test(code))
|
|
410
|
+
return canonicalEpsgCode(code);
|
|
411
|
+
if (authority === "OGC" && code && /^CRS:?84$/i.test(code))
|
|
412
|
+
return "ogc:crs84";
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
function canonicalEpsgCode(code) {
|
|
416
|
+
const numeric = Number(code);
|
|
417
|
+
return Number.isSafeInteger(numeric) && numeric > 0 ? `epsg:${numeric}` : undefined;
|
|
418
|
+
}
|
|
419
|
+
function invalidCapabilities(message, cause) {
|
|
420
|
+
return new HonuaWfsProtocolError("invalid-capabilities", message, cause === undefined ? {} : { cause });
|
|
421
|
+
}
|
|
422
|
+
async function executeWfsPage(adapter, compiled, evidence, signal, paging) {
|
|
423
|
+
const count = paging?.count ?? compiled.count;
|
|
424
|
+
const startIndex = paging?.startIndex ?? compiled.startIndex;
|
|
425
|
+
const params = {
|
|
426
|
+
method: compiled.method,
|
|
427
|
+
outputFormat: evidence.format.format,
|
|
428
|
+
...(evidence.namespace ? { namespace: evidence.namespace } : {}),
|
|
429
|
+
...(compiled.method === "GET" && compiled.filter !== undefined ? { filter: compiled.filter } : {}),
|
|
430
|
+
...(compiled.bbox !== undefined ? { bbox: compiled.bbox } : {}),
|
|
431
|
+
...(compiled.propertyName !== undefined ? { propertyName: [...compiled.propertyName] } : {}),
|
|
432
|
+
...(compiled.sortBy !== undefined ? { sortBy: compiled.sortBy } : {}),
|
|
433
|
+
...(count !== undefined ? { count } : {}),
|
|
434
|
+
...(startIndex !== undefined && startIndex > 0 ? { startIndex } : {}),
|
|
435
|
+
...(compiled.srsName !== undefined ? { srsName: compiled.srsName } : {}),
|
|
436
|
+
...(signal ? { signal } : {}),
|
|
437
|
+
};
|
|
438
|
+
if (compiled.method === "POST" && compiled.filter !== undefined) {
|
|
439
|
+
params.body = buildWfsPostGetFeatureBody({
|
|
440
|
+
typeName: compiled.typeName,
|
|
441
|
+
filter: compiled.filter,
|
|
442
|
+
count,
|
|
443
|
+
startIndex: startIndex !== undefined && startIndex > 0 ? startIndex : undefined,
|
|
444
|
+
outputFormat: evidence.format.format,
|
|
445
|
+
...(evidence.namespace ? { namespace: evidence.namespace } : {}),
|
|
446
|
+
...(compiled.propertyName !== undefined ? { propertyNames: compiled.propertyName } : {}),
|
|
447
|
+
...(compiled.sortBy !== undefined ? { sortBy: compiled.sortBy } : {}),
|
|
448
|
+
...(compiled.srsName !== undefined ? { srsName: compiled.srsName } : {}),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
let response;
|
|
452
|
+
try {
|
|
453
|
+
response = await adapter.getFeature(params);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (error instanceof Error &&
|
|
457
|
+
error.message.startsWith("WFS GetFeature returned non-JSON output despite Accept negotiation:")) {
|
|
458
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature returned malformed JSON despite GeoJSON negotiation");
|
|
459
|
+
}
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
if (response.kind !== "json") {
|
|
463
|
+
throw new HonuaCapabilityNotSupportedError("query", "wfs", `WFS GetFeature returned ${response.contentType}; canonical surface only carries JSON. Reach raw output through Source.protocol("wfs")`);
|
|
464
|
+
}
|
|
465
|
+
const result = wfsProtocolResultFromGeoJson(response.data);
|
|
466
|
+
if (count !== undefined && result.features.length > count) {
|
|
467
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature returned more rows than the requested count permits");
|
|
468
|
+
}
|
|
469
|
+
const pageEnd = checkedWfsPageEnd(startIndex ?? 0, result.features.length);
|
|
470
|
+
if (result.totalCount !== undefined && pageEnd > result.totalCount) {
|
|
471
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature returned more rows than numberMatched permits");
|
|
472
|
+
}
|
|
473
|
+
if (result.totalCount === undefined)
|
|
474
|
+
return result;
|
|
475
|
+
return copyWfsPageFingerprint(result, {
|
|
476
|
+
...result,
|
|
477
|
+
exceededTransferLimit: pageEnd < result.totalCount,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function resolveWfsQueryAllLogicalLimit(compiled, requestedLimit) {
|
|
481
|
+
if (requestedLimit !== undefined && (!Number.isSafeInteger(requestedLimit) || requestedLimit < 0)) {
|
|
482
|
+
throw new TypeError("wfs: queryAll logical limit must be a non-negative safe integer");
|
|
483
|
+
}
|
|
484
|
+
if (compiled.count === undefined) {
|
|
485
|
+
if (requestedLimit !== undefined) {
|
|
486
|
+
throw new TypeError("wfs: queryAll compiled count does not match the logical limit");
|
|
487
|
+
}
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
if (!Number.isSafeInteger(compiled.count) || compiled.count < 1) {
|
|
491
|
+
throw new TypeError("wfs: queryAll compiled count must contain one lookahead row");
|
|
492
|
+
}
|
|
493
|
+
const compiledLogicalLimit = compiled.count - 1;
|
|
494
|
+
if (requestedLimit !== undefined && requestedLimit !== compiledLogicalLimit) {
|
|
495
|
+
throw new TypeError("wfs: queryAll compiled count does not match the logical limit");
|
|
496
|
+
}
|
|
497
|
+
return requestedLimit ?? compiledLogicalLimit;
|
|
498
|
+
}
|
|
499
|
+
/** Project one GeoJSON FeatureCollection into the protocol-neutral Result envelope. */
|
|
500
|
+
export function wfsProtocolResultFromGeoJson(data) {
|
|
501
|
+
if (!isPlainRecord(data) || data.type !== "FeatureCollection" || !Array.isArray(data.features)) {
|
|
502
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature did not return a GeoJSON FeatureCollection with a features array");
|
|
503
|
+
}
|
|
504
|
+
const features = data.features.map((feature, index) => {
|
|
505
|
+
if (!isPlainRecord(feature) ||
|
|
506
|
+
feature.type !== "Feature" ||
|
|
507
|
+
(feature.properties !== null && !isPlainRecord(feature.properties)) ||
|
|
508
|
+
(feature.geometry !== null && !isGeoJsonGeometry(feature.geometry))) {
|
|
509
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", `WFS GetFeature returned an invalid GeoJSON feature at index ${index}`);
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
attributes: (feature.properties ?? {}),
|
|
513
|
+
geometry: feature.geometry,
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
const totalCount = parseGeoJsonCount(data.numberMatched, "numberMatched");
|
|
517
|
+
const numberReturned = parseGeoJsonCount(data.numberReturned, "numberReturned");
|
|
518
|
+
if (numberReturned !== undefined && numberReturned !== features.length) {
|
|
519
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature numberReturned does not match the GeoJSON features array");
|
|
520
|
+
}
|
|
521
|
+
let pageFingerprint;
|
|
522
|
+
try {
|
|
523
|
+
pageFingerprint = sha256(canonicalStringify(toJsonValue(data.features)));
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature features must contain only finite, acyclic JSON values", { cause: error });
|
|
527
|
+
}
|
|
528
|
+
const result = {
|
|
529
|
+
features,
|
|
530
|
+
exceededTransferLimit: totalCount !== undefined && features.length < totalCount,
|
|
531
|
+
...(totalCount !== undefined ? { totalCount } : {}),
|
|
532
|
+
};
|
|
533
|
+
WFS_PAGE_FINGERPRINTS.set(result, pageFingerprint);
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
function parseGeoJsonCount(value, name) {
|
|
537
|
+
if (value === undefined || value === "unknown")
|
|
538
|
+
return undefined;
|
|
539
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
540
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", `WFS GetFeature ${name} must be a non-negative safe integer or "unknown"`);
|
|
541
|
+
}
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
function isPlainRecord(value) {
|
|
545
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
546
|
+
return false;
|
|
547
|
+
const prototype = Object.getPrototypeOf(value);
|
|
548
|
+
return prototype === Object.prototype || prototype === null;
|
|
549
|
+
}
|
|
550
|
+
function isGeoJsonGeometry(value) {
|
|
551
|
+
return validateGeoJsonGeometry(value, 0, {
|
|
552
|
+
geometryObjects: 0,
|
|
553
|
+
coordinateContainers: 0,
|
|
554
|
+
positions: 0,
|
|
555
|
+
coordinateValues: 0,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
const WFS_GEOJSON_MAX_GEOMETRY_DEPTH = 32;
|
|
559
|
+
const WFS_GEOJSON_MAX_GEOMETRY_OBJECTS = 12_000;
|
|
560
|
+
const WFS_GEOJSON_MAX_COORDINATE_CONTAINERS = 1_100_000;
|
|
561
|
+
const WFS_GEOJSON_MAX_POSITIONS = 1_000_000;
|
|
562
|
+
const WFS_GEOJSON_MAX_COORDINATE_VALUES = 8_000_000;
|
|
563
|
+
const WFS_GEOJSON_MAX_POSITION_DIMENSIONS = 8;
|
|
564
|
+
function validateGeoJsonGeometry(value, depth, budget) {
|
|
565
|
+
if (depth > WFS_GEOJSON_MAX_GEOMETRY_DEPTH ||
|
|
566
|
+
!isPlainRecord(value) ||
|
|
567
|
+
++budget.geometryObjects > WFS_GEOJSON_MAX_GEOMETRY_OBJECTS) {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
switch (value.type) {
|
|
571
|
+
case "Point":
|
|
572
|
+
return isEmptyCoordinates(value.coordinates, budget) || isGeoJsonPosition(value.coordinates, budget);
|
|
573
|
+
case "MultiPoint":
|
|
574
|
+
return isGeoJsonPositionArray(value.coordinates, budget, 0);
|
|
575
|
+
case "LineString":
|
|
576
|
+
return isGeoJsonPositionArray(value.coordinates, budget, 2);
|
|
577
|
+
case "MultiLineString":
|
|
578
|
+
return isGeoJsonLineArray(value.coordinates, budget);
|
|
579
|
+
case "Polygon":
|
|
580
|
+
return isGeoJsonPolygonCoordinates(value.coordinates, budget);
|
|
581
|
+
case "MultiPolygon":
|
|
582
|
+
return isGeoJsonMultiPolygonCoordinates(value.coordinates, budget);
|
|
583
|
+
case "GeometryCollection":
|
|
584
|
+
if (!Array.isArray(value.geometries) || !consumeCoordinateContainer(budget))
|
|
585
|
+
return false;
|
|
586
|
+
return value.geometries.every((geometry) => validateGeoJsonGeometry(geometry, depth + 1, budget));
|
|
587
|
+
default:
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function isEmptyCoordinates(value, budget) {
|
|
592
|
+
return Array.isArray(value) && value.length === 0 && consumeCoordinateContainer(budget);
|
|
593
|
+
}
|
|
594
|
+
function isGeoJsonPosition(value, budget) {
|
|
595
|
+
if (!Array.isArray(value) ||
|
|
596
|
+
value.length < 2 ||
|
|
597
|
+
value.length > WFS_GEOJSON_MAX_POSITION_DIMENSIONS ||
|
|
598
|
+
!consumeCoordinateContainer(budget) ||
|
|
599
|
+
++budget.positions > WFS_GEOJSON_MAX_POSITIONS ||
|
|
600
|
+
budget.coordinateValues + value.length > WFS_GEOJSON_MAX_COORDINATE_VALUES) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
budget.coordinateValues += value.length;
|
|
604
|
+
return value.every((coordinate) => typeof coordinate === "number" && Number.isFinite(coordinate));
|
|
605
|
+
}
|
|
606
|
+
function isGeoJsonPositionArray(value, budget, minimumLength) {
|
|
607
|
+
if (!Array.isArray(value) || !consumeCoordinateContainer(budget) || value.length > WFS_GEOJSON_MAX_POSITIONS) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
if (value.length === 0)
|
|
611
|
+
return true;
|
|
612
|
+
return value.length >= minimumLength && value.every((position) => isGeoJsonPosition(position, budget));
|
|
613
|
+
}
|
|
614
|
+
function isGeoJsonLineArray(value, budget) {
|
|
615
|
+
if (!Array.isArray(value) || !consumeCoordinateContainer(budget))
|
|
616
|
+
return false;
|
|
617
|
+
return value.every((line) => isGeoJsonPositionArray(line, budget, 2));
|
|
618
|
+
}
|
|
619
|
+
function isGeoJsonLinearRing(value, budget) {
|
|
620
|
+
if (!Array.isArray(value) || value.length < 4 || !consumeCoordinateContainer(budget))
|
|
621
|
+
return false;
|
|
622
|
+
const first = value[0];
|
|
623
|
+
const last = value[value.length - 1];
|
|
624
|
+
if (!Array.isArray(first) ||
|
|
625
|
+
!Array.isArray(last) ||
|
|
626
|
+
first.length !== last.length ||
|
|
627
|
+
!first.every((coordinate, index) => coordinate === last[index])) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
return value.every((position) => isGeoJsonPosition(position, budget));
|
|
631
|
+
}
|
|
632
|
+
function isGeoJsonPolygonCoordinates(value, budget) {
|
|
633
|
+
if (!Array.isArray(value) || !consumeCoordinateContainer(budget))
|
|
634
|
+
return false;
|
|
635
|
+
if (value.length === 0)
|
|
636
|
+
return true;
|
|
637
|
+
return value.every((ring) => isGeoJsonLinearRing(ring, budget));
|
|
638
|
+
}
|
|
639
|
+
function isGeoJsonMultiPolygonCoordinates(value, budget) {
|
|
640
|
+
if (!Array.isArray(value) || !consumeCoordinateContainer(budget))
|
|
641
|
+
return false;
|
|
642
|
+
return value.every((polygon) => isGeoJsonPolygonCoordinates(polygon, budget));
|
|
643
|
+
}
|
|
644
|
+
function consumeCoordinateContainer(budget) {
|
|
645
|
+
budget.coordinateContainers += 1;
|
|
646
|
+
return budget.coordinateContainers <= WFS_GEOJSON_MAX_COORDINATE_CONTAINERS;
|
|
647
|
+
}
|
|
648
|
+
function copyWfsPageFingerprint(source, target) {
|
|
649
|
+
const fingerprint = WFS_PAGE_FINGERPRINTS.get(source);
|
|
650
|
+
if (fingerprint !== undefined)
|
|
651
|
+
WFS_PAGE_FINGERPRINTS.set(target, fingerprint);
|
|
652
|
+
return target;
|
|
653
|
+
}
|
|
654
|
+
/** @internal Fail closed when a WFS server repeats a page instead of advancing. */
|
|
655
|
+
export function assertWfsPageProgress(result, seenPages, startIndex) {
|
|
656
|
+
const fingerprint = WFS_PAGE_FINGERPRINTS.get(result);
|
|
657
|
+
if (fingerprint === undefined) {
|
|
658
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature page identity was not retained by the protocol module");
|
|
659
|
+
}
|
|
660
|
+
if (seenPages.has(fingerprint)) {
|
|
661
|
+
throw new HonuaWfsProtocolError("paging-stalled", `WFS GetFeature repeated a prior page at startIndex ${startIndex}`);
|
|
662
|
+
}
|
|
663
|
+
seenPages.add(fingerprint);
|
|
664
|
+
}
|
|
665
|
+
function consistentWfsTotalCount(current, next) {
|
|
666
|
+
if (next === undefined)
|
|
667
|
+
return current;
|
|
668
|
+
if (current !== undefined && current !== next) {
|
|
669
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature numberMatched changed while paging one query");
|
|
670
|
+
}
|
|
671
|
+
return next;
|
|
672
|
+
}
|
|
673
|
+
/** @internal Reject paging arithmetic that cannot remain exact in JavaScript. */
|
|
674
|
+
export function checkedWfsPageEnd(startIndex, returnedRows) {
|
|
675
|
+
if (!Number.isSafeInteger(startIndex) ||
|
|
676
|
+
startIndex < 0 ||
|
|
677
|
+
!Number.isSafeInteger(returnedRows) ||
|
|
678
|
+
returnedRows < 0 ||
|
|
679
|
+
!Number.isSafeInteger(startIndex + returnedRows)) {
|
|
680
|
+
throw new HonuaWfsProtocolError("invalid-feature-response", "WFS GetFeature paging exceeded the safe-integer range");
|
|
681
|
+
}
|
|
682
|
+
return startIndex + returnedRows;
|
|
683
|
+
}
|
|
684
|
+
function applyLimit(features, limit) {
|
|
685
|
+
if (typeof limit !== "number" || limit < 0) {
|
|
686
|
+
return { features: [...features], exceededTransferLimit: false };
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
features: features.slice(0, limit),
|
|
690
|
+
exceededTransferLimit: features.length > limit,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
/** @internal Shared by the module executor and legacy non-query WFS drains. */
|
|
694
|
+
export function buildWfsPostGetFeatureBody(options) {
|
|
695
|
+
const countAttr = typeof options.count === "number" ? ` count="${options.count}"` : "";
|
|
696
|
+
const startAttr = typeof options.startIndex === "number" ? ` startIndex="${options.startIndex}"` : "";
|
|
697
|
+
const outputAttr = ` outputFormat="${escapeXmlAttr(options.outputFormat)}"`;
|
|
698
|
+
const queryAttrs = options.srsName !== undefined ? ` srsName="${escapeXmlAttr(options.srsName)}"` : "";
|
|
699
|
+
if (options.namespace !== undefined && !/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(options.namespace.prefix)) {
|
|
700
|
+
throw new TypeError("wfs: POST namespace prefix is invalid");
|
|
701
|
+
}
|
|
702
|
+
const namespaceAttr = options.namespace !== undefined
|
|
703
|
+
? ` xmlns:${options.namespace.prefix}="${escapeXmlAttr(options.namespace.uri)}"`
|
|
704
|
+
: "";
|
|
705
|
+
const propertyXml = options.propertyNames && options.propertyNames.length > 0
|
|
706
|
+
? options.propertyNames.map((name) => `<wfs:PropertyName>${escapeXmlText(name)}</wfs:PropertyName>`).join("")
|
|
707
|
+
: "";
|
|
708
|
+
const sortByXml = options.sortBy !== undefined ? buildSortByXml(options.sortBy) : "";
|
|
709
|
+
return `<wfs:GetFeature xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0"${namespaceAttr} service="WFS" version="2.0.0"${outputAttr}${countAttr}${startAttr}><wfs:Query typeNames="${escapeXmlAttr(options.typeName)}"${queryAttrs}>${propertyXml}${options.filter}${sortByXml}</wfs:Query></wfs:GetFeature>`;
|
|
710
|
+
}
|
|
711
|
+
function buildSortByXml(sortBy) {
|
|
712
|
+
const entries = sortBy
|
|
713
|
+
.split(",")
|
|
714
|
+
.map((entry) => entry.trim())
|
|
715
|
+
.filter((entry) => entry.length > 0);
|
|
716
|
+
if (entries.length === 0)
|
|
717
|
+
return "";
|
|
718
|
+
const properties = entries.map((entry) => {
|
|
719
|
+
const parts = entry.split(/\s+/);
|
|
720
|
+
const field = parts[0] ?? "";
|
|
721
|
+
const direction = parts[1]?.toUpperCase() === "D" ? "DESC" : "ASC";
|
|
722
|
+
return `<fes:SortProperty><fes:ValueReference>${escapeXmlText(field)}</fes:ValueReference><fes:SortOrder>${direction}</fes:SortOrder></fes:SortProperty>`;
|
|
723
|
+
});
|
|
724
|
+
return `<fes:SortBy>${properties.join("")}</fes:SortBy>`;
|
|
725
|
+
}
|
|
726
|
+
function escapeXmlAttr(value) {
|
|
727
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
728
|
+
}
|
|
729
|
+
function escapeXmlText(value) {
|
|
730
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
731
|
+
}
|