package zhuye; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.List; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.Timer; import dikuai.Dikuai; import gecaoji.Gecaoji; import gecaoji.lujingdraw; import publicway.buttonset; import zhangaiwu.Obstacledge; /** * 导航预览 - 在地图上预览导航路径和已割区域 */ public class daohangyulan { private static boolean active = false; private static Shouye shouyeInstance; private static Dikuai currentDikuai; private static Runnable returnCallback; // 导航模拟相关 private static List pathPoints = new ArrayList<>(); private static int currentPathIndex = 0; private static double currentSpeed = 1.0; // 默认1米/秒 private static Timer navigationTimer; private static double totalPathLength = 0.0; private static double traveledDistance = 0.0; private static List mowedTrack = new ArrayList<>(); // UI组件 private static JButton exitButton; private static JButton speedUpButton; private static JButton speedDownButton; private static JPanel navigationButtonPanel; private daohangyulan() { } /** * 启动导航预览 * @param shouye 主页面实例 * @param dikuai 地块数据 * @param callback 返回回调函数 */ public static void startNavigationPreview(Shouye shouye, Dikuai dikuai, Runnable callback) { if (shouye == null || dikuai == null) { return; } active = true; shouyeInstance = shouye; currentDikuai = dikuai; returnCallback = callback; // 首先获取路径坐标 String plannedPath = dikuai.getPlannedPath(); if (plannedPath == null || plannedPath.trim().isEmpty() || "-1".equals(plannedPath.trim())) { // 如果路径不存在,提示用户 SwingUtilities.invokeLater(() -> { javax.swing.JOptionPane.showMessageDialog(shouye, "无法获取有效的路径坐标,请先设置地块边界坐标和割草宽度,然后生成路径规划", "提示", javax.swing.JOptionPane.WARNING_MESSAGE); }); return; } // 解析路径坐标 pathPoints = parsePlannedPath(plannedPath); if (pathPoints.isEmpty()) { SwingUtilities.invokeLater(() -> { javax.swing.JOptionPane.showMessageDialog(shouye, "路径坐标解析失败,请检查路径数据", "提示", javax.swing.JOptionPane.WARNING_MESSAGE); }); return; } // 计算总路径长度 totalPathLength = calculatePathLength(pathPoints); traveledDistance = 0.0; currentPathIndex = 0; currentSpeed = 1.0; // 默认1米/秒 mowedTrack.clear(); // 获取地块数据 String landNumber = dikuai.getLandNumber(); String landName = dikuai.getLandName(); String boundary = dikuai.getBoundaryCoordinates(); // 获取障碍物坐标 String obstacles = getObstacleCoordinates(landNumber); // 获取割草宽度(转换为米) double mowingWidthMeters = getMowingWidthInMeters(dikuai); // 设置导航预览轨迹和宽度到MapRenderer MapRenderer renderer = shouye.getMapRenderer(); if (renderer != null) { renderer.setNavigationPreviewTrack(new ArrayList<>()); renderer.setNavigationPreviewWidth(mowingWidthMeters); } // 使用startMowingPathPreview来显示返回按钮和设置预览状态 Runnable returnAction = () -> { handleReturn(); }; shouye.startMowingPathPreview(landNumber, landName, boundary, obstacles, plannedPath, returnAction); // 更新导航预览状态显示(在作业状态左边,间隔10像素) shouye.updateNavigationPreviewStatus(true); // 创建并显示导航控制按钮 createNavigationButtons(shouye); // 初始化割草机位置到路径起点 if (!pathPoints.isEmpty()) { Point2D.Double startPoint = pathPoints.get(0); Gecaoji mower = renderer != null ? renderer.getMower() : null; if (mower != null) { mower.setPosition(startPoint.x, startPoint.y); } // 添加起点到已割轨迹 mowedTrack.add(new Point2D.Double(startPoint.x, startPoint.y)); if (renderer != null) { renderer.addNavigationPreviewTrackPoint(new Point2D.Double(startPoint.x, startPoint.y)); } } // 启动导航模拟定时器 startNavigationSimulation(shouye, renderer, mowingWidthMeters); // 刷新地图显示 shouye.repaint(); } /** * 停止导航预览 */ public static void stopNavigationPreview() { if (!active) { return; } active = false; // 停止导航模拟定时器 stopNavigationSimulation(); // 隐藏导航控制按钮 hideNavigationButtons(); // 清除导航预览轨迹 if (shouyeInstance != null) { MapRenderer renderer = shouyeInstance.getMapRenderer(); if (renderer != null) { renderer.clearNavigationPreviewTrack(); } // 更新导航预览状态显示 shouyeInstance.updateNavigationPreviewStatus(false); // 刷新地图显示 shouyeInstance.repaint(); } // 执行返回回调 if (returnCallback != null) { Runnable callback = returnCallback; returnCallback = null; SwingUtilities.invokeLater(callback); } shouyeInstance = null; currentDikuai = null; pathPoints.clear(); mowedTrack.clear(); } /** * 检查是否处于导航预览模式 */ public static boolean isActive() { return active; } /** * 处理返回按钮点击 */ public static void handleReturn() { stopNavigationPreview(); } /** * 创建导航控制按钮 */ private static void createNavigationButtons(Shouye shouye) { if (shouye == null) { return; } JPanel visualizationPanel = shouye.getVisualizationPanel(); if (visualizationPanel == null) { return; } // 创建按钮面板 navigationButtonPanel = new JPanel(); navigationButtonPanel.setLayout(new BoxLayout(navigationButtonPanel, BoxLayout.Y_AXIS)); navigationButtonPanel.setOpaque(false); navigationButtonPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); // 创建按钮 exitButton = createNavigationButton("退出"); exitButton.addActionListener(e -> handleReturn()); speedUpButton = createNavigationButton("加速"); speedUpButton.addActionListener(e -> { currentSpeed *= 2.0; updateSpeedDisplay(shouye); }); speedDownButton = createNavigationButton("减速"); speedDownButton.addActionListener(e -> { currentSpeed /= 2.0; if (currentSpeed < 0.1) { currentSpeed = 0.1; } updateSpeedDisplay(shouye); }); // 垂直排列 navigationButtonPanel.add(speedUpButton); navigationButtonPanel.add(Box.createRigidArea(new Dimension(0, 8))); navigationButtonPanel.add(speedDownButton); navigationButtonPanel.add(Box.createRigidArea(new Dimension(0, 8))); navigationButtonPanel.add(exitButton); // 设置面板大小 navigationButtonPanel.setPreferredSize(new Dimension(80, 120)); navigationButtonPanel.setSize(80, 120); // 添加到容器 visualizationPanel.add(navigationButtonPanel); visualizationPanel.setComponentZOrder(navigationButtonPanel, 0); // 初始化位置 SwingUtilities.invokeLater(() -> { updateButtonPositions(visualizationPanel); }); // 监听缩放/调整大小 visualizationPanel.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { updateButtonPositions(visualizationPanel); } }); } private static JButton createNavigationButton(String text) { return buttonset.createStyledButton(text, new Color(46, 139, 87)); } /** * 更新按钮位置 - 修改此处以匹配图中红框位置 */ private static void updateButtonPositions(Component panel) { if (navigationButtonPanel == null || panel == null) { return; } int panelWidth = panel.getWidth(); int panelHeight = panel.getHeight(); if (panelWidth <= 0 || panelHeight <= 0) { Component parent = panel.getParent(); if (parent != null) { if (panelWidth <= 0) { panelWidth = parent.getWidth(); } if (panelHeight <= 0) { panelHeight = parent.getHeight(); } } if (panelWidth <= 0) { panelWidth = 800; // 默认宽度 } if (panelHeight <= 0) { panelHeight = 600; // 默认高度 } } // 按钮面板尺寸 int buttonPanelWidth = 80; int buttonPanelHeight = 120; // 右下角位置:距离右边20像素,距离底部20像素(在缩放文字上方) // 缩放控制面板在右下角,有20像素的右边距和底部边距 // 按钮面板放在缩放控制面板左侧,避免重叠 int x = panelWidth - buttonPanelWidth - 20; // 距离右边20像素 // Y坐标计算说明: // 按钮面板高度为 120。 // 为了避开底部的“缩放: 3.91x”文字(该文字通常在底部向上20-30像素处), // 我们将按钮面板放在距离底部约 50-60 像素的位置。 // y = 总高度 - 按钮面板高度 - 底部预留空间 int y = panelHeight - buttonPanelHeight - 20; // 距离底部20像素 navigationButtonPanel.setBounds(x, y, buttonPanelWidth, buttonPanelHeight); navigationButtonPanel.setVisible(true); // 确保层级在最前 Component buttonParent = navigationButtonPanel.getParent(); if (buttonParent instanceof java.awt.Container) { ((java.awt.Container) buttonParent).setComponentZOrder(navigationButtonPanel, 0); } } private static void hideNavigationButtons() { if (navigationButtonPanel != null) { navigationButtonPanel.setVisible(false); if (navigationButtonPanel.getParent() != null) { navigationButtonPanel.getParent().remove(navigationButtonPanel); } } exitButton = null; speedUpButton = null; speedDownButton = null; navigationButtonPanel = null; } private static void startNavigationSimulation(Shouye shouye, MapRenderer renderer, double mowingWidthMeters) { if (pathPoints.isEmpty() || renderer == null) return; if (navigationTimer != null && navigationTimer.isRunning()) { navigationTimer.stop(); } navigationTimer = new Timer(100, e -> { if (!active) { stopNavigationSimulation(); return; } if (currentPathIndex >= pathPoints.size() - 1) { stopNavigationSimulation(); return; } updateNavigation(shouye, renderer, mowingWidthMeters); }); navigationTimer.start(); } private static void stopNavigationSimulation() { if (navigationTimer != null) { navigationTimer.stop(); navigationTimer = null; } } private static void updateNavigation(Shouye shouye, MapRenderer renderer, double mowingWidthMeters) { if (pathPoints.isEmpty() || currentPathIndex >= pathPoints.size() - 1) { stopNavigationSimulation(); return; } double deltaTime = 0.1; double moveDistance = currentSpeed * deltaTime; Point2D.Double currentPos = pathPoints.get(currentPathIndex); Point2D.Double nextPos = pathPoints.get(currentPathIndex + 1); double segmentLength = Math.hypot(nextPos.x - currentPos.x, nextPos.y - currentPos.y); if (segmentLength < 0.001) { currentPathIndex++; return; } Gecaoji mower = renderer.getMower(); if (moveDistance >= segmentLength) { currentPathIndex++; traveledDistance += segmentLength; if (mower != null && currentPathIndex < pathPoints.size()) { Point2D.Double newPos = pathPoints.get(currentPathIndex); mower.setPosition(newPos.x, newPos.y); mowedTrack.add(new Point2D.Double(newPos.x, newPos.y)); renderer.addNavigationPreviewTrackPoint(new Point2D.Double(newPos.x, newPos.y)); } } else { double ratio = moveDistance / segmentLength; double newX = currentPos.x + (nextPos.x - currentPos.x) * ratio; double newY = currentPos.y + (nextPos.y - currentPos.y) * ratio; if (mower != null) { mower.setPosition(newX, newY); if (mowedTrack.isEmpty() || Math.hypot(newX - mowedTrack.get(mowedTrack.size() - 1).x, newY - mowedTrack.get(mowedTrack.size() - 1).y) > 0.05) { mowedTrack.add(new Point2D.Double(newX, newY)); renderer.addNavigationPreviewTrackPoint(new Point2D.Double(newX, newY)); } } traveledDistance += moveDistance; } updateProgressAndSpeed(shouye, renderer, mowingWidthMeters); shouye.repaint(); } private static void updateProgressAndSpeed(Shouye shouye, MapRenderer renderer, double mowingWidthMeters) { if (shouye == null || renderer == null) return; double completedArea = traveledDistance * mowingWidthMeters; double totalArea = renderer.getTotalLandAreaSqMeters(); if (totalArea <= 0 && currentDikuai != null) { try { String areaStr = currentDikuai.getLandArea(); if (areaStr != null) totalArea = Double.parseDouble(areaStr.trim()); } catch (Exception e) {} } if (totalArea > 0) { double percentage = Math.max(0.0, Math.min(100.0, (completedArea / totalArea) * 100.0)); shouye.updateMowingProgress(percentage, completedArea, totalArea); } shouye.updateMowerSpeed(currentSpeed * 3.6); } private static void updateSpeedDisplay(Shouye shouye) { if (shouye != null) shouye.updateMowerSpeed(currentSpeed * 3.6); } private static double calculatePathLength(List points) { double total = 0.0; for (int i = 0; i < points.size() - 1; i++) { total += Math.hypot(points.get(i+1).x - points.get(i).x, points.get(i+1).y - points.get(i).y); } return total; } private static List parsePlannedPath(String plannedPath) { List points = lujingdraw.parsePlannedPath(plannedPath); return points != null ? points : new ArrayList<>(); } private static double getMowingWidthInMeters(Dikuai dikuai) { if (dikuai == null) return 0.4; try { String bladeWidth = dikuai.getMowingBladeWidth(); if (bladeWidth != null && !"-1".equals(bladeWidth)) return Double.parseDouble(bladeWidth); String mowWidth = dikuai.getMowingWidth(); if (mowWidth != null && !"-1".equals(mowWidth)) { double val = Double.parseDouble(mowWidth); return val > 10 ? val / 100.0 : val; } } catch (Exception e) {} return 0.4; } private static String getObstacleCoordinates(String landNumber) { if (landNumber == null || "-1".equals(landNumber)) return null; try { java.io.File configFile = new java.io.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; List obsList = plot.getObstacles(); if (obsList == null) return null; StringBuilder sb = new StringBuilder(); for (Obstacledge.Obstacle obs : obsList) { String coords = obs.getXyCoordsString(); if (coords != null && !"-1".equals(coords)) { if (sb.length() > 0) sb.append(" "); sb.append("(").append(coords).append(")"); } } return sb.length() > 0 ? sb.toString() : null; } catch (Exception e) { return null; } } }