import { useEffect, useState } from "react"; import { ChevronDown, Crosshair, RotateCcw, X, ZoomIn } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { cn } from "@/shared/ui/cn"; import { LAYER_GROUP_GEOMETRIES, type LayerGroupId, type LayerGroupStylePatch } from "../map/layer-group-style"; import { MAP_STYLE_TOKENS } from "../map/map-colors"; import { getNumericFeatureProperties } from "../map/value-label"; import type { FeatureTarget, WorkbenchMapCommands, WorkbenchMapControllerState } from "../map/workbench-map-controller"; import { AVAILABLE_WATER_NETWORK_SOURCE_IDS, type AvailableWaterNetworkSourceId } from "../map/sources"; import type { DetailFeature } from "../types"; type MapDevPanelProps = { commands: WorkbenchMapCommands; controllerState: WorkbenchMapControllerState; detailFeature: DetailFeature | null; onClose: () => void; }; const GROUP_LABELS: Record = { pipes: "管线", junctions: "节点", valves: "阀门", reservoirs: "水库", scada: "SCADA" }; const STYLE_GROUP_IDS = Object.keys(LAYER_GROUP_GEOMETRIES) as LayerGroupId[]; const LABEL_PRESETS: Record< LayerGroupId, { property: string; precision: number; unit: string; options: { value: string; label: string }[]; } > = { pipes: { property: "diameter", precision: 0, unit: "mm", options: [ { value: "diameter", label: "管径 diameter" }, { value: "length", label: "长度 length" }, { value: "roughness", label: "粗糙系数 roughness" }, { value: "minor_loss", label: "局部损失 minor_loss" } ] }, junctions: { property: "elevation", precision: 1, unit: "m", options: [ { value: "elevation", label: "高程 elevation" }, { value: "demand", label: "需水量 demand" } ] }, valves: { property: "diameter", precision: 0, unit: "mm", options: [ { value: "diameter", label: "管径 diameter" }, { value: "setting", label: "设定值 setting" }, { value: "minor_loss", label: "局部损失 minor_loss" } ] }, reservoirs: { property: "head", precision: 1, unit: "m", options: [{ value: "head", label: "水头 head" }] } }; const ERROR_LABELS: Record = { MAP_NOT_READY: "地图尚未就绪", FEATURE_NOT_FOUND: "目标要素不存在", WFS_UNAVAILABLE: "WFS 查询暂不可用", FLOW_UNAVAILABLE: "水流图层初始化失败", INVALID_FLOW_CALCULATION: "管道 ID 或有符号流速无效", INVALID_STYLE_PATCH: "参数不符合受控范围" }; export function MapDevPanel({ commands, controllerState, detailFeature, onClose }: MapDevPanelProps) { const [sourceId, setSourceId] = useState("pipes"); const [featureId, setFeatureId] = useState("440681"); const [property, setProperty] = useState("diameter"); const [flowPipeId, setFlowPipeId] = useState("440681"); const [flowVelocity, setFlowVelocity] = useState("-1"); const [precision, setPrecision] = useState(0); const [unit, setUnit] = useState("mm"); const [groupId, setGroupId] = useState("pipes"); const [showValueLabels, setShowValueLabels] = useState(true); const [showFeatureIds, setShowFeatureIds] = useState(false); const [labelSize, setLabelSize] = useState(12); const [labelColor, setLabelColor] = useState(MAP_STYLE_TOKENS.label.primary); const [draft, setDraft] = useState>({ color: MAP_STYLE_TOKENS.supply.pipeMedium, fillColor: MAP_STYLE_TOKENS.supply.junction, strokeColor: "#FFFFFF", opacity: 0.9, widthMultiplier: 1, radiusMultiplier: 1, strokeMultiplier: 1, iconMultiplier: 1, dash: "original" }); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [onClose]); const target: FeatureTarget = { sourceId, featureId: featureId.trim() }; const geometry = LAYER_GROUP_GEOMETRIES[groupId]; const activeLabel = controllerState.valueLabels[groupId]; const hasActiveLabels = Object.keys(controllerState.valueLabels).length > 0; function selectStyleGroup(nextGroupId: LayerGroupId) { const preset = LABEL_PRESETS[nextGroupId]; const saved = controllerState.valueLabels[nextGroupId]; setGroupId(nextGroupId); setProperty(saved?.property ?? preset.property); setPrecision(saved?.precision ?? preset.precision); setUnit(saved?.unit ?? preset.unit); setShowValueLabels(saved?.showValue ?? true); setShowFeatureIds(saved?.showId ?? false); setLabelSize(saved?.textSize ?? 12); setLabelColor(saved?.textColor ?? MAP_STYLE_TOKENS.label.primary); } function useSelectedFeature() { if (!detailFeature) return; if ( AVAILABLE_WATER_NETWORK_SOURCE_IDS.includes( detailFeature.layer as AvailableWaterNetworkSourceId ) ) { setSourceId(detailFeature.layer as AvailableWaterNetworkSourceId); } setFeatureId(detailFeature.id); if (detailFeature.layer === "pipes") setFlowPipeId(detailFeature.id); if (detailFeature.layer in LAYER_GROUP_GEOMETRIES) { const nextGroupId = detailFeature.layer as LayerGroupId; selectStyleGroup(nextGroupId); const allowedProperties = new Set( LABEL_PRESETS[nextGroupId].options.map((option) => option.value) ); const firstNumeric = getNumericFeatureProperties(detailFeature.properties).find( ([name]) => allowedProperties.has(name) ); if (firstNumeric) setProperty(firstNumeric[0]); } } function applyLayerStyle() { let patch: LayerGroupStylePatch; if (geometry === "line") { patch = { color: String(draft.color), opacity: Number(draft.opacity), widthMultiplier: Number(draft.widthMultiplier), dash: draft.dash as "original" | "solid" | "dashed" }; } else { patch = { fillColor: String(draft.fillColor), strokeColor: String(draft.strokeColor), opacity: Number(draft.opacity), radiusMultiplier: Number(draft.radiusMultiplier), strokeMultiplier: Number(draft.strokeMultiplier), iconMultiplier: Number(draft.iconMultiplier) }; } commands.applyLayerGroupStyle(groupId, patch); } function applyLabels() { if (showValueLabels || showFeatureIds) { void commands.showValueLabel({ sourceId: groupId, property, precision, unit, showValue: showValueLabels, showId: showFeatureIds, textSize: labelSize, textColor: labelColor }); } else { commands.clearValueLabel(groupId); } } function applyFlowCalculation() { commands.updatePipeFlowCalculation({ pipeId: flowPipeId.trim(), velocity: Number(flowVelocity) }); } const flowSummary = controllerState.flowSummary; const validFlowCalculation = flowPipeId.trim().length > 0 && Number.isFinite(Number(flowVelocity)); return ( ); } function PanelSection({ title, description, children }: { title: string; description: string; children: React.ReactNode; }) { return (

{title}

{description}

{children}
); } function Field({ label, children }: { label: string; children: React.ReactNode }) { return ( ); } function ToggleField({ label, checked, onChange }: { label: string; checked: boolean; onChange: (checked: boolean) => void; }) { return ( ); } function ColorField({ label, value, onChange }: { label: string; value: string; onChange: (value: string) => void; }) { return (
onChange(event.target.value.toUpperCase())} className="h-5 w-7 cursor-pointer border-0 bg-transparent p-0" /> {value}
); } function RangeField({ label, min, max, step, value, onChange }: { label: string; min: number; max: number; step: number; value: number; onChange: (value: number) => void; }) { return ( onChange(Number(event.target.value))} className="h-6 w-full accent-blue-600" /> ); } function StatusText({ children }: { children: React.ReactNode }) { return (

{children}

); } const inputClassName = "h-9 w-full rounded-lg border border-slate-200 bg-white/80 px-2.5 text-xs text-slate-800 outline-hidden transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/15";