diff --git a/src/components/olmap/core/Controls/Toolbar.tsx b/src/components/olmap/core/Controls/Toolbar.tsx index e2a0747..03f38f7 100644 --- a/src/components/olmap/core/Controls/Toolbar.tsx +++ b/src/components/olmap/core/Controls/Toolbar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { useData, useMap } from "../MapComponent"; import ToolbarButton from "@/components/olmap/common/ToolbarButton"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; @@ -8,27 +8,24 @@ import QueryStatsOutlinedIcon from "@mui/icons-material/QueryStatsOutlined"; import CompareArrowsOutlinedIcon from "@mui/icons-material/CompareArrowsOutlined"; import PropertyPanel from "./PropertyPanel"; // 引入属性面板组件 import DrawPanel from "./DrawPanel"; // 引入绘图面板组件 -import HistoryDataPanel from "./HistoryDataPanel"; // 引入绘图面板组件 -import SCADADataPanel from "@components/olmap/SCADA/SCADADataPanel"; 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 { GeoJSON } from "ol/format"; -import Point from "ol/geom/Point"; -import { bbox, featureCollection } from "@turf/turf"; import StyleEditorPanel from "./StyleEditorPanel"; import { LayerStyleState } from "./StyleEditorPanel"; import StyleLegend from "./StyleLegend"; // 引入图例组件 -import { handleMapClickSelectFeatures as mapClickSelectFeatures, queryFeaturesByIds } from "@/utils/mapQueryService"; +import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; import { useNotification } from "@refinedev/core"; -import { useChatToolActionHandler } from "@/hooks/useChatToolActionHandler"; -import { applyJunctionAreaRender } from "@components/olmap/DMALeakDetection/applyJunctionAreaRender"; +import ToolbarHistoryPanel from "./ToolbarHistoryPanel"; +import { + buildFeatureProperties, +} from "./toolbarFeatureHelpers"; +import { useToolbarChatActions } from "./useToolbarChatActions"; import { config } from "@/config/config"; import { apiFetch } from "@/lib/apiFetch"; -import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units"; // 添加接口定义隐藏按钮的props interface ToolbarProps { @@ -82,119 +79,15 @@ const Toolbar: React.FC = ({ startTime?: string; endTime?: string; } | null>(null); - const chatJunctionRenderCleanupRef = useRef<(() => void) | null>(null); - const disposeChatJunctionRender = useCallback(() => { - chatJunctionRenderCleanupRef.current?.(); - chatJunctionRenderCleanupRef.current = null; - }, []); - - useEffect(() => () => disposeChatJunctionRender(), [disposeChatJunctionRender]); - - // Wire up chat tool actions (locate, view_history, view_scada, render_junctions) - useChatToolActionHandler( - useCallback( - (action) => { - const geojsonFormat = new GeoJSON(); - const zoomToFeatures = ( - features: Feature[], - geometryKind: "point" | "line", - ) => { - if (features.length === 0) return; - - if (geometryKind === "point" && features.length === 1) { - const geometry = features[0].getGeometry(); - if (geometry instanceof Point) { - map?.getView().animate({ - center: geometry.getCoordinates(), - zoom: 18, - duration: 1000, - }); - return; - } - } - - const geojsonFeatures = features.map((f) => - geojsonFormat.writeFeatureObject(f), - ); - const extent = bbox(featureCollection(geojsonFeatures as any)); - if (extent) { - map?.getView().fit(extent, { - maxZoom: 18, - duration: 1000, - padding: geometryKind === "line" ? [60, 60, 60, 60] : [40, 40, 40, 40], - }); - } - }; - const locateFeatures = ( - ids: string[], - layer: string, - geometryKind: "point" | "line", - ) => { - queryFeaturesByIds(ids, layer).then((features) => { - if (features.length > 0) { - setHighlightFeatures(features); - zoomToFeatures(features, geometryKind); - } - }); - }; - - switch (action.type) { - case "locate_features": { - locateFeatures(action.ids, action.layer, action.geometryKind); - break; - } - case "view_history": { - setChatPanelFeatureInfos(action.featureInfos); - setChatPanelType(action.dataType); - setChatPanelTimeRange({ - startTime: action.startTime, - endTime: action.endTime, - }); - setShowHistoryPanel(true); - break; - } - case "view_scada": { - setChatPanelFeatureInfos(action.featureInfos); - setChatPanelType("none"); - setChatPanelTimeRange({ - startTime: action.startTime, - endTime: action.endTime, - }); - setShowHistoryPanel(true); - setActiveTools((prev) => { - if (prev.includes("history")) { - return prev; - } - return [...prev, "history"]; - }); - break; - } - case "render_junctions": { - disposeChatJunctionRender(); - - if (Object.keys(action.nodeAreaMap).length === 0) { - break; - } - - if (map) { - chatJunctionRenderCleanupRef.current = applyJunctionAreaRender( - map, - { - nodeAreaMap: action.nodeAreaMap, - areaIds: action.areaIds, - areaColors: action.areaColors, - }, - { propertyKey: "chat_junction_render_index" }, - ); - } - break; - } - } - }, - [disposeChatJunctionRender, map], - ), - ); + useToolbarChatActions({ + setHighlightFeatures, + setChatPanelFeatureInfos, + setChatPanelType, + setChatPanelTimeRange, + setShowHistoryPanel, + setActiveTools, + }); // 样式状态管理 - 在 Toolbar 中管理,带有默认样式 const [layerStyleStates, setLayerStyleStates] = useState([ @@ -556,306 +449,10 @@ const Toolbar: React.FC = ({ if (currentTime !== -1 && queryType) queryComputedProperties(); }, [highlightFeatures, currentTime, selectedDate, queryType, schemeName, schemeType, showPropertyPanel]); - // 从要素属性中提取属性面板需要的数据 - 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: `${FLOW_DISPLAY_UNIT}` }, - { 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: `${FLOW_DISPLAY_UNIT}` }, - { 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]; - - if (key === "flow" && value !== undefined) { - value = toM3h(value, "lps"); - } - - // 如果是单位水头损失且后端未返回,则通过水头损失/长度计算 (单位 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) => { - let d = properties?.[`demand${idx}`]; - const p = properties?.[`pattern${idx}`]; - // 仅当 demand 有效时展示该行 - if (d !== undefined && d !== null && d !== "") { - d = toM3h(Number(d), "lps"); - 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) { - let value = computedProperties[key]; - if (key === "actual_demand") { - value = toM3h(value, "lps"); - } - result.properties.push({ - label, - value: - value?.toFixed?.(3) || value, - 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]); + const propertyPanelData = useMemo( + () => buildFeatureProperties(highlightFeatures[0], computedProperties), + [highlightFeatures, computedProperties], + ); if (!data) { return null; @@ -908,7 +505,7 @@ const Toolbar: React.FC = ({ {showPropertyPanel && ( { deactivateTool("info"); setActiveTools((prev) => prev.filter((t) => t !== "info")); @@ -922,115 +519,20 @@ const Toolbar: React.FC = ({ setLayerStyleStates={setLayerStyleStates} /> - {showHistoryPanel && - (chatPanelType === "none" && chatPanelFeatureInfos ? ( - id)} - visible={showHistoryPanel} - start_time={chatPanelTimeRange?.startTime} - end_time={chatPanelTimeRange?.endTime} - onClose={() => { - deactivateTool("history"); - setActiveTools((prev) => prev.filter((t) => t !== "history")); - }} - /> - ) : 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={chatPanelFeatureInfos ? chatPanelType : (queryType as "realtime" | "scheme" | "none")} - start_time={chatPanelTimeRange?.startTime} - end_time={chatPanelTimeRange?.endTime} - onClose={() => { - deactivateTool("history"); - setActiveTools((prev) => prev.filter((t) => t !== "history")); - }} - /> - ) : ( - { - 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={chatPanelFeatureInfos ? chatPanelType : (queryType as "realtime" | "scheme" | "none")} - start_time={chatPanelTimeRange?.startTime} - end_time={chatPanelTimeRange?.endTime} - onClose={() => { - deactivateTool("history"); - setActiveTools((prev) => prev.filter((t) => t !== "history")); - }} - /> - ))} + { + deactivateTool("history"); + setActiveTools((prev) => prev.filter((t) => t !== "history")); + }} + /> {/* 图例显示 */} {activeLegendConfigs.length > 0 && ( diff --git a/src/components/olmap/core/Controls/ToolbarHistoryPanel.tsx b/src/components/olmap/core/Controls/ToolbarHistoryPanel.tsx new file mode 100644 index 0000000..552aad4 --- /dev/null +++ b/src/components/olmap/core/Controls/ToolbarHistoryPanel.tsx @@ -0,0 +1,92 @@ +"use client"; + +import React, { useMemo } from "react"; +import Feature from "ol/Feature"; + +import SCADADataPanel from "@components/olmap/SCADA/SCADADataPanel"; +import { inferHistoryFeatureInfos } from "./toolbarFeatureHelpers"; +import HistoryDataPanel from "./HistoryDataPanel"; + +type ToolbarHistoryPanelProps = { + showHistoryPanel: boolean; + chatPanelType: "realtime" | "scheme" | "none"; + chatPanelFeatureInfos: [string, string][] | null; + chatPanelTimeRange: { + startTime?: string; + endTime?: string; + } | null; + highlightFeatures: Feature[]; + HistoryPanel?: React.FC; + schemeName?: string; + queryType?: string; + onClose: () => void; +}; + +const ToolbarHistoryPanel: React.FC = ({ + showHistoryPanel, + chatPanelType, + chatPanelFeatureInfos, + chatPanelTimeRange, + highlightFeatures, + HistoryPanel, + schemeName, + queryType, + onClose, +}) => { + const featureInfos = useMemo( + () => chatPanelFeatureInfos ?? inferHistoryFeatureInfos(highlightFeatures), + [chatPanelFeatureInfos, highlightFeatures], + ); + + if (!showHistoryPanel) { + return null; + } + + if (chatPanelType === "none" && chatPanelFeatureInfos) { + return ( + id)} + visible={showHistoryPanel} + start_time={chatPanelTimeRange?.startTime} + end_time={chatPanelTimeRange?.endTime} + onClose={onClose} + /> + ); + } + + if (HistoryPanel) { + return ( + + ); + } + + return ( + + ); +}; + +export default ToolbarHistoryPanel; diff --git a/src/components/olmap/core/Controls/toolbarFeatureHelpers.ts b/src/components/olmap/core/Controls/toolbarFeatureHelpers.ts new file mode 100644 index 0000000..dc1019b --- /dev/null +++ b/src/components/olmap/core/Controls/toolbarFeatureHelpers.ts @@ -0,0 +1,350 @@ +import Feature from "ol/Feature"; + +import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units"; + +type ToolbarBaseProperty = { + label: string; + value: string | number; + unit?: string; + formatter?: (value: string | number) => string; +}; + +type ToolbarTableProperty = { + type: "table"; + label: string; + columns: string[]; + rows: (string | number)[][]; +}; + +export type ToolbarPropertyItem = ToolbarBaseProperty | ToolbarTableProperty; + +export type ToolbarPropertyPanelData = { + id?: string; + type?: string; + properties?: ToolbarPropertyItem[]; +}; + +const getFeatureHistoryType = (feature: Feature): string | null => { + const layerId = feature.getId()?.toString().split(".")[0] || ""; + if (layerId.includes("pipe")) return "pipe"; + if (layerId.includes("junction")) return "junction"; + if (layerId.includes("tank")) return "tank"; + if (layerId.includes("reservoir")) return "reservoir"; + if (layerId.includes("pump")) return "pump"; + if (layerId.includes("valve")) return "valve"; + return null; +}; + +export const inferHistoryFeatureInfos = ( + highlightFeatures: Feature[], +): [string, string][] => + highlightFeatures + .map((feature) => { + const properties = feature.getProperties(); + const id = properties.id; + if (!id) return null; + + const type = getFeatureHistoryType(feature); + if (type !== "pipe" && type !== "junction") { + return null; + } + + return [id, type] as [string, string]; + }) + .filter(Boolean) as [string, string][]; + +export const buildFeatureProperties = ( + highlightFeature: Feature | undefined, + computedProperties: Record, +): ToolbarPropertyPanelData => { + if (!highlightFeature) return {}; + + const layer = highlightFeature.getId()?.toString().split(".")[0]; + const properties = highlightFeature.getProperties(); + const pipeComputedFields = [ + { key: "flow", label: "流量", unit: `${FLOW_DISPLAY_UNIT}` }, + { 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: `${FLOW_DISPLAY_UNIT}` }, + { 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") { + const result: ToolbarPropertyPanelData = { + 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: "开" }, + ], + }; + + pipeComputedFields.forEach(({ key, label, unit }) => { + let value = computedProperties[key]; + + if (key === "flow" && value !== undefined) { + value = toM3h(value, "lps"); + } + + 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") { + const result: ToolbarPropertyPanelData = { + id: properties.id, + type: "节点", + properties: [ + { + label: "高程", + value: properties.elevation?.toFixed?.(1), + unit: "m", + }, + { + type: "table", + label: "基本需水量", + columns: ["demand", "pattern"], + rows: Array.from({ length: 5 }, (_, i) => i + 1) + .map((idx) => { + let demand = properties?.[`demand${idx}`]; + const pattern = properties?.[`pattern${idx}`]; + if ( + demand !== undefined && + demand !== null && + demand !== "" + ) { + demand = toM3h(Number(demand), "lps"); + return [ + typeof demand === "number" ? demand.toFixed(3) : demand, + pattern ?? "-", + ]; + } + return null; + }) + .filter(Boolean) as (string | number)[][], + }, + ], + }; + + nodeComputedFields.forEach(({ key, label, unit }) => { + if (computedProperties[key] !== undefined) { + let value = computedProperties[key]; + if (key === "actual_demand") { + value = toM3h(value, "lps"); + } + result.properties?.push({ + label, + value: value?.toFixed?.(3) || value, + 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.overflow ? "是" : "否", + }, + ], + }; + } + + if (layer === "geo_reservoirs_mat" || layer === "geo_reservoirs") { + return { + id: properties.id, + type: "水库", + properties: [ + { + label: "水头", + value: properties.head?.toFixed?.(1), + unit: "m", + }, + ], + }; + } + + 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.minor_loss?.toFixed?.(2), + }, + ], + }; + } + + const getTransmissionFrequency = (transmissionFrequency: string) => { + const parts = transmissionFrequency.split(":"); + if (parts.length !== 3) return transmissionFrequency; + const hours = parseInt(parts[0], 10); + const minutes = parseInt(parts[1], 10); + const seconds = parseInt(parts[2], 10); + return hours * 60 + minutes + (seconds >= 30 ? 1 : 0); + }; + + 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") { + return { + 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 {}; +}; diff --git a/src/components/olmap/core/Controls/useToolbarChatActions.ts b/src/components/olmap/core/Controls/useToolbarChatActions.ts new file mode 100644 index 0000000..b606d01 --- /dev/null +++ b/src/components/olmap/core/Controls/useToolbarChatActions.ts @@ -0,0 +1,157 @@ +import { useCallback, useEffect, useRef, type Dispatch, type SetStateAction } from "react"; +import Feature from "ol/Feature"; +import { GeoJSON } from "ol/format"; +import Point from "ol/geom/Point"; +import { bbox, featureCollection } from "@turf/turf"; + +import { useChatToolActionHandler } from "@/hooks/useChatToolActionHandler"; +import { applyJunctionAreaRender } from "@components/olmap/DMALeakDetection/applyJunctionAreaRender"; +import { queryFeaturesByIds } from "@/utils/mapQueryService"; +import { useMap } from "../MapComponent"; + +type UseToolbarChatActionsParams = { + setHighlightFeatures: Dispatch>; + setChatPanelFeatureInfos: Dispatch>; + setChatPanelType: Dispatch>; + setChatPanelTimeRange: Dispatch< + SetStateAction<{ startTime?: string; endTime?: string } | null> + >; + setShowHistoryPanel: Dispatch>; + setActiveTools: Dispatch>; +}; + +export const useToolbarChatActions = ({ + setHighlightFeatures, + setChatPanelFeatureInfos, + setChatPanelType, + setChatPanelTimeRange, + setShowHistoryPanel, + setActiveTools, +}: UseToolbarChatActionsParams) => { + const map = useMap(); + const chatJunctionRenderCleanupRef = useRef<(() => void) | null>(null); + + const disposeChatJunctionRender = useCallback(() => { + chatJunctionRenderCleanupRef.current?.(); + chatJunctionRenderCleanupRef.current = null; + }, []); + + useEffect(() => () => disposeChatJunctionRender(), [disposeChatJunctionRender]); + + useChatToolActionHandler( + useCallback( + (action) => { + const geojsonFormat = new GeoJSON(); + const zoomToFeatures = ( + features: Feature[], + geometryKind: "point" | "line", + ) => { + if (features.length === 0) return; + + if (geometryKind === "point" && features.length === 1) { + const geometry = features[0].getGeometry(); + if (geometry instanceof Point) { + map?.getView().animate({ + center: geometry.getCoordinates(), + zoom: 18, + duration: 1000, + }); + return; + } + } + + const geojsonFeatures = features.map((feature) => + geojsonFormat.writeFeatureObject(feature), + ); + const extent = bbox(featureCollection(geojsonFeatures as any)); + if (extent) { + map?.getView().fit(extent, { + maxZoom: 18, + duration: 1000, + padding: + geometryKind === "line" + ? [60, 60, 60, 60] + : [40, 40, 40, 40], + }); + } + }; + + const locateFeatures = ( + ids: string[], + layer: string, + geometryKind: "point" | "line", + ) => { + queryFeaturesByIds(ids, layer).then((features) => { + if (features.length > 0) { + setHighlightFeatures(features); + zoomToFeatures(features, geometryKind); + } + }); + }; + + switch (action.type) { + case "locate_features": { + locateFeatures(action.ids, action.layer, action.geometryKind); + break; + } + case "view_history": { + setChatPanelFeatureInfos(action.featureInfos); + setChatPanelType(action.dataType); + setChatPanelTimeRange({ + startTime: action.startTime, + endTime: action.endTime, + }); + setShowHistoryPanel(true); + break; + } + case "view_scada": { + setChatPanelFeatureInfos(action.featureInfos); + setChatPanelType("none"); + setChatPanelTimeRange({ + startTime: action.startTime, + endTime: action.endTime, + }); + setShowHistoryPanel(true); + setActiveTools((prev) => { + if (prev.includes("history")) { + return prev; + } + return [...prev, "history"]; + }); + break; + } + case "render_junctions": { + disposeChatJunctionRender(); + + if (Object.keys(action.nodeAreaMap).length === 0) { + break; + } + + if (map) { + chatJunctionRenderCleanupRef.current = applyJunctionAreaRender( + map, + { + nodeAreaMap: action.nodeAreaMap, + areaIds: action.areaIds, + areaColors: action.areaColors, + }, + { propertyKey: "chat_junction_render_index" }, + ); + } + break; + } + } + }, + [ + disposeChatJunctionRender, + map, + setActiveTools, + setChatPanelFeatureInfos, + setChatPanelTimeRange, + setChatPanelType, + setHighlightFeatures, + setShowHistoryPanel, + ], + ), + ); +};