129 lines
5.3 KiB
TypeScript
129 lines
5.3 KiB
TypeScript
import type { ExpressionSpecification, Map as MapLibreMap, StyleSpecification } from "maplibre-gl";
|
|
import { MAP_STYLE_TOKENS } from "./map-colors";
|
|
|
|
export const SCADA_SOURCE_ID = "scada";
|
|
export const SCADA_SOURCE_LAYER = "scada_points";
|
|
export const SCADA_HIT_LAYER_ID = "scada-hit";
|
|
|
|
export const SCADA_ICON_PATHS = {
|
|
waterQuality: "/icons/scada-water-quality.svg",
|
|
radarLevel: "/icons/scada-radar-level.svg",
|
|
ultrasonicFlow: "/icons/scada-ultrasonic-flow.svg",
|
|
unknown: "/icons/scada-unknown.svg"
|
|
} as const;
|
|
|
|
export const SCADA_MAP_ICON_PATHS = {
|
|
waterQuality: "/icons/scada-water-quality.png",
|
|
radarLevel: "/icons/scada-radar-level.png",
|
|
ultrasonicFlow: "/icons/scada-ultrasonic-flow.png",
|
|
unknown: "/icons/scada-unknown.png"
|
|
} as const;
|
|
|
|
export const SCADA_IMAGE_IDS = {
|
|
waterQuality: "scada-water-quality",
|
|
radarLevel: "scada-radar-level",
|
|
ultrasonicFlow: "scada-ultrasonic-flow",
|
|
unknown: "scada-unknown"
|
|
} as const;
|
|
|
|
export const SCADA_ICON_EXPRESSION: ExpressionSpecification = [
|
|
"match", ["get", "kind"],
|
|
"water_quality", SCADA_IMAGE_IDS.waterQuality,
|
|
"radar_level", SCADA_IMAGE_IDS.radarLevel,
|
|
"ultrasonic_flow", SCADA_IMAGE_IDS.ultrasonicFlow,
|
|
SCADA_IMAGE_IDS.unknown
|
|
];
|
|
|
|
const stateOpacity = (state: "hovered" | "selected"): ExpressionSpecification => [
|
|
"case", ["boolean", ["feature-state", state], false], 1, 0
|
|
];
|
|
|
|
const SCADA_ICON_SIZE = 0.75;
|
|
const SCADA_SELECTED_RADIUS = 12.5;
|
|
const SCADA_SELECTED_OUTER_STROKE_WIDTH = 6;
|
|
const SCADA_SELECTED_STROKE_WIDTH = 3;
|
|
|
|
type Layer = NonNullable<StyleSpecification["layers"]>[number];
|
|
|
|
export type ScadaImageRegistrationResult = {
|
|
status: "ready" | "degraded" | "unavailable";
|
|
failedImageIds: string[];
|
|
};
|
|
|
|
const scadaSymbolLayer: Layer = {
|
|
id: "scada-symbol", type: "symbol", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12,
|
|
layout: { "icon-image": SCADA_ICON_EXPRESSION, "icon-size": SCADA_ICON_SIZE, "icon-allow-overlap": true, "icon-ignore-placement": true }
|
|
};
|
|
|
|
const scadaFallbackLayer: Layer = {
|
|
id: "scada-fallback", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12,
|
|
paint: { "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 5, 20, 8], "circle-color": "#0E7490", "circle-stroke-color": "white", "circle-stroke-width": 2 }
|
|
};
|
|
|
|
const scadaInteractionLayers: Layer[] = [
|
|
{ id: "scada-hover", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": 14, "circle-color": "rgba(255,255,255,0)", "circle-stroke-color": "#0E7490", "circle-stroke-width": 2, "circle-opacity": stateOpacity("hovered"), "circle-stroke-opacity": stateOpacity("hovered") } },
|
|
{ id: "scada-selected-outer", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": SCADA_SELECTED_RADIUS, "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing, "circle-stroke-width": SCADA_SELECTED_OUTER_STROKE_WIDTH, "circle-opacity": stateOpacity("selected"), "circle-stroke-opacity": stateOpacity("selected") } },
|
|
{ id: "scada-selected", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": SCADA_SELECTED_RADIUS, "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-stroke-color": MAP_STYLE_TOKENS.state.selected, "circle-stroke-width": SCADA_SELECTED_STROKE_WIDTH, "circle-opacity": stateOpacity("selected"), "circle-stroke-opacity": stateOpacity("selected") } },
|
|
{ id: SCADA_HIT_LAYER_ID, type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": 16, "circle-color": "rgba(0,0,0,0)", "circle-opacity": 0 } }
|
|
];
|
|
|
|
export const scadaLayers: Layer[] = [
|
|
scadaSymbolLayer,
|
|
...scadaInteractionLayers
|
|
];
|
|
|
|
export const scadaFallbackLayers: Layer[] = [
|
|
scadaFallbackLayer,
|
|
...scadaInteractionLayers
|
|
];
|
|
|
|
export async function registerScadaImages(
|
|
map: Pick<MapLibreMap, "loadImage" | "addImage" | "hasImage">
|
|
): Promise<ScadaImageRegistrationResult> {
|
|
let unknown: Awaited<ReturnType<typeof loadImage>>;
|
|
|
|
try {
|
|
unknown = await loadImage(map, SCADA_MAP_ICON_PATHS.unknown);
|
|
addImageIfMissing(map, SCADA_IMAGE_IDS.unknown, unknown);
|
|
} catch {
|
|
return { status: "unavailable", failedImageIds: Object.values(SCADA_IMAGE_IDS) };
|
|
}
|
|
|
|
const failedImageIds: string[] = [];
|
|
for (const [key, path] of Object.entries(SCADA_MAP_ICON_PATHS)) {
|
|
if (key === "unknown") continue;
|
|
const imageId = SCADA_IMAGE_IDS[key as Exclude<keyof typeof SCADA_IMAGE_IDS, "unknown">];
|
|
if (map.hasImage(imageId)) continue;
|
|
|
|
try {
|
|
const image = await loadImage(map, path);
|
|
addImageIfMissing(map, imageId, image);
|
|
} catch {
|
|
failedImageIds.push(imageId);
|
|
try {
|
|
addImageIfMissing(map, imageId, unknown);
|
|
} catch {
|
|
return { status: "unavailable", failedImageIds };
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
status: failedImageIds.length > 0 ? "degraded" : "ready",
|
|
failedImageIds
|
|
};
|
|
}
|
|
|
|
async function loadImage(map: Pick<MapLibreMap, "loadImage">, path: string) {
|
|
const response = await map.loadImage(path);
|
|
return response.data;
|
|
}
|
|
|
|
function addImageIfMissing(
|
|
map: Pick<MapLibreMap, "addImage" | "hasImage">,
|
|
imageId: string,
|
|
image: Awaited<ReturnType<typeof loadImage>>
|
|
) {
|
|
if (!map.hasImage(imageId)) map.addImage(imageId, image, { pixelRatio: 2 });
|
|
}
|