地图样式新增自定义分类
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
|
TextField,
|
||||||
Box,
|
Box,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
@@ -46,6 +47,7 @@ interface StyleConfig {
|
|||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
adjustWidthByProperty: boolean; // 是否根据属性调整线条宽度
|
adjustWidthByProperty: boolean; // 是否根据属性调整线条宽度
|
||||||
|
customBreaks: number[]; // 自定义断点(用于 custom_breaks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图层样式状态接口
|
// 图层样式状态接口
|
||||||
@@ -143,6 +145,7 @@ const CLASSIFICATION_METHODS = [
|
|||||||
{ name: "优雅分段", value: "pretty_breaks" },
|
{ name: "优雅分段", value: "pretty_breaks" },
|
||||||
// 浏览器中实现Jenks算法性能较差,暂时移除
|
// 浏览器中实现Jenks算法性能较差,暂时移除
|
||||||
// { name: "自然间断", value: "jenks_optimized" },
|
// { name: "自然间断", value: "jenks_optimized" },
|
||||||
|
{ name: "自定义", value: "custom_breaks" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||||
@@ -193,6 +196,7 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
showLabels: false,
|
showLabels: false,
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
adjustWidthByProperty: true,
|
adjustWidthByProperty: true,
|
||||||
|
customBreaks: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 颜色方案选择
|
// 颜色方案选择
|
||||||
@@ -305,6 +309,32 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
const layerId = selectedRenderLayer.get("value");
|
const layerId = selectedRenderLayer.get("value");
|
||||||
const property = styleConfig.property;
|
const property = styleConfig.property;
|
||||||
if (layerId !== undefined && property !== undefined) {
|
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 (layerId === "junctions") {
|
||||||
if (setJunctionText && setShowJunctionText) {
|
if (setJunctionText && setShowJunctionText) {
|
||||||
@@ -351,11 +381,30 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
|
|
||||||
// 更新节点数据属性
|
// 更新节点数据属性
|
||||||
const segments = junctionStyleConfigState?.styleConfig.segments ?? 5;
|
const segments = junctionStyleConfigState?.styleConfig.segments ?? 5;
|
||||||
const breaks = calculateClassification(
|
let breaks: number[] = [];
|
||||||
currentJunctionCalData.map((d) => d.value),
|
if (
|
||||||
segments,
|
junctionStyleConfigState?.styleConfig.classificationMethod ===
|
||||||
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) {
|
if (breaks.length === 0) {
|
||||||
console.warn("计算的 breaks 为空,无法应用样式");
|
console.warn("计算的 breaks 为空,无法应用样式");
|
||||||
return;
|
return;
|
||||||
@@ -373,11 +422,29 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
);
|
);
|
||||||
// 更新管道数据属性
|
// 更新管道数据属性
|
||||||
const segments = pipeStyleConfigState?.styleConfig.segments ?? 5;
|
const segments = pipeStyleConfigState?.styleConfig.segments ?? 5;
|
||||||
const breaks = calculateClassification(
|
let breaks: number[] = [];
|
||||||
currentPipeCalData.map((d) => d.value),
|
if (
|
||||||
segments,
|
pipeStyleConfigState?.styleConfig.classificationMethod ===
|
||||||
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);
|
if (pipeStyleConfigState) applyLayerStyle(pipeStyleConfigState, breaks);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -784,6 +851,45 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
}
|
}
|
||||||
}, [styleConfig.colorType]);
|
}, [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 = () => {
|
const getColorSetting = () => {
|
||||||
if (styleConfig.colorType === "single") {
|
if (styleConfig.colorType === "single") {
|
||||||
return (
|
return (
|
||||||
@@ -1222,13 +1328,67 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
onChange={(_, value) =>
|
onChange={(_, value) =>
|
||||||
setStyleConfig((prev) => ({ ...prev, segments: value as number }))
|
setStyleConfig((prev) => ({ ...prev, segments: value as number }))
|
||||||
}
|
}
|
||||||
min={3}
|
min={2}
|
||||||
max={10}
|
max={10}
|
||||||
step={1}
|
step={1}
|
||||||
marks
|
marks
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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">
|
||||||
|
{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">
|
<FormControl variant="standard" fullWidth margin="dense">
|
||||||
<InputLabel>
|
<InputLabel>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ interface StyleConfig {
|
|||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
adjustWidthByProperty: boolean;
|
adjustWidthByProperty: boolean;
|
||||||
|
customBreaks: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegendStyleConfig {
|
interface LegendStyleConfig {
|
||||||
@@ -98,6 +99,7 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
|
|||||||
showLabels: false,
|
showLabels: false,
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
adjustWidthByProperty: true,
|
adjustWidthByProperty: true,
|
||||||
|
customBreaks: [],
|
||||||
},
|
},
|
||||||
legendConfig: {
|
legendConfig: {
|
||||||
layerId: "junctions",
|
layerId: "junctions",
|
||||||
@@ -128,6 +130,7 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
|
|||||||
showLabels: false,
|
showLabels: false,
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
adjustWidthByProperty: true,
|
adjustWidthByProperty: true,
|
||||||
|
customBreaks: [],
|
||||||
},
|
},
|
||||||
legendConfig: {
|
legendConfig: {
|
||||||
layerId: "pipes",
|
layerId: "pipes",
|
||||||
|
|||||||
Reference in New Issue
Block a user