新增有障碍物的路径规划算法和优化没有障碍物的路径算法
| | |
| | | # å²èæºå°åéç¢ç©é
ç½®æä»¶ |
| | | # çææ¶é´ï¼2025-12-09T11:53:37.128295200 |
| | | # çææ¶é´ï¼2025-12-17T12:03:32.881913900 |
| | | # åæ ç³»ï¼WGS84ï¼åº¦åæ ¼å¼ï¼ |
| | | |
| | | # ============ å°ååºåç«é
ç½® ============ |
| | |
| | | #Dikuai Properties |
| | | #Fri Dec 12 15:42:15 CST 2025 |
| | | #Wed Dec 17 12:03:32 CST 2025 |
| | | LAND1.angleThreshold=-1 |
| | | LAND1.baseStationCoordinates=3949.90238860,N,11616.75692000,E |
| | | LAND1.boundaryCoordinates=1.31,-9.59;1.86,-11.61;3.12,-12.49;5.50,-12.06;5.95,-10.88;4.97,-3.86;3.16,-0.87;2.79,-2.61;2.42,-4.35;2.05,-6.10;1.68,-7.84;1.31,-9.59 |
| | | LAND1.boundaryOriginalCoordinates=39.831620,116.279297,39.70;39.831618,116.279298,39.80;39.831616,116.279298,39.70;39.831614,116.279298,39.70;39.831612,116.279299,39.70;39.831610,116.279299,39.70;39.831608,116.279300,39.70;39.831606,116.279301,39.70;39.831604,116.279303,39.70;39.831602,116.279304,39.70;39.831600,116.279306,39.70;39.831598,116.279307,39.70;39.831598,116.279309,39.70;39.831597,116.279312,39.60;39.831596,116.279314,39.60;39.831595,116.279317,39.60;39.831594,116.279319,39.60;39.831594,116.279320,39.60;39.831594,116.279323,39.60;39.831595,116.279326,39.60;39.831595,116.279329,39.60;39.831595,116.279331,39.70;39.831595,116.279334,39.70;39.831596,116.279337,39.60;39.831596,116.279339,39.60;39.831596,116.279342,39.60;39.831597,116.279344,39.60;39.831598,116.279346,39.70;39.831600,116.279348,39.70;39.831601,116.279349,39.70;39.831603,116.279350,39.80;39.831605,116.279350,39.70;39.831606,116.279351,39.70;39.831609,116.279352,39.80;39.831611,116.279352,39.70;39.831613,116.279352,39.70;39.831615,116.279352,39.70;39.831617,116.279353,39.70;39.831619,116.279353,39.70;39.831621,116.279353,39.80;39.831623,116.279353,39.80;39.831625,116.279353,39.80;39.831627,116.279353,39.80;39.831629,116.279352,39.80;39.831631,116.279352,39.80;39.831634,116.279351,39.70;39.831636,116.279351,39.70;39.831637,116.279350,39.70;39.831639,116.279350,39.80;39.831641,116.279349,39.70;39.831643,116.279348,39.70;39.831645,116.279348,39.70;39.831647,116.279347,39.70;39.831649,116.279346,39.80;39.831651,116.279346,39.80;39.831653,116.279345,39.80;39.831655,116.279345,39.80;39.831657,116.279344,39.80;39.831659,116.279343,39.70;39.831661,116.279343,39.70;39.831663,116.279342,39.70;39.831665,116.279342,39.70;39.831667,116.279341,39.70;39.831670,116.279341,39.70;39.831672,116.279340,39.70;39.831674,116.279339,39.80;39.831676,116.279338,39.80;39.831678,116.279337,39.80;39.831679,116.279336,39.70;39.831680,116.279334,39.70;39.831681,116.279332,39.70;39.831683,116.279331,39.70;39.831684,116.279329,39.70;39.831686,116.279327,39.70;39.831687,116.279325,39.70;39.831689,116.279323,39.70;39.831691,116.279322,39.70;39.831693,116.279321,39.70;39.831694,116.279320,39.70;39.831696,116.279319,39.70;39.831699,116.279319,39.70 |
| | | LAND1.boundaryCoordinates=77.19,-32.68;80.71,-54.97;80.99,-55.90;83.54,-56.46;85.04,-55.55;85.94,-53.74;83.24,-35.82;84.55,-34.54;94.02,-31.92;94.10,-31.11;90.88,-20.39;90.35,-19.53;88.33,-19.00;84.12,-19.47;78.92,-22.36;76.63,-25.55;76.93,-29.84;77.06,-31.26;77.19,-32.68 |
| | | LAND1.boundaryOriginalCoordinates=39.831413,116.280186,49.12;39.831409,116.280188,49.09;39.831403,116.280187,49.12;39.831395,116.280189,49.13;39.831388,116.280191,49.16;39.831379,116.280193,49.18;39.831370,116.280194,49.16;39.831362,116.280195,49.15;39.831353,116.280197,49.11;39.831344,116.280200,49.15;39.831335,116.280202,49.15;39.831326,116.280204,49.08;39.831317,116.280206,49.19;39.831309,116.280209,49.18;39.831301,116.280210,49.19;39.831293,116.280212,49.11;39.831284,116.280214,49.06;39.831275,116.280215,49.16;39.831266,116.280217,49.14;39.831258,116.280220,49.08;39.831249,116.280223,49.09;39.831240,116.280225,49.10;39.831231,116.280226,49.04;39.831222,116.280226,49.17;39.831212,116.280227,49.11;39.831204,116.280230,49.09;39.831201,116.280238,49.10;39.831199,116.280249,49.07;39.831199,116.280260,49.21;39.831202,116.280270,49.16;39.831207,116.280278,49.06;39.831212,116.280284,49.04;39.831217,116.280287,49.05;39.831223,116.280288,49.09;39.831229,116.280287,49.10;39.831237,116.280286,49.05;39.831245,116.280286,49.08;39.831254,116.280284,49.07;39.831263,116.280283,49.05;39.831272,116.280280,49.11;39.831282,116.280278,49.10;39.831291,116.280276,49.11;39.831300,116.280274,49.16;39.831308,116.280270,49.13;39.831318,116.280268,49.10;39.831327,116.280267,49.14;39.831337,116.280266,49.08;39.831347,116.280263,49.10;39.831356,116.280261,49.20;39.831366,116.280258,49.14;39.831375,116.280256,49.09;39.831384,116.280257,49.13;39.831392,116.280263,49.10;39.831396,116.280272,49.12;39.831398,116.280283,49.16;39.831401,116.280294,49.11;39.831403,116.280307,49.13;39.831405,116.280318,49.19;39.831406,116.280328,49.20;39.831408,116.280340,49.22;39.831411,116.280353,49.19;39.831414,116.280363,49.26;39.831416,116.280374,49.22;39.831419,116.280383,49.20;39.831427,116.280384,49.21;39.831433,116.280379,49.17;39.831441,116.280375,49.19;39.831451,116.280372,49.09;39.831459,116.280370,49.16;39.831467,116.280364,49.21;39.831476,116.280360,49.22;39.831485,116.280357,49.20;39.831495,116.280355,49.26;39.831505,116.280351,49.21;39.831514,116.280348,49.17;39.831523,116.280346,49.20;39.831531,116.280340,49.04;39.831535,116.280328,49.08;39.831536,116.280316,49.03;39.831535,116.280304,49.03;39.831533,116.280291,49.06;39.831532,116.280279,49.07;39.831531,116.280267,49.11;39.831528,116.280257,49.09;39.831525,116.280246,49.11;39.831521,116.280237,49.09;39.831516,116.280227,49.08;39.831511,116.280216,49.12;39.831505,116.280206,49.14;39.831499,116.280197,49.12;39.831492,116.280189,49.15;39.831484,116.280184,49.14;39.831477,116.280179,49.12;39.831469,116.280178,49.12;39.831462,116.280181,49.13;39.831454,116.280182,49.12;39.831445,116.280183,49.12;39.831439,116.280183,49.14;39.831438,116.280183,49.12 |
| | | LAND1.boundaryPointInterval=-1 |
| | | LAND1.createTime=2025-12-09 11\:16\:40 |
| | | LAND1.createTime=2025-12-16 15\:43\:39 |
| | | LAND1.intelligentSceneAnalysis=-1 |
| | | LAND1.landArea=35.87 |
| | | LAND1.landName=1234 |
| | | LAND1.landArea=327.17 |
| | | LAND1.landName=yyii |
| | | LAND1.landNumber=LAND1 |
| | | LAND1.mowingPattern=èºæå¼ |
| | | LAND1.mowingTrack=5.952,-10.672 |
| | | LAND1.mowingPattern=å¹³è¡çº¿ |
| | | LAND1.mowingTrack= |
| | | LAND1.mowingWidth=40 |
| | | LAND1.plannedPath=1.88,-7.88;2.62,-4.39;3.25,-1.41;4.78,-3.93;5.74,-10.86;5.35,-11.88;3.17,-12.28;2.03,-11.49;1.52,-9.58;1.88,-7.88;2.27,-7.96;3.01,-4.47;3.43,-2.48;4.39,-4.07;5.33,-10.81;5.06,-11.53;3.26,-11.86;2.38,-11.24;1.93,-9.57;2.27,-7.96;2.66,-8.05;3.40,-4.56;3.61,-3.55;4.01,-4.20;4.92,-10.76;4.77,-11.18;3.35,-11.43;2.73,-11.00;2.34,-9.56;2.66,-8.05;3.05,-8.13;3.71,-4.99;4.51,-10.72;4.47,-10.82;3.44,-11.01;3.08,-10.75;2.75,-9.55;3.05,-8.13;3.44,-8.21;3.63,-7.30;4.08,-10.49;3.54,-10.59;3.43,-10.51;3.16,-9.54;3.44,-8.21 |
| | | LAND1.plannedPath=77.17,-29.65;81.28,-55.71;81.70,-55.80;76.93,-25.57;77.26,-25.10;82.12,-55.89;82.54,-55.98;77.59,-24.64;77.92,-24.18;82.96,-56.08;83.38,-56.17;78.25,-23.72;78.59,-23.25;83.76,-56.03;84.13,-55.81;78.92,-22.79;79.27,-22.45;84.50,-55.58;84.87,-55.33;79.64,-22.24;80.01,-22.04;85.18,-54.72;85.48,-54.10;80.39,-21.83;80.76,-21.62;83.00,-35.83;83.34,-35.38;81.13,-21.42;81.50,-21.21;83.69,-35.03;84.04,-34.69;81.88,-21.00;82.25,-20.80;84.39,-34.35;84.77,-34.22;82.62,-20.59;82.99,-20.38;85.16,-34.11;85.55,-34.00;83.37,-20.18;83.74,-19.97;85.94,-33.90;86.32,-33.79;84.11,-19.76;84.50,-19.68;86.71,-33.68;87.10,-33.57;84.90,-19.63;85.30,-19.59;87.49,-33.47;87.88,-33.36;85.70,-19.55;86.09,-19.50;88.26,-33.25;88.65,-33.15;86.49,-19.46;86.89,-19.41;89.04,-33.04;89.43,-32.93;87.29,-19.37;87.69,-19.32;89.82,-32.82;90.20,-32.72;88.08,-19.28;88.49,-19.30;90.59,-32.61;90.98,-32.50;88.91,-19.41;89.34,-19.52;91.37,-32.39;91.76,-32.29;89.76,-19.63;90.18,-19.74;92.14,-32.18;92.53,-32.07;90.76,-20.88;91.62,-23.72;92.92,-31.96;93.31,-31.86;92.47,-26.56;93.33,-29.40;93.70,-31.75 |
| | | LAND1.returnPointCoordinates=-1 |
| | | LAND1.updateTime=2025-12-09 11\:53\:37 |
| | | LAND1.updateTime=2025-12-17 12\:03\:32 |
| | | LAND1.userId=-1 |
| | |
| | | #Mower Configuration Properties - Updated |
| | | #Tue Dec 16 16:27:10 CST 2025 |
| | | #Wed Dec 17 12:04:00 CST 2025 |
| | | appVersion=-1 |
| | | currentWorkLandNumber=LAND1 |
| | | cuttingWidth=200 |
| | | firmwareVersion=-1 |
| | | handheldMarkerId= |
| | | idleTrailDurationSeconds=60 |
| | | mapScale=20.09 |
| | | mapScale=15.31 |
| | | mowerId=1234 |
| | | serialAutoConnect=true |
| | | serialBaudRate=115200 |
| | | serialPortName=COM15 |
| | | simCardNumber=-1 |
| | | viewCenterX=0.55 |
| | | viewCenterY=12.53 |
| | | viewCenterX=-85.37 |
| | | viewCenterY=37.73 |
| | |
| | | import java.util.Properties; |
| | | |
| | | import lujing.Lunjingguihua; |
| | | import lujing.MowingPathGenerationPage; |
| | | import zhangaiwu.AddDikuai; |
| | | import zhangaiwu.Obstacledge; |
| | | import zhuye.MapRenderer; |
| | |
| | | JButton deleteBtn = createDeleteButton(); |
| | | deleteBtn.addActionListener(e -> deleteDikuai(dikuai)); |
| | | |
| | | JButton generatePathBtn = createPrimaryFooterButton("çæå²èè·¯å¾"); |
| | | generatePathBtn.addActionListener(e -> generateMowingPath(dikuai)); |
| | | JButton generatePathBtn = createPrimaryFooterButton("è·¯å¾è§å"); |
| | | generatePathBtn.addActionListener(e -> showPathPlanningPage(dikuai)); |
| | | |
| | | JPanel footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); |
| | | footerPanel.setBackground(CARD_BACKGROUND); |
| | |
| | | return trimmed; |
| | | } |
| | | |
| | | /** |
| | | * æ¾ç¤ºè·¯å¾è§åé¡µé¢ |
| | | */ |
| | | private void showPathPlanningPage(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | |
| | | // è·åå°ååºæ¬æ°æ® |
| | | String baseStationValue = prepareCoordinateForEditor(dikuai.getBaseStationCoordinates()); |
| | | String boundaryValue = prepareCoordinateForEditor(dikuai.getBoundaryCoordinates()); |
| | | List<Obstacledge.Obstacle> configuredObstacles = getConfiguredObstacles(dikuai); |
| | | String obstacleValue = determineInitialObstacleValue(dikuai, configuredObstacles); |
| | | String widthValue = sanitizeWidthString(dikuai.getMowingWidth()); |
| | | if (widthValue != null) { |
| | | try { |
| | | double widthCm = Double.parseDouble(widthValue); |
| | | widthValue = formatWidthForStorage(widthCm); |
| | | } catch (NumberFormatException ignored) { |
| | | // ä¿æåå§å符串ï¼ç¨åæ ¡éªæç¤º |
| | | } |
| | | } |
| | | String modeValue = sanitizeValueOrNull(dikuai.getMowingPattern()); |
| | | String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath()); |
| | | |
| | | // å建ä¿ååè°æ¥å£å®ç° |
| | | MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() { |
| | | @Override |
| | | public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) { |
| | | return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue); |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveMowingWidth(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "mowingWidth", value); |
| | | } |
| | | |
| | | @Override |
| | | public boolean savePlannedPath(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "plannedPath", value); |
| | | } |
| | | }; |
| | | |
| | | // æ¾ç¤ºè·¯å¾è§åé¡µé¢ |
| | | MowingPathGenerationPage dialog = new MowingPathGenerationPage( |
| | | owner, |
| | | dikuai, |
| | | baseStationValue, |
| | | boundaryValue, |
| | | obstacleValue, |
| | | widthValue, |
| | | modeValue, |
| | | existingPath, |
| | | callback |
| | | ); |
| | | |
| | | dialog.setVisible(true); |
| | | } |
| | | |
| | | private void generateMowingPath(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | |
| | | String modeValue, |
| | | String initialGeneratedPath) { |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | JDialog dialog = new JDialog(owner, "çæå²èè·¯å¾", Dialog.ModalityType.APPLICATION_MODAL); |
| | | dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); |
| | | dialog.getContentPane().setLayout(new BorderLayout()); |
| | | dialog.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 = getDisplayValue(dikuai.getLandName(), "æªç¥å°å"); |
| | | String landNumber = getDisplayValue(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)); |
| | | |
| | | JTextField baseStationField = createInfoTextField(baseStationValue != null ? baseStationValue : "", true); |
| | | contentPanel.add(createTextFieldSection("åºç«åæ ", baseStationField)); |
| | | |
| | | JTextArea boundaryArea = createInfoTextArea(boundaryValue != null ? boundaryValue : "", true, 6); |
| | | contentPanel.add(createTextAreaSection("å°åè¾¹ç", boundaryArea)); |
| | | |
| | | JTextArea obstacleArea = createInfoTextArea(obstacleValue != null ? obstacleValue : "", true, 6); |
| | | contentPanel.add(createTextAreaSection("éç¢ç©åæ ", obstacleArea)); |
| | | |
| | | JTextField widthField = createInfoTextField(widthValue != null ? widthValue : "", true); |
| | | contentPanel.add(createTextFieldSection("å²è宽度 (åç±³)", widthField)); |
| | | contentPanel.add(createInfoValueSection("å²è模å¼", formatMowingPatternForDialog(modeValue))); |
| | | |
| | | String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath()); |
| | | String pathSeed = initialGeneratedPath != null ? initialGeneratedPath : existingPath; |
| | | JTextArea pathArea = createInfoTextArea(pathSeed != null ? pathSeed : "", true, 10); |
| | | contentPanel.add(createTextAreaSection("å²èè·¯å¾åæ ", pathArea)); |
| | | |
| | | JScrollPane dialogScrollPane = new JScrollPane(contentPanel); |
| | | dialogScrollPane.setBorder(BorderFactory.createEmptyBorder()); |
| | | dialogScrollPane.getVerticalScrollBar().setUnitIncrement(16); |
| | | dialog.add(dialogScrollPane, BorderLayout.CENTER); |
| | | |
| | | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 12, 12)); |
| | | buttonPanel.setBackground(BACKGROUND_COLOR); |
| | | |
| | | JButton generateBtn = createPrimaryFooterButton("çæå²èè·¯å¾"); |
| | | JButton saveBtn = createPrimaryFooterButton("ä¿åè·¯å¾"); |
| | | JButton cancelBtn = createPrimaryFooterButton("åæ¶"); |
| | | |
| | | generateBtn.addActionListener(e -> { |
| | | String sanitizedWidth = sanitizeWidthString(widthField.getText()); |
| | | if (sanitizedWidth != null) { |
| | | try { |
| | | double widthCm = Double.parseDouble(sanitizedWidth); |
| | | widthField.setText(formatWidthForStorage(widthCm)); |
| | | sanitizedWidth = formatWidthForStorage(widthCm); |
| | | } catch (NumberFormatException ex) { |
| | | widthField.setText(sanitizedWidth); |
| | | } |
| | | |
| | | // å建ä¿ååè°æ¥å£å®ç° |
| | | MowingPathGenerationPage.PathSaveCallback callback = new MowingPathGenerationPage.PathSaveCallback() { |
| | | @Override |
| | | public boolean saveBaseStationCoordinates(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "baseStationCoordinates", value); |
| | | } |
| | | String generated = attemptMowingPathPreview( |
| | | boundaryArea.getText(), |
| | | obstacleArea.getText(), |
| | | sanitizedWidth, |
| | | modeValue, |
| | | dialog, |
| | | true |
| | | ); |
| | | if (generated != null) { |
| | | pathArea.setText(generated); |
| | | pathArea.setCaretPosition(0); |
| | | |
| | | @Override |
| | | public boolean saveBoundaryCoordinates(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "boundaryCoordinates", value); |
| | | } |
| | | }); |
| | | |
| | | saveBtn.addActionListener(e -> { |
| | | String baseStationNormalized = normalizeCoordinateInput(baseStationField.getText()); |
| | | String boundaryNormalized = normalizeCoordinateInput(boundaryArea.getText()); |
| | | if (!"-1".equals(boundaryNormalized)) { |
| | | boundaryNormalized = boundaryNormalized |
| | | .replace("\r\n", ";") |
| | | .replace('\r', ';') |
| | | .replace('\n', ';') |
| | | .replaceAll(";+", ";") |
| | | .replaceAll("\\s*;\\s*", ";") |
| | | .trim(); |
| | | if (boundaryNormalized.isEmpty()) { |
| | | boundaryNormalized = "-1"; |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue) { |
| | | return persistObstaclesForLand(dikuai, baseStationValue, obstacleValue); |
| | | } |
| | | String obstacleNormalized = normalizeCoordinateInput(obstacleArea.getText()); |
| | | if (!"-1".equals(obstacleNormalized)) { |
| | | obstacleNormalized = obstacleNormalized |
| | | .replace("\r\n", " ") |
| | | .replace('\r', ' ') |
| | | .replace('\n', ' ') |
| | | .replaceAll("\\s{2,}", " ") |
| | | .trim(); |
| | | if (obstacleNormalized.isEmpty()) { |
| | | obstacleNormalized = "-1"; |
| | | } |
| | | |
| | | @Override |
| | | public boolean saveMowingWidth(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "mowingWidth", value); |
| | | } |
| | | String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : ""; |
| | | String widthSanitized = sanitizeWidthString(widthField.getText()); |
| | | if (widthSanitized == null) { |
| | | String message = rawWidthInput.isEmpty() ? "请å
设置å²è宽度(åç±³)" : "å²èå®½åº¦æ ¼å¼ä¸æ£ç¡®"; |
| | | JOptionPane.showMessageDialog(dialog, message, "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | |
| | | @Override |
| | | public boolean savePlannedPath(Dikuai dikuai, String value) { |
| | | return saveFieldAndRefresh(dikuai, "plannedPath", value); |
| | | } |
| | | double widthCm; |
| | | try { |
| | | widthCm = Double.parseDouble(widthSanitized); |
| | | } catch (NumberFormatException ex) { |
| | | JOptionPane.showMessageDialog(dialog, "å²èå®½åº¦æ ¼å¼ä¸æ£ç¡®", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | if (widthCm <= 0) { |
| | | JOptionPane.showMessageDialog(dialog, "å²è宽度å¿
须大äº0", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | String widthNormalized = formatWidthForStorage(widthCm); |
| | | widthField.setText(widthNormalized); |
| | | String pathNormalized = normalizeCoordinateInput(pathArea.getText()); |
| | | if (!"-1".equals(pathNormalized)) { |
| | | pathNormalized = pathNormalized |
| | | .replace("\r\n", ";") |
| | | .replace('\r', ';') |
| | | .replace('\n', ';') |
| | | .replaceAll(";+", ";") |
| | | .replaceAll("\\s*;\\s*", ";") |
| | | .trim(); |
| | | if (pathNormalized.isEmpty()) { |
| | | pathNormalized = "-1"; |
| | | } |
| | | } |
| | | if ("-1".equals(pathNormalized)) { |
| | | JOptionPane.showMessageDialog(dialog, "请å
çæå²èè·¯å¾", "æç¤º", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | if (!saveFieldAndRefresh(dikuai, "baseStationCoordinates", baseStationNormalized)) { |
| | | JOptionPane.showMessageDialog(dialog, "æ æ³ä¿ååºç«åæ ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (!saveFieldAndRefresh(dikuai, "boundaryCoordinates", boundaryNormalized)) { |
| | | JOptionPane.showMessageDialog(dialog, "æ æ³ä¿åå°åè¾¹ç", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (!persistObstaclesForLand(dikuai, baseStationNormalized, obstacleNormalized)) { |
| | | JOptionPane.showMessageDialog(dialog, "æ æ³ä¿åéç¢ç©åæ ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (!saveFieldAndRefresh(dikuai, "mowingWidth", widthNormalized)) { |
| | | JOptionPane.showMessageDialog(dialog, "æ æ³ä¿åå²è宽度", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (!saveFieldAndRefresh(dikuai, "plannedPath", pathNormalized)) { |
| | | JOptionPane.showMessageDialog(dialog, "æ æ³ä¿åå²èè·¯å¾", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | JOptionPane.showMessageDialog(dialog, "å²èè·¯å¾å·²ä¿å", "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | dialog.dispose(); |
| | | }); |
| | | |
| | | cancelBtn.addActionListener(e -> dialog.dispose()); |
| | | |
| | | buttonPanel.add(generateBtn); |
| | | buttonPanel.add(saveBtn); |
| | | buttonPanel.add(cancelBtn); |
| | | dialog.add(buttonPanel, BorderLayout.SOUTH); |
| | | |
| | | dialog.pack(); |
| | | dialog.setSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT)); |
| | | dialog.setLocationRelativeTo(owner); |
| | | }; |
| | | |
| | | // ä½¿ç¨æ°çç¬ç«é¡µé¢ç±» |
| | | MowingPathGenerationPage dialog = new MowingPathGenerationPage( |
| | | owner, |
| | | dikuai, |
| | | baseStationValue, |
| | | boundaryValue, |
| | | obstacleValue, |
| | | widthValue, |
| | | modeValue, |
| | | initialGeneratedPath, |
| | | callback |
| | | ); |
| | | |
| | | dialog.setVisible(true); |
| | | } |
| | | |
| | |
| | | /** |
| | | * çæå²èè·¯å¾æ®µå表ã |
| | | * |
| | | * @param polygonCoords å¤è¾¹å½¢è¾¹çåæ ï¼æ ¼å¼å¦ "x1,y1;x2,y2;..."ï¼ç±³ï¼ |
| | | * @param polygonCoords å¤è¾¹å½¢è¾¹çåæ ï¼æ ¼å¼å¦ "x1,y1;x2,y2;..."ï¼ç±³ï¼ |
| | | * @param obstaclesCoords éç¢ç©åæ ï¼æ¯æå¤ä¸ªæ¬å·æ®µæåå½¢å®ä¹ï¼ä¾ "(x1,y1;x2,y2)(cx,cy;px,py)" |
| | | * @param mowingWidth å²è宽度å符串ï¼ç±³åä½ï¼å
许ä¿ç两ä½å°æ° |
| | | * @param modeStr å²è模å¼ï¼"0"/空为平è¡çº¿ï¼"1" æ "spiral" è¡¨ç¤ºèºææ¨¡å¼ï¼å½åä»
å¹³è¡çº¿å®ç°ï¼ |
| | | * @param mowingWidth å²è宽度å符串ï¼ç±³åä½ï¼å
许ä¿ç两ä½å°æ° |
| | | * @param safetyDistStr å®å
¨è·ç¦»å符串ï¼ç±³åä½ãè·¯å¾å°ä¸è¾¹çåéç¢ç©ä¿ææ¤è·ç¦»ã |
| | | * @param modeStr å²è模å¼ï¼"0"/空为平è¡çº¿ï¼"1" æ "spiral" è¡¨ç¤ºèºææ¨¡å¼ï¼å½åä»
å¹³è¡çº¿å®ç°ï¼ |
| | | * @return è·¯å¾æ®µåè¡¨ï¼æè¡é©¶é¡ºåºæå |
| | | */ |
| | | public static List<PathSegment> generatePathSegments(String polygonCoords, |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | | String modeStr) { |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | | String safetyDistStr, |
| | | String modeStr) { |
| | | List<Coordinate> polygon = parseCoordinates(polygonCoords); |
| | | if (polygon.size() < 4) { |
| | | throw new IllegalArgumentException("å¤è¾¹å½¢åæ æ°éä¸è¶³ï¼è³å°éè¦ä¸ä¸ªç¹"); |
| | |
| | | if (width <= 0) { |
| | | throw new IllegalArgumentException("å²è宽度å¿
é¡»å¤§äº 0"); |
| | | } |
| | | |
| | | // è§£æå®å
¨è·ç¦»ï¼å¦ææªè®¾ç½®ææ æï¼é»è®¤ä¸º NaN (å¨ PlannerCore ä¸å¤çé»è®¤å¼) |
| | | double safetyDistance = parseDoubleOrDefault(safetyDistStr, Double.NaN); |
| | | |
| | | List<List<Coordinate>> obstacles = parseObstacles(obstaclesCoords); |
| | | String mode = normalizeMode(modeStr); |
| | | |
| | | PlannerCore planner = new PlannerCore(polygon, width, mode, obstacles); |
| | | PlannerCore planner = new PlannerCore(polygon, width, safetyDistance, mode, obstacles); |
| | | return planner.generate(); |
| | | } |
| | | |
| | | /** |
| | | * ä¿æååå
¼å®¹çéè½½æ¹æ³ï¼ä¸å¸¦ safeDistanceï¼ä½¿ç¨é»è®¤è®¡ç®ï¼ |
| | | */ |
| | | public static List<PathSegment> generatePathSegments(String polygonCoords, |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | | String modeStr) { |
| | | return generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, null, modeStr); |
| | | } |
| | | |
| | | /** |
| | | * éè¿åç¬¦ä¸²åæ°çæå²èè·¯å¾åæ ã |
| | | * |
| | | * @param polygonCoords å¤è¾¹å½¢è¾¹çåæ ï¼æ ¼å¼å¦ "x1,y1;x2,y2;..."ï¼ç±³ï¼ |
| | | * @param polygonCoords å¤è¾¹å½¢è¾¹çåæ ï¼æ ¼å¼å¦ "x1,y1;x2,y2;..."ï¼ç±³ï¼ |
| | | * @param obstaclesCoords éç¢ç©åæ ï¼æ¯æå¤ä¸ªæ¬å·æ®µæåå½¢å®ä¹ï¼ä¾ "(x1,y1;x2,y2)(cx,cy;px,py)" |
| | | * @param mowingWidth å²è宽度å符串ï¼ç±³åä½ï¼å
许ä¿ç两ä½å°æ° |
| | | * @param modeStr å²è模å¼ï¼"0"/空为平è¡çº¿ï¼"1" æ "spiral" è¡¨ç¤ºèºææ¨¡å¼ï¼å½åä»
å¹³è¡çº¿å®ç°ï¼ |
| | | * @param mowingWidth å²è宽度å符串ï¼ç±³åä½ï¼å
许ä¿ç两ä½å°æ° |
| | | * @param safetyDistStr å®å
¨è·ç¦»å符串ï¼ç±³åä½ã |
| | | * @param modeStr å²è模å¼ï¼"0"/空为平è¡çº¿ï¼"1" æ "spiral" è¡¨ç¤ºèºææ¨¡å¼ï¼å½åä»
å¹³è¡çº¿å®ç°ï¼ |
| | | * @return è¿ç»è·¯å¾åæ å符串ï¼é¡ºåºç´§è·å²èæºè¡è¿è·¯çº¿ |
| | | */ |
| | | public static String generatePathFromStrings(String polygonCoords, |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | | String safetyDistStr, |
| | | String modeStr) { |
| | | List<PathSegment> segments = generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, modeStr); |
| | | List<PathSegment> segments = generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, safetyDistStr, modeStr); |
| | | return formatPathSegments(segments); |
| | | } |
| | | |
| | | /** |
| | | * ä¿æååå
¼å®¹çéè½½æ¹æ³ |
| | | */ |
| | | public static String generatePathFromStrings(String polygonCoords, |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | | String modeStr) { |
| | | return generatePathFromStrings(polygonCoords, obstaclesCoords, mowingWidth, null, modeStr); |
| | | } |
| | | |
| | | /** |
| | | * å°è·¯å¾æ®µå表转æ¢ä¸ºåæ å符串ã |
| | |
| | | try { |
| | | return Double.parseDouble(value.trim()); |
| | | } catch (NumberFormatException ex) { |
| | | throw new IllegalArgumentException("å²èå®½åº¦æ ¼å¼ä¸æ£ç¡®: " + value, ex); |
| | | throw new IllegalArgumentException("æ ¼å¼ä¸æ£ç¡®: " + value, ex); |
| | | } |
| | | } |
| | | |
| | |
| | | public boolean isStartPoint; |
| | | public boolean isEndPoint; |
| | | |
| | | PathSegment(Coordinate start, Coordinate end, boolean isMowing) { |
| | | public PathSegment(Coordinate start, Coordinate end, boolean isMowing) { |
| | | this.start = start; |
| | | this.end = end; |
| | | this.isMowing = isMowing; |
| | |
| | | /** |
| | | * å
鍿 ¸å¿è§åå¨ï¼å®ç°ä¸ MowingPathPlanner çæçé»è¾ã |
| | | */ |
| | | private static final class PlannerCore { |
| | | static final class PlannerCore { |
| | | private final List<Coordinate> polygon; |
| | | private final double width; |
| | | private final double safetyDistance; // æ°å¢å®å
¨è·ç¦»å段 |
| | | private final String mode; |
| | | private final List<List<Coordinate>> obstacles; |
| | | private final GeometryFactory gf = new GeometryFactory(); |
| | | |
| | | PlannerCore(List<Coordinate> polygon, double width, String mode, List<List<Coordinate>> obstacles) { |
| | | PlannerCore(List<Coordinate> polygon, double width, double safetyDistance, String mode, List<List<Coordinate>> obstacles) { |
| | | this.polygon = polygon; |
| | | this.width = width; |
| | | this.mode = mode; |
| | | this.obstacles = obstacles != null ? obstacles : new ArrayList<>(); |
| | | |
| | | // åå§åå®å
¨è·ç¦»é»è¾ |
| | | if (Double.isNaN(safetyDistance)) { |
| | | // å¦ææªæä¾ï¼ä½¿ç¨é»è®¤å¼ï¼å®½åº¦çä¸å + 0.05ç±³ |
| | | this.safetyDistance = width / 2.0 + 0.05; |
| | | } else { |
| | | // 妿æä¾äºï¼ä½¿ç¨æä¾çå¼ï¼ä½è³å°è¦ä¿è¯æºå¨ä¸å¿ä¸ç¢°å£ï¼å®½åº¦ä¸åï¼ |
| | | // å
è®¸ç¨æ·è®¾ç½®æ¯ width/2 æ´å¤§ç弿¥è¿ç¦»è¾¹ç |
| | | this.safetyDistance = Math.max(safetyDistance, width / 2.0); |
| | | } |
| | | } |
| | | |
| | | // å
¼å®¹æ§æé 彿° |
| | | PlannerCore(List<Coordinate> polygon, double width, String mode, List<List<Coordinate>> obstacles) { |
| | | this(polygon, width, Double.NaN, mode, obstacles); |
| | | } |
| | | |
| | | List<PathSegment> generate() { |
| | | // 妿æéç¢ç©ï¼ä½¿ç¨å¸¦éç¢ç©é¿è®©çè·¯å¾è§åå¨ |
| | | if (!obstacles.isEmpty()) { |
| | | // 使ç¨è®¡ç®å¥½çå®å
¨è·ç¦» |
| | | ObstaclePathPlanner obstaclePlanner = new ObstaclePathPlanner( |
| | | polygon, width, mode, obstacles, this.safetyDistance); |
| | | return obstaclePlanner.generate(); |
| | | } |
| | | |
| | | // 没æéç¢ç©æ¶ä½¿ç¨åæé»è¾ |
| | | if ("spiral".equals(mode)) { |
| | | return generateSpiralPath(); |
| | | } |
| | | return generateParallelPath(); |
| | | } |
| | | |
| | | private List<PathSegment> generateParallelPath() { |
| | | List<PathSegment> generateParallelPath() { |
| | | List<PathSegment> path = new ArrayList<>(); |
| | | Geometry safeArea = buildSafeArea(); |
| | | if (safeArea == null || safeArea.isEmpty()) { |
| | |
| | | longest.end.y - longest.start.y).normalize(); |
| | | Vector2D perp = baseDir.rotate90CCW(); |
| | | Vector2D baseStartVec = new Vector2D(longest.start.x, longest.start.y); |
| | | double baseProjection = perp.dot(baseStartVec); // keep offsets relative to the longest edge start |
| | | double baseProjection = perp.dot(baseStartVec); |
| | | |
| | | double minProj = Double.POSITIVE_INFINITY; |
| | | double maxProj = Double.NEGATIVE_INFINITY; |
| | |
| | | return path; |
| | | } |
| | | |
| | | private List<PathSegment> generateSpiralPath() { |
| | | List<PathSegment> generateSpiralPath() { |
| | | Geometry safeArea = buildSafeArea(); |
| | | if (safeArea == null || safeArea.isEmpty()) { |
| | | System.err.println("å®å
¨åºåä¸ºç©ºï¼æ æ³çæèºæè·¯å¾"); |
| | |
| | | } |
| | | } |
| | | |
| | | Geometry shrunk = shrinkStraight(result, width / 2.0); |
| | | // ä¿®æ¹ï¼ä½¿ç¨ä¼ å
¥ç safetyDistance æ¥è¿è¡è¾¹çå
缩 |
| | | // ä¹åæ¯ width / 2.0ï¼ç°å¨ä½¿ç¨ this.safetyDistance |
| | | // è¿ç¡®ä¿äºè·¯å¾è§ååºåä¸è¾¹çä¿æç¨æ·æå®çè·ç¦» |
| | | Geometry shrunk = shrinkStraight(result, this.safetyDistance); |
| | | return shrunk.isEmpty() ? result : shrunk; |
| | | } catch (Exception ex) { |
| | | System.err.println("æå»ºå®å
¨åºå失败: " + ex.getMessage()); |
| | |
| | | this.index = index; |
| | | } |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package lujing; |
| | | |
| | | import javax.swing.*; |
| | | import java.awt.*; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | import dikuai.Dikuai; |
| | | import lujing.Lunjingguihua; |
| | | import lujing.ObstaclePathPlanner; |
| | | import org.locationtech.jts.geom.Coordinate; |
| | | |
| | | /** |
| | | * çæå²èè·¯å¾é¡µé¢ |
| | | * ç¬ç«çå¯¹è¯æ¡ç±»ï¼ç¨äºçæåç¼è¾å²èè·¯å¾ |
| | | */ |
| | | public class MowingPathGenerationPage 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); |
| | | |
| | | // æ°æ®ä¿ååè°æ¥å£ |
| | | public interface PathSaveCallback { |
| | | boolean saveBaseStationCoordinates(Dikuai dikuai, String value); |
| | | boolean saveBoundaryCoordinates(Dikuai dikuai, String value); |
| | | boolean saveObstacleCoordinates(Dikuai dikuai, String baseStationValue, String obstacleValue); |
| | | boolean saveMowingWidth(Dikuai dikuai, String value); |
| | | boolean savePlannedPath(Dikuai dikuai, String value); |
| | | } |
| | | |
| | | private final Dikuai dikuai; |
| | | private final PathSaveCallback saveCallback; |
| | | |
| | | // UIç»ä»¶ |
| | | private JTextField baseStationField; |
| | | private JTextArea boundaryArea; |
| | | private JTextArea obstacleArea; |
| | | private JTextField widthField; |
| | | private JTextArea pathArea; |
| | | |
| | | /** |
| | | * æé 彿° |
| | | * @param owner ç¶çªå£ |
| | | * @param dikuai å°å对象 |
| | | * @param baseStationValue åºç«åæ |
| | | * @param boundaryValue å°åè¾¹ç |
| | | * @param obstacleValue éç¢ç©åæ |
| | | * @param widthValue å²è宽度 |
| | | * @param modeValue å²èæ¨¡å¼ |
| | | * @param initialGeneratedPath åå§çæçè·¯å¾ |
| | | * @param saveCallback ä¿ååè°æ¥å£ |
| | | */ |
| | | public MowingPathGenerationPage(Window owner, |
| | | Dikuai dikuai, |
| | | String baseStationValue, |
| | | String boundaryValue, |
| | | String obstacleValue, |
| | | String widthValue, |
| | | String modeValue, |
| | | String initialGeneratedPath, |
| | | PathSaveCallback saveCallback) { |
| | | super(owner, "è·¯å¾è§å页é¢", Dialog.ModalityType.APPLICATION_MODAL); |
| | | this.dikuai = dikuai; |
| | | this.saveCallback = saveCallback; |
| | | |
| | | setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); |
| | | getContentPane().setLayout(new BorderLayout()); |
| | | getContentPane().setBackground(BACKGROUND_COLOR); |
| | | |
| | | initializeUI(baseStationValue, boundaryValue, obstacleValue, |
| | | widthValue, modeValue, initialGeneratedPath); |
| | | |
| | | pack(); |
| | | setSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT)); |
| | | setLocationRelativeTo(owner); |
| | | } |
| | | |
| | | /** |
| | | * åå§åUI |
| | | */ |
| | | private void initializeUI(String baseStationValue, String boundaryValue, |
| | | String obstacleValue, String widthValue, |
| | | String modeValue, String initialGeneratedPath) { |
| | | 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 = getDisplayValue(dikuai.getLandName(), "æªç¥å°å"); |
| | | String landNumber = getDisplayValue(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)); |
| | | |
| | | // åºç«åæ |
| | | baseStationField = createInfoTextField(baseStationValue != null ? baseStationValue : "", true); |
| | | contentPanel.add(createTextFieldSection("åºç«åæ ", baseStationField)); |
| | | |
| | | // å°åè¾¹ç |
| | | boundaryArea = createInfoTextArea(boundaryValue != null ? boundaryValue : "", true, 6); |
| | | contentPanel.add(createTextAreaSection("å°åè¾¹ç", boundaryArea)); |
| | | |
| | | // éç¢ç©åæ |
| | | obstacleArea = createInfoTextArea(obstacleValue != null ? obstacleValue : "", true, 6); |
| | | contentPanel.add(createTextAreaSection("éç¢ç©åæ ", obstacleArea)); |
| | | |
| | | // å²è宽度 |
| | | widthField = createInfoTextField(widthValue != null ? widthValue : "", true); |
| | | contentPanel.add(createTextFieldSection("å²è宽度 (åç±³)", widthField)); |
| | | |
| | | // å²è模å¼ï¼åªè¯»æ¾ç¤ºï¼ |
| | | contentPanel.add(createInfoValueSection("å²è模å¼", formatMowingPatternForDialog(modeValue))); |
| | | |
| | | // å²èè·¯å¾åæ |
| | | String existingPath = prepareCoordinateForEditor(dikuai.getPlannedPath()); |
| | | String pathSeed = initialGeneratedPath != null ? initialGeneratedPath : existingPath; |
| | | pathArea = createInfoTextArea(pathSeed != null ? pathSeed : "", true, 10); |
| | | contentPanel.add(createTextAreaSection("å²èè·¯å¾åæ ", pathArea)); |
| | | |
| | | 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 generateBtn = createPrimaryFooterButton("çæå²èè·¯å¾"); |
| | | JButton saveBtn = createPrimaryFooterButton("ä¿åè·¯å¾"); |
| | | JButton cancelBtn = createPrimaryFooterButton("åæ¶"); |
| | | |
| | | generateBtn.addActionListener(e -> generatePath(modeValue)); |
| | | saveBtn.addActionListener(e -> savePath()); |
| | | cancelBtn.addActionListener(e -> dispose()); |
| | | |
| | | buttonPanel.add(generateBtn); |
| | | buttonPanel.add(saveBtn); |
| | | buttonPanel.add(cancelBtn); |
| | | add(buttonPanel, BorderLayout.SOUTH); |
| | | } |
| | | |
| | | /** |
| | | * çæè·¯å¾ |
| | | */ |
| | | private void generatePath(String modeValue) { |
| | | String sanitizedWidth = sanitizeWidthString(widthField.getText()); |
| | | if (sanitizedWidth != null) { |
| | | try { |
| | | double widthCm = Double.parseDouble(sanitizedWidth); |
| | | widthField.setText(formatWidthForStorage(widthCm)); |
| | | sanitizedWidth = formatWidthForStorage(widthCm); |
| | | } catch (NumberFormatException ex) { |
| | | widthField.setText(sanitizedWidth); |
| | | } |
| | | } |
| | | |
| | | String generated = attemptMowingPathPreview( |
| | | boundaryArea.getText(), |
| | | obstacleArea.getText(), |
| | | sanitizedWidth, |
| | | modeValue, |
| | | this, |
| | | true |
| | | ); |
| | | |
| | | if (generated != null) { |
| | | pathArea.setText(generated); |
| | | pathArea.setCaretPosition(0); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * ä¿åè·¯å¾ |
| | | */ |
| | | private void savePath() { |
| | | String baseStationNormalized = normalizeCoordinateInput(baseStationField.getText()); |
| | | String boundaryNormalized = normalizeCoordinateInput(boundaryArea.getText()); |
| | | if (!"-1".equals(boundaryNormalized)) { |
| | | boundaryNormalized = boundaryNormalized |
| | | .replace("\r\n", ";") |
| | | .replace('\r', ';') |
| | | .replace('\n', ';') |
| | | .replaceAll(";+", ";") |
| | | .replaceAll("\\s*;\\s*", ";") |
| | | .trim(); |
| | | if (boundaryNormalized.isEmpty()) { |
| | | boundaryNormalized = "-1"; |
| | | } |
| | | } |
| | | |
| | | String obstacleNormalized = normalizeCoordinateInput(obstacleArea.getText()); |
| | | if (!"-1".equals(obstacleNormalized)) { |
| | | obstacleNormalized = obstacleNormalized |
| | | .replace("\r\n", " ") |
| | | .replace('\r', ' ') |
| | | .replace('\n', ' ') |
| | | .replaceAll("\\s{2,}", " ") |
| | | .trim(); |
| | | if (obstacleNormalized.isEmpty()) { |
| | | obstacleNormalized = "-1"; |
| | | } |
| | | } |
| | | |
| | | String rawWidthInput = widthField.getText() != null ? widthField.getText().trim() : ""; |
| | | String widthSanitized = sanitizeWidthString(widthField.getText()); |
| | | if (widthSanitized == null) { |
| | | String message = rawWidthInput.isEmpty() ? "请å
设置å²è宽度(åç±³)" : "å²èå®½åº¦æ ¼å¼ä¸æ£ç¡®"; |
| | | JOptionPane.showMessageDialog(this, message, "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | double widthCm; |
| | | try { |
| | | widthCm = Double.parseDouble(widthSanitized); |
| | | } catch (NumberFormatException ex) { |
| | | JOptionPane.showMessageDialog(this, "å²èå®½åº¦æ ¼å¼ä¸æ£ç¡®", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | if (widthCm <= 0) { |
| | | JOptionPane.showMessageDialog(this, "å²è宽度å¿
须大äº0", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | String widthNormalized = formatWidthForStorage(widthCm); |
| | | widthField.setText(widthNormalized); |
| | | |
| | | String pathNormalized = normalizeCoordinateInput(pathArea.getText()); |
| | | if (!"-1".equals(pathNormalized)) { |
| | | pathNormalized = pathNormalized |
| | | .replace("\r\n", ";") |
| | | .replace('\r', ';') |
| | | .replace('\n', ';') |
| | | .replaceAll(";+", ";") |
| | | .replaceAll("\\s*;\\s*", ";") |
| | | .trim(); |
| | | if (pathNormalized.isEmpty()) { |
| | | pathNormalized = "-1"; |
| | | } |
| | | } |
| | | |
| | | if ("-1".equals(pathNormalized)) { |
| | | JOptionPane.showMessageDialog(this, "请å
çæå²èè·¯å¾", "æç¤º", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // è°ç¨åè°ä¿åæ°æ® |
| | | if (saveCallback != null) { |
| | | if (!saveCallback.saveBaseStationCoordinates(dikuai, baseStationNormalized)) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³ä¿ååºç«åæ ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (!saveCallback.saveBoundaryCoordinates(dikuai, boundaryNormalized)) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³ä¿åå°åè¾¹ç", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (!saveCallback.saveObstacleCoordinates(dikuai, baseStationNormalized, obstacleNormalized)) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³ä¿åéç¢ç©åæ ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (!saveCallback.saveMowingWidth(dikuai, widthNormalized)) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³ä¿åå²è宽度", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | if (!saveCallback.savePlannedPath(dikuai, pathNormalized)) { |
| | | JOptionPane.showMessageDialog(this, "æ æ³ä¿åå²èè·¯å¾", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | JOptionPane.showMessageDialog(this, "å²èè·¯å¾å·²ä¿å", "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | dispose(); |
| | | } |
| | | |
| | | /** |
| | | * å°è¯çæè·¯å¾é¢è§ |
| | | */ |
| | | private String attemptMowingPathPreview(String boundaryInput, String obstacleInput, |
| | | String widthCmInput, String modeInput, |
| | | Component parentComponent, boolean showMessages) { |
| | | String boundary = sanitizeValueOrNull(boundaryInput); |
| | | if (boundary == null) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "å½åå°åæªè®¾ç½®è¾¹çåæ ï¼æ æ³çæè·¯å¾", |
| | | "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | String rawWidth = widthCmInput != null ? widthCmInput.trim() : ""; |
| | | String widthStr = sanitizeWidthString(widthCmInput); |
| | | if (widthStr == null) { |
| | | if (showMessages) { |
| | | String message = rawWidth.isEmpty() ? "请å
设置å²è宽度(åç±³)" : "å²èå®½åº¦æ ¼å¼ä¸æ£ç¡®"; |
| | | JOptionPane.showMessageDialog(parentComponent, message, "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | double widthCm; |
| | | try { |
| | | widthCm = Double.parseDouble(widthStr); |
| | | } catch (NumberFormatException ex) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "å²èå®½åº¦æ ¼å¼ä¸æ£ç¡®", |
| | | "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | if (widthCm <= 0) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "å²è宽度å¿
须大äº0", |
| | | "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | double widthMeters = widthCm / 100.0d; |
| | | String plannerWidth = BigDecimal.valueOf(widthMeters) |
| | | .setScale(3, RoundingMode.HALF_UP) |
| | | .stripTrailingZeros() |
| | | .toPlainString(); |
| | | |
| | | // æ£æ¥åå§è¾å
¥æ¯å¦æéç¢ç©ï¼å¨sanitizeä¹åæ£æ¥ï¼é¿å
丢失信æ¯ï¼ |
| | | String rawObstacleInput = obstacleInput != null ? obstacleInput.trim() : ""; |
| | | boolean hasObstacleInput = !rawObstacleInput.isEmpty() && !"-1".equals(rawObstacleInput); |
| | | |
| | | String obstacles = sanitizeValueOrNull(obstacleInput); |
| | | if (obstacles != null) { |
| | | obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' '); |
| | | } |
| | | |
| | | String mode = normalizeExistingMowingPattern(modeInput); |
| | | try { |
| | | // è§£æéç¢ç©å表 |
| | | List<List<Coordinate>> obstacleList = Lunjingguihua.parseObstacles(obstacles); |
| | | if (obstacleList == null) { |
| | | obstacleList = new ArrayList<>(); |
| | | } |
| | | |
| | | // 夿æ¯å¦æéç¢ç©ï¼åªè¦åå§è¾å
¥æéç¢ç©å
容ï¼å°±ä½¿ç¨ObstaclePathPlanner |
| | | // å³ä½¿è§£æåå表为空ï¼ä¹å°è¯ä½¿ç¨ObstaclePathPlannerï¼å®ä¼å¤ç空éç¢ç©å表çæ
åµï¼ |
| | | boolean hasObstacles = hasObstacleInput && !obstacleList.isEmpty(); |
| | | |
| | | // 妿åå§è¾å
¥æéç¢ç©ä½è§£æå¤±è´¥ï¼ç»åºæç¤º |
| | | if (hasObstacleInput && obstacleList.isEmpty()) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, |
| | | "éç¢ç©åæ æ ¼å¼å¯è½ä¸æ£ç¡®ï¼å°å°è¯çæè·¯å¾ãå¦æè·¯å¾ä¸æ£ç¡®ï¼è¯·æ£æ¥éç¢ç©åæ æ ¼å¼ã", |
| | | "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | // ä»ç¶å°è¯ä½¿ç¨ObstaclePathPlannerï¼å³ä½¿éç¢ç©å表为空 |
| | | // è¿æ ·è³å°å¯ä»¥ç¡®ä¿ä½¿ç¨æ£ç¡®çè·¯å¾è§åå¨ |
| | | } |
| | | |
| | | String generated; |
| | | |
| | | if (!hasObstacles && !hasObstacleInput) { |
| | | // å®å
¨æ²¡æéç¢ç©è¾å
¥æ¶ï¼ä½¿ç¨Lunjingguihuaç±»çæ¹æ³çæè·¯å¾ |
| | | generated = Lunjingguihua.generatePathFromStrings( |
| | | boundary, |
| | | obstacles != null ? obstacles : "", |
| | | plannerWidth, |
| | | mode |
| | | ); |
| | | } else { |
| | | // æéç¢ç©è¾å
¥æ¶ï¼å³ä½¿è§£æå¤±è´¥ï¼ï¼ä½¿ç¨ObstaclePathPlannerå¤çè·¯å¾çæ |
| | | List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary); |
| | | if (polygon.size() < 4) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "å¤è¾¹å½¢åæ æ°éä¸è¶³ï¼è³å°éè¦ä¸ä¸ªç¹", |
| | | "é误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | // æ ¹æ®æ¯å¦æéç¢ç©è®¾ç½®ä¸åçå®å
¨è·ç¦» |
| | | double safetyDistance; |
| | | if (!obstacleList.isEmpty()) { |
| | | // æéç¢ç©æ¶ä½¿ç¨å²è宽度çä¸å + 0.05ç±³é¢å¤å®å
¨è·ç¦» |
| | | safetyDistance = widthMeters / 2.0 + 0.05; |
| | | } else { |
| | | // éç¢ç©è§£æå¤±è´¥ä½è¾å
¥åå¨ï¼ä½¿ç¨è¾å°çå®å
¨è·ç¦» |
| | | safetyDistance = 0.01; |
| | | } |
| | | |
| | | ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( |
| | | polygon, widthMeters, mode, obstacleList, safetyDistance); |
| | | List<Lunjingguihua.PathSegment> segments = pathPlanner.generate(); |
| | | generated = Lunjingguihua.formatPathSegments(segments); |
| | | } |
| | | |
| | | String trimmed = generated != null ? generated.trim() : ""; |
| | | if (trimmed.isEmpty()) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "æªçæææçå²èè·¯å¾ï¼è¯·æ£æ¥å°åæ°æ®", |
| | | "æç¤º", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | return null; |
| | | } |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "å²èè·¯å¾å·²çæ", |
| | | "æå", JOptionPane.INFORMATION_MESSAGE); |
| | | } |
| | | return trimmed; |
| | | } catch (IllegalArgumentException ex) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "çæå²èè·¯å¾å¤±è´¥: " + ex.getMessage(), |
| | | "é误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | } catch (Exception ex) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, "çæå²èè·¯å¾æ¶åçå¼å¸¸: " + ex.getMessage(), |
| | | "é误", JOptionPane.ERROR_MESSAGE); |
| | | } |
| | | ex.printStackTrace(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | // ========== UIè¾
婿¹æ³ ========== |
| | | |
| | | 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); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软é
é»", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel, 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 JTextField createInfoTextField(String text, boolean editable) { |
| | | JTextField field = new JTextField(text); |
| | | field.setEditable(editable); |
| | | field.setFont(new Font("微软é
é»", Font.PLAIN, 13)); |
| | | field.setForeground(TEXT_COLOR); |
| | | field.setBackground(editable ? WHITE : new Color(245, 245, 245)); |
| | | field.setCaretPosition(0); |
| | | field.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6)); |
| | | field.setFocusable(true); |
| | | field.setOpaque(true); |
| | | return field; |
| | | } |
| | | |
| | | private JPanel createTextFieldSection(String title, JTextField textField) { |
| | | JPanel section = new JPanel(new BorderLayout(0, 6)); |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | JLabel titleLabel = new JLabel(title); |
| | | titleLabel.setFont(new Font("微软é
é»", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel, BorderLayout.NORTH); |
| | | |
| | | JPanel fieldWrapper = new JPanel(new BorderLayout()); |
| | | fieldWrapper.setBackground(textField.isEditable() ? WHITE : new Color(245, 245, 245)); |
| | | fieldWrapper.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); |
| | | fieldWrapper.add(textField, BorderLayout.CENTER); |
| | | section.add(fieldWrapper, BorderLayout.CENTER); |
| | | |
| | | section.setBorder(BorderFactory.createEmptyBorder(4, 0, 12, 0)); |
| | | return section; |
| | | } |
| | | |
| | | private JPanel createInfoValueSection(String title, String value) { |
| | | JPanel section = new JPanel(); |
| | | section.setLayout(new BoxLayout(section, BoxLayout.X_AXIS)); |
| | | section.setBackground(BACKGROUND_COLOR); |
| | | section.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | section.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); |
| | | |
| | | JLabel titleLabel = new JLabel(title + ":"); |
| | | titleLabel.setFont(new Font("微软é
é»", Font.BOLD, 14)); |
| | | titleLabel.setForeground(TEXT_COLOR); |
| | | section.add(titleLabel); |
| | | section.add(Box.createHorizontalStrut(8)); |
| | | |
| | | JLabel valueLabel = new JLabel(value); |
| | | valueLabel.setFont(new Font("微软é
é»", Font.PLAIN, 14)); |
| | | valueLabel.setForeground(TEXT_COLOR); |
| | | section.add(valueLabel); |
| | | section.add(Box.createHorizontalGlue()); |
| | | 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 getDisplayValue(String value, String defaultValue) { |
| | | if (value == null || value.equals("-1") || value.trim().isEmpty()) { |
| | | return defaultValue; |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | private String prepareCoordinateForEditor(String value) { |
| | | if (value == null) { |
| | | return ""; |
| | | } |
| | | String trimmed = value.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return ""; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private String normalizeCoordinateInput(String input) { |
| | | if (input == null) { |
| | | return "-1"; |
| | | } |
| | | String trimmed = input.trim(); |
| | | return trimmed.isEmpty() ? "-1" : trimmed; |
| | | } |
| | | |
| | | private String sanitizeValueOrNull(String input) { |
| | | if (input == null) { |
| | | return null; |
| | | } |
| | | String trimmed = input.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | return trimmed; |
| | | } |
| | | |
| | | private String sanitizeWidthString(String input) { |
| | | if (input == null) { |
| | | return null; |
| | | } |
| | | String trimmed = input.trim(); |
| | | if (trimmed.isEmpty() || "-1".equals(trimmed)) { |
| | | return null; |
| | | } |
| | | String cleaned = trimmed.replaceAll("[^0-9.+-]", ""); |
| | | return cleaned.isEmpty() ? null : cleaned; |
| | | } |
| | | |
| | | private String formatWidthForStorage(double widthCm) { |
| | | return BigDecimal.valueOf(widthCm) |
| | | .setScale(2, RoundingMode.HALF_UP) |
| | | .stripTrailingZeros() |
| | | .toPlainString(); |
| | | } |
| | | |
| | | private String formatMowingPatternForDialog(String patternValue) { |
| | | String sanitized = sanitizeValueOrNull(patternValue); |
| | | if (sanitized == null) { |
| | | return "æªè®¾ç½®"; |
| | | } |
| | | String normalized = normalizeExistingMowingPattern(sanitized); |
| | | if ("parallel".equals(normalized)) { |
| | | return "å¹³è¡æ¨¡å¼ (parallel)"; |
| | | } |
| | | if ("spiral".equals(normalized)) { |
| | | return "èºææ¨¡å¼ (spiral)"; |
| | | } |
| | | return sanitized; |
| | | } |
| | | |
| | | private String normalizeExistingMowingPattern(String patternValue) { |
| | | if (patternValue == null) { |
| | | return "parallel"; |
| | | } |
| | | String trimmed = patternValue.trim().toLowerCase(); |
| | | if ("1".equals(trimmed) || "spiral".equals(trimmed)) { |
| | | return "spiral"; |
| | | } |
| | | return "parallel"; |
| | | } |
| | | } |
| | | |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package lujing; |
| | | |
| | | import org.locationtech.jts.algorithm.Angle; |
| | | import org.locationtech.jts.geom.*; |
| | | import org.locationtech.jts.operation.distance.DistanceOp; |
| | | import org.locationtech.jts.operation.union.CascadedPolygonUnion; |
| | | |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * éç¢ç©è·¯å¾è§åå¨ |
| | | * * ä¼åæ¹æ¡ï¼ |
| | | * 1. é¢è§åï¼å
ä¸èèéç¢ç©ï¼è§ååºè¦çå
¨å¾çå¹³è¡çº¿è·¯å¾ï¼å¼åå½¢ï¼ã |
| | | * 2. å ä½åå²ï¼æ ¹æ®éç¢ç©ç夿©ï¼å®å
¨è·ç¦»ï¼å ä½ä½ï¼å°è·¯å¾åæï¼ç§»é¤è½å¨éç¢ç©å
çé¨åã |
| | | * 3. åºåéç»ï¼å°å©ä½çè·¯å¾æ®µæè¿éæ§è类为è¥å¹²ä¸ªâè¿ç»åºåâï¼æ¯ä¸ªåºåå
é¨éè¿å¼åå½¢è¿æ¥ã |
| | | * 4. å
¨å±è¿æ¥ï¼ä½¿ç¨é¿éç®æ³ï¼A*å¯è§å¾ï¼å°è¿äºå¤ç«çåºå串èèµ·æ¥ã |
| | | * * * ä¿®å¤é®é¢ï¼ |
| | | * 1. ä¿®å¤è·¯å¾ç©¿è¶éç¢ç©çé®é¢ï¼éè¿æ´ä¸¥æ ¼ç Interior Intersection æ£æ¥ï¼ã |
| | | * 2. ä¿®å¤è·¯å¾è¶
åºå°åè¾¹ççé®é¢ï¼éè¿å å
¥ Boundary Covers 约æï¼ã |
| | | * 3. [æ¬æ¬¡ä¿®å¤] ä¿®å¤ IntersectionMatrix ç±»åæ¥éï¼ä½¿ç¨æ£ç¡®çç©éµåå
æ ¼æ£æ¥æ¿ä»£ä¸åå¨çæ¹æ³ã |
| | | * * * äºæ¬¡ä¼åï¼éå¯¹ç¨æ·åé¦ï¼ï¼ |
| | | * 1. å¼å
¥ Tolerance Buffer (å
缩) ç¨äºç¸äº¤æ£æµï¼è§£å³âæ¥è§¦å³ç¸äº¤â导è´çåæ³è¾¹ç¼è·¯å¾è¢«è¯¯å¤é®é¢ã |
| | | * 2. å¢å¼º isLineSafe çè¾¹ç约æï¼ç¡®ä¿è·¯å¾ä¸¥æ ¼å¨ Boundary å
é¨ã |
| | | * 3. ä¼å findSafePathï¼å¢å ç¹ä½æ ¡éªä¸å¸éï¼Snapï¼ï¼é²æ¢å æµ®ç¹è¯¯å·®å¯¼è´ç寻路失败èåéå°ç´çº¿ã |
| | | */ |
| | | public class ObstaclePathPlanner { |
| | | private final List<Coordinate> polygon; // å²èåºåè¾¹çç¹é |
| | | private final double width; // å²è宽度 |
| | | private final String mode; // æ¨¡å¼ |
| | | private final List<List<Coordinate>> obstacles; // éç¢ç©å表 |
| | | private final double safetyDistance; // å®å
¨è·ç¦» |
| | | private final GeometryFactory gf = new GeometryFactory(); |
| | | |
| | | // ä¿åå°åçå ä½å½¢ç¶ï¼ç¨äºè¾¹ççº¦ææ£æ¥ |
| | | private Geometry boundaryGeom; |
| | | // ç¨äºæ£æµçéç¢ç©å ä½ä½ï¼å¯è½ç»è¿å¾®è°ï¼ |
| | | private Geometry checkObstacleGeom; |
| | | |
| | | public ObstaclePathPlanner(List<Coordinate> polygon, double width, String mode, |
| | | List<List<Coordinate>> obstacles, double safetyDistance) { |
| | | this.polygon = polygon; |
| | | this.width = width; |
| | | this.mode = mode; |
| | | this.obstacles = obstacles != null ? obstacles : new ArrayList<>(); |
| | | this.safetyDistance = safetyDistance; |
| | | |
| | | initBoundaryGeom(); |
| | | } |
| | | |
| | | /** |
| | | * åå§åè¾¹çå ä½ä½ï¼ç¨äºåç»ççº¦ææ£æ¥ |
| | | */ |
| | | private void initBoundaryGeom() { |
| | | if (polygon == null || polygon.size() < 3) { |
| | | this.boundaryGeom = gf.createPolygon(); |
| | | return; |
| | | } |
| | | List<Coordinate> closed = new ArrayList<>(polygon); |
| | | if (!closed.get(0).equals2D(closed.get(closed.size() - 1))) { |
| | | closed.add(new Coordinate(closed.get(0))); |
| | | } |
| | | LinearRing ring = gf.createLinearRing(closed.toArray(new Coordinate[0])); |
| | | Polygon poly = gf.createPolygon(ring); |
| | | // ç¡®ä¿å ä½æææ§ |
| | | this.boundaryGeom = poly.isValid() ? poly : poly.buffer(0); |
| | | } |
| | | |
| | | /** |
| | | * çæè·¯å¾ç主å
¥å£ |
| | | */ |
| | | public List<Lunjingguihua.PathSegment> generate() { |
| | | // 1. çæåå§ç宿´å¹³è¡çº¿è·¯å¾ï¼å¿½ç¥éç¢ç©ï¼ |
| | | List<Lunjingguihua.PathSegment> fullPath = generateFullPathWithoutObstacles(); |
| | | if (fullPath.isEmpty()) return fullPath; |
| | | |
| | | // 2. æå»ºéç¢ç©ç夿©å®å
¨åºå (Buffer) |
| | | Geometry obstacleBuffer = createObstacleBuffer(); |
| | | |
| | | // åå§åç¨äºæ£æµçå ä½ä½ï¼å
缩ä¸ç¹ç¹ï¼å
许路å¾è´´è¾¹ï¼ |
| | | if (!obstacleBuffer.isEmpty()) { |
| | | this.checkObstacleGeom = obstacleBuffer.buffer(-0.01); // å
缩1cmï¼å®¹å¿æµ®ç¹è¯¯å·® |
| | | } else { |
| | | this.checkObstacleGeom = obstacleBuffer; |
| | | } |
| | | |
| | | if (obstacleBuffer.isEmpty()) return fullPath; |
| | | |
| | | // 3. åå²è·¯å¾ï¼ç§»é¤ä¸éç¢ç©éå çé¨å |
| | | List<Lunjingguihua.PathSegment> clippedSegments = clipPathWithObstacles(fullPath, obstacleBuffer); |
| | | if (clippedSegments.isEmpty()) return new ArrayList<>(); |
| | | |
| | | // 4. éæ°è§åï¼å°ç¢çåç线段éç»æè¿ç»çå¼å形路å¾ï¼å¹¶è¿è¡é¿éè¿æ¥ |
| | | List<Lunjingguihua.PathSegment> finalPath = reorganizeAndConnectPath(clippedSegments, obstacleBuffer); |
| | | |
| | | // 5. åå¤çï¼æ è®°èµ·ç»ç¹çï¼ |
| | | postProcessPath(finalPath); |
| | | |
| | | return finalPath; |
| | | } |
| | | |
| | | /** |
| | | * æ¥éª¤1ï¼è°ç¨æ ¸å¿åºçææ éç¢çå¹³è¡çº¿è·¯å¾ |
| | | */ |
| | | private List<Lunjingguihua.PathSegment> generateFullPathWithoutObstacles() { |
| | | Lunjingguihua.PlannerCore tempPlanner = new Lunjingguihua.PlannerCore( |
| | | polygon, width, mode, new ArrayList<>()); |
| | | return tempPlanner.generateParallelPath(); |
| | | } |
| | | |
| | | /** |
| | | * æ¥éª¤2ï¼å建ææéç¢ç©çå¹¶é + å®å
¨è·ç¦»å¤æ© |
| | | */ |
| | | private Geometry createObstacleBuffer() { |
| | | if (obstacles.isEmpty()) return gf.createPolygon(); |
| | | |
| | | List<Geometry> geoms = new ArrayList<>(); |
| | | for (List<Coordinate> obs : obstacles) { |
| | | if (obs == null || obs.size() < 3) continue; |
| | | List<Coordinate> closed = new ArrayList<>(obs); |
| | | if (!closed.get(0).equals2D(closed.get(closed.size() - 1))) { |
| | | closed.add(new Coordinate(closed.get(0))); |
| | | } |
| | | LinearRing ring = gf.createLinearRing(closed.toArray(new Coordinate[0])); |
| | | Polygon poly = gf.createPolygon(ring); |
| | | if (!poly.isValid()) poly = (Polygon) poly.buffer(0); |
| | | geoms.add(poly); |
| | | } |
| | | |
| | | if (geoms.isEmpty()) return gf.createPolygon(); |
| | | |
| | | Geometry union = CascadedPolygonUnion.union(geoms); |
| | | // 对åå¹¶åçéç¢ç©è¿è¡å¤æ©ï¼Bufferï¼ |
| | | Geometry buffered = union.buffer(safetyDistance, 8); |
| | | return buffered.isValid() ? buffered : buffered.buffer(0); |
| | | } |
| | | |
| | | /** |
| | | * æ¥éª¤3ï¼ç¨éç¢ç©å ä½ä½åå²è·¯å¾ |
| | | */ |
| | | private List<Lunjingguihua.PathSegment> clipPathWithObstacles(List<Lunjingguihua.PathSegment> fullPath, Geometry obstacleBuffer) { |
| | | List<Lunjingguihua.PathSegment> validSegments = new ArrayList<>(); |
| | | |
| | | for (Lunjingguihua.PathSegment seg : fullPath) { |
| | | if (!seg.isMowing) continue; |
| | | |
| | | LineString line = gf.createLineString(new Coordinate[]{seg.start, seg.end}); |
| | | Geometry diff; |
| | | try { |
| | | diff = line.difference(obstacleBuffer); |
| | | } catch (Exception e) { |
| | | continue; |
| | | } |
| | | |
| | | if (diff.isEmpty()) continue; |
| | | |
| | | for (int i = 0; i < diff.getNumGeometries(); i++) { |
| | | Geometry g = diff.getGeometryN(i); |
| | | if (g instanceof LineString) { |
| | | Coordinate[] coords = g.getCoordinates(); |
| | | for (int k = 0; k < coords.length - 1; k++) { |
| | | Coordinate p1 = coords[k]; |
| | | Coordinate p2 = coords[k+1]; |
| | | if (p1.distance(p2) > 0.1) { |
| | | validSegments.add(new Lunjingguihua.PathSegment(p1, p2, true)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return validSegments; |
| | | } |
| | | |
| | | /** |
| | | * æ¥éª¤4ï¼æ ¸å¿éç»é»è¾ |
| | | */ |
| | | private List<Lunjingguihua.PathSegment> reorganizeAndConnectPath(List<Lunjingguihua.PathSegment> segments, Geometry obstacleBuffer) { |
| | | if (segments.isEmpty()) return new ArrayList<>(); |
| | | |
| | | // --- A. åææ«æçº¿æ¹å --- |
| | | Lunjingguihua.PathSegment firstSeg = segments.get(0); |
| | | double angle = Math.atan2(firstSeg.end.y - firstSeg.start.y, firstSeg.end.x - firstSeg.start.x); |
| | | double sin = Math.sin(angle); |
| | | double cos = Math.cos(angle); |
| | | |
| | | // --- B. ç»æ¯ä¸ªçº¿æ®µåé
æ«æçº¿ç´¢å¼ (Grid Index) --- |
| | | double gridStep = width; |
| | | |
| | | class IndexedSegment { |
| | | final Lunjingguihua.PathSegment segment; |
| | | final int gridIndex; |
| | | final double projectVal; |
| | | |
| | | IndexedSegment(Lunjingguihua.PathSegment s) { |
| | | this.segment = s; |
| | | double cx = (s.start.x + s.end.x) / 2; |
| | | double cy = (s.start.y + s.end.y) / 2; |
| | | double perpDist = -cx * sin + cy * cos; |
| | | this.gridIndex = (int) Math.floor(perpDist / gridStep); |
| | | this.projectVal = cx * cos + cy * sin; |
| | | } |
| | | } |
| | | |
| | | List<IndexedSegment> indexedSegments = segments.stream() |
| | | .map(IndexedSegment::new) |
| | | .sorted(Comparator.comparingInt((IndexedSegment s) -> s.gridIndex) |
| | | .thenComparingDouble(s -> s.projectVal)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // --- C. æå»ºâåºåé¾â (Zones) --- |
| | | List<List<Lunjingguihua.PathSegment>> zones = new ArrayList<>(); |
| | | Set<IndexedSegment> visited = new HashSet<>(); |
| | | |
| | | while (visited.size() < indexedSegments.size()) { |
| | | IndexedSegment startNode = null; |
| | | for (IndexedSegment is : indexedSegments) { |
| | | if (!visited.contains(is)) { |
| | | startNode = is; |
| | | break; |
| | | } |
| | | } |
| | | if (startNode == null) break; |
| | | |
| | | List<Lunjingguihua.PathSegment> zone = new ArrayList<>(); |
| | | zone.add(startNode.segment); |
| | | visited.add(startNode); |
| | | |
| | | IndexedSegment current = startNode; |
| | | boolean lookingForNext = true; |
| | | |
| | | while (lookingForNext) { |
| | | IndexedSegment bestNext = null; |
| | | double minDistance = Double.MAX_VALUE; |
| | | |
| | | // æç´¢æä½³åç»çº¿æ®µ |
| | | for (int i = 0; i < indexedSegments.size(); i++) { |
| | | IndexedSegment candidate = indexedSegments.get(i); |
| | | if (visited.contains(candidate)) continue; |
| | | |
| | | if (Math.abs(candidate.gridIndex - current.gridIndex) > 1) continue; |
| | | |
| | | double d = current.segment.end.distance(candidate.segment.start); |
| | | |
| | | if (d > width * 3.0) continue; |
| | | |
| | | if (d < minDistance) { |
| | | // ä½¿ç¨ checkObstacleGeom è¿è¡æ£æµ |
| | | if (isLineSafe(current.segment.end, candidate.segment.start)) { |
| | | minDistance = d; |
| | | bestNext = candidate; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (bestNext != null) { |
| | | zone.add(bestNext.segment); |
| | | visited.add(bestNext); |
| | | current = bestNext; |
| | | } else { |
| | | lookingForNext = false; |
| | | } |
| | | } |
| | | zones.add(zone); |
| | | } |
| | | |
| | | // --- D. è¿æ¥ææ Zones --- |
| | | List<Lunjingguihua.PathSegment> resultPath = new ArrayList<>(); |
| | | |
| | | List<List<Lunjingguihua.PathSegment>> remainingZones = new ArrayList<>(zones); |
| | | List<Lunjingguihua.PathSegment> currentProcessingZone = remainingZones.remove(0); |
| | | |
| | | addZoneToPath(resultPath, currentProcessingZone, obstacleBuffer, false); |
| | | |
| | | while (!remainingZones.isEmpty()) { |
| | | Lunjingguihua.PathSegment lastSeg = resultPath.get(resultPath.size() - 1); |
| | | Coordinate currentPos = lastSeg.end; |
| | | |
| | | int bestZoneIdx = -1; |
| | | double minDist = Double.MAX_VALUE; |
| | | |
| | | for (int i = 0; i < remainingZones.size(); i++) { |
| | | List<Lunjingguihua.PathSegment> z = remainingZones.get(i); |
| | | if (z.isEmpty()) continue; |
| | | double d = currentPos.distance(z.get(0).start); |
| | | if (d < minDist) { |
| | | minDist = d; |
| | | bestZoneIdx = i; |
| | | } |
| | | } |
| | | |
| | | if (bestZoneIdx != -1) { |
| | | List<Lunjingguihua.PathSegment> nextZone = remainingZones.remove(bestZoneIdx); |
| | | addZoneToPath(resultPath, nextZone, obstacleBuffer, true); |
| | | } else { |
| | | break; |
| | | } |
| | | } |
| | | |
| | | return resultPath; |
| | | } |
| | | |
| | | /** |
| | | * å°ä¸ä¸ª Zone æ·»å å°ç»æè·¯å¾ä¸ |
| | | */ |
| | | private void addZoneToPath(List<Lunjingguihua.PathSegment> path, |
| | | List<Lunjingguihua.PathSegment> zone, |
| | | Geometry obstacleBuffer, |
| | | boolean needConnectToZoneStart) { |
| | | if (zone.isEmpty()) return; |
| | | |
| | | // 1. è¿æ¥å° Zone çèµ·ç¹ |
| | | if (needConnectToZoneStart && !path.isEmpty()) { |
| | | Coordinate from = path.get(path.size() - 1).end; |
| | | Coordinate to = zone.get(0).start; |
| | | List<Coordinate> travel = findSafePath(from, to, obstacleBuffer); |
| | | if (travel.size() > 1) { |
| | | for (int i = 0; i < travel.size() - 1; i++) { |
| | | path.add(new Lunjingguihua.PathSegment(travel.get(i), travel.get(i+1), false)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 2. å¤ç Zone å
é¨ |
| | | for (int i = 0; i < zone.size(); i++) { |
| | | Lunjingguihua.PathSegment seg = zone.get(i); |
| | | |
| | | if (i > 0) { |
| | | Coordinate prevEnd = zone.get(i-1).end; |
| | | Coordinate currStart = seg.start; |
| | | if (!prevEnd.equals2D(currStart)) { |
| | | if (isLineSafe(prevEnd, currStart)) { |
| | | path.add(new Lunjingguihua.PathSegment(prevEnd, currStart, false)); |
| | | } else { |
| | | List<Coordinate> detour = findSafePath(prevEnd, currStart, obstacleBuffer); |
| | | if (detour.size() > 1) { |
| | | for (int k = 0; k < detour.size() - 1; k++) { |
| | | path.add(new Lunjingguihua.PathSegment(detour.get(k), detour.get(k+1), false)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | path.add(seg); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ£æ¥ä¸¤ç¹è¿çº¿æ¯å¦å®å
¨ |
| | | * ä¿®æ¹ç¹ï¼ |
| | | * 1. ä¸¥æ ¼æ£æ¥ Boundary (Covers) |
| | | * 2. ä½¿ç¨ checkObstacleGeom (å
缩ç) æ£æ¥éç¢ç©ï¼å
许贴边 |
| | | * 3. [Fix] ä½¿ç¨ matrix.get() 代æ¿ä¸åå¨ç isIntersects(int) |
| | | */ |
| | | private boolean isLineSafe(Coordinate p1, Coordinate p2) { |
| | | if (p1.equals2D(p2)) return true; |
| | | LineString line = gf.createLineString(new Coordinate[]{p1, p2}); |
| | | |
| | | // 1. è¾¹ç约æï¼çº¿æ®µå¿
é¡»å®å
¨å¨å°åå
é¨ |
| | | if (boundaryGeom != null && !boundaryGeom.covers(line)) { |
| | | return false; |
| | | } |
| | | |
| | | // 2. é¿é约æï¼ä½¿ç¨å
缩åç buffer æ£æ¥ |
| | | // 妿 checkObstacleGeom ä¸ºç©ºï¼æ éç¢ï¼ï¼åå®å
¨ |
| | | if (checkObstacleGeom == null || checkObstacleGeom.isEmpty()) return true; |
| | | |
| | | IntersectionMatrix matrix = line.relate(checkObstacleGeom); |
| | | |
| | | // æä»¬è¦æ£æ¥ï¼çº¿æ®µçä»»ä½é¨åï¼Interiorï¼æ¯å¦ç©¿è¿éç¢ç©å
é¨ï¼Interiorï¼ |
| | | // æè
线段ç端ç¹ï¼Boundaryï¼æ¯å¦å¨éç¢ç©å
é¨ï¼Interiorï¼ |
| | | // 妿䏤è
齿¯ Dimension.FALSE (-1)ï¼åè¯´ææ²¡æç©¿è¿å
é¨ |
| | | |
| | | boolean interiorIntersects = matrix.get(Location.INTERIOR, Location.INTERIOR) != Dimension.FALSE; |
| | | boolean boundaryIntersects = matrix.get(Location.BOUNDARY, Location.INTERIOR) != Dimension.FALSE; |
| | | |
| | | return !interiorIntersects && !boundaryIntersects; |
| | | } |
| | | |
| | | /** |
| | | * 寻æ¾ä¸¤ç¹é´çå®å
¨è·¯å¾ |
| | | * ä¿®æ¹ç¹ï¼ |
| | | * 1. å¢å ç¹ä½æ ¡éªä¸å¸éï¼Snapï¼ï¼ç¡®ä¿èµ·ç¹ç»ç¹ææ |
| | | * 2. ç§»é¤ç´çº¿å¼ºå¶åéï¼è¥å¯»è·¯å¤±è´¥åè¿åç©ºï¼æä¿çèµ·ç¹ï¼ï¼é¿å
ç©¿å¢ |
| | | */ |
| | | private List<Coordinate> findSafePath(Coordinate start, Coordinate end, Geometry obstacleBuffer) { |
| | | // 0. æ°æ®æ¸
æ´ï¼å¸éèµ·ç¹ç»ç¹å°åæ³åºå |
| | | Coordinate safeStart = snapPointToValid(start); |
| | | Coordinate safeEnd = snapPointToValid(end); |
| | | |
| | | List<Coordinate> path = new ArrayList<>(); |
| | | |
| | | // 1. å°è¯ç´è¿ |
| | | if (isLineSafe(safeStart, safeEnd)) { |
| | | path.add(safeStart); |
| | | path.add(safeEnd); |
| | | return path; |
| | | } |
| | | |
| | | // 2. æå»ºå¯è§å¾ |
| | | Set<Coordinate> nodes = new HashSet<>(); |
| | | nodes.add(safeStart); |
| | | nodes.add(safeEnd); |
| | | |
| | | // æåéç¢ç©é¡¶ç¹ |
| | | addPolygonVertices(obstacleBuffer, nodes); |
| | | // æåè¾¹çé¡¶ç¹ï¼å
³é®ï¼å¤çå¹å½¢è¾¹çï¼ |
| | | addPolygonVertices(boundaryGeom, nodes); |
| | | |
| | | List<Coordinate> nodeList = new ArrayList<>(nodes); |
| | | |
| | | // æå»ºé»æ¥è¡¨ |
| | | Map<Coordinate, List<Coordinate>> graph = new HashMap<>(); |
| | | for (Coordinate c1 : nodeList) { |
| | | for (Coordinate c2 : nodeList) { |
| | | if (c1 == c2) continue; |
| | | if (isLineSafe(c1, c2)) { |
| | | graph.computeIfAbsent(c1, k -> new ArrayList<>()).add(c2); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Dijkstra 寻路 |
| | | Map<Coordinate, Double> dist = new HashMap<>(); |
| | | Map<Coordinate, Coordinate> prev = new HashMap<>(); |
| | | PriorityQueue<Coordinate> pq = new PriorityQueue<>(Comparator.comparingDouble(dist::get)); |
| | | |
| | | for (Coordinate n : nodeList) dist.put(n, Double.MAX_VALUE); |
| | | dist.put(safeStart, 0.0); |
| | | pq.add(safeStart); |
| | | |
| | | while (!pq.isEmpty()) { |
| | | Coordinate u = pq.poll(); |
| | | if (u.equals2D(safeEnd)) break; |
| | | if (dist.get(u) == Double.MAX_VALUE) break; |
| | | |
| | | if (graph.containsKey(u)) { |
| | | for (Coordinate v : graph.get(u)) { |
| | | double alt = dist.get(u) + u.distance(v); |
| | | if (alt < dist.get(v)) { |
| | | dist.put(v, alt); |
| | | prev.put(v, u); |
| | | pq.add(v); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // éæè·¯å¾ |
| | | if (prev.containsKey(safeEnd)) { |
| | | LinkedList<Coordinate> p = new LinkedList<>(); |
| | | Coordinate curr = safeEnd; |
| | | while (curr != null) { |
| | | p.addFirst(curr); |
| | | curr = prev.get(curr); |
| | | } |
| | | return p; |
| | | } |
| | | |
| | | // 寻路失败ï¼è¿å空ï¼é¿å
é误çç´çº¿ç©¿è¶ï¼ |
| | | return path; |
| | | } |
| | | |
| | | // è¾
å©ï¼éªè¯å¹¶å¸éç¹å°åæ³åºå |
| | | private Coordinate snapPointToValid(Coordinate p) { |
| | | Point point = gf.createPoint(p); |
| | | boolean inBoundary = (boundaryGeom == null) || boundaryGeom.covers(point); |
| | | boolean outObstacle = (checkObstacleGeom == null) || !checkObstacleGeom.contains(point); // ä½¿ç¨ contains è䏿¯ intersects interiorï¼ç¨å¾®ä¸¥æ ¼ç¹ |
| | | |
| | | if (inBoundary && outObstacle) return p; |
| | | |
| | | // å¦æç¹æ æï¼å°è¯æ¾æè¿çææç¹ï¼è¾¹çæéç¢ç©è¾¹ç¼ï¼ |
| | | // è¿éç®åå¤çï¼å¦æå¨éç¢ç©å
ï¼å¸éå°éç¢ç©è¾¹çï¼å¦æå¨è¾¹çå¤ï¼å¸éå°è¾¹ç |
| | | // å®é
ä¸ JTS DistanceOp.nearestPoints å¯ä»¥åè¿ä¸ª |
| | | |
| | | Geometry target = boundaryGeom; |
| | | if (!outObstacle && checkObstacleGeom != null) { |
| | | // å¨éç¢ç©å
ï¼ä¼å
å¸éåºéç¢ç© |
| | | Coordinate[] nearest = DistanceOp.nearestPoints(point, checkObstacleGeom.getBoundary()); |
| | | return nearest[1]; |
| | | } |
| | | |
| | | if (!inBoundary && boundaryGeom != null) { |
| | | Coordinate[] nearest = DistanceOp.nearestPoints(point, boundaryGeom); |
| | | return nearest[1]; |
| | | } |
| | | |
| | | return p; |
| | | } |
| | | |
| | | private void addPolygonVertices(Geometry geom, Set<Coordinate> nodes) { |
| | | if (geom == null) return; |
| | | if (geom instanceof Polygon) { |
| | | Collections.addAll(nodes, ((Polygon) geom).getExteriorRing().getCoordinates()); |
| | | for(int i=0; i<((Polygon)geom).getNumInteriorRing(); i++) { |
| | | Collections.addAll(nodes, ((Polygon)geom).getInteriorRingN(i).getCoordinates()); |
| | | } |
| | | } else if (geom instanceof MultiPolygon) { |
| | | MultiPolygon mp = (MultiPolygon) geom; |
| | | for (int i = 0; i < mp.getNumGeometries(); i++) { |
| | | addPolygonVertices(mp.getGeometryN(i), nodes); |
| | | } |
| | | } else if (geom instanceof GeometryCollection) { |
| | | GeometryCollection gc = (GeometryCollection) geom; |
| | | for (int i = 0; i < gc.getNumGeometries(); i++) { |
| | | addPolygonVertices(gc.getGeometryN(i), nodes); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * åå¤çï¼ç§»é¤ççº¿æ®µï¼æ è®°èµ·ç»ç¹ |
| | | */ |
| | | private void postProcessPath(List<Lunjingguihua.PathSegment> path) { |
| | | if (path.isEmpty()) return; |
| | | path.removeIf(seg -> seg.start.distance(seg.end) < 0.05); |
| | | for (Lunjingguihua.PathSegment seg : path) { |
| | | seg.isStartPoint = false; |
| | | seg.isEndPoint = false; |
| | | } |
| | | boolean startFound = false; |
| | | for (Lunjingguihua.PathSegment seg : path) { |
| | | if (seg.isMowing) { |
| | | seg.setAsStartPoint(); |
| | | startFound = true; |
| | | break; |
| | | } |
| | | } |
| | | for (int i = path.size() - 1; i >= 0; i--) { |
| | | Lunjingguihua.PathSegment seg = path.get(i); |
| | | if (seg.isMowing) { |
| | | seg.setAsEndPoint(); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | /** |
| | | * éç¢ç©ç»å¶ç±» - è´è´£ç»å¶å°åä¸çéç¢ç© |
| | | * |
| | | * 注æï¼éç¢ç©å¾å±éè¦å¤äºå°åå导èªè·¯å¾ä¸æ¹ã |
| | | * å¨ MapRenderer.renderMap() ä¸ï¼ç»å¶é¡ºåºåºä¸ºï¼ |
| | | * 1. å°åè¾¹çï¼åºå±ï¼ |
| | | * 2. 导èªè·¯å¾ï¼ä¸å±ï¼ |
| | | * 3. éç¢ç©ï¼é¡¶å±ï¼æ¾ç¤ºå¨å°åå导èªè·¯å¾ä¸æ¹ï¼ |
| | | */ |
| | | public class Obstacledraw { |
| | | |
| | | // é¢è²å®ä¹ |
| | | private static final Color CIRCLE_FILL_COLOR = new Color(255, 182, 193, 120); // å形填å
è² - æµ
ç²çº¢ |
| | | private static final Color CIRCLE_FILL_COLOR = new Color(128, 128, 128, 128); // å形填å
è² - ç°è²ï¼éæåº¦50% |
| | | private static final Color CIRCLE_BORDER_COLOR = new Color(199, 21, 133); // å形边æ¡è² - æ·±ç²çº¢ |
| | | private static final Color POLYGON_FILL_COLOR = new Color(173, 216, 230, 120); // å¤è¾¹å½¢å¡«å
è² - æµ
è |
| | | private static final Color POLYGON_FILL_COLOR = new Color(128, 128, 128, 128); // å¤è¾¹å½¢å¡«å
è² - ç°è²ï¼éæåº¦50% |
| | | private static final Color POLYGON_BORDER_COLOR = new Color(25, 25, 112); // å¤è¾¹å½¢è¾¹æ¡è² - æ·±è |
| | | private static final Color OBSTACLE_LABEL_COLOR = Color.BLACK; |
| | | private static final Color OBSTACLE_POINT_COLOR = Color.RED; |
| | | |
| | | // 尺寸å®ä¹ |
| | | private static final double OBSTACLE_POINT_SIZE = 0.1; // éç¢ç©æ§å¶ç¹å¤§å°ï¼ç±³ï¼ |
| | | private static final float DEFAULT_BORDER_WIDTH = 1.0f; |
| | | private static final float SELECTED_BORDER_WIDTH = 2.5f; |
| | | // è¾¹ç线宽度ä¸å°åè¾¹ç线宽度ä¸è´ï¼3 / Math.max(0.5, scale) |
| | | private static final float BOUNDARY_STROKE_BASE = 3.0f; // ä¸å°åè¾¹ç线宽度ä¸è´ |
| | | private static final float SELECTED_WIDTH_MULTIPLIER = 1.5f; // é䏿¶çå®½åº¦åæ° |
| | | |
| | | /** |
| | | * ç»å¶å°åçææéç¢ç© |
| | |
| | | g2d.setColor(CIRCLE_FILL_COLOR); |
| | | g2d.fill(circle); |
| | | |
| | | // 设置边æ¡é¢è²å宽度 |
| | | // 设置边æ¡é¢è²å宽度ï¼ä¸å°åè¾¹ç线宽度ä¸è´ï¼ |
| | | float strokeWidth = (float)(BOUNDARY_STROKE_BASE / Math.max(0.5, scale)); |
| | | if (isSelected) { |
| | | g2d.setColor(CIRCLE_BORDER_COLOR.darker()); |
| | | g2d.setStroke(new BasicStroke(SELECTED_BORDER_WIDTH / (float)scale)); |
| | | // é䏿¶ç¨å¾®å ç² |
| | | g2d.setStroke(new BasicStroke(strokeWidth * SELECTED_WIDTH_MULTIPLIER)); |
| | | } else { |
| | | g2d.setColor(CIRCLE_BORDER_COLOR); |
| | | g2d.setStroke(new BasicStroke(DEFAULT_BORDER_WIDTH / (float)scale)); |
| | | g2d.setStroke(new BasicStroke(strokeWidth)); |
| | | } |
| | | g2d.draw(circle); |
| | | |
| | |
| | | g2d.setColor(POLYGON_FILL_COLOR); |
| | | g2d.fill(polygon); |
| | | |
| | | // 设置边æ¡é¢è²å宽度 |
| | | // 设置边æ¡é¢è²å宽度ï¼ä¸å°åè¾¹ç线宽度ä¸è´ï¼ |
| | | float strokeWidth = (float)(BOUNDARY_STROKE_BASE / Math.max(0.5, scale)); |
| | | if (isSelected) { |
| | | g2d.setColor(POLYGON_BORDER_COLOR.darker()); |
| | | g2d.setStroke(new BasicStroke(SELECTED_BORDER_WIDTH / (float)scale)); |
| | | // é䏿¶ç¨å¾®å ç² |
| | | g2d.setStroke(new BasicStroke(strokeWidth * SELECTED_WIDTH_MULTIPLIER)); |
| | | } else { |
| | | g2d.setColor(POLYGON_BORDER_COLOR); |
| | | g2d.setStroke(new BasicStroke(DEFAULT_BORDER_WIDTH / (float)scale)); |
| | | g2d.setStroke(new BasicStroke(strokeWidth)); |
| | | } |
| | | g2d.draw(polygon); |
| | | |
| | |
| | | |
| | | /** |
| | | * ç»å¶éç¢ç©æ ç¾ |
| | | * æå大å°ä¸"缩æ¾"æåä¸è´ï¼11å·åä½ï¼ï¼ä¸ä¸éå°å¾ç¼©æ¾åå |
| | | */ |
| | | private static void drawObstacleLabel(Graphics2D g2d, Obstacledge.Obstacle obstacle, |
| | | double scale) { |
| | |
| | | return; |
| | | } |
| | | |
| | | // ä¿åå½å忢 |
| | | AffineTransform originalTransform = g2d.getTransform(); |
| | | |
| | | double centerX; |
| | | double centerY; |
| | | |
| | |
| | | centerY = centroid.y; |
| | | } |
| | | |
| | | // å°ä¸çåæ è½¬æ¢ä¸ºå±å¹åæ |
| | | Point2D.Double worldPoint = new Point2D.Double(centerX, centerY); |
| | | Point2D.Double screenPoint = new Point2D.Double(); |
| | | originalTransform.transform(worldPoint, screenPoint); |
| | | |
| | | // æ¢å¤åå§åæ¢ä»¥ä½¿ç¨å±å¹åæ ç»å¶ |
| | | g2d.setTransform(new AffineTransform()); |
| | | |
| | | // è·åéç¢ç©åç§° |
| | | String obstacleName = obstacle.getObstacleName(); |
| | | if (obstacleName == null || obstacleName.trim().isEmpty()) { |
| | |
| | | obstacleName = obstacleName.trim(); |
| | | } |
| | | |
| | | // 设置åä½åé¢è² |
| | | // 设置åä½åé¢è²ï¼ä¸"缩æ¾"æåä¸è´ï¼ |
| | | g2d.setColor(OBSTACLE_LABEL_COLOR); |
| | | |
| | | // æ ¹æ®ç¼©æ¾æ¯ä¾è°æ´åä½å¤§å° |
| | | int fontSize = (int)(10 / scale); |
| | | fontSize = Math.max(8, Math.min(fontSize, 14)); // éå¶åä½å¤§å°èå´ |
| | | |
| | | g2d.setFont(new Font("微软é
é»", Font.PLAIN, fontSize)); |
| | | g2d.setFont(new Font("微软é
é»", Font.PLAIN, 11)); // ä¸"缩æ¾"æå大å°ä¸è´ |
| | | |
| | | // ç»å¶æ ç¾ |
| | | String label = obstacleName; |
| | | String label = obstacleName; |
| | | FontMetrics metrics = g2d.getFontMetrics(); |
| | | int textWidth = metrics.stringWidth(label); |
| | | int textHeight = metrics.getHeight(); |
| | | |
| | | // å¨ä¸å¿ç¹ç»å¶æ ç¾ |
| | | int textX = (int)(centerX - textWidth / 2.0); |
| | | int textY = (int)(centerY + textHeight / 4.0); // ç¨å¾®åä¸åç§» |
| | | // å¨å±å¹åæ ä¸å¿ç¹ç»å¶æ ç¾ |
| | | int textX = (int)(screenPoint.x - textWidth / 2.0); |
| | | int textY = (int)(screenPoint.y + textHeight / 4.0); // ç¨å¾®åä¸åç§» |
| | | |
| | | g2d.drawString(label, textX, textY); |
| | | |
| | | // æ¢å¤åå§åæ¢ |
| | | g2d.setTransform(originalTransform); |
| | | } |
| | | |
| | | private static Point2D.Double computePolygonCentroid(List<Obstacledge.XYCoordinate> xyCoords) { |
| | |
| | | mainPanel.setBackground(Color.WHITE); |
| | | mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 10, 15)); |
| | | |
| | | // 计ç®å¾ä¾å
容颿¿ç宽度ï¼ç¨äºè®¾ç½®å¾æ å°ºå¯¸ï¼ |
| | | // å¾ä¾å¯¹è¯æ¡å®½åº¦ = DIALOG_WIDTH * 0.8 |
| | | // 䏻颿¿å·¦å³è¾¹æ¡å15åç´ ï¼å¾ä¾å
容颿¿å·¦å³å
è¾¹è·å10åç´ |
| | | int adjustedWidth = (int) Math.round(UIConfig.DIALOG_WIDTH * 0.8); |
| | | int iconSize = adjustedWidth - 30 - 20; // åå»ä¸»é¢æ¿å·¦å³è¾¹æ¡(15*2)åå¾ä¾å
容颿¿å·¦å³å
è¾¹è·(10*2) |
| | | |
| | | // å建å²èæºå¾æ 颿¿ |
| | | JPanel iconPanel = new JPanel(new BorderLayout()); |
| | | iconPanel.setBackground(Color.WHITE); |
| | | iconPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); // åºé¨é´è·10åç´ |
| | | |
| | | JLabel gecaojiLabel = new JLabel(); |
| | | gecaojiLabel.setHorizontalAlignment(SwingConstants.CENTER); |
| | | ImageIcon gecaojiIcon = loadIcon("image/gecaoji.png", iconSize, iconSize); |
| | | if (gecaojiIcon != null) { |
| | | gecaojiLabel.setIcon(gecaojiIcon); |
| | | } else { |
| | | // 妿徿 å è½½å¤±è´¥ï¼æ¾ç¤ºå ä½ææ¬ |
| | | gecaojiLabel.setText("å²èæºå¾æ "); |
| | | gecaojiLabel.setFont(new Font("微软é
é»", Font.PLAIN, 12)); |
| | | } |
| | | iconPanel.add(gecaojiLabel, BorderLayout.CENTER); |
| | | |
| | | // å¾ä¾å
容颿¿ - ç´æ¥æ·»å ï¼ä¸ä½¿ç¨æ»å¨æ¡ |
| | | JPanel contentPanel = new JPanel(); |
| | | contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); |
| | |
| | | contentPanel.remove(contentPanel.getComponentCount() - 1); |
| | | } |
| | | |
| | | // æ·»å 徿 颿¿åå¾ä¾å
容颿¿ |
| | | mainPanel.add(iconPanel, BorderLayout.NORTH); |
| | | // æ·»å å¾ä¾å
容颿¿ |
| | | mainPanel.add(contentPanel, BorderLayout.CENTER); |
| | | |
| | | getContentPane().add(mainPanel); |
| | |
| | | boolean hasPlannedPath = currentPlannedPath != null && currentPlannedPath.size() >= 2; |
| | | boolean hasObstacles = currentObstacles != null && !currentObstacles.isEmpty(); |
| | | |
| | | // ç»å¶å°åè¾¹çï¼åºå±ï¼ |
| | | if (hasBoundary) { |
| | | drawCurrentBoundary(g2d); |
| | | } |
| | | |
| | | if (hasObstacles) { |
| | | Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName); |
| | | } |
| | | |
| | | yulanzhangaiwu.renderPreview(g2d, scale); |
| | | |
| | | if (!circleSampleMarkers.isEmpty()) { |
| | |
| | | |
| | | adddikuaiyulan.drawPreview(g2d, handheldBoundaryPreview, scale, handheldBoundaryPreviewActive, boundaryPreviewMarkerScale); |
| | | |
| | | // ç»å¶å¯¼èªè·¯å¾ï¼ä¸å±ï¼ |
| | | if (hasPlannedPath) { |
| | | drawCurrentPlannedPath(g2d); |
| | | } |
| | | |
| | | // ç»å¶éç¢ç©ï¼é¡¶å±ï¼æ¾ç¤ºå¨å°åå导èªè·¯å¾ä¸æ¹ï¼ |
| | | if (hasObstacles) { |
| | | Obstacledraw.drawObstacles(g2d, currentObstacles, scale, selectedObstacleName); |
| | | } |
| | | |
| | | if (boundaryPointsVisible && hasBoundary) { |
| | | double markerScale = boundaryPointSizeScale * (previewSizingEnabled ? PREVIEW_BOUNDARY_MARKER_SCALE : 1.0d); |
| | | pointandnumber.drawBoundaryPoints( |
| | |
| | | import java.awt.event.*; |
| | | |
| | | import chuankou.dellmessage; |
| | | import chuankou.sendmessage; |
| | | import chuankou.SerialPortService; |
| | | import dikuai.Dikuai; |
| | | import dikuai.Dikuaiguanli; |
| | | import dikuai.addzhangaiwu; |
| | |
| | | // è¾¹çæ£æ¥å®æ¶å¨ï¼æ¯500msæ£æ¥ä¸æ¬¡å²èæºæ¯å¦å¨è¾¹çå
|
| | | boundaryWarningTimer = new Timer(500, e -> { |
| | | checkMowerBoundaryStatus(); |
| | | // åæ¶æ´æ°èç徿 ç¶æ |
| | | updateBluetoothButtonIcon(); |
| | | }); |
| | | boundaryWarningTimer.setInitialDelay(0); |
| | | boundaryWarningTimer.start(); |
| | |
| | | } |
| | | }); |
| | | ensureBluetoothIconsLoaded(); |
| | | bluetoothConnected = Bluelink.isConnected(); |
| | | ImageIcon initialIcon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | // æ ¹æ®ä¸²å£è¿æ¥ç¶ææ¾ç¤ºå¾æ |
| | | SerialPortService service = sendmessage.getActiveService(); |
| | | boolean serialConnected = (service != null && service.isOpen()); |
| | | ImageIcon initialIcon = serialConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | if (initialIcon != null) { |
| | | button.setIcon(initialIcon); |
| | | } else { |
| | | button.setText(bluetoothConnected ? "å·²è¿" : "èç"); |
| | | button.setText(serialConnected ? "å·²è¿" : "èç"); |
| | | } |
| | | return button; |
| | | } |
| | |
| | | return; |
| | | } |
| | | ensureBluetoothIconsLoaded(); |
| | | bluetoothConnected = Bluelink.isConnected(); |
| | | ImageIcon icon = bluetoothConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | // æ ¹æ®ä¸²å£è¿æ¥ç¶ææ¾ç¤ºå¾æ |
| | | SerialPortService service = sendmessage.getActiveService(); |
| | | boolean serialConnected = (service != null && service.isOpen()); |
| | | ImageIcon icon = serialConnected ? bluetoothLinkedIcon : bluetoothIcon; |
| | | if (icon != null) { |
| | | bluetoothBtn.setIcon(icon); |
| | | bluetoothBtn.setText(null); |
| | | } else { |
| | | bluetoothBtn.setText(bluetoothConnected ? "å·²è¿" : "èç"); |
| | | bluetoothBtn.setText(serialConnected ? "å·²è¿" : "èç"); |
| | | } |
| | | } |
| | | |