package zhuye; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.FontMetrics; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.math.BigDecimal; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.io.File; import set.Setsys; import gecaoji.Device; import gecaoji.Gecaoji; import gecaoji.GecaojiMeg; import gecaoji.gecaolunjing; import gecaoji.lujingdraw; import dikuai.Dikuaiguanli; import dikuai.Dikuai; import zhangaiwu.Obstacledraw; import zhangaiwu.Obstacledge; import zhangaiwu.yulanzhangaiwu; import yaokong.Control03; /** * 地图渲染器 - 负责坐标系绘制、视图变换等功能 */ public class MapRenderer { // 视图变换参数 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); private final Color BACKGROUND_COLOR = new Color(250, 250, 250); private static final Color GRASS_FILL_COLOR = new Color(144, 238, 144, 120); private static final Color GRASS_BORDER_COLOR = new Color(60, 179, 113); private static final Color BOUNDARY_POINT_COLOR = new Color(128, 0, 128); private static final Color OBSTACLE_POINT_COLOR = new Color(255, 140, 0); // 橙色,用于区分障碍物点 private static final Color CIRCLE_SAMPLE_COLOR = new Color(220, 20, 60, 230); private static final double CIRCLE_SAMPLE_SIZE = 0.54d; private static final double BOUNDARY_POINT_MERGE_THRESHOLD = 0.05; private static final double BOUNDARY_CONTAINS_TOLERANCE = 0.05; private static final double PREVIEW_BOUNDARY_MARKER_SCALE = 0.25d; // 组件引用 private JPanel visualizationPanel; private List currentBoundary; private Rectangle2D.Double boundaryBounds; private Path2D.Double currentBoundaryPath; private List currentPlannedPath; private Rectangle2D.Double plannedPathBounds; private List currentObstacles; private Rectangle2D.Double obstacleBounds; private String selectedObstacleName; private String currentObstacleLandNumber; private String boundaryName; private boolean boundaryPointsVisible; private boolean obstaclePointsVisible; private boolean boundaryLengthVisible = false; // 是否显示边界距离,默认关闭 private double boundaryPointSizeScale = 1.0d; private boolean previewSizingEnabled; private String currentBoundaryLandNumber; private boolean dragInProgress; private final Gecaoji mower; private final Timer mowerUpdateTimer; private final GecaojiMeg mowerInfoManager; private CircleCaptureOverlay circleCaptureOverlay; private final List circleSampleMarkers = new ArrayList<>(); private final List realtimeMowingTrack = new ArrayList<>(); private final Deque idleMowerTrail = new ArrayDeque<>(); private final List handheldBoundaryPreview = new ArrayList<>(); private double boundaryPreviewMarkerScale = 1.0d; private boolean realtimeTrackRecording; private String realtimeTrackLandNumber; private double mowerEffectiveWidthMeters; private double defaultMowerWidthMeters; private double totalLandAreaSqMeters; private double trackLengthMeters; private double completedMowingAreaSqMeters; private double mowingCompletionRatio; private long lastTrackPersistTimeMillis; private boolean trackDirty; private boolean handheldBoundaryPreviewActive; private boolean pendingTrackBreak = true; private boolean idleTrailSuppressed; private Path2D.Double realtimeBoundaryPathCache; private String realtimeBoundaryPathLand; private static final double TRACK_SAMPLE_MIN_DISTANCE_METERS = 0.2d; private static final double TRACK_DUPLICATE_TOLERANCE_METERS = 1e-3d; private static final long TRACK_PERSIST_INTERVAL_MS = 5_000L; 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; this.mower = new Gecaoji(); 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)); } /** * 设置鼠标监听器 */ private void setupMouseListeners() { // 鼠标滚轮缩放 visualizationPanel.addMouseWheelListener(e -> { Point referencePoint = e.getPoint(); int notches = e.getWheelRotation(); double zoomFactor = notches < 0 ? ZOOM_STEP_FACTOR : 1 / ZOOM_STEP_FACTOR; zoomAtPoint(referencePoint, zoomFactor); }); // 鼠标拖拽移动 visualizationPanel.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { if (SwingUtilities.isRightMouseButton(e)) { resetView(); } else { dragInProgress = false; lastDragPoint = e.getPoint(); } } public void mouseReleased(MouseEvent e) { lastDragPoint = null; dragInProgress = false; } public void mouseClicked(MouseEvent e) { if (dragInProgress) { dragInProgress = false; return; } if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() != 1) { return; } if (handleMowerClick(e.getPoint())) { return; } // 优先处理障碍物边界点点击(如果可见) if (obstaclePointsVisible && handleObstaclePointClick(e.getPoint())) { return; } // 然后处理地块边界点点击 if (boundaryPointsVisible) { handleBoundaryPointClick(e.getPoint()); } } }); visualizationPanel.addMouseMotionListener(new MouseAdapter() { public void mouseDragged(MouseEvent e) { if (lastDragPoint != null && !SwingUtilities.isRightMouseButton(e)) { int dx = e.getX() - lastDragPoint.x; int dy = e.getY() - lastDragPoint.y; translateX += dx / scale; translateY += dy / scale; lastDragPoint = e.getPoint(); dragInProgress = true; visualizationPanel.repaint(); } } }); } private Timer createMowerTimer() { Timer timer = new Timer(300, e -> { mower.refreshFromDevice(); updateIdleMowerTrail(); if (realtimeTrackRecording) { captureRealtimeTrackPoint(); } if (visualizationPanel != null) { visualizationPanel.repaint(); } }); timer.setInitialDelay(0); timer.setRepeats(true); timer.start(); return timer; } /** * 基于鼠标位置的缩放 */ private void zoomAtPoint(Point referencePoint, double zoomFactor) { if (visualizationPanel == null) { return; } if (referencePoint == null) { referencePoint = new Point(visualizationPanel.getWidth() / 2, visualizationPanel.getHeight() / 2); } 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 = DEFAULT_SCALE; translateX = 0.0; translateY = 0.0; // 保存缩放比例到配置文件 saveScaleToProperties(); visualizationPanel.repaint(); } /** * 绘制地图内容 */ public void renderMap(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 保存原始变换 AffineTransform originalTransform = g2d.getTransform(); // 应用视图变换 g2d.translate(visualizationPanel.getWidth()/2, visualizationPanel.getHeight()/2); g2d.scale(scale, scale); g2d.translate(translateX, translateY); // 绘制坐标系内容 drawCoordinateSystem(g2d); boolean hasBoundary = currentBoundary != null && currentBoundary.size() >= 2; boolean hasPlannedPath = currentPlannedPath != null && currentPlannedPath.size() >= 2; boolean hasObstacles = currentObstacles != null && !currentObstacles.isEmpty(); // 绘制地块边界(底层) if (hasBoundary) { drawCurrentBoundary(g2d); } yulanzhangaiwu.renderPreview(g2d, scale); if (!circleSampleMarkers.isEmpty()) { drawCircleSampleMarkers(g2d, circleSampleMarkers, scale); } if (circleCaptureOverlay != null) { drawCircleCaptureOverlay(g2d, circleCaptureOverlay, scale); } adddikuaiyulan.drawPreview(g2d, handheldBoundaryPreview, scale, handheldBoundaryPreviewActive, boundaryPreviewMarkerScale); // 绘制导航路径(中层) if (hasPlannedPath) { drawCurrentPlannedPath(g2d); } // 绘制障碍物(顶层,显示在地块和导航路径上方) if (hasObstacles) { Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName); } // 显示边界点(如果边界点可见,或者边界距离可见) if ((boundaryPointsVisible || boundaryLengthVisible) && hasBoundary) { // 预览模式下显示序号 if (previewSizingEnabled) { drawBoundaryPointsWithNumbers(g2d, currentBoundary, scale); } else { double markerScale = boundaryPointSizeScale; pointandnumber.drawBoundaryPoints( g2d, currentBoundary, scale, BOUNDARY_POINT_MERGE_THRESHOLD, BOUNDARY_POINT_COLOR, markerScale ); } } // 绘制障碍物坐标点(带序号) if (obstaclePointsVisible && hasObstacles) { drawObstaclePointsWithNumbers(g2d, currentObstacles, scale); } if (shouldRenderIdleTrail()) { tuowei.draw(g2d, idleMowerTrail, scale); } if (!realtimeMowingTrack.isEmpty()) { drawRealtimeMowingCoverage(g2d); } drawMower(g2d); // 保存当前变换(包含视图变换)用于坐标转换 AffineTransform currentTransformForLength = g2d.getTransform(); // 恢复原始变换 g2d.setTransform(originalTransform); // 绘制边界长度(如果启用)- 在恢复原始变换后绘制 if (boundaryLengthVisible && hasBoundary) { bianjie.BoundaryLengthDrawer.drawBoundaryLengths(g2d, currentBoundary, scale, visualizationPanel.getWidth(), visualizationPanel.getHeight(), translateX, translateY); } // 绘制视图信息 drawViewInfo(g2d); } /** * 绘制坐标系 */ private void drawCoordinateSystem(Graphics2D g2d) { // 绘制原点 - 红色实心小圆圈 g2d.setColor(Color.RED); g2d.fill(new Ellipse2D.Double(-0.5d, -0.5d, 1d, 1d)); } /** * 绘制割草机 */ private void drawMower(Graphics2D g2d) { mower.draw(g2d, scale); } private void drawRealtimeMowingCoverage(Graphics2D g2d) { if (realtimeMowingTrack == null || realtimeMowingTrack.size() < 2) { return; } Path2D.Double boundaryPath = getRealtimeBoundaryPath(); double effectiveWidth = getEffectiveMowerWidthMeters(); gecaolunjing.draw(g2d, realtimeMowingTrack, effectiveWidth, boundaryPath); } private Path2D.Double getRealtimeBoundaryPath() { if (realtimeTrackLandNumber == null) { return null; } if (currentBoundaryLandNumber != null && realtimeTrackLandNumber.equals(currentBoundaryLandNumber)) { if (currentBoundaryPath == null) { currentBoundaryPath = buildBoundaryPath(currentBoundary); } return currentBoundaryPath; } if (realtimeBoundaryPathCache != null && realtimeTrackLandNumber.equals(realtimeBoundaryPathLand)) { return realtimeBoundaryPathCache; } Dikuai dikuai = Dikuai.getDikuai(realtimeTrackLandNumber); if (dikuai == null) { realtimeBoundaryPathCache = null; realtimeBoundaryPathLand = null; return null; } String normalized = normalizeValue(dikuai.getBoundaryCoordinates()); if (normalized == null) { realtimeBoundaryPathCache = null; realtimeBoundaryPathLand = null; return null; } List parsed = parseBoundary(normalized); if (parsed.size() < 3) { realtimeBoundaryPathCache = null; realtimeBoundaryPathLand = null; return null; } realtimeBoundaryPathCache = buildBoundaryPath(parsed); realtimeBoundaryPathLand = realtimeTrackLandNumber; return realtimeBoundaryPathCache; } private boolean shouldRenderIdleTrail() { return !idleTrailSuppressed && !realtimeTrackRecording && !handheldBoundaryPreviewActive && idleMowerTrail.size() >= 2; } private void captureRealtimeTrackPoint() { if (!realtimeTrackRecording) { return; } if (realtimeTrackLandNumber == null || visualizationPanel == null) { pendingTrackBreak = true; return; } Device device = Device.getGecaoji(); if (device == null) { pendingTrackBreak = true; return; } String fixQuality = device.getPositioningStatus(); if (!isHighPrecisionFix(fixQuality)) { pendingTrackBreak = true; return; } Point2D.Double position = mower.getPosition(); if (position == null || !Double.isFinite(position.x) || !Double.isFinite(position.y)) { pendingTrackBreak = true; return; } if (!isPointInsideActiveBoundary(position)) { pendingTrackBreak = true; return; } Point2D.Double candidate = new Point2D.Double(position.x, position.y); Point2D.Double lastPoint = realtimeMowingTrack.isEmpty() ? null : realtimeMowingTrack.get(realtimeMowingTrack.size() - 1); double distance = Double.NaN; if (lastPoint != null) { double dx = candidate.x - lastPoint.x; double dy = candidate.y - lastPoint.y; distance = Math.hypot(dx, dy); if (distance <= TRACK_DUPLICATE_TOLERANCE_METERS) { return; } if (distance < TRACK_SAMPLE_MIN_DISTANCE_METERS) { return; } } realtimeMowingTrack.add(candidate); if (!pendingTrackBreak && lastPoint != null && Double.isFinite(distance)) { trackLengthMeters += distance; } updateCompletionMetrics(); trackDirty = true; maybePersistRealtimeTrack(false); pendingTrackBreak = false; } private void updateIdleMowerTrail() { 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; } 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); } /** * 强制更新拖尾(用于收到$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()) { return; } long cutoff = now - idleTrailDurationMs; boolean modified = false; while (!idleMowerTrail.isEmpty() && idleMowerTrail.peekFirst().getTimestamp() < cutoff) { idleMowerTrail.removeFirst(); modified = true; } if (modified && visualizationPanel != null) { visualizationPanel.repaint(); } } private void clearIdleMowerTrail() { if (idleMowerTrail.isEmpty()) { return; } idleMowerTrail.clear(); if (visualizationPanel != null) { visualizationPanel.repaint(); } } private void updateCompletionMetrics() { double widthMeters = getEffectiveMowerWidthMeters(); if (widthMeters > 0 && trackLengthMeters > 0) { completedMowingAreaSqMeters = trackLengthMeters * widthMeters; } else { completedMowingAreaSqMeters = 0.0; } if (totalLandAreaSqMeters > 0 && completedMowingAreaSqMeters >= 0) { mowingCompletionRatio = Math.max(0.0, Math.min(1.0, completedMowingAreaSqMeters / totalLandAreaSqMeters)); } else { mowingCompletionRatio = 0.0; } } private void maybePersistRealtimeTrack(boolean force) { if (!trackDirty) { return; } long now = System.currentTimeMillis(); if (!force && (now - lastTrackPersistTimeMillis) < TRACK_PERSIST_INTERVAL_MS) { return; } persistRealtimeTrack(); } private void persistRealtimeTrack() { if (realtimeTrackLandNumber == null) { trackDirty = false; return; } String serialized = serializeRealtimeTrack(); String storedValue = (serialized == null || serialized.isEmpty()) ? "-1" : serialized; boolean updated = Dikuai.updateField(realtimeTrackLandNumber, "mowingTrack", storedValue); if (updated) { Dikuai dikuai = Dikuai.getDikuai(realtimeTrackLandNumber); if (dikuai != null) { dikuai.setMowingTrack(storedValue); } Dikuai.saveToProperties(); trackDirty = false; lastTrackPersistTimeMillis = System.currentTimeMillis(); } } private String serializeRealtimeTrack() { if (realtimeMowingTrack.isEmpty()) { return ""; } StringBuilder builder = new StringBuilder(); for (Point2D.Double point : realtimeMowingTrack) { if (point == null) { continue; } if (builder.length() > 0) { builder.append(';'); } builder.append(formatTrackCoordinate(point.x)).append(',').append(formatTrackCoordinate(point.y)); } return builder.toString(); } private String formatTrackCoordinate(double value) { if (!Double.isFinite(value)) { return "0"; } return String.format(Locale.US, "%.3f", value); } private double getEffectiveMowerWidthMeters() { if (mowerEffectiveWidthMeters > 0) { return mowerEffectiveWidthMeters; } if (defaultMowerWidthMeters > 0) { return defaultMowerWidthMeters; } return 0.0; } public void applyLandMetadata(Dikuai dikuai) { String landNumber = normalizeValue(dikuai != null ? dikuai.getLandNumber() : null); totalLandAreaSqMeters = parseLandAreaSqMeters(dikuai != null ? dikuai.getLandArea() : null); defaultMowerWidthMeters = parseMowerWidthMeters(dikuai != null ? dikuai.getMowingWidth() : null); // 若当前未录制或切换地块,则更新有效割草宽度 if (!realtimeTrackRecording || !equalsLand(landNumber, realtimeTrackLandNumber)) { mowerEffectiveWidthMeters = defaultMowerWidthMeters; } loadRealtimeTrack(landNumber, dikuai != null ? dikuai.getMowingTrack() : null); visualizationPanel.repaint(); } public void startRealtimeTrackRecording(String landNumber, double widthMeters) { String normalizedLand = normalizeValue(landNumber); if (normalizedLand == null) { return; } if (!equalsLand(normalizedLand, realtimeTrackLandNumber)) { Dikuai dikuai = Dikuai.getDikuai(normalizedLand); totalLandAreaSqMeters = parseLandAreaSqMeters(dikuai != null ? dikuai.getLandArea() : null); defaultMowerWidthMeters = parseMowerWidthMeters(dikuai != null ? dikuai.getMowingWidth() : null); loadRealtimeTrack(normalizedLand, dikuai != null ? dikuai.getMowingTrack() : null); } if (widthMeters > 0) { mowerEffectiveWidthMeters = widthMeters; } else if (mowerEffectiveWidthMeters <= 0) { mowerEffectiveWidthMeters = defaultMowerWidthMeters; } idleTrailSuppressed = true; clearIdleMowerTrail(); realtimeTrackLandNumber = normalizedLand; realtimeTrackRecording = true; pendingTrackBreak = true; captureRealtimeTrackPoint(); } public void pauseRealtimeTrackRecording() { realtimeTrackRecording = false; pendingTrackBreak = true; idleTrailSuppressed = false; maybePersistRealtimeTrack(true); } public void stopRealtimeTrackRecording() { realtimeTrackRecording = false; pendingTrackBreak = true; idleTrailSuppressed = false; maybePersistRealtimeTrack(true); } public void forceRealtimeTrackSnapshot() { if (!realtimeTrackRecording) { return; } captureRealtimeTrackPoint(); } public void clearRealtimeTrack() { realtimeTrackRecording = false; realtimeMowingTrack.clear(); trackLengthMeters = 0.0; completedMowingAreaSqMeters = 0.0; mowingCompletionRatio = 0.0; trackDirty = true; pendingTrackBreak = true; idleTrailSuppressed = false; maybePersistRealtimeTrack(true); visualizationPanel.repaint(); } public void clearIdleTrail() { clearIdleMowerTrail(); } public void setIdleTrailDurationSeconds(int seconds) { int sanitized = seconds; if (sanitized < 5 || sanitized > 600) { sanitized = DEFAULT_IDLE_TRAIL_DURATION_SECONDS; } idleTrailDurationMs = sanitized * 1_000L; pruneIdleMowerTrail(System.currentTimeMillis()); } public int getIdleTrailDurationSeconds() { long seconds = idleTrailDurationMs / 1_000L; if (seconds <= 0L) { return DEFAULT_IDLE_TRAIL_DURATION_SECONDS; } if (seconds > Integer.MAX_VALUE) { return DEFAULT_IDLE_TRAIL_DURATION_SECONDS; } return (int) seconds; } public double getMowingCompletionRatio() { if (!isMowerInsideSelectedBoundary()) { return 0.0; } return mowingCompletionRatio; } public double getCompletedMowingAreaSqMeters() { if (!isMowerInsideSelectedBoundary()) { return 0.0; } return completedMowingAreaSqMeters; } public double getTotalLandAreaSqMeters() { return totalLandAreaSqMeters; } public double getTrackLengthMeters() { return trackLengthMeters; } private boolean isMowerInsideSelectedBoundary() { Point2D.Double position = mower.getPosition(); if (position == null) { return false; } return isPointInsideActiveBoundary(position); } public void flushRealtimeTrack() { maybePersistRealtimeTrack(true); } private void loadRealtimeTrack(String landNumber, String trackData) { realtimeTrackRecording = false; realtimeTrackLandNumber = landNumber; realtimeMowingTrack.clear(); trackLengthMeters = 0.0; completedMowingAreaSqMeters = 0.0; mowingCompletionRatio = 0.0; trackDirty = false; lastTrackPersistTimeMillis = 0L; pendingTrackBreak = true; realtimeBoundaryPathCache = null; realtimeBoundaryPathLand = null; String trimmed = normalizeValue(trackData); if (trimmed == null || trimmed.isEmpty()) { updateCompletionMetrics(); return; } String[] segments = trimmed.split(";"); Path2D.Double boundaryPath = getRealtimeBoundaryPath(); Point2D.Double lastPoint = null; for (String segment : segments) { if (segment == null || segment.trim().isEmpty()) { continue; } String[] parts = segment.trim().split(","); if (parts.length < 2) { continue; } try { double x = Double.parseDouble(parts[0].trim()); double y = Double.parseDouble(parts[1].trim()); if (!Double.isFinite(x) || !Double.isFinite(y)) { continue; } Point2D.Double current = new Point2D.Double(x, y); if (boundaryPath != null && !isPointInsideBoundary(current, boundaryPath)) { continue; } if (lastPoint != null) { double dx = current.x - lastPoint.x; double dy = current.y - lastPoint.y; double distance = Math.hypot(dx, dy); if (distance <= TRACK_DUPLICATE_TOLERANCE_METERS) { continue; } if (distance < TRACK_SAMPLE_MIN_DISTANCE_METERS) { continue; } trackLengthMeters += distance; } realtimeMowingTrack.add(current); lastPoint = current; } catch (NumberFormatException ignored) { // 跳过异常条目 } } updateCompletionMetrics(); } private double parseLandAreaSqMeters(String raw) { if (raw == null) { return 0.0; } String trimmed = raw.trim(); if (trimmed.isEmpty() || "-1".equals(trimmed)) { return 0.0; } try { double area = Double.parseDouble(trimmed); return area > 0 ? area : 0.0; } catch (NumberFormatException ex) { return 0.0; } } private double parseMowerWidthMeters(String raw) { if (raw == null) { return 0.0; } String sanitized = raw.trim().toLowerCase(Locale.ROOT); if (sanitized.isEmpty() || "-1".equals(sanitized)) { return 0.0; } sanitized = sanitized.replace("厘米", "cm"); sanitized = sanitized.replace("公分", "cm"); sanitized = sanitized.replace("米", "m"); sanitized = sanitized.replace("cm", ""); sanitized = sanitized.replace("m", ""); sanitized = sanitized.trim(); if (sanitized.isEmpty()) { return 0.0; } try { double value = Double.parseDouble(sanitized); if (value <= 0) { return 0.0; } if (value > 10) { return value / 100.0; // 视为厘米 } return value; } catch (NumberFormatException ex) { return 0.0; } } private String normalizeValue(String value) { if (value == null) { return null; } String trimmed = value.trim(); if (trimmed.isEmpty() || "-1".equals(trimmed)) { return null; } return trimmed; } private boolean equalsLand(String a, String b) { if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } return a.equals(b); } private boolean handleMowerClick(Point screenPoint) { if (!mower.hasValidPosition()) { return false; } Point2D.Double mowerPosition = mower.getPosition(); if (mowerPosition == null) { return false; } Point2D.Double worldPoint = screenToWorld(screenPoint); double radius = mower.getWorldRadius(scale); if (Double.isNaN(radius)) { return false; } double dx = worldPoint.x - mowerPosition.x; double dy = worldPoint.y - mowerPosition.y; if (dx * dx + dy * dy <= radius * radius) { showMowerInfo(); return true; } return false; } private Point2D.Double screenToWorld(Point screenPoint) { double worldX = (screenPoint.x - visualizationPanel.getWidth() / 2.0) / scale - translateX; double worldY = (screenPoint.y - visualizationPanel.getHeight() / 2.0) / scale - translateY; return new Point2D.Double(worldX, worldY); } private void drawCurrentBoundary(Graphics2D g2d) { bianjiedrwa.drawBoundary(g2d, currentBoundary, scale, GRASS_FILL_COLOR, GRASS_BORDER_COLOR); } private void drawCurrentPlannedPath(Graphics2D g2d) { double arrowScale = previewSizingEnabled ? 0.5d : 1.0d; lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale, arrowScale); } private void drawCircleSampleMarkers(Graphics2D g2d, List markers, double scale) { if (markers == null || markers.isEmpty()) { return; } Shape markerShape; double half = CIRCLE_SAMPLE_SIZE / 2.0; g2d.setColor(CIRCLE_SAMPLE_COLOR); g2d.setStroke(new BasicStroke((float) (1.2f / scale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); Font originalFont = g2d.getFont(); float baseSize = (float) Math.max(12f / scale, 9f); float reducedSize = Math.max(baseSize / 3f, 3f); Font labelFont = originalFont.deriveFont(reducedSize); g2d.setFont(labelFont); FontMetrics metrics = g2d.getFontMetrics(); for (double[] pt : markers) { if (pt == null || pt.length < 2 || !Double.isFinite(pt[0]) || !Double.isFinite(pt[1])) { continue; } double x = pt[0]; double y = pt[1]; markerShape = new Ellipse2D.Double(x - half, y - half, CIRCLE_SAMPLE_SIZE, CIRCLE_SAMPLE_SIZE); g2d.fill(markerShape); String label = String.format(Locale.US, "%.2f,%.2f", x, y); int textWidth = metrics.stringWidth(label); float textX = (float) (x - textWidth / 2.0); float textY = (float) (y - half - 0.2d) - metrics.getDescent(); g2d.setColor(new Color(33, 37, 41, 220)); g2d.drawString(label, textX, textY); g2d.setColor(CIRCLE_SAMPLE_COLOR); } g2d.setFont(originalFont); } private void drawCircleCaptureOverlay(Graphics2D g2d, CircleCaptureOverlay overlay, double scale) { double diameter = overlay.radius * 2.0; Ellipse2D outline = new Ellipse2D.Double( overlay.centerX - overlay.radius, overlay.centerY - overlay.radius, diameter, diameter); Color fillColor = new Color(255, 152, 0, 80); Color borderColor = new Color(255, 87, 34, 230); Color centerColor = new Color(46, 139, 87, 230); g2d.setColor(fillColor); g2d.fill(outline); g2d.setColor(borderColor); g2d.setStroke(new BasicStroke((float) (1.8f / scale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g2d.draw(outline); double markerSize = 0.18d; double centerMarkerSize = 0.22d; Ellipse2D centerMarker = new Ellipse2D.Double( overlay.centerX - centerMarkerSize / 2.0, overlay.centerY - centerMarkerSize / 2.0, centerMarkerSize, centerMarkerSize); g2d.setColor(centerColor); g2d.fill(centerMarker); } public void showCircleCaptureOverlay(double centerX, double centerY, double radius, List samplePoints) { List copies = new ArrayList<>(); if (samplePoints != null) { for (double[] pt : samplePoints) { if (pt == null || pt.length < 2) { continue; } copies.add(new double[]{pt[0], pt[1]}); } } circleCaptureOverlay = new CircleCaptureOverlay(centerX, centerY, radius, copies); updateCircleSampleMarkers(samplePoints); if (visualizationPanel != null) { visualizationPanel.repaint(); } } public void clearCircleCaptureOverlay() { circleCaptureOverlay = null; if (visualizationPanel != null) { visualizationPanel.repaint(); } } public void updateCircleSampleMarkers(List samplePoints) { circleSampleMarkers.clear(); if (samplePoints != null) { for (double[] pt : samplePoints) { if (pt == null || pt.length < 2) { continue; } double x = pt[0]; double y = pt[1]; if (!Double.isFinite(x) || !Double.isFinite(y)) { continue; } circleSampleMarkers.add(new double[]{x, y}); } } if (visualizationPanel != null) { visualizationPanel.repaint(); } } public void clearCircleSampleMarkers() { if (!circleSampleMarkers.isEmpty()) { circleSampleMarkers.clear(); if (visualizationPanel != null) { visualizationPanel.repaint(); } } } private static final class CircleCaptureOverlay { final double centerX; final double centerY; final double radius; final List samplePoints; CircleCaptureOverlay(double centerX, double centerY, double radius, List samplePoints) { this.centerX = centerX; this.centerY = centerY; this.radius = radius; this.samplePoints = samplePoints; } } public void showMowerInfo() { if (mowerInfoManager != null) { mowerInfoManager.showMowerInfo(); } } /** * 处理障碍物边界点点击 * @param screenPoint 屏幕坐标点 * @return 如果处理了点击返回true,否则返回false */ private boolean handleObstaclePointClick(Point screenPoint) { if (currentObstacles == null || currentObstacles.isEmpty() || currentObstacleLandNumber == null) { return false; } double threshold = computeSelectionThresholdPixels(); // 遍历所有障碍物,找到被点击的点 for (Obstacledge.Obstacle obstacle : currentObstacles) { if (obstacle == null) { continue; } List xyCoords = obstacle.getXyCoordinates(); if (xyCoords == null || xyCoords.isEmpty()) { continue; } // 检查每个点 for (int i = 0; i < xyCoords.size(); i++) { Obstacledge.XYCoordinate coord = xyCoords.get(i); Point2D.Double worldPoint = new Point2D.Double(coord.getX(), coord.getY()); Point2D.Double screenPosition = worldToScreen(worldPoint); double dx = screenPosition.x - screenPoint.x; double dy = screenPosition.y - screenPoint.y; if (Math.hypot(dx, dy) <= threshold) { // 找到被点击的点 String obstacleName = obstacle.getObstacleName(); String pointLabel = (i + 1) + ""; String message = "确定要删除障碍物 \"" + obstacleName + "\" 的第" + pointLabel + "号边界点吗?"; int choice = JOptionPane.showConfirmDialog( visualizationPanel, message, "删除障碍物边界点", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE ); if (choice == JOptionPane.OK_OPTION) { removeObstaclePoint(obstacle, i); } return true; } } } return false; } /** * 删除障碍物的指定边界点 */ private void removeObstaclePoint(Obstacledge.Obstacle obstacle, int pointIndex) { if (obstacle == null || currentObstacleLandNumber == null) { return; } List xyCoords = obstacle.getXyCoordinates(); if (xyCoords == null || pointIndex < 0 || pointIndex >= xyCoords.size()) { return; } // 检查删除后是否还有足够的点 Obstacledge.ObstacleShape shape = obstacle.getShape(); int minPoints = (shape == Obstacledge.ObstacleShape.CIRCLE) ? 2 : 3; if (xyCoords.size() <= minPoints) { JOptionPane.showMessageDialog( visualizationPanel, "障碍物至少需要" + minPoints + "个点,无法删除", "提示", JOptionPane.INFORMATION_MESSAGE ); return; } // 创建新的坐标列表(移除指定点) List updatedCoords = new ArrayList<>(xyCoords); updatedCoords.remove(pointIndex); // 更新障碍物坐标 obstacle.setXyCoordinates(updatedCoords); // 保存到配置文件 try { File configFile = new File("Obstacledge.properties"); Obstacledge.ConfigManager manager = new Obstacledge.ConfigManager(); if (configFile.exists()) { manager.loadFromFile(configFile.getAbsolutePath()); } Obstacledge.Plot plot = manager.getPlotById(currentObstacleLandNumber.trim()); if (plot != null) { // 移除旧障碍物并添加更新后的障碍物 plot.removeObstacleByName(obstacle.getObstacleName()); plot.addObstacle(obstacle); manager.saveToFile(configFile.getAbsolutePath()); // 更新地块更新时间 Dikuai.updateField(currentObstacleLandNumber.trim(), "updateTime", new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date())); Dikuai.saveToProperties(); // 重新加载障碍物数据以刷新显示 List updatedObstacles = new ArrayList<>(); for (Obstacledge.Obstacle obs : currentObstacles) { if (obs.getObstacleName().equals(obstacle.getObstacleName())) { updatedObstacles.add(obstacle); // 使用更新后的障碍物 } else { updatedObstacles.add(obs); // 保持其他障碍物不变 } } applyObstaclesToRenderer(updatedObstacles, currentObstacleLandNumber); visualizationPanel.repaint(); } } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog( visualizationPanel, "保存失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE ); } } private void handleBoundaryPointClick(Point screenPoint) { if (currentBoundary == null || currentBoundaryLandNumber == null) { return; } int hitIndex = findBoundaryPointIndex(screenPoint); if (hitIndex < 0) { return; } String pointLabel = String.valueOf(hitIndex + 1); int choice = JOptionPane.showConfirmDialog( visualizationPanel, "确定要删除第" + pointLabel + "号边界点吗?", "删除边界点", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE ); if (choice == JOptionPane.OK_OPTION) { removeBoundaryPoint(hitIndex); } } private int findBoundaryPointIndex(Point screenPoint) { if (currentBoundary == null || currentBoundary.size() < 2) { return -1; } boolean closed = isBoundaryClosed(currentBoundary); int effectiveCount = closed ? currentBoundary.size() - 1 : currentBoundary.size(); if (effectiveCount <= 0) { return -1; } double threshold = computeSelectionThresholdPixels(); for (int i = 0; i < effectiveCount; i++) { Point2D.Double worldPoint = currentBoundary.get(i); Point2D.Double screenPosition = worldToScreen(worldPoint); double dx = screenPosition.x - screenPoint.x; double dy = screenPosition.y - screenPoint.y; if (Math.hypot(dx, dy) <= threshold) { return i; } } return -1; } private double computeSelectionThresholdPixels() { double scaleFactor = Math.max(0.5, scale); double diameterScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d); if (!Double.isFinite(diameterScale) || diameterScale <= 0.0d) { diameterScale = 1.0d; } double markerDiameterWorld = Math.max(1.0, (10.0 / scaleFactor) * 0.2 * diameterScale); double markerDiameterPixels = markerDiameterWorld * scale; return Math.max(8.0, markerDiameterPixels * 1.5); } private Point2D.Double worldToScreen(Point2D.Double worldPoint) { double screenX = (worldPoint.x + translateX) * scale + visualizationPanel.getWidth() / 2.0; double screenY = (worldPoint.y + translateY) * scale + visualizationPanel.getHeight() / 2.0; return new Point2D.Double(screenX, screenY); } private void removeBoundaryPoint(int index) { if (currentBoundary == null || currentBoundary.size() < 2) { return; } List updated = new ArrayList<>(currentBoundary); boolean closed = isBoundaryClosed(updated); int effectiveCount = closed ? updated.size() - 1 : updated.size(); if (index < 0 || index >= effectiveCount) { return; } updated.remove(index); if (closed && updated.size() >= 2) { Point2D.Double first = updated.get(0); Point2D.Double last = updated.get(updated.size() - 1); if (!arePointsClose(first, last)) { updated.set(updated.size() - 1, new Point2D.Double(first.x, first.y)); } } boolean success = persistBoundaryChanges(updated); if (!success) { return; } if (updated.size() < 2) { currentBoundary = null; currentBoundaryPath = null; boundaryBounds = null; boundaryPointsVisible = false; Dikuaiguanli.updateBoundaryPointVisibility(currentBoundaryLandNumber, false); visualizationPanel.repaint(); adjustViewAfterBoundaryReset(); } else { currentBoundary = updated; rebuildBoundaryPath(); boundaryBounds = computeBounds(updated); Dikuaiguanli.updateBoundaryPointVisibility(currentBoundaryLandNumber, boundaryPointsVisible); visualizationPanel.repaint(); } pendingTrackBreak = true; } private boolean persistBoundaryChanges(List updatedBoundary) { if (currentBoundaryLandNumber == null) { return false; } String serialized = serializeBoundary(updatedBoundary); String storedValue = (serialized == null || serialized.trim().isEmpty()) ? "-1" : serialized; boolean updated = Dikuai.updateField(currentBoundaryLandNumber, "boundaryCoordinates", storedValue); if (!updated) { JOptionPane.showMessageDialog(visualizationPanel, "无法更新边界数据", "错误", JOptionPane.ERROR_MESSAGE); return false; } Dikuai.saveToProperties(); Dikuaiguanli.notifyExternalCreation(currentBoundaryLandNumber); return true; } private String serializeBoundary(List boundary) { if (boundary == null || boundary.isEmpty()) { return ""; } StringBuilder builder = new StringBuilder(); for (int i = 0; i < boundary.size(); i++) { Point2D.Double point = boundary.get(i); builder.append(formatCoordinate(point.x)) .append(',') .append(formatCoordinate(point.y)); if (i < boundary.size() - 1) { builder.append(';'); } } return builder.toString(); } private String formatCoordinate(double value) { BigDecimal decimal = BigDecimal.valueOf(value).stripTrailingZeros(); return decimal.toPlainString(); } private boolean isBoundaryClosed(List boundary) { if (boundary == null || boundary.size() < 2) { return false; } Point2D.Double first = boundary.get(0); Point2D.Double last = boundary.get(boundary.size() - 1); return arePointsClose(first, last); } private boolean arePointsClose(Point2D.Double a, Point2D.Double b) { if (a == null || b == null) { return false; } double dx = a.x - b.x; double dy = a.y - b.y; return Math.hypot(dx, dy) <= BOUNDARY_POINT_MERGE_THRESHOLD; } private boolean isHighPrecisionFix(String fixQuality) { if (fixQuality == null) { return false; } String trimmed = fixQuality.trim(); if (trimmed.isEmpty()) { return false; } if ("4".equals(trimmed)) { return true; } try { double value = Double.parseDouble(trimmed); return Math.abs(value - 4.0d) < 1e-6; } catch (NumberFormatException ex) { 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)) { return false; } if (realtimeTrackLandNumber == null) { return false; } Path2D.Double path = getRealtimeBoundaryPath(); return isPointInsideBoundary(point, path); } private boolean isPointInsideBoundary(Point2D.Double point, Path2D.Double path) { if (point == null || path == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { return false; } if (path.contains(point.x, point.y)) { return true; } double size = BOUNDARY_CONTAINS_TOLERANCE * 2.0; return path.intersects(point.x - BOUNDARY_CONTAINS_TOLERANCE, point.y - BOUNDARY_CONTAINS_TOLERANCE, size, size); } private void rebuildBoundaryPath() { currentBoundaryPath = buildBoundaryPath(currentBoundary); } private Path2D.Double buildBoundaryPath(List boundary) { if (boundary == null || boundary.size() < 3) { return null; } Path2D.Double path = new Path2D.Double(); boolean started = false; for (Point2D.Double point : boundary) { if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { continue; } if (!started) { path.moveTo(point.x, point.y); started = true; } else { path.lineTo(point.x, point.y); } } if (!started) { return null; } path.closePath(); return path; } /** * 绘制视图信息 */ /** * 绘制障碍物坐标点(带序号) * 序号显示在点中心,字体大小与障碍物名称一致(11号),不随缩放变化 */ private void drawObstaclePointsWithNumbers(Graphics2D g2d, List obstacles, double scale) { if (obstacles == null || obstacles.isEmpty()) { return; } // 保存原始变换 AffineTransform originalTransform = g2d.getTransform(); // 设置点的大小(随缩放变化) double scaleFactor = Math.max(0.5, scale); double clampedScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d); if (!Double.isFinite(clampedScale) || clampedScale <= 0.0d) { clampedScale = 1.0d; } double minimumDiameter = clampedScale < 1.0 ? 0.5 : 1.0; double markerDiameter = Math.max(minimumDiameter, (10.0 / scaleFactor) * 0.2 * clampedScale); double markerRadius = markerDiameter / 2.0; // 设置字体(与障碍物名称一致,不随缩放变化) Font labelFont = new Font("微软雅黑", Font.PLAIN, 11); g2d.setFont(labelFont); FontMetrics fontMetrics = g2d.getFontMetrics(labelFont); // 遍历所有障碍物 for (Obstacledge.Obstacle obstacle : obstacles) { if (obstacle == null || !obstacle.isValid()) { continue; } List xyCoords = obstacle.getXyCoordinates(); if (xyCoords == null || xyCoords.isEmpty()) { continue; } // 绘制每个点及其序号 for (int i = 0; i < xyCoords.size(); i++) { Obstacledge.XYCoordinate coord = xyCoords.get(i); double x = coord.getX(); double y = coord.getY(); // 绘制点(在世界坐标系中,随缩放变化) g2d.setColor(OBSTACLE_POINT_COLOR); Ellipse2D.Double marker = new Ellipse2D.Double( x - markerRadius, y - markerRadius, markerDiameter, markerDiameter ); g2d.fill(marker); // 将世界坐标转换为屏幕坐标以绘制序号(不随缩放变化) Point2D.Double worldPoint = new Point2D.Double(x, y); Point2D.Double screenPoint = new Point2D.Double(); originalTransform.transform(worldPoint, screenPoint); // 保存当前变换 AffineTransform savedTransform = g2d.getTransform(); // 重置变换为屏幕坐标系统 g2d.setTransform(new AffineTransform()); // 绘制序号(在屏幕坐标系中,不随缩放变化) String numberText = String.valueOf(i + 1); int textWidth = fontMetrics.stringWidth(numberText); int textHeight = fontMetrics.getHeight(); // 在点中心绘制序号 int textX = (int)(screenPoint.x - textWidth / 2.0); int textY = (int)(screenPoint.y + textHeight / 4.0); // 绘制序号文字(无背景) g2d.setColor(Color.BLACK); g2d.drawString(numberText, textX, textY); // 恢复变换 g2d.setTransform(savedTransform); } } // 恢复原始变换 g2d.setTransform(originalTransform); } /** * 绘制边界点(带序号) * 序号显示在点中心,字体大小与障碍物序号一致(11号),不随缩放变化 */ private void drawBoundaryPointsWithNumbers(Graphics2D g2d, List boundary, double scale) { if (boundary == null || boundary.size() < 2) { return; } // 保存原始变换 AffineTransform originalTransform = g2d.getTransform(); int totalPoints = boundary.size(); boolean closed = totalPoints > 2 && areBoundaryPointsClose(boundary.get(0), boundary.get(totalPoints - 1)); int effectiveCount = closed ? totalPoints - 1 : totalPoints; if (effectiveCount <= 0) { return; } // 设置点的大小(与边界线宽度一致) // 边界线宽度:3 / Math.max(0.5, scale) double scaleFactor = Math.max(0.5, scale); double markerDiameter = 3.0 / scaleFactor; // 与边界线宽度一致 double markerRadius = markerDiameter / 2.0; // 设置字体(与障碍物序号一致,不随缩放变化) Font labelFont = new Font("微软雅黑", Font.PLAIN, 11); g2d.setFont(labelFont); FontMetrics fontMetrics = g2d.getFontMetrics(labelFont); // 绘制每个点及其序号 for (int i = 0; i < effectiveCount; i++) { Point2D.Double point = boundary.get(i); double x = point.x; double y = point.y; // 绘制点(在世界坐标系中,随缩放变化) g2d.setColor(BOUNDARY_POINT_COLOR); Ellipse2D.Double marker = new Ellipse2D.Double( x - markerRadius, y - markerRadius, markerDiameter, markerDiameter ); g2d.fill(marker); // 将世界坐标转换为屏幕坐标以绘制序号(不随缩放变化) Point2D.Double worldPoint = new Point2D.Double(x, y); Point2D.Double screenPoint = new Point2D.Double(); originalTransform.transform(worldPoint, screenPoint); // 保存当前变换 AffineTransform savedTransform = g2d.getTransform(); // 重置变换为屏幕坐标系统 g2d.setTransform(new AffineTransform()); // 绘制序号(在屏幕坐标系中,不随缩放变化) String numberText = String.valueOf(i + 1); int textWidth = fontMetrics.stringWidth(numberText); int textHeight = fontMetrics.getHeight(); // 在点中心绘制序号 int textX = (int)(screenPoint.x - textWidth / 2.0); int textY = (int)(screenPoint.y + textHeight / 4.0); // 绘制序号文字(无背景) g2d.setColor(Color.BLACK); g2d.drawString(numberText, textX, textY); // 恢复变换 g2d.setTransform(savedTransform); } // 恢复原始变换 g2d.setTransform(originalTransform); } /** * 检查两个边界点是否接近(用于判断边界是否闭合) */ private boolean areBoundaryPointsClose(Point2D.Double a, Point2D.Double b) { if (a == null || b == null) { return false; } double dx = a.x - b.x; double dy = a.y - b.y; return Math.hypot(dx, dy) <= BOUNDARY_POINT_MERGE_THRESHOLD; } private void drawViewInfo(Graphics2D g2d) { g2d.setColor(new Color(80, 80, 80)); g2d.setFont(new Font("微软雅黑", Font.PLAIN, 11)); // 在地图顶部左侧显示遥控摇杆对应速度(若非零) 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 info = String.format("缩放: %.2fx", scale); g2d.setColor(new Color(80, 80, 80)); g2d.drawString(info, 15, visualizationPanel.getHeight() - 15); } /** * 获取当前平移量X */ public double getTranslateX() { return translateX; } /** * 获取当前平移量Y */ public double getTranslateY() { return translateY; } /** * 设置视图变换参数(用于程序化控制) */ public void setViewTransform(double scale, double translateX, double translateY) { // 限制缩放范围 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(); } public void setCurrentBoundary(String boundaryCoordinates, String landNumber, String landName) { this.boundaryName = landName; this.currentBoundaryLandNumber = landNumber; this.realtimeBoundaryPathCache = null; this.realtimeBoundaryPathLand = null; if (boundaryCoordinates == null) { clearBoundaryData(); adjustViewAfterBoundaryReset(); return; } String trimmed = boundaryCoordinates.trim(); if (trimmed.isEmpty() || "-1".equals(trimmed)) { clearBoundaryData(); adjustViewAfterBoundaryReset(); return; } List parsed = parseBoundary(trimmed); if (parsed.size() < 2) { clearBoundaryData(); adjustViewAfterBoundaryReset(); return; } currentBoundary = parsed; rebuildBoundaryPath(); pendingTrackBreak = true; boundaryBounds = computeBounds(parsed); Rectangle2D.Double bounds = boundaryBounds; SwingUtilities.invokeLater(() -> { fitBoundsToView(bounds); visualizationPanel.repaint(); }); } private void clearBoundaryData() { currentBoundary = null; currentBoundaryPath = null; boundaryBounds = null; boundaryName = null; boundaryPointsVisible = false; currentBoundaryLandNumber = null; pendingTrackBreak = true; realtimeBoundaryPathCache = null; realtimeBoundaryPathLand = null; } public void setCurrentObstacles(String obstaclesData, String landNumber) { List parsed = parseObstacles(obstaclesData, landNumber); applyObstaclesToRenderer(parsed, landNumber); } public void setCurrentObstacles(List obstacles, String landNumber) { List cloned = cloneObstacles(obstacles, landNumber); applyObstaclesToRenderer(cloned, landNumber); } private void applyObstaclesToRenderer(List obstacles, String landNumber) { List safeList = obstacles != null ? obstacles : Collections.emptyList(); String normalizedLand = (landNumber != null) ? landNumber.trim() : null; if (normalizedLand != null && !normalizedLand.isEmpty()) { safeList = filterObstaclesForLand(safeList, normalizedLand); } if (normalizedLand == null || normalizedLand.isEmpty()) { clearObstacleData(); if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) { resetView(); } else { visualizationPanel.repaint(); } return; } if (safeList.isEmpty()) { clearObstacleData(); if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) { resetView(); } else { visualizationPanel.repaint(); } return; } currentObstacles = Collections.unmodifiableList(new ArrayList<>(safeList)); currentObstacleLandNumber = normalizedLand; obstacleBounds = convertObstacleBounds(Obstacledraw.getAllObstaclesBounds(currentObstacles)); selectedObstacleName = null; if (!hasRenderableBoundary() && !hasRenderablePlannedPath() && obstacleBounds != null) { Rectangle2D.Double bounds = obstacleBounds; SwingUtilities.invokeLater(() -> { fitBoundsToView(bounds); visualizationPanel.repaint(); }); } else { visualizationPanel.repaint(); } } private List cloneObstacles(List obstacles, String landNumber) { List result = new ArrayList<>(); if (obstacles == null || obstacles.isEmpty()) { return result; } String normalizedLandNumber = landNumber != null ? landNumber.trim() : null; Set usedNames = new LinkedHashSet<>(); int fallbackIndex = 1; for (Obstacledge.Obstacle source : obstacles) { if (source == null) { continue; } if (normalizedLandNumber != null && !normalizedLandNumber.isEmpty()) { String sourcePlotId = source.getPlotId(); if (sourcePlotId != null && !sourcePlotId.trim().isEmpty() && !normalizedLandNumber.equalsIgnoreCase(sourcePlotId.trim())) { continue; } } Obstacledge.ObstacleShape shape = source.getShape(); List xySource = source.getXyCoordinates(); if (shape == null || xySource == null) { continue; } List xyCopy = copyXYCoordinates(xySource); if (shape == Obstacledge.ObstacleShape.CIRCLE && xyCopy.size() < 2) { continue; } if (shape == Obstacledge.ObstacleShape.POLYGON && xyCopy.size() < 3) { continue; } String desiredName = source.getObstacleName(); if (desiredName == null || desiredName.trim().isEmpty()) { desiredName = "障碍物" + fallbackIndex++; } String uniqueName = ensureUniqueName(usedNames, desiredName.trim()); Obstacledge.Obstacle copy = new Obstacledge.Obstacle( normalizedLandNumber != null ? normalizedLandNumber : source.getPlotId(), uniqueName, shape ); copy.setXyCoordinates(xyCopy); List dmCopy = copyDMCoordinates(source.getOriginalCoordinates()); if (dmCopy.isEmpty()) { populateDummyOriginalCoordinates(copy, xyCopy.size()); } else { copy.setOriginalCoordinates(dmCopy); } result.add(copy); } return result; } private List filterObstaclesForLand(List obstacles, String landNumber) { if (obstacles == null || obstacles.isEmpty()) { return Collections.emptyList(); } if (landNumber == null || landNumber.trim().isEmpty()) { return Collections.emptyList(); } String normalized = landNumber.trim(); List filtered = new ArrayList<>(); for (Obstacledge.Obstacle obstacle : obstacles) { if (obstacle == null) { continue; } String plotId = obstacle.getPlotId(); if (plotId == null || plotId.trim().isEmpty()) { filtered.add(obstacle); continue; } if (normalized.equalsIgnoreCase(plotId.trim())) { filtered.add(obstacle); } } return filtered; } private String ensureUniqueName(Set usedNames, String preferredName) { String base = (preferredName == null || preferredName.trim().isEmpty()) ? "障碍物" : preferredName.trim(); String normalized = base.toLowerCase(Locale.ROOT); if (usedNames.add(normalized)) { return base; } int suffix = 2; while (true) { String attempt = base + suffix; String attemptKey = attempt.toLowerCase(Locale.ROOT); if (usedNames.add(attemptKey)) { return attempt; } suffix++; } } private List copyXYCoordinates(List source) { List copy = new ArrayList<>(); if (source == null) { return copy; } for (Obstacledge.XYCoordinate coord : source) { if (coord == null) { continue; } double x = coord.getX(); double y = coord.getY(); if (!Double.isFinite(x) || !Double.isFinite(y)) { continue; } copy.add(new Obstacledge.XYCoordinate(x, y)); } return copy; } private List copyDMCoordinates(List source) { List copy = new ArrayList<>(); if (source == null) { return copy; } for (Obstacledge.DMCoordinate coord : source) { if (coord == null) { continue; } copy.add(new Obstacledge.DMCoordinate(coord.getDegreeMinute(), coord.getDirection())); } return copy; } private void clearObstacleData() { currentObstacles = null; obstacleBounds = null; selectedObstacleName = null; currentObstacleLandNumber = null; obstaclePointsVisible = false; } private List parseObstacles(String obstaclesData, String landNumber) { List obstacles = new ArrayList<>(); if (obstaclesData == null) { return obstacles; } String normalized = stripInlineComment(obstaclesData.trim()); if (normalized.isEmpty() || "-1".equals(normalized)) { return obstacles; } List entries = splitObstacleEntries(normalized); int defaultIndex = 1; for (String entry : entries) { String trimmedEntry = stripInlineComment(entry); if (trimmedEntry.isEmpty()) { continue; } String nameToken = null; String shapeToken = null; String coordsSection = trimmedEntry; if (trimmedEntry.contains("::")) { String[] parts = trimmedEntry.split("::", 3); if (parts.length == 3) { nameToken = parts[0].trim(); shapeToken = parts[1].trim(); coordsSection = parts[2].trim(); } } else if (trimmedEntry.contains("@")) { String[] parts = trimmedEntry.split("@", 3); if (parts.length == 3) { nameToken = parts[0].trim(); shapeToken = parts[1].trim(); coordsSection = parts[2].trim(); } else if (parts.length == 2) { shapeToken = parts[0].trim(); coordsSection = parts[1].trim(); } } else if (trimmedEntry.contains(":")) { String[] parts = trimmedEntry.split(":", 3); if (parts.length == 3) { nameToken = parts[0].trim(); shapeToken = parts[1].trim(); coordsSection = parts[2].trim(); } else if (parts.length == 2) { if (looksLikeShapeToken(parts[0])) { shapeToken = parts[0].trim(); coordsSection = parts[1].trim(); } else { nameToken = parts[0].trim(); coordsSection = parts[1].trim(); } } } List xyCoordinates = parseObstacleCoordinates(coordsSection); if (xyCoordinates.size() < 2) { continue; } Obstacledge.ObstacleShape shape = resolveObstacleShape(shapeToken, xyCoordinates.size()); if (shape == null) { continue; } String obstacleName = (nameToken != null && !nameToken.isEmpty()) ? nameToken : "障碍物" + defaultIndex++; Obstacledge.Obstacle obstacle = new Obstacledge.Obstacle(landNumber, obstacleName, shape); obstacle.setXyCoordinates(new ArrayList<>(xyCoordinates)); populateDummyOriginalCoordinates(obstacle, xyCoordinates.size()); if (obstacle.isValid()) { obstacles.add(obstacle); } } return obstacles; } private boolean looksLikeShapeToken(String token) { if (token == null) { return false; } String normalized = token.trim().toLowerCase(Locale.ROOT); return "circle".equals(normalized) || "polygon".equals(normalized) || "圆形".equals(normalized) || "多边形".equals(normalized) || "0".equals(normalized) || "1".equals(normalized); } private List parseObstacleCoordinates(String coordsSection) { List coords = new ArrayList<>(); if (coordsSection == null) { return coords; } String sanitized = stripInlineComment(coordsSection.trim()); if (sanitized.isEmpty() || "-1".equals(sanitized)) { return coords; } // Remove wrapper characters like parentheses that are used when persisting payloads sanitized = sanitized.replace("(", "").replace(")", ""); String[] pairs = sanitized.split(";"); for (String pair : pairs) { if (pair == null) { continue; } String trimmed = stripInlineComment(pair.trim()); if (trimmed.isEmpty()) { continue; } trimmed = trimmed.replace("(", "").replace(")", ""); if (trimmed.isEmpty()) { continue; } String[] parts = trimmed.split(","); if (parts.length < 2) { continue; } try { double x = Double.parseDouble(parts[0].trim()); double y = Double.parseDouble(parts[1].trim()); coords.add(new Obstacledge.XYCoordinate(x, y)); } catch (NumberFormatException ignored) { // Skip malformed coordinate pair } } return coords; } private Obstacledge.ObstacleShape resolveObstacleShape(String shapeToken, int coordinateCount) { if (shapeToken != null && !shapeToken.trim().isEmpty()) { String normalized = shapeToken.trim().toLowerCase(Locale.ROOT); if ("circle".equals(normalized) || "圆形".equals(normalized) || "0".equals(normalized)) { return Obstacledge.ObstacleShape.CIRCLE; } if ("polygon".equals(normalized) || "多边形".equals(normalized) || "1".equals(normalized)) { return Obstacledge.ObstacleShape.POLYGON; } } if (coordinateCount == 2) { return Obstacledge.ObstacleShape.CIRCLE; } if (coordinateCount >= 3) { return Obstacledge.ObstacleShape.POLYGON; } return null; } private void populateDummyOriginalCoordinates(Obstacledge.Obstacle obstacle, int xyCount) { List dmCoordinates = new ArrayList<>(); int points = Math.max(1, xyCount); for (int i = 0; i < points; i++) { dmCoordinates.add(new Obstacledge.DMCoordinate(0.0, 'N')); dmCoordinates.add(new Obstacledge.DMCoordinate(0.0, 'E')); } obstacle.setOriginalCoordinates(dmCoordinates); } private List splitObstacleEntries(String data) { List entries = new ArrayList<>(); if (data.indexOf('|') >= 0) { String[] parts = data.split("\\|"); for (String part : parts) { if (part != null && !part.trim().isEmpty()) { entries.add(part.trim()); } } } else if (data.contains("\n")) { String[] lines = data.split("\r?\n"); for (String line : lines) { if (line != null && !line.trim().isEmpty()) { entries.add(line.trim()); } } } else { entries.add(data); } return entries; } private String stripInlineComment(String text) { if (text == null) { return ""; } int hashIndex = text.indexOf('#'); if (hashIndex >= 0) { return text.substring(0, hashIndex).trim(); } return text.trim(); } private Rectangle2D.Double convertObstacleBounds(double[] bounds) { if (bounds == null || bounds.length < 4) { return null; } double minX = bounds[0]; double minY = bounds[1]; double maxX = bounds[2]; double maxY = bounds[3]; return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); } private boolean hasRenderableBoundary() { return currentBoundary != null && currentBoundary.size() >= 2; } private boolean hasRenderablePlannedPath() { return currentPlannedPath != null && currentPlannedPath.size() >= 2; } private void adjustViewAfterBoundaryReset() { if (plannedPathBounds != null) { Rectangle2D.Double bounds = plannedPathBounds; SwingUtilities.invokeLater(() -> { fitBoundsToView(bounds); visualizationPanel.repaint(); }); return; } if (obstacleBounds != null) { Rectangle2D.Double bounds = obstacleBounds; SwingUtilities.invokeLater(() -> { fitBoundsToView(bounds); visualizationPanel.repaint(); }); return; } resetView(); } public void setCurrentPlannedPath(String plannedPath) { if (plannedPath == null) { currentPlannedPath = null; plannedPathBounds = null; if (!hasRenderableBoundary()) { resetView(); } else { visualizationPanel.repaint(); } return; } List parsed = lujingdraw.parsePlannedPath(plannedPath); if (parsed.size() < 2) { currentPlannedPath = null; plannedPathBounds = null; if (!hasRenderableBoundary()) { resetView(); } else { visualizationPanel.repaint(); } return; } currentPlannedPath = parsed; plannedPathBounds = computeBounds(parsed); Rectangle2D.Double bounds = plannedPathBounds; SwingUtilities.invokeLater(() -> { if (!hasRenderableBoundary()) { fitBoundsToView(bounds); } visualizationPanel.repaint(); }); } public void setBoundaryPointsVisible(boolean visible) { this.boundaryPointsVisible = visible; visualizationPanel.repaint(); } public void setObstaclePointsVisible(boolean visible) { this.obstaclePointsVisible = visible; visualizationPanel.repaint(); } /** * 设置是否显示边界距离 */ public void setBoundaryLengthVisible(boolean visible) { boundaryLengthVisible = visible; if (visualizationPanel != null) { visualizationPanel.repaint(); } } /** * 获取是否显示边界距离 */ public boolean isBoundaryLengthVisible() { return boundaryLengthVisible; } public void setBoundaryPointSizeScale(double sizeScale) { double normalized = (Double.isFinite(sizeScale) && sizeScale > 0.0d) ? sizeScale : 1.0d; if (Math.abs(boundaryPointSizeScale - normalized) < 1e-6) { return; } boundaryPointSizeScale = normalized; if (visualizationPanel == null) { return; } if (SwingUtilities.isEventDispatchThread()) { visualizationPanel.repaint(); } else { SwingUtilities.invokeLater(visualizationPanel::repaint); } } public void setPathPreviewSizingEnabled(boolean enabled) { previewSizingEnabled = enabled; if (visualizationPanel == null) { return; } if (SwingUtilities.isEventDispatchThread()) { visualizationPanel.repaint(); } else { SwingUtilities.invokeLater(visualizationPanel::repaint); } } public void setBoundaryPreviewMarkerScale(double markerScale) { double normalized = Double.isFinite(markerScale) && markerScale > 0.0d ? markerScale : 1.0d; if (Math.abs(boundaryPreviewMarkerScale - normalized) < 1e-6) { return; } boundaryPreviewMarkerScale = normalized; if (visualizationPanel == null) { return; } if (SwingUtilities.isEventDispatchThread()) { visualizationPanel.repaint(); } else { SwingUtilities.invokeLater(visualizationPanel::repaint); } } public boolean setHandheldMowerIconActive(boolean handheldActive) { if (mower == null) { return false; } boolean changed = mower.useHandheldIcon(handheldActive); if (changed && visualizationPanel != null) { if (SwingUtilities.isEventDispatchThread()) { visualizationPanel.repaint(); } else { SwingUtilities.invokeLater(visualizationPanel::repaint); } } return changed; } public void beginHandheldBoundaryPreview() { handheldBoundaryPreviewActive = true; handheldBoundaryPreview.clear(); visualizationPanel.repaint(); } public void addHandheldBoundaryPoint(double x, double y) { if (!Double.isFinite(x) || !Double.isFinite(y)) { return; } if (!handheldBoundaryPreviewActive) { beginHandheldBoundaryPreview(); } Point2D.Double last = handheldBoundaryPreview.isEmpty() ? null : handheldBoundaryPreview.get(handheldBoundaryPreview.size() - 1); if (last != null) { double dx = x - last.x; double dy = y - last.y; if (Math.hypot(dx, dy) < 1e-6) { visualizationPanel.repaint(); return; } } handheldBoundaryPreview.add(new Point2D.Double(x, y)); visualizationPanel.repaint(); } public void clearHandheldBoundaryPreview() { handheldBoundaryPreviewActive = false; handheldBoundaryPreview.clear(); boundaryPreviewMarkerScale = 1.0d; visualizationPanel.repaint(); } public List getHandheldBoundaryPreviewPoints() { return new ArrayList<>(handheldBoundaryPreview); } private List parseBoundary(String boundaryCoordinates) { List points = new ArrayList<>(); String[] entries = boundaryCoordinates.split(";"); for (String entry : entries) { if (entry == null || entry.trim().isEmpty()) { continue; } String[] parts = entry.trim().split(","); if (parts.length < 2) { continue; } try { double x = Double.parseDouble(parts[0].trim()); double y = Double.parseDouble(parts[1].trim()); points.add(new Point2D.Double(x, y)); } catch (NumberFormatException ex) { // ignore invalid entries } } return points; } private Rectangle2D.Double computeBounds(List points) { double minX = Double.MAX_VALUE; double minY = Double.MAX_VALUE; double maxX = -Double.MAX_VALUE; double maxY = -Double.MAX_VALUE; for (Point2D.Double point : points) { if (point.x < minX) minX = point.x; if (point.x > maxX) maxX = point.x; if (point.y < minY) minY = point.y; if (point.y > maxY) maxY = point.y; } if (minX == Double.MAX_VALUE) { return null; } return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); } private void fitBoundsToView(Rectangle2D.Double bounds) { if (bounds == null || visualizationPanel.getWidth() <= 0 || visualizationPanel.getHeight() <= 0) { return; } Rectangle2D.Double targetBounds = includeMowerInBounds(bounds); double width = Math.max(targetBounds.width, 1); double height = Math.max(targetBounds.height, 1); double targetWidth = width * 1.2; double targetHeight = height * 1.2; double panelWidth = visualizationPanel.getWidth(); double panelHeight = visualizationPanel.getHeight(); double newScale = Math.min(panelWidth / targetWidth, panelHeight / targetHeight); newScale = Math.max(0.05, Math.min(newScale, 50.0)); this.scale = newScale; this.translateX = -targetBounds.getCenterX(); this.translateY = -targetBounds.getCenterY(); } // Keep the mower marker inside the viewport whenever the camera refits to scene bounds. private Rectangle2D.Double includeMowerInBounds(Rectangle2D.Double bounds) { Rectangle2D.Double expanded = new Rectangle2D.Double( bounds.x, bounds.y, Math.max(0.0, bounds.width), Math.max(0.0, bounds.height) ); if (mower == null || !mower.hasValidPosition()) { return expanded; } Point2D.Double mowerPosition = mower.getPosition(); if (mowerPosition == null || !Double.isFinite(mowerPosition.x) || !Double.isFinite(mowerPosition.y)) { return expanded; } double minX = Math.min(expanded.x, mowerPosition.x); double minY = Math.min(expanded.y, mowerPosition.y); double maxX = Math.max(expanded.x + expanded.width, mowerPosition.x); double maxY = Math.max(expanded.y + expanded.height, mowerPosition.y); expanded.x = minX; expanded.y = minY; expanded.width = Math.max(0.0, maxX - minX); expanded.height = Math.max(0.0, maxY - minY); return expanded; } public void dispose() { mowerUpdateTimer.stop(); mowerInfoManager.dispose(); } /** * 获取当前边界点列表 * @return 当前边界点列表,如果没有边界则返回null */ public List getCurrentBoundary() { return currentBoundary; } /** * 获取割草机实例 * @return 割草机实例 */ public Gecaoji getMower() { return mower; } }