package zhangaiwu; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Comparator; import java.awt.geom.Point2D; import baseStation.BaseStation; import bianjie.jisuanmianjie; import dikuai.Dikuai; import dikuai.Dikuaiguanli; import bianjie.bianjieguihua2; import lujing.Lunjingguihua; import set.Setsys; import ui.UIConfig; import zhuye.MowerLocationData; import zhuye.Shouye; import zhuye.Coordinate; import zhuye.buttonset; import gecaoji.Device; /** * 新增地块对话框 - 多步骤表单设计 * 适配6.5寸竖屏优化版本 - 左对齐版 */ public class AddDikuai extends JDialog { private static final long serialVersionUID = 1L; // 主题颜色 - 优化配色方案 private final Color PRIMARY_COLOR = new Color(46, 139, 87); private final Color PRIMARY_LIGHT = new Color(232, 245, 233); private final Color PRIMARY_DARK = new Color(30, 107, 69); private final Color WHITE = Color.WHITE; private final Color LIGHT_GRAY = new Color(248, 249, 250); 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 BORDER_COLOR = new Color(222, 226, 230); private final Color SUCCESS_COLOR = new Color(40, 167, 69); private final Color ERROR_COLOR = new Color(220, 53, 69); private static final String KEY_PATH_MESSAGE_TEXT = "__pathMessageText"; private static final String KEY_PATH_MESSAGE_SUCCESS = "__pathMessageSuccess"; // 步骤面板 private JPanel mainPanel; private CardLayout cardLayout; private JPanel stepsPanel; // 步骤组件 private JTextField landNumberField; private JTextField areaNameField; private JComboBox mowingPatternCombo; private JTextField mowingWidthField; // 割草机割刀宽度 private JTextField overlapDistanceField; // 相邻行重叠距离 private JTextField mowingSafetyDistanceField; // 割草安全距离 private JLabel calculatedMowingWidthLabel; // 计算后的割草宽度显示 private JPanel previewPanel; private Map drawingOptionPanels = new HashMap<>(); // 步骤控制 private int currentStep = 1; private JButton prevButton; private JButton nextButton; private JButton createButton; private JButton previewButton; private Component previewButtonSpacer; private JLabel boundaryCountLabel; private JPanel obstacleListContainer; private JTextArea pathGenerationMessageArea; private JPanel pathMessageWrapper; // 地块数据 private Map dikuaiData = new HashMap<>(); private String createdLandNumber; private String pendingLandNumber; // 步骤2选项状态 private JPanel selectedOptionPanel = null; private JButton startEndDrawingBtn; private boolean isDrawing = false; private static DrawingSession activeSession; private static boolean resumeRequested; private static final List TEMP_HANDHELD_POINTS = new ArrayList<>(); private static final class BoundarySnapshotResult { final boolean success; final String errorMessage; final String originalBoundary; final String optimizedBoundary; final String areaSqMeters; final String baseStationCoordinates; final int messageType; private BoundarySnapshotResult(boolean success, String errorMessage, String originalBoundary, String optimizedBoundary, String areaSqMeters, String baseStationCoordinates, int messageType) { this.success = success; this.errorMessage = errorMessage; this.originalBoundary = originalBoundary; this.optimizedBoundary = optimizedBoundary; this.areaSqMeters = areaSqMeters; this.baseStationCoordinates = baseStationCoordinates; this.messageType = messageType; } static BoundarySnapshotResult success(String original, String optimized, String area, String baseStation) { return new BoundarySnapshotResult(true, null, original, optimized, area, baseStation, JOptionPane.INFORMATION_MESSAGE); } static BoundarySnapshotResult failure(String message, int messageType) { return new BoundarySnapshotResult(false, message, null, null, null, null, messageType); } } private static class DrawingSession { String landNumber; String areaName; Map data = new HashMap<>(); boolean drawingCompleted; } public static void recordTemporaryBoundaryPoints(List points) { synchronized (TEMP_HANDHELD_POINTS) { TEMP_HANDHELD_POINTS.clear(); if (points == null) { return; } for (Point2D.Double point : points) { if (point == null) { continue; } TEMP_HANDHELD_POINTS.add(new Point2D.Double(point.x, point.y)); } } } public static List getTemporaryBoundaryPointsSnapshot() { synchronized (TEMP_HANDHELD_POINTS) { return new ArrayList<>(TEMP_HANDHELD_POINTS); } } public AddDikuai(JFrame parent) { super(parent, "新增地块", true); Dikuai.initFromProperties(); initializeUI(); setupEventHandlers(); } public AddDikuai(JDialog parent) { super(parent, "新增地块", true); Dikuai.initFromProperties(); initializeUI(); setupEventHandlers(); } private void initializeUI() { setLayout(new BorderLayout()); setBackground(WHITE); // 统一使用 6.5 寸竖屏适配尺寸 setSize(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT); setLocationRelativeTo(getParent()); setResizable(false); createMainPanel(); add(mainPanel, BorderLayout.CENTER); } private void createMainPanel() { mainPanel = new JPanel(new BorderLayout()); mainPanel.setBackground(WHITE); mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); // 步骤内容面板 cardLayout = new CardLayout(); stepsPanel = new JPanel(cardLayout); stepsPanel.setBackground(WHITE); stepsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); stepsPanel.add(createStep1Panel(), "step1"); stepsPanel.add(createStep2Panel(), "step2"); stepsPanel.add(createStep3Panel(), "step3"); mainPanel.add(stepsPanel, BorderLayout.CENTER); // 按钮面板 mainPanel.add(createButtonPanel(), BorderLayout.SOUTH); // 显示第一步 showStep(1); } private JPanel createStep1Panel() { JPanel stepPanel = new JPanel(); stepPanel.setLayout(new BoxLayout(stepPanel, BoxLayout.Y_AXIS)); stepPanel.setBackground(WHITE); stepPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); // 步骤标题 - 左对齐 JLabel stepTitle = new JLabel("步骤1:基本信息"); stepTitle.setFont(new Font("微软雅黑", Font.BOLD, 20)); stepTitle.setForeground(TEXT_COLOR); stepTitle.setAlignmentX(Component.LEFT_ALIGNMENT); stepPanel.add(stepTitle); stepPanel.add(Box.createRigidArea(new Dimension(0, 20))); // 表单组 JPanel formGroup = new JPanel(); formGroup.setLayout(new BoxLayout(formGroup, BoxLayout.Y_AXIS)); formGroup.setBackground(WHITE); formGroup.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel numberLabel = new JLabel("地块编号"); numberLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); numberLabel.setForeground(TEXT_COLOR); numberLabel.setAlignmentX(Component.LEFT_ALIGNMENT); landNumberField = new JTextField(); landNumberField.setFont(new Font("微软雅黑", Font.PLAIN, 16)); landNumberField.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50)); landNumberField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(12, 15, 12, 15) )); landNumberField.setEditable(false); landNumberField.setFocusable(false); landNumberField.setBackground(LIGHT_GRAY); landNumberField.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel nameLabel = new JLabel("地块名称"); nameLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); nameLabel.setForeground(TEXT_COLOR); nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); areaNameField = new JTextField(); areaNameField.setFont(new Font("微软雅黑", Font.PLAIN, 16)); areaNameField.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50)); areaNameField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(12, 15, 12, 15) )); areaNameField.setAlignmentX(Component.LEFT_ALIGNMENT); // 添加输入框焦点效果 areaNameField.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { areaNameField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(PRIMARY_COLOR, 2), BorderFactory.createEmptyBorder(12, 15, 12, 15) )); } @Override public void focusLost(FocusEvent e) { areaNameField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(12, 15, 12, 15) )); } }); JLabel hintLabel = new JLabel("例如: 前院草坪、后院草坪等"); hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); hintLabel.setForeground(LIGHT_TEXT); hintLabel.setAlignmentX(Component.LEFT_ALIGNMENT); formGroup.add(numberLabel); formGroup.add(Box.createRigidArea(new Dimension(0, 10))); formGroup.add(landNumberField); formGroup.add(Box.createRigidArea(new Dimension(0, 18))); formGroup.add(nameLabel); formGroup.add(Box.createRigidArea(new Dimension(0, 10))); formGroup.add(areaNameField); formGroup.add(Box.createRigidArea(new Dimension(0, 8))); formGroup.add(hintLabel); stepPanel.add(formGroup); stepPanel.add(Box.createVerticalGlue()); landNumberField.setText(getPendingLandNumber()); return stepPanel; } private JPanel createObstacleSummarySection() { JPanel section = new JPanel(); section.setLayout(new BoxLayout(section, BoxLayout.Y_AXIS)); section.setOpaque(false); section.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel title = new JLabel("已有障碍物"); title.setFont(new Font("微软雅黑", Font.BOLD, 16)); title.setForeground(TEXT_COLOR); title.setAlignmentX(Component.LEFT_ALIGNMENT); section.add(title); section.add(Box.createRigidArea(new Dimension(0, 8))); obstacleListContainer = new JPanel(); obstacleListContainer.setLayout(new BoxLayout(obstacleListContainer, BoxLayout.Y_AXIS)); obstacleListContainer.setOpaque(false); obstacleListContainer.setAlignmentX(Component.LEFT_ALIGNMENT); section.add(obstacleListContainer); updateObstacleSummary(); return section; } private void updateObstacleSummary() { if (obstacleListContainer == null) { return; } obstacleListContainer.removeAll(); List summaries = loadExistingObstacles(); if (summaries.isEmpty()) { JLabel emptyLabel = new JLabel("暂无障碍物"); emptyLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); emptyLabel.setForeground(LIGHT_TEXT); emptyLabel.setAlignmentX(Component.LEFT_ALIGNMENT); obstacleListContainer.add(emptyLabel); } else { int index = 1; for (ObstacleSummary summary : summaries) { JPanel row = new JPanel(new BorderLayout()); row.setOpaque(false); row.setAlignmentX(Component.LEFT_ALIGNMENT); row.setBorder(BorderFactory.createEmptyBorder(6, 0, 6, 0)); String labelText = String.format(Locale.CHINA, "%02d. %s", index++, summary.getName()); JLabel nameLabel = new JLabel(labelText); nameLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); nameLabel.setForeground(TEXT_COLOR); JLabel coordLabel = new JLabel(summary.getDisplayCoords()); coordLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); coordLabel.setForeground(LIGHT_TEXT); coordLabel.setToolTipText(summary.getFullCoords()); row.add(nameLabel, BorderLayout.WEST); row.add(coordLabel, BorderLayout.CENTER); obstacleListContainer.add(row); } } obstacleListContainer.revalidate(); obstacleListContainer.repaint(); } private List loadExistingObstacles() { List summaries = new ArrayList<>(); List obstacles = fetchExistingObstacleDetails(); if (obstacles.isEmpty()) { return summaries; } for (ExistingObstacle obstacle : obstacles) { if (obstacle == null) { continue; } String name = obstacle.getName(); if (!isMeaningfulValue(name)) { continue; } String coords = obstacle.getDisplayCoordinates(); String fullCoords = isMeaningfulValue(coords) ? coords.trim() : ""; String preview = buildCoordinatePreview(fullCoords, 20); summaries.add(new ObstacleSummary(name.trim(), fullCoords, preview)); } return summaries; } private List fetchExistingObstacleDetails() { File configFile = new File("Obstacledge.properties"); if (!configFile.exists()) { return Collections.emptyList(); } List result = new ArrayList<>(); try { Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); if (!manager.loadFromFile(configFile.getAbsolutePath())) { return Collections.emptyList(); } for (Obstacledge.Plot plot : manager.getPlots()) { if (plot == null) { continue; } String landNumber = isMeaningfulValue(plot.getPlotId()) ? plot.getPlotId().trim() : ""; List plotObstacles = plot.getObstacles(); if (plotObstacles == null || plotObstacles.isEmpty()) { continue; } for (Obstacledge.Obstacle obstacle : plotObstacles) { if (obstacle == null) { continue; } String name = obstacle.getObstacleName(); if (!isMeaningfulValue(name)) { continue; } String coords = extractObstacleCoordinates(obstacle); result.add(new ExistingObstacle(landNumber, name.trim(), coords)); } } } catch (Exception ex) { System.err.println("加载已有障碍物失败: " + ex.getMessage()); return Collections.emptyList(); } if (result.isEmpty()) { return Collections.emptyList(); } result.sort(Comparator.comparing(ExistingObstacle::getName, String.CASE_INSENSITIVE_ORDER)); return result; } private String extractObstacleCoordinates(Obstacledge.Obstacle obstacle) { if (obstacle == null) { return ""; } String xy = obstacle.getXyCoordsString(); if (isMeaningfulValue(xy)) { return xy.trim(); } String original = obstacle.getOriginalCoordsString(); if (isMeaningfulValue(original)) { return original.trim(); } return ""; } private String buildCoordinatePreview(String coords, int keepLength) { if (!isMeaningfulValue(coords)) { return "无坐标"; } String sanitized = sanitizeCoordinateString(coords); if (sanitized.length() <= keepLength) { return sanitized; } if (keepLength <= 0) { return "..."; } return sanitized.substring(0, keepLength) + "..."; } private List splitObstacleEntries(String data) { List entries = new ArrayList<>(); if (data.indexOf('|') >= 0) { String[] parts = data.split("\\|"); for (String part : parts) { if (part != null && !part.trim().isEmpty()) { entries.add(part.trim()); } } } else if (data.contains("\n")) { String[] lines = data.split("\r?\n"); for (String line : lines) { if (line != null && !line.trim().isEmpty()) { entries.add(line.trim()); } } } else { entries.add(data); } return entries; } private String stripInlineComment(String text) { if (text == null) { return ""; } int hashIndex = text.indexOf('#'); if (hashIndex >= 0) { return text.substring(0, hashIndex).trim(); } return text.trim(); } private boolean looksLikeShapeToken(String token) { if (token == null) { return false; } String normalized = token.trim().toLowerCase(Locale.ROOT); return "circle".equals(normalized) || "polygon".equals(normalized) || "圆形".equals(normalized) || "多边形".equals(normalized) || "0".equals(normalized) || "1".equals(normalized); } private String sanitizeCoordinateString(String value) { if (value == null) { return ""; } String stripped = stripInlineComment(value); if (stripped.isEmpty()) { return ""; } return stripped.replace("\r", "").replace("\n", "").replace("\t", "").replace(" ", ""); } private String truncateCoordinateForDisplay(String coords, int maxLength) { if (coords == null) { return ""; } String trimmed = coords.trim(); if (trimmed.length() <= maxLength) { return trimmed; } if (maxLength <= 3) { return trimmed.substring(0, maxLength); } return trimmed.substring(0, maxLength - 3) + "..."; } private static final class ObstacleSummary { private final String name; private final String fullCoords; private final String displayCoords; ObstacleSummary(String name, String fullCoords, String displayCoords) { this.name = name; this.fullCoords = fullCoords; this.displayCoords = displayCoords; } public String getName() { return name; } public String getFullCoords() { return fullCoords; } public String getDisplayCoords() { return displayCoords; } } private static final class ExistingObstacle { private final String landNumber; private final String name; private final String coordinates; ExistingObstacle(String landNumber, String name, String coordinates) { this.landNumber = landNumber != null ? landNumber : ""; this.name = name != null ? name : ""; this.coordinates = coordinates != null ? coordinates : ""; } String getLandNumber() { return landNumber; } String getName() { return name; } String getDisplayCoordinates() { return coordinates; } } private JPanel createStep2Panel() { JPanel stepPanel = new JPanel(); stepPanel.setLayout(new BoxLayout(stepPanel, BoxLayout.Y_AXIS)); stepPanel.setBackground(WHITE); stepPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); // 步骤标题 - 左对齐 JLabel stepTitle = new JLabel("步骤2:绘制边界"); stepTitle.setFont(new Font("微软雅黑", Font.BOLD, 20)); stepTitle.setForeground(TEXT_COLOR); stepTitle.setAlignmentX(Component.LEFT_ALIGNMENT); stepPanel.add(stepTitle); stepPanel.add(Box.createRigidArea(new Dimension(0, 20))); // 步骤说明 JPanel instructionPanel = createInstructionPanel( "请选择绘制边界的方式。割草机绘制需要驾驶割草机沿边界行驶," + "手持设备绘制则使用便携设备标记边界点。" ); instructionPanel.setAlignmentX(Component.LEFT_ALIGNMENT); stepPanel.add(instructionPanel); stepPanel.add(Box.createRigidArea(new Dimension(0, 25))); // 绘制方式选择 JLabel methodLabel = new JLabel("选择绘制方式"); methodLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); methodLabel.setForeground(TEXT_COLOR); methodLabel.setAlignmentX(Component.LEFT_ALIGNMENT); stepPanel.add(methodLabel); stepPanel.add(Box.createRigidArea(new Dimension(0, 15))); // 绘制选项面板 - 垂直布局以完整显示图标 JPanel optionsPanel = new JPanel(); optionsPanel.setLayout(new BoxLayout(optionsPanel, BoxLayout.Y_AXIS)); optionsPanel.setBackground(WHITE); optionsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); // 割草机绘制选项 JPanel mowerOption = createDrawingOption("🚜", "割草机绘制", "", "mower"); mowerOption.setAlignmentX(Component.LEFT_ALIGNMENT); mowerOption.setBorder(BorderFactory.createLineBorder(BORDER_COLOR, 2)); // 手持设备绘制选项 JPanel handheldOption = createDrawingOption("📱", "手持设备绘制", "", "handheld"); handheldOption.setAlignmentX(Component.LEFT_ALIGNMENT); handheldOption.setBorder(BorderFactory.createLineBorder(BORDER_COLOR, 2)); optionsPanel.add(mowerOption); optionsPanel.add(Box.createRigidArea(new Dimension(0, 15))); optionsPanel.add(handheldOption); stepPanel.add(optionsPanel); stepPanel.add(Box.createRigidArea(new Dimension(0, 30))); // 开始/结束绘制按钮 startEndDrawingBtn = createPrimaryButton("开始绘制", 16); startEndDrawingBtn.setAlignmentX(Component.LEFT_ALIGNMENT); startEndDrawingBtn.setMaximumSize(new Dimension(400, 55)); startEndDrawingBtn.setEnabled(false); // 初始不可用 startEndDrawingBtn.addActionListener(e -> toggleDrawing()); stepPanel.add(startEndDrawingBtn); boundaryCountLabel = new JLabel("已采集到边界点0个"); boundaryCountLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); boundaryCountLabel.setForeground(LIGHT_TEXT); boundaryCountLabel.setAlignmentX(Component.LEFT_ALIGNMENT); boundaryCountLabel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0)); boundaryCountLabel.setVisible(false); stepPanel.add(boundaryCountLabel); stepPanel.add(Box.createVerticalGlue()); return stepPanel; } private JPanel createDrawingOption(String icon, String text, String description, String type) { JPanel optionPanel = new JPanel(new BorderLayout(15, 0)); optionPanel.setBackground(WHITE); optionPanel.setCursor(new Cursor(Cursor.HAND_CURSOR)); optionPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); optionPanel.setMaximumSize(new Dimension(500, 120)); // 图标区域 JLabel iconLabel; if ("handheld".equals(type) || "mower".equals(type)) { iconLabel = new JLabel(); iconLabel.setHorizontalAlignment(SwingConstants.CENTER); iconLabel.setPreferredSize(new Dimension(80, 80)); String iconPath = "handheld".equals(type) ? "image/URT.png" : "image/mow.png"; 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)); } else { iconLabel.setText(icon); iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 48)); } } else { iconLabel = new JLabel(icon, JLabel.CENTER); iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 48)); iconLabel.setPreferredSize(new Dimension(80, 80)); } // 文本区域 JPanel textPanel = new JPanel(new GridBagLayout()); textPanel.setBackground(WHITE); JLabel textLabel = new JLabel(text); textLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); textLabel.setHorizontalAlignment(SwingConstants.CENTER); textLabel.setVerticalAlignment(SwingConstants.CENTER); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1; gbc.weighty = description == null || description.trim().isEmpty() ? 1 : 0; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.CENTER; textPanel.add(textLabel, gbc); if (description != null && !description.trim().isEmpty()) { JLabel descLabel = new JLabel(description); descLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); descLabel.setForeground(LIGHT_TEXT); descLabel.setHorizontalAlignment(SwingConstants.CENTER); GridBagConstraints descGbc = new GridBagConstraints(); descGbc.gridx = 0; descGbc.gridy = 1; descGbc.weightx = 1; descGbc.weighty = 1; descGbc.fill = GridBagConstraints.HORIZONTAL; descGbc.anchor = GridBagConstraints.CENTER; textPanel.add(descLabel, descGbc); } optionPanel.putClientProperty("titleLabel", textLabel); optionPanel.add(iconLabel, BorderLayout.WEST); optionPanel.add(textPanel, BorderLayout.CENTER); // 添加点击事件 optionPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (!optionPanel.isEnabled()) { return; } if (selectDrawingOption(optionPanel, type, true)) { startEndDrawingBtn.setEnabled(true); // 选择后启用按钮 } } @Override public void mouseEntered(MouseEvent e) { if (optionPanel != selectedOptionPanel) { optionPanel.setBackground(new Color(245, 245, 245)); } } @Override public void mouseExited(MouseEvent e) { if (optionPanel != selectedOptionPanel) { optionPanel.setBackground(WHITE); } } }); drawingOptionPanels.put(type, optionPanel); return optionPanel; } private boolean selectDrawingOption(JPanel optionPanel, String type, boolean userTriggered) { if (optionPanel == null) { return false; } if (userTriggered && "handheld".equalsIgnoreCase(type) && !hasConfiguredHandheldMarker()) { JOptionPane.showMessageDialog(this, "请先去系统设置添加便携打点器编号", "提示", JOptionPane.WARNING_MESSAGE); return false; } // 重置之前选中的选项 if (selectedOptionPanel != null) { selectedOptionPanel.setBorder(BorderFactory.createLineBorder(BORDER_COLOR, 2)); selectedOptionPanel.setBackground(WHITE); Object oldTitle = selectedOptionPanel.getClientProperty("titleLabel"); if (oldTitle instanceof JLabel) { ((JLabel) oldTitle).setForeground(TEXT_COLOR); } } // 设置新的选中状态 optionPanel.setBorder(BorderFactory.createLineBorder(PRIMARY_COLOR, 3)); optionPanel.setBackground(PRIMARY_LIGHT); Object titleObj = optionPanel.getClientProperty("titleLabel"); if (titleObj instanceof JLabel) { ((JLabel) titleObj).setForeground(PRIMARY_COLOR); } selectedOptionPanel = optionPanel; // 保存选择 dikuaiData.put("drawingMethod", type); return true; } private boolean hasConfiguredHandheldMarker() { String handheldId = Setsys.getPropertyValue("handheldMarkerId"); return handheldId != null && !handheldId.trim().isEmpty(); } private void toggleDrawing() { if (!isDrawing) { if (!prepareDrawingSession()) { return; } isDrawing = true; hideBoundaryPointSummary(); Coordinate.setStartSaveGngga(true); startEndDrawingBtn.setText("结束绘制"); startEndDrawingBtn.setBackground(new Color(220, 53, 69)); updateOtherOptionsState(true); if (!startDrawingBoundary()) { Coordinate.setStartSaveGngga(false); resetDrawingState(); } } else { // 用户在对话框内主动结束(未触发外部流程) isDrawing = false; Coordinate.setStartSaveGngga(false); startEndDrawingBtn.setText("开始绘制"); startEndDrawingBtn.setBackground(PRIMARY_COLOR); updateOtherOptionsState(false); dikuaiData.put("boundaryDrawn", "true"); JOptionPane.showMessageDialog(this, "边界绘制已完成", "提示", JOptionPane.INFORMATION_MESSAGE); showBoundaryPointSummary(); } } private boolean prepareDrawingSession() { String areaName = areaNameField.getText().trim(); if (areaName.isEmpty()) { JOptionPane.showMessageDialog(this, "请先填写地块名称", "提示", JOptionPane.WARNING_MESSAGE); areaNameField.requestFocus(); return false; } if (!dikuaiData.containsKey("drawingMethod")) { JOptionPane.showMessageDialog(this, "请选择绘制方式", "提示", JOptionPane.WARNING_MESSAGE); return false; } dikuaiData.put("areaName", areaName); String landNumber = getPendingLandNumber(); if (activeSession == null) { activeSession = new DrawingSession(); } activeSession.landNumber = landNumber; activeSession.areaName = areaName; activeSession.drawingCompleted = false; activeSession.data = new HashMap<>(dikuaiData); return true; } private void updateOtherOptionsState(boolean disable) { if (selectedOptionPanel == null) { return; } Component[] components = selectedOptionPanel.getParent().getComponents(); for (Component comp : components) { if (comp instanceof JPanel && comp != selectedOptionPanel) { comp.setEnabled(!disable); ((JPanel) comp).setBackground(disable ? LIGHT_GRAY : WHITE); } } } private void resetDrawingState() { isDrawing = false; Coordinate.setStartSaveGngga(false); startEndDrawingBtn.setText("开始绘制"); startEndDrawingBtn.setBackground(PRIMARY_COLOR); updateOtherOptionsState(false); hideBoundaryPointSummary(); } private JPanel createStep3Panel() { JPanel stepPanel = new JPanel(); stepPanel.setLayout(new BoxLayout(stepPanel, BoxLayout.Y_AXIS)); stepPanel.setBackground(WHITE); stepPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); // 步骤标题 - 左对齐 JLabel stepTitle = new JLabel("步骤3:生成割草路径"); stepTitle.setFont(new Font("微软雅黑", Font.BOLD, 20)); stepTitle.setForeground(TEXT_COLOR); stepTitle.setAlignmentX(Component.LEFT_ALIGNMENT); stepPanel.add(stepTitle); stepPanel.add(Box.createRigidArea(new Dimension(0, 20))); // 设置面板容器 JPanel settingsPanel = new JPanel(); settingsPanel.setLayout(new BoxLayout(settingsPanel, BoxLayout.Y_AXIS)); settingsPanel.setBackground(WHITE); settingsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); // 割草模式选择 JPanel patternPanel = createFormGroupWithoutHint("割草模式"); mowingPatternCombo = new JComboBox<>(new String[]{"平行线", "螺旋式"}); mowingPatternCombo.setFont(new Font("微软雅黑", Font.PLAIN, 16)); mowingPatternCombo.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); mowingPatternCombo.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); mowingPatternCombo.setSelectedIndex(0); mowingPatternCombo.setAlignmentX(Component.LEFT_ALIGNMENT); // 添加下拉框焦点效果 mowingPatternCombo.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { mowingPatternCombo.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(PRIMARY_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); } @Override public void focusLost(FocusEvent e) { mowingPatternCombo.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); } }); patternPanel.add(mowingPatternCombo); settingsPanel.add(patternPanel); settingsPanel.add(Box.createRigidArea(new Dimension(0, 10))); // 割草机割刀宽度设置 JPanel widthPanel = createFormGroupWithoutHint("割草机割刀宽度"); JPanel widthInputPanel = new JPanel(new BorderLayout()); widthInputPanel.setBackground(WHITE); widthInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); widthInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT); mowingWidthField = new JTextField("0.40"); mowingWidthField.setFont(new Font("微软雅黑", Font.PLAIN, 16)); mowingWidthField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); // 添加文本框焦点效果和变化监听 mowingWidthField.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { mowingWidthField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(PRIMARY_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); } @Override public void focusLost(FocusEvent e) { mowingWidthField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); updateCalculatedMowingWidth(); } }); JLabel unitLabel = new JLabel("米"); unitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); unitLabel.setForeground(LIGHT_TEXT); unitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0)); widthInputPanel.add(mowingWidthField, BorderLayout.CENTER); widthInputPanel.add(unitLabel, BorderLayout.EAST); widthPanel.add(widthInputPanel); settingsPanel.add(widthPanel); settingsPanel.add(Box.createRigidArea(new Dimension(0, 10))); // 相邻行重叠距离设置 JPanel overlapPanel = createFormGroupWithoutHint("相邻行重叠距离"); JPanel overlapInputPanel = new JPanel(new BorderLayout()); overlapInputPanel.setBackground(WHITE); overlapInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); overlapInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT); overlapDistanceField = new JTextField("0.06"); overlapDistanceField.setFont(new Font("微软雅黑", Font.PLAIN, 16)); overlapDistanceField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); // 添加文本框焦点效果和变化监听 overlapDistanceField.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { overlapDistanceField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(PRIMARY_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); } @Override public void focusLost(FocusEvent e) { overlapDistanceField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); updateCalculatedMowingWidth(); } }); JLabel overlapUnitLabel = new JLabel("米"); overlapUnitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); overlapUnitLabel.setForeground(LIGHT_TEXT); overlapUnitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0)); overlapInputPanel.add(overlapDistanceField, BorderLayout.CENTER); overlapInputPanel.add(overlapUnitLabel, BorderLayout.EAST); overlapPanel.add(overlapInputPanel); settingsPanel.add(overlapPanel); settingsPanel.add(Box.createRigidArea(new Dimension(0, 10))); // 割草宽度显示(自动计算,不可修改) JPanel calculatedWidthPanel = createFormGroup("割草宽度", "割草宽度 = 割草机割刀宽度 - 重叠距离(自动计算)"); JPanel calculatedWidthDisplayPanel = new JPanel(new BorderLayout()); calculatedWidthDisplayPanel.setBackground(LIGHT_GRAY); calculatedWidthDisplayPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); calculatedWidthDisplayPanel.setAlignmentX(Component.LEFT_ALIGNMENT); calculatedWidthDisplayPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); calculatedMowingWidthLabel = new JLabel("0.00 米"); calculatedMowingWidthLabel.setFont(new Font("微软雅黑", Font.PLAIN, 16)); calculatedMowingWidthLabel.setForeground(TEXT_COLOR); calculatedWidthDisplayPanel.add(calculatedMowingWidthLabel, BorderLayout.CENTER); calculatedWidthPanel.add(calculatedWidthDisplayPanel); settingsPanel.add(calculatedWidthPanel); settingsPanel.add(Box.createRigidArea(new Dimension(0, 10))); // 割草安全距离 JPanel safetyDistancePanel = createFormGroupWithoutHint("割草安全距离"); JPanel safetyDistanceInputPanel = new JPanel(new BorderLayout()); safetyDistanceInputPanel.setBackground(WHITE); safetyDistanceInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); safetyDistanceInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT); // 获取默认值 String defaultSafetyDistance = "0.5"; // 默认值 try { Device device = Device.getGecaoji(); if (device != null) { String safetyDistance = device.getMowingSafetyDistance(); if (safetyDistance != null && !safetyDistance.trim().isEmpty() && !"-1".equals(safetyDistance.trim())) { defaultSafetyDistance = safetyDistance.trim(); } } } catch (Exception e) { // 如果获取失败,使用默认值 } mowingSafetyDistanceField = new JTextField(defaultSafetyDistance); mowingSafetyDistanceField.setFont(new Font("微软雅黑", Font.PLAIN, 16)); mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); // 添加文本框焦点效果 mowingSafetyDistanceField.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(PRIMARY_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); } @Override public void focusLost(FocusEvent e) { mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 12, 10, 12) )); } }); JLabel safetyDistanceUnitLabel = new JLabel("米"); safetyDistanceUnitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); safetyDistanceUnitLabel.setForeground(LIGHT_TEXT); safetyDistanceUnitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0)); safetyDistanceInputPanel.add(mowingSafetyDistanceField, BorderLayout.CENTER); safetyDistanceInputPanel.add(safetyDistanceUnitLabel, BorderLayout.EAST); safetyDistancePanel.add(safetyDistanceInputPanel); settingsPanel.add(safetyDistancePanel); settingsPanel.add(Box.createRigidArea(new Dimension(0, 25))); stepPanel.add(settingsPanel); // 初始化计算后的割草宽度 updateCalculatedMowingWidth(); JButton generatePathButton = createPrimaryButton("生成割草路径", 16); generatePathButton.setAlignmentX(Component.LEFT_ALIGNMENT); generatePathButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 55)); generatePathButton.addActionListener(e -> generateMowingPath()); stepPanel.add(Box.createRigidArea(new Dimension(0, 20))); stepPanel.add(generatePathButton); stepPanel.add(Box.createRigidArea(new Dimension(0, 12))); pathMessageWrapper = new JPanel(new BorderLayout()); pathMessageWrapper.setAlignmentX(Component.LEFT_ALIGNMENT); pathMessageWrapper.setBackground(PRIMARY_LIGHT); pathMessageWrapper.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(PRIMARY_COLOR, 1), BorderFactory.createEmptyBorder(12, 12, 12, 12) )); pathMessageWrapper.setVisible(false); pathGenerationMessageArea = new JTextArea(); pathGenerationMessageArea.setFont(new Font("微软雅黑", Font.PLAIN, 14)); pathGenerationMessageArea.setForeground(TEXT_COLOR); pathGenerationMessageArea.setOpaque(false); pathGenerationMessageArea.setEditable(false); pathGenerationMessageArea.setLineWrap(true); pathGenerationMessageArea.setWrapStyleWord(true); pathGenerationMessageArea.setFocusable(false); pathGenerationMessageArea.setBorder(null); pathMessageWrapper.add(pathGenerationMessageArea, BorderLayout.CENTER); stepPanel.add(pathMessageWrapper); stepPanel.add(Box.createVerticalGlue()); return stepPanel; } /** * 更新计算后的割草宽度显示 */ private void updateCalculatedMowingWidth() { if (calculatedMowingWidthLabel == null || mowingWidthField == null || overlapDistanceField == null) { return; } try { String widthText = mowingWidthField.getText().trim(); String overlapText = overlapDistanceField.getText().trim(); if (widthText.isEmpty() || overlapText.isEmpty()) { calculatedMowingWidthLabel.setText("0.00 米"); return; } double bladeWidthMeters = Double.parseDouble(widthText); double overlapMeters = Double.parseDouble(overlapText); double calculatedWidthMeters = bladeWidthMeters - overlapMeters; if (calculatedWidthMeters <= 0) { calculatedMowingWidthLabel.setText("无效(割刀宽度需大于重叠距离)"); calculatedMowingWidthLabel.setForeground(ERROR_COLOR); } else { calculatedMowingWidthLabel.setText(String.format(Locale.US, "%.2f 米", calculatedWidthMeters)); calculatedMowingWidthLabel.setForeground(TEXT_COLOR); } } catch (NumberFormatException e) { calculatedMowingWidthLabel.setText("0.00 米"); } catch (Exception e) { calculatedMowingWidthLabel.setText("0.00 米"); } } private JPanel createInstructionPanel(String text) { JPanel instructionPanel = new JPanel(new BorderLayout()); instructionPanel.setBackground(PRIMARY_LIGHT); instructionPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createMatteBorder(0, 5, 0, 0, PRIMARY_COLOR), BorderFactory.createEmptyBorder(12, 12, 12, 12) )); instructionPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 70)); JLabel iconLabel = new JLabel("💡"); iconLabel.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 16)); iconLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10)); JTextArea instructionText = new JTextArea(text); instructionText.setFont(new Font("微软雅黑", Font.PLAIN, 13)); instructionText.setForeground(TEXT_COLOR); instructionText.setBackground(PRIMARY_LIGHT); instructionText.setLineWrap(true); instructionText.setWrapStyleWord(true); instructionText.setEditable(false); instructionPanel.add(iconLabel, BorderLayout.WEST); instructionPanel.add(instructionText, BorderLayout.CENTER); return instructionPanel; } private JPanel createFormGroup(String label, String hint) { JPanel formGroup = new JPanel(); formGroup.setLayout(new BoxLayout(formGroup, BoxLayout.Y_AXIS)); formGroup.setBackground(WHITE); formGroup.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel nameLabel = new JLabel(label); nameLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); nameLabel.setForeground(TEXT_COLOR); nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel hintLabel = new JLabel(hint); hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13)); hintLabel.setForeground(LIGHT_TEXT); hintLabel.setAlignmentX(Component.LEFT_ALIGNMENT); formGroup.add(nameLabel); formGroup.add(Box.createRigidArea(new Dimension(0, 6))); formGroup.add(hintLabel); formGroup.add(Box.createRigidArea(new Dimension(0, 8))); return formGroup; } private JPanel createFormGroupWithoutHint(String label) { JPanel formGroup = new JPanel(); formGroup.setLayout(new BoxLayout(formGroup, BoxLayout.Y_AXIS)); formGroup.setBackground(WHITE); formGroup.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel nameLabel = new JLabel(label); nameLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); nameLabel.setForeground(TEXT_COLOR); nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); formGroup.add(nameLabel); formGroup.add(Box.createRigidArea(new Dimension(0, 8))); return formGroup; } private void generateMowingPath() { if (!dikuaiData.containsKey("boundaryDrawn")) { JOptionPane.showMessageDialog(this, "请先完成边界绘制后再生成路径", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("请先完成边界绘制后再生成路径。", false); setPathAvailability(false); showStep(2); return; } Dikuai dikuai = getOrCreatePendingDikuai(); String boundaryCoords = null; if (dikuai != null) { boundaryCoords = normalizeCoordinateValue(dikuai.getBoundaryCoordinates()); } if (boundaryCoords == null) { boundaryCoords = normalizeCoordinateValue(dikuaiData.get("boundaryCoordinates")); } if (boundaryCoords == null) { JOptionPane.showMessageDialog(this, "未找到有效的地块边界坐标,无法生成路径", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("未找到有效的地块边界坐标,无法生成路径。", false); setPathAvailability(false); return; } String obstacleCoords = null; String landNumber = getPendingLandNumber(); if (isMeaningfulValue(landNumber)) { obstacleCoords = normalizeCoordinateValue(resolveObstaclePayloadFromConfig(landNumber)); } if (obstacleCoords == null) { obstacleCoords = normalizeCoordinateValue(dikuaiData.get("obstacleCoordinates")); } String patternDisplay = (String) mowingPatternCombo.getSelectedItem(); dikuaiData.put("mowingPattern", patternDisplay); String widthText = mowingWidthField.getText().trim(); if (widthText.isEmpty()) { JOptionPane.showMessageDialog(this, "割草机割刀宽度不能为空", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("割草机割刀宽度不能为空,请重新输入。", false); setPathAvailability(false); return; } double bladeWidthMeters; try { bladeWidthMeters = Double.parseDouble(widthText); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(this, "割草机割刀宽度输入无效", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("割草机割刀宽度输入无效,请重新输入。", false); setPathAvailability(false); return; } if (bladeWidthMeters <= 0) { JOptionPane.showMessageDialog(this, "割草机割刀宽度必须大于0", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("割草机割刀宽度必须大于0,请重新设置。", false); setPathAvailability(false); return; } // 保存割草机割刀宽度(米) dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters)); // 获取重叠距离 String overlapText = overlapDistanceField.getText().trim(); if (overlapText.isEmpty()) { JOptionPane.showMessageDialog(this, "相邻行重叠距离不能为空", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("相邻行重叠距离不能为空,请重新输入。", false); setPathAvailability(false); return; } double overlapMeters; try { overlapMeters = Double.parseDouble(overlapText); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(this, "相邻行重叠距离输入无效", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("相邻行重叠距离输入无效,请重新输入。", false); setPathAvailability(false); return; } if (overlapMeters < 0) { JOptionPane.showMessageDialog(this, "相邻行重叠距离不能为负数", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("相邻行重叠距离不能为负数,请重新设置。", false); setPathAvailability(false); return; } // 保存重叠距离到地块数据 dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters)); // 计算实际使用的割草宽度(割刀宽度 - 重叠距离) double calculatedWidthMeters = bladeWidthMeters - overlapMeters; if (calculatedWidthMeters <= 0) { JOptionPane.showMessageDialog(this, "计算后的割草宽度必须大于0(割刀宽度必须大于重叠距离)", "提示", JOptionPane.WARNING_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("计算后的割草宽度必须大于0,请调整割刀宽度或重叠距离。", false); setPathAvailability(false); return; } // 保存割草宽度(计算后的值,转换为厘米存储,保持与原有数据格式兼容) double calculatedWidthCm = calculatedWidthMeters * 100.0; dikuaiData.put("mowingWidth", String.format(Locale.US, "%.0f", calculatedWidthCm)); String widthMeters = String.format(Locale.US, "%.2f", calculatedWidthMeters); String plannerMode = resolvePlannerMode(patternDisplay); try { List segments = Lunjingguihua.generatePathSegments( boundaryCoords, obstacleCoords, widthMeters, plannerMode ); String plannedPath = Lunjingguihua.formatPathSegments(segments); if (!isMeaningfulValue(plannedPath)) { JOptionPane.showMessageDialog(this, "生成割草路径失败: 生成结果为空", "错误", JOptionPane.ERROR_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("生成割草路径失败:生成结果为空。", false); setPathAvailability(false); return; } if (isMeaningfulValue(boundaryCoords)) { dikuaiData.put("boundaryCoordinates", boundaryCoords); } if (isMeaningfulValue(obstacleCoords)) { dikuaiData.put("obstacleCoordinates", obstacleCoords); } dikuaiData.put("plannedPath", plannedPath); setPathAvailability(true); showPathGenerationMessage( "已根据当前设置生成割草路径,共生成 " + segments.size() + " 段。\n点击“预览”按钮可在主页面查看效果。", true); } catch (IllegalArgumentException ex) { JOptionPane.showMessageDialog(this, "生成割草路径失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("生成割草路径失败:" + ex.getMessage(), false); setPathAvailability(false); } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, "生成割草路径时发生异常: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); dikuaiData.remove("plannedPath"); showPathGenerationMessage("生成割草路径时发生异常:" + ex.getMessage(), false); setPathAvailability(false); } } private void previewMowingPath() { if (!hasGeneratedPath()) { showPathGenerationMessage("请先生成割草路径后再预览。", false); setPathAvailability(false); return; } persistStep3Inputs(); String landNumber = getPendingLandNumber(); String trimmedAreaName = areaNameField.getText() != null ? areaNameField.getText().trim() : ""; String displayAreaName = isMeaningfulValue(trimmedAreaName) ? trimmedAreaName : landNumber; String plannedPath = dikuaiData.get("plannedPath"); if (!isMeaningfulValue(plannedPath)) { showPathGenerationMessage("请先生成割草路径后再预览。", false); setPathAvailability(false); return; } String boundary = null; Dikuai pending = getOrCreatePendingDikuai(); if (pending != null) { boundary = normalizeCoordinateValue(pending.getBoundaryCoordinates()); } if (boundary == null) { boundary = normalizeCoordinateValue(dikuaiData.get("boundaryCoordinates")); } String obstacles = normalizeCoordinateValue(dikuaiData.get("obstacleCoordinates")); if (!isMeaningfulValue(obstacles)) { obstacles = resolveObstaclePayloadFromConfig(landNumber); if (isMeaningfulValue(obstacles)) { dikuaiData.put("obstacleCoordinates", obstacles); } } Shouye shouye = Shouye.getInstance(); if (shouye == null) { JOptionPane.showMessageDialog(this, "无法打开主页面,请稍后重试", "提示", JOptionPane.WARNING_MESSAGE); return; } dikuaiData.put("areaName", trimmedAreaName); if (isMeaningfulValue(boundary)) { dikuaiData.put("boundaryCoordinates", boundary); } pendingLandNumber = landNumber; captureSessionSnapshot(); resumeRequested = true; boolean started = shouye.startMowingPathPreview( landNumber, displayAreaName, boundary, obstacles, plannedPath, AddDikuai::resumeFromPreview ); if (!started) { resumeRequested = false; JOptionPane.showMessageDialog(this, "无法启动预览,请稍后再试", "提示", JOptionPane.WARNING_MESSAGE); return; } closePreviewAndDispose(); } private void persistStep3Inputs() { String trimmedName = areaNameField.getText() != null ? areaNameField.getText().trim() : ""; dikuaiData.put("areaName", trimmedName); if (mowingPatternCombo != null) { Object selection = mowingPatternCombo.getSelectedItem(); if (selection != null) { dikuaiData.put("mowingPattern", selection.toString()); } } // 保存割草机割刀宽度 if (mowingWidthField != null) { String bladeWidthText = mowingWidthField.getText().trim(); if (!bladeWidthText.isEmpty()) { try { double bladeWidthMeters = Double.parseDouble(bladeWidthText); dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters)); } catch (NumberFormatException e) { // 如果解析失败,直接保存文本 dikuaiData.put("mowingBladeWidth", bladeWidthText); } } } // 保存相邻行重叠距离 if (overlapDistanceField != null) { String overlapText = overlapDistanceField.getText().trim(); if (!overlapText.isEmpty()) { try { double overlapMeters = Double.parseDouble(overlapText); dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters)); } catch (NumberFormatException e) { // 如果解析失败,直接保存文本 dikuaiData.put("mowingOverlapDistance", overlapText); } } } // 计算并保存割草宽度(割刀宽度 - 重叠距离) if (mowingWidthField != null && overlapDistanceField != null) { try { String bladeWidthText = mowingWidthField.getText().trim(); String overlapText = overlapDistanceField.getText().trim(); if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) { double bladeWidthMeters = Double.parseDouble(bladeWidthText); double overlapMeters = Double.parseDouble(overlapText); double calculatedWidthMeters = bladeWidthMeters - overlapMeters; if (calculatedWidthMeters > 0) { // 转换为厘米存储,保持与原有数据格式兼容 int widthCm = (int) Math.round(calculatedWidthMeters * 100.0); dikuaiData.put("mowingWidth", Integer.toString(widthCm)); } } } catch (NumberFormatException e) { // 如果计算失败,不保存割草宽度 } } } private void captureSessionSnapshot() { if (activeSession == null) { activeSession = new DrawingSession(); } String landNumber = getPendingLandNumber(); activeSession.landNumber = landNumber; activeSession.areaName = areaNameField.getText() != null ? areaNameField.getText().trim() : ""; activeSession.drawingCompleted = true; activeSession.data = new HashMap<>(dikuaiData); } private void closePreviewAndDispose() { setVisible(false); dispose(); } private JButton createPrimaryButton(String text, int fontSize) { JButton button = buttonset.createStyledButton(text, PRIMARY_COLOR); button.setFont(new Font("微软雅黑", Font.BOLD, fontSize)); button.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(PRIMARY_DARK, 2), BorderFactory.createEmptyBorder(12, 25, 12, 25) )); button.setCursor(new Cursor(Cursor.HAND_CURSOR)); 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); } } }); return button; } private void showPathGenerationMessage(String message, boolean success) { if (pathGenerationMessageArea == null || pathMessageWrapper == null) { return; } String display = message == null ? "" : message.trim(); if (display.isEmpty()) { dikuaiData.remove(KEY_PATH_MESSAGE_TEXT); dikuaiData.remove(KEY_PATH_MESSAGE_SUCCESS); } else { dikuaiData.put(KEY_PATH_MESSAGE_TEXT, display); dikuaiData.put(KEY_PATH_MESSAGE_SUCCESS, success ? "true" : "false"); } pathGenerationMessageArea.setText(display); Color borderColor = success ? PRIMARY_COLOR : ERROR_COLOR; Color textColor = success ? PRIMARY_DARK : ERROR_COLOR; Color backgroundColor = success ? PRIMARY_LIGHT : new Color(255, 235, 238); pathGenerationMessageArea.setForeground(textColor); pathMessageWrapper.setBackground(backgroundColor); pathMessageWrapper.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(borderColor, 1), BorderFactory.createEmptyBorder(12, 12, 12, 12) )); pathMessageWrapper.setVisible(!display.isEmpty()); pathMessageWrapper.revalidate(); pathMessageWrapper.repaint(); } private void setPathAvailability(boolean available) { boolean effective = available && currentStep == 3; if (createButton != null) { createButton.setEnabled(effective); } if (previewButton != null) { boolean visible = previewButton.isVisible(); previewButton.setEnabled(effective && visible); } } private JPanel createButtonPanel() { JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); buttonPanel.setBackground(WHITE); buttonPanel.setBorder(BorderFactory.createEmptyBorder(20, 0, 0, 0)); prevButton = buttonset.createStyledButton("上一步", MEDIUM_GRAY); prevButton.setFont(new Font("微软雅黑", Font.BOLD, 16)); prevButton.setForeground(TEXT_COLOR); prevButton.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(BORDER_COLOR, 2), BorderFactory.createEmptyBorder(10, 25, 10, 25) )); prevButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); nextButton = createPrimaryButton("下一步", 16); createButton = createPrimaryButton("保存", 16); createButton.setVisible(false); createButton.setEnabled(false); previewButton = createPrimaryButton("预览", 16); previewButton.setVisible(false); previewButton.setEnabled(false); previewButtonSpacer = Box.createHorizontalStrut(15); previewButtonSpacer.setVisible(false); buttonPanel.add(prevButton); buttonPanel.add(Box.createHorizontalGlue()); buttonPanel.add(nextButton); buttonPanel.add(previewButtonSpacer); buttonPanel.add(previewButton); buttonPanel.add(Box.createHorizontalStrut(15)); buttonPanel.add(createButton); return buttonPanel; } private void showBoundaryPointSummary() { if (boundaryCountLabel == null) { return; } int count = Coordinate.coordinates != null ? Coordinate.coordinates.size() : 0; double area = jisuanmianjie.calculatePolygonArea(); DecimalFormat areaFormat = new DecimalFormat("0.00"); boundaryCountLabel.setText("已采集到边界点" + count + "个,当前地块面积为" + areaFormat.format(area) + "㎡"); boundaryCountLabel.setVisible(true); } private boolean prepareBoundaryTransition() { BoundarySnapshotResult snapshot = computeBoundarySnapshot(); if (!snapshot.success) { if (snapshot.errorMessage != null) { JOptionPane.showMessageDialog(this, snapshot.errorMessage, "提示", snapshot.messageType); } return false; } String landNumber = getPendingLandNumber(); Dikuai dikuai = getOrCreatePendingDikuai(); if (dikuai != null) { dikuai.setBoundaryOriginalCoordinates(snapshot.originalBoundary); dikuai.setBoundaryCoordinates(snapshot.optimizedBoundary); dikuai.setLandArea(snapshot.areaSqMeters); dikuai.setBaseStationCoordinates(snapshot.baseStationCoordinates); dikuai.setUpdateTime(getCurrentTime()); Dikuai.putDikuai(landNumber, dikuai); } dikuaiData.put("boundaryOriginalCoordinates", snapshot.originalBoundary); dikuaiData.put("boundaryCoordinates", snapshot.optimizedBoundary); dikuaiData.put("landArea", snapshot.areaSqMeters); dikuaiData.put("baseStationCoordinates", snapshot.baseStationCoordinates); if (activeSession != null) { activeSession.data.put("boundaryOriginalCoordinates", snapshot.originalBoundary); activeSession.data.put("boundaryCoordinates", snapshot.optimizedBoundary); activeSession.data.put("landArea", snapshot.areaSqMeters); activeSession.data.put("baseStationCoordinates", snapshot.baseStationCoordinates); } return true; } private static List sanitizeCoordinateList(List source) { if (source == null || source.isEmpty()) { return Collections.emptyList(); } List snapshot = new ArrayList<>(); for (Coordinate coordinate : source) { if (coordinate != null) { snapshot.add(coordinate); } } if (snapshot.isEmpty()) { return Collections.emptyList(); } DecimalFormat latLonFormat = new DecimalFormat("0.000000"); LinkedHashMap unique = new LinkedHashMap<>(); for (Coordinate coord : snapshot) { double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection()); double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection()); String key = latLonFormat.format(lat) + "," + latLonFormat.format(lon); unique.putIfAbsent(key, coord); } return new ArrayList<>(unique.values()); } private static String buildOriginalBoundaryString(List coordinates) { if (coordinates == null || coordinates.isEmpty()) { return "-1"; } StringBuilder sb = new StringBuilder(); DecimalFormat latLonFormat = new DecimalFormat("0.000000"); DecimalFormat elevationFormat = new DecimalFormat("0.00"); for (Coordinate coord : coordinates) { double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection()); double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection()); double elevation = coord.getElevation(); if (sb.length() > 0) { sb.append(";"); } sb.append(latLonFormat.format(lat)).append(",") .append(latLonFormat.format(lon)).append(",") .append(elevationFormat.format(elevation)); } return sb.toString(); } private static double convertToDecimalDegree(String dmm, String direction) { if (dmm == null || dmm.isEmpty()) { return 0.0; } try { int dotIndex = dmm.indexOf('.'); if (dotIndex == -1) { dotIndex = dmm.length(); } if (dotIndex < 2) { return 0.0; } int degrees = Integer.parseInt(dmm.substring(0, dotIndex - 2)); double minutes = Double.parseDouble(dmm.substring(dotIndex - 2)); double decimalDegrees = degrees + minutes / 60.0; if (direction != null && ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction))) { decimalDegrees = -decimalDegrees; } return decimalDegrees; } catch (NumberFormatException ex) { System.err.println("坐标格式转换错误: " + dmm); return 0.0; } } private Dikuai getOrCreatePendingDikuai() { String landNumber = getPendingLandNumber(); if (landNumber == null) { return null; } Dikuai dikuai = Dikuai.getDikuai(landNumber); if (dikuai == null) { dikuai = new Dikuai(); dikuai.setLandNumber(landNumber); Dikuai.putDikuai(landNumber, dikuai); } return dikuai; } private void hideBoundaryPointSummary() { if (boundaryCountLabel != null) { boundaryCountLabel.setVisible(false); } } private boolean hasGeneratedPath() { return isMeaningfulValue(dikuaiData.get("plannedPath")); } private String resolveObstaclePayloadFromConfig(String landNumber) { if (!isMeaningfulValue(landNumber)) { return null; } try { File configFile = new File("Obstacledge.properties"); if (!configFile.exists()) { return null; } Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); if (!manager.loadFromFile(configFile.getAbsolutePath())) { return null; } Obstacledge.Plot plot = manager.getPlotById(landNumber.trim()); if (plot == null) { return null; } return Obstacledge.buildPlannerPayload(plot.getObstacles()); } catch (Exception ex) { System.err.println("加载障碍物配置失败: " + ex.getMessage()); return null; } } private static String normalizeCoordinateValue(String value) { return isMeaningfulValue(value) ? value.trim() : null; } private static boolean isMeaningfulValue(String value) { if (value == null) { return false; } String trimmed = value.trim(); return !trimmed.isEmpty() && !"-1".equals(trimmed); } private static BoundarySnapshotResult computeBoundarySnapshot() { List uniqueCoordinates; synchronized (Coordinate.coordinates) { uniqueCoordinates = sanitizeCoordinateList(Coordinate.coordinates); Coordinate.coordinates.clear(); Coordinate.coordinates.addAll(uniqueCoordinates); } if (uniqueCoordinates.size() < 3) { return BoundarySnapshotResult.failure("采集的边界点不足,无法生成地块边界", JOptionPane.WARNING_MESSAGE); } double area = jisuanmianjie.calculatePolygonArea(); if (area <= 0) { return BoundarySnapshotResult.failure("当前地块面积为0,无法继续", JOptionPane.WARNING_MESSAGE); } BaseStation baseStation = new BaseStation(); baseStation.load(); String baseStationCoordinates = normalizeCoordinateValue(baseStation.getInstallationCoordinates()); if (!isMeaningfulValue(baseStationCoordinates)) { return BoundarySnapshotResult.failure("未获取到有效的基准站坐标,请先在基准站管理中设置", JOptionPane.WARNING_MESSAGE); } String optimizedBoundary; try { optimizedBoundary = bianjieguihua2.processCoordinateListAuto(baseStationCoordinates); } catch (RuntimeException ex) { ex.printStackTrace(); return BoundarySnapshotResult.failure("生成地块边界失败: " + ex.getMessage(), JOptionPane.ERROR_MESSAGE); } String originalBoundary = buildOriginalBoundaryString(uniqueCoordinates); DecimalFormat areaFormat = new DecimalFormat("0.00"); String areaString = areaFormat.format(area); return BoundarySnapshotResult.success(originalBoundary, optimizedBoundary, areaString, baseStationCoordinates); } private String resolvePlannerMode(String patternDisplay) { if (patternDisplay == null) { return "parallel"; } String trimmed = patternDisplay.trim(); if (trimmed.isEmpty()) { return "parallel"; } if ("螺旋式".equals(trimmed) || "spiral".equalsIgnoreCase(trimmed) || "1".equals(trimmed)) { return "spiral"; } return "parallel"; } private void setupEventHandlers() { // 上一步按钮 prevButton.addActionListener(e -> { if (currentStep > 1) { showStep(currentStep - 1); } }); // 下一步按钮 nextButton.addActionListener(e -> { if (validateCurrentStep()) { if (currentStep < 3) { // 步骤2特殊验证:必须完成边界绘制才能进入下一步 if (currentStep == 2 && !dikuaiData.containsKey("boundaryDrawn")) { JOptionPane.showMessageDialog(this, "请先完成边界绘制", "提示", JOptionPane.WARNING_MESSAGE); return; } if (currentStep == 2 && !prepareBoundaryTransition()) { return; } showStep(currentStep + 1); } } }); // 创建地块按钮 createButton.addActionListener(e -> createDikuai()); if (previewButton != null) { previewButton.addActionListener(e -> previewMowingPath()); } // 关闭对话框 addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { dispose(); } }); } private void showStep(int step) { currentStep = step; cardLayout.show(stepsPanel, "step" + step); // 更新按钮状态 updateButtonState(step); } private void updateButtonState(int step) { prevButton.setVisible(step > 1); if (step < 3) { nextButton.setVisible(true); createButton.setVisible(false); setPathAvailability(false); if (previewButton != null) { previewButton.setVisible(false); previewButton.setEnabled(false); } if (previewButtonSpacer != null) { previewButtonSpacer.setVisible(false); } } else { nextButton.setVisible(false); createButton.setVisible(true); if (previewButton != null) { previewButton.setVisible(true); } if (previewButtonSpacer != null) { previewButtonSpacer.setVisible(true); } setPathAvailability(hasGeneratedPath()); } Container parent = prevButton.getParent(); if (parent != null) { parent.revalidate(); parent.repaint(); } } private boolean validateCurrentStep() { switch (currentStep) { case 1: String name = areaNameField.getText().trim(); if (name.isEmpty()) { JOptionPane.showMessageDialog(this, "请输入地块名称", "提示", JOptionPane.WARNING_MESSAGE); areaNameField.requestFocus(); return false; } String currentLandNumber = getPendingLandNumber(); if (isLandNameDuplicate(name, currentLandNumber)) { JOptionPane.showMessageDialog(this, "地块名称已存在,请输入唯一名称", "提示", JOptionPane.WARNING_MESSAGE); areaNameField.requestFocus(); return false; } dikuaiData.put("areaName", name); break; case 2: if (!dikuaiData.containsKey("drawingMethod")) { JOptionPane.showMessageDialog(this, "请选择绘制方式", "提示", JOptionPane.WARNING_MESSAGE); return false; } // 步骤2的特殊验证:必须完成边界绘制 if (!dikuaiData.containsKey("boundaryDrawn")) { JOptionPane.showMessageDialog(this, "请先完成边界绘制", "提示", JOptionPane.WARNING_MESSAGE); return false; } break; case 3: dikuaiData.put("mowingPattern", (String) mowingPatternCombo.getSelectedItem()); // 保存割草机割刀宽度 if (mowingWidthField != null) { String bladeWidthText = mowingWidthField.getText().trim(); if (!bladeWidthText.isEmpty()) { try { double bladeWidthMeters = Double.parseDouble(bladeWidthText); dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters)); } catch (NumberFormatException e) { dikuaiData.put("mowingBladeWidth", bladeWidthText); } } } // 保存相邻行重叠距离 if (overlapDistanceField != null) { String overlapText = overlapDistanceField.getText().trim(); if (!overlapText.isEmpty()) { try { double overlapMeters = Double.parseDouble(overlapText); dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters)); } catch (NumberFormatException e) { dikuaiData.put("mowingOverlapDistance", overlapText); } } } // 计算并保存割草宽度(割刀宽度 - 重叠距离) if (mowingWidthField != null && overlapDistanceField != null) { try { String bladeWidthText = mowingWidthField.getText().trim(); String overlapText = overlapDistanceField.getText().trim(); if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) { double bladeWidthMeters = Double.parseDouble(bladeWidthText); double overlapMeters = Double.parseDouble(overlapText); double calculatedWidthMeters = bladeWidthMeters - overlapMeters; if (calculatedWidthMeters > 0) { // 转换为厘米存储,保持与原有数据格式兼容 int widthCm = (int) Math.round(calculatedWidthMeters * 100.0); dikuaiData.put("mowingWidth", Integer.toString(widthCm)); } } } catch (NumberFormatException e) { // 如果计算失败,不保存割草宽度 } } if (!hasGeneratedPath()) { JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.WARNING_MESSAGE); return false; } break; } return true; } private boolean startDrawingBoundary() { String method = dikuaiData.get("drawingMethod"); if ("mower".equals(method)) { Shouye shouye = Shouye.getInstance(); if (shouye == null) { JOptionPane.showMessageDialog(this, "无法进入主页面,请稍后重试", "提示", JOptionPane.WARNING_MESSAGE); return false; } if (!shouye.startMowerBoundaryCapture()) { JOptionPane.showMessageDialog(this, "未能开始割草机绘制,请确认设备状态和基准站设置后重试", "提示", JOptionPane.WARNING_MESSAGE); return false; } closeForDrawingSession(); return true; } else if ("handheld".equals(method)) { if (closeForHandheldDrawingSession()) { return true; } resetDrawingState(); return false; } return false; } private boolean captureGnssSnapshot() { if (activeSession != null && activeSession.drawingCompleted) { return false; } Object qualityObj = MowerLocationData.getProperty("positioningQuality"); if (!"4".equals(String.valueOf(qualityObj))) { return false; } Object latObj = MowerLocationData.getProperty("latitude"); Object lonObj = MowerLocationData.getProperty("longitude"); if (!(latObj instanceof Number) || !(lonObj instanceof Number)) { return false; } double latitude = ((Number) latObj).doubleValue(); double longitude = ((Number) lonObj).doubleValue(); if (!Double.isFinite(latitude) || !Double.isFinite(longitude)) { return false; } File file = new File("huizhiibanjieGNSS.propertie"); try { if (!file.exists()) { File parent = file.getAbsoluteFile().getParentFile(); if (parent != null && !parent.exists()) { parent.mkdirs(); } file.createNewFile(); } List lines = Files.exists(file.toPath()) ? Files.readAllLines(file.toPath(), StandardCharsets.UTF_8) : new ArrayList<>(); String formatted = formatCoordinate(latitude, longitude); if (lines.isEmpty()) { lines.add(formatted); } else { String existing = lines.get(0).trim(); if (!existing.isEmpty()) { existing = existing + ";" + formatted; } else { existing = formatted; } lines.set(0, existing); } Files.write(file.toPath(), lines, StandardCharsets.UTF_8); appendCoordinateToSession(formatted); return true; } catch (IOException ex) { ex.printStackTrace(); return false; } } private void appendCoordinateToSession(String coordinate) { if (coordinate == null || coordinate.isEmpty()) { return; } if (activeSession != null && activeSession.drawingCompleted) { return; } if (getPendingLandNumber() == null) { return; } String existing = dikuaiData.get("boundaryOriginalCoordinates"); if ((existing == null || existing.isEmpty()) && activeSession != null) { existing = activeSession.data.get("boundaryOriginalCoordinates"); } String updated = mergeCoordinates(existing, coordinate); dikuaiData.put("boundaryOriginalCoordinates", updated); if (activeSession != null) { activeSession.data.put("boundaryOriginalCoordinates", updated); } } private String formatCoordinate(double latitude, double longitude) { DecimalFormat df = new DecimalFormat("0.000000"); return df.format(latitude) + "," + df.format(longitude); } private String mergeCoordinates(String existing, String coordinate) { if (existing == null || existing.trim().isEmpty() || "-1".equals(existing.trim())) { return coordinate; } String trimmed = existing.trim(); if (trimmed.endsWith(";")) { trimmed = trimmed.substring(0, trimmed.length() - 1); } if (trimmed.contains(coordinate)) { return trimmed; } return trimmed + ";" + coordinate; } private String getPendingLandNumber() { if (pendingLandNumber != null) { updateLandNumberField(pendingLandNumber); return pendingLandNumber; } if (activeSession != null && activeSession.landNumber != null) { pendingLandNumber = activeSession.landNumber; updateLandNumberField(pendingLandNumber); return pendingLandNumber; } pendingLandNumber = generateNewLandNumber(); if (activeSession != null) { activeSession.landNumber = pendingLandNumber; } updateLandNumberField(pendingLandNumber); return pendingLandNumber; } private String generateNewLandNumber() { Map existing = Dikuai.getAllDikuai(); int attempt = 1; while (true) { String candidate = "LAND" + attempt; if (!existing.containsKey(candidate)) { return candidate; } attempt++; } } private void updateLandNumberField(String value) { if (landNumberField != null && value != null) { landNumberField.setText(value); } } private boolean isLandNameDuplicate(String name, String currentLandNumber) { if (name == null || name.isEmpty()) { return false; } Map existing = Dikuai.getAllDikuai(); for (Map.Entry entry : existing.entrySet()) { String key = entry.getKey(); Dikuai dikuai = entry.getValue(); if (key == null || dikuai == null) { continue; } if (key.equals(currentLandNumber)) { continue; } String existingName = dikuai.getLandName(); if (existingName != null && !"-1".equals(existingName) && existingName.trim().equalsIgnoreCase(name.trim())) { return true; } } return false; } private void closeForDrawingSession() { isDrawing = false; Shouye shouye = Shouye.getInstance(); if (shouye != null) { shouye.showEndDrawingButton(AddDikuai::finishDrawingSession); } setVisible(false); dispose(); } private boolean closeForHandheldDrawingSession() { isDrawing = false; Coordinate.setStartSaveGngga(false); Shouye shouye = Shouye.getInstance(); if (shouye == null) { JOptionPane.showMessageDialog(this, "无法进入主页面,请稍后重试", "提示", JOptionPane.WARNING_MESSAGE); return false; } boolean started = shouye.startHandheldBoundaryCapture(); if (!started) { JOptionPane.showMessageDialog(this, "无法开始手持采集,请确认设备状态", "提示", JOptionPane.WARNING_MESSAGE); return false; } setVisible(false); dispose(); return true; } private void applySessionData(DrawingSession session) { if (session == null) { return; } pendingLandNumber = session.landNumber; dikuaiData.clear(); dikuaiData.putAll(session.data); updateLandNumberField(pendingLandNumber); areaNameField.setText(session.areaName != null ? session.areaName : ""); String method = session.data.get("drawingMethod"); if (method != null) { JPanel panel = drawingOptionPanels.get(method); if (panel != null) { selectDrawingOption(panel, method, false); } } if (session.drawingCompleted) { dikuaiData.put("boundaryDrawn", "true"); if (startEndDrawingBtn != null) { startEndDrawingBtn.setText("已完成"); startEndDrawingBtn.setBackground(MEDIUM_GRAY); startEndDrawingBtn.setEnabled(false); } updateOtherOptionsState(true); isDrawing = false; showStep(2); showBoundaryPointSummary(); } else { if (startEndDrawingBtn != null) { startEndDrawingBtn.setText("开始绘制"); startEndDrawingBtn.setBackground(PRIMARY_COLOR); startEndDrawingBtn.setEnabled(true); } updateOtherOptionsState(false); showStep(1); hideBoundaryPointSummary(); } restoreGeneratedPathState(session); } private void restoreGeneratedPathState(DrawingSession session) { if (session == null || session.data == null) { showPathGenerationMessage("", true); return; } Map data = session.data; if (mowingPatternCombo != null) { String pattern = data.get("mowingPattern"); if (pattern != null) { ComboBoxModel model = mowingPatternCombo.getModel(); for (int i = 0; i < model.getSize(); i++) { String candidate = model.getElementAt(i); if (pattern.equals(candidate)) { mowingPatternCombo.setSelectedIndex(i); break; } } } } // 恢复割草机割刀宽度(优先从mowingBladeWidth获取) if (mowingWidthField != null) { String bladeWidth = data.get("mowingBladeWidth"); if (isMeaningfulValue(bladeWidth)) { try { double bladeWidthMeters = Double.parseDouble(bladeWidth.trim()); mowingWidthField.setText(String.format(Locale.US, "%.2f", bladeWidthMeters)); } catch (NumberFormatException ignored) { // 如果mowingBladeWidth不存在或解析失败,尝试从mowingWidth恢复 String width = data.get("mowingWidth"); if (isMeaningfulValue(width)) { try { // 如果存储的是厘米,转换为米显示 double parsed = Double.parseDouble(width.trim()); // 假设如果值大于10,则是厘米,需要转换为米;否则已经是米 double widthMeters = parsed > 10 ? parsed / 100.0 : parsed; mowingWidthField.setText(String.format(Locale.US, "%.2f", widthMeters)); } catch (NumberFormatException ignored2) { // 保持当前值 } } } } else { // 如果mowingBladeWidth不存在,尝试从mowingWidth恢复 String width = data.get("mowingWidth"); if (isMeaningfulValue(width)) { try { // 如果存储的是厘米,转换为米显示 double parsed = Double.parseDouble(width.trim()); // 假设如果值大于10,则是厘米,需要转换为米;否则已经是米 double widthMeters = parsed > 10 ? parsed / 100.0 : parsed; mowingWidthField.setText(String.format(Locale.US, "%.2f", widthMeters)); } catch (NumberFormatException ignored) { // 保持当前值 } } } } if (overlapDistanceField != null) { String overlap = data.get("mowingOverlapDistance"); if (isMeaningfulValue(overlap)) { try { double overlapMeters = Double.parseDouble(overlap.trim()); overlapDistanceField.setText(String.format(Locale.US, "%.2f", overlapMeters)); } catch (NumberFormatException ignored) { // 保持当前值 } } } boolean hasPath = isMeaningfulValue(data.get("plannedPath")); if (!hasPath) { showPathGenerationMessage("", true); if (currentStep == 3) { setPathAvailability(false); } return; } String message = data.get(KEY_PATH_MESSAGE_TEXT); boolean success = !"false".equalsIgnoreCase(data.get(KEY_PATH_MESSAGE_SUCCESS)); showStep(3); if (isMeaningfulValue(message)) { showPathGenerationMessage(message, success); } else { showPathGenerationMessage("已生成割草路径,可点击“预览”按钮查看效果。", true); } setPathAvailability(true); } public static void finishDrawingSession() { Shouye shouye = Shouye.getInstance(); List previewPoints = shouye != null ? shouye.getHandheldTemporaryPointsSnapshot() : Collections.emptyList(); recordTemporaryBoundaryPoints(previewPoints); BoundarySnapshotResult snapshot = computeBoundarySnapshot(); if (snapshot.success && activeSession != null) { activeSession.data.put("boundaryOriginalCoordinates", snapshot.originalBoundary); activeSession.data.put("boundaryCoordinates", snapshot.optimizedBoundary); activeSession.data.put("landArea", snapshot.areaSqMeters); activeSession.data.put("baseStationCoordinates", snapshot.baseStationCoordinates); } if (shouye != null) { shouye.hideEndDrawingButton(); } if (activeSession == null) { return; } activeSession.drawingCompleted = true; activeSession.data.put("boundaryDrawn", "true"); Coordinate.setStartSaveGngga(false); resumeRequested = true; Component parent = shouye != null ? shouye : null; showAddDikuaiDialog(parent); } public static void resumeFromPreview() { Shouye shouye = Shouye.getInstance(); if (shouye != null) { shouye.exitMowingPathPreview(); } if (activeSession == null) { return; } resumeRequested = true; Component parent = shouye != null ? shouye : null; SwingUtilities.invokeLater(() -> showAddDikuaiDialog(parent)); } private void createDikuai() { if (!validateCurrentStep()) { return; } String areaName = areaNameField.getText().trim(); dikuaiData.put("areaName", areaName); String landNumber = getPendingLandNumber(); Dikuai dikuai = Dikuai.getDikuai(landNumber); if (dikuai == null) { dikuai = new Dikuai(); dikuai.setLandNumber(landNumber); dikuai.setCreateTime(getCurrentTime()); } else if (dikuai.getCreateTime() == null || "-1".equals(dikuai.getCreateTime())) { dikuai.setCreateTime(getCurrentTime()); } dikuai.setLandName(areaName); dikuai.setUpdateTime(getCurrentTime()); if (dikuaiData.containsKey("boundaryOriginalCoordinates")) { dikuai.setBoundaryOriginalCoordinates(dikuaiData.get("boundaryOriginalCoordinates")); } if (dikuaiData.containsKey("boundaryCoordinates")) { dikuai.setBoundaryCoordinates(dikuaiData.get("boundaryCoordinates")); } if (dikuaiData.containsKey("landArea")) { dikuai.setLandArea(dikuaiData.get("landArea")); } if (dikuaiData.containsKey("baseStationCoordinates")) { dikuai.setBaseStationCoordinates(dikuaiData.get("baseStationCoordinates")); } if (dikuaiData.containsKey("mowingPattern")) { dikuai.setMowingPattern(dikuaiData.get("mowingPattern")); } // 保存割草机割刀宽度(优先从dikuaiData获取,否则从TextField获取) if (dikuaiData.containsKey("mowingBladeWidth")) { dikuai.setMowingBladeWidth(dikuaiData.get("mowingBladeWidth")); } else if (mowingWidthField != null) { String bladeWidthText = mowingWidthField.getText().trim(); if (!bladeWidthText.isEmpty()) { try { double bladeWidthMeters = Double.parseDouble(bladeWidthText); dikuai.setMowingBladeWidth(String.format(Locale.US, "%.2f", bladeWidthMeters)); } catch (NumberFormatException e) { dikuai.setMowingBladeWidth(bladeWidthText); } } } // 保存相邻行重叠距离(优先从dikuaiData获取,否则从TextField获取) if (dikuaiData.containsKey("mowingOverlapDistance")) { dikuai.setMowingOverlapDistance(dikuaiData.get("mowingOverlapDistance")); } else if (overlapDistanceField != null) { String overlapText = overlapDistanceField.getText().trim(); if (!overlapText.isEmpty()) { try { double overlapMeters = Double.parseDouble(overlapText); dikuai.setMowingOverlapDistance(String.format(Locale.US, "%.2f", overlapMeters)); } catch (NumberFormatException e) { dikuai.setMowingOverlapDistance(overlapText); } } } // 保存割草宽度(计算后的值,优先从dikuaiData获取) if (dikuaiData.containsKey("mowingWidth")) { dikuai.setMowingWidth(dikuaiData.get("mowingWidth")); } else { // 如果没有在dikuaiData中,则从TextField计算 if (mowingWidthField != null && overlapDistanceField != null) { try { String bladeWidthText = mowingWidthField.getText().trim(); String overlapText = overlapDistanceField.getText().trim(); if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) { double bladeWidthMeters = Double.parseDouble(bladeWidthText); double overlapMeters = Double.parseDouble(overlapText); double calculatedWidthMeters = bladeWidthMeters - overlapMeters; if (calculatedWidthMeters > 0) { // 转换为厘米存储,保持与原有数据格式兼容 int widthCm = (int) Math.round(calculatedWidthMeters * 100.0); dikuai.setMowingWidth(Integer.toString(widthCm)); } } } catch (NumberFormatException e) { // 如果计算失败,保持原有值或使用默认值 } } } String plannedPath = dikuaiData.get("plannedPath"); if (isMeaningfulValue(plannedPath)) { dikuai.setPlannedPath(plannedPath); } Dikuai.putDikuai(landNumber, dikuai); Dikuai.saveToProperties(); JOptionPane.showMessageDialog(this, "地块创建成功!\n地块编号: " + landNumber + "\n地块名称: " + areaName, "成功", JOptionPane.INFORMATION_MESSAGE); createdLandNumber = landNumber; pendingLandNumber = null; if (activeSession != null && landNumber.equals(activeSession.landNumber)) { activeSession = null; } resumeRequested = false; Shouye shouye = Shouye.getInstance(); if (shouye != null) { shouye.hideEndDrawingButton(); } Dikuaiguanli.notifyExternalCreation(landNumber); dispose(); } private static String getCurrentTime() { java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(new java.util.Date()); } /** * 显示新增地块对话框 * @param parent 父组件 */ public static String showAddDikuaiDialog(Component parent) { AddDikuai dialog; Window parentWindow = null; if (parent != null) { parentWindow = SwingUtilities.getWindowAncestor(parent); } if (parentWindow instanceof JFrame) { dialog = new AddDikuai((JFrame) parentWindow); } else if (parentWindow instanceof JDialog) { dialog = new AddDikuai((JDialog) parentWindow); } else { dialog = new AddDikuai((JFrame) null); } if (resumeRequested && activeSession != null) { dialog.applySessionData(activeSession); resumeRequested = false; } dialog.setVisible(true); return dialog.createdLandNumber; } }