Files
next-tjwater-frontend/src/features/workbench/map/map-feature-query.ts
T
jiang f9c71cc076
Generic Container CI/CD / test-build-publish (push) Successful in 26s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 26s
feat(workbench): align v2 GIS and SCADA API
2026-09-08 18:18:40 +08:00

96 lines
3.1 KiB
TypeScript

import type { FeatureCollection } from "geojson";
import { GEOSERVER_WORKSPACE, MAP_URL } from "./geoserver-config";
import {
SOURCE_LAYERS,
GEOSERVER_WATER_NETWORK_SOURCE_IDS,
type GeoServerWaterNetworkSourceId
} from "./sources";
export const MAP_FEATURE_ID_FIELDS: Record<GeoServerWaterNetworkSourceId, "id"> = {
pipes: "id",
junctions: "id",
valves: "id",
reservoirs: "id",
pumps: "id",
tanks: "id"
};
export const MAX_MAP_FEATURE_IDS = 100;
export const MAX_MAP_FEATURE_ID_LENGTH = 128;
export const MAX_PIPE_FEATURES = 5000;
export type MapFeatureQuery = {
sourceId: GeoServerWaterNetworkSourceId;
featureIds?: string[];
};
export type MapFeatureQueryValidation =
| { ok: true; value: MapFeatureQuery }
| { ok: false; code: "INVALID_REQUEST" | "UNSUPPORTED_SOURCE"; status: 400 | 422 };
export function parseMapFeatureQuery(body: unknown): MapFeatureQueryValidation {
if (!body || typeof body !== "object") {
return { ok: false, code: "INVALID_REQUEST", status: 400 };
}
const { sourceId, featureIds } = body as { sourceId?: unknown; featureIds?: unknown };
if (
typeof sourceId !== "string" ||
!(GEOSERVER_WATER_NETWORK_SOURCE_IDS as readonly string[]).includes(sourceId)
) {
return { ok: false, code: "UNSUPPORTED_SOURCE", status: 422 };
}
if (featureIds === undefined) {
return sourceId === "pipes"
? { ok: true, value: { sourceId } }
: { ok: false, code: "INVALID_REQUEST", status: 400 };
}
if (
!Array.isArray(featureIds) ||
featureIds.length < 1 ||
featureIds.length > MAX_MAP_FEATURE_IDS ||
!featureIds.every(
(id) => typeof id === "string" && id.trim().length > 0 && id.trim().length <= MAX_MAP_FEATURE_ID_LENGTH
)
) {
return { ok: false, code: "INVALID_REQUEST", status: 400 };
}
return {
ok: true,
value: { sourceId: sourceId as GeoServerWaterNetworkSourceId, featureIds: featureIds.map((id) => id.trim()) }
};
}
export function escapeCqlLiteral(value: string) {
return `'${value.replaceAll("'", "''")}'`;
}
export function createMapFeatureWfsUrl(query: MapFeatureQuery) {
const idField = MAP_FEATURE_ID_FIELDS[query.sourceId];
const params = new URLSearchParams({
service: "WFS",
version: "2.0.0",
request: "GetFeature",
typeNames: `${GEOSERVER_WORKSPACE}:${SOURCE_LAYERS[query.sourceId]}`,
outputFormat: "application/json",
srsName: "EPSG:4326",
count: String(query.featureIds ? MAX_MAP_FEATURE_IDS : MAX_PIPE_FEATURES)
});
if (query.featureIds) {
params.set("cql_filter", `"${idField}" IN (${query.featureIds.map(escapeCqlLiteral).join(",")})`);
}
return new URL(`${MAP_URL}/${GEOSERVER_WORKSPACE}/ows?${params.toString()}`);
}
export function normalizeMapFeatureCollection(value: unknown, maxFeatures: number): FeatureCollection | null {
if (!value || typeof value !== "object") return null;
const collection = value as FeatureCollection;
if (collection.type !== "FeatureCollection" || !Array.isArray(collection.features)) return null;
return { type: "FeatureCollection", features: collection.features.slice(0, maxFeatures) };
}