张世豪
4 天以前 dc9dce0555beb85d1262893fd5d56747d6a83855
新增了导航预览功能
已添加1个文件
已修改22个文件
2543 ■■■■ 文件已修改
Obstacledge.properties 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basestation.properties 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
device.properties 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dikuai.properties 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bianjie/BoundaryLengthDrawer.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuai.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/Dikuaiguanli.java 186 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/ObstacleManagementPage.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/addzhangaiwu.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dikuai/daohangyulan.java 548 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/gecaoji/Gecaoji.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/gecaoji/gecaolunjing.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/Lunjingguihua.java 701 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/MowingPathGenerationPage.java 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/ObstaclePathPlanner.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhangaiwu/AddDikuai.java 438 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/Shouye.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/adddikuaiyulan.java 78 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/bianjiedrwa.java 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/celiangmoshi.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/pointandnumber.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Obstacledge.properties
@@ -1,19 +1,14 @@
# å‰²è‰æœºåœ°å—障碍物配置文件
# ç”Ÿæˆæ—¶é—´ï¼š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
basestation.properties
@@ -1,7 +1,7 @@
#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
device.properties
@@ -1,29 +1,29 @@
#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
@@ -1,19 +1,21 @@
#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
set.properties
@@ -1,18 +1,18 @@
#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
src/bianjie/BoundaryLengthDrawer.java
@@ -134,3 +134,8 @@
    }
}
src/dikuai/Dikuai.java
@@ -43,6 +43,10 @@
    private String mowingPattern;
    // å‰²è‰å®½åº¦
    private String mowingWidth;
    // å‰²è‰æœºå‰²åˆ€å®½åº¦ï¼ˆç±³ï¼‰ï¼Œé»˜è®¤0.40ç±³
    private String mowingBladeWidth;
    // å‰²è‰é‡å è·ç¦»ï¼ˆç±³ï¼‰ï¼Œé»˜è®¤0.06ç±³
    private String mowingOverlapDistance;
    // å­˜å‚¨å¤šä¸ªåœ°å—的映射表,键为地块编号
    private static Map<String, Dikuai> dikuaiMap = new HashMap<>();
@@ -104,6 +108,8 @@
                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);
            }
@@ -225,6 +231,12 @@
            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;
@@ -259,6 +271,8 @@
            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 {
@@ -410,6 +424,22 @@
        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{" +
src/dikuai/Dikuaiguanli.java
@@ -268,9 +268,7 @@
    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),
            "点击查看/编辑地块边界坐标");
@@ -291,56 +289,60 @@
    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)));
@@ -361,11 +363,16 @@
        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);
@@ -387,6 +394,7 @@
        
        itemPanel.add(labelComp, BorderLayout.WEST);
        itemPanel.add(valueComp, BorderLayout.EAST);
        itemPanel.putClientProperty("titleLabel", labelComp);
        
        return itemPanel;
    }
@@ -425,7 +433,35 @@
        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,加上上下边距)
@@ -445,18 +481,25 @@
            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;
@@ -499,6 +542,24 @@
                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() {
@@ -859,6 +920,31 @@
    }
    /**
     * å¯åŠ¨å¯¼èˆªé¢„è§ˆ
     */
    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) {
@@ -940,6 +1026,7 @@
        dialog.setVisible(true);
    }
    private void generateMowingPath(Dikuai dikuai) {
        if (dikuai == null) {
            return;
@@ -1183,6 +1270,21 @@
        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) {
src/dikuai/ObstacleManagementPage.java
@@ -299,7 +299,17 @@
        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));
@@ -338,6 +348,16 @@
        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);
    }
@@ -366,12 +386,13 @@
        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;
    }
@@ -545,6 +566,9 @@
            return;
        }
        
        // è®¡ç®—障碍物的中心点坐标
        double[] centerCoords = calculateObstacleCenter(obstacle);
        List<Obstacledge.Obstacle> allObstacles = loadObstacles();
        String allObstaclesCoords = buildAllObstaclesCoordinates(allObstacles);
        
@@ -568,11 +592,85 @@
                        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;
@@ -897,3 +995,8 @@
        return value != null && !value.trim().isEmpty() && !"-1".equals(value.trim());
    }
}
src/dikuai/addzhangaiwu.java
@@ -797,7 +797,7 @@
            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) {
src/dikuai/daohangyulan.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,548 @@
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;
    }
}
src/gecaoji/Gecaoji.java
@@ -179,6 +179,38 @@
        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;
src/gecaoji/gecaolunjing.java
@@ -24,9 +24,10 @@
 * å·¥å…·ç±»ï¼šè´Ÿè´£ç»˜åˆ¶å‰²è‰å®Œæˆè·¯å¾„覆盖效果,并提供一系列增强调优能力。
 */
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();
src/lujing/Lunjingguihua.java
@@ -1,25 +1,20 @@
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 {
@@ -27,16 +22,7 @@
        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,
@@ -44,15 +30,11 @@
                                                         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);
