属性查询面板添加计算值;完善时间轴获取数据的缓存机制;

This commit is contained in:
JIANG
2025-10-17 18:15:09 +08:00
parent 6ddce65445
commit 4e819b20ea
4 changed files with 251 additions and 77 deletions

View File

@@ -28,6 +28,7 @@ import { FlatStyleLike } from "ol/style/flat";
import { calculateClassification } from "@utils/breaks_classification"; import { calculateClassification } from "@utils/breaks_classification";
import { parseColor } from "@utils/parseColor"; import { parseColor } from "@utils/parseColor";
import { VectorTile } from "ol"; import { VectorTile } from "ol";
import { useNotification } from "@refinedev/core";
interface StyleConfig { interface StyleConfig {
property: string; property: string;
@@ -117,6 +118,8 @@ const StyleEditorPanel: React.FC = () => {
setPipeText, setPipeText,
} = data; } = data;
const { open, close } = useNotification();
const [applyJunctionStyle, setApplyJunctionStyle] = useState(false); const [applyJunctionStyle, setApplyJunctionStyle] = useState(false);
const [applyPipeStyle, setApplyPipeStyle] = useState(false); const [applyPipeStyle, setApplyPipeStyle] = useState(false);
const [styleUpdateTrigger, setStyleUpdateTrigger] = useState(0); // 用于触发样式更新的状态 const [styleUpdateTrigger, setStyleUpdateTrigger] = useState(0); // 用于触发样式更新的状态
@@ -194,7 +197,7 @@ const StyleEditorPanel: React.FC = () => {
const finalLegendConfig: LegendStyleConfig = legendConfig || { const finalLegendConfig: LegendStyleConfig = legendConfig || {
layerId, layerId,
layerName, layerName,
property: styleConfig.property, property: selectedProperty.name,
colors: [], colors: [],
type: "point", type: "point",
dimensions: [], dimensions: [],
@@ -240,15 +243,22 @@ const StyleEditorPanel: React.FC = () => {
setShowJunctionText(styleConfig.showLabels); setShowJunctionText(styleConfig.showLabels);
setApplyJunctionStyle(true); setApplyJunctionStyle(true);
saveLayerStyle(layerId); saveLayerStyle(layerId);
open?.({
type: "success",
message: "节点图层样式设置成功,等待数据更新。",
});
} }
} }
if (layerId === "pipes") { if (layerId === "pipes") {
console.log(styleConfig);
if (setPipeText && setShowPipeText) { if (setPipeText && setShowPipeText) {
setPipeText(property); setPipeText(property);
setShowPipeText(styleConfig.showLabels); setShowPipeText(styleConfig.showLabels);
setApplyPipeStyle(true); setApplyPipeStyle(true);
saveLayerStyle(layerId); saveLayerStyle(layerId);
open?.({
type: "success",
message: "管道图层样式设置成功,等待数据更新。",
});
} }
} }
// 触发样式更新 // 触发样式更新

View File

@@ -33,16 +33,25 @@ const Timeline: React.FC = () => {
return <div>Loading...</div>; // 或其他占位符 return <div>Loading...</div>; // 或其他占位符
} }
const { const {
currentTime,
setCurrentTime,
selectedDate,
setSelectedDate,
setCurrentJunctionCalData, setCurrentJunctionCalData,
setCurrentPipeCalData, setCurrentPipeCalData,
junctionText, junctionText,
pipeText, pipeText,
} = data; } = data;
if (
setCurrentTime === undefined ||
currentTime === undefined ||
selectedDate === undefined ||
setSelectedDate === undefined
) {
return <div>Loading...</div>; // 或其他占位符
}
const { open, close } = useNotification(); const { open, close } = useNotification();
const [currentTime, setCurrentTime] = useState<number>(0); // 分钟数 (0-1439)
const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
// const [selectedDate, setSelectedDate] = useState<Date>(new Date()); // 默认今天
const [isPlaying, setIsPlaying] = useState<boolean>(false); const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒 const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
const [calculatedInterval, setCalculatedInterval] = useState<number>(1440); // 分钟 const [calculatedInterval, setCalculatedInterval] = useState<number>(1440); // 分钟
@@ -51,62 +60,79 @@ const Timeline: React.FC = () => {
const intervalRef = useRef<NodeJS.Timeout | null>(null); const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timelineRef = useRef<HTMLDivElement>(null); const timelineRef = useRef<HTMLDivElement>(null);
// 添加缓存引用 // 添加缓存引用
const cacheRef = useRef< const nodeCacheRef = useRef<Map<string, any[]>>(new Map());
Map<string, { nodeRecords: any[]; linkRecords: any[] }> const linkCacheRef = useRef<Map<string, any[]>>(new Map());
>(new Map());
// 添加防抖引用 // 添加防抖引用
const debounceRef = useRef<NodeJS.Timeout | null>(null); const debounceRef = useRef<NodeJS.Timeout | null>(null);
const fetchFrameData = async (queryTime: Date) => { const fetchFrameData = async (
queryTime: Date,
junctionProperties: string,
pipeProperties: string
) => {
const query_time = queryTime.toISOString(); const query_time = queryTime.toISOString();
const cacheKey = query_time; let nodeRecords: any = { results: [] };
// console.log("Fetching data for time:", query_time); let linkRecords: any = { results: [] };
// console.log("Junction Property:", junctionText); const requests: Promise<Response>[] = [];
// console.log("Pipe Property:", pipeText); let nodePromise: Promise<any> | null = null;
// 检查缓存 let linkPromise: Promise<any> | null = null;
if (cacheRef.current.has(cacheKey)) { // 检查node缓存
const { nodeRecords, linkRecords } = cacheRef.current.get(cacheKey)!; if (junctionProperties !== "") {
// 使用缓存数据更新状态 const nodeCacheKey = `${query_time}_${junctionProperties}`;
updateDataStates(nodeRecords, linkRecords); if (nodeCacheRef.current.has(nodeCacheKey)) {
return; nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
} } else {
nodePromise = fetch(
try {
// 定义需要查询的属性
const junctionProperties = junctionText;
const pipeProperties = pipeText;
// 如果属性未定义或为空,直接返回
if (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}` `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}`
), );
fetch( requests.push(nodePromise);
`${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);
} }
// 检查link缓存
if (pipeProperties !== "") {
const linkCacheKey = `${query_time}_${pipeProperties}`;
if (linkCacheRef.current.has(linkCacheKey)) {
linkRecords = linkCacheRef.current.get(linkCacheKey)!;
} else {
linkPromise = fetch(
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}`
);
requests.push(linkPromise);
}
}
console.log(
"Query Time:",
queryTime.toLocaleDateString() + " " + queryTime.toLocaleTimeString()
);
// 等待所有有效请求
const responses = await Promise.all(requests);
if (nodePromise) {
const nodeResponse = responses.shift()!;
if (!nodeResponse.ok)
throw new Error(`Node fetch failed: ${nodeResponse.status}`);
nodeRecords = await nodeResponse.json();
// 缓存数据
nodeCacheRef.current.set(
`${query_time}_${junctionProperties}`,
nodeRecords || []
);
}
if (linkPromise) {
const linkResponse = responses.shift()!;
if (!linkResponse.ok)
throw new Error(`Link fetch failed: ${linkResponse.status}`);
linkRecords = await linkResponse.json();
// 缓存数据
linkCacheRef.current.set(
`${query_time}_${pipeProperties}`,
linkRecords || []
);
}
// 更新状态
updateDataStates(nodeRecords.results || [], linkRecords.results || []);
}; };
// 提取更新状态的逻辑 // 提取更新状态的逻辑
@@ -181,10 +207,10 @@ const Timeline: React.FC = () => {
// 播放控制 // 播放控制
const handlePlay = useCallback(() => { const handlePlay = useCallback(() => {
if (!isPlaying) { if (!isPlaying) {
if (junctionText === "" || pipeText === "") { if (junctionText === "" && pipeText === "") {
open?.({ open?.({
type: "error", type: "error",
message: "请先设置节点和管道的属性。", message: "请至少设定并应用一个图层的样式。",
}); });
return; return;
} }
@@ -270,8 +296,25 @@ const Timeline: React.FC = () => {
// 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据 // 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据
useEffect(() => { useEffect(() => {
fetchFrameData(currentTimeToDate(selectedDate, currentTime)); // 首次加载时,如果 selectedDatecurrentTime 未定义,则跳过执行,避免报错
}, [currentTime, selectedDate]); if (selectedDate && currentTime !== undefined) {
// 检查至少一个属性有值
const junctionProperties = junctionText;
const pipeProperties = pipeText;
if (junctionProperties === "" && pipeProperties === "") {
open?.({
type: "error",
message: "请至少设定并应用一个图层的样式。",
});
return;
}
fetchFrameData(
currentTimeToDate(selectedDate, currentTime),
junctionText,
pipeText
);
}
}, [junctionText, pipeText, currentTime, selectedDate]);
// 组件卸载时清理定时器和防抖 // 组件卸载时清理定时器和防抖
useEffect(() => { useEffect(() => {

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { useMap } from "../MapComponent"; import { useData, useMap } from "../MapComponent";
import ToolbarButton from "@/components/olmap/common/ToolbarButton"; import ToolbarButton from "@/components/olmap/common/ToolbarButton";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
@@ -23,8 +23,14 @@ import { toLonLat } from "ol/proj";
import { booleanIntersects, buffer, point, toWgs84 } from "@turf/turf"; import { booleanIntersects, buffer, point, toWgs84 } from "@turf/turf";
import RenderFeature from "ol/render/Feature"; import RenderFeature from "ol/render/Feature";
import { config } from "@/config/config";
const backendUrl = config.backendUrl;
const Toolbar: React.FC = () => { const Toolbar: React.FC = () => {
const map = useMap(); const map = useMap();
const data = useData();
if (!data) return null;
const { currentTime, selectedDate } = data;
const [activeTools, setActiveTools] = useState<string[]>([]); const [activeTools, setActiveTools] = useState<string[]>([]);
const [highlightFeature, setHighlightFeature] = useState<FeatureLike | null>( const [highlightFeature, setHighlightFeature] = useState<FeatureLike | null>(
null null
@@ -186,7 +192,7 @@ const Toolbar: React.FC = () => {
const features = results const features = results
.filter((json) => json !== null) // 过滤掉失败的请求 .filter((json) => json !== null) // 过滤掉失败的请求
.flatMap((json) => new GeoJSON().readFeatures(json)); .flatMap((json) => new GeoJSON().readFeatures(json));
console.log("查询到的要素:", features); // console.log("查询到的要素:", features);
return features; return features;
} else { } else {
// 查询指定图层 // 查询指定图层
@@ -201,7 +207,7 @@ const Toolbar: React.FC = () => {
} }
const json = await response.json(); const json = await response.json();
const features = new GeoJSON().readFeatures(json); const features = new GeoJSON().readFeatures(json);
console.log("查询到的要素:", features); // console.log("查询到的要素:", features);
return features; return features;
} }
} catch (error) { } catch (error) {
@@ -399,45 +405,145 @@ const Toolbar: React.FC = () => {
setShowDrawPanel(false); setShowDrawPanel(false);
// 样式编辑器保持其当前状态,不自动关闭 // 样式编辑器保持其当前状态,不自动关闭
}; };
const [computedProperties, setComputedProperties] = useState<
Record<string, any>
>({});
// 添加 useEffect 来查询计算属性
useEffect(() => {
if (!highlightFeature || !selectedDate) {
setComputedProperties({});
return;
}
const id = highlightFeature.getProperties().id;
if (!id) {
setComputedProperties({});
return;
}
const queryComputedProperties = async () => {
try {
const properties = highlightFeature?.getProperties?.() || {};
const type =
properties.geometry?.getType?.() === "LineString" ? "link" : "node";
// selectedDate 格式化为 YYYY-MM-DD
let dateObj: Date;
if (selectedDate instanceof Date) {
dateObj = new Date(selectedDate);
} else {
dateObj = new Date(selectedDate);
}
const minutes = Number(currentTime) || 0;
dateObj.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
// 转为 UTC ISO 字符串
const querytime = dateObj.toISOString(); // 例如 "2025-09-16T16:30:00.000Z"
const response = await fetch(
`${backendUrl}/queryrecordsbyidtime/?id=${id}&querytime=${querytime}&type=${type}`
);
if (!response.ok) {
throw new Error("API request failed");
}
const data = await response.json();
setComputedProperties(data.results[0] || {});
} catch (error) {
console.error("Error querying computed properties:", error);
setComputedProperties({});
}
};
queryComputedProperties();
}, [highlightFeature, currentTime, selectedDate]);
// 从要素属性中提取属性面板需要的数据 // 从要素属性中提取属性面板需要的数据
const getFeatureProperties = useCallback(() => { const getFeatureProperties = useCallback(() => {
if (!highlightFeature) return {}; if (!highlightFeature) return {};
const properties = highlightFeature.getProperties(); const properties = highlightFeature.getProperties();
console.log(properties, properties.geometry.type, "properties"); // 计算属性字段,增加 key 字段
const pipeComputedFields = [
{ key: "flow", label: "流量", unit: "m³/s" },
{ key: "friction", label: "摩阻", unit: "" },
{ key: "headloss", label: "水头损失", unit: "m" },
{ key: "quality", label: "水质", unit: "mg/L" },
{ key: "reaction", label: "反应", unit: "1/s" },
{ key: "setting", label: "设置", unit: "" },
{ key: "status", label: "状态", unit: "" },
{ key: "velocity", label: "流速", unit: "m/s" },
];
const nodeComputedFields = [
{ key: "actualdemand", label: "实际需水量", unit: "m³/s" },
{ key: "head", label: "水头", unit: "m" },
{ key: "pressure", label: "压力", unit: "kPa" },
{ key: "quality", label: "水质", unit: "mg/L" },
];
if (properties.geometry.getType() === "LineString") { if (properties.geometry.getType() === "LineString") {
console.log(properties, "properties"); let result = {
return {
id: properties.id, id: properties.id,
type: "管道", type: "管道",
properties: [ properties: [
{ label: "起始节点ID", value: properties.node1 }, { label: "起始节点ID", value: properties.node1 },
{ label: "终点节点ID", value: properties.node2 }, { label: "终点节点ID", value: properties.node2 },
{ label: "长度", value: properties.length.toFixed(1), unit: "m" }, { label: "长度", value: properties.length?.toFixed?.(1), unit: "m" },
{ label: "管径", value: properties.diameter.toFixed(1), unit: "mm" }, {
label: "管径",
value: properties.diameter?.toFixed?.(1),
unit: "mm",
},
{ label: "粗糙度", value: properties.roughness }, { label: "粗糙度", value: properties.roughness },
{ label: "局部损失", value: properties.minor_loss }, { label: "局部损失", value: properties.minor_loss },
{ label: "初始状态", value: "开" }, { label: "初始状态", value: "开" },
], ],
}; };
// 追加计算属性
if (computedProperties) {
pipeComputedFields.forEach(({ key, label, unit }) => {
if (computedProperties[key] !== undefined) {
result.properties.push({
label,
value:
computedProperties[key].toFixed?.(2) || computedProperties[key],
unit,
});
}
});
}
return result;
} }
if (properties.geometry.getType() === "Point") { if (properties.geometry.getType() === "Point") {
return { let result = {
id: properties.id, id: properties.id,
type: "节点", type: "节点",
properties: [ properties: [
{ label: "海拔", value: properties.elevation.toFixed(1), unit: "m" }, {
label: "海拔",
value: properties.elevation?.toFixed?.(1),
unit: "m",
},
{ {
label: "需求量", label: "需求量",
value: properties.demand.toFixed(1), value: properties.demand?.toFixed?.(1),
unit: "m³/s", unit: "m³/s",
}, },
], ],
}; };
// 追加计算属性
if (computedProperties) {
nodeComputedFields.forEach(({ key, label, unit }) => {
if (computedProperties[key] !== undefined) {
result.properties.push({
label,
value:
computedProperties[key].toFixed?.(2) || computedProperties[key],
unit,
});
}
});
}
return result;
} }
return {}; return {};
}, [highlightFeature]); }, [highlightFeature, computedProperties]);
return ( return (
<> <>

View File

@@ -29,6 +29,10 @@ interface MapComponentProps {
children?: React.ReactNode; children?: React.ReactNode;
} }
interface DataContextType { interface DataContextType {
currentTime?: number; // 当前时间
setCurrentTime?: React.Dispatch<React.SetStateAction<number>>;
selectedDate?: Date; // 选择的日期
setSelectedDate?: React.Dispatch<React.SetStateAction<Date>>;
currentJunctionCalData?: any[]; // 当前计算结果 currentJunctionCalData?: any[]; // 当前计算结果
setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>; setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
currentPipeCalData?: any[]; // 当前计算结果 currentPipeCalData?: any[]; // 当前计算结果
@@ -97,6 +101,10 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const [map, setMap] = useState<OlMap>(); const [map, setMap] = useState<OlMap>();
// currentCalData 用于存储当前计算结果 // currentCalData 用于存储当前计算结果
const [currentTime, setCurrentTime] = useState<number>(0);
const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
// const [selectedDate, setSelectedDate] = useState<Date>(new Date()); // 默认今天
const [currentJunctionCalData, setCurrentJunctionCalData] = useState<any[]>( const [currentJunctionCalData, setCurrentJunctionCalData] = useState<any[]>(
[] []
); );
@@ -113,8 +121,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const [showPipeText, setShowPipeText] = useState(false); // 控制管道文本显示 const [showPipeText, setShowPipeText] = useState(false); // 控制管道文本显示
const [showJunctionTextLayer, setShowJunctionTextLayer] = useState(true); // 控制节点文本图层显示 const [showJunctionTextLayer, setShowJunctionTextLayer] = useState(true); // 控制节点文本图层显示
const [showPipeTextLayer, setShowPipeTextLayer] = useState(true); // 控制管道文本图层显示 const [showPipeTextLayer, setShowPipeTextLayer] = useState(true); // 控制管道文本图层显示
const [junctionText, setJunctionText] = useState("pressure"); const [junctionText, setJunctionText] = useState("");
const [pipeText, setPipeText] = useState("flow"); const [pipeText, setPipeText] = useState("");
const flowAnimation = useRef(false); // 添加动画控制标志 const flowAnimation = useRef(false); // 添加动画控制标志
const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别 const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别
// 防抖更新函数 // 防抖更新函数
@@ -418,7 +426,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
fontFamily: "Monaco, monospace", fontFamily: "Monaco, monospace",
getText: (d: any) => getText: (d: any) =>
d[junctionText] ? (d[junctionText] as number).toFixed(3) : "", d[junctionText] ? (d[junctionText] as number).toFixed(3) : "",
getSize: 12, getSize: 14,
fontWeight: "bold",
getColor: [150, 150, 255], getColor: [150, 150, 255],
getAngle: 0, getAngle: 0,
getTextAnchor: "middle", getTextAnchor: "middle",
@@ -448,7 +457,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
fontFamily: "Monaco, monospace", fontFamily: "Monaco, monospace",
getText: (d: any) => getText: (d: any) =>
d[pipeText] ? (d[pipeText] as number).toFixed(3) : "", d[pipeText] ? (d[pipeText] as number).toFixed(3) : "",
getSize: 14, getSize: 12,
fontWeight: "bold",
getColor: [120, 128, 181], getColor: [120, 128, 181],
getAngle: (d: any) => d.angle || 0, getAngle: (d: any) => d.angle || 0,
getPixelOffset: [0, -8], getPixelOffset: [0, -8],
@@ -491,13 +501,14 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const waterflowLayer = new TripsLayer({ const waterflowLayer = new TripsLayer({
id: "waterflowLayer", id: "waterflowLayer",
data: pipeData, data: pipeData,
getPath: (d) => (flowAnimation.current ? d.path : []), getPath: (d) => d.path,
getTimestamps: (d) => { getTimestamps: (d) => {
return d.timestamps; // 这些应该是与 currentTime 匹配的数值 return d.timestamps; // 这些应该是与 currentTime 匹配的数值
}, },
getColor: [0, 220, 255], getColor: [0, 220, 255],
opacity: 0.8, opacity: 0.8,
visible: currentZoom >= 12 && currentZoom <= 24, visible:
flowAnimation.current && currentZoom >= 12 && currentZoom <= 24,
widthMinPixels: 5, widthMinPixels: 5,
jointRounded: true, // 拐角变圆 jointRounded: true, // 拐角变圆
// capRounded: true, // 端点变圆 // capRounded: true, // 端点变圆
@@ -589,6 +600,10 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
<> <>
<DataContext.Provider <DataContext.Provider
value={{ value={{
currentTime,
setCurrentTime,
selectedDate,
setSelectedDate,
currentJunctionCalData, currentJunctionCalData,
setCurrentJunctionCalData, setCurrentJunctionCalData,
currentPipeCalData, currentPipeCalData,