826220679@qq.com
4 小时以前 7881cef5c3dcea8e6037101db2c3eeb2fd3ba5da
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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
package gecaoji; // 包声明
import java.awt.BasicStroke; // 引入基础描边类
import java.awt.Color; // 引入颜色类
import java.awt.Graphics2D; // 引入2D图形上下文
import java.awt.Stroke; // 引入描边接口
import java.awt.geom.Path2D; // 引入路径绘制类
import java.awt.geom.Point2D; // 引入二维点类
import java.util.ArrayList; // 引入动态数组
import java.util.List; // 引入列表接口
import org.locationtech.jts.geom.Coordinate; // 引入坐标类
import org.locationtech.jts.geom.GeometryFactory; // 引入几何工厂类
import org.locationtech.jts.geom.Polygon; // 引入多边形类
 
/** // 文档注释开头
 * Helper class to parse and render planned mowing paths. // 辅助类说明
 */ // 文档注释结尾
public final class lujingdraw { // 类定义,防止继承
    // 马尔斯绿色 (RGB: 102, 205, 170) - 用于内缩边界(围边)
    private static final Color INNER_BOUNDARY_COLOR = new Color(102, 205, 170);
    // 提香红透明度70% (RGB: 210, 4, 45, alpha: 178) - 用于割草作业路径
    private static final Color MOWING_PATH_COLOR = new Color(210, 4, 45, 178);
    // 蓝色 - 用于非作业移动路径
    private static final Color TRAVEL_PATH_COLOR = new Color(0, 0, 255);
    // 虚线样式 - 用于非作业移动路径
    private static final float[] DASH_PATTERN = {5.0f, 5.0f};
    private static final Color START_POINT_COLOR = new Color(0, 0, 0, 220); // 起点箭头颜色
    private static final Color END_POINT_COLOR = new Color(0, 0, 0, 220); // 终点箭头颜色
 
    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. // 方法说明
     */ // 文档注释结束
    public static List<Point2D.Double> parsePlannedPath(String plannedPath) { // 解析路径字符串
        List<Point2D.Double> points = new ArrayList<>(); // 存放解析后的点集
        if (plannedPath == null) { // 判空
            return points; // 返回空集合
        } // if结束
 
        String normalized = plannedPath.trim(); // 去除首尾空格
        if (normalized.isEmpty() || "-1".equals(normalized)) { // 无效判断
            return points; // 返回空集合
        } // if结束
 
        int commentIndex = normalized.indexOf('#'); // 查找注释符号
        if (commentIndex >= 0) { // 找到注释
            normalized = normalized.substring(0, commentIndex).trim(); // 截取注释前内容
        } // if结束
 
        if (normalized.isEmpty()) { // 再次判空
            return points; // 返回空集合
        } // if结束
 
        String[] segments = normalized.split(";"); // 切分各段
        for (String segment : segments) { // 遍历每段
            if (segment == null) { // 判空
                continue; // 跳过
            } // if结束
            String trimmed = segment.trim(); // 去除空格
            if (trimmed.isEmpty()) { // 空段
                continue; // 跳过
            } // if结束
            String[] parts = trimmed.split(","); // 切分坐标
            if (parts.length < 2) { // 检查长度
                continue; // 跳过
            } // if结束
            try { // 数值解析
                double x = Double.parseDouble(parts[0].trim()); // 解析X坐标
                double y = Double.parseDouble(parts[1].trim()); // 解析Y坐标
                points.add(new Point2D.Double(x, y)); // 加入点集
            } catch (NumberFormatException ignored) { // 捕获格式异常
                // Ignore malformed coordinates // 忽略非法坐标
            } // try结束
        } // for结束
        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. // 绘制路径
     */ // 文档注释结束
    public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale, double arrowScale) { // 绘制主方法
        drawPlannedPath(g2d, path, scale, arrowScale, null, null, null, null, null); // 调用重载方法
    } // 方法结束
    
