| | |
| | | package lujing; |
| | | |
| | | import javax.swing.*; |
| | | import javax.swing.SwingUtilities; |
| | | import java.awt.*; |
| | | import java.awt.datatransfer.Clipboard; |
| | | import java.awt.datatransfer.StringSelection; |
| | | import java.awt.event.ActionEvent; |
| | | import java.awt.event.ActionListener; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.ArrayList; |
| | |
| | | import lujing.Lunjingguihua; |
| | | import lujing.ObstaclePathPlanner; |
| | | import org.locationtech.jts.geom.Coordinate; |
| | | import gecaoji.Device; |
| | | |
| | | /** |
| | | * 生成割草路径页面 |
| | |
| | | widthField = createInfoTextField(widthValue != null ? widthValue : "", true); |
| | | contentPanel.add(createTextFieldSection("割草宽度 (厘米)", widthField)); |
| | | |
| | | // 割草安全距离(只读显示) |
| | | String displaySafetyDistance = "未设置"; |
| | | Device device = Device.getActiveDevice(); |
| | | if (device != null) { |
| | | String safetyDistanceValue = device.getMowingSafetyDistance(); |
| | | if (safetyDistanceValue != null && !"-1".equals(safetyDistanceValue) && !safetyDistanceValue.trim().isEmpty()) { |
| | | try { |
| | | double distanceMeters = Double.parseDouble(safetyDistanceValue.trim()); |
| | | // 如果值大于100,认为是厘米,需要转换为米 |
| | | if (distanceMeters > 100) { |
| | | distanceMeters = distanceMeters / 100.0; |
| | | } |
| | | displaySafetyDistance = String.format("%.2f米", distanceMeters); |
| | | } catch (NumberFormatException e) { |
| | | displaySafetyDistance = "未设置"; |
| | | } |
| | | } |
| | | } |
| | | contentPanel.add(createInfoValueSection("割草安全距离", displaySafetyDistance)); |
| | | |
| | | // 割草模式(只读显示) |
| | | contentPanel.add(createInfoValueSection("割草模式", formatMowingPatternForDialog(modeValue))); |
| | | |
| | |
| | | buttonPanel.setBackground(BACKGROUND_COLOR); |
| | | |
| | | JButton generateBtn = createPrimaryFooterButton("生成割草路径"); |
| | | JButton previewBtn = createPrimaryFooterButton("预览"); |
| | | JButton saveBtn = createPrimaryFooterButton("保存路径"); |
| | | JButton cancelBtn = createPrimaryFooterButton("取消"); |
| | | |
| | | generateBtn.addActionListener(e -> generatePath(modeValue)); |
| | | previewBtn.addActionListener(e -> previewPath()); |
| | | saveBtn.addActionListener(e -> savePath()); |
| | | cancelBtn.addActionListener(e -> dispose()); |
| | | |
| | | buttonPanel.add(generateBtn); |
| | | buttonPanel.add(previewBtn); |
| | | buttonPanel.add(saveBtn); |
| | | buttonPanel.add(cancelBtn); |
| | | add(buttonPanel, BorderLayout.SOUTH); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 预览路径 |
| | | */ |
| | | private void previewPath() { |
| | | // 先保存当前路径到地块(临时保存,用于预览) |
| | | String pathNormalized = normalizeCoordinateInput(pathArea.getText()); |
| | | if (!"-1".equals(pathNormalized)) { |
| | | pathNormalized = pathNormalized |
| | | .replace("\r\n", ";") |
| | | .replace('\r', ';') |
| | | .replace('\n', ';') |
| | | .replaceAll(";+", ";") |
| | | .replaceAll("\\s*;\\s*", ";") |
| | | .trim(); |
| | | if (pathNormalized.isEmpty()) { |
| | | pathNormalized = "-1"; |
| | | } |
| | | } |
| | | |
| | | if ("-1".equals(pathNormalized)) { |
| | | JOptionPane.showMessageDialog(this, "请先生成割草路径", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 临时保存路径到地块对象(不持久化) |
| | | if (saveCallback != null) { |
| | | saveCallback.savePlannedPath(dikuai, pathNormalized); |
| | | } |
| | | |
| | | // 保存当前页面状态,用于返回时恢复 |
| | | String currentBaseStation = baseStationField.getText(); |
| | | String currentBoundary = boundaryArea.getText(); |
| | | String currentObstacle = obstacleArea.getText(); |
| | | String currentWidth = widthField.getText(); |
| | | String currentPath = pathArea.getText(); |
| | | |
| | | // 获取地块信息 |
| | | String landNumber = dikuai.getLandNumber(); |
| | | String landName = dikuai.getLandName(); |
| | | |
| | | // 处理边界坐标,确保变量是 effectively final |
| | | String boundaryInput = normalizeCoordinateInput(boundaryArea.getText()); |
| | | final String boundary; |
| | | if (!"-1".equals(boundaryInput)) { |
| | | String processed = boundaryInput.replace("\r\n", ";") |
| | | .replace('\r', ';') |
| | | .replace('\n', ';') |
| | | .replaceAll(";+", ";") |
| | | .replaceAll("\\s*;\\s*", ";") |
| | | .trim(); |
| | | if (processed.isEmpty()) { |
| | | boundary = dikuai.getBoundaryCoordinates(); |
| | | } else { |
| | | boundary = processed; |
| | | } |
| | | } else { |
| | | boundary = dikuai.getBoundaryCoordinates(); |
| | | } |
| | | |
| | | // 处理障碍物坐标,确保变量是 effectively final |
| | | String obstaclesInput = normalizeCoordinateInput(obstacleArea.getText()); |
| | | final String obstacles; |
| | | if (!"-1".equals(obstaclesInput)) { |
| | | String processed = obstaclesInput.replace("\r\n", " ") |
| | | .replace('\r', ' ') |
| | | .replace('\n', ' ') |
| | | .replaceAll("\\s{2,}", " ") |
| | | .trim(); |
| | | if (processed.isEmpty()) { |
| | | obstacles = null; |
| | | } else { |
| | | obstacles = processed; |
| | | } |
| | | } else { |
| | | obstacles = null; |
| | | } |
| | | |
| | | // 保存最终值到 final 变量,以便在 lambda 中使用 |
| | | final String finalPathNormalized = pathNormalized; |
| | | final String finalLandNumber = landNumber; |
| | | final String finalLandName = landName; |
| | | |
| | | // 关闭路径规划页面 |
| | | setVisible(false); |
| | | |
| | | // 打开主页面并显示路径预览 |
| | | SwingUtilities.invokeLater(() -> { |
| | | zhuye.Shouye shouye = zhuye.Shouye.getInstance(); |
| | | if (shouye != null) { |
| | | // 显示路径预览,并设置返回回调 |
| | | shouye.startMowingPathPreview( |
| | | finalLandNumber, |
| | | finalLandName, |
| | | boundary, |
| | | obstacles, |
| | | finalPathNormalized, |
| | | () -> { |
| | | // 返回回调:重新打开路径规划页面 |
| | | SwingUtilities.invokeLater(() -> { |
| | | setVisible(true); |
| | | // 恢复之前的状态 |
| | | baseStationField.setText(currentBaseStation); |
| | | boundaryArea.setText(currentBoundary); |
| | | obstacleArea.setText(currentObstacle); |
| | | widthField.setText(currentWidth); |
| | | pathArea.setText(currentPath); |
| | | }); |
| | | } |
| | | ); |
| | | } else { |
| | | // 如果主页面不存在,提示用户并重新显示路径规划页面 |
| | | JOptionPane.showMessageDialog(null, "无法打开主页面进行预览", "提示", JOptionPane.WARNING_MESSAGE); |
| | | setVisible(true); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 保存路径 |
| | | */ |
| | | private void savePath() { |
| | |
| | | obstacleList = new ArrayList<>(); |
| | | } |
| | | |
| | | // 判断是否有障碍物:只要原始输入有障碍物内容,就使用ObstaclePathPlanner |
| | | // 即使解析后列表为空,也尝试使用ObstaclePathPlanner(它会处理空障碍物列表的情况) |
| | | boolean hasObstacles = hasObstacleInput && !obstacleList.isEmpty(); |
| | | |
| | | // 如果原始输入有障碍物但解析失败,给出提示 |
| | | if (hasObstacleInput && obstacleList.isEmpty()) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, |
| | | "障碍物坐标格式可能不正确,将尝试生成路径。如果路径不正确,请检查障碍物坐标格式。", |
| | | "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | // 仍然尝试使用ObstaclePathPlanner,即使障碍物列表为空 |
| | | // 这样至少可以确保使用正确的路径规划器 |
| | | } |
| | | // 判断是否有有效的障碍物:只有当解析成功且列表不为空时,才认为有障碍物 |
| | | boolean hasValidObstacles = !obstacleList.isEmpty(); |
| | | |
| | | String generated; |
| | | |
| | | if (!hasObstacles && !hasObstacleInput) { |
| | | // 完全没有障碍物输入时,使用Lunjingguihua类的方法生成路径 |
| | | if (!hasValidObstacles) { |
| | | // 障碍物坐标不存在或为空时,使用Lunjingguihua类的方法生成路径 |
| | | generated = Lunjingguihua.generatePathFromStrings( |
| | | boundary, |
| | | obstacles != null ? obstacles : "", |
| | | plannerWidth, |
| | | null, // safetyDistStr,使用默认值 |
| | | mode |
| | | ); |
| | | } else { |
| | | // 有障碍物输入时(即使解析失败),使用ObstaclePathPlanner处理路径生成 |
| | | // 有有效障碍物时,使用ObstaclePathPlanner处理路径生成 |
| | | List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary); |
| | | if (polygon.size() < 4) { |
| | | if (showMessages) { |
| | |
| | | return null; |
| | | } |
| | | |
| | | // 根据是否有障碍物设置不同的安全距离 |
| | | double safetyDistance; |
| | | if (!obstacleList.isEmpty()) { |
| | | // 有障碍物时使用割草宽度的一半 + 0.05米额外安全距离 |
| | | safetyDistance = widthMeters / 2.0 + 0.05; |
| | | } else { |
| | | // 障碍物解析失败但输入存在,使用较小的安全距离 |
| | | safetyDistance = 0.01; |
| | | } |
| | | // 有障碍物时使用割草宽度的一半 + 0.05米额外安全距离 |
| | | double safetyDistance = widthMeters / 2.0 + 0.05; |
| | | |
| | | ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( |
| | | polygon, widthMeters, mode, obstacleList, safetyDistance); |
| | |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | // 创建标题面板,包含标题和复制图标 |
| | | JPanel titlePanel = new JPanel(new BorderLayout()); |
| | | titlePanel.setBackground(BACKGROUND_COLOR); |
| | | titlePanel.setOpaque(false); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel, BorderLayout.NORTH); |
| | | titlePanel.add(titleLabel, BorderLayout.WEST); |
| | | |
| | | // 创建复制按钮 |
| | | JButton copyButton = createCopyButton(title, () -> { |
| | | String text = textArea.getText(); |
| | | if (text == null || text.trim().isEmpty()) { |
| | | JOptionPane.showMessageDialog(this, title + " 未设置", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | copyToClipboard(text, title); |
| | | }); |
| | | titlePanel.add(copyButton, BorderLayout.EAST); |
| | | |
| | | section.add(titlePanel, BorderLayout.NORTH); |
| | | |
| | | JScrollPane scrollPane = new JScrollPane(textArea); |
| | | scrollPane.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); |
| | |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | // 创建标题面板,包含标题和复制图标 |
| | | JPanel titlePanel = new JPanel(new BorderLayout()); |
| | | titlePanel.setBackground(BACKGROUND_COLOR); |
| | | titlePanel.setOpaque(false); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel, BorderLayout.NORTH); |
| | | titlePanel.add(titleLabel, BorderLayout.WEST); |
| | | |
| | | // 创建复制按钮 |
| | | JButton copyButton = createCopyButton(title, () -> { |
| | | String text = textField.getText(); |
| | | if (text == null || text.trim().isEmpty()) { |
| | | JOptionPane.showMessageDialog(this, title + " 未设置", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | copyToClipboard(text, title); |
| | | }); |
| | | titlePanel.add(copyButton, BorderLayout.EAST); |
| | | |
| | | section.add(titlePanel, BorderLayout.NORTH); |
| | | |
| | | JPanel fieldWrapper = new JPanel(new BorderLayout()); |
| | | fieldWrapper.setBackground(textField.isEditable() ? WHITE : new Color(245, 245, 245)); |
| | |
| | | } |
| | | return "parallel"; |
| | | } |
| | | |
| | | /** |
| | | * 创建复制按钮 |
| | | */ |
| | | private JButton createCopyButton(String title, Runnable copyAction) { |
| | | JButton copyButton = new JButton(); |
| | | Font titleFont = new Font("微软雅黑", Font.BOLD, 14); |
| | | FontMetrics metrics = getFontMetrics(titleFont); |
| | | int iconSize = metrics.getHeight(); // 使用标题字体高度作为图标大小 |
| | | |
| | | // 加载复制图标 |
| | | ImageIcon copyIcon = null; |
| | | ImageIcon successIcon = null; |
| | | try { |
| | | ImageIcon originalCopyIcon = new ImageIcon("image/fuzhi.png"); |
| | | Image scaledCopyImage = originalCopyIcon.getImage().getScaledInstance(iconSize, iconSize, Image.SCALE_SMOOTH); |
| | | copyIcon = new ImageIcon(scaledCopyImage); |
| | | |
| | | // 加载成功图标 |
| | | ImageIcon originalSuccessIcon = new ImageIcon("image/fuzhisucc.png"); |
| | | Image scaledSuccessImage = originalSuccessIcon.getImage().getScaledInstance(iconSize, iconSize, Image.SCALE_SMOOTH); |
| | | successIcon = new ImageIcon(scaledSuccessImage); |
| | | } catch (Exception e) { |
| | | // 如果图片加载失败,使用文本 |
| | | copyButton.setText("复制"); |
| | | copyButton.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | System.err.println("无法加载复制图标: " + e.getMessage()); |
| | | } |
| | | |
| | | final ImageIcon finalCopyIcon = copyIcon; |
| | | final ImageIcon finalSuccessIcon = successIcon; |
| | | |
| | | copyButton.setIcon(finalCopyIcon); |
| | | copyButton.setContentAreaFilled(false); |
| | | copyButton.setBorder(null); |
| | | copyButton.setFocusPainted(false); |
| | | copyButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | copyButton.setToolTipText("复制" + title); |
| | | |
| | | // 添加点击事件 |
| | | copyButton.addActionListener(e -> { |
| | | copyAction.run(); |
| | | // 复制成功后切换图标 |
| | | if (finalSuccessIcon != null) { |
| | | copyButton.setIcon(finalSuccessIcon); |
| | | // 1秒后恢复原图标 |
| | | Timer timer = new Timer(1000, evt -> { |
| | | copyButton.setIcon(finalCopyIcon); |
| | | }); |
| | | timer.setRepeats(false); |
| | | timer.start(); |
| | | } |
| | | }); |
| | | |
| | | return copyButton; |
| | | } |
| | | |
| | | /** |
| | | * 复制文本到剪贴板 |
| | | */ |
| | | private void copyToClipboard(String text, String title) { |
| | | if (text == null || text.trim().isEmpty()) { |
| | | JOptionPane.showMessageDialog(this, title + " 未设置", "提示", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | StringSelection selection = new StringSelection(text); |
| | | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); |
| | | clipboard.setContents(selection, selection); |
| | | // 去掉成功提示弹窗 |
| | | } catch (Exception ex) { |
| | | JOptionPane.showMessageDialog(this, "复制失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |