diff --git a/src/app/OlMap/Controls/LayerControl.tsx b/src/app/OlMap/Controls/LayerControl.tsx index d8883ee..a669a9f 100644 --- a/src/app/OlMap/Controls/LayerControl.tsx +++ b/src/app/OlMap/Controls/LayerControl.tsx @@ -40,7 +40,7 @@ const LayerControl: React.FC = () => { return deckLayers.some((dl: any) => dl.id === "waterflowLayer"); } return false; - }) as Layer[]; + }) as DeckLayer[]; // 合并所有可控制的图层 const allLayers = [...mapLayers, ...deckFlowLayers]; @@ -55,6 +55,7 @@ const LayerControl: React.FC = () => { "valves", "scada", "waterflow", + "contourLayer", ]; // 过滤并排序图层:只显示在 layerOrder 中的图层 @@ -81,10 +82,18 @@ const LayerControl: React.FC = () => { 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); + // 对于 DeckLayer,需要设置内部 deck.gl 图层的可见性 + const deckLayers = layer.getDeckLayers(); + deckLayers.forEach((deckLayer: any) => { + if ( + deckLayer && + (deckLayer.id === "waterflowLayer" || + deckLayer.id === "contourLayer") + ) { + const visible = layer.getDeckLayerVisible(deckLayer.id); + layer.setDeckLayerVisible(deckLayer.id, !visible); + } + }); } else { // 对于普通 OpenLayers 图层 visible.set(layer, layer.getVisible()); diff --git a/src/app/OlMap/Controls/StyleEditorPanel.tsx b/src/app/OlMap/Controls/StyleEditorPanel.tsx index a55b852..e46d150 100644 --- a/src/app/OlMap/Controls/StyleEditorPanel.tsx +++ b/src/app/OlMap/Controls/StyleEditorPanel.tsx @@ -31,7 +31,7 @@ import { parseColor } from "@utils/parseColor"; import { VectorTile } from "ol"; import { useNotification } from "@refinedev/core"; import { config } from "@/config/config"; -import { constructNow, min } from "date-fns"; +import { DeckLayer } from "@utils/layers"; interface StyleConfig { property: string; @@ -190,6 +190,7 @@ const StyleEditorPanel: React.FC = ({ pipeText, setShowJunctionText, setShowPipeText, + setShowContourLayer, setJunctionText, setPipeText, } = data; @@ -443,6 +444,7 @@ const StyleEditorPanel: React.FC = ({ } if (junctionStyleConfigState) applyLayerStyle(junctionStyleConfigState, breaks); + updateContourLayerStyle(breaks, junctionStyleConfigState?.styleConfig); } else if ( layerType === "pipes" && currentPipeCalData && @@ -1426,6 +1428,110 @@ const StyleEditorPanel: React.FC = ({ ); } }; + // 更新 ContourLayer 的样式,并显示在地图上 + const updateContourLayerStyle = (breaks: any, styleConfig: any) => { + if (!map) return; + // 查找包含 contourLayer 的 DeckLayer + const deckLayerWrapper = map + .getLayers() + .getArray() + .find((layer) => { + if (layer instanceof DeckLayer) { + const deckLayers = layer.getDeckLayers(); + // 检查是否包含 contourLayer + return deckLayers.some((dl: any) => dl.id === "contourLayer"); + } + return false; + }) as DeckLayer | undefined; + + if (!deckLayerWrapper) return; + + // 计算颜色 + const segmentCount = breaks.length - 1; + if (segmentCount <= 0) return; + + const thresholdColor = () => { + let colors: string[] = []; + if (styleConfig.colorType === "single") { + const c = SINGLE_COLOR_PALETTES[styleConfig.singlePaletteIndex].color; + colors = Array(segmentCount).fill(c); + } else if (styleConfig.colorType === "gradient") { + const { start, end } = + GRADIENT_PALETTES[styleConfig.gradientPaletteIndex]; + const startColor = parseColor(start); + const endColor = parseColor(end); + for (let i = 0; i < segmentCount; i++) { + const ratio = segmentCount > 1 ? i / (segmentCount - 1) : 1; + const r = Math.round( + startColor.r + (endColor.r - startColor.r) * ratio + ); + const g = Math.round( + startColor.g + (endColor.g - startColor.g) * ratio + ); + const b = Math.round( + startColor.b + (endColor.b - startColor.b) * ratio + ); + colors.push(`rgba(${r}, ${g}, ${b}, 1)`); + } + } else if (styleConfig.colorType === "rainbow") { + const baseColors = + RAINBOW_PALETTES[styleConfig.rainbowPaletteIndex].colors; + colors = Array.from( + { length: segmentCount }, + (_, i) => baseColors[i % baseColors.length] + ); + } else if (styleConfig.colorType === "custom") { + const custom = styleConfig.customColors || []; + const result = [...custom]; + const reverseRainbowColors = RAINBOW_PALETTES[1].colors; + while (result.length < segmentCount) { + result.push( + reverseRainbowColors[ + (result.length - custom.length) % reverseRainbowColors.length + ] + ); + } + colors = result.slice(0, segmentCount); + } + return colors; + }; + + const colors = thresholdColor(); + // 构建 contours 配置 + const contours: any[] = []; + for (let i = 0; i < segmentCount; i++) { + const start = breaks[i]; + const end = breaks[i + 1]; + const colorStr = colors[i]; + try { + const c = parseColor(colorStr); + contours.push({ + threshold: [start, end], + color: [c.r, c.g, c.b], + }); + } catch (e) { + console.warn("Color parse error", colorStr); + } + } + // 更新 DeckLayer + const deck = (deckLayerWrapper as any).deck; + if (deck && deck.props && deck.props.layers) { + const currentLayers = deck.props.layers; + const newLayers = currentLayers.map((layer: any) => { + if (layer.id === "contourLayer") { + return layer.clone({ + contours: contours, + }); + } + return layer; + }); + console.log(newLayers); + deck.setProps({ layers: newLayers }); + } + + // 显示 contourLayer + // if (setShowContourLayer) setShowContourLayer(true); + }; return ( <> diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx index 5be0e20..a7334b0 100644 --- a/src/app/OlMap/Controls/Timeline.tsx +++ b/src/app/OlMap/Controls/Timeline.tsx @@ -241,13 +241,13 @@ const Timeline: React.FC = ({ // 播放控制 const handlePlay = useCallback(() => { if (!isPlaying) { - if (junctionText === "" && pipeText === "") { - open?.({ - type: "error", - message: "请至少设定并应用一个图层的样式。", - }); - // return; - } + // if (junctionText === "" && pipeText === "") { + // open?.({ + // type: "error", + // message: "请至少设定并应用一个图层的样式。", + // }); + // return; + // } setIsPlaying(true); intervalRef.current = setInterval(() => { @@ -367,13 +367,13 @@ const Timeline: React.FC = ({ // 检查至少一个属性有值 const junctionProperties = junctionText; const pipeProperties = pipeText; - if (junctionProperties === "" && pipeProperties === "") { - open?.({ - type: "error", - message: "请至少设定并应用一个图层的样式。", - }); - return; - } + // if (junctionProperties === "" && pipeProperties === "") { + // open?.({ + // type: "error", + // message: "请至少设定并应用一个图层的样式。", + // }); + // return; + // } fetchFrameData( currentTimeToDate(selectedDate, currentTime), junctionText, diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx index defed16..0830730 100644 --- a/src/app/OlMap/MapComponent.tsx +++ b/src/app/OlMap/MapComponent.tsx @@ -49,6 +49,7 @@ interface DataContextType { showPipeText?: boolean; // 是否显示管道文本 setShowJunctionText?: React.Dispatch>; setShowPipeText?: React.Dispatch>; + setShowContourLayer?: React.Dispatch>; junctionText: string; pipeText: string; setJunctionText?: React.Dispatch>; @@ -86,7 +87,7 @@ export const useData = () => { const MapComponent: React.FC = ({ children }) => { const mapRef = useRef(null); const deckRef = useRef(null); - const deckFlowRef = useRef(null); + const deckLayerRef = useRef(null); const [map, setMap] = useState(); // currentCalData 用于存储当前计算结果 @@ -111,10 +112,10 @@ const MapComponent: React.FC = ({ children }) => { const [showPipeText, setShowPipeText] = useState(false); // 控制管道文本显示 const [showJunctionTextLayer, setShowJunctionTextLayer] = useState(true); // 控制节点文本图层显示 const [showPipeTextLayer, setShowPipeTextLayer] = useState(true); // 控制管道文本图层显示 + const [showContourLayer, setShowContourLayer] = useState(true); // 控制等高线图层显示 const [junctionText, setJunctionText] = useState("pressure"); const [pipeText, setPipeText] = useState("flow"); const flowAnimation = useRef(false); // 添加动画控制标志 - const waterflowUserVisible = useRef(true); // 用户设置的水流图层可见性 const [currentZoom, setCurrentZoom] = useState(11); // 当前缩放级别 // 防抖更新函数 @@ -571,6 +572,7 @@ const MapComponent: React.FC = ({ children }) => { controls: [], }); setMap(map); + // 恢复上次视图;如果没有则适配 MAP_EXTENT try { const stored = localStorage.getItem(MAP_VIEW_STORAGE_KEY); @@ -650,37 +652,13 @@ const MapComponent: React.FC = ({ children }) => { layers: [], }); deckRef.current = deck; - const deckLayer = new DeckLayer(deck); - // deckLayer.setZIndex(1000); // 确保在最上层 + const deckLayer = new DeckLayer(deck, { + name: "deckLayer", + value: "deckLayer", + }); + deckLayerRef.current = deckLayer; 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, { - name: "水流动画", - value: "waterflow", - 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); @@ -688,85 +666,119 @@ const MapComponent: React.FC = ({ children }) => { map.setTarget(undefined); map.dispose(); deck.finalize(); - deckFlow.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: 18, - fontWeight: "bold", - getColor: [0, 0, 0], - getAngle: 0, - getTextAnchor: "middle", - getAlignmentBaseline: "center", - getPixelOffset: [0, -10], - visible: - showJunctionTextLayer && currentZoom >= 15 && currentZoom <= 24, - extensions: [new CollisionFilterExtension()], - collisionTestProps: { - sizeScale: 3, // 增加碰撞检测的尺寸以提供更大间距 - }, - // 可读性设置 - characterSet: "auto", - fontSettings: { - sdf: true, - fontSize: 64, - buffer: 6, - }, - // outlineWidth: 10, - // outlineColor: [242, 244, 246, 255], - }), - new TextLayer({ - id: "pipeTextLayer", - zIndex: 10, - data: showPipeText ? pipeData : [], - getPosition: (d: any) => d.position, - fontFamily: "Monaco, monospace", - getText: (d: any) => - d[pipeText] ? Math.abs(d[pipeText] as number).toFixed(3) : "", - getSize: 18, - fontWeight: "bold", - getColor: [0, 0, 0], - getAngle: (d: any) => d.angle || 0, - getPixelOffset: [0, -8], - getTextAnchor: "middle", - getAlignmentBaseline: "bottom", - visible: showPipeTextLayer && currentZoom >= 15 && currentZoom <= 24, - extensions: [new CollisionFilterExtension()], - collisionTestProps: { - sizeScale: 3, // 增加碰撞检测的尺寸以提供更大间距 - }, - // 可读性设置 - characterSet: "auto", - fontSettings: { - sdf: true, - fontSize: 64, - buffer: 6, - }, - // outlineWidth: 10, - // outlineColor: [242, 244, 246, 255], - }), - ]; - deck.setProps({ layers: newLayers }); + const deckLayer = deckLayerRef.current; + if (!deckLayer) return; // 如果 deck 实例还未创建,则退出 + if (!junctionData.length) return; + if (!pipeData.length) return; + console.log(pipeData); + console.log(pipeText); + const junctionTextLayer = 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: 18, + fontWeight: "bold", + getColor: [0, 0, 0], + getAngle: 0, + getTextAnchor: "middle", + getAlignmentBaseline: "center", + getPixelOffset: [0, -10], + visible: showJunctionTextLayer && currentZoom >= 15 && currentZoom <= 24, + extensions: [new CollisionFilterExtension()], + collisionTestProps: { + sizeScale: 3, // 增加碰撞检测的尺寸以提供更大间距 + }, + // 可读性设置 + characterSet: "auto", + fontSettings: { + sdf: true, + fontSize: 64, + buffer: 6, + }, + // outlineWidth: 10, + // outlineColor: [242, 244, 246, 255], + }); + const pipeTextLayer = new TextLayer({ + id: "pipeTextLayer", + zIndex: 10, + data: showPipeText ? pipeData : [], + getPosition: (d: any) => d.position, + fontFamily: "Monaco, monospace", + getText: (d: any) => + d[pipeText] ? Math.abs(d[pipeText] as number).toFixed(3) : "", + getSize: 18, + fontWeight: "bold", + getColor: [0, 0, 0], + getAngle: (d: any) => d.angle || 0, + getPixelOffset: [0, -8], + getTextAnchor: "middle", + getAlignmentBaseline: "bottom", + visible: showPipeTextLayer && currentZoom >= 15 && currentZoom <= 24, + extensions: [new CollisionFilterExtension()], + collisionTestProps: { + sizeScale: 3, // 增加碰撞检测的尺寸以提供更大间距 + }, + // 可读性设置 + characterSet: "auto", + fontSettings: { + sdf: true, + fontSize: 64, + buffer: 6, + }, + // outlineWidth: 10, + // outlineColor: [242, 244, 246, 255], + }); + if (deckLayer.getDeckLayerById("junctionTextLayer")) { + // 传入完整 layer 实例以保证 clone/替换时保留 layer 类型和方法 + deckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer); + } else { + deckLayer.addDeckLayer(junctionTextLayer); + } + if (deckLayer.getDeckLayerById("pipeTextLayer")) { + deckLayer.updateDeckLayer("pipeTextLayer", pipeTextLayer); + } else { + deckLayer.addDeckLayer(pipeTextLayer); + } + console.log(deckLayer.getDeckLayers()); + }, [ + junctionData, + pipeData, + currentZoom, + showJunctionText, + showPipeText, + showJunctionTextLayer, + showPipeTextLayer, + showContourLayer, + junctionText, + pipeText, + ]); + // 控制流动动画开关 + useEffect(() => { + if (pipeText === "flow" && currentPipeCalData.length > 0) { + flowAnimation.current = true; + } else { + flowAnimation.current = false; + } + const deckLayer = deckLayerRef.current; + if (!deckLayer) return; // 如果 deck 实例还未创建,则退出 + + let animationFrameId: number; // 保存 requestAnimationFrame 的 ID // 动画循环 const animate = () => { - if (!deck || !flowAnimation.current) return; // 添加检查,防止空数据或停止旧循环 + if (!deckRef.current || !flowAnimation.current) return; // 添加检查,防止空数据或停止旧循环 // 动画总时长(秒) if (pipeData.length === 0) { - requestAnimationFrame(animate); + animationFrameId = requestAnimationFrame(animate); return; } const animationDuration = 10; @@ -787,119 +799,31 @@ const MapComponent: React.FC = ({ children }) => { getColor: [0, 220, 255], opacity: 0.8, visible: - waterflowUserVisible.current && - flowAnimation.current && - currentZoom >= 12 && - currentZoom <= 24, + flowAnimation.current && 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], - }); - + if (deckLayer.getDeckLayerById("waterflowLayer")) { + deckLayer.updateDeckLayer("waterflowLayer", waterflowLayer); + } else { + deckLayer.addDeckLayer(waterflowLayer); + } // 继续请求动画帧,每帧执行一次函数 - requestAnimationFrame(animate); - }; - animate(); - }, [ - flowAnimation, - junctionData, - pipeData, - currentZoom, - showJunctionText, - showPipeText, - showJunctionTextLayer, - showPipeTextLayer, - junctionText, - pipeText, - ]); - // 控制流动动画开关 - useEffect(() => { - if (pipeText === "flow" && currentPipeCalData.length > 0) { - flowAnimation.current = true; - } else { - 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); + animationFrameId = requestAnimationFrame(animate); }; animate(); - // 清理函数 + // 清理函数:取消动画帧 return () => { - if (animationId) { - cancelAnimationFrame(animationId); - } - if (deckFlow) { - deckFlow.setProps({ layers: [] }); + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); } }; - }, [flowAnimation, pipeData, currentZoom]); + }, [currentZoom, currentPipeCalData, pipeText, pipeData.length]); + // 计算值更新时,更新 junctionData 和 pipeData useEffect(() => { const junctionProperties = junctionText; @@ -965,6 +889,7 @@ const MapComponent: React.FC = ({ children }) => { setCurrentPipeCalData, setShowJunctionText, setShowPipeText, + setShowContourLayer, setJunctionText, setPipeText, showJunctionText, @@ -980,7 +905,6 @@ const MapComponent: React.FC = ({ children }) => { {children} - diff --git a/src/utils/layers.ts b/src/utils/layers.ts index 23dec82..d28670c 100644 --- a/src/utils/layers.ts +++ b/src/utils/layers.ts @@ -67,6 +67,14 @@ export class DeckLayer extends Layer { // 添加图层 addDeckLayer(layer: any): void { const currentLayers = this.getDeckLayers(); + // 如果已有同 id 图层,则替换保持顺序;否则追加 + const idx = currentLayers.findIndex((l: any) => l && l.id === layer.id); + if (idx >= 0) { + const copy = [...currentLayers]; + copy[idx] = layer; + this.deck.setProps({ layers: copy }); + return; + } this.deck.setProps({ layers: [...currentLayers, layer] }); } @@ -85,15 +93,41 @@ export class DeckLayer extends Layer { return layers.find((layer: any) => layer && layer.id === layerId); } - // 更新特定图层 - updateDeckLayer(layerId: string, props: any): void { + // 更新特定图层:支持传入一个新的 Layer 实例或一个 props 对象 + // - 如果传入的是 Layer 实例,则直接替换同 id 的图层为该实例 + // - 如果传入的是 props(普通对象),则基于原图层调用 clone(props) + updateDeckLayer(layerId: string, layerOrProps: any): void { const layers = this.getDeckLayers(); const updatedLayers = layers.map((layer: any) => { - if (layer && layer.id === layerId) { - return layer.clone(props); + if (!layer || layer.id !== layerId) return layer; + + // 如果传入的是一个 deck.gl Layer 实例(通常包含 id 和 props) + if ( + layerOrProps && + typeof layerOrProps === "object" && + layerOrProps.id !== undefined && + typeof layerOrProps.clone === "function" + ) { + // 替换为新的 layer 实例 + return layerOrProps; + } + + // 否则假定传入的是 props 对象,使用现有 layer.clone(props) 创建新实例 + try { + return layer.clone(layerOrProps); + } catch (err) { + // 如果 clone 失败,作为降级策略尝试手动复制 props 到新对象(保留原 layer) + // 这通常不应该发生,但保证不会抛出而破坏整个 layers 列表 + const newLayer = layer.clone + ? layer.clone(layerOrProps) + : { + ...layer, + props: { ...(layer.props || {}), ...(layerOrProps || {}) }, + }; + return newLayer; } - return layer; }); + this.deck.setProps({ layers: updatedLayers }); } @@ -113,7 +147,16 @@ export class DeckLayer extends Layer { // 存储用户设置的可见性 this.userVisibility.set(layerId, visible); // 更新图层(注意:实际的 visible 可能还受其他条件控制) - this.updateDeckLayer(layerId, { visible }); + // 优先尝试使用传入的 layer 实例替换,否则使用 clone({ visible }) 来保留图层类型 + const found = this.getDeckLayerById(layerId); + if (!found) return; + try { + // 使用 clone 来确保保持同类型实例 + this.updateDeckLayer(layerId, { ...found.props, visible }); + } catch (err) { + // 降级:直接替换属性 + this.updateDeckLayer(layerId, { visible }); + } // 触发回调通知外部 if (this.onVisibilityChange) { this.onVisibilityChange(layerId, visible);