826220679@qq.com
6 小时以前 7881cef5c3dcea8e6037101db2c3eeb2fd3ba5da
src/gecaoji/lujingdraw.java
@@ -1,5 +1,4 @@
package gecaoji; // 包声明
import java.awt.BasicStroke; // 引入基础描边类
import java.awt.Color; // 引入颜色类
import java.awt.Graphics2D; // 引入2D图形上下文
@@ -8,18 +7,38 @@
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 { // 类定义,防止继承
    private static final Color PATH_COLOR = new Color(255, 0, 0, 220); // 路径颜色(红色半透明)
    // 马尔斯绿色 (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<Point2D.Double> points;
        public final List<Boolean> isMowingFlags; // 每段对应标注:true=作业段,false=旅行段
        public ParsedPath(List<Point2D.Double> points, List<Boolean> flags) {
            this.points = points;
            this.isMowingFlags = flags;
        }
    }
    /** // 文档注释开始
     * Parse the planned path string (semicolon-separated "x,y" pairs) into a list of points in meters. // 方法说明
     */ // 文档注释结束
@@ -67,42 +86,328 @@
        return points; // 返回结果
    } // 方法结束
    /**
     * 解析带段标注的路径字符串。格式:第一个点 "x,y";后续点可为 "x,y,M" 或 "x,y,T",分别表示上一点至当前点为作业段或旅行段。
     * 兼容旧格式(无第三字段),此时 flags 返回 null。
     */
    public static ParsedPath parsePlannedPathWithFlags(String plannedPath) {
        List<Point2D.Double> points = new ArrayList<>();
        List<Boolean> 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<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结束
        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结束
        Stroke previous = g2d.getStroke(); // 保存原描边
        float strokeWidth = (float) (2.5 / Math.max(0.5, scale)); // 根据缩放计算线宽
        g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // 设置圆头圆角描边
        g2d.setColor(PATH_COLOR); // 设置线路颜色
        g2d.draw(polyline); // 绘制折线
        // 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.get(1); // 第二个点
        Point2D.Double second = path.size() > 1 ? path.get(1) : path.get(0); // 第二个点
        Point2D.Double end = path.get(path.size() - 1); // 终点
        Point2D.Double prev = path.get(path.size() - 2); // 倒数第二个点
        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); // 绘制终点箭头
        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<Point2D.Double> path, List<Boolean> 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<Coordinate> 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<Coordinate> 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; // 数据不足返回
@@ -144,4 +449,36 @@
        g2d.setColor(color); // 设置颜色
        g2d.fill(arrow); // 填充箭头
    } // 方法结束
    /**
     * 解析坐标字符串
     */
    private static List<Coordinate> parseCoordinates(String s) {
        List<Coordinate> 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;
    }
} // 类结束