Files
next-tjwater-frontend/src/features/workbench/map/map-control-config.ts
T

599 lines
19 KiB
TypeScript

import type { Map as MapLibreMap } from "maplibre-gl";
import type {
BaseLayerOption,
MapLayerControlItem,
MapLayerControlVisual,
MapLegendItem
} from "@/features/map/core";
import { MAP_STYLE_TOKENS } from "./map-colors";
import { SOURCE_GIS_MIN_ZOOM, SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
import {
AVAILABLE_WATER_NETWORK_SOURCE_IDS,
SUPPLY_LAYER_CATALOG,
type WaterNetworkSourceId
} from "./sources";
import { SCADA_ICON_PATHS, SCADA_LAYER_CONTROL_ITEMS, SCADA_LAYER_GROUPS } from "./scada";
import { SUPPLY_ASSET_ICON_PATHS } from "./supply-icons";
export const INITIAL_LAYER_VISIBILITY: Record<string, boolean> = {
...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]))
};
export type AnalysisOverlayLayer = {
id: string;
title: string;
description: string;
visible: boolean;
disabled?: boolean;
featureCount: number;
color?: string;
};
const SOURCE_CONTROL_LABELS = Object.fromEntries(
SUPPLY_LAYER_CATALOG.filter((layer) => layer.available).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: SUPPLY_LAYER_VISUALS.junctions.minzoom,
visual: { kind: "point", color: MAP_STYLE_TOKENS.supply.junction }
},
valves: {
description: "控制设施 · 符号自动避让",
minZoom: SUPPLY_LAYER_VISUALS.valves.minzoom,
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: SUPPLY_LAYER_VISUALS.pumps.minzoom,
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.pump }
},
tanks: {
description: "调蓄设施",
minZoom: SUPPLY_LAYER_VISUALS.tanks.minzoom,
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.tank }
},
water_quality_monitoring_points: {
description: "灵敏度分析选点",
minZoom: SUPPLY_LAYER_VISUALS.waterQuality.minzoom,
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.waterQuality }
},
fx_20200423_pipes: {
description: "全口径管线",
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_pipes,
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: SOURCE_GIS_MIN_ZOOM.fx_20200811_dn100_pipes,
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: SOURCE_GIS_MIN_ZOOM.fx_20200423_valves,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceValve }
},
fx_20200423_drain_valves: {
description: "源设施台账",
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_drain_valves,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceDrainValve }
},
fx_20200423_flow_meters: {
description: "源仪表台账",
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_flow_meters,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceFlowMeter }
},
fx_20200423_hydrants: {
description: "源设施台账",
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_hydrants,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceHydrant }
},
fx_20200423_service_connections: {
description: "源设施台账",
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_service_connections,
collection: "fx-water-assets-20200423",
collectionLabel: "2020-04-23 · 全网除头桥快照",
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceConnection }
},
fx_20200423_customer_meters: {
description: "源水表台账",
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_customer_meters,
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: SOURCE_GIS_MIN_ZOOM.fx_20200423_non_residential_meters,
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 及以上",
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
}
];
export function createVisibleMapLegendItems(
layerItems: readonly MapLayerControlItem[]
) {
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
),
...layerItems.filter((item) => item.group === "analysis" && item.visible).map((item) => ({
id: item.id,
label: item.label,
description: item.description,
color: item.visual?.color ?? MAP_STYLE_TOKENS.state.risk,
shape: "area" as const,
layerControlId: item.id,
...ANALYSIS_LEGEND_GROUP
}))
];
}
export const BASE_LAYER_GROUPS = {
"mapbox-light": ["mapbox-light"],
"mapbox-satellite": ["mapbox-satellite"],
"tianditu-vector": ["tianditu-vector", "tianditu-vector-annotation"],
"tianditu-image": ["tianditu-image", "tianditu-image-annotation"]
} as const;
export const BASE_LAYER_IDS = Object.values(BASE_LAYER_GROUPS).flat();
export function applyBaseLayerVisibility(map: MapLibreMap, activeBaseLayerId: string) {
const activeLayerIds = new Set<string>(
BASE_LAYER_GROUPS[activeBaseLayerId as keyof typeof BASE_LAYER_GROUPS] ?? []
);
BASE_LAYER_IDS.forEach((layerId) => {
if (map.getLayer(layerId)) {
map.setLayoutProperty(
layerId,
"visibility",
activeLayerIds.has(layerId) ? "visible" : "none"
);
}
});
}
export const BASE_LAYER_OPTIONS: BaseLayerOption[] = [
{
id: "mapbox-light",
label: "浅色",
description: "道路底图",
swatchClassName: "bg-gradient-to-br from-slate-50 via-slate-100 to-blue-100"
},
{
id: "mapbox-satellite",
label: "影像",
description: "卫星街道",
swatchClassName: "bg-gradient-to-br from-emerald-950 via-emerald-700 to-yellow-200"
},
{
id: "tianditu-vector",
label: "天地图矢量",
description: "矢量与中文注记",
swatchClassName: "bg-gradient-to-br from-sky-50 via-stone-100 to-emerald-100"
},
{
id: "tianditu-image",
label: "天地图影像",
description: "影像与中文注记",
swatchClassName: "bg-gradient-to-br from-slate-950 via-emerald-800 to-stone-300"
},
{
id: "none",
label: "无底图",
description: "仅业务图层",
swatchClassName: "bg-slate-100"
}
];
export function createLayerControlItems(
layerVisibility: Record<string, boolean>,
availableSourceIds: readonly WaterNetworkSourceId[] = AVAILABLE_WATER_NETWORK_SOURCE_IDS,
featureCounts: Partial<Record<WaterNetworkSourceId, number>> = {},
analysisOverlays: readonly AnalysisOverlayLayer[] = []
): 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 [
...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: item.minZoom,
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
})),
...analysisOverlays.map((overlay) => ({
id: overlay.id,
label: overlay.title,
description: overlay.description,
featureCount: overlay.featureCount,
visual: { kind: "area" as const, color: overlay.color ?? MAP_STYLE_TOKENS.state.risk },
visible: overlay.visible,
disabled: overlay.disabled,
group: "analysis",
groupLabel: "分析覆盖",
groupDescription: "Agent 生成的范围与分析成果"
}))
];
}
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()
.layers.filter((layer) => "source" in layer && layer.source === groupId)
.map((layer) => layer.id);
}
return [];
}
function isWaterNetworkSourceId(value: string): value is WaterNetworkSourceId {
return (AVAILABLE_WATER_NETWORK_SOURCE_IDS as readonly string[]).includes(value);
}