| | |
| | | package zhuye; |
| | | |
| | | import javax.swing.*; |
| | | import javax.swing.Timer; |
| | | |
| | | import baseStation.BaseStation; |
| | | import baseStation.BaseStationDialog; |
| | |
| | | import gecaoji.Device; |
| | | import set.Sets; |
| | | import udpdell.UDPServer; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | |
| | | 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 JButton homeNavBtn; |
| | |
| | | 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 JDialog circleGuidanceDialog; |
| | | private boolean circleDialogMode; |
| | | private ComponentAdapter circleDialogOwnerAdapter; |
| | | private static final double METERS_PER_DEGREE_LAT = 111320.0d; |
| | | private final List<double[]> circleCapturedPoints = new ArrayList<>(); |
| | | private double[] circleBaseLatLon; |
| | | private Timer circleDataMonitor; |
| | | private Coordinate lastCapturedCoordinate; |
| | | private boolean startButtonShowingPause = true; |
| | | private boolean stopButtonActive = false; |
| | | private boolean bluetoothConnected = false; |
| | | |
| | | public Shouye() { |
| | | instance = this; |
| | |
| | | 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); |
| | |
| | | JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 20, 0)); |
| | | buttonPanel.setBackground(PANEL_BACKGROUND); |
| | | |
| | | startBtn = createControlButton("开始", THEME_COLOR); |
| | | applyButtonIcon(startBtn, "image/startzuoye.png"); |
| | | pauseBtn = createControlButton("暂停", Color.ORANGE); |
| | | applyButtonIcon(pauseBtn, "image/zantingzuoye.png"); |
| | | 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>"); |
| | | button.setFont(new Font("微软雅黑", Font.PLAIN, 12)); |
| | |
| | | |
| | | // 功能按钮事件 |
| | | 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() { |
| | |
| | | } |
| | | } |
| | | |
| | | private void startMowing() { |
| | | private void toggleStartPause() { |
| | | if (startBtn == null) { |
| | | return; |
| | | } |
| | | startButtonShowingPause = !startButtonShowingPause; |
| | | if (startButtonShowingPause) { |
| | | statusLabel.setText("作业中"); |
| | | startBtn.setEnabled(false); |
| | | pauseBtn.setEnabled(true); |
| | | if (stopButtonActive) { |
| | | stopButtonActive = false; |
| | | updateStopButtonIcon(); |
| | | } |
| | | } else { |
| | | statusLabel.setText("暂停中"); |
| | | } |
| | | updateStartButtonAppearance(); |
| | | } |
| | | |
| | | private void pauseMowing() { |
| | | statusLabel.setText("暂停中"); |
| | | startBtn.setEnabled(true); |
| | | pauseBtn.setEnabled(false); |
| | | private void handleStopAction() { |
| | | stopButtonActive = !stopButtonActive; |
| | | updateStopButtonIcon(); |
| | | if (stopButtonActive) { |
| | | statusLabel.setText("已结束"); |
| | | startButtonShowingPause = false; |
| | | } else { |
| | | statusLabel.setText("待机"); |
| | | startButtonShowingPause = true; |
| | | } |
| | | updateStartButtonAppearance(); |
| | | } |
| | | |
| | | private void updateStartButtonAppearance() { |
| | | if (startBtn == null) { |
| | | return; |
| | | } |
| | | String iconPath = startButtonShowingPause ? "image/start0.png" : "image/start1.png"; |
| | | startBtn.setText(startButtonShowingPause ? "暂停" : "开始"); |
| | | applyButtonIcon(startBtn, iconPath); |
| | | } |
| | | |
| | | private void updateStopButtonIcon() { |
| | | if (stopBtn == null) { |
| | | return; |
| | | } |
| | | String iconPath = stopButtonActive ? "image/stop1.png" : "image/stop0.png"; |
| | | applyButtonIcon(stopBtn, iconPath); |
| | | } |
| | | |
| | | private void toggleBluetoothConnection() { |
| | | if (bluetoothBtn == null) { |
| | | return; |
| | | } |
| | | if (Bluelink.isConnected()) { |
| | | Bluelink.disconnect(); |
| | | bluetoothConnected = false; |
| | | } else { |
| | | boolean success = Bluelink.connect(); |
| | | if (success) { |
| | | bluetoothConnected = true; |
| | | } else { |
| | | bluetoothConnected = false; |
| | | JOptionPane.showMessageDialog(this, "蓝牙连接失败,请重试", "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | } |
| | | updateBluetoothButtonIcon(); |
| | | } |
| | | |
| | | private void updateBluetoothButtonIcon() { |
| | | if (bluetoothBtn == null) { |
| | | return; |
| | | } |
| | | ensureBluetoothIconsLoaded(); |
| | | bluetoothConnected = Bluelink.isConnected(); |
| | | ImageIcon icon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | if (icon != null) { |
| | | bluetoothBtn.setIcon(icon); |
| | | bluetoothBtn.setText(null); |
| | | } else { |
| | | bluetoothBtn.setText(bluetoothConnected ? "已连" : "蓝牙"); |
| | | } |
| | | } |
| | | |
| | | private void applyStatusLabelColor(String statusText) { |
| | | if (statusLabel == null) { |
| | | return; |
| | | } |
| | | if ("作业中".equals(statusText)) { |
| | | statusLabel.setForeground(THEME_COLOR); |
| | | } else if ("暂停中".equals(statusText)) { |
| | | statusLabel.setForeground(STATUS_PAUSE_COLOR); |
| | | } else { |
| | | statusLabel.setForeground(Color.GRAY); |
| | | } |
| | | } |
| | | |
| | | private void ensureBluetoothIconsLoaded() { |
| | | if (bluetoothIcon == null) { |
| | | bluetoothIcon = loadScaledIcon("image/blue.png", 28, 28); |
| | | } |
| | | if (bluetoothLinkedIcon == null) { |
| | | bluetoothLinkedIcon = loadScaledIcon("image/bluelink.png", 28, 28); |
| | | } |
| | | } |
| | | |
| | | private JButton createFloatingIconButton() { |
| | |
| | | public void showEndDrawingButton(Runnable callback, String drawingShape) { |
| | | endDrawingCallback = callback; |
| | | applyDrawingPauseState(false, false); |
| | | circleDialogMode = drawingShape != null && "circle".equalsIgnoreCase(drawingShape); |
| | | |
| | | if (circleDialogMode) { |
| | | hideFloatingDrawingControls(); |
| | | ensureCircleGuidancePanel(); |
| | | showCircleGuidanceStep(1); |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | return; |
| | | } |
| | | |
| | | circleDialogMode = false; |
| | | hideCircleGuidancePanel(); |
| | | |
| | | ensureFloatingIconsLoaded(); |
| | | ensureFloatingButtonInfrastructure(); |
| | | |
| | | boolean enableCircleGuidance = drawingShape != null |
| | | && "circle".equalsIgnoreCase(drawingShape.trim()); |
| | | if (enableCircleGuidance) { |
| | | prepareCircleGuidanceState(); |
| | | showCircleGuidanceStep(1); |
| | | endDrawingButton.setVisible(false); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(false); |
| | | } |
| | | } else { |
| | | clearCircleGuidanceArtifacts(); |
| | | endDrawingButton.setVisible(true); |
| | | if (drawingPauseButton != null) { |
| | | drawingPauseButton.setVisible(true); |
| | | } |
| | | } |
| | | |
| | | floatingButtonPanel.setVisible(true); |
| | | if (floatingButtonPanel.getParent() != visualizationPanel) { |
| | |
| | | } |
| | | circleGuidanceStep = step; |
| | | if (step == 1) { |
| | | circleGuidanceLabel.setText("是否将当前点设置为圆心?"); |
| | | circleGuidancePrimaryButton.setText("是"); |
| | | circleGuidanceLabel.setText("采集第1个点"); |
| | | circleGuidancePrimaryButton.setText("确认第1点"); |
| | | circleGuidanceSecondaryButton.setText("返回"); |
| | | circleGuidanceSecondaryButton.setVisible(true); |
| | | } else if (step == 2) { |
| | | circleGuidanceLabel.setText("是否将当前点设置为半径点?"); |
| | | circleGuidancePrimaryButton.setText("是"); |
| | | circleGuidanceSecondaryButton.setVisible(false); |
| | | 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) { |
| | |
| | | button.addMouseListener(new MouseAdapter() { |
| | | @Override |
| | | public void mouseEntered(MouseEvent e) { |
| | | if (!button.isEnabled()) { |
| | | return; |
| | | } |
| | | if (filled) { |
| | | button.setBackground(THEME_HOVER_COLOR); |
| | | } else { |
| | |
| | | |
| | | @Override |
| | | public void mouseExited(MouseEvent e) { |
| | | if (!button.isEnabled()) { |
| | | return; |
| | | } |
| | | if (filled) { |
| | | button.setBackground(bg); |
| | | } else { |
| | |
| | | } |
| | | |
| | | private void onCircleGuidancePrimary() { |
| | | if (circleGuidanceStep == 1) { |
| | | showCircleGuidanceStep(2); |
| | | 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) { |
| | | if (circleGuidanceStep == 1 || circleGuidanceStep == 2 || circleGuidanceStep == 3) { |
| | | handleCircleAbort(null); |
| | | } else if (circleGuidanceStep == 2) { |
| | | handleCircleAbort(null); |
| | | } else { |
| | | hideCircleGuidancePanel(); |
| | | } else if (circleGuidanceStep == 4) { |
| | | restartCircleCapture(); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | private void handleCircleCompletion() { |
| | | hideCircleGuidancePanel(); |
| | | clearCircleGuidanceArtifacts(); |
| | | if (endDrawingCallback != null) { |
| | | endDrawingCallback.run(); |
| | | } else { |
| | |
| | | } |
| | | } |
| | | |
| | | private void prepareCircleGuidanceState() { |
| | | clearCircleGuidanceArtifacts(); |
| | | circleBaseLatLon = resolveCircleBaseLatLon(); |
| | | startCircleDataMonitor(); |
| | | } |
| | | |
| | | private void clearCircleGuidanceArtifacts() { |
| | | stopCircleDataMonitor(); |
| | | circleCapturedPoints.clear(); |
| | | circleBaseLatLon = null; |
| | | lastCapturedCoordinate = null; |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearCircleCaptureOverlay(); |
| | | mapRenderer.clearCircleSampleMarkers(); |
| | | } |
| | | } |
| | | |
| | | private void restartCircleCapture() { |
| | | clearCircleGuidanceArtifacts(); |
| | | synchronized (Coordinate.coordinates) { |
| | | Coordinate.coordinates.clear(); |
| | | } |
| | | Coordinate.setStartSaveGngga(true); |
| | | circleBaseLatLon = resolveCircleBaseLatLon(); |
| | | showCircleGuidanceStep(1); |
| | | startCircleDataMonitor(); |
| | | } |
| | | |
| | | private boolean recordCircleSamplePoint() { |
| | | Coordinate latest = getLatestCoordinate(); |
| | | if (latest == null) { |
| | | JOptionPane.showMessageDialog(this, "未获取到当前位置坐标,请稍后重试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | double[] base = ensureCircleBaseLatLon(); |
| | | if (base == null) { |
| | | JOptionPane.showMessageDialog(this, "基准站坐标无效,请先在基准站管理中完成设置。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection()); |
| | | double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection()); |
| | | if (!Double.isFinite(lat) || !Double.isFinite(lon)) { |
| | | JOptionPane.showMessageDialog(this, "采集点坐标无效,请重新采集。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | | double[] local = convertLatLonToLocal(lat, lon, base[0], base[1]); |
| | | circleCapturedPoints.add(local); |
| | | if (mapRenderer != null) { |
| | | mapRenderer.updateCircleSampleMarkers(circleCapturedPoints); |
| | | } |
| | | lastCapturedCoordinate = latest; |
| | | refreshCircleGuidanceButtonAvailability(); |
| | | |
| | | if (circleCapturedPoints.size() >= 3) { |
| | | CircleSolution solution = fitCircleFromPoints(circleCapturedPoints); |
| | | if (solution == null) { |
| | | circleCapturedPoints.remove(circleCapturedPoints.size() - 1); |
| | | JOptionPane.showMessageDialog(this, "无法根据当前三个点生成圆,请重新采集点。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | restartCircleCapture(); |
| | | return false; |
| | | } |
| | | if (mapRenderer != null) { |
| | | mapRenderer.showCircleCaptureOverlay(solution.centerX, solution.centerY, solution.radius, circleCapturedPoints); |
| | | } |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | private void startCircleDataMonitor() { |
| | | if (circleDataMonitor == null) { |
| | | circleDataMonitor = new Timer(600, e -> refreshCircleGuidanceButtonAvailability()); |
| | | circleDataMonitor.setRepeats(true); |
| | | } |
| | | if (!circleDataMonitor.isRunning()) { |
| | | circleDataMonitor.start(); |
| | | } |
| | | refreshCircleGuidanceButtonAvailability(); |
| | | } |
| | | |
| | | private void stopCircleDataMonitor() { |
| | | if (circleDataMonitor != null) { |
| | | circleDataMonitor.stop(); |
| | | } |
| | | } |
| | | |
| | | private void refreshCircleGuidanceButtonAvailability() { |
| | | if (circleGuidancePrimaryButton == null) { |
| | | return; |
| | | } |
| | | boolean shouldEnable = circleGuidanceStep >= 1 && circleGuidanceStep <= 3 |
| | | ? isCircleDataAvailable() |
| | | : true; |
| | | applyCirclePrimaryButtonState(shouldEnable); |
| | | } |
| | | |
| | | private boolean isCircleDataAvailable() { |
| | | Coordinate latest = getLatestCoordinate(); |
| | | if (latest == null) { |
| | | return false; |
| | | } |
| | | return lastCapturedCoordinate == null || latest != lastCapturedCoordinate; |
| | | } |
| | | |
| | | private void applyCirclePrimaryButtonState(boolean enabled) { |
| | | if (circleGuidancePrimaryButton == null) { |
| | | return; |
| | | } |
| | | if (circleGuidanceStep >= 4) { |
| | | enabled = true; |
| | | } |
| | | circleGuidancePrimaryButton.setEnabled(enabled); |
| | | if (enabled) { |
| | | circleGuidancePrimaryButton.setBackground(THEME_COLOR); |
| | | circleGuidancePrimaryButton.setForeground(Color.WHITE); |
| | | circleGuidancePrimaryButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
| | | } else { |
| | | circleGuidancePrimaryButton.setBackground(new Color(200, 200, 200)); |
| | | circleGuidancePrimaryButton.setForeground(new Color(120, 120, 120)); |
| | | circleGuidancePrimaryButton.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); |
| | | } |
| | | } |
| | | |
| | | private Coordinate getLatestCoordinate() { |
| | | synchronized (Coordinate.coordinates) { |
| | | int size = Coordinate.coordinates.size(); |
| | | if (size == 0) { |
| | | return null; |
| | | } |
| | | return Coordinate.coordinates.get(size - 1); |
| | | } |
| | | } |
| | | |
| | | private double[] ensureCircleBaseLatLon() { |
| | | if (circleBaseLatLon == null) { |
| | | circleBaseLatLon = resolveCircleBaseLatLon(); |
| | | } |
| | | return circleBaseLatLon; |
| | | } |
| | | |
| | | private double[] resolveCircleBaseLatLon() { |
| | | String coords = null; |
| | | String landNumber = Dikuaiguanli.getCurrentWorkLandNumber(); |
| | | if (isMeaningfulValue(landNumber)) { |
| | | Dikuai current = Dikuai.getDikuai(landNumber); |
| | | if (current != null) { |
| | | coords = current.getBaseStationCoordinates(); |
| | | } |
| | | } |
| | | if (!isMeaningfulValue(coords)) { |
| | | coords = addzhangaiwu.getActiveSessionBaseStation(); |
| | | } |
| | | if (!isMeaningfulValue(coords) && baseStation != null) { |
| | | coords = baseStation.getInstallationCoordinates(); |
| | | } |
| | | if (!isMeaningfulValue(coords)) { |
| | | return null; |
| | | } |
| | | String[] parts = coords.split(","); |
| | | if (parts.length < 4) { |
| | | return null; |
| | | } |
| | | double baseLat = parseDMToDecimal(parts[0], parts[1]); |
| | | double baseLon = parseDMToDecimal(parts[2], parts[3]); |
| | | if (!Double.isFinite(baseLat) || !Double.isFinite(baseLon)) { |
| | | return null; |
| | | } |
| | | return new double[]{baseLat, baseLon}; |
| | | } |
| | | |
| | | private double parseDMToDecimal(String dmm, String direction) { |
| | | if (dmm == null || dmm.trim().isEmpty()) { |
| | | return Double.NaN; |
| | | } |
| | | try { |
| | | String trimmed = dmm.trim(); |
| | | int dotIndex = trimmed.indexOf('.'); |
| | | if (dotIndex < 2) { |
| | | return Double.NaN; |
| | | } |
| | | int degrees = Integer.parseInt(trimmed.substring(0, dotIndex - 2)); |
| | | double minutes = Double.parseDouble(trimmed.substring(dotIndex - 2)); |
| | | double decimal = degrees + minutes / 60.0; |
| | | if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) { |
| | | decimal = -decimal; |
| | | } |
| | | return decimal; |
| | | } catch (NumberFormatException ex) { |
| | | return Double.NaN; |
| | | } |
| | | } |
| | | |
| | | private double[] convertLatLonToLocal(double lat, double lon, double baseLat, double baseLon) { |
| | | double deltaLat = lat - baseLat; |
| | | double deltaLon = lon - baseLon; |
| | | double meanLatRad = Math.toRadians((baseLat + lat) / 2.0); |
| | | double eastMeters = deltaLon * METERS_PER_DEGREE_LAT * Math.cos(meanLatRad); |
| | | double northMeters = deltaLat * METERS_PER_DEGREE_LAT; |
| | | return new double[]{eastMeters, northMeters}; |
| | | } |
| | | |
| | | private CircleSolution fitCircleFromPoints(List<double[]> points) { |
| | | if (points == null || points.size() < 3) { |
| | | return null; |
| | | } |
| | | CircleSolution best = null; |
| | | double bestScore = 0.0; |
| | | int n = points.size(); |
| | | for (int i = 0; i < n - 2; i++) { |
| | | double[] p1 = points.get(i); |
| | | for (int j = i + 1; j < n - 1; j++) { |
| | | double[] p2 = points.get(j); |
| | | for (int k = j + 1; k < n; k++) { |
| | | double[] p3 = points.get(k); |
| | | CircleSolution candidate = circleFromThreePoints(p1, p2, p3); |
| | | if (candidate == null || candidate.radius <= 0) { |
| | | continue; |
| | | } |
| | | double minEdge = Math.min(distance(p1, p2), Math.min(distance(p2, p3), distance(p1, p3))); |
| | | if (minEdge > bestScore) { |
| | | bestScore = minEdge; |
| | | best = candidate; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return best; |
| | | } |
| | | |
| | | private CircleSolution circleFromThreePoints(double[] p1, double[] p2, double[] p3) { |
| | | if (p1 == null || p2 == null || p3 == null) { |
| | | return null; |
| | | } |
| | | double x1 = p1[0]; |
| | | double y1 = p1[1]; |
| | | double x2 = p2[0]; |
| | | double y2 = p2[1]; |
| | | double x3 = p3[0]; |
| | | double y3 = p3[1]; |
| | | |
| | | double a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2; |
| | | double d = 2.0 * a; |
| | | if (Math.abs(d) < 1e-6) { |
| | | return null; |
| | | } |
| | | |
| | | double x1Sq = x1 * x1 + y1 * y1; |
| | | double x2Sq = x2 * x2 + y2 * y2; |
| | | double x3Sq = x3 * x3 + y3 * y3; |
| | | |
| | | double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / d; |
| | | double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / d; |
| | | double radius = Math.hypot(centerX - x1, centerY - y1); |
| | | if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) { |
| | | return null; |
| | | } |
| | | if (radius < 0.05) { |
| | | return null; |
| | | } |
| | | return new CircleSolution(centerX, centerY, radius); |
| | | } |
| | | |
| | | private double distance(double[] a, double[] b) { |
| | | if (a == null || b == null) { |
| | | return 0.0; |
| | | } |
| | | double dx = a[0] - b[0]; |
| | | double dy = a[1] - b[1]; |
| | | return Math.hypot(dx, dy); |
| | | } |
| | | |
| | | private static final class CircleSolution { |
| | | final double centerX; |
| | | final double centerY; |
| | | final double radius; |
| | | |
| | | CircleSolution(double centerX, double centerY, double radius) { |
| | | this.centerX = centerX; |
| | | this.centerY = centerY; |
| | | this.radius = radius; |
| | | } |
| | | } |
| | | |
| | | public void hideEndDrawingButton() { |
| | | hideCircleGuidancePanel(); |
| | | clearCircleGuidanceArtifacts(); |
| | | hideFloatingDrawingControls(); |
| | | circleDialogMode = false; |
| | | applyDrawingPauseState(false, false); |