Files
TJWaterServer/src/app/OlMap/Controls/StyleEditorPanel.tsx

1769 lines
59 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback, useRef } 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 { 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;
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<React.SetStateAction<LayerStyleState[]>>;
}
// 预设颜色方案
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)";
};
const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
layerStyleStates,
setLayerStyleStates,
}) => {
const map = useMap();
const data = useData();
if (!data) {
return <div>Loading...</div>; // 或其他占位符
}
const {
currentJunctionCalData,
currentPipeCalData,
pipeData,
junctionText,
pipeText,
setShowJunctionTextLayer,
setShowPipeTextLayer,
setContourLayerAvailable,
setWaterflowLayerAvailable,
setJunctionText,
setPipeText,
setContours,
} = data;
const { open } = useNotification();
const [applyJunctionStyle, setApplyJunctionStyle] = useState(false);
const [applyPipeStyle, setApplyPipeStyle] = useState(false);
const [styleUpdateTrigger, setStyleUpdateTrigger] = useState(0); // 用于触发样式更新的状态
const prevStyleUpdateTriggerRef = useRef<number>(0);
const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]);
const [selectedRenderLayer, setSelectedRenderLayer] =
useState<WebGLVectorTileLayer>();
const [availableProperties, setAvailableProperties] = useState<
{ name: string; value: string }[]
>([]);
const [styleConfig, setStyleConfig] = useState<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,
opacity: 0.9,
adjustWidthByProperty: true,
customBreaks: [],
customColors: [],
});
// 根据分段数生成相应数量的渐进颜色
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, parseColor]
);
// 根据分段数生成彩虹色
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 = useCallback(
(layerId?: string, newLegendConfig?: LegendStyleConfig) => {
if (!selectedRenderLayer || !styleConfig.property) {
console.warn("无法保存样式:缺少必要的图层或样式配置");
return;
}
if (!layerId) return; // 如果没有传入 layerId则不保存
// 如果没有传入图例配置,则创建一个默认的空配置
const layerName =
newLegendConfig?.layerName ||
selectedRenderLayer.get("name") ||
`图层${layerId}`;
const property = availableProperties.find(
(p) => p.value === styleConfig.property
);
let legendConfig: LegendStyleConfig = newLegendConfig || {
layerId,
layerName,
property: property?.name || styleConfig.property,
colors: [],
type: selectedRenderLayer.get("type"),
dimensions: [],
breaks: [],
};
const newStyleState: LayerStyleState = {
layerId,
layerName,
styleConfig: { ...styleConfig },
legendConfig: { ...legendConfig },
isActive: true,
};
setLayerStyleStates((prev) => {
// 检查是否已存在该图层的样式状态
const existingIndex = prev.findIndex(
(state) => state.layerId === layerId
);
if (existingIndex !== -1) {
// 更新已存在的状态
const updated = [...prev];
updated[existingIndex] = newStyleState;
return updated;
} else {
// 添加新的状态
return [...prev, newStyleState];
}
});
},
[selectedRenderLayer, styleConfig]
);
// 设置分类样式参数,触发样式应用
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") {
if (setJunctionText && setShowJunctionTextLayer) {
setJunctionText(property);
setShowJunctionTextLayer(styleConfig.showLabels);
setApplyJunctionStyle(true);
if (property === "pressure" && setContourLayerAvailable) {
setContourLayerAvailable(true);
}
saveLayerStyle(layerId);
open?.({
type: "success",
message: "节点图层样式设置成功,等待数据更新。",
});
}
}
if (layerId === "pipes") {
if (setPipeText && setShowPipeTextLayer) {
setPipeText(property);
setShowPipeTextLayer(styleConfig.showLabels);
setApplyPipeStyle(true);
setWaterflowLayerAvailable && setWaterflowLayerAvailable(true);
saveLayerStyle(layerId);
open?.({
type: "success",
message: "管道图层样式设置成功,等待数据更新。",
});
}
}
// 触发样式更新
setStyleUpdateTrigger((prev) => prev + 1);
}
};
// 计算分类样式,并应用到对应图层
const applyClassificationStyle = (
layerType: "junctions" | "pipes",
styleConfig: any
) => {
if (
layerType === "junctions" &&
currentJunctionCalData &&
currentJunctionCalData.length > 0
) {
// 应用节点样式
let junctionStyleConfigState = layerStyleStates.find(
(s) => s.layerId === "junctions"
);
// 更新节点数据属性
const segments = junctionStyleConfigState?.styleConfig.segments ?? 5;
let breaks: number[] = [];
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(
currentJunctionCalData.map((d) => d.value),
segments,
styleConfig.classificationMethod
);
breaks = calc;
}
if (breaks.length === 0) {
console.warn("计算的 breaks 为空,无法应用样式");
return;
}
// 计算最大最小值,判断是否包含并插入 breaks
const data = currentJunctionCalData.map((d) => d.value);
const min_val = Math.max(
data.reduce((min, val) => Math.min(min, val), Infinity),
0
);
const max_val = data.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
) {
// 应用管道样式
let pipeStyleConfigState = layerStyleStates.find(
(s) => s.layerId === "pipes"
);
// 更新管道数据属性
const segments = pipeStyleConfigState?.styleConfig.segments ?? 5;
let breaks: number[] = [];
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(
currentPipeCalData.map((d) => d.value),
segments,
styleConfig.classificationMethod
);
breaks = calc;
}
if (breaks.length === 0) {
console.warn("计算的 breaks 为空,无法应用样式");
return;
}
// 计算最大最小值,判断是否包含并插入 breaks
const data = currentPipeCalData.map((d) => d.value);
const min_val = Math.max(
data.reduce((min, val) => Math.min(min, val), Infinity),
0
);
const max_val = data.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 renderLayer = renderLayers.filter((layer) => {
return layer.get("value") === layerStyleConfig.layerId;
})[0];
if (!renderLayer || !styleConfig?.property) return;
const layerType: string = renderLayer?.get("type");
const source = renderLayer.getSource();
if (!source) 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);
})();
// 计算每个分段的线条粗细和点大小
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++) {
// 添加条件:属性值 <= 当前断点
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++) {
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;
}
renderLayer.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);
}, 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 = useCallback(() => {
if (!selectedRenderLayer) return;
// 重置 WebGL 图层样式
const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE;
selectedRenderLayer.setStyle(defaultFlatStyle);
// 删除对应图层的样式状态,从而移除图例显示
const layerId = selectedRenderLayer.get("value");
if (layerId !== undefined) {
setLayerStyleStates((prev) =>
prev.filter((state) => state.layerId !== layerId)
);
// 重置样式应用状态
if (layerId === "junctions") {
setApplyJunctionStyle(false);
if (setShowJunctionTextLayer) setShowJunctionTextLayer(false);
if (setJunctionText) setJunctionText("");
setContours && setContours([]);
setContourLayerAvailable && setContourLayerAvailable(false);
} else if (layerId === "pipes") {
setApplyPipeStyle(false);
if (setShowPipeTextLayer) setShowPipeTextLayer(false);
if (setPipeText) setPipeText("");
setWaterflowLayerAvailable && setWaterflowLayerAvailable(false);
}
}
}, [selectedRenderLayer]);
// 更新当前 VectorTileSource 中的所有缓冲要素属性
const updateVectorTileSource = (property: string, data: any[]) => {
if (!map) return;
const vectorTileSources = map
.getAllLayers()
.filter((layer) => layer instanceof WebGLVectorTileLayer)
.map((layer) => layer.getSource() as VectorTileSource)
.filter((source) => source);
if (!vectorTileSources.length) return;
// 创建 id 到 value 的映射
const dataMap = new Map<string, number>();
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 [tileLoadListeners, setTileLoadListeners] = useState<
Map<VectorTileSource, (event: any) => void>
>(new Map());
const attachVectorTileSourceLoadedEvent = (
layerId: string,
property: string,
data: any[]
) => {
if (!map) return;
const vectorTileSource = map
.getAllLayers()
.filter((layer) => layer.get("value") === layerId)
.map((layer) => layer.getSource() as VectorTileSource)
.filter((source) => source)[0];
if (!vectorTileSource) return;
// 创建 id 到 value 的映射
const dataMap = new Map<string, number>();
data.forEach((d: any) => {
dataMap.set(d.ID, d.value || 0);
});
// 新增监听器并保存
const newListeners = new Map<VectorTileSource, (event: any) => void>();
const listener = (event: any) => {
try {
if (event.tile instanceof VectorTile) {
const renderFeatures = event.tile.getFeatures();
if (!renderFeatures || renderFeatures.length === 0) return;
// 直接更新要素属性
renderFeatures.forEach((renderFeature: any) => {
const featureId = renderFeature.get("id");
const value = dataMap.get(featureId);
if (value !== undefined) {
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);
}
};
vectorTileSource.on("tileloadend", listener);
newListeners.set(vectorTileSource, listener);
setTileLoadListeners(newListeners);
};
// 新增函数:取消对应 layerId 已添加的 on 事件
const removeVectorTileSourceLoadedEvent = (layerId: string) => {
if (!map) return;
const vectorTileSource = map
.getAllLayers()
.filter((layer) => layer.get("value") === layerId)
.map((layer) => layer.getSource() as VectorTileSource)
.filter((source) => source)[0];
if (!vectorTileSource) return;
const listener = tileLoadListeners.get(vectorTileSource);
if (listener) {
vectorTileSource.un("tileloadend", listener);
setTileLoadListeners((prev) => {
const newMap = new Map(prev);
newMap.delete(vectorTileSource);
return newMap;
});
}
};
// 监听数据变化,重新应用样式。由样式应用按钮触发,或由数据变化触发
useEffect(() => {
// 判断此次触发是否由用户点击“应用”按钮引起
const isUserTrigger =
styleUpdateTrigger !== prevStyleUpdateTriggerRef.current;
// 更新 prevStyleUpdateTrigger
prevStyleUpdateTriggerRef.current = styleUpdateTrigger;
const updateJunctionStyle = () => {
if (!currentJunctionCalData) return;
const junctionStyleConfigState = layerStyleStates.find(
(s) => s.layerId === "junctions"
);
// setStyle() 会清除渲染器缓存,这是闪烁的主要原因 WebGLVectorTile.js:114-118
// 尝试考虑使用 updateStyleVariables() 更新
applyClassificationStyle(
"junctions",
junctionStyleConfigState?.styleConfig
);
// 更新现有的 VectorTileSource
updateVectorTileSource(junctionText, currentJunctionCalData);
// 移除旧的监听器,并添加新的监听器
removeVectorTileSourceLoadedEvent("junctions");
attachVectorTileSourceLoadedEvent(
"junctions",
junctionText,
currentJunctionCalData
);
};
const updatePipeStyle = () => {
if (!currentPipeCalData) return;
const pipeStyleConfigState = layerStyleStates.find(
(s) => s.layerId === "pipes"
);
applyClassificationStyle("pipes", pipeStyleConfigState?.styleConfig);
// 更新现有的 VectorTileSource
updateVectorTileSource(pipeText, currentPipeCalData);
// 移除旧的监听器,并添加新的监听器
removeVectorTileSourceLoadedEvent("pipes");
attachVectorTileSourceLoadedEvent("pipes", pipeText, currentPipeCalData);
};
if (isUserTrigger) {
if (selectedRenderLayer?.get("value") === "junctions") {
updateJunctionStyle();
} else if (selectedRenderLayer?.get("value") === "pipes") {
updatePipeStyle();
}
return;
}
if (
applyJunctionStyle &&
currentJunctionCalData &&
currentJunctionCalData.length > 0
) {
updateJunctionStyle();
}
if (applyPipeStyle && currentPipeCalData && currentPipeCalData.length > 0) {
updatePipeStyle();
}
if (!applyJunctionStyle) {
removeVectorTileSourceLoadedEvent("junctions");
}
if (!applyPipeStyle) {
removeVectorTileSourceLoadedEvent("pipes");
}
}, [
styleUpdateTrigger,
applyJunctionStyle,
applyPipeStyle,
currentJunctionCalData,
currentPipeCalData,
]);
// 获取地图中的矢量图层,用于选择图层选项
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]);
// 获取选中图层的属性,并检查是否有已缓存的样式状态
useEffect(() => {
// 如果没有矢量图层或没有选中图层,清空属性列表
if (!renderLayers || renderLayers.length === 0) {
setAvailableProperties([]);
return;
}
// 如果没有选中图层,清空属性列表
if (!selectedRenderLayer) {
setAvailableProperties([]);
return;
}
// 获取第一个要素的数值型属性
const properties = selectedRenderLayer.get("properties") || {};
setAvailableProperties(properties);
// 设置选中的渲染图层
const renderLayer = renderLayers.filter((layer) => {
return layer.get("value") === selectedRenderLayer?.get("value");
})[0];
setSelectedRenderLayer(renderLayer);
// 检查是否有已缓存的样式状态,如果有则自动恢复
const layerId = selectedRenderLayer.get("value");
const cachedStyleState = layerStyleStates.find(
(state) => state.layerId === layerId
);
if (cachedStyleState) {
setStyleConfig(cachedStyleState.styleConfig);
}
}, [renderLayers, selectedRenderLayer, map, renderLayers, layerStyleStates]);
// 监听颜色类型变化,当切换到单一色时自动勾选宽度调整选项
useEffect(() => {
if (styleConfig.colorType === "single") {
setStyleConfig((prev) => ({
...prev,
adjustWidthByProperty: true,
}));
}
}, [styleConfig.colorType]);
// 初始化或调整自定义断点数组长度,默认使用 pretty_breaks 生成若存在数据
useEffect(() => {
if (styleConfig.classificationMethod !== "custom_breaks") return;
const numBreaks = styleConfig.segments;
setStyleConfig((prev) => {
const prevBreaks = prev.customBreaks || [];
if (prevBreaks.length === numBreaks) return prev;
const selectedLayerId = selectedRenderLayer?.get("value");
let dataArr: number[] = [];
if (selectedLayerId === "junctions" && currentJunctionCalData)
dataArr = currentJunctionCalData.map((d) => d.value);
else if (selectedLayerId === "pipes" && currentPipeCalData)
dataArr = currentPipeCalData.map((d) => d.value);
let defaultBreaks: number[] = Array.from({ length: numBreaks }, () => 0);
if (dataArr && dataArr.length > 0) {
defaultBreaks = calculateClassification(
dataArr,
styleConfig.segments,
"pretty_breaks"
);
defaultBreaks = defaultBreaks.slice(0, numBreaks);
if (defaultBreaks.length < numBreaks)
while (defaultBreaks.length < numBreaks)
defaultBreaks.push(defaultBreaks[defaultBreaks.length - 1] ?? 0);
}
return { ...prev, customBreaks: defaultBreaks };
});
}, [
styleConfig.classificationMethod,
styleConfig.segments,
selectedRenderLayer,
currentJunctionCalData,
currentPipeCalData,
]);
// 初始化或调整自定义颜色数组长度
useEffect(() => {
const numColors = styleConfig.segments;
setStyleConfig((prev) => {
const prevColors = prev.customColors || [];
if (prevColors.length === numColors) return prev;
const newColors = [...prevColors];
const baseColors = RAINBOW_PALETTES[0].colors;
while (newColors.length < numColors) {
newColors.push(baseColors[newColors.length % baseColors.length]);
}
return { ...prev, customColors: newColors.slice(0, numColors) };
});
}, [styleConfig.segments]);
const getColorSetting = () => {
if (styleConfig.colorType === "single") {
return (
<FormControl
variant="standard"
fullWidth
margin="dense"
className="mt-3"
>
<InputLabel></InputLabel>
<Select
value={styleConfig.singlePaletteIndex}
onChange={(e) =>
setStyleConfig((prev) => ({
...prev,
singlePaletteIndex: Number(e.target.value),
}))
}
>
{SINGLE_COLOR_PALETTES.map((p, idx) => {
return (
<MenuItem key={idx} value={idx}>
<Box
width="100%"
sx={{ display: "flex", alignItems: "center" }}
>
<Box
key={idx}
sx={{
width: "80%",
height: 16,
borderRadius: 2,
background: p.color,
marginRight: 1,
border: "1px solid #ccc",
}}
/>
</Box>
</MenuItem>
);
})}
</Select>
</FormControl>
);
}
if (styleConfig.colorType === "gradient") {
return (
<FormControl
variant="standard"
fullWidth
margin="dense"
className="mt-3"
>
<InputLabel></InputLabel>
<Select
value={styleConfig.gradientPaletteIndex}
onChange={(e) =>
setStyleConfig((prev) => ({
...prev,
gradientPaletteIndex: Number(e.target.value),
}))
}
>
{GRADIENT_PALETTES.map((p, idx) => {
const numColors = styleConfig.segments + 1;
const previewColors = Array.from(
{ length: numColors },
(_, i) => {
const ratio = numColors > 1 ? i / (numColors - 1) : 1;
const startColor = parseColor(p.start);
const endColor = parseColor(p.end);
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)`;
}
);
return (
<MenuItem key={idx} value={idx}>
<Box
width="100%"
sx={{ display: "flex", alignItems: "center" }}
>
<Box
sx={{
width: "80%",
height: 16,
borderRadius: 2,
display: "flex",
overflow: "hidden",
marginRight: 1,
border: "1px solid #ccc",
}}
>
{previewColors.map((color, colorIdx) => (
<Box
key={colorIdx}
sx={{
flex: 1,
backgroundColor: color,
}}
/>
))}
</Box>
</Box>
</MenuItem>
);
})}
</Select>
</FormControl>
);
}
if (styleConfig.colorType === "rainbow") {
return (
<FormControl
variant="standard"
fullWidth
margin="dense"
className="mt-3"
>
<InputLabel></InputLabel>
<Select
value={styleConfig.rainbowPaletteIndex}
onChange={(e) =>
setStyleConfig((prev) => ({
...prev,
rainbowPaletteIndex: Number(e.target.value),
}))
}
>
{RAINBOW_PALETTES.map((p, idx) => {
// 根据当前分段数+1生成该方案的预览颜色
const baseColors = p.colors;
const numColors = styleConfig.segments + 1;
const previewColors = Array.from(
{ length: numColors },
(_, i) => baseColors[i % baseColors.length]
);
return (
<MenuItem key={idx} value={idx}>
<Box
width="100%"
sx={{ display: "flex", alignItems: "center" }}
>
<Typography sx={{ marginRight: 1 }}>{p.name}</Typography>
<Box
sx={{
width: "60%",
height: 16,
borderRadius: 2,
display: "flex",
border: "1px solid #ccc",
overflow: "hidden",
}}
>
{previewColors.map((color, colorIdx) => (
<Box
key={colorIdx}
sx={{
flex: 1,
backgroundColor: color,
}}
/>
))}
</Box>
</Box>
</MenuItem>
);
})}
</Select>
</FormControl>
);
}
if (styleConfig.colorType === "custom") {
return (
<Box className="mt-3">
<Typography variant="subtitle2" gutterBottom>
</Typography>
<Box
className="flex flex-col gap-2"
sx={{ maxHeight: "160px", overflowY: "auto", paddingTop: "4px" }}
>
{Array.from({ length: styleConfig.segments }).map((_, idx) => {
const color =
(styleConfig.customColors && styleConfig.customColors[idx]) ||
"rgba(0,0,0,1)";
return (
<Box key={idx} className="flex items-center gap-2">
<Typography variant="caption" sx={{ width: 40 }}>
{idx + 1}
</Typography>
<input
type="color"
value={rgbaToHex(color)}
onChange={(e) => {
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",
}}
/>
</Box>
);
})}
</Box>
</Box>
);
}
};
// 根据不同图层的类型和颜色分类方案显示不同的大小设置
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 (
<Box className="mt-3">
<Typography gutterBottom>
: {styleConfig.minSize} - {styleConfig.maxSize}
</Typography>
<Box className="flex items-center gap-4">
<Box className="flex-1">
<Typography variant="caption" gutterBottom>
</Typography>
<Slider
value={styleConfig.minSize}
onChange={(_, value) =>
setStyleConfig((prev) => ({
...prev,
minSize: value as number,
}))
}
min={2}
max={8}
step={1}
size="small"
/>
</Box>
<Box className="flex-1">
<Typography variant="caption" gutterBottom>
</Typography>
<Slider
value={styleConfig.maxSize}
onChange={(_, value) =>
setStyleConfig((prev) => ({
...prev,
maxSize: value as number,
}))
}
min={10}
max={16}
step={1}
size="small"
/>
</Box>
</Box>
{/* 点大小预览 */}
<Box className="flex items-center gap-2 mt-2 p-2 bg-gray-50 rounded">
<Typography variant="caption">:</Typography>
<Box
sx={{
width: styleConfig.minSize,
height: styleConfig.minSize,
borderRadius: "50%",
backgroundColor: colors[0],
}}
/>
<Typography variant="caption"></Typography>
<Box
sx={{
width: styleConfig.maxSize,
height: styleConfig.maxSize,
borderRadius: "50%",
backgroundColor: colors[colors.length - 1],
}}
/>
</Box>
</Box>
);
}
if (selectedRenderLayer?.get("type") === "linestring") {
return (
<Box className="mt-3">
{/* 勾选项:是否根据属性调整线条宽度 */}
<FormControlLabel
control={
<Checkbox
checked={styleConfig.adjustWidthByProperty}
onChange={(e) =>
setStyleConfig((prev) => ({
...prev,
adjustWidthByProperty: e.target.checked,
}))
}
disabled={styleConfig.colorType === "single"}
/>
}
label="根据数值分段调整线条宽度"
/>
{styleConfig.adjustWidthByProperty && (
<>
<Typography gutterBottom>
线: {styleConfig.minStrokeWidth} -{" "}
{styleConfig.maxStrokeWidth}px
</Typography>
<Box className="flex items-center gap-4">
<Box className="flex-1">
<Typography variant="caption" gutterBottom>
</Typography>
<Slider
value={styleConfig.minStrokeWidth}
onChange={(_, value) =>
setStyleConfig((prev) => ({
...prev,
minStrokeWidth: value as number,
}))
}
min={1}
max={4}
step={0.5}
size="small"
/>
</Box>
<Box className="flex-1">
<Typography variant="caption" gutterBottom>
</Typography>
<Slider
value={styleConfig.maxStrokeWidth}
onChange={(_, value) =>
setStyleConfig((prev) => ({
...prev,
maxStrokeWidth: value as number,
}))
}
min={6}
max={12}
step={0.5}
size="small"
/>
</Box>
</Box>
{/* 线条宽度预览 */}
<Box className="flex items-center gap-2 mt-2 p-2 bg-gray-50 rounded">
<Typography variant="caption">:</Typography>
<Box
sx={{
width: 50,
height: styleConfig.minStrokeWidth,
backgroundColor: colors[0],
border: `1px solid ${colors[0]}`,
borderRadius: 1,
}}
/>
<Typography variant="caption"></Typography>
<Box
sx={{
width: 50,
height: styleConfig.maxStrokeWidth,
backgroundColor: colors[colors.length - 1],
border: `1px solid ${colors[colors.length - 1]}`,
borderRadius: 1,
}}
/>
</Box>
</>
)}
{!styleConfig.adjustWidthByProperty && (
<>
<Typography gutterBottom>
线: {styleConfig.fixedStrokeWidth}px
</Typography>
<Slider
value={styleConfig.fixedStrokeWidth}
onChange={(_, value) =>
setStyleConfig((prev) => ({
...prev,
fixedStrokeWidth: value as number,
}))
}
min={1}
max={10}
step={0.5}
size="small"
/>
{/* 固定宽度预览 */}
<Box className="flex items-center gap-2 mt-2 p-2 bg-gray-50 rounded">
<Typography variant="caption">:</Typography>
<Box
sx={{
width: 50,
height: styleConfig.fixedStrokeWidth,
backgroundColor: colors[0],
border: `1px solid ${colors[0]}`,
borderRadius: 1,
}}
/>
</Box>
</>
)}
</Box>
);
}
};
return (
<>
<div className="absolute top-20 left-4 bg-white p-4 rounded-xl shadow-lg opacity-95 hover:opacity-100 transition-opacity w-80 z-1300">
{/* 图层选择 */}
<FormControl variant="standard" fullWidth margin="dense">
<InputLabel></InputLabel>
<Select
value={
selectedRenderLayer
? renderLayers.indexOf(selectedRenderLayer)
: ""
}
onChange={(e) => {
const index = e.target.value as number;
const newLayer = index >= 0 ? renderLayers[index] : undefined;
setSelectedRenderLayer(newLayer);
// 检查新图层是否有缓存的样式,没有才清空
if (newLayer) {
const layerId = newLayer.get("value");
const cachedStyleState = layerStyleStates.find(
(state) => state.layerId === layerId
);
// 只有在没有缓存时才清空属性
if (!cachedStyleState) {
setStyleConfig((prev) => ({ ...prev, property: "" }));
}
}
}}
>
{renderLayers.map((layer, index) => {
const name = layer.get("name");
return (
<MenuItem key={index} value={index}>
{name}
</MenuItem>
);
})}
</Select>
</FormControl>
{/* 属性选择 */}
<FormControl variant="standard" fullWidth margin="dense">
<InputLabel></InputLabel>
<Select
value={styleConfig.property}
onChange={(e) => {
setStyleConfig((prev) => ({ ...prev, property: e.target.value }));
}}
disabled={!selectedRenderLayer}
>
{availableProperties.map((prop) => (
<MenuItem key={prop.name} value={prop.value}>
{prop.name}
</MenuItem>
))}
</Select>
</FormControl>
{/* 分类选择 */}
<FormControl variant="standard" fullWidth margin="dense">
<InputLabel></InputLabel>
<Select
value={styleConfig.classificationMethod}
onChange={(e) => {
setStyleConfig((prev) => ({
...prev,
classificationMethod: e.target.value,
}));
}}
>
{CLASSIFICATION_METHODS.map((method) => (
<MenuItem key={method.value} value={method.value}>
{method.name}
</MenuItem>
))}
</Select>
</FormControl>
{/* 分类数量 */}
<Box className="mt-3">
<Typography gutterBottom>: {styleConfig.segments}</Typography>
<Slider
value={styleConfig.segments}
onChange={(_, value) =>
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,
customColors: newCustomColors,
};
})
}
min={2}
max={10}
step={1}
marks
size="small"
/>
</Box>
{/* 自定义分类:手动填写区间分段(仅当选择自定义方式时显示) */}
{styleConfig.classificationMethod === "custom_breaks" && (
<Box className="mt-3 p-2 bg-gray-50 rounded">
<Typography variant="subtitle2" gutterBottom>
{">="} 0
</Typography>
<Box
className="flex flex-col gap-2"
sx={{ maxHeight: "160px", overflowY: "auto", paddingTop: "12px" }}
>
{Array.from({ length: styleConfig.segments }).map((_, idx) => (
<TextField
key={idx}
label={`阈值 ${idx + 1}`}
type="number"
size="small"
slotProps={{ input: { inputProps: { min: 0, step: 0.1 } } }}
value={
(styleConfig.customBreaks &&
styleConfig.customBreaks[idx]) ??
""
}
onChange={(e) => {
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 };
});
}}
/>
))}
</Box>
</Box>
)}
{/* 颜色方案 */}
<FormControl variant="standard" fullWidth margin="dense">
<InputLabel>
<ColorLensIcon className="mr-1" />
</InputLabel>
<Select
value={styleConfig.colorType}
onChange={(e) => {
const newColorType = e.target.value;
setStyleConfig((prev) => {
let newCustomColors = prev.customColors;
if (
newColorType === "custom" &&
(!prev.customColors || prev.customColors.length === 0)
) {
const baseColors = RAINBOW_PALETTES[0].colors;
newCustomColors = Array.from(
{ length: prev.segments },
(_, i) => baseColors[i % baseColors.length]
);
}
return {
...prev,
colorType: newColorType,
customColors: newCustomColors,
};
});
}}
>
<MenuItem value="single"></MenuItem>
<MenuItem value="gradient"></MenuItem>
<MenuItem value="rainbow"></MenuItem>
<MenuItem value="custom"></MenuItem>
</Select>
{getColorSetting()}
</FormControl>
{/* 大小设置 */}
{getSizeSetting()}
{/* 透明度设置 */}
<Box className="mt-3">
<Typography gutterBottom>
: {(styleConfig.opacity * 100).toFixed(0)}%
</Typography>
<Slider
value={styleConfig.opacity}
onChange={(_, value) =>
setStyleConfig((prev) => ({
...prev,
opacity: value as number,
}))
}
min={0.1}
max={1}
step={0.05}
size="small"
/>
</Box>
{/* 是否显示属性文字 */}
<FormControlLabel
control={
<Checkbox
checked={styleConfig.showLabels}
onChange={(e) =>
setStyleConfig((prev) => ({
...prev,
showLabels: e.target.checked,
}))
}
/>
}
label="显示属性(缩放 >=15 级时显示)"
/>
<div className="my-3"></div>
{/* 操作按钮 */}
<Box className="flex gap-2">
<Button
variant="contained"
color="primary"
onClick={() => {
setStyleState();
}}
disabled={!selectedRenderLayer || !styleConfig.property}
startIcon={<ApplyIcon />}
fullWidth
>
</Button>
<Button
variant="outlined"
onClick={() => {
resetStyle();
}}
disabled={!selectedRenderLayer}
startIcon={<ResetIcon />}
fullWidth
>
</Button>
</Box>
</div>
</>
);
};
export default StyleEditorPanel;