张世豪
11 小时以前 13d032241e1a2938a8be4f64c9171e1240e9ea1e
新增了边界管理页面和首页边界虚线功能
已添加2个文件
已修改16个文件
1378 ■■■■■ 文件已修改
set.properties 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bianjie/BoundaryLengthDrawer.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bianjie/shudongdraw.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuai.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuaiguanli.java 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuanbianjipage.java 316 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/ObstacleManagementPage.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/daohangyulan.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/AoxinglujingNoObstacle.java 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/MowingPathGenerationPage.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/ObstaclePathPlanner.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/YixinglujingNoObstacle.java 457 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhangaiwu/AddDikuai.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/ShouyeLauncher.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/bianjiedrwa.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/celiangmoshi.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
set.properties
@@ -1,5 +1,5 @@
#Mower Configuration Properties - Updated
#Mon Dec 22 13:47:46 CST 2025
#Mon Dec 22 18:50:02 CST 2025
appVersion=-1
boundaryLengthVisible=false
currentWorkLandNumber=LAND2
@@ -8,12 +8,12 @@
handheldMarkerId=1872
idleTrailDurationSeconds=60
manualBoundaryDrawingMode=false
mapScale=25.45
mapScale=16.74
measurementModeEnabled=false
mowerId=860
serialAutoConnect=true
serialBaudRate=115200
serialPortName=COM15
simCardNumber=-1
viewCenterX=-27.62
viewCenterY=-15.01
viewCenterX=4.67
viewCenterY=0.55
src/bianjie/BoundaryLengthDrawer.java
@@ -139,3 +139,5 @@
src/bianjie/shudongdraw.java
@@ -201,3 +201,5 @@
        g2d.setStroke(originalStroke);
    }
}
src/dikuai/Dikuai.java
@@ -17,6 +17,8 @@
    private String landName;
    // è¾¹ç•ŒåŽŸå§‹åæ ‡
    private String boundaryOriginalCoordinates;
    // è¾¹ç•ŒåŽŸå§‹XY坐标(相对于基准站的XY坐标)
    private String boundaryOriginalXY;
    // è¾¹ç•Œåæ ‡ï¼ˆå­˜å‚¨å¤šè¾¹å½¢åæ ‡ç‚¹é›†åˆï¼‰
    private String boundaryCoordinates;
    // è§„划路径(存储路径坐标点集合)
@@ -101,6 +103,7 @@
                dikuai.userId = landProps.getProperty("userId", "-1");
                dikuai.landName = landProps.getProperty("landName", "-1");
                dikuai.boundaryOriginalCoordinates = landProps.getProperty("boundaryOriginalCoordinates", "-1");
                dikuai.boundaryOriginalXY = landProps.getProperty("boundaryOriginalXY", "-1");
                dikuai.boundaryCoordinates = landProps.getProperty("boundaryCoordinates", "-1");
                dikuai.plannedPath = landProps.getProperty("plannedPath", "-1");
                dikuai.returnPointCoordinates = landProps.getProperty("returnPointCoordinates", "-1");
@@ -201,6 +204,9 @@
            case "boundaryOriginalCoordinates":
                this.boundaryOriginalCoordinates = value;
                return true;
            case "boundaryOriginalXY":
                this.boundaryOriginalXY = value;
                return true;
            case "boundaryCoordinates":
                this.boundaryCoordinates = value;
                return true;
@@ -276,6 +282,7 @@
            if (dikuai.landNumber != null) properties.setProperty(landNumber + ".landNumber", dikuai.landNumber);
            if (dikuai.landName != null) properties.setProperty(landNumber + ".landName", dikuai.landName);
            if (dikuai.boundaryOriginalCoordinates != null) properties.setProperty(landNumber + ".boundaryOriginalCoordinates", dikuai.boundaryOriginalCoordinates);
            if (dikuai.boundaryOriginalXY != null) properties.setProperty(landNumber + ".boundaryOriginalXY", dikuai.boundaryOriginalXY);
            if (dikuai.boundaryCoordinates != null) properties.setProperty(landNumber + ".boundaryCoordinates", dikuai.boundaryCoordinates);
            if (dikuai.plannedPath != null) properties.setProperty(landNumber + ".plannedPath", dikuai.plannedPath);
            if (dikuai.returnPointCoordinates != null) properties.setProperty(landNumber + ".returnPointCoordinates", dikuai.returnPointCoordinates);
@@ -341,6 +348,14 @@
        this.boundaryOriginalCoordinates = boundaryOriginalCoordinates;
    }
    public String getBoundaryOriginalXY() {
        return boundaryOriginalXY;
    }
    public void setBoundaryOriginalXY(String boundaryOriginalXY) {
        this.boundaryOriginalXY = boundaryOriginalXY;
    }
    public String getBoundaryCoordinates() {
        return boundaryCoordinates;
    }
@@ -492,6 +507,7 @@
                ", landNumber='" + landNumber + '\'' +
                ", landName='" + landName + '\'' +
                ", boundaryOriginalCoordinates='" + boundaryOriginalCoordinates + '\'' +
                ", boundaryOriginalXY='" + boundaryOriginalXY + '\'' +
                ", boundaryCoordinates='" + boundaryCoordinates + '\'' +
                ", plannedPath='" + plannedPath + '\'' +
                ", returnPointCoordinates='" + returnPointCoordinates + '\'' +
src/dikuai/Dikuaiguanli.java
@@ -337,8 +337,9 @@
        contentPanel.add(obstaclePanel);
    contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        // åœ°å—边界坐标(带显示顶点按钮)
        JPanel boundaryPanel = createBoundaryInfoItem(dikuai);
        // åœ°å—边界坐标(带查看按钮)
        JPanel boundaryPanel = createCardInfoItemWithIconButton("地块边界:",
            createViewButton(e -> editBoundaryCoordinates(dikuai)));
        configureInteractiveLabel(getInfoItemTitleLabel(boundaryPanel),
            () -> editBoundaryCoordinates(dikuai),
            "点击查看/编辑地块边界坐标");
@@ -362,14 +363,6 @@
    contentPanel.add(baseStationPanel);
    contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
    JPanel boundaryOriginalPanel = createCardInfoItemWithIconButton("边界原始坐标:",
        createViewButton(e -> editBoundaryOriginalCoordinates(dikuai)));
    configureInteractiveLabel(getInfoItemTitleLabel(boundaryOriginalPanel),
        () -> editBoundaryOriginalCoordinates(dikuai),
        "点击查看/编辑边界原始坐标");
    contentPanel.add(boundaryOriginalPanel);
        contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        JPanel completedTrackPanel = createCardInfoItemWithButton("已完成割草路径:",
            getTruncatedValue(dikuai.getMowingTrack(), 12, "未记录"),
            createViewButton(e -> showCompletedMowingTrackDialog(dikuai)));
