| | |
| | | import javax.swing.Timer; |
| | | |
| | | import baseStation.BaseStation; |
| | | import set.Setsys; |
| | | import baseStation.BaseStationDialog; |
| | | |
| | | import java.awt.*; |
| | | import java.awt.event.*; |
| | | |
| | | import chuankou.dellmessage; |
| | | import dikuai.Dikuai; |
| | | import dikuai.Dikuaiguanli; |
| | | import dikuai.addzhangaiwu; |
| | | import gecaoji.Device; |
| | | import 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 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 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<String> 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<double[]> circleCapturedPoints = new ArrayList<>(); |
| | | private double[] circleBaseLatLon; |
| | | private Timer circleDataMonitor; |
| | | private Coordinate lastCapturedCoordinate; |
| | | private boolean handheldCaptureActive; |
| | | private int handheldCapturedPoints; |
| | | private final List<Point2D.Double> handheldTemporaryPoints = new ArrayList<>(); |
| | | private final List<Point2D.Double> mowerTemporaryPoints = new ArrayList<>(); |
| | | private enum BoundaryCaptureMode { NONE, HANDHELD, MOWER } |
| | | private BoundaryCaptureMode activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | private boolean mowerBoundaryCaptureActive; |
| | | private Timer mowerBoundaryMonitor; |
| | | private Coordinate lastMowerCoordinate; |
| | | private double[] mowerBaseLatLon; |
| | | private boolean startButtonShowingPause = true; |
| | | private boolean stopButtonActive = false; |
| | | private boolean bluetoothConnected = false; |
| | | private Timer mowerSpeedRefreshTimer; |
| | | private boolean drawingControlModeActive; |
| | | private boolean storedStartButtonShowingPause; |
| | | private boolean storedStopButtonActive; |
| | | private String storedStatusBeforeDrawing; |
| | | private boolean handheldCaptureInlineUiActive; |
| | | private Timer handheldCaptureStatusTimer; |
| | | private String handheldCaptureStoredStatusText; |
| | | private Color handheldStartButtonOriginalBackground; |
| | | private Color handheldStartButtonOriginalForeground; |
| | | private Color handheldStopButtonOriginalBackground; |
| | | private Color handheldStopButtonOriginalForeground; |
| | | |
| | | public Shouye() { |
| | | instance = this; |
| | | baseStation = new BaseStation(); |
| | | baseStation.load(); |
| | | dellmessage.registerLineListener(serialLineListener); |
| | | initializeUI(); |
| | | setupEventHandlers(); |
| | | scheduleIdentifierCheck(); |
| | | } |
| | | |
| | | public static Shouye getInstance() { |
| | | 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(); |
| | | } |
| | | |
| | | 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(); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 保存当前地图缩放比例和视图中心坐标到配置文件 |
| | | */ |
| | | 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 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); |
| | | } |
| | | |
| | | 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 gecaojiIcon = 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像素 |
| | | gecaojiIcon = loadScaledIcon("image/gecaoji.png", GECAOJI_ICON_SIZE, GECAOJI_ICON_SIZE); |
| | | // 启用工具提示 |
| | | setToolTipText(""); |
| | | } |
| | | |
| | | /** |
| | | * 检查鼠标位置是否在割草机图标区域内 |
| | | */ |
| | | 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 "以割草机为中心"; |
| | | } |
| | | return super.getToolTipText(event); |
| | | } |
| | | |
| | | @Override |
| | | protected void paintComponent(Graphics g) { |
| | | super.paintComponent(g); |
| | | // 委托给MapRenderer进行绘制 |
| | | if (mapRenderer != null) { |
| | | mapRenderer.renderMap(g); |
| | | } |
| | | // 在地图左上角绘制割草机图标 |
| | | // 水平方向与速度指示器对齐(x=37) |
| | | // 垂直方向与卫星状态图标对齐(y=10,速度指示器面板顶部边距10像素,使图标中心对齐) |
| | | if (gecaojiIcon != null) { |
| | | g.drawImage(gecaojiIcon.getImage(), GECAOJI_ICON_X, GECAOJI_ICON_Y, null); |
| | | } |
| | | } |
| | | }; |
| | | 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) { |
| | | // 点击了割草机图标,将地图视图中心移动到割草机位置 |
| | | if (mapRenderer != null) { |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | |
| | | 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(); |
| | | bluetoothConnected = Bluelink.isConnected(); |
| | | ImageIcon initialIcon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | if (initialIcon != null) { |
| | | button.setIcon(initialIcon); |
| | | } else { |
| | | button.setText(bluetoothConnected ? "已连" : "蓝牙"); |
| | | } |
| | | return button; |
| | | } |
| | | |
| | | private JButton createFunctionButton(String text, String icon) { |
| | | JButton button = new JButton("<html><center>" + icon + "<br>" + text + "</center></html>"); |
| | | button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | 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("<html><center>" + icon + "<br>" + text + "</center></html>"); |
| | | 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<Point2D.Double> 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("<html><div style='text-align: center;'>" + message + "</div></html>"); |
| | | 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 = "<html><center>采集点" + nextIndex + "<br>确定</center></html>"; |
| | | startBtn.setText(prompt); |
| | | startBtn.setEnabled(canConfirm); |
| | | if (canConfirm) { |
| | | if (handheldStartButtonOriginalBackground != null) { |
| | | startBtn.setBackground(handheldStartButtonOriginalBackground); |
| | | } |
| | | if (handheldStartButtonOriginalForeground != null) { |
| | | startBtn.setForeground(handheldStartButtonOriginalForeground); |
| | | } |
| | | startBtn.setToolTipText(null); |
| | | } else { |
| | | startBtn.setBackground(new Color(200, 200, 200)); |
| | | startBtn.setForeground(new Color(130, 130, 130)); |
| | | startBtn.setToolTipText(resolveHandheldConfirmTooltip(hasFix, hasValid, duplicate)); |
| | | } |
| | | } |
| | | |
| | | if (stopBtn != null) { |
| | | boolean canFinish = handheldCapturedPoints >= 3; |
| | | stopBtn.setText("结束"); |
| | | stopBtn.setEnabled(canFinish); |
| | | if (canFinish) { |
| | | if (handheldStopButtonOriginalBackground != null) { |
| | | stopBtn.setBackground(handheldStopButtonOriginalBackground); |
| | | } |
| | | if (handheldStopButtonOriginalForeground != null) { |
| | | stopBtn.setForeground(handheldStopButtonOriginalForeground); |
| | | } |
| | | stopBtn.setToolTipText("结束采集并返回新增地块"); |
| | | } else { |
| | | stopBtn.setBackground(new Color(220, 220, 220)); |
| | | stopBtn.setForeground(new Color(130, 130, 130)); |
| | | stopBtn.setToolTipText("至少采集三个点才能结束"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private String resolveHandheldConfirmTooltip(boolean hasFix, boolean hasValidPosition, boolean duplicate) { |
| | | if (!hasFix) { |
| | | return "当前定位质量不足,无法采集"; |
| | | } |
| | | if (!hasValidPosition) { |
| | | return "当前定位数据无效,请稍后再试"; |
| | | } |
| | | if (duplicate) { |
| | | return "当前坐标已采集,请移动到新的位置"; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private boolean hasHighPrecisionFix() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return false; |
| | | } |
| | | String status = device.getPositioningStatus(); |
| | | return status != null && "4".equals(status.trim()); |
| | | } |
| | | |
| | | private boolean canConfirmHandheldPoint() { |
| | | return handheldCaptureActive |
| | | && hasHighPrecisionFix() |
| | | && hasValidRealtimeHandheldPosition() |
| | | && !isCurrentHandheldPointDuplicate(); |
| | | } |
| | | |
| | | private void enterDrawingControlMode() { |
| | | if (drawingControlModeActive) { |
| | | return; |
| | | } |
| | | storedStartButtonShowingPause = startButtonShowingPause; |
| | | storedStopButtonActive = stopButtonActive; |
| | | storedStatusBeforeDrawing = statusLabel != null ? statusLabel.getText() : null; |
| | | drawingControlModeActive = true; |
| | | applyDrawingPauseState(false, false); |
| | | updateDrawingControlButtonLabels(); |
| | | } |
| | | |
| | | private void exitDrawingControlMode() { |
| | | if (!drawingControlModeActive) { |
| | | return; |
| | | } |
| | | drawingControlModeActive = false; |
| | | applyDrawingPauseState(false, false); |
| | | drawingPaused = false; |
| | | stopButtonActive = storedStopButtonActive; |
| | | startButtonShowingPause = storedStartButtonShowingPause; |
| | | if (startBtn != null) { |
| | | updateStartButtonAppearance(); |
| | | } |
| | | if (stopBtn != null) { |
| | | stopBtn.setText("结束"); |
| | | updateStopButtonIcon(); |
| | | } |
| | | if (statusLabel != null) { |
| | | statusLabel.setText(storedStatusBeforeDrawing != null ? storedStatusBeforeDrawing : "待机"); |
| | | } |
| | | storedStatusBeforeDrawing = null; |
| | | } |
| | | |
| | | private void updateDrawingControlButtonLabels() { |
| | | if (!drawingControlModeActive) { |
| | | return; |
| | | } |
| | | configureButtonForDrawingMode(startBtn); |
| | | configureButtonForDrawingMode(stopBtn); |
| | | if (startBtn != null) { |
| | | startBtn.setText(drawingPaused ? "开始绘制" : "暂停绘制"); |
| | | } |
| | | if (stopBtn != null) { |
| | | stopBtn.setText("结束绘制"); |
| | | } |
| | | } |
| | | |
| | | private void configureButtonForDrawingMode(JButton button) { |
| | | if (button == null) { |
| | | return; |
| | | } |
| | | button.setIcon(null); |
| | | button.setIconTextGap(0); |
| | | button.setHorizontalAlignment(SwingConstants.CENTER); |
| | | button.setHorizontalTextPosition(SwingConstants.CENTER); |
| | | } |
| | | |
| | | private void updateStartButtonAppearance() { |
| | | if (startBtn == null) { |
| | | return; |
| | | } |
| | | 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(); |
| | | bluetoothConnected = Bluelink.isConnected(); |
| | | ImageIcon icon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | if (icon != null) { |
| | | bluetoothBtn.setIcon(icon); |
| | | bluetoothBtn.setText(null); |
| | | } else { |
| | | bluetoothBtn.setText(bluetoothConnected ? "已连" : "蓝牙"); |
| | | } |
| | | } |
| | | |
| | | 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<Point2D.Double> snapshot; |
| | | synchronized (mowerTemporaryPoints) { |
| | | for (Point2D.Double existing : mowerTemporaryPoints) { |
| | | if (existing != null && arePointsClose(existing, candidate)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | } |
| | | mowerTemporaryPoints.add(candidate); |
| | | snapshot = new ArrayList<>(mowerTemporaryPoints.size() + 1); |
| | | for (Point2D.Double point : mowerTemporaryPoints) { |
| | | if (point != null) { |
| | | snapshot.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | ensureClosed(snapshot); |
| | | AddDikuai.recordTemporaryBoundaryPoints(snapshot); |
| | | if (mapRenderer != null) { |
| | | mapRenderer.addHandheldBoundaryPoint(candidate.x, candidate.y); |
| | | } |
| | | lastMowerCoordinate = latest; |
| | | } |
| | | |
| | | private void stopMowerBoundaryCapture() { |
| | | stopMowerBoundaryMonitor(); |
| | | mowerBoundaryCaptureActive = false; |
| | | lastMowerCoordinate = null; |
| | | mowerBaseLatLon = null; |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | } |
| | | Coordinate.setStartSaveGngga(false); |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | setHandheldMowerIconActive(false); |
| | | } |
| | | |
| | | private void discardLatestCoordinate(Coordinate coordinate) { |
| | | if (coordinate == null) { |
| | | return; |
| | | } |
| | | synchronized (Coordinate.coordinates) { |
| | | int size = Coordinate.coordinates.size(); |
| | | if (size == 0) { |
| | | return; |
| | | } |
| | | int lastIndex = size - 1; |
| | | if (Coordinate.coordinates.get(lastIndex) == coordinate) { |
| | | Coordinate.coordinates.remove(lastIndex); |
| | | } else { |
| | | Coordinate.coordinates.remove(coordinate); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void ensureClosed(List<Point2D.Double> points) { |
| | | if (points == null || points.size() < 3) { |
| | | return; |
| | | } |
| | | Point2D.Double first = points.get(0); |
| | | Point2D.Double last = points.get(points.size() - 1); |
| | | if (first == null || last == null) { |
| | | return; |
| | | } |
| | | if (!arePointsClose(first, last)) { |
| | | points.add(new Point2D.Double(first.x, first.y)); |
| | | } |
| | | } |
| | | |
| | | int captureHandheldBoundaryPoint() { |
| | | if (!handheldCaptureActive) { |
| | | return -1; |
| | | } |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | JOptionPane.showMessageDialog(this, "未检测到采集设备,请检查连接。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | String[] latParts = splitCoordinateComponents(device.getRealtimeLatitude(), true); |
| | | String[] lonParts = splitCoordinateComponents(device.getRealtimeLongitude(), false); |
| | | if (latParts == null || lonParts == null) { |
| | | JOptionPane.showMessageDialog(this, "当前定位无效,请在定位稳定后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | JOptionPane.showMessageDialog(this, "当前定位数据无效,请稍后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | if (isDuplicateHandheldPoint(x, y)) { |
| | | JOptionPane.showMessageDialog(this, "当前坐标已采集,请移动到新的位置后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | double altitude = parseAltitudeValue(device.getRealtimeAltitude()); |
| | | Coordinate coordinate = new Coordinate(latParts[0], latParts[1], lonParts[0], lonParts[1], altitude); |
| | | synchronized (Coordinate.coordinates) { |
| | | Coordinate.coordinates.add(coordinate); |
| | | } |
| | | |
| | | if (mapRenderer != null) { |
| | | mapRenderer.addHandheldBoundaryPoint(x, y); |
| | | } |
| | | |
| | | List<Point2D.Double> snapshot; |
| | | synchronized (handheldTemporaryPoints) { |
| | | handheldTemporaryPoints.add(new Point2D.Double(x, y)); |
| | | snapshot = new ArrayList<>(handheldTemporaryPoints); |
| | | } |
| | | AddDikuai.recordTemporaryBoundaryPoints(snapshot); |
| | | |
| | | handheldCapturedPoints++; |
| | | return handheldCapturedPoints; |
| | | } |
| | | |
| | | boolean finishHandheldBoundaryCapture() { |
| | | if (!handheldCaptureActive) { |
| | | return false; |
| | | } |
| | | if (handheldCapturedPoints < 3) { |
| | | JOptionPane.showMessageDialog(this, "至少采集三个点才能生成边界。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | List<Point2D.Double> closedSnapshot = createClosedHandheldPointSnapshot(); |
| | | handheldCaptureActive = false; |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | Coordinate.setStartSaveGngga(false); |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | } |
| | | |
| | | AddDikuai.recordTemporaryBoundaryPoints(closedSnapshot); |
| | | |
| | | exitHandheldCaptureInlineUi(); |
| | | |
| | | SwingUtilities.invokeLater(AddDikuai::finishDrawingSession); |
| | | return true; |
| | | } |
| | | |
| | | int getHandheldCapturedPointCount() { |
| | | return handheldCapturedPoints; |
| | | } |
| | | |
| | | public List<Point2D.Double> getHandheldTemporaryPointsSnapshot() { |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | return createClosedMowerPointSnapshot(); |
| | | } |
| | | if (!handheldCaptureActive) { |
| | | return createClosedHandheldPointSnapshot(); |
| | | } |
| | | synchronized (handheldTemporaryPoints) { |
| | | return new ArrayList<>(handheldTemporaryPoints); |
| | | } |
| | | } |
| | | |
| | | public boolean isCurrentHandheldPointDuplicate() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return false; |
| | | } |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | return false; |
| | | } |
| | | return isDuplicateHandheldPoint(x, y); |
| | | } |
| | | |
| | | public boolean hasValidRealtimeHandheldPosition() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return false; |
| | | } |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | return Double.isFinite(x) && Double.isFinite(y); |
| | | } |
| | | |
| | | private boolean isDuplicateHandheldPoint(double x, double y) { |
| | | Point2D.Double candidate = new Point2D.Double(x, y); |
| | | synchronized (handheldTemporaryPoints) { |
| | | for (Point2D.Double existing : handheldTemporaryPoints) { |
| | | if (existing == null) { |
| | | continue; |
| | | } |
| | | if (arePointsClose(existing, candidate)) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean arePointsClose(Point2D.Double a, Point2D.Double b) { |
| | | if (a == null || b == null) { |
| | | return false; |
| | | } |
| | | double dx = a.x - b.x; |
| | | double dy = a.y - b.y; |
| | | return Math.hypot(dx, dy) < HANDHELD_DUPLICATE_THRESHOLD_METERS; |
| | | } |
| | | |
| | | private List<Point2D.Double> createClosedHandheldPointSnapshot() { |
| | | List<Point2D.Double> copy = new ArrayList<>(); |
| | | synchronized (handheldTemporaryPoints) { |
| | | for (Point2D.Double point : handheldTemporaryPoints) { |
| | | if (point != null) { |
| | | copy.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | ensureClosed(copy); |
| | | return copy; |
| | | } |
| | | |
| | | private List<Point2D.Double> createClosedMowerPointSnapshot() { |
| | | List<Point2D.Double> copy = new ArrayList<>(); |
| | | synchronized (mowerTemporaryPoints) { |
| | | for (Point2D.Double point : mowerTemporaryPoints) { |
| | | if (point != null) { |
| | | copy.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | ensureClosed(copy); |
| | | return copy; |
| | | } |
| | | |
| | | private String[] splitCoordinateComponents(String combined, boolean latitude) { |
| | | if (combined == null) { |
| | | return null; |
| | | } |
| | | String trimmed = combined.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | |
| | | String valuePart; |
| | | String directionPart = null; |
| | | String[] parts = trimmed.split(","); |
| | | if (parts.length >= 2) { |
| | | valuePart = parts[0].trim(); |
| | | directionPart = parts[1].trim(); |
| | | } else { |
| | | valuePart = trimmed; |
| | | } |
| | | |
| | | if (valuePart.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | if (directionPart == null || directionPart.isEmpty()) { |
| | | char lastChar = valuePart.charAt(valuePart.length() - 1); |
| | | if (Character.isLetter(lastChar)) { |
| | | directionPart = String.valueOf(lastChar); |
| | | valuePart = valuePart.substring(0, valuePart.length() - 1).trim(); |
| | | } |
| | | } |
| | | |
| | | if (valuePart.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | directionPart = normalizeHemisphere(directionPart, latitude); |
| | | return new String[]{valuePart, directionPart}; |
| | | } |
| | | |
| | | private String normalizeHemisphere(String direction, boolean latitude) { |
| | | if (direction == null || direction.trim().isEmpty()) { |
| | | return latitude ? "N" : "E"; |
| | | } |
| | | String normalized = direction.trim().toUpperCase(Locale.ROOT); |
| | | if (latitude) { |
| | | if (!"N".equals(normalized) && !"S".equals(normalized)) { |
| | | return "N"; |
| | | } |
| | | } else { |
| | | if (!"E".equals(normalized) && !"W".equals(normalized)) { |
| | | return "E"; |
| | | } |
| | | } |
| | | return normalized; |
| | | } |
| | | |
| | | private double parseMetersValue(String raw) { |
| | | if (raw == null) { |
| | | return Double.NaN; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return Double.NaN; |
| | | } |
| | | try { |
| | | return Double.parseDouble(trimmed); |
| | | } catch (NumberFormatException ex) { |
| | | return Double.NaN; |
| | | } |
| | | } |
| | | |
| | | private double parseAltitudeValue(String raw) { |
| | | if (raw == null) { |
| | | return 0.0; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | return Double.parseDouble(trimmed); |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | private boolean beginMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return false; |
| | | } |
| | | String landNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | if (!isMeaningfulValue(landNumber)) { |
| | | JOptionPane.showMessageDialog(this, "请先选择地块后再开始作业", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | double widthMeters = resolveMowerWidthMeters(landNumber); |
| | | if (widthMeters <= 0) { |
| | | JOptionPane.showMessageDialog(this, "未配置割草宽度,将无法计算作业面积", "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | |
| | | mapRenderer.startRealtimeTrackRecording(landNumber, widthMeters); |
| | | refreshMowerSpeedLabel(); |
| | | return true; |
| | | } |
| | | |
| | | private void pauseMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | mapRenderer.pauseRealtimeTrackRecording(); |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | private void stopMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | mapRenderer.stopRealtimeTrackRecording(); |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | private double resolveMowerWidthMeters(String landNumber) { |
| | | double width = 0.0; |
| | | if (isMeaningfulValue(landNumber)) { |
| | | Dikuai current = Dikuai.getDikuai(landNumber); |
| | | if (current != null) { |
| | | width = parseMowerWidthMeters(current.getMowingWidth()); |
| | | } |
| | | } |
| | | if (width > 0) { |
| | | return width; |
| | | } |
| | | return parseMowerWidthFromDevice(); |
| | | } |
| | | |
| | | private double parseMowerWidthFromDevice() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return 0.0; |
| | | } |
| | | return parseMowerWidthMeters(device.getMowingWidth()); |
| | | } |
| | | |
| | | private double parseMowerWidthMeters(String raw) { |
| | | if (raw == null) { |
| | | return 0.0; |
| | | } |
| | | String sanitized = raw.trim().toLowerCase(Locale.ROOT); |
| | | if (sanitized.isEmpty() || "-1".equals(sanitized)) { |
| | | return 0.0; |
| | | } |
| | | sanitized = sanitized.replace("厘米", "cm"); |
| | | sanitized = sanitized.replace("公分", "cm"); |
| | | sanitized = sanitized.replace("米", "m"); |
| | | sanitized = sanitized.replace("cm", ""); |
| | | sanitized = sanitized.replace("m", ""); |
| | | sanitized = sanitized.trim(); |
| | | if (sanitized.isEmpty()) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | double value = Double.parseDouble(sanitized); |
| | | if (value <= 0) { |
| | | return 0.0; |
| | | } |
| | | if (value > 10) { |
| | | return value / 100.0; |
| | | } |
| | | return value; |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | private void applyStatusLabelColor(String statusText) { |
| | | if (statusLabel == null) { |
| | | return; |
| | | } |
| | | if ("作业中".equals(statusText) || "绘制中".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<double[]> points) { |
| | | if (points == null || points.size() < 3) { |
| | | return null; |
| | | } |
| | | CircleSolution best = null; |
| | | double bestScore = 0.0; |
| | | int n = points.size(); |
| | | for (int i = 0; i < n - 2; i++) { |
| | | double[] p1 = points.get(i); |
| | | for (int j = i + 1; j < n - 1; j++) { |
| | | double[] p2 = points.get(j); |
| | | for (int k = j + 1; k < n; k++) { |
| | | double[] p3 = points.get(k); |
| | | CircleSolution candidate = circleFromThreePoints(p1, p2, p3); |
| | | if (candidate == null || candidate.radius <= 0) { |
| | | continue; |
| | | } |
| | | double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3))); |
| | | if (minEdge > bestScore) { |
| | | bestScore = minEdge; |
| | | best = candidate; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return best; |
| | | } |
| | | |
| | | private CircleSolution circleFromThreePoints(double[] p1, double[] p2, double[] p3) { |
| | | if (p1 == null || p2 == null || p3 == null) { |
| | | return null; |
| | | } |
| | | double x1 = p1[0]; |
| | | double y1 = p1[1]; |
| | | double x2 = p2[0]; |
| | | double y2 = p2[1]; |
| | | double x3 = p3[0]; |
| | | double y3 = p3[1]; |
| | | |
| | | double a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2; |
| | | double d = 2.0 * a; |
| | | if (Math.abs(d) < 1e-6) { |
| | | return null; |
| | | } |
| | | |
| | | double x1Sq = x1 * x1 + y1 * y1; |
| | | double x2Sq = x2 * x2 + y2 * y2; |
| | | double x3Sq = x3 * x3 + y3 * y3; |
| | | |
| | | double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / d; |
| | | double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / d; |
| | | double radius = Math.hypot(centerX - x1, centerY - y1); |
| | | if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) { |
| | | return null; |
| | | } |
| | | if (radius < 0.05) { |
| | | return null; |
| | | } |
| | | return new CircleSolution(centerX, centerY, radius); |
| | | } |
| | | |
| | | private double distance(double[] a, double[] b) { |
| | | if (a == null || b == null) { |
| | | return 0.0; |
| | | } |
| | | double dx = a[0] - b[0]; |
| | | double dy = a[1] - b[1]; |
| | | return Math.hypot(dx, dy); |
| | | } |
| | | |
| | | private static final class CircleSolution { |
| | | final double centerX; |
| | | final double centerY; |
| | | final double radius; |
| | | |
| | | CircleSolution(double centerX, double centerY, double radius) { |
| | | this.centerX = centerX; |
| | | this.centerY = centerY; |
| | | this.radius = radius; |
| | | } |
| | | } |
| | | |
| | | public void hideEndDrawingButton() { |
| | | hideCircleGuidancePanel(); |
| | | clearCircleGuidanceArtifacts(); |
| | | hideFloatingDrawingControls(); |
| | | circleDialogMode = false; |
| | | exitHandheldCaptureInlineUi(); |
| | | handheldCaptureActive = false; |
| | | exitDrawingControlMode(); |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | stopMowerBoundaryCapture(); |
| | | } else if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD && !handheldCaptureActive) { |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | endDrawingCallback = null; |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | setHandheldMowerIconActive(false); |
| | | } |
| | | |
| | | private void showPathPreviewReturnControls() { |
| | | ensureFloatingButtonInfrastructure(); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(false); |
| | | } |
| | | if (endDrawingButton != null) { |
| | | endDrawingButton.setVisible(false); |
| | | } |
| | | if (pathPreviewReturnButton == null) { |
| | | pathPreviewReturnButton = createFloatingTextButton("返回"); |
| | | pathPreviewReturnButton.setToolTipText("返回新增地块步骤"); |
| | | pathPreviewReturnButton.addActionListener(e -> handlePathPreviewReturn()); |
| | | } |
| | | pathPreviewReturnButton.setVisible(true); |
| | | if (floatingButtonPanel != null) { |
| | | floatingButtonPanel.setVisible(true); |
| | | if (floatingButtonPanel.getParent() != visualizationPanel) { |
| | | visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH); |
| | | } |
| | | } |
| | | rebuildFloatingButtonColumn(); |
| | | } |
| | | |
| | | private void hidePathPreviewReturnControls() { |
| | | if (pathPreviewReturnButton != null) { |
| | | pathPreviewReturnButton.setVisible(false); |
| | | } |
| | | rebuildFloatingButtonColumn(); |
| | | if (floatingButtonPanel != null && floatingButtonColumn != null |
| | | && floatingButtonColumn.getComponentCount() == 0) { |
| | | floatingButtonPanel.setVisible(false); |
| | | } |
| | | } |
| | | |
| | | private void handlePathPreviewReturn() { |
| | | Runnable callback = pathPreviewReturnAction; |
| | | exitMowingPathPreview(); |
| | | if (callback != null) { |
| | | callback.run(); |
| | | } |
| | | } |
| | | |
| | | public boolean startMowingPathPreview(String landNumber, |
| | | String landName, |
| | | String boundary, |
| | | String obstacles, |
| | | String plannedPath, |
| | | Runnable returnAction) { |
| | | if (mapRenderer == null || !isMeaningfulValue(plannedPath)) { |
| | | return false; |
| | | } |
| | | |
| | | if (pathPreviewActive) { |
| | | exitMowingPathPreview(); |
| | | } |
| | | |
| | | exitDrawingControlMode(); |
| | | hideCircleGuidancePanel(); |
| | | clearCircleGuidanceArtifacts(); |
| | | |
| | | pathPreviewReturnAction = returnAction; |
| | | pathPreviewActive = true; |
| | | mapRenderer.setPathPreviewSizingEnabled(true); |
| | | |
| | | previewRestoreLandNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | previewRestoreLandName = null; |
| | | if (isMeaningfulValue(previewRestoreLandNumber)) { |
| | | Dikuai existing = Dikuai.getDikuai(previewRestoreLandNumber); |
| | | if (existing != null) { |
| | | previewRestoreLandName = existing.getLandName(); |
| | | } |
| | | } |
| | | |
| | | mapRenderer.setCurrentBoundary(boundary, landNumber, landName); |
| | | mapRenderer.setCurrentObstacles(obstacles, landNumber); |
| | | mapRenderer.setCurrentPlannedPath(plannedPath); |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | mapRenderer.setBoundaryPointSizeScale(1.0d); |
| | | mapRenderer.setBoundaryPointsVisible(isMeaningfulValue(boundary)); |
| | | |
| | | String displayName = isMeaningfulValue(landName) ? landName : landNumber; |
| | | updateCurrentAreaName(displayName); |
| | | |
| | | showPathPreviewReturnControls(); |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | return true; |
| | | } |
| | | |
| | | public void exitMowingPathPreview() { |
| | | if (!pathPreviewActive) { |
| | | return; |
| | | } |
| | | pathPreviewActive = false; |
| | | if (mapRenderer != null) { |
| | | mapRenderer.setPathPreviewSizingEnabled(false); |
| | | } |
| | | hidePathPreviewReturnControls(); |
| | | |
| | | String restoreNumber = previewRestoreLandNumber; |
| | | String restoreName = previewRestoreLandName; |
| | | previewRestoreLandNumber = null; |
| | | previewRestoreLandName = null; |
| | | pathPreviewReturnAction = null; |
| | | |
| | | if (restoreNumber != null) { |
| | | Dikuaiguanli.setCurrentWorkLand(restoreNumber, restoreName); |
| | | } else if (mapRenderer != null) { |
| | | mapRenderer.setCurrentBoundary(null, null, null); |
| | | mapRenderer.setCurrentObstacles((String) null, null); |
| | | mapRenderer.setCurrentPlannedPath(null); |
| | | mapRenderer.setBoundaryPointsVisible(false); |
| | | mapRenderer.setBoundaryPointSizeScale(1.0d); |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | mapRenderer.resetView(); |
| | | updateCurrentAreaName(null); |
| | | } |
| | | |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | /** |
| | | * 获取地图渲染器实例 |
| | | */ |
| | | 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<String, Dikuai> 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<String, Dikuai> 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); |
| | | } |
| | | |
| | | |
| | | // 主题颜色 |
| | | 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 mowerSpeedValueLabel; |
| | | private JLabel mowerSpeedUnitLabel; |
| | | private JLabel mowingProgressLabel; |
| | | |
| | | // 导航按钮 |
| | | private JButton homeNavBtn; |
| | | private JButton areasNavBtn; |
| | | private JButton settingsNavBtn; |
| | | |
| | | // 状态显示 |
| | | private JLabel statusLabel; |
| | | private JLabel areaNameLabel; |
| | | |
| | | // 当前选中的导航按钮 |
| | | private JButton currentNavButton; |
| | | |
| | | // 对话框引用 |
| | | private LegendDialog legendDialog; |
| | | private RemoteControlDialog remoteDialog; |
| | | private AreaSelectionDialog areaDialog; |
| | | private BaseStationDialog baseStationDialog; |
| | | private Sets settingsDialog; |
| | | private BaseStation baseStation; |
| | | |
| | | // 地图渲染器 |
| | | private MapRenderer mapRenderer; |
| | | private static final int FLOAT_ICON_SIZE = 32; |
| | | private JButton endDrawingButton; |
| | | private JButton drawingPauseButton; |
| | | private JPanel floatingButtonPanel; |
| | | private JPanel floatingButtonColumn; |
| | | private Runnable endDrawingCallback; |
| | | 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<double[]> circleCapturedPoints = new ArrayList<>(); |
| | | private double[] circleBaseLatLon; |
| | | private Timer circleDataMonitor; |
| | | private Coordinate lastCapturedCoordinate; |
| | | private HandheldBoundaryCaptureDialog handheldCaptureDialog; |
| | | private boolean handheldCaptureActive; |
| | | private int handheldCapturedPoints; |
| | | private final List<Point2D.Double> handheldTemporaryPoints = new ArrayList<>(); |
| | | private final List<Point2D.Double> mowerTemporaryPoints = new ArrayList<>(); |
| | | private enum BoundaryCaptureMode { NONE, HANDHELD, MOWER } |
| | | private BoundaryCaptureMode activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | private boolean mowerBoundaryCaptureActive; |
| | | private Timer mowerBoundaryMonitor; |
| | | private Coordinate lastMowerCoordinate; |
| | | private double[] mowerBaseLatLon; |
| | | private boolean startButtonShowingPause = true; |
| | | private boolean stopButtonActive = false; |
| | | private boolean bluetoothConnected = false; |
| | | private Timer mowerSpeedRefreshTimer; |
| | | |
| | | public Shouye() { |
| | | instance = this; |
| | | baseStation = new BaseStation(); |
| | | baseStation.load(); |
| | | initializeUI(); |
| | | setupEventHandlers(); |
| | | } |
| | | |
| | | 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); |
| | | |
| | | // 初始化对话框引用为null,延迟创建 |
| | | legendDialog = null; |
| | | remoteDialog = null; |
| | | areaDialog = null; |
| | | baseStationDialog = null; |
| | | settingsDialog = null; |
| | | |
| | | // 设置默认状态 |
| | | setNavigationActive(homeNavBtn); |
| | | |
| | | initializeDefaultAreaSelection(); |
| | | refreshMapForSelectedArea(); |
| | | } |
| | | |
| | | 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(new GridLayout(2, 1)); |
| | | leftInfoPanel.setBackground(PANEL_BACKGROUND); |
| | | |
| | | 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()); |
| | | |
| | | leftInfoPanel.add(areaNameLabel); |
| | | leftInfoPanel.add(statusLabel); |
| | | |
| | | // 右侧操作区域 |
| | | 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() { |
| | | @Override |
| | | protected void paintComponent(Graphics g) { |
| | | super.paintComponent(g); |
| | | // 委托给MapRenderer进行绘制 |
| | | if (mapRenderer != null) { |
| | | mapRenderer.renderMap(g); |
| | | } |
| | | } |
| | | }; |
| | | visualizationPanel.setLayout(new BorderLayout()); |
| | | |
| | | 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); |
| | | |
| | | mainContentPanel.add(visualizationPanel, BorderLayout.CENTER); |
| | | |
| | | startMowerSpeedUpdates(); |
| | | } |
| | | |
| | | 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(); |
| | | bluetoothConnected = Bluelink.isConnected(); |
| | | ImageIcon initialIcon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | if (initialIcon != null) { |
| | | button.setIcon(initialIcon); |
| | | } else { |
| | | button.setText(bluetoothConnected ? "已连" : "蓝牙"); |
| | | } |
| | | return button; |
| | | } |
| | | |
| | | private JButton createFunctionButton(String text, String icon) { |
| | | JButton button = new JButton("<html><center>" + icon + "<br>" + text + "</center></html>"); |
| | | button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | | 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("<html><center>" + icon + "<br>" + text + "</center></html>"); |
| | | 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) { |
| | | remoteDialog = new RemoteControlDialog(this, THEME_COLOR); |
| | | } else { |
| | | remoteDialog = new RemoteControlDialog((JFrame) null, THEME_COLOR); |
| | | } |
| | | } |
| | | if (remoteDialog != null) { |
| | | positionRemoteDialogBottomCenter(remoteDialog); |
| | | } |
| | | remoteDialog.setVisible(true); |
| | | } |
| | | |
| | | private void positionRemoteDialogBottomCenter(RemoteControlDialog dialog) { |
| | | if (dialog == null) { |
| | | return; |
| | | } |
| | | Rectangle targetBounds = computeVisualizationBoundsOnScreen(); |
| | | Dimension dialogSize = dialog.getSize(); |
| | | |
| | | int x; |
| | | int y; |
| | | if (targetBounds != null) { |
| | | x = targetBounds.x + (targetBounds.width - dialogSize.width) / 2; |
| | | y = targetBounds.y + targetBounds.height - dialogSize.height; |
| | | } else { |
| | | Component parentComponent = this; |
| | | Point parentOnScreen = parentComponent.getLocationOnScreen(); |
| | | int parentWidth = parentComponent.getWidth(); |
| | | int parentHeight = parentComponent.getHeight(); |
| | | x = parentOnScreen.x + (parentWidth - dialogSize.width) / 2; |
| | | y = parentOnScreen.y + parentHeight - dialogSize.height; |
| | | } |
| | | |
| | | dialog.setLocation(Math.max(x, 0), Math.max(y, 0)); |
| | | } |
| | | |
| | | private Rectangle computeVisualizationBoundsOnScreen() { |
| | | if (visualizationPanel == null || !visualizationPanel.isShowing()) { |
| | | return null; |
| | | } |
| | | Point location = visualizationPanel.getLocationOnScreen(); |
| | | Dimension size = visualizationPanel.getSize(); |
| | | return new Rectangle(location.x, location.y, size.width, size.height); |
| | | } |
| | | |
| | | private void showAreaSelectionDialog() { |
| | | 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 = new Device(); |
| | | device.initFromProperties(); |
| | | |
| | | if (baseStationDialog == null) { |
| | | baseStationDialog = new BaseStationDialog(dialogParent, THEME_COLOR, device, baseStation); |
| | | } else { |
| | | baseStationDialog.refreshData(); |
| | | } |
| | | baseStationDialog.setVisible(true); |
| | | } |
| | | |
| | | 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 (startBtn == null) { |
| | | return; |
| | | } |
| | | startButtonShowingPause = !startButtonShowingPause; |
| | | if (startButtonShowingPause) { |
| | | statusLabel.setText("作业中"); |
| | | if (stopButtonActive) { |
| | | stopButtonActive = false; |
| | | updateStopButtonIcon(); |
| | | } |
| | | if (!beginMowingSession()) { |
| | | startButtonShowingPause = false; |
| | | statusLabel.setText("待机"); |
| | | updateStartButtonAppearance(); |
| | | return; |
| | | } |
| | | } else { |
| | | statusLabel.setText("暂停中"); |
| | | pauseMowingSession(); |
| | | } |
| | | updateStartButtonAppearance(); |
| | | } |
| | | |
| | | private void handleStopAction() { |
| | | stopButtonActive = !stopButtonActive; |
| | | updateStopButtonIcon(); |
| | | if (stopButtonActive) { |
| | | statusLabel.setText("已结束"); |
| | | startButtonShowingPause = false; |
| | | stopMowingSession(); |
| | | } else { |
| | | statusLabel.setText("待机"); |
| | | startButtonShowingPause = true; |
| | | pauseMowingSession(); |
| | | } |
| | | updateStartButtonAppearance(); |
| | | } |
| | | |
| | | private void updateStartButtonAppearance() { |
| | | if (startBtn == null) { |
| | | return; |
| | | } |
| | | String iconPath = startButtonShowingPause ? "image/start0.png" : "image/start1.png"; |
| | | startBtn.setText(startButtonShowingPause ? "暂停" : "开始"); |
| | | applyButtonIcon(startBtn, iconPath); |
| | | } |
| | | |
| | | private void updateStopButtonIcon() { |
| | | if (stopBtn == null) { |
| | | return; |
| | | } |
| | | String iconPath = stopButtonActive ? "image/stop1.png" : "image/stop0.png"; |
| | | applyButtonIcon(stopBtn, iconPath); |
| | | } |
| | | |
| | | private void toggleBluetoothConnection() { |
| | | if (bluetoothBtn == null) { |
| | | return; |
| | | } |
| | | if (Bluelink.isConnected()) { |
| | | Bluelink.disconnect(); |
| | | bluetoothConnected = false; |
| | | } else { |
| | | boolean success = Bluelink.connect(); |
| | | if (success) { |
| | | bluetoothConnected = true; |
| | | } else { |
| | | bluetoothConnected = false; |
| | | JOptionPane.showMessageDialog(this, "蓝牙连接失败,请重试", "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | } |
| | | updateBluetoothButtonIcon(); |
| | | } |
| | | |
| | | private void updateBluetoothButtonIcon() { |
| | | if (bluetoothBtn == null) { |
| | | return; |
| | | } |
| | | ensureBluetoothIconsLoaded(); |
| | | bluetoothConnected = Bluelink.isConnected(); |
| | | ImageIcon icon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | if (icon != null) { |
| | | bluetoothBtn.setIcon(icon); |
| | | bluetoothBtn.setText(null); |
| | | } else { |
| | | bluetoothBtn.setText(bluetoothConnected ? "已连" : "蓝牙"); |
| | | } |
| | | } |
| | | |
| | | private JPanel createSpeedIndicatorPanel() { |
| | | JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); |
| | | panel.setOpaque(false); |
| | | panel.setBorder(BorderFactory.createEmptyBorder(10, 20, 5, 20)); |
| | | |
| | | 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); |
| | | |
| | | panel.add(mowingProgressLabel); |
| | | panel.add(mowerSpeedValueLabel); |
| | | panel.add(mowerSpeedUnitLabel); |
| | | 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(); |
| | | } |
| | | |
| | | 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 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 boolean startMowerBoundaryCapture() { |
| | | if (mapRenderer == null) { |
| | | return false; |
| | | } |
| | | double[] baseLatLonCandidate = resolveCircleBaseLatLon(); |
| | | if (baseLatLonCandidate == null) { |
| | | return false; |
| | | } |
| | | |
| | | 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.beginHandheldBoundaryPreview(); |
| | | } |
| | | |
| | | startMowerBoundaryMonitor(); |
| | | return true; |
| | | } |
| | | |
| | | public boolean startHandheldBoundaryCapture() { |
| | | if (mapRenderer == null) { |
| | | return false; |
| | | } |
| | | if (handheldCaptureDialog != null && handheldCaptureDialog.isShowing()) { |
| | | handheldCaptureDialog.toFront(); |
| | | return true; |
| | | } |
| | | |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | stopMowerBoundaryCapture(); |
| | | } |
| | | |
| | | 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.beginHandheldBoundaryPreview(); |
| | | |
| | | Window ownerWindow = SwingUtilities.getWindowAncestor(this); |
| | | SwingUtilities.invokeLater(() -> { |
| | | Window targetOwner = ownerWindow; |
| | | if (targetOwner == null) { |
| | | targetOwner = SwingUtilities.getWindowAncestor(Shouye.this); |
| | | } |
| | | HandheldBoundaryCaptureDialog dialog = new HandheldBoundaryCaptureDialog(targetOwner, Shouye.this, visualizationPanel, THEME_COLOR); |
| | | handheldCaptureDialog = dialog; |
| | | dialog.setVisible(true); |
| | | }); |
| | | |
| | | return true; |
| | | } |
| | | |
| | | private void startMowerBoundaryMonitor() { |
| | | if (mowerBoundaryMonitor == null) { |
| | | mowerBoundaryMonitor = new Timer(600, e -> pollMowerBoundaryCoordinate()); |
| | | mowerBoundaryMonitor.setRepeats(true); |
| | | } |
| | | if (!mowerBoundaryMonitor.isRunning()) { |
| | | mowerBoundaryMonitor.start(); |
| | | } |
| | | pollMowerBoundaryCoordinate(); |
| | | } |
| | | |
| | | private void stopMowerBoundaryMonitor() { |
| | | if (mowerBoundaryMonitor != null && mowerBoundaryMonitor.isRunning()) { |
| | | mowerBoundaryMonitor.stop(); |
| | | } |
| | | } |
| | | |
| | | private void pollMowerBoundaryCoordinate() { |
| | | if (!mowerBoundaryCaptureActive) { |
| | | return; |
| | | } |
| | | |
| | | Coordinate latest = getLatestCoordinate(); |
| | | if (latest == null || latest == lastMowerCoordinate) { |
| | | return; |
| | | } |
| | | |
| | | double[] base = mowerBaseLatLon; |
| | | if (base == null || base.length < 2) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | |
| | | double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection()); |
| | | double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection()); |
| | | if (!Double.isFinite(lat) || !Double.isFinite(lon)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | |
| | | double[] local = convertLatLonToLocal(lat, lon, base[0], base[1]); |
| | | Point2D.Double candidate = new Point2D.Double(local[0], local[1]); |
| | | if (!Double.isFinite(candidate.x) || !Double.isFinite(candidate.y)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | |
| | | List<Point2D.Double> snapshot; |
| | | synchronized (mowerTemporaryPoints) { |
| | | for (Point2D.Double existing : mowerTemporaryPoints) { |
| | | if (existing != null && arePointsClose(existing, candidate)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | } |
| | | mowerTemporaryPoints.add(candidate); |
| | | snapshot = new ArrayList<>(mowerTemporaryPoints.size() + 1); |
| | | for (Point2D.Double point : mowerTemporaryPoints) { |
| | | if (point != null) { |
| | | snapshot.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | ensureClosed(snapshot); |
| | | AddDikuai.recordTemporaryBoundaryPoints(snapshot); |
| | | if (mapRenderer != null) { |
| | | mapRenderer.addHandheldBoundaryPoint(candidate.x, candidate.y); |
| | | } |
| | | lastMowerCoordinate = latest; |
| | | } |
| | | |
| | | private void stopMowerBoundaryCapture() { |
| | | stopMowerBoundaryMonitor(); |
| | | mowerBoundaryCaptureActive = false; |
| | | lastMowerCoordinate = null; |
| | | mowerBaseLatLon = null; |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | } |
| | | Coordinate.setStartSaveGngga(false); |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | } |
| | | |
| | | private void discardLatestCoordinate(Coordinate coordinate) { |
| | | if (coordinate == null) { |
| | | return; |
| | | } |
| | | synchronized (Coordinate.coordinates) { |
| | | int size = Coordinate.coordinates.size(); |
| | | if (size == 0) { |
| | | return; |
| | | } |
| | | int lastIndex = size - 1; |
| | | if (Coordinate.coordinates.get(lastIndex) == coordinate) { |
| | | Coordinate.coordinates.remove(lastIndex); |
| | | } else { |
| | | Coordinate.coordinates.remove(coordinate); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void ensureClosed(List<Point2D.Double> points) { |
| | | if (points == null || points.size() < 3) { |
| | | return; |
| | | } |
| | | Point2D.Double first = points.get(0); |
| | | Point2D.Double last = points.get(points.size() - 1); |
| | | if (first == null || last == null) { |
| | | return; |
| | | } |
| | | if (!arePointsClose(first, last)) { |
| | | points.add(new Point2D.Double(first.x, first.y)); |
| | | } |
| | | } |
| | | |
| | | int captureHandheldBoundaryPoint() { |
| | | if (!handheldCaptureActive) { |
| | | return -1; |
| | | } |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | JOptionPane.showMessageDialog(this, "未检测到采集设备,请检查连接。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | String[] latParts = splitCoordinateComponents(device.getRealtimeLatitude(), true); |
| | | String[] lonParts = splitCoordinateComponents(device.getRealtimeLongitude(), false); |
| | | if (latParts == null || lonParts == null) { |
| | | JOptionPane.showMessageDialog(this, "当前定位无效,请在定位稳定后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | JOptionPane.showMessageDialog(this, "当前定位数据无效,请稍后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | if (isDuplicateHandheldPoint(x, y)) { |
| | | JOptionPane.showMessageDialog(this, "当前坐标已采集,请移动到新的位置后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | double altitude = parseAltitudeValue(device.getRealtimeAltitude()); |
| | | Coordinate coordinate = new Coordinate(latParts[0], latParts[1], lonParts[0], lonParts[1], altitude); |
| | | synchronized (Coordinate.coordinates) { |
| | | Coordinate.coordinates.add(coordinate); |
| | | } |
| | | |
| | | if (mapRenderer != null) { |
| | | mapRenderer.addHandheldBoundaryPoint(x, y); |
| | | } |
| | | |
| | | List<Point2D.Double> snapshot; |
| | | synchronized (handheldTemporaryPoints) { |
| | | handheldTemporaryPoints.add(new Point2D.Double(x, y)); |
| | | snapshot = new ArrayList<>(handheldTemporaryPoints); |
| | | } |
| | | AddDikuai.recordTemporaryBoundaryPoints(snapshot); |
| | | |
| | | handheldCapturedPoints++; |
| | | return handheldCapturedPoints; |
| | | } |
| | | |
| | | boolean finishHandheldBoundaryCapture() { |
| | | if (!handheldCaptureActive) { |
| | | return false; |
| | | } |
| | | if (handheldCapturedPoints < 3) { |
| | | JOptionPane.showMessageDialog(this, "至少采集三个点才能生成边界。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | List<Point2D.Double> closedSnapshot = createClosedHandheldPointSnapshot(); |
| | | handheldCaptureActive = false; |
| | | Coordinate.setStartSaveGngga(false); |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | } |
| | | |
| | | AddDikuai.recordTemporaryBoundaryPoints(closedSnapshot); |
| | | |
| | | SwingUtilities.invokeLater(AddDikuai::finishDrawingSession); |
| | | return true; |
| | | } |
| | | |
| | | void handheldBoundaryCaptureDialogClosed(HandheldBoundaryCaptureDialog dialog) { |
| | | if (handheldCaptureDialog == dialog) { |
| | | handheldCaptureDialog = null; |
| | | } |
| | | handheldCaptureActive = false; |
| | | if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD) { |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | } |
| | | |
| | | int getHandheldCapturedPointCount() { |
| | | return handheldCapturedPoints; |
| | | } |
| | | |
| | | public List<Point2D.Double> getHandheldTemporaryPointsSnapshot() { |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | return createClosedMowerPointSnapshot(); |
| | | } |
| | | if (!handheldCaptureActive) { |
| | | return createClosedHandheldPointSnapshot(); |
| | | } |
| | | synchronized (handheldTemporaryPoints) { |
| | | return new ArrayList<>(handheldTemporaryPoints); |
| | | } |
| | | } |
| | | |
| | | public boolean isCurrentHandheldPointDuplicate() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return false; |
| | | } |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | return false; |
| | | } |
| | | return isDuplicateHandheldPoint(x, y); |
| | | } |
| | | |
| | | public boolean hasValidRealtimeHandheldPosition() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return false; |
| | | } |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | return Double.isFinite(x) && Double.isFinite(y); |
| | | } |
| | | |
| | | private boolean isDuplicateHandheldPoint(double x, double y) { |
| | | Point2D.Double candidate = new Point2D.Double(x, y); |
| | | synchronized (handheldTemporaryPoints) { |
| | | for (Point2D.Double existing : handheldTemporaryPoints) { |
| | | if (existing == null) { |
| | | continue; |
| | | } |
| | | if (arePointsClose(existing, candidate)) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean arePointsClose(Point2D.Double a, Point2D.Double b) { |
| | | if (a == null || b == null) { |
| | | return false; |
| | | } |
| | | double dx = a.x - b.x; |
| | | double dy = a.y - b.y; |
| | | return Math.hypot(dx, dy) < HANDHELD_DUPLICATE_THRESHOLD_METERS; |
| | | } |
| | | |
| | | private List<Point2D.Double> createClosedHandheldPointSnapshot() { |
| | | List<Point2D.Double> copy = new ArrayList<>(); |
| | | synchronized (handheldTemporaryPoints) { |
| | | for (Point2D.Double point : handheldTemporaryPoints) { |
| | | if (point != null) { |
| | | copy.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | ensureClosed(copy); |
| | | return copy; |
| | | } |
| | | |
| | | private List<Point2D.Double> createClosedMowerPointSnapshot() { |
| | | List<Point2D.Double> copy = new ArrayList<>(); |
| | | synchronized (mowerTemporaryPoints) { |
| | | for (Point2D.Double point : mowerTemporaryPoints) { |
| | | if (point != null) { |
| | | copy.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | ensureClosed(copy); |
| | | return copy; |
| | | } |
| | | |
| | | private String[] splitCoordinateComponents(String combined, boolean latitude) { |
| | | if (combined == null) { |
| | | return null; |
| | | } |
| | | String trimmed = combined.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | |
| | | String valuePart; |
| | | String directionPart = null; |
| | | String[] parts = trimmed.split(","); |
| | | if (parts.length >= 2) { |
| | | valuePart = parts[0].trim(); |
| | | directionPart = parts[1].trim(); |
| | | } else { |
| | | valuePart = trimmed; |
| | | } |
| | | |
| | | if (valuePart.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | if (directionPart == null || directionPart.isEmpty()) { |
| | | char lastChar = valuePart.charAt(valuePart.length() - 1); |
| | | if (Character.isLetter(lastChar)) { |
| | | directionPart = String.valueOf(lastChar); |
| | | valuePart = valuePart.substring(0, valuePart.length() - 1).trim(); |
| | | } |
| | | } |
| | | |
| | | if (valuePart.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | directionPart = normalizeHemisphere(directionPart, latitude); |
| | | return new String[]{valuePart, directionPart}; |
| | | } |
| | | |
| | | private String normalizeHemisphere(String direction, boolean latitude) { |
| | | if (direction == null || direction.trim().isEmpty()) { |
| | | return latitude ? "N" : "E"; |
| | | } |
| | | String normalized = direction.trim().toUpperCase(Locale.ROOT); |
| | | if (latitude) { |
| | | if (!"N".equals(normalized) && !"S".equals(normalized)) { |
| | | return "N"; |
| | | } |
| | | } else { |
| | | if (!"E".equals(normalized) && !"W".equals(normalized)) { |
| | | return "E"; |
| | | } |
| | | } |
| | | return normalized; |
| | | } |
| | | |
| | | private double parseMetersValue(String raw) { |
| | | if (raw == null) { |
| | | return Double.NaN; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return Double.NaN; |
| | | } |
| | | try { |
| | | return Double.parseDouble(trimmed); |
| | | } catch (NumberFormatException ex) { |
| | | return Double.NaN; |
| | | } |
| | | } |
| | | |
| | | private double parseAltitudeValue(String raw) { |
| | | if (raw == null) { |
| | | return 0.0; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | return Double.parseDouble(trimmed); |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | private boolean beginMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return false; |
| | | } |
| | | String landNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | if (!isMeaningfulValue(landNumber)) { |
| | | JOptionPane.showMessageDialog(this, "请先选择地块后再开始作业", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | double widthMeters = resolveMowerWidthMeters(landNumber); |
| | | if (widthMeters <= 0) { |
| | | JOptionPane.showMessageDialog(this, "未配置割草宽度,将无法计算作业面积", "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | |
| | | mapRenderer.startRealtimeTrackRecording(landNumber, widthMeters); |
| | | refreshMowerSpeedLabel(); |
| | | return true; |
| | | } |
| | | |
| | | private void pauseMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | mapRenderer.pauseRealtimeTrackRecording(); |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | private void stopMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | mapRenderer.stopRealtimeTrackRecording(); |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | private double resolveMowerWidthMeters(String landNumber) { |
| | | double width = 0.0; |
| | | if (isMeaningfulValue(landNumber)) { |
| | | Dikuai current = Dikuai.getDikuai(landNumber); |
| | | if (current != null) { |
| | | width = parseMowerWidthMeters(current.getMowingWidth()); |
| | | } |
| | | } |
| | | if (width > 0) { |
| | | return width; |
| | | } |
| | | return parseMowerWidthFromDevice(); |
| | | } |
| | | |
| | | private double parseMowerWidthFromDevice() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return 0.0; |
| | | } |
| | | return parseMowerWidthMeters(device.getMowingWidth()); |
| | | } |
| | | |
| | | private double parseMowerWidthMeters(String raw) { |
| | | if (raw == null) { |
| | | return 0.0; |
| | | } |
| | | String sanitized = raw.trim().toLowerCase(Locale.ROOT); |
| | | if (sanitized.isEmpty() || "-1".equals(sanitized)) { |
| | | return 0.0; |
| | | } |
| | | sanitized = sanitized.replace("厘米", "cm"); |
| | | sanitized = sanitized.replace("公分", "cm"); |
| | | sanitized = sanitized.replace("米", "m"); |
| | | sanitized = sanitized.replace("cm", ""); |
| | | sanitized = sanitized.replace("m", ""); |
| | | sanitized = sanitized.trim(); |
| | | if (sanitized.isEmpty()) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | double value = Double.parseDouble(sanitized); |
| | | if (value <= 0) { |
| | | return 0.0; |
| | | } |
| | | if (value > 10) { |
| | | return value / 100.0; |
| | | } |
| | | return value; |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | private void applyStatusLabelColor(String statusText) { |
| | | if (statusLabel == null) { |
| | | return; |
| | | } |
| | | if ("作业中".equals(statusText)) { |
| | | statusLabel.setForeground(THEME_COLOR); |
| | | } else if ("暂停中".equals(statusText)) { |
| | | statusLabel.setForeground(STATUS_PAUSE_COLOR); |
| | | } else { |
| | | statusLabel.setForeground(Color.GRAY); |
| | | } |
| | | } |
| | | |
| | | private void ensureBluetoothIconsLoaded() { |
| | | if (bluetoothIcon == null) { |
| | | bluetoothIcon = loadScaledIcon("image/blue.png", 28, 28); |
| | | } |
| | | if (bluetoothLinkedIcon == null) { |
| | | bluetoothLinkedIcon = loadScaledIcon("image/bluelink.png", 28, 28); |
| | | } |
| | | } |
| | | |
| | | private JButton createFloatingIconButton() { |
| | | 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 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); |
| | | } |
| | | } |
| | | |
| | | private void toggleDrawingPause() { |
| | | applyDrawingPauseState(!drawingPaused, true); |
| | | } |
| | | |
| | | public void showEndDrawingButton(Runnable callback) { |
| | | showEndDrawingButton(callback, null); |
| | | } |
| | | |
| | | public void showEndDrawingButton(Runnable callback, String drawingShape) { |
| | | endDrawingCallback = callback; |
| | | applyDrawingPauseState(false, false); |
| | | circleDialogMode = false; |
| | | hideCircleGuidancePanel(); |
| | | |
| | | ensureFloatingIconsLoaded(); |
| | | ensureFloatingButtonInfrastructure(); |
| | | |
| | | boolean enableCircleGuidance = drawingShape != null |
| | | && "circle".equalsIgnoreCase(drawingShape.trim()); |
| | | if (enableCircleGuidance) { |
| | | prepareCircleGuidanceState(); |
| | | showCircleGuidanceStep(1); |
| | | endDrawingButton.setVisible(false); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(false); |
| | | } |
| | | } else { |
| | | clearCircleGuidanceArtifacts(); |
| | | endDrawingButton.setVisible(true); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(true); |
| | | } |
| | | } |
| | | |
| | | floatingButtonPanel.setVisible(true); |
| | | if (floatingButtonPanel.getParent() != visualizationPanel) { |
| | | visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH); |
| | | } |
| | | |
| | | rebuildFloatingButtonColumn(); |
| | | 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); |
| | | } |
| | | 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; |
| | | String landNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | if (isMeaningfulValue(landNumber)) { |
| | | Dikuai current = Dikuai.getDikuai(landNumber); |
| | | if (current != null) { |
| | | coords = current.getBaseStationCoordinates(); |
| | | } |
| | | } |
| | | if (!isMeaningfulValue(coords)) { |
| | | coords = addzhangaiwu.getActiveSessionBaseStation(); |
| | | } |
| | | if (!isMeaningfulValue(coords) && baseStation != null) { |
| | | coords = baseStation.getInstallationCoordinates(); |
| | | } |
| | | if (!isMeaningfulValue(coords)) { |
| | | return null; |
| | | } |
| | | String[] parts = coords.split(","); |
| | | if (parts.length < 4) { |
| | | return null; |
| | | } |
| | | double baseLat = parseDMToDecimal(parts[0], parts[1]); |
| | | double baseLon = parseDMToDecimal(parts[2], parts[3]); |
| | | if (!Double.isFinite(baseLat) || !Double.isFinite(baseLon)) { |
| | | return null; |
| | | } |
| | | return new double[]{baseLat, baseLon}; |
| | | } |
| | | |
| | | private double parseDMToDecimal(String dmm, String direction) { |
| | | if (dmm == null || dmm.trim().isEmpty()) { |
| | | return Double.NaN; |
| | | } |
| | | try { |
| | | String trimmed = dmm.trim(); |
| | | int dotIndex = trimmed.indexOf('.'); |
| | | if (dotIndex < 2) { |
| | | return Double.NaN; |
| | | } |
| | | int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2)); |
| | | double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2)); |
| | | double decimal = degrees + minutes / 60.0; |
| | | if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) { |
| | | decimal = -decimal; |
| | | } |
| | | return decimal; |
| | | } catch (NumberFormatException ex) { |
| | | return Double.NaN; |
| | | } |
| | | } |
| | | |
| | | private double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) { |
| | | double deltaLat = lat - baseLat; |
| | | double deltaLon = lon - baseLon; |
| | | double meanLatRad = Math.toRadians((baseLat + lat) / 2.0); |
| | | double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad); |
| | | double northMeters = deltaLat * METERS_PER_DEGREE_LAT; |
| | | return new double[]{eastMeters, northMeters}; |
| | | } |
| | | |
| | | private CircleSolution fitCircleFromPoints(List<double[]> points) { |
| | | if (points == null || points.size() < 3) { |
| | | return null; |
| | | } |
| | | CircleSolution best = null; |
| | | double bestScore = 0.0; |
| | | int n = points.size(); |
| | | for (int i = 0; i < n - 2; i++) { |
| | | double[] p1 = points.get(i); |
| | | for (int j = i + 1; j < n - 1; j++) { |
| | | double[] p2 = points.get(j); |
| | | for (int k = j + 1; k < n; k++) { |
| | | double[] p3 = points.get(k); |
| | | CircleSolution candidate = circleFromThreePoints(p1, p2, p3); |
| | | if (candidate == null || candidate.radius <= 0) { |
| | | continue; |
| | | } |
| | | double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3))); |
| | | if (minEdge > bestScore) { |
| | | bestScore = minEdge; |
| | | best = candidate; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return best; |
| | | } |
| | | |
| | | private CircleSolution circleFromThreePoints(double[] p1, double[] p2, double[] p3) { |
| | | if (p1 == null || p2 == null || p3 == null) { |
| | | return null; |
| | | } |
| | | double x1 = p1[0]; |
| | | double y1 = p1[1]; |
| | | double x2 = p2[0]; |
| | | double y2 = p2[1]; |
| | | double x3 = p3[0]; |
| | | double y3 = p3[1]; |
| | | |
| | | double a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2; |
| | | double d = 2.0 * a; |
| | | if (Math.abs(d) < 1e-6) { |
| | | return null; |
| | | } |
| | | |
| | | double x1Sq = x1 * x1 + y1 * y1; |
| | | double x2Sq = x2 * x2 + y2 * y2; |
| | | double x3Sq = x3 * x3 + y3 * y3; |
| | | |
| | | double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / d; |
| | | double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / d; |
| | | double radius = Math.hypot(centerX - x1, centerY - y1); |
| | | if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) { |
| | | return null; |
| | | } |
| | | if (radius < 0.05) { |
| | | return null; |
| | | } |
| | | return new CircleSolution(centerX, centerY, radius); |
| | | } |
| | | |
| | | private double distance(double[] a, double[] b) { |
| | | if (a == null || b == null) { |
| | | return 0.0; |
| | | } |
| | | double dx = a[0] - b[0]; |
| | | double dy = a[1] - b[1]; |
| | | return Math.hypot(dx, dy); |
| | | } |
| | | |
| | | private static final class CircleSolution { |
| | | final double centerX; |
| | | final double centerY; |
| | | final double radius; |
| | | |
| | | CircleSolution(double centerX, double centerY, double radius) { |
| | | this.centerX = centerX; |
| | | this.centerY = centerY; |
| | | this.radius = radius; |
| | | } |
| | | } |
| | | |
| | | public void hideEndDrawingButton() { |
| | | hideCircleGuidancePanel(); |
| | | clearCircleGuidanceArtifacts(); |
| | | hideFloatingDrawingControls(); |
| | | circleDialogMode = false; |
| | | applyDrawingPauseState(false, false); |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | stopMowerBoundaryCapture(); |
| | | } else if (activeBoundaryMode == BoundaryCaptureMode.HANDHELD && !handheldCaptureActive) { |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | endDrawingCallback = 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(); |
| | | Map<String, Dikuai> 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<String, Dikuai> 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); |