From 55d41a127dd0f15008e4357c5fed4e67b69a9822 Mon Sep 17 00:00:00 2001 From: JIANG Date: Mon, 17 Nov 2025 17:32:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=B0=E5=9B=BE=E6=A0=B7=E5=BC=8F=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=87=AA=E5=AE=9A=E4=B9=89=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/OlMap/Controls/StyleEditorPanel.tsx | 182 ++++++++++++++++++-- src/app/OlMap/Controls/Toolbar.tsx | 3 + 2 files changed, 174 insertions(+), 11 deletions(-) diff --git a/src/app/OlMap/Controls/StyleEditorPanel.tsx b/src/app/OlMap/Controls/StyleEditorPanel.tsx index 82d1848..626ec6d 100644 --- a/src/app/OlMap/Controls/StyleEditorPanel.tsx +++ b/src/app/OlMap/Controls/StyleEditorPanel.tsx @@ -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 = ({ @@ -193,6 +196,7 @@ const StyleEditorPanel: React.FC = ({ showLabels: false, opacity: 0.9, adjustWidthByProperty: true, + customBreaks: [], }); // 颜色方案选择 @@ -305,6 +309,32 @@ const StyleEditorPanel: React.FC = ({ 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 = ({ // 更新节点数据属性 const segments = junctionStyleConfigState?.styleConfig.segments ?? 5; - const breaks = calculateClassification( - currentJunctionCalData.map((d) => d.value), - segments, - styleConfig.classificationMethod - ); + 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 = ({ ); // 更新管道数据属性 const segments = pipeStyleConfigState?.styleConfig.segments ?? 5; - const breaks = calculateClassification( - currentPipeCalData.map((d) => d.value), - segments, - styleConfig.classificationMethod - ); + 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 = ({ } }, [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 = ({ onChange={(_, value) => setStyleConfig((prev) => ({ ...prev, segments: value as number })) } - min={3} + min={2} max={10} step={1} marks size="small" /> + {/* 自定义分类:手动填写区间分段(仅当选择自定义方式时显示) */} + {styleConfig.classificationMethod === "custom_breaks" && ( + + + 手动设置区间阈值(按升序填写,最小值 >= 0) + + + {Array.from({ length: styleConfig.segments + 1 }).map( + (_, idx) => ( + { + 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 }; + }); + }} + /> + ) + )} + + + 注: 阈值数量由分类数量决定 (segments + 1)。例如 segments=5 将显示 + 6 个阈值。 + + + )} {/* 颜色方案 */} diff --git a/src/app/OlMap/Controls/Toolbar.tsx b/src/app/OlMap/Controls/Toolbar.tsx index ff935f5..2db6946 100644 --- a/src/app/OlMap/Controls/Toolbar.tsx +++ b/src/app/OlMap/Controls/Toolbar.tsx @@ -37,6 +37,7 @@ interface StyleConfig { showLabels: boolean; opacity: number; adjustWidthByProperty: boolean; + customBreaks: number[]; } interface LegendStyleConfig { @@ -98,6 +99,7 @@ const Toolbar: React.FC = ({ hiddenButtons, queryType }) => { showLabels: false, opacity: 0.9, adjustWidthByProperty: true, + customBreaks: [], }, legendConfig: { layerId: "junctions", @@ -128,6 +130,7 @@ const Toolbar: React.FC = ({ hiddenButtons, queryType }) => { showLabels: false, opacity: 0.9, adjustWidthByProperty: true, + customBreaks: [], }, legendConfig: { layerId: "pipes",