diff --git a/src/components/olmap/BurstLocation/LocationResults.tsx b/src/components/olmap/BurstLocation/LocationResults.tsx index d000c99..afef844 100644 --- a/src/components/olmap/BurstLocation/LocationResults.tsx +++ b/src/components/olmap/BurstLocation/LocationResults.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Typography, @@ -115,7 +115,7 @@ const EmptyState = () => ( const LocationResults: React.FC = ({ result }) => { const map = useMap(); - const [highlightLayer, setHighlightLayer] = useState | null>(null); + const highlightLayerRef = useRef | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); const candidatePipes = useMemo(() => { @@ -128,13 +128,13 @@ const LocationResults: React.FC = ({ result }) => { return base; }, [result]); - const allCandidatePipeIds = useMemo(() => { + const allCandidatePipeIds = (() => { const ids = candidatePipes.map((item) => item.pipe_id); if (result?.located_pipe) { ids.unshift(result.located_pipe); } return Array.from(new Set(ids.filter(Boolean))); - }, [candidatePipes, result?.located_pipe]); + })(); useEffect(() => { if (!map) return; @@ -159,19 +159,20 @@ const LocationResults: React.FC = ({ result }) => { }, }); map.addLayer(layer); - setHighlightLayer(layer); + highlightLayerRef.current = layer; return () => { + highlightLayerRef.current = null; map.removeLayer(layer); }; }, [map]); useEffect(() => { - const source = highlightLayer?.getSource(); + const source = highlightLayerRef.current?.getSource(); if (!source) return; source.clear(); highlightFeatures.forEach((feature) => source.addFeature(feature)); - }, [highlightFeatures, highlightLayer]); + }, [highlightFeatures]); const locatePipes = async (pipeIds: string[]) => { if (!pipeIds.length || !map) return; diff --git a/src/components/olmap/BurstSimulation/LocationResults.tsx b/src/components/olmap/BurstSimulation/LocationResults.tsx index 6f5efcf..d5b3503 100644 --- a/src/components/olmap/BurstSimulation/LocationResults.tsx +++ b/src/components/olmap/BurstSimulation/LocationResults.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Box, Typography, @@ -41,8 +41,7 @@ interface LocationResultsProps { const LocationResults: React.FC = ({ results = [], }) => { - const [highlightLayer, setHighlightLayer] = - useState | null>(null); + const highlightLayerRef = useRef | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); const map = useMap(); @@ -145,19 +144,17 @@ const LocationResults: React.FC = ({ }); map.addLayer(highlightLayer); - setHighlightLayer(highlightLayer); + highlightLayerRef.current = highlightLayer; return () => { + highlightLayerRef.current = null; map.removeLayer(highlightLayer); }; }, [map]); // 高亮要素的函数 useEffect(() => { - if (!highlightLayer) { - return; - } - const source = highlightLayer.getSource(); + const source = highlightLayerRef.current?.getSource(); if (!source) { return; } @@ -169,7 +166,7 @@ const LocationResults: React.FC = ({ source.addFeature(feature); } }); - }, [highlightFeatures, highlightLayer]); + }, [highlightFeatures]); // 取第一条记录或空对象 const result = results.length > 0 ? results[0] : null; diff --git a/src/components/olmap/BurstSimulation/ValveIsolation.tsx b/src/components/olmap/BurstSimulation/ValveIsolation.tsx index def9331..b4985a8 100644 --- a/src/components/olmap/BurstSimulation/ValveIsolation.tsx +++ b/src/components/olmap/BurstSimulation/ValveIsolation.tsx @@ -1090,7 +1090,7 @@ const ValveIsolation: React.FC = ({ ) : ( - 请先在流程2中选择不可用阀门,然后点击"扩大搜索"按钮 + 请先在流程2中选择不可用阀门,然后点击“扩大搜索”按钮 )} diff --git a/src/components/olmap/HealthRiskAnalysis/Timeline.tsx b/src/components/olmap/HealthRiskAnalysis/Timeline.tsx index 8c4a561..a4f0bde 100644 --- a/src/components/olmap/HealthRiskAnalysis/Timeline.tsx +++ b/src/components/olmap/HealthRiskAnalysis/Timeline.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useRef, useCallback } from "react"; +import React, { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { useNotification } from "@refinedev/core"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; @@ -27,7 +27,6 @@ import dayjs from "dayjs"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb"; import { FiSkipBack, FiSkipForward } from "react-icons/fi"; -import { useData } from "@components/olmap/core/MapComponent"; import { config, NETWORK_NAME } from "@/config/config"; import { apiFetch } from "@/lib/apiFetch"; import { useMap } from "@components/olmap/core/MapComponent"; @@ -63,10 +62,6 @@ interface TimelineProps { const Timeline: React.FC = ({ disableDateSelection = false, }) => { - const data = useData(); - if (!data) { - return
Loading...
; // 或其他占位符 - } const { open } = useNotification(); const { predictionResults, @@ -79,7 +74,6 @@ const Timeline: React.FC = ({ const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(5000); // 毫秒 const [isPredicting, setIsPredicting] = useState(false); - const [pipeLayer, setPipeLayer] = useState(null); // 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定 const healthDataRef = useRef>(new Map()); @@ -228,10 +222,21 @@ const Timeline: React.FC = ({ clearTimeout(debounceRef.current); } }; - }, [pipeLayer]); + }, []); // 获取地图实例 const map = useMap(); + const pipeLayer = useMemo(() => { + if (!map) return null; + + const layers = map.getLayers().getArray(); + return ( + layers.find( + (layer) => + layer instanceof WebGLVectorTileLayer && layer.get("value") === "pipes", + ) as WebGLVectorTileLayer | undefined + ) ?? null; + }, [map]); // 根据索引从 survival_function 中获取生存概率 const getSurvivalProbabilityAtYear = useCallback( @@ -362,21 +367,6 @@ const Timeline: React.FC = ({ updatePipeHealthData, ]); - // 初始化管道图层 - useEffect(() => { - if (!map) return; - - const layers = map.getLayers().getArray(); - const pipesLayer = layers.find( - (layer) => - layer instanceof WebGLVectorTileLayer && layer.get("value") === "pipes", - ) as WebGLVectorTileLayer | undefined; - - if (pipesLayer) { - setPipeLayer(pipesLayer); - } - }, [map]); - // 监听依赖变化,更新样式 useEffect(() => { if (predictionResults.length > 0 && pipeLayer) { diff --git a/src/components/olmap/core/Controls/DrawPanel.tsx b/src/components/olmap/core/Controls/DrawPanel.tsx index 3980cc9..448f09f 100644 --- a/src/components/olmap/core/Controls/DrawPanel.tsx +++ b/src/components/olmap/core/Controls/DrawPanel.tsx @@ -31,9 +31,7 @@ import { useMap } from "../MapComponent"; const DrawPanel: React.FC = () => { const map = useMap(); const [activeTool, setActiveTool] = useState("pan"); - const [drawLayer, setDrawLayer] = useState | null>( - null - ); + const drawLayerRef = useRef | null>(null); const [drawnFeatures, setDrawnFeatures] = useState[]>([]); const [history, setHistory] = useState<{ stack: Feature[][]; @@ -79,13 +77,14 @@ const DrawPanel: React.FC = () => { }); map.addLayer(drawVectorLayer); - setDrawLayer(drawVectorLayer); + drawLayerRef.current = drawVectorLayer; return () => { if (drawInteractionRef.current && map) { map.removeInteraction(drawInteractionRef.current); drawInteractionRef.current = null; } + drawLayerRef.current = null; map.removeLayer(drawVectorLayer); }; }, [map, drawInteractionRef]); @@ -110,6 +109,7 @@ const DrawPanel: React.FC = () => { type: GeometryType, geometryFunction?: GeometryFunction ) => { + const drawLayer = drawLayerRef.current; if (!drawLayer) return; if (!map) return; @@ -285,6 +285,7 @@ const DrawPanel: React.FC = () => { // 删除所有绘制的要素 const handleDelete = () => { + const drawLayer = drawLayerRef.current; if (!drawLayer) return; const source = drawLayer.getSource(); @@ -301,6 +302,7 @@ const DrawPanel: React.FC = () => { // 更新绘图图层 const updateDrawLayer = (features: Feature[]) => { + const drawLayer = drawLayerRef.current; if (!drawLayer) return; const source = drawLayer.getSource(); diff --git a/src/components/olmap/core/Controls/LayerControl.tsx b/src/components/olmap/core/Controls/LayerControl.tsx index 8b539a5..d71fe10 100644 --- a/src/components/olmap/core/Controls/LayerControl.tsx +++ b/src/components/olmap/core/Controls/LayerControl.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { useData, useMap } from "../MapComponent"; import { Checkbox, FormControlLabel } from "@mui/material"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; @@ -18,15 +18,12 @@ interface LayerItem { const LayerControl: React.FC = () => { const map = useMap(); const data = useData(); - if (!data) return; - const { - deckLayer, - isContourLayerAvailable, - isWaterflowLayerAvailable, - setShowWaterflowLayer, - setShowContourLayer, - } = data; - const [layerItems, setLayerItems] = useState([]); + const [refreshKey, setRefreshKey] = useState(0); + const deckLayer = data?.deckLayer; + const isContourLayerAvailable = data?.isContourLayerAvailable; + const isWaterflowLayerAvailable = data?.isWaterflowLayerAvailable; + const setShowWaterflowLayer = data?.setShowWaterflowLayer; + const setShowContourLayer = data?.setShowContourLayer; const layerOrder = [ "junctions", @@ -40,16 +37,12 @@ const LayerControl: React.FC = () => { "junctionContourLayer", ]; - // 更新图层列表 - const updateLayers = useCallback(() => { - if (!map || !data) return; + const layerItems = useMemo(() => { + if (!map || !data) return []; const items: LayerItem[] = []; - // 1. 获取 OpenLayers 图层 - const mapLayers = map.getLayers().getArray(); - mapLayers.forEach((layer) => { - // 筛选特定类型的 OpenLayers 图层 + map.getLayers().getArray().forEach((layer) => { if ( layer instanceof WebGLVectorTileLayer || layer instanceof VectorTileLayer || @@ -57,7 +50,6 @@ const LayerControl: React.FC = () => { ) { const value = layer.get("value"); const name = layer.get("name"); - // 只有设置了 value (作为 ID) 的图层才会被纳入控制 if (value) { items.push({ id: value, @@ -70,66 +62,56 @@ const LayerControl: React.FC = () => { } }); - // 2. 获取 DeckLayer 中的子图层 if (deckLayer && deckLayer instanceof DeckLayer) { - const deckLayers = deckLayer.getDeckLayers(); - deckLayers.forEach((layer: any) => { - if (layer && layer.id) { - // 仅处理 junctionContourLayer 和 waterflowLayer - if ( - layer.id !== "junctionContourLayer" && - layer.id !== "waterflowLayer" - ) { - return; - } - // 检查可用性 - if ( - (layer.id === "junctionContourLayer" && !isContourLayerAvailable) || - (layer.id === "waterflowLayer" && !isWaterflowLayerAvailable) - ) { - return; // 跳过不可用图层 - } - const visible = - deckLayer.getDeckLayerVisible(layer.id) ?? - layer.props?.visible ?? - true; - items.push({ - id: layer.props.id, - name: layer.props.name, // 使用 name 属性作为显示名称 - visible: visible, - type: "deck", - layerRef: layer, - }); + deckLayer.getDeckLayers().forEach((layer: any) => { + if (!layer?.id) return; + if (layer.id !== "junctionContourLayer" && layer.id !== "waterflowLayer") { + return; } + if ( + (layer.id === "junctionContourLayer" && !isContourLayerAvailable) || + (layer.id === "waterflowLayer" && !isWaterflowLayerAvailable) + ) { + return; + } + + items.push({ + id: layer.props.id, + name: layer.props.name, + visible: + deckLayer.getDeckLayerVisible(layer.id) ?? layer.props?.visible ?? true, + type: "deck", + layerRef: layer, + }); }); } - // 过滤并排序 - const sortedItems = items + return items .filter((item) => layerOrder.includes(item.id)) - .sort((a, b) => { - const indexA = layerOrder.indexOf(a.id); - const indexB = layerOrder.indexOf(b.id); - return indexA - indexB; - }); - - setLayerItems(sortedItems); - }, [map, deckLayer, isWaterflowLayerAvailable, isContourLayerAvailable]); + .sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id)); + }, [ + map, + data, + deckLayer, + isContourLayerAvailable, + isWaterflowLayerAvailable, + refreshKey, + ]); useEffect(() => { - updateLayers(); + if (!map) return; - if (map) { - const layerCollection = map.getLayers(); - layerCollection.on("change:length", updateLayers); - } + const layerCollection = map.getLayers(); + const handleLayerChange = () => { + setRefreshKey((prev) => prev + 1); + }; + + layerCollection.on("change:length", handleLayerChange); return () => { - if (map) { - map.getLayers().un("change:length", updateLayers); - } + map.getLayers().un("change:length", handleLayerChange); }; - }, [map, updateLayers]); + }, [map]); const handleVisibilityChange = (item: LayerItem, checked: boolean) => { if (item.type === "ol") { @@ -142,10 +124,7 @@ const LayerControl: React.FC = () => { setShowWaterflowLayer && setShowWaterflowLayer(checked); } } - - setLayerItems((prev) => - prev.map((i) => (i.id === item.id ? { ...i, visible: checked } : i)), - ); + setRefreshKey((prev) => prev + 1); }; if (!data) { diff --git a/src/components/olmap/core/Controls/StyleEditorPanel.tsx b/src/components/olmap/core/Controls/StyleEditorPanel.tsx index 25dfec7..a3ad909 100644 --- a/src/components/olmap/core/Controls/StyleEditorPanel.tsx +++ b/src/components/olmap/core/Controls/StyleEditorPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; // 导入Material-UI图标和组件 import ColorLensIcon from "@mui/icons-material/ColorLens"; @@ -180,26 +180,21 @@ const StyleEditorPanel: React.FC = ({ }) => { const map = useMap(); const data = useData(); - if (!data) { - return
Loading...
; // 或其他占位符 - } - const { - currentJunctionCalData, - currentPipeCalData, - junctionText, - pipeText, - setShowJunctionTextLayer, - setShowPipeTextLayer, - setShowJunctionId, - setShowPipeId, - setContourLayerAvailable, - setWaterflowLayerAvailable, - setJunctionText, - setPipeText, - setContours, - diameterRange, - elevationRange, - } = data; + const currentJunctionCalData = data?.currentJunctionCalData; + const currentPipeCalData = data?.currentPipeCalData; + const junctionText = data?.junctionText ?? ""; + const pipeText = data?.pipeText ?? ""; + const setShowJunctionTextLayer = data?.setShowJunctionTextLayer; + const setShowPipeTextLayer = data?.setShowPipeTextLayer; + const setShowJunctionId = data?.setShowJunctionId; + const setShowPipeId = data?.setShowPipeId; + const setContourLayerAvailable = data?.setContourLayerAvailable; + const setWaterflowLayerAvailable = data?.setWaterflowLayerAvailable; + const setJunctionText = data?.setJunctionText; + const setPipeText = data?.setPipeText; + const setContours = data?.setContours; + const diameterRange = data?.diameterRange; + const elevationRange = data?.elevationRange; const unitHeadlossRange = [0, 5]; @@ -213,9 +208,6 @@ const StyleEditorPanel: React.FC = ({ const [renderLayers, setRenderLayers] = useState([]); const [selectedRenderLayer, setSelectedRenderLayer] = useState(); - const [availableProperties, setAvailableProperties] = useState< - { name: string; value: string }[] - >([]); const [styleConfig, setStyleConfig] = useState({ property: "", classificationMethod: "pretty_breaks", @@ -237,6 +229,74 @@ const StyleEditorPanel: React.FC = ({ customColors: [], }); + const getDefaultCustomColors = ( + segments: number, + existingColors: string[] = [] + ) => { + const nextColors = [...existingColors]; + const baseColors = RAINBOW_PALETTES[0].colors; + + while (nextColors.length < segments) { + nextColors.push(baseColors[nextColors.length % baseColors.length]); + } + + return nextColors.slice(0, segments); + }; + + const getDefaultCustomBreaks = ( + segments: number, + property: string, + layer: WebGLVectorTileLayer | undefined = selectedRenderLayer + ) => { + if (!layer || !property) { + return Array.from({ length: segments }, () => 0); + } + + const selectedLayerId = layer.get("value"); + let dataArr: number[] = []; + + const isElevation = + selectedLayerId === "junctions" && property === "elevation"; + const isDiameter = selectedLayerId === "pipes" && property === "diameter"; + + if (isElevation && elevationRange) { + dataArr = [elevationRange[0], elevationRange[1]]; + } else if (isDiameter && diameterRange) { + dataArr = [diameterRange[0], diameterRange[1]]; + } else if (selectedLayerId === "junctions" && currentJunctionCalData) { + dataArr = currentJunctionCalData.map((d: any) => d.value); + } else if (selectedLayerId === "pipes" && currentPipeCalData) { + dataArr = currentPipeCalData.map((d: any) => d.value); + } + + if (dataArr.length === 0) { + return Array.from({ length: segments }, () => 0); + } + + const defaultBreaks = calculateClassification( + dataArr, + segments, + "pretty_breaks" + ).slice(0, segments); + + while (defaultBreaks.length < segments) { + defaultBreaks.push(defaultBreaks[defaultBreaks.length - 1] ?? 0); + } + + return defaultBreaks; + }; + + const availableProperties = useMemo<{ name: string; value: string }[]>(() => { + if (!selectedRenderLayer) { + return []; + } + + return (selectedRenderLayer.get("properties") || []) as { + name: string; + value: string; + }[]; + }, [selectedRenderLayer]); + // 根据分段数生成相应数量的渐进颜色 const generateGradientColors = useCallback( (segments: number): string[] => { @@ -278,63 +338,56 @@ const StyleEditorPanel: React.FC = ({ [styleConfig.rainbowPaletteIndex] ); // 保存当前图层的样式状态 - const saveLayerStyle = useCallback( - ( - layerId?: string, - newLegendConfig?: LegendStyleConfig, - overrideStyleConfig?: StyleConfig - ) => { - const currentStyleConfig = overrideStyleConfig || styleConfig; + const saveLayerStyle = ( + layerId?: string, + newLegendConfig?: LegendStyleConfig, + overrideStyleConfig?: StyleConfig + ) => { + const currentStyleConfig = overrideStyleConfig || styleConfig; - if (!currentStyleConfig.property) { - console.warn("无法保存样式:缺少必要的图层或样式配置"); - return; + if (!currentStyleConfig.property) { + console.warn("无法保存样式:缺少必要的图层或样式配置"); + return; + } + if (!layerId) return; + + const layerName = + newLegendConfig?.layerName || + selectedRenderLayer?.get("name") || + `图层${layerId}`; + const property = availableProperties.find( + (p) => p.value === currentStyleConfig.property + ); + const legendConfig: LegendStyleConfig = newLegendConfig || { + layerId, + layerName, + property: property?.name || currentStyleConfig.property, + colors: [], + type: selectedRenderLayer?.get("type") || "point", + dimensions: [], + breaks: [], + }; + + const newStyleState: LayerStyleState = { + layerId, + layerName, + styleConfig: { ...currentStyleConfig }, + legendConfig: { ...legendConfig }, + isActive: true, + }; + + setLayerStyleStates((prev) => { + const existingIndex = prev.findIndex((state) => state.layerId === layerId); + + if (existingIndex !== -1) { + const updated = [...prev]; + updated[existingIndex] = newStyleState; + return updated; } - if (!layerId) return; // 如果没有传入 layerId,则不保存 - // 如果没有传入图例配置,则创建一个默认的空配置 - const layerName = - newLegendConfig?.layerName || - selectedRenderLayer?.get("name") || - `图层${layerId}`; - const property = availableProperties.find( - (p) => p.value === currentStyleConfig.property - ); - let legendConfig: LegendStyleConfig = newLegendConfig || { - layerId, - layerName, - property: property?.name || currentStyleConfig.property, - colors: [], - type: selectedRenderLayer?.get("type") || "point", - dimensions: [], - breaks: [], - }; - const newStyleState: LayerStyleState = { - layerId, - layerName, - styleConfig: { ...currentStyleConfig }, - legendConfig: { ...legendConfig }, - 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, availableProperties] - ); + return [...prev, newStyleState]; + }); + }; // 设置分类样式参数,触发样式应用 const setStyleState = () => { if (!selectedRenderLayer) return; @@ -787,7 +840,7 @@ const StyleEditorPanel: React.FC = ({ }; // 重置样式 - const resetStyle = useCallback(() => { + const resetStyle = () => { if (!selectedRenderLayer) return; // 重置 WebGL 图层样式 const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE; @@ -815,7 +868,7 @@ const StyleEditorPanel: React.FC = ({ setWaterflowLayerAvailable && setWaterflowLayerAvailable(false); } } - }, [selectedRenderLayer]); + }; // 更新当前 VectorTileSource 中的所有缓冲要素属性 const updateVectorTileSource = (property: string, data: any[]) => { if (!map) return; @@ -857,7 +910,7 @@ const StyleEditorPanel: React.FC = ({ }); }; // 新增事件,监听 VectorTileSource 的 tileloadend 事件,为新增瓦片数据动态更新要素属性 - const [tileLoadListeners, setTileLoadListeners] = useState< + const tileLoadListenersRef = useRef< Map void> >(new Map()); @@ -879,8 +932,6 @@ const StyleEditorPanel: React.FC = ({ dataMap.set(d.ID, d.value || 0); }); // 新增监听器并保存 - const newListeners = new Map void>(); - const listener = (event: any) => { try { if (event.tile instanceof VectorTile) { @@ -906,8 +957,7 @@ const StyleEditorPanel: React.FC = ({ }; vectorTileSource.on("tileloadend", listener); - newListeners.set(vectorTileSource, listener); - setTileLoadListeners(newListeners); + tileLoadListenersRef.current.set(vectorTileSource, listener); }; // 新增函数:取消对应 layerId 已添加的 on 事件 const removeVectorTileSourceLoadedEvent = (layerId: string) => { @@ -918,14 +968,10 @@ const StyleEditorPanel: React.FC = ({ .map((layer) => layer.getSource() as VectorTileSource) .filter((source) => source)[0]; if (!vectorTileSource) return; - const listener = tileLoadListeners.get(vectorTileSource); + const listener = tileLoadListenersRef.current.get(vectorTileSource); if (listener) { vectorTileSource.un("tileloadend", listener); - setTileLoadListeners((prev) => { - const newMap = new Map(prev); - newMap.delete(vectorTileSource); - return newMap; - }); + tileLoadListenersRef.current.delete(vectorTileSource); } }; @@ -1044,117 +1090,9 @@ const StyleEditorPanel: React.FC = ({ updateVisibleLayers(); }, [map]); - // 获取选中图层的属性,并检查是否有已缓存的样式状态 - useEffect(() => { - // 如果没有矢量图层或没有选中图层,清空属性列表 - if (!renderLayers || renderLayers.length === 0) { - setAvailableProperties([]); - return; - } - // 如果没有选中图层,清空属性列表 - if (!selectedRenderLayer) { - setAvailableProperties([]); - return; - } - - // 获取第一个要素的数值型属性 - const properties = selectedRenderLayer.get("properties") || {}; - setAvailableProperties(properties); - - // 设置选中的渲染图层 - const renderLayer = renderLayers.filter((layer) => { - return layer.get("value") === selectedRenderLayer?.get("value"); - })[0]; - setSelectedRenderLayer(renderLayer); - - // 检查是否有已缓存的样式状态,如果有则自动恢复 - const layerId = selectedRenderLayer.get("value"); - const cachedStyleState = layerStyleStates.find( - (state) => state.layerId === layerId - ); - if (cachedStyleState) { - setStyleConfig(cachedStyleState.styleConfig); - } - }, [renderLayers, selectedRenderLayer, map, renderLayers, layerStyleStates]); - - // 监听颜色类型变化,当切换到单一色时自动勾选宽度调整选项 - useEffect(() => { - if (styleConfig.colorType === "single") { - setStyleConfig((prev) => ({ - ...prev, - adjustWidthByProperty: true, - })); - } - }, [styleConfig.colorType]); - - // 初始化或调整自定义断点数组长度,默认使用 pretty_breaks 生成若存在数据 - useEffect(() => { - if (styleConfig.classificationMethod !== "custom_breaks") return; - - const numBreaks = styleConfig.segments; - setStyleConfig((prev) => { - const prevBreaks = prev.customBreaks || []; - if (prevBreaks.length === numBreaks) return prev; - - const selectedLayerId = selectedRenderLayer?.get("value"); - let dataArr: number[] = []; - - const isElevation = - selectedLayerId === "junctions" && styleConfig.property === "elevation"; - const isDiameter = - selectedLayerId === "pipes" && styleConfig.property === "diameter"; - - if (isElevation && elevationRange) { - dataArr = [elevationRange[0], elevationRange[1]]; - } else if (isDiameter && diameterRange) { - dataArr = [diameterRange[0], diameterRange[1]]; - } else if (selectedLayerId === "junctions" && currentJunctionCalData) { - dataArr = currentJunctionCalData.map((d: any) => d.value); - } else if (selectedLayerId === "pipes" && currentPipeCalData) { - dataArr = currentPipeCalData.map((d: any) => d.value); - } - - let defaultBreaks: number[] = Array.from({ length: numBreaks }, () => 0); - if (dataArr && dataArr.length > 0) { - defaultBreaks = calculateClassification( - dataArr, - styleConfig.segments, - "pretty_breaks" - ); - defaultBreaks = defaultBreaks.slice(0, numBreaks); - if (defaultBreaks.length < numBreaks) - while (defaultBreaks.length < numBreaks) - defaultBreaks.push(defaultBreaks[defaultBreaks.length - 1] ?? 0); - } - - return { ...prev, customBreaks: defaultBreaks }; - }); - }, [ - styleConfig.classificationMethod, - styleConfig.segments, - styleConfig.property, - selectedRenderLayer, - currentJunctionCalData, - currentPipeCalData, - elevationRange, - diameterRange, - ]); - - // 初始化或调整自定义颜色数组长度 - useEffect(() => { - const numColors = styleConfig.segments; - setStyleConfig((prev) => { - const prevColors = prev.customColors || []; - if (prevColors.length === numColors) return prev; - - const newColors = [...prevColors]; - const baseColors = RAINBOW_PALETTES[0].colors; - while (newColors.length < numColors) { - newColors.push(baseColors[newColors.length % baseColors.length]); - } - return { ...prev, customColors: newColors.slice(0, numColors) }; - }); - }, [styleConfig.segments]); + if (!data) { + return
Loading...
; + } const getColorSetting = () => { if (styleConfig.colorType === "single") { @@ -1624,9 +1562,21 @@ const StyleEditorPanel: React.FC = ({ const cachedStyleState = layerStyleStates.find( (state) => state.layerId === layerId ); - // 只有在没有缓存时才清空属性 - if (!cachedStyleState) { - setStyleConfig((prev) => ({ ...prev, property: "" })); + if (cachedStyleState) { + setStyleConfig(cachedStyleState.styleConfig); + } else { + setStyleConfig((prev) => ({ + ...prev, + property: "", + customBreaks: + prev.classificationMethod === "custom_breaks" + ? getDefaultCustomBreaks(prev.segments, "", newLayer) + : prev.customBreaks, + customColors: getDefaultCustomColors( + prev.segments, + prev.customColors + ), + })); } } }} @@ -1647,7 +1597,15 @@ const StyleEditorPanel: React.FC = ({ { + const nextMethod = e.target.value; setStyleConfig((prev) => ({ ...prev, - classificationMethod: e.target.value, + classificationMethod: nextMethod, + customBreaks: + nextMethod === "custom_breaks" + ? getDefaultCustomBreaks(prev.segments, prev.property) + : prev.customBreaks, })); }} > @@ -1695,7 +1658,14 @@ const StyleEditorPanel: React.FC = ({ return { ...prev, segments: newSegments, - customColors: newCustomColors, + customBreaks: + prev.classificationMethod === "custom_breaks" + ? getDefaultCustomBreaks(newSegments, prev.property) + : prev.customBreaks, + customColors: getDefaultCustomColors( + newSegments, + newCustomColors + ), }; }) } @@ -1782,6 +1752,10 @@ const StyleEditorPanel: React.FC = ({ return { ...prev, colorType: newColorType, + adjustWidthByProperty: + newColorType === "single" + ? true + : prev.adjustWidthByProperty, customColors: newCustomColors, }; }); diff --git a/src/components/olmap/core/Controls/Timeline.tsx b/src/components/olmap/core/Controls/Timeline.tsx index 208e984..eefdeed 100644 --- a/src/components/olmap/core/Controls/Timeline.tsx +++ b/src/components/olmap/core/Controls/Timeline.tsx @@ -47,29 +47,21 @@ const Timeline: React.FC = ({ schemeType = "burst_Analysis", }) => { const data = useData(); - if (!data) { - return
Loading...
; // 或其他占位符 - } - const { - currentTime, - setCurrentTime, - selectedDate, - setSelectedDate, - setCurrentJunctionCalData, - setCurrentPipeCalData, - junctionText, - pipeText, - } = data; - if ( - setCurrentTime === undefined || - currentTime === undefined || - selectedDate === undefined || - setSelectedDate === undefined - ) { - return
Loading...
; // 或其他占位符 - } + const hasTimelineState = + data && + data.setCurrentTime !== undefined && + data.currentTime !== undefined && + data.selectedDate !== undefined && + data.setSelectedDate !== undefined; + const currentTime = data?.currentTime ?? -1; + const setCurrentTime = data?.setCurrentTime ?? ((_: any) => undefined); + const selectedDate = data?.selectedDate ?? new Date(); + const setSelectedDate = data?.setSelectedDate ?? ((_: any) => undefined); + const setCurrentJunctionCalData = data?.setCurrentJunctionCalData; + const setCurrentPipeCalData = data?.setCurrentPipeCalData; + const junctionText = data?.junctionText ?? ""; + const pipeText = data?.pipeText ?? ""; const { open } = useNotification(); - const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(15000); // 毫秒 const [calculatedInterval, setCalculatedInterval] = useState(15); // 分钟 @@ -549,6 +541,10 @@ const Timeline: React.FC = ({ } }; + if (!hasTimelineState) { + return
Loading...
; + } + return (
= ({ const map = useMap(); const data = useData(); const { open } = useNotification(); - if (!data) return null; - const { currentTime, selectedDate, schemeName } = data; const [activeTools, setActiveTools] = useState([]); const [highlightFeatures, setHighlightFeatures] = useState([]); const [showPropertyPanel, setShowPropertyPanel] = useState(false); @@ -49,6 +47,9 @@ const Toolbar: React.FC = ({ const [showHistoryPanel, setShowHistoryPanel] = useState(false); const [highlightLayer, setHighlightLayer] = useState | null>(null); + const currentTime = data?.currentTime; + const selectedDate = data?.selectedDate; + const schemeName = data?.schemeName; // 样式状态管理 - 在 Toolbar 中管理,带有默认样式 const [layerStyleStates, setLayerStyleStates] = useState([ @@ -721,6 +722,10 @@ const Toolbar: React.FC = ({ return {}; }, [highlightFeatures, computedProperties]); + if (!data) { + return null; + } + return ( <>
diff --git a/src/components/olmap/core/MapComponent.tsx b/src/components/olmap/core/MapComponent.tsx index d319678..e142b47 100644 --- a/src/components/olmap/core/MapComponent.tsx +++ b/src/components/olmap/core/MapComponent.tsx @@ -78,15 +78,33 @@ const MapContext = createContext(undefined); const DataContext = createContext(undefined); // 添加防抖函数 -function debounce any>(func: F, waitFor: number) { +type DebouncedFunction any> = (( + ...args: Parameters +) => void) & { + cancel: () => void; +}; + +function debounce any>( + func: F, + waitFor: number +): DebouncedFunction { let timeout: ReturnType | null = null; - return (...args: Parameters): void => { + const debounced = (...args: Parameters): void => { if (timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(() => func(...args), waitFor); }; + + debounced.cancel = () => { + if (timeout !== null) { + clearTimeout(timeout); + timeout = null; + } + }; + + return debounced; } export const useMap = () => { @@ -187,20 +205,6 @@ const MapComponent: React.FC = ({ children }) => { [number, number] | undefined >(); - // 防抖更新函数 - 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; @@ -232,6 +236,7 @@ const MapComponent: React.FC = ({ children }) => { }); } }; + const setPipeData = (newData: any[]) => { const uniqueNewData = newData.filter((item) => { if (!item || !item.id) return false; @@ -263,6 +268,28 @@ const MapComponent: React.FC = ({ children }) => { }); } }; + + const debouncedUpdateDataRef = useRef void> | null>( + null, + ); + + useEffect(() => { + debouncedUpdateDataRef.current = debounce(() => { + if (tileJunctionDataBuffer.current.length > 0) { + setJunctionData(tileJunctionDataBuffer.current); + tileJunctionDataBuffer.current = []; + } + if (tilePipeDataBuffer.current.length > 0) { + setPipeData(tilePipeDataBuffer.current); + tilePipeDataBuffer.current = []; + } + }, 100); + + return () => { + debouncedUpdateDataRef.current?.cancel(); + debouncedUpdateDataRef.current = null; + }; + }, []); // 配置地图数据源、图层和样式 const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE; // 定义 SCADA 图层的样式函数,根据 type 字段选择不同图标 @@ -520,7 +547,7 @@ const MapComponent: React.FC = ({ children }) => { uniqueData.forEach((item) => tileJunctionDataBuffer.current.push(item), ); - debouncedUpdateData.current(); + debouncedUpdateDataRef.current?.(); } } } catch (error) { @@ -600,7 +627,7 @@ const MapComponent: React.FC = ({ children }) => { const uniqueData = Array.from(data.values()); if (uniqueData.length > 0) { uniqueData.forEach((item) => tilePipeDataBuffer.current.push(item)); - debouncedUpdateData.current(); + debouncedUpdateDataRef.current?.(); } } } catch (error) { diff --git a/src/contexts/color-mode/index.tsx b/src/contexts/color-mode/index.tsx index bfd7936..9027599 100644 --- a/src/contexts/color-mode/index.tsx +++ b/src/contexts/color-mode/index.tsx @@ -29,26 +29,20 @@ type ColorModeContextProviderProps = { export const ColorModeContextProvider: React.FC< PropsWithChildren > = ({ children, defaultMode }) => { - const [isMounted, setIsMounted] = useState(false); - const [mode, setMode] = useState(defaultMode || "light"); - - useEffect(() => { - setIsMounted(true); - }, []); - const systemTheme = useMediaQuery(`(prefers-color-scheme: dark)`); - - useEffect(() => { - if (isMounted) { - const theme = Cookies.get("theme") || (systemTheme ? "dark" : "light"); - setMode(theme); + const [storedMode, setStoredMode] = useState(() => { + if (typeof window === "undefined") { + return defaultMode ?? null; } - }, [isMounted, systemTheme]); + + return Cookies.get("theme") || defaultMode || null; + }); + const mode = storedMode || (systemTheme ? "dark" : "light"); const toggleTheme = () => { const nextTheme = mode === "light" ? "dark" : "light"; - setMode(nextTheme); + setStoredMode(nextTheme); Cookies.set("theme", nextTheme); };