578 lines
20 KiB
TypeScript
578 lines
20 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 { 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;
|