张世豪
7 天以前 f784463ab019c1113cf0b31a249e8802b07a57fa
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;
@@ -28,17 +29,22 @@
import zhangaiwu.Obstacledraw;
import zhangaiwu.Obstacledge;
import zhangaiwu.yulanzhangaiwu;
import yaokong.Control03;
/**
 * 地图渲染器 - 负责坐标系绘制、视图变换等功能
 */
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 Point mousePoint;
    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);
@@ -100,6 +106,7 @@
    public static final int DEFAULT_IDLE_TRAIL_DURATION_SECONDS = 60;
    private static final double IDLE_TRAIL_SAMPLE_DISTANCE_METERS = 0.05d;
    private long idleTrailDurationMs = DEFAULT_IDLE_TRAIL_DURATION_SECONDS * 1_000L;
    private static final double ZOOM_STEP_FACTOR = 1.2d;
    
    public MapRenderer(JPanel visualizationPanel) {
        this.visualizationPanel = visualizationPanel;
@@ -107,6 +114,64 @@
        this.mowerUpdateTimer = createMowerTimer();
        this.mowerInfoManager = new GecaojiMeg(visualizationPanel, mower);
        setupMouseListeners();
        // 从配置文件读取上次保存的缩放比例和视图中心坐标
        loadViewSettingsFromProperties();
    }
    /**
     * 从配置文件读取缩放比例和视图中心坐标
     */
    private void loadViewSettingsFromProperties() {
        // 加载缩放比例
        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;
        }
        // 加载视图中心坐标
        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;
        }
    }
    /**
     * 保存缩放比例到配置文件
     */
    private void saveScaleToProperties() {
        Setsys setsys = new Setsys();
        // 保留2位小数
        setsys.updateProperty(MAP_SCALE_PROPERTY, String.format("%.2f", scale));
    }
    
    /**
@@ -115,10 +180,10 @@
    private void setupMouseListeners() {
        // 鼠标滚轮缩放
        visualizationPanel.addMouseWheelListener(e -> {
            mousePoint = e.getPoint();
            Point referencePoint = e.getPoint();
            int notches = e.getWheelRotation();
            double zoomFactor = notches < 0 ? 1.2 : 1/1.2;
            zoomAtPoint(zoomFactor);
            double zoomFactor = notches < 0 ? ZOOM_STEP_FACTOR : 1 / ZOOM_STEP_FACTOR;
            zoomAtPoint(referencePoint, zoomFactor);
        });
        
        // 鼠标拖拽移动
@@ -168,12 +233,6 @@
                    visualizationPanel.repaint();
                }
            }
            public void mouseMoved(MouseEvent e) {
                // 更新鼠标位置用于显示坐标
                mousePoint = e.getPoint();
                visualizationPanel.repaint();
            }
        });
    }
@@ -197,34 +256,71 @@
    /**
     * 基于鼠标位置的缩放
     */
    private void zoomAtPoint(double zoomFactor) {
        if (mousePoint == null) return;
        // 计算鼠标位置在世界坐标系中的坐标
        double worldX = (mousePoint.x - visualizationPanel.getWidth()/2) / scale - translateX;
        double worldY = (mousePoint.y - visualizationPanel.getHeight()/2) / scale - translateY;
    private void zoomAtPoint(Point referencePoint, double zoomFactor) {
        if (visualizationPanel == null) {
            return;
        }
        if (referencePoint == null) {
            referencePoint = new Point(visualizationPanel.getWidth() / 2, visualizationPanel.getHeight() / 2);
        }
        scale *= zoomFactor;
        scale = Math.max(0.05, Math.min(scale, 50.0)); // 限制缩放范围,允许最多缩小至原始的1/20
        // 计算缩放后同一鼠标位置在世界坐标系中的新坐标
        double newWorldX = (mousePoint.x - visualizationPanel.getWidth()/2) / scale - translateX;
        double newWorldY = (mousePoint.y - visualizationPanel.getHeight()/2) / scale - translateY;
        // 调整平移量,使鼠标指向的世界坐标保持不变
        double panelCenterX = visualizationPanel.getWidth() / 2.0;
        double panelCenterY = visualizationPanel.getHeight() / 2.0;
        double worldX = (referencePoint.x - panelCenterX) / scale - translateX;
        double worldY = (referencePoint.y - panelCenterY) / scale - translateY;
    scale *= zoomFactor;
    scale = Math.max(MIN_SCALE, Math.min(scale, MAX_SCALE)); // 限制缩放范围,允许最多缩小至原始的1/20
        double newWorldX = (referencePoint.x - panelCenterX) / scale - translateX;
        double newWorldY = (referencePoint.y - panelCenterY) / scale - translateY;
        translateX += (newWorldX - worldX);
        translateY += (newWorldY - worldY);
        // 保存缩放比例到配置文件
        saveScaleToProperties();
        visualizationPanel.repaint();
    }
    public void zoomInFromCenter() {
        zoomAtPoint(null, ZOOM_STEP_FACTOR);
    }
    public void zoomOutFromCenter() {
        zoomAtPoint(null, 1 / ZOOM_STEP_FACTOR);
    }
    public boolean canZoomIn() {
        return scale < MAX_SCALE - SCALE_EPSILON;
    }
    public boolean canZoomOut() {
        return scale > MIN_SCALE + SCALE_EPSILON;
    }
    public double getScale() {
        return scale;
    }
    public double getMaxScale() {
        return MAX_SCALE;
    }
    public double getMinScale() {
        return MIN_SCALE;
    }
    
    /**
     * 重置视图
     */
    public void resetView() {
        scale = 1.0;
        scale = DEFAULT_SCALE;
        translateX = 0.0;
        translateY = 0.0;
        // 保存缩放比例到配置文件
        saveScaleToProperties();
        visualizationPanel.repaint();
    }
    
@@ -451,7 +547,8 @@
        if (device == null) {
            return;
        }
        if (!isHighPrecisionFix(device.getPositioningStatus())) {
        // 使用更宽松的定位状态判断,允许状态1和4显示拖尾
        if (!isValidFixForTrail(device.getPositioningStatus())) {
            return;
        }
@@ -473,6 +570,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()) {
@@ -1202,6 +1349,32 @@
            return false;
        }
    }
    /**
     * 判断定位状态是否有效,可用于显示拖尾
     * 接受状态1(单点定位)、2(码差分)、3(无效PPS)、4(固定解)、5(浮点解)
     */
    private boolean isValidFixForTrail(String fixQuality) {
        if (fixQuality == null) {
            return false;
        }
        String trimmed = fixQuality.trim();
        if (trimmed.isEmpty()) {
            return false;
        }
        // 接受状态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, 2.0, 3.0, 4.0, 5.0(只要不是0)
            return value >= 1.0 && value <= 5.0;
        } catch (NumberFormatException ex) {
            return false;
        }
    }
    private boolean isPointInsideActiveBoundary(Point2D.Double point) {
        if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) {
@@ -1260,30 +1433,34 @@
    private void drawViewInfo(Graphics2D g2d) {
        g2d.setColor(new Color(80, 80, 80));
        g2d.setFont(new Font("微软雅黑", Font.PLAIN, 11));
        // 只显示缩放比例,去掉平移信息
        String info = String.format("缩放: %.2fx", scale);
        g2d.drawString(info, 15, visualizationPanel.getHeight() - 15);
        if (mousePoint != null) {
            // 计算鼠标位置在世界坐标系中的坐标
            double worldX = (mousePoint.x - visualizationPanel.getWidth()/2) / scale - translateX;
            double worldY = (mousePoint.y - visualizationPanel.getHeight()/2) / scale - translateY;
            String mouseCoord = String.format("坐标: (%.1f, %.1f)m", worldX, worldY);
            g2d.drawString(mouseCoord, visualizationPanel.getWidth() - 150, visualizationPanel.getHeight() - 15);
        // 在地图顶部左侧显示遥控摇杆对应速度(若非零)
        try {
            int forward = Control03.getCurrentForwardSpeed();
            int steer = Control03.getCurrentSteeringSpeed();
            if (forward != 0 || steer != 0) {
                String speedInfo = String.format("行进:%d 转向:%d", forward, steer);
                // 背景半透明矩形增强可读性
                FontMetrics fm = g2d.getFontMetrics();
                int padding = 6;
                int w = fm.stringWidth(speedInfo) + padding * 2;
                int h = fm.getHeight() + padding;
                int x = 12;
                int y = 12;
                Color bg = new Color(255, 255, 255, 180);
                g2d.setColor(bg);
                g2d.fillRoundRect(x, y, w, h, 8, 8);
                g2d.setColor(new Color(120, 120, 120));
                g2d.drawString(speedInfo, x + padding, y + fm.getAscent() + (padding/2));
            }
        } catch (Throwable t) {
            // 不应阻塞渲染,静默处理任何异常
        }
        // 去掉操作提示:"滚轮缩放 | 拖拽移动 | 右键重置"
        // String tips = "滚轮缩放 | 拖拽移动 | 右键重置";
        // g2d.drawString(tips, visualizationPanel.getWidth() - 200, 30);
    }
    /**
     * 获取当前缩放比例
     */
    public double getScale() {
        return scale;
        // 保留底部的缩放比例信息
        String info = String.format("缩放: %.2fx", scale);
        g2d.setColor(new Color(80, 80, 80));
        g2d.drawString(info, 15, visualizationPanel.getHeight() - 15);
    }
    
    /**
@@ -1304,7 +1481,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();
@@ -2046,4 +2231,20 @@
        mowerInfoManager.dispose();
    }
    /**
     * 获取当前边界点列表
     * @return 当前边界点列表,如果没有边界则返回null
     */
    public List<Point2D.Double> getCurrentBoundary() {
        return currentBoundary;
    }
    /**
     * 获取割草机实例
     * @return 割草机实例
     */
    public Gecaoji getMower() {
        return mower;
    }
}