package dikuai; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.FlowLayout; 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; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; 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},支持通过实地绘制采集障碍物坐标。 */ public class addzhangaiwu extends JDialog { private static final long serialVersionUID = 1L; private static final double METERS_PER_DEGREE_LAT = 111320.0d; private final Color PRIMARY_COLOR = new Color(46, 139, 87); private final Color PRIMARY_DARK = new Color(30, 107, 69); private final Color PRIMARY_LIGHT = new Color(232, 245, 233); private final Color WHITE = Color.WHITE; private final Color BORDER_COLOR = new Color(222, 226, 230); 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 existingObstacles; private final Map formData = new HashMap<>(); private CardLayout cardLayout; private JPanel stepsPanel; private JButton prevButton; private JButton nextButton; private JButton saveButton; private JPanel methodOptionsPanel; private JPanel shapeOptionsPanel; private final Map methodOptionPanels = new HashMap<>(); private final Map shapeOptionPanels = new HashMap<>(); 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; private String activeDrawingShape; private static ObstacleDrawingSession activeSession; private static boolean resumeRequested; private static final class ObstacleDrawingSession { Dikuai target; Map data = new HashMap<>(); String baseStation; boolean drawingCompleted; boolean captureSuccessful; String captureMessage; } private addzhangaiwu(Window owner, Dikuai target, List obstacleNames) { super(owner, "新增障碍物", ModalityType.APPLICATION_MODAL); if (target == null) { throw new IllegalArgumentException("targetDikuai 不能为空"); } Coordinate.coordinates.clear(); Coordinate.setStartSaveGngga(false); Coordinate.clearActiveDeviceIdFilter(); this.targetDikuai = target; this.existingObstacles = new ArrayList<>(resolveExistingObstacles(target, obstacleNames)); initializeUI(); setupEventHandlers(); preloadData(); } private void initializeUI() { Dimension dialogSize = new Dimension(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT); setSize(dialogSize); setPreferredSize(dialogSize); setMinimumSize(dialogSize); setMaximumSize(dialogSize); setResizable(false); setLocationRelativeTo(getOwner()); getContentPane().setLayout(new BorderLayout()); getContentPane().setBackground(WHITE); JPanel container = new JPanel(new BorderLayout()); container.setBackground(WHITE); container.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); container.setPreferredSize(new Dimension(UIConfig.DIALOG_WIDTH - 40, UIConfig.DIALOG_HEIGHT - 40)); cardLayout = new CardLayout(); stepsPanel = new JPanel(cardLayout); stepsPanel.setBackground(WHITE); stepsPanel.setPreferredSize(new Dimension(UIConfig.DIALOG_WIDTH - 40, UIConfig.DIALOG_HEIGHT - 140)); stepsPanel.add(createStep1Panel(), "step1"); stepsPanel.add(createStep2Panel(), "step2"); container.add(stepsPanel, BorderLayout.CENTER); container.add(createButtonPanel(), BorderLayout.SOUTH); add(container, BorderLayout.CENTER); showStep(1); } private JPanel createStep1Panel() { JPanel stepPanel = new JPanel(); stepPanel.setLayout(new BoxLayout(stepPanel, BoxLayout.Y_AXIS)); stepPanel.setBackground(WHITE); JLabel title = new JLabel("步骤1:确认地块信息"); title.setFont(new Font("微软雅黑", Font.BOLD, 20)); title.setForeground(TEXT_COLOR); title.setAlignmentX(Component.LEFT_ALIGNMENT); title.setHorizontalAlignment(SwingConstants.LEFT); stepPanel.add(title); stepPanel.add(Box.createRigidArea(new Dimension(0, 20))); JPanel infoContainer = new JPanel(); infoContainer.setLayout(new BoxLayout(infoContainer, BoxLayout.Y_AXIS)); infoContainer.setOpaque(false); infoContainer.setAlignmentX(Component.LEFT_ALIGNMENT); infoContainer.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); 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, 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); stepPanel.add(Box.createVerticalGlue()); return stepPanel; } private JPanel buildExistingObstacleSection() { JPanel section = new JPanel(); section.setLayout(new BoxLayout(section, BoxLayout.Y_AXIS)); section.setOpaque(false); section.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel titleLabel = new JLabel("已有障碍物"); titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); titleLabel.setForeground(TEXT_COLOR); titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); 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); 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))); } } } existingObstacleListPanel.revalidate(); existingObstacleListPanel.repaint(); } private JPanel createObstacleSummaryRow(ExistingObstacle obstacle) { 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, 32)); JLabel infoLabel = new JLabel(buildObstacleSummaryText(obstacle)); infoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); infoLabel.setForeground(TEXT_COLOR); infoLabel.setAlignmentX(Component.LEFT_ALIGNMENT); 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 = buttonset.createStyledButton(text, PRIMARY_COLOR); button.setFont(new Font("微软雅黑", Font.BOLD, 12)); button.setBorder(BorderFactory.createEmptyBorder(6, 16, 6, 16)); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); Dimension size = new Dimension(72, 28); button.setPreferredSize(size); button.setMinimumSize(size); button.setMaximumSize(size); button.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { button.setBackground(PRIMARY_DARK); } @Override public void mouseExited(MouseEvent e) { button.setBackground(PRIMARY_COLOR); } }); 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.getDisplayCoordinates(), 5); return String.format(Locale.CHINA, "%s,%s,坐标:%s", name, shape, coordPreview); } private String buildCoordinatePreview(String coords, int keepLength) { if (!isMeaningfulValue(coords)) { return "无坐标"; } String sanitized = coords.replaceAll("\\s+", ""); if (sanitized.length() <= keepLength) { return sanitized; } if (keepLength <= 0) { return "..."; } 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 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); selectShapeOption(shapePanel, shapeKey, false); } updateDrawingStatus(); 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)); stepPanel.setBackground(WHITE); JLabel title = new JLabel("步骤2:绘制障碍物坐标"); title.setFont(new Font("微软雅黑", Font.BOLD, 20)); title.setForeground(TEXT_COLOR); title.setAlignmentX(Component.LEFT_ALIGNMENT); stepPanel.add(title); stepPanel.add(Box.createRigidArea(new Dimension(0, 16))); stepPanel.add(createSectionHeader("选择绘制方式", "支持割草机或手持设备采集障碍物边界")); stepPanel.add(Box.createRigidArea(new Dimension(0, 10))); methodOptionsPanel = new JPanel(); methodOptionsPanel.setLayout(new BoxLayout(methodOptionsPanel, BoxLayout.Y_AXIS)); methodOptionsPanel.setOpaque(false); methodOptionsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); methodOptionsPanel.add(createMethodOption("mower", "割草机绘制", "驾驶割草机沿障碍物边界行驶采集坐标", "image/mow.png")); methodOptionsPanel.add(Box.createRigidArea(new Dimension(0, 10))); methodOptionsPanel.add(createMethodOption("handheld", "手持设备绘制", "使用手持设备沿障碍物边界行走记录坐标", "image/URT.png")); stepPanel.add(methodOptionsPanel); stepPanel.add(Box.createRigidArea(new Dimension(0, 20))); stepPanel.add(createSectionHeader("障碍物形状", "多边形需采集多个点,圆形需在圆周上采集至少三个点")); stepPanel.add(Box.createRigidArea(new Dimension(0, 10))); shapeOptionsPanel = new JPanel(); shapeOptionsPanel.setLayout(new BoxLayout(shapeOptionsPanel, BoxLayout.Y_AXIS)); shapeOptionsPanel.setOpaque(false); shapeOptionsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); shapeOptionsPanel.add(createShapeOption("polygon", "多边形", "依次采集轮廓上的多个点或者沿障碍物边缘走一圈")); shapeOptionsPanel.add(Box.createRigidArea(new Dimension(0, 10))); 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.addActionListener(e -> startDrawingWorkflow()); 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))); drawingStatusLabel = new JLabel("尚未采集障碍物坐标"); drawingStatusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); 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; } private JPanel createButtonPanel() { JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); buttonPanel.setBackground(WHITE); buttonPanel.setBorder(BorderFactory.createEmptyBorder(20, 0, 0, 0)); 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(Box.createRigidArea(new Dimension(12, 0))); buttonPanel.add(previewButton); buttonPanel.add(Box.createRigidArea(new Dimension(10, 0))); buttonPanel.add(saveButton); attachNextButtonToStep1Row(); return buttonPanel; } private JPanel createInstructionPanel(String text) { JPanel instruction = new JPanel(new BorderLayout()); instruction.setBackground(PRIMARY_LIGHT); instruction.setAlignmentX(Component.LEFT_ALIGNMENT); instruction.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createMatteBorder(0, 5, 0, 0, PRIMARY_COLOR), BorderFactory.createEmptyBorder(12, 14, 12, 14))); JLabel icon = new JLabel("💡", SwingConstants.CENTER); icon.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 18)); icon.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10)); instruction.add(icon, BorderLayout.WEST); JLabel content = new JLabel(text); content.setFont(new Font("微软雅黑", Font.PLAIN, 13)); content.setForeground(TEXT_COLOR); instruction.add(content, BorderLayout.CENTER); return instruction; } private JPanel createSectionHeader(String title, String hint) { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.setOpaque(false); panel.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel titleLabel = new JLabel(title); titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); titleLabel.setForeground(TEXT_COLOR); titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); panel.add(titleLabel); if (hint != null && !hint.trim().isEmpty()) { panel.add(Box.createRigidArea(new Dimension(0, 4))); JLabel hintLabel = new JLabel(hint); hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); hintLabel.setForeground(LIGHT_TEXT); hintLabel.setAlignmentX(Component.LEFT_ALIGNMENT); panel.add(hintLabel); } return panel; } private JPanel createMethodOption(String type, String title, String description, String iconPath) { JPanel option = new JPanel(new BorderLayout(15, 0)); option.setBackground(WHITE); option.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(18, 20, 18, 20))); option.putClientProperty("paddingInsets", new int[]{18, 20, 18, 20}); option.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); option.setAlignmentX(Component.LEFT_ALIGNMENT); option.setMaximumSize(new Dimension(520, 120)); JLabel iconLabel = new JLabel(); iconLabel.setHorizontalAlignment(SwingConstants.CENTER); iconLabel.setPreferredSize(new Dimension(80, 80)); if (iconPath != null && !iconPath.trim().isEmpty()) { ImageIcon rawIcon = new ImageIcon(iconPath); if (rawIcon.getIconWidth() > 0 && rawIcon.getIconHeight() > 0) { Image scaled = rawIcon.getImage().getScaledInstance(64, 64, Image.SCALE_SMOOTH); iconLabel.setIcon(new ImageIcon(scaled)); } } if (iconLabel.getIcon() == null) { iconLabel.setText("mower".equals(type) ? "🚜" : "📱"); iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 44)); } option.add(iconLabel, BorderLayout.WEST); JPanel textPanel = new JPanel(); textPanel.setOpaque(false); textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS)); JLabel titleLabel = new JLabel(title); titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); titleLabel.setForeground(TEXT_COLOR); titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); option.putClientProperty("titleLabel", titleLabel); textPanel.add(titleLabel); if (description != null && !description.trim().isEmpty()) { textPanel.add(Box.createRigidArea(new Dimension(0, 8))); JLabel descLabel = new JLabel(description); descLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); descLabel.setForeground(LIGHT_TEXT); descLabel.setAlignmentX(Component.LEFT_ALIGNMENT); textPanel.add(descLabel); } option.add(textPanel, BorderLayout.CENTER); option.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { selectMethodOption(option, type, true); } @Override public void mouseEntered(MouseEvent e) { if (option != selectedMethodPanel) { option.setBackground(new Color(245, 245, 245)); } } @Override public void mouseExited(MouseEvent e) { if (option != selectedMethodPanel) { option.setBackground(WHITE); } } }); methodOptionPanels.put(type, option); return option; } private JPanel createShapeOption(String type, String title, String description) { JPanel option = new JPanel(); option.setLayout(new BoxLayout(option, BoxLayout.Y_AXIS)); option.setBackground(WHITE); option.setAlignmentX(Component.LEFT_ALIGNMENT); option.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(12, 18, 12, 18))); option.putClientProperty("paddingInsets", new int[]{12, 18, 12, 18}); option.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); JLabel titleLabel = new JLabel(title); titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 15)); titleLabel.setForeground(TEXT_COLOR); titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); option.putClientProperty("titleLabel", titleLabel); option.add(titleLabel); if (description != null && !description.trim().isEmpty()) { option.add(Box.createRigidArea(new Dimension(0, 4))); JLabel descLabel = new JLabel(description); descLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12)); descLabel.setForeground(LIGHT_TEXT); descLabel.setAlignmentX(Component.LEFT_ALIGNMENT); option.add(descLabel); } option.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { selectShapeOption(option, type, true); } @Override public void mouseEntered(MouseEvent e) { if (option != selectedShapePanel) { option.setBackground(new Color(245, 245, 245)); } } @Override public void mouseExited(MouseEvent e) { if (option != selectedShapePanel) { option.setBackground(WHITE); } } }); shapeOptionPanels.put(type, option); return option; } private void selectMethodOption(JPanel option, String type, boolean userTriggered) { if (option == null) { return; } if (userTriggered && "handheld".equalsIgnoreCase(type) && !hasConfiguredHandheldMarker()) { JOptionPane.showMessageDialog(this, "请先去系统设置添加便携打点器编号", "提示", JOptionPane.WARNING_MESSAGE); return; } if (selectedMethodPanel != null && selectedMethodPanel != option) { resetOptionAppearance(selectedMethodPanel); } selectedMethodPanel = option; highlightOption(option); formData.put("drawingMethod", type); if (userTriggered) { updateSaveButtonState(); } } 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; } if (selectedShapePanel != null && selectedShapePanel != option) { resetOptionAppearance(selectedShapePanel); } selectedShapePanel = option; highlightOption(option); formData.put("obstacleShape", type); if (userTriggered) { updateSaveButtonState(); } } private void highlightOption(JPanel option) { option.setBackground(PRIMARY_LIGHT); option.setBorder(buildOptionBorder(PRIMARY_COLOR, 3, option)); Object titleObj = option.getClientProperty("titleLabel"); if (titleObj instanceof JLabel) { ((JLabel) titleObj).setForeground(PRIMARY_COLOR); } } private void resetOptionAppearance(JPanel option) { if (option == null) { return; } option.setBackground(WHITE); option.setBorder(buildOptionBorder(BORDER_COLOR, 2, option)); Object titleObj = option.getClientProperty("titleLabel"); if (titleObj instanceof JLabel) { ((JLabel) titleObj).setForeground(TEXT_COLOR); } } private Border buildOptionBorder(Color color, int thickness, JPanel option) { Object paddingObj = option != null ? option.getClientProperty("paddingInsets") : null; if (paddingObj instanceof int[]) { int[] padding = (int[]) paddingObj; if (padding.length == 4) { Border line = BorderFactory.createLineBorder(color, thickness); Border paddingBorder = BorderFactory.createEmptyBorder(padding[0], padding[1], padding[2], padding[3]); return BorderFactory.createCompoundBorder(line, paddingBorder); } } return BorderFactory.createLineBorder(color, thickness); } private void setupEventHandlers() { prevButton.addActionListener(e -> { if (currentStep > 1) { showStep(currentStep - 1); } }); 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); } }); saveButton.addActionListener(e -> saveObstacleAndClose()); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { dispose(); } }); } private void startDrawingWorkflow() { if (drawingInProgress) { return; } 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)) { JOptionPane.showMessageDialog(this, "请选择绘制方式", "提示", JOptionPane.WARNING_MESSAGE); return; } String shape = formData.get("obstacleShape"); if (!isMeaningfulValue(shape)) { JOptionPane.showMessageDialog(this, "请选择障碍物形状", "提示", JOptionPane.WARNING_MESSAGE); return; } String baseStation = resolveBaseStationCoordinates(); if (!isMeaningfulValue(baseStation)) { JOptionPane.showMessageDialog(this, "未获取到有效的基准站坐标,请先在基准站管理中完成设置", "提示", JOptionPane.WARNING_MESSAGE); 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); if ("circle".equals(activeDrawingShape)) { drawingStatusLabel.setText("正在采集圆形障碍物,请沿圆周采集至少三个点后在主界面结束绘制。"); } else { drawingStatusLabel.setText("正在采集障碍物坐标,请在主界面完成绘制。"); } if (activeSession == null) { activeSession = new ObstacleDrawingSession(); } activeSession.target = targetDikuai; activeSession.data.clear(); activeSession.data.putAll(formData); activeSession.baseStation = baseStation; activeSession.drawingCompleted = false; activeSession.captureSuccessful = false; activeSession.captureMessage = null; resumeRequested = false; closeForDrawingSession(); } private void closeForDrawingSession() { Shouye shouye = Shouye.getInstance(); if (shouye != null) { shouye.showEndDrawingButton(addzhangaiwu::finishDrawingSession, activeDrawingShape); } activeDrawingShape = null; setVisible(false); dispose(); } public static void finishDrawingSession() { Coordinate.setStartSaveGngga(false); Coordinate.clearActiveDeviceIdFilter(); yulanzhangaiwu.stopPreview(); Shouye shouye = Shouye.getInstance(); if (shouye != null) { shouye.hideEndDrawingButton(); } if (activeSession == null) { return; } processCapturedCoordinates(activeSession); activeSession.drawingCompleted = true; resumeRequested = true; Component parent = shouye != null ? shouye : null; 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) { return; } activeSession.captureSuccessful = false; activeSession.drawingCompleted = false; activeSession.captureMessage = (message != null && !message.trim().isEmpty()) ? message.trim() : null; resumeRequested = true; Component parent = Shouye.getInstance(); Dikuai target = activeSession.target; SwingUtilities.invokeLater(() -> showDialog(parent, target)); } private static void processCapturedCoordinates(ObstacleDrawingSession session) { List captured = new ArrayList<>(Coordinate.coordinates); Coordinate.coordinates.clear(); if (captured.isEmpty()) { session.captureSuccessful = false; session.captureMessage = "未采集到任何障碍物坐标,请重新绘制"; return; } if (!isMeaningfulValue(session.baseStation)) { session.captureSuccessful = false; session.captureMessage = "基准站坐标无效,无法转换障碍物坐标"; return; } String originalCoordStr = buildOriginalCoordinateString(captured); if (isMeaningfulValue(originalCoordStr)) { session.data.put("obstacleOriginalCoordinates", originalCoordStr); } else { session.data.remove("obstacleOriginalCoordinates"); } List xyPoints = convertToLocalXY(captured, session.baseStation); if (xyPoints.isEmpty()) { session.captureSuccessful = false; session.captureMessage = "坐标转换失败,请检查基准站设置"; return; } String shape = session.data.get("obstacleShape"); if ("circle".equals(shape)) { if (xyPoints.size() < 3) { session.captureSuccessful = false; session.captureMessage = "圆形障碍物至少需要三个采集点(圆周上的点)"; return; } 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", circle.centerX, circle.centerY, radiusX, radiusY); session.data.put("obstacleCoordinates", result); session.captureSuccessful = true; session.captureMessage = "已采集圆形障碍物,共 " + xyPoints.size() + " 个点"; } else { if (xyPoints.size() < 3) { session.captureSuccessful = false; session.captureMessage = "多边形障碍物至少需要三个采集点"; return; } StringBuilder sb = new StringBuilder(); for (double[] pt : xyPoints) { if (sb.length() > 0) { sb.append(";"); } sb.append(String.format(Locale.US, "%.2f,%.2f", pt[0], pt[1])); } session.data.put("obstacleCoordinates", sb.toString()); session.captureSuccessful = true; session.captureMessage = "已采集多边形障碍物,共 " + xyPoints.size() + " 个点"; } } private static List convertToLocalXY(List coords, String baseStation) { List result = new ArrayList<>(); if (!isMeaningfulValue(baseStation)) { return result; } String[] parts = baseStation.split(","); if (parts.length < 4) { return result; } double baseLat = parseDMToDecimal(parts[0], parts[1]); double baseLon = parseDMToDecimal(parts[2], parts[3]); if (!Double.isFinite(baseLat) || !Double.isFinite(baseLon)) { return result; } for (Coordinate coord : coords) { double lat = parseDMToDecimal(coord.getLatitude(), coord.getLatDirection()); double lon = parseDMToDecimal(coord.getLongitude(), coord.getLonDirection()); if (!Double.isFinite(lat) || !Double.isFinite(lon)) { continue; } result.add(convertLatLonToLocal(lat, lon, baseLat, baseLon)); } return result; } private static String buildOriginalCoordinateString(List 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; } try { String trimmed = dmm.trim(); int dotIndex = trimmed.indexOf('.'); if (dotIndex < 2) { return Double.NaN; } int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2)); double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2)); double decimal = degrees + minutes / 60.0; if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) { decimal = -decimal; } return decimal; } catch (NumberFormatException ex) { return Double.NaN; } } private static double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) { double deltaLat = lat - baseLat; double deltaLon = lon - baseLon; double meanLatRad = Math.toRadians((baseLat + lat) / 2.0); double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad); double northMeters = deltaLat * METERS_PER_DEGREE_LAT; return new double[]{eastMeters, northMeters}; } private static CircleFitResult fitCircleFromPoints(List 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 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 resolveExistingObstacles(Dikuai target, List providedNames) { String landNumber = target != null ? target.getLandNumber() : null; Map configMap = loadObstacleDetailsFromConfig(landNumber); List result = new ArrayList<>(); List sanitized = sanitizeObstacleNames(providedNames); if (!sanitized.isEmpty()) { for (String name : sanitized) { ExistingObstacle info = configMap.remove(name); if (info != null) { result.add(info); } else { result.add(ExistingObstacle.placeholder(name)); } } } if (!configMap.isEmpty()) { List remaining = new ArrayList<>(configMap.values()); remaining.sort(Comparator.comparing(ExistingObstacle::getName, String.CASE_INSENSITIVE_ORDER)); result.addAll(remaining); } if (result.isEmpty()) { return Collections.emptyList(); } return result; } private List sanitizeObstacleNames(List names) { List sanitized = new ArrayList<>(); if (names == null) { return sanitized; } for (String name : names) { if (isMeaningfulValue(name)) { sanitized.add(name.trim()); } } return sanitized; } private Map loadObstacleDetailsFromConfig(String landNumber) { Map details = new HashMap<>(); if (!isMeaningfulValue(landNumber)) { return details; } try { File configFile = new File("Obstacledge.properties"); if (!configFile.exists()) { return details; } Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); if (!manager.loadFromFile(configFile.getAbsolutePath())) { return details; } Obstacledge.Plot plot = manager.getPlotById(landNumber.trim()); if (plot == null) { return details; } for (Obstacledge.Obstacle obstacle : plot.getObstacles()) { if (obstacle == null) { continue; } String name = obstacle.getObstacleName(); if (!isMeaningfulValue(name)) { continue; } String trimmedName = name.trim(); Obstacledge.ObstacleShape shape = obstacle.getShape(); 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()); } return details; } private void preloadData() { 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() { if (drawingStatusLabel == null) { return; } String coords = formData.get("obstacleCoordinates"); String shapeKey = formData.get("obstacleShape"); boolean isCircle = "circle".equals(shapeKey); if (isMeaningfulValue(coords)) { 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) { if (!isMeaningfulValue(coords)) { return 0; } String[] parts = coords.split(";+"); int count = 0; for (String part : parts) { if (part == null) { continue; } String trimmed = part.trim(); if (trimmed.isEmpty()) { continue; } if (trimmed.contains(",")) { count++; } } 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) { 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; } return true; } private void saveObstacleAndClose() { if (!validateStep2()) { return; } String landNumber = targetDikuai.getLandNumber(); 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); 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 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 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 coordinateList = parseOriginalCoordinatesToCoordinateList(originalCoords); if (coordinateList.isEmpty()) { JOptionPane.showMessageDialog(this, "原始坐标数据无效", "错误", JOptionPane.ERROR_MESSAGE); return; } // 保存当前的Coordinate.coordinates List 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 parseOriginalCoordinatesToCoordinateList(String originalCoords) { List 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()); obstacle.setOriginalCoordinates(new ArrayList()); obstacle.setXyCoordsString(normalizedXy); if (isMeaningfulValue(originalCoords)) { try { obstacle.setOriginalCoordsString(originalCoords); } catch (Exception parseEx) { System.err.println("解析障碍物原始坐标失败,将使用占位值: " + parseEx.getMessage()); obstacle.setOriginalCoordinates(new ArrayList()); } } 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 originals = obstacle.getOriginalCoordinates(); if (originals != null && !originals.isEmpty()) { return; } List xyCoords = obstacle.getXyCoordinates(); int pointCount = (xyCoords != null && !xyCoords.isEmpty()) ? xyCoords.size() : 1; List 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 xyCoordinates; private final String originalCoordinates; ExistingObstacle(String name, Obstacledge.ObstacleShape shape, String xyCoordinates, String originalCoordinates) { this.name = name != null ? name : ""; this.shape = shape; this.xyCoordinates = safeCoordString(xyCoordinates); this.originalCoordinates = safeCoordString(originalCoordinates); } static ExistingObstacle placeholder(String name) { return new ExistingObstacle(name, null, "", ""); } String getName() { return name; } String getShapeDisplay() { if (shape == null) { return "未知形状"; } return shape.getDescription(); } String getShapeKey() { if (shape == Obstacledge.ObstacleShape.CIRCLE) { return "circle"; } if (shape == Obstacledge.ObstacleShape.POLYGON) { return "polygon"; } return null; } String getCoordinates() { 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); } } private void showStep(int step) { currentStep = step; cardLayout.show(stepsPanel, "step" + step); prevButton.setVisible(step > 1); 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(); repaint(); } private void applySessionData(ObstacleDrawingSession session) { drawingInProgress = false; if (drawButton != null) { drawButton.setEnabled(true); } 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); if (panel != null) { selectMethodOption(panel, method, false); } } String shape = session.data.get("obstacleShape"); if (shape != null) { JPanel panel = shapeOptionPanels.get(shape); if (panel != null) { selectShapeOption(panel, shape, false); } } if (session.captureMessage != null && !session.captureSuccessful) { JOptionPane.showMessageDialog(this, session.captureMessage, session.captureSuccessful ? "成功" : "提示", session.captureSuccessful ? JOptionPane.INFORMATION_MESSAGE : JOptionPane.WARNING_MESSAGE); } 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); } private JPanel createInfoRow(String label, String value) { 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)); row.setOpaque(false); row.setAlignmentX(Component.LEFT_ALIGNMENT); row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); JLabel labelComp = new JLabel(label); labelComp.setFont(new Font("微软雅黑", Font.BOLD, 14)); labelComp.setForeground(TEXT_COLOR); labelComp.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel valueComp = new JLabel(value != null ? value : ""); valueComp.setFont(new Font("微软雅黑", Font.PLAIN, 14)); valueComp.setForeground(TEXT_COLOR); valueComp.setVerticalAlignment(SwingConstants.TOP); valueComp.setAlignmentX(Component.LEFT_ALIGNMENT); if (tooltip != null && !tooltip.trim().isEmpty()) { valueComp.setToolTipText(tooltip); } row.add(labelComp); row.add(Box.createRigidArea(new Dimension(6, 0))); row.add(valueComp); return row; } private JButton createPrimaryButton(String text, int fontSize) { JButton button = buttonset.createStyledButton(text, PRIMARY_COLOR); button.setFont(new Font("微软雅黑", Font.BOLD, fontSize)); button.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(PRIMARY_DARK, 2), BorderFactory.createEmptyBorder(10, 22, 10, 22))); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); button.setOpaque(true); button.setContentAreaFilled(true); button.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { if (button.isEnabled()) { button.setBackground(PRIMARY_DARK); } } @Override public void mouseExited(MouseEvent e) { if (button.isEnabled()) { button.setBackground(PRIMARY_COLOR); } else { // 禁用时保持灰色背景 button.setBackground(MEDIUM_GRAY); } } }); return button; } private JButton createSecondaryButton(String text) { JButton button = buttonset.createStyledButton(text, MEDIUM_GRAY); button.setFont(new Font("微软雅黑", Font.BOLD, 16)); 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.setOpaque(true); button.setContentAreaFilled(true); return button; } private static boolean isMeaningfulValue(String value) { if (value == null) { return false; } String trimmed = value.trim(); return !trimmed.isEmpty() && !"-1".equals(trimmed); } private String resolveBaseStationCoordinates() { String coords = targetDikuai.getBaseStationCoordinates(); if (isMeaningfulValue(coords)) { return coords.trim(); } Device device = new Device(); device.initFromProperties(); coords = device.getBaseStationCoordinates(); if (isMeaningfulValue(coords)) { return coords.trim(); } BaseStation baseStation = new BaseStation(); baseStation.load(); coords = baseStation.getInstallationCoordinates(); if (isMeaningfulValue(coords)) { return coords.trim(); } 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; } return value.trim(); } private String currentTime() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(new Date()); } /** * 显示新增障碍物对话框。 */ public static void showDialog(Component parent, Dikuai target) { showDialog(parent, target, null); } public static void showDialog(Component parent, Dikuai target, List obstacleNames) { Window owner = null; if (parent instanceof Window) { owner = (Window) parent; } else if (parent != null) { owner = SwingUtilities.getWindowAncestor(parent); } addzhangaiwu dialog = new addzhangaiwu(owner, target, obstacleNames); if (resumeRequested && activeSession != null && activeSession.target == target) { dialog.applySessionData(activeSession); resumeRequested = false; } dialog.setVisible(true); } }