张世豪
16 小时以前 b272034a1fdbfe32b355fc6c264a4c45df107190
src/zhangaiwu/AddDikuai.java
@@ -1,7 +1,5 @@
package zhangaiwu;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
@@ -9,24 +7,29 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Comparator;
import java.awt.geom.Point2D;
import baseStation.BaseStation;
import bianjie.jisuanmianjie;
import dikuai.Dikuai;
import dikuai.Dikuaiguanli;
import gecaoji.Device;
import bianjie.bianjieguihua2;
import dikuai.Gecaokuanjisuan;
import dikuai.Gecaoanquanjuli;
import bianjie.Bianjieyouhuatoxy;
import lujing.Lunjingguihua;
import set.Setsys;
import ui.UIConfig;
import zhuye.MowerLocationData;
import zhuye.Shouye;
import zhuye.Coordinate;
import publicway.buttonset;
/**
 * 新增地块对话框 - 多步骤表单设计
@@ -46,6 +49,9 @@
    private final Color LIGHT_TEXT = new Color(108, 117, 125);
    private final Color BORDER_COLOR = new Color(222, 226, 230);
    private final Color SUCCESS_COLOR = new Color(40, 167, 69);
    private final Color ERROR_COLOR = new Color(220, 53, 69);
    private static final String KEY_PATH_MESSAGE_TEXT = "__pathMessageText";
    private static final String KEY_PATH_MESSAGE_SUCCESS = "__pathMessageSuccess";
    
    // 步骤面板
    private JPanel mainPanel;
@@ -56,7 +62,9 @@
    private JTextField landNumberField;
    private JTextField areaNameField;
    private JComboBox<String> mowingPatternCombo;
    private JSpinner mowingWidthSpinner;
    private JTextField bladeWidthField;  // 割草机割刀宽度(m)
    private JTextField mowingWidthField;  // 割草宽度(m,可编辑)
    private JTextField safetyDistanceField;  // 割草安全距离(m,可编辑)
    private JPanel previewPanel;
    private Map<String, JPanel> drawingOptionPanels = new HashMap<>();
    
@@ -65,8 +73,14 @@
    private JButton prevButton;
    private JButton nextButton;
    private JButton createButton;
    private JButton previewButton;
    private Component previewButtonSpacer;
    private JLabel boundaryCountLabel;
    private JTextArea boundaryXYTextArea;  // 显示边界XY坐标的文本域
    private JScrollPane boundaryXYScrollPane;  // 边界XY坐标文本域的滚动面板
    private JPanel obstacleListContainer;
    private JTextArea pathGenerationMessageArea;
    private JPanel pathMessageWrapper;
    
    // 地块数据
    private Map<String, String> dikuaiData = new HashMap<>();
@@ -80,6 +94,41 @@
    private static DrawingSession activeSession;
    private static boolean resumeRequested;
    private static final List<Point2D.Double> TEMP_HANDHELD_POINTS = new ArrayList<>();
    private static final class BoundarySnapshotResult {
        final boolean success;
        final String errorMessage;
        final String originalBoundary;
        final String optimizedBoundary;
        final String areaSqMeters;
        final String baseStationCoordinates;
        final int messageType;
        private BoundarySnapshotResult(boolean success,
                                       String errorMessage,
                                       String originalBoundary,
                                       String optimizedBoundary,
                                       String areaSqMeters,
                                       String baseStationCoordinates,
                                       int messageType) {
            this.success = success;
            this.errorMessage = errorMessage;
            this.originalBoundary = originalBoundary;
            this.optimizedBoundary = optimizedBoundary;
            this.areaSqMeters = areaSqMeters;
            this.baseStationCoordinates = baseStationCoordinates;
            this.messageType = messageType;
        }
        static BoundarySnapshotResult success(String original, String optimized, String area, String baseStation) {
            return new BoundarySnapshotResult(true, null, original, optimized, area, baseStation, JOptionPane.INFORMATION_MESSAGE);
        }
        static BoundarySnapshotResult failure(String message, int messageType) {
            return new BoundarySnapshotResult(false, message, null, null, null, null, messageType);
        }
    }
    private static class DrawingSession {
        String landNumber;
@@ -87,6 +136,27 @@
        Map<String, String> data = new HashMap<>();
        boolean drawingCompleted;
    }
    public static void recordTemporaryBoundaryPoints(List<Point2D.Double> points) {
        synchronized (TEMP_HANDHELD_POINTS) {
            TEMP_HANDHELD_POINTS.clear();
            if (points == null) {
                return;
            }
            for (Point2D.Double point : points) {
                if (point == null) {
                    continue;
                }
                TEMP_HANDHELD_POINTS.add(new Point2D.Double(point.x, point.y));
            }
        }
    }
    public static List<Point2D.Double> getTemporaryBoundaryPointsSnapshot() {
        synchronized (TEMP_HANDHELD_POINTS) {
            return new ArrayList<>(TEMP_HANDHELD_POINTS);
        }
    }
    
    public AddDikuai(JFrame parent) {
        super(parent, "新增地块", true);
@@ -304,93 +374,105 @@
    private List<ObstacleSummary> loadExistingObstacles() {
        List<ObstacleSummary> summaries = new ArrayList<>();
        List<ExistingObstacle> obstacles = fetchExistingObstacleDetails();
        String landNumber = getPendingLandNumber();
        String raw = null;
        if (landNumber != null) {
            Dikuai dikuai = Dikuai.getDikuai(landNumber);
            if (dikuai != null) {
                raw = normalizeCoordinateValue(dikuai.getObstacleCoordinates());
            }
        }
        if (raw == null) {
            raw = normalizeCoordinateValue(dikuaiData.get("obstacleCoordinates"));
        }
        if (!isMeaningfulValue(raw)) {
        if (obstacles.isEmpty()) {
            return summaries;
        }
        String normalized = stripInlineComment(raw);
        if (normalized.isEmpty()) {
            return summaries;
        }
        List<String> entries = splitObstacleEntries(normalized);
        int defaultIndex = 1;
        for (String entry : entries) {
            String trimmedEntry = stripInlineComment(entry);
            if (trimmedEntry.isEmpty()) {
        for (ExistingObstacle obstacle : obstacles) {
            if (obstacle == null) {
                continue;
            }
            String nameToken = null;
            String shapeToken = null;
            String coordsSection = trimmedEntry;
            if (trimmedEntry.contains("::")) {
                String[] parts = trimmedEntry.split("::", 3);
                if (parts.length == 3) {
                    nameToken = parts[0].trim();
                    shapeToken = parts[1].trim();
                    coordsSection = parts[2].trim();
                }
            } else if (trimmedEntry.contains("@")) {
                String[] parts = trimmedEntry.split("@", 3);
                if (parts.length == 3) {
                    nameToken = parts[0].trim();
                    shapeToken = parts[1].trim();
                    coordsSection = parts[2].trim();
                } else if (parts.length == 2) {
                    shapeToken = parts[0].trim();
                    coordsSection = parts[1].trim();
                }
            } else if (trimmedEntry.contains(":")) {
                String[] parts = trimmedEntry.split(":", 3);
                if (parts.length == 3) {
                    nameToken = parts[0].trim();
                    shapeToken = parts[1].trim();
                    coordsSection = parts[2].trim();
                } else if (parts.length == 2) {
                    if (looksLikeShapeToken(parts[0])) {
                        shapeToken = parts[0].trim();
                        coordsSection = parts[1].trim();
                    } else {
                        nameToken = parts[0].trim();
                        coordsSection = parts[1].trim();
                    }
                }
            }
            String sanitizedCoords = sanitizeCoordinateString(coordsSection);
            if (!isMeaningfulValue(sanitizedCoords)) {
            String name = obstacle.getName();
            if (!isMeaningfulValue(name)) {
                continue;
            }
            String resolvedName;
            if (nameToken != null && !nameToken.isEmpty()) {
                resolvedName = nameToken;
            } else {
                resolvedName = "障碍物" + defaultIndex++;
            }
            String displayCoords = truncateCoordinateForDisplay(sanitizedCoords, 12);
            summaries.add(new ObstacleSummary(resolvedName, sanitizedCoords, displayCoords));
            String coords = obstacle.getDisplayCoordinates();
            String fullCoords = isMeaningfulValue(coords) ? coords.trim() : "";
            String preview = buildCoordinatePreview(fullCoords, 20);
            summaries.add(new ObstacleSummary(name.trim(), fullCoords, preview));
        }
        return summaries;
    }
    private List<ExistingObstacle> fetchExistingObstacleDetails() {
        File configFile = new File("Obstacledge.properties");
        if (!configFile.exists()) {
            return Collections.emptyList();
        }
        List<ExistingObstacle> result = new ArrayList<>();
        try {
            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
            if (!manager.loadFromFile(configFile.getAbsolutePath())) {
                return Collections.emptyList();
            }
            for (Obstacledge.Plot plot : manager.getPlots()) {
                if (plot == null) {
                    continue;
                }
                String landNumber = isMeaningfulValue(plot.getPlotId()) ? plot.getPlotId().trim() : "";
                List<Obstacledge.Obstacle> plotObstacles = plot.getObstacles();
                if (plotObstacles == null || plotObstacles.isEmpty()) {
                    continue;
                }
                for (Obstacledge.Obstacle obstacle : plotObstacles) {
                    if (obstacle == null) {
                        continue;
                    }
                    String name = obstacle.getObstacleName();
                    if (!isMeaningfulValue(name)) {
                        continue;
                    }
                    String coords = extractObstacleCoordinates(obstacle);
                    result.add(new ExistingObstacle(landNumber, name.trim(), coords));
                }
            }
        } catch (Exception ex) {
            System.err.println("加载已有障碍物失败: " + ex.getMessage());
            return Collections.emptyList();
        }
        if (result.isEmpty()) {
            return Collections.emptyList();
        }
        result.sort(Comparator.comparing(ExistingObstacle::getName, String.CASE_INSENSITIVE_ORDER));
        return result;
    }
    private String extractObstacleCoordinates(Obstacledge.Obstacle obstacle) {
        if (obstacle == null) {
            return "";
        }
        String xy = obstacle.getXyCoordsString();
        if (isMeaningfulValue(xy)) {
            return xy.trim();
        }
        String original = obstacle.getOriginalCoordsString();
        if (isMeaningfulValue(original)) {
            return original.trim();
        }
        return "";
    }
    private String buildCoordinatePreview(String coords, int keepLength) {
        if (!isMeaningfulValue(coords)) {
            return "无坐标";
        }
        String sanitized = sanitizeCoordinateString(coords);
        if (sanitized.length() <= keepLength) {
            return sanitized;
        }
        if (keepLength <= 0) {
            return "...";
        }
        return sanitized.substring(0, keepLength) + "...";
    }
    private List<String> splitObstacleEntries(String data) {
        List<String> entries = new ArrayList<>();
        if (data.indexOf('|') >= 0) {
@@ -485,6 +567,30 @@
            return displayCoords;
        }
    }
    private static final class ExistingObstacle {
        private final String landNumber;
        private final String name;
        private final String coordinates;
        ExistingObstacle(String landNumber, String name, String coordinates) {
            this.landNumber = landNumber != null ? landNumber : "";
            this.name = name != null ? name : "";
            this.coordinates = coordinates != null ? coordinates : "";
        }
        String getLandNumber() {
            return landNumber;
        }
        String getName() {
            return name;
        }
        String getDisplayCoordinates() {
            return coordinates;
        }
    }
    
    private JPanel createStep2Panel() {
        JPanel stepPanel = new JPanel();
@@ -552,6 +658,34 @@
        startEndDrawingBtn.addActionListener(e -> toggleDrawing());
        
        stepPanel.add(startEndDrawingBtn);
        // 边界XY坐标显示文本域(3行,带滚动条,在已完成按钮下方)
        boundaryXYTextArea = new JTextArea(3, 0);
        boundaryXYTextArea.setFont(new Font("微软雅黑", Font.PLAIN, 12));
        boundaryXYTextArea.setForeground(TEXT_COLOR);
        boundaryXYTextArea.setEditable(false);
        boundaryXYTextArea.setBackground(LIGHT_GRAY);
        boundaryXYTextArea.setLineWrap(true);
        boundaryXYTextArea.setWrapStyleWord(true);
        boundaryXYTextArea.setAlignmentX(Component.LEFT_ALIGNMENT);
        boundaryXYTextArea.setVisible(false);
        // 创建滚动面板,默认显示3行
        boundaryXYScrollPane = new JScrollPane(boundaryXYTextArea);
        boundaryXYScrollPane.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 1),
            BorderFactory.createEmptyBorder(0, 0, 0, 0)
        ));
        boundaryXYScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        boundaryXYScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        boundaryXYScrollPane.getVerticalScrollBar().setUnitIncrement(16);
        boundaryXYScrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
        boundaryXYScrollPane.setVisible(false);
        boundaryXYScrollPane.setMaximumSize(new Dimension(Integer.MAX_VALUE, 80));
        stepPanel.add(Box.createRigidArea(new Dimension(0, 15)));
        stepPanel.add(boundaryXYScrollPane);
        boundaryCountLabel = new JLabel("已采集到边界点0个");
        boundaryCountLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        boundaryCountLabel.setForeground(LIGHT_TEXT);
@@ -638,8 +772,9 @@
                if (!optionPanel.isEnabled()) {
                    return;
                }
                selectDrawingOption(optionPanel, type);
                startEndDrawingBtn.setEnabled(true); // 选择后启用按钮
                if (selectDrawingOption(optionPanel, type, true)) {
                    startEndDrawingBtn.setEnabled(true); // 选择后启用按钮
                }
            }
            
            @Override
@@ -661,7 +796,15 @@
        return optionPanel;
    }
    
    private void selectDrawingOption(JPanel optionPanel, String type) {
    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));
@@ -671,7 +814,7 @@
                ((JLabel) oldTitle).setForeground(TEXT_COLOR);
            }
        }
        // 设置新的选中状态
        optionPanel.setBorder(BorderFactory.createLineBorder(PRIMARY_COLOR, 3));
        optionPanel.setBackground(PRIMARY_LIGHT);
@@ -680,9 +823,15 @@
            ((JLabel) titleObj).setForeground(PRIMARY_COLOR);
        }
        selectedOptionPanel = optionPanel;
        // 保存选择
        dikuaiData.put("drawingMethod", type);
        return true;
    }
    private boolean hasConfiguredHandheldMarker() {
        String handheldId = Setsys.getPropertyValue("handheldMarkerId");
        return handheldId != null && !handheldId.trim().isEmpty();
    }
    
    private void toggleDrawing() {
@@ -705,12 +854,18 @@
            // 用户在对话框内主动结束(未触发外部流程)
            isDrawing = false;
            Coordinate.setStartSaveGngga(false);
            startEndDrawingBtn.setText("开始绘制");
            startEndDrawingBtn.setBackground(PRIMARY_COLOR);
            updateOtherOptionsState(false);
            startEndDrawingBtn.setText("已完成");
            startEndDrawingBtn.setBackground(MEDIUM_GRAY);
            startEndDrawingBtn.setEnabled(false);
            updateOtherOptionsState(true);
            // 优化边界
            optimizeBoundaryAndSave();
            dikuaiData.put("boundaryDrawn", "true");
            JOptionPane.showMessageDialog(this, "边界绘制已完成", "提示", JOptionPane.INFORMATION_MESSAGE);
            showBoundaryPointSummary();
            updateBoundaryXYDisplay();
        }
    }
@@ -761,6 +916,80 @@
        hideBoundaryPointSummary();
    }
    
    /**
     * 优化边界并保存优化后的数据
     */
    private void optimizeBoundaryAndSave() {
        try {
            // 获取基准站坐标
            BaseStation baseStation = new BaseStation();
            baseStation.load();
            String baseStationCoordinates = normalizeCoordinateValue(baseStation.getInstallationCoordinates());
            if (!isMeaningfulValue(baseStationCoordinates)) {
                System.err.println("未获取到有效的基准站坐标,无法优化边界");
                return;
            }
            // 获取原始坐标列表
            List<Coordinate> uniqueCoordinates;
            synchronized (Coordinate.coordinates) {
                uniqueCoordinates = sanitizeCoordinateList(Coordinate.coordinates);
            }
            if (uniqueCoordinates.size() < 3) {
                System.err.println("采集的边界点不足,无法优化边界");
                return;
            }
            // 构建边界字符串,格式为 "(lat1,lon1,alt1;lat2,lon2,alt2;...)"
            String boundaryStr = buildBoundaryStringForOptimization(uniqueCoordinates);
            // 调用优化方法
            String optimizedXYStr = Bianjieyouhuatoxy.optimizeBoundary(baseStationCoordinates, boundaryStr);
            // 保存优化后的XY坐标字符串
            if (optimizedXYStr != null && !optimizedXYStr.isEmpty() && !optimizedXYStr.startsWith("ERROR")) {
                dikuaiData.put("optimizedBoundaryXY", optimizedXYStr);
                if (activeSession != null) {
                    activeSession.data.put("optimizedBoundaryXY", optimizedXYStr);
                }
            } else {
                System.err.println("边界优化失败: " + optimizedXYStr);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("优化边界时发生错误: " + e.getMessage());
        }
    }
    /**
     * 构建用于优化的边界字符串,格式为 "(lat1,lon1,alt1;lat2,lon2,alt2;...)"
     * 其中lat和lon是度分格式(DMM格式),例如 "3949.89151752"
     */
    private static String buildBoundaryStringForOptimization(List<Coordinate> coordinates) {
        if (coordinates == null || coordinates.isEmpty()) {
            return "()";
        }
        StringBuilder sb = new StringBuilder("(");
        DecimalFormat elevationFormat = new DecimalFormat("0.00");
        for (int i = 0; i < coordinates.size(); i++) {
            Coordinate coord = coordinates.get(i);
            // Coordinate类中的getLatitude()和getLongitude()已经返回度分格式(DMM格式)
            String latDMM = coord.getLatitude();
            String lonDMM = coord.getLongitude();
            double elevation = coord.getElevation();
            if (i > 0) {
                sb.append(";");
            }
            sb.append(latDMM).append(",")
              .append(lonDMM).append(",")
              .append(elevationFormat.format(elevation));
        }
        sb.append(")");
        return sb.toString();
    }
    private JPanel createStep3Panel() {
        JPanel stepPanel = new JPanel();
        stepPanel.setLayout(new BoxLayout(stepPanel, BoxLayout.Y_AXIS));
@@ -782,7 +1011,7 @@
        settingsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        
        // 割草模式选择
        JPanel patternPanel = createFormGroup("割草模式", "选择割草路径的生成模式");
        JPanel patternPanel = createFormGroup("割草模式", "");
        mowingPatternCombo = new JComboBox<>(new String[]{"平行线", "螺旋式"});
        mowingPatternCombo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        mowingPatternCombo.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
@@ -816,27 +1045,23 @@
        settingsPanel.add(patternPanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        
        // 割草宽度设置
        JPanel widthPanel = createFormGroup("割草宽度", "设置割草机单次割草的宽度");
        JPanel widthInputPanel = new JPanel(new BorderLayout());
        widthInputPanel.setBackground(WHITE);
        widthInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        widthInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        // 割草机割刀宽度设置
        JPanel bladeWidthPanel = createFormGroup("割草机割刀宽度", "");
        JPanel bladeWidthInputPanel = new JPanel(new BorderLayout());
        bladeWidthInputPanel.setBackground(WHITE);
        bladeWidthInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        bladeWidthInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        
        SpinnerNumberModel widthModel = new SpinnerNumberModel(40, 20, 60, 1);
        mowingWidthSpinner = new JSpinner(widthModel);
        mowingWidthSpinner.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) mowingWidthSpinner.getEditor();
        editor.getTextField().setBorder(BorderFactory.createCompoundBorder(
        bladeWidthField = new JTextField("0.50");
        bladeWidthField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        bladeWidthField.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 12, 10, 12)
        ));
        // 添加微调器焦点效果
        mowingWidthSpinner.addFocusListener(new FocusAdapter() {
        bladeWidthField.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                editor.getTextField().setBorder(BorderFactory.createCompoundBorder(
                bladeWidthField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(PRIMARY_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
@@ -844,26 +1069,124 @@
            
            @Override
            public void focusLost(FocusEvent e) {
                editor.getTextField().setBorder(BorderFactory.createCompoundBorder(
                bladeWidthField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(BORDER_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
                updateMowingWidthAndSafetyDistance();
            }
        });
        JLabel bladeUnitLabel = new JLabel("m");
        bladeUnitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        bladeUnitLabel.setForeground(LIGHT_TEXT);
        bladeUnitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0));
        bladeWidthInputPanel.add(bladeWidthField, BorderLayout.CENTER);
        bladeWidthInputPanel.add(bladeUnitLabel, BorderLayout.EAST);
        bladeWidthPanel.add(bladeWidthInputPanel);
        settingsPanel.add(bladeWidthPanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        // 割草宽度设置(可编辑)
        JPanel widthPanel = createFormGroup("割草宽度", "计算方法:割草宽度 = 割刀宽 ×85%");
        JPanel widthInputPanel = new JPanel(new BorderLayout());
        widthInputPanel.setBackground(WHITE);
        widthInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        widthInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        mowingWidthField = new JTextField();
        mowingWidthField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        mowingWidthField.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 12, 10, 12)
        ));
        mowingWidthField.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                mowingWidthField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(PRIMARY_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
            @Override
            public void focusLost(FocusEvent e) {
                mowingWidthField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(BORDER_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
        });
        
        JLabel unitLabel = new JLabel("厘米");
        JLabel unitLabel = new JLabel("m");
        unitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        unitLabel.setForeground(LIGHT_TEXT);
        unitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0));
        
        widthInputPanel.add(mowingWidthSpinner, BorderLayout.CENTER);
        widthInputPanel.add(mowingWidthField, BorderLayout.CENTER);
        widthInputPanel.add(unitLabel, BorderLayout.EAST);
        
        widthPanel.add(widthInputPanel);
        settingsPanel.add(widthPanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        // 割草安全距离设置(可编辑)
        JPanel safetyDistancePanel = createFormGroup("割草安全距离", "根据割草机尺寸自动计算的安全距离");
        JPanel safetyDistanceInputPanel = new JPanel(new BorderLayout());
        safetyDistanceInputPanel.setBackground(WHITE);
        safetyDistanceInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        safetyDistanceInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        safetyDistanceField = new JTextField();
        safetyDistanceField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        safetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 12, 10, 12)
        ));
        safetyDistanceField.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                safetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(PRIMARY_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
            @Override
            public void focusLost(FocusEvent e) {
                safetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(BORDER_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
        });
        JLabel safetyUnitLabel = new JLabel("m");
        safetyUnitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        safetyUnitLabel.setForeground(LIGHT_TEXT);
        safetyUnitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0));
        safetyDistanceInputPanel.add(safetyDistanceField, BorderLayout.CENTER);
        safetyDistanceInputPanel.add(safetyUnitLabel, BorderLayout.EAST);
        safetyDistancePanel.add(safetyDistanceInputPanel);
        settingsPanel.add(safetyDistancePanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 25)));
        
        stepPanel.add(settingsPanel);
        // 初始化计算值
        updateMowingWidthAndSafetyDistance();
        // 将设置面板放入滚动面板
        JScrollPane scrollPane = new JScrollPane(settingsPanel);
        scrollPane.setBorder(null);
        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        scrollPane.getVerticalScrollBar().setUnitIncrement(16);
        scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT);
        stepPanel.add(scrollPane);
        
        JButton generatePathButton = createPrimaryButton("生成割草路径", 16);
        generatePathButton.setAlignmentX(Component.LEFT_ALIGNMENT);
