"use client"; import { config } from "@/config/config"; import React, { createContext, useContext, useState, useEffect, useRef, } from "react"; import { Map as OlMap, VectorTile } from "ol"; import View from "ol/View.js"; import "ol/ol.css"; import MapTools from "./MapTools"; import { Layer } from "ol/layer"; // 保留导入,但用于继承 import VectorTileSource from "ol/source/VectorTile"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; import MVT from "ol/format/MVT"; import { FlatStyleLike } from "ol/style/flat"; import { toLonLat } from "ol/proj"; import { center } from "@turf/center"; import { bearing } from "@turf/turf"; import { Deck } from "@deck.gl/core"; import { TextLayer } from "@deck.gl/layers"; import { TripsLayer } from "@deck.gl/geo-layers"; import { CollisionFilterExtension } from "@deck.gl/extensions"; interface MapComponentProps { children?: React.ReactNode; } interface DataContextType { currentJunctionCalData?: any[]; // 当前计算结果 setCurrentJunctionCalData?: React.Dispatch>; currentPipeCalData?: any[]; // 当前计算结果 setCurrentPipeCalData?: React.Dispatch>; showJunctionText?: boolean; // 是否显示节点文本 showPipeText?: boolean; // 是否显示管道文本 setShowJunctionText?: React.Dispatch>; setShowPipeText?: React.Dispatch>; junctionText: string; pipeText: string; setJunctionText?: React.Dispatch>; setPipeText?: React.Dispatch>; } // 创建自定义Layer类来包装deck.gl class DeckLayer extends Layer { private deck: Deck; constructor(deckInstance: Deck) { super({}); this.deck = deckInstance; } render(frameState: any): HTMLElement { const { size, viewState } = frameState; const [width, height] = size; const [longitude, latitude] = toLonLat(viewState.center); const zoom = viewState.zoom - 1; // 调整 zoom 以匹配 const bearing = (-viewState.rotation * 180) / Math.PI; const deckViewState = { bearing, longitude, latitude, zoom }; this.deck.setProps({ width, height, viewState: deckViewState }); this.deck.redraw(); // 返回deck.gl的canvas元素 return document.getElementById("deck-canvas") as HTMLElement; } } // 跨组件传递 const MapContext = createContext(undefined); const DataContext = createContext(undefined); const extent = config.mapExtent; const mapUrl = config.mapUrl; // 添加防抖函数 function debounce any>(func: F, waitFor: number) { let timeout: ReturnType | null = null; return (...args: Parameters): void => { if (timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(() => func(...args), waitFor); }; } export const useMap = () => { return useContext(MapContext); }; export const useData = () => { return useContext(DataContext); }; const MapComponent: React.FC = ({ children }) => { const mapRef = useRef(null); const deckRef = useRef(null); const [map, setMap] = useState(); // currentCalData 用于存储当前计算结果 const [currentJunctionCalData, setCurrentJunctionCalData] = useState( [] ); const [currentPipeCalData, setCurrentPipeCalData] = useState([]); // junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染 const [junctionData, setJunctionDataState] = useState([]); const [pipeData, setPipeDataState] = useState([]); const junctionDataIds = useRef(new Set()); const pipeDataIds = useRef(new Set()); const tileJunctionDataBuffer = useRef([]); const tilePipeDataBuffer = useRef([]); const [showJunctionText, setShowJunctionText] = useState(false); // 控制节点文本显示 const [showPipeText, setShowPipeText] = useState(false); // 控制管道文本显示 const [showJunctionTextLayer, setShowJunctionTextLayer] = useState(true); // 控制节点文本图层显示 const [showPipeTextLayer, setShowPipeTextLayer] = useState(true); // 控制管道文本图层显示 const [junctionText, setJunctionText] = useState("pressure"); const [pipeText, setPipeText] = useState("flow"); const flowAnimation = useRef(false); // 添加动画控制标志 const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别 // 防抖更新函数 const debouncedUpdateData = useRef( debounce(() => { if (tileJunctionDataBuffer.current.length > 0) { setJunctionData(tileJunctionDataBuffer.current); tileJunctionDataBuffer.current = []; } if (tilePipeDataBuffer.current.length > 0) { setPipeData(tilePipeDataBuffer.current); tilePipeDataBuffer.current = []; } }, 100) ); const setJunctionData = (newData: any[]) => { const uniqueNewData = newData.filter((item) => { if (!item || !item.id) return false; if (!junctionDataIds.current.has(item.id)) { junctionDataIds.current.add(item.id); return true; } return false; }); if (uniqueNewData.length > 0) { setJunctionDataState((prev) => [...prev, ...uniqueNewData]); } }; const setPipeData = (newData: any[]) => { const uniqueNewData = newData.filter((item) => { if (!item || !item.id) return false; if (!pipeDataIds.current.has(item.id)) { pipeDataIds.current.add(item.id); return true; } return false; }); if (uniqueNewData.length > 0) { setPipeDataState((prev) => [...prev, ...uniqueNewData]); } }; const defaultFlatStyle: FlatStyleLike = { "stroke-width": 3, "stroke-color": "rgba(51, 153, 204, 0.9)", "circle-fill-color": "rgba(255,255,255,0.4)", "circle-stroke-color": "rgba(255,255,255,0.9)", "circle-radius": [ "interpolate", ["linear"], ["zoom"], 12, 1, // 在缩放级别 12 时,圆形半径为 1px 24, 12, // 在缩放级别 24 时,圆形半径为 12px ], }; // 矢量瓦片数据源和图层 const junctionSource = new VectorTileSource({ url: `${mapUrl}/gwc/service/tms/1.0.0/TJWater:geo_junctions_mat@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL format: new MVT(), projection: "EPSG:3857", }); const pipeSource = new VectorTileSource({ url: `${mapUrl}/gwc/service/tms/1.0.0/TJWater:geo_pipes_mat@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL format: new MVT(), projection: "EPSG:3857", }); // WebGL 渲染优化显示 const junctionLayer = new WebGLVectorTileLayer({ source: junctionSource as any, // 使用 WebGL 渲染 style: defaultFlatStyle, extent: extent, // 设置图层范围 maxZoom: 24, minZoom: 12, properties: { name: "节点图层", // 设置图层名称 value: "junctions", type: "point", properties: [ // { name: "需求量", value: "demand" }, // { name: "海拔高度", value: "elevation" }, { name: "实际需求量", value: "actualdemand" }, { name: "水头", value: "head" }, { name: "压力", value: "pressure" }, { name: "水质", value: "quality" }, ], }, }); const pipeLayer = new WebGLVectorTileLayer({ source: pipeSource as any, // 使用 WebGL 渲染 style: defaultFlatStyle, extent: extent, // 设置图层范围 maxZoom: 24, minZoom: 12, properties: { name: "管道图层", // 设置图层名称 value: "pipes", type: "linestring", properties: [ // { name: "直径", value: "diameter" }, // { name: "粗糙度", value: "roughness" }, // { name: "局部损失", value: "minor_loss" }, { name: "流量", value: "flow" }, { name: "摩阻系数", value: "friction" }, { name: "水头损失", value: "headloss" }, { name: "水质", value: "quality" }, { name: "反应速率", value: "reaction" }, { name: "设置值", value: "setting" }, { name: "状态", value: "status" }, { name: "流速", value: "velocity" }, ], }, }); useEffect(() => { if (!mapRef.current) return; // 缓存 junction、pipe 数据,提供给 deck.gl 显示标签使用 junctionSource.on("tileloadend", (event) => { try { if (event.tile instanceof VectorTile) { const renderFeatures = event.tile.getFeatures(); const data = new Map(); renderFeatures.forEach((renderFeature) => { const props = renderFeature.getProperties(); const featureId = props.id; if (featureId && !junctionDataIds.current.has(featureId)) { const geometry = renderFeature.getGeometry(); if (geometry) { const coordinates = geometry.getFlatCoordinates(); const coordWGS84 = toLonLat(coordinates); data.set(featureId, { id: featureId, position: coordWGS84, elevation: props.elevation || 0, demand: props.demand || 0, }); } } }); const uniqueData = Array.from(data.values()); if (uniqueData.length > 0) { tileJunctionDataBuffer.current.push(...uniqueData); debouncedUpdateData.current(); } } } catch (error) { console.error("Junction tile load error:", error); } }); pipeSource.on("tileloadend", (event) => { try { if (event.tile instanceof VectorTile) { const renderFeatures = event.tile.getFeatures(); const data = new Map(); renderFeatures.forEach((renderFeature) => { try { const props = renderFeature.getProperties(); const featureId = props.id; if (featureId && !pipeDataIds.current.has(featureId)) { const geometry = renderFeature.getGeometry(); if (geometry) { const flatCoordinates = geometry.getFlatCoordinates(); const stride = geometry.getStride(); // 获取步长,通常为 2 // 重建为 LineString GeoJSON 格式的 coordinates: [[x1, y1], [x2, y2], ...] const lineCoords = []; for (let i = 0; i < flatCoordinates.length; i += stride) { lineCoords.push([ flatCoordinates[i], flatCoordinates[i + 1], ]); } const lineCoordsWGS84 = lineCoords.map((coord) => { const [lon, lat] = toLonLat(coord); return [lon, lat]; }); // 计算中点 const midPoint = center({ type: "LineString", coordinates: lineCoordsWGS84, }).geometry.coordinates; // 计算角度 let lineAngle = bearing( lineCoordsWGS84[0], lineCoordsWGS84[lineCoordsWGS84.length - 1] ); lineAngle = -lineAngle + 90; if (lineAngle < -90 || lineAngle > 90) { lineAngle += 180; } // 计算时间戳(可选) const numSegments = lineCoordsWGS84.length - 1; const timestamps = [0]; if (numSegments > 0) { for (let i = 1; i <= numSegments; i++) { timestamps.push((i / numSegments) * 10); } } data.set(featureId, { id: featureId, diameter: props.diameter || 0, path: lineCoordsWGS84, // 使用重建后的坐标 position: midPoint, angle: lineAngle, timestamps, }); } } } catch (geomError) { console.error("Geometry calculation error:", geomError); } }); const uniqueData = Array.from(data.values()); if (uniqueData.length > 0) { tilePipeDataBuffer.current.push(...uniqueData); debouncedUpdateData.current(); } } } catch (error) { console.error("Pipe tile load error:", error); } }); // 更新标签可见性状态 // 监听 junctionLayer 的 visible 变化 const handleJunctionVisibilityChange = () => { const isVisible = junctionLayer.getVisible(); setShowJunctionTextLayer(isVisible); }; // 监听 pipeLayer 的 visible 变化 const handlePipeVisibilityChange = () => { const isVisible = pipeLayer.getVisible(); setShowPipeTextLayer(isVisible); }; // 添加事件监听器 junctionLayer.on("change:visible", handleJunctionVisibilityChange); pipeLayer.on("change:visible", handlePipeVisibilityChange); const map = new OlMap({ target: mapRef.current, view: new View({ projection: "EPSG:3857", }), // 图层依面、线、点、标注次序添加 layers: [pipeLayer, junctionLayer], controls: [], }); setMap(map); map.getView().fit(extent, { padding: [50, 50, 50, 50], // 添加一些内边距 duration: 1000, // 动画持续时间 }); // 监听缩放变化 map.getView().on("change", () => { setTimeout(() => { const zoom = map.getView().getZoom() || 0; setCurrentZoom(zoom); }, 0); }); // 初始化 deck.gl const deck = new Deck({ initialViewState: { longitude: 0, latitude: 0, zoom: 1, }, canvas: "deck-canvas", controller: false, // 由 OpenLayers 控制视图 layers: [], }); deckRef.current = deck; const deckLayer = new DeckLayer(deck); // deckLayer.setZIndex(1000); // 确保在最上层 map.addLayer(deckLayer); // 清理函数 return () => { junctionLayer.un("change:visible", handleJunctionVisibilityChange); pipeLayer.un("change:visible", handlePipeVisibilityChange); map.setTarget(undefined); map.dispose(); deck.finalize(); }; }, []); // 当数据变化时,更新 deck.gl 图层 useEffect(() => { const deck = deckRef.current; if (!deck) return; // 如果 deck 实例还未创建,则退出 const newLayers = [ new TextLayer({ id: "junctionTextLayer", zIndex: 10, data: showJunctionText ? junctionData : [], getPosition: (d: any) => d.position, fontFamily: "Monaco, monospace", getText: (d: any) => d[junctionText] ? (d[junctionText] as number).toFixed(3) : "", getSize: 12, getColor: [150, 150, 255], getAngle: 0, getTextAnchor: "middle", getAlignmentBaseline: "center", getPixelOffset: [0, -10], visible: showJunctionTextLayer && currentZoom >= 15 && currentZoom <= 24, extensions: [new CollisionFilterExtension()], collisionTestProps: { sizeScale: 2, // 增加碰撞检测的尺寸以提供更大间距 }, fontSettings: { sdf: true, fontSize: 64, // 字体图集大小,默认 64 buffer: 6, // 字符间距缓冲,默认 4 radius: 12, // SDF 半径,默认 12 cutoff: 0.25, // 控制字符粗细,默认 0.25 smoothing: 0.1, // 边缘平滑度,默认 0.1 }, outlineWidth: 10, outlineColor: [255, 255, 255, 255], }), new TextLayer({ id: "pipeTextLayer", zIndex: 10, data: showPipeText ? pipeData : [], getPosition: (d: any) => d.position, fontFamily: "Monaco, monospace", getText: (d: any) => d[pipeText] ? (d[pipeText] as number).toFixed(3) : "", getSize: 14, getColor: [120, 128, 181], getAngle: (d: any) => d.angle || 0, getPixelOffset: [0, -8], getTextAnchor: "middle", getAlignmentBaseline: "bottom", visible: showPipeTextLayer && currentZoom >= 15 && currentZoom <= 24, extensions: [new CollisionFilterExtension()], collisionTestProps: { sizeScale: 2, // 增加碰撞检测的尺寸以提供更大间距 }, fontSettings: { sdf: true, fontSize: 64, // 字体图集大小,默认 64 buffer: 6, // 字符间距缓冲,默认 4 radius: 12, // SDF 半径,默认 12 cutoff: 0.25, // 控制字符粗细,默认 0.25 smoothing: 0.1, // 边缘平滑度,默认 0.1 }, outlineWidth: 10, outlineColor: [255, 255, 255, 255], }), ]; deck.setProps({ layers: newLayers }); // 动画循环 const animate = () => { if (!deck || !flowAnimation.current) return; // 添加检查,防止空数据或停止旧循环 // 动画总时长(秒) if (pipeData.length === 0) { requestAnimationFrame(animate); return; } const animationDuration = 10; // 缓冲时间(秒) const bufferTime = 2; // 完整循环周期 const loopLength = animationDuration + bufferTime; // 确保时间范围与你的时间戳数据匹配 const currentTime = (Date.now() / 1000) % loopLength; // (0,12) 之间循环 // console.log("Current Time:", currentTime); const waterflowLayer = new TripsLayer({ id: "waterflowLayer", data: pipeData, getPath: (d) => (flowAnimation.current ? d.path : []), getTimestamps: (d) => { return d.timestamps; // 这些应该是与 currentTime 匹配的数值 }, getColor: [0, 220, 255], opacity: 0.8, visible: currentZoom >= 12 && currentZoom <= 24, widthMinPixels: 5, jointRounded: true, // 拐角变圆 // capRounded: true, // 端点变圆 trailLength: 2, // 水流尾迹淡出时间 currentTime: currentTime, }); // 获取当前除 waterflowLayer 之外的所有图层 const otherLayers = deck.props.layers.filter( (layer: any) => layer && layer.id !== "waterflowLayer" ); deck.setProps({ layers: [...otherLayers, waterflowLayer], }); // 继续请求动画帧,每帧执行一次函数 requestAnimationFrame(animate); }; animate(); }, [ flowAnimation, junctionData, pipeData, currentZoom, showJunctionText, showPipeText, showJunctionTextLayer, showPipeTextLayer, junctionText, pipeText, ]); useEffect(() => { if (pipeText === "flow") { flowAnimation.current = true; } else { flowAnimation.current = false; } }, [pipeText]); // 计算值更新时,更新 junctionData 和 pipeData useEffect(() => { const junctionProperties = junctionText; const pipeProperties = pipeText; // 将 nodeRecords 转换为 Map 以提高查找效率 const nodeMap: Map = new Map( currentJunctionCalData.map((r: any) => [r.ID, r]) ); // 将 linkRecords 转换为 Map 以提高查找效率 const linkMap: Map = new Map( currentPipeCalData.map((r: any) => [r.ID, r]) ); // 更新junctionData setJunctionDataState((prev: any[]) => prev.map((j) => { const record = nodeMap.get(j.id); if (record) { return { ...j, [junctionProperties]: record.value, }; } return j; }) ); // 更新pipeData setPipeDataState((prev: any[]) => prev.map((p) => { const record = linkMap.get(p.id); if (record) { return { ...p, flowFlag: pipeProperties === "flow" && record.value < 0 ? -1 : 1, path: pipeProperties === "flow" && record.value < 0 && p.flowFlag > 0 ? [...p.path].reverse() : p.path, [pipeProperties]: record.value, }; } return p; }) ); }, [currentJunctionCalData, currentPipeCalData]); return ( <>
{children}
); }; export default MapComponent;