| | |
| | | # 割草机地块障碍物配置文件 |
| | | # 生成时间:2025-12-27T22:43:36.141786300 |
| | | # 生成时间:2025-12-27T23:31:09.655894400 |
| | | # 坐标系:WGS84(度分格式) |
| | | |
| | | # ============ 地块基准站配置 ============ |
| | |
| | | #Dikuai Properties |
| | | #Sat Dec 27 22:43:36 GMT+08:00 2025 |
| | | #Sat Dec 27 23:31:09 GMT+08:00 2025 |
| | | LAND1.intelligentSceneAnalysis=-1 |
| | | LAND1.mowingSafetyDistance=0.53 |
| | | LAND1.landArea=577.12 |
| | |
| | | LAND1.returnPathRawCoordinates=-1 |
| | | LAND1.boundaryOriginalXY=-1 |
| | | LAND1.mowingWidth=1.00 |
| | | LAND1.plannedPath=73.87,49.87;50.78,50.04;41.97,22.44;49.61,19.99;51.85,34.26;66.29,36.70;66.29,21.40;78.09,24.75;73.87,49.87;73.96,49.37;50.62,49.54;50.30,48.54;74.12,48.37;74.29,47.37;49.98,47.54;49.66,46.55;74.46,46.37;74.63,45.37;49.34,45.55;49.02,44.55;74.80,44.37;74.96,43.37;48.71,43.55;48.39,42.55;75.13,42.36;75.30,41.36;48.07,41.56;47.75,40.56;75.47,40.36;75.64,39.36;47.43,39.56;47.11,38.56;75.80,38.36;75.97,37.36;46.79,37.57;66.29,36.43;76.14,36.36;76.31,35.36;66.29,35.43;66.29,34.43;76.48,34.35;76.64,33.35;66.29,33.43;66.29,32.43;76.81,32.35;76.98,31.35;66.29,31.43;66.29,30.43;77.15,30.35;77.32,29.35;66.29,29.43;66.29,28.43;77.48,28.35;77.65,27.35;66.29,27.43;66.29,26.43;77.82,26.34;77.99,25.34;66.29,25.43;66.29,24.43;76.69,24.35;73.25,23.38;66.29,23.43;66.29,22.43;69.81,22.40;66.38,21.43;66.29,21.43;47.85,20.56;49.70,20.54;46.48,36.57;64.75,36.44;59.07,35.48;46.16,35.57;45.84,34.57;53.38,34.52;51.73,33.53;45.52,33.57;45.20,32.58;51.57,32.53;51.42,31.53;44.88,31.58;44.56,30.58;51.26,30.53;51.11,29.53;44.25,29.58;43.93,28.59;50.95,28.54;50.79,27.54;43.61,27.59;43.29,26.59;50.64,26.54;50.48,25.54;42.97,25.59;42.65,24.59;50.32,24.54;50.17,23.54;42.33,23.60;42.02,22.60;50.01,22.54;49.85,21.54;44.66,21.58 |
| | | LAND1.updateTime=2025-12-27 22\:43\:36 |
| | | LAND1.plannedPath=73.82,49.86;50.78,49.99,M;41.98,22.49,M;49.63,20.04,M;51.80,34.27,M;66.28,36.75,M;66.34,21.40,M;78.08,24.80,M;73.82,49.86,M;73.87,49.87;73.87,49.87,T;73.87,49.87,T;73.87,49.87,T;73.87,49.87,T;73.96,49.37,T;73.96,49.37,T;73.96,49.37,T;73.96,49.37,T;73.96,49.37,T;73.91,49.36;50.66,49.52,M;50.62,49.54;50.62,49.54,T;50.62,49.54,T;50.62,49.54,T;50.62,49.54,T;50.30,48.54,T;50.30,48.54,T;50.30,48.54,T;50.30,48.54,T;50.30,48.54,T;50.35,48.53;74.07,48.36,M;74.12,48.37;74.12,48.37,T;74.12,48.37,T;74.12,48.37,T;74.12,48.37,T;74.29,47.37,T;74.29,47.37,T;74.29,47.37,T;74.29,47.37,T;74.29,47.37,T;74.24,47.36;50.03,47.53,M;49.98,47.54;49.98,47.54,T;49.98,47.54,T;49.98,47.54,T;49.98,47.54,T;49.66,46.55,T;49.66,46.55,T;49.66,46.55,T;49.66,46.55,T;49.66,46.55,T;49.71,46.53;74.41,46.36,M;74.46,46.37;74.46,46.37,T;74.46,46.37,T;74.46,46.37,T;74.46,46.37,T;74.63,45.37,T;74.63,45.37,T;74.63,45.37,T;74.63,45.37,T;74.63,45.37,T;74.58,45.36;49.39,45.53,M;49.34,45.55;49.34,45.55,T;49.34,45.55,T;49.34,45.55,T;49.34,45.55,T;49.02,44.55,T;49.02,44.55,T;49.02,44.55,T;49.02,44.55,T;49.02,44.55,T;49.07,44.53;74.75,44.36,M;74.80,44.37;74.80,44.37,T;74.80,44.37,T;74.80,44.37,T;74.80,44.37,T;74.96,43.37,T;74.96,43.37,T;74.96,43.37,T;74.96,43.37,T;74.96,43.37,T;74.91,43.36;48.75,43.54,M;48.71,43.55;48.71,43.55,T;48.71,43.55,T;48.71,43.55,T;48.71,43.55,T;48.39,42.55,T;48.39,42.55,T;48.39,42.55,T;48.39,42.55,T;48.39,42.55,T;48.43,42.54;75.08,42.36,M;75.13,42.36;75.13,42.36,T;75.13,42.36,T;75.13,42.36,T;75.13,42.36,T;75.30,41.36,T;75.30,41.36,T;75.30,41.36,T;75.30,41.36,T;75.30,41.36,T;75.25,41.35;48.12,41.54,M;48.07,41.56;48.07,41.56,T;48.07,41.56,T;48.07,41.56,T;48.07,41.56,T;47.75,40.56,T;47.75,40.56,T;47.75,40.56,T;47.75,40.56,T;47.75,40.56,T;75.47,40.36,M;75.47,40.36,T;75.47,40.36,T;75.47,40.36,T;75.47,40.36,T;75.64,39.36,T;75.64,39.36,T;75.64,39.36,T;75.64,39.36,T;75.64,39.36,T;75.59,39.35;47.48,39.55,M;47.43,39.56;47.43,39.56,T;47.43,39.56,T;47.43,39.56,T;47.43,39.56,T;47.11,38.56,T;47.11,38.56,T;47.11,38.56,T;47.11,38.56,T;47.11,38.56,T;47.16,38.55;75.75,38.35,M;75.80,38.36;75.80,38.36,T;75.80,38.36,T;75.80,38.36,T;75.80,38.36,T;75.97,37.36,T;75.97,37.36,T;75.97,37.36,T;75.97,37.36,T;75.97,37.36,T;46.79,37.57,M;46.79,37.57,T;46.79,37.57,T;46.79,37.57,T;46.79,37.57,T;41.97,22.44,T;41.97,22.44,T;41.97,22.44,T;41.97,22.44,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;66.29,36.70,T;66.29,36.70,T;66.29,36.70,T;66.29,36.70,T;66.29,36.43,T;66.29,36.43,T;66.29,36.43,T;66.29,36.43,T;66.29,36.43,T;66.34,36.43;76.09,36.35,M;76.14,36.36;76.14,36.36,T;76.14,36.36,T;76.14,36.36,T;76.14,36.36,T;76.31,35.36,T;76.31,35.36,T;76.31,35.36,T;76.31,35.36,T;76.31,35.36,T;66.29,35.43,M;66.29,35.43,T;66.29,35.43,T;66.29,35.43,T;66.29,35.43,T;66.29,34.43,T;66.29,34.43,T;66.29,34.43,T;66.29,34.43,T;66.29,34.43,T;66.34,34.43;76.43,34.35,M;76.48,34.35;76.48,34.35,T;76.48,34.35,T;76.48,34.35,T;76.48,34.35,T;76.64,33.35,T;76.64,33.35,T;76.64,33.35,T;76.64,33.35,T;76.64,33.35,T;76.60,33.34;66.34,33.43,M;66.29,33.43;66.29,33.43,T;66.29,33.43,T;66.29,33.43,T;66.29,33.43,T;66.29,32.43,T;66.29,32.43,T;66.29,32.43,T;66.29,32.43,T;66.29,32.43,T;66.34,32.43;76.76,32.34,M;76.81,32.35;76.81,32.35,T;76.81,32.35,T;76.81,32.35,T;76.81,32.35,T;76.98,31.35,T;76.98,31.35,T;76.98,31.35,T;76.98,31.35,T;76.98,31.35,T;76.93,31.34;66.34,31.43,M;66.29,31.43;66.29,31.43,T;66.29,31.43,T;66.29,31.43,T;66.29,31.43,T;66.29,30.43,T;66.29,30.43,T;66.29,30.43,T;66.29,30.43,T;66.29,30.43,T;66.34,30.43;77.10,30.34,M;77.15,30.35;77.15,30.35,T;77.15,30.35,T;77.15,30.35,T;77.15,30.35,T;77.32,29.35,T;77.32,29.35,T;77.32,29.35,T;77.32,29.35,T;77.32,29.35,T;77.27,29.34;66.34,29.43,M;66.29,29.43;66.29,29.43,T;66.29,29.43,T;66.29,29.43,T;66.29,29.43,T;66.29,28.43,T;66.29,28.43,T;66.29,28.43,T;66.29,28.43,T;66.29,28.43,T;66.34,28.43;77.44,28.34,M;77.48,28.35;77.48,28.35,T;77.48,28.35,T;77.48,28.35,T;77.48,28.35,T;77.65,27.35,T;77.65,27.35,T;77.65,27.35,T;77.65,27.35,T;77.65,27.35,T;77.60,27.34;66.34,27.43,M;66.29,27.43;66.29,27.43,T;66.29,27.43,T;66.29,27.43,T;66.29,27.43,T;66.29,26.43,T;66.29,26.43,T;66.29,26.43,T;66.29,26.43,T;66.29,26.43,T;66.34,26.43;77.77,26.34,M;77.82,26.34;77.82,26.34,T;77.82,26.34,T;77.82,26.34,T;77.82,26.34,T;77.99,25.34,T;77.99,25.34,T;77.99,25.34,T;77.99,25.34,T;77.99,25.34,T;77.94,25.34;66.34,25.43,M;66.29,25.43;66.29,25.43,T;66.29,25.43,T;66.29,25.43,T;66.29,25.43,T;66.29,24.43,T;66.29,24.43,T;66.29,24.43,T;66.29,24.43,T;66.29,24.43,T;66.34,24.43;76.67,24.40,M;76.69,24.35;76.69,24.35,T;76.69,24.35,T;76.69,24.35,T;76.69,24.35,T;73.25,23.38,T;73.25,23.38,T;73.25,23.38,T;73.25,23.38,T;73.25,23.38,T;73.24,23.43;66.34,23.43,M;66.29,23.43;66.29,23.43,T;66.29,23.43,T;66.29,23.43,T;66.29,23.43,T;66.29,22.43,T;66.29,22.43,T;66.29,22.43,T;66.29,22.43,T;66.29,22.43,T;66.34,22.43;69.80,22.45,M;69.81,22.40;69.81,22.40,T;69.81,22.40,T;69.81,22.40,T;69.81,22.40,T;66.38,21.43,T;66.38,21.43,T;66.38,21.43,T;66.38,21.43,T;66.38,21.43,T;66.36,21.47;66.34,21.43,M;66.29,21.43;66.29,21.43,T;66.29,21.43,T;66.29,21.43,T;66.29,21.43,T;66.29,36.70,T;66.29,36.70,T;66.29,36.70,T;66.29,36.70,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;47.85,20.56,T;47.85,20.56,T;47.85,20.56,T;47.85,20.56,T;47.85,20.56,T;47.87,20.60;49.65,20.55,M;49.70,20.54;49.70,20.54,T;49.70,20.54,T;49.70,20.54,T;49.70,20.54,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;49.61,19.99,T;41.97,22.44,T;41.97,22.44,T;41.97,22.44,T;41.97,22.44,T;46.48,36.57,T;46.48,36.57,T;46.48,36.57,T;46.48,36.57,T;46.48,36.57,T;46.52,36.55;64.75,36.49,M;64.75,36.44;64.75,36.44,T;64.75,36.44,T;64.75,36.44,T;64.75,36.44,T;59.07,35.48,T;59.07,35.48,T;59.07,35.48,T;59.07,35.48,T;59.07,35.48,T;59.06,35.53;46.20,35.55,M;46.16,35.57;46.16,35.57,T;46.16,35.57,T;46.16,35.57,T;46.16,35.57,T;45.84,34.57,T;45.84,34.57,T;45.84,34.57,T;45.84,34.57,T;45.84,34.57,T;45.89,34.56;53.37,34.57,M;53.38,34.52;53.38,34.52,T;53.38,34.52,T;53.38,34.52,T;53.38,34.52,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;51.85,34.26,T;51.73,33.53,T;51.73,33.53,T;51.73,33.53,T;51.73,33.53,T;51.73,33.53,T;51.68,33.54;45.57,33.56,M;45.52,33.57;45.52,33.57,T;45.52,33.57,T;45.52,33.57,T;45.52,33.57,T;45.20,32.58,T;45.20,32.58,T;45.20,32.58,T;45.20,32.58,T;45.20,32.58,T;45.25,32.56;51.53,32.54,M;51.57,32.53;51.57,32.53,T;51.57,32.53,T;51.57,32.53,T;51.57,32.53,T;51.42,31.53,T;51.42,31.53,T;51.42,31.53,T;51.42,31.53,T;51.42,31.53,T;51.37,31.54;44.93,31.56,M;44.88,31.58;44.88,31.58,T;44.88,31.58,T;44.88,31.58,T;44.88,31.58,T;44.56,30.58,T;44.56,30.58,T;44.56,30.58,T;44.56,30.58,T;44.56,30.58,T;44.61,30.57;51.21,30.54,M;51.26,30.53;51.26,30.53,T;51.26,30.53,T;51.26,30.53,T;51.26,30.53,T;51.11,29.53,T;51.11,29.53,T;51.11,29.53,T;51.11,29.53,T;51.11,29.53,T;51.06,29.54;44.29,29.57,M;44.25,29.58;44.25,29.58,T;44.25,29.58,T;44.25,29.58,T;44.25,29.58,T;43.93,28.59,T;43.93,28.59,T;43.93,28.59,T;43.93,28.59,T;43.93,28.59,T;43.97,28.57;50.90,28.54,M;50.95,28.54;50.95,28.54,T;50.95,28.54,T;50.95,28.54,T;50.95,28.54,T;50.79,27.54,T;50.79,27.54,T;50.79,27.54,T;50.79,27.54,T;50.79,27.54,T;50.74,27.54;43.66,27.57,M;43.61,27.59;43.61,27.59,T;43.61,27.59,T;43.61,27.59,T;43.61,27.59,T;43.29,26.59,T;43.29,26.59,T;43.29,26.59,T;43.29,26.59,T;43.29,26.59,T;43.34,26.57;50.59,26.55,M;50.64,26.54;50.64,26.54,T;50.64,26.54,T;50.64,26.54,T;50.64,26.54,T;50.48,25.54,T;50.48,25.54,T;50.48,25.54,T;50.48,25.54,T;50.48,25.54,T;50.43,25.55;43.02,25.58,M;42.97,25.59;42.97,25.59,T;42.97,25.59,T;42.97,25.59,T;42.97,25.59,T;42.65,24.59,T;42.65,24.59,T;42.65,24.59,T;42.65,24.59,T;42.65,24.59,T;42.70,24.58;50.27,24.55,M;50.32,24.54;50.32,24.54,T;50.32,24.54,T;50.32,24.54,T;50.32,24.54,T;50.17,23.54,T;50.17,23.54,T;50.17,23.54,T;50.17,23.54,T;50.17,23.54,T;50.12,23.55;42.38,23.58,M;42.33,23.60;42.33,23.60,T;42.33,23.60,T;42.33,23.60,T;42.33,23.60,T;42.02,22.60,T;42.02,22.60,T;42.02,22.60,T;42.02,22.60,T;42.02,22.60,T;42.06,22.58;49.96,22.55,M;50.01,22.54;50.01,22.54,T;50.01,22.54,T;50.01,22.54,T;50.01,22.54,T;49.85,21.54,T;49.85,21.54,T;49.85,21.54,T;49.85,21.54,T;49.85,21.54,T;49.81,21.55;44.68,21.63,M |
| | | LAND1.updateTime=2025-12-27 23\:31\:09 |
| | | LAND1.baseStationCoordinates=3949.89151752,N,11616.79267501,E |
| | | LAND1.boundaryPointInterval=-1 |
| | | LAND1.createTime=2025-12-23 17\:08\:09 |
| | |
| | | #Mower Configuration Properties - Updated |
| | | #Sat Dec 27 22:43:28 GMT+08:00 2025 |
| | | #Sat Dec 27 23:41:35 GMT+08:00 2025 |
| | | appVersion=-1 |
| | | simCardNumber=-1 |
| | | currentWorkLandNumber=LAND1 |
| | |
| | | boundaryLengthVisible=false |
| | | idleTrailDurationSeconds=60 |
| | | handheldMarkerId=1872 |
| | | viewCenterX=-66.17 |
| | | viewCenterY=-34.20 |
| | | viewCenterX=-57.93 |
| | | viewCenterY=-27.08 |
| | | manualBoundaryDrawingMode=false |
| | | mowerId=6258 |
| | | serialPortName=COM15 |
| | | serialAutoConnect=true |
| | | mapScale=10.32 |
| | | mapScale=8.60 |
| | | measurementModeEnabled=false |
| | | firmwareVersion=-1 |
| | | cuttingWidth=200 |
| | |
| | | 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. // 方法说明 |
| | | */ // 文档注释结束 |
| | |
| | | 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. // 绘制路径 |
| | | */ // 文档注释结束 |
| | |
| | | * @param obstaclesCoords 障碍物坐标字符串 |
| | | * @param mowingPattern 割草模式("parallel"或"spiral") |
| | | */ // 文档注释结束 |
| | | public static void drawPlannedPath(Graphics2D g2d, List<Point2D.Double> path, double scale, double arrowScale, |
| | | 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; // 数据不足直接返回 |
| | |
| | | drawInnerBoundary(g2d, boundaryCoords, safetyDistance, scale); |
| | | } |
| | | |
| | | // 2. 直接绘制路径(不再重新生成) |
| | | 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); // 绘制折线 |
| | | // 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); // 绘制终点箭头 |
| | | |
| | | 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; // 检测异常时,按作业段处理以不中断绘制 |
| | | } |
| | | } |
| | | |
| | | /** // 文档注释开始 |
| | | * 绘制内缩边界(围边)- 马尔斯绿色 |
| | |
| | | return ""; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | AoxinglujingNoObstacle.Point last = null; |
| | | for (AoxinglujingNoObstacle.PathSegment segment : segments) { |
| | | // 只添加割草工作段,跳过过渡段 |
| | | if (segment.isMowing) { |
| | | // 如果起点与上一个终点不同,添加起点 |
| | | if (last == null || !equals2D(last, segment.start)) { |
| | | appendPoint(sb, segment.start); |
| | | } |
| | | // 添加终点 |
| | | appendPoint(sb, segment.end); |
| | | last = segment.end; |
| | | AoxinglujingNoObstacle.Point lastEnd = null; |
| | | boolean firstWritten = false; |
| | | for (AoxinglujingNoObstacle.PathSegment s : segments) { |
| | | if (!firstWritten) { |
| | | appendPoint(sb, s.start); |
| | | firstWritten = true; |
| | | lastEnd = s.start; |
| | | } else if (lastEnd == null || !equals2D(lastEnd, s.start)) { |
| | | // 非连续段,开始新的子路径 |
| | | appendPoint(sb, s.start); |
| | | lastEnd = s.start; |
| | | } |
| | | appendPointWithType(sb, s.end, s.isMowing); |
| | | lastEnd = s.end; |
| | | } |
| | | return sb.toString(); |
| | | } |
| | |
| | | return ""; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | YixinglujingNoObstacle.Point last = null; |
| | | for (YixinglujingNoObstacle.PathSegment segment : segments) { |
| | | // 只添加割草工作段,跳过过渡段 |
| | | if (segment.isMowing) { |
| | | // 如果起点与上一个终点不同,添加起点 |
| | | if (last == null || !equalsYixingPoint(last, segment.start)) { |
| | | appendYixingPoint(sb, segment.start); |
| | | } |
| | | // 添加终点 |
| | | appendYixingPoint(sb, segment.end); |
| | | last = segment.end; |
| | | YixinglujingNoObstacle.Point lastEnd = null; |
| | | boolean firstWritten = false; |
| | | for (YixinglujingNoObstacle.PathSegment s : segments) { |
| | | if (!firstWritten) { |
| | | appendYixingPoint(sb, s.start); |
| | | firstWritten = true; |
| | | lastEnd = s.start; |
| | | } else if (lastEnd == null || !equalsYixingPoint(lastEnd, s.start)) { |
| | | // 非连续段,开始新的子路径 |
| | | appendYixingPoint(sb, s.start); |
| | | lastEnd = s.start; |
| | | } |
| | | appendYixingPointWithType(sb, s.end, s.isMowing); |
| | | lastEnd = s.end; |
| | | } |
| | | return sb.toString(); |
| | | } |
| | |
| | | return ""; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | YixinglujingHaveObstacel.Point last = null; |
| | | for (YixinglujingHaveObstacel.PathSegment segment : segments) { |
| | | // 如果是第一段,或者当前段起点与上一段终点不连续,则添加起点 |
| | | if (last == null || !equalsYixingHaveObstaclePoint(last, segment.start)) { |
| | | appendYixingHaveObstaclePoint(sb, segment.start); |
| | | YixinglujingHaveObstacel.Point lastEnd = null; |
| | | boolean firstWritten = false; |
| | | for (YixinglujingHaveObstacel.PathSegment s : segments) { |
| | | if (!firstWritten) { |
| | | appendYixingHaveObstaclePoint(sb, s.start); |
| | | firstWritten = true; |
| | | lastEnd = s.start; |
| | | } else if (lastEnd == null || !equalsYixingHaveObstaclePoint(lastEnd, s.start)) { |
| | | appendYixingHaveObstaclePoint(sb, s.start); |
| | | lastEnd = s.start; |
| | | } |
| | | // 添加终点 |
| | | appendYixingHaveObstaclePoint(sb, segment.end); |
| | | last = segment.end; |
| | | appendYixingHaveObstaclePointWithType(sb, s.end, s.isMowing); |
| | | lastEnd = s.end; |
| | | } |
| | | return sb.toString(); |
| | | } |
| | |
| | | sb.append(String.format(Locale.US, "%.2f,%.2f", point.x, point.y)); |
| | | } |
| | | |
| | | private void appendYixingHaveObstaclePointWithType(StringBuilder sb, YixinglujingHaveObstacel.Point point, boolean isMowing) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format(Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T")); |
| | | } |
| | | |
| | | /** |
| | | * 比较两个点是否相同(使用小的容差) |
| | | */ |
| | |
| | | } |
| | | sb.append(String.format(Locale.US, "%.2f,%.2f", point.x, point.y)); |
| | | } |
| | | |
| | | private void appendPointWithType(StringBuilder sb, AoxinglujingNoObstacle.Point point, boolean isMowing) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format(Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T")); |
| | | } |
| | | |
| | | /** |
| | | * 比较两个 YixinglujingNoObstacle.Point 是否相同(使用小的容差) |
| | |
| | | sb.append(String.format(Locale.US, "%.2f,%.2f", point.x, point.y)); |
| | | } |
| | | |
| | | private void appendYixingPointWithType(StringBuilder sb, YixinglujingNoObstacle.Point point, boolean isMowing) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format(Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T")); |
| | | } |
| | | |
| | | /** |
| | | * 比较两个 AoxinglujingHaveObstacel.Point 是否相同(使用小的容差) |
| | | */ |
| | |
| | | * 修复:解决凹多边形扫描线跨越边界的问题,优化路径对齐 |
| | | */ |
| | | public class YixinglujingNoObstacle { |
| | | // 开关:是否强制所有非作业连接沿安全边界行走(避免任何内区直线跨越) |
| | | // 改为可动态设置,自动依据地块形状启用 |
| | | private static boolean FORCE_BOUNDARY_TRAVEL = true; |
| | | // 用法说明(无障碍物路径规划): |
| | | // - 方法用途:根据地块边界、割草宽度与安全边距,生成覆盖全区域的割草路径。 |
| | | // - 参数: |
| | |
| | | |
| | | // 3. 确定最优作业角度 |
| | | double bestAngle = findOptimalAngle(boundary); |
| | | |
| | | // 3.1 自动判断是否需要强制沿边界旅行(检测凹部/多段扫描行) |
| | | FORCE_BOUNDARY_TRAVEL = shouldForceBoundaryTravel(boundary, mowWidth, bestAngle); |
| | | |
| | | // 4. 获取首个作业点,用于对齐围边起点 |
| | | Point firstScanStart = getFirstScanPoint(boundary, mowWidth, bestAngle); |
| | |
| | | |
| | | finalPath.addAll(scanPath); |
| | | |
| | | // 8. 格式化坐标:保留两位小数 |
| | | // 8. 最终安全净化:确保所有段在内缩边界上或内部(自动贴边阈值) |
| | | sanitizePath(finalPath, boundary, mowWidth, safeMargin); |
| | | |
| | | // 9. 格式化坐标:保留两位小数 |
| | | 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; |
| | |
| | | return finalPath; |
| | | } |
| | | |
| | | // 对所有路径段进行安全净化: |
| | | // - 非作业段:统一沿边界路径替换 |
| | | // - 作业段:若端点在外或段与边界相交,吸附端点到边界并向内侧偏移 epsilon |
| | | private static void sanitizePath(List<PathSegment> segments, List<Point> polygon, double width, double margin) { |
| | | double epsilon = computeAutoInnerOffset(polygon, width, margin); |
| | | List<PathSegment> sanitized = new ArrayList<>(); |
| | | for (PathSegment s : segments) { |
| | | boolean startInside = isPointInPolygon(s.start, polygon); |
| | | boolean endInside = isPointInPolygon(s.end, polygon); |
| | | boolean intersects = segmentIntersectsBoundary(s.start, s.end, polygon); |
| | | if (!s.isMowing) { |
| | | // 非作业段统一替换为沿边界路径 |
| | | List<Point> path = getBoundaryPathWithSnap(s.start, s.end, polygon); |
| | | for (int i = 0; i < path.size() - 1; i++) { |
| | | sanitized.add(new PathSegment(path.get(i), path.get(i+1), false)); |
| | | } |
| | | } else { |
| | | if (!startInside || !endInside || intersects) { |
| | | SnapResult s1 = snapToBoundary(s.start, polygon); |
| | | SnapResult s2 = snapToBoundary(s.end, polygon); |
| | | Point p1 = pushInsideOnEdge(s1, polygon, epsilon); |
| | | Point p2 = pushInsideOnEdge(s2, polygon, epsilon); |
| | | sanitized.add(new PathSegment(p1, p2, true)); |
| | | } else { |
| | | sanitized.add(s); |
| | | } |
| | | } |
| | | } |
| | | segments.clear(); |
| | | segments.addAll(sanitized); |
| | | } |
| | | |
| | | private static boolean segmentIntersectsBoundary(Point a, Point b, List<Point> polygon) { |
| | | for (int i = 0; i < polygon.size(); i++) { |
| | | Point c = polygon.get(i); |
| | | Point d = polygon.get((i + 1) % polygon.size()); |
| | | // 忽略共享端点的相交 |
| | | if (isSamePoint(a, c) || isSamePoint(a, d) || isSamePoint(b, c) || isSamePoint(b, d)) continue; |
| | | if (segmentsIntersect(a, b, c, d)) return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | // 将边界上的投影点向内侧偏移 epsilon |
| | | private static Point pushInsideOnEdge(SnapResult sr, List<Point> poly, double epsilon) { |
| | | int i = sr.edgeIndex; |
| | | Point s = poly.get(i); |
| | | Point e = poly.get((i + 1) % poly.size()); |
| | | double dx = e.x - s.x, dy = e.y - s.y; |
| | | double len = Math.hypot(dx, dy); |
| | | if (len < 1e-6) return sr.onEdge; |
| | | // 对于逆时针(CCW)多边形,左转法向量 (-dy, dx) 指向内侧 |
| | | double nx = -dy / len, ny = dx / len; |
| | | return new Point(sr.onEdge.x + nx * epsilon, sr.onEdge.y + ny * epsilon); |
| | | } |
| | | |
| | | // 自动计算贴边内偏移阈值 epsilon:根据地块尺度、最短边、割草宽度与安全边距综合估算 |
| | | private static double computeAutoInnerOffset(List<Point> polygon, double width, double margin) { |
| | | double minEdge = Double.MAX_VALUE; |
| | | double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE; |
| | | double maxX = -Double.MAX_VALUE, maxY = -Double.MAX_VALUE; |
| | | for (int i = 0; i < polygon.size(); i++) { |
| | | Point a = polygon.get(i); |
| | | Point b = polygon.get((i + 1) % polygon.size()); |
| | | minEdge = Math.min(minEdge, Math.hypot(a.x - b.x, a.y - b.y)); |
| | | minX = Math.min(minX, a.x); minY = Math.min(minY, a.y); |
| | | maxX = Math.max(maxX, a.x); maxY = Math.max(maxY, a.y); |
| | | } |
| | | double diag = Math.hypot(maxX - minX, maxY - minY); |
| | | // 基础量:数值稳定需要的最小内偏移(取割幅的1%与对角线的0.2%之间的较大值) |
| | | double base = Math.max(width * 0.01, diag * 0.002); |
| | | // 上限:不超过安全边距的20%与割幅的10% |
| | | double upper = Math.min(margin * 0.2, width * 0.1); |
| | | // 受边长约束:不超过最短边的2% |
| | | double edgeLimit = minEdge * 0.02; |
| | | double eps = Math.min(upper, Math.max(base, edgeLimit * 0.5)); |
| | | // 下限/上限最终钳位:3mm 到 5cm |
| | | eps = Math.max(0.003, Math.min(eps, 0.05)); |
| | | return eps; |
| | | } |
| | | |
| | | // 根据扫描行的交点数量来判断是否存在“多段行”(>=2段),有凹部或窄通道时启用强制沿边界旅行 |
| | | private static boolean shouldForceBoundaryTravel(List<Point> polygon, double width, double angle) { |
| | | List<Point> 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); } |
| | | |
| | | int multiSegmentRows = 0; |
| | | int totalRows = 0; |
| | | for (double y = minY + width/2; y <= maxY - width/2; y += width) { |
| | | List<Double> xIntersections = getXIntersections(rotatedPoly, y); |
| | | if (xIntersections.size() < 2) continue; |
| | | totalRows++; |
| | | if (xIntersections.size() >= 4) multiSegmentRows++; // 同一行出现两个及以上作业段 |
| | | } |
| | | // 只要出现过“多段行”,就强制沿边界旅行;也可按比例阈值触发(例如 >=10%) |
| | | if (multiSegmentRows > 0) return true; |
| | | double ratio = totalRows == 0 ? 0.0 : (double) multiSegmentRows / (double) totalRows; |
| | | return ratio >= 0.1; // 兜底阈值 |
| | | } |
| | | |
| | | private static List<PathSegment> generateGlobalScanPath(List<Point> polygon, double width, double angle, Point currentPos) { |
| | | // 先尝试将凹陷处视为两个独立区域,分两次扫描,避免跨区直线连接 |
| | | List<PathSegment> all = new ArrayList<>(); |
| | |
| | | } |
| | | |
| | | private static void addSafeConnection(List<PathSegment> segments, Point start, Point end, List<Point> polygon) { |
| | | if (isSegmentSafe(start, end, polygon)) { |
| | | if (!FORCE_BOUNDARY_TRAVEL && isSegmentSafe(start, end, polygon)) { |
| | | segments.add(new PathSegment(start, end, false)); |
| | | } else { |
| | | List<Point> path = getBoundaryPath(start, end, polygon); |
| | | for (int i = 0; i < path.size() - 1; i++) { |
| | | segments.add(new PathSegment(path.get(i), path.get(i+1), false)); |
| | | } |
| | | return; |
| | | } |
| | | List<Point> path = getBoundaryPathWithSnap(start, end, polygon); |
| | | for (int i = 0; i < path.size() - 1; i++) { |
| | | segments.add(new PathSegment(path.get(i), path.get(i+1), false)); |
| | | } |
| | | } |
| | | |
| | | // 强制沿边界绕行的连接(不做直线安全判断),用来在同一扫描行的多个作业段之间跳转 |
| | | private static void addBoundaryConnection(List<PathSegment> segments, Point start, Point end, List<Point> polygon) { |
| | | List<Point> path = getBoundaryPath(start, end, polygon); |
| | | List<Point> path = getBoundaryPathWithSnap(start, end, polygon); |
| | | for (int i = 0; i < path.size() - 1; i++) { |
| | | segments.add(new PathSegment(path.get(i), path.get(i+1), false)); |
| | | } |
| | | } |
| | | |
| | | // 将任意两点通过“吸附到边界”后沿边界最短路径连接 |
| | | private static List<Point> getBoundaryPathWithSnap(Point start, Point end, List<Point> polygon) { |
| | | SnapResult s1 = snapToBoundary(start, polygon); |
| | | SnapResult s2 = snapToBoundary(end, polygon); |
| | | int n = polygon.size(); |
| | | |
| | | // 前向路径(顺边) |
| | | List<Point> pathFwd = new ArrayList<>(); |
| | | pathFwd.add(start); |
| | | pathFwd.add(s1.onEdge); |
| | | int curr = s1.edgeIndex; |
| | | while (curr != s2.edgeIndex) { |
| | | pathFwd.add(polygon.get((curr + 1) % n)); |
| | | curr = (curr + 1) % n; |
| | | } |
| | | pathFwd.add(s2.onEdge); |
| | | pathFwd.add(end); |
| | | |
| | | // 反向路径(逆边) |
| | | List<Point> pathRev = new ArrayList<>(); |
| | | pathRev.add(start); |
| | | pathRev.add(s1.onEdge); |
| | | curr = s1.edgeIndex; |
| | | while (curr != s2.edgeIndex) { |
| | | pathRev.add(polygon.get(curr)); |
| | | curr = (curr - 1 + n) % n; |
| | | } |
| | | pathRev.add(s2.onEdge); |
| | | pathRev.add(end); |
| | | |
| | | return getPathLength(pathFwd) < getPathLength(pathRev) ? pathFwd : pathRev; |
| | | } |
| | | |
| | | private static class SnapResult { |
| | | Point onEdge; |
| | | int edgeIndex; |
| | | SnapResult(Point p, int idx) { this.onEdge = p; this.edgeIndex = idx; } |
| | | } |
| | | |
| | | // 计算点到边界最近的投影点以及所在边索引 |
| | | private static SnapResult snapToBoundary(Point p, List<Point> poly) { |
| | | double minD = Double.MAX_VALUE; |
| | | Point bestProj = p; |
| | | int bestIdx = -1; |
| | | for (int i = 0; i < poly.size(); i++) { |
| | | Point s = poly.get(i); |
| | | Point e = poly.get((i + 1) % poly.size()); |
| | | double l2 = (s.x - e.x)*(s.x - e.x) + (s.y - e.y)*(s.y - e.y); |
| | | if (l2 == 0) { |
| | | double d = Math.hypot(p.x - s.x, p.y - s.y); |
| | | if (d < minD) { minD = d; bestProj = s; bestIdx = i; } |
| | | continue; |
| | | } |
| | | double t = ((p.x - s.x) * (e.x - s.x) + (p.y - s.y) * (e.y - s.y)) / l2; |
| | | t = Math.max(0, Math.min(1, t)); |
| | | Point proj = new Point(s.x + t * (e.x - s.x), s.y + t * (e.y - s.y)); |
| | | double d = Math.hypot(p.x - proj.x, p.y - proj.y); |
| | | if (d < minD) { minD = d; bestProj = proj; bestIdx = i; } |
| | | } |
| | | return new SnapResult(bestProj, bestIdx == -1 ? 0 : bestIdx); |
| | | } |
| | | |
| | | private static boolean isSegmentSafe(Point p1, Point p2, List<Point> polygon) { |
| | | Point mid = new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); |
| | | if (!isPointInPolygon(mid, polygon)) return false; |
| | |
| | | return ""; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | AoxinglujingNoObstacle.Point last = null; |
| | | for (AoxinglujingNoObstacle.PathSegment segment : segments) { |
| | | // 只添加割草工作段,跳过过渡段 |
| | | if (segment.isMowing) { |
| | | // 如果起点与上一个终点不同,添加起点 |
| | | if (last == null || !equalsAoxingPoint(last, segment.start)) { |
| | | appendAoxingPoint(sb, segment.start); |
| | | } |
| | | // 添加终点 |
| | | appendAoxingPoint(sb, segment.end); |
| | | last = segment.end; |
| | | AoxinglujingNoObstacle.Point lastEnd = null; |
| | | boolean firstWritten = false; |
| | | for (AoxinglujingNoObstacle.PathSegment s : segments) { |
| | | if (!firstWritten) { |
| | | appendAoxingPoint(sb, s.start); |
| | | firstWritten = true; |
| | | lastEnd = s.start; |
| | | } else if (lastEnd == null || !equalsAoxingPoint(lastEnd, s.start)) { |
| | | appendAoxingPoint(sb, s.start); |
| | | lastEnd = s.start; |
| | | } |
| | | appendAoxingPointWithType(sb, s.end, s.isMowing); |
| | | lastEnd = s.end; |
| | | } |
| | | return sb.toString(); |
| | | } |
| | |
| | | return ""; |
| | | } |
| | | StringBuilder sb = new StringBuilder(); |
| | | YixinglujingNoObstacle.Point last = null; |
| | | for (YixinglujingNoObstacle.PathSegment segment : segments) { |
| | | // 只添加割草工作段,跳过过渡段 |
| | | if (segment.isMowing) { |
| | | // 如果起点与上一个终点不同,添加起点 |
| | | if (last == null || !equalsYixingPoint(last, segment.start)) { |
| | | appendYixingPoint(sb, segment.start); |
| | | } |
| | | // 添加终点 |
| | | appendYixingPoint(sb, segment.end); |
| | | last = segment.end; |
| | | YixinglujingNoObstacle.Point lastEnd = null; |
| | | boolean firstWritten = false; |
| | | for (YixinglujingNoObstacle.PathSegment s : segments) { |
| | | if (!firstWritten) { |
| | | appendYixingPoint(sb, s.start); |
| | | firstWritten = true; |
| | | lastEnd = s.start; |
| | | } else if (lastEnd == null || !equalsYixingPoint(lastEnd, s.start)) { |
| | | appendYixingPoint(sb, s.start); |
| | | lastEnd = s.start; |
| | | } |
| | | appendYixingPointWithType(sb, s.end, s.isMowing); |
| | | lastEnd = s.end; |
| | | } |
| | | return sb.toString(); |
| | | } |
| | |
| | | * 添加 AoxinglujingNoObstacle.Point 到字符串构建器 |
| | | */ |
| | | private void appendAoxingPoint(StringBuilder sb, AoxinglujingNoObstacle.Point point) { |
| | | private void appendAoxingPointWithType(StringBuilder sb, AoxinglujingNoObstacle.Point point, boolean isMowing) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format(java.util.Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T")); |
| | | } |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | |
| | | * 添加 YixinglujingNoObstacle.Point 到字符串构建器 |
| | | */ |
| | | private void appendYixingPoint(StringBuilder sb, YixinglujingNoObstacle.Point point) { |
| | | private void appendYixingPointWithType(StringBuilder sb, YixinglujingNoObstacle.Point point, boolean isMowing) { |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | | sb.append(String.format(java.util.Locale.US, "%.2f,%.2f,%s", point.x, point.y, isMowing ? "M" : "T")); |
| | | } |
| | | if (sb.length() > 0) { |
| | | sb.append(";"); |
| | | } |
| | |
| | | |
| | | // 如果从地块获取到了路径,使用地块的路径;否则使用currentPlannedPath |
| | | List<Point2D.Double> pathToDraw = currentPlannedPath; |
| | | List<Boolean> flags = null; |
| | | if (plannedPathStr != null && !plannedPathStr.trim().isEmpty() && !"-1".equals(plannedPathStr.trim())) { |
| | | // 从地块获取的路径 |
| | | pathToDraw = lujingdraw.parsePlannedPath(plannedPathStr); |
| | | // 优先解析带标注格式 |
| | | lujingdraw.ParsedPath parsed = lujingdraw.parsePlannedPathWithFlags(plannedPathStr); |
| | | pathToDraw = parsed.points; |
| | | flags = parsed.isMowingFlags; |
| | | } |
| | | |
| | | // 调用带地块信息的绘制方法 |
| | | |
| | | // 调用带地块信息的绘制方法(如有标注优先使用) |
| | | if (pathToDraw != null && pathToDraw.size() >= 2) { |
| | | lujingdraw.drawPlannedPath(g2d, pathToDraw, scale, arrowScale, |
| | | boundaryCoords, mowingWidth, safetyDistance, obstaclesCoords, mowingPattern); |
| | | if (flags != null && flags.size() == pathToDraw.size() - 1) { |
| | | lujingdraw.drawPlannedPath(g2d, pathToDraw, flags, scale, arrowScale, |
| | | boundaryCoords, mowingWidth, safetyDistance, obstaclesCoords, mowingPattern); |
| | | } else { |
| | | lujingdraw.drawPlannedPath(g2d, pathToDraw, scale, arrowScale, |
| | | boundaryCoords, mowingWidth, safetyDistance, obstaclesCoords, mowingPattern); |
| | | } |
| | | } |
| | | } |
| | | |