import { useEffect, useState } from "react"; import { RotateCcw, X } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { Checkbox } from "@/shared/ui/checkbox"; import { Disclosure } from "@/shared/ui/disclosure"; import { FieldSelect } from "@/shared/ui/field-select"; import { Input } from "@/shared/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs"; 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 { WorkbenchMapCommands, WorkbenchMapControllerState } from "../map/workbench-map-controller"; import { SUPPLY_LAYER_CATALOG, type AvailableWaterNetworkSourceId } from "../map/sources"; import type { DetailFeature } from "../types"; import { INVESTIGATION_DEV_NETWORK } from "../utils/investigation-dev-network"; import { InvestigationDevTools, type InvestigationDevToolsProps } from "./investigation-dev-tools"; import { FeatureLocationPanel } from "./feature-location-panel"; import { FEATURE_LOCATION_ERROR_LABELS } from "../map/map-feature-feedback"; import type { ScadaAlertIntensity } from "./scada-alert-atmosphere"; import { NoticeDevTools } from "./notice-dev-tools"; type MapDevPanelProps = { commands: WorkbenchMapCommands; controllerState: WorkbenchMapControllerState; detailFeature: DetailFeature | null; flowDataSource?: "realtime" | "mock"; onClose: () => void; investigationFollow?: boolean; onInvestigationFollowChange?: (enabled: boolean) => void; investigationTools?: InvestigationDevToolsProps; onPreviewScadaAlert?: () => void; onEndScadaAlertPreview?: () => void; scadaAlertPreviewActive?: boolean; scadaAlertIntensity?: ScadaAlertIntensity; onScadaAlertIntensityChange?: (intensity: ScadaAlertIntensity) => void; }; const GROUP_LABELS = Object.fromEntries( SUPPLY_LAYER_CATALOG.map((layer) => [layer.id, layer.label]) ) as Record; 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: "base_demand", label: "基础需水量 base_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 = { ...FEATURE_LOCATION_ERROR_LABELS, FLOW_UNAVAILABLE: "水流图层初始化失败", INVALID_FLOW_CALCULATION: "管道 ID 或有符号流速无效", INVALID_STYLE_PATCH: "参数不符合受控范围" }; export function MapDevPanel({ commands, controllerState, detailFeature, flowDataSource = "mock", onClose, investigationFollow = true, onInvestigationFollowChange, investigationTools, onPreviewScadaAlert, onEndScadaAlertPreview, scadaAlertPreviewActive = false, scadaAlertIntensity = "standard", onScadaAlertIntensityChange }: MapDevPanelProps) { const [activeTab, setActiveTab] = useState(onInvestigationFollowChange ? "investigation" : "map"); const [locationDraft, setLocationDraft] = useState<{ sourceId: AvailableWaterNetworkSourceId; featureId: string }>({ sourceId: "pipes", featureId: INVESTIGATION_DEV_NETWORK.pipeIds[0] }); const { sourceId, featureId } = locationDraft; const featureIds = [...new Set(featureId.split(/[\s,,;;、]+/u).filter(Boolean))]; const [property, setProperty] = useState("diameter"); const [flowPipeId, setFlowPipeId] = useState(INVESTIGATION_DEV_NETWORK.pipeIds[0]); 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" && !event.defaultPrevented) onClose(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [onClose]); 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 (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, initiallyOpen = false, status, children }: { title: string; description: string; initiallyOpen?: boolean; status?: string; children: React.ReactNode; }) { return ( {title} {status ? ( {status} ) : null} } >

{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())} aria-label={label} className="h-10 w-10 shrink-0 cursor-pointer rounded border-0 bg-transparent p-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--action-blue)]" /> {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-10 w-full rounded accent-[var(--action-blue)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--action-blue)]" /> ); } function StatusText({ children, tone = "success" }: { children: React.ReactNode; tone?: "success" | "warning" }) { return (

{children}

); } const inputClassName = "h-10 w-full rounded-lg border border-input bg-background px-2.5 text-xs text-foreground transition-colors";