张世豪
6 天以前 1cf1ecbc75c6d14b40efb3161e7db0b8b64f7de2
新增有障碍物的路径规划算法和优化没有障碍物的路径算法
已添加2个文件
已修改9个文件
1695 ■■■■ 文件已修改
Obstacledge.properties 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
dikuai.properties 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuaiguanli.java 280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/Lunjingguihua.java 95 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/MowingPathGenerationPage.java 634 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/ObstaclePathPlanner.java 536 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhangaiwu/Obstacledraw.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/LegendDialog.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Obstacledge.properties
@@ -1,5 +1,5 @@
# å‰²è‰æœºåœ°å—障碍物配置文件
# ç”Ÿæˆæ—¶é—´ï¼š2025-12-09T11:53:37.128295200
# ç”Ÿæˆæ—¶é—´ï¼š2025-12-17T12:03:32.881913900
# åæ ‡ç³»ï¼šWGS84(度分格式)
# ============ åœ°å—基准站配置 ============
dikuai.properties
@@ -1,19 +1,19 @@
#Dikuai Properties
#Fri Dec 12 15:42:15 CST 2025
#Wed Dec 17 12:03:32 CST 2025
LAND1.angleThreshold=-1
LAND1.baseStationCoordinates=3949.90238860,N,11616.75692000,E
LAND1.boundaryCoordinates=1.31,-9.59;1.86,-11.61;3.12,-12.49;5.50,-12.06;5.95,-10.88;4.97,-3.86;3.16,-0.87;2.79,-2.61;2.42,-4.35;2.05,-6.10;1.68,-7.84;1.31,-9.59
LAND1.boundaryOriginalCoordinates=39.831620,116.279297,39.70;39.831618,116.279298,39.80;39.831616,116.279298,39.70;39.831614,116.279298,39.70;39.831612,116.279299,39.70;39.831610,116.279299,39.70;39.831608,116.279300,39.70;39.831606,116.279301,39.70;39.831604,116.279303,39.70;39.831602,116.279304,39.70;39.831600,116.279306,39.70;39.831598,116.279307,39.70;39.831598,116.279309,39.70;39.831597,116.279312,39.60;39.831596,116.279314,39.60;39.831595,116.279317,39.60;39.831594,116.279319,39.60;39.831594,116.279320,39.60;39.831594,116.279323,39.60;39.831595,116.279326,39.60;39.831595,116.279329,39.60;39.831595,116.279331,39.70;39.831595,116.279334,39.70;39.831596,116.279337,39.60;39.831596,116.279339,39.60;39.831596,116.279342,39.60;39.831597,116.279344,39.60;39.831598,116.279346,39.70;39.831600,116.279348,39.70;39.831601,116.279349,39.70;39.831603,116.279350,39.80;39.831605,116.279350,39.70;39.831606,116.279351,39.70;39.831609,116.279352,39.80;39.831611,116.279352,39.70;39.831613,116.279352,39.70;39.831615,116.279352,39.70;39.831617,116.279353,39.70;39.831619,116.279353,39.70;39.831621,116.279353,39.80;39.831623,116.279353,39.80;39.831625,116.279353,39.80;39.831627,116.279353,39.80;39.831629,116.279352,39.80;39.831631,116.279352,39.80;39.831634,116.279351,39.70;39.831636,116.279351,39.70;39.831637,116.279350,39.70;39.831639,116.279350,39.80;39.831641,116.279349,39.70;39.831643,116.279348,39.70;39.831645,116.279348,39.70;39.831647,116.279347,39.70;39.831649,116.279346,39.80;39.831651,116.279346,39.80;39.831653,116.279345,39.80;39.831655,116.279345,39.80;39.831657,116.279344,39.80;39.831659,116.279343,39.70;39.831661,116.279343,39.70;39.831663,116.279342,39.70;39.831665,116.279342,39.70;39.831667,116.279341,39.70;39.831670,116.279341,39.70;39.831672,116.279340,39.70;39.831674,116.279339,39.80;39.831676,116.279338,39.80;39.831678,116.279337,39.80;39.831679,116.279336,39.70;39.831680,116.279334,39.70;39.831681,116.279332,39.70;39.831683,116.279331,39.70;39.831684,116.279329,39.70;39.831686,116.279327,39.70;39.831687,116.279325,39.70;39.831689,116.279323,39.70;39.831691,116.279322,39.70;39.831693,116.279321,39.70;39.831694,116.279320,39.70;39.831696,116.279319,39.70;39.831699,116.279319,39.70
LAND1.boundaryCoordinates=77.19,-32.68;80.71,-54.97;80.99,-55.90;83.54,-56.46;85.04,-55.55;85.94,-53.74;83.24,-35.82;84.55,-34.54;94.02,-31.92;94.10,-31.11;90.88,-20.39;90.35,-19.53;88.33,-19.00;84.12,-19.47;78.92,-22.36;76.63,-25.55;76.93,-29.84;77.06,-31.26;77.19,-32.68
LAND1.boundaryOriginalCoordinates=39.831413,116.280186,49.12;39.831409,116.280188,49.09;39.831403,116.280187,49.12;39.831395,116.280189,49.13;39.831388,116.280191,49.16;39.831379,116.280193,49.18;39.831370,116.280194,49.16;39.831362,116.280195,49.15;39.831353,116.280197,49.11;39.831344,116.280200,49.15;39.831335,116.280202,49.15;39.831326,116.280204,49.08;39.831317,116.280206,49.19;39.831309,116.280209,49.18;39.831301,116.280210,49.19;39.831293,116.280212,49.11;39.831284,116.280214,49.06;39.831275,116.280215,49.16;39.831266,116.280217,49.14;39.831258,116.280220,49.08;39.831249,116.280223,49.09;39.831240,116.280225,49.10;39.831231,116.280226,49.04;39.831222,116.280226,49.17;39.831212,116.280227,49.11;39.831204,116.280230,49.09;39.831201,116.280238,49.10;39.831199,116.280249,49.07;39.831199,116.280260,49.21;39.831202,116.280270,49.16;39.831207,116.280278,49.06;39.831212,116.280284,49.04;39.831217,116.280287,49.05;39.831223,116.280288,49.09;39.831229,116.280287,49.10;39.831237,116.280286,49.05;39.831245,116.280286,49.08;39.831254,116.280284,49.07;39.831263,116.280283,49.05;39.831272,116.280280,49.11;39.831282,116.280278,49.10;39.831291,116.280276,49.11;39.831300,116.280274,49.16;39.831308,116.280270,49.13;39.831318,116.280268,49.10;39.831327,116.280267,49.14;39.831337,116.280266,49.08;39.831347,116.280263,49.10;39.831356,116.280261,49.20;39.831366,116.280258,49.14;39.831375,116.280256,49.09;39.831384,116.280257,49.13;39.831392,116.280263,49.10;39.831396,116.280272,49.12;39.831398,116.280283,49.16;39.831401,116.280294,49.11;39.831403,116.280307,49.13;39.831405,116.280318,49.19;39.831406,116.280328,49.20;39.831408,116.280340,49.22;39.831411,116.280353,49.19;39.831414,116.280363,49.26;39.831416,116.280374,49.22;39.831419,116.280383,49.20;39.831427,116.280384,49.21;39.831433,116.280379,49.17;39.831441,116.280375,49.19;39.831451,116.280372,49.09;39.831459,116.280370,49.16;39.831467,116.280364,49.21;39.831476,116.280360,49.22;39.831485,116.280357,49.20;39.831495,116.280355,49.26;39.831505,116.280351,49.21;39.831514,116.280348,49.17;39.831523,116.280346,49.20;39.831531,116.280340,49.04;39.831535,116.280328,49.08;39.831536,116.280316,49.03;39.831535,116.280304,49.03;39.831533,116.280291,49.06;39.831532,116.280279,49.07;39.831531,116.280267,49.11;39.831528,116.280257,49.09;39.831525,116.280246,49.11;39.831521,116.280237,49.09;39.831516,116.280227,49.08;39.831511,116.280216,49.12;39.831505,116.280206,49.14;39.831499,116.280197,49.12;39.831492,116.280189,49.15;39.831484,116.280184,49.14;39.831477,116.280179,49.12;39.831469,116.280178,49.12;39.831462,116.280181,49.13;39.831454,116.280182,49.12;39.831445,116.280183,49.12;39.831439,116.280183,49.14;39.831438,116.280183,49.12
LAND1.boundaryPointInterval=-1
LAND1.createTime=2025-12-09 11\:16\:40
LAND1.createTime=2025-12-16 15\:43\:39
LAND1.intelligentSceneAnalysis=-1
LAND1.landArea=35.87
LAND1.landName=1234
LAND1.landArea=327.17
LAND1.landName=yyii
LAND1.landNumber=LAND1
LAND1.mowingPattern=螺旋式
LAND1.mowingTrack=5.952,-10.672
LAND1.mowingPattern=平行线
LAND1.mowingTrack=
LAND1.mowingWidth=40
LAND1.plannedPath=1.88,-7.88;2.62,-4.39;3.25,-1.41;4.78,-3.93;5.74,-10.86;5.35,-11.88;3.17,-12.28;2.03,-11.49;1.52,-9.58;1.88,-7.88;2.27,-7.96;3.01,-4.47;3.43,-2.48;4.39,-4.07;5.33,-10.81;5.06,-11.53;3.26,-11.86;2.38,-11.24;1.93,-9.57;2.27,-7.96;2.66,-8.05;3.40,-4.56;3.61,-3.55;4.01,-4.20;4.92,-10.76;4.77,-11.18;3.35,-11.43;2.73,-11.00;2.34,-9.56;2.66,-8.05;3.05,-8.13;3.71,-4.99;4.51,-10.72;4.47,-10.82;3.44,-11.01;3.08,-10.75;2.75,-9.55;3.05,-8.13;3.44,-8.21;3.63,-7.30;4.08,-10.49;3.54,-10.59;3.43,-10.51;3.16,-9.54;3.44,-8.21
LAND1.plannedPath=77.17,-29.65;81.28,-55.71;81.70,-55.80;76.93,-25.57;77.26,-25.10;82.12,-55.89;82.54,-55.98;77.59,-24.64;77.92,-24.18;82.96,-56.08;83.38,-56.17;78.25,-23.72;78.59,-23.25;83.76,-56.03;84.13,-55.81;78.92,-22.79;79.27,-22.45;84.50,-55.58;84.87,-55.33;79.64,-22.24;80.01,-22.04;85.18,-54.72;85.48,-54.10;80.39,-21.83;80.76,-21.62;83.00,-35.83;83.34,-35.38;81.13,-21.42;81.50,-21.21;83.69,-35.03;84.04,-34.69;81.88,-21.00;82.25,-20.80;84.39,-34.35;84.77,-34.22;82.62,-20.59;82.99,-20.38;85.16,-34.11;85.55,-34.00;83.37,-20.18;83.74,-19.97;85.94,-33.90;86.32,-33.79;84.11,-19.76;84.50,-19.68;86.71,-33.68;87.10,-33.57;84.90,-19.63;85.30,-19.59;87.49,-33.47;87.88,-33.36;85.70,-19.55;86.09,-19.50;88.26,-33.25;88.65,-33.15;86.49,-19.46;86.89,-19.41;89.04,-33.04;89.43,-32.93;87.29,-19.37;87.69,-19.32;89.82,-32.82;90.20,-32.72;88.08,-19.28;88.49,-19.30;90.59,-32.61;90.98,-32.50;88.91,-19.41;89.34,-19.52;91.37,-32.39;91.76,-32.29;89.76,-19.63;90.18,-19.74;92.14,-32.18;92.53,-32.07;90.76,-20.88;91.62,-23.72;92.92,-31.96;93.31,-31.86;92.47,-26.56;93.33,-29.40;93.70,-31.75
LAND1.returnPointCoordinates=-1
LAND1.updateTime=2025-12-09 11\:53\:37
LAND1.updateTime=2025-12-17 12\:03\:32
LAND1.userId=-1
set.properties
@@ -1,16 +1,16 @@
#Mower Configuration Properties - Updated
#Tue Dec 16 16:27:10 CST 2025
#Wed Dec 17 12:04:00 CST 2025
appVersion=-1
currentWorkLandNumber=LAND1
cuttingWidth=200
firmwareVersion=-1
handheldMarkerId=
idleTrailDurationSeconds=60
mapScale=20.09
mapScale=15.31
mowerId=1234
serialAutoConnect=true
serialBaudRate=115200
serialPortName=COM15
simCardNumber=-1
viewCenterX=0.55
viewCenterY=12.53
viewCenterX=-85.37
viewCenterY=37.73
src/dikuai/Dikuaiguanli.java
@@ -24,6 +24,7 @@
import java.util.Properties;
import lujing.Lunjingguihua;
import lujing.MowingPathGenerationPage;
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
import zhuye.MapRenderer;
@@ -334,8 +335,8 @@
        JButton deleteBtn = createDeleteButton();
        deleteBtn.addActionListener(e -> deleteDikuai(dikuai));
        JButton generatePathBtn = createPrimaryFooterButton("生成割草路径");
        generatePathBtn.addActionListener(e -> generateMowingPath(dikuai));
        JButton generatePathBtn = createPrimaryFooterButton("路径规划");
        generatePathBtn.addActionListener(e -> showPathPlanningPage(dikuai));
        JPanel footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        footerPanel.setBackground(CARD_BACKGROUND);
