package lujing; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 异形草地路径规划 - 障碍物裁剪优化版 V9.0 * 核心逻辑:先生成全覆盖扫描路径,再利用外扩后的障碍物对路径进行裁剪。 */ public class YixinglujingHaveObstacel { /** * 规划路径主入口 */ public static List planPath(String coordinates, String obstaclesStr, String widthStr, String marginStr) { // 1. 解析参数 List rawPoints = parseCoordinates(coordinates); if (rawPoints.size() < 3) return new ArrayList<>(); double mowWidth = Double.parseDouble(widthStr); double safeMargin = Double.parseDouble(marginStr); // 2. 预处理地块边界 (确保逆时针) ensureCounterClockwise(rawPoints); // 3. 生成地块内缩的安全作业边界 (Inset) List mowingBoundary = getOffsetPolygon(rawPoints, safeMargin); // 正数内缩 if (mowingBoundary.size() < 3) return new ArrayList<>(); // 4. 第一步:生成“无视障碍物”的全覆盖扫描路径 // 直接使用扫描线算法生成填满整个内缩边界的路径 List rawPath = generateFullCoveragePath(mowingBoundary, mowWidth); // 5. 解析障碍物并进行外扩 (Outset) // 注意:障碍物外扩距离 = 割草机安全边距,确保不发生碰撞 List obstacles = parseObstacles(obstaclesStr, safeMargin); // 6. 第二步:使用障碍物裁剪路径 (核心步骤) return clipPathWithObstacles(rawPath, obstacles); } /** * 使用障碍物集合裁剪原始路径 */ private static List clipPathWithObstacles(List rawPath, List obstacles) { List finalPath = new ArrayList<>(); Point currentPos = (rawPath.isEmpty()) ? new Point(0,0) : rawPath.get(0).start; for (PathSegment segment : rawPath) { // 将当前这一段路径,拿去跟所有障碍物进行碰撞检测和裁剪 // 初始时,这一段是完整的 List segmentsToProcess = new ArrayList<>(); segmentsToProcess.add(segment); for (Obstacle obs : obstacles) { List nextIterSegments = new ArrayList<>(); for (PathSegment seg : segmentsToProcess) { // 如果是割草路径,需要裁剪;如果是空走路径,通常也需要避障, // 但这里主要处理扫描线的裁剪。 if (seg.isMowing) { nextIterSegments.addAll(obs.clip(seg)); } else { // 空走路径暂时保留(高级避障需要A*算法,此处简化为保留) nextIterSegments.add(seg); } } segmentsToProcess = nextIterSegments; } // 将裁剪后剩余的线段加入最终路径 for (PathSegment s : segmentsToProcess) { // 过滤掉因为裁剪产生的极短线段 if (distance(s.start, s.end) < 0.05) continue; // 如果当前点和线段起点不连贯,加入连接路径(空走) if (distance(currentPos, s.start) > 0.05) { finalPath.add(new PathSegment(currentPos, s.start, false)); } finalPath.add(s); currentPos = s.end; } } return finalPath; } // --- 路径生成核心算法 (移植自 NoObstacle 类) --- private static List generateFullCoveragePath(List boundary, double width) { // 1. 寻找最优角度 double angle = findOptimalAngle(boundary); // 2. 旋转多边形以对齐坐标轴 List rotatedPoly = new ArrayList<>(); for (Point p : boundary) rotatedPoly.add(rotatePoint(p, -angle)); double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE; for (Point p : rotatedPoly) { minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); } // 3. 生成扫描线 List segments = new ArrayList<>(); boolean l2r = true; // 围边路径先生成 Point scanStartPoint = null; // 这里我们先计算扫描线,最后再决定围边起点以减少空走 List> scanRows = new ArrayList<>(); for (double y = minY + width/2; y <= maxY - width/2; y += width) { List xInters = getXIntersections(rotatedPoly, y); if (xInters.size() < 2) continue; Collections.sort(xInters); List row = new ArrayList<>(); // 两两配对形成线段 for (int i = 0; i < xInters.size() - 1; i += 2) { Point s = rotatePoint(new Point(xInters.get(i), y), angle); Point e = rotatePoint(new Point(xInters.get(i + 1), y), angle); row.add(new PathSegment(s, e, true)); } // 蛇形排序 if (!l2r) { Collections.reverse(row); for (PathSegment s : row) { Point tmp = s.start; s.start = s.end; s.end = tmp; } } scanRows.add(row); if (scanStartPoint == null && !row.isEmpty()) scanStartPoint = row.get(0).start; l2r = !l2r; } // 4. 生成围边路径 (对齐到第一个扫描点) List alignedBoundary = alignBoundaryStart(boundary, scanStartPoint); for (int i = 0; i < alignedBoundary.size(); i++) { segments.add(new PathSegment(alignedBoundary.get(i), alignedBoundary.get((i+1)%alignedBoundary.size()), true)); } // 5. 加入扫描路径 for (List row : scanRows) { segments.addAll(row); } return segments; } // --- 障碍物处理类 --- private static List parseObstacles(String obsStr, double margin) { List list = new ArrayList<>(); if (obsStr == null || obsStr.trim().isEmpty()) return list; // 处理格式 (x,y;...)(x,y;...) 或 $ 分隔 String cleanStr = obsStr.replaceAll("\\s+", ""); String[] parts; if (cleanStr.contains("(") && cleanStr.contains(")")) { List matches = new ArrayList<>(); java.util.regex.Matcher m = java.util.regex.Pattern.compile("\\(([^)]+)\\)").matcher(cleanStr); while (m.find()) matches.add(m.group(1)); parts = matches.toArray(new String[0]); } else { parts = cleanStr.split("\\$"); } for (String pStr : parts) { List pts = parseCoordinates(pStr); if (pts.isEmpty()) continue; if (pts.size() == 2) { // 圆形障碍物 double r = distance(pts.get(0), pts.get(1)); list.add(new CircleObstacle(pts.get(0), r + margin)); // 半径增加margin } else { // 多边形障碍物 ensureCounterClockwise(pts); // 外扩障碍物 (Offset Out) // 注意:在通用偏移算法中,逆时针多边形,负数通常表示外扩,或者使用特定算法 // 这里我们复用 getOffsetPolygon,并传入负的margin来实现外扩 // *但在本类目前的 getOffsetPolygon 实现中(基于角平分线),如果是逆时针: // 正数是向左(内缩),负数是向右(外扩)* List expanded = getOffsetPolygon(pts, -margin); list.add(new PolyObstacle(expanded)); } } return list; } abstract static class Obstacle { // 返回裁剪后的线段列表(即保留在障碍物外部的线段) abstract List clip(PathSegment seg); } static class CircleObstacle extends Obstacle { Point c; double r; CircleObstacle(Point c, double r) { this.c = c; this.r = r; } @Override List clip(PathSegment seg) { // 计算直线与圆的交点 t值 (0..1) double dx = seg.end.x - seg.start.x; double dy = seg.end.y - seg.start.y; double fx = seg.start.x - c.x; double fy = seg.start.y - c.y; double A = dx*dx + dy*dy; double B = 2*(fx*dx + fy*dy); double C = (fx*fx + fy*fy) - r*r; double delta = B*B - 4*A*C; List result = new ArrayList<>(); if (delta < 0) { // 无交点,全保留或全剔除 if (!isInside(seg.start)) result.add(seg); return result; } double t1 = (-B - Math.sqrt(delta)) / (2*A); double t2 = (-B + Math.sqrt(delta)) / (2*A); List ts = new ArrayList<>(); ts.add(0.0); if (t1 > 0 && t1 < 1) ts.add(t1); if (t2 > 0 && t2 < 1) ts.add(t2); ts.add(1.0); Collections.sort(ts); for (int i = 0; i < ts.size()-1; i++) { double midT = (ts.get(i) + ts.get(i+1)) / 2; Point mid = interpolate(seg.start, seg.end, midT); if (!isInside(mid)) { result.add(new PathSegment(interpolate(seg.start, seg.end, ts.get(i)), interpolate(seg.start, seg.end, ts.get(i+1)), seg.isMowing)); } } return result; } boolean isInside(Point p) { return (p.x-c.x)*(p.x-c.x) + (p.y-c.y)*(p.y-c.y) < r*r; } } static class PolyObstacle extends Obstacle { List points; double minX, maxX, minY, maxY; PolyObstacle(List pts) { this.points = pts; updateBounds(); } void updateBounds() { minX = minY = Double.MAX_VALUE; maxX = maxY = -Double.MAX_VALUE; for (Point p : points) { minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); } } boolean isInside(Point p) { if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) return false; boolean result = false; for (int i = 0, j = points.size() - 1; i < points.size(); j = i++) { if ((points.get(i).y > p.y) != (points.get(j).y > p.y) && (p.x < (points.get(j).x - points.get(i).x) * (p.y - points.get(i).y) / (points.get(j).y - points.get(i).y) + points.get(i).x)) { result = !result; } } return result; } @Override List clip(PathSegment seg) { List ts = new ArrayList<>(); ts.add(0.0); ts.add(1.0); // 计算线段与障碍物每一条边的交点 for (int i = 0; i < points.size(); i++) { Point p1 = points.get(i); Point p2 = points.get((i+1)%points.size()); double t = getIntersectionT(seg.start, seg.end, p1, p2); if (t > 1e-6 && t < 1 - 1e-6) { ts.add(t); } } Collections.sort(ts); List result = new ArrayList<>(); // 检查每一小段的中点是否在障碍物内 for (int i = 0; i < ts.size() - 1; i++) { double tMid = (ts.get(i) + ts.get(i+1)) / 2.0; // 如果两点极其接近,跳过 if (ts.get(i+1) - ts.get(i) < 1e-6) continue; Point mid = interpolate(seg.start, seg.end, tMid); if (!isInside(mid)) { // 在外部,保留 Point s = interpolate(seg.start, seg.end, ts.get(i)); Point e = interpolate(seg.start, seg.end, ts.get(i+1)); result.add(new PathSegment(s, e, seg.isMowing)); } } return result; } } // --- 通用几何算法 --- private static List getOffsetPolygon(List points, double offset) { List result = new ArrayList<>(); int n = points.size(); for (int i = 0; i < n; i++) { Point p1 = points.get((i - 1 + n) % n); Point p2 = points.get(i); Point p3 = points.get((i + 1) % n); // 向量 p1->p2 和 p2->p3 double v1x = p2.x - p1.x, v1y = p2.y - p1.y; double v2x = p3.x - p2.x, v2y = p3.y - p2.y; double l1 = Math.hypot(v1x, v1y), l2 = Math.hypot(v2x, v2y); if (l1 < 1e-5 || l2 < 1e-5) continue; // 法向量 (向左转90度: -y, x) double n1x = -v1y / l1, n1y = v1x / l1; double n2x = -v2y / l2, n2y = v2x / l2; // 角平分线 double bx = n1x + n2x, by = n1y + n2y; double bl = Math.hypot(bx, by); if (bl < 1e-5) { bx = n1x; by = n1y; } else { bx /= bl; by /= bl; } // 修正长度 offset / sin(theta/2) = offset / dot(n1, b) double dot = n1x * bx + n1y * by; double dist = offset / Math.max(Math.abs(dot), 0.1); // 防止尖角过长 // 阈值限制,防止自交或畸变过大 dist = Math.signum(offset) * Math.min(Math.abs(dist), Math.abs(offset) * 3); result.add(new Point(p2.x + bx * dist, p2.y + by * dist)); } return result; } private static double findOptimalAngle(List poly) { double bestA = 0, minH = Double.MAX_VALUE; for (int i = 0; i < poly.size(); i++) { Point p1 = poly.get(i), p2 = poly.get((i + 1) % poly.size()); double a = Math.atan2(p2.y - p1.y, p2.x - p1.x); double h = calcHeight(poly, a); if (h < minH) { minH = h; bestA = a; } } return bestA; } private static double calcHeight(List poly, double ang) { double min = Double.MAX_VALUE, max = -Double.MAX_VALUE; for (Point p : poly) { Point r = rotatePoint(p, -ang); min = Math.min(min, r.y); max = Math.max(max, r.y); } return max - min; } private static double getIntersectionT(Point a, Point b, Point c, Point d) { double ux = b.x - a.x, uy = b.y - a.y; double vx = d.x - c.x, vy = d.y - c.y; double det = vx * uy - vy * ux; if (Math.abs(det) < 1e-8) return -1; double wx = c.x - a.x, wy = c.y - a.y; double t = (vx * wy - vy * wx) / det; double u = (ux * wy - uy * wx) / det; if (u >= 0 && u <= 1) return t; // 只保证交点在线段CD上,t是AB上的比例 return -1; } private static List getXIntersections(List poly, double y) { List res = new ArrayList<>(); for (int i = 0; i < poly.size(); i++) { Point p1 = poly.get(i), p2 = poly.get((i + 1) % poly.size()); if ((p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y)) { res.add(p1.x + (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y)); } } return res; } private static List alignBoundaryStart(List poly, Point target) { if (target == null) return poly; int idx = 0; double minD = Double.MAX_VALUE; for (int i = 0; i < poly.size(); i++) { double d = distance(poly.get(i), target); if (d < minD) { minD = d; idx = i; } } List res = new ArrayList<>(); for (int i = 0; i < poly.size(); i++) res.add(poly.get((idx + i) % poly.size())); return res; } private static void ensureCounterClockwise(List pts) { double s = 0; for (int i = 0; i < pts.size(); i++) { Point p1 = pts.get(i), p2 = pts.get((i + 1) % pts.size()); s += (p2.x - p1.x) * (p2.y + p1.y); } if (s > 0) Collections.reverse(pts); // 假设屏幕坐标系Y向下?通常多边形面积公式s>0是顺时针(Y向下)或逆时针(Y向上) // 此处沿用您代码的逻辑:如果Sum>0 则反转。 } private static Point rotatePoint(Point p, double a) { double c = Math.cos(a), s = Math.sin(a); return new Point(p.x * c - p.y * s, p.x * s + p.y * c); } private static Point interpolate(Point a, Point b, double t) { return new Point(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); } private static double distance(Point a, Point b) { return Math.hypot(a.x - b.x, a.y - b.y); } private static List parseCoordinates(String s) { List pts = new ArrayList<>(); if (s == null || s.isEmpty()) return pts; for (String p : s.split(";")) { String[] xy = p.split(","); if (xy.length >= 2) pts.add(new Point(Double.parseDouble(xy[0]), Double.parseDouble(xy[1]))); } if (pts.size() > 1 && distance(pts.get(0), pts.get(pts.size() - 1)) < 1e-4) pts.remove(pts.size() - 1); return pts; } // --- 数据结构 --- public static class Point { public double x, y; public Point(double x, double y) { this.x = x; this.y = y; } } public static class PathSegment { public Point start, end; public boolean isMowing; public PathSegment(Point s, Point e, boolean m) { this.start = s; this.end = e; this.isMowing = m; } @Override public String toString() { return String.format("%.6f,%.6f;%.6f,%.6f", start.x, start.y, end.x, end.y); } } }