张世豪
17 小时以前 a541fbdc8812337de120aad3792a2033a5dd7afe
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
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 lujing.Lunjingguihua; // 引入路径规划类
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 = {10.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() { // 私有构造防止实例化
    } // 空实现
 
    /** // 文档注释开始
     * 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; // 返回结果
    } // 方法结束
 
    /** // 文档注释开始
     * 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. 尝试重新生成路径段以区分作业路径和移动路径
        List<Lunjingguihua.PathSegment> segments = null;
        if (boundaryCoords != null && mowingWidth != null) {
            try {
                // 解析割草模式
                String mode = "parallel"; // 默认平行模式
                if (mowingPattern != null && !mowingPattern.trim().isEmpty()) {
                    String pattern = mowingPattern.trim().toLowerCase();
                    if ("1".equals(pattern) || "spiral".equals(pattern) || "螺旋式".equals(pattern) || "螺旋".equals(pattern)) {
                        mode = "spiral";
                    } else if ("parallel".equals(pattern) || "平行线".equals(pattern) || "平行".equals(pattern)) {
                        mode = "parallel";
                    }
                }
                segments = Lunjingguihua.generatePathSegments(
                    boundaryCoords, 
                    obstaclesCoords != null ? obstaclesCoords : "", 
                    mowingWidth, 
                    safetyDistance, 
                    mode
                );
            } catch (Exception e) {
                // 如果重新生成失败,使用简单绘制方式
                segments = null;
            }
        }
        
        // 3. 根据是否有段信息选择不同的绘制方式
        if (segments != null && !segments.isEmpty()) {
            // 有段信息:分别绘制作业路径和移动路径
            drawPathSegments(g2d, segments, scale, arrowScale);
        } else {
            // 无段信息:使用简单绘制方式(所有路径使用作业路径颜色)
            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结束
            
            g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // 设置圆头圆角描边
            g2d.setColor(MOWING_PATH_COLOR); // 设置作业路径颜色(提香红70%透明度)
            g2d.draw(polyline); // 绘制折线
            
            Point2D.Double start = path.get(0); // 起点
            Point2D.Double second = path.get(1); // 第二个点
            Point2D.Double end = path.get(path.size() - 1); // 终点
            Point2D.Double prev = path.get(path.size() - 2); // 倒数第二个点
 
            drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale); // 绘制起点箭头
            drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale); // 绘制终点箭头
        }
 
        g2d.setStroke(previous); // 恢复原描边
    } // 方法结束
    
    /** // 文档注释开始
     * 绘制内缩边界(围边)- 马尔斯绿色
     */ // 文档注释结束
    private static void drawInnerBoundary(Graphics2D g2d, String boundaryCoords, String safetyDistanceStr, double scale) {
        try {
            List<Coordinate> boundary = Lunjingguihua.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 void drawPathSegments(Graphics2D g2d, List<Lunjingguihua.PathSegment> segments, double scale, double arrowScale) {
        if (segments == null || segments.isEmpty()) {
            return;
        }
        
        float strokeWidth = (float) (2.5 / Math.max(0.5, scale));
        Stroke previous = g2d.getStroke();
        
        // 分别绘制作业路径和移动路径
        Path2D.Double mowingPath = new Path2D.Double();
        Path2D.Double travelPath = new Path2D.Double();
        boolean mowingStarted = false;
        boolean travelStarted = false;
        
        Coordinate lastMowingEnd = null;
        Coordinate lastTravelEnd = null;
        
        for (Lunjingguihua.PathSegment seg : segments) {
            if (seg == null || seg.start == null || seg.end == null) {
                continue;
            }
            
            if (seg.isMowing) {
                // 作业路径 - 提香红70%透明度
                if (!mowingStarted || lastMowingEnd == null || !equals2D(lastMowingEnd, seg.start)) {
                    mowingPath.moveTo(seg.start.x, seg.start.y);
                    mowingStarted = true;
                }
                mowingPath.lineTo(seg.end.x, seg.end.y);
                lastMowingEnd = seg.end;
            } else {
                // 移动路径 - 蓝色虚线
                if (!travelStarted || lastTravelEnd == null || !equals2D(lastTravelEnd, seg.start)) {
                    travelPath.moveTo(seg.start.x, seg.start.y);
                    travelStarted = true;
                }
                travelPath.lineTo(seg.end.x, seg.end.y);
                lastTravelEnd = seg.end;
            }
        }
        
        // 绘制作业路径
        if (mowingStarted) {
            g2d.setColor(MOWING_PATH_COLOR); // 提香红70%透明度
            g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
            g2d.draw(mowingPath);
        }
        
        // 绘制移动路径(虚线)
        if (travelStarted) {
            g2d.setColor(TRAVEL_PATH_COLOR); // 蓝色
            g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, DASH_PATTERN, 0));
            g2d.draw(travelPath);
        }
        
        // 绘制起点和终点箭头
        if (!segments.isEmpty()) {
            Lunjingguihua.PathSegment firstSeg = segments.get(0);
            if (firstSeg != null && firstSeg.start != null && segments.size() > 1) {
                Lunjingguihua.PathSegment secondSeg = segments.get(1);
                if (secondSeg != null && secondSeg.start != null) {
                    Point2D.Double start = new Point2D.Double(firstSeg.start.x, firstSeg.start.y);
                    Point2D.Double second = new Point2D.Double(secondSeg.start.x, secondSeg.start.y);
                    drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale);
                }
            }
            
            Lunjingguihua.PathSegment lastSeg = segments.get(segments.size() - 1);
            if (lastSeg != null && lastSeg.end != null && segments.size() > 1) {
                Lunjingguihua.PathSegment prevSeg = segments.get(segments.size() - 2);
                if (prevSeg != null && prevSeg.end != null) {
                    Point2D.Double prev = new Point2D.Double(prevSeg.end.x, prevSeg.end.y);
                    Point2D.Double end = new Point2D.Double(lastSeg.end.x, lastSeg.end.y);
                    drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale);
                }
            }
        }
        
        g2d.setStroke(previous);
    } // 方法结束
    
    /** // 文档注释开始
     * 比较两个坐标是否相同(容差)
     */ // 文档注释结束
    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); // 填充箭头
    } // 方法结束
} // 类结束