| | |
| | | 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. // 方法说明 |
| | | */ // 文档注释结束 |
| | |
| | | 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. // 绘制路径 |
| | | */ // 文档注释结束 |
| | |
| | | * @param obstaclesCoords 障碍物坐标字符串 |
| | | * @param mowingPattern 割草模式("parallel"或"spiral") |
| | | */ // 文档注释结束 |
| | | public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale, double arrowScale, |
| | | 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; // 数据不足直接返回 |
| | |
| | | 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结束 |
| | | |
| | | g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // 设置圆头圆角描边 |
| | | g2d.setColor(MOWING_PATH_COLOR); // 设置作业路径颜色(提香红70%透明度) |
| | | g2d.draw(polyline); // 绘制折线 |
| | | // 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.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); // 绘制终点箭头 |
| | | |
| | | 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; // 检测异常时,按作业段处理以不中断绘制 |
| | | } |
| | | } |
| | | |
| | | /** // 文档注释开始 |
| | | * 绘制内缩边界(围边)- 马尔斯绿色 |