拆分、重构 Toolbar

This commit is contained in:
2026-05-18 15:49:38 +08:00
parent 45274955c6
commit 39ee9a02e5
4 changed files with 633 additions and 532 deletions
+34 -532
View File
@@ -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<ToolbarProps> = ({
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<LayerStyleState[]>([
@@ -556,306 +449,10 @@ const Toolbar: React.FC<ToolbarProps> = ({
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: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 {};
}, [highlightFeatures, computedProperties]);
const propertyPanelData = useMemo(
() => buildFeatureProperties(highlightFeatures[0], computedProperties),
[highlightFeatures, computedProperties],
);
if (!data) {
return null;
@@ -908,7 +505,7 @@ const Toolbar: React.FC<ToolbarProps> = ({
</div>
{showPropertyPanel && (
<PropertyPanel
{...getFeatureProperties()}
{...propertyPanelData}
onClose={() => {
deactivateTool("info");
setActiveTools((prev) => prev.filter((t) => t !== "info"));
@@ -922,115 +519,20 @@ const Toolbar: React.FC<ToolbarProps> = ({
setLayerStyleStates={setLayerStyleStates}
/>
</div>
{showHistoryPanel &&
(chatPanelType === "none" && chatPanelFeatureInfos ? (
<SCADADataPanel
deviceIds={chatPanelFeatureInfos.map(([id]) => id)}
visible={showHistoryPanel}
start_time={chatPanelTimeRange?.startTime}
end_time={chatPanelTimeRange?.endTime}
onClose={() => {
deactivateTool("history");
setActiveTools((prev) => prev.filter((t) => t !== "history"));
}}
/>
) : HistoryPanel ? (
<HistoryPanel
featureInfos={chatPanelFeatureInfos ?? (() => {
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"));
}}
/>
) : (
<HistoryDataPanel
featureInfos={chatPanelFeatureInfos ?? (() => {
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"));
}}
/>
))}
<ToolbarHistoryPanel
showHistoryPanel={showHistoryPanel}
chatPanelType={chatPanelType}
chatPanelFeatureInfos={chatPanelFeatureInfos}
chatPanelTimeRange={chatPanelTimeRange}
highlightFeatures={highlightFeatures}
HistoryPanel={HistoryPanel}
schemeName={schemeName}
queryType={queryType}
onClose={() => {
deactivateTool("history");
setActiveTools((prev) => prev.filter((t) => t !== "history"));
}}
/>
{/* 图例显示 */}
{activeLegendConfigs.length > 0 && (
@@ -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<any>;
schemeName?: string;
queryType?: string;
onClose: () => void;
};
const ToolbarHistoryPanel: React.FC<ToolbarHistoryPanelProps> = ({
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 (
<SCADADataPanel
deviceIds={chatPanelFeatureInfos.map(([id]) => id)}
visible={showHistoryPanel}
start_time={chatPanelTimeRange?.startTime}
end_time={chatPanelTimeRange?.endTime}
onClose={onClose}
/>
);
}
if (HistoryPanel) {
return (
<HistoryPanel
featureInfos={featureInfos}
scheme_type="burst_analysis"
scheme_name={schemeName}
type={
chatPanelFeatureInfos
? chatPanelType
: (queryType as "realtime" | "scheme" | "none")
}
start_time={chatPanelTimeRange?.startTime}
end_time={chatPanelTimeRange?.endTime}
onClose={onClose}
/>
);
}
return (
<HistoryDataPanel
featureInfos={featureInfos}
scheme_type="burst_analysis"
scheme_name={schemeName}
type={
chatPanelFeatureInfos
? chatPanelType
: (queryType as "realtime" | "scheme" | "none")
}
start_time={chatPanelTimeRange?.startTime}
end_time={chatPanelTimeRange?.endTime}
onClose={onClose}
/>
);
};
export default ToolbarHistoryPanel;
@@ -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<string, any>,
): 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 {};
};
@@ -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<SetStateAction<Feature[]>>;
setChatPanelFeatureInfos: Dispatch<SetStateAction<[string, string][] | null>>;
setChatPanelType: Dispatch<SetStateAction<"realtime" | "scheme" | "none">>;
setChatPanelTimeRange: Dispatch<
SetStateAction<{ startTime?: string; endTime?: string } | null>
>;
setShowHistoryPanel: Dispatch<SetStateAction<boolean>>;
setActiveTools: Dispatch<SetStateAction<string[]>>;
};
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,
],
),
);
};