张世豪
4 天以前 32c98d4855b6178554c787103dc956d161e152b3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
package lujing;
 
import java.util.ArrayList;
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.MultiLineString;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.MultiPolygon;
 
/**
 * 优化后的割草路径规划类
 * 修复:解决路径超出地块边界的问题,增加安全边距计算的健壮性。
 */
public final class Lunjingguihua {
 
    private Lunjingguihua() {
        throw new IllegalStateException("Utility class");
    }
 
    /**
     * 生成割草路径段列表(5参数版本)
     * 根据给定的多边形边界、障碍物、割草宽度、安全距离和模式,生成完整的割草路径规划
     * 
     * @param polygonCoords 多边形边界坐标字符串,格式:"x1,y1;x2,y2;x3,y3;..." 或 "(x1,y1);(x2,y2);..."
     *                      支持分号、空格、逗号等多种分隔符,会自动闭合多边形
     * @param obstaclesCoords 障碍物坐标字符串,格式:"(x1,y1;x2,y2;...)" 多个障碍物用括号分隔
     *                        如果为空或null,则视为无障碍物
     * @param mowingWidth 割草宽度(米),字符串格式,如 "0.34" 表示34厘米
     *                    如果为空或无法解析,默认使用0.34米
     * @param safetyDistStr 安全边距(米),字符串格式,用于内缩边界和障碍物外扩
     *                      如果为空或null,将自动计算为 width/2 + 0.2米,确保割草机实体完全在界内
     * @param modeStr 路径模式字符串,"1" 或 "spiral" 表示螺旋模式,其他值表示平行模式(默认)
     * @return 路径段列表,每个PathSegment包含起点、终点和是否为割草段的标志
     *         如果多边形坐标不足4个点,将抛出IllegalArgumentException异常
     */
    public static List<PathSegment> generatePathSegments(String polygonCoords,
                                                         String obstaclesCoords,
                                                         String mowingWidth,
                                                         String safetyDistStr,
                                                         String modeStr) {
        List<Coordinate> polygon = parseCoordinates(polygonCoords);
        if (polygon.size() < 4) {
            throw new IllegalArgumentException("多边形坐标数量不足");
        }
 
        double width = parseDoubleOrDefault(mowingWidth, 0.34);
        // 如果传入空,设为 NaN,在 PlannerCore 中进行智能计算
        double safetyDistance = parseDoubleOrDefault(safetyDistStr, Double.NaN);
 
        List<List<Coordinate>> obstacles = parseObstacles(obstaclesCoords);
        String mode = normalizeMode(modeStr);
 
        PlannerCore planner = new PlannerCore(polygon, width, safetyDistance, mode, obstacles);
        return planner.generate();
    }
 
