fix(map): show simulation pipe ids

This commit is contained in:
2026-07-16 14:50:22 +08:00
parent 209da0d295
commit ef2b045306
4 changed files with 300 additions and 6 deletions
@@ -43,6 +43,7 @@ export interface SchemeItem {
scheme_start_time: string; scheme_start_time: string;
scheme_detail?: { scheme_detail?: {
modify_total_duration: number; modify_total_duration: number;
burst_ID?: string[] | string;
}; };
} }
@@ -237,7 +238,24 @@ const AnalysisParameters: React.FC<Props> = ({
}, },
); );
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?.({ open?.({
key: "burst-location-analysis-success", key: "burst-location-analysis-success",
type: "success", type: "success",
@@ -503,3 +521,11 @@ const AnalysisParameters: React.FC<Props> = ({
}; };
export default AnalysisParameters; 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)),
);
};
@@ -13,6 +13,7 @@ import {
TableHead, TableHead,
TableRow, TableRow,
Button, Button,
Link,
} from "@mui/material"; } from "@mui/material";
import { import {
FormatListBulleted, FormatListBulleted,
@@ -251,6 +252,7 @@ const LocationResults: React.FC<Props> = ({ result }) => {
); );
const sourceLabel = getDataSourceLabel(result); const sourceLabel = getDataSourceLabel(result);
const normalDataDescription = getNormalDataDescription(result); const normalDataDescription = getNormalDataDescription(result);
const simulationBurstIds = result.simulation_scheme?.burst_ids ?? [];
return ( return (
<Box className="h-full overflow-auto p-1"> <Box className="h-full overflow-auto p-1">
@@ -287,6 +289,7 @@ const LocationResults: React.FC<Props> = ({ result }) => {
variant="outlined" variant="outlined"
startIcon={<LocationOnIcon />} startIcon={<LocationOnIcon />}
onClick={() => locatePipes([result.located_pipe])} onClick={() => locatePipes([result.located_pipe])}
disabled={!result.located_pipe}
sx={{ sx={{
height: 24, height: 24,
minWidth: 0, minWidth: 0,
@@ -348,6 +351,55 @@ const LocationResults: React.FC<Props> = ({ result }) => {
: {result.simulation_scheme.name} : {result.simulation_scheme.name}
</Typography> </Typography>
) : null} ) : null}
{simulationBurstIds.length > 0 ? (
<Box className="mt-1 flex flex-wrap items-center gap-1">
<Typography variant="caption" className="text-purple-600">
:
</Typography>
{simulationBurstIds.map((pipeId) => (
<Link
key={pipeId}
component="button"
variant="caption"
onClick={() => 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}
</Link>
))}
<Button
size="small"
variant="text"
startIcon={<LocationOnIcon />}
onClick={() => locatePipes(simulationBurstIds)}
sx={{
minWidth: 0,
px: 0.5,
py: 0,
color: "#7c3aed",
fontSize: "0.72rem",
}}
>
</Button>
</Box>
) : null}
</Box> </Box>
</Box> </Box>
@@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useMemo, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { import {
Box, Box,
Button, Button,
@@ -13,8 +13,12 @@ import {
IconButton, IconButton,
Tooltip, Tooltip,
Typography, Typography,
Link,
} from "@mui/material"; } 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 { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
@@ -24,6 +28,14 @@ import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config"; import { NETWORK_NAME, config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState"; 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 { import {
BurstLocationResult, BurstLocationResult,
BurstLocationSchemeDetail, BurstLocationSchemeDetail,
@@ -43,6 +55,7 @@ export interface BurstLocationSchemeQueryState {
queryAll: boolean; queryAll: boolean;
queryDate: Dayjs | null; queryDate: Dayjs | null;
expandedId: number | null; expandedId: number | null;
simulationBurstIdsByName: Record<string, string[]>;
} }
export const createBurstLocationSchemeQueryState = export const createBurstLocationSchemeQueryState =
@@ -50,6 +63,7 @@ export const createBurstLocationSchemeQueryState =
queryAll: true, queryAll: true,
queryDate: dayjs(), queryDate: dayjs(),
expandedId: null, expandedId: null,
simulationBurstIdsByName: {},
}); });
const SchemeQuery: React.FC<Props> = ({ const SchemeQuery: React.FC<Props> = ({
@@ -60,12 +74,16 @@ const SchemeQuery: React.FC<Props> = ({
onStateChange, onStateChange,
}) => { }) => {
const { open } = useNotification(); const { open } = useNotification();
const map = useMap();
const highlightLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [queryState, , setQueryField] = useControllableObjectState( const [queryState, , setQueryField] = useControllableObjectState(
state, state,
onStateChange, onStateChange,
createBurstLocationSchemeQueryState(), createBurstLocationSchemeQueryState(),
); );
const { queryAll, queryDate, expandedId } = queryState; const { queryAll, queryDate, expandedId } = queryState;
const simulationBurstIdsByName = queryState.simulationBurstIdsByName ?? {};
const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]); const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
@@ -81,6 +99,92 @@ const SchemeQuery: React.FC<Props> = ({
[schemes], [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 = ( const buildDisplayResult = (
scheme: Pick<BurstSchemeRecord, "scheme_name" | "username" | "create_time">, scheme: Pick<BurstSchemeRecord, "scheme_name" | "username" | "create_time">,
detail?: BurstLocationSchemeDetail, detail?: BurstLocationSchemeDetail,
@@ -125,9 +229,30 @@ const SchemeQuery: React.FC<Props> = ({
params.query_date = queryDate.startOf("day").toISOString(); 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[]; 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?.({ open?.({
type: "success", type: "success",
message: "查询成功", message: "查询成功",
@@ -167,7 +292,7 @@ const SchemeQuery: React.FC<Props> = ({
if (!normalizedResult) { if (!normalizedResult) {
throw new Error("方案详情缺少定位结果数据"); throw new Error("方案详情缺少定位结果数据");
} }
onViewResult(normalizedResult); onViewResult(enrichResultWithSimulationBurstIds(normalizedResult));
open?.({ open?.({
type: "success", type: "success",
message: "方案加载成功", message: "方案加载成功",
@@ -264,6 +389,7 @@ const SchemeQuery: React.FC<Props> = ({
const summary = scheme.scheme_detail?.result_summary; const summary = scheme.scheme_detail?.result_summary;
const payload = scheme.scheme_detail?.result_payload; const payload = scheme.scheme_detail?.result_payload;
const locatedPipe = payload?.located_pipe ?? summary?.located_pipe ?? "-"; const locatedPipe = payload?.located_pipe ?? summary?.located_pipe ?? "-";
const simulationBurstIds = getSimulationBurstIds(payload);
const leakage = const leakage =
payload?.burst_leakage ?? scheme.scheme_detail?.algorithm_params?.burst_leakage; payload?.burst_leakage ?? scheme.scheme_detail?.algorithm_params?.burst_leakage;
@@ -340,6 +466,52 @@ const SchemeQuery: React.FC<Props> = ({
{locatedPipe} {locatedPipe}
</Typography> </Typography>
</Box> </Box>
{simulationBurstIds.length > 0 ? (
<Box className="grid grid-cols-[78px_1fr] items-start gap-x-2">
<Typography variant="caption" className="mt-1 text-gray-600">
:
</Typography>
<Box className="flex flex-wrap gap-1">
{simulationBurstIds.map((pipeId) => (
<Link
key={pipeId}
component="button"
variant="caption"
onClick={() => 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}
</Link>
))}
<Tooltip title="定位全部模拟管段">
<IconButton
size="small"
color="secondary"
onClick={() => locatePipes(simulationBurstIds)}
className="h-6 w-6 p-0"
>
<LocationOnIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
) : null}
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2"> <Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600"> <Typography variant="caption" className="text-gray-600">
: :
@@ -383,3 +555,46 @@ const SchemeQuery: React.FC<Props> = ({
}; };
export default SchemeQuery; 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<string, string[]>,
) => {
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,
},
},
},
};
};
@@ -37,6 +37,7 @@ export interface BurstLocationResult {
simulation_scheme?: { simulation_scheme?: {
name?: string; name?: string;
type?: string; type?: string;
burst_ids?: string[];
}; };
} }