@@ -1035,17 +1028,35 @@
        if (dikuai == null) {
            return;
        }
        String edited = promptCoordinateEditing("查看 / ç¼–辑地块边界坐标", dikuai.getBoundaryCoordinates());
        Window owner = SwingUtilities.getWindowAncestor(this);
        // èŽ·å–åœ°å—ç®¡ç†å¯¹è¯æ¡†ï¼Œå‡†å¤‡åœ¨æ‰“å¼€è¾¹ç•Œç¼–è¾‘é¡µé¢æ—¶å…³é—­
        Window managementWindow = null;
        if (owner instanceof JDialog) {
            managementWindow = owner;
        }
        // æ‰“开边界编辑页面
        Dikuanbianjipage page = new Dikuanbianjipage(owner, "地块边界管理页面", dikuai.getBoundaryCoordinates(), dikuai);
        page.setVisible(true);
        // å…³é—­åœ°å—管理页面
        if (managementWindow != null) {
            managementWindow.dispose();
        }
        // èŽ·å–ç¼–è¾‘ç»“æžœå¹¶ä¿å­˜
        String edited = page.getResult();
        if (edited == null) {
            return;
        }
        String normalized = normalizeCoordinateInput(edited);
        if (!saveFieldAndRefresh(dikuai, "boundaryCoordinates", normalized)) {
            JOptionPane.showMessageDialog(this, "无法更新地块边界坐标", "错误", JOptionPane.ERROR_MESSAGE);
            JOptionPane.showMessageDialog(null, "无法更新地块边界坐标", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }
        String message = "-1".equals(normalized) ? "地块边界坐标已清空" : "地块边界坐标已更新";
        JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
        JOptionPane.showMessageDialog(null, message, "成功", JOptionPane.INFORMATION_MESSAGE);
    }
    private void editPlannedPath(Dikuai dikuai) {
@@ -1082,22 +1093,6 @@
        JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
    }
    private void editBoundaryOriginalCoordinates(Dikuai dikuai) {
        if (dikuai == null) {
            return;
        }
        String edited = promptCoordinateEditing("查看 / ç¼–辑边界原始坐标", dikuai.getBoundaryOriginalCoordinates());
        if (edited == null) {
            return;
        }
        String normalized = normalizeCoordinateInput(edited);
        if (!saveFieldAndRefresh(dikuai, "boundaryOriginalCoordinates", normalized)) {
            JOptionPane.showMessageDialog(this, "无法更新边界原始坐标", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }
        String message = "-1".equals(normalized) ? "边界原始坐标已清空" : "边界原始坐标已更新";
        JOptionPane.showMessageDialog(this, message, "成功", JOptionPane.INFORMATION_MESSAGE);
    }
    private void editMowingPattern(Dikuai dikuai) {
        if (dikuai == null) {
src/dikuai/Dikuanbianjipage.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,316 @@
package dikuai;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import bianjie.bianjieguihua2;
import publicway.Fuzhibutton;
import zhuye.Coordinate;
import zhuye.Shouye;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * åœ°å—边界管理页面
 * æ˜¾ç¤ºï¼šåŽŸå§‹è¾¹ç•Œåæ ‡ï¼ˆç»çº¬åº¦ã€é«˜ç¨‹ï¼‰ã€åŽŸå§‹è¾¹ç•ŒXY(相对于基准站)、以及可编辑的优化后边界坐标
 */
public class Dikuanbianjipage 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);
    private String result = null;
    private Dikuai dikuai;
    public Dikuanbianjipage(Window owner, String title, String initialValue, Dikuai dikuai) {
        super(owner, title, Dialog.ModalityType.APPLICATION_MODAL);
        this.dikuai = dikuai;
        initializeUI(title, initialValue, dikuai);
    }
    public String getResult() {
        return result;
    }
    private void initializeUI(String title, String initialValue, Dikuai dikuai) {
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        getContentPane().setLayout(new BorderLayout());
        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 = dikuai != null ? (dikuai.getLandName() != null ? dikuai.getLandName() : "未知地块") : "未知地块";
        String landNumber = dikuai != null ? (dikuai.getLandNumber() != null ? 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));
        // åŽŸå§‹è¾¹ç•Œåæ ‡ï¼ˆç»çº¬åº¦, é«˜ç¨‹ï¼‰
        String rawCoords = dikuai != null ? prepareCoordinateForEditor(dikuai.getBoundaryOriginalCoordinates()) : "";
        int rawCount = 0;
        if (rawCoords != null && !rawCoords.isEmpty() && !"-1".equals(rawCoords)) {
            rawCount = rawCoords.split(";").length;
        }
        JTextArea rawTextArea = createInfoTextArea(rawCoords, false, 6);
        contentPanel.add(createTextAreaSection("原始边界坐标 (经度, çº¬åº¦, é«˜ç¨‹) (" + rawCount + "点)", rawTextArea));
        // åŽŸå§‹è¾¹ç•ŒXY坐标(相对于基准站)
        String rawXY = dikuai != null ? prepareCoordinateForEditor(dikuai.getBoundaryOriginalXY()) : "";
        int xyCount = 0;
        if (rawXY != null && !rawXY.isEmpty() && !"-1".equals(rawXY)) {
            xyCount = rawXY.split(";").length;
        }
        JTextArea rawXYArea = createInfoTextArea(rawXY, false, 6);
        contentPanel.add(createTextAreaSection("原始边界XY坐标(相对于基准站) (" + xyCount + "点)", rawXYArea));
        // ä¼˜åŒ–后边界坐标(可编辑)
        String optCoords = prepareCoordinateForEditor(initialValue);
        int optCount = 0;
        if (optCoords != null && !optCoords.isEmpty() && !"-1".equals(optCoords)) {
            optCount = optCoords.split(";").length;
        }
        JTextArea optTextArea = createInfoTextArea(optCoords, true, 6);
        contentPanel.add(createTextAreaSection("优化后地块边界坐标 (" + optCount + "点)", optTextArea));
        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 optimizeBtn = createPrimaryFooterButton("优化边界坐标");
        JButton saveBtn = createPrimaryFooterButton("保存");
        JButton previewBtn = createPrimaryFooterButton("预览");
        JButton closeBtn = createPrimaryFooterButton("关闭");
        optimizeBtn.addActionListener(e -> {
            if (dikuai == null) {
                JOptionPane.showMessageDialog(this, "未找到地块信息,无法优化", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            String baseStation = dikuai.getBaseStationCoordinates();
            if (baseStation == null || baseStation.trim().isEmpty() || "-1".equals(baseStation)) {
                JOptionPane.showMessageDialog(this, "基站坐标未设置,无法优化", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            // å‡†å¤‡ Coordinate åˆ—表
            String originalCoords = dikuai.getBoundaryOriginalCoordinates();
            if (originalCoords == null || originalCoords.trim().isEmpty() || "-1".equals(originalCoords)) {
                JOptionPane.showMessageDialog(this, "原始边界坐标为空,无法优化", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            try {
                // è§£æžåŽŸå§‹åæ ‡åˆ° Coordinate.coordinates
                List<Coordinate> coords = new ArrayList<>();
                String[] points = originalCoords.split(";");
                for (String point : points) {
                    String[] parts = point.split(",");
                    if (parts.length >= 2) {
                        double lonDecimal = Double.parseDouble(parts[0].trim());
                        double latDecimal = Double.parseDouble(parts[1].trim());
                        double alt = parts.length > 2 ? Double.parseDouble(parts[2].trim()) : 0.0;
                        // å°†åè¿›åˆ¶åº¦è½¬æ¢ä¸ºåº¦åˆ†æ ¼å¼å­—符串
                        String latDM = decimalToDegreeMinute(latDecimal);
                        String lonDM = decimalToDegreeMinute(lonDecimal);
                        String latDir = latDecimal >= 0 ? "N" : "S";
                        String lonDir = lonDecimal >= 0 ? "E" : "W";
                        coords.add(new Coordinate(latDM, latDir, lonDM, lonDir, alt));
                    }
                }
                Coordinate.coordinates = coords;
                // è°ƒç”¨ä¼˜åŒ–算法
                String optimized = bianjieguihua2.processCoordinateListAuto(baseStation);
                optTextArea.setText(optimized);
                JOptionPane.showMessageDialog(this, "边界优化完成", "提示", JOptionPane.INFORMATION_MESSAGE);
            } catch (Exception ex) {
                JOptionPane.showMessageDialog(this, "优化失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
                ex.printStackTrace();
            }
        });
        previewBtn.addActionListener(e -> {
            if (dikuai == null) {
                return;
            }
            // å…³é—­å½“前对话框
            dispose();
            // èŽ·å–å½“å‰ä¼˜åŒ–åŽçš„è¾¹ç•Œ
            String currentOptimized = optTextArea.getText();
            // è°ƒç”¨é¦–页显示预览
            SwingUtilities.invokeLater(() -> {
                Shouye.showBoundaryPreview(dikuai, currentOptimized, () -> {
                    // è¿”回回调:重新打开此页面
                    new Dikuanbianjipage(getOwner(), getTitle(), currentOptimized, dikuai).setVisible(true);
                });
            });
        });
        saveBtn.addActionListener(e -> {
            if (dikuai == null) {
                JOptionPane.showMessageDialog(this, "未找到地块信息,无法保存", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            String currentText = optTextArea.getText();
            if (currentText == null || currentText.trim().isEmpty() || "-1".equals(currentText.trim())) {
                JOptionPane.showMessageDialog(this, "优化后地块边界为空,无法保存", "提示", JOptionPane.WARNING_MESSAGE);
                return;
            }
            String trimmed = currentText.trim();
            // ä¿å­˜åˆ°åœ°å—对象
            if (!Dikuai.updateField(dikuai.getLandNumber(), "boundaryCoordinates", trimmed)) {
                JOptionPane.showMessageDialog(this, "无法更新地块边界坐标", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            Dikuai.updateField(dikuai.getLandNumber(), "updateTime", getCurrentTime());
            Dikuai.saveToProperties();
            this.result = trimmed;
            JOptionPane.showMessageDialog(this, "地块边界坐标已更新", "成功", JOptionPane.INFORMATION_MESSAGE);
            dispose();
        });
        closeBtn.addActionListener(e -> dispose());
        // æŒ‰é’®é¡ºåºï¼šä¼˜åŒ–边界坐标、保存、预览、关闭
        buttonPanel.add(optimizeBtn);
        buttonPanel.add(saveBtn);
        buttonPanel.add(previewBtn);
        buttonPanel.add(closeBtn);
        add(buttonPanel, BorderLayout.SOUTH);
        pack();
        setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
        setLocationRelativeTo(getOwner());
    }
    private String prepareCoordinateForEditor(String value) {
        if (value == null) {
            return "";
        }
        String trimmed = value.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return "";
        }
        return trimmed;
    }
    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);
        JPanel titlePanel = new JPanel(new BorderLayout());
        titlePanel.setBackground(BACKGROUND_COLOR);
        titlePanel.setOpaque(false);
        JLabel titleLabel = new JLabel(title);
        titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
        titleLabel.setForeground(TEXT_COLOR);
        titlePanel.add(titleLabel, BorderLayout.WEST);
        JButton copyButton = Fuzhibutton.createCopyButton(
            () -> {
                String text = textArea.getText();
                if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) {
                    return null;
                }
                return text;
            },
            "复制",
            new Color(230, 250, 240)
        );
        copyButton.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        copyButton.setPreferredSize(new Dimension(50, 24));
        copyButton.setMargin(new Insets(0,0,0,0));
        titlePanel.add(copyButton, BorderLayout.EAST);
        section.add(titlePanel, 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 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 getCurrentTime() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(new Date());
    }
    /**
     * å°†åè¿›åˆ¶åº¦è½¬æ¢ä¸ºåº¦åˆ†æ ¼å¼å­—符串
     * ä¾‹å¦‚:39.831468 -> "3949.888080" (39度49.888080分)
     *
     * @param decimalDegrees åè¿›åˆ¶åº¦
     * @return åº¦åˆ†æ ¼å¼å­—符串
     */
    private String decimalToDegreeMinute(double decimalDegrees) {
        double absDecimal = Math.abs(decimalDegrees);
        int degrees = (int) Math.floor(absDecimal);
        double minutes = (absDecimal - degrees) * 60.0;
        double degreeMinutes = degrees * 100.0 + minutes;
        return String.format(java.util.Locale.US, "%.8f", degreeMinutes);
    }
}
src/dikuai/ObstacleManagementPage.java
@@ -984,3 +984,5 @@
src/dikuai/daohangyulan.java
@@ -546,3 +546,5 @@
        return isNavigating;
    }
}
src/lujing/AoxinglujingNoObstacle.java
@@ -1,14 +1,19 @@
package lujing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
 * æ— éšœç¢ç‰©å‡¸å½¢è‰åœ°è·¯å¾„规划类 (优化版)
 * æ— éšœç¢ç‰©å‡¸å½¢è‰åœ°è·¯å¾„规划类 (终极优化版)
 * ç‰¹æ€§ï¼š
 * 1. æœ€å°æŠ•影宽度方向选择 (效率最高,转弯最少)
 * 2. è¾¹ç¼˜è½®å»“优先切割 (无死角覆盖)
 * 3. æ”¯æŒå¤–部传入已包含重叠率的宽度参数
 */
public class AoxinglujingNoObstacle {
    // ä¼˜åŒ–:引入极小值用于浮点数比较,处理几何精度误差
    // å¼•入极小值用于浮点数比较,处理几何精度误差
    private static final double EPSILON = 1e-6;
    /**
@@ -49,15 +54,15 @@
    }
    /**
     * å¯¹å¤–公开的静态调用方法 (保留字符串入参格式)
     * å¯¹å¤–公开的静态调用方法
     *
     * @param boundaryCoordsStr åœ°å—边界坐标字符串 "x1,y1;x2,y2;..."
     * @param mowingWidthStr    å‰²è‰å®½åº¦å­—符串,如 "0.34"
     * @param mowingWidthStr    æœ‰æ•ˆå‰²è‰å®½åº¦å­—符串 (已包含重叠率),如 "0.30"
     * @param safetyMarginStr   å®‰å…¨è¾¹è·å­—符串,如 "0.2"
     * @return è·¯å¾„段列表
     */
    public static List<PathSegment> planPath(String boundaryCoordsStr, String mowingWidthStr, String safetyMarginStr) {
        // 1. è§£æžå‚æ•° (优化:单独提取解析逻辑)
        // 1. è§£æžå‚æ•°
        List<Point> originalPolygon = parseCoords(boundaryCoordsStr);
        double mowingWidth;
        double safetyMargin;
@@ -74,85 +79,119 @@
    }
    /**
     * æ ¸å¿ƒç®—法逻辑 (强类型入参,便于测试和内部调用)
     * æ ¸å¿ƒç®—法逻辑
     */
    private static List<PathSegment> planPathCore(List<Point> originalPolygon, double mowingWidth, double safetyMargin) {
        // ä¼˜åŒ–:三角形也是合法的凸多边形,限制改为小于3
    private static List<PathSegment> planPathCore(List<Point> originalPolygon, double width, double safetyMargin) {
        if (originalPolygon == null || originalPolygon.size() < 3) {
            throw new IllegalArgumentException("多边形坐标点不足3个,无法构成有效区域");
            return new ArrayList<>(); // æˆ–抛出异常,视业务需求而定
        }
        // ç¡®ä¿å¤šè¾¹å½¢æ˜¯é€†æ—¶é’ˆæ–¹å‘
        ensureCCW(originalPolygon);
        // 1. æ ¹æ®å®‰å…¨è¾¹è·å†…缩
        List<Point> shrunkPolygon = shrinkPolygon(originalPolygon, safetyMargin);
        // 1. æ ¹æ®å®‰å…¨è¾¹è·å†…缩,得到实际作业区域
        List<Point> workAreaPolygon = shrinkPolygon(originalPolygon, safetyMargin);
        // ä¼˜åŒ–:内缩后如果多边形失效(例如地块太窄,内缩后消失),直接返回空路径,避免后续报错
        if (shrunkPolygon.size() < 3) {
            // è¿™é‡Œå¯ä»¥è®°å½•日志:地块过小,无法满足安全距离作业
        // å¦‚果内缩后区域失效(如地块太小),返回空路径
        if (workAreaPolygon.size() < 3) {
            return new ArrayList<>();
        }
        // 2. è®¡ç®—最长边角度 (作为扫描方向)
        double angle = calculateLongestEdgeAngle(originalPolygon);
        List<PathSegment> finalPath = new ArrayList<>();
        // 3. & 4. æ—‹è½¬æ‰«æå¹¶å‰ªè£
        List<PathSegment> mowingLines = generateClippedMowingLines(shrunkPolygon, angle, mowingWidth);
        // 2. [优化] ä¼˜å…ˆç”Ÿæˆè½®å»“路径 (Contour Pass)
        // æ²¿ä½œä¸šè¾¹ç•Œèµ°ä¸€åœˆï¼Œç¡®ä¿è¾¹ç¼˜æ•´é½ä¸”无遗漏
        addContourPath(workAreaPolygon, finalPath);
        // 5. å¼“字形连接
        return connectPathSegments(mowingLines);
    }
        // 3. [优化] è®¡ç®—最佳扫描角度
        // å¯»æ‰¾è®©å¤šè¾¹å½¢æŠ•影高度最小的角度,从而最小化转弯次数
        double bestAngle = findOptimalScanAngle(workAreaPolygon);
    // ================= æ ¸å¿ƒç®—法辅助方法 =================
        // 4. ç”Ÿæˆå†…部弓字形路径
        // ç›´æŽ¥ä½¿ç”¨ä¼ å…¥çš„ width (已包含重叠率)
        List<PathSegment> zigZagPaths = generateClippedMowingLines(workAreaPolygon, bestAngle, width);
    private static List<Point> shrinkPolygon(List<Point> polygon, double margin) {
        List<Point> newPoints = new ArrayList<>();
        int n = polygon.size();
        for (int i = 0; i < n; i++) {
            Point p1 = polygon.get(i);
            Point p2 = polygon.get((i + 1) % n);
            Point p0 = polygon.get((i - 1 + n) % n);
            Line line1 = offsetLine(p1, p2, margin);
            Line line0 = offsetLine(p0, p1, margin);
            Point intersection = getIntersection(line0, line1);
        // 5. è¿žæŽ¥è½®å»“路径和弓字形路径
        if (!finalPath.isEmpty() && !zigZagPaths.isEmpty()) {
            Point contourEnd = finalPath.get(finalPath.size() - 1).end;
            Point zigzagStart = zigZagPaths.get(0).start;
            
            // ä¼˜åŒ–:增加非空判断,且如果交点异常远(尖角效应),实际工程中可能需要切角处理
            // è¿™é‡Œæš‚保留基础逻辑,但在凸多边形中通常没问题
            if (intersection != null) {
                newPoints.add(intersection);
            // å¦‚果轮廓终点与弓字形起点不重合,添加过渡段
            if (distanceSq(contourEnd, zigzagStart) > EPSILON) {
                finalPath.add(new PathSegment(contourEnd, zigzagStart, false));
            }
        }
        return newPoints;
        // 6. åˆå¹¶å¼“字形路径
        finalPath.addAll(connectPathSegments(zigZagPaths));
        return finalPath;
    }
    private static double calculateLongestEdgeAngle(List<Point> polygon) {
        double maxDistSq = -1;
        double angle = 0;
        int n = polygon.size();
    // ================= æ ¸å¿ƒé€»è¾‘辅助方法 =================
    /**
     * æ·»åŠ è½®å»“è·¯å¾„ (围着多边形走一圈)
     */
    private static void addContourPath(List<Point> polygon, List<PathSegment> path) {
        int n = polygon.size();
        for (int i = 0; i < n; i++) {
            Point p1 = polygon.get(i);
            Point p2 = polygon.get((i + 1) % n);
            double dx = p2.x - p1.x;
            double dy = p2.y - p1.y;
            double distSq = dx * dx + dy * dy;
            path.add(new PathSegment(p1, p2, true));
        }
    }
            if (distSq > maxDistSq) {
                maxDistSq = distSq;
                angle = Math.atan2(dy, dx);
    /**
     * å¯»æ‰¾æœ€ä¼˜æ‰«æè§’度 (最小投影高度法)
     */
    private static double findOptimalScanAngle(List<Point> polygon) {
        double minHeight = Double.MAX_VALUE;
        double bestAngle = 0;
        int n = polygon.size();
        // éåŽ†æ¯ä¸€æ¡è¾¹ï¼Œè®¡ç®—ä»¥è¯¥è¾¹ä¸ºâ€œåº•â€æ—¶ï¼Œå¤šè¾¹å½¢çš„é«˜åº¦
        for (int i = 0; i < n; i++) {
            Point p1 = polygon.get(i);
            Point p2 = polygon.get((i + 1) % n);
            // å½“前边的角度
            double currentAngle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
            // è®¡ç®—在这个角度下的投影高度
            double height = calculatePolygonHeightAtAngle(polygon, currentAngle);
            if (height < minHeight) {
                minHeight = height;
                bestAngle = currentAngle;
            }
        }
        return angle;
        return bestAngle;
    }
    /**
     * è®¡ç®—多边形在特定旋转角度下的Y轴投影高度
     */
    private static double calculatePolygonHeightAtAngle(List<Point> poly, double angle) {
        double minY = Double.MAX_VALUE;
        double maxY = -Double.MAX_VALUE;
        double cos = Math.cos(-angle);
        double sin = Math.sin(-angle);
        for (Point p : poly) {
            // åªéœ€è®¡ç®—旋转后的Y坐标
            double rotatedY = p.x * sin + p.y * cos;
            if (rotatedY < minY) minY = rotatedY;
            if (rotatedY > maxY) maxY = rotatedY;
        }
        return maxY - minY;
    }
    private static List<PathSegment> generateClippedMowingLines(List<Point> polygon, double angle, double width) {
        List<PathSegment> segments = new ArrayList<>();
        // æ—‹è½¬è‡³æ°´å¹³
        // æ—‹è½¬å¤šè¾¹å½¢è‡³æ°´å¹³
        List<Point> rotatedPoly = rotatePolygon(polygon, -angle);
        double minY = Double.MAX_VALUE;
@@ -162,28 +201,27 @@
            if (p.y > maxY) maxY = p.y;
        }
        // ä¼˜åŒ–:起始扫描线增加一个微小的偏移量 EPSILON
        // é¿å…æ‰«æçº¿æ­£å¥½è½åœ¨é¡¶ç‚¹ä¸Šï¼Œå¯¼è‡´äº¤ç‚¹åˆ¤æ–­é€»è¾‘出现二义性
        // èµ·å§‹æ‰«æçº¿ä½ç½®ï¼š
        // ä»Ž minY + width/2 å¼€å§‹ï¼Œå› ä¸ºä¹‹å‰å·²ç»èµ°äº†è½®å»“线(Contour Pass)。
        // è½®å»“线负责清理边缘区域,内部填充线保持 width çš„间距即可。
        // åŠ ä¸Š EPSILON é˜²æ­¢æµ®ç‚¹æ•°åˆšå¥½è½åœ¨è¾¹ç•Œä¸Šå¯¼è‡´çš„判断误差
        double currentY = minY + width / 2.0 + EPSILON;
        while (currentY < maxY) {
            List<Double> xIntersections = new ArrayList<>();
            int n = rotatedPoly.size();
            for (int i = 0; i < n; i++) {
                Point p1 = rotatedPoly.get(i);
                Point p2 = rotatedPoly.get((i + 1) % n);
                // ä¼˜åŒ–:忽略水平线段 (p1.y == p2.y),避免除零错误,水平线段不参与垂直扫描线求交
                // å¿½ç•¥æ°´å¹³çº¿æ®µ
                if (Math.abs(p1.y - p2.y) < EPSILON) continue;
                // åˆ¤æ–­çº¿æ®µæ˜¯å¦è·¨è¶Š currentY
                // ä½¿ç”¨ä¸¥æ ¼çš„不等式逻辑配合范围判断
                double minP = Math.min(p1.y, p2.y);
                double maxP = Math.max(p1.y, p2.y);
                if (currentY >= minP && currentY < maxP) {
                    // çº¿æ€§æ’值求X
                    double x = p1.x + (currentY - p1.y) * (p2.x - p1.x) / (p2.y - p1.y);
                    xIntersections.add(x);
                }
@@ -191,20 +229,19 @@
            Collections.sort(xIntersections);
            // æå–线段,通常是成对出现
            if (xIntersections.size() >= 2) {
                // å–最左和最右点连接(应对可能出现的微小计算误差导致的多个交点)
                // å–最左和最右交点
                double xStart = xIntersections.get(0);
                double xEnd = xIntersections.get(xIntersections.size() - 1);
                // åªæœ‰å½“线段长度大于极小值时才添加,避免生成噪点路径
                if (xEnd - xStart > EPSILON) {
                    Point rStart = rotatePoint(new Point(xStart, currentY), angle); // è¿™é‡Œçš„currentY实际上带了epsilon,还原时没问题
                    // åå‘旋转回原坐标系
                    Point rStart = rotatePoint(new Point(xStart, currentY), angle);
                    Point rEnd = rotatePoint(new Point(xEnd, currentY), angle);
                    segments.add(new PathSegment(rStart, rEnd, true));
                }
            }
            // æ­¥è¿›
            currentY += width;
        }
@@ -231,7 +268,6 @@
            // æ·»åŠ è¿‡æ¸¡æ®µ
            if (i > 0) {
                Point prevEnd = result.get(result.size() - 1).end;
                // åªæœ‰å½“距离确实存在时才添加过渡段(避免重合点)
                if (distanceSq(prevEnd, actualStart) > EPSILON) {
                    result.add(new PathSegment(prevEnd, actualStart, false));
                }
@@ -242,12 +278,12 @@
        return result;
    }
    // ================= åŸºç¡€å‡ ä½•工具 (优化版) =================
    // ================= åŸºç¡€å‡ ä½•工具 =================
    private static List<Point> parseCoords(String s) {
        List<Point> list = new ArrayList<>();
        if (s == null || s.trim().isEmpty()) return list;
        String[] parts = s.split(";");
        for (String part : parts) {
            String[] xy = part.split(",");
@@ -257,14 +293,13 @@
                    double y = Double.parseDouble(xy[1].trim());
                    list.add(new Point(x, y));
                } catch (NumberFormatException e) {
                    // å¿½ç•¥æ ¼å¼é”™è¯¯çš„单个点
                    // å¿½ç•¥æ ¼å¼é”™è¯¯
                }
            }
        }
        return list;
    }
    // Shoelace公式判断方向并调整
    private static void ensureCCW(List<Point> polygon) {
        double sum = 0;
        for (int i = 0; i < polygon.size(); i++) {
@@ -272,14 +307,33 @@
            Point p2 = polygon.get((i + 1) % polygon.size());
            sum += (p2.x - p1.x) * (p2.y + p1.y);
        }
        // å‡è®¾æ ‡å‡†ç¬›å¡å°”坐标系,sum > 0 ä¸ºé¡ºæ—¶é’ˆï¼Œéœ€è¦åè½¬ä¸ºé€†æ—¶é’ˆ
        if (sum > 0) {
            Collections.reverse(polygon);
        }
    }
    private static List<Point> shrinkPolygon(List<Point> polygon, double margin) {
        List<Point> newPoints = new ArrayList<>();
        int n = polygon.size();
        for (int i = 0; i < n; i++) {
            Point p1 = polygon.get(i);
            Point p2 = polygon.get((i + 1) % n);
            Point p0 = polygon.get((i - 1 + n) % n);
            Line line1 = offsetLine(p1, p2, margin);
            Line line0 = offsetLine(p0, p1, margin);
            Point intersection = getIntersection(line0, line1);
            if (intersection != null) {
                newPoints.add(intersection);
            }
        }
        return newPoints;
    }
    private static class Line {
        double a, b, c;
        double a, b, c;
        public Line(double a, double b, double c) { this.a = a; this.b = b; this.c = c; }
    }
@@ -287,13 +341,13 @@
        double dx = p2.x - p1.x;
        double dy = p2.y - p1.y;
        double len = Math.sqrt(dx * dx + dy * dy);
        // é˜²æ­¢é™¤ä»¥0
        if (len < EPSILON) return new Line(0, 0, 0);
        double nx = -dy / len;
        double ny = dx / len;
        // å‘左侧平移(假设逆时针)
        double newX = p1.x + nx * dist;
        double newY = p1.y + ny * dist;
@@ -305,7 +359,7 @@
    private static Point getIntersection(Line l1, Line l2) {
        double det = l1.a * l2.b - l2.a * l1.b;
        if (Math.abs(det) < EPSILON) return null; // å¹³è¡Œ
        if (Math.abs(det) < EPSILON) return null;
        double x = (l1.b * l2.c - l2.b * l1.c) / det;
        double y = (l2.a * l1.c - l1.a * l2.c) / det;
        return new Point(x, y);
@@ -324,7 +378,7 @@
        }
        return res;
    }
    private static double distanceSq(Point p1, Point p2) {
        return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y);
    }
src/lujing/MowingPathGenerationPage.java
@@ -540,9 +540,11 @@
                    // å¼‚形地块,无障碍物 -> è°ƒç”¨ YixinglujingNoObstacle
                    // æ³¨æ„ï¼šå¦‚果该类还没有实现,这里会抛出异常或返回null
                    try {
                        // å‡è®¾ YixinglujingNoObstacle æœ‰ç±»ä¼¼çš„æ–¹æ³•签名
                        // å¦‚果类还没有实现,可能需要使用原来的方法作为后备
                        generated = YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr);
                        // è°ƒç”¨ YixinglujingNoObstacle.planPath èŽ·å–è·¯å¾„æ®µåˆ—è¡¨
                        List<YixinglujingNoObstacle.PathSegment> segments =
                            YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr);
                        // æ ¼å¼åŒ–路径段列表为字符串
                        generated = formatYixingPathSegments(segments);
                    } catch (Exception e) {
                        // å¦‚果类还没有实现,使用原来的方法作为后备
                        if (showMessages) {
@@ -711,6 +713,30 @@
    }
    
    /**
     * æ ¼å¼åŒ– YixinglujingNoObstacle.PathSegment åˆ—表为坐标字符串
     */
    private String formatYixingPathSegments(List<YixinglujingNoObstacle.PathSegment> segments) {
        if (segments == null || segments.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        YixinglujingNoObstacle.Point last = null;
        for (YixinglujingNoObstacle.PathSegment segment : segments) {
            // åªæ·»åŠ å‰²è‰å·¥ä½œæ®µï¼Œè·³è¿‡è¿‡æ¸¡æ®µ
            if (segment.isMowing) {
                // å¦‚果起点与上一个终点不同,添加起点
                if (last == null || !equalsYixingPoint(last, segment.start)) {
                    appendYixingPoint(sb, segment.start);
                }
                // æ·»åŠ ç»ˆç‚¹
                appendYixingPoint(sb, segment.end);
                last = segment.end;
            }
        }
        return sb.toString();
    }
    /**
     * æ¯”较两个点是否相同(使用小的容差)
     */
    private boolean equals2D(AoxinglujingNoObstacle.Point p1, AoxinglujingNoObstacle.Point p2) {
@@ -731,6 +757,27 @@
        sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y));
    }
    
    /**
     * æ¯”较两个 YixinglujingNoObstacle.Point æ˜¯å¦ç›¸åŒï¼ˆä½¿ç”¨å°çš„容差)
     */
    private boolean equalsYixingPoint(YixinglujingNoObstacle.Point p1, YixinglujingNoObstacle.Point p2) {
        if (p1 == null || p2 == null) {
            return p1 == p2;
        }
        double tolerance = 1e-6;
        return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance;
    }
    /**
     * æ·»åŠ  YixinglujingNoObstacle.Point åˆ°å­—符串构建器
     */
    private void appendYixingPoint(StringBuilder sb, YixinglujingNoObstacle.Point point) {
        if (sb.length() > 0) {
            sb.append(";");
        }
        sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y));
    }
    // ========== UI辅助方法 ==========
    
    private JTextArea createInfoTextArea(String text, boolean editable, int rows) {
src/lujing/ObstaclePathPlanner.java
@@ -538,3 +538,5 @@
src/lujing/YixinglujingNoObstacle.java
@@ -1,23 +1,454 @@
package lujing;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
 * æ— éšœç¢ç‰©å¼‚形地块路径规划类
 * å¼‚形(无障碍物)草地路径规划类 - ä¼˜åŒ–版 V2.0
 * * åŠŸèƒ½ç‰¹ç‚¹ï¼š
 * 1. è‡ªåŠ¨å¤„ç†å‡¹å¤šè¾¹å½¢ï¼ˆé€šè¿‡è€³åˆ‡æ³•åˆ†å‰²ï¼‰
 * 2. å¢žåŠ â€œå›´è¾¹â€è·¯å¾„ï¼Œä¿è¯è¾¹ç¼˜å‰²è‰æ•´æ´
 * 3. è‡ªåŠ¨è®¡ç®—æ¯ä¸ªå­åŒºåŸŸçš„æœ€ä¼˜æ‰«æè§’åº¦ï¼ˆå‡å°‘æŽ‰å¤´æ¬¡æ•°ï¼‰
 * 4. æ™ºèƒ½åŒºåŸŸè¿žæŽ¥ï¼ˆæ”¯æŒåŒå‘路径选择)
 */
public class YixinglujingNoObstacle {
    // ==========================================
    // å¯¹å¤–接口
    // ==========================================
    /**
     * ç”Ÿæˆè·¯å¾„
     * @param boundaryCoordsStr åœ°å—边界坐标字符串 "x1,y1;x2,y2;..."
     * @param mowingWidthStr å‰²è‰å®½åº¦å­—符串,如 "0.34"
     * @param safetyMarginStr å®‰å…¨è¾¹è·å­—符串,如 "0.2"
     * @return è·¯å¾„坐标字符串,格式 "x1,y1;x2,y2;..."
     * è§„划异形草地割草路径
     *
     * @param coordinates åœ°å—边界坐标字符串,格式:"x1,y1;x2,y2;x3,y3;..."
     * @param widthStr    å‰²è‰å®½åº¦ï¼ˆç±³ï¼‰ï¼Œå¦‚ "0.34"
     * @param marginStr   å®‰å…¨è¾¹è·ï¼ˆç±³ï¼‰ï¼Œå¦‚ "0.2"
     * @return è·¯å¾„段列表
     */
    public static String planPath(String boundaryCoordsStr, String mowingWidthStr, String safetyMarginStr) {
        // TODO: å®žçŽ°å¼‚å½¢åœ°å—æ— éšœç¢ç‰©è·¯å¾„è§„åˆ’ç®—æ³•
        // ç›®å‰ä½¿ç”¨é»˜è®¤æ–¹æ³•作为临时实现
        throw new UnsupportedOperationException("YixinglujingNoObstacle.planPath å°šæœªå®žçް");
    public static List<PathSegment> planPath(String coordinates, String widthStr, String marginStr) {
        // 1. å‚数解析与预处理
        List<Point> rawPoints = parseCoordinates(coordinates);
        if (rawPoints.size() < 3) {
            throw new IllegalArgumentException("多边形点数不足,无法构成地块");
        }
        // ç¡®ä¿é€†æ—¶é’ˆé¡ºåºï¼Œæ–¹ä¾¿åŽç»­å‡ ä½•计算
        ensureCounterClockwise(rawPoints);
        double mowWidth = Double.parseDouble(widthStr);
        double safeMargin = Double.parseDouble(marginStr);
        List<PathSegment> finalPath = new ArrayList<>();
        // 2. ç”Ÿæˆå›´è¾¹è·¯å¾„ (Contour Path)
        // è¿™ä¸€æ­¥å…ˆè§„划一圈轮廓,解决异形边缘难处理的问题
        List<Point> contourPoly = getInsetPolygon(rawPoints, safeMargin);
        // å¦‚果内缩后面积太小或点数不足,直接返回空
        if (contourPoly.size() < 3) {
            return new ArrayList<>();
        }
        // å°†å›´è¾¹è·¯å¾„加入结果
        for (int i = 0; i < contourPoly.size(); i++) {
            Point p1 = contourPoly.get(i);
            Point p2 = contourPoly.get((i + 1) % contourPoly.size());
            finalPath.add(new PathSegment(p1, p2, true)); // true = å‰²è‰
        }
        // è®°å½•围边结束后的位置(通常回到围边起点)
        Point endOfContour = contourPoly.get(0);
        // 3. åŒºåŸŸåˆ†å‰² (Decomposition)
        // ä½¿ç”¨è€³åˆ‡æ³•将围边后的多边形分割为多个凸多边形(三角形)
        // è¿™æ ·å¯ä»¥ä¿è¯è¦†ç›–无遗漏
        List<List<Point>> triangles = triangulatePolygon(contourPoly);
        // 4. å¯¹æ¯ä¸ªåŒºåŸŸç”Ÿæˆå†…部填充路径
        List<List<PathSegment>> allRegionPaths = new ArrayList<>();
        for (List<Point> triangle : triangles) {
            // ã€ä¼˜åŒ–】寻找最优扫描角度:
            // éåŽ†ä¸‰è§’å½¢çš„ä¸‰æ¡è¾¹ï¼Œè®¡ç®—ä»¥å“ªæ¡è¾¹ä¸ºåŸºå‡†æ‰«ææ—¶ï¼Œç”Ÿæˆçš„è¡Œæ•°æœ€å°‘ï¼ˆè½¬å¼¯æœ€å°‘ï¼‰
            List<PathSegment> regionPath = planConvexPathOptimal(triangle, mowWidth);
            if (!regionPath.isEmpty()) {
                allRegionPaths.add(regionPath);
            }
        }
        // 5. è¿žæŽ¥æ‰€æœ‰å†…部区域 (Greedy Connection)
        // ä»Žå›´è¾¹ç»“束点开始,寻找最近的下一个区域
        List<PathSegment> internalPaths = connectRegions(allRegionPaths, endOfContour);
        finalPath.addAll(internalPaths);
        return finalPath;
    }
}
    // ==========================================
    // æ ¸å¿ƒè§„划算法
    // ==========================================
    /**
     * è§„划凸多边形路径,自动选择最优角度
     */
    private static List<PathSegment> planConvexPathOptimal(List<Point> polygon, double width) {
        if (polygon.size() < 3) return new ArrayList<>();
        double bestAngle = 0;
        double minLines = Double.MAX_VALUE;
        // éåŽ†å¤šè¾¹å½¢çš„æ¯ä¸€æ¡è¾¹ï¼Œå°è¯•ä»¥è¯¥è¾¹è§’åº¦è¿›è¡Œæ‰«æ
        for (int i = 0; i < polygon.size(); i++) {
            Point p1 = polygon.get(i);
            Point p2 = polygon.get((i + 1) % polygon.size());
            // è®¡ç®—边的角度
            double angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
            // è®¡ç®—在这个角度下,多边形的垂直投影高度
            // é«˜åº¦è¶Šå°ï¼Œæ„å‘³ç€æ²¿æ­¤æ–¹å‘扫描的行数越少,效率越高
            double height = calculatePolygonHeight(polygon, -angle);
            if (height < minLines) {
                minLines = height;
                bestAngle = angle;
            }
        }
        // ä½¿ç”¨æœ€ä½³è§’度生成路径
        return generatePathWithAngle(polygon, width, bestAngle);
    }
    /**
     * æ ¹æ®æŒ‡å®šè§’度生成弓字形路径
     */
    private static List<PathSegment> generatePathWithAngle(List<Point> polygon, double width, double angle) {
        // 1. å°†å¤šè¾¹å½¢æ—‹è½¬åˆ°æ°´å¹³ä½ç½®
        List<Point> rotatedPoints = new ArrayList<>();
        for (Point p : polygon) {
            rotatedPoints.add(rotatePoint(p, -angle));
        }
        // 2. è®¡ç®—Y轴范围
        double minY = Double.MAX_VALUE;
        double maxY = -Double.MAX_VALUE;
        for (Point p : rotatedPoints) {
            minY = Math.min(minY, p.y);
            maxY = Math.max(maxY, p.y);
        }
        List<PathSegment> segments = new ArrayList<>();
        boolean leftToRight = true;
        // 3. æ‰«æçº¿ç”Ÿæˆ (从 minY + width/2 å¼€å§‹ï¼Œä¿è¯ç¬¬ä¸€åˆ€åˆ‡åœ¨å¤šè¾¹å½¢å†…)
        for (double y = minY + width / 2; y <= maxY; y += width) {
            List<Double> intersections = new ArrayList<>();
            for (int i = 0; i < rotatedPoints.size(); i++) {
                Point p1 = rotatedPoints.get(i);
                Point p2 = rotatedPoints.get((i + 1) % rotatedPoints.size());
                // åˆ¤æ–­æ‰«æçº¿æ˜¯å¦ç©¿è¿‡è¾¹
                if ((p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y)) {
                    double x = p1.x + (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y);
                    intersections.add(x);
                }
            }
            Collections.sort(intersections);
            // æˆå¯¹ç”Ÿæˆçº¿æ®µ
            for (int k = 0; k < intersections.size() - 1; k += 2) {
                double x1 = leftToRight ? intersections.get(k) : intersections.get(k + 1);
                double x2 = leftToRight ? intersections.get(k + 1) : intersections.get(k);
                Point start = new Point(x1, y);
                Point end = new Point(x2, y);
                // æ—‹è½¬å›žåŽŸå§‹åæ ‡ç³»
                Point originalStart = rotatePoint(start, angle);
                Point originalEnd = rotatePoint(end, angle);
                // è¿žæŽ¥é€»è¾‘:如果不是第一段,需要从上一段终点连过来
                if (!segments.isEmpty()) {
                    PathSegment prev = segments.get(segments.size() - 1);
                    // æ·»åŠ è¿žæŽ¥çº¿ï¼ˆé€šå¸¸ç®—ä½œå‰²è‰è·¯å¾„çš„ä¸€éƒ¨åˆ†ï¼Œä¿æŒå¼“å­—å½¢è¿žç»­ï¼‰
                    segments.add(new PathSegment(prev.end, originalStart, true));
                }
                segments.add(new PathSegment(originalStart, originalEnd, true));
            }
            leftToRight = !leftToRight; // æ¢å‘
        }
        return segments;
    }
    /**
     * è¿žæŽ¥æ‰€æœ‰åˆ†å‰²åŽçš„区域 (贪心策略 + åŒå‘优化)
     */
    private static List<PathSegment> connectRegions(List<List<PathSegment>> regions, Point startPoint) {
        List<PathSegment> result = new ArrayList<>();
        if (regions.isEmpty()) return result;
        List<List<PathSegment>> remaining = new ArrayList<>(regions);
        Point currentPos = startPoint;
        while (!remaining.isEmpty()) {
            int bestIndex = -1;
            double minDist = Double.MAX_VALUE;
            boolean needReverse = false;
            // å¯»æ‰¾ç¦»å½“前位置最近的区域起点或终点
            for (int i = 0; i < remaining.size(); i++) {
                List<PathSegment> region = remaining.get(i);
                Point pStart = region.get(0).start;
                Point pEnd = region.get(region.size() - 1).end;
                double dStart = distance(currentPos, pStart);
                double dEnd = distance(currentPos, pEnd);
                // æ£€æŸ¥æ­£å‘进入
                if (dStart < minDist) {
                    minDist = dStart;
                    bestIndex = i;
                    needReverse = false;
                }
                // æ£€æŸ¥åå‘进入(倒着割草如果更近)
                if (dEnd < minDist) {
                    minDist = dEnd;
                    bestIndex = i;
                    needReverse = true;
                }
            }
            if (bestIndex != -1) {
                List<PathSegment> targetRegion = remaining.remove(bestIndex);
                if (needReverse) {
                    // åè½¬è¯¥åŒºåŸŸçš„æ‰€æœ‰è·¯å¾„
                    List<PathSegment> reversedRegion = new ArrayList<>();
                    for (int k = targetRegion.size() - 1; k >= 0; k--) {
                        PathSegment seg = targetRegion.get(k);
                        // äº¤æ¢èµ·ç‚¹ç»ˆç‚¹
                        reversedRegion.add(new PathSegment(seg.end, seg.start, seg.isMowing));
                    }
                    targetRegion = reversedRegion;
                }
                // æ·»åŠ è¿‡æ¸¡è·¯å¾„ï¼ˆæŠ¬åˆ€ç§»åŠ¨ï¼ŒisMowing=false)
                Point nextStart = targetRegion.get(0).start;
                // åªæœ‰è·ç¦»æ˜¾è‘—才添加移动段
                if (distance(currentPos, nextStart) > 0.01) {
                    result.add(new PathSegment(currentPos, nextStart, false));
                }
                result.addAll(targetRegion);
                currentPos = targetRegion.get(targetRegion.size() - 1).end;
            } else {
                break; // é˜²å¾¡æ€§ä»£ç 
            }
        }
        return result;
    }
    // ==========================================
    // å‡ ä½•运算辅助方法
    // ==========================================
    /**
     * å†…缩多边形 (基于角平分线)
     */
    private static List<Point> getInsetPolygon(List<Point> points, double margin) {
        List<Point> result = new ArrayList<>();
        int n = points.size();
        for (int i = 0; i < n; i++) {
            Point pPrev = points.get((i - 1 + n) % n);
            Point pCurr = points.get(i);
            Point pNext = points.get((i + 1) % n);
            Point v1 = new Point(pCurr.x - pPrev.x, pCurr.y - pPrev.y);
            Point v2 = new Point(pNext.x - pCurr.x, pNext.y - pCurr.y);
            double len1 = Math.hypot(v1.x, v1.y);
            double len2 = Math.hypot(v2.x, v2.y);
            if (len1 < 1e-6 || len2 < 1e-6) continue;
            // å½’一化向量
            Point n1 = new Point(v1.x / len1, v1.y / len1);
            Point n2 = new Point(v2.x / len2, v2.y / len2);
            // è®¡ç®—平分线方向
            // v1反向 + v2
            Point bisector = new Point(-n1.x + n2.x, -n1.y + n2.y);
            double biLen = Math.hypot(bisector.x, bisector.y);
            // è®¡ç®—半角 sin(theta/2)
            double cross = n1.x * n2.y - n1.y * n2.x; // å‰ç§¯åˆ¤æ–­è½¬å‘
            // é»˜è®¤å‘左侧内缩 (CCW多边形)
            if (biLen < 1e-6) {
                // å…±çº¿ï¼Œæ²¿æ³•线方向
                bisector = new Point(-n1.y, n1.x);
            } else {
                bisector.x /= biLen;
                bisector.y /= biLen;
            }
            // è®¡ç®—偏移距离
            double dot = n1.x * n2.x + n1.y * n2.y;
            double angle = Math.acos(Math.max(-1, Math.min(1, dot)));
            double dist = margin / Math.sin(angle / 2.0);
            // æ–¹å‘修正:确保平分线指向多边形内部(逆时针多边形的左侧)
            Point leftNormal = new Point(-n1.y, n1.x);
            if (bisector.x * leftNormal.x + bisector.y * leftNormal.y < 0) {
                 bisector.x = -bisector.x;
                 bisector.y = -bisector.y;
            }
            // å¦‚果是凹角(cross < 0),平分线指向外部,距离需要反转或者特殊处理
            // ç®€å•处理:对于凹角,偏移点实际上会远离原点,上述逻辑通常能覆盖,
            // ä½†æžç«¯é”è§’可能导致dist过大。此处做简单截断保护是不够的,
            // ä½†é’ˆå¯¹ä¸€èˆ¬è‰åœ°å½¢çŠ¶ï¼Œæ­¤é€»è¾‘å¯ç”¨ã€‚
            result.add(new Point(pCurr.x + bisector.x * dist, pCurr.y + bisector.y * dist));
        }
        return result;
    }
    /**
     * è€³åˆ‡æ³•分割多边形
     */
    private static List<List<Point>> triangulatePolygon(List<Point> poly) {
        List<List<Point>> triangles = new ArrayList<>();
        List<Point> remaining = new ArrayList<>(poly);
        int maxIter = remaining.size() * 3;
        int iter = 0;
        while (remaining.size() > 3 && iter++ < maxIter) {
            int n = remaining.size();
            boolean earFound = false;
            for (int i = 0; i < n; i++) {
                Point prev = remaining.get((i - 1 + n) % n);
                Point curr = remaining.get(i);
                Point next = remaining.get((i + 1) % n);
                if (isConvex(prev, curr, next)) {
                    boolean hasPoint = false;
                    for (int j = 0; j < n; j++) {
                        if (j == i || j == (i - 1 + n) % n || j == (i + 1) % n) continue;
                        if (isPointInTriangle(remaining.get(j), prev, curr, next)) {
                            hasPoint = true;
                            break;
                        }
                    }
                    if (!hasPoint) {
                        List<Point> tri = new ArrayList<>();
                        tri.add(prev); tri.add(curr); tri.add(next);
                        triangles.add(tri);
                        remaining.remove(i);
                        earFound = true;
                        break;
                    }
                }
            }
            if (!earFound) break;
        }
        if (remaining.size() == 3) {
            triangles.add(remaining);
        }
        return triangles;
    }
    private static double calculatePolygonHeight(List<Point> poly, double angle) {
        double minY = Double.MAX_VALUE;
        double maxY = -Double.MAX_VALUE;
        for (Point p : poly) {
            Point r = rotatePoint(p, angle);
            minY = Math.min(minY, r.y);
            maxY = Math.max(maxY, r.y);
        }
        return maxY - minY;
    }
    private static Point rotatePoint(Point p, double angle) {
        double cos = Math.cos(angle);
        double sin = Math.sin(angle);
        return new Point(p.x * cos - p.y * sin, p.x * sin + p.y * cos);
    }
    private static boolean isConvex(Point a, Point b, Point c) {
        return (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x) >= 0;
    }
    private static boolean isPointInTriangle(Point p, Point a, Point b, Point c) {
        double areaABC = Math.abs((a.x*(b.y-c.y) + b.x*(c.y-a.y) + c.x*(a.y-b.y))/2.0);
        double areaPBC = Math.abs((p.x*(b.y-c.y) + b.x*(c.y-p.y) + c.x*(p.y-b.y))/2.0);
        double areaPAC = Math.abs((a.x*(p.y-c.y) + p.x*(c.y-a.y) + c.x*(a.y-p.y))/2.0);
        double areaPAB = Math.abs((a.x*(b.y-p.y) + b.x*(p.y-a.y) + p.x*(a.y-b.y))/2.0);
        return Math.abs(areaABC - (areaPBC + areaPAC + areaPAB)) < 1e-6;
    }
    private static List<Point> parseCoordinates(String coordinates) {
        List<Point> points = new ArrayList<>();
        String cleanStr = coordinates.replaceAll("[()\\[\\]{}]", "").trim();
        String[] pairs = cleanStr.split(";");
        for (String pair : pairs) {
            pair = pair.trim();
            if (pair.isEmpty()) continue;
            String[] xy = pair.split(",");
            if (xy.length == 2) {
                points.add(new Point(Double.parseDouble(xy[0].trim()), Double.parseDouble(xy[1].trim())));
            }
        }
        return points;
    }
    private static void ensureCounterClockwise(List<Point> points) {
        double sum = 0;
        for (int i = 0; i < points.size(); i++) {
            Point p1 = points.get(i);
            Point p2 = points.get((i + 1) % points.size());
            sum += (p2.x - p1.x) * (p2.y + p1.y);
        }
        if (sum > 0) {
            Collections.reverse(points);
        }
    }
    private static double distance(Point p1, Point p2) {
        return Math.hypot(p1.x - p2.x, p1.y - p2.y);
    }
    // ==========================================
    // å†…部数据结构
    // ==========================================
    public static class Point {
        public double x, y;
        public Point(double x, double y) { this.x = x; this.y = y; }
        @Override public String toString() { return String.format("%.2f,%.2f", x, y); }
    }
    public static class PathSegment {
        public Point start;
        public Point end;
        public boolean isMowing;
        public PathSegment(Point start, Point end, boolean isMowing) {
            this.start = start;
            this.end = end;
            this.isMowing = isMowing;
        }
        @Override
        public String toString() {
            return String.format("[%s -> %s, å‰²è‰:%b]", start, end, isMowing);
        }
    }
}
src/zhangaiwu/AddDikuai.java
@@ -103,6 +103,7 @@
        final boolean success;
        final String errorMessage;
        final String originalBoundary;
        final String originalBoundaryXY;
        final String optimizedBoundary;
        final String areaSqMeters;
        final String baseStationCoordinates;
@@ -111,6 +112,7 @@
        private BoundarySnapshotResult(boolean success,
                                       String errorMessage,
                                       String originalBoundary,
                                       String originalBoundaryXY,
                                       String optimizedBoundary,
                                       String areaSqMeters,
                                       String baseStationCoordinates,
@@ -118,18 +120,19 @@
            this.success = success;
            this.errorMessage = errorMessage;
            this.originalBoundary = originalBoundary;
            this.originalBoundaryXY = originalBoundaryXY;
            this.optimizedBoundary = optimizedBoundary;
            this.areaSqMeters = areaSqMeters;
            this.baseStationCoordinates = baseStationCoordinates;
            this.messageType = messageType;
        }
        static BoundarySnapshotResult success(String original, String optimized, String area, String baseStation) {
            return new BoundarySnapshotResult(true, null, original, optimized, area, baseStation, JOptionPane.INFORMATION_MESSAGE);
        static BoundarySnapshotResult success(String original, String originalXY, String optimized, String area, String baseStation) {
            return new BoundarySnapshotResult(true, null, original, originalXY, optimized, area, baseStation, JOptionPane.INFORMATION_MESSAGE);
        }
        static BoundarySnapshotResult failure(String message, int messageType) {
            return new BoundarySnapshotResult(false, message, null, null, null, null, messageType);
            return new BoundarySnapshotResult(false, message, null, null, null, null, null, messageType);
        }
    }
@@ -1675,6 +1678,7 @@
        Dikuai dikuai = getOrCreatePendingDikuai();
        if (dikuai != null) {
            dikuai.setBoundaryOriginalCoordinates(snapshot.originalBoundary);
            dikuai.setBoundaryOriginalXY(snapshot.originalBoundaryXY);
            dikuai.setBoundaryCoordinates(snapshot.optimizedBoundary);
            dikuai.setLandArea(snapshot.areaSqMeters);
            dikuai.setBaseStationCoordinates(snapshot.baseStationCoordinates);
@@ -1683,11 +1687,13 @@
        }
        dikuaiData.put("boundaryOriginalCoordinates", snapshot.originalBoundary);
        dikuaiData.put("boundaryOriginalXY", snapshot.originalBoundaryXY);
        dikuaiData.put("boundaryCoordinates", snapshot.optimizedBoundary);
        dikuaiData.put("landArea", snapshot.areaSqMeters);
        dikuaiData.put("baseStationCoordinates", snapshot.baseStationCoordinates);
        if (activeSession != null) {
            activeSession.data.put("boundaryOriginalCoordinates", snapshot.originalBoundary);
            activeSession.data.put("boundaryOriginalXY", snapshot.originalBoundaryXY);
            activeSession.data.put("boundaryCoordinates", snapshot.optimizedBoundary);
            activeSession.data.put("landArea", snapshot.areaSqMeters);
            activeSession.data.put("baseStationCoordinates", snapshot.baseStationCoordinates);
@@ -1743,6 +1749,47 @@
        }
        return sb.toString();
    }
    /**
     * æž„建原始边界XY坐标字符串(相对于基准站的XY坐标)
     */
    private static String buildOriginalBoundaryXYString(List<Coordinate> coordinates, String baseStationCoordinates) {
        if (coordinates == null || coordinates.isEmpty()) {
            return "-1";
        }
        if (baseStationCoordinates == null || baseStationCoordinates.trim().isEmpty() || "-1".equals(baseStationCoordinates.trim())) {
            return "-1";
        }
        try {
            // è§£æžåŸºå‡†ç«™åæ ‡
            String[] baseParts = baseStationCoordinates.split(",");
            if (baseParts.length < 4) {
                return "-1";
            }
            double baseLat = publicway.Gpstoxuzuobiao.parseDMToDecimal(baseParts[0], baseParts[1]);
            double baseLon = publicway.Gpstoxuzuobiao.parseDMToDecimal(baseParts[2], baseParts[3]);
            StringBuilder sb = new StringBuilder();
            DecimalFormat xyFormat = new DecimalFormat("0.00");
            for (Coordinate coord : coordinates) {
                double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection());
                double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection());
                // è½¬æ¢ä¸ºXY坐标
                double[] xy = publicway.Gpstoxuzuobiao.convertLatLonToLocal(lat, lon, baseLat, baseLon);
                if (sb.length() > 0) {
                    sb.append(";");
                }
                sb.append(xyFormat.format(xy[0])).append(",").append(xyFormat.format(xy[1]));
            }
            return sb.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return "-1";
        }
    }
    private static double convertToDecimalDegree(String dmm, String direction) {
        if (dmm == null || dmm.isEmpty()) {
@@ -1865,10 +1912,11 @@
        }
    String originalBoundary = buildOriginalBoundaryString(uniqueCoordinates);
        String originalBoundaryXY = buildOriginalBoundaryXYString(uniqueCoordinates, baseStationCoordinates);
        DecimalFormat areaFormat = new DecimalFormat("0.00");
        String areaString = areaFormat.format(area);
        return BoundarySnapshotResult.success(originalBoundary, optimizedBoundary, areaString, baseStationCoordinates);
        return BoundarySnapshotResult.success(originalBoundary, originalBoundaryXY, optimizedBoundary, areaString, baseStationCoordinates);
    }
    private String resolvePlannerMode(String patternDisplay) {
@@ -2407,6 +2455,7 @@
        BoundarySnapshotResult snapshot = computeBoundarySnapshot();
        if (snapshot.success && activeSession != null) {
            activeSession.data.put("boundaryOriginalCoordinates", snapshot.originalBoundary);
            activeSession.data.put("boundaryOriginalXY", snapshot.originalBoundaryXY);
            activeSession.data.put("boundaryCoordinates", snapshot.optimizedBoundary);
            activeSession.data.put("landArea", snapshot.areaSqMeters);
            activeSession.data.put("baseStationCoordinates", snapshot.baseStationCoordinates);
@@ -2466,6 +2515,9 @@
        if (dikuaiData.containsKey("boundaryOriginalCoordinates")) {
            dikuai.setBoundaryOriginalCoordinates(dikuaiData.get("boundaryOriginalCoordinates"));
        }
        if (dikuaiData.containsKey("boundaryOriginalXY")) {
            dikuai.setBoundaryOriginalXY(dikuaiData.get("boundaryOriginalXY"));
        }
        if (dikuaiData.containsKey("boundaryCoordinates")) {
            dikuai.setBoundaryCoordinates(dikuaiData.get("boundaryCoordinates"));
        }
src/zhuye/MapRenderer.java
@@ -110,6 +110,9 @@
    private WangfanDraw returnPathDrawer;  // å¾€è¿”路径绘制管理器
    private List<Point2D.Double> currentReturnPath; // å½“前地块的往返路径(用于显示)
    private List<Point2D.Double> previewReturnPath; // é¢„览的往返路径
    private List<Point2D.Double> previewOriginalBoundary; // é¢„览的原始边界(紫色)
    private List<Point2D.Double> previewOptimizedBoundary; // é¢„览的优化后边界
    private boolean boundaryPreviewActive; // æ˜¯å¦å¤„于边界预览模式
    private static final double TRACK_SAMPLE_MIN_DISTANCE_METERS = 0.2d;
    private static final double TRACK_DUPLICATE_TOLERANCE_METERS = 1e-3d;
@@ -397,6 +400,11 @@
        if (hasBoundary) {
            drawCurrentBoundary(g2d);
        }
        // ç»˜åˆ¶è¾¹ç•Œé¢„览(原始边界-紫色,优化后边界)
        if (boundaryPreviewActive) {
            drawBoundaryPreview(g2d);
        }
        yulanzhangaiwu.renderPreview(g2d, scale);
@@ -3073,5 +3081,68 @@
        }
        return new ArrayList<>();
    }
    /**
     * è®¾ç½®è¾¹ç•Œé¢„览数据(原始边界和优化后边界)
     */
    public void setBoundaryPreview(String originalBoundaryXY, String optimizedBoundary) {
        if (originalBoundaryXY != null && !originalBoundaryXY.trim().isEmpty() && !"-1".equals(originalBoundaryXY.trim())) {
            previewOriginalBoundary = parseBoundary(originalBoundaryXY.trim());
        } else {
            previewOriginalBoundary = null;
        }
        if (optimizedBoundary != null && !optimizedBoundary.trim().isEmpty() && !"-1".equals(optimizedBoundary.trim())) {
            previewOptimizedBoundary = parseBoundary(optimizedBoundary.trim());
        } else {
            previewOptimizedBoundary = null;
        }
        boundaryPreviewActive = (previewOriginalBoundary != null && previewOriginalBoundary.size() >= 2) ||
                               (previewOptimizedBoundary != null && previewOptimizedBoundary.size() >= 2);
        if (boundaryPreviewActive) {
            // è®¡ç®—预览边界的边界框并调整视图
            List<Point2D.Double> allPoints = new ArrayList<>();
            if (previewOriginalBoundary != null) allPoints.addAll(previewOriginalBoundary);
            if (previewOptimizedBoundary != null) allPoints.addAll(previewOptimizedBoundary);
            if (!allPoints.isEmpty()) {
                Rectangle2D.Double bounds = computeBounds(allPoints);
                SwingUtilities.invokeLater(() -> {
                    fitBoundsToView(bounds);
                    visualizationPanel.repaint();
                });
            }
        } else {
            visualizationPanel.repaint();
        }
    }
    /**
     * æ¸…除边界预览
     */
    public void clearBoundaryPreview() {
        previewOriginalBoundary = null;
        previewOptimizedBoundary = null;
        boundaryPreviewActive = false;
        visualizationPanel.repaint();
    }
    /**
     * ç»˜åˆ¶è¾¹ç•Œé¢„览(原始边界-紫色,优化后边界-绿色)
     */
    private void drawBoundaryPreview(Graphics2D g2d) {
        // ç»˜åˆ¶åŽŸå§‹è¾¹ç•Œï¼ˆç´«è‰²ï¼‰
        if (previewOriginalBoundary != null && previewOriginalBoundary.size() >= 2) {
            Color purpleFill = new Color(128, 0, 128, 80); // ç´«è‰²åŠé€æ˜Žå¡«å……
            Color purpleBorder = new Color(128, 0, 128, 255); // ç´«è‰²è¾¹æ¡†
            bianjiedrwa.drawBoundary(g2d, previewOriginalBoundary, scale, purpleFill, purpleBorder);
        }
        // ç»˜åˆ¶ä¼˜åŒ–后边界(绿色,与正常边界颜色一致)
        if (previewOptimizedBoundary != null && previewOptimizedBoundary.size() >= 2) {
            bianjiedrwa.drawBoundary(g2d, previewOptimizedBoundary, scale, GRASS_FILL_COLOR, GRASS_BORDER_COLOR);
        }
    }
}
src/zhuye/Shouye.java
@@ -4296,6 +4296,90 @@
            pathPreviewReturnAction.run();
        }
    }
    /**
     * æ˜¾ç¤ºè¾¹ç•Œé¢„览(原始边界-紫色,优化后边界-绿色)
     * @param dikuai åœ°å—对象
     * @param optimizedBoundary ä¼˜åŒ–后的边界坐标字符串
     * @param returnCallback è¿”回回调
     */
    public static void showBoundaryPreview(dikuai.Dikuai dikuai, String optimizedBoundary, Runnable returnCallback) {
        Shouye shouye = getInstance();
        if (shouye == null || shouye.mapRenderer == null || dikuai == null) {
            return;
        }
        // èŽ·å–åŽŸå§‹è¾¹ç•ŒXY坐标
        String originalBoundaryXY = dikuai.getBoundaryOriginalXY();
        // è®¾ç½®è¾¹ç•Œé¢„览
        shouye.mapRenderer.setBoundaryPreview(originalBoundaryXY, optimizedBoundary);
        // è®¾ç½®è¿”回回调
        shouye.pathPreviewReturnAction = returnCallback;
        shouye.pathPreviewActive = true;
        // ç¡®ä¿æ‚¬æµ®æŒ‰é’®åŸºç¡€è®¾æ–½å·²åˆ›å»º
        shouye.ensureFloatingButtonInfrastructure();
        // åˆ›å»ºæˆ–显示返回按钮
        if (shouye.pathPreviewReturnButton == null) {
            shouye.pathPreviewReturnButton = publicway.Fanhuibutton.createReturnButton(e -> shouye.handleBoundaryPreviewReturn());
            shouye.pathPreviewReturnButton.setToolTipText("返回边界编辑页面");
        }
        // éšè—å…¶ä»–悬浮按钮
        shouye.hideFloatingDrawingControls();
        // æ˜¾ç¤ºè¿”回按钮
        shouye.pathPreviewReturnButton.setVisible(true);
        if (shouye.floatingButtonPanel != null) {
            shouye.floatingButtonPanel.setVisible(true);
            if (shouye.floatingButtonPanel.getParent() != shouye.visualizationPanel) {
                shouye.visualizationPanel.add(shouye.floatingButtonPanel, BorderLayout.SOUTH);
            }
        }
        shouye.rebuildFloatingButtonColumn();
        shouye.visualizationPanel.revalidate();
        shouye.visualizationPanel.repaint();
    }
    /**
     * å¤„理边界预览返回
     */
    private void handleBoundaryPreviewReturn() {
        Runnable callback = pathPreviewReturnAction;
        exitBoundaryPreview();
        if (callback != null) {
            callback.run();
        }
    }
    /**
     * é€€å‡ºè¾¹ç•Œé¢„览
     */
    private void exitBoundaryPreview() {
        pathPreviewActive = false;
        // æ¸…除边界预览
        if (mapRenderer != null) {
            mapRenderer.clearBoundaryPreview();
        }
        // éšè—è¿”回按钮
        if (pathPreviewReturnButton != null) {
            pathPreviewReturnButton.setVisible(false);
        }
        // éšè—æ‚¬æµ®é¢æ¿
        if (floatingButtonPanel != null) {
            floatingButtonPanel.setVisible(false);
        }
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
    }
    // æµ‹è¯•方法
    public static void main(String[] args) {
src/zhuye/ShouyeLauncher.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package zhuye;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import udpdell.UDPServer;
public class ShouyeLauncher {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            System.out.println("Starting Shouye via Launcher...");
            JFrame frame = new JFrame("AutoMow - é¦–页");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(400, 800);
            frame.setLocationRelativeTo(null);
            Shouye shouye = new Shouye();
            frame.add(shouye);
            frame.setVisible(true);
            UDPServer.startAsync();
        });
    }
}
src/zhuye/bianjiedrwa.java
@@ -23,7 +23,7 @@
            return; // æ— æ•°æ®è¿”回
        } // if结束
        float strokeWidth = (float) (3 / Math.max(0.5, scale)); // è®¡ç®—边线宽度
        float strokeWidth = (float) (6 / Math.max(0.5, scale)); // è®¡ç®—边线宽度(增加一倍,从3改为6)
        
        // å¡«å……区域
        Path2D fillPath = new Path2D.Double(); // åˆ›å»ºå¡«å……路径
