826220679@qq.com
6 小时以前 7881cef5c3dcea8e6037101db2c3eeb2fd3ba5da
1211
已修改8个文件
568 ■■■■ 文件已修改
Obstacledge.properties 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
dikuai.properties 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
set.properties 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/gecaoji/lujingdraw.java 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/MowingPathGenerationPage.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lujing/YixinglujingNoObstacle.java 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhangaiwu/AddDikuai.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/zhuye/MapRenderer.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Obstacledge.properties
@@ -1,5 +1,5 @@
# 割草机地块障碍物配置文件
# 生成时间:2025-12-27T22:43:36.141786300
# 生成时间:2025-12-27T23:31:09.655894400
# 坐标系:WGS84(度分格式)
# ============ 地块基准站配置 ============
dikuai.properties
@@ -1,5 +1,5 @@
#Dikuai Properties
#Sat Dec 27 22:43:36 GMT+08:00 2025
#Sat Dec 27 23:31:09 GMT+08:00 2025
LAND1.intelligentSceneAnalysis=-1
LAND1.mowingSafetyDistance=0.53
LAND1.landArea=577.12
@@ -11,8 +11,8 @@
LAND1.returnPathRawCoordinates=-1
LAND1.boundaryOriginalXY=-1
LAND1.mowingWidth=1.00
LAND1.plannedPath=73.87,49.87;50.78,50.04;41.97,22.44;49.61,19.99;51.85,34.26;66.29,36.70;66.29,21.40;78.09,24.75;73.87,49.87;73.96,49.37;50.62,49.54;50.30,48.54;74.12,48.37;74.29,47.37;49.98,47.54;49.66,46.55;74.46,46.37;74.63,45.37;49.34,45.55;49.02,44.55;74.80,44.37;74.96,43.37;48.71,43.55;48.39,42.55;75.13,42.36;75.30,41.36;48.07,41.56;47.75,40.56;75.47,40.36;75.64,39.36;47.43,39.56;47.11,38.56;75.80,38.36;75.97,37.36;46.79,37.57;66.29,36.43;76.14,36.36;76.31,35.36;66.29,35.43;66.29,34.43;76.48,34.35;76.64,33.35;66.29,33.43;66.29,32.43;76.81,32.35;76.98,31.35;66.29,31.43;66.29,30.43;77.15,30.35;77.32,29.35;66.29,29.43;66.29,28.43;77.48,28.35;77.65,27.35;66.29,27.43;66.29,26.43;77.82,26.34;77.99,25.34;66.29,25.43;66.29,24.43;76.69,24.35;73.25,23.38;66.29,23.43;66.29,22.43;69.81,22.40;66.38,21.43;66.29,21.43;47.85,20.56;49.70,20.54;46.48,36.57;64.75,36.44;59.07,35.48;46.16,35.57;45.84,34.57;53.38,34.52;51.73,33.53;45.52,33.57;45.20,32.58;51.57,32.53;51.42,31.53;44.88,31.58;44.56,30.58;51.26,30.53;51.11,29.53;44.25,29.58;43.93,28.59;50.95,28.54;50.79,27.54;43.61,27.59;43.29,26.59;50.64,26.54;50.48,25.54;42.97,25.59;42.65,24.59;50.32,24.54;50.17,23.54;42.33,23.60;42.02,22.60;50.01,22.54;49.85,21.54;44.66,21.58
LAND1.updateTime=2025-12-27 22\:43\:36
LAND1.plannedPath=73.82,49.86;50.78,49.99,M;41.98,22.49,M;49.63,20.04,M;51.80,34.27,M;66.28,36.75,M;66.34,21.40,M;78.08,24.80,M;73.82,49.86,M;73.87,49.87;73.87,49.87,T;73.87,49.87,T;73.87,49.87,T;73.87,49.87,T;73.96,49.37,T;73.96,49.37,T;73.96,49.37,T;73.96,49.37,T;73.96,49.37,T;73.91,49.36;50.66,49.52,M;50.62,49.54;50.62,49.54,T;50.62,49.54,T;50.62,49.54,T;50.62,49.54,T;50.30,48.54,T;50.30,48.54,T;50.30,48.54,T;50.30,48.54,T;50.30,48.54,T;50.35,48.53;74.07,48.36,M;74.12,48.37;74.12,48.37,T;74.12,48.37,T;74.12,48.37,T;74.12,48.37,T;74.29,47.37,T;74.29,47.37,T;74.29,47.37,T;74.29,47.37,T;74.29,47.37,T;74.24,47.36;50.03,47.53,M;49.98,47.54;49.98,47.54,T;49.98,47.54,T;49.98,47.54,T;49.98,47.54,T;49.66,46.55,T;49.66,46.55,T;49.66,46.55,T;49.66,46.55,T;49.66,46.55,T;49.71,46.53;74.41,46.36,M;74.46,46.37;74.46,46.37,T;74.46,46.37,T;74.46,46.37,T;74.46,46.37,T;74.63,45.37,T;74.63,45.37,T;74.63,45.37,T;74.63,45.37,T;74.63,45.37,T;74.58,45.36;49.39,45.53,M;49.34,45.55;49.34,45.55,T;49.34,45.55,T;49.34,45.55,T;49.34,45.55,T;49.02,44.55,T;49.02,44.55,T;49.02,44.55,T;49.02,44.55,T;49.02,44.55,T;49.07,44.53;74.75,44.36,M;74.80,44.37;74.80,44.37,T;74.80,44.37,T;74.80,44.37,T;74.80,44.37,T;74.96,43.37,T;74.96,43.37,T;74.96,43.37,T;74.96,43.37,T;74.96,43.37,T;74.91,43.36;48.75,43.54,M;48.71,43.55;48.71,43.55,T;48.71,43.55,T;48.71,43.55,T;48.71,43.55,T;48.39,42.55,T;48.39,42.55,T;48.39,42.55,T;48.39,42.55,T;48.39,42.55,T;48.43,42.54;75.08,42.36,M;75.13,42.36;75.13,42.36,T;75.13,42.36,T;75.13,42.36,T;75.13,42.36,T;75.30,41.36,T;75.30,41.36,T;75.30,41.36,T;75.30,41.36,T;75.30,41.36,T;75.25,41.35;48.12,41.54,M;48.07,41.56;48.07,41.56,T;48.07,41.56,T;48.07,41.56,T;48.07,41.56,T;47.75,40.56,T;47.75,40.56,T;47.75,40.56,T;47.75,40.56,T;47.75,40.56,T;75.47,40.36,M;75.47,40.36,T;75.47,40.36,T;75.47,40.36,T;75.47,40.36,T;75.64,39.36,T;75.64,39.36,T;75.64,39.36,T;75.64,39.36,T;75.64,39.36,T;75.59,39.35;47.48,39.55,M;47.43,39.56;47.43,39.56,T;47.43,39.56,T;47.43,39.56,T;47.43,39.56,T;47.11,38.56,T;47.11,38.56,T;47.11,38.56,T;47.11,38.56,T;47.11,38.56,T;47.16,38.55;75.75,38.35,M;75.80,38.36;75.80,38.36,T;75.80,38.36,T;75.80,38.36,T;75.80,38.36,T;75.97,37.36,T;75.97,37.36,T;75.97,37.36,T;75.97,37.36,T;75.97,37.36,T;46.79,37.57,M;46.79,37.57,T;46.79,37.57,T;46.79,37.57,T;46.79,37.57,T;41.97,22.44,T;41.97,22.44,T;41.97,22.44,T;41.97,22.44,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;66.29,36.70,T;66.29,36.70,T;66.29,36.70,T;66.29,36.70,T;66.29,36.43,T;66.29,36.43,T;66.29,36.43,T;66.29,36.43,T;66.29,36.43,T;66.34,36.43;76.09,36.35,M;76.14,36.36;76.14,36.36,T;76.14,36.36,T;76.14,36.36,T;76.14,36.36,T;76.31,35.36,T;76.31,35.36,T;76.31,35.36,T;76.31,35.36,T;76.31,35.36,T;66.29,35.43,M;66.29,35.43,T;66.29,35.43,T;66.29,35.43,T;66.29,35.43,T;66.29,34.43,T;66.29,34.43,T;66.29,34.43,T;66.29,34.43,T;66.29,34.43,T;66.34,34.43;76.43,34.35,M;76.48,34.35;76.48,34.35,T;76.48,34.35,T;76.48,34.35,T;76.48,34.35,T;76.64,33.35,T;76.64,33.35,T;76.64,33.35,T;76.64,33.35,T;76.64,33.35,T;76.60,33.34;66.34,33.43,M;66.29,33.43;66.29,33.43,T;66.29,33.43,T;66.29,33.43,T;66.29,33.43,T;66.29,32.43,T;66.29,32.43,T;66.29,32.43,T;66.29,32.43,T;66.29,32.43,T;66.34,32.43;76.76,32.34,M;76.81,32.35;76.81,32.35,T;76.81,32.35,T;76.81,32.35,T;76.81,32.35,T;76.98,31.35,T;76.98,31.35,T;76.98,31.35,T;76.98,31.35,T;76.98,31.35,T;76.93,31.34;66.34,31.43,M;66.29,31.43;66.29,31.43,T;66.29,31.43,T;66.29,31.43,T;66.29,31.43,T;66.29,30.43,T;66.29,30.43,T;66.29,30.43,T;66.29,30.43,T;66.29,30.43,T;66.34,30.43;77.10,30.34,M;77.15,30.35;77.15,30.35,T;77.15,30.35,T;77.15,30.35,T;77.15,30.35,T;77.32,29.35,T;77.32,29.35,T;77.32,29.35,T;77.32,29.35,T;77.32,29.35,T;77.27,29.34;66.34,29.43,M;66.29,29.43;66.29,29.43,T;66.29,29.43,T;66.29,29.43,T;66.29,29.43,T;66.29,28.43,T;66.29,28.43,T;66.29,28.43,T;66.29,28.43,T;66.29,28.43,T;66.34,28.43;77.44,28.34,M;77.48,28.35;77.48,28.35,T;77.48,28.35,T;77.48,28.35,T;77.48,28.35,T;77.65,27.35,T;77.65,27.35,T;77.65,27.35,T;77.65,27.35,T;77.65,27.35,T;77.60,27.34;66.34,27.43,M;66.29,27.43;66.29,27.43,T;66.29,27.43,T;66.29,27.43,T;66.29,27.43,T;66.29,26.43,T;66.29,26.43,T;66.29,26.43,T;66.29,26.43,T;66.29,26.43,T;66.34,26.43;77.77,26.34,M;77.82,26.34;77.82,26.34,T;77.82,26.34,T;77.82,26.34,T;77.82,26.34,T;77.99,25.34,T;77.99,25.34,T;77.99,25.34,T;77.99,25.34,T;77.99,25.34,T;77.94,25.34;66.34,25.43,M;66.29,25.43;66.29,25.43,T;66.29,25.43,T;66.29,25.43,T;66.29,25.43,T;66.29,24.43,T;66.29,24.43,T;66.29,24.43,T;66.29,24.43,T;66.29,24.43,T;66.34,24.43;76.67,24.40,M;76.69,24.35;76.69,24.35,T;76.69,24.35,T;76.69,24.35,T;76.69,24.35,T;73.25,23.38,T;73.25,23.38,T;73.25,23.38,T;73.25,23.38,T;73.25,23.38,T;73.24,23.43;66.34,23.43,M;66.29,23.43;66.29,23.43,T;66.29,23.43,T;66.29,23.43,T;66.29,23.43,T;66.29,22.43,T;66.29,22.43,T;66.29,22.43,T;66.29,22.43,T;66.29,22.43,T;66.34,22.43;69.80,22.45,M;69.81,22.40;69.81,22.40,T;69.81,22.40,T;69.81,22.40,T;69.81,22.40,T;66.38,21.43,T;66.38,21.43,T;66.38,21.43,T;66.38,21.43,T;66.38,21.43,T;66.36,21.47;66.34,21.43,M;66.29,21.43;66.29,21.43,T;66.29,21.43,T;66.29,21.43,T;66.29,21.43,T;66.29,36.70,T;66.29,36.70,T;66.29,36.70,T;66.29,36.70,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;47.85,20.56,T;47.85,20.56,T;47.85,20.56,T;47.85,20.56,T;47.85,20.56,T;47.87,20.60;49.65,20.55,M;49.70,20.54;49.70,20.54,T;49.70,20.54,T;49.70,20.54,T;49.70,20.54,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;41.97,22.44,T;41.97,22.44,T;41.97,22.44,T;41.97,22.44,T;46.48,36.57,T;46.48,36.57,T;46.48,36.57,T;46.48,36.57,T;46.48,36.57,T;46.52,36.55;64.75,36.49,M;64.75,36.44;64.75,36.44,T;64.75,36.44,T;64.75,36.44,T;64.75,36.44,T;59.07,35.48,T;59.07,35.48,T;59.07,35.48,T;59.07,35.48,T;59.07,35.48,T;59.06,35.53;46.20,35.55,M;46.16,35.57;46.16,35.57,T;46.16,35.57,T;46.16,35.57,T;46.16,35.57,T;45.84,34.57,T;45.84,34.57,T;45.84,34.57,T;45.84,34.57,T;45.84,34.57,T;45.89,34.56;53.37,34.57,M;53.38,34.52;53.38,34.52,T;53.38,34.52,T;53.38,34.52,T;53.38,34.52,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;51.73,33.53,T;51.73,33.53,T;51.73,33.53,T;51.73,33.53,T;51.73,33.53,T;51.68,33.54;45.57,33.56,M;45.52,33.57;45.52,33.57,T;45.52,33.57,T;45.52,33.57,T;45.52,33.57,T;45.20,32.58,T;45.20,32.58,T;45.20,32.58,T;45.20,32.58,T;45.20,32.58,T;45.25,32.56;51.53,32.54,M;51.57,32.53;51.57,32.53,T;51.57,32.53,T;51.57,32.53,T;51.57,32.53,T;51.42,31.53,T;51.42,31.53,T;51.42,31.53,T;51.42,31.53,T;51.42,31.53,T;51.37,31.54;44.93,31.56,M;44.88,31.58;44.88,31.58,T;44.88,31.58,T;44.88,31.58,T;44.88,31.58,T;44.56,30.58,T;44.56,30.58,T;44.56,30.58,T;44.56,30.58,T;44.56,30.58,T;44.61,30.57;51.21,30.54,M;51.26,30.53;51.26,30.53,T;51.26,30.53,T;51.26,30.53,T;51.26,30.53,T;51.11,29.53,T;51.11,29.53,T;51.11,29.53,T;51.11,29.53,T;51.11,29.53,T;51.06,29.54;44.29,29.57,M;44.25,29.58;44.25,29.58,T;44.25,29.58,T;44.25,29.58,T;44.25,29.58,T;43.93,28.59,T;43.93,28.59,T;43.93,28.59,T;43.93,28.59,T;43.93,28.59,T;43.97,28.57;50.90,28.54,M;50.95,28.54;50.95,28.54,T;50.95,28.54,T;50.95,28.54,T;50.95,28.54,T;50.79,27.54,T;50.79,27.54,T;50.79,27.54,T;50.79,27.54,T;50.79,27.54,T;50.74,27.54;43.66,27.57,M;43.61,27.59;43.61,27.59,T;43.61,27.59,T;43.61,27.59,T;43.61,27.59,T;43.29,26.59,T;43.29,26.59,T;43.29,26.59,T;43.29,26.59,T;43.29,26.59,T;43.34,26.57;50.59,26.55,M;50.64,26.54;50.64,26.54,T;50.64,26.54,T;50.64,26.54,T;50.64,26.54,T;50.48,25.54,T;50.48,25.54,T;50.48,25.54,T;50.48,25.54,T;50.48,25.54,T;50.43,25.55;43.02,25.58,M;42.97,25.59;42.97,25.59,T;42.97,25.59,T;42.97,25.59,T;42.97,25.59,T;42.65,24.59,T;42.65,24.59,T;42.65,24.59,T;42.65,24.59,T;42.65,24.59,T;42.70,24.58;50.27,24.55,M;50.32,24.54;50.32,24.54,T;50.32,24.54,T;50.32,24.54,T;50.32,24.54,T;50.17,23.54,T;50.17,23.54,T;50.17,23.54,T;50.17,23.54,T;50.17,23.54,T;50.12,23.55;42.38,23.58,M;42.33,23.60;42.33,23.60,T;42.33,23.60,T;42.33,23.60,T;42.33,23.60,T;42.02,22.60,T;42.02,22.60,T;42.02,22.60,T;42.02,22.60,T;42.02,22.60,T;42.06,22.58;49.96,22.55,M;50.01,22.54;50.01,22.54,T;50.01,22.54,T;50.01,22.54,T;50.01,22.54,T;49.85,21.54,T;49.85,21.54,T;49.85,21.54,T;49.85,21.54,T;49.85,21.54,T;49.81,21.55;44.68,21.63,M
LAND1.updateTime=2025-12-27 23\:31\:09
LAND1.baseStationCoordinates=3949.89151752,N,11616.79267501,E
LAND1.boundaryPointInterval=-1
LAND1.createTime=2025-12-23 17\:08\:09
set.properties
@@ -1,5 +1,5 @@
#Mower Configuration Properties - Updated
#Sat Dec 27 22:43:28 GMT+08:00 2025
#Sat Dec 27 23:41:35 GMT+08:00 2025
appVersion=-1
simCardNumber=-1
currentWorkLandNumber=LAND1
@@ -7,13 +7,13 @@
boundaryLengthVisible=false
idleTrailDurationSeconds=60
handheldMarkerId=1872
viewCenterX=-66.17
viewCenterY=-34.20
viewCenterX=-57.93
viewCenterY=-27.08
manualBoundaryDrawingMode=false
mowerId=6258
serialPortName=COM15
serialAutoConnect=true
mapScale=10.32
mapScale=8.60
measurementModeEnabled=false
firmwareVersion=-1
cuttingWidth=200
src/gecaoji/lujingdraw.java
@@ -29,6 +29,16 @@
    private lujingdraw() { // 私有构造防止实例化
    } // 空实现
    // 解析结果:包含点与每个相邻段的作业标注(size=points.size()-1)
    public static final class ParsedPath {
        public final List<Point2D.Double> points;
        public final List<Boolean> isMowingFlags; // 每段对应标注:true=作业段,false=旅行段
        public ParsedPath(List<Point2D.Double> points, List<Boolean> flags) {
            this.points = points;
            this.isMowingFlags = flags;
        }
    }
    /** // 文档注释开始
     * Parse the planned path string (semicolon-separated "x,y" pairs) into a list of points in meters. // 方法说明
     */ // 文档注释结束
