package lujing; import java.util.*; import java.util.regex.*; /** * 异形草地路径规划 - 凹多边形修复版 V12.0 * 修改说明: * 1. 按照用户要求,先生成无障碍物的完整路径(围边+扫描+连接)。 * 2. 对完整路径进行障碍物裁剪。 * 3. 对裁剪产生的断点,尝试沿障碍物边界进行连接。 */ public class YixinglujingHaveObstacel { private static final double EPS = 1e-8; private static final double MIN_SEG_LEN = 0.02; // 忽略小于2cm的碎线 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. 统一多边形方向为逆时针 (CCW) ensureCCW(rawPoints); // 2. 生成作业内缩边界 List mowingBoundary = getOffsetPolygon(rawPoints, safeMargin); if (mowingBoundary.size() < 3) return new ArrayList<>(); // 3. 解析并外扩障碍物 List obstacles = parseObstacles(obstaclesStr, safeMargin); // 4. 生成全覆盖路径(不考虑障碍物) List fullPath = generateFullPath(mowingBoundary, mowWidth); // 5. 裁剪并连接 return processObstacles(fullPath, obstacles); } /** * 生成全覆盖路径(围边 + 扫描 + 连接),不考虑障碍物 */ private static List generateFullPath(List boundary, double width) { List path = new ArrayList<>(); // A. 围边路径(首圈) for (int i = 0; i < boundary.size(); i++) { path.add(new PathSegment(boundary.get(i), boundary.get((i + 1) % boundary.size()), true)); } // B. 扫描路径生成 double angle = findOptimalAngle(boundary); List rotPoly = rotatePoints(boundary, -angle); double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE; for (Point p : rotPoly) { minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); } boolean l2r = true; List scanSegments = new ArrayList<>(); for (double y = minY + width / 2; y <= maxY - width / 2; y += width) { List xInters = getXIntersections(rotPoly, 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; } } scanSegments.addAll(row); l2r = !l2r; } // C. 连接扫描线 if (!scanSegments.isEmpty()) { Point currentPos = path.isEmpty() ? scanSegments.get(0).start : path.get(path.size() - 1).end; for (PathSegment seg : scanSegments) { if (distance(currentPos, seg.start) > MIN_SEG_LEN) { path.add(new PathSegment(currentPos, seg.start, false)); } path.add(seg); currentPos = seg.end; } } return path; } /** * 处理障碍物:裁剪路径并生成绕行连接 */ private static List processObstacles(List fullPath, List obstacles) { List result = new ArrayList<>(); if (fullPath.isEmpty()) return result; Point currentPos = fullPath.get(0).start; for (PathSegment seg : fullPath) { // 裁剪单条线段 List pieces = clipSegment(seg, obstacles); for (PathSegment piece : pieces) { // 如果有断点,尝试连接 if (distance(currentPos, piece.start) > MIN_SEG_LEN) { List detour = findDetour(currentPos, piece.start, obstacles); result.addAll(detour); } result.add(piece); currentPos = piece.end; } } return result; } private static List findDetour(Point p1, Point p2, List obstacles) { // 检查断点是否在同一个障碍物上 for (Obstacle obs : obstacles) { if (obs.isOnBoundary(p1) && obs.isOnBoundary(p2)) { return obs.getBoundaryPath(p1, p2); } } // 如果不在同一个障碍物上(理论上较少见,除非跨越了多个障碍物),直接连接 List res = new ArrayList<>(); res.add(new PathSegment(p1, p2, false)); return res; } private static List clipSegment(PathSegment seg, List obstacles) { List result = new ArrayList<>(); result.add(seg); for (Obstacle obs : obstacles) { List next = new ArrayList<>(); for (PathSegment s : result) { next.addAll(obs.clip(s)); } result = next; } return result; } // --- 几何修正算法 --- /** * 修正后的方向判定:鞋带公式 Sum (x2-x1)(y2+y1) * 在标准笛卡尔坐标系中,Sum < 0 为逆时针 */ private static void ensureCCW(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 getOffsetPolygon(List pts, double offset) { List result = new ArrayList<>(); int n = pts.size(); for (int i = 0; i < n; i++) { Point p1 = pts.get((i - 1 + n) % n), p2 = pts.get(i), p3 = pts.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 < EPS || l2 < EPS) 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 < EPS) { bx = n1x; by = n1y; } else { bx /= bl; by /= bl; } double dist = offset / Math.max(Math.abs(n1x * bx + n1y * by), 0.1); result.add(new Point(p2.x + bx * dist, p2.y + by * dist)); } return result; } 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; } // --- 障碍物模型 --- abstract static class Obstacle { abstract List clip(PathSegment seg); abstract boolean isInside(Point p); abstract boolean isOnBoundary(Point p); abstract List getBoundaryPath(Point p1, Point p2); } static class PolyObstacle extends Obstacle { List pts; PolyObstacle(List p) { this.pts = p; } @Override boolean isInside(Point p) { boolean in = false; for (int i = 0, j = pts.size() - 1; i < pts.size(); j = i++) { if (((pts.get(i).y > p.y) != (pts.get(j).y > p.y)) && (p.x < (pts.get(j).x - pts.get(i).x) * (p.y - pts.get(i).y) / (pts.get(j).y - pts.get(i).y) + pts.get(i).x)) { in = !in; } } return in; } @Override List clip(PathSegment seg) { List ts = new ArrayList<>(Arrays.asList(0.0, 1.0)); for (int i = 0; i < pts.size(); i++) { double t = getIntersectT(seg.start, seg.end, pts.get(i), pts.get((i + 1) % pts.size())); if (t > EPS && t < 1.0 - EPS) ts.add(t); } Collections.sort(ts); List res = new ArrayList<>(); for (int i = 0; i < ts.size() - 1; i++) { double tMid = (ts.get(i) + ts.get(i + 1)) / 2.0; if (!isInside(interpolate(seg.start, seg.end, tMid))) { res.add(new PathSegment(interpolate(seg.start, seg.end, ts.get(i)), interpolate(seg.start, seg.end, ts.get(i+1)), seg.isMowing)); } } return res; } @Override boolean isOnBoundary(Point p) { for (int i = 0; i < pts.size(); i++) { if (distToSegment(p, pts.get(i), pts.get((i + 1) % pts.size())) < 1e-4) return true; } return false; } @Override List getBoundaryPath(Point p1, Point p2) { // 寻找最近的顶点索引 int idx1 = -1, idx2 = -1; double minD1 = Double.MAX_VALUE, minD2 = Double.MAX_VALUE; for (int i = 0; i < pts.size(); i++) { double d1 = distToSegment(p1, pts.get(i), pts.get((i + 1) % pts.size())); if (d1 < minD1) { minD1 = d1; idx1 = i; } double d2 = distToSegment(p2, pts.get(i), pts.get((i + 1) % pts.size())); if (d2 < minD2) { minD2 = d2; idx2 = i; } } List pathPoints = new ArrayList<>(); pathPoints.add(p1); // 简单策略:沿多边形顶点移动。由于是障碍物,我们选择较短路径 // 顺时针和逆时针比较 List ccw = new ArrayList<>(); int curr = idx1; while (curr != idx2) { curr = (curr + 1) % pts.size(); ccw.add(pts.get(curr)); } List cw = new ArrayList<>(); curr = (idx1 + 1) % pts.size(); // idx1 is the start of edge containing p1 // Wait, idx1 is index of point? No, index of edge start. // Edge i is pts[i] -> pts[i+1] // If p1 is on edge idx1, p2 is on edge idx2. // Let's simplify: collect all vertices in order // Path 1: p1 -> pts[idx1+1] -> ... -> pts[idx2] -> p2 // Path 2: p1 -> pts[idx1] -> ... -> pts[idx2+1] -> p2 // Calculate lengths and choose shortest List res = new ArrayList<>(); // For now, just return straight line to avoid complexity bugs in blind coding // But user wants to avoid obstacle. // Let's implement a simple vertex traversal // CCW path (pts order) List path1 = new ArrayList<>(); path1.add(p1); int i = idx1; while (i != idx2) { i = (i + 1) % pts.size(); path1.add(pts.get(i)); } path1.add(pts.get((idx2 + 1) % pts.size())); // End of edge idx2? No. // If p2 is on edge idx2 (pts[idx2]->pts[idx2+1]) // We arrive at pts[idx2], then go to p2? No. // If we go CCW: p1 -> pts[idx1+1] -> pts[idx1+2] ... -> pts[idx2] -> p2 // Let's rebuild path1 correctly List p1List = new ArrayList<>(); p1List.add(p1); int k = idx1; while (k != idx2) { k = (k + 1) % pts.size(); p1List.add(pts.get(k)); } p1List.add(p2); // Finally to p2 (which is on edge idx2) // CW path List p2List = new ArrayList<>(); p2List.add(p1); k = idx1; // Start at edge idx1 // Go backwards: p1 -> pts[idx1] -> pts[idx1-1] ... -> pts[idx2+1] -> p2 p2List.add(pts.get(k)); k = (k - 1 + pts.size()) % pts.size(); while (k != idx2) { p2List.add(pts.get(k)); k = (k - 1 + pts.size()) % pts.size(); } p2List.add(pts.get((idx2 + 1) % pts.size())); p2List.add(p2); double len1 = getPathLen(p1List); double len2 = getPathLen(p2List); List best = (len1 < len2) ? p1List : p2List; for (int j = 0; j < best.size() - 1; j++) { res.add(new PathSegment(best.get(j), best.get(j+1), false)); } return res; } private double getPathLen(List ps) { double l = 0; for(int i=0;i clip(PathSegment seg) { double dx = seg.end.x - seg.start.x, dy = seg.end.y - seg.start.y; double fx = seg.start.x - c.x, fy = seg.start.y - c.y; double A = dx*dx + dy*dy, B = 2*(fx*dx + fy*dy), C = fx*fx + fy*fy - r*r; double delta = B*B - 4*A*C; List ts = new ArrayList<>(Arrays.asList(0.0, 1.0)); if (delta > 0) { double t1 = (-B-Math.sqrt(delta))/(2*A), t2 = (-B+Math.sqrt(delta))/(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++) { if (!isInside(interpolate(seg.start, seg.end, (ts.get(i)+ts.get(i+1))/2.0))) res.add(new PathSegment(interpolate(seg.start, seg.end, ts.get(i)), interpolate(seg.start, seg.end, ts.get(i+1)), seg.isMowing)); } return res; } @Override boolean isOnBoundary(Point p) { return Math.abs(distance(p, c) - r) < 1e-4; } @Override List getBoundaryPath(Point p1, Point p2) { List res = new ArrayList<>(); double a1 = Math.atan2(p1.y - c.y, p1.x - c.x); double a2 = Math.atan2(p2.y - c.y, p2.x - c.x); double da = a2 - a1; while (da <= -Math.PI) da += 2*Math.PI; while (da > Math.PI) da -= 2*Math.PI; // Choose shorter arc // If da is positive, CCW is shorter? No, da is signed diff. // We just interpolate angles. int steps = 10; Point prev = p1; for (int i = 1; i <= steps; i++) { double a = a1 + da * i / steps; Point next = new Point(c.x + r * Math.cos(a), c.y + r * Math.sin(a)); res.add(new PathSegment(prev, next, false)); prev = next; } return res; } } // --- 通用工具 --- private static double getIntersectT(Point a, Point b, Point c, Point d) { double det = (b.x - a.x) * (d.y - c.y) - (b.y - a.y) * (d.x - c.x); if (Math.abs(det) < 1e-10) return -1; double t = ((c.x - a.x) * (d.y - c.y) - (c.y - a.y) * (d.x - c.x)) / det; double u = ((c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x)) / det; return (t >= 0 && t <= 1 && u >= 0 && u <= 1) ? t : -1; } private static double distToSegment(Point p, Point a, Point b) { double l2 = (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y); if (l2 == 0) return distance(p, a); double t = ((p.x-a.x)*(b.x-a.x) + (p.y-a.y)*(b.y-a.y)) / l2; t = Math.max(0, Math.min(1, t)); return distance(p, new Point(a.x + t*(b.x-a.x), a.y + t*(b.y-a.y))); } private static List parseObstacles(String obsStr, double margin) { List list = new ArrayList<>(); if (obsStr == null || obsStr.isEmpty()) return list; Matcher m = Pattern.compile("\\(([^)]+)\\)").matcher(obsStr); while (m.find()) { List pts = parseCoordinates(m.group(1)); if (pts.size() == 2) list.add(new CircleObstacle(pts.get(0), distance(pts.get(0), pts.get(1)) + margin)); else if (pts.size() >= 3) { ensureCCW(pts); list.add(new PolyObstacle(getOffsetPolygon(pts, -margin))); // 负值外扩 } } return list; } 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 maxV = -Double.MAX_VALUE, minV = Double.MAX_VALUE; for (Point p : poly) { double v = p.y * Math.cos(a) - p.x * Math.sin(a); maxV = Math.max(maxV, v); minV = Math.min(minV, v); } if (maxV - minV < minH) { minH = maxV - minV; bestA = a; } } return bestA; } private static List parseCoordinates(String s) { List list = new ArrayList<>(); for (String p : s.split(";")) { String[] xy = p.trim().split(","); if (xy.length == 2) list.add(new Point(Double.parseDouble(xy[0]), Double.parseDouble(xy[1]))); } return list; } private static double distance(Point a, Point b) { return Math.hypot(a.x - b.x, a.y - b.y); } 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 a) { return new Point(p.x*Math.cos(a)-p.y*Math.sin(a), p.x*Math.sin(a)+p.y*Math.cos(a)); } private static List rotatePoints(List pts, double a) { List res = new ArrayList<>(); for (Point p : pts) res.add(rotatePoint(p, a)); return res; } 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; } } }