| | |
| | | package zhuye; |
| | | |
| | | import javax.swing.*; |
| | | import javax.swing.Timer; |
| | | |
| | | import baseStation.BaseStation; |
| | | import set.Setsys; |
| | | import baseStation.BaseStationDialog; |
| | | |
| | | import java.awt.*; |
| | | import java.awt.event.*; |
| | | |
| | | import chuankou.dellmessage; |
| | | import dikuai.Dikuai; |
| | | import dikuai.Dikuaiguanli; |
| | | import dikuai.addzhangaiwu; |
| | | import gecaoji.Device; |
| | | import set.Sets; |
| | | import udpdell.UDPServer; |
| | | import zhangaiwu.AddDikuai; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Locale; |
| | | import java.util.Objects; |
| | | import java.util.function.Consumer; |
| | | import java.awt.geom.Point2D; |
| | | |
| | | /** |
| | | * 首页界面 - 适配6.5寸竖屏,使用独立的MapRenderer进行绘制 |
| | |
| | | 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 JButton startBtn; |
| | | private JButton pauseBtn; |
| | | 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 FixQualityIndicator fixQualityIndicator; |
| | | |
| | | // 导航按钮 |
| | | private JButton homeNavBtn; |
| | |
| | | private BaseStationDialog baseStationDialog; |
| | | private Sets settingsDialog; |
| | | private BaseStation baseStation; |
| | | |
| | | private final Consumer<String> serialLineListener = line -> SwingUtilities.invokeLater(this::updateDataPacketCountLabel); |
| | | |
| | | // 地图渲染器 |
| | | private MapRenderer mapRenderer; |
| | |
| | | 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() { |
| | |
| | | add(controlPanel, BorderLayout.SOUTH); |
| | | |
| | | // 初始化地图渲染器 |
| | | mapRenderer = new MapRenderer(visualizationPanel); |
| | | mapRenderer = new MapRenderer(visualizationPanel); |
| | | applyIdleTrailDurationFromSettings(); |
| | | |
| | | // 初始化对话框引用为null,延迟创建 |
| | | legendDialog = 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(); |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | addHierarchyListener(listener); |
| | | } |
| | | |
| | | private void showInitialMowerSelfCheckDialogIfNeeded() { |
| | | zijian.showInitialPromptIfNeeded(this, this::showRemoteControlDialog); |
| | | } |
| | | |
| | | private void applyIdleTrailDurationFromSettings() { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | int durationSeconds = MapRenderer.DEFAULT_IDLE_TRAIL_DURATION_SECONDS; |
| | | String configuredValue = Setsys.getPropertyValue("idleTrailDurationSeconds"); |
| | | if (configuredValue != null) { |
| | | String trimmed = configuredValue.trim(); |
| | | if (!trimmed.isEmpty()) { |
| | | try { |
| | | int parsed = Integer.parseInt(trimmed); |
| | | if (parsed >= 5 && parsed <= 600) { |
| | | durationSeconds = parsed; |
| | | } |
| | | } catch (NumberFormatException ignored) { |
| | | durationSeconds = MapRenderer.DEFAULT_IDLE_TRAIL_DURATION_SECONDS; |
| | | } |
| | | } |
| | | } |
| | | mapRenderer.setIdleTrailDurationSeconds(durationSeconds); |
| | | } |
| | | |
| | | private void createHeaderPanel() { |
| | |
| | | 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); |
| | | |
| | | JButton notificationBtn = createIconButton("🔔", 40); |
| | | bluetoothBtn = createBluetoothButton(); |
| | | |
| | | // 修改设置按钮:使用图片替代Unicode图标 |
| | | JButton settingsBtn = new JButton(); |
| | |
| | | // 添加设置按钮事件 |
| | | settingsBtn.addActionListener(e -> showSettingsDialog()); |
| | | |
| | | rightActionPanel.add(notificationBtn); |
| | | rightActionPanel.add(bluetoothBtn); |
| | | rightActionPanel.add(settingsBtn); |
| | | |
| | | headerPanel.add(leftInfoPanel, BorderLayout.WEST); |
| | |
| | | }; |
| | | visualizationPanel.setLayout(new BorderLayout()); |
| | | |
| | | JPanel speedIndicatorPanel = createSpeedIndicatorPanel(); |
| | | visualizationPanel.add(speedIndicatorPanel, BorderLayout.NORTH); |
| | | |
| | | // 创建功能按钮面板(放在左上角) |
| | | JPanel functionButtonsPanel = new JPanel(); |
| | | functionButtonsPanel.setLayout(new BoxLayout(functionButtonsPanel, BoxLayout.Y_AXIS)); |
| | |
| | | visualizationPanel.add(functionButtonsPanel, BorderLayout.WEST); |
| | | |
| | | mainContentPanel.add(visualizationPanel, BorderLayout.CENTER); |
| | | |
| | | startMowerSpeedUpdates(); |
| | | } |
| | | |
| | | private void createControlPanel() { |
| | |
| | | JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 20, 0)); |
| | | buttonPanel.setBackground(PANEL_BACKGROUND); |
| | | |
| | | startBtn = createControlButton("开始", THEME_COLOR); |
| | | pauseBtn = createControlButton("暂停", Color.ORANGE); |
| | | pauseBtn.setEnabled(false); |
| | | startBtn = createControlButton("暂停", THEME_COLOR); |
| | | updateStartButtonAppearance(); |
| | | |
| | | stopBtn = createControlButton("结束", Color.ORANGE); |
| | | updateStopButtonIcon(); |
| | | |
| | | buttonPanel.add(startBtn); |
| | | buttonPanel.add(pauseBtn); |
| | | buttonPanel.add(stopBtn); |
| | | |
| | | controlPanel.add(buttonPanel, BorderLayout.CENTER); |
| | | } |
| | |
| | | |
| | | 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>"); |
| | |
| | | |
| | | 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>"); |
| | |
| | | |
| | | // 功能按钮事件 |
| | | legendBtn.addActionListener(e -> showLegendDialog()); |
| | | if (bluetoothBtn != null) { |
| | | bluetoothBtn.addActionListener(e -> toggleBluetoothConnection()); |
| | | } |
| | | remoteBtn.addActionListener(e -> showRemoteControlDialog()); |
| | | areaSelectBtn.addActionListener(e -> { |
| | | // 点击“地块”直接打开地块管理对话框(若需要可传入特定地块编号) |
| | |
| | | baseStationBtn.addActionListener(e -> showBaseStationDialog()); |
| | | |
| | | // 控制按钮事件 |
| | | startBtn.addActionListener(e -> startMowing()); |
| | | pauseBtn.addActionListener(e -> pauseMowing()); |
| | | startBtn.addActionListener(e -> toggleStartPause()); |
| | | stopBtn.addActionListener(e -> handleStopAction()); |
| | | } |
| | | |
| | | private void showSettingsDialog() { |
| | |
| | | remoteDialog = new RemoteControlDialog((JFrame) null, THEME_COLOR); |
| | | } |
| | | } |
| | | remoteDialog.setVisible(true); |
| | | if (remoteDialog != null) { |
| | | positionRemoteDialogBottomCenter(remoteDialog); |
| | | zijian.markSelfCheckCompleted(); |
| | | remoteDialog.setVisible(true); |
| | | } |
| | | } |
| | | |
| | | private void positionRemoteDialogBottomCenter(RemoteControlDialog dialog) { |
| | | if (dialog == null) { |
| | | return; |
| | | } |
| | | Rectangle targetBounds = computeVisualizationBoundsOnScreen(); |
| | | Dimension dialogSize = dialog.getSize(); |
| | | |
| | | int x; |
| | | int y; |
| | | if (targetBounds != null) { |
| | | x = targetBounds.x + (targetBounds.width - dialogSize.width) / 2; |
| | | y = targetBounds.y + targetBounds.height - dialogSize.height; |
| | | } else { |
| | | Component parentComponent = this; |
| | | Point parentOnScreen = parentComponent.getLocationOnScreen(); |
| | | int parentWidth = parentComponent.getWidth(); |
| | | int parentHeight = parentComponent.getHeight(); |
| | | x = parentOnScreen.x + (parentWidth - dialogSize.width) / 2; |
| | | y = parentOnScreen.y + parentHeight - dialogSize.height; |
| | | } |
| | | |
| | | dialog.setLocation(Math.max(x, 0), Math.max(y, 0)); |
| | | } |
| | | |
| | | private Rectangle computeVisualizationBoundsOnScreen() { |
| | | if (visualizationPanel == null || !visualizationPanel.isShowing()) { |
| | | return null; |
| | | } |
| | | Point location = visualizationPanel.getLocationOnScreen(); |
| | | Dimension size = visualizationPanel.getSize(); |
| | | return new Rectangle(location.x, location.y, size.width, size.height); |
| | | } |
| | | |
| | | private void showAreaSelectionDialog() { |
| | |
| | | } |
| | | } |
| | | |
| | | Device device = new Device(); |
| | | device.initFromProperties(); |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | device = new Device(); |
| | | device.initFromProperties(); |
| | | Device.setGecaoji(device); |
| | | } |
| | | |
| | | if (baseStationDialog == null) { |
| | | baseStationDialog = new BaseStationDialog(dialogParent, THEME_COLOR, device, baseStation); |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | private void startMowing() { |
| | | statusLabel.setText("作业中"); |
| | | startBtn.setEnabled(false); |
| | | pauseBtn.setEnabled(true); |
| | | private void toggleStartPause() { |
| | | if (handheldCaptureInlineUiActive) { |
| | | handleHandheldConfirmAction(); |
| | | return; |
| | | } |
| | | if (drawingControlModeActive) { |
| | | toggleDrawingPause(); |
| | | return; |
| | | } |
| | | if (startBtn == null) { |
| | | return; |
| | | } |
| | | if (startButtonShowingPause) { |
| | | if (!zijian.ensureBeforeMowing(this, this::showRemoteControlDialog)) { |
| | | return; |
| | | } |
| | | } |
| | | startButtonShowingPause = !startButtonShowingPause; |
| | | if (!startButtonShowingPause) { |
| | | statusLabel.setText("作业中"); |
| | | if (stopButtonActive) { |
| | | stopButtonActive = false; |
| | | updateStopButtonIcon(); |
| | | } |
| | | if (!beginMowingSession()) { |
| | | startButtonShowingPause = true; |
| | | statusLabel.setText("待机"); |
| | | updateStartButtonAppearance(); |
| | | return; |
| | | } |
| | | } else { |
| | | statusLabel.setText("暂停中"); |
| | | pauseMowingSession(); |
| | | } |
| | | updateStartButtonAppearance(); |
| | | } |
| | | |
| | | private void pauseMowing() { |
| | | statusLabel.setText("暂停中"); |
| | | startBtn.setEnabled(true); |
| | | pauseBtn.setEnabled(false); |
| | | |
| | | 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; |
| | | } |
| | | 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 BorderLayout()); |
| | | panel.setOpaque(false); |
| | | panel.setBorder(BorderFactory.createEmptyBorder(10, 20, 5, 20)); |
| | | |
| | | JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); |
| | | rightPanel.setOpaque(false); |
| | | |
| | | fixQualityIndicator = new FixQualityIndicator(); |
| | | fixQualityIndicator.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | fixQualityIndicator.addMouseListener(new MouseAdapter() { |
| | | @Override |
| | | public void mouseClicked(MouseEvent e) { |
| | | if (SwingUtilities.isLeftMouseButton(e) && mapRenderer != null) { |
| | | mapRenderer.showMowerInfo(); |
| | | } |
| | | } |
| | | }); |
| | | mowingProgressLabel = new JLabel("--%"); |
| | | mowingProgressLabel.setFont(new Font("微软雅黑", Font.BOLD, 12)); |
| | | mowingProgressLabel.setForeground(THEME_COLOR); |
| | | |
| | | mowerSpeedValueLabel = new JLabel("--"); |
| | | mowerSpeedValueLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); |
| | | mowerSpeedValueLabel.setForeground(THEME_COLOR); |
| | | |
| | | mowerSpeedUnitLabel = new JLabel("km/h"); |
| | | mowerSpeedUnitLabel.setFont(new Font("微软雅黑", Font.BOLD, 9)); |
| | | mowerSpeedUnitLabel.setForeground(THEME_COLOR); |
| | | |
| | | dataPacketCountLabel = new JLabel("--"); |
| | | dataPacketCountLabel.setFont(new Font("微软雅黑", Font.BOLD, 12)); |
| | | dataPacketCountLabel.setForeground(THEME_COLOR); |
| | | |
| | | rightPanel.add(fixQualityIndicator); |
| | | |
| | | JSeparator areaSeparator = new JSeparator(SwingConstants.VERTICAL); |
| | | areaSeparator.setPreferredSize(new Dimension(1, 16)); |
| | | rightPanel.add(areaSeparator); |
| | | |
| | | rightPanel.add(mowingProgressLabel); |
| | | JSeparator speedSeparator = new JSeparator(SwingConstants.VERTICAL); |
| | | speedSeparator.setPreferredSize(new Dimension(1, 16)); |
| | | rightPanel.add(speedSeparator); |
| | | rightPanel.add(mowerSpeedValueLabel); |
| | | rightPanel.add(mowerSpeedUnitLabel); |
| | | |
| | | JSeparator separator = new JSeparator(SwingConstants.VERTICAL); |
| | | separator.setPreferredSize(new Dimension(1, 16)); |
| | | rightPanel.add(separator); |
| | | |
| | | rightPanel.add(dataPacketCountLabel); |
| | | |
| | | panel.add(rightPanel, BorderLayout.EAST); |
| | | updateFixQualityIndicator(); |
| | | updateDataPacketCountLabel(); |
| | | return panel; |
| | | } |
| | | |
| | | private void startMowerSpeedUpdates() { |
| | | if (mowerSpeedRefreshTimer == null) { |
| | | mowerSpeedRefreshTimer = new Timer(1000, e -> refreshMowerSpeedLabel()); |
| | | mowerSpeedRefreshTimer.setRepeats(true); |
| | | } |
| | | if (!mowerSpeedRefreshTimer.isRunning()) { |
| | | mowerSpeedRefreshTimer.start(); |
| | | } |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | private void refreshMowerSpeedLabel() { |
| | | if (mowerSpeedValueLabel == null) { |
| | | return; |
| | | } |
| | | String display = "--"; |
| | | Device device = Device.getGecaoji(); |
| | | if (device != null) { |
| | | String sanitized = sanitizeSpeedValue(device.getRealtimeSpeed()); |
| | | if (sanitized != null) { |
| | | display = sanitized; |
| | | } |
| | | } |
| | | mowerSpeedValueLabel.setText(display); |
| | | if (mowerSpeedUnitLabel != null) { |
| | | mowerSpeedUnitLabel.setText("km/h"); |
| | | } |
| | | updateMowingProgressLabel(); |
| | | updateFixQualityIndicator(); |
| | | updateDataPacketCountLabel(); |
| | | } |
| | | |
| | | private void updateDataPacketCountLabel() { |
| | | if (dataPacketCountLabel == null) { |
| | | return; |
| | | } |
| | | int udpCount = UDPServer.getReceivedPacketCount(); |
| | | int serialCount = dellmessage.getProcessedLineCount(); |
| | | int displayCount = Math.max(udpCount, serialCount); |
| | | |
| | | if (displayCount <= 0) { |
| | | dataPacketCountLabel.setText("--"); |
| | | dataPacketCountLabel.setToolTipText(null); |
| | | } else { |
| | | dataPacketCountLabel.setText(String.valueOf(displayCount)); |
| | | dataPacketCountLabel.setToolTipText(String.format("串口: %d UDP: %d", serialCount, udpCount)); |
| | | } |
| | | } |
| | | |
| | | private void updateFixQualityIndicator() { |
| | | if (fixQualityIndicator == null) { |
| | | return; |
| | | } |
| | | Device device = Device.getGecaoji(); |
| | | String code = null; |
| | | if (device != null) { |
| | | code = sanitizeDeviceValue(device.getPositioningStatus()); |
| | | } |
| | | fixQualityIndicator.setQuality(code); |
| | | } |
| | | |
| | | private Color resolveFixQualityColor(String code) { |
| | | if (code == null) { |
| | | return new Color(160, 160, 160); |
| | | } |
| | | switch (code) { |
| | | case "0": |
| | | return new Color(160, 160, 160); |
| | | case "1": |
| | | return new Color(52, 152, 219); |
| | | case "2": |
| | | return new Color(26, 188, 156); |
| | | case "3": |
| | | return new Color(155, 89, 182); |
| | | case "4": |
| | | return THEME_COLOR; |
| | | case "5": |
| | | return new Color(241, 196, 15); |
| | | case "6": |
| | | return new Color(231, 76, 60); |
| | | case "7": |
| | | return new Color(230, 126, 34); |
| | | default: |
| | | return new Color(95, 95, 95); |
| | | } |
| | | } |
| | | |
| | | private String resolveFixQualityDescription(String code) { |
| | | if (code == null) { |
| | | return "未知"; |
| | | } |
| | | switch (code) { |
| | | case "0": |
| | | return "未定位"; |
| | | case "1": |
| | | return "单点定位"; |
| | | case "2": |
| | | return "码差分"; |
| | | case "3": |
| | | return "无效PPS"; |
| | | case "4": |
| | | return "固定解"; |
| | | case "5": |
| | | return "浮点解"; |
| | | case "6": |
| | | return "正在估算"; |
| | | case "7": |
| | | return "人工输入固定值"; |
| | | default: |
| | | return "其他"; |
| | | } |
| | | } |
| | | |
| | | private String sanitizeSpeedValue(String raw) { |
| | | if (raw == null) { |
| | | return null; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | if (trimmed.toLowerCase().endsWith("km/h")) { |
| | | trimmed = trimmed.substring(0, trimmed.length() - 4).trim(); |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private String sanitizeDeviceValue(String raw) { |
| | | if (raw == null) { |
| | | return null; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed) || "null".equalsIgnoreCase(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private void updateMowingProgressLabel() { |
| | | if (mowingProgressLabel == null) { |
| | | return; |
| | | } |
| | | if (mapRenderer == null) { |
| | | mowingProgressLabel.setText("--%"); |
| | | mowingProgressLabel.setToolTipText(null); |
| | | return; |
| | | } |
| | | |
| | | double totalArea = mapRenderer.getTotalLandAreaSqMeters(); |
| | | double completedArea = mapRenderer.getCompletedMowingAreaSqMeters(); |
| | | double ratio = mapRenderer.getMowingCompletionRatio(); |
| | | |
| | | if (totalArea <= 0) { |
| | | mowingProgressLabel.setText("--%"); |
| | | mowingProgressLabel.setToolTipText("暂无地块面积数据"); |
| | | return; |
| | | } |
| | | |
| | | double percent = Math.max(0.0, Math.min(1.0, ratio)) * 100.0; |
| | | mowingProgressLabel.setText(String.format(Locale.US, "%.1f%%", percent)); |
| | | mowingProgressLabel.setToolTipText(String.format(Locale.US, "%.1f㎡ / %.1f㎡", completedArea, totalArea)); |
| | | } |
| | | |
| | | public void refreshMowingIndicators() { |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | public void setHandheldMowerIconActive(boolean active) { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | mapRenderer.setHandheldMowerIconActive(active); |
| | | } |
| | | |
| | | public boolean startMowerBoundaryCapture() { |
| | | if (mapRenderer == null) { |
| | | return false; |
| | | } |
| | | double[] baseLatLonCandidate = resolveCircleBaseLatLon(); |
| | | if (baseLatLonCandidate == null) { |
| | | return false; |
| | | } |
| | | |
| | | mapRenderer.clearIdleTrail(); |
| | | |
| | | activeBoundaryMode = BoundaryCaptureMode.MOWER; |
| | | mowerBoundaryCaptureActive = true; |
| | | mowerBaseLatLon = baseLatLonCandidate; |
| | | lastMowerCoordinate = null; |
| | | |
| | | synchronized (Coordinate.coordinates) { |
| | | Coordinate.coordinates.clear(); |
| | | } |
| | | synchronized (mowerTemporaryPoints) { |
| | | mowerTemporaryPoints.clear(); |
| | | } |
| | | |
| | | AddDikuai.recordTemporaryBoundaryPoints(Collections.emptyList()); |
| | | Coordinate.setStartSaveGngga(true); |
| | | |
| | | if (mapRenderer != null) { |
| | | mapRenderer.setBoundaryPreviewMarkerScale(2.0d); |
| | | mapRenderer.beginHandheldBoundaryPreview(); |
| | | } |
| | | |
| | | setHandheldMowerIconActive(false); |
| | | |
| | | startMowerBoundaryMonitor(); |
| | | return true; |
| | | } |
| | | |
| | | public boolean startHandheldBoundaryCapture() { |
| | | if (mapRenderer == null) { |
| | | return false; |
| | | } |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | stopMowerBoundaryCapture(); |
| | | } |
| | | |
| | | mapRenderer.clearIdleTrail(); |
| | | |
| | | activeBoundaryMode = BoundaryCaptureMode.HANDHELD; |
| | | handheldCaptureActive = true; |
| | | handheldCapturedPoints = 0; |
| | | Coordinate.setStartSaveGngga(false); |
| | | synchronized (Coordinate.coordinates) { |
| | | Coordinate.coordinates.clear(); |
| | | } |
| | | synchronized (handheldTemporaryPoints) { |
| | | handheldTemporaryPoints.clear(); |
| | | } |
| | | AddDikuai.recordTemporaryBoundaryPoints(Collections.emptyList()); |
| | | mapRenderer.setBoundaryPreviewMarkerScale(1.0d); |
| | | mapRenderer.beginHandheldBoundaryPreview(); |
| | | setHandheldMowerIconActive(true); |
| | | enterHandheldCaptureInlineUi(); |
| | | return true; |
| | | } |
| | | |
| | | private void startMowerBoundaryMonitor() { |
| | | if (mowerBoundaryMonitor == null) { |
| | | mowerBoundaryMonitor = new Timer(600, e -> pollMowerBoundaryCoordinate()); |
| | | mowerBoundaryMonitor.setRepeats(true); |
| | | } |
| | | if (!mowerBoundaryMonitor.isRunning()) { |
| | | mowerBoundaryMonitor.start(); |
| | | } |
| | | pollMowerBoundaryCoordinate(); |
| | | } |
| | | |
| | | private void stopMowerBoundaryMonitor() { |
| | | if (mowerBoundaryMonitor != null && mowerBoundaryMonitor.isRunning()) { |
| | | mowerBoundaryMonitor.stop(); |
| | | } |
| | | } |
| | | |
| | | private void pollMowerBoundaryCoordinate() { |
| | | if (!mowerBoundaryCaptureActive) { |
| | | return; |
| | | } |
| | | |
| | | Coordinate latest = getLatestCoordinate(); |
| | | if (latest == null || latest == lastMowerCoordinate) { |
| | | return; |
| | | } |
| | | |
| | | double[] base = mowerBaseLatLon; |
| | | if (base == null || base.length < 2) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | |
| | | double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection()); |
| | | double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection()); |
| | | if (!Double.isFinite(lat) || !Double.isFinite(lon)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | |
| | | double[] local = convertLatLonToLocal(lat, lon, base[0], base[1]); |
| | | Point2D.Double candidate = new Point2D.Double(local[0], local[1]); |
| | | if (!Double.isFinite(candidate.x) || !Double.isFinite(candidate.y)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | |
| | | List<Point2D.Double> snapshot; |
| | | synchronized (mowerTemporaryPoints) { |
| | | for (Point2D.Double existing : mowerTemporaryPoints) { |
| | | if (existing != null && arePointsClose(existing, candidate)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | | } |
| | | } |
| | | mowerTemporaryPoints.add(candidate); |
| | | snapshot = new ArrayList<>(mowerTemporaryPoints.size() + 1); |
| | | for (Point2D.Double point : mowerTemporaryPoints) { |
| | | if (point != null) { |
| | | snapshot.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | ensureClosed(snapshot); |
| | | AddDikuai.recordTemporaryBoundaryPoints(snapshot); |
| | | if (mapRenderer != null) { |
| | | mapRenderer.addHandheldBoundaryPoint(candidate.x, candidate.y); |
| | | } |
| | | lastMowerCoordinate = latest; |
| | | } |
| | | |
| | | private void stopMowerBoundaryCapture() { |
| | | stopMowerBoundaryMonitor(); |
| | | mowerBoundaryCaptureActive = false; |
| | | lastMowerCoordinate = null; |
| | | mowerBaseLatLon = null; |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | } |
| | | Coordinate.setStartSaveGngga(false); |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | setHandheldMowerIconActive(false); |
| | | } |
| | | |
| | | private void discardLatestCoordinate(Coordinate coordinate) { |
| | | if (coordinate == null) { |
| | | return; |
| | | } |
| | | synchronized (Coordinate.coordinates) { |
| | | int size = Coordinate.coordinates.size(); |
| | | if (size == 0) { |
| | | return; |
| | | } |
| | | int lastIndex = size - 1; |
| | | if (Coordinate.coordinates.get(lastIndex) == coordinate) { |
| | | Coordinate.coordinates.remove(lastIndex); |
| | | } else { |
| | | Coordinate.coordinates.remove(coordinate); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void ensureClosed(List<Point2D.Double> points) { |
| | | if (points == null || points.size() < 3) { |
| | | return; |
| | | } |
| | | Point2D.Double first = points.get(0); |
| | | Point2D.Double last = points.get(points.size() - 1); |
| | | if (first == null || last == null) { |
| | | return; |
| | | } |
| | | if (!arePointsClose(first, last)) { |
| | | points.add(new Point2D.Double(first.x, first.y)); |
| | | } |
| | | } |
| | | |
| | | int captureHandheldBoundaryPoint() { |
| | | if (!handheldCaptureActive) { |
| | | return -1; |
| | | } |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | JOptionPane.showMessageDialog(this, "未检测到采集设备,请检查连接。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | String[] latParts = splitCoordinateComponents(device.getRealtimeLatitude(), true); |
| | | String[] lonParts = splitCoordinateComponents(device.getRealtimeLongitude(), false); |
| | | if (latParts == null || lonParts == null) { |
| | | JOptionPane.showMessageDialog(this, "当前定位无效,请在定位稳定后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | JOptionPane.showMessageDialog(this, "当前定位数据无效,请稍后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | if (isDuplicateHandheldPoint(x, y)) { |
| | | JOptionPane.showMessageDialog(this, "当前坐标已采集,请移动到新的位置后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | | |
| | | double altitude = parseAltitudeValue(device.getRealtimeAltitude()); |
| | | Coordinate coordinate = new Coordinate(latParts[0], latParts[1], lonParts[0], lonParts[1], altitude); |
| | | synchronized (Coordinate.coordinates) { |
| | | Coordinate.coordinates.add(coordinate); |
| | | } |
| | | |
| | | if (mapRenderer != null) { |
| | | mapRenderer.addHandheldBoundaryPoint(x, y); |
| | | } |
| | | |
| | | List<Point2D.Double> snapshot; |
| | | synchronized (handheldTemporaryPoints) { |
| | | handheldTemporaryPoints.add(new Point2D.Double(x, y)); |
| | | snapshot = new ArrayList<>(handheldTemporaryPoints); |
| | | } |
| | | AddDikuai.recordTemporaryBoundaryPoints(snapshot); |
| | | |
| | | handheldCapturedPoints++; |
| | | return handheldCapturedPoints; |
| | | } |
| | | |
| | | boolean finishHandheldBoundaryCapture() { |
| | | if (!handheldCaptureActive) { |
| | | return false; |
| | | } |
| | | if (handheldCapturedPoints < 3) { |
| | | JOptionPane.showMessageDialog(this, "至少采集三个点才能生成边界。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | List<Point2D.Double> closedSnapshot = createClosedHandheldPointSnapshot(); |
| | | handheldCaptureActive = false; |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | Coordinate.setStartSaveGngga(false); |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearHandheldBoundaryPreview(); |
| | | } |
| | | |
| | | AddDikuai.recordTemporaryBoundaryPoints(closedSnapshot); |
| | | |
| | | exitHandheldCaptureInlineUi(); |
| | | |
| | | SwingUtilities.invokeLater(AddDikuai::finishDrawingSession); |
| | | return true; |
| | | } |
| | | |
| | | int getHandheldCapturedPointCount() { |
| | | return handheldCapturedPoints; |
| | | } |
| | | |
| | | public List<Point2D.Double> getHandheldTemporaryPointsSnapshot() { |
| | | if (activeBoundaryMode == BoundaryCaptureMode.MOWER) { |
| | | return createClosedMowerPointSnapshot(); |
| | | } |
| | | if (!handheldCaptureActive) { |
| | | return createClosedHandheldPointSnapshot(); |
| | | } |
| | | synchronized (handheldTemporaryPoints) { |
| | | return new ArrayList<>(handheldTemporaryPoints); |
| | | } |
| | | } |
| | | |
| | | public boolean isCurrentHandheldPointDuplicate() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return false; |
| | | } |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | return false; |
| | | } |
| | | return isDuplicateHandheldPoint(x, y); |
| | | } |
| | | |
| | | public boolean hasValidRealtimeHandheldPosition() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return false; |
| | | } |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | return Double.isFinite(x) && Double.isFinite(y); |
| | | } |
| | | |
| | | private boolean isDuplicateHandheldPoint(double x, double y) { |
| | | Point2D.Double candidate = new Point2D.Double(x, y); |
| | | synchronized (handheldTemporaryPoints) { |
| | | for (Point2D.Double existing : handheldTemporaryPoints) { |
| | | if (existing == null) { |
| | | continue; |
| | | } |
| | | if (arePointsClose(existing, candidate)) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean arePointsClose(Point2D.Double a, Point2D.Double b) { |
| | | if (a == null || b == null) { |
| | | return false; |
| | | } |
| | | double dx = a.x - b.x; |
| | | double dy = a.y - b.y; |
| | | return Math.hypot(dx, dy) < HANDHELD_DUPLICATE_THRESHOLD_METERS; |
| | | } |
| | | |
| | | private List<Point2D.Double> createClosedHandheldPointSnapshot() { |
| | | List<Point2D.Double> copy = new ArrayList<>(); |
| | | synchronized (handheldTemporaryPoints) { |
| | | for (Point2D.Double point : handheldTemporaryPoints) { |
| | | if (point != null) { |
| | | copy.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | ensureClosed(copy); |
| | | return copy; |
| | | } |
| | | |
| | | private List<Point2D.Double> createClosedMowerPointSnapshot() { |
| | | List<Point2D.Double> copy = new ArrayList<>(); |
| | | synchronized (mowerTemporaryPoints) { |
| | | for (Point2D.Double point : mowerTemporaryPoints) { |
| | | if (point != null) { |
| | | copy.add(new Point2D.Double(point.x, point.y)); |
| | | } |
| | | } |
| | | } |
| | | ensureClosed(copy); |
| | | return copy; |
| | | } |
| | | |
| | | private String[] splitCoordinateComponents(String combined, boolean latitude) { |
| | | if (combined == null) { |
| | | return null; |
| | | } |
| | | String trimmed = combined.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | |
| | | String valuePart; |
| | | String directionPart = null; |
| | | String[] parts = trimmed.split(","); |
| | | if (parts.length >= 2) { |
| | | valuePart = parts[0].trim(); |
| | | directionPart = parts[1].trim(); |
| | | } else { |
| | | valuePart = trimmed; |
| | | } |
| | | |
| | | if (valuePart.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | if (directionPart == null || directionPart.isEmpty()) { |
| | | char lastChar = valuePart.charAt(valuePart.length() - 1); |
| | | if (Character.isLetter(lastChar)) { |
| | | directionPart = String.valueOf(lastChar); |
| | | valuePart = valuePart.substring(0, valuePart.length() - 1).trim(); |
| | | } |
| | | } |
| | | |
| | | if (valuePart.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | directionPart = normalizeHemisphere(directionPart, latitude); |
| | | return new String[]{valuePart, directionPart}; |
| | | } |
| | | |
| | | private String normalizeHemisphere(String direction, boolean latitude) { |
| | | if (direction == null || direction.trim().isEmpty()) { |
| | | return latitude ? "N" : "E"; |
| | | } |
| | | String normalized = direction.trim().toUpperCase(Locale.ROOT); |
| | | if (latitude) { |
| | | if (!"N".equals(normalized) && !"S".equals(normalized)) { |
| | | return "N"; |
| | | } |
| | | } else { |
| | | if (!"E".equals(normalized) && !"W".equals(normalized)) { |
| | | return "E"; |
| | | } |
| | | } |
| | | return normalized; |
| | | } |
| | | |
| | | private double parseMetersValue(String raw) { |
| | | if (raw == null) { |
| | | return Double.NaN; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return Double.NaN; |
| | | } |
| | | try { |
| | | return Double.parseDouble(trimmed); |
| | | } catch (NumberFormatException ex) { |
| | | return Double.NaN; |
| | | } |
| | | } |
| | | |
| | | private double parseAltitudeValue(String raw) { |
| | | if (raw == null) { |
| | | return 0.0; |
| | | } |
| | | String trimmed = raw.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | return Double.parseDouble(trimmed); |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | private boolean beginMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return false; |
| | | } |
| | | String landNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | if (!isMeaningfulValue(landNumber)) { |
| | | JOptionPane.showMessageDialog(this, "请先选择地块后再开始作业", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | double widthMeters = resolveMowerWidthMeters(landNumber); |
| | | if (widthMeters <= 0) { |
| | | JOptionPane.showMessageDialog(this, "未配置割草宽度,将无法计算作业面积", "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | |
| | | mapRenderer.startRealtimeTrackRecording(landNumber, widthMeters); |
| | | refreshMowerSpeedLabel(); |
| | | return true; |
| | | } |
| | | |
| | | private void pauseMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | mapRenderer.pauseRealtimeTrackRecording(); |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | private void stopMowingSession() { |
| | | if (mapRenderer == null) { |
| | | return; |
| | | } |
| | | mapRenderer.stopRealtimeTrackRecording(); |
| | | refreshMowerSpeedLabel(); |
| | | } |
| | | |
| | | private double resolveMowerWidthMeters(String landNumber) { |
| | | double width = 0.0; |
| | | if (isMeaningfulValue(landNumber)) { |
| | | Dikuai current = Dikuai.getDikuai(landNumber); |
| | | if (current != null) { |
| | | width = parseMowerWidthMeters(current.getMowingWidth()); |
| | | } |
| | | } |
| | | if (width > 0) { |
| | | return width; |
| | | } |
| | | return parseMowerWidthFromDevice(); |
| | | } |
| | | |
| | | private double parseMowerWidthFromDevice() { |
| | | Device device = Device.getGecaoji(); |
| | | if (device == null) { |
| | | return 0.0; |
| | | } |
| | | return parseMowerWidthMeters(device.getMowingWidth()); |
| | | } |
| | | |
| | | private double parseMowerWidthMeters(String raw) { |
| | | if (raw == null) { |
| | | return 0.0; |
| | | } |
| | | String sanitized = raw.trim().toLowerCase(Locale.ROOT); |
| | | if (sanitized.isEmpty() || "-1".equals(sanitized)) { |
| | | return 0.0; |
| | | } |
| | | sanitized = sanitized.replace("厘米", "cm"); |
| | | sanitized = sanitized.replace("公分", "cm"); |
| | | sanitized = sanitized.replace("米", "m"); |
| | | sanitized = sanitized.replace("cm", ""); |
| | | sanitized = sanitized.replace("m", ""); |
| | | sanitized = sanitized.trim(); |
| | | if (sanitized.isEmpty()) { |
| | | return 0.0; |
| | | } |
| | | try { |
| | | double value = Double.parseDouble(sanitized); |
| | | if (value <= 0) { |
| | | return 0.0; |
| | | } |
| | | if (value > 10) { |
| | | return value / 100.0; |
| | | } |
| | | return value; |
| | | } catch (NumberFormatException ex) { |
| | | return 0.0; |
| | | } |
| | | } |
| | | |
| | | private void applyStatusLabelColor(String statusText) { |
| | | if (statusLabel == null) { |
| | | return; |
| | | } |
| | | if ("作业中".equals(statusText) || "绘制中".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() { |
| | |
| | | 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 (notifyCoordinate) { |
| | | Coordinate.setStartSaveGngga(!paused); |
| | | } |
| | | if (drawingControlModeActive) { |
| | | updateDrawingControlButtonLabels(); |
| | | if (statusLabel != null) { |
| | | statusLabel.setText(paused ? "绘制暂停" : "绘制中"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void toggleDrawingPause() { |
| | |
| | | } |
| | | |
| | | public void showEndDrawingButton(Runnable callback) { |
| | | ensureFloatingIconsLoaded(); |
| | | 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 -> { |
| | |
| | | floatingButtonColumn = new JPanel(); |
| | | floatingButtonColumn.setOpaque(false); |
| | | floatingButtonColumn.setLayout(new BoxLayout(floatingButtonColumn, BoxLayout.Y_AXIS)); |
| | | floatingButtonColumn.add(drawingPauseButton); |
| | | floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | floatingButtonColumn.add(endDrawingButton); |
| | | floatingButtonPanel.add(floatingButtonColumn, BorderLayout.EAST); |
| | | } else if (floatingButtonColumn != null) { |
| | | floatingButtonColumn.removeAll(); |
| | | floatingButtonColumn.add(drawingPauseButton); |
| | | floatingButtonColumn.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | floatingButtonColumn.add(endDrawingButton); |
| | | } |
| | | |
| | | endDrawingCallback = callback; |
| | | endDrawingButton.setVisible(true); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(true); |
| | | } |
| | | applyDrawingPauseState(false, false); |
| | | floatingButtonPanel.setVisible(true); |
| | | if (floatingButtonPanel.getParent() != visualizationPanel) { |
| | | visualizationPanel.add(floatingButtonPanel, BorderLayout.SOUTH); |
| | | } |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | public void hideEndDrawingButton() { |
| | | if (endDrawingButton != null) { |
| | | endDrawingButton.setVisible(false); |
| | | } |
| | | private void hideFloatingDrawingControls() { |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(false); |
| | | } |
| | | if (endDrawingButton != null) { |
| | | endDrawingButton.setVisible(false); |
| | | } |
| | | if (floatingButtonPanel != null) { |
| | | floatingButtonPanel.setVisible(false); |
| | | } |
| | | applyDrawingPauseState(false, 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(); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | 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); |
| | |
| | | } |
| | | } |
| | | |
| | | 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 class FixQualityIndicator extends JComponent { |
| | | private static final long serialVersionUID = 1L; |
| | | private static final int DIAMETER = 16; |
| | | private String currentCode; |
| | | private Color currentColor = new Color(160, 160, 160); |
| | | |
| | | private FixQualityIndicator() { |
| | | setPreferredSize(new Dimension(DIAMETER, DIAMETER)); |
| | | setMinimumSize(new Dimension(DIAMETER, DIAMETER)); |
| | | setMaximumSize(new Dimension(DIAMETER, DIAMETER)); |
| | | setToolTipText("未知"); |
| | | } |
| | | |
| | | private void setQuality(String code) { |
| | | if (Objects.equals(currentCode, code)) { |
| | | return; |
| | | } |
| | | currentCode = code; |
| | | currentColor = resolveFixQualityColor(code); |
| | | setToolTipText(resolveFixQualityDescription(code)); |
| | | repaint(); |
| | | } |
| | | |
| | | @Override |
| | | protected void paintComponent(Graphics g) { |
| | | super.paintComponent(g); |
| | | Graphics2D g2 = (Graphics2D) g.create(); |
| | | try { |
| | | g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
| | | int diameter = Math.min(getWidth(), getHeight()) - 2; |
| | | int x = (getWidth() - diameter) / 2; |
| | | int y = (getHeight() - diameter) / 2; |
| | | g2.setColor(currentColor); |
| | | g2.fillOval(x, y, diameter, diameter); |
| | | g2.setColor(new Color(255, 255, 255, 128)); |
| | | g2.drawOval(x, y, diameter, diameter); |
| | | } finally { |
| | | g2.dispose(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 测试方法 |
| | | public static void main(String[] args) { |
| | | JFrame frame = new JFrame("AutoMow - 首页"); |