package lujing; import org.locationtech.jts.algorithm.Angle; import org.locationtech.jts.geom.*; import org.locationtech.jts.operation.distance.DistanceOp; import org.locationtech.jts.operation.union.CascadedPolygonUnion; import java.util.*; import java.util.stream.Collectors; /** * 障碍物路径规划器 * * 优化方案: * 1. 预规划:先不考虑障碍物,规划出覆盖全图的平行线路径(弓字形)。 * 2. 几何切割:根据障碍物的外扩(安全距离)几何体,将路径切断,移除落在障碍物内的部分。 * 3. 区域重组:将剩余的路径段按连通性聚类为若干个“连续区域”,每个区域内部通过弓字形连接。 * 4. 全局连接:使用避障算法(A*可视图)将这些孤立的区域串联起来。 * * * 修复问题: * 1. 修复路径穿越障碍物的问题(通过更严格的 Interior Intersection 检查)。 * 2. 修复路径超出地块边界的问题(通过加入 Boundary Covers 约束)。 * 3. [本次修复] 修复 IntersectionMatrix 类型报错,使用正确的矩阵单元格检查替代不存在的方法。 * * * 二次优化(针对用户反馈): * 1. 引入 Tolerance Buffer (内缩) 用于相交检测,解决“接触即相交”导致的合法边缘路径被误判问题。 * 2. 增强 isLineSafe 的边界约束,确保路径严格在 Boundary 内部。 * 3. 优化 findSafePath,增加点位校验与吸附(Snap),防止因浮点误差导致的寻路失败而回退到直线。 */ public class ObstaclePathPlanner { private final List polygon; // 割草区域边界点集 private final double width; // 割草宽度 private final String mode; // 模式 private final List> obstacles; // 障碍物列表 private final double safetyDistance; // 安全距离 private final GeometryFactory gf = new GeometryFactory(); // 保存地块的几何形状,用于边界约束检查 private Geometry boundaryGeom; // 用于检测的障碍物几何体(可能经过微调) private Geometry checkObstacleGeom; public ObstaclePathPlanner(List polygon, double width, String mode, List> obstacles, double safetyDistance) { this.polygon = polygon; this.width = width; this.mode = mode; this.obstacles = obstacles != null ? obstacles : new ArrayList<>(); this.safetyDistance = safetyDistance; initBoundaryGeom(); } /** * 初始化边界几何体,用于后续的约束检查 */ private void initBoundaryGeom() { if (polygon == null || polygon.size() < 3) { this.boundaryGeom = gf.createPolygon(); return; } List closed = new ArrayList<>(polygon); if (!closed.get(0).equals2D(closed.get(closed.size() - 1))) { closed.add(new Coordinate(closed.get(0))); } LinearRing ring = gf.createLinearRing(closed.toArray(new Coordinate[0])); Polygon poly = gf.createPolygon(ring); // 确保几何有效性 this.boundaryGeom = poly.isValid() ? poly : poly.buffer(0); } /** * 生成路径的主入口 */ public List generate() { // 1. 生成初始的完整平行线路径(忽略障碍物) List fullPath = generateFullPathWithoutObstacles(); if (fullPath.isEmpty()) return fullPath; // 2. 构建障碍物的外扩安全区域 (Buffer) Geometry obstacleBuffer = createObstacleBuffer(); // 初始化用于检测的几何体(内缩一点点,允许路径贴边) if (!obstacleBuffer.isEmpty()) { this.checkObstacleGeom = obstacleBuffer.buffer(-0.01); // 内缩1cm,容忍浮点误差 } else { this.checkObstacleGeom = obstacleBuffer; } if (obstacleBuffer.isEmpty()) return fullPath; // 3. 切割路径:移除与障碍物重叠的部分 List clippedSegments = clipPathWithObstacles(fullPath, obstacleBuffer); if (clippedSegments.isEmpty()) return new ArrayList<>(); // 4. 重新规划:将碎片化的线段重组成连续的弓字形路径,并进行避障连接 List finalPath = reorganizeAndConnectPath(clippedSegments, obstacleBuffer); // 5. 后处理(标记起终点等) postProcessPath(finalPath); return finalPath; } /** * 步骤1:调用核心库生成无障碍的平行线路径 */ private List generateFullPathWithoutObstacles() { Lunjingguihua.PlannerCore tempPlanner = new Lunjingguihua.PlannerCore( polygon, width, mode, new ArrayList<>()); return tempPlanner.generateParallelPath(); } /** * 步骤2:创建所有障碍物的并集 + 安全距离外扩 */ private Geometry createObstacleBuffer() { if (obstacles.isEmpty()) return gf.createPolygon(); List geoms = new ArrayList<>(); for (List obs : obstacles) { if (obs == null || obs.size() < 3) continue; List closed = new ArrayList<>(obs); if (!closed.get(0).equals2D(closed.get(closed.size() - 1))) { closed.add(new Coordinate(closed.get(0))); } LinearRing ring = gf.createLinearRing(closed.toArray(new Coordinate[0])); Polygon poly = gf.createPolygon(ring); if (!poly.isValid()) poly = (Polygon) poly.buffer(0); geoms.add(poly); } if (geoms.isEmpty()) return gf.createPolygon(); Geometry union = CascadedPolygonUnion.union(geoms); // 对合并后的障碍物进行外扩(Buffer) Geometry buffered = union.buffer(safetyDistance, 8); return buffered.isValid() ? buffered : buffered.buffer(0); } /** * 步骤3:用障碍物几何体切割路径 */ private List clipPathWithObstacles(List fullPath, Geometry obstacleBuffer) { List validSegments = new ArrayList<>(); for (Lunjingguihua.PathSegment seg : fullPath) { if (!seg.isMowing) continue; LineString line = gf.createLineString(new Coordinate[]{seg.start, seg.end}); Geometry diff; try { diff = line.difference(obstacleBuffer); } catch (Exception e) { continue; } if (diff.isEmpty()) continue; for (int i = 0; i < diff.getNumGeometries(); i++) { Geometry g = diff.getGeometryN(i); if (g instanceof LineString) { Coordinate[] coords = g.getCoordinates(); for (int k = 0; k < coords.length - 1; k++) { Coordinate p1 = coords[k]; Coordinate p2 = coords[k+1]; if (p1.distance(p2) > 0.1) { validSegments.add(new Lunjingguihua.PathSegment(p1, p2, true)); } } } } } return validSegments; } /** * 步骤4:核心重组逻辑 */ private List reorganizeAndConnectPath(List segments, Geometry obstacleBuffer) { if (segments.isEmpty()) return new ArrayList<>(); // --- A. 分析扫描线方向 --- Lunjingguihua.PathSegment firstSeg = segments.get(0); double angle = Math.atan2(firstSeg.end.y - firstSeg.start.y, firstSeg.end.x - firstSeg.start.x); double sin = Math.sin(angle); double cos = Math.cos(angle); // --- B. 给每个线段分配扫描线索引 (Grid Index) --- double gridStep = width; class IndexedSegment { final Lunjingguihua.PathSegment segment; final int gridIndex; final double projectVal; IndexedSegment(Lunjingguihua.PathSegment s) { this.segment = s; double cx = (s.start.x + s.end.x) / 2; double cy = (s.start.y + s.end.y) / 2; double perpDist = -cx * sin + cy * cos; this.gridIndex = (int) Math.floor(perpDist / gridStep); this.projectVal = cx * cos + cy * sin; } } List indexedSegments = segments.stream() .map(IndexedSegment::new) .sorted(Comparator.comparingInt((IndexedSegment s) -> s.gridIndex) .thenComparingDouble(s -> s.projectVal)) .collect(Collectors.toList()); // --- C. 构建“区域链” (Zones) --- List> zones = new ArrayList<>(); Set visited = new HashSet<>(); while (visited.size() < indexedSegments.size()) { IndexedSegment startNode = null; for (IndexedSegment is : indexedSegments) { if (!visited.contains(is)) { startNode = is; break; } } if (startNode == null) break; List zone = new ArrayList<>(); zone.add(startNode.segment); visited.add(startNode); IndexedSegment current = startNode; boolean lookingForNext = true; while (lookingForNext) { IndexedSegment bestNext = null; double minDistance = Double.MAX_VALUE; // 搜索最佳后续线段 for (int i = 0; i < indexedSegments.size(); i++) { IndexedSegment candidate = indexedSegments.get(i); if (visited.contains(candidate)) continue; if (Math.abs(candidate.gridIndex - current.gridIndex) > 1) continue; double d = current.segment.end.distance(candidate.segment.start); if (d > width * 3.0) continue; if (d < minDistance) { // 使用 checkObstacleGeom 进行检测 if (isLineSafe(current.segment.end, candidate.segment.start)) { minDistance = d; bestNext = candidate; } } } if (bestNext != null) { zone.add(bestNext.segment); visited.add(bestNext); current = bestNext; } else { lookingForNext = false; } } zones.add(zone); } // --- D. 连接所有 Zones --- List resultPath = new ArrayList<>(); List> remainingZones = new ArrayList<>(zones); List currentProcessingZone = remainingZones.remove(0); addZoneToPath(resultPath, currentProcessingZone, obstacleBuffer, false); while (!remainingZones.isEmpty()) { Lunjingguihua.PathSegment lastSeg = resultPath.get(resultPath.size() - 1); Coordinate currentPos = lastSeg.end; int bestZoneIdx = -1; double minDist = Double.MAX_VALUE; for (int i = 0; i < remainingZones.size(); i++) { List z = remainingZones.get(i); if (z.isEmpty()) continue; double d = currentPos.distance(z.get(0).start); if (d < minDist) { minDist = d; bestZoneIdx = i; } } if (bestZoneIdx != -1) { List nextZone = remainingZones.remove(bestZoneIdx); addZoneToPath(resultPath, nextZone, obstacleBuffer, true); } else { break; } } return resultPath; } /** * 将一个 Zone 添加到结果路径中 */ private void addZoneToPath(List path, List zone, Geometry obstacleBuffer, boolean needConnectToZoneStart) { if (zone.isEmpty()) return; // 1. 连接到 Zone 的起点 if (needConnectToZoneStart && !path.isEmpty()) { Coordinate from = path.get(path.size() - 1).end; Coordinate to = zone.get(0).start; List travel = findSafePath(from, to, obstacleBuffer); if (travel.size() > 1) { for (int i = 0; i < travel.size() - 1; i++) { path.add(new Lunjingguihua.PathSegment(travel.get(i), travel.get(i+1), false)); } } } // 2. 处理 Zone 内部 for (int i = 0; i < zone.size(); i++) { Lunjingguihua.PathSegment seg = zone.get(i); if (i > 0) { Coordinate prevEnd = zone.get(i-1).end; Coordinate currStart = seg.start; if (!prevEnd.equals2D(currStart)) { if (isLineSafe(prevEnd, currStart)) { path.add(new Lunjingguihua.PathSegment(prevEnd, currStart, false)); } else { List detour = findSafePath(prevEnd, currStart, obstacleBuffer); if (detour.size() > 1) { for (int k = 0; k < detour.size() - 1; k++) { path.add(new Lunjingguihua.PathSegment(detour.get(k), detour.get(k+1), false)); } } } } } path.add(seg); } } /** * 检查两点连线是否安全 * 修改点: * 1. 严格检查 Boundary (Covers) * 2. 使用 checkObstacleGeom (内缩版) 检查障碍物,允许贴边 * 3. [Fix] 使用 matrix.get() 代替不存在的 isIntersects(int) */ private boolean isLineSafe(Coordinate p1, Coordinate p2) { if (p1.equals2D(p2)) return true; LineString line = gf.createLineString(new Coordinate[]{p1, p2}); // 1. 边界约束:线段必须完全在地块内部 if (boundaryGeom != null && !boundaryGeom.covers(line)) { return false; } // 2. 避障约束:使用内缩后的 buffer 检查 // 如果 checkObstacleGeom 为空(无障碍),则安全 if (checkObstacleGeom == null || checkObstacleGeom.isEmpty()) return true; IntersectionMatrix matrix = line.relate(checkObstacleGeom); // 我们要检查:线段的任何部分(Interior)是否穿过障碍物内部(Interior) // 或者 线段的端点(Boundary)是否在障碍物内部(Interior) // 如果两者都是 Dimension.FALSE (-1),则说明没有穿过内部 boolean interiorIntersects = matrix.get(Location.INTERIOR, Location.INTERIOR) != Dimension.FALSE; boolean boundaryIntersects = matrix.get(Location.BOUNDARY, Location.INTERIOR) != Dimension.FALSE; return !interiorIntersects && !boundaryIntersects; } /** * 寻找两点间的安全路径 * 修改点: * 1. 增加点位校验与吸附(Snap),确保起点终点有效 * 2. 移除直线强制回退,若寻路失败则返回空(或保留起点),避免穿墙 */ private List findSafePath(Coordinate start, Coordinate end, Geometry obstacleBuffer) { // 0. 数据清洗:吸附起点终点到合法区域 Coordinate safeStart = snapPointToValid(start); Coordinate safeEnd = snapPointToValid(end); List path = new ArrayList<>(); // 1. 尝试直连 if (isLineSafe(safeStart, safeEnd)) { path.add(safeStart); path.add(safeEnd); return path; } // 2. 构建可视图 Set nodes = new HashSet<>(); nodes.add(safeStart); nodes.add(safeEnd); // 提取障碍物顶点 addPolygonVertices(obstacleBuffer, nodes); // 提取边界顶点(关键:处理凹形边界) addPolygonVertices(boundaryGeom, nodes); List nodeList = new ArrayList<>(nodes); // 构建邻接表 Map> graph = new HashMap<>(); for (Coordinate c1 : nodeList) { for (Coordinate c2 : nodeList) { if (c1 == c2) continue; if (isLineSafe(c1, c2)) { graph.computeIfAbsent(c1, k -> new ArrayList<>()).add(c2); } } } // Dijkstra 寻路 Map dist = new HashMap<>(); Map prev = new HashMap<>(); PriorityQueue pq = new PriorityQueue<>(Comparator.comparingDouble(dist::get)); for (Coordinate n : nodeList) dist.put(n, Double.MAX_VALUE); dist.put(safeStart, 0.0); pq.add(safeStart); while (!pq.isEmpty()) { Coordinate u = pq.poll(); if (u.equals2D(safeEnd)) break; if (dist.get(u) == Double.MAX_VALUE) break; if (graph.containsKey(u)) { for (Coordinate v : graph.get(u)) { double alt = dist.get(u) + u.distance(v); if (alt < dist.get(v)) { dist.put(v, alt); prev.put(v, u); pq.add(v); } } } } // 重构路径 if (prev.containsKey(safeEnd)) { LinkedList p = new LinkedList<>(); Coordinate curr = safeEnd; while (curr != null) { p.addFirst(curr); curr = prev.get(curr); } return p; } // 寻路失败,返回空(避免错误的直线穿越) return path; } // 辅助:验证并吸附点到合法区域 private Coordinate snapPointToValid(Coordinate p) { Point point = gf.createPoint(p); boolean inBoundary = (boundaryGeom == null) || boundaryGeom.covers(point); boolean outObstacle = (checkObstacleGeom == null) || !checkObstacleGeom.contains(point); // 使用 contains 而不是 intersects interior,稍微严格点 if (inBoundary && outObstacle) return p; // 如果点无效,尝试找最近的有效点(边界或障碍物边缘) // 这里简化处理:如果在障碍物内,吸附到障碍物边界;如果在边界外,吸附到边界 // 实际上 JTS DistanceOp.nearestPoints 可以做这个 Geometry target = boundaryGeom; if (!outObstacle && checkObstacleGeom != null) { // 在障碍物内,优先吸附出障碍物 Coordinate[] nearest = DistanceOp.nearestPoints(point, checkObstacleGeom.getBoundary()); return nearest[1]; } if (!inBoundary && boundaryGeom != null) { Coordinate[] nearest = DistanceOp.nearestPoints(point, boundaryGeom); return nearest[1]; } return p; } private void addPolygonVertices(Geometry geom, Set nodes) { if (geom == null) return; if (geom instanceof Polygon) { Collections.addAll(nodes, ((Polygon) geom).getExteriorRing().getCoordinates()); for(int i=0; i<((Polygon)geom).getNumInteriorRing(); i++) { Collections.addAll(nodes, ((Polygon)geom).getInteriorRingN(i).getCoordinates()); } } else if (geom instanceof MultiPolygon) { MultiPolygon mp = (MultiPolygon) geom; for (int i = 0; i < mp.getNumGeometries(); i++) { addPolygonVertices(mp.getGeometryN(i), nodes); } } else if (geom instanceof GeometryCollection) { GeometryCollection gc = (GeometryCollection) geom; for (int i = 0; i < gc.getNumGeometries(); i++) { addPolygonVertices(gc.getGeometryN(i), nodes); } } } /** * 后处理:移除短线段,标记起终点 */ private void postProcessPath(List path) { if (path.isEmpty()) return; path.removeIf(seg -> seg.start.distance(seg.end) < 0.05); for (Lunjingguihua.PathSegment seg : path) { seg.isStartPoint = false; seg.isEndPoint = false; } boolean startFound = false; for (Lunjingguihua.PathSegment seg : path) { if (seg.isMowing) { seg.setAsStartPoint(); startFound = true; break; } } for (int i = path.size() - 1; i >= 0; i--) { Lunjingguihua.PathSegment seg = path.get(i); if (seg.isMowing) { seg.setAsEndPoint(); break; } } } }