张世豪
2025-12-09 32524195d474b74e48916867b2a6c2f022a40d98
src/zhuye/Shouye.java
@@ -4,20 +4,28 @@
import javax.swing.Timer;
import baseStation.BaseStation;
import set.Setsys;
import baseStation.BaseStationDialog;
import java.awt.*;
import java.awt.event.*;
import chuankou.dellmessage;
import dikuai.Dikuai;
import dikuai.Dikuaiguanli;
import dikuai.addzhangaiwu;
import gecaoji.Device;
import set.Sets;
import udpdell.UDPServer;
import zhangaiwu.AddDikuai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Consumer;
import java.awt.geom.Point2D;
/**
 * 首页界面 - 适配6.5寸竖屏,使用独立的MapRenderer进行绘制
@@ -48,6 +56,11 @@
    private JButton areaSelectBtn;
    private JButton baseStationBtn;
    private JButton bluetoothBtn;
    private JLabel dataPacketCountLabel;
    private JLabel mowerSpeedValueLabel;
    private JLabel mowerSpeedUnitLabel;
    private JLabel mowingProgressLabel;
    private FixQualityIndicator fixQualityIndicator;
    
    // 导航按钮
    private JButton homeNavBtn;
@@ -68,6 +81,8 @@
    private BaseStationDialog baseStationDialog;
    private Sets settingsDialog;
    private BaseStation baseStation;
    private final Consumer<String> serialLineListener = line -> SwingUtilities.invokeLater(this::updateDataPacketCountLabel);
    
    // 地图渲染器
    private MapRenderer mapRenderer;
@@ -77,6 +92,11 @@
    private JPanel floatingButtonPanel;
    private JPanel floatingButtonColumn;
    private Runnable endDrawingCallback;
    private JButton pathPreviewReturnButton;
    private boolean pathPreviewActive;
    private Runnable pathPreviewReturnAction;
    private String previewRestoreLandNumber;
    private String previewRestoreLandName;
    private boolean drawingPaused;
    private ImageIcon pauseIcon;
    private ImageIcon pauseActiveIcon;
@@ -92,20 +112,45 @@
    private boolean circleDialogMode;
    private ComponentAdapter circleDialogOwnerAdapter;
    private static final double METERS_PER_DEGREE_LAT = 111320.0d;
    private static final double HANDHELD_DUPLICATE_THRESHOLD_METERS = 0.01d;
    private final List<double[]> circleCapturedPoints = new ArrayList<>();
    private double[] circleBaseLatLon;
    private Timer circleDataMonitor;
    private Coordinate lastCapturedCoordinate;
    private boolean handheldCaptureActive;
    private int handheldCapturedPoints;
    private final List<Point2D.Double> handheldTemporaryPoints = new ArrayList<>();
    private final List<Point2D.Double> mowerTemporaryPoints = new ArrayList<>();
    private enum BoundaryCaptureMode { NONE, HANDHELD, MOWER }
    private BoundaryCaptureMode activeBoundaryMode = BoundaryCaptureMode.NONE;
    private boolean mowerBoundaryCaptureActive;
    private Timer mowerBoundaryMonitor;
    private Coordinate lastMowerCoordinate;
    private double[] mowerBaseLatLon;
    private boolean startButtonShowingPause = true;
    private boolean stopButtonActive = false;
    private boolean bluetoothConnected = false;
    private Timer mowerSpeedRefreshTimer;
    private boolean drawingControlModeActive;
    private boolean storedStartButtonShowingPause;
    private boolean storedStopButtonActive;
    private String storedStatusBeforeDrawing;
    private boolean handheldCaptureInlineUiActive;
    private Timer handheldCaptureStatusTimer;
    private String handheldCaptureStoredStatusText;
    private Color handheldStartButtonOriginalBackground;
    private Color handheldStartButtonOriginalForeground;
    private Color handheldStopButtonOriginalBackground;
    private Color handheldStopButtonOriginalForeground;
    
    public Shouye() {
        instance = this;
        baseStation = new BaseStation();
        baseStation.load();
        dellmessage.registerLineListener(serialLineListener);
        initializeUI();
        setupEventHandlers();
        scheduleIdentifierCheck();
    }
    public static Shouye getInstance() {
@@ -128,7 +173,8 @@
        add(controlPanel, BorderLayout.SOUTH);
        
        // 初始化地图渲染器
        mapRenderer = new MapRenderer(visualizationPanel);
    mapRenderer = new MapRenderer(visualizationPanel);
    applyIdleTrailDurationFromSettings();
        
        // 初始化对话框引用为null,延迟创建
        legendDialog = null;
@@ -143,6 +189,48 @@
        initializeDefaultAreaSelection();
        refreshMapForSelectedArea();
    }
    private void scheduleIdentifierCheck() {
        HierarchyListener listener = new HierarchyListener() {
            @Override
            public void hierarchyChanged(HierarchyEvent e) {
                if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && Shouye.this.isShowing()) {
                    Shouye.this.removeHierarchyListener(this);
                    SwingUtilities.invokeLater(() -> {
                        Shouye.this.checkIdentifiersAndPromptIfNeeded();
                        Shouye.this.showInitialMowerSelfCheckDialogIfNeeded();
                    });
                }
            }
        };
        addHierarchyListener(listener);
    }
    private void showInitialMowerSelfCheckDialogIfNeeded() {
        zijian.showInitialPromptIfNeeded(this, this::showRemoteControlDialog);
    }
    private void applyIdleTrailDurationFromSettings() {
        if (mapRenderer == null) {
            return;
        }
        int durationSeconds = MapRenderer.DEFAULT_IDLE_TRAIL_DURATION_SECONDS;
        String configuredValue = Setsys.getPropertyValue("idleTrailDurationSeconds");
        if (configuredValue != null) {
            String trimmed = configuredValue.trim();
            if (!trimmed.isEmpty()) {
                try {
                    int parsed = Integer.parseInt(trimmed);
                    if (parsed >= 5 && parsed <= 600) {
                        durationSeconds = parsed;
                    }
                } catch (NumberFormatException ignored) {
                    durationSeconds = MapRenderer.DEFAULT_IDLE_TRAIL_DURATION_SECONDS;
                }
            }
        }
        mapRenderer.setIdleTrailDurationSeconds(durationSeconds);
    }
    
    private void createHeaderPanel() {
        headerPanel = new JPanel(new BorderLayout());
@@ -247,6 +335,9 @@
        };
        visualizationPanel.setLayout(new BorderLayout());
        
    JPanel speedIndicatorPanel = createSpeedIndicatorPanel();
    visualizationPanel.add(speedIndicatorPanel, BorderLayout.NORTH);
        // 创建功能按钮面板(放在左上角)
        JPanel functionButtonsPanel = new JPanel();
        functionButtonsPanel.setLayout(new BoxLayout(functionButtonsPanel, BoxLayout.Y_AXIS));
@@ -269,6 +360,8 @@
        visualizationPanel.add(functionButtonsPanel, BorderLayout.WEST);
        
        mainContentPanel.add(visualizationPanel, BorderLayout.CENTER);
    startMowerSpeedUpdates();
    }
    
    private void createControlPanel() {
@@ -525,7 +618,44 @@
                remoteDialog = new RemoteControlDialog((JFrame) null, THEME_COLOR);
            }
        }
        remoteDialog.setVisible(true);
        if (remoteDialog != null) {
            positionRemoteDialogBottomCenter(remoteDialog);
            zijian.markSelfCheckCompleted();
            remoteDialog.setVisible(true);
        }
    }
    private void positionRemoteDialogBottomCenter(RemoteControlDialog dialog) {
        if (dialog == null) {
            return;
        }
        Rectangle targetBounds = computeVisualizationBoundsOnScreen();
        Dimension dialogSize = dialog.getSize();
        int x;
        int y;
        if (targetBounds != null) {
            x = targetBounds.x + (targetBounds.width - dialogSize.width) / 2;
            y = targetBounds.y + targetBounds.height - dialogSize.height;
        } else {
            Component parentComponent = this;
            Point parentOnScreen = parentComponent.getLocationOnScreen();
            int parentWidth = parentComponent.getWidth();
            int parentHeight = parentComponent.getHeight();
            x = parentOnScreen.x + (parentWidth - dialogSize.width) / 2;
            y = parentOnScreen.y + parentHeight - dialogSize.height;
        }
        dialog.setLocation(Math.max(x, 0), Math.max(y, 0));
    }
    private Rectangle computeVisualizationBoundsOnScreen() {
        if (visualizationPanel == null || !visualizationPanel.isShowing()) {
            return null;
        }
        Point location = visualizationPanel.getLocationOnScreen();
        Dimension size = visualizationPanel.getSize();
        return new Rectangle(location.x, location.y, size.width, size.height);
    }
    
    private void showAreaSelectionDialog() {
@@ -555,8 +685,12 @@
            }
        }
        Device device = new Device();
        device.initFromProperties();
        Device device = Device.getGecaoji();
        if (device == null) {
            device = new Device();
            device.initFromProperties();
            Device.setGecaoji(device);
        }
        if (baseStationDialog == null) {
            baseStationDialog = new BaseStationDialog(dialogParent, THEME_COLOR, device, baseStation);
@@ -566,6 +700,142 @@
        baseStationDialog.setVisible(true);
    }
    private void checkIdentifiersAndPromptIfNeeded() {
        if (baseStation == null) {
            baseStation = new BaseStation();
        }
        baseStation.load();
        String currentMowerId = Setsys.getPropertyValue("mowerId");
        String currentBaseStationId = baseStation.getDeviceId();
        if (!isIdentifierMissing(currentMowerId) && !isIdentifierMissing(currentBaseStationId)) {
            return;
        }
        Window owner = SwingUtilities.getWindowAncestor(this);
        promptForMissingIdentifiers(owner, currentMowerId, currentBaseStationId);
    }
    private void promptForMissingIdentifiers(Window owner, String currentMowerId, String currentBaseStationId) {
        while (true) {
            JTextField mowerField = new JTextField(10);
            JTextField baseField = new JTextField(10);
            if (!isIdentifierMissing(currentMowerId)) {
                mowerField.setText(currentMowerId.trim());
            }
            if (!isIdentifierMissing(currentBaseStationId)) {
                baseField.setText(currentBaseStationId.trim());
            }
            JPanel panel = new JPanel();
            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
            panel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
            JLabel mowerLabel = new JLabel("割草机编号");
            JLabel baseLabel = new JLabel("差分基准站编号");
            mowerField.setMaximumSize(new Dimension(Integer.MAX_VALUE, mowerField.getPreferredSize().height));
            baseField.setMaximumSize(new Dimension(Integer.MAX_VALUE, baseField.getPreferredSize().height));
            panel.add(mowerLabel);
            panel.add(Box.createVerticalStrut(4));
            panel.add(mowerField);
            panel.add(Box.createVerticalStrut(10));
            panel.add(baseLabel);
            panel.add(Box.createVerticalStrut(4));
            panel.add(baseField);
            Object[] options = {"保存", "取消"};
            int result = JOptionPane.showOptionDialog(owner, panel, "完善设备信息",
                    JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[0]);
            if (result != 0) {
                break;
            }
            String mowerInput = mowerField.getText().trim();
            String baseInput = baseField.getText().trim();
            if (mowerInput.isEmpty()) {
                JOptionPane.showMessageDialog(owner, "割草机编号不能为空。", "提示", JOptionPane.WARNING_MESSAGE);
                continue;
            }
            if (baseInput.isEmpty()) {
                JOptionPane.showMessageDialog(owner, "差分基准站编号不能为空。", "提示", JOptionPane.WARNING_MESSAGE);
                continue;
            }
            boolean mowerSaved = persistMowerIdentifier(mowerInput);
            boolean baseSaved = persistBaseStationIdentifier(baseInput);
            if (mowerSaved && baseSaved) {
                JOptionPane.showMessageDialog(owner, "编号已保存。", "成功", JOptionPane.INFORMATION_MESSAGE);
                break;
            }
            StringBuilder errorBuilder = new StringBuilder();
            if (!mowerSaved) {
                errorBuilder.append("割草机编号保存失败。");
            }
            if (!baseSaved) {
                if (errorBuilder.length() > 0) {
                    errorBuilder.append('\n');
                }
                errorBuilder.append("差分基准站编号保存失败。");
            }
            JOptionPane.showMessageDialog(owner, errorBuilder.toString(), "保存失败", JOptionPane.ERROR_MESSAGE);
            currentMowerId = Setsys.getPropertyValue("mowerId");
            baseStation.load();
            currentBaseStationId = baseStation.getDeviceId();
        }
    }
    private boolean isIdentifierMissing(String value) {
        if (value == null) {
            return true;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() || "-1".equals(trimmed);
    }
    private boolean persistMowerIdentifier(String mowerId) {
        try {
            Setsys setsys = new Setsys();
            setsys.initializeFromProperties();
            boolean updated = setsys.updateProperty("mowerId", mowerId);
            if (updated) {
                Device.initializeActiveDevice(mowerId);
            }
            return updated;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
    private boolean persistBaseStationIdentifier(String baseStationId) {
        if (baseStation == null) {
            baseStation = new BaseStation();
        }
        try {
            baseStation.updateByDeviceId(baseStationId,
                    baseStation.getInstallationCoordinates(),
                    baseStation.getIotSimCardNumber(),
                    baseStation.getDeviceActivationTime(),
                    baseStation.getDataUpdateTime());
            baseStation.load();
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
    private boolean hasValidBaseStationId() {
        if (baseStation == null) {
            return false;
@@ -616,35 +886,327 @@
    }
    
    private void toggleStartPause() {
        if (handheldCaptureInlineUiActive) {
            handleHandheldConfirmAction();
            return;
        }
        if (drawingControlModeActive) {
            toggleDrawingPause();
            return;
        }
        if (startBtn == null) {
            return;
        }
        startButtonShowingPause = !startButtonShowingPause;
        if (startButtonShowingPause) {
            if (!zijian.ensureBeforeMowing(this, this::showRemoteControlDialog)) {
                return;
            }
        }
        startButtonShowingPause = !startButtonShowingPause;
        if (!startButtonShowingPause) {
            statusLabel.setText("作业中");
            if (stopButtonActive) {
                stopButtonActive = false;
                updateStopButtonIcon();
            }
            if (!beginMowingSession()) {
                startButtonShowingPause = true;
                statusLabel.setText("待机");
                updateStartButtonAppearance();
                return;
            }
        } else {
            statusLabel.setText("暂停中");
            pauseMowingSession();
        }
        updateStartButtonAppearance();
    }
    private void handleStopAction() {
        if (handheldCaptureInlineUiActive) {
            handleHandheldFinishAction();
            return;
        }
        if (drawingControlModeActive) {
            handleDrawingStopFromControlPanel();
            return;
        }
        stopButtonActive = !stopButtonActive;
        updateStopButtonIcon();
        if (stopButtonActive) {
            statusLabel.setText("已结束");
            startButtonShowingPause = false;
            stopMowingSession();
        } else {
            statusLabel.setText("待机");
            startButtonShowingPause = true;
            pauseMowingSession();
        }
        updateStartButtonAppearance();
    }
    private void handleDrawingStopFromControlPanel() {
        if (endDrawingCallback != null) {
            endDrawingCallback.run();
        } else {
            addzhangaiwu.finishDrawingSession();
        }
    }
    private void handleHandheldConfirmAction() {
        if (!handheldCaptureInlineUiActive) {
            return;
        }
        if (!canConfirmHandheldPoint()) {
            refreshHandheldCaptureUiState();
            return;
        }
        int count = captureHandheldBoundaryPoint();
        if (count <= 0) {
            refreshHandheldCaptureUiState();
            return;
        }
        refreshHandheldCaptureUiState();
    }
    private void handleHandheldFinishAction() {
        if (!handheldCaptureInlineUiActive) {
            return;
        }
        if (stopBtn != null && !stopBtn.isEnabled()) {
            refreshHandheldCaptureUiState();
            return;
        }
        if (!finishHandheldBoundaryCapture()) {
            refreshHandheldCaptureUiState();
        }
    }
    private void enterHandheldCaptureInlineUi() {
        if (handheldCaptureInlineUiActive) {
            refreshHandheldCaptureUiState();
            return;
        }
        handheldCaptureInlineUiActive = true;
        handheldCaptureStoredStatusText = statusLabel != null ? statusLabel.getText() : null;
        if (statusLabel != null) {
            statusLabel.setText("手持采集中");
        }
        if (startBtn != null) {
            handheldStartButtonOriginalBackground = startBtn.getBackground();
            handheldStartButtonOriginalForeground = startBtn.getForeground();
            startBtn.setIcon(null);
            startBtn.setIconTextGap(0);
            startBtn.setHorizontalAlignment(SwingConstants.CENTER);
            startBtn.setHorizontalTextPosition(SwingConstants.CENTER);
            startBtn.setVerticalTextPosition(SwingConstants.CENTER);
        }
        if (stopBtn != null) {
            handheldStopButtonOriginalBackground = stopBtn.getBackground();
            handheldStopButtonOriginalForeground = stopBtn.getForeground();
            stopBtn.setIcon(null);
            stopBtn.setIconTextGap(0);
            stopBtn.setHorizontalAlignment(SwingConstants.CENTER);
            stopBtn.setHorizontalTextPosition(SwingConstants.CENTER);
            stopBtn.setVerticalTextPosition(SwingConstants.CENTER);
            stopBtn.setText("结束");
        }
        startHandheldCaptureStatusTimer();
        refreshHandheldCaptureUiState();
    }
    private void exitHandheldCaptureInlineUi() {
        if (!handheldCaptureInlineUiActive) {
            return;
        }
        handheldCaptureInlineUiActive = false;
        stopHandheldCaptureStatusTimer();
        if (statusLabel != null) {
            statusLabel.setText(handheldCaptureStoredStatusText != null ? handheldCaptureStoredStatusText : "待机");
        }
        if (startBtn != null) {
            startBtn.setToolTipText(null);
            if (handheldStartButtonOriginalBackground != null) {
                startBtn.setBackground(handheldStartButtonOriginalBackground);
            }
            if (handheldStartButtonOriginalForeground != null) {
                startBtn.setForeground(handheldStartButtonOriginalForeground);
            }
            startBtn.setEnabled(true);
            updateStartButtonAppearance();
        }
        if (stopBtn != null) {
            stopBtn.setToolTipText(null);
            if (handheldStopButtonOriginalBackground != null) {
                stopBtn.setBackground(handheldStopButtonOriginalBackground);
            }
            if (handheldStopButtonOriginalForeground != null) {
                stopBtn.setForeground(handheldStopButtonOriginalForeground);
            }
            stopBtn.setEnabled(true);
            stopBtn.setText("结束");
            updateStopButtonIcon();
        }
        handheldCaptureStoredStatusText = null;
        handheldStartButtonOriginalBackground = null;
        handheldStartButtonOriginalForeground = null;
        handheldStopButtonOriginalBackground = null;
        handheldStopButtonOriginalForeground = null;
    }
    private void startHandheldCaptureStatusTimer() {
        if (handheldCaptureStatusTimer == null) {
            handheldCaptureStatusTimer = new Timer(400, e -> refreshHandheldCaptureUiState());
            handheldCaptureStatusTimer.setRepeats(true);
        }
        if (!handheldCaptureStatusTimer.isRunning()) {
            handheldCaptureStatusTimer.start();
        }
    }
    private void stopHandheldCaptureStatusTimer() {
        if (handheldCaptureStatusTimer != null && handheldCaptureStatusTimer.isRunning()) {
            handheldCaptureStatusTimer.stop();
        }
    }
    // Update inline handheld capture buttons based on the current device reading.
    private void refreshHandheldCaptureUiState() {
        if (!handheldCaptureInlineUiActive) {
            return;
        }
        int nextIndex = handheldCapturedPoints + 1;
        boolean hasFix = hasHighPrecisionFix();
        boolean hasValid = hasValidRealtimeHandheldPosition();
        boolean duplicate = hasValid && isCurrentHandheldPointDuplicate();
        boolean canConfirm = handheldCaptureActive && hasFix && hasValid && !duplicate;
        if (startBtn != null) {
            String prompt = "<html><center>采集点" + nextIndex + "<br>确定</center></html>";
            startBtn.setText(prompt);
            startBtn.setEnabled(canConfirm);
            if (canConfirm) {
                if (handheldStartButtonOriginalBackground != null) {
                    startBtn.setBackground(handheldStartButtonOriginalBackground);
                }
                if (handheldStartButtonOriginalForeground != null) {
                    startBtn.setForeground(handheldStartButtonOriginalForeground);
                }
                startBtn.setToolTipText(null);
            } else {
                startBtn.setBackground(new Color(200, 200, 200));
                startBtn.setForeground(new Color(130, 130, 130));
                startBtn.setToolTipText(resolveHandheldConfirmTooltip(hasFix, hasValid, duplicate));
            }
        }
        if (stopBtn != null) {
            boolean canFinish = handheldCapturedPoints >= 3;
            stopBtn.setText("结束");
            stopBtn.setEnabled(canFinish);
            if (canFinish) {
                if (handheldStopButtonOriginalBackground != null) {
                    stopBtn.setBackground(handheldStopButtonOriginalBackground);
                }
                if (handheldStopButtonOriginalForeground != null) {
                    stopBtn.setForeground(handheldStopButtonOriginalForeground);
                }
                stopBtn.setToolTipText("结束采集并返回新增地块");
            } else {
                stopBtn.setBackground(new Color(220, 220, 220));
                stopBtn.setForeground(new Color(130, 130, 130));
                stopBtn.setToolTipText("至少采集三个点才能结束");
            }
        }
    }
    private String resolveHandheldConfirmTooltip(boolean hasFix, boolean hasValidPosition, boolean duplicate) {
        if (!hasFix) {
            return "当前定位质量不足,无法采集";
        }
        if (!hasValidPosition) {
            return "当前定位数据无效,请稍后再试";
        }
        if (duplicate) {
            return "当前坐标已采集,请移动到新的位置";
        }
        return null;
    }
    private boolean hasHighPrecisionFix() {
        Device device = Device.getGecaoji();
        if (device == null) {
            return false;
        }
        String status = device.getPositioningStatus();
        return status != null && "4".equals(status.trim());
    }
    private boolean canConfirmHandheldPoint() {
        return handheldCaptureActive
            && hasHighPrecisionFix()
            && hasValidRealtimeHandheldPosition()
            && !isCurrentHandheldPointDuplicate();
    }
    private void enterDrawingControlMode() {
        if (drawingControlModeActive) {
            return;
        }
        storedStartButtonShowingPause = startButtonShowingPause;
        storedStopButtonActive = stopButtonActive;
        storedStatusBeforeDrawing = statusLabel != null ? statusLabel.getText() : null;
        drawingControlModeActive = true;
        applyDrawingPauseState(false, false);
        updateDrawingControlButtonLabels();
    }
    private void exitDrawingControlMode() {
        if (!drawingControlModeActive) {
            return;
        }
        drawingControlModeActive = false;
        applyDrawingPauseState(false, false);
        drawingPaused = false;
        stopButtonActive = storedStopButtonActive;
        startButtonShowingPause = storedStartButtonShowingPause;
        if (startBtn != null) {
            updateStartButtonAppearance();
        }
        if (stopBtn != null) {
            stopBtn.setText("结束");
            updateStopButtonIcon();
        }
        if (statusLabel != null) {
            statusLabel.setText(storedStatusBeforeDrawing != null ? storedStatusBeforeDrawing : "待机");
        }
        storedStatusBeforeDrawing = null;
    }
    private void updateDrawingControlButtonLabels() {
        if (!drawingControlModeActive) {
            return;
        }
        configureButtonForDrawingMode(startBtn);
        configureButtonForDrawingMode(stopBtn);
        if (startBtn != null) {
            startBtn.setText(drawingPaused ? "开始绘制" : "暂停绘制");
        }
        if (stopBtn != null) {
            stopBtn.setText("结束绘制");
        }
    }
    private void configureButtonForDrawingMode(JButton button) {
        if (button == null) {
            return;
        }
        button.setIcon(null);
        button.setIconTextGap(0);
        button.setHorizontalAlignment(SwingConstants.CENTER);
        button.setHorizontalTextPosition(SwingConstants.CENTER);
    }
    private void updateStartButtonAppearance() {
        if (startBtn == null) {
            return;
@@ -696,13 +1258,772 @@
        }
    }
    private JPanel createSpeedIndicatorPanel() {
        JPanel panel = new JPanel(new BorderLayout());
        panel.setOpaque(false);
        panel.setBorder(BorderFactory.createEmptyBorder(10, 20, 5, 20));
        JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
        rightPanel.setOpaque(false);
        fixQualityIndicator = new FixQualityIndicator();
        fixQualityIndicator.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        fixQualityIndicator.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (SwingUtilities.isLeftMouseButton(e) && mapRenderer != null) {
                    mapRenderer.showMowerInfo();
                }
            }
        });
        mowingProgressLabel = new JLabel("--%");
        mowingProgressLabel.setFont(new Font("微软雅黑", Font.BOLD, 12));
        mowingProgressLabel.setForeground(THEME_COLOR);
        mowerSpeedValueLabel = new JLabel("--");
        mowerSpeedValueLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
        mowerSpeedValueLabel.setForeground(THEME_COLOR);
        mowerSpeedUnitLabel = new JLabel("km/h");
        mowerSpeedUnitLabel.setFont(new Font("微软雅黑", Font.BOLD, 9));
        mowerSpeedUnitLabel.setForeground(THEME_COLOR);
        dataPacketCountLabel = new JLabel("--");
        dataPacketCountLabel.setFont(new Font("微软雅黑", Font.BOLD, 12));
        dataPacketCountLabel.setForeground(THEME_COLOR);
        rightPanel.add(fixQualityIndicator);
        JSeparator areaSeparator = new JSeparator(SwingConstants.VERTICAL);
        areaSeparator.setPreferredSize(new Dimension(1, 16));
        rightPanel.add(areaSeparator);
        rightPanel.add(mowingProgressLabel);
    JSeparator speedSeparator = new JSeparator(SwingConstants.VERTICAL);
    speedSeparator.setPreferredSize(new Dimension(1, 16));
    rightPanel.add(speedSeparator);
        rightPanel.add(mowerSpeedValueLabel);
        rightPanel.add(mowerSpeedUnitLabel);
        JSeparator separator = new JSeparator(SwingConstants.VERTICAL);
        separator.setPreferredSize(new Dimension(1, 16));
        rightPanel.add(separator);
        rightPanel.add(dataPacketCountLabel);
        panel.add(rightPanel, BorderLayout.EAST);
        updateFixQualityIndicator();
        updateDataPacketCountLabel();
        return panel;
    }
    private void startMowerSpeedUpdates() {
        if (mowerSpeedRefreshTimer == null) {
            mowerSpeedRefreshTimer = new Timer(1000, e -> refreshMowerSpeedLabel());
            mowerSpeedRefreshTimer.setRepeats(true);
        }
        if (!mowerSpeedRefreshTimer.isRunning()) {
            mowerSpeedRefreshTimer.start();
        }
        refreshMowerSpeedLabel();
    }
    private void refreshMowerSpeedLabel() {
        if (mowerSpeedValueLabel == null) {
            return;
        }
        String display = "--";
        Device device = Device.getGecaoji();
        if (device != null) {
            String sanitized = sanitizeSpeedValue(device.getRealtimeSpeed());
            if (sanitized != null) {
                display = sanitized;
            }
        }
        mowerSpeedValueLabel.setText(display);
        if (mowerSpeedUnitLabel != null) {
            mowerSpeedUnitLabel.setText("km/h");
        }
        updateMowingProgressLabel();
        updateFixQualityIndicator();
        updateDataPacketCountLabel();
    }
    private void updateDataPacketCountLabel() {
        if (dataPacketCountLabel == null) {
            return;
        }
        int udpCount = UDPServer.getReceivedPacketCount();
        int serialCount = dellmessage.getProcessedLineCount();
        int displayCount = Math.max(udpCount, serialCount);
        if (displayCount <= 0) {
            dataPacketCountLabel.setText("--");
            dataPacketCountLabel.setToolTipText(null);
        } else {
            dataPacketCountLabel.setText(String.valueOf(displayCount));
            dataPacketCountLabel.setToolTipText(String.format("串口: %d  UDP: %d", serialCount, udpCount));
        }
    }
    private void updateFixQualityIndicator() {
        if (fixQualityIndicator == null) {
            return;
        }
        Device device = Device.getGecaoji();
        String code = null;
        if (device != null) {
            code = sanitizeDeviceValue(device.getPositioningStatus());
        }
        fixQualityIndicator.setQuality(code);
    }
    private Color resolveFixQualityColor(String code) {
        if (code == null) {
            return new Color(160, 160, 160);
        }
        switch (code) {
            case "0":
                return new Color(160, 160, 160);
            case "1":
                return new Color(52, 152, 219);
            case "2":
                return new Color(26, 188, 156);
            case "3":
                return new Color(155, 89, 182);
            case "4":
                return THEME_COLOR;
            case "5":
                return new Color(241, 196, 15);
            case "6":
                return new Color(231, 76, 60);
            case "7":
                return new Color(230, 126, 34);
            default:
                return new Color(95, 95, 95);
        }
    }
    private String resolveFixQualityDescription(String code) {
        if (code == null) {
            return "未知";
        }
        switch (code) {
            case "0":
                return "未定位";
            case "1":
                return "单点定位";
            case "2":
                return "码差分";
            case "3":
                return "无效PPS";
            case "4":
                return "固定解";
            case "5":
                return "浮点解";
            case "6":
                return "正在估算";
            case "7":
                return "人工输入固定值";
            default:
                return "其他";
        }
    }
    private String sanitizeSpeedValue(String raw) {
        if (raw == null) {
            return null;
        }
        String trimmed = raw.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return null;
        }
        if (trimmed.toLowerCase().endsWith("km/h")) {
            trimmed = trimmed.substring(0, trimmed.length() - 4).trim();
        }
        return trimmed;
    }
    private String sanitizeDeviceValue(String raw) {
        if (raw == null) {
            return null;
        }
        String trimmed = raw.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed) || "null".equalsIgnoreCase(trimmed)) {
            return null;
        }
        return trimmed;
    }
    private void updateMowingProgressLabel() {
        if (mowingProgressLabel == null) {
            return;
        }
        if (mapRenderer == null) {
            mowingProgressLabel.setText("--%");
            mowingProgressLabel.setToolTipText(null);
            return;
        }
        double totalArea = mapRenderer.getTotalLandAreaSqMeters();
        double completedArea = mapRenderer.getCompletedMowingAreaSqMeters();
        double ratio = mapRenderer.getMowingCompletionRatio();
        if (totalArea <= 0) {
            mowingProgressLabel.setText("--%");
            mowingProgressLabel.setToolTipText("暂无地块面积数据");
            return;
        }
        double percent = Math.max(0.0, Math.min(1.0, ratio)) * 100.0;
        mowingProgressLabel.setText(String.format(Locale.US, "%.1f%%", percent));
        mowingProgressLabel.setToolTipText(String.format(Locale.US, "%.1f㎡ / %.1f㎡", completedArea, totalArea));
    }
    public void refreshMowingIndicators() {
        refreshMowerSpeedLabel();
    }
    public void setHandheldMowerIconActive(boolean active) {
        if (mapRenderer == null) {
            return;
        }
        mapRenderer.setHandheldMowerIconActive(active);
    }
    public boolean startMowerBoundaryCapture() {
        if (mapRenderer == null) {
            return false;
        }
        double[] baseLatLonCandidate = resolveCircleBaseLatLon();
        if (baseLatLonCandidate == null) {
            return false;
        }
        mapRenderer.clearIdleTrail();
        activeBoundaryMode = BoundaryCaptureMode.MOWER;
        mowerBoundaryCaptureActive = true;
        mowerBaseLatLon = baseLatLonCandidate;
        lastMowerCoordinate = null;
        synchronized (Coordinate.coordinates) {
            Coordinate.coordinates.clear();
        }
        synchronized (mowerTemporaryPoints) {
            mowerTemporaryPoints.clear();
        }
        AddDikuai.recordTemporaryBoundaryPoints(Collections.emptyList());
        Coordinate.setStartSaveGngga(true);
        if (mapRenderer != null) {
            mapRenderer.setBoundaryPreviewMarkerScale(2.0d);
            mapRenderer.beginHandheldBoundaryPreview();
        }
        setHandheldMowerIconActive(false);
        startMowerBoundaryMonitor();
        return true;
    }
    public boolean startHandheldBoundaryCapture() {
        if (mapRenderer == null) {
            return false;
        }
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            stopMowerBoundaryCapture();
        }
        mapRenderer.clearIdleTrail();
        activeBoundaryMode = BoundaryCaptureMode.HANDHELD;
        handheldCaptureActive = true;
        handheldCapturedPoints = 0;
        Coordinate.setStartSaveGngga(false);
        synchronized (Coordinate.coordinates) {
            Coordinate.coordinates.clear();
        }
        synchronized (handheldTemporaryPoints) {
            handheldTemporaryPoints.clear();
        }
        AddDikuai.recordTemporaryBoundaryPoints(Collections.emptyList());
    mapRenderer.setBoundaryPreviewMarkerScale(1.0d);
    mapRenderer.beginHandheldBoundaryPreview();
        setHandheldMowerIconActive(true);
        enterHandheldCaptureInlineUi();
        return true;
    }
    private void startMowerBoundaryMonitor() {
        if (mowerBoundaryMonitor == null) {
            mowerBoundaryMonitor = new Timer(600, e -> pollMowerBoundaryCoordinate());
            mowerBoundaryMonitor.setRepeats(true);
        }
        if (!mowerBoundaryMonitor.isRunning()) {
            mowerBoundaryMonitor.start();
        }
        pollMowerBoundaryCoordinate();
    }
    private void stopMowerBoundaryMonitor() {
        if (mowerBoundaryMonitor != null && mowerBoundaryMonitor.isRunning()) {
            mowerBoundaryMonitor.stop();
        }
    }
    private void pollMowerBoundaryCoordinate() {
        if (!mowerBoundaryCaptureActive) {
            return;
        }
        Coordinate latest = getLatestCoordinate();
        if (latest == null || latest == lastMowerCoordinate) {
            return;
        }
        double[] base = mowerBaseLatLon;
        if (base == null || base.length < 2) {
            discardLatestCoordinate(latest);
            lastMowerCoordinate = latest;
            return;
        }
        double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection());
        double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection());
        if (!Double.isFinite(lat) || !Double.isFinite(lon)) {
            discardLatestCoordinate(latest);
            lastMowerCoordinate = latest;
            return;
        }
        double[] local = convertLatLonToLocal(lat, lon, base[0], base[1]);
        Point2D.Double candidate = new Point2D.Double(local[0], local[1]);
        if (!Double.isFinite(candidate.x) || !Double.isFinite(candidate.y)) {
            discardLatestCoordinate(latest);
            lastMowerCoordinate = latest;
            return;
        }
        List<Point2D.Double> snapshot;
        synchronized (mowerTemporaryPoints) {
            for (Point2D.Double existing : mowerTemporaryPoints) {
                if (existing != null && arePointsClose(existing, candidate)) {
                    discardLatestCoordinate(latest);
                    lastMowerCoordinate = latest;
                    return;
                }
            }
            mowerTemporaryPoints.add(candidate);
            snapshot = new ArrayList<>(mowerTemporaryPoints.size() + 1);
            for (Point2D.Double point : mowerTemporaryPoints) {
                if (point != null) {
                    snapshot.add(new Point2D.Double(point.x, point.y));
                }
            }
        }
        ensureClosed(snapshot);
        AddDikuai.recordTemporaryBoundaryPoints(snapshot);
        if (mapRenderer != null) {
            mapRenderer.addHandheldBoundaryPoint(candidate.x, candidate.y);
        }
        lastMowerCoordinate = latest;
    }
    private void stopMowerBoundaryCapture() {
        stopMowerBoundaryMonitor();
        mowerBoundaryCaptureActive = false;
        lastMowerCoordinate = null;
        mowerBaseLatLon = null;
        if (mapRenderer != null) {
            mapRenderer.clearHandheldBoundaryPreview();
        }
        Coordinate.setStartSaveGngga(false);
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            activeBoundaryMode = BoundaryCaptureMode.NONE;
        }
        setHandheldMowerIconActive(false);
    }
    private void discardLatestCoordinate(Coordinate coordinate) {
        if (coordinate == null) {
            return;
        }
        synchronized (Coordinate.coordinates) {
            int size = Coordinate.coordinates.size();
            if (size == 0) {
                return;
            }
            int lastIndex = size - 1;
            if (Coordinate.coordinates.get(lastIndex) == coordinate) {
                Coordinate.coordinates.remove(lastIndex);
            } else {
                Coordinate.coordinates.remove(coordinate);
            }
        }
    }
    private void ensureClosed(List<Point2D.Double> points) {
        if (points == null || points.size() < 3) {
            return;
        }
        Point2D.Double first = points.get(0);
        Point2D.Double last = points.get(points.size() - 1);
        if (first == null || last == null) {
            return;
        }
        if (!arePointsClose(first, last)) {
            points.add(new Point2D.Double(first.x, first.y));
        }
    }
    int captureHandheldBoundaryPoint() {
        if (!handheldCaptureActive) {
            return -1;
        }
        Device device = Device.getGecaoji();
        if (device == null) {
            JOptionPane.showMessageDialog(this, "未检测到采集设备,请检查连接。", "提示", JOptionPane.WARNING_MESSAGE);
            return -1;
        }
        String[] latParts = splitCoordinateComponents(device.getRealtimeLatitude(), true);
        String[] lonParts = splitCoordinateComponents(device.getRealtimeLongitude(), false);
        if (latParts == null || lonParts == null) {
            JOptionPane.showMessageDialog(this, "当前定位无效,请在定位稳定后再试。", "提示", JOptionPane.WARNING_MESSAGE);
            return -1;
        }
        double x = parseMetersValue(device.getRealtimeX());
        double y = parseMetersValue(device.getRealtimeY());
        if (!Double.isFinite(x) || !Double.isFinite(y)) {
            JOptionPane.showMessageDialog(this, "当前定位数据无效,请稍后再试。", "提示", JOptionPane.WARNING_MESSAGE);
            return -1;
        }
        if (isDuplicateHandheldPoint(x, y)) {
            JOptionPane.showMessageDialog(this, "当前坐标已采集,请移动到新的位置后再试。", "提示", JOptionPane.WARNING_MESSAGE);
            return -1;
        }
        double altitude = parseAltitudeValue(device.getRealtimeAltitude());
        Coordinate coordinate = new Coordinate(latParts[0], latParts[1], lonParts[0], lonParts[1], altitude);
        synchronized (Coordinate.coordinates) {
            Coordinate.coordinates.add(coordinate);
        }
        if (mapRenderer != null) {
            mapRenderer.addHandheldBoundaryPoint(x, y);
        }
        List<Point2D.Double> snapshot;
        synchronized (handheldTemporaryPoints) {
            handheldTemporaryPoints.add(new Point2D.Double(x, y));
            snapshot = new ArrayList<>(handheldTemporaryPoints);
        }
    AddDikuai.recordTemporaryBoundaryPoints(snapshot);
        handheldCapturedPoints++;
        return handheldCapturedPoints;
    }
    boolean finishHandheldBoundaryCapture() {
        if (!handheldCaptureActive) {
            return false;
        }
        if (handheldCapturedPoints < 3) {
            JOptionPane.showMessageDialog(this, "至少采集三个点才能生成边界。", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
        }
        List<Point2D.Double> closedSnapshot = createClosedHandheldPointSnapshot();
        handheldCaptureActive = false;
        activeBoundaryMode = BoundaryCaptureMode.NONE;
        Coordinate.setStartSaveGngga(false);
        if (mapRenderer != null) {
            mapRenderer.clearHandheldBoundaryPreview();
        }
    AddDikuai.recordTemporaryBoundaryPoints(closedSnapshot);
        exitHandheldCaptureInlineUi();
        SwingUtilities.invokeLater(AddDikuai::finishDrawingSession);
        return true;
    }
    int getHandheldCapturedPointCount() {
        return handheldCapturedPoints;
    }
    public List<Point2D.Double> getHandheldTemporaryPointsSnapshot() {
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            return createClosedMowerPointSnapshot();
        }
        if (!handheldCaptureActive) {
            return createClosedHandheldPointSnapshot();
        }
        synchronized (handheldTemporaryPoints) {
            return new ArrayList<>(handheldTemporaryPoints);
        }
    }
    public boolean isCurrentHandheldPointDuplicate() {
        Device device = Device.getGecaoji();
        if (device == null) {
            return false;
        }
        double x = parseMetersValue(device.getRealtimeX());
        double y = parseMetersValue(device.getRealtimeY());
        if (!Double.isFinite(x) || !Double.isFinite(y)) {
            return false;
        }
        return isDuplicateHandheldPoint(x, y);
    }
    public boolean hasValidRealtimeHandheldPosition() {
        Device device = Device.getGecaoji();
        if (device == null) {
            return false;
        }
        double x = parseMetersValue(device.getRealtimeX());
        double y = parseMetersValue(device.getRealtimeY());
        return Double.isFinite(x) && Double.isFinite(y);
    }
    private boolean isDuplicateHandheldPoint(double x, double y) {
        Point2D.Double candidate = new Point2D.Double(x, y);
        synchronized (handheldTemporaryPoints) {
            for (Point2D.Double existing : handheldTemporaryPoints) {
                if (existing == null) {
                    continue;
                }
                if (arePointsClose(existing, candidate)) {
                    return true;
                }
            }
        }
        return false;
    }
    private boolean arePointsClose(Point2D.Double a, Point2D.Double b) {
        if (a == null || b == null) {
            return false;
        }
        double dx = a.x - b.x;
        double dy = a.y - b.y;
        return Math.hypot(dx, dy) < HANDHELD_DUPLICATE_THRESHOLD_METERS;
    }
    private List<Point2D.Double> createClosedHandheldPointSnapshot() {
        List<Point2D.Double> copy = new ArrayList<>();
        synchronized (handheldTemporaryPoints) {
            for (Point2D.Double point : handheldTemporaryPoints) {
                if (point != null) {
                    copy.add(new Point2D.Double(point.x, point.y));
                }
            }
        }
        ensureClosed(copy);
        return copy;
    }
    private List<Point2D.Double> createClosedMowerPointSnapshot() {
        List<Point2D.Double> copy = new ArrayList<>();
        synchronized (mowerTemporaryPoints) {
            for (Point2D.Double point : mowerTemporaryPoints) {
                if (point != null) {
                    copy.add(new Point2D.Double(point.x, point.y));
                }
            }
        }
        ensureClosed(copy);
        return copy;
    }
    private String[] splitCoordinateComponents(String combined, boolean latitude) {
        if (combined == null) {
            return null;
        }
        String trimmed = combined.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return null;
        }
        String valuePart;
        String directionPart = null;
        String[] parts = trimmed.split(",");
        if (parts.length >= 2) {
            valuePart = parts[0].trim();
            directionPart = parts[1].trim();
        } else {
            valuePart = trimmed;
        }
        if (valuePart.isEmpty()) {
            return null;
        }
        if (directionPart == null || directionPart.isEmpty()) {
            char lastChar = valuePart.charAt(valuePart.length() - 1);
            if (Character.isLetter(lastChar)) {
                directionPart = String.valueOf(lastChar);
                valuePart = valuePart.substring(0, valuePart.length() - 1).trim();
            }
        }
        if (valuePart.isEmpty()) {
            return null;
        }
        directionPart = normalizeHemisphere(directionPart, latitude);
        return new String[]{valuePart, directionPart};
    }
    private String normalizeHemisphere(String direction, boolean latitude) {
        if (direction == null || direction.trim().isEmpty()) {
            return latitude ? "N" : "E";
        }
        String normalized = direction.trim().toUpperCase(Locale.ROOT);
        if (latitude) {
            if (!"N".equals(normalized) && !"S".equals(normalized)) {
                return "N";
            }
        } else {
            if (!"E".equals(normalized) && !"W".equals(normalized)) {
                return "E";
            }
        }
        return normalized;
    }
    private double parseMetersValue(String raw) {
        if (raw == null) {
            return Double.NaN;
        }
        String trimmed = raw.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return Double.NaN;
        }
        try {
            return Double.parseDouble(trimmed);
        } catch (NumberFormatException ex) {
            return Double.NaN;
        }
    }
    private double parseAltitudeValue(String raw) {
        if (raw == null) {
            return 0.0;
        }
        String trimmed = raw.trim();
        if (trimmed.isEmpty() || "-1".equals(trimmed)) {
            return 0.0;
        }
        try {
            return Double.parseDouble(trimmed);
        } catch (NumberFormatException ex) {
            return 0.0;
        }
    }
    private boolean beginMowingSession() {
        if (mapRenderer == null) {
            return false;
        }
        String landNumber = Dikuaiguanli.getCurrentWorkLandNumber();
        if (!isMeaningfulValue(landNumber)) {
            JOptionPane.showMessageDialog(this, "请先选择地块后再开始作业", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
        }
        double widthMeters = resolveMowerWidthMeters(landNumber);
        if (widthMeters <= 0) {
            JOptionPane.showMessageDialog(this, "未配置割草宽度,将无法计算作业面积", "提示", JOptionPane.WARNING_MESSAGE);
        }
        mapRenderer.startRealtimeTrackRecording(landNumber, widthMeters);
        refreshMowerSpeedLabel();
        return true;
    }
    private void pauseMowingSession() {
        if (mapRenderer == null) {
            return;
        }
        mapRenderer.pauseRealtimeTrackRecording();
        refreshMowerSpeedLabel();
    }
    private void stopMowingSession() {
        if (mapRenderer == null) {
            return;
        }
        mapRenderer.stopRealtimeTrackRecording();
        refreshMowerSpeedLabel();
    }
    private double resolveMowerWidthMeters(String landNumber) {
        double width = 0.0;
        if (isMeaningfulValue(landNumber)) {
            Dikuai current = Dikuai.getDikuai(landNumber);
            if (current != null) {
                width = parseMowerWidthMeters(current.getMowingWidth());
            }
        }
        if (width > 0) {
            return width;
        }
        return parseMowerWidthFromDevice();
    }
    private double parseMowerWidthFromDevice() {
        Device device = Device.getGecaoji();
        if (device == null) {
            return 0.0;
        }
        return parseMowerWidthMeters(device.getMowingWidth());
    }
    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 void applyStatusLabelColor(String statusText) {
        if (statusLabel == null) {
            return;
        }
        if ("作业中".equals(statusText)) {
        if ("作业中".equals(statusText) || "绘制中".equals(statusText)) {
            statusLabel.setForeground(THEME_COLOR);
        } else if ("暂停中".equals(statusText)) {
        } else if ("暂停中".equals(statusText) || "绘制暂停".equals(statusText)) {
            statusLabel.setForeground(STATUS_PAUSE_COLOR);
        } else {
            statusLabel.setForeground(Color.GRAY);
@@ -730,6 +2051,29 @@
        return button;
    }
    private JButton createFloatingTextButton(String text) {
        JButton button = new JButton(text);
        button.setFont(new Font("微软雅黑", Font.BOLD, 15));
        button.setForeground(Color.WHITE);
        button.setBackground(THEME_COLOR);
        button.setBorder(BorderFactory.createEmptyBorder(10, 18, 10, 18));
        button.setFocusPainted(false);
        button.setOpaque(true);
        button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                button.setBackground(THEME_HOVER_COLOR);
            }
            @Override
            public void mouseExited(MouseEvent e) {
                button.setBackground(THEME_COLOR);
            }
        });
        return button;
    }
    private ImageIcon loadScaledIcon(String path, int width, int height) {
        try {
            ImageIcon icon = new ImageIcon(path);
@@ -787,6 +2131,12 @@
        if (notifyCoordinate) {
            Coordinate.setStartSaveGngga(!paused);
        }
        if (drawingControlModeActive) {
            updateDrawingControlButtonLabels();
            if (statusLabel != null) {
                statusLabel.setText(paused ? "绘制暂停" : "绘制中");
            }
        }
    }
    private void toggleDrawingPause() {
@@ -799,36 +2149,33 @@
    public void showEndDrawingButton(Runnable callback, String drawingShape) {
        endDrawingCallback = callback;
        applyDrawingPauseState(false, false);
        circleDialogMode = false;
        hideCircleGuidancePanel();
        ensureFloatingIconsLoaded();
        ensureFloatingButtonInfrastructure();
        enterDrawingControlMode();
        boolean enableCircleGuidance = drawingShape != null
                && "circle".equalsIgnoreCase(drawingShape.trim());
        if (enableCircleGuidance) {
            prepareCircleGuidanceState();
            showCircleGuidanceStep(1);
            endDrawingButton.setVisible(false);
            ensureFloatingIconsLoaded();
            ensureFloatingButtonInfrastructure();
            if (drawingPauseButton != null) {
                drawingPauseButton.setVisible(false);
            }
            if (endDrawingButton != null) {
                endDrawingButton.setVisible(false);
            }
            prepareCircleGuidanceState();
            showCircleGuidanceStep(1);
            floatingButtonPanel.setVisible(true);
            if (floatingButtonPanel.getParent() != visualizationPanel) {
                visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH);
            }
            rebuildFloatingButtonColumn();
        } else {
            clearCircleGuidanceArtifacts();
            endDrawingButton.setVisible(true);
            if (drawingPauseButton != null) {
                drawingPauseButton.setVisible(true);
            }
            hideFloatingDrawingControls();
        }
        floatingButtonPanel.setVisible(true);
        if (floatingButtonPanel.getParent() != visualizationPanel) {
            visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH);
        }
        rebuildFloatingButtonColumn();
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
    }
@@ -905,6 +2252,14 @@
                floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10)));
            }
            floatingButtonColumn.add(endDrawingButton);
            added = true;
        }
        if (pathPreviewReturnButton != null && pathPreviewReturnButton.isVisible()) {
            if (added) {
                floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10)));
            }
            floatingButtonColumn.add(pathPreviewReturnButton);
            added = true;
        }
        floatingButtonColumn.revalidate();
        floatingButtonColumn.repaint();
@@ -916,6 +2271,7 @@
            return;
        }
        circleGuidanceStep = step;
        if (step == 1) {
            circleGuidanceLabel.setText("采集第1个点");
            circleGuidancePrimaryButton.setText("确认第1点");
@@ -1312,21 +2668,27 @@
    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 (baseStation == null) {
            baseStation = new BaseStation();
        }
        baseStation.load();
        coords = baseStation.getInstallationCoordinates();
        if (!isMeaningfulValue(coords)) {
            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;
            if (!isMeaningfulValue(coords)) {
                coords = addzhangaiwu.getActiveSessionBaseStation();
            }
            if (!isMeaningfulValue(coords)) {
                return null;
            }
        }
        String[] parts = coords.split(",");
        if (parts.length < 4) {
@@ -1458,10 +2820,140 @@
        clearCircleGuidanceArtifacts();
        hideFloatingDrawingControls();
        circleDialogMode = false;
        applyDrawingPauseState(false, false);
        exitHandheldCaptureInlineUi();
        handheldCaptureActive = false;
        exitDrawingControlMode();
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            stopMowerBoundaryCapture();
        } else if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD && !handheldCaptureActive) {
            activeBoundaryMode = BoundaryCaptureMode.NONE;
        }
        endDrawingCallback = null;
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
        setHandheldMowerIconActive(false);
    }
    private void showPathPreviewReturnControls() {
        ensureFloatingButtonInfrastructure();
        if (drawingPauseButton != null) {
            drawingPauseButton.setVisible(false);
        }
        if (endDrawingButton != null) {
            endDrawingButton.setVisible(false);
        }
        if (pathPreviewReturnButton == null) {
            pathPreviewReturnButton = createFloatingTextButton("返回");
            pathPreviewReturnButton.setToolTipText("返回新增地块步骤");
            pathPreviewReturnButton.addActionListener(e -> handlePathPreviewReturn());
        }
        pathPreviewReturnButton.setVisible(true);
        if (floatingButtonPanel != null) {
            floatingButtonPanel.setVisible(true);
            if (floatingButtonPanel.getParent() != visualizationPanel) {
                visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH);
            }
        }
        rebuildFloatingButtonColumn();
    }
    private void hidePathPreviewReturnControls() {
        if (pathPreviewReturnButton != null) {
            pathPreviewReturnButton.setVisible(false);
        }
        rebuildFloatingButtonColumn();
        if (floatingButtonPanel != null && floatingButtonColumn != null
                && floatingButtonColumn.getComponentCount() == 0) {
            floatingButtonPanel.setVisible(false);
        }
    }
    private void handlePathPreviewReturn() {
        Runnable callback = pathPreviewReturnAction;
        exitMowingPathPreview();
        if (callback != null) {
            callback.run();
        }
    }
    public boolean startMowingPathPreview(String landNumber,
                                          String landName,
                                          String boundary,
                                          String obstacles,
                                          String plannedPath,
                                          Runnable returnAction) {
        if (mapRenderer == null || !isMeaningfulValue(plannedPath)) {
            return false;
        }
        if (pathPreviewActive) {
            exitMowingPathPreview();
        }
        exitDrawingControlMode();
        hideCircleGuidancePanel();
        clearCircleGuidanceArtifacts();
        pathPreviewReturnAction = returnAction;
        pathPreviewActive = true;
    mapRenderer.setPathPreviewSizingEnabled(true);
        previewRestoreLandNumber = Dikuaiguanli.getCurrentWorkLandNumber();
        previewRestoreLandName = null;
        if (isMeaningfulValue(previewRestoreLandNumber)) {
            Dikuai existing = Dikuai.getDikuai(previewRestoreLandNumber);
            if (existing != null) {
                previewRestoreLandName = existing.getLandName();
            }
        }
        mapRenderer.setCurrentBoundary(boundary, landNumber, landName);
        mapRenderer.setCurrentObstacles(obstacles, landNumber);
        mapRenderer.setCurrentPlannedPath(plannedPath);
        mapRenderer.clearHandheldBoundaryPreview();
    mapRenderer.setBoundaryPointSizeScale(1.0d);
        mapRenderer.setBoundaryPointsVisible(isMeaningfulValue(boundary));
        String displayName = isMeaningfulValue(landName) ? landName : landNumber;
        updateCurrentAreaName(displayName);
        showPathPreviewReturnControls();
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
        return true;
    }
    public void exitMowingPathPreview() {
        if (!pathPreviewActive) {
            return;
        }
        pathPreviewActive = false;
        if (mapRenderer != null) {
            mapRenderer.setPathPreviewSizingEnabled(false);
        }
        hidePathPreviewReturnControls();
        String restoreNumber = previewRestoreLandNumber;
        String restoreName = previewRestoreLandName;
        previewRestoreLandNumber = null;
        previewRestoreLandName = null;
        pathPreviewReturnAction = null;
        if (restoreNumber != null) {
            Dikuaiguanli.setCurrentWorkLand(restoreNumber, restoreName);
        } else if (mapRenderer != null) {
            mapRenderer.setCurrentBoundary(null, null, null);
            mapRenderer.setCurrentObstacles((String) null, null);
            mapRenderer.setCurrentPlannedPath(null);
            mapRenderer.setBoundaryPointsVisible(false);
            mapRenderer.setBoundaryPointSizeScale(1.0d);
            mapRenderer.clearHandheldBoundaryPreview();
            mapRenderer.resetView();
            updateCurrentAreaName(null);
        }
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
    }
    
    /**
@@ -1493,6 +2985,16 @@
    private void initializeDefaultAreaSelection() {
        Dikuai.initFromProperties();
        String persistedLandNumber = Dikuaiguanli.getPersistedWorkLandNumber();
        if (persistedLandNumber != null) {
            Dikuai stored = Dikuai.getDikuai(persistedLandNumber);
            if (stored != null) {
                Dikuaiguanli.setCurrentWorkLand(persistedLandNumber, stored.getLandName());
                return;
            }
            Dikuaiguanli.setCurrentWorkLand(null, null);
        }
        Map<String, Dikuai> all = Dikuai.getAllDikuai();
        if (all.isEmpty()) {
            Dikuaiguanli.setCurrentWorkLand(null, null);
@@ -1557,6 +3059,48 @@
        return !"未选择地块".equals(trimmed);
    }
    private final class FixQualityIndicator extends JComponent {
        private static final long serialVersionUID = 1L;
        private static final int DIAMETER = 16;
        private String currentCode;
        private Color currentColor = new Color(160, 160, 160);
        private FixQualityIndicator() {
            setPreferredSize(new Dimension(DIAMETER, DIAMETER));
            setMinimumSize(new Dimension(DIAMETER, DIAMETER));
            setMaximumSize(new Dimension(DIAMETER, DIAMETER));
            setToolTipText("未知");
        }
        private void setQuality(String code) {
            if (Objects.equals(currentCode, code)) {
                return;
            }
            currentCode = code;
            currentColor = resolveFixQualityColor(code);
            setToolTipText(resolveFixQualityDescription(code));
            repaint();
        }
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g.create();
            try {
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                int diameter = Math.min(getWidth(), getHeight()) - 2;
                int x = (getWidth() - diameter) / 2;
                int y = (getHeight() - diameter) / 2;
                g2.setColor(currentColor);
                g2.fillOval(x, y, diameter, diameter);
                g2.setColor(new Color(255, 255, 255, 128));
                g2.drawOval(x, y, diameter, diameter);
            } finally {
                g2.dispose();
            }
        }
    }
    // 测试方法
    public static void main(String[] args) {
        JFrame frame = new JFrame("AutoMow - 首页");