diff --git a/src/app/OlMap/Controls/DrawPanel.tsx b/src/app/OlMap/Controls/DrawPanel.tsx index 98c2375..6b52468 100644 --- a/src/app/OlMap/Controls/DrawPanel.tsx +++ b/src/app/OlMap/Controls/DrawPanel.tsx @@ -65,6 +65,12 @@ const DrawPanel: React.FC = () => { }), }), }), + properties: { + name: "绘制图层", // 设置图层名称 + value: "drawLayer", + type: "multigeometry", + properties: [], + }, }); map.addLayer(drawVectorLayer); diff --git a/src/app/OlMap/Controls/LayerControl.tsx b/src/app/OlMap/Controls/LayerControl.tsx index bc3c265..d8883ee 100644 --- a/src/app/OlMap/Controls/LayerControl.tsx +++ b/src/app/OlMap/Controls/LayerControl.tsx @@ -5,6 +5,7 @@ import { Checkbox, FormControlLabel } from "@mui/material"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; import VectorLayer from "ol/layer/Vector"; import VectorTileLayer from "ol/layer/VectorTile"; +import { DeckLayer } from "@utils/layers"; const LayerControl: React.FC = () => { const map = useMap(); @@ -12,56 +13,126 @@ const LayerControl: React.FC = () => { const [layerVisibilities, setLayerVisibilities] = useState< Map >(new Map()); + const userChangedRef = React.useRef>(new Set()); useEffect(() => { if (!map) return; - const mapLayers = map - .getLayers() - .getArray() - .filter( - (layer) => - layer instanceof WebGLVectorTileLayer || - layer instanceof VectorTileLayer || - layer instanceof VectorLayer - ) as Layer[]; - // 定义图层排序顺序 - const layerOrder = [ - "junctions", - "reservoirs", - "tanks", - "pipes", - "pumps", - "valves", - "scada", - ]; + const updateLayers = () => { + const mapLayers = map + .getLayers() + .getArray() + .filter( + (layer) => + layer instanceof WebGLVectorTileLayer || + layer instanceof VectorTileLayer || + layer instanceof VectorLayer + ) as Layer[]; - // 对图层进行排序 - const sortedLayers = mapLayers.sort((a, b) => { - const nameA = a.get("value")?.toLowerCase() || ""; - const nameB = b.get("value")?.toLowerCase() || ""; - const indexA = layerOrder.indexOf(nameA); - const indexB = layerOrder.indexOf(nameB); + // 查找包含 waterflowLayer 的 DeckLayer + const deckFlowLayers = map + .getLayers() + .getArray() + .filter((layer) => { + if (layer instanceof DeckLayer) { + const deckLayers = layer.getDeckLayers(); + // 检查是否包含 waterflowLayer + return deckLayers.some((dl: any) => dl.id === "waterflowLayer"); + } + return false; + }) as Layer[]; - // 如果图层不在排序列表中,放到最后 - if (indexA === -1 && indexB === -1) return 0; - if (indexA === -1) return 1; - if (indexB === -1) return -1; + // 合并所有可控制的图层 + const allLayers = [...mapLayers, ...deckFlowLayers]; - return indexA - indexB; - }); + // 定义图层排序顺序 + const layerOrder = [ + "junctions", + "reservoirs", + "tanks", + "pipes", + "pumps", + "valves", + "scada", + "waterflow", + ]; - setLayers(sortedLayers); - const visible = new Map(); - sortedLayers.forEach((layer) => { - visible.set(layer, layer.getVisible()); - }); - setLayerVisibilities(visible); + // 过滤并排序图层:只显示在 layerOrder 中的图层 + const sortedLayers = allLayers + .filter((layer) => { + const value = layer.get("value")?.toLowerCase(); + return layerOrder.includes(value); + }) + .sort((a, b) => { + const nameA = a.get("value")?.toLowerCase(); + const nameB = b.get("value")?.toLowerCase(); + const indexA = layerOrder.indexOf(nameA); + const indexB = layerOrder.indexOf(nameB); + + return indexA - indexB; + }); + + setLayers(sortedLayers); + + setLayerVisibilities((prevVisibilities) => { + const visible = new Map(); + sortedLayers.forEach((layer) => { + // 如果用户刚手动改变了这个图层,使用本地状态 + if (userChangedRef.current.has(layer)) { + visible.set(layer, prevVisibilities.get(layer) ?? true); + } else if (layer instanceof DeckLayer) { + // 对于 DeckLayer,获取内部 deck.gl 图层的可见性 + const waterflowVisible = + layer.getDeckLayerVisible("waterflowLayer"); + visible.set(layer, waterflowVisible ?? true); + } else { + // 对于普通 OpenLayers 图层 + visible.set(layer, layer.getVisible()); + } + }); + return visible; + }); + }; + + // 初始更新 + updateLayers(); + + // 监听图层集合的变化 + const layerCollection = map.getLayers(); + layerCollection.on("change:length", updateLayers); + + // 设置定时器定期检查 DeckLayer 内部图层的变化 + const intervalId = setInterval(updateLayers, 500); + + return () => { + layerCollection.un("change:length", updateLayers); + clearInterval(intervalId); + }; }, [map]); const handleVisibilityChange = (layer: Layer, visible: boolean) => { - layer.setVisible(visible); + // 判断图层类型并调用相应的方法 + if (layer instanceof DeckLayer) { + // 对于 DeckLayer,需要设置内部 deck.gl 图层的可见性 + const deckLayers = layer.getDeckLayers(); + deckLayers.forEach((deckLayer: any) => { + if (deckLayer && deckLayer.id === "waterflowLayer") { + layer.setDeckLayerVisible("waterflowLayer", visible); + } + }); + } else { + // 对于普通 OpenLayers 图层,使用 setVisible 方法 + layer.setVisible(visible); + } + + // 标记这个图层为用户手动改变 + userChangedRef.current.add(layer); setLayerVisibilities((prev) => new Map(prev).set(layer, visible)); + + // 1秒后移除标记,允许定时器更新 + setTimeout(() => { + userChangedRef.current.delete(layer); + }, 1000); }; return ( diff --git a/src/app/OlMap/Controls/Toolbar.tsx b/src/app/OlMap/Controls/Toolbar.tsx index e2572f1..eca99db 100644 --- a/src/app/OlMap/Controls/Toolbar.tsx +++ b/src/app/OlMap/Controls/Toolbar.tsx @@ -142,6 +142,12 @@ const Toolbar: React.FC = ({ hiddenButtons, queryType }) => { }), }), }), + properties: { + name: "属性查询高亮图层", // 设置图层名称 + value: "info_highlight_layer", + type: "multigeometry", + properties: [], + }, }); map.addLayer(highLightLayer); diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx index 448a972..4b2fb4b 100644 --- a/src/app/OlMap/MapComponent.tsx +++ b/src/app/OlMap/MapComponent.tsx @@ -12,7 +12,8 @@ import View from "ol/View.js"; import "ol/ol.css"; import MapTools from "./MapTools"; -import { Layer } from "ol/layer"; // 保留导入,但用于继承 +// 导入 DeckLayer +import { DeckLayer } from "@utils/layers"; import VectorTileSource from "ol/source/VectorTile"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; import MVT from "ol/format/MVT"; @@ -55,28 +56,6 @@ interface DataContextType { scadaData?: any[]; // SCADA 数据 } -// 创建自定义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); @@ -107,6 +86,7 @@ export const useData = () => { const MapComponent: React.FC = ({ children }) => { const mapRef = useRef(null); const deckRef = useRef(null); + const deckFlowRef = useRef(null); const [map, setMap] = useState(); // currentCalData 用于存储当前计算结果 @@ -134,6 +114,7 @@ const MapComponent: React.FC = ({ children }) => { const [junctionText, setJunctionText] = useState("pressure"); const [pipeText, setPipeText] = useState("flow"); const flowAnimation = useRef(false); // 添加动画控制标志 + const waterflowUserVisible = useRef(true); // 用户设置的水流图层可见性 const [currentZoom, setCurrentZoom] = useState(11); // 当前缩放级别 // 防抖更新函数 @@ -648,6 +629,15 @@ const MapComponent: React.FC = ({ children }) => { }, 0); }; map.getView().on("change", handleViewChange); + + // 初始化当前缩放级别并强制触发瓦片加载 + setTimeout(() => { + const initialZoom = map.getView().getZoom() || 11; + setCurrentZoom(initialZoom); + // 强制触发地图渲染,让瓦片加载事件触发 + map.render(); + }, 100); + // 初始化 deck.gl const deck = new Deck({ initialViewState: { @@ -664,6 +654,32 @@ const MapComponent: React.FC = ({ children }) => { // deckLayer.setZIndex(1000); // 确保在最上层 map.addLayer(deckLayer); + // 初始化水流动画的 deck.gl + const deckFlow = new Deck({ + initialViewState: { + longitude: 0, + latitude: 0, + zoom: 1, + }, + canvas: "deck-flow-canvas", + controller: false, + layers: [], + }); + deckFlowRef.current = deckFlow; + const deckFlowLayer = new DeckLayer(deckFlow); + deckFlowLayer.set("name", "水流动画"); + deckFlowLayer.set("value", "waterflow"); + deckFlowLayer.set("type", "animation"); + // 初始化用户可见性状态(默认为 true) + deckFlowLayer.initUserVisibility("waterflowLayer", true); + // 设置可见性变化回调,同步更新 waterflowUserVisible + deckFlowLayer.setVisibilityChangeCallback((layerId, visible) => { + if (layerId === "waterflowLayer") { + waterflowUserVisible.current = visible; + } + }); + map.addLayer(deckFlowLayer); + // 清理函数 return () => { junctionsLayer.un("change:visible", handleJunctionVisibilityChange); @@ -671,6 +687,7 @@ const MapComponent: React.FC = ({ children }) => { map.setTarget(undefined); map.dispose(); deck.finalize(); + deckFlow.finalize(); }; }, []); @@ -769,7 +786,10 @@ const MapComponent: React.FC = ({ children }) => { getColor: [0, 220, 255], opacity: 0.8, visible: - flowAnimation.current && currentZoom >= 12 && currentZoom <= 24, + waterflowUserVisible.current && + flowAnimation.current && + currentZoom >= 12 && + currentZoom <= 24, widthMinPixels: 5, jointRounded: true, // 拐角变圆 // capRounded: true, // 端点变圆 @@ -810,6 +830,75 @@ const MapComponent: React.FC = ({ children }) => { flowAnimation.current = false; } }, [currentPipeCalData, pipeText]); + + // 水流动画循环 + useEffect(() => { + const deckFlow = deckFlowRef.current; + if (!deckFlow || !flowAnimation.current || pipeData.length === 0) { + // 如果不需要动画,清空图层 + if (deckFlow) { + deckFlow.setProps({ layers: [] }); + } + return; + } + + let animationId: number; + const animate = () => { + if (!deckFlow || !flowAnimation.current) return; + if (pipeData.length === 0) { + animationId = requestAnimationFrame(animate); + return; + } + + // 动画总时长(秒) + const animationDuration = 10; + // 缓冲时间(秒) + const bufferTime = 2; + // 完整循环周期 + const loopLength = animationDuration + bufferTime; + // 确保时间范围与你的时间戳数据匹配 + const currentTime = (Date.now() / 1000) % loopLength; // (0,12) 之间循环 + + const waterflowLayer = new TripsLayer({ + id: "waterflowLayer", + data: pipeData, + getPath: (d) => d.path, + getTimestamps: (d) => { + return d.timestamps; // 这些应该是与 currentTime 匹配的数值 + }, + getColor: [0, 220, 255], + opacity: 0.8, + visible: + waterflowUserVisible.current && + flowAnimation.current && + currentZoom >= 12 && + currentZoom <= 24, + widthMinPixels: 5, + jointRounded: true, // 拐角变圆 + // capRounded: true, // 端点变圆 + trailLength: 2, // 水流尾迹淡出时间 + currentTime: currentTime, + }); + + deckFlow.setProps({ + layers: [waterflowLayer], + }); + + // 继续请求动画帧,每帧执行一次函数 + animationId = requestAnimationFrame(animate); + }; + animate(); + + // 清理函数 + return () => { + if (animationId) { + cancelAnimationFrame(animationId); + } + if (deckFlow) { + deckFlow.setProps({ layers: [] }); + } + }; + }, [flowAnimation, pipeData, currentZoom]); // 计算值更新时,更新 junctionData 和 pipeData useEffect(() => { const junctionProperties = junctionText; @@ -890,6 +979,7 @@ const MapComponent: React.FC = ({ children }) => { {children} + diff --git a/src/utils/layers.ts b/src/utils/layers.ts new file mode 100644 index 0000000..a3a7d3f --- /dev/null +++ b/src/utils/layers.ts @@ -0,0 +1,121 @@ +import { Layer } from "ol/layer"; +import { Deck } from "@deck.gl/core"; +import { toLonLat } from "ol/proj"; + +/** + * 自定义 Layer 类来包装 deck.gl + * 将 deck.gl 图层集成到 OpenLayers 地图中 + */ +export class DeckLayer extends Layer { + private deck: Deck; + private onVisibilityChange?: (layerId: string, visible: boolean) => void; + private userVisibility: Map = new Map(); // 存储用户设置的可见性 + + constructor(deckInstance: Deck) { + super({}); + this.deck = deckInstance; + } + + // 设置可见性变化回调 + setVisibilityChangeCallback( + callback: (layerId: string, visible: boolean) => void + ): void { + this.onVisibilityChange = callback; + } + + // 初始化用户可见性状态 + initUserVisibility(layerId: string, visible: boolean): void { + this.userVisibility.set(layerId, visible); + } + + 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(); + return document.getElementById("deck-canvas") as HTMLElement; + } + + // 获取 Deck 实例 + getDeck(): Deck { + return this.deck; + } + + // 设置图层 + setDeckLayers(layers: any[]): void { + this.deck.setProps({ layers }); + } + + // 获取当前图层 + getDeckLayers(): any[] { + return this.deck.props.layers || []; + } + + // 添加图层 + addDeckLayer(layer: any): void { + const currentLayers = this.getDeckLayers(); + this.deck.setProps({ layers: [...currentLayers, layer] }); + } + + // 移除图层 + removeDeckLayer(layerId: string): void { + const currentLayers = this.getDeckLayers(); + const filteredLayers = currentLayers.filter( + (layer: any) => layer && layer.id !== layerId + ); + this.deck.setProps({ layers: filteredLayers }); + } + + // 根据 ID 查找图层 + getDeckLayerById(layerId: string): any | undefined { + const layers = this.getDeckLayers(); + return layers.find((layer: any) => layer && layer.id === layerId); + } + + // 更新特定图层 + updateDeckLayer(layerId: string, props: any): void { + const layers = this.getDeckLayers(); + const updatedLayers = layers.map((layer: any) => { + if (layer && layer.id === layerId) { + return layer.clone(props); + } + return layer; + }); + this.deck.setProps({ layers: updatedLayers }); + } + + // 获取图层的可见性(用户设置的状态,不受其他条件影响) + getDeckLayerVisible(layerId: string): boolean | undefined { + // 优先返回用户设置的可见性 + if (this.userVisibility.has(layerId)) { + return this.userVisibility.get(layerId); + } + // 如果没有用户设置,返回实际图层的可见性 + const layer = this.getDeckLayerById(layerId); + return layer ? layer.props.visible : undefined; + } + + // 设置图层的可见性 + setDeckLayerVisible(layerId: string, visible: boolean): void { + // 存储用户设置的可见性 + this.userVisibility.set(layerId, visible); + // 更新图层(注意:实际的 visible 可能还受其他条件控制) + this.updateDeckLayer(layerId, { visible }); + // 触发回调通知外部 + if (this.onVisibilityChange) { + this.onVisibilityChange(layerId, visible); + } + } + + // 切换图层的可见性 + toggleDeckLayerVisible(layerId: string): void { + const currentVisible = this.getDeckLayerVisible(layerId); + if (currentVisible !== undefined) { + this.setDeckLayerVisible(layerId, !currentVisible); + } + } +}