826220679@qq.com
2 天以前 48ee74129bb09a817a0bbbabe860c4007b74c66b
新增了往返路径
已添加6个文件
已修改24个文件
已重命名3个文件
2467 ■■■■■ 文件已修改
.vscode/settings.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dikuai.properties 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/baseStation/BaseStationDialog.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuai.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuaiguanli.java 220 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/FanhuiDialog.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Huizhiwanfanpath.java 896 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/ObstacleManagementPage.java 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Wangfanpathpage.java 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/addzhangaiwu.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/daohangyulan.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/gecaoji/Device.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/gecaoji/GecaojiMeg.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/MowingPathGenerationPage.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/publicway/Fuzhibutton.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/publicway/Gpstoxuzuobiao.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/publicway/Lookbutton.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/publicway/buttonset.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/set/Sets.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/set/debug.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/udpdell/UDPServer.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/yaokong/Control02.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhangaiwu/AddDikuai.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhangaiwu/yulanzhangaiwu.java 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/AreaSelectionDialog.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/LegendDialog.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 225 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/WangfanDraw.java 411 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/daohangyulan.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/zijian.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.vscode/settings.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
{
    "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx4G -Xms100m -Xlog:disable"
}
dikuai.properties
@@ -1,5 +1,5 @@
#Dikuai Properties
#Sat Dec 20 14:47:09 GMT+08:00 2025
#Sun Dec 21 11:15:33 GMT+08:00 2025
LAND2.boundaryCoordinates=5.38,-6.41;-11.15,-8.94;-12.75,-4.68;-12.23,-3.28;-11.12,-3.17;-9.29,-3.53;-7.46,-3.89;-5.62,-4.25;-3.79,-4.61;-1.96,-4.97;-0.12,-5.33;1.71,-5.69;3.54,-6.05;5.38,-6.41
LAND1.intelligentSceneAnalysis=-1
LAND1.mowingSafetyDistance=-1
@@ -11,13 +11,15 @@
LAND2.intelligentSceneAnalysis=-1
LAND2.mowingOverlapDistance=0.06
LAND2.landArea=55.11
LAND2.returnPathRawCoordinates=38.12,1.53;36.68,1.24;35.24,0.94;33.80,0.59;32.43,0.16;31.15,-0.38;30.26,-1.26;30.03,-2.37;30.12,-3.81;30.34,-5.22;30.59,-6.60;30.87,-7.98;31.10,-9.36;31.33,-10.77;31.57,-12.20;31.82,-13.68;32.07,-15.16;32.28,-16.60;32.52,-17.92;32.78,-19.37;33.07,-20.80;33.35,-22.37;33.62,-23.91;33.89,-25.35;34.16,-26.79;34.23,-28.32;33.75,-29.45;32.72,-30.03;31.41,-30.28;30.15,-30.22;29.26,-29.73;28.93,-28.62;28.72,-27.16;28.54,-25.66;28.26,-24.17;28.01,-22.74
LAND1.returnPathCoordinates=-1
LAND1.mowingPattern=平行线
LAND1.mowingOverlapDistance=0.06
LAND2.updateTime=2025-12-20 14\:47\:09
LAND2.updateTime=2025-12-21 11\:15\:33
LAND1.returnPathRawCoordinates=-1
LAND2.createTime=2025-12-20 12\:24\:28
LAND1.mowingWidth=34
LAND2.returnPathCoordinates=-1
LAND2.returnPathCoordinates=38.12,1.53;33.80,0.59;31.15,-0.38;30.26,-1.26;30.03,-2.37;30.34,-5.22;34.16,-26.79;34.23,-28.32;33.75,-29.45;32.72,-30.03;31.41,-30.28;30.15,-30.22;29.26,-29.73;28.93,-28.62;28.01,-22.74
LAND2.angleThreshold=-1
LAND2.boundaryPointInterval=-1
LAND2.mowingWidth=34
pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>gecao-app</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- Use a widely compatible release for baseline; will upgrade to 21 in plan -->
        <maven.compiler.release>11</maven.compiler.release>
    </properties>
    <build>
        <!-- Project uses non-standard src layout, point Maven to existing source directory -->
        <sourceDirectory>src</sourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <release>${maven.compiler.release}</release>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <!-- Add dependencies as needed; current project appears to use local libs -->
    </dependencies>
</project>
set.properties
@@ -1,5 +1,5 @@
#Current work land selection updated
#Sat Dec 20 15:10:30 GMT+08:00 2025
#Mower Configuration Properties - Updated
#Sun Dec 21 12:36:00 GMT+08:00 2025
appVersion=-1
simCardNumber=-1
currentWorkLandNumber=LAND2
@@ -7,13 +7,13 @@
boundaryLengthVisible=false
idleTrailDurationSeconds=60
handheldMarkerId=1872
viewCenterX=5.62
viewCenterY=6.22
viewCenterX=-13.31
viewCenterY=3.75
manualBoundaryDrawingMode=false
mowerId=860
serialPortName=COM15
serialAutoConnect=true
mapScale=13.68
mapScale=11.63
measurementModeEnabled=false
firmwareVersion=-1
cuttingWidth=200
src/baseStation/BaseStationDialog.java
@@ -3,6 +3,7 @@
import javax.swing.*;
import gecaoji.Device;
import publicway.buttonset;
import java.awt.*;
import java.awt.event.ActionListener;
@@ -16,8 +17,6 @@
import java.util.Locale;
import java.util.Properties;
import zhuye.buttonset;
public class BaseStationDialog extends JDialog {
    private final Color THEME_COLOR;
    private final Device device;
src/dikuai/Dikuai.java
@@ -25,6 +25,8 @@
    private String returnPointCoordinates;
    // å¾€è¿”路径坐标(割草机完成割草作业返回的路径坐标,格式:X1,Y1;X2,Y2;...;XN,YN)
    private String returnPathCoordinates;
    // å¾€è¿”路径原始坐标
    private String returnPathRawCoordinates;
    // è¾¹ç•Œç‚¹é—´éš”
    private String boundaryPointInterval;
    // è§’度阈值
@@ -103,6 +105,7 @@
                dikuai.plannedPath = landProps.getProperty("plannedPath", "-1");
                dikuai.returnPointCoordinates = landProps.getProperty("returnPointCoordinates", "-1");
                dikuai.returnPathCoordinates = landProps.getProperty("returnPathCoordinates", "-1");
                dikuai.returnPathRawCoordinates = landProps.getProperty("returnPathRawCoordinates", "-1");
                dikuai.boundaryPointInterval = landProps.getProperty("boundaryPointInterval", "-1");
                dikuai.angleThreshold = landProps.getProperty("angleThreshold", "-1");
                dikuai.intelligentSceneAnalysis = landProps.getProperty("intelligentSceneAnalysis", "-1");
@@ -210,6 +213,9 @@
            case "returnPathCoordinates":
                this.returnPathCoordinates = value;
                return true;
            case "returnPathRawCoordinates":
                this.returnPathRawCoordinates = value;
                return true;
            case "boundaryPointInterval":
                this.boundaryPointInterval = value;
                return true;
@@ -274,6 +280,7 @@
            if (dikuai.plannedPath != null) properties.setProperty(landNumber + ".plannedPath", dikuai.plannedPath);
            if (dikuai.returnPointCoordinates != null) properties.setProperty(landNumber + ".returnPointCoordinates", dikuai.returnPointCoordinates);
            if (dikuai.returnPathCoordinates != null) properties.setProperty(landNumber + ".returnPathCoordinates", dikuai.returnPathCoordinates);
            if (dikuai.returnPathRawCoordinates != null) properties.setProperty(landNumber + ".returnPathRawCoordinates", dikuai.returnPathRawCoordinates);
            if (dikuai.boundaryPointInterval != null) properties.setProperty(landNumber + ".boundaryPointInterval", dikuai.boundaryPointInterval);
            if (dikuai.angleThreshold != null) properties.setProperty(landNumber + ".angleThreshold", dikuai.angleThreshold);
            if (dikuai.intelligentSceneAnalysis != null) properties.setProperty(landNumber + ".intelligentSceneAnalysis", dikuai.intelligentSceneAnalysis);
@@ -366,6 +373,14 @@
        this.returnPathCoordinates = returnPathCoordinates;
    }
    public String getReturnPathRawCoordinates() {
        return returnPathRawCoordinates;
    }
    public void setReturnPathRawCoordinates(String returnPathRawCoordinates) {
        this.returnPathRawCoordinates = returnPathRawCoordinates;
    }
    public String getBoundaryPointInterval() {
        return boundaryPointInterval;
    }
@@ -481,6 +496,7 @@
                ", plannedPath='" + plannedPath + '\'' +
                ", returnPointCoordinates='" + returnPointCoordinates + '\'' +
                ", returnPathCoordinates='" + returnPathCoordinates + '\'' +
                ", returnPathRawCoordinates='" + returnPathRawCoordinates + '\'' +
                ", boundaryPointInterval='" + boundaryPointInterval + '\'' +
                ", angleThreshold='" + angleThreshold + '\'' +
                ", intelligentSceneAnalysis='" + intelligentSceneAnalysis + '\'' +
