张世豪
18 小时以前 1175f5fbe8fd832943880bfc37c0e2a451a0688a
src/lujing/MowingPathGenerationPage.java
@@ -1,16 +1,14 @@
package lujing;
import javax.swing.*;
import javax.swing.SwingUtilities;
import java.awt.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import dikuai.Dikuai;
import lujing.Lunjingguihua;
import lujing.ObstaclePathPlanner;
import org.locationtech.jts.geom.Coordinate;
import publicway.Fuzhibutton;
import gecaoji.Device;
import java.util.Locale;
/**
 * 生成割草路径页面
@@ -38,6 +36,7 @@
        boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue);
        boolean saveMowingWidth(Dikuai dikuai, String value);
        boolean savePlannedPath(Dikuai dikuai, String value);
        boolean saveMowingSafetyDistance(Dikuai dikuai, String value);
    }
    
    private final Dikuai dikuai;
@@ -114,15 +113,68 @@
        
        // 地块边界
        boundaryArea = createInfoTextArea(boundaryValue != null ? boundaryValue : "", true, 6);
        contentPanel.add(createTextAreaSection("地块边界", boundaryArea));
        String boundaryTitle = "地块边界";
        if (boundaryValue != null && !boundaryValue.trim().isEmpty() && !"-1".equals(boundaryValue.trim())) {
            int boundaryCount = boundaryValue.split(";").length;
            boundaryTitle = "地块边界 (" + boundaryCount + "点)";
        }
        contentPanel.add(createTextAreaSection(boundaryTitle, boundaryArea));
        
        // 障碍物坐标
        obstacleArea = createInfoTextArea(obstacleValue != null ? obstacleValue : "", true, 6);
        contentPanel.add(createTextAreaSection("障碍物坐标", obstacleArea));
        String obstacleTitle = "障碍物坐标";
        if (obstacleValue != null && !obstacleValue.trim().isEmpty() && !"-1".equals(obstacleValue.trim())) {
            // 障碍物坐标格式可能是空格分隔的多个障碍物,每个障碍物用分号分隔坐标点
            // 计算所有障碍物的总坐标点数
            String[] obstacles = obstacleValue.trim().split("\\s+");
            int totalObstaclePoints = 0;
            for (String obstacle : obstacles) {
                if (obstacle != null && !obstacle.trim().isEmpty()) {
                    totalObstaclePoints += obstacle.split(";").length;
                }
            }
            if (totalObstaclePoints > 0) {
                obstacleTitle = "障碍物坐标 (" + totalObstaclePoints + "点)";
            }
        }
        contentPanel.add(createTextAreaSection(obstacleTitle, obstacleArea));
        
        // 割草宽度
        widthField = createInfoTextField(widthValue != null ? widthValue : "", true);
        contentPanel.add(createTextFieldSection("割草宽度 (厘米)", widthField));
        contentPanel.add(createTextFieldSection("割草宽度 (cm)", widthField));
        // 割草安全距离(只读显示)
        // 优先从Dikuai对象获取,如果Dikuai中没有,再从Device获取
        String displaySafetyDistance = "未设置";
        String safetyDistanceValue = null;
        // 首先尝试从Dikuai对象获取
        if (dikuai != null) {
            safetyDistanceValue = dikuai.getMowingSafetyDistance();
        }
        // 如果Dikuai中没有,从Device获取
        if ((safetyDistanceValue == null || "-1".equals(safetyDistanceValue) || safetyDistanceValue.trim().isEmpty())) {
            Device device = Device.getActiveDevice();
            if (device != null) {
                safetyDistanceValue = device.getMowingSafetyDistance();
            }
        }
        // 格式化显示值
        if (safetyDistanceValue != null && !"-1".equals(safetyDistanceValue) && !safetyDistanceValue.trim().isEmpty()) {
            try {
                double distanceMeters = Double.parseDouble(safetyDistanceValue.trim());
                // 如果值大于100,认为是厘米,需要转换为米
                if (distanceMeters > 100) {
                    distanceMeters = distanceMeters / 100.0;
                }
                displaySafetyDistance = String.format(Locale.US, "%.2fm", distanceMeters);
            } catch (NumberFormatException e) {
                displaySafetyDistance = "未设置";
            }
        }
        contentPanel.add(createInfoValueSection("割草安全距离", displaySafetyDistance));
        
        // 割草模式(只读显示)
        contentPanel.add(createInfoValueSection("割草模式", formatMowingPatternForDialog(modeValue)));
@@ -131,7 +183,12 @@
        String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath());
        String pathSeed = initialGeneratedPath != null ? initialGeneratedPath : existingPath;
        pathArea = createInfoTextArea(pathSeed != null ? pathSeed : "", true, 10);
        contentPanel.add(createTextAreaSection("割草路径坐标", pathArea));
        String pathTitle = "割草路径坐标";
        if (pathSeed != null && !pathSeed.trim().isEmpty() && !"-1".equals(pathSeed.trim())) {
            int pathCount = pathSeed.split(";").length;
            pathTitle = "割草路径坐标 (" + pathCount + "点)";
        }
        contentPanel.add(createTextAreaSection(pathTitle, pathArea));
        
        JScrollPane dialogScrollPane = new JScrollPane(contentPanel);
        dialogScrollPane.setBorder(BorderFactory.createEmptyBorder());
@@ -143,14 +200,17 @@
        buttonPanel.setBackground(BACKGROUND_COLOR);
        
        JButton generateBtn = createPrimaryFooterButton("生成割草路径");
        JButton previewBtn = createPrimaryFooterButton("预览");
        JButton saveBtn = createPrimaryFooterButton("保存路径");
        JButton cancelBtn = createPrimaryFooterButton("取消");
        
        generateBtn.addActionListener(e -> generatePath(modeValue));
        previewBtn.addActionListener(e -> previewPath());
        saveBtn.addActionListener(e -> savePath());
        cancelBtn.addActionListener(e -> dispose());
        
        buttonPanel.add(generateBtn);
        buttonPanel.add(previewBtn);
        buttonPanel.add(saveBtn);
        buttonPanel.add(cancelBtn);
        add(buttonPanel, BorderLayout.SOUTH);
@@ -187,6 +247,132 @@
    }
    
    /**
     * 预览路径
     */
    private void previewPath() {
        // 直接从文本域获取路径数据
        String rawPath = pathArea.getText();
        String pathNormalized = normalizeCoordinateInput(rawPath);
        if (!"-1".equals(pathNormalized)) {
            // 规范化路径数据:支持换行、空格等分隔符
            pathNormalized = pathNormalized
                .replace("\r\n", ";")
                .replace('\r', ';')
                .replace('\n', ';')
                .replaceAll("\\s+", ";") // 将所有空白字符替换为分号
                .replaceAll(";+", ";");  // 合并连续分号
            // 去除首尾分号
            if (pathNormalized.startsWith(";")) pathNormalized = pathNormalized.substring(1);
            if (pathNormalized.endsWith(";")) pathNormalized = pathNormalized.substring(0, pathNormalized.length() - 1);
            if (pathNormalized.isEmpty()) {
                pathNormalized = "-1";
            }
        }
        if ("-1".equals(pathNormalized)) {
            JOptionPane.showMessageDialog(this, "请先生成割草路径或在文本框中输入有效坐标", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        // 注意:预览时不自动保存路径到地块,仅使用文本域中的数据进行预览
        // 只有点击"保存路径"按钮时才持久化数据
        // 保存当前页面状态,用于返回时恢复
        String currentBaseStation = baseStationField.getText();
        String currentBoundary = boundaryArea.getText();
        String currentObstacle = obstacleArea.getText();
        String currentWidth = widthField.getText();
        String currentPath = pathArea.getText();
        // 获取地块信息
        String landNumber = dikuai.getLandNumber();
        String landName = dikuai.getLandName();
        // 处理边界坐标,确保变量是 effectively final
        String boundaryInput = normalizeCoordinateInput(boundaryArea.getText());
        final String boundary;
        if (!"-1".equals(boundaryInput)) {
            String processed = boundaryInput
                .replace("\r\n", ";")
                .replace('\r', ';')
                .replace('\n', ';')
                .replaceAll("\\s+", ";")
                .replaceAll(";+", ";");
            if (processed.startsWith(";")) processed = processed.substring(1);
            if (processed.endsWith(";")) processed = processed.substring(0, processed.length() - 1);
            if (processed.isEmpty()) {
                boundary = dikuai.getBoundaryCoordinates();
            } else {
                boundary = processed;
            }
        } else {
            boundary = dikuai.getBoundaryCoordinates();
        }
        // 处理障碍物坐标,确保变量是 effectively final
        String obstaclesInput = normalizeCoordinateInput(obstacleArea.getText());
        final String obstacles;
        if (!"-1".equals(obstaclesInput)) {
            String processed = obstaclesInput.replace("\r\n", " ")
                .replace('\r', ' ')
                .replace('\n', ' ')
                .replaceAll("\\s{2,}", " ")
                .trim();
            if (processed.isEmpty()) {
                obstacles = null;
            } else {
                obstacles = processed;
            }
        } else {
            obstacles = null;
        }
        // 保存最终值到 final 变量,以便在 lambda 中使用
        final String finalPathNormalized = pathNormalized;
        final String finalLandNumber = landNumber;
        final String finalLandName = landName;
        // 关闭路径规划页面
        setVisible(false);
        // 打开主页面并显示路径预览
        SwingUtilities.invokeLater(() -> {
            zhuye.Shouye shouye = zhuye.Shouye.getInstance();
            if (shouye != null) {
                // 显示路径预览,并设置返回回调
                shouye.startMowingPathPreview(
                    finalLandNumber,
                    finalLandName,
                    boundary,
                    obstacles,
                    finalPathNormalized,
                    () -> {
                        // 返回回调:重新打开路径规划页面
                        SwingUtilities.invokeLater(() -> {
                            setVisible(true);
                            // 恢复之前的状态
                            baseStationField.setText(currentBaseStation);
                            boundaryArea.setText(currentBoundary);
                            obstacleArea.setText(currentObstacle);
                            widthField.setText(currentWidth);
                            pathArea.setText(currentPath);
                        });
                    }
                );
            } else {
                // 如果主页面不存在,提示用户并重新显示路径规划页面
                JOptionPane.showMessageDialog(null, "无法打开主页面进行预览", "提示", JOptionPane.WARNING_MESSAGE);
                setVisible(true);
            }
        });
    }
    /**
     * 保存路径
     */
    private void savePath() {
@@ -221,7 +407,7 @@
        String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : "";
        String widthSanitized = sanitizeWidthString(widthField.getText());
        if (widthSanitized == null) {
            String message = rawWidthInput.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确";
                String message = rawWidthInput.isEmpty() ? "请先设置割草宽度(cm)" : "割草宽度格式不正确";
            JOptionPane.showMessageDialog(this, message, "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
@@ -260,6 +446,21 @@
            JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        // 更新当前地块对象的属性
        if (dikuai != null) {
            dikuai.setBaseStationCoordinates(baseStationNormalized);
            dikuai.setBoundaryCoordinates(boundaryNormalized);
            dikuai.setMowingWidth(widthNormalized);
            dikuai.setPlannedPath(pathNormalized);
            dikuai.setObstacleCoordinates(obstacleNormalized);
            // 获取并更新安全距离
            String safetyDistance = getSafetyDistanceString();
            if (safetyDistance != null) {
                dikuai.setMowingSafetyDistance(safetyDistance);
            }
        }
        
        // 调用回调保存数据
        if (saveCallback != null) {
@@ -283,10 +484,19 @@
                JOptionPane.showMessageDialog(this, "无法保存割草路径", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            // 保存安全距离
            String safetyDistance = getSafetyDistanceString();
            if (safetyDistance != null) {
                if (!saveCallback.saveMowingSafetyDistance(dikuai, safetyDistance)) {
                    JOptionPane.showMessageDialog(this, "无法保存割草安全距离", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
            }
        }
        
        JOptionPane.showMessageDialog(this, "割草路径已保存", "成功", JOptionPane.INFORMATION_MESSAGE);
        dispose();
        // dispose(); // 用户要求保存后不关闭页面
    }
    
    /**
@@ -308,7 +518,7 @@
        String widthStr = sanitizeWidthString(widthCmInput);
        if (widthStr == null) {
            if (showMessages) {
                String message = rawWidth.isEmpty() ? "请先设置割草宽度(厘米)" : "割草宽度格式不正确";
                String message = rawWidth.isEmpty() ? "请先设置割草宽度(cm)" : "割草宽度格式不正确";
                JOptionPane.showMessageDialog(parentComponent, message, "提示", JOptionPane.WARNING_MESSAGE);
            }
            return null;
@@ -345,67 +555,90 @@
        
        String obstacles = sanitizeValueOrNull(obstacleInput);
        if (obstacles != null) {
            obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' ');
            // 按照用户要求,多个障碍物之间用 $ 符号分隔
            // 如果输入中包含 $,则保留 $,否则将换行符替换为 $
            if (obstacles.contains("$")) {
                // 已经是 $ 分隔的格式,只需清理换行符
                obstacles = obstacles.replace("\r\n", "").replace('\r', ' ').replace('\n', ' ');
            } else {
                // 尝试将换行符转换为 $,或者如果是一行则保持原样
                // 这里假设用户可能用换行分隔多个障碍物
                // 但根据需求描述,似乎输入本身就应该是 $ 分隔的,或者我们需要处理成 $ 分隔
                // 为了兼容性,如果用户输入的是换行分隔的多个障碍物,我们将其转换为 $ 分隔
                // 但通常障碍物坐标是一串坐标点,如果用户没有显式用 $ 分隔,我们很难区分是同一个障碍物的点还是多个障碍物
                // 因此,这里主要处理清理工作,具体的解析逻辑在各实现类中处理
                obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' ');
            }
        }
        // 获取安全距离
        String safetyMarginStr = getSafetyDistanceString();
        if (safetyMarginStr == null) {
            // 如果没有设置安全距离,使用默认值:割草宽度的一半 + 0.2米
            double defaultSafetyDistance = widthMeters / 2.0 + 0.2;
            safetyMarginStr = BigDecimal.valueOf(defaultSafetyDistance)
                .setScale(3, RoundingMode.HALF_UP)
                .stripTrailingZeros()
                .toPlainString();
        }
        String mode = normalizeExistingMowingPattern(modeInput);
        try {
            // 解析障碍物列表
            List<List<Coordinate>> obstacleList = Lunjingguihua.parseObstacles(obstacles);
            if (obstacleList == null) {
                obstacleList = new ArrayList<>();
            }
            // 判断是否有障碍物:只要原始输入有障碍物内容,就使用ObstaclePathPlanner
            // 即使解析后列表为空,也尝试使用ObstaclePathPlanner(它会处理空障碍物列表的情况)
            boolean hasObstacles = hasObstacleInput && !obstacleList.isEmpty();
            // 1. 首先判断地块类型(凸形还是异形)
            Qufenxingzhuang shapeJudger = new Qufenxingzhuang();
            int grassType = shapeJudger.judgeGrassType(boundary);
            // grassType: 0=无法判断, 1=凸形, 2=异形
            
            // 如果原始输入有障碍物但解析失败,给出提示
            if (hasObstacleInput && obstacleList.isEmpty()) {
                if (showMessages) {
                    JOptionPane.showMessageDialog(parentComponent,
                        "障碍物坐标格式可能不正确,将尝试生成路径。如果路径不正确,请检查障碍物坐标格式。",
                        "提示", JOptionPane.WARNING_MESSAGE);
                }
                // 仍然尝试使用ObstaclePathPlanner,即使障碍物列表为空
                // 这样至少可以确保使用正确的路径规划器
            }
            String generated = null;
            
            String generated;
            if (!hasObstacles && !hasObstacleInput) {
                // 完全没有障碍物输入时,使用Lunjingguihua类的方法生成路径
                generated = Lunjingguihua.generatePathFromStrings(
                    boundary,
                    obstacles != null ? obstacles : "",
                    plannerWidth,
                    mode
                );
            } else {
                // 有障碍物输入时(即使解析失败),使用ObstaclePathPlanner处理路径生成
                List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary);
                if (polygon.size() < 4) {
                    if (showMessages) {
                        JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点",
                            "错误", JOptionPane.ERROR_MESSAGE);
                    }
                    return null;
                }
                // 根据是否有障碍物设置不同的安全距离
                double safetyDistance;
                if (!obstacleList.isEmpty()) {
                    // 有障碍物时使用割草宽度的一半 + 0.05米额外安全距离
                    safetyDistance = widthMeters / 2.0 + 0.05;
            // 2. 根据地块类型和是否有障碍物,调用不同的路径生成类
            if (!hasObstacleInput) {
                // 无障碍物的情况
                if (grassType == 1) {
                    // 凸形地块,无障碍物 -> 调用 AoxinglujingNoObstacle
                    List<AoxinglujingNoObstacle.PathSegment> segments =
                        AoxinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr);
                    generated = formatAoxingPathSegments(segments);
                } else if (grassType == 2) {
                    // 异形地块,无障碍物 -> 调用 YixinglujingNoObstacle
                    // 调用 YixinglujingNoObstacle.planPath 获取路径段列表
                    List<YixinglujingNoObstacle.PathSegment> segments =
                        YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr);
                    // 格式化路径段列表为字符串
                    generated = formatYixingPathSegments(segments);
                } else {
                    // 障碍物解析失败但输入存在,使用较小的安全距离
                    safetyDistance = 0.01;
                    // 无法判断地块类型,默认按凸形处理或提示
                    if (showMessages) {
                        JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,尝试按凸形地块处理",
                            "提示", JOptionPane.WARNING_MESSAGE);
                    }
                    List<AoxinglujingNoObstacle.PathSegment> segments =
                        AoxinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr);
                    generated = formatAoxingPathSegments(segments);
                }
                ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner(
                    polygon, widthMeters, mode, obstacleList, safetyDistance);
                List<Lunjingguihua.PathSegment> segments = pathPlanner.generate();
                generated = Lunjingguihua.formatPathSegments(segments);
            } else {
                // 有障碍物的情况
                if (grassType == 1) {
                    // 凸形地块,有障碍物 -> 调用 AoxinglujingHaveObstacel
                    // 传入参数:boundary(A), obstacles(B), plannerWidth(C), safetyMarginStr(D)
                    List<AoxinglujingHaveObstacel.PathSegment> segments =
                        AoxinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr);
                    generated = formatAoxingHaveObstaclePathSegments(segments);
                } else if (grassType == 2) {
                    // 异形地块,有障碍物 -> 调用 YixinglujingHaveObstacel
                    // 传入参数:boundary(A), obstacles(B), plannerWidth(C), safetyMarginStr(D)
                    // 注意:YixinglujingHaveObstacel.planPath 返回 String
                    generated = YixinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr);
                } else {
                    // 无法判断地块类型,默认按凸形处理或提示
                    if (showMessages) {
                        JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,尝试按凸形地块处理",
                            "提示", JOptionPane.WARNING_MESSAGE);
                    }
                    List<AoxinglujingHaveObstacel.PathSegment> segments =
                        AoxinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr);
                    generated = formatAoxingHaveObstaclePathSegments(segments);
                }
            }
            
            String trimmed = generated != null ? generated.trim() : "";
@@ -436,6 +669,180 @@
        return null;
    }
    
    /**
     * 获取安全距离字符串(米)
     * 优先从Dikuai对象获取,如果Dikuai中没有,再从Device获取
     */
    private String getSafetyDistanceString() {
        String safetyDistanceValue = null;
        // 首先尝试从Dikuai对象获取
        if (dikuai != null) {
            safetyDistanceValue = dikuai.getMowingSafetyDistance();
        }
        // 如果Dikuai中没有,从Device获取
        if ((safetyDistanceValue == null || "-1".equals(safetyDistanceValue) || safetyDistanceValue.trim().isEmpty())) {
            Device device = Device.getActiveDevice();
            if (device != null) {
                safetyDistanceValue = device.getMowingSafetyDistance();
            }
        }
        // 格式化并返回
        if (safetyDistanceValue != null && !"-1".equals(safetyDistanceValue) && !safetyDistanceValue.trim().isEmpty()) {
            try {
                double distanceMeters = Double.parseDouble(safetyDistanceValue.trim());
                // 如果值大于100,认为是厘米,需要转换为米
                if (distanceMeters > 100) {
                    distanceMeters = distanceMeters / 100.0;
                }
                return BigDecimal.valueOf(distanceMeters)
                    .setScale(3, RoundingMode.HALF_UP)
                    .stripTrailingZeros()
                    .toPlainString();
            } catch (NumberFormatException e) {
                // 解析失败,返回null,使用默认值
            }
        }
        return null;
    }
    /**
     * 格式化 AoxinglujingNoObstacle.PathSegment 列表为坐标字符串
     */
    private String formatAoxingPathSegments(List<AoxinglujingNoObstacle.PathSegment> segments) {
        if (segments == null || segments.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        AoxinglujingNoObstacle.Point last = null;
        for (AoxinglujingNoObstacle.PathSegment segment : segments) {
            // 只添加割草工作段,跳过过渡段
            if (segment.isMowing) {
                // 如果起点与上一个终点不同,添加起点
                if (last == null || !equals2D(last, segment.start)) {
                    appendPoint(sb, segment.start);
                }
                // 添加终点
                appendPoint(sb, segment.end);
                last = segment.end;
            }
        }
        return sb.toString();
    }
    /**
     * 格式化 YixinglujingNoObstacle.PathSegment 列表为坐标字符串
     */
    private String formatYixingPathSegments(List<YixinglujingNoObstacle.PathSegment> segments) {
        if (segments == null || segments.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        YixinglujingNoObstacle.Point last = null;
        for (YixinglujingNoObstacle.PathSegment segment : segments) {
            // 只添加割草工作段,跳过过渡段
            if (segment.isMowing) {
                // 如果起点与上一个终点不同,添加起点
                if (last == null || !equalsYixingPoint(last, segment.start)) {
                    appendYixingPoint(sb, segment.start);
                }
                // 添加终点
                appendYixingPoint(sb, segment.end);
                last = segment.end;
            }
        }
        return sb.toString();
    }
    /**
     * 格式化 AoxinglujingHaveObstacel.PathSegment 列表为坐标字符串
     */
    private String formatAoxingHaveObstaclePathSegments(List<AoxinglujingHaveObstacel.PathSegment> segments) {
        if (segments == null || segments.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        AoxinglujingHaveObstacel.Point last = null;
        for (AoxinglujingHaveObstacel.PathSegment segment : segments) {
            // 只添加割草工作段,跳过过渡段
            if (segment.isMowing) {
                // 如果起点与上一个终点不同,添加起点
                if (last == null || !equals2D(last, segment.start)) {
                    appendPoint(sb, segment.start);
                }
                // 添加终点
                appendPoint(sb, segment.end);
                last = segment.end;
            }
        }
        return sb.toString();
    }
    /**
     * 比较两个点是否相同(使用小的容差)
     */
    private boolean equals2D(AoxinglujingNoObstacle.Point p1, AoxinglujingNoObstacle.Point p2) {
        if (p1 == null || p2 == null) {
            return p1 == p2;
        }
        double tolerance = 1e-6;
        return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance;
    }
    /**
     * 添加点到字符串构建器
     */
    private void appendPoint(StringBuilder sb, AoxinglujingNoObstacle.Point point) {
        if (sb.length() > 0) {
            sb.append(";");
        }
        sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y));
    }
    /**
     * 比较两个 YixinglujingNoObstacle.Point 是否相同(使用小的容差)
     */
    private boolean equalsYixingPoint(YixinglujingNoObstacle.Point p1, YixinglujingNoObstacle.Point p2) {
        if (p1 == null || p2 == null) {
            return p1 == p2;
        }
        double tolerance = 1e-6;
        return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance;
    }
    /**
     * 添加 YixinglujingNoObstacle.Point 到字符串构建器
     */
    private void appendYixingPoint(StringBuilder sb, YixinglujingNoObstacle.Point point) {
        if (sb.length() > 0) {
            sb.append(";");
        }
        sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y));
    }
    /**
     * 比较两个 AoxinglujingHaveObstacel.Point 是否相同(使用小的容差)
     */
    private boolean equals2D(AoxinglujingHaveObstacel.Point p1, AoxinglujingHaveObstacel.Point p2) {
        if (p1 == null || p2 == null) {
            return p1 == p2;
        }
        double tolerance = 1e-6;
        return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance;
    }
    /**
     * 添加 AoxinglujingHaveObstacel.Point 到字符串构建器
     */
    private void appendPoint(StringBuilder sb, AoxinglujingHaveObstacel.Point point) {
        if (sb.length() > 0) {
            sb.append(";");
        }
        sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y));
    }
    // ========== UI辅助方法 ==========
    
    private JTextArea createInfoTextArea(String text, boolean editable, int rows) {
@@ -456,10 +863,31 @@
        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);
        section.add(titleLabel, BorderLayout.NORTH);
        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;
            },
            "复制" + title,
            new Color(230, 250, 240)
        );
        titlePanel.add(copyButton, BorderLayout.EAST);
        section.add(titlePanel, BorderLayout.NORTH);
        
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
@@ -488,10 +916,31 @@
        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);
        section.add(titleLabel, BorderLayout.NORTH);
        titlePanel.add(titleLabel, BorderLayout.WEST);
        // 创建复制按钮(使用 Fuzhibutton)
        JButton copyButton = Fuzhibutton.createCopyButton(
            () -> {
                String text = textField.getText();
                if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) {
                    return null; // 返回null会触发"未设置"提示
                }
                return text;
            },
            "复制" + title,
            new Color(230, 250, 240)
        );
        titlePanel.add(copyButton, BorderLayout.EAST);
        section.add(titlePanel, BorderLayout.NORTH);
        
        JPanel fieldWrapper = new JPanel(new BorderLayout());
        fieldWrapper.setBackground(textField.isEditable() ? WHITE : new Color(245, 245, 245));
@@ -629,6 +1078,7 @@
        }
        return "parallel";
    }
}