From fa0970bd793500ac8446bbaeba694ffc747f0ee9 Mon Sep 17 00:00:00 2001 From: JIANG Date: Fri, 10 Oct 2025 15:12:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=97=B6=E9=97=B4=E8=BD=B4?= =?UTF-8?q?=E5=89=8D=E5=90=8E=E7=AB=AF=E6=95=B0=E6=8D=AE=E8=BF=9E=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/network-simulation/page.tsx | 9 +- src/app/OlMap/Controls/DrawPanel.tsx | 2 +- src/app/OlMap/Controls/PropertyPanel.tsx | 4 +- src/app/OlMap/Controls/ScaleLine.tsx | 4 +- src/app/OlMap/Controls/StyleEditorPanel.tsx | 192 ++++++++--------- src/app/OlMap/Controls/StyleLegend.tsx | 2 +- src/app/OlMap/Controls/Timeline.tsx | 215 +++++++++++++++----- src/app/OlMap/MapComponent.tsx | 194 +++++++----------- src/app/OlMap/MapTools.tsx | 3 +- src/components/olmap/SCADADataPanel.tsx | 7 +- src/components/olmap/SCADADeviceList.tsx | 9 +- src/utils/breaks_classification.js | 25 ++- src/utils/parseColor.js | 35 ++++ 13 files changed, 416 insertions(+), 285 deletions(-) create mode 100644 src/utils/parseColor.js diff --git a/src/app/(main)/network-simulation/page.tsx b/src/app/(main)/network-simulation/page.tsx index 8d1c9ef..9306ea5 100644 --- a/src/app/(main)/network-simulation/page.tsx +++ b/src/app/(main)/network-simulation/page.tsx @@ -74,10 +74,11 @@ export default function Home() { return (
- -
- -
+ +
+ +
+
{ const isSaveDisabled = drawnFeatures.length === 0; return ( -
+
} diff --git a/src/app/OlMap/Controls/PropertyPanel.tsx b/src/app/OlMap/Controls/PropertyPanel.tsx index f1138c3..f96ccd3 100644 --- a/src/app/OlMap/Controls/PropertyPanel.tsx +++ b/src/app/OlMap/Controls/PropertyPanel.tsx @@ -20,7 +20,7 @@ const PropertyPanel: React.FC = ({ }) => { if (!id) { return ( -
+

属性面板

请选择一个要素以查看其属性。

@@ -38,7 +38,7 @@ const PropertyPanel: React.FC = ({ }; return ( -
+

属性面板

diff --git a/src/app/OlMap/Controls/ScaleLine.tsx b/src/app/OlMap/Controls/ScaleLine.tsx index 0911ef8..0b9090a 100644 --- a/src/app/OlMap/Controls/ScaleLine.tsx +++ b/src/app/OlMap/Controls/ScaleLine.tsx @@ -36,9 +36,9 @@ const Scale: React.FC = () => { return (
-
Zoom Level: {zoomLevel.toFixed(1)}
+
缩放: {zoomLevel.toFixed(1)}
- Coordinates: {coordinates[0]}, {coordinates[1]} + 坐标: {coordinates[0]}, {coordinates[1]}
); diff --git a/src/app/OlMap/Controls/StyleEditorPanel.tsx b/src/app/OlMap/Controls/StyleEditorPanel.tsx index 1ef5ce5..9410c81 100644 --- a/src/app/OlMap/Controls/StyleEditorPanel.tsx +++ b/src/app/OlMap/Controls/StyleEditorPanel.tsx @@ -19,11 +19,14 @@ import { // 导入OpenLayers样式相关模块 import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; -import { useMap } from "../MapComponent"; +import { useData, useMap } from "../MapComponent"; import StyleLegend, { LegendStyleConfig } from "./StyleLegend"; import { FlatStyleLike } from "ol/style/flat"; +import { calculateClassification } from "@utils/breaks_classification"; +import { parseColor } from "@utils/parseColor"; + interface StyleConfig { property: string; classificationMethod: string; // 分类方法 @@ -96,6 +99,22 @@ const CLASSIFICATION_METHODS = [ const StyleEditorPanel: React.FC = () => { const map = useMap(); + const data = useData(); + if (!data) { + return
Loading...
; // 或其他占位符 + } + const { + junctionData, + pipeData, + setShowJunctionText, + setShowPipeText, + setJunctionText, + setPipeText, + } = data; + + const [applyJunctionStyle, setApplyJunctionStyle] = useState(false); + const [applyPipeStyle, setApplyPipeStyle] = useState(false); + const [renderLayers, setRenderLayers] = useState([]); const [selectedRenderLayer, setSelectedRenderLayer] = useState(); @@ -134,75 +153,10 @@ const StyleEditorPanel: React.FC = () => { breaks: [], } ); - // 样式状态管理 - 存储多个图层的样式状态 const [layerStyleStates, setLayerStyleStates] = useState( [] ); - - // 通用颜色解析函数 - const parseColor = useCallback((color: string) => { - // 解析 rgba 格式的颜色 - const match = color.match( - /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/ - ); - if (match) { - return { - r: parseInt(match[1], 10), - g: parseInt(match[2], 10), - b: parseInt(match[3], 10), - // 如果没有 alpha 值,默认为 1 - a: match[4] ? parseFloat(match[4]) : 1, - }; - } - // 如果还是十六进制格式,保持原来的解析方式 - const hex = color.replace("#", ""); - return { - r: parseInt(hex.slice(0, 2), 16), - g: parseInt(hex.slice(2, 4), 16), - b: parseInt(hex.slice(4, 6), 16), - }; - }, []); - // 获取数据分段分类结果 - const fetchClassification = async ( - layer_name: string, - prop: string, - n_classes: number, - algorithm: string - ) => { - if (!algorithm) { - algorithm = "pretty_breaks"; // 默认算法 - } - const response = await fetch("http://localhost:8000/jenks-classification", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - layer_name: layer_name, // 图层名称 - prop: prop, // 属性名称 - n_classes: n_classes, // 分段数 - algorithm: algorithm, - // algorithm: "pretty_breaks", - // algorithm: "hybrid_jenks" - }), - }); - if (!response.ok) { - console.error("API 请求失败:", response.status, response.statusText); - return false; - } - - const data = await response.json(); - const breaks = data.breaks; // 从响应对象中提取 breaks 数组 - // console.log(breaks); - // 验证返回的数据 - if (!Array.isArray(breaks) || breaks.length === 0) { - console.error("API 返回的 breaks 不是有效数组:", breaks); - return false; - } - - return breaks; - }; // 颜色方案选择 const [singlePaletteIndex, setSinglePaletteIndex] = useState(0); const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0); @@ -232,21 +186,45 @@ const StyleEditorPanel: React.FC = () => { [gradientPaletteIndex, parseColor] ); // 应用分类样式 - const applyStyle = (breaks?: number[]) => { + const setStyleState = (layer: any) => { + if ( + layer.get("value") !== undefined && + styleConfig.property !== undefined + ) { + // 更新文字标签设置 + if (layer.get("value") === "junctions") { + if (setJunctionText && setShowJunctionText) { + setJunctionText(styleConfig.property); + setShowJunctionText(styleConfig.showLabels); + setApplyJunctionStyle(true); + } + } + if (layer.get("value") === "pipes") { + if (setPipeText && setShowPipeText) { + setPipeText(styleConfig.property); + setShowPipeText(styleConfig.showLabels); + setApplyPipeStyle(true); + } + } + } + }; + const applyStyle = (layerId: string, breaks?: number[]) => { // 使用传入的 breaks 数据 - if (!breaks) { + if (!breaks || breaks.length === 0) { console.warn("没有有效的 breaks 数据"); return; } - if (!selectedRenderLayer || !styleConfig.property) return; + const styleConfig = layerStyleStates.find( + (s) => s.layerId === layerId + )?.styleConfig; + const selectedRenderLayer = renderLayers.find( + (l) => l.get("id") === layerId + ); + if (!selectedRenderLayer || !styleConfig?.property) return; const layerType: string = selectedRenderLayer?.get("type"); const source = selectedRenderLayer.getSource(); if (!source) return; - if (breaks.length === 0) { - console.log("没有有效的 breaks 数据,无法应用样式"); - return; - } const breaksLength = breaks.length; // 根据 breaks 计算每个分段的颜色,线条粗细 const colors: string[] = @@ -385,8 +363,45 @@ const StyleEditorPanel: React.FC = () => { setLayerStyleStates((prev) => prev.filter((state) => state.layerId !== layerId) ); + // 重置样式应用状态 + if (layerId === "junctions") { + setApplyJunctionStyle(false); + } else if (layerId === "pipes") { + setApplyPipeStyle(false); + } } }, [selectedRenderLayer]); + + useEffect(() => { + if (applyJunctionStyle && junctionData.length > 0) { + // 应用节点样式 + const junctionStyleConfigState = layerStyleStates.find( + (s) => s.layerId === "junctions" + ); + if (!junctionStyleConfigState) return; + const segments = junctionStyleConfigState?.styleConfig.segments; + const breaks = calculateClassification( + junctionData, + segments, + styleConfig.classificationMethod + ); + applyStyle(junctionStyleConfigState.layerId, breaks); + } + if (applyPipeStyle && pipeData.length > 0) { + // 应用管道样式 + const pipeStyleConfigState = layerStyleStates.find( + (s) => s.layerId === "pipes" + ); + if (!pipeStyleConfigState) return; + const segments = pipeStyleConfigState?.styleConfig.segments; + const breaks = calculateClassification( + pipeData, + segments, + styleConfig.classificationMethod + ); + applyStyle(pipeStyleConfigState.layerId, breaks); + } + }, [junctionData, pipeData, applyJunctionStyle, applyPipeStyle]); // 样式状态管理功能 // 保存当前图层的样式状态 const saveLayerStyle = useCallback( @@ -436,8 +451,9 @@ const StyleEditorPanel: React.FC = () => { const updateVisibleLayers = () => { const layers = map.getAllLayers(); // 筛选矢量瓦片图层 - const webGLVectorTileLayers = layers.filter((layer) => - layer.get("value") + const webGLVectorTileLayers = layers.filter( + (layer) => + layer.get("value") === "junctions" || layer.get("value") === "pipes" // 暂时只处理这两个图层 ) as WebGLVectorTileLayer[]; setRenderLayers(webGLVectorTileLayers); @@ -505,6 +521,7 @@ const StyleEditorPanel: React.FC = () => { })); } }, [styleConfig.colorType]); + // 获取所有激活的图例配置 const getActiveLegendConfigs = useCallback(() => { return layerStyleStates @@ -803,7 +820,7 @@ const StyleEditorPanel: React.FC = () => { return ( <> -
+
{/* 图层选择 */} 选择图层 @@ -948,25 +965,8 @@ const StyleEditorPanel: React.FC = () => {
{/* 显示多图层图例 */} {getActiveLegendConfigs().length > 0 && ( -
+
{getActiveLegendConfigs().map((config, index) => ( diff --git a/src/app/OlMap/Controls/StyleLegend.tsx b/src/app/OlMap/Controls/StyleLegend.tsx index cd7f879..8c8af74 100644 --- a/src/app/OlMap/Controls/StyleLegend.tsx +++ b/src/app/OlMap/Controls/StyleLegend.tsx @@ -27,7 +27,7 @@ const StyleLegend: React.FC = ({ return ( {layerName} - {property} diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx index 7c22f82..4ea87a6 100644 --- a/src/app/OlMap/Controls/Timeline.tsx +++ b/src/app/OlMap/Controls/Timeline.tsx @@ -7,7 +7,6 @@ import { Slider, Typography, Paper, - TextField, MenuItem, Select, FormControl, @@ -22,26 +21,18 @@ import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import { zhCN } from "date-fns/locale"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { TbRewindBackward5, TbRewindForward5 } from "react-icons/tb"; +import { useData } from "../MapComponent"; +import { config } from "@/config/config"; -interface TimelineProps { - onTimeChange?: (time: string) => void; - onDateChange?: (date: Date) => void; - onPlay?: () => void; - onPause?: () => void; - onStop?: () => void; - onRefresh?: () => void; - onFetch?: () => void; -} +const backendUrl = config.backendUrl; +const Timeline: React.FC = () => { + const data = useData(); + if (!data) { + return
Loading...
; // 或其他占位符 + } + const { setJunctionDataState, setPipeDataState, junctionText, pipeText } = + data; -const Timeline: React.FC = ({ - onTimeChange, - onDateChange, - onPlay, - onPause, - onStop, - onRefresh, - onFetch, -}) => { const [currentTime, setCurrentTime] = useState(0); // 分钟数 (0-1439) const [selectedDate, setSelectedDate] = useState(new Date()); const [isPlaying, setIsPlaying] = useState(false); @@ -51,6 +42,114 @@ const Timeline: React.FC = ({ const intervalRef = useRef(null); const timelineRef = useRef(null); + // 添加缓存引用 + const cacheRef = useRef< + Map + >(new Map()); + // 添加防抖引用 + const debounceRef = useRef(null); + + const fetchFrameData = async (queryTime: Date) => { + const query_time = queryTime.toISOString(); + const cacheKey = query_time; + + // 检查缓存 + if (cacheRef.current.has(cacheKey)) { + const { nodeRecords, linkRecords } = cacheRef.current.get(cacheKey)!; + // 使用缓存数据更新状态 + updateDataStates(nodeRecords, linkRecords); + return; + } + + try { + // 定义需要查询的属性 + const junctionProperties = junctionText; + const pipeProperties = pipeText; + if ( + !junctionProperties || + !pipeProperties || + junctionProperties === "" || + pipeProperties === "" + ) { + return; + } + console.log( + "Query Time:", + queryTime.toLocaleDateString() + " " + queryTime.toLocaleTimeString() + ); + // 同时查询节点和管道数据 + const [nodeResponse, linkResponse] = await Promise.all([ + fetch( + `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` + ), + fetch( + `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` + ), + ]); + + const nodeRecords = await nodeResponse.json(); + const linkRecords = await linkResponse.json(); + + // 缓存数据 + cacheRef.current.set(cacheKey, { + nodeRecords: nodeRecords.results, + linkRecords: linkRecords.results, + }); + + // 更新状态 + updateDataStates(nodeRecords.results, linkRecords.results); + } catch (error) { + console.error("Error fetching data:", error); + } + }; + + // 提取更新状态的逻辑 + const updateDataStates = (nodeResults: any[], linkResults: any[]) => { + const junctionProperties = junctionText; + const pipeProperties = pipeText; + + // 将 nodeRecords 转换为 Map 以提高查找效率 + const nodeMap: Map = new Map( + nodeResults.map((r: any) => [r.ID, r]) + ); + // 将 linkRecords 转换为 Map 以提高查找效率 + const linkMap: Map = new Map( + linkResults.map((r: any) => [r.ID, r]) + ); + + // 更新junctionData + setJunctionDataState((prev: any[]) => + prev.map((j) => { + const record = nodeMap.get(j.id); + if (record) { + return { + ...j, + [junctionProperties]: record.value, + }; + } + return j; + }) + ); + + // 更新pipeData + setPipeDataState((prev: any[]) => + prev.map((p) => { + const record = linkMap.get(p.id); + if (record) { + return { + ...p, + flowFlag: pipeProperties === "flow" && record.value < 0 ? -1 : 1, + path: + pipeProperties === "flow" && record.value < 0 && p.flowFlag > 0 + ? [...p.path].reverse() + : p.path, + [pipeProperties]: record.value, + }; + } + return p; + }) + ); + }; // 时间刻度数组 (每5分钟一个刻度) const timeMarks = Array.from({ length: 288 }, (_, i) => ({ @@ -67,14 +166,22 @@ const Timeline: React.FC = ({ .padStart(2, "0")}`; } + function currentTimeToDate(selectedDate: Date, minutes: number): Date { + const date = new Date(selectedDate); + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + date.setHours(hours, mins, 0, 0); + return date; + } + // 播放时间间隔选项 const intervalOptions = [ - { value: 1000, label: "1秒" }, + // { value: 1000, label: "1秒" }, { value: 2000, label: "2秒" }, { value: 5000, label: "5秒" }, { value: 10000, label: "10秒" }, ]; - // 播放时间间隔选项 + // 强制计算时间段选项 const calculatedIntervalOptions = [ { value: 1440, label: "1 天" }, { value: 60, label: "1 小时" }, @@ -88,79 +195,73 @@ const Timeline: React.FC = ({ (event: Event, newValue: number | number[]) => { const value = Array.isArray(newValue) ? newValue[0] : newValue; setSliderValue(value); - setCurrentTime(value); - onTimeChange?.(formatTime(value)); + // 防抖设置currentTime,避免频繁触发数据获取 + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + debounceRef.current = setTimeout(() => { + setCurrentTime(value); + }, 300); // 300ms 防抖延迟 }, - [onTimeChange] + [] ); // 播放控制 const handlePlay = useCallback(() => { if (!isPlaying) { setIsPlaying(true); - onPlay?.(); intervalRef.current = setInterval(() => { setCurrentTime((prev) => { const next = prev >= 1435 ? 0 : prev + 5; // 到达23:55后回到00:00 setSliderValue(next); - onTimeChange?.(formatTime(next)); return next; }); }, playInterval); } - }, [isPlaying, playInterval, onPlay, onTimeChange]); + }, [isPlaying, playInterval]); const handlePause = useCallback(() => { setIsPlaying(false); - onPause?.(); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } - }, [onPause]); + }, []); const handleStop = useCallback(() => { setIsPlaying(false); setCurrentTime(0); setSliderValue(0); - onStop?.(); - onTimeChange?.(formatTime(0)); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } - }, [onStop, onTimeChange]); + }, []); // 步进控制 const handleStepBackward = useCallback(() => { setCurrentTime((prev) => { const next = prev <= 0 ? 1435 : prev - 5; setSliderValue(next); - onTimeChange?.(formatTime(next)); return next; }); - }, [onTimeChange]); + }, []); const handleStepForward = useCallback(() => { setCurrentTime((prev) => { const next = prev >= 1435 ? 0 : prev + 5; setSliderValue(next); - onTimeChange?.(formatTime(next)); return next; }); - }, [onTimeChange]); + }, []); // 日期选择处理 - const handleDateChange = useCallback( - (newDate: Date | null) => { - if (newDate) { - setSelectedDate(newDate); - onDateChange?.(newDate); - } - }, - [onDateChange] - ); + const handleDateChange = useCallback((newDate: Date | null) => { + if (newDate) { + setSelectedDate(newDate); + } + }, []); // 播放间隔改变处理 const handleIntervalChange = useCallback( @@ -175,25 +276,33 @@ const Timeline: React.FC = ({ setCurrentTime((prev) => { const next = prev >= 1435 ? 0 : prev + 5; setSliderValue(next); - onTimeChange?.(formatTime(next)); return next; }); }, newInterval); } }, - [isPlaying, onTimeChange] + [isPlaying] ); // 计算时间段改变处理 const handleCalculatedIntervalChange = useCallback((event: any) => { const newInterval = event.target.value; setCalculatedInterval(newInterval); }, []); - // 组件卸载时清理定时器 + + // 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据 + useEffect(() => { + fetchFrameData(currentTimeToDate(selectedDate, currentTime)); + }, [currentTime, selectedDate]); + + // 组件卸载时清理定时器和防抖 useEffect(() => { return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } }; }, []); @@ -224,7 +333,13 @@ const Timeline: React.FC = ({ handleDateChange(newValue)} + onChange={(newValue) => + handleDateChange( + newValue && "toDate" in newValue + ? newValue.toDate() + : (newValue as Date | null) + ) + } enableAccessibleFieldDOMStructure={false} format="yyyy-MM-dd" sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }} @@ -308,7 +423,7 @@ const Timeline: React.FC = ({ variant="outlined" size="small" startIcon={} - onClick={onRefresh} + // onClick={onRefresh} > 强制计算 diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx index 6c3ad30..2ca0247 100644 --- a/src/app/OlMap/MapComponent.tsx +++ b/src/app/OlMap/MapComponent.tsx @@ -24,7 +24,24 @@ import { bearing } from "@turf/turf"; import { Deck } from "@deck.gl/core"; import { TextLayer } from "@deck.gl/layers"; import { TripsLayer } from "@deck.gl/geo-layers"; -import { tr } from "date-fns/locale"; + +interface MapComponentProps { + children?: React.ReactNode; +} +interface DataContextType { + junctionData: any[]; + pipeData: any[]; + setJunctionDataState: React.Dispatch>; + setPipeDataState: React.Dispatch>; + showJunctionText?: boolean; // 是否显示节点文本 + showPipeText?: boolean; // 是否显示管道文本 + setShowJunctionText?: React.Dispatch>; + setShowPipeText?: React.Dispatch>; + junctionText: string; + pipeText: string; + setJunctionText?: React.Dispatch>; + setPipeText?: React.Dispatch>; +} // 创建自定义Layer类来包装deck.gl class DeckLayer extends Layer { @@ -50,8 +67,9 @@ class DeckLayer extends Layer { } // 跨组件传递 const MapContext = createContext(undefined); +const DataContext = createContext(undefined); + const extent = config.mapExtent; -const backendUrl = config.backendUrl; const mapUrl = config.mapUrl; // 添加防抖函数 @@ -69,16 +87,15 @@ function debounce any>(func: F, waitFor: number) { export const useMap = () => { return useContext(MapContext); }; +export const useData = () => { + return useContext(DataContext); +}; -const MapComponent: React.FC = () => { +const MapComponent: React.FC = ({ children }) => { const mapRef = useRef(null); const deckRef = useRef(null); const [map, setMap] = useState(); - const [currentTime, setCurrentTime] = useState( - new Date("2025-09-17T00:30:00+08:00") - ); - const intervalRef = useRef(null); const [junctionData, setJunctionDataState] = useState([]); const [pipeData, setPipeDataState] = useState([]); const junctionDataIds = useRef(new Set()); @@ -86,12 +103,11 @@ const MapComponent: React.FC = () => { const tileJunctionDataBuffer = useRef([]); const tilePipeDataBuffer = useRef([]); - let showJunctionText = true; // 控制节点文本显示 - let showPipeText = true; // 控制管道文本显示 - let junctionText = "pressure"; - let pipeText = "flow"; - let animate = false; // 控制是否动画 - const isAnimating = useRef(false); // 添加动画控制标志 + const [showJunctionText, setShowJunctionText] = useState(false); // 控制节点文本显示 + const [showPipeText, setShowPipeText] = useState(false); // 控制管道文本显示 + const [junctionText, setJunctionText] = useState(""); + const [pipeText, setPipeText] = useState(""); + const flowAnimation = useRef(true); // 添加动画控制标志 const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别 // 防抖更新函数 const debouncedUpdateData = useRef( @@ -134,80 +150,6 @@ const MapComponent: React.FC = () => { } }; - const setFrameData = async (queryTime: Date) => { - const query_time = queryTime.toISOString(); - console.log("Query Time:", query_time); - try { - // 定义需要查询的属性 - const junctionProperties = junctionText; - const pipeProperties = pipeText; - // 同时查询节点和管道数据 - const starttime = Date.now(); - const [nodeResponse, linkResponse] = await Promise.all([ - fetch( - `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` - ), - fetch( - `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` - ), - ]); - - const nodeRecords = await nodeResponse.json(); - const linkRecords = await linkResponse.json(); - // 将 nodeRecords 转换为 Map 以提高查找效率 - const nodeMap: Map = new Map( - nodeRecords.results.map((r: any) => [r.ID, r]) - ); - // 将 linkRecords 转换为 Map 以提高查找效率 - const linkMap: Map = new Map( - linkRecords.results.map((r: any) => [r.ID, r]) - ); - - // 更新junctionData - setJunctionDataState((prev) => - prev.map((j) => { - const record = nodeMap.get(j.id); - if (record) { - return { - ...j, - [junctionProperties]: record.value, - }; - } - return j; - }) - ); - - // 更新pipeData - setPipeDataState((prev) => - prev.map((p) => { - const record = linkMap.get(p.id); - if (record) { - return { - ...p, - flowFlag: pipeProperties === "flow" && record.value < 0 ? -1 : 1, - path: - pipeProperties === "flow" && record.value < 0 && p.flowFlag > 0 - ? [...p.path].reverse() - : p.path, - [pipeProperties]: record.value, - }; - } - return p; - }) - ); - // 属性为 flow 时启动动画 - if (pipeProperties === "flow" && animate) { - isAnimating.current = true; - } else { - isAnimating.current = false; - } - const endtime = Date.now(); - console.log("Data fetch and update time:", endtime - starttime, "ms"); - } catch (error) { - console.error("Error fetching data:", error); - } - }; - useEffect(() => { if (!mapRef.current) return; // 添加 MVT 瓦片加载逻辑 @@ -361,8 +303,12 @@ const MapComponent: React.FC = () => { value: "junctions", type: "point", properties: [ - { name: "需求量", value: "demand" }, - { name: "海拔高度", value: "elevation" }, + // { name: "需求量", value: "demand" }, + // { name: "海拔高度", value: "elevation" }, + { name: "实际需求量", value: "actualdemand" }, + { name: "水头", value: "head" }, + { name: "压力", value: "pressure" }, + { name: "水质", value: "quality" }, ], }, }); @@ -378,9 +324,17 @@ const MapComponent: React.FC = () => { value: "pipes", type: "linestring", properties: [ - { name: "直径", value: "diameter" }, - { name: "粗糙度", value: "roughness" }, - { name: "局部损失", value: "minor_loss" }, + // { name: "直径", value: "diameter" }, + // { name: "粗糙度", value: "roughness" }, + // { name: "局部损失", value: "minor_loss" }, + { name: "流量", value: "flow" }, + { name: "摩阻系数", value: "friction" }, + { name: "水头损失", value: "headloss" }, + { name: "水质", value: "quality" }, + { name: "反应速率", value: "reaction" }, + { name: "设置值", value: "setting" }, + { name: "状态", value: "status" }, + { name: "流速", value: "velocity" }, ], }, }); @@ -436,7 +390,7 @@ const MapComponent: React.FC = () => { const newLayers = [ new TextLayer({ id: "junctionTextLayer", - zIndex: 1000, + zIndex: 10, data: showJunctionText ? junctionData : [], getPosition: (d: any) => d.position, fontFamily: "Monaco, monospace", @@ -456,7 +410,7 @@ const MapComponent: React.FC = () => { }), new TextLayer({ id: "pipeTextLayer", - zIndex: 1000, + zIndex: 10, data: showPipeText ? pipeData : [], getPosition: (d: any) => d.position, fontFamily: "Monaco, monospace", @@ -479,7 +433,7 @@ const MapComponent: React.FC = () => { // 动画循环 const animate = () => { - if (!deck || !isAnimating.current) return; // 添加检查,防止空数据或停止旧循环 + if (!deck || !flowAnimation.current) return; // 添加检查,防止空数据或停止旧循环 // 动画总时长(秒) if (pipeData.length === 0) { requestAnimationFrame(animate); @@ -496,7 +450,7 @@ const MapComponent: React.FC = () => { const waterflowLayer = new TripsLayer({ id: "waterflowLayer", data: pipeData, - getPath: (d) => (isAnimating.current ? d.path : []), + getPath: (d) => (flowAnimation.current ? d.path : []), getTimestamps: (d) => { return d.timestamps; // 这些应该是与 currentTime 匹配的数值 }, @@ -522,35 +476,31 @@ const MapComponent: React.FC = () => { requestAnimationFrame(animate); }; animate(); - }, [isAnimating, junctionData, pipeData]); - - // 启动时间更新interval - useEffect(() => { - intervalRef.current = setInterval(() => { - setCurrentTime((prev) => new Date(prev.getTime() + 1800 * 1000)); - }, 10 * 1000); - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, []); - - // 当currentTime改变时,获取数据 - useEffect(() => { - const fetchData = async () => { - await setFrameData(currentTime); - }; - fetchData(); - }, [currentTime]); + }, [flowAnimation, junctionData, pipeData]); return ( <> - -
-
- -
- -
+ + +
+
+ + {children} +
+ +
+
); }; diff --git a/src/app/OlMap/MapTools.tsx b/src/app/OlMap/MapTools.tsx index 2842f27..7cbdac8 100644 --- a/src/app/OlMap/MapTools.tsx +++ b/src/app/OlMap/MapTools.tsx @@ -4,8 +4,9 @@ import BaseLayers from "./Controls/BaseLayers"; import MapToolbar from "./Controls/Toolbar"; import ScaleLine from "./Controls/ScaleLine"; import LayerControl from "./Controls/LayerControl"; +interface MapToolsProps {} -const MapTools = () => { +const MapTools: React.FC = () => { return ( <> diff --git a/src/components/olmap/SCADADataPanel.tsx b/src/components/olmap/SCADADataPanel.tsx index 094067a..81a96c0 100644 --- a/src/components/olmap/SCADADataPanel.tsx +++ b/src/components/olmap/SCADADataPanel.tsx @@ -294,6 +294,7 @@ const SCADADataPanel: React.FC = ({ justifyContent: "center", py: 6, color: "text.secondary", + height: 376, }} > @@ -307,7 +308,7 @@ const SCADADataPanel: React.FC = ({ const chartSection = hasData ? ( = ({ return ( {/* Header */} diff --git a/src/components/olmap/SCADADeviceList.tsx b/src/components/olmap/SCADADeviceList.tsx index ff6b31f..d98c253 100644 --- a/src/components/olmap/SCADADeviceList.tsx +++ b/src/components/olmap/SCADADeviceList.tsx @@ -178,7 +178,7 @@ const SCADADeviceList: React.FC = ({ }; return ( - + {/* 头部控制栏 */} = ({ {/* 搜索框 */} - + = ({ } + slotProps={{ + secondary: { + component: "div", // 使其支持多行 + }, + }} /> diff --git a/src/utils/breaks_classification.js b/src/utils/breaks_classification.js index 649281c..a81bb32 100644 --- a/src/utils/breaks_classification.js +++ b/src/utils/breaks_classification.js @@ -151,4 +151,27 @@ function jenks_with_stratified_sampling(data, n_classes, sample_size = 10000) { return jenks_breaks_jenkspy(sampled_data, n_classes); } -module.exports = { prettyBreaksClassification, jenks_breaks_jenkspy, jenks_with_stratified_sampling }; \ No newline at end of file +/** + * 根据指定的方法计算数据的分类断点。 + * @param {Array} data - 要分类的数值数据数组。 + * @param {number} segments - 要创建的段数或类别数。 + * @param {string} classificationMethod - 要使用的分类方法。支持的值:"pretty_breaks" 或 "jenks_optimized"。 + * @returns {Array} 分类的断点数组。如果数据为空或无效,则返回空数组。 + */ +function calculateClassification( + data, + segments, + classificationMethod +) { + if (!data || data.length === 0) { + return []; + } + if (classificationMethod === "pretty_breaks") { + return prettyBreaksClassification(data, segments); + } + if (classificationMethod === "jenks_optimized") { + return jenks_with_stratified_sampling(data, segments); + } +} + +module.exports = { prettyBreaksClassification, jenks_breaks_jenkspy, jenks_with_stratified_sampling, calculateClassification }; \ No newline at end of file diff --git a/src/utils/parseColor.js b/src/utils/parseColor.js new file mode 100644 index 0000000..89b0206 --- /dev/null +++ b/src/utils/parseColor.js @@ -0,0 +1,35 @@ +/** + * 将颜色字符串解析为包含红色、绿色、蓝色和 alpha 分量的对象。 + * 支持 rgba、rgb 和十六进制颜色格式。对于 rgba 和 rgb,提取 r、g、b 和 a(如果未提供,则默认为 1)。 + * 对于十六进制(例如 #RRGGBB),提取 r、g、b。(e.g., "rgba(255, 0, 0, 0.5)", "rgb(255, 0, 0)", or "#FF0000"). + * @param {string} color - 要解析的颜色字符串(例如 "rgba(255, 0, 0, 0.5)"、"rgb(255, 0, 0)" 或 "#FF0000")。 + * @returns {{r: number, g: number, b: number, a?: number}} 包含颜色分量的对象: + * - r: 红色分量 (0-255) + * - g: 绿色分量 (0-255) + * - b: 蓝色分量 (0-255) + * - a: Alpha 分量 (0-1),如果未指定则默认为 1 + **/ +function parseColor(color) { + // 解析 rgba 格式的颜色 + const match = color.match( + /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/ + ); + if (match) { + return { + r: parseInt(match[1], 10), + g: parseInt(match[2], 10), + b: parseInt(match[3], 10), + // 如果没有 alpha 值,默认为 1 + a: match[4] ? parseFloat(match[4]) : 1, + }; + } + // 如果还是十六进制格式,保持原来的解析方式 + const hex = color.replace("#", ""); + return { + r: parseInt(hex.slice(0, 2), 16), + g: parseInt(hex.slice(2, 4), 16), + b: parseInt(hex.slice(4, 6), 16), + }; +} + +module.exports = { parseColor }; \ No newline at end of file