张世豪
2025-12-02 6799351be12deb2f713f2c0a2b4c467a6d1098c3
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();
        
        // 修改设置按钮:使用图片替代Unicode图标
        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);
    }
@@ -311,6 +331,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>");
        button.setFont(new Font("微软雅黑", Font.PLAIN, 12));
@@ -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() {
    private void toggleStartPause() {
        if (startBtn == null) {
            return;
        }
        startButtonShowingPause = !startButtonShowingPause;
        if (startButtonShowingPause) {
        statusLabel.setText("作业中");
        startBtn.setEnabled(false);
        pauseBtn.setEnabled(true);
            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,27 +800,28 @@
    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();
        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);
        if (floatingButtonPanel.getParent() != visualizationPanel) {
@@ -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);
        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);