feat: extend agent-driven map workbench
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { DRAINAGE_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
|
||||
export type LineLayerGroupStylePatch = {
|
||||
color?: string;
|
||||
opacity?: number;
|
||||
widthMultiplier?: number;
|
||||
dash?: "original" | "solid" | "dashed";
|
||||
};
|
||||
|
||||
export type PointLayerGroupStylePatch = {
|
||||
fillColor?: string;
|
||||
strokeColor?: string;
|
||||
opacity?: number;
|
||||
radiusMultiplier?: number;
|
||||
strokeMultiplier?: number;
|
||||
};
|
||||
|
||||
export type ScadaLayerGroupStylePatch = PointLayerGroupStylePatch & {
|
||||
iconMultiplier?: number;
|
||||
};
|
||||
|
||||
export type LayerGroupStylePatch = LineLayerGroupStylePatch | PointLayerGroupStylePatch | ScadaLayerGroupStylePatch;
|
||||
export type LayerGroupGeometry = "line" | "point" | "scada";
|
||||
|
||||
export const LAYER_GROUP_GEOMETRIES = {
|
||||
conduits: "line",
|
||||
junctions: "point",
|
||||
orifices: "line",
|
||||
outfalls: "point",
|
||||
pumps: "line",
|
||||
scada: "scada"
|
||||
} as const satisfies Record<string, LayerGroupGeometry>;
|
||||
|
||||
export type LayerGroupId = keyof typeof LAYER_GROUP_GEOMETRIES;
|
||||
|
||||
const ORIGINAL_DASH: Partial<Record<LayerGroupId, number[] | undefined>> = {
|
||||
conduits: undefined,
|
||||
orifices: [4, 2],
|
||||
pumps: [6, 2, 1.5, 2]
|
||||
};
|
||||
|
||||
const LINE_PREFIX: Record<"conduits" | "orifices" | "pumps", string> = {
|
||||
conduits: "pipes",
|
||||
orifices: "orifices",
|
||||
pumps: "pumps"
|
||||
};
|
||||
|
||||
export function validateLayerGroupStylePatch(groupId: string, patch: unknown): patch is LayerGroupStylePatch {
|
||||
if (!(groupId in LAYER_GROUP_GEOMETRIES) || !patch || typeof patch !== "object" || Array.isArray(patch)) return false;
|
||||
const geometry = LAYER_GROUP_GEOMETRIES[groupId as LayerGroupId];
|
||||
const values = patch as Record<string, unknown>;
|
||||
const allowed = geometry === "line"
|
||||
? ["color", "opacity", "widthMultiplier", "dash"]
|
||||
: geometry === "scada"
|
||||
? ["fillColor", "strokeColor", "opacity", "radiusMultiplier", "strokeMultiplier", "iconMultiplier"]
|
||||
: ["fillColor", "strokeColor", "opacity", "radiusMultiplier", "strokeMultiplier"];
|
||||
if (Object.keys(values).some((key) => !allowed.includes(key))) return false;
|
||||
if ("color" in values && !isColor(values.color)) return false;
|
||||
if ("fillColor" in values && !isColor(values.fillColor)) return false;
|
||||
if ("strokeColor" in values && !isColor(values.strokeColor)) return false;
|
||||
if ("opacity" in values && !inRange(values.opacity, 0, 1)) return false;
|
||||
if ("widthMultiplier" in values && !inRange(values.widthMultiplier, 0.5, 3)) return false;
|
||||
if ("radiusMultiplier" in values && !inRange(values.radiusMultiplier, 0.5, 3)) return false;
|
||||
if ("strokeMultiplier" in values && !inRange(values.strokeMultiplier, 0.5, 3)) return false;
|
||||
if ("iconMultiplier" in values && !inRange(values.iconMultiplier, 0.5, 2)) return false;
|
||||
if ("dash" in values && !["original", "solid", "dashed"].includes(String(values.dash))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function applyLayerGroupStyle(map: MapLibreMap, groupId: LayerGroupId, patch: LayerGroupStylePatch) {
|
||||
const geometry = LAYER_GROUP_GEOMETRIES[groupId];
|
||||
if (geometry === "line") applyLineStyle(map, groupId as "conduits" | "orifices" | "pumps", patch as LineLayerGroupStylePatch);
|
||||
else if (geometry === "scada") applyScadaStyle(map, patch as ScadaLayerGroupStylePatch);
|
||||
else applyPointStyle(map, groupId as "junctions" | "outfalls", patch as PointLayerGroupStylePatch);
|
||||
}
|
||||
|
||||
export function resetLayerGroupStyle(map: MapLibreMap, groupId: LayerGroupId) {
|
||||
const geometry = LAYER_GROUP_GEOMETRIES[groupId];
|
||||
if (geometry === "line") resetLineStyle(map, groupId as "conduits" | "orifices" | "pumps");
|
||||
else if (geometry === "scada") resetScadaStyle(map);
|
||||
else resetPointStyle(map, groupId as "junctions" | "outfalls");
|
||||
}
|
||||
|
||||
function applyLineStyle(map: MapLibreMap, groupId: "conduits" | "orifices" | "pumps", patch: LineLayerGroupStylePatch) {
|
||||
const prefix = LINE_PREFIX[groupId];
|
||||
const coreId = groupId === "conduits" ? "pipes-flow" : groupId;
|
||||
if (patch.color !== undefined) setPaint(map, coreId, "line-color", stateColor(patch.color));
|
||||
if (patch.opacity !== undefined) setPaint(map, coreId, "line-opacity", patch.opacity);
|
||||
if (patch.dash !== undefined) {
|
||||
const dash = patch.dash === "solid" ? [1, 0] : patch.dash === "dashed" ? [3, 2] : ORIGINAL_DASH[groupId];
|
||||
setPaint(map, coreId, "line-dasharray", dash);
|
||||
}
|
||||
if (patch.widthMultiplier !== undefined) {
|
||||
const visuals = DRAINAGE_LAYER_VISUALS[groupId];
|
||||
setPaint(map, coreId, "line-width", scale(visuals.width, patch.widthMultiplier));
|
||||
setPaint(map, `${prefix}-casing`, "line-width", scale(visuals.casingWidth, patch.widthMultiplier));
|
||||
setPaint(map, `${prefix}-hover-outline`, "line-width", scale(visuals.hoverOutlineWidth, patch.widthMultiplier));
|
||||
setPaint(map, `${prefix}-hover`, "line-width", scale(visuals.hoverWidth, patch.widthMultiplier));
|
||||
for (const id of [`${prefix}-selected-outer`, `${prefix}-selected-outline`, `${prefix}-selected-separator`]) {
|
||||
setPaint(map, id, "line-gap-width", scale(visuals.selectedWidth, patch.widthMultiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetLineStyle(map: MapLibreMap, groupId: "conduits" | "orifices" | "pumps") {
|
||||
const prefix = LINE_PREFIX[groupId];
|
||||
const coreId = groupId === "conduits" ? "pipes-flow" : groupId;
|
||||
const visuals = DRAINAGE_LAYER_VISUALS[groupId];
|
||||
const colors = {
|
||||
conduits: MAP_STYLE_TOKENS.asset.conduit,
|
||||
orifices: MAP_STYLE_TOKENS.asset.orifice,
|
||||
pumps: MAP_STYLE_TOKENS.asset.pump
|
||||
} as const;
|
||||
setPaint(map, coreId, "line-color", stateColor(colors[groupId]));
|
||||
setPaint(map, coreId, "line-opacity", MAP_STYLE_TOKENS.opacity.core);
|
||||
setPaint(map, coreId, "line-dasharray", ORIGINAL_DASH[groupId]);
|
||||
setPaint(map, coreId, "line-width", visuals.width);
|
||||
setPaint(map, `${prefix}-casing`, "line-width", visuals.casingWidth);
|
||||
setPaint(map, `${prefix}-hover-outline`, "line-width", visuals.hoverOutlineWidth);
|
||||
setPaint(map, `${prefix}-hover`, "line-width", visuals.hoverWidth);
|
||||
for (const id of [`${prefix}-selected-outer`, `${prefix}-selected-outline`, `${prefix}-selected-separator`]) {
|
||||
setPaint(map, id, "line-gap-width", visuals.selectedWidth);
|
||||
}
|
||||
}
|
||||
|
||||
function applyPointStyle(map: MapLibreMap, groupId: "junctions" | "outfalls", patch: PointLayerGroupStylePatch) {
|
||||
const coreId = groupId;
|
||||
if (patch.fillColor !== undefined) setPaint(map, coreId, "circle-color", stateColor(patch.fillColor));
|
||||
if (patch.strokeColor !== undefined) setPaint(map, coreId, "circle-stroke-color", stateColor(patch.strokeColor));
|
||||
if (patch.opacity !== undefined) setPaint(map, coreId, "circle-opacity", patch.opacity);
|
||||
if (patch.radiusMultiplier !== undefined) {
|
||||
const visual = DRAINAGE_LAYER_VISUALS[groupId];
|
||||
setPaint(map, coreId, "circle-radius", scale(visual.radius, patch.radiusMultiplier));
|
||||
setPaint(map, `${groupId}-halo`, "circle-radius", scale(visual.haloRadius, patch.radiusMultiplier));
|
||||
setPaint(map, `${groupId}-hover`, "circle-radius", scale(visual.hoverRadius, patch.radiusMultiplier));
|
||||
setPaint(map, `${groupId}-selected`, "circle-radius", scale(visual.selectedRadius, patch.radiusMultiplier));
|
||||
setPaint(map, `${groupId}-selected-outer`, "circle-radius", scale(visual.selectedRadius, patch.radiusMultiplier));
|
||||
}
|
||||
if (patch.strokeMultiplier !== undefined) {
|
||||
setPaint(map, coreId, "circle-stroke-width", 2 * patch.strokeMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
function resetPointStyle(map: MapLibreMap, groupId: "junctions" | "outfalls") {
|
||||
const visual = DRAINAGE_LAYER_VISUALS[groupId];
|
||||
const colors = {
|
||||
junctions: MAP_STYLE_TOKENS.asset.junction,
|
||||
outfalls: MAP_STYLE_TOKENS.asset.outfall
|
||||
} as const;
|
||||
setPaint(map, groupId, "circle-color", groupId === "outfalls" ? MAP_STYLE_TOKENS.canvas.casing : stateColor(colors[groupId]));
|
||||
setPaint(map, groupId, "circle-stroke-color", groupId === "outfalls" ? stateColor(colors[groupId]) : undefined);
|
||||
setPaint(map, groupId, "circle-opacity", groupId === "outfalls" ? 1 : MAP_STYLE_TOKENS.opacity.core);
|
||||
setPaint(map, groupId, "circle-radius", visual.radius);
|
||||
setPaint(map, `${groupId}-halo`, "circle-radius", visual.haloRadius);
|
||||
setPaint(map, `${groupId}-hover`, "circle-radius", visual.hoverRadius);
|
||||
setPaint(map, `${groupId}-selected`, "circle-radius", visual.selectedRadius);
|
||||
setPaint(map, `${groupId}-selected-outer`, "circle-radius", visual.selectedRadius);
|
||||
setPaint(map, groupId, "circle-stroke-width", groupId === "outfalls" ? 2 : undefined);
|
||||
}
|
||||
|
||||
function applyScadaStyle(map: MapLibreMap, patch: ScadaLayerGroupStylePatch) {
|
||||
if (patch.iconMultiplier !== undefined) setLayout(map, "scada-symbol", "icon-size", 0.75 * patch.iconMultiplier);
|
||||
if (patch.opacity !== undefined) {
|
||||
setPaint(map, "scada-symbol", "icon-opacity", patch.opacity);
|
||||
setPaint(map, "scada-fallback", "circle-opacity", patch.opacity);
|
||||
}
|
||||
if (patch.fillColor !== undefined) setPaint(map, "scada-fallback", "circle-color", patch.fillColor);
|
||||
if (patch.strokeColor !== undefined) setPaint(map, "scada-fallback", "circle-stroke-color", patch.strokeColor);
|
||||
if (patch.radiusMultiplier !== undefined) setPaint(map, "scada-fallback", "circle-radius", scale(["interpolate", ["linear"], ["zoom"], 12, 5, 20, 8], patch.radiusMultiplier));
|
||||
if (patch.strokeMultiplier !== undefined) setPaint(map, "scada-fallback", "circle-stroke-width", 2 * patch.strokeMultiplier);
|
||||
}
|
||||
|
||||
function resetScadaStyle(map: MapLibreMap) {
|
||||
setLayout(map, "scada-symbol", "icon-size", 0.75);
|
||||
setPaint(map, "scada-symbol", "icon-opacity", 1);
|
||||
setPaint(map, "scada-fallback", "circle-color", "#0E7490");
|
||||
setPaint(map, "scada-fallback", "circle-stroke-color", "#FFFFFF");
|
||||
setPaint(map, "scada-fallback", "circle-opacity", 1);
|
||||
setPaint(map, "scada-fallback", "circle-radius", ["interpolate", ["linear"], ["zoom"], 12, 5, 20, 8]);
|
||||
setPaint(map, "scada-fallback", "circle-stroke-width", 2);
|
||||
}
|
||||
|
||||
function scale(value: unknown, multiplier: number): unknown {
|
||||
return ["*", value, multiplier];
|
||||
}
|
||||
|
||||
function stateColor(color: string): unknown {
|
||||
return [
|
||||
"case",
|
||||
["in", ["downcase", ["to-string", ["coalesce", ["get", "status"], ""]]], ["literal", ["closed", "off", "inactive"]]],
|
||||
MAP_STYLE_TOKENS.state.inactive,
|
||||
color
|
||||
];
|
||||
}
|
||||
|
||||
function setPaint(map: MapLibreMap, layerId: string, property: string, value: unknown) {
|
||||
if (map.getLayer(layerId)) map.setPaintProperty(layerId, property, (value ?? null) as never);
|
||||
}
|
||||
|
||||
function setLayout(map: MapLibreMap, layerId: string, property: string, value: unknown) {
|
||||
if (map.getLayer(layerId)) map.setLayoutProperty(layerId, property, value as never);
|
||||
}
|
||||
|
||||
function inRange(value: unknown, min: number, max: number): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
||||
}
|
||||
|
||||
function isColor(value: unknown): value is string {
|
||||
return typeof value === "string" && /^#[\da-f]{6}$/i.test(value);
|
||||
}
|
||||
Reference in New Issue
Block a user