拆分、重构 Toolbar
This commit is contained in:
@@ -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: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<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,
|
||||
],
|
||||
),
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user