package lujing; import java.util.*; /** * 异形草地路径规划 - 围边+全局扫描版 V4.1 * 优化:围边终点与弓字形起点自动对齐,实现无缝切换,确保路径不越界 */ public class YixinglujingNoObstacle { public static List planPath(String coordinates, 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); // 2. 生成内缩多边形 List boundary = getInsetPolygon(rawPoints, safeMargin); if (boundary.size() < 3) return new ArrayList<>(); // 3. 确定最优扫描角度并找到弓字形路径的第一个作业起点 double bestAngle = findOptimalAngle(boundary); Point firstScanStart = getFirstScanPoint(boundary, mowWidth, bestAngle); // 4. 对齐围边起点:重新排列围边坐标,使最后一个点靠近(或等于)扫描起点 List alignedBoundary = alignBoundaryStart(boundary, firstScanStart); List finalPath = new ArrayList<>(); // 5. 【第一步】生成围边路径 for (int i = 0; i < alignedBoundary.size(); i++) { Point pStart = alignedBoundary.get(i); Point pEnd = alignedBoundary.get((i + 1) % alignedBoundary.size()); finalPath.add(new PathSegment(pStart, pEnd, true)); } // 6. 【第二步】从对齐后的终点开始生成内部扫描路径 Point lastEdgePos = alignedBoundary.get(0); // 围边闭合回到起点 List scanPath = generateGlobalScanPath(boundary, mowWidth, bestAngle, lastEdgePos); finalPath.addAll(scanPath); return finalPath; } /** * 计算并获取扫描路径的第一行起点 */ private static Point getFirstScanPoint(List polygon, double width, double angle) { List rotatedPoly = new ArrayList<>(); for (Point p : polygon) rotatedPoly.add(rotatePoint(p, -angle)); double minY = Double.MAX_VALUE; for (Point p : rotatedPoly) minY = Math.min(minY, p.y); double firstY = minY + width; List xIntersections = getXIntersections(rotatedPoly, firstY); if (xIntersections.isEmpty()) return polygon.get(0); return rotatePoint(new Point(Collections.min(xIntersections), firstY), angle); } /** * 重新排列多边形顶点,使起始点与扫描起点对接 */ private static List alignBoundaryStart(List boundary, Point targetStart) { int bestIdx = 0; double minDist = Double.MAX_VALUE; for (int i = 0; i < boundary.size(); i++) { double d = Math.hypot(boundary.get(i).x - targetStart.x, boundary.get(i).y - targetStart.y); if (d < minDist) { minDist = d; bestIdx = i; } } List aligned = new ArrayList<>(); for (int i = 0; i < boundary.size(); i++) { aligned.add(boundary.get((bestIdx + i) % boundary.size())); } return aligned; } private static List generateGlobalScanPath(List polygon, double width, double angle, Point currentPos) { List segments = new ArrayList<>(); List rotatedPoly = new ArrayList<>(); for (Point p : polygon) 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); } boolean leftToRight = true; // 从 minY + width 开始,避开围边已割区域 for (double y = minY + width; y <= maxY - width/2; y += width) { List xIntersections = getXIntersections(rotatedPoly, y); if (xIntersections.size() < 2) continue; Collections.sort(xIntersections); List lineRows = new ArrayList<>(); for (int i = 0; i < xIntersections.size() - 1; i += 2) { Point pS = rotatePoint(new Point(xIntersections.get(i), y), angle); Point pE = rotatePoint(new Point(xIntersections.get(i + 1), y), angle); lineRows.add(new PathSegment(pS, pE, true)); } if (!leftToRight) { Collections.reverse(lineRows); for (PathSegment s : lineRows) { Point t = s.start; s.start = s.end; s.end = t; } } for (PathSegment s : lineRows) { // 如果间距极小,视为无缝衔接 if (Math.hypot(currentPos.x - s.start.x, currentPos.y - s.start.y) > 0.05) { segments.add(new PathSegment(currentPos, s.start, false)); } segments.add(s); currentPos = s.end; } leftToRight = !leftToRight; } return segments; } private static List getXIntersections(List rotatedPoly, double y) { List xIntersections = new ArrayList<>(); for (int i = 0; i < rotatedPoly.size(); i++) { Point p1 = rotatedPoly.get(i); Point p2 = rotatedPoly.get((i + 1) % rotatedPoly.size()); if ((p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y)) { double x = p1.x + (y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y); xIntersections.add(x); } } return xIntersections; } private static double findOptimalAngle(List polygon) { double bestAngle = 0; double minHeight = Double.MAX_VALUE; for (int i = 0; i < polygon.size(); i++) { Point p1 = polygon.get(i), p2 = polygon.get((i + 1) % polygon.size()); double angle = Math.atan2(p2.y - p1.y, p2.x - p1.x); double h = calculateHeightAtAngle(polygon, angle); if (h < minHeight) { minHeight = h; bestAngle = angle; } } return bestAngle; } private static double calculateHeightAtAngle(List poly, double angle) { double minY = Double.MAX_VALUE, maxY = -Double.MAX_VALUE; for (Point p : poly) { Point rp = rotatePoint(p, -angle); minY = Math.min(minY, rp.y); maxY = Math.max(maxY, rp.y); } return maxY - minY; } private static List getInsetPolygon(List points, double margin) { List result = new ArrayList<>(); int n = points.size(); for (int i = 0; i < n; i++) { Point pPrev = points.get((i - 1 + n) % n); Point pCurr = points.get(i); Point pNext = points.get((i + 1) % n); double d1x = pCurr.x - pPrev.x, d1y = pCurr.y - pPrev.y; double l1 = Math.hypot(d1x, d1y); double d2x = pNext.x - pCurr.x, d2y = pNext.y - pCurr.y; double l2 = Math.hypot(d2x, d2y); if (l1 < 1e-6 || l2 < 1e-6) continue; double n1x = -d1y / l1, n1y = d1x / l1; double n2x = -d2y / l2, n2y = d2x / l2; double bisectorX = n1x + n2x, bisectorY = n1y + n2y; double bLen = Math.hypot(bisectorX, bisectorY); if (bLen < 1e-6) { bisectorX = n1x; bisectorY = n1y; } else { bisectorX /= bLen; bisectorY /= bLen; } double cosHalfAngle = n1x * bisectorX + n1y * bisectorY; double dist = margin / Math.max(cosHalfAngle, 0.1); result.add(new Point(pCurr.x + bisectorX * dist, pCurr.y + bisectorY * dist)); } return result; } private static Point rotatePoint(Point p, double angle) { double cos = Math.cos(angle), sin = Math.sin(angle); return new Point(p.x * cos - p.y * sin, p.x * sin + p.y * cos); } private static void ensureCounterClockwise(List points) { double sum = 0; for (int i = 0; i < points.size(); i++) { Point p1 = points.get(i), p2 = points.get((i + 1) % points.size()); sum += (p2.x - p1.x) * (p2.y + p1.y); } if (sum > 0) Collections.reverse(points); } private static List parseCoordinates(String coordinates) { List points = new ArrayList<>(); String[] pairs = coordinates.split(";"); for (String pair : pairs) { String[] xy = pair.split(","); if (xy.length == 2) points.add(new Point(Double.parseDouble(xy[0]), Double.parseDouble(xy[1]))); } if (points.size() > 1 && points.get(0).equals(points.get(points.size()-1))) points.remove(points.size()-1); return points; } 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; } } }