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

490 lines
15 KiB
TypeScript

import React, { useState, useEffect, useCallback } from "react";
import { useData, useMap } from "../MapComponent";
import ToolbarButton from "@/components/olmap/common/ToolbarButton";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import PaletteOutlinedIcon from "@mui/icons-material/PaletteOutlined";
import PropertyPanel from "./PropertyPanel"; // 引入属性面板组件
import DrawPanel from "./DrawPanel"; // 引入绘图面板组件
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import { Style, Stroke, Fill, Circle } from "ol/style";
import { FeatureLike } from "ol/Feature";
import Feature from "ol/Feature";
import StyleEditorPanel from "./StyleEditorPanel";
import StyleLegend from "./StyleLegend"; // 引入图例组件
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import { config } from "@/config/config";
const backendUrl = config.backendUrl;
// 图层样式状态接口
interface StyleConfig {
property: string;
classificationMethod: string;
segments: number;
minSize: number;
maxSize: number;
minStrokeWidth: number;
maxStrokeWidth: number;
fixedStrokeWidth: number;
colorType: string;
startColor: string;
endColor: string;
showLabels: boolean;
opacity: number;
adjustWidthByProperty: boolean;
}
interface LegendStyleConfig {
layerId: string;
layerName: string;
property: string;
colors: string[];
type: string;
dimensions: number[];
breaks: number[];
}
interface LayerStyleState {
layerId: string;
layerName: string;
styleConfig: StyleConfig;
legendConfig: LegendStyleConfig;
isActive: boolean;
}
// 添加接口定义隐藏按钮的props
interface ToolbarProps {
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
queryType?: string; // 可选的查询类型参数
}
const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
const map = useMap();
const data = useData();
if (!data) return null;
const { currentTime, selectedDate, schemeName } = data;
const [activeTools, setActiveTools] = useState<string[]>([]);
const [highlightFeature, setHighlightFeature] = useState<FeatureLike | null>(
null
);
const [showPropertyPanel, setShowPropertyPanel] = useState<boolean>(false);
const [showDrawPanel, setShowDrawPanel] = useState<boolean>(false);
const [showStyleEditor, setShowStyleEditor] = useState<boolean>(false);
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
{
isActive: false, // 默认不激活,不显示图例
layerId: "junctions",
layerName: "节点图层",
styleConfig: {
property: "pressure",
classificationMethod: "pretty_breaks",
segments: 6,
minSize: 4,
maxSize: 12,
minStrokeWidth: 2,
maxStrokeWidth: 8,
fixedStrokeWidth: 3,
colorType: "gradient",
startColor: "rgba(51, 153, 204, 0.9)",
endColor: "rgba(204, 51, 51, 0.9)",
showLabels: false,
opacity: 0.9,
adjustWidthByProperty: true,
},
legendConfig: {
layerId: "junctions",
layerName: "节点图层",
property: "压力", // 暂时为空,等计算后更新
colors: [],
type: "point",
dimensions: [],
breaks: [],
},
},
{
isActive: false, // 默认不激活,不显示图例
layerId: "pipes",
layerName: "管道图层",
styleConfig: {
property: "flow",
classificationMethod: "pretty_breaks",
segments: 6,
minSize: 4,
maxSize: 12,
minStrokeWidth: 2,
maxStrokeWidth: 8,
fixedStrokeWidth: 3,
colorType: "gradient",
startColor: "rgba(51, 153, 204, 0.9)",
endColor: "rgba(204, 51, 51, 0.9)",
showLabels: false,
opacity: 0.9,
adjustWidthByProperty: true,
},
legendConfig: {
layerId: "pipes",
layerName: "管道图层",
property: "流量", // 暂时为空,等计算后更新
colors: [],
type: "linestring",
dimensions: [],
breaks: [],
},
},
]);
// 计算激活的图例配置
const activeLegendConfigs = layerStyleStates
.filter((state) => state.isActive && state.legendConfig.property)
.map((state) => ({
...state.legendConfig,
layerName: state.layerName,
layerId: state.layerId,
}));
// 创建高亮图层
useEffect(() => {
if (!map) return;
const highLightSource = new VectorSource();
const highLightLayer = new VectorLayer({
source: highLightSource,
style: new Style({
stroke: new Stroke({
color: `rgba(255, 0, 0, 1)`,
width: 5,
}),
fill: new Fill({
color: `rgba(255, 0, 0, 0.2)`,
}),
image: new Circle({
radius: 7,
stroke: new Stroke({
color: `rgba(255, 0, 0, 1)`,
width: 3,
}),
fill: new Fill({
color: `rgba(255, 0, 0, 0.2)`,
}),
}),
}),
});
map.addLayer(highLightLayer);
setHighlightLayer(highLightLayer);
return () => {
map.removeLayer(highLightLayer);
};
}, [map]);
// 高亮要素的函数
useEffect(() => {
if (!highlightLayer) {
return;
}
const source = highlightLayer.getSource();
if (!source) {
return;
}
// 清除之前的高亮
source.clear();
// 添加新的高亮要素
if (highlightFeature instanceof Feature) {
source.addFeature(highlightFeature);
}
}, [highlightFeature]);
// 地图点击选择要素事件处理函数
const handleMapClickSelectFeatures = useCallback(
async (event: { coordinate: number[] }) => {
if (!map) return;
const feature = await mapClickSelectFeatures(event, map); // 调用导入的函数
setHighlightFeature(feature);
},
[map, setHighlightFeature]
);
// 添加矢量属性查询事件监听器
useEffect(() => {
if (!activeTools.includes("info") || !map) return;
map.on("click", handleMapClickSelectFeatures);
return () => {
map.un("click", handleMapClickSelectFeatures);
};
}, [activeTools, map, handleMapClickSelectFeatures]);
// 处理工具栏按钮点击事件
const handleToolClick = (tool: string) => {
// 样式工具的特殊处理 - 只有再次点击时才会取消激活和关闭
if (tool === "style") {
if (activeTools.includes("style")) {
// 如果样式工具已激活,点击时关闭
setShowStyleEditor(false);
setActiveTools((prev) => prev.filter((t) => t !== "style"));
} else {
// 激活样式工具,打开样式面板
setActiveTools((prev) => [...prev, "style"]);
setShowStyleEditor(true);
}
return;
}
// 其他工具的处理逻辑
if (activeTools.includes(tool)) {
// 如果当前工具已激活,再次点击时取消激活并关闭面板
deactivateTool(tool);
setActiveTools((prev) => prev.filter((t) => t !== tool));
} else {
// 如果当前工具未激活,先关闭所有其他工具,然后激活当前工具
// 关闭所有面板(但保持样式编辑器状态)
closeAllPanelsExceptStyle();
// 取消激活所有非样式工具
setActiveTools((prev) => {
const styleActive = prev.includes("style");
return styleActive ? ["style", tool] : [tool];
});
// 激活当前工具并打开对应面板
activateTool(tool);
}
};
// 取消激活指定工具并关闭对应面板
const deactivateTool = (tool: string) => {
switch (tool) {
case "info":
setShowPropertyPanel(false);
setHighlightFeature(null);
break;
case "draw":
setShowDrawPanel(false);
break;
}
};
// 激活指定工具并打开对应面板
const activateTool = (tool: string) => {
switch (tool) {
case "info":
setShowPropertyPanel(true);
break;
case "draw":
setShowDrawPanel(true);
break;
}
};
// 关闭所有面板(除了样式编辑器)
const closeAllPanelsExceptStyle = () => {
setShowPropertyPanel(false);
setHighlightFeature(null);
setShowDrawPanel(false);
// 样式编辑器保持其当前状态,不自动关闭
};
const [computedProperties, setComputedProperties] = useState<
Record<string, any>
>({});
// 添加 useEffect 来查询计算属性
useEffect(() => {
if (!highlightFeature || !selectedDate) {
setComputedProperties({});
return;
}
const id = highlightFeature.getProperties().id;
if (!id) {
setComputedProperties({});
return;
}
const queryComputedProperties = async () => {
try {
const properties = highlightFeature?.getProperties?.() || {};
const type =
properties.geometry?.getType?.() === "LineString" ? "link" : "node";
// selectedDate 格式化为 YYYY-MM-DD
let dateObj: Date;
if (selectedDate instanceof Date) {
dateObj = new Date(selectedDate);
} else {
dateObj = new Date(selectedDate);
}
const minutes = Number(currentTime) || 0;
dateObj.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
// 转为 UTC ISO 字符串
const querytime = dateObj.toISOString(); // 例如 "2025-09-16T16:30:00.000Z"
let response;
if (queryType === "scheme") {
response = await fetch(
`${backendUrl}/queryschemesimulationrecordsbyidtime/?scheme_name=${schemeName}&id=${id}&querytime=${querytime}&type=${type}`
);
} else {
response = await fetch(
`${backendUrl}/querysimulationrecordsbyidtime/?id=${id}&querytime=${querytime}&type=${type}`
);
}
if (!response.ok) {
throw new Error("API request failed");
}
const data = await response.json();
setComputedProperties(data.results[0] || {});
} catch (error) {
console.error("Error querying computed properties:", error);
setComputedProperties({});
}
};
// 仅当 currentTime 有效时查询
if (currentTime !== -1 && queryType) queryComputedProperties();
}, [highlightFeature, currentTime, selectedDate]);
// 从要素属性中提取属性面板需要的数据
const getFeatureProperties = useCallback(() => {
if (!highlightFeature) return {};
const properties = highlightFeature.getProperties();
// 计算属性字段,增加 key 字段
const pipeComputedFields = [
{ key: "flow", label: "流量", unit: "m³/s" },
{ key: "friction", label: "摩阻", unit: "" },
{ key: "headloss", label: "水头损失", unit: "m" },
{ key: "quality", label: "水质", unit: "mg/L" },
{ key: "reaction", label: "反应", unit: "1/s" },
{ key: "setting", label: "设置", unit: "" },
{ key: "status", label: "状态", unit: "" },
{ key: "velocity", label: "流速", unit: "m/s" },
];
const nodeComputedFields = [
{ key: "actualdemand", label: "实际需水量", unit: "m³/s" },
{ key: "head", label: "水头", unit: "m" },
{ key: "pressure", label: "压力", unit: "kPa" },
{ key: "quality", label: "水质", unit: "mg/L" },
];
if (properties.geometry.getType() === "LineString") {
let result = {
id: properties.id,
type: "管道",
properties: [
{ label: "起始节点ID", value: properties.node1 },
{ label: "终点节点ID", value: properties.node2 },
{ label: "长度", value: properties.length?.toFixed?.(1), unit: "m" },
{
label: "管径",
value: properties.diameter?.toFixed?.(1),
unit: "mm",
},
{ label: "粗糙度", value: properties.roughness },
{ label: "局部损失", value: properties.minor_loss },
{ label: "初始状态", value: "开" },
],
};
// 追加计算属性
if (computedProperties) {
pipeComputedFields.forEach(({ key, label, unit }) => {
if (computedProperties[key] !== undefined) {
result.properties.push({
label,
value:
computedProperties[key].toFixed?.(2) || computedProperties[key],
unit,
});
}
});
}
return result;
}
if (properties.geometry.getType() === "Point") {
let result = {
id: properties.id,
type: "节点",
properties: [
{
label: "海拔",
value: properties.elevation?.toFixed?.(1),
unit: "m",
},
{
label: "需求量",
value: properties.demand?.toFixed?.(1),
unit: "m³/s",
},
],
};
// 追加计算属性
if (computedProperties) {
nodeComputedFields.forEach(({ key, label, unit }) => {
if (computedProperties[key] !== undefined) {
result.properties.push({
label,
value:
computedProperties[key].toFixed?.(2) || computedProperties[key],
unit,
});
}
});
}
return result;
}
return {};
}, [highlightFeature, computedProperties]);
return (
<>
<div className="absolute top-4 left-4 bg-white p-1 rounded-xl shadow-lg flex opacity-85 hover:opacity-100 transition-opacity">
{!hiddenButtons?.includes("info") && (
<ToolbarButton
icon={<InfoOutlinedIcon />}
name="查看属性"
isActive={activeTools.includes("info")}
onClick={() => handleToolClick("info")}
/>
)}
{!hiddenButtons?.includes("draw") && (
<ToolbarButton
icon={<EditOutlinedIcon />}
name="矢量编辑"
isActive={activeTools.includes("draw")}
onClick={() => handleToolClick("draw")}
/>
)}
{!hiddenButtons?.includes("style") && (
<ToolbarButton
icon={<PaletteOutlinedIcon />}
name="图层样式"
isActive={activeTools.includes("style")}
onClick={() => handleToolClick("style")}
/>
)}
</div>
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
{showDrawPanel && map && <DrawPanel />}
{showStyleEditor && (
<StyleEditorPanel
layerStyleStates={layerStyleStates}
setLayerStyleStates={setLayerStyleStates}
/>
)}
{/* 图例显示 */}
{activeLegendConfigs.length > 0 && (
<div className="absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
<div className="flex flex-row gap-3">
{activeLegendConfigs.map((config, index) => (
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
))}
</div>
</div>
)}
</>
);
};
export default Toolbar;