地图样式新增自定义分类

This commit is contained in:
JIANG
2025-11-17 17:32:22 +08:00
parent 8dde587745
commit 55d41a127d
2 changed files with 174 additions and 11 deletions

View File

@@ -12,6 +12,7 @@ import {
Slider,
Typography,
Button,
TextField,
Box,
Checkbox,
FormControlLabel,
@@ -46,6 +47,7 @@ interface StyleConfig {
showLabels: boolean;
opacity: number;
adjustWidthByProperty: boolean; // 是否根据属性调整线条宽度
customBreaks: number[]; // 自定义断点(用于 custom_breaks
}
// 图层样式状态接口
@@ -143,6 +145,7 @@ const CLASSIFICATION_METHODS = [
{ name: "优雅分段", value: "pretty_breaks" },
// 浏览器中实现Jenks算法性能较差暂时移除
// { name: "自然间断", value: "jenks_optimized" },
{ name: "自定义", value: "custom_breaks" },
];
const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
@@ -193,6 +196,7 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
showLabels: false,
opacity: 0.9,
adjustWidthByProperty: true,
customBreaks: [],
});
// 颜色方案选择
@@ -305,6 +309,32 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
const layerId = selectedRenderLayer.get("value");
const property = styleConfig.property;
if (layerId !== undefined && property !== undefined) {
// 验证自定义断点设置
if (styleConfig.classificationMethod === "custom_breaks") {
const expected = styleConfig.segments + 1;
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 && setShowJunctionText) {
@@ -351,11 +381,30 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
// 更新节点数据属性
const segments = junctionStyleConfigState?.styleConfig.segments ?? 5;
const breaks = calculateClassification(
let breaks: number[] = [];
if (
junctionStyleConfigState?.styleConfig.classificationMethod ===
"custom_breaks"
) {
// 使用自定义断点(保证为 segments + 1 个断点,按升序)
const desired = segments + 1;
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;
@@ -373,11 +422,29 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
);
// 更新管道数据属性
const segments = pipeStyleConfigState?.styleConfig.segments ?? 5;
const breaks = calculateClassification(
let breaks: number[] = [];
if (
pipeStyleConfigState?.styleConfig.classificationMethod ===
"custom_breaks"
) {
// 使用自定义断点(保证为 segments + 1 个断点,按升序)
const desired = segments + 1;
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 (pipeStyleConfigState) applyLayerStyle(pipeStyleConfigState, breaks);
}
};
@@ -784,6 +851,45 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
}
}, [styleConfig.colorType]);
// 初始化或调整自定义断点数组长度,默认使用 pretty_breaks 生成若存在数据
useEffect(() => {
if (styleConfig.classificationMethod !== "custom_breaks") return;
const numBreaks = styleConfig.segments + 1;
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,
]);
const getColorSetting = () => {
if (styleConfig.colorType === "single") {
return (
@@ -1222,13 +1328,67 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
onChange={(_, value) =>
setStyleConfig((prev) => ({ ...prev, segments: value as number }))
}
min={3}
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>
&gt;= 0
</Typography>
<Box className="flex flex-col gap-2">
{Array.from({ length: styleConfig.segments + 1 }).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 + 1)
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>
<Typography variant="caption" color="textSecondary">
: 阈值数量由分类数量决定 (segments + 1) segments=5
6
</Typography>
</Box>
)}
{/* 颜色方案 */}
<FormControl variant="standard" fullWidth margin="dense">
<InputLabel>

View File

@@ -37,6 +37,7 @@ interface StyleConfig {
showLabels: boolean;
opacity: number;
adjustWidthByProperty: boolean;
customBreaks: number[];
}
interface LegendStyleConfig {
@@ -98,6 +99,7 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
showLabels: false,
opacity: 0.9,
adjustWidthByProperty: true,
customBreaks: [],
},
legendConfig: {
layerId: "junctions",
@@ -128,6 +130,7 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
showLabels: false,
opacity: 0.9,
adjustWidthByProperty: true,
customBreaks: [],
},
legendConfig: {
layerId: "pipes",