feat: initialize TJWater WebGIS frontend
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import type { ExpressionSpecification, Map as MapLibreMap, StyleSpecification } from "maplibre-gl";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import {
|
||||
createSupplyStateColor,
|
||||
supplyFacilityOpacityExpression,
|
||||
supplyIconOpacityExpression
|
||||
} from "./supply-style-expressions";
|
||||
|
||||
export const SCADA_SOURCE_ID = "scada";
|
||||
export const SCADA_SOURCE_LAYER = "geo_scada";
|
||||
export const SCADA_HIT_LAYER_ID = "scada-hit";
|
||||
|
||||
export const SCADA_ICON_PATHS = {
|
||||
flow: "/map/scada-flow.png",
|
||||
pressure: "/map/scada-pressure.png"
|
||||
} as const;
|
||||
|
||||
export const SCADA_MAP_ICON_PATHS = SCADA_ICON_PATHS;
|
||||
|
||||
export const SCADA_IMAGE_IDS = {
|
||||
flow: "scada-flow",
|
||||
pressure: "scada-pressure"
|
||||
} as const;
|
||||
|
||||
export const SCADA_ICON_EXPRESSION: ExpressionSpecification = [
|
||||
"match", ["downcase", ["to-string", ["coalesce", ["get", "type"], ""]]],
|
||||
"pipe_flow", SCADA_IMAGE_IDS.flow,
|
||||
"pressure", SCADA_IMAGE_IDS.pressure,
|
||||
SCADA_IMAGE_IDS.pressure
|
||||
];
|
||||
|
||||
const stateOpacity = (state: "hovered" | "selected"): ExpressionSpecification => [
|
||||
"case",
|
||||
state === "selected"
|
||||
? ["any", ["boolean", ["feature-state", "selected"], false], ["boolean", ["feature-state", "highlighted"], false]]
|
||||
: ["boolean", ["feature-state", state], false],
|
||||
1,
|
||||
0
|
||||
];
|
||||
|
||||
const scadaColor: ExpressionSpecification = [
|
||||
"match", ["downcase", ["to-string", ["coalesce", ["get", "type"], ""]]],
|
||||
"pipe_flow", MAP_STYLE_TOKENS.supply.scadaFlow,
|
||||
MAP_STYLE_TOKENS.supply.scadaPressure
|
||||
];
|
||||
|
||||
type Layer = NonNullable<StyleSpecification["layers"]>[number];
|
||||
|
||||
export type ScadaImageRegistrationResult = {
|
||||
status: "ready" | "degraded" | "unavailable";
|
||||
failedImageIds: string[];
|
||||
};
|
||||
|
||||
const scadaCasingLayer: Layer = {
|
||||
id: "scada-casing", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
paint: {
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.scada.haloRadius,
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-opacity": 0.94
|
||||
}
|
||||
};
|
||||
|
||||
const scadaFillLayer: Layer = {
|
||||
id: "scada-fill", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
paint: {
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.scada.radius,
|
||||
"circle-color": createSupplyStateColor(scadaColor),
|
||||
"circle-opacity": supplyFacilityOpacityExpression
|
||||
}
|
||||
};
|
||||
|
||||
const scadaSymbolLayer: Layer = {
|
||||
id: "scada-symbol", type: "symbol", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
layout: { "icon-image": SCADA_ICON_EXPRESSION, "icon-size": SUPPLY_LAYER_VISUALS.scada.iconSize, "icon-allow-overlap": true, "icon-ignore-placement": true },
|
||||
paint: { "icon-opacity": supplyIconOpacityExpression }
|
||||
};
|
||||
|
||||
const scadaFallbackLayer: Layer = {
|
||||
id: "scada-fallback", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
paint: {
|
||||
"circle-radius": ["interpolate", ["linear"], ["zoom"], 9, 1.7, 12, 2.1, 16, 2.7, 20, 3.4, 24, 4.2],
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-opacity": supplyIconOpacityExpression
|
||||
}
|
||||
};
|
||||
|
||||
export const scadaInteractionLayers: Layer[] = [
|
||||
{ id: "scada-hover", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom, paint: { "circle-radius": SUPPLY_LAYER_VISUALS.scada.hoverRadius, "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-stroke-color": scadaColor, "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: SUPPLY_LAYER_VISUALS.scada.minzoom, paint: { "circle-radius": SUPPLY_LAYER_VISUALS.scada.selectedRadius, "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing, "circle-stroke-width": 5, "circle-opacity": stateOpacity("selected"), "circle-stroke-opacity": stateOpacity("selected") } },
|
||||
{ id: "scada-selected", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom, paint: { "circle-radius": SUPPLY_LAYER_VISUALS.scada.selectedRadius, "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-stroke-color": MAP_STYLE_TOKENS.state.selected, "circle-stroke-width": 2.5, "circle-opacity": stateOpacity("selected"), "circle-stroke-opacity": stateOpacity("selected") } }
|
||||
];
|
||||
|
||||
const scadaHitLayer: Layer = {
|
||||
id: SCADA_HIT_LAYER_ID, type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
paint: { "circle-radius": SUPPLY_LAYER_VISUALS.scada.hitRadius, "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-opacity": 0 }
|
||||
};
|
||||
|
||||
export const scadaBusinessLayers: Layer[] = [
|
||||
scadaCasingLayer,
|
||||
scadaFillLayer,
|
||||
scadaSymbolLayer
|
||||
];
|
||||
|
||||
export const scadaFallbackBusinessLayers: Layer[] = [
|
||||
scadaCasingLayer,
|
||||
scadaFillLayer,
|
||||
scadaFallbackLayer
|
||||
];
|
||||
|
||||
export const scadaHitLayers: Layer[] = [scadaHitLayer];
|
||||
|
||||
export const scadaLayers: Layer[] = [
|
||||
...scadaBusinessLayers,
|
||||
...scadaInteractionLayers,
|
||||
scadaHitLayer
|
||||
];
|
||||
|
||||
export const scadaFallbackLayers: Layer[] = [
|
||||
...scadaFallbackBusinessLayers,
|
||||
...scadaInteractionLayers,
|
||||
scadaHitLayer
|
||||
];
|
||||
|
||||
export async function registerScadaImages(
|
||||
map: Pick<MapLibreMap, "loadImage" | "addImage" | "hasImage">
|
||||
): Promise<ScadaImageRegistrationResult> {
|
||||
let pressure: Awaited<ReturnType<typeof loadImage>>;
|
||||
|
||||
try {
|
||||
pressure = await loadImage(map, SCADA_MAP_ICON_PATHS.pressure);
|
||||
addImageIfMissing(map, SCADA_IMAGE_IDS.pressure, pressure);
|
||||
} 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 === "pressure") continue;
|
||||
const imageId = SCADA_IMAGE_IDS[key as Exclude<keyof typeof SCADA_IMAGE_IDS, "pressure">];
|
||||
if (map.hasImage(imageId)) continue;
|
||||
|
||||
try {
|
||||
const image = await loadImage(map, path);
|
||||
addImageIfMissing(map, imageId, image);
|
||||
} catch {
|
||||
failedImageIds.push(imageId);
|
||||
try {
|
||||
addImageIfMissing(map, imageId, pressure);
|
||||
} 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 });
|
||||
}
|
||||
Reference in New Issue
Block a user