package gecaoji; // 包声明
|
|
import java.awt.BasicStroke; // 引入基础描边类
|
import java.awt.Color; // 引入颜色类
|
import java.awt.Graphics2D; // 引入2D图形上下文
|
import java.awt.Stroke; // 引入描边接口
|
import java.awt.geom.Path2D; // 引入路径绘制类
|
import java.awt.geom.Point2D; // 引入二维点类
|
import java.util.ArrayList; // 引入动态数组
|
import java.util.List; // 引入列表接口
|
import lujing.Lunjingguihua; // 引入路径规划类
|
import org.locationtech.jts.geom.Coordinate; // 引入坐标类
|
import org.locationtech.jts.geom.GeometryFactory; // 引入几何工厂类
|
import org.locationtech.jts.geom.Polygon; // 引入多边形类
|
|
/** // 文档注释开头
|
* Helper class to parse and render planned mowing paths. // 辅助类说明
|
*/ // 文档注释结尾
|
public final class lujingdraw { // 类定义,防止继承
|
// 马尔斯绿色 (RGB: 102, 205, 170) - 用于内缩边界(围边)
|
private static final Color INNER_BOUNDARY_COLOR = new Color(102, 205, 170);
|
// 提香红透明度70% (RGB: 210, 4, 45, alpha: 178) - 用于割草作业路径
|
private static final Color MOWING_PATH_COLOR = new Color(210, 4, 45, 178);
|
// 蓝色 - 用于非作业移动路径
|
private static final Color TRAVEL_PATH_COLOR = new Color(0, 0, 255);
|
// 虚线样式 - 用于非作业移动路径
|
private static final float[] DASH_PATTERN = {10.0f, 5.0f};
|
private static final Color START_POINT_COLOR = new Color(0, 0, 0, 220); // 起点箭头颜色
|
private static final Color END_POINT_COLOR = new Color(0, 0, 0, 220); // 终点箭头颜色
|
|
private lujingdraw() { // 私有构造防止实例化
|
} // 空实现
|
|
/** // 文档注释开始
|
* Parse the planned path string (semicolon-separated "x,y" pairs) into a list of points in meters. // 方法说明
|
*/ // 文档注释结束
|
public static List<Point2D.Double> parsePlannedPath(String plannedPath) { // 解析路径字符串
|
List<Point2D.Double> points = new ArrayList<>(); // 存放解析后的点集
|
if (plannedPath == null) { // 判空
|
return points; // 返回空集合
|
} // if结束
|
|
String normalized = plannedPath.trim(); // 去除首尾空格
|
if (normalized.isEmpty() || "-1".equals(normalized)) { // 无效判断
|
return points; // 返回空集合
|
} // if结束
|
|
int commentIndex = normalized.indexOf('#'); // 查找注释符号
|
if (commentIndex >= 0) { // 找到注释
|
normalized = normalized.substring(0, commentIndex).trim(); // 截取注释前内容
|
} // if结束
|
|
if (normalized.isEmpty()) { // 再次判空
|
return points; // 返回空集合
|
} // if结束
|
|
String[] segments = normalized.split(";"); // 切分各段
|
for (String segment : segments) { // 遍历每段
|
if (segment == null) { // 判空
|
continue; // 跳过
|
} // if结束
|
String trimmed = segment.trim(); // 去除空格
|
if (trimmed.isEmpty()) { // 空段
|
continue; // 跳过
|
} // if结束
|
String[] parts = trimmed.split(","); // 切分坐标
|
if (parts.length < 2) { // 检查长度
|
continue; // 跳过
|
} // if结束
|
try { // 数值解析
|
double x = Double.parseDouble(parts[0].trim()); // 解析X坐标
|
double y = Double.parseDouble(parts[1].trim()); // 解析Y坐标
|
points.add(new Point2D.Double(x, y)); // 加入点集
|
} catch (NumberFormatException ignored) { // 捕获格式异常
|
// Ignore malformed coordinates // 忽略非法坐标
|
} // try结束
|
} // for结束
|
return points; // 返回结果
|
} // 方法结束
|
|
/** // 文档注释开始
|
* Draw the planned mowing path. // 绘制路径
|
*/ // 文档注释结束
|
public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale, double arrowScale) { // 绘制主方法
|
drawPlannedPath(g2d, path, scale, arrowScale, null, null, null, null, null); // 调用重载方法
|
} // 方法结束
|
|
/** // 文档注释开始
|
* Draw the planned mowing path with segment information. // 绘制路径(带段信息)
|
* @param boundaryCoords 地块边界坐标字符串,用于绘制内缩边界
|
* @param mowingWidth 割草宽度(米),字符串格式
|
* @param safetyDistance 安全距离(米),字符串格式
|
* @param obstaclesCoords 障碍物坐标字符串
|
* @param mowingPattern 割草模式("parallel"或"spiral")
|
*/ // 文档注释结束
|
public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale, double arrowScale,
|
String boundaryCoords, String mowingWidth, String safetyDistance, String obstaclesCoords, String mowingPattern) { // 绘制主方法(重载)
|
if (path == null || path.size() < 2) { // 判定点数
|
return; // 数据不足直接返回
|
} // if结束
|
|
Stroke previous = g2d.getStroke(); // 保存原描边
|
float strokeWidth = (float) (2.5 / Math.max(0.5, scale)); // 根据缩放计算线宽
|
|
// 1. 绘制内缩边界(围边)- 马尔斯绿色
|
if (boundaryCoords != null && !boundaryCoords.trim().isEmpty() && !"-1".equals(boundaryCoords.trim())) {
|
drawInnerBoundary(g2d, boundaryCoords, safetyDistance, scale);
|
}
|
|
// 2. 尝试重新生成路径段以区分作业路径和移动路径
|
List<Lunjingguihua.PathSegment> segments = null;
|
if (boundaryCoords != null && mowingWidth != null) {
|
try {
|
// 解析割草模式
|
String mode = "parallel"; // 默认平行模式
|
if (mowingPattern != null && !mowingPattern.trim().isEmpty()) {
|
String pattern = mowingPattern.trim().toLowerCase();
|
if ("1".equals(pattern) || "spiral".equals(pattern) || "螺旋式".equals(pattern) || "螺旋".equals(pattern)) {
|
mode = "spiral";
|
} else if ("parallel".equals(pattern) || "平行线".equals(pattern) || "平行".equals(pattern)) {
|
mode = "parallel";
|
}
|
}
|
segments = Lunjingguihua.generatePathSegments(
|
boundaryCoords,
|
obstaclesCoords != null ? obstaclesCoords : "",
|
mowingWidth,
|
safetyDistance,
|
mode
|
);
|
} catch (Exception e) {
|
// 如果重新生成失败,使用简单绘制方式
|
segments = null;
|
}
|
}
|
|
// 3. 根据是否有段信息选择不同的绘制方式
|
if (segments != null && !segments.isEmpty()) {
|
// 有段信息:分别绘制作业路径和移动路径
|
drawPathSegments(g2d, segments, scale, arrowScale);
|
} else {
|
// 无段信息:使用简单绘制方式(所有路径使用作业路径颜色)
|
Path2D polyline = new Path2D.Double(); // 创建折线
|
boolean move = true; // 首段标记
|
for (Point2D.Double point : path) { // 遍历点集
|
if (move) { // 第一段
|
polyline.moveTo(point.x, point.y); // 移动到首点
|
move = false; // 更新标记
|
} else { // 后续段
|
polyline.lineTo(point.x, point.y); // 连线到下一点
|
} // if结束
|
} // for结束
|
|
g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // 设置圆头圆角描边
|
g2d.setColor(MOWING_PATH_COLOR); // 设置作业路径颜色(提香红70%透明度)
|
g2d.draw(polyline); // 绘制折线
|
|
Point2D.Double start = path.get(0); // 起点
|
Point2D.Double second = path.get(1); // 第二个点
|
Point2D.Double end = path.get(path.size() - 1); // 终点
|
Point2D.Double prev = path.get(path.size() - 2); // 倒数第二个点
|
|
drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale); // 绘制起点箭头
|
drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale); // 绘制终点箭头
|
}
|
|
g2d.setStroke(previous); // 恢复原描边
|
} // 方法结束
|
|
/** // 文档注释开始
|
* 绘制内缩边界(围边)- 马尔斯绿色
|
*/ // 文档注释结束
|
private static void drawInnerBoundary(Graphics2D g2d, String boundaryCoords, String safetyDistanceStr, double scale) {
|
try {
|
List<Coordinate> boundary = Lunjingguihua.parseCoordinates(boundaryCoords);
|
if (boundary.size() < 4) {
|
return; // 边界点不足
|
}
|
|
// 解析安全距离
|
double safetyDistance = 0.2; // 默认0.2米
|
if (safetyDistanceStr != null && !safetyDistanceStr.trim().isEmpty()) {
|
try {
|
safetyDistance = Double.parseDouble(safetyDistanceStr.trim());
|
} catch (NumberFormatException e) {
|
// 使用默认值
|
}
|
}
|
|
// 创建多边形并内缩
|
GeometryFactory gf = new GeometryFactory();
|
Polygon poly = gf.createPolygon(gf.createLinearRing(boundary.toArray(new Coordinate[0])));
|
if (!poly.isValid()) {
|
poly = (Polygon) poly.buffer(0);
|
}
|
|
// 内缩生成安全边界
|
org.locationtech.jts.geom.Geometry innerBoundary = poly.buffer(-safetyDistance);
|
if (innerBoundary == null || innerBoundary.isEmpty()) {
|
return;
|
}
|
|
// 绘制内缩边界
|
g2d.setColor(INNER_BOUNDARY_COLOR); // 马尔斯绿色
|
float strokeWidth = (float) (3.0 / Math.max(0.5, scale));
|
g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
|
// 将JTS几何体转换为Path2D并绘制
|
Path2D.Double innerPath = geometryToPath2D(innerBoundary);
|
if (innerPath != null) {
|
g2d.draw(innerPath);
|
}
|
} catch (Exception e) {
|
// 绘制失败时忽略,不影响其他路径绘制
|
}
|
} // 方法结束
|
|
/** // 文档注释开始
|
* 将JTS几何体转换为Path2D
|
*/ // 文档注释结束
|
private static Path2D.Double geometryToPath2D(org.locationtech.jts.geom.Geometry geom) {
|
if (geom == null) {
|
return null;
|
}
|
Path2D.Double path = new Path2D.Double();
|
if (geom instanceof Polygon) {
|
Polygon poly = (Polygon) geom;
|
addRingToPath(path, poly.getExteriorRing().getCoordinates(), true);
|
for (int i = 0; i < poly.getNumInteriorRing(); i++) {
|
addRingToPath(path, poly.getInteriorRingN(i).getCoordinates(), true);
|
}
|
} else if (geom instanceof org.locationtech.jts.geom.MultiPolygon) {
|
org.locationtech.jts.geom.MultiPolygon mp = (org.locationtech.jts.geom.MultiPolygon) geom;
|
for (int i = 0; i < mp.getNumGeometries(); i++) {
|
Polygon poly = (Polygon) mp.getGeometryN(i);
|
addRingToPath(path, poly.getExteriorRing().getCoordinates(), true);
|
for (int j = 0; j < poly.getNumInteriorRing(); j++) {
|
addRingToPath(path, poly.getInteriorRingN(j).getCoordinates(), true);
|
}
|
}
|
}
|
return path;
|
} // 方法结束
|
|
/** // 文档注释开始
|
* 将坐标环添加到Path2D
|
*/ // 文档注释结束
|
private static void addRingToPath(Path2D.Double path, Coordinate[] coords, boolean close) {
|
if (coords == null || coords.length < 2) {
|
return;
|
}
|
path.moveTo(coords[0].x, coords[0].y);
|
for (int i = 1; i < coords.length; i++) {
|
path.lineTo(coords[i].x, coords[i].y);
|
}
|
if (close) {
|
path.closePath();
|
}
|
} // 方法结束
|
|
/** // 文档注释开始
|
* 绘制路径段(区分作业路径和移动路径)
|
*/ // 文档注释结束
|
private static void drawPathSegments(Graphics2D g2d, List<Lunjingguihua.PathSegment> segments, double scale, double arrowScale) {
|
if (segments == null || segments.isEmpty()) {
|
return;
|
}
|
|
float strokeWidth = (float) (2.5 / Math.max(0.5, scale));
|
Stroke previous = g2d.getStroke();
|
|
// 分别绘制作业路径和移动路径
|
Path2D.Double mowingPath = new Path2D.Double();
|
Path2D.Double travelPath = new Path2D.Double();
|
boolean mowingStarted = false;
|
boolean travelStarted = false;
|
|
Coordinate lastMowingEnd = null;
|
Coordinate lastTravelEnd = null;
|
|
for (Lunjingguihua.PathSegment seg : segments) {
|
if (seg == null || seg.start == null || seg.end == null) {
|
continue;
|
}
|
|
if (seg.isMowing) {
|
// 作业路径 - 提香红70%透明度
|
if (!mowingStarted || lastMowingEnd == null || !equals2D(lastMowingEnd, seg.start)) {
|
mowingPath.moveTo(seg.start.x, seg.start.y);
|
mowingStarted = true;
|
}
|
mowingPath.lineTo(seg.end.x, seg.end.y);
|
lastMowingEnd = seg.end;
|
} else {
|
// 移动路径 - 蓝色虚线
|
if (!travelStarted || lastTravelEnd == null || !equals2D(lastTravelEnd, seg.start)) {
|
travelPath.moveTo(seg.start.x, seg.start.y);
|
travelStarted = true;
|
}
|
travelPath.lineTo(seg.end.x, seg.end.y);
|
lastTravelEnd = seg.end;
|
}
|
}
|
|
// 绘制作业路径
|
if (mowingStarted) {
|
g2d.setColor(MOWING_PATH_COLOR); // 提香红70%透明度
|
g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
g2d.draw(mowingPath);
|
}
|
|
// 绘制移动路径(虚线)
|
if (travelStarted) {
|
g2d.setColor(TRAVEL_PATH_COLOR); // 蓝色
|
g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, DASH_PATTERN, 0));
|
g2d.draw(travelPath);
|
}
|
|
// 绘制起点和终点箭头
|
if (!segments.isEmpty()) {
|
Lunjingguihua.PathSegment firstSeg = segments.get(0);
|
if (firstSeg != null && firstSeg.start != null && segments.size() > 1) {
|
Lunjingguihua.PathSegment secondSeg = segments.get(1);
|
if (secondSeg != null && secondSeg.start != null) {
|
Point2D.Double start = new Point2D.Double(firstSeg.start.x, firstSeg.start.y);
|
Point2D.Double second = new Point2D.Double(secondSeg.start.x, secondSeg.start.y);
|
drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale);
|
}
|
}
|
|
Lunjingguihua.PathSegment lastSeg = segments.get(segments.size() - 1);
|
if (lastSeg != null && lastSeg.end != null && segments.size() > 1) {
|
Lunjingguihua.PathSegment prevSeg = segments.get(segments.size() - 2);
|
if (prevSeg != null && prevSeg.end != null) {
|
Point2D.Double prev = new Point2D.Double(prevSeg.end.x, prevSeg.end.y);
|
Point2D.Double end = new Point2D.Double(lastSeg.end.x, lastSeg.end.y);
|
drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale);
|
}
|
}
|
}
|
|
g2d.setStroke(previous);
|
} // 方法结束
|
|
/** // 文档注释开始
|
* 比较两个坐标是否相同(容差)
|
*/ // 文档注释结束
|
private static boolean equals2D(Coordinate a, Coordinate b) {
|
if (a == b) return true;
|
if (a == null || b == null) return false;
|
return a.distance(b) < 1e-4;
|
} // 方法结束
|
|
private static void drawArrowMarker(Graphics2D g2d, Point2D.Double from, Point2D.Double to, Color color, double scale, double sizeScale) { // 绘制箭头辅助
|
if (from == null || to == null) { // 判空
|
return; // 数据不足返回
|
} // if结束
|
double dx = to.x - from.x; // 计算X增量
|
double dy = to.y - from.y; // 计算Y增量
|
double length = Math.hypot(dx, dy); // 计算向量长度
|
if (length == 0) { // 防止零长度
|
return; // 无方向返回
|
} // if结束
|
|
double arrowLength = Math.max(2.5, 5.5 / Math.max(0.5, scale)); // 计算箭头长度
|
double clampedScale = sizeScale > 0 ? sizeScale : 1.0; // 防止非法缩放
|
arrowLength *= 0.25 * clampedScale; // 缩小箭头至原来的一半
|
double arrowWidth = arrowLength * 0.45; // 计算箭头宽度
|
|
double ux = dx / length; // 单位向量X
|
double uy = dy / length; // 单位向量Y
|
|
double tipX = to.x; // 箭头尖端X
|
double tipY = to.y; // 箭头尖端Y
|
double baseX = tipX - ux * arrowLength; // 箭头底部X
|
double baseY = tipY - uy * arrowLength; // 箭头底部Y
|
|
double perpX = -uy; // 垂直向量X
|
double perpY = ux; // 垂直向量Y
|
|
double leftX = baseX + perpX * arrowWidth * 0.5; // 左侧点X
|
double leftY = baseY + perpY * arrowWidth * 0.5; // 左侧点Y
|
double rightX = baseX - perpX * arrowWidth * 0.5; // 右侧点X
|
double rightY = baseY - perpY * arrowWidth * 0.5; // 右侧点Y
|
|
Path2D arrow = new Path2D.Double(); // 创建箭头路径
|
arrow.moveTo(tipX, tipY); // 起点移动到尖端
|
arrow.lineTo(leftX, leftY); // 连到左侧
|
arrow.lineTo(rightX, rightY); // 连到右侧
|
arrow.closePath(); // 闭合形成三角形
|
|
g2d.setColor(color); // 设置颜色
|
g2d.fill(arrow); // 填充箭头
|
} // 方法结束
|
} // 类结束
|