From 0e82c080df1bc63c3708b71b57ed3a9d982b1925 Mon Sep 17 00:00:00 2001 From: Huarch Date: Fri, 29 May 2026 10:02:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=87=AA=E5=8A=A8=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E6=A0=B7=E5=BC=8F=E5=8A=9F=E8=83=BD=EF=BC=9B=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E8=AE=A1=E7=AE=97=E5=90=8E=E8=87=AA=E5=8A=A8=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E9=BB=98=E8=AE=A4=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../olmap/core/Controls/StyleEditorForm.tsx | 590 +++++ .../olmap/core/Controls/StyleEditorPanel.tsx | 1968 +---------------- .../olmap/core/Controls/Timeline.tsx | 2 + .../olmap/core/Controls/Toolbar.tsx | 81 +- .../olmap/core/Controls/styleEditorPresets.ts | 200 ++ .../olmap/core/Controls/styleEditorTypes.ts | 62 + .../olmap/core/Controls/styleEditorUtils.ts | 348 +++ .../olmap/core/Controls/useStyleEditor.ts | 944 ++++++++ src/components/olmap/core/MapComponent.tsx | 8 +- 9 files changed, 2201 insertions(+), 2002 deletions(-) create mode 100644 src/components/olmap/core/Controls/StyleEditorForm.tsx create mode 100644 src/components/olmap/core/Controls/styleEditorPresets.ts create mode 100644 src/components/olmap/core/Controls/styleEditorTypes.ts create mode 100644 src/components/olmap/core/Controls/styleEditorUtils.ts create mode 100644 src/components/olmap/core/Controls/useStyleEditor.ts diff --git a/src/components/olmap/core/Controls/StyleEditorForm.tsx b/src/components/olmap/core/Controls/StyleEditorForm.tsx new file mode 100644 index 0000000..eabf1ea --- /dev/null +++ b/src/components/olmap/core/Controls/StyleEditorForm.tsx @@ -0,0 +1,590 @@ +import ApplyIcon from "@mui/icons-material/Check"; +import ColorLensIcon from "@mui/icons-material/ColorLens"; +import ResetIcon from "@mui/icons-material/Refresh"; +import { + Box, + Button, + Checkbox, + FormControl, + FormControlLabel, + InputLabel, + MenuItem, + Select, + Slider, + TextField, + Typography, +} from "@mui/material"; +import React from "react"; + +import { + CLASSIFICATION_METHODS, + COLOR_TYPE_OPTIONS, + GRADIENT_PALETTES, + RAINBOW_PALETTES, + SINGLE_COLOR_PALETTES, +} from "./styleEditorPresets"; +import { StyleEditorFormProps } from "./styleEditorTypes"; +import { + getSizePreviewColors, + hexToRgba, + resolveStyleColors, + rgbaToHex, +} from "./styleEditorUtils"; + +const StyleEditorForm: React.FC = ({ + renderLayers, + selectedRenderLayer, + styleConfig, + setStyleConfig, + availableProperties, + onLayerChange, + onPropertyChange, + onClassificationMethodChange, + onSegmentsChange, + onCustomBreakChange, + onCustomBreakBlur, + onColorTypeChange, + onApply, + onReset, +}) => { + const renderColorSetting = () => { + if (styleConfig.colorType === "single") { + return ( + + 单一色方案 + + + ); + } + + if (styleConfig.colorType === "gradient") { + return ( + + 渐进色方案 + + + ); + } + + if (styleConfig.colorType === "rainbow") { + return ( + + 离散彩虹方案 + + + ); + } + + if (styleConfig.colorType === "custom") { + return ( + + + 自定义颜色 + + + {Array.from({ length: styleConfig.segments }).map((_, index) => { + const color = styleConfig.customColors?.[index] || "rgba(0,0,0,1)"; + return ( + + + 分段{index + 1} + + { + const nextColor = hexToRgba(e.target.value); + setStyleConfig((prev) => { + const nextColors = [...(prev.customColors || [])]; + while (nextColors.length < prev.segments) { + nextColors.push("rgba(0,0,0,1)"); + } + nextColors[index] = nextColor; + return { ...prev, customColors: nextColors }; + }); + }} + style={{ + width: "100%", + height: "32px", + cursor: "pointer", + border: "1px solid #ccc", + borderRadius: "4px", + }} + /> + + ); + })} + + + ); + } + + return null; + }; + + const renderSizeSetting = () => { + const previewColors = getSizePreviewColors(styleConfig); + + if (selectedRenderLayer?.get("type") === "point") { + return ( + + + 点大小范围: {styleConfig.minSize} - {styleConfig.maxSize} 像素 + + + + + 最小值 + + + setStyleConfig((prev) => ({ ...prev, minSize: value as number })) + } + min={2} + max={8} + step={1} + size="small" + /> + + + + 最大值 + + + setStyleConfig((prev) => ({ ...prev, maxSize: value as number })) + } + min={10} + max={16} + step={1} + size="small" + /> + + + + 预览: + + + + + + ); + } + + if (selectedRenderLayer?.get("type") === "linestring") { + return ( + + + setStyleConfig((prev) => ({ + ...prev, + adjustWidthByProperty: e.target.checked, + })) + } + disabled={styleConfig.colorType === "single"} + /> + } + label="根据数值分段调整线条宽度" + /> + {styleConfig.adjustWidthByProperty ? ( + <> + + 线条宽度范围: {styleConfig.minStrokeWidth} - {styleConfig.maxStrokeWidth} + px + + + + + 最小值 + + + setStyleConfig((prev) => ({ + ...prev, + minStrokeWidth: value as number, + })) + } + min={1} + max={4} + step={0.5} + size="small" + /> + + + + 最大值 + + + setStyleConfig((prev) => ({ + ...prev, + maxStrokeWidth: value as number, + })) + } + min={6} + max={12} + step={0.5} + size="small" + /> + + + + 预览: + + + + + + ) : ( + <> + + 固定线条宽度: {styleConfig.fixedStrokeWidth}px + + + setStyleConfig((prev) => ({ + ...prev, + fixedStrokeWidth: value as number, + })) + } + min={1} + max={10} + step={0.5} + size="small" + /> + + 预览: + + + + )} + + ); + } + + return null; + }; + + return ( +
+ + 选择图层 + + + + + 分级属性 + + + + + 分类方法 + + + + + 分类数量: {styleConfig.segments} + onSegmentsChange(value as number)} + min={2} + max={10} + step={1} + marks + size="small" + /> + + + {styleConfig.classificationMethod === "custom_breaks" && ( + + + 手动设置区间阈值(按升序填写,最小值 {">="} 0) + + + {Array.from({ length: styleConfig.segments }).map((_, index) => ( + onCustomBreakChange(index, e.target.value)} + onBlur={onCustomBreakBlur} + /> + ))} + + + )} + + + + + 颜色方案 + + + {renderColorSetting()} + + + {renderSizeSetting()} + + + + 透明度: {(styleConfig.opacity * 100).toFixed(0)}% + + + setStyleConfig((prev) => ({ ...prev, opacity: value as number })) + } + min={0.1} + max={1} + step={0.05} + size="small" + /> + + + + setStyleConfig((prev) => ({ ...prev, showId: e.target.checked })) + } + /> + } + label="显示 ID(缩放 >=15 级时显示)" + /> + + + setStyleConfig((prev) => ({ ...prev, showLabels: e.target.checked })) + } + /> + } + label="显示属性(缩放 >=15 级时显示)" + /> + +
+ + + + + +
+ ); +}; + +export default StyleEditorForm; diff --git a/src/components/olmap/core/Controls/StyleEditorPanel.tsx b/src/components/olmap/core/Controls/StyleEditorPanel.tsx index 8336655..946cd3a 100644 --- a/src/components/olmap/core/Controls/StyleEditorPanel.tsx +++ b/src/components/olmap/core/Controls/StyleEditorPanel.tsx @@ -1,1941 +1,59 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import React from "react"; -// 导入Material-UI图标和组件 -import ColorLensIcon from "@mui/icons-material/ColorLens"; -import ApplyIcon from "@mui/icons-material/Check"; -import ResetIcon from "@mui/icons-material/Refresh"; -import { - Select, - MenuItem, - FormControl, - InputLabel, - Slider, - Typography, - Button, - TextField, - Box, - Checkbox, - FormControlLabel, -} from "@mui/material"; - -// 导入OpenLayers样式相关模块 -import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; -import VectorTileSource from "ol/source/VectorTile"; -import { useData, useMap } from "../MapComponent"; - -import { LegendStyleConfig } from "./StyleLegend"; -import { FlatStyleLike } from "ol/style/flat"; - -import { calculateClassification } from "@utils/breaks_classification"; -import { parseColor } from "@utils/parseColor"; -import { VectorTile } from "ol"; -import type { Map as OlMap } from "ol"; -import { useNotification } from "@refinedev/core"; -import { config } from "@/config/config"; - -interface StyleConfig { - property: string; - classificationMethod: string; // 分类方法 - segments: number; - minSize: number; // 最小点尺寸 - maxSize: number; // 最大点尺寸 - minStrokeWidth: number; // 最小线宽 - maxStrokeWidth: number; // 最大线宽 - fixedStrokeWidth: number; // 固定线宽 - colorType: string; // 颜色类型 - singlePaletteIndex: number; - gradientPaletteIndex: number; - rainbowPaletteIndex: number; - showLabels: boolean; - showId: boolean; - opacity: number; - adjustWidthByProperty: boolean; // 是否根据属性调整线条宽度 - customBreaks?: number[]; // 自定义断点(用于 custom_breaks) - customColors?: string[]; // 自定义颜色(用于 colorType="custom") -} - -// 图层样式状态接口 -export interface LayerStyleState { - layerId: string; - layerName: string; - styleConfig: StyleConfig; - legendConfig: LegendStyleConfig; - isActive: boolean; -} - -// StyleEditorPanel 组件 Props 接口 -interface StyleEditorPanelProps { - layerStyleStates: LayerStyleState[]; - setLayerStyleStates: React.Dispatch>; -} - -// 预设颜色方案 -const SINGLE_COLOR_PALETTES = [ - { - color: "rgba(51, 153, 204, 1)", - }, - { - color: "rgba(255, 138, 92, 1)", - }, - { - color: "rgba(204, 51, 51, 1)", - }, - { - color: "rgba(255, 235, 59, 1)", - }, - { - color: "rgba(44, 160, 44, 1)", - }, - { - color: "rgba(227, 119, 194, 1)", - }, - { - color: "rgba(148, 103, 189, 1)", - }, -]; -const GRADIENT_PALETTES = [ - { - name: "蓝-红", - start: "rgba(51, 153, 204, 1)", - end: "rgba(204, 51, 51, 1)", - }, - { - name: "黄-绿", - start: "rgba(255, 235, 59, 1)", - end: "rgba(44, 160, 44, 1)", - }, - { - name: "粉-紫", - start: "rgba(227, 119, 194, 1)", - end: "rgba(148, 103, 189, 1)", - }, -]; -// 离散彩虹色系 - 提供高区分度的颜色 -const RAINBOW_PALETTES = [ - { - name: "正向彩虹", - colors: [ - "rgba(255, 0, 0, 1)", // 红 #FF0000 - "rgba(255, 127, 0, 1)", // 橙 #FF7F00 - "rgba(255, 215, 0, 1)", // 金黄 #FFD700 - "rgba(199, 224, 0, 1)", // 黄绿 #C7E000 - "rgba(76, 175, 80, 1)", // 中绿 #4CAF50 - "rgba(0, 158, 115, 1)", // 青绿/翡翠 #009E73 - "rgba(0, 188, 212, 1)", // 青/青色 #00BCD4 - "rgba(33, 150, 243, 1)", // 天蓝 #2196F3 - "rgba(63, 81, 181, 1)", // 靛青 #3F51B5 - "rgba(142, 68, 173, 1)", // 紫 #8E44AD - ], - }, - { - name: "反向彩虹", - colors: [ - "rgba(142, 68, 173, 1)", // 紫 #8E44AD - "rgba(63, 81, 181, 1)", // 靛青 #3F51B5 - "rgba(33, 150, 243, 1)", // 天蓝 #2196F3 - "rgba(0, 188, 212, 1)", // 青/青色 #00BCD4 - "rgba(0, 158, 115, 1)", // 青绿/翡翠 #009E73 - "rgba(76, 175, 80, 1)", // 中绿 #4CAF50 - "rgba(199, 224, 0, 1)", // 黄绿 #C7E000 - "rgba(255, 215, 0, 1)", // 金黄 #FFD700 - "rgba(255, 127, 0, 1)", // 橙 #FF7F00 - "rgba(255, 0, 0, 1)", // 红 #FF0000 - ], - }, -]; - -// 预设分类方法 -const CLASSIFICATION_METHODS = [ - { name: "优雅分段", value: "pretty_breaks" }, - // 浏览器中实现Jenks算法性能较差,暂时移除 - // { name: "自然间断", value: "jenks_optimized" }, - { name: "自定义", value: "custom_breaks" }, -]; - -const rgbaToHex = (rgba: string) => { - try { - const c = parseColor(rgba); - const toHex = (n: number) => { - const hex = Math.round(n).toString(16); - return hex.length === 1 ? "0" + hex : hex; - }; - return `#${toHex(c.r)}${toHex(c.g)}${toHex(c.b)}`; - } catch (e) { - return "#000000"; - } -}; - -const hexToRgba = (hex: string) => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result - ? `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt( - result[3], - 16 - )}, 1)` - : "rgba(0, 0, 0, 1)"; -}; +import StyleEditorForm from "./StyleEditorForm"; +import { createDefaultLayerStyleState, createDefaultLayerStyleStates } from "./styleEditorPresets"; +import { useStyleEditor } from "./useStyleEditor"; +import { LayerStyleState, StyleConfig, StyleEditorPanelProps } from "./styleEditorTypes"; const StyleEditorPanel: React.FC = ({ layerStyleStates, setLayerStyleStates, }) => { - const map = useMap(); - const data = useData(); - const currentJunctionCalData = data?.currentJunctionCalData; - const currentPipeCalData = data?.currentPipeCalData; - const compareJunctionCalData = data?.compareJunctionCalData; - const comparePipeCalData = data?.comparePipeCalData; - const compareMap = data?.compareMap; - const activeMaps = useMemo( - () => (data?.maps?.length ? data.maps : map ? [map] : []), - [data?.maps, map] - ); - 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]; - - const { open } = useNotification(); - - 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] = - useState(); - const [styleConfig, setStyleConfig] = useState({ - property: "", - classificationMethod: "pretty_breaks", - segments: 5, - minSize: 4, - maxSize: 12, - minStrokeWidth: 2, - maxStrokeWidth: 6, - fixedStrokeWidth: 3, - colorType: "single", - singlePaletteIndex: 0, - gradientPaletteIndex: 0, - rainbowPaletteIndex: 0, - showLabels: false, - showId: false, - opacity: 0.9, - adjustWidthByProperty: true, - customBreaks: [], - customColors: [], + const { + isReady, + renderLayers, + selectedRenderLayer, + styleConfig, + setStyleConfig, + availableProperties, + handleLayerChange, + handlePropertyChange, + handleClassificationMethodChange, + handleSegmentsChange, + handleCustomBreakChange, + handleCustomBreakBlur, + handleColorTypeChange, + handleApply, + handleReset, + } = useStyleEditor({ + layerStyleStates, + setLayerStyleStates, }); - const getRenderLayersById = useCallback( - (layerId: string) => - activeMaps.flatMap((targetMap) => - targetMap - .getAllLayers() - .filter((layer) => layer.get("value") === layerId) - .filter((layer): layer is WebGLVectorTileLayer => layer instanceof WebGLVectorTileLayer) - ), - [activeMaps] - ); - - const getMapKey = useCallback((targetMap: OlMap, layerId: string) => { - const mapUid = (targetMap as unknown as { ol_uid?: string }).ol_uid || "map"; - return `${mapUid}:${layerId}`; - }, []); - - const getDataForMap = useCallback( - (targetMap: OlMap, layerId: string) => { - if (layerId === "junctions") { - return targetMap === compareMap - ? compareJunctionCalData || [] - : currentJunctionCalData || []; - } - if (layerId === "pipes") { - return targetMap === compareMap - ? comparePipeCalData || [] - : currentPipeCalData || []; - } - return []; - }, - [ - compareJunctionCalData, - compareMap, - comparePipeCalData, - currentJunctionCalData, - currentPipeCalData, - ] - ); - - 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[] => { - const { start, end } = - GRADIENT_PALETTES[styleConfig.gradientPaletteIndex]; - const colors: string[] = []; - const startColor = parseColor(start); - const endColor = parseColor(end); - - for (let i = 0; i < segments; i++) { - const ratio = segments > 1 ? i / (segments - 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)`); - } - return colors; - }, - [styleConfig.gradientPaletteIndex] - ); - - // 根据分段数生成彩虹色 - const generateRainbowColors = useCallback( - (segments: number): string[] => { - const baseColors = - RAINBOW_PALETTES[styleConfig.rainbowPaletteIndex].colors; - // 严格按顺序返回 N 个颜色 - return Array.from( - { length: segments }, - (_, i) => baseColors[i % baseColors.length] - ); - }, - [styleConfig.rainbowPaletteIndex] - ); - // 保存当前图层的样式状态 - const saveLayerStyle = ( - layerId?: string, - newLegendConfig?: LegendStyleConfig, - overrideStyleConfig?: StyleConfig - ) => { - const currentStyleConfig = overrideStyleConfig || styleConfig; - - 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; - } - - return [...prev, newStyleState]; - }); - }; - // 设置分类样式参数,触发样式应用 - const setStyleState = () => { - if (!selectedRenderLayer) return; - const layerId = selectedRenderLayer.get("value"); - const property = styleConfig.property; - if (layerId !== undefined && property !== undefined) { - // 验证自定义断点设置 - if (styleConfig.classificationMethod === "custom_breaks") { - const expected = styleConfig.segments; - const custom = styleConfig.customBreaks || []; - if ( - custom.length !== expected || - custom.some((v) => v === undefined || v === null || isNaN(v)) - ) { - open?.({ - type: "error", - message: `请设置 ${expected} 个有效的自定义阈值(数字)`, - }); - return; - } - if (custom.some((v) => v < 0)) { - open?.({ type: "error", message: "自定义阈值必须大于等于 0" }); - return; - } - // 升序排序 - setStyleConfig((prev) => ({ - ...prev, - customBreaks: (prev.customBreaks || []) - .slice(0, expected) - .sort((a, b) => a - b), - })); - } - // 更新文字标签设置 - if (layerId === "junctions") { - setJunctionText && setJunctionText(property); - setShowJunctionTextLayer && - setShowJunctionTextLayer(styleConfig.showLabels); - setShowJunctionId && setShowJunctionId(styleConfig.showId); - setApplyJunctionStyle(true); - if (property === "pressure" && setContourLayerAvailable) { - setContourLayerAvailable(true); - } - saveLayerStyle(layerId); - open?.({ - type: "success", - message: "节点图层样式设置成功,等待数据更新。", - }); - } - if (layerId === "pipes") { - setPipeText && setPipeText(property); - setShowPipeTextLayer && setShowPipeTextLayer(styleConfig.showLabels); - setShowPipeId && setShowPipeId(styleConfig.showId); - setApplyPipeStyle(true); - setWaterflowLayerAvailable && setWaterflowLayerAvailable(true); - saveLayerStyle(layerId); - open?.({ - type: "success", - message: "管道图层样式设置成功,等待数据更新。", - }); - } - // 触发样式更新 - setStyleUpdateTrigger((prev) => prev + 1); - } - }; - // 计算分类样式,并应用到对应图层 - const applyClassificationStyle = ( - layerType: "junctions" | "pipes", - styleConfig: any - ) => { - const isElevation = - layerType === "junctions" && styleConfig.property === "elevation"; - const isDiameter = - layerType === "pipes" && styleConfig.property === "diameter"; - const isUnitHeadloss = - layerType === "pipes" && styleConfig.property === "unit_headloss"; - - if ( - layerType === "junctions" && - ((currentJunctionCalData && currentJunctionCalData.length > 0) || - (isElevation && elevationRange)) - ) { - // 应用节点样式 - let junctionStyleConfigState = layerStyleStates.find( - (s) => s.layerId === "junctions" - ); - - // 更新节点数据属性 - const segments = junctionStyleConfigState?.styleConfig.segments ?? 5; - let breaks: number[] = []; - - const dataValues = - isElevation && elevationRange - ? [elevationRange[0], elevationRange[1]] - : currentJunctionCalData?.map((d: any) => d.value) || []; - - if (dataValues.length === 0) return; - - if ( - junctionStyleConfigState?.styleConfig.classificationMethod === - "custom_breaks" - ) { - // 使用自定义断点(保证为 segments 个断点,按升序) - const desired = segments; - breaks = ( - junctionStyleConfigState?.styleConfig.customBreaks || [] - ).slice(0, desired); - breaks.sort((a, b) => a - b); - // 过滤出 >= 0 - breaks = breaks.filter((v) => v >= 0); - // 如果不足则补齐最后一个值 - while (breaks.length < desired) - breaks.push(breaks[breaks.length - 1] ?? 0); - } else { - const calc = calculateClassification( - dataValues, - segments, - styleConfig.classificationMethod - ); - breaks = calc; - } - if (breaks.length === 0) { - console.warn("计算的 breaks 为空,无法应用样式"); - return; - } - // 计算最大最小值,判断是否包含并插入 breaks - const min_val = Math.max( - dataValues.reduce((min, val) => Math.min(min, val), Infinity), - 0 - ); - const max_val = dataValues.reduce( - (max, val) => Math.max(max, val), - -Infinity - ); - if (breaks.includes(min_val) === false) { - breaks.push(min_val); - breaks.sort((a, b) => a - b); - } - if (breaks.includes(max_val) === false) { - breaks.push(max_val); - breaks.sort((a, b) => a - b); - } - if (junctionStyleConfigState) { - applyLayerStyle(junctionStyleConfigState, breaks); - applyContourLayerStyle(junctionStyleConfigState, breaks); - } - } else if ( - layerType === "pipes" && - ((currentPipeCalData && currentPipeCalData.length > 0) || - (isDiameter && diameterRange) || - isUnitHeadloss) - ) { - // 应用管道样式 - let pipeStyleConfigState = layerStyleStates.find( - (s) => s.layerId === "pipes" - ); - // 更新管道数据属性 - const segments = pipeStyleConfigState?.styleConfig.segments ?? 5; - let breaks: number[] = []; - - const dataValues = - isDiameter && diameterRange - ? [diameterRange[0], diameterRange[1]] - : isUnitHeadloss - ? [unitHeadlossRange[0], unitHeadlossRange[1]] - : currentPipeCalData?.map((d: any) => d.value) || []; - - if (dataValues.length === 0) return; - - if ( - pipeStyleConfigState?.styleConfig.classificationMethod === - "custom_breaks" - ) { - // 使用自定义断点(保证为 segments 个断点,按升序) - const desired = segments; - breaks = (pipeStyleConfigState?.styleConfig.customBreaks || []).slice( - 0, - desired - ); - breaks.sort((a, b) => a - b); - breaks = breaks.filter((v) => v >= 0); - while (breaks.length < desired) - breaks.push(breaks[breaks.length - 1] ?? 0); - } else { - const calc = calculateClassification( - dataValues, - segments, - styleConfig.classificationMethod - ); - breaks = calc; - } - if (breaks.length === 0) { - console.warn("计算的 breaks 为空,无法应用样式"); - return; - } - // 计算最大最小值,判断是否包含并插入 breaks - const min_val = Math.max( - dataValues.reduce((min, val) => Math.min(min, val), Infinity), - 0 - ); - const max_val = dataValues.reduce( - (max, val) => Math.max(max, val), - -Infinity - ); - if (breaks.includes(min_val) === false) { - breaks.push(min_val); - breaks.sort((a, b) => a - b); - } - if (breaks.includes(max_val) === false) { - breaks.push(max_val); - breaks.sort((a, b) => a - b); - } - if (pipeStyleConfigState) applyLayerStyle(pipeStyleConfigState, breaks); - } - }; - // 应用样式函数,传入 breaks 数据 - const applyLayerStyle = ( - layerStyleConfig: LayerStyleState, - breaks?: number[] - ) => { - // 使用传入的 breaks 数据 - if (!breaks || breaks.length === 0) { - console.warn("没有有效的 breaks 数据"); - return; - } - const styleConfig = layerStyleConfig.styleConfig; - const targetLayers = getRenderLayersById(layerStyleConfig.layerId); - const renderLayer = targetLayers[0]; - if (!renderLayer || !styleConfig?.property) return; - const layerType: string = renderLayer.get("type"); - - const breaksLength = breaks.length; - // 根据 breaks 计算每个分段的颜色,线条粗细 - const colors: string[] = - styleConfig.colorType === "single" - ? // 单一色重复多次 - Array.from({ length: breaksLength }, () => { - return SINGLE_COLOR_PALETTES[styleConfig.singlePaletteIndex].color; - }) - : styleConfig.colorType === "gradient" - ? generateGradientColors(breaksLength) - : styleConfig.colorType === "rainbow" - ? generateRainbowColors(breaksLength) - : (() => { - // 自定义颜色 - const custom = styleConfig.customColors || []; - // 如果自定义颜色数量不足,用反向彩虹色填充 - const result = [...custom]; - const reverseRainbowColors = RAINBOW_PALETTES[1].colors; - while (result.length < breaksLength) { - result.push( - reverseRainbowColors[ - (result.length - custom.length) % reverseRainbowColors.length - ] - ); - } - return result.slice(0, breaksLength); - })(); - // 计算每个分段的线条粗细和点大小 - const dimensions: number[] = - layerType === "linestring" - ? styleConfig.adjustWidthByProperty - ? Array.from({ length: breaksLength }, (_, i) => { - const ratio = i / (breaksLength - 1); - return ( - styleConfig.minStrokeWidth + - (styleConfig.maxStrokeWidth - styleConfig.minStrokeWidth) * - ratio - ); - }) - : Array.from( - { length: breaksLength }, - () => styleConfig.fixedStrokeWidth - ) // 使用固定宽度 - : Array.from({ length: breaksLength }, (_, i) => { - const ratio = i / (breaksLength - 1); - return ( - styleConfig.minSize + - (styleConfig.maxSize - styleConfig.minSize) * ratio - ); - }); - - // 动态生成颜色条件表达式 - const generateColorConditions = (property: string): any[] => { - const conditions: any[] = ["case"]; - for (let i = 1; i < breaks.length; i++) { - // 添加条件:属性值 <= 当前断点 - if (property === "unit_headloss") { - conditions.push([ - "<=", - ["/", ["get", "unit_headloss"], ["/", ["get", "length"], 1000]], - breaks[i], - ]); - } else { - conditions.push(["<=", ["get", property], breaks[i]]); - } - // 添加对应的颜色值 - const colorObj = parseColor(colors[i - 1]); - const color = `rgba(${colorObj.r}, ${colorObj.g}, ${colorObj.b}, ${styleConfig.opacity})`; - conditions.push(color); - } - const colorObj = parseColor(colors[0]); - const color = `rgba(${colorObj.r}, ${colorObj.g}, ${colorObj.b}, ${styleConfig.opacity})`; - // 添加默认值(首个颜色) - conditions.push(color); - return conditions; - }; - // 动态生成尺寸条件表达式 - const generateDimensionConditions = (property: string): any[] => { - const conditions: any[] = ["case"]; - for (let i = 0; i < breaks.length; i++) { - // 单独处理 unit_headloss 属性 - if (property === "unit_headloss") { - conditions.push([ - "<=", - ["/", ["get", "headloss"], ["get", "length"]], - breaks[i], - ]); - } else { - conditions.push(["<=", ["get", property], breaks[i]]); - } - conditions.push(dimensions[i]); - } - conditions.push(dimensions[dimensions.length - 1]); - return conditions; - }; - const generateDimensionPointConditions = (property: string): any[] => { - const conditions: any[] = ["case"]; - for (let i = 0; i < breaks.length; i++) { - conditions.push(["<=", ["get", property], breaks[i]]); - conditions.push([ - "interpolate", - ["linear"], - ["zoom"], - 12, - 1, // 使用配置的最小尺寸 - 24, - dimensions[i], - ]); - } - conditions.push(dimensions[dimensions.length - 1]); - return conditions; - }; - // 创建基于 breaks 的动态 FlatStyle - const dynamicStyle: FlatStyleLike = {}; - - // 根据图层类型设置不同的样式属性 - if (layerType === "linestring") { - dynamicStyle["stroke-color"] = generateColorConditions( - styleConfig.property - ); - dynamicStyle["stroke-width"] = generateDimensionConditions( - styleConfig.property - ); - } else if (layerType === "point") { - dynamicStyle["circle-fill-color"] = generateColorConditions( - styleConfig.property - ); - dynamicStyle["circle-radius"] = generateDimensionPointConditions( - styleConfig.property - ); - dynamicStyle["circle-stroke-color"] = generateColorConditions( - styleConfig.property - ); - dynamicStyle["circle-stroke-width"] = 2; - } - // 应用样式到图层 - targetLayers.forEach((targetLayer) => { - targetLayer.setStyle(dynamicStyle); - }); - // 用初始化时的样式配置更新图例配置,避免覆盖已有的图例名称和属性 - const layerId = renderLayer.get("value"); - const initLayerStyleState = layerStyleStates.find( - (s) => s.layerId === layerId - ); - // 创建图例配置对象 - const legendConfig: LegendStyleConfig = { - layerName: initLayerStyleState?.layerName || `图层${layerId}`, - layerId: layerId, - property: initLayerStyleState?.legendConfig.property || "", - colors: colors, - type: layerType, - dimensions: dimensions, - breaks: breaks, - }; - // 自动保存样式状态,直接传入图例配置 - setTimeout(() => { - saveLayerStyle(renderLayer.get("value"), legendConfig, styleConfig); - }, 100); - }; - // 应用样式函数,传入 breaks 数据 - const applyContourLayerStyle = ( - layerStyleConfig: LayerStyleState, - breaks?: number[] - ) => { - // 使用传入的 breaks 数据 - if (!breaks || breaks.length === 0) { - console.warn("没有有效的 breaks 数据"); - return; - } - const styleConfig = layerStyleConfig.styleConfig; - - if (!setContours) return; - - const breaksLength = breaks.length; - // 根据 breaks 计算每个分段的颜色 - const colors: string[] = - styleConfig.colorType === "single" - ? // 单一色重复多次 - Array.from({ length: breaksLength }, () => { - return SINGLE_COLOR_PALETTES[styleConfig.singlePaletteIndex].color; - }) - : styleConfig.colorType === "gradient" - ? generateGradientColors(breaksLength) - : styleConfig.colorType === "rainbow" - ? generateRainbowColors(breaksLength) - : (() => { - // 自定义颜色 - const custom = styleConfig.customColors || []; - // 如果自定义颜色数量不足,用反向彩虹色填充 - const result = [...custom]; - const reverseRainbowColors = RAINBOW_PALETTES[1].colors; - while (result.length < breaksLength) { - result.push( - reverseRainbowColors[ - (result.length - custom.length) % reverseRainbowColors.length - ] - ); - } - return result.slice(0, breaksLength); - })(); - - // 构造 ContourLayer 所需的 contours 配置 - const contours = []; - for (let i = 0; i < breaks.length - 1; i++) { - const colorObj = parseColor(colors[i]); - contours.push({ - threshold: [breaks[i], breaks[i + 1]], - color: [ - colorObj.r, - colorObj.g, - colorObj.b, - Math.round(styleConfig.opacity * 255), - ], - strokeWidth: 0, - }); - } - // 应用样式到等值线图层 - setContours(contours); - }; - - // 重置样式 - const resetStyle = () => { - if (!selectedRenderLayer) return; - // 重置 WebGL 图层样式 - const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE; - const layerId = selectedRenderLayer.get("value"); - getRenderLayersById(layerId).forEach((targetLayer) => { - targetLayer.setStyle(defaultFlatStyle); - }); - - // 删除对应图层的样式状态,从而移除图例显示 - if (layerId !== undefined) { - setLayerStyleStates((prev) => - prev.filter((state) => state.layerId !== layerId) - ); - // 重置样式应用状态 - if (layerId === "junctions") { - setApplyJunctionStyle(false); - if (setShowJunctionTextLayer) setShowJunctionTextLayer(false); - if (setShowJunctionId) setShowJunctionId(false); - if (setJunctionText) setJunctionText(""); - setContours && setContours([]); - setContourLayerAvailable && setContourLayerAvailable(false); - } else if (layerId === "pipes") { - setApplyPipeStyle(false); - if (setShowPipeTextLayer) setShowPipeTextLayer(false); - if (setShowPipeId) setShowPipeId(false); - if (setPipeText) setPipeText(""); - setWaterflowLayerAvailable && setWaterflowLayerAvailable(false); - } - } - }; - // 更新当前 VectorTileSource 中的所有缓冲要素属性 - const updateVectorTileSource = ( - targetMap: OlMap, - layerId: string, - property: string, - data: any[] - ) => { - const vectorTileSources = targetMap - .getAllLayers() - .filter((layer) => layer.get("value") === layerId) - .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) { - if (property === "flow") { - // 特殊处理流量属性,取绝对值 - (renderFeature as any).properties_[property] = Math.abs(value); - } else { - (renderFeature as any).properties_[property] = value; - } - } - }); - }); - }); - }; - // 新增事件,监听 VectorTileSource 的 tileloadend 事件,为新增瓦片数据动态更新要素属性 - const tileLoadListenersRef = useRef< - Map void }> - >(new Map()); - - const attachVectorTileSourceLoadedEvent = ( - targetMap: OlMap, - layerId: string, - property: string, - data: any[] - ) => { - const vectorTileSource = targetMap - .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 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) { - if (property === "flow") { - // 特殊处理流量属性,取绝对值 - (renderFeature as any).properties_[property] = Math.abs(value); - } else { - (renderFeature as any).properties_[property] = value; - } - } - }); - } - } catch (error) { - console.error("Error processing tile load event:", error); - } - }; - - const listenerKey = getMapKey(targetMap, layerId); - vectorTileSource.on("tileloadend", listener); - tileLoadListenersRef.current.set(listenerKey, { - source: vectorTileSource, - listener, - }); - }; - // 新增函数:取消对应 layerId 已添加的 on 事件 - const removeVectorTileSourceLoadedEvent = useCallback( - (targetMap: OlMap, layerId: string) => { - const listenerKey = getMapKey(targetMap, layerId); - const listenerState = tileLoadListenersRef.current.get(listenerKey); - if (listenerState) { - listenerState.source.un("tileloadend", listenerState.listener); - tileLoadListenersRef.current.delete(listenerKey); - } - }, - [getMapKey] - ); - - // 监听数据变化,重新应用样式。由样式应用按钮触发,或由数据变化触发 - useEffect(() => { - // 判断此次触发是否由用户点击“应用”按钮引起 - const isUserTrigger = - styleUpdateTrigger !== prevStyleUpdateTriggerRef.current; - // 更新 prevStyleUpdateTrigger - prevStyleUpdateTriggerRef.current = styleUpdateTrigger; - - const updateJunctionStyle = () => { - const junctionStyleConfigState = layerStyleStates.find( - (s) => s.layerId === "junctions" - ); - const isElevation = - junctionStyleConfigState?.styleConfig.property === "elevation"; - - // setStyle() 会清除渲染器缓存,这是闪烁的主要原因 WebGLVectorTile.js:114-118 - // 尝试考虑使用 updateStyleVariables() 更新 - applyClassificationStyle( - "junctions", - junctionStyleConfigState?.styleConfig - ); - - if (isElevation) { - activeMaps.forEach((targetMap) => { - removeVectorTileSourceLoadedEvent(targetMap, "junctions"); - }); - return; - } - - activeMaps.forEach((targetMap) => { - const targetData = getDataForMap(targetMap, "junctions"); - if (!targetData || targetData.length === 0) return; - updateVectorTileSource(targetMap, "junctions", junctionText, targetData); - removeVectorTileSourceLoadedEvent(targetMap, "junctions"); - attachVectorTileSourceLoadedEvent( - targetMap, - "junctions", - junctionText, - targetData - ); - }); - }; - const updatePipeStyle = () => { - const pipeStyleConfigState = layerStyleStates.find( - (s) => s.layerId === "pipes" - ); - const isDiameter = - pipeStyleConfigState?.styleConfig.property === "diameter"; - - applyClassificationStyle("pipes", pipeStyleConfigState?.styleConfig); - - if (isDiameter) { - activeMaps.forEach((targetMap) => { - removeVectorTileSourceLoadedEvent(targetMap, "pipes"); - }); - return; - } - - activeMaps.forEach((targetMap) => { - const targetData = getDataForMap(targetMap, "pipes"); - if (!targetData || targetData.length === 0) return; - updateVectorTileSource(targetMap, "pipes", pipeText, targetData); - removeVectorTileSourceLoadedEvent(targetMap, "pipes"); - attachVectorTileSourceLoadedEvent( - targetMap, - "pipes", - pipeText, - targetData - ); - }); - }; - if (isUserTrigger) { - if (selectedRenderLayer?.get("value") === "junctions") { - updateJunctionStyle(); - } else if (selectedRenderLayer?.get("value") === "pipes") { - updatePipeStyle(); - } - return; - } - - const isElevation = junctionText === "elevation"; - const isDiameter = pipeText === "diameter"; - - if ( - applyJunctionStyle && - ((currentJunctionCalData && currentJunctionCalData.length > 0) || - isElevation) - ) { - updateJunctionStyle(); - } - if ( - applyPipeStyle && - ((currentPipeCalData && currentPipeCalData.length > 0) || isDiameter) - ) { - updatePipeStyle(); - } - if (!applyJunctionStyle) { - activeMaps.forEach((targetMap) => { - removeVectorTileSourceLoadedEvent(targetMap, "junctions"); - }); - } - if (!applyPipeStyle) { - activeMaps.forEach((targetMap) => { - removeVectorTileSourceLoadedEvent(targetMap, "pipes"); - }); - } - // This effect is intentionally driven by explicit style triggers and data snapshots. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - styleUpdateTrigger, - applyJunctionStyle, - applyPipeStyle, - currentJunctionCalData, - currentPipeCalData, - compareJunctionCalData, - comparePipeCalData, - activeMaps, - ]); - - useEffect(() => { - return () => { - activeMaps.forEach((targetMap) => { - removeVectorTileSourceLoadedEvent(targetMap, "junctions"); - removeVectorTileSourceLoadedEvent(targetMap, "pipes"); - }); - }; - }, [activeMaps, removeVectorTileSourceLoadedEvent]); - - // 获取地图中的矢量图层,用于选择图层选项 - useEffect(() => { - if (!map) return; - - const updateVisibleLayers = () => { - const layers = map.getAllLayers(); - // 筛选矢量瓦片图层 - const webGLVectorTileLayers = layers.filter( - (layer) => - layer.get("value") === "junctions" || layer.get("value") === "pipes" // 暂时只处理这两个图层 - ) as WebGLVectorTileLayer[]; - - setRenderLayers(webGLVectorTileLayers); - }; - - updateVisibleLayers(); - }, [map]); - if (!data) { + if (!isReady) { return
Loading...
; } - const getColorSetting = () => { - if (styleConfig.colorType === "single") { - return ( - - 单一色方案 - - - ); - } - if (styleConfig.colorType === "gradient") { - return ( - - 渐进色方案 - - - ); - } - if (styleConfig.colorType === "rainbow") { - return ( - - 离散彩虹方案 - - - ); - } - if (styleConfig.colorType === "custom") { - return ( - - - 自定义颜色 - - - {Array.from({ length: styleConfig.segments }).map((_, idx) => { - const color = - (styleConfig.customColors && styleConfig.customColors[idx]) || - "rgba(0,0,0,1)"; - return ( - - - 分段{idx + 1} - - { - const hex = e.target.value; - const newColor = hexToRgba(hex); - setStyleConfig((prev) => { - const newColors = [...(prev.customColors || [])]; - while (newColors.length < styleConfig.segments) - newColors.push("rgba(0,0,0,1)"); - newColors[idx] = newColor; - return { ...prev, customColors: newColors }; - }); - }} - style={{ - width: "100%", - height: "32px", - cursor: "pointer", - border: "1px solid #ccc", - borderRadius: "4px", - }} - /> - - ); - })} - - - ); - } - }; - // 根据不同图层的类型和颜色分类方案显示不同的大小设置 - const getSizeSetting = () => { - let colors: string[] = []; - if (styleConfig.colorType === "single") { - const color = SINGLE_COLOR_PALETTES[styleConfig.singlePaletteIndex].color; - colors = [color, color]; - } else if (styleConfig.colorType === "gradient") { - const { start, end } = - GRADIENT_PALETTES[styleConfig.gradientPaletteIndex]; - colors = [start, end]; - } else if (styleConfig.colorType === "rainbow") { - const rainbowColors = - RAINBOW_PALETTES[styleConfig.rainbowPaletteIndex].colors; - colors = [rainbowColors[0], rainbowColors[rainbowColors.length - 1]]; - } else if (styleConfig.colorType === "custom") { - const customColors = styleConfig.customColors || []; - colors = [ - customColors[0] || "rgba(0,0,0,1)", - customColors[customColors.length - 1] || "rgba(0,0,0,1)", - ]; - } - - if (selectedRenderLayer?.get("type") === "point") { - return ( - - - 点大小范围: {styleConfig.minSize} - {styleConfig.maxSize} 像素 - - - - - 最小值 - - - setStyleConfig((prev) => ({ - ...prev, - minSize: value as number, - })) - } - min={2} - max={8} - step={1} - size="small" - /> - - - - 最大值 - - - setStyleConfig((prev) => ({ - ...prev, - maxSize: value as number, - })) - } - min={10} - max={16} - step={1} - size="small" - /> - - - {/* 点大小预览 */} - - 预览: - - - - - - ); - } - if (selectedRenderLayer?.get("type") === "linestring") { - return ( - - {/* 勾选项:是否根据属性调整线条宽度 */} - - setStyleConfig((prev) => ({ - ...prev, - adjustWidthByProperty: e.target.checked, - })) - } - disabled={styleConfig.colorType === "single"} - /> - } - label="根据数值分段调整线条宽度" - /> - {styleConfig.adjustWidthByProperty && ( - <> - - 线条宽度范围: {styleConfig.minStrokeWidth} -{" "} - {styleConfig.maxStrokeWidth}px - - - - - 最小值 - - - setStyleConfig((prev) => ({ - ...prev, - minStrokeWidth: value as number, - })) - } - min={1} - max={4} - step={0.5} - size="small" - /> - - - - 最大值 - - - setStyleConfig((prev) => ({ - ...prev, - maxStrokeWidth: value as number, - })) - } - min={6} - max={12} - step={0.5} - size="small" - /> - - - {/* 线条宽度预览 */} - - 预览: - - - - - - )} - {!styleConfig.adjustWidthByProperty && ( - <> - - 固定线条宽度: {styleConfig.fixedStrokeWidth}px - - - setStyleConfig((prev) => ({ - ...prev, - fixedStrokeWidth: value as number, - })) - } - min={1} - max={10} - step={0.5} - size="small" - /> - {/* 固定宽度预览 */} - - 预览: - - - - )} - - ); - } - }; - return ( - <> -
- {/* 图层选择 */} - - 选择图层 - - - {/* 属性选择 */} - - 分级属性 - - - {/* 分类选择 */} - - 分类方法 - - - {/* 分类数量 */} - - 分类数量: {styleConfig.segments} - - setStyleConfig((prev) => { - const newSegments = value as number; - const newCustomColors = [...(prev.customColors || [])]; - if (newSegments > newCustomColors.length) { - const baseColors = RAINBOW_PALETTES[0].colors; - for (let i = newCustomColors.length; i < newSegments; i++) { - newCustomColors.push(baseColors[i % baseColors.length]); - } - } - return { - ...prev, - segments: newSegments, - customBreaks: - prev.classificationMethod === "custom_breaks" - ? getDefaultCustomBreaks(newSegments, prev.property) - : prev.customBreaks, - customColors: getDefaultCustomColors( - newSegments, - newCustomColors - ), - }; - }) - } - min={2} - max={10} - step={1} - marks - size="small" - /> - - {/* 自定义分类:手动填写区间分段(仅当选择自定义方式时显示) */} - {styleConfig.classificationMethod === "custom_breaks" && ( - - - 手动设置区间阈值(按升序填写,最小值 {">="} 0) - - - {Array.from({ length: styleConfig.segments }).map((_, idx) => ( - { - const v = parseFloat(e.target.value); - setStyleConfig((prev) => { - const prevBreaks = prev.customBreaks - ? [...prev.customBreaks] - : []; - // 保证长度 - while (prevBreaks.length < styleConfig.segments) - prevBreaks.push(0); - prevBreaks[idx] = isNaN(v) ? 0 : Math.max(0, v); - return { ...prev, customBreaks: prevBreaks }; - }); - }} - onBlur={() => { - // on blur 保证升序 - setStyleConfig((prev) => { - const prevBreaks = (prev.customBreaks || []).slice( - 0, - styleConfig.segments + 1 - ); - prevBreaks.sort((a, b) => a - b); - return { ...prev, customBreaks: prevBreaks }; - }); - }} - /> - ))} - - - )} - {/* 颜色方案 */} - - - - 颜色方案 - - - {getColorSetting()} - - - {/* 大小设置 */} - {getSizeSetting()} - - {/* 透明度设置 */} - - - 透明度: {(styleConfig.opacity * 100).toFixed(0)}% - - - setStyleConfig((prev) => ({ - ...prev, - opacity: value as number, - })) - } - min={0.1} - max={1} - step={0.05} - size="small" - /> - - - {/* 是否显示ID */} - - setStyleConfig((prev) => ({ - ...prev, - showId: e.target.checked, - })) - } - /> - } - label="显示 ID(缩放 >=15 级时显示)" - /> - - {/* 是否显示属性文字 */} - - setStyleConfig((prev) => ({ - ...prev, - showLabels: e.target.checked, - })) - } - /> - } - label="显示属性(缩放 >=15 级时显示)" - /> -
- {/* 操作按钮 */} - - - - -
- + ); }; export default StyleEditorPanel; +export type { LayerStyleState, StyleConfig } from "./styleEditorTypes"; +export { createDefaultLayerStyleState, createDefaultLayerStyleStates }; diff --git a/src/components/olmap/core/Controls/Timeline.tsx b/src/components/olmap/core/Controls/Timeline.tsx index 6bfbfce..41b1c3f 100644 --- a/src/components/olmap/core/Controls/Timeline.tsx +++ b/src/components/olmap/core/Controls/Timeline.tsx @@ -85,6 +85,7 @@ const Timeline: React.FC = ({ const isCompareMode = data?.isCompareMode ?? false; const junctionText = data?.junctionText ?? ""; const pipeText = data?.pipeText ?? ""; + const setForceStyleAutoApplyVersion = data?.setForceStyleAutoApplyVersion; const { open } = useNotification(); const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(15000); // 毫秒 @@ -657,6 +658,7 @@ const Timeline: React.FC = ({ }); // 清空当天当前时刻及之后的缓存并重新获取数据 clearCacheAndRefetch(calculationDate, calculationTime); + setForceStyleAutoApplyVersion?.((prev) => prev + 1); } else { open?.({ type: "error", diff --git a/src/components/olmap/core/Controls/Toolbar.tsx b/src/components/olmap/core/Controls/Toolbar.tsx index 03f38f7..333434e 100644 --- a/src/components/olmap/core/Controls/Toolbar.tsx +++ b/src/components/olmap/core/Controls/Toolbar.tsx @@ -14,7 +14,8 @@ import VectorLayer from "ol/layer/Vector"; import { Style, Stroke, Fill, Circle } from "ol/style"; import Feature from "ol/Feature"; import StyleEditorPanel from "./StyleEditorPanel"; -import { LayerStyleState } from "./StyleEditorPanel"; +import { createDefaultLayerStyleStates } from "./styleEditorPresets"; +import { LayerStyleState } from "./styleEditorTypes"; import StyleLegend from "./StyleLegend"; // 引入图例组件 import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; import { useNotification } from "@refinedev/core"; @@ -90,81 +91,9 @@ const Toolbar: React.FC = ({ }); // 样式状态管理 - 在 Toolbar 中管理,带有默认样式 - const [layerStyleStates, setLayerStyleStates] = useState([ - { - isActive: false, // 默认不激活,不显示图例 - layerId: "junctions", - layerName: "节点", - styleConfig: { - property: "pressure", - classificationMethod: "custom_breaks", - customBreaks: [16, 18, 20, 22, 24, 26], - customColors: [ - "rgba(255, 0, 0, 1)", - "rgba(255, 127, 0, 1)", - "rgba(255, 215, 0, 1)", - "rgba(199, 224, 0, 1)", - "rgba(76, 175, 80, 1)", - "rgba(0, 158, 115, 1)", - ], - segments: 6, - minSize: 4, - maxSize: 12, - minStrokeWidth: 2, - maxStrokeWidth: 8, - fixedStrokeWidth: 3, - colorType: "rainbow", - singlePaletteIndex: 0, - gradientPaletteIndex: 0, - rainbowPaletteIndex: 0, - showLabels: false, - showId: false, - opacity: 0.9, - adjustWidthByProperty: true, - }, - legendConfig: { - layerId: "junctions", - layerName: "节点", - property: "压力", // 暂时为空,等计算后更新 - colors: [], - type: "point", - dimensions: [], - breaks: [], - }, - }, - { - isActive: false, // 默认不激活,不显示图例 - layerId: "pipes", - layerName: "管道", - styleConfig: { - property: "flow", - classificationMethod: "pretty_breaks", - segments: 6, - minSize: 4, - maxSize: 12, - minStrokeWidth: 2, - maxStrokeWidth: 8, - fixedStrokeWidth: 3, - colorType: "gradient", - singlePaletteIndex: 0, - gradientPaletteIndex: 0, - rainbowPaletteIndex: 0, - showLabels: false, - showId: false, - opacity: 0.9, - adjustWidthByProperty: true, - }, - legendConfig: { - layerId: "pipes", - layerName: "管道", - property: "流量", // 暂时为空,等计算后更新 - colors: [], - type: "linestring", - dimensions: [], - breaks: [], - }, - }, - ]); + const [layerStyleStates, setLayerStyleStates] = useState( + () => createDefaultLayerStyleStates() + ); // 计算激活的图例配置 const activeLegendConfigs = layerStyleStates diff --git a/src/components/olmap/core/Controls/styleEditorPresets.ts b/src/components/olmap/core/Controls/styleEditorPresets.ts new file mode 100644 index 0000000..fcda8b9 --- /dev/null +++ b/src/components/olmap/core/Controls/styleEditorPresets.ts @@ -0,0 +1,200 @@ +import { LayerStyleState, StyleConfig, DefaultLayerStyleId } from "./styleEditorTypes"; + +export const SINGLE_COLOR_PALETTES = [ + { color: "rgba(51, 153, 204, 1)" }, + { color: "rgba(255, 138, 92, 1)" }, + { color: "rgba(204, 51, 51, 1)" }, + { color: "rgba(255, 235, 59, 1)" }, + { color: "rgba(44, 160, 44, 1)" }, + { color: "rgba(227, 119, 194, 1)" }, + { color: "rgba(148, 103, 189, 1)" }, +]; + +export const GRADIENT_PALETTES = [ + { + name: "蓝-红", + start: "rgba(51, 153, 204, 1)", + end: "rgba(204, 51, 51, 1)", + }, + { + name: "黄-绿", + start: "rgba(255, 235, 59, 1)", + end: "rgba(44, 160, 44, 1)", + }, + { + name: "粉-紫", + start: "rgba(227, 119, 194, 1)", + end: "rgba(148, 103, 189, 1)", + }, +]; + +export const RAINBOW_PALETTES = [ + { + name: "正向彩虹", + colors: [ + "rgba(255, 0, 0, 1)", + "rgba(255, 127, 0, 1)", + "rgba(255, 215, 0, 1)", + "rgba(199, 224, 0, 1)", + "rgba(76, 175, 80, 1)", + "rgba(0, 158, 115, 1)", + "rgba(0, 188, 212, 1)", + "rgba(33, 150, 243, 1)", + "rgba(63, 81, 181, 1)", + "rgba(142, 68, 173, 1)", + ], + }, + { + name: "反向彩虹", + colors: [ + "rgba(142, 68, 173, 1)", + "rgba(63, 81, 181, 1)", + "rgba(33, 150, 243, 1)", + "rgba(0, 188, 212, 1)", + "rgba(0, 158, 115, 1)", + "rgba(76, 175, 80, 1)", + "rgba(199, 224, 0, 1)", + "rgba(255, 215, 0, 1)", + "rgba(255, 127, 0, 1)", + "rgba(255, 0, 0, 1)", + ], + }, +]; + +export const CLASSIFICATION_METHODS = [ + { name: "优雅分段", value: "pretty_breaks" }, + { name: "自定义", value: "custom_breaks" }, +]; + +export const COLOR_TYPE_OPTIONS = [ + { label: "单一色", value: "single" }, + { label: "渐进色", value: "gradient" }, + { label: "离散彩虹", value: "rainbow" }, + { label: "自定义", value: "custom" }, +]; + +const DEFAULT_LAYER_STYLE_PRESETS: Record< + DefaultLayerStyleId, + Omit +> = { + junctions: { + layerId: "junctions", + layerName: "节点", + styleConfig: { + property: "pressure", + classificationMethod: "custom_breaks", + customBreaks: [16, 18, 20, 22, 24, 26], + customColors: [ + "rgba(255, 0, 0, 1)", + "rgba(255, 127, 0, 1)", + "rgba(255, 215, 0, 1)", + "rgba(199, 224, 0, 1)", + "rgba(76, 175, 80, 1)", + "rgba(0, 158, 115, 1)", + ], + segments: 6, + minSize: 4, + maxSize: 12, + minStrokeWidth: 2, + maxStrokeWidth: 8, + fixedStrokeWidth: 3, + colorType: "rainbow", + singlePaletteIndex: 0, + gradientPaletteIndex: 0, + rainbowPaletteIndex: 0, + showLabels: true, + showId: false, + opacity: 0.9, + adjustWidthByProperty: true, + }, + legendConfig: { + layerId: "junctions", + layerName: "节点", + property: "压力", + colors: [], + type: "point", + dimensions: [], + breaks: [], + }, + }, + pipes: { + layerId: "pipes", + layerName: "管道", + styleConfig: { + property: "velocity", + classificationMethod: "custom_breaks", + segments: 6, + minSize: 4, + maxSize: 12, + minStrokeWidth: 2, + maxStrokeWidth: 8, + fixedStrokeWidth: 3, + colorType: "gradient", + singlePaletteIndex: 0, + gradientPaletteIndex: 0, + rainbowPaletteIndex: 0, + showLabels: true, + showId: false, + opacity: 0.9, + adjustWidthByProperty: true, + customBreaks: [0.2, 0.4, 0.6, 0.8, 1.0, 1.2], + customColors: [], + }, + legendConfig: { + layerId: "pipes", + layerName: "管道", + property: "流速", + colors: [], + type: "linestring", + dimensions: [], + breaks: [], + }, + }, +}; + +export const createEmptyStyleConfig = (): StyleConfig => ({ + property: "", + classificationMethod: "pretty_breaks", + segments: 5, + minSize: 4, + maxSize: 12, + minStrokeWidth: 2, + maxStrokeWidth: 6, + fixedStrokeWidth: 3, + colorType: "single", + singlePaletteIndex: 0, + gradientPaletteIndex: 0, + rainbowPaletteIndex: 0, + showLabels: false, + showId: false, + opacity: 0.9, + adjustWidthByProperty: true, + customBreaks: [], + customColors: [], +}); + +export const createDefaultLayerStyleState = ( + layerId: DefaultLayerStyleId +): LayerStyleState => { + const preset = DEFAULT_LAYER_STYLE_PRESETS[layerId]; + return { + ...preset, + styleConfig: { + ...preset.styleConfig, + customBreaks: [...(preset.styleConfig.customBreaks || [])], + customColors: [...(preset.styleConfig.customColors || [])], + }, + legendConfig: { + ...preset.legendConfig, + colors: [...preset.legendConfig.colors], + dimensions: [...preset.legendConfig.dimensions], + breaks: [...preset.legendConfig.breaks], + }, + isActive: false, + }; +}; + +export const createDefaultLayerStyleStates = (): LayerStyleState[] => [ + createDefaultLayerStyleState("junctions"), + createDefaultLayerStyleState("pipes"), +]; diff --git a/src/components/olmap/core/Controls/styleEditorTypes.ts b/src/components/olmap/core/Controls/styleEditorTypes.ts new file mode 100644 index 0000000..034bce7 --- /dev/null +++ b/src/components/olmap/core/Controls/styleEditorTypes.ts @@ -0,0 +1,62 @@ +import React from "react"; +import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; + +import { LegendStyleConfig } from "./StyleLegend"; + +export interface StyleConfig { + property: string; + classificationMethod: string; + segments: number; + minSize: number; + maxSize: number; + minStrokeWidth: number; + maxStrokeWidth: number; + fixedStrokeWidth: number; + colorType: string; + singlePaletteIndex: number; + gradientPaletteIndex: number; + rainbowPaletteIndex: number; + showLabels: boolean; + showId: boolean; + opacity: number; + adjustWidthByProperty: boolean; + customBreaks?: number[]; + customColors?: string[]; +} + +export interface LayerStyleState { + layerId: string; + layerName: string; + styleConfig: StyleConfig; + legendConfig: LegendStyleConfig; + isActive: boolean; +} + +export type DefaultLayerStyleId = "junctions" | "pipes"; + +export interface StyleEditorPanelProps { + layerStyleStates: LayerStyleState[]; + setLayerStyleStates: React.Dispatch>; +} + +export interface AvailableProperty { + name: string; + value: string; +} + +export interface StyleEditorFormProps { + renderLayers: WebGLVectorTileLayer[]; + selectedRenderLayer?: WebGLVectorTileLayer; + styleConfig: StyleConfig; + setStyleConfig: React.Dispatch>; + availableProperties: AvailableProperty[]; + onLayerChange: (index: number) => void; + onPropertyChange: (property: string) => void; + onClassificationMethodChange: (method: string) => void; + onSegmentsChange: (segments: number) => void; + onCustomBreakChange: (index: number, value: string) => void; + onCustomBreakBlur: () => void; + onColorTypeChange: (colorType: string) => void; + onApply: () => void; + onReset: () => void; +} diff --git a/src/components/olmap/core/Controls/styleEditorUtils.ts b/src/components/olmap/core/Controls/styleEditorUtils.ts new file mode 100644 index 0000000..4e6f24c --- /dev/null +++ b/src/components/olmap/core/Controls/styleEditorUtils.ts @@ -0,0 +1,348 @@ +import { FlatStyleLike } from "ol/style/flat"; + +import { calculateClassification } from "@utils/breaks_classification"; +import { parseColor } from "@utils/parseColor"; + +import { + GRADIENT_PALETTES, + RAINBOW_PALETTES, + SINGLE_COLOR_PALETTES, +} from "./styleEditorPresets"; +import { StyleConfig } from "./styleEditorTypes"; + +export const rgbaToHex = (rgba: string) => { + try { + const c = parseColor(rgba); + const toHex = (n: number) => { + const hex = Math.round(n).toString(16); + return hex.length === 1 ? `0${hex}` : hex; + }; + return `#${toHex(c.r)}${toHex(c.g)}${toHex(c.b)}`; + } catch { + return "#000000"; + } +}; + +export const hexToRgba = (hex: string) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt( + result[3], + 16 + )}, 1)` + : "rgba(0, 0, 0, 1)"; +}; + +export 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); +}; + +export const getDefaultCustomBreaks = ({ + segments, + property, + layerId, + elevationRange, + diameterRange, + currentJunctionCalData, + currentPipeCalData, +}: { + segments: number; + property: string; + layerId?: string; + elevationRange?: [number, number]; + diameterRange?: [number, number]; + currentJunctionCalData?: any[]; + currentPipeCalData?: any[]; +}) => { + if (!layerId || !property) { + return Array.from({ length: segments }, () => 0); + } + + let dataArr: number[] = []; + + const isElevation = layerId === "junctions" && property === "elevation"; + const isDiameter = layerId === "pipes" && property === "diameter"; + + if (isElevation && elevationRange) { + dataArr = [elevationRange[0], elevationRange[1]]; + } else if (isDiameter && diameterRange) { + dataArr = [diameterRange[0], diameterRange[1]]; + } else if (layerId === "junctions" && currentJunctionCalData) { + dataArr = currentJunctionCalData.map((d: any) => d.value); + } else if (layerId === "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; +}; + +export const normalizeCustomBreaks = (breaks: number[], desired: number) => { + const nextBreaks = [...breaks] + .slice(0, desired) + .filter((value) => value >= 0) + .sort((a, b) => a - b); + + while (nextBreaks.length < desired) { + nextBreaks.push(nextBreaks[nextBreaks.length - 1] ?? 0); + } + + return nextBreaks; +}; + +export const addBreakExtrema = (breaks: number[], dataValues: number[]) => { + const nextBreaks = [...breaks]; + const minValue = Math.max( + dataValues.reduce((min, value) => Math.min(min, value), Infinity), + 0 + ); + const maxValue = dataValues.reduce( + (max, value) => Math.max(max, value), + -Infinity + ); + + if (!nextBreaks.includes(minValue)) { + nextBreaks.push(minValue); + } + + if (!nextBreaks.includes(maxValue)) { + nextBreaks.push(maxValue); + } + + nextBreaks.sort((a, b) => a - b); + return nextBreaks; +}; + +export const resolveStyleColors = ( + styleConfig: StyleConfig, + breaksLength: number +): string[] => { + if (styleConfig.colorType === "single") { + return Array.from( + { length: breaksLength }, + () => SINGLE_COLOR_PALETTES[styleConfig.singlePaletteIndex].color + ); + } + + if (styleConfig.colorType === "gradient") { + const { start, end } = GRADIENT_PALETTES[styleConfig.gradientPaletteIndex]; + const startColor = parseColor(start); + const endColor = parseColor(end); + + return Array.from({ length: breaksLength }, (_, index) => { + const ratio = breaksLength > 1 ? index / (breaksLength - 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); + return `rgba(${r}, ${g}, ${b}, 1)`; + }); + } + + if (styleConfig.colorType === "rainbow") { + const baseColors = RAINBOW_PALETTES[styleConfig.rainbowPaletteIndex].colors; + return Array.from( + { length: breaksLength }, + (_, index) => baseColors[index % baseColors.length] + ); + } + + const customColors = styleConfig.customColors || []; + const reverseRainbowColors = RAINBOW_PALETTES[1].colors; + const result = [...customColors]; + + while (result.length < breaksLength) { + result.push( + reverseRainbowColors[ + (result.length - customColors.length) % reverseRainbowColors.length + ] + ); + } + + return result.slice(0, breaksLength); +}; + +export const getSizePreviewColors = (styleConfig: StyleConfig) => { + if (styleConfig.colorType === "single") { + const color = SINGLE_COLOR_PALETTES[styleConfig.singlePaletteIndex].color; + return [color, color]; + } + + if (styleConfig.colorType === "gradient") { + const { start, end } = GRADIENT_PALETTES[styleConfig.gradientPaletteIndex]; + return [start, end]; + } + + if (styleConfig.colorType === "rainbow") { + const rainbowColors = RAINBOW_PALETTES[styleConfig.rainbowPaletteIndex].colors; + return [rainbowColors[0], rainbowColors[rainbowColors.length - 1]]; + } + + const customColors = styleConfig.customColors || []; + return [ + customColors[0] || "rgba(0,0,0,1)", + customColors[customColors.length - 1] || "rgba(0,0,0,1)", + ]; +}; + +export const resolveDimensions = ({ + layerType, + styleConfig, + breaksLength, +}: { + layerType: string; + styleConfig: StyleConfig; + breaksLength: number; +}) => { + if (layerType === "linestring") { + if (styleConfig.adjustWidthByProperty) { + return Array.from({ length: breaksLength }, (_, index) => { + const ratio = index / (breaksLength - 1); + return ( + styleConfig.minStrokeWidth + + (styleConfig.maxStrokeWidth - styleConfig.minStrokeWidth) * ratio + ); + }); + } + + return Array.from( + { length: breaksLength }, + () => styleConfig.fixedStrokeWidth + ); + } + + return Array.from({ length: breaksLength }, (_, index) => { + const ratio = index / (breaksLength - 1); + return styleConfig.minSize + (styleConfig.maxSize - styleConfig.minSize) * ratio; + }); +}; + +export const buildDynamicStyle = ({ + layerType, + styleConfig, + breaks, + colors, + dimensions, +}: { + layerType: string; + styleConfig: StyleConfig; + breaks: number[]; + colors: string[]; + dimensions: number[]; +}): FlatStyleLike => { + const generateColorConditions = (property: string): any[] => { + const conditions: any[] = ["case"]; + for (let index = 1; index < breaks.length; index++) { + if (property === "unit_headloss") { + conditions.push([ + "<=", + ["/", ["get", "unit_headloss"], ["/", ["get", "length"], 1000]], + breaks[index], + ]); + } else { + conditions.push(["<=", ["get", property], breaks[index]]); + } + const colorObj = parseColor(colors[index - 1]); + conditions.push( + `rgba(${colorObj.r}, ${colorObj.g}, ${colorObj.b}, ${styleConfig.opacity})` + ); + } + const defaultColor = parseColor(colors[0]); + conditions.push( + `rgba(${defaultColor.r}, ${defaultColor.g}, ${defaultColor.b}, ${styleConfig.opacity})` + ); + return conditions; + }; + + const generateDimensionConditions = (property: string): any[] => { + const conditions: any[] = ["case"]; + for (let index = 0; index < breaks.length; index++) { + if (property === "unit_headloss") { + conditions.push([ + "<=", + ["/", ["get", "headloss"], ["get", "length"]], + breaks[index], + ]); + } else { + conditions.push(["<=", ["get", property], breaks[index]]); + } + conditions.push(dimensions[index]); + } + conditions.push(dimensions[dimensions.length - 1]); + return conditions; + }; + + const generatePointDimensionConditions = (property: string): any[] => { + const conditions: any[] = ["case"]; + for (let index = 0; index < breaks.length; index++) { + conditions.push(["<=", ["get", property], breaks[index]]); + conditions.push(["interpolate", ["linear"], ["zoom"], 12, 1, 24, dimensions[index]]); + } + conditions.push(dimensions[dimensions.length - 1]); + return conditions; + }; + + const dynamicStyle: FlatStyleLike = {}; + + if (layerType === "linestring") { + dynamicStyle["stroke-color"] = generateColorConditions(styleConfig.property); + dynamicStyle["stroke-width"] = generateDimensionConditions(styleConfig.property); + } else if (layerType === "point") { + dynamicStyle["circle-fill-color"] = generateColorConditions(styleConfig.property); + dynamicStyle["circle-radius"] = generatePointDimensionConditions( + styleConfig.property + ); + dynamicStyle["circle-stroke-color"] = generateColorConditions(styleConfig.property); + dynamicStyle["circle-stroke-width"] = 2; + } + + return dynamicStyle; +}; + +export const buildContourDefinitions = ({ + styleConfig, + breaks, + colors, +}: { + styleConfig: StyleConfig; + breaks: number[]; + colors: string[]; +}) => { + const contours = []; + for (let index = 0; index < breaks.length - 1; index++) { + const colorObj = parseColor(colors[index]); + contours.push({ + threshold: [breaks[index], breaks[index + 1]], + color: [ + colorObj.r, + colorObj.g, + colorObj.b, + Math.round(styleConfig.opacity * 255), + ], + strokeWidth: 0, + }); + } + return contours; +}; diff --git a/src/components/olmap/core/Controls/useStyleEditor.ts b/src/components/olmap/core/Controls/useStyleEditor.ts new file mode 100644 index 0000000..1da8a3f --- /dev/null +++ b/src/components/olmap/core/Controls/useStyleEditor.ts @@ -0,0 +1,944 @@ +import { useNotification } from "@refinedev/core"; +import { VectorTile } from "ol"; +import type { Map as OlMap } from "ol"; +import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; +import VectorTileSource from "ol/source/VectorTile"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { FlatStyleLike } from "ol/style/flat"; + +import { config } from "@/config/config"; + +import { useData, useMap } from "../MapComponent"; +import { + createDefaultLayerStyleState, + createEmptyStyleConfig, +} from "./styleEditorPresets"; +import { + addBreakExtrema, + buildContourDefinitions, + buildDynamicStyle, + getDefaultCustomBreaks, + getDefaultCustomColors, + normalizeCustomBreaks, + resolveDimensions, + resolveStyleColors, +} from "./styleEditorUtils"; +import { + AvailableProperty, + LayerStyleState, + StyleEditorPanelProps, +} from "./styleEditorTypes"; +import { LegendStyleConfig } from "./StyleLegend"; +import { calculateClassification } from "@utils/breaks_classification"; + +const UNIT_HEADLOSS_RANGE: [number, number] = [0, 5]; + +export const useStyleEditor = ({ + layerStyleStates, + setLayerStyleStates, +}: StyleEditorPanelProps) => { + const map = useMap(); + const data = useData(); + const { open } = useNotification(); + + const currentJunctionCalData = data?.currentJunctionCalData; + const currentPipeCalData = data?.currentPipeCalData; + const compareJunctionCalData = data?.compareJunctionCalData; + const comparePipeCalData = data?.comparePipeCalData; + const compareMap = data?.compareMap; + const activeMaps = useMemo( + () => (data?.maps?.length ? data.maps : map ? [map] : []), + [data?.maps, map] + ); + 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 forceStyleAutoApplyVersion = data?.forceStyleAutoApplyVersion ?? 0; + + const [applyJunctionStyle, setApplyJunctionStyle] = useState(false); + const [applyPipeStyle, setApplyPipeStyle] = useState(false); + const [styleUpdateTrigger, setStyleUpdateTrigger] = useState(0); + const prevStyleUpdateTriggerRef = useRef(0); + const lastForceStyleAutoApplyVersionRef = useRef(0); + + const [renderLayers, setRenderLayers] = useState([]); + const [selectedRenderLayer, setSelectedRenderLayer] = + useState(); + const [styleConfig, setStyleConfig] = useState(createEmptyStyleConfig); + const latestLayerStyleStatesRef = useRef(layerStyleStates); + + const tileLoadListenersRef = useRef< + Map void }> + >(new Map()); + + useEffect(() => { + latestLayerStyleStatesRef.current = layerStyleStates; + }, [layerStyleStates]); + + const getRenderLayersById = useCallback( + (layerId: string) => + activeMaps.flatMap((targetMap) => + targetMap + .getAllLayers() + .filter((layer) => layer.get("value") === layerId) + .filter( + (layer): layer is WebGLVectorTileLayer => + layer instanceof WebGLVectorTileLayer + ) + ), + [activeMaps] + ); + + const getMapKey = useCallback((targetMap: OlMap, layerId: string) => { + const mapUid = (targetMap as unknown as { ol_uid?: string }).ol_uid || "map"; + return `${mapUid}:${layerId}`; + }, []); + + const getDataForMap = useCallback( + (targetMap: OlMap, layerId: string) => { + if (layerId === "junctions") { + return targetMap === compareMap + ? compareJunctionCalData || [] + : currentJunctionCalData || []; + } + if (layerId === "pipes") { + return targetMap === compareMap + ? comparePipeCalData || [] + : currentPipeCalData || []; + } + return []; + }, + [ + compareJunctionCalData, + compareMap, + comparePipeCalData, + currentJunctionCalData, + currentPipeCalData, + ] + ); + + const availableProperties = useMemo(() => { + if (!selectedRenderLayer) { + return []; + } + + return (selectedRenderLayer.get("properties") || []) as AvailableProperty[]; + }, [selectedRenderLayer]); + + const getBreakDefaults = useCallback( + (segments: number, property: string, layer = selectedRenderLayer) => + getDefaultCustomBreaks({ + segments, + property, + layerId: layer?.get("value"), + elevationRange, + diameterRange, + currentJunctionCalData, + currentPipeCalData, + }), + [ + currentJunctionCalData, + currentPipeCalData, + diameterRange, + elevationRange, + selectedRenderLayer, + ] + ); + + const saveLayerStyle = useCallback( + ( + layerId?: string, + newLegendConfig?: LegendStyleConfig, + overrideStyleConfig = styleConfig + ) => { + if (!overrideStyleConfig.property || !layerId) { + return; + } + + const layerName = + newLegendConfig?.layerName || + selectedRenderLayer?.get("name") || + `图层${layerId}`; + const property = availableProperties.find( + (item) => item.value === overrideStyleConfig.property + ); + + const legendConfig: LegendStyleConfig = newLegendConfig || { + layerId, + layerName, + property: property?.name || overrideStyleConfig.property, + colors: [], + type: selectedRenderLayer?.get("type") || "point", + dimensions: [], + breaks: [], + }; + + const newStyleState: LayerStyleState = { + layerId, + layerName, + styleConfig: { ...overrideStyleConfig }, + legendConfig: { ...legendConfig }, + isActive: true, + }; + + setLayerStyleStates((prev) => { + const existingIndex = prev.findIndex((state) => state.layerId === layerId); + if (existingIndex !== -1) { + const existingState = prev[existingIndex]; + if ( + JSON.stringify(existingState.styleConfig) === + JSON.stringify(newStyleState.styleConfig) && + JSON.stringify(existingState.legendConfig) === + JSON.stringify(newStyleState.legendConfig) && + existingState.layerName === newStyleState.layerName && + existingState.isActive === newStyleState.isActive + ) { + return prev; + } + const updated = [...prev]; + updated[existingIndex] = newStyleState; + return updated; + } + return [...prev, newStyleState]; + }); + }, + [availableProperties, selectedRenderLayer, setLayerStyleStates, styleConfig] + ); + + const applyContourLayerStyle = useCallback( + (layerStyleConfig: LayerStyleState, breaks?: number[]) => { + if (!breaks || breaks.length === 0 || !setContours) { + return; + } + + const colors = resolveStyleColors(layerStyleConfig.styleConfig, breaks.length); + setContours( + buildContourDefinitions({ + styleConfig: layerStyleConfig.styleConfig, + breaks, + colors, + }) + ); + }, + [setContours] + ); + + const applyLayerStyle = useCallback( + (layerStyleConfig: LayerStyleState, breaks?: number[]) => { + if (!breaks || breaks.length === 0) { + return; + } + + const nextStyleConfig = layerStyleConfig.styleConfig; + const targetLayers = getRenderLayersById(layerStyleConfig.layerId); + const renderLayer = targetLayers[0]; + if (!renderLayer || !nextStyleConfig.property) { + return; + } + + const layerType = renderLayer.get("type") as string; + const colors = resolveStyleColors(nextStyleConfig, breaks.length); + const dimensions = resolveDimensions({ + layerType, + styleConfig: nextStyleConfig, + breaksLength: breaks.length, + }); + const dynamicStyle = buildDynamicStyle({ + layerType, + styleConfig: nextStyleConfig, + breaks, + colors, + dimensions, + }); + + targetLayers.forEach((targetLayer) => { + targetLayer.setStyle(dynamicStyle); + }); + + const layerId = renderLayer.get("value"); + const initLayerStyleState = latestLayerStyleStatesRef.current.find( + (state) => state.layerId === layerId + ); + const legendConfig: LegendStyleConfig = { + layerName: initLayerStyleState?.layerName || `图层${layerId}`, + layerId, + property: initLayerStyleState?.legendConfig.property || "", + colors, + type: layerType, + dimensions, + breaks, + }; + + setTimeout(() => { + saveLayerStyle(layerId, legendConfig, nextStyleConfig); + }, 100); + }, + [getRenderLayersById, saveLayerStyle] + ); + + const applyClassificationStyle = useCallback( + (layerType: "junctions" | "pipes", fallbackStyleConfig?: LayerStyleState["styleConfig"]) => { + const layerStyleState = latestLayerStyleStatesRef.current.find( + (state) => state.layerId === layerType + ); + const effectiveStyleConfig = layerStyleState?.styleConfig || fallbackStyleConfig; + + if (!effectiveStyleConfig) { + return; + } + + const isElevation = + layerType === "junctions" && effectiveStyleConfig.property === "elevation"; + const isDiameter = + layerType === "pipes" && effectiveStyleConfig.property === "diameter"; + const isUnitHeadloss = + layerType === "pipes" && effectiveStyleConfig.property === "unit_headloss"; + + const dataValues = + layerType === "junctions" + ? isElevation && elevationRange + ? [elevationRange[0], elevationRange[1]] + : currentJunctionCalData?.map((item: any) => item.value) || [] + : isDiameter && diameterRange + ? [diameterRange[0], diameterRange[1]] + : isUnitHeadloss + ? [UNIT_HEADLOSS_RANGE[0], UNIT_HEADLOSS_RANGE[1]] + : currentPipeCalData?.map((item: any) => item.value) || []; + + const canApply = + layerType === "junctions" + ? dataValues.length > 0 + : dataValues.length > 0 || isUnitHeadloss; + + if (!canApply || dataValues.length === 0) { + return; + } + + const segments = effectiveStyleConfig.segments ?? 5; + let breaks = + effectiveStyleConfig.classificationMethod === "custom_breaks" + ? normalizeCustomBreaks(effectiveStyleConfig.customBreaks || [], segments) + : calculateClassification( + dataValues, + segments, + effectiveStyleConfig.classificationMethod + ); + + if (breaks.length === 0) { + return; + } + + breaks = addBreakExtrema(breaks, dataValues); + + const styleStateToApply = + layerStyleState || + ({ + layerId: layerType, + layerName: layerType === "junctions" ? "节点" : "管道", + styleConfig: effectiveStyleConfig, + legendConfig: { + layerId: layerType, + layerName: layerType === "junctions" ? "节点" : "管道", + property: effectiveStyleConfig.property, + colors: [], + type: layerType === "junctions" ? "point" : "linestring", + dimensions: [], + breaks: [], + }, + isActive: true, + } as LayerStyleState); + + applyLayerStyle(styleStateToApply, breaks); + if (layerType === "junctions") { + applyContourLayerStyle(styleStateToApply, breaks); + } + }, + [ + applyContourLayerStyle, + applyLayerStyle, + currentJunctionCalData, + currentPipeCalData, + diameterRange, + elevationRange, + ] + ); + + const updateVectorTileSource = useCallback( + (targetMap: OlMap, layerId: string, property: string, records: any[]) => { + const vectorTileSources = targetMap + .getAllLayers() + .filter((layer) => layer.get("value") === layerId) + .map((layer) => layer.getSource() as VectorTileSource) + .filter((source) => source); + + if (!vectorTileSources.length) { + return; + } + + const dataMap = new Map(); + records.forEach((record: any) => { + dataMap.set(record.ID, record.value || 0); + }); + + vectorTileSources.forEach((vectorTileSource) => { + const sourceTiles = (vectorTileSource as any).sourceTiles_; + Object.values(sourceTiles).forEach((vectorTile: any) => { + const renderFeatures = vectorTile.getFeatures(); + if (!renderFeatures || renderFeatures.length === 0) { + return; + } + + renderFeatures.forEach((renderFeature: any) => { + const featureId = renderFeature.get("id"); + const value = dataMap.get(featureId); + if (value === undefined) { + return; + } + + renderFeature.properties_[property] = + property === "flow" ? Math.abs(value) : value; + }); + }); + }); + }, + [] + ); + + const attachVectorTileSourceLoadedEvent = useCallback( + (targetMap: OlMap, layerId: string, property: string, records: any[]) => { + const vectorTileSource = targetMap + .getAllLayers() + .filter((layer) => layer.get("value") === layerId) + .map((layer) => layer.getSource() as VectorTileSource) + .filter((source) => source)[0]; + + if (!vectorTileSource) { + return; + } + + const dataMap = new Map(); + records.forEach((record: any) => { + dataMap.set(record.ID, record.value || 0); + }); + + const listener = (event: any) => { + try { + if (!(event.tile instanceof VectorTile)) { + return; + } + + 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) { + return; + } + + renderFeature.properties_[property] = + property === "flow" ? Math.abs(value) : value; + }); + } catch (error) { + console.error("Error processing tile load event:", error); + } + }; + + const listenerKey = getMapKey(targetMap, layerId); + vectorTileSource.on("tileloadend", listener); + tileLoadListenersRef.current.set(listenerKey, { + source: vectorTileSource, + listener, + }); + }, + [getMapKey] + ); + + const removeVectorTileSourceLoadedEvent = useCallback( + (targetMap: OlMap, layerId: string) => { + const listenerKey = getMapKey(targetMap, layerId); + const listenerState = tileLoadListenersRef.current.get(listenerKey); + if (listenerState) { + listenerState.source.un("tileloadend", listenerState.listener); + tileLoadListenersRef.current.delete(listenerKey); + } + }, + [getMapKey] + ); + + const handleApply = useCallback(() => { + if (!selectedRenderLayer || !styleConfig.property) { + return; + } + + const layerId = selectedRenderLayer.get("value"); + const property = styleConfig.property; + + if (styleConfig.classificationMethod === "custom_breaks") { + const expected = styleConfig.segments; + const custom = styleConfig.customBreaks || []; + + if ( + custom.length !== expected || + custom.some((value) => value === undefined || value === null || isNaN(value)) + ) { + open?.({ + type: "error", + message: `请设置 ${expected} 个有效的自定义阈值(数字)`, + }); + return; + } + + if (custom.some((value) => value < 0)) { + open?.({ type: "error", message: "自定义阈值必须大于等于 0" }); + return; + } + + setStyleConfig((prev) => ({ + ...prev, + customBreaks: [...(prev.customBreaks || [])] + .slice(0, expected) + .sort((a, b) => a - b), + })); + } + + if (layerId === "junctions") { + setJunctionText?.(property); + setShowJunctionTextLayer?.(styleConfig.showLabels); + setShowJunctionId?.(styleConfig.showId); + setApplyJunctionStyle(true); + if (property === "pressure") { + setContourLayerAvailable?.(true); + } + saveLayerStyle(layerId); + open?.({ + type: "success", + message: "节点图层样式设置成功,等待数据更新。", + }); + } + + if (layerId === "pipes") { + setPipeText?.(property); + setShowPipeTextLayer?.(styleConfig.showLabels); + setShowPipeId?.(styleConfig.showId); + setApplyPipeStyle(true); + setWaterflowLayerAvailable?.(true); + saveLayerStyle(layerId); + open?.({ + type: "success", + message: "管道图层样式设置成功,等待数据更新。", + }); + } + + setStyleUpdateTrigger((prev) => prev + 1); + }, [ + open, + saveLayerStyle, + selectedRenderLayer, + setContourLayerAvailable, + setJunctionText, + setPipeText, + setShowJunctionId, + setShowJunctionTextLayer, + setShowPipeId, + setShowPipeTextLayer, + setWaterflowLayerAvailable, + styleConfig, + ]); + + const handleReset = useCallback(() => { + if (!selectedRenderLayer) { + return; + } + + const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE; + const layerId = selectedRenderLayer.get("value"); + + getRenderLayersById(layerId).forEach((targetLayer) => { + targetLayer.setStyle(defaultFlatStyle); + }); + + setLayerStyleStates((prev) => prev.filter((state) => state.layerId !== layerId)); + + if (layerId === "junctions") { + setApplyJunctionStyle(false); + setShowJunctionTextLayer?.(false); + setShowJunctionId?.(false); + setJunctionText?.(""); + setContours?.([]); + setContourLayerAvailable?.(false); + } else if (layerId === "pipes") { + setApplyPipeStyle(false); + setShowPipeTextLayer?.(false); + setShowPipeId?.(false); + setPipeText?.(""); + setWaterflowLayerAvailable?.(false); + } + }, [ + getRenderLayersById, + selectedRenderLayer, + setContourLayerAvailable, + setContours, + setJunctionText, + setLayerStyleStates, + setPipeText, + setShowJunctionId, + setShowJunctionTextLayer, + setShowPipeId, + setShowPipeTextLayer, + setWaterflowLayerAvailable, + ]); + + const handleLayerChange = useCallback( + (index: number) => { + const newLayer = index >= 0 ? renderLayers[index] : undefined; + setSelectedRenderLayer(newLayer); + + if (!newLayer) { + return; + } + + const layerId = newLayer.get("value"); + const cachedStyleState = layerStyleStates.find((state) => state.layerId === layerId); + + if (cachedStyleState) { + setStyleConfig(cachedStyleState.styleConfig); + return; + } + + setStyleConfig((prev) => ({ + ...prev, + property: "", + customBreaks: + prev.classificationMethod === "custom_breaks" + ? getBreakDefaults(prev.segments, "", newLayer) + : prev.customBreaks, + customColors: getDefaultCustomColors(prev.segments, prev.customColors), + })); + }, + [getBreakDefaults, layerStyleStates, renderLayers] + ); + + const handlePropertyChange = useCallback( + (property: string) => { + setStyleConfig((prev) => ({ + ...prev, + property, + customBreaks: + prev.classificationMethod === "custom_breaks" + ? getBreakDefaults(prev.segments, property) + : prev.customBreaks, + })); + }, + [getBreakDefaults] + ); + + const handleClassificationMethodChange = useCallback( + (classificationMethod: string) => { + setStyleConfig((prev) => ({ + ...prev, + classificationMethod, + customBreaks: + classificationMethod === "custom_breaks" + ? getBreakDefaults(prev.segments, prev.property) + : prev.customBreaks, + })); + }, + [getBreakDefaults] + ); + + const handleSegmentsChange = useCallback( + (segments: number) => { + setStyleConfig((prev) => { + const newCustomColors = [...(prev.customColors || [])]; + return { + ...prev, + segments, + customBreaks: + prev.classificationMethod === "custom_breaks" + ? getBreakDefaults(segments, prev.property) + : prev.customBreaks, + customColors: getDefaultCustomColors(segments, newCustomColors), + }; + }); + }, + [getBreakDefaults] + ); + + const handleCustomBreakChange = useCallback( + (index: number, value: string) => { + const nextValue = parseFloat(value); + setStyleConfig((prev) => { + const nextBreaks = [...(prev.customBreaks || [])]; + while (nextBreaks.length < prev.segments) { + nextBreaks.push(0); + } + nextBreaks[index] = isNaN(nextValue) ? 0 : Math.max(0, nextValue); + return { ...prev, customBreaks: nextBreaks }; + }); + }, + [] + ); + + const handleCustomBreakBlur = useCallback(() => { + setStyleConfig((prev) => ({ + ...prev, + customBreaks: [...(prev.customBreaks || [])] + .slice(0, prev.segments + 1) + .sort((a, b) => a - b), + })); + }, []); + + const handleColorTypeChange = useCallback((colorType: string) => { + setStyleConfig((prev) => { + let customColors = prev.customColors; + if (colorType === "custom" && (!customColors || customColors.length === 0)) { + customColors = getDefaultCustomColors(prev.segments, []); + } + + return { + ...prev, + colorType, + adjustWidthByProperty: colorType === "single" ? true : prev.adjustWidthByProperty, + customColors, + }; + }); + }, []); + + useEffect(() => { + if ( + forceStyleAutoApplyVersion <= 0 || + forceStyleAutoApplyVersion === lastForceStyleAutoApplyVersionRef.current + ) { + return; + } + + lastForceStyleAutoApplyVersionRef.current = forceStyleAutoApplyVersion; + + const defaultJunctionStyleState = { + ...createDefaultLayerStyleState("junctions"), + isActive: true, + }; + const defaultPipeStyleState = { + ...createDefaultLayerStyleState("pipes"), + isActive: true, + }; + + setLayerStyleStates((prev) => { + const nextStates = [...prev]; + [defaultJunctionStyleState, defaultPipeStyleState].forEach((defaultState) => { + const index = nextStates.findIndex((state) => state.layerId === defaultState.layerId); + if (index === -1) { + nextStates.push(defaultState); + } else { + nextStates[index] = defaultState; + } + }); + return nextStates; + }); + + setJunctionText?.(defaultJunctionStyleState.styleConfig.property); + setPipeText?.(defaultPipeStyleState.styleConfig.property); + setShowJunctionTextLayer?.(defaultJunctionStyleState.styleConfig.showLabels); + setShowPipeTextLayer?.(defaultPipeStyleState.styleConfig.showLabels); + setShowJunctionId?.(defaultJunctionStyleState.styleConfig.showId); + setShowPipeId?.(defaultPipeStyleState.styleConfig.showId); + setContourLayerAvailable?.( + defaultJunctionStyleState.styleConfig.property === "pressure" + ); + setWaterflowLayerAvailable?.( + defaultPipeStyleState.styleConfig.property === "flow" + ); + setApplyJunctionStyle(true); + setApplyPipeStyle(true); + + const selectedLayerId = selectedRenderLayer?.get("value"); + if (selectedLayerId === "junctions") { + setStyleConfig(defaultJunctionStyleState.styleConfig); + } else if (selectedLayerId === "pipes") { + setStyleConfig(defaultPipeStyleState.styleConfig); + } + }, [ + forceStyleAutoApplyVersion, + selectedRenderLayer, + setContourLayerAvailable, + setJunctionText, + setLayerStyleStates, + setPipeText, + setShowJunctionId, + setShowJunctionTextLayer, + setShowPipeId, + setShowPipeTextLayer, + setWaterflowLayerAvailable, + ]); + + useEffect(() => { + const isUserTrigger = styleUpdateTrigger !== prevStyleUpdateTriggerRef.current; + prevStyleUpdateTriggerRef.current = styleUpdateTrigger; + + const updateJunctionStyle = () => { + const junctionStyleState = latestLayerStyleStatesRef.current.find( + (state) => state.layerId === "junctions" + ); + const isElevation = + junctionStyleState?.styleConfig.property === "elevation"; + + applyClassificationStyle("junctions", junctionStyleState?.styleConfig); + + if (isElevation) { + activeMaps.forEach((targetMap) => { + removeVectorTileSourceLoadedEvent(targetMap, "junctions"); + }); + return; + } + + activeMaps.forEach((targetMap) => { + const targetData = getDataForMap(targetMap, "junctions"); + if (!targetData || targetData.length === 0) { + return; + } + updateVectorTileSource(targetMap, "junctions", junctionText, targetData); + removeVectorTileSourceLoadedEvent(targetMap, "junctions"); + attachVectorTileSourceLoadedEvent( + targetMap, + "junctions", + junctionText, + targetData + ); + }); + }; + + const updatePipeStyle = () => { + const pipeStyleState = latestLayerStyleStatesRef.current.find( + (state) => state.layerId === "pipes" + ); + const isDiameter = pipeStyleState?.styleConfig.property === "diameter"; + + applyClassificationStyle("pipes", pipeStyleState?.styleConfig); + + if (isDiameter) { + activeMaps.forEach((targetMap) => { + removeVectorTileSourceLoadedEvent(targetMap, "pipes"); + }); + return; + } + + activeMaps.forEach((targetMap) => { + const targetData = getDataForMap(targetMap, "pipes"); + if (!targetData || targetData.length === 0) { + return; + } + updateVectorTileSource(targetMap, "pipes", pipeText, targetData); + removeVectorTileSourceLoadedEvent(targetMap, "pipes"); + attachVectorTileSourceLoadedEvent(targetMap, "pipes", pipeText, targetData); + }); + }; + + if (isUserTrigger) { + if (selectedRenderLayer?.get("value") === "junctions") { + updateJunctionStyle(); + } else if (selectedRenderLayer?.get("value") === "pipes") { + updatePipeStyle(); + } + return; + } + + const isElevation = junctionText === "elevation"; + const isDiameter = pipeText === "diameter"; + + if ( + applyJunctionStyle && + ((currentJunctionCalData && currentJunctionCalData.length > 0) || isElevation) + ) { + updateJunctionStyle(); + } + + if ( + applyPipeStyle && + ((currentPipeCalData && currentPipeCalData.length > 0) || isDiameter) + ) { + updatePipeStyle(); + } + + if (!applyJunctionStyle) { + activeMaps.forEach((targetMap) => { + removeVectorTileSourceLoadedEvent(targetMap, "junctions"); + }); + } + + if (!applyPipeStyle) { + activeMaps.forEach((targetMap) => { + removeVectorTileSourceLoadedEvent(targetMap, "pipes"); + }); + } + // This effect is intentionally driven by explicit style triggers and data snapshots. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + styleUpdateTrigger, + applyJunctionStyle, + applyPipeStyle, + currentJunctionCalData, + currentPipeCalData, + compareJunctionCalData, + comparePipeCalData, + activeMaps, + ]); + + useEffect(() => { + return () => { + activeMaps.forEach((targetMap) => { + removeVectorTileSourceLoadedEvent(targetMap, "junctions"); + removeVectorTileSourceLoadedEvent(targetMap, "pipes"); + }); + }; + }, [activeMaps, removeVectorTileSourceLoadedEvent]); + + useEffect(() => { + if (!map) { + return; + } + + const updateVisibleLayers = () => { + const layers = map.getAllLayers(); + const webGLVectorTileLayers = layers.filter( + (layer) => + layer.get("value") === "junctions" || layer.get("value") === "pipes" + ) as WebGLVectorTileLayer[]; + + setRenderLayers(webGLVectorTileLayers); + }; + + updateVisibleLayers(); + }, [map]); + + return { + isReady: Boolean(data), + renderLayers, + selectedRenderLayer, + styleConfig, + setStyleConfig, + availableProperties, + handleLayerChange, + handlePropertyChange, + handleClassificationMethodChange, + handleSegmentsChange, + handleCustomBreakChange, + handleCustomBreakBlur, + handleColorTypeChange, + handleApply, + handleReset, + }; +}; diff --git a/src/components/olmap/core/MapComponent.tsx b/src/components/olmap/core/MapComponent.tsx index 64ad653..b8eca89 100644 --- a/src/components/olmap/core/MapComponent.tsx +++ b/src/components/olmap/core/MapComponent.tsx @@ -85,6 +85,8 @@ interface DataContextType { maps?: OlMap[]; diameterRange?: [number, number]; elevationRange?: [number, number]; + forceStyleAutoApplyVersion?: number; + setForceStyleAutoApplyVersion?: React.Dispatch>; } // 跨组件传递 @@ -184,7 +186,7 @@ const MapComponent: React.FC = ({ children }) => { const [showPipeId, setShowPipeId] = useState(false); // 控制管道ID显示 const [showContourLayer, setShowContourLayer] = useState(false); // 控制等高线图层显示 const [junctionText, setJunctionText] = useState("pressure"); - const [pipeText, setPipeText] = useState("flow"); + const [pipeText, setPipeText] = useState("velocity"); const [contours, setContours] = useState([]); const flowAnimation = useRef(false); // 添加动画控制标志 const [isContourLayerAvailable, setContourLayerAvailable] = useState(false); // 控制等高线图层显示 @@ -263,6 +265,8 @@ const MapComponent: React.FC = ({ children }) => { const [elevationRange, setElevationRange] = useState< [number, number] | undefined >(); + const [forceStyleAutoApplyVersion, setForceStyleAutoApplyVersion] = + useState(0); const toggleCompareMode = useCallback(() => { setCompareMode((prev) => !prev); @@ -1526,6 +1530,8 @@ const MapComponent: React.FC = ({ children }) => { maps, diameterRange, elevationRange, + forceStyleAutoApplyVersion, + setForceStyleAutoApplyVersion, }} >