src/dikuai/Dikuaiguanli.java
@@ -23,14 +23,14 @@
import lujing.Lunjingguihua;
import lujing.MowingPathGenerationPage;
import publicway.Fuzhibutton;
import publicway.Lookbutton;
import publicway.buttonset;
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
import zhuye.MapRenderer;
import zhuye.Shouye;
import zhuye.Coordinate;
import zhuye.buttonset;
import zhuye.Fuzhibutton;
import zhuye.Lookbutton;
import gecaoji.Device;
/**
@@ -316,8 +316,7 @@
        contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        // å¾€è¿”点路径(带查看图标按钮)
        JPanel returnPathPanel = createCardInfoItemWithButton("往返点路径:",
            getDisplayValue(dikuai.getReturnPathCoordinates(), "未设置"),
        JPanel returnPathPanel = createCardInfoItemWithIconButton("往返点路径:",
            createViewButton(e -> editReturnPath(dikuai)));
        configureInteractiveLabel(getInfoItemTitleLabel(returnPathPanel),
            () -> editReturnPath(dikuai),
@@ -755,6 +754,20 @@
    }
    private String promptCoordinateEditing(String title, String initialValue) {
        return promptCoordinateEditing(title, initialValue, null);
    }
    private String promptCoordinateEditing(String title, String initialValue, Dikuai dikuai) {
        // åˆ¤æ–­æ˜¯å¦æ˜¯å¾€è¿”点路径对话框
        boolean isReturnPathDialog = title != null && title.contains("往返点路径");
        if (isReturnPathDialog) {
            Window owner = SwingUtilities.getWindowAncestor(this);
            Wangfanpathpage page = new Wangfanpathpage(owner, title, initialValue, dikuai);
            page.setVisible(true);
            return page.getResult();
        }
        JTextArea textArea = new JTextArea(prepareCoordinateForEditor(initialValue));
        textArea.setLineWrap(true);
        textArea.setWrapStyleWord(true);
@@ -763,7 +776,8 @@
        textArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setPreferredSize(new Dimension(360, 240));
        // å¦‚果是往返点路径对话框,高度调整为适应两个文本域
        scrollPane.setPreferredSize(new Dimension(360, isReturnPathDialog ? 100 : 240));
        Window owner = SwingUtilities.getWindowAncestor(this);
        JDialog dialog;
@@ -776,17 +790,115 @@
        }
        dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        JPanel contentPanel = new JPanel(new BorderLayout());
        JPanel contentPanel = new JPanel();
        contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        contentPanel.add(scrollPane, BorderLayout.CENTER);
        if (isReturnPathDialog) {
            contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
            // å‡å°è¾¹è·ä»¥å¢žåŠ æ–‡æœ¬åŸŸå®½åº¦ (98%左右)
            contentPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
            // 1. åŽŸå§‹å¾€è¿”è·¯å¾„åæ ‡åŒºåŸŸ
            String rawCoords = dikuai != null ? prepareCoordinateForEditor(dikuai.getReturnPathRawCoordinates()) : "";
            int rawCount = 0;
            if (rawCoords != null && !rawCoords.isEmpty() && !"-1".equals(rawCoords)) {
                rawCount = rawCoords.split(";").length;
            }
            JPanel rawHeaderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
            rawHeaderPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
            rawHeaderPanel.setBackground(BACKGROUND_COLOR);
            rawHeaderPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
            JLabel rawTitleLabel = new JLabel("原始往返路径坐标 (" + rawCount + "点)  ");
            rawTitleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
            rawHeaderPanel.add(rawTitleLabel);
            // åŽŸå§‹åæ ‡å¤åˆ¶æŒ‰é’®
            final String finalRawCoords = rawCoords;
            JButton rawCopyBtn = Fuzhibutton.createCopyButton(
                () -> {
                    if (finalRawCoords == null || finalRawCoords.isEmpty() || "-1".equals(finalRawCoords)) return null;
                    return finalRawCoords;
                },
                "复制",
                new Color(230, 250, 240)
            );
            rawCopyBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
            rawCopyBtn.setPreferredSize(new Dimension(50, 24));
            rawCopyBtn.setMargin(new Insets(0,0,0,0));
            rawHeaderPanel.add(rawCopyBtn);
            contentPanel.add(rawHeaderPanel);
            contentPanel.add(Box.createVerticalStrut(5));
            JTextArea rawTextArea = new JTextArea(rawCoords);
            rawTextArea.setLineWrap(true);
            rawTextArea.setWrapStyleWord(true);
            rawTextArea.setFont(new Font("微软雅黑", Font.PLAIN, 13));
            rawTextArea.setEditable(false); // åŽŸå§‹åæ ‡é€šå¸¸ä¸å¯ç¼–è¾‘
            rawTextArea.setRows(4);
            rawTextArea.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
            JScrollPane rawScroll = new JScrollPane(rawTextArea);
            rawScroll.setAlignmentX(Component.LEFT_ALIGNMENT);
            // è®¾ç½®æœ€å¤§å®½åº¦å…è®¸æ‰©å±•,首选宽度适中
            rawScroll.setPreferredSize(new Dimension(300, 100));
            rawScroll.setMaximumSize(new Dimension(Integer.MAX_VALUE, 100));
            contentPanel.add(rawScroll);
            contentPanel.add(Box.createVerticalStrut(15));
            // 2. ä¼˜åŒ–后往返路径坐标区域
            String optCoords = prepareCoordinateForEditor(initialValue);
            int optCount = 0;
            if (optCoords != null && !optCoords.isEmpty() && !"-1".equals(optCoords)) {
                optCount = optCoords.split(";").length;
            }
            JPanel optHeaderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
            optHeaderPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
            optHeaderPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
            JLabel optTitleLabel = new JLabel("优化后往返路径坐标 (" + optCount + "点)  ");
            optTitleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
            optHeaderPanel.add(optTitleLabel);
            // ä¼˜åŒ–坐标复制按钮 - åŠ¨æ€èŽ·å–æ–‡æœ¬åŸŸå†…å®¹
            JButton optCopyBtn = 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)
            );
            optCopyBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
            optCopyBtn.setPreferredSize(new Dimension(50, 24));
            optCopyBtn.setMargin(new Insets(0,0,0,0));
            optHeaderPanel.add(optCopyBtn);
            contentPanel.add(optHeaderPanel);
            contentPanel.add(Box.createVerticalStrut(5));
            // ä½¿ç”¨ä¼ å…¥çš„ textArea (已初始化为 initialValue)
            textArea.setRows(4);
            scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
            scrollPane.setPreferredSize(new Dimension(300, 100));
            scrollPane.setMaximumSize(new Dimension(Integer.MAX_VALUE, 100));
            contentPanel.add(scrollPane);
        } else {
            contentPanel.setLayout(new BorderLayout());
            contentPanel.add(scrollPane, BorderLayout.CENTER);
        }
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        
        // åˆ¤æ–­æ˜¯å¦æ˜¯å¾€è¿”点路径对话框
        boolean isReturnPathDialog = title != null && title.contains("往返点路径");
        JButton okButton;
        JButton cancelButton;
        JButton copyButton;
        JButton copyButton = null; // åˆå§‹åŒ–为null
        
        if (isReturnPathDialog) {
            // å¾€è¿”点路径对话框:使用 buttonset é£Žæ ¼çš„确定按钮,图标按钮
@@ -811,22 +923,8 @@
                public void mouseExited(MouseEvent e) { cancelButton.setOpaque(false); }
            });
            
            // ä½¿ç”¨ Fuzhibutton åˆ›å»ºå¤åˆ¶æŒ‰é’®
            copyButton = Fuzhibutton.createCopyButton(
                (java.util.function.Supplier<String>) () -> {
                    String text = textArea.getText();
                    if (text == null) {
                        text = "";
                    }
                    String trimmed = text.trim();
                    if (trimmed.isEmpty() || "-1".equals(trimmed)) {
                        return null; // è¿”回null会触发"未设置要复制的内容"提示
                    }
                    return text;
                },
                "复制" + title,
                new Color(230, 250, 240)
            );
            // ä½¿ç”¨ Fuzhibutton åˆ›å»ºå¤åˆ¶æŒ‰é’® (这里不再需要底部的复制按钮,因为上面已经有了)
            // copyButton = ...
            
        } else {
            // å…¶ä»–对话框保持原有样式
@@ -860,18 +958,41 @@
        final String[] resultHolder = new String[1];
        okButton.addActionListener(e -> {
            resultHolder[0] = textArea.getText();
            confirmed[0] = true;
            dialog.dispose();
            if (isReturnPathDialog) {
                // å¾€è¿”点路径对话框:标记为打开绘制页面
                // å¦‚果文本域中已经有坐标,表示要重新绘制
                String currentText = textArea.getText();
                if (currentText != null && !currentText.trim().isEmpty() && !"-1".equals(currentText.trim())) {
                    // æœ‰åæ ‡ï¼Œè¡¨ç¤ºé‡æ–°ç»˜åˆ¶
                    resultHolder[0] = "__OPEN_DRAW_PAGE_REFRESH__";
                } else {
                    // æ²¡æœ‰åæ ‡ï¼Œæ­£å¸¸ç»˜åˆ¶
                    resultHolder[0] = "__OPEN_DRAW_PAGE__";
                }
                confirmed[0] = true;
                dialog.dispose();
            } else {
                resultHolder[0] = textArea.getText();
                confirmed[0] = true;
                dialog.dispose();
            }
        });
        cancelButton.addActionListener(e -> dialog.dispose());
        buttonPanel.add(okButton);
        buttonPanel.add(cancelButton);
        buttonPanel.add(copyButton);
        if (copyButton != null) {
            buttonPanel.add(copyButton);
        }
        contentPanel.add(buttonPanel, BorderLayout.SOUTH);
        contentPanel.add(buttonPanel, isReturnPathDialog ? null : BorderLayout.SOUTH);
        if (isReturnPathDialog) {
            // å¯¹äºŽ BoxLayout,直接添加到底部
            JPanel bottomWrapper = new JPanel(new BorderLayout());
            bottomWrapper.add(buttonPanel, BorderLayout.EAST);
            contentPanel.add(bottomWrapper);
        }
        dialog.setContentPane(contentPanel);
        dialog.getRootPane().setDefaultButton(okButton);
        dialog.pack();
@@ -1914,10 +2035,41 @@
        if (dikuai == null) {
            return;
        }
        String edited = promptCoordinateEditing("查看 / ç¼–辑往返点路径", dikuai.getReturnPathCoordinates());
        String edited = promptCoordinateEditing("查看 / ç¼–辑往返点路径", dikuai.getReturnPathCoordinates(), dikuai);
        if (edited == null) {
            return;
        }
        // æ£€æŸ¥æ˜¯å¦æ˜¯ç‰¹æ®Šæ ‡è®°ï¼Œè¡¨ç¤ºç‚¹å‡»äº†"去绘制"按钮
        if ("__OPEN_DRAW_PAGE__".equals(edited) || "__OPEN_DRAW_PAGE_REFRESH__".equals(edited)) {
            // èŽ·å–åœ°å—ç®¡ç†å¯¹è¯æ¡†
            Window owner = SwingUtilities.getWindowAncestor(this);
            Window managementWindow = null;
            if (owner instanceof JDialog) {
                managementWindow = owner;
            }
            // èŽ·å–åœ°å—ç®¡ç†å¯¹è¯æ¡†çš„çˆ¶çª—å£ï¼ˆä¸»çª—å£ï¼‰ï¼Œä½œä¸ºç»˜åˆ¶é¡µé¢çš„çˆ¶çª—å£
            Window drawPageOwner = null;
            if (managementWindow != null) {
                drawPageOwner = managementWindow.getOwner();
            }
            if (drawPageOwner == null && owner != null) {
                drawPageOwner = owner.getOwner();
            }
            if (drawPageOwner == null) {
                drawPageOwner = owner;
            }
            // å…ˆå…³é—­åœ°å—管理页面
            if (managementWindow != null) {
                managementWindow.dispose();
            }
            // ç„¶åŽæ‰“开绘制页面,如果是重新绘制,传递标记
            boolean isRefresh = "__OPEN_DRAW_PAGE_REFRESH__".equals(edited);
            Huizhiwanfanpath.showDrawReturnPathDialog(drawPageOwner, dikuai, isRefresh);
            return;
        }
        String normalized = normalizeCoordinateInput(edited);
        if (!saveFieldAndRefresh(dikuai, "returnPathCoordinates", normalized)) {
            JOptionPane.showMessageDialog(this, "无法更新往返点路径", "错误", JOptionPane.ERROR_MESSAGE);
@@ -2322,4 +2474,6 @@
            return new ArrayList<>(names);
        }
    }
}
src/dikuai/FanhuiDialog.java
@@ -1,13 +1,16 @@
package dikuai;
import javax.swing.*;
import publicway.buttonset;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileInputStream;
import java.util.Properties;
import zhuye.MowerLocationData;
import zhuye.buttonset;
import java.text.DecimalFormat;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
src/dikuai/Huizhiwanfanpath.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,896 @@
package dikuai;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import set.Setsys;
import ui.UIConfig;
import zhuye.Coordinate;
import lujing.WangfanpathJisuan;
import publicway.buttonset;
/**
 * ç»˜åˆ¶å¾€è¿”路径页面
 * å‚考新增地块步骤2,用于绘制往返点路径
 */
