@honua/mcp-server 0.1.2-beta.0 → 0.1.8-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/LICENSE +201 -0
- package/README.md +232 -9
- package/dist/src/certification/admin-parity.d.ts +96 -0
- package/dist/src/certification/admin-parity.d.ts.map +1 -0
- package/dist/src/certification/admin-parity.js +235 -0
- package/dist/src/certification/census-fixture-client.d.ts.map +1 -1
- package/dist/src/certification/census-fixture-client.js +5 -0
- package/dist/src/certification/certifier.d.ts +1 -1
- package/dist/src/certification/certifier.d.ts.map +1 -1
- package/dist/src/certification/certifier.js +28 -20
- package/dist/src/certification/cli.js +1 -1
- package/dist/src/certification/cql2.d.ts +24 -0
- package/dist/src/certification/cql2.d.ts.map +1 -0
- package/dist/src/certification/cql2.js +354 -0
- package/dist/src/certification/fixtures.d.ts +8 -0
- package/dist/src/certification/fixtures.d.ts.map +1 -1
- package/dist/src/certification/fixtures.js +26 -10
- package/dist/src/certification/ogc-data.d.ts +30 -0
- package/dist/src/certification/ogc-data.d.ts.map +1 -0
- package/dist/src/certification/ogc-data.js +726 -0
- package/dist/src/certification/ogc-fixture-client.d.ts +4 -0
- package/dist/src/certification/ogc-fixture-client.d.ts.map +1 -0
- package/dist/src/certification/ogc-fixture-client.js +190 -0
- package/dist/src/certification/target.d.ts +10 -1
- package/dist/src/certification/target.d.ts.map +1 -1
- package/dist/src/certification/target.js +63 -28
- package/dist/src/credential-endpoint.d.ts +3 -0
- package/dist/src/credential-endpoint.d.ts.map +1 -0
- package/dist/src/credential-endpoint.js +24 -0
- package/dist/src/eval/cli.js +6 -3
- package/dist/src/eval/corpus.d.ts +7 -0
- package/dist/src/eval/corpus.d.ts.map +1 -1
- package/dist/src/eval/corpus.js +12 -0
- package/dist/src/eval/ogc-corpus.d.ts +3 -0
- package/dist/src/eval/ogc-corpus.d.ts.map +1 -0
- package/dist/src/eval/ogc-corpus.js +437 -0
- package/dist/src/eval/runner.d.ts +6 -0
- package/dist/src/eval/runner.d.ts.map +1 -1
- package/dist/src/eval/runner.js +21 -7
- package/dist/src/helpers.d.ts +8 -13
- package/dist/src/helpers.d.ts.map +1 -1
- package/dist/src/helpers.js +7 -35
- package/dist/src/index.d.ts +36 -8
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +55 -18
- package/dist/src/neutral/errors.d.ts +73 -0
- package/dist/src/neutral/errors.d.ts.map +1 -0
- package/dist/src/neutral/errors.js +112 -0
- package/dist/src/neutral/filter.d.ts +108 -0
- package/dist/src/neutral/filter.d.ts.map +1 -0
- package/dist/src/neutral/filter.js +271 -0
- package/dist/src/neutral/query.d.ts +101 -0
- package/dist/src/neutral/query.d.ts.map +1 -0
- package/dist/src/neutral/query.js +180 -0
- package/dist/src/neutral/result.d.ts +33 -0
- package/dist/src/neutral/result.d.ts.map +1 -0
- package/dist/src/neutral/result.js +38 -0
- package/dist/src/neutral/source-ref.d.ts +126 -0
- package/dist/src/neutral/source-ref.d.ts.map +1 -0
- package/dist/src/neutral/source-ref.js +267 -0
- package/dist/src/nl-map-control.d.ts.map +1 -1
- package/dist/src/proxy.d.ts.map +1 -1
- package/dist/src/proxy.js +29 -14
- package/dist/src/release/zero-to-map-checkpoint.d.ts +34 -0
- package/dist/src/release/zero-to-map-checkpoint.d.ts.map +1 -0
- package/dist/src/release/zero-to-map-checkpoint.js +369 -0
- package/dist/src/release/zero-to-map-cli.d.ts +51 -0
- package/dist/src/release/zero-to-map-cli.d.ts.map +1 -0
- package/dist/src/release/zero-to-map-cli.js +880 -0
- package/dist/src/release/zero-to-map-provision.d.ts +37 -0
- package/dist/src/release/zero-to-map-provision.d.ts.map +1 -0
- package/dist/src/release/zero-to-map-provision.js +177 -0
- package/dist/src/release/zero-to-map.d.ts +296 -0
- package/dist/src/release/zero-to-map.d.ts.map +1 -0
- package/dist/src/release/zero-to-map.js +1069 -0
- package/dist/src/tools/admin-install-local.d.ts +30 -0
- package/dist/src/tools/admin-install-local.d.ts.map +1 -0
- package/dist/src/tools/admin-install-local.js +22 -0
- package/dist/src/tools/control-plane-command.d.ts +99 -0
- package/dist/src/tools/control-plane-command.d.ts.map +1 -0
- package/dist/src/tools/control-plane-command.js +180 -0
- package/dist/src/tools/count-features.d.ts +85 -11
- package/dist/src/tools/count-features.d.ts.map +1 -1
- package/dist/src/tools/count-features.js +49 -31
- package/dist/src/tools/describe-layer.d.ts +30 -6
- package/dist/src/tools/describe-layer.d.ts.map +1 -1
- package/dist/src/tools/describe-layer.js +99 -8
- package/dist/src/tools/docs-search.d.ts +100 -0
- package/dist/src/tools/docs-search.d.ts.map +1 -0
- package/dist/src/tools/docs-search.js +381 -0
- package/dist/src/tools/get-extent.d.ts +69 -11
- package/dist/src/tools/get-extent.d.ts.map +1 -1
- package/dist/src/tools/get-extent.js +28 -46
- package/dist/src/tools/list-services.d.ts +7 -0
- package/dist/src/tools/list-services.d.ts.map +1 -1
- package/dist/src/tools/list-services.js +8 -1
- package/dist/src/tools/list-sources.d.ts +44 -0
- package/dist/src/tools/list-sources.d.ts.map +1 -0
- package/dist/src/tools/list-sources.js +138 -0
- package/dist/src/tools/query-features.d.ts +99 -19
- package/dist/src/tools/query-features.d.ts.map +1 -1
- package/dist/src/tools/query-features.js +38 -45
- package/dist/src/tools/statistics.d.ts +80 -12
- package/dist/src/tools/statistics.d.ts.map +1 -1
- package/dist/src/tools/statistics.js +56 -23
- package/package.json +37 -8
- package/release/zero-to-map/README.md +174 -0
- package/release/zero-to-map/configs/claude-code.mcp.json +11 -0
- package/release/zero-to-map/configs/claude-desktop.json +11 -0
- package/release/zero-to-map/configs/cursor.json +11 -0
- package/release/zero-to-map/contracts/aws-ecs-provision-binding.schema.json +59 -0
- package/release/zero-to-map/contracts/checkpoint.schema.json +152 -0
- package/release/zero-to-map/contracts/console-receipt.schema.json +150 -0
- package/release/zero-to-map/contracts/esri-gp-mcp.v1.json +73 -0
- package/release/zero-to-map/fixtures/parcels.geojson +33 -0
- package/release/zero-to-map/fixtures/zoning.geojson +24 -0
- package/release/zero-to-map/journey.v1.json +1248 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { geoJsonToEsri } from "@honua/sdk-js/geometry";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { GEOMETRY_TYPES, resolveGeometryType } from "../helpers.js";
|
|
4
|
+
/**
|
|
5
|
+
* PROTOCOL-NEUTRAL filter + geometry vocabulary for the MCP tool contract (#1005).
|
|
6
|
+
*
|
|
7
|
+
* The SDK's `Query.filter` is a typed semantic filter that compiles to
|
|
8
|
+
* GeoServices SQL-92, CQL2 (OGC API Features / STAC), FES 2.0 (WFS), OData
|
|
9
|
+
* `$filter`, and DuckDB SQL — and refuses, naming the construct and the
|
|
10
|
+
* protocol, when a target cannot express a construct exactly. Exposing it on
|
|
11
|
+
* the tool surface is what lets an agent write ONE filter and have it mean the
|
|
12
|
+
* same thing on every backend, instead of hand-writing Esri SQL.
|
|
13
|
+
*
|
|
14
|
+
* The wire shape here is a flattened projection of that AST: one `op`
|
|
15
|
+
* discriminator per node instead of the SDK's `kind` + `operator` pair, because
|
|
16
|
+
* a flat discriminated union is markedly easier for a model to emit correctly.
|
|
17
|
+
* {@link toQueryFilter} lowers it onto the canonical expression losslessly.
|
|
18
|
+
*
|
|
19
|
+
* Geometry is canonical **GeoJSON** (RFC 7946). Esri-JSON geometry is still
|
|
20
|
+
* accepted for compatibility with existing clients and is converted on the way
|
|
21
|
+
* in; nothing on the surface requires it.
|
|
22
|
+
*/
|
|
23
|
+
const scalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
24
|
+
/** Protocol-neutral spatial predicates (`QueryFilterSpatialPredicate`). */
|
|
25
|
+
export const SPATIAL_PREDICATES = [
|
|
26
|
+
"intersects",
|
|
27
|
+
"contains",
|
|
28
|
+
"within",
|
|
29
|
+
"crosses",
|
|
30
|
+
"touches",
|
|
31
|
+
"overlaps",
|
|
32
|
+
"bbox-intersects",
|
|
33
|
+
];
|
|
34
|
+
const TEMPORAL_PREDICATES = ["before", "after", "during", "time-intersects"];
|
|
35
|
+
/** GeoJSON geometry accepted anywhere a spatial constraint is expressed. */
|
|
36
|
+
export const geoJsonGeometrySchema = z
|
|
37
|
+
.object({
|
|
38
|
+
type: z.enum(["Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon"]),
|
|
39
|
+
coordinates: z.array(z.unknown()),
|
|
40
|
+
})
|
|
41
|
+
.passthrough();
|
|
42
|
+
/**
|
|
43
|
+
* A geometry operand: GeoJSON (canonical) or Esri-JSON (accepted for
|
|
44
|
+
* compatibility). Kept as a loose record so an Esri envelope / rings payload
|
|
45
|
+
* from an existing client still validates.
|
|
46
|
+
*/
|
|
47
|
+
export const geometrySchema = z.record(z.unknown());
|
|
48
|
+
export const bboxSchema = z
|
|
49
|
+
.array(z.number())
|
|
50
|
+
.length(4)
|
|
51
|
+
.describe("Bounding box [minX, minY, maxX, maxY]. Interpreted in the source CRS (WGS84 unless bboxSrid says otherwise).");
|
|
52
|
+
/**
|
|
53
|
+
* Recursive zod schema for {@link FilterNode}. `z.lazy` keeps `and`/`or`/`not`
|
|
54
|
+
* genuinely recursive; zod-to-json-schema emits it as a `$ref`, which stays
|
|
55
|
+
* well-formed JSON Schema for MCP clients and for the certification harness.
|
|
56
|
+
*/
|
|
57
|
+
export const filterSchema = z.lazy(() => z.union([
|
|
58
|
+
z.object({
|
|
59
|
+
op: z.enum(["and", "or"]),
|
|
60
|
+
args: z.array(filterSchema).min(1),
|
|
61
|
+
}),
|
|
62
|
+
z.object({ op: z.literal("not"), arg: filterSchema }),
|
|
63
|
+
z.object({
|
|
64
|
+
op: z.enum(["eq", "ne", "lt", "lte", "gt", "gte"]),
|
|
65
|
+
field: z.string().min(1),
|
|
66
|
+
value: scalarSchema,
|
|
67
|
+
}),
|
|
68
|
+
z.object({ op: z.literal("in"), field: z.string().min(1), values: z.array(scalarSchema).min(1) }),
|
|
69
|
+
z.object({
|
|
70
|
+
op: z.literal("between"),
|
|
71
|
+
field: z.string().min(1),
|
|
72
|
+
lower: scalarSchema,
|
|
73
|
+
upper: scalarSchema,
|
|
74
|
+
}),
|
|
75
|
+
z.object({ op: z.enum(["is-null", "is-not-null"]), field: z.string().min(1) }),
|
|
76
|
+
z.object({
|
|
77
|
+
op: z.literal("like"),
|
|
78
|
+
field: z.string().min(1),
|
|
79
|
+
pattern: z.string(),
|
|
80
|
+
caseSensitive: z.boolean().optional(),
|
|
81
|
+
}),
|
|
82
|
+
z.object({
|
|
83
|
+
op: z.enum(SPATIAL_PREDICATES),
|
|
84
|
+
geometry: geometrySchema.optional(),
|
|
85
|
+
bbox: z.array(z.number()).length(4).optional(),
|
|
86
|
+
field: z.string().optional(),
|
|
87
|
+
geometryType: z.enum(GEOMETRY_TYPES).optional(),
|
|
88
|
+
}),
|
|
89
|
+
z.object({
|
|
90
|
+
op: z.enum(TEMPORAL_PREDICATES),
|
|
91
|
+
field: z.string().min(1),
|
|
92
|
+
// An interval is a 2-element array rather than a JSON Schema tuple: the
|
|
93
|
+
// draft-07 tuple form (`items: [...]`) is invalid under 2020-12, and the
|
|
94
|
+
// certification harness validates advertised schemas under both dialects.
|
|
95
|
+
value: z.union([z.string(), z.array(z.string()).length(2)]),
|
|
96
|
+
}),
|
|
97
|
+
]));
|
|
98
|
+
export class FilterInputError extends Error {
|
|
99
|
+
code = "INVALID_FILTER";
|
|
100
|
+
constructor(message) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = "FilterInputError";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isGeoJsonGeometry(value) {
|
|
106
|
+
return typeof value.type === "string" && Array.isArray(value.coordinates);
|
|
107
|
+
}
|
|
108
|
+
/** Build an Esri envelope from a `[minX, minY, maxX, maxY]` bbox. */
|
|
109
|
+
export function bboxToOperand(bbox, srid) {
|
|
110
|
+
const [xmin, ymin, xmax, ymax] = bbox;
|
|
111
|
+
const geometry = { xmin, ymin, xmax, ymax };
|
|
112
|
+
if (srid !== undefined) {
|
|
113
|
+
geometry.spatialReference = { wkid: srid };
|
|
114
|
+
}
|
|
115
|
+
return { geometry, geometryType: "esriGeometryEnvelope" };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Normalize a geometry input onto the canonical geometry operand.
|
|
119
|
+
*
|
|
120
|
+
* GeoJSON is the documented input; Esri-JSON is accepted so existing clients
|
|
121
|
+
* keep working. The canonical operand is Esri-shaped inside the SDK — that is
|
|
122
|
+
* an SDK implementation detail, not part of this tool contract.
|
|
123
|
+
*/
|
|
124
|
+
export function toGeometryOperand(geometry, esriGeometryType, srid) {
|
|
125
|
+
if (isGeoJsonGeometry(geometry)) {
|
|
126
|
+
const converted = geoJsonToEsri(geometry, srid !== undefined ? { wkid: srid } : undefined);
|
|
127
|
+
if (!converted) {
|
|
128
|
+
throw new FilterInputError(`GeoJSON geometry of type "${String(geometry.type)}" could not be converted to a query geometry.`);
|
|
129
|
+
}
|
|
130
|
+
const asRecord = converted;
|
|
131
|
+
const geometryType = resolveGeometryType(asRecord, undefined);
|
|
132
|
+
if (!geometryType) {
|
|
133
|
+
throw new FilterInputError("Converted geometry has no recognizable geometry type.");
|
|
134
|
+
}
|
|
135
|
+
return { geometry: asRecord, geometryType };
|
|
136
|
+
}
|
|
137
|
+
const geometryType = resolveGeometryType(geometry, esriGeometryType);
|
|
138
|
+
if (!geometryType) {
|
|
139
|
+
throw new FilterInputError('geometry is neither GeoJSON (an object with "type" and "coordinates") nor a recognizable Esri-JSON geometry. Pass GeoJSON, a bbox, or an explicit geometryType.');
|
|
140
|
+
}
|
|
141
|
+
return { geometry, geometryType };
|
|
142
|
+
}
|
|
143
|
+
function spatialOperand(node) {
|
|
144
|
+
if (node.geometry && node.bbox) {
|
|
145
|
+
throw new FilterInputError(`filter node "${node.op}" carries both geometry and bbox; pass exactly one.`);
|
|
146
|
+
}
|
|
147
|
+
if (node.bbox) {
|
|
148
|
+
return bboxToOperand(node.bbox);
|
|
149
|
+
}
|
|
150
|
+
if (node.geometry) {
|
|
151
|
+
return toGeometryOperand(node.geometry, node.geometryType);
|
|
152
|
+
}
|
|
153
|
+
throw new FilterInputError(`filter node "${node.op}" requires either geometry (GeoJSON) or bbox.`);
|
|
154
|
+
}
|
|
155
|
+
function isSpatialOp(op) {
|
|
156
|
+
return SPATIAL_PREDICATES.includes(op);
|
|
157
|
+
}
|
|
158
|
+
function isTemporalOp(op) {
|
|
159
|
+
return TEMPORAL_PREDICATES.includes(op);
|
|
160
|
+
}
|
|
161
|
+
/** Lower the wire filter DSL onto the SDK's canonical typed filter expression. */
|
|
162
|
+
export function toQueryFilter(node) {
|
|
163
|
+
switch (node.op) {
|
|
164
|
+
case "and":
|
|
165
|
+
case "or":
|
|
166
|
+
return { kind: "boolean", operator: node.op, args: node.args.map(toQueryFilter) };
|
|
167
|
+
case "not":
|
|
168
|
+
return { kind: "not", arg: toQueryFilter(node.arg) };
|
|
169
|
+
case "eq":
|
|
170
|
+
case "ne":
|
|
171
|
+
case "lt":
|
|
172
|
+
case "lte":
|
|
173
|
+
case "gt":
|
|
174
|
+
case "gte":
|
|
175
|
+
return {
|
|
176
|
+
kind: "comparison",
|
|
177
|
+
operator: node.op,
|
|
178
|
+
left: { kind: "property", name: node.field },
|
|
179
|
+
right: { kind: "literal", value: node.value },
|
|
180
|
+
};
|
|
181
|
+
case "in":
|
|
182
|
+
return {
|
|
183
|
+
kind: "list",
|
|
184
|
+
operator: "in",
|
|
185
|
+
operand: { kind: "property", name: node.field },
|
|
186
|
+
values: node.values.map((value) => ({ kind: "literal", value })),
|
|
187
|
+
};
|
|
188
|
+
case "between":
|
|
189
|
+
return {
|
|
190
|
+
kind: "range",
|
|
191
|
+
operator: "between",
|
|
192
|
+
operand: { kind: "property", name: node.field },
|
|
193
|
+
lower: { kind: "literal", value: node.lower },
|
|
194
|
+
upper: { kind: "literal", value: node.upper },
|
|
195
|
+
};
|
|
196
|
+
case "is-null":
|
|
197
|
+
case "is-not-null":
|
|
198
|
+
return { kind: "null", operator: node.op, operand: { kind: "property", name: node.field } };
|
|
199
|
+
case "like":
|
|
200
|
+
return {
|
|
201
|
+
kind: "pattern",
|
|
202
|
+
operator: "like",
|
|
203
|
+
operand: { kind: "property", name: node.field },
|
|
204
|
+
pattern: node.pattern,
|
|
205
|
+
...(node.caseSensitive === undefined ? {} : { caseSensitive: node.caseSensitive }),
|
|
206
|
+
};
|
|
207
|
+
default:
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (isSpatialOp(node.op)) {
|
|
211
|
+
const spatial = node;
|
|
212
|
+
return {
|
|
213
|
+
kind: "spatial",
|
|
214
|
+
operator: spatial.op,
|
|
215
|
+
geometry: spatialOperand(spatial),
|
|
216
|
+
...(spatial.field ? { property: spatial.field } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (isTemporalOp(node.op)) {
|
|
220
|
+
const temporal = node;
|
|
221
|
+
const value = temporal.value;
|
|
222
|
+
return {
|
|
223
|
+
kind: "temporal",
|
|
224
|
+
operator: temporal.op,
|
|
225
|
+
operand: { kind: "property", name: temporal.field },
|
|
226
|
+
value: typeof value === "string"
|
|
227
|
+
? { kind: "temporal-literal", valueType: "instant", value }
|
|
228
|
+
: { kind: "temporal-literal", valueType: "interval", value: [value[0], value[1]] },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
throw new FilterInputError(`unknown filter operator "${String(node.op)}".`);
|
|
232
|
+
}
|
|
233
|
+
/** Conjoin filter expressions; `undefined` when nothing was supplied. */
|
|
234
|
+
export function andFilters(parts) {
|
|
235
|
+
if (parts.length === 0)
|
|
236
|
+
return undefined;
|
|
237
|
+
if (parts.length === 1)
|
|
238
|
+
return parts[0];
|
|
239
|
+
return { kind: "boolean", operator: "and", args: [...parts] };
|
|
240
|
+
}
|
|
241
|
+
/** Canonical time constraint accepted on the tool surface. */
|
|
242
|
+
export const temporalSchema = z
|
|
243
|
+
.object({
|
|
244
|
+
instant: z.string().optional().describe("RFC 3339 instant, e.g. 2026-05-01T00:00:00Z."),
|
|
245
|
+
start: z.string().nullable().optional().describe("Interval start (null = open ended)."),
|
|
246
|
+
end: z.string().nullable().optional().describe("Interval end (null = open ended)."),
|
|
247
|
+
field: z
|
|
248
|
+
.string()
|
|
249
|
+
.optional()
|
|
250
|
+
.describe("Time column to constrain. Omit to target the source's own time dimension (GeoServices time=, OGC/STAC datetime=); OData, WFS, and GeoParquet require an explicit field."),
|
|
251
|
+
})
|
|
252
|
+
.describe("Protocol-neutral time constraint: either `instant`, or `start`/`end` for an interval.");
|
|
253
|
+
/** Lower the wire temporal input onto `Query.temporalFilter`. */
|
|
254
|
+
export function toTemporalFilter(input) {
|
|
255
|
+
const field = input.field ? { field: input.field } : {};
|
|
256
|
+
if (input.instant !== undefined) {
|
|
257
|
+
if (input.start !== undefined || input.end !== undefined) {
|
|
258
|
+
throw new FilterInputError("temporal accepts either `instant` or `start`/`end`, not both.");
|
|
259
|
+
}
|
|
260
|
+
return { kind: "instant", instant: input.instant, ...field };
|
|
261
|
+
}
|
|
262
|
+
if (input.start === undefined && input.end === undefined) {
|
|
263
|
+
throw new FilterInputError("temporal requires `instant`, or at least one of `start` / `end`.");
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
kind: "interval",
|
|
267
|
+
start: input.start ?? null,
|
|
268
|
+
end: input.end ?? null,
|
|
269
|
+
...field,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Query, SortSpec } from "@honua/sdk-js/contract";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { GEOMETRY_TYPES } from "../helpers.js";
|
|
4
|
+
import { SPATIAL_PREDICATES, temporalSchema } from "./filter.js";
|
|
5
|
+
/**
|
|
6
|
+
* PROTOCOL-NEUTRAL query vocabulary for the MCP tool contract (#1005).
|
|
7
|
+
*
|
|
8
|
+
* Everything here lowers onto the SDK's stable `Query` envelope, which each
|
|
9
|
+
* protocol adapter translates into its own dialect. Nothing in the vocabulary
|
|
10
|
+
* is Esri-shaped: the filter is the typed semantic filter, geometry is GeoJSON
|
|
11
|
+
* (or a bbox), the spatial predicate is the neutral one, sorting is
|
|
12
|
+
* `field`/`direction`, and paging is `limit`/`offset`.
|
|
13
|
+
*
|
|
14
|
+
* Two deprecated compatibility inputs remain so existing clients keep working:
|
|
15
|
+
* `where` (source-native filter text, whose language depends on the backend)
|
|
16
|
+
* and `geometryType` (an Esri geometry-type tag for Esri-JSON geometry). Both
|
|
17
|
+
* are optional; neither is required by any tool.
|
|
18
|
+
*/
|
|
19
|
+
/** Filter + spatial + temporal fields shared by every query-family tool. */
|
|
20
|
+
export declare const queryFilterFields: {
|
|
21
|
+
readonly filter: z.ZodOptional<z.ZodType<import("./filter.js").FilterNode, z.ZodTypeDef, import("./filter.js").FilterNode>>;
|
|
22
|
+
readonly where: z.ZodOptional<z.ZodString>;
|
|
23
|
+
readonly bbox: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
|
|
24
|
+
readonly bboxSrid: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
readonly geometry: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
26
|
+
readonly geometryType: z.ZodOptional<z.ZodEnum<["esriGeometryPoint", "esriGeometryPolyline", "esriGeometryPolygon", "esriGeometryEnvelope", "esriGeometryMultipoint"]>>;
|
|
27
|
+
readonly spatialRel: z.ZodOptional<z.ZodEnum<["intersects", "contains", "within", "crosses", "touches", "overlaps", "bbox-intersects"]>>;
|
|
28
|
+
readonly temporal: z.ZodOptional<z.ZodObject<{
|
|
29
|
+
instant: z.ZodOptional<z.ZodString>;
|
|
30
|
+
start: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
31
|
+
end: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
32
|
+
field: z.ZodOptional<z.ZodString>;
|
|
33
|
+
}, "strip", z.ZodTypeAny, {
|
|
34
|
+
field?: string | undefined;
|
|
35
|
+
instant?: string | undefined;
|
|
36
|
+
start?: string | null | undefined;
|
|
37
|
+
end?: string | null | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
field?: string | undefined;
|
|
40
|
+
instant?: string | undefined;
|
|
41
|
+
start?: string | null | undefined;
|
|
42
|
+
end?: string | null | undefined;
|
|
43
|
+
}>>;
|
|
44
|
+
};
|
|
45
|
+
/** Projection / ordering / paging fields for the feature-returning tools. */
|
|
46
|
+
export declare const queryShapeFields: {
|
|
47
|
+
readonly outFields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
48
|
+
readonly orderBy: z.ZodOptional<z.ZodUnion<[z.ZodArray<z.ZodObject<{
|
|
49
|
+
field: z.ZodString;
|
|
50
|
+
direction: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
|
|
51
|
+
}, "strip", z.ZodTypeAny, {
|
|
52
|
+
field: string;
|
|
53
|
+
direction?: "asc" | "desc" | undefined;
|
|
54
|
+
}, {
|
|
55
|
+
field: string;
|
|
56
|
+
direction?: "asc" | "desc" | undefined;
|
|
57
|
+
}>, "many">, z.ZodString]>>;
|
|
58
|
+
readonly limit: z.ZodOptional<z.ZodNumber>;
|
|
59
|
+
readonly offset: z.ZodOptional<z.ZodNumber>;
|
|
60
|
+
readonly returnGeometry: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
61
|
+
readonly geometryFormat: z.ZodOptional<z.ZodEnum<["geojson", "esri-json"]>>;
|
|
62
|
+
readonly outSr: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
63
|
+
};
|
|
64
|
+
export interface NeutralQueryInput {
|
|
65
|
+
filter?: unknown;
|
|
66
|
+
where?: string | undefined;
|
|
67
|
+
bbox?: number[] | undefined;
|
|
68
|
+
bboxSrid?: number | undefined;
|
|
69
|
+
geometry?: Record<string, unknown> | undefined;
|
|
70
|
+
geometryType?: (typeof GEOMETRY_TYPES)[number] | undefined;
|
|
71
|
+
spatialRel?: (typeof SPATIAL_PREDICATES)[number] | undefined;
|
|
72
|
+
temporal?: z.infer<typeof temporalSchema> | undefined;
|
|
73
|
+
outFields?: string[] | undefined;
|
|
74
|
+
orderBy?: string | {
|
|
75
|
+
field: string;
|
|
76
|
+
direction?: "asc" | "desc";
|
|
77
|
+
}[] | undefined;
|
|
78
|
+
limit?: number | undefined;
|
|
79
|
+
offset?: number | undefined;
|
|
80
|
+
returnGeometry?: boolean | undefined;
|
|
81
|
+
outSr?: string | number | undefined;
|
|
82
|
+
}
|
|
83
|
+
/** Parse the legacy `"NAME DESC,VALUE ASC"` order string into canonical sort specs. */
|
|
84
|
+
export declare function parseOrderBy(orderBy: NeutralQueryInput["orderBy"]): SortSpec[] | undefined;
|
|
85
|
+
export interface ToQueryOptions {
|
|
86
|
+
/** Protocol of the resolved source; drives protocol-quirk compensation only. */
|
|
87
|
+
readonly protocol: string;
|
|
88
|
+
/** Apply the default page size / cap. Off for aggregate + extent queries. */
|
|
89
|
+
readonly paginate?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Lower the neutral tool input onto the SDK's canonical `Query`.
|
|
93
|
+
*
|
|
94
|
+
* The one protocol-specific compensation is `outFields`: a GeoServices
|
|
95
|
+
* `query` with no `outFields` returns OBJECTID only, whereas every other
|
|
96
|
+
* protocol returns all properties. Requesting `*` for the GeoServices family
|
|
97
|
+
* keeps "omit outFields ⇒ all fields" true across protocols rather than
|
|
98
|
+
* leaking an Esri default into the neutral contract.
|
|
99
|
+
*/
|
|
100
|
+
export declare function toQuery(input: NeutralQueryInput, options: ToQueryOptions): Query;
|
|
101
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../src/neutral/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAyB,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAc,MAAM,eAAe,CAAC;AAC3D,OAAO,EAEL,kBAAkB,EAKlB,cAAc,EAIf,MAAM,aAAa,CAAC;AAGrB;;;;;;;;;;;;;GAaG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;CA4BpB,CAAC;AAOX,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;CAmBnB,CAAC;AAEX,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC/C,YAAY,CAAC,EAAE,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAC3D,UAAU,CAAC,EAAE,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAC7D,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,GAAG,SAAS,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,EAAE,GAAG,SAAS,CAAC;IAC/E,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,uFAAuF;AACvF,wBAAgB,YAAY,CAAC,OAAO,EAAE,iBAAiB,CAAC,SAAS,CAAC,GAAG,QAAQ,EAAE,GAAG,SAAS,CAmB1F;AA2CD,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,cAAc,GAAG,KAAK,CAwChF"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { GEOMETRY_TYPES, clampLimit } from "../helpers.js";
|
|
3
|
+
import { FilterInputError, SPATIAL_PREDICATES, andFilters, bboxToOperand, filterSchema, geometrySchema, temporalSchema, toGeometryOperand, toQueryFilter, toTemporalFilter, } from "./filter.js";
|
|
4
|
+
import { isGeoServicesProtocol } from "./source-ref.js";
|
|
5
|
+
/**
|
|
6
|
+
* PROTOCOL-NEUTRAL query vocabulary for the MCP tool contract (#1005).
|
|
7
|
+
*
|
|
8
|
+
* Everything here lowers onto the SDK's stable `Query` envelope, which each
|
|
9
|
+
* protocol adapter translates into its own dialect. Nothing in the vocabulary
|
|
10
|
+
* is Esri-shaped: the filter is the typed semantic filter, geometry is GeoJSON
|
|
11
|
+
* (or a bbox), the spatial predicate is the neutral one, sorting is
|
|
12
|
+
* `field`/`direction`, and paging is `limit`/`offset`.
|
|
13
|
+
*
|
|
14
|
+
* Two deprecated compatibility inputs remain so existing clients keep working:
|
|
15
|
+
* `where` (source-native filter text, whose language depends on the backend)
|
|
16
|
+
* and `geometryType` (an Esri geometry-type tag for Esri-JSON geometry). Both
|
|
17
|
+
* are optional; neither is required by any tool.
|
|
18
|
+
*/
|
|
19
|
+
/** Filter + spatial + temporal fields shared by every query-family tool. */
|
|
20
|
+
export const queryFilterFields = {
|
|
21
|
+
filter: filterSchema
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Typed, protocol-neutral filter. Nodes are {"op": …}: comparison (eq/ne/lt/lte/gt/gte with field+value), in (field+values), between (field+lower+upper), is-null/is-not-null (field), like (field+pattern[+caseSensitive]), and/or (args), not (arg), spatial (intersects/contains/within/crosses/touches/overlaps/bbox-intersects with geometry or bbox), temporal (before/after/during/time-intersects with field+value). Compiles to SQL-92, CQL2, FES 2.0, or OData depending on the backend, and fails with a named capability error rather than silently dropping a construct.'),
|
|
24
|
+
where: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("[DEPRECATED — use `filter`] Source-native filter text. Its language depends on the backend (GeoServices SQL-92, CQL2, OData); it is not protocol neutral."),
|
|
28
|
+
bbox: z.array(z.number()).length(4).optional().describe("Spatial filter envelope [minX, minY, maxX, maxY]."),
|
|
29
|
+
bboxSrid: z.number().int().optional().describe("Spatial reference (SRID/WKID) of the bbox ordinates (default 4326)."),
|
|
30
|
+
geometry: geometrySchema
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Spatial filter geometry as GeoJSON (RFC 7946). Esri-JSON geometry is still accepted for compatibility and is converted on the way in."),
|
|
33
|
+
geometryType: z
|
|
34
|
+
.enum(GEOMETRY_TYPES)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("[DEPRECATED — send GeoJSON] Esri geometry type tag, used only to disambiguate Esri-JSON geometry."),
|
|
37
|
+
spatialRel: z
|
|
38
|
+
.enum(SPATIAL_PREDICATES)
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Protocol-neutral spatial predicate applied to `geometry`/`bbox` (default: intersects)."),
|
|
41
|
+
temporal: temporalSchema.optional(),
|
|
42
|
+
};
|
|
43
|
+
const sortSpecSchema = z.object({
|
|
44
|
+
field: z.string().min(1),
|
|
45
|
+
direction: z.enum(["asc", "desc"]).optional(),
|
|
46
|
+
});
|
|
47
|
+
/** Projection / ordering / paging fields for the feature-returning tools. */
|
|
48
|
+
export const queryShapeFields = {
|
|
49
|
+
outFields: z.array(z.string()).optional().describe("Fields to return; defaults to all."),
|
|
50
|
+
orderBy: z
|
|
51
|
+
.union([z.array(sortSpecSchema), z.string()])
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Sort order as [{"field":"NAME","direction":"desc"}], or the legacy string form "NAME DESC,VALUE ASC".'),
|
|
54
|
+
limit: z.number().int().positive().optional().describe("Max features to return (default 100, max 2000)."),
|
|
55
|
+
offset: z.number().int().nonnegative().optional().describe("Number of features to skip (for pagination)."),
|
|
56
|
+
returnGeometry: z.boolean().optional().default(false).describe("Include geometry in results (default: false)."),
|
|
57
|
+
geometryFormat: z
|
|
58
|
+
.enum(["geojson", "esri-json"])
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Geometry encoding of returned features. Defaults to `geojson` for neutrally addressed sources, and to `esri-json` when the deprecated serviceId/layerId pair was used (preserving legacy client output)."),
|
|
61
|
+
outSr: z
|
|
62
|
+
.union([z.string(), z.number()])
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Output spatial reference (WKID or CRS identifier) for returned geometry."),
|
|
65
|
+
};
|
|
66
|
+
/** Parse the legacy `"NAME DESC,VALUE ASC"` order string into canonical sort specs. */
|
|
67
|
+
export function parseOrderBy(orderBy) {
|
|
68
|
+
if (orderBy === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (Array.isArray(orderBy)) {
|
|
71
|
+
return orderBy.length > 0
|
|
72
|
+
? orderBy.map((spec) => ({ field: spec.field, direction: spec.direction ?? "asc" }))
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
const specs = orderBy
|
|
76
|
+
.split(",")
|
|
77
|
+
.map((part) => part.trim())
|
|
78
|
+
.filter((part) => part.length > 0)
|
|
79
|
+
.map((part) => {
|
|
80
|
+
const [field, direction] = part.split(/\s+/);
|
|
81
|
+
return {
|
|
82
|
+
field,
|
|
83
|
+
direction: (direction ?? "asc").toLowerCase() === "desc" ? "desc" : "asc",
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
return specs.length > 0 ? specs : undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Lower the spatial constraint onto whichever canonical channel the widest set
|
|
90
|
+
* of protocols can serve.
|
|
91
|
+
*
|
|
92
|
+
* An envelope tested with `intersects` is the one spatial constraint every
|
|
93
|
+
* protocol expresses natively — it is the OGC API Features / STAC `bbox`
|
|
94
|
+
* parameter (Part 1, no filtering extension required) and a plain GeoServices
|
|
95
|
+
* geometry parameter — so it travels as `Query.spatialFilter`. Anything richer
|
|
96
|
+
* (a real polygon, `within`, `crosses`, …) becomes a typed spatial filter node,
|
|
97
|
+
* which compiles to CQL2 / FES / SQL-92 where supported and fails closed,
|
|
98
|
+
* naming the construct, where it is not.
|
|
99
|
+
*/
|
|
100
|
+
function spatialConstraint(input) {
|
|
101
|
+
if (input.geometry && input.bbox) {
|
|
102
|
+
throw new FilterInputError("Pass either `geometry` or `bbox` for the spatial filter, not both.");
|
|
103
|
+
}
|
|
104
|
+
const operand = input.bbox
|
|
105
|
+
? bboxToOperand(input.bbox, input.bboxSrid)
|
|
106
|
+
: input.geometry
|
|
107
|
+
? toGeometryOperand(input.geometry, input.geometryType, input.bboxSrid)
|
|
108
|
+
: undefined;
|
|
109
|
+
if (!operand)
|
|
110
|
+
return {};
|
|
111
|
+
const predicate = input.spatialRel ?? "intersects";
|
|
112
|
+
const isPortableEnvelope = operand.geometryType === "esriGeometryEnvelope" && (predicate === "intersects" || predicate === "bbox-intersects");
|
|
113
|
+
if (isPortableEnvelope) {
|
|
114
|
+
return {
|
|
115
|
+
spatialFilter: {
|
|
116
|
+
geometry: operand.geometry,
|
|
117
|
+
geometryType: operand.geometryType,
|
|
118
|
+
spatialRel: predicate === "intersects" ? "esriSpatialRelIntersects" : "esriSpatialRelEnvelopeIntersects",
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { filterNode: { kind: "spatial", operator: predicate, geometry: operand } };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Lower the neutral tool input onto the SDK's canonical `Query`.
|
|
126
|
+
*
|
|
127
|
+
* The one protocol-specific compensation is `outFields`: a GeoServices
|
|
128
|
+
* `query` with no `outFields` returns OBJECTID only, whereas every other
|
|
129
|
+
* protocol returns all properties. Requesting `*` for the GeoServices family
|
|
130
|
+
* keeps "omit outFields ⇒ all fields" true across protocols rather than
|
|
131
|
+
* leaking an Esri default into the neutral contract.
|
|
132
|
+
*/
|
|
133
|
+
export function toQuery(input, options) {
|
|
134
|
+
const conjuncts = [];
|
|
135
|
+
if (input.filter !== undefined && input.filter !== null) {
|
|
136
|
+
conjuncts.push(toQueryFilter(filterSchema.parse(input.filter)));
|
|
137
|
+
}
|
|
138
|
+
const spatial = spatialConstraint(input);
|
|
139
|
+
if (spatial.filterNode)
|
|
140
|
+
conjuncts.push(spatial.filterNode);
|
|
141
|
+
const query = {};
|
|
142
|
+
const filter = andFilters(conjuncts);
|
|
143
|
+
if (filter)
|
|
144
|
+
query.filter = filter;
|
|
145
|
+
if (spatial.spatialFilter)
|
|
146
|
+
query.spatialFilter = spatial.spatialFilter;
|
|
147
|
+
if (input.where !== undefined && input.where.trim().length > 0)
|
|
148
|
+
query.where = input.where;
|
|
149
|
+
if (input.temporal)
|
|
150
|
+
query.temporalFilter = toTemporalFilter(input.temporal);
|
|
151
|
+
const outFields = input.outFields && input.outFields.length > 0
|
|
152
|
+
? input.outFields
|
|
153
|
+
: isGeoServicesProtocol(options.protocol)
|
|
154
|
+
? ["*"]
|
|
155
|
+
: undefined;
|
|
156
|
+
if (outFields)
|
|
157
|
+
query.outFields = outFields;
|
|
158
|
+
const orderBy = parseOrderBy(input.orderBy);
|
|
159
|
+
if (orderBy)
|
|
160
|
+
query.orderBy = orderBy;
|
|
161
|
+
if (options.paginate !== false) {
|
|
162
|
+
const pagination = { limit: clampLimit(input.limit) };
|
|
163
|
+
if (input.offset !== undefined)
|
|
164
|
+
pagination.offset = input.offset;
|
|
165
|
+
query.pagination = pagination;
|
|
166
|
+
}
|
|
167
|
+
else if (input.offset !== undefined || input.limit !== undefined) {
|
|
168
|
+
const pagination = {};
|
|
169
|
+
if (input.limit !== undefined)
|
|
170
|
+
pagination.limit = clampLimit(input.limit);
|
|
171
|
+
if (input.offset !== undefined)
|
|
172
|
+
pagination.offset = input.offset;
|
|
173
|
+
query.pagination = pagination;
|
|
174
|
+
}
|
|
175
|
+
if (input.returnGeometry !== undefined)
|
|
176
|
+
query.returnGeometry = input.returnGeometry;
|
|
177
|
+
if (input.outSr !== undefined)
|
|
178
|
+
query.outSr = input.outSr;
|
|
179
|
+
return query;
|
|
180
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DegradedReason, Result } from "@honua/sdk-js/contract";
|
|
2
|
+
/**
|
|
3
|
+
* PROTOCOL-NEUTRAL result projection (#1005).
|
|
4
|
+
*
|
|
5
|
+
* A `Result` carries whatever geometry encoding its protocol produced —
|
|
6
|
+
* Esri-JSON from GeoServices, GeoJSON from OGC API Features / STAC. Returning
|
|
7
|
+
* that raw makes the tool output protocol-dependent, so the neutral contract
|
|
8
|
+
* normalizes to GeoJSON and says which encoding it used.
|
|
9
|
+
*
|
|
10
|
+
* `Result.degraded` is surfaced verbatim: when a protocol served a request a
|
|
11
|
+
* different way than asked (client-side aggregation on OGC, for example), the
|
|
12
|
+
* answer says so instead of quietly presenting a weaker number as authoritative.
|
|
13
|
+
*/
|
|
14
|
+
export type GeometryFormat = "geojson" | "esri-json";
|
|
15
|
+
/** Re-encode one feature geometry into the requested format. */
|
|
16
|
+
export declare function projectGeometry(geometry: Record<string, unknown> | null | undefined, format: GeometryFormat): Record<string, unknown> | null;
|
|
17
|
+
/** Normalize `Result.degraded` for tool output; `undefined` when nothing degraded. */
|
|
18
|
+
export declare function projectDegraded(degraded: readonly DegradedReason[] | undefined): {
|
|
19
|
+
capability: import("@honua/sdk-js/contract").Capability;
|
|
20
|
+
reason: string;
|
|
21
|
+
protocol: import("@honua/sdk-js/contract").Protocol | null;
|
|
22
|
+
sourceId: string | null;
|
|
23
|
+
}[] | undefined;
|
|
24
|
+
export interface ProjectFeaturesOptions {
|
|
25
|
+
readonly returnGeometry: boolean;
|
|
26
|
+
readonly geometryFormat: GeometryFormat;
|
|
27
|
+
}
|
|
28
|
+
/** Project canonical features onto the tool's neutral feature shape. */
|
|
29
|
+
export declare function projectFeatures(result: Result, options: ProjectFeaturesOptions): {
|
|
30
|
+
geometry?: Record<string, unknown> | undefined;
|
|
31
|
+
attributes: Record<string, unknown>;
|
|
32
|
+
}[];
|
|
33
|
+
//# sourceMappingURL=result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../../src/neutral/result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAGrE;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,CAAC;AAMrD,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EACpD,MAAM,EAAE,cAAc,GACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAMhC;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS;;;;;gBAQ9E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB;;;IAU9E"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { esriToGeoJson } from "@honua/sdk-js/geometry";
|
|
2
|
+
function looksGeoJson(geometry) {
|
|
3
|
+
return typeof geometry.type === "string" && "coordinates" in geometry;
|
|
4
|
+
}
|
|
5
|
+
/** Re-encode one feature geometry into the requested format. */
|
|
6
|
+
export function projectGeometry(geometry, format) {
|
|
7
|
+
if (!geometry)
|
|
8
|
+
return null;
|
|
9
|
+
if (format === "esri-json")
|
|
10
|
+
return geometry;
|
|
11
|
+
if (looksGeoJson(geometry))
|
|
12
|
+
return geometry;
|
|
13
|
+
const converted = esriToGeoJson(geometry);
|
|
14
|
+
return converted ? converted : null;
|
|
15
|
+
}
|
|
16
|
+
/** Normalize `Result.degraded` for tool output; `undefined` when nothing degraded. */
|
|
17
|
+
export function projectDegraded(degraded) {
|
|
18
|
+
if (!degraded || degraded.length === 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
return degraded.map((reason) => ({
|
|
21
|
+
capability: reason.capability,
|
|
22
|
+
reason: reason.reason,
|
|
23
|
+
protocol: reason.protocol ?? null,
|
|
24
|
+
sourceId: reason.sourceId ?? null,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
/** Project canonical features onto the tool's neutral feature shape. */
|
|
28
|
+
export function projectFeatures(result, options) {
|
|
29
|
+
return result.features.map((feature) => {
|
|
30
|
+
const geometry = options.returnGeometry
|
|
31
|
+
? projectGeometry(feature.geometry, options.geometryFormat)
|
|
32
|
+
: null;
|
|
33
|
+
return {
|
|
34
|
+
attributes: feature.attributes,
|
|
35
|
+
...(geometry ? { geometry } : {}),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|