package lujing; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * 异形草地路径规划 - 避障增强版 V8.0 * 修复说明: * 1. 修正了地块内缩和障碍物外扩的正负逻辑。 * 2. 优化了多边形偏移算法,确保逆时针点序下正值内缩,负值外扩。 * 3. 增强了障碍物解析的健壮性。 */ public class YixinglujingHaveObstacel { public static List planPath(String coordinates, String obstaclesStr, String widthStr, String marginStr) { List rawPoints = parseCoordinates(coordinates); if (rawPoints.size() < 3) return new ArrayList<>(); double mowWidth = Double.parseDouble(widthStr); double safeMargin = Double.parseDouble(marginStr); // 1. 预处理地块(确保逆时针顺序) ensureCounterClockwise(rawPoints); // 【核心修复】:对于逆时针多边形,正数是向内偏移(Inset) List boundary = getOffsetPolygon(rawPoints, safeMargin); if (boundary.size() < 3) return new ArrayList<>(); // 2. 确定最优角度并规划基础路径 double bestAngle = findOptimalAngle(boundary); Point firstScanStart = getFirstScanPoint(boundary, mowWidth, bestAngle); List alignedBoundary = alignBoundaryStart(boundary, firstScanStart); List baseLines = new ArrayList<>(); // 第一阶段:围边路径 for (int i = 0; i < alignedBoundary.size(); i++) { baseLines.add(new PathSegment(alignedBoundary.get(i), alignedBoundary.get((i + 1) % alignedBoundary.size()), true)); } // 第二阶段:生成内部扫描路径 Point lastEdgePos = alignedBoundary.get(0); baseLines.addAll(generateGlobalScanPath(boundary, mowWidth, bestAngle, lastEdgePos)); // 3. 处理障碍物:解析并执行【外扩】 // 【核心修复】:对于逆时针障碍物,负数是向外偏移(Outset) List obstacles = parseObstacles(obstaclesStr, safeMargin); // 4. 路径裁剪与优化连接 return optimizeAndClipPath(baseLines, obstacles); } private static List parseObstacles(String obsStr, double margin) { List obstacles = new ArrayList<>(); if (obsStr == null || obsStr.trim().isEmpty()) return obstacles; for (String group : obsStr.split("\\$")) { List pts = parseCoordinates(group); if (pts.isEmpty()) continue; if (pts.size() == 2) { // 圆形障碍物:第一个点心,第二个点上一点,半径增加 margin double r = Math.hypot(pts.get(0).x - pts.get(1).x, pts.get(0).y - pts.get(1).y); obstacles.add(new CircleObstacle(pts.get(0), r + margin)); } else if (pts.size() > 2) { // 多边形障碍物:确保逆时针,然后使用负 margin 进行【外扩】 ensureCounterClockwise(pts); obstacles.add(new PolyObstacle(getOffsetPolygon(pts, -margin))); } } return obstacles; } /** * 多边形偏移算法:基于角平分线偏移 * 在逆时针顺序下:offset > 0 为内缩,offset < 0 为外扩 */ 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); 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-6 || l2 < 1e-6) continue; // 获取两条边的法向量(向左偏移) 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-6) { bx = n1x; by = n1y; } else { bx /= bl; by /= bl; } // 计算偏移长度修正系数:1/sin(theta/2) double cosHalf = n1x * bx + n1y * by; double d = offset / Math.max(cosHalf, 0.1); // 避免分母过小导致无穷大 // 限制最大位移量,防止极尖角畸变 d = Math.signum(offset) * Math.min(Math.abs(d), Math.abs(offset) * 5); result.add(new Point(p2.x + bx * d, p2.y + by * d)); } return result; } private static List optimizeAndClipPath(List originalPath, List obstacles) { List result = new ArrayList<>(); Point currentPos = null; for (PathSegment segment : originalPath) { List clipped = new ArrayList<>(); clipped.add(segment); // 用每一个障碍物依次裁剪 for (Obstacle obs : obstacles) { List nextIter = new ArrayList<>(); for (PathSegment s : clipped) { nextIter.addAll(obs.clipSegment(s)); } clipped = nextIter; } for (PathSegment s : clipped) { // 剔除微小段 if (Math.hypot(s.start.x - s.end.x, s.start.y - s.end.y) < 1e-4) continue; // 如果新段的起点与上段的终点不连贯,添加空走(非割草)路径 if (currentPos != null && Math.hypot(currentPos.x - s.start.x, currentPos.y - s.start.y) > 0.01) { result.add(new PathSegment(currentPos, s.start, false)); } result.add(s); currentPos = s.end; } } return result; } // --- 障碍物类定义 --- abstract static class Obstacle { abstract boolean isInside(Point p); abstract List clipSegment(PathSegment seg); } static class PolyObstacle extends Obstacle { List points; double minX, maxX, minY, maxY; public PolyObstacle(List pts) { this.points = pts; minX = minY = Double.MAX_VALUE; maxX = maxY = -Double.MAX_VALUE; for (Point p : pts) { minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); } } @Override boolean isInside(Point p) { if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) return false; boolean inside = 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)) { inside = !inside; } } return inside; } @Override List clipSegment(PathSegment seg) { List ts = new ArrayList<>(Arrays.asList(0.0, 1.0)); for (int i = 0; i < points.size(); i++) { double t = getIntersectionT(seg.start, seg.end, points.get(i), points.get((i + 1) % points.size())); if (t > 0 && t < 1) ts.add(t); } Collections.sort(ts); List res = new ArrayList<>(); for (int i = 0; i < ts.size() - 1; i++) { Point s = interpolate(seg.start, seg.end, ts.get(i)); Point e = interpolate(seg.start, seg.end, ts.get(i + 1)); // 检查中点是否在障碍物内 if (!isInside(new Point((s.x + e.x) / 2, (s.y + e.y) / 2))) { res.add(new PathSegment(s, e, seg.isMowing)); } } return res; } } static class CircleObstacle extends Obstacle { Point center; double radius; public CircleObstacle(Point c, double r) { this.center = c; this.radius = r; } @Override boolean isInside(Point p) { return Math.hypot(p.x - center.x, p.y - center.y) < radius - 1e-4; } @Override List clipSegment(PathSegment seg) { List ts = new ArrayList<>(Arrays.asList(0.0, 1.0)); double dx = seg.end.x - seg.start.x, dy = seg.end.y - seg.start.y; double fx = seg.start.x - center.x, fy = seg.start.y - center.y; double a = dx * dx + dy * dy; double b = 2 * (fx * dx + fy * dy); double c = fx * fx + fy * fy - radius * radius; double disc = b * b - 4 * a * c; if (disc >= 0) { disc = Math.sqrt(disc); double t1 = (-b - disc) / (2 * a), t2 = (-b + disc) / (2 * a); if (t1 > 0 && t1 < 1) ts.add(t1); if (t2 > 0 && t2 < 1) ts.add(t2); } Collections.sort(ts); List res = new ArrayList<>(); for (int i = 0; i < ts.size() - 1; i++) { Point s = interpolate(seg.start, seg.end, ts.get(i)); Point e = interpolate(seg.start, seg.end, ts.get(i + 1)); if (!isInside(new Point((s.x + e.x) / 2, (s.y + e.y) / 2))) res.add(new PathSegment(s, e, seg.isMowing)); } return res; } } // --- 内部算法与数学支持 --- private static List generateGlobalScanPath(List polygon, double width, double angle, Point currentPos) { List segments = new ArrayList<>(); List rotated = new ArrayList<>(); for (Point p : polygon) rotated.add(rotatePoint(p, -angle)); double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE; for (Point p : rotated) { minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); } boolean l2r = true; for (double y = minY + width/2; y <= maxY - width/2; y += width) { List xInters = getXIntersections(rotated, 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 t = s.start; s.start = s.end; s.end = t; } } for (PathSegment s : row) { if (Math.hypot(currentPos.x - s.start.x, currentPos.y - s.start.y) > 0.01) { segments.add(new PathSegment(currentPos, s.start, false)); } segments.add(s); currentPos = s.end; } l2r = !l2r; } return segments; } private static double getIntersectionT(Point a, Point b, Point c, Point d) { double ux = b.x - a.x, uy = b.y - a.y, vx = d.x - c.x, vy = d.y - c.y; double det = vx * uy - vy * ux; if (Math.abs(det) < 1e-6) return -1; return (vx * (c.y - a.y) - vy * (c.x - a.x)) / det; } 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 Point rotatePoint(Point p, double ang) { double cos = Math.cos(ang), sin = Math.sin(ang); return new Point(p.x * cos - p.y * sin, p.x * sin + p.y * cos); } 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 Point getFirstScanPoint(List poly, double w, double a) { List rot = new ArrayList<>(); for (Point p : poly) rot.add(rotatePoint(p, -a)); double minY = Double.MAX_VALUE; for (Point p : rot) minY = Math.min(minY, p.y); List xs = getXIntersections(rot, minY + w/2); if (xs.isEmpty()) return poly.get(0); Collections.sort(xs); return rotatePoint(new Point(xs.get(0), minY + w/2), a); } private static List alignBoundaryStart(List poly, Point target) { int idx = 0; double minD = Double.MAX_VALUE; for (int i = 0; i < poly.size(); i++) { double d = Math.hypot(poly.get(i).x - target.x, poly.get(i).y - target.y); 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 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 miY = Double.MAX_VALUE, maY = -Double.MAX_VALUE; for (Point p : poly) { Point r = rotatePoint(p, -a); miY = Math.min(miY, r.y); maY = Math.max(maY, r.y); } if (maY - miY < minH) { minH = maY - miY; bestA = a; } } return bestA; } 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); } 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 && pts.get(0).equals(pts.get(pts.size() - 1))) 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; } @Override public boolean equals(Object o) { if (!(o instanceof Point)) return false; Point p = (Point) o; return Math.abs(x - p.x) < 1e-4 && Math.abs(y - p.y) < 1e-4; } } 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; } } }