+
diff --git a/src/app/OlMap/Controls/StyleEditorPanel.tsx b/src/app/OlMap/Controls/StyleEditorPanel.tsx
index a92167a..9bce045 100644
--- a/src/app/OlMap/Controls/StyleEditorPanel.tsx
+++ b/src/app/OlMap/Controls/StyleEditorPanel.tsx
@@ -29,6 +29,7 @@ import { calculateClassification } from "@utils/breaks_classification";
import { parseColor } from "@utils/parseColor";
import { VectorTile } from "ol";
import { useNotification } from "@refinedev/core";
+import { config } from "@/config/config";
interface StyleConfig {
property: string;
@@ -64,6 +65,9 @@ const SINGLE_COLOR_PALETTES = [
{
color: "rgba(51, 153, 204, 1)",
},
+ {
+ color: "rgba(255, 138, 92, 1)",
+ },
{
color: "rgba(204, 51, 51, 1)",
},
@@ -405,6 +409,25 @@ const StyleEditorPanel: React.FC = () => {
conditions.push(dimensions[i]);
}
conditions.push(dimensions[dimensions.length - 1]);
+ console.log("生成的尺寸条件表达式:", conditions);
+ return conditions;
+ };
+ const generateDimensionPointConditions = (property: string): any[] => {
+ const conditions: any[] = ["case"];
+ for (let i = 0; i < breaks.length; i++) {
+ conditions.push(["<=", ["get", property], breaks[i]]);
+ conditions.push([
+ "interpolate",
+ ["linear"],
+ ["zoom"],
+ 12,
+ 1, // 使用配置的最小尺寸
+ 24,
+ dimensions[i],
+ ]);
+ }
+ conditions.push(dimensions[dimensions.length - 1]);
+ console.log("生成的点尺寸条件表达式:", conditions);
return conditions;
};
// 创建基于 breaks 的动态 FlatStyle
@@ -422,7 +445,7 @@ const StyleEditorPanel: React.FC = () => {
dynamicStyle["circle-fill-color"] = generateColorConditions(
styleConfig.property
);
- dynamicStyle["circle-radius"] = generateDimensionConditions(
+ dynamicStyle["circle-radius"] = generateDimensionPointConditions(
styleConfig.property
);
dynamicStyle["circle-stroke-color"] = generateColorConditions(
@@ -456,21 +479,7 @@ const StyleEditorPanel: React.FC = () => {
const resetStyle = useCallback(() => {
if (!selectedRenderLayer) return;
// 重置 WebGL 图层样式
- const defaultFlatStyle: FlatStyleLike = {
- "stroke-width": 3,
- "stroke-color": "rgba(51, 153, 204, 0.9)",
- "circle-fill-color": "rgba(255,255,255,0.4)",
- "circle-stroke-color": "rgba(255,255,255,0.9)",
- "circle-radius": [
- "interpolate",
- ["linear"],
- ["zoom"],
- 12,
- 1, // 在缩放级别 12 时,圆形半径为 1px
- 24,
- 12, // 在缩放级别 24 时,圆形半径为 12px
- ],
- };
+ const defaultFlatStyle: FlatStyleLike = config.mapDefaultStyle;
selectedRenderLayer.setStyle(defaultFlatStyle);
// 删除对应图层的样式状态,从而移除图例显示
@@ -531,6 +540,7 @@ const StyleEditorPanel: React.FC = () => {
const [tileLoadListeners, setTileLoadListeners] = useState<
Map
void>
>(new Map());
+
const attachVectorTileSourceLoadedEvent = (
layerId: string,
property: string,
diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx
index b0d6b83..135b39b 100644
--- a/src/app/OlMap/Controls/Timeline.tsx
+++ b/src/app/OlMap/Controls/Timeline.tsx
@@ -23,6 +23,7 @@ import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { zhCN } from "date-fns/locale";
import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material";
import { TbRewindBackward15, TbRewindForward15 } from "react-icons/tb";
+import { FiSkipBack, FiSkipForward } from "react-icons/fi";
import { useData } from "../MapComponent";
import { config } from "@/config/config";
import { useMap } from "../MapComponent";
@@ -210,7 +211,7 @@ const Timeline: React.FC = () => {
type: "error",
message: "请至少设定并应用一个图层的样式。",
});
- return;
+ // return;
}
setIsPlaying(true);
@@ -233,7 +234,10 @@ const Timeline: React.FC = () => {
const handleStop = useCallback(() => {
setIsPlaying(false);
- setCurrentTime(0);
+ // 设置为当前时间
+ const currentTime = new Date();
+ const minutes = currentTime.getHours() * 60 + currentTime.getMinutes();
+ setCurrentTime(minutes); // 组件卸载时重置时间
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
@@ -241,6 +245,20 @@ const Timeline: React.FC = () => {
}, []);
// 步进控制
+ const handleDayStepBackward = useCallback(() => {
+ setSelectedDate((prev) => {
+ const newDate = new Date(prev);
+ newDate.setDate(newDate.getDate() - 1);
+ return newDate;
+ });
+ }, []);
+ const handleDayStepForward = useCallback(() => {
+ setSelectedDate((prev) => {
+ const newDate = new Date(prev);
+ newDate.setDate(newDate.getDate() + 1);
+ return newDate;
+ });
+ }, []);
const handleStepBackward = useCallback(() => {
setCurrentTime((prev) => {
const next = prev <= 0 ? 1440 : prev - 15;
@@ -299,7 +317,7 @@ const Timeline: React.FC = () => {
type: "error",
message: "请至少设定并应用一个图层的样式。",
});
- return;
+ // return;
}
fetchFrameData(
currentTimeToDate(selectedDate, currentTime),
@@ -311,7 +329,12 @@ const Timeline: React.FC = () => {
// 组件卸载时清理定时器和防抖
useEffect(() => {
- setCurrentTime(0); // 组件卸载时重置时间
+ // 设置为当前时间
+ const currentTime = new Date();
+ const minutes = currentTime.getHours() * 60 + currentTime.getMinutes();
+ // 找到最近的前15分钟刻度
+ const roundedMinutes = Math.floor(minutes / 15) * 15;
+ setCurrentTime(roundedMinutes); // 组件卸载时重置时间
return () => {
if (intervalRef.current) {
@@ -363,6 +386,15 @@ const Timeline: React.FC = () => {
alignItems="center"
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }}
>
+
+
+
+
+
{/* 日期选择器 */}
{
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
maxDate={new Date()} // 禁止选取未来的日期
/>
-
+
+
+
+
+
{/* 播放控制按钮 */}
-
+
{/* 播放间隔选择 */}
播放间隔
@@ -434,7 +477,7 @@ const Timeline: React.FC = () => {
-
+
{/* 强制计算时间段 */}
计算时间段
diff --git a/src/app/OlMap/Controls/Toolbar.tsx b/src/app/OlMap/Controls/Toolbar.tsx
index 7353b07..33dc959 100644
--- a/src/app/OlMap/Controls/Toolbar.tsx
+++ b/src/app/OlMap/Controls/Toolbar.tsx
@@ -21,6 +21,7 @@ import VectorTileSource from "ol/source/VectorTile";
import TileState from "ol/TileState";
import { toLonLat } from "ol/proj";
import { booleanIntersects, buffer, point, toWgs84 } from "@turf/turf";
+// import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import RenderFeature from "ol/render/Feature";
import { config } from "@/config/config";
@@ -329,7 +330,13 @@ const Toolbar: React.FC = ({ hiddenButtons }) => {
},
[map, highlightLayer, setHighlightFeature]
);
-
+ // const handleMapClickSelectFeatures = useCallback(
+ // (event: { coordinate: number[] }) => {
+ // if (!map) return;
+ // mapClickSelectFeatures(event, map, setHighlightFeature); // 调用导入的函数
+ // },
+ // [map, setHighlightFeature]
+ // );
// 添加矢量属性查询事件监听器
useEffect(() => {
if (!activeTools.includes("info") || !map) return;
diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx
index 65ec286..2c6992f 100644
--- a/src/app/OlMap/MapComponent.tsx
+++ b/src/app/OlMap/MapComponent.tsx
@@ -104,9 +104,9 @@ const MapComponent: React.FC = ({ children }) => {
const [map, setMap] = useState();
// currentCalData 用于存储当前计算结果
- const [currentTime, setCurrentTime] = useState(-1); // 默认-1表示未选择时间
- const [selectedDate, setSelectedDate] = useState(new Date("2025-9-17"));
- // const [selectedDate, setSelectedDate] = useState(new Date()); // 默认今天
+ const [currentTime, setCurrentTime] = useState(-1); // 默认选择当前时间
+ // const [selectedDate, setSelectedDate] = useState(new Date("2025-9-17"));
+ const [selectedDate, setSelectedDate] = useState(new Date()); // 默认今天
const [currentJunctionCalData, setCurrentJunctionCalData] = useState(
[]
@@ -174,21 +174,7 @@ const MapComponent: React.FC = ({ children }) => {
setPipeDataState((prev) => [...prev, ...uniqueNewData]);
}
};
- const defaultFlatStyle: FlatStyleLike = {
- "stroke-width": 3,
- "stroke-color": "rgba(51, 153, 204, 0.9)",
- "circle-fill-color": "rgba(255,255,255,0.4)",
- "circle-stroke-color": "rgba(255,255,255,0.9)",
- "circle-radius": [
- "interpolate",
- ["linear"],
- ["zoom"],
- 12,
- 1, // 在缩放级别 12 时,圆形半径为 1px
- 24,
- 12, // 在缩放级别 24 时,圆形半径为 12px
- ],
- };
+ const defaultFlatStyle: FlatStyleLike = config.mapDefaultStyle;
// 矢量瓦片数据源和图层
const junctionSource = new VectorTileSource({
url: `${mapUrl}/gwc/service/tms/1.0.0/TJWater:geo_junctions_mat@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL
@@ -435,9 +421,9 @@ const MapComponent: React.FC = ({ children }) => {
fontFamily: "Monaco, monospace",
getText: (d: any) =>
d[junctionText] ? (d[junctionText] as number).toFixed(3) : "",
- getSize: 14,
+ getSize: 18,
fontWeight: "bold",
- getColor: [150, 150, 255],
+ getColor: [255, 138, 92],
getAngle: 0,
getTextAnchor: "middle",
getAlignmentBaseline: "center",
@@ -455,8 +441,8 @@ const MapComponent: React.FC = ({ children }) => {
fontSize: 64,
buffer: 6,
},
- outlineWidth: 10,
- outlineColor: [255, 255, 255, 255],
+ // outlineWidth: 10,
+ // outlineColor: [242, 244, 246, 255],
}),
new TextLayer({
id: "pipeTextLayer",
@@ -466,9 +452,9 @@ const MapComponent: React.FC = ({ children }) => {
fontFamily: "Monaco, monospace",
getText: (d: any) =>
d[pipeText] ? (d[pipeText] as number).toFixed(3) : "",
- getSize: 12,
+ getSize: 18,
fontWeight: "bold",
- getColor: [120, 128, 181],
+ getColor: [51, 153, 204],
getAngle: (d: any) => d.angle || 0,
getPixelOffset: [0, -8],
getTextAnchor: "middle",
@@ -485,8 +471,8 @@ const MapComponent: React.FC = ({ children }) => {
fontSize: 64,
buffer: 6,
},
- outlineWidth: 10,
- outlineColor: [255, 255, 255, 255],
+ // outlineWidth: 10,
+ // outlineColor: [242, 244, 246, 255],
}),
];
deck.setProps({ layers: newLayers });
diff --git a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx
new file mode 100644
index 0000000..9dc087c
--- /dev/null
+++ b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx
@@ -0,0 +1,371 @@
+"use client";
+
+import React, { useState, useRef, useEffect } 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 from "ol/style/Style";
+import Stroke from "ol/style/Stroke";
+import GeoJson from "ol/format/GeoJSON";
+import config from "@config/config";
+import type { Feature } from "ol";
+import type { Geometry } from "ol/geom";
+
+const mapUrl = config.mapUrl;
+
+interface PipePoint {
+ id: string;
+ diameter: number;
+ area: number;
+ feature?: any; // 存储管道要素用于高亮
+}
+
+interface AnalysisParametersProps {
+ onAnalyze?: (params: AnalysisParams) => void;
+}
+
+interface AnalysisParams {
+ pipePoints: PipePoint[];
+ startTime: Dayjs | null;
+ duration: number;
+ schemeName: string;
+}
+
+const AnalysisParameters: React.FC = ({
+ onAnalyze,
+}) => {
+ const map = useMap();
+
+ const [pipePoints, setPipePoints] = useState([
+ { id: "541022", diameter: 110, area: 15 },
+ { id: "532748", diameter: 110, area: 15 },
+ ]);
+ const [startTime, setStartTime] = useState(
+ dayjs("2025-10-21T00:00:00")
+ );
+ const [duration, setDuration] = useState(3000);
+ const [schemeName, setSchemeName] = useState("Fangan1021100506");
+ const [isSelecting, setIsSelecting] = useState(false);
+
+ const highlightLayerRef = useRef | null>(null);
+ const clickListenerRef = useRef<((evt: any) => void) | null>(null);
+
+ // 初始化管道图层和高亮图层
+ useEffect(() => {
+ if (!map) return;
+
+ // 创建高亮图层
+ const highlightLayer = new VectorLayer({
+ source: new VectorSource(),
+ style: new Style({
+ stroke: new Stroke({
+ color: "#ff0000",
+ width: 5,
+ }),
+ }),
+ properties: {
+ name: "高亮管道",
+ value: "highlight_pipeline",
+ },
+ zIndex: 999,
+ });
+
+ map.addLayer(highlightLayer);
+ highlightLayerRef.current = highlightLayer;
+
+ return () => {
+ map.removeLayer(highlightLayer);
+ if (clickListenerRef.current) {
+ map.un("click", clickListenerRef.current);
+ }
+ };
+ }, [map]);
+
+ // 开始选择管道
+ const handleStartSelection = () => {
+ if (!map) return;
+
+ setIsSelecting(true);
+ // 显示管道图层
+
+ // 注册点击事件
+ const clickListener = (evt: any) => {
+ let clickedFeature: any = null;
+
+ map.forEachFeatureAtPixel(
+ evt.pixel,
+ (feature) => {
+ if (!clickedFeature) {
+ clickedFeature = feature;
+ }
+ return true;
+ },
+ { hitTolerance: 5 }
+ );
+
+ if (clickedFeature) {
+ const properties = clickedFeature.getProperties();
+ const pipeId = properties.Id || properties.id || properties.ID;
+ const diameter = properties.Diameter || properties.diameter || 100;
+
+ // 检查是否已存在
+ const exists = pipePoints.some((pipe) => pipe.id === pipeId);
+ if (!exists && pipeId) {
+ const newPipe: PipePoint = {
+ id: String(pipeId),
+ diameter: Number(diameter),
+ area: 15,
+ feature: clickedFeature,
+ };
+
+ setPipePoints((prev) => [...prev, newPipe]);
+
+ // 添加到高亮图层
+ const highlightSource = highlightLayerRef.current?.getSource();
+ if (highlightSource) {
+ highlightSource.addFeature(clickedFeature);
+ }
+ }
+ }
+ };
+
+ clickListenerRef.current = clickListener;
+ map.on("click", clickListener);
+ };
+
+ // 结束选择管道
+ const handleEndSelection = () => {
+ if (!map) return;
+
+ setIsSelecting(false);
+
+ // 移除点击事件
+ if (clickListenerRef.current) {
+ map.un("click", clickListenerRef.current);
+ clickListenerRef.current = null;
+ }
+ };
+
+ const handleRemovePipe = (id: string) => {
+ // 找到要删除的管道
+ const pipeToRemove = pipePoints.find((pipe) => pipe.id === id);
+
+ // 从高亮图层中移除对应的要素
+ if (pipeToRemove && pipeToRemove.feature && highlightLayerRef.current) {
+ const highlightSource = highlightLayerRef.current.getSource();
+ if (highlightSource) {
+ highlightSource.removeFeature(pipeToRemove.feature);
+ }
+ }
+
+ // 从状态中移除
+ setPipePoints((prev) => prev.filter((pipe) => pipe.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 = () => {
+ if (onAnalyze) {
+ onAnalyze({
+ pipePoints,
+ startTime,
+ duration,
+ schemeName,
+ });
+ }
+ };
+
+ return (
+
+ {/* 选择爆管点 */}
+
+
+
+ 选择爆管点
+
+ {/* 开始/结束选择按钮 */}
+ {!isSelecting ? (
+
+ ) : (
+
+ )}
+
+
+ {isSelecting && (
+
+ 💡 点击地图上的管道添加爆管点
+
+ )}
+
+
+ {pipePoints.map((pipe) => (
+
+
+ {pipe.id}
+
+
+ 管径: {pipe.diameter} mm
+
+
+ 爆管点面积
+
+ handleAreaChange(pipe.id, e.target.value)}
+ type="number"
+ className="w-25"
+ slotProps={{
+ input: {
+ endAdornment: (
+ cm²
+ ),
+ },
+ }}
+ />
+ handleRemovePipe(pipe.id)}
+ className="ml-auto"
+ >
+
+
+
+ ))}
+
+
+
+ {/* 选择开始时间 */}
+
+
+ 选择开始时间
+
+
+
+ value && dayjs.isDayjs(value) && setStartTime(value)
+ }
+ format="YYYY-MM-DD HH:mm"
+ slotProps={{
+ textField: {
+ size: "small",
+ fullWidth: true,
+ },
+ }}
+ />
+
+
+
+ {/* 持续时长 */}
+
+
+ 持续时长 (秒)
+
+ setDuration(parseInt(e.target.value) || 0)}
+ placeholder="输入持续时长"
+ />
+
+
+ {/* 方案名称 */}
+
+
+ 方案名称
+
+ setSchemeName(e.target.value)}
+ placeholder="输入方案名称"
+ />
+
+
+ {/* 方案分析按钮 */}
+
+
+
+
+ );
+};
+
+export default AnalysisParameters;
diff --git a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx
new file mode 100644
index 0000000..44e142a
--- /dev/null
+++ b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx
@@ -0,0 +1,205 @@
+'use client';
+
+import React, { useState } from 'react';
+import {
+ Box,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Chip,
+ IconButton,
+ Tooltip,
+} from '@mui/material';
+import { LocationOn as LocationIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
+
+interface LocationResult {
+ id: number;
+ nodeName: string;
+ nodeId: string;
+ pressure: number;
+ waterLevel: number;
+ flow: number;
+ status: 'normal' | 'warning' | 'danger';
+ coordinates: [number, number];
+}
+
+interface LocationResultsProps {
+ onLocate?: (coordinates: [number, number]) => void;
+ onViewDetail?: (id: number) => void;
+}
+
+const LocationResults: React.FC = ({ onLocate, onViewDetail }) => {
+ const [results, setResults] = useState([
+ // 示例数据
+ // {
+ // id: 1,
+ // nodeName: '节点A',
+ // nodeId: 'N001',
+ // pressure: 0.35,
+ // waterLevel: 12.5,
+ // flow: 85.3,
+ // status: 'normal',
+ // coordinates: [120.15, 30.25],
+ // },
+ ]);
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'normal':
+ return 'success';
+ case 'warning':
+ return 'warning';
+ case 'danger':
+ return 'error';
+ default:
+ return 'default';
+ }
+ };
+
+ const getStatusText = (status: string) => {
+ switch (status) {
+ case 'normal':
+ return '正常';
+ case 'warning':
+ return '预警';
+ case 'danger':
+ return '危险';
+ default:
+ return '未知';
+ }
+ };
+
+ return (
+
+ {/* 统计信息 */}
+
+
+ 定位结果统计
+
+
+
+
+ 总数
+
+
+ {results.length}
+
+
+
+
+ 正常
+
+
+ {results.filter((r) => r.status === 'normal').length}
+
+
+
+
+ 预警
+
+
+ {results.filter((r) => r.status === 'warning').length}
+
+
+
+
+ 危险
+
+
+ {results.filter((r) => r.status === 'danger').length}
+
+
+
+
+
+ {/* 结果列表 */}
+
+ {results.length === 0 ? (
+
+
+
+
+ 暂无定位结果
+
+ 请先执行方案分析
+
+
+ ) : (
+
+
+
+
+ 节点名称
+ 节点ID
+ 压力 (MPa)
+ 水位 (m)
+ 流量 (m³/h)
+ 状态
+ 操作
+
+
+
+ {results.map((result) => (
+
+ {result.nodeName}
+ {result.nodeId}
+ {result.pressure.toFixed(2)}
+ {result.waterLevel.toFixed(2)}
+ {result.flow.toFixed(1)}
+
+
+
+
+
+ onLocate?.(result.coordinates)}
+ color="primary"
+ >
+
+
+
+
+ onViewDetail?.(result.id)}
+ color="primary"
+ >
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+ );
+};
+
+export default LocationResults;
diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx
new file mode 100644
index 0000000..bb59a72
--- /dev/null
+++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx
@@ -0,0 +1,190 @@
+"use client";
+
+import React, { useState } from "react";
+import {
+ Box,
+ Button,
+ Typography,
+ Checkbox,
+ FormControlLabel,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ IconButton,
+} from "@mui/material";
+import {
+ Info as InfoIcon,
+ LocationOn as LocationIcon,
+} 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";
+import dayjs, { Dayjs } from "dayjs";
+import "dayjs/locale/zh-cn";
+
+interface SchemeRecord {
+ id: number;
+ schemeName: string;
+ type: string;
+ user: string;
+ createTime: string;
+ startTime: string;
+}
+
+interface SchemeQueryProps {
+ onViewDetails?: (id: number) => void;
+ onLocate?: (id: number) => void;
+}
+
+const SchemeQuery: React.FC = ({
+ onViewDetails,
+ onLocate,
+}) => {
+ const [queryAll, setQueryAll] = useState(true);
+ const [queryDate, setQueryDate] = useState(dayjs("2025-10-21"));
+ const [schemes, setSchemes] = useState([]);
+
+ const handleQuery = () => {
+ // TODO: 实际查询逻辑
+ console.log("查询方案", { queryAll, queryDate });
+ // 这里应该调用API获取数据
+ };
+
+ return (
+
+ {/* 查询条件 */}
+
+
+ setQueryAll(e.target.checked)}
+ />
+ }
+ label="查询全部"
+ />
+
+
+ value && dayjs.isDayjs(value) && setQueryDate(value)
+ }
+ format="YYYY-MM-DD"
+ disabled={queryAll}
+ slotProps={{
+ textField: {
+ size: "small",
+ className: "flex-1",
+ },
+ }}
+ />
+
+
+
+
+
+ {/* 结果列表 */}
+
+ {schemes.length === 0 ? (
+
+
+
+
+ 总共 0 条
+
+ No data
+
+
+ ) : (
+
+
+
+
+ ID
+ 方案名称
+ 类型
+ 用户
+ 创建时间
+ 开始时间
+ 详情
+ 定位
+
+
+
+ {schemes.map((scheme) => (
+
+ {scheme.id}
+ {scheme.schemeName}
+ {scheme.type}
+ {scheme.user}
+ {scheme.createTime}
+ {scheme.startTime}
+
+ onViewDetails?.(scheme.id)}
+ color="primary"
+ >
+
+
+
+
+ onLocate?.(scheme.id)}
+ color="primary"
+ >
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+ );
+};
+
+export default SchemeQuery;
diff --git a/src/components/olmap/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysisPanel.tsx
new file mode 100644
index 0000000..db97f1d
--- /dev/null
+++ b/src/components/olmap/BurstPipeAnalysisPanel.tsx
@@ -0,0 +1,217 @@
+"use client";
+
+import React, { useState } from "react";
+import { Box, Drawer, Tabs, Tab, Typography, IconButton } from "@mui/material";
+import {
+ ChevronRight as ChevronRightIcon,
+ ChevronLeft as ChevronLeftIcon,
+ Analytics as AnalyticsIcon,
+ Search as SearchIcon,
+ MyLocation as MyLocationIcon,
+} from "@mui/icons-material";
+import AnalysisParameters from "./BurstPipeAnalysis/AnalysisParameters";
+import SchemeQuery from "./BurstPipeAnalysis/SchemeQuery";
+import LocationResults from "./BurstPipeAnalysis/LocationResults";
+
+interface TabPanelProps {
+ children?: React.ReactNode;
+ index: number;
+ value: number;
+}
+
+const TabPanel: React.FC = ({ children, value, index }) => {
+ return (
+
+ {value === index && (
+ {children}
+ )}
+
+ );
+};
+
+interface BurstPipeAnalysisPanelProps {
+ open?: boolean;
+ onToggle?: () => void;
+}
+
+const BurstPipeAnalysisPanel: React.FC = ({
+ open: controlledOpen,
+ onToggle,
+}) => {
+ const [internalOpen, setInternalOpen] = useState(true);
+ const [currentTab, setCurrentTab] = useState(0);
+
+ // 使用受控或非受控状态
+ const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
+ const handleToggle = () => {
+ if (onToggle) {
+ onToggle();
+ } else {
+ setInternalOpen(!internalOpen);
+ }
+ };
+
+ const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
+ setCurrentTab(newValue);
+ };
+
+ const drawerWidth = 520;
+
+ return (
+ <>
+ {/* 收起时的触发按钮 */}
+ {!isOpen && (
+
+
+
+
+ 爆管分析
+
+
+
+
+ )}
+
+ {/* 主面板 */}
+
+
+ {/* 头部 */}
+
+
+
+
+ 爆管分析
+
+
+
+
+
+
+
+ {/* Tabs 导航 */}
+
+
+ }
+ iconPosition="start"
+ label="分析要件"
+ />
+ }
+ iconPosition="start"
+ label="方案查询"
+ />
+ }
+ iconPosition="start"
+ label="定位结果"
+ />
+
+
+
+ {/* Tab 内容 */}
+
+ {
+ console.log("开始分析:", params);
+ // TODO: 调用分析API
+ }}
+ />
+
+
+
+ {
+ console.log("查看详情:", id);
+ // TODO: 显示方案详情
+ }}
+ onLocate={(id) => {
+ console.log("定位方案:", id);
+ // TODO: 在地图上定位
+ }}
+ />
+
+
+
+ {
+ console.log("定位到:", coordinates);
+ // TODO: 地图定位到指定坐标
+ }}
+ onViewDetail={(id) => {
+ console.log("查看节点详情:", id);
+ // TODO: 显示节点详细信息
+ }}
+ />
+
+
+
+ >
+ );
+};
+
+export default BurstPipeAnalysisPanel;
diff --git a/src/config/config.ts b/src/config/config.ts
index 4b296cd..265e5a5 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -1,18 +1,33 @@
export const config = {
- backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL || "http://192.168.1.42:8000",
- mapUrl: process.env.NEXT_PUBLIC_MAP_URL || "http://127.0.0.1:8080/geoserver",
- mapExtent: [13508849, 3608035.75, 13555781, 3633812.75],
+ backendUrl: process.env.BACKEND_URL || "http://192.168.1.42:8000",
+ mapUrl: process.env.MAP_URL || "http://127.0.0.1:8080/geoserver",
+ mapExtent: process.env.MAP_EXTENT
+ ? process.env.MAP_EXTENT.split(",").map(Number)
+ : [13508849, 3608035.75, 13555781, 3633812.75],
+ mapDefaultStyle: {
+ "stroke-width": 3,
+ "stroke-color": "rgba(51, 153, 204, 0.9)",
+ "circle-fill-color": "rgba(255,138, 92,0.8)",
+ "circle-stroke-color": "rgba(255,138, 92,0.9)",
+ "circle-radius": [
+ "interpolate",
+ ["linear"],
+ ["zoom"],
+ 12,
+ 1, // 在缩放级别 12 时,圆形半径为 1px
+ 24,
+ 12, // 在缩放级别 24 时,圆形半径为 12px
+ ],
+ },
// 添加其他配置项...
};
export const MAPBOX_TOKEN =
- process.env.NEXT_PUBLIC_MAPBOX_TOKEN ||
+ process.env.MAPBOX_TOKEN ||
"pk.eyJ1IjoiemhpZnUiLCJhIjoiY205azNyNGY1MGkyZDJxcTJleDUwaHV1ZCJ9.wOmSdOnDDdre-mB1Lpy6Fg";
export const TIANDITU_TOKEN =
- process.env.NEXT_PUBLIC_TIANDITU_TOKEN || "e3e8ad95ee911741fa71ed7bff2717ec";
-export const PROJECT_TITLE =
- process.env.NEXT_PUBLIC_PROJECT_TITLE || "TJWater Project";
-
+ process.env.TIANDITU_TOKEN || "e3e8ad95ee911741fa71ed7bff2717ec";
+export const PROJECT_TITLE = process.env.PROJECT_TITLE || "TJWater Project";
export const META_DATA = {
title: PROJECT_TITLE,
description: "Developed by TJWATER",
diff --git a/src/utils/mapQueryService.js b/src/utils/mapQueryService.js
new file mode 100644
index 0000000..5683f74
--- /dev/null
+++ b/src/utils/mapQueryService.js
@@ -0,0 +1,438 @@
+/**
+ * OpenLayers 地图工具函数集合
+ * 提供地图要素查询、选择和处理功能
+ */
+
+import { GeoJSON } from 'ol/format';
+import { Feature } from 'ol';
+import { Point, LineString, Polygon } from 'ol/geom';
+import Geometry from 'ol/geom/Geometry';
+import TileState from 'ol/TileState';
+import { toLonLat } from 'ol/proj';
+import { booleanIntersects, buffer, point, toWgs84 } from "@turf/turf";
+import config from "@config/config";
+// ========== 常量配置 ==========
+
+const GEOSERVER_CONFIG = {
+ url: config.GEOSERVER_URL,
+ network: config.GEOSERVER_NETWORK,
+ layers: ['geo_pipes_mat', 'geo_junctions_mat'],
+ wfsVersion: '1.0.0',
+ outputFormat: 'application/json',
+};
+
+const MAP_CONFIG = {
+ hitTolerance: 5, // 像素容差
+ bufferUnits: 'meters',
+};
+
+// ========== 几何类型枚举 ==========
+
+const GEOMETRY_TYPES = {
+ POINT: 'Point',
+ LINE_STRING: 'LineString',
+ POLYGON: 'Polygon',
+};
+
+// ========== 工具函数 ==========
+
+/**
+ * 构建 WFS 查询 URL
+ * @param {string} layer - 图层名称
+ * @param {string} cqlFilter - CQL 过滤条件
+ * @returns {string} 完整的 WFS 查询 URL
+ */
+const buildWfsUrl = (layer, cqlFilter) => {
+ const { url, network, wfsVersion, outputFormat } = GEOSERVER_CONFIG;
+ const params = new URLSearchParams({
+ service: 'WFS',
+ version: wfsVersion,
+ request: 'GetFeature',
+ typeName: `${network}:${layer}`,
+ outputFormat,
+ CQL_FILTER: cqlFilter,
+ });
+ return `${url}/${network}/ows?${params.toString()}`;
+};
+
+/**
+ * 查询单个图层的要素
+ * @param {string} layer - 图层名称
+ * @param {string} cqlFilter - CQL 过滤条件
+ * @returns {Promise} 要素数组
+ */
+const queryLayerFeatures = async (layer, cqlFilter) => {
+ try {
+ const url = buildWfsUrl(layer, cqlFilter);
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`请求失败: ${response.statusText}`);
+ }
+
+ const json = await response.json();
+ return new GeoJSON().readFeatures(json);
+ } catch (error) {
+ console.error(`图层 ${layer} 查询失败:`, error);
+ return [];
+ }
+};
+
+/**
+ * 根据 IDs,通过 Geoserver WFS 服务查询要素
+ * @param {string[]} ids - 要素 ID 数组
+ * @param {string} [layer] - 可选的特定图层名称,不传则查询所有图层
+ * @returns {Promise} 查询到的要素数组
+ */
+export const queryFeaturesByIds = async (ids, layer = null) => {
+ if (!ids || !ids.length) {
+ return [];
+ }
+
+ const cqlFilter = ids.map((id) => `id=${id}`).join(' OR ');
+
+ try {
+ if (layer) {
+ // 查询指定图层
+ return await queryLayerFeatures(layer, cqlFilter);
+ }
+
+ // 查询所有图层
+ const { layers } = GEOSERVER_CONFIG;
+ const promises = layers.map((layerName) =>
+ queryLayerFeatures(layerName, cqlFilter)
+ );
+
+ const results = await Promise.all(promises);
+ const features = results.flat();
+
+ return features;
+ } catch (error) {
+ console.error('根据 IDs 查询要素时出错:', error);
+ return [];
+ }
+};
+
+/**
+ * 将扁平坐标数组转换为坐标对数组
+ * @param {number[]} flatCoordinates - 扁平坐标数组 [x1, y1, x2, y2, ...]
+ * @returns {number[][]} 坐标对数组 [[x1, y1], [x2, y2], ...]
+ */
+const flatCoordinatesToPairs = (flatCoordinates) => {
+ const pairs = [];
+ for (let i = 0; i < flatCoordinates.length; i += 2) {
+ pairs.push([flatCoordinates[i], flatCoordinates[i + 1]]);
+ }
+ return pairs;
+};
+
+/**
+ * 创建点几何对象
+ * @param {number[]} flatCoordinates - 扁平坐标数组
+ * @returns {Point} 点几何对象
+ */
+const createPointGeometry = (flatCoordinates) => {
+ const coordinates = [flatCoordinates[0], flatCoordinates[1]];
+ return new Point(coordinates);
+};
+
+/**
+ * 创建线几何对象
+ * @param {number[]} flatCoordinates - 扁平坐标数组
+ * @returns {LineString} 线几何对象
+ */
+const createLineStringGeometry = (flatCoordinates) => {
+ const lineCoords = flatCoordinatesToPairs(flatCoordinates);
+ return new LineString(lineCoords);
+};
+
+/**
+ * 创建面几何对象
+ * @param {number[]} flatCoordinates - 扁平坐标数组
+ * @param {Object} geometry - 原始几何对象
+ * @returns {Polygon} 面几何对象
+ */
+const createPolygonGeometry = (flatCoordinates, geometry) => {
+ // 获取环的结束位置
+ const ends = geometry.getEnds ? geometry.getEnds() : [flatCoordinates.length];
+ const rings = [];
+ let start = 0;
+
+ for (const end of ends) {
+ const ring = [];
+ for (let i = start; i < end; i += 2) {
+ ring.push([flatCoordinates[i], flatCoordinates[i + 1]]);
+ }
+ rings.push(ring);
+ start = end;
+ }
+
+ return new Polygon(rings);
+};
+
+/**
+ * 将 RenderFeature 转换为标准 Feature
+ * @param {Object} renderFeature - 渲染要素对象
+ * @returns {Feature|null} OpenLayers Feature 对象,转换失败返回 null
+ */
+const renderFeature2Feature = (renderFeature) => {
+ if (!renderFeature) {
+ return null;
+ }
+
+ const geometry = renderFeature.getGeometry();
+ if (!geometry) {
+ return null;
+ }
+
+ try {
+ let clonedGeometry;
+
+ if (geometry instanceof Geometry) {
+ // 标准 Feature 的几何体,直接使用
+ clonedGeometry = geometry;
+ } else {
+ // RenderFeature 的几何体,需要转换
+ const type = geometry.getType();
+ const flatCoordinates = geometry.getFlatCoordinates();
+
+ switch (type) {
+ case 'Point':
+ clonedGeometry = createPointGeometry(flatCoordinates);
+ break;
+
+ case 'LineString':
+ clonedGeometry = createLineStringGeometry(flatCoordinates);
+ break;
+
+ case 'Polygon':
+ clonedGeometry = createPolygonGeometry(flatCoordinates, geometry);
+ break;
+
+ default:
+ console.warn('不支持的几何体类型:', type);
+ return null;
+ }
+ }
+
+ // 创建新的 Feature,包含几何体和属性
+ const feature = new Feature({
+ geometry: clonedGeometry,
+ ...renderFeature.getProperties(),
+ });
+
+ return feature;
+ } catch (error) {
+ console.error('RenderFeature 转换 Feature 时出错:', error);
+ return null;
+ }
+};
+
+/**
+ * 对要素按几何类型进行分类
+ * @param {Feature} feature - OpenLayers 要素
+ * @param {Object} categorized - 分类存储对象
+ */
+const categorizeFeatureByGeometry = (feature, categorized) => {
+ const geometryType = feature.getGeometry()?.getType();
+
+ if (geometryType === GEOMETRY_TYPES.POINT) {
+ categorized.points.push(feature);
+ } else if (geometryType === GEOMETRY_TYPES.LINE_STRING) {
+ categorized.lines.push(feature);
+ } else {
+ categorized.others.push(feature);
+ }
+};
+
+/**
+ * 检查要素是否在缓冲区内
+ * @param {Feature} feature - OpenLayers 要素
+ * @param {Object} bufferedGeometry - 缓冲区几何对象
+ * @returns {boolean} 是否相交
+ */
+const isFeatureInBuffer = (feature, bufferedGeometry) => {
+ if (!feature || !bufferedGeometry) {
+ return false;
+ }
+
+ try {
+ const geoJSONGeometry = new GeoJSON().writeGeometryObject(
+ feature.getGeometry()
+ );
+ return booleanIntersects(toWgs84(geoJSONGeometry), bufferedGeometry);
+ } catch (error) {
+ console.error('要素缓冲区检查失败:', error);
+ return false;
+ }
+};
+
+/**
+ * 处理矢量瓦片,提取符合条件的要素
+ * @param {Object} vectorTile - 矢量瓦片对象
+ * @param {Object} buffered - 缓冲区对象
+ * @param {Object} categorized - 分类存储对象
+ */
+const processVectorTile = (vectorTile, buffered, categorized) => {
+ if (vectorTile.getState() !== TileState.LOADED) {
+ return;
+ }
+
+ const renderFeatures = vectorTile.getFeatures();
+ if (!renderFeatures || renderFeatures.length === 0) {
+ return;
+ }
+
+ const selectedFeatures = renderFeatures
+ .map((renderFeature) => renderFeature2Feature(renderFeature))
+ .filter((feature) => feature !== null) // 过滤转换失败的要素
+ .filter((feature) => isFeatureInBuffer(feature, buffered?.geometry));
+
+ selectedFeatures.forEach((feature) =>
+ categorizeFeatureByGeometry(feature, categorized)
+ );
+};
+
+/**
+ * 处理矢量瓦片源,提取所有符合条件的要素
+ * @param {Object} vectorTileSource - 矢量瓦片源
+ * @param {number[]} coord - 坐标
+ * @param {number} z - 缩放级别
+ * @param {Object} projection - 投影
+ * @param {Object} categorized - 分类存储对象
+ */
+const processVectorTileSource = (
+ vectorTileSource,
+ coord,
+ z,
+ projection,
+ categorized
+) => {
+ const tileGrid = vectorTileSource.getTileGrid();
+
+ if (!tileGrid) {
+ return;
+ }
+
+ // 确保缩放级别在有效范围内
+ const minZoom = tileGrid.getMinZoom();
+ const maxZoom = tileGrid.getMaxZoom();
+ const validZ = Math.max(minZoom, Math.min(z, maxZoom));
+
+ const [x, y] = coord;
+ const tileCoord = tileGrid.getTileCoordForCoordAndZ([x, y], validZ);
+ const resolution = tileGrid.getResolution(tileCoord[0]);
+
+ // 创建缓冲区用于容差计算
+ const { hitTolerance, bufferUnits } = MAP_CONFIG;
+ const hitPoint = point(toLonLat(coord));
+ const buffered = buffer(hitPoint, resolution * hitTolerance, {
+ units: bufferUnits,
+ });
+
+ // 获取矢量渲染瓦片
+ const pixelRatio = window.devicePixelRatio;
+ const vectorRenderTile = vectorTileSource.getTile(
+ tileCoord[0],
+ tileCoord[1],
+ tileCoord[2],
+ pixelRatio,
+ projection
+ );
+ // 检查 vectorRenderTile 是否有效
+ if (!vectorRenderTile) {
+ return;
+ }
+ // 获取源瓦片
+ const vectorTiles = typeof vectorTileSource.getSourceTiles === 'function' ? vectorTileSource.getSourceTiles(
+ pixelRatio,
+ projection,
+ vectorRenderTile
+ ) : [];
+
+ vectorTiles.forEach((vectorTile) =>
+ processVectorTile(vectorTile, buffered, categorized)
+ );
+};
+
+/**
+ * 处理地图点击事件,选择要素
+ * @param {Object} event - 地图点击事件
+ * @param {Object} map - OpenLayers 地图对象
+ * @param {Function} setHighlightFeature - 设置高亮要素的回调函数
+ */
+export const handleMapClickSelectFeatures = (
+ event,
+ map,
+ setHighlightFeature
+) => {
+ if (!map || !event?.coordinate) {
+ return;
+ }
+
+ const coord = event.coordinate;
+ const view = map.getView();
+ const currentZoom = view.getZoom() || 0;
+ const z = Math.floor(currentZoom) - 1;
+ const projection = view.getProjection();
+
+ // 获取所有矢量瓦片图层源
+ const vectorTileSources = map
+ .getAllLayers()
+ .filter((layer) => layer.getSource && layer.getSource())
+ .map((layer) => layer.getSource())
+ .filter((source) => source && source.getTileGrid);
+
+ if (!vectorTileSources.length) {
+ return;
+ }
+
+ // 按几何类型分类存储要素
+ const categorized = {
+ points: [],
+ lines: [],
+ others: [],
+ };
+
+ // 处理所有矢量瓦片源
+ vectorTileSources.forEach((vectorTileSource) =>
+ processVectorTileSource(vectorTileSource, coord, z, projection, categorized)
+ );
+
+ // 按优先级合并要素:点 > 线 > 其他
+ const selectedFeatures = [
+ ...categorized.points,
+ ...categorized.lines,
+ ...categorized.others,
+ ];
+
+ // 处理选中的第一个要素
+ if (selectedFeatures.length > 0) {
+ const firstFeature = selectedFeatures[0];
+ const queryId = firstFeature?.getProperties()?.id;
+
+ if (queryId) {
+ queryFeaturesByIds([queryId])
+ .then((features) => {
+ if (features && features.length > 0) {
+ setHighlightFeature(features[0]);
+ } else {
+ setHighlightFeature(null);
+ }
+ })
+ .catch((error) => {
+ console.error('查询要素详情失败:', error);
+ setHighlightFeature(null);
+ });
+ } else {
+ setHighlightFeature(null);
+ }
+ } else {
+ setHighlightFeature(null);
+ }
+};
+
+export default {
+ handleMapClickSelectFeatures,
+ queryFeaturesByIds,
+};
\ No newline at end of file