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([]); const [highlightFeature, setHighlightFeature] = useState( null ); const [showPropertyPanel, setShowPropertyPanel] = useState(false); const [showDrawPanel, setShowDrawPanel] = useState(false); const [showStyleEditor, setShowStyleEditor] = useState(false); const [highlightLayer, setHighlightLayer] = useState | 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 ) .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; 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 >({}); // 添加 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 ( <>
} name="查看属性" isActive={activeTools.includes("info")} onClick={() => handleToolClick("info")} /> } name="矢量编辑" isActive={activeTools.includes("draw")} onClick={() => handleToolClick("draw")} /> } name="图层样式" isActive={activeTools.includes("style")} onClick={() => handleToolClick("style")} />
{showPropertyPanel && } {showDrawPanel && map && } {showStyleEditor && } ); }; export default Toolbar;