@@ -76,6 +86,58 @@
        return points; // 返回结果
    } // 方法结束
    /**
     * 解析带段标注的路径字符串。格式:第一个点 "x,y";后续点可为 "x,y,M" 或 "x,y,T",分别表示上一点至当前点为作业段或旅行段。
     * 兼容旧格式(无第三字段),此时 flags 返回 null。
     */
    public static ParsedPath parsePlannedPathWithFlags(String plannedPath) {
        List<Point2D.Double> points = new ArrayList<>();
        List<Boolean> flags = new ArrayList<>();
        if (plannedPath == null) {
            return new ParsedPath(points, null);
        }
        String normalized = plannedPath.trim();
        if (normalized.isEmpty() || "-1".equals(normalized)) {
            return new ParsedPath(points, null);
        }
        int commentIndex = normalized.indexOf('#');
        if (commentIndex >= 0) {
            normalized = normalized.substring(0, commentIndex).trim();
        }
        if (normalized.isEmpty()) {
            return new ParsedPath(points, null);
        }
        String[] segments = normalized.split(";");
        boolean sawTypeField = false;
        for (int i = 0; i < segments.length; i++) {
            String segment = segments[i];
            if (segment == null) continue;
            String trimmed = segment.trim();
            if (trimmed.isEmpty()) continue;
            String[] parts = trimmed.split(",");
            if (parts.length < 2) continue;
            try {
                double x = Double.parseDouble(parts[0].trim());
                double y = Double.parseDouble(parts[1].trim());
                points.add(new Point2D.Double(x, y));
                if (i > 0) {
                    // 第一个点无标注;后续若存在第三字段则解析
                    if (parts.length >= 3) {
                        String t = parts[2].trim();
                        boolean isMowing = t.equalsIgnoreCase("M") || t.equalsIgnoreCase("m") || t.equals("1") || t.equalsIgnoreCase("WORK");
                        flags.add(isMowing);
                        sawTypeField = true;
                    } else {
                        flags.add(Boolean.TRUE); // 默认作业段,便于向后兼容
                    }
                }
            } catch (NumberFormatException ignored) {
                // 忽略非法坐标
            }
        }
        return new ParsedPath(points, sawTypeField ? flags : null);
    }
    /** // 文档注释开始
     * Draw the planned mowing path. // 绘制路径
     */ // 文档注释结束