    /** // 文档注释开始
     * Draw the planned mowing path with segment information. // 绘制路径(带段信息)
     * @param boundaryCoords 地块边界坐标字符串,用于绘制内缩边界
     * @param mowingWidth 割草宽度(米),字符串格式
     * @param safetyDistance 安全距离(米),字符串格式
     * @param obstaclesCoords 障碍物坐标字符串
     * @param mowingPattern 割草模式("parallel"或"spiral")
     */ // 文档注释结束
    public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale, double arrowScale, 
                                      String boundaryCoords, String mowingWidth, String safetyDistance, String obstaclesCoords, String mowingPattern) { // 绘制主方法(重载)
        if (path == null || path.size() < 2) { // 判定点数
            return; // 数据不足直接返回
        } // if结束
 
        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. 按段绘制路径(逐段绘制,避免将不相邻段首尾连成越界直线)
        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);
 
            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.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); // 恢复原描边
    } // 方法结束
 
    /**
     * 使用算法端提供的段标注绘制路径(优先使用标注,不做内缩边界推断)。
     * 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; // 检测异常时,按作业段处理以不中断绘制
        }
    }
    
    /** // 文档注释开始
     * 绘制内缩边界(围边)- 马尔斯绿色
     */ // 文档注释结束
    private static void drawInnerBoundary(Graphics2D g2d, String boundaryCoords, String safetyDistanceStr, double scale) {
        try {
            List<Coordinate> boundary = parseCoordinates(boundaryCoords);
            if (boundary.size() < 4) {
                return; // 边界点不足
            }
            
            // 解析安全距离
            double safetyDistance = 0.2; // 默认0.2米
            if (safetyDistanceStr != null && !safetyDistanceStr.trim().isEmpty()) {
                try {
                    safetyDistance = Double.parseDouble(safetyDistanceStr.trim());
                } catch (NumberFormatException e) {
                    // 使用默认值
                }
            }
            
            // 创建多边形并内缩
            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;
            }
            
            // 绘制内缩边界
            g2d.setColor(INNER_BOUNDARY_COLOR); // 马尔斯绿色
            float strokeWidth = (float) (3.0 / Math.max(0.5, scale));
            g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
            
            // 将JTS几何体转换为Path2D并绘制
            Path2D.Double innerPath = geometryToPath2D(innerBoundary);
            if (innerPath != null) {
                g2d.draw(innerPath);
            }
        } catch (Exception e) {
            // 绘制失败时忽略,不影响其他路径绘制
        }
    } // 方法结束
    
    /** // 文档注释开始
     * 将JTS几何体转换为Path2D
     */ // 文档注释结束
    private static Path2D.Double geometryToPath2D(org.locationtech.jts.geom.Geometry geom) {
        if (geom == null) {
            return null;
        }
        Path2D.Double path = new Path2D.Double();
        if (geom instanceof Polygon) {
            Polygon poly = (Polygon) geom;
            addRingToPath(path, poly.getExteriorRing().getCoordinates(), true);
            for (int i = 0; i < poly.getNumInteriorRing(); i++) {
                addRingToPath(path, poly.getInteriorRingN(i).getCoordinates(), true);
            }
        } else if (geom instanceof org.locationtech.jts.geom.MultiPolygon) {
            org.locationtech.jts.geom.MultiPolygon mp = (org.locationtech.jts.geom.MultiPolygon) geom;
            for (int i = 0; i < mp.getNumGeometries(); i++) {
                Polygon poly = (Polygon) mp.getGeometryN(i);
                addRingToPath(path, poly.getExteriorRing().getCoordinates(), true);
                for (int j = 0; j < poly.getNumInteriorRing(); j++) {
                    addRingToPath(path, poly.getInteriorRingN(j).getCoordinates(), true);
                }
            }
        }
        return path;
    } // 方法结束
    
    /** // 文档注释开始
     * 将坐标环添加到Path2D
     */ // 文档注释结束
    private static void addRingToPath(Path2D.Double path, Coordinate[] coords, boolean close) {
        if (coords == null || coords.length < 2) {
            return;
        }
        path.moveTo(coords[0].x, coords[0].y);
        for (int i = 1; i < coords.length; i++) {
            path.lineTo(coords[i].x, coords[i].y);
        }
        if (close) {
            path.closePath();
        }
    } // 方法结束
    
 
    
    /** // 文档注释开始
     * 比较两个坐标是否相同(容差)
     */ // 文档注释结束
    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 drawArrowMarker(Graphics2D g2d, Point2D.Double from, Point2D.Double to, Color color, double scale, double sizeScale) { // 绘制箭头辅助
        if (from == null || to == null) { // 判空
            return; // 数据不足返回
        } // if结束
        double dx = to.x - from.x; // 计算X增量
        double dy = to.y - from.y; // 计算Y增量
        double length = Math.hypot(dx, dy); // 计算向量长度
        if (length == 0) { // 防止零长度
            return; // 无方向返回
        } // if结束
 
    double arrowLength = Math.max(2.5, 5.5 / Math.max(0.5, scale)); // 计算箭头长度
    double clampedScale = sizeScale > 0 ? sizeScale : 1.0; // 防止非法缩放
    arrowLength *= 0.25 * clampedScale; // 缩小箭头至原来的一半
        double arrowWidth = arrowLength * 0.45; // 计算箭头宽度
 
        double ux = dx / length; // 单位向量X
        double uy = dy / length; // 单位向量Y
 
        double tipX = to.x; // 箭头尖端X
        double tipY = to.y; // 箭头尖端Y
        double baseX = tipX - ux * arrowLength; // 箭头底部X
        double baseY = tipY - uy * arrowLength; // 箭头底部Y
 
        double perpX = -uy; // 垂直向量X
        double perpY = ux; // 垂直向量Y
 
        double leftX = baseX + perpX * arrowWidth * 0.5; // 左侧点X
        double leftY = baseY + perpY * arrowWidth * 0.5; // 左侧点Y
        double rightX = baseX - perpX * arrowWidth * 0.5; // 右侧点X
        double rightY = baseY - perpY * arrowWidth * 0.5; // 右侧点Y
 
        Path2D arrow = new Path2D.Double(); // 创建箭头路径
        arrow.moveTo(tipX, tipY); // 起点移动到尖端
        arrow.lineTo(leftX, leftY); // 连到左侧
        arrow.lineTo(rightX, rightY); // 连到右侧
        arrow.closePath(); // 闭合形成三角形
 
        g2d.setColor(color); // 设置颜色
        g2d.fill(arrow); // 填充箭头
    } // 方法结束
 
    /**
     * 解析坐标字符串
     */
    private 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;
    }
} // 类结束