492 lines
14 KiB
TypeScript
492 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
import {
|
|
Box,
|
|
TextField,
|
|
Button,
|
|
Typography,
|
|
IconButton,
|
|
Stack,
|
|
} from "@mui/material";
|
|
import { Close as CloseIcon } from "@mui/icons-material";
|
|
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
|
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
|
import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales";
|
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
|
import "dayjs/locale/zh-cn"; // 引入中文包
|
|
import dayjs, { Dayjs } from "dayjs";
|
|
import { useMap } from "@app/OlMap/MapComponent";
|
|
import VectorLayer from "ol/layer/Vector";
|
|
import VectorSource from "ol/source/Vector";
|
|
import { Style, Stroke, Icon } from "ol/style";
|
|
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
|
import Feature, { FeatureLike } from "ol/Feature";
|
|
import { useNotification } from "@refinedev/core";
|
|
import axios from "axios";
|
|
import { config, NETWORK_NAME } from "@/config/config";
|
|
import { along, lineString, length, toMercator } from "@turf/turf";
|
|
import { Point } from "ol/geom";
|
|
import { toLonLat } from "ol/proj";
|
|
|
|
interface PipePoint {
|
|
id: string;
|
|
diameter: number;
|
|
area: number;
|
|
feature?: any; // 存储管道要素用于高亮
|
|
}
|
|
|
|
const AnalysisParameters: React.FC = () => {
|
|
const map = useMap();
|
|
const { open } = useNotification();
|
|
|
|
const [pipePoints, setPipePoints] = useState<PipePoint[]>([]);
|
|
const [startTime, setStartTime] = useState<Dayjs | null>(dayjs(new Date()));
|
|
const [duration, setDuration] = useState<number>(3600);
|
|
const [schemeName, setSchemeName] = useState<string>(
|
|
"FANGAN" + new Date().getTime(),
|
|
);
|
|
const [network, setNetwork] = useState<string>(NETWORK_NAME);
|
|
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
|
|
|
const [highlightLayer, setHighlightLayer] =
|
|
useState<VectorLayer<VectorSource> | null>(null);
|
|
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
|
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
|
|
|
// 检查是否所有必要参数都已填写
|
|
const isFormValid =
|
|
pipePoints.length > 0 &&
|
|
startTime !== null &&
|
|
duration > 0 &&
|
|
schemeName.trim() !== "";
|
|
|
|
// 初始化管道图层和高亮图层
|
|
useEffect(() => {
|
|
if (!map) return;
|
|
|
|
const burstPipeStyle = function (feature: FeatureLike) {
|
|
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) {
|
|
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;
|
|
};
|
|
// 创建高亮图层
|
|
const highlightLayer = new VectorLayer({
|
|
source: new VectorSource(),
|
|
style: burstPipeStyle,
|
|
properties: {
|
|
name: "高亮管道",
|
|
value: "highlight_pipeline",
|
|
},
|
|
});
|
|
|
|
map.addLayer(highlightLayer);
|
|
setHighlightLayer(highlightLayer);
|
|
|
|
return () => {
|
|
map.removeLayer(highlightLayer);
|
|
map.un("click", handleMapClickSelectFeatures);
|
|
};
|
|
}, [map]);
|
|
// 高亮要素的函数
|
|
useEffect(() => {
|
|
if (!highlightLayer) {
|
|
return;
|
|
}
|
|
const source = highlightLayer.getSource();
|
|
if (!source) {
|
|
return;
|
|
}
|
|
// 清除之前的高亮
|
|
source.clear();
|
|
// 添加新的高亮要素
|
|
highlightFeatures.forEach((feature) => {
|
|
if (feature instanceof Feature) {
|
|
source.addFeature(feature);
|
|
}
|
|
});
|
|
}, [highlightFeatures]);
|
|
|
|
// 同步高亮要素和爆管点信息
|
|
useEffect(() => {
|
|
setPipePoints((prevPipes) => {
|
|
// 移除不在highlightFeatures中的
|
|
const filtered = prevPipes.filter((pipe) =>
|
|
highlightFeatures.some(
|
|
(feature) => feature.getProperties().id === pipe.id,
|
|
),
|
|
);
|
|
// 添加新的
|
|
const newPipes = highlightFeatures
|
|
.filter(
|
|
(feature) =>
|
|
!filtered.some((p) => p.id === feature.getProperties().id),
|
|
)
|
|
.map((feature) => {
|
|
const properties = feature.getProperties();
|
|
return {
|
|
id: properties.id,
|
|
diameter: properties.diameter || 0,
|
|
area: 15,
|
|
feature: feature,
|
|
};
|
|
});
|
|
return [...filtered, ...newPipes];
|
|
});
|
|
}, [highlightFeatures]);
|
|
|
|
// 地图点击选择要素事件处理函数
|
|
const handleMapClickSelectFeatures = useCallback(
|
|
async (event: { coordinate: number[] }) => {
|
|
if (!map) return;
|
|
const feature = await mapClickSelectFeatures(event, map);
|
|
const layer = feature?.getId()?.toString().split(".")[0];
|
|
|
|
if (!feature) return;
|
|
if (
|
|
feature.getGeometry()?.getType() === "Point" ||
|
|
(layer !== "geo_pipes_mat" && layer !== "geo_pipes")
|
|
) {
|
|
// 点类型几何不处理
|
|
open?.({
|
|
type: "error",
|
|
message: "请选择线类型管道要素。",
|
|
});
|
|
return;
|
|
}
|
|
const featureId = feature.getProperties().id;
|
|
setHighlightFeatures((prev) => {
|
|
const existingIndex = prev.findIndex(
|
|
(f) => f.getProperties().id === featureId,
|
|
);
|
|
if (existingIndex !== -1) {
|
|
// 如果已存在,移除
|
|
return prev.filter((_, i) => i !== existingIndex);
|
|
} else {
|
|
// 如果不存在,添加
|
|
return [...prev, feature];
|
|
}
|
|
});
|
|
},
|
|
[map],
|
|
);
|
|
|
|
// 开始选择管道
|
|
const handleStartSelection = () => {
|
|
if (!map) return;
|
|
setIsSelecting(true);
|
|
// 注册点击事件
|
|
map.on("click", handleMapClickSelectFeatures);
|
|
};
|
|
|
|
// 结束选择管道
|
|
const handleEndSelection = () => {
|
|
if (!map) return;
|
|
|
|
setIsSelecting(false);
|
|
|
|
// 移除点击事件
|
|
map.un("click", handleMapClickSelectFeatures);
|
|
};
|
|
|
|
const handleRemovePipe = (id: string) => {
|
|
// 从高亮features中移除
|
|
setHighlightFeatures((prev) =>
|
|
prev.filter((f) => f.getProperties().id !== id),
|
|
);
|
|
};
|
|
|
|
const handleAreaChange = (id: string, value: string) => {
|
|
const numValue = parseFloat(value) || 0;
|
|
setPipePoints((prev) =>
|
|
prev.map((pipe) => (pipe.id === id ? { ...pipe, area: numValue } : pipe)),
|
|
);
|
|
};
|
|
|
|
const handleAnalyze = async () => {
|
|
setAnalyzing(true);
|
|
|
|
// 显示处理中的通知
|
|
open?.({
|
|
key: "burst-analysis",
|
|
type: "progress",
|
|
message: "方案提交分析中",
|
|
undoableTimeout: 3,
|
|
});
|
|
|
|
const burst_ID = pipePoints.map((pipe) => pipe.id);
|
|
const burst_size = pipePoints.map((pipe) =>
|
|
parseInt(pipe.area.toString(), 10),
|
|
);
|
|
// 格式化开始时间,去除秒部分
|
|
const modify_pattern_start_time = startTime
|
|
? startTime.format("YYYY-MM-DDTHH:mm:00Z")
|
|
: "";
|
|
const modify_total_duration = duration;
|
|
const params = {
|
|
network: network,
|
|
modify_pattern_start_time: modify_pattern_start_time,
|
|
burst_ID: burst_ID,
|
|
burst_size: burst_size,
|
|
modify_total_duration: modify_total_duration,
|
|
scheme_name: schemeName,
|
|
};
|
|
|
|
try {
|
|
await axios.get(`${config.BACKEND_URL}/api/v1/burst_analysis/`, {
|
|
params,
|
|
});
|
|
// 更新弹窗为成功状态
|
|
open?.({
|
|
key: "burst-analysis",
|
|
type: "success",
|
|
message: "方案分析成功",
|
|
description: "方案分析完成,请在方案查询中查看结果。",
|
|
});
|
|
} catch (error) {
|
|
console.error("分析请求失败:", error);
|
|
|
|
// 更新弹窗为失败状态
|
|
open?.({
|
|
key: "burst-analysis",
|
|
type: "error",
|
|
message: "提交分析失败",
|
|
description:
|
|
error instanceof Error ? error.message : "请检查网络连接或稍后重试",
|
|
});
|
|
} finally {
|
|
setAnalyzing(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box className="flex flex-col h-full">
|
|
{/* 选择爆管点 */}
|
|
<Box className="mb-4">
|
|
<Box className="flex items-center justify-between mb-2">
|
|
<Typography variant="subtitle2" className="font-medium">
|
|
选择爆管点
|
|
</Typography>
|
|
{/* 开始/结束选择按钮 */}
|
|
{!isSelecting ? (
|
|
<Button
|
|
variant="outlined"
|
|
size="small"
|
|
onClick={handleStartSelection}
|
|
className="border-blue-500 text-blue-600 hover:bg-blue-50"
|
|
startIcon={
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
|
/>
|
|
</svg>
|
|
}
|
|
>
|
|
开始选择
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="contained"
|
|
size="small"
|
|
onClick={handleEndSelection}
|
|
className="bg-red-500 hover:bg-red-600"
|
|
startIcon={
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
}
|
|
>
|
|
结束选择
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
|
|
{isSelecting && (
|
|
<Box className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
|
💡 点击地图上的管道添加爆管点
|
|
</Box>
|
|
)}
|
|
|
|
<Stack spacing={2}>
|
|
{pipePoints.map((pipe) => (
|
|
<Box
|
|
key={pipe.id}
|
|
className="flex items-center gap-2 p-2 bg-gray-50 rounded"
|
|
>
|
|
<Typography className="flex-shrink-0 text-sm">
|
|
{pipe.id}
|
|
</Typography>
|
|
<Typography className="flex-shrink-0 text-sm text-gray-600">
|
|
管径: {pipe.diameter} mm
|
|
</Typography>
|
|
<Typography className="flex-shrink-0 text-sm text-gray-600 mr-2">
|
|
爆管点面积
|
|
</Typography>
|
|
<TextField
|
|
size="small"
|
|
value={pipe.area}
|
|
onChange={(e) => handleAreaChange(pipe.id, e.target.value)}
|
|
type="number"
|
|
className="w-25"
|
|
slotProps={{
|
|
input: {
|
|
endAdornment: (
|
|
<span className="text-xs text-gray-500">cm²</span>
|
|
),
|
|
},
|
|
}}
|
|
/>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => handleRemovePipe(pipe.id)}
|
|
className="ml-auto"
|
|
>
|
|
<CloseIcon fontSize="small" />
|
|
</IconButton>
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
|
|
{/* 选择开始时间 */}
|
|
<Box className="mb-4">
|
|
<Typography variant="subtitle2" className="mb-2 font-medium">
|
|
选择开始时间
|
|
</Typography>
|
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
|
<DateTimePicker
|
|
value={startTime}
|
|
onChange={(value) =>
|
|
value && dayjs.isDayjs(value) && setStartTime(value)
|
|
}
|
|
format="YYYY-MM-DD HH:mm"
|
|
slotProps={{
|
|
textField: {
|
|
size: "small",
|
|
fullWidth: true,
|
|
},
|
|
}}
|
|
localeText={
|
|
pickerZhCN.components.MuiLocalizationProvider.defaultProps
|
|
.localeText
|
|
}
|
|
/>
|
|
</LocalizationProvider>
|
|
</Box>
|
|
|
|
{/* 持续时长 */}
|
|
<Box className="mb-4">
|
|
<Typography variant="subtitle2" className="mb-2 font-medium">
|
|
持续时长 (秒)
|
|
</Typography>
|
|
<TextField
|
|
fullWidth
|
|
size="small"
|
|
type="number"
|
|
value={duration}
|
|
onChange={(e) => setDuration(parseInt(e.target.value) || 0)}
|
|
placeholder="输入持续时长"
|
|
/>
|
|
</Box>
|
|
|
|
{/* 方案名称 */}
|
|
<Box className="mb-4">
|
|
<Typography variant="subtitle2" className="mb-2 font-medium">
|
|
方案名称
|
|
</Typography>
|
|
<TextField
|
|
fullWidth
|
|
size="small"
|
|
value={schemeName}
|
|
onChange={(e) => setSchemeName(e.target.value)}
|
|
placeholder="输入方案名称"
|
|
/>
|
|
</Box>
|
|
|
|
{/* 方案分析按钮 */}
|
|
<Box className="mt-auto">
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
size="large"
|
|
onClick={handleAnalyze}
|
|
disabled={analyzing || !isFormValid}
|
|
className="bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
{analyzing ? "方案提交分析中..." : "方案分析"}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default AnalysisParameters;
|