@@ -105,26 +167,37 @@
            drawInnerBoundary(g2d, boundaryCoords, safetyDistance, scale);
        }
        
        // 2. 直接绘制路径(不再重新生成)
        Path2D polyline = new Path2D.Double(); // 创建折线
        boolean move = true; // 首段标记
        for (Point2D.Double point : path) { // 遍历点集
            if (move) { // 第一段
                polyline.moveTo(point.x, point.y); // 移动到首点
                move = false; // 更新标记
            } else { // 后续段
                polyline.lineTo(point.x, point.y); // 连线到下一点
            } // if结束
        } // for结束
        // 2. 按段绘制路径(逐段绘制,避免将不相邻段首尾连成越界直线)
        org.locationtech.jts.geom.Geometry innerBoundaryGeom = buildInnerBoundaryGeom(boundaryCoords, safetyDistance);
        for (int i = 1; i < path.size(); i++) {
            Point2D.Double a = path.get(i - 1);
            Point2D.Double b = path.get(i);
        
        g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // 设置圆头圆角描边
        g2d.setColor(MOWING_PATH_COLOR); // 设置作业路径颜色(提香红70%透明度)
        g2d.draw(polyline); // 绘制折线
            Path2D segment = new Path2D.Double();
            segment.moveTo(a.x, a.y);
            segment.lineTo(b.x, b.y);
            boolean inside = true; // 默认按作业段绘制
            if (innerBoundaryGeom != null) {
                inside = isSegmentInsideInnerBoundary(innerBoundaryGeom, a, b);
            }
            if (inside) {
                // 作业段:实线 + 提香红
                g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                g2d.setColor(MOWING_PATH_COLOR);
            } else {
                // 旅行段(或沿边界段):虚线 + 蓝色
                g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f, DASH_PATTERN, 0.0f));
                g2d.setColor(TRAVEL_PATH_COLOR);
            }
            g2d.draw(segment);
        }
        
        Point2D.Double start = path.get(0); // 起点
        Point2D.Double second = path.get(1); // 第二个点
        Point2D.Double second = path.size() > 1 ? path.get(1) : path.get(0); // 第二个点
        Point2D.Double end = path.get(path.size() - 1); // 终点
        Point2D.Double prev = path.get(path.size() - 2); // 倒数第二个点
        Point2D.Double prev = path.size() > 1 ? path.get(path.size() - 2) : path.get(path.size() - 1); // 倒数第二个点
        drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale); // 绘制起点箭头
        drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale); // 绘制终点箭头
