feat: initialize TJWater WebGIS frontend
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
export type TrustedMapAction =
|
||||
| {
|
||||
type: "locate_features";
|
||||
featureIds: string[];
|
||||
layer?: string;
|
||||
fallbackText?: string;
|
||||
}
|
||||
| {
|
||||
type: "zoom_to_map";
|
||||
center: [number, number];
|
||||
zoom?: number;
|
||||
durationMs?: number;
|
||||
fallbackText?: string;
|
||||
}
|
||||
| {
|
||||
type: "apply_layer_style";
|
||||
layerGroupId?: string;
|
||||
layerId?: string;
|
||||
visible?: boolean;
|
||||
fallbackText?: string;
|
||||
}
|
||||
| {
|
||||
type: "render_junctions";
|
||||
renderRef: string;
|
||||
fallbackText?: string;
|
||||
};
|
||||
|
||||
export function toTrustedMapAction(action: string, params: unknown, fallbackText?: string): TrustedMapAction | null {
|
||||
if (!isRecord(params)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (action === "locate_features" || action in LEGACY_LOCATE_ACTION_LAYERS) {
|
||||
const featureIds = readLocateIds(params);
|
||||
if (featureIds.length === 0 || featureIds.length > 100 || featureIds.some((id) => id.length > 128)) return null;
|
||||
return {
|
||||
type: "locate_features",
|
||||
featureIds,
|
||||
layer:
|
||||
stringValue(params.layer ?? params.target_layer ?? params.targetLayer) ??
|
||||
LEGACY_LOCATE_ACTION_LAYERS[action],
|
||||
fallbackText
|
||||
};
|
||||
}
|
||||
|
||||
if (action === "zoom_to_map") {
|
||||
const center = parseCenter(params);
|
||||
if (!center) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const zoom = numberValue(params.zoom);
|
||||
const durationMs = numberValue(params.duration_ms ?? params.durationMs);
|
||||
if (center[0] < -180 || center[0] > 180 || center[1] < -90 || center[1] > 90 ||
|
||||
(zoom !== undefined && (zoom < 0 || zoom > 24))) return null;
|
||||
|
||||
return {
|
||||
type: "zoom_to_map",
|
||||
center,
|
||||
zoom,
|
||||
...(durationMs === undefined ? {} : { durationMs }),
|
||||
fallbackText
|
||||
};
|
||||
}
|
||||
|
||||
if (action === "apply_layer_style") {
|
||||
const layerGroupId = stringValue(params.layer_group_id ?? params.layerGroupId);
|
||||
const layerId = stringValue(params.layer_id ?? params.layerId);
|
||||
const visible = booleanValue(params.visible);
|
||||
if ((!layerGroupId && !layerId) || visible === undefined || (layerId && !ALLOWED_LAYER_IDS.has(layerId))) return null;
|
||||
return {
|
||||
type: "apply_layer_style",
|
||||
layerGroupId,
|
||||
layerId,
|
||||
visible,
|
||||
fallbackText
|
||||
};
|
||||
}
|
||||
|
||||
if (action === "render_junctions") {
|
||||
const renderRef = stringValue(params.render_ref ?? params.renderRef);
|
||||
if (!renderRef || !RESULT_REF_PATTERN.test(renderRef)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "render_junctions",
|
||||
renderRef,
|
||||
fallbackText
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCenter(params: Record<string, unknown>): [number, number] | null {
|
||||
const sourceCrs = params.source_crs ?? params.sourceCrs;
|
||||
|
||||
if (Array.isArray(params.center) && params.center.length >= 2) {
|
||||
const lng = numberValue(params.center[0]);
|
||||
const lat = numberValue(params.center[1]);
|
||||
return lng === undefined || lat === undefined ? null : normalizeCenter(lng, lat, sourceCrs);
|
||||
}
|
||||
|
||||
const rawCoordinate = params.coordinate ?? params.coordinates;
|
||||
if (Array.isArray(rawCoordinate) && rawCoordinate.length >= 2) {
|
||||
const lng = numberValue(rawCoordinate[0]);
|
||||
const lat = numberValue(rawCoordinate[1]);
|
||||
return lng === undefined || lat === undefined ? null : normalizeCenter(lng, lat, sourceCrs);
|
||||
}
|
||||
|
||||
const lng = numberValue(params.lng ?? params.lon ?? params.longitude ?? params.x);
|
||||
const lat = numberValue(params.lat ?? params.latitude ?? params.y);
|
||||
return lng === undefined || lat === undefined ? null : normalizeCenter(lng, lat, sourceCrs);
|
||||
}
|
||||
|
||||
function normalizeCenter(x: number, y: number, sourceCrs: unknown): [number, number] {
|
||||
if (sourceCrs === "EPSG:3857") {
|
||||
return webMercatorToLngLat(x, y);
|
||||
}
|
||||
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
function webMercatorToLngLat(x: number, y: number): [number, number] {
|
||||
const lng = (x / WEB_MERCATOR_RADIUS) * (180 / Math.PI);
|
||||
const lat = (2 * Math.atan(Math.exp(y / WEB_MERCATOR_RADIUS)) - Math.PI / 2) * (180 / Math.PI);
|
||||
|
||||
return [lng, lat];
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function numberValue(value: unknown) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function booleanValue(value: unknown) {
|
||||
return typeof value === "boolean" ? value : undefined;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
const LEGACY_LOCATE_ACTION_LAYERS: Record<string, string> = {
|
||||
locate_junctions: "geo_junctions_mat",
|
||||
locate_pipes: "geo_pipes_mat",
|
||||
locate_valves: "geo_valves",
|
||||
locate_reservoirs: "geo_reservoirs",
|
||||
locate_scada: "geo_scada",
|
||||
locate_pumps: "geo_pumps",
|
||||
locate_tanks: "geo_tanks"
|
||||
};
|
||||
|
||||
const RESULT_REF_PATTERN = /^res-[A-Za-z0-9_-]{8,128}$/;
|
||||
const ALLOWED_LAYER_IDS = new Set(["junctions", "pipes", "valves", "reservoirs", "scada"]);
|
||||
const WEB_MERCATOR_RADIUS = 6378137;
|
||||
|
||||
const LOCATE_ID_PARAM_KEYS = [
|
||||
"feature_ids",
|
||||
"feature_id",
|
||||
"featureIds",
|
||||
"featureId",
|
||||
"ids",
|
||||
"id",
|
||||
"node_ids",
|
||||
"node_id",
|
||||
"junction_ids",
|
||||
"junction_id",
|
||||
"pipe_ids",
|
||||
"pipe_id",
|
||||
"valve_ids",
|
||||
"valve_id",
|
||||
"reservoir_ids",
|
||||
"reservoir_id",
|
||||
"scada_ids",
|
||||
"scada_id",
|
||||
"pump_ids",
|
||||
"pump_id",
|
||||
"tank_ids",
|
||||
"tank_id"
|
||||
] as const;
|
||||
|
||||
function readLocateIds(params: Record<string, unknown>) {
|
||||
for (const key of LOCATE_ID_PARAM_KEYS) {
|
||||
const value = params[key];
|
||||
const ids = normalizeIds(value);
|
||||
if (ids.length > 0) {
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeIds(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => String(item).trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
return String(value)
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user