feat: extend agent-driven map workbench
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import type { Feature, FeatureCollection, Point } from "geojson";
|
||||
import type { ExpressionSpecification, Map as MapLibreMap, StyleSpecification } from "maplibre-gl";
|
||||
|
||||
export const SCADA_ANALYSIS_SOURCE_ID = "agent-scada-analysis";
|
||||
export const SCADA_ANALYSIS_LAYER_IDS = {
|
||||
casing: "agent-scada-analysis-casing",
|
||||
level: "agent-scada-analysis-level",
|
||||
indicator: "agent-scada-analysis-indicator",
|
||||
label: "agent-scada-analysis-label"
|
||||
} as const;
|
||||
|
||||
export const SCADA_ANALYSIS_LEVELS = ["high", "medium", "low", "unrated"] as const;
|
||||
export type ScadaAnalysisLevel = (typeof SCADA_ANALYSIS_LEVELS)[number];
|
||||
export type ScadaAnalysisItem = { sensor_id: string; level: ScadaAnalysisLevel };
|
||||
export type ScadaAnalysisLevelCounts = Record<ScadaAnalysisLevel, number>;
|
||||
export type ScadaAnalysisRenderResult = {
|
||||
rendered_ids: string[];
|
||||
missing_ids: string[];
|
||||
level_counts: ScadaAnalysisLevelCounts;
|
||||
fitted: true;
|
||||
};
|
||||
|
||||
export const SCADA_ANALYSIS_LEVEL_COLORS: Record<ScadaAnalysisLevel, string> = {
|
||||
high: "#DC2626",
|
||||
medium: "#F59E0B",
|
||||
low: "#16A34A",
|
||||
unrated: "#7C3AED"
|
||||
};
|
||||
|
||||
const SCADA_ANALYSIS_LABEL_COLORS: Record<ScadaAnalysisLevel, string> = {
|
||||
high: "#991B1B",
|
||||
medium: "#92400E",
|
||||
low: "#166534",
|
||||
unrated: "#5B21B6"
|
||||
};
|
||||
|
||||
const levelColorExpression = (
|
||||
colors: Record<ScadaAnalysisLevel, string>
|
||||
): ExpressionSpecification => [
|
||||
"match",
|
||||
["get", "analysis_level"],
|
||||
"high", colors.high,
|
||||
"medium", colors.medium,
|
||||
"low", colors.low,
|
||||
colors.unrated
|
||||
];
|
||||
|
||||
export const SCADA_ANALYSIS_LEVEL_LABELS: Record<ScadaAnalysisLevel, string> = {
|
||||
high: "高",
|
||||
medium: "中",
|
||||
low: "低",
|
||||
unrated: "未分级"
|
||||
};
|
||||
|
||||
const SCADA_ANALYSIS_SORT_PRIORITY: Record<ScadaAnalysisLevel, number> = {
|
||||
high: 0,
|
||||
medium: 1,
|
||||
low: 2,
|
||||
unrated: 3
|
||||
};
|
||||
|
||||
type Layer = NonNullable<StyleSpecification["layers"]>[number];
|
||||
|
||||
export const scadaAnalysisLayers: Layer[] = [
|
||||
{
|
||||
id: SCADA_ANALYSIS_LAYER_IDS.casing,
|
||||
type: "circle",
|
||||
source: SCADA_ANALYSIS_SOURCE_ID,
|
||||
paint: {
|
||||
"circle-radius": 16,
|
||||
"circle-color": "rgba(255,255,255,0)",
|
||||
"circle-stroke-color": "#FFFFFF",
|
||||
"circle-stroke-width": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
id: SCADA_ANALYSIS_LAYER_IDS.level,
|
||||
type: "circle",
|
||||
source: SCADA_ANALYSIS_SOURCE_ID,
|
||||
paint: {
|
||||
"circle-radius": 16,
|
||||
"circle-color": "rgba(255,255,255,0)",
|
||||
"circle-stroke-color": levelColorExpression(SCADA_ANALYSIS_LEVEL_COLORS),
|
||||
"circle-stroke-width": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
id: SCADA_ANALYSIS_LAYER_IDS.indicator,
|
||||
type: "circle",
|
||||
source: SCADA_ANALYSIS_SOURCE_ID,
|
||||
paint: {
|
||||
"circle-radius": 5,
|
||||
"circle-translate": [11, -11],
|
||||
"circle-color": levelColorExpression(SCADA_ANALYSIS_LEVEL_COLORS),
|
||||
"circle-stroke-color": "#FFFFFF",
|
||||
"circle-stroke-width": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id: SCADA_ANALYSIS_LAYER_IDS.label,
|
||||
type: "symbol",
|
||||
source: SCADA_ANALYSIS_SOURCE_ID,
|
||||
layout: {
|
||||
"symbol-sort-key": ["get", "sort_priority"],
|
||||
"text-field": ["get", "analysis_label"],
|
||||
"text-size": 15,
|
||||
"text-font": ["Noto Sans Regular"],
|
||||
"text-letter-spacing": 0.015,
|
||||
"text-offset": [0, -2.05],
|
||||
"text-anchor": "bottom",
|
||||
"text-allow-overlap": false,
|
||||
"text-ignore-placement": false
|
||||
},
|
||||
paint: {
|
||||
"text-color": levelColorExpression(SCADA_ANALYSIS_LABEL_COLORS),
|
||||
"text-halo-color": "#FFFFFF",
|
||||
"text-halo-width": 3,
|
||||
"text-halo-blur": 0.5
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export function ensureScadaAnalysisLayers(
|
||||
map: Pick<MapLibreMap, "addLayer" | "getLayer">
|
||||
): void {
|
||||
scadaAnalysisLayers.forEach((layer, index) => {
|
||||
if (map.getLayer(layer.id)) return;
|
||||
const beforeId = scadaAnalysisLayers
|
||||
.slice(index + 1)
|
||||
.map((candidate) => candidate.id)
|
||||
.find((candidateId) => Boolean(map.getLayer(candidateId)));
|
||||
map.addLayer(layer, beforeId);
|
||||
});
|
||||
}
|
||||
|
||||
export function createEmptyScadaAnalysisCollection(): FeatureCollection<Point> {
|
||||
return { type: "FeatureCollection", features: [] };
|
||||
}
|
||||
|
||||
export function parseScadaAnalysisItems(value: unknown): ScadaAnalysisItem[] | null {
|
||||
if (!Array.isArray(value) || value.length < 1 || value.length > 100) return null;
|
||||
const items: ScadaAnalysisItem[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const entry of value) {
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
||||
const keys = Object.keys(entry);
|
||||
if (keys.length !== 2 || !keys.includes("sensor_id") || !keys.includes("level")) return null;
|
||||
const { sensor_id: rawSensorId, level } = entry as Record<string, unknown>;
|
||||
const sensorId = typeof rawSensorId === "string" ? rawSensorId.trim() : "";
|
||||
if (
|
||||
!sensorId ||
|
||||
sensorId.length > 128 ||
|
||||
!SCADA_ANALYSIS_LEVELS.includes(level as ScadaAnalysisLevel) ||
|
||||
seen.has(sensorId)
|
||||
) return null;
|
||||
seen.add(sensorId);
|
||||
items.push({ sensor_id: sensorId, level: level as ScadaAnalysisLevel });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export function createScadaAnalysisCollection(
|
||||
collection: FeatureCollection,
|
||||
items: ScadaAnalysisItem[]
|
||||
): { collection: FeatureCollection<Point>; result: Omit<ScadaAnalysisRenderResult, "fitted"> } {
|
||||
const featuresBySensorId = new Map<string, Feature<Point>>();
|
||||
collection.features.forEach((feature) => {
|
||||
const sensorId = typeof feature.properties?.sensor_id === "string"
|
||||
? feature.properties.sensor_id.trim()
|
||||
: "";
|
||||
if (sensorId && feature.geometry?.type === "Point" && !featuresBySensorId.has(sensorId)) {
|
||||
featuresBySensorId.set(sensorId, feature as Feature<Point>);
|
||||
}
|
||||
});
|
||||
|
||||
const levelCounts = createEmptyScadaAnalysisLevelCounts();
|
||||
const renderedIds: string[] = [];
|
||||
const missingIds: string[] = [];
|
||||
const features: Array<Feature<Point>> = [];
|
||||
items.forEach((item) => {
|
||||
const feature = featuresBySensorId.get(item.sensor_id);
|
||||
if (!feature) {
|
||||
missingIds.push(item.sensor_id);
|
||||
return;
|
||||
}
|
||||
const trustedSensorId = String(feature.properties?.sensor_id ?? "").trim();
|
||||
if (!trustedSensorId) {
|
||||
missingIds.push(item.sensor_id);
|
||||
return;
|
||||
}
|
||||
renderedIds.push(trustedSensorId);
|
||||
levelCounts[item.level] += 1;
|
||||
features.push({
|
||||
...feature,
|
||||
properties: {
|
||||
...feature.properties,
|
||||
sensor_id: trustedSensorId,
|
||||
analysis_level: item.level,
|
||||
analysis_level_label: SCADA_ANALYSIS_LEVEL_LABELS[item.level],
|
||||
analysis_label: `${trustedSensorId} · ${SCADA_ANALYSIS_LEVEL_LABELS[item.level]}`,
|
||||
sort_priority: SCADA_ANALYSIS_SORT_PRIORITY[item.level]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
collection: { type: "FeatureCollection", features },
|
||||
result: { rendered_ids: renderedIds, missing_ids: missingIds, level_counts: levelCounts }
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyScadaAnalysisLevelCounts(): ScadaAnalysisLevelCounts {
|
||||
return { high: 0, medium: 0, low: 0, unrated: 0 };
|
||||
}
|
||||
Reference in New Issue
Block a user