@@ -132,6 +205,107 @@
        g2d.setStroke(previous); // 恢复原描边
    } // 方法结束
    
    /**
     * 使用算法端提供的段标注绘制路径(优先使用标注,不做内缩边界推断)。
     * flags.size() 必须为 path.size()-1。
     */
    public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, List<Boolean> flags,
                                       double scale, double arrowScale,
                                       String boundaryCoords, String mowingWidth, String safetyDistance,
                                       String obstaclesCoords, String mowingPattern) {
        if (path == null || path.size() < 2 || flags == null || flags.size() != path.size() - 1) {
            // 回退到推断版本
            drawPlannedPath(g2d, path, scale, arrowScale, boundaryCoords, mowingWidth, safetyDistance, obstaclesCoords, mowingPattern);
            return;
        }
        Stroke previous = g2d.getStroke();
        float strokeWidth = (float) (2.5 / Math.max(0.5, scale));
        // 1. 绘制内缩边界(围边)
        if (boundaryCoords != null && !boundaryCoords.trim().isEmpty() && !"-1".equals(boundaryCoords.trim())) {
            drawInnerBoundary(g2d, boundaryCoords, safetyDistance, scale);
        }
        // 2. 按段绘制,完全遵循算法端标注
        for (int i = 1; i < path.size(); i++) {
            Point2D.Double a = path.get(i - 1);
            Point2D.Double b = path.get(i);
            boolean isMowing = Boolean.TRUE.equals(flags.get(i - 1));
            Path2D segment = new Path2D.Double();
            segment.moveTo(a.x, a.y);
            segment.lineTo(b.x, b.y);
            if (isMowing) {
                g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                g2d.setColor(MOWING_PATH_COLOR);
            } else {
                g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f, DASH_PATTERN, 0.0f));
                g2d.setColor(TRAVEL_PATH_COLOR);
            }
            g2d.draw(segment);
        }
        Point2D.Double start = path.get(0);
        Point2D.Double second = path.size() > 1 ? path.get(1) : path.get(0);
        Point2D.Double end = path.get(path.size() - 1);
        Point2D.Double prev = path.size() > 1 ? path.get(path.size() - 2) : path.get(path.size() - 1);
        drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale);
        drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale);
        g2d.setStroke(previous);
    }
    // 构建内缩边界几何体,用于安全检测与分段绘制
    private static org.locationtech.jts.geom.Geometry buildInnerBoundaryGeom(String boundaryCoords, String safetyDistanceStr) {
        try {
            if (boundaryCoords == null || boundaryCoords.trim().isEmpty() || "-1".equals(boundaryCoords.trim())) {
                return null;
            }
            List<Coordinate> boundary = parseCoordinates(boundaryCoords);
            if (boundary.size() < 4) {
                return null; // 边界点不足
            }
            double safetyDistance = 0.2; // 默认0.2米
            if (safetyDistanceStr != null && !safetyDistanceStr.trim().isEmpty()) {
                try {
                    safetyDistance = Double.parseDouble(safetyDistanceStr.trim());
                } catch (NumberFormatException ignored) {}
            }
            GeometryFactory gf = new GeometryFactory();
            Polygon poly = gf.createPolygon(gf.createLinearRing(boundary.toArray(new Coordinate[0])));
            if (!poly.isValid()) {
                poly = (Polygon) poly.buffer(0);
            }
            org.locationtech.jts.geom.Geometry innerBoundary = poly.buffer(-safetyDistance);
            if (innerBoundary == null || innerBoundary.isEmpty()) {
                return null;
            }
            return innerBoundary;
        } catch (Exception e) {
            return null;
        }
    }
    // 判断一段是否在内缩边界内或沿边界(不越界)
    private static boolean isSegmentInsideInnerBoundary(org.locationtech.jts.geom.Geometry innerBoundary, Point2D.Double a, Point2D.Double b) {
        try {
            GeometryFactory gf = new GeometryFactory();
            Coordinate[] seg = new Coordinate[] { new Coordinate(a.x, a.y), new Coordinate(b.x, b.y) };
            org.locationtech.jts.geom.LineString ls = gf.createLineString(seg);
            // 包含或覆盖均视为合法,不与内边界外部交叉
            if (innerBoundary.contains(ls) || innerBoundary.covers(ls)) {
                return true;
            }
            // 如与内边界边界相交,视为不在内部(用于改变样式并断开连线)
            return !ls.crosses(innerBoundary.getBoundary());
        } catch (Exception e) {
            return true; // 检测异常时,按作业段处理以不中断绘制
        }
    }
    /** // 文档注释开始
     * 绘制内缩边界(围边)- 马尔斯绿色
     */ // 文档注释结束
