From 6799351be12deb2f713f2c0a2b4c467a6d1098c3 Mon Sep 17 00:00:00 2001
From: 张世豪 <979909237@qq.com>
Date: 星期二, 02 十二月 2025 19:51:00 +0800
Subject: [PATCH] 2025122

---
 src/zhuye/Shouye.java |  550 ++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 503 insertions(+), 47 deletions(-)

diff --git a/src/zhuye/Shouye.java b/src/zhuye/Shouye.java
index 14565cb..4f20092 100644
--- a/src/zhuye/Shouye.java
+++ b/src/zhuye/Shouye.java
@@ -1,6 +1,7 @@
 package zhuye;
 
 import javax.swing.*;
+import javax.swing.Timer;
 
 import baseStation.BaseStation;
 import baseStation.BaseStationDialog;
@@ -14,6 +15,8 @@
 import gecaoji.Device;
 import set.Sets;
 import udpdell.UDPServer;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -28,6 +31,7 @@
     private final Color THEME_HOVER_COLOR = new Color(30, 107, 69);
     private final Color BACKGROUND_COLOR = new Color(250, 250, 250);
     private final Color PANEL_BACKGROUND = new Color(255, 255, 255);
+    private final Color STATUS_PAUSE_COLOR = new Color(255, 165, 0);
     
     // 缁勪欢
     private JPanel headerPanel;
@@ -38,11 +42,12 @@
     
     // 鎸夐挳
     private JButton startBtn;
-    private JButton pauseBtn;
+    private JButton stopBtn;
     private JButton legendBtn;
     private JButton remoteBtn;
     private JButton areaSelectBtn;
     private JButton baseStationBtn;
+    private JButton bluetoothBtn;
     
     // 瀵艰埅鎸夐挳
     private JButton homeNavBtn;
@@ -76,6 +81,8 @@
     private ImageIcon pauseIcon;
     private ImageIcon pauseActiveIcon;
     private ImageIcon endIcon;
+    private ImageIcon bluetoothIcon;
+    private ImageIcon bluetoothLinkedIcon;
     private JPanel circleGuidancePanel;
     private JLabel circleGuidanceLabel;
     private JButton circleGuidancePrimaryButton;
@@ -84,6 +91,14 @@
     private JDialog circleGuidanceDialog;
     private boolean circleDialogMode;
     private ComponentAdapter circleDialogOwnerAdapter;
+    private static final double METERS_PER_DEGREE_LAT = 111320.0d;
+    private final List<double[]> circleCapturedPoints = new ArrayList<>();
+    private double[] circleBaseLatLon;
+    private Timer circleDataMonitor;
+    private Coordinate lastCapturedCoordinate;
+    private boolean startButtonShowingPause = true;
+    private boolean stopButtonActive = false;
+    private boolean bluetoothConnected = false;
     
     public Shouye() {
         instance = this;
@@ -163,6 +178,11 @@
         statusLabel = new JLabel("寰呮満");
         statusLabel.setFont(new Font("寰蒋闆呴粦", Font.PLAIN, 14));
         statusLabel.setForeground(Color.GRAY);
+        statusLabel.addPropertyChangeListener("text", evt -> {
+            Object newValue = evt.getNewValue();
+            applyStatusLabelColor(newValue instanceof String ? (String) newValue : null);
+        });
+        applyStatusLabelColor(statusLabel.getText());
         
         leftInfoPanel.add(areaNameLabel);
         leftInfoPanel.add(statusLabel);
@@ -171,7 +191,7 @@
         JPanel rightActionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
         rightActionPanel.setBackground(PANEL_BACKGROUND);
         
-        JButton notificationBtn = createIconButton("馃敂", 40);
+    bluetoothBtn = createBluetoothButton();
         
         // 淇敼璁剧疆鎸夐挳锛氫娇鐢ㄥ浘鐗囨浛浠nicode鍥炬爣
         JButton settingsBtn = new JButton();
@@ -203,7 +223,7 @@
         // 娣诲姞璁剧疆鎸夐挳浜嬩欢
         settingsBtn.addActionListener(e -> showSettingsDialog());
         
-        rightActionPanel.add(notificationBtn);
+    rightActionPanel.add(bluetoothBtn);
         rightActionPanel.add(settingsBtn);
         
         headerPanel.add(leftInfoPanel, BorderLayout.WEST);
@@ -260,14 +280,14 @@
         JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 20, 0));
         buttonPanel.setBackground(PANEL_BACKGROUND);
         
