"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Box, Typography, Chip, CircularProgress, IconButton, Tooltip, Button, Stack, Divider, Stepper, Step, StepLabel, StepContent, Paper, Alert, } from "@mui/material"; import { LocationOn as LocationIcon, Close as CloseIcon, CheckCircle as CheckCircleIcon, Search as SearchIcon, ExpandMore as ExpandMoreIcon, CheckBox as CheckBoxIcon, CheckBoxOutlineBlank as CheckBoxOutlineBlankIcon, } from "@mui/icons-material"; import { api } from "@/lib/api"; import { config, NETWORK_NAME } from "@config/config"; import { ValveIsolationResult } from "./types"; import { useNotification } from "@refinedev/core"; import { queryFeaturesByIds, handleMapClickSelectFeatures, } from "@/utils/mapQueryService"; import { useMap } from "@components/olmap/core/MapComponent"; import { GeoJSON } from "ol/format"; import VectorLayer from "ol/layer/Vector"; import VectorSource from "ol/source/Vector"; import { Circle as CircleStyle, Fill, Stroke, Style, Icon } from "ol/style"; import Feature, { FeatureLike } from "ol/Feature"; import { bbox, featureCollection, along, lineString, length, toMercator, } from "@turf/turf"; import { Point } from "ol/geom"; import { toLonLat } from "ol/proj"; interface ValveIsolationProps { initialPipeIds?: string[]; shouldFetch?: boolean; onFetchComplete?: () => void; loading?: boolean; result?: ValveIsolationResult | null; onLoadingChange?: (loading: boolean) => void; onResultChange?: (result: ValveIsolationResult | null) => void; } const ValveIsolation: React.FC = ({ initialPipeIds = [], shouldFetch = false, onFetchComplete, loading: externalLoading, result: externalResult, onLoadingChange, onResultChange, }) => { const [internalLoading, setInternalLoading] = useState(false); const [internalResult, setInternalResult] = useState(null); // 使用外部状态或内部状态 const loading = externalLoading !== undefined ? externalLoading : internalLoading; const result = externalResult !== undefined ? externalResult : internalResult; const setLoading = onLoadingChange || setInternalLoading; const setResult = onResultChange || setInternalResult; const [selectedPipeId, setSelectedPipeId] = useState(null); const [highlightFeature, setHighlightFeature] = useState(null); const [isSelecting, setIsSelecting] = useState(false); const [activeStep, setActiveStep] = useState(0); const [expandedResult, setExpandedResult] = useState(true); const [disabledValves, setDisabledValves] = useState([]); const { open } = useNotification(); const map = useMap(); const handleMapClick = useCallback( async (event: any) => { if (!isSelecting || !map) return; const feature = await handleMapClickSelectFeatures(event, map); if (feature) { const pipeId = feature.get("id"); if (pipeId) { // 确保是管道 const layerId = feature.getId()?.toString().split(".")[0] || ""; const isPipe = layerId.includes("pipe") || layerId.includes("Pipe"); if (!isPipe) { open?.({ type: "error", message: "请选择管道类型要素", }); return; } setSelectedPipeId(pipeId); setHighlightFeature(feature); setIsSelecting(false); setResult(null); // 清除旧结果 } } }, [isSelecting, map, open, setResult], ); useEffect(() => { if (!map) return; if (isSelecting) { map.on("click", handleMapClick); } else { map.un("click", handleMapClick); } return () => { map.un("click", handleMapClick); }; }, [map, isSelecting, handleMapClick]); const clearSelectedPipe = () => { setSelectedPipeId(null); setHighlightFeature(null); setHighlightFeatures([]); setResult?.(null); setActiveStep(0); setExpandedResult(false); setDisabledValves([]); }; const [highlightLayer, setHighlightLayer] = useState | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); const [highlightType, setHighlightType] = useState< "must_close" | "optional" | "affected_node" | "pipe" >("affected_node"); const handleLocatePipes = (pipeIds: string[], highlight: boolean = true) => { if (pipeIds.length > 0) { queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => { if (features.length > 0) { if (highlight) { // 设置高亮类型为管段 setHighlightType("pipe"); // 设置高亮要素 setHighlightFeatures(features); } // 将 OpenLayers Feature 转换为 GeoJSON Feature const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature), ); const extent = bbox(featureCollection(geojsonFeatures as any)); if (extent) { map?.getView().fit(extent, { maxZoom: 18, duration: 1000 }); } } }); } }; const handleLocateNodes = (nodeIds: string[]) => { if (nodeIds.length > 0) { queryFeaturesByIds(nodeIds, "geo_junctions").then((features) => { if (features.length > 0) { // 设置高亮类型为受影响节点 setHighlightType("affected_node"); // 设置高亮要素 setHighlightFeatures(features); // 将 OpenLayers Feature 转换为 GeoJSON Feature const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature), ); const extent = bbox(featureCollection(geojsonFeatures as any)); if (extent) { map?.getView().fit(extent, { maxZoom: 18, duration: 1000 }); } } }); } }; const handleLocateMustCloseValves = (valveIds: string[]) => { if (valveIds.length > 0) { queryFeaturesByIds(valveIds, "geo_valves").then((features) => { if (features.length > 0) { // 设置高亮类型为必关阀门 setHighlightType("must_close"); // 设置高亮要素 setHighlightFeatures(features); // 将 OpenLayers Feature 转换为 GeoJSON Feature const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature), ); const extent = bbox(featureCollection(geojsonFeatures as any)); if (extent) { map?.getView().fit(extent, { maxZoom: 18, duration: 1000 }); } } }); } }; const handleLocateOptionalValves = (valveIds: string[]) => { if (valveIds.length > 0) { queryFeaturesByIds(valveIds, "geo_valves").then((features) => { if (features.length > 0) { // 设置高亮类型为可选阀门 setHighlightType("optional"); // 设置高亮要素 setHighlightFeatures(features); // 将 OpenLayers Feature 转换为 GeoJSON Feature const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature), ); const extent = bbox(featureCollection(geojsonFeatures as any)); if (extent) { map?.getView().fit(extent, { maxZoom: 18, duration: 1000 }); } } }); } }; const fetchAnalysis = useCallback( async (ids: string[], disabled: string[] = []) => { if (!ids || ids.length === 0) { open?.({ type: "error", message: "请在地图上选择要分析的管段" }); return; } setLoading(true); const isExpandSearch = disabled.length > 0; if (!isExpandSearch) { setResult(null); setDisabledValves([]); } try { const params: any = { network: NETWORK_NAME, accident_element: ids, }; if (disabled.length > 0) { params.disabled_valves = disabled; } const response = await api.get( `${config.BACKEND_URL}/api/v1/valve_isolation_analysis/`, { params, paramsSerializer: { indexes: null, // 生成格式: accident_element=P1&accident_element=P2&disabled_valves=V1&disabled_valves=V2 }, }, ); setResult(response.data); if (!isExpandSearch) { setActiveStep(1); } else { setActiveStep(2); } open?.({ type: "success", message: isExpandSearch ? "扩大搜索成功" : "分析成功" }); } catch (error) { console.error(error); open?.({ type: "error", message: "分析失败", description: "无法获取关阀分析结果", }); } finally { setLoading(false); } }, [open, setLoading, setResult], ); // 监听外部传入的分析请求 useEffect(() => { if (shouldFetch && initialPipeIds.length > 0) { // 这里简单地取第一个作为 selectedPipeId,实际 fetchAnalysis 支持数组 setSelectedPipeId(initialPipeIds[0]); // 尝试获取Feature以高亮 (可选) queryFeaturesByIds(initialPipeIds, "geo_pipes_mat").then((features) => { if (features && features.length > 0) { setHighlightFeature(features[0]); } }); fetchAnalysis(initialPipeIds); if (onFetchComplete) { onFetchComplete(); } } }, [shouldFetch, initialPipeIds, fetchAnalysis, onFetchComplete]); // 初始化高亮图层 useEffect(() => { if (!map) return; // 动态样式函数,根据 highlightType 和 selectedPipeId 返回不同的样式 const getHighlightStyle = (feature: FeatureLike) => { // 如果是当前选择的爆管点feature if (highlightFeature && feature === highlightFeature) { const styles = []; // 线条样式(底层发光,主线条,内层高亮线) styles.push( new Style({ stroke: new Stroke({ color: "rgba(255, 0, 0, 0.3)", width: 14, }), }), new Style({ stroke: new Stroke({ color: "rgba(255, 0, 0, 1)", width: 7, lineDash: [15, 10], }), }), new Style({ stroke: new Stroke({ color: "rgba(255, 102, 102, 1)", width: 4, lineDash: [15, 10], }), }), ); const geometry = feature.getGeometry(); const lineCoords = geometry?.getType() === "LineString" ? (geometry as any).getCoordinates() : null; if (geometry && lineCoords) { const lineCoordsWGS84 = lineCoords.map((coord: []) => { const [lon, lat] = toLonLat(coord); return [lon, lat]; }); // 计算中点 const lineStringFeature = lineString(lineCoordsWGS84); const lineLength = length(lineStringFeature); const midPoint = along(lineStringFeature, lineLength / 2).geometry .coordinates; // 在中点添加 icon 样式 const midPointMercator = toMercator(midPoint); styles.push( new Style({ geometry: new Point(midPointMercator), image: new Icon({ src: "/icons/burst_pipe.svg", scale: 0.25, anchor: [0.5, 1], }), }), ); } return styles; } if (highlightType === "pipe") { // 管段 - 多层红色线条样式 + 中点图标 const styles = []; // 线条样式(底层发光,主线条,内层高亮线) styles.push( new Style({ stroke: new Stroke({ color: "rgba(255, 0, 0, 0.3)", width: 12, }), }), new Style({ stroke: new Stroke({ color: "rgba(255, 0, 0, 1)", width: 6, lineDash: [15, 10], }), }), new Style({ stroke: new Stroke({ color: "rgba(255, 102, 102, 1)", width: 3, lineDash: [15, 10], }), }), ); const geometry = feature.getGeometry(); const lineCoords = geometry?.getType() === "LineString" ? (geometry as any).getCoordinates() : null; if (geometry && lineCoords) { const lineCoordsWGS84 = lineCoords.map((coord: []) => { const [lon, lat] = toLonLat(coord); return [lon, lat]; }); // 计算中点 const lineStringFeature = lineString(lineCoordsWGS84); const lineLength = length(lineStringFeature); const midPoint = along(lineStringFeature, lineLength / 2).geometry .coordinates; // 在中点添加 icon 样式 const midPointMercator = toMercator(midPoint); styles.push( new Style({ geometry: new Point(midPointMercator), image: new Icon({ src: "/icons/burst_pipe.svg", scale: 0.2, anchor: [0.5, 1], }), }), ); } return styles; } // 阀门和节点的样式 let color: string; let strokeColor: string; let radius: number; switch (highlightType) { case "must_close": // 必关阀门 - 深红色 color = "rgba(211, 47, 47, 0.6)"; strokeColor = "rgba(211, 47, 47, 1)"; radius = 10; break; case "optional": // 可选阀门 - 橙色 color = "rgba(237, 108, 2, 0.6)"; strokeColor = "rgba(237, 108, 2, 1)"; radius = 10; break; case "affected_node": default: // 受影响节点 - 蓝色 color = "rgba(25, 118, 210, 0.6)"; strokeColor = "rgba(25, 118, 210, 1)"; radius = 8; break; } return new Style({ image: new CircleStyle({ radius: radius, fill: new Fill({ color: color, }), stroke: new Stroke({ color: strokeColor, width: 3, }), }), }); }; // 创建高亮图层 const highlightLayer = new VectorLayer({ source: new VectorSource(), style: getHighlightStyle, maxZoom: 24, minZoom: 12, properties: { name: "阀门节点高亮", value: "valve_node_highlight", }, }); map.addLayer(highlightLayer); setHighlightLayer(highlightLayer); return () => { map.removeLayer(highlightLayer); }; }, [map, highlightType, highlightFeature]); // 高亮要素的函数 useEffect(() => { if (!highlightLayer) { return; } const source = highlightLayer.getSource(); if (!source) { return; } // 清除之前的高亮 source.clear(); // 如果有选中的爆管点(pipe),优先添加到source if (highlightFeature) { // 设置一个特殊的属性来区分 highlightFeature.set('isHighlightPipe', true); source.addFeature(highlightFeature); } // 添加其他高亮要素 highlightFeatures.forEach((feature) => { if (feature instanceof Feature) { source.addFeature(feature); } }); }, [highlightFeatures, highlightLayer, highlightFeature]); // 切换不可用阀门的选择状态 const toggleDisabledValve = (valveId: string) => { setDisabledValves((prev) => { if (prev.includes(valveId)) { return prev.filter((id) => id !== valveId); } else { return [...prev, valveId]; } }); }; // 渲染结果卡片 const renderResultCard = (isExpanded: boolean = false, allowSelectDisabled: boolean = false) => { if (!result) return null; return ( {/* 状态信息 */} {isExpanded ? "扩大搜索结果" : "分析结果"} {/* 事故管段 */} 目标事故管段 {/* {result.accident_elements && result.accident_elements.length > 0 && ( handleLocatePipes(result.accident_elements!)} sx={{ color: "rgb(220, 38, 38)", padding: "2px" }} > )} */} {result.accident_elements?.map((pipeId, idx) => ( handleLocatePipes([pipeId], false)} sx={{ backgroundColor: "rgb(254, 242, 242)", border: "1px solid rgb(252, 165, 165)", color: "rgb(185, 28, 28)", fontWeight: 600, "&:hover": { backgroundColor: "rgb(254, 226, 226)", borderColor: "rgb(239, 68, 68)", }, }} /> ))} {/* 统计概览 */} {[ { label: "必关阀门", value: result.must_close_valves?.length || 0, color: "red", bgInfo: "from-red-50 to-red-100", textInfo: "text-red-700" }, { label: "可选阀门", value: result.optional_valves?.length || 0, color: "orange", bgInfo: "from-orange-50 to-orange-100", textInfo: "text-orange-700" }, { label: "影响节点", value: result.affected_nodes?.length || 0, color: "blue", bgInfo: "from-blue-50 to-blue-100", textInfo: "text-blue-700" }, ].map((item, index) => ( {item.value} {item.label} ))} {/* 必关阀门选择提示 - 只在流程2显示 */} {allowSelectDisabled && result.must_close_valves && result.must_close_valves.length > 0 && ( 可选择无法关闭的阀门 点击下方必关阀门列表中的阀门进行选择,已选择的阀门将在扩大搜索时作为不可用阀门处理 )} {/* 详细列表 - 可折叠 */} {expandedResult && ( {/* 必关阀门 */} {result.must_close_valves && result.must_close_valves.length > 0 && ( 必关阀门列表 ({result.must_close_valves.length}) {allowSelectDisabled && " - 点击勾选不可用阀门"} handleLocateMustCloseValves(result.must_close_valves!)} sx={{ color: "rgb(211, 47, 47)", backgroundColor: "rgba(211, 47, 47, 0.1)", "&:hover": { backgroundColor: "rgba(211, 47, 47, 0.2)", }, }} > {result.must_close_valves.map((valveId, idx) => { const isSelected = disabledValves.includes(valveId); return ( handleLocateMustCloseValves([valveId])} sx={{ "&:active": { transform: "scale(0.98)", }, }} > {valveId} {allowSelectDisabled && ( { e.stopPropagation(); toggleDisabledValve(valveId); }} className="cursor-pointer" > {isSelected ? ( ) : ( )} )} ); })} )} {/* 可选阀门 */} {result.optional_valves && result.optional_valves.length > 0 && ( 可选阀门列表 ({result.optional_valves.length}) handleLocateOptionalValves(result.optional_valves!)} sx={{ color: "rgb(237, 108, 2)", backgroundColor: "rgba(237, 108, 2, 0.1)", "&:hover": { backgroundColor: "rgba(237, 108, 2, 0.2)", }, }} > {result.optional_valves.map((valveId, idx) => ( handleLocateOptionalValves([valveId])} sx={{ "&:active": { transform: "scale(0.98)", }, }} > {valveId} ))} )} {/* 受影响节点 */} {result.affected_nodes && result.affected_nodes.length > 0 && ( 受影响节点 ({result.affected_nodes.length}) handleLocateNodes(result.affected_nodes!)} sx={{ color: "rgb(25, 118, 210)", backgroundColor: "rgba(25, 118, 210, 0.1)", "&:hover": { backgroundColor: "rgba(25, 118, 210, 0.2)", }, }} > {result.affected_nodes.map((nodeId, idx) => ( handleLocateNodes([nodeId])} sx={{ "&:active": { transform: "scale(0.98)", }, }} > {nodeId} ))} )} )} ); }; return ( {/* 流程区域 */} {/* 流程1:选择管段并分析 */} ( = 0 ? "bg-blue-600 text-white" : "bg-gray-300 text-gray-600" }`} > 1 )} > 选择管段并进行分析 {/* 选择管段 */} 选择爆管管段 {!isSelecting ? ( ) : ( )} {isSelecting && ( 💡 点击地图上的管道添加爆管点 )} {selectedPipeId ? ( {selectedPipeId} 已选择 ) : ( 暂未选择管段 )} {/* 操作按钮 */} {/* 流程2:查看分析结果 */} ( = 1 ? "bg-blue-600 text-white" : "bg-gray-300 text-gray-600" }`} > 2 )} > 查看分析结果 {loading ? ( 分析计算中,请稍候... ) : result ? ( <> {renderResultCard(false, true)} {/* 操作按钮 */} ) : ( 请先完成流程1的分析操作 )} {/* 流程3:扩大搜索结果 */} ( = 2 ? "bg-blue-600 text-white" : "bg-gray-300 text-gray-600" }`} > 3 )} > 扩大搜索结果(可选) {loading ? ( 扩大搜索中,请稍候... ) : activeStep >= 2 && result ? ( <> {renderResultCard(true, false)} {/* 最终结果提示 */} {result.isolatable ? ( ✓ 扩大搜索成功,找到可行的隔离方案 已标记 {disabledValves.length} 个阀门为不可用状态,可以按照上述新的阀门配置进行隔离操作 ) : ( ✗ 扩大搜索后仍无法完全隔离 即使排除了 {disabledValves.length} 个不可用阀门,仍无法找到有效隔离方案。建议检查管网拓扑结构或阀门配置 )} {/* 操作按钮 */} ) : ( 请先在流程2中选择不可用阀门,然后点击“扩大搜索”按钮 )} ); }; export default ValveIsolation;