@@ -872,6 +1195,29 @@
        stepPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        stepPanel.add(generatePathButton);
        stepPanel.add(Box.createRigidArea(new Dimension(0, 12)));
        pathMessageWrapper = new JPanel(new BorderLayout());
        pathMessageWrapper.setAlignmentX(Component.LEFT_ALIGNMENT);
        pathMessageWrapper.setBackground(PRIMARY_LIGHT);
        pathMessageWrapper.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(PRIMARY_COLOR, 1),
            BorderFactory.createEmptyBorder(12, 12, 12, 12)
        ));
        pathMessageWrapper.setVisible(false);
        pathGenerationMessageArea = new JTextArea();
        pathGenerationMessageArea.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        pathGenerationMessageArea.setForeground(TEXT_COLOR);
        pathGenerationMessageArea.setOpaque(false);
        pathGenerationMessageArea.setEditable(false);
        pathGenerationMessageArea.setLineWrap(true);
        pathGenerationMessageArea.setWrapStyleWord(true);
        pathGenerationMessageArea.setFocusable(false);
        pathGenerationMessageArea.setBorder(null);
        pathMessageWrapper.add(pathGenerationMessageArea, BorderLayout.CENTER);
        stepPanel.add(pathMessageWrapper);
        stepPanel.add(Box.createVerticalGlue());
        return stepPanel;
