From 2144172c7b961d4112850692ed77b46f1ae5d373 Mon Sep 17 00:00:00 2001
From: 张世豪 <979909237@qq.com>
Date: 星期五, 05 十二月 2025 19:34:53 +0800
Subject: [PATCH] 20251205
---
src/zhuye/MapRenderer.java | 1228 +++++++++++++++++++++++++++++++++++++++++++++++----------
1 files changed, 999 insertions(+), 229 deletions(-)
diff --git a/src/zhuye/MapRenderer.java b/src/zhuye/MapRenderer.java
index aebc291..5f5dd06 100644
--- a/src/zhuye/MapRenderer.java
+++ b/src/zhuye/MapRenderer.java
@@ -3,21 +3,31 @@
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.text.SimpleDateFormat;
+import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.Date;
+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 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;
/**
* 鍦板浘娓叉煋鍣� - 璐熻矗鍧愭爣绯荤粯鍒躲�佽鍥惧彉鎹㈢瓑鍔熻兘
@@ -36,40 +46,62 @@
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 BOUNDARY_LABEL_COLOR = Color.BLACK;
+ 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 SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ private static final double BOUNDARY_CONTAINS_TOLERANCE = 0.05;
// 缁勪欢寮曠敤
private JPanel visualizationPanel;
private List<Point2D.Double> currentBoundary;
private Rectangle2D.Double boundaryBounds;
+ private Path2D.Double currentBoundaryPath;
private List<Point2D.Double> currentPlannedPath;
private Rectangle2D.Double plannedPathBounds;
private List<Obstacledge.Obstacle> currentObstacles;
private Rectangle2D.Double obstacleBounds;
private String selectedObstacleName;
+ private String currentObstacleLandNumber;
private String boundaryName;
private boolean boundaryPointsVisible;
private String currentBoundaryLandNumber;
private boolean dragInProgress;
private final Gecaoji mower;
private final Timer mowerUpdateTimer;
- private JDialog mowerInfoDialog;
- private Timer mowerInfoRefreshTimer;
- private JLabel mowerNumberValueLabel;
- private JLabel realtimeXValueLabel;
- private JLabel realtimeYValueLabel;
- private JLabel positioningStatusValueLabel;
- private JLabel satelliteCountValueLabel;
- private JLabel realtimeSpeedValueLabel;
- private JLabel headingValueLabel;
- private JLabel updateTimeValueLabel;
+ private final GecaojiMeg mowerInfoManager;
+ private CircleCaptureOverlay circleCaptureOverlay;
+ private final List<double[]> circleSampleMarkers = new ArrayList<>();
+ private final List<Point2D.Double> realtimeMowingTrack = new ArrayList<>();
+ private final Deque<tuowei.TrailSample> idleMowerTrail = new ArrayDeque<>();
+ private final List<Point2D.Double> handheldBoundaryPreview = new ArrayList<>();
+ 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;
public MapRenderer(JPanel visualizationPanel) {
this.visualizationPanel = visualizationPanel;
this.mower = new Gecaoji();
this.mowerUpdateTimer = createMowerTimer();
+ this.mowerInfoManager = new GecaojiMeg(visualizationPanel, mower);
setupMouseListeners();
}
@@ -144,6 +176,10 @@
private Timer createMowerTimer() {
Timer timer = new Timer(300, e -> {
mower.refreshFromDevice();
+ updateIdleMowerTrail();
+ if (realtimeTrackRecording) {
+ captureRealtimeTrackPoint();
+ }
if (visualizationPanel != null) {
visualizationPanel.repaint();
}
@@ -218,6 +254,18 @@
Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName);
}
+ yulanzhangaiwu.renderPreview(g2d, scale);
+
+ if (!circleSampleMarkers.isEmpty()) {
+ drawCircleSampleMarkers(g2d, circleSampleMarkers, scale);
+ }
+
+ if (circleCaptureOverlay != null) {
+ drawCircleCaptureOverlay(g2d, circleCaptureOverlay, scale);
+ }
+
+ adddikuaiyulan.drawPreview(g2d, handheldBoundaryPreview, scale, handheldBoundaryPreviewActive);
+
if (hasPlannedPath) {
drawCurrentPlannedPath(g2d);
}
@@ -228,11 +276,18 @@
currentBoundary,
scale,
BOUNDARY_POINT_MERGE_THRESHOLD,
- BOUNDARY_POINT_COLOR,
- BOUNDARY_LABEL_COLOR
+ BOUNDARY_POINT_COLOR
);
}
+ if (shouldRenderIdleTrail()) {
+ tuowei.draw(g2d, idleMowerTrail, scale);
+ }
+
+ if (!realtimeMowingTrack.isEmpty()) {
+ drawRealtimeMowingCoverage(g2d);
+ }
+
drawMower(g2d);
// 鎭㈠鍘熷鍙樻崲
@@ -248,7 +303,7 @@
private void drawCoordinateSystem(Graphics2D g2d) {
// 缁樺埗鍘熺偣 - 绾㈣壊瀹炲績灏忓渾鍦�
g2d.setColor(Color.RED);
- g2d.fillOval(-1, -1, 2, 2);
+ g2d.fill(new Ellipse2D.Double(-0.5d, -0.5d, 1d, 1d));
}
@@ -260,6 +315,523 @@
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<Point2D.Double> 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;
+ }
+ if (!isHighPrecisionFix(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);
+ }
+
+ 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 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;
@@ -296,217 +868,140 @@
lujingdraw.drawPlannedPath(g2d, currentPlannedPath, scale);
}
- private void showMowerInfo() {
- Device device = Device.getGecaoji();
- if (device == null) {
- JOptionPane.showMessageDialog(
- visualizationPanel,
- "鏃犺澶囨暟鎹�",
- "鍓茶崏鏈轰俊鎭�",
- JOptionPane.INFORMATION_MESSAGE
- );
+ private void drawCircleSampleMarkers(Graphics2D g2d, List<double[]> markers, double scale) {
+ if (markers == null || markers.isEmpty()) {
return;
}
-
- ensureMowerInfoDialog();
- updateMowerInfoLabels();
- positionMowerDialog();
- if (!mowerInfoDialog.isVisible()) {
- mowerInfoDialog.setVisible(true);
- } else {
- mowerInfoDialog.toFront();
- }
- startMowerInfoTimer();
- }
-
- private void ensureMowerInfoDialog() {
- if (mowerInfoDialog != null) {
- return;
- }
-
- Window owner = SwingUtilities.getWindowAncestor(visualizationPanel);
- JDialog dialog = new JDialog(owner, "鍓茶崏鏈轰俊鎭�", Dialog.ModalityType.MODELESS);
- dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
-
- JPanel content = new JPanel(new BorderLayout(0, 12));
- content.setBorder(BorderFactory.createEmptyBorder(16, 20, 16, 20));
-
- JPanel grid = new JPanel(new GridLayout(0, 2, 12, 8));
-
- grid.add(new JLabel("璁惧缂栧彿锛�"));
- mowerNumberValueLabel = createValueLabel();
- grid.add(mowerNumberValueLabel);
-
- grid.add(new JLabel("瀹炴椂X鍧愭爣锛�"));
- realtimeXValueLabel = createValueLabel();
- grid.add(realtimeXValueLabel);
-
- grid.add(new JLabel("瀹炴椂Y鍧愭爣锛�"));
- realtimeYValueLabel = createValueLabel();
- grid.add(realtimeYValueLabel);
-
- grid.add(new JLabel("瀹氫綅璐ㄩ噺锛�"));
- positioningStatusValueLabel = createValueLabel();
- grid.add(positioningStatusValueLabel);
-
- grid.add(new JLabel("鍗槦棰楁暟锛�"));
- satelliteCountValueLabel = createValueLabel();
- grid.add(satelliteCountValueLabel);
-
- grid.add(new JLabel("琛岄┒閫熷害锛�"));
- realtimeSpeedValueLabel = createValueLabel();
- grid.add(realtimeSpeedValueLabel);
-
- grid.add(new JLabel("杩愬姩鑸悜锛�"));
- headingValueLabel = createValueLabel();
- grid.add(headingValueLabel);
-
- grid.add(new JLabel("鏇存柊鏃堕棿锛�"));
- updateTimeValueLabel = createValueLabel();
- grid.add(updateTimeValueLabel);
-
- content.add(grid, BorderLayout.CENTER);
-
- JButton closeButton = new JButton("鍏抽棴");
- closeButton.addActionListener(e -> dialog.dispose());
- JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
- buttonPanel.add(closeButton);
- content.add(buttonPanel, BorderLayout.SOUTH);
-
- dialog.setContentPane(content);
- dialog.pack();
- dialog.setResizable(false);
- dialog.addWindowListener(new WindowAdapter() {
- @Override
- public void windowClosed(WindowEvent e) {
- stopMowerInfoTimer();
- resetMowerInfoLabels();
- mowerInfoDialog = null;
+ 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;
}
- });
-
- mowerInfoDialog = dialog;
+ 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 positionMowerDialog() {
- if (mowerInfoDialog == null || visualizationPanel == null) {
- return;
- }
- int panelWidth = visualizationPanel.getWidth();
- int panelHeight = visualizationPanel.getHeight();
- int dialogHeight = mowerInfoDialog.getHeight();
- if (dialogHeight <= 0) {
- dialogHeight = mowerInfoDialog.getPreferredSize().height;
- }
- if (panelWidth > 0) {
- mowerInfoDialog.setSize(panelWidth, dialogHeight);
- }
+ 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);
- try {
- Point panelOnScreen = visualizationPanel.getLocationOnScreen();
- int dialogWidth = mowerInfoDialog.getWidth();
- int targetX = panelOnScreen.x + (panelWidth - dialogWidth) / 2;
- int targetY = panelOnScreen.y + panelHeight / 3 - dialogHeight / 2;
- mowerInfoDialog.setLocation(targetX, targetY);
- } catch (IllegalComponentStateException ex) {
- // component not yet showing; ignore positioning
+ 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<double[]> samplePoints) {
+ List<double[]> 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();
}
}
- private JLabel createValueLabel() {
- JLabel label = new JLabel("--");
- label.setHorizontalAlignment(SwingConstants.LEFT);
- return label;
- }
-
- private void startMowerInfoTimer() {
- if (mowerInfoRefreshTimer == null) {
- mowerInfoRefreshTimer = new Timer(1000, e -> updateMowerInfoLabels());
- mowerInfoRefreshTimer.setRepeats(true);
- }
- if (!mowerInfoRefreshTimer.isRunning()) {
- mowerInfoRefreshTimer.start();
+ public void clearCircleCaptureOverlay() {
+ circleCaptureOverlay = null;
+ if (visualizationPanel != null) {
+ visualizationPanel.repaint();
}
}
- private void stopMowerInfoTimer() {
- if (mowerInfoRefreshTimer != null) {
- mowerInfoRefreshTimer.stop();
- mowerInfoRefreshTimer = null;
+ public void updateCircleSampleMarkers(List<double[]> 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();
}
}
- private void resetMowerInfoLabels() {
- mowerNumberValueLabel = null;
- realtimeXValueLabel = null;
- realtimeYValueLabel = null;
- positioningStatusValueLabel = null;
- satelliteCountValueLabel = null;
- realtimeSpeedValueLabel = null;
- headingValueLabel = null;
- updateTimeValueLabel = null;
+ public void clearCircleSampleMarkers() {
+ if (!circleSampleMarkers.isEmpty()) {
+ circleSampleMarkers.clear();
+ if (visualizationPanel != null) {
+ visualizationPanel.repaint();
+ }
+ }
}
- private void updateMowerInfoLabels() {
- if (mowerNumberValueLabel == null) {
- return;
+ private static final class CircleCaptureOverlay {
+ final double centerX;
+ final double centerY;
+ final double radius;
+ final List<double[]> samplePoints;
+
+ CircleCaptureOverlay(double centerX, double centerY, double radius, List<double[]> samplePoints) {
+ this.centerX = centerX;
+ this.centerY = centerY;
+ this.radius = radius;
+ this.samplePoints = samplePoints;
}
-
- Device device = Device.getGecaoji();
- if (device == null) {
- setMowerInfoLabels("--");
- return;
- }
-
- mower.refreshFromDevice();
-
- mowerNumberValueLabel.setText(formatDeviceValue(device.getMowerNumber()));
- realtimeXValueLabel.setText(formatDeviceValue(device.getRealtimeX()));
- realtimeYValueLabel.setText(formatDeviceValue(device.getRealtimeY()));
- positioningStatusValueLabel.setText(formatDeviceValue(device.getPositioningStatus()));
- satelliteCountValueLabel.setText(formatDeviceValue(device.getSatelliteCount()));
- realtimeSpeedValueLabel.setText(formatDeviceValue(device.getRealtimeSpeed()));
- headingValueLabel.setText(formatDeviceValue(device.getHeading()));
- updateTimeValueLabel.setText(formatTimestamp(device.getGupdateTime()));
}
- private void setMowerInfoLabels(String value) {
- if (mowerNumberValueLabel != null) mowerNumberValueLabel.setText(value);
- if (realtimeXValueLabel != null) realtimeXValueLabel.setText(value);
- if (realtimeYValueLabel != null) realtimeYValueLabel.setText(value);
- if (positioningStatusValueLabel != null) positioningStatusValueLabel.setText(value);
- if (satelliteCountValueLabel != null) satelliteCountValueLabel.setText(value);
- if (realtimeSpeedValueLabel != null) realtimeSpeedValueLabel.setText(value);
- if (headingValueLabel != null) headingValueLabel.setText(value);
- if (updateTimeValueLabel != null) updateTimeValueLabel.setText(value);
- }
-
- private String sanitizeDeviceValue(String value) {
- if (value == null) {
- return null;
- }
- String trimmed = value.trim();
- if (trimmed.isEmpty() || "-1".equals(trimmed)) {
- return null;
- }
- return trimmed;
- }
-
- private String formatDeviceValue(String value) {
- String sanitized = sanitizeDeviceValue(value);
- return sanitized == null ? "--" : sanitized;
- }
-
- private String formatTimestamp(String value) {
- String sanitized = sanitizeDeviceValue(value);
- if (sanitized == null) {
- return "--";
- }
- try {
- long millis = Long.parseLong(sanitized);
- return TIMESTAMP_FORMAT.format(new Date(millis));
- } catch (NumberFormatException ex) {
- return sanitized;
+ public void showMowerInfo() {
+ if (mowerInfoManager != null) {
+ mowerInfoManager.showMowerInfo();
}
}
@@ -599,6 +1094,7 @@
if (updated.size() < 2) {
currentBoundary = null;
+ currentBoundaryPath = null;
boundaryBounds = null;
boundaryPointsVisible = false;
Dikuaiguanli.updateBoundaryPointVisibility(currentBoundaryLandNumber, false);
@@ -606,10 +1102,12 @@
adjustViewAfterBoundaryReset();
} else {
currentBoundary = updated;
+ rebuildBoundaryPath();
boundaryBounds = computeBounds(updated);
Dikuaiguanli.updateBoundaryPointVisibility(currentBoundaryLandNumber, boundaryPointsVisible);
visualizationPanel.repaint();
}
+ pendingTrackBreak = true;
}
private boolean persistBoundaryChanges(List<Point2D.Double> updatedBoundary) {
@@ -671,6 +1169,75 @@
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;
+ }
+ }
+
+ 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<Point2D.Double> 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;
+ }
+
/**
* 缁樺埗瑙嗗浘淇℃伅
@@ -731,6 +1298,8 @@
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();
@@ -752,8 +1321,10 @@
return;
}
- currentBoundary = parsed;
- boundaryBounds = computeBounds(parsed);
+ currentBoundary = parsed;
+ rebuildBoundaryPath();
+ pendingTrackBreak = true;
+ boundaryBounds = computeBounds(parsed);
Rectangle2D.Double bounds = boundaryBounds;
SwingUtilities.invokeLater(() -> {
@@ -764,25 +1335,35 @@
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) {
- if (landNumber == null) {
- clearObstacleData();
- if (!hasRenderableBoundary() && !hasRenderablePlannedPath()) {
- resetView();
- } else {
- visualizationPanel.repaint();
- }
- return;
- }
-
List<Obstacledge.Obstacle> parsed = parseObstacles(obstaclesData, landNumber);
- if (parsed.isEmpty()) {
+ applyObstaclesToRenderer(parsed, landNumber);
+ }
+
+ public void setCurrentObstacles(List<Obstacledge.Obstacle> obstacles, String landNumber) {
+ List<Obstacledge.Obstacle> cloned = cloneObstacles(obstacles, landNumber);
+ applyObstaclesToRenderer(cloned, landNumber);
+ }
+
+ private void applyObstaclesToRenderer(List<Obstacledge.Obstacle> obstacles, String landNumber) {
+ List<Obstacledge.Obstacle> 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();
@@ -792,8 +1373,19 @@
return;
}
- currentObstacles = parsed;
- obstacleBounds = convertObstacleBounds(Obstacledraw.getAllObstaclesBounds(parsed));
+ 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) {
@@ -807,10 +1399,150 @@
}
}
+ private List<Obstacledge.Obstacle> cloneObstacles(List<Obstacledge.Obstacle> obstacles, String landNumber) {
+ List<Obstacledge.Obstacle> result = new ArrayList<>();
+ if (obstacles == null || obstacles.isEmpty()) {
+ return result;
+ }
+
+ String normalizedLandNumber = landNumber != null ? landNumber.trim() : null;
+ Set<String> 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<Obstacledge.XYCoordinate> xySource = source.getXyCoordinates();
+ if (shape == null || xySource == null) {
+ continue;
+ }
+
+ List<Obstacledge.XYCoordinate> 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<Obstacledge.DMCoordinate> dmCopy = copyDMCoordinates(source.getOriginalCoordinates());
+ if (dmCopy.isEmpty()) {
+ populateDummyOriginalCoordinates(copy, xyCopy.size());
+ } else {
+ copy.setOriginalCoordinates(dmCopy);
+ }
+
+ result.add(copy);
+ }
+
+ return result;
+ }
+
+ private List<Obstacledge.Obstacle> filterObstaclesForLand(List<Obstacledge.Obstacle> 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<Obstacledge.Obstacle> 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<String> 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<Obstacledge.XYCoordinate> copyXYCoordinates(List<Obstacledge.XYCoordinate> source) {
+ List<Obstacledge.XYCoordinate> 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<Obstacledge.DMCoordinate> copyDMCoordinates(List<Obstacledge.DMCoordinate> source) {
+ List<Obstacledge.DMCoordinate> 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;
}
private List<Obstacledge.Obstacle> parseObstacles(String obstaclesData, String landNumber) {
@@ -921,6 +1653,9 @@
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) {
@@ -930,6 +1665,10 @@
if (trimmed.isEmpty()) {
continue;
}
+ trimmed = trimmed.replace("(", "").replace(")", "");
+ if (trimmed.isEmpty()) {
+ continue;
+ }
String[] parts = trimmed.split(",");
if (parts.length < 2) {
continue;
@@ -1091,6 +1830,42 @@
visualizationPanel.repaint();
}
+ 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();
+ visualizationPanel.repaint();
+ }
+
+ public List<Point2D.Double> getHandheldBoundaryPreviewPoints() {
+ return new ArrayList<>(handheldBoundaryPreview);
+ }
+
private List<Point2D.Double> parseBoundary(String boundaryCoordinates) {
List<Point2D.Double> points = new ArrayList<>();
String[] entries = boundaryCoordinates.split(";");
@@ -1158,12 +1933,7 @@
public void dispose() {
mowerUpdateTimer.stop();
- stopMowerInfoTimer();
- if (mowerInfoDialog != null) {
- mowerInfoDialog.dispose();
- mowerInfoDialog = null;
- }
- resetMowerInfoLabels();
+ mowerInfoManager.dispose();
}
}
\ No newline at end of file
--
Gitblit v1.10.0