| | |
| | | #Mower Configuration Properties - Updated |
| | | #Mon Dec 22 13:47:46 CST 2025 |
| | | #Mon Dec 22 18:50:02 CST 2025 |
| | | appVersion=-1 |
| | | boundaryLengthVisible=false |
| | | currentWorkLandNumber=LAND2 |
| | |
| | | handheldMarkerId=1872 |
| | | idleTrailDurationSeconds=60 |
| | | manualBoundaryDrawingMode=false |
| | | mapScale=25.45 |
| | | mapScale=16.74 |
| | | measurementModeEnabled=false |
| | | mowerId=860 |
| | | serialAutoConnect=true |
| | | serialBaudRate=115200 |
| | | serialPortName=COM15 |
| | | simCardNumber=-1 |
| | | viewCenterX=-27.62 |
| | | viewCenterY=-15.01 |
| | | viewCenterX=4.67 |
| | | viewCenterY=0.55 |
| | |
| | | g2d.setStroke(originalStroke); |
| | | } |
| | | } |
| | | |
| | | |
| | |
| | | private String landName; |
| | | // è¾¹çåå§åæ |
| | | private String boundaryOriginalCoordinates; |
| | | // è¾¹çåå§XYåæ ï¼ç¸å¯¹äºåºåç«çXYåæ ï¼ |
| | | private String boundaryOriginalXY; |
| | | // è¾¹çåæ ï¼åå¨å¤è¾¹å½¢åæ ç¹éåï¼ |
| | | private String boundaryCoordinates; |
| | | // è§åè·¯å¾ï¼åå¨è·¯å¾åæ ç¹éåï¼ |
| | |
| | | dikuai.userId = landProps.getProperty("userId", "-1"); |
| | | dikuai.landName = landProps.getProperty("landName", "-1"); |
| | | dikuai.boundaryOriginalCoordinates = landProps.getProperty("boundaryOriginalCoordinates", "-1"); |
| | | dikuai.boundaryOriginalXY = landProps.getProperty("boundaryOriginalXY", "-1"); |
| | | dikuai.boundaryCoordinates = landProps.getProperty("boundaryCoordinates", "-1"); |
| | | dikuai.plannedPath = landProps.getProperty("plannedPath", "-1"); |
| | | dikuai.returnPointCoordinates = landProps.getProperty("returnPointCoordinates", "-1"); |
| | |
| | | case "boundaryOriginalCoordinates": |
| | | this.boundaryOriginalCoordinates = value; |
| | | return true; |
| | | case "boundaryOriginalXY": |
| | | this.boundaryOriginalXY = value; |
| | | return true; |
| | | case "boundaryCoordinates": |
| | | this.boundaryCoordinates = value; |
| | | return true; |
| | |
| | | if (dikuai.landNumber != null) properties.setProperty(landNumber + ".landNumber", dikuai.landNumber); |
| | | if (dikuai.landName != null) properties.setProperty(landNumber + ".landName", dikuai.landName); |
| | | if (dikuai.boundaryOriginalCoordinates != null) properties.setProperty(landNumber + ".boundaryOriginalCoordinates", dikuai.boundaryOriginalCoordinates); |
| | | if (dikuai.boundaryOriginalXY != null) properties.setProperty(landNumber + ".boundaryOriginalXY", dikuai.boundaryOriginalXY); |
| | | if (dikuai.boundaryCoordinates != null) properties.setProperty(landNumber + ".boundaryCoordinates", dikuai.boundaryCoordinates); |
| | | if (dikuai.plannedPath != null) properties.setProperty(landNumber + ".plannedPath", dikuai.plannedPath); |
| | | if (dikuai.returnPointCoordinates != null) properties.setProperty(landNumber + ".returnPointCoordinates", dikuai.returnPointCoordinates); |
| | |
| | | this.boundaryOriginalCoordinates = boundaryOriginalCoordinates; |
| | | } |
| | | |
| | | public String getBoundaryOriginalXY() { |
| | | return boundaryOriginalXY; |
| | | } |
| | | |
| | | public void setBoundaryOriginalXY(String boundaryOriginalXY) { |
| | | this.boundaryOriginalXY = boundaryOriginalXY; |
| | | } |
| | | |
| | | public String getBoundaryCoordinates() { |
| | | return boundaryCoordinates; |
| | | } |
| | |
| | | ", landNumber='" + landNumber + '\'' + |
| | | ", landName='" + landName + '\'' + |
| | | ", boundaryOriginalCoordinates='" + boundaryOriginalCoordinates + '\'' + |
| | | ", boundaryOriginalXY='" + boundaryOriginalXY + '\'' + |
| | | ", boundaryCoordinates='" + boundaryCoordinates + '\'' + |
| | | ", plannedPath='" + plannedPath + '\'' + |
| | | ", returnPointCoordinates='" + returnPointCoordinates + '\'' + |
| | |
| | | contentPanel.add(obstaclePanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // å°åè¾¹çåæ ï¼å¸¦æ¾ç¤ºé¡¶ç¹æé®ï¼ |
| | | JPanel boundaryPanel = createBoundaryInfoItem(dikuai); |
| | | // å°åè¾¹çåæ ï¼å¸¦æ¥çæé®ï¼ |
| | | JPanel boundaryPanel = createCardInfoItemWithIconButton("å°åè¾¹ç:", |
| | | createViewButton(e -> editBoundaryCoordinates(dikuai))); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(boundaryPanel), |
| | | () -> editBoundaryCoordinates(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾å°åè¾¹çåæ "); |
| | |
| | | contentPanel.add(baseStationPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | JPanel boundaryOriginalPanel = createCardInfoItemWithIconButton("è¾¹çåå§åæ :", |
| | | createViewButton(e -> editBoundaryOriginalCoordinates(dikuai))); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(boundaryOriginalPanel), |
| | | () -> editBoundaryOriginalCoordinates(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾è¾¹çåå§åæ "); |
| | | contentPanel.add(boundaryOriginalPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | JPanel completedTrackPanel = createCardInfoItemWithButton("已宿å²èè·¯å¾:", |
| | | getTruncatedValue(dikuai.getMowingTrack(), 12, "æªè®°å½"), |
| | | createViewButton(e -> showCompletedMowingTrackDialog(dikuai))); |
| | |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | String edited = promptCoordinateEditing("æ¥ç / ç¼è¾å°åè¾¹çåæ ", dikuai.getBoundaryCoordinates()); |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | |
| | | // è·åå°å管çå¯¹è¯æ¡ï¼åå¤å¨æå¼è¾¹çç¼è¾é¡µé¢æ¶å
³é |
| | | Window managementWindow = null; |
| | | if (owner instanceof JDialog) { |
| | | managementWindow = owner; |
| | | } |
| | | |
| | | // æå¼è¾¹çç¼è¾é¡µé¢ |
| | | Dikuanbianjipage page = new Dikuanbianjipage(owner, "å°åè¾¹ç管ç页é¢", dikuai.getBoundaryCoordinates(), dikuai); |
| | | page.setVisible(true); |
| | | |
| | | // å
³éå°å管çé¡µé¢ |
| | | if (managementWindow != null) { |
| | | managementWindow.dispose(); |
| | | } |
| | | |
| | | // è·åç¼è¾ç»æå¹¶ä¿å |
| | | String edited = page.getResult(); |
| | | if (edited == null) { |
| | | return; |
| | | } |
| | | String normalized = normalizeCoordinateInput(edited); |
| | | if (!saveFieldAndRefresh(dikuai, "boundaryCoordinates", normalized)) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³æ´æ°å°åè¾¹çåæ ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | JOptionPane.showMessageDialog(null, "æ æ³æ´æ°å°åè¾¹çåæ ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | String message = "-1".equals(normalized) ? "å°åè¾¹çåæ å·²æ¸
空" : "å°åè¾¹çåæ å·²æ´æ°"; |
| | | JOptionPane.showMessageDialog(this, message, "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | JOptionPane.showMessageDialog(null, message, "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | private void editPlannedPath(Dikuai dikuai) { |
| | |
| | | JOptionPane.showMessageDialog(this, message, "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | private void editBoundaryOriginalCoordinates(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | String edited = promptCoordinateEditing("æ¥ç / ç¼è¾è¾¹çåå§åæ ", dikuai.getBoundaryOriginalCoordinates()); |
| | | if (edited == null) { |
| | | return; |
| | | } |
| | | String normalized = normalizeCoordinateInput(edited); |
| | | if (!saveFieldAndRefresh(dikuai, "boundaryOriginalCoordinates", normalized)) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³æ´æ°è¾¹çåå§åæ ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | String message = "-1".equals(normalized) ? "è¾¹çåå§åæ å·²æ¸
空" : "è¾¹çåå§åæ å·²æ´æ°"; |
| | | JOptionPane.showMessageDialog(this, message, "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | private void editMowingPattern(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package dikuai; |
| | | |
| | | import javax.swing.*; |
| | | import java.awt.*; |
| | | import java.awt.event.*; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | import bianjie.bianjieguihua2; |
| | | import publicway.Fuzhibutton; |
| | | import zhuye.Coordinate; |
| | | import zhuye.Shouye; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | |
| | | /** |
| | | * å°åè¾¹ç管çé¡µé¢ |
| | | * æ¾ç¤ºï¼åå§è¾¹çåæ ï¼ç»çº¬åº¦ãé«ç¨ï¼ãåå§è¾¹çXYï¼ç¸å¯¹äºåºåç«ï¼ã以åå¯ç¼è¾çä¼ååè¾¹çåæ |
| | | */ |
| | | public class Dikuanbianjipage extends JDialog { |
| | | private static final long serialVersionUID = 1L; |
| | | private static final int SCREEN_WIDTH = 400; |
| | | private static final int SCREEN_HEIGHT = 800; |
| | | private static final Color PRIMARY_COLOR = new Color(46, 139, 87); |
| | | private static final Color PRIMARY_DARK = new Color(30, 107, 69); |
| | | private static final Color TEXT_COLOR = new Color(51, 51, 51); |
| | | private static final Color WHITE = Color.WHITE; |
| | | private static final Color BORDER_COLOR = new Color(200, 200, 200); |
| | | private static final Color BACKGROUND_COLOR = new Color(250, 250, 250); |
| | | |
| | | private String result = null; |
| | | private Dikuai dikuai; |
| | | |
| | | public Dikuanbianjipage(Window owner, String title, String initialValue, Dikuai dikuai) { |
| | | super(owner, title, Dialog.ModalityType.APPLICATION_MODAL); |
| | | this.dikuai = dikuai; |
| | | initializeUI(title, initialValue, dikuai); |
| | | } |
| | | |
| | | public String getResult() { |
| | | return result; |
| | | } |
| | | |
| | | private void initializeUI(String title, String initialValue, Dikuai dikuai) { |
| | | setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); |
| | | getContentPane().setLayout(new BorderLayout()); |
| | | getContentPane().setBackground(BACKGROUND_COLOR); |
| | | |
| | | JPanel contentPanel = new JPanel(); |
| | | contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); |
| | | contentPanel.setBackground(BACKGROUND_COLOR); |
| | | contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 16, 12, 16)); |
| | | |
| | | String landName = dikuai != null ? (dikuai.getLandName() != null ? dikuai.getLandName() : "æªç¥å°å") : "æªç¥å°å"; |
| | | String landNumber = dikuai != null ? (dikuai.getLandNumber() != null ? dikuai.getLandNumber() : "æªç¥ç¼å·") : "æªç¥ç¼å·"; |
| | | JLabel headerLabel = new JLabel(landName + " / " + landNumber); |
| | | headerLabel.setFont(new Font("微软é
é»", Font.BOLD, 16)); |
| | | headerLabel.setForeground(TEXT_COLOR); |
| | | headerLabel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | contentPanel.add(headerLabel); |
| | | contentPanel.add(Box.createVerticalStrut(12)); |
| | | |
| | | // åå§è¾¹çåæ ï¼ç»çº¬åº¦, é«ç¨ï¼ |
| | | String rawCoords = dikuai != null ? prepareCoordinateForEditor(dikuai.getBoundaryOriginalCoordinates()) : ""; |
| | | int rawCount = 0; |
| | | if (rawCoords != null && !rawCoords.isEmpty() && !"-1".equals(rawCoords)) { |
| | | rawCount = rawCoords.split(";").length; |
| | | } |
| | | JTextArea rawTextArea = createInfoTextArea(rawCoords, false, 6); |
| | | contentPanel.add(createTextAreaSection("åå§è¾¹çåæ (ç»åº¦, 纬度, é«ç¨) (" + rawCount + "ç¹)", rawTextArea)); |
| | | |
| | | // åå§è¾¹çXYåæ ï¼ç¸å¯¹äºåºåç«ï¼ |
| | | String rawXY = dikuai != null ? prepareCoordinateForEditor(dikuai.getBoundaryOriginalXY()) : ""; |
| | | int xyCount = 0; |
| | | if (rawXY != null && !rawXY.isEmpty() && !"-1".equals(rawXY)) { |
| | | xyCount = rawXY.split(";").length; |
| | | } |
| | | JTextArea rawXYArea = createInfoTextArea(rawXY, false, 6); |
| | | contentPanel.add(createTextAreaSection("åå§è¾¹çXYåæ ï¼ç¸å¯¹äºåºåç«ï¼ (" + xyCount + "ç¹)", rawXYArea)); |
| | | |
| | | // ä¼ååè¾¹çåæ ï¼å¯ç¼è¾ï¼ |
| | | String optCoords = prepareCoordinateForEditor(initialValue); |
| | | int optCount = 0; |
| | | if (optCoords != null && !optCoords.isEmpty() && !"-1".equals(optCoords)) { |
| | | optCount = optCoords.split(";").length; |
| | | } |
| | | JTextArea optTextArea = createInfoTextArea(optCoords, true, 6); |
| | | contentPanel.add(createTextAreaSection("ä¼ååå°åè¾¹çåæ (" + optCount + "ç¹)", optTextArea)); |
| | | |
| | | JScrollPane dialogScrollPane = new JScrollPane(contentPanel); |
| | | dialogScrollPane.setBorder(BorderFactory.createEmptyBorder()); |
| | | dialogScrollPane.getVerticalScrollBar().setUnitIncrement(16); |
| | | add(dialogScrollPane, BorderLayout.CENTER); |
| | | |
| | | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 12, 12)); |
| | | buttonPanel.setBackground(BACKGROUND_COLOR); |
| | | |
| | | JButton optimizeBtn = createPrimaryFooterButton("ä¼åè¾¹çåæ "); |
| | | JButton saveBtn = createPrimaryFooterButton("ä¿å"); |
| | | JButton previewBtn = createPrimaryFooterButton("é¢è§"); |
| | | JButton closeBtn = createPrimaryFooterButton("å
³é"); |
| | | |
| | | optimizeBtn.addActionListener(e -> { |
| | | if (dikuai == null) { |
| | | JOptionPane.showMessageDialog(this, "æªæ¾å°å°åä¿¡æ¯ï¼æ æ³ä¼å", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | String baseStation = dikuai.getBaseStationCoordinates(); |
| | | if (baseStation == null || baseStation.trim().isEmpty() || "-1".equals(baseStation)) { |
| | | JOptionPane.showMessageDialog(this, "åºç«åæ æªè®¾ç½®ï¼æ æ³ä¼å", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // åå¤ Coordinate å表 |
| | | String originalCoords = dikuai.getBoundaryOriginalCoordinates(); |
| | | if (originalCoords == null || originalCoords.trim().isEmpty() || "-1".equals(originalCoords)) { |
| | | JOptionPane.showMessageDialog(this, "åå§è¾¹çåæ ä¸ºç©ºï¼æ æ³ä¼å", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | // è§£æåå§åæ å° Coordinate.coordinates |
| | | List<Coordinate> coords = new ArrayList<>(); |
| | | String[] points = originalCoords.split(";"); |
| | | for (String point : points) { |
| | | String[] parts = point.split(","); |
| | | if (parts.length >= 2) { |
| | | double lonDecimal = Double.parseDouble(parts[0].trim()); |
| | | double latDecimal = Double.parseDouble(parts[1].trim()); |
| | | double alt = parts.length > 2 ? Double.parseDouble(parts[2].trim()) : 0.0; |
| | | |
| | | // å°åè¿å¶åº¦è½¬æ¢ä¸ºåº¦åæ ¼å¼å符串 |
| | | String latDM = decimalToDegreeMinute(latDecimal); |
| | | String lonDM = decimalToDegreeMinute(lonDecimal); |
| | | String latDir = latDecimal >= 0 ? "N" : "S"; |
| | | String lonDir = lonDecimal >= 0 ? "E" : "W"; |
| | | |
| | | coords.add(new Coordinate(latDM, latDir, lonDM, lonDir, alt)); |
| | | } |
| | | } |
| | | Coordinate.coordinates = coords; |
| | | |
| | | // è°ç¨ä¼åç®æ³ |
| | | String optimized = bianjieguihua2.processCoordinateListAuto(baseStation); |
| | | optTextArea.setText(optimized); |
| | | JOptionPane.showMessageDialog(this, "è¾¹çä¼å宿", "æç¤º", JOptionPane.INFORMATION_MESSAGE); |
| | | } catch (Exception ex) { |
| | | JOptionPane.showMessageDialog(this, "ä¼å失败: " + ex.getMessage(), "é误", JOptionPane.ERROR_MESSAGE); |
| | | ex.printStackTrace(); |
| | | } |
| | | }); |
| | | |
| | | previewBtn.addActionListener(e -> { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | // å
³éå½åå¯¹è¯æ¡ |
| | | dispose(); |
| | | |
| | | // è·åå½åä¼ååçè¾¹ç |
| | | String currentOptimized = optTextArea.getText(); |
| | | |
| | | // è°ç¨é¦é¡µæ¾ç¤ºé¢è§ |
| | | SwingUtilities.invokeLater(() -> { |
| | | Shouye.showBoundaryPreview(dikuai, currentOptimized, () -> { |
| | | // è¿ååè°ï¼éæ°æå¼æ¤é¡µé¢ |
| | | new Dikuanbianjipage(getOwner(), getTitle(), currentOptimized, dikuai).setVisible(true); |
| | | }); |
| | | }); |
| | | }); |
| | | |
| | | saveBtn.addActionListener(e -> { |
| | | if (dikuai == null) { |
| | | JOptionPane.showMessageDialog(this, "æªæ¾å°å°åä¿¡æ¯ï¼æ æ³ä¿å", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | String currentText = optTextArea.getText(); |
| | | if (currentText == null || currentText.trim().isEmpty() || "-1".equals(currentText.trim())) { |
| | | JOptionPane.showMessageDialog(this, "ä¼ååå°åè¾¹çä¸ºç©ºï¼æ æ³ä¿å", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | String trimmed = currentText.trim(); |
| | | // ä¿åå°å°å对象 |
| | | if (!Dikuai.updateField(dikuai.getLandNumber(), "boundaryCoordinates", trimmed)) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³æ´æ°å°åè¾¹çåæ ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | Dikuai.updateField(dikuai.getLandNumber(), "updateTime", getCurrentTime()); |
| | | Dikuai.saveToProperties(); |
| | | this.result = trimmed; |
| | | JOptionPane.showMessageDialog(this, "å°åè¾¹çåæ å·²æ´æ°", "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | dispose(); |
| | | }); |
| | | |
| | | closeBtn.addActionListener(e -> dispose()); |
| | | |
| | | // æé®é¡ºåºï¼ä¼åè¾¹çåæ ãä¿åãé¢è§ãå
³é |
| | | buttonPanel.add(optimizeBtn); |
| | | buttonPanel.add(saveBtn); |
| | | buttonPanel.add(previewBtn); |
| | | buttonPanel.add(closeBtn); |
| | | add(buttonPanel, BorderLayout.SOUTH); |
| | | |
| | | pack(); |
| | | setSize(SCREEN_WIDTH, SCREEN_HEIGHT); |
| | | setLocationRelativeTo(getOwner()); |
| | | } |
| | | |
| | | private String prepareCoordinateForEditor(String value) { |
| | | if (value == null) { |
| | | return ""; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return ""; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private JTextArea createInfoTextArea(String text, boolean editable, int rows) { |
| | | JTextArea area = new JTextArea(text); |
| | | area.setEditable(editable); |
| | | area.setLineWrap(true); |
| | | area.setWrapStyleWord(true); |
| | | area.setFont(new Font("微软é
é»", Font.PLAIN, 13)); |
| | | area.setRows(Math.max(rows, 2)); |
| | | area.setCaretPosition(0); |
| | | area.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); |
| | | area.setBackground(editable ? WHITE : new Color(245, 245, 245)); |
| | | return area; |
| | | } |
| | | |
| | | private JPanel createTextAreaSection(String title, JTextArea textArea) { |
| | | JPanel section = new JPanel(new BorderLayout(0, 6)); |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | JPanel titlePanel = new JPanel(new BorderLayout()); |
| | | titlePanel.setBackground(BACKGROUND_COLOR); |
| | | titlePanel.setOpaque(false); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软é
é»", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | titlePanel.add(titleLabel, BorderLayout.WEST); |
| | | |
| | | JButton copyButton = Fuzhibutton.createCopyButton( |
| | | () -> { |
| | | String text = textArea.getText(); |
| | | if (text == null || text.trim().isEmpty() || "-1".equals(text.trim())) { |
| | | return null; |
| | | } |
| | | return text; |
| | | }, |
| | | "å¤å¶", |
| | | new Color(230, 250, 240) |
| | | ); |
| | | copyButton.setFont(new Font("微软é
é»", Font.PLAIN, 12)); |
| | | copyButton.setPreferredSize(new Dimension(50, 24)); |
| | | copyButton.setMargin(new Insets(0,0,0,0)); |
| | | |
| | | titlePanel.add(copyButton, BorderLayout.EAST); |
| | | |
| | | section.add(titlePanel, BorderLayout.NORTH); |
| | | |
| | | JScrollPane scrollPane = new JScrollPane(textArea); |
| | | scrollPane.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); |
| | | scrollPane.getVerticalScrollBar().setUnitIncrement(12); |
| | | section.add(scrollPane, BorderLayout.CENTER); |
| | | |
| | | section.setBorder(BorderFactory.createEmptyBorder(4, 0, 12, 0)); |
| | | return section; |
| | | } |
| | | |
| | | private JButton createPrimaryFooterButton(String text) { |
| | | JButton button = new JButton(text); |
| | | button.setFont(new Font("微软é
é»", Font.PLAIN, 12)); |
| | | button.setBackground(PRIMARY_COLOR); |
| | | button.setForeground(WHITE); |
| | | button.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12)); |
| | | button.setFocusPainted(false); |
| | | button.setCursor(new Cursor(Cursor.HAND_CURSOR)); |
| | | |
| | | button.addMouseListener(new java.awt.event.MouseAdapter() { |
| | | public void mouseEntered(java.awt.event.MouseEvent e) { |
| | | button.setBackground(PRIMARY_DARK); |
| | | } |
| | | |
| | | public void mouseExited(java.awt.event.MouseEvent e) { |
| | | button.setBackground(PRIMARY_COLOR); |
| | | } |
| | | }); |
| | | |
| | | return button; |
| | | } |
| | | |
| | | private String getCurrentTime() { |
| | | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | return sdf.format(new Date()); |
| | | } |
| | | |
| | | /** |
| | | * å°åè¿å¶åº¦è½¬æ¢ä¸ºåº¦åæ ¼å¼å符串 |
| | | * ä¾å¦ï¼39.831468 -> "3949.888080" (39度49.888080å) |
| | | * |
| | | * @param decimalDegrees åè¿å¶åº¦ |
| | | * @return åº¦åæ ¼å¼å符串 |
| | | */ |
| | | private String decimalToDegreeMinute(double decimalDegrees) { |
| | | double absDecimal = Math.abs(decimalDegrees); |
| | | int degrees = (int) Math.floor(absDecimal); |
| | | double minutes = (absDecimal - degrees) * 60.0; |
| | | double degreeMinutes = degrees * 100.0 + minutes; |
| | | return String.format(java.util.Locale.US, "%.8f", degreeMinutes); |
| | | } |
| | | } |
| | |
| | | package lujing; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * æ éç¢ç©å¸å½¢èå°è·¯å¾è§åç±» (ä¼åç) |
| | | * æ éç¢ç©å¸å½¢èå°è·¯å¾è§åç±» (ç»æä¼åç) |
| | | * ç¹æ§ï¼ |
| | | * 1. æå°æå½±å®½åº¦æ¹åéæ© (æçæé«ï¼è½¬å¼¯æå°) |
| | | * 2. è¾¹ç¼è½®å»ä¼å
åå² (æ æ»è§è¦ç) |
| | | * 3. æ¯æå¤é¨ä¼ å
¥å·²å
å«éå ççå®½åº¦åæ° |
| | | */ |
| | | public class AoxinglujingNoObstacle { |
| | | |
| | | // ä¼åï¼å¼å
¥æå°å¼ç¨äºæµ®ç¹æ°æ¯è¾ï¼å¤çå ä½ç²¾åº¦è¯¯å·® |
| | | // å¼å
¥æå°å¼ç¨äºæµ®ç¹æ°æ¯è¾ï¼å¤çå ä½ç²¾åº¦è¯¯å·® |
| | | private static final double EPSILON = 1e-6; |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 对å¤å
¬å¼çéæè°ç¨æ¹æ³ (ä¿çå符串å
¥åæ ¼å¼) |
| | | * 对å¤å
¬å¼çéæè°ç¨æ¹æ³ |
| | | * |
| | | * @param boundaryCoordsStr å°åè¾¹çåæ å符串 "x1,y1;x2,y2;..." |
| | | * @param mowingWidthStr å²è宽度å符串ï¼å¦ "0.34" |
| | | * @param mowingWidthStr ææå²è宽度å符串 (å·²å
å«éå ç)ï¼å¦ "0.30" |
| | | * @param safetyMarginStr å®å
¨è¾¹è·å符串ï¼å¦ "0.2" |
| | | * @return è·¯å¾æ®µå表 |
| | | */ |
| | | public static List<PathSegment> planPath(String boundaryCoordsStr, String mowingWidthStr, String safetyMarginStr) { |
| | | // 1. è§£æåæ° (ä¼åï¼åç¬æåè§£æé»è¾) |
| | | // 1. è§£æåæ° |
| | | List<Point> originalPolygon = parseCoords(boundaryCoordsStr); |
| | | double mowingWidth; |
| | | double safetyMargin; |
| | |
| | | } |
| | | |
| | | /** |
| | | * æ ¸å¿ç®æ³é»è¾ (强类åå
¥åï¼ä¾¿äºæµè¯åå
é¨è°ç¨) |
| | | * æ ¸å¿ç®æ³é»è¾ |
| | | */ |
| | | private static List<PathSegment> planPathCore(List<Point> originalPolygon, double mowingWidth, double safetyMargin) { |
| | | // ä¼åï¼ä¸è§å½¢ä¹æ¯åæ³çå¸å¤è¾¹å½¢ï¼éå¶æ¹ä¸ºå°äº3 |
| | | private static List<PathSegment> planPathCore(List<Point> originalPolygon, double width, double safetyMargin) { |
| | | if (originalPolygon == null || originalPolygon.size() < 3) { |
| | | throw new IllegalArgumentException("å¤è¾¹å½¢åæ ç¹ä¸è¶³3ä¸ªï¼æ æ³ææææåºå"); |
| | | return new ArrayList<>(); // ææåºå¼å¸¸ï¼è§ä¸å¡éæ±èå® |
| | | } |
| | | |
| | | // ç¡®ä¿å¤è¾¹å½¢æ¯éæ¶éæ¹å |
| | | ensureCCW(originalPolygon); |
| | | |
| | | // 1. æ ¹æ®å®å
¨è¾¹è·å
缩 |
| | | List<Point> shrunkPolygon = shrinkPolygon(originalPolygon, safetyMargin); |
| | | // 1. æ ¹æ®å®å
¨è¾¹è·å
缩ï¼å¾å°å®é
ä½ä¸åºå |
| | | List<Point> workAreaPolygon = shrinkPolygon(originalPolygon, safetyMargin); |
| | | |
| | | // ä¼åï¼å
缩å妿å¤è¾¹å½¢å¤±æï¼ä¾å¦å°å太çªï¼å
ç¼©åæ¶å¤±ï¼ï¼ç´æ¥è¿å空路å¾ï¼é¿å
åç»æ¥é |
| | | if (shrunkPolygon.size() < 3) { |
| | | // è¿éå¯ä»¥è®°å½æ¥å¿ï¼å°åè¿å°ï¼æ æ³æ»¡è¶³å®å
¨è·ç¦»ä½ä¸ |
| | | // 妿å
缩ååºå失æï¼å¦å°å太å°ï¼ï¼è¿åç©ºè·¯å¾ |
| | | if (workAreaPolygon.size() < 3) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | // 2. è®¡ç®æé¿è¾¹è§åº¦ (ä½ä¸ºæ«ææ¹å) |
| | | double angle = calculateLongestEdgeAngle(originalPolygon); |
| | | List<PathSegment> finalPath = new ArrayList<>(); |
| | | |
| | | // 3. & 4. æè½¬æ«æå¹¶åªè£ |
| | | List<PathSegment> mowingLines = generateClippedMowingLines(shrunkPolygon, angle, mowingWidth); |
| | | // 2. [ä¼å] ä¼å
çæè½®å»è·¯å¾ (Contour Pass) |
| | | // 沿ä½ä¸è¾¹çèµ°ä¸åï¼ç¡®ä¿è¾¹ç¼æ´é½ä¸æ éæ¼ |
| | | addContourPath(workAreaPolygon, finalPath); |
| | | |
| | | // 5. å¼åå½¢è¿æ¥ |
| | | return connectPathSegments(mowingLines); |
| | | } |
| | | // 3. [ä¼å] è®¡ç®æä½³æ«æè§åº¦ |
| | | // 寻æ¾è®©å¤è¾¹å½¢æå½±é«åº¦æå°çè§åº¦ï¼ä»èæå°åè½¬å¼¯æ¬¡æ° |
| | | double bestAngle = findOptimalScanAngle(workAreaPolygon); |
| | | |
| | | // ================= æ ¸å¿ç®æ³è¾
婿¹æ³ ================= |
| | | // 4. çæå
é¨å¼åå½¢è·¯å¾ |
| | | // ç´æ¥ä½¿ç¨ä¼ å
¥ç width (å·²å
å«éå ç) |
| | | List<PathSegment> zigZagPaths = generateClippedMowingLines(workAreaPolygon, bestAngle, width); |
| | | |
| | | private static List<Point> shrinkPolygon(List<Point> polygon, double margin) { |
| | | List<Point> newPoints = new ArrayList<>(); |
| | | int n = polygon.size(); |
| | | |
| | | for (int i = 0; i < n; i++) { |
| | | Point p1 = polygon.get(i); |
| | | Point p2 = polygon.get((i + 1) % n); |
| | | Point p0 = polygon.get((i - 1 + n) % n); |
| | | |
| | | Line line1 = offsetLine(p1, p2, margin); |
| | | Line line0 = offsetLine(p0, p1, margin); |
| | | |
| | | Point intersection = getIntersection(line0, line1); |
| | | // 5. è¿æ¥è½®å»è·¯å¾åå¼åå½¢è·¯å¾ |
| | | if (!finalPath.isEmpty() && !zigZagPaths.isEmpty()) { |
| | | Point contourEnd = finalPath.get(finalPath.size() - 1).end; |
| | | Point zigzagStart = zigZagPaths.get(0).start; |
| | | |
| | | // ä¼åï¼å¢å éç©ºå¤æï¼ä¸å¦æäº¤ç¹å¼å¸¸è¿ï¼å°è§æåºï¼ï¼å®é
å·¥ç¨ä¸å¯è½éè¦åè§å¤ç |
| | | // è¿éæä¿çåºç¡é»è¾ï¼ä½å¨å¸å¤è¾¹å½¢ä¸é常没é®é¢ |
| | | if (intersection != null) { |
| | | newPoints.add(intersection); |
| | | // å¦æè½®å»ç»ç¹ä¸å¼å形起ç¹ä¸éåï¼æ·»å è¿æ¸¡æ®µ |
| | | if (distanceSq(contourEnd, zigzagStart) > EPSILON) { |
| | | finalPath.add(new PathSegment(contourEnd, zigzagStart, false)); |
| | | } |
| | | } |
| | | return newPoints; |
| | | |
| | | // 6. åå¹¶å¼åå½¢è·¯å¾ |
| | | finalPath.addAll(connectPathSegments(zigZagPaths)); |
| | | |
| | | return finalPath; |
| | | } |
| | | |
| | | private static double calculateLongestEdgeAngle(List<Point> polygon) { |
| | | double maxDistSq = -1; |
| | | double angle = 0; |
| | | int n = polygon.size(); |
| | | // ================= æ ¸å¿é»è¾è¾
婿¹æ³ ================= |
| | | |
| | | /** |
| | | * æ·»å è½®å»è·¯å¾ (å´çå¤è¾¹å½¢èµ°ä¸å) |
| | | */ |
| | | private static void addContourPath(List<Point> polygon, List<PathSegment> path) { |
| | | int n = polygon.size(); |
| | | for (int i = 0; i < n; i++) { |
| | | Point p1 = polygon.get(i); |
| | | Point p2 = polygon.get((i + 1) % n); |
| | | double dx = p2.x - p1.x; |
| | | double dy = p2.y - p1.y; |
| | | double distSq = dx * dx + dy * dy; |
| | | path.add(new PathSegment(p1, p2, true)); |
| | | } |
| | | } |
| | | |
| | | if (distSq > maxDistSq) { |
| | | maxDistSq = distSq; |
| | | angle = Math.atan2(dy, dx); |
| | | /** |
| | | * å¯»æ¾æä¼æ«æè§åº¦ (æå°æå½±é«åº¦æ³) |
| | | */ |
| | | private static double findOptimalScanAngle(List<Point> polygon) { |
| | | double minHeight = Double.MAX_VALUE; |
| | | double bestAngle = 0; |
| | | int n = polygon.size(); |
| | | |
| | | // é忝䏿¡è¾¹ï¼è®¡ç®ä»¥è¯¥è¾¹ä¸ºâåºâæ¶ï¼å¤è¾¹å½¢çé«åº¦ |
| | | for (int i = 0; i < n; i++) { |
| | | Point p1 = polygon.get(i); |
| | | Point p2 = polygon.get((i + 1) % n); |
| | | |
| | | // å½åè¾¹çè§åº¦ |
| | | double currentAngle = Math.atan2(p2.y - p1.y, p2.x - p1.x); |
| | | |
| | | // 计ç®å¨è¿ä¸ªè§åº¦ä¸çæå½±é«åº¦ |
| | | double height = calculatePolygonHeightAtAngle(polygon, currentAngle); |
| | | |
| | | if (height < minHeight) { |
| | | minHeight = height; |
| | | bestAngle = currentAngle; |
| | | } |
| | | } |
| | | return angle; |
| | | return bestAngle; |
| | | } |
| | | |
| | | /** |
| | | * 计ç®å¤è¾¹å½¢å¨ç¹å®æè½¬è§åº¦ä¸çYè½´æå½±é«åº¦ |
| | | */ |
| | | private static double calculatePolygonHeightAtAngle(List<Point> poly, double angle) { |
| | | double minY = Double.MAX_VALUE; |
| | | double maxY = -Double.MAX_VALUE; |
| | | |
| | | double cos = Math.cos(-angle); |
| | | double sin = Math.sin(-angle); |
| | | |
| | | for (Point p : poly) { |
| | | // åªéè®¡ç®æè½¬åçYåæ |
| | | double rotatedY = p.x * sin + p.y * cos; |
| | | if (rotatedY < minY) minY = rotatedY; |
| | | if (rotatedY > maxY) maxY = rotatedY; |
| | | } |
| | | return maxY - minY; |
| | | } |
| | | |
| | | private static List<PathSegment> generateClippedMowingLines(List<Point> polygon, double angle, double width) { |
| | | List<PathSegment> segments = new ArrayList<>(); |
| | | |
| | | // æè½¬è³æ°´å¹³ |
| | | |
| | | // æè½¬å¤è¾¹å½¢è³æ°´å¹³ |
| | | List<Point> rotatedPoly = rotatePolygon(polygon, -angle); |
| | | |
| | | double minY = Double.MAX_VALUE; |
| | |
| | | if (p.y > maxY) maxY = p.y; |
| | | } |
| | | |
| | | // ä¼åï¼èµ·å§æ«æçº¿å¢å ä¸ä¸ªå¾®å°çåç§»é EPSILON |
| | | // é¿å
æ«æçº¿æ£å¥½è½å¨é¡¶ç¹ä¸ï¼å¯¼è´äº¤ç¹å¤æé»è¾åºç°äºä¹æ§ |
| | | // èµ·å§æ«æçº¿ä½ç½®ï¼ |
| | | // ä» minY + width/2 å¼å§ï¼å 为ä¹åå·²ç»èµ°äºè½®å»çº¿(Contour Pass)ã |
| | | // è½®å»çº¿è´è´£æ¸
çè¾¹ç¼åºåï¼å
é¨å¡«å
çº¿ä¿æ width çé´è·å³å¯ã |
| | | // å ä¸ EPSILON 鲿¢æµ®ç¹æ°å好è½å¨è¾¹çä¸å¯¼è´çå¤æè¯¯å·® |
| | | double currentY = minY + width / 2.0 + EPSILON; |
| | | |
| | | while (currentY < maxY) { |
| | | List<Double> xIntersections = new ArrayList<>(); |
| | | int n = rotatedPoly.size(); |
| | | |
| | | |
| | | for (int i = 0; i < n; i++) { |
| | | Point p1 = rotatedPoly.get(i); |
| | | Point p2 = rotatedPoly.get((i + 1) % n); |
| | | |
| | | // ä¼åï¼å¿½ç¥æ°´å¹³çº¿æ®µ (p1.y == p2.y)ï¼é¿å
é¤é¶éè¯¯ï¼æ°´å¹³çº¿æ®µä¸åä¸åç´æ«æçº¿æ±äº¤ |
| | | // å¿½ç¥æ°´å¹³çº¿æ®µ |
| | | if (Math.abs(p1.y - p2.y) < EPSILON) continue; |
| | | |
| | | // å¤æçº¿æ®µæ¯å¦è·¨è¶ currentY |
| | | // 使ç¨ä¸¥æ ¼çä¸çå¼é»è¾é
åèå´å¤æ |
| | | double minP = Math.min(p1.y, p2.y); |
| | | double maxP = Math.max(p1.y, p2.y); |
| | | |
| | | if (currentY >= minP && currentY < maxP) { |
| | | // çº¿æ§æå¼æ±X |
| | | double x = p1.x + (currentY - p1.y) * (p2.x - p1.x) / (p2.y - p1.y); |
| | | xIntersections.add(x); |
| | | } |
| | |
| | | |
| | | Collections.sort(xIntersections); |
| | | |
| | | // æå线段ï¼éå¸¸æ¯æå¯¹åºç° |
| | | if (xIntersections.size() >= 2) { |
| | | // åæå·¦åæå³ç¹è¿æ¥ï¼åºå¯¹å¯è½åºç°çå¾®å°è®¡ç®è¯¯å·®å¯¼è´çå¤ä¸ªäº¤ç¹ï¼ |
| | | // åæå·¦åæå³äº¤ç¹ |
| | | double xStart = xIntersections.get(0); |
| | | double xEnd = xIntersections.get(xIntersections.size() - 1); |
| | | |
| | | // åªæå½çº¿æ®µé¿åº¦å¤§äºæå°å¼æ¶ææ·»å ï¼é¿å
çæåªç¹è·¯å¾ |
| | | if (xEnd - xStart > EPSILON) { |
| | | Point rStart = rotatePoint(new Point(xStart, currentY), angle); // è¿éçcurrentYå®é
ä¸å¸¦äºepsilonï¼è¿åæ¶æ²¡é®é¢ |
| | | // ååæè½¬åååæ ç³» |
| | | Point rStart = rotatePoint(new Point(xStart, currentY), angle); |
| | | Point rEnd = rotatePoint(new Point(xEnd, currentY), angle); |
| | | segments.add(new PathSegment(rStart, rEnd, true)); |
| | | } |
| | | } |
| | | |
| | | // æ¥è¿ |
| | | currentY += width; |
| | | } |
| | | |
| | |
| | | // æ·»å è¿æ¸¡æ®µ |
| | | if (i > 0) { |
| | | Point prevEnd = result.get(result.size() - 1).end; |
| | | // åªæå½è·ç¦»ç¡®å®å卿¶ææ·»å è¿æ¸¡æ®µï¼é¿å
éåç¹ï¼ |
| | | if (distanceSq(prevEnd, actualStart) > EPSILON) { |
| | | result.add(new PathSegment(prevEnd, actualStart, false)); |
| | | } |
| | |
| | | return result; |
| | | } |
| | | |
| | | // ================= åºç¡å ä½å·¥å
· (ä¼åç) ================= |
| | | // ================= åºç¡å ä½å·¥å
· ================= |
| | | |
| | | private static List<Point> parseCoords(String s) { |
| | | List<Point> list = new ArrayList<>(); |
| | | if (s == null || s.trim().isEmpty()) return list; |
| | | |
| | | |
| | | String[] parts = s.split(";"); |
| | | for (String part : parts) { |
| | | String[] xy = part.split(","); |
| | |
| | | double y = Double.parseDouble(xy[1].trim()); |
| | | list.add(new Point(x, y)); |
| | | } catch (NumberFormatException e) { |
| | | // å¿½ç¥æ ¼å¼é误çåä¸ªç¹ |
| | | // å¿½ç¥æ ¼å¼é误 |
| | | } |
| | | } |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | // Shoelaceå
¬å¼å¤ææ¹åå¹¶è°æ´ |
| | | private static void ensureCCW(List<Point> polygon) { |
| | | double sum = 0; |
| | | for (int i = 0; i < polygon.size(); i++) { |
| | |
| | | Point p2 = polygon.get((i + 1) % polygon.size()); |
| | | sum += (p2.x - p1.x) * (p2.y + p1.y); |
| | | } |
| | | // å设æ åç¬å¡å°åæ ç³»ï¼sum > 0 为顺æ¶éï¼éè¦åè½¬ä¸ºéæ¶é |
| | | if (sum > 0) { |
| | | Collections.reverse(polygon); |
| | | } |
| | | } |
| | | |
| | | private static List<Point> shrinkPolygon(List<Point> polygon, double margin) { |
| | | List<Point> newPoints = new ArrayList<>(); |
| | | int n = polygon.size(); |
| | | |
| | | for (int i = 0; i < n; i++) { |
| | | Point p1 = polygon.get(i); |
| | | Point p2 = polygon.get((i + 1) % n); |
| | | Point p0 = polygon.get((i - 1 + n) % n); |
| | | |
| | | Line line1 = offsetLine(p1, p2, margin); |
| | | Line line0 = offsetLine(p0, p1, margin); |
| | | |
| | | Point intersection = getIntersection(line0, line1); |
| | | if (intersection != null) { |
| | | newPoints.add(intersection); |
| | | } |
| | | } |
| | | return newPoints; |
| | | } |
| | | |
| | | private static class Line { |
| | | double a, b, c; |
| | | double a, b, c; |
| | | public Line(double a, double b, double c) { this.a = a; this.b = b; this.c = c; } |
| | | } |
| | | |
| | |
| | | double dx = p2.x - p1.x; |
| | | double dy = p2.y - p1.y; |
| | | double len = Math.sqrt(dx * dx + dy * dy); |
| | | |
| | | // 鲿¢é¤ä»¥0 |
| | | |
| | | if (len < EPSILON) return new Line(0, 0, 0); |
| | | |
| | | double nx = -dy / len; |
| | | double ny = dx / len; |
| | | |
| | | // å左侧平移ï¼åè®¾éæ¶éï¼ |
| | | double newX = p1.x + nx * dist; |
| | | double newY = p1.y + ny * dist; |
| | | |
| | |
| | | |
| | | private static Point getIntersection(Line l1, Line l2) { |
| | | double det = l1.a * l2.b - l2.a * l1.b; |
| | | if (Math.abs(det) < EPSILON) return null; // å¹³è¡ |
| | | if (Math.abs(det) < EPSILON) return null; |
| | | double x = (l1.b * l2.c - l2.b * l1.c) / det; |
| | | double y = (l2.a * l1.c - l1.a * l2.c) / det; |
| | | return new Point(x, y); |
| | |
| | | } |
| | | return res; |
| | | } |
| | | |
| | | |
| | | private static double distanceSq(Point p1, Point p2) { |
| | | return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y); |
| | | } |
| | |
| | | // å¼å½¢å°åï¼æ éç¢ç© -> è°ç¨ YixinglujingNoObstacle |
| | | // 注æï¼å¦æè¯¥ç±»è¿æ²¡æå®ç°ï¼è¿é伿åºå¼å¸¸æè¿ånull |
| | | try { |
| | | // å设 YixinglujingNoObstacle æç±»ä¼¼çæ¹æ³ç¾å |
| | | // å¦æç±»è¿æ²¡æå®ç°ï¼å¯è½éè¦ä½¿ç¨åæ¥çæ¹æ³ä½ä¸ºåå¤ |
| | | generated = YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); |
| | | // è°ç¨ YixinglujingNoObstacle.planPath è·åè·¯å¾æ®µå表 |
| | | List<YixinglujingNoObstacle.PathSegment> segments = |
| | | YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); |
| | | // æ ¼å¼åè·¯å¾æ®µå表为å符串 |
| | | generated = formatYixingPathSegments(segments); |
| | | } catch (Exception e) { |
| | | // å¦æç±»è¿æ²¡æå®ç°ï¼ä½¿ç¨åæ¥çæ¹æ³ä½ä¸ºåå¤ |
| | | if (showMessages) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * æ ¼å¼å YixinglujingNoObstacle.PathSegment åè¡¨ä¸ºåæ å符串 |
| | | */ |
| | | private String formatYixingPathSegments(List<YixinglujingNoObstacle.PathSegment> segments) { |
| | | if (segments == null || segments.isEmpty()) { |
| | | return ""; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | YixinglujingNoObstacle.Point last = null; |
| | | for (YixinglujingNoObstacle.PathSegment segment : segments) { |
| | | // åªæ·»å å²è工使®µï¼è·³è¿è¿æ¸¡æ®µ |
| | | if (segment.isMowing) { |
| | | // å¦æèµ·ç¹ä¸ä¸ä¸ä¸ªç»ç¹ä¸åï¼æ·»å èµ·ç¹ |
| | | if (last == null || !equalsYixingPoint(last, segment.start)) { |
| | | appendYixingPoint(sb, segment.start); |
| | | } |
| | | // æ·»å ç»ç¹ |
| | | appendYixingPoint(sb, segment.end); |
| | | last = segment.end; |
| | | } |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * æ¯è¾ä¸¤ä¸ªç¹æ¯å¦ç¸åï¼ä½¿ç¨å°çå®¹å·®ï¼ |
| | | */ |
| | | private boolean equals2D(AoxinglujingNoObstacle.Point p1, AoxinglujingNoObstacle.Point p2) { |
| | |
| | | sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y)); |
| | | } |
| | | |
| | | /** |
| | | * æ¯è¾ä¸¤ä¸ª YixinglujingNoObstacle.Point æ¯å¦ç¸åï¼ä½¿ç¨å°çå®¹å·®ï¼ |
| | | */ |
| | | private boolean equalsYixingPoint(YixinglujingNoObstacle.Point p1, YixinglujingNoObstacle.Point p2) { |
| | | if (p1 == null || p2 == null) { |
| | | return p1 == p2; |
| | | } |
| | | double tolerance = 1e-6; |
| | | return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance; |
| | | } |
| | | |
| | | /** |
| | | * æ·»å YixinglujingNoObstacle.Point å°å符串æå»ºå¨ |
| | | */ |
| | | private void appendYixingPoint(StringBuilder sb, YixinglujingNoObstacle.Point point) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format(Locale.US, "%.6f,%.6f", point.x, point.y)); |
| | | } |
| | | |
| | | // ========== UIè¾
婿¹æ³ ========== |
| | | |
| | | private JTextArea createInfoTextArea(String text, boolean editable, int rows) { |
| | |
| | | package lujing; |
| | | |
| | | import java.util.List; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * æ éç¢ç©å¼å½¢å°åè·¯å¾è§åç±» |
| | | * å¼å½¢ï¼æ éç¢ç©ï¼èå°è·¯å¾è§åç±» - ä¼åç V2.0 |
| | | * * åè½ç¹ç¹ï¼ |
| | | * 1. èªå¨å¤çå¹å¤è¾¹å½¢ï¼éè¿è³åæ³åå²ï¼ |
| | | * 2. å¢å âå´è¾¹âè·¯å¾ï¼ä¿è¯è¾¹ç¼å²èæ´æ´ |
| | | * 3. èªå¨è®¡ç®æ¯ä¸ªååºåçæä¼æ«æè§åº¦ï¼åå°æå¤´æ¬¡æ°ï¼ |
| | | * 4. æºè½åºåè¿æ¥ï¼æ¯æååè·¯å¾éæ©ï¼ |
| | | */ |
| | | public class YixinglujingNoObstacle { |
| | | |
| | | |
| | | // ========================================== |
| | | // 坹夿¥å£ |
| | | // ========================================== |
| | | |
| | | /** |
| | | * çæè·¯å¾ |
| | | * @param boundaryCoordsStr å°åè¾¹çåæ å符串 "x1,y1;x2,y2;..." |
| | | * @param mowingWidthStr å²è宽度å符串ï¼å¦ "0.34" |
| | | * @param safetyMarginStr å®å
¨è¾¹è·å符串ï¼å¦ "0.2" |
| | | * @return è·¯å¾åæ åç¬¦ä¸²ï¼æ ¼å¼ "x1,y1;x2,y2;..." |
| | | * è§åå¼å½¢èå°å²èè·¯å¾ |
| | | * |
| | | * @param coordinates å°åè¾¹çåæ åç¬¦ä¸²ï¼æ ¼å¼ï¼"x1,y1;x2,y2;x3,y3;..." |
| | | * @param widthStr å²è宽度ï¼ç±³ï¼ï¼å¦ "0.34" |
| | | * @param marginStr å®å
¨è¾¹è·ï¼ç±³ï¼ï¼å¦ "0.2" |
| | | * @return è·¯å¾æ®µå表 |
| | | */ |
| | | public static String planPath(String boundaryCoordsStr, String mowingWidthStr, String safetyMarginStr) { |
| | | // TODO: å®ç°å¼å½¢å°åæ éç¢ç©è·¯å¾è§åç®æ³ |
| | | // ç®å使ç¨é»è®¤æ¹æ³ä½ä¸ºä¸´æ¶å®ç° |
| | | throw new UnsupportedOperationException("YixinglujingNoObstacle.planPath å°æªå®ç°"); |
| | | public static List<PathSegment> planPath(String coordinates, String widthStr, String marginStr) { |
| | | // 1. åæ°è§£æä¸é¢å¤ç |
| | | List<Point> rawPoints = parseCoordinates(coordinates); |
| | | if (rawPoints.size() < 3) { |
| | | throw new IllegalArgumentException("å¤è¾¹å½¢ç¹æ°ä¸è¶³ï¼æ æ³ææå°å"); |
| | | } |
| | | // ç¡®ä¿éæ¶é顺åºï¼æ¹ä¾¿åç»å ä½è®¡ç® |
| | | ensureCounterClockwise(rawPoints); |
| | | |
| | | double mowWidth = Double.parseDouble(widthStr); |
| | | double safeMargin = Double.parseDouble(marginStr); |
| | | |
| | | List<PathSegment> finalPath = new ArrayList<>(); |
| | | |
| | | // 2. çæå´è¾¹è·¯å¾ (Contour Path) |
| | | // è¿ä¸æ¥å
è§åä¸åè½®å»ï¼è§£å³å¼å½¢è¾¹ç¼é¾å¤ççé®é¢ |
| | | List<Point> contourPoly = getInsetPolygon(rawPoints, safeMargin); |
| | | |
| | | // 妿å
缩åé¢ç§¯å¤ªå°æç¹æ°ä¸è¶³ï¼ç´æ¥è¿å空 |
| | | if (contourPoly.size() < 3) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | // å°å´è¾¹è·¯å¾å å
¥ç»æ |
| | | for (int i = 0; i < contourPoly.size(); i++) { |
| | | Point p1 = contourPoly.get(i); |
| | | Point p2 = contourPoly.get((i + 1) % contourPoly.size()); |
| | | finalPath.add(new PathSegment(p1, p2, true)); // true = å²è |
| | | } |
| | | |
| | | // è®°å½å´è¾¹ç»æåçä½ç½®ï¼é常åå°å´è¾¹èµ·ç¹ï¼ |
| | | Point endOfContour = contourPoly.get(0); |
| | | |
| | | // 3. åºååå² (Decomposition) |
| | | // 使ç¨è³åæ³å°å´è¾¹åçå¤è¾¹å½¢åå²ä¸ºå¤ä¸ªå¸å¤è¾¹å½¢ï¼ä¸è§å½¢ï¼ |
| | | // è¿æ ·å¯ä»¥ä¿è¯è¦çæ éæ¼ |
| | | List<List<Point>> triangles = triangulatePolygon(contourPoly); |
| | | |
| | | // 4. 对æ¯ä¸ªåºåçæå
é¨å¡«å
è·¯å¾ |
| | | List<List<PathSegment>> allRegionPaths = new ArrayList<>(); |
| | | |
| | | for (List<Point> triangle : triangles) { |
| | | // ãä¼åãå¯»æ¾æä¼æ«æè§åº¦ï¼ |
| | | // éåä¸è§å½¢ç䏿¡è¾¹ï¼è®¡ç®ä»¥åªæ¡è¾¹ä¸ºåºåæ«ææ¶ï¼çæçè¡æ°æå°ï¼è½¬å¼¯æå°ï¼ |
| | | List<PathSegment> regionPath = planConvexPathOptimal(triangle, mowWidth); |
| | | if (!regionPath.isEmpty()) { |
| | | allRegionPaths.add(regionPath); |
| | | } |
| | | } |
| | | |
| | | // 5. è¿æ¥ææå
é¨åºå (Greedy Connection) |
| | | // ä»å´è¾¹ç»æç¹å¼å§ï¼å¯»æ¾æè¿çä¸ä¸ä¸ªåºå |
| | | List<PathSegment> internalPaths = connectRegions(allRegionPaths, endOfContour); |
| | | finalPath.addAll(internalPaths); |
| | | |
| | | return finalPath; |
| | | } |
| | | } |
| | | |
| | | // ========================================== |
| | | // æ ¸å¿è§åç®æ³ |
| | | // ========================================== |
| | | |
| | | /** |
| | | * è§åå¸å¤è¾¹å½¢è·¯å¾ï¼èªå¨éæ©æä¼è§åº¦ |
| | | */ |
| | | private static List<PathSegment> planConvexPathOptimal(List<Point> polygon, double width) { |
| | | if (polygon.size() < 3) return new ArrayList<>(); |
| | | |
| | | double bestAngle = 0; |
| | | double minLines = Double.MAX_VALUE; |
| | | |
| | | // éåå¤è¾¹å½¢çæ¯ä¸æ¡è¾¹ï¼å°è¯ä»¥è¯¥è¾¹è§åº¦è¿è¡æ«æ |
| | | for (int i = 0; i < polygon.size(); i++) { |
| | | Point p1 = polygon.get(i); |
| | | Point p2 = polygon.get((i + 1) % polygon.size()); |
| | | |
| | | // 计ç®è¾¹çè§åº¦ |
| | | double angle = Math.atan2(p2.y - p1.y, p2.x - p1.x); |
| | | |
| | | // 计ç®å¨è¿ä¸ªè§åº¦ä¸ï¼å¤è¾¹å½¢çåç´æå½±é«åº¦ |
| | | // é«åº¦è¶å°ï¼æå³çæ²¿æ¤æ¹åæ«æçè¡æ°è¶å°ï¼æçè¶é« |
| | | double height = calculatePolygonHeight(polygon, -angle); |
| | | |
| | | if (height < minLines) { |
| | | minLines = height; |
| | | bestAngle = angle; |
| | | } |
| | | } |
| | | |
| | | // ä½¿ç¨æä½³è§åº¦çæè·¯å¾ |
| | | return generatePathWithAngle(polygon, width, bestAngle); |
| | | } |
| | | |
| | | /** |
| | | * æ ¹æ®æå®è§åº¦çæå¼åå½¢è·¯å¾ |
| | | */ |
| | | private static List<PathSegment> generatePathWithAngle(List<Point> polygon, double width, double angle) { |
| | | // 1. å°å¤è¾¹å½¢æè½¬å°æ°´å¹³ä½ç½® |
| | | List<Point> rotatedPoints = new ArrayList<>(); |
| | | for (Point p : polygon) { |
| | | rotatedPoints.add(rotatePoint(p, -angle)); |
| | | } |
| | | |
| | | // 2. 计ç®Yè½´èå´ |
| | | double minY = Double.MAX_VALUE; |
| | | double maxY = -Double.MAX_VALUE; |
| | | for (Point p : rotatedPoints) { |
| | | minY = Math.min(minY, p.y); |
| | | maxY = Math.max(maxY, p.y); |
| | | } |
| | | |
| | | List<PathSegment> segments = new ArrayList<>(); |
| | | boolean leftToRight = true; |
| | | |
| | | // 3. æ«æçº¿çæ (ä» minY + width/2 å¼å§ï¼ä¿è¯ç¬¬ä¸ååå¨å¤è¾¹å½¢å
) |
| | | for (double y = minY + width / 2; y <= maxY; y += width) { |
| | | List<Double> intersections = new ArrayList<>(); |
| | | for (int i = 0; i < rotatedPoints.size(); i++) { |
| | | Point p1 = rotatedPoints.get(i); |
| | | Point p2 = rotatedPoints.get((i + 1) % rotatedPoints.size()); |
| | | |
| | | // å¤ææ«æçº¿æ¯å¦ç©¿è¿è¾¹ |
| | | if ((p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y)) { |
| | | double x = p1.x + (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y); |
| | | intersections.add(x); |
| | | } |
| | | } |
| | | Collections.sort(intersections); |
| | | |
| | | // æå¯¹çæçº¿æ®µ |
| | | for (int k = 0; k < intersections.size() - 1; k += 2) { |
| | | double x1 = leftToRight ? intersections.get(k) : intersections.get(k + 1); |
| | | double x2 = leftToRight ? intersections.get(k + 1) : intersections.get(k); |
| | | |
| | | Point start = new Point(x1, y); |
| | | Point end = new Point(x2, y); |
| | | |
| | | // æè½¬ååå§åæ ç³» |
| | | Point originalStart = rotatePoint(start, angle); |
| | | Point originalEnd = rotatePoint(end, angle); |
| | | |
| | | // è¿æ¥é»è¾ï¼å¦æä¸æ¯ç¬¬ä¸æ®µï¼éè¦ä»ä¸ä¸æ®µç»ç¹è¿è¿æ¥ |
| | | if (!segments.isEmpty()) { |
| | | PathSegment prev = segments.get(segments.size() - 1); |
| | | // æ·»å è¿æ¥çº¿ï¼é常ç®ä½å²èè·¯å¾çä¸é¨åï¼ä¿æå¼åå½¢è¿ç»ï¼ |
| | | segments.add(new PathSegment(prev.end, originalStart, true)); |
| | | } |
| | | |
| | | segments.add(new PathSegment(originalStart, originalEnd, true)); |
| | | } |
| | | leftToRight = !leftToRight; // æ¢å |
| | | } |
| | | |
| | | return segments; |
| | | } |
| | | |
| | | /** |
| | | * è¿æ¥ææåå²åçåºå (è´ªå¿çç¥ + ååä¼å) |
| | | */ |
| | | private static List<PathSegment> connectRegions(List<List<PathSegment>> regions, Point startPoint) { |
| | | List<PathSegment> result = new ArrayList<>(); |
| | | if (regions.isEmpty()) return result; |
| | | |
| | | List<List<PathSegment>> remaining = new ArrayList<>(regions); |
| | | Point currentPos = startPoint; |
| | | |
| | | while (!remaining.isEmpty()) { |
| | | int bestIndex = -1; |
| | | double minDist = Double.MAX_VALUE; |
| | | boolean needReverse = false; |
| | | |
| | | // 寻æ¾ç¦»å½åä½ç½®æè¿çåºåèµ·ç¹æç»ç¹ |
| | | for (int i = 0; i < remaining.size(); i++) { |
| | | List<PathSegment> region = remaining.get(i); |
| | | Point pStart = region.get(0).start; |
| | | Point pEnd = region.get(region.size() - 1).end; |
| | | |
| | | double dStart = distance(currentPos, pStart); |
| | | double dEnd = distance(currentPos, pEnd); |
| | | |
| | | // æ£æ¥æ£åè¿å
¥ |
| | | if (dStart < minDist) { |
| | | minDist = dStart; |
| | | bestIndex = i; |
| | | needReverse = false; |
| | | } |
| | | // æ£æ¥ååè¿å
¥ï¼åçå²è妿æ´è¿ï¼ |
| | | if (dEnd < minDist) { |
| | | minDist = dEnd; |
| | | bestIndex = i; |
| | | needReverse = true; |
| | | } |
| | | } |
| | | |
| | | if (bestIndex != -1) { |
| | | List<PathSegment> targetRegion = remaining.remove(bestIndex); |
| | | |
| | | if (needReverse) { |
| | | // å转该åºåçææè·¯å¾ |
| | | List<PathSegment> reversedRegion = new ArrayList<>(); |
| | | for (int k = targetRegion.size() - 1; k >= 0; k--) { |
| | | PathSegment seg = targetRegion.get(k); |
| | | // 交æ¢èµ·ç¹ç»ç¹ |
| | | reversedRegion.add(new PathSegment(seg.end, seg.start, seg.isMowing)); |
| | | } |
| | | targetRegion = reversedRegion; |
| | | } |
| | | |
| | | // æ·»å è¿æ¸¡è·¯å¾ï¼æ¬åç§»å¨ï¼isMowing=falseï¼ |
| | | Point nextStart = targetRegion.get(0).start; |
| | | // åªæè·ç¦»æ¾èææ·»å ç§»å¨æ®µ |
| | | if (distance(currentPos, nextStart) > 0.01) { |
| | | result.add(new PathSegment(currentPos, nextStart, false)); |
| | | } |
| | | |
| | | result.addAll(targetRegion); |
| | | currentPos = targetRegion.get(targetRegion.size() - 1).end; |
| | | } else { |
| | | break; // é²å¾¡æ§ä»£ç |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | // ========================================== |
| | | // å ä½è¿ç®è¾
婿¹æ³ |
| | | // ========================================== |
| | | |
| | | /** |
| | | * å
缩å¤è¾¹å½¢ (åºäºè§å¹³å线) |
| | | */ |
| | | private static List<Point> getInsetPolygon(List<Point> points, double margin) { |
| | | List<Point> result = new ArrayList<>(); |
| | | int n = points.size(); |
| | | |
| | | for (int i = 0; i < n; i++) { |
| | | Point pPrev = points.get((i - 1 + n) % n); |
| | | Point pCurr = points.get(i); |
| | | Point pNext = points.get((i + 1) % n); |
| | | |
| | | Point v1 = new Point(pCurr.x - pPrev.x, pCurr.y - pPrev.y); |
| | | Point v2 = new Point(pNext.x - pCurr.x, pNext.y - pCurr.y); |
| | | |
| | | double len1 = Math.hypot(v1.x, v1.y); |
| | | double len2 = Math.hypot(v2.x, v2.y); |
| | | |
| | | if (len1 < 1e-6 || len2 < 1e-6) continue; |
| | | |
| | | // å½ä¸ååé |
| | | Point n1 = new Point(v1.x / len1, v1.y / len1); |
| | | Point n2 = new Point(v2.x / len2, v2.y / len2); |
| | | |
| | | // 计ç®å¹³å线æ¹å |
| | | // v1åå + v2 |
| | | Point bisector = new Point(-n1.x + n2.x, -n1.y + n2.y); |
| | | double biLen = Math.hypot(bisector.x, bisector.y); |
| | | |
| | | // 计ç®åè§ sin(theta/2) |
| | | double cross = n1.x * n2.y - n1.y * n2.x; // åç§¯å¤æè½¬å |
| | | |
| | | // é»è®¤å左侧å
缩 (CCWå¤è¾¹å½¢) |
| | | if (biLen < 1e-6) { |
| | | // å
±çº¿ï¼æ²¿æ³çº¿æ¹å |
| | | bisector = new Point(-n1.y, n1.x); |
| | | } else { |
| | | bisector.x /= biLen; |
| | | bisector.y /= biLen; |
| | | } |
| | | |
| | | // 计ç®åç§»è·ç¦» |
| | | double dot = n1.x * n2.x + n1.y * n2.y; |
| | | double angle = Math.acos(Math.max(-1, Math.min(1, dot))); |
| | | double dist = margin / Math.sin(angle / 2.0); |
| | | |
| | | // æ¹åä¿®æ£ï¼ç¡®ä¿å¹³å线æåå¤è¾¹å½¢å
é¨ï¼éæ¶éå¤è¾¹å½¢çå·¦ä¾§ï¼ |
| | | Point leftNormal = new Point(-n1.y, n1.x); |
| | | if (bisector.x * leftNormal.x + bisector.y * leftNormal.y < 0) { |
| | | bisector.x = -bisector.x; |
| | | bisector.y = -bisector.y; |
| | | } |
| | | |
| | | // 妿æ¯å¹è§ï¼cross < 0ï¼ï¼å¹³å线æåå¤é¨ï¼è·ç¦»éè¦å转æè
ç¹æ®å¤ç |
| | | // ç®åå¤çï¼å¯¹äºå¹è§ï¼åç§»ç¹å®é
ä¸ä¼è¿ç¦»åç¹ï¼ä¸è¿°é»è¾é常è½è¦çï¼ |
| | | // ä½æç«¯éè§å¯è½å¯¼è´distè¿å¤§ãæ¤å¤åç®åæªæä¿æ¤æ¯ä¸å¤çï¼ |
| | | // ä½é对ä¸è¬èå°å½¢ç¶ï¼æ¤é»è¾å¯ç¨ã |
| | | |
| | | result.add(new Point(pCurr.x + bisector.x * dist, pCurr.y + bisector.y * dist)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * è³åæ³åå²å¤è¾¹å½¢ |
| | | */ |
| | | private static List<List<Point>> triangulatePolygon(List<Point> poly) { |
| | | List<List<Point>> triangles = new ArrayList<>(); |
| | | List<Point> remaining = new ArrayList<>(poly); |
| | | |
| | | int maxIter = remaining.size() * 3; |
| | | int iter = 0; |
| | | |
| | | while (remaining.size() > 3 && iter++ < maxIter) { |
| | | int n = remaining.size(); |
| | | boolean earFound = false; |
| | | |
| | | for (int i = 0; i < n; i++) { |
| | | Point prev = remaining.get((i - 1 + n) % n); |
| | | Point curr = remaining.get(i); |
| | | Point next = remaining.get((i + 1) % n); |
| | | |
| | | if (isConvex(prev, curr, next)) { |
| | | boolean hasPoint = false; |
| | | for (int j = 0; j < n; j++) { |
| | | if (j == i || j == (i - 1 + n) % n || j == (i + 1) % n) continue; |
| | | if (isPointInTriangle(remaining.get(j), prev, curr, next)) { |
| | | hasPoint = true; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (!hasPoint) { |
| | | List<Point> tri = new ArrayList<>(); |
| | | tri.add(prev); tri.add(curr); tri.add(next); |
| | | triangles.add(tri); |
| | | remaining.remove(i); |
| | | earFound = true; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | if (!earFound) break; |
| | | } |
| | | |
| | | if (remaining.size() == 3) { |
| | | triangles.add(remaining); |
| | | } |
| | | return triangles; |
| | | } |
| | | |
| | | private static double calculatePolygonHeight(List<Point> poly, double angle) { |
| | | double minY = Double.MAX_VALUE; |
| | | double maxY = -Double.MAX_VALUE; |
| | | for (Point p : poly) { |
| | | Point r = rotatePoint(p, angle); |
| | | minY = Math.min(minY, r.y); |
| | | maxY = Math.max(maxY, r.y); |
| | | } |
| | | return maxY - minY; |
| | | } |
| | | |
| | | private static Point rotatePoint(Point p, double angle) { |
| | | double cos = Math.cos(angle); |
| | | double sin = Math.sin(angle); |
| | | return new Point(p.x * cos - p.y * sin, p.x * sin + p.y * cos); |
| | | } |
| | | |
| | | private static boolean isConvex(Point a, Point b, Point c) { |
| | | return (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x) >= 0; |
| | | } |
| | | |
| | | private static boolean isPointInTriangle(Point p, Point a, Point b, Point c) { |
| | | double areaABC = Math.abs((a.x*(b.y-c.y) + b.x*(c.y-a.y) + c.x*(a.y-b.y))/2.0); |
| | | double areaPBC = Math.abs((p.x*(b.y-c.y) + b.x*(c.y-p.y) + c.x*(p.y-b.y))/2.0); |
| | | double areaPAC = Math.abs((a.x*(p.y-c.y) + p.x*(c.y-a.y) + c.x*(a.y-p.y))/2.0); |
| | | double areaPAB = Math.abs((a.x*(b.y-p.y) + b.x*(p.y-a.y) + p.x*(a.y-b.y))/2.0); |
| | | return Math.abs(areaABC - (areaPBC + areaPAC + areaPAB)) < 1e-6; |
| | | } |
| | | |
| | | private static List<Point> parseCoordinates(String coordinates) { |
| | | List<Point> points = new ArrayList<>(); |
| | | String cleanStr = coordinates.replaceAll("[()\\[\\]{}]", "").trim(); |
| | | String[] pairs = cleanStr.split(";"); |
| | | for (String pair : pairs) { |
| | | pair = pair.trim(); |
| | | if (pair.isEmpty()) continue; |
| | | String[] xy = pair.split(","); |
| | | if (xy.length == 2) { |
| | | points.add(new Point(Double.parseDouble(xy[0].trim()), Double.parseDouble(xy[1].trim()))); |
| | | } |
| | | } |
| | | return points; |
| | | } |
| | | |
| | | private static void ensureCounterClockwise(List<Point> points) { |
| | | double sum = 0; |
| | | for (int i = 0; i < points.size(); i++) { |
| | | Point p1 = points.get(i); |
| | | Point p2 = points.get((i + 1) % points.size()); |
| | | sum += (p2.x - p1.x) * (p2.y + p1.y); |
| | | } |
| | | if (sum > 0) { |
| | | Collections.reverse(points); |
| | | } |
| | | } |
| | | |
| | | private static double distance(Point p1, Point p2) { |
| | | return Math.hypot(p1.x - p2.x, p1.y - p2.y); |
| | | } |
| | | |
| | | // ========================================== |
| | | // å
鍿°æ®ç»æ |
| | | // ========================================== |
| | | |
| | | public static class Point { |
| | | public double x, y; |
| | | public Point(double x, double y) { this.x = x; this.y = y; } |
| | | @Override public String toString() { return String.format("%.2f,%.2f", x, y); } |
| | | } |
| | | |
| | | public static class PathSegment { |
| | | public Point start; |
| | | public Point end; |
| | | public boolean isMowing; |
| | | |
| | | public PathSegment(Point start, Point end, boolean isMowing) { |
| | | this.start = start; |
| | | this.end = end; |
| | | this.isMowing = isMowing; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return String.format("[%s -> %s, å²è:%b]", start, end, isMowing); |
| | | } |
| | | } |
| | | } |
| | |
| | | final boolean success; |
| | | final String errorMessage; |
| | | final String originalBoundary; |
| | | final String originalBoundaryXY; |
| | | final String optimizedBoundary; |
| | | final String areaSqMeters; |
| | | final String baseStationCoordinates; |
| | |
| | | private BoundarySnapshotResult(boolean success, |
| | | String errorMessage, |
| | | String originalBoundary, |
| | | String originalBoundaryXY, |
| | | String optimizedBoundary, |
| | | String areaSqMeters, |
| | | String baseStationCoordinates, |
| | |
| | | this.success = success; |
| | | this.errorMessage = errorMessage; |
| | | this.originalBoundary = originalBoundary; |
| | | this.originalBoundaryXY = originalBoundaryXY; |
| | | this.optimizedBoundary = optimizedBoundary; |
| | | this.areaSqMeters = areaSqMeters; |
| | | this.baseStationCoordinates = baseStationCoordinates; |
| | | this.messageType = messageType; |
| | | } |
| | | |
| | | static BoundarySnapshotResult success(String original, String optimized, String area, String baseStation) { |
| | | return new BoundarySnapshotResult(true, null, original, optimized, area, baseStation, JOptionPane.INFORMATION_MESSAGE); |
| | | static BoundarySnapshotResult success(String original, String originalXY, String optimized, String area, String baseStation) { |
| | | return new BoundarySnapshotResult(true, null, original, originalXY, optimized, area, baseStation, JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | |
| | | static BoundarySnapshotResult failure(String message, int messageType) { |
| | | return new BoundarySnapshotResult(false, message, null, null, null, null, messageType); |
| | | return new BoundarySnapshotResult(false, message, null, null, null, null, null, messageType); |
| | | } |
| | | } |
| | | |
| | |
| | | Dikuai dikuai = getOrCreatePendingDikuai(); |
| | | if (dikuai != null) { |
| | | dikuai.setBoundaryOriginalCoordinates(snapshot.originalBoundary); |
| | | dikuai.setBoundaryOriginalXY(snapshot.originalBoundaryXY); |
| | | dikuai.setBoundaryCoordinates(snapshot.optimizedBoundary); |
| | | dikuai.setLandArea(snapshot.areaSqMeters); |
| | | dikuai.setBaseStationCoordinates(snapshot.baseStationCoordinates); |
| | |
| | | } |
| | | |
| | | dikuaiData.put("boundaryOriginalCoordinates", snapshot.originalBoundary); |
| | | dikuaiData.put("boundaryOriginalXY", snapshot.originalBoundaryXY); |
| | | dikuaiData.put("boundaryCoordinates", snapshot.optimizedBoundary); |
| | | dikuaiData.put("landArea", snapshot.areaSqMeters); |
| | | dikuaiData.put("baseStationCoordinates", snapshot.baseStationCoordinates); |
| | | if (activeSession != null) { |
| | | activeSession.data.put("boundaryOriginalCoordinates", snapshot.originalBoundary); |
| | | activeSession.data.put("boundaryOriginalXY", snapshot.originalBoundaryXY); |
| | | activeSession.data.put("boundaryCoordinates", snapshot.optimizedBoundary); |
| | | activeSession.data.put("landArea", snapshot.areaSqMeters); |
| | | activeSession.data.put("baseStationCoordinates", snapshot.baseStationCoordinates); |
| | |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * æå»ºåå§è¾¹çXYåæ å符串ï¼ç¸å¯¹äºåºåç«çXYåæ ï¼ |
| | | */ |
| | | private static String buildOriginalBoundaryXYString(List<Coordinate> coordinates, String baseStationCoordinates) { |
| | | if (coordinates == null || coordinates.isEmpty()) { |
| | | return "-1"; |
| | | } |
| | | if (baseStationCoordinates == null || baseStationCoordinates.trim().isEmpty() || "-1".equals(baseStationCoordinates.trim())) { |
| | | return "-1"; |
| | | } |
| | | |
| | | try { |
| | | // è§£æåºåç«åæ |
| | | String[] baseParts = baseStationCoordinates.split(","); |
| | | if (baseParts.length < 4) { |
| | | return "-1"; |
| | | } |
| | | double baseLat = publicway.Gpstoxuzuobiao.parseDMToDecimal(baseParts[0], baseParts[1]); |
| | | double baseLon = publicway.Gpstoxuzuobiao.parseDMToDecimal(baseParts[2], baseParts[3]); |
| | | |
| | | StringBuilder sb = new StringBuilder(); |
| | | DecimalFormat xyFormat = new DecimalFormat("0.00"); |
| | | for (Coordinate coord : coordinates) { |
| | | double lat = convertToDecimalDegree(coord.getLatitude(), coord.getLatDirection()); |
| | | double lon = convertToDecimalDegree(coord.getLongitude(), coord.getLonDirection()); |
| | | |
| | | // 转æ¢ä¸ºXYåæ |
| | | double[] xy = publicway.Gpstoxuzuobiao.convertLatLonToLocal(lat, lon, baseLat, baseLon); |
| | | |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(xyFormat.format(xy[0])).append(",").append(xyFormat.format(xy[1])); |
| | | } |
| | | return sb.toString(); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | return "-1"; |
| | | } |
| | | } |
| | | |
| | | private static double convertToDecimalDegree(String dmm, String direction) { |
| | | if (dmm == null || dmm.isEmpty()) { |
| | |
| | | } |
| | | |
| | | String originalBoundary = buildOriginalBoundaryString(uniqueCoordinates); |
| | | String originalBoundaryXY = buildOriginalBoundaryXYString(uniqueCoordinates, baseStationCoordinates); |
| | | DecimalFormat areaFormat = new DecimalFormat("0.00"); |
| | | String areaString = areaFormat.format(area); |
| | | |
| | | return BoundarySnapshotResult.success(originalBoundary, optimizedBoundary, areaString, baseStationCoordinates); |
| | | return BoundarySnapshotResult.success(originalBoundary, originalBoundaryXY, optimizedBoundary, areaString, baseStationCoordinates); |
| | | } |
| | | |
| | | private String resolvePlannerMode(String patternDisplay) { |
| | |
| | | BoundarySnapshotResult snapshot = computeBoundarySnapshot(); |
| | | if (snapshot.success && activeSession != null) { |
| | | activeSession.data.put("boundaryOriginalCoordinates", snapshot.originalBoundary); |
| | | activeSession.data.put("boundaryOriginalXY", snapshot.originalBoundaryXY); |
| | | activeSession.data.put("boundaryCoordinates", snapshot.optimizedBoundary); |
| | | activeSession.data.put("landArea", snapshot.areaSqMeters); |
| | | activeSession.data.put("baseStationCoordinates", snapshot.baseStationCoordinates); |
| | |
| | | if (dikuaiData.containsKey("boundaryOriginalCoordinates")) { |
| | | dikuai.setBoundaryOriginalCoordinates(dikuaiData.get("boundaryOriginalCoordinates")); |
| | | } |
| | | if (dikuaiData.containsKey("boundaryOriginalXY")) { |
| | | dikuai.setBoundaryOriginalXY(dikuaiData.get("boundaryOriginalXY")); |
| | | } |
| | | if (dikuaiData.containsKey("boundaryCoordinates")) { |
| | | dikuai.setBoundaryCoordinates(dikuaiData.get("boundaryCoordinates")); |
| | | } |
| | |
| | | private WangfanDraw returnPathDrawer; // å¾è¿è·¯å¾ç»å¶ç®¡çå¨ |
| | | private List<Point2D.Double> currentReturnPath; // å½åå°åçå¾è¿è·¯å¾ï¼ç¨äºæ¾ç¤ºï¼ |
| | | private List<Point2D.Double> previewReturnPath; // é¢è§çå¾è¿è·¯å¾ |
| | | private List<Point2D.Double> previewOriginalBoundary; // é¢è§çåå§è¾¹çï¼ç´«è²ï¼ |
| | | private List<Point2D.Double> previewOptimizedBoundary; // é¢è§çä¼ååè¾¹ç |
| | | private boolean boundaryPreviewActive; // æ¯å¦å¤äºè¾¹çé¢è§æ¨¡å¼ |
| | | |
| | | private static final double TRACK_SAMPLE_MIN_DISTANCE_METERS = 0.2d; |
| | | private static final double TRACK_DUPLICATE_TOLERANCE_METERS = 1e-3d; |
| | |
| | | if (hasBoundary) { |
| | | drawCurrentBoundary(g2d); |
| | | } |
| | | |
| | | // ç»å¶è¾¹çé¢è§ï¼åå§è¾¹ç-ç´«è²ï¼ä¼ååè¾¹çï¼ |
| | | if (boundaryPreviewActive) { |
| | | drawBoundaryPreview(g2d); |
| | | } |
| | | |
| | | yulanzhangaiwu.renderPreview(g2d, scale); |
| | | |
| | |
| | | } |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | /** |
| | | * 设置边çé¢è§æ°æ®ï¼åå§è¾¹çåä¼ååè¾¹çï¼ |
| | | */ |
| | | public void setBoundaryPreview(String originalBoundaryXY, String optimizedBoundary) { |
| | | if (originalBoundaryXY != null && !originalBoundaryXY.trim().isEmpty() && !"-1".equals(originalBoundaryXY.trim())) { |
| | | previewOriginalBoundary = parseBoundary(originalBoundaryXY.trim()); |
| | | } else { |
| | | previewOriginalBoundary = null; |
| | | } |
| | | |
| | | if (optimizedBoundary != null && !optimizedBoundary.trim().isEmpty() && !"-1".equals(optimizedBoundary.trim())) { |
| | | previewOptimizedBoundary = parseBoundary(optimizedBoundary.trim()); |
| | | } else { |
| | | previewOptimizedBoundary = null; |
| | | } |
| | | |
| | | boundaryPreviewActive = (previewOriginalBoundary != null && previewOriginalBoundary.size() >= 2) || |
| | | (previewOptimizedBoundary != null && previewOptimizedBoundary.size() >= 2); |
| | | |
| | | if (boundaryPreviewActive) { |
| | | // 计ç®é¢è§è¾¹ççè¾¹çæ¡å¹¶è°æ´è§å¾ |
| | | List<Point2D.Double> allPoints = new ArrayList<>(); |
| | | if (previewOriginalBoundary != null) allPoints.addAll(previewOriginalBoundary); |
| | | if (previewOptimizedBoundary != null) allPoints.addAll(previewOptimizedBoundary); |
| | | if (!allPoints.isEmpty()) { |
| | | Rectangle2D.Double bounds = computeBounds(allPoints); |
| | | SwingUtilities.invokeLater(() -> { |
| | | fitBoundsToView(bounds); |
| | | visualizationPanel.repaint(); |
| | | }); |
| | | } |
| | | } else { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ¸
é¤è¾¹çé¢è§ |
| | | */ |
| | | public void clearBoundaryPreview() { |
| | | previewOriginalBoundary = null; |
| | | previewOptimizedBoundary = null; |
| | | boundaryPreviewActive = false; |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | /** |
| | | * ç»å¶è¾¹çé¢è§ï¼åå§è¾¹ç-ç´«è²ï¼ä¼ååè¾¹ç-绿è²ï¼ |
| | | */ |
| | | private void drawBoundaryPreview(Graphics2D g2d) { |
| | | // ç»å¶åå§è¾¹çï¼ç´«è²ï¼ |
| | | if (previewOriginalBoundary != null && previewOriginalBoundary.size() >= 2) { |
| | | Color purpleFill = new Color(128, 0, 128, 80); // ç´«è²åéæå¡«å
|
| | | Color purpleBorder = new Color(128, 0, 128, 255); // ç´«è²è¾¹æ¡ |
| | | bianjiedrwa.drawBoundary(g2d, previewOriginalBoundary, scale, purpleFill, purpleBorder); |
| | | } |
| | | |
| | | // ç»å¶ä¼ååè¾¹çï¼ç»¿è²ï¼ä¸æ£å¸¸è¾¹çé¢è²ä¸è´ï¼ |
| | | if (previewOptimizedBoundary != null && previewOptimizedBoundary.size() >= 2) { |
| | | bianjiedrwa.drawBoundary(g2d, previewOptimizedBoundary, scale, GRASS_FILL_COLOR, GRASS_BORDER_COLOR); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | pathPreviewReturnAction.run(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ¾ç¤ºè¾¹çé¢è§ï¼åå§è¾¹ç-ç´«è²ï¼ä¼ååè¾¹ç-绿è²ï¼ |
| | | * @param dikuai å°å对象 |
| | | * @param optimizedBoundary ä¼ååçè¾¹çåæ å符串 |
| | | * @param returnCallback è¿ååè° |
| | | */ |
| | | public static void showBoundaryPreview(dikuai.Dikuai dikuai, String optimizedBoundary, Runnable returnCallback) { |
| | | Shouye shouye = getInstance(); |
| | | if (shouye == null || shouye.mapRenderer == null || dikuai == null) { |
| | | return; |
| | | } |
| | | |
| | | // è·ååå§è¾¹çXYåæ |
| | | String originalBoundaryXY = dikuai.getBoundaryOriginalXY(); |
| | | |
| | | // 设置边çé¢è§ |
| | | shouye.mapRenderer.setBoundaryPreview(originalBoundaryXY, optimizedBoundary); |
| | | |
| | | // 设置è¿ååè° |
| | | shouye.pathPreviewReturnAction = returnCallback; |
| | | shouye.pathPreviewActive = true; |
| | | |
| | | // ç¡®ä¿æ¬æµ®æé®åºç¡è®¾æ½å·²å建 |
| | | shouye.ensureFloatingButtonInfrastructure(); |
| | | |
| | | // åå»ºææ¾ç¤ºè¿åæé® |
| | | if (shouye.pathPreviewReturnButton == null) { |
| | | shouye.pathPreviewReturnButton = publicway.Fanhuibutton.createReturnButton(e -> shouye.handleBoundaryPreviewReturn()); |
| | | shouye.pathPreviewReturnButton.setToolTipText("è¿åè¾¹çç¼è¾é¡µé¢"); |
| | | } |
| | | |
| | | // éèå
¶ä»æ¬æµ®æé® |
| | | shouye.hideFloatingDrawingControls(); |
| | | |
| | | // æ¾ç¤ºè¿åæé® |
| | | shouye.pathPreviewReturnButton.setVisible(true); |
| | | if (shouye.floatingButtonPanel != null) { |
| | | shouye.floatingButtonPanel.setVisible(true); |
| | | if (shouye.floatingButtonPanel.getParent() != shouye.visualizationPanel) { |
| | | shouye.visualizationPanel.add(shouye.floatingButtonPanel, BorderLayout.SOUTH); |
| | | } |
| | | } |
| | | shouye.rebuildFloatingButtonColumn(); |
| | | |
| | | shouye.visualizationPanel.revalidate(); |
| | | shouye.visualizationPanel.repaint(); |
| | | } |
| | | |
| | | /** |
| | | * å¤çè¾¹çé¢è§è¿å |
| | | */ |
| | | private void handleBoundaryPreviewReturn() { |
| | | Runnable callback = pathPreviewReturnAction; |
| | | exitBoundaryPreview(); |
| | | if (callback != null) { |
| | | callback.run(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * éåºè¾¹çé¢è§ |
| | | */ |
| | | private void exitBoundaryPreview() { |
| | | pathPreviewActive = false; |
| | | |
| | | // æ¸
é¤è¾¹çé¢è§ |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearBoundaryPreview(); |
| | | } |
| | | |
| | | // éèè¿åæé® |
| | | if (pathPreviewReturnButton != null) { |
| | | pathPreviewReturnButton.setVisible(false); |
| | | } |
| | | |
| | | // éèæ¬æµ®é¢æ¿ |
| | | if (floatingButtonPanel != null) { |
| | | floatingButtonPanel.setVisible(false); |
| | | } |
| | | |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | } |
| | | |
| | | // æµè¯æ¹æ³ |
| | | public static void main(String[] args) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package zhuye; |
| | | |
| | | import javax.swing.JFrame; |
| | | import javax.swing.SwingUtilities; |
| | | import udpdell.UDPServer; |
| | | |
| | | public class ShouyeLauncher { |
| | | public static void main(String[] args) { |
| | | SwingUtilities.invokeLater(() -> { |
| | | System.out.println("Starting Shouye via Launcher..."); |
| | | JFrame frame = new JFrame("AutoMow - é¦é¡µ"); |
| | | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); |
| | | frame.setSize(400, 800); |
| | | frame.setLocationRelativeTo(null); |
| | | |
| | | Shouye shouye = new Shouye(); |
| | | frame.add(shouye); |
| | | |
| | | frame.setVisible(true); |
| | | UDPServer.startAsync(); |
| | | }); |
| | | } |
| | | } |
| | |
| | | return; // æ æ°æ®è¿å |
| | | } // ifç»æ |
| | | |
| | | float strokeWidth = (float) (3 / Math.max(0.5, scale)); // 计ç®è¾¹çº¿å®½åº¦ |
| | | float strokeWidth = (float) (6 / Math.max(0.5, scale)); // 计ç®è¾¹çº¿å®½åº¦ï¼å¢å ä¸åï¼ä»3æ¹ä¸º6ï¼ |
| | | |
| | | // å¡«å
åºå |
| | | Path2D fillPath = new Path2D.Double(); // å建填å
è·¯å¾ |
| | |
| | | borderPath.lineTo(firstPoint.x, firstPoint.y); // è¿æ¥å°èµ·ç¹å½¢æéå |
| | | } |
| | | |
| | | // ç»å¶å¤å±å®çº¿è¾¹çï¼å®½åº¦å¢å ä¸åï¼ |
| | | g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // 设置å®çº¿æè¾¹ |
| | | g2d.setColor(borderColor); // 设置边线é¢è² |
| | | g2d.draw(borderPath); // ç»å¶å®æ´è¾¹çï¼å
æ¬èµ·ç¹å°ç»ç¹çè¿æ¥ï¼ |
| | | |
| | | // å¨è¾¹ç线ä¸é´ç»å¶æ·±ç°è²å°åç¹è线 |
| | | float dashedLineWidth = strokeWidth / 3.0f; // è线宽度为å®çº¿çä¸åä¹ä¸ |
| | | float[] dashPattern = {2.0f, 2.0f}; // çé´éå°åç¹è线模å¼ï¼2åç´ å®çº¿ï¼å½¢æåç¹ï¼ï¼2åç´ ç©ºç½ï¼ååé´éï¼ |
| | | BasicStroke dashedStroke = new BasicStroke( |
| | | dashedLineWidth, |
| | | BasicStroke.CAP_ROUND, // 使ç¨å形端ç¹ï¼å½¢æåç¹ææ |
| | | BasicStroke.JOIN_ROUND, |
| | | 0.0f, |
| | | dashPattern, |
| | | 0.0f |
| | | ); |
| | | g2d.setStroke(dashedStroke); // 设置è线æè¾¹ |
| | | Color darkGrayColor = new Color(64, 64, 64); // æ·±ç°è² |
| | | g2d.setColor(darkGrayColor); // 设置è线é¢è² |
| | | g2d.draw(borderPath); // å¨ä¸é´ç»å¶å°åç¹è线 |
| | | } |
| | | } // æ¹æ³ç»æ |
| | | } // ç±»ç»æ |