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 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 = {5.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() { // 私有构造防止实例化 } // 空实现 // 解析结果:包含点与每个相邻段的作业标注(size=points.size()-1) public static final class ParsedPath { public final List points; public final List isMowingFlags; // 每段对应标注:true=作业段,false=旅行段 public ParsedPath(List points, List flags) { this.points = points; this.isMowingFlags = flags; } } /** // 文档注释开始 * Parse the planned path string (semicolon-separated "x,y" pairs) into a list of points in meters. // 方法说明 */ // 文档注释结束 public static List parsePlannedPath(String plannedPath) { // 解析路径字符串 List 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; // 返回结果 } // 方法结束 /** * 解析带段标注的路径字符串。格式:第一个点 "x,y";后续点可为 "x,y,M" 或 "x,y,T",分别表示上一点至当前点为作业段或旅行段。 * 兼容旧格式(无第三字段),此时 flags 返回 null。 */ public static ParsedPath parsePlannedPathWithFlags(String plannedPath) { List points = new ArrayList<>(); List flags = new ArrayList<>(); if (plannedPath == null) { return new ParsedPath(points, null); } String normalized = plannedPath.trim(); if (normalized.isEmpty() || "-1".equals(normalized)) { return new ParsedPath(points, null); } int commentIndex = normalized.indexOf('#'); if (commentIndex >= 0) { normalized = normalized.substring(0, commentIndex).trim(); } if (normalized.isEmpty()) { return new ParsedPath(points, null); } String[] segments = normalized.split(";"); boolean sawTypeField = false; for (int i = 0; i < segments.length; i++) { String segment = segments[i]; if (segment == null) continue; String trimmed = segment.trim(); if (trimmed.isEmpty()) continue; String[] parts = trimmed.split(","); if (parts.length < 2) continue; try { double x = Double.parseDouble(parts[0].trim()); double y = Double.parseDouble(parts[1].trim()); points.add(new Point2D.Double(x, y)); if (i > 0) { // 第一个点无标注;后续若存在第三字段则解析 if (parts.length >= 3) { String t = parts[2].trim(); boolean isMowing = t.equalsIgnoreCase("M") || t.equalsIgnoreCase("m") || t.equals("1") || t.equalsIgnoreCase("WORK"); flags.add(isMowing); sawTypeField = true; } else { flags.add(Boolean.TRUE); // 默认作业段,便于向后兼容 } } } catch (NumberFormatException ignored) { // 忽略非法坐标 } } return new ParsedPath(points, sawTypeField ? flags : null); } /** // 文档注释开始 * Draw the planned mowing path. // 绘制路径 */ // 文档注释结束 public static void drawPlannedPath(Graphics2D g2d, List 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 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. 按段绘制路径(逐段绘制,避免将不相邻段首尾连成越界直线) org.locationtech.jts.geom.Geometry innerBoundaryGeom = buildInnerBoundaryGeom(boundaryCoords, safetyDistance); for (int i = 1; i < path.size(); i++) { Point2D.Double a = path.get(i - 1); Point2D.Double b = path.get(i); Path2D segment = new Path2D.Double(); segment.moveTo(a.x, a.y); segment.lineTo(b.x, b.y); boolean inside = true; // 默认按作业段绘制 if (innerBoundaryGeom != null) { inside = isSegmentInsideInnerBoundary(innerBoundaryGeom, a, b); } if (inside) { // 作业段:实线 + 提香红 g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g2d.setColor(MOWING_PATH_COLOR); } else { // 旅行段(或沿边界段):虚线 + 蓝色 g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f, DASH_PATTERN, 0.0f)); g2d.setColor(TRAVEL_PATH_COLOR); } g2d.draw(segment); } Point2D.Double start = path.get(0); // 起点 Point2D.Double second = path.size() > 1 ? path.get(1) : path.get(0); // 第二个点 Point2D.Double end = path.get(path.size() - 1); // 终点 Point2D.Double prev = path.size() > 1 ? path.get(path.size() - 2) : path.get(path.size() - 1); // 倒数第二个点 drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale); // 绘制起点箭头 drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale); // 绘制终点箭头 g2d.setStroke(previous); // 恢复原描边 } // 方法结束 /** * 使用算法端提供的段标注绘制路径(优先使用标注,不做内缩边界推断)。 * flags.size() 必须为 path.size()-1。 */ public static void drawPlannedPath(Graphics2D g2d, List path, List flags, double scale, double arrowScale, String boundaryCoords, String mowingWidth, String safetyDistance, String obstaclesCoords, String mowingPattern) { if (path == null || path.size() < 2 || flags == null || flags.size() != path.size() - 1) { // 回退到推断版本 drawPlannedPath(g2d, path, scale, arrowScale, boundaryCoords, mowingWidth, safetyDistance, obstaclesCoords, mowingPattern); return; } 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. 按段绘制,完全遵循算法端标注 for (int i = 1; i < path.size(); i++) { Point2D.Double a = path.get(i - 1); Point2D.Double b = path.get(i); boolean isMowing = Boolean.TRUE.equals(flags.get(i - 1)); Path2D segment = new Path2D.Double(); segment.moveTo(a.x, a.y); segment.lineTo(b.x, b.y); if (isMowing) { g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g2d.setColor(MOWING_PATH_COLOR); } else { g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f, DASH_PATTERN, 0.0f)); g2d.setColor(TRAVEL_PATH_COLOR); } g2d.draw(segment); } Point2D.Double start = path.get(0); Point2D.Double second = path.size() > 1 ? path.get(1) : path.get(0); Point2D.Double end = path.get(path.size() - 1); Point2D.Double prev = path.size() > 1 ? path.get(path.size() - 2) : path.get(path.size() - 1); drawArrowMarker(g2d, start, second, START_POINT_COLOR, scale, arrowScale); drawArrowMarker(g2d, prev, end, END_POINT_COLOR, scale, arrowScale); g2d.setStroke(previous); } // 构建内缩边界几何体,用于安全检测与分段绘制 private static org.locationtech.jts.geom.Geometry buildInnerBoundaryGeom(String boundaryCoords, String safetyDistanceStr) { try { if (boundaryCoords == null || boundaryCoords.trim().isEmpty() || "-1".equals(boundaryCoords.trim())) { return null; } List boundary = parseCoordinates(boundaryCoords); if (boundary.size() < 4) { return null; // 边界点不足 } double safetyDistance = 0.2; // 默认0.2米 if (safetyDistanceStr != null && !safetyDistanceStr.trim().isEmpty()) { try { safetyDistance = Double.parseDouble(safetyDistanceStr.trim()); } catch (NumberFormatException ignored) {} } 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 null; } return innerBoundary; } catch (Exception e) { return null; } } // 判断一段是否在内缩边界内或沿边界(不越界) private static boolean isSegmentInsideInnerBoundary(org.locationtech.jts.geom.Geometry innerBoundary, Point2D.Double a, Point2D.Double b) { try { GeometryFactory gf = new GeometryFactory(); Coordinate[] seg = new Coordinate[] { new Coordinate(a.x, a.y), new Coordinate(b.x, b.y) }; org.locationtech.jts.geom.LineString ls = gf.createLineString(seg); // 包含或覆盖均视为合法,不与内边界外部交叉 if (innerBoundary.contains(ls) || innerBoundary.covers(ls)) { return true; } // 如与内边界边界相交,视为不在内部(用于改变样式并断开连线) return !ls.crosses(innerBoundary.getBoundary()); } catch (Exception e) { return true; // 检测异常时,按作业段处理以不中断绘制 } } /** // 文档注释开始 * 绘制内缩边界(围边)- 马尔斯绿色 */ // 文档注释结束 private static void drawInnerBoundary(Graphics2D g2d, String boundaryCoords, String safetyDistanceStr, double scale) { try { List boundary = 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 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); // 填充箭头 } // 方法结束 /** * 解析坐标字符串 */ private static List parseCoordinates(String s) { List list = new ArrayList<>(); if (s == null || s.trim().isEmpty()) return list; // 增强正则:处理可能存在的多种分隔符 String[] pts = s.split("[;\\s]+"); for (String p : pts) { String trimmed = p.trim().replace("(", "").replace(")", ""); if (trimmed.isEmpty()) continue; String[] xy = trimmed.split("[,,\\s]+"); if (xy.length >= 2) { try { double x = Double.parseDouble(xy[0].trim()); double y = Double.parseDouble(xy[1].trim()); // 过滤无效坐标 if (!Double.isNaN(x) && !Double.isNaN(y) && !Double.isInfinite(x) && !Double.isInfinite(y)) { list.add(new Coordinate(x, y)); } } catch (NumberFormatException ex) { // 忽略解析错误的点 } } } // 确保多边形闭合 if (list.size() > 2 && !list.get(0).equals2D(list.get(list.size() - 1))) { list.add(new Coordinate(list.get(0))); } return list; } } // 类结束