张世豪
4 天以前 32c98d4855b6178554c787103dc956d161e152b3
增加了异形分析逻辑
已添加5个文件
已修改8个文件
895 ■■■■■ 文件已修改
Obstacledge.properties 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
device.properties 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dikuai.properties 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
shoudongbianjie.properties 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/AoxinglujingHaveObstacel.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/AoxinglujingNoObstacle.java 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/Lunjingguihua.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/MowingPathGenerationPage.java 218 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/Qufenxingzhuang.java 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/YixinglujingHaveObstacel.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/YixinglujingNoObstacle.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhangaiwu/AddDikuai.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Obstacledge.properties
@@ -1,5 +1,5 @@
# å‰²è‰æœºåœ°å—障碍物配置文件
# ç”Ÿæˆæ—¶é—´ï¼š2025-12-18T19:44:44.126173900
# ç”Ÿæˆæ—¶é—´ï¼š2025-12-19T19:40:57.347052
# åæ ‡ç³»ï¼šWGS84(度分格式)
# ============ åœ°å—基准站配置 ============
device.properties
@@ -1,5 +1,5 @@
#Updated device properties
#Fri Dec 19 12:08:39 CST 2025
#Fri Dec 19 19:43:54 CST 2025
BupdateTime=-1
GupdateTime=-1
baseStationCardNumber=-1
@@ -11,15 +11,15 @@
differentialAge=-1
heading=-1
mowerBladeHeight=-1
mowerLength=3.00
mowerLength=0.40
mowerLightStatus=-1
mowerModel=-1
mowerName=-1
mowerNumber=-1
mowerStartStatus=-1
mowerWidth=2.00
mowerWidth=0.20
mowingHeight=-1
mowingSafetyDistance=1.86
mowingSafetyDistance=0.28
mowingWidth=-1
pitch=-1
positioningStatus=-1
dikuai.properties
@@ -1,21 +1,21 @@
#Dikuai Properties
#Thu Dec 18 19:44:44 CST 2025
#Fri Dec 19 19:44:17 CST 2025
LAND1.angleThreshold=-1
LAND1.baseStationCoordinates=3949.89151752,N,11616.79267501,E
LAND1.boundaryCoordinates=2.87,-0.19;6.73,-1.23;22.76,1.06;27.88,3.65;35.84,9.96;38.40,10.35;41.91,8.49;44.09,4.86;43.49,2.92;33.76,0.59;30.22,-1.26;30.09,-3.80;34.19,-28.28;32.68,-29.99;30.12,-30.19;28.89,-28.59;25.58,-6.77;24.09,-4.26;21.10,-3.94;-10.36,-8.92;-11.69,-8.57;-12.75,-4.68;-12.23,-3.28;-6.21,-2.34;0.00,0.00;1.44,-0.09;2.87,-0.19
LAND1.boundaryOriginalCoordinates=39.831524,116.279912,49.30;39.831523,116.279911,49.23;39.831521,116.279915,49.31;39.831517,116.279925,49.34;39.831514,116.279940,49.30;39.831514,116.279957,49.28;39.831516,116.279974,49.28;39.831518,116.279991,49.29;39.831521,116.280008,49.24;39.831524,116.280025,49.30;39.831526,116.280042,49.24;39.831529,116.280059,49.29;39.831529,116.280076,49.26;39.831530,116.280093,49.32;39.831531,116.280110,49.28;39.831533,116.280127,49.28;39.831535,116.280144,49.26;39.831539,116.280161,49.27;39.831544,116.280175,49.25;39.831551,116.280190,49.24;39.831558,116.280204,49.26;39.831566,116.280219,49.26;39.831574,116.280234,49.22;39.831583,116.280248,49.24;39.831591,116.280260,49.24;39.831600,116.280272,49.23;39.831608,116.280285,49.18;39.831615,116.280298,49.12;39.831618,116.280312,49.11;39.831618,116.280328,49.12;39.831615,116.280342,49.15;39.831610,116.280356,49.21;39.831602,116.280369,49.23;39.831592,116.280379,49.25;39.831581,116.280388,49.25;39.831569,116.280394,49.19;39.831559,116.280395,49.23;39.831552,116.280387,49.28;39.831547,116.280373,49.32;39.831544,116.280357,49.33;39.831541,116.280340,49.29;39.831539,116.280324,49.27;39.831536,116.280307,49.24;39.831534,116.280290,49.25;39.831531,116.280273,49.26;39.831527,116.280257,49.28;39.831522,116.280242,49.21;39.831514,116.280232,49.28;39.831504,116.280229,49.24;39.831491,116.280230,49.33;39.831478,116.280233,49.34;39.831466,116.280236,49.31;39.831454,116.280239,49.31;39.831441,116.280242,49.26;39.831429,116.280244,49.23;39.831416,116.280247,49.25;39.831402,116.280250,49.22;39.831389,116.280253,49.25;39.831376,116.280256,49.26;39.831364,116.280258,49.24;39.831351,116.280261,49.25;39.831338,116.280265,49.26;39.831324,116.280268,49.20;39.831311,116.280271,49.16;39.831298,116.280274,49.17;39.831285,116.280277,49.22;39.831271,116.280278,49.16;39.831261,116.280273,49.23;39.831256,116.280261,49.30;39.831253,116.280245,49.20;39.831254,116.280231,49.22;39.831258,116.280220,49.10;39.831268,116.280216,49.17;39.831281,116.280214,49.14;39.831295,116.280212,49.14;39.831308,116.280209,49.17;39.831321,116.280206,49.14;39.831334,116.280204,49.17;39.831348,116.280202,49.21;39.831362,116.280198,49.21;39.831375,116.280195,49.23;39.831390,116.280193,49.25;39.831405,116.280189,49.20;39.831420,116.280187,49.19;39.831436,116.280185,49.21;39.831450,116.280182,49.19;39.831464,116.280177,49.17;39.831478,116.280171,49.14;39.831487,116.280160,49.25;39.831491,116.280143,49.21;39.831490,116.280125,49.18;39.831487,116.280107,49.23;39.831485,116.280089,49.27;39.831483,116.280072,49.24;39.831482,116.280054,49.25;39.831480,116.280037,49.25;39.831477,116.280020,49.30;39.831475,116.280004,49.31;39.831473,116.279989,49.30;39.831472,116.279973,49.23;39.831470,116.279958,49.26;39.831468,116.279941,49.29;39.831465,116.279925,49.33;39.831463,116.279908,49.34;39.831462,116.279891,49.35;39.831461,116.279874,49.33;39.831458,116.279859,49.34;39.831456,116.279843,49.32;39.831454,116.279827,49.32;39.831452,116.279813,49.42;39.831450,116.279798,49.41;39.831448,116.279783,49.36;39.831447,116.279769,49.26;39.831445,116.279757,49.24;39.831445,116.279747,49.38;39.831448,116.279741,49.27;39.831455,116.279736,49.26;39.831463,116.279733,49.26;39.831473,116.279731,49.26;39.831483,116.279729,49.25;39.831491,116.279730,49.26;39.831496,116.279735,49.22;39.831497,116.279748,49.27;39.831498,116.279762,49.27;39.831500,116.279776,49.34;39.831502,116.279791,49.32;39.831504,116.279805,49.27;39.831507,116.279820,49.28;39.831510,116.279835,49.20;39.831513,116.279849,49.25;39.831517,116.279860,49.30;39.831520,116.279864,49.32;39.831520,116.279865,49.34;39.831522,116.279873,49.25;39.831524,116.279878,49.25;39.831525,116.279878,49.24
LAND1.boundaryCoordinates=-10.36,-8.92;-12.10,-7.80;-12.66,-3.77;-4.96,-1.98;-6.04,-3.37;-7.12,-4.76;-8.20,-6.14;-9.28,-7.53;-10.36,-8.92
LAND1.boundaryOriginalCoordinates=39.831445,116.279757,49.24;39.831445,116.279747,49.38;39.831448,116.279741,49.27;39.831455,116.279736,49.26;39.831463,116.279733,49.26;39.831473,116.279731,49.26;39.831483,116.279729,49.25;39.831491,116.279730,49.26;39.831496,116.279735,49.22;39.831497,116.279748,49.27;39.831498,116.279762,49.27;39.831500,116.279776,49.34;39.831502,116.279791,49.32;39.831504,116.279805,49.27;39.831507,116.279820,49.28
LAND1.boundaryPointInterval=-1
LAND1.createTime=2025-12-18 17\:12\:20
LAND1.createTime=2025-12-19 18\:31\:53
LAND1.intelligentSceneAnalysis=-1
LAND1.landArea=483.34
LAND1.landName=123
LAND1.landArea=26.75
LAND1.landName=21233
LAND1.landNumber=LAND1
LAND1.mowingBladeWidth=0.40
LAND1.mowingOverlapDistance=0.06
LAND1.mowingPattern=平行线
LAND1.mowingTrack=-1
LAND1.mowingWidth=34
LAND1.plannedPath=35.722,9.394;38.565,9.844;39.065,9.579;35.179,8.964;34.636,8.534;39.565,9.314;40.065,9.049;34.094,8.104;33.551,7.673;40.565,8.784;41.066,8.519;33.009,7.243;32.466,6.813;41.566,8.254;41.804,7.947;31.923,6.383;31.381,5.953;41.993,7.633;42.182,7.319;30.838,5.523;30.296,5.093;42.371,7.004;42.560,6.690;29.753,4.663;29.210,4.232;42.748,6.375;0.018,-0.389;0.094,-0.377;1.645,-0.475;-1.557,-0.982;42.937,6.061;28.668,3.802;28.125,3.372;43.126,5.747;-3.133,-1.576;2.993,-0.606;3.797,-0.823;-4.708,-2.169;43.315,5.432;27.172,2.877;26.181,2.376;43.503,5.118;-11.978,-3.664;4.602,-1.040;5.407,-1.257;-12.114,-4.030;43.685,4.803;25.191,1.875;24.200,1.374;43.573,4.441;-12.250,-4.396;6.212,-1.474;15.278,-0.383;-12.346,-4.755;43.461,4.079;23.210,0.873;-12.256,-5.085;43.349,3.717;43.237,3.355;-12.166,-5.415;-12.076,-5.745;40.411,2.563;36.170,1.548;-11.986,-6.075;-11.896,-6.405;33.250,0.741;32.306,0.247;-11.806,-6.735;-11.717,-7.065;31.361,-0.246;30.416,-0.740;-11.627,-7.395;-11.537,-7.725;29.861,-1.172;29.836,-1.520;-11.447,-8.055;-11.095,-8.344;29.818,-1.867;29.801,-2.215;21.190,-3.578;22.488,-3.716;29.783,-2.562;29.765,-2.909;23.785,-3.855;24.424,-4.098;29.747,-3.256;29.730,-3.603;24.611,-4.413;24.798,-4.728;29.739,-3.945;29.795,-4.281;24.985,-5.042;25.171,-5.357;29.852,-4.616;29.908,-4.951;25.358,-5.672;25.545,-5.986;29.964,-5.287;30.020,-5.622;25.732,-6.301;25.915,-6.616;30.076,-5.957;30.132,-6.293;25.982,-6.950;26.033,-7.286;30.189,-6.628;30.245,-6.964;26.084,-7.622;26.135,-7.958;30.301,-7.299;30.357,-7.634;26.185,-8.295;26.236,-8.631;30.413,-7.970;30.469,-8.305;26.287,-8.967;26.338,-9.303;30.526,-8.640;30.582,-8.976;26.389,-9.639;26.440,-9.975;30.638,-9.311;30.694,-9.646;26.491,-10.312;26.542,-10.648;30.750,-9.982;30.806,-10.317;26.593,-10.984;26.644,-11.320;30.862,-10.652;30.919,-10.988;26.695,-11.656;26.746,-11.992;30.975,-11.323;31.031,-11.658;26.797,-12.328;26.848,-12.665;31.087,-11.994;31.143,-12.329;26.899,-13.001;26.950,-13.337;31.199,-12.664;31.256,-13.000;27.001,-13.673;27.052,-14.009;31.312,-13.335;31.368,-13.670;27.103,-14.345;27.154,-14.682;31.424,-14.006;31.480,-14.341;27.205,-15.018;27.256,-15.354;31.536,-14.676;31.593,-15.012;27.307,-15.690;27.358,-16.026;31.649,-15.347;31.705,-15.682;27.409,-16.362;27.460,-16.699;31.761,-16.018;31.817,-16.353;27.511,-17.035;27.562,-17.371;31.873,-16.688;31.930,-17.024;27.613,-17.707;27.664,-18.043;31.986,-17.359;32.042,-17.694;27.715,-18.379;27.766,-18.716;32.098,-18.030;32.154,-18.365;27.817,-19.052;27.868,-19.388;32.210,-18.701;32.267,-19.036;27.919,-19.724;27.970,-20.060;32.323,-19.371;32.379,-19.707;28.021,-20.396;28.072,-20.733;32.435,-20.042;32.491,-20.377;28.123,-21.069;28.174,-21.405;32.547,-20.713;32.604,-21.048;28.225,-21.741;28.276,-22.077;32.660,-21.383;32.716,-21.719;28.327,-22.413;28.378,-22.749;32.772,-22.054;32.828,-22.389;28.429,-23.086;28.480,-23.422;32.884,-22.725;32.941,-23.060;28.531,-23.758;28.582,-24.094;32.997,-23.395;33.053,-23.731;28.633,-24.430;28.684,-24.766;33.109,-24.066;33.165,-24.401;28.735,-25.103;28.786,-25.439;33.221,-24.737;33.278,-25.072;28.837,-25.775;28.888,-26.111;33.334,-25.407;33.390,-25.743;28.939,-26.447;28.990,-26.783;33.446,-26.078;33.502,-26.413;29.041,-27.120;29.092,-27.456;33.558,-26.749;33.615,-27.084;29.143,-27.792;29.194,-28.128;33.671,-27.419;33.727,-27.755;29.258,-28.462;29.494,-28.769;33.783,-28.090;33.524,-28.475;29.730,-29.076;29.966,-29.383;33.170,-28.876;32.817,-29.276;30.202,-29.690
LAND1.plannedPath=-5.830841,-2.644443;-12.323966,-4.153883;-12.276978,-4.492026;-6.161845,-3.070456;-6.492848,-3.496470;-12.229991,-4.830169;-12.183003,-5.168312;-6.823852,-3.922484;-7.154855,-4.348497;-12.136016,-5.506455;-12.089028,-5.844598;-7.487144,-4.774810;-7.821079,-5.201505;-12.042040,-6.182741;-11.995053,-6.520885;-8.155014,-5.628200;-8.700347,-6.104039;-11.948065,-6.859028;-11.901077,-7.197171;-10.070186,-6.771548;-11.440025,-7.439057;-11.854090,-7.535314
LAND1.returnPointCoordinates=-1
LAND1.updateTime=2025-12-18 19\:44\:44
LAND1.updateTime=2025-12-19 19\:44\:17
LAND1.userId=-1
set.properties
@@ -1,5 +1,5 @@
#Mower Configuration Properties - Updated
#Fri Dec 19 17:40:36 CST 2025
#Fri Dec 19 19:44:56 CST 2025
appVersion=-1
boundaryLengthVisible=false
currentWorkLandNumber=LAND1
@@ -8,12 +8,12 @@
handheldMarkerId=1872
idleTrailDurationSeconds=60
manualBoundaryDrawingMode=false
mapScale=5.63
mapScale=41.56
measurementModeEnabled=false
mowerId=860
serialAutoConnect=true
serialBaudRate=115200
serialPortName=COM15
simCardNumber=-1
viewCenterX=-15.67
viewCenterY=9.92
viewCenterX=8.76
viewCenterY=4.44
shoudongbianjie.properties
@@ -1,11 +1,11 @@
#\u624B\u52A8\u7ED8\u5236\u8FB9\u754C\u5750\u6807 - \u683C\u5F0F: x1,y1;x2,y2;...;xn,yn (\u5355\u4F4D:\u7C73,\u7CBE\u786E\u5230\u5C0F\u6570\u70B9\u540E2\u4F4D)
#Fri Dec 19 16:55:22 CST 2025
boundaryCoordinates=2.64,9.22;1.40,10.29;9.39,20.06;19.16,22.54;19.69,12.60;13.48,8.34;6.20,6.74
#Fri Dec 19 19:18:05 CST 2025
boundaryCoordinates=-7.66,-10.73;-9.27,-9.14;-5.87,-8.78
email=789
language=zh
lastLoginTime=-1
password=123
pointCount=7
pointCount=3
registrationTime=-1
status=-1
userId=-1
src/lujing/AoxinglujingHaveObstacel.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package lujing;
import java.util.List;
/**
 * æœ‰éšœç¢ç‰©å‡¸å½¢åœ°å—路径规划类
 */
