张世豪
4 天以前 32c98d4855b6178554c787103dc956d161e152b3
src/zhangaiwu/AddDikuai.java
@@ -11,22 +11,29 @@
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
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 lujing.Lunjingguihua;
import set.Setsys;
import ui.UIConfig;
import zhuye.MowerLocationData;
import zhuye.Shouye;
import zhuye.Coordinate;
import zhuye.buttonset;
import gecaoji.Device;
/**
 * 新增地块对话框 - 多步骤表单设计
@@ -46,6 +53,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 +66,10 @@
    private JTextField landNumberField;
    private JTextField areaNameField;
    private JComboBox<String> mowingPatternCombo;
    private JSpinner mowingWidthSpinner;
    private JTextField mowingWidthField; // 割草机割刀宽度
    private JTextField overlapDistanceField; // 相邻行重叠距离
    private JTextField mowingSafetyDistanceField; // 割草安全距离
    private JLabel calculatedMowingWidthLabel; // 计算后的割草宽度显示
    private JPanel previewPanel;
    private Map<String, JPanel> drawingOptionPanels = new HashMap<>();
    
@@ -65,8 +78,12 @@
    private JButton prevButton;
    private JButton nextButton;
    private JButton createButton;
    private JButton previewButton;
    private Component previewButtonSpacer;
    private JLabel boundaryCountLabel;
    private JPanel obstacleListContainer;
    private JTextArea pathGenerationMessageArea;
    private JPanel pathMessageWrapper;
    
    // 地块数据
    private Map<String, String> dikuaiData = new HashMap<>();
@@ -80,6 +97,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 +139,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);
@@ -223,10 +296,6 @@
        formGroup.add(areaNameField);
        formGroup.add(Box.createRigidArea(new Dimension(0, 8)));
        formGroup.add(hintLabel);
    formGroup.add(Box.createRigidArea(new Dimension(0, 20)));
    JPanel obstacleSection = createObstacleSummarySection();
    formGroup.add(obstacleSection);
        
        stepPanel.add(formGroup);
        stepPanel.add(Box.createVerticalGlue());
@@ -304,93 +373,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 +566,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();
@@ -638,8 +743,9 @@
                if (!optionPanel.isEnabled()) {
                    return;
                }
                selectDrawingOption(optionPanel, type);
                startEndDrawingBtn.setEnabled(true); // 选择后启用按钮
                if (selectDrawingOption(optionPanel, type, true)) {
                    startEndDrawingBtn.setEnabled(true); // 选择后启用按钮
                }
            }
            
            @Override
@@ -661,7 +767,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 +785,7 @@
                ((JLabel) oldTitle).setForeground(TEXT_COLOR);
            }
        }
        // 设置新的选中状态
        optionPanel.setBorder(BorderFactory.createLineBorder(PRIMARY_COLOR, 3));
        optionPanel.setBackground(PRIMARY_LIGHT);
@@ -680,9 +794,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() {
@@ -782,7 +902,7 @@
        settingsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        
        // 割草模式选择
        JPanel patternPanel = createFormGroup("割草模式", "选择割草路径的生成模式");
        JPanel patternPanel = createFormGroupWithoutHint("割草模式");
        mowingPatternCombo = new JComboBox<>(new String[]{"平行线", "螺旋式"});
        mowingPatternCombo.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        mowingPatternCombo.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
@@ -814,29 +934,27 @@
        
        patternPanel.add(mowingPatternCombo);
        settingsPanel.add(patternPanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        
        // 割草宽度设置
        JPanel widthPanel = createFormGroup("割草宽度", "设置割草机单次割草的宽度");
        // 割草机割刀宽度设置
        JPanel widthPanel = createFormGroupWithoutHint("割草机割刀宽度");
        JPanel widthInputPanel = new JPanel(new BorderLayout());
        widthInputPanel.setBackground(WHITE);
        widthInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        widthInputPanel.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(
        mowingWidthField = new JTextField("0.40");
        mowingWidthField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        mowingWidthField.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 12, 10, 12)
        ));
        
        // 添加微调器焦点效果
        mowingWidthSpinner.addFocusListener(new FocusAdapter() {
        // 添加文本框焦点效果和变化监听
        mowingWidthField.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                editor.getTextField().setBorder(BorderFactory.createCompoundBorder(
                mowingWidthField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(PRIMARY_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
@@ -844,27 +962,156 @@
            
            @Override
            public void focusLost(FocusEvent e) {
                editor.getTextField().setBorder(BorderFactory.createCompoundBorder(
                mowingWidthField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(BORDER_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
                updateCalculatedMowingWidth();
            }
        });
        JLabel unitLabel = new JLabel("米");
        unitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        unitLabel.setForeground(LIGHT_TEXT);
        unitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0));
        widthInputPanel.add(mowingWidthField, BorderLayout.CENTER);
        widthInputPanel.add(unitLabel, BorderLayout.EAST);
        widthPanel.add(widthInputPanel);
        settingsPanel.add(widthPanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        // 相邻行重叠距离设置
        JPanel overlapPanel = createFormGroupWithoutHint("相邻行重叠距离");
        JPanel overlapInputPanel = new JPanel(new BorderLayout());
        overlapInputPanel.setBackground(WHITE);
        overlapInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        overlapInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        overlapDistanceField = new JTextField("0.06");
        overlapDistanceField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        overlapDistanceField.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 12, 10, 12)
        ));
        // 添加文本框焦点效果和变化监听
        overlapDistanceField.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                overlapDistanceField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(PRIMARY_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
            @Override
            public void focusLost(FocusEvent e) {
                overlapDistanceField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(BORDER_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
                updateCalculatedMowingWidth();
            }
        });
        JLabel overlapUnitLabel = new JLabel("米");
        overlapUnitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        overlapUnitLabel.setForeground(LIGHT_TEXT);
        overlapUnitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0));
        overlapInputPanel.add(overlapDistanceField, BorderLayout.CENTER);
        overlapInputPanel.add(overlapUnitLabel, BorderLayout.EAST);
        overlapPanel.add(overlapInputPanel);
        settingsPanel.add(overlapPanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        // 割草宽度显示(自动计算,不可修改)
        JPanel calculatedWidthPanel = createFormGroup("割草宽度", "割草宽度 = 割草机割刀宽度 - 重叠距离(自动计算)");
        JPanel calculatedWidthDisplayPanel = new JPanel(new BorderLayout());
        calculatedWidthDisplayPanel.setBackground(LIGHT_GRAY);
        calculatedWidthDisplayPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        calculatedWidthDisplayPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        calculatedWidthDisplayPanel.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 12, 10, 12)
        ));
        calculatedMowingWidthLabel = new JLabel("0.00 米");
        calculatedMowingWidthLabel.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        calculatedMowingWidthLabel.setForeground(TEXT_COLOR);
        calculatedWidthDisplayPanel.add(calculatedMowingWidthLabel, BorderLayout.CENTER);
        calculatedWidthPanel.add(calculatedWidthDisplayPanel);
        settingsPanel.add(calculatedWidthPanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        // 割草安全距离
        JPanel safetyDistancePanel = createFormGroupWithoutHint("割草安全距离");
        JPanel safetyDistanceInputPanel = new JPanel(new BorderLayout());
        safetyDistanceInputPanel.setBackground(WHITE);
        safetyDistanceInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        safetyDistanceInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        // 获取默认值
        String defaultSafetyDistance = "0.5"; // 默认值
        try {
            Device device = Device.getGecaoji();
            if (device != null) {
                String safetyDistance = device.getMowingSafetyDistance();
                if (safetyDistance != null && !safetyDistance.trim().isEmpty() && !"-1".equals(safetyDistance.trim())) {
                    defaultSafetyDistance = safetyDistance.trim();
                }
            }
        } catch (Exception e) {
            // 如果获取失败,使用默认值
        }
        mowingSafetyDistanceField = new JTextField(defaultSafetyDistance);
        mowingSafetyDistanceField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 12, 10, 12)
        ));
        // 添加文本框焦点效果
        mowingSafetyDistanceField.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(PRIMARY_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
            @Override
            public void focusLost(FocusEvent e) {
                mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(BORDER_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
        });
        
        JLabel unitLabel = new JLabel("厘米");
        unitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        unitLabel.setForeground(LIGHT_TEXT);
        unitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0));
        JLabel safetyDistanceUnitLabel = new JLabel("米");
        safetyDistanceUnitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        safetyDistanceUnitLabel.setForeground(LIGHT_TEXT);
        safetyDistanceUnitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0));
        
        widthInputPanel.add(mowingWidthSpinner, BorderLayout.CENTER);
        widthInputPanel.add(unitLabel, BorderLayout.EAST);
        safetyDistanceInputPanel.add(mowingSafetyDistanceField, BorderLayout.CENTER);
        safetyDistanceInputPanel.add(safetyDistanceUnitLabel, BorderLayout.EAST);
        
        widthPanel.add(widthInputPanel);
        settingsPanel.add(widthPanel);
        safetyDistancePanel.add(safetyDistanceInputPanel);
        settingsPanel.add(safetyDistancePanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 25)));
        
        stepPanel.add(settingsPanel);
        
        // 初始化计算后的割草宽度
        updateCalculatedMowingWidth();
        JButton generatePathButton = createPrimaryButton("生成割草路径", 16);
        generatePathButton.setAlignmentX(Component.LEFT_ALIGNMENT);
        generatePathButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 55));
@@ -872,11 +1119,69 @@
        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;
    }
    
    /**
     * 更新计算后的割草宽度显示
     */
    private void updateCalculatedMowingWidth() {
        if (calculatedMowingWidthLabel == null || mowingWidthField == null || overlapDistanceField == null) {
            return;
        }
        try {
            String widthText = mowingWidthField.getText().trim();
            String overlapText = overlapDistanceField.getText().trim();
            if (widthText.isEmpty() || overlapText.isEmpty()) {
                calculatedMowingWidthLabel.setText("0.00 米");
                return;
            }
            double bladeWidthMeters = Double.parseDouble(widthText);
            double overlapMeters = Double.parseDouble(overlapText);
            double calculatedWidthMeters = bladeWidthMeters - overlapMeters;
            if (calculatedWidthMeters <= 0) {
                calculatedMowingWidthLabel.setText("无效(割刀宽度需大于重叠距离)");
                calculatedMowingWidthLabel.setForeground(ERROR_COLOR);
            } else {
                calculatedMowingWidthLabel.setText(String.format(Locale.US, "%.2f 米", calculatedWidthMeters));
                calculatedMowingWidthLabel.setForeground(TEXT_COLOR);
            }
        } catch (NumberFormatException e) {
            calculatedMowingWidthLabel.setText("0.00 米");
        } catch (Exception e) {
            calculatedMowingWidthLabel.setText("0.00 米");
        }
    }
    private JPanel createInstructionPanel(String text) {
        JPanel instructionPanel = new JPanel(new BorderLayout());
        instructionPanel.setBackground(PRIMARY_LIGHT);
@@ -927,10 +1232,30 @@
        
        return formGroup;
    }
    private JPanel createFormGroupWithoutHint(String label) {
        JPanel formGroup = new JPanel();
        formGroup.setLayout(new BoxLayout(formGroup, BoxLayout.Y_AXIS));
        formGroup.setBackground(WHITE);
        formGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
        JLabel nameLabel = new JLabel(label);
        nameLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
        nameLabel.setForeground(TEXT_COLOR);
        nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        formGroup.add(nameLabel);
        formGroup.add(Box.createRigidArea(new Dimension(0, 8)));
        return formGroup;
    }
    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 +1269,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,25 +1287,79 @@
        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 widthText = mowingWidthField.getText().trim();
        if (widthText.isEmpty()) {
            JOptionPane.showMessageDialog(this, "割草机割刀宽度不能为空", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("割草机割刀宽度不能为空,请重新输入。", false);
            setPathAvailability(false);
            return;
        }
        double widthCm = ((Number) widthObj).doubleValue();
        if (widthCm <= 0) {
            JOptionPane.showMessageDialog(this, "割草宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE);
            if (createButton != null) {
                createButton.setEnabled(false);
            }
        double bladeWidthMeters;
        try {
            bladeWidthMeters = Double.parseDouble(widthText);
        } catch (NumberFormatException e) {
            JOptionPane.showMessageDialog(this, "割草机割刀宽度输入无效", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("割草机割刀宽度输入无效,请重新输入。", false);
            setPathAvailability(false);
            return;
        }
        dikuaiData.put("mowingWidth", widthObj.toString());
        if (bladeWidthMeters <= 0) {
            JOptionPane.showMessageDialog(this, "割草机割刀宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("割草机割刀宽度必须大于0,请重新设置。", false);
            setPathAvailability(false);
            return;
        }
        // 保存割草机割刀宽度(米)
        dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters));
        // 获取重叠距离
        String overlapText = overlapDistanceField.getText().trim();
        if (overlapText.isEmpty()) {
            JOptionPane.showMessageDialog(this, "相邻行重叠距离不能为空", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("相邻行重叠距离不能为空,请重新输入。", false);
            setPathAvailability(false);
            return;
        }
        double overlapMeters;
        try {
            overlapMeters = Double.parseDouble(overlapText);
        } catch (NumberFormatException e) {
            JOptionPane.showMessageDialog(this, "相邻行重叠距离输入无效", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("相邻行重叠距离输入无效,请重新输入。", false);
            setPathAvailability(false);
            return;
        }
        if (overlapMeters < 0) {
            JOptionPane.showMessageDialog(this, "相邻行重叠距离不能为负数", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("相邻行重叠距离不能为负数,请重新设置。", false);
            setPathAvailability(false);
            return;
        }
        // 保存重叠距离到地块数据
        dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters));
        // 计算实际使用的割草宽度(割刀宽度 - 重叠距离)
        double calculatedWidthMeters = bladeWidthMeters - overlapMeters;
        if (calculatedWidthMeters <= 0) {
            JOptionPane.showMessageDialog(this, "计算后的割草宽度必须大于0(割刀宽度必须大于重叠距离)", "提示", JOptionPane.WARNING_MESSAGE);
            dikuaiData.remove("plannedPath");
            showPathGenerationMessage("计算后的割草宽度必须大于0,请调整割刀宽度或重叠距离。", false);
            setPathAvailability(false);
            return;
        }
        String widthMeters = String.format(Locale.US, "%.2f", widthCm / 100.0);
        // 保存割草宽度(计算后的值,转换为厘米存储,保持与原有数据格式兼容)
        double calculatedWidthCm = calculatedWidthMeters * 100.0;
        dikuaiData.put("mowingWidth", String.format(Locale.US, "%.0f", calculatedWidthCm));
        String widthMeters = String.format(Locale.US, "%.2f", calculatedWidthMeters);
        String plannerMode = resolvePlannerMode(patternDisplay);
        try {
@@ -992,46 +1372,190 @@
            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(
                "已根据当前设置生成割草路径,共生成 " + segments.size() + " 段。\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 bladeWidthText = mowingWidthField.getText().trim();
            if (!bladeWidthText.isEmpty()) {
                try {
                    double bladeWidthMeters = Double.parseDouble(bladeWidthText);
                    dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters));
                } catch (NumberFormatException e) {
                    // 如果解析失败,直接保存文本
                    dikuaiData.put("mowingBladeWidth", bladeWidthText);
                }
            }
        }
        // 保存相邻行重叠距离
        if (overlapDistanceField != null) {
            String overlapText = overlapDistanceField.getText().trim();
            if (!overlapText.isEmpty()) {
                try {
                    double overlapMeters = Double.parseDouble(overlapText);
                    dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters));
                } catch (NumberFormatException e) {
                    // 如果解析失败,直接保存文本
                    dikuaiData.put("mowingOverlapDistance", overlapText);
                }
            }
        }
        // 计算并保存割草宽度(割刀宽度 - 重叠距离)
        if (mowingWidthField != null && overlapDistanceField != null) {
            try {
                String bladeWidthText = mowingWidthField.getText().trim();
                String overlapText = overlapDistanceField.getText().trim();
                if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) {
                    double bladeWidthMeters = Double.parseDouble(bladeWidthText);
                    double overlapMeters = Double.parseDouble(overlapText);
                    double calculatedWidthMeters = bladeWidthMeters - overlapMeters;
                    if (calculatedWidthMeters > 0) {
                        // 转换为厘米存储,保持与原有数据格式兼容
                        int widthCm = (int) Math.round(calculatedWidthMeters * 100.0);
                        dikuaiData.put("mowingWidth", Integer.toString(widthCm));
                    }
                }
            } catch (NumberFormatException e) {
                // 如果计算失败,不保存割草宽度
            }
        }
    }
    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 +1563,7 @@
                    button.setBackground(PRIMARY_DARK);
                }
            }
            @Override
            public void mouseExited(MouseEvent e) {
                if (button.isEnabled()) {
@@ -1047,9 +1571,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 +1619,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);
@@ -1094,71 +1663,73 @@
    }
    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 +1744,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;
        }