@@ -828,6 +829,77 @@
        return trimmed;
    }
    /**
     * æ˜¾ç¤ºè·¯å¾„规划页面
     */
    private void showPathPlanningPage(Dikuai dikuai) {
        if (dikuai == null) {
            return;
        }
        Window owner = SwingUtilities.getWindowAncestor(this);
        // èŽ·å–åœ°å—åŸºæœ¬æ•°æ®
        String baseStationValue = prepareCoordinateForEditor(dikuai.getBaseStationCoordinates());
        String boundaryValue = prepareCoordinateForEditor(dikuai.getBoundaryCoordinates());
        List<Obstacledge.Obstacle> configuredObstacles = getConfiguredObstacles(dikuai);
        String obstacleValue = determineInitialObstacleValue(dikuai, configuredObstacles);
        String widthValue = sanitizeWidthString(dikuai.getMowingWidth());
        if (widthValue != null) {
            try {
                double widthCm = Double.parseDouble(widthValue);
                widthValue = formatWidthForStorage(widthCm);
            } catch (NumberFormatException ignored) {
                // ä¿æŒåŽŸå§‹å­—ç¬¦ä¸²ï¼Œç¨åŽæ ¡éªŒæç¤º
            }
        }
        String modeValue = sanitizeValueOrNull(dikuai.getMowingPattern());
        String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath());
        // åˆ›å»ºä¿å­˜å›žè°ƒæŽ¥å£å®žçް
        MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() {
            @Override
            public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) {
                return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value);
            }
            @Override
            public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) {
                return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value);
            }
            @Override
            public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) {
                return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue);
            }
            @Override
            public boolean saveMowingWidth(Dikuai dikuai, String value) {
                return saveFieldAndRefresh(dikuai, "mowingWidth", value);
            }
            @Override
            public boolean savePlannedPath(Dikuai dikuai, String value) {
                return saveFieldAndRefresh(dikuai, "plannedPath", value);
            }
        };
        // æ˜¾ç¤ºè·¯å¾„规划页面
        MowingPathGenerationPage dialog = new MowingPathGenerationPage(
            owner,
            dikuai,
            baseStationValue,
            boundaryValue,
            obstacleValue,
            widthValue,
            modeValue,
            existingPath,
            callback
        );
        dialog.setVisible(true);
    }
    private void generateMowingPath(Dikuai dikuai) {
        if (dikuai == null) {
            return;
@@ -866,178 +938,48 @@
        String modeValue,
        String initialGeneratedPath) {
        Window owner = SwingUtilities.getWindowAncestor(this);
        JDialog dialog = new JDialog(owner, "生成割草路径", Dialog.ModalityType.APPLICATION_MODAL);
        dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        dialog.getContentPane().setLayout(new BorderLayout());
        dialog.getContentPane().setBackground(BACKGROUND_COLOR);
        JPanel contentPanel = new JPanel();
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
        contentPanel.setBackground(BACKGROUND_COLOR);
        contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 16, 12, 16));
        String landName = getDisplayValue(dikuai.getLandName(), "未知地块");
        String landNumber = getDisplayValue(dikuai.getLandNumber(), "未知编号");
        JLabel headerLabel = new JLabel(landName + " / " + landNumber);
        headerLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
        headerLabel.setForeground(TEXT_COLOR);
        headerLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        contentPanel.add(headerLabel);
        contentPanel.add(Box.createVerticalStrut(12));
        JTextField baseStationField = createInfoTextField(baseStationValue != null ? baseStationValue : "", true);
        contentPanel.add(createTextFieldSection("基站坐标", baseStationField));
        JTextArea boundaryArea = createInfoTextArea(boundaryValue != null ? boundaryValue : "", true, 6);
        contentPanel.add(createTextAreaSection("地块边界", boundaryArea));
        JTextArea obstacleArea = createInfoTextArea(obstacleValue != null ? obstacleValue : "", true, 6);
        contentPanel.add(createTextAreaSection("障碍物坐标", obstacleArea));
        JTextField widthField = createInfoTextField(widthValue != null ? widthValue : "", true);
        contentPanel.add(createTextFieldSection("割草宽度 (厘米)", widthField));
        contentPanel.add(createInfoValueSection("割草模式", formatMowingPatternForDialog(modeValue)));
        String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath());
        String pathSeed = initialGeneratedPath != null ? initialGeneratedPath : existingPath;
        JTextArea pathArea = createInfoTextArea(pathSeed != null ? pathSeed : "", true, 10);
        contentPanel.add(createTextAreaSection("割草路径坐标", pathArea));
        JScrollPane dialogScrollPane = new JScrollPane(contentPanel);
        dialogScrollPane.setBorder(BorderFactory.createEmptyBorder());
        dialogScrollPane.getVerticalScrollBar().setUnitIncrement(16);
        dialog.add(dialogScrollPane, BorderLayout.CENTER);
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 12, 12));
        buttonPanel.setBackground(BACKGROUND_COLOR);
        JButton generateBtn = createPrimaryFooterButton("生成割草路径");
        JButton saveBtn = createPrimaryFooterButton("保存路径");
        JButton cancelBtn = createPrimaryFooterButton("取消");
        generateBtn.addActionListener(e -> {
            String sanitizedWidth = sanitizeWidthString(widthField.getText());
            if (sanitizedWidth != null) {
                try {
                    double widthCm = Double.parseDouble(sanitizedWidth);
                    widthField.setText(formatWidthForStorage(widthCm));
                    sanitizedWidth = formatWidthForStorage(widthCm);
                } catch (NumberFormatException ex) {
                    widthField.setText(sanitizedWidth);
                }
        // åˆ›å»ºä¿å­˜å›žè°ƒæŽ¥å£å®žçް
        MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() {
            @Override
            public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) {
                return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value);
            }
            String generated = attemptMowingPathPreview(
                boundaryArea.getText(),
                obstacleArea.getText(),
                sanitizedWidth,
                modeValue,
                dialog,
                true
            );
            if (generated != null) {
                pathArea.setText(generated);
                pathArea.setCaretPosition(0);
            @Override
            public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) {
                return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value);
            }
        });
        saveBtn.addActionListener(e -> {
            String baseStationNormalized = normalizeCoordinateInput(baseStationField.getText());
            String boundaryNormalized = normalizeCoordinateInput(boundaryArea.getText());
            if (!"-1".equals(boundaryNormalized)) {
                boundaryNormalized = boundaryNormalized
                    .replace("\r\n", ";")
                    .replace('\r', ';')
                    .replace('\n', ';')
                    .replaceAll(";+", ";")
                    .replaceAll("\\s*;\\s*", ";")
                    .trim();
                if (boundaryNormalized.isEmpty()) {
                    boundaryNormalized = "-1";
                }
            @Override
            public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) {
                return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue);
            }
            String obstacleNormalized = normalizeCoordinateInput(obstacleArea.getText());
            if (!"-1".equals(obstacleNormalized)) {
                obstacleNormalized = obstacleNormalized
                    .replace("\r\n", " ")
                    .replace('\r', ' ')
                    .replace('\n', ' ')
                    .replaceAll("\\s{2,}", " ")
                    .trim();
                if (obstacleNormalized.isEmpty()) {
                    obstacleNormalized = "-1";
                }
            @Override
            public boolean saveMowingWidth(Dikuai dikuai, String value) {
                return saveFieldAndRefresh(dikuai, "mowingWidth", value);
            }
            String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : "";
            String widthSanitized = sanitizeWidthString(widthField.getText());
            if (widthSanitized == null) {
                String message = rawWidthInput.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确";
                JOptionPane.showMessageDialog(dialog, message, "提示", JOptionPane.WARNING_MESSAGE);
                return;
            @Override
            public boolean savePlannedPath(Dikuai dikuai, String value) {
                return saveFieldAndRefresh(dikuai, "plannedPath", value);
            }
            double widthCm;
            try {
                widthCm = Double.parseDouble(widthSanitized);
            } catch (NumberFormatException ex) {
                JOptionPane.showMessageDialog(dialog, "割草宽度格式不正确", "提示", JOptionPane.WARNING_MESSAGE);
                return;
            }
            if (widthCm <= 0) {
                JOptionPane.showMessageDialog(dialog, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE);
                return;
            }
            String widthNormalized = formatWidthForStorage(widthCm);
            widthField.setText(widthNormalized);
            String pathNormalized = normalizeCoordinateInput(pathArea.getText());
            if (!"-1".equals(pathNormalized)) {
                pathNormalized = pathNormalized
                    .replace("\r\n", ";")
                    .replace('\r', ';')
                    .replace('\n', ';')
                    .replaceAll(";+", ";")
                    .replaceAll("\\s*;\\s*", ";")
                    .trim();
                if (pathNormalized.isEmpty()) {
                    pathNormalized = "-1";
                }
            }
            if ("-1".equals(pathNormalized)) {
                JOptionPane.showMessageDialog(dialog, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE);
                return;
            }
            if (!saveFieldAndRefresh(dikuai, "baseStationCoordinates", baseStationNormalized)) {
                JOptionPane.showMessageDialog(dialog, "无法保存基站坐标", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if (!saveFieldAndRefresh(dikuai, "boundaryCoordinates", boundaryNormalized)) {
                JOptionPane.showMessageDialog(dialog, "无法保存地块边界", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if (!persistObstaclesForLand(dikuai, baseStationNormalized, obstacleNormalized)) {
                JOptionPane.showMessageDialog(dialog, "无法保存障碍物坐标", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if (!saveFieldAndRefresh(dikuai, "mowingWidth", widthNormalized)) {
                JOptionPane.showMessageDialog(dialog, "无法保存割草宽度", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if (!saveFieldAndRefresh(dikuai, "plannedPath", pathNormalized)) {
                JOptionPane.showMessageDialog(dialog, "无法保存割草路径", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            JOptionPane.showMessageDialog(dialog, "割草路径已保存", "成功", JOptionPane.INFORMATION_MESSAGE);
            dialog.dispose();
        });
        cancelBtn.addActionListener(e -> dialog.dispose());
        buttonPanel.add(generateBtn);
        buttonPanel.add(saveBtn);
        buttonPanel.add(cancelBtn);
        dialog.add(buttonPanel, BorderLayout.SOUTH);
        dialog.pack();
        dialog.setSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
        dialog.setLocationRelativeTo(owner);
        };
        // ä½¿ç”¨æ–°çš„独立页面类
        MowingPathGenerationPage dialog = new MowingPathGenerationPage(
            owner,
            dikuai,
            baseStationValue,
            boundaryValue,
            obstacleValue,
            widthValue,
            modeValue,
            initialGeneratedPath,
            callback
        );
        dialog.setVisible(true);
    }
src/lujing/Lunjingguihua.java
@@ -30,16 +30,18 @@
    /**
     * ç”Ÿæˆå‰²è‰è·¯å¾„段列表。
     *
     * @param polygonCoords å¤šè¾¹å½¢è¾¹ç•Œåæ ‡ï¼Œæ ¼å¼å¦‚ "x1,y1;x2,y2;..."(米)
     * @param polygonCoords   å¤šè¾¹å½¢è¾¹ç•Œåæ ‡ï¼Œæ ¼å¼å¦‚ "x1,y1;x2,y2;..."(米)
     * @param obstaclesCoords éšœç¢ç‰©åæ ‡ï¼Œæ”¯æŒå¤šä¸ªæ‹¬å·æ®µæˆ–圆形定义,例 "(x1,y1;x2,y2)(cx,cy;px,py)"
     * @param mowingWidth å‰²è‰å®½åº¦å­—符串,米单位,允许保留两位小数
     * @param modeStr å‰²è‰æ¨¡å¼ï¼Œ"0"/空为平行线,"1" æˆ– "spiral" è¡¨ç¤ºèžºæ—‹æ¨¡å¼ï¼ˆå½“前仅平行线实现)
     * @param mowingWidth     å‰²è‰å®½åº¦å­—符串,米单位,允许保留两位小数
     * @param safetyDistStr   å®‰å…¨è·ç¦»å­—符串,米单位。路径将与边界和障碍物保持此距离。
     * @param modeStr         å‰²è‰æ¨¡å¼ï¼Œ"0"/空为平行线,"1" æˆ– "spiral" è¡¨ç¤ºèžºæ—‹æ¨¡å¼ï¼ˆå½“前仅平行线实现)
     * @return è·¯å¾„段列表,按行驶顺序排列
     */
    public static List<PathSegment> generatePathSegments(String polygonCoords,
                                                          String obstaclesCoords,
                                                          String mowingWidth,
                                                          String modeStr) {
                                                         String obstaclesCoords,
                                                         String mowingWidth,
                                                         String safetyDistStr,
                                                         String modeStr) {
        List<Coordinate> polygon = parseCoordinates(polygonCoords);
        if (polygon.size() < 4) {
            throw new IllegalArgumentException("多边形坐标数量不足,至少需要三个点");
@@ -49,30 +51,55 @@
        if (width <= 0) {
            throw new IllegalArgumentException("割草宽度必须大于 0");
        }
        // è§£æžå®‰å…¨è·ç¦»ï¼Œå¦‚果未设置或无效,默认为 NaN (在 PlannerCore ä¸­å¤„理默认值)
        double safetyDistance = parseDoubleOrDefault(safetyDistStr, Double.NaN);
        List<List<Coordinate>> obstacles = parseObstacles(obstaclesCoords);
        String mode = normalizeMode(modeStr);
        PlannerCore planner = new PlannerCore(polygon, width, mode, obstacles);
        PlannerCore planner = new PlannerCore(polygon, width, safetyDistance, mode, obstacles);
        return planner.generate();
    }
    /**
     * ä¿æŒå‘后兼容的重载方法(不带 safeDistance,使用默认计算)
     */
    public static List<PathSegment> generatePathSegments(String polygonCoords,
                                                         String obstaclesCoords,
                                                         String mowingWidth,
                                                         String modeStr) {
        return generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, null, modeStr);
    }
    /**
     * é€šè¿‡å­—符串参数生成割草路径坐标。
     *
     * @param polygonCoords å¤šè¾¹å½¢è¾¹ç•Œåæ ‡ï¼Œæ ¼å¼å¦‚ "x1,y1;x2,y2;..."(米)
     * @param polygonCoords   å¤šè¾¹å½¢è¾¹ç•Œåæ ‡ï¼Œæ ¼å¼å¦‚ "x1,y1;x2,y2;..."(米)
     * @param obstaclesCoords éšœç¢ç‰©åæ ‡ï¼Œæ”¯æŒå¤šä¸ªæ‹¬å·æ®µæˆ–圆形定义,例 "(x1,y1;x2,y2)(cx,cy;px,py)"
     * @param mowingWidth å‰²è‰å®½åº¦å­—符串,米单位,允许保留两位小数
     * @param modeStr å‰²è‰æ¨¡å¼ï¼Œ"0"/空为平行线,"1" æˆ– "spiral" è¡¨ç¤ºèžºæ—‹æ¨¡å¼ï¼ˆå½“前仅平行线实现)
     * @param mowingWidth     å‰²è‰å®½åº¦å­—符串,米单位,允许保留两位小数
     * @param safetyDistStr   å®‰å…¨è·ç¦»å­—符串,米单位。
     * @param modeStr         å‰²è‰æ¨¡å¼ï¼Œ"0"/空为平行线,"1" æˆ– "spiral" è¡¨ç¤ºèžºæ—‹æ¨¡å¼ï¼ˆå½“前仅平行线实现)
     * @return è¿žç»­è·¯å¾„坐标字符串,顺序紧跟割草机行进路线
     */
    public static String generatePathFromStrings(String polygonCoords,
                                                 String obstaclesCoords,
                                                 String mowingWidth,
                                                 String safetyDistStr,
                                                 String modeStr) {
        List<PathSegment> segments = generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, modeStr);
        List<PathSegment> segments = generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, safetyDistStr, modeStr);
        return formatPathSegments(segments);
    }
    /**
     * ä¿æŒå‘后兼容的重载方法
     */
    public static String generatePathFromStrings(String polygonCoords,
                                                 String obstaclesCoords,
                                                 String mowingWidth,
                                                 String modeStr) {
        return generatePathFromStrings(polygonCoords, obstaclesCoords, mowingWidth, null, modeStr);
    }
    /**
     * å°†è·¯å¾„段列表转换为坐标字符串。
@@ -168,7 +195,7 @@
        try {
            return Double.parseDouble(value.trim());
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("割草宽度格式不正确: " + value, ex);
            throw new IllegalArgumentException("格式不正确: " + value, ex);
        }
    }
@@ -227,7 +254,7 @@
        public boolean isStartPoint;
        public boolean isEndPoint;
        PathSegment(Coordinate start, Coordinate end, boolean isMowing) {
        public PathSegment(Coordinate start, Coordinate end, boolean isMowing) {
            this.start = start;
            this.end = end;
            this.isMowing = isMowing;
@@ -251,28 +278,53 @@
    /**
     * å†…部核心规划器,实现与 MowingPathPlanner ç­‰æ•ˆçš„逻辑。
     */
    private static final class PlannerCore {
    static final class PlannerCore {
        private final List<Coordinate> polygon;
        private final double width;
        private final double safetyDistance; // æ–°å¢žå®‰å…¨è·ç¦»å­—段
        private final String mode;
        private final List<List<Coordinate>> obstacles;
        private final GeometryFactory gf = new GeometryFactory();
        PlannerCore(List<Coordinate> polygon, double width, String mode, List<List<Coordinate>> obstacles) {
        PlannerCore(List<Coordinate> polygon, double width, double safetyDistance, String mode, List<List<Coordinate>> obstacles) {
            this.polygon = polygon;
            this.width = width;
            this.mode = mode;
            this.obstacles = obstacles != null ? obstacles : new ArrayList<>();
            // åˆå§‹åŒ–安全距离逻辑
            if (Double.isNaN(safetyDistance)) {
                // å¦‚果未提供,使用默认值:宽度的一半 + 0.05ç±³
                this.safetyDistance = width / 2.0 + 0.05;
            } else {
                // å¦‚果提供了,使用提供的值,但至少要保证机器中心不碰壁(宽度一半)
                // å…è®¸ç”¨æˆ·è®¾ç½®æ¯” width/2 æ›´å¤§çš„值来远离边界
                this.safetyDistance = Math.max(safetyDistance, width / 2.0);
            }
        }
        // å…¼å®¹æ—§æž„造函数
        PlannerCore(List<Coordinate> polygon, double width, String mode, List<List<Coordinate>> obstacles) {
            this(polygon, width, Double.NaN, mode, obstacles);
        }
        List<PathSegment> generate() {
            // å¦‚果有障碍物,使用带障碍物避让的路径规划器
            if (!obstacles.isEmpty()) {
                // ä½¿ç”¨è®¡ç®—好的安全距离
                ObstaclePathPlanner obstaclePlanner = new ObstaclePathPlanner(
                    polygon, width, mode, obstacles, this.safetyDistance);
                return obstaclePlanner.generate();
            }
            // æ²¡æœ‰éšœç¢ç‰©æ—¶ä½¿ç”¨åŽŸæœ‰é€»è¾‘
            if ("spiral".equals(mode)) {
                return generateSpiralPath();
            }
            return generateParallelPath();
        }
        private List<PathSegment> generateParallelPath() {
        List<PathSegment> generateParallelPath() {
            List<PathSegment> path = new ArrayList<>();
            Geometry safeArea = buildSafeArea();
            if (safeArea == null || safeArea.isEmpty()) {
@@ -285,7 +337,7 @@
                                            longest.end.y - longest.start.y).normalize();
            Vector2D perp = baseDir.rotate90CCW();
            Vector2D baseStartVec = new Vector2D(longest.start.x, longest.start.y);
            double baseProjection = perp.dot(baseStartVec); // keep offsets relative to the longest edge start
            double baseProjection = perp.dot(baseStartVec);
            double minProj = Double.POSITIVE_INFINITY;
            double maxProj = Double.NEGATIVE_INFINITY;
@@ -361,7 +413,7 @@
            return path;
        }
        private List<PathSegment> generateSpiralPath() {
        List<PathSegment> generateSpiralPath() {
            Geometry safeArea = buildSafeArea();
            if (safeArea == null || safeArea.isEmpty()) {
                System.err.println("安全区域为空,无法生成螺旋路径");
@@ -418,7 +470,10 @@
                    }
                }
                Geometry shrunk = shrinkStraight(result, width / 2.0);
                // ä¿®æ”¹ï¼šä½¿ç”¨ä¼ å…¥çš„ safetyDistance æ¥è¿›è¡Œè¾¹ç•Œå†…缩
                // ä¹‹å‰æ˜¯ width / 2.0,现在使用 this.safetyDistance
                // è¿™ç¡®ä¿äº†è·¯å¾„规划区域与边界保持用户指定的距离
                Geometry shrunk = shrinkStraight(result, this.safetyDistance);
                return shrunk.isEmpty() ? result : shrunk;
            } catch (Exception ex) {
                System.err.println("构建安全区域失败: " + ex.getMessage());
@@ -620,4 +675,4 @@
            this.index = index;
        }
    }
}
}
src/lujing/MowingPathGenerationPage.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,634 @@
package lujing;
import javax.swing.*;
import java.awt.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import dikuai.Dikuai;
import lujing.Lunjingguihua;
import lujing.ObstaclePathPlanner;
import org.locationtech.jts.geom.Coordinate;
/**
 * ç”Ÿæˆå‰²è‰è·¯å¾„页面
 * ç‹¬ç«‹çš„对话框类,用于生成和编辑割草路径
 */
public class MowingPathGenerationPage extends JDialog {
    private static final long serialVersionUID = 1L;
    // å°ºå¯¸å¸¸é‡
    private static final int SCREEN_WIDTH = 400;
    private static final int SCREEN_HEIGHT = 800;
    // é¢œè‰²å¸¸é‡
    private static final Color PRIMARY_COLOR = new Color(46, 139, 87);
    private static final Color PRIMARY_DARK = new Color(30, 107, 69);
    private static final Color TEXT_COLOR = new Color(51, 51, 51);
    private static final Color WHITE = Color.WHITE;
    private static final Color BORDER_COLOR = new Color(200, 200, 200);
    private static final Color BACKGROUND_COLOR = new Color(250, 250, 250);
    // æ•°æ®ä¿å­˜å›žè°ƒæŽ¥å£
    public interface PathSaveCallback {
        boolean saveBaseStationCoordinates(Dikuai dikuai, String value);
        boolean saveBoundaryCoordinates(Dikuai dikuai, String value);
        boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue);
        boolean saveMowingWidth(Dikuai dikuai, String value);
        boolean savePlannedPath(Dikuai dikuai, String value);
    }
    private final Dikuai dikuai;
    private final PathSaveCallback saveCallback;
    // UI组件
    private JTextField baseStationField;
    private JTextArea boundaryArea;
    private JTextArea obstacleArea;
    private JTextField widthField;
    private JTextArea pathArea;
    /**
     * æž„造函数
     * @param owner çˆ¶çª—口
     * @param dikuai åœ°å—对象
     * @param baseStationValue åŸºç«™åæ ‡
     * @param boundaryValue åœ°å—边界
     * @param obstacleValue éšœç¢ç‰©åæ ‡
     * @param widthValue å‰²è‰å®½åº¦
     * @param modeValue å‰²è‰æ¨¡å¼
     * @param initialGeneratedPath åˆå§‹ç”Ÿæˆçš„路径
     * @param saveCallback ä¿å­˜å›žè°ƒæŽ¥å£
     */
    public MowingPathGenerationPage(Window owner,
                                    Dikuai dikuai,
                                    String baseStationValue,
                                    String boundaryValue,
                                    String obstacleValue,
                                    String widthValue,
                                    String modeValue,
                                    String initialGeneratedPath,
                                    PathSaveCallback saveCallback) {
        super(owner, "路径规划页面", Dialog.ModalityType.APPLICATION_MODAL);
        this.dikuai = dikuai;
        this.saveCallback = saveCallback;
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        getContentPane().setLayout(new BorderLayout());
        getContentPane().setBackground(BACKGROUND_COLOR);
        initializeUI(baseStationValue, boundaryValue, obstacleValue,
                     widthValue, modeValue, initialGeneratedPath);
        pack();
        setSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
        setLocationRelativeTo(owner);
    }
    /**
     * åˆå§‹åŒ–UI
     */
    private void initializeUI(String baseStationValue, String boundaryValue,
                              String obstacleValue, String widthValue,
                              String modeValue, String initialGeneratedPath) {
        JPanel contentPanel = new JPanel();
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
        contentPanel.setBackground(BACKGROUND_COLOR);
        contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 16, 12, 16));
        // æ ‡é¢˜
        String landName = getDisplayValue(dikuai.getLandName(), "未知地块");
        String landNumber = getDisplayValue(dikuai.getLandNumber(), "未知编号");
        JLabel headerLabel = new JLabel(landName + " / " + landNumber);
        headerLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
        headerLabel.setForeground(TEXT_COLOR);
        headerLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        contentPanel.add(headerLabel);
        contentPanel.add(Box.createVerticalStrut(12));
        // åŸºç«™åæ ‡
        baseStationField = createInfoTextField(baseStationValue != null ? baseStationValue : "", true);
        contentPanel.add(createTextFieldSection("基站坐标", baseStationField));
        // åœ°å—边界
        boundaryArea = createInfoTextArea(boundaryValue != null ? boundaryValue : "", true, 6);
        contentPanel.add(createTextAreaSection("地块边界", boundaryArea));
        // éšœç¢ç‰©åæ ‡
        obstacleArea = createInfoTextArea(obstacleValue != null ? obstacleValue : "", true, 6);
        contentPanel.add(createTextAreaSection("障碍物坐标", obstacleArea));
        // å‰²è‰å®½åº¦
        widthField = createInfoTextField(widthValue != null ? widthValue : "", true);
        contentPanel.add(createTextFieldSection("割草宽度 (厘米)", widthField));
        // å‰²è‰æ¨¡å¼ï¼ˆåªè¯»æ˜¾ç¤ºï¼‰
        contentPanel.add(createInfoValueSection("割草模式", formatMowingPatternForDialog(modeValue)));
        // å‰²è‰è·¯å¾„坐标
        String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath());
        String pathSeed = initialGeneratedPath != null ? initialGeneratedPath : existingPath;
        pathArea = createInfoTextArea(pathSeed != null ? pathSeed : "", true, 10);
        contentPanel.add(createTextAreaSection("割草路径坐标", pathArea));
        JScrollPane dialogScrollPane = new JScrollPane(contentPanel);
        dialogScrollPane.setBorder(BorderFactory.createEmptyBorder());
        dialogScrollPane.getVerticalScrollBar().setUnitIncrement(16);
        add(dialogScrollPane, BorderLayout.CENTER);
        // æŒ‰é’®é¢æ¿
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 12, 12));
        buttonPanel.setBackground(BACKGROUND_COLOR);
        JButton generateBtn = createPrimaryFooterButton("生成割草路径");
        JButton saveBtn = createPrimaryFooterButton("保存路径");
        JButton cancelBtn = createPrimaryFooterButton("取消");
        generateBtn.addActionListener(e -> generatePath(modeValue));
        saveBtn.addActionListener(e -> savePath());
        cancelBtn.addActionListener(e -> dispose());
        buttonPanel.add(generateBtn);
        buttonPanel.add(saveBtn);
        buttonPanel.add(cancelBtn);
        add(buttonPanel, BorderLayout.SOUTH);
    }
    /**
     * ç”Ÿæˆè·¯å¾„
     */
    private void generatePath(String modeValue) {
        String sanitizedWidth = sanitizeWidthString(widthField.getText());
        if (sanitizedWidth != null) {
            try {
                double widthCm = Double.parseDouble(sanitizedWidth);
                widthField.setText(formatWidthForStorage(widthCm));
                sanitizedWidth = formatWidthForStorage(widthCm);
            } catch (NumberFormatException ex) {
                widthField.setText(sanitizedWidth);
            }
        }
        String generated = attemptMowingPathPreview(
            boundaryArea.getText(),
            obstacleArea.getText(),
            sanitizedWidth,
            modeValue,
            this,
            true
        );
        if (generated != null) {
            pathArea.setText(generated);
            pathArea.setCaretPosition(0);
        }
    }
    /**
     * ä¿å­˜è·¯å¾„
     */
    private void savePath() {
        String baseStationNormalized = normalizeCoordinateInput(baseStationField.getText());
        String boundaryNormalized = normalizeCoordinateInput(boundaryArea.getText());
        if (!"-1".equals(boundaryNormalized)) {
            boundaryNormalized = boundaryNormalized
                .replace("\r\n", ";")
                .replace('\r', ';')
                .replace('\n', ';')
                .replaceAll(";+", ";")
                .replaceAll("\\s*;\\s*", ";")
                .trim();
            if (boundaryNormalized.isEmpty()) {
                boundaryNormalized = "-1";
            }
        }
        String obstacleNormalized = normalizeCoordinateInput(obstacleArea.getText());
        if (!"-1".equals(obstacleNormalized)) {
            obstacleNormalized = obstacleNormalized
                .replace("\r\n", " ")
                .replace('\r', ' ')
                .replace('\n', ' ')
                .replaceAll("\\s{2,}", " ")
                .trim();
            if (obstacleNormalized.isEmpty()) {
                obstacleNormalized = "-1";
            }
        }
        String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : "";
        String widthSanitized = sanitizeWidthString(widthField.getText());
        if (widthSanitized == null) {
            String message = rawWidthInput.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确";
            JOptionPane.showMessageDialog(this, message, "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        double widthCm;
        try {
            widthCm = Double.parseDouble(widthSanitized);
        } catch (NumberFormatException ex) {
            JOptionPane.showMessageDialog(this, "割草宽度格式不正确", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        if (widthCm <= 0) {
            JOptionPane.showMessageDialog(this, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        String widthNormalized = formatWidthForStorage(widthCm);
        widthField.setText(widthNormalized);
        String pathNormalized = normalizeCoordinateInput(pathArea.getText());
        if (!"-1".equals(pathNormalized)) {
            pathNormalized = pathNormalized
                .replace("\r\n", ";")
                .replace('\r', ';')
                .replace('\n', ';')
                .replaceAll(";+", ";")
                .replaceAll("\\s*;\\s*", ";")
                .trim();
            if (pathNormalized.isEmpty()) {
                pathNormalized = "-1";
            }
        }
        if ("-1".equals(pathNormalized)) {
            JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        // è°ƒç”¨å›žè°ƒä¿å­˜æ•°æ®
        if (saveCallback != null) {
            if (!saveCallback.saveBaseStationCoordinates(dikuai, baseStationNormalized)) {
                JOptionPane.showMessageDialog(this, "无法保存基站坐标", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if (!saveCallback.saveBoundaryCoordinates(dikuai, boundaryNormalized)) {
                JOptionPane.showMessageDialog(this, "无法保存地块边界", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if (!saveCallback.saveObstacleCoordinates(dikuai, baseStationNormalized, obstacleNormalized)) {
                JOptionPane.showMessageDialog(this, "无法保存障碍物坐标", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if (!saveCallback.saveMowingWidth(dikuai, widthNormalized)) {
                JOptionPane.showMessageDialog(this, "无法保存割草宽度", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            if (!saveCallback.savePlannedPath(dikuai, pathNormalized)) {
                JOptionPane.showMessageDialog(this, "无法保存割草路径", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
        }
        JOptionPane.showMessageDialog(this, "割草路径已保存", "成功", JOptionPane.INFORMATION_MESSAGE);
        dispose();
    }
    /**
     * å°è¯•生成路径预览
     */
    private String attemptMowingPathPreview(String boundaryInput, String obstacleInput,
                                            String widthCmInput, String modeInput,
                                            Component parentComponent, boolean showMessages) {
        String boundary = sanitizeValueOrNull(boundaryInput);
        if (boundary == null) {
            if (showMessages) {
                JOptionPane.showMessageDialog(parentComponent, "当前地块未设置边界坐标,无法生成路径",
                    "提示", JOptionPane.WARNING_MESSAGE);
            }
            return null;
        }
        String rawWidth = widthCmInput != null ? widthCmInput.trim() : "";
        String widthStr = sanitizeWidthString(widthCmInput);
        if (widthStr == null) {
            if (showMessages) {
                String message = rawWidth.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确";
                JOptionPane.showMessageDialog(parentComponent, message, "提示", JOptionPane.WARNING_MESSAGE);
            }
            return null;
        }
        double widthCm;
        try {
            widthCm = Double.parseDouble(widthStr);
        } catch (NumberFormatException ex) {
            if (showMessages) {
                JOptionPane.showMessageDialog(parentComponent, "割草宽度格式不正确",
                    "提示", JOptionPane.WARNING_MESSAGE);
            }
            return null;
        }
        if (widthCm <= 0) {
            if (showMessages) {
                JOptionPane.showMessageDialog(parentComponent, "割草宽度必须大于0",
                    "提示", JOptionPane.WARNING_MESSAGE);
            }
            return null;
        }
        double widthMeters = widthCm / 100.0d;
        String plannerWidth = BigDecimal.valueOf(widthMeters)
            .setScale(3, RoundingMode.HALF_UP)
            .stripTrailingZeros()
            .toPlainString();
        // æ£€æŸ¥åŽŸå§‹è¾“å…¥æ˜¯å¦æœ‰éšœç¢ç‰©ï¼ˆåœ¨sanitize之前检查,避免丢失信息)
        String rawObstacleInput = obstacleInput != null ? obstacleInput.trim() : "";
        boolean hasObstacleInput = !rawObstacleInput.isEmpty() && !"-1".equals(rawObstacleInput);
        String obstacles = sanitizeValueOrNull(obstacleInput);
        if (obstacles != null) {
            obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' ');
        }
        String mode = normalizeExistingMowingPattern(modeInput);
        try {
            // è§£æžéšœç¢ç‰©åˆ—表
            List<List<Coordinate>> obstacleList = Lunjingguihua.parseObstacles(obstacles);
            if (obstacleList == null) {
                obstacleList = new ArrayList<>();
            }
            // åˆ¤æ–­æ˜¯å¦æœ‰éšœç¢ç‰©ï¼šåªè¦åŽŸå§‹è¾“å…¥æœ‰éšœç¢ç‰©å†…å®¹ï¼Œå°±ä½¿ç”¨ObstaclePathPlanner
            // å³ä½¿è§£æžåŽåˆ—表为空,也尝试使用ObstaclePathPlanner(它会处理空障碍物列表的情况)
            boolean hasObstacles = hasObstacleInput && !obstacleList.isEmpty();
            // å¦‚果原始输入有障碍物但解析失败,给出提示
            if (hasObstacleInput && obstacleList.isEmpty()) {
                if (showMessages) {
                    JOptionPane.showMessageDialog(parentComponent,
                        "障碍物坐标格式可能不正确,将尝试生成路径。如果路径不正确,请检查障碍物坐标格式。",
                        "提示", JOptionPane.WARNING_MESSAGE);
                }
                // ä»ç„¶å°è¯•使用ObstaclePathPlanner,即使障碍物列表为空
                // è¿™æ ·è‡³å°‘可以确保使用正确的路径规划器
            }
            String generated;
            if (!hasObstacles && !hasObstacleInput) {
                // å®Œå…¨æ²¡æœ‰éšœç¢ç‰©è¾“入时,使用Lunjingguihua类的方法生成路径
                generated = Lunjingguihua.generatePathFromStrings(
                    boundary,
                    obstacles != null ? obstacles : "",
                    plannerWidth,
                    mode
                );
            } else {
                // æœ‰éšœç¢ç‰©è¾“入时(即使解析失败),使用ObstaclePathPlanner处理路径生成
                List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary);
                if (polygon.size() < 4) {
                    if (showMessages) {
                        JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点",
                            "错误", JOptionPane.ERROR_MESSAGE);
                    }
                    return null;
                }
                // æ ¹æ®æ˜¯å¦æœ‰éšœç¢ç‰©è®¾ç½®ä¸åŒçš„安全距离
                double safetyDistance;
                if (!obstacleList.isEmpty()) {
                    // æœ‰éšœç¢ç‰©æ—¶ä½¿ç”¨å‰²è‰å®½åº¦çš„一半 + 0.05米额外安全距离
                    safetyDistance = widthMeters / 2.0 + 0.05;
                } else {
                    // éšœç¢ç‰©è§£æžå¤±è´¥ä½†è¾“入存在,使用较小的安全距离
                    safetyDistance = 0.01;
                }
                ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner(
                    polygon, widthMeters, mode, obstacleList, safetyDistance);
                List<Lunjingguihua.PathSegment> segments = pathPlanner.generate();
                generated = Lunjingguihua.formatPathSegments(segments);
            }
            String trimmed = generated != null ? generated.trim() : "";
            if (trimmed.isEmpty()) {
                if (showMessages) {
                    JOptionPane.showMessageDialog(parentComponent, "未生成有效的割草路径,请检查地块数据",
                        "提示", JOptionPane.INFORMATION_MESSAGE);
                }
                return null;
            }
            if (showMessages) {
                JOptionPane.showMessageDialog(parentComponent, "割草路径已生成",
                    "成功", JOptionPane.INFORMATION_MESSAGE);
            }
            return trimmed;
        } catch (IllegalArgumentException ex) {
            if (showMessages) {
                JOptionPane.showMessageDialog(parentComponent, "生成割草路径失败: " + ex.getMessage(),
                    "错误", JOptionPane.ERROR_MESSAGE);
            }
        } catch (Exception ex) {
            if (showMessages) {
                JOptionPane.showMessageDialog(parentComponent, "生成割草路径时发生异常: " + ex.getMessage(),
                    "错误", JOptionPane.ERROR_MESSAGE);
            }
            ex.printStackTrace();
        }
        return null;
    }
    // ========== UI辅助方法 ==========
    private JTextArea createInfoTextArea(String text, boolean editable, int rows) {
        JTextArea area = new JTextArea(text);
        area.setEditable(editable);
        area.setLineWrap(true);
        area.setWrapStyleWord(true);
        area.setFont(new Font("微软雅黑", Font.PLAIN, 13));
        area.setRows(Math.max(rows, 2));
        area.setCaretPosition(0);
        area.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
        area.setBackground(editable ? WHITE : new Color(245, 245, 245));
        return area;
    }
    private JPanel createTextAreaSection(String title, JTextArea textArea) {
        JPanel section = new JPanel(new BorderLayout(0, 6));
        section.setBackground(BACKGROUND_COLOR);
        section.setAlignmentX(Component.LEFT_ALIGNMENT);
        JLabel titleLabel = new JLabel(title);
        titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
        titleLabel.setForeground(TEXT_COLOR);
        section.add(titleLabel, BorderLayout.NORTH);
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
        scrollPane.getVerticalScrollBar().setUnitIncrement(12);
        section.add(scrollPane, BorderLayout.CENTER);
        section.setBorder(BorderFactory.createEmptyBorder(4, 0, 12, 0));
        return section;
    }
    private JTextField createInfoTextField(String text, boolean editable) {
        JTextField field = new JTextField(text);
        field.setEditable(editable);
        field.setFont(new Font("微软雅黑", Font.PLAIN, 13));
        field.setForeground(TEXT_COLOR);
        field.setBackground(editable ? WHITE : new Color(245, 245, 245));
        field.setCaretPosition(0);
        field.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
        field.setFocusable(true);
        field.setOpaque(true);
        return field;
    }
    private JPanel createTextFieldSection(String title, JTextField textField) {
        JPanel section = new JPanel(new BorderLayout(0, 6));
        section.setBackground(BACKGROUND_COLOR);
        section.setAlignmentX(Component.LEFT_ALIGNMENT);
        JLabel titleLabel = new JLabel(title);
        titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
        titleLabel.setForeground(TEXT_COLOR);
        section.add(titleLabel, BorderLayout.NORTH);
        JPanel fieldWrapper = new JPanel(new BorderLayout());
        fieldWrapper.setBackground(textField.isEditable() ? WHITE : new Color(245, 245, 245));
        fieldWrapper.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
        fieldWrapper.add(textField, BorderLayout.CENTER);
        section.add(fieldWrapper, BorderLayout.CENTER);
        section.setBorder(BorderFactory.createEmptyBorder(4, 0, 12, 0));
        return section;
    }
    private JPanel createInfoValueSection(String title, String value) {
        JPanel section = new JPanel();
        section.setLayout(new BoxLayout(section, BoxLayout.X_AXIS));
        section.setBackground(BACKGROUND_COLOR);
        section.setAlignmentX(Component.LEFT_ALIGNMENT);
        section.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0));
        JLabel titleLabel = new JLabel(title + ":");
        titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
        titleLabel.setForeground(TEXT_COLOR);
        section.add(titleLabel);
        section.add(Box.createHorizontalStrut(8));
        JLabel valueLabel = new JLabel(value);
        valueLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        valueLabel.setForeground(TEXT_COLOR);
        section.add(valueLabel);
        section.add(Box.createHorizontalGlue());
        return section;
    }
    private JButton createPrimaryFooterButton(String text) {
        JButton button = new JButton(text);
        button.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        button.setBackground(PRIMARY_COLOR);
        button.setForeground(WHITE);
        button.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12));
        button.setFocusPainted(false);
        button.setCursor(new Cursor(Cursor.HAND_CURSOR));
        button.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mouseEntered(java.awt.event.MouseEvent e) {
                button.setBackground(PRIMARY_DARK);
            }
            public void mouseExited(java.awt.event.MouseEvent e) {
                button.setBackground(PRIMARY_COLOR);
            }
        });
        return button;
    }
    // ========== æ•°æ®å¤„理辅助方法 ==========
    private String getDisplayValue(String value, String defaultValue) {
        if (value == null || value.equals("-1") || value.trim().isEmpty()) {
            return defaultValue;
        }
        return value;
    }
    private String prepareCoordinateForEditor(String value) {
        if (value == null) {
            return "";
        }
        String trimmed = value.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return "";
        }
        return trimmed;
    }
    private String normalizeCoordinateInput(String input) {
        if (input == null) {
            return "-1";
        }
        String trimmed = input.trim();
        return trimmed.isEmpty() ? "-1" : trimmed;
    }
    private String sanitizeValueOrNull(String input) {
        if (input == null) {
            return null;
        }
        String trimmed = input.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return null;
        }
        return trimmed;
    }
    private String sanitizeWidthString(String input) {
        if (input == null) {
            return null;
        }
        String trimmed = input.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return null;
        }
        String cleaned = trimmed.replaceAll("[^0-9.+-]", "");
        return cleaned.isEmpty() ? null : cleaned;
    }
    private String formatWidthForStorage(double widthCm) {
        return BigDecimal.valueOf(widthCm)
            .setScale(2, RoundingMode.HALF_UP)
            .stripTrailingZeros()
            .toPlainString();
    }
    private String formatMowingPatternForDialog(String patternValue) {
        String sanitized = sanitizeValueOrNull(patternValue);
        if (sanitized == null) {
            return "未设置";
        }
        String normalized = normalizeExistingMowingPattern(sanitized);
        if ("parallel".equals(normalized)) {
            return "平行模式 (parallel)";
        }
        if ("spiral".equals(normalized)) {
            return "螺旋模式 (spiral)";
        }
        return sanitized;
    }
    private String normalizeExistingMowingPattern(String patternValue) {
        if (patternValue == null) {
            return "parallel";
        }
        String trimmed = patternValue.trim().toLowerCase();
        if ("1".equals(trimmed) || "spiral".equals(trimmed)) {
            return "spiral";
        }
        return "parallel";
    }
}
src/lujing/ObstaclePathPlanner.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,536 @@
package lujing;
import org.locationtech.jts.algorithm.Angle;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.operation.distance.DistanceOp;
import org.locationtech.jts.operation.union.CascadedPolygonUnion;
import java.util.*;
import java.util.stream.Collectors;
/**
 * éšœç¢ç‰©è·¯å¾„规划器
 * * ä¼˜åŒ–方案:
 * 1. é¢„规划:先不考虑障碍物,规划出覆盖全图的平行线路径(弓字形)。
 * 2. å‡ ä½•切割:根据障碍物的外扩(安全距离)几何体,将路径切断,移除落在障碍物内的部分。
 * 3. åŒºåŸŸé‡ç»„:将剩余的路径段按连通性聚类为若干个“连续区域”,每个区域内部通过弓字形连接。
 * 4. å…¨å±€è¿žæŽ¥ï¼šä½¿ç”¨é¿éšœç®—法(A*可视图)将这些孤立的区域串联起来。
 * * * ä¿®å¤é—®é¢˜ï¼š
 * 1. ä¿®å¤è·¯å¾„穿越障碍物的问题(通过更严格的 Interior Intersection æ£€æŸ¥ï¼‰ã€‚
 * 2. ä¿®å¤è·¯å¾„超出地块边界的问题(通过加入 Boundary Covers çº¦æŸï¼‰ã€‚
 * 3. [本次修复] ä¿®å¤ IntersectionMatrix ç±»åž‹æŠ¥é”™ï¼Œä½¿ç”¨æ­£ç¡®çš„矩阵单元格检查替代不存在的方法。
 * * * äºŒæ¬¡ä¼˜åŒ–(针对用户反馈):
 * 1. å¼•å…¥ Tolerance Buffer (内缩) ç”¨äºŽç›¸äº¤æ£€æµ‹ï¼Œè§£å†³â€œæŽ¥è§¦å³ç›¸äº¤â€å¯¼è‡´çš„合法边缘路径被误判问题。
 * 2. å¢žå¼º isLineSafe çš„边界约束,确保路径严格在 Boundary å†…部。
 * 3. ä¼˜åŒ– findSafePath,增加点位校验与吸附(Snap),防止因浮点误差导致的寻路失败而回退到直线。
 */
public class ObstaclePathPlanner {
    private final List<Coordinate> polygon;           // å‰²è‰åŒºåŸŸè¾¹ç•Œç‚¹é›†
    private final double width;                      // å‰²è‰å®½åº¦
    private final String mode;                       // æ¨¡å¼
    private final List<List<Coordinate>> obstacles;   // éšœç¢ç‰©åˆ—表
    private final double safetyDistance;             // å®‰å…¨è·ç¦»
    private final GeometryFactory gf = new GeometryFactory();
    // ä¿å­˜åœ°å—的几何形状,用于边界约束检查
    private Geometry boundaryGeom;
    // ç”¨äºŽæ£€æµ‹çš„障碍物几何体(可能经过微调)
    private Geometry checkObstacleGeom;
    public ObstaclePathPlanner(List<Coordinate> polygon, double width, String mode,
                               List<List<Coordinate>> obstacles, double safetyDistance) {
        this.polygon = polygon;
        this.width = width;
        this.mode = mode;
        this.obstacles = obstacles != null ? obstacles : new ArrayList<>();
        this.safetyDistance = safetyDistance;
        initBoundaryGeom();
    }
    /**
     * åˆå§‹åŒ–边界几何体,用于后续的约束检查
     */
    private void initBoundaryGeom() {
        if (polygon == null || polygon.size() < 3) {
            this.boundaryGeom = gf.createPolygon();
            return;
        }
        List<Coordinate> closed = new ArrayList<>(polygon);
        if (!closed.get(0).equals2D(closed.get(closed.size() - 1))) {
            closed.add(new Coordinate(closed.get(0)));
        }
        LinearRing ring = gf.createLinearRing(closed.toArray(new Coordinate[0]));
        Polygon poly = gf.createPolygon(ring);
        // ç¡®ä¿å‡ ä½•有效性
        this.boundaryGeom = poly.isValid() ? poly : poly.buffer(0);
    }
    /**
     * ç”Ÿæˆè·¯å¾„的主入口
     */
    public List<Lunjingguihua.PathSegment> generate() {
        // 1. ç”Ÿæˆåˆå§‹çš„完整平行线路径(忽略障碍物)
        List<Lunjingguihua.PathSegment> fullPath = generateFullPathWithoutObstacles();
        if (fullPath.isEmpty()) return fullPath;
        // 2. æž„建障碍物的外扩安全区域 (Buffer)
        Geometry obstacleBuffer = createObstacleBuffer();
        // åˆå§‹åŒ–用于检测的几何体(内缩一点点,允许路径贴边)
        if (!obstacleBuffer.isEmpty()) {
            this.checkObstacleGeom = obstacleBuffer.buffer(-0.01); // å†…缩1cm,容忍浮点误差
        } else {
            this.checkObstacleGeom = obstacleBuffer;
        }
        if (obstacleBuffer.isEmpty()) return fullPath;
        // 3. åˆ‡å‰²è·¯å¾„:移除与障碍物重叠的部分
        List<Lunjingguihua.PathSegment> clippedSegments = clipPathWithObstacles(fullPath, obstacleBuffer);
        if (clippedSegments.isEmpty()) return new ArrayList<>();
        // 4. é‡æ–°è§„划:将碎片化的线段重组成连续的弓字形路径,并进行避障连接
        List<Lunjingguihua.PathSegment> finalPath = reorganizeAndConnectPath(clippedSegments, obstacleBuffer);
        // 5. åŽå¤„理(标记起终点等)
        postProcessPath(finalPath);
        return finalPath;
    }
    /**
     * æ­¥éª¤1:调用核心库生成无障碍的平行线路径
     */
    private List<Lunjingguihua.PathSegment> generateFullPathWithoutObstacles() {
        Lunjingguihua.PlannerCore tempPlanner = new Lunjingguihua.PlannerCore(
                polygon, width, mode, new ArrayList<>());
        return tempPlanner.generateParallelPath();
    }
    /**
     * æ­¥éª¤2:创建所有障碍物的并集 + å®‰å…¨è·ç¦»å¤–扩
     */
    private Geometry createObstacleBuffer() {
        if (obstacles.isEmpty()) return gf.createPolygon();
        List<Geometry> geoms = new ArrayList<>();
        for (List<Coordinate> obs : obstacles) {
            if (obs == null || obs.size() < 3) continue;
            List<Coordinate> closed = new ArrayList<>(obs);
            if (!closed.get(0).equals2D(closed.get(closed.size() - 1))) {
                closed.add(new Coordinate(closed.get(0)));
            }
            LinearRing ring = gf.createLinearRing(closed.toArray(new Coordinate[0]));
            Polygon poly = gf.createPolygon(ring);
            if (!poly.isValid()) poly = (Polygon) poly.buffer(0);
            geoms.add(poly);
        }
        if (geoms.isEmpty()) return gf.createPolygon();
        Geometry union = CascadedPolygonUnion.union(geoms);
        // å¯¹åˆå¹¶åŽçš„障碍物进行外扩(Buffer)
        Geometry buffered = union.buffer(safetyDistance, 8);
        return buffered.isValid() ? buffered : buffered.buffer(0);
    }
    /**
     * æ­¥éª¤3:用障碍物几何体切割路径
     */
    private List<Lunjingguihua.PathSegment> clipPathWithObstacles(List<Lunjingguihua.PathSegment> fullPath, Geometry obstacleBuffer) {
        List<Lunjingguihua.PathSegment> validSegments = new ArrayList<>();
        for (Lunjingguihua.PathSegment seg : fullPath) {
            if (!seg.isMowing) continue;
            LineString line = gf.createLineString(new Coordinate[]{seg.start, seg.end});
            Geometry diff;
            try {
                diff = line.difference(obstacleBuffer);
            } catch (Exception e) {
                continue;
            }
            if (diff.isEmpty()) continue;
            for (int i = 0; i < diff.getNumGeometries(); i++) {
                Geometry g = diff.getGeometryN(i);
                if (g instanceof LineString) {
                    Coordinate[] coords = g.getCoordinates();
                    for (int k = 0; k < coords.length - 1; k++) {
                        Coordinate p1 = coords[k];
                        Coordinate p2 = coords[k+1];
                        if (p1.distance(p2) > 0.1) {
                            validSegments.add(new Lunjingguihua.PathSegment(p1, p2, true));
                        }
                    }
                }
            }
        }
        return validSegments;
    }
    /**
     * æ­¥éª¤4:核心重组逻辑
     */
    private List<Lunjingguihua.PathSegment> reorganizeAndConnectPath(List<Lunjingguihua.PathSegment> segments, Geometry obstacleBuffer) {
        if (segments.isEmpty()) return new ArrayList<>();
        // --- A. åˆ†æžæ‰«æçº¿æ–¹å‘ ---
        Lunjingguihua.PathSegment firstSeg = segments.get(0);
        double angle = Math.atan2(firstSeg.end.y - firstSeg.start.y, firstSeg.end.x - firstSeg.start.x);
        double sin = Math.sin(angle);
        double cos = Math.cos(angle);
        // --- B. ç»™æ¯ä¸ªçº¿æ®µåˆ†é…æ‰«æçº¿ç´¢å¼• (Grid Index) ---
        double gridStep = width;
        class IndexedSegment {
            final Lunjingguihua.PathSegment segment;
            final int gridIndex;
            final double projectVal;
            IndexedSegment(Lunjingguihua.PathSegment s) {
                this.segment = s;
                double cx = (s.start.x + s.end.x) / 2;
                double cy = (s.start.y + s.end.y) / 2;
                double perpDist = -cx * sin + cy * cos;
                this.gridIndex = (int) Math.floor(perpDist / gridStep);
                this.projectVal = cx * cos + cy * sin;
            }
        }
        List<IndexedSegment> indexedSegments = segments.stream()
                .map(IndexedSegment::new)
                .sorted(Comparator.comparingInt((IndexedSegment s) -> s.gridIndex)
                        .thenComparingDouble(s -> s.projectVal))
                .collect(Collectors.toList());
        // --- C. æž„建“区域链” (Zones) ---
        List<List<Lunjingguihua.PathSegment>> zones = new ArrayList<>();
        Set<IndexedSegment> visited = new HashSet<>();
        while (visited.size() < indexedSegments.size()) {
            IndexedSegment startNode = null;
            for (IndexedSegment is : indexedSegments) {
                if (!visited.contains(is)) {
                    startNode = is;
                    break;
                }
            }
            if (startNode == null) break;
            List<Lunjingguihua.PathSegment> zone = new ArrayList<>();
            zone.add(startNode.segment);
            visited.add(startNode);
            IndexedSegment current = startNode;
            boolean lookingForNext = true;
            while (lookingForNext) {
                IndexedSegment bestNext = null;
                double minDistance = Double.MAX_VALUE;
                // æœç´¢æœ€ä½³åŽç»­çº¿æ®µ
                for (int i = 0; i < indexedSegments.size(); i++) {
                    IndexedSegment candidate = indexedSegments.get(i);
                    if (visited.contains(candidate)) continue;
                    if (Math.abs(candidate.gridIndex - current.gridIndex) > 1) continue;
                    double d = current.segment.end.distance(candidate.segment.start);
                    if (d > width * 3.0) continue;
                    if (d < minDistance) {
                        // ä½¿ç”¨ checkObstacleGeom è¿›è¡Œæ£€æµ‹
                        if (isLineSafe(current.segment.end, candidate.segment.start)) {
                            minDistance = d;
                            bestNext = candidate;
                        }
                    }
                }
                if (bestNext != null) {
                    zone.add(bestNext.segment);
                    visited.add(bestNext);
                    current = bestNext;
                } else {
                    lookingForNext = false;
                }
            }
            zones.add(zone);
        }
        // --- D. è¿žæŽ¥æ‰€æœ‰ Zones ---
        List<Lunjingguihua.PathSegment> resultPath = new ArrayList<>();
        List<List<Lunjingguihua.PathSegment>> remainingZones = new ArrayList<>(zones);
        List<Lunjingguihua.PathSegment> currentProcessingZone = remainingZones.remove(0);
        addZoneToPath(resultPath, currentProcessingZone, obstacleBuffer, false);
        while (!remainingZones.isEmpty()) {
            Lunjingguihua.PathSegment lastSeg = resultPath.get(resultPath.size() - 1);
            Coordinate currentPos = lastSeg.end;
            int bestZoneIdx = -1;
            double minDist = Double.MAX_VALUE;
            for (int i = 0; i < remainingZones.size(); i++) {
                List<Lunjingguihua.PathSegment> z = remainingZones.get(i);
                if (z.isEmpty()) continue;
                double d = currentPos.distance(z.get(0).start);
                if (d < minDist) {
                    minDist = d;
                    bestZoneIdx = i;
                }
            }
            if (bestZoneIdx != -1) {
                List<Lunjingguihua.PathSegment> nextZone = remainingZones.remove(bestZoneIdx);
                addZoneToPath(resultPath, nextZone, obstacleBuffer, true);
            } else {
                break;
            }
        }
        return resultPath;
    }
    /**
     * å°†ä¸€ä¸ª Zone æ·»åŠ åˆ°ç»“æžœè·¯å¾„ä¸­
     */
    private void addZoneToPath(List<Lunjingguihua.PathSegment> path,
                               List<Lunjingguihua.PathSegment> zone,
                               Geometry obstacleBuffer,
                               boolean needConnectToZoneStart) {
        if (zone.isEmpty()) return;
        // 1. è¿žæŽ¥åˆ° Zone çš„起点
        if (needConnectToZoneStart && !path.isEmpty()) {
            Coordinate from = path.get(path.size() - 1).end;
            Coordinate to = zone.get(0).start;
            List<Coordinate> travel = findSafePath(from, to, obstacleBuffer);
            if (travel.size() > 1) {
                for (int i = 0; i < travel.size() - 1; i++) {
                    path.add(new Lunjingguihua.PathSegment(travel.get(i), travel.get(i+1), false));
                }
            }
        }
        // 2. å¤„理 Zone å†…部
        for (int i = 0; i < zone.size(); i++) {
            Lunjingguihua.PathSegment seg = zone.get(i);
            if (i > 0) {
                Coordinate prevEnd = zone.get(i-1).end;
                Coordinate currStart = seg.start;
                if (!prevEnd.equals2D(currStart)) {
                    if (isLineSafe(prevEnd, currStart)) {
                         path.add(new Lunjingguihua.PathSegment(prevEnd, currStart, false));
                    } else {
                        List<Coordinate> detour = findSafePath(prevEnd, currStart, obstacleBuffer);
                        if (detour.size() > 1) {
                            for (int k = 0; k < detour.size() - 1; k++) {
                                path.add(new Lunjingguihua.PathSegment(detour.get(k), detour.get(k+1), false));
                            }
                        }
                    }
                }
            }
            path.add(seg);
        }
    }
    /**
     * æ£€æŸ¥ä¸¤ç‚¹è¿žçº¿æ˜¯å¦å®‰å…¨
     * ä¿®æ”¹ç‚¹ï¼š
     * 1. ä¸¥æ ¼æ£€æŸ¥ Boundary (Covers)
     * 2. ä½¿ç”¨ checkObstacleGeom (内缩版) æ£€æŸ¥éšœç¢ç‰©ï¼Œå…è®¸è´´è¾¹
     * 3. [Fix] ä½¿ç”¨ matrix.get() ä»£æ›¿ä¸å­˜åœ¨çš„ isIntersects(int)
     */
    private boolean isLineSafe(Coordinate p1, Coordinate p2) {
        if (p1.equals2D(p2)) return true;
        LineString line = gf.createLineString(new Coordinate[]{p1, p2});
        // 1. è¾¹ç•Œçº¦æŸï¼šçº¿æ®µå¿…须完全在地块内部
        if (boundaryGeom != null && !boundaryGeom.covers(line)) {
            return false;
        }
        // 2. é¿éšœçº¦æŸï¼šä½¿ç”¨å†…缩后的 buffer æ£€æŸ¥
        // å¦‚æžœ checkObstacleGeom ä¸ºç©ºï¼ˆæ— éšœç¢ï¼‰ï¼Œåˆ™å®‰å…¨
        if (checkObstacleGeom == null || checkObstacleGeom.isEmpty()) return true;
        IntersectionMatrix matrix = line.relate(checkObstacleGeom);
        // æˆ‘们要检查:线段的任何部分(Interior)是否穿过障碍物内部(Interior)
        // æˆ–者 çº¿æ®µçš„端点(Boundary)是否在障碍物内部(Interior)
        // å¦‚果两者都是 Dimension.FALSE (-1),则说明没有穿过内部
        boolean interiorIntersects = matrix.get(Location.INTERIOR, Location.INTERIOR) != Dimension.FALSE;
        boolean boundaryIntersects = matrix.get(Location.BOUNDARY, Location.INTERIOR) != Dimension.FALSE;
        return !interiorIntersects && !boundaryIntersects;
    }
    /**
     * å¯»æ‰¾ä¸¤ç‚¹é—´çš„安全路径
     * ä¿®æ”¹ç‚¹ï¼š
     * 1. å¢žåŠ ç‚¹ä½æ ¡éªŒä¸Žå¸é™„ï¼ˆSnap),确保起点终点有效
     * 2. ç§»é™¤ç›´çº¿å¼ºåˆ¶å›žé€€ï¼Œè‹¥å¯»è·¯å¤±è´¥åˆ™è¿”回空(或保留起点),避免穿墙
     */
    private List<Coordinate> findSafePath(Coordinate start, Coordinate end, Geometry obstacleBuffer) {
        // 0. æ•°æ®æ¸…洗:吸附起点终点到合法区域
        Coordinate safeStart = snapPointToValid(start);
        Coordinate safeEnd = snapPointToValid(end);
        List<Coordinate> path = new ArrayList<>();
        // 1. å°è¯•直连
        if (isLineSafe(safeStart, safeEnd)) {
            path.add(safeStart);
            path.add(safeEnd);
            return path;
        }
        // 2. æž„建可视图
        Set<Coordinate> nodes = new HashSet<>();
        nodes.add(safeStart);
        nodes.add(safeEnd);
        // æå–障碍物顶点
        addPolygonVertices(obstacleBuffer, nodes);
        // æå–边界顶点(关键:处理凹形边界)
        addPolygonVertices(boundaryGeom, nodes);
        List<Coordinate> nodeList = new ArrayList<>(nodes);
        // æž„建邻接表
        Map<Coordinate, List<Coordinate>> graph = new HashMap<>();
        for (Coordinate c1 : nodeList) {
            for (Coordinate c2 : nodeList) {
                if (c1 == c2) continue;
                if (isLineSafe(c1, c2)) {
                    graph.computeIfAbsent(c1, k -> new ArrayList<>()).add(c2);
                }
            }
        }
        // Dijkstra å¯»è·¯
        Map<Coordinate, Double> dist = new HashMap<>();
        Map<Coordinate, Coordinate> prev = new HashMap<>();
        PriorityQueue<Coordinate> pq = new PriorityQueue<>(Comparator.comparingDouble(dist::get));
        for (Coordinate n : nodeList) dist.put(n, Double.MAX_VALUE);
        dist.put(safeStart, 0.0);
        pq.add(safeStart);
        while (!pq.isEmpty()) {
            Coordinate u = pq.poll();
            if (u.equals2D(safeEnd)) break;
            if (dist.get(u) == Double.MAX_VALUE) break;
            if (graph.containsKey(u)) {
                for (Coordinate v : graph.get(u)) {
                    double alt = dist.get(u) + u.distance(v);
                    if (alt < dist.get(v)) {
                        dist.put(v, alt);
                        prev.put(v, u);
                        pq.add(v);
                    }
                }
            }
        }
        // é‡æž„路径
        if (prev.containsKey(safeEnd)) {
            LinkedList<Coordinate> p = new LinkedList<>();
            Coordinate curr = safeEnd;
            while (curr != null) {
                p.addFirst(curr);
                curr = prev.get(curr);
            }
            return p;
        }
        // å¯»è·¯å¤±è´¥ï¼Œè¿”回空(避免错误的直线穿越)
        return path;
    }
    // è¾…助:验证并吸附点到合法区域
    private Coordinate snapPointToValid(Coordinate p) {
        Point point = gf.createPoint(p);
        boolean inBoundary = (boundaryGeom == null) || boundaryGeom.covers(point);
        boolean outObstacle = (checkObstacleGeom == null) || !checkObstacleGeom.contains(point); // ä½¿ç”¨ contains è€Œä¸æ˜¯ intersects interior,稍微严格点
        if (inBoundary && outObstacle) return p;
        // å¦‚果点无效,尝试找最近的有效点(边界或障碍物边缘)
        // è¿™é‡Œç®€åŒ–处理:如果在障碍物内,吸附到障碍物边界;如果在边界外,吸附到边界
        // å®žé™…上 JTS DistanceOp.nearestPoints å¯ä»¥åšè¿™ä¸ª
        Geometry target = boundaryGeom;
        if (!outObstacle && checkObstacleGeom != null) {
            // åœ¨éšœç¢ç‰©å†…,优先吸附出障碍物
             Coordinate[] nearest = DistanceOp.nearestPoints(point, checkObstacleGeom.getBoundary());
             return nearest[1];
        }
        if (!inBoundary && boundaryGeom != null) {
             Coordinate[] nearest = DistanceOp.nearestPoints(point, boundaryGeom);
             return nearest[1];
        }
        return p;
    }
    private void addPolygonVertices(Geometry geom, Set<Coordinate> nodes) {
        if (geom == null) return;
        if (geom instanceof Polygon) {
            Collections.addAll(nodes, ((Polygon) geom).getExteriorRing().getCoordinates());
            for(int i=0; i<((Polygon)geom).getNumInteriorRing(); i++) {
                Collections.addAll(nodes, ((Polygon)geom).getInteriorRingN(i).getCoordinates());
            }
        } else if (geom instanceof MultiPolygon) {
            MultiPolygon mp = (MultiPolygon) geom;
            for (int i = 0; i < mp.getNumGeometries(); i++) {
                addPolygonVertices(mp.getGeometryN(i), nodes);
            }
        } else if (geom instanceof GeometryCollection) {
             GeometryCollection gc = (GeometryCollection) geom;
             for (int i = 0; i < gc.getNumGeometries(); i++) {
                 addPolygonVertices(gc.getGeometryN(i), nodes);
             }
        }
    }
    /**
     * åŽå¤„理:移除短线段,标记起终点
     */
    private void postProcessPath(List<Lunjingguihua.PathSegment> path) {
        if (path.isEmpty()) return;
        path.removeIf(seg -> seg.start.distance(seg.end) < 0.05);
        for (Lunjingguihua.PathSegment seg : path) {
            seg.isStartPoint = false;
            seg.isEndPoint = false;
        }
        boolean startFound = false;
        for (Lunjingguihua.PathSegment seg : path) {
            if (seg.isMowing) {
                seg.setAsStartPoint();
                startFound = true;
                break;
            }
        }
        for (int i = path.size() - 1; i >= 0; i--) {
            Lunjingguihua.PathSegment seg = path.get(i);
            if (seg.isMowing) {
                seg.setAsEndPoint();
                break;
            }
        }
    }
}
src/zhangaiwu/Obstacledraw.java
@@ -10,21 +10,28 @@
/**
 * éšœç¢ç‰©ç»˜åˆ¶ç±» - è´Ÿè´£ç»˜åˆ¶åœ°å—中的障碍物
 *
 * æ³¨æ„ï¼šéšœç¢ç‰©å›¾å±‚需要处于地块和导航路径上方。
 * åœ¨ MapRenderer.renderMap() ä¸­ï¼Œç»˜åˆ¶é¡ºåºåº”为:
 * 1. åœ°å—边界(底层)
 * 2. å¯¼èˆªè·¯å¾„(中层)
 * 3. éšœç¢ç‰©ï¼ˆé¡¶å±‚,显示在地块和导航路径上方)
 */
public class Obstacledraw {
    
    // é¢œè‰²å®šä¹‰
    private static final Color CIRCLE_FILL_COLOR = new Color(255, 182, 193, 120); // åœ†å½¢å¡«å……色 - æµ…粉红
    private static final Color CIRCLE_FILL_COLOR = new Color(128, 128, 128, 128); // åœ†å½¢å¡«å……色 - ç°è‰²ï¼Œé€æ˜Žåº¦50%
    private static final Color CIRCLE_BORDER_COLOR = new Color(199, 21, 133);    // åœ†å½¢è¾¹æ¡†è‰² - æ·±ç²‰çº¢
    private static final Color POLYGON_FILL_COLOR = new Color(173, 216, 230, 120); // å¤šè¾¹å½¢å¡«å……色 - æµ…蓝
    private static final Color POLYGON_FILL_COLOR = new Color(128, 128, 128, 128); // å¤šè¾¹å½¢å¡«å……色 - ç°è‰²ï¼Œé€æ˜Žåº¦50%
    private static final Color POLYGON_BORDER_COLOR = new Color(25, 25, 112);    // å¤šè¾¹å½¢è¾¹æ¡†è‰² - æ·±è“
    private static final Color OBSTACLE_LABEL_COLOR = Color.BLACK;
    private static final Color OBSTACLE_POINT_COLOR = Color.RED;
    
    // å°ºå¯¸å®šä¹‰
    private static final double OBSTACLE_POINT_SIZE = 0.1; // éšœç¢ç‰©æŽ§åˆ¶ç‚¹å¤§å°ï¼ˆç±³ï¼‰
    private static final float DEFAULT_BORDER_WIDTH = 1.0f;
    private static final float SELECTED_BORDER_WIDTH = 2.5f;
    // è¾¹ç•Œçº¿å®½åº¦ä¸Žåœ°å—边界线宽度一致:3 / Math.max(0.5, scale)
    private static final float BOUNDARY_STROKE_BASE = 3.0f; // ä¸Žåœ°å—边界线宽度一致
    private static final float SELECTED_WIDTH_MULTIPLIER = 1.5f; // é€‰ä¸­æ—¶çš„宽度倍数
    
    /**
     * ç»˜åˆ¶åœ°å—的所有障碍物
@@ -109,13 +116,15 @@
        g2d.setColor(CIRCLE_FILL_COLOR);
        g2d.fill(circle);
        
        // è®¾ç½®è¾¹æ¡†é¢œè‰²å’Œå®½åº¦
        // è®¾ç½®è¾¹æ¡†é¢œè‰²å’Œå®½åº¦ï¼ˆä¸Žåœ°å—边界线宽度一致)
        float strokeWidth = (float)(BOUNDARY_STROKE_BASE / Math.max(0.5, scale));
        if (isSelected) {
            g2d.setColor(CIRCLE_BORDER_COLOR.darker());
            g2d.setStroke(new BasicStroke(SELECTED_BORDER_WIDTH / (float)scale));
            // é€‰ä¸­æ—¶ç¨å¾®åŠ ç²—
            g2d.setStroke(new BasicStroke(strokeWidth * SELECTED_WIDTH_MULTIPLIER));
        } else {
            g2d.setColor(CIRCLE_BORDER_COLOR);
            g2d.setStroke(new BasicStroke(DEFAULT_BORDER_WIDTH / (float)scale));
            g2d.setStroke(new BasicStroke(strokeWidth));
        }
        g2d.draw(circle);
        
@@ -153,13 +162,15 @@
        g2d.setColor(POLYGON_FILL_COLOR);
        g2d.fill(polygon);
        
        // è®¾ç½®è¾¹æ¡†é¢œè‰²å’Œå®½åº¦
        // è®¾ç½®è¾¹æ¡†é¢œè‰²å’Œå®½åº¦ï¼ˆä¸Žåœ°å—边界线宽度一致)
        float strokeWidth = (float)(BOUNDARY_STROKE_BASE / Math.max(0.5, scale));
        if (isSelected) {
            g2d.setColor(POLYGON_BORDER_COLOR.darker());
            g2d.setStroke(new BasicStroke(SELECTED_BORDER_WIDTH / (float)scale));
            // é€‰ä¸­æ—¶ç¨å¾®åŠ ç²—
            g2d.setStroke(new BasicStroke(strokeWidth * SELECTED_WIDTH_MULTIPLIER));
        } else {
            g2d.setColor(POLYGON_BORDER_COLOR);
            g2d.setStroke(new BasicStroke(DEFAULT_BORDER_WIDTH / (float)scale));
            g2d.setStroke(new BasicStroke(strokeWidth));
        }
        g2d.draw(polygon);
        
@@ -202,6 +213,7 @@
    
    /**
     * ç»˜åˆ¶éšœç¢ç‰©æ ‡ç­¾
     * æ–‡å­—大小与"缩放"文字一致(11号字体),且不随地图缩放变化
     */
    private static void drawObstacleLabel(Graphics2D g2d, Obstacledge.Obstacle obstacle, 
                                          double scale) {
@@ -210,6 +222,9 @@
            return;
        }
        
        // ä¿å­˜å½“前变换
        AffineTransform originalTransform = g2d.getTransform();
        double centerX;
        double centerY;
@@ -224,6 +239,14 @@
            centerY = centroid.y;
        }
        
        // å°†ä¸–界坐标转换为屏幕坐标
        Point2D.Double worldPoint = new Point2D.Double(centerX, centerY);
        Point2D.Double screenPoint = new Point2D.Double();
        originalTransform.transform(worldPoint, screenPoint);
        // æ¢å¤åŽŸå§‹å˜æ¢ä»¥ä½¿ç”¨å±å¹•åæ ‡ç»˜åˆ¶
        g2d.setTransform(new AffineTransform());
        // èŽ·å–éšœç¢ç‰©åç§°
        String obstacleName = obstacle.getObstacleName();
        if (obstacleName == null || obstacleName.trim().isEmpty()) {
@@ -232,26 +255,24 @@
            obstacleName = obstacleName.trim();
        }
        
        // è®¾ç½®å­—体和颜色
        // è®¾ç½®å­—体和颜色(与"缩放"文字一致)
        g2d.setColor(OBSTACLE_LABEL_COLOR);
        // æ ¹æ®ç¼©æ”¾æ¯”例调整字体大小
        int fontSize = (int)(10 / scale);
        fontSize = Math.max(8, Math.min(fontSize, 14)); // é™åˆ¶å­—体大小范围
        g2d.setFont(new Font("微软雅黑", Font.PLAIN, fontSize));
        g2d.setFont(new Font("微软雅黑", Font.PLAIN, 11)); // ä¸Ž"缩放"文字大小一致
        
        // ç»˜åˆ¶æ ‡ç­¾
    String label = obstacleName;
        String label = obstacleName;
        FontMetrics metrics = g2d.getFontMetrics();
        int textWidth = metrics.stringWidth(label);
        int textHeight = metrics.getHeight();
        
        // åœ¨ä¸­å¿ƒç‚¹ç»˜åˆ¶æ ‡ç­¾
        int textX = (int)(centerX - textWidth / 2.0);
        int textY = (int)(centerY + textHeight / 4.0); // ç¨å¾®å‘下偏移
        // åœ¨å±å¹•坐标中心点绘制标签
        int textX = (int)(screenPoint.x - textWidth / 2.0);
        int textY = (int)(screenPoint.y + textHeight / 4.0); // ç¨å¾®å‘下偏移
        
        g2d.drawString(label, textX, textY);
        // æ¢å¤åŽŸå§‹å˜æ¢
        g2d.setTransform(originalTransform);
    }
    private static Point2D.Double computePolygonCentroid(List<Obstacledge.XYCoordinate> xyCoords) {
src/zhuye/LegendDialog.java
@@ -32,29 +32,6 @@
        mainPanel.setBackground(Color.WHITE);
        mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 10, 15));
        
        // è®¡ç®—图例内容面板的宽度(用于设置图标尺寸)
        // å›¾ä¾‹å¯¹è¯æ¡†å®½åº¦ = DIALOG_WIDTH * 0.8
        // ä¸»é¢æ¿å·¦å³è¾¹æ¡†å„15像素,图例内容面板左右内边距各10像素
        int adjustedWidth = (int) Math.round(UIConfig.DIALOG_WIDTH * 0.8);
        int iconSize = adjustedWidth - 30 - 20; // å‡åŽ»ä¸»é¢æ¿å·¦å³è¾¹æ¡†(15*2)和图例内容面板左右内边距(10*2)
        // åˆ›å»ºå‰²è‰æœºå›¾æ ‡é¢æ¿
        JPanel iconPanel = new JPanel(new BorderLayout());
        iconPanel.setBackground(Color.WHITE);
        iconPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); // åº•部间距10像素
        JLabel gecaojiLabel = new JLabel();
        gecaojiLabel.setHorizontalAlignment(SwingConstants.CENTER);
        ImageIcon gecaojiIcon = loadIcon("image/gecaoji.png", iconSize, iconSize);
        if (gecaojiIcon != null) {
            gecaojiLabel.setIcon(gecaojiIcon);
        } else {
            // å¦‚果图标加载失败,显示占位文本
            gecaojiLabel.setText("割草机图标");
            gecaojiLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        }
        iconPanel.add(gecaojiLabel, BorderLayout.CENTER);
        // å›¾ä¾‹å†…容面板 - ç›´æŽ¥æ·»åŠ ï¼Œä¸ä½¿ç”¨æ»šåŠ¨æ¡
        JPanel contentPanel = new JPanel();
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
@@ -84,8 +61,7 @@
            contentPanel.remove(contentPanel.getComponentCount() - 1);
        }
        
        // æ·»åŠ å›¾æ ‡é¢æ¿å’Œå›¾ä¾‹å†…å®¹é¢æ¿
        mainPanel.add(iconPanel, BorderLayout.NORTH);
        // æ·»åŠ å›¾ä¾‹å†…å®¹é¢æ¿
        mainPanel.add(contentPanel, BorderLayout.CENTER);
        
        getContentPane().add(mainPanel);
src/zhuye/MapRenderer.java
@@ -346,14 +346,11 @@
        boolean hasPlannedPath = currentPlannedPath != null && currentPlannedPath.size() >= 2;
        boolean hasObstacles = currentObstacles != null && !currentObstacles.isEmpty();
        // ç»˜åˆ¶åœ°å—边界(底层)
        if (hasBoundary) {
            drawCurrentBoundary(g2d);
        }
        if (hasObstacles) {
            Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName);
        }
        yulanzhangaiwu.renderPreview(g2d, scale);
        if (!circleSampleMarkers.isEmpty()) {
@@ -366,10 +363,16 @@
    adddikuaiyulan.drawPreview(g2d, handheldBoundaryPreview, scale, handheldBoundaryPreviewActive, boundaryPreviewMarkerScale);
        // ç»˜åˆ¶å¯¼èˆªè·¯å¾„(中层)
        if (hasPlannedPath) {
            drawCurrentPlannedPath(g2d);
        }
        // ç»˜åˆ¶éšœç¢ç‰©ï¼ˆé¡¶å±‚,显示在地块和导航路径上方)
        if (hasObstacles) {
            Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName);
        }
        if (boundaryPointsVisible && hasBoundary) {
            double markerScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d);
            pointandnumber.drawBoundaryPoints(
src/zhuye/Shouye.java
@@ -11,6 +11,8 @@
import java.awt.event.*;
import chuankou.dellmessage;
import chuankou.sendmessage;
import chuankou.SerialPortService;
import dikuai.Dikuai;
import dikuai.Dikuaiguanli;
import dikuai.addzhangaiwu;
@@ -287,6 +289,8 @@
        // è¾¹ç•Œæ£€æŸ¥å®šæ—¶å™¨ï¼šæ¯500ms检查一次割草机是否在边界内
        boundaryWarningTimer = new Timer(500, e -> {
            checkMowerBoundaryStatus();
            // åŒæ—¶æ›´æ–°è“ç‰™å›¾æ ‡çŠ¶æ€
            updateBluetoothButtonIcon();
        });
        boundaryWarningTimer.setInitialDelay(0);
        boundaryWarningTimer.start();
@@ -957,12 +961,14 @@
            }
        });
        ensureBluetoothIconsLoaded();
        bluetoothConnected = Bluelink.isConnected();
        ImageIcon initialIcon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon;
        // æ ¹æ®ä¸²å£è¿žæŽ¥çŠ¶æ€æ˜¾ç¤ºå›¾æ ‡
        SerialPortService service = sendmessage.getActiveService();
        boolean serialConnected = (service != null && service.isOpen());
        ImageIcon initialIcon = serialConnected ? bluetoothLinkedIcon : bluetoothIcon;
        if (initialIcon != null) {
            button.setIcon(initialIcon);
        } else {
            button.setText(bluetoothConnected ? "已连" : "蓝牙");
            button.setText(serialConnected ? "已连" : "蓝牙");
        }
        return button;
    }
@@ -1862,13 +1868,15 @@
            return;
        }
        ensureBluetoothIconsLoaded();
        bluetoothConnected = Bluelink.isConnected();
        ImageIcon icon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon;
        // æ ¹æ®ä¸²å£è¿žæŽ¥çŠ¶æ€æ˜¾ç¤ºå›¾æ ‡
        SerialPortService service = sendmessage.getActiveService();
        boolean serialConnected = (service != null && service.isOpen());
        ImageIcon icon = serialConnected ? bluetoothLinkedIcon : bluetoothIcon;
        if (icon != null) {
            bluetoothBtn.setIcon(icon);
            bluetoothBtn.setText(null);
        } else {
            bluetoothBtn.setText(bluetoothConnected ? "已连" : "蓝牙");
            bluetoothBtn.setText(serialConnected ? "已连" : "蓝牙");
        }
    }