public class AoxinglujingHaveObstacel {
    /**
     * ç”Ÿæˆè·¯å¾„
     * @param boundaryCoordsStr åœ°å—边界坐标字符串 "x1,y1;x2,y2;..."
     * @param obstacleCoordsStr éšœç¢ç‰©åæ ‡å­—符串
     * @param mowingWidthStr å‰²è‰å®½åº¦å­—符串,如 "0.34"
     * @param safetyMarginStr å®‰å…¨è¾¹è·å­—符串,如 "0.2"
     * @return è·¯å¾„坐标字符串,格式 "x1,y1;x2,y2;..."
     */
    public static String planPath(String boundaryCoordsStr, String obstacleCoordsStr, String mowingWidthStr, String safetyMarginStr) {
        // TODO: å®žçŽ°å‡¸å½¢åœ°å—æœ‰éšœç¢ç‰©è·¯å¾„è§„åˆ’ç®—æ³•
        // ç›®å‰ä½¿ç”¨é»˜è®¤æ–¹æ³•作为临时实现
        throw new UnsupportedOperationException("AoxinglujingHaveObstacel.planPath å°šæœªå®žçް");
    }
}
src/lujing/AoxinglujingNoObstacle.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,331 @@
package lujing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
 * æ— éšœç¢ç‰©å‡¸å½¢è‰åœ°è·¯å¾„规划类 (优化版)
 */
