1768 lines
59 KiB
TypeScript
1768 lines
59 KiB
TypeScript
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,
|
||
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;
|