89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
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<WaterNetworkSourceId, "id" | "sensor_id"> = {
|
|
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) };
|
|
}
|