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 HistoryDataPanel from "./HistoryDataPanel"; // 引入绘图面板组件 import VectorSource from "ol/source/Vector"; import VectorLayer from "ol/layer/Vector"; import { Style, Stroke, Fill, Circle } from "ol/style"; import Feature from "ol/Feature"; import StyleEditorPanel from "./StyleEditorPanel"; import { LayerStyleState } from "./StyleEditorPanel"; import StyleLegend from "./StyleLegend"; // 引入图例组件 import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; import { useNotification } from "@refinedev/core"; import { config } from "@/config/config"; const backendUrl = config.BACKEND_URL; // 添加接口定义隐藏按钮的props interface ToolbarProps { hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style'] queryType?: string; // 可选的查询类型参数 HistoryPanel?: React.FC; // 可选的自定义历史数据面板 } const Toolbar: React.FC = ({ hiddenButtons, queryType, HistoryPanel, }) => { const map = useMap(); const data = useData(); const { open } = useNotification(); if (!data) return null; const { currentTime, selectedDate, schemeName } = data; const [activeTools, setActiveTools] = useState([]); const [highlightFeatures, setHighlightFeatures] = useState([]); const [showPropertyPanel, setShowPropertyPanel] = useState(false); const [showDrawPanel, setShowDrawPanel] = useState(false); const [showStyleEditor, setShowStyleEditor] = useState(false); const [showHistoryPanel, setShowHistoryPanel] = useState(false); const [highlightLayer, setHighlightLayer] = useState | null>(null); // 样式状态管理 - 在 Toolbar 中管理,带有默认样式 const [layerStyleStates, setLayerStyleStates] = useState([ { isActive: false, // 默认不激活,不显示图例 layerId: "junctions", layerName: "节点", styleConfig: { property: "pressure", classificationMethod: "custom_breaks", customBreaks: [16, 18, 20, 22, 24, 26], customColors: [ "rgba(255, 0, 0, 1)", "rgba(255, 127, 0, 1)", "rgba(255, 215, 0, 1)", "rgba(199, 224, 0, 1)", "rgba(76, 175, 80, 1)", "rgba(0, 158, 115, 1)", ], segments: 6, minSize: 4, maxSize: 12, minStrokeWidth: 2, maxStrokeWidth: 8, fixedStrokeWidth: 3, colorType: "rainbow", singlePaletteIndex: 0, gradientPaletteIndex: 0, rainbowPaletteIndex: 0, showLabels: false, showId: 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, showId: 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(); // 添加新的高亮要素 highlightFeatures.forEach((feature) => { if (feature instanceof Feature) { source.addFeature(feature); } }); }, [highlightFeatures, highlightLayer]); // 地图点击选择要素事件处理函数 const handleMapClickSelectFeatures = useCallback( async (event: { coordinate: number[] }) => { if (!map) return; const feature = await mapClickSelectFeatures(event, map); // 调用导入的函数 if (!feature || !(feature instanceof Feature)) { // 如果没有点击到要素,且当前是 info 模式,则清除高亮 if (activeTools.includes("info")) { setHighlightFeatures([]); } return; } if (activeTools.includes("history")) { // 历史查询模式:支持同类型多选 const featureId = feature.getProperties().id; const layerId = feature.getId()?.toString().split(".")[0] || ""; console.log("点击选择要素", feature, "图层:", layerId); // 简单的类型检查函数 const getBaseType = (lid: string) => { if (lid.includes("pipe")) return "pipe"; if (lid.includes("junction")) return "junction"; if (lid.includes("tank")) return "tank"; if (lid.includes("reservoir")) return "reservoir"; if (lid.includes("pump")) return "pump"; if (lid.includes("valve")) return "valve"; return lid; }; // 检查是否与已选要素类型一致 if (highlightFeatures.length > 0) { const firstLayerId = highlightFeatures[0].getId()?.toString().split(".")[0] || ""; if (getBaseType(layerId) !== getBaseType(firstLayerId)) { // 如果点击的是已选中的要素(为了取消选中),则不报错 const isAlreadySelected = highlightFeatures.some( (f) => f.getProperties().id === featureId ); if (!isAlreadySelected) { open?.({ type: "error", message: "请选择相同类型的要素进行多选查询。", }); return; } } } setHighlightFeatures((prev) => { const existingIndex = prev.findIndex( (f) => f.getProperties().id === featureId ); if (existingIndex !== -1) { // 如果已存在,移除 return prev.filter((_, i) => i !== existingIndex); } else { // 如果不存在,添加 return [...prev, feature]; } }); } else { // 其他模式(如 info):单选 setHighlightFeatures([feature]); } }, [map, activeTools, highlightFeatures, open] ); // 添加矢量属性查询事件监听器 useEffect(() => { if (!map) return; // 监听 info 或 history 工具激活时添加 if (activeTools.includes("info") || activeTools.includes("history")) { 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); setHighlightFeatures([]); break; case "draw": setShowDrawPanel(false); break; case "history": setShowHistoryPanel(false); setHighlightFeatures([]); break; } }; // 激活指定工具并打开对应面板 const activateTool = (tool: string) => { switch (tool) { case "info": setShowPropertyPanel(true); break; case "draw": setShowDrawPanel(true); break; case "history": setShowHistoryPanel(true); // 激活历史查询后:HistoryDataPanel 自行负责根据传入的 props 拉取数据。 break; } }; // 关闭所有面板(除了样式编辑器) const closeAllPanelsExceptStyle = () => { setShowPropertyPanel(false); setHighlightFeatures([]); setShowDrawPanel(false); setShowHistoryPanel(false); // 样式编辑器保持其当前状态,不自动关闭 }; const [computedProperties, setComputedProperties] = useState< Record >({}); // 添加 useEffect 来查询计算属性 useEffect(() => { if (highlightFeatures.length === 0 || !selectedDate || !showPropertyPanel) { setComputedProperties({}); return; } const highlightFeature = highlightFeatures[0]; 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}` `${backendUrl}/timescaledb/scheme/query/by-id-time?scheme_name=${schemeName}&id=${id}&type=${type}&query_time=${querytime}` ); } else { response = await fetch( // `${backendUrl}/querysimulationrecordsbyidtime/?id=${id}&querytime=${querytime}&type=${type}` `${backendUrl}/timescaledb/realtime/query/by-id-time?id=${id}&type=${type}&query_time=${querytime}` ); } 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(); }, [highlightFeatures, currentTime, selectedDate]); // 从要素属性中提取属性面板需要的数据 const getFeatureProperties = useCallback(() => { if (highlightFeatures.length === 0) return {}; const highlightFeature = highlightFeatures[0]; const layer = highlightFeature?.getId()?.toString().split(".")[0]; const properties = highlightFeature.getProperties(); // 计算属性字段,增加 key 字段 const pipeComputedFields = [ { key: "flow", label: "流量", unit: "m³/h" }, { key: "friction", label: "摩阻", unit: "" }, { key: "headloss", label: "水头损失", unit: "m" }, { key: "unit_headloss", label: "单位水头损失", unit: "m/km" }, { key: "quality", label: "水质", unit: "mg/L" }, { key: "reaction", label: "反应", unit: "1/d" }, { key: "setting", label: "设置", unit: "" }, { key: "status", label: "状态", unit: "" }, { key: "velocity", label: "流速", unit: "m/s" }, ]; const nodeComputedFields = [ { key: "actual_demand", label: "实际需水量", unit: "m³/h" }, { key: "total_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 }) => { let value = computedProperties[key]; // 如果是单位水头损失且后端未返回,则通过水头损失/长度计算 (单位 m/km) if ( key === "unit_headloss" && value === undefined && computedProperties.headloss !== undefined && properties.length ) { value = (computedProperties.headloss / properties.length) * 1000; } if (value !== undefined) { result.properties.push({ label, value: typeof value === "number" ? value.toFixed(3) : value, 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:00,00:05:00,00:10:00,00:30:00,01: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 {}; }, [highlightFeatures, computedProperties]); return ( <>
{!hiddenButtons?.includes("info") && ( } name="查看属性" isActive={activeTools.includes("info")} onClick={() => handleToolClick("info")} /> )} {!hiddenButtons?.includes("history") && ( } name="查询历史数据" isActive={activeTools.includes("history")} onClick={() => handleToolClick("history")} /> )} {!hiddenButtons?.includes("draw") && ( } name="标记绘制" isActive={activeTools.includes("draw")} onClick={() => handleToolClick("draw")} /> )} {!hiddenButtons?.includes("style") && ( } name="图层样式" isActive={activeTools.includes("style")} onClick={() => handleToolClick("style")} /> )}
{showPropertyPanel && } {showDrawPanel && map && }
{showHistoryPanel && (HistoryPanel ? ( { if (highlightFeatures.length === 0 || !showHistoryPanel) return []; return highlightFeatures .map((feature) => { const properties = feature.getProperties(); const id = properties.id; if (!id) return null; // 从图层名称推断类型 const layerId = feature.getId()?.toString().split(".")[0] || ""; let type = "unknown"; if (layerId.includes("pipe")) { type = "pipe"; } else if (layerId.includes("junction")) { type = "junction"; } else if (layerId.includes("tank")) { type = "tank"; } else if (layerId.includes("reservoir")) { type = "reservoir"; } else if (layerId.includes("pump")) { type = "pump"; } else if (layerId.includes("valve")) { type = "valve"; } // 仅处理 type 为 pipe 或 junction 的情况 if (type !== "pipe" && type !== "junction") { return null; } return [id, type]; }) .filter(Boolean) as [string, string][]; })()} scheme_type="burst_Analysis" scheme_name={schemeName} type={queryType as "realtime" | "scheme" | "none"} /> ) : ( { if (highlightFeatures.length === 0 || !showHistoryPanel) return []; return highlightFeatures .map((feature) => { const properties = feature.getProperties(); const id = properties.id; if (!id) return null; // 从图层名称推断类型 const layerId = feature.getId()?.toString().split(".")[0] || ""; let type = "unknown"; if (layerId.includes("pipe")) { type = "pipe"; } else if (layerId.includes("junction")) { type = "junction"; } else if (layerId.includes("tank")) { type = "tank"; } else if (layerId.includes("reservoir")) { type = "reservoir"; } else if (layerId.includes("pump")) { type = "pump"; } else if (layerId.includes("valve")) { type = "valve"; } // 仅处理 type 为 pipe 或 junction 的情况 if (type !== "pipe" && type !== "junction") { return null; } return [id, type]; }) .filter(Boolean) as [string, string][]; })()} scheme_type="burst_Analysis" scheme_name={schemeName} type={queryType as "realtime" | "scheme" | "none"} /> ))} {/* 图例显示 */} {activeLegendConfigs.length > 0 && (
{activeLegendConfigs.map((config, index) => ( ))}
)} ); }; export default Toolbar;