public class Huizhiwanfanpath extends JDialog {
    private static final long serialVersionUID = 1L;
    // ä¸»é¢˜é¢œè‰² - ä¼˜åŒ–配色方案
    private final Color PRIMARY_COLOR = new Color(46, 139, 87);
    private final Color PRIMARY_LIGHT = new Color(232, 245, 233);
    private final Color PRIMARY_DARK = new Color(30, 107, 69);
    private final Color WHITE = Color.WHITE;
    private final Color LIGHT_GRAY = new Color(248, 249, 250);
    private final Color TEXT_COLOR = new Color(33, 37, 41);
    private final Color LIGHT_TEXT = new Color(108, 117, 125);
    private final Color BORDER_COLOR = new Color(222, 226, 230);
    private final Color ERROR_COLOR = new Color(220, 53, 69);
    // ä¸»é¢æ¿
    private JPanel mainPanel;
    private Map<String, JPanel> drawingOptionPanels = new HashMap<>();
    // é€‰é¡¹çŠ¶æ€
    private JPanel selectedOptionPanel = null;
    private JButton startEndDrawingBtn;
    private boolean isDrawing = false;
    private boolean hasDrawnPath = false;  // æ˜¯å¦å·²ç»ç»˜åˆ¶è¿‡è·¯å¾„
    private JLabel pathCountLabel;
    private JLabel drawingMethodHintLabel;  // ç»˜åˆ¶æ–¹å¼æç¤ºæ ‡ç­¾
    private JButton generatePathBtn;  // ç”Ÿæˆè·¯å¾„按钮
    private JButton previewBtn;  // é¢„览按钮
    private JButton saveBtn;  // ä¿å­˜æŒ‰é’®
    private JPanel actionButtonsPanel;  // æ“ä½œæŒ‰é’®é¢æ¿
    // åœ°å—信息
    private Dikuai currentDikuai;
    private boolean isRefreshMode = false;  // æ˜¯å¦é‡æ–°ç»˜åˆ¶æ¨¡å¼
    private String optimizedPathCoordinates = null;  // ä¼˜åŒ–后的路径坐标
    private JTextArea rawCoordinatesArea; // åŽŸå§‹åæ ‡æ–‡æœ¬åŸŸ
    private JTextArea optimizedCoordinatesArea; // è®¡ç®—后坐标文本域
    private JLabel rawCoordinatesLabel; // åŽŸå§‹åæ ‡æ ‡é¢˜
    private JLabel optimizedCoordinatesLabel; // ä¼˜åŒ–后坐标标题
    public Huizhiwanfanpath(Window parent, Dikuai dikuai) {
        this(parent, dikuai, false);
    }
    public Huizhiwanfanpath(Window parent, Dikuai dikuai, boolean isRefresh) {
        super(parent, "绘制往返路径", Dialog.ModalityType.APPLICATION_MODAL);
        this.currentDikuai = dikuai;
        this.isRefreshMode = isRefresh;
        initializeUI();
    }
    private void initializeUI() {
        setLayout(new BorderLayout());
        setBackground(WHITE);
        // ç»Ÿä¸€ä½¿ç”¨ 6.5 å¯¸ç«–屏适配尺寸
        setSize(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT);
        setLocationRelativeTo(getParent());
        setResizable(false);
        createMainPanel();
        add(mainPanel, BorderLayout.CENTER);
    }
    private void createMainPanel() {
        mainPanel = new JPanel();
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
        mainPanel.setBackground(WHITE);
        mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        // æ­¥éª¤æ ‡é¢˜ - å·¦å¯¹é½ï¼ˆåŽ»æŽ‰"绘制边界"文字)
        // JLabel stepTitle = new JLabel("步骤1:");
        // stepTitle.setFont(new Font("微软雅黑", Font.BOLD, 20));
        // stepTitle.setForeground(TEXT_COLOR);
        // stepTitle.setAlignmentX(Component.LEFT_ALIGNMENT);
        // mainPanel.add(stepTitle);
        // mainPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        // æ­¥éª¤è¯´æ˜Ž
        JPanel instructionPanel = createInstructionPanel(
            "请选择绘制往返路径的方式。割草机绘制需要驾驶割草机沿路径行驶," +
            "手持设备绘制则使用便携设备标记路径点。"
        );
        instructionPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        mainPanel.add(instructionPanel);
        mainPanel.add(Box.createRigidArea(new Dimension(0, 25)));
        // ç»˜åˆ¶æ–¹å¼é€‰æ‹©
        JLabel methodLabel = new JLabel("选择绘制方式");
        methodLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
        methodLabel.setForeground(TEXT_COLOR);
        methodLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        mainPanel.add(methodLabel);
        mainPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        // ç»˜åˆ¶é€‰é¡¹é¢æ¿ - åž‚直布局以完整显示图标
        JPanel optionsPanel = new JPanel();
        optionsPanel.setLayout(new BoxLayout(optionsPanel, BoxLayout.Y_AXIS));
        optionsPanel.setBackground(WHITE);
        optionsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        // å‰²è‰æœºç»˜åˆ¶é€‰é¡¹
        JPanel mowerOption = createDrawingOption("🚜", "割草机绘制",
            "", "mower");
        mowerOption.setAlignmentX(Component.LEFT_ALIGNMENT);
        mowerOption.setBorder(BorderFactory.createLineBorder(BORDER_COLOR, 2));
        // æ‰‹æŒè®¾å¤‡ç»˜åˆ¶é€‰é¡¹
        JPanel handheldOption = createDrawingOption("📱", "手持设备绘制",
            "", "handheld");
        handheldOption.setAlignmentX(Component.LEFT_ALIGNMENT);
        handheldOption.setBorder(BorderFactory.createLineBorder(BORDER_COLOR, 2));
        optionsPanel.add(mowerOption);
        optionsPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        optionsPanel.add(handheldOption);
        mainPanel.add(optionsPanel);
        mainPanel.add(Box.createRigidArea(new Dimension(0, 30)));
        // ç»˜åˆ¶æ–¹å¼æç¤ºæ ‡ç­¾ï¼ˆå¦‚果没有选择绘制方式时显示)
        // æç¤ºæ–‡æœ¬ï¼šæé†’先选择绘制方式再开始
        drawingMethodHintLabel = new JLabel("请先选择绘制方式再点击开始绘制");
        drawingMethodHintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        drawingMethodHintLabel.setForeground(ERROR_COLOR);
        drawingMethodHintLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        drawingMethodHintLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
        drawingMethodHintLabel.setVisible(false);
        mainPanel.add(drawingMethodHintLabel);
        // å¼€å§‹/结束绘制按钮
        startEndDrawingBtn = createPrimaryButton("开始绘制", 16);
        startEndDrawingBtn.setAlignmentX(Component.LEFT_ALIGNMENT);
        startEndDrawingBtn.setMaximumSize(new Dimension(400, 55));
        startEndDrawingBtn.setEnabled(true); // åˆå§‹å¯ç”¨ï¼Œä»¥ä¾¿ç‚¹å‡»æ—¶æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†ç»˜åˆ¶æ–¹å¼
        startEndDrawingBtn.addActionListener(e -> toggleDrawing());
        mainPanel.add(startEndDrawingBtn);
        // åŽŸå§‹åæ ‡æ˜¾ç¤ºåŒºåŸŸï¼ˆé‡æ–°ç»˜åˆ¶æŒ‰é’®ä¸‹æ–¹ï¼‰
        rawCoordinatesLabel = new JLabel("路径原始坐标");
        rawCoordinatesLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
        rawCoordinatesLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        rawCoordinatesLabel.setBorder(BorderFactory.createEmptyBorder(20, 0, 5, 0));
        rawCoordinatesLabel.setVisible(false);
        mainPanel.add(rawCoordinatesLabel);
        rawCoordinatesArea = new JTextArea(3, 20);
        rawCoordinatesArea.setFont(new Font("Consolas", Font.PLAIN, 12));
        rawCoordinatesArea.setLineWrap(true);
        rawCoordinatesArea.setWrapStyleWord(true);
        rawCoordinatesArea.setEditable(false);
        rawCoordinatesArea.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
        JScrollPane rawScrollPane = new JScrollPane(rawCoordinatesArea);
        rawScrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
        rawScrollPane.setMaximumSize(new Dimension(400, 60)); // é™åˆ¶é«˜åº¦
        rawScrollPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); // åŽ»æŽ‰é¡¶éƒ¨é—´éš”ï¼Œç”±æ ‡ç­¾æä¾›
        rawScrollPane.setVisible(false); // åˆå§‹éšè—
        mainPanel.add(rawScrollPane);
        // æ“ä½œæŒ‰é’®é¢æ¿ï¼ˆç”Ÿæˆè·¯å¾„、预览、保存)
        actionButtonsPanel = createActionButtonsPanel();
        actionButtonsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        actionButtonsPanel.setVisible(false);
        mainPanel.add(actionButtonsPanel);
        // è®¡ç®—后坐标显示区域(保存按钮下方)
        optimizedCoordinatesLabel = new JLabel("优化后路径坐标");
        optimizedCoordinatesLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
        optimizedCoordinatesLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        optimizedCoordinatesLabel.setBorder(BorderFactory.createEmptyBorder(20, 0, 5, 0));
        optimizedCoordinatesLabel.setVisible(false);
        mainPanel.add(optimizedCoordinatesLabel);
        optimizedCoordinatesArea = new JTextArea(3, 20);
        optimizedCoordinatesArea.setFont(new Font("Consolas", Font.PLAIN, 12));
        optimizedCoordinatesArea.setLineWrap(true);
        optimizedCoordinatesArea.setWrapStyleWord(true);
        optimizedCoordinatesArea.setEditable(false);
        optimizedCoordinatesArea.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
        JScrollPane optimizedScrollPane = new JScrollPane(optimizedCoordinatesArea);
        optimizedScrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
        optimizedScrollPane.setMaximumSize(new Dimension(400, 60)); // é™åˆ¶é«˜åº¦
        optimizedScrollPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); // åŽ»æŽ‰é¡¶éƒ¨é—´éš”ï¼Œç”±æ ‡ç­¾æä¾›
        optimizedScrollPane.setVisible(false); // åˆå§‹éšè—
        mainPanel.add(optimizedScrollPane);
        mainPanel.add(Box.createVerticalGlue());
    }
    private JPanel createDrawingOption(String icon, String text, String description, String type) {
        JPanel optionPanel = new JPanel(new BorderLayout(15, 0));
        optionPanel.setBackground(WHITE);
        optionPanel.setCursor(new Cursor(Cursor.HAND_CURSOR));
        optionPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        optionPanel.setMaximumSize(new Dimension(500, 120));
        // å›¾æ ‡åŒºåŸŸ
        JLabel iconLabel;
        if ("handheld".equals(type) || "mower".equals(type)) {
            iconLabel = new JLabel();
            iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
            iconLabel.setPreferredSize(new Dimension(80, 80));
            String iconPath = "handheld".equals(type) ? "image/URT.png" : "image/mow.png";
            ImageIcon rawIcon = new ImageIcon(iconPath);
            if (rawIcon.getIconWidth() > 0 && rawIcon.getIconHeight() > 0) {
                Image scaled = rawIcon.getImage().getScaledInstance(64, 64, Image.SCALE_SMOOTH);
                iconLabel.setIcon(new ImageIcon(scaled));
            } else {
                iconLabel.setText(icon);
                iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 48));
            }
        } else {
            iconLabel = new JLabel(icon, JLabel.CENTER);
            iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 48));
            iconLabel.setPreferredSize(new Dimension(80, 80));
        }
        // æ–‡æœ¬åŒºåŸŸ
        JPanel textPanel = new JPanel(new GridBagLayout());
        textPanel.setBackground(WHITE);
        JLabel textLabel = new JLabel(text);
        textLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
        textLabel.setHorizontalAlignment(SwingConstants.CENTER);
        textLabel.setVerticalAlignment(SwingConstants.CENTER);
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.weightx = 1;
        gbc.weighty = description == null || description.trim().isEmpty() ? 1 : 0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.anchor = GridBagConstraints.CENTER;
        textPanel.add(textLabel, gbc);
        if (description != null && !description.trim().isEmpty()) {
            JLabel descLabel = new JLabel(description);
            descLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
            descLabel.setForeground(LIGHT_TEXT);
            descLabel.setHorizontalAlignment(SwingConstants.CENTER);
            GridBagConstraints descGbc = new GridBagConstraints();
            descGbc.gridx = 0;
            descGbc.gridy = 1;
            descGbc.weightx = 1;
            descGbc.weighty = 1;
            descGbc.fill = GridBagConstraints.HORIZONTAL;
            descGbc.anchor = GridBagConstraints.CENTER;
            textPanel.add(descLabel, descGbc);
        }
        optionPanel.putClientProperty("titleLabel", textLabel);
        optionPanel.add(iconLabel, BorderLayout.WEST);
        optionPanel.add(textPanel, BorderLayout.CENTER);
        // æ·»åŠ ç‚¹å‡»äº‹ä»¶
        optionPanel.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (!optionPanel.isEnabled()) {
                    return;
                }
                if (selectDrawingOption(optionPanel, type, true)) {
                    startEndDrawingBtn.setEnabled(true); // é€‰æ‹©åŽå¯ç”¨æŒ‰é’®
                    // éšè—æç¤ºæ–‡å­—
                    if (drawingMethodHintLabel != null) {
                        drawingMethodHintLabel.setVisible(false);
                    }
                }
            }
            @Override
            public void mouseEntered(MouseEvent e) {
                if (optionPanel != selectedOptionPanel) {
                    optionPanel.setBackground(new Color(245, 245, 245));
                }
            }
            @Override
            public void mouseExited(MouseEvent e) {
                if (optionPanel != selectedOptionPanel) {
                    optionPanel.setBackground(WHITE);
                }
            }
        });
        drawingOptionPanels.put(type, optionPanel);
        return optionPanel;
    }
    private boolean selectDrawingOption(JPanel optionPanel, String type, boolean userTriggered) {
        if (optionPanel == null) {
            return false;
        }
        if (userTriggered && "handheld".equalsIgnoreCase(type) && !hasConfiguredHandheldMarker()) {
            JOptionPane.showMessageDialog(this, "请先去系统设置添加便携打点器编号", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
        }
        // é‡ç½®ä¹‹å‰é€‰ä¸­çš„选项
        if (selectedOptionPanel != null) {
            selectedOptionPanel.setBorder(BorderFactory.createLineBorder(BORDER_COLOR, 2));
            selectedOptionPanel.setBackground(WHITE);
            Object oldTitle = selectedOptionPanel.getClientProperty("titleLabel");
            if (oldTitle instanceof JLabel) {
                ((JLabel) oldTitle).setForeground(TEXT_COLOR);
            }
        }
        // è®¾ç½®æ–°çš„选中状态
        optionPanel.setBorder(BorderFactory.createLineBorder(PRIMARY_COLOR, 3));
        optionPanel.setBackground(PRIMARY_LIGHT);
        Object titleObj = optionPanel.getClientProperty("titleLabel");
        if (titleObj instanceof JLabel) {
            ((JLabel) titleObj).setForeground(PRIMARY_COLOR);
        }
        selectedOptionPanel = optionPanel;
        return true;
    }
    private boolean hasConfiguredHandheldMarker() {
        String handheldId = Setsys.getPropertyValue("handheldMarkerId");
        return handheldId != null && !handheldId.trim().isEmpty();
    }
    private void toggleDrawing() {
        if (!isDrawing) {
            // æ£€æŸ¥æ˜¯å¦é€‰æ‹©äº†ç»˜åˆ¶æ–¹å¼
            if (selectedOptionPanel == null) {
                // æ˜¾ç¤ºæç¤ºæ–‡å­—
                if (drawingMethodHintLabel != null) {
                    drawingMethodHintLabel.setVisible(true);
                }
                return;
            } else {
                // éšè—æç¤ºæ–‡å­—
                if (drawingMethodHintLabel != null) {
                    drawingMethodHintLabel.setVisible(false);
                }
            }
            // å¦‚果是重新绘制,清空之前的路径点
            if (hasDrawnPath || isRefreshMode) {
                int confirm = JOptionPane.showConfirmDialog(this,
                    "重新绘制将清空之前的路径点,是否继续?",
                    "确认",
                    JOptionPane.YES_NO_OPTION);
                if (confirm != JOptionPane.YES_OPTION) {
                    return;
                }
                // æ¸…空之前的坐标
                Coordinate.coordinates.clear();
                lujing.SavaXyZuobiao.clearCoordinates(); // æ¸…空工具类坐标
                hasDrawnPath = false;
                isRefreshMode = false;
                optimizedPathCoordinates = null;
                // éšè—æ“ä½œæŒ‰é’®
                if (actionButtonsPanel != null) {
                    actionButtonsPanel.setVisible(false);
                }
                // éšè—åŽŸå§‹åæ ‡å’Œè®¡ç®—åŽåæ ‡
                if (rawCoordinatesArea != null && rawCoordinatesArea.getParent() instanceof JViewport) {
                    rawCoordinatesArea.getParent().getParent().setVisible(false);
                    rawCoordinatesArea.setText("");
                }
                if (optimizedCoordinatesArea != null && optimizedCoordinatesArea.getParent() instanceof JViewport) {
                    optimizedCoordinatesArea.getParent().getParent().setVisible(false);
                    optimizedCoordinatesArea.setText("");
                }
                // é‡ç½®ä¿å­˜æŒ‰é’®çŠ¶æ€
                if (saveBtn != null) {
                    saveBtn.setEnabled(false);
                }
                hidePathPointSummary();
            }
            if (!prepareDrawingSession()) {
                return;
            }
            isDrawing = true;
            // hidePathPointSummary();
            Coordinate.setStartSaveGngga(true);
            lujing.SavaXyZuobiao.startSaving(); // å¼€å§‹ä¿å­˜åæ ‡
            startEndDrawingBtn.setText("结束绘制");
            startEndDrawingBtn.setBackground(ERROR_COLOR);
            updateOtherOptionsState(true);
            if (!startDrawingPath()) {
                Coordinate.setStartSaveGngga(false);
                resetDrawingState();
            }
        } else {
            // ç”¨æˆ·åœ¨å¯¹è¯æ¡†å†…主动结束
            isDrawing = false;
            Coordinate.setStartSaveGngga(false);
            startEndDrawingBtn.setText(hasDrawnPath ? "重新绘制" : "开始绘制");
            startEndDrawingBtn.setBackground(PRIMARY_COLOR);
            updateOtherOptionsState(false);
            JOptionPane.showMessageDialog(this, "往返路径绘制已完成", "提示", JOptionPane.INFORMATION_MESSAGE);
            // showPathPointSummary();
            // æ˜¾ç¤ºæ“ä½œæŒ‰é’®
            if (actionButtonsPanel != null) {
                actionButtonsPanel.setVisible(true);
            }
            // é‡ç½®ä¿å­˜æŒ‰é’®çŠ¶æ€ï¼ˆéœ€è¦ç”Ÿæˆè·¯å¾„åŽæ‰èƒ½ä¿å­˜ï¼‰
            if (saveBtn != null) {
                saveBtn.setEnabled(false);
            }
            optimizedPathCoordinates = null;  // æ¸…空优化路径
            hasDrawnPath = true;
        }
    }
    private boolean prepareDrawingSession() {
        if (currentDikuai == null) {
            JOptionPane.showMessageDialog(this, "地块信息不存在", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
        }
        if (selectedOptionPanel == null) {
            JOptionPane.showMessageDialog(this, "请选择绘制方式", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
        }
        // æ¸…空之前的坐标
        Coordinate.coordinates.clear();
        return true;
    }
    private void updateOtherOptionsState(boolean disable) {
        if (selectedOptionPanel == null) {
            return;
        }
        Component[] components = selectedOptionPanel.getParent().getComponents();
        for (Component comp : components) {
            if (comp instanceof JPanel && comp != selectedOptionPanel) {
                comp.setEnabled(!disable);
                ((JPanel) comp).setBackground(disable ? LIGHT_GRAY : WHITE);
            }
        }
    }
    private void resetDrawingState() {
        isDrawing = false;
        Coordinate.setStartSaveGngga(false);
        startEndDrawingBtn.setText("开始绘制");
        startEndDrawingBtn.setBackground(PRIMARY_COLOR);
        updateOtherOptionsState(false);
        // hidePathPointSummary();
    }
    private boolean startDrawingPath() {
        // èŽ·å–é€‰ä¸­çš„ç»˜åˆ¶æ–¹å¼
        String method = null;
        for (Map.Entry<String, JPanel> entry : drawingOptionPanels.entrySet()) {
            if (entry.getValue() == selectedOptionPanel) {
                method = entry.getKey();
                break;
            }
        }
        if ("mower".equals(method) || "handheld".equals(method)) {
            zhuye.Shouye shouye = zhuye.Shouye.getInstance();
            if (shouye == null) {
                JOptionPane.showMessageDialog(this, "无法进入主页面,请稍后重试", "提示", JOptionPane.WARNING_MESSAGE);
                return false;
            }
            // åˆ›å»ºå®Œæˆç»˜åˆ¶å›žè°ƒï¼Œè¿”回到绘制页面
            Runnable finishCallback = () -> {
                SwingUtilities.invokeLater(() -> {
                    // åœæ­¢ä¿å­˜åæ ‡
                    lujing.SavaXyZuobiao.pauseSaving();
                    // æ¢å¤é¦–页默认的暂停和结束按钮
                    // è¿™ä¸ªåœ¨ stopReturnPathDrawing ä¸­å·²ç»å¤„理了
                    // æ˜¾ç¤ºç»˜åˆ¶é¡µé¢
                    Window owner = SwingUtilities.getWindowAncestor(this);
                    if (owner == null) {
                        owner = (Window) SwingUtilities.getWindowAncestor(shouye);
                    }
                    if (owner != null) {
                        setLocationRelativeTo(owner);
                    }
                    // é‡ç½®ç»˜åˆ¶çŠ¶æ€
                    isDrawing = false;
                    hasDrawnPath = true;  // æ ‡è®°å·²ç»˜åˆ¶è¿‡è·¯å¾„
                    startEndDrawingBtn.setText("重新绘制");
                    startEndDrawingBtn.setBackground(PRIMARY_COLOR);
                    startEndDrawingBtn.setEnabled(true);  // ä¿æŒå¯ç”¨çŠ¶æ€
                    updateOtherOptionsState(false);
                    // æ˜¾ç¤ºåŽŸå§‹åæ ‡
                    if (rawCoordinatesArea != null) {
                        String rawCoords = lujing.SavaXyZuobiao.getCoordinatesString();
                        rawCoordinatesArea.setText(rawCoords);
                        if (rawCoordinatesArea.getParent() instanceof JViewport) {
                            rawCoordinatesArea.getParent().getParent().setVisible(true);
                        }
                        if (rawCoordinatesLabel != null) {
                            int count = 0;
                            if (rawCoords != null && !rawCoords.trim().isEmpty()) {
                                count = rawCoords.split(";").length;
                            }
                            rawCoordinatesLabel.setText("路径原始坐标 (共" + count + "个点)");
                            rawCoordinatesLabel.setVisible(true);
                        }
                    }
                    // æ˜¾ç¤ºæ“ä½œæŒ‰é’®
                    if (actionButtonsPanel != null) {
                        actionButtonsPanel.setVisible(true);
                    }
                    // é‡ç½®ä¿å­˜æŒ‰é’®çŠ¶æ€ï¼ˆéœ€è¦ç”Ÿæˆè·¯å¾„åŽæ‰èƒ½ä¿å­˜ï¼‰
                    if (saveBtn != null) {
                        saveBtn.setEnabled(false);
                    }
                    optimizedPathCoordinates = null;  // æ¸…空优化路径
                    // å¼ºåˆ¶åˆ·æ–°å¸ƒå±€
                    if (mainPanel != null) {
                        mainPanel.revalidate();
                        mainPanel.repaint();
                    }
                    setVisible(true);
                    // æ›´æ–°è·¯å¾„点计数
                    // showPathPointSummary();
                    // JOptionPane.showMessageDialog(this, "往返路径绘制已完成", "提示", JOptionPane.INFORMATION_MESSAGE);
                });
            };
            // å¯åŠ¨å¾€è¿”è·¯å¾„ç»˜åˆ¶
            if (!shouye.startReturnPathDrawing(finishCallback)) {
                JOptionPane.showMessageDialog(this, "未能开始绘制,请确认设备状态和基准站设置后重试", "提示", JOptionPane.WARNING_MESSAGE);
                return false;
            }
            // æ˜¾ç¤ºé¦–页的“结束绘制”悬浮按钮,并绑定结束逻辑
            shouye.showEndDrawingButton(() -> {
                // åœæ­¢ç»˜åˆ¶å¹¶æ‰§è¡Œå®Œæˆå›žè°ƒï¼ˆæ¢å¤åˆ°æœ¬é¡µé¢å¹¶æ˜¾ç¤ºä¿å­˜/预览等)
                shouye.stopReturnPathDrawing();
                zhuye.WangfanDraw drawer = shouye.getReturnPathDrawer();
                if (drawer != null) {
                    drawer.executeFinishCallback();
                }
                // å…³é—­æ‚¬æµ®æŽ§åˆ¶
                shouye.hideEndDrawingButton();
            });
            setVisible(false);
            return true;
        }
        return false;
    }
    /**
     * ä¿å­˜è·¯å¾„坐标(保存优化后的坐标,如果没有优化则保存原始坐标)
     */
    private void savePathCoordinates() {
        if (currentDikuai == null) {
            return;
        }
        // å¦‚果已经生成优化路径,使用优化后的坐标
        String pathCoordinates = optimizedPathCoordinates;
        if (pathCoordinates == null || pathCoordinates.trim().isEmpty()) {
            // å¦‚果没有优化路径,使用原始坐标点(X,Y格式,单位米)
            pathCoordinates = buildXYCoordinatesString();
        }
        if (pathCoordinates == null || pathCoordinates.trim().isEmpty() || "-1".equals(pathCoordinates.trim())) {
            JOptionPane.showMessageDialog(this, "没有可保存的路径坐标", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        // ä¿å­˜åˆ°åœ°å—
        if (Dikuai.updateField(currentDikuai.getLandNumber(), "returnPathCoordinates", pathCoordinates)) {
            // åŒæ—¶ä¿å­˜åŽŸå§‹åæ ‡
            String rawCoords = rawCoordinatesArea != null ? rawCoordinatesArea.getText() : "";
            if (rawCoords != null && !rawCoords.trim().isEmpty()) {
                Dikuai.updateField(currentDikuai.getLandNumber(), "returnPathRawCoordinates", rawCoords);
            }
            Dikuai.updateField(currentDikuai.getLandNumber(), "updateTime", getCurrentTime());
            Dikuai.saveToProperties();
            // æ¸…空工具类坐标
            lujing.SavaXyZuobiao.clearCoordinates();
            JOptionPane.showMessageDialog(this, "路径已保存", "成功", JOptionPane.INFORMATION_MESSAGE);
        } else {
            JOptionPane.showMessageDialog(this, "保存失败", "错误", JOptionPane.ERROR_MESSAGE);
        }
    }
    /**
     * æž„建X,Y坐标字符串(单位米,精确到小数点后2位)
     */
    private String buildXYCoordinatesString() {
        // ä»Ž WangfanDraw èŽ·å–è·¯å¾„ç‚¹ï¼ˆå·²ç»æ˜¯X,Y坐标,单位米)
        zhuye.Shouye shouye = zhuye.Shouye.getInstance();
        if (shouye == null || shouye.getReturnPathDrawer() == null) {
            return "-1";
        }
        List<java.awt.geom.Point2D.Double> points = shouye.getReturnPathDrawer().getPointsSnapshot();
        if (points == null || points.isEmpty()) {
            return "-1";
        }
        StringBuilder sb = new StringBuilder();
        java.text.DecimalFormat xyFormat = new java.text.DecimalFormat("0.00");
        for (java.awt.geom.Point2D.Double point : points) {
            if (point == null) continue;
            if (sb.length() > 0) {
                sb.append(";");
            }
            sb.append(xyFormat.format(point.x)).append(",")
              .append(xyFormat.format(point.y));
        }
        return sb.length() > 0 ? sb.toString() : "-1";
    }
    private double convertToDecimalDegree(String dmm, String direction) {
        if (dmm == null || dmm.isEmpty()) {
            return 0.0;
        }
        try {
            int dotIndex = dmm.indexOf('.');
            if (dotIndex == -1) {
                return 0.0;
            }
            int degrees = Integer.parseInt(dmm.substring(0, dotIndex - 2));
            double minutes = Double.parseDouble(dmm.substring(dotIndex - 2));
            double decimal = degrees + minutes / 60.0;
            if ("S".equals(direction) || "W".equals(direction)) {
                decimal = -decimal;
            }
            return decimal;
        } catch (Exception e) {
            return 0.0;
        }
    }
    private String getCurrentTime() {
        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(new java.util.Date());
    }
    private JPanel createInstructionPanel(String text) {
        JPanel instructionPanel = new JPanel(new BorderLayout());
        instructionPanel.setBackground(PRIMARY_LIGHT);
        instructionPanel.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createMatteBorder(0, 5, 0, 0, PRIMARY_COLOR),
            BorderFactory.createEmptyBorder(12, 12, 12, 12)
        ));
        instructionPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 70));
        JLabel iconLabel = new JLabel("💡");
        iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 16));
        iconLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10));
        JTextArea instructionText = new JTextArea(text);
        instructionText.setFont(new Font("微软雅黑", Font.PLAIN, 13));
        instructionText.setForeground(TEXT_COLOR);
        instructionText.setBackground(PRIMARY_LIGHT);
        instructionText.setLineWrap(true);
        instructionText.setWrapStyleWord(true);
        instructionText.setEditable(false);
        instructionPanel.add(iconLabel, BorderLayout.WEST);
        instructionPanel.add(instructionText, BorderLayout.CENTER);
        return instructionPanel;
    }
    private JButton createPrimaryButton(String text, int fontSize) {
        JButton button = buttonset.createStyledButton(text, PRIMARY_COLOR);
        button.setFont(new Font("微软雅黑", Font.BOLD, fontSize));
        button.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(PRIMARY_DARK, 2),
            BorderFactory.createEmptyBorder(12, 25, 12, 25)
        ));
        button.setCursor(new Cursor(Cursor.HAND_CURSOR));
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                if (button.isEnabled()) {
                    button.setBackground(PRIMARY_DARK);
                }
            }
            @Override
            public void mouseExited(MouseEvent e) {
                if (button.isEnabled()) {
                    button.setBackground(PRIMARY_COLOR);
                }
            }
        });
        return button;
    }
    private void showPathPointSummary() {
        if (pathCountLabel == null) {
            return;
        }
        int count = Coordinate.coordinates != null ? Coordinate.coordinates.size() : 0;
        pathCountLabel.setText("已采集到有效坐标点" + count + "个");
        // pathCountLabel.setVisible(true);
    }
    private void hidePathPointSummary() {
        if (pathCountLabel != null) {
            // pathCountLabel.setVisible(false);
        }
    }
    /**
     * åˆ›å»ºæ“ä½œæŒ‰é’®é¢æ¿ï¼ˆç”Ÿæˆè·¯å¾„、预览、保存)
     */
    private JPanel createActionButtonsPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
        panel.setBackground(WHITE);
        panel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0));
        panel.setAlignmentX(Component.LEFT_ALIGNMENT);
        // ç”Ÿæˆè·¯å¾„按钮
        generatePathBtn = createPrimaryButton("生成路径", 16);
        generatePathBtn.setAlignmentX(Component.LEFT_ALIGNMENT);
        generatePathBtn.addActionListener(e -> generatePath());
        // é¢„览按钮
        previewBtn = createPrimaryButton("预览", 16);
        previewBtn.setAlignmentX(Component.LEFT_ALIGNMENT);
        previewBtn.addActionListener(e -> previewPath());
        // ä¿å­˜æŒ‰é’®ï¼ˆåˆå§‹ä¸å¯ç”¨ï¼‰
        saveBtn = createPrimaryButton("保存", 16);
        saveBtn.setAlignmentX(Component.LEFT_ALIGNMENT);
        saveBtn.setEnabled(false);  // åˆå§‹ä¸å¯ç”¨ï¼Œç”Ÿæˆè·¯å¾„后才可点击
        saveBtn.addActionListener(e -> savePath());
        panel.add(generatePathBtn);
        panel.add(Box.createHorizontalStrut(15));
        panel.add(previewBtn);
        panel.add(Box.createHorizontalStrut(15));
        panel.add(saveBtn);
        return panel;
    }
    /**
     * ç”Ÿæˆè·¯å¾„
     */
    private void generatePath() {
        // èŽ·å–è·¯å¾„ç‚¹ï¼ˆä»ŽåŽŸå§‹åæ ‡æ–‡æœ¬åŸŸèŽ·å–ï¼‰
        String pathStr = rawCoordinatesArea != null ? rawCoordinatesArea.getText() : "";
        if (pathStr == null || pathStr.trim().isEmpty()) {
            JOptionPane.showMessageDialog(this, "没有可用的路径点,请先完成绘制", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        try {
            // è°ƒç”¨ WangfanpathJisuan ä¼˜åŒ–路径
            lujing.WangfanpathJisuan calculator = new lujing.WangfanpathJisuan();
            // è®¾ç½®è¾“出精度为2位小数
            lujing.WangfanpathJisuan.OptimizationConfig config = new lujing.WangfanpathJisuan.OptimizationConfig();
            config.setOutputPrecision(2);
            String optimizedPath = calculator.optimizePath(pathStr, config);
            if (optimizedPath == null || optimizedPath.trim().isEmpty()) {
                JOptionPane.showMessageDialog(this, "路径优化失败", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            // ä¿å­˜ä¼˜åŒ–后的路径坐标
            optimizedPathCoordinates = optimizedPath;
            // æ˜¾ç¤ºè®¡ç®—后坐标
            if (optimizedCoordinatesArea != null) {
                optimizedCoordinatesArea.setText(optimizedPath);
                if (optimizedCoordinatesArea.getParent() instanceof JViewport) {
                    optimizedCoordinatesArea.getParent().getParent().setVisible(true);
                }
                if (optimizedCoordinatesLabel != null) {
                    int count = 0;
                    if (optimizedPath != null && !optimizedPath.trim().isEmpty()) {
                        count = optimizedPath.split(";").length;
                    }
                    optimizedCoordinatesLabel.setText("优化后路径坐标 (共" + count + "个点)");
                    optimizedCoordinatesLabel.setVisible(true);
                }
            }
            // å¯ç”¨ä¿å­˜æŒ‰é’®
            if (saveBtn != null) {
                saveBtn.setEnabled(true);
            }
            // åˆ·æ–°ç•Œé¢
            if (mainPanel != null) {
                mainPanel.revalidate();
                mainPanel.repaint();
            }
        } catch (Exception e) {
            JOptionPane.showMessageDialog(this, "路径生成失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
        }
    }
    /**
     * é¢„览路径
     */
    private void previewPath() {
        if (optimizedPathCoordinates == null || optimizedPathCoordinates.trim().isEmpty()) {
            JOptionPane.showMessageDialog(this, "请先生成路径", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        zhuye.Shouye shouye = zhuye.Shouye.getInstance();
        if (shouye != null) {
            // éšè—å½“前窗口
            setVisible(false);
            // å¯åŠ¨é¢„è§ˆ
            shouye.startReturnPathPreview(optimizedPathCoordinates, () -> {
                // è¿”回回调:显示当前窗口
                setVisible(true);
            });
        }
    }
    /**
     * ä¿å­˜è·¯å¾„
     */
    private void savePath() {
        if (optimizedPathCoordinates == null || optimizedPathCoordinates.trim().isEmpty()) {
            JOptionPane.showMessageDialog(this, "请先生成路径", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        savePathCoordinates();
    }
    /**
     * æ˜¾ç¤ºç»˜åˆ¶å¾€è¿”路径对话框
     */
    public static void showDrawReturnPathDialog(Window parent, Dikuai dikuai) {
        showDrawReturnPathDialog(parent, dikuai, false);
    }
    /**
     * æ˜¾ç¤ºç»˜åˆ¶å¾€è¿”路径对话框
     * @param parent çˆ¶çª—口
     * @param dikuai åœ°å—信息
     * @param isRefresh æ˜¯å¦é‡æ–°ç»˜åˆ¶ï¼ˆå¦‚果已有坐标)
     */
    public static void showDrawReturnPathDialog(Window parent, Dikuai dikuai, boolean isRefresh) {
        Huizhiwanfanpath dialog = new Huizhiwanfanpath(parent, dikuai, isRefresh);
        dialog.setVisible(true);
    }
}
src/dikuai/ObstacleManagementPage.java
@@ -20,6 +20,8 @@
/**
 * éšœç¢ç‰©ç®¡ç†é¡µé¢ - UI优化版
 */
import publicway.Gpstoxuzuobiao;
public class ObstacleManagementPage extends JDialog {
    private static final long serialVersionUID = 1L;
    
@@ -904,29 +906,11 @@
    }
    private double parseDMToDecimal(String dmm, String direction) {
        if (dmm == null || dmm.trim().isEmpty()) return Double.NaN;
        try {
            String trimmed = dmm.trim();
            int dotIndex = trimmed.indexOf('.');
            if (dotIndex < 2) return Double.NaN;
            int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2));
            double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2));
            double decimal = degrees + minutes / 60.0;
            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) decimal = -decimal;
            return decimal;
        } catch (NumberFormatException ex) {
            return Double.NaN;
        }
        return Gpstoxuzuobiao.parseDMToDecimal(dmm, direction);
    }
    
    private double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
        double deltaLat = lat - baseLat;
        double deltaLon = lon - baseLon;
        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
        double METERS_PER_DEGREE_LAT = 111320.0;
        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
        return new double[]{eastMeters, northMeters};
        return Gpstoxuzuobiao.convertLatLonToLocal(lat, lon, baseLat, baseLon);
    }
    
    private void deleteObstacle(Obstacledge.Obstacle obstacle) {
src/dikuai/Wangfanpathpage.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,200 @@
package dikuai;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import publicway.Fuzhibutton;
import zhuye.Shouye;
/**
 * æŸ¥çœ‹/编辑往返点路径页面
 * ç‹¬ç«‹çš„对话框类,用于显示和编辑往返路径
 */
public class Wangfanpathpage 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;
    public Wangfanpathpage(Window owner, String title, String initialValue, Dikuai dikuai) {
        super(owner, title, Dialog.ModalityType.APPLICATION_MODAL);
        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));
        // 1. åŽŸå§‹å¾€è¿”è·¯å¾„åæ ‡åŒºåŸŸ
        String rawCoords = dikuai != null ? prepareCoordinateForEditor(dikuai.getReturnPathRawCoordinates()) : "";
        int rawCount = 0;
        if (rawCoords != null && !rawCoords.isEmpty() && !"-1".equals(rawCoords)) {
            rawCount = rawCoords.split(";").length;
        }
        JTextArea rawTextArea = createInfoTextArea(rawCoords, false, 4);
        contentPanel.add(createTextAreaSection("原始往返路径坐标 (" + rawCount + "点)", rawTextArea));
        // 2. ä¼˜åŒ–后往返路径坐标区域
        String optCoords = prepareCoordinateForEditor(initialValue);
        int optCount = 0;
        if (optCoords != null && !optCoords.isEmpty() && !"-1".equals(optCoords)) {
            optCount = optCoords.split(";").length;
        }
        JTextArea optTextArea = createInfoTextArea(optCoords, true, 4);
        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 goDrawBtn = createPrimaryFooterButton("去绘制");
        JButton closeBtn = createPrimaryFooterButton("关闭");
        goDrawBtn.addActionListener(e -> {
            String currentText = optTextArea.getText();
            if (currentText != null && !currentText.trim().isEmpty() && !"-1".equals(currentText.trim())) {
                result = "__OPEN_DRAW_PAGE_REFRESH__";
            } else {
                result = "__OPEN_DRAW_PAGE__";
            }
            dispose();
        });
        closeBtn.addActionListener(e -> dispose());
        buttonPanel.add(goDrawBtn);
        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);
        // åˆ›å»ºå¤åˆ¶æŒ‰é’®ï¼ˆä½¿ç”¨ Fuzhibutton)
        JButton copyButton = Fuzhibutton.createCopyButton(
            () -> {
                String text = textArea.getText();
                if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) {
                    return null; // è¿”回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;
    }
}
src/dikuai/addzhangaiwu.java
@@ -46,6 +46,7 @@
import baseStation.BaseStation;
import gecaoji.Device;
import publicway.buttonset;
import set.Setsys;
import ui.UIConfig;
import zhuye.Coordinate;
@@ -54,13 +55,14 @@
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
import zhangaiwu.yulanzhangaiwu;
import zhuye.buttonset;
import bianjie.bianjieguihua2;
import bianjie.ThreePointCircle;
/**
 * éšœç¢ç‰©æ–°å¢ž/编辑对话框。设计语言参考 {@link AddDikuai},支持通过实地绘制采集障碍物坐标。
 */
import publicway.Gpstoxuzuobiao;
public class addzhangaiwu extends JDialog {
    private static final long serialVersionUID = 1L;
    private static final double METERS_PER_DEGREE_LAT = 111320.0d;
@@ -1217,34 +1219,11 @@
    }
    private static double parseDMToDecimal(String dmm, String direction) {
        if (dmm == null || dmm.trim().isEmpty()) {
            return Double.NaN;
        }
        try {
            String trimmed = dmm.trim();
            int dotIndex = trimmed.indexOf('.');
            if (dotIndex < 2) {
                return Double.NaN;
            }
            int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2));
            double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2));
            double decimal = degrees + minutes / 60.0;
            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) {
                decimal = -decimal;
            }
            return decimal;
        } catch (NumberFormatException ex) {
            return Double.NaN;
        }
        return Gpstoxuzuobiao.parseDMToDecimal(dmm, direction);
    }
    private static double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
        double deltaLat = lat - baseLat;
        double deltaLon = lon - baseLon;
        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
        return new double[]{eastMeters, northMeters};
        return Gpstoxuzuobiao.convertLatLonToLocal(lat, lon, baseLat, baseLon);
    }
    private static CircleFitResult fitCircleFromPoints(List<double[]> points) {
src/dikuai/daohangyulan.java
@@ -9,9 +9,9 @@
import java.util.ArrayList;
import zhuye.Shouye;
import zhuye.MapRenderer;
import zhuye.buttonset;
import gecaoji.Gecaoji;
import gecaoji.lujingdraw;
import publicway.buttonset;
/**
 * å¯¼èˆªé¢„览功能类
src/gecaoji/Device.java
@@ -1,6 +1,7 @@
package gecaoji;
import baseStation.BaseStation;
import set.Setsys;
import zhuye.MowerLocationData;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -436,6 +437,12 @@
        }
        positioningStatus = defaultIfEmpty(sanitizeField(fields, 6));
        // åŒæ­¥åˆ°ç»˜åˆ¶æ¨¡å—的数据源,保证往返绘制定时器能识别定位质量
        try {
            MowerLocationData.updateProperty("positioningQuality", positioningStatus);
        } catch (Throwable ignored) {
            // é˜²å¾¡å¼ï¼šå³ä½¿æ›´æ–°å¤±è´¥ä¹Ÿä¸å½±å“è®¾å¤‡æ•°æ®å¤„理
        }
        satelliteCount = defaultIfEmpty(sanitizeField(fields, 7));
        differentialAge = defaultIfEmpty(sanitizeField(fields, 13));
        battery = defaultIfEmpty(sanitizeField(fields, 16));
@@ -485,6 +492,12 @@
        }
        positioningStatus = defaultIfEmpty(sanitizeField(fields, 6));
        // åŒæ­¥åˆ°ç»˜åˆ¶æ¨¡å—的数据源,保证往返绘制定时器能识别定位质量
        try {
            MowerLocationData.updateProperty("positioningQuality", positioningStatus);
        } catch (Throwable ignored) {
            // é˜²å¾¡å¼ï¼šå³ä½¿æ›´æ–°å¤±è´¥ä¹Ÿä¸å½±å“è®¾å¤‡æ•°æ®å¤„理
        }
        satelliteCount = defaultIfEmpty(sanitizeField(fields, 7));
        differentialAge = defaultIfEmpty(sanitizeField(fields, 13));       
        realtimeSpeed ="0";        
@@ -546,6 +559,9 @@
        if (Double.isFinite(eastMeters) && Double.isFinite(northMeters)) {
            realtimeX = formatMeters(eastMeters);
            realtimeY = formatMeters(northMeters);
            // ä¿å­˜åæ ‡åˆ°å·¥å…·ç±»
            lujing.SavaXyZuobiao.addCoordinate(eastMeters, northMeters);
        }
    }
src/gecaoji/GecaojiMeg.java
@@ -1,12 +1,14 @@
package gecaoji;
import javax.swing.*;
import publicway.buttonset;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.SimpleDateFormat;
import java.util.Date;
import zhuye.buttonset;
// Manages the mower info dialog to keep MapRenderer focused on rendering.
public class GecaojiMeg {
src/lujing/MowingPathGenerationPage.java
@@ -16,12 +16,12 @@
import lujing.Qufenxingzhuang;
import lujing.AoxinglujingNoObstacle;
import lujing.YixinglujingNoObstacle;
import publicway.Fuzhibutton;
import lujing.AoxinglujingHaveObstacel;
import lujing.YixinglujingHaveObstacel;
import org.locationtech.jts.geom.Coordinate;
import gecaoji.Device;
import java.util.Locale;
import zhuye.Fuzhibutton;
/**
 * ç”Ÿæˆå‰²è‰è·¯å¾„页面
src/publicway/Fuzhibutton.java
ÎļþÃû´Ó src/zhuye/Fuzhibutton.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package zhuye;
package publicway;
import javax.swing.*;
import java.awt.*;
src/publicway/Gpstoxuzuobiao.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
package publicway;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class Gpstoxuzuobiao {
    private static final double METERS_PER_DEGREE_LAT = 111320.0d;
    // ç¼“存基准站坐标
    private static double cachedBaseLat = 0.0;
    private static double cachedBaseLon = 0.0;
    private static boolean baseStationLoaded = false;
    /**
     * è§£æžGNGGA数据并转换为XY坐标
     * @param gnggaData $GNGGA数据
     * @return double[]{x, y} ç›¸å¯¹åæ ‡ï¼Œå¦‚果解析失败或无基站数据返回null
     */
    public static double[] processGNGGAToXY(String gnggaData) {
        ensureBaseStationLoaded();
        if (!baseStationLoaded) {
            return null;
        }
        return processGNGGAToXY(gnggaData, cachedBaseLat, cachedBaseLon);
    }
    /**
     * è§£æžGNGGA数据并转换为XY坐标
     * @param gnggaData $GNGGA数据
     * @param baseLat åŸºå‡†ç«™çº¬åº¦
     * @param baseLon åŸºå‡†ç«™ç»åº¦
     * @return double[]{x, y} ç›¸å¯¹åæ ‡
     */
    public static double[] processGNGGAToXY(String gnggaData, double baseLat, double baseLon) {
        if (gnggaData == null || !gnggaData.contains("$GNGGA")) {
            return null;
        }
        // ç®€å•的解析逻辑,提取经纬度
        // æ ¼å¼: $GNGGA,hhmmss.ss,lat,latDir,lon,lonDir,quality,sats,hdop,alt,units,sep,units,age,refID*cs
        String[] parts = gnggaData.split(",");
        // æ‰¾åˆ°$GNGGA的位置
        int index = -1;
        for(int i=0; i<parts.length; i++) {
            if (parts[i].contains("$GNGGA")) {
                index = i;
                break;
            }
        }
        // ç¡®ä¿æœ‰è¶³å¤Ÿçš„字段: lat(2), latDir(3), lon(4), lonDir(5)
        if (index == -1 || index + 5 >= parts.length) {
            return null;
        }
        String latStr = parts[index + 2];
        String latDir = parts[index + 3];
        String lonStr = parts[index + 4];
        String lonDir = parts[index + 5];
        if (latStr.isEmpty() || lonStr.isEmpty()) {
            return null;
        }
        double lat = parseDMToDecimal(latStr, latDir);
        double lon = parseDMToDecimal(lonStr, lonDir);
        return convertLatLonToLocal(lat, lon, baseLat, baseLon);
    }
    /**
     * å°†åº¦åˆ†æ ¼å¼(DMM)转换为十进制格式(DD)
     * @param dmm åº¦åˆ†æ ¼å¼å­—符串 (e.g. "3015.1234")
     * @param direction æ–¹å‘ (N/S/E/W)
     * @return åè¿›åˆ¶ç»çº¬åº¦
     */
    public static double parseDMToDecimal(String dmm, String direction) {
        if (dmm == null || dmm.isEmpty()) {
            return 0.0;
        }
        try {
            double val = Double.parseDouble(dmm);
            int degrees = (int) (val / 100);
            double minutes = val - degrees * 100;
            double decimal = degrees + minutes / 60.0;
            if (direction != null && (direction.equalsIgnoreCase("S") || direction.equalsIgnoreCase("W"))) {
                decimal = -decimal;
            }
            return decimal;
        } catch (NumberFormatException e) {
            return 0.0;
        }
    }
    /**
     * å°†ç»çº¬åº¦è½¬æ¢ä¸ºç›¸å¯¹åæ ‡(XY)
     * @param lat ç›®æ ‡çº¬åº¦
     * @param lon ç›®æ ‡ç»åº¦
     * @param baseLat åŸºå‡†ç«™çº¬åº¦
     * @param baseLon åŸºå‡†ç«™ç»åº¦
     * @return double[]{x, y} (单位: ç±³)
     */
    public static double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
        double deltaLat = lat - baseLat;
        double deltaLon = lon - baseLon;
        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
        return new double[]{eastMeters, northMeters};
    }
    private static void ensureBaseStationLoaded() {
        if (baseStationLoaded) return;
        Properties props = new Properties();
        try (FileInputStream input = new FileInputStream("basestation.properties")) {
            props.load(input);
            String coords = props.getProperty("installationCoordinates");
            if (coords != null && !coords.isEmpty() && !"-1".equals(coords)) {
                String[] parts = coords.split(",");
                if (parts.length >= 4) {
                    cachedBaseLat = parseDMToDecimal(parts[0], parts[1]);
                    cachedBaseLon = parseDMToDecimal(parts[2], parts[3]);
                    baseStationLoaded = true;
                }
            }
        } catch (IOException e) {
            // ignore
        }
    }
    /**
     * é‡æ–°åŠ è½½åŸºå‡†ç«™ä¿¡æ¯
     */
    public static void reloadBaseStation() {
        baseStationLoaded = false;
        ensureBaseStationLoaded();
    }
}
src/publicway/Lookbutton.java
ÎļþÃû´Ó src/zhuye/Lookbutton.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package zhuye;
package publicway;
import javax.swing.*;
import java.awt.*;
@@ -105,3 +105,10 @@
    }
}
src/publicway/buttonset.java
ÎļþÃû´Ó src/zhuye/buttonset.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package zhuye;
package publicway;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
src/set/Sets.java
@@ -3,10 +3,9 @@
import baseStation.BaseStation;
import gecaoji.Device;
import gecaoji.MowerSafetyDistanceCalculator;
import publicway.buttonset;
import zhuye.MapRenderer;
import zhuye.Shouye;
import zhuye.buttonset;
import zhuye.celiangmoshi;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
src/set/debug.java
@@ -4,6 +4,8 @@
import chuankou.SerialPortPreferences;
import chuankou.SerialPortService;
import chuankou.sendmessage;
import publicway.buttonset;
import com.fazecast.jSerialComm.SerialPort;
import ui.UIConfig;
import javax.swing.*;
@@ -16,7 +18,6 @@
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import zhuye.buttonset;
/**
 * ç³»ç»Ÿè°ƒè¯•对话框。
src/udpdell/UDPServer.java
@@ -10,6 +10,8 @@
import gecaoji.Device;
import zhuye.Coordinate;
import publicway.Gpstoxuzuobiao;
public class UDPServer {
    private static final int PORT = 7000; // é»˜è®¤UDP监听端口
    private static final int BUFFER_SIZE = 65507; // UDP最大包大小
@@ -87,6 +89,14 @@
        }
        int sequence = incrementReceivedPacketCounter();
        System.out.println("收到了差分数据(" + sequence + "):" + message);
        // ä½¿ç”¨Gpstoxuzuobiao处理并获取XY坐标
        double[] xy = Gpstoxuzuobiao.processGNGGAToXY(message);
        if (xy != null) {
            // è¿™é‡Œå¯ä»¥å°†XY坐标传递给其他方法使用
            // System.out.println("UDP GNGGA -> XY: " + xy[0] + ", " + xy[1]);
        }
        Coordinate.parseGNGGAToCoordinateList(message);
        int count = Coordinate.coordinates.size();
        System.out.println("savenum:" + count);
@@ -110,6 +120,14 @@
        }
        int sequence = incrementReceivedPacketCounter();
        System.out.println("收到了串口数据(" + sequence + "):" + message);
        // ä½¿ç”¨Gpstoxuzuobiao处理并获取XY坐标
        double[] xy = Gpstoxuzuobiao.processGNGGAToXY(message);
        if (xy != null) {
            // è¿™é‡Œå¯ä»¥å°†XY坐标传递给其他方法使用
            // System.out.println("Serial GNGGA -> XY: " + xy[0] + ", " + xy[1]);
        }
        Coordinate.dellchuankougngga(message);
        int count = Coordinate.coordinates.size();
        System.out.println("savenum:" + count);
src/yaokong/Control02.java
@@ -3,6 +3,8 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import publicway.Gpstoxuzuobiao;
public class Control02 {
    
    /**
@@ -98,29 +100,7 @@
     * ä¾‹å¦‚:3949.90238860 -> 39.83170647666667
     */
    private static double parseDMToDecimal(String dmm, String direction) {
        try {
            // æ‰¾åˆ°å°æ•°ç‚¹çš„位置
            int dotIndex = dmm.indexOf('.');
            if (dotIndex < 2) {
                throw new IllegalArgumentException("度分格式错误: " + dmm);
            }
            // æå–度和分
            int degrees = Integer.parseInt(dmm.substring(0, dotIndex - 2));
            double minutes = Double.parseDouble(dmm.substring(dotIndex - 2));
            // è½¬æ¢ä¸ºåè¿›åˆ¶
            double decimal = degrees + minutes / 60.0;
            // æ ¹æ®æ–¹å‘调整正负
            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) {
                decimal = -decimal;
            }
            return decimal;
        } catch (Exception e) {
            throw new IllegalArgumentException("坐标格式解析错误: " + dmm, e);
        }
        return Gpstoxuzuobiao.parseDMToDecimal(dmm, direction);
    }
    
    private static String bytesToHex(byte[] bytes) {
src/zhangaiwu/AddDikuai.java
@@ -27,12 +27,12 @@
import dikuai.Dikuaiguanli;
import bianjie.bianjieguihua2;
import lujing.Lunjingguihua;
import publicway.buttonset;
import set.Setsys;
import ui.UIConfig;
import zhuye.MowerLocationData;
import zhuye.Shouye;
import zhuye.Coordinate;
import zhuye.buttonset;
import gecaoji.Device;
/**
src/zhangaiwu/yulanzhangaiwu.java
@@ -12,6 +12,7 @@
import java.util.List;
import java.util.Locale;
import javax.swing.JDialog;
import javax.swing.SwingUtilities;
import zhuye.Coordinate;
@@ -20,7 +21,9 @@
/**
 * åœ¨åœ°å›¾ä¸Šå®žæ—¶é¢„览正在绘制的障碍物。
 */
public final class yulanzhangaiwu {
import publicway.Gpstoxuzuobiao;
public class yulanzhangaiwu extends JDialog {
    private static final Object LOCK = new Object();
    private static final double METERS_PER_DEGREE_LAT = 111320.0d;
    private static final Color PREVIEW_LINE_COLOR = new Color(0, 123, 255, 200);
@@ -229,34 +232,11 @@
    }
    private static double parseDMToDecimal(String dmm, String direction) {
        if (dmm == null || dmm.trim().isEmpty()) {
            return Double.NaN;
        }
        try {
            String trimmed = dmm.trim();
            int dotIndex = trimmed.indexOf('.');
            if (dotIndex < 2) {
                return Double.NaN;
            }
            int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2));
            double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2));
            double decimal = degrees + minutes / 60.0;
            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) {
                decimal = -decimal;
            }
            return decimal;
        } catch (NumberFormatException ex) {
            return Double.NaN;
        }
        return Gpstoxuzuobiao.parseDMToDecimal(dmm, direction);
    }
    private static double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
        double deltaLat = lat - baseLat;
        double deltaLon = lon - baseLon;
        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
        return new double[]{eastMeters, northMeters};
        return Gpstoxuzuobiao.convertLatLonToLocal(lat, lon, baseLat, baseLon);
    }
    private static CircleState fitCircle(List<double[]> points) {
src/zhuye/AreaSelectionDialog.java
@@ -3,9 +3,11 @@
import ui.UIConfig;
import javax.swing.*;
import publicway.buttonset;
import java.awt.*;
import java.awt.event.*;
import zhuye.buttonset;
public class AreaSelectionDialog extends JDialog {
    private final Color THEME_COLOR;
src/zhuye/LegendDialog.java
@@ -48,6 +48,7 @@
            {"路径方向", "255,0,0", "arrow"},
            {"障碍物区域", "255,0,0", "obstacle_fill"},
            {"割草机位置", "0,150,0", "mow"},
            {"往返路径", "0,0,0", "railway"},
            {"割草轨迹", "100,150,200", "trail"}
        };
        
@@ -120,11 +121,30 @@
                        g2d.fillPolygon(xPoints, yPoints, 3);
                        break;
                    case "mow":
                        g2d.setColor(itemColor);
                        g2d.fillOval(x, y, size, size);
                        g2d.setColor(new Color(100, 100, 100));
                        g2d.setStroke(new BasicStroke(1));
                        g2d.drawOval(x, y, size, size);
                        ImageIcon icon = loadIcon("image/gecaoji.png", size + 8, size + 8);
                        if (icon != null) {
                            icon.paintIcon(this, g2d, x - 4, y - 4);
                        } else {
                            g2d.setColor(itemColor);
                            g2d.fillOval(x, y, size, size);
                            g2d.setColor(new Color(100, 100, 100));
                            g2d.setStroke(new BasicStroke(1));
                            g2d.drawOval(x, y, size, size);
                        }
                        break;
                    case "railway":
                        // 1. ç»˜åˆ¶åº•层黑色实线
                        g2d.setColor(Color.BLACK);
                        g2d.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                        g2d.drawLine(x, y + size/2, x + size, y + size/2);
                        // 2. ç»˜åˆ¶é¡¶å±‚白色虚线
                        float dashLen = 3.0f * 2.0f;
                        float dashSpace = 3.0f * 2.0f;
                        float[] dashPattern = {dashLen, dashSpace};
                        g2d.setColor(Color.WHITE);
                        g2d.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 10.0f, dashPattern, 0.0f));
                        g2d.drawLine(x, y + size/2, x + size, y + size/2);
                        break;
                    case "trail":
                        g2d.setColor(itemColor);
