feat(workbench): integrate runtime data and map tooling

This commit is contained in:
2026-09-22 10:45:46 +08:00
parent bf44cc0f25
commit bebcf2a06c
163 changed files with 15642 additions and 1312 deletions
+363 -32
View File
@@ -1,13 +1,21 @@
import type { Map as MapLibreMap } from "maplibre-gl";
import type { BaseLayerOption, MapLayerControlItem, MapLegendItem } from "@/features/map/core";
import { GEOSERVER_WORKSPACE } from "./geoserver-config";
import type {
BaseLayerOption,
MapLayerControlItem,
MapLayerControlVisual,
MapLegendItem
} from "@/features/map/core";
import { MAP_STYLE_TOKENS } from "./map-colors";
import {
AVAILABLE_WATER_NETWORK_SOURCE_IDS,
SUPPLY_LAYER_CATALOG,
type WaterNetworkSourceId
} from "./sources";
import { SCADA_ICON_PATHS } from "./scada";
import {
SCADA_ICON_PATHS,
SCADA_LAYER_CONTROL_ITEMS,
SCADA_LAYER_GROUPS
} from "./scada";
import { SUPPLY_ASSET_ICON_PATHS } from "./supply-icons";
export const WORKBENCH_LAYER_GROUPS: Record<string, string[]> = {
@@ -24,39 +32,306 @@ export const WORKBENCH_LAYER_GROUPS: Record<string, string[]> = {
};
export const INITIAL_LAYER_VISIBILITY: Record<string, boolean> = {
...Object.fromEntries(AVAILABLE_WATER_NETWORK_SOURCE_IDS.map((sourceId) => [sourceId, true])),
...Object.fromEntries(
SUPPLY_LAYER_CATALOG
.filter((layer) => layer.id !== "scada")
.map((layer) => [layer.id, layer.initialVisible])
),
...Object.fromEntries(SCADA_LAYER_CONTROL_ITEMS.map((item) => [item.id, true])),
simulation: false
};
const SOURCE_CONTROL_LABELS = Object.fromEntries(
SUPPLY_LAYER_CATALOG
.filter((layer) => layer.available)
.map((layer) => [
layer.id,
{
label: layer.label,
description: layer.id === "scada"
? "后端业务 API"
: `${GEOSERVER_WORKSPACE}:${layer.sourceLayer}`
}
])
) as Record<WaterNetworkSourceId, { label: string; description: string }>;
.map((layer) => [layer.id, layer.label])
) as Record<WaterNetworkSourceId, string>;
type LayerControlPresentation = {
description: string;
minZoom?: number;
collection?: string;
collectionLabel?: string;
visual: MapLayerControlVisual;
};
const LAYER_CONTROL_PRESENTATION: Record<Exclude<WaterNetworkSourceId, "scada">, LayerControlPresentation> = {
pipes: {
description: "按管径分级",
minZoom: 8.5,
visual: {
kind: "line-stack",
colors: [
MAP_STYLE_TOKENS.supply.pipeMinor,
MAP_STYLE_TOKENS.supply.pipeMedium,
MAP_STYLE_TOKENS.supply.pipeMajor
]
}
},
junctions: {
description: "拓扑连接点",
minZoom: 15,
visual: { kind: "point", color: MAP_STYLE_TOKENS.supply.junction }
},
valves: {
description: "控制设施",
minZoom: 8.5,
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.valve }
},
reservoirs: {
description: "供水设施",
minZoom: 8.5,
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.reservoir }
},
pumps: {
description: "加压设施",
minZoom: 11,
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.pump }
},
tanks: {
description: "调蓄设施",
minZoom: 11,
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.tank }
},
water_quality_monitoring_points: {
description: "灵敏度分析选点",
minZoom: 8,
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.waterQuality }
},
fx_20200423_pipes: {
description: "全口径管线",
minZoom: 10,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: {
kind: "line-stack",
colors: [
MAP_STYLE_TOKENS.supply.sourcePipe,
MAP_STYLE_TOKENS.supply.sourcePipeMedium,
MAP_STYLE_TOKENS.supply.sourcePipeMajor
]
}
},
fx_20200811_dn100_pipes: {
description: "DN100 及以上",
minZoom: 9,
collection: "fx-pipe-inventory-20200811",
collectionLabel: "2020-08-11 · 专项管线台账",
visual: {
kind: "line",
color: MAP_STYLE_TOKENS.supply.sourcePipeInventory,
dashed: true
}
},
fx_20200423_valves: {
description: "源设施台账",
minZoom: 14,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceValve }
},
fx_20200423_drain_valves: {
description: "源设施台账",
minZoom: 10,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceDrainValve }
},
fx_20200423_flow_meters: {
description: "源仪表台账",
minZoom: 10,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceFlowMeter }
},
fx_20200423_hydrants: {
description: "源设施台账",
minZoom: 13,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceHydrant }
},
fx_20200423_service_connections: {
description: "源设施台账",
minZoom: 13,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceConnection }
},
fx_20200423_customer_meters: {
description: "源水表台账",
minZoom: 13,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceCustomerMeter }
},
fx_20200423_non_residential_meters: {
description: "源水表台账",
minZoom: 15,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceNonResidentialMeter }
}
};
const MODEL_PIPE_LEGEND_GROUP = {
group: "model-pipe",
groupLabel: "模型管线",
groupDescription: "颜色与线宽共同表示口径",
groupLayout: "list"
} as const;
const MODEL_FACILITY_LEGEND_GROUP = {
group: "model-facility",
groupLabel: "供水设施",
groupDescription: "拓扑节点与关键设施",
groupLayout: "grid"
} as const;
const MONITORING_LEGEND_GROUP = {
group: "monitoring",
groupLabel: "监测点",
groupDescription: "在线监测与水质选点",
groupLayout: "grid"
} as const;
const SOURCE_APRIL_PIPE_LEGEND_GROUP = {
group: "source-pipe-april",
groupLabel: "原始 GIS 管线 · 2020-04-23",
groupDescription: "全网快照,实线按已知口径分级",
groupLayout: "list"
} as const;
const SOURCE_AUGUST_PIPE_LEGEND_GROUP = {
group: "source-pipe-august",
groupLabel: "原始 GIS 管线 · 2020-08-11",
groupDescription: "DN100+ 专项台账,蓝色虚线",
groupLayout: "list"
} as const;
const SOURCE_FACILITY_LEGEND_GROUP = {
group: "source-facility",
groupLabel: "原始 GIS 设施",
groupDescription: "源设施与水表台账",
groupLayout: "grid"
} as const;
const ANALYSIS_LEGEND_GROUP = {
group: "analysis",
groupLabel: "分析覆盖",
groupDescription: "仅随空间分析成果显示",
groupLayout: "list"
} as const;
export const MAP_LEGEND_ITEMS: MapLegendItem[] = [
{ id: "major-pipe", label: "DN600 及以上管线", color: MAP_STYLE_TOKENS.supply.pipeMajor, shape: "line" },
{ id: "medium-pipe", label: "DN300-DN599 管线", color: MAP_STYLE_TOKENS.supply.pipeMedium, shape: "line" },
{ id: "minor-pipe", label: "DN300 以下管线", color: MAP_STYLE_TOKENS.supply.pipeMinor, shape: "line" },
{ id: "inactive-pipe", label: "停运管线", color: MAP_STYLE_TOKENS.state.inactive, shape: "dashed-line" },
{ id: "junction", label: "节点", color: MAP_STYLE_TOKENS.supply.junction, shape: "dot" },
{ id: "valve", label: "阀门", color: MAP_STYLE_TOKENS.supply.valve, imageSrc: SUPPLY_ASSET_ICON_PATHS.valve },
{ id: "reservoir", label: "水库", color: MAP_STYLE_TOKENS.supply.reservoir, imageSrc: SUPPLY_ASSET_ICON_PATHS.reservoir },
{ id: "pump", label: "水泵", color: MAP_STYLE_TOKENS.supply.pump, imageSrc: SUPPLY_ASSET_ICON_PATHS.pump },
{ id: "tank", label: "水箱", color: MAP_STYLE_TOKENS.supply.tank, imageSrc: SUPPLY_ASSET_ICON_PATHS.tank },
{ id: "scada-flow", label: "SCADA 流量", color: MAP_STYLE_TOKENS.supply.scadaFlow, imageSrc: SCADA_ICON_PATHS.flow },
{ id: "scada-pressure", label: "SCADA 压力", color: MAP_STYLE_TOKENS.supply.scadaPressure, imageSrc: SCADA_ICON_PATHS.pressure },
{ id: "impact-area", label: "影响范围", color: MAP_STYLE_TOKENS.state.incident, shape: "square" }
{
id: "major-pipe",
label: "DN600 及以上",
description: "主干管",
color: MAP_STYLE_TOKENS.supply.pipeMajor,
lineWidth: 5,
shape: "line",
layerControlId: "pipes",
...MODEL_PIPE_LEGEND_GROUP
},
{
id: "medium-pipe",
label: "DN300-DN599",
description: "次干管",
color: MAP_STYLE_TOKENS.supply.pipeMedium,
lineWidth: 3.5,
shape: "line",
layerControlId: "pipes",
...MODEL_PIPE_LEGEND_GROUP
},
{
id: "minor-pipe",
label: "DN300 以下",
description: "配水支管",
color: MAP_STYLE_TOKENS.supply.pipeMinor,
lineWidth: 2,
shape: "line",
layerControlId: "pipes",
...MODEL_PIPE_LEGEND_GROUP
},
{
id: "inactive-pipe",
label: "停运管线",
description: "虚线",
color: MAP_STYLE_TOKENS.state.inactive,
lineWidth: 2.5,
shape: "dashed-line",
layerControlId: "pipes",
...MODEL_PIPE_LEGEND_GROUP
},
{ id: "junction", label: "节点", color: MAP_STYLE_TOKENS.supply.junction, shape: "dot", layerControlId: "junctions", ...MODEL_FACILITY_LEGEND_GROUP },
{ id: "valve", label: "阀门", color: MAP_STYLE_TOKENS.supply.valve, imageSrc: SUPPLY_ASSET_ICON_PATHS.valve, layerControlId: "valves", ...MODEL_FACILITY_LEGEND_GROUP },
{ id: "reservoir", label: "水库", color: MAP_STYLE_TOKENS.supply.reservoir, imageSrc: SUPPLY_ASSET_ICON_PATHS.reservoir, layerControlId: "reservoirs", ...MODEL_FACILITY_LEGEND_GROUP },
{ id: "pump", label: "水泵", color: MAP_STYLE_TOKENS.supply.pump, imageSrc: SUPPLY_ASSET_ICON_PATHS.pump, layerControlId: "pumps", ...MODEL_FACILITY_LEGEND_GROUP },
{ id: "tank", label: "水箱", color: MAP_STYLE_TOKENS.supply.tank, imageSrc: SUPPLY_ASSET_ICON_PATHS.tank, layerControlId: "tanks", ...MODEL_FACILITY_LEGEND_GROUP },
{ id: "scada-flow", label: "流量监测", color: MAP_STYLE_TOKENS.supply.scadaFlow, imageSrc: SCADA_ICON_PATHS.flow, layerControlId: "scada-flow", ...MONITORING_LEGEND_GROUP },
{ id: "scada-pressure", label: "压力监测", color: MAP_STYLE_TOKENS.supply.scadaPressure, imageSrc: SCADA_ICON_PATHS.pressure, layerControlId: "scada-pressure", ...MONITORING_LEGEND_GROUP },
{ id: "water-quality-monitoring", label: "水质监测", color: MAP_STYLE_TOKENS.supply.waterQuality, imageSrc: SUPPLY_ASSET_ICON_PATHS.waterQuality, layerControlId: "water_quality_monitoring_points", ...MONITORING_LEGEND_GROUP },
{
id: "source-major-pipe",
label: "DN600 及以上",
color: MAP_STYLE_TOKENS.supply.sourcePipeMajor,
lineWidth: 5,
shape: "line",
layerControlId: "fx_20200423_pipes",
...SOURCE_APRIL_PIPE_LEGEND_GROUP
},
{
id: "source-medium-pipe",
label: "DN300-DN599",
color: MAP_STYLE_TOKENS.supply.sourcePipeMedium,
lineWidth: 3.5,
shape: "line",
layerControlId: "fx_20200423_pipes",
...SOURCE_APRIL_PIPE_LEGEND_GROUP
},
{
id: "source-minor-pipe",
label: "DN300 以下",
color: MAP_STYLE_TOKENS.supply.sourcePipe,
lineWidth: 2,
shape: "line",
layerControlId: "fx_20200423_pipes",
...SOURCE_APRIL_PIPE_LEGEND_GROUP
},
{
id: "source-unknown-pipe",
label: "未标注口径",
color: MAP_STYLE_TOKENS.state.inactive,
lineWidth: 2,
shape: "line",
layerControlId: "fx_20200423_pipes",
...SOURCE_APRIL_PIPE_LEGEND_GROUP
},
{
id: "source-inventory-pipe",
label: "DN100 及以上",
color: MAP_STYLE_TOKENS.supply.sourcePipeInventory,
lineWidth: 3,
shape: "dashed-line",
layerControlId: "fx_20200811_dn100_pipes",
...SOURCE_AUGUST_PIPE_LEGEND_GROUP
},
{ id: "source-valve", label: "GIS 阀门", color: MAP_STYLE_TOKENS.supply.sourceValve, imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceValve, layerControlId: "fx_20200423_valves", ...SOURCE_FACILITY_LEGEND_GROUP },
{ id: "source-drain-valve", label: "排水阀", color: MAP_STYLE_TOKENS.supply.sourceDrainValve, imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceDrainValve, layerControlId: "fx_20200423_drain_valves", ...SOURCE_FACILITY_LEGEND_GROUP },
{ id: "source-flow-meter", label: "流量仪", color: MAP_STYLE_TOKENS.supply.sourceFlowMeter, imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceFlowMeter, layerControlId: "fx_20200423_flow_meters", ...SOURCE_FACILITY_LEGEND_GROUP },
{ id: "source-hydrant", label: "消火栓", color: MAP_STYLE_TOKENS.supply.sourceHydrant, imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceHydrant, layerControlId: "fx_20200423_hydrants", ...SOURCE_FACILITY_LEGEND_GROUP },
{ id: "source-connection", label: "接水点", color: MAP_STYLE_TOKENS.supply.sourceConnection, imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceConnection, layerControlId: "fx_20200423_service_connections", ...SOURCE_FACILITY_LEGEND_GROUP },
{ id: "source-customer-meter", label: "用户水表", color: MAP_STYLE_TOKENS.supply.sourceCustomerMeter, imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceCustomerMeter, layerControlId: "fx_20200423_customer_meters", ...SOURCE_FACILITY_LEGEND_GROUP },
{ id: "source-non-residential-meter", label: "非居民水表", color: MAP_STYLE_TOKENS.supply.sourceNonResidentialMeter, imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceNonResidentialMeter, layerControlId: "fx_20200423_non_residential_meters", ...SOURCE_FACILITY_LEGEND_GROUP },
{ id: "impact-area", label: "影响范围", description: "事故或分析结果", color: MAP_STYLE_TOKENS.state.incident, shape: "area", layerControlId: "simulation", ...ANALYSIS_LEGEND_GROUP }
];
export function createVisibleMapLegendItems(
layerItems: readonly Pick<MapLayerControlItem, "id" | "visible">[]
) {
const visibleLayerIds = new Set(
layerItems.filter((item) => item.visible).map((item) => item.id)
);
return MAP_LEGEND_ITEMS.filter((item) =>
item.layerControlId ? visibleLayerIds.has(item.layerControlId) : true
);
}
export const BASE_LAYER_IDS = ["mapbox-light", "mapbox-satellite"] as const;
export function applyBaseLayerVisibility(map: MapLibreMap, activeBaseLayerId: string) {
@@ -94,19 +369,71 @@ export const BASE_LAYER_OPTIONS: BaseLayerOption[] = [
export function createLayerControlItems(
layerVisibility: Record<string, boolean>,
availableSourceIds: readonly WaterNetworkSourceId[] = AVAILABLE_WATER_NETWORK_SOURCE_IDS
availableSourceIds: readonly WaterNetworkSourceId[] = AVAILABLE_WATER_NETWORK_SOURCE_IDS,
featureCounts: Partial<Record<WaterNetworkSourceId, number>> = {}
): MapLayerControlItem[] {
const available = new Set<WaterNetworkSourceId>(availableSourceIds);
const catalogItems = SUPPLY_LAYER_CATALOG.filter(
(layer) => layer.available && layer.id !== "scada" && available.has(layer.id)
);
const modelItems = catalogItems.filter((layer) => layer.group === "model");
const monitoringItems = catalogItems.filter((layer) => layer.group === "monitoring");
const sourceGisItems = catalogItems.filter((layer) => layer.group === "source-gis");
return [
...availableSourceIds.map((sourceId) => ({
id: sourceId,
...SOURCE_CONTROL_LABELS[sourceId],
visible: layerVisibility[sourceId]
...modelItems.map((layer) => ({
id: layer.id,
label: SOURCE_CONTROL_LABELS[layer.id],
...LAYER_CONTROL_PRESENTATION[layer.id as Exclude<WaterNetworkSourceId, "scada">],
featureCount: featureCounts[layer.id],
visible: layerVisibility[layer.id],
group: "model",
groupLabel: "管网模型",
groupDescription: "计算模型与基础设施"
})),
...(available.has("scada") ? SCADA_LAYER_CONTROL_ITEMS.map((item) => ({
id: item.id,
label: item.label,
description: item.description,
minZoom: 8.5,
visual: {
kind: "image" as const,
imageSrc: item.id === "scada-pressure" ? SCADA_ICON_PATHS.pressure : SCADA_ICON_PATHS.flow
},
visible: layerVisibility[item.id],
group: "monitoring",
groupLabel: "监测点",
groupDescription: "在线监测与分析选点"
})) : []),
...monitoringItems.map((layer) => ({
id: layer.id,
label: SOURCE_CONTROL_LABELS[layer.id],
...LAYER_CONTROL_PRESENTATION[layer.id as Exclude<WaterNetworkSourceId, "scada">],
featureCount: featureCounts[layer.id],
visible: layerVisibility[layer.id],
group: "monitoring",
groupLabel: "监测点",
groupDescription: "在线监测与分析选点"
})),
...sourceGisItems.map((layer) => ({
id: layer.id,
label: SOURCE_CONTROL_LABELS[layer.id],
...LAYER_CONTROL_PRESENTATION[layer.id as Exclude<WaterNetworkSourceId, "scada">],
featureCount: featureCounts[layer.id],
visible: layerVisibility[layer.id],
group: "source-gis",
groupLabel: "GIS 原始数据",
groupDescription: "源数据快照与专项台账",
groupDefaultOpen: false
})),
{
id: "simulation",
label: "模拟结果",
description: "影响范围与事故标注",
visible: layerVisibility.simulation
visual: { kind: "area", color: MAP_STYLE_TOKENS.state.incident },
visible: layerVisibility.simulation,
group: "analysis",
groupLabel: "分析覆盖",
groupDescription: "随已打开的空间成果显示"
}
];
}
@@ -115,6 +442,10 @@ export function getWorkbenchLayerIds(
map: Pick<MapLibreMap, "getLayer" | "getStyle">,
groupId: string
) {
if (groupId in SCADA_LAYER_GROUPS) {
return SCADA_LAYER_GROUPS[groupId as keyof typeof SCADA_LAYER_GROUPS]
.filter((layerId) => map.getLayer(layerId));
}
if (isWaterNetworkSourceId(groupId)) {
return map
.getStyle()