feat: extend agent-driven map workbench

This commit is contained in:
2026-07-20 19:57:35 +08:00
parent 86e9b08235
commit 7fbd8a5618
63 changed files with 4506 additions and 457 deletions
+214
View File
@@ -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 };
}