use crate::error::{SpatialError, SpatialResult};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Pose2D {
pub x: f64,
pub y: f64,
pub theta: f64,
}
impl Pose2D {
pub fn new(x: f64, y: f64, theta: f64) -> Self {
Self { x, y, theta }
}
pub fn distance_to(&self, other: &Pose2D) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
pub fn normalize_angle(&self) -> Self {
let mut normalized_theta = self.theta % (2.0 * PI);
if normalized_theta > PI {
normalized_theta -= 2.0 * PI;
} else if normalized_theta < -PI {
normalized_theta += 2.0 * PI;
}
Self {
x: self.x,
y: self.y,
theta: normalized_theta,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DubinsPathType {
LSL,
LSR,
RSL,
RSR,
LRL,
RLR,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SegmentType {
Left,
Right,
Straight,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DubinsSegment {
pub segment_type: SegmentType,
pub length: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DubinsPath {
start: Pose2D,
goal: Pose2D,
turning_radius: f64,
path_type: DubinsPathType,
segments: [DubinsSegment; 3],
length: f64,
}
impl DubinsPath {
fn new(
start: Pose2D,
goal: Pose2D,
turning_radius: f64,
path_type: DubinsPathType,
segments: [DubinsSegment; 3],
) -> Self {
let length = segments.iter().map(|s| s.length).sum();
Self {
start,
goal,
turning_radius,
path_type,
segments,
length,
}
}
pub fn length(&self) -> f64 {
self.length
}
pub fn path_type(&self) -> DubinsPathType {
self.path_type
}
pub fn segments(&self) -> &[DubinsSegment; 3] {
&self.segments
}
pub fn start(&self) -> &Pose2D {
&self.start
}
pub fn goal(&self) -> &Pose2D {
&self.goal
}
pub fn turning_radius(&self) -> f64 {
self.turning_radius
}
pub fn sample(&self, t: f64) -> SpatialResult<Pose2D> {
if !(0.0..=1.0).contains(&t) {
return Err(SpatialError::ValueError(
"Parameter t must be in [0, 1]".to_string(),
));
}
let target_distance = t * self.length;
let mut current_distance = 0.0;
let mut current_pose = self.start;
for segment in &self.segments {
if current_distance + segment.length >= target_distance {
let segment_distance = target_distance - current_distance;
let segment_t = if segment.length > 0.0 {
segment_distance / segment.length
} else {
0.0
};
return self.sample_segment(¤t_pose, segment, segment_t);
}
current_pose = self.sample_segment(¤t_pose, segment, 1.0)?;
current_distance += segment.length;
}
Ok(self.goal)
}
fn sample_segment(
&self,
start_pose: &Pose2D,
segment: &DubinsSegment,
t: f64,
) -> SpatialResult<Pose2D> {
match segment.segment_type {
SegmentType::Straight => {
let distance = t * segment.length;
let new_x = start_pose.x + distance * start_pose.theta.cos();
let new_y = start_pose.y + distance * start_pose.theta.sin();
Ok(Pose2D::new(new_x, new_y, start_pose.theta))
}
SegmentType::Left => {
let angle = t * segment.length / self.turning_radius;
let center_x = start_pose.x - self.turning_radius * start_pose.theta.sin();
let center_y = start_pose.y + self.turning_radius * start_pose.theta.cos();
let new_theta = start_pose.theta + angle;
let new_x = center_x + self.turning_radius * new_theta.sin();
let new_y = center_y - self.turning_radius * new_theta.cos();
Ok(Pose2D::new(new_x, new_y, new_theta))
}
SegmentType::Right => {
let angle = t * segment.length / self.turning_radius;
let center_x = start_pose.x + self.turning_radius * start_pose.theta.sin();
let center_y = start_pose.y - self.turning_radius * start_pose.theta.cos();
let new_theta = start_pose.theta - angle;
let new_x = center_x - self.turning_radius * new_theta.sin();
let new_y = center_y + self.turning_radius * new_theta.cos();
Ok(Pose2D::new(new_x, new_y, new_theta))
}
}
}
}
pub struct DubinsPlanner {
turning_radius: f64,
}
impl DubinsPlanner {
pub fn new(_turningradius: f64) -> Self {
Self {
turning_radius: _turningradius,
}
}
pub fn plan(&self, start: &Pose2D, goal: &Pose2D) -> SpatialResult<DubinsPath> {
if self.turning_radius <= 0.0 {
return Err(SpatialError::ValueError(
"Turning radius must be positive".to_string(),
));
}
let _start = start.normalize_angle();
let goal = goal.normalize_angle();
let dx = goal.x - start.x;
let dy = goal.y - start.y;
let d = (dx * dx + dy * dy).sqrt();
let theta = (dy).atan2(dx);
let alpha = Self::normalize_angle(_start.theta - theta);
let beta = Self::normalize_angle(goal.theta - theta);
let mut best_path = None;
let mut best_length = f64::INFINITY;
for path_type in [
DubinsPathType::LSL,
DubinsPathType::LSR,
DubinsPathType::RSL,
DubinsPathType::RSR,
DubinsPathType::LRL,
DubinsPathType::RLR,
] {
if let Ok(segments) = self.compute_path_segments(d, alpha, beta, path_type) {
let path_length: f64 = segments.iter().map(|s| s.length).sum();
if path_length < best_length {
best_length = path_length;
best_path = Some(DubinsPath::new(
*start,
goal,
self.turning_radius,
path_type,
segments,
));
}
}
}
best_path.ok_or_else(|| {
SpatialError::ComputationError("Failed to compute any valid Dubins path".to_string())
})
}
fn compute_path_segments(
&self,
d: f64,
alpha: f64,
beta: f64,
path_type: DubinsPathType,
) -> SpatialResult<[DubinsSegment; 3]> {
let d_norm = d / self.turning_radius;
match path_type {
DubinsPathType::LSL => self.lsl(d_norm, alpha, beta),
DubinsPathType::LSR => self.lsr(d_norm, alpha, beta),
DubinsPathType::RSL => self.rsl(d_norm, alpha, beta),
DubinsPathType::RSR => self.rsr(d_norm, alpha, beta),
DubinsPathType::LRL => self.lrl(d_norm, alpha, beta),
DubinsPathType::RLR => self.rlr(d_norm, alpha, beta),
}
}
fn lsl(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
let tmp0 = d + alpha.sin() - beta.sin();
let p_squared =
2.0 + d * d - 2.0 * (alpha - beta).cos() + 2.0 * d * (alpha.sin() - beta.sin());
if p_squared < 0.0 {
return Err(SpatialError::ComputationError(
"Invalid LSL path".to_string(),
));
}
let tmp1 = (beta - alpha).atan2(tmp0);
let t = Self::normalize_angle(-alpha + tmp1);
let p = p_squared.sqrt();
let q = Self::normalize_angle(beta - tmp1);
Ok([
DubinsSegment {
segment_type: SegmentType::Left,
length: t * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Straight,
length: p * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Left,
length: q * self.turning_radius,
},
])
}
fn lsr(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
let p_squared =
-2.0 + d * d + 2.0 * (alpha - beta).cos() + 2.0 * d * (alpha.sin() + beta.sin());
if p_squared < 0.0 {
return Err(SpatialError::ComputationError(
"Invalid LSR path".to_string(),
));
}
let p = p_squared.sqrt();
let tmp2 = ((-alpha - beta).cos() + d).atan2((-alpha - beta).sin() + p);
let t = Self::normalize_angle(-alpha + tmp2);
let q = Self::normalize_angle(-beta + tmp2);
Ok([
DubinsSegment {
segment_type: SegmentType::Left,
length: t * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Straight,
length: p * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Right,
length: q * self.turning_radius,
},
])
}
fn rsl(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
let p_squared =
d * d - 2.0 + 2.0 * (alpha - beta).cos() - 2.0 * d * (alpha.sin() + beta.sin());
if p_squared < 0.0 {
return Err(SpatialError::ComputationError(
"Invalid RSL path".to_string(),
));
}
let p = p_squared.sqrt();
let tmp2 = ((alpha + beta).cos() - d).atan2((alpha + beta).sin() - p);
let t = Self::normalize_angle(alpha - tmp2);
let q = Self::normalize_angle(beta - tmp2);
Ok([
DubinsSegment {
segment_type: SegmentType::Right,
length: t * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Straight,
length: p * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Left,
length: q * self.turning_radius,
},
])
}
fn rsr(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
let tmp0 = d - alpha.sin() + beta.sin();
let p_squared =
2.0 + d * d - 2.0 * (alpha - beta).cos() - 2.0 * d * (alpha.sin() - beta.sin());
if p_squared < 0.0 {
return Err(SpatialError::ComputationError(
"Invalid RSR path".to_string(),
));
}
let tmp1 = (alpha - beta).atan2(tmp0);
let t = Self::normalize_angle(alpha - tmp1);
let p = p_squared.sqrt();
let q = Self::normalize_angle(-beta + tmp1);
Ok([
DubinsSegment {
segment_type: SegmentType::Right,
length: t * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Straight,
length: p * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Right,
length: q * self.turning_radius,
},
])
}
fn lrl(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
let tmp0 =
(6.0 - d * d + 2.0 * (alpha - beta).cos() + 2.0 * d * (alpha.sin() - beta.sin())) / 8.0;
if tmp0.abs() > 1.0 {
return Err(SpatialError::ComputationError(
"Invalid LRL path".to_string(),
));
}
let p = (2.0 * PI - tmp0.acos()).abs();
let t = Self::normalize_angle(
-alpha + (tmp0 - (alpha - beta).cos()).atan2(d + alpha.sin() - beta.sin()),
);
let q = Self::normalize_angle(-Self::normalize_angle(p / 2.0) + beta - alpha + t);
Ok([
DubinsSegment {
segment_type: SegmentType::Left,
length: t * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Right,
length: p * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Left,
length: q * self.turning_radius,
},
])
}
fn rlr(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
let tmp0 =
(6.0 - d * d + 2.0 * (alpha - beta).cos() - 2.0 * d * (alpha.sin() - beta.sin())) / 8.0;
if tmp0.abs() > 1.0 {
return Err(SpatialError::ComputationError(
"Invalid RLR path".to_string(),
));
}
let p = (2.0 * PI - tmp0.acos()).abs();
let t = Self::normalize_angle(
alpha - (tmp0 - (alpha - beta).cos()).atan2(d - alpha.sin() + beta.sin()),
);
let q = Self::normalize_angle(Self::normalize_angle(p / 2.0) - beta + alpha - t);
Ok([
DubinsSegment {
segment_type: SegmentType::Right,
length: t * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Left,
length: p * self.turning_radius,
},
DubinsSegment {
segment_type: SegmentType::Right,
length: q * self.turning_radius,
},
])
}
fn normalize_angle(angle: f64) -> f64 {
let mut normalized = angle % (2.0 * PI);
if normalized > PI {
normalized -= 2.0 * PI;
} else if normalized < -PI {
normalized += 2.0 * PI;
}
normalized
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_pose2d_basic() {
let pose = Pose2D::new(1.0, 2.0, PI / 4.0);
assert_eq!(pose.x, 1.0);
assert_eq!(pose.y, 2.0);
assert_eq!(pose.theta, PI / 4.0);
}
#[test]
fn test_pose2d_distance() {
let pose1 = Pose2D::new(0.0, 0.0, 0.0);
let pose2 = Pose2D::new(3.0, 4.0, 0.0);
assert_relative_eq!(pose1.distance_to(&pose2), 5.0, epsilon = 1e-10);
}
#[test]
fn test_pose2d_normalize_angle() {
let pose = Pose2D::new(0.0, 0.0, 3.0 * PI);
let normalized = pose.normalize_angle();
assert!(
(normalized.theta - PI).abs() < 1e-10 || (normalized.theta - (-PI)).abs() < 1e-10,
"Expected angle to be ±π, got {}",
normalized.theta
);
}
#[test]
fn test_dubins_planner_creation() {
let planner = DubinsPlanner::new(1.0);
assert_eq!(planner.turning_radius, 1.0);
}
#[test]
fn test_dubins_straight_line() {
let start = Pose2D::new(0.0, 0.0, 0.0);
let goal = Pose2D::new(5.0, 0.0, 0.0);
let planner = DubinsPlanner::new(1.0);
let path = planner.plan(&start, &goal).expect("Operation failed");
assert!(path.length() >= 5.0);
assert!(path.length() < 6.0); }
#[test]
fn test_dubins_path_sampling() {
let start = Pose2D::new(0.0, 0.0, 0.0);
let goal = Pose2D::new(2.0, 0.0, 0.0);
let planner = DubinsPlanner::new(1.0);
let path = planner.plan(&start, &goal).expect("Operation failed");
let start_sample = path.sample(0.0).expect("Operation failed");
assert_relative_eq!(start_sample.x, start.x, epsilon = 1e-10);
assert_relative_eq!(start_sample.y, start.y, epsilon = 1e-10);
let goal_sample = path.sample(1.0).expect("Operation failed");
assert_relative_eq!(goal_sample.x, goal.x, epsilon = 1e-2);
assert_relative_eq!(goal_sample.y, goal.y, epsilon = 1e-2);
}
#[test]
fn test_dubins_path_invalid_parameter() {
let start = Pose2D::new(0.0, 0.0, 0.0);
let goal = Pose2D::new(2.0, 0.0, 0.0);
let planner = DubinsPlanner::new(1.0);
let path = planner.plan(&start, &goal).expect("Operation failed");
assert!(path.sample(-0.1).is_err());
assert!(path.sample(1.1).is_err());
}
#[test]
fn test_dubins_path_types() {
let start = Pose2D::new(0.0, 0.0, 0.0);
let planner = DubinsPlanner::new(1.0);
let goals = [
Pose2D::new(5.0, 0.0, 0.0), Pose2D::new(0.0, 5.0, PI / 2.0), Pose2D::new(3.0, 3.0, PI / 4.0), ];
for goal in &goals {
let path_result = planner.plan(&start, goal);
if let Ok(path) = path_result {
assert!(
path.length() > 0.0,
"Path length should be positive for goal {goal:?}"
);
assert_eq!(path.segments().len(), 3);
} else {
println!("No valid path found for goal {goal:?}");
}
}
}
#[test]
fn test_normalize_angle() {
assert_relative_eq!(DubinsPlanner::normalize_angle(0.0), 0.0, epsilon = 1e-10);
assert_relative_eq!(DubinsPlanner::normalize_angle(PI), PI, epsilon = 1e-10);
assert_relative_eq!(DubinsPlanner::normalize_angle(-PI), -PI, epsilon = 1e-10);
let normalized_3pi = DubinsPlanner::normalize_angle(3.0 * PI);
assert!(
(normalized_3pi - PI).abs() < 1e-10 || (normalized_3pi - (-PI)).abs() < 1e-10,
"Expected ±π, got {normalized_3pi}"
);
let normalized_neg3pi = DubinsPlanner::normalize_angle(-3.0 * PI);
assert!(
(normalized_neg3pi - PI).abs() < 1e-10 || (normalized_neg3pi - (-PI)).abs() < 1e-10,
"Expected ±π, got {normalized_neg3pi}"
);
}
#[test]
fn test_dubins_invalid_turning_radius() {
let start = Pose2D::new(0.0, 0.0, 0.0);
let goal = Pose2D::new(1.0, 0.0, 0.0);
let planner = DubinsPlanner::new(-1.0);
let result = planner.plan(&start, &goal);
assert!(result.is_err());
}
}