Files
TJWaterServer/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx

486 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 { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn";
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, close } = 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);
if (!feature) return;
if (feature.getGeometry()?.getType() === "Point") {
// 点类型几何不处理
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 body = {
name: 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.post(`${config.backendUrl}/burst_analysis/`, body, {
headers: {
"Accept-Encoding": "gzip",
"Content-Type": "application/json",
},
});
// 更新弹窗为成功状态
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,
},
}}
/>
</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;