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 (
<>
-
-
-
-
+
+
+
+
+
+
>
);
};
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