| | |
| | | remainingSegments.addAll(clippedSegments); |
| | | } |
| | | |
| | | // 3. 重新连接路径段(弓字形连接) |
| | | return reconnectSegments(remainingSegments); |
| | | // 3. 重新连接路径段(弓字形连接,智能处理边界穿越) |
| | | return reconnectSegments(remainingSegments, polygon); |
| | | } |
| | | |
| | | /** |
| | |
| | | return result; |
| | | } else { |
| | | // 一端在内部,一端在外部 |
| | | Point insidePoint = startInside ? segment.start : segment.end; |
| | | Point outsidePoint = startInside ? segment.end : segment.start; |
| | | |
| | | List<Point> intersections = obstacle.getIntersections(segment); |
| | |
| | | |
| | | /** |
| | | * 重新连接路径段,形成连续弓字形路径 |
| | | * 优化:智能处理边界穿越,当换行路径穿越边界时,沿边界行走 |
| | | */ |
| | | private static List<PathSegment> reconnectSegments(List<PathSegment> segments) { |
| | | private static List<PathSegment> reconnectSegments(List<PathSegment> segments, List<Point> boundary) { |
| | | if (segments.isEmpty()) return new ArrayList<>(); |
| | | |
| | | List<PathSegment> reconnected = new ArrayList<>(); |
| | |
| | | if (seg.isMowing) { |
| | | // 割草段:检查是否需要添加空走段 |
| | | if (distance(currentPos, seg.start) > 0.01) { |
| | | reconnected.add(new PathSegment(currentPos, seg.start, false)); |
| | | // 使用智能连接方法生成换行路径 |
| | | List<PathSegment> connectionPath = buildSmartConnection(currentPos, seg.start, boundary); |
| | | reconnected.addAll(connectionPath); |
| | | } |
| | | reconnected.add(seg); |
| | | currentPos = seg.end; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 智能连接两点:如果直线不穿越边界则直接连接,否则使用直线+边界混合路径 |
| | | * 优化逻辑: |
| | | * 1. 如果AB线不穿越边界C,直接使用AB作为换行路线 |
| | | * 2. 如果AB线穿越了边界C,找到所有交点,将AB分成多个段 |
| | | * - 对于在边界内部的段(如DF段、GH段),沿边界行走 |
| | | * - 对于在边界外部的段,沿AB直线行走 |
| | | * 路径示例:A → D(直线) → F(沿边界) → G(直线) → H(沿边界) → B(直线) |
| | | * |
| | | * @param pointA 起点(上一段结束的终点) |
| | | * @param pointB 终点(下一段需要割草路径的起始点) |
| | | * @param boundary 安全内缩边界C |
| | | * @return 连接路径段列表(全部为isMowing=false的空走段) |
| | | */ |
| | | private static List<PathSegment> buildSmartConnection(Point pointA, Point pointB, List<Point> boundary) { |
| | | List<PathSegment> result = new ArrayList<>(); |
| | | |
| | | // 1. 检查AB直线是否穿越边界C |
| | | if (!segmentIntersectsBoundary(pointA, pointB, boundary)) { |
| | | // 不穿越边界,直接使用AB作为换行路线 |
| | | result.add(new PathSegment(pointA, pointB, false)); |
| | | return result; |
| | | } |
| | | |
| | | // 2. AB线穿越了边界C,需要找到所有交点 |
| | | List<IntersectionInfo> intersections = getAllBoundaryIntersections(pointA, pointB, boundary); |
| | | |
| | | if (intersections.isEmpty()) { |
| | | // 没有交点(不应该发生,但安全处理),使用直线 |
| | | result.add(new PathSegment(pointA, pointB, false)); |
| | | return result; |
| | | } |
| | | |
| | | // 3. 按距离起点A的距离排序交点 |
| | | intersections.sort(Comparator.comparingDouble(inter -> distance(pointA, inter.point))); |
| | | |
| | | // 4. 构建完整的点序列:A, I1, I2, ..., In, B(I为交点) |
| | | List<Point> pointSequence = new ArrayList<>(); |
| | | pointSequence.add(pointA); |
| | | for (IntersectionInfo inter : intersections) { |
| | | pointSequence.add(inter.point); |
| | | } |
| | | pointSequence.add(pointB); |
| | | |
| | | // 5. 处理每两个相邻点之间的段 |
| | | Point currentPos = pointA; |
| | | |
| | | for (int i = 0; i < pointSequence.size() - 1; i++) { |
| | | Point p1 = pointSequence.get(i); |
| | | Point p2 = pointSequence.get(i + 1); |
| | | |
| | | // 判断p1到p2的段(AB线段的一部分)是否在边界C内部(检查中点) |
| | | Point midPoint = new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); |
| | | boolean segmentInsideBoundary = isPointInPolygon(midPoint, boundary); |
| | | |
| | | if (segmentInsideBoundary) { |
| | | // 段在边界内部(如DF段、GH段),需要沿边界行走 |
| | | if (i == 0) { |
| | | // 第一个段:从A到第一个交点D |
| | | // 如果段在边界内部,说明A在边界内部,需要先从A沿边界走到第一个交点 |
| | | IntersectionInfo firstInter = intersections.get(0); |
| | | SnapResult snapA = snapToBoundary(currentPos, boundary); |
| | | List<PathSegment> boundaryPath = getBoundaryPathBetweenPoints( |
| | | snapA.onEdge, snapA.edgeIndex, |
| | | firstInter.point, firstInter.edgeIndex, |
| | | boundary); |
| | | result.addAll(boundaryPath); |
| | | currentPos = firstInter.point; |
| | | } else if (i == pointSequence.size() - 2) { |
| | | // 最后一个段:从最后一个交点H到B |
| | | // 需要沿边界从当前点(应该是最后一个交点H)到B在边界上的投影 |
| | | IntersectionInfo lastInter = intersections.get(intersections.size() - 1); |
| | | SnapResult snapB = snapToBoundary(pointB, boundary); |
| | | List<PathSegment> boundaryPath = getBoundaryPathBetweenPoints( |
| | | currentPos, lastInter.edgeIndex, |
| | | snapB.onEdge, snapB.edgeIndex, |
| | | boundary); |
| | | result.addAll(boundaryPath); |
| | | // 如果B不在边界上,从边界投影直线到B |
| | | if (distance(snapB.onEdge, pointB) > 1e-6) { |
| | | result.add(new PathSegment(snapB.onEdge, pointB, false)); |
| | | } |
| | | currentPos = pointB; |
| | | } else { |
| | | // 中间段:两个交点之间的段(都在边界上),沿边界行走 |
| | | IntersectionInfo inter1 = intersections.get(i - 1); |
| | | IntersectionInfo inter2 = intersections.get(i); |
| | | List<PathSegment> boundaryPath = getBoundaryPathBetweenPoints( |
| | | inter1.point, inter1.edgeIndex, |
| | | inter2.point, inter2.edgeIndex, |
| | | boundary); |
| | | result.addAll(boundaryPath); |
| | | currentPos = inter2.point; |
| | | } |
| | | } else { |
| | | // 段在边界外部,可以直接沿AB直线连接(如A到D,F到G,H到B) |
| | | if (distance(p1, p2) > 1e-6) { |
| | | // 如果当前点不在p1,先连接到p1 |
| | | if (distance(currentPos, p1) > 1e-6) { |
| | | result.add(new PathSegment(currentPos, p1, false)); |
| | | } |
| | | // 从p1直线到p2 |
| | | result.add(new PathSegment(p1, p2, false)); |
| | | currentPos = p2; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 检查线段是否穿越边界(与边界边相交,不包括端点) |
| | | */ |
| | | private static boolean segmentIntersectsBoundary(Point a, Point b, List<Point> boundary) { |
| | | for (int i = 0; i < boundary.size(); i++) { |
| | | Point c = boundary.get(i); |
| | | Point d = boundary.get((i + 1) % boundary.size()); |
| | | // 忽略共享端点的相交 |
| | | if (isSamePoint(a, c) || isSamePoint(a, d) || isSamePoint(b, c) || isSamePoint(b, d)) { |
| | | continue; |
| | | } |
| | | if (segmentsIntersect(a, b, c, d)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 获取线段与边界的所有交点信息(包括点和对应边索引) |
| | | */ |
| | | private static List<IntersectionInfo> getAllBoundaryIntersections(Point a, Point b, List<Point> boundary) { |
| | | List<IntersectionInfo> intersections = new ArrayList<>(); |
| | | |
| | | for (int i = 0; i < boundary.size(); i++) { |
| | | Point c = boundary.get(i); |
| | | Point d = boundary.get((i + 1) % boundary.size()); |
| | | |
| | | // 忽略共享端点 |
| | | if (isSamePoint(a, c) || isSamePoint(a, d) || isSamePoint(b, c) || isSamePoint(b, d)) { |
| | | continue; |
| | | } |
| | | |
| | | Point intersection = getLineIntersection(a, b, c, d); |
| | | if (intersection != null) { |
| | | intersections.add(new IntersectionInfo(intersection, i)); |
| | | } |
| | | } |
| | | |
| | | return intersections; |
| | | } |
| | | |
| | | /** |
| | | * 获取边界上两点之间的路径(沿边界行走) |
| | | * @param start 起点(必须在边界上) |
| | | * @param startEdgeIndex 起点所在的边索引 |
| | | * @param end 终点(必须在边界上) |
| | | * @param endEdgeIndex 终点所在的边索引 |
| | | * @param boundary 边界点列表 |
| | | * @return 沿边界的路径段列表 |
| | | */ |
| | | private static List<PathSegment> getBoundaryPathBetweenPoints( |
| | | Point start, int startEdgeIndex, |
| | | Point end, int endEdgeIndex, |
| | | List<Point> boundary) { |
| | | |
| | | List<PathSegment> result = new ArrayList<>(); |
| | | |
| | | if (startEdgeIndex == endEdgeIndex) { |
| | | // 在同一条边上,直接连接 |
| | | if (distance(start, end) > 1e-6) { |
| | | result.add(new PathSegment(start, end, false)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | int n = boundary.size(); |
| | | |
| | | // 计算顺时针路径 |
| | | List<Point> pathClockwise = new ArrayList<>(); |
| | | pathClockwise.add(start); |
| | | |
| | | int curr = startEdgeIndex; |
| | | while (curr != endEdgeIndex) { |
| | | pathClockwise.add(boundary.get((curr + 1) % n)); |
| | | curr = (curr + 1) % n; |
| | | } |
| | | pathClockwise.add(end); |
| | | |
| | | // 计算逆时针路径 |
| | | List<Point> pathCounterClockwise = new ArrayList<>(); |
| | | pathCounterClockwise.add(start); |
| | | curr = startEdgeIndex; |
| | | while (curr != endEdgeIndex) { |
| | | pathCounterClockwise.add(boundary.get(curr)); |
| | | curr = (curr - 1 + n) % n; |
| | | } |
| | | pathCounterClockwise.add(end); |
| | | |
| | | // 选择较短的路径 |
| | | List<Point> chosenPath = getPathLength(pathClockwise) < getPathLength(pathCounterClockwise) |
| | | ? pathClockwise : pathCounterClockwise; |
| | | |
| | | // 转换为路径段 |
| | | for (int i = 0; i < chosenPath.size() - 1; i++) { |
| | | if (distance(chosenPath.get(i), chosenPath.get(i + 1)) > 1e-6) { |
| | | result.add(new PathSegment(chosenPath.get(i), chosenPath.get(i + 1), false)); |
| | | } |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 计算路径总长度 |
| | | */ |
| | | private static double getPathLength(List<Point> path) { |
| | | double len = 0; |
| | | for (int i = 0; i < path.size() - 1; i++) { |
| | | len += distance(path.get(i), path.get(i + 1)); |
| | | } |
| | | return len; |
| | | } |
| | | |
| | | /** |
| | | * 判断两个点是否相同(考虑浮点误差) |
| | | */ |
| | | private static boolean isSamePoint(Point a, Point b) { |
| | | return Math.abs(a.x - b.x) < 1e-6 && Math.abs(a.y - b.y) < 1e-6; |
| | | } |
| | | |
| | | /** |
| | | * 判断两条线段是否相交(不包括端点) |
| | | */ |
| | | private static boolean segmentsIntersect(Point a, Point b, Point c, Point d) { |
| | | return ccw(a, c, d) != ccw(b, c, d) && ccw(a, b, c) != ccw(a, b, d); |
| | | } |
| | | |
| | | /** |
| | | * 判断三点是否逆时针排列 |
| | | */ |
| | | private static boolean ccw(Point a, Point b, Point c) { |
| | | return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x); |
| | | } |
| | | |
| | | /** |
| | | * 交点信息内部类 |
| | | */ |
| | | private static class IntersectionInfo { |
| | | Point point; // 交点坐标 |
| | | int edgeIndex; // 交点所在的边界边索引 |
| | | |
| | | IntersectionInfo(Point point, int edgeIndex) { |
| | | this.point = point; |
| | | this.edgeIndex = edgeIndex; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 边界吸附结果内部类 |
| | | */ |
| | | private static class SnapResult { |
| | | Point onEdge; // 在边界上的投影点 |
| | | int edgeIndex; // 所在的边界边索引 |
| | | |
| | | SnapResult(Point p, int idx) { |
| | | this.onEdge = p; |
| | | this.edgeIndex = idx; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 计算点到边界最近的投影点以及所在边索引 |
| | | * @param p 要吸附的点 |
| | | * @param poly 边界多边形 |
| | | * @return 吸附结果 |
| | | */ |
| | | private static SnapResult snapToBoundary(Point p, List<Point> poly) { |
| | | double minD = Double.MAX_VALUE; |
| | | Point bestProj = p; |
| | | int bestIdx = -1; |
| | | for (int i = 0; i < poly.size(); i++) { |
| | | Point s = poly.get(i); |
| | | Point e = poly.get((i + 1) % poly.size()); |
| | | double l2 = (s.x - e.x) * (s.x - e.x) + (s.y - e.y) * (s.y - e.y); |
| | | if (l2 < 1e-10) { |
| | | double d = Math.hypot(p.x - s.x, p.y - s.y); |
| | | if (d < minD) { |
| | | minD = d; |
| | | bestProj = s; |
| | | bestIdx = i; |
| | | } |
| | | continue; |
| | | } |
| | | double t = ((p.x - s.x) * (e.x - s.x) + (p.y - s.y) * (e.y - s.y)) / l2; |
| | | t = Math.max(0, Math.min(1, t)); |
| | | Point proj = new Point(s.x + t * (e.x - s.x), s.y + t * (e.y - s.y)); |
| | | double d = Math.hypot(p.x - proj.x, p.y - proj.y); |
| | | if (d < minD) { |
| | | minD = d; |
| | | bestProj = proj; |
| | | bestIdx = i; |
| | | } |
| | | } |
| | | return new SnapResult(bestProj, bestIdx == -1 ? 0 : bestIdx); |
| | | } |
| | | |
| | | /** |
| | | * 判断点是否在边界上(距离边界很近) |
| | | * @param p 要检查的点 |
| | | * @param boundary 边界多边形 |
| | | * @return 是否在边界上 |
| | | */ |
| | | @SuppressWarnings("unused") |
| | | private static boolean isPointOnBoundary(Point p, List<Point> boundary) { |
| | | double threshold = 1e-4; // 阈值,考虑浮点误差 |
| | | for (int i = 0; i < boundary.size(); i++) { |
| | | Point s = boundary.get(i); |
| | | Point e = boundary.get((i + 1) % boundary.size()); |
| | | double dist = distToSegment(p, s, e); |
| | | if (dist < threshold) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 计算点到线段的距离 |
| | | * @param p 点 |
| | | * @param s 线段起点 |
| | | * @param e 线段终点 |
| | | * @return 距离 |
| | | */ |
| | | private static double distToSegment(Point p, Point s, Point e) { |
| | | double l2 = (s.x - e.x) * (s.x - e.x) + (s.y - e.y) * (s.y - e.y); |
| | | if (l2 < 1e-10) { |
| | | return Math.hypot(p.x - s.x, p.y - s.y); |
| | | } |
| | | double t = ((p.x - s.x) * (e.x - s.x) + (p.y - s.y) * (e.y - s.y)) / l2; |
| | | t = Math.max(0, Math.min(1, t)); |
| | | return Math.hypot(p.x - (s.x + t * (e.x - s.x)), p.y - (s.y + t * (e.y - s.y))); |
| | | } |
| | | |
| | | /** |
| | | * 生成原始扫描路径(无障碍物版本) |
| | | */ |
| | | private static List<PathSegment> generateGlobalScanPath( |