@@ -915,22 +1261,73 @@
        nameLabel.setForeground(TEXT_COLOR);
        nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        
        JLabel hintLabel = new JLabel(hint);
        hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
        hintLabel.setForeground(LIGHT_TEXT);
        hintLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        formGroup.add(nameLabel);
        formGroup.add(Box.createRigidArea(new Dimension(0, 6)));
        formGroup.add(hintLabel);
        formGroup.add(Box.createRigidArea(new Dimension(0, 8)));
        // 只有当提示文字不为空时才显示
        if (hint != null && !hint.trim().isEmpty()) {
            formGroup.add(Box.createRigidArea(new Dimension(0, 6)));
            JLabel hintLabel = new JLabel(hint);
            hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
            hintLabel.setForeground(LIGHT_TEXT);
            hintLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
            formGroup.add(hintLabel);
            formGroup.add(Box.createRigidArea(new Dimension(0, 8)));
        } else {
            formGroup.add(Box.createRigidArea(new Dimension(0, 8)));
        }
        
        return formGroup;
    }
    /**
     * 根据割刀宽度更新割草宽度和安全距离
     */
    private void updateMowingWidthAndSafetyDistance() {
        try {
            String bladeWidthStr = bladeWidthField.getText().trim();
            if (bladeWidthStr == null || bladeWidthStr.isEmpty()) {
                mowingWidthField.setText("");
                safetyDistanceField.setText("");
                return;
            }
            double bladeWidthMeters = Double.parseDouble(bladeWidthStr);
            if (bladeWidthMeters <= 0) {
                mowingWidthField.setText("");
                safetyDistanceField.setText("");
                return;
            }
            // 调用 Gecaokuanjisuan 类计算割草宽度(返回厘米,保留2位小数,需要转换为米)
            double mowingWidthCm = Gecaokuanjisuan.calculateMowingWidth(bladeWidthMeters);
            if (mowingWidthCm > 0) {
                // 将厘米转换为米,并保留2位小数
                double mowingWidthMeters = mowingWidthCm / 100.0;
                mowingWidthField.setText(String.format(Locale.US, "%.2f", mowingWidthMeters));
            } else {
                mowingWidthField.setText("");
            }
            // 调用 Gecaoanquanjuli 类计算安全距离
            String safetyDistanceStr = Gecaoanquanjuli.calculateSafetyDistanceFromString(bladeWidthStr);
            safetyDistanceField.setText(safetyDistanceStr);
        } catch (NumberFormatException e) {
            mowingWidthField.setText("");
            safetyDistanceField.setText("");
        } catch (Exception e) {
            e.printStackTrace();
            mowingWidthField.setText("");
            safetyDistanceField.setText("");
        }
    }
    private void generateMowingPath() {
        if (!dikuaiData.containsKey("boundaryDrawn")) {
            JOptionPane.showMessageDialog(this, "请先完成边界绘制后再生成路径", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("请先完成边界绘制后再生成路径。", false);
            setPathAvailability(false);
            showStep(2);
            return;
        }
@@ -944,15 +1341,16 @@
        }
        if (boundaryCoords == null) {
            JOptionPane.showMessageDialog(this, "未找到有效的地块边界坐标,无法生成路径", "提示", JOptionPane.WARNING_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("未找到有效的地块边界坐标,无法生成路径。", false);
            setPathAvailability(false);
            return;
        }
        String obstacleCoords = null;
        if (dikuai != null) {
            obstacleCoords = normalizeCoordinateValue(dikuai.getObstacleCoordinates());
        String landNumber = getPendingLandNumber();
        if (isMeaningfulValue(landNumber)) {
            obstacleCoords = normalizeCoordinateValue(resolveObstaclePayloadFromConfig(landNumber));
        }
        if (obstacleCoords == null) {
            obstacleCoords = normalizeCoordinateValue(dikuaiData.get("obstacleCoordinates"));
@@ -961,77 +1359,218 @@
        String patternDisplay = (String) mowingPatternCombo.getSelectedItem();
        dikuaiData.put("mowingPattern", patternDisplay);
        Object widthObj = mowingWidthSpinner.getValue();
        if (!(widthObj instanceof Number)) {
            JOptionPane.showMessageDialog(this, "割草宽度输入无效", "提示", JOptionPane.WARNING_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
        // 获取割草宽度(从文本框,单位:米)
        String mowingWidthStr = mowingWidthField.getText().trim();
        if (mowingWidthStr == null || mowingWidthStr.isEmpty()) {
            JOptionPane.showMessageDialog(this, "请先输入割草宽度", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("请先输入割草宽度。", false);
            setPathAvailability(false);
            return;
        }
        double widthCm = ((Number) widthObj).doubleValue();
        if (widthCm <= 0) {
        double widthMeters;
        try {
            widthMeters = Double.parseDouble(mowingWidthStr);
        } catch (NumberFormatException e) {
            JOptionPane.showMessageDialog(this, "割草宽度格式不正确", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("割草宽度格式不正确,请重新输入。", false);
            setPathAvailability(false);
            return;
        }
        if (widthMeters <= 0) {
            JOptionPane.showMessageDialog(this, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("割草宽度必须大于0,请重新设置。", false);
            setPathAvailability(false);
            return;
        }
        dikuaiData.put("mowingWidth", widthObj.toString());
        // 保存割草宽度(转换为厘米保存,保持与原有数据格式一致)
        double widthCm = widthMeters * 100.0;
        dikuaiData.put("mowingWidth", String.format(Locale.US, "%.2f", widthCm));
        String widthMeters = String.format(Locale.US, "%.2f", widthCm / 100.0);
        // 获取安全距离
        String safetyDistanceStr = safetyDistanceField.getText().trim();
        double safetyDistanceMeters = Double.NaN;
        if (safetyDistanceStr != null && !safetyDistanceStr.isEmpty()) {
            try {
                safetyDistanceMeters = Double.parseDouble(safetyDistanceStr);
            } catch (NumberFormatException e) {
                // 使用NaN,让系统自动计算
            }
        }
        String widthMetersStr = String.format(Locale.US, "%.3f", widthMeters);
        String safetyDistanceMetersStr = Double.isNaN(safetyDistanceMeters) ? null : String.format(Locale.US, "%.3f", safetyDistanceMeters);
        String plannerMode = resolvePlannerMode(patternDisplay);
        try {
            List<Lunjingguihua.PathSegment> segments = Lunjingguihua.generatePathSegments(
            // 使用与路径规划页面相同的方法:Lunjingguihua.generatePathFromStrings
            String plannedPath = Lunjingguihua.generatePathFromStrings(
                boundaryCoords,
                obstacleCoords,
                widthMeters,
                obstacleCoords != null ? obstacleCoords : "",
                widthMetersStr,
                safetyDistanceMetersStr,
                plannerMode
            );
            String plannedPath = Lunjingguihua.formatPathSegments(segments);
            if (!isMeaningfulValue(plannedPath)) {
                JOptionPane.showMessageDialog(this, "生成割草路径失败: 生成结果为空", "错误", JOptionPane.ERROR_MESSAGE);
                if (createButton != null) {
                    createButton.setEnabled(false);
                }
                dikuaiData.remove("plannedPath");
                showPathGenerationMessage("生成割草路径失败:生成结果为空。", false);
                setPathAvailability(false);
                return;
            }
            dikuaiData.put("plannedPath", plannedPath);
            if (createButton != null) {
                createButton.setEnabled(true);
            if (isMeaningfulValue(boundaryCoords)) {
                dikuaiData.put("boundaryCoordinates", boundaryCoords);
            }
            JOptionPane.showMessageDialog(this,
                "已根据当前设置生成割草路径,共生成 " + segments.size() + " 段。",
                "成功",
                JOptionPane.INFORMATION_MESSAGE);
            if (isMeaningfulValue(obstacleCoords)) {
                dikuaiData.put("obstacleCoordinates", obstacleCoords);
            }
            dikuaiData.put("plannedPath", plannedPath);
            setPathAvailability(true);
            showPathGenerationMessage(
                "已根据当前设置生成割草路径。\n点击预览按钮可在主页面查看效果。",
                true);
        } catch (IllegalArgumentException ex) {
            JOptionPane.showMessageDialog(this, "生成割草路径失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("生成割草路径失败:" + ex.getMessage(), false);
            setPathAvailability(false);
        } catch (Exception ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(this, "生成割草路径时发生异常: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("生成割草路径时发生异常:" + ex.getMessage(), false);
            setPathAvailability(false);
        }
    }
    private void previewMowingPath() {
        if (!hasGeneratedPath()) {
            showPathGenerationMessage("请先生成割草路径后再预览。", false);
            setPathAvailability(false);
            return;
        }
        persistStep3Inputs();
        String landNumber = getPendingLandNumber();
        String trimmedAreaName = areaNameField.getText() != null ? areaNameField.getText().trim() : "";
        String displayAreaName = isMeaningfulValue(trimmedAreaName) ? trimmedAreaName : landNumber;
        String plannedPath = dikuaiData.get("plannedPath");
        if (!isMeaningfulValue(plannedPath)) {
            showPathGenerationMessage("请先生成割草路径后再预览。", false);
            setPathAvailability(false);
            return;
        }
        String boundary = null;
        Dikuai pending = getOrCreatePendingDikuai();
        if (pending != null) {
            boundary = normalizeCoordinateValue(pending.getBoundaryCoordinates());
        }
        if (boundary == null) {
            boundary = normalizeCoordinateValue(dikuaiData.get("boundaryCoordinates"));
        }
        String obstacles = normalizeCoordinateValue(dikuaiData.get("obstacleCoordinates"));
        if (!isMeaningfulValue(obstacles)) {
            obstacles = resolveObstaclePayloadFromConfig(landNumber);
            if (isMeaningfulValue(obstacles)) {
                dikuaiData.put("obstacleCoordinates", obstacles);
            }
        }
        Shouye shouye = Shouye.getInstance();
        if (shouye == null) {
            JOptionPane.showMessageDialog(this, "无法打开主页面,请稍后重试", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        dikuaiData.put("areaName", trimmedAreaName);
        if (isMeaningfulValue(boundary)) {
            dikuaiData.put("boundaryCoordinates", boundary);
        }
        pendingLandNumber = landNumber;
        captureSessionSnapshot();
        resumeRequested = true;
        boolean started = shouye.startMowingPathPreview(
            landNumber,
            displayAreaName,
            boundary,
            obstacles,
            plannedPath,
            AddDikuai::resumeFromPreview
        );
        if (!started) {
            resumeRequested = false;
            JOptionPane.showMessageDialog(this, "无法启动预览,请稍后再试", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        closePreviewAndDispose();
    }
    private void persistStep3Inputs() {
        String trimmedName = areaNameField.getText() != null ? areaNameField.getText().trim() : "";
        dikuaiData.put("areaName", trimmedName);
        if (mowingPatternCombo != null) {
            Object selection = mowingPatternCombo.getSelectedItem();
            if (selection != null) {
                dikuaiData.put("mowingPattern", selection.toString());
            }
        }
        // 保存割草宽度(从文本框获取,单位:米,转换为厘米保存)
        if (mowingWidthField != null) {
            String widthStr = mowingWidthField.getText().trim();
            if (widthStr != null && !widthStr.isEmpty()) {
                try {
                    double widthMeters = Double.parseDouble(widthStr);
                    double widthCm = widthMeters * 100.0;
                    dikuaiData.put("mowingWidth", String.format(Locale.US, "%.2f", widthCm));
                } catch (NumberFormatException e) {
                    // 保持原值
                    dikuaiData.put("mowingWidth", widthStr);
                }
            }
        }
    }
    private void captureSessionSnapshot() {
        if (activeSession == null) {
            activeSession = new DrawingSession();
        }
        String landNumber = getPendingLandNumber();
        activeSession.landNumber = landNumber;
        activeSession.areaName = areaNameField.getText() != null ? areaNameField.getText().trim() : "";
        activeSession.drawingCompleted = true;
        activeSession.data = new HashMap<>(dikuaiData);
    }
    private void closePreviewAndDispose() {
        setVisible(false);
        dispose();
    }
    
    private JButton createPrimaryButton(String text, int fontSize) {
        JButton button = new JButton(text);
        JButton button = buttonset.createStyledButton(text, PRIMARY_COLOR);
        button.setFont(new Font("微软雅黑", Font.BOLD, fontSize));
        button.setBackground(PRIMARY_COLOR);
        button.setForeground(WHITE);
        button.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(PRIMARY_DARK, 2),
            BorderFactory.createEmptyBorder(12, 25, 12, 25)
        ));
        button.setFocusPainted(false);
        button.setCursor(new Cursor(Cursor.HAND_CURSOR));
        // 按钮悬停效果
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
@@ -1039,7 +1578,7 @@
                    button.setBackground(PRIMARY_DARK);
                }
            }
            @Override
            public void mouseExited(MouseEvent e) {
                if (button.isEnabled()) {
@@ -1047,9 +1586,47 @@
                }
            }
        });
        return button;
    }
    private void showPathGenerationMessage(String message, boolean success) {
        if (pathGenerationMessageArea == null || pathMessageWrapper == null) {
            return;
        }
        String display = message == null ? "" : message.trim();
        if (display.isEmpty()) {
            dikuaiData.remove(KEY_PATH_MESSAGE_TEXT);
            dikuaiData.remove(KEY_PATH_MESSAGE_SUCCESS);
        } else {
            dikuaiData.put(KEY_PATH_MESSAGE_TEXT, display);
            dikuaiData.put(KEY_PATH_MESSAGE_SUCCESS, success ? "true" : "false");
        }
        pathGenerationMessageArea.setText(display);
        Color borderColor = success ? PRIMARY_COLOR : ERROR_COLOR;
        Color textColor = success ? PRIMARY_DARK : ERROR_COLOR;
        Color backgroundColor = success ? PRIMARY_LIGHT : new Color(255, 235, 238);
        pathGenerationMessageArea.setForeground(textColor);
        pathMessageWrapper.setBackground(backgroundColor);
        pathMessageWrapper.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(borderColor, 1),
            BorderFactory.createEmptyBorder(12, 12, 12, 12)
        ));
        pathMessageWrapper.setVisible(!display.isEmpty());
        pathMessageWrapper.revalidate();
        pathMessageWrapper.repaint();
    }
    private void setPathAvailability(boolean available) {
        boolean effective = available && currentStep == 3;
        if (createButton != null) {
            createButton.setEnabled(effective);
        }
        if (previewButton != null) {
            boolean visible = previewButton.isVisible();
            previewButton.setEnabled(effective && visible);
        }
    }
    
    private JPanel createButtonPanel() {
        JPanel buttonPanel = new JPanel();
@@ -1057,25 +1634,32 @@
        buttonPanel.setBackground(WHITE);
        buttonPanel.setBorder(BorderFactory.createEmptyBorder(20, 0, 0, 0));
        prevButton = new JButton("上一步");
        prevButton = buttonset.createStyledButton("上一步", MEDIUM_GRAY);
        prevButton.setFont(new Font("微软雅黑", Font.BOLD, 16));
        prevButton.setBackground(MEDIUM_GRAY);
        prevButton.setForeground(TEXT_COLOR);
        prevButton.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 25, 10, 25)
        ));
        prevButton.setFocusPainted(false);
        prevButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
        nextButton = createPrimaryButton("下一步", 16);
        createButton = createPrimaryButton("保存", 16);
        createButton.setVisible(false);
    createButton.setEnabled(false);
        createButton.setEnabled(false);
        previewButton = createPrimaryButton("预览", 16);
        previewButton.setVisible(false);
        previewButton.setEnabled(false);
        previewButtonSpacer = Box.createHorizontalStrut(15);
        previewButtonSpacer.setVisible(false);
        buttonPanel.add(prevButton);
        buttonPanel.add(Box.createHorizontalGlue());
        buttonPanel.add(nextButton);
        buttonPanel.add(previewButtonSpacer);
        buttonPanel.add(previewButton);
        buttonPanel.add(Box.createHorizontalStrut(15));
        buttonPanel.add(createButton);
@@ -1086,79 +1670,91 @@
        if (boundaryCountLabel == null) {
            return;
        }
        int count = Coordinate.coordinates != null ? Coordinate.coordinates.size() : 0;
        double area = jisuanmianjie.calculatePolygonArea();
        DecimalFormat areaFormat = new DecimalFormat("0.00");
        boundaryCountLabel.setText("已采集到边界点" + count + "个,当前地块面积为" + areaFormat.format(area) + "㎡");
        // 使用优化后的边界数据计算点数和面积
        String optimizedXYStr = dikuaiData.get("optimizedBoundaryXY");
        if (optimizedXYStr != null && !optimizedXYStr.isEmpty() && !optimizedXYStr.startsWith("ERROR")) {
            int count = Bianjieyouhuatoxy.calculatePointCount(optimizedXYStr);
            String areaStr = Bianjieyouhuatoxy.calculateArea(optimizedXYStr);
            boundaryCountLabel.setText("已采集到边界点" + count + "个,当前地块面积为" + areaStr + "㎡");
        } else {
            // 如果没有优化后的数据,使用原始数据
            int count = Coordinate.coordinates != null ? Coordinate.coordinates.size() : 0;
            double area = jisuanmianjie.calculatePolygonArea();
            DecimalFormat areaFormat = new DecimalFormat("0.00");
            boundaryCountLabel.setText("已采集到边界点" + count + "个,当前地块面积为" + areaFormat.format(area) + "㎡");
        }
        boundaryCountLabel.setVisible(true);
    }
    private boolean prepareBoundaryTransition() {
        int count = Coordinate.coordinates != null ? Coordinate.coordinates.size() : 0;
        // TODO: 恢复边界点数校验
        // if (count < 3) {
        //     JOptionPane.showMessageDialog(this, "采集的边界点不足,无法生成地块边界", "提示", JOptionPane.WARNING_MESSAGE);
        //     return false;
        // }
        double area = jisuanmianjie.calculatePolygonArea();
        // TODO: 恢复地块面积校验
        // if (area <= 0) {
        //     JOptionPane.showMessageDialog(this, "当前地块面积为0,无法继续", "提示", JOptionPane.WARNING_MESSAGE);
        //     return false;
        // }
        Device device = new Device();
        device.initFromProperties();
        String baseStationCoordinates = device.getBaseStationCoordinates();
        if (baseStationCoordinates != null) {
            baseStationCoordinates = baseStationCoordinates.trim();
        }
        if (baseStationCoordinates == null || baseStationCoordinates.isEmpty() || "-1".equals(baseStationCoordinates)) {
            JOptionPane.showMessageDialog(this, "未获取到有效的基准站坐标,请先在基准站管理中设置", "提示", JOptionPane.WARNING_MESSAGE);
        BoundarySnapshotResult snapshot = computeBoundarySnapshot();
        if (!snapshot.success) {
            if (snapshot.errorMessage != null) {
                JOptionPane.showMessageDialog(this, snapshot.errorMessage, "提示", snapshot.messageType);
            }
            return false;
        }
        String optimizedBoundary;
        try {
            optimizedBoundary = bianjieguihua2.processCoordinateListAuto(baseStationCoordinates);
        } catch (RuntimeException ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(this, "生成地块边界失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            return false;
        }
        String originalBoundary = buildOriginalBoundaryString();
        DecimalFormat areaFormat = new DecimalFormat("0.00");
        String areaString = areaFormat.format(area);
        String landNumber = getPendingLandNumber();
        Dikuai dikuai = getOrCreatePendingDikuai();
        if (dikuai != null) {
            dikuai.setBoundaryOriginalCoordinates(originalBoundary);
            dikuai.setBoundaryCoordinates(optimizedBoundary);
            dikuai.setLandArea(areaString);
            dikuai.setBaseStationCoordinates(baseStationCoordinates);
            dikuai.setBoundaryOriginalCoordinates(snapshot.originalBoundary);
            dikuai.setBoundaryCoordinates(snapshot.optimizedBoundary);
            dikuai.setLandArea(snapshot.areaSqMeters);
            dikuai.setBaseStationCoordinates(snapshot.baseStationCoordinates);
            dikuai.setUpdateTime(getCurrentTime());
            Dikuai.putDikuai(landNumber, dikuai);
        }
        dikuaiData.put("boundaryOriginalCoordinates", originalBoundary);
        dikuaiData.put("boundaryCoordinates", optimizedBoundary);
        dikuaiData.put("landArea", areaString);
        dikuaiData.put("baseStationCoordinates", baseStationCoordinates);
        dikuaiData.put("boundaryOriginalCoordinates", snapshot.originalBoundary);
        dikuaiData.put("boundaryCoordinates", snapshot.optimizedBoundary);
        dikuaiData.put("landArea", snapshot.areaSqMeters);
        dikuaiData.put("baseStationCoordinates", snapshot.baseStationCoordinates);
        if (activeSession != null) {
            activeSession.data.put("boundaryOriginalCoordinates", snapshot.originalBoundary);
            activeSession.data.put("boundaryCoordinates", snapshot.optimizedBoundary);
            activeSession.data.put("landArea", snapshot.areaSqMeters);
            activeSession.data.put("baseStationCoordinates", snapshot.baseStationCoordinates);
        }
        return true;
    }
    private String buildOriginalBoundaryString() {
        if (Coordinate.coordinates == null || Coordinate.coordinates.isEmpty()) {
    private static List<Coordinate> sanitizeCoordinateList(List<Coordinate> source) {
        if (source == null || source.isEmpty()) {
            return Collections.emptyList();
        }
        List<Coordinate> snapshot = new ArrayList<>();
        for (Coordinate coordinate : source) {
            if (coordinate != null) {
                snapshot.add(coordinate);
            }
        }
        if (snapshot.isEmpty()) {
            return Collections.emptyList();
        }
        DecimalFormat latLonFormat = new DecimalFormat("0.000000");
        LinkedHashMap<String, Coordinate> unique = new LinkedHashMap<>();
        for (Coordinate coord : snapshot) {
            double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection());
            double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection());
            String key = latLonFormat.format(lat) + "," + latLonFormat.format(lon);
            unique.putIfAbsent(key, coord);
        }
        return new ArrayList<>(unique.values());
    }
    private static String buildOriginalBoundaryString(List<Coordinate> coordinates) {
        if (coordinates == null || coordinates.isEmpty()) {
            return "-1";
        }
        StringBuilder sb = new StringBuilder();
        DecimalFormat latLonFormat = new DecimalFormat("0.000000");
        DecimalFormat elevationFormat = new DecimalFormat("0.00");
        for (Coordinate coord : Coordinate.coordinates) {
        for (Coordinate coord : coordinates) {
            double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection());
            double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection());
            double elevation = coord.getElevation();
@@ -1173,7 +1769,7 @@
        return sb.toString();
    }
    private double convertToDecimalDegree(String dmm, String direction) {
    private static double convertToDecimalDegree(String dmm, String direction) {
        if (dmm == null || dmm.isEmpty()) {
            return 0.0;
        }
@@ -1219,17 +1815,88 @@
        if (boundaryCountLabel != null) {
            boundaryCountLabel.setVisible(false);
        }
        if (boundaryXYTextArea != null) {
            boundaryXYTextArea.setVisible(false);
            boundaryXYTextArea.setText("");
        }
        if (boundaryXYScrollPane != null) {
            boundaryXYScrollPane.setVisible(false);
        }
    }
    /**
     * 更新边界XY坐标显示
     */
    private void updateBoundaryXYDisplay() {
        if (boundaryXYTextArea == null || boundaryXYScrollPane == null) {
            return;
        }
        // 获取优化后的边界XY坐标
        String optimizedXYStr = dikuaiData.get("optimizedBoundaryXY");
        if (optimizedXYStr != null && !optimizedXYStr.isEmpty() && !optimizedXYStr.startsWith("ERROR")) {
            boundaryXYTextArea.setText(optimizedXYStr);
            boundaryXYTextArea.setVisible(true);
            boundaryXYScrollPane.setVisible(true);
        } else {
            // 如果没有优化后的数据,尝试从原始坐标优化并显示
            // 如果已经有原始坐标但还没有优化,则进行优化
            if (dikuaiData.containsKey("boundaryDrawn") &&
                (Coordinate.coordinates != null && !Coordinate.coordinates.isEmpty())) {
                // 尝试优化边界
                optimizeBoundaryAndSave();
                // 再次获取优化后的坐标
                optimizedXYStr = dikuaiData.get("optimizedBoundaryXY");
                if (optimizedXYStr != null && !optimizedXYStr.isEmpty() && !optimizedXYStr.startsWith("ERROR")) {
                    boundaryXYTextArea.setText(optimizedXYStr);
                    boundaryXYTextArea.setVisible(true);
                    boundaryXYScrollPane.setVisible(true);
                } else {
                    boundaryXYTextArea.setText("边界坐标数据未就绪");
                    boundaryXYTextArea.setVisible(true);
                    boundaryXYScrollPane.setVisible(true);
                }
            } else {
                boundaryXYTextArea.setText("边界坐标数据未就绪");
                boundaryXYTextArea.setVisible(true);
                boundaryXYScrollPane.setVisible(true);
            }
        }
    }
    private boolean hasGeneratedPath() {
        return isMeaningfulValue(dikuaiData.get("plannedPath"));
    }
    private String normalizeCoordinateValue(String value) {
    private String resolveObstaclePayloadFromConfig(String landNumber) {
        if (!isMeaningfulValue(landNumber)) {
            return null;
        }
        try {
            File configFile = new File("Obstacledge.properties");
            if (!configFile.exists()) {
                return null;
            }
            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
            if (!manager.loadFromFile(configFile.getAbsolutePath())) {
                return null;
            }
            Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
            if (plot == null) {
                return null;
            }
            return Obstacledge.buildPlannerPayload(plot.getObstacles());
        } catch (Exception ex) {
            System.err.println("加载障碍物配置失败: " + ex.getMessage());
            return null;
        }
    }
    private static String normalizeCoordinateValue(String value) {
        return isMeaningfulValue(value) ? value.trim() : null;
    }
    private boolean isMeaningfulValue(String value) {
    private static boolean isMeaningfulValue(String value) {
        if (value == null) {
            return false;
        }
@@ -1237,6 +1904,51 @@
        return !trimmed.isEmpty() && !"-1".equals(trimmed);
    }
    private static BoundarySnapshotResult computeBoundarySnapshot() {
        List<Coordinate> uniqueCoordinates;
        synchronized (Coordinate.coordinates) {
            uniqueCoordinates = sanitizeCoordinateList(Coordinate.coordinates);
            Coordinate.coordinates.clear();
            Coordinate.coordinates.addAll(uniqueCoordinates);
        }
        if (uniqueCoordinates.size() < 3) {
            return BoundarySnapshotResult.failure("采集的边界点不足,无法生成地块边界", JOptionPane.WARNING_MESSAGE);
        }
        double area = jisuanmianjie.calculatePolygonArea();
        if (area <= 0) {
            return BoundarySnapshotResult.failure("当前地块面积为0,无法继续", JOptionPane.WARNING_MESSAGE);
        }
    BaseStation baseStation = new BaseStation();
    baseStation.load();
    String baseStationCoordinates = normalizeCoordinateValue(baseStation.getInstallationCoordinates());
        if (!isMeaningfulValue(baseStationCoordinates)) {
            return BoundarySnapshotResult.failure("未获取到有效的基准站坐标,请先在基准站管理中设置", JOptionPane.WARNING_MESSAGE);
        }
        String optimizedBoundary;
        try {
            // 构建边界字符串,格式为 "(lat1,lon1,alt1;lat2,lon2,alt2;...)"
            String boundaryStr = buildBoundaryStringForOptimization(uniqueCoordinates);
            // 调用 Bianjieyouhuatoxy.optimizeBoundary 方法
            optimizedBoundary = Bianjieyouhuatoxy.optimizeBoundary(baseStationCoordinates, boundaryStr);
            if (optimizedBoundary == null || optimizedBoundary.isEmpty() || optimizedBoundary.startsWith("ERROR")) {
                return BoundarySnapshotResult.failure("生成地块边界失败: 优化后的边界坐标无效", JOptionPane.ERROR_MESSAGE);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            return BoundarySnapshotResult.failure("生成地块边界失败: " + ex.getMessage(), JOptionPane.ERROR_MESSAGE);
        }
    String originalBoundary = buildOriginalBoundaryString(uniqueCoordinates);
        DecimalFormat areaFormat = new DecimalFormat("0.00");
        String areaString = areaFormat.format(area);
        return BoundarySnapshotResult.success(originalBoundary, optimizedBoundary, areaString, baseStationCoordinates);
    }
    private String resolvePlannerMode(String patternDisplay) {
        if (patternDisplay == null) {
            return "parallel";
@@ -1278,6 +1990,10 @@
        
        // 创建地块按钮
        createButton.addActionListener(e -> createDikuai());
        if (previewButton != null) {
            previewButton.addActionListener(e -> previewMowingPath());
        }
        
        // 关闭对话框
        addWindowListener(new WindowAdapter() {
@@ -1306,11 +2022,24 @@
        if (step < 3) {
            nextButton.setVisible(true);
            createButton.setVisible(false);
            createButton.setEnabled(false);
            setPathAvailability(false);
            if (previewButton != null) {
                previewButton.setVisible(false);
                previewButton.setEnabled(false);
            }
            if (previewButtonSpacer != null) {
                previewButtonSpacer.setVisible(false);
            }
        } else {
            nextButton.setVisible(false);
            createButton.setVisible(true);
            createButton.setEnabled(hasGeneratedPath());
            if (previewButton != null) {
                previewButton.setVisible(true);
            }
            if (previewButtonSpacer != null) {
                previewButtonSpacer.setVisible(true);
            }
            setPathAvailability(hasGeneratedPath());
        }
        Container parent = prevButton.getParent();
@@ -1352,7 +2081,20 @@
            case 3:
                dikuaiData.put("mowingPattern", (String) mowingPatternCombo.getSelectedItem());
                dikuaiData.put("mowingWidth", mowingWidthSpinner.getValue().toString());
                // 保存割草宽度(从文本框获取,单位:米,转换为厘米保存)
                if (mowingWidthField != null) {
                    String widthStr = mowingWidthField.getText().trim();
                    if (widthStr != null && !widthStr.isEmpty()) {
                        try {
                            double widthMeters = Double.parseDouble(widthStr);
                            double widthCm = widthMeters * 100.0;
                            dikuaiData.put("mowingWidth", String.format(Locale.US, "%.2f", widthCm));
                        } catch (NumberFormatException e) {
                            // 保持原值
                            dikuaiData.put("mowingWidth", widthStr);
                        }
                    }
                }
                if (!hasGeneratedPath()) {
                    JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.WARNING_MESSAGE);
                    return false;
@@ -1365,19 +2107,23 @@
    private boolean startDrawingBoundary() {
        String method = dikuaiData.get("drawingMethod");
        if ("mower".equals(method)) {
//            if (captureGnssSnapshot()) {
         if (true) {
                JOptionPane.showMessageDialog(this, "已记录割草机当前位置作为边界起点,请继续驾驶割草机完成边界绘制", "开始绘制边界", JOptionPane.INFORMATION_MESSAGE);
                closeForDrawingSession();
                return true;
            } else {
                JOptionPane.showMessageDialog(this, "未能获取有效的割草机定位数据,请确认设备状态后重试", "提示", JOptionPane.WARNING_MESSAGE);
            Shouye shouye = Shouye.getInstance();
            if (shouye == null) {
                JOptionPane.showMessageDialog(this, "无法进入主页面,请稍后重试", "提示", JOptionPane.WARNING_MESSAGE);
                return false;
            }
        } else if ("handheld".equals(method)) {
            JOptionPane.showMessageDialog(this, "正在使用手持设备绘制边界,请沿地块边界行走...", "开始绘制边界", JOptionPane.INFORMATION_MESSAGE);
            if (!shouye.startMowerBoundaryCapture()) {
                JOptionPane.showMessageDialog(this, "未能开始割草机绘制,请确认设备状态和基准站设置后重试", "提示", JOptionPane.WARNING_MESSAGE);
                return false;
            }
            closeForDrawingSession();
            return true;
        } else if ("handheld".equals(method)) {
            if (closeForHandheldDrawingSession()) {
                return true;
            }
            resetDrawingState();
            return false;
        }
        return false;
    }
@@ -1546,6 +2292,24 @@
        dispose();
    }
    private boolean closeForHandheldDrawingSession() {
        isDrawing = false;
        Coordinate.setStartSaveGngga(false);
        Shouye shouye = Shouye.getInstance();
        if (shouye == null) {
            JOptionPane.showMessageDialog(this, "无法进入主页面,请稍后重试", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
        }
        boolean started = shouye.startHandheldBoundaryCapture();
        if (!started) {
            JOptionPane.showMessageDialog(this, "无法开始手持采集,请确认设备状态", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
        }
        setVisible(false);
        dispose();
        return true;
    }
    private void applySessionData(DrawingSession session) {
        if (session == null) {
            return;
@@ -1553,14 +2317,14 @@
        pendingLandNumber = session.landNumber;
        dikuaiData.clear();
        dikuaiData.putAll(session.data);
    updateLandNumberField(pendingLandNumber);
        updateLandNumberField(pendingLandNumber);
        areaNameField.setText(session.areaName != null ? session.areaName : "");
        String method = session.data.get("drawingMethod");
        if (method != null) {
            JPanel panel = drawingOptionPanels.get(method);
            if (panel != null) {
                selectDrawingOption(panel, method);
                selectDrawingOption(panel, method, false);
            }
        }
@@ -1575,6 +2339,7 @@
            isDrawing = false;
            showStep(2);
            showBoundaryPointSummary();
            updateBoundaryXYDisplay();
        } else {
            if (startEndDrawingBtn != null) {
                startEndDrawingBtn.setText("开始绘制");
@@ -1585,14 +2350,105 @@
            showStep(1);
            hideBoundaryPointSummary();
        }
        restoreGeneratedPathState(session);
    }
    private void restoreGeneratedPathState(DrawingSession session) {
        if (session == null || session.data == null) {
            showPathGenerationMessage("", true);
            return;
        }
        Map<String, String> data = session.data;
        if (mowingPatternCombo != null) {
            String pattern = data.get("mowingPattern");
            if (pattern != null) {
                ComboBoxModel<String> model = mowingPatternCombo.getModel();
                for (int i = 0; i < model.getSize(); i++) {
                    String candidate = model.getElementAt(i);
                    if (pattern.equals(candidate)) {
                        mowingPatternCombo.setSelectedIndex(i);
                        break;
                    }
                }
            }
        }
        // 恢复割草宽度:从保存的割草宽度(厘米)转换为米并设置
        String width = data.get("mowingWidth");
        if (isMeaningfulValue(width) && mowingWidthField != null) {
            try {
                double mowingWidthCm = Double.parseDouble(width.trim());
                // 将厘米转换为米
                double mowingWidthMeters = mowingWidthCm / 100.0;
                mowingWidthField.setText(String.format(Locale.US, "%.3f", mowingWidthMeters));
            } catch (NumberFormatException ignored) {
                // 保持当前值
            }
        }
        // 恢复割刀宽度:从保存的割草宽度反推割刀宽度并设置
        // 由于割草宽度 = 割刀宽度 * 0.85,所以割刀宽度 = 割草宽度 / 0.85
        if (isMeaningfulValue(width) && bladeWidthField != null) {
            try {
                double mowingWidthCm = Double.parseDouble(width.trim());
                // 将厘米转换为米,然后反推割刀宽度
                double mowingWidthMeters = mowingWidthCm / 100.0;
                double bladeWidthMeters = mowingWidthMeters / 0.85;
                bladeWidthField.setText(String.format(Locale.US, "%.3f", bladeWidthMeters));
                // 触发自动计算安全距离
                if (safetyDistanceField != null) {
                    String safetyDistanceStr = Gecaoanquanjuli.calculateSafetyDistanceFromString(
                        String.format(Locale.US, "%.3f", bladeWidthMeters));
                    safetyDistanceField.setText(safetyDistanceStr);
                }
            } catch (NumberFormatException ignored) {
                // 保持当前值
            }
        }
        boolean hasPath = isMeaningfulValue(data.get("plannedPath"));
        if (!hasPath) {
            showPathGenerationMessage("", true);
            if (currentStep == 3) {
                setPathAvailability(false);
            }
            return;
        }
        String message = data.get(KEY_PATH_MESSAGE_TEXT);
        boolean success = !"false".equalsIgnoreCase(data.get(KEY_PATH_MESSAGE_SUCCESS));
        showStep(3);
        if (isMeaningfulValue(message)) {
            showPathGenerationMessage(message, success);
        } else {
            showPathGenerationMessage("已生成割草路径,可点击“预览”按钮查看效果。", true);
        }
        setPathAvailability(true);
    }
    public static void finishDrawingSession() {
        Shouye shouye = Shouye.getInstance();
        List<Point2D.Double> previewPoints = shouye != null
            ? shouye.getHandheldTemporaryPointsSnapshot()
            : Collections.emptyList();
    recordTemporaryBoundaryPoints(previewPoints);
        BoundarySnapshotResult snapshot = computeBoundarySnapshot();
        if (snapshot.success && activeSession != null) {
            activeSession.data.put("boundaryOriginalCoordinates", snapshot.originalBoundary);
            activeSession.data.put("boundaryCoordinates", snapshot.optimizedBoundary);
            activeSession.data.put("landArea", snapshot.areaSqMeters);
            activeSession.data.put("baseStationCoordinates", snapshot.baseStationCoordinates);
        }
        if (shouye != null) {
            shouye.hideEndDrawingButton();
        }
        if (activeSession == null) {
            Shouye shouye = Shouye.getInstance();
            if (shouye != null) {
                shouye.hideEndDrawingButton();
            }
            return;
        }
@@ -1600,15 +2456,23 @@
        activeSession.data.put("boundaryDrawn", "true");
        Coordinate.setStartSaveGngga(false);
        Shouye shouye = Shouye.getInstance();
        if (shouye != null) {
            shouye.hideEndDrawingButton();
        }
        resumeRequested = true;
        Component parent = shouye != null ? shouye : null;
        showAddDikuaiDialog(parent);
    }
    public static void resumeFromPreview() {
        Shouye shouye = Shouye.getInstance();
        if (shouye != null) {
            shouye.exitMowingPathPreview();
        }
        if (activeSession == null) {
            return;
        }
        resumeRequested = true;
        Component parent = shouye != null ? shouye : null;
        SwingUtilities.invokeLater(() -> showAddDikuaiDialog(parent));
    }
    
    private void createDikuai() {
        if (!validateCurrentStep()) {