-    startBtn = createControlButton("寮�濮�", THEME_COLOR);
-    applyButtonIcon(startBtn, "image/startzuoye.png");
-    pauseBtn = createControlButton("鏆傚仠", Color.ORANGE);
-    applyButtonIcon(pauseBtn, "image/zantingzuoye.png");
-        pauseBtn.setEnabled(false);
+        startBtn = createControlButton("鏆傚仠", THEME_COLOR);
+        updateStartButtonAppearance();
+
+        stopBtn = createControlButton("缁撴潫", Color.ORANGE);
+        updateStopButtonIcon();
         
         buttonPanel.add(startBtn);
-        buttonPanel.add(pauseBtn);
+        buttonPanel.add(stopBtn);
         
         controlPanel.add(buttonPanel, BorderLayout.CENTER);
     }
@@ -310,6 +330,35 @@
         
         return button;
     }
+
+    private JButton createBluetoothButton() {
+        JButton button = new JButton();
+        button.setPreferredSize(new Dimension(40, 40));
+        button.setBackground(PANEL_BACKGROUND);
+        button.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        button.setFocusPainted(false);
+        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+        button.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseEntered(MouseEvent e) {
+                button.setBackground(new Color(240, 240, 240));
+            }
+
+            @Override
+            public void mouseExited(MouseEvent e) {
+                button.setBackground(PANEL_BACKGROUND);
+            }
+        });
+        ensureBluetoothIconsLoaded();
+        bluetoothConnected = Bluelink.isConnected();
+        ImageIcon initialIcon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon;
+        if (initialIcon != null) {
+            button.setIcon(initialIcon);
+        } else {
+            button.setText(bluetoothConnected ? "宸茶繛" : "钃濈墮");
+        }
+        return button;
+    }
     
     private JButton createFunctionButton(String text, String icon) {
         JButton button = new JButton("<html><center>" + icon + "<br>" + text + "</center></html>");
@@ -424,6 +473,9 @@
         
         // 鍔熻兘鎸夐挳浜嬩欢
         legendBtn.addActionListener(e -> showLegendDialog());
+        if (bluetoothBtn != null) {
+            bluetoothBtn.addActionListener(e -> toggleBluetoothConnection());
+        }
         remoteBtn.addActionListener(e -> showRemoteControlDialog());
         areaSelectBtn.addActionListener(e -> {
             // 鐐瑰嚮鈥滃湴鍧椻�濈洿鎺ユ墦寮�鍦板潡绠$悊瀵硅瘽妗嗭紙鑻ラ渶瑕佸彲浼犲叆鐗瑰畾鍦板潡缂栧彿锛�
@@ -432,8 +484,8 @@
         baseStationBtn.addActionListener(e -> showBaseStationDialog());
         
         // 鎺у埗鎸夐挳浜嬩欢
-        startBtn.addActionListener(e -> startMowing());
-        pauseBtn.addActionListener(e -> pauseMowing());
+    startBtn.addActionListener(e -> toggleStartPause());
+    stopBtn.addActionListener(e -> handleStopAction());
     }
     
     private void showSettingsDialog() {
@@ -563,16 +615,107 @@
         }
     }
     
-    private void startMowing() {
-        statusLabel.setText("浣滀笟涓�");
-        startBtn.setEnabled(false);
-        pauseBtn.setEnabled(true);
+    private void toggleStartPause() {
+        if (startBtn == null) {
+            return;
+        }
+        startButtonShowingPause = !startButtonShowingPause;
+        if (startButtonShowingPause) {
+            statusLabel.setText("浣滀笟涓�");
+            if (stopButtonActive) {
+                stopButtonActive = false;
+                updateStopButtonIcon();
+            }
+        } else {
+            statusLabel.setText("鏆傚仠涓�");
+        }
+        updateStartButtonAppearance();
     }
-    
-    private void pauseMowing() {
-        statusLabel.setText("鏆傚仠涓�");
-        startBtn.setEnabled(true);
-        pauseBtn.setEnabled(false);
+
+    private void handleStopAction() {
+        stopButtonActive = !stopButtonActive;
+        updateStopButtonIcon();
+        if (stopButtonActive) {
+            statusLabel.setText("宸茬粨鏉�");
+            startButtonShowingPause = false;
+        } else {
+            statusLabel.setText("寰呮満");
+            startButtonShowingPause = true;
+        }
+        updateStartButtonAppearance();
+    }
+
+    private void updateStartButtonAppearance() {
+        if (startBtn == null) {
+            return;
+        }
+        String iconPath = startButtonShowingPause ? "image/start0.png" : "image/start1.png";
+        startBtn.setText(startButtonShowingPause ? "鏆傚仠" : "寮�濮�");
+        applyButtonIcon(startBtn, iconPath);
+    }
+
+    private void updateStopButtonIcon() {
+        if (stopBtn == null) {
+            return;
+        }
+        String iconPath = stopButtonActive ? "image/stop1.png" : "image/stop0.png";
+        applyButtonIcon(stopBtn, iconPath);
+    }
+
+    private void toggleBluetoothConnection() {
+        if (bluetoothBtn == null) {
+            return;
+        }
+        if (Bluelink.isConnected()) {
+            Bluelink.disconnect();
+            bluetoothConnected = false;
+        } else {
+            boolean success = Bluelink.connect();
+            if (success) {
+                bluetoothConnected = true;
+            } else {
+                bluetoothConnected = false;
+                JOptionPane.showMessageDialog(this, "钃濈墮杩炴帴澶辫触锛岃閲嶈瘯", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            }
+        }
+        updateBluetoothButtonIcon();
+    }
+
+    private void updateBluetoothButtonIcon() {
+        if (bluetoothBtn == null) {
+            return;
+        }
+        ensureBluetoothIconsLoaded();
+        bluetoothConnected = Bluelink.isConnected();
+        ImageIcon icon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon;
+        if (icon != null) {
+            bluetoothBtn.setIcon(icon);
+            bluetoothBtn.setText(null);
+        } else {
+            bluetoothBtn.setText(bluetoothConnected ? "宸茶繛" : "钃濈墮");
+        }
+    }
+
+    private void applyStatusLabelColor(String statusText) {
+        if (statusLabel == null) {
+            return;
+        }
+        if ("浣滀笟涓�".equals(statusText)) {
+            statusLabel.setForeground(THEME_COLOR);
+        } else if ("鏆傚仠涓�".equals(statusText)) {
+            statusLabel.setForeground(STATUS_PAUSE_COLOR);
+        } else {
+            statusLabel.setForeground(Color.GRAY);
+        }
+    }
+
+    private void ensureBluetoothIconsLoaded() {
+        if (bluetoothIcon == null) {
+            bluetoothIcon = loadScaledIcon("image/blue.png", 28, 28);
+        }
+        if (bluetoothLinkedIcon == null) {
+            bluetoothLinkedIcon = loadScaledIcon("image/bluelink.png", 28, 28);
+        }
     }
 
     private JButton createFloatingIconButton() {
@@ -657,26 +800,27 @@
     public void showEndDrawingButton(Runnable callback, String drawingShape) {
         endDrawingCallback = callback;
         applyDrawingPauseState(false, false);
-        circleDialogMode = drawingShape != null && "circle".equalsIgnoreCase(drawingShape);
-
-        if (circleDialogMode) {
-            hideFloatingDrawingControls();
-            ensureCircleGuidancePanel();
-            showCircleGuidanceStep(1);
-            visualizationPanel.revalidate();
-            visualizationPanel.repaint();
-            return;
-        }
-
         circleDialogMode = false;
         hideCircleGuidancePanel();
 
         ensureFloatingIconsLoaded();
         ensureFloatingButtonInfrastructure();
 
-        endDrawingButton.setVisible(true);
-        if (drawingPauseButton != null) {
-            drawingPauseButton.setVisible(true);
+        boolean enableCircleGuidance = drawingShape != null
+                && "circle".equalsIgnoreCase(drawingShape.trim());
+        if (enableCircleGuidance) {
+            prepareCircleGuidanceState();
+            showCircleGuidanceStep(1);
+            endDrawingButton.setVisible(false);
+            if (drawingPauseButton != null) {
+                drawingPauseButton.setVisible(false);
+            }
+        } else {
+            clearCircleGuidanceArtifacts();
+            endDrawingButton.setVisible(true);
+            if (drawingPauseButton != null) {
+                drawingPauseButton.setVisible(true);
+            }
         }
 
         floatingButtonPanel.setVisible(true);
@@ -773,20 +917,33 @@
         }
         circleGuidanceStep = step;
         if (step == 1) {
-            circleGuidanceLabel.setText("鏄惁灏嗗綋鍓嶇偣璁剧疆涓哄渾蹇冿紵");
-            circleGuidancePrimaryButton.setText("鏄�");
+            circleGuidanceLabel.setText("閲囬泦绗�1涓偣");
+            circleGuidancePrimaryButton.setText("纭绗�1鐐�");
             circleGuidanceSecondaryButton.setText("杩斿洖");
             circleGuidanceSecondaryButton.setVisible(true);
         } else if (step == 2) {
-            circleGuidanceLabel.setText("鏄惁灏嗗綋鍓嶇偣璁剧疆涓哄崐寰勭偣锛�");
-            circleGuidancePrimaryButton.setText("鏄�");
-            circleGuidanceSecondaryButton.setVisible(false);
+            circleGuidanceLabel.setText("閲囬泦绗�2涓偣");
+            circleGuidancePrimaryButton.setText("纭绗�2鐐�");
+            circleGuidanceSecondaryButton.setText("杩斿洖");
+            circleGuidanceSecondaryButton.setVisible(true);
+        } else if (step == 3) {
+            circleGuidanceLabel.setText("閲囬泦绗�3涓偣");
+            circleGuidancePrimaryButton.setText("纭绗�3鐐�");
+            circleGuidanceSecondaryButton.setText("杩斿洖");
+            circleGuidanceSecondaryButton.setVisible(true);
+        } else if (step == 4) {
+            circleGuidanceLabel.setText("宸查噰闆嗕笁涓偣");
+            circleGuidancePrimaryButton.setText("缁撴潫缁樺埗");
+            circleGuidanceSecondaryButton.setText("閲嶆柊閲囬泦");
+            circleGuidanceSecondaryButton.setVisible(true);
         } else {
             hideCircleGuidancePanel();
             return;
         }
         circleGuidancePanel.setVisible(true);
 
+        refreshCircleGuidanceButtonAvailability();
+
         if (circleDialogMode) {
             ensureCircleGuidanceDialog();
             if (circleGuidanceDialog != null) {
@@ -933,6 +1090,9 @@
         button.addMouseListener(new MouseAdapter() {
             @Override
             public void mouseEntered(MouseEvent e) {
+                if (!button.isEnabled()) {
+                    return;
+                }
                 if (filled) {
                     button.setBackground(THEME_HOVER_COLOR);
                 } else {
@@ -942,6 +1102,9 @@
 
             @Override
             public void mouseExited(MouseEvent e) {
+                if (!button.isEnabled()) {
+                    return;
+                }
                 if (filled) {
                     button.setBackground(bg);
                 } else {
@@ -953,20 +1116,25 @@
     }
 
     private void onCircleGuidancePrimary() {
-        if (circleGuidanceStep == 1) {
-            showCircleGuidanceStep(2);
-        } else {
+        if (circleGuidanceStep >= 1 && circleGuidanceStep <= 3) {
+            if (!recordCircleSamplePoint()) {
+                return;
+            }
+            if (circleGuidanceStep == 3) {
+                showCircleGuidanceStep(4);
+            } else {
+                showCircleGuidanceStep(circleGuidanceStep + 1);
+            }
+        } else if (circleGuidanceStep == 4) {
             handleCircleCompletion();
         }
     }
 
     private void onCircleGuidanceSecondary() {
-        if (circleGuidanceStep == 1) {
+        if (circleGuidanceStep == 1 || circleGuidanceStep == 2 || circleGuidanceStep == 3) {
             handleCircleAbort(null);
-        } else if (circleGuidanceStep == 2) {
-            handleCircleAbort(null);
-        } else {
-            hideCircleGuidancePanel();
+        } else if (circleGuidanceStep == 4) {
+            restartCircleCapture();
         }
     }
 
@@ -991,6 +1159,7 @@
 
     private void handleCircleCompletion() {
         hideCircleGuidancePanel();
+        clearCircleGuidanceArtifacts();
         if (endDrawingCallback != null) {
             endDrawingCallback.run();
         } else {
@@ -998,8 +1167,295 @@
         }
     }
 
+    private void prepareCircleGuidanceState() {
+        clearCircleGuidanceArtifacts();
+        circleBaseLatLon = resolveCircleBaseLatLon();
+        startCircleDataMonitor();
+    }
+
+    private void clearCircleGuidanceArtifacts() {
+        stopCircleDataMonitor();
+        circleCapturedPoints.clear();
+        circleBaseLatLon = null;
+        lastCapturedCoordinate = null;
+        if (mapRenderer != null) {
+            mapRenderer.clearCircleCaptureOverlay();
+            mapRenderer.clearCircleSampleMarkers();
+        }
+    }
+
+    private void restartCircleCapture() {
+        clearCircleGuidanceArtifacts();
+        synchronized (Coordinate.coordinates) {
+            Coordinate.coordinates.clear();
+        }
+        Coordinate.setStartSaveGngga(true);
+        circleBaseLatLon = resolveCircleBaseLatLon();
+        showCircleGuidanceStep(1);
+        startCircleDataMonitor();
+    }
+
+    private boolean recordCircleSamplePoint() {
+        Coordinate latest = getLatestCoordinate();
+        if (latest == null) {
+            JOptionPane.showMessageDialog(this, "鏈幏鍙栧埌褰撳墠浣嶇疆鍧愭爣锛岃绋嶅悗閲嶈瘯銆�", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return false;
+        }
+
+        double[] base = ensureCircleBaseLatLon();
+        if (base == null) {
+            JOptionPane.showMessageDialog(this, "鍩哄噯绔欏潗鏍囨棤鏁堬紝璇峰厛鍦ㄥ熀鍑嗙珯绠$悊涓畬鎴愯缃��", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return false;
+        }
+
+        double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection());
+        double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection());
+        if (!Double.isFinite(lat) || !Double.isFinite(lon)) {
+            JOptionPane.showMessageDialog(this, "閲囬泦鐐瑰潗鏍囨棤鏁堬紝璇烽噸鏂伴噰闆嗐��", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+            return false;
+        }
+
+        double[] local = convertLatLonToLocal(lat, lon, base[0], base[1]);
+        circleCapturedPoints.add(local);
+        if (mapRenderer != null) {
+            mapRenderer.updateCircleSampleMarkers(circleCapturedPoints);
+        }
+        lastCapturedCoordinate = latest;
+        refreshCircleGuidanceButtonAvailability();
+
+        if (circleCapturedPoints.size() >= 3) {
+            CircleSolution solution = fitCircleFromPoints(circleCapturedPoints);
+            if (solution == null) {
+                circleCapturedPoints.remove(circleCapturedPoints.size() - 1);
+                JOptionPane.showMessageDialog(this, "鏃犳硶鏍规嵁褰撳墠涓変釜鐐圭敓鎴愬渾锛岃閲嶆柊閲囬泦鐐广��", "鎻愮ず", JOptionPane.WARNING_MESSAGE);
+                restartCircleCapture();
+                return false;
+            }
+            if (mapRenderer != null) {
+                mapRenderer.showCircleCaptureOverlay(solution.centerX, solution.centerY, solution.radius, circleCapturedPoints);
+            }
+        }
+
+        return true;
+    }
+
+    private void startCircleDataMonitor() {
+        if (circleDataMonitor == null) {
+            circleDataMonitor = new Timer(600, e -> refreshCircleGuidanceButtonAvailability());
+            circleDataMonitor.setRepeats(true);
+        }
+        if (!circleDataMonitor.isRunning()) {
+            circleDataMonitor.start();
+        }
+        refreshCircleGuidanceButtonAvailability();
+    }
+
+    private void stopCircleDataMonitor() {
+        if (circleDataMonitor != null) {
+            circleDataMonitor.stop();
+        }
+    }
+
+    private void refreshCircleGuidanceButtonAvailability() {
+        if (circleGuidancePrimaryButton == null) {
+            return;
+        }
+        boolean shouldEnable = circleGuidanceStep >= 1 && circleGuidanceStep <= 3
+                ? isCircleDataAvailable()
+                : true;
+        applyCirclePrimaryButtonState(shouldEnable);
+    }
+
+    private boolean isCircleDataAvailable() {
+        Coordinate latest = getLatestCoordinate();
+        if (latest == null) {
+            return false;
+        }
+        return lastCapturedCoordinate == null || latest != lastCapturedCoordinate;
+    }
+
+    private void applyCirclePrimaryButtonState(boolean enabled) {
+        if (circleGuidancePrimaryButton == null) {
+            return;
+        }
+        if (circleGuidanceStep >= 4) {
+            enabled = true;
+        }
+        circleGuidancePrimaryButton.setEnabled(enabled);
+        if (enabled) {
+            circleGuidancePrimaryButton.setBackground(THEME_COLOR);
+            circleGuidancePrimaryButton.setForeground(Color.WHITE);
+            circleGuidancePrimaryButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+        } else {
+            circleGuidancePrimaryButton.setBackground(new Color(200, 200, 200));
+            circleGuidancePrimaryButton.setForeground(new Color(120, 120, 120));
+            circleGuidancePrimaryButton.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+        }
+    }
+
+    private Coordinate getLatestCoordinate() {
+        synchronized (Coordinate.coordinates) {
+            int size = Coordinate.coordinates.size();
+            if (size == 0) {
+                return null;
+            }
+            return Coordinate.coordinates.get(size - 1);
+        }
+    }
+
+    private double[] ensureCircleBaseLatLon() {
+        if (circleBaseLatLon == null) {
+            circleBaseLatLon = resolveCircleBaseLatLon();
+        }
+        return circleBaseLatLon;
+    }
+
+    private double[] resolveCircleBaseLatLon() {
+        String coords = null;
+        String landNumber = Dikuaiguanli.getCurrentWorkLandNumber();
+        if (isMeaningfulValue(landNumber)) {
+            Dikuai current = Dikuai.getDikuai(landNumber);
+            if (current != null) {
+                coords = current.getBaseStationCoordinates();
+            }
+        }
+        if (!isMeaningfulValue(coords)) {
+            coords = addzhangaiwu.getActiveSessionBaseStation();
+        }
+        if (!isMeaningfulValue(coords) && baseStation != null) {
+            coords = baseStation.getInstallationCoordinates();
+        }
+        if (!isMeaningfulValue(coords)) {
+            return null;
+        }
+        String[] parts = coords.split(",");
+        if (parts.length < 4) {
+            return null;
+        }
+        double baseLat = parseDMToDecimal(parts[0], parts[1]);
+        double baseLon = parseDMToDecimal(parts[2], parts[3]);
+        if (!Double.isFinite(baseLat) || !Double.isFinite(baseLon)) {
+            return null;
+        }
+        return new double[]{baseLat, baseLon};
+    }
+
+    private double parseDMToDecimal(String dmm, String direction) {
+        if (dmm == null || dmm.trim().isEmpty()) {
+            return Double.NaN;
+        }
+        try {
+            String trimmed = dmm.trim();
+            int dotIndex = trimmed.indexOf('.');
+            if (dotIndex < 2) {
+                return Double.NaN;
+            }
+            int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2));
+            double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2));
+            double decimal = degrees + minutes / 60.0;
+            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) {
+                decimal = -decimal;
+            }
+            return decimal;
+        } catch (NumberFormatException ex) {
+            return Double.NaN;
+        }
+    }
+
+    private double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) {
+        double deltaLat = lat - baseLat;
+        double deltaLon = lon - baseLon;
+        double meanLatRad = Math.toRadians((baseLat + lat) / 2.0);
+        double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad);
+        double northMeters = deltaLat * METERS_PER_DEGREE_LAT;
+        return new double[]{eastMeters, northMeters};
+    }
+
+    private CircleSolution fitCircleFromPoints(List<double[]> points) {
+        if (points == null || points.size() < 3) {
+            return null;
+        }
+        CircleSolution best = null;
+        double bestScore = 0.0;
+        int n = points.size();
+        for (int i = 0; i < n - 2; i++) {
+            double[] p1 = points.get(i);
+            for (int j = i + 1; j < n - 1; j++) {
+                double[] p2 = points.get(j);
+                for (int k = j + 1; k < n; k++) {
+                    double[] p3 = points.get(k);
+                    CircleSolution candidate = circleFromThreePoints(p1, p2, p3);
+                    if (candidate == null || candidate.radius <= 0) {
+                        continue;
+                    }
+                    double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3)));
+                    if (minEdge > bestScore) {
+                        bestScore = minEdge;
+                        best = candidate;
+                    }
+                }
+            }
+        }
+        return best;
+    }
+
+    private CircleSolution circleFromThreePoints(double[] p1, double[] p2, double[] p3) {
+        if (p1 == null || p2 == null || p3 == null) {
+            return null;
+        }
+        double x1 = p1[0];
+        double y1 = p1[1];
+        double x2 = p2[0];
+        double y2 = p2[1];
+        double x3 = p3[0];
+        double y3 = p3[1];
+
+        double a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2;
+        double d = 2.0 * a;
+        if (Math.abs(d) < 1e-6) {
+            return null;
+        }
+
+        double x1Sq = x1 * x1 + y1 * y1;
+        double x2Sq = x2 * x2 + y2 * y2;
+        double x3Sq = x3 * x3 + y3 * y3;
+
+        double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / d;
+        double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / d;
+        double radius = Math.hypot(centerX - x1, centerY - y1);
+        if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) {
+            return null;
+        }
+        if (radius < 0.05) {
+            return null;
+        }
+        return new CircleSolution(centerX, centerY, radius);
+    }
+
+    private double distance(double[] a, double[] b) {
+        if (a == null || b == null) {
+            return 0.0;
+        }
+        double dx = a[0] - b[0];
+        double dy = a[1] - b[1];
+        return Math.hypot(dx, dy);
+    }
+
+    private static final class CircleSolution {
+        final double centerX;
+        final double centerY;
+        final double radius;
+
+        CircleSolution(double centerX, double centerY, double radius) {
+            this.centerX = centerX;
+            this.centerY = centerY;
+            this.radius = radius;
+        }
+    }
+
     public void hideEndDrawingButton() {
         hideCircleGuidancePanel();
+        clearCircleGuidanceArtifacts();
         hideFloatingDrawingControls();
         circleDialogMode = false;
         applyDrawingPauseState(false, false);

--
Gitblit v1.10.0