@@ -62,9 +44,7 @@
        return planner.generate();
    }
    /**
     * ä¿æŒå‘后兼容的重载方法(不带 safeDistance,使用默认计算)
     */
    // 4参数重载,适配 AddDikuai.java
    public static List<PathSegment> generatePathSegments(String polygonCoords,
                                                         String obstaclesCoords,
                                                         String mowingWidth,
@@ -72,16 +52,7 @@
        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,
@@ -90,10 +61,8 @@
        List<PathSegment> segments = generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, safetyDistStr, modeStr);
        return formatPathSegments(segments);
    }
    /**
     * ä¿æŒå‘后兼容的重载方法
     */
    // 4参数路径字符串生成重载
    public static String generatePathFromStrings(String polygonCoords,
                                                 String obstaclesCoords,
                                                 String mowingWidth,
@@ -101,158 +70,93 @@
        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;
@@ -260,419 +164,228 @@
            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); }
    }
}
src/lujing/MowingPathGenerationPage.java
@@ -477,33 +477,22 @@
                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) {
@@ -513,15 +502,8 @@
                    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);
src/lujing/ObstaclePathPlanner.java
@@ -533,4 +533,8 @@
            }
        }
    }
}
}
src/zhangaiwu/AddDikuai.java
@@ -65,7 +65,9 @@
    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<>();
    
@@ -768,7 +770,7 @@
            return false;
        }
        if (userTriggered && "handheld".equalsIgnoreCase(type) && !hasConfiguredHandheldMarker()) {
            JOptionPane.showMessageDialog(this, "请先添加便携打点器编号", "提示", JOptionPane.WARNING_MESSAGE);
            JOptionPane.showMessageDialog(this, "请先去系统设置添加便携打点器编号", "提示", JOptionPane.WARNING_MESSAGE);
            return false;
        }
@@ -898,7 +900,7 @@
        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));
@@ -930,29 +932,27 @@
        
        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)
                ));
@@ -960,27 +960,97 @@
            
            @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));
@@ -1016,6 +1086,41 @@
        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);
@@ -1066,6 +1171,23 @@
        
        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")) {
@@ -1104,25 +1226,79 @@
        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 {
@@ -1245,13 +1421,51 @@
            }
        }
        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) {
                // å¦‚果计算失败,不保存割草宽度
            }
        }
    }
@@ -1724,7 +1938,53 @@
            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;
@@ -2005,21 +2265,51 @@
            }
        }
        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) {
                    // ä¿æŒå½“前值
                }
@@ -2128,8 +2418,60 @@
        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");
src/zhuye/MapRenderer.java
@@ -85,6 +85,7 @@
    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;
@@ -417,9 +418,19 @@
        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);
@@ -458,6 +469,60 @@
    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) {
@@ -468,6 +533,91 @@
        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) {
@@ -1870,10 +2020,11 @@
            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;
        
        // è®¾ç½®å­—体(与障碍物序号一致,不随缩放变化)
src/zhuye/Shouye.java
@@ -80,6 +80,8 @@
    private JLabel statusLabel;
    private JLabel speedLabel;  // é€Ÿåº¦æ˜¾ç¤ºæ ‡ç­¾
    private JLabel areaNameLabel;
    private JLabel drawingBoundaryLabel;  // æ­£åœ¨ç»˜åˆ¶è¾¹ç•ŒçŠ¶æ€æ ‡ç­¾
    private JLabel navigationPreviewLabel;  // å¯¼èˆªé¢„览模式标签
    
    // è¾¹ç•Œè­¦å‘Šç›¸å…³
    private Timer boundaryWarningTimer;  // è¾¹ç•Œè­¦å‘Šæ£€æŸ¥å®šæ—¶å™¨
@@ -511,14 +513,28 @@
        // æ·»åŠ é€Ÿåº¦æ˜¾ç¤ºæ ‡ç­¾
        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 ä¸­é å·¦æ˜¾ç¤º
@@ -1154,7 +1170,7 @@
            if (parentWindow != null) {
                // ä½¿ç”¨ yaokong åŒ…中的 RemoteControlDialog å®žçް
                remoteDialog = new yaokong.RemoteControlDialog(this, THEME_COLOR, speedLabel);
            } else {
            } else {/*  */
                remoteDialog = new yaokong.RemoteControlDialog((JFrame) null, THEME_COLOR, speedLabel);
            }
        }
@@ -2793,6 +2809,11 @@
        circleDialogMode = false;
        hideCircleGuidancePanel();
        enterDrawingControlMode();
        // æ˜¾ç¤º"正在绘制边界"提示
        if (drawingBoundaryLabel != null) {
            drawingBoundaryLabel.setVisible(true);
        }
        boolean enableCircleGuidance = drawingShape != null
                && "circle".equalsIgnoreCase(drawingShape.trim());
@@ -3497,6 +3518,12 @@
            activeBoundaryMode = BoundaryCaptureMode.NONE;
        }
        endDrawingCallback = null;
        // éšè—"正在绘制边界"提示
        if (drawingBoundaryLabel != null) {
            drawingBoundaryLabel.setVisible(false);
        }
        visualizationPanel.revalidate();
        visualizationPanel.repaint();
        setHandheldMowerIconActive(false);
@@ -3642,6 +3669,55 @@
        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;
src/zhuye/adddikuaiyulan.java
@@ -38,37 +38,81 @@
            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);
src/zhuye/bianjiedrwa.java
@@ -23,24 +23,42 @@
            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); // ç»˜åˆ¶å®Œæ•´è¾¹ç•Œï¼ˆåŒ…括起点到终点的连接)
        }
    } // æ–¹æ³•结束
} // ç±»ç»“束
src/zhuye/celiangmoshi.java
@@ -81,3 +81,8 @@
        measurementPoints.clear();
    }
}
src/zhuye/pointandnumber.java
@@ -34,8 +34,14 @@
        }
    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++) { // éåŽ†æœ‰æ•ˆç‚¹