import type { FeatureCollection } from "geojson"; import { GEOSERVER_WORKSPACE, MAP_URL } from "../../../lib/config"; import { SOURCE_LAYERS, type WaterNetworkSourceId } from "./sources"; export const MAP_FEATURE_ID_FIELDS: Record = { conduits: "id", junctions: "id", orifices: "id", outfalls: "id", pumps: "id", scada: "sensor_id" }; export const MAX_MAP_FEATURE_IDS = 100; export const MAX_MAP_FEATURE_ID_LENGTH = 128; export const MAX_CONDUIT_FEATURES = 5000; export type MapFeatureQuery = { sourceId: WaterNetworkSourceId; 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" || !(sourceId in SOURCE_LAYERS)) { return { ok: false, code: "UNSUPPORTED_SOURCE", status: 422 }; } if (featureIds === undefined) { return sourceId === "conduits" ? { 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 WaterNetworkSourceId, 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_CONDUIT_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) }; }