| image/xia1.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| image/xia10.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| image/xia2.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| image/xia20.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| set.properties | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/denglu/UserChuShiHua.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/gecaoji/Device.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/set/Setsys.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/yaokong/RemoteControlDialog.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/zhuye/LegendDialog.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/zhuye/MapRenderer.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/zhuye/Shouye.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
image/xia1.pngimage/xia10.pngimage/xia2.pngimage/xia20.pngset.properties
@@ -1,11 +1,12 @@ #Serial Port Preferences Updated #Mon Dec 15 15:45:14 CST 2025 #Mower Configuration Properties - Updated #Mon Dec 15 19:36:26 CST 2025 appVersion=-1 currentWorkLandNumber=LAND1 cuttingWidth=200 firmwareVersion=-1 handheldMarkerId= idleTrailDurationSeconds=60 mapScale=41.66666666666667 mowerId=1234 serialAutoConnect=true serialBaudRate=115200 src/denglu/UserChuShiHua.java
@@ -28,7 +28,6 @@ try (OutputStream output = new FileOutputStream(FILE_PATH)) { userProperties.store(output, "Updated User Properties"); System.out.println("属性 " + key + " 已更新为: " + value); } catch (IOException e) { System.err.println("更新失败,文件写入错误: " + e.getMessage()); } src/gecaoji/Device.java
@@ -467,6 +467,30 @@ GupdateTime = String.valueOf(System.currentTimeMillis()); updateRelativeCoordinates(latitudeValue, latitudeHemisphere, longitudeValue, longitudeHemisphere); // 串口收到GNGGA数据后,触发拖尾更新 notifyMowerTrailUpdate(); } /** * 通知地图渲染器更新割草机拖尾 * 当串口收到GNGGA数据并更新位置后调用 */ private void notifyMowerTrailUpdate() { try { // 通过Shouye.getInstance()获取实例,避免循环依赖 zhuye.Shouye shouye = zhuye.Shouye.getInstance(); if (shouye != null) { zhuye.MapRenderer mapRenderer = shouye.getMapRenderer(); if (mapRenderer != null) { // 调用更新拖尾方法 mapRenderer.forceUpdateIdleMowerTrail(); } } } catch (Exception e) { // 如果调用失败,静默处理(不影响主要功能) // System.err.println("通知拖尾更新失败: " + e.getMessage()); } } private void updateRelativeCoordinates(String latValue, String latHemisphere, src/set/Setsys.java
@@ -145,9 +145,12 @@ this.idleTrailDurationSeconds = durationSeconds; value = String.valueOf(durationSeconds); break; case "mapScale": // mapScale不需要在内存中存储,直接更新到文件 break; default: System.err.println("未知的属性名: " + propertyName); return false; // 对于其他属性,也允许直接更新到文件(不打印错误) break; } // 更新properties文件 @@ -173,7 +176,6 @@ // 写回文件 try (FileOutputStream output = new FileOutputStream(PROPERTIES_FILE)) { props.store(output, "Mower Configuration Properties - Updated"); System.out.println("属性 " + propertyName + " 已更新为: " + value); return true; } catch (IOException e) { System.err.println("更新属性文件失败: " + e.getMessage()); src/yaokong/RemoteControlDialog.java
@@ -25,6 +25,9 @@ private Timer steeringControlTimer; // 转向控制定时器 private int targetForwardSpeed = 0; // 目标前进/后退速度 private int targetSteeringSpeed = 0; // 目标转向速度 // 独立跟踪每个摇杆的当前速度,避免相互影响 private int independentForwardSpeed = 0; // 独立的前进速度(不受转向摇杆影响) private int independentSteeringSpeed = 0; // 独立的转向速度(不受前进摇杆影响) private List<JButton> bladeButtons = new ArrayList<>(); // 存储刀盘控制按钮,用于清理定时器 private String bladeUpDefaultText = "刀盘升"; // 刀盘升按钮默认文字 private String bladeDownDefaultText = "刀盘降"; // 刀盘降按钮默认文字 @@ -158,27 +161,38 @@ moveJoystick.setJoystickListener(new JoystickListener() { @Override public void onJoystickMoved(double x, double y) { // 只使用Y轴控制前进后退,向上(北)为正 // 计算并四舍五入到整数速度值,正为前进,负为后退 // 只使用Y轴控制前进后退 // y值范围:-1.0(向上)到 1.0(向下) // 计算速度值:向下(y>0)为后退(负值),向上(y<0)为前进(正值) // 后退值范围:0 到 -100,前进值范围:0 到 100 int forwardVal = (int) Math.round(-y * 100.0); // 限制在 [-100, 100] forwardVal = Math.max(-100, Math.min(100, forwardVal)); // 更新目标速度 targetForwardSpeed = forwardVal; if (Math.abs(y) > 0.1) { // 摇杆不在中心位置,启动持续发送定时器 startForwardControlTimer(); } else { // 摇杆回到中心位置,停止发送 stopForwardControlTimer(); stopForward(); // 死区处理:如果速度值在-10到10之间,视为0(避免微小抖动) if (Math.abs(forwardVal) <= 10) { forwardVal = 0; } // 更新顶部显示(移动显示当前前进/后退速度,转向取当前转向速度作为参考) int steeringVal = Control03.getCurrentSteeringSpeed(); updateJoystickValues(forwardVal, steeringVal); // 更新目标速度和独立速度 targetForwardSpeed = forwardVal; independentForwardSpeed = forwardVal; if (forwardVal != 0) { // 摇杆不在死区,启动持续发送定时器 // 后退时 forwardVal 为负值(-100到-11),前进时 forwardVal 为正值(11到100) startForwardControlTimer(); } else { // 摇杆在死区或中心位置,停止定时器 stopForwardControlTimer(); // 将独立的前进速度设置为0 independentForwardSpeed = 0; // 发送停止指令(保持转向速度不变) Control03.setAndSendSpeeds(independentSteeringSpeed, 0); } // 更新顶部显示(使用独立的速度值) updateJoystickValues(forwardVal, independentSteeringSpeed); } }); // 转向摇杆(蓝色主题) @@ -187,26 +201,37 @@ turnJoystick.setJoystickListener(new JoystickListener() { @Override public void onJoystickMoved(double x, double y) { // 只使用X轴控制左右转向,向右为正 // 计算并四舍五入到整数转向值,正为右转,负为左转 // 只使用X轴控制左右转向 // x值范围:-1.0(向左)到 1.0(向右) // 计算转向值:向左(x<0)为左转(负值),向右(x>0)为右转(正值) // 左转值范围:0 到 -100,右转值范围:0 到 100 int steeringVal = (int) Math.round(x * 100.0); steeringVal = Math.max(-100, Math.min(100, steeringVal)); // 更新目标速度 targetSteeringSpeed = steeringVal; if (Math.abs(x) > 0.1) { // 摇杆不在中心位置,启动持续发送定时器 startSteeringControlTimer(); } else { // 摇杆回到中心位置,停止发送 stopSteeringControlTimer(); stopSteering(); // 死区处理:如果速度值在-10到10之间,视为0(避免微小抖动) if (Math.abs(steeringVal) <= 10) { steeringVal = 0; } // 更新顶部显示(转向显示当前转向速度,移动显示当前前进速度) int forwardVal = Control03.getCurrentForwardSpeed(); updateJoystickValues(forwardVal, steeringVal); // 更新目标速度和独立速度 targetSteeringSpeed = steeringVal; independentSteeringSpeed = steeringVal; if (steeringVal != 0) { // 摇杆不在死区,启动持续发送定时器 // 左转时 steeringVal 为负值(-100到-11),右转时 steeringVal 为正值(11到100) startSteeringControlTimer(); } else { // 摇杆在死区或中心位置,停止定时器 stopSteeringControlTimer(); // 将独立的转向速度设置为0 independentSteeringSpeed = 0; // 发送停止指令(保持前进速度不变) Control03.setAndSendSpeeds(0, independentForwardSpeed); } // 更新顶部显示(使用独立的速度值) updateJoystickValues(independentForwardSpeed, steeringVal); } }); joystickPanel.add(moveJoystick); @@ -625,14 +650,9 @@ } private void stopForward() { if (Control03.getCurrentForwardSpeed() != 0) { boolean success = Control03.approachForwardSpeedToZero(20); if (!success) { showSerialClosedWarning(); return; } serialWarningShown = false; } // 将独立的前进速度设置为0,保持转向速度不变 independentForwardSpeed = 0; Control03.setAndSendSpeeds(independentSteeringSpeed, 0); } private void applySteeringSpeed(int speed) { @@ -655,14 +675,9 @@ } private void stopSteering() { if (Control03.getCurrentSteeringSpeed() != 0) { boolean success = Control03.approachSteeringSpeedToZero(25); if (!success) { showSerialClosedWarning(); return; } serialWarningShown = false; } // 将独立的转向速度设置为0,保持前进速度不变 independentSteeringSpeed = 0; Control03.setAndSendSpeeds(0, independentForwardSpeed); } /** @@ -729,36 +744,24 @@ * 持续发送前进/后退速度指令 */ private void applyForwardSpeedContinuously(int targetSpeed) { int currentSpeed = Control03.getCurrentForwardSpeed(); int currentSteeringSpeed = Control03.getCurrentSteeringSpeed(); // 更新独立的前进速度 independentForwardSpeed = targetSpeed; // 如果已经达到目标速度,直接发送一次以保持状态 if (currentSpeed == targetSpeed) { // 直接发送目标速度指令以保持状态(即使速度相同也要发送) Control03.setAndSendSpeeds(currentSteeringSpeed, targetSpeed); } else { // 逐步调整到目标速度 int delta = targetSpeed > currentSpeed ? 10 : -10; Control03.adjustForwardSpeed(delta); } // 使用独立的转向速度(不受前进摇杆影响),只更新前进速度 // 这样前进摇杆的操作不会影响转向速度 Control03.setAndSendSpeeds(independentSteeringSpeed, independentForwardSpeed); } /** * 持续发送转向速度指令 */ private void applySteeringSpeedContinuously(int targetSpeed) { int currentSpeed = Control03.getCurrentSteeringSpeed(); int currentForwardSpeed = Control03.getCurrentForwardSpeed(); // 更新独立的转向速度 independentSteeringSpeed = targetSpeed; // 如果已经达到目标速度,直接发送一次以保持状态 if (currentSpeed == targetSpeed) { // 直接发送目标速度指令以保持状态(即使速度相同也要发送) Control03.setAndSendSpeeds(targetSpeed, currentForwardSpeed); } else { // 逐步调整到目标速度 int delta = targetSpeed > currentSpeed ? 15 : -15; Control03.adjustSteeringSpeed(delta); } // 使用独立的前进速度(不受转向摇杆影响),只更新转向速度 // 这样转向摇杆的操作不会影响前进速度 Control03.setAndSendSpeeds(independentSteeringSpeed, independentForwardSpeed); } // 更新顶部显示的摇杆数值(在 EDT 上调用),文字根据数值映射为方向描述 src/zhuye/LegendDialog.java
@@ -32,6 +32,29 @@ mainPanel.setBackground(Color.WHITE); mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 10, 15)); // 计算图例内容面板的宽度(用于设置图标尺寸) // 图例对话框宽度 = DIALOG_WIDTH * 0.8 // 主面板左右边框各15像素,图例内容面板左右内边距各10像素 int adjustedWidth = (int) Math.round(UIConfig.DIALOG_WIDTH * 0.8); int iconSize = adjustedWidth - 30 - 20; // 减去主面板左右边框(15*2)和图例内容面板左右内边距(10*2) // 创建割草机图标面板 JPanel iconPanel = new JPanel(new BorderLayout()); iconPanel.setBackground(Color.WHITE); iconPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); // 底部间距10像素 JLabel gecaojiLabel = new JLabel(); gecaojiLabel.setHorizontalAlignment(SwingConstants.CENTER); ImageIcon gecaojiIcon = loadIcon("image/gecaoji.png", iconSize, iconSize); if (gecaojiIcon != null) { gecaojiLabel.setIcon(gecaojiIcon); } else { // 如果图标加载失败,显示占位文本 gecaojiLabel.setText("割草机图标"); gecaojiLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12)); } iconPanel.add(gecaojiLabel, BorderLayout.CENTER); // 图例内容面板 - 直接添加,不使用滚动条 JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); @@ -61,7 +84,8 @@ contentPanel.remove(contentPanel.getComponentCount() - 1); } // 直接添加内容面板,不使用滚动条 // 添加图标面板和图例内容面板 mainPanel.add(iconPanel, BorderLayout.NORTH); mainPanel.add(contentPanel, BorderLayout.CENTER); getContentPane().add(mainPanel); @@ -154,4 +178,37 @@ return itemPanel; } /** * 加载并缩放图标 * @param iconPath 图标路径 * @param width 目标宽度 * @param height 目标高度 * @return 缩放后的图标 */ private ImageIcon loadIcon(String iconPath, int width, int height) { try { java.net.URL imgURL = getClass().getClassLoader().getResource(iconPath); if (imgURL == null) { // 尝试从文件系统加载 java.io.File imgFile = new java.io.File(iconPath); if (imgFile.exists()) { ImageIcon originalIcon = new ImageIcon(imgFile.getAbsolutePath()); Image scaledImage = originalIcon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH); ImageIcon scaledIcon = new ImageIcon(scaledImage); scaledIcon.setDescription(iconPath); return scaledIcon; } } else { ImageIcon originalIcon = new ImageIcon(imgURL); Image scaledImage = originalIcon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH); ImageIcon scaledIcon = new ImageIcon(scaledImage); scaledIcon.setDescription(iconPath); return scaledIcon; } } catch (Exception e) { System.err.println("无法加载图标: " + iconPath + " - " + e.getMessage()); } return null; } } src/zhuye/MapRenderer.java
@@ -18,6 +18,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; import set.Setsys; import gecaoji.Device; import gecaoji.Gecaoji; import gecaoji.GecaojiMeg; @@ -35,13 +36,15 @@ */ public class MapRenderer { // 视图变换参数 private double scale = 1.0; private static final double DEFAULT_SCALE = 20.0; // 默认缩放比例 private double scale = DEFAULT_SCALE; private double translateX = 0.0; private double translateY = 0.0; private Point lastDragPoint; private static final double MIN_SCALE = 0.05d; private static final double MAX_SCALE = 50.0d; private static final double SCALE_EPSILON = 1e-6d; private static final String MAP_SCALE_PROPERTY = "mapScale"; // 属性文件中的键名 // 主题颜色 private final Color THEME_COLOR = new Color(46, 139, 87); @@ -111,6 +114,40 @@ this.mowerUpdateTimer = createMowerTimer(); this.mowerInfoManager = new GecaojiMeg(visualizationPanel, mower); setupMouseListeners(); // 从配置文件读取上次保存的缩放比例 loadScaleFromProperties(); } /** * 从配置文件读取缩放比例 */ private void loadScaleFromProperties() { String scaleValue = Setsys.getPropertyValue(MAP_SCALE_PROPERTY); if (scaleValue != null && !scaleValue.trim().isEmpty()) { try { double savedScale = Double.parseDouble(scaleValue.trim()); // 验证缩放比例是否在有效范围内 if (savedScale >= MIN_SCALE && savedScale <= MAX_SCALE) { scale = savedScale; } else { scale = DEFAULT_SCALE; } } catch (NumberFormatException e) { // 如果解析失败,使用默认值 scale = DEFAULT_SCALE; } } else { // 如果没有保存的值,使用默认值 scale = DEFAULT_SCALE; } } /** * 保存缩放比例到配置文件 */ private void saveScaleToProperties() { Setsys setsys = new Setsys(); setsys.updateProperty(MAP_SCALE_PROPERTY, String.valueOf(scale)); } /** @@ -218,6 +255,8 @@ translateX += (newWorldX - worldX); translateY += (newWorldY - worldY); // 保存缩放比例到配置文件 saveScaleToProperties(); visualizationPanel.repaint(); } @@ -253,9 +292,11 @@ * 重置视图 */ public void resetView() { scale = 1.0; scale = DEFAULT_SCALE; translateX = 0.0; translateY = 0.0; // 保存缩放比例到配置文件 saveScaleToProperties(); visualizationPanel.repaint(); } @@ -482,7 +523,8 @@ if (device == null) { return; } if (!isHighPrecisionFix(device.getPositioningStatus())) { // 使用更宽松的定位状态判断,允许状态1和4显示拖尾 if (!isValidFixForTrail(device.getPositioningStatus())) { return; } @@ -504,6 +546,56 @@ idleMowerTrail.addLast(new tuowei.TrailSample(now, new Point2D.Double(position.x, position.y))); pruneIdleMowerTrail(now); } /** * 强制更新拖尾(用于收到$GNGGA数据时立即更新) * 这个方法会刷新mower位置并立即添加到拖尾 */ public void forceUpdateIdleMowerTrail() { long now = System.currentTimeMillis(); pruneIdleMowerTrail(now); if (idleTrailSuppressed || realtimeTrackRecording) { if (!idleMowerTrail.isEmpty()) { clearIdleMowerTrail(); } return; } Device device = Device.getGecaoji(); if (device == null) { return; } // 使用更宽松的定位状态判断,允许状态1和4显示拖尾 if (!isValidFixForTrail(device.getPositioningStatus())) { return; } // 刷新mower位置,使用最新的Device数据 mower.refreshFromDevice(); Point2D.Double position = mower.getPosition(); if (position == null || !Double.isFinite(position.x) || !Double.isFinite(position.y)) { return; } tuowei.TrailSample lastSample = idleMowerTrail.peekLast(); if (lastSample != null) { Point2D.Double lastPoint = lastSample.getPoint(); double dx = position.x - lastPoint.x; double dy = position.y - lastPoint.y; if (Math.hypot(dx, dy) < IDLE_TRAIL_SAMPLE_DISTANCE_METERS) { return; } } idleMowerTrail.addLast(new tuowei.TrailSample(now, new Point2D.Double(position.x, position.y))); pruneIdleMowerTrail(now); // 立即重绘,确保拖尾及时显示 if (visualizationPanel != null) { visualizationPanel.repaint(); } } private void pruneIdleMowerTrail(long now) { if (idleMowerTrail.isEmpty()) { @@ -1233,6 +1325,31 @@ return false; } } /** * 判断定位状态是否有效,可用于显示拖尾 * 接受状态1(单点定位)和4(固定解) */ private boolean isValidFixForTrail(String fixQuality) { if (fixQuality == null) { return false; } String trimmed = fixQuality.trim(); if (trimmed.isEmpty()) { return false; } // 接受状态1(单点定位)和4(固定解) if ("1".equals(trimmed) || "4".equals(trimmed)) { return true; } try { double value = Double.parseDouble(trimmed); // 接受1.0或4.0 return Math.abs(value - 1.0d) < 1e-6 || Math.abs(value - 4.0d) < 1e-6; } catch (NumberFormatException ex) { return false; } } private boolean isPointInsideActiveBoundary(Point2D.Double point) { if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { @@ -1339,7 +1456,15 @@ * 设置视图变换参数(用于程序化控制) */ public void setViewTransform(double scale, double translateX, double translateY) { this.scale = scale; // 限制缩放范围 scale = Math.max(MIN_SCALE, Math.min(scale, MAX_SCALE)); // 如果缩放比例改变了,保存到配置文件 if (Math.abs(this.scale - scale) > SCALE_EPSILON) { this.scale = scale; saveScaleToProperties(); } else { this.scale = scale; } this.translateX = translateX; this.translateY = translateY; visualizationPanel.repaint(); src/zhuye/Shouye.java
@@ -91,10 +91,20 @@ private Sets settingsDialog; private BaseStation baseStation; private final Consumer<String> serialLineListener = line -> SwingUtilities.invokeLater(this::updateDataPacketCountLabel); // 地图渲染器 private MapRenderer mapRenderer; private final Consumer<String> serialLineListener = line -> { SwingUtilities.invokeLater(() -> { updateDataPacketCountLabel(); // 如果收到$GNGGA数据,立即更新拖尾 if (line != null && line.trim().startsWith("$GNGGA")) { if (mapRenderer != null) { mapRenderer.forceUpdateIdleMowerTrail(); } } }); }; private static final int FLOAT_ICON_SIZE = 32; private JButton endDrawingButton; private JButton drawingPauseButton; @@ -208,12 +218,42 @@ SwingUtilities.invokeLater(() -> { Shouye.this.checkIdentifiersAndPromptIfNeeded(); Shouye.this.showInitialMowerSelfCheckDialogIfNeeded(); // 设置窗口关闭监听器,在关闭时保存缩放比例 setupWindowCloseListener(); }); } } }; addHierarchyListener(listener); } /** * 设置窗口关闭监听器,在窗口关闭时保存当前缩放比例 */ private void setupWindowCloseListener() { Window window = SwingUtilities.getWindowAncestor(this); if (window != null && window instanceof JFrame) { JFrame frame = (JFrame) window; frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { // 保存当前缩放比例 saveCurrentScale(); } }); } } /** * 保存当前地图缩放比例到配置文件 */ public void saveCurrentScale() { if (mapRenderer != null) { double currentScale = mapRenderer.getScale(); Setsys setsys = new Setsys(); setsys.updateProperty("mapScale", String.valueOf(currentScale)); } } private void showInitialMowerSelfCheckDialogIfNeeded() { // 已移除进入主页时的自检提示(按用户要求删除) @@ -352,6 +392,37 @@ // 可视化区域 - 使用MapRenderer进行绘制 visualizationPanel = new JPanel() { private ImageIcon gecaojiIcon = null; private static final int GECAOJI_ICON_X = 37; private static final int GECAOJI_ICON_Y = 10; private static final int GECAOJI_ICON_SIZE = 20; { // 加载割草机图标,大小20x20像素 gecaojiIcon = loadScaledIcon("image/gecaoji.png", GECAOJI_ICON_SIZE, GECAOJI_ICON_SIZE); // 启用工具提示 setToolTipText(""); } /** * 检查鼠标位置是否在割草机图标区域内 */ private boolean isMouseOnGecaojiIcon(Point mousePoint) { return mousePoint.x >= GECAOJI_ICON_X && mousePoint.x <= GECAOJI_ICON_X + GECAOJI_ICON_SIZE && mousePoint.y >= GECAOJI_ICON_Y && mousePoint.y <= GECAOJI_ICON_Y + GECAOJI_ICON_SIZE; } @Override public String getToolTipText(MouseEvent event) { // 如果鼠标在割草机图标区域内,显示提示文字 if (isMouseOnGecaojiIcon(event.getPoint())) { return "以割草机为中心"; } return super.getToolTipText(event); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); @@ -359,14 +430,48 @@ if (mapRenderer != null) { mapRenderer.renderMap(g); } // 在地图左上角绘制割草机图标 // 水平方向与速度指示器对齐(x=37) // 垂直方向与卫星状态图标对齐(y=10,速度指示器面板顶部边距10像素,使图标中心对齐) if (gecaojiIcon != null) { g.drawImage(gecaojiIcon.getImage(), GECAOJI_ICON_X, GECAOJI_ICON_Y, null); } } }; visualizationPanel.setLayout(new BorderLayout()); // 添加鼠标点击监听器,检测是否点击了割草机图标 visualizationPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) { Point clickPoint = e.getPoint(); // 检查是否点击了割草机图标区域(37, 10, 20, 20) if (clickPoint.x >= 37 && clickPoint.x <= 57 && clickPoint.y >= 10 && clickPoint.y <= 30) { // 点击了割草机图标,将地图视图中心移动到割草机位置 if (mapRenderer != null) { Gecaoji mower = mapRenderer.getMower(); if (mower != null && mower.hasValidPosition()) { Point2D.Double mowerPosition = mower.getPosition(); if (mowerPosition != null) { // 获取当前缩放比例 double currentScale = mapRenderer.getScale(); // 设置视图变换,使割草机位置对应到屏幕中心 // translateX = -mowerX, translateY = -mowerY 可以让割草机在屏幕中心 mapRenderer.setViewTransform(currentScale, -mowerPosition.x, -mowerPosition.y); } } } } } } }); JPanel speedIndicatorPanel = createSpeedIndicatorPanel(); visualizationPanel.add(speedIndicatorPanel, BorderLayout.NORTH); // 创建功能按钮面板(放在左上角) // 创建功能按钮面板 JPanel functionButtonsPanel = new JPanel(); functionButtonsPanel.setLayout(new BoxLayout(functionButtonsPanel, BoxLayout.Y_AXIS)); functionButtonsPanel.setOpaque(false);