1use crate::error::{SpatialError, SpatialResult};
33use std::f64::consts::PI;
34
35#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct Pose2D {
38 pub x: f64,
40 pub y: f64,
42 pub theta: f64,
44}
45
46impl Pose2D {
47 pub fn new(x: f64, y: f64, theta: f64) -> Self {
59 Self { x, y, theta }
60 }
61
62 pub fn distance_to(&self, other: &Pose2D) -> f64 {
72 ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
73 }
74
75 pub fn normalize_angle(&self) -> Self {
81 let mut normalized_theta = self.theta % (2.0 * PI);
82 if normalized_theta > PI {
83 normalized_theta -= 2.0 * PI;
84 } else if normalized_theta < -PI {
85 normalized_theta += 2.0 * PI;
86 }
87 Self {
88 x: self.x,
89 y: self.y,
90 theta: normalized_theta,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum DubinsPathType {
98 LSL,
100 LSR,
102 RSL,
104 RSR,
106 LRL,
108 RLR,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum SegmentType {
115 Left,
117 Right,
119 Straight,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq)]
125pub struct DubinsSegment {
126 pub segment_type: SegmentType,
128 pub length: f64,
130}
131
132#[derive(Debug, Clone, PartialEq)]
134pub struct DubinsPath {
135 start: Pose2D,
137 goal: Pose2D,
139 turning_radius: f64,
141 path_type: DubinsPathType,
143 segments: [DubinsSegment; 3],
145 length: f64,
147}
148
149impl DubinsPath {
150 fn new(
152 start: Pose2D,
153 goal: Pose2D,
154 turning_radius: f64,
155 path_type: DubinsPathType,
156 segments: [DubinsSegment; 3],
157 ) -> Self {
158 let length = segments.iter().map(|s| s.length).sum();
159 Self {
160 start,
161 goal,
162 turning_radius,
163 path_type,
164 segments,
165 length,
166 }
167 }
168
169 pub fn length(&self) -> f64 {
175 self.length
176 }
177
178 pub fn path_type(&self) -> DubinsPathType {
184 self.path_type
185 }
186
187 pub fn segments(&self) -> &[DubinsSegment; 3] {
193 &self.segments
194 }
195
196 pub fn start(&self) -> &Pose2D {
202 &self.start
203 }
204
205 pub fn goal(&self) -> &Pose2D {
211 &self.goal
212 }
213
214 pub fn turning_radius(&self) -> f64 {
220 self.turning_radius
221 }
222
223 pub fn sample(&self, t: f64) -> SpatialResult<Pose2D> {
233 if !(0.0..=1.0).contains(&t) {
234 return Err(SpatialError::ValueError(
235 "Parameter t must be in [0, 1]".to_string(),
236 ));
237 }
238
239 let target_distance = t * self.length;
240 let mut current_distance = 0.0;
241 let mut current_pose = self.start;
242
243 for segment in &self.segments {
244 if current_distance + segment.length >= target_distance {
245 let segment_distance = target_distance - current_distance;
247 let segment_t = if segment.length > 0.0 {
248 segment_distance / segment.length
249 } else {
250 0.0
251 };
252
253 return self.sample_segment(¤t_pose, segment, segment_t);
254 }
255
256 current_pose = self.sample_segment(¤t_pose, segment, 1.0)?;
258 current_distance += segment.length;
259 }
260
261 Ok(self.goal)
262 }
263
264 fn sample_segment(
266 &self,
267 start_pose: &Pose2D,
268 segment: &DubinsSegment,
269 t: f64,
270 ) -> SpatialResult<Pose2D> {
271 match segment.segment_type {
272 SegmentType::Straight => {
273 let distance = t * segment.length;
274 let new_x = start_pose.x + distance * start_pose.theta.cos();
275 let new_y = start_pose.y + distance * start_pose.theta.sin();
276 Ok(Pose2D::new(new_x, new_y, start_pose.theta))
277 }
278 SegmentType::Left => {
279 let angle = t * segment.length / self.turning_radius;
280 let center_x = start_pose.x - self.turning_radius * start_pose.theta.sin();
281 let center_y = start_pose.y + self.turning_radius * start_pose.theta.cos();
282 let new_theta = start_pose.theta + angle;
283 let new_x = center_x + self.turning_radius * new_theta.sin();
284 let new_y = center_y - self.turning_radius * new_theta.cos();
285 Ok(Pose2D::new(new_x, new_y, new_theta))
286 }
287 SegmentType::Right => {
288 let angle = t * segment.length / self.turning_radius;
289 let center_x = start_pose.x + self.turning_radius * start_pose.theta.sin();
290 let center_y = start_pose.y - self.turning_radius * start_pose.theta.cos();
291 let new_theta = start_pose.theta - angle;
292 let new_x = center_x - self.turning_radius * new_theta.sin();
293 let new_y = center_y + self.turning_radius * new_theta.cos();
294 Ok(Pose2D::new(new_x, new_y, new_theta))
295 }
296 }
297 }
298}
299
300pub struct DubinsPlanner {
302 turning_radius: f64,
304}
305
306impl DubinsPlanner {
307 pub fn new(_turningradius: f64) -> Self {
325 Self {
326 turning_radius: _turningradius,
327 }
328 }
329
330 pub fn plan(&self, start: &Pose2D, goal: &Pose2D) -> SpatialResult<DubinsPath> {
354 if self.turning_radius <= 0.0 {
355 return Err(SpatialError::ValueError(
356 "Turning radius must be positive".to_string(),
357 ));
358 }
359
360 let _start = start.normalize_angle();
362 let goal = goal.normalize_angle();
363
364 let dx = goal.x - start.x;
366 let dy = goal.y - start.y;
367 let d = (dx * dx + dy * dy).sqrt();
368 let theta = (dy).atan2(dx);
369
370 let alpha = Self::normalize_angle(_start.theta - theta);
371 let beta = Self::normalize_angle(goal.theta - theta);
372
373 let mut best_path = None;
375 let mut best_length = f64::INFINITY;
376
377 for path_type in [
378 DubinsPathType::LSL,
379 DubinsPathType::LSR,
380 DubinsPathType::RSL,
381 DubinsPathType::RSR,
382 DubinsPathType::LRL,
383 DubinsPathType::RLR,
384 ] {
385 if let Ok(segments) = self.compute_path_segments(d, alpha, beta, path_type) {
386 let path_length: f64 = segments.iter().map(|s| s.length).sum();
387 if path_length < best_length {
388 best_length = path_length;
389 best_path = Some(DubinsPath::new(
390 start.clone(),
391 goal.clone(),
392 self.turning_radius,
393 path_type,
394 segments,
395 ));
396 }
397 }
398 }
399
400 best_path.ok_or_else(|| {
401 SpatialError::ComputationError("Failed to compute any valid Dubins path".to_string())
402 })
403 }
404
405 fn compute_path_segments(
407 &self,
408 d: f64,
409 alpha: f64,
410 beta: f64,
411 path_type: DubinsPathType,
412 ) -> SpatialResult<[DubinsSegment; 3]> {
413 let d_norm = d / self.turning_radius;
414
415 match path_type {
416 DubinsPathType::LSL => self.lsl(d_norm, alpha, beta),
417 DubinsPathType::LSR => self.lsr(d_norm, alpha, beta),
418 DubinsPathType::RSL => self.rsl(d_norm, alpha, beta),
419 DubinsPathType::RSR => self.rsr(d_norm, alpha, beta),
420 DubinsPathType::LRL => self.lrl(d_norm, alpha, beta),
421 DubinsPathType::RLR => self.rlr(d_norm, alpha, beta),
422 }
423 }
424
425 fn lsl(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
427 let tmp0 = d + alpha.sin() - beta.sin();
428 let p_squared =
429 2.0 + d * d - 2.0 * (alpha - beta).cos() + 2.0 * d * (alpha.sin() - beta.sin());
430
431 if p_squared < 0.0 {
432 return Err(SpatialError::ComputationError(
433 "Invalid LSL path".to_string(),
434 ));
435 }
436
437 let tmp1 = (beta - alpha).atan2(tmp0);
438 let t = Self::normalize_angle(-alpha + tmp1);
439 let p = p_squared.sqrt();
440 let q = Self::normalize_angle(beta - tmp1);
441
442 Ok([
443 DubinsSegment {
444 segment_type: SegmentType::Left,
445 length: t * self.turning_radius,
446 },
447 DubinsSegment {
448 segment_type: SegmentType::Straight,
449 length: p * self.turning_radius,
450 },
451 DubinsSegment {
452 segment_type: SegmentType::Left,
453 length: q * self.turning_radius,
454 },
455 ])
456 }
457
458 fn lsr(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
460 let p_squared =
461 -2.0 + d * d + 2.0 * (alpha - beta).cos() + 2.0 * d * (alpha.sin() + beta.sin());
462
463 if p_squared < 0.0 {
464 return Err(SpatialError::ComputationError(
465 "Invalid LSR path".to_string(),
466 ));
467 }
468
469 let p = p_squared.sqrt();
470 let tmp2 = ((-alpha - beta).cos() + d).atan2((-alpha - beta).sin() + p);
471 let t = Self::normalize_angle(-alpha + tmp2);
472 let q = Self::normalize_angle(-beta + tmp2);
473
474 Ok([
475 DubinsSegment {
476 segment_type: SegmentType::Left,
477 length: t * self.turning_radius,
478 },
479 DubinsSegment {
480 segment_type: SegmentType::Straight,
481 length: p * self.turning_radius,
482 },
483 DubinsSegment {
484 segment_type: SegmentType::Right,
485 length: q * self.turning_radius,
486 },
487 ])
488 }
489
490 fn rsl(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
492 let p_squared =
493 d * d - 2.0 + 2.0 * (alpha - beta).cos() - 2.0 * d * (alpha.sin() + beta.sin());
494
495 if p_squared < 0.0 {
496 return Err(SpatialError::ComputationError(
497 "Invalid RSL path".to_string(),
498 ));
499 }
500
501 let p = p_squared.sqrt();
502 let tmp2 = ((alpha + beta).cos() - d).atan2((alpha + beta).sin() - p);
503 let t = Self::normalize_angle(alpha - tmp2);
504 let q = Self::normalize_angle(beta - tmp2);
505
506 Ok([
507 DubinsSegment {
508 segment_type: SegmentType::Right,
509 length: t * self.turning_radius,
510 },
511 DubinsSegment {
512 segment_type: SegmentType::Straight,
513 length: p * self.turning_radius,
514 },
515 DubinsSegment {
516 segment_type: SegmentType::Left,
517 length: q * self.turning_radius,
518 },
519 ])
520 }
521
522 fn rsr(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
524 let tmp0 = d - alpha.sin() + beta.sin();
525 let p_squared =
526 2.0 + d * d - 2.0 * (alpha - beta).cos() - 2.0 * d * (alpha.sin() - beta.sin());
527
528 if p_squared < 0.0 {
529 return Err(SpatialError::ComputationError(
530 "Invalid RSR path".to_string(),
531 ));
532 }
533
534 let tmp1 = (alpha - beta).atan2(tmp0);
535 let t = Self::normalize_angle(alpha - tmp1);
536 let p = p_squared.sqrt();
537 let q = Self::normalize_angle(-beta + tmp1);
538
539 Ok([
540 DubinsSegment {
541 segment_type: SegmentType::Right,
542 length: t * self.turning_radius,
543 },
544 DubinsSegment {
545 segment_type: SegmentType::Straight,
546 length: p * self.turning_radius,
547 },
548 DubinsSegment {
549 segment_type: SegmentType::Right,
550 length: q * self.turning_radius,
551 },
552 ])
553 }
554
555 fn lrl(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
557 let tmp0 =
558 (6.0 - d * d + 2.0 * (alpha - beta).cos() + 2.0 * d * (alpha.sin() - beta.sin())) / 8.0;
559
560 if tmp0.abs() > 1.0 {
561 return Err(SpatialError::ComputationError(
562 "Invalid LRL path".to_string(),
563 ));
564 }
565
566 let p = (2.0 * PI - tmp0.acos()).abs();
567 let t = Self::normalize_angle(
568 -alpha + (tmp0 - (alpha - beta).cos()).atan2(d + alpha.sin() - beta.sin()),
569 );
570 let q = Self::normalize_angle(-Self::normalize_angle(p / 2.0) + beta - alpha + t);
571
572 Ok([
573 DubinsSegment {
574 segment_type: SegmentType::Left,
575 length: t * self.turning_radius,
576 },
577 DubinsSegment {
578 segment_type: SegmentType::Right,
579 length: p * self.turning_radius,
580 },
581 DubinsSegment {
582 segment_type: SegmentType::Left,
583 length: q * self.turning_radius,
584 },
585 ])
586 }
587
588 fn rlr(&self, d: f64, alpha: f64, beta: f64) -> SpatialResult<[DubinsSegment; 3]> {
590 let tmp0 =
591 (6.0 - d * d + 2.0 * (alpha - beta).cos() - 2.0 * d * (alpha.sin() - beta.sin())) / 8.0;
592
593 if tmp0.abs() > 1.0 {
594 return Err(SpatialError::ComputationError(
595 "Invalid RLR path".to_string(),
596 ));
597 }
598
599 let p = (2.0 * PI - tmp0.acos()).abs();
600 let t = Self::normalize_angle(
601 alpha - (tmp0 - (alpha - beta).cos()).atan2(d - alpha.sin() + beta.sin()),
602 );
603 let q = Self::normalize_angle(Self::normalize_angle(p / 2.0) - beta + alpha - t);
604
605 Ok([
606 DubinsSegment {
607 segment_type: SegmentType::Right,
608 length: t * self.turning_radius,
609 },
610 DubinsSegment {
611 segment_type: SegmentType::Left,
612 length: p * self.turning_radius,
613 },
614 DubinsSegment {
615 segment_type: SegmentType::Right,
616 length: q * self.turning_radius,
617 },
618 ])
619 }
620
621 fn normalize_angle(angle: f64) -> f64 {
623 let mut normalized = angle % (2.0 * PI);
624 if normalized > PI {
625 normalized -= 2.0 * PI;
626 } else if normalized < -PI {
627 normalized += 2.0 * PI;
628 }
629 normalized
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use approx::assert_relative_eq;
637
638 #[test]
639 fn test_pose2d_basic() {
640 let pose = Pose2D::new(1.0, 2.0, PI / 4.0);
641 assert_eq!(pose.x, 1.0);
642 assert_eq!(pose.y, 2.0);
643 assert_eq!(pose.theta, PI / 4.0);
644 }
645
646 #[test]
647 fn test_pose2d_distance() {
648 let pose1 = Pose2D::new(0.0, 0.0, 0.0);
649 let pose2 = Pose2D::new(3.0, 4.0, 0.0);
650 assert_relative_eq!(pose1.distance_to(&pose2), 5.0, epsilon = 1e-10);
651 }
652
653 #[test]
654 fn test_pose2d_normalize_angle() {
655 let pose = Pose2D::new(0.0, 0.0, 3.0 * PI);
656 let normalized = pose.normalize_angle();
657 assert!(
659 (normalized.theta - PI).abs() < 1e-10 || (normalized.theta - (-PI)).abs() < 1e-10,
660 "Expected angle to be ±π, got {}",
661 normalized.theta
662 );
663 }
664
665 #[test]
666 fn test_dubins_planner_creation() {
667 let planner = DubinsPlanner::new(1.0);
668 assert_eq!(planner.turning_radius, 1.0);
669 }
670
671 #[test]
672 fn test_dubins_straight_line() {
673 let start = Pose2D::new(0.0, 0.0, 0.0);
674 let goal = Pose2D::new(5.0, 0.0, 0.0);
675 let planner = DubinsPlanner::new(1.0);
676
677 let path = planner.plan(&start, &goal).unwrap();
678
679 assert!(path.length() >= 5.0);
681 assert!(path.length() < 6.0); }
683
684 #[test]
685 fn test_dubins_path_sampling() {
686 let start = Pose2D::new(0.0, 0.0, 0.0);
687 let goal = Pose2D::new(2.0, 0.0, 0.0);
688 let planner = DubinsPlanner::new(1.0);
689
690 let path = planner.plan(&start, &goal).unwrap();
691
692 let start_sample = path.sample(0.0).unwrap();
694 assert_relative_eq!(start_sample.x, start.x, epsilon = 1e-10);
695 assert_relative_eq!(start_sample.y, start.y, epsilon = 1e-10);
696
697 let goal_sample = path.sample(1.0).unwrap();
699 assert_relative_eq!(goal_sample.x, goal.x, epsilon = 1e-2);
700 assert_relative_eq!(goal_sample.y, goal.y, epsilon = 1e-2);
701 }
702
703 #[test]
704 fn test_dubins_path_invalid_parameter() {
705 let start = Pose2D::new(0.0, 0.0, 0.0);
706 let goal = Pose2D::new(2.0, 0.0, 0.0);
707 let planner = DubinsPlanner::new(1.0);
708
709 let path = planner.plan(&start, &goal).unwrap();
710
711 assert!(path.sample(-0.1).is_err());
713 assert!(path.sample(1.1).is_err());
714 }
715
716 #[test]
717 fn test_dubins_path_types() {
718 let start = Pose2D::new(0.0, 0.0, 0.0);
719 let planner = DubinsPlanner::new(1.0);
720
721 let goals = [
723 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), ];
727
728 for goal in &goals {
729 let path_result = planner.plan(&start, goal);
730 if let Ok(path) = path_result {
731 assert!(
732 path.length() > 0.0,
733 "Path length should be positive for goal {goal:?}"
734 );
735 assert_eq!(path.segments().len(), 3);
736 } else {
737 println!("No valid path found for goal {goal:?}");
740 }
741 }
742 }
743
744 #[test]
745 fn test_normalize_angle() {
746 assert_relative_eq!(DubinsPlanner::normalize_angle(0.0), 0.0, epsilon = 1e-10);
747 assert_relative_eq!(DubinsPlanner::normalize_angle(PI), PI, epsilon = 1e-10);
748 assert_relative_eq!(DubinsPlanner::normalize_angle(-PI), -PI, epsilon = 1e-10);
749
750 let normalized_3pi = DubinsPlanner::normalize_angle(3.0 * PI);
752 assert!(
753 (normalized_3pi - PI).abs() < 1e-10 || (normalized_3pi - (-PI)).abs() < 1e-10,
754 "Expected ±π, got {normalized_3pi}"
755 );
756
757 let normalized_neg3pi = DubinsPlanner::normalize_angle(-3.0 * PI);
758 assert!(
759 (normalized_neg3pi - PI).abs() < 1e-10 || (normalized_neg3pi - (-PI)).abs() < 1e-10,
760 "Expected ±π, got {normalized_neg3pi}"
761 );
762 }
763
764 #[test]
765 fn test_dubins_invalid_turning_radius() {
766 let start = Pose2D::new(0.0, 0.0, 0.0);
767 let goal = Pose2D::new(1.0, 0.0, 0.0);
768 let planner = DubinsPlanner::new(-1.0);
769
770 let result = planner.plan(&start, &goal);
771 assert!(result.is_err());
772 }
773}