de75ec84e295c3f952a200897aa22aa73d7d5867..68b1f4e85c29164d5de189262282454f9a0b1cc0
7 天以前 张世豪
以割草机视图为中心显示功能
68b1f4 对比 | 目录
7 天以前 张世豪
保存地图最后位置
f78446 对比 | 目录
已修改4个文件
333 ■■■■ 文件已修改
set.properties 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/set/Sets.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 239 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
set.properties
@@ -1,14 +1,16 @@
#Mower Configuration Properties - Updated
#Mon Dec 15 19:36:26 CST 2025
#Tue Dec 16 16:27:10 CST 2025
appVersion=-1
currentWorkLandNumber=LAND1
cuttingWidth=200
firmwareVersion=-1
handheldMarkerId=
idleTrailDurationSeconds=60
mapScale=41.66666666666667
mapScale=20.09
mowerId=1234
serialAutoConnect=true
serialBaudRate=115200
serialPortName=COM15
simCardNumber=-1
viewCenterX=0.55
viewCenterY=12.53
src/set/Sets.java
@@ -5,6 +5,7 @@
import zhuye.MapRenderer;
import zhuye.Shouye;
import zhuye.buttonset;
import set.Setsys;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
@@ -333,6 +334,48 @@
        setData.initializeFromProperties();
        baseStation.load();
        updateDisplay();
        // 加载并应用上次保存的视图中心坐标
        loadViewCenterFromProperties();
    }
    /**
     * 从配置文件加载视图中心坐标并应用到MapRenderer
     */
    private void loadViewCenterFromProperties() {
        Shouye shouye = Shouye.getInstance();
        if (shouye == null) {
            return;
        }
        MapRenderer renderer = shouye.getMapRenderer();
        if (renderer == null) {
            return;
        }
        // 从配置文件读取视图中心坐标
        String viewCenterXValue = Setsys.getPropertyValue("viewCenterX");
        String viewCenterYValue = Setsys.getPropertyValue("viewCenterY");
        double savedTranslateX = 0.0;
        double savedTranslateY = 0.0;
        if (viewCenterXValue != null && !viewCenterXValue.trim().isEmpty()) {
            try {
                savedTranslateX = Double.parseDouble(viewCenterXValue.trim());
            } catch (NumberFormatException e) {
                savedTranslateX = 0.0;
            }
        }
        if (viewCenterYValue != null && !viewCenterYValue.trim().isEmpty()) {
            try {
                savedTranslateY = Double.parseDouble(viewCenterYValue.trim());
            } catch (NumberFormatException e) {
                savedTranslateY = 0.0;
            }
        }
        // 应用视图中心坐标(保持当前缩放比例)
        double currentScale = renderer.getScale();
        renderer.setViewTransform(currentScale, savedTranslateX, savedTranslateY);
    }
    
    private void updateDisplay() {
src/zhuye/MapRenderer.java
@@ -114,14 +114,15 @@
        this.mowerUpdateTimer = createMowerTimer();
        this.mowerInfoManager = new GecaojiMeg(visualizationPanel, mower);
        setupMouseListeners();
        // 从配置文件读取上次保存的缩放比例
        loadScaleFromProperties();
        // 从配置文件读取上次保存的缩放比例和视图中心坐标
        loadViewSettingsFromProperties();
    }
    
    /**
     * 从配置文件读取缩放比例
     * 从配置文件读取缩放比例和视图中心坐标
     */
    private void loadScaleFromProperties() {
    private void loadViewSettingsFromProperties() {
        // 加载缩放比例
        String scaleValue = Setsys.getPropertyValue(MAP_SCALE_PROPERTY);
        if (scaleValue != null && !scaleValue.trim().isEmpty()) {
            try {
@@ -140,6 +141,28 @@
            // 如果没有保存的值,使用默认值
            scale = DEFAULT_SCALE;
        }
        // 加载视图中心坐标
        String viewCenterXValue = Setsys.getPropertyValue("viewCenterX");
        String viewCenterYValue = Setsys.getPropertyValue("viewCenterY");
        if (viewCenterXValue != null && !viewCenterXValue.trim().isEmpty()) {
            try {
                translateX = Double.parseDouble(viewCenterXValue.trim());
            } catch (NumberFormatException e) {
                translateX = 0.0;
            }
        } else {
            translateX = 0.0;
        }
        if (viewCenterYValue != null && !viewCenterYValue.trim().isEmpty()) {
            try {
                translateY = Double.parseDouble(viewCenterYValue.trim());
            } catch (NumberFormatException e) {
                translateY = 0.0;
            }
        } else {
            translateY = 0.0;
        }
    }
    
    /**
@@ -147,7 +170,8 @@
     */
    private void saveScaleToProperties() {
        Setsys setsys = new Setsys();
        setsys.updateProperty(MAP_SCALE_PROPERTY, String.valueOf(scale));
        // 保留2位小数
        setsys.updateProperty(MAP_SCALE_PROPERTY, String.format("%.2f", scale));
    }
    
    /**
@@ -1328,7 +1352,7 @@
    
    /**
     * 判断定位状态是否有效,可用于显示拖尾
     * 接受状态1(单点定位)和4(固定解)
     * 接受状态1(单点定位)、2(码差分)、3(无效PPS)、4(固定解)、5(浮点解)
     */
    private boolean isValidFixForTrail(String fixQuality) {
        if (fixQuality == null) {
@@ -1338,14 +1362,15 @@
        if (trimmed.isEmpty()) {
            return false;
        }
        // 接受状态1(单点定位)和4(固定解)
        if ("1".equals(trimmed) || "4".equals(trimmed)) {
        // 接受状态1,2,3,4,5(只要不是0或无效状态)
        if ("1".equals(trimmed) || "2".equals(trimmed) || "3".equals(trimmed) ||
            "4".equals(trimmed) || "5".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;
            // 接受1.0, 2.0, 3.0, 4.0, 5.0(只要不是0)
            return value >= 1.0 && value <= 5.0;
        } catch (NumberFormatException ex) {
            return false;
        }
src/zhuye/Shouye.java
@@ -78,6 +78,15 @@
    private JLabel statusLabel;
    private JLabel speedLabel;  // 速度显示标签
    private JLabel areaNameLabel;
    // 边界警告相关
    private Timer boundaryWarningTimer;  // 边界警告检查定时器
    private Timer warningBlinkTimer;  // 警告图标闪烁定时器
    private boolean isMowerOutsideBoundary = false;  // 割草机是否在边界外
    private boolean warningIconVisible = true;  // 警告图标显示状态(用于闪烁)
    // 以割草机为中心视图模式
    private boolean centerOnMowerMode = false;  // 是否处于以割草机为中心的模式
    // 当前选中的导航按钮
    private JButton currentNavButton;
@@ -207,6 +216,9 @@
        initializeDefaultAreaSelection();
        refreshMapForSelectedArea();
        // 启动边界警告检查定时器
        startBoundaryWarningTimer();
    }
    private void scheduleIdentifierCheck() {
@@ -239,19 +251,175 @@
                public void windowClosing(WindowEvent e) {
                    // 保存当前缩放比例
                    saveCurrentScale();
                    // 停止边界警告定时器
                    if (boundaryWarningTimer != null && boundaryWarningTimer.isRunning()) {
                        boundaryWarningTimer.stop();
                    }
                    // 停止闪烁定时器
                    if (warningBlinkTimer != null && warningBlinkTimer.isRunning()) {
                        warningBlinkTimer.stop();
                    }
                }
            });
        }
    }
    
    /**
     * 保存当前地图缩放比例到配置文件
     * 保存当前地图缩放比例和视图中心坐标到配置文件
     */
    public void saveCurrentScale() {
        if (mapRenderer != null) {
            double currentScale = mapRenderer.getScale();
            double translateX = mapRenderer.getTranslateX();
            double translateY = mapRenderer.getTranslateY();
            Setsys setsys = new Setsys();
            setsys.updateProperty("mapScale", String.valueOf(currentScale));
            // 保留2位小数
            setsys.updateProperty("mapScale", String.format("%.2f", currentScale));
            setsys.updateProperty("viewCenterX", String.format("%.2f", translateX));
            setsys.updateProperty("viewCenterY", String.format("%.2f", translateY));
        }
    }
    /**
     * 启动边界警告检查定时器
     */
    private void startBoundaryWarningTimer() {
        // 边界检查定时器:每500ms检查一次割草机是否在边界内
        boundaryWarningTimer = new Timer(500, e -> {
            checkMowerBoundaryStatus();
        });
        boundaryWarningTimer.setInitialDelay(0);
        boundaryWarningTimer.start();
        // 闪烁定时器:每1秒切换一次警告图标显示状态
        warningBlinkTimer = new Timer(1000, e -> {
            if (isMowerOutsideBoundary) {
                warningIconVisible = !warningIconVisible;
                if (visualizationPanel != null) {
                    visualizationPanel.repaint();
                }
            }
        });
        warningBlinkTimer.setInitialDelay(0);
        warningBlinkTimer.start();
    }
    /**
     * 切换以割草机为中心的模式
     */
    private void toggleCenterOnMowerMode() {
        centerOnMowerMode = !centerOnMowerMode;
        if (centerOnMowerMode) {
            // 开启模式:立即将视图中心移动到割草机位置
            updateViewToCenterOnMower();
        }
        // 关闭模式时不需要做任何操作,用户已经可以自由移动地图
        // 更新工具提示
        if (visualizationPanel != null) {
            visualizationPanel.repaint();
        }
    }
    /**
     * 更新视图中心到割草机位置
     */
    private void updateViewToCenterOnMower() {
        if (mapRenderer == null) {
            return;
        }
        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);
            }
        }
    }
    /**
     * 检查割草机边界状态
     */
    private void checkMowerBoundaryStatus() {
        // 如果处于以割草机为中心的模式,实时更新视图中心
        if (centerOnMowerMode) {
            updateViewToCenterOnMower();
        }
        // 检查是否在作业中
        if (statusLabel == null || !"作业中".equals(statusLabel.getText())) {
            // 不在作业中,重置状态
            if (isMowerOutsideBoundary) {
                isMowerOutsideBoundary = false;
                warningIconVisible = true;
                if (visualizationPanel != null) {
                    visualizationPanel.repaint();
                }
            }
            return;
        }
        // 在作业中,检查是否在边界内
        if (mapRenderer == null) {
            return;
        }
        // 获取当前边界
        List<Point2D.Double> boundary = mapRenderer.getCurrentBoundary();
        if (boundary == null || boundary.size() < 3) {
            // 没有边界,重置状态
            if (isMowerOutsideBoundary) {
                isMowerOutsideBoundary = false;
                warningIconVisible = true;
                if (visualizationPanel != null) {
                    visualizationPanel.repaint();
                }
            }
            return;
        }
        // 获取割草机位置
        Gecaoji mower = mapRenderer.getMower();
        if (mower == null || !mower.hasValidPosition()) {
            // 无法获取位置,重置状态
            if (isMowerOutsideBoundary) {
                isMowerOutsideBoundary = false;
                warningIconVisible = true;
                if (visualizationPanel != null) {
                    visualizationPanel.repaint();
                }
            }
            return;
        }
        Point2D.Double mowerPosition = mower.getPosition();
        if (mowerPosition == null) {
            return;
        }
        // 使用 MowerBoundaryChecker 检查是否在边界内
        boolean isInside = MowerBoundaryChecker.isInsideBoundaryPoints(
            boundary,
            mowerPosition.x,
            mowerPosition.y
        );
        // 更新状态
        boolean wasOutside = isMowerOutsideBoundary;
        isMowerOutsideBoundary = !isInside;
        // 如果状态改变,立即重绘
        if (wasOutside != isMowerOutsideBoundary) {
            warningIconVisible = true;
            if (visualizationPanel != null) {
                visualizationPanel.repaint();
            }
        }
    }