@@ -1225,11 +1796,35 @@
        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 +1832,45 @@
        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 {
            optimizedBoundary = bianjieguihua2.processCoordinateListAuto(baseStationCoordinates);
        } catch (RuntimeException 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 +1912,10 @@
        
        // 创建地块按钮
        createButton.addActionListener(e -> createDikuai());
        if (previewButton != null) {
            previewButton.addActionListener(e -> previewMowingPath());
        }
        
        // 关闭对话框
        addWindowListener(new WindowAdapter() {
@@ -1291,10 +1929,6 @@
    private void showStep(int step) {
        currentStep = step;
        cardLayout.show(stepsPanel, "step" + step);
        if (step == 1) {
            updateObstacleSummary();
        }
        // 更新按钮状态
        updateButtonState(step);
@@ -1306,11 +1940,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 +1999,53 @@
            case 3:
                dikuaiData.put("mowingPattern", (String) mowingPatternCombo.getSelectedItem());
                dikuaiData.put("mowingWidth", mowingWidthSpinner.getValue().toString());
                // 保存割草机割刀宽度
                if (mowingWidthField != null) {
                    String bladeWidthText = mowingWidthField.getText().trim();
                    if (!bladeWidthText.isEmpty()) {
                        try {
                            double bladeWidthMeters = Double.parseDouble(bladeWidthText);
                            dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters));
                        } catch (NumberFormatException e) {
                            dikuaiData.put("mowingBladeWidth", bladeWidthText);
                        }
                    }
                }
                // 保存相邻行重叠距离
                if (overlapDistanceField != null) {
                    String overlapText = overlapDistanceField.getText().trim();
                    if (!overlapText.isEmpty()) {
                        try {
                            double overlapMeters = Double.parseDouble(overlapText);
                            dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters));
                        } catch (NumberFormatException e) {
                            dikuaiData.put("mowingOverlapDistance", overlapText);
                        }
                    }
                }
                // 计算并保存割草宽度(割刀宽度 - 重叠距离)
                if (mowingWidthField != null && overlapDistanceField != null) {
                    try {
                        String bladeWidthText = mowingWidthField.getText().trim();
                        String overlapText = overlapDistanceField.getText().trim();
                        if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) {
                            double bladeWidthMeters = Double.parseDouble(bladeWidthText);
                            double overlapMeters = Double.parseDouble(overlapText);
                            double calculatedWidthMeters = bladeWidthMeters - overlapMeters;
                            if (calculatedWidthMeters > 0) {
                                // 转换为厘米存储,保持与原有数据格式兼容
                                int widthCm = (int) Math.round(calculatedWidthMeters * 100.0);
                                dikuaiData.put("mowingWidth", Integer.toString(widthCm));
                            }
                        }
                    } catch (NumberFormatException e) {
                        // 如果计算失败,不保存割草宽度
                    }
                }
                if (!hasGeneratedPath()) {
                    JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.WARNING_MESSAGE);
                    return false;
