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<PathSegment> planPath(String coordinates, String obstaclesStr, String widthStr, String marginStr) {
|
List<Point> 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<Point> mowingBoundary = getOffsetPolygon(rawPoints, safeMargin);
|
if (mowingBoundary.size() < 3) return new ArrayList<>();
|
|
// 3. 解析并外扩障碍物
|
List<Obstacle> obstacles = parseObstacles(obstaclesStr, safeMargin);
|
|
// 4. 生成全覆盖路径(不考虑障碍物)
|
List<PathSegment> fullPath = generateFullPath(mowingBoundary, mowWidth);
|
|
// 5. 裁剪并连接
|
return processObstacles(fullPath, obstacles);
|
}
|
|
/**
|
* 生成全覆盖路径(围边 + 扫描 + 连接),不考虑障碍物
|
*/
|
private static List<PathSegment> generateFullPath(List<Point> boundary, double width) {
|
List<PathSegment> 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<Point> 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<PathSegment> scanSegments = new ArrayList<>();
|
for (double y = minY + width / 2; y <= maxY - width / 2; y += width) {
|
List<Double> xInters = getXIntersections(rotPoly, y);
|
if (xInters.size() < 2) continue;
|
Collections.sort(xInters);
|
|
List<PathSegment> 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<PathSegment> processObstacles(List<PathSegment> fullPath, List<Obstacle> obstacles) {
|
List<PathSegment> result = new ArrayList<>();
|
if (fullPath.isEmpty()) return result;
|
|
Point currentPos = fullPath.get(0).start;
|
|
for (PathSegment seg : fullPath) {
|
// 裁剪单条线段
|
List<PathSegment> pieces = clipSegment(seg, obstacles);
|
|
for (PathSegment piece : pieces) {
|
// 如果有断点,尝试连接
|
if (distance(currentPos, piece.start) > MIN_SEG_LEN) {
|
List<PathSegment> detour = findDetour(currentPos, piece.start, obstacles);
|
result.addAll(detour);
|
}
|
result.add(piece);
|
currentPos = piece.end;
|
}
|
}
|
return result;
|
}
|
|
private static List<PathSegment> findDetour(Point p1, Point p2, List<Obstacle> obstacles) {
|
// 检查断点是否在同一个障碍物上
|
for (Obstacle obs : obstacles) {
|
if (obs.isOnBoundary(p1) && obs.isOnBoundary(p2)) {
|
return obs.getBoundaryPath(p1, p2);
|
}
|
}
|
// 如果不在同一个障碍物上(理论上较少见,除非跨越了多个障碍物),直接连接
|
List<PathSegment> res = new ArrayList<>();
|
res.add(new PathSegment(p1, p2, false));
|
return res;
|
}
|
|
private static List<PathSegment> clipSegment(PathSegment seg, List<Obstacle> obstacles) {
|
List<PathSegment> result = new ArrayList<>();
|
result.add(seg);
|
for (Obstacle obs : obstacles) {
|
List<PathSegment> 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<Point> 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<Point> getOffsetPolygon(List<Point> pts, double offset) {
|
List<Point> 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<Double> getXIntersections(List<Point> poly, double y) {
|
List<Double> 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<PathSegment> clip(PathSegment seg);
|
abstract boolean isInside(Point p);
|
abstract boolean isOnBoundary(Point p);
|
abstract List<PathSegment> getBoundaryPath(Point p1, Point p2);
|
}
|
|
static class PolyObstacle extends Obstacle {
|
List<Point> pts;
|
PolyObstacle(List<Point> 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<PathSegment> clip(PathSegment seg) {
|
List<Double> 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<PathSegment> 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<PathSegment> 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<Point> pathPoints = new ArrayList<>();
|
pathPoints.add(p1);
|
|
// 简单策略:沿多边形顶点移动。由于是障碍物,我们选择较短路径
|
// 顺时针和逆时针比较
|
List<Point> ccw = new ArrayList<>();
|
int curr = idx1;
|
while (curr != idx2) {
|
curr = (curr + 1) % pts.size();
|
ccw.add(pts.get(curr));
|
}
|
|
List<Point> 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<PathSegment> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> ps) {
|
double l = 0;
|
for(int i=0;i<ps.size()-1;i++) l+=distance(ps.get(i), ps.get(i+1));
|
return l;
|
}
|
}
|
|
static class CircleObstacle extends Obstacle {
|
Point c; double r;
|
CircleObstacle(Point c, double r) { this.c = c; this.r = r; }
|
@Override
|
boolean isInside(Point p) { return distance(p, c) < r - EPS; }
|
@Override
|
List<PathSegment> 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<Double> 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<PathSegment> 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<PathSegment> getBoundaryPath(Point p1, Point p2) {
|
List<PathSegment> 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<Obstacle> parseObstacles(String obsStr, double margin) {
|
List<Obstacle> list = new ArrayList<>();
|
if (obsStr == null || obsStr.isEmpty()) return list;
|
Matcher m = Pattern.compile("\\(([^)]+)\\)").matcher(obsStr);
|
while (m.find()) {
|
List<Point> 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<Point> 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<Point> parseCoordinates(String s) {
|
List<Point> 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<Point> rotatePoints(List<Point> pts, double a) {
|
List<Point> 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; }
|
}
|
}
|