@@ -400,8 +568,6 @@
            {
                // 加载割草机图标,大小20x20像素
                gecaojiIcon = loadScaledIcon("image/gecaoji.png", GECAOJI_ICON_SIZE, GECAOJI_ICON_SIZE);
                // 启用工具提示
                setToolTipText("");
            }
            
            /**
@@ -418,9 +584,11 @@
            public String getToolTipText(MouseEvent event) {
                // 如果鼠标在割草机图标区域内,显示提示文字
                if (isMouseOnGecaojiIcon(event.getPoint())) {
                    return "以割草机为中心";
                    // 根据当前模式显示不同的提示文字
                    return centerOnMowerMode ? "取消以割草机为中心" : "以割草机为中心";
                }
                return super.getToolTipText(event);
                // 不在图标上时返回null,不显示工具提示框
                return null;
            }
            
            @Override
@@ -430,13 +598,48 @@
                if (mapRenderer != null) {
                    mapRenderer.renderMap(g);
                }
                // 在地图左上角绘制割草机图标
                // 水平方向与速度指示器对齐(x=37)
                // 垂直方向与卫星状态图标对齐(y=10,速度指示器面板顶部边距10像素,使图标中心对齐)
                if (gecaojiIcon != null) {
                // 检查是否需要显示警告图标
                if (isMowerOutsideBoundary && warningIconVisible) {
                    // 绘制红色三角形警告图标(带叹号)
                    drawWarningIcon(g, GECAOJI_ICON_X, GECAOJI_ICON_Y, GECAOJI_ICON_SIZE);
                } else if (gecaojiIcon != null) {
                    // 绘制正常的割草机图标
                    // 水平方向与速度指示器对齐(x=37)
                    // 垂直方向与卫星状态图标对齐(y=10,速度指示器面板顶部边距10像素,使图标中心对齐)
                    g.drawImage(gecaojiIcon.getImage(), GECAOJI_ICON_X, GECAOJI_ICON_Y, null);
                }
            }
            /**
             * 绘制红色三角形警告图标(带叹号)
             */
            private void drawWarningIcon(Graphics g, int x, int y, int size) {
                Graphics2D g2d = (Graphics2D) g.create();
                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                // 绘制红色三角形
                int[] xPoints = {x + size / 2, x, x + size};
                int[] yPoints = {y, y + size, y + size};
                g2d.setColor(Color.RED);
                g2d.fillPolygon(xPoints, yPoints, 3);
                // 绘制白色边框
                g2d.setColor(Color.WHITE);
                g2d.setStroke(new BasicStroke(1.5f));
                g2d.drawPolygon(xPoints, yPoints, 3);
                // 绘制白色叹号
                g2d.setColor(Color.WHITE);
                g2d.setFont(new Font("Arial", Font.BOLD, size * 3 / 4));
                FontMetrics fm = g2d.getFontMetrics();
                String exclamation = "!";
                int textWidth = fm.stringWidth(exclamation);
                int textHeight = fm.getAscent();
                g2d.drawString(exclamation, x + (size - textWidth) / 2, y + (size + textHeight) / 2 - 2);
                g2d.dispose();
            }
        };
        visualizationPanel.setLayout(new BorderLayout());
        
@@ -449,20 +652,8 @@
                    // 检查是否点击了割草机图标区域(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);
                                }
                            }
                        }
                        // 切换以割草机为中心的模式
                        toggleCenterOnMowerMode();
                    }
                }
            }