张世豪
2025-12-09 32524195d474b74e48916867b2a6c2f022a40d98
src/zhuye/Shouye.java
@@ -92,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;
@@ -112,7 +117,6 @@
    private double[] circleBaseLatLon;
    private Timer circleDataMonitor;
    private Coordinate lastCapturedCoordinate;
    private HandheldBoundaryCaptureDialog handheldCaptureDialog;
    private boolean handheldCaptureActive;
    private int handheldCapturedPoints;
    private final List<Point2D.Double> handheldTemporaryPoints = new ArrayList<>();
@@ -127,14 +131,26 @@
    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);
        dellmessage.registerLineListener(serialLineListener);
        initializeUI();
        setupEventHandlers();
        scheduleIdentifierCheck();
    }
    public static Shouye getInstance() {
@@ -174,6 +190,26 @@
        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;
@@ -584,8 +620,9 @@
        }
        if (remoteDialog != null) {
            positionRemoteDialogBottomCenter(remoteDialog);
            zijian.markSelfCheckCompleted();
            remoteDialog.setVisible(true);
        }
        remoteDialog.setVisible(true);
    }
    private void positionRemoteDialogBottomCenter(RemoteControlDialog dialog) {
@@ -663,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;
@@ -713,9 +886,22 @@
    }
    
    private void toggleStartPause() {
        if (handheldCaptureInlineUiActive) {
            handleHandheldConfirmAction();
            return;
        }
        if (drawingControlModeActive) {
            toggleDrawingPause();
            return;
        }
        if (startBtn == null) {
            return;
        }
        if (startButtonShowingPause) {
            if (!zijian.ensureBeforeMowing(this, this::showRemoteControlDialog)) {
                return;
            }
        }
        startButtonShowingPause = !startButtonShowingPause;
        if (!startButtonShowingPause) {
            statusLabel.setText("作业中");
@@ -737,6 +923,14 @@
    }
    private void handleStopAction() {
        if (handheldCaptureInlineUiActive) {
            handleHandheldFinishAction();
            return;
        }
        if (drawingControlModeActive) {
            handleDrawingStopFromControlPanel();
            return;
        }
        stopButtonActive = !stopButtonActive;
        updateStopButtonIcon();
        if (stopButtonActive) {
@@ -751,6 +945,268 @@
        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;
@@ -1028,6 +1484,13 @@
        refreshMowerSpeedLabel();
    }
    public void setHandheldMowerIconActive(boolean active) {
        if (mapRenderer == null) {
            return;
        }
        mapRenderer.setHandheldMowerIconActive(active);
    }
    public boolean startMowerBoundaryCapture() {
        if (mapRenderer == null) {
            return false;
@@ -1037,6 +1500,8 @@
            return false;
        }
        mapRenderer.clearIdleTrail();
        activeBoundaryMode = BoundaryCaptureMode.MOWER;
        mowerBoundaryCaptureActive = true;
        mowerBaseLatLon = baseLatLonCandidate;
@@ -1053,9 +1518,12 @@
        Coordinate.setStartSaveGngga(true);
        if (mapRenderer != null) {
            mapRenderer.setBoundaryPreviewMarkerScale(2.0d);
            mapRenderer.beginHandheldBoundaryPreview();
        }
        setHandheldMowerIconActive(false);
        startMowerBoundaryMonitor();
        return true;
    }
@@ -1064,15 +1532,12 @@
        if (mapRenderer == null) {
            return false;
        }
        if (handheldCaptureDialog != null && handheldCaptureDialog.isShowing()) {
            handheldCaptureDialog.toFront();
            return true;
        }
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            stopMowerBoundaryCapture();
        }
        mapRenderer.clearIdleTrail();
        activeBoundaryMode = BoundaryCaptureMode.HANDHELD;
        handheldCaptureActive = true;
        handheldCapturedPoints = 0;
@@ -1084,19 +1549,10 @@
            handheldTemporaryPoints.clear();
        }
        AddDikuai.recordTemporaryBoundaryPoints(Collections.emptyList());
        mapRenderer.beginHandheldBoundaryPreview();
        Window ownerWindow = SwingUtilities.getWindowAncestor(this);
        SwingUtilities.invokeLater(() -> {
            Window targetOwner = ownerWindow;
            if (targetOwner == null) {
                targetOwner = SwingUtilities.getWindowAncestor(Shouye.this);
            }
            HandheldBoundaryCaptureDialog dialog = new HandheldBoundaryCaptureDialog(targetOwner, Shouye.this, visualizationPanel, THEME_COLOR);
            handheldCaptureDialog = dialog;
            dialog.setVisible(true);
        });
    mapRenderer.setBoundaryPreviewMarkerScale(1.0d);
    mapRenderer.beginHandheldBoundaryPreview();
        setHandheldMowerIconActive(true);
        enterHandheldCaptureInlineUi();
        return true;
    }
@@ -1188,6 +1644,7 @@
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            activeBoundaryMode = BoundaryCaptureMode.NONE;
        }
        setHandheldMowerIconActive(false);
    }
    private void discardLatestCoordinate(Coordinate coordinate) {
@@ -1282,6 +1739,7 @@
        List<Point2D.Double> closedSnapshot = createClosedHandheldPointSnapshot();
        handheldCaptureActive = false;
        activeBoundaryMode = BoundaryCaptureMode.NONE;
        Coordinate.setStartSaveGngga(false);
        if (mapRenderer != null) {
            mapRenderer.clearHandheldBoundaryPreview();
@@ -1289,20 +1747,12 @@
    AddDikuai.recordTemporaryBoundaryPoints(closedSnapshot);
        exitHandheldCaptureInlineUi();
        SwingUtilities.invokeLater(AddDikuai::finishDrawingSession);
        return true;
    }
    void handheldBoundaryCaptureDialogClosed(HandheldBoundaryCaptureDialog dialog) {
        if (handheldCaptureDialog == dialog) {
            handheldCaptureDialog = null;
        }
        handheldCaptureActive = false;
        if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD) {
            activeBoundaryMode = BoundaryCaptureMode.NONE;
        }
    }
    int getHandheldCapturedPointCount() {
        return handheldCapturedPoints;
    }
@@ -1571,9 +2021,9 @@
        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);
@@ -1601,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);
@@ -1658,6 +2131,12 @@
        if (notifyCoordinate) {
            Coordinate.setStartSaveGngga(!paused);
        }
        if (drawingControlModeActive) {
            updateDrawingControlButtonLabels();
            if (statusLabel != null) {
                statusLabel.setText(paused ? "绘制暂停" : "绘制中");
            }
        }
    }
    private void toggleDrawingPause() {
@@ -1670,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();
    }
@@ -1776,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();
@@ -2184,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) {
@@ -2330,7 +2820,9 @@
        clearCircleGuidanceArtifacts();
        hideFloatingDrawingControls();
        circleDialogMode = false;
        applyDrawingPauseState(false, false);
        exitHandheldCaptureInlineUi();
        handheldCaptureActive = false;
        exitDrawingControlMode();
        if (activeBoundaryMode == BoundaryCaptureMode.MOWER) {
            stopMowerBoundaryCapture();
        } else if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD && !handheldCaptureActive) {
@@ -2339,6 +2831,129 @@
        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();
    }
    
    /**
@@ -2370,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);