package zhuye; import javax.swing.*; import javax.swing.Timer; import baseStation.BaseStation; import set.Setsys; import baseStation.BaseStationDialog; import java.awt.*; import java.awt.event.*; import chuankou.dellmessage; import chuankou.sendmessage; import chuankou.SerialPortService; import dikuai.Dikuai; import dikuai.Dikuaiguanli; import dikuai.addzhangaiwu; import gecaoji.Device; import gecaoji.Gecaoji; import gecaoji.MowerBoundaryChecker; import set.Sets; import set.debug; import udpdell.UDPServer; import zhangaiwu.AddDikuai; import yaokong.Control04; import yaokong.RemoteControlDialog; 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.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.awt.geom.Point2D; /** * 首页界面 - 适配6.5寸竖屏,使用独立的MapRenderer进行绘制 */ public class Shouye extends JPanel { private static final long serialVersionUID = 1L; private static Shouye instance; // 主题颜色 private final Color THEME_COLOR = new Color(46, 139, 87); private final Color THEME_HOVER_COLOR = new Color(30, 107, 69); private final Color BACKGROUND_COLOR = new Color(250, 250, 250); private final Color PANEL_BACKGROUND = new Color(255, 255, 255); private final Color STATUS_PAUSE_COLOR = new Color(255, 165, 0); // 组件 private JPanel headerPanel; private JPanel mainContentPanel; private JPanel controlPanel; private JPanel navigationPanel; private JPanel visualizationPanel; // 按钮 private JButton startBtn; private JButton stopBtn; private JButton legendBtn; private JButton remoteBtn; private JButton areaSelectBtn; private JButton baseStationBtn; private JButton bluetoothBtn; private JLabel dataPacketCountLabel; private JLabel mowerSpeedValueLabel; private JLabel mowerSpeedUnitLabel; private JLabel mowingProgressLabel; private gpszhuangtai fixQualityIndicator; // 导航按钮 private JButton homeNavBtn; private JButton areasNavBtn; private JButton settingsNavBtn; // 状态显示 private JLabel statusLabel; private JLabel speedLabel; // 速度显示标签 private JLabel areaNameLabel; // 边界警告相关 private Timer boundaryWarningTimer; // 边界警告检查定时器 private Timer warningBlinkTimer; // 警告图标闪烁定时器 private boolean isMowerOutsideBoundary = false; // 割草机是否在边界外 private boolean warningIconVisible = true; // 警告图标显示状态(用于闪烁) // 以割草机为中心视图模式 private boolean centerOnMowerMode = false; // 是否处于以割草机为中心的模式 // 当前选中的导航按钮 private JButton currentNavButton; // 对话框引用 private LegendDialog legendDialog; // 使用完全限定名以避免与同包的 RemoteControlDialog 冲突,确保使用 yaokong 包下的实现 private yaokong.RemoteControlDialog remoteDialog; private AreaSelectionDialog areaDialog; private BaseStationDialog baseStationDialog; private Sets settingsDialog; private BaseStation baseStation; // 地图渲染器 private MapRenderer mapRenderer; private final Consumer serialLineListener = line -> { SwingUtilities.invokeLater(() -> { updateDataPacketCountLabel(); // 如果收到$GNGGA数据,立即更新拖尾 if (line != null && line.trim().startsWith("$GNGGA")) { if (mapRenderer != null) { mapRenderer.forceUpdateIdleMowerTrail(); } } }); }; private static final int FLOAT_ICON_SIZE = 32; private JButton endDrawingButton; private JButton drawingPauseButton; 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; private ImageIcon endIcon; private ImageIcon bluetoothIcon; private ImageIcon bluetoothLinkedIcon; private JPanel circleGuidancePanel; private JLabel circleGuidanceLabel; private JButton circleGuidancePrimaryButton; private JButton circleGuidanceSecondaryButton; private int circleGuidanceStep; private JDialog circleGuidanceDialog; 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 circleCapturedPoints = new ArrayList<>(); private double[] circleBaseLatLon; private Timer circleDataMonitor; private Coordinate lastCapturedCoordinate; private boolean handheldCaptureActive; private int handheldCapturedPoints; private final List handheldTemporaryPoints = new ArrayList<>(); private final List 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() { return instance; } private void initializeUI() { setLayout(new BorderLayout()); setBackground(BACKGROUND_COLOR); // 创建各个面板 createHeaderPanel(); createMainContentPanel(); createControlPanel(); createNavigationPanel(); // 添加到主面板 add(headerPanel, BorderLayout.NORTH); add(mainContentPanel, BorderLayout.CENTER); add(controlPanel, BorderLayout.SOUTH); // 初始化地图渲染器 mapRenderer = new MapRenderer(visualizationPanel); applyIdleTrailDurationFromSettings(); // 初始化对话框引用为null,延迟创建 legendDialog = null; remoteDialog = null; areaDialog = null; baseStationDialog = null; settingsDialog = null; // 设置默认状态 setNavigationActive(homeNavBtn); initializeDefaultAreaSelection(); refreshMapForSelectedArea(); // 启动边界警告检查定时器 startBoundaryWarningTimer(); } 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(); // 设置窗口关闭监听器,在关闭时保存缩放比例 setupWindowCloseListener(); }); } } }; addHierarchyListener(listener); } /** * 设置窗口关闭监听器,在窗口关闭时保存当前缩放比例 */ private void setupWindowCloseListener() { Window window = SwingUtilities.getWindowAncestor(this); if (window != null && window instanceof JFrame) { JFrame frame = (JFrame) window; frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { // 保存当前缩放比例 saveCurrentScale(); // 停止边界警告定时器 if (boundaryWarningTimer != null && boundaryWarningTimer.isRunning()) { boundaryWarningTimer.stop(); } // 停止闪烁定时器 if (warningBlinkTimer != null && warningBlinkTimer.isRunning()) { warningBlinkTimer.stop(); } } }); } } /** * 保存当前地图缩放比例和视图中心坐标到配置文件 */ public void saveCurrentScale() { if (mapRenderer != null) { double currentScale = mapRenderer.getScale(); double translateX = mapRenderer.getTranslateX(); double translateY = mapRenderer.getTranslateY(); Setsys setsys = new Setsys(); // 保留2位小数 setsys.updateProperty("mapScale", String.format("%.2f", currentScale)); setsys.updateProperty("viewCenterX", String.format("%.2f", translateX)); setsys.updateProperty("viewCenterY", String.format("%.2f", translateY)); } } /** * 启动边界警告检查定时器 */ private void startBoundaryWarningTimer() { // 边界检查定时器:每500ms检查一次割草机是否在边界内 boundaryWarningTimer = new Timer(500, e -> { checkMowerBoundaryStatus(); // 同时更新蓝牙图标状态 updateBluetoothButtonIcon(); }); boundaryWarningTimer.setInitialDelay(0); boundaryWarningTimer.start(); // 闪烁定时器:每1秒切换一次警告图标显示状态 warningBlinkTimer = new Timer(1000, e -> { if (isMowerOutsideBoundary) { warningIconVisible = !warningIconVisible; if (visualizationPanel != null) { visualizationPanel.repaint(); } } }); warningBlinkTimer.setInitialDelay(0); warningBlinkTimer.start(); } /** * 切换以割草机为中心的模式 */ private void toggleCenterOnMowerMode() { centerOnMowerMode = !centerOnMowerMode; if (centerOnMowerMode) { // 开启模式:立即将视图中心移动到割草机位置 updateViewToCenterOnMower(); } // 关闭模式时不需要做任何操作,用户已经可以自由移动地图 // 更新图标显示(重绘以切换图标) if (visualizationPanel != null) { visualizationPanel.repaint(); } } /** * 更新视图中心到割草机位置 */ private void updateViewToCenterOnMower() { if (mapRenderer == null) { return; } Gecaoji mower = mapRenderer.getMower(); if (mower != null && mower.hasValidPosition()) { Point2D.Double mowerPosition = mower.getPosition(); if (mowerPosition != null) { // 获取当前缩放比例 double currentScale = mapRenderer.getScale(); // 设置视图变换,使割草机位置对应到屏幕中心 // translateX = -mowerX, translateY = -mowerY 可以让割草机在屏幕中心 mapRenderer.setViewTransform(currentScale, -mowerPosition.x, -mowerPosition.y); } } } /** * 检查割草机边界状态 */ private void checkMowerBoundaryStatus() { // 如果处于以割草机为中心的模式,实时更新视图中心 if (centerOnMowerMode) { updateViewToCenterOnMower(); } // 检查是否在作业中 if (statusLabel == null || !"作业中".equals(statusLabel.getText())) { // 不在作业中,重置状态 if (isMowerOutsideBoundary) { isMowerOutsideBoundary = false; warningIconVisible = true; if (visualizationPanel != null) { visualizationPanel.repaint(); } } return; } // 在作业中,检查是否在边界内 if (mapRenderer == null) { return; } // 获取当前边界 List boundary = mapRenderer.getCurrentBoundary(); if (boundary == null || boundary.size() < 3) { // 没有边界,重置状态 if (isMowerOutsideBoundary) { isMowerOutsideBoundary = false; warningIconVisible = true; if (visualizationPanel != null) { visualizationPanel.repaint(); } } return; } // 获取割草机位置 Gecaoji mower = mapRenderer.getMower(); if (mower == null || !mower.hasValidPosition()) { // 无法获取位置,重置状态 if (isMowerOutsideBoundary) { isMowerOutsideBoundary = false; warningIconVisible = true; if (visualizationPanel != null) { visualizationPanel.repaint(); } } return; } Point2D.Double mowerPosition = mower.getPosition(); if (mowerPosition == null) { return; } // 使用 MowerBoundaryChecker 检查是否在边界内 boolean isInside = MowerBoundaryChecker.isInsideBoundaryPoints( boundary, mowerPosition.x, mowerPosition.y ); // 更新状态 boolean wasOutside = isMowerOutsideBoundary; isMowerOutsideBoundary = !isInside; // 如果状态改变,立即重绘 if (wasOutside != isMowerOutsideBoundary) { warningIconVisible = true; if (visualizationPanel != null) { visualizationPanel.repaint(); } } } private void showInitialMowerSelfCheckDialogIfNeeded() { // 已移除进入主页时的自检提示(按用户要求删除) // 以前这里会调用 zijian.showInitialPromptIfNeeded(...) 展示自检对话框,现已禁用。 } 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); // 应用边界距离显示设置 Setsys setsys = new Setsys(); setsys.initializeFromProperties(); mapRenderer.setBoundaryLengthVisible(setsys.isBoundaryLengthVisible()); } private void createHeaderPanel() { headerPanel = new JPanel(new BorderLayout()); headerPanel.setBackground(PANEL_BACKGROUND); headerPanel.setBorder(BorderFactory.createEmptyBorder(15, 20, 15, 20)); headerPanel.setPreferredSize(new Dimension(0, 80)); // 左侧信息区域(垂直排列:地块名称在上,状态行在下) JPanel leftInfoPanel = new JPanel(); leftInfoPanel.setBackground(PANEL_BACKGROUND); leftInfoPanel.setLayout(new BoxLayout(leftInfoPanel, BoxLayout.Y_AXIS)); // 保证子组件左对齐 leftInfoPanel.setAlignmentX(Component.LEFT_ALIGNMENT); areaNameLabel = new JLabel("未选择地块"); areaNameLabel.setFont(new Font("微软雅黑", Font.BOLD, 18)); areaNameLabel.setForeground(Color.BLACK); areaNameLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); areaNameLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { Dikuaiguanli.showDikuaiManagement(Shouye.this, null); } @Override public void mouseEntered(MouseEvent e) { areaNameLabel.setForeground(THEME_COLOR); } @Override public void mouseExited(MouseEvent e) { areaNameLabel.setForeground(Color.BLACK); } }); statusLabel = new JLabel("待机"); statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); statusLabel.setForeground(Color.GRAY); statusLabel.addPropertyChangeListener("text", evt -> { Object newValue = evt.getNewValue(); applyStatusLabelColor(newValue instanceof String ? (String) newValue : null); }); applyStatusLabelColor(statusLabel.getText()); // 添加速度显示标签 speedLabel = new JLabel(""); speedLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12)); speedLabel.setForeground(Color.GRAY); speedLabel.setVisible(false); // 默认隐藏 // 将状态与速度放在同一行,显示在地块名称下面一行 JPanel statusRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); statusRow.setOpaque(false); statusRow.add(statusLabel); statusRow.add(speedLabel); // 左对齐标签与状态行,确保它们在 BoxLayout 中靠左显示 areaNameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); statusRow.setAlignmentX(Component.LEFT_ALIGNMENT); leftInfoPanel.add(areaNameLabel); leftInfoPanel.add(statusRow); // 右侧操作区域 JPanel rightActionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); rightActionPanel.setBackground(PANEL_BACKGROUND); bluetoothBtn = createBluetoothButton(); // 修改设置按钮:使用图片替代Unicode图标 JButton settingsBtn = new JButton(); try { ImageIcon settingsIcon = new ImageIcon("image/sets.png"); // 调整图片大小以适应按钮 Image scaledImage = settingsIcon.getImage().getScaledInstance(30, 30, Image.SCALE_SMOOTH); settingsBtn.setIcon(new ImageIcon(scaledImage)); } catch (Exception e) { // 如果图片加载失败,使用默认文本 settingsBtn.setText("设置"); System.err.println("无法加载设置图标: " + e.getMessage()); } settingsBtn.setPreferredSize(new Dimension(40, 40)); settingsBtn.setBackground(PANEL_BACKGROUND); settingsBtn.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); settingsBtn.setFocusPainted(false); // 添加悬停效果 settingsBtn.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { settingsBtn.setBackground(new Color(240, 240, 240)); } public void mouseExited(MouseEvent e) { settingsBtn.setBackground(PANEL_BACKGROUND); } }); // 添加设置按钮事件 settingsBtn.addActionListener(e -> showSettingsDialog()); rightActionPanel.add(bluetoothBtn); rightActionPanel.add(settingsBtn); headerPanel.add(leftInfoPanel, BorderLayout.WEST); headerPanel.add(rightActionPanel, BorderLayout.EAST); } private void createMainContentPanel() { mainContentPanel = new JPanel(new BorderLayout()); mainContentPanel.setBackground(BACKGROUND_COLOR); // 可视化区域 - 使用MapRenderer进行绘制 visualizationPanel = new JPanel() { private ImageIcon gecaojiIcon1 = null; // 默认图标 private ImageIcon gecaojiIcon2 = null; // 以割草机为中心模式图标 private static final int GECAOJI_ICON_X = 37; private static final int GECAOJI_ICON_Y = 10; private static final int GECAOJI_ICON_SIZE = 20; { // 加载割草机图标,大小20x20像素 gecaojiIcon1 = loadScaledIcon("image/gecaojishijiao1.png", GECAOJI_ICON_SIZE, GECAOJI_ICON_SIZE); gecaojiIcon2 = loadScaledIcon("image/gecaojishijiao2.png", GECAOJI_ICON_SIZE, GECAOJI_ICON_SIZE); } /** * 检查鼠标位置是否在割草机图标区域内 */ private boolean isMouseOnGecaojiIcon(Point mousePoint) { return mousePoint.x >= GECAOJI_ICON_X && mousePoint.x <= GECAOJI_ICON_X + GECAOJI_ICON_SIZE && mousePoint.y >= GECAOJI_ICON_Y && mousePoint.y <= GECAOJI_ICON_Y + GECAOJI_ICON_SIZE; } @Override public String getToolTipText(MouseEvent event) { // 如果鼠标在割草机图标区域内,显示提示文字 if (isMouseOnGecaojiIcon(event.getPoint())) { // 根据当前模式显示不同的提示文字 return centerOnMowerMode ? "取消以割草机为中心" : "以割草机为中心"; } // 不在图标上时返回null,不显示工具提示框 return null; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 委托给MapRenderer进行绘制 if (mapRenderer != null) { mapRenderer.renderMap(g); } // 检查是否需要显示警告图标 if (isMowerOutsideBoundary && warningIconVisible) { // 绘制红色三角形警告图标(带叹号) drawWarningIcon(g, GECAOJI_ICON_X, GECAOJI_ICON_Y, GECAOJI_ICON_SIZE); } else { // 根据模式选择不同的图标 ImageIcon iconToDraw = centerOnMowerMode ? gecaojiIcon2 : gecaojiIcon1; if (iconToDraw != null) { // 绘制割草机图标 // 水平方向与速度指示器对齐(x=37) // 垂直方向与卫星状态图标对齐(y=10,速度指示器面板顶部边距10像素,使图标中心对齐) g.drawImage(iconToDraw.getImage(), GECAOJI_ICON_X, GECAOJI_ICON_Y, null); } } } /** * 绘制红色三角形警告图标(带叹号) */ private void drawWarningIcon(Graphics g, int x, int y, int size) { Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 绘制红色三角形 int[] xPoints = {x + size / 2, x, x + size}; int[] yPoints = {y, y + size, y + size}; g2d.setColor(Color.RED); g2d.fillPolygon(xPoints, yPoints, 3); // 绘制白色边框 g2d.setColor(Color.WHITE); g2d.setStroke(new BasicStroke(1.5f)); g2d.drawPolygon(xPoints, yPoints, 3); // 绘制白色叹号 g2d.setColor(Color.WHITE); g2d.setFont(new Font("Arial", Font.BOLD, size * 3 / 4)); FontMetrics fm = g2d.getFontMetrics(); String exclamation = "!"; int textWidth = fm.stringWidth(exclamation); int textHeight = fm.getAscent(); g2d.drawString(exclamation, x + (size - textWidth) / 2, y + (size + textHeight) / 2 - 2); g2d.dispose(); } }; visualizationPanel.setLayout(new BorderLayout()); // 添加鼠标点击监听器,检测是否点击了割草机图标 visualizationPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) { Point clickPoint = e.getPoint(); // 检查是否点击了割草机图标区域(37, 10, 20, 20) if (clickPoint.x >= 37 && clickPoint.x <= 57 && clickPoint.y >= 10 && clickPoint.y <= 30) { // 切换以割草机为中心的模式 toggleCenterOnMowerMode(); } } } }); JPanel speedIndicatorPanel = createSpeedIndicatorPanel(); visualizationPanel.add(speedIndicatorPanel, BorderLayout.NORTH); // 创建功能按钮面板 JPanel functionButtonsPanel = new JPanel(); functionButtonsPanel.setLayout(new BoxLayout(functionButtonsPanel, BoxLayout.Y_AXIS)); functionButtonsPanel.setOpaque(false); functionButtonsPanel.setBorder(BorderFactory.createEmptyBorder(20, 5, 0, 0)); // 左边距改为5像素 legendBtn = createFunctionButton("图例", "📖"); baseStationBtn = createFunctionButton("基准站", "📡"); // 调整到第二位 areaSelectBtn = createFunctionButton("地块", "🌿"); // 调整到第三位 remoteBtn = createFunctionButton("遥控", "🎮"); // 调整到最后 functionButtonsPanel.add(legendBtn); functionButtonsPanel.add(Box.createRigidArea(new Dimension(0, 10))); functionButtonsPanel.add(baseStationBtn); functionButtonsPanel.add(Box.createRigidArea(new Dimension(0, 10))); functionButtonsPanel.add(areaSelectBtn); functionButtonsPanel.add(Box.createRigidArea(new Dimension(0, 10))); functionButtonsPanel.add(remoteBtn); visualizationPanel.add(functionButtonsPanel, BorderLayout.WEST); JPanel zoomControlPanel = createZoomControlPanel(); visualizationPanel.add(zoomControlPanel, BorderLayout.EAST); mainContentPanel.add(visualizationPanel, BorderLayout.CENTER); startMowerSpeedUpdates(); } private JPanel createZoomControlPanel() { JPanel container = new JPanel(new BorderLayout()); container.setOpaque(false); container.setBorder(BorderFactory.createEmptyBorder(0, 0, 20, 20)); JPanel alignmentPanel = new JPanel(); alignmentPanel.setOpaque(false); alignmentPanel.setLayout(new BoxLayout(alignmentPanel, BoxLayout.Y_AXIS)); alignmentPanel.add(Box.createVerticalGlue()); JPanel buttonStack = new JPanel(); buttonStack.setOpaque(false); buttonStack.setLayout(new BoxLayout(buttonStack, BoxLayout.Y_AXIS)); JButton zoomInButton = createZoomButton("+"); JButton zoomOutButton = createZoomButton("-"); AtomicBoolean skipZoomInClick = new AtomicBoolean(false); AtomicBoolean skipZoomOutClick = new AtomicBoolean(false); Timer zoomInHoldTimer = new Timer(120, event -> { if (mapRenderer == null || !zoomInButton.getModel().isPressed()) { ((Timer) event.getSource()).stop(); return; } if (!mapRenderer.canZoomIn()) { ((Timer) event.getSource()).stop(); return; } skipZoomInClick.set(true); mapRenderer.zoomInFromCenter(); if (!mapRenderer.canZoomIn()) { ((Timer) event.getSource()).stop(); } }); zoomInHoldTimer.setInitialDelay(180); zoomInHoldTimer.setDelay(120); Timer zoomOutHoldTimer = new Timer(120, event -> { if (mapRenderer == null || !zoomOutButton.getModel().isPressed()) { ((Timer) event.getSource()).stop(); return; } if (!mapRenderer.canZoomOut()) { ((Timer) event.getSource()).stop(); return; } skipZoomOutClick.set(true); mapRenderer.zoomOutFromCenter(); if (!mapRenderer.canZoomOut()) { ((Timer) event.getSource()).stop(); } }); zoomOutHoldTimer.setInitialDelay(180); zoomOutHoldTimer.setDelay(120); zoomInButton.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e) || mapRenderer == null || !zoomInButton.isEnabled()) { return; } if (mapRenderer.canZoomIn()) { skipZoomInClick.set(true); mapRenderer.zoomInFromCenter(); if (mapRenderer.canZoomIn()) { zoomInHoldTimer.restart(); } else { zoomInHoldTimer.stop(); } } else { skipZoomInClick.set(false); zoomInHoldTimer.stop(); } } @Override public void mouseReleased(MouseEvent e) { zoomInHoldTimer.stop(); if (!zoomInButton.contains(e.getPoint())) { skipZoomInClick.set(false); } } @Override public void mouseExited(MouseEvent e) { if (!zoomInButton.getModel().isPressed()) { zoomInHoldTimer.stop(); } } }); zoomOutButton.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e) || mapRenderer == null || !zoomOutButton.isEnabled()) { return; } if (mapRenderer.canZoomOut()) { skipZoomOutClick.set(true); mapRenderer.zoomOutFromCenter(); if (mapRenderer.canZoomOut()) { zoomOutHoldTimer.restart(); } else { zoomOutHoldTimer.stop(); } } else { skipZoomOutClick.set(false); zoomOutHoldTimer.stop(); } } @Override public void mouseReleased(MouseEvent e) { zoomOutHoldTimer.stop(); if (!zoomOutButton.contains(e.getPoint())) { skipZoomOutClick.set(false); } } @Override public void mouseExited(MouseEvent e) { if (!zoomOutButton.getModel().isPressed()) { zoomOutHoldTimer.stop(); } } }); zoomInButton.addActionListener(e -> { if (skipZoomInClick.getAndSet(false)) { return; } if (mapRenderer != null) { mapRenderer.zoomInFromCenter(); } }); zoomOutButton.addActionListener(e -> { if (skipZoomOutClick.getAndSet(false)) { return; } if (mapRenderer != null) { mapRenderer.zoomOutFromCenter(); } }); buttonStack.add(zoomInButton); buttonStack.add(Box.createRigidArea(new Dimension(0, 8))); buttonStack.add(zoomOutButton); buttonStack.setAlignmentX(Component.CENTER_ALIGNMENT); alignmentPanel.add(buttonStack); container.add(alignmentPanel, BorderLayout.CENTER); return container; } private JButton createZoomButton(String symbol) { JButton button = new JButton(symbol); Dimension size = new Dimension(44, 44); button.setPreferredSize(size); button.setMinimumSize(size); button.setMaximumSize(size); button.setFont(new Font("微软雅黑", Font.BOLD, 22)); button.setMargin(new Insets(0, 0, 0, 0)); button.setFocusPainted(false); button.setBackground(Color.WHITE); button.setForeground(Color.DARK_GRAY); button.setBorder(BorderFactory.createLineBorder(new Color(210, 210, 210), 1, true)); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); button.setOpaque(true); button.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { button.setBackground(new Color(245, 245, 245)); } @Override public void mouseExited(MouseEvent e) { button.setBackground(Color.WHITE); } }); return button; } private void createControlPanel() { controlPanel = new JPanel(new BorderLayout()); controlPanel.setBackground(PANEL_BACKGROUND); controlPanel.setBorder(BorderFactory.createEmptyBorder(15, 20, 15, 20)); controlPanel.setPreferredSize(new Dimension(0, 100)); JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 20, 0)); buttonPanel.setBackground(PANEL_BACKGROUND); startBtn = createControlButton("暂停", THEME_COLOR); updateStartButtonAppearance(); stopBtn = createControlButton("结束", Color.ORANGE); updateStopButtonIcon(); buttonPanel.add(startBtn); buttonPanel.add(stopBtn); controlPanel.add(buttonPanel, BorderLayout.CENTER); } private void createNavigationPanel() { navigationPanel = new JPanel(new GridLayout(1, 3)); navigationPanel.setBackground(PANEL_BACKGROUND); navigationPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); navigationPanel.setPreferredSize(new Dimension(0, 70)); homeNavBtn = createNavButton("首页", "🏠"); areasNavBtn = createNavButton("地块", "🌿"); settingsNavBtn = createNavButton("设置", "⚙️"); navigationPanel.add(homeNavBtn); navigationPanel.add(areasNavBtn); navigationPanel.add(settingsNavBtn); // 添加到主界面底部 add(navigationPanel, BorderLayout.SOUTH); } private JButton createIconButton(String icon, int size) { JButton button = new JButton(icon); button.setFont(new Font("Segoe UI Emoji", Font.PLAIN, 20)); button.setPreferredSize(new Dimension(size, size)); button.setBackground(PANEL_BACKGROUND); button.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); button.setFocusPainted(false); // 悬停效果 button.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { button.setBackground(new Color(240, 240, 240)); } public void mouseExited(MouseEvent e) { button.setBackground(PANEL_BACKGROUND); } }); return button; } private JButton createBluetoothButton() { JButton button = new JButton(); button.setPreferredSize(new Dimension(40, 40)); button.setBackground(PANEL_BACKGROUND); button.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); button.setFocusPainted(false); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); button.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { button.setBackground(new Color(240, 240, 240)); } @Override public void mouseExited(MouseEvent e) { button.setBackground(PANEL_BACKGROUND); } }); ensureBluetoothIconsLoaded(); // 根据串口连接状态显示图标 SerialPortService service = sendmessage.getActiveService(); boolean serialConnected = (service != null && service.isOpen()); ImageIcon initialIcon = serialConnected ? bluetoothLinkedIcon : bluetoothIcon; if (initialIcon != null) { button.setIcon(initialIcon); } else { button.setText(serialConnected ? "已连" : "蓝牙"); } return button; } private JButton createFunctionButton(String text, String icon) { JButton button = new JButton("
" + icon + "
" + text + "
"); button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); button.setPreferredSize(new Dimension(80, 70)); button.setBackground(new Color(0, 0, 0, 0)); // 完全透明背景 button.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); // 只保留内边距 button.setFocusPainted(false); button.setOpaque(false); // 设置为不透明,确保背景透明 // 去掉悬停效果 button.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { // 悬停时不改变任何样式 } public void mouseExited(MouseEvent e) { // 悬停时不改变任何样式 } }); return button; } private JButton createControlButton(String text, Color color) { JButton button = new JButton(text); button.setFont(new Font("微软雅黑", Font.BOLD, 16)); button.setBackground(color); button.setForeground(Color.WHITE); button.setBorder(BorderFactory.createEmptyBorder(15, 0, 15, 0)); button.setFocusPainted(false); // 悬停效果 button.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { if (button.isEnabled()) { if (color == THEME_COLOR) { button.setBackground(THEME_HOVER_COLOR); } else { button.setBackground(new Color(255, 165, 0)); } } } public void mouseExited(MouseEvent e) { if (button.isEnabled()) { button.setBackground(color); } } }); return button; } private void applyButtonIcon(JButton button, String imagePath) { try { ImageIcon icon = new ImageIcon(imagePath); Image scaledImage = icon.getImage().getScaledInstance(28, 28, Image.SCALE_SMOOTH); button.setIcon(new ImageIcon(scaledImage)); button.setHorizontalAlignment(SwingConstants.CENTER); button.setIconTextGap(10); button.setHorizontalTextPosition(SwingConstants.RIGHT); button.setVerticalTextPosition(SwingConstants.CENTER); } catch (Exception e) { System.err.println("无法加载按钮图标: " + imagePath + " -> " + e.getMessage()); } } private JButton createNavButton(String text, String icon) { JButton button = new JButton("
" + icon + "
" + text + "
"); button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); button.setBackground(PANEL_BACKGROUND); button.setBorder(BorderFactory.createEmptyBorder(10, 5, 10, 5)); button.setFocusPainted(false); // 悬停效果 button.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { if (button != currentNavButton) { button.setBackground(new Color(240, 240, 240)); } } public void mouseExited(MouseEvent e) { if (button != currentNavButton) { button.setBackground(PANEL_BACKGROUND); } } }); return button; } private void setNavigationActive(JButton navButton) { // 重置所有导航按钮样式 for (Component comp : navigationPanel.getComponents()) { if (comp instanceof JButton) { JButton btn = (JButton) comp; btn.setBackground(PANEL_BACKGROUND); btn.setForeground(Color.BLACK); } } // 设置当前选中按钮样式 navButton.setBackground(THEME_COLOR); navButton.setForeground(Color.WHITE); currentNavButton = navButton; } private void setupEventHandlers() { // 导航按钮事件 homeNavBtn.addActionListener(e -> setNavigationActive(homeNavBtn)); areasNavBtn.addActionListener(e -> setNavigationActive(areasNavBtn)); settingsNavBtn.addActionListener(e -> setNavigationActive(settingsNavBtn)); // 功能按钮事件 legendBtn.addActionListener(e -> showLegendDialog()); if (bluetoothBtn != null) { bluetoothBtn.addActionListener(e -> toggleBluetoothConnection()); } remoteBtn.addActionListener(e -> showRemoteControlDialog()); areaSelectBtn.addActionListener(e -> { // 点击“地块”直接打开地块管理对话框(若需要可传入特定地块编号) Dikuaiguanli.showDikuaiManagement(this, null); }); baseStationBtn.addActionListener(e -> showBaseStationDialog()); // 控制按钮事件 startBtn.addActionListener(e -> toggleStartPause()); stopBtn.addActionListener(e -> handleStopAction()); } private void showSettingsDialog() { if (settingsDialog == null) { Window parentWindow = SwingUtilities.getWindowAncestor(this); if (parentWindow instanceof JFrame) { settingsDialog = new Sets((JFrame) parentWindow, THEME_COLOR); } else if (parentWindow instanceof JDialog) { settingsDialog = new Sets((JDialog) parentWindow, THEME_COLOR); } else { // Fallback to a frameless dialog when no parent is available settingsDialog = new Sets((JFrame) null, THEME_COLOR); } } settingsDialog.setVisible(true); } private void showLegendDialog() { if (legendDialog == null) { Window parentWindow = SwingUtilities.getWindowAncestor(this); if (parentWindow != null) { legendDialog = new LegendDialog(this, THEME_COLOR); } else { // 如果没有父窗口,创建无父窗口的对话框 legendDialog = new LegendDialog((JFrame) null, THEME_COLOR); } } legendDialog.setVisible(true); } private void showRemoteControlDialog() { if (remoteDialog == null) { Window parentWindow = SwingUtilities.getWindowAncestor(this); if (parentWindow != null) { // 使用 yaokong 包中的 RemoteControlDialog 实现 remoteDialog = new yaokong.RemoteControlDialog(this, THEME_COLOR, speedLabel); } else { remoteDialog = new yaokong.RemoteControlDialog((JFrame) null, THEME_COLOR, speedLabel); } } if (remoteDialog != null) { positionRemoteDialogBottomCenter(remoteDialog); zijian.markSelfCheckCompleted(); remoteDialog.setVisible(true); } } private void positionRemoteDialogBottomCenter(RemoteControlDialog dialog) { if (dialog == null) { return; } // 将对话框底部与整个首页(Shouye 面板)的下边框对齐, // 而不是与 visualizationPanel 对齐,以满足设计要求。 Dimension dialogSize = dialog.getSize(); // 获取当前 Shouye 面板在屏幕上的位置和尺寸 Point parentOnScreen = null; try { parentOnScreen = this.getLocationOnScreen(); } catch (IllegalComponentStateException ex) { // 如果组件尚未显示或无法获取屏幕位置,回退到窗口层级位置获取 Window owner = SwingUtilities.getWindowAncestor(this); if (owner != null) { parentOnScreen = owner.getLocationOnScreen(); } } int x = 0, y = 0; if (parentOnScreen != null) { int parentWidth = this.getWidth(); int parentHeight = this.getHeight(); x = parentOnScreen.x + (parentWidth - dialogSize.width) / 2; y = parentOnScreen.y + parentHeight - dialogSize.height; } else { // 作为最后的回退,使用屏幕中心对齐底部 Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); x = (screen.width - dialogSize.width) / 2; y = screen.height - 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() { if (areaDialog == null) { Window parentWindow = SwingUtilities.getWindowAncestor(this); if (parentWindow != null) { areaDialog = new AreaSelectionDialog(this, THEME_COLOR, areaNameLabel, statusLabel); } else { areaDialog = new AreaSelectionDialog((JFrame) null, THEME_COLOR, areaNameLabel, statusLabel); } } areaDialog.setVisible(true); } private void showBaseStationDialog() { if (baseStation == null) { baseStation = new BaseStation(); } baseStation.load(); Component dialogParent = this; if (!hasValidBaseStationId()) { boolean recorded = promptForBaseStationId(dialogParent); if (!recorded) { return; } } 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); } else { baseStationDialog.refreshData(); } 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; } String deviceId = baseStation.getDeviceId(); if (deviceId == null) { return false; } String trimmed = deviceId.trim(); return !trimmed.isEmpty() && !"-1".equals(trimmed); } private boolean promptForBaseStationId(Component parentComponent) { if (baseStation == null) { baseStation = new BaseStation(); } while (true) { String input = JOptionPane.showInputDialog(parentComponent, "请输入基准站编号", "录入基准站编号", JOptionPane.PLAIN_MESSAGE); if (input == null) { return false; } String trimmed = input.trim(); if (trimmed.isEmpty()) { JOptionPane.showMessageDialog(parentComponent, "基准站编号不能为空,请重新输入。", "提示", JOptionPane.WARNING_MESSAGE); continue; } try { baseStation.updateByDeviceId(trimmed, baseStation.getInstallationCoordinates(), baseStation.getIotSimCardNumber(), baseStation.getDeviceActivationTime(), baseStation.getDataUpdateTime()); baseStation.load(); JOptionPane.showMessageDialog(parentComponent, "基准站编号已保存。", "操作成功", JOptionPane.INFORMATION_MESSAGE); return true; } catch (IllegalArgumentException ex) { JOptionPane.showMessageDialog(parentComponent, ex.getMessage(), "输入错误", JOptionPane.ERROR_MESSAGE); } } } private void toggleStartPause() { if (handheldCaptureInlineUiActive) { handleHandheldConfirmAction(); return; } if (drawingControlModeActive) { toggleDrawingPause(); return; } if (startBtn == null) { return; } if (startButtonShowingPause) { // 点击开始按钮时不再弹出自检提示(按用户要求删除) // 旧逻辑:调用 zijian.ensureBeforeMowing(...) 并在未确认自检时阻止开始 // 新逻辑:直接允许开始作业 } startButtonShowingPause = !startButtonShowingPause; if (!startButtonShowingPause) { // 检查割草机是否在作业地块边界范围内 if (!checkMowerInBoundary()) { startButtonShowingPause = true; statusLabel.setText("待机"); updateStartButtonAppearance(); return; } statusLabel.setText("作业中"); if (stopButtonActive) { stopButtonActive = false; updateStopButtonIcon(); } if (!beginMowingSession()) { startButtonShowingPause = true; statusLabel.setText("待机"); updateStartButtonAppearance(); return; } } else { statusLabel.setText("暂停中"); pauseMowingSession(); } updateStartButtonAppearance(); } /** * 检查割草机是否在当前选中的作业地块边界范围内 * @return 如果割草机在边界内返回true,否则返回false并显示提示 */ private boolean checkMowerInBoundary() { if (mapRenderer == null) { return true; // 如果没有地图渲染器,跳过检查 } // 获取当前边界 List boundary = mapRenderer.getCurrentBoundary(); if (boundary == null || boundary.size() < 3) { return true; // 如果没有边界或边界点不足,跳过检查 } // 获取割草机位置 Gecaoji mower = mapRenderer.getMower(); if (mower == null || !mower.hasValidPosition()) { showCustomMessageDialog("无法获取割草机位置,请检查设备连接", "提示"); return false; } Point2D.Double mowerPosition = mower.getPosition(); if (mowerPosition == null) { showCustomMessageDialog("无法获取割草机位置,请检查设备连接", "提示"); return false; } // 使用 MowerBoundaryChecker 检查是否在边界内 boolean isInside = MowerBoundaryChecker.isInsideBoundaryPoints( boundary, mowerPosition.x, mowerPosition.y ); if (!isInside) { showCustomMessageDialog("请将割草机开到作业地块内然后点击开始作业", "提示"); return false; } return true; } /** * 显示自定义消息对话框,使用 buttonset 创建确定按钮 * @param message 消息内容 * @param title 对话框标题 */ private void showCustomMessageDialog(String message, String title) { Window parentWindow = SwingUtilities.getWindowAncestor(this); JDialog dialog = new JDialog(parentWindow, title, Dialog.ModalityType.APPLICATION_MODAL); dialog.setLayout(new BorderLayout(20, 20)); dialog.setResizable(false); // 内容面板 JPanel contentPanel = new JPanel(new BorderLayout(0, 15)); contentPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 10, 20)); contentPanel.setBackground(Color.WHITE); // 消息标签 JLabel messageLabel = new JLabel("
" + message + "
"); messageLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); messageLabel.setHorizontalAlignment(SwingConstants.CENTER); contentPanel.add(messageLabel, BorderLayout.CENTER); // 按钮面板 JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0)); buttonPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0)); buttonPanel.setOpaque(false); // 使用 buttonset 创建确定按钮 JButton okButton = buttonset.createStyledButton("确定", THEME_COLOR); okButton.addActionListener(e -> dialog.dispose()); buttonPanel.add(okButton); contentPanel.add(buttonPanel, BorderLayout.SOUTH); dialog.add(contentPanel, BorderLayout.CENTER); dialog.pack(); dialog.setLocationRelativeTo(this); dialog.setVisible(true); } 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 = "
采集点" + nextIndex + "
确定
"; 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; } String iconPath = startButtonShowingPause ? "image/start0.png" : "image/start1.png"; startBtn.setText(startButtonShowingPause ? "暂停" : "开始"); applyButtonIcon(startBtn, iconPath); } private void updateStopButtonIcon() { if (stopBtn == null) { return; } String iconPath = stopButtonActive ? "image/stop1.png" : "image/stop0.png"; applyButtonIcon(stopBtn, iconPath); } private void toggleBluetoothConnection() { if (bluetoothBtn == null) { return; } // 弹出系统调试页面 showDebugDialog(); } private void showDebugDialog() { Window parentWindow = SwingUtilities.getWindowAncestor(this); debug debugDialog = new debug(parentWindow, THEME_COLOR); debugDialog.setLocationRelativeTo(this); // 居中显示在首页 debugDialog.setVisible(true); } private void updateBluetoothButtonIcon() { if (bluetoothBtn == null) { return; } ensureBluetoothIconsLoaded(); // 根据串口连接状态显示图标 SerialPortService service = sendmessage.getActiveService(); boolean serialConnected = (service != null && service.isOpen()); ImageIcon icon = serialConnected ? bluetoothLinkedIcon : bluetoothIcon; if (icon != null) { bluetoothBtn.setIcon(icon); bluetoothBtn.setText(null); } else { bluetoothBtn.setText(serialConnected ? "已连" : "蓝牙"); } } 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 gpszhuangtai(THEME_COLOR); 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 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 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 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 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 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 createClosedHandheldPointSnapshot() { List 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 createClosedMowerPointSnapshot() { List 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) || "绘制中".equals(statusText)) { statusLabel.setForeground(THEME_COLOR); } else if ("暂停中".equals(statusText) || "绘制暂停".equals(statusText)) { statusLabel.setForeground(STATUS_PAUSE_COLOR); } else { statusLabel.setForeground(Color.GRAY); } } private void ensureBluetoothIconsLoaded() { if (bluetoothIcon == null) { bluetoothIcon = loadScaledIcon("image/blue.png", 28, 28); } if (bluetoothLinkedIcon == null) { bluetoothLinkedIcon = loadScaledIcon("image/bluelink.png", 28, 28); } } private JButton createFloatingIconButton() { JButton button = new JButton(); button.setContentAreaFilled(false); button.setBorder(BorderFactory.createEmptyBorder()); button.setFocusPainted(false); button.setOpaque(false); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); int size = FLOAT_ICON_SIZE + 8; button.setPreferredSize(new Dimension(size, size)); 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); if (icon.getIconWidth() <= 0 || icon.getIconHeight() <= 0) { return null; } Image scaled = icon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH); return new ImageIcon(scaled); } catch (Exception ex) { System.err.println("加载图标失败: " + path + " - " + ex.getMessage()); return null; } } private void ensureFloatingIconsLoaded() { if (pauseIcon == null) { pauseIcon = loadScaledIcon("image/zanting.png", FLOAT_ICON_SIZE, FLOAT_ICON_SIZE); } if (pauseActiveIcon == null) { pauseActiveIcon = loadScaledIcon("image/zantingzhong.png", FLOAT_ICON_SIZE, FLOAT_ICON_SIZE); } if (endIcon == null) { endIcon = loadScaledIcon("image/end.png", FLOAT_ICON_SIZE, FLOAT_ICON_SIZE); } } private void updatePauseButtonVisual() { if (drawingPauseButton == null) { return; } if (drawingPaused) { if (pauseActiveIcon != null) { drawingPauseButton.setIcon(pauseActiveIcon); drawingPauseButton.setText(null); } else { drawingPauseButton.setText("暂停中"); drawingPauseButton.setIcon(null); } drawingPauseButton.setToolTipText("点击恢复绘制"); } else { if (pauseIcon != null) { drawingPauseButton.setIcon(pauseIcon); drawingPauseButton.setText(null); } else { drawingPauseButton.setText("暂停"); drawingPauseButton.setIcon(null); } drawingPauseButton.setToolTipText("点击暂停绘制"); } } private void applyDrawingPauseState(boolean paused, boolean notifyCoordinate) { drawingPaused = paused; updatePauseButtonVisual(); if (notifyCoordinate) { Coordinate.setStartSaveGngga(!paused); } if (drawingControlModeActive) { updateDrawingControlButtonLabels(); if (statusLabel != null) { statusLabel.setText(paused ? "绘制暂停" : "绘制中"); } } } private void toggleDrawingPause() { applyDrawingPauseState(!drawingPaused, true); } public void showEndDrawingButton(Runnable callback) { showEndDrawingButton(callback, null); } public void showEndDrawingButton(Runnable callback, String drawingShape) { endDrawingCallback = callback; circleDialogMode = false; hideCircleGuidancePanel(); enterDrawingControlMode(); boolean enableCircleGuidance = drawingShape != null && "circle".equalsIgnoreCase(drawingShape.trim()); if (enableCircleGuidance) { 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(); hideFloatingDrawingControls(); } visualizationPanel.revalidate(); visualizationPanel.repaint(); } private void ensureFloatingButtonInfrastructure() { if (endDrawingButton == null) { endDrawingButton = createFloatingIconButton(); endDrawingButton.addActionListener(e -> { if (endDrawingCallback != null) { endDrawingCallback.run(); } }); } if (endIcon != null) { endDrawingButton.setIcon(endIcon); endDrawingButton.setText(null); } else { endDrawingButton.setText("结束绘制"); } endDrawingButton.setToolTipText("结束绘制"); if (drawingPauseButton == null) { drawingPauseButton = createFloatingIconButton(); drawingPauseButton.addActionListener(e -> toggleDrawingPause()); } updatePauseButtonVisual(); if (floatingButtonPanel == null) { floatingButtonPanel = new JPanel(new BorderLayout()); floatingButtonPanel.setOpaque(false); floatingButtonPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 20, 20)); floatingButtonColumn = new JPanel(); floatingButtonColumn.setOpaque(false); floatingButtonColumn.setLayout(new BoxLayout(floatingButtonColumn, BoxLayout.Y_AXIS)); floatingButtonPanel.add(floatingButtonColumn, BorderLayout.EAST); } } private void hideFloatingDrawingControls() { if (drawingPauseButton != null) { drawingPauseButton.setVisible(false); } if (endDrawingButton != null) { endDrawingButton.setVisible(false); } if (floatingButtonPanel != null) { floatingButtonPanel.setVisible(false); } if (!circleDialogMode) { rebuildFloatingButtonColumn(); } } private void rebuildFloatingButtonColumn() { if (floatingButtonColumn == null) { return; } floatingButtonColumn.removeAll(); boolean added = false; if (!circleDialogMode && circleGuidancePanel != null && circleGuidancePanel.isVisible()) { floatingButtonColumn.add(circleGuidancePanel); added = true; } if (!circleDialogMode && drawingPauseButton != null && drawingPauseButton.isVisible()) { if (added) { floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10))); } floatingButtonColumn.add(drawingPauseButton); added = true; } if (!circleDialogMode && endDrawingButton != null && endDrawingButton.isVisible()) { if (added) { 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(); } private void showCircleGuidanceStep(int step) { ensureCircleGuidancePanel(); if (circleGuidancePanel == null) { return; } circleGuidanceStep = step; if (step == 1) { circleGuidanceLabel.setText("采集第1个点"); circleGuidancePrimaryButton.setText("确认第1点"); circleGuidanceSecondaryButton.setText("返回"); circleGuidanceSecondaryButton.setVisible(true); } else if (step == 2) { circleGuidanceLabel.setText("采集第2个点"); circleGuidancePrimaryButton.setText("确认第2点"); circleGuidanceSecondaryButton.setText("返回"); circleGuidanceSecondaryButton.setVisible(true); } else if (step == 3) { circleGuidanceLabel.setText("采集第3个点"); circleGuidancePrimaryButton.setText("确认第3点"); circleGuidanceSecondaryButton.setText("返回"); circleGuidanceSecondaryButton.setVisible(true); } else if (step == 4) { circleGuidanceLabel.setText("已采集三个点"); circleGuidancePrimaryButton.setText("结束绘制"); circleGuidanceSecondaryButton.setText("重新采集"); circleGuidanceSecondaryButton.setVisible(true); } else { hideCircleGuidancePanel(); return; } circleGuidancePanel.setVisible(true); refreshCircleGuidanceButtonAvailability(); if (circleDialogMode) { ensureCircleGuidanceDialog(); if (circleGuidanceDialog != null) { circleGuidanceDialog.pack(); positionCircleGuidanceDialog(); circleGuidanceDialog.setVisible(true); circleGuidanceDialog.toFront(); } } else { rebuildFloatingButtonColumn(); } } private void ensureCircleGuidancePanel() { if (circleGuidancePanel != null) { return; } circleGuidancePanel = new JPanel(); circleGuidancePanel.setLayout(new BoxLayout(circleGuidancePanel, BoxLayout.Y_AXIS)); circleGuidancePanel.setOpaque(true); circleGuidancePanel.setBackground(new Color(255, 255, 255, 235)); circleGuidancePanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(THEME_COLOR, 1), BorderFactory.createEmptyBorder(10, 12, 10, 12))); circleGuidancePanel.setAlignmentX(Component.LEFT_ALIGNMENT); circleGuidanceLabel = new JLabel(); circleGuidanceLabel.setFont(new Font("微软雅黑", Font.BOLD, 13)); circleGuidanceLabel.setForeground(new Color(33, 37, 41)); circleGuidanceLabel.setAlignmentX(Component.LEFT_ALIGNMENT); JPanel buttonRow = new JPanel(); buttonRow.setLayout(new BoxLayout(buttonRow, BoxLayout.X_AXIS)); buttonRow.setOpaque(false); buttonRow.setAlignmentX(Component.LEFT_ALIGNMENT); circleGuidancePrimaryButton = createGuidanceButton("是", THEME_COLOR, Color.WHITE, true); circleGuidancePrimaryButton.addActionListener(e -> onCircleGuidancePrimary()); circleGuidanceSecondaryButton = createGuidanceButton("否", Color.WHITE, THEME_COLOR, false); circleGuidanceSecondaryButton.addActionListener(e -> onCircleGuidanceSecondary()); buttonRow.add(circleGuidancePrimaryButton); buttonRow.add(Box.createRigidArea(new Dimension(8, 0))); buttonRow.add(circleGuidanceSecondaryButton); circleGuidancePanel.add(circleGuidanceLabel); circleGuidancePanel.add(Box.createRigidArea(new Dimension(0, 8))); circleGuidancePanel.add(buttonRow); circleGuidancePanel.setVisible(false); } private void ensureCircleGuidanceDialog() { if (circleGuidancePanel == null) { return; } if (circleGuidanceDialog != null) { if (circleGuidancePanel.getParent() != circleGuidanceDialog.getContentPane()) { detachCircleGuidancePanel(); circleGuidanceDialog.getContentPane().removeAll(); circleGuidanceDialog.getContentPane().add(circleGuidancePanel, BorderLayout.CENTER); circleGuidanceDialog.pack(); } return; } Window owner = SwingUtilities.getWindowAncestor(this); circleGuidanceDialog = new JDialog(owner, "绘制提示", Dialog.ModalityType.MODELESS); circleGuidanceDialog.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); circleGuidanceDialog.setResizable(false); circleGuidanceDialog.setAlwaysOnTop(true); if (owner != null && circleDialogOwnerAdapter == null) { circleDialogOwnerAdapter = new ComponentAdapter() { @Override public void componentMoved(ComponentEvent e) { positionCircleGuidanceDialog(); } @Override public void componentResized(ComponentEvent e) { positionCircleGuidanceDialog(); } }; owner.addComponentListener(circleDialogOwnerAdapter); } detachCircleGuidancePanel(); circleGuidanceDialog.getContentPane().setLayout(new BorderLayout()); circleGuidanceDialog.getContentPane().add(circleGuidancePanel, BorderLayout.CENTER); circleGuidanceDialog.pack(); } private void detachCircleGuidancePanel() { if (circleGuidancePanel == null) { return; } Container parent = circleGuidancePanel.getParent(); if (parent != null) { parent.remove(circleGuidancePanel); parent.revalidate(); parent.repaint(); } } private void positionCircleGuidanceDialog() { if (circleGuidanceDialog == null) { return; } Window owner = SwingUtilities.getWindowAncestor(this); if (owner == null || !owner.isShowing()) { return; } try { Point ownerLocation = owner.getLocationOnScreen(); int x = ownerLocation.x + owner.getWidth() - circleGuidanceDialog.getWidth() - 30; int y = ownerLocation.y + owner.getHeight() - circleGuidanceDialog.getHeight() - 40; x = Math.max(ownerLocation.x, x); y = Math.max(ownerLocation.y, y); circleGuidanceDialog.setLocation(x, y); } catch (IllegalComponentStateException ex) { // Owner not yet displayable; skip positioning } } private JButton createGuidanceButton(String text, Color bg, Color fg, boolean filled) { JButton button = new JButton(text); button.setFont(new Font("微软雅黑", Font.BOLD, 12)); button.setForeground(fg); button.setBackground(bg); button.setOpaque(true); button.setFocusPainted(false); button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); button.setAlignmentX(Component.LEFT_ALIGNMENT); if (filled) { button.setBorder(BorderFactory.createEmptyBorder(6, 14, 6, 14)); } else { button.setBackground(Color.WHITE); button.setOpaque(true); button.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(THEME_COLOR, 1), BorderFactory.createEmptyBorder(5, 12, 5, 12))); } button.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { if (!button.isEnabled()) { return; } if (filled) { button.setBackground(THEME_HOVER_COLOR); } else { button.setBackground(new Color(245, 245, 245)); } } @Override public void mouseExited(MouseEvent e) { if (!button.isEnabled()) { return; } if (filled) { button.setBackground(bg); } else { button.setBackground(Color.WHITE); } } }); return button; } private void onCircleGuidancePrimary() { if (circleGuidanceStep >= 1 && circleGuidanceStep <= 3) { if (!recordCircleSamplePoint()) { return; } if (circleGuidanceStep == 3) { showCircleGuidanceStep(4); } else { showCircleGuidanceStep(circleGuidanceStep + 1); } } else if (circleGuidanceStep == 4) { handleCircleCompletion(); } } private void onCircleGuidanceSecondary() { if (circleGuidanceStep == 1 || circleGuidanceStep == 2 || circleGuidanceStep == 3) { handleCircleAbort(null); } else if (circleGuidanceStep == 4) { restartCircleCapture(); } } private void hideCircleGuidancePanel() { circleGuidanceStep = 0; if (circleGuidancePanel != null) { circleGuidancePanel.setVisible(false); } if (circleGuidanceDialog != null) { circleGuidanceDialog.setVisible(false); } if (!circleDialogMode) { rebuildFloatingButtonColumn(); } } private void handleCircleAbort(String message) { // Return the wizard to step 2 without committing any captured points hideEndDrawingButton(); addzhangaiwu.abortCircleDrawingAndReturn(message); } private void handleCircleCompletion() { hideCircleGuidancePanel(); clearCircleGuidanceArtifacts(); if (endDrawingCallback != null) { endDrawingCallback.run(); } else { addzhangaiwu.finishDrawingSession(); } } private void prepareCircleGuidanceState() { clearCircleGuidanceArtifacts(); circleBaseLatLon = resolveCircleBaseLatLon(); startCircleDataMonitor(); } private void clearCircleGuidanceArtifacts() { stopCircleDataMonitor(); circleCapturedPoints.clear(); circleBaseLatLon = null; lastCapturedCoordinate = null; if (mapRenderer != null) { mapRenderer.clearCircleCaptureOverlay(); mapRenderer.clearCircleSampleMarkers(); } } private void restartCircleCapture() { clearCircleGuidanceArtifacts(); synchronized (Coordinate.coordinates) { Coordinate.coordinates.clear(); } Coordinate.setStartSaveGngga(true); circleBaseLatLon = resolveCircleBaseLatLon(); showCircleGuidanceStep(1); startCircleDataMonitor(); } private boolean recordCircleSamplePoint() { Coordinate latest = getLatestCoordinate(); if (latest == null) { JOptionPane.showMessageDialog(this, "未获取到当前位置坐标,请稍后重试。", "提示", JOptionPane.WARNING_MESSAGE); return false; } double[] base = ensureCircleBaseLatLon(); if (base == null) { JOptionPane.showMessageDialog(this, "基准站坐标无效,请先在基准站管理中完成设置。", "提示", JOptionPane.WARNING_MESSAGE); return false; } double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection()); double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection()); if (!Double.isFinite(lat) || !Double.isFinite(lon)) { JOptionPane.showMessageDialog(this, "采集点坐标无效,请重新采集。", "提示", JOptionPane.WARNING_MESSAGE); return false; } double[] local = convertLatLonToLocal(lat, lon, base[0], base[1]); circleCapturedPoints.add(local); if (mapRenderer != null) { mapRenderer.updateCircleSampleMarkers(circleCapturedPoints); } lastCapturedCoordinate = latest; refreshCircleGuidanceButtonAvailability(); if (circleCapturedPoints.size() >= 3) { CircleSolution solution = fitCircleFromPoints(circleCapturedPoints); if (solution == null) { circleCapturedPoints.remove(circleCapturedPoints.size() - 1); JOptionPane.showMessageDialog(this, "无法根据当前三个点生成圆,请重新采集点。", "提示", JOptionPane.WARNING_MESSAGE); restartCircleCapture(); return false; } if (mapRenderer != null) { mapRenderer.showCircleCaptureOverlay(solution.centerX, solution.centerY, solution.radius, circleCapturedPoints); } } return true; } private void startCircleDataMonitor() { if (circleDataMonitor == null) { circleDataMonitor = new Timer(600, e -> refreshCircleGuidanceButtonAvailability()); circleDataMonitor.setRepeats(true); } if (!circleDataMonitor.isRunning()) { circleDataMonitor.start(); } refreshCircleGuidanceButtonAvailability(); } private void stopCircleDataMonitor() { if (circleDataMonitor != null) { circleDataMonitor.stop(); } } private void refreshCircleGuidanceButtonAvailability() { if (circleGuidancePrimaryButton == null) { return; } boolean shouldEnable = circleGuidanceStep >= 1 && circleGuidanceStep <= 3 ? isCircleDataAvailable() : true; applyCirclePrimaryButtonState(shouldEnable); } private boolean isCircleDataAvailable() { Coordinate latest = getLatestCoordinate(); if (latest == null) { return false; } return lastCapturedCoordinate == null || latest != lastCapturedCoordinate; } private void applyCirclePrimaryButtonState(boolean enabled) { if (circleGuidancePrimaryButton == null) { return; } if (circleGuidanceStep >= 4) { enabled = true; } circleGuidancePrimaryButton.setEnabled(enabled); if (enabled) { circleGuidancePrimaryButton.setBackground(THEME_COLOR); circleGuidancePrimaryButton.setForeground(Color.WHITE); circleGuidancePrimaryButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { circleGuidancePrimaryButton.setBackground(new Color(200, 200, 200)); circleGuidancePrimaryButton.setForeground(new Color(120, 120, 120)); circleGuidancePrimaryButton.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } private Coordinate getLatestCoordinate() { synchronized (Coordinate.coordinates) { int size = Coordinate.coordinates.size(); if (size == 0) { return null; } return Coordinate.coordinates.get(size - 1); } } private double[] ensureCircleBaseLatLon() { if (circleBaseLatLon == null) { circleBaseLatLon = resolveCircleBaseLatLon(); } return circleBaseLatLon; } private double[] resolveCircleBaseLatLon() { String coords = null; 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)) { return null; } } String[] parts = coords.split(","); if (parts.length < 4) { return null; } double baseLat = parseDMToDecimal(parts[0], parts[1]); double baseLon = parseDMToDecimal(parts[2], parts[3]); if (!Double.isFinite(baseLat) || !Double.isFinite(baseLon)) { return null; } return new double[]{baseLat, baseLon}; } private double parseDMToDecimal(String dmm, String direction) { if (dmm == null || dmm.trim().isEmpty()) { return Double.NaN; } try { String trimmed = dmm.trim(); int dotIndex = trimmed.indexOf('.'); if (dotIndex < 2) { return Double.NaN; } int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2)); double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2)); double decimal = degrees + minutes / 60.0; if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) { decimal = -decimal; } return decimal; } catch (NumberFormatException ex) { return Double.NaN; } } private double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) { double deltaLat = lat - baseLat; double deltaLon = lon - baseLon; double meanLatRad = Math.toRadians((baseLat + lat) / 2.0); double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad); double northMeters = deltaLat * METERS_PER_DEGREE_LAT; return new double[]{eastMeters, northMeters}; } private CircleSolution fitCircleFromPoints(List points) { if (points == null || points.size() < 3) { return null; } CircleSolution best = null; double bestScore = 0.0; int n = points.size(); for (int i = 0; i < n - 2; i++) { double[] p1 = points.get(i); for (int j = i + 1; j < n - 1; j++) { double[] p2 = points.get(j); for (int k = j + 1; k < n; k++) { double[] p3 = points.get(k); CircleSolution candidate = circleFromThreePoints(p1, p2, p3); if (candidate == null || candidate.radius <= 0) { continue; } double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3))); if (minEdge > bestScore) { bestScore = minEdge; best = candidate; } } } } return best; } private CircleSolution circleFromThreePoints(double[] p1, double[] p2, double[] p3) { if (p1 == null || p2 == null || p3 == null) { return null; } double x1 = p1[0]; double y1 = p1[1]; double x2 = p2[0]; double y2 = p2[1]; double x3 = p3[0]; double y3 = p3[1]; double a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2; double d = 2.0 * a; if (Math.abs(d) < 1e-6) { return null; } double x1Sq = x1 * x1 + y1 * y1; double x2Sq = x2 * x2 + y2 * y2; double x3Sq = x3 * x3 + y3 * y3; double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / d; double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / d; double radius = Math.hypot(centerX - x1, centerY - y1); if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) { return null; } if (radius < 0.05) { return null; } return new CircleSolution(centerX, centerY, radius); } private double distance(double[] a, double[] b) { if (a == null || b == null) { return 0.0; } double dx = a[0] - b[0]; double dy = a[1] - b[1]; return Math.hypot(dx, dy); } private static final class CircleSolution { final double centerX; final double centerY; final double radius; CircleSolution(double centerX, double centerY, double radius) { this.centerX = centerX; this.centerY = centerY; this.radius = radius; } } public void hideEndDrawingButton() { hideCircleGuidancePanel(); clearCircleGuidanceArtifacts(); hideFloatingDrawingControls(); circleDialogMode = false; 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) { return false; } // 允许没有路径的预览(例如障碍物预览),只要有返回回调即可 if (!isMeaningfulValue(plannedPath) && returnAction == null) { 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); // 只有在有路径时才设置路径 if (isMeaningfulValue(plannedPath)) { mapRenderer.setCurrentPlannedPath(plannedPath); } else { mapRenderer.setCurrentPlannedPath(null); } mapRenderer.clearHandheldBoundaryPreview(); mapRenderer.setBoundaryPointSizeScale(1.0d); mapRenderer.setBoundaryPointsVisible(isMeaningfulValue(boundary)); // 启用障碍物边界点显示 mapRenderer.setObstaclePointsVisible(isMeaningfulValue(obstacles)); 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(); } /** * 获取地图渲染器实例 */ public MapRenderer getMapRenderer() { return mapRenderer; } public void updateCurrentAreaName(String areaName) { if (areaNameLabel == null) { return; } if (areaName == null || areaName.trim().isEmpty()) { areaNameLabel.setText("未选择地块"); } else { areaNameLabel.setText(areaName); } } /** * 重置地图视图 */ public void resetMapView() { if (mapRenderer != null) { mapRenderer.resetView(); } } 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 all = Dikuai.getAllDikuai(); if (all.isEmpty()) { Dikuaiguanli.setCurrentWorkLand(null, null); } else if (all.size() == 1) { Dikuai only = all.values().iterator().next(); if (only != null) { Dikuaiguanli.setCurrentWorkLand(only.getLandNumber(), only.getLandName()); } } } private void refreshMapForSelectedArea() { if (mapRenderer == null) { return; } String currentLandNumber = Dikuaiguanli.getCurrentWorkLandNumber(); if (isMeaningfulValue(currentLandNumber)) { Dikuai current = Dikuai.getDikuai(currentLandNumber); String landName = current != null ? current.getLandName() : null; Dikuaiguanli.setCurrentWorkLand(currentLandNumber, landName); return; } String labelName = areaNameLabel != null ? areaNameLabel.getText() : null; if (!isMeaningfulAreaName(labelName)) { Dikuaiguanli.setCurrentWorkLand(null, null); return; } Map all = Dikuai.getAllDikuai(); for (Dikuai dikuai : all.values()) { if (dikuai == null) { continue; } String candidateName = dikuai.getLandName(); if (candidateName != null && candidateName.trim().equals(labelName.trim())) { Dikuaiguanli.setCurrentWorkLand(dikuai.getLandNumber(), candidateName); return; } } Dikuaiguanli.setCurrentWorkLand(null, null); } private boolean isMeaningfulValue(String value) { if (value == null) { return false; } String trimmed = value.trim(); return !trimmed.isEmpty() && !"-1".equals(trimmed); } private boolean isMeaningfulAreaName(String value) { if (value == null) { return false; } String trimmed = value.trim(); if (trimmed.isEmpty()) { return false; } return !"未选择地块".equals(trimmed); } // 测试方法 public static void main(String[] args) { JFrame frame = new JFrame("AutoMow - 首页"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(400, 800); frame.setLocationRelativeTo(null); Shouye shouye = new Shouye(); frame.add(shouye); frame.setVisible(true); UDPServer.startAsync();//启动数据接收线程 } }