@@ -56,9 +56,26 @@
                borderPath.lineTo(firstPoint.x, firstPoint.y); // è¿žæŽ¥åˆ°èµ·ç‚¹å½¢æˆé—­åˆ
            }
            
            // ç»˜åˆ¶å¤–层实线边界(宽度增加一倍)
            g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // è®¾ç½®å®žçº¿æè¾¹
            g2d.setColor(borderColor); // è®¾ç½®è¾¹çº¿é¢œè‰²
            g2d.draw(borderPath); // ç»˜åˆ¶å®Œæ•´è¾¹ç•Œï¼ˆåŒ…括起点到终点的连接)
            // åœ¨è¾¹ç•Œçº¿ä¸­é—´ç»˜åˆ¶æ·±ç°è‰²å°åœ†ç‚¹è™šçº¿
            float dashedLineWidth = strokeWidth / 3.0f; // è™šçº¿å®½åº¦ä¸ºå®žçº¿çš„三分之一
            float[] dashPattern = {2.0f, 2.0f}; // ç­‰é—´éš”小圆点虚线模式:2像素实线(形成圆点),2像素空白(均匀间隔)
            BasicStroke dashedStroke = new BasicStroke(
                dashedLineWidth,
                BasicStroke.CAP_ROUND, // ä½¿ç”¨åœ†å½¢ç«¯ç‚¹ï¼Œå½¢æˆåœ†ç‚¹æ•ˆæžœ
                BasicStroke.JOIN_ROUND,
                0.0f,
                dashPattern,
                0.0f
            );
            g2d.setStroke(dashedStroke); // è®¾ç½®è™šçº¿æè¾¹
            Color darkGrayColor = new Color(64, 64, 64); // æ·±ç°è‰²
            g2d.setColor(darkGrayColor); // è®¾ç½®è™šçº¿é¢œè‰²
            g2d.draw(borderPath); // åœ¨ä¸­é—´ç»˜åˆ¶å°åœ†ç‚¹è™šçº¿
        }
    } // æ–¹æ³•结束
} // ç±»ç»“束
src/zhuye/celiangmoshi.java
@@ -86,3 +86,5 @@