package lujing; import java.util.*; /** * 异形草地路径规划 - 含障碍物版 * 功能:在地块内部避开障碍物,生成连续弓字形割草路径 */ 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); // 解析障碍物 List obstacles = parseObstacles(obstaclesStr); // 2. 预处理:确保边界逆时针 ensureCounterClockwise(rawPoints); // 3. 生成内缩多边形(安全边界) List boundary = getInsetPolygon(rawPoints, safeMargin); if (boundary.size() < 3) return new ArrayList<>(); // 4. 外扩障碍物(安全边距) List expandedObstacles = expandObstacles(obstacles, safeMargin); // 5. 确定最优作业角度 double bestAngle = findOptimalAngle(boundary); // 6. 获取首个作业点,用于对齐围边起点 Point firstScanStart = getFirstScanPoint(boundary, mowWidth, bestAngle); // 7. 对齐围边 List alignedBoundary = alignBoundaryStart(boundary, firstScanStart); // 8. 第一阶段:围边路径 List finalPath = new ArrayList<>(); 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)); } // 9. 第二阶段:生成内部扫描路径(考虑障碍物) Point lastEdgePos = alignedBoundary.get(0); List scanPath = generateGlobalScanPathWithObstacles( boundary, expandedObstacles, mowWidth, bestAngle, lastEdgePos); finalPath.addAll(scanPath); // 10. 格式化坐标:保留两位小数 for (PathSegment segment : finalPath) { segment.start.x = Math.round(segment.start.x * 100.0) / 100.0; segment.start.y = Math.round(segment.start.y * 100.0) / 100.0; segment.end.x = Math.round(segment.end.x * 100.0) / 100.0; segment.end.y = Math.round(segment.end.y * 100.0) / 100.0; } // 11. 打印输出路径坐标 printPathCoordinates(finalPath); return finalPath; } /** * 生成带障碍物的扫描路径 */ private static List generateGlobalScanPathWithObstacles( List polygon, List obstacles, double width, double angle, Point startPos) { // 1. 生成原始扫描线(无障碍物) List originalSegments = generateGlobalScanPath(polygon, width, angle, startPos); // 2. 移除在障碍物内部的线段 List remainingSegments = new ArrayList<>(); for (PathSegment seg : originalSegments) { if (!seg.isMowing) { // 空走段直接保留 remainingSegments.add(seg); continue; } // 将割草段与所有障碍物进行裁剪 List clippedSegments = new ArrayList<>(); clippedSegments.add(seg); for (Obstacle obs : obstacles) { List newSegments = new ArrayList<>(); for (PathSegment s : clippedSegments) { newSegments.addAll(clipSegmentWithObstacle(s, obs)); } clippedSegments = newSegments; } remainingSegments.addAll(clippedSegments); } // 3. 重新连接路径段(弓字形连接) return reconnectSegments(remainingSegments); } /** * 将线段与障碍物进行裁剪 * 返回不在障碍物内部的子线段 */ private static List clipSegmentWithObstacle(PathSegment segment, Obstacle obstacle) { List result = new ArrayList<>(); // 检查线段是否完全在障碍物外部 boolean startInside = obstacle.contains(segment.start); boolean endInside = obstacle.contains(segment.end); if (!startInside && !endInside) { // 线段两端都在外部,检查是否穿过障碍物 List intersections = obstacle.getIntersections(segment); if (intersections.isEmpty()) { // 完全在外部 result.add(segment); } else { // 穿过障碍物,分割线段 intersections.sort(Comparator.comparingDouble(p -> distance(segment.start, p))); Point prevPoint = segment.start; for (Point inter : intersections) { result.add(new PathSegment(prevPoint, inter, true)); prevPoint = inter; } result.add(new PathSegment(prevPoint, segment.end, true)); // 移除在障碍物内部的段(奇数索引的段) List filtered = new ArrayList<>(); for (int i = 0; i < result.size(); i++) { PathSegment s = result.get(i); Point midPoint = new Point( (s.start.x + s.end.x) / 2, (s.start.y + s.end.y) / 2 ); if (!obstacle.contains(midPoint)) { filtered.add(s); } } return filtered; } } else if (startInside && endInside) { // 完全在内部,丢弃 return result; } else { // 一端在内部,一端在外部 Point insidePoint = startInside ? segment.start : segment.end; Point outsidePoint = startInside ? segment.end : segment.start; List intersections = obstacle.getIntersections(segment); if (!intersections.isEmpty()) { // 取离外部点最近的交点 intersections.sort(Comparator.comparingDouble(p -> distance(outsidePoint, p))); Point inter = intersections.get(0); // 只保留外部部分 if (startInside) { result.add(new PathSegment(inter, outsidePoint, true)); } else { result.add(new PathSegment(outsidePoint, inter, true)); } } } return result; } /** * 重新连接路径段,形成连续弓字形路径 */ private static List reconnectSegments(List segments) { if (segments.isEmpty()) return new ArrayList<>(); List reconnected = new ArrayList<>(); Point currentPos = segments.get(0).start; for (PathSegment seg : segments) { if (seg.isMowing) { // 割草段:检查是否需要添加空走段 if (distance(currentPos, seg.start) > 0.01) { reconnected.add(new PathSegment(currentPos, seg.start, false)); } reconnected.add(seg); currentPos = seg.end; } else { // 空走段直接添加 reconnected.add(seg); currentPos = seg.end; } } return reconnected; } /** * 生成原始扫描路径(无障碍物版本) */ 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; for (double y = minY + width/2; y <= maxY - width/2; y += width) { List xIntersections = getXIntersections(rotatedPoly, y); if (xIntersections.size() < 2) continue; Collections.sort(xIntersections); List lineSegmentsInRow = 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); lineSegmentsInRow.add(new PathSegment(pS, pE, true)); } if (!leftToRight) { Collections.reverse(lineSegmentsInRow); for (PathSegment s : lineSegmentsInRow) { Point temp = s.start; s.start = s.end; s.end = temp; } } for (PathSegment s : lineSegmentsInRow) { if (distance(currentPos, s.start) > 0.01) { segments.add(new PathSegment(currentPos, s.start, false)); } segments.add(s); currentPos = s.end; } leftToRight = !leftToRight; } return segments; } /** * 解析障碍物字符串 * 格式:"(x1,y1;x2,y2)(x1,y1;x2,y2;x3,y3)" */ private static List parseObstacles(String obstaclesStr) { List obstacles = new ArrayList<>(); if (obstaclesStr == null || obstaclesStr.trim().isEmpty()) { return obstacles; } String trimmed = obstaclesStr.trim(); List obstacleStrs = new ArrayList<>(); // 分割每个障碍物(用括号分隔) int start = trimmed.indexOf('('); while (start != -1) { int end = trimmed.indexOf(')', start); if (end == -1) break; String obsStr = trimmed.substring(start + 1, end); obstacleStrs.add(obsStr); start = trimmed.indexOf('(', end); } // 解析每个障碍物 for (String obsStr : obstacleStrs) { List points = new ArrayList<>(); String[] pairs = obsStr.split(";"); for (String pair : pairs) { String[] xy = pair.split(","); if (xy.length == 2) { points.add(new Point( Double.parseDouble(xy[0].trim()), Double.parseDouble(xy[1].trim()) )); } } if (points.size() == 2) { // 圆形障碍物:第一个点为圆心,第二个点为圆上一点 Point center = points.get(0); Point onCircle = points.get(1); double radius = distance(center, onCircle); obstacles.add(new Obstacle(center, radius)); } else if (points.size() > 2) { // 多边形障碍物 obstacles.add(new Obstacle(points)); } } return obstacles; } /** * 外扩障碍物(增加安全边距) */ private static List expandObstacles(List obstacles, double margin) { List expanded = new ArrayList<>(); for (Obstacle obs : obstacles) { if (obs.isCircle()) { // 圆形:半径增加安全边距 expanded.add(new Obstacle(obs.center, obs.radius + margin)); } else { // 多边形:向外偏移(与边界内缩方向相反) List expandedPoints = getOutsetPolygon(obs.points, margin); expanded.add(new Obstacle(expandedPoints)); } } return expanded; } /** * 多边形外扩(与内缩方向相反) */ private static List getOutsetPolygon(List points, double margin) { // 这里使用简化的外扩方法:沿法线向外移动 List outset = 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 v1x = pCurr.x - pPrev.x, v1y = pCurr.y - pPrev.y; double v2x = pNext.x - pCurr.x, v2y = pNext.y - pCurr.y; // 计算法线(确保向外) double nx1 = -v1y, ny1 = v1x; double nx2 = -v2y, ny2 = v2x; // 归一化 double len1 = Math.hypot(nx1, ny1); double len2 = Math.hypot(nx2, ny2); if (len1 > 1e-6) { nx1 /= len1; ny1 /= len1; } if (len2 > 1e-6) { nx2 /= len2; ny2 /= len2; } // 计算平均法线方向 double nx = (nx1 + nx2) / 2; double ny = (ny1 + ny2) / 2; double len = Math.hypot(nx, ny); if (len > 1e-6) { nx /= len; ny /= len; } // 向外移动 outset.add(new Point( pCurr.x + nx * margin, pCurr.y + ny * margin )); } return outset; } /** * 障碍物类 */ private static class Obstacle { List points; // 多边形顶点(对圆形为空) Point center; // 圆心(仅对圆形有效) double radius; // 半径(仅对圆形有效) boolean isCircle; // 多边形构造函数 Obstacle(List points) { this.points = new ArrayList<>(points); this.isCircle = false; ensureCounterClockwise(this.points); // 确保顺时针(对障碍物是内部区域) } // 圆形构造函数 Obstacle(Point center, double radius) { this.center = new Point(center.x, center.y); this.radius = radius; this.isCircle = true; this.points = new ArrayList<>(); } // 判断点是否在障碍物内部 boolean contains(Point p) { if (isCircle) { return distance(p, center) <= radius; } else { return isPointInPolygon(p, points); } } // 获取线段与障碍物的交点 List getIntersections(PathSegment segment) { List intersections = new ArrayList<>(); if (isCircle) { // 线段与圆的交点 double dx = segment.end.x - segment.start.x; double dy = segment.end.y - segment.start.y; double a = dx * dx + dy * dy; double b = 2 * (dx * (segment.start.x - center.x) + dy * (segment.start.y - center.y)); double c = (segment.start.x - center.x) * (segment.start.x - center.x) + (segment.start.y - center.y) * (segment.start.y - center.y) - radius * radius; double discriminant = b * b - 4 * a * c; if (discriminant >= 0) { discriminant = Math.sqrt(discriminant); for (int sign = -1; sign <= 1; sign += 2) { double t = (-b + sign * discriminant) / (2 * a); if (t >= 0 && t <= 1) { intersections.add(new Point( segment.start.x + t * dx, segment.start.y + t * dy )); } } } } else { // 线段与多边形的交点 for (int i = 0; i < points.size(); i++) { Point p1 = points.get(i); Point p2 = points.get((i + 1) % points.size()); Point inter = getLineIntersection( segment.start, segment.end, p1, p2); if (inter != null) { intersections.add(inter); } } } return intersections; } boolean isCircle() { return isCircle; } } /** * 判断点是否在多边形内部(射线法) */ private static boolean isPointInPolygon(Point p, List polygon) { boolean inside = false; for (int i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { Point pi = polygon.get(i); Point pj = polygon.get(j); if (((pi.y > p.y) != (pj.y > p.y)) && (p.x < (pj.x - pi.x) * (p.y - pi.y) / (pj.y - pi.y) + pi.x)) { inside = !inside; } } return inside; } /** * 计算两条线段的交点 */ private static Point getLineIntersection(Point p1, Point p2, Point p3, Point p4) { double denom = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x); if (Math.abs(denom) < 1e-6) return null; // 平行 double t = ((p1.x - p3.x) * (p3.y - p4.y) - (p1.y - p3.y) * (p3.x - p4.x)) / denom; double u = -((p1.x - p2.x) * (p1.y - p3.y) - (p1.y - p2.y) * (p1.x - p3.x)) / denom; if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { return new Point( p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y) ); } return null; } /** * 计算两点距离 */ private static double distance(Point p1, Point p2) { return Math.hypot(p1.x - p2.x, p1.y - p2.y); } // ============ 以下是从A代码复用的方法 ============ 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/2; List xInter = getXIntersections(rotatedPoly, firstY); if (xInter.isEmpty()) return polygon.get(0); Collections.sort(xInter); return rotatePoint(new Point(xInter.get(0), 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 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); dist = Math.min(dist, margin * 5); 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; } /** * 打印输出路径坐标到控制台 */ private static void printPathCoordinates(List path) { if (path == null || path.isEmpty()) { System.out.println("路径为空"); return; } System.out.println("========== 路径坐标输出 =========="); System.out.println("总路径段数: " + path.size()); System.out.println(); System.out.println("路径坐标序列 (格式: x,y;x,y;...):"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < path.size(); i++) { PathSegment segment = path.get(i); if (i == 0) { // 第一个段的起点 sb.append(String.format("%.2f,%.2f", segment.start.x, segment.start.y)); } // 每个段的终点 sb.append(";"); sb.append(String.format("%.2f,%.2f", segment.end.x, segment.end.y)); } System.out.println(sb.toString()); System.out.println(); System.out.println("详细路径信息:"); for (int i = 0; i < path.size(); i++) { PathSegment segment = path.get(i); String type = segment.isMowing ? "割草" : "空走"; System.out.println(String.format("段 %d [%s]: (%.2f,%.2f) -> (%.2f,%.2f)", i + 1, type, segment.start.x, segment.start.y, segment.end.x, segment.end.y)); } System.out.println("=================================="); } 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; } } }