From ef2b045306c4fffcc42cd5d9a59d3e1dd747d205 Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 16 Jul 2026 14:50:22 +0800 Subject: [PATCH] fix(map): show simulation pipe ids --- .../BurstLocation/AnalysisParameters.tsx | 28 ++- .../olmap/BurstLocation/LocationResults.tsx | 52 ++++ .../olmap/BurstLocation/SchemeQuery.tsx | 225 +++++++++++++++++- src/components/olmap/BurstLocation/types.ts | 1 + 4 files changed, 300 insertions(+), 6 deletions(-) diff --git a/src/components/olmap/BurstLocation/AnalysisParameters.tsx b/src/components/olmap/BurstLocation/AnalysisParameters.tsx index c8d955b..eefd57d 100644 --- a/src/components/olmap/BurstLocation/AnalysisParameters.tsx +++ b/src/components/olmap/BurstLocation/AnalysisParameters.tsx @@ -43,6 +43,7 @@ export interface SchemeItem { scheme_start_time: string; scheme_detail?: { modify_total_duration: number; + burst_ID?: string[] | string; }; } @@ -237,7 +238,24 @@ const AnalysisParameters: React.FC = ({ }, ); - onResult(response.data as BurstLocationResult); + const resultPayload = response.data as BurstLocationResult; + const selectedBurstIds = normalizeBurstIds(selectedScheme?.scheme_detail?.burst_ID); + onResult( + selectedBurstIds.length > 0 + ? { + ...resultPayload, + simulation_scheme: { + ...resultPayload.simulation_scheme, + name: resultPayload.simulation_scheme?.name ?? selectedScheme?.scheme_name, + type: resultPayload.simulation_scheme?.type ?? selectedScheme?.scheme_type, + burst_ids: + resultPayload.simulation_scheme?.burst_ids?.length + ? resultPayload.simulation_scheme.burst_ids + : selectedBurstIds, + }, + } + : resultPayload, + ); open?.({ key: "burst-location-analysis-success", type: "success", @@ -503,3 +521,11 @@ const AnalysisParameters: React.FC = ({ }; export default AnalysisParameters; + +const normalizeBurstIds = (value: string[] | string | undefined) => { + if (!value) return []; + const values = Array.isArray(value) ? value : [value]; + return Array.from( + new Set(values.map((item) => String(item).trim()).filter(Boolean)), + ); +}; diff --git a/src/components/olmap/BurstLocation/LocationResults.tsx b/src/components/olmap/BurstLocation/LocationResults.tsx index c654403..a697b4d 100644 --- a/src/components/olmap/BurstLocation/LocationResults.tsx +++ b/src/components/olmap/BurstLocation/LocationResults.tsx @@ -13,6 +13,7 @@ import { TableHead, TableRow, Button, + Link, } from "@mui/material"; import { FormatListBulleted, @@ -251,6 +252,7 @@ const LocationResults: React.FC = ({ result }) => { ); const sourceLabel = getDataSourceLabel(result); const normalDataDescription = getNormalDataDescription(result); + const simulationBurstIds = result.simulation_scheme?.burst_ids ?? []; return ( @@ -287,6 +289,7 @@ const LocationResults: React.FC = ({ result }) => { variant="outlined" startIcon={} onClick={() => locatePipes([result.located_pipe])} + disabled={!result.located_pipe} sx={{ height: 24, minWidth: 0, @@ -348,6 +351,55 @@ const LocationResults: React.FC = ({ result }) => { 爆管方案: {result.simulation_scheme.name} ) : null} + {simulationBurstIds.length > 0 ? ( + + + 模拟管段: + + {simulationBurstIds.map((pipeId) => ( + locatePipes([pipeId])} + title={pipeId} + sx={{ + maxWidth: 132, + color: "#7c3aed", + fontSize: "0.75rem", + fontWeight: 700, + lineHeight: "22px", + cursor: "pointer", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + textDecoration: "underline", + textUnderlineOffset: "2px", + "&:hover": { + color: "#5b21b6", + }, + }} + > + {pipeId} + + ))} + + + ) : null} diff --git a/src/components/olmap/BurstLocation/SchemeQuery.tsx b/src/components/olmap/BurstLocation/SchemeQuery.tsx index a3e29a4..c64cd58 100644 --- a/src/components/olmap/BurstLocation/SchemeQuery.tsx +++ b/src/components/olmap/BurstLocation/SchemeQuery.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Button, @@ -13,8 +13,12 @@ import { IconButton, Tooltip, Typography, + Link, } from "@mui/material"; -import { Info as InfoIcon } from "@mui/icons-material"; +import { + Info as InfoIcon, + LocationOn as LocationOnIcon, +} from "@mui/icons-material"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; @@ -24,6 +28,14 @@ import { useNotification } from "@refinedev/core"; import { api } from "@/lib/api"; import { NETWORK_NAME, config } from "@config/config"; import { useControllableObjectState } from "@components/olmap/core/useControllableState"; +import { useMap } from "@components/olmap/core/MapComponent"; +import { queryFeaturesByIds } from "@/utils/mapQueryService"; +import { GeoJSON } from "ol/format"; +import Feature from "ol/Feature"; +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; +import { Stroke, Style, Circle, Fill } from "ol/style"; +import { bbox, featureCollection } from "@turf/turf"; import { BurstLocationResult, BurstLocationSchemeDetail, @@ -43,6 +55,7 @@ export interface BurstLocationSchemeQueryState { queryAll: boolean; queryDate: Dayjs | null; expandedId: number | null; + simulationBurstIdsByName: Record; } export const createBurstLocationSchemeQueryState = @@ -50,6 +63,7 @@ export const createBurstLocationSchemeQueryState = queryAll: true, queryDate: dayjs(), expandedId: null, + simulationBurstIdsByName: {}, }); const SchemeQuery: React.FC = ({ @@ -60,12 +74,16 @@ const SchemeQuery: React.FC = ({ onStateChange, }) => { const { open } = useNotification(); + const map = useMap(); + const highlightLayerRef = useRef | null>(null); + const [highlightFeatures, setHighlightFeatures] = useState([]); const [queryState, , setQueryField] = useControllableObjectState( state, onStateChange, createBurstLocationSchemeQueryState(), ); const { queryAll, queryDate, expandedId } = queryState; + const simulationBurstIdsByName = queryState.simulationBurstIdsByName ?? {}; const [internalSchemes, setInternalSchemes] = useState([]); const [loading, setLoading] = useState(false); const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; @@ -81,6 +99,92 @@ const SchemeQuery: React.FC = ({ [schemes], ); + useEffect(() => { + if (!map) return; + + const layer = new VectorLayer({ + source: new VectorSource(), + style: new Style({ + stroke: new Stroke({ + color: "#a855f7", + width: 6, + }), + image: new Circle({ + radius: 8, + fill: new Fill({ color: "#a855f7" }), + stroke: new Stroke({ color: "#fff", width: 2 }), + }), + }), + properties: { + name: "爆管定位模拟管段高亮", + value: "burst_location_simulation_pipe_highlight", + }, + }); + map.addLayer(layer); + highlightLayerRef.current = layer; + + return () => { + highlightLayerRef.current = null; + map.removeLayer(layer); + }; + }, [map]); + + useEffect(() => { + const source = highlightLayerRef.current?.getSource(); + if (!source) return; + source.clear(); + highlightFeatures.forEach((feature) => source.addFeature(feature)); + }, [highlightFeatures]); + + const locatePipes = async (pipeIds: string[]) => { + const uniquePipeIds = Array.from(new Set(pipeIds.filter(Boolean))); + if (!uniquePipeIds.length || !map) return; + + try { + let features = await queryFeaturesByIds(uniquePipeIds, "geo_pipes_mat"); + if (features.length === 0) { + features = await queryFeaturesByIds(uniquePipeIds, "geo_pipes"); + } + if (features.length === 0) return; + + setHighlightFeatures(features); + const geojsonFormat = new GeoJSON(); + const geojsonFeatures = features.map((feature) => + geojsonFormat.writeFeatureObject(feature), + ); + // @ts-ignore turf typing with ol geojson objects + const extent = bbox(featureCollection(geojsonFeatures)); + map.getView().fit(extent, { + maxZoom: 19, + duration: 1000, + padding: [100, 100, 100, 100], + }); + } catch (error) { + console.error("Locate failed", error); + } + }; + + const getSimulationBurstIds = (payload?: BurstLocationResult) => { + const directIds = payload?.simulation_scheme?.burst_ids ?? []; + if (directIds.length > 0) return directIds; + const simulationSchemeName = payload?.simulation_scheme?.name; + return simulationSchemeName + ? simulationBurstIdsByName[simulationSchemeName] ?? [] + : []; + }; + + const enrichResultWithSimulationBurstIds = (payload: BurstLocationResult) => { + const simulationBurstIds = getSimulationBurstIds(payload); + if (simulationBurstIds.length === 0) return payload; + return { + ...payload, + simulation_scheme: { + ...payload.simulation_scheme, + burst_ids: simulationBurstIds, + }, + }; + }; + const buildDisplayResult = ( scheme: Pick, detail?: BurstLocationSchemeDetail, @@ -125,9 +229,30 @@ const SchemeQuery: React.FC = ({ params.query_date = queryDate.startOf("day").toISOString(); } - const response = await api.get(url, { params }); + const [response, simulationResponse] = await Promise.all([ + api.get(url, { params }), + api.get(`${config.BACKEND_URL}/api/v1/schemes`, { + params: { network: NETWORK_NAME }, + }), + ]); const nextSchemes = response.data as BurstSchemeRecord[]; - setSchemes(nextSchemes); + const nextSimulationBurstIdsByName = Object.fromEntries( + (simulationResponse.data as BurstSimulationSchemeItem[]) + .filter((scheme) => scheme.scheme_type === "burst_analysis") + .map((scheme) => [ + scheme.scheme_name, + normalizeBurstIds(scheme.scheme_detail?.burst_ID), + ]), + ); + setQueryField("simulationBurstIdsByName", nextSimulationBurstIdsByName); + setSchemes( + nextSchemes.map((scheme) => + enrichSchemeWithSimulationBurstIds( + scheme, + nextSimulationBurstIdsByName, + ), + ), + ); open?.({ type: "success", message: "查询成功", @@ -167,7 +292,7 @@ const SchemeQuery: React.FC = ({ if (!normalizedResult) { throw new Error("方案详情缺少定位结果数据"); } - onViewResult(normalizedResult); + onViewResult(enrichResultWithSimulationBurstIds(normalizedResult)); open?.({ type: "success", message: "方案加载成功", @@ -264,6 +389,7 @@ const SchemeQuery: React.FC = ({ const summary = scheme.scheme_detail?.result_summary; const payload = scheme.scheme_detail?.result_payload; const locatedPipe = payload?.located_pipe ?? summary?.located_pipe ?? "-"; + const simulationBurstIds = getSimulationBurstIds(payload); const leakage = payload?.burst_leakage ?? scheme.scheme_detail?.algorithm_params?.burst_leakage; @@ -340,6 +466,52 @@ const SchemeQuery: React.FC = ({ {locatedPipe} + {simulationBurstIds.length > 0 ? ( + + + 模拟管段: + + + {simulationBurstIds.map((pipeId) => ( + locatePipes([pipeId])} + title={pipeId} + sx={{ + maxWidth: 132, + color: "#7c3aed", + fontSize: "0.75rem", + fontWeight: 700, + lineHeight: "22px", + cursor: "pointer", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + textDecoration: "underline", + textUnderlineOffset: "2px", + "&:hover": { + color: "#5b21b6", + }, + }} + > + {pipeId} + + ))} + + locatePipes(simulationBurstIds)} + className="h-6 w-6 p-0" + > + + + + + + ) : null} 漏损量: @@ -383,3 +555,46 @@ const SchemeQuery: React.FC = ({ }; export default SchemeQuery; + +interface BurstSimulationSchemeItem { + scheme_name: string; + scheme_type: string; + scheme_detail?: { + burst_ID?: string[] | string; + }; +} + +const normalizeBurstIds = (value: string[] | string | undefined) => { + if (!value) return []; + const values = Array.isArray(value) ? value : [value]; + return Array.from( + new Set(values.map((item) => String(item).trim()).filter(Boolean)), + ); +}; + +const enrichSchemeWithSimulationBurstIds = ( + scheme: BurstSchemeRecord, + simulationBurstIdsByName: Record, +) => { + const payload = scheme.scheme_detail?.result_payload; + const simulationSchemeName = payload?.simulation_scheme?.name; + const simulationBurstIds = simulationSchemeName + ? simulationBurstIdsByName[simulationSchemeName] ?? [] + : []; + if (!payload || simulationBurstIds.length === 0) return scheme; + if (payload.simulation_scheme?.burst_ids?.length) return scheme; + + return { + ...scheme, + scheme_detail: { + ...scheme.scheme_detail, + result_payload: { + ...payload, + simulation_scheme: { + ...payload.simulation_scheme, + burst_ids: simulationBurstIds, + }, + }, + }, + }; +}; diff --git a/src/components/olmap/BurstLocation/types.ts b/src/components/olmap/BurstLocation/types.ts index 0500a36..498e73a 100644 --- a/src/components/olmap/BurstLocation/types.ts +++ b/src/components/olmap/BurstLocation/types.ts @@ -37,6 +37,7 @@ export interface BurstLocationResult { simulation_scheme?: { name?: string; type?: string; + burst_ids?: string[]; }; }