    /**
     * 生成割草路径段列表(4参数版本)
     * 这是5参数版本的重载方法,安全距离参数自动设为null,系统将使用默认计算值
     * 主要用于适配 AddDikuai.java 等不需要指定安全距离的场景
     * 
     * @param polygonCoords 多边形边界坐标字符串,格式:"x1,y1;x2,y2;x3,y3;..."
     * @param obstaclesCoords 障碍物坐标字符串,格式:"(x1,y1;x2,y2;...)",可为空
     * @param mowingWidth 割草宽度(米),字符串格式,如 "0.34"
     * @param modeStr 路径模式字符串,"1" 或 "spiral" 表示螺旋模式,其他值表示平行模式
     * @return 路径段列表,每个PathSegment包含起点、终点和是否为割草段的标志
     *         安全距离将自动计算为 width/2 + 0.2米
     */
    public static List<PathSegment> generatePathSegments(String polygonCoords,
                                                         String obstaclesCoords,
                                                         String mowingWidth,
                                                         String modeStr) {
        return generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, null, modeStr);
    }
 
    // 5参数路径字符串生成
    public static String generatePathFromStrings(String polygonCoords,
                                                 String obstaclesCoords,
                                                 String mowingWidth,
                                                 String safetyDistStr,
                                                 String modeStr) {
        List<PathSegment> segments = generatePathSegments(polygonCoords, obstaclesCoords, mowingWidth, safetyDistStr, modeStr);
        return formatPathSegments(segments);
    }
 
    // 4参数路径字符串生成重载
    public static String generatePathFromStrings(String polygonCoords,
                                                 String obstaclesCoords,
                                                 String mowingWidth,
                                                 String modeStr) {
        return generatePathFromStrings(polygonCoords, obstaclesCoords, mowingWidth, null, modeStr);
    }
 
    public static String formatPathSegments(List<PathSegment> path) {
        if (path == null || path.isEmpty()) return "";
        StringBuilder sb = new StringBuilder();
        Coordinate last = null;
        for (PathSegment segment : path) {
            if (last == null || !equals2D(last, segment.start)) {
                appendPoint(sb, segment.start);
            }
            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;
        // 增强正则:处理可能存在的多种分隔符
        String[] pts = s.split("[;\\s]+");
        for (String p : pts) {
            String trimmed = p.trim().replace("(", "").replace(")", "");
            if (trimmed.isEmpty()) continue;
            String[] xy = trimmed.split("[,,\\s]+");
            if (xy.length >= 2) {
                try {
                    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) {
                    // 忽略解析错误的点
                }
            }
        }
        // 确保多边形闭合
        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;
        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);
        }
        if (obs.isEmpty()) {
            List<Coordinate> coords = parseCoordinates(str);
            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;
        try {
            return Double.parseDouble(value.trim());
        } catch (NumberFormatException ex) {
            return defaultValue;
        }
    }
 
    private static String normalizeMode(String modeStr) {
        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.distance(b) < 1e-4;
    }
 
    private static void appendPoint(StringBuilder sb, Coordinate point) {
        if (sb.length() > 0) sb.append(";");
        sb.append(String.format("%.3f,%.3f", point.x, point.y));
    }
 
    public static final class PathSegment {
        public Coordinate start, end;
        public boolean isMowing;
        public boolean isStartPoint, isEndPoint;
 
        public PathSegment(Coordinate start, Coordinate end, boolean isMowing) {
            this.start = start;
            this.end = end;
            this.isMowing = isMowing;
        }
 
        public void setAsStartPoint() { this.isStartPoint = true; }
        public void setAsEndPoint() { this.isEndPoint = true; }
    }
 
    public static final class PlannerCore {
        private final List<Coordinate> polygon;
        private final double width;
        private final double safetyDistance;
        private final String mode;
        private final List<List<Coordinate>> obstacles;
        private final GeometryFactory gf = new GeometryFactory();
 
        // 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<>();
            
            // FIX: 增加默认安全边距。原逻辑为 width/2 + 0.05,容易造成误差出界。
            // 现改为 width/2 + 0.2 (20cm余量),确保割草机实体完全在界内。
            if (Double.isNaN(safetyDistance) || safetyDistance <= 0) {
                this.safetyDistance = (width / 2.0) + 0.20; 
            } else {
                this.safetyDistance = safetyDistance;
            }
        }
 
        // 2. 4参数构造函数
        public PlannerCore(List<Coordinate> polygon, double width, String mode, List<List<Coordinate>> obstacles) {
            this(polygon, width, Double.NaN, mode, obstacles);
        }
 
        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 (safeArea == null || safeArea.isEmpty()) return totalPath;
 
            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));
                }
            }
 
            for (Polygon region : subRegions) {
                if (region.isEmpty()) continue;
 
                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) {
                        // 忽略极其罕见的拓扑异常,防止崩溃
                    }
                }
            }
            return markStartEnd(totalPath);
        }
 
        private Geometry buildSafeArea() {
            try {
                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);
 
                // 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) {
                        // 忽略错误的障碍物数据
                    }
                }
                
                // 5. 最终清理
                if (!safe.isValid()) safe = safe.buffer(0);
                return safe;
            } catch (Exception e) {
                // 如果几何构建完全失败,返回空
                return gf.createPolygon();
            }
        }
 
        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 bestDir;
        }
 
        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 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;
            }
        }
 
        List<PathSegment> generateSpiralPath() { return new ArrayList<>(); }
    }
 
    private static final class Vector2D {
        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 normalize() { 
            double len = Math.hypot(x, y); 
            return len < 1e-9 ? new Vector2D(1, 0) : 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 mul(double k) { return new Vector2D(x * k, y * k); }
    }
}