src/zhuye/MapRenderer.java
@@ -107,6 +107,9 @@
    private boolean idleTrailSuppressed;
    private Path2D.Double realtimeBoundaryPathCache;
    private String realtimeBoundaryPathLand;
    private WangfanDraw returnPathDrawer;  // å¾€è¿”路径绘制管理器
    private List<Point2D.Double> currentReturnPath; // å½“前地块的往返路径(用于显示)
    private List<Point2D.Double> previewReturnPath; // é¢„览的往返路径
    private static final double TRACK_SAMPLE_MIN_DISTANCE_METERS = 0.2d;
    private static final double TRACK_DUPLICATE_TOLERANCE_METERS = 1e-3d;
@@ -459,6 +462,17 @@
            drawNavigationPreviewCoverage(g2d);
        }
        // å…ˆç”»å¾€è¿”路径(线+点),保证割草机图标在其上方
        if (returnPathDrawer != null && returnPathDrawer.isActive()) {
            returnPathDrawer.draw(g2d, scale);
        } else if (previewReturnPath != null && !previewReturnPath.isEmpty()) {
            // ç»˜åˆ¶é¢„览的往返路径(铁线路图风格)
            WangfanDraw.drawRailwayPath(g2d, previewReturnPath, scale);
        } else if (currentReturnPath != null && !currentReturnPath.isEmpty()) {
            // ç»˜åˆ¶ä¿å­˜çš„往返路径(铁线路图风格)
            WangfanDraw.drawRailwayPath(g2d, currentReturnPath, scale);
        }
        drawMower(g2d);
        
        // ç»˜åˆ¶å¯¼èˆªé¢„览速度(如果正在导航预览)
