diff --git a/src/app/OlMap/Controls/BaseLayers.tsx b/src/app/OlMap/Controls/BaseLayers.tsx index 393d936..6dfc9e6 100644 --- a/src/app/OlMap/Controls/BaseLayers.tsx +++ b/src/app/OlMap/Controls/BaseLayers.tsx @@ -214,7 +214,7 @@ const BaseLayers: React.FC = () => { {isExpanded && (
{ @@ -104,8 +108,10 @@ const StyleEditorPanel: React.FC = () => { return
Loading...
; // 或其他占位符 } const { - junctionData, - pipeData, + currentJunctionCalData, + currentPipeCalData, + junctionText, + pipeText, setShowJunctionText, setShowPipeText, setJunctionText, @@ -114,6 +120,8 @@ const StyleEditorPanel: React.FC = () => { const [applyJunctionStyle, setApplyJunctionStyle] = useState(false); const [applyPipeStyle, setApplyPipeStyle] = useState(false); + const [styleUpdateTrigger, setStyleUpdateTrigger] = useState(0); // 用于触发样式更新的状态 + const prevStyleUpdateTriggerRef = useRef(0); const [renderLayers, setRenderLayers] = useState([]); const [selectedRenderLayer, setSelectedRenderLayer] = @@ -131,7 +139,7 @@ const StyleEditorPanel: React.FC = () => { classificationMethod: "pretty_breaks", segments: 5, minSize: 4, - maxSize: 15, + maxSize: 12, minStrokeWidth: 2, maxStrokeWidth: 6, fixedStrokeWidth: 3, @@ -142,17 +150,6 @@ const StyleEditorPanel: React.FC = () => { opacity: 0.9, adjustWidthByProperty: true, }); - const [legendStyleConfig, setLegendStyleConfig] = useState( - { - layerName: "", - layerId: "", - property: "", - colors: [], - type: "point", - dimensions: [], - breaks: [], - } - ); // 样式状态管理 - 存储多个图层的样式状态 const [layerStyleStates, setLayerStyleStates] = useState( [] @@ -185,41 +182,141 @@ const StyleEditorPanel: React.FC = () => { }, [gradientPaletteIndex, parseColor] ); - // 应用分类样式 - const setStyleState = (layer: any) => { - if ( - layer.get("value") !== undefined && - styleConfig.property !== undefined - ) { + // 保存当前图层的样式状态 + const saveLayerStyle = useCallback( + (layerId?: string, legendConfig?: LegendStyleConfig) => { + if (!selectedRenderLayer || !styleConfig.property) { + console.warn("无法保存样式:缺少必要的图层或样式配置"); + return; + } + if (!layerId) return; // 如果没有传入 layerId,则不保存 + const layerName = selectedRenderLayer.get("name") || `图层${layerId}`; + // 如果没有传入图例配置,则创建一个默认的空配置 + const finalLegendConfig: LegendStyleConfig = legendConfig || { + layerId, + layerName, + property: styleConfig.property, + colors: [], + type: "point", + dimensions: [], + breaks: [], + }; + const newStyleState: LayerStyleState = { + layerId, + layerName, + styleConfig: { ...styleConfig }, + legendConfig: { ...finalLegendConfig }, + isActive: true, + }; + + setLayerStyleStates((prev) => { + // 检查是否已存在该图层的样式状态 + const existingIndex = prev.findIndex( + (state) => state.layerId === layerId + ); + + if (existingIndex !== -1) { + // 更新已存在的状态 + const updated = [...prev]; + updated[existingIndex] = newStyleState; + return updated; + } else { + // 添加新的状态 + return [...prev, newStyleState]; + } + }); + }, + [selectedRenderLayer, styleConfig] + ); + // 设置分类样式参数,触发样式应用 + const setStyleState = () => { + if (!selectedRenderLayer) return; + const layerId = selectedRenderLayer.get("value"); + const property = styleConfig.property; + if (layerId !== undefined && property !== undefined) { // 更新文字标签设置 - if (layer.get("value") === "junctions") { + if (layerId === "junctions") { if (setJunctionText && setShowJunctionText) { - setJunctionText(styleConfig.property); + setJunctionText(property); setShowJunctionText(styleConfig.showLabels); setApplyJunctionStyle(true); + saveLayerStyle(layerId); } } - if (layer.get("value") === "pipes") { + if (layerId === "pipes") { + console.log(styleConfig); if (setPipeText && setShowPipeText) { - setPipeText(styleConfig.property); + setPipeText(property); setShowPipeText(styleConfig.showLabels); setApplyPipeStyle(true); + saveLayerStyle(layerId); } } + // 触发样式更新 + setStyleUpdateTrigger((prev) => prev + 1); } }; - const applyStyle = (layerId: string, breaks?: number[]) => { + // 计算分类样式,并应用到对应图层 + const applyClassificationStyle = ( + layerType: "junctions" | "pipes", + styleConfig: any + ) => { + if ( + layerType === "junctions" && + currentJunctionCalData && + currentJunctionCalData.length > 0 + ) { + // 应用节点样式 + let junctionStyleConfigState = layerStyleStates.find( + (s) => s.layerId === "junctions" + ); + + // 更新节点数据属性 + const segments = junctionStyleConfigState?.styleConfig.segments ?? 5; + const breaks = calculateClassification( + currentJunctionCalData.map((d) => d.value), + segments, + styleConfig.classificationMethod + ); + if (breaks.length === 0) { + console.warn("计算的 breaks 为空,无法应用样式"); + return; + } + if (junctionStyleConfigState) + applyLayerStyle(junctionStyleConfigState, breaks); + } else if ( + layerType === "pipes" && + currentPipeCalData && + currentPipeCalData.length > 0 + ) { + // 应用管道样式 + let pipeStyleConfigState = layerStyleStates.find( + (s) => s.layerId === "pipes" + ); + // 更新管道数据属性 + const segments = pipeStyleConfigState?.styleConfig.segments ?? 5; + const breaks = calculateClassification( + currentPipeCalData.map((d) => d.value), + segments, + styleConfig.classificationMethod + ); + if (pipeStyleConfigState) applyLayerStyle(pipeStyleConfigState, breaks); + } + }; + // 应用样式函数,传入 breaks 数据 + const applyLayerStyle = ( + layerStyleConfig: LayerStyleState, + breaks?: number[] + ) => { // 使用传入的 breaks 数据 if (!breaks || breaks.length === 0) { console.warn("没有有效的 breaks 数据"); return; } - const styleConfig = layerStyleStates.find( - (s) => s.layerId === layerId - )?.styleConfig; - const selectedRenderLayer = renderLayers.find( - (l) => l.get("id") === layerId - ); + const styleConfig = layerStyleConfig.styleConfig; + const selectedRenderLayer = renderLayers.filter((layer) => { + return layer.get("value") === layerStyleConfig.layerId; + })[0]; if (!selectedRenderLayer || !styleConfig?.property) return; const layerType: string = selectedRenderLayer?.get("type"); const source = selectedRenderLayer.getSource(); @@ -257,26 +354,14 @@ const StyleEditorPanel: React.FC = () => { (styleConfig.maxSize - styleConfig.minSize) * ratio ); }); - // 创建图例配置对象 - const legendConfig: LegendStyleConfig = { - layerName: selectedRenderLayer.get("name"), - layerId: selectedRenderLayer.get("value"), - property: selectedProperty.name, - colors: colors, - type: layerType, - dimensions: dimensions, - breaks: breaks, - }; - // 更新图例配置 - setLegendStyleConfig(legendConfig); // 动态生成颜色条件表达式 - const generateColorConditions = (): any[] => { + const generateColorConditions = (property: string): any[] => { const conditions: any[] = ["case"]; for (let i = 0; i < breaks.length; i++) { // 添加条件:属性值 <= 当前断点 - conditions.push(["<=", ["get", styleConfig.property], breaks[i]]); + conditions.push(["<=", ["get", property], breaks[i]]); // 添加对应的颜色值 const colorObj = parseColor(colors[i]); const color = `rgba(${colorObj.r}, ${colorObj.g}, ${colorObj.b}, ${styleConfig.opacity})`; @@ -289,14 +374,12 @@ const StyleEditorPanel: React.FC = () => { return conditions; }; // 动态生成尺寸条件表达式 - const generateDimensionConditions = (): any[] => { + const generateDimensionConditions = (property: string): any[] => { const conditions: any[] = ["case"]; - for (let i = 0; i < breaks.length; i++) { - conditions.push(["<=", ["get", styleConfig.property], breaks[i]]); + conditions.push(["<=", ["get", property], breaks[i]]); conditions.push(dimensions[i]); } - conditions.push(dimensions[dimensions.length - 1]); return conditions; }; @@ -305,25 +388,29 @@ const StyleEditorPanel: React.FC = () => { // 根据图层类型设置不同的样式属性 if (layerType === "linestring") { - dynamicStyle["stroke-color"] = generateColorConditions(); - dynamicStyle["stroke-width"] = generateDimensionConditions(); + dynamicStyle["stroke-color"] = generateColorConditions( + styleConfig.property + ); + dynamicStyle["stroke-width"] = generateDimensionConditions( + styleConfig.property + ); } else if (layerType === "point") { - dynamicStyle["circle-fill-color"] = generateColorConditions(); - dynamicStyle["circle-radius"] = generateDimensionConditions(); - dynamicStyle["circle-stroke-color"] = generateColorConditions(); + dynamicStyle["circle-fill-color"] = generateColorConditions( + styleConfig.property + ); + dynamicStyle["circle-radius"] = generateDimensionConditions( + styleConfig.property + ); + dynamicStyle["circle-stroke-color"] = generateColorConditions( + styleConfig.property + ); dynamicStyle["circle-stroke-width"] = 2; - } else { - // 面要素 - dynamicStyle["fill-color"] = generateColorConditions(); - dynamicStyle["stroke-color"] = generateColorConditions(); - dynamicStyle["stroke-width"] = generateDimensionConditions(); } selectedRenderLayer.setStyle(dynamicStyle); - // console.log(map?.getAllLayers()); // 创建图例配置对象 - const finalLegendConfig: LegendStyleConfig = { + const legendConfig: LegendStyleConfig = { layerName: selectedRenderLayer.get("name"), layerId: selectedRenderLayer.get("value"), property: selectedProperty.name, @@ -332,28 +419,29 @@ const StyleEditorPanel: React.FC = () => { dimensions: dimensions, breaks: breaks, }; - - // 更新图例配置 - setLegendStyleConfig(finalLegendConfig); - // 自动保存样式状态,直接传入图例配置 setTimeout(() => { - saveLayerStyle(finalLegendConfig); + saveLayerStyle(selectedRenderLayer.get("value"), legendConfig); }, 100); }; // 重置样式 const resetStyle = useCallback(() => { if (!selectedRenderLayer) return; - // 重置 WebGL 图层样式 const defaultFlatStyle: FlatStyleLike = { - "stroke-width": 2, - "stroke-color": `rgba(51, 153, 204, 0.9)`, - "fill-color": `rgba(51, 153, 204, 0.5)`, - "circle-radius": 7, - "circle-stroke-width": 2, - "circle-stroke-color": `rgba(51, 153, 204, 0.9)`, - "circle-fill-color": `rgba(51, 153, 204, 0.5)`, + "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 + ], }; selectedRenderLayer.setStyle(defaultFlatStyle); @@ -366,85 +454,190 @@ const StyleEditorPanel: React.FC = () => { // 重置样式应用状态 if (layerId === "junctions") { setApplyJunctionStyle(false); + if (setShowJunctionText) setShowJunctionText(false); + if (setJunctionText) setJunctionText(""); } else if (layerId === "pipes") { setApplyPipeStyle(false); + if (setShowPipeText) setShowPipeText(false); + if (setPipeText) setPipeText(""); } } }, [selectedRenderLayer]); + // 更新当前 VectorTileSource 中的所有缓冲要素属性 + const updateVectorTileSource = (property: string, data: any[]) => { + if (!map) return; + const vectorTileSources = map + .getAllLayers() + .filter((layer) => layer instanceof WebGLVectorTileLayer) + .map((layer) => layer.getSource() as VectorTileSource) + .filter((source) => source); + + if (!vectorTileSources.length) return; + + // 创建 id 到 value 的映射 + const dataMap = new Map(); + data.forEach((d: any) => { + dataMap.set(d.ID, d.value || 0); + }); + + // 直接遍历所有瓦片和要素,无需分批处理 + vectorTileSources.forEach((vectorTileSource) => { + const sourceTiles = vectorTileSource.sourceTiles_; + + Object.values(sourceTiles).forEach((vectorTile) => { + const renderFeatures = vectorTile.getFeatures(); + if (!renderFeatures || renderFeatures.length === 0) return; + // 直接更新要素属性 + renderFeatures.forEach((renderFeature) => { + const featureId = renderFeature.get("id"); + const value = dataMap.get(featureId); + if (value !== undefined) { + (renderFeature as any).properties_[property] = value; + } + }); + }); + }); + }; + // 新增事件,监听 VectorTileSource 的 tileloadend 事件,为新增瓦片数据动态更新要素属性 + const [tileLoadListeners, setTileLoadListeners] = useState< + Map void> + >(new Map()); + const attachVectorTileSourceLoadedEvent = ( + layerId: string, + property: string, + data: any[] + ) => { + if (!map) return; + const vectorTileSource = map + .getAllLayers() + .filter((layer) => layer.get("value") === layerId) + .map((layer) => layer.getSource() as VectorTileSource) + .filter((source) => source)[0]; + if (!vectorTileSource) return; + // 创建 id 到 value 的映射 + const dataMap = new Map(); + data.forEach((d: any) => { + dataMap.set(d.ID, d.value || 0); + }); + // 新增监听器并保存 + const newListeners = new Map void>(); + + const listener = (event: any) => { + try { + if (event.tile instanceof VectorTile) { + const renderFeatures = event.tile.getFeatures(); + if (!renderFeatures || renderFeatures.length === 0) return; + // 直接更新要素属性 + renderFeatures.forEach((renderFeature: any) => { + const featureId = renderFeature.get("id"); + const value = dataMap.get(featureId); + if (value !== undefined) { + (renderFeature as any).properties_[property] = value; + } + }); + } + } catch (error) { + console.error("Error processing tile load event:", error); + } + }; + + vectorTileSource.on("tileloadend", listener); + newListeners.set(vectorTileSource, listener); + setTileLoadListeners(newListeners); + }; + // 新增函数:取消对应 layerId 已添加的 on 事件 + const removeVectorTileSourceLoadedEvent = (layerId: string) => { + if (!map) return; + const vectorTileSource = map + .getAllLayers() + .filter((layer) => layer.get("value") === layerId) + .map((layer) => layer.getSource() as VectorTileSource) + .filter((source) => source)[0]; + if (!vectorTileSource) return; + const listener = tileLoadListeners.get(vectorTileSource); + if (listener) { + vectorTileSource.un("tileloadend", listener); + setTileLoadListeners((prev) => { + const newMap = new Map(prev); + newMap.delete(vectorTileSource); + return newMap; + }); + } + }; + + // 监听数据变化,重新应用样式。由样式应用按钮触发,或由数据变化触发 useEffect(() => { - if (applyJunctionStyle && junctionData.length > 0) { - // 应用节点样式 + // 判断此次触发是否由用户点击“应用”按钮引起 + const isUserTrigger = + styleUpdateTrigger !== prevStyleUpdateTriggerRef.current; + // 更新 prevStyleUpdateTriggerRef + prevStyleUpdateTriggerRef.current = styleUpdateTrigger; + + const updateJunctionStyle = () => { + if (!currentJunctionCalData) return; const junctionStyleConfigState = layerStyleStates.find( (s) => s.layerId === "junctions" ); - if (!junctionStyleConfigState) return; - const segments = junctionStyleConfigState?.styleConfig.segments; - const breaks = calculateClassification( - junctionData, - segments, - styleConfig.classificationMethod + applyClassificationStyle( + "junctions", + junctionStyleConfigState?.styleConfig ); - applyStyle(junctionStyleConfigState.layerId, breaks); - } - if (applyPipeStyle && pipeData.length > 0) { - // 应用管道样式 + // 更新现有的 VectorTileSource + updateVectorTileSource(junctionText, currentJunctionCalData); + // 移除旧的监听器,并添加新的监听器 + removeVectorTileSourceLoadedEvent("junctions"); + attachVectorTileSourceLoadedEvent( + "junctions", + junctionText, + currentJunctionCalData + ); + }; + const updatePipeStyle = () => { + if (!currentPipeCalData) return; const pipeStyleConfigState = layerStyleStates.find( (s) => s.layerId === "pipes" ); - if (!pipeStyleConfigState) return; - const segments = pipeStyleConfigState?.styleConfig.segments; - const breaks = calculateClassification( - pipeData, - segments, - styleConfig.classificationMethod - ); - applyStyle(pipeStyleConfigState.layerId, breaks); - } - }, [junctionData, pipeData, applyJunctionStyle, applyPipeStyle]); - // 样式状态管理功能 - // 保存当前图层的样式状态 - const saveLayerStyle = useCallback( - (overrideLegendConfig?: LegendStyleConfig) => { - if (!selectedRenderLayer || !styleConfig.property) { - console.warn("无法保存样式:缺少必要的图层或样式配置"); - return; + applyClassificationStyle("pipes", pipeStyleConfigState?.styleConfig); + // 更新现有的 VectorTileSource + updateVectorTileSource(pipeText, currentPipeCalData); + // 移除旧的监听器,并添加新的监听器 + removeVectorTileSourceLoadedEvent("pipes"); + attachVectorTileSourceLoadedEvent("pipes", pipeText, currentPipeCalData); + }; + if (isUserTrigger) { + if (selectedRenderLayer?.get("value") === "junctions") { + updateJunctionStyle(); + } else if (selectedRenderLayer?.get("value") === "pipes") { + updatePipeStyle(); } + return; + } + if ( + applyJunctionStyle && + currentJunctionCalData && + currentJunctionCalData.length > 0 + ) { + updateJunctionStyle(); + } + if (applyPipeStyle && currentPipeCalData && currentPipeCalData.length > 0) { + updatePipeStyle(); + } + if (!applyJunctionStyle) { + removeVectorTileSourceLoadedEvent("junctions"); + } + if (!applyPipeStyle) { + removeVectorTileSourceLoadedEvent("pipes"); + } + }, [ + styleUpdateTrigger, + applyJunctionStyle, + applyPipeStyle, + currentJunctionCalData, + currentPipeCalData, + ]); - const layerId = selectedRenderLayer.get("value"); - const layerName = selectedRenderLayer.get("name") || `图层${layerId}`; - - // 使用传入的图例配置,或者使用当前状态的图例配置 - const finalLegendConfig = overrideLegendConfig || legendStyleConfig; - - const newStyleState: LayerStyleState = { - layerId, - layerName, - styleConfig: { ...styleConfig }, - legendConfig: { ...finalLegendConfig }, - isActive: true, - }; - - setLayerStyleStates((prev) => { - // 检查是否已存在该图层的样式状态 - const existingIndex = prev.findIndex( - (state) => state.layerId === layerId - ); - - if (existingIndex !== -1) { - // 更新已存在的状态 - const updated = [...prev]; - updated[existingIndex] = newStyleState; - return updated; - } else { - // 添加新的状态 - return [...prev, newStyleState]; - } - }); - }, - [selectedRenderLayer, styleConfig, legendStyleConfig] - ); - // 获取地图中的矢量图层 + // 获取地图中的矢量图层,用于选择图层选项 useEffect(() => { if (!map) return; @@ -461,18 +654,16 @@ const StyleEditorPanel: React.FC = () => { updateVisibleLayers(); }, [map]); - // 获取选中图层的属性 + // 获取选中图层的属性,并检查是否有已缓存的样式状态 useEffect(() => { // 如果没有矢量图层或没有选中图层,清空属性列表 if (!renderLayers || renderLayers.length === 0) { setAvailableProperties([]); - // console.log("没有可用的矢量图层"); return; } // 如果没有选中图层,清空属性列表 if (!selectedRenderLayer) { setAvailableProperties([]); - // console.log("没有选中的图层"); return; } @@ -491,11 +682,8 @@ const StyleEditorPanel: React.FC = () => { const cachedStyleState = layerStyleStates.find( (state) => state.layerId === layerId ); - if (cachedStyleState) { setStyleConfig(cachedStyleState.styleConfig); - setLegendStyleConfig(cachedStyleState.legendConfig); - // console.log(`已自动恢复图层 ${cachedStyleState.layerName} 的样式状态`); } }, [renderLayers, selectedRenderLayer, map, renderLayers, layerStyleStates]); // 同步属性状态 @@ -643,8 +831,8 @@ const StyleEditorPanel: React.FC = () => { minSize: value as number, })) } - min={5} - max={15} + min={2} + max={8} step={1} size="small" /> @@ -661,8 +849,8 @@ const StyleEditorPanel: React.FC = () => { maxSize: value as number, })) } - min={20} - max={30} + min={10} + max={16} step={1} size="small" /> @@ -966,7 +1154,7 @@ const StyleEditorPanel: React.FC = () => { variant="contained" color="primary" onClick={() => { - setStyleState(selectedRenderLayer); + setStyleState(); }} disabled={!selectedRenderLayer || !styleConfig.property} startIcon={} @@ -989,7 +1177,7 @@ const StyleEditorPanel: React.FC = () => {
{/* 显示多图层图例 */} {getActiveLegendConfigs().length > 0 && ( -
+
{getActiveLegendConfigs().map((config, index) => ( diff --git a/src/app/OlMap/Controls/StyleLegend.tsx b/src/app/OlMap/Controls/StyleLegend.tsx index 8c8af74..18afa9b 100644 --- a/src/app/OlMap/Controls/StyleLegend.tsx +++ b/src/app/OlMap/Controls/StyleLegend.tsx @@ -27,7 +27,7 @@ const StyleLegend: React.FC = ({ return ( {layerName} - {property} @@ -71,4 +71,4 @@ const StyleLegend: React.FC = ({ }; export default StyleLegend; -export type { LegendStyleConfig }; \ No newline at end of file +export type { LegendStyleConfig }; diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx index 4ea87a6..4cce809 100644 --- a/src/app/OlMap/Controls/Timeline.tsx +++ b/src/app/OlMap/Controls/Timeline.tsx @@ -1,6 +1,8 @@ "use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; +import { useNotification } from "@refinedev/core"; + import { Box, Button, @@ -20,21 +22,28 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import { zhCN } from "date-fns/locale"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; -import { TbRewindBackward5, TbRewindForward5 } from "react-icons/tb"; +import { TbRewindBackward15, TbRewindForward15 } from "react-icons/tb"; import { useData } from "../MapComponent"; import { config } from "@/config/config"; - +import { useMap } from "../MapComponent"; +import { set } from "ol/transform"; const backendUrl = config.backendUrl; const Timeline: React.FC = () => { const data = useData(); if (!data) { return
Loading...
; // 或其他占位符 } - const { setJunctionDataState, setPipeDataState, junctionText, pipeText } = - data; + const { + setCurrentJunctionCalData, + setCurrentPipeCalData, + junctionText, + pipeText, + } = data; + const { open, close } = useNotification(); const [currentTime, setCurrentTime] = useState(0); // 分钟数 (0-1439) - const [selectedDate, setSelectedDate] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(new Date("2025-9-17")); + // const [selectedDate, setSelectedDate] = useState(new Date()); // 默认今天 const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(5000); // 毫秒 const [calculatedInterval, setCalculatedInterval] = useState(1440); // 分钟 @@ -52,7 +61,9 @@ const Timeline: React.FC = () => { const fetchFrameData = async (queryTime: Date) => { const query_time = queryTime.toISOString(); const cacheKey = query_time; - + // console.log("Fetching data for time:", query_time); + // console.log("Junction Property:", junctionText); + // console.log("Pipe Property:", pipeText); // 检查缓存 if (cacheRef.current.has(cacheKey)) { const { nodeRecords, linkRecords } = cacheRef.current.get(cacheKey)!; @@ -65,12 +76,8 @@ const Timeline: React.FC = () => { // 定义需要查询的属性 const junctionProperties = junctionText; const pipeProperties = pipeText; - if ( - !junctionProperties || - !pipeProperties || - junctionProperties === "" || - pipeProperties === "" - ) { + // 如果属性未定义或为空,直接返回 + if (junctionProperties === "" || pipeProperties === "") { return; } console.log( @@ -105,50 +112,16 @@ const Timeline: React.FC = () => { // 提取更新状态的逻辑 const updateDataStates = (nodeResults: any[], linkResults: any[]) => { - const junctionProperties = junctionText; - const pipeProperties = pipeText; - - // 将 nodeRecords 转换为 Map 以提高查找效率 - const nodeMap: Map = new Map( - nodeResults.map((r: any) => [r.ID, r]) - ); - // 将 linkRecords 转换为 Map 以提高查找效率 - const linkMap: Map = new Map( - linkResults.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; - }) - ); + if (setCurrentJunctionCalData) { + setCurrentJunctionCalData(nodeResults); + } else { + console.log("setCurrentJunctionCalData is undefined"); + } + if (setCurrentPipeCalData) { + setCurrentPipeCalData(linkResults); + } else { + console.log("setCurrentPipeCalData is undefined"); + } }; // 时间刻度数组 (每5分钟一个刻度) @@ -209,11 +182,18 @@ const Timeline: React.FC = () => { // 播放控制 const handlePlay = useCallback(() => { if (!isPlaying) { + if (junctionText === "" || pipeText === "") { + open?.({ + type: "error", + message: "请先设置节点和管道的属性。", + }); + return; + } setIsPlaying(true); intervalRef.current = setInterval(() => { setCurrentTime((prev) => { - const next = prev >= 1435 ? 0 : prev + 5; // 到达23:55后回到00:00 + const next = prev >= 1440 ? 0 : prev + 15; // 到达24:00后回到00:00 setSliderValue(next); return next; }); @@ -242,7 +222,7 @@ const Timeline: React.FC = () => { // 步进控制 const handleStepBackward = useCallback(() => { setCurrentTime((prev) => { - const next = prev <= 0 ? 1435 : prev - 5; + const next = prev <= 0 ? 1440 : prev - 15; setSliderValue(next); return next; }); @@ -250,7 +230,7 @@ const Timeline: React.FC = () => { const handleStepForward = useCallback(() => { setCurrentTime((prev) => { - const next = prev >= 1435 ? 0 : prev + 5; + const next = prev >= 1440 ? 0 : prev + 15; setSliderValue(next); return next; }); @@ -274,7 +254,7 @@ const Timeline: React.FC = () => { clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setCurrentTime((prev) => { - const next = prev >= 1435 ? 0 : prev + 5; + const next = prev >= 1440 ? 0 : prev + 15; setSliderValue(next); return next; }); @@ -305,6 +285,23 @@ const Timeline: React.FC = () => { } }; }, []); + // 获取地图实例 + const map = useMap(); + // 这里防止地图缩放时,瓦片重新加载引起的属性更新出错 + useEffect(() => { + // 监听地图缩放事件,缩放时停止播放 + if (map) { + const onZoom = () => { + handlePause(); + }; + map.getView().on("change:resolution", onZoom); + + // 清理事件监听 + return () => { + map.getView().un("change:resolution", onZoom); + }; + } + }, [map, handlePause]); return ( @@ -370,7 +367,7 @@ const Timeline: React.FC = () => { onClick={handleStepBackward} size="small" > - + @@ -390,7 +387,7 @@ const Timeline: React.FC = () => { onClick={handleStepForward} size="small" > - + @@ -448,8 +445,8 @@ const Timeline: React.FC = () => { index % 12 === 0)} // 每小时显示一个标记 onChange={handleSliderChange} valueLabelDisplay="auto" diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx index 2ca0247..b352b9f 100644 --- a/src/app/OlMap/MapComponent.tsx +++ b/src/app/OlMap/MapComponent.tsx @@ -24,15 +24,18 @@ 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 { el, tr } from "date-fns/locale"; +import RenderFeature from "ol/render/Feature"; +import { set } from "date-fns"; interface MapComponentProps { children?: React.ReactNode; } interface DataContextType { - junctionData: any[]; - pipeData: any[]; - setJunctionDataState: React.Dispatch>; - setPipeDataState: React.Dispatch>; + currentJunctionCalData?: any[]; // 当前计算结果 + setCurrentJunctionCalData?: React.Dispatch>; + currentPipeCalData?: any[]; // 当前计算结果 + setCurrentPipeCalData?: React.Dispatch>; showJunctionText?: boolean; // 是否显示节点文本 showPipeText?: boolean; // 是否显示管道文本 setShowJunctionText?: React.Dispatch>; @@ -96,6 +99,12 @@ const MapComponent: React.FC = ({ children }) => { 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()); @@ -105,9 +114,11 @@ const MapComponent: React.FC = ({ children }) => { const [showJunctionText, setShowJunctionText] = useState(false); // 控制节点文本显示 const [showPipeText, setShowPipeText] = useState(false); // 控制管道文本显示 - const [junctionText, setJunctionText] = useState(""); - const [pipeText, setPipeText] = useState(""); - const flowAnimation = useRef(true); // 添加动画控制标志 + 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( @@ -149,37 +160,82 @@ const MapComponent: React.FC = ({ children }) => { 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; - // 添加 MVT 瓦片加载逻辑 - 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", - }); - - // 缓存数据 + // 缓存 junction、pipe 数据,提供给 deck.gl 显示标签使用 junctionSource.on("tileloadend", (event) => { try { if (event.tile instanceof VectorTile) { @@ -290,54 +346,20 @@ const MapComponent: React.FC = ({ children }) => { console.error("Pipe tile load error:", error); } }); - - // 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" }, - ], - }, - }); + // 更新标签可见性状态 + // 监听 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, @@ -375,8 +397,11 @@ const MapComponent: React.FC = ({ children }) => { 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(); @@ -402,7 +427,8 @@ const MapComponent: React.FC = ({ children }) => { getTextAnchor: "middle", getAlignmentBaseline: "center", getPixelOffset: [0, -10], - visible: currentZoom >= 15 && currentZoom <= 24, + visible: + showJunctionTextLayer && currentZoom >= 15 && currentZoom <= 24, // --- 修改以下属性 --- // characterSet: "auto", // outlineWidth: 4, @@ -422,7 +448,7 @@ const MapComponent: React.FC = ({ children }) => { getPixelOffset: [0, -8], getTextAnchor: "middle", getAlignmentBaseline: "bottom", - visible: currentZoom >= 15 && currentZoom <= 24, + visible: showPipeTextLayer && currentZoom >= 15 && currentZoom <= 24, // --- 修改以下属性 --- // characterSet: "auto", // outlineWidth: 5, @@ -456,6 +482,7 @@ const MapComponent: React.FC = ({ children }) => { }, getColor: [0, 220, 255], opacity: 0.8, + visible: currentZoom >= 12 && currentZoom <= 24, widthMinPixels: 5, jointRounded: true, // 拐角变圆 // capRounded: true, // 端点变圆 @@ -476,16 +503,85 @@ const MapComponent: React.FC = ({ children }) => { requestAnimationFrame(animate); }; animate(); - }, [flowAnimation, junctionData, pipeData]); + }, [ + 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 ( <>