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 = { ...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; type LayerControlPresentation = { description: string; minZoom?: number; collection?: string; collectionLabel?: string; visual: MapLayerControlVisual; }; const LAYER_CONTROL_PRESENTATION: Record< Exclude, 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( 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, availableSourceIds: readonly WaterNetworkSourceId[] = AVAILABLE_WATER_NETWORK_SOURCE_IDS, featureCounts: Partial> = {}, analysisOverlays: readonly AnalysisOverlayLayer[] = [] ): MapLayerControlItem[] { const available = new Set(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], 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], 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], 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, 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); }