@@ -962,6 +976,14 @@
            mowerEffectiveWidthMeters = defaultMowerWidthMeters;
        }
        // åŠ è½½å¾€è¿”è·¯å¾„
        String returnPathStr = dikuai != null ? dikuai.getReturnPathCoordinates() : null;
        if (returnPathStr != null && !returnPathStr.isEmpty() && !"-1".equals(returnPathStr)) {
            currentReturnPath = lujingdraw.parsePlannedPath(returnPathStr);
        } else {
            currentReturnPath = null;
        }
        loadRealtimeTrack(landNumber, dikuai != null ? dikuai.getMowingTrack() : null);
        visualizationPanel.repaint();
    }
@@ -2993,5 +3015,63 @@
    public Gecaoji getMower() {
        return mower;
    }
    /**
     * è®¾ç½®å¾€è¿”路径绘制管理器
     */
    public void setReturnPathDrawer(WangfanDraw drawer) {
        this.returnPathDrawer = drawer;
    }
    /**
     * è®¾ç½®é¢„览的往返路径
     */
    public void setPreviewReturnPath(List<Point2D.Double> path) {
        this.previewReturnPath = path;
        if (visualizationPanel != null) {
            visualizationPanel.repaint();
        }
    }
    /**
     * å¼€å§‹å¾€è¿”路径绘制
     */
    public void startReturnPathDrawing() {
        if (returnPathDrawer != null) {
            // ç¦ç”¨æ‹–尾效果(在往返路径绘制模式下不显示实时轨迹拖尾)
            idleTrailSuppressed = true;
            clearIdleMowerTrail();
            // æ¸…空之前的路径点(通过 WangfanDraw ç®¡ç†ï¼‰
            repaint();
        }
    }
    /**
     * åœæ­¢å¾€è¿”路径绘制
     */
    public void stopReturnPathDrawing() {
        // æ¢å¤æ‹–尾效果
        idleTrailSuppressed = false;
        repaint();
    }
    /**
     * æ·»åŠ å¾€è¿”è·¯å¾„ç‚¹ï¼ˆå·²åºŸå¼ƒï¼Œè·¯å¾„ç‚¹ç”± WangfanDraw ç›´æŽ¥ç®¡ç†ï¼‰
     */
    @Deprecated
    public void addReturnPathPoint(double x, double y) {
        // è·¯å¾„点由 WangfanDraw ç›´æŽ¥ç®¡ç†ï¼Œè¿™é‡Œåªéœ€è¦é‡ç»˜
        repaint();
    }
    /**
     * èŽ·å–å¾€è¿”è·¯å¾„ç‚¹åˆ—è¡¨çš„å¿«ç…§
     */
    public List<Point2D.Double> getReturnPathPointsSnapshot() {
        if (returnPathDrawer != null) {
            return returnPathDrawer.getPointsSnapshot();
        }
        return new ArrayList<>();
    }
}
src/zhuye/Shouye.java
@@ -19,23 +19,23 @@
import gecaoji.Device;
import gecaoji.Gecaoji;
import gecaoji.MowerBoundaryChecker;
import publicway.buttonset;
import set.Sets;
import set.debug;
import udpdell.UDPServer;
import zhangaiwu.AddDikuai;
import yaokong.Control04;
import yaokong.RemoteControlDialog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.awt.geom.Point2D;
import publicway.Gpstoxuzuobiao;
/**
 * é¦–页界面 - é€‚配6.5寸竖屏,使用独立的MapRenderer进行绘制
 */
