diff --git a/public/icons/contaminant_source.svg b/public/icons/contaminant_source.svg
new file mode 100644
index 0000000..5b3130e
--- /dev/null
+++ b/public/icons/contaminant_source.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/app/OlMap/Controls/LayerControl.tsx b/src/app/OlMap/Controls/LayerControl.tsx
index fe16d37..8b539a5 100644
--- a/src/app/OlMap/Controls/LayerControl.tsx
+++ b/src/app/OlMap/Controls/LayerControl.tsx
@@ -28,10 +28,21 @@ const LayerControl: React.FC = () => {
} = data;
const [layerItems, setLayerItems] = useState([]);
+ const layerOrder = [
+ "junctions",
+ "reservoirs",
+ "tanks",
+ "pipes",
+ "pumps",
+ "valves",
+ "scada",
+ "waterflowLayer",
+ "junctionContourLayer",
+ ];
+
// 更新图层列表
const updateLayers = useCallback(() => {
if (!map || !data) return;
- const { deckLayer } = data;
const items: LayerItem[] = [];
@@ -93,19 +104,6 @@ const LayerControl: React.FC = () => {
});
}
- // 3. 定义图层显示顺序和过滤白名单
- const layerOrder = [
- "junctions",
- "reservoirs",
- "tanks",
- "pipes",
- "pumps",
- "valves",
- "scada",
- "waterflowLayer",
- "junctionContourLayer",
- ];
-
// 过滤并排序
const sortedItems = items
.filter((item) => layerOrder.includes(item.id))
@@ -116,7 +114,7 @@ const LayerControl: React.FC = () => {
});
setLayerItems(sortedItems);
- }, [map, isWaterflowLayerAvailable, isContourLayerAvailable]);
+ }, [map, deckLayer, isWaterflowLayerAvailable, isContourLayerAvailable]);
useEffect(() => {
updateLayers();
@@ -146,7 +144,7 @@ const LayerControl: React.FC = () => {
}
setLayerItems((prev) =>
- prev.map((i) => (i.id === item.id ? { ...i, visible: checked } : i))
+ prev.map((i) => (i.id === item.id ? { ...i, visible: checked } : i)),
);
};
diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx
index 86ffd5c..c2e1068 100644
--- a/src/app/OlMap/MapComponent.tsx
+++ b/src/app/OlMap/MapComponent.tsx
@@ -111,7 +111,7 @@ const MapComponent: React.FC = ({ children }) => {
const [schemeName, setSchemeName] = useState(""); // 当前方案名称
// 记录 id、对应属性的计算值
const [currentJunctionCalData, setCurrentJunctionCalData] = useState(
- []
+ [],
);
const [currentPipeCalData, setCurrentPipeCalData] = useState([]);
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
@@ -180,7 +180,7 @@ const MapComponent: React.FC = ({ children }) => {
setPipeData(tilePipeDataBuffer.current);
tilePipeDataBuffer.current = [];
}
- }, 100)
+ }, 100),
);
const setJunctionData = (newData: any[]) => {
@@ -317,7 +317,7 @@ const MapComponent: React.FC = ({ children }) => {
scale: 0.12,
anchor: [0.5, 0.5],
}),
- })
+ }),
);
}
return styles;
@@ -500,7 +500,7 @@ const MapComponent: React.FC = ({ children }) => {
const uniqueData = Array.from(data.values());
if (uniqueData.length > 0) {
uniqueData.forEach((item) =>
- tileJunctionDataBuffer.current.push(item)
+ tileJunctionDataBuffer.current.push(item),
);
debouncedUpdateData.current();
}
@@ -709,7 +709,7 @@ const MapComponent: React.FC = ({ children }) => {
if (center && typeof zoom === "number") {
localStorage.setItem(
MAP_VIEW_STORAGE_KEY,
- JSON.stringify({ center, zoom })
+ JSON.stringify({ center, zoom }),
);
}
} catch (err) {
@@ -947,27 +947,12 @@ const MapComponent: React.FC = ({ children }) => {
// 动画循环
const animate = () => {
- if (!flowAnimation.current) {
- try {
- deckLayer.removeDeckLayer("waterflowLayer");
- } catch (error) {
- console.error("Error in animation loop:", error);
- }
- return;
- }
// 动画总时长(秒)
- if (mergedPipeData.length === 0) {
- animationFrameId = requestAnimationFrame(animate);
- return;
- }
const animationDuration = 10;
- // 缓冲时间(秒)
const bufferTime = 2;
- // 完整循环周期
const loopLength = animationDuration + bufferTime;
- // 确保时间范围与你的时间戳数据匹配
- const currentTime = (Date.now() / 1000) % loopLength; // (0,12) 之间循环
- // console.log("Current Time:", currentTime);
+ const currentTime = (Date.now() / 1000) % loopLength;
+
const waterflowLayer = new TripsLayer({
id: "waterflowLayer",
name: "水流",
@@ -981,7 +966,7 @@ const MapComponent: React.FC = ({ children }) => {
visible:
isWaterflowLayerAvailable &&
showWaterflowLayer &&
- flowAnimation.current &&
+ flowAnimation.current && // 保持动画标志作为可见性的一部分
currentZoom >= 12 &&
currentZoom <= 24,
widthMinPixels: 5,
@@ -990,13 +975,17 @@ const MapComponent: React.FC = ({ children }) => {
trailLength: 2, // 水流尾迹淡出时间
currentTime: currentTime,
});
+
if (deckLayer.getDeckLayerById("waterflowLayer")) {
deckLayer.updateDeckLayer("waterflowLayer", waterflowLayer);
} else {
deckLayer.addDeckLayer(waterflowLayer);
}
- // 继续请求动画帧,每帧执行一次函数
- animationFrameId = requestAnimationFrame(animate);
+
+ // 只有在需要动画时才请求下一帧,但图层已经添加到了 deckLayer 中
+ if (flowAnimation.current) {
+ animationFrameId = requestAnimationFrame(animate);
+ }
};
animate();
@@ -1007,6 +996,7 @@ const MapComponent: React.FC = ({ children }) => {
}
};
}, [
+ currentPipeCalData,
currentZoom,
mergedPipeData,
pipeText,
diff --git a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx
index 25dbe73..37d0ea4 100644
--- a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx
+++ b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx
@@ -44,7 +44,7 @@ const AnalysisParameters: React.FC = () => {
const [startTime, setStartTime] = useState(dayjs(new Date()));
const [duration, setDuration] = useState(3600);
const [schemeName, setSchemeName] = useState(
- "FANGAN" + new Date().getTime()
+ "FANGAN" + new Date().getTime(),
);
const [network, setNetwork] = useState(NETWORK_NAME);
const [isSelecting, setIsSelecting] = useState(false);
@@ -88,7 +88,7 @@ const AnalysisParameters: React.FC = () => {
width: 3,
lineDash: [15, 10],
}),
- })
+ }),
);
const geometry = feature.getGeometry();
const lineCoords =
@@ -115,7 +115,7 @@ const AnalysisParameters: React.FC = () => {
scale: 0.2,
anchor: [0.5, 1],
}),
- })
+ }),
);
}
return styles;
@@ -163,14 +163,14 @@ const AnalysisParameters: React.FC = () => {
// 移除不在highlightFeatures中的
const filtered = prevPipes.filter((pipe) =>
highlightFeatures.some(
- (feature) => feature.getProperties().id === pipe.id
- )
+ (feature) => feature.getProperties().id === pipe.id,
+ ),
);
// 添加新的
const newPipes = highlightFeatures
.filter(
(feature) =>
- !filtered.some((p) => p.id === feature.getProperties().id)
+ !filtered.some((p) => p.id === feature.getProperties().id),
)
.map((feature) => {
const properties = feature.getProperties();
@@ -207,7 +207,7 @@ const AnalysisParameters: React.FC = () => {
const featureId = feature.getProperties().id;
setHighlightFeatures((prev) => {
const existingIndex = prev.findIndex(
- (f) => f.getProperties().id === featureId
+ (f) => f.getProperties().id === featureId,
);
if (existingIndex !== -1) {
// 如果已存在,移除
@@ -218,7 +218,7 @@ const AnalysisParameters: React.FC = () => {
}
});
},
- [map]
+ [map],
);
// 开始选择管道
@@ -242,14 +242,14 @@ const AnalysisParameters: React.FC = () => {
const handleRemovePipe = (id: string) => {
// 从高亮features中移除
setHighlightFeatures((prev) =>
- prev.filter((f) => f.getProperties().id !== id)
+ 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))
+ prev.map((pipe) => (pipe.id === id ? { ...pipe, area: numValue } : pipe)),
);
};
@@ -266,30 +266,26 @@ const AnalysisParameters: React.FC = () => {
const burst_ID = pipePoints.map((pipe) => pipe.id);
const burst_size = pipePoints.map((pipe) =>
- parseInt(pipe.area.toString(), 10)
+ 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,
+ 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,
+ scheme_name: schemeName,
};
try {
- await axios.post(`${config.BACKEND_URL}/api/v1/burst_analysis/`, body, {
- headers: {
- "Accept-Encoding": "gzip",
- "Content-Type": "application/json",
- },
+ await axios.get(`${config.BACKEND_URL}/api/v1/burst_analysis/`, {
+ params,
});
-
// 更新弹窗为成功状态
open?.({
key: "burst-analysis",
diff --git a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx
index 1a8a4fe..3460efc 100644
--- a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx
+++ b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx
@@ -19,7 +19,7 @@ import {
} from "@mui/icons-material";
import AnalysisParameters from "./AnalysisParameters";
import SchemeQuery from "./SchemeQuery";
-import LocationResults, { LocationResult } from "./LocationResults";
+import LocationResults from "./LocationResults";
import ContaminantAnalysisParameters from "../ContaminantSimulation/AnalysisParameters";
import ContaminantSchemeQuery from "../ContaminantSimulation/SchemeQuery";
import ContaminantResultsPanel from "../ContaminantSimulation/ResultsPanel";
@@ -27,25 +27,7 @@ import axios from "axios";
import { config } from "@config/config";
import { useNotification } from "@refinedev/core";
import { useData } from "@app/OlMap/MapComponent";
-interface SchemeDetail {
- burst_ID: string[];
- burst_size: number[];
- modify_total_duration: number;
- modify_fixed_pump_pattern: any;
- modify_valve_opening: any;
- modify_variable_pump_pattern: any;
-}
-
-interface SchemeRecord {
- id: number;
- schemeName: string;
- type: string;
- user: string;
- create_time: string;
- startTime: string;
- // 详情信息
- schemeDetail?: SchemeDetail;
-}
+import { LocationResult, SchemeRecord } from "./types";
interface TabPanelProps {
children?: React.ReactNode;
@@ -82,7 +64,7 @@ const BurstPipeAnalysisPanel: React.FC = ({
const [currentTab, setCurrentTab] = useState(0);
const [panelMode, setPanelMode] = useState("burst");
const previousMapText = useRef<{ junction?: string; pipe?: string } | null>(
- null
+ null,
);
const data = useData();
@@ -108,28 +90,10 @@ const BurstPipeAnalysisPanel: React.FC = ({
setCurrentTab(newValue);
};
- useEffect(() => {
- if (!data) return;
- if (panelMode === "contaminant") {
- if (!previousMapText.current) {
- previousMapText.current = {
- junction: data.junctionText,
- pipe: data.pipeText,
- };
- }
- data.setJunctionText?.("quality");
- data.setPipeText?.("quality");
- } else if (panelMode === "burst" && previousMapText.current) {
- data.setJunctionText?.(previousMapText.current.junction || "pressure");
- data.setPipeText?.(previousMapText.current.pipe || "flow");
- previousMapText.current = null;
- }
- }, [panelMode, data]);
-
const handleLocateScheme = async (scheme: SchemeRecord) => {
try {
const response = await axios.get(
- `${config.BACKEND_URL}/api/v1/burst-locate-result/${scheme.schemeName}`
+ `${config.BACKEND_URL}/api/v1/burst-locate-result/${scheme.schemeName}`,
);
setLocationResults(response.data);
setCurrentTab(2); // 切换到定位结果标签页
@@ -304,7 +268,7 @@ const BurstPipeAnalysisPanel: React.FC = ({
onLocate={handleLocateScheme}
/>
) : (
-
+ setCurrentTab(2)} />
)}
diff --git a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx
index 2292ad9..047b730 100644
--- a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx
+++ b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx
@@ -29,15 +29,7 @@ import { Point } from "ol/geom";
import { toLonLat } from "ol/proj";
import moment from "moment";
import "moment-timezone";
-
-export interface LocationResult {
- id: number;
- type: string;
- burst_incident: string;
- leakage: number | null;
- detect_time: string;
- locate_result: string[] | null;
-}
+import { LocationResult } from "./types";
interface LocationResultsProps {
results?: LocationResult[];
@@ -56,7 +48,7 @@ const LocationResults: React.FC = ({ results = [] }) => {
const handleLocatePipes = (pipeIds: string[]) => {
if (pipeIds.length > 0) {
- queryFeaturesByIds(pipeIds).then((features) => {
+ queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => {
if (features.length > 0) {
// 设置高亮要素
setHighlightFeatures(features);
diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx
index a380693..f32ffc1 100644
--- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx
+++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import ReactDOM from "react-dom"; // 添加这行
import {
@@ -49,36 +49,7 @@ import {
import { Point } from "ol/geom";
import { toLonLat } from "ol/proj";
import Timeline from "@app/OlMap/Controls/Timeline";
-
-interface SchemeDetail {
- burst_ID: string[];
- burst_size: number[];
- modify_total_duration: number;
- modify_fixed_pump_pattern: any;
- modify_valve_opening: any;
- modify_variable_pump_pattern: any;
-}
-
-interface SchemeRecord {
- id: number;
- schemeName: string;
- type: string;
- user: string;
- create_time: string;
- startTime: string;
- // 详情信息
- schemeDetail?: SchemeDetail;
-}
-
-interface SchemaItem {
- scheme_id: number;
- scheme_name: string;
- scheme_type: string;
- username: string;
- create_time: string;
- scheme_start_time: string;
- scheme_detail?: SchemeDetail;
-}
+import { SchemaItem, SchemeRecord } from "./types";
interface SchemeQueryProps {
schemes?: SchemeRecord[];
@@ -87,6 +58,8 @@ interface SchemeQueryProps {
network?: string;
}
+const SCHEME_TYPE = "burst_analysis";
+
const SchemeQuery: React.FC = ({
schemes: externalSchemes,
onSchemesChange,
@@ -114,8 +87,8 @@ const SchemeQuery: React.FC = ({
const map = useMap();
const data = useData();
- if (!data) return null;
- const { schemeName, setSchemeName } = data;
+ const { schemeName, setSchemeName } = data || {};
+
// 使用外部提供的 schemes 或内部状态
const schemes =
externalSchemes !== undefined ? externalSchemes : internalSchemes;
@@ -127,13 +100,17 @@ const SchemeQuery: React.FC = ({
return time.format("MM-DD");
};
+ const filteredSchemes = useMemo(() => {
+ return schemes.filter((scheme) => scheme.type === SCHEME_TYPE);
+ }, [schemes]);
+
const handleQuery = async () => {
if (!queryAll && !queryDate) return;
setLoading(true);
try {
const response = await axios.get(
- `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`
+ `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`,
);
let filteredResults = response.data;
@@ -154,7 +131,7 @@ const SchemeQuery: React.FC = ({
create_time: item.create_time,
startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail,
- }))
+ })),
);
if (filteredResults.length === 0) {
@@ -180,14 +157,14 @@ const SchemeQuery: React.FC = ({
const handleLocatePipes = (pipeIds: string[]) => {
if (pipeIds.length > 0) {
- queryFeaturesByIds(pipeIds).then((features) => {
+ queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => {
if (features.length > 0) {
// 设置高亮要素
setHighlightFeatures(features);
// 将 OpenLayers Feature 转换为 GeoJSON Feature
const geojsonFormat = new GeoJSON();
const geojsonFeatures = features.map((feature) =>
- geojsonFormat.writeFeatureObject(feature)
+ geojsonFormat.writeFeatureObject(feature),
);
const extent = bbox(featureCollection(geojsonFeatures as any));
@@ -202,25 +179,24 @@ const SchemeQuery: React.FC = ({
// 内部的方案查询函数
const handleViewDetails = (id: number) => {
+ const scheme = filteredSchemes.find((s) => s.id === id);
+ if (!scheme) return;
+
setShowTimeline(true);
// 计算时间范围
- const scheme = schemes.find((s) => s.id === id);
- const burstPipeIds = scheme?.schemeDetail?.burst_ID || [];
- const schemeDate = scheme?.startTime
+ const schemeDate = scheme.startTime
? new Date(scheme.startTime)
: undefined;
- if (scheme?.startTime && scheme.schemeDetail?.modify_total_duration) {
+ if (scheme.startTime && scheme.schemeDetail?.modify_total_duration) {
const start = new Date(scheme.startTime);
const end = new Date(
- start.getTime() + scheme.schemeDetail.modify_total_duration * 1000
+ start.getTime() + scheme.schemeDetail.modify_total_duration * 1000,
);
setSelectedDate(schemeDate);
setTimeRange({ start, end });
- if (setSchemeName) {
- setSchemeName(scheme.schemeName);
- }
- handleLocatePipes(burstPipeIds);
}
+ setSchemeName?.(scheme.schemeName);
+ handleLocatePipes(scheme.schemeDetail?.burst_ID || []);
};
// 初始化管道图层和高亮图层
@@ -254,7 +230,7 @@ const SchemeQuery: React.FC = ({
width: 3,
lineDash: [15, 10],
}),
- })
+ }),
);
const geometry = feature.getGeometry();
const lineCoords =
@@ -281,7 +257,7 @@ const SchemeQuery: React.FC = ({
scale: 0.2,
anchor: [0.5, 1],
}),
- })
+ }),
);
}
return styles;
@@ -336,9 +312,9 @@ const SchemeQuery: React.FC = ({
timeRange={timeRange}
disableDateSelection={!!timeRange}
schemeName={schemeName}
- schemeType="burst_Analysis"
+ schemeType={SCHEME_TYPE}
/>,
- mapContainer // 渲染到地图容器中,而不是 body
+ mapContainer, // 渲染到地图容器中,而不是 body
)}
{/* 查询条件 - 单行布局 */}
@@ -391,7 +367,7 @@ const SchemeQuery: React.FC = ({
{/* 结果列表 */}
- {schemes.length === 0 ? (
+ {filteredSchemes.length === 0 ? (
@@ -652,7 +632,7 @@ const SchemeQuery: React.FC = ({
className="border-blue-600 text-blue-600 hover:bg-blue-50"
onClick={() =>
handleLocatePipes?.(
- scheme.schemeDetail!.burst_ID
+ scheme.schemeDetail!.burst_ID,
)
}
sx={{
diff --git a/src/components/olmap/BurstPipeAnalysis/types.ts b/src/components/olmap/BurstPipeAnalysis/types.ts
new file mode 100644
index 0000000..9da252f
--- /dev/null
+++ b/src/components/olmap/BurstPipeAnalysis/types.ts
@@ -0,0 +1,38 @@
+export interface SchemeDetail {
+ burst_ID: string[];
+ burst_size: number[];
+ modify_total_duration: number;
+ modify_fixed_pump_pattern: any;
+ modify_valve_opening: any;
+ modify_variable_pump_pattern: any;
+}
+
+export interface SchemeRecord {
+ id: number;
+ schemeName: string;
+ type: string;
+ user: string;
+ create_time: string;
+ startTime: string;
+ // 详情信息
+ schemeDetail?: SchemeDetail;
+}
+
+export interface SchemaItem {
+ scheme_id: number;
+ scheme_name: string;
+ scheme_type: string;
+ username: string;
+ create_time: string;
+ scheme_start_time: string;
+ scheme_detail?: SchemeDetail;
+}
+
+export interface LocationResult {
+ id: number;
+ type: string;
+ burst_incident: string;
+ leakage: number | null;
+ detect_time: string;
+ locate_result: string[] | null;
+}
diff --git a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx
index 353ab29..560ef75 100644
--- a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx
+++ b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx
@@ -22,7 +22,7 @@ import { config, NETWORK_NAME } from "@/config/config";
import { useMap } from "@app/OlMap/MapComponent";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
-import { Style, Stroke, Fill, Circle as CircleStyle } from "ol/style";
+import { Style, Stroke, Fill, Circle as CircleStyle, Icon } from "ol/style";
import Feature from "ol/Feature";
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
@@ -31,10 +31,13 @@ const AnalysisParameters: React.FC = () => {
const { open } = useNotification();
const network = NETWORK_NAME;
+ const [schemeName, setSchemeName] = useState(
+ "WQ_" + new Date().getTime(),
+ );
const [startTime, setStartTime] = useState(dayjs(new Date()));
const [sourceNode, setSourceNode] = useState("");
- const [concentration, setConcentration] = useState(1);
- const [duration, setDuration] = useState(900);
+ const [concentration, setConcentration] = useState(100);
+ const [duration, setDuration] = useState(3600);
const [pattern, setPattern] = useState("");
const [isSelecting, setIsSelecting] = useState(false);
const [submitting, setSubmitting] = useState(false);
@@ -42,7 +45,7 @@ const AnalysisParameters: React.FC = () => {
const [highlightLayer, setHighlightLayer] =
useState | null>(null);
const [highlightFeature, setHighlightFeature] = useState(
- null
+ null,
);
const isFormValid = useMemo(() => {
@@ -51,20 +54,41 @@ const AnalysisParameters: React.FC = () => {
Boolean(startTime) &&
Boolean(sourceNode) &&
concentration > 0 &&
- duration > 0
+ duration > 0 &&
+ schemeName.trim() !== ""
);
- }, [network, startTime, sourceNode, concentration, duration]);
+ }, [network, startTime, sourceNode, concentration, duration, schemeName]);
useEffect(() => {
if (!map) return;
- const sourceStyle = new Style({
- image: new CircleStyle({
- radius: 10,
- fill: new Fill({ color: "rgba(37, 125, 212, 0.35)" }),
- stroke: new Stroke({ color: "rgba(37, 125, 212, 1)", width: 3 }),
+ const themeColor = "rgba(3, 168, 107"; // #03a86b
+
+ const sourceStyle = [
+ // 外层扩散光圈
+ new Style({
+ image: new CircleStyle({
+ radius: 12,
+ fill: new Fill({ color: `${themeColor}, 0.2)` }),
+ }),
}),
- });
+ // 中层扩散背景
+ new Style({
+ image: new CircleStyle({
+ radius: 8,
+ stroke: new Stroke({ color: `${themeColor}, 0.5)`, width: 2 }),
+ fill: new Fill({ color: `${themeColor}, 0.3)` }),
+ }),
+ }),
+ // 上层图标
+ new Style({
+ image: new Icon({
+ src: "/icons/contaminant_source.svg",
+ scale: 0.2,
+ anchor: [0.5, 1],
+ }),
+ }),
+ ];
const layer = new VectorLayer({
source: new VectorSource(),
@@ -117,7 +141,7 @@ const AnalysisParameters: React.FC = () => {
setIsSelecting(false);
map.un("click", handleMapClickSelectFeatures);
},
- [map, open]
+ [map, open],
);
const handleStartSelection = () => {
@@ -146,15 +170,19 @@ const AnalysisParameters: React.FC = () => {
message: "方案提交分析中",
undoableTimeout: 3,
});
-
+ // 格式化开始时间,去除秒部分
+ const start_time = startTime
+ ? startTime.format("YYYY-MM-DDTHH:mm:00Z")
+ : "";
try {
const params = {
network,
- start_time: startTime.toISOString(),
+ start_time: start_time,
source: sourceNode,
concentration,
duration,
pattern: pattern || undefined,
+ scheme_name: schemeName,
};
await axios.get(`${config.BACKEND_URL}/api/v1/contaminant_simulation/`, {
@@ -297,13 +325,14 @@ const AnalysisParameters: React.FC = () => {
- 管网名称
+ 方案名称
setSchemeName(e.target.value)}
+ placeholder="输入方案名称"
/>
diff --git a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx
index 51c2b2c..0dc0c87 100644
--- a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx
+++ b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx
@@ -31,24 +31,36 @@ import { useNotification } from "@refinedev/core";
import { config, NETWORK_NAME } from "@config/config";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import { useData, useMap } from "@app/OlMap/MapComponent";
+import { GeoJSON } from "ol/format";
+import VectorLayer from "ol/layer/Vector";
+import VectorSource from "ol/source/Vector";
+import { Style, Icon, Circle, Fill, Stroke } from "ol/style";
+import Feature from "ol/Feature";
+import { bbox, featureCollection } from "@turf/turf";
import Timeline from "@app/OlMap/Controls/Timeline";
import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types";
interface SchemeQueryProps {
schemes?: ContaminantSchemeRecord[];
onSchemesChange?: (schemes: ContaminantSchemeRecord[]) => void;
+ onViewResults?: () => void;
network?: string;
}
-const SCHEME_TYPE = "contaminant_simulation";
+const SCHEME_TYPE = "contaminant_analysis";
const SchemeQuery: React.FC = ({
schemes: externalSchemes,
onSchemesChange,
+ onViewResults,
network = NETWORK_NAME,
}) => {
const [queryAll, setQueryAll] = useState(true);
const [queryDate, setQueryDate] = useState(dayjs(new Date()));
+ const [highlightLayer, setHighlightLayer] =
+ useState | null>(null);
+ const [highlightFeatures, setHighlightFeatures] = useState([]);
+
const [showTimeline, setShowTimeline] = useState(false);
const [selectedDate, setSelectedDate] = useState(undefined);
const [timeRange, setTimeRange] = useState<
@@ -64,8 +76,7 @@ const SchemeQuery: React.FC = ({
const { open } = useNotification();
const map = useMap();
const data = useData();
- const { schemeName, setSchemeName, setJunctionText, setPipeText } =
- data || {};
+ const { schemeName, setSchemeName } = data || {};
const schemes =
externalSchemes !== undefined ? externalSchemes : internalSchemes;
@@ -79,6 +90,83 @@ const SchemeQuery: React.FC = ({
}
}, [map]);
+ // 初始化高亮图层
+ useEffect(() => {
+ if (!map) return;
+
+ const themeColor = "rgba(3, 168, 107"; // #03a86b
+
+ const sourceStyle = [
+ // 外层扩散光圈
+ new Style({
+ image: new Circle({
+ radius: 12,
+ fill: new Fill({
+ color: `${themeColor}, 0.2)`,
+ }),
+ }),
+ }),
+ // 中层扩散背景
+ new Style({
+ image: new Circle({
+ radius: 8,
+ stroke: new Stroke({
+ color: `${themeColor}, 0.5)`,
+ width: 2,
+ }),
+ fill: new Fill({
+ color: `${themeColor}, 0.3)`,
+ }),
+ }),
+ }),
+ // 上层图标
+ new Style({
+ image: new Icon({
+ src: "/icons/contaminant_source.svg",
+ scale: 0.2,
+ anchor: [0.5, 1],
+ }),
+ }),
+ ];
+
+ const highlightLayer = new VectorLayer({
+ source: new VectorSource(),
+ style: sourceStyle,
+ maxZoom: 24,
+ minZoom: 12,
+ properties: {
+ name: "污染源高亮",
+ value: "contaminant_source_highlight",
+ },
+ });
+
+ map.addLayer(highlightLayer);
+ setHighlightLayer(highlightLayer);
+
+ return () => {
+ map.removeLayer(highlightLayer);
+ };
+ }, [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, highlightLayer]);
+
const formatTime = (timeStr: string) => {
const time = moment(timeStr);
return time.format("MM-DD");
@@ -93,16 +181,18 @@ const SchemeQuery: React.FC = ({
setLoading(true);
try {
const response = await axios.get(
- `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`
+ `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`,
);
let filteredResults = response.data;
if (!queryAll) {
const formattedDate = queryDate!.format("YYYY-MM-DD");
- filteredResults = response.data.filter((item: ContaminantSchemaItem) => {
- const itemDate = moment(item.create_time).format("YYYY-MM-DD");
- return itemDate === formattedDate;
- });
+ filteredResults = response.data.filter(
+ (item: ContaminantSchemaItem) => {
+ const itemDate = moment(item.create_time).format("YYYY-MM-DD");
+ return itemDate === formattedDate;
+ },
+ );
}
setSchemes(
@@ -114,7 +204,7 @@ const SchemeQuery: React.FC = ({
create_time: item.create_time,
startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail,
- }))
+ })),
);
if (filteredResults.length === 0) {
@@ -138,19 +228,29 @@ const SchemeQuery: React.FC = ({
}
};
- const handleLocateSource = (sourceId?: string) => {
- if (!sourceId) return;
- queryFeaturesByIds([sourceId], "geo_junctions_mat").then((features) => {
- if (features.length > 0) {
- const extent = features[0].getGeometry()?.getExtent();
- if (extent) {
- map?.getView().fit(extent, {
- maxZoom: 18,
- duration: 1000,
- });
+ const handleLocateSource = (sourceIds: string[]) => {
+ if (sourceIds.length > 0) {
+ queryFeaturesByIds(sourceIds, "geo_junctions_mat").then((features) => {
+ if (features.length > 0) {
+ // 设置高亮要素
+ setHighlightFeatures(features);
+ // 将 OpenLayers Feature 转换为 GeoJSON Feature
+ const geojsonFormat = new GeoJSON();
+ const geojsonFeatures = features.map((feature) =>
+ geojsonFormat.writeFeatureObject(feature),
+ );
+
+ const extent = bbox(featureCollection(geojsonFeatures as any));
+
+ if (extent) {
+ map?.getView().fit(extent, {
+ maxZoom: 18,
+ duration: 1000,
+ });
+ }
}
- }
- });
+ });
+ }
};
const handleViewDetails = (id: number) => {
@@ -158,17 +258,21 @@ const SchemeQuery: React.FC = ({
if (!scheme) return;
setShowTimeline(true);
- const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined;
+ const schemeDate = scheme.startTime
+ ? new Date(scheme.startTime)
+ : undefined;
if (scheme.startTime && scheme.schemeDetail?.duration) {
const start = new Date(scheme.startTime);
- const end = new Date(start.getTime() + scheme.schemeDetail.duration * 1000);
+ const end = new Date(
+ start.getTime() + scheme.schemeDetail.duration * 1000,
+ );
setSelectedDate(schemeDate);
setTimeRange({ start, end });
}
setSchemeName?.(scheme.schemeName);
- setJunctionText?.("quality");
- setPipeText?.("quality");
- handleLocateSource(scheme.schemeDetail?.source);
+ if (scheme.schemeDetail?.source) {
+ handleLocateSource([scheme.schemeDetail.source]);
+ }
};
return (
@@ -183,7 +287,7 @@ const SchemeQuery: React.FC = ({
schemeName={schemeName}
schemeType={SCHEME_TYPE}
/>,
- mapContainer
+ mapContainer,
)}
@@ -291,7 +395,11 @@ const SchemeQuery: React.FC = ({
{scheme.schemeName}
= ({
variant="caption"
className="text-gray-500 block"
>
- ID: {scheme.id} · 日期: {formatTime(scheme.create_time)}
+ ID: {scheme.id} · 日期:{" "}
+ {formatTime(scheme.create_time)}
setExpandedId(
- expandedId === scheme.id ? null : scheme.id
+ expandedId === scheme.id ? null : scheme.id,
)
}
color="primary"
@@ -322,16 +433,19 @@ const SchemeQuery: React.FC = ({
-
+ {/*
handleLocateSource(scheme.schemeDetail?.source)}
+ onClick={() =>
+ scheme.schemeDetail?.source &&
+ handleLocateSource([scheme.schemeDetail.source])
+ }
color="primary"
className="p-1"
>
-
+ */}
@@ -355,7 +469,9 @@ const SchemeQuery: React.FC = ({
className="font-medium text-blue-600 hover:text-blue-800 underline cursor-pointer"
onClick={(e) => {
e.preventDefault();
- handleLocateSource(scheme.schemeDetail?.source);
+ handleLocateSource([
+ scheme.schemeDetail!.source!,
+ ]);
}}
>
{scheme.schemeDetail.source}
@@ -381,7 +497,8 @@ const SchemeQuery: React.FC = ({
variant="caption"
className="font-medium text-gray-900"
>
- {scheme.schemeDetail?.concentration ?? "N/A"} mg/L
+ {scheme.schemeDetail?.concentration ?? "N/A"}{" "}
+ mg/L
@@ -443,7 +560,7 @@ const SchemeQuery: React.FC = ({
className="font-medium text-gray-900"
>
{moment(scheme.create_time).format(
- "YYYY-MM-DD HH:mm"
+ "YYYY-MM-DD HH:mm",
)}
@@ -459,7 +576,7 @@ const SchemeQuery: React.FC = ({
className="font-medium text-gray-900"
>
{moment(scheme.startTime).format(
- "YYYY-MM-DD HH:mm"
+ "YYYY-MM-DD HH:mm",
)}
@@ -473,7 +590,10 @@ const SchemeQuery: React.FC = ({
fullWidth
size="small"
className="border-blue-600 text-blue-600 hover:bg-blue-50"
- onClick={() => handleLocateSource(scheme.schemeDetail?.source)}
+ onClick={() =>
+ scheme.schemeDetail?.source &&
+ handleLocateSource([scheme.schemeDetail.source])
+ }
sx={{
textTransform: "none",
fontWeight: 500,
diff --git a/src/components/olmap/ContaminantSimulation/types.ts b/src/components/olmap/ContaminantSimulation/types.ts
index 407c08d..fd8ef45 100644
--- a/src/components/olmap/ContaminantSimulation/types.ts
+++ b/src/components/olmap/ContaminantSimulation/types.ts
@@ -3,7 +3,6 @@ export interface ContaminantSchemeDetail {
concentration: number;
duration: number;
pattern?: string | null;
- start_time?: string;
}
export interface ContaminantSchemeRecord {
diff --git a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx
index 5414a1c..ad36ab6 100644
--- a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx
+++ b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx
@@ -141,6 +141,7 @@ const SchemeQuery: React.FC = ({
}
});
}, [highlightFeatures]);
+
// 查询方案
const handleQuery = async () => {
if (!queryAll && !queryDate) return;
@@ -148,7 +149,7 @@ const SchemeQuery: React.FC = ({
setLoading(true);
try {
const response = await axios.get(
- `${config.BACKEND_URL}/api/v1/getallsensorplacements/?network=${network}`
+ `${config.BACKEND_URL}/api/v1/getallsensorplacements/?network=${network}`,
);
let filteredResults = response.data;
@@ -171,7 +172,7 @@ const SchemeQuery: React.FC = ({
user: item.username,
create_time: item.create_time,
sensorLocation: item.sensor_location,
- }))
+ })),
);
if (filteredResults.length === 0) {
@@ -206,14 +207,14 @@ const SchemeQuery: React.FC = ({
}
if (sensorIds.length > 0) {
- queryFeaturesByIds(sensorIds).then((features) => {
+ queryFeaturesByIds(sensorIds, "geo_junctions_mat").then((features) => {
if (features.length > 0) {
// 设置高亮要素
setHighlightFeatures(features);
// 将 OpenLayers Feature 转换为 GeoJSON Feature
const geojsonFormat = new GeoJSON();
const geojsonFeatures = features.map((feature) =>
- geojsonFormat.writeFeatureObject(feature)
+ geojsonFormat.writeFeatureObject(feature),
);
const extent = bbox(featureCollection(geojsonFeatures as any));
@@ -376,7 +377,7 @@ const SchemeQuery: React.FC = ({
size="small"
onClick={() =>
setExpandedId(
- expandedId === scheme.id ? null : scheme.id
+ expandedId === scheme.id ? null : scheme.id,
)
}
color="primary"
@@ -385,7 +386,7 @@ const SchemeQuery: React.FC = ({
-
+ {/*
onLocate?.(scheme.id)}
@@ -394,7 +395,7 @@ const SchemeQuery: React.FC = ({
>
-
+ */}
@@ -466,7 +467,7 @@ const SchemeQuery: React.FC = ({
className="font-medium text-gray-900"
>
{moment(scheme.create_time).format(
- "YYYY-MM-DD HH:mm"
+ "YYYY-MM-DD HH:mm",
)}
@@ -500,7 +501,7 @@ const SchemeQuery: React.FC = ({
>
{sensorId}
- )
+ ),
)}