张世豪
6 天以前 c498385fb7e372d13e2ee76d7b54ae2381728082
src/dikuai/addzhangaiwu.java
@@ -10,8 +10,11 @@
import java.awt.Font;
import java.awt.Image;
import java.awt.Window;
import java.awt.Container;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
@@ -36,15 +39,24 @@
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.JTextField;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import baseStation.BaseStation;
import gecaoji.Device;
import set.Setsys;
import ui.UIConfig;
import zhuye.Coordinate;
import zhuye.MapRenderer;
import zhuye.Shouye;
import zhangaiwu.AddDikuai;
import zhangaiwu.Obstacledge;
import zhangaiwu.yulanzhangaiwu;
import zhuye.buttonset;
import bianjie.bianjieguihua2;
import bianjie.ThreePointCircle;
/**
 * 障碍物新增/编辑对话框。设计语言参考 {@link AddDikuai},支持通过实地绘制采集障碍物坐标。
@@ -61,6 +73,8 @@
    private final Color MEDIUM_GRAY = new Color(233, 236, 239);
    private final Color TEXT_COLOR = new Color(33, 37, 41);
    private final Color LIGHT_TEXT = new Color(108, 117, 125);
    private final Color DANGER_COLOR = new Color(220, 53, 69);
    private final Color DANGER_LIGHT = new Color(255, 235, 238);
    private final Dikuai targetDikuai;
    private final List<ExistingObstacle> existingObstacles;
@@ -79,7 +93,13 @@
    private JPanel selectedMethodPanel;
    private JPanel selectedShapePanel;
    private JButton drawButton;
    private JButton generateBoundaryButton;  // 生成障碍物边界按钮
    private JButton previewButton;  // 预览按钮
    private JLabel drawingStatusLabel;
    private JLabel boundaryStatusLabel;  // 生成障碍物边界状态标签
    private JTextField obstacleNameField;
    private JPanel existingObstacleListPanel;
    private JPanel step1NextButtonRow;
    private int currentStep = 1;
    private boolean drawingInProgress;
@@ -102,8 +122,11 @@
        if (target == null) {
            throw new IllegalArgumentException("targetDikuai 不能为空");
        }
    this.targetDikuai = target;
    this.existingObstacles = Collections.unmodifiableList(resolveExistingObstacles(target, obstacleNames));
        Coordinate.coordinates.clear();
        Coordinate.setStartSaveGngga(false);
        Coordinate.clearActiveDeviceIdFilter();
        this.targetDikuai = target;
        this.existingObstacles = new ArrayList<>(resolveExistingObstacles(target, obstacleNames));
        initializeUI();
        setupEventHandlers();
        preloadData();
@@ -161,7 +184,11 @@
    infoContainer.add(createInfoRow("地块编号:", safeValue(targetDikuai.getLandNumber(), "未设置")));
    infoContainer.add(Box.createRigidArea(new Dimension(0, 16)));
    infoContainer.add(createInfoRow("地块名称:", safeValue(targetDikuai.getLandName(), "未命名")));
    infoContainer.add(Box.createRigidArea(new Dimension(0, 20)));
    infoContainer.add(Box.createRigidArea(new Dimension(0, 16)));
    infoContainer.add(createObstacleNameRow());
    infoContainer.add(Box.createRigidArea(new Dimension(0, 12)));
    infoContainer.add(createStep1NextButtonRow());
    infoContainer.add(Box.createRigidArea(new Dimension(0, 12)));
    infoContainer.add(buildExistingObstacleSection());
    stepPanel.add(infoContainer);
@@ -182,32 +209,64 @@
        section.add(titleLabel);
        section.add(Box.createRigidArea(new Dimension(0, 8)));
        existingObstacleListPanel = new JPanel();
        existingObstacleListPanel.setLayout(new BoxLayout(existingObstacleListPanel, BoxLayout.Y_AXIS));
        existingObstacleListPanel.setOpaque(false);
        existingObstacleListPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        section.add(existingObstacleListPanel);
        refreshExistingObstacleList();
        return section;
    }
    private JPanel createStep1NextButtonRow() {
        if (step1NextButtonRow == null) {
            step1NextButtonRow = new JPanel();
            step1NextButtonRow.setLayout(new BoxLayout(step1NextButtonRow, BoxLayout.X_AXIS));
            step1NextButtonRow.setOpaque(false);
            step1NextButtonRow.setAlignmentX(Component.LEFT_ALIGNMENT);
            step1NextButtonRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 36));
            step1NextButtonRow.add(Box.createHorizontalGlue());
        }
        return step1NextButtonRow;
    }
    private void attachNextButtonToStep1Row() {
        if (step1NextButtonRow == null || nextButton == null) {
            return;
        }
        step1NextButtonRow.removeAll();
        step1NextButtonRow.add(Box.createHorizontalGlue());
        nextButton.setAlignmentX(Component.RIGHT_ALIGNMENT);
        step1NextButtonRow.add(nextButton);
        step1NextButtonRow.revalidate();
        step1NextButtonRow.repaint();
    }
    private void refreshExistingObstacleList() {
        if (existingObstacleListPanel == null) {
            return;
        }
        existingObstacleListPanel.removeAll();
        if (existingObstacles.isEmpty()) {
            JLabel emptyLabel = new JLabel("暂无");
            emptyLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
            emptyLabel.setForeground(LIGHT_TEXT);
            emptyLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
            section.add(emptyLabel);
            return section;
        }
        JPanel listPanel = new JPanel();
        listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.Y_AXIS));
        listPanel.setOpaque(false);
        listPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        for (int i = 0; i < existingObstacles.size(); i++) {
            ExistingObstacle obstacle = existingObstacles.get(i);
            JPanel row = createObstacleSummaryRow(obstacle);
            row.setAlignmentX(Component.LEFT_ALIGNMENT);
            listPanel.add(row);
            if (i < existingObstacles.size() - 1) {
                listPanel.add(Box.createRigidArea(new Dimension(0, 6)));
            existingObstacleListPanel.add(emptyLabel);
        } else {
            for (int i = 0; i < existingObstacles.size(); i++) {
                ExistingObstacle obstacle = existingObstacles.get(i);
                JPanel row = createObstacleSummaryRow(obstacle);
                row.setAlignmentX(Component.LEFT_ALIGNMENT);
                existingObstacleListPanel.add(row);
                if (i < existingObstacles.size() - 1) {
                    existingObstacleListPanel.add(Box.createRigidArea(new Dimension(0, 6)));
                }
            }
        }
        section.add(listPanel);
        return section;
        existingObstacleListPanel.revalidate();
        existingObstacleListPanel.repaint();
    }
    private JPanel createObstacleSummaryRow(ExistingObstacle obstacle) {
@@ -225,20 +284,22 @@
        JButton editButton = createInlineButton("修改");
        editButton.addActionListener(e -> populateObstacleForEditing(obstacle));
        JButton deleteButton = createInlineIconButton("image/delete.png", "删除障碍物");
        deleteButton.addActionListener(e -> handleDeleteExistingObstacle(obstacle));
        row.add(infoLabel);
        row.add(Box.createHorizontalGlue());
        row.add(editButton);
        row.add(Box.createRigidArea(new Dimension(6, 0)));
        row.add(deleteButton);
        return row;
    }
    private JButton createInlineButton(String text) {
        JButton button = new JButton(text);
        JButton button = buttonset.createStyledButton(text, PRIMARY_COLOR);
        button.setFont(new Font("微软雅黑", Font.BOLD, 12));
        button.setForeground(WHITE);
        button.setBackground(PRIMARY_COLOR);
        button.setBorder(BorderFactory.createEmptyBorder(6, 16, 6, 16));
        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        button.setFocusPainted(false);
        Dimension size = new Dimension(72, 28);
        button.setPreferredSize(size);
        button.setMinimumSize(size);
@@ -257,10 +318,56 @@
        return button;
    }
    private JButton createInlineIconButton(String iconPath, String tooltip) {
        JButton button = new JButton();
        button.setPreferredSize(new Dimension(32, 28));
        button.setMinimumSize(new Dimension(32, 28));
        button.setMaximumSize(new Dimension(32, 28));
        button.setBackground(WHITE);
        button.setBorder(BorderFactory.createLineBorder(DANGER_COLOR));
        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        button.setFocusPainted(false);
        button.setFocusable(false);
        button.setOpaque(true);
        button.setContentAreaFilled(true);
        if (tooltip != null && !tooltip.trim().isEmpty()) {
            button.setToolTipText(tooltip);
        }
        ImageIcon icon = null;
        if (iconPath != null && !iconPath.trim().isEmpty()) {
            ImageIcon rawIcon = new ImageIcon(iconPath);
            if (rawIcon.getIconWidth() > 0 && rawIcon.getIconHeight() > 0) {
                Image scaled = rawIcon.getImage().getScaledInstance(16, 16, Image.SCALE_SMOOTH);
                icon = new ImageIcon(scaled);
            }
        }
        if (icon != null) {
            button.setIcon(icon);
        } else {
            button.setText("删");
            button.setFont(new Font("微软雅黑", Font.BOLD, 12));
            button.setForeground(DANGER_COLOR);
        }
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                button.setBackground(DANGER_LIGHT);
            }
            @Override
            public void mouseExited(MouseEvent e) {
                button.setBackground(WHITE);
            }
        });
        return button;
    }
    private String buildObstacleSummaryText(ExistingObstacle obstacle) {
        String name = obstacle.getName();
        String shape = obstacle.getShapeDisplay();
        String coordPreview = buildCoordinatePreview(obstacle.getCoordinates(), 5);
    String coordPreview = buildCoordinatePreview(obstacle.getDisplayCoordinates(), 5);
        return String.format(Locale.CHINA, "%s,%s,坐标:%s", name, shape, coordPreview);
    }
@@ -278,16 +385,70 @@
        return sanitized.substring(0, keepLength) + "...";
    }
    private boolean deleteObstacleFromConfig(String obstacleName) {
        String landNumber = targetDikuai != null ? targetDikuai.getLandNumber() : null;
        if (!isMeaningfulValue(landNumber) || !isMeaningfulValue(obstacleName)) {
            return false;
        }
        try {
            File configFile = new File("Obstacledge.properties");
            if (!configFile.exists()) {
                return false;
            }
            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
            if (!manager.loadFromFile(configFile.getAbsolutePath())) {
                return false;
            }
            Obstacledge.Plot plot = manager.getPlotById(landNumber.trim());
            if (plot == null) {
                return false;
            }
            if (!plot.removeObstacleByName(obstacleName.trim())) {
                return false;
            }
            if (!manager.saveToFile(configFile.getAbsolutePath())) {
                return false;
            }
            Dikuai.updateField(landNumber.trim(), "updateTime", currentTime());
            Dikuai.saveToProperties();
            Dikuaiguanli.notifyExternalCreation(landNumber.trim());
            return true;
        } catch (Exception ex) {
            System.err.println("删除障碍物失败: " + ex.getMessage());
            return false;
        }
    }
    private void populateObstacleForEditing(ExistingObstacle obstacle) {
        if (obstacle == null) {
            return;
        }
        String coords = obstacle.getCoordinates();
        if (isMeaningfulValue(coords)) {
            formData.put("obstacleCoordinates", coords.trim());
        String name = obstacle.getName();
        if (isMeaningfulValue(name)) {
            formData.put("obstacleName", name.trim());
            formData.put("editingObstacleName", name.trim());
        } else {
            formData.remove("obstacleName");
            formData.remove("editingObstacleName");
        }
        if (obstacleNameField != null) {
            obstacleNameField.setText(isMeaningfulValue(name) ? name.trim() : "");
        }
        String xyCoords = obstacle.getXyCoordinates();
        String displayCoords = obstacle.getDisplayCoordinates();
        if (isMeaningfulValue(xyCoords)) {
            formData.put("obstacleCoordinates", xyCoords.trim());
        } else if (isMeaningfulValue(displayCoords)) {
            formData.put("obstacleCoordinates", displayCoords.trim());
        } else {
            formData.remove("obstacleCoordinates");
        }
        String originalCoords = obstacle.getOriginalCoordinates();
        if (isMeaningfulValue(originalCoords)) {
            formData.put("obstacleOriginalCoordinates", originalCoords.trim());
        } else {
            formData.remove("obstacleOriginalCoordinates");
        }
        String shapeKey = obstacle.getShapeKey();
        if (shapeKey != null) {
            JPanel shapePanel = shapeOptionPanels.get(shapeKey);
@@ -297,6 +458,58 @@
        showStep(2);
    }
    private void handleDeleteExistingObstacle(ExistingObstacle obstacle) {
        if (obstacle == null) {
            return;
        }
        String obstacleName = obstacle.getName();
        if (!isMeaningfulValue(obstacleName)) {
            JOptionPane.showMessageDialog(this, "无法删除:障碍物名称无效", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        int choice = JOptionPane.showConfirmDialog(this,
                "确定要删除障碍物 \"" + obstacleName + "\" 吗?此操作无法撤销。",
                "删除确认",
                JOptionPane.YES_NO_OPTION,
                JOptionPane.WARNING_MESSAGE);
        if (choice != JOptionPane.YES_OPTION) {
            return;
        }
        boolean removedFromConfig = deleteObstacleFromConfig(obstacleName);
    boolean hasPersistedData = obstacle.hasPersistedData() || obstacle.getShapeKey() != null;
        if (!removedFromConfig && hasPersistedData) {
            JOptionPane.showMessageDialog(this, "删除失败,未能在配置中移除该障碍物。", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        if (!existingObstacles.remove(obstacle)) {
            existingObstacles.removeIf(item -> obstacleName.equals(item != null ? item.getName() : null));
        }
        refreshExistingObstacleList();
        clearEditingStateIfDeleted(obstacleName);
        JOptionPane.showMessageDialog(this, "障碍物已删除", "成功", JOptionPane.INFORMATION_MESSAGE);
    }
    private void clearEditingStateIfDeleted(String obstacleName) {
        if (!isMeaningfulValue(obstacleName)) {
            return;
        }
        String editingName = formData.get("editingObstacleName");
        if (isMeaningfulValue(editingName) && obstacleName.equals(editingName)) {
            formData.remove("editingObstacleName");
            formData.remove("obstacleCoordinates");
            formData.remove("obstacleOriginalCoordinates");
            formData.remove("generatedBoundaryCoordinates");  // 清除生成的边界坐标
            if (obstacleNameField != null) {
                obstacleNameField.setText("");
            } else {
                formData.remove("obstacleName");
            }
            updateDrawingStatus();
            updateSaveButtonState();
            updatePreviewButtonState();
        }
    }
    private JPanel createStep2Panel() {
        JPanel stepPanel = new JPanel();
        stepPanel.setLayout(new BoxLayout(stepPanel, BoxLayout.Y_AXIS));
@@ -322,7 +535,7 @@
        stepPanel.add(methodOptionsPanel);
        stepPanel.add(Box.createRigidArea(new Dimension(0, 20)));
        stepPanel.add(createSectionHeader("障碍物形状", "多边形需采集多个点,圆形只需圆心与圆周上一点"));
    stepPanel.add(createSectionHeader("障碍物形状", "多边形需采集多个点,圆形需在圆周上采集至少三个点"));
        stepPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        shapeOptionsPanel = new JPanel();
@@ -331,15 +544,29 @@
        shapeOptionsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
    shapeOptionsPanel.add(createShapeOption("polygon", "多边形", "依次采集轮廓上的多个点或者沿障碍物边缘走一圈"));
        shapeOptionsPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        shapeOptionsPanel.add(createShapeOption("circle", "圆形", "先采集圆心坐标,再采集圆周上一点"));
    shapeOptionsPanel.add(createShapeOption("circle", "圆形", "沿圆周任意采集三个点自动生成圆"));
        stepPanel.add(shapeOptionsPanel);
        stepPanel.add(Box.createRigidArea(new Dimension(0, 24)));
        // 按钮容器:重新绘制 + 生成障碍物边界
        JPanel buttonRow = new JPanel();
        buttonRow.setLayout(new BoxLayout(buttonRow, BoxLayout.X_AXIS));
        buttonRow.setOpaque(false);
        buttonRow.setAlignmentX(Component.LEFT_ALIGNMENT);
        drawButton = createPrimaryButton("开始绘制", 16);
        drawButton.setAlignmentX(Component.LEFT_ALIGNMENT);
        drawButton.addActionListener(e -> startDrawingWorkflow());
        stepPanel.add(drawButton);
        buttonRow.add(drawButton);
        buttonRow.add(Box.createRigidArea(new Dimension(10, 0)));
        generateBoundaryButton = createPrimaryButton("生成障碍物边界", 16);
        generateBoundaryButton.setVisible(false);  // 初始隐藏,绘制完成后显示
        generateBoundaryButton.addActionListener(e -> generateObstacleBoundary());
        buttonRow.add(generateBoundaryButton);
        stepPanel.add(buttonRow);
        stepPanel.add(Box.createRigidArea(new Dimension(0, 12)));
@@ -348,6 +575,15 @@
        drawingStatusLabel.setForeground(LIGHT_TEXT);
        drawingStatusLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        stepPanel.add(drawingStatusLabel);
        // 添加生成障碍物边界状态标签
        stepPanel.add(Box.createRigidArea(new Dimension(0, 8)));
        boundaryStatusLabel = new JLabel("");
        boundaryStatusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
        boundaryStatusLabel.setForeground(LIGHT_TEXT);
        boundaryStatusLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        boundaryStatusLabel.setVisible(false);  // 初始隐藏
        stepPanel.add(boundaryStatusLabel);
        stepPanel.add(Box.createVerticalGlue());
        return stepPanel;
@@ -361,15 +597,28 @@
        prevButton = createSecondaryButton("上一步");
        nextButton = createPrimaryButton("下一步", 16);
        // 设置下一步按钮宽度为300像素
        nextButton.setPreferredSize(new Dimension(300, nextButton.getPreferredSize().height));
        nextButton.setMaximumSize(new Dimension(300, nextButton.getPreferredSize().height));
        previewButton = createSecondaryButton("预览");
        previewButton.setVisible(false);  // 初始隐藏,生成边界后显示
        previewButton.setOpaque(true);
        previewButton.setContentAreaFilled(true);
        previewButton.addActionListener(e -> previewObstacleBoundary());
        saveButton = createPrimaryButton("保存", 16);
        saveButton.setVisible(false);
        buttonPanel.add(prevButton);
        buttonPanel.add(Box.createHorizontalGlue());
        buttonPanel.add(nextButton);
        buttonPanel.add(Box.createRigidArea(new Dimension(12, 0)));
        buttonPanel.add(previewButton);
        buttonPanel.add(Box.createRigidArea(new Dimension(10, 0)));
        buttonPanel.add(saveButton);
        attachNextButtonToStep1Row();
        return buttonPanel;
    }
@@ -547,6 +796,10 @@
        if (option == null) {
            return;
        }
        if (userTriggered && "handheld".equalsIgnoreCase(type) && !hasConfiguredHandheldMarker()) {
            JOptionPane.showMessageDialog(this, "请先添加便携打点器编号", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        if (selectedMethodPanel != null && selectedMethodPanel != option) {
            resetOptionAppearance(selectedMethodPanel);
        }
@@ -558,6 +811,11 @@
        }
    }
    private boolean hasConfiguredHandheldMarker() {
        String handheldId = Setsys.getPropertyValue("handheldMarkerId");
        return handheldId != null && !handheldId.trim().isEmpty();
    }
    private void selectShapeOption(JPanel option, String type, boolean userTriggered) {
        if (option == null) {
            return;
@@ -616,6 +874,22 @@
        nextButton.addActionListener(e -> {
            if (currentStep == 1) {
                String name = obstacleNameField != null ? obstacleNameField.getText() : null;
                if (!isMeaningfulValue(name)) {
                    JOptionPane.showMessageDialog(this, "请先填写障碍物名称", "提示", JOptionPane.WARNING_MESSAGE);
                    if (obstacleNameField != null) {
                        obstacleNameField.requestFocusInWindow();
                    }
                    return;
                }
                if (!isObstacleNameUnique(name)) {
                    JOptionPane.showMessageDialog(this, "障碍物名称已存在,请输入唯一名称", "提示", JOptionPane.WARNING_MESSAGE);
                    if (obstacleNameField != null) {
                        obstacleNameField.requestFocusInWindow();
                    }
                    return;
                }
                formData.put("obstacleName", name.trim());
                Coordinate.coordinates.clear();
                showStep(2);
            }
@@ -637,6 +911,23 @@
        }
        activeDrawingShape = null;
        // 重新绘制时清除之前生成的边界坐标
        formData.remove("generatedBoundaryCoordinates");
        if (previewButton != null) {
            previewButton.setVisible(false);
        }
        // 清除边界状态标签
        if (boundaryStatusLabel != null) {
            boundaryStatusLabel.setVisible(false);
            boundaryStatusLabel.setText("");
        }
        // 对于圆形障碍物,隐藏生成边界按钮
        String shapeKey = formData.get("obstacleShape");
        boolean isCircle = "circle".equals(shapeKey);
        if (isCircle && generateBoundaryButton != null) {
            generateBoundaryButton.setVisible(false);
        }
        String method = formData.get("drawingMethod");
        if (!isMeaningfulValue(method)) {
@@ -656,14 +947,37 @@
            return;
        }
    String deviceId = resolveDrawingDeviceId(method);
    if (!isMeaningfulValue(deviceId)) {
        String idLabel = "handheld".equalsIgnoreCase(method) ? "手持设备编号" : "割草机编号";
        JOptionPane.showMessageDialog(this,
            "未获取到有效的" + idLabel + ",请先在系统设置中完成配置", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        activeDrawingShape = shape.toLowerCase(Locale.ROOT);
        Shouye shouyeInstance = Shouye.getInstance();
        if (shouyeInstance != null) {
            shouyeInstance.setHandheldMowerIconActive("handheld".equalsIgnoreCase(method));
            MapRenderer renderer = shouyeInstance.getMapRenderer();
            if (renderer != null) {
                renderer.clearIdleTrail();
            }
        }
        Coordinate.coordinates.clear();
        Coordinate.setActiveDeviceIdFilter(deviceId);
        Coordinate.setStartSaveGngga(true);
        yulanzhangaiwu.startPreview(activeDrawingShape, baseStation);
        drawingInProgress = true;
        drawButton.setText("正在绘制...");
        drawButton.setEnabled(false);
        drawingStatusLabel.setText("正在采集障碍物坐标,请在主界面完成绘制。");
        if ("circle".equals(activeDrawingShape)) {
            drawingStatusLabel.setText("正在采集圆形障碍物,请沿圆周采集至少三个点后在主界面结束绘制。");
        } else {
            drawingStatusLabel.setText("正在采集障碍物坐标,请在主界面完成绘制。");
        }
        if (activeSession == null) {
            activeSession = new ObstacleDrawingSession();
@@ -692,6 +1006,8 @@
    public static void finishDrawingSession() {
        Coordinate.setStartSaveGngga(false);
        Coordinate.clearActiveDeviceIdFilter();
        yulanzhangaiwu.stopPreview();
        Shouye shouye = Shouye.getInstance();
        if (shouye != null) {
@@ -710,11 +1026,20 @@
        showDialog(parent, activeSession.target);
    }
    public static String getActiveSessionBaseStation() {
        if (activeSession != null && isMeaningfulValue(activeSession.baseStation)) {
            return activeSession.baseStation.trim();
        }
        return null;
    }
    /**
     * Stops an in-progress circle capture and reopens the wizard on step 2.
     */
    public static void abortCircleDrawingAndReturn(String message) {
        Coordinate.setStartSaveGngga(false);
        Coordinate.clearActiveDeviceIdFilter();
        yulanzhangaiwu.stopPreview();
        Coordinate.coordinates.clear();
        if (activeSession == null || activeSession.target == null) {
@@ -750,6 +1075,13 @@
            return;
        }
        String originalCoordStr = buildOriginalCoordinateString(captured);
        if (isMeaningfulValue(originalCoordStr)) {
            session.data.put("obstacleOriginalCoordinates", originalCoordStr);
        } else {
            session.data.remove("obstacleOriginalCoordinates");
        }
        List<double[]> xyPoints = convertToLocalXY(captured, session.baseStation);
        if (xyPoints.isEmpty()) {
            session.captureSuccessful = false;
@@ -759,18 +1091,25 @@
        String shape = session.data.get("obstacleShape");
        if ("circle".equals(shape)) {
            if (xyPoints.size() < 2) {
            if (xyPoints.size() < 3) {
                session.captureSuccessful = false;
                session.captureMessage = "圆形障碍物至少需要两个采集点(圆心和圆周点)";
                session.captureMessage = "圆形障碍物至少需要三个采集点(圆周上的点)";
                return;
            }
            double[] center = xyPoints.get(0);
            double[] radiusPoint = xyPoints.get(xyPoints.size() - 1);
            CircleFitResult circle = fitCircleFromPoints(xyPoints);
            if (circle == null) {
                session.captureSuccessful = false;
                session.captureMessage = "无法根据采集的点生成圆,请确保选择了三个非共线的圆周点";
                return;
            }
            // 使用计算出的半径生成一个真正的圆上点(圆心右侧的点),确保预览时计算的半径和结束绘制时的半径一致
            double radiusX = circle.centerX + circle.radius;
            double radiusY = circle.centerY;
            String result = String.format(Locale.US, "%.2f,%.2f;%.2f,%.2f",
                    center[0], center[1], radiusPoint[0], radiusPoint[1]);
                    circle.centerX, circle.centerY, radiusX, radiusY);
            session.data.put("obstacleCoordinates", result);
            session.captureSuccessful = true;
            session.captureMessage = "已采集圆形障碍物坐标";
            session.captureMessage = "已采集圆形障碍物,共 " + xyPoints.size() + " 个点";
        } else {
            if (xyPoints.size() < 3) {
                session.captureSuccessful = false;
@@ -816,6 +1155,67 @@
        return result;
    }
    private static String buildOriginalCoordinateString(List<Coordinate> coords) {
        if (coords == null || coords.isEmpty()) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        for (Coordinate coord : coords) {
            if (coord == null) {
                continue;
            }
            String latToken = sanitizeCoordinateToken(coord.getLatitude());
            String lonToken = sanitizeCoordinateToken(coord.getLongitude());
            if (latToken == null || lonToken == null) {
                continue;
            }
            char latDir = sanitizeDirection(coord.getLatDirection(), 'N');
            char lonDir = sanitizeDirection(coord.getLonDirection(), 'E');
            if (sb.length() > 0) {
                sb.append(";");
            }
            sb.append(latToken)
              .append(",")
              .append(latDir)
              .append(",")
              .append(lonToken)
              .append(",")
              .append(lonDir);
        }
        return sb.length() > 0 ? sb.toString() : null;
    }
    private static String sanitizeCoordinateToken(String token) {
        if (token == null) {
            return null;
        }
        String trimmed = token.trim();
        if (trimmed.isEmpty()) {
            return null;
        }
        try {
            Double.parseDouble(trimmed);
            return trimmed;
        } catch (NumberFormatException ex) {
            return null;
        }
    }
    private static char sanitizeDirection(String direction, char fallback) {
        if (direction == null) {
            return fallback;
        }
        String trimmed = direction.trim();
        if (trimmed.isEmpty()) {
            return fallback;
        }
        char ch = Character.toUpperCase(trimmed.charAt(0));
        if (ch != 'N' && ch != 'S' && ch != 'E' && ch != 'W') {
            return fallback;
        }
        return ch;
    }
    private static double parseDMToDecimal(String dmm, String direction) {
        if (dmm == null || dmm.trim().isEmpty()) {
            return Double.NaN;
@@ -847,6 +1247,101 @@
        return new double[]{eastMeters, northMeters};
    }
    private static CircleFitResult fitCircleFromPoints(List<double[]> points) {
        if (points == null || points.size() < 3) {
            return null;
        }
        CircleFitResult best = null;
        double bestScore = 0.0;
        int n = points.size();
        for (int i = 0; i < n - 2; i++) {
            double[] p1 = points.get(i);
            for (int j = i + 1; j < n - 1; j++) {
                double[] p2 = points.get(j);
                for (int k = j + 1; k < n; k++) {
                    double[] p3 = points.get(k);
                    CircleFitResult candidate = circleFromThreePoints(p1, p2, p3);
                    if (candidate == null || candidate.radius <= 0) {
                        continue;
                    }
                    double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3)));
                    if (minEdge > bestScore) {
                        bestScore = minEdge;
                        best = candidate;
                    }
                }
            }
        }
        return best;
    }
    private static CircleFitResult circleFromThreePoints(double[] p1, double[] p2, double[] p3) {
        double x1 = p1[0];
        double y1 = p1[1];
        double x2 = p2[0];
        double y2 = p2[1];
        double x3 = p3[0];
        double y3 = p3[1];
        double determinant = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
        if (Math.abs(determinant) < 1e-6) {
            return null;
        }
        double x1Sq = x1 * x1 + y1 * y1;
        double x2Sq = x2 * x2 + y2 * y2;
        double x3Sq = x3 * x3 + y3 * y3;
        double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / determinant;
        double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / determinant;
        double radius = Math.hypot(centerX - x1, centerY - y1);
        if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) {
            return null;
        }
        if (radius < 0.05) {
            return null;
        }
        return new CircleFitResult(centerX, centerY, radius, new double[]{x1, y1});
    }
    private static double[] pickRadiusPoint(List<double[]> points, CircleFitResult circle) {
        if (circle == null || points == null || points.isEmpty()) {
            return null;
        }
        double[] best = null;
        double bestDistance = 0.0;
        for (double[] pt : points) {
            double dist = Math.hypot(pt[0] - circle.centerX, pt[1] - circle.centerY);
            if (dist > bestDistance) {
                bestDistance = dist;
                best = pt;
            }
        }
        return best;
    }
    private static double distance(double[] a, double[] b) {
        double dx = a[0] - b[0];
        double dy = a[1] - b[1];
        return Math.hypot(dx, dy);
    }
    private static final class CircleFitResult {
        final double centerX;
        final double centerY;
        final double radius;
        final double[] referencePoint;
        CircleFitResult(double centerX, double centerY, double radius, double[] referencePoint) {
            this.centerX = centerX;
            this.centerY = centerY;
            this.radius = radius;
            this.referencePoint = referencePoint;
        }
    }
    private List<ExistingObstacle> resolveExistingObstacles(Dikuai target, List<String> providedNames) {
        String landNumber = target != null ? target.getLandNumber() : null;
        Map<String, ExistingObstacle> configMap = loadObstacleDetailsFromConfig(landNumber);
@@ -914,11 +1409,11 @@
                }
                String trimmedName = name.trim();
                Obstacledge.ObstacleShape shape = obstacle.getShape();
                String xyCoords = obstacle.getXyCoordsString();
                String original = obstacle.getOriginalCoordsString();
                String coords = isMeaningfulValue(xyCoords) ? xyCoords.trim()
                        : (isMeaningfulValue(original) ? original.trim() : "");
                details.put(trimmedName, new ExistingObstacle(trimmedName, shape, coords));
        String xyCoords = obstacle.getXyCoordsString();
        String original = obstacle.getOriginalCoordsString();
        String xy = isMeaningfulValue(xyCoords) ? xyCoords.trim() : "";
        String originalTrimmed = isMeaningfulValue(original) ? original.trim() : "";
        details.put(trimmedName, new ExistingObstacle(trimmedName, shape, xy, originalTrimmed));
            }
        } catch (Exception ex) {
            System.err.println("加载障碍物详情失败: " + ex.getMessage());
@@ -927,11 +1422,17 @@
    }
    private void preloadData() {
        String existing = targetDikuai.getObstacleCoordinates();
        if (isMeaningfulValue(existing)) {
            formData.put("obstacleCoordinates", existing.trim());
        }
        formData.remove("obstacleCoordinates");
        formData.remove("obstacleOriginalCoordinates");
        formData.remove("generatedBoundaryCoordinates");  // 清除生成的边界坐标
        formData.remove("editingObstacleName");
        updateDrawingStatus();
        updatePreviewButtonState();
        // 清除边界状态标签
        if (boundaryStatusLabel != null) {
            boundaryStatusLabel.setVisible(false);
            boundaryStatusLabel.setText("");
        }
    }
    private void updateDrawingStatus() {
@@ -939,21 +1440,140 @@
            return;
        }
        String coords = formData.get("obstacleCoordinates");
        String shapeKey = formData.get("obstacleShape");
        boolean isCircle = "circle".equals(shapeKey);
        if (isMeaningfulValue(coords)) {
            int count = countCoordinatePairs(coords);
            drawingStatusLabel.setText("已采集障碍物数据,点数:" + count);
            if (isCircle) {
                // 对于圆形障碍物,显示圆心坐标和半径
                String statusText = parseCircleStatusText(coords);
                drawingStatusLabel.setText(statusText);
            } else {
                // 对于多边形障碍物,显示点数
                int count = countCoordinatePairs(coords);
                drawingStatusLabel.setText("已采集障碍物数据,点数:" + count);
            }
            if (!drawingInProgress && drawButton != null) {
                drawButton.setText("重新绘制");
                drawButton.setEnabled(true);
            }
            // 对于圆形障碍物,不显示"生成障碍物边界"按钮
            if (generateBoundaryButton != null) {
                generateBoundaryButton.setVisible(!isCircle);
            }
        } else {
            drawingStatusLabel.setText("尚未采集障碍物坐标");
            if (!drawingInProgress && drawButton != null) {
                drawButton.setText("开始绘制");
                drawButton.setEnabled(true);
            }
            // 未绘制时隐藏"生成障碍物边界"按钮
            if (generateBoundaryButton != null) {
                generateBoundaryButton.setVisible(false);
            }
            // 未生成边界时隐藏预览按钮
            if (previewButton != null) {
                previewButton.setVisible(false);
            }
            // 隐藏边界状态标签
            if (boundaryStatusLabel != null) {
                boundaryStatusLabel.setVisible(false);
            }
        }
        updateSaveButtonState();
        updatePreviewButtonState();
    }
    /**
     * 更新边界状态标签
     */
    private void updateBoundaryStatusLabel(int pointCount) {
        if (boundaryStatusLabel == null) {
            return;
        }
        if (pointCount > 0) {
            boundaryStatusLabel.setText("生成障碍物边界点数:" + pointCount);
            boundaryStatusLabel.setVisible(true);
        } else {
            boundaryStatusLabel.setText("");
            boundaryStatusLabel.setVisible(false);
        }
    }
    /**
     * 更新预览按钮的显示状态
     */
    private void updatePreviewButtonState() {
        if (previewButton == null) {
            return;
        }
        // 对于圆形障碍物,不显示预览按钮
        String shapeKey = formData.get("obstacleShape");
        boolean isCircle = "circle".equals(shapeKey);
        if (isCircle) {
            previewButton.setVisible(false);
            previewButton.setEnabled(false);
            return;
        }
        // 只有在生成边界后才显示预览按钮
        String generatedBoundary = formData.get("generatedBoundaryCoordinates");
        boolean hasGeneratedBoundary = isMeaningfulValue(generatedBoundary);
        if (hasGeneratedBoundary) {
            // 生成边界后,重新创建绿色的预览按钮
            // 获取按钮的父容器和位置
            Container parent = previewButton.getParent();
            if (parent != null) {
                // 保存原有的ActionListener
                ActionListener[] listeners = previewButton.getActionListeners();
                // 移除旧按钮
                parent.remove(previewButton);
                // 创建新的绿色预览按钮(使用主按钮样式)
                previewButton = createPrimaryButton("预览", 16);
                previewButton.setVisible(true);
                previewButton.setEnabled(true);
                // 恢复ActionListener
                for (ActionListener listener : listeners) {
                    previewButton.addActionListener(listener);
                }
                // 添加到父容器(在保存按钮之前)
                int previewIndex = -1;
                Component[] components = parent.getComponents();
                for (int i = 0; i < components.length; i++) {
                    if (components[i] instanceof JButton) {
                        JButton btn = (JButton) components[i];
                        if ("保存".equals(btn.getText())) {
                            previewIndex = i;
                            break;
                        }
                    }
                }
                if (previewIndex >= 0) {
                    parent.add(previewButton, previewIndex);
                    parent.add(Box.createRigidArea(new Dimension(10, 0)), previewIndex + 1);
                } else {
                    // 如果找不到保存按钮,添加到末尾
                    parent.add(Box.createRigidArea(new Dimension(12, 0)));
                    parent.add(previewButton);
                    parent.add(Box.createRigidArea(new Dimension(10, 0)));
                }
                // 刷新布局
                parent.revalidate();
                parent.repaint();
            }
        } else {
            // 未生成边界时,隐藏按钮
            previewButton.setVisible(false);
            previewButton.setEnabled(false);
        }
    }
    private int countCoordinatePairs(String coords) {
@@ -976,14 +1596,102 @@
        }
        return count;
    }
    /**
     * 解析圆形障碍物坐标,生成状态文本
     * 格式:centerX,centerY;radiusX,radiusY
     * 返回:当前采集圆形圆心坐标x,y,半径n米
     */
    private String parseCircleStatusText(String coords) {
        if (!isMeaningfulValue(coords)) {
            return "尚未采集圆形障碍物坐标";
        }
        try {
            String[] pairs = coords.split(";");
            if (pairs.length < 2) {
                return "圆形障碍物坐标格式错误";
            }
            // 解析圆心坐标
            String[] centerParts = pairs[0].trim().split(",");
            if (centerParts.length < 2) {
                return "圆形障碍物坐标格式错误";
            }
            double centerX = Double.parseDouble(centerParts[0].trim());
            double centerY = Double.parseDouble(centerParts[1].trim());
            // 解析圆上一点坐标
            String[] radiusParts = pairs[1].trim().split(",");
            if (radiusParts.length < 2) {
                return "圆形障碍物坐标格式错误";
            }
            double radiusX = Double.parseDouble(radiusParts[0].trim());
            double radiusY = Double.parseDouble(radiusParts[1].trim());
            // 计算半径(米)
            double radius = Math.sqrt(Math.pow(radiusX - centerX, 2) + Math.pow(radiusY - centerY, 2));
            // 格式化显示:当前采集圆形圆心坐标x,y,半径n米
            return String.format(Locale.US, "当前采集圆形圆心坐标%.2f,%.2f,半径%.2f米",
                    centerX, centerY, radius);
        } catch (Exception e) {
            return "圆形障碍物坐标解析失败";
        }
    }
    private void updateSaveButtonState() {
        if (saveButton != null) {
            saveButton.setEnabled(isMeaningfulValue(formData.get("obstacleCoordinates")));
            boolean hasCoords = isMeaningfulValue(formData.get("obstacleCoordinates"));
            boolean hasName = isMeaningfulValue(formData.get("obstacleName"));
            // 检查是否生成了障碍物边界坐标
            String shapeKey = formData.get("obstacleShape");
            boolean hasGeneratedBoundary = isMeaningfulValue(formData.get("generatedBoundaryCoordinates"));
            // 如果是多边形障碍物,必须生成边界坐标才能保存
            // 如果是圆形障碍物或其他形状,不需要生成边界就能保存
            boolean boundaryRequirementMet = true;
            if ("polygon".equals(shapeKey)) {
                boundaryRequirementMet = hasGeneratedBoundary;
            }
            boolean enabled = hasCoords && hasName && boundaryRequirementMet;
            saveButton.setEnabled(enabled);
            if (enabled) {
                saveButton.setBackground(PRIMARY_COLOR);
                saveButton.setForeground(WHITE);
                saveButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                saveButton.setOpaque(true);
                saveButton.setContentAreaFilled(true);
            } else {
                saveButton.setBackground(MEDIUM_GRAY);
                saveButton.setForeground(TEXT_COLOR);
                saveButton.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                saveButton.setOpaque(true);
                saveButton.setContentAreaFilled(true);
            }
        }
    }
    private boolean validateStep2() {
        String name = formData.get("obstacleName");
        if (!isMeaningfulValue(name)) {
            JOptionPane.showMessageDialog(this, "请填写障碍物名称", "提示", JOptionPane.WARNING_MESSAGE);
            showStep(1);
            if (obstacleNameField != null) {
                obstacleNameField.requestFocusInWindow();
            }
            return false;
        }
        if (!isObstacleNameUnique(name)) {
            JOptionPane.showMessageDialog(this, "障碍物名称已存在,请输入唯一名称", "提示", JOptionPane.WARNING_MESSAGE);
            showStep(1);
            if (obstacleNameField != null) {
                obstacleNameField.requestFocusInWindow();
            }
            return false;
        }
        if (!isMeaningfulValue(formData.get("obstacleCoordinates"))) {
            JOptionPane.showMessageDialog(this, "请先完成障碍物绘制", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
@@ -995,34 +1703,504 @@
        if (!validateStep2()) {
            return;
        }
        String coords = formData.get("obstacleCoordinates").trim();
        String landNumber = targetDikuai.getLandNumber();
        if (!Dikuai.updateField(landNumber, "obstacleCoordinates", coords)) {
            JOptionPane.showMessageDialog(this, "无法更新障碍物坐标", "错误", JOptionPane.ERROR_MESSAGE);
        String obstacleName = formData.get("obstacleName");
        String coordsValue = formData.get("obstacleCoordinates");
        String originalValue = formData.get("obstacleOriginalCoordinates");
        String generatedBoundaryValue = formData.get("generatedBoundaryCoordinates");  // 生成的边界坐标
        String shapeKey = formData.get("obstacleShape");
        String previousName = formData.get("editingObstacleName");
        if (!isMeaningfulValue(coordsValue)) {
            JOptionPane.showMessageDialog(this, "障碍物坐标无效", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }
        String coords = coordsValue.trim();
        String originalCoords = isMeaningfulValue(originalValue) ? originalValue.trim() : null;
        // 对于圆形障碍物,直接使用obstacleCoordinates(在第3个点确认时已生成)
        // 对于多边形障碍物,如果有生成的边界坐标,使用生成的边界坐标;否则使用原始坐标
        String finalCoords;
        if ("circle".equals(shapeKey)) {
            // 圆形障碍物直接使用已生成的坐标
            finalCoords = coords;
        } else {
            // 多边形障碍物优先使用生成的边界坐标
            finalCoords = isMeaningfulValue(generatedBoundaryValue) ? generatedBoundaryValue.trim() : coords;
        }
        String trimmedName = isMeaningfulValue(obstacleName) ? obstacleName.trim() : null;
        if (!isMeaningfulValue(trimmedName)) {
            JOptionPane.showMessageDialog(this, "障碍物名称无效", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }
        formData.put("obstacleName", trimmedName);
        if (isMeaningfulValue(previousName)) {
            formData.put("editingObstacleName", trimmedName);
        }
        if (!persistObstacleToConfig(landNumber, previousName, trimmedName, shapeKey, finalCoords, originalCoords)) {
            JOptionPane.showMessageDialog(this, "写入障碍物配置失败,请重试", "错误", JOptionPane.ERROR_MESSAGE);
            return;
        }
        Dikuai.updateField(landNumber, "updateTime", currentTime());
        Dikuai.saveToProperties();
        Dikuaiguanli.notifyExternalCreation(landNumber);
        JOptionPane.showMessageDialog(this, "障碍物坐标已保存", "成功", JOptionPane.INFORMATION_MESSAGE);
    JOptionPane.showMessageDialog(this, "障碍物数据已保存", "成功", JOptionPane.INFORMATION_MESSAGE);
        activeSession = null;
        dispose();
    }
    /**
     * 生成障碍物边界坐标
     */
    private void generateObstacleBoundary() {
        String originalCoords = formData.get("obstacleOriginalCoordinates");
        if (!isMeaningfulValue(originalCoords)) {
            JOptionPane.showMessageDialog(this, "无原始坐标数据,无法生成边界", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        String baseStation = targetDikuai.getBaseStationCoordinates();
        if (!isMeaningfulValue(baseStation)) {
            JOptionPane.showMessageDialog(this, "地块未设置基站坐标", "提示", JOptionPane.WARNING_MESSAGE);
            return;
        }
        String shapeKey = formData.get("obstacleShape");
        try {
            String method = formData.get("drawingMethod");
            // 处理圆形障碍物
            if ("circle".equals(shapeKey)) {
                if (!"mower".equals(method)) {
                    JOptionPane.showMessageDialog(this, "只有割草机绘制的圆形障碍物才支持生成边界坐标", "提示", JOptionPane.INFORMATION_MESSAGE);
                    return;
                }
                // 将原始坐标转换为Coordinate对象列表
                List<Coordinate> coordinateList = parseOriginalCoordinatesToCoordinateList(originalCoords);
                if (coordinateList.size() < 3) {
                    JOptionPane.showMessageDialog(this, "圆形障碍物至少需要三个采集点", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                // 解析基站坐标
                String[] baseParts = baseStation.split(",");
                if (baseParts.length < 4) {
                    JOptionPane.showMessageDialog(this, "基站坐标格式无效", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                double baseLat = parseDMToDecimal(baseParts[0].trim(), baseParts[1].trim());
                double baseLon = parseDMToDecimal(baseParts[2].trim(), baseParts[3].trim());
                // 将原始坐标转换为XY坐标(本地坐标系)
                List<double[]> xyPoints = new ArrayList<>();
                for (Coordinate coord : coordinateList) {
                    double lat = parseDMToDecimal(coord.getLatitude(), coord.getLatDirection());
                    double lon = parseDMToDecimal(coord.getLongitude(), coord.getLonDirection());
                    xyPoints.add(convertLatLonToLocal(lat, lon, baseLat, baseLon));
                }
                // 使用ThreePointCircle算法计算圆心和半径
                if (xyPoints.size() < 3) {
                    JOptionPane.showMessageDialog(this, "至少需要三个点才能生成圆形边界", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                // 取前三个点计算圆
                double[] p1 = xyPoints.get(0);
                double[] p2 = xyPoints.get(1);
                double[] p3 = xyPoints.get(2);
                String point1 = String.format(Locale.US, "%.2f,%.2f", p1[0], p1[1]);
                String point2 = String.format(Locale.US, "%.2f,%.2f", p2[0], p2[1]);
                String point3 = String.format(Locale.US, "%.2f,%.2f", p3[0], p3[1]);
                String circleResult = bianjie.ThreePointCircle.getCircleFromPoints(point1, point2, point3);
                // 解析结果:格式为 "圆心: x,y; 半径: r"
                if (circleResult == null || circleResult.startsWith("错误")) {
                    JOptionPane.showMessageDialog(this, "生成圆形边界失败: " + circleResult, "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                // 解析圆心和半径
                String[] parts = circleResult.split(";");
                if (parts.length < 2) {
                    JOptionPane.showMessageDialog(this, "解析圆形边界结果失败", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                // 提取圆心坐标
                String centerPart = parts[0].trim(); // "圆心: x,y"
                String radiusPart = parts[1].trim(); // "半径: r"
                String centerCoords = centerPart.substring(centerPart.indexOf(":") + 1).trim();
                String radiusStr = radiusPart.substring(radiusPart.indexOf(":") + 1).trim();
                String[] centerXY = centerCoords.split(",");
                if (centerXY.length < 2) {
                    JOptionPane.showMessageDialog(this, "解析圆心坐标失败", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                double centerX = Double.parseDouble(centerXY[0].trim());
                double centerY = Double.parseDouble(centerXY[1].trim());
                double radius = Double.parseDouble(radiusStr.trim());
                // 计算圆上一点(圆心右侧的点)
                double radiusX = centerX + radius;
                double radiusY = centerY;
                // 生成边界坐标格式:圆心X,圆心Y;圆上点X,圆上点Y
                String boundaryCoords = String.format(Locale.US, "%.2f,%.2f;%.2f,%.2f",
                    centerX, centerY, radiusX, radiusY);
                // 保存生成的边界坐标
                formData.put("generatedBoundaryCoordinates", boundaryCoords);
                // 更新边界状态标签显示(圆形只有2个点:圆心和圆上一点)
                updateBoundaryStatusLabel(2);
                // 更新预览按钮显示(变成绿色可点击)
                updatePreviewButtonState();
                // 更新保存按钮状态(变成可点击)
                updateSaveButtonState();
                // 强制刷新UI
                SwingUtilities.invokeLater(() -> {
                    if (previewButton != null) {
                        previewButton.revalidate();
                        previewButton.repaint();
                    }
                });
                JOptionPane.showMessageDialog(this, "圆形障碍物边界坐标已生成", "成功", JOptionPane.INFORMATION_MESSAGE);
                return;
            }
            // 处理多边形障碍物
            if (!"polygon".equals(shapeKey)) {
                JOptionPane.showMessageDialog(this, "只有多边形或圆形障碍物才需要生成边界坐标", "提示", JOptionPane.INFORMATION_MESSAGE);
                return;
            }
            // 检查绘制方式,只有割草机绘制的多边形才调用bianjieguihua2
            if (!"mower".equals(method)) {
                JOptionPane.showMessageDialog(this, "只有割草机绘制的多边形才支持生成边界坐标", "提示", JOptionPane.INFORMATION_MESSAGE);
                return;
            }
            // 将原始坐标转换为Coordinate对象列表
            List<Coordinate> coordinateList = parseOriginalCoordinatesToCoordinateList(originalCoords);
            if (coordinateList.isEmpty()) {
                JOptionPane.showMessageDialog(this, "原始坐标数据无效", "错误", JOptionPane.ERROR_MESSAGE);
                return;
            }
            // 保存当前的Coordinate.coordinates
            List<Coordinate> savedCoordinates = new ArrayList<>(Coordinate.coordinates);
            try {
                // 设置到全局坐标列表
                Coordinate.coordinates.clear();
                Coordinate.coordinates.addAll(coordinateList);
                // 调用bianjieguihua2算法生成优化后的多边形边界坐标
                String optimizedCoordsStr = bianjieguihua2.processCoordinateListAuto(baseStation);
                if (optimizedCoordsStr == null || optimizedCoordsStr.trim().isEmpty()) {
                    JOptionPane.showMessageDialog(this, "生成边界坐标失败", "错误", JOptionPane.ERROR_MESSAGE);
                    return;
                }
                // 保存生成的边界坐标
                formData.put("generatedBoundaryCoordinates", optimizedCoordsStr.trim());
                // 计算生成的边界点数
                int boundaryPointCount = countCoordinatePairs(optimizedCoordsStr.trim());
                // 更新边界状态标签显示
                updateBoundaryStatusLabel(boundaryPointCount);
                // 更新预览按钮显示(变成绿色可点击)
                updatePreviewButtonState();
                // 更新保存按钮状态(变成可点击)
                updateSaveButtonState();
                // 强制刷新UI
                SwingUtilities.invokeLater(() -> {
                    if (previewButton != null) {
                        previewButton.revalidate();
                        previewButton.repaint();
                    }
                });
                JOptionPane.showMessageDialog(this, "障碍物边界坐标已生成", "成功", JOptionPane.INFORMATION_MESSAGE);
            } finally {
                // 恢复原来的坐标列表
                Coordinate.coordinates.clear();
                Coordinate.coordinates.addAll(savedCoordinates);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(this, "生成边界坐标时发生错误: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
        }
    }
    /**
     * 将原始坐标字符串解析为Coordinate对象列表
     */
    private List<Coordinate> parseOriginalCoordinatesToCoordinateList(String originalCoords) {
        List<Coordinate> coordinateList = new ArrayList<>();
        if (!isMeaningfulValue(originalCoords)) {
            return coordinateList;
        }
        // 原始坐标格式:纬度1,方向1,经度1,方向1;纬度2,方向2,经度2,方向2;...
        String[] pointStrings = originalCoords.split(";");
        for (String pointStr : pointStrings) {
            pointStr = pointStr.trim();
            if (pointStr.isEmpty()) continue;
            String[] parts = pointStr.split(",");
            if (parts.length >= 4) {
                try {
                    String lat = parts[0].trim();
                    String latDir = parts[1].trim();
                    String lon = parts[2].trim();
                    String lonDir = parts[3].trim();
                    Coordinate coord = new Coordinate(lat, latDir, lon, lonDir, 0.0);
                    coordinateList.add(coord);
                } catch (Exception e) {
                    // 跳过无效的坐标点
                    continue;
                }
            }
        }
        return coordinateList;
    }
    /**
     * 预览障碍物边界
     */
    private void previewObstacleBoundary() {
        String generatedBoundary = formData.get("generatedBoundaryCoordinates");
        if (!isMeaningfulValue(generatedBoundary)) {
            JOptionPane.showMessageDialog(this, "请先生成障碍物边界坐标", "提示", JOptionPane.INFORMATION_MESSAGE);
            return;
        }
        String landNumber = targetDikuai.getLandNumber();
        String landName = targetDikuai.getLandName();
        String boundary = targetDikuai.getBoundaryCoordinates();
        // 获取障碍物坐标(优先使用生成的边界坐标,如果没有则使用原始坐标)
        String obstacleCoords = generatedBoundary;
        String shapeKey = formData.get("obstacleShape");
        String obstacleName = formData.get("obstacleName");
        // 对于圆形障碍物,生成的边界坐标格式就是障碍物坐标格式,可以直接使用
        // 对于多边形障碍物,也需要使用生成的边界坐标
        // 如果生成的边界坐标不可用,尝试使用原始障碍物坐标
        if (!isMeaningfulValue(obstacleCoords)) {
            obstacleCoords = formData.get("obstacleCoordinates");
            if (!isMeaningfulValue(obstacleCoords)) {
                JOptionPane.showMessageDialog(this, "无法获取障碍物坐标进行预览", "提示", JOptionPane.INFORMATION_MESSAGE);
                return;
            }
        }
        // 构建障碍物数据字符串,包含名称、形状和坐标
        // 格式:障碍物名称::形状::坐标 或 障碍物名称:形状:坐标
        final String obstacleData;
        if (isMeaningfulValue(obstacleName) && isMeaningfulValue(shapeKey)) {
            // 使用 :: 分隔符格式:名称::形状::坐标
            obstacleData = obstacleName.trim() + "::" + shapeKey.trim() + "::" + obstacleCoords;
        } else if (isMeaningfulValue(shapeKey)) {
            // 只有形状:形状::坐标
            obstacleData = shapeKey.trim() + "::" + obstacleCoords;
        } else {
            // 只有坐标
            obstacleData = obstacleCoords;
        }
        // 关闭当前对话框
        setVisible(false);
        SwingUtilities.invokeLater(() -> {
            Shouye shouye = Shouye.getInstance();
            if (shouye != null) {
                // 传递回调以重新打开新增障碍物步骤2页面
                shouye.startMowingPathPreview(
                    landNumber,
                    landName,
                    boundary,
                    obstacleData,  // 使用包含名称和形状的障碍物数据
                    null,
                    () -> SwingUtilities.invokeLater(() -> {
                        // 重新打开新增障碍物步骤2页面
                        Window owner = SwingUtilities.getWindowAncestor(shouye);
                        setVisible(true);
                    })
                );
            } else {
                JOptionPane.showMessageDialog(null, "无法打开主页面进行预览", "提示", JOptionPane.WARNING_MESSAGE);
                setVisible(true);
            }
        });
    }
    private boolean persistObstacleToConfig(String landNumber, String previousName, String obstacleName,
                                            String shapeKey, String xyCoords, String originalCoords) {
        if (!isMeaningfulValue(landNumber) || !isMeaningfulValue(obstacleName) || !isMeaningfulValue(xyCoords)) {
            return false;
        }
        String normalizedLandNumber = landNumber.trim();
        String normalizedNewName = obstacleName.trim();
        String normalizedPrevious = isMeaningfulValue(previousName) ? previousName.trim() : null;
        String normalizedXy = xyCoords.trim();
        try {
            File configFile = new File("Obstacledge.properties");
            Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager();
            if (configFile.exists()) {
                if (!manager.loadFromFile(configFile.getAbsolutePath())) {
                    return false;
                }
            }
            Obstacledge.Plot plot = manager.getPlotById(normalizedLandNumber);
            if (plot == null) {
                plot = new Obstacledge.Plot(normalizedLandNumber);
                manager.addPlot(plot);
            }
            ensurePlotBaseStation(plot);
            if (normalizedPrevious != null && !normalizedPrevious.equals(normalizedNewName)) {
                plot.removeObstacleByName(normalizedPrevious);
            }
            Obstacledge.Obstacle obstacle = plot.getObstacleByName(normalizedNewName);
            if (obstacle == null) {
                obstacle = new Obstacledge.Obstacle(normalizedLandNumber, normalizedNewName,
                        determineObstacleShape(shapeKey, normalizedXy));
                plot.addObstacle(obstacle);
            }
            obstacle.setPlotId(normalizedLandNumber);
            obstacle.setObstacleName(normalizedNewName);
            obstacle.setShape(determineObstacleShape(shapeKey, normalizedXy));
            obstacle.setXyCoordinates(new ArrayList<Obstacledge.XYCoordinate>());
            obstacle.setOriginalCoordinates(new ArrayList<Obstacledge.DMCoordinate>());
            obstacle.setXyCoordsString(normalizedXy);
            if (isMeaningfulValue(originalCoords)) {
                try {
                    obstacle.setOriginalCoordsString(originalCoords);
                } catch (Exception parseEx) {
                    System.err.println("解析障碍物原始坐标失败,将使用占位值: " + parseEx.getMessage());
                    obstacle.setOriginalCoordinates(new ArrayList<Obstacledge.DMCoordinate>());
                }
            }
            ensurePlaceholderOriginalCoords(obstacle);
            return manager.saveToFile(configFile.getAbsolutePath());
        } catch (Exception ex) {
            System.err.println("保存障碍物配置失败: " + ex.getMessage());
            return false;
        }
    }
    private Obstacledge.ObstacleShape determineObstacleShape(String shapeKey, String xyCoords) {
        if (isMeaningfulValue(shapeKey)) {
            String normalized = shapeKey.trim().toLowerCase(Locale.ROOT);
            if ("circle".equals(normalized) || "圆形".equals(normalized) || "0".equals(normalized)) {
                return Obstacledge.ObstacleShape.CIRCLE;
            }
            if ("polygon".equals(normalized) || "多边形".equals(normalized) || "1".equals(normalized)) {
                return Obstacledge.ObstacleShape.POLYGON;
            }
        }
        int pairCount = countCoordinatePairs(xyCoords);
        if (pairCount <= 0) {
            return Obstacledge.ObstacleShape.POLYGON;
        }
        if (pairCount <= 2) {
            return Obstacledge.ObstacleShape.CIRCLE;
        }
        return Obstacledge.ObstacleShape.POLYGON;
    }
    private void ensurePlaceholderOriginalCoords(Obstacledge.Obstacle obstacle) {
        if (obstacle == null) {
            return;
        }
        List<Obstacledge.DMCoordinate> originals = obstacle.getOriginalCoordinates();
        if (originals != null && !originals.isEmpty()) {
            return;
        }
        List<Obstacledge.XYCoordinate> xyCoords = obstacle.getXyCoordinates();
        int pointCount = (xyCoords != null && !xyCoords.isEmpty()) ? xyCoords.size() : 1;
        List<Obstacledge.DMCoordinate> placeholders = new ArrayList<>(pointCount * 2);
        for (int i = 0; i < pointCount; i++) {
            placeholders.add(new Obstacledge.DMCoordinate(0.0, 'N'));
            placeholders.add(new Obstacledge.DMCoordinate(0.0, 'E'));
        }
        obstacle.setOriginalCoordinates(placeholders);
    }
    private void ensurePlotBaseStation(Obstacledge.Plot plot) {
        if (plot == null) {
            return;
        }
        String existing = plot.getBaseStationString();
        if (isMeaningfulValue(existing)) {
            return;
        }
        String baseStation = resolveBaseStationCoordinates();
        if (!isMeaningfulValue(baseStation)) {
            return;
        }
        try {
            plot.setBaseStationString(baseStation.trim());
        } catch (Exception ex) {
            System.err.println("设置基准站坐标失败: " + ex.getMessage());
        }
    }
    private static final class ExistingObstacle {
        private final String name;
        private final Obstacledge.ObstacleShape shape;
        private final String coordinates;
        private final String xyCoordinates;
        private final String originalCoordinates;
        ExistingObstacle(String name, Obstacledge.ObstacleShape shape, String coordinates) {
        ExistingObstacle(String name, Obstacledge.ObstacleShape shape, String xyCoordinates, String originalCoordinates) {
            this.name = name != null ? name : "";
            this.shape = shape;
            this.coordinates = coordinates != null ? coordinates : "";
            this.xyCoordinates = safeCoordString(xyCoordinates);
            this.originalCoordinates = safeCoordString(originalCoordinates);
        }
        static ExistingObstacle placeholder(String name) {
            return new ExistingObstacle(name, null, "");
            return new ExistingObstacle(name, null, "", "");
        }
        String getName() {
@@ -1047,7 +2225,38 @@
        }
        String getCoordinates() {
            return coordinates;
            return getDisplayCoordinates();
        }
        String getDisplayCoordinates() {
            return hasText(xyCoordinates) ? xyCoordinates : originalCoordinates;
        }
        String getXyCoordinates() {
            return xyCoordinates;
        }
        String getOriginalCoordinates() {
            return originalCoordinates;
        }
        boolean hasPersistedData() {
            return hasText(xyCoordinates) || hasText(originalCoordinates);
        }
        private static String safeCoordString(String value) {
            if (value == null) {
                return "";
            }
            return value.trim();
        }
        private static boolean hasText(String value) {
            if (value == null) {
                return false;
            }
            String trimmed = value.trim();
            return !trimmed.isEmpty() && !"-1".equals(trimmed);
        }
    }
@@ -1058,10 +2267,25 @@
        if (step < 2) {
            nextButton.setVisible(true);
            saveButton.setVisible(false);
            if (previewButton != null) {
                previewButton.setVisible(false);
            }
        } else {
            nextButton.setVisible(false);
            saveButton.setVisible(true);
            updateDrawingStatus();
            updatePreviewButtonState();
            // 对于圆形障碍物,确保预览按钮和生成边界按钮隐藏
            String shapeKey = formData.get("obstacleShape");
            boolean isCircle = "circle".equals(shapeKey);
            if (isCircle) {
                if (previewButton != null) {
                    previewButton.setVisible(false);
                }
                if (generateBoundaryButton != null) {
                    generateBoundaryButton.setVisible(false);
                }
            }
        }
        updateSaveButtonState();
        revalidate();
@@ -1076,6 +2300,11 @@
        formData.clear();
        formData.putAll(session.data);
        String sessionName = session.data.get("obstacleName");
        if (obstacleNameField != null) {
            obstacleNameField.setText(sessionName != null ? sessionName : "");
        }
        String method = session.data.get("drawingMethod");
        if (method != null) {
            JPanel panel = methodOptionPanels.get(method);
@@ -1092,7 +2321,7 @@
            }
        }
        if (session.captureMessage != null) {
    if (session.captureMessage != null && !session.captureSuccessful) {
            JOptionPane.showMessageDialog(this,
                    session.captureMessage,
                    session.captureSuccessful ? "成功" : "提示",
@@ -1100,6 +2329,24 @@
        }
        updateDrawingStatus();
        // 如果已有生成的边界坐标,更新边界状态标签
        String generatedBoundary = session.data.get("generatedBoundaryCoordinates");
        if (isMeaningfulValue(generatedBoundary)) {
            int boundaryPointCount = countCoordinatePairs(generatedBoundary);
            updateBoundaryStatusLabel(boundaryPointCount);
        } else {
            // 如果没有生成的边界坐标,隐藏边界状态标签
            if (boundaryStatusLabel != null) {
                boundaryStatusLabel.setVisible(false);
                boundaryStatusLabel.setText("");
            }
        }
        // 更新按钮状态
        updatePreviewButtonState();
        updateSaveButtonState();
        currentStep = 2;
        showStep(2);
    }
@@ -1108,6 +2355,93 @@
        return createInfoRow(label, value, null);
    }
    private JPanel createObstacleNameRow() {
        JPanel row = new JPanel();
        row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS));
        row.setOpaque(false);
        row.setAlignmentX(Component.LEFT_ALIGNMENT);
        row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 36));
        JLabel label = new JLabel("障碍物名称:");
        label.setFont(new Font("微软雅黑", Font.BOLD, 14));
        label.setForeground(TEXT_COLOR);
        label.setAlignmentX(Component.LEFT_ALIGNMENT);
        if (obstacleNameField == null) {
            obstacleNameField = new JTextField();
            obstacleNameField.setFont(new Font("微软雅黑", Font.PLAIN, 14));
            obstacleNameField.setColumns(16);
            Dimension fieldSize = new Dimension(240, 30);
            obstacleNameField.setPreferredSize(fieldSize);
            obstacleNameField.setMinimumSize(fieldSize);
            obstacleNameField.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
            obstacleNameField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(BORDER_COLOR, 1),
                    BorderFactory.createEmptyBorder(4, 8, 4, 8)));
            obstacleNameField.getDocument().addDocumentListener(new DocumentListener() {
                @Override
                public void insertUpdate(DocumentEvent e) {
                    handleObstacleNameChanged();
                }
                @Override
                public void removeUpdate(DocumentEvent e) {
                    handleObstacleNameChanged();
                }
                @Override
                public void changedUpdate(DocumentEvent e) {
                    handleObstacleNameChanged();
                }
            });
        }
        String existing = formData.get("obstacleName");
        if (existing != null && !existing.equals(obstacleNameField.getText())) {
            obstacleNameField.setText(existing);
        }
        row.add(label);
        row.add(Box.createRigidArea(new Dimension(10, 0)));
        row.add(obstacleNameField);
        row.add(Box.createHorizontalGlue());
        return row;
    }
    private void handleObstacleNameChanged() {
        if (obstacleNameField == null) {
            return;
        }
        String text = obstacleNameField.getText();
        if (isMeaningfulValue(text)) {
            formData.put("obstacleName", text.trim());
        } else {
            formData.remove("obstacleName");
        }
        updateSaveButtonState();
    }
    private boolean isObstacleNameUnique(String candidate) {
        if (!isMeaningfulValue(candidate)) {
            return false;
        }
        String trimmed = candidate.trim().toLowerCase(Locale.ROOT);
        String original = formData.get("editingObstacleName");
        if (isMeaningfulValue(original) && trimmed.equals(original.trim().toLowerCase(Locale.ROOT))) {
            return true;
        }
        for (ExistingObstacle obstacle : existingObstacles) {
            if (obstacle == null) {
                continue;
            }
            String existingName = obstacle.getName();
            if (isMeaningfulValue(existingName) && trimmed.equals(existingName.trim().toLowerCase(Locale.ROOT))) {
                return false;
            }
        }
        return true;
    }
    private JPanel createInfoRow(String label, String value, String tooltip) {
        JPanel row = new JPanel();
        row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS));
@@ -1136,15 +2470,14 @@
    }
    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(10, 22, 10, 22)));
        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        button.setFocusPainted(false);
        button.setOpaque(true);
        button.setContentAreaFilled(true);
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
@@ -1157,6 +2490,9 @@
            public void mouseExited(MouseEvent e) {
                if (button.isEnabled()) {
                    button.setBackground(PRIMARY_COLOR);
                } else {
                    // 禁用时保持灰色背景
                    button.setBackground(MEDIUM_GRAY);
                }
            }
        });
@@ -1164,15 +2500,15 @@
    }
    private JButton createSecondaryButton(String text) {
        JButton button = new JButton(text);
        JButton button = buttonset.createStyledButton(text, MEDIUM_GRAY);
        button.setFont(new Font("微软雅黑", Font.BOLD, 16));
        button.setBackground(MEDIUM_GRAY);
        button.setForeground(TEXT_COLOR);
        button.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(BORDER_COLOR, 2),
                BorderFactory.createEmptyBorder(10, 22, 10, 22)));
        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        button.setFocusPainted(false);
        button.setOpaque(true);
        button.setContentAreaFilled(true);
        return button;
    }
@@ -1206,6 +2542,18 @@
        return null;
    }
    private String resolveDrawingDeviceId(String method) {
        if (!isMeaningfulValue(method)) {
            return null;
        }
        String key = "handheld".equalsIgnoreCase(method) ? "handheldMarkerId" : "mowerId";
        String value = Setsys.getPropertyValue(key);
        if (!isMeaningfulValue(value)) {
            return null;
        }
        return value.trim();
    }
    private String safeValue(String value, String fallback) {
        if (!isMeaningfulValue(value)) {
            return fallback;