@@ -107,12 +107,14 @@
    // åœ°å›¾æ¸²æŸ“器
    private MapRenderer mapRenderer;
    
    private boolean pathPreviewActive;
    private final Consumer<String> serialLineListener = line -> {
        SwingUtilities.invokeLater(() -> {
            updateDataPacketCountLabel();
            // å¦‚果收到$GNGGA数据,立即更新拖尾
            if (line != null && line.trim().startsWith("$GNGGA")) {
                if (mapRenderer != null) {
                if (mapRenderer != null && !pathPreviewActive) {
                    mapRenderer.forceUpdateIdleMowerTrail();
                }
            }
@@ -125,7 +127,6 @@
    private JPanel floatingButtonColumn;
    private Runnable endDrawingCallback;
    private JButton pathPreviewReturnButton;
    private boolean pathPreviewActive;
    private Runnable pathPreviewReturnAction;
    private JButton settingsReturnButton;  // è¿”回系统设置页面的悬浮按钮
    private JButton saveManualBoundaryButton;  // ä¿å­˜æ‰‹åŠ¨ç»˜åˆ¶è¾¹ç•Œçš„æŒ‰é’®
@@ -170,6 +171,7 @@
    private boolean storedStopButtonActive;
    private String storedStatusBeforeDrawing;
    private boolean handheldCaptureInlineUiActive;
    private WangfanDraw returnPathDrawer;  // å¾€è¿”路径绘制管理器
    private Timer handheldCaptureStatusTimer;
    private String handheldCaptureStoredStatusText;
    private Color handheldStartButtonOriginalBackground;
@@ -209,6 +211,54 @@
        // åˆå§‹åŒ–地图渲染器
        mapRenderer = new MapRenderer(visualizationPanel);
        applyIdleTrailDurationFromSettings();
        // åˆå§‹åŒ–往返路径绘制管理器
        returnPathDrawer = new WangfanDraw(this, mapRenderer, new WangfanDraw.DrawingHelper() {
            @Override
            public double[] resolveBaseLatLon() {
                return resolveCircleBaseLatLon();
            }
            @Override
            public Coordinate getLatestCoordinate() {
                return Shouye.this.getLatestCoordinate();
            }
            @Override
            public double parseDMToDecimal(String dmm, String direction) {
                return Shouye.this.parseDMToDecimal(dmm, direction);
            }
            @Override
            public double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
                return Shouye.this.convertLatLonToLocal(lat, lon, baseLat, baseLon);
            }
            @Override
            public boolean arePointsClose(Point2D.Double a, Point2D.Double b) {
                return Shouye.this.arePointsClose(a, b);
            }
            @Override
            public void enterDrawingControlMode() {
                Shouye.this.enterDrawingControlMode();
            }
            @Override
            public void exitDrawingControlMode() {
                Shouye.this.exitDrawingControlMode();
            }
            @Override
            public boolean isDrawingPaused() {
                return drawingPaused;
            }
        });
        // è®¾ç½® MapRenderer çš„往返路径绘制管理器
        if (mapRenderer != null) {
            mapRenderer.setReturnPathDrawer(returnPathDrawer);
        }
        // åˆå§‹åŒ–对话框引用为null,延迟创建
        legendDialog = null;
@@ -1839,7 +1889,11 @@
    }
    private void handleDrawingStopFromControlPanel() {
        if (endDrawingCallback != null) {
        // å¦‚果是往返路径绘制模式,调用完成绘制回调
        if (returnPathDrawer != null && returnPathDrawer.isActive()) {
            returnPathDrawer.stop();
            returnPathDrawer.executeFinishCallback();
        } else if (endDrawingCallback != null) {
            endDrawingCallback.run();
        } else {
            addzhangaiwu.finishDrawingSession();
@@ -2086,7 +2140,8 @@
            startBtn.setText(drawingPaused ? "开始绘制" : "暂停绘制");
        }
        if (stopBtn != null) {
            stopBtn.setText("结束绘制");
            // å¦‚果是往返路径绘制模式,显示"完成绘制",否则显示"结束绘制"
            stopBtn.setText((returnPathDrawer != null && returnPathDrawer.isActive()) ? "完成绘制" : "结束绘制");
        }
    }
@@ -3645,34 +3700,11 @@
    }
    private double parseDMToDecimal(String dmm, String direction) {
        if (dmm == null || dmm.trim().isEmpty()) {
            return Double.NaN;
        }
        try {
            String trimmed = dmm.trim();
            int dotIndex = trimmed.indexOf('.');
            if (dotIndex < 2) {
                return Double.NaN;
            }
            int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2));
            double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2));
            double decimal = degrees + minutes / 60.0;
            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) {
                decimal = -decimal;
            }
            return decimal;
        } catch (NumberFormatException ex) {
            return Double.NaN;
        }
        return Gpstoxuzuobiao.parseDMToDecimal(dmm, direction);
    }
    private double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
        double deltaLat = lat - baseLat;
        double deltaLon = lon - baseLon;
        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
        return new double[]{eastMeters, northMeters};
        return Gpstoxuzuobiao.convertLatLonToLocal(lat, lon, baseLat, baseLon);
    }
    private CircleSolution fitCircleFromPoints(List<double[]> points) {
@@ -4114,6 +4146,135 @@
    }
    
    /**
     * å¯åŠ¨å¾€è¿”è·¯å¾„ç»˜åˆ¶
     * @param finishCallback å®Œæˆç»˜åˆ¶æ—¶çš„回调
     * @return æ˜¯å¦æˆåŠŸå¯åŠ¨
     */
    public boolean startReturnPathDrawing(Runnable finishCallback) {
        if (returnPathDrawer == null) {
            return false;
        }
        return returnPathDrawer.start(finishCallback);
    }
    /**
     * åœæ­¢å¾€è¿”路径绘制
     */
    public void stopReturnPathDrawing() {
        if (returnPathDrawer != null) {
            returnPathDrawer.stop();
        }
    }
    /**
     * èŽ·å–å¾€è¿”è·¯å¾„ç»˜åˆ¶ç®¡ç†å™¨
     */
    public WangfanDraw getReturnPathDrawer() {
        return returnPathDrawer;
    }
    /**
     * å¯åŠ¨å¾€è¿”è·¯å¾„é¢„è§ˆ
     * @param coordinatesStr è·¯å¾„坐标字符串 (x,y;x,y)
     * @param returnCallback è¿”回回调
     */
    public void startReturnPathPreview(String coordinatesStr, Runnable returnCallback) {
        if (returnPathDrawer == null || coordinatesStr == null || coordinatesStr.isEmpty()) {
            return;
        }
        // è§£æžåæ ‡
        List<Point2D.Double> points = new ArrayList<>();
        String[] pairs = coordinatesStr.split(";");
        for (String pair : pairs) {
            String[] xy = pair.split(",");
            if (xy.length == 2) {
                try {
                    double x = Double.parseDouble(xy[0]);
                    double y = Double.parseDouble(xy[1]);
                    points.add(new Point2D.Double(x, y));
                } catch (NumberFormatException e) {
                    // å¿½ç•¥æ— æ•ˆåæ ‡
                }
            }
        }
        if (points.isEmpty()) {
            JOptionPane.showMessageDialog(this, "没有有效的路径点可预览", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        // è®¾ç½®é¢„览点
        returnPathDrawer.setPoints(points);
        if (mapRenderer != null) {
            mapRenderer.setPreviewReturnPath(points);
        }
        // å¼€å¯é¢„览模式
        pathPreviewActive = true;
        if (mapRenderer != null) {
            mapRenderer.clearIdleTrail();
        }
        pathPreviewReturnAction = returnCallback;
        // ç¡®ä¿æ‚¬æµ®æŒ‰é’®åŸºç¡€è®¾æ–½å·²åˆ›å»º
        ensureFloatingButtonInfrastructure();
        // åˆ›å»ºæˆ–显示返回按钮
        if (pathPreviewReturnButton == null) {
            pathPreviewReturnButton = publicway.buttonset.createStyledButton("返回", null);
            pathPreviewReturnButton.setToolTipText("返回绘制页面");
            pathPreviewReturnButton.addActionListener(e -> {
                // åœæ­¢é¢„览
                stopReturnPathPreview();
            });
        }
        // éšè—å…¶ä»–悬浮按钮
        hideFloatingDrawingControls();
        // æ˜¾ç¤ºè¿”回按钮
        pathPreviewReturnButton.setVisible(true);
        floatingButtonPanel.setVisible(true);
        if (floatingButtonPanel.getParent() != visualizationPanel) {
            visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH);
        }
        rebuildFloatingButtonColumn();
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
    }
    /**
     * åœæ­¢å¾€è¿”路径预览
     */
    private void stopReturnPathPreview() {
        pathPreviewActive = false;
        // æ¸…空预览点
        if (returnPathDrawer != null) {
            returnPathDrawer.clearPoints();
        }
        if (mapRenderer != null) {
            mapRenderer.setPreviewReturnPath(null);
        }
        // éšè—è¿”回按钮
        if (pathPreviewReturnButton != null) {
            pathPreviewReturnButton.setVisible(false);
        }
        // éšè—æ‚¬æµ®é¢æ¿
        if (floatingButtonPanel != null) {
            floatingButtonPanel.setVisible(false);
        }
        // æ‰§è¡Œè¿”回回调
        if (pathPreviewReturnAction != null) {
            pathPreviewReturnAction.run();
        }
    }
    // æµ‹è¯•方法
    public static void main(String[] args) {
src/zhuye/WangfanDraw.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,411 @@
package zhuye;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Timer;
import gecaoji.Device;
/**
 * å¾€è¿”路径绘制管理器
 * è´Ÿè´£ç®¡ç†å¾€è¿”路径的绘制逻辑和状态
 */
public class WangfanDraw {
    // å¾€è¿”路径点列表
    private final List<Point2D.Double> returnPathPoints = new ArrayList<>();
    // ç»˜åˆ¶çŠ¶æ€
    private boolean drawingActive = false;
    private boolean paused = false;
    // åŸºå‡†åæ ‡
    private double[] baseLatLon;
    // ç›‘控定时器
    private Timer monitorTimer;
    // å›žè°ƒæŽ¥å£
    private Runnable finishCallback;
    // ä¾èµ–对象
    private Shouye shouye;
    private MapRenderer mapRenderer;
    // å¾€è¿”路径绘制接口
    public interface DrawingHelper {
        double[] resolveBaseLatLon();
        Coordinate getLatestCoordinate();
        double parseDMToDecimal(String dmm, String direction);
        double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon);
        boolean arePointsClose(Point2D.Double a, Point2D.Double b);
        void enterDrawingControlMode();
        void exitDrawingControlMode();
        boolean isDrawingPaused();
    }
    private DrawingHelper helper;
    /**
     * æž„造函数
     */
    public WangfanDraw(Shouye shouye, MapRenderer mapRenderer, DrawingHelper helper) {
        this.shouye = shouye;
        this.mapRenderer = mapRenderer;
        this.helper = helper;
    }
    /**
     * å¯åŠ¨å¾€è¿”è·¯å¾„ç»˜åˆ¶
     * @param finishCallback å®Œæˆç»˜åˆ¶æ—¶çš„回调
     * @return æ˜¯å¦æˆåŠŸå¯åŠ¨
     */
    public boolean start(Runnable finishCallback) {
        if (mapRenderer == null || helper == null) {
            return false;
        }
        double[] baseLatLonCandidate = helper.resolveBaseLatLon();
        if (baseLatLonCandidate == null) {
            return false;
        }
        if (mapRenderer != null) {
            mapRenderer.clearIdleTrail();
        }
        drawingActive = true;
        paused = false;
        this.finishCallback = finishCallback;
        this.baseLatLon = baseLatLonCandidate;
        // æ¸…空路径点
        synchronized (returnPathPoints) {
            returnPathPoints.clear();
        }
        synchronized (Coordinate.coordinates) {
            Coordinate.coordinates.clear();
        }
        if (mapRenderer != null) {
            mapRenderer.startReturnPathDrawing();
        }
        Coordinate.setStartSaveGngga(true);
        if (helper != null) {
            helper.enterDrawingControlMode();
        }
        startMonitor();
        return true;
    }
    /**
     * è®¾ç½®é¢„览路径点
     * @param points è·¯å¾„点列表
     */
    public void setPoints(List<Point2D.Double> points) {
        synchronized (returnPathPoints) {
            returnPathPoints.clear();
            if (points != null) {
                returnPathPoints.addAll(points);
            }
        }
        if (mapRenderer != null) {
            mapRenderer.repaint();
        }
    }
    /**
     * æ¸…空路径点
     */
    public void clearPoints() {
        synchronized (returnPathPoints) {
            returnPathPoints.clear();
        }
        if (mapRenderer != null) {
            mapRenderer.repaint();
        }
    }
    /**
     * å¯åŠ¨ç›‘æŽ§å®šæ—¶å™¨
     */
    private void startMonitor() {
        if (monitorTimer == null) {
            monitorTimer = new Timer(600, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    pollCoordinate();
                }
            });
            monitorTimer.setRepeats(true);
        }
        if (!monitorTimer.isRunning()) {
            monitorTimer.start();
        }
        pollCoordinate();
    }
    /**
     * åœæ­¢ç›‘控定时器
     */
    private void stopMonitor() {
        if (monitorTimer != null && monitorTimer.isRunning()) {
            monitorTimer.stop();
        }
    }
    /**
     * è½®è¯¢å¾€è¿”路径坐标(只保存定位状态=4的有效坐标)
     */
    private void pollCoordinate() {
        if (!drawingActive || (helper != null && helper.isDrawingPaused())) {
            return;
        }
        // æ£€æŸ¥å®šä½çŠ¶æ€æ˜¯å¦ä¸º4(固定解)- ç»Ÿä¸€æ¥æºä¸ºè®¾å¤‡æ•°æ®
        Device device = Device.getGecaoji();
        if (device == null) {
            return;
        }
        String positioningQuality = device.getPositioningStatus();
        if (positioningQuality == null || !"4".equals(positioningQuality.trim())) {
            return;  // åªä¿å­˜å®šä½çŠ¶æ€=4的有效坐标
        }
        if (helper == null) {
            return;
        }
        Coordinate latest = helper.getLatestCoordinate();
        if (latest == null) {
            return;
        }
        if (baseLatLon == null || baseLatLon.length < 2) {
            return;
        }
        double lat = helper.parseDMToDecimal(latest.getLatitude(), latest.getLatDirection());
        double lon = helper.parseDMToDecimal(latest.getLongitude(), latest.getLonDirection());
        if (!Double.isFinite(lat) || !Double.isFinite(lon)) {
            return;
        }
        double[] local = helper.convertLatLonToLocal(lat, lon, baseLatLon[0], baseLatLon[1]);
        Point2D.Double candidate = new Point2D.Double(local[0], local[1]);
        if (!Double.isFinite(candidate.x) || !Double.isFinite(candidate.y)) {
            return;
        }
        // æ£€æŸ¥æ˜¯å¦ä¸Žä¸Šä¸€ä¸ªç‚¹å¤ªè¿‘(避免重复点)
        synchronized (returnPathPoints) {
            if (!returnPathPoints.isEmpty()) {
                Point2D.Double lastPoint = returnPathPoints.get(returnPathPoints.size() - 1);
                if (helper.arePointsClose(lastPoint, candidate)) {
                    return;  // ç‚¹å¤ªè¿‘,跳过
                }
            }
        }
        // æ·»åŠ åˆ°è·¯å¾„ç‚¹åˆ—è¡¨
        synchronized (returnPathPoints) {
            returnPathPoints.add(candidate);
        }
        // è§¦å‘重绘
        if (mapRenderer != null) {
            mapRenderer.repaint();
        }
        // åŒæ—¶ä¿å­˜åˆ° Coordinate.coordinates ç”¨äºŽåŽç»­ä¿å­˜
        synchronized (Coordinate.coordinates) {
            Coordinate.coordinates.add(latest);
        }
    }
    /**
     * åœæ­¢å¾€è¿”路径绘制
     */
    public void stop() {
        stopMonitor();
        drawingActive = false;
        paused = false;
        baseLatLon = null;
        if (mapRenderer != null) {
            mapRenderer.stopReturnPathDrawing();
        }
        Coordinate.setStartSaveGngga(false);
        if (helper != null) {
            helper.exitDrawingControlMode();
        }
    }
    /**
     * æš‚停/恢复绘制
     */
    public void setPaused(boolean paused) {
        this.paused = paused;
    }
    /**
     * æ£€æŸ¥æ˜¯å¦æ­£åœ¨ç»˜åˆ¶
     */
    public boolean isActive() {
        return drawingActive;
    }
    /**
     * æ£€æŸ¥æ˜¯å¦æš‚停
     */
    public boolean isPaused() {
        return paused;
    }
    /**
     * èŽ·å–å®Œæˆç»˜åˆ¶å›žè°ƒ
     */
    public Runnable getFinishCallback() {
        return finishCallback;
    }
    /**
     * æ‰§è¡Œå®Œæˆç»˜åˆ¶å›žè°ƒ
     */
    public void executeFinishCallback() {
        if (finishCallback != null) {
            finishCallback.run();
            finishCallback = null;
        }
    }
    /**
     * èŽ·å–è·¯å¾„ç‚¹åˆ—è¡¨çš„å¿«ç…§
     */
    public List<Point2D.Double> getPointsSnapshot() {
        synchronized (returnPathPoints) {
            return new ArrayList<>(returnPathPoints);
        }
    }
    /**
     * ç»˜åˆ¶å¾€è¿”路径
     */
    public void draw(Graphics2D g2d, double scale) {
        List<Point2D.Double> points = getPointsSnapshot();
        if (points.isEmpty()) {
            return;
        }
        // ä¿å­˜åŽŸå§‹å˜æ¢
        AffineTransform originalTransform = g2d.getTransform();
        // è®¾ç½®å¾€è¿”路径颜色
        Color lineColor = Color.GREEN;  // ç»¿è‰²è¿žçº¿
        Color pointColor = Color.RED;   // çº¢è‰²å®žå¿ƒåœ†
        // è®¾ç½®çº¿å®½
        float lineWidth = (float)(3.0 / Math.max(0.5, scale));
        g2d.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        // ç‚¹çš„大小(在世界坐标系中,米),直径为线宽度的3倍
        // æ³¨æ„ï¼šlineWidth已经是世界坐标系下的宽度(3.0/scale),所以直接乘以倍数即可得到世界坐标系下的直径
        // ä¸éœ€è¦å†é™¤ä»¥scale,否则会导致放大地图时圆圈变小
        double pointSizeInWorld = lineWidth * 3.0;
        double halfSize = pointSizeInWorld / 2.0;
        // å…ˆç»˜åˆ¶æ‰€æœ‰è·¯å¾„点(作为基础层)
        g2d.setColor(pointColor);
        for (Point2D.Double point : points) {
            Ellipse2D.Double pointShape = new Ellipse2D.Double(
                point.x - halfSize,
                point.y - halfSize,
                pointSizeInWorld,
                pointSizeInWorld
            );
            g2d.fill(pointShape);
        }
        // ç„¶åŽç»˜åˆ¶è¿žçº¿
        g2d.setColor(lineColor);
        if (points.size() >= 2) {
            Path2D.Double linePath = new Path2D.Double();
            linePath.moveTo(points.get(0).x, points.get(0).y);
            for (int i = 1; i < points.size(); i++) {
                linePath.lineTo(points.get(i).x, points.get(i).y);
            }
            g2d.draw(linePath);
        }
        // æœ€åŽå†æ¬¡ç»˜åˆ¶è·¯å¾„点(在连线之上,确保点覆盖在线的端点上)
        // å‡†å¤‡å­—体:大小与圆圈一致(世界坐标系),黑色
        float fontSizeInWorld = (float)pointSizeInWorld;
        // ä½¿ç”¨0.8倍大小以确保数字在圆圈内不拥挤
        Font font = new Font("Arial", Font.BOLD, 1).deriveFont(fontSizeInWorld * 0.8f);
        g2d.setFont(font);
        FontMetrics fm = g2d.getFontMetrics();
        for (int i = 0; i < points.size(); i++) {
            Point2D.Double point = points.get(i);
            // ç»˜åˆ¶ç‚¹
            g2d.setColor(pointColor);
            Ellipse2D.Double pointShape = new Ellipse2D.Double(
                point.x - halfSize,
                point.y - halfSize,
                pointSizeInWorld,
                pointSizeInWorld
            );
            g2d.fill(pointShape);
            // ç»˜åˆ¶ç¼–号
            g2d.setColor(Color.BLACK);
            String number = String.valueOf(i + 1);
            java.awt.geom.Rectangle2D bounds = fm.getStringBounds(number, g2d);
            double textX = point.x - bounds.getWidth() / 2.0;
            double textY = point.y - bounds.getCenterY();
            g2d.drawString(number, (float)textX, (float)textY);
        }
    }
    /**
     * ç»˜åˆ¶å®žçº¿çº¿æ¡ä¸­é—´åŠ ä¸Šè™šçº¿è™šçº¿ç™½è‰²ï¼Œå®žçº¿é»‘è‰²çš„é£Žæ ¼
     * @param g2d Graphics2D对象
     * @param points è·¯å¾„点列表
     * @param scale å½“前缩放比例
     */
    public static void drawRailwayPath(Graphics2D g2d, List<Point2D.Double> points, double scale) {
        if (points == null || points.size() < 2) {
            return;
        }
        // 1. ç»˜åˆ¶åº•层黑色实线
        float baseWidth = (float)(3.0 / Math.max(0.5, scale)); // åŸºç¡€å®½åº¦
        g2d.setColor(Color.BLACK);
        g2d.setStroke(new BasicStroke(baseWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        Path2D.Double path = new Path2D.Double();
        path.moveTo(points.get(0).x, points.get(0).y);
        for (int i = 1; i < points.size(); i++) {
            path.lineTo(points.get(i).x, points.get(i).y);
        }
        g2d.draw(path);
        // 2. ç»˜åˆ¶é¡¶å±‚白色虚线
        float dashWidth = baseWidth * 0.4f; // è™šçº¿å®½åº¦æ¯”实线细一些
        float dashLen = baseWidth * 2.0f;   // è™šçº¿æ®µé•¿åº¦
        float dashSpace = baseWidth * 2.0f; // è™šçº¿é—´éš”
        float[] dashPattern = {dashLen, dashSpace};
        g2d.setColor(Color.WHITE);
        g2d.setStroke(new BasicStroke(dashWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 10.0f, dashPattern, 0.0f));
        g2d.draw(path);
    }
}
src/zhuye/daohangyulan.java
@@ -19,6 +19,7 @@
import dikuai.Dikuai;
import gecaoji.Gecaoji;
import gecaoji.lujingdraw;
import publicway.buttonset;
import zhangaiwu.Obstacledge;
/**
src/zhuye/zijian.java
@@ -7,6 +7,9 @@
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import publicway.buttonset;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;