public class AoxinglujingNoObstacle {
    // ä¼˜åŒ–:引入极小值用于浮点数比较,处理几何精度误差
    private static final double EPSILON = 1e-6;
    /**
     * è·¯å¾„段类
     */
    public static class PathSegment {
        public Point start;
        public Point end;
        public boolean isMowing; // true为割草工作段,false为过渡段
        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, isMowing=%b]", start, end, isMowing);
        }
    }
    /**
     * åæ ‡ç‚¹ç±»
     */
    public static class Point {
        double x, y;
        public Point(double x, double y) {
            this.x = x;
            this.y = y;
        }
        @Override
        public String toString() {
            return String.format("(%.4f, %.4f)", x, y);
        }
    }
    /**
     * å¯¹å¤–公开的静态调用方法 (保留字符串入参格式)
     *
     * @param boundaryCoordsStr åœ°å—边界坐标字符串 "x1,y1;x2,y2;..."
     * @param mowingWidthStr    å‰²è‰å®½åº¦å­—符串,如 "0.34"
     * @param safetyMarginStr   å®‰å…¨è¾¹è·å­—符串,如 "0.2"
     * @return è·¯å¾„段列表
     */
    public static List<PathSegment> planPath(String boundaryCoordsStr, String mowingWidthStr, String safetyMarginStr) {
        // 1. è§£æžå‚æ•° (优化:单独提取解析逻辑)
        List<Point> originalPolygon = parseCoords(boundaryCoordsStr);
        double mowingWidth;
        double safetyMargin;
        try {
            mowingWidth = Double.parseDouble(mowingWidthStr);
            safetyMargin = Double.parseDouble(safetyMarginStr);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("割草宽度或安全边距格式错误");
        }
        // 2. è°ƒç”¨æ ¸å¿ƒç®—法
        return planPathCore(originalPolygon, mowingWidth, safetyMargin);
    }
    /**
     * æ ¸å¿ƒç®—法逻辑 (强类型入参,便于测试和内部调用)
     */
    private static List<PathSegment> planPathCore(List<Point> originalPolygon, double mowingWidth, double safetyMargin) {
        // ä¼˜åŒ–:三角形也是合法的凸多边形,限制改为小于3
        if (originalPolygon == null || originalPolygon.size() < 3) {
            throw new IllegalArgumentException("多边形坐标点不足3个,无法构成有效区域");
        }
        // ç¡®ä¿å¤šè¾¹å½¢æ˜¯é€†æ—¶é’ˆæ–¹å‘
        ensureCCW(originalPolygon);
        // 1. æ ¹æ®å®‰å…¨è¾¹è·å†…缩
        List<Point> shrunkPolygon = shrinkPolygon(originalPolygon, safetyMargin);
        // ä¼˜åŒ–:内缩后如果多边形失效(例如地块太窄,内缩后消失),直接返回空路径,避免后续报错
        if (shrunkPolygon.size() < 3) {
            // è¿™é‡Œå¯ä»¥è®°å½•日志:地块过小,无法满足安全距离作业
            return new ArrayList<>();
        }
        // 2. è®¡ç®—最长边角度 (作为扫描方向)
        double angle = calculateLongestEdgeAngle(originalPolygon);
        // 3. & 4. æ—‹è½¬æ‰«æå¹¶å‰ªè£
        List<PathSegment> mowingLines = generateClippedMowingLines(shrunkPolygon, angle, mowingWidth);
        // 5. å¼“字形连接
        return connectPathSegments(mowingLines);
    }
    // ================= æ ¸å¿ƒç®—法辅助方法 =================
    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 double calculateLongestEdgeAngle(List<Point> polygon) {
        double maxDistSq = -1;
        double angle = 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 dx = p2.x - p1.x;
            double dy = p2.y - p1.y;
            double distSq = dx * dx + dy * dy;
            if (distSq > maxDistSq) {
                maxDistSq = distSq;
                angle = Math.atan2(dy, dx);
            }
        }
        return angle;
    }
    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;
        double maxY = -Double.MAX_VALUE;
        for (Point p : rotatedPoly) {
            if (p.y < minY) minY = p.y;
            if (p.y > maxY) maxY = p.y;
        }
        // ä¼˜åŒ–:起始扫描线增加一个微小的偏移量 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 rEnd = rotatePoint(new Point(xEnd, currentY), angle);
                    segments.add(new PathSegment(rStart, rEnd, true));
                }
            }
            currentY += width;
        }
        return segments;
    }
    private static List<PathSegment> connectPathSegments(List<PathSegment> lines) {
        List<PathSegment> result = new ArrayList<>();
        if (lines.isEmpty()) return result;
        for (int i = 0; i < lines.size(); i++) {
            PathSegment currentLine = lines.get(i);
            Point actualStart, actualEnd;
            // å¼“字形规划:偶数行正向,奇数行反向
            if (i % 2 == 0) {
                actualStart = currentLine.start;
                actualEnd = currentLine.end;
            } else {
                actualStart = currentLine.end;
                actualEnd = currentLine.start;
            }
            // æ·»åŠ è¿‡æ¸¡æ®µ
            if (i > 0) {
                Point prevEnd = result.get(result.size() - 1).end;
                // åªæœ‰å½“距离确实存在时才添加过渡段(避免重合点)
                if (distanceSq(prevEnd, actualStart) > EPSILON) {
                    result.add(new PathSegment(prevEnd, actualStart, false));
                }
            }
            result.add(new PathSegment(actualStart, actualEnd, true));
        }
        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(",");
            if (xy.length >= 2) {
                try {
                    double x = Double.parseDouble(xy[0].trim());
                    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 p1 = polygon.get(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 class Line {
        double a, b, c;
        public Line(double a, double b, double c) { this.a = a; this.b = b; this.c = c; }
    }
    private static Line offsetLine(Point p1, Point p2, double dist) {
        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;
        double a = -dy;
        double b = dx;
        double c = -a * newX - b * newY;
        return new Line(a, b, c);
    }
    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; // å¹³è¡Œ
        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);
    }
    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 List<Point> rotatePolygon(List<Point> poly, double angle) {
        List<Point> res = new ArrayList<>();
        for (Point p : poly) {
            res.add(rotatePoint(p, angle));
        }
        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);
    }
}
src/lujing/Lunjingguihua.java
@@ -22,7 +22,22 @@
        throw new IllegalStateException("Utility class");
    }
    // 5参数核心生成方法
    /**
     * ç”Ÿæˆå‰²è‰è·¯å¾„段列表(5参数版本)
     * æ ¹æ®ç»™å®šçš„多边形边界、障碍物、割草宽度、安全距离和模式,生成完整的割草路径规划
     *
     * @param polygonCoords å¤šè¾¹å½¢è¾¹ç•Œåæ ‡å­—符串,格式:"x1,y1;x2,y2;x3,y3;..." æˆ– "(x1,y1);(x2,y2);..."
     *                      æ”¯æŒåˆ†å·ã€ç©ºæ ¼ã€é€—号等多种分隔符,会自动闭合多边形
     * @param obstaclesCoords éšœç¢ç‰©åæ ‡å­—符串,格式:"(x1,y1;x2,y2;...)" å¤šä¸ªéšœç¢ç‰©ç”¨æ‹¬å·åˆ†éš”
     *                        å¦‚果为空或null,则视为无障碍物
     * @param mowingWidth å‰²è‰å®½åº¦ï¼ˆç±³ï¼‰ï¼Œå­—符串格式,如 "0.34" è¡¨ç¤º34厘米
     *                    å¦‚果为空或无法解析,默认使用0.34ç±³
     * @param safetyDistStr å®‰å…¨è¾¹è·ï¼ˆç±³ï¼‰ï¼Œå­—符串格式,用于内缩边界和障碍物外扩
     *                      å¦‚果为空或null,将自动计算为 width/2 + 0.2米,确保割草机实体完全在界内
     * @param modeStr è·¯å¾„模式字符串,"1" æˆ– "spiral" è¡¨ç¤ºèžºæ—‹æ¨¡å¼ï¼Œå…¶ä»–值表示平行模式(默认)
     * @return è·¯å¾„段列表,每个PathSegment包含起点、终点和是否为割草段的标志
     *         å¦‚果多边形坐标不足4个点,将抛出IllegalArgumentException异常
     */
    public static List<PathSegment> generatePathSegments(String polygonCoords,
                                                         String obstaclesCoords,
                                                         String mowingWidth,
@@ -44,7 +59,18 @@
        return planner.generate();
    }
    // 4参数重载,适配 AddDikuai.java
    /**
     * ç”Ÿæˆå‰²è‰è·¯å¾„段列表(4参数版本)
     * è¿™æ˜¯5参数版本的重载方法,安全距离参数自动设为null,系统将使用默认计算值
     * ä¸»è¦ç”¨äºŽé€‚配 AddDikuai.java ç­‰ä¸éœ€è¦æŒ‡å®šå®‰å…¨è·ç¦»çš„场景
     *
     * @param polygonCoords å¤šè¾¹å½¢è¾¹ç•Œåæ ‡å­—符串,格式:"x1,y1;x2,y2;x3,y3;..."
     * @param obstaclesCoords éšœç¢ç‰©åæ ‡å­—符串,格式:"(x1,y1;x2,y2;...)",可为空
     * @param mowingWidth å‰²è‰å®½åº¦ï¼ˆç±³ï¼‰ï¼Œå­—符串格式,如 "0.34"
     * @param modeStr è·¯å¾„模式字符串,"1" æˆ– "spiral" è¡¨ç¤ºèžºæ—‹æ¨¡å¼ï¼Œå…¶ä»–值表示平行模式
     * @return è·¯å¾„段列表,每个PathSegment包含起点、终点和是否为割草段的标志
     *         å®‰å…¨è·ç¦»å°†è‡ªåŠ¨è®¡ç®—ä¸º width/2 + 0.2ç±³
     */
    public static List<PathSegment> generatePathSegments(String polygonCoords,
                                                         String obstaclesCoords,
                                                         String mowingWidth,
src/lujing/MowingPathGenerationPage.java
@@ -15,8 +15,14 @@
import dikuai.Dikuai;
import lujing.Lunjingguihua;
import lujing.ObstaclePathPlanner;
import lujing.Qufenxingzhuang;
import lujing.AoxinglujingNoObstacle;
import lujing.YixinglujingNoObstacle;
import lujing.AoxinglujingHaveObstacel;
import lujing.YixinglujingHaveObstacel;
import org.locationtech.jts.geom.Coordinate;
import gecaoji.Device;
import java.util.Locale;
/**
 * ç”Ÿæˆå‰²è‰è·¯å¾„页面
@@ -494,8 +500,24 @@
            obstacles = obstacles.replace("\r\n", " ").replace('\r', ' ').replace('\n', ' ');
        }
        // èŽ·å–å®‰å…¨è·ç¦»
        String safetyMarginStr = getSafetyDistanceString();
        if (safetyMarginStr == null) {
            // å¦‚果没有设置安全距离,使用默认值:割草宽度的一半 + 0.2ç±³
            double defaultSafetyDistance = widthMeters / 2.0 + 0.2;
            safetyMarginStr = BigDecimal.valueOf(defaultSafetyDistance)
                .setScale(3, RoundingMode.HALF_UP)
                .stripTrailingZeros()
                .toPlainString();
        }
        String mode = normalizeExistingMowingPattern(modeInput);
        try {
            // 1. é¦–先判断地块类型(凸形还是异形)
            Qufenxingzhuang shapeJudger = new Qufenxingzhuang();
            int grassType = shapeJudger.judgeGrassType(boundary);
            // grassType: 0=无法判断, 1=凸形, 2=异形
            // è§£æžéšœç¢ç‰©åˆ—表
            List<List<Coordinate>> obstacleList = Lunjingguihua.parseObstacles(obstacles);
            if (obstacleList == null) {
@@ -505,35 +527,110 @@
            // åˆ¤æ–­æ˜¯å¦æœ‰æœ‰æ•ˆçš„障碍物:只有当解析成功且列表不为空时,才认为有障碍物
            boolean hasValidObstacles = !obstacleList.isEmpty();
            
            String generated;
            String generated = null;
            
            // 2. æ ¹æ®åœ°å—类型和是否有障碍物,调用不同的路径生成类
            if (!hasValidObstacles) {
                // éšœç¢ç‰©åæ ‡ä¸å­˜åœ¨æˆ–为空时,使用Lunjingguihua类的方法生成路径
                generated = Lunjingguihua.generatePathFromStrings(
                    boundary,
                    obstacles != null ? obstacles : "",
                    plannerWidth,
                    null,  // safetyDistStr,使用默认值
                    mode
                );
            } else {
                // æœ‰æœ‰æ•ˆéšœç¢ç‰©æ—¶ï¼Œä½¿ç”¨ObstaclePathPlanner处理路径生成
                List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary);
                if (polygon.size() < 4) {
                    if (showMessages) {
                        JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点",
                            "错误", JOptionPane.ERROR_MESSAGE);
                // æ— éšœç¢ç‰©çš„æƒ…况
                if (grassType == 1) {
                    // å‡¸å½¢åœ°å—,无障碍物 -> è°ƒç”¨ AoxinglujingNoObstacle
                    List<AoxinglujingNoObstacle.PathSegment> segments =
                        AoxinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr);
                    generated = formatAoxingPathSegments(segments);
                } else if (grassType == 2) {
                    // å¼‚形地块,无障碍物 -> è°ƒç”¨ YixinglujingNoObstacle
                    // æ³¨æ„ï¼šå¦‚果该类还没有实现,这里会抛出异常或返回null
                    try {
                        // å‡è®¾ YixinglujingNoObstacle æœ‰ç±»ä¼¼çš„æ–¹æ³•签名
                        // å¦‚果类还没有实现,可能需要使用原来的方法作为后备
                        generated = YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr);
                    } catch (Exception e) {
                        // å¦‚果类还没有实现,使用原来的方法作为后备
                        if (showMessages) {
                            System.err.println("YixinglujingNoObstacle å°šæœªå®žçŽ°ï¼Œä½¿ç”¨é»˜è®¤æ–¹æ³•: " + e.getMessage());
                        }
                        generated = Lunjingguihua.generatePathFromStrings(
                            boundary, obstacles != null ? obstacles : "", plannerWidth, safetyMarginStr, mode);
                    }
                    return null;
                } else {
                    // æ— æ³•判断地块类型,使用原来的方法作为后备
                    if (showMessages) {
                        JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,使用默认路径生成方法",
                            "提示", JOptionPane.WARNING_MESSAGE);
                    }
                    generated = Lunjingguihua.generatePathFromStrings(
                        boundary, obstacles != null ? obstacles : "", plannerWidth, safetyMarginStr, mode);
                }
                // æœ‰éšœç¢ç‰©æ—¶ä½¿ç”¨å‰²è‰å®½åº¦çš„一半 + 0.05米额外安全距离
                double safetyDistance = widthMeters / 2.0 + 0.05;
                ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner(
                    polygon, widthMeters, mode, obstacleList, safetyDistance);
                List<Lunjingguihua.PathSegment> segments = pathPlanner.generate();
                generated = Lunjingguihua.formatPathSegments(segments);
            } else {
                // æœ‰éšœç¢ç‰©çš„æƒ…况
                if (grassType == 1) {
                    // å‡¸å½¢åœ°å—,有障碍物 -> è°ƒç”¨ AoxinglujingHaveObstacel
                    try {
                        // å‡è®¾ AoxinglujingHaveObstacel æœ‰ç±»ä¼¼çš„æ–¹æ³•签名
                        generated = AoxinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr);
                    } catch (Exception e) {
                        // å¦‚果类还没有实现,使用原来的方法作为后备
                        if (showMessages) {
                            System.err.println("AoxinglujingHaveObstacel å°šæœªå®žçŽ°ï¼Œä½¿ç”¨é»˜è®¤æ–¹æ³•: " + e.getMessage());
                        }
                        List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary);
                        if (polygon.size() < 4) {
                            if (showMessages) {
                                JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点",
                                    "错误", JOptionPane.ERROR_MESSAGE);
                            }
                            return null;
                        }
                        double safetyDistance = Double.parseDouble(safetyMarginStr);
                        ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner(
                            polygon, widthMeters, mode, obstacleList, safetyDistance);
                        List<Lunjingguihua.PathSegment> segments = pathPlanner.generate();
                        generated = Lunjingguihua.formatPathSegments(segments);
                    }
                } else if (grassType == 2) {
                    // å¼‚形地块,有障碍物 -> è°ƒç”¨ YixinglujingHaveObstacel
                    try {
                        // å‡è®¾ YixinglujingHaveObstacel æœ‰ç±»ä¼¼çš„æ–¹æ³•签名
                        generated = YixinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr);
                    } catch (Exception e) {
                        // å¦‚果类还没有实现,使用原来的方法作为后备
                        if (showMessages) {
                            System.err.println("YixinglujingHaveObstacel å°šæœªå®žçŽ°ï¼Œä½¿ç”¨é»˜è®¤æ–¹æ³•: " + e.getMessage());
                        }
                        List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary);
                        if (polygon.size() < 4) {
                            if (showMessages) {
                                JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点",
                                    "错误", JOptionPane.ERROR_MESSAGE);
                            }
                            return null;
                        }
                        double safetyDistance = Double.parseDouble(safetyMarginStr);
                        ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner(
                            polygon, widthMeters, mode, obstacleList, safetyDistance);
                        List<Lunjingguihua.PathSegment> segments = pathPlanner.generate();
                        generated = Lunjingguihua.formatPathSegments(segments);
                    }
                } else {
                    // æ— æ³•判断地块类型,使用原来的方法作为后备
                    if (showMessages) {
                        JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,使用默认路径生成方法",
                            "提示", JOptionPane.WARNING_MESSAGE);
                    }
                    List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary);
                    if (polygon.size() < 4) {
                        if (showMessages) {
                            JOptionPane.showMessageDialog(parentComponent, "多边形坐标数量不足,至少需要三个点",
                                "错误", JOptionPane.ERROR_MESSAGE);
                        }
                        return null;
                    }
                    double safetyDistance = Double.parseDouble(safetyMarginStr);
                    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() : "";
@@ -564,6 +661,77 @@
        return null;
    }
    
    /**
     * èŽ·å–å®‰å…¨è·ç¦»å­—ç¬¦ä¸²ï¼ˆç±³ï¼‰
     */
    private String getSafetyDistanceString() {
        Device device = Device.getActiveDevice();
        if (device != null) {
            String safetyDistanceValue = device.getMowingSafetyDistance();
            if (safetyDistanceValue != null && !"-1".equals(safetyDistanceValue) && !safetyDistanceValue.trim().isEmpty()) {
                try {
                    double distanceMeters = Double.parseDouble(safetyDistanceValue.trim());
                    // å¦‚果值大于100,认为是厘米,需要转换为米
                    if (distanceMeters > 100) {
                        distanceMeters = distanceMeters / 100.0;
                    }
                    return BigDecimal.valueOf(distanceMeters)
                        .setScale(3, RoundingMode.HALF_UP)
                        .stripTrailingZeros()
                        .toPlainString();
                } catch (NumberFormatException e) {
                    // è§£æžå¤±è´¥ï¼Œè¿”回null,使用默认值
                }
            }
        }
        return null;
    }
    /**
     * æ ¼å¼åŒ– AoxinglujingNoObstacle.PathSegment åˆ—表为坐标字符串
     */
    private String formatAoxingPathSegments(List<AoxinglujingNoObstacle.PathSegment> segments) {
        if (segments == null || segments.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        AoxinglujingNoObstacle.Point last = null;
        for (AoxinglujingNoObstacle.PathSegment segment : segments) {
            // åªæ·»åŠ å‰²è‰å·¥ä½œæ®µï¼Œè·³è¿‡è¿‡æ¸¡æ®µ
            if (segment.isMowing) {
                // å¦‚果起点与上一个终点不同,添加起点
                if (last == null || !equals2D(last, segment.start)) {
                    appendPoint(sb, segment.start);
                }
                // æ·»åŠ ç»ˆç‚¹
                appendPoint(sb, segment.end);
                last = segment.end;
            }
        }
        return sb.toString();
    }
    /**
     * æ¯”较两个点是否相同(使用小的容差)
     */
    private boolean equals2D(AoxinglujingNoObstacle.Point p1, AoxinglujingNoObstacle.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;
    }
    /**
     * æ·»åŠ ç‚¹åˆ°å­—ç¬¦ä¸²æž„å»ºå™¨
     */
    private void appendPoint(StringBuilder sb, AoxinglujingNoObstacle.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) {
src/lujing/Qufenxingzhuang.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,146 @@
package lujing;
import java.util.ArrayList;
import java.util.List;
public class Qufenxingzhuang {
    // å®¹å·®é˜ˆå€¼ï¼šsin(角度)。
    // 0.05 å¤§çº¦å¯¹åº” 2.8度。意味着小于3度的微小凹陷或凸起会被忽略,视为直线。
    // å¦‚果您觉得判断依然太严,可以将此值调大到 0.1 (约5.7度)。
    private static final double ANGLE_TOLERANCE = 0.1;
    // åˆ¤æ–­è‰åœ°ç±»åž‹çš„主方法
    public int judgeGrassType(String coordinates) {
        try {
            // 1. è§£æžåæ ‡å­—符串
            List<Point> points = parseCoordinates(coordinates);
            // 2. æ•°æ®é¢„处理:删除最后一个闭合点
            // åŽŸå› ï¼šè¾“å…¥æ•°æ®é€šå¸¸é¦–å°¾ç›¸åŒï¼Œåˆ é™¤æœ€åŽä¸€ä¸ªç‚¹é¿å…é‡å¤è®¡ç®—
            if (!points.isEmpty()) {
                points.remove(points.size() - 1);
            }
            // 3. æ£€æŸ¥ç‚¹æ•°ï¼ˆç§»é™¤åŽè‡³å°‘需要3个点)
            if (points.size() < 3) {
                return 0; // æ— æ³•构成多边形
            }
            // 4. åˆ¤æ–­å½¢çж
            if (isConvexPolygon(points)) {
                return 1; // å‡¸å½¢è‰åœ°
            } else {
                return 2; // å¼‚形草地(明显的凹多边形)
            }
        } catch (Exception e) {
            // è§£æžå¤±è´¥æˆ–计算异常
            return 0;
        }
    }
    // è§£æžåæ ‡å­—符串
    private List<Point> parseCoordinates(String coordinates) {
        List<Point> points = new ArrayList<>();
        String cleanStr = coordinates.replaceAll("[()\\[\\]{}]", "").trim();
        String[] pointStrings = cleanStr.split(";");
        for (String pointStr : pointStrings) {
            pointStr = pointStr.trim();
            if (pointStr.isEmpty()) continue;
            String[] xy = pointStr.split(",");
            if (xy.length != 2) throw new IllegalArgumentException("格式错误");
            points.add(new Point(Double.parseDouble(xy[0].trim()), Double.parseDouble(xy[1].trim())));
        }
        return points;
    }
    // ã€æ ¸å¿ƒä¿®æ”¹ã€‘判断多边形是否为凸多边形(带容差)
    private boolean isConvexPolygon(List<Point> points) {
        // å…ˆæ¸…理极度接近的点,避免除以零错误
        List<Point> cleanedPoints = cleanDuplicatePoints(points);
        int n = cleanedPoints.size();
        if (n < 3) return false;
        boolean hasPositive = false;
        boolean hasNegative = false;
        for (int i = 0; i < n; i++) {
            Point p0 = cleanedPoints.get(i);
            Point p1 = cleanedPoints.get((i + 1) % n);
            Point p2 = cleanedPoints.get((i + 2) % n);
            // è®¡ç®—向量 p0->p1 å’Œ p1->p2
            double dx1 = p1.x - p0.x;
            double dy1 = p1.y - p0.y;
            double dx2 = p2.x - p1.x;
            double dy2 = p2.y - p1.y;
            // è®¡ç®—叉积
            double cross = dx1 * dy2 - dx2 * dy1;
            // è®¡ç®—边长
            double len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
            double len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
            // é˜²æ­¢æžçŸ­è¾¹å¯¼è‡´çš„计算错误
            if (len1 < 1e-6 || len2 < 1e-6) continue;
            // ã€å…³é”®ä¿®æ”¹ã€‘计算归一化的叉积值 (相当于 sinθ)
            // è¿™ä»£è¡¨äº†å¼¯æ›²çš„程度,排除了边长对叉积数值大小的干扰
            double sinTheta = cross / (len1 * len2);
            // åº”用容差逻辑:
            // åªæœ‰å½“弯曲程度超过阈值时,才记录方向
            if (sinTheta > ANGLE_TOLERANCE) {
                hasPositive = true; // æ˜Žæ˜¾çš„左转(凸)
            } else if (sinTheta < -ANGLE_TOLERANCE) {
                hasNegative = true; // æ˜Žæ˜¾çš„右转(凹)
            }
            // å¦‚æžœ sinTheta åœ¨ -0.05 åˆ° 0.05 ä¹‹é—´ï¼Œè®¤ä¸ºæ˜¯ç›´çº¿æŠ–动,忽略不计
            // å¦‚果既有明显的向左转,又有明显的向右转,那就是异形(凹多边形)
            if (hasPositive && hasNegative) {
                return false;
            }
        }
        // å¦‚果没有产生冲突的转向,或者是单纯的直线(所有点共线),视为凸形
        return true;
    }
    // è¾…助:仅去除重复点,不再过度清理共线点(因为主逻辑已经能处理共线抖动)
    private List<Point> cleanDuplicatePoints(List<Point> points) {
        List<Point> cleaned = new ArrayList<>();
        double tolerance = 1e-6;
        for (Point p : points) {
            if (!cleaned.isEmpty()) {
                Point last = cleaned.get(cleaned.size() - 1);
                if (Math.abs(p.x - last.x) < tolerance && Math.abs(p.y - last.y) < tolerance) {
                    continue; // è·³è¿‡é‡å¤ç‚¹
                }
            }
            cleaned.add(p);
        }
        // æ£€æŸ¥é¦–尾是否因为循环导致重复(虽然主方法删除了最后一个点,为了健壮性再查一次)
        if (cleaned.size() > 1) {
            Point first = cleaned.get(0);
            Point last = cleaned.get(cleaned.size() - 1);
            if (Math.abs(first.x - last.x) < tolerance && Math.abs(first.y - last.y) < tolerance) {
                cleaned.remove(cleaned.size() - 1);
            }
        }
        return cleaned;
    }
    // ç‚¹ç±»
    private static class Point {
        double x, y;
        Point(double x, double y) { this.x = x; this.y = y; }
    }
}
src/lujing/YixinglujingHaveObstacel.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package lujing;
import java.util.List;
/**
 * æœ‰éšœç¢ç‰©å¼‚形地块路径规划类
 */
public class YixinglujingHaveObstacel {
    /**
     * ç”Ÿæˆè·¯å¾„
     * @param boundaryCoordsStr åœ°å—边界坐标字符串 "x1,y1;x2,y2;..."
     * @param obstacleCoordsStr éšœç¢ç‰©åæ ‡å­—符串
     * @param mowingWidthStr å‰²è‰å®½åº¦å­—符串,如 "0.34"
     * @param safetyMarginStr å®‰å…¨è¾¹è·å­—符串,如 "0.2"
     * @return è·¯å¾„坐标字符串,格式 "x1,y1;x2,y2;..."
     */
    public static String planPath(String boundaryCoordsStr, String obstacleCoordsStr, String mowingWidthStr, String safetyMarginStr) {
        // TODO: å®žçŽ°å¼‚å½¢åœ°å—æœ‰éšœç¢ç‰©è·¯å¾„è§„åˆ’ç®—æ³•
        // ç›®å‰ä½¿ç”¨é»˜è®¤æ–¹æ³•作为临时实现
        throw new UnsupportedOperationException("YixinglujingHaveObstacel.planPath å°šæœªå®žçް");
    }
}
src/lujing/YixinglujingNoObstacle.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package lujing;
import java.util.List;
import java.util.ArrayList;
/**
 * æ— éšœç¢ç‰©å¼‚形地块路径规划类
 */
public class YixinglujingNoObstacle {
    /**
     * ç”Ÿæˆè·¯å¾„
     * @param boundaryCoordsStr åœ°å—边界坐标字符串 "x1,y1;x2,y2;..."
     * @param mowingWidthStr å‰²è‰å®½åº¦å­—符串,如 "0.34"
     * @param safetyMarginStr å®‰å…¨è¾¹è·å­—符串,如 "0.2"
     * @return è·¯å¾„坐标字符串,格式 "x1,y1;x2,y2;..."
     */
    public static String planPath(String boundaryCoordsStr, String mowingWidthStr, String safetyMarginStr) {
        // TODO: å®žçŽ°å¼‚å½¢åœ°å—æ— éšœç¢ç‰©è·¯å¾„è§„åˆ’ç®—æ³•
        // ç›®å‰ä½¿ç”¨é»˜è®¤æ–¹æ³•作为临时实现
        throw new UnsupportedOperationException("YixinglujingNoObstacle.planPath å°šæœªå®žçް");
    }
}
src/zhangaiwu/AddDikuai.java
@@ -33,6 +33,7 @@
import zhuye.Shouye;
import zhuye.Coordinate;
import zhuye.buttonset;
import gecaoji.Device;
/**
 * æ–°å¢žåœ°å—对话框 - å¤šæ­¥éª¤è¡¨å•设计
@@ -67,6 +68,7 @@
    private JComboBox<String> mowingPatternCombo;
    private JTextField mowingWidthField; // å‰²è‰æœºå‰²åˆ€å®½åº¦
    private JTextField overlapDistanceField; // ç›¸é‚»è¡Œé‡å è·ç¦»
    private JTextField mowingSafetyDistanceField; // å‰²è‰å®‰å…¨è·ç¦»
    private JLabel calculatedMowingWidthLabel; // è®¡ç®—后的割草宽度显示
    private JPanel previewPanel;
    private Map<String, JPanel> drawingOptionPanels = new HashMap<>();
@@ -1044,6 +1046,65 @@
        
        calculatedWidthPanel.add(calculatedWidthDisplayPanel);
        settingsPanel.add(calculatedWidthPanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 10)));
        // å‰²è‰å®‰å…¨è·ç¦»
        JPanel safetyDistancePanel = createFormGroupWithoutHint("割草安全距离");
        JPanel safetyDistanceInputPanel = new JPanel(new BorderLayout());
        safetyDistanceInputPanel.setBackground(WHITE);
        safetyDistanceInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48));
        safetyDistanceInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        // èŽ·å–é»˜è®¤å€¼
        String defaultSafetyDistance = "0.5"; // é»˜è®¤å€¼
        try {
            Device device = Device.getGecaoji();
            if (device != null) {
                String safetyDistance = device.getMowingSafetyDistance();
                if (safetyDistance != null && !safetyDistance.trim().isEmpty() && !"-1".equals(safetyDistance.trim())) {
                    defaultSafetyDistance = safetyDistance.trim();
                }
            }
        } catch (Exception e) {
            // å¦‚果获取失败,使用默认值
        }
        mowingSafetyDistanceField = new JTextField(defaultSafetyDistance);
        mowingSafetyDistanceField.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(BORDER_COLOR, 2),
            BorderFactory.createEmptyBorder(10, 12, 10, 12)
        ));
        // æ·»åŠ æ–‡æœ¬æ¡†ç„¦ç‚¹æ•ˆæžœ
        mowingSafetyDistanceField.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(PRIMARY_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
            @Override
            public void focusLost(FocusEvent e) {
                mowingSafetyDistanceField.setBorder(BorderFactory.createCompoundBorder(
                    BorderFactory.createLineBorder(BORDER_COLOR, 2),
                    BorderFactory.createEmptyBorder(10, 12, 10, 12)
                ));
            }
        });
        JLabel safetyDistanceUnitLabel = new JLabel("ç±³");
        safetyDistanceUnitLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        safetyDistanceUnitLabel.setForeground(LIGHT_TEXT);
        safetyDistanceUnitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0));
        safetyDistanceInputPanel.add(mowingSafetyDistanceField, BorderLayout.CENTER);
        safetyDistanceInputPanel.add(safetyDistanceUnitLabel, BorderLayout.EAST);
        safetyDistancePanel.add(safetyDistanceInputPanel);
        settingsPanel.add(safetyDistancePanel);
        settingsPanel.add(Box.createRigidArea(new Dimension(0, 25)));
        
        stepPanel.add(settingsPanel);