| | |
| | | # å²èæºå°åéç¢ç©é
ç½®æä»¶ |
| | | # çææ¶é´ï¼2025-12-17T19:15:02.630670200 |
| | | # çææ¶é´ï¼2025-12-18T19:44:44.126173900 |
| | | # åæ ç³»ï¼WGS84ï¼åº¦åæ ¼å¼ï¼ |
| | | |
| | | # ============ å°ååºåç«é
ç½® ============ |
| | | # æ ¼å¼ï¼plot.[å°åç¼å·].baseStation=[ç»åº¦],[N/S],[纬度],[E/W] |
| | | plot.DK-001.baseStation=3949.902389,N,11616.756920,E |
| | | plot.LAND1.baseStation=3949.902389,N,11616.756920,E |
| | | plot.LAND1.baseStation=3949.891518,N,11616.792675,E |
| | | |
| | | # ============ éç¢ç©é
ç½® ============ |
| | | # æ ¼å¼ï¼plot.[å°åç¼å·].obstacle.[éç¢ç©åç§°].shape=[0|1] |
| | | # æ ¼å¼ï¼plot.[å°åç¼å·].obstacle.[éç¢ç©åç§°].originalCoords=[åæ ä¸²] |
| | | # æ ¼å¼ï¼plot.[å°åç¼å·].obstacle.[éç¢ç©åç§°].xyCoords=[åæ ä¸²] |
| | | |
| | | # --- å°åLAND1çéç¢ç© --- |
| | | plot.LAND1.obstacle.1234.shape=0 |
| | | plot.LAND1.obstacle.1234.originalCoords=3949.902533,N;11616.757411,E;3949.902502,N;11616.757243,E;3949.902473,N;11616.757063,E;3949.902460,N;11616.756947,E;3949.902438,N;11616.756821,E;3949.902430,N;11616.756804,E;3949.902408,N;11616.756828,E;3949.902392,N;11616.756884,E;3949.902388,N;11616.756920,E;3949.902389,N;11616.756920,E;3949.902387,N;11616.756920,E;3949.902388,N;11616.756919,E;3949.902388,N;11616.756922,E;3949.902388,N;11616.756922,E;3949.902388,N;11616.756922,E;3949.902389,N;11616.756923,E;3949.902389,N;11616.756920,E;3949.901649,N;11616.757013,E;3949.901650,N;11616.757016,E;3949.901650,N;11616.757014,E;3949.901650,N;11616.757013,E |
| | | plot.LAND1.obstacle.1234.xyCoords=0.43,-0.56;1.30,-0.56 |
| | | |
| | |
| | | #Base station properties |
| | | #Tue Dec 09 19:03:31 CST 2025 |
| | | #Thu Dec 18 16:15:01 CST 2025 |
| | | dataUpdateTime=-1 |
| | | deviceActivationTime=-1 |
| | | deviceId=4567 |
| | | installationCoordinates=3949.90238860,N,11616.75692000,E |
| | | deviceId=1872 |
| | | installationCoordinates=3949.89151752,N,11616.79267501,E |
| | | iotSimCardNumber=-1 |
| | |
| | | #Updated base station coordinates |
| | | #Mon Nov 24 14:54:00 CST 2025 |
| | | #Updated base station information |
| | | #Thu Dec 18 16:15:01 CST 2025 |
| | | BupdateTime=-1 |
| | | GupdateTime=-1 |
| | | baseStationCardNumber=-1 |
| | | baseStationCoordinates=2324.194945,N,11330.938547,E |
| | | baseStationCoordinates=3949.89151752,N,11616.79267501,E |
| | | baseStationNumber=-1 |
| | | battery=-1 |
| | | createTime=-1 |
| | | deviceCardnumber=-1 |
| | | differentialAge=-1 |
| | | heading=-1 |
| | | mowerBladeHeight=-1 |
| | | mowerLightStatus=-1 |
| | | mowerModel=-1 |
| | | mowerName=-1 |
| | | mowerNumber=-1 |
| | | mowerStartStatus=-1 |
| | | mowingHeight=-1 |
| | | mowingWidth=-1 |
| | | pitch=-1 |
| | | positioningStatus=-1 |
| | | realtimeAltitude=-1 |
| | | realtimeLatitude=-1 |
| | | realtimeLongitude=-1 |
| | | realtimeAltitude=-1 |
| | | realtimeSpeed=-1 |
| | | realtimeX=-1 |
| | | realtimeY=-1 |
| | | realtimeSpeed=-1 |
| | | heading=-1 |
| | | pitch=-1 |
| | | battery=-1 |
| | | positioningStatus=-1 |
| | | satelliteCount=-1 |
| | | differentialAge=-1 |
| | | mowerStartStatus=-1 |
| | | mowerLightStatus=-1 |
| | | mowerBladeHeight=-1 |
| | |
| | | #Dikuai Properties |
| | | #Wed Dec 17 19:15:02 CST 2025 |
| | | #Thu Dec 18 19:44:44 CST 2025 |
| | | LAND1.angleThreshold=-1 |
| | | LAND1.baseStationCoordinates=3949.90238860,N,11616.75692000,E |
| | | LAND1.boundaryCoordinates=77.19,-32.68;80.71,-54.97;80.99,-55.90;83.54,-56.46;85.04,-55.55;85.94,-53.74;83.24,-35.82;84.55,-34.54;94.02,-31.92;94.10,-31.11;90.88,-20.39;90.35,-19.53;88.33,-19.00;84.12,-19.47;78.92,-22.36;76.63,-25.55;76.93,-29.84;77.06,-31.26;77.19,-32.68 |
| | | LAND1.boundaryOriginalCoordinates=39.831413,116.280186,49.12;39.831409,116.280188,49.09;39.831403,116.280187,49.12;39.831395,116.280189,49.13;39.831388,116.280191,49.16;39.831379,116.280193,49.18;39.831370,116.280194,49.16;39.831362,116.280195,49.15;39.831353,116.280197,49.11;39.831344,116.280200,49.15;39.831335,116.280202,49.15;39.831326,116.280204,49.08;39.831317,116.280206,49.19;39.831309,116.280209,49.18;39.831301,116.280210,49.19;39.831293,116.280212,49.11;39.831284,116.280214,49.06;39.831275,116.280215,49.16;39.831266,116.280217,49.14;39.831258,116.280220,49.08;39.831249,116.280223,49.09;39.831240,116.280225,49.10;39.831231,116.280226,49.04;39.831222,116.280226,49.17;39.831212,116.280227,49.11;39.831204,116.280230,49.09;39.831201,116.280238,49.10;39.831199,116.280249,49.07;39.831199,116.280260,49.21;39.831202,116.280270,49.16;39.831207,116.280278,49.06;39.831212,116.280284,49.04;39.831217,116.280287,49.05;39.831223,116.280288,49.09;39.831229,116.280287,49.10;39.831237,116.280286,49.05;39.831245,116.280286,49.08;39.831254,116.280284,49.07;39.831263,116.280283,49.05;39.831272,116.280280,49.11;39.831282,116.280278,49.10;39.831291,116.280276,49.11;39.831300,116.280274,49.16;39.831308,116.280270,49.13;39.831318,116.280268,49.10;39.831327,116.280267,49.14;39.831337,116.280266,49.08;39.831347,116.280263,49.10;39.831356,116.280261,49.20;39.831366,116.280258,49.14;39.831375,116.280256,49.09;39.831384,116.280257,49.13;39.831392,116.280263,49.10;39.831396,116.280272,49.12;39.831398,116.280283,49.16;39.831401,116.280294,49.11;39.831403,116.280307,49.13;39.831405,116.280318,49.19;39.831406,116.280328,49.20;39.831408,116.280340,49.22;39.831411,116.280353,49.19;39.831414,116.280363,49.26;39.831416,116.280374,49.22;39.831419,116.280383,49.20;39.831427,116.280384,49.21;39.831433,116.280379,49.17;39.831441,116.280375,49.19;39.831451,116.280372,49.09;39.831459,116.280370,49.16;39.831467,116.280364,49.21;39.831476,116.280360,49.22;39.831485,116.280357,49.20;39.831495,116.280355,49.26;39.831505,116.280351,49.21;39.831514,116.280348,49.17;39.831523,116.280346,49.20;39.831531,116.280340,49.04;39.831535,116.280328,49.08;39.831536,116.280316,49.03;39.831535,116.280304,49.03;39.831533,116.280291,49.06;39.831532,116.280279,49.07;39.831531,116.280267,49.11;39.831528,116.280257,49.09;39.831525,116.280246,49.11;39.831521,116.280237,49.09;39.831516,116.280227,49.08;39.831511,116.280216,49.12;39.831505,116.280206,49.14;39.831499,116.280197,49.12;39.831492,116.280189,49.15;39.831484,116.280184,49.14;39.831477,116.280179,49.12;39.831469,116.280178,49.12;39.831462,116.280181,49.13;39.831454,116.280182,49.12;39.831445,116.280183,49.12;39.831439,116.280183,49.14;39.831438,116.280183,49.12 |
| | | LAND1.baseStationCoordinates=3949.89151752,N,11616.79267501,E |
| | | LAND1.boundaryCoordinates=2.87,-0.19;6.73,-1.23;22.76,1.06;27.88,3.65;35.84,9.96;38.40,10.35;41.91,8.49;44.09,4.86;43.49,2.92;33.76,0.59;30.22,-1.26;30.09,-3.80;34.19,-28.28;32.68,-29.99;30.12,-30.19;28.89,-28.59;25.58,-6.77;24.09,-4.26;21.10,-3.94;-10.36,-8.92;-11.69,-8.57;-12.75,-4.68;-12.23,-3.28;-6.21,-2.34;0.00,0.00;1.44,-0.09;2.87,-0.19 |
| | | LAND1.boundaryOriginalCoordinates=39.831524,116.279912,49.30;39.831523,116.279911,49.23;39.831521,116.279915,49.31;39.831517,116.279925,49.34;39.831514,116.279940,49.30;39.831514,116.279957,49.28;39.831516,116.279974,49.28;39.831518,116.279991,49.29;39.831521,116.280008,49.24;39.831524,116.280025,49.30;39.831526,116.280042,49.24;39.831529,116.280059,49.29;39.831529,116.280076,49.26;39.831530,116.280093,49.32;39.831531,116.280110,49.28;39.831533,116.280127,49.28;39.831535,116.280144,49.26;39.831539,116.280161,49.27;39.831544,116.280175,49.25;39.831551,116.280190,49.24;39.831558,116.280204,49.26;39.831566,116.280219,49.26;39.831574,116.280234,49.22;39.831583,116.280248,49.24;39.831591,116.280260,49.24;39.831600,116.280272,49.23;39.831608,116.280285,49.18;39.831615,116.280298,49.12;39.831618,116.280312,49.11;39.831618,116.280328,49.12;39.831615,116.280342,49.15;39.831610,116.280356,49.21;39.831602,116.280369,49.23;39.831592,116.280379,49.25;39.831581,116.280388,49.25;39.831569,116.280394,49.19;39.831559,116.280395,49.23;39.831552,116.280387,49.28;39.831547,116.280373,49.32;39.831544,116.280357,49.33;39.831541,116.280340,49.29;39.831539,116.280324,49.27;39.831536,116.280307,49.24;39.831534,116.280290,49.25;39.831531,116.280273,49.26;39.831527,116.280257,49.28;39.831522,116.280242,49.21;39.831514,116.280232,49.28;39.831504,116.280229,49.24;39.831491,116.280230,49.33;39.831478,116.280233,49.34;39.831466,116.280236,49.31;39.831454,116.280239,49.31;39.831441,116.280242,49.26;39.831429,116.280244,49.23;39.831416,116.280247,49.25;39.831402,116.280250,49.22;39.831389,116.280253,49.25;39.831376,116.280256,49.26;39.831364,116.280258,49.24;39.831351,116.280261,49.25;39.831338,116.280265,49.26;39.831324,116.280268,49.20;39.831311,116.280271,49.16;39.831298,116.280274,49.17;39.831285,116.280277,49.22;39.831271,116.280278,49.16;39.831261,116.280273,49.23;39.831256,116.280261,49.30;39.831253,116.280245,49.20;39.831254,116.280231,49.22;39.831258,116.280220,49.10;39.831268,116.280216,49.17;39.831281,116.280214,49.14;39.831295,116.280212,49.14;39.831308,116.280209,49.17;39.831321,116.280206,49.14;39.831334,116.280204,49.17;39.831348,116.280202,49.21;39.831362,116.280198,49.21;39.831375,116.280195,49.23;39.831390,116.280193,49.25;39.831405,116.280189,49.20;39.831420,116.280187,49.19;39.831436,116.280185,49.21;39.831450,116.280182,49.19;39.831464,116.280177,49.17;39.831478,116.280171,49.14;39.831487,116.280160,49.25;39.831491,116.280143,49.21;39.831490,116.280125,49.18;39.831487,116.280107,49.23;39.831485,116.280089,49.27;39.831483,116.280072,49.24;39.831482,116.280054,49.25;39.831480,116.280037,49.25;39.831477,116.280020,49.30;39.831475,116.280004,49.31;39.831473,116.279989,49.30;39.831472,116.279973,49.23;39.831470,116.279958,49.26;39.831468,116.279941,49.29;39.831465,116.279925,49.33;39.831463,116.279908,49.34;39.831462,116.279891,49.35;39.831461,116.279874,49.33;39.831458,116.279859,49.34;39.831456,116.279843,49.32;39.831454,116.279827,49.32;39.831452,116.279813,49.42;39.831450,116.279798,49.41;39.831448,116.279783,49.36;39.831447,116.279769,49.26;39.831445,116.279757,49.24;39.831445,116.279747,49.38;39.831448,116.279741,49.27;39.831455,116.279736,49.26;39.831463,116.279733,49.26;39.831473,116.279731,49.26;39.831483,116.279729,49.25;39.831491,116.279730,49.26;39.831496,116.279735,49.22;39.831497,116.279748,49.27;39.831498,116.279762,49.27;39.831500,116.279776,49.34;39.831502,116.279791,49.32;39.831504,116.279805,49.27;39.831507,116.279820,49.28;39.831510,116.279835,49.20;39.831513,116.279849,49.25;39.831517,116.279860,49.30;39.831520,116.279864,49.32;39.831520,116.279865,49.34;39.831522,116.279873,49.25;39.831524,116.279878,49.25;39.831525,116.279878,49.24 |
| | | LAND1.boundaryPointInterval=-1 |
| | | LAND1.createTime=2025-12-16 15\:43\:39 |
| | | LAND1.createTime=2025-12-18 17\:12\:20 |
| | | LAND1.intelligentSceneAnalysis=-1 |
| | | LAND1.landArea=327.17 |
| | | LAND1.landName=yyii |
| | | LAND1.landArea=483.34 |
| | | LAND1.landName=123 |
| | | LAND1.landNumber=LAND1 |
| | | LAND1.mowingBladeWidth=0.40 |
| | | LAND1.mowingOverlapDistance=0.06 |
| | | LAND1.mowingPattern=å¹³è¡çº¿ |
| | | LAND1.mowingTrack= |
| | | LAND1.mowingWidth=40 |
| | | LAND1.plannedPath=77.45,-31.44;81.28,-55.71;81.70,-55.80;77.91,-31.78;78.05,-31.91;78.17,-31.98;78.35,-32.01;82.12,-55.89;82.54,-55.98;78.77,-32.09;78.95,-32.12;79.17,-32.09;82.96,-56.08;83.38,-56.17;79.57,-32.03;79.96,-31.95;83.76,-56.03;84.13,-55.81;80.35,-31.86;80.50,-31.80;80.72,-31.63;84.50,-55.58;84.87,-55.33;81.08,-31.34;81.41,-30.87;85.18,-54.72;85.48,-54.10;81.75,-30.45;81.89,-30.27;81.94,-30.19;82.05,-29.82;83.00,-35.83;83.34,-35.38;81.43,-23.31;81.47,-22.18;81.43,-22.04;81.32,-21.94;81.21,-21.92;81.13,-21.42;81.50,-21.21;83.69,-35.03;84.04,-34.69;81.88,-21.00;82.25,-20.80;84.39,-34.35;84.77,-34.22;82.62,-20.59;82.99,-20.38;85.16,-34.11;85.55,-34.00;83.37,-20.18;83.74,-19.97;85.94,-33.90;86.32,-33.79;84.11,-19.76;84.50,-19.68;86.71,-33.68;87.10,-33.57;84.90,-19.63;85.30,-19.59;87.49,-33.47;87.88,-33.36;85.70,-19.55;86.09,-19.50;88.26,-33.25;88.65,-33.15;86.49,-19.46;86.89,-19.41;89.04,-33.04;89.43,-32.93;87.29,-19.37;87.69,-19.32;89.82,-32.82;90.20,-32.72;88.08,-19.28;88.49,-19.30;90.59,-32.61;90.98,-32.50;88.91,-19.41;89.34,-19.52;91.37,-32.39;91.76,-32.29;89.76,-19.63;90.18,-19.74;92.14,-32.18;92.53,-32.07;90.76,-20.88;91.62,-23.72;92.92,-31.96;93.31,-31.86;92.47,-26.56;93.33,-29.40;93.70,-31.75;81.40,-22.00;81.27,-21.93;81.21,-21.92;81.08,-21.96;80.98,-22.05;79.85,-23.55;79.64,-22.24;80.01,-22.04;80.22,-23.37;80.58,-23.05;80.39,-21.83;80.76,-21.62;80.87,-22.34;77.96,-24.59;77.63,-24.92;77.59,-24.64;77.92,-24.18;77.99,-24.57;78.36,-24.38;78.25,-23.72;78.59,-23.25;78.73,-24.19;79.11,-23.99;78.92,-22.79;79.27,-22.45;79.48,-23.76;77.99,-24.57;77.93,-24.62;77.51,-25.05;77.47,-25.11;77.20,-25.66;77.01,-26.12;76.93,-25.57;77.26,-25.10;77.31,-25.43 |
| | | LAND1.mowingTrack=-1 |
| | | LAND1.mowingWidth=34 |
| | | LAND1.plannedPath=35.722,9.394;38.565,9.844;39.065,9.579;35.179,8.964;34.636,8.534;39.565,9.314;40.065,9.049;34.094,8.104;33.551,7.673;40.565,8.784;41.066,8.519;33.009,7.243;32.466,6.813;41.566,8.254;41.804,7.947;31.923,6.383;31.381,5.953;41.993,7.633;42.182,7.319;30.838,5.523;30.296,5.093;42.371,7.004;42.560,6.690;29.753,4.663;29.210,4.232;42.748,6.375;0.018,-0.389;0.094,-0.377;1.645,-0.475;-1.557,-0.982;42.937,6.061;28.668,3.802;28.125,3.372;43.126,5.747;-3.133,-1.576;2.993,-0.606;3.797,-0.823;-4.708,-2.169;43.315,5.432;27.172,2.877;26.181,2.376;43.503,5.118;-11.978,-3.664;4.602,-1.040;5.407,-1.257;-12.114,-4.030;43.685,4.803;25.191,1.875;24.200,1.374;43.573,4.441;-12.250,-4.396;6.212,-1.474;15.278,-0.383;-12.346,-4.755;43.461,4.079;23.210,0.873;-12.256,-5.085;43.349,3.717;43.237,3.355;-12.166,-5.415;-12.076,-5.745;40.411,2.563;36.170,1.548;-11.986,-6.075;-11.896,-6.405;33.250,0.741;32.306,0.247;-11.806,-6.735;-11.717,-7.065;31.361,-0.246;30.416,-0.740;-11.627,-7.395;-11.537,-7.725;29.861,-1.172;29.836,-1.520;-11.447,-8.055;-11.095,-8.344;29.818,-1.867;29.801,-2.215;21.190,-3.578;22.488,-3.716;29.783,-2.562;29.765,-2.909;23.785,-3.855;24.424,-4.098;29.747,-3.256;29.730,-3.603;24.611,-4.413;24.798,-4.728;29.739,-3.945;29.795,-4.281;24.985,-5.042;25.171,-5.357;29.852,-4.616;29.908,-4.951;25.358,-5.672;25.545,-5.986;29.964,-5.287;30.020,-5.622;25.732,-6.301;25.915,-6.616;30.076,-5.957;30.132,-6.293;25.982,-6.950;26.033,-7.286;30.189,-6.628;30.245,-6.964;26.084,-7.622;26.135,-7.958;30.301,-7.299;30.357,-7.634;26.185,-8.295;26.236,-8.631;30.413,-7.970;30.469,-8.305;26.287,-8.967;26.338,-9.303;30.526,-8.640;30.582,-8.976;26.389,-9.639;26.440,-9.975;30.638,-9.311;30.694,-9.646;26.491,-10.312;26.542,-10.648;30.750,-9.982;30.806,-10.317;26.593,-10.984;26.644,-11.320;30.862,-10.652;30.919,-10.988;26.695,-11.656;26.746,-11.992;30.975,-11.323;31.031,-11.658;26.797,-12.328;26.848,-12.665;31.087,-11.994;31.143,-12.329;26.899,-13.001;26.950,-13.337;31.199,-12.664;31.256,-13.000;27.001,-13.673;27.052,-14.009;31.312,-13.335;31.368,-13.670;27.103,-14.345;27.154,-14.682;31.424,-14.006;31.480,-14.341;27.205,-15.018;27.256,-15.354;31.536,-14.676;31.593,-15.012;27.307,-15.690;27.358,-16.026;31.649,-15.347;31.705,-15.682;27.409,-16.362;27.460,-16.699;31.761,-16.018;31.817,-16.353;27.511,-17.035;27.562,-17.371;31.873,-16.688;31.930,-17.024;27.613,-17.707;27.664,-18.043;31.986,-17.359;32.042,-17.694;27.715,-18.379;27.766,-18.716;32.098,-18.030;32.154,-18.365;27.817,-19.052;27.868,-19.388;32.210,-18.701;32.267,-19.036;27.919,-19.724;27.970,-20.060;32.323,-19.371;32.379,-19.707;28.021,-20.396;28.072,-20.733;32.435,-20.042;32.491,-20.377;28.123,-21.069;28.174,-21.405;32.547,-20.713;32.604,-21.048;28.225,-21.741;28.276,-22.077;32.660,-21.383;32.716,-21.719;28.327,-22.413;28.378,-22.749;32.772,-22.054;32.828,-22.389;28.429,-23.086;28.480,-23.422;32.884,-22.725;32.941,-23.060;28.531,-23.758;28.582,-24.094;32.997,-23.395;33.053,-23.731;28.633,-24.430;28.684,-24.766;33.109,-24.066;33.165,-24.401;28.735,-25.103;28.786,-25.439;33.221,-24.737;33.278,-25.072;28.837,-25.775;28.888,-26.111;33.334,-25.407;33.390,-25.743;28.939,-26.447;28.990,-26.783;33.446,-26.078;33.502,-26.413;29.041,-27.120;29.092,-27.456;33.558,-26.749;33.615,-27.084;29.143,-27.792;29.194,-28.128;33.671,-27.419;33.727,-27.755;29.258,-28.462;29.494,-28.769;33.783,-28.090;33.524,-28.475;29.730,-29.076;29.966,-29.383;33.170,-28.876;32.817,-29.276;30.202,-29.690 |
| | | LAND1.returnPointCoordinates=-1 |
| | | LAND1.updateTime=2025-12-17 19\:15\:02 |
| | | LAND1.updateTime=2025-12-18 19\:44\:44 |
| | | LAND1.userId=-1 |
| | |
| | | #Mower Configuration Properties - Updated |
| | | #Wed Dec 17 19:35:27 CST 2025 |
| | | #Fri Dec 19 11:48:07 CST 2025 |
| | | appVersion=-1 |
| | | boundaryLengthVisible=false |
| | | currentWorkLandNumber=LAND1 |
| | | cuttingWidth=200 |
| | | firmwareVersion=-1 |
| | | handheldMarkerId= |
| | | handheldMarkerId=1872 |
| | | idleTrailDurationSeconds=60 |
| | | mapScale=28.94 |
| | | mapScale=5.63 |
| | | measurementModeEnabled=false |
| | | mowerId=1872 |
| | | mowerId=860 |
| | | serialAutoConnect=true |
| | | serialBaudRate=115200 |
| | | serialPortName=COM15 |
| | | simCardNumber=-1 |
| | | viewCenterX=-3.17 |
| | | viewCenterY=0.87 |
| | | viewCenterX=-15.67 |
| | | viewCenterY=9.92 |
| | |
| | | private String mowingPattern; |
| | | // å²è宽度 |
| | | private String mowingWidth; |
| | | // å²èæºå²å宽度ï¼ç±³ï¼ï¼é»è®¤0.40ç±³ |
| | | private String mowingBladeWidth; |
| | | // å²èéå è·ç¦»ï¼ç±³ï¼ï¼é»è®¤0.06ç±³ |
| | | private String mowingOverlapDistance; |
| | | |
| | | // åå¨å¤ä¸ªå°åçæ å°è¡¨ï¼é®ä¸ºå°åç¼å· |
| | | private static Map<String, Dikuai> dikuaiMap = new HashMap<>(); |
| | |
| | | dikuai.mowingPattern = landProps.getProperty("mowingPattern", "-1"); |
| | | dikuai.mowingWidth = landProps.getProperty("mowingWidth", "-1"); |
| | | dikuai.mowingTrack = landProps.getProperty("mowingTrack", "-1"); |
| | | dikuai.mowingBladeWidth = landProps.getProperty("mowingBladeWidth", "0.40"); |
| | | dikuai.mowingOverlapDistance = landProps.getProperty("mowingOverlapDistance", "0.06"); |
| | | |
| | | dikuaiMap.put(landNum, dikuai); |
| | | } |
| | |
| | | case "mowingTrack": |
| | | this.mowingTrack = value; |
| | | return true; |
| | | case "mowingBladeWidth": |
| | | this.mowingBladeWidth = value; |
| | | return true; |
| | | case "mowingOverlapDistance": |
| | | this.mowingOverlapDistance = value; |
| | | return true; |
| | | default: |
| | | System.err.println("æªç¥å段: " + fieldName); |
| | | return false; |
| | |
| | | if (dikuai.mowingPattern != null) properties.setProperty(landNumber + ".mowingPattern", dikuai.mowingPattern); |
| | | if (dikuai.mowingWidth != null) properties.setProperty(landNumber + ".mowingWidth", dikuai.mowingWidth); |
| | | if (dikuai.mowingTrack != null) properties.setProperty(landNumber + ".mowingTrack", dikuai.mowingTrack); |
| | | if (dikuai.mowingBladeWidth != null) properties.setProperty(landNumber + ".mowingBladeWidth", dikuai.mowingBladeWidth); |
| | | if (dikuai.mowingOverlapDistance != null) properties.setProperty(landNumber + ".mowingOverlapDistance", dikuai.mowingOverlapDistance); |
| | | } |
| | | |
| | | try { |
| | |
| | | this.mowingTrack = mowingTrack; |
| | | } |
| | | |
| | | public String getMowingBladeWidth() { |
| | | return mowingBladeWidth; |
| | | } |
| | | |
| | | public void setMowingBladeWidth(String mowingBladeWidth) { |
| | | this.mowingBladeWidth = mowingBladeWidth; |
| | | } |
| | | |
| | | public String getMowingOverlapDistance() { |
| | | return mowingOverlapDistance; |
| | | } |
| | | |
| | | public void setMowingOverlapDistance(String mowingOverlapDistance) { |
| | | this.mowingOverlapDistance = mowingOverlapDistance; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return "Dikuai{" + |
| | |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | |
| | | // å°åè¾¹çåæ ï¼å¸¦æ¾ç¤ºé¡¶ç¹æé®ï¼ |
| | | JPanel boundaryPanel = createBoundaryInfoItem(dikuai, |
| | | getTruncatedValue(dikuai.getBoundaryCoordinates(), 12, "æªè®¾ç½®")); |
| | | setInfoItemTooltip(boundaryPanel, dikuai.getBoundaryCoordinates()); |
| | | JPanel boundaryPanel = createBoundaryInfoItem(dikuai); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(boundaryPanel), |
| | | () -> editBoundaryCoordinates(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾å°åè¾¹çåæ "); |
| | |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | |
| | | // è·¯å¾åæ ï¼å¸¦æ¥çæé®ï¼ |
| | | JPanel pathPanel = createCardInfoItemWithButton("è·¯å¾åæ :", |
| | | getTruncatedValue(dikuai.getPlannedPath(), 12, "æªè®¾ç½®"), |
| | | "å¤å¶", e -> copyCoordinatesAction("è·¯å¾åæ ", dikuai.getPlannedPath())); |
| | | setInfoItemTooltip(pathPanel, dikuai.getPlannedPath()); |
| | | JPanel pathPanel = createCardInfoItemWithButtonOnly("è·¯å¾åæ :", |
| | | "æ¥ç", e -> editPlannedPath(dikuai)); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(pathPanel), |
| | | () -> editPlannedPath(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾è·¯å¾åæ "); |
| | | contentPanel.add(pathPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | |
| | | JPanel baseStationPanel = createCardInfoItemWithButton("åºç«åæ :", |
| | | getTruncatedValue(dikuai.getBaseStationCoordinates(), 12, "æªè®¾ç½®"), |
| | | "å¤å¶", e -> copyCoordinatesAction("åºç«åæ ", dikuai.getBaseStationCoordinates())); |
| | | setInfoItemTooltip(baseStationPanel, dikuai.getBaseStationCoordinates()); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(baseStationPanel), |
| | | () -> editBaseStationCoordinates(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾åºç«åæ "); |
| | | contentPanel.add(baseStationPanel); |
| | | JPanel baseStationPanel = createCardInfoItemWithButtonOnly("åºç«åæ :", |
| | | "æ¥ç", e -> editBaseStationCoordinates(dikuai)); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(baseStationPanel), |
| | | () -> editBaseStationCoordinates(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾åºç«åæ "); |
| | | contentPanel.add(baseStationPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | |
| | | JPanel boundaryOriginalPanel = createCardInfoItemWithButton("è¾¹çåå§åæ :", |
| | | getTruncatedValue(dikuai.getBoundaryOriginalCoordinates(), 12, "æªè®¾ç½®"), |
| | | "å¤å¶", e -> copyCoordinatesAction("è¾¹çåå§åæ ", dikuai.getBoundaryOriginalCoordinates())); |
| | | setInfoItemTooltip(boundaryOriginalPanel, dikuai.getBoundaryOriginalCoordinates()); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(boundaryOriginalPanel), |
| | | () -> editBoundaryOriginalCoordinates(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾è¾¹çåå§åæ "); |
| | | contentPanel.add(boundaryOriginalPanel); |
| | | JPanel boundaryOriginalPanel = createCardInfoItemWithButtonOnly("è¾¹çåå§åæ :", |
| | | "æ¥ç", e -> editBoundaryOriginalCoordinates(dikuai)); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(boundaryOriginalPanel), |
| | | () -> editBoundaryOriginalCoordinates(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾è¾¹çåå§åæ "); |
| | | contentPanel.add(boundaryOriginalPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | |
| | | JPanel mowingPatternPanel = createCardInfoItemWithButton("å²è模å¼:", |
| | | getTruncatedValue(dikuai.getMowingPattern(), 12, "æªè®¾ç½®"), |
| | | "å¤å¶", e -> copyCoordinatesAction("å²è模å¼", dikuai.getMowingPattern())); |
| | | setInfoItemTooltip(mowingPatternPanel, dikuai.getMowingPattern()); |
| | | JPanel mowingPatternPanel = createCardInfoItem("å²è模å¼:", |
| | | formatMowingPatternForDisplay(dikuai.getMowingPattern())); |
| | | configureInteractiveLabel(getInfoItemTitleLabel(mowingPatternPanel), |
| | | () -> editMowingPattern(dikuai), |
| | | "ç¹å»æ¥ç/ç¼è¾å²è模å¼"); |
| | | contentPanel.add(mowingPatternPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | |
| | | String mowingWidthValue = dikuai.getMowingWidth(); |
| | | String widthSource = null; |
| | | if (mowingWidthValue != null && !"-1".equals(mowingWidthValue) && !mowingWidthValue.trim().isEmpty()) { |
| | | widthSource = mowingWidthValue + "åç±³"; |
| | | // å²èæºå²å宽度 |
| | | String mowingBladeWidthValue = dikuai.getMowingBladeWidth(); |
| | | String displayBladeWidth = "æªè®¾ç½®"; |
| | | if (mowingBladeWidthValue != null && !"-1".equals(mowingBladeWidthValue) && !mowingBladeWidthValue.trim().isEmpty()) { |
| | | try { |
| | | double bladeWidthMeters = Double.parseDouble(mowingBladeWidthValue.trim()); |
| | | double bladeWidthCm = bladeWidthMeters * 100.0; |
| | | displayBladeWidth = String.format("%.2fåç±³", bladeWidthCm); |
| | | } catch (NumberFormatException e) { |
| | | displayBladeWidth = "æªè®¾ç½®"; |
| | | } |
| | | } |
| | | String displayWidth = getTruncatedValue(widthSource, 12, "æªè®¾ç½®"); |
| | | JPanel mowingWidthPanel = createCardInfoItemWithButton("å²è宽度:", |
| | | displayWidth, |
| | | "ç¼è¾", e -> editMowingWidth(dikuai)); |
| | | setInfoItemTooltip(mowingWidthPanel, widthSource); |
| | | JPanel mowingBladeWidthPanel = createCardInfoItem("å²èæºå²å宽度:", displayBladeWidth); |
| | | contentPanel.add(mowingBladeWidthPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | |
| | | String mowingWidthValue = dikuai.getMowingWidth(); |
| | | String displayWidth = "æªè®¾ç½®"; |
| | | if (mowingWidthValue != null && !"-1".equals(mowingWidthValue) && !mowingWidthValue.trim().isEmpty()) { |
| | | displayWidth = mowingWidthValue + "åç±³"; |
| | | } |
| | | JPanel mowingWidthPanel = createCardInfoItem("å²è宽度:", displayWidth); |
| | | contentPanel.add(mowingWidthPanel); |
| | | contentPanel.add(Box.createRigidArea(new Dimension(0, 15))); |
| | | |
| | |
| | | JButton generatePathBtn = createPrimaryFooterButton("è·¯å¾è§å"); |
| | | generatePathBtn.addActionListener(e -> showPathPlanningPage(dikuai)); |
| | | |
| | | JButton navigationPreviewBtn = createPrimaryFooterButton("导èªé¢è§"); |
| | | navigationPreviewBtn.addActionListener(e -> startNavigationPreview(dikuai)); |
| | | |
| | | JPanel footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); |
| | | footerPanel.setBackground(CARD_BACKGROUND); |
| | | footerPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); |
| | | footerPanel.add(generatePathBtn); |
| | | footerPanel.add(Box.createHorizontalStrut(12)); |
| | | footerPanel.add(navigationPreviewBtn); |
| | | footerPanel.add(Box.createHorizontalStrut(12)); |
| | | footerPanel.add(deleteBtn); |
| | | card.add(footerPanel, BorderLayout.SOUTH); |
| | | |
| | |
| | | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(valueComp, BorderLayout.EAST); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | |
| | | return itemPanel; |
| | | } |
| | | |
| | | private JPanel createBoundaryInfoItem(Dikuai dikuai, String displayValue) { |
| | | private JPanel createCardInfoItemWithButtonOnly(String label, String buttonText, ActionListener listener) { |
| | | JPanel itemPanel = new JPanel(new BorderLayout()); |
| | | itemPanel.setBackground(CARD_BACKGROUND); |
| | | // å¢å é«åº¦ä»¥ç¡®ä¿æé®å®æ´æ¾ç¤ºï¼æé®é«åº¦çº¦24-28åç´ ï¼å ä¸ä¸ä¸è¾¹è·ï¼ |
| | | itemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35)); |
| | | itemPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, 30)); |
| | | itemPanel.setMinimumSize(new Dimension(0, 28)); |
| | | |
| | | JLabel labelComp = new JLabel(label); |
| | | labelComp.setFont(new Font("微软é
é»", Font.PLAIN, 14)); |
| | | labelComp.setForeground(LIGHT_TEXT); |
| | | |
| | | JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); |
| | | rightPanel.setBackground(CARD_BACKGROUND); |
| | | // æ·»å åç´å
è¾¹è·ä»¥ç¡®ä¿æé®ä¸è¢«è£åª |
| | | rightPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0)); |
| | | |
| | | JButton button = createSmallLinkButton(buttonText, listener); |
| | | |
| | | rightPanel.add(button); |
| | | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | | } |
| | | |
| | | private JPanel createBoundaryInfoItem(Dikuai dikuai) { |
| | | JPanel itemPanel = new JPanel(new BorderLayout()); |
| | | itemPanel.setBackground(CARD_BACKGROUND); |
| | | // å¢å é«åº¦ä»¥ç¡®ä¿æé®ä¸è¾¹ç¼å®æ´æ¾ç¤ºï¼æé®é«åº¦56ï¼å ä¸ä¸ä¸è¾¹è·ï¼ |
| | |
| | | rightPanel.setBackground(CARD_BACKGROUND); |
| | | rightPanel.setBorder(BorderFactory.createEmptyBorder(verticalPadding, 0, verticalPadding, 0)); |
| | | |
| | | JLabel valueComp = new JLabel(displayValue); |
| | | valueComp.setFont(new Font("微软é
é»", Font.PLAIN, 14)); |
| | | valueComp.setForeground(TEXT_COLOR); |
| | | // ç¶ææç¤ºæåæ ç¾ |
| | | JLabel statusLabel = new JLabel(); |
| | | statusLabel.setFont(new Font("微软é
é»", Font.PLAIN, 13)); |
| | | statusLabel.setForeground(LIGHT_TEXT); |
| | | |
| | | JButton toggleButton = createBoundaryToggleButton(dikuai); |
| | | // å°ç¶ææ ç¾åæé®å
³èï¼ä»¥ä¾¿å¨æé®ç¶æååæ¶æ´æ°æ ç¾ |
| | | toggleButton.putClientProperty("statusLabel", statusLabel); |
| | | |
| | | rightPanel.add(valueComp); |
| | | // åå§åç¶ææå |
| | | String landNumber = dikuai.getLandNumber(); |
| | | boolean isVisible = boundaryPointVisibility.getOrDefault(landNumber, false); |
| | | updateBoundaryStatusLabel(statusLabel, isVisible); |
| | | |
| | | rightPanel.add(statusLabel); |
| | | rightPanel.add(toggleButton); |
| | | |
| | | itemPanel.add(labelComp, BorderLayout.WEST); |
| | | itemPanel.add(rightPanel, BorderLayout.CENTER); |
| | | itemPanel.putClientProperty("valueLabel", valueComp); |
| | | itemPanel.putClientProperty("titleLabel", labelComp); |
| | | |
| | | return itemPanel; |
| | |
| | | button.setOpaque(true); |
| | | } |
| | | button.setToolTipText(active ? "éèè¾¹çç¹åºå·" : "æ¾ç¤ºè¾¹çç¹åºå·"); |
| | | |
| | | // æ´æ°ç¶ææç¤ºæå |
| | | Object statusLabelObj = button.getClientProperty("statusLabel"); |
| | | if (statusLabelObj instanceof JLabel) { |
| | | JLabel statusLabel = (JLabel) statusLabelObj; |
| | | updateBoundaryStatusLabel(statusLabel, active); |
| | | } |
| | | } |
| | | |
| | | private void updateBoundaryStatusLabel(JLabel statusLabel, boolean active) { |
| | | if (statusLabel == null) { |
| | | return; |
| | | } |
| | | if (active) { |
| | | statusLabel.setText("å·²å¼å¯è¾¹çç¹æ¾ç¤º"); |
| | | } else { |
| | | statusLabel.setText("å·²å
³éè¾¹çç¹æ¾ç¤º"); |
| | | } |
| | | } |
| | | |
| | | private void ensureBoundaryToggleIconsLoaded() { |
| | |
| | | } |
| | | |
| | | /** |
| | | * å¯å¨å¯¼èªé¢è§ |
| | | */ |
| | | private void startNavigationPreview(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | | } |
| | | |
| | | Window owner = SwingUtilities.getWindowAncestor(this); |
| | | |
| | | // è·åå°å管çå¯¹è¯æ¡ï¼åå¤å¨æå¼å¯¼èªé¢è§æ¶å
³é |
| | | Window managementWindow = null; |
| | | if (owner instanceof JDialog) { |
| | | managementWindow = owner; |
| | | } |
| | | |
| | | // å
³éå°å管çé¡µé¢ |
| | | if (managementWindow != null) { |
| | | managementWindow.dispose(); |
| | | } |
| | | |
| | | // å¯å¨å¯¼èªé¢è§ |
| | | daohangyulan.getInstance().startNavigationPreview(dikuai); |
| | | } |
| | | |
| | | /** |
| | | * æ¾ç¤ºè·¯å¾è§åé¡µé¢ |
| | | */ |
| | | private void showPathPlanningPage(Dikuai dikuai) { |
| | |
| | | dialog.setVisible(true); |
| | | } |
| | | |
| | | |
| | | private void generateMowingPath(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | return; |
| | |
| | | return section; |
| | | } |
| | | |
| | | private String formatMowingPatternForDisplay(String patternValue) { |
| | | String sanitized = sanitizeValueOrNull(patternValue); |
| | | if (sanitized == null) { |
| | | return "æªè®¾ç½®"; |
| | | } |
| | | String normalized = normalizeExistingMowingPattern(sanitized); |
| | | if ("parallel".equals(normalized)) { |
| | | return "å¹³è¡æ¨¡å¼ (parallel)"; |
| | | } |
| | | if ("spiral".equals(normalized)) { |
| | | return "èºææ¨¡å¼ (spiral)"; |
| | | } |
| | | return sanitized; |
| | | } |
| | | |
| | | private String formatMowingPatternForDialog(String patternValue) { |
| | | String sanitized = sanitizeValueOrNull(patternValue); |
| | | if (sanitized == null) { |
| | |
| | | JTextArea xyArea = createDataTextArea(genCoords, 3); // å¼ç¨ä»¥ä¾¿æ´æ° |
| | | JScrollPane scrollXY = new JScrollPane(xyArea); |
| | | scrollXY.setBorder(BorderFactory.createEmptyBorder()); // å¤é¨ç±Panelæä¾è¾¹æ¡ |
| | | |
| | | // 设置æ»å¨æ¡çç¥ï¼éè¦æ¶æ¾ç¤ºåç´æ»å¨æ¡ï¼ä¸ä½¿ç¨æ°´å¹³æ»å¨æ¡ï¼å 为å¯ç¨äºèªå¨æ¢è¡ï¼ |
| | | scrollXY.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); |
| | | scrollXY.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); |
| | | // 设置æ»å¨æ¡åä½å¢éï¼ä½¿æ»å¨æ´æµç
|
| | | scrollXY.getVerticalScrollBar().setUnitIncrement(16); |
| | | // 设置æ»å¨é¢æ¿çé¦é大å°ï¼ç¡®ä¿å¨å
容è¶
åºæ¶æ¾ç¤ºæ»å¨æ¡ |
| | | int lineHeight = xyArea.getFontMetrics(xyArea.getFont()).getHeight(); |
| | | int preferredHeight = 3 * lineHeight + 10; // 3è¡çé«åº¦ |
| | | scrollXY.setPreferredSize(new Dimension(Integer.MAX_VALUE, preferredHeight)); |
| | | scrollXY.setMaximumSize(new Dimension(Integer.MAX_VALUE, 200)); // æå¤§é«åº¦200åç´ |
| | | |
| | | JPanel xyWrapper = createWrapperPanel("çæåæ (XYç±³)", scrollXY); |
| | | card.add(xyWrapper); |
| | | card.add(Box.createVerticalStrut(15)); |
| | |
| | | JTextArea area = createDataTextArea(content, rows); |
| | | JScrollPane scroll = new JScrollPane(area); |
| | | scroll.setBorder(BorderFactory.createEmptyBorder()); |
| | | // 设置æ»å¨æ¡çç¥ï¼éè¦æ¶æ¾ç¤ºåç´æ»å¨æ¡ï¼ä¸ä½¿ç¨æ°´å¹³æ»å¨æ¡ï¼å 为å¯ç¨äºèªå¨æ¢è¡ï¼ |
| | | scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); |
| | | scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); |
| | | // 设置æ»å¨æ¡åä½å¢éï¼ä½¿æ»å¨æ´æµç
|
| | | scroll.getVerticalScrollBar().setUnitIncrement(16); |
| | | // 设置æ»å¨é¢æ¿çé¦é大å°ï¼ç¡®ä¿å¨å
容è¶
åºæ¶æ¾ç¤ºæ»å¨æ¡ |
| | | int lineHeight = area.getFontMetrics(area.getFont()).getHeight(); |
| | | int preferredHeight = rows * lineHeight + 10; // æ ¹æ®è¡æ°è®¡ç®é¦éé«åº¦ |
| | | scroll.setPreferredSize(new Dimension(Integer.MAX_VALUE, preferredHeight)); |
| | | scroll.setMaximumSize(new Dimension(Integer.MAX_VALUE, 200)); // æå¤§é«åº¦200åç´ |
| | | return createWrapperPanel(title, scroll); |
| | | } |
| | | |
| | |
| | | JTextArea area = new JTextArea(text); |
| | | area.setRows(rows); |
| | | area.setEditable(false); |
| | | // å¯ç¨èªå¨æ¢è¡ |
| | | area.setLineWrap(true); |
| | | area.setWrapStyleWord(true); |
| | | area.setBackground(BG_INPUT); |
| | | area.setForeground(new Color(50, 50, 50)); |
| | | // 使ç¨ç宽å使¾ç¤ºæ°æ®ï¼çèµ·æ¥æ´ä¸ä¸ |
| | | area.setFont(new Font("Monospaced", Font.PLAIN, 12)); |
| | | area.setFont(new Font("Monospaced", Font.PLAIN, 12)); |
| | | return area; |
| | | } |
| | | |
| | |
| | | return; |
| | | } |
| | | |
| | | // 计ç®éç¢ç©çä¸å¿ç¹åæ |
| | | double[] centerCoords = calculateObstacleCenter(obstacle); |
| | | |
| | | List<Obstacledge.Obstacle> allObstacles = loadObstacles(); |
| | | String allObstaclesCoords = buildAllObstaclesCoordinates(allObstacles); |
| | | |
| | |
| | | newPage.setVisible(true); |
| | | }) |
| | | ); |
| | | |
| | | // å°å°å¾è§å¾ä¸å¿è®¾ç½®ä¸ºéç¢ç©çä¸å¿ä½ç½® |
| | | if (centerCoords != null && shouye.getMapRenderer() != null) { |
| | | double currentScale = shouye.getMapRenderer().getScale(); |
| | | // å°è§å¾ä¸å¿è®¾ç½®ä¸ºéç¢ç©ä¸å¿ï¼ä½¿ç¨è´å¼ï¼å 为translateæ¯ç¸å¯¹äºåç¹çåç§»ï¼ |
| | | shouye.getMapRenderer().setViewTransform(currentScale, -centerCoords[0], -centerCoords[1]); |
| | | } |
| | | } else { |
| | | JOptionPane.showMessageDialog(null, "æ æ³æå¼ä¸»é¡µé¢è¿è¡é¢è§", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 计ç®éç¢ç©çä¸å¿ç¹åæ |
| | | * @param obstacle éç¢ç© |
| | | * @return ä¸å¿ç¹åæ [centerX, centerY]ï¼å¦ææ æ³è®¡ç®åè¿ånull |
| | | */ |
| | | private double[] calculateObstacleCenter(Obstacledge.Obstacle obstacle) { |
| | | if (obstacle == null) { |
| | | return null; |
| | | } |
| | | |
| | | List<Obstacledge.XYCoordinate> xyCoords = obstacle.getXyCoordinates(); |
| | | if (xyCoords == null || xyCoords.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | Obstacledge.ObstacleShape shape = obstacle.getShape(); |
| | | double centerX, centerY; |
| | | |
| | | if (shape == Obstacledge.ObstacleShape.CIRCLE) { |
| | | // åå½¢éç¢ç©ï¼ç¬¬ä¸ä¸ªåæ ç¹å°±æ¯åå¿ |
| | | if (xyCoords.size() < 1) { |
| | | return null; |
| | | } |
| | | Obstacledge.XYCoordinate centerCoord = xyCoords.get(0); |
| | | centerX = centerCoord.getX(); |
| | | centerY = centerCoord.getY(); |
| | | } else if (shape == Obstacledge.ObstacleShape.POLYGON) { |
| | | // å¤è¾¹å½¢éç¢ç©ï¼è®¡ç®éå¿ |
| | | centerX = 0.0; |
| | | centerY = 0.0; |
| | | double area = 0.0; |
| | | int n = xyCoords.size(); |
| | | |
| | | for (int i = 0; i < n; i++) { |
| | | Obstacledge.XYCoordinate current = xyCoords.get(i); |
| | | Obstacledge.XYCoordinate next = xyCoords.get((i + 1) % n); |
| | | double x0 = current.getX(); |
| | | double y0 = current.getY(); |
| | | double x1 = next.getX(); |
| | | double y1 = next.getY(); |
| | | double cross = x0 * y1 - x1 * y0; |
| | | area += cross; |
| | | centerX += (x0 + x1) * cross; |
| | | centerY += (y0 + y1) * cross; |
| | | } |
| | | |
| | | double areaFactor = area * 0.5; |
| | | if (Math.abs(areaFactor) < 1e-9) { |
| | | // 妿é¢ç§¯ä¸º0ææ¥è¿0ï¼ä½¿ç¨ç®åå¹³å |
| | | for (Obstacledge.XYCoordinate coord : xyCoords) { |
| | | centerX += coord.getX(); |
| | | centerY += coord.getY(); |
| | | } |
| | | int size = Math.max(1, xyCoords.size()); |
| | | centerX /= size; |
| | | centerY /= size; |
| | | } else { |
| | | centerX = centerX / (6.0 * areaFactor); |
| | | centerY = centerY / (6.0 * areaFactor); |
| | | } |
| | | } else { |
| | | return null; |
| | | } |
| | | |
| | | return new double[]{centerX, centerY}; |
| | | } |
| | | |
| | | private String buildAllObstaclesCoordinates(List<Obstacledge.Obstacle> obstacles) { |
| | | if (obstacles == null || obstacles.isEmpty()) return null; |
| | |
| | | return value != null && !value.trim().isEmpty() && !"-1".equals(value.trim()); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | |
| | | return; |
| | | } |
| | | if (userTriggered && "handheld".equalsIgnoreCase(type) && !hasConfiguredHandheldMarker()) { |
| | | JOptionPane.showMessageDialog(this, "请å
æ·»å ä¾¿æºæç¹å¨ç¼å·", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | JOptionPane.showMessageDialog(this, "请å
å»ç³»ç»è®¾ç½®æ·»å ä¾¿æºæç¹å¨ç¼å·", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | if (selectedMethodPanel != null && selectedMethodPanel != option) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package dikuai; |
| | | |
| | | import javax.swing.*; |
| | | import java.awt.*; |
| | | import java.awt.event.ActionEvent; |
| | | import java.awt.event.ActionListener; |
| | | import java.awt.geom.Point2D; |
| | | import java.util.List; |
| | | import java.util.ArrayList; |
| | | import zhuye.Shouye; |
| | | import zhuye.MapRenderer; |
| | | import zhuye.buttonset; |
| | | import gecaoji.Gecaoji; |
| | | import gecaoji.lujingdraw; |
| | | |
| | | /** |
| | | * 导èªé¢è§åè½ç±» |
| | | * å®ç°å²èæºæ²¿è·¯å¾æ¨¡æå¯¼èª |
| | | */ |
| | | public class daohangyulan { |
| | | private static daohangyulan instance; |
| | | private Shouye shouye; |
| | | private MapRenderer mapRenderer; |
| | | private Gecaoji mower; |
| | | private Dikuai currentDikuai; |
| | | |
| | | // è·¯å¾ç¸å
³ |
| | | private List<Point2D.Double> pathPoints; |
| | | private int currentPathIndex; |
| | | private double currentSpeed; // ç±³/ç§ |
| | | private static final double DEFAULT_SPEED = 1.0; // é»è®¤1ç±³/ç§ |
| | | private static final double SPEED_MULTIPLIER = 1.0; // æ¯æ¬¡å é/åéçåæ° |
| | | |
| | | // 宿¶å¨ |
| | | private Timer navigationTimer; |
| | | private static final int TIMER_INTERVAL_MS = 50; // 50æ¯«ç§æ´æ°ä¸æ¬¡ |
| | | |
| | | // 导èªé¢è§æé® |
| | | private JButton speedUpBtn; |
| | | private JButton speedDownBtn; |
| | | private JButton exitBtn; |
| | | |
| | | // ä¿ååå§æé®å颿¿çå¼ç¨ |
| | | private JButton originalStartBtn; |
| | | private JButton originalStopBtn; |
| | | private JPanel originalButtonPanel; |
| | | private LayoutManager originalButtonPanelLayout; |
| | | |
| | | // 导èªé¢è§è½¨è¿¹ |
| | | private List<Point2D.Double> navigationTrack; |
| | | |
| | | // æ¯å¦æ£å¨å¯¼èªé¢è§ |
| | | private boolean isNavigating; |
| | | |
| | | private daohangyulan() { |
| | | this.currentSpeed = DEFAULT_SPEED; |
| | | this.navigationTrack = new ArrayList<>(); |
| | | this.isNavigating = false; |
| | | } |
| | | |
| | | public static daohangyulan getInstance() { |
| | | if (instance == null) { |
| | | instance = new daohangyulan(); |
| | | } |
| | | return instance; |
| | | } |
| | | |
| | | /** |
| | | * å¯å¨å¯¼èªé¢è§ |
| | | * @param dikuai å°å对象 |
| | | */ |
| | | public void startNavigationPreview(Dikuai dikuai) { |
| | | if (dikuai == null) { |
| | | JOptionPane.showMessageDialog(null, "å°åæ°æ®æ æ", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // è·åå°åçå²èè·¯å¾åæ |
| | | String plannedPath = dikuai.getPlannedPath(); |
| | | if (plannedPath == null || plannedPath.trim().isEmpty() || "-1".equals(plannedPath.trim())) { |
| | | JOptionPane.showMessageDialog(null, "å½åå°å没æè§åè·¯å¾ï¼æ æ³è¿è¡å¯¼èªé¢è§", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // è§£æè·¯å¾åæ |
| | | pathPoints = lujingdraw.parsePlannedPath(plannedPath); |
| | | if (pathPoints == null || pathPoints.size() < 2) { |
| | | JOptionPane.showMessageDialog(null, "è·¯å¾åæ è§£æå¤±è´¥ï¼æ æ³è¿è¡å¯¼èªé¢è§", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | this.currentDikuai = dikuai; |
| | | this.currentPathIndex = 0; |
| | | this.currentSpeed = DEFAULT_SPEED; |
| | | this.navigationTrack.clear(); |
| | | |
| | | // è·åé¦é¡µåå°å¾æ¸²æå¨ |
| | | shouye = Shouye.getInstance(); |
| | | if (shouye == null) { |
| | | JOptionPane.showMessageDialog(null, "æ æ³è·åé¦é¡µå®ä¾", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | mapRenderer = shouye.getMapRenderer(); |
| | | if (mapRenderer == null) { |
| | | JOptionPane.showMessageDialog(null, "æ æ³è·åå°å¾æ¸²æå¨", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | mower = mapRenderer.getMower(); |
| | | if (mower == null) { |
| | | JOptionPane.showMessageDialog(null, "æ æ³è·åå²èæºå®ä¾", "é误", JOptionPane.ERROR_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | // 设置å²èæºåå§ä½ç½®ä¸ºè·¯å¾èµ·ç¹ |
| | | Point2D.Double startPoint = pathPoints.get(0); |
| | | mower.setPosition(startPoint.x, startPoint.y); |
| | | |
| | | // 计ç®åå§æ¹åï¼æå第ä¸ä¸ªç®æ ç¹ï¼ |
| | | if (pathPoints.size() > 1) { |
| | | Point2D.Double nextPoint = pathPoints.get(1); |
| | | double heading = calculateHeading(startPoint, nextPoint); |
| | | setMowerHeading(heading); |
| | | } else { |
| | | // å¦æåªæä¸ä¸ªç¹ï¼è®¾ç½®é»è®¤æ¹åï¼0度ï¼åå³ï¼ |
| | | setMowerHeading(0.0); |
| | | } |
| | | |
| | | // åå§å导èªé¢è§è½¨è¿¹ |
| | | navigationTrack.clear(); |
| | | navigationTrack.add(new Point2D.Double(startPoint.x, startPoint.y)); |
| | | mapRenderer.clearNavigationPreviewTrack(); |
| | | mapRenderer.addNavigationPreviewTrackPoint(startPoint); |
| | | |
| | | // è·åå²èæºå²å宽度 |
| | | String bladeWidthStr = dikuai.getMowingBladeWidth(); |
| | | double bladeWidthMeters = 0.5; // é»è®¤0.5ç±³ |
| | | if (bladeWidthStr != null && !bladeWidthStr.trim().isEmpty() && !"-1".equals(bladeWidthStr.trim())) { |
| | | try { |
| | | bladeWidthMeters = Double.parseDouble(bladeWidthStr.trim()); |
| | | } catch (NumberFormatException e) { |
| | | // 使ç¨é»è®¤å¼ |
| | | } |
| | | } |
| | | mapRenderer.setNavigationPreviewWidth(bladeWidthMeters); |
| | | |
| | | // 设置åå§é度æ¾ç¤º |
| | | mapRenderer.setNavigationPreviewSpeed(currentSpeed); |
| | | |
| | | // å建并æ¾ç¤ºå¯¼èªé¢è§æé®ï¼æ¿æ¢åºé¨æé®ï¼ |
| | | createNavigationButtons(); |
| | | |
| | | // æ¾ç¤ºå¯¼èªé¢è§æ¨¡å¼æ ç¾ |
| | | if (shouye != null) { |
| | | shouye.setNavigationPreviewLabelVisible(true); |
| | | } |
| | | |
| | | // å¯å¨å¯¼èªå®æ¶å¨ |
| | | startNavigationTimer(); |
| | | |
| | | isNavigating = true; |
| | | |
| | | // å·æ°å°å¾æ¾ç¤º |
| | | mapRenderer.repaint(); |
| | | } |
| | | |
| | | /** |
| | | * å建导èªé¢è§æé®ï¼æ¿æ¢åºé¨çæåãç»ææé®ï¼ |
| | | */ |
| | | private void createNavigationButtons() { |
| | | // ç§»é¤æ§çæé®ï¼å¦æåå¨ï¼ |
| | | removeNavigationButtons(); |
| | | |
| | | if (shouye == null) { |
| | | return; |
| | | } |
| | | |
| | | // è·ååå§æé® |
| | | originalStartBtn = shouye.getStartButton(); |
| | | originalStopBtn = shouye.getStopButton(); |
| | | |
| | | if (originalStartBtn == null || originalStopBtn == null) { |
| | | return; |
| | | } |
| | | |
| | | // è·åæ§å¶é¢æ¿ |
| | | JPanel controlPanel = shouye.getControlPanel(); |
| | | if (controlPanel == null) { |
| | | return; |
| | | } |
| | | |
| | | // æ¥æ¾æé®é¢æ¿ï¼å
å« startBtn å stopBtn ç颿¿ï¼ |
| | | JPanel buttonPanel = null; |
| | | for (Component comp : controlPanel.getComponents()) { |
| | | if (comp instanceof JPanel) { |
| | | JPanel panel = (JPanel) comp; |
| | | // æ£æ¥æ¯å¦æ¯æé®é¢æ¿ï¼å
å« startBtn å stopBtnï¼ |
| | | boolean hasStartBtn = false; |
| | | boolean hasStopBtn = false; |
| | | for (Component child : panel.getComponents()) { |
| | | if (child == originalStartBtn) { |
| | | hasStartBtn = true; |
| | | } |
| | | if (child == originalStopBtn) { |
| | | hasStopBtn = true; |
| | | } |
| | | } |
| | | if (hasStartBtn && hasStopBtn) { |
| | | buttonPanel = panel; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (buttonPanel == null) { |
| | | return; |
| | | } |
| | | |
| | | // ä¿ååå§æé®é¢æ¿åå¸å± |
| | | originalButtonPanel = buttonPanel; |
| | | originalButtonPanelLayout = buttonPanel.getLayout(); |
| | | |
| | | // éèåå§æé® |
| | | if (originalStartBtn != null) { |
| | | originalStartBtn.setVisible(false); |
| | | } |
| | | if (originalStopBtn != null) { |
| | | originalStopBtn.setVisible(false); |
| | | } |
| | | |
| | | // ä¿®æ¹æé®é¢æ¿å¸å±ä¸º3åï¼å éãåéãéåºï¼ |
| | | // åå°æé®é´è·ï¼ç»æé®æ´å¤ç©ºé´æ¾ç¤ºæå |
| | | buttonPanel.setLayout(new GridLayout(1, 3, 10, 0)); |
| | | |
| | | // å建å éæé® |
| | | speedUpBtn = createControlButton("å é", new Color(46, 139, 87)); |
| | | speedUpBtn.addActionListener(e -> speedUp()); |
| | | |
| | | // å建åéæé® |
| | | speedDownBtn = createControlButton("åé", new Color(255, 140, 0)); |
| | | speedDownBtn.addActionListener(e -> speedDown()); |
| | | |
| | | // å建éåºæé® |
| | | exitBtn = createControlButton("éåº", new Color(255, 107, 107)); |
| | | exitBtn.addActionListener(e -> exitNavigationPreview()); |
| | | |
| | | // æ·»å æ°æé®å°æé®é¢æ¿ |
| | | buttonPanel.add(speedUpBtn); |
| | | buttonPanel.add(speedDownBtn); |
| | | buttonPanel.add(exitBtn); |
| | | |
| | | // å·æ°æ¾ç¤º |
| | | buttonPanel.revalidate(); |
| | | buttonPanel.repaint(); |
| | | controlPanel.revalidate(); |
| | | controlPanel.repaint(); |
| | | } |
| | | |
| | | /** |
| | | * å建æ§å¶æé®ï¼ä½¿ç¨buttonset飿 ¼ï¼å°ºå¯¸40x80ï¼ |
| | | */ |
| | | private JButton createControlButton(String text, Color color) { |
| | | // 使ç¨buttonsetå建æé® |
| | | JButton button = buttonset.createStyledButton(text, color); |
| | | |
| | | // 设置æé®å°ºå¯¸ï¼å®½åº¦40åç´ ï¼é«åº¦80åç´ |
| | | Dimension buttonSize = new Dimension(80, 40); |
| | | button.setPreferredSize(buttonSize); |
| | | button.setMinimumSize(buttonSize); |
| | | button.setMaximumSize(buttonSize); |
| | | |
| | | return button; |
| | | } |
| | | |
| | | /** |
| | | * ç§»é¤å¯¼èªé¢è§æé®ï¼æ¢å¤åå§æé®ï¼ |
| | | */ |
| | | private void removeNavigationButtons() { |
| | | if (originalButtonPanel == null) { |
| | | return; |
| | | } |
| | | |
| | | // ç§»é¤å¯¼èªé¢è§æé® |
| | | if (speedUpBtn != null && speedUpBtn.getParent() == originalButtonPanel) { |
| | | originalButtonPanel.remove(speedUpBtn); |
| | | } |
| | | if (speedDownBtn != null && speedDownBtn.getParent() == originalButtonPanel) { |
| | | originalButtonPanel.remove(speedDownBtn); |
| | | } |
| | | if (exitBtn != null && exitBtn.getParent() == originalButtonPanel) { |
| | | originalButtonPanel.remove(exitBtn); |
| | | } |
| | | |
| | | // æ¢å¤åå§å¸å± |
| | | if (originalButtonPanelLayout != null) { |
| | | originalButtonPanel.setLayout(originalButtonPanelLayout); |
| | | } |
| | | |
| | | // æ¢å¤å姿鮿¾ç¤º |
| | | if (originalStartBtn != null) { |
| | | originalStartBtn.setVisible(true); |
| | | if (originalStartBtn.getParent() != originalButtonPanel) { |
| | | originalButtonPanel.add(originalStartBtn); |
| | | } |
| | | } |
| | | if (originalStopBtn != null) { |
| | | originalStopBtn.setVisible(true); |
| | | if (originalStopBtn.getParent() != originalButtonPanel) { |
| | | originalButtonPanel.add(originalStopBtn); |
| | | } |
| | | } |
| | | |
| | | // å·æ°æ¾ç¤º |
| | | originalButtonPanel.revalidate(); |
| | | originalButtonPanel.repaint(); |
| | | |
| | | if (shouye != null && shouye.getControlPanel() != null) { |
| | | shouye.getControlPanel().revalidate(); |
| | | shouye.getControlPanel().repaint(); |
| | | } |
| | | |
| | | // æ¸
空å¼ç¨ |
| | | speedUpBtn = null; |
| | | speedDownBtn = null; |
| | | exitBtn = null; |
| | | originalButtonPanel = null; |
| | | originalButtonPanelLayout = null; |
| | | originalStartBtn = null; |
| | | originalStopBtn = null; |
| | | } |
| | | |
| | | /** |
| | | * å¯å¨å¯¼èªå®æ¶å¨ |
| | | */ |
| | | private void startNavigationTimer() { |
| | | if (navigationTimer != null) { |
| | | navigationTimer.stop(); |
| | | } |
| | | |
| | | navigationTimer = new Timer(TIMER_INTERVAL_MS, new ActionListener() { |
| | | @Override |
| | | public void actionPerformed(ActionEvent e) { |
| | | updateNavigation(); |
| | | } |
| | | }); |
| | | navigationTimer.start(); |
| | | } |
| | | |
| | | /** |
| | | * æ´æ°å¯¼èªç¶æ |
| | | */ |
| | | private void updateNavigation() { |
| | | if (pathPoints == null || pathPoints.size() < 2 || currentPathIndex >= pathPoints.size() - 1) { |
| | | // è·¯å¾å®æ |
| | | stopNavigation(); |
| | | JOptionPane.showMessageDialog(null, "导èªé¢è§å®æ", "æç¤º", JOptionPane.INFORMATION_MESSAGE); |
| | | return; |
| | | } |
| | | |
| | | Point2D.Double currentPos = mower.getPosition(); |
| | | if (currentPos == null) { |
| | | return; |
| | | } |
| | | |
| | | Point2D.Double targetPoint = pathPoints.get(currentPathIndex + 1); |
| | | |
| | | // 计ç®å°ç®æ ç¹çè·ç¦» |
| | | double dx = targetPoint.x - currentPos.x; |
| | | double dy = targetPoint.y - currentPos.y; |
| | | double distance = Math.hypot(dx, dy); |
| | | |
| | | // è®¡ç®æ¯å¸§ç§»å¨çè·ç¦»ï¼ç±³ï¼ |
| | | double moveDistance = currentSpeed * (TIMER_INTERVAL_MS / 1000.0); |
| | | |
| | | if (distance <= moveDistance) { |
| | | // å°è¾¾ç®æ ç¹ï¼ç§»å¨å°ä¸ä¸ä¸ªç¹ |
| | | mower.setPosition(targetPoint.x, targetPoint.y); |
| | | navigationTrack.add(new Point2D.Double(targetPoint.x, targetPoint.y)); |
| | | mapRenderer.addNavigationPreviewTrackPoint(targetPoint); |
| | | |
| | | currentPathIndex++; |
| | | |
| | | // å¦æè¿æä¸ä¸ä¸ªç¹ï¼è®¡ç®æ¹å |
| | | if (currentPathIndex < pathPoints.size() - 1) { |
| | | Point2D.Double nextPoint = pathPoints.get(currentPathIndex + 1); |
| | | double heading = calculateHeading(targetPoint, nextPoint); |
| | | setMowerHeading(heading); |
| | | } |
| | | } else { |
| | | // åç®æ ç¹ç§»å¨ |
| | | // å
æ´æ°æ¹åï¼ç¡®ä¿è½¦å¤´æåç®æ ç¹ |
| | | double heading = calculateHeading(currentPos, targetPoint); |
| | | setMowerHeading(heading); |
| | | |
| | | double ratio = moveDistance / distance; |
| | | double newX = currentPos.x + dx * ratio; |
| | | double newY = currentPos.y + dy * ratio; |
| | | |
| | | mower.setPosition(newX, newY); |
| | | navigationTrack.add(new Point2D.Double(newX, newY)); |
| | | mapRenderer.addNavigationPreviewTrackPoint(new Point2D.Double(newX, newY)); |
| | | } |
| | | |
| | | // æ´æ°é度æ¾ç¤ºå°å°å¾æ¸²æå¨ |
| | | if (mapRenderer != null) { |
| | | mapRenderer.setNavigationPreviewSpeed(currentSpeed); |
| | | } |
| | | |
| | | // æ´æ°å°å¾æ¾ç¤º |
| | | mapRenderer.repaint(); |
| | | |
| | | // æ´æ°é度æ¾ç¤ºï¼å¦æéè¦ï¼ |
| | | updateSpeedDisplay(); |
| | | } |
| | | |
| | | /** |
| | | * 计ç®ä¸¤ç¹ä¹é´çæ¹åè§ï¼åº¦ï¼ |
| | | * 徿 é»è®¤æä¸ï¼åå³æè½¬90度车头æå³ |
| | | * atan2è¿åçè§åº¦ï¼å峿¯0度ï¼å䏿¯90度 |
| | | * éè¦è½¬æ¢ä¸ºå¾æ æè½¬è§åº¦ï¼åå³éè¦90度ï¼åä¸éè¦0度 |
| | | */ |
| | | private double calculateHeading(Point2D.Double from, Point2D.Double to) { |
| | | double dx = to.x - from.x; |
| | | double dy = to.y - from.y; |
| | | // atan2è¿åçè§åº¦ï¼å峿¯0度ï¼å䏿¯90度ï¼åå·¦æ¯180度ï¼å䏿¯-90度ï¼270åº¦ï¼ |
| | | double atan2Angle = Math.toDegrees(Math.atan2(dy, dx)); |
| | | |
| | | // 转æ¢ä¸º0-360度èå´ |
| | | if (atan2Angle < 0) { |
| | | atan2Angle += 360; |
| | | } |
| | | |
| | | // 徿 é»è®¤æä¸ï¼0度ï¼ï¼åå³æè½¬90度车头æå³ |
| | | // æä»¥ï¼è¿å¨æ¹ååå³ï¼0度ï¼â éè¦æè½¬90度 |
| | | // è¿å¨æ¹ååä¸ï¼90度ï¼â éè¦æè½¬0度 |
| | | // è¿å¨æ¹ååå·¦ï¼180度ï¼â éè¦æè½¬270度 |
| | | // è¿å¨æ¹ååä¸ï¼270度ï¼â éè¦æè½¬180度 |
| | | // å
¬å¼ï¼heading = (90 - atan2Angle + 360) % 360 |
| | | double heading = (90.0 - atan2Angle + 360.0) % 360.0; |
| | | |
| | | return heading; |
| | | } |
| | | |
| | | /** |
| | | * 设置å²èæºæ¹å |
| | | */ |
| | | private void setMowerHeading(double headingDegrees) { |
| | | if (mower != null) { |
| | | mower.setHeading(headingDegrees); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * å é |
| | | */ |
| | | private void speedUp() { |
| | | currentSpeed += SPEED_MULTIPLIER; |
| | | updateSpeedDisplay(); |
| | | } |
| | | |
| | | /** |
| | | * åé |
| | | */ |
| | | private void speedDown() { |
| | | if (currentSpeed > 0.1) { // æå°é度0.1ç±³/ç§ |
| | | currentSpeed -= SPEED_MULTIPLIER; |
| | | if (currentSpeed < 0.1) { |
| | | currentSpeed = 0.1; |
| | | } |
| | | } |
| | | updateSpeedDisplay(); |
| | | } |
| | | |
| | | /** |
| | | * æ´æ°é度æ¾ç¤º |
| | | */ |
| | | private void updateSpeedDisplay() { |
| | | // å¯ä»¥å¨å°å¾ä¸æ¾ç¤ºå½åé度 |
| | | // è¿éææ¶ä¸å®ç°ï¼å¦æéè¦å¯ä»¥å¨MapRenderer䏿·»å é度æ¾ç¤º |
| | | } |
| | | |
| | | /** |
| | | * åæ¢å¯¼èª |
| | | */ |
| | | private void stopNavigation() { |
| | | if (navigationTimer != null) { |
| | | navigationTimer.stop(); |
| | | navigationTimer = null; |
| | | } |
| | | isNavigating = false; |
| | | } |
| | | |
| | | /** |
| | | * éåºå¯¼èªé¢è§ |
| | | */ |
| | | public void exitNavigationPreview() { |
| | | stopNavigation(); |
| | | removeNavigationButtons(); |
| | | |
| | | // éè导èªé¢è§æ¨¡å¼æ ç¾ |
| | | if (shouye != null) { |
| | | shouye.setNavigationPreviewLabelVisible(false); |
| | | } |
| | | |
| | | // æ¸
é¤å¯¼èªé¢è§è½¨è¿¹ |
| | | if (mapRenderer != null) { |
| | | mapRenderer.clearNavigationPreviewTrack(); |
| | | mapRenderer.setNavigationPreviewSpeed(0.0); // æ¸
é¤é度æ¾ç¤º |
| | | mapRenderer.repaint(); |
| | | } |
| | | |
| | | // æ¢å¤å°å管çé¡µé¢ |
| | | // 卿¸
空currentDikuaiä¹åä¿åå°åç¼å·ï¼ä½¿ç¨finalåé以便å¨lambdaä¸ä½¿ç¨ |
| | | final String landNumber = (currentDikuai != null) ? currentDikuai.getLandNumber() : null; |
| | | |
| | | isNavigating = false; |
| | | currentDikuai = null; |
| | | |
| | | // 妿æå°åç¼å·ï¼æ¾ç¤ºå°å管çé¡µé¢ |
| | | if (landNumber != null && !landNumber.trim().isEmpty()) { |
| | | SwingUtilities.invokeLater(() -> { |
| | | try { |
| | | Component parent = null; |
| | | if (shouye != null) { |
| | | Window owner = SwingUtilities.getWindowAncestor(shouye); |
| | | if (owner instanceof Component) { |
| | | parent = (Component) owner; |
| | | } else { |
| | | parent = shouye; |
| | | } |
| | | } |
| | | Dikuaiguanli.showDikuaiManagement(parent, landNumber); |
| | | } catch (Exception e) { |
| | | System.err.println("æ¾ç¤ºå°å管ç页é¢å¤±è´¥: " + e.getMessage()); |
| | | e.printStackTrace(); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ£æ¥æ¯å¦æ£å¨å¯¼èªé¢è§ |
| | | */ |
| | | public boolean isNavigating() { |
| | | return isNavigating; |
| | | } |
| | | } |
| | |
| | | return new Point2D.Double(position.x, position.y); |
| | | } |
| | | |
| | | /** |
| | | * 设置å²èæºä½ç½®ï¼ç¨äºå¯¼èªé¢è§çåºæ¯ï¼ |
| | | * @param x Xåæ |
| | | * @param y Yåæ |
| | | */ |
| | | public void setPosition(double x, double y) { |
| | | ensurePosition(); |
| | | position.x = x; |
| | | position.y = y; |
| | | positionValid = true; |
| | | } |
| | | |
| | | /** |
| | | * 设置å²èæºæ¹åï¼ç¨äºå¯¼èªé¢è§çåºæ¯ï¼ |
| | | * @param headingDegrees æ¹åè§åº¦ï¼åº¦ï¼0-360ï¼ |
| | | */ |
| | | public void setHeading(double headingDegrees) { |
| | | double normalized = headingDegrees % 360.0; |
| | | if (normalized < 0) { |
| | | normalized += 360.0; |
| | | } |
| | | this.headingDegrees = normalized; |
| | | } |
| | | |
| | | /** |
| | | * è·åå²èæºæ¹å |
| | | * @return æ¹åè§åº¦ï¼åº¦ï¼0-360ï¼ |
| | | */ |
| | | public double getHeading() { |
| | | return headingDegrees; |
| | | } |
| | | |
| | | public double getWorldRadius(double scale) { |
| | | if (!positionValid) { |
| | | return Double.NaN; |
| | |
| | | * å·¥å
·ç±»ï¼è´è´£ç»å¶å²èå®æè·¯å¾è¦çææï¼å¹¶æä¾ä¸ç³»åå¢å¼ºè°ä¼è½åã |
| | | */ |
| | | public final class gecaolunjing { |
| | | private static final Color COVERAGE_FILL_COLOR = new Color(34, 139, 34, 120); |
| | | private static final Color COVERAGE_BORDER_COLOR = new Color(24, 98, 52, 200); |
| | | private static final Color COVERAGE_PATH_COLOR = new Color(0, 90, 0, 204); |
| | | // 深绿è²ï¼ç¨äºæ¾ç¤ºå²å®çåºå |
| | | private static final Color COVERAGE_FILL_COLOR = new Color(0, 100, 0, 150); // 深绿è²ï¼åéæ |
| | | private static final Color COVERAGE_BORDER_COLOR = new Color(0, 80, 0, 200); // æ´æ·±ç绿è²è¾¹æ¡ |
| | | private static final Color COVERAGE_PATH_COLOR = new Color(0, 90, 0, 204); // è·¯å¾é¢è² |
| | | |
| | | private static final double MIN_WIDTH_METERS = 0.3d; |
| | | private static final CoverageState STATE = new CoverageState(); |
| | |
| | | package lujing; |
| | | |
| | | import java.awt.geom.Line2D; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | import org.locationtech.jts.geom.Coordinate; |
| | | import org.locationtech.jts.geom.Envelope; |
| | | import org.locationtech.jts.geom.Geometry; |
| | | import org.locationtech.jts.geom.GeometryFactory; |
| | | import org.locationtech.jts.geom.LineString; |
| | | import org.locationtech.jts.geom.LinearRing; |
| | | import org.locationtech.jts.geom.MultiLineString; |
| | | import org.locationtech.jts.geom.MultiPolygon; |
| | | import org.locationtech.jts.geom.Polygon; |
| | | import org.locationtech.jts.operation.union.CascadedPolygonUnion; |
| | | import org.locationtech.jts.geom.MultiPolygon; |
| | | |
| | | /** |
| | | * å²èè·¯å¾è§åå®ç¨ç±»ï¼ä¾å
¶ä»é¡¹ç®ç´æ¥è°ç¨ã |
| | | * æä¾å符串å
¥åçå²èè·¯å¾çæè½åï¼å¹¶å°è£
å¿
è¦çè§£æä¸å ä½å¤çé»è¾ã |
| | | * ä¼ååçå²èè·¯å¾è§åç±» |
| | | * ä¿®å¤ï¼è§£å³è·¯å¾è¶
åºå°åè¾¹ççé®é¢ï¼å¢å å®å
¨è¾¹è·è®¡ç®çå¥å£®æ§ã |
| | | */ |
| | | public final class Lunjingguihua { |
| | | |
| | |
| | | throw new IllegalStateException("Utility class"); |
| | | } |
| | | |
| | | /** |
| | | * çæå²èè·¯å¾æ®µå表ã |
| | | * |
| | | * @param polygonCoords å¤è¾¹å½¢è¾¹çåæ ï¼æ ¼å¼å¦ "x1,y1;x2,y2;..."ï¼ç±³ï¼ |
| | | * @param obstaclesCoords éç¢ç©åæ ï¼æ¯æå¤ä¸ªæ¬å·æ®µæåå½¢å®ä¹ï¼ä¾ "(x1,y1;x2,y2)(cx,cy;px,py)" |
| | | * @param mowingWidth å²è宽度å符串ï¼ç±³åä½ï¼å
许ä¿ç两ä½å°æ° |
| | | * @param safetyDistStr å®å
¨è·ç¦»å符串ï¼ç±³åä½ãè·¯å¾å°ä¸è¾¹çåéç¢ç©ä¿ææ¤è·ç¦»ã |
| | | * @param modeStr å²è模å¼ï¼"0"/空为平è¡çº¿ï¼"1" æ "spiral" è¡¨ç¤ºèºææ¨¡å¼ï¼å½åä»
å¹³è¡çº¿å®ç°ï¼ |
| | | * @return è·¯å¾æ®µåè¡¨ï¼æè¡é©¶é¡ºåºæå |
| | | */ |
| | | // 5åæ°æ ¸å¿çææ¹æ³ |
| | | public static List<PathSegment> generatePathSegments(String polygonCoords, |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | |
| | | String modeStr) { |
| | | List<Coordinate> polygon = parseCoordinates(polygonCoords); |
| | | if (polygon.size() < 4) { |
| | | throw new IllegalArgumentException("å¤è¾¹å½¢åæ æ°éä¸è¶³ï¼è³å°éè¦ä¸ä¸ªç¹"); |
| | | throw new IllegalArgumentException("å¤è¾¹å½¢åæ æ°éä¸è¶³"); |
| | | } |
| | | |
| | | double width = parseDoubleOrDefault(mowingWidth, 2.0); |
| | | if (width <= 0) { |
| | | throw new IllegalArgumentException("å²è宽度å¿
é¡»å¤§äº 0"); |
| | | } |
| | | |
| | | // è§£æå®å
¨è·ç¦»ï¼å¦ææªè®¾ç½®ææ æï¼é»è®¤ä¸º NaN (å¨ PlannerCore ä¸å¤çé»è®¤å¼) |
| | | double width = parseDoubleOrDefault(mowingWidth, 0.34); |
| | | // å¦æä¼ å
¥ç©ºï¼è®¾ä¸º NaNï¼å¨ PlannerCore ä¸è¿è¡æºè½è®¡ç® |
| | | double safetyDistance = parseDoubleOrDefault(safetyDistStr, Double.NaN); |
| | | |
| | | List<List<Coordinate>> obstacles = parseObstacles(obstaclesCoords); |
| | |
| | | return planner.generate(); |
| | | } |
| | | |
| | | /** |
| | | * ä¿æååå
¼å®¹çéè½½æ¹æ³ï¼ä¸å¸¦ safeDistanceï¼ä½¿ç¨é»è®¤è®¡ç®ï¼ |
| | | */ |
| | | // 4åæ°éè½½ï¼éé
AddDikuai.java |
| | | public static List<PathSegment> generatePathSegments(String polygonCoords, |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | |
| | | return generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, null, modeStr); |
| | | } |
| | | |
| | | /** |
| | | * éè¿åç¬¦ä¸²åæ°çæå²èè·¯å¾åæ ã |
| | | * |
| | | * @param polygonCoords å¤è¾¹å½¢è¾¹çåæ ï¼æ ¼å¼å¦ "x1,y1;x2,y2;..."ï¼ç±³ï¼ |
| | | * @param obstaclesCoords éç¢ç©åæ ï¼æ¯æå¤ä¸ªæ¬å·æ®µæåå½¢å®ä¹ï¼ä¾ "(x1,y1;x2,y2)(cx,cy;px,py)" |
| | | * @param mowingWidth å²è宽度å符串ï¼ç±³åä½ï¼å
许ä¿ç两ä½å°æ° |
| | | * @param safetyDistStr å®å
¨è·ç¦»å符串ï¼ç±³åä½ã |
| | | * @param modeStr å²è模å¼ï¼"0"/空为平è¡çº¿ï¼"1" æ "spiral" è¡¨ç¤ºèºææ¨¡å¼ï¼å½åä»
å¹³è¡çº¿å®ç°ï¼ |
| | | * @return è¿ç»è·¯å¾åæ å符串ï¼é¡ºåºç´§è·å²èæºè¡è¿è·¯çº¿ |
| | | */ |
| | | // 5åæ°è·¯å¾åç¬¦ä¸²çæ |
| | | public static String generatePathFromStrings(String polygonCoords, |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | |
| | | List<PathSegment> segments = generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, safetyDistStr, modeStr); |
| | | return formatPathSegments(segments); |
| | | } |
| | | |
| | | /** |
| | | * ä¿æååå
¼å®¹çéè½½æ¹æ³ |
| | | */ |
| | | |
| | | // 4åæ°è·¯å¾å符串çæéè½½ |
| | | public static String generatePathFromStrings(String polygonCoords, |
| | | String obstaclesCoords, |
| | | String mowingWidth, |
| | |
| | | return generatePathFromStrings(polygonCoords, obstaclesCoords, mowingWidth, null, modeStr); |
| | | } |
| | | |
| | | /** |
| | | * å°è·¯å¾æ®µå表转æ¢ä¸ºåæ å符串ã |
| | | */ |
| | | public static String formatPathSegments(List<PathSegment> path) { |
| | | if (path == null || path.isEmpty()) { |
| | | return ""; |
| | | } |
| | | if (path == null || path.isEmpty()) return ""; |
| | | StringBuilder sb = new StringBuilder(); |
| | | Coordinate last = null; |
| | | for (PathSegment segment : path) { |
| | | if (!equals2D(last, segment.start)) { |
| | | if (last == null || !equals2D(last, segment.start)) { |
| | | appendPoint(sb, segment.start); |
| | | last = new Coordinate(segment.start); |
| | | } |
| | | if (!equals2D(last, segment.end)) { |
| | | appendPoint(sb, segment.end); |
| | | last = new Coordinate(segment.end); |
| | | } |
| | | appendPoint(sb, segment.end); |
| | | last = segment.end; |
| | | } |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * è§£æåæ å符串ã |
| | | */ |
| | | public static List<Coordinate> parseCoordinates(String s) { |
| | | List<Coordinate> list = new ArrayList<>(); |
| | | if (s == null || s.trim().isEmpty()) { |
| | | return list; |
| | | } |
| | | if (s == null || s.trim().isEmpty()) return list; |
| | | // å¢å¼ºæ£åï¼å¤çå¯è½åå¨çå¤ç§åé符 |
| | | String[] pts = s.split("[;\\s]+"); |
| | | for (String p : pts) { |
| | | String trimmed = p.trim().replace("(", "").replace(")", ""); |
| | | if (trimmed.isEmpty()) { |
| | | continue; |
| | | } |
| | | if (trimmed.isEmpty()) continue; |
| | | String[] xy = trimmed.split("[,ï¼\\s]+"); |
| | | if (xy.length >= 2) { |
| | | try { |
| | | list.add(new Coordinate(Double.parseDouble(xy[0].trim()), |
| | | Double.parseDouble(xy[1].trim()))); |
| | | double x = Double.parseDouble(xy[0].trim()); |
| | | double y = Double.parseDouble(xy[1].trim()); |
| | | // è¿æ»¤æ æåæ |
| | | if (!Double.isNaN(x) && !Double.isNaN(y) && !Double.isInfinite(x) && !Double.isInfinite(y)) { |
| | | list.add(new Coordinate(x, y)); |
| | | } |
| | | } catch (NumberFormatException ex) { |
| | | System.err.println("åæ è§£æå¤±è´¥: " + trimmed); |
| | | // 忽ç¥è§£æé误çç¹ |
| | | } |
| | | } |
| | | } |
| | | // ç¡®ä¿å¤è¾¹å½¢éå |
| | | if (list.size() > 2 && !list.get(0).equals2D(list.get(list.size() - 1))) { |
| | | list.add(new Coordinate(list.get(0))); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | /** |
| | | * è§£æéç¢ç©å符串ï¼å
¼å®¹å¤éç¢ç©ä¸åå½¢å®ä¹ã |
| | | */ |
| | | public static List<List<Coordinate>> parseObstacles(String str) { |
| | | List<List<Coordinate>> obs = new ArrayList<>(); |
| | | if (str == null || str.trim().isEmpty()) { |
| | | return obs; |
| | | } |
| | | if (str == null || str.trim().isEmpty()) return obs; |
| | | java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\(([^)]+)\\)"); |
| | | java.util.regex.Matcher matcher = pattern.matcher(str); |
| | | while (matcher.find()) { |
| | | List<Coordinate> coords = parseCoordinates(matcher.group(1)); |
| | | if (coords.size() >= 3) { |
| | | obs.add(coords); |
| | | } else if (coords.size() == 2) { |
| | | List<Coordinate> circle = approximateCircle(coords.get(0), coords.get(1)); |
| | | if (!circle.isEmpty()) { |
| | | obs.add(circle); |
| | | } |
| | | } |
| | | if (coords.size() >= 3) obs.add(coords); |
| | | } |
| | | if (obs.isEmpty()) { |
| | | List<Coordinate> coords = parseCoordinates(str); |
| | | if (coords.size() >= 3) { |
| | | obs.add(coords); |
| | | } else if (coords.size() == 2) { |
| | | List<Coordinate> circle = approximateCircle(coords.get(0), coords.get(1)); |
| | | if (!circle.isEmpty()) { |
| | | obs.add(circle); |
| | | } |
| | | } |
| | | if (coords.size() >= 3) obs.add(coords); |
| | | } |
| | | return obs; |
| | | } |
| | | |
| | | private static double parseDoubleOrDefault(String value, double defaultValue) { |
| | | if (value == null || value.trim().isEmpty()) { |
| | | return defaultValue; |
| | | } |
| | | if (value == null || value.trim().isEmpty()) return defaultValue; |
| | | try { |
| | | return Double.parseDouble(value.trim()); |
| | | } catch (NumberFormatException ex) { |
| | | throw new IllegalArgumentException("æ ¼å¼ä¸æ£ç¡®: " + value, ex); |
| | | return defaultValue; |
| | | } |
| | | } |
| | | |
| | | private static String normalizeMode(String modeStr) { |
| | | if (modeStr == null) { |
| | | return "parallel"; |
| | | } |
| | | String trimmed = modeStr.trim().toLowerCase(); |
| | | if ("1".equals(trimmed) || "spiral".equals(trimmed)) { |
| | | return "spiral"; |
| | | } |
| | | return "parallel"; |
| | | return (modeStr != null && (modeStr.equals("1") || modeStr.equalsIgnoreCase("spiral"))) ? "spiral" : "parallel"; |
| | | } |
| | | |
| | | private static boolean equals2D(Coordinate a, Coordinate b) { |
| | | if (a == b) { |
| | | return true; |
| | | } |
| | | if (a == null || b == null) { |
| | | return false; |
| | | } |
| | | return a.equals2D(b); |
| | | if (a == b) return true; |
| | | if (a == null || b == null) return false; |
| | | return a.distance(b) < 1e-4; |
| | | } |
| | | |
| | | private static void appendPoint(StringBuilder sb, Coordinate point) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format("%.2f,%.2f", point.x, point.y)); |
| | | if (sb.length() > 0) sb.append(";"); |
| | | sb.append(String.format("%.3f,%.3f", point.x, point.y)); |
| | | } |
| | | |
| | | private static List<Coordinate> approximateCircle(Coordinate center, Coordinate edge) { |
| | | double radius = center.distance(edge); |
| | | if (radius <= 0) { |
| | | return Collections.emptyList(); |
| | | } |
| | | int segments = 36; |
| | | List<Coordinate> circle = new ArrayList<>(segments + 1); |
| | | for (int i = 0; i < segments; i++) { |
| | | double angle = 2 * Math.PI * i / segments; |
| | | double x = center.x + radius * Math.cos(angle); |
| | | double y = center.y + radius * Math.sin(angle); |
| | | circle.add(new Coordinate(x, y)); |
| | | } |
| | | circle.add(new Coordinate(circle.get(0))); |
| | | return circle; |
| | | } |
| | | |
| | | /** |
| | | * è·¯å¾æ®µæ°æ®ç»æã |
| | | */ |
| | | public static final class PathSegment { |
| | | public Coordinate start; |
| | | public Coordinate end; |
| | | public Coordinate start, end; |
| | | public boolean isMowing; |
| | | public boolean isStartPoint; |
| | | public boolean isEndPoint; |
| | | public boolean isStartPoint, isEndPoint; |
| | | |
| | | public PathSegment(Coordinate start, Coordinate end, boolean isMowing) { |
| | | this.start = start; |
| | |
| | | this.isMowing = isMowing; |
| | | } |
| | | |
| | | public void setAsStartPoint() { |
| | | this.isStartPoint = true; |
| | | } |
| | | |
| | | public void setAsEndPoint() { |
| | | this.isEndPoint = true; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return String.format("PathSegment[(%.2f,%.2f)->(%.2f,%.2f) mowing=%b start=%b end=%b]", |
| | | start.x, start.y, end.x, end.y, isMowing, isStartPoint, isEndPoint); |
| | | } |
| | | public void setAsStartPoint() { this.isStartPoint = true; } |
| | | public void setAsEndPoint() { this.isEndPoint = true; } |
| | | } |
| | | |
| | | /** |
| | | * å
鍿 ¸å¿è§åå¨ï¼å®ç°ä¸ MowingPathPlanner çæçé»è¾ã |
| | | */ |
| | | static final class PlannerCore { |
| | | public static final class PlannerCore { |
| | | private final List<Coordinate> polygon; |
| | | private final double width; |
| | | private final double safetyDistance; // æ°å¢å®å
¨è·ç¦»å段 |
| | | private final double safetyDistance; |
| | | private final String mode; |
| | | private final List<List<Coordinate>> obstacles; |
| | | private final GeometryFactory gf = new GeometryFactory(); |
| | | |
| | | PlannerCore(List<Coordinate> polygon, double width, double safetyDistance, String mode, List<List<Coordinate>> obstacles) { |
| | | // 1. å
¨åæ°æé 彿° |
| | | public PlannerCore(List<Coordinate> polygon, double width, double safetyDistance, String mode, List<List<Coordinate>> obstacles) { |
| | | this.polygon = polygon; |
| | | this.width = width; |
| | | this.mode = mode; |
| | | this.obstacles = obstacles != null ? obstacles : new ArrayList<>(); |
| | | |
| | | // åå§åå®å
¨è·ç¦»é»è¾ |
| | | if (Double.isNaN(safetyDistance)) { |
| | | // å¦ææªæä¾ï¼ä½¿ç¨é»è®¤å¼ï¼å®½åº¦çä¸å + 0.05ç±³ |
| | | this.safetyDistance = width / 2.0 + 0.05; |
| | | // FIX: å¢å é»è®¤å®å
¨è¾¹è·ãåé»è¾ä¸º width/2 + 0.05ï¼å®¹æé æè¯¯å·®åºçã |
| | | // ç°æ¹ä¸º width/2 + 0.2 (20cmä½é)ï¼ç¡®ä¿å²èæºå®ä½å®å
¨å¨çå
ã |
| | | if (Double.isNaN(safetyDistance) || safetyDistance <= 0) { |
| | | this.safetyDistance = (width / 2.0) + 0.20; |
| | | } else { |
| | | // 妿æä¾äºï¼ä½¿ç¨æä¾çå¼ï¼ä½è³å°è¦ä¿è¯æºå¨ä¸å¿ä¸ç¢°å£ï¼å®½åº¦ä¸åï¼ |
| | | // å
è®¸ç¨æ·è®¾ç½®æ¯ width/2 æ´å¤§ç弿¥è¿ç¦»è¾¹ç |
| | | this.safetyDistance = Math.max(safetyDistance, width / 2.0); |
| | | this.safetyDistance = safetyDistance; |
| | | } |
| | | } |
| | | |
| | | // å
¼å®¹æ§æé 彿° |
| | | PlannerCore(List<Coordinate> polygon, double width, String mode, List<List<Coordinate>> obstacles) { |
| | | |
| | | // 2. 4åæ°æé 彿° |
| | | public PlannerCore(List<Coordinate> polygon, double width, String mode, List<List<Coordinate>> obstacles) { |
| | | this(polygon, width, Double.NaN, mode, obstacles); |
| | | } |
| | | |
| | | List<PathSegment> generate() { |
| | | // 妿æéç¢ç©ï¼ä½¿ç¨å¸¦éç¢ç©é¿è®©çè·¯å¾è§åå¨ |
| | | if (!obstacles.isEmpty()) { |
| | | // 使ç¨è®¡ç®å¥½çå®å
¨è·ç¦» |
| | | ObstaclePathPlanner obstaclePlanner = new ObstaclePathPlanner( |
| | | polygon, width, mode, obstacles, this.safetyDistance); |
| | | return obstaclePlanner.generate(); |
| | | } |
| | | public List<PathSegment> generate() { |
| | | if ("spiral".equals(mode)) return generateSpiralPath(); |
| | | return generateDividedParallelPath(); |
| | | } |
| | | |
| | | public List<PathSegment> generateParallelPath() { |
| | | return generateDividedParallelPath(); |
| | | } |
| | | |
| | | private List<PathSegment> generateDividedParallelPath() { |
| | | List<PathSegment> totalPath = new ArrayList<>(); |
| | | Geometry safeArea = buildSafeArea(); |
| | | |
| | | // 没æéç¢ç©æ¶ä½¿ç¨åæé»è¾ |
| | | if ("spiral".equals(mode)) { |
| | | return generateSpiralPath(); |
| | | } |
| | | return generateParallelPath(); |
| | | } |
| | | if (safeArea == null || safeArea.isEmpty()) return totalPath; |
| | | |
| | | List<PathSegment> generateParallelPath() { |
| | | List<PathSegment> path = new ArrayList<>(); |
| | | Geometry safeArea = buildSafeArea(); |
| | | if (safeArea == null || safeArea.isEmpty()) { |
| | | System.err.println("å®å
¨åºåä¸ºç©ºï¼æ æ³çæè·¯å¾"); |
| | | return path; |
| | | List<Polygon> subRegions = new ArrayList<>(); |
| | | if (safeArea instanceof Polygon) subRegions.add((Polygon) safeArea); |
| | | else if (safeArea instanceof MultiPolygon) { |
| | | for (int i = 0; i < safeArea.getNumGeometries(); i++) { |
| | | subRegions.add((Polygon) safeArea.getGeometryN(i)); |
| | | } |
| | | } |
| | | |
| | | LineSegment longest = findLongestEdge(polygon); |
| | | Vector2D baseDir = new Vector2D(longest.end.x - longest.start.x, |
| | | longest.end.y - longest.start.y).normalize(); |
| | | Vector2D perp = baseDir.rotate90CCW(); |
| | | Vector2D baseStartVec = new Vector2D(longest.start.x, longest.start.y); |
| | | double baseProjection = perp.dot(baseStartVec); |
| | | for (Polygon region : subRegions) { |
| | | if (region.isEmpty()) continue; |
| | | |
| | | double minProj = Double.POSITIVE_INFINITY; |
| | | double maxProj = Double.NEGATIVE_INFINITY; |
| | | Coordinate[] supportCoords = safeArea.getCoordinates(); |
| | | if (supportCoords != null && supportCoords.length > 0) { |
| | | for (Coordinate coord : supportCoords) { |
| | | double projection = perp.dot(new Vector2D(coord.x, coord.y)) - baseProjection; |
| | | if (projection < minProj) { |
| | | minProj = projection; |
| | | } |
| | | if (projection > maxProj) { |
| | | maxProj = projection; |
| | | Vector2D baseDir = calculateMainDirection(region); |
| | | Vector2D perp = baseDir.rotate90CCW(); |
| | | Envelope env = region.getEnvelopeInternal(); |
| | | |
| | | double minProj = Double.MAX_VALUE, maxProj = -Double.MAX_VALUE; |
| | | Coordinate[] coords = region.getCoordinates(); |
| | | for (Coordinate c : coords) { |
| | | double p = perp.dot(new Vector2D(c)); |
| | | minProj = Math.min(minProj, p); |
| | | maxProj = Math.max(maxProj, p); |
| | | } |
| | | |
| | | int lineIdx = 0; |
| | | // ä» minProj + width/2 å¼å§ï¼ç¡®ä¿ç¬¬ä¸æ¡çº¿å¨å®å
¨åºåå
ä¾§ |
| | | for (double d = minProj + width / 2.0; d <= maxProj; d += width) { |
| | | LineString scanLine = createScanLine(d, perp, baseDir, env); |
| | | |
| | | try { |
| | | Geometry intersections = region.intersection(scanLine); |
| | | if (intersections.isEmpty()) continue; |
| | | |
| | | List<LineString> parts = extractLineStrings(intersections); |
| | | |
| | | // æç
§æ«ææ¹åæåºï¼å¤çå¹å¤è¾¹å½¢æéç¢ç© |
| | | parts.sort((a, b) -> Double.compare( |
| | | baseDir.dot(new Vector2D(a.getCoordinateN(0))), |
| | | baseDir.dot(new Vector2D(b.getCoordinateN(0))) |
| | | )); |
| | | |
| | | // è形路å¾ï¼å¥æ°è¡å转 |
| | | if (lineIdx % 2 != 0) Collections.reverse(parts); |
| | | |
| | | for (LineString part : parts) { |
| | | Coordinate[] cs = part.getCoordinates(); |
| | | if (cs.length < 2) continue; |
| | | |
| | | if (lineIdx % 2 != 0) reverseArray(cs); |
| | | |
| | | // ç¡®ä¿ç¹åæ ææ |
| | | totalPath.add(new PathSegment(cs[0], cs[cs.length - 1], true)); |
| | | } |
| | | lineIdx++; |
| | | } catch (Exception e) { |
| | | // å¿½ç¥æå
¶ç½è§çææå¼å¸¸ï¼é²æ¢å´©æº |
| | | } |
| | | } |
| | | } else { |
| | | Envelope env = safeArea.getEnvelopeInternal(); |
| | | minProj = perp.dot(new Vector2D(env.getMinX(), env.getMinY())) - baseProjection; |
| | | maxProj = perp.dot(new Vector2D(env.getMaxX(), env.getMaxY())) - baseProjection; |
| | | } |
| | | if (minProj > maxProj) { |
| | | double tmp = minProj; |
| | | minProj = maxProj; |
| | | maxProj = tmp; |
| | | } |
| | | double first = minProj - width / 2.0; |
| | | |
| | | Geometry originalPoly = createPolygonFromCoordinates(polygon); |
| | | Coordinate lastEnd = null; |
| | | int idx = 0; |
| | | |
| | | for (double offset = first; offset <= maxProj + width / 2.0; offset += width) { |
| | | Line2D.Double raw = createInfiniteLine(longest, perp, offset); |
| | | List<Line2D.Double> segs = clipLineToPolygon(raw, safeArea); |
| | | if (segs.isEmpty()) { |
| | | continue; |
| | | } |
| | | |
| | | List<Line2D.Double> finalSegs = new ArrayList<>(); |
| | | for (Line2D.Double seg : segs) { |
| | | finalSegs.addAll(clipLineToPolygon(seg, originalPoly)); |
| | | } |
| | | if (finalSegs.isEmpty()) { |
| | | continue; |
| | | } |
| | | |
| | | finalSegs.sort((a, b) -> Double.compare(baseDir.dot(midV(a)), baseDir.dot(midV(b)))); |
| | | boolean even = (idx % 2 == 0); |
| | | for (Line2D.Double seg : finalSegs) { |
| | | Coordinate entry = even ? new Coordinate(seg.x1, seg.y1) |
| | | : new Coordinate(seg.x2, seg.y2); |
| | | Coordinate exit = even ? new Coordinate(seg.x2, seg.y2) |
| | | : new Coordinate(seg.x1, seg.y1); |
| | | |
| | | if (lastEnd != null && lastEnd.distance(entry) > 1e-3) { |
| | | path.add(new PathSegment(lastEnd, entry, false)); |
| | | } |
| | | |
| | | PathSegment mowingSeg = new PathSegment(entry, exit, true); |
| | | if (path.isEmpty()) { |
| | | mowingSeg.setAsStartPoint(); |
| | | } |
| | | path.add(mowingSeg); |
| | | lastEnd = exit; |
| | | } |
| | | idx++; |
| | | } |
| | | |
| | | if (!path.isEmpty()) { |
| | | path.get(path.size() - 1).setAsEndPoint(); |
| | | } |
| | | |
| | | postProcess(path); |
| | | return path; |
| | | } |
| | | |
| | | List<PathSegment> generateSpiralPath() { |
| | | Geometry safeArea = buildSafeArea(); |
| | | if (safeArea == null || safeArea.isEmpty()) { |
| | | System.err.println("å®å
¨åºåä¸ºç©ºï¼æ æ³çæèºæè·¯å¾"); |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | List<PathSegment> spiral = luoxuan.generateSpiralPath(safeArea, width); |
| | | if (spiral.isEmpty()) { |
| | | return spiral; |
| | | } |
| | | |
| | | postProcess(spiral); |
| | | PathSegment firstMowing = null; |
| | | PathSegment endCandidate = null; |
| | | for (int i = 0; i < spiral.size(); i++) { |
| | | PathSegment seg = spiral.get(i); |
| | | if (seg != null && seg.isMowing) { |
| | | if (firstMowing == null) { |
| | | firstMowing = seg; |
| | | } |
| | | endCandidate = seg; |
| | | } |
| | | } |
| | | if (firstMowing != null) { |
| | | firstMowing.setAsStartPoint(); |
| | | } |
| | | if (endCandidate != null && endCandidate != firstMowing) { |
| | | endCandidate.setAsEndPoint(); |
| | | } |
| | | return spiral; |
| | | return markStartEnd(totalPath); |
| | | } |
| | | |
| | | private Geometry buildSafeArea() { |
| | | try { |
| | | Coordinate[] coords = polygon.toArray(new Coordinate[0]); |
| | | if (!coords[0].equals2D(coords[coords.length - 1])) { |
| | | coords = Arrays.copyOf(coords, coords.length + 1); |
| | | coords[coords.length - 1] = coords[0]; |
| | | } |
| | | Polygon origin = gf.createPolygon(gf.createLinearRing(coords)); |
| | | Geometry result = origin; |
| | | Polygon poly = gf.createPolygon(gf.createLinearRing(polygon.toArray(new Coordinate[0]))); |
| | | |
| | | // 1. åå§ä¿®å¤ï¼å¤çèªç¸äº¤ |
| | | if (!poly.isValid()) poly = (Polygon) poly.buffer(0); |
| | | |
| | | // 2. å
缩çæå®å
¨åºå |
| | | Geometry safe = poly.buffer(-safetyDistance); |
| | | |
| | | // 3. äºæ¬¡ä¿®å¤ï¼è´ç¼å²åå¯è½äº§çä¸è§èå ä½ä½ |
| | | if (!safe.isValid()) safe = safe.buffer(0); |
| | | |
| | | if (!obstacles.isEmpty()) { |
| | | List<Geometry> obsGeom = new ArrayList<>(); |
| | | for (List<Coordinate> obs : obstacles) { |
| | | Geometry g = createPolygonFromCoordinates(obs); |
| | | if (g != null && !g.isEmpty()) { |
| | | obsGeom.add(g); |
| | | } |
| | | } |
| | | if (!obsGeom.isEmpty()) { |
| | | Geometry union = CascadedPolygonUnion.union(obsGeom); |
| | | result = origin.difference(union); |
| | | // 4. å¤çéç¢ç© |
| | | for (List<Coordinate> obsCoords : obstacles) { |
| | | if (obsCoords.size() < 3) continue; |
| | | try { |
| | | Polygon obs = gf.createPolygon(gf.createLinearRing(obsCoords.toArray(new Coordinate[0]))); |
| | | if (!obs.isValid()) obs = (Polygon) obs.buffer(0); |
| | | // éç¢ç©å¤æ©å®å
¨è·ç¦» |
| | | safe = safe.difference(obs.buffer(safetyDistance)); |
| | | } catch (Exception e) { |
| | | // 忽ç¥é误çéç¢ç©æ°æ® |
| | | } |
| | | } |
| | | |
| | | // ä¿®æ¹ï¼ä½¿ç¨ä¼ å
¥ç safetyDistance æ¥è¿è¡è¾¹çå
缩 |
| | | // ä¹åæ¯ width / 2.0ï¼ç°å¨ä½¿ç¨ this.safetyDistance |
| | | // è¿ç¡®ä¿äºè·¯å¾è§ååºåä¸è¾¹çä¿æç¨æ·æå®çè·ç¦» |
| | | Geometry shrunk = shrinkStraight(result, this.safetyDistance); |
| | | return shrunk.isEmpty() ? result : shrunk; |
| | | } catch (Exception ex) { |
| | | System.err.println("æå»ºå®å
¨åºå失败: " + ex.getMessage()); |
| | | |
| | | // 5. æç»æ¸
ç |
| | | if (!safe.isValid()) safe = safe.buffer(0); |
| | | return safe; |
| | | } catch (Exception e) { |
| | | // 妿å ä½æå»ºå®å
¨å¤±è´¥ï¼è¿å空 |
| | | return gf.createPolygon(); |
| | | } |
| | | } |
| | | |
| | | private LineSegment findLongestEdge(List<Coordinate> ring) { |
| | | double max = -1.0; |
| | | LineSegment best = null; |
| | | for (int i = 0; i < ring.size() - 1; i++) { |
| | | double d = ring.get(i).distance(ring.get(i + 1)); |
| | | if (d > max) { |
| | | max = d; |
| | | best = new LineSegment(ring.get(i), ring.get(i + 1), i); |
| | | private Vector2D calculateMainDirection(Polygon region) { |
| | | Coordinate[] coords = region.getExteriorRing().getCoordinates(); |
| | | double maxLen = -1; |
| | | Vector2D bestDir = new Vector2D(1, 0); |
| | | |
| | | // å¯»æ¾æé¿è¾¹ä½ä¸ºä¸»æ¹åï¼åå°è½¬å¼¯æ¬¡æ° |
| | | for (int i = 0; i < coords.length - 1; i++) { |
| | | double d = coords[i].distance(coords[i+1]); |
| | | if (d > maxLen && d > 1e-4) { |
| | | maxLen = d; |
| | | bestDir = new Vector2D(coords[i+1].x - coords[i].x, coords[i+1].y - coords[i].y).normalize(); |
| | | } |
| | | } |
| | | return best; |
| | | return bestDir; |
| | | } |
| | | |
| | | private Line2D.Double createInfiniteLine(LineSegment base, Vector2D perp, double offset) { |
| | | Vector2D baseStart = new Vector2D(base.start.x, base.start.y); |
| | | Vector2D baseDir = new Vector2D(base.end.x - base.start.x, |
| | | base.end.y - base.start.y).normalize(); |
| | | Vector2D center = baseStart.add(perp.mul(offset)); |
| | | double ext = 1.5 * diagonalLength(); |
| | | Vector2D p1 = center.sub(baseDir.mul(ext)); |
| | | Vector2D p2 = center.add(baseDir.mul(ext)); |
| | | return new Line2D.Double(p1.x, p1.y, p2.x, p2.y); |
| | | } |
| | | |
| | | private List<Line2D.Double> clipLineToPolygon(Line2D.Double line, Geometry poly) { |
| | | List<Line2D.Double> list = new ArrayList<>(); |
| | | LineString ls = gf.createLineString(new Coordinate[]{ |
| | | new Coordinate(line.x1, line.y1), |
| | | new Coordinate(line.x2, line.y2) |
| | | }); |
| | | Geometry inter = poly.intersection(ls); |
| | | if (inter.isEmpty()) { |
| | | return list; |
| | | } |
| | | |
| | | if (inter instanceof LineString) { |
| | | addCoords((LineString) inter, list); |
| | | } else if (inter instanceof MultiLineString) { |
| | | MultiLineString mls = (MultiLineString) inter; |
| | | for (int i = 0; i < mls.getNumGeometries(); i++) { |
| | | addCoords((LineString) mls.getGeometryN(i), list); |
| | | private List<LineString> extractLineStrings(Geometry geom) { |
| | | List<LineString> list = new ArrayList<>(); |
| | | if (geom instanceof LineString) list.add((LineString) geom); |
| | | else if (geom instanceof MultiLineString) { |
| | | for (int i = 0; i < geom.getNumGeometries(); i++) list.add((LineString) geom.getGeometryN(i)); |
| | | } else if (geom instanceof org.locationtech.jts.geom.GeometryCollection) { |
| | | for (int i = 0; i < geom.getNumGeometries(); i++) { |
| | | if (geom.getGeometryN(i) instanceof LineString) { |
| | | list.add((LineString) geom.getGeometryN(i)); |
| | | } |
| | | } |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | private void addCoords(LineString ls, List<Line2D.Double> bucket) { |
| | | Coordinate[] cs = ls.getCoordinateSequence().toCoordinateArray(); |
| | | for (int i = 0; i < cs.length - 1; i++) { |
| | | bucket.add(new Line2D.Double(cs[i].x, cs[i].y, cs[i + 1].x, cs[i + 1].y)); |
| | | private LineString createScanLine(double dist, Vector2D perp, Vector2D baseDir, Envelope env) { |
| | | // æ©å¤§æ«æçº¿é¿åº¦ï¼ç¡®ä¿è¦çæè½¬åçå¤è¾¹å½¢ |
| | | double size = Math.max(env.getWidth(), env.getHeight()); |
| | | // å¤çéåå
å´ç |
| | | if (size < 1.0) size = 1000.0; |
| | | |
| | | double len = size * 3.0; // 3å尺寸确ä¿è¶³å¤é¿ |
| | | |
| | | // ä¸å¿ç¹è®¡ç®ï¼å¨åç´æ¹åä¸è·ç¦»åç¹ dist çä½ç½® |
| | | Vector2D center = perp.mul(dist); |
| | | |
| | | return gf.createLineString(new Coordinate[]{ |
| | | new Coordinate(center.x + baseDir.x * len, center.y + baseDir.y * len), |
| | | new Coordinate(center.x - baseDir.x * len, center.y - baseDir.y * len) |
| | | }); |
| | | } |
| | | |
| | | private List<PathSegment> markStartEnd(List<PathSegment> path) { |
| | | if (!path.isEmpty()) { |
| | | path.get(0).setAsStartPoint(); |
| | | path.get(path.size() - 1).setAsEndPoint(); |
| | | } |
| | | return path; |
| | | } |
| | | |
| | | private void reverseArray(Coordinate[] arr) { |
| | | for (int i = 0; i < arr.length / 2; i++) { |
| | | Coordinate t = arr[i]; |
| | | arr[i] = arr[arr.length - 1 - i]; |
| | | arr[arr.length - 1 - i] = t; |
| | | } |
| | | } |
| | | |
| | | private Geometry createPolygonFromCoordinates(List<Coordinate> coords) { |
| | | if (coords.size() < 3) { |
| | | return gf.createPolygon(); |
| | | } |
| | | List<Coordinate> closed = new ArrayList<>(coords); |
| | | if (!closed.get(0).equals2D(closed.get(closed.size() - 1))) { |
| | | closed.add(new Coordinate(closed.get(0))); |
| | | } |
| | | LinearRing shell = gf.createLinearRing(closed.toArray(new Coordinate[0])); |
| | | Polygon polygonGeom = gf.createPolygon(shell); |
| | | return polygonGeom.isValid() ? polygonGeom : (Polygon) polygonGeom.buffer(0); |
| | | } |
| | | |
| | | private double diagonalLength() { |
| | | double minX = polygon.stream().mapToDouble(c -> c.x).min().orElse(0); |
| | | double maxX = polygon.stream().mapToDouble(c -> c.x).max().orElse(0); |
| | | double minY = polygon.stream().mapToDouble(c -> c.y).min().orElse(0); |
| | | double maxY = polygon.stream().mapToDouble(c -> c.y).max().orElse(0); |
| | | return Math.hypot(maxX - minX, maxY - minY); |
| | | } |
| | | |
| | | private Vector2D midV(Line2D.Double l) { |
| | | return new Vector2D((l.x1 + l.x2) / 2.0, (l.y1 + l.y2) / 2.0); |
| | | } |
| | | |
| | | private void postProcess(List<PathSegment> path) { |
| | | if (path == null || path.isEmpty()) { |
| | | return; |
| | | } |
| | | List<PathSegment> tmp = new ArrayList<>(path); |
| | | path.clear(); |
| | | Coordinate prevEnd = null; |
| | | for (PathSegment seg : tmp) { |
| | | if (prevEnd != null && seg.start.distance(prevEnd) < 1e-3) { |
| | | seg.start = prevEnd; |
| | | } |
| | | if (!seg.isMowing && !path.isEmpty()) { |
| | | PathSegment last = path.get(path.size() - 1); |
| | | if (!last.isMowing && isCollinear(last.start, last.end, seg.end)) { |
| | | last.end = seg.end; |
| | | prevEnd = seg.end; |
| | | continue; |
| | | } |
| | | } |
| | | path.add(seg); |
| | | prevEnd = seg.end; |
| | | } |
| | | } |
| | | |
| | | private boolean isCollinear(Coordinate a, Coordinate b, Coordinate c) { |
| | | double dx1 = b.x - a.x; |
| | | double dy1 = b.y - a.y; |
| | | double dx2 = c.x - b.x; |
| | | double dy2 = c.y - b.y; |
| | | double cross = dx1 * dy2 - dy1 * dx2; |
| | | return Math.abs(cross) < 1e-6; |
| | | } |
| | | |
| | | private Geometry shrinkStraight(Geometry outer, double dist) { |
| | | Geometry buf = outer.buffer(-dist); |
| | | if (buf.isEmpty()) { |
| | | return buf; |
| | | } |
| | | |
| | | Geometry poly = (buf instanceof Polygon) ? buf |
| | | : (buf instanceof MultiPolygon) ? ((MultiPolygon) buf).getGeometryN(0) : null; |
| | | if (!(poly instanceof Polygon)) { |
| | | return buf; |
| | | } |
| | | |
| | | Coordinate[] ring = ((Polygon) poly).getExteriorRing().getCoordinateSequence().toCoordinateArray(); |
| | | List<Coordinate> straight = new ArrayList<>(); |
| | | final double EPS = 1e-3; |
| | | for (int i = 0; i < ring.length - 1; i++) { |
| | | Coordinate prev = (i == 0) ? ring[ring.length - 2] : ring[i - 1]; |
| | | Coordinate curr = ring[i]; |
| | | Coordinate next = ring[i + 1]; |
| | | double cross = Math.abs((next.x - curr.x) * (curr.y - prev.y) |
| | | - (curr.x - prev.x) * (next.y - curr.y)); |
| | | if (cross > EPS) { |
| | | straight.add(curr); |
| | | } |
| | | } |
| | | if (straight.isEmpty()) { |
| | | return buf; |
| | | } |
| | | straight.add(new Coordinate(straight.get(0))); |
| | | return straight.size() < 4 ? gf.createPolygon() |
| | | : gf.createPolygon(gf.createLinearRing(straight.toArray(new Coordinate[0]))); |
| | | } |
| | | List<PathSegment> generateSpiralPath() { return new ArrayList<>(); } |
| | | } |
| | | |
| | | private static final class Vector2D { |
| | | final double x; |
| | | final double y; |
| | | final double x, y; |
| | | Vector2D(double x, double y) { this.x = x; this.y = y; } |
| | | Vector2D(Coordinate c) { this.x = c.x; this.y = c.y; } |
| | | |
| | | Vector2D(double x, double y) { |
| | | this.x = x; |
| | | this.y = y; |
| | | Vector2D normalize() { |
| | | double len = Math.hypot(x, y); |
| | | return len < 1e-9 ? new Vector2D(1, 0) : new Vector2D(x / len, y / len); |
| | | } |
| | | |
| | | Vector2D normalize() { |
| | | double len = Math.hypot(x, y); |
| | | if (len < 1e-12) { |
| | | return new Vector2D(1, 0); |
| | | } |
| | | return new Vector2D(x / len, y / len); |
| | | } |
| | | |
| | | Vector2D rotate90CCW() { |
| | | return new Vector2D(-y, x); |
| | | } |
| | | |
| | | double dot(Vector2D v) { |
| | | return x * v.x + y * v.y; |
| | | } |
| | | |
| | | Vector2D sub(Vector2D v) { |
| | | return new Vector2D(x - v.x, y - v.y); |
| | | } |
| | | |
| | | Vector2D add(Vector2D v) { |
| | | return new Vector2D(x + v.x, y + v.y); |
| | | } |
| | | |
| | | Vector2D mul(double k) { |
| | | return new Vector2D(x * k, y * k); |
| | | } |
| | | } |
| | | |
| | | private static final class LineSegment { |
| | | final Coordinate start; |
| | | final Coordinate end; |
| | | final int index; |
| | | |
| | | LineSegment(Coordinate start, Coordinate end, int index) { |
| | | this.start = start; |
| | | this.end = end; |
| | | this.index = index; |
| | | } |
| | | Vector2D rotate90CCW() { return new Vector2D(-y, x); } |
| | | double dot(Vector2D v) { return x * v.x + y * v.y; } |
| | | Vector2D mul(double k) { return new Vector2D(x * k, y * k); } |
| | | } |
| | | } |
| | |
| | | obstacleList = new ArrayList<>(); |
| | | } |
| | | |
| | | // 夿æ¯å¦æéç¢ç©ï¼åªè¦åå§è¾å
¥æéç¢ç©å
容ï¼å°±ä½¿ç¨ObstaclePathPlanner |
| | | // å³ä½¿è§£æåå表为空ï¼ä¹å°è¯ä½¿ç¨ObstaclePathPlannerï¼å®ä¼å¤ç空éç¢ç©å表çæ
åµï¼ |
| | | boolean hasObstacles = hasObstacleInput && !obstacleList.isEmpty(); |
| | | |
| | | // 妿åå§è¾å
¥æéç¢ç©ä½è§£æå¤±è´¥ï¼ç»åºæç¤º |
| | | if (hasObstacleInput && obstacleList.isEmpty()) { |
| | | if (showMessages) { |
| | | JOptionPane.showMessageDialog(parentComponent, |
| | | "éç¢ç©åæ æ ¼å¼å¯è½ä¸æ£ç¡®ï¼å°å°è¯çæè·¯å¾ãå¦æè·¯å¾ä¸æ£ç¡®ï¼è¯·æ£æ¥éç¢ç©åæ æ ¼å¼ã", |
| | | "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | } |
| | | // ä»ç¶å°è¯ä½¿ç¨ObstaclePathPlannerï¼å³ä½¿éç¢ç©å表为空 |
| | | // è¿æ ·è³å°å¯ä»¥ç¡®ä¿ä½¿ç¨æ£ç¡®çè·¯å¾è§åå¨ |
| | | } |
| | | // 夿æ¯å¦æææçéç¢ç©ï¼åªæå½è§£ææåä¸å表ä¸ä¸ºç©ºæ¶ï¼æè®¤ä¸ºæéç¢ç© |
| | | boolean hasValidObstacles = !obstacleList.isEmpty(); |
| | | |
| | | String generated; |
| | | |
| | | if (!hasObstacles && !hasObstacleInput) { |
| | | // å®å
¨æ²¡æéç¢ç©è¾å
¥æ¶ï¼ä½¿ç¨Lunjingguihuaç±»çæ¹æ³çæè·¯å¾ |
| | | if (!hasValidObstacles) { |
| | | // éç¢ç©åæ ä¸åå¨æä¸ºç©ºæ¶ï¼ä½¿ç¨Lunjingguihuaç±»çæ¹æ³çæè·¯å¾ |
| | | generated = Lunjingguihua.generatePathFromStrings( |
| | | boundary, |
| | | obstacles != null ? obstacles : "", |
| | | plannerWidth, |
| | | null, // safetyDistStrï¼ä½¿ç¨é»è®¤å¼ |
| | | mode |
| | | ); |
| | | } else { |
| | | // æéç¢ç©è¾å
¥æ¶ï¼å³ä½¿è§£æå¤±è´¥ï¼ï¼ä½¿ç¨ObstaclePathPlannerå¤çè·¯å¾çæ |
| | | // æææéç¢ç©æ¶ï¼ä½¿ç¨ObstaclePathPlannerå¤çè·¯å¾çæ |
| | | List<Coordinate> polygon = Lunjingguihua.parseCoordinates(boundary); |
| | | if (polygon.size() < 4) { |
| | | if (showMessages) { |
| | |
| | | return null; |
| | | } |
| | | |
| | | // æ ¹æ®æ¯å¦æéç¢ç©è®¾ç½®ä¸åçå®å
¨è·ç¦» |
| | | double safetyDistance; |
| | | if (!obstacleList.isEmpty()) { |
| | | // æéç¢ç©æ¶ä½¿ç¨å²è宽度çä¸å + 0.05ç±³é¢å¤å®å
¨è·ç¦» |
| | | safetyDistance = widthMeters / 2.0 + 0.05; |
| | | } else { |
| | | // éç¢ç©è§£æå¤±è´¥ä½è¾å
¥åå¨ï¼ä½¿ç¨è¾å°çå®å
¨è·ç¦» |
| | | safetyDistance = 0.01; |
| | | } |
| | | // æéç¢ç©æ¶ä½¿ç¨å²è宽度çä¸å + 0.05ç±³é¢å¤å®å
¨è·ç¦» |
| | | double safetyDistance = widthMeters / 2.0 + 0.05; |
| | | |
| | | ObstaclePathPlanner pathPlanner = new ObstaclePathPlanner( |
| | | polygon, widthMeters, mode, obstacleList, safetyDistance); |
| | |
| | | private JTextField landNumberField; |
| | | private JTextField areaNameField; |
| | | private JComboBox<String> mowingPatternCombo; |
| | | private JSpinner mowingWidthSpinner; |
| | | private JTextField mowingWidthField; // å²èæºå²å宽度 |
| | | private JTextField overlapDistanceField; // ç¸é»è¡éå è·ç¦» |
| | | private JLabel calculatedMowingWidthLabel; // 计ç®åçå²è宽度æ¾ç¤º |
| | | private JPanel previewPanel; |
| | | private Map<String, JPanel> drawingOptionPanels = new HashMap<>(); |
| | | |
| | |
| | | return false; |
| | | } |
| | | if (userTriggered && "handheld".equalsIgnoreCase(type) && !hasConfiguredHandheldMarker()) { |
| | | JOptionPane.showMessageDialog(this, "请å
æ·»å ä¾¿æºæç¹å¨ç¼å·", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | JOptionPane.showMessageDialog(this, "请å
å»ç³»ç»è®¾ç½®æ·»å ä¾¿æºæç¹å¨ç¼å·", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | | } |
| | | |
| | |
| | | settingsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | // å²è模å¼éæ© |
| | | JPanel patternPanel = createFormGroup("å²è模å¼", "éæ©å²èè·¯å¾ççææ¨¡å¼"); |
| | | JPanel patternPanel = createFormGroupWithoutHint("å²è模å¼"); |
| | | mowingPatternCombo = new JComboBox<>(new String[]{"å¹³è¡çº¿", "èºæå¼"}); |
| | | mowingPatternCombo.setFont(new Font("微软é
é»", Font.PLAIN, 16)); |
| | | mowingPatternCombo.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); |
| | |
| | | |
| | | patternPanel.add(mowingPatternCombo); |
| | | settingsPanel.add(patternPanel); |
| | | settingsPanel.add(Box.createRigidArea(new Dimension(0, 20))); |
| | | settingsPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // å²è宽度设置 |
| | | JPanel widthPanel = createFormGroup("å²è宽度", "设置å²èæºå次å²èç宽度"); |
| | | // å²èæºå²å宽度设置 |
| | | JPanel widthPanel = createFormGroupWithoutHint("å²èæºå²å宽度"); |
| | | JPanel widthInputPanel = new JPanel(new BorderLayout()); |
| | | widthInputPanel.setBackground(WHITE); |
| | | widthInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); |
| | | widthInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | SpinnerNumberModel widthModel = new SpinnerNumberModel(40, 20, 60, 1); |
| | | mowingWidthSpinner = new JSpinner(widthModel); |
| | | mowingWidthSpinner.setFont(new Font("微软é
é»", Font.PLAIN, 16)); |
| | | JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) mowingWidthSpinner.getEditor(); |
| | | editor.getTextField().setBorder(BorderFactory.createCompoundBorder( |
| | | mowingWidthField = new JTextField("0.40"); |
| | | mowingWidthField.setFont(new Font("微软é
é»", Font.PLAIN, 16)); |
| | | mowingWidthField.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(BORDER_COLOR, 2), |
| | | BorderFactory.createEmptyBorder(10, 12, 10, 12) |
| | | )); |
| | | |
| | | // æ·»å å¾®è°å¨ç¦ç¹ææ |
| | | mowingWidthSpinner.addFocusListener(new FocusAdapter() { |
| | | // æ·»å ææ¬æ¡ç¦ç¹ææåååçå¬ |
| | | mowingWidthField.addFocusListener(new FocusAdapter() { |
| | | @Override |
| | | public void focusGained(FocusEvent e) { |
| | | editor.getTextField().setBorder(BorderFactory.createCompoundBorder( |
| | | mowingWidthField.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(PRIMARY_COLOR, 2), |
| | | BorderFactory.createEmptyBorder(10, 12, 10, 12) |
| | | )); |
| | |
| | | |
| | | @Override |
| | | public void focusLost(FocusEvent e) { |
| | | editor.getTextField().setBorder(BorderFactory.createCompoundBorder( |
| | | mowingWidthField.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(BORDER_COLOR, 2), |
| | | BorderFactory.createEmptyBorder(10, 12, 10, 12) |
| | | )); |
| | | updateCalculatedMowingWidth(); |
| | | } |
| | | }); |
| | | |
| | | JLabel unitLabel = new JLabel("åç±³"); |
| | | JLabel unitLabel = new JLabel("ç±³"); |
| | | unitLabel.setFont(new Font("微软é
é»", Font.PLAIN, 14)); |
| | | unitLabel.setForeground(LIGHT_TEXT); |
| | | unitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0)); |
| | | |
| | | widthInputPanel.add(mowingWidthSpinner, BorderLayout.CENTER); |
| | | widthInputPanel.add(mowingWidthField, BorderLayout.CENTER); |
| | | widthInputPanel.add(unitLabel, BorderLayout.EAST); |
| | | |
| | | widthPanel.add(widthInputPanel); |
| | | settingsPanel.add(widthPanel); |
| | | settingsPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // ç¸é»è¡éå è·ç¦»è®¾ç½® |
| | | JPanel overlapPanel = createFormGroupWithoutHint("ç¸é»è¡éå è·ç¦»"); |
| | | JPanel overlapInputPanel = new JPanel(new BorderLayout()); |
| | | overlapInputPanel.setBackground(WHITE); |
| | | overlapInputPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); |
| | | overlapInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | overlapDistanceField = new JTextField("0.06"); |
| | | overlapDistanceField.setFont(new Font("微软é
é»", Font.PLAIN, 16)); |
| | | overlapDistanceField.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(BORDER_COLOR, 2), |
| | | BorderFactory.createEmptyBorder(10, 12, 10, 12) |
| | | )); |
| | | |
| | | // æ·»å ææ¬æ¡ç¦ç¹ææåååçå¬ |
| | | overlapDistanceField.addFocusListener(new FocusAdapter() { |
| | | @Override |
| | | public void focusGained(FocusEvent e) { |
| | | overlapDistanceField.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(PRIMARY_COLOR, 2), |
| | | BorderFactory.createEmptyBorder(10, 12, 10, 12) |
| | | )); |
| | | } |
| | | |
| | | @Override |
| | | public void focusLost(FocusEvent e) { |
| | | overlapDistanceField.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(BORDER_COLOR, 2), |
| | | BorderFactory.createEmptyBorder(10, 12, 10, 12) |
| | | )); |
| | | updateCalculatedMowingWidth(); |
| | | } |
| | | }); |
| | | |
| | | JLabel overlapUnitLabel = new JLabel("ç±³"); |
| | | overlapUnitLabel.setFont(new Font("微软é
é»", Font.PLAIN, 14)); |
| | | overlapUnitLabel.setForeground(LIGHT_TEXT); |
| | | overlapUnitLabel.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 0)); |
| | | |
| | | overlapInputPanel.add(overlapDistanceField, BorderLayout.CENTER); |
| | | overlapInputPanel.add(overlapUnitLabel, BorderLayout.EAST); |
| | | |
| | | overlapPanel.add(overlapInputPanel); |
| | | settingsPanel.add(overlapPanel); |
| | | settingsPanel.add(Box.createRigidArea(new Dimension(0, 10))); |
| | | |
| | | // å²è宽度æ¾ç¤ºï¼èªå¨è®¡ç®ï¼ä¸å¯ä¿®æ¹ï¼ |
| | | JPanel calculatedWidthPanel = createFormGroup("å²è宽度", "å²è宽度 = å²èæºå²å宽度 - éå è·ç¦»ï¼èªå¨è®¡ç®ï¼"); |
| | | JPanel calculatedWidthDisplayPanel = new JPanel(new BorderLayout()); |
| | | calculatedWidthDisplayPanel.setBackground(LIGHT_GRAY); |
| | | calculatedWidthDisplayPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 48)); |
| | | calculatedWidthDisplayPanel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | calculatedWidthDisplayPanel.setBorder(BorderFactory.createCompoundBorder( |
| | | BorderFactory.createLineBorder(BORDER_COLOR, 2), |
| | | BorderFactory.createEmptyBorder(10, 12, 10, 12) |
| | | )); |
| | | |
| | | calculatedMowingWidthLabel = new JLabel("0.00 ç±³"); |
| | | calculatedMowingWidthLabel.setFont(new Font("微软é
é»", Font.PLAIN, 16)); |
| | | calculatedMowingWidthLabel.setForeground(TEXT_COLOR); |
| | | calculatedWidthDisplayPanel.add(calculatedMowingWidthLabel, BorderLayout.CENTER); |
| | | |
| | | calculatedWidthPanel.add(calculatedWidthDisplayPanel); |
| | | settingsPanel.add(calculatedWidthPanel); |
| | | settingsPanel.add(Box.createRigidArea(new Dimension(0, 25))); |
| | | |
| | | stepPanel.add(settingsPanel); |
| | | |
| | | // åå§å计ç®åçå²è宽度 |
| | | updateCalculatedMowingWidth(); |
| | | |
| | | JButton generatePathButton = createPrimaryButton("çæå²èè·¯å¾", 16); |
| | | generatePathButton.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | generatePathButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 55)); |
| | |
| | | return stepPanel; |
| | | } |
| | | |
| | | /** |
| | | * æ´æ°è®¡ç®åçå²è宽度æ¾ç¤º |
| | | */ |
| | | private void updateCalculatedMowingWidth() { |
| | | if (calculatedMowingWidthLabel == null || mowingWidthField == null || overlapDistanceField == null) { |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | String widthText = mowingWidthField.getText().trim(); |
| | | String overlapText = overlapDistanceField.getText().trim(); |
| | | |
| | | if (widthText.isEmpty() || overlapText.isEmpty()) { |
| | | calculatedMowingWidthLabel.setText("0.00 ç±³"); |
| | | return; |
| | | } |
| | | |
| | | double bladeWidthMeters = Double.parseDouble(widthText); |
| | | double overlapMeters = Double.parseDouble(overlapText); |
| | | double calculatedWidthMeters = bladeWidthMeters - overlapMeters; |
| | | |
| | | if (calculatedWidthMeters <= 0) { |
| | | calculatedMowingWidthLabel.setText("æ æï¼å²å宽度é大äºéå è·ç¦»ï¼"); |
| | | calculatedMowingWidthLabel.setForeground(ERROR_COLOR); |
| | | } else { |
| | | calculatedMowingWidthLabel.setText(String.format(Locale.US, "%.2f ç±³", calculatedWidthMeters)); |
| | | calculatedMowingWidthLabel.setForeground(TEXT_COLOR); |
| | | } |
| | | } catch (NumberFormatException e) { |
| | | calculatedMowingWidthLabel.setText("0.00 ç±³"); |
| | | } catch (Exception e) { |
| | | calculatedMowingWidthLabel.setText("0.00 ç±³"); |
| | | } |
| | | } |
| | | |
| | | private JPanel createInstructionPanel(String text) { |
| | | JPanel instructionPanel = new JPanel(new BorderLayout()); |
| | | instructionPanel.setBackground(PRIMARY_LIGHT); |
| | |
| | | |
| | | return formGroup; |
| | | } |
| | | |
| | | private JPanel createFormGroupWithoutHint(String label) { |
| | | JPanel formGroup = new JPanel(); |
| | | formGroup.setLayout(new BoxLayout(formGroup, BoxLayout.Y_AXIS)); |
| | | formGroup.setBackground(WHITE); |
| | | formGroup.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | JLabel nameLabel = new JLabel(label); |
| | | nameLabel.setFont(new Font("微软é
é»", Font.BOLD, 16)); |
| | | nameLabel.setForeground(TEXT_COLOR); |
| | | nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); |
| | | |
| | | formGroup.add(nameLabel); |
| | | formGroup.add(Box.createRigidArea(new Dimension(0, 8))); |
| | | |
| | | return formGroup; |
| | | } |
| | | |
| | | private void generateMowingPath() { |
| | | if (!dikuaiData.containsKey("boundaryDrawn")) { |
| | |
| | | String patternDisplay = (String) mowingPatternCombo.getSelectedItem(); |
| | | dikuaiData.put("mowingPattern", patternDisplay); |
| | | |
| | | Object widthObj = mowingWidthSpinner.getValue(); |
| | | if (!(widthObj instanceof Number)) { |
| | | JOptionPane.showMessageDialog(this, "å²è宽度è¾å
¥æ æ", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | String widthText = mowingWidthField.getText().trim(); |
| | | if (widthText.isEmpty()) { |
| | | JOptionPane.showMessageDialog(this, "å²èæºå²å宽度ä¸è½ä¸ºç©º", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("å²è宽度è¾å
¥æ æï¼è¯·éæ°è¾å
¥ã", false); |
| | | showPathGenerationMessage("å²èæºå²å宽度ä¸è½ä¸ºç©ºï¼è¯·éæ°è¾å
¥ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | double widthCm = ((Number) widthObj).doubleValue(); |
| | | if (widthCm <= 0) { |
| | | JOptionPane.showMessageDialog(this, "å²è宽度å¿
须大äº0", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | double bladeWidthMeters; |
| | | try { |
| | | bladeWidthMeters = Double.parseDouble(widthText); |
| | | } catch (NumberFormatException e) { |
| | | JOptionPane.showMessageDialog(this, "å²èæºå²å宽度è¾å
¥æ æ", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("å²è宽度å¿
须大äº0ï¼è¯·éæ°è®¾ç½®ã", false); |
| | | showPathGenerationMessage("å²èæºå²å宽度è¾å
¥æ æï¼è¯·éæ°è¾å
¥ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | dikuaiData.put("mowingWidth", widthObj.toString()); |
| | | if (bladeWidthMeters <= 0) { |
| | | JOptionPane.showMessageDialog(this, "å²èæºå²å宽度å¿
须大äº0", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("å²èæºå²å宽度å¿
须大äº0ï¼è¯·éæ°è®¾ç½®ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | // ä¿åå²èæºå²å宽度ï¼ç±³ï¼ |
| | | dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters)); |
| | | |
| | | // è·åéå è·ç¦» |
| | | String overlapText = overlapDistanceField.getText().trim(); |
| | | if (overlapText.isEmpty()) { |
| | | JOptionPane.showMessageDialog(this, "ç¸é»è¡éå è·ç¦»ä¸è½ä¸ºç©º", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("ç¸é»è¡éå è·ç¦»ä¸è½ä¸ºç©ºï¼è¯·éæ°è¾å
¥ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | double overlapMeters; |
| | | try { |
| | | overlapMeters = Double.parseDouble(overlapText); |
| | | } catch (NumberFormatException e) { |
| | | JOptionPane.showMessageDialog(this, "ç¸é»è¡éå è·ç¦»è¾å
¥æ æ", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("ç¸é»è¡éå è·ç¦»è¾å
¥æ æï¼è¯·éæ°è¾å
¥ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | if (overlapMeters < 0) { |
| | | JOptionPane.showMessageDialog(this, "ç¸é»è¡éå è·ç¦»ä¸è½ä¸ºè´æ°", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("ç¸é»è¡éå è·ç¦»ä¸è½ä¸ºè´æ°ï¼è¯·éæ°è®¾ç½®ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | |
| | | // ä¿åéå è·ç¦»å°å°åæ°æ® |
| | | dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters)); |
| | | |
| | | // 计ç®å®é
使ç¨çå²è宽度ï¼å²å宽度 - éå è·ç¦»ï¼ |
| | | double calculatedWidthMeters = bladeWidthMeters - overlapMeters; |
| | | if (calculatedWidthMeters <= 0) { |
| | | JOptionPane.showMessageDialog(this, "计ç®åçå²è宽度å¿
须大äº0ï¼å²å宽度å¿
须大äºéå è·ç¦»ï¼", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | dikuaiData.remove("plannedPath"); |
| | | showPathGenerationMessage("计ç®åçå²è宽度å¿
须大äº0ï¼è¯·è°æ´å²å宽度æéå è·ç¦»ã", false); |
| | | setPathAvailability(false); |
| | | return; |
| | | } |
| | | |
| | | String widthMeters = String.format(Locale.US, "%.2f", widthCm / 100.0); |
| | | // ä¿åå²è宽度ï¼è®¡ç®åçå¼ï¼è½¬æ¢ä¸ºåç±³åå¨ï¼ä¿æä¸åææ°æ®æ ¼å¼å
¼å®¹ï¼ |
| | | double calculatedWidthCm = calculatedWidthMeters * 100.0; |
| | | dikuaiData.put("mowingWidth", String.format(Locale.US, "%.0f", calculatedWidthCm)); |
| | | |
| | | String widthMeters = String.format(Locale.US, "%.2f", calculatedWidthMeters); |
| | | String plannerMode = resolvePlannerMode(patternDisplay); |
| | | |
| | | try { |
| | |
| | | } |
| | | } |
| | | |
| | | if (mowingWidthSpinner != null) { |
| | | Object widthValue = mowingWidthSpinner.getValue(); |
| | | if (widthValue instanceof Number) { |
| | | int widthInt = ((Number) widthValue).intValue(); |
| | | dikuaiData.put("mowingWidth", Integer.toString(widthInt)); |
| | | } else if (widthValue != null) { |
| | | dikuaiData.put("mowingWidth", widthValue.toString()); |
| | | // ä¿åå²èæºå²å宽度 |
| | | if (mowingWidthField != null) { |
| | | String bladeWidthText = mowingWidthField.getText().trim(); |
| | | if (!bladeWidthText.isEmpty()) { |
| | | try { |
| | | double bladeWidthMeters = Double.parseDouble(bladeWidthText); |
| | | dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters)); |
| | | } catch (NumberFormatException e) { |
| | | // å¦æè§£æå¤±è´¥ï¼ç´æ¥ä¿åææ¬ |
| | | dikuaiData.put("mowingBladeWidth", bladeWidthText); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ä¿åç¸é»è¡éå è·ç¦» |
| | | if (overlapDistanceField != null) { |
| | | String overlapText = overlapDistanceField.getText().trim(); |
| | | if (!overlapText.isEmpty()) { |
| | | try { |
| | | double overlapMeters = Double.parseDouble(overlapText); |
| | | dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters)); |
| | | } catch (NumberFormatException e) { |
| | | // å¦æè§£æå¤±è´¥ï¼ç´æ¥ä¿åææ¬ |
| | | dikuaiData.put("mowingOverlapDistance", overlapText); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 计ç®å¹¶ä¿åå²è宽度ï¼å²å宽度 - éå è·ç¦»ï¼ |
| | | if (mowingWidthField != null && overlapDistanceField != null) { |
| | | try { |
| | | String bladeWidthText = mowingWidthField.getText().trim(); |
| | | String overlapText = overlapDistanceField.getText().trim(); |
| | | if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) { |
| | | double bladeWidthMeters = Double.parseDouble(bladeWidthText); |
| | | double overlapMeters = Double.parseDouble(overlapText); |
| | | double calculatedWidthMeters = bladeWidthMeters - overlapMeters; |
| | | if (calculatedWidthMeters > 0) { |
| | | // 转æ¢ä¸ºåç±³åå¨ï¼ä¿æä¸åææ°æ®æ ¼å¼å
¼å®¹ |
| | | int widthCm = (int) Math.round(calculatedWidthMeters * 100.0); |
| | | dikuaiData.put("mowingWidth", Integer.toString(widthCm)); |
| | | } |
| | | } |
| | | } catch (NumberFormatException e) { |
| | | // å¦æè®¡ç®å¤±è´¥ï¼ä¸ä¿åå²è宽度 |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | case 3: |
| | | dikuaiData.put("mowingPattern", (String) mowingPatternCombo.getSelectedItem()); |
| | | dikuaiData.put("mowingWidth", mowingWidthSpinner.getValue().toString()); |
| | | |
| | | // ä¿åå²èæºå²å宽度 |
| | | if (mowingWidthField != null) { |
| | | String bladeWidthText = mowingWidthField.getText().trim(); |
| | | if (!bladeWidthText.isEmpty()) { |
| | | try { |
| | | double bladeWidthMeters = Double.parseDouble(bladeWidthText); |
| | | dikuaiData.put("mowingBladeWidth", String.format(Locale.US, "%.2f", bladeWidthMeters)); |
| | | } catch (NumberFormatException e) { |
| | | dikuaiData.put("mowingBladeWidth", bladeWidthText); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ä¿åç¸é»è¡éå è·ç¦» |
| | | if (overlapDistanceField != null) { |
| | | String overlapText = overlapDistanceField.getText().trim(); |
| | | if (!overlapText.isEmpty()) { |
| | | try { |
| | | double overlapMeters = Double.parseDouble(overlapText); |
| | | dikuaiData.put("mowingOverlapDistance", String.format(Locale.US, "%.2f", overlapMeters)); |
| | | } catch (NumberFormatException e) { |
| | | dikuaiData.put("mowingOverlapDistance", overlapText); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 计ç®å¹¶ä¿åå²è宽度ï¼å²å宽度 - éå è·ç¦»ï¼ |
| | | if (mowingWidthField != null && overlapDistanceField != null) { |
| | | try { |
| | | String bladeWidthText = mowingWidthField.getText().trim(); |
| | | String overlapText = overlapDistanceField.getText().trim(); |
| | | if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) { |
| | | double bladeWidthMeters = Double.parseDouble(bladeWidthText); |
| | | double overlapMeters = Double.parseDouble(overlapText); |
| | | double calculatedWidthMeters = bladeWidthMeters - overlapMeters; |
| | | if (calculatedWidthMeters > 0) { |
| | | // 转æ¢ä¸ºåç±³åå¨ï¼ä¿æä¸åææ°æ®æ ¼å¼å
¼å®¹ |
| | | int widthCm = (int) Math.round(calculatedWidthMeters * 100.0); |
| | | dikuaiData.put("mowingWidth", Integer.toString(widthCm)); |
| | | } |
| | | } |
| | | } catch (NumberFormatException e) { |
| | | // å¦æè®¡ç®å¤±è´¥ï¼ä¸ä¿åå²è宽度 |
| | | } |
| | | } |
| | | |
| | | if (!hasGeneratedPath()) { |
| | | JOptionPane.showMessageDialog(this, "请å
çæå²èè·¯å¾", "æç¤º", JOptionPane.WARNING_MESSAGE); |
| | | return false; |
| | |
| | | } |
| | | } |
| | | |
| | | if (mowingWidthSpinner != null) { |
| | | String width = data.get("mowingWidth"); |
| | | if (isMeaningfulValue(width)) { |
| | | // æ¢å¤å²èæºå²å宽度ï¼ä¼å
ä»mowingBladeWidthè·åï¼ |
| | | if (mowingWidthField != null) { |
| | | String bladeWidth = data.get("mowingBladeWidth"); |
| | | if (isMeaningfulValue(bladeWidth)) { |
| | | try { |
| | | double parsed = Double.parseDouble(width.trim()); |
| | | SpinnerNumberModel model = (SpinnerNumberModel) mowingWidthSpinner.getModel(); |
| | | int min = ((Number) model.getMinimum()).intValue(); |
| | | int max = ((Number) model.getMaximum()).intValue(); |
| | | int rounded = (int) Math.round(parsed); |
| | | if (rounded < min) { |
| | | rounded = min; |
| | | } else if (rounded > max) { |
| | | rounded = max; |
| | | double bladeWidthMeters = Double.parseDouble(bladeWidth.trim()); |
| | | mowingWidthField.setText(String.format(Locale.US, "%.2f", bladeWidthMeters)); |
| | | } catch (NumberFormatException ignored) { |
| | | // 妿mowingBladeWidthä¸åå¨æè§£æå¤±è´¥ï¼å°è¯ä»mowingWidthæ¢å¤ |
| | | String width = data.get("mowingWidth"); |
| | | if (isMeaningfulValue(width)) { |
| | | try { |
| | | // 妿åå¨çæ¯åç±³ï¼è½¬æ¢ä¸ºç±³æ¾ç¤º |
| | | double parsed = Double.parseDouble(width.trim()); |
| | | // åè®¾å¦æå¼å¤§äº10ï¼åæ¯åç±³ï¼éè¦è½¬æ¢ä¸ºç±³ï¼å¦åå·²ç»æ¯ç±³ |
| | | double widthMeters = parsed > 10 ? parsed / 100.0 : parsed; |
| | | mowingWidthField.setText(String.format(Locale.US, "%.2f", widthMeters)); |
| | | } catch (NumberFormatException ignored2) { |
| | | // ä¿æå½åå¼ |
| | | } |
| | | } |
| | | mowingWidthSpinner.setValue(rounded); |
| | | } |
| | | } else { |
| | | // 妿mowingBladeWidthä¸åå¨ï¼å°è¯ä»mowingWidthæ¢å¤ |
| | | String width = data.get("mowingWidth"); |
| | | if (isMeaningfulValue(width)) { |
| | | try { |
| | | // 妿åå¨çæ¯åç±³ï¼è½¬æ¢ä¸ºç±³æ¾ç¤º |
| | | double parsed = Double.parseDouble(width.trim()); |
| | | // åè®¾å¦æå¼å¤§äº10ï¼åæ¯åç±³ï¼éè¦è½¬æ¢ä¸ºç±³ï¼å¦åå·²ç»æ¯ç±³ |
| | | double widthMeters = parsed > 10 ? parsed / 100.0 : parsed; |
| | | mowingWidthField.setText(String.format(Locale.US, "%.2f", widthMeters)); |
| | | } catch (NumberFormatException ignored) { |
| | | // ä¿æå½åå¼ |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (overlapDistanceField != null) { |
| | | String overlap = data.get("mowingOverlapDistance"); |
| | | if (isMeaningfulValue(overlap)) { |
| | | try { |
| | | double overlapMeters = Double.parseDouble(overlap.trim()); |
| | | overlapDistanceField.setText(String.format(Locale.US, "%.2f", overlapMeters)); |
| | | } catch (NumberFormatException ignored) { |
| | | // ä¿æå½åå¼ |
| | | } |
| | |
| | | if (dikuaiData.containsKey("mowingPattern")) { |
| | | dikuai.setMowingPattern(dikuaiData.get("mowingPattern")); |
| | | } |
| | | |
| | | // ä¿åå²èæºå²å宽度ï¼ä¼å
ä»dikuaiDataè·åï¼å¦åä»TextFieldè·åï¼ |
| | | if (dikuaiData.containsKey("mowingBladeWidth")) { |
| | | dikuai.setMowingBladeWidth(dikuaiData.get("mowingBladeWidth")); |
| | | } else if (mowingWidthField != null) { |
| | | String bladeWidthText = mowingWidthField.getText().trim(); |
| | | if (!bladeWidthText.isEmpty()) { |
| | | try { |
| | | double bladeWidthMeters = Double.parseDouble(bladeWidthText); |
| | | dikuai.setMowingBladeWidth(String.format(Locale.US, "%.2f", bladeWidthMeters)); |
| | | } catch (NumberFormatException e) { |
| | | dikuai.setMowingBladeWidth(bladeWidthText); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ä¿åç¸é»è¡éå è·ç¦»ï¼ä¼å
ä»dikuaiDataè·åï¼å¦åä»TextFieldè·åï¼ |
| | | if (dikuaiData.containsKey("mowingOverlapDistance")) { |
| | | dikuai.setMowingOverlapDistance(dikuaiData.get("mowingOverlapDistance")); |
| | | } else if (overlapDistanceField != null) { |
| | | String overlapText = overlapDistanceField.getText().trim(); |
| | | if (!overlapText.isEmpty()) { |
| | | try { |
| | | double overlapMeters = Double.parseDouble(overlapText); |
| | | dikuai.setMowingOverlapDistance(String.format(Locale.US, "%.2f", overlapMeters)); |
| | | } catch (NumberFormatException e) { |
| | | dikuai.setMowingOverlapDistance(overlapText); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ä¿åå²è宽度ï¼è®¡ç®åçå¼ï¼ä¼å
ä»dikuaiDataè·åï¼ |
| | | if (dikuaiData.containsKey("mowingWidth")) { |
| | | dikuai.setMowingWidth(dikuaiData.get("mowingWidth")); |
| | | } else { |
| | | // å¦ææ²¡æå¨dikuaiDataä¸ï¼åä»TextFieldè®¡ç® |
| | | if (mowingWidthField != null && overlapDistanceField != null) { |
| | | try { |
| | | String bladeWidthText = mowingWidthField.getText().trim(); |
| | | String overlapText = overlapDistanceField.getText().trim(); |
| | | if (!bladeWidthText.isEmpty() && !overlapText.isEmpty()) { |
| | | double bladeWidthMeters = Double.parseDouble(bladeWidthText); |
| | | double overlapMeters = Double.parseDouble(overlapText); |
| | | double calculatedWidthMeters = bladeWidthMeters - overlapMeters; |
| | | if (calculatedWidthMeters > 0) { |
| | | // 转æ¢ä¸ºåç±³åå¨ï¼ä¿æä¸åææ°æ®æ ¼å¼å
¼å®¹ |
| | | int widthCm = (int) Math.round(calculatedWidthMeters * 100.0); |
| | | dikuai.setMowingWidth(Integer.toString(widthCm)); |
| | | } |
| | | } |
| | | } catch (NumberFormatException e) { |
| | | // å¦æè®¡ç®å¤±è´¥ï¼ä¿æåæå¼æä½¿ç¨é»è®¤å¼ |
| | | } |
| | | } |
| | | } |
| | | |
| | | String plannedPath = dikuaiData.get("plannedPath"); |
| | |
| | | private CircleCaptureOverlay circleCaptureOverlay; |
| | | private final List<double[]> circleSampleMarkers = new ArrayList<>(); |
| | | private final List<Point2D.Double> realtimeMowingTrack = new ArrayList<>(); |
| | | private final List<Point2D.Double> navigationPreviewTrack = new ArrayList<>(); // 导èªé¢è§è½¨è¿¹ |
| | | private final Deque<tuowei.TrailSample> idleMowerTrail = new ArrayDeque<>(); |
| | | private final List<Point2D.Double> handheldBoundaryPreview = new ArrayList<>(); |
| | | private double boundaryPreviewMarkerScale = 1.0d; |
| | |
| | | if (!realtimeMowingTrack.isEmpty()) { |
| | | drawRealtimeMowingCoverage(g2d); |
| | | } |
| | | |
| | | // ç»å¶å¯¼èªé¢è§å·²å²åºå |
| | | if (!navigationPreviewTrack.isEmpty()) { |
| | | drawNavigationPreviewCoverage(g2d); |
| | | } |
| | | |
| | | drawMower(g2d); |
| | | |
| | | // ç»å¶å¯¼èªé¢è§é度ï¼å¦ææ£å¨å¯¼èªé¢è§ï¼ |
| | | if (navigationPreviewSpeed > 0 && mower != null && mower.hasValidPosition()) { |
| | | drawNavigationPreviewSpeed(g2d, scale); |
| | | } |
| | | |
| | | // ç»å¶æµé模å¼ï¼å¦ææ¿æ´»ï¼ |
| | | if (measurementModeActive) { |
| | | drawMeasurementMode(g2d, scale); |
| | |
| | | private void drawMower(Graphics2D g2d) { |
| | | mower.draw(g2d, scale); |
| | | } |
| | | |
| | | /** |
| | | * ç»å¶å¯¼èªé¢è§é度ï¼å¨å²èæºå¾æ 䏿¹ï¼ |
| | | */ |
| | | private void drawNavigationPreviewSpeed(Graphics2D g2d, double scale) { |
| | | if (mower == null || !mower.hasValidPosition()) { |
| | | return; |
| | | } |
| | | |
| | | Point2D.Double mowerPos = mower.getPosition(); |
| | | if (mowerPos == null) { |
| | | return; |
| | | } |
| | | |
| | | // å°é度ä»ç±³/ç§è½¬æ¢ä¸ºKM/h |
| | | double speedKmh = navigationPreviewSpeed * 3.6; |
| | | String speedText = String.format("%.1f km/h", speedKmh); |
| | | |
| | | // ä¿ååå§åæ¢ |
| | | AffineTransform originalTransform = g2d.getTransform(); |
| | | |
| | | // å°ä¸çåæ è½¬æ¢ä¸ºå±å¹åæ |
| | | Point2D.Double screenPos = worldToScreen(mowerPos); |
| | | |
| | | // æ¢å¤åå§åæ¢ä»¥ç»å¶æåï¼åºå®å¤§å°ï¼ä¸é缩æ¾ååï¼ |
| | | g2d.setTransform(new AffineTransform()); |
| | | |
| | | // 设置åä½ï¼ä¸ç¼©æ¾æå大å°ä¸è´ï¼11å·åä½ï¼ |
| | | Font labelFont = new Font("微软é
é»", Font.PLAIN, 11); |
| | | g2d.setFont(labelFont); |
| | | FontMetrics metrics = g2d.getFontMetrics(labelFont); |
| | | |
| | | // è®¡ç®æåä½ç½®ï¼å¨å²èæºå¾æ 䏿¹ï¼ |
| | | int textWidth = metrics.stringWidth(speedText); |
| | | int textHeight = metrics.getHeight(); |
| | | int textX = (int)Math.round(screenPos.x - textWidth / 2.0); |
| | | // å¨å²èæºå¾æ 䏿¹ï¼çåºä¸å®é´è· |
| | | // 徿 å¨ä¸çåæ ç³»ä¸ç大å°çº¦ä¸º 48 * 0.8 / scale ç±³ |
| | | // 转æ¢ä¸ºå±å¹åç´ ï¼å¾æ é«åº¦ï¼åç´ ï¼= (48 * 0.8 / scale) * scale = 48 * 0.8 = 38.4 åç´ |
| | | double iconSizePixels = 48.0 * 0.8; // 徿 å¨å±å¹ä¸ç大å°ï¼åç´ ï¼ |
| | | int spacing = 8; // é´è·ï¼åç´ ï¼ |
| | | int textY = (int)Math.round(screenPos.y - iconSizePixels / 2.0 - spacing - textHeight); |
| | | |
| | | // ç»å¶æåèæ¯ï¼åéæç½è²ï¼å¢å¼ºå¯è¯»æ§ï¼ |
| | | g2d.setColor(new Color(255, 255, 255, 200)); |
| | | g2d.fillRoundRect(textX - 4, textY - metrics.getAscent() - 2, textWidth + 8, textHeight + 4, 4, 4); |
| | | |
| | | // ç»å¶æå |
| | | g2d.setColor(new Color(46, 139, 87)); // 使ç¨ä¸»é¢ç»¿è² |
| | | g2d.drawString(speedText, textX, textY); |
| | | |
| | | // æ¢å¤åæ¢ |
| | | g2d.setTransform(originalTransform); |
| | | } |
| | | |
| | | private void drawRealtimeMowingCoverage(Graphics2D g2d) { |
| | | if (realtimeMowingTrack == null || realtimeMowingTrack.size() < 2) { |
| | |
| | | double effectiveWidth = getEffectiveMowerWidthMeters(); |
| | | gecaolunjing.draw(g2d, realtimeMowingTrack, effectiveWidth, boundaryPath); |
| | | } |
| | | |
| | | /** |
| | | * ç»å¶å¯¼èªé¢è§å·²å²åºå |
| | | */ |
| | | private void drawNavigationPreviewCoverage(Graphics2D g2d) { |
| | | if (navigationPreviewTrack == null || navigationPreviewTrack.size() < 2) { |
| | | return; |
| | | } |
| | | |
| | | Path2D.Double boundaryPath = currentBoundaryPath; |
| | | // è·å导èªé¢è§çå²è宽度ï¼ä»daohangyulanè·åï¼ |
| | | double previewWidth = getNavigationPreviewWidth(); |
| | | if (previewWidth <= 0) { |
| | | previewWidth = 0.5; // é»è®¤50åç±³ |
| | | } |
| | | gecaolunjing.draw(g2d, navigationPreviewTrack, previewWidth, boundaryPath); |
| | | } |
| | | |
| | | /** |
| | | * 设置导èªé¢è§è½¨è¿¹ |
| | | */ |
| | | public void setNavigationPreviewTrack(List<Point2D.Double> track) { |
| | | if (track == null) { |
| | | navigationPreviewTrack.clear(); |
| | | } else { |
| | | navigationPreviewTrack.clear(); |
| | | navigationPreviewTrack.addAll(track); |
| | | } |
| | | if (visualizationPanel != null) { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ·»å 导èªé¢è§è½¨è¿¹ç¹ |
| | | */ |
| | | public void addNavigationPreviewTrackPoint(Point2D.Double point) { |
| | | if (point != null && Double.isFinite(point.x) && Double.isFinite(point.y)) { |
| | | navigationPreviewTrack.add(new Point2D.Double(point.x, point.y)); |
| | | if (visualizationPanel != null) { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ¸
é¤å¯¼èªé¢è§è½¨è¿¹ |
| | | */ |
| | | public void clearNavigationPreviewTrack() { |
| | | navigationPreviewTrack.clear(); |
| | | if (visualizationPanel != null) { |
| | | visualizationPanel.repaint(); |
| | | } |
| | | } |
| | | |
| | | private double navigationPreviewWidth = 0.5; // 导èªé¢è§çå²è宽度ï¼ç±³ï¼ |
| | | private double navigationPreviewSpeed = 0.0; // 导èªé¢è§çå²èæºé度ï¼ç±³/ç§ï¼ |
| | | |
| | | /** |
| | | * 设置导èªé¢è§çå²è宽度 |
| | | */ |
| | | public void setNavigationPreviewWidth(double widthMeters) { |
| | | navigationPreviewWidth = widthMeters > 0 ? widthMeters : 0.5; |
| | | } |
| | | |
| | | /** |
| | | * è·å导èªé¢è§çå²è宽度 |
| | | */ |
| | | private double getNavigationPreviewWidth() { |
| | | return navigationPreviewWidth; |
| | | } |
| | | |
| | | /** |
| | | * 设置导èªé¢è§çå²èæºé度ï¼ç±³/ç§ï¼ |
| | | */ |
| | | public void setNavigationPreviewSpeed(double speedMetersPerSecond) { |
| | | navigationPreviewSpeed = speedMetersPerSecond >= 0 ? speedMetersPerSecond : 0.0; |
| | | } |
| | | |
| | | /** |
| | | * è·å导èªé¢è§çå²èæºé度ï¼ç±³/ç§ï¼ |
| | | */ |
| | | private double getNavigationPreviewSpeed() { |
| | | return navigationPreviewSpeed; |
| | | } |
| | | |
| | | private Path2D.Double getRealtimeBoundaryPath() { |
| | | if (realtimeTrackLandNumber == null) { |
| | |
| | | return; |
| | | } |
| | | |
| | | // 设置ç¹ç大å°ï¼ä¸è¾¹ç线宽度ä¸è´ï¼ |
| | | // 设置ç¹ç大å°ï¼è¾¹ç线宽度ç2åï¼ |
| | | // è¾¹ç线宽度ï¼3 / Math.max(0.5, scale) |
| | | double scaleFactor = Math.max(0.5, scale); |
| | | double markerDiameter = 3.0 / scaleFactor; // ä¸è¾¹ç线宽度ä¸è´ |
| | | double boundaryLineWidth = 3.0 / scaleFactor; // è¾¹ç线宽度 |
| | | double markerDiameter = boundaryLineWidth * 2.0; // è¾¹çç¹ç´å¾ = è¾¹ç线宽度ç2å |
| | | double markerRadius = markerDiameter / 2.0; |
| | | |
| | | // 设置åä½ï¼ä¸éç¢ç©åºå·ä¸è´ï¼ä¸é缩æ¾ååï¼ |
| | |
| | | private JLabel statusLabel; |
| | | private JLabel speedLabel; // é度æ¾ç¤ºæ ç¾ |
| | | private JLabel areaNameLabel; |
| | | private JLabel drawingBoundaryLabel; // æ£å¨ç»å¶è¾¹çç¶ææ ç¾ |
| | | private JLabel navigationPreviewLabel; // 导èªé¢è§æ¨¡å¼æ ç¾ |
| | | |
| | | // è¾¹çè¦åç¸å
³ |
| | | private Timer boundaryWarningTimer; // è¾¹çè¦åæ£æ¥å®æ¶å¨ |
| | |
| | | |
| | | // æ·»å é度æ¾ç¤ºæ ç¾ |
| | | speedLabel = new JLabel(""); |
| | | speedLabel.setFont(new Font("微软é
é»", Font.PLAIN, 12)); |
| | | speedLabel.setForeground(Color.GRAY); |
| | | speedLabel.setVisible(false); // é»è®¤éè |
| | | speedLabel.setFont(new Font("微软é
é»", Font.PLAIN, 12)); |
| | | speedLabel.setForeground(Color.GRAY); |
| | | speedLabel.setVisible(false); // é»è®¤éè |
| | | |
| | | // æ£å¨ç»å¶è¾¹çç¶ææ ç¾ |
| | | drawingBoundaryLabel = new JLabel("æ£å¨ç»å¶è¾¹ç"); |
| | | drawingBoundaryLabel.setFont(new Font("微软é
é»", Font.PLAIN, 14)); |
| | | drawingBoundaryLabel.setForeground(new Color(46, 139, 87)); |
| | | drawingBoundaryLabel.setVisible(false); // é»è®¤éè |
| | | |
| | | // 导èªé¢è§æ¨¡å¼æ ç¾ |
| | | navigationPreviewLabel = new JLabel("å½å导èªé¢è§æ¨¡å¼"); |
| | | navigationPreviewLabel.setFont(new Font("微软é
é»", Font.PLAIN, 14)); |
| | | navigationPreviewLabel.setForeground(new Color(46, 139, 87)); |
| | | navigationPreviewLabel.setVisible(false); // é»è®¤éè |
| | | |
| | | // å°ç¶æä¸é度æ¾å¨åä¸è¡ï¼æ¾ç¤ºå¨å°ååç§°ä¸é¢ä¸è¡ |
| | | JPanel statusRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); |
| | | statusRow.setOpaque(false); |
| | | statusRow.add(statusLabel); |
| | | statusRow.add(drawingBoundaryLabel); |
| | | statusRow.add(navigationPreviewLabel); |
| | | statusRow.add(speedLabel); |
| | | |
| | | // 左坹齿 ç¾ä¸ç¶æè¡ï¼ç¡®ä¿å®ä»¬å¨ BoxLayout ä¸é å·¦æ¾ç¤º |
| | |
| | | if (parentWindow != null) { |
| | | // ä½¿ç¨ yaokong å
ä¸ç RemoteControlDialog å®ç° |
| | | remoteDialog = new yaokong.RemoteControlDialog(this, THEME_COLOR, speedLabel); |
| | | } else { |
| | | } else {/* */ |
| | | remoteDialog = new yaokong.RemoteControlDialog((JFrame) null, THEME_COLOR, speedLabel); |
| | | } |
| | | } |
| | |
| | | circleDialogMode = false; |
| | | hideCircleGuidancePanel(); |
| | | enterDrawingControlMode(); |
| | | |
| | | // æ¾ç¤º"æ£å¨ç»å¶è¾¹ç"æç¤º |
| | | if (drawingBoundaryLabel != null) { |
| | | drawingBoundaryLabel.setVisible(true); |
| | | } |
| | | |
| | | boolean enableCircleGuidance = drawingShape != null |
| | | && "circle".equalsIgnoreCase(drawingShape.trim()); |
| | |
| | | activeBoundaryMode = BoundaryCaptureMode.NONE; |
| | | } |
| | | endDrawingCallback = null; |
| | | |
| | | // éè"æ£å¨ç»å¶è¾¹ç"æç¤º |
| | | if (drawingBoundaryLabel != null) { |
| | | drawingBoundaryLabel.setVisible(false); |
| | | } |
| | | |
| | | visualizationPanel.revalidate(); |
| | | visualizationPanel.repaint(); |
| | | setHandheldMowerIconActive(false); |
| | |
| | | return mapRenderer; |
| | | } |
| | | |
| | | /** |
| | | * è·åæ§å¶é¢æ¿ï¼ç¨äºå¯¼èªé¢è§æ¶æ¿æ¢æé®ï¼ |
| | | * @return æ§å¶é¢æ¿ |
| | | */ |
| | | public JPanel getControlPanel() { |
| | | return controlPanel; |
| | | } |
| | | |
| | | /** |
| | | * è·åå¼å§æé®ï¼ç¨äºå¯¼èªé¢è§æ¶éèï¼ |
| | | * @return å¼å§æé® |
| | | */ |
| | | public JButton getStartButton() { |
| | | return startBtn; |
| | | } |
| | | |
| | | /** |
| | | * è·åç»ææé®ï¼ç¨äºå¯¼èªé¢è§æ¶éèï¼ |
| | | * @return ç»ææé® |
| | | */ |
| | | public JButton getStopButton() { |
| | | return stopBtn; |
| | | } |
| | | |
| | | /** |
| | | * 设置导èªé¢è§æ¨¡å¼æ ç¾çæ¾ç¤ºç¶æ |
| | | * @param visible æ¯å¦æ¾ç¤º |
| | | */ |
| | | public void setNavigationPreviewLabelVisible(boolean visible) { |
| | | if (navigationPreviewLabel != null) { |
| | | navigationPreviewLabel.setVisible(visible); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * è·åå¯è§å颿¿å®ä¾ |
| | | */ |
| | | public JPanel getVisualizationPanel() { |
| | | return visualizationPanel; |
| | | } |
| | | |
| | | /** |
| | | * è·å主å
容颿¿å®ä¾ï¼ç¨äºæ·»å æµ®å¨æé®ï¼ |
| | | */ |
| | | public JPanel getMainContentPanel() { |
| | | return mainContentPanel; |
| | | } |
| | | |
| | | |
| | | public void updateCurrentAreaName(String areaName) { |
| | | if (areaNameLabel == null) { |
| | | return; |
| | |
| | | return; |
| | | } |
| | | |
| | | Path2D.Double path = new Path2D.Double(); |
| | | boolean started = false; |
| | | // è¿æ»¤ææç¹ |
| | | List<Point2D.Double> validPoints = new java.util.ArrayList<>(); |
| | | for (Point2D.Double point : previewPoints) { |
| | | if (point == null || !Double.isFinite(point.x) || !Double.isFinite(point.y)) { |
| | | continue; |
| | | } |
| | | if (!started) { |
| | | path.moveTo(point.x, point.y); |
| | | started = true; |
| | | } else { |
| | | path.lineTo(point.x, point.y); |
| | | if (point != null && Double.isFinite(point.x) && Double.isFinite(point.y)) { |
| | | validPoints.add(point); |
| | | } |
| | | } |
| | | |
| | | if (!started) { |
| | | if (validPoints.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | Stroke originalStroke = g2d.getStroke(); |
| | | Color originalColor = g2d.getColor(); |
| | | |
| | | if (previewPoints.size() >= 3) { |
| | | path.closePath(); |
| | | // å建填å
è·¯å¾ï¼å¦æç¹æ°>=3ï¼éè¦éå以填å
ï¼ |
| | | Path2D.Double fillPath = new Path2D.Double(); |
| | | if (validPoints.size() >= 3) { |
| | | fillPath.moveTo(validPoints.get(0).x, validPoints.get(0).y); |
| | | for (int i = 1; i < validPoints.size(); i++) { |
| | | fillPath.lineTo(validPoints.get(i).x, validPoints.get(i).y); |
| | | } |
| | | fillPath.closePath(); |
| | | g2d.setColor(HANDHELD_BOUNDARY_FILL); |
| | | g2d.fill(path); |
| | | g2d.fill(fillPath); |
| | | } |
| | | |
| | | float outlineWidth = 0.1f; |
| | | g2d.setStroke(new BasicStroke(outlineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | g2d.setColor(HANDHELD_BOUNDARY_BORDER); |
| | | g2d.draw(path); |
| | | |
| | | if (validPoints.size() >= 3) { |
| | | // ç¹æ°>=3æ¶ï¼éè¦åå«ç»å¶å®çº¿åè线 |
| | | // ç»å¶å®çº¿é¨åï¼ä»èµ·ç¹ä¾æ¬¡è¿æ¥å°å个ç¹ï¼ä¸éåï¼ä¸å
æ¬èµ·ç¹å°ç»ç¹çç´æ¥è¿çº¿ï¼ |
| | | Path2D.Double solidPath = new Path2D.Double(); |
| | | solidPath.moveTo(validPoints.get(0).x, validPoints.get(0).y); |
| | | // ä»ç¬¬äºä¸ªç¹å¼å§ï¼ä¾æ¬¡è¿æ¥å°æåä¸ä¸ªç¹ï¼å½¢æä¸éåçè·¯å¾ï¼ |
| | | for (int i = 1; i < validPoints.size(); i++) { |
| | | solidPath.lineTo(validPoints.get(i).x, validPoints.get(i).y); |
| | | } |
| | | g2d.setStroke(new BasicStroke(outlineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | g2d.setColor(HANDHELD_BOUNDARY_BORDER); |
| | | g2d.draw(solidPath); |
| | | |
| | | // ç¨è线ç»å¶èµ·ç¹å°ç»ç¹çè¿çº¿ï¼éåçº¿æ®µï¼ |
| | | Point2D.Double startPoint = validPoints.get(0); |
| | | Point2D.Double endPoint = validPoints.get(validPoints.size() - 1); |
| | | |
| | | // å建èçº¿æ ·å¼ï¼æ ¹æ®ç¼©æ¾è°æ´è线模å¼ï¼ |
| | | double effectiveScale = Math.max(0.01d, scale); |
| | | float dashLength = (float) (0.05 / effectiveScale); // è线é¿åº¦é缩æ¾è°æ´ |
| | | float[] dashPattern = new float[]{dashLength, dashLength}; // è线模å¼ï¼å®çº¿ãç©ºç½ |
| | | BasicStroke dashedStroke = new BasicStroke( |
| | | outlineWidth, |
| | | BasicStroke.CAP_ROUND, |
| | | BasicStroke.JOIN_ROUND, |
| | | 1.0f, |
| | | dashPattern, |
| | | 0.0f |
| | | ); |
| | | |
| | | g2d.setStroke(dashedStroke); |
| | | g2d.setColor(HANDHELD_BOUNDARY_BORDER); |
| | | // 使ç¨Path2Dç»å¶èµ·ç¹å°ç»ç¹çè线ï¼ä»¥ä¾¿æ¯ææµ®ç¹åæ |
| | | Path2D.Double dashedLine = new Path2D.Double(); |
| | | dashedLine.moveTo(startPoint.x, startPoint.y); |
| | | dashedLine.lineTo(endPoint.x, endPoint.y); |
| | | g2d.draw(dashedLine); |
| | | } else if (validPoints.size() == 2) { |
| | | // å¦æåªæ2个ç¹ï¼ç´æ¥ç»å¶å®çº¿ |
| | | Path2D.Double simplePath = new Path2D.Double(); |
| | | simplePath.moveTo(validPoints.get(0).x, validPoints.get(0).y); |
| | | simplePath.lineTo(validPoints.get(1).x, validPoints.get(1).y); |
| | | g2d.setStroke(new BasicStroke(outlineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | g2d.setColor(HANDHELD_BOUNDARY_BORDER); |
| | | g2d.draw(simplePath); |
| | | } |
| | | |
| | | if (cachedMarkerPixelDiameter <= 0.0d) { |
| | | double previousPixelDiameter = Math.abs(BASE_WORLD_MARKER_SIZE * scale); |
| | |
| | | return; // æ æ°æ®è¿å |
| | | } // ifç»æ |
| | | |
| | | Path2D path = new Path2D.Double(); // åå»ºè·¯å¾ |
| | | float strokeWidth = (float) (3 / Math.max(0.5, scale)); // 计ç®è¾¹çº¿å®½åº¦ |
| | | |
| | | // å¡«å
åºå |
| | | Path2D fillPath = new Path2D.Double(); // å建填å
è·¯å¾ |
| | | boolean first = true; // é¦ç¹æ è®° |
| | | for (Point2D.Double point : boundary) { // éåç¹ |
| | | if (first) { // é¦ä¸ªç¹ |
| | | path.moveTo(point.x, point.y); // ç§»å¨å°å¼å§ç¹ |
| | | fillPath.moveTo(point.x, point.y); // ç§»å¨å°å¼å§ç¹ |
| | | first = false; // æ´æ°æ è®° |
| | | } else { // å
¶ä»ç¹ |
| | | path.lineTo(point.x, point.y); // è¿çº¿å°ä¸ä¸ªç¹ |
| | | fillPath.lineTo(point.x, point.y); // è¿çº¿å°ä¸ä¸ªç¹ |
| | | } // ifç»æ |
| | | } // forç»æ |
| | | path.closePath(); // éåè·¯å¾ |
| | | |
| | | float strokeWidth = (float) (3 / Math.max(0.5, scale)); // 计ç®è¾¹çº¿å®½åº¦ |
| | | g2d.setStroke(new BasicStroke(strokeWidth)); // 设置æè¾¹ |
| | | fillPath.closePath(); // éåè·¯å¾ |
| | | |
| | | g2d.setColor(fillColor); // 设置填å
è² |
| | | g2d.fill(path); // å¡«å
åºå |
| | | g2d.fill(fillPath); // å¡«å
åºå |
| | | |
| | | g2d.setColor(borderColor); // 设置边线é¢è² |
| | | g2d.draw(path); // ç»å¶è¾¹ç |
| | | // ç»å¶è¾¹ç线ï¼å
æ¬èµ·ç¹å°ç»ç¹çè¿æ¥ï¼- å®çº¿ |
| | | if (boundary.size() >= 2) { |
| | | Path2D.Double borderPath = new Path2D.Double(); // å建边çè·¯å¾ |
| | | Point2D.Double firstPoint = boundary.get(0); |
| | | borderPath.moveTo(firstPoint.x, firstPoint.y); // ç§»å¨å°èµ·ç¹ |
| | | for (int i = 1; i < boundary.size(); i++) { // ä»ç¬¬äºä¸ªç¹å°æåä¸ä¸ªç¹ |
| | | Point2D.Double point = boundary.get(i); |
| | | borderPath.lineTo(point.x, point.y); // è¿çº¿ |
| | | } |
| | | // 妿æåä¸ä¸ªç¹ä¸æ¯ç¬¬ä¸ä¸ªç¹ï¼åè¿æ¥å°èµ·ç¹å½¢æéå |
| | | Point2D.Double lastPoint = boundary.get(boundary.size() - 1); |
| | | if (!lastPoint.equals(firstPoint)) { |
| | | borderPath.lineTo(firstPoint.x, firstPoint.y); // è¿æ¥å°èµ·ç¹å½¢æéå |
| | | } |
| | | |
| | | g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // 设置å®çº¿æè¾¹ |
| | | g2d.setColor(borderColor); // 设置边线é¢è² |
| | | g2d.draw(borderPath); // ç»å¶å®æ´è¾¹çï¼å
æ¬èµ·ç¹å°ç»ç¹çè¿æ¥ï¼ |
| | | } |
| | | } // æ¹æ³ç»æ |
| | | } // ç±»ç»æ |
| | |
| | | measurementPoints.clear(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | |
| | | } |
| | | |
| | | double scaleFactor = Math.max(0.5, scale); // 鲿¢è¿å°ç¼©æ¾ |
| | | // è¾¹çç¹ç´å¾ä¸è¾¹ç线宽度ä¸è´ï¼3 / Math.max(0.5, scale) |
| | | double markerDiameter = 3.0 / scaleFactor; // æç¹ç´å¾ï¼ä¸è¾¹ç线宽度ä¸è´ï¼ |
| | | // è¾¹ç线宽度ï¼3 / Math.max(0.5, scale) |
| | | // è¾¹çç¹ç´å¾ = è¾¹ç线宽度ç2å |
| | | double boundaryLineWidth = 3.0 / scaleFactor; // è¾¹ç线宽度 |
| | | double markerDiameter = boundaryLineWidth * 2.0; // æç¹ç´å¾ï¼è¾¹ç线宽度ç2åï¼ |
| | | // åºç¨ç´å¾ç¼©æ¾å å |
| | | if (diameterScale > 0.0 && Double.isFinite(diameterScale)) { |
| | | markerDiameter *= diameterScale; |
| | | } |
| | | double markerRadius = markerDiameter / 2.0; // åå¾ |
| | | |
| | | for (int i = 0; i < effectiveCount; i++) { // éåææç¹ |