Files
TJWaterFrontend_Refine/src/app/OlMap/Controls/Toolbar.tsx

703 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback } from "react";
import { useData, useMap } from "../MapComponent";
import ToolbarButton from "@/components/olmap/common/ToolbarButton";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import PaletteOutlinedIcon from "@mui/icons-material/PaletteOutlined";
import QueryStatsOutlinedIcon from "@mui/icons-material/QueryStatsOutlined";
import PropertyPanel from "./PropertyPanel"; // 引入属性面板组件
import DrawPanel from "./DrawPanel"; // 引入绘图面板组件
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import { Style, Stroke, Fill, Circle } from "ol/style";
import { FeatureLike } from "ol/Feature";
import Feature from "ol/Feature";
import StyleEditorPanel from "./StyleEditorPanel";
import StyleLegend from "./StyleLegend"; // 引入图例组件
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import { config } from "@/config/config";
const backendUrl = config.BACKEND_URL;
import { LayerStyleState } from "./StyleEditorPanel";
// 添加接口定义隐藏按钮的props
interface ToolbarProps {
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
queryType?: string; // 可选的查询类型参数
}
const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
const map = useMap();
const data = useData();
if (!data) return null;
const { currentTime, selectedDate, schemeName } = data;
const [activeTools, setActiveTools] = useState<string[]>([]);
const [highlightFeature, setHighlightFeature] = useState<FeatureLike | null>(
null
);
const [showPropertyPanel, setShowPropertyPanel] = useState<boolean>(false);
const [showDrawPanel, setShowDrawPanel] = useState<boolean>(false);
const [showStyleEditor, setShowStyleEditor] = useState<boolean>(false);
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
{
isActive: false, // 默认不激活,不显示图例
layerId: "junctions",
layerName: "节点图层",
styleConfig: {
property: "pressure",
classificationMethod: "pretty_breaks",
segments: 6,
minSize: 4,
maxSize: 12,
minStrokeWidth: 2,
maxStrokeWidth: 8,
fixedStrokeWidth: 3,
colorType: "gradient",
singlePaletteIndex: 0,
gradientPaletteIndex: 0,
rainbowPaletteIndex: 0,
showLabels: false,
opacity: 0.9,
adjustWidthByProperty: true,
},
legendConfig: {
layerId: "junctions",
layerName: "节点图层",
property: "压力", // 暂时为空,等计算后更新
colors: [],
type: "point",
dimensions: [],
breaks: [],
},
},
{
isActive: false, // 默认不激活,不显示图例
layerId: "pipes",
layerName: "管道图层",
styleConfig: {
property: "flow",
classificationMethod: "pretty_breaks",
segments: 6,
minSize: 4,
maxSize: 12,
minStrokeWidth: 2,
maxStrokeWidth: 8,
fixedStrokeWidth: 3,
colorType: "gradient",
singlePaletteIndex: 0,
gradientPaletteIndex: 0,
rainbowPaletteIndex: 0,
showLabels: false,
opacity: 0.9,
adjustWidthByProperty: true,
},
legendConfig: {
layerId: "pipes",
layerName: "管道图层",
property: "流量", // 暂时为空,等计算后更新
colors: [],
type: "linestring",
dimensions: [],
breaks: [],
},
},
]);
// 计算激活的图例配置
const activeLegendConfigs = layerStyleStates
.filter((state) => state.isActive && state.legendConfig.property)
.map((state) => ({
...state.legendConfig,
layerName: state.layerName,
layerId: state.layerId,
}));
// 创建高亮图层
useEffect(() => {
if (!map) return;
const highLightSource = new VectorSource();
const highLightLayer = new VectorLayer({
source: highLightSource,
style: new Style({
stroke: new Stroke({
color: `rgba(255, 0, 0, 1)`,
width: 5,
}),
fill: new Fill({
color: `rgba(255, 0, 0, 0.2)`,
}),
image: new Circle({
radius: 7,
stroke: new Stroke({
color: `rgba(255, 0, 0, 1)`,
width: 3,
}),
fill: new Fill({
color: `rgba(255, 0, 0, 0.2)`,
}),
}),
}),
properties: {
name: "属性查询高亮图层", // 设置图层名称
value: "info_highlight_layer",
type: "multigeometry",
properties: [],
},
});
map.addLayer(highLightLayer);
setHighlightLayer(highLightLayer);
return () => {
map.removeLayer(highLightLayer);
};
}, [map]);
// 高亮要素的函数
useEffect(() => {
if (!highlightLayer) {
return;
}
const source = highlightLayer.getSource();
if (!source) {
return;
}
// 清除之前的高亮
source.clear();
// 添加新的高亮要素
if (highlightFeature instanceof Feature) {
source.addFeature(highlightFeature);
}
}, [highlightFeature]);
// 地图点击选择要素事件处理函数
const handleMapClickSelectFeatures = useCallback(
async (event: { coordinate: number[] }) => {
if (!map) return;
const feature = await mapClickSelectFeatures(event, map); // 调用导入的函数
setHighlightFeature(feature);
},
[map, setHighlightFeature]
);
// 添加矢量属性查询事件监听器
useEffect(() => {
if (!activeTools.includes("info") || !map) return;
map.on("click", handleMapClickSelectFeatures);
return () => {
map.un("click", handleMapClickSelectFeatures);
};
}, [activeTools, map, handleMapClickSelectFeatures]);
// 处理工具栏按钮点击事件
const handleToolClick = (tool: string) => {
// 样式工具的特殊处理 - 只有再次点击时才会取消激活和关闭
if (tool === "style") {
if (activeTools.includes("style")) {
// 如果样式工具已激活,点击时关闭
setShowStyleEditor(false);
setActiveTools((prev) => prev.filter((t) => t !== "style"));
} else {
// 激活样式工具,打开样式面板
setActiveTools((prev) => [...prev, "style"]);
setShowStyleEditor(true);
}
return;
}
// 其他工具的处理逻辑
if (activeTools.includes(tool)) {
// 如果当前工具已激活,再次点击时取消激活并关闭面板
deactivateTool(tool);
setActiveTools((prev) => prev.filter((t) => t !== tool));
} else {
// 如果当前工具未激活,先关闭所有其他工具,然后激活当前工具
// 关闭所有面板(但保持样式编辑器状态)
closeAllPanelsExceptStyle();
// 取消激活所有非样式工具
setActiveTools((prev) => {
const styleActive = prev.includes("style");
return styleActive ? ["style", tool] : [tool];
});
// 激活当前工具并打开对应面板
activateTool(tool);
}
};
// 取消激活指定工具并关闭对应面板
const deactivateTool = (tool: string) => {
switch (tool) {
case "info":
setShowPropertyPanel(false);
setHighlightFeature(null);
break;
case "draw":
setShowDrawPanel(false);
break;
case "history":
// 取消历史查询激活时的清理(目前不保留额外状态)
break;
}
};
// 激活指定工具并打开对应面板
const activateTool = (tool: string) => {
switch (tool) {
case "info":
setShowPropertyPanel(true);
break;
case "draw":
setShowDrawPanel(true);
break;
case "history":
// 激活历史查询后立即触发一次网络历史数据查询(结果暂时打印到控制台)
queryNetworkHistory();
break;
}
};
// 查询管网历史数据的函数(激活时调用)
const queryNetworkHistory = async () => {
try {
// 由当前选中日期和 currentTime 构造查询时间UTC ISO
let dateObj: Date;
if (selectedDate instanceof Date) {
dateObj = new Date(selectedDate);
} else {
dateObj = new Date(selectedDate as any);
}
const minutes = Number(currentTime) || 0;
dateObj.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
const querytime = dateObj.toISOString();
let url: string;
if (queryType === "scheme") {
url = `${backendUrl}/queryschemesimulationrecordsbytime/?scheme_name=${schemeName}&querytime=${querytime}`;
} else {
url = `${backendUrl}/querysimulationrecordsbytime/?querytime=${querytime}`;
}
const response = await fetch(url);
if (!response.ok) {
console.error("查询管网历史数据失败:", response.statusText);
return;
}
const result = await response.json();
// TODO: 根据需要把结果展示到面板或状态中,目前先打印
console.log("管网历史数据:", result);
// 简单提示用户已查询(可改为更友好的 UI
// eslint-disable-next-line no-alert
alert("已查询管网历史数据(请查看控制台或后续面板展示)。");
} catch (error) {
console.error("查询管网历史数据出错:", error);
}
};
// 关闭所有面板(除了样式编辑器)
const closeAllPanelsExceptStyle = () => {
setShowPropertyPanel(false);
setHighlightFeature(null);
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"
let response;
if (queryType === "scheme") {
response = await fetch(
`${backendUrl}/queryschemesimulationrecordsbyidtime/?scheme_name=${schemeName}&id=${id}&querytime=${querytime}&type=${type}`
);
} else {
response = await fetch(
`${backendUrl}/querysimulationrecordsbyidtime/?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({});
}
};
// 仅当 currentTime 有效时查询
if (currentTime !== -1 && queryType) queryComputedProperties();
}, [highlightFeature, currentTime, selectedDate]);
// 从要素属性中提取属性面板需要的数据
const getFeatureProperties = useCallback(() => {
if (!highlightFeature) return {};
const layer = highlightFeature?.getId()?.toString().split(".")[0];
const properties = highlightFeature.getProperties();
// 计算属性字段,增加 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: "m" },
{ key: "quality", label: "水质", unit: "mg/L" },
];
if (layer === "geo_pipes_mat" || layer === "geo_pipes") {
let result = {
id: properties.id,
type: "管道",
properties: [
{ label: "起始节点ID", value: properties.node1 },
{ label: "终点节点ID", value: properties.node2 },
{ label: "长度", value: properties.length?.toFixed?.(1), unit: "m" },
{
label: "管径",
value: properties.diameter?.toFixed?.(1),
unit: "mm",
},
{ label: "粗糙度", value: properties.roughness },
{ label: "局部损失", value: properties.minor_loss },
{ label: "初始状态", value: "开" },
],
};
// 追加计算属性
if (computedProperties) {
pipeComputedFields.forEach(({ key, label, unit }) => {
if (computedProperties[key] !== undefined) {
result.properties.push({
label,
value:
computedProperties[key].toFixed?.(3) || computedProperties[key],
unit,
});
}
});
}
return result;
}
if (layer === "geo_junctions_mat" || layer === "geo_junctions") {
let result = {
id: properties.id,
type: "节点",
properties: [
{
label: "海拔",
value: properties.elevation?.toFixed?.(1),
unit: "m",
},
// 将 demand1~demand5 与 pattern1~pattern5 作为二级表格展示
{
type: "table",
label: "基本需水量",
columns: ["demand", "pattern"],
rows: Array.from({ length: 5 }, (_, i) => i + 1)
.map((idx) => {
const d = properties?.[`demand${idx}`]?.toFixed?.(3);
const p = properties?.[`pattern${idx}`];
// 仅当 demand 有效时展示该行
if (d !== undefined && d !== null && d !== "") {
return [typeof d === "number" ? d.toFixed(3) : d, p ?? "-"];
}
})
.filter(Boolean) as (string | number)[][],
} as any,
],
};
// 追加计算属性
if (computedProperties) {
nodeComputedFields.forEach(({ key, label, unit }) => {
if (computedProperties[key] !== undefined) {
result.properties.push({
label,
value:
computedProperties[key].toFixed?.(3) || computedProperties[key],
unit,
});
}
});
}
return result;
}
if (layer === "geo_tanks_mat" || layer === "geo_tanks") {
return {
id: properties.id,
type: "水池",
properties: [
{
label: "海拔",
value: properties.elevation?.toFixed?.(1),
unit: "m",
},
{
label: "初始水位",
value: properties.init_level?.toFixed?.(1),
unit: "m",
},
{
label: "最低水位",
value: properties.min_level?.toFixed?.(1),
unit: "m",
},
{
label: "最高水位",
value: properties.max_level?.toFixed?.(1),
unit: "m",
},
{
label: "直径",
value: properties.diameter?.toFixed?.(1),
unit: "m",
},
{
label: "最小容积",
value: properties.min_vol?.toFixed?.(1),
unit: "m³",
},
// {
// label: "容积曲线",
// value: properties.vol_curve,
// },
{
label: "溢出",
value: properties.overflow ? "是" : "否",
},
],
};
}
if (layer === "geo_reservoirs_mat" || layer === "geo_reservoirs") {
return {
id: properties.id,
type: "水库",
properties: [
{
label: "水头",
value: properties.head?.toFixed?.(1),
unit: "m",
},
// {
// label: "模式",
// value: properties.pattern,
// },
],
};
}
if (layer === "geo_pumps_mat" || layer === "geo_pumps") {
return {
id: properties.id,
type: "水泵",
properties: [
{ label: "起始节点 ID", value: properties.node1 },
{ label: "终点节点 ID", value: properties.node2 },
{
label: "功率",
value: properties.power?.toFixed?.(1),
unit: "kW",
},
{
label: "扬程",
value: properties.head?.toFixed?.(1),
unit: "m",
},
{
label: "转速",
value: properties.speed?.toFixed?.(1),
unit: "rpm",
},
{
label: "模式",
value: properties.pattern,
},
],
};
}
if (layer === "geo_valves_mat" || layer === "geo_valves") {
return {
id: properties.id,
type: "阀门",
properties: [
{ label: "起始节点 ID", value: properties.node1 },
{ label: "终点节点 ID", value: properties.node2 },
{
label: "直径",
value: properties.diameter?.toFixed?.(1),
unit: "mm",
},
{
label: "阀门类型",
value: properties.v_type,
},
// {
// label: "设置",
// value: properties.setting?.toFixed?.(2),
// },
{
label: "局部损失",
value: properties.minor_loss?.toFixed?.(2),
},
],
};
}
// 传输频率文字对应
const getTransmissionFrequency = (transmission_frequency: string) => {
// 传输频率文本00:01:0000:05:0000:10:0000:30:0001:00:00转换为分钟数
const parts = transmission_frequency.split(":");
if (parts.length !== 3) return transmission_frequency;
const hours = parseInt(parts[0], 10);
const minutes = parseInt(parts[1], 10);
const seconds = parseInt(parts[2], 10);
const totalMinutes = hours * 60 + minutes + (seconds >= 30 ? 1 : 0);
return totalMinutes;
};
// 可靠度文字映射
const getReliability = (reliability: number) => {
switch (reliability) {
case 1:
return "高";
case 2:
return "中";
case 3:
return "低";
default:
return "未知";
}
};
if (layer === "geo_scada_mat" || layer === "geo_scada") {
let result = {
id: properties.id,
type: "SCADA设备",
properties: [
{
label: "类型",
value:
properties.type === "pipe_flow" ? "流量传感器" : "压力传感器",
},
{
label: "关联节点 ID",
value: properties.associated_element_id,
},
{
label: "传输模式",
value:
properties.transmission_mode === "non_realtime"
? "定时传输"
: "实时传输",
},
{
label: "传输频率",
value: getTransmissionFrequency(properties.transmission_frequency),
unit: "分钟",
},
{
label: "可靠性",
value: getReliability(properties.reliability),
},
],
};
return result;
}
return {};
}, [highlightFeature, computedProperties]);
return (
<>
<div className="absolute top-4 left-4 bg-white p-1 rounded-xl shadow-lg flex opacity-85 hover:opacity-100 transition-opacity">
{!hiddenButtons?.includes("info") && (
<ToolbarButton
icon={<InfoOutlinedIcon />}
name="查看属性"
isActive={activeTools.includes("info")}
onClick={() => handleToolClick("info")}
/>
)}
{!hiddenButtons?.includes("history") && (
<ToolbarButton
icon={<QueryStatsOutlinedIcon />}
name="查询历史数据"
isActive={activeTools.includes("history")}
onClick={() => handleToolClick("history")}
/>
)}
{!hiddenButtons?.includes("draw") && (
<ToolbarButton
icon={<EditOutlinedIcon />}
name="标记绘制"
isActive={activeTools.includes("draw")}
onClick={() => handleToolClick("draw")}
/>
)}
{!hiddenButtons?.includes("style") && (
<ToolbarButton
icon={<PaletteOutlinedIcon />}
name="图层样式"
isActive={activeTools.includes("style")}
onClick={() => handleToolClick("style")}
/>
)}
</div>
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
{showDrawPanel && map && <DrawPanel />}
{showStyleEditor && (
<StyleEditorPanel
layerStyleStates={layerStyleStates}
setLayerStyleStates={setLayerStyleStates}
/>
)}
{/* 图例显示 */}
{activeLegendConfigs.length > 0 && (
<div className="absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
<div className="flex flex-row gap-3">
{activeLegendConfigs.map((config, index) => (
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
))}
</div>
</div>
)}
</>
);
};
export default Toolbar;