src/lujing/MowingPathGenerationPage.java
@@ -736,18 +736,20 @@
            return "";
        }
        StringBuilder sb = new StringBuilder();
        AoxinglujingNoObstacle.Point last = null;
        for (AoxinglujingNoObstacle.PathSegment segment : segments) {
            // 只添加割草工作段,跳过过渡段
            if (segment.isMowing) {
                // 如果起点与上一个终点不同,添加起点
                if (last == null || !equals2D(last, segment.start)) {
                    appendPoint(sb, segment.start);
        AoxinglujingNoObstacle.Point lastEnd = null;
        boolean firstWritten = false;
        for (AoxinglujingNoObstacle.PathSegment s : segments) {
            if (!firstWritten) {
                appendPoint(sb, s.start);
                firstWritten = true;
                lastEnd = s.start;
            } else if (lastEnd == null || !equals2D(lastEnd, s.start)) {
                // 非连续段,开始新的子路径
                appendPoint(sb, s.start);
                lastEnd = s.start;
                }
                // 添加终点
                appendPoint(sb, segment.end);
                last = segment.end;
            }
            appendPointWithType(sb, s.end, s.isMowing);
            lastEnd = s.end;
        }
        return sb.toString();
    }
@@ -760,18 +762,20 @@
            return "";
        }
        StringBuilder sb = new StringBuilder();
        YixinglujingNoObstacle.Point last = null;
        for (YixinglujingNoObstacle.PathSegment segment : segments) {
            // 只添加割草工作段,跳过过渡段
            if (segment.isMowing) {
                // 如果起点与上一个终点不同,添加起点
                if (last == null || !equalsYixingPoint(last, segment.start)) {
                    appendYixingPoint(sb, segment.start);
        YixinglujingNoObstacle.Point lastEnd = null;
        boolean firstWritten = false;
        for (YixinglujingNoObstacle.PathSegment s : segments) {
            if (!firstWritten) {
                appendYixingPoint(sb, s.start);
                firstWritten = true;
                lastEnd = s.start;
            } else if (lastEnd == null || !equalsYixingPoint(lastEnd, s.start)) {
                // 非连续段,开始新的子路径
                appendYixingPoint(sb, s.start);
                lastEnd = s.start;
                }
                // 添加终点
                appendYixingPoint(sb, segment.end);
                last = segment.end;
            }
            appendYixingPointWithType(sb, s.end, s.isMowing);
            lastEnd = s.end;
        }
        return sb.toString();
    }
@@ -808,15 +812,19 @@
            return "";
        }
        StringBuilder sb = new StringBuilder();
        YixinglujingHaveObstacel.Point last = null;
        for (YixinglujingHaveObstacel.PathSegment segment : segments) {
            // 如果是第一段,或者当前段起点与上一段终点不连续,则添加起点
            if (last == null || !equalsYixingHaveObstaclePoint(last, segment.start)) {
                appendYixingHaveObstaclePoint(sb, segment.start);
        YixinglujingHaveObstacel.Point lastEnd = null;
        boolean firstWritten = false;
        for (YixinglujingHaveObstacel.PathSegment s : segments) {
            if (!firstWritten) {
                appendYixingHaveObstaclePoint(sb, s.start);
                firstWritten = true;
                lastEnd = s.start;
            } else if (lastEnd == null || !equalsYixingHaveObstaclePoint(lastEnd, s.start)) {
                appendYixingHaveObstaclePoint(sb, s.start);
                lastEnd = s.start;
            }
            // 添加终点
            appendYixingHaveObstaclePoint(sb, segment.end);
            last = segment.end;
            appendYixingHaveObstaclePointWithType(sb, s.end, s.isMowing);
            lastEnd = s.end;
        }
        return sb.toString();
    }
@@ -834,6 +842,13 @@
        sb.append(String.format(Locale.US, "%.2f,%.2f", point.x, point.y));
    }
    private void appendYixingHaveObstaclePointWithType(StringBuilder sb, YixinglujingHaveObstacel.Point point, boolean isMowing) {
        if (sb.length() > 0) {
            sb.append(";");
        }
        sb.append(String.format(Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T"));
    }
    /**
     * 比较两个点是否相同(使用小的容差)
     */
@@ -855,6 +870,13 @@
        sb.append(String.format(Locale.US, "%.2f,%.2f", point.x, point.y));
    }
    
    private void appendPointWithType(StringBuilder sb, AoxinglujingNoObstacle.Point point, boolean isMowing) {
        if (sb.length() > 0) {
            sb.append(";");
        }
        sb.append(String.format(Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T"));
    }
    /**
     * 比较两个 YixinglujingNoObstacle.Point 是否相同(使用小的容差)
     */
@@ -876,6 +898,13 @@
        sb.append(String.format(Locale.US, "%.2f,%.2f", point.x, point.y));
    }
    private void appendYixingPointWithType(StringBuilder sb, YixinglujingNoObstacle.Point point, boolean isMowing) {
        if (sb.length() > 0) {
            sb.append(";");
        }
        sb.append(String.format(Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T"));
    }
    /**
     * 比较两个 AoxinglujingHaveObstacel.Point 是否相同(使用小的容差)
     */
src/lujing/YixinglujingNoObstacle.java
@@ -7,6 +7,9 @@
 * 修复:解决凹多边形扫描线跨越边界的问题,优化路径对齐
 */
public class YixinglujingNoObstacle {
    // 开关:是否强制所有非作业连接沿安全边界行走(避免任何内区直线跨越)
    // 改为可动态设置,自动依据地块形状启用
    private static boolean FORCE_BOUNDARY_TRAVEL = true;
    // 用法说明(无障碍物路径规划):
    // - 方法用途:根据地块边界、割草宽度与安全边距,生成覆盖全区域的割草路径。
    // - 参数:
@@ -38,6 +41,9 @@
        // 3. 确定最优作业角度
        double bestAngle = findOptimalAngle(boundary);
        
        // 3.1 自动判断是否需要强制沿边界旅行(检测凹部/多段扫描行)
        FORCE_BOUNDARY_TRAVEL = shouldForceBoundaryTravel(boundary, mowWidth, bestAngle);
        // 4. 获取首个作业点,用于对齐围边起点
        Point firstScanStart = getFirstScanPoint(boundary, mowWidth, bestAngle);
@@ -59,7 +65,10 @@
        
        finalPath.addAll(scanPath);
        // 8. 格式化坐标:保留两位小数
        // 8. 最终安全净化:确保所有段在内缩边界上或内部(自动贴边阈值)
        sanitizePath(finalPath, boundary, mowWidth, safeMargin);
        // 9. 格式化坐标:保留两位小数
        for (PathSegment segment : finalPath) {
            segment.start.x = Math.round(segment.start.x * 100.0) / 100.0;
            segment.start.y = Math.round(segment.start.y * 100.0) / 100.0;
@@ -70,6 +79,108 @@
        return finalPath;
    }
    // 对所有路径段进行安全净化:
    // - 非作业段:统一沿边界路径替换
    // - 作业段:若端点在外或段与边界相交,吸附端点到边界并向内侧偏移 epsilon
    private static void sanitizePath(List<PathSegment> segments, List<Point> polygon, double width, double margin) {
        double epsilon = computeAutoInnerOffset(polygon, width, margin);
        List<PathSegment> sanitized = new ArrayList<>();
        for (PathSegment s : segments) {
            boolean startInside = isPointInPolygon(s.start, polygon);
            boolean endInside = isPointInPolygon(s.end, polygon);
            boolean intersects = segmentIntersectsBoundary(s.start, s.end, polygon);
            if (!s.isMowing) {
                // 非作业段统一替换为沿边界路径
                List<Point> path = getBoundaryPathWithSnap(s.start, s.end, polygon);
                for (int i = 0; i < path.size() - 1; i++) {
                    sanitized.add(new PathSegment(path.get(i), path.get(i+1), false));
                }
            } else {
                if (!startInside || !endInside || intersects) {
                    SnapResult s1 = snapToBoundary(s.start, polygon);
                    SnapResult s2 = snapToBoundary(s.end, polygon);
                    Point p1 = pushInsideOnEdge(s1, polygon, epsilon);
                    Point p2 = pushInsideOnEdge(s2, polygon, epsilon);
                    sanitized.add(new PathSegment(p1, p2, true));
                } else {
                    sanitized.add(s);
                }
            }
        }
        segments.clear();
        segments.addAll(sanitized);
    }
    private static boolean segmentIntersectsBoundary(Point a, Point b, List<Point> polygon) {
        for (int i = 0; i < polygon.size(); i++) {
            Point c = polygon.get(i);
            Point d = polygon.get((i + 1) % polygon.size());
            // 忽略共享端点的相交
            if (isSamePoint(a, c) || isSamePoint(a, d) || isSamePoint(b, c) || isSamePoint(b, d)) continue;
            if (segmentsIntersect(a, b, c, d)) return true;
        }
        return false;
    }
    // 将边界上的投影点向内侧偏移 epsilon
    private static Point pushInsideOnEdge(SnapResult sr, List<Point> poly, double epsilon) {
        int i = sr.edgeIndex;
        Point s = poly.get(i);
        Point e = poly.get((i + 1) % poly.size());
        double dx = e.x - s.x, dy = e.y - s.y;
        double len = Math.hypot(dx, dy);
        if (len < 1e-6) return sr.onEdge;
        // 对于逆时针(CCW)多边形,左转法向量 (-dy, dx) 指向内侧
        double nx = -dy / len, ny = dx / len;
        return new Point(sr.onEdge.x + nx * epsilon, sr.onEdge.y + ny * epsilon);
    }
    // 自动计算贴边内偏移阈值 epsilon:根据地块尺度、最短边、割草宽度与安全边距综合估算
    private static double computeAutoInnerOffset(List<Point> polygon, double width, double margin) {
        double minEdge = Double.MAX_VALUE;
        double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
        double maxX = -Double.MAX_VALUE, maxY = -Double.MAX_VALUE;
        for (int i = 0; i < polygon.size(); i++) {
            Point a = polygon.get(i);
            Point b = polygon.get((i + 1) % polygon.size());
            minEdge = Math.min(minEdge, Math.hypot(a.x - b.x, a.y - b.y));
            minX = Math.min(minX, a.x); minY = Math.min(minY, a.y);
            maxX = Math.max(maxX, a.x); maxY = Math.max(maxY, a.y);
        }
        double diag = Math.hypot(maxX - minX, maxY - minY);
        // 基础量:数值稳定需要的最小内偏移(取割幅的1%与对角线的0.2%之间的较大值)
        double base = Math.max(width * 0.01, diag * 0.002);
        // 上限:不超过安全边距的20%与割幅的10%
        double upper = Math.min(margin * 0.2, width * 0.1);
        // 受边长约束:不超过最短边的2%
        double edgeLimit = minEdge * 0.02;
        double eps = Math.min(upper, Math.max(base, edgeLimit * 0.5));
        // 下限/上限最终钳位:3mm 到 5cm
        eps = Math.max(0.003, Math.min(eps, 0.05));
        return eps;
    }
    // 根据扫描行的交点数量来判断是否存在“多段行”(>=2段),有凹部或窄通道时启用强制沿边界旅行
    private static boolean shouldForceBoundaryTravel(List<Point> polygon, double width, double angle) {
        List<Point> rotatedPoly = new ArrayList<>();
        for (Point p : polygon) rotatedPoly.add(rotatePoint(p, -angle));
        double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE;
        for (Point p : rotatedPoly) { minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); }
        int multiSegmentRows = 0;
        int totalRows = 0;
        for (double y = minY + width/2; y <= maxY - width/2; y += width) {
            List<Double> xIntersections = getXIntersections(rotatedPoly, y);
            if (xIntersections.size() < 2) continue;
            totalRows++;
            if (xIntersections.size() >= 4) multiSegmentRows++; // 同一行出现两个及以上作业段
        }
        // 只要出现过“多段行”,就强制沿边界旅行;也可按比例阈值触发(例如 >=10%)
        if (multiSegmentRows > 0) return true;
        double ratio = totalRows == 0 ? 0.0 : (double) multiSegmentRows / (double) totalRows;
        return ratio >= 0.1; // 兜底阈值
    }
    private static List<PathSegment> generateGlobalScanPath(List<Point> polygon, double width, double angle, Point currentPos) {
        // 先尝试将凹陷处视为两个独立区域,分两次扫描,避免跨区直线连接
        List<PathSegment> all = new ArrayList<>();
@@ -258,24 +369,86 @@
    }
    private static void addSafeConnection(List<PathSegment> segments, Point start, Point end, List<Point> polygon) {
        if (isSegmentSafe(start, end, polygon)) {
        if (!FORCE_BOUNDARY_TRAVEL && isSegmentSafe(start, end, polygon)) {
            segments.add(new PathSegment(start, end, false));
        } else {
            List<Point> path = getBoundaryPath(start, end, polygon);
            return;
        }
        List<Point> path = getBoundaryPathWithSnap(start, end, polygon);
            for (int i = 0; i < path.size() - 1; i++) {
                segments.add(new PathSegment(path.get(i), path.get(i+1), false));
            }
        }
    }
    // 强制沿边界绕行的连接(不做直线安全判断),用来在同一扫描行的多个作业段之间跳转
    private static void addBoundaryConnection(List<PathSegment> segments, Point start, Point end, List<Point> polygon) {
        List<Point> path = getBoundaryPath(start, end, polygon);
        List<Point> path = getBoundaryPathWithSnap(start, end, polygon);
        for (int i = 0; i < path.size() - 1; i++) {
            segments.add(new PathSegment(path.get(i), path.get(i+1), false));
        }
    }
    // 将任意两点通过“吸附到边界”后沿边界最短路径连接
    private static List<Point> getBoundaryPathWithSnap(Point start, Point end, List<Point> polygon) {
        SnapResult s1 = snapToBoundary(start, polygon);
        SnapResult s2 = snapToBoundary(end, polygon);
        int n = polygon.size();
        // 前向路径(顺边)
        List<Point> pathFwd = new ArrayList<>();
        pathFwd.add(start);
        pathFwd.add(s1.onEdge);
        int curr = s1.edgeIndex;
        while (curr != s2.edgeIndex) {
            pathFwd.add(polygon.get((curr + 1) % n));
            curr = (curr + 1) % n;
        }
        pathFwd.add(s2.onEdge);
        pathFwd.add(end);
        // 反向路径(逆边)
        List<Point> pathRev = new ArrayList<>();
        pathRev.add(start);
        pathRev.add(s1.onEdge);
        curr = s1.edgeIndex;
        while (curr != s2.edgeIndex) {
            pathRev.add(polygon.get(curr));
            curr = (curr - 1 + n) % n;
        }
        pathRev.add(s2.onEdge);
        pathRev.add(end);
        return getPathLength(pathFwd) < getPathLength(pathRev) ? pathFwd : pathRev;
    }
    private static class SnapResult {
        Point onEdge;
        int edgeIndex;
        SnapResult(Point p, int idx) { this.onEdge = p; this.edgeIndex = idx; }
    }
    // 计算点到边界最近的投影点以及所在边索引
    private static SnapResult snapToBoundary(Point p, List<Point> poly) {
        double minD = Double.MAX_VALUE;
        Point bestProj = p;
        int bestIdx = -1;
        for (int i = 0; i < poly.size(); i++) {
            Point s = poly.get(i);
            Point e = poly.get((i + 1) % poly.size());
            double l2 = (s.x - e.x)*(s.x - e.x) + (s.y - e.y)*(s.y - e.y);
            if (l2 == 0) {
                double d = Math.hypot(p.x - s.x, p.y - s.y);
                if (d < minD) { minD = d; bestProj = s; bestIdx = i; }
                continue;
            }
            double t = ((p.x - s.x) * (e.x - s.x) + (p.y - s.y) * (e.y - s.y)) / l2;
            t = Math.max(0, Math.min(1, t));
            Point proj = new Point(s.x + t * (e.x - s.x), s.y + t * (e.y - s.y));
            double d = Math.hypot(p.x - proj.x, p.y - proj.y);
            if (d < minD) { minD = d; bestProj = proj; bestIdx = i; }
        }
        return new SnapResult(bestProj, bestIdx == -1 ? 0 : bestIdx);
    }
    private static boolean isSegmentSafe(Point p1, Point p2, List<Point> polygon) {
        Point mid = new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
        if (!isPointInPolygon(mid, polygon)) return false;
src/zhangaiwu/AddDikuai.java
@@ -1516,18 +1516,19 @@
            return "";
        }
        StringBuilder sb = new StringBuilder();
        AoxinglujingNoObstacle.Point last = null;
        for (AoxinglujingNoObstacle.PathSegment segment : segments) {
            // 只添加割草工作段,跳过过渡段
            if (segment.isMowing) {
                // 如果起点与上一个终点不同,添加起点
                if (last == null || !equalsAoxingPoint(last, segment.start)) {
                    appendAoxingPoint(sb, segment.start);
        AoxinglujingNoObstacle.Point lastEnd = null;
        boolean firstWritten = false;
        for (AoxinglujingNoObstacle.PathSegment s : segments) {
            if (!firstWritten) {
                appendAoxingPoint(sb, s.start);
                firstWritten = true;
                lastEnd = s.start;
            } else if (lastEnd == null || !equalsAoxingPoint(lastEnd, s.start)) {
                appendAoxingPoint(sb, s.start);
                lastEnd = s.start;
                }
                // 添加终点
                appendAoxingPoint(sb, segment.end);
                last = segment.end;
            }
            appendAoxingPointWithType(sb, s.end, s.isMowing);
            lastEnd = s.end;
        }
        return sb.toString();
    }
@@ -1540,18 +1541,19 @@
            return "";
        }
        StringBuilder sb = new StringBuilder();
        YixinglujingNoObstacle.Point last = null;
        for (YixinglujingNoObstacle.PathSegment segment : segments) {
            // 只添加割草工作段,跳过过渡段
            if (segment.isMowing) {
                // 如果起点与上一个终点不同,添加起点
                if (last == null || !equalsYixingPoint(last, segment.start)) {
                    appendYixingPoint(sb, segment.start);
        YixinglujingNoObstacle.Point lastEnd = null;
        boolean firstWritten = false;
        for (YixinglujingNoObstacle.PathSegment s : segments) {
            if (!firstWritten) {
                appendYixingPoint(sb, s.start);
                firstWritten = true;
                lastEnd = s.start;
            } else if (lastEnd == null || !equalsYixingPoint(lastEnd, s.start)) {
                appendYixingPoint(sb, s.start);
                lastEnd = s.start;
                }
                // 添加终点
                appendYixingPoint(sb, segment.end);
                last = segment.end;
            }
            appendYixingPointWithType(sb, s.end, s.isMowing);
            lastEnd = s.end;
        }
        return sb.toString();
    }
@@ -1571,6 +1573,12 @@
     * 添加 AoxinglujingNoObstacle.Point 到字符串构建器
     */
    private void appendAoxingPoint(StringBuilder sb, AoxinglujingNoObstacle.Point point) {
            private void appendAoxingPointWithType(StringBuilder sb, AoxinglujingNoObstacle.Point point, boolean isMowing) {
                if (sb.length() > 0) {
                    sb.append(";");
                }
                sb.append(String.format(java.util.Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T"));
            }
        if (sb.length() > 0) {
            sb.append(";");
        }
@@ -1592,6 +1600,12 @@
     * 添加 YixinglujingNoObstacle.Point 到字符串构建器
     */
    private void appendYixingPoint(StringBuilder sb, YixinglujingNoObstacle.Point point) {
            private void appendYixingPointWithType(StringBuilder sb, YixinglujingNoObstacle.Point point, boolean isMowing) {
                if (sb.length() > 0) {
                    sb.append(";");
                }
                sb.append(String.format(java.util.Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T"));
            }
        if (sb.length() > 0) {
            sb.append(";");
        }
src/zhuye/MapRenderer.java
@@ -1531,17 +1531,25 @@
        
        // 如果从地块获取到了路径,使用地块的路径;否则使用currentPlannedPath
        List<Point2D.Double> pathToDraw = currentPlannedPath;
        List<Boolean> flags = null;
        if (plannedPathStr != null && !plannedPathStr.trim().isEmpty() && !"-1".equals(plannedPathStr.trim())) {
            // 从地块获取的路径
            pathToDraw = lujingdraw.parsePlannedPath(plannedPathStr);
            // 优先解析带标注格式
            lujingdraw.ParsedPath parsed = lujingdraw.parsePlannedPathWithFlags(plannedPathStr);
            pathToDraw = parsed.points;
            flags = parsed.isMowingFlags;
        }
        
        // 调用带地块信息的绘制方法
        // 调用带地块信息的绘制方法(如有标注优先使用)
        if (pathToDraw != null && pathToDraw.size() >= 2) {
            if (flags != null && flags.size() == pathToDraw.size() - 1) {
                lujingdraw.drawPlannedPath(g2d, pathToDraw, flags, scale, arrowScale,
                        boundaryCoords, mowingWidth, safetyDistance, obstaclesCoords, mowingPattern);
            } else {
            lujingdraw.drawPlannedPath(g2d, pathToDraw, scale, arrowScale, 
                                   boundaryCoords, mowingWidth, safetyDistance, obstaclesCoords, mowingPattern);
        }
    }
    }
    private void drawCircleSampleMarkers(Graphics2D g2d, List<double[]> markers, double scale) {
        if (markers == null || markers.isEmpty()) {