@@ -1365,19 +2058,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 +2243,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 +2268,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);
            }
        }
@@ -1585,14 +2300,123 @@
            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;
                    }
                }
            }
        }
        // 恢复割草机割刀宽度(优先从mowingBladeWidth获取)
        if (mowingWidthField != null) {
            String bladeWidth = data.get("mowingBladeWidth");
            if (isMeaningfulValue(bladeWidth)) {
                try {
                    double bladeWidthMeters = Double.parseDouble(bladeWidth.trim());
                    mowingWidthField.setText(String.format(Locale.US, "%.2f", bladeWidthMeters));
                } catch (NumberFormatException ignored) {
                    // 如果mowingBladeWidth不存在或解析失败,尝试从mowingWidth恢复
                    String width = data.get("mowingWidth");
                    if (isMeaningfulValue(width)) {
                        try {
                            // 如果存储的是厘米,转换为米显示
                            double parsed = Double.parseDouble(width.trim());
                            // 假设如果值大于10,则是厘米,需要转换为米;否则已经是米
                            double widthMeters = parsed > 10 ? parsed / 100.0 : parsed;
                            mowingWidthField.setText(String.format(Locale.US, "%.2f", widthMeters));
                        } catch (NumberFormatException ignored2) {
                            // 保持当前值
                        }
                    }
                }
            } else {
                // 如果mowingBladeWidth不存在,尝试从mowingWidth恢复
                String width = data.get("mowingWidth");
                if (isMeaningfulValue(width)) {
                    try {
                        // 如果存储的是厘米,转换为米显示
                        double parsed = Double.parseDouble(width.trim());
                        // 假设如果值大于10,则是厘米,需要转换为米;否则已经是米
                        double widthMeters = parsed > 10 ? parsed / 100.0 : parsed;
                        mowingWidthField.setText(String.format(Locale.US, "%.2f", widthMeters));
                    } catch (NumberFormatException ignored) {
                        // 保持当前值
                    }
                }
            }
        }
        if (overlapDistanceField != null) {
            String overlap = data.get("mowingOverlapDistance");
            if (isMeaningfulValue(overlap)) {
                try {
                    double overlapMeters = Double.parseDouble(overlap.trim());
                    overlapDistanceField.setText(String.format(Locale.US, "%.2f", overlapMeters));
                } 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 +2424,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()) {
@@ -1647,8 +2479,60 @@
        if (dikuaiData.containsKey("mowingPattern")) {
            dikuai.setMowingPattern(dikuaiData.get("mowingPattern"));
        }
        // 保存割草机割刀宽度(优先从dikuaiData获取,否则从TextField获取)
        if (dikuaiData.containsKey("mowingBladeWidth")) {
            dikuai.setMowingBladeWidth(dikuaiData.get("mowingBladeWidth"));
        } else if (mowingWidthField != null) {
            String bladeWidthText = mowingWidthField.getText().trim();
            if (!bladeWidthText.isEmpty()) {
                try {
                    double bladeWidthMeters = Double.parseDouble(bladeWidthText);
                    dikuai.setMowingBladeWidth(String.format(Locale.US, "%.2f", bladeWidthMeters));
                } catch (NumberFormatException e) {
                    dikuai.setMowingBladeWidth(bladeWidthText);
                }
            }
        }
        // 保存相邻行重叠距离(优先从dikuaiData获取,否则从TextField获取)
        if (dikuaiData.containsKey("mowingOverlapDistance")) {
            dikuai.setMowingOverlapDistance(dikuaiData.get("mowingOverlapDistance"));
        } else if (overlapDistanceField != null) {
            String overlapText = overlapDistanceField.getText().trim();
            if (!overlapText.isEmpty()) {
                try {
                    double overlapMeters = Double.parseDouble(overlapText);
                    dikuai.setMowingOverlapDistance(String.format(Locale.US, "%.2f", overlapMeters));
                } catch (NumberFormatException e) {
                    dikuai.setMowingOverlapDistance(overlapText);
                }
            }
        }
        // 保存割草宽度(计算后的值,优先从dikuaiData获取)
        if (dikuaiData.containsKey("mowingWidth")) {
            dikuai.setMowingWidth(dikuaiData.get("mowingWidth"));
        } else {
            // 如果没有在dikuaiData中,则从TextField计算
            if (mowingWidthField != null && overlapDistanceField != null) {
                try {
                    String bladeWidthText = mowingWidthField.getText().trim();
                    String overlapText = overlapDistanceField.getText().trim();
                    if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) {
                        double bladeWidthMeters = Double.parseDouble(bladeWidthText);
                        double overlapMeters = Double.parseDouble(overlapText);
                        double calculatedWidthMeters = bladeWidthMeters - overlapMeters;
                        if (calculatedWidthMeters > 0) {
                            // 转换为厘米存储,保持与原有数据格式兼容
                            int widthCm = (int) Math.round(calculatedWidthMeters * 100.0);
                            dikuai.setMowingWidth(Integer.toString(widthCm));
                        }
                    }
                } catch (NumberFormatException e) {
                    // 如果计算失败,保持原有值或使用默认值
                }
            }
        }
        String plannedPath = dikuaiData.get("plannedPath");