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

578 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { Geometry } from "ol/geom";
import { Point, LineString, Polygon } from "ol/geom";
import { FeatureLike } from "ol/Feature";
import Feature from "ol/Feature";
import GeoJSON from "ol/format/GeoJSON";
import StyleEditorPanel from "./StyleEditorPanel";
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
import VectorTileSource from "ol/source/VectorTile";
import TileState from "ol/TileState";
import { toLonLat } from "ol/proj";
import { booleanIntersects, buffer, point, toWgs84 } from "@turf/turf";
import RenderFeature from "ol/render/Feature";
import { config } from "@/config/config";
const backendUrl = config.backendUrl;
const Toolbar: React.FC = () => {
const map = useMap();
const data = useData();
if (!data) return null;
const { currentTime, selectedDate } = 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);
// 创建高亮图层
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]);
// 将 RenderFeature 转换为 Feature
const renderFeature2Feature = (renderFeature: RenderFeature) => {
if (renderFeature) {
const geometry = renderFeature.getGeometry();
if (geometry) {
try {
let clonedGeometry;
if (geometry instanceof Geometry) {
// 标准 Feature 的几何体
clonedGeometry = geometry;
} else {
// RenderFeature 或其他类型的几何体
const type = geometry.getType();
const flatCoordinates = geometry.getFlatCoordinates();
let coordinates: number[] | number[][] | number[][][];
switch (type) {
case "Point":
// Point: [x, y]
coordinates = [flatCoordinates[0], flatCoordinates[1]];
clonedGeometry = new Point(coordinates as number[]);
break;
case "LineString":
// LineString: [[x1, y1], [x2, y2], ...]
const lineCoords: number[][] = [];
for (let i = 0; i < flatCoordinates.length; i += 2) {
lineCoords.push([flatCoordinates[i], flatCoordinates[i + 1]]);
}
clonedGeometry = new LineString(lineCoords);
break;
case "Polygon":
// Polygon: [[[x1, y1], [x2, y2], ...]]
// 需要获取环的结束位置
const ends = (
geometry as { getEnds?: () => number[] }
).getEnds?.() || [flatCoordinates.length];
const rings: number[][][] = [];
let start = 0;
for (const end of ends) {
const ring: number[][] = [];
for (let i = start; i < end; i += 2) {
ring.push([flatCoordinates[i], flatCoordinates[i + 1]]);
}
rings.push(ring);
start = end;
}
clonedGeometry = new Polygon(rings);
break;
default:
console.log("不支持的几何体类型:", type);
return;
}
}
const feature = new Feature({
geometry: clonedGeometry,
...renderFeature.getProperties(),
});
return feature;
} catch (error) {
console.error("RenderFeature转换Feature时出错:", error);
}
}
}
};
// 根据 IDs通过 Geoserver WFS 服务查询要素
const queryFeaturesByIds = async (ids: string[], layer?: string) => {
if (!ids.length) return [];
const geoserverUrl = "http://127.0.0.1:8080/geoserver";
const network = "TJWater";
const layers = ["geo_pipes_mat", "geo_junctions_mat"];
const orFilter = ids.map((id) => `id=${id}`).join(" OR ");
try {
if (!layer) {
// 遍历所有图层
const promises = layers.map(async (layer) => {
try {
const url =
`${geoserverUrl}/${network}/ows?` +
`service=WFS&version=1.0.0&request=GetFeature&` +
`typeName=${network}:${layer}&outputFormat=application/json&` +
`CQL_FILTER=${encodeURIComponent(orFilter)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`请求失败: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error(`图层 ${layer} 查询失败:`, error);
return null; // 返回 null 表示该图层查询失败
}
});
const results = await Promise.all(promises);
const features = results
.filter((json) => json !== null) // 过滤掉失败的请求
.flatMap((json) => new GeoJSON().readFeatures(json));
// console.log("查询到的要素:", features);
return features;
} else {
// 查询指定图层
const url =
`${geoserverUrl}/${network}/ows?` +
`service=WFS&version=1.0.0&request=GetFeature&` +
`typeName=${network}:${layer}&outputFormat=application/json&` +
`CQL_FILTER=${encodeURIComponent(orFilter)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`请求失败: ${response.statusText}`);
}
const json = await response.json();
const features = new GeoJSON().readFeatures(json);
// console.log("查询到的要素:", features);
return features;
}
} catch (error) {
console.error("根据 IDs 查询要素时出错:", error);
return [];
}
};
// 处理地图点击选择要素
const handleMapClickSelectFeatures = useCallback(
(event: { coordinate: number[] }) => {
if (!map) return;
const coord = event.coordinate;
let z = Math.floor(map.getView().getZoom() || 0) - 1; // 确保 z 是整数
const projection = map.getView().getProjection(); // 获取地图的投影
const pixelRatio = window.devicePixelRatio; // 获取设备像素比率
const [x, y] = coord;
// 遍历所有的 VectorTileSources
const vectorTileSources = map
.getAllLayers()
.filter((layer) => layer instanceof WebGLVectorTileLayer)
.map((layer) => layer.getSource() as VectorTileSource)
.filter((source) => source);
if (!vectorTileSources.length) return;
// 按几何类型分类,优先处理级别
const points: any[] = [];
const lines: any[] = [];
const others: any[] = [];
vectorTileSources.forEach((vectorTileSource) => {
const tileGrid = vectorTileSource.getTileGrid();
if (tileGrid) {
const minZoom = tileGrid.getMinZoom(); // 最小缩放级别
const maxZoom = tileGrid.getMaxZoom(); // 最大缩放级别
// 确保 z 在有效范围内
if (z < minZoom) z = minZoom;
if (z > maxZoom) z = maxZoom;
} else {
return;
}
const tileCoord = tileGrid.getTileCoordForCoordAndZ([x, y], z);
// 设置 resolution 用于基于屏幕像素的 buffer 容差计算
const resolution = tileGrid.getResolution(tileCoord[0]);
const hitTolerance = 5; // 像素容差
const hitPoint = point(toLonLat(coord));
const buffered = buffer(hitPoint, resolution * hitTolerance, {
units: "meters",
});
// 获取 VectorRenderTile
const vectorRenderTile = vectorTileSource.getTile(
tileCoord[0],
tileCoord[1],
tileCoord[2],
pixelRatio,
projection
);
// 获取 SourceTiles
const vectorTiles = vectorTileSource.getSourceTiles(
pixelRatio,
projection,
vectorRenderTile
);
vectorTiles.forEach((vectorTile) => {
if (vectorTile.getState() === TileState.LOADED) {
const renderFeatures = vectorTile.getFeatures();
const selectedFeatures = renderFeatures
.map(
(renderFeature) =>
renderFeature2Feature(renderFeature) as Feature<any>
)
.filter((feature) => {
if (feature && buffered) {
const geoJSONGeometry = new GeoJSON().writeGeometryObject(
feature.getGeometry()
);
const bufferedGeometry = buffered.geometry;
return booleanIntersects(
toWgs84(geoJSONGeometry),
bufferedGeometry
);
}
return false;
});
selectedFeatures.forEach((selectedFeature) => {
const geometryType = selectedFeature.getGeometry()?.getType();
if (geometryType === "Point") {
points.push(selectedFeature);
} else if (geometryType === "LineString") {
lines.push(selectedFeature);
} else {
others.push(selectedFeature);
}
});
}
});
});
// 按优先级处理:点 > 线 > 其他
const selectedFeatures = [...points, ...lines, ...others];
const firstFeature = selectedFeatures[0] as Feature<any>;
const queryId = firstFeature?.getProperties().id;
// console.log(queryId, "queryId");
if (queryId) {
queryFeaturesByIds([queryId]).then((features) => {
// console.log("查询到的要素:", features);
setHighlightFeature(features[0]);
});
} else {
setHighlightFeature(null);
}
},
[map, highlightLayer, 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"
const response = await fetch(
`${backendUrl}/queryrecordsbyidtime/?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({});
}
};
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">
<ToolbarButton
icon={<InfoOutlinedIcon />}
name="查看属性"
isActive={activeTools.includes("info")}
onClick={() => handleToolClick("info")}
/>
<ToolbarButton
icon={<EditOutlinedIcon />}
name="矢量编辑"
isActive={activeTools.includes("draw")}
onClick={() => handleToolClick("draw")}
/>
<ToolbarButton
icon={<PaletteOutlinedIcon />}
name="图层样式"
isActive={activeTools.includes("style")}
onClick={() => handleToolClick("style")}
/>
</div>
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
{showDrawPanel && map && <DrawPanel />}
{showStyleEditor && <StyleEditorPanel />}
</>
);
};
export default Toolbar;