| | |
| | | <attribute name="module" value="true"/> |
| | | </attributes> |
| | | </classpathentry> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/jackson-annotations-2.15.2.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/jackson-core-2.15.2.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/jackson-databind-2.15.2.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/jSerialComm-2.10.4.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/jts-core-1.19.0.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/lombok-1.18.36.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/MQTT-1.0-SNAPSHOT.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/org.eclipse.paho.client.mqttv3-1.2.2.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/slf4j-api-1.7.30.jar"/> |
| | | <classpathentry kind="lib" path="E:/Users/hxzk/eclipse-workspace/GeCaoAPP/lib/slf4j-simple-1.7.30.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/jackson-annotations-2.15.2.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/jackson-core-2.15.2.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/jackson-databind-2.15.2.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/jSerialComm-2.10.4.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/jts-core-1.19.0.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/lombok-1.18.36.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/MQTT-1.0-SNAPSHOT.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/org.eclipse.paho.client.mqttv3-1.2.2.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/slf4j-api-1.7.30.jar"/> |
| | | <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/GeCaoAPP/lib/slf4j-simple-1.7.30.jar"/> |
| | | <classpathentry kind="output" path="bin"/> |
| | | </classpath> |
| | |
| | | # 割草机地块障碍物配置文件 |
| | | # 生成时间:2025-12-26T19:43:09.374455600 |
| | | # 生成时间:2025-12-27T13:39:05.917584700 |
| | | # 坐标系:WGS84(度分格式) |
| | | |
| | | # ============ 地块基准站配置 ============ |
| | |
| | | |
| | | # --- 地块LAND1的障碍物 --- |
| | | plot.LAND1.obstacle.障碍物1.shape=1 |
| | | plot.LAND1.obstacle.障碍物1.originalCoords=0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E |
| | | plot.LAND1.obstacle.障碍物1.xyCoords=25.17,9.94;17.74,67.19;113.88,72.00;120.00,19.12;74.99,-4.48 |
| | | plot.LAND1.obstacle.障碍物1.originalCoords=0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E;0.000000,N;0.000000,E |
| | | plot.LAND1.obstacle.障碍物1.xyCoords=53.95,39.88;55.58,45.69;64.53,45.45;66.97,40.57 |
| | | |
| | |
| | | #Dikuai Properties |
| | | #Fri Dec 26 19:43:09 CST 2025 |
| | | LAND1.angleThreshold=-1 |
| | | LAND1.baseStationCoordinates=3949.89151752,N,11616.79267501,E |
| | | LAND1.boundaryCoordinates=4.30,87.65;-2.36,-65.51;44.25,-66.72;49.70,-14.05;98.13,-15.87;99.34,-69.75;137.48,-67.93;134.45,90.07;4.30,87.65 |
| | | LAND1.boundaryOriginalCoordinates=39.831522,116.279873,49.25;39.831524,116.279878,49.25;39.831525,116.279878,49.24;39.831524,116.279912,49.30;39.831524,116.279911,49.29;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 |
| | | #Sat Dec 27 13:39:05 GMT+08:00 2025 |
| | | LAND1.intelligentSceneAnalysis=-1 |
| | | LAND1.mowingSafetyDistance=0.53 |
| | | LAND1.landArea=577.12 |
| | | LAND1.returnPointCoordinates=-1 |
| | | LAND1.landNumber=LAND1 |
| | | LAND1.returnPathCoordinates=-1 |
| | | LAND1.mowingPattern=平行线 |
| | | LAND1.mowingOverlapDistance=0.06 |
| | | LAND1.returnPathRawCoordinates=-1 |
| | | LAND1.boundaryOriginalXY=-1 |
| | | LAND1.mowingWidth=1.00 |
| | | LAND1.plannedPath=73.871028,49.873176;50.776259,50.037243;41.965934,22.443191;49.612130,19.993609;51.845415,34.259086;66.290000,36.696945;66.290000,21.401369;78.088741,24.750277;73.871028,49.873176;73.955072,49.372566;50.616974,49.538362;50.298405,48.540599;74.123160,48.371347;74.291248,47.370128;49.979836,47.542837;49.661267,46.545075;74.459336,46.368908;74.627423,45.367689;49.342698,45.547313;49.024129,44.549551;55.663908,44.502382;64.552007,44.439240;74.795511,44.366470;74.963599,43.365250;65.098159,43.435335;55.361561,43.504504;48.705560,43.551789;48.386992,42.554027;55.059215,42.506627;65.644310,42.431430;75.131687,42.364031;75.299775,41.362812;66.190462,41.427525;54.756869,41.508750;48.068423,41.556265;47.749854,40.558503;54.454523,40.510872;59.489469,40.475104;75.467863,40.361592;75.635951,39.360373;47.431285,39.560741;47.112716,38.562978;75.804039,38.359154;75.972127,37.357934;46.794147,37.565216;46.475578,36.567454;64.753396,36.437608;66.290000,36.426692;76.140215,36.356715;76.308303,35.355496;66.290000,35.426666;59.067471,35.477976;46.157009,35.569692;45.838440,34.571930;53.381545,34.518343;66.290000,34.426641;76.476391,34.354276;76.644478,33.353057;66.290000,33.426616;51.731282,33.530042;45.519871,33.574168;45.201302,32.576406;51.574900,32.531127;66.290000,32.426591;76.812566,32.351838;76.980654,31.350618;66.290000,31.426565;51.418519,31.532213;44.882733,31.578644;44.564164,30.580882;51.262137,30.533299;66.290000,30.426540;77.148742,30.349399;77.316830,29.348180;66.290000,29.426515;51.105755,29.534385;44.245595,29.583120;43.927026,28.585357;50.949373,28.535470;66.290000,28.426490;77.484918,28.346960;77.653006,27.345741;66.290000,27.426464;50.792992,27.536556;43.608458,27.587595;43.289889,26.589833;50.636610,26.537642;66.290000,26.426439;77.821094,26.344522;77.989182,25.343302;66.290000,25.426414;50.480228,25.538727;42.971320,25.592071;42.652751,24.594309;50.323846,24.539813;66.290000,24.426389;76.687398,24.352525;73.250178,23.376918;66.290000,23.426363;50.167465,23.540899;42.334182,23.596547;42.015613,22.598785;50.011083,22.541985;66.290000,22.426338;69.812957,22.401311;66.375737,21.425704;66.290000,21.426313;49.854701,21.543070;44.660415,21.579971;47.852711,20.557267;49.698319,20.544156 |
| | | LAND1.updateTime=2025-12-27 13\:39\:05 |
| | | LAND1.baseStationCoordinates=3949.89151752,N,11616.79267501,E |
| | | LAND1.boundaryPointInterval=-1 |
| | | LAND1.createTime=2025-12-23 17\:08\:09 |
| | | LAND1.intelligentSceneAnalysis=-1 |
| | | LAND1.landArea=577.12 |
| | | LAND1.landName=123 |
| | | LAND1.landNumber=LAND1 |
| | | LAND1.mowingBladeWidth=0.51 |
| | | LAND1.mowingOverlapDistance=0.06 |
| | | LAND1.mowingPattern=平行线 |
| | | LAND1.mowingSafetyDistance=0.53 |
| | | LAND1.mowingTrack=-1 |
| | | LAND1.mowingWidth=0.50 |
| | | LAND1.obstacleCoordinates=(25.17,9.94;17.74,67.19;113.88,72.00;120.00,19.12;74.99,-4.48) |
| | | LAND1.plannedPath=133.930254,89.530244;4.309853,87.120092;4.829500,87.626975;4.690221,87.491117;4.690221,87.127164;5.190221,87.136461;5.190221,-65.175826;4.690221,-65.162846;4.690221,84.423985;5.690221,87.145758;5.690221,-65.188806;6.190221,-65.201786;6.190221,87.155055;6.690221,87.164352;6.690221,-65.214766;7.190221,-65.227746;7.190221,87.173649;7.690221,87.182946;7.690221,-65.240726;8.190221,-65.253706;8.190221,87.192243;8.690221,87.201540;8.690221,-65.266686;9.190221,-65.279666;9.190221,87.210837;9.690221,87.220134;9.690221,-65.292646;10.190221,-65.305626;10.190221,87.229431;10.690221,87.238728;10.690221,-65.318606;11.190221,-65.331586;11.190221,87.248025;11.690221,87.257322;11.690221,-65.344567;12.190221,-65.357547;12.190221,87.266619;12.690221,87.275916;12.690221,-65.370527;13.190221,-65.383507;13.190221,87.285213;13.690221,87.294510;13.690221,-65.396487;14.190221,-65.409467;14.190221,87.303806;14.690221,87.313103;14.690221,-65.422447;15.190221,-65.435427;15.190221,87.322400;15.690221,87.331697;15.690221,-65.448407;16.190221,-65.461387;16.190221,87.340994;16.690221,87.350291;16.690221,-65.474367;17.190221,-65.487347;17.190221,67.308153;17.190221,67.693157;17.190221,87.359588;17.690221,87.368885;17.690221,67.718172;18.190221,67.743188;18.190221,87.378182;18.690221,87.387479;18.690221,67.768204;19.190221,67.793219;19.190221,87.396776;19.690221,87.406073;19.690221,67.818235;20.190221,67.843250;20.190221,87.415370;20.690221,87.424667;20.690221,67.868266;21.190221,67.893282;21.190221,87.433964;21.690221,87.443261;21.690221,67.918297;22.190221,67.943313;22.190221,87.452558;22.690221,87.461855;22.690221,67.968328;23.190221,67.993344;23.190221,87.471152;23.690221,87.480449;23.690221,68.018360;24.190221,68.043375;24.190221,87.489746;24.690221,87.499043;24.690221,68.068391;25.190221,68.093406;25.190221,87.508340;25.690221,87.517637;25.690221,68.118422;26.190221,68.143438;26.190221,87.526934;26.690221,87.536231;26.690221,68.168453;27.190221,68.193469;27.190221,87.545528;27.690221,87.554825;27.690221,68.218484;28.190221,68.243500;28.190221,87.564121;28.690221,87.573418;28.690221,68.268516;29.190221,68.293531;29.190221,87.582715;29.690221,87.592012;29.690221,68.318547;30.190221,68.343562;30.190221,87.601309;30.690221,87.610606;30.690221,68.368578;31.190221,68.393594;31.190221,87.619903;31.690221,87.629200;31.690221,68.418609;32.190221,68.443625;32.190221,87.638497;32.690221,87.647794;32.690221,68.468640;33.190221,68.493656;33.190221,87.657091;33.690221,87.666388;33.690221,68.518672;34.190221,68.543687;34.190221,87.675685;34.690221,87.684982;34.690221,68.568703;35.190221,68.593718;35.190221,87.694279;35.690221,87.703576;35.690221,68.618734;36.190221,68.643750;36.190221,87.712873;36.690221,87.722170;36.690221,68.668765;37.190221,68.693781;37.190221,87.731467;37.690221,87.740764;37.690221,68.718797;38.190221,68.743812;38.190221,87.750061;38.690221,87.759358;38.690221,68.768828;39.190221,68.793843;39.190221,87.768655;39.690221,87.777952;39.690221,68.818859;40.190221,68.843875;40.190221,87.787249;40.690221,87.796546;40.690221,68.868890;41.190221,68.893906;41.190221,87.805843;41.690221,87.815140;41.690221,68.918921;42.190221,68.943937;42.190221,87.824437;42.690221,87.833733;42.690221,68.968953;43.190221,68.993968;43.190221,87.843030;43.690221,87.852327;43.690221,69.018984;44.190221,69.043999;44.190221,87.861624;44.690221,87.870921;44.690221,69.069015;45.190221,69.094031;45.190221,87.880218;45.690221,87.889515;45.690221,69.119046;46.190221,69.144062;46.190221,87.898812;46.690221,87.908109;46.690221,69.169077;47.190221,69.194093;47.190221,87.917406;47.690221,87.926703;47.690221,69.219109;48.190221,69.244124;48.190221,87.936000;48.690221,87.945297;48.690221,69.269140;49.190221,69.294155;49.190221,87.954594;49.690221,87.963891;49.690221,69.319171;50.190221,69.344187;50.190221,87.973188;50.690221,87.982485;50.690221,69.369202;51.190221,69.394218;51.190221,87.991782;51.690221,88.001079;51.690221,69.419233;52.190221,69.444249;52.190221,88.010376;52.690221,88.019673;52.690221,69.469265;53.190221,69.494280;53.190221,88.028970;53.690221,88.038267;53.690221,69.519296;54.190221,69.544311;54.190221,88.047564;54.690221,88.056861;54.690221,69.569327;55.190221,69.594343;55.190221,88.066158;55.690221,88.075455;55.690221,69.619358;56.190221,69.644374;56.190221,88.084752;56.690221,88.094048;56.690221,69.669389;57.190221,69.694405;57.190221,88.103345;57.690221,88.112642;57.690221,69.719421;58.190221,69.744436;58.190221,88.121939;58.690221,88.131236;58.690221,69.769452;59.190221,69.794467;59.190221,88.140533;59.690221,88.149830;59.690221,69.819483;60.190221,69.844499;60.190221,88.159127;60.690221,88.168424;60.690221,69.869514;61.190221,69.894530;61.190221,88.177721;61.690221,88.187018;61.690221,69.919545;62.190221,69.944561;62.190221,88.196315;62.690221,88.205612;62.690221,69.969577;63.190221,69.994592;63.190221,88.214909;63.690221,88.224206;63.690221,70.019608;64.190221,70.044623;64.190221,88.233503;64.690221,88.242800;64.690221,70.069639;65.190221,70.094655;65.190221,88.252097;65.690221,88.261394;65.690221,70.119670;66.190221,70.144686;66.190221,88.270691;66.690221,88.279988;66.690221,70.169701;67.190221,70.194717;67.190221,88.289285;67.690221,88.298582;67.690221,70.219733;68.190221,70.244748;68.190221,88.307879;68.690221,88.317176;68.690221,70.269764;69.190221,70.294779;69.190221,88.326473;69.690221,88.335770;69.690221,70.319795;70.190221,70.344811;70.190221,88.345067;70.690221,88.354364;70.690221,70.369826;71.190221,70.394842;71.190221,88.363660;71.690221,88.372957;71.690221,70.419857;72.190221,70.444873;72.190221,88.382254;72.690221,88.391551;72.690221,70.469889;73.190221,70.494904;73.190221,88.400848;73.690221,88.410145;73.690221,70.519920;74.190221,70.544935;74.190221,88.419442;74.690221,88.428739;74.690221,70.569951;75.190221,70.594967;75.190221,88.438036;75.690221,88.447333;75.690221,70.619982;76.190221,70.644998;76.190221,88.456630;76.690221,88.465927;76.690221,70.670013;77.190221,70.695029;77.190221,88.475224;77.690221,88.484521;77.690221,70.720045;78.190221,70.745060;78.190221,88.493818;78.690221,88.503115;78.690221,70.770076;79.190221,70.795091;79.190221,88.512412;79.690221,88.521709;79.690221,70.820107;80.190221,70.845123;80.190221,88.531006;80.690221,88.540303;80.690221,70.870138;81.190221,70.895154;81.190221,88.549600;81.690221,88.558897;81.690221,70.920169;82.190221,70.945185;82.190221,88.568194;82.690221,88.577491;82.690221,70.970201;83.190221,70.995216;83.190221,88.586788;83.690221,88.596085;83.690221,71.020232;84.190221,71.045248;84.190221,88.605382;84.690221,88.614679;84.690221,71.070263;85.190221,71.095279;85.190221,88.623976;85.690221,88.633272;85.690221,71.120294;86.190221,71.145310;86.190221,88.642569;86.690221,88.651866;86.690221,71.170326;87.190221,71.195341;87.190221,88.661163;87.690221,88.670460;87.690221,71.220357;88.190221,71.245372;88.190221,88.679757;88.690221,88.689054;88.690221,71.270388;89.190221,71.295404;89.190221,88.698351;89.690221,88.707648;89.690221,71.320419;90.190221,71.345435;90.190221,88.716945;90.690221,88.726242;90.690221,71.370450;91.190221,71.395466;91.190221,88.735539;91.690221,88.744836;91.690221,71.420482;92.190221,71.445497;92.190221,88.754133;92.690221,88.763430;92.690221,71.470513;93.190221,71.495528;93.190221,88.772727;93.690221,88.782024;93.690221,71.520544;94.190221,71.545560;94.190221,88.791321;94.690221,88.800618;94.690221,71.570575;95.190221,71.595591;95.190221,88.809915;95.690221,88.819212;95.690221,71.620606;96.190221,71.645622;96.190221,88.828509;96.690221,88.837806;96.690221,71.670638;97.190221,71.695653;97.190221,88.847103;97.690221,88.856400;97.690221,71.720669;98.190221,71.745684;98.190221,88.865697;98.690221,88.874994;98.690221,71.770700;99.190221,71.795716;99.190221,88.884291;99.690221,88.893587;99.690221,71.820731;100.190221,71.845747;100.190221,88.902884;100.690221,88.912181;100.690221,71.870762;101.190221,71.895778;101.190221,88.921478;101.690221,88.930775;101.690221,71.920794;102.190221,71.945809;102.190221,88.940072;102.690221,88.949369;102.690221,71.970825;103.190221,71.995840;103.190221,88.958666;103.690221,88.967963;103.690221,72.020856;104.190221,72.045872;104.190221,88.977260;104.690221,88.986557;104.690221,72.070887;105.190221,72.095903;105.190221,88.995854;105.690221,89.005151;105.690221,72.120918;106.190221,72.145934;106.190221,89.014448;106.690221,89.023745;106.690221,72.170950;107.190221,72.195965;107.190221,89.033042;107.690221,89.042339;107.690221,72.220981;108.190221,72.245996;108.190221,89.051636;108.690221,89.060933;108.690221,72.271012;109.190221,72.296028;109.190221,89.070230;109.690221,89.079527;109.690221,72.321043;110.190221,72.346059;110.190221,89.088824;110.690221,89.098121;110.690221,72.371074;111.190221,72.396090;111.190221,89.107418;111.690221,89.116715;111.690221,72.421106;112.190221,72.446121;112.190221,89.126012;112.690221,89.135309;112.690221,72.471137;113.190221,72.496152;113.190221,89.144606;113.690221,89.153903;113.690221,72.521168;114.190221,72.546184;114.190221,89.163199;114.690221,89.172496;114.690221,69.609311;115.190221,65.289050;115.190221,89.181793;115.690221,89.191090;115.690221,60.968788;116.190221,56.648527;116.190221,89.200387;116.690221,89.209684;116.690221,52.328266;117.190221,48.008004;117.190221,89.218981;117.690221,89.228278;117.690221,43.687743;118.190221,39.367481;118.190221,89.237575;118.690221,89.246872;118.690221,35.047220;119.190221,30.726958;119.190221,89.256169;119.690221,89.265466;119.690221,26.406697;120.190221,22.086435;120.190221,89.274763;120.690221,89.284060;120.690221,-68.200587;121.190221,-68.176728;121.190221,89.293357;121.690221,89.302654;121.690221,-68.152868;122.190221,-68.129009;122.190221,89.311951;122.690221,89.321248;122.690221,-68.105149;123.190221,-68.081290;123.190221,89.330545;123.690221,89.339842;123.690221,-68.057430;124.190221,-68.033571;124.190221,89.349139;124.690221,89.358436;124.690221,-68.009711;125.190221,-67.985852;125.190221,89.367733;125.690221,89.377030;125.690221,-67.961993;126.190221,-67.938133;126.190221,89.386327;126.690221,89.395624;126.690221,-67.914274;127.190221,-67.890414;127.190221,89.404921;127.690221,89.414218;127.690221,-67.866555;128.190221,-67.842695;128.190221,89.423514;128.690221,89.432811;128.690221,-67.818836;129.190221,-67.794976;129.190221,89.442108;129.690221,89.451405;129.690221,-67.771117;130.190221,-67.747257;130.190221,89.460702;130.690221,89.469999;130.690221,-67.723398;131.190221,-67.699538;131.190221,89.479296;131.690221,89.488593;131.690221,-67.675679;132.190221,-67.651820;132.190221,89.497890;132.690221,89.507187;132.690221,-67.627960;133.190221,-67.604101;133.190221,89.516484;133.690221,89.525781;133.690221,-67.580241;134.190221,-67.556382;134.190221,75.974185;134.690221,49.901578;134.690221,-67.532522;135.190221,23.828971;119.690221,-68.248306;119.690221,18.359139;119.190221,18.096975;119.190221,-68.272166;118.690221,-68.296025;118.690221,17.834811;118.190221,17.572647;118.190221,-68.319885;117.690221,-68.343744;117.690221,17.310483;117.190221,17.048319;117.190221,-68.367603;116.690221,-68.391463;116.690221,16.786155;116.190221,16.523991;116.190221,-68.415322;115.690221,-68.439182;115.690221,16.261827;115.190221,15.999663;115.190221,-68.463041;114.690221,-68.486901;114.690221,15.737499;114.190221,15.475335;114.190221,-68.510760;113.690221,-68.534620;113.690221,15.213171;113.190221,14.951007;113.190221,-68.558479;112.690221,-68.582339;112.690221,14.688843;112.190221,14.426679;112.190221,-68.606198;111.690221,-68.630058;111.690221,14.164515;111.190221,13.902351;111.190221,-68.653917;110.690221,-68.677777;110.690221,13.640187;110.190221,13.378023;110.190221,-68.701636;109.690221,-68.725495;109.690221,13.115860;109.190221,12.853696;109.190221,-68.749355;108.690221,-68.773214;108.690221,12.591532;108.190221,12.329368;108.190221,-68.797074;107.690221,-68.820933;107.690221,12.067204;107.190221,11.805040;107.190221,-68.844793;106.690221,-68.868652;106.690221,11.542876;106.190221,11.280712;106.190221,-68.892512;105.690221,-68.916371;105.690221,11.018548;105.190221,10.756384;105.190221,-68.940231;104.690221,-68.964090;104.690221,10.494220;104.190221,10.232056;104.190221,-68.987950;103.690221,-69.011809;103.690221,9.969892;103.190221,9.707728;103.190221,-69.035668;102.690221,-69.059528;102.690221,9.445564;102.190221,9.183400;102.190221,-69.083387;101.690221,-69.107247;101.690221,8.921236;101.190221,8.659072;101.190221,-69.131106;100.690221,-69.154966;100.690221,8.396908;100.190221,8.134744;100.190221,-69.178825;99.690221,-61.738685;99.690221,7.872580;99.190221,7.610416;99.190221,-39.474222;98.690221,-17.209759;98.690221,7.348252;98.190221,7.086088;98.190221,-15.341889;97.690221,-15.323099;97.690221,6.823924;97.190221,6.561760;97.190221,-15.304309;96.690221,-15.285519;96.690221,6.299596;96.190221,6.037433;96.190221,-15.266729;95.690221,-15.247939;95.690221,5.775269;95.190221,5.513105;95.190221,-15.229149;94.690221,-15.210359;94.690221,5.250941;94.190221,4.988777;94.190221,-15.191569;93.690221,-15.172779;93.690221,4.726613;93.190221,4.464449;93.190221,-15.153989;92.690221,-15.135199;92.690221,4.202285;92.190221,3.940121;92.190221,-15.116409;91.690221,-15.097619;91.690221,3.677957;91.190221,3.415793;91.190221,-15.078829;90.690221,-15.060039;90.690221,3.153629;90.190221,2.891465;90.190221,-15.041249;89.690221,-15.022459;89.690221,2.629301;89.190221,2.367137;89.190221,-15.003669;88.690221,-14.984879;88.690221,2.104973;88.190221,1.842809;88.190221,-14.966089;87.690221,-14.947299;87.690221,1.580645;87.190221,1.318481;87.190221,-14.928509;86.690221,-14.909719;86.690221,1.056317;86.190221,0.794153;86.190221,-14.890929;85.690221,-14.872139;85.690221,0.531989;85.190221,0.269825;85.190221,-14.853349;84.690221,-14.834559;84.690221,0.007661;84.190221,-0.254503;84.190221,-14.815769;83.690221,-14.796979;83.690221,-0.516667;83.190221,-0.778831;83.190221,-14.778189;82.690221,-14.759399;82.690221,-1.040995;82.190221,-1.303158;82.190221,-14.740609;81.690221,-14.721819;81.690221,-1.565322;81.190221,-1.827486;81.190221,-14.703029;80.690221,-14.684239;80.690221,-2.089650;80.190221,-2.351814;80.190221,-14.665449;79.690221,-14.646659;79.690221,-2.613978;79.190221,-2.876142;79.190221,-14.627869;78.690221,-14.609079;78.690221,-3.138306;78.190221,-3.400470;78.190221,-14.590289;77.690221,-14.571499;77.690221,-3.662634;77.190221,-3.924798;77.190221,-14.552709;76.690221,-14.533919;76.690221,-4.186962;76.190221,-4.449126;76.190221,-14.515129;75.690221,-14.496339;75.690221,-4.711290;75.190221,-4.973454;75.190221,-14.477549;74.690221,-14.458759;74.690221,-4.944986;74.190221,-4.800265;74.190221,-14.439969;73.690221,-14.421179;73.690221,-4.655544;73.190221,-4.510823;73.190221,-14.402389;72.690221,-14.383599;72.690221,-4.366102;72.190221,-4.221381;72.190221,-14.364809;71.690221,-14.346019;71.690221,-4.076660;71.190221,-3.931939;71.190221,-14.327229;70.690221,-14.308439;70.690221,-3.787218;70.190221,-3.642497;70.190221,-14.289649;69.690221,-14.270859;69.690221,-3.497776;69.190221,-3.353055;69.190221,-14.252069;68.690221,-14.233279;68.690221,-3.208334;68.190221,-3.063613;68.190221,-14.214489;67.690221,-14.195699;67.690221,-2.918892;67.190221,-2.774171;67.190221,-14.176909;66.690221,-14.158119;66.690221,-2.629450;66.190221,-2.484729;66.190221,-14.139329;65.690221,-14.120539;65.690221,-2.340008;65.190221,-2.195287;65.190221,-14.101749;64.690221,-14.082959;64.690221,-2.050566;64.190221,-1.905845;64.190221,-14.064169;63.690221,-14.045379;63.690221,-1.761124;63.190221,-1.616403;63.190221,-14.026589;62.690221,-14.007799;62.690221,-1.471682;62.190221,-1.326961;62.190221,-13.989009;61.690221,-13.970219;61.690221,-1.182240;61.190221,-1.037519;61.190221,-13.951429;60.690221,-13.932639;60.690221,-0.892798;60.190221,-0.748077;60.190221,-13.913849;59.690221,-13.895059;59.690221,-0.603356;59.190221,-0.458635;59.190221,-13.876269;58.690221,-13.857479;58.690221,-0.313914;58.190221,-0.169193;58.190221,-13.838688;57.690221,-13.819898;57.690221,-0.024472;57.190221,0.120249;57.190221,-13.801108;56.690221,-13.782318;56.690221,0.264970;56.190221,0.409691;56.190221,-13.763528;55.690221,-13.744738;55.690221,0.554412;55.190221,0.699133;55.190221,-13.725948;54.690221,-13.707158;54.690221,0.843854;54.190221,0.988575;54.190221,-13.688368;53.690221,-13.669578;53.690221,1.133296;53.190221,1.278017;53.190221,-13.650788;52.690221,-13.631998;52.690221,1.422738;52.190221,1.567459;52.190221,-13.613208;51.690221,-13.594418;51.690221,1.712180;51.190221,1.856901;51.190221,-13.575628;50.690221,-13.556838;50.690221,2.001622;50.190221,2.146343;50.190221,-13.538048;49.690221,-13.519258;49.690221,2.291064;49.190221,2.435785;49.190221,-13.827232;48.690221,-18.659342;48.690221,2.580506;48.190221,2.725227;48.190221,-23.491452;47.690221,-28.323562;47.690221,2.869948;47.190221,3.014669;47.190221,-33.155672;46.690221,-37.987782;46.690221,3.159390;46.190221,3.304111;46.190221,-42.819892;45.690221,-47.652003;45.690221,3.448832;45.190221,3.593553;45.190221,-52.484113;44.690221,-57.316223;44.690221,3.738274;44.190221,3.882995;44.190221,-62.148333;43.690221,-66.175290;43.690221,4.027716;43.190221,4.172437;43.190221,-66.162309;42.690221,-66.149329;42.690221,4.317158;42.190221,4.461879;42.190221,-66.136349;41.690221,-66.123369;41.690221,4.606600;41.190221,4.751321;41.190221,-66.110389;40.690221,-66.097409;40.690221,4.896042;40.190221,5.040763;40.190221,-66.084429;39.690221,-66.071449;39.690221,5.185484;39.190221,5.330205;39.190221,-66.058469;38.690221,-66.045489;38.690221,5.474926;38.190221,5.619647;38.190221,-66.032509;37.690221,-66.019529;37.690221,5.764368;37.190221,5.909089;37.190221,-66.006549;36.690221,-65.993569;36.690221,6.053810;36.190221,6.198531;36.190221,-65.980589;35.690221,-65.967609;35.690221,6.343252;35.190221,6.487973;35.190221,-65.954629;34.690221,-65.941649;34.690221,6.632694;34.190221,6.777415;34.190221,-65.928669;33.690221,-65.915689;33.690221,6.922136;33.190221,7.066857;33.190221,-65.902709;32.690221,-65.889728;32.690221,7.211578;32.190221,7.356299;32.190221,-65.876748;31.690221,-65.863768;31.690221,7.501020;31.190221,7.645741;31.190221,-65.850788;30.690221,-65.837808;30.690221,7.790462;30.190221,7.935183;30.190221,-65.824828;29.690221,-65.811848;29.690221,8.079904;29.190221,8.224625;29.190221,-65.798868;28.690221,-65.785888;28.690221,8.369346;28.190221,8.514067;28.190221,-65.772908;27.690221,-65.759928;27.690221,8.658788;27.190221,8.803509;27.190221,-65.746948;26.690221,-65.733968;26.690221,8.948230;26.190221,9.092951;26.190221,-65.720988;25.690221,-65.708008;25.690221,9.237672;25.190221,9.382393;25.190221,-65.695028;24.690221,-65.682048;24.690221,9.527114;24.190221,13.371410;24.190221,-65.669068;23.690221,-65.656088;23.690221,17.224035;23.190221,21.076659;23.190221,-65.643108;22.690221,-65.630128;22.690221,24.929284;22.190221,28.781908;22.190221,-65.617147;21.690221,-65.604167;21.690221,32.634533;21.190221,36.487157;21.190221,-65.591187;20.690221,-65.578207;20.690221,40.339782;20.190221,44.192406;20.190221,-65.565227;19.690221,-65.552247;19.690221,48.045031;19.190221,51.897655;19.190221,-65.539267;18.690221,-65.526287;18.690221,55.750280;18.190221,59.602904;18.190221,-65.513307;3.690221,61.426988;3.190221,-65.123906;3.190221,49.928490;2.690221,38.429991;2.690221,-65.110926;2.190221,-65.097946;2.190221,26.931493;1.690221,15.432994;1.690221,-65.084966;1.190221,-65.071986;1.190221,3.934496;0.690221,-7.564003;0.690221,-65.059005;0.190221,-65.046025;0.190221,-19.062501;-0.309779,-30.561000;-0.309779,-65.033045;-0.809779,-65.020065;-0.809779,-42.059498;-1.309779,-53.557997;-1.309779,-65.007085;135.690221,-2.243636;136.190221,-67.460944;136.190221,-28.316244;136.690221,-54.388851;136.690221,-67.437084 |
| | | LAND1.returnPathCoordinates=-1 |
| | | LAND1.returnPathRawCoordinates=-1 |
| | | LAND1.returnPointCoordinates=-1 |
| | | LAND1.updateTime=2025-12-26 19\:43\:09 |
| | | LAND1.angleThreshold=-1 |
| | | LAND1.userId=-1 |
| | | LAND1.landName=123 |
| | | LAND1.mowingTrack=-1 |
| | | LAND1.boundaryOriginalCoordinates=39.831522,116.279873,49.25;39.831524,116.279878,49.25;39.831525,116.279878,49.24;39.831524,116.279912,49.30;39.831524,116.279911,49.29;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 |
| | | LAND1.obstacleCoordinates=(53.95,39.88;55.58,45.69;64.53,45.45;66.97,40.57) |
| | | LAND1.boundaryCoordinates=50.39,50.57;74.32,50.40;78.69,24.37;65.76,20.70;65.76,36.07;52.31,33.80;50.04,19.30;41.30,22.10 |
| | | LAND1.mowingBladeWidth=0.51 |
| | |
| | | #Mower Configuration Properties - Updated |
| | | #Fri Dec 26 19:43:47 CST 2025 |
| | | #Sat Dec 27 13:39:12 GMT+08:00 2025 |
| | | appVersion=-1 |
| | | boundaryLengthVisible=false |
| | | currentWorkLandNumber=LAND1 |
| | | cuttingWidth=200 |
| | | firmwareVersion=-1 |
| | | handheldMarkerId=1872 |
| | | idleTrailDurationSeconds=60 |
| | | manualBoundaryDrawingMode=false |
| | | mapScale=11.81 |
| | | measurementModeEnabled=false |
| | | mowerId=6258 |
| | | serialAutoConnect=true |
| | | serialBaudRate=115200 |
| | | serialPortName=COM15 |
| | | simCardNumber=-1 |
| | | viewCenterX=-134.26 |
| | | viewCenterY=20.96 |
| | | currentWorkLandNumber=LAND1 |
| | | serialBaudRate=115200 |
| | | boundaryLengthVisible=false |
| | | idleTrailDurationSeconds=60 |
| | | handheldMarkerId=1872 |
| | | viewCenterX=-60.00 |
| | | viewCenterY=-34.94 |
| | | manualBoundaryDrawingMode=false |
| | | mowerId=6258 |
| | | serialPortName=COM15 |
| | | serialAutoConnect=true |
| | | mapScale=10.32 |
| | | measurementModeEnabled=false |
| | | firmwareVersion=-1 |
| | | cuttingWidth=200 |
| | |
| | | #\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 26 15:33:26 CST 2025 |
| | | boundaryCoordinates=25.17,9.94;17.74,67.19;113.88,72.00;120.00,19.12;74.99,-4.48 |
| | | email=789 |
| | | language=zh |
| | | #Sat Dec 27 13:37:22 GMT+08:00 2025 |
| | | registrationTime=-1 |
| | | lastLoginTime=-1 |
| | | password=123 |
| | | pointCount=5 |
| | | registrationTime=-1 |
| | | status=-1 |
| | | userId=-1 |
| | | pointCount=4 |
| | | boundaryCoordinates=53.95,39.88;55.58,45.69;64.53,45.45;66.97,40.57 |
| | | language=zh |
| | | userName=233 |
| | | userId=-1 |
| | | email=789 |
| | | status=-1 |
| | |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | | import java.util.function.Consumer; |
| | | // import java.util.function.Consumer; |
| | | |
| | | import com.fazecast.jSerialComm.SerialPort; |
| | | |
| | |
| | | private volatile boolean capturing = false; |
| | | private volatile boolean paused = true; |
| | | private Thread readerThread; |
| | | private Consumer<byte[]> responseConsumer; |
| | | private DataListener<byte[]> responseConsumer; |
| | | |
| | | // 优化:重用缓冲区,减少内存分配 |
| | | private byte[] readBuffer = new byte[200]; |
| | | private ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024); |
| | | private Consumer<byte[]> dataReceivedCallback; |
| | | private DataListener<byte[]> dataReceivedCallback; |
| | | |
| | | |
| | | // 新增:数据条数计数器 |
| | |
| | | if (dataReceivedCallback != null) { |
| | | startCapture(dataReceivedCallback); |
| | | } else { |
| | | System.err.println("No data received callback set. Please call startCapture(Consumer<byte[]> onReceived) first."); |
| | | System.err.println("No data received callback set. Please call startCapture(DataListener<byte[]> onReceived) first."); |
| | | } |
| | | } |
| | | |
| | |
| | | return port.openPort(); |
| | | } |
| | | |
| | | public void setResponseConsumer(Consumer<byte[]> consumer) { |
| | | public void setResponseConsumer(DataListener<byte[]> consumer) { |
| | | this.responseConsumer = consumer; |
| | | } |
| | | |
| | |
| | | /** |
| | | * 启动数据接收线程 |
| | | */ |
| | | public void startCapture(Consumer<byte[]> onReceived) { |
| | | public void startCapture(DataListener<byte[]> onReceived) { |
| | | this.dataReceivedCallback = onReceived; |
| | | if (capturing || port == null || !port.isOpen()) return; |
| | | capturing = true; |
| | | paused = false; |
| | | |
| | | readerThread = new Thread(() -> { |
| | | readerThread = new Thread(new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | buffer.reset(); |
| | | long lastReceivedTime = 0; |
| | | |
| | |
| | | responseConsumer.accept(data); |
| | | } |
| | | } |
| | | }); |
| | | } }); |
| | | readerThread.setDaemon(true); |
| | | readerThread.start(); |
| | | } |
| | |
| | | import java.util.concurrent.CopyOnWriteArrayList; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | import java.util.function.Consumer; |
| | | // import java.util.function.Consumer; |
| | | |
| | | /** |
| | | * 串口实时数据调度中心。 |
| | |
| | | * GNGGA 数据的内置解析支持,复用 UDP 处理逻辑。 |
| | | */ |
| | | public final class dellmessage { |
| | | private static final CopyOnWriteArrayList<Consumer<byte[]>> RAW_CONSUMERS = new CopyOnWriteArrayList<>(); |
| | | private static final CopyOnWriteArrayList<Consumer<String>> LINE_CONSUMERS = new CopyOnWriteArrayList<>(); |
| | | private static final CopyOnWriteArrayList<DataListener<byte[]>> RAW_CONSUMERS = new CopyOnWriteArrayList<>(); |
| | | private static final CopyOnWriteArrayList<DataListener<String>> LINE_CONSUMERS = new CopyOnWriteArrayList<>(); |
| | | private static final StringBuilder LINE_BUFFER = new StringBuilder(512); |
| | | private static final AtomicInteger LINE_COUNTER = new AtomicInteger(0); |
| | | private static final AtomicReference<String> LAST_LINE = new AtomicReference<>(""); |
| | |
| | | * |
| | | * @param consumer 接收完整数据帧的监听器 |
| | | */ |
| | | public static void registerRawListener(Consumer<byte[]> consumer) { |
| | | public static void registerRawListener(DataListener<byte[]> consumer) { |
| | | // 用法:在需要直接处理原始串口字节流的模块启动时调用,传入回调处理数据帧。 |
| | | if (consumer != null) { |
| | | RAW_CONSUMERS.addIfAbsent(consumer); |
| | |
| | | /** |
| | | * 注销原始字节监听器。 |
| | | */ |
| | | public static void unregisterRawListener(Consumer<byte[]> consumer) { |
| | | public static void unregisterRawListener(DataListener<byte[]> consumer) { |
| | | // 用法:模块销毁或不再需要接收原始数据时调用,避免内存泄漏。 |
| | | if (consumer != null) { |
| | | RAW_CONSUMERS.remove(consumer); |
| | |
| | | * <p> |
| | | * 每一条经由换行符截断的完整文本行将触发一次回调。 |
| | | */ |
| | | public static void registerLineListener(Consumer<String> consumer) { |
| | | public static void registerLineListener(DataListener<String> consumer) { |
| | | // 用法:需要按行读取串口文本(如 NMEA 报文)时调用,回调拿到完整文本行。 |
| | | if (consumer != null) { |
| | | LINE_CONSUMERS.addIfAbsent(consumer); |
| | |
| | | /** |
| | | * 注销文本行监听器。 |
| | | */ |
| | | public static void unregisterLineListener(Consumer<String> consumer) { |
| | | public static void unregisterLineListener(DataListener<String> consumer) { |
| | | // 用法:对应 registerLineListener 的反注册操作,通常在窗口关闭或服务停止时调用。 |
| | | if (consumer != null) { |
| | | LINE_CONSUMERS.remove(consumer); |
| | |
| | | } |
| | | |
| | | private static void notifyRawConsumers(byte[] data) { |
| | | for (Consumer<byte[]> consumer : RAW_CONSUMERS) { |
| | | for (DataListener<byte[]> consumer : RAW_CONSUMERS) { |
| | | try { |
| | | consumer.accept(data); |
| | | } catch (Exception ex) { |
| | |
| | | } |
| | | |
| | | LAST_LINE.set(line); |
| | | LINE_COUNTER.updateAndGet(count -> count >= 10000 ? 1 : count + 1); |
| | | // LINE_COUNTER.updateAndGet(count -> count >= 10000 ? 1 : count + 1); |
| | | int current, next; |
| | | do { |
| | | current = LINE_COUNTER.get(); |
| | | next = (current >= 10000) ? 1 : current + 1; |
| | | } while (!LINE_COUNTER.compareAndSet(current, next)); |
| | | |
| | | for (Consumer<String> consumer : LINE_CONSUMERS) { |
| | | for (DataListener<String> consumer : LINE_CONSUMERS) { |
| | | try { |
| | | consumer.accept(line); |
| | | } catch (Exception ex) { |
| | |
| | | public static void launchMainApp() { |
| | | System.out.println("准备打开主应用程序..."); |
| | | |
| | | SwingUtilities.invokeLater(() -> { |
| | | JFrame mainFrame = new JFrame("智能割草系统"); |
| | | mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); |
| | | SwingUtilities.invokeLater(new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | JFrame mainFrame = new JFrame("智能割草系统"); |
| | | mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); |
| | | |
| | | Shouye homePanel = new Shouye(); |
| | | mainFrame.setContentPane(homePanel); |
| | | Shouye homePanel = new Shouye(); |
| | | mainFrame.setContentPane(homePanel); |
| | | |
| | | // 设置与登录页面相同的尺寸 |
| | | mainFrame.setSize(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT); |
| | | mainFrame.setMinimumSize(new Dimension(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT)); |
| | | mainFrame.setResizable(true); |
| | | mainFrame.setLocationRelativeTo(null); |
| | | mainFrame.setVisible(true); |
| | | // 设置与登录页面相同的尺寸 |
| | | mainFrame.setSize(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT); |
| | | mainFrame.setMinimumSize(new Dimension(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT)); |
| | | mainFrame.setResizable(true); |
| | | mainFrame.setLocationRelativeTo(null); |
| | | mainFrame.setVisible(true); |
| | | |
| | | System.out.println("主应用程序已启动"); |
| | | |
| | | // 启动后连接MQTT |
| | | new Thread(() -> { |
| | | System.out.println("正在连接MQTT服务器..."); |
| | | Client.lianjiemqqt(); |
| | | }).start(); |
| | | System.out.println("主应用程序已启动"); |
| | | |
| | | // 启动后连接MQTT |
| | | new Thread(new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | System.out.println("正在连接MQTT服务器..."); |
| | | Client.lianjiemqqt(); |
| | | } |
| | | }).start(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | |
| | | // 无障碍物的情况 |
| | | if (grassType == 1) { |
| | | // 凸形地块,无障碍物 -> 调用 AoxinglujingNoObstacle |
| | | System.out.println("调用算法: 凸形无障碍物, 类名: AoxinglujingNoObstacle"); |
| | | List<AoxinglujingNoObstacle.PathSegment> segments = |
| | | AoxinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); |
| | | generated = formatAoxingPathSegments(segments); |
| | | } else if (grassType == 2) { |
| | | // 异形地块,无障碍物 -> 调用 YixinglujingNoObstacle |
| | | // 调用 YixinglujingNoObstacle.planPath 获取路径段列表 |
| | | System.out.println("调用算法: 异形无障碍物, 类名: YixinglujingNoObstacle"); |
| | | List<YixinglujingNoObstacle.PathSegment> segments = |
| | | YixinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); |
| | | // 格式化路径段列表为字符串 |
| | |
| | | JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,尝试按凸形地块处理", |
| | | "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | System.out.println("调用算法: 无法判断类型(默认凸形无障碍物), 类名: AoxinglujingNoObstacle"); |
| | | List<AoxinglujingNoObstacle.PathSegment> segments = |
| | | AoxinglujingNoObstacle.planPath(boundary, plannerWidth, safetyMarginStr); |
| | | generated = formatAoxingPathSegments(segments); |
| | |
| | | if (grassType == 1) { |
| | | // 凸形地块,有障碍物 -> 调用 AoxinglujingHaveObstacel |
| | | // 传入参数:boundary(A), obstacles(B), plannerWidth(C), safetyMarginStr(D) |
| | | System.out.println("调用算法: 凸形有障碍物, 类名: AoxinglujingHaveObstacel"); |
| | | List<AoxinglujingHaveObstacel.PathSegment> segments = |
| | | AoxinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr); |
| | | generated = formatAoxingHaveObstaclePathSegments(segments); |
| | | } else if (grassType == 2) { |
| | | // 异形地块,有障碍物 -> 调用 YixinglujingHaveObstacel |
| | | // 传入参数:boundary(A), obstacles(B), plannerWidth(C), safetyMarginStr(D) |
| | | System.out.println("调用算法: 异形有障碍物, 类名: YixinglujingHaveObstacel"); |
| | | List<YixinglujingHaveObstacel.PathSegment> segments = |
| | | YixinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr); |
| | | generated = formatYixingHaveObstaclePathSegments(segments); |
| | |
| | | JOptionPane.showMessageDialog(parentComponent, "无法判断地块类型,尝试按凸形地块处理", |
| | | "提示", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | System.out.println("调用算法: 无法判断类型(默认凸形有障碍物), 类名: AoxinglujingHaveObstacel"); |
| | | List<AoxinglujingHaveObstacel.PathSegment> segments = |
| | | AoxinglujingHaveObstacel.planPath(boundary, obstacles, plannerWidth, safetyMarginStr); |
| | | generated = formatAoxingHaveObstaclePathSegments(segments); |
| | |
| | | package lujing; |
| | | |
| | | import java.util.*; |
| | | import java.util.regex.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | |
| | | /** |
| | | * 异形草地路径规划 - 优化完善版 |
| | | * 采用更完善的算法: |
| | | * 1. 使用多边形裁剪库计算更精确的内缩边界 |
| | | * 2. 使用扫描线填充算法生成更优化的路径 |
| | | * 3. 使用可见图算法寻找最优绕行路径 |
| | | * 4. 使用路径优化算法减少空行和转弯 |
| | | * 异形草地路径规划 - 含障碍物版 |
| | | * 功能:在地块内部避开障碍物,生成连续弓字形割草路径 |
| | | */ |
| | | public class YixinglujingHaveObstacel { |
| | | |
| | | private static final double EPS = 1e-10; |
| | | private static final double MIN_SEG_LEN = 0.01; // 忽略小于1cm的碎线 |
| | | private static final double CORNER_THRESHOLD = Math.toRadians(30); // 30度以下的角度合并 |
| | | |
| | | public static List<PathSegment> planPath(String coordinates, String obstaclesStr, String widthStr, String marginStr) { |
| | | try { |
| | | // 解析输入参数 |
| | | double mowWidth = Double.parseDouble(widthStr); |
| | | double safeMargin = Double.parseDouble(marginStr); |
| | | |
| | | // 解析多边形和障碍物 |
| | | List<Point> boundary = parseCoordinates(coordinates); |
| | | if (boundary.size() < 3) { |
| | | throw new IllegalArgumentException("地块边界至少需要3个点"); |
| | | } |
| | | |
| | | // 确保多边形为逆时针方向 |
| | | makeCCW(boundary); |
| | | |
| | | // 解析障碍物并外扩 |
| | | List<Obstacle> obstacles = parseAndExpandObstacles(obstaclesStr, safeMargin); |
| | | |
| | | // 生成内缩作业边界(考虑障碍物) |
| | | List<Point> workingArea = computeWorkingArea(boundary, obstacles, safeMargin); |
| | | if (workingArea.isEmpty()) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | // 生成完整的全覆盖路径(不考虑障碍物) |
| | | List<PathSegment> fullPath = generateCompleteCoverage(workingArea, mowWidth); |
| | | |
| | | // 用障碍物裁剪路径 |
| | | List<PathSegment> clippedPath = clipPathWithObstacles(fullPath, obstacles); |
| | | |
| | | // 连接和优化路径(限制在作业边界内) |
| | | List<PathSegment> finalPath = connectAndOptimizePath(clippedPath, obstacles, mowWidth, workingArea); |
| | | |
| | | return finalPath; |
| | | |
| | | } catch (Exception e) { |
| | | System.err.println("路径规划错误: " + e.getMessage()); |
| | | e.printStackTrace(); |
| | | return new ArrayList<>(); |
| | | public static List<PathSegment> planPath(String coordinates, String obstaclesStr, |
| | | String widthStr, String marginStr) { |
| | | // 1. 解析参数 |
| | | List<Point> rawPoints = parseCoordinates(coordinates); |
| | | if (rawPoints.size() < 3) return new ArrayList<>(); |
| | | |
| | | double mowWidth = Double.parseDouble(widthStr); |
| | | double safeMargin = Double.parseDouble(marginStr); |
| | | |
| | | // 解析障碍物 |
| | | List<Obstacle> obstacles = parseObstacles(obstaclesStr); |
| | | |
| | | // 2. 预处理:确保边界逆时针 |
| | | ensureCounterClockwise(rawPoints); |
| | | |
| | | // 3. 生成内缩多边形(安全边界) |
| | | List<Point> boundary = getInsetPolygon(rawPoints, safeMargin); |
| | | if (boundary.size() < 3) return new ArrayList<>(); |
| | | |
| | | // 4. 外扩障碍物(安全边距) |
| | | List<Obstacle> expandedObstacles = expandObstacles(obstacles, safeMargin); |
| | | |
| | | // 5. 确定最优作业角度 |
| | | double bestAngle = findOptimalAngle(boundary); |
| | | |
| | | // 6. 获取首个作业点,用于对齐围边起点 |
| | | Point firstScanStart = getFirstScanPoint(boundary, mowWidth, bestAngle); |
| | | |
| | | // 7. 对齐围边 |
| | | List<Point> alignedBoundary = alignBoundaryStart(boundary, firstScanStart); |
| | | |
| | | // 8. 第一阶段:围边路径 |
| | | List<PathSegment> finalPath = new ArrayList<>(); |
| | | for (int i = 0; i < alignedBoundary.size(); i++) { |
| | | Point pStart = alignedBoundary.get(i); |
| | | Point pEnd = alignedBoundary.get((i + 1) % alignedBoundary.size()); |
| | | finalPath.add(new PathSegment(pStart, pEnd, true)); |
| | | } |
| | | |
| | | // 9. 第二阶段:生成内部扫描路径(考虑障碍物) |
| | | Point lastEdgePos = alignedBoundary.get(0); |
| | | List<PathSegment> scanPath = generateGlobalScanPathWithObstacles( |
| | | boundary, expandedObstacles, mowWidth, bestAngle, lastEdgePos); |
| | | |
| | | finalPath.addAll(scanPath); |
| | | |
| | | return finalPath; |
| | | } |
| | | |
| | | /** |
| | | * 计算作业区域(考虑障碍物) |
| | | * 生成带障碍物的扫描路径 |
| | | */ |
| | | private static List<Point> computeWorkingArea(List<Point> boundary, List<Obstacle> obstacles, double margin) { |
| | | // 首先生成内缩边界 |
| | | List<Point> offsetBoundary = offsetPolygon(boundary, margin); |
| | | private static List<PathSegment> generateGlobalScanPathWithObstacles( |
| | | List<Point> polygon, List<Obstacle> obstacles, |
| | | double width, double angle, Point startPos) { |
| | | |
| | | if (obstacles.isEmpty()) { |
| | | return offsetBoundary; |
| | | } |
| | | // 1. 生成原始扫描线(无障碍物) |
| | | List<PathSegment> originalSegments = generateGlobalScanPath(polygon, width, angle, startPos); |
| | | |
| | | // 如果存在障碍物,从内缩边界中减去障碍物区域 |
| | | // 简化处理:工作区域仍以内缩边界为主,具体裁剪在路径层面完成 |
| | | makeCCW(offsetBoundary); |
| | | return offsetBoundary; |
| | | } |
| | | |
| | | /** |
| | | * 生成完整的全覆盖路径 |
| | | */ |
| | | private static List<PathSegment> generateCompleteCoverage(List<Point> polygon, double width) { |
| | | List<PathSegment> path = new ArrayList<>(); |
| | | |
| | | // 1. 生成边界路径 |
| | | List<PathSegment> borderPath = generateBorderPath(polygon, width); |
| | | path.addAll(borderPath); |
| | | |
| | | // 2. 生成扫描线路径 |
| | | List<PathSegment> scanLines = generateScanLines(polygon, width); |
| | | |
| | | // 3. 连接扫描线 |
| | | if (!scanLines.isEmpty()) { |
| | | Point currentPos = path.isEmpty() ? scanLines.get(0).start : |
| | | path.get(path.size() - 1).end; |
| | | |
| | | for (PathSegment scanLine : scanLines) { |
| | | // 添加空行连接 |
| | | if (distance(currentPos, scanLine.start) > MIN_SEG_LEN) { |
| | | path.add(new PathSegment(currentPos, scanLine.start, false)); |
| | | } |
| | | path.add(scanLine); |
| | | currentPos = scanLine.end; |
| | | } |
| | | |
| | | // 连接回起点 |
| | | if (distance(currentPos, path.get(0).start) > MIN_SEG_LEN) { |
| | | path.add(new PathSegment(currentPos, path.get(0).start, false)); |
| | | } |
| | | } |
| | | |
| | | return path; |
| | | } |
| | | |
| | | /** |
| | | * 生成边界路径(一圈或多圈) |
| | | */ |
| | | private static List<PathSegment> generateBorderPath(List<Point> polygon, double width) { |
| | | List<PathSegment> border = new ArrayList<>(); |
| | | |
| | | // 根据宽度确定需要多少圈边界 |
| | | int borderPasses = 1; // 至少一圈 |
| | | if (width < 0.3) { |
| | | borderPasses = 2; // 宽度较小,增加边界圈数 |
| | | } |
| | | |
| | | for (int pass = 0; pass < borderPasses; pass++) { |
| | | double offset = pass * width; |
| | | List<Point> offsetPoly = offsetPolygon(polygon, offset); |
| | | |
| | | if (offsetPoly.size() < 3) break; |
| | | |
| | | for (int i = 0; i < offsetPoly.size(); i++) { |
| | | Point start = offsetPoly.get(i); |
| | | Point end = offsetPoly.get((i + 1) % offsetPoly.size()); |
| | | border.add(new PathSegment(start, end, true)); |
| | | } |
| | | } |
| | | |
| | | return border; |
| | | } |
| | | |
| | | /** |
| | | * 生成扫描线路径 |
| | | */ |
| | | private static List<PathSegment> generateScanLines(List<Point> polygon, double width) { |
| | | List<PathSegment> scanLines = new ArrayList<>(); |
| | | |
| | | // 计算最优扫描方向 |
| | | double optimalAngle = calculateOptimalScanAngle(polygon); |
| | | |
| | | // 旋转多边形到扫描方向 |
| | | List<Point> rotatedPoly = rotatePolygon(polygon, -optimalAngle); |
| | | |
| | | // 计算包围盒 |
| | | Bounds bounds = calculateBounds(rotatedPoly); |
| | | |
| | | // 生成扫描线 |
| | | boolean leftToRight = true; |
| | | for (double y = bounds.minY + width / 2; y <= bounds.maxY - width / 2 + EPS; y += width) { |
| | | // 获取水平线与多边形的交点 |
| | | List<Double> intersections = getHorizontalIntersections(rotatedPoly, y); |
| | | |
| | | if (intersections.size() < 2) continue; |
| | | |
| | | // 交点排序并成对处理 |
| | | Collections.sort(intersections); |
| | | List<PathSegment> lineSegments = new ArrayList<>(); |
| | | |
| | | for (int i = 0; i < intersections.size(); i += 2) { |
| | | if (i + 1 >= intersections.size()) break; |
| | | |
| | | double x1 = intersections.get(i); |
| | | double x2 = intersections.get(i + 1); |
| | | |
| | | if (x2 - x1 < MIN_SEG_LEN) continue; |
| | | |
| | | // 旋转回原始坐标系 |
| | | Point start = rotatePoint(new Point(x1, y), optimalAngle); |
| | | Point end = rotatePoint(new Point(x2, y), optimalAngle); |
| | | |
| | | lineSegments.add(new PathSegment(start, end, true)); |
| | | } |
| | | |
| | | // 方向交替 |
| | | if (!leftToRight) { |
| | | Collections.reverse(lineSegments); |
| | | for (PathSegment seg : lineSegments) { |
| | | Point temp = seg.start; |
| | | seg.start = seg.end; |
| | | seg.end = temp; |
| | | } |
| | | } |
| | | |
| | | scanLines.addAll(lineSegments); |
| | | leftToRight = !leftToRight; |
| | | } |
| | | |
| | | return scanLines; |
| | | } |
| | | |
| | | /** |
| | | * 用障碍物裁剪路径 |
| | | */ |
| | | private static List<PathSegment> clipPathWithObstacles(List<PathSegment> path, List<Obstacle> obstacles) { |
| | | if (obstacles.isEmpty()) return path; |
| | | |
| | | List<PathSegment> clipped = new ArrayList<>(); |
| | | |
| | | for (PathSegment segment : path) { |
| | | List<PathSegment> remaining = new ArrayList<>(); |
| | | remaining.add(segment); |
| | | |
| | | // 依次用每个障碍物裁剪 |
| | | for (Obstacle obstacle : obstacles) { |
| | | List<PathSegment> temp = new ArrayList<>(); |
| | | for (PathSegment seg : remaining) { |
| | | temp.addAll(obstacle.clipSegment(seg)); |
| | | } |
| | | remaining = temp; |
| | | } |
| | | |
| | | clipped.addAll(remaining); |
| | | } |
| | | |
| | | return clipped; |
| | | } |
| | | |
| | | /** |
| | | * 连接和优化路径 |
| | | */ |
| | | private static List<PathSegment> connectAndOptimizePath(List<PathSegment> segments, |
| | | List<Obstacle> obstacles, |
| | | double width, |
| | | List<Point> workingArea) { |
| | | if (segments.isEmpty()) return new ArrayList<>(); |
| | | |
| | | // 1. 先按类型分组:割草段和连接段 |
| | | List<PathSegment> mowingSegments = segments.stream() |
| | | .filter(s -> s.isMowing) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 2. 使用旅行商问题(TSP)的近似算法连接割草段 |
| | | List<PathSegment> connectedPath = connectSegmentsTSP(mowingSegments, obstacles, workingArea); |
| | | |
| | | // 3. 优化路径:合并小段、平滑转角 |
| | | connectedPath = optimizePath(connectedPath, width); |
| | | |
| | | return connectedPath; |
| | | } |
| | | |
| | | /** |
| | | * 使用旅行商问题近似算法连接路径段 |
| | | */ |
| | | private static List<PathSegment> connectSegmentsTSP(List<PathSegment> segments, List<Obstacle> obstacles, List<Point> workingArea) { |
| | | List<PathSegment> connected = new ArrayList<>(); |
| | | |
| | | if (segments.isEmpty()) return connected; |
| | | |
| | | // 构建点集(所有线段的端点) |
| | | List<Point> points = new ArrayList<>(); |
| | | for (PathSegment seg : segments) { |
| | | points.add(seg.start); |
| | | points.add(seg.end); |
| | | } |
| | | |
| | | // 使用最近邻算法构建路径 |
| | | boolean[] visited = new boolean[segments.size()]; |
| | | Point currentPos = segments.get(0).start; |
| | | |
| | | while (true) { |
| | | int bestIdx = -1; |
| | | double bestDist = Double.MAX_VALUE; |
| | | boolean useStart = true; |
| | | |
| | | // 寻找最近的未访问线段 |
| | | for (int i = 0; i < segments.size(); i++) { |
| | | if (visited[i]) continue; |
| | | |
| | | PathSegment seg = segments.get(i); |
| | | double distToStart = distance(currentPos, seg.start); |
| | | double distToEnd = distance(currentPos, seg.end); |
| | | |
| | | if (distToStart < bestDist) { |
| | | bestDist = distToStart; |
| | | bestIdx = i; |
| | | useStart = true; |
| | | } |
| | | if (distToEnd < bestDist) { |
| | | bestDist = distToEnd; |
| | | bestIdx = i; |
| | | useStart = false; |
| | | } |
| | | } |
| | | |
| | | if (bestIdx == -1) break; |
| | | |
| | | // 添加连接路径 |
| | | PathSegment bestSeg = segments.get(bestIdx); |
| | | Point targetPoint = useStart ? bestSeg.start : bestSeg.end; |
| | | |
| | | if (distance(currentPos, targetPoint) > MIN_SEG_LEN) { |
| | | // 寻找安全连接路径(受作业边界限制) |
| | | List<PathSegment> detour = findSafePath(currentPos, targetPoint, obstacles, workingArea); |
| | | connected.addAll(detour); |
| | | } |
| | | |
| | | // 添加割草线段(可能反转方向) |
| | | PathSegment toAdd = bestSeg; |
| | | if (!useStart) { |
| | | toAdd = new PathSegment(bestSeg.end, bestSeg.start, true); |
| | | } |
| | | connected.add(toAdd); |
| | | |
| | | currentPos = toAdd.end; |
| | | visited[bestIdx] = true; |
| | | } |
| | | |
| | | return connected; |
| | | } |
| | | |
| | | /** |
| | | * 寻找安全路径(A*算法) |
| | | */ |
| | | private static List<PathSegment> findSafePath(Point start, Point end, List<Obstacle> obstacles, List<Point> workingArea) { |
| | | // 如果直线路径安全,直接使用 |
| | | if (isLineSafe(start, end, obstacles, workingArea)) { |
| | | List<PathSegment> direct = new ArrayList<>(); |
| | | direct.add(new PathSegment(start, end, false)); |
| | | return direct; |
| | | } |
| | | |
| | | // 否则使用A*算法寻找绕行路径 |
| | | return aStarPathFinding(start, end, obstacles, workingArea); |
| | | } |
| | | |
| | | /** |
| | | * A*算法路径寻找 |
| | | */ |
| | | private static List<PathSegment> aStarPathFinding(Point start, Point end, List<Obstacle> obstacles, List<Point> workingArea) { |
| | | // 简化的A*算法实现 |
| | | // 这里我们使用障碍物边界上的关键点作为路径节点 |
| | | |
| | | List<Point> nodes = new ArrayList<>(); |
| | | nodes.add(start); |
| | | nodes.add(end); |
| | | |
| | | // 添加障碍物的顶点作为候选节点 |
| | | for (Obstacle obs : obstacles) { |
| | | nodes.addAll(obs.getKeyPoints()); |
| | | } |
| | | // 添加作业边界顶点,允许贴边绕行 |
| | | if (workingArea != null && workingArea.size() >= 3) { |
| | | nodes.addAll(workingArea); |
| | | } |
| | | |
| | | // 构建图 |
| | | Map<Point, Map<Point, Double>> graph = new HashMap<>(); |
| | | for (Point p1 : nodes) { |
| | | graph.put(p1, new HashMap<>()); |
| | | for (Point p2 : nodes) { |
| | | if (p1 == p2) continue; |
| | | if (isLineSafe(p1, p2, obstacles, workingArea)) { |
| | | graph.get(p1).put(p2, distance(p1, p2)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // A*搜索 |
| | | Map<Point, Double> gScore = new HashMap<>(); |
| | | Map<Point, Double> fScore = new HashMap<>(); |
| | | Map<Point, Point> cameFrom = new HashMap<>(); |
| | | PriorityQueue<Point> openSet = new PriorityQueue<>( |
| | | Comparator.comparingDouble(p -> fScore.getOrDefault(p, Double.MAX_VALUE)) |
| | | ); |
| | | |
| | | gScore.put(start, 0.0); |
| | | fScore.put(start, heuristic(start, end)); |
| | | openSet.add(start); |
| | | |
| | | while (!openSet.isEmpty()) { |
| | | Point current = openSet.poll(); |
| | | |
| | | if (current.equals(end)) { |
| | | return reconstructPath(cameFrom, current); |
| | | } |
| | | |
| | | for (Map.Entry<Point, Double> neighborEntry : graph.getOrDefault(current, new HashMap<>()).entrySet()) { |
| | | Point neighbor = neighborEntry.getKey(); |
| | | double tentativeGScore = gScore.get(current) + neighborEntry.getValue(); |
| | | |
| | | if (tentativeGScore < gScore.getOrDefault(neighbor, Double.MAX_VALUE)) { |
| | | cameFrom.put(neighbor, current); |
| | | gScore.put(neighbor, tentativeGScore); |
| | | fScore.put(neighbor, tentativeGScore + heuristic(neighbor, end)); |
| | | |
| | | if (!openSet.contains(neighbor)) { |
| | | openSet.add(neighbor); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 如果没有找到路径,不做不安全的连接 |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | /** |
| | | * 重构路径 |
| | | */ |
| | | private static List<PathSegment> reconstructPath(Map<Point, Point> cameFrom, Point current) { |
| | | List<Point> pathPoints = new ArrayList<>(); |
| | | while (current != null) { |
| | | pathPoints.add(current); |
| | | current = cameFrom.get(current); |
| | | } |
| | | Collections.reverse(pathPoints); |
| | | |
| | | List<PathSegment> path = new ArrayList<>(); |
| | | for (int i = 0; i < pathPoints.size() - 1; i++) { |
| | | path.add(new PathSegment(pathPoints.get(i), pathPoints.get(i + 1), false)); |
| | | } |
| | | return path; |
| | | } |
| | | |
| | | /** |
| | | * 启发函数 |
| | | */ |
| | | private static double heuristic(Point a, Point b) { |
| | | return distance(a, b); |
| | | } |
| | | |
| | | /** |
| | | * 优化路径 |
| | | */ |
| | | private static List<PathSegment> optimizePath(List<PathSegment> path, double width) { |
| | | if (path.size() <= 1) return path; |
| | | |
| | | List<PathSegment> optimized = new ArrayList<>(); |
| | | PathSegment current = path.get(0); |
| | | |
| | | for (int i = 1; i < path.size(); i++) { |
| | | PathSegment next = path.get(i); |
| | | |
| | | // 检查是否可以合并当前线段和下一线段 |
| | | if (canMergeSegments(current, next, width)) { |
| | | // 合并线段 |
| | | current = mergeSegments(current, next); |
| | | } else { |
| | | // 添加当前线段,开始新的合并 |
| | | optimized.add(current); |
| | | current = next; |
| | | } |
| | | } |
| | | |
| | | optimized.add(current); |
| | | |
| | | // 平滑转角 |
| | | optimized = smoothCorners(optimized, width); |
| | | |
| | | return optimized; |
| | | } |
| | | |
| | | /** |
| | | * 检查是否可以合并两个线段 |
| | | */ |
| | | private static boolean canMergeSegments(PathSegment a, PathSegment b, double width) { |
| | | if (!a.isMowing || !b.isMowing) return false; |
| | | |
| | | // 检查端点是否重合 |
| | | if (!a.end.equals(b.start) && !a.end.equals(b.end)) return false; |
| | | |
| | | // 检查方向是否一致 |
| | | Point dir1 = new Point(a.end.x - a.start.x, a.end.y - a.start.y); |
| | | Point dir2; |
| | | if (a.end.equals(b.start)) { |
| | | dir2 = new Point(b.end.x - b.start.x, b.end.y - b.start.y); |
| | | } else { |
| | | dir2 = new Point(b.start.x - b.end.x, b.start.y - b.end.y); |
| | | } |
| | | |
| | | double angle = angleBetween(dir1, dir2); |
| | | return angle < Math.toRadians(10); // 角度小于10度可以合并 |
| | | } |
| | | |
| | | /** |
| | | * 合并两个线段 |
| | | */ |
| | | private static PathSegment mergeSegments(PathSegment a, PathSegment b) { |
| | | Point newEnd = a.end.equals(b.start) ? b.end : b.start; |
| | | return new PathSegment(a.start, newEnd, true); |
| | | } |
| | | |
| | | /** |
| | | * 平滑转角 |
| | | */ |
| | | private static List<PathSegment> smoothCorners(List<PathSegment> path, double width) { |
| | | if (path.size() < 3) return path; |
| | | |
| | | List<PathSegment> smoothed = new ArrayList<>(); |
| | | smoothed.add(path.get(0)); |
| | | |
| | | for (int i = 1; i < path.size() - 1; i++) { |
| | | PathSegment prev = path.get(i - 1); |
| | | PathSegment curr = path.get(i); |
| | | PathSegment next = path.get(i + 1); |
| | | |
| | | if (!prev.isMowing || !curr.isMowing || !next.isMowing) { |
| | | smoothed.add(curr); |
| | | // 2. 移除在障碍物内部的线段 |
| | | List<PathSegment> remainingSegments = new ArrayList<>(); |
| | | for (PathSegment seg : originalSegments) { |
| | | if (!seg.isMowing) { |
| | | // 空走段直接保留 |
| | | remainingSegments.add(seg); |
| | | continue; |
| | | } |
| | | |
| | | // 计算转角 |
| | | Point inVec = new Point(curr.start.x - prev.end.x, curr.start.y - prev.end.y); |
| | | Point outVec = new Point(next.start.x - curr.end.x, next.start.y - curr.end.y); |
| | | // 将割草段与所有障碍物进行裁剪 |
| | | List<PathSegment> clippedSegments = new ArrayList<>(); |
| | | clippedSegments.add(seg); |
| | | |
| | | double angle = angleBetween(inVec, outVec); |
| | | for (Obstacle obs : obstacles) { |
| | | List<PathSegment> newSegments = new ArrayList<>(); |
| | | for (PathSegment s : clippedSegments) { |
| | | newSegments.addAll(clipSegmentWithObstacle(s, obs)); |
| | | } |
| | | clippedSegments = newSegments; |
| | | } |
| | | |
| | | if (angle < CORNER_THRESHOLD) { |
| | | // 小角度,可以直接连接 |
| | | PathSegment direct = new PathSegment(prev.end, next.start, true); |
| | | smoothed.remove(smoothed.size() - 1); // 移除上一个线段 |
| | | smoothed.add(direct); |
| | | i++; // 跳过下一个线段 |
| | | remainingSegments.addAll(clippedSegments); |
| | | } |
| | | |
| | | // 3. 重新连接路径段(弓字形连接) |
| | | return reconnectSegments(remainingSegments); |
| | | } |
| | | |
| | | /** |
| | | * 将线段与障碍物进行裁剪 |
| | | * 返回不在障碍物内部的子线段 |
| | | */ |
| | | private static List<PathSegment> clipSegmentWithObstacle(PathSegment segment, Obstacle obstacle) { |
| | | List<PathSegment> result = new ArrayList<>(); |
| | | |
| | | // 检查线段是否完全在障碍物外部 |
| | | boolean startInside = obstacle.contains(segment.start); |
| | | boolean endInside = obstacle.contains(segment.end); |
| | | |
| | | if (!startInside && !endInside) { |
| | | // 线段两端都在外部,检查是否穿过障碍物 |
| | | List<Point> intersections = obstacle.getIntersections(segment); |
| | | if (intersections.isEmpty()) { |
| | | // 完全在外部 |
| | | result.add(segment); |
| | | } else { |
| | | smoothed.add(curr); |
| | | } |
| | | } |
| | | |
| | | if (path.size() > 1) { |
| | | smoothed.add(path.get(path.size() - 1)); |
| | | } |
| | | |
| | | return smoothed; |
| | | } |
| | | |
| | | // ==================== 几何计算工具 ==================== |
| | | |
| | | /** |
| | | * 多边形偏移算法 |
| | | */ |
| | | private static List<Point> offsetPolygon(List<Point> polygon, double d) { |
| | | // 基于“偏移边直线交点”的较稳健实现。约定polygon为CCW,左法向量为外侧。 |
| | | if (polygon == null || polygon.size() < 3) return new ArrayList<>(); |
| | | List<Point> poly = new ArrayList<>(polygon); |
| | | makeCCW(poly); |
| | | int n = poly.size(); |
| | | List<Point> out = new ArrayList<>(n); |
| | | |
| | | for (int i = 0; i < n; i++) { |
| | | Point A = poly.get((i - 1 + n) % n); |
| | | Point B = poly.get(i); |
| | | Point C = poly.get((i + 1) % n); |
| | | |
| | | Point e1 = normalize(subtract(B, A)); |
| | | Point e2 = normalize(subtract(C, B)); |
| | | Point n1 = new Point(-e1.y, e1.x); |
| | | Point n2 = new Point(-e2.y, e2.x); |
| | | |
| | | Point p1 = add(B, multiply(n1, d)); |
| | | Point p2 = add(B, multiply(n2, d)); |
| | | |
| | | Point dir1 = e1; |
| | | Point dir2 = e2; |
| | | |
| | | Point inter = intersectLines(p1, dir1, p2, dir2); |
| | | if (inter == null) { |
| | | // 平行或数值不稳定时退化 |
| | | Point avgN = add(n1, n2); |
| | | if (magnitude(avgN) < EPS) avgN = n1; |
| | | else avgN = normalize(avgN); |
| | | inter = add(B, multiply(avgN, d)); |
| | | } |
| | | out.add(inter); |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | // 计算两条参数直线的交点 p=p0+t*v, q=q0+s*w |
| | | private static Point intersectLines(Point p0, Point v, Point q0, Point w) { |
| | | double det = v.x * w.y - v.y * w.x; |
| | | if (Math.abs(det) < EPS) return null; |
| | | double t = ((q0.x - p0.x) * w.y - (q0.y - p0.y) * w.x) / det; |
| | | return new Point(p0.x + t * v.x, p0.y + t * v.y); |
| | | } |
| | | |
| | | /** |
| | | * 计算最优扫描角度 |
| | | */ |
| | | private static double calculateOptimalScanAngle(List<Point> polygon) { |
| | | double bestAngle = 0; |
| | | double minSpan = Double.MAX_VALUE; |
| | | |
| | | // 尝试多个角度 |
| | | for (int i = 0; i < 180; i += 5) { |
| | | double angle = Math.toRadians(i); |
| | | List<Point> rotated = rotatePolygon(polygon, angle); |
| | | |
| | | Bounds bounds = calculateBounds(rotated); |
| | | double span = bounds.maxY - bounds.minY; |
| | | |
| | | if (span < minSpan) { |
| | | minSpan = span; |
| | | bestAngle = angle; |
| | | } |
| | | } |
| | | |
| | | return bestAngle; |
| | | } |
| | | |
| | | /** |
| | | * 获取水平线与多边形的交点 |
| | | */ |
| | | private static List<Double> getHorizontalIntersections(List<Point> polygon, double y) { |
| | | List<Double> intersections = 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); |
| | | |
| | | // 检查边是否与水平线相交 |
| | | if ((p1.y <= y && p2.y >= y) || (p1.y >= y && p2.y <= y)) { |
| | | if (Math.abs(p2.y - p1.y) < EPS) { |
| | | // 水平边,跳过 |
| | | continue; |
| | | } |
| | | // 穿过障碍物,分割线段 |
| | | intersections.sort(Comparator.comparingDouble(p -> |
| | | distance(segment.start, p))); |
| | | |
| | | double t = (y - p1.y) / (p2.y - p1.y); |
| | | if (t >= -EPS && t <= 1 + EPS) { |
| | | double x = p1.x + t * (p2.x - p1.x); |
| | | intersections.add(x); |
| | | Point prevPoint = segment.start; |
| | | for (Point inter : intersections) { |
| | | result.add(new PathSegment(prevPoint, inter, true)); |
| | | prevPoint = inter; |
| | | } |
| | | result.add(new PathSegment(prevPoint, segment.end, true)); |
| | | |
| | | // 移除在障碍物内部的段(奇数索引的段) |
| | | List<PathSegment> filtered = new ArrayList<>(); |
| | | for (int i = 0; i < result.size(); i++) { |
| | | PathSegment s = result.get(i); |
| | | Point midPoint = new Point( |
| | | (s.start.x + s.end.x) / 2, |
| | | (s.start.y + s.end.y) / 2 |
| | | ); |
| | | if (!obstacle.contains(midPoint)) { |
| | | filtered.add(s); |
| | | } |
| | | } |
| | | return filtered; |
| | | } |
| | | } else if (startInside && endInside) { |
| | | // 完全在内部,丢弃 |
| | | return result; |
| | | } else { |
| | | // 一端在内部,一端在外部 |
| | | Point insidePoint = startInside ? segment.start : segment.end; |
| | | Point outsidePoint = startInside ? segment.end : segment.start; |
| | | |
| | | List<Point> intersections = obstacle.getIntersections(segment); |
| | | if (!intersections.isEmpty()) { |
| | | // 取离外部点最近的交点 |
| | | intersections.sort(Comparator.comparingDouble(p -> |
| | | distance(outsidePoint, p))); |
| | | Point inter = intersections.get(0); |
| | | |
| | | // 只保留外部部分 |
| | | if (startInside) { |
| | | result.add(new PathSegment(inter, outsidePoint, true)); |
| | | } else { |
| | | result.add(new PathSegment(outsidePoint, inter, true)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 去重并排序 |
| | | intersections = intersections.stream() |
| | | .distinct() |
| | | .sorted() |
| | | .collect(Collectors.toList()); |
| | | |
| | | return intersections; |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 判断直线是否安全 |
| | | * 重新连接路径段,形成连续弓字形路径 |
| | | */ |
| | | private static boolean isLineSafe(Point p1, Point p2, List<Obstacle> obstacles, List<Point> workingArea) { |
| | | // 必须完全在作业内缩边界内 |
| | | if (workingArea != null && !isSegmentInsidePolygon(p1, p2, workingArea)) { |
| | | return false; |
| | | } |
| | | for (Obstacle obs : obstacles) { |
| | | if (obs.doesSegmentIntersect(p1, p2)) { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | // 判断线段是否位于多边形内部(不越界) |
| | | private static boolean isSegmentInsidePolygon(Point a, Point b, List<Point> polygon) { |
| | | if (polygon == null || polygon.size() < 3) return true; |
| | | // 中点在内 |
| | | Point mid = new Point((a.x + b.x) / 2.0, (a.y + b.y) / 2.0); |
| | | if (!pointInPolygon(mid, polygon)) return false; |
| | | // 不与边界相交(允许端点接触) |
| | | int n = polygon.size(); |
| | | for (int i = 0; i < n; i++) { |
| | | Point p1 = polygon.get(i); |
| | | Point p2 = polygon.get((i + 1) % n); |
| | | if (lineSegmentIntersection(a, b, p1, p2)) { |
| | | // 忽略仅在端点处的小接触 |
| | | if (distance(a, p1) < EPS || distance(a, p2) < EPS || distance(b, p1) < EPS || distance(b, p2) < EPS) { |
| | | continue; |
| | | private static List<PathSegment> reconnectSegments(List<PathSegment> segments) { |
| | | if (segments.isEmpty()) return new ArrayList<>(); |
| | | |
| | | List<PathSegment> reconnected = new ArrayList<>(); |
| | | Point currentPos = segments.get(0).start; |
| | | |
| | | for (PathSegment seg : segments) { |
| | | if (seg.isMowing) { |
| | | // 割草段:检查是否需要添加空走段 |
| | | if (distance(currentPos, seg.start) > 0.01) { |
| | | reconnected.add(new PathSegment(currentPos, seg.start, false)); |
| | | } |
| | | return false; |
| | | reconnected.add(seg); |
| | | currentPos = seg.end; |
| | | } else { |
| | | // 空走段直接添加 |
| | | reconnected.add(seg); |
| | | currentPos = seg.end; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private static boolean pointInPolygon(Point p, List<Point> poly) { |
| | | boolean inside = false; |
| | | for (int i = 0, j = poly.size() - 1; i < poly.size(); j = i++) { |
| | | Point pi = poly.get(i), pj = poly.get(j); |
| | | boolean intersect = ((pi.y > p.y) != (pj.y > p.y)) && |
| | | (p.x < (pj.x - pi.x) * (p.y - pi.y) / (pj.y - pi.y + EPS) + pi.x); |
| | | if (intersect) inside = !inside; |
| | | } |
| | | return inside; |
| | | } |
| | | |
| | | // ==================== 向量运算工具 ==================== |
| | | |
| | | private static Point add(Point a, Point b) { |
| | | return new Point(a.x + b.x, a.y + b.y); |
| | | } |
| | | |
| | | private static Point subtract(Point a, Point b) { |
| | | return new Point(a.x - b.x, a.y - b.y); |
| | | } |
| | | |
| | | private static Point multiply(Point p, double scalar) { |
| | | return new Point(p.x * scalar, p.y * scalar); |
| | | } |
| | | |
| | | private static Point normalize(Point p) { |
| | | double mag = magnitude(p); |
| | | if (mag < EPS) return p; |
| | | return new Point(p.x / mag, p.y / mag); |
| | | } |
| | | |
| | | private static double magnitude(Point p) { |
| | | return Math.sqrt(p.x * p.x + p.y * p.y); |
| | | } |
| | | |
| | | private static double dot(Point a, Point b) { |
| | | return a.x * b.x + a.y * b.y; |
| | | } |
| | | |
| | | private static double angleBetween(Point a, Point b) { |
| | | double dotProd = dot(a, b); |
| | | double magA = magnitude(a); |
| | | double magB = magnitude(b); |
| | | |
| | | if (magA < EPS || magB < EPS) return 0; |
| | | return reconnected; |
| | | } |
| | | |
| | | /** |
| | | * 生成原始扫描路径(无障碍物版本) |
| | | */ |
| | | private static List<PathSegment> generateGlobalScanPath( |
| | | List<Point> polygon, double width, double angle, Point currentPos) { |
| | | |
| | | double cosAngle = dotProd / (magA * magB); |
| | | cosAngle = Math.max(-1, Math.min(1, cosAngle)); |
| | | return Math.acos(cosAngle); |
| | | } |
| | | |
| | | private static double distance(Point a, Point b) { |
| | | return magnitude(subtract(a, b)); |
| | | } |
| | | |
| | | 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> polygon, double angle) { |
| | | return polygon.stream() |
| | | .map(p -> rotatePoint(p, angle)) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | private static Bounds calculateBounds(List<Point> points) { |
| | | double minX = Double.MAX_VALUE, maxX = -Double.MAX_VALUE; |
| | | List<PathSegment> segments = new ArrayList<>(); |
| | | List<Point> rotatedPoly = new ArrayList<>(); |
| | | for (Point p : polygon) rotatedPoly.add(rotatePoint(p, -angle)); |
| | | |
| | | double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE; |
| | | |
| | | for (Point p : points) { |
| | | minX = Math.min(minX, p.x); |
| | | maxX = Math.max(maxX, p.x); |
| | | for (Point p : rotatedPoly) { |
| | | minY = Math.min(minY, p.y); |
| | | maxY = Math.max(maxY, p.y); |
| | | } |
| | | |
| | | return new Bounds(minX, maxX, minY, maxY); |
| | | } |
| | | |
| | | private static void makeCCW(List<Point> polygon) { |
| | | double area = 0; |
| | | int n = polygon.size(); |
| | | |
| | | for (int i = 0; i < n; i++) { |
| | | Point p1 = polygon.get(i); |
| | | Point p2 = polygon.get((i + 1) % n); |
| | | area += (p2.x - p1.x) * (p2.y + p1.y); |
| | | boolean leftToRight = true; |
| | | for (double y = minY + width/2; y <= maxY - width/2; y += width) { |
| | | List<Double> xIntersections = getXIntersections(rotatedPoly, y); |
| | | if (xIntersections.size() < 2) continue; |
| | | Collections.sort(xIntersections); |
| | | |
| | | List<PathSegment> lineSegmentsInRow = new ArrayList<>(); |
| | | for (int i = 0; i < xIntersections.size() - 1; i += 2) { |
| | | Point pS = rotatePoint(new Point(xIntersections.get(i), y), angle); |
| | | Point pE = rotatePoint(new Point(xIntersections.get(i + 1), y), angle); |
| | | lineSegmentsInRow.add(new PathSegment(pS, pE, true)); |
| | | } |
| | | |
| | | if (!leftToRight) { |
| | | Collections.reverse(lineSegmentsInRow); |
| | | for (PathSegment s : lineSegmentsInRow) { |
| | | Point temp = s.start; |
| | | s.start = s.end; |
| | | s.end = temp; |
| | | } |
| | | } |
| | | |
| | | for (PathSegment s : lineSegmentsInRow) { |
| | | if (distance(currentPos, s.start) > 0.01) { |
| | | segments.add(new PathSegment(currentPos, s.start, false)); |
| | | } |
| | | segments.add(s); |
| | | currentPos = s.end; |
| | | } |
| | | leftToRight = !leftToRight; |
| | | } |
| | | |
| | | if (area > 0) { |
| | | Collections.reverse(polygon); |
| | | } |
| | | return segments; |
| | | } |
| | | |
| | | // ==================== 障碍物处理 ==================== |
| | | |
| | | private static List<Obstacle> parseAndExpandObstacles(String obstaclesStr, double margin) { |
| | | /** |
| | | * 解析障碍物字符串 |
| | | * 格式:"(x1,y1;x2,y2)(x1,y1;x2,y2;x3,y3)" |
| | | */ |
| | | private static List<Obstacle> parseObstacles(String obstaclesStr) { |
| | | List<Obstacle> obstacles = new ArrayList<>(); |
| | | |
| | | if (obstaclesStr == null || obstaclesStr.trim().isEmpty()) { |
| | | return obstacles; |
| | | } |
| | | |
| | | // 解析障碍物字符串 |
| | | Pattern pattern = Pattern.compile("\\(([^)]+)\\)"); |
| | | Matcher matcher = pattern.matcher(obstaclesStr); |
| | | String trimmed = obstaclesStr.trim(); |
| | | List<String> obstacleStrs = new ArrayList<>(); |
| | | |
| | | while (matcher.find()) { |
| | | String coords = matcher.group(1); |
| | | List<Point> points = parseCoordinates(coords); |
| | | // 分割每个障碍物(用括号分隔) |
| | | int start = trimmed.indexOf('('); |
| | | while (start != -1) { |
| | | int end = trimmed.indexOf(')', start); |
| | | if (end == -1) break; |
| | | |
| | | String obsStr = trimmed.substring(start + 1, end); |
| | | obstacleStrs.add(obsStr); |
| | | start = trimmed.indexOf('(', end); |
| | | } |
| | | |
| | | // 解析每个障碍物 |
| | | for (String obsStr : obstacleStrs) { |
| | | List<Point> points = new ArrayList<>(); |
| | | String[] pairs = obsStr.split(";"); |
| | | |
| | | for (String pair : pairs) { |
| | | String[] xy = pair.split(","); |
| | | if (xy.length == 2) { |
| | | points.add(new Point( |
| | | Double.parseDouble(xy[0].trim()), |
| | | Double.parseDouble(xy[1].trim()) |
| | | )); |
| | | } |
| | | } |
| | | |
| | | if (points.size() == 2) { |
| | | // 圆形障碍物 |
| | | // 圆形障碍物:第一个点为圆心,第二个点为圆上一点 |
| | | Point center = points.get(0); |
| | | double radius = distance(center, points.get(1)) + margin; |
| | | obstacles.add(new CircularObstacle(center, radius)); |
| | | } else if (points.size() >= 3) { |
| | | Point onCircle = points.get(1); |
| | | double radius = distance(center, onCircle); |
| | | obstacles.add(new Obstacle(center, radius)); |
| | | } else if (points.size() > 2) { |
| | | // 多边形障碍物 |
| | | makeCCW(points); |
| | | List<Point> expanded = offsetPolygon(points, -margin); |
| | | obstacles.add(new PolygonalObstacle(expanded)); |
| | | obstacles.add(new Obstacle(points)); |
| | | } |
| | | } |
| | | |
| | | return obstacles; |
| | | } |
| | | |
| | | private static List<Point> parseCoordinates(String str) { |
| | | List<Point> points = new ArrayList<>(); |
| | | /** |
| | | * 外扩障碍物(增加安全边距) |
| | | */ |
| | | private static List<Obstacle> expandObstacles(List<Obstacle> obstacles, double margin) { |
| | | List<Obstacle> expanded = new ArrayList<>(); |
| | | |
| | | if (str == null || str.trim().isEmpty()) { |
| | | return points; |
| | | } |
| | | |
| | | String[] tokens = str.split(";"); |
| | | for (String token : tokens) { |
| | | token = token.trim(); |
| | | if (token.isEmpty()) continue; |
| | | |
| | | String[] xy = token.split(","); |
| | | if (xy.length == 2) { |
| | | try { |
| | | double x = Double.parseDouble(xy[0].trim()); |
| | | double y = Double.parseDouble(xy[1].trim()); |
| | | points.add(new Point(x, y)); |
| | | } catch (NumberFormatException e) { |
| | | System.err.println("无效坐标: " + token); |
| | | } |
| | | for (Obstacle obs : obstacles) { |
| | | if (obs.isCircle()) { |
| | | // 圆形:半径增加安全边距 |
| | | expanded.add(new Obstacle(obs.center, obs.radius + margin)); |
| | | } else { |
| | | // 多边形:向外偏移(与边界内缩方向相反) |
| | | List<Point> expandedPoints = getOutsetPolygon(obs.points, margin); |
| | | expanded.add(new Obstacle(expandedPoints)); |
| | | } |
| | | } |
| | | |
| | | return points; |
| | | } |
| | | |
| | | // ==================== 内部类定义 ==================== |
| | | |
| | | /** |
| | | * 障碍物基类 |
| | | */ |
| | | abstract static class Obstacle { |
| | | abstract List<PathSegment> clipSegment(PathSegment seg); |
| | | abstract boolean doesSegmentIntersect(Point p1, Point p2); |
| | | abstract boolean containsPoint(Point p); |
| | | abstract List<Point> getKeyPoints(); |
| | | return expanded; |
| | | } |
| | | |
| | | /** |
| | | * 多边形障碍物 |
| | | * 多边形外扩(与内缩方向相反) |
| | | */ |
| | | static class PolygonalObstacle extends Obstacle { |
| | | List<Point> vertices; |
| | | private static List<Point> getOutsetPolygon(List<Point> points, double margin) { |
| | | // 这里使用简化的外扩方法:沿法线向外移动 |
| | | List<Point> outset = new ArrayList<>(); |
| | | int n = points.size(); |
| | | |
| | | PolygonalObstacle(List<Point> vertices) { |
| | | this.vertices = vertices; |
| | | } |
| | | |
| | | @Override |
| | | List<PathSegment> clipSegment(PathSegment seg) { |
| | | List<Double> tValues = new ArrayList<>(); |
| | | tValues.add(0.0); |
| | | tValues.add(1.0); |
| | | for (int i = 0; i < n; i++) { |
| | | Point pPrev = points.get((i - 1 + n) % n); |
| | | Point pCurr = points.get(i); |
| | | Point pNext = points.get((i + 1) % n); |
| | | |
| | | // 收集所有交点 |
| | | for (int i = 0; i < vertices.size(); i++) { |
| | | Point p1 = vertices.get(i); |
| | | Point p2 = vertices.get((i + 1) % vertices.size()); |
| | | |
| | | Double t = lineIntersection(seg.start, seg.end, p1, p2); |
| | | if (t != null) { |
| | | tValues.add(t); |
| | | } |
| | | // 计算两个边的向量 |
| | | double v1x = pCurr.x - pPrev.x, v1y = pCurr.y - pPrev.y; |
| | | double v2x = pNext.x - pCurr.x, v2y = pNext.y - pCurr.y; |
| | | |
| | | // 计算法线(确保向外) |
| | | double nx1 = -v1y, ny1 = v1x; |
| | | double nx2 = -v2y, ny2 = v2x; |
| | | |
| | | // 归一化 |
| | | double len1 = Math.hypot(nx1, ny1); |
| | | double len2 = Math.hypot(nx2, ny2); |
| | | if (len1 > 1e-6) { nx1 /= len1; ny1 /= len1; } |
| | | if (len2 > 1e-6) { nx2 /= len2; ny2 /= len2; } |
| | | |
| | | // 计算平均法线方向 |
| | | double nx = (nx1 + nx2) / 2; |
| | | double ny = (ny1 + ny2) / 2; |
| | | double len = Math.hypot(nx, ny); |
| | | if (len > 1e-6) { |
| | | nx /= len; |
| | | ny /= len; |
| | | } |
| | | |
| | | Collections.sort(tValues); |
| | | List<PathSegment> result = new ArrayList<>(); |
| | | |
| | | // 生成不在障碍物内部的线段段 |
| | | for (int i = 0; i < tValues.size() - 1; i++) { |
| | | double t1 = tValues.get(i); |
| | | double t2 = tValues.get(i + 1); |
| | | double tMid = (t1 + t2) / 2; |
| | | |
| | | Point midPoint = interpolate(seg.start, seg.end, tMid); |
| | | if (!containsPoint(midPoint)) { |
| | | Point start = interpolate(seg.start, seg.end, t1); |
| | | Point end = interpolate(seg.start, seg.end, t2); |
| | | result.add(new PathSegment(start, end, seg.isMowing)); |
| | | } |
| | | } |
| | | |
| | | return result; |
| | | // 向外移动 |
| | | outset.add(new Point( |
| | | pCurr.x + nx * margin, |
| | | pCurr.y + ny * margin |
| | | )); |
| | | } |
| | | |
| | | @Override |
| | | boolean doesSegmentIntersect(Point p1, Point p2) { |
| | | for (int i = 0; i < vertices.size(); i++) { |
| | | Point v1 = vertices.get(i); |
| | | Point v2 = vertices.get((i + 1) % vertices.size()); |
| | | |
| | | if (lineSegmentIntersection(p1, p2, v1, v2)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | boolean containsPoint(Point p) { |
| | | int crossings = 0; |
| | | |
| | | for (int i = 0; i < vertices.size(); i++) { |
| | | Point v1 = vertices.get(i); |
| | | Point v2 = vertices.get((i + 1) % vertices.size()); |
| | | |
| | | if (((v1.y <= p.y && p.y < v2.y) || (v2.y <= p.y && p.y < v1.y)) && |
| | | (p.x < (v2.x - v1.x) * (p.y - v1.y) / (v2.y - v1.y) + v1.x)) { |
| | | crossings++; |
| | | } |
| | | } |
| | | |
| | | return (crossings % 2) == 1; |
| | | } |
| | | |
| | | @Override |
| | | List<Point> getKeyPoints() { |
| | | return new ArrayList<>(vertices); |
| | | } |
| | | return outset; |
| | | } |
| | | |
| | | /** |
| | | * 圆形障碍物 |
| | | * 障碍物类 |
| | | */ |
| | | static class CircularObstacle extends Obstacle { |
| | | Point center; |
| | | double radius; |
| | | private static class Obstacle { |
| | | List<Point> points; // 多边形顶点(对圆形为空) |
| | | Point center; // 圆心(仅对圆形有效) |
| | | double radius; // 半径(仅对圆形有效) |
| | | boolean isCircle; |
| | | |
| | | CircularObstacle(Point center, double radius) { |
| | | this.center = center; |
| | | // 多边形构造函数 |
| | | Obstacle(List<Point> points) { |
| | | this.points = new ArrayList<>(points); |
| | | this.isCircle = false; |
| | | ensureCounterClockwise(this.points); // 确保顺时针(对障碍物是内部区域) |
| | | } |
| | | |
| | | // 圆形构造函数 |
| | | Obstacle(Point center, double radius) { |
| | | this.center = new Point(center.x, center.y); |
| | | this.radius = radius; |
| | | this.isCircle = true; |
| | | this.points = new ArrayList<>(); |
| | | } |
| | | |
| | | @Override |
| | | List<PathSegment> clipSegment(PathSegment seg) { |
| | | double dx = seg.end.x - seg.start.x; |
| | | double dy = seg.end.y - seg.start.y; |
| | | double fx = seg.start.x - center.x; |
| | | double fy = seg.start.y - center.y; |
| | | |
| | | double a = dx * dx + dy * dy; |
| | | double b = 2 * (fx * dx + fy * dy); |
| | | double c = fx * fx + fy * fy - radius * radius; |
| | | |
| | | List<Double> tValues = new ArrayList<>(); |
| | | tValues.add(0.0); |
| | | tValues.add(1.0); |
| | | |
| | | double discriminant = b * b - 4 * a * c; |
| | | if (discriminant > 0) { |
| | | double sqrtDisc = Math.sqrt(discriminant); |
| | | double t1 = (-b - sqrtDisc) / (2 * a); |
| | | double t2 = (-b + sqrtDisc) / (2 * a); |
| | | |
| | | if (t1 > EPS && t1 < 1 - EPS) tValues.add(t1); |
| | | if (t2 > EPS && t2 < 1 - EPS) tValues.add(t2); |
| | | // 判断点是否在障碍物内部 |
| | | boolean contains(Point p) { |
| | | if (isCircle) { |
| | | return distance(p, center) <= radius; |
| | | } else { |
| | | return isPointInPolygon(p, points); |
| | | } |
| | | } |
| | | |
| | | // 获取线段与障碍物的交点 |
| | | List<Point> getIntersections(PathSegment segment) { |
| | | List<Point> intersections = new ArrayList<>(); |
| | | |
| | | Collections.sort(tValues); |
| | | List<PathSegment> result = new ArrayList<>(); |
| | | |
| | | for (int i = 0; i < tValues.size() - 1; i++) { |
| | | double t1 = tValues.get(i); |
| | | double t2 = tValues.get(i + 1); |
| | | double tMid = (t1 + t2) / 2; |
| | | if (isCircle) { |
| | | // 线段与圆的交点 |
| | | double dx = segment.end.x - segment.start.x; |
| | | double dy = segment.end.y - segment.start.y; |
| | | double a = dx * dx + dy * dy; |
| | | double b = 2 * (dx * (segment.start.x - center.x) + |
| | | dy * (segment.start.y - center.y)); |
| | | double c = (segment.start.x - center.x) * (segment.start.x - center.x) + |
| | | (segment.start.y - center.y) * (segment.start.y - center.y) - |
| | | radius * radius; |
| | | |
| | | Point midPoint = interpolate(seg.start, seg.end, tMid); |
| | | if (!containsPoint(midPoint)) { |
| | | Point start = interpolate(seg.start, seg.end, t1); |
| | | Point end = interpolate(seg.start, seg.end, t2); |
| | | result.add(new PathSegment(start, end, seg.isMowing)); |
| | | double discriminant = b * b - 4 * a * c; |
| | | if (discriminant >= 0) { |
| | | discriminant = Math.sqrt(discriminant); |
| | | for (int sign = -1; sign <= 1; sign += 2) { |
| | | double t = (-b + sign * discriminant) / (2 * a); |
| | | if (t >= 0 && t <= 1) { |
| | | intersections.add(new Point( |
| | | segment.start.x + t * dx, |
| | | segment.start.y + t * dy |
| | | )); |
| | | } |
| | | } |
| | | } |
| | | } else { |
| | | // 线段与多边形的交点 |
| | | for (int i = 0; i < points.size(); i++) { |
| | | Point p1 = points.get(i); |
| | | Point p2 = points.get((i + 1) % points.size()); |
| | | |
| | | Point inter = getLineIntersection( |
| | | segment.start, segment.end, p1, p2); |
| | | if (inter != null) { |
| | | intersections.add(inter); |
| | | } |
| | | } |
| | | } |
| | | |
| | | return result; |
| | | return intersections; |
| | | } |
| | | |
| | | @Override |
| | | boolean doesSegmentIntersect(Point p1, Point p2) { |
| | | Point closest = closestPointOnSegment(center, p1, p2); |
| | | // 将与圆的相切也视为相交,避免路径擦边 |
| | | return distance(center, closest) <= radius + EPS; |
| | | boolean isCircle() { |
| | | return isCircle; |
| | | } |
| | | |
| | | @Override |
| | | boolean containsPoint(Point p) { |
| | | return distance(center, p) < radius - EPS; |
| | | } |
| | | |
| | | @Override |
| | | List<Point> getKeyPoints() { |
| | | List<Point> points = new ArrayList<>(); |
| | | int numPoints = 8; // 八边形近似 |
| | | } |
| | | |
| | | /** |
| | | * 判断点是否在多边形内部(射线法) |
| | | */ |
| | | private static boolean isPointInPolygon(Point p, List<Point> polygon) { |
| | | boolean inside = false; |
| | | for (int i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { |
| | | Point pi = polygon.get(i); |
| | | Point pj = polygon.get(j); |
| | | |
| | | for (int i = 0; i < numPoints; i++) { |
| | | double angle = 2 * Math.PI * i / numPoints; |
| | | points.add(new Point( |
| | | center.x + radius * Math.cos(angle), |
| | | center.y + radius * Math.sin(angle) |
| | | )); |
| | | if (((pi.y > p.y) != (pj.y > p.y)) && |
| | | (p.x < (pj.x - pi.x) * (p.y - pi.y) / (pj.y - pi.y) + pi.x)) { |
| | | inside = !inside; |
| | | } |
| | | |
| | | return points; |
| | | } |
| | | return inside; |
| | | } |
| | | |
| | | /** |
| | | * 路径段 |
| | | * 计算两条线段的交点 |
| | | */ |
| | | public static class PathSegment { |
| | | public Point start, end; |
| | | public boolean isMowing; |
| | | private static Point getLineIntersection(Point p1, Point p2, Point p3, Point p4) { |
| | | double denom = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x); |
| | | if (Math.abs(denom) < 1e-6) return null; // 平行 |
| | | |
| | | public PathSegment(Point start, Point end, boolean isMowing) { |
| | | this.start = start; |
| | | this.end = end; |
| | | this.isMowing = isMowing; |
| | | double t = ((p1.x - p3.x) * (p3.y - p4.y) - (p1.y - p3.y) * (p3.x - p4.x)) / denom; |
| | | double u = -((p1.x - p2.x) * (p1.y - p3.y) - (p1.y - p2.y) * (p1.x - p3.x)) / denom; |
| | | |
| | | if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { |
| | | return new Point( |
| | | p1.x + t * (p2.x - p1.x), |
| | | p1.y + t * (p2.y - p1.y) |
| | | ); |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return String.format("%s -> %s [%s]", start, end, isMowing ? "MOW" : "MOVE"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 点类 |
| | | */ |
| | | public static class Point { |
| | | public double x, y; |
| | | |
| | | public Point(double x, double y) { |
| | | this.x = x; |
| | | this.y = y; |
| | | } |
| | | |
| | | @Override |
| | | public boolean equals(Object obj) { |
| | | if (this == obj) return true; |
| | | if (!(obj instanceof Point)) return false; |
| | | Point other = (Point) obj; |
| | | return Math.abs(x - other.x) < EPS && Math.abs(y - other.y) < EPS; |
| | | } |
| | | |
| | | @Override |
| | | public int hashCode() { |
| | | return Double.hashCode(x) * 31 + Double.hashCode(y); |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return String.format("(%.2f, %.2f)", x, y); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 边界框 |
| | | */ |
| | | private static class Bounds { |
| | | double minX, maxX, minY, maxY; |
| | | |
| | | Bounds(double minX, double maxX, double minY, double maxY) { |
| | | this.minX = minX; |
| | | this.maxX = maxX; |
| | | this.minY = minY; |
| | | this.maxY = maxY; |
| | | } |
| | | } |
| | | |
| | | // ==================== 几何工具函数 ==================== |
| | | |
| | | private static Double lineIntersection(Point a1, Point a2, Point b1, Point b2) { |
| | | double det = (a2.x - a1.x) * (b2.y - b1.y) - (a2.y - a1.y) * (b2.x - b1.x); |
| | | |
| | | if (Math.abs(det) < EPS) return null; |
| | | |
| | | double t = ((b1.x - a1.x) * (b2.y - b1.y) - (b1.y - a1.y) * (b2.x - b1.x)) / det; |
| | | double u = ((a1.x - b1.x) * (a2.y - a1.y) - (a1.y - b1.y) * (a2.x - a1.x)) / (-det); |
| | | |
| | | if (t >= -EPS && t <= 1 + EPS && u >= -EPS && u <= 1 + EPS) { |
| | | return Math.max(0, Math.min(1, t)); |
| | | } |
| | | |
| | | return null; |
| | | } |
| | | |
| | | private static boolean lineSegmentIntersection(Point a1, Point a2, Point b1, Point b2) { |
| | | Double t = lineIntersection(a1, a2, b1, b2); |
| | | return t != null; |
| | | /** |
| | | * 计算两点距离 |
| | | */ |
| | | private static double distance(Point p1, Point p2) { |
| | | return Math.hypot(p1.x - p2.x, p1.y - p2.y); |
| | | } |
| | | |
| | | private static Point interpolate(Point a, Point b, double t) { |
| | | return new Point(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); |
| | | // ============ 以下是从A代码复用的方法 ============ |
| | | |
| | | private static Point getFirstScanPoint(List<Point> polygon, double width, double angle) { |
| | | List<Point> rotatedPoly = new ArrayList<>(); |
| | | for (Point p : polygon) rotatedPoly.add(rotatePoint(p, -angle)); |
| | | double minY = Double.MAX_VALUE; |
| | | for (Point p : rotatedPoly) minY = Math.min(minY, p.y); |
| | | |
| | | double firstY = minY + width/2; |
| | | List<Double> xInter = getXIntersections(rotatedPoly, firstY); |
| | | if (xInter.isEmpty()) return polygon.get(0); |
| | | Collections.sort(xInter); |
| | | return rotatePoint(new Point(xInter.get(0), firstY), angle); |
| | | } |
| | | |
| | | private static Point closestPointOnSegment(Point p, Point a, Point b) { |
| | | double ax = b.x - a.x; |
| | | double ay = b.y - a.y; |
| | | double bx = p.x - a.x; |
| | | double by = p.y - a.y; |
| | | |
| | | double dot = ax * bx + ay * by; |
| | | double lenSq = ax * ax + ay * ay; |
| | | |
| | | double t = (lenSq > EPS) ? Math.max(0, Math.min(1, dot / lenSq)) : 0; |
| | | |
| | | return new Point(a.x + t * ax, a.y + t * ay); |
| | | } |
| | | |
| | | } |
| | | private static List<Point> alignBoundaryStart(List<Point> boundary, Point targetStart) { |
| | | int bestIdx = 0; |
| | | double minDist = Double.MAX_VALUE; |
| | | for (int i = 0; i < boundary.size(); i++) { |
| | | double d = Math.hypot(boundary.get(i).x - targetStart.x, boundary.get(i).y - targetStart.y); |
| | | if (d < minDist) { minDist = d; bestIdx = i; } |
| | | } |
| | | List<Point> aligned = new ArrayList<>(); |
| | | for (int i = 0; i < boundary.size(); i++) { |
| | | aligned.add(boundary.get((bestIdx + i) % boundary.size())); |
| | | } |
| | | return aligned; |
| | | } |
| | | |
| | | private static List<Double> getXIntersections(List<Point> rotatedPoly, double y) { |
| | | List<Double> xIntersections = new ArrayList<>(); |
| | | for (int i = 0; i < rotatedPoly.size(); i++) { |
| | | Point p1 = rotatedPoly.get(i); |
| | | Point p2 = rotatedPoly.get((i + 1) % rotatedPoly.size()); |
| | | if ((p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y)) { |
| | | double x = p1.x + (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y); |
| | | xIntersections.add(x); |
| | | } |
| | | } |
| | | return xIntersections; |
| | | } |
| | | |
| | | private static double findOptimalAngle(List<Point> polygon) { |
| | | double bestAngle = 0; |
| | | double minHeight = Double.MAX_VALUE; |
| | | for (int i = 0; i < polygon.size(); i++) { |
| | | Point p1 = polygon.get(i), p2 = polygon.get((i + 1) % polygon.size()); |
| | | double angle = Math.atan2(p2.y - p1.y, p2.x - p1.x); |
| | | double h = calculateHeightAtAngle(polygon, angle); |
| | | if (h < minHeight) { minHeight = h; bestAngle = angle; } |
| | | } |
| | | return bestAngle; |
| | | } |
| | | |
| | | private static double calculateHeightAtAngle(List<Point> poly, double angle) { |
| | | double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE; |
| | | for (Point p : poly) { |
| | | Point rp = rotatePoint(p, -angle); |
| | | minY = Math.min(minY, rp.y); maxY = Math.max(maxY, rp.y); |
| | | } |
| | | return maxY - minY; |
| | | } |
| | | |
| | | private static List<Point> getInsetPolygon(List<Point> points, double margin) { |
| | | List<Point> result = new ArrayList<>(); |
| | | int n = points.size(); |
| | | for (int i = 0; i < n; i++) { |
| | | Point pPrev = points.get((i - 1 + n) % n); |
| | | Point pCurr = points.get(i); |
| | | Point pNext = points.get((i + 1) % n); |
| | | |
| | | double d1x = pCurr.x - pPrev.x, d1y = pCurr.y - pPrev.y; |
| | | double l1 = Math.hypot(d1x, d1y); |
| | | double d2x = pNext.x - pCurr.x, d2y = pNext.y - pCurr.y; |
| | | double l2 = Math.hypot(d2x, d2y); |
| | | |
| | | if (l1 < 1e-6 || l2 < 1e-6) continue; |
| | | |
| | | double n1x = -d1y / l1, n1y = d1x / l1; |
| | | double n2x = -d2y / l2, n2y = d2x / l2; |
| | | |
| | | double bisectorX = n1x + n2x, bisectorY = n1y + n2y; |
| | | double bLen = Math.hypot(bisectorX, bisectorY); |
| | | if (bLen < 1e-6) { bisectorX = n1x; bisectorY = n1y; } |
| | | else { bisectorX /= bLen; bisectorY /= bLen; } |
| | | |
| | | double cosHalfAngle = n1x * bisectorX + n1y * bisectorY; |
| | | double dist = margin / Math.max(cosHalfAngle, 0.1); |
| | | |
| | | dist = Math.min(dist, margin * 5); |
| | | |
| | | result.add(new Point(pCurr.x + bisectorX * dist, pCurr.y + bisectorY * dist)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private static Point rotatePoint(Point p, double angle) { |
| | | double cos = Math.cos(angle), sin = Math.sin(angle); |
| | | return new Point(p.x * cos - p.y * sin, p.x * sin + p.y * cos); |
| | | } |
| | | |
| | | private static void ensureCounterClockwise(List<Point> points) { |
| | | double sum = 0; |
| | | for (int i = 0; i < points.size(); i++) { |
| | | Point p1 = points.get(i), p2 = points.get((i + 1) % points.size()); |
| | | sum += (p2.x - p1.x) * (p2.y + p1.y); |
| | | } |
| | | if (sum > 0) Collections.reverse(points); |
| | | } |
| | | |
| | | private static List<Point> parseCoordinates(String coordinates) { |
| | | List<Point> points = new ArrayList<>(); |
| | | String[] pairs = coordinates.split(";"); |
| | | for (String pair : pairs) { |
| | | String[] xy = pair.split(","); |
| | | if (xy.length == 2) points.add(new Point(Double.parseDouble(xy[0]), Double.parseDouble(xy[1]))); |
| | | } |
| | | if (points.size() > 1 && points.get(0).equals(points.get(points.size()-1))) points.remove(points.size()-1); |
| | | return points; |
| | | } |
| | | |
| | | public static class Point { |
| | | public double x, y; |
| | | public Point(double x, double y) { this.x = x; this.y = y; } |
| | | @Override |
| | | public boolean equals(Object o) { |
| | | if (!(o instanceof Point)) return false; |
| | | Point p = (Point) o; |
| | | return Math.abs(x - p.x) < 1e-4 && Math.abs(y - p.y) < 1e-4; |
| | | } |
| | | } |
| | | |
| | | public static class PathSegment { |
| | | public Point start, end; |
| | | public boolean isMowing; |
| | | public PathSegment(Point s, Point e, boolean m) { |
| | | this.start = s; |
| | | this.end = e; |
| | | this.isMowing = m; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | * 修复:解决凹多边形扫描线跨越边界的问题,优化路径对齐 |
| | | */ |
| | | public class YixinglujingNoObstacle { |
| | | |
| | | // 用法说明(无障碍物路径规划): |
| | | // - 方法用途:根据地块边界、割草宽度与安全边距,生成覆盖全区域的割草路径。 |
| | | // - 参数: |
| | | // coordinates:地块边界坐标字符串,格式 "x1,y1;x2,y2;...",至少3个点,单位为米。 |
| | | // widthStr:割草宽度(字符串,单位米),用于确定扫描线间距。 |
| | | // marginStr:安全边距(字符串,单位米),用于将地块边界向内收缩,避免贴边作业。 |
| | | // - 返回值:List<PathSegment>,其中 PathSegment.start/end 为坐标点,isMowing 为 true 表示割草段,false 表示空走段。 |
| | | // - 失败情况:当边界点不足或内缩后区域过小,返回空列表。 |
| | | // - 使用示例: |
| | | // String boundary = "0,0;20,0;20,15;0,15"; |
| | | // String width = "0.3"; |
| | | // String margin = "0.5"; |
| | | // List<YixinglujingNoObstacle.PathSegment> path = |
| | | // YixinglujingNoObstacle.planPath(boundary, width, margin); |
| | | public static List<PathSegment> planPath(String coordinates, String widthStr, String marginStr) { |
| | | List<Point> rawPoints = parseCoordinates(coordinates); |
| | | if (rawPoints.size() < 3) return new ArrayList<>(); |
| | |
| | | import java.util.Map; |
| | | import java.util.Locale; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.function.Consumer; |
| | | // import java.util.function.Consumer; |
| | | import chuankou.DataListener; |
| | | import java.awt.geom.Point2D; |
| | | |
| | | import publicway.Gpstoxuzuobiao; |
| | |
| | | |
| | | private boolean pathPreviewActive; |
| | | |
| | | private final Consumer<String> serialLineListener = line -> { |
| | | SwingUtilities.invokeLater(() -> { |
| | | updateDataPacketCountLabel(); |
| | | // 如果收到GGA数据,立即更新拖尾 |
| | | if (line != null) { |
| | | String trimmed = line.trim(); |
| | | if (trimmed.startsWith("$GNGGA") || trimmed.startsWith("$GPGGA") || trimmed.startsWith("$GBGGA")) { |
| | | if (mapRenderer != null && !pathPreviewActive) { |
| | | mapRenderer.forceUpdateIdleMowerTrail(); |
| | | private final DataListener<String> serialLineListener = new DataListener<String>() { |
| | | @Override |
| | | public void accept(final String line) { |
| | | SwingUtilities.invokeLater(new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | updateDataPacketCountLabel(); |
| | | // 如果收到GGA数据,立即更新拖尾 |
| | | if (line != null) { |
| | | String trimmed = line.trim(); |
| | | if (trimmed.startsWith("$GNGGA") || trimmed.startsWith("$GPGGA") || trimmed.startsWith("$GBGGA")) { |
| | | if (mapRenderer != null && !pathPreviewActive) { |
| | | mapRenderer.forceUpdateIdleMowerTrail(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | }); |
| | | } |
| | | }; |
| | | private static final int FLOAT_ICON_SIZE = 32; |
| | | private JButton endDrawingButton; |
| | |
| | | scheduleIdentifierCheck(); |
| | | } |
| | | |
| | | private static boolean isFinite(double d) { |
| | | return !Double.isNaN(d) && !Double.isInfinite(d); |
| | | } |
| | | |
| | | public static Shouye getInstance() { |
| | | return instance; |
| | | } |
| | |
| | | |
| | | double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection()); |
| | | double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection()); |
| | | if (!Double.isFinite(lat) || !Double.isFinite(lon)) { |
| | | if (!isFinite(lat) || !isFinite(lon)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | |
| | | |
| | | double[] local = convertLatLonToLocal(lat, lon, base[0], base[1]); |
| | | Point2D.Double candidate = new Point2D.Double(local[0], local[1]); |
| | | if (!Double.isFinite(candidate.x) || !Double.isFinite(candidate.y)) { |
| | | if (!isFinite(candidate.x) || !isFinite(candidate.y)) { |
| | | discardLatestCoordinate(latest); |
| | | lastMowerCoordinate = latest; |
| | | return; |
| | |
| | | |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | if (!isFinite(x) || !isFinite(y)) { |
| | | JOptionPane.showMessageDialog(this, "当前定位数据无效,请稍后再试。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return -1; |
| | | } |
| | |
| | | } |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | if (!Double.isFinite(x) || !Double.isFinite(y)) { |
| | | if (!isFinite(x) || !isFinite(y)) { |
| | | return false; |
| | | } |
| | | return isDuplicateHandheldPoint(x, y); |
| | |
| | | } |
| | | double x = parseMetersValue(device.getRealtimeX()); |
| | | double y = parseMetersValue(device.getRealtimeY()); |
| | | return Double.isFinite(x) && Double.isFinite(y); |
| | | return isFinite(x) && isFinite(y); |
| | | } |
| | | |
| | | private boolean isDuplicateHandheldPoint(double x, double y) { |
| | |
| | | |
| | | double lat = parseDMToDecimal(latest.getLatitude(), latest.getLatDirection()); |
| | | double lon = parseDMToDecimal(latest.getLongitude(), latest.getLonDirection()); |
| | | if (!Double.isFinite(lat) || !Double.isFinite(lon)) { |
| | | if (!isFinite(lat) || !isFinite(lon)) { |
| | | JOptionPane.showMessageDialog(this, "采集点坐标无效,请重新采集。", "提示", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | |
| | | } |
| | | double baseLat = parseDMToDecimal(parts[0], parts[1]); |
| | | double baseLon = parseDMToDecimal(parts[2], parts[3]); |
| | | if (!Double.isFinite(baseLat) || !Double.isFinite(baseLon)) { |
| | | if (!isFinite(baseLat) || !isFinite(baseLon)) { |
| | | return null; |
| | | } |
| | | return new double[]{baseLat, baseLon}; |
| | |
| | | double centerX = (x1Sq * (y2 - y3) + x2Sq * (y3 - y1) + x3Sq * (y1 - y2)) / d; |
| | | double centerY = (x1Sq * (x3 - x2) + x2Sq * (x1 - x3) + x3Sq * (x2 - x1)) / d; |
| | | double radius = Math.hypot(centerX - x1, centerY - y1); |
| | | if (!Double.isFinite(centerX) || !Double.isFinite(centerY) || !Double.isFinite(radius)) { |
| | | if (!isFinite(centerX) || !isFinite(centerY) || !isFinite(radius)) { |
| | | return null; |
| | | } |
| | | if (radius < 0.05) { |