张世豪
8 天以前 de75ec84e295c3f952a200897aa22aa73d7d5867
新增了串口割草机拖尾和缩放比例保存功能
已修改12个文件
480 ■■■■ 文件已修改
image/xia1.png 补丁 | 查看 | 原始文档 | blame | 历史
image/xia10.png 补丁 | 查看 | 原始文档 | blame | 历史
image/xia2.png 补丁 | 查看 | 原始文档 | blame | 历史
image/xia20.png 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/denglu/UserChuShiHua.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/gecaoji/Device.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/set/Setsys.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/yaokong/RemoteControlDialog.java 139 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/LegendDialog.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
image/xia1.png

image/xia10.png

image/xia2.png

image/xia20.png

set.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);