490 lines
15 KiB
TypeScript
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;
|