""" Simple path planner to generate approach path from mower start position to planned path start. """ import numpy as np from math import hypot, atan2, cos, sin, pi def plan_approach_path(start_pos, start_heading, target_pos, target_heading, method='line'): """ Generate approach path from mower start to planned path start. Args: start_pos: (x, y) mower starting position start_heading: mower starting heading in radians target_pos: (x, y) planned path starting position target_heading: planned path starting heading in radians method: 'line' | 'dubins' | 'smooth' Returns: List of (x, y) waypoints forming the approach path """ sx, sy = start_pos tx, ty = target_pos dist = hypot(tx - sx, ty - sy) if dist < 0.05: # Already at start, no approach needed return [start_pos, target_pos] if method == 'line': # Simple straight line with waypoints every 0.5m num_points = max(3, int(dist / 0.5) + 1) path = [] for i in range(num_points): t = i / (num_points - 1) x = sx + t * (tx - sx) y = sy + t * (ty - sy) path.append((x, y)) return path elif method == 'smooth': # Smooth curve considering start and target headings # Use cubic bezier curve # Control points based on headings ctrl_dist = min(dist * 0.4, 2.0) # Control point distance # Start control point along start heading c1x = sx + ctrl_dist * cos(start_heading) c1y = sy + ctrl_dist * sin(start_heading) # End control point along target heading (backwards) c2x = tx - ctrl_dist * cos(target_heading) c2y = ty - ctrl_dist * sin(target_heading) # Generate bezier curve points num_points = max(5, int(dist / 0.3) + 1) path = [] for i in range(num_points): t = i / (num_points - 1) # Cubic bezier formula x = (1-t)**3 * sx + 3*(1-t)**2*t * c1x + 3*(1-t)*t**2 * c2x + t**3 * tx y = (1-t)**3 * sy + 3*(1-t)**2*t * c1y + 3*(1-t)*t**2 * c2y + t**3 * ty path.append((x, y)) return path else: raise ValueError(f"Unknown method: {method}") def compute_path_heading(path, idx=0): """Compute heading of path at given index.""" if idx >= len(path) - 1: idx = len(path) - 2 if idx < 0: idx = 0 dx = path[idx + 1][0] - path[idx][0] dy = path[idx + 1][1] - path[idx][1] return atan2(dy, dx) def combine_paths(approach_path, work_path): """ Combine approach path and work path into single path with metadata. Returns: combined_path: List of (x, y) waypoints approach_end_idx: Index where approach path ends (work path starts) """ # Remove duplicate point at junction if exists if len(approach_path) > 0 and len(work_path) > 0: if hypot(approach_path[-1][0] - work_path[0][0], approach_path[-1][1] - work_path[0][1]) < 0.01: combined = approach_path[:-1] + work_path approach_end_idx = len(approach_path) - 1 else: combined = approach_path + work_path approach_end_idx = len(approach_path) else: combined = approach_path + work_path approach_end_idx = len(approach_path) return combined, approach_end_idx if __name__ == '__main__': # Test path planning import json import matplotlib.pyplot as plt # Load planned path with open('example_path.json', 'r') as f: work_path = json.load(f) # Mower starts 2m away from path start mower_start = (work_path[0][0] - 1.5, work_path[0][1] - 1.5) mower_heading = pi / 4 # 45 degrees # Plan path heading plan_heading = compute_path_heading(work_path, 0) # Generate approach paths with different methods approach_line = plan_approach_path(mower_start, mower_heading, work_path[0], plan_heading, 'line') approach_smooth = plan_approach_path(mower_start, mower_heading, work_path[0], plan_heading, 'smooth') # Plot comparison fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) # Line method ax1.plot([p[0] for p in work_path], [p[1] for p in work_path], 'b-', linewidth=2, label='Work Path') ax1.plot([p[0] for p in approach_line], [p[1] for p in approach_line], 'r--', linewidth=2, marker='o', markersize=4, label='Approach (Line)') ax1.plot(mower_start[0], mower_start[1], 'go', markersize=10, label='Mower Start') ax1.arrow(mower_start[0], mower_start[1], 0.3*cos(mower_heading), 0.3*sin(mower_heading), head_width=0.15, head_length=0.1, fc='g', ec='g') ax1.set_xlabel('X (m)') ax1.set_ylabel('Y (m)') ax1.set_title('Line Approach') ax1.legend() ax1.grid(True) ax1.axis('equal') # Smooth method ax2.plot([p[0] for p in work_path], [p[1] for p in work_path], 'b-', linewidth=2, label='Work Path') ax2.plot([p[0] for p in approach_smooth], [p[1] for p in approach_smooth], 'r--', linewidth=2, marker='o', markersize=4, label='Approach (Smooth)') ax2.plot(mower_start[0], mower_start[1], 'go', markersize=10, label='Mower Start') ax2.arrow(mower_start[0], mower_start[1], 0.3*cos(mower_heading), 0.3*sin(mower_heading), head_width=0.15, head_length=0.1, fc='g', ec='g') ax2.set_xlabel('X (m)') ax2.set_ylabel('Y (m)') ax2.set_title('Smooth Approach') ax2.legend() ax2.grid(True) ax2.axis('equal') plt.tight_layout() plt.savefig('approach_path_test.png', dpi=150) print("Saved approach_path_test.png") print(f"Line approach: {len(approach_line)} points") print(f"Smooth approach: {len(approach_smooth)} points")