Skip to main content

rust_robotics_planning/
visibility_road_map.rs

1#![allow(dead_code)]
2
3//! Visibility Road-Map path planning
4//!
5//! Builds a graph by connecting start, goal, and expanded obstacle vertices
6//! that have line-of-sight to each other (no obstacle edge intersections),
7//! then searches the graph with Dijkstra's algorithm.
8//!
9//! Reference: PythonRobotics VisibilityRoadMap
10
11use std::cmp::Ordering;
12use std::collections::{BinaryHeap, HashMap};
13
14// ---------------------------------------------------------------------------
15// Geometry helpers
16// ---------------------------------------------------------------------------
17
18/// A 2-D point.
19#[derive(Clone, Copy, Debug)]
20struct Point {
21    x: f64,
22    y: f64,
23}
24
25/// Orientation of an ordered triplet (p, q, r).
26/// Returns 0 if collinear, 1 if clockwise, 2 if counter-clockwise.
27fn orientation(p: Point, q: Point, r: Point) -> i32 {
28    let val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
29    if val > 0.0 {
30        1
31    } else if val < 0.0 {
32        2
33    } else {
34        0
35    }
36}
37
38/// Check whether point `q` lies on segment `pr`.
39fn on_segment(p: Point, q: Point, r: Point) -> bool {
40    q.x <= p.x.max(r.x) && q.x >= p.x.min(r.x) && q.y <= p.y.max(r.y) && q.y >= p.y.min(r.y)
41}
42
43/// Return `true` when segments (p1, q1) and (p2, q2) intersect (proper or
44/// collinear-touching).
45fn segments_intersect(p1: Point, q1: Point, p2: Point, q2: Point) -> bool {
46    let o1 = orientation(p1, q1, p2);
47    let o2 = orientation(p1, q1, q2);
48    let o3 = orientation(p2, q2, p1);
49    let o4 = orientation(p2, q2, q1);
50
51    if o1 != o2 && o3 != o4 {
52        return true;
53    }
54    if o1 == 0 && on_segment(p1, p2, q1) {
55        return true;
56    }
57    if o2 == 0 && on_segment(p1, q2, q1) {
58        return true;
59    }
60    if o3 == 0 && on_segment(p2, p1, q2) {
61        return true;
62    }
63    if o4 == 0 && on_segment(p2, q1, q2) {
64        return true;
65    }
66    false
67}
68
69// ---------------------------------------------------------------------------
70// Obstacle polygon
71// ---------------------------------------------------------------------------
72
73/// A closed polygon obstacle.
74///
75/// Vertices are stored in clockwise order with the last vertex equal to the
76/// first (closed).
77#[derive(Clone, Debug)]
78pub struct ObstaclePolygon {
79    /// x-coordinates (closed: first == last).
80    pub x_list: Vec<f64>,
81    /// y-coordinates (closed: first == last).
82    pub y_list: Vec<f64>,
83}
84
85impl ObstaclePolygon {
86    /// Create a new obstacle polygon from vertex lists.
87    ///
88    /// The polygon is automatically closed and reordered to clockwise.
89    pub fn new(mut x_list: Vec<f64>, mut y_list: Vec<f64>) -> Self {
90        assert!(
91            x_list.len() >= 3 && x_list.len() == y_list.len(),
92            "polygon must have at least 3 vertices"
93        );
94
95        // Close the polygon if not already closed.
96        let is_closed = (x_list[0] - *x_list.last().unwrap()).abs() < 1e-12
97            && (y_list[0] - *y_list.last().unwrap()).abs() < 1e-12;
98        if !is_closed {
99            x_list.push(x_list[0]);
100            y_list.push(y_list[0]);
101        }
102
103        // Ensure clockwise winding.
104        if !Self::is_clockwise(&x_list, &y_list) {
105            x_list.reverse();
106            y_list.reverse();
107        }
108
109        ObstaclePolygon { x_list, y_list }
110    }
111
112    fn is_clockwise(x: &[f64], y: &[f64]) -> bool {
113        let n = x.len();
114        let mut sum = 0.0;
115        for i in 0..n - 1 {
116            sum += (x[i + 1] - x[i]) * (y[i + 1] + y[i]);
117        }
118        sum += (x[0] - x[n - 1]) * (y[0] + y[n - 1]);
119        sum >= 0.0
120    }
121
122    /// Number of unique vertices (excluding the closing duplicate).
123    fn vertex_count(&self) -> usize {
124        self.x_list.len() - 1
125    }
126}
127
128// ---------------------------------------------------------------------------
129// Dijkstra helpers (self-contained, same pattern as voronoi_road_map)
130// ---------------------------------------------------------------------------
131
132#[derive(Clone)]
133struct Node {
134    x: f64,
135    y: f64,
136    cost: f64,
137    parent: Option<usize>,
138}
139
140impl Node {
141    fn new(x: f64, y: f64) -> Self {
142        Node {
143            x,
144            y,
145            cost: f64::INFINITY,
146            parent: None,
147        }
148    }
149}
150
151#[derive(Clone)]
152struct QueueItem {
153    cost: f64,
154    index: usize,
155}
156
157impl PartialEq for QueueItem {
158    fn eq(&self, other: &Self) -> bool {
159        self.cost == other.cost
160    }
161}
162
163impl Eq for QueueItem {}
164
165impl Ord for QueueItem {
166    fn cmp(&self, other: &Self) -> Ordering {
167        other
168            .cost
169            .partial_cmp(&self.cost)
170            .unwrap_or(Ordering::Equal)
171    }
172}
173
174impl PartialOrd for QueueItem {
175    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
176        Some(self.cmp(other))
177    }
178}
179
180/// Run Dijkstra on an adjacency-list graph, returning the path as a sequence
181/// of node indices from `start_idx` to `goal_idx`, or `None`.
182fn dijkstra_search(
183    nodes: &mut [Node],
184    road_map: &[Vec<usize>],
185    start_idx: usize,
186    goal_idx: usize,
187) -> Option<Vec<usize>> {
188    nodes[start_idx].cost = 0.0;
189
190    let mut open = BinaryHeap::new();
191    open.push(QueueItem {
192        cost: 0.0,
193        index: start_idx,
194    });
195
196    let mut closed: HashMap<usize, bool> = HashMap::new();
197
198    while let Some(cur) = open.pop() {
199        if cur.index == goal_idx {
200            // Reconstruct.
201            let mut path = Vec::new();
202            let mut idx = goal_idx;
203            loop {
204                path.push(idx);
205                match nodes[idx].parent {
206                    Some(p) => idx = p,
207                    None => break,
208                }
209            }
210            path.reverse();
211            return Some(path);
212        }
213
214        if closed.contains_key(&cur.index) {
215            continue;
216        }
217        closed.insert(cur.index, true);
218
219        for &nb in &road_map[cur.index] {
220            if closed.contains_key(&nb) {
221                continue;
222            }
223            let dx = nodes[nb].x - nodes[cur.index].x;
224            let dy = nodes[nb].y - nodes[cur.index].y;
225            let new_cost = nodes[cur.index].cost + (dx * dx + dy * dy).sqrt();
226            if new_cost < nodes[nb].cost {
227                nodes[nb].cost = new_cost;
228                nodes[nb].parent = Some(cur.index);
229                open.push(QueueItem {
230                    cost: new_cost,
231                    index: nb,
232                });
233            }
234        }
235    }
236    None
237}
238
239// ---------------------------------------------------------------------------
240// Visibility Road-Map planner
241// ---------------------------------------------------------------------------
242
243/// Visibility Road-Map planner.
244///
245/// Given polygon obstacles, the planner expands each obstacle vertex outward
246/// by `expand_distance`, connects all mutually visible vertices (including
247/// start and goal), and finds the shortest path via Dijkstra.
248pub struct VisibilityRoadMap {
249    /// Distance to expand obstacle vertices outward in configuration space.
250    expand_distance: f64,
251}
252
253impl VisibilityRoadMap {
254    /// Create a new planner.
255    pub fn new(expand_distance: f64) -> Self {
256        VisibilityRoadMap { expand_distance }
257    }
258
259    /// Plan a path from `(sx, sy)` to `(gx, gy)` avoiding `obstacles`.
260    ///
261    /// Returns the path as `(xs, ys)` vectors, or `None` if unreachable.
262    pub fn plan(
263        &self,
264        sx: f64,
265        sy: f64,
266        gx: f64,
267        gy: f64,
268        obstacles: &[ObstaclePolygon],
269    ) -> Option<(Vec<f64>, Vec<f64>)> {
270        let mut nodes = self.generate_visibility_nodes(sx, sy, gx, gy, obstacles);
271        let road_map = self.generate_road_map(&nodes, obstacles);
272
273        let path_indices = dijkstra_search(&mut nodes, &road_map, 0, 1)?;
274
275        let rx: Vec<f64> = path_indices.iter().map(|&i| nodes[i].x).collect();
276        let ry: Vec<f64> = path_indices.iter().map(|&i| nodes[i].y).collect();
277        Some((rx, ry))
278    }
279
280    // -- node generation ----------------------------------------------------
281
282    fn generate_visibility_nodes(
283        &self,
284        sx: f64,
285        sy: f64,
286        gx: f64,
287        gy: f64,
288        obstacles: &[ObstaclePolygon],
289    ) -> Vec<Node> {
290        let mut nodes = vec![Node::new(sx, sy), Node::new(gx, gy)];
291
292        for obs in obstacles {
293            let (cvx, cvy) = self.vertices_in_configuration_space(obs);
294            for (vx, vy) in cvx.into_iter().zip(cvy) {
295                nodes.push(Node::new(vx, vy));
296            }
297        }
298
299        nodes
300    }
301
302    /// Expand each obstacle vertex outward by `expand_distance`.
303    fn vertices_in_configuration_space(&self, obs: &ObstaclePolygon) -> (Vec<f64>, Vec<f64>) {
304        let n = obs.vertex_count();
305        // Work with the unique vertices (excluding closing duplicate).
306        let x = &obs.x_list[..n];
307        let y = &obs.y_list[..n];
308
309        let mut cvx = Vec::with_capacity(n);
310        let mut cvy = Vec::with_capacity(n);
311
312        for i in 0..n {
313            let prev = if i == 0 { n - 1 } else { i - 1 };
314            let next = (i + 1) % n;
315            let (ox, oy) = self.calc_offset(x[prev], y[prev], x[i], y[i], x[next], y[next]);
316            cvx.push(ox);
317            cvy.push(oy);
318        }
319
320        (cvx, cvy)
321    }
322
323    /// Compute the offset position for a vertex given its predecessor and successor.
324    fn calc_offset(&self, px: f64, py: f64, x: f64, y: f64, nx: f64, ny: f64) -> (f64, f64) {
325        let p_vec = (y - py).atan2(x - px);
326        let n_vec = (ny - y).atan2(nx - x);
327        let offset_vec = (p_vec.sin() + n_vec.sin()).atan2(p_vec.cos() + n_vec.cos())
328            + std::f64::consts::FRAC_PI_2;
329        let offset_x = x + self.expand_distance * offset_vec.cos();
330        let offset_y = y + self.expand_distance * offset_vec.sin();
331        (offset_x, offset_y)
332    }
333
334    // -- road-map generation ------------------------------------------------
335
336    fn generate_road_map(&self, nodes: &[Node], obstacles: &[ObstaclePolygon]) -> Vec<Vec<usize>> {
337        let n = nodes.len();
338        let mut road_map: Vec<Vec<usize>> = Vec::with_capacity(n);
339
340        for i in 0..n {
341            let mut neighbors = Vec::new();
342            for j in 0..n {
343                if i == j {
344                    continue;
345                }
346                let dx = nodes[i].x - nodes[j].x;
347                let dy = nodes[i].y - nodes[j].y;
348                if (dx * dx + dy * dy).sqrt() <= 0.1 {
349                    continue; // nearly coincident
350                }
351
352                if obstacles
353                    .iter()
354                    .all(|obs| Self::is_edge_valid(&nodes[i], &nodes[j], obs))
355                {
356                    neighbors.push(j);
357                }
358            }
359            road_map.push(neighbors);
360        }
361
362        road_map
363    }
364
365    /// Return `true` when the segment between two nodes does not cross any
366    /// edge of the given obstacle polygon.
367    fn is_edge_valid(a: &Node, b: &Node, obs: &ObstaclePolygon) -> bool {
368        let p1 = Point { x: a.x, y: a.y };
369        let p2 = Point { x: b.x, y: b.y };
370
371        for i in 0..obs.x_list.len() - 1 {
372            let p3 = Point {
373                x: obs.x_list[i],
374                y: obs.y_list[i],
375            };
376            let p4 = Point {
377                x: obs.x_list[i + 1],
378                y: obs.y_list[i + 1],
379            };
380            if segments_intersect(p1, p2, p3, p4) {
381                return false;
382            }
383        }
384        true
385    }
386
387    /// Return the road-map edges for visualisation purposes.
388    ///
389    /// Each entry is `((x1, y1), (x2, y2))` with `i < j` to avoid duplicates.
390    pub fn get_edges(
391        &self,
392        sx: f64,
393        sy: f64,
394        gx: f64,
395        gy: f64,
396        obstacles: &[ObstaclePolygon],
397    ) -> Vec<((f64, f64), (f64, f64))> {
398        let nodes = self.generate_visibility_nodes(sx, sy, gx, gy, obstacles);
399        let road_map = self.generate_road_map(&nodes, obstacles);
400
401        let mut edges = Vec::new();
402        for (i, neighbors) in road_map.iter().enumerate() {
403            for &j in neighbors {
404                if i < j {
405                    edges.push(((nodes[i].x, nodes[i].y), (nodes[j].x, nodes[j].y)));
406                }
407            }
408        }
409        edges
410    }
411}
412
413// ---------------------------------------------------------------------------
414// Tests
415// ---------------------------------------------------------------------------
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    /// Build the same obstacle set used in the Python reference `main()`.
422    fn make_obstacles() -> Vec<ObstaclePolygon> {
423        vec![
424            ObstaclePolygon::new(vec![20.0, 30.0, 15.0], vec![20.0, 20.0, 30.0]),
425            ObstaclePolygon::new(vec![40.0, 45.0, 50.0, 40.0], vec![50.0, 40.0, 20.0, 40.0]),
426            ObstaclePolygon::new(vec![20.0, 30.0, 30.0, 20.0], vec![40.0, 45.0, 60.0, 50.0]),
427        ]
428    }
429
430    // -- geometry -----------------------------------------------------------
431
432    #[test]
433    fn test_segments_intersect_crossing() {
434        let p1 = Point { x: 0.0, y: 0.0 };
435        let q1 = Point { x: 10.0, y: 10.0 };
436        let p2 = Point { x: 10.0, y: 0.0 };
437        let q2 = Point { x: 0.0, y: 10.0 };
438        assert!(segments_intersect(p1, q1, p2, q2));
439    }
440
441    #[test]
442    fn test_segments_no_intersect() {
443        let p1 = Point { x: 0.0, y: 0.0 };
444        let q1 = Point { x: 5.0, y: 0.0 };
445        let p2 = Point { x: 0.0, y: 1.0 };
446        let q2 = Point { x: 5.0, y: 1.0 };
447        assert!(!segments_intersect(p1, q1, p2, q2));
448    }
449
450    #[test]
451    fn test_segments_collinear_overlap() {
452        let p1 = Point { x: 0.0, y: 0.0 };
453        let q1 = Point { x: 5.0, y: 0.0 };
454        let p2 = Point { x: 3.0, y: 0.0 };
455        let q2 = Point { x: 7.0, y: 0.0 };
456        assert!(segments_intersect(p1, q1, p2, q2));
457    }
458
459    // -- obstacle polygon ---------------------------------------------------
460
461    #[test]
462    fn test_polygon_closes_and_clockwise() {
463        let obs = ObstaclePolygon::new(vec![0.0, 1.0, 0.5], vec![0.0, 0.0, 1.0]);
464        // Closed: first == last.
465        assert_eq!(obs.x_list.first(), obs.x_list.last());
466        assert_eq!(obs.y_list.first(), obs.y_list.last());
467        // Clockwise check.
468        assert!(ObstaclePolygon::is_clockwise(&obs.x_list, &obs.y_list));
469    }
470
471    #[test]
472    fn test_polygon_vertex_count() {
473        let obs = ObstaclePolygon::new(vec![0.0, 4.0, 4.0, 0.0], vec![0.0, 0.0, 4.0, 4.0]);
474        assert_eq!(obs.vertex_count(), 4);
475    }
476
477    // -- configuration-space expansion --------------------------------------
478
479    #[test]
480    fn test_configuration_space_vertices_count() {
481        let planner = VisibilityRoadMap::new(5.0);
482        let obs = ObstaclePolygon::new(vec![20.0, 30.0, 15.0], vec![20.0, 20.0, 30.0]);
483        let (cvx, cvy) = planner.vertices_in_configuration_space(&obs);
484        // One expanded vertex per original vertex.
485        assert_eq!(cvx.len(), obs.vertex_count());
486        assert_eq!(cvy.len(), obs.vertex_count());
487    }
488
489    #[test]
490    fn test_expanded_vertices_are_further_from_centroid() {
491        let planner = VisibilityRoadMap::new(5.0);
492        let obs = ObstaclePolygon::new(vec![0.0, 10.0, 5.0], vec![0.0, 0.0, 8.66]);
493        let (cvx, cvy) = planner.vertices_in_configuration_space(&obs);
494
495        let cx: f64 =
496            obs.x_list[..obs.vertex_count()].iter().sum::<f64>() / obs.vertex_count() as f64;
497        let cy: f64 =
498            obs.y_list[..obs.vertex_count()].iter().sum::<f64>() / obs.vertex_count() as f64;
499
500        for i in 0..obs.vertex_count() {
501            let orig_dist = ((obs.x_list[i] - cx).powi(2) + (obs.y_list[i] - cy).powi(2)).sqrt();
502            let exp_dist = ((cvx[i] - cx).powi(2) + (cvy[i] - cy).powi(2)).sqrt();
503            assert!(
504                exp_dist > orig_dist,
505                "expanded vertex {i} should be further from centroid"
506            );
507        }
508    }
509
510    // -- edge validity ------------------------------------------------------
511
512    #[test]
513    fn test_edge_valid_no_obstacle_crossing() {
514        let a = Node::new(0.0, 0.0);
515        let b = Node::new(10.0, 0.0);
516        let obs = ObstaclePolygon::new(vec![0.0, 5.0, 5.0, 0.0], vec![5.0, 5.0, 10.0, 10.0]);
517        assert!(VisibilityRoadMap::is_edge_valid(&a, &b, &obs));
518    }
519
520    #[test]
521    fn test_edge_invalid_crosses_obstacle() {
522        let a = Node::new(0.0, 0.0);
523        let b = Node::new(10.0, 10.0);
524        let obs = ObstaclePolygon::new(vec![3.0, 7.0, 5.0], vec![0.0, 0.0, 10.0]);
525        assert!(!VisibilityRoadMap::is_edge_valid(&a, &b, &obs));
526    }
527
528    // -- full planning ------------------------------------------------------
529
530    #[test]
531    fn test_plan_finds_path() {
532        let planner = VisibilityRoadMap::new(5.0);
533        let obstacles = make_obstacles();
534        let result = planner.plan(10.0, 10.0, 50.0, 50.0, &obstacles);
535        assert!(result.is_some(), "planner should find a path");
536
537        let (rx, ry) = result.unwrap();
538        assert!(rx.len() >= 2);
539        assert_eq!(rx.len(), ry.len());
540
541        // Path starts at start.
542        assert!((rx[0] - 10.0).abs() < 1e-9);
543        assert!((ry[0] - 10.0).abs() < 1e-9);
544
545        // Path ends at goal.
546        assert!((*rx.last().unwrap() - 50.0).abs() < 1e-9);
547        assert!((*ry.last().unwrap() - 50.0).abs() < 1e-9);
548    }
549
550    #[test]
551    fn test_plan_with_no_obstacles() {
552        let planner = VisibilityRoadMap::new(5.0);
553        let result = planner.plan(0.0, 0.0, 10.0, 10.0, &[]);
554        assert!(result.is_some());
555
556        let (rx, ry) = result.unwrap();
557        // Direct line: start -> goal.
558        assert_eq!(rx.len(), 2);
559        assert!((rx[0] - 0.0).abs() < 1e-9);
560        assert!((*rx.last().unwrap() - 10.0).abs() < 1e-9);
561        assert!((ry[0] - 0.0).abs() < 1e-9);
562        assert!((*ry.last().unwrap() - 10.0).abs() < 1e-9);
563    }
564
565    #[test]
566    fn test_get_edges_nonempty() {
567        let planner = VisibilityRoadMap::new(5.0);
568        let obstacles = make_obstacles();
569        let edges = planner.get_edges(10.0, 10.0, 50.0, 50.0, &obstacles);
570        assert!(!edges.is_empty(), "road-map should have visibility edges");
571    }
572
573    #[test]
574    fn test_plan_path_cost_reasonable() {
575        let planner = VisibilityRoadMap::new(5.0);
576        let obstacles = make_obstacles();
577        let (rx, ry) = planner.plan(10.0, 10.0, 50.0, 50.0, &obstacles).unwrap();
578
579        // Compute path length.
580        let mut length = 0.0;
581        for i in 1..rx.len() {
582            length += ((rx[i] - rx[i - 1]).powi(2) + (ry[i] - ry[i - 1]).powi(2)).sqrt();
583        }
584
585        let straight = ((50.0 - 10.0_f64).powi(2) + (50.0 - 10.0_f64).powi(2)).sqrt();
586        // Path should be longer than straight-line (obstacles in the way)
587        // but not absurdly long.
588        assert!(
589            length >= straight,
590            "path cannot be shorter than straight line"
591        );
592        assert!(
593            length < straight * 3.0,
594            "path length {length:.1} seems excessively long (straight={straight:.1})"
595        );
596    }
597}