alg-grid 0.1.1

Algorithms for pathfinding in a 2D or 3D grid.
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use core::ops::Add;
use core::cmp::{max, min};
use heapless::{
    Vec,
    binary_heap::{BinaryHeap, Max},
    spsc::Queue,
    consts::*
};
use map_vec::Map;
use num_rational::BigRational;

/// Representation of a point in two dimensions
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Point2D {
    pub x: i32,
    pub y: i32,
}

impl Default for Point2D {
    fn default() -> Self {
        Self {
            x: 0,
            y: 0,
        }
    }
}

impl Add for Point2D {
    type Output = Self;
    fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

/// Trait for implementing a Grid in two dimensions
pub trait Grid2D {
    /// The dimensions of the Grid. The only method that must be defined.
    fn dimensions(&self) -> Point2D;

    /// The lower bound. Defaults to (0, 0).
    fn lower_bound(&self) -> Point2D {
        Default::default()
    }

    /// The upper bound. Defaults to the dimensions itself.
    fn upper_bound(&self) -> Point2D {
        let dim = self.dimensions();
        let low = self.lower_bound();
        Point2D {
            x: low.x + dim.x,
            y: low.y + dim.y,
        }
    }

    /// Check if a point is in the bounds of the grid.
    fn in_bounds(&self, point: Point2D) -> bool {
        let low = self.lower_bound();
        let upp = self.upper_bound();
        low.x <= point.x && point.x < upp.x &&
        low.y <= point.y && point.y < upp.y
    }

    /// Convert a point to an index.
    /// 
    /// Useful if you store the grid in a one-dimensional array.
    fn point2d_to_index(&self, point: Point2D) -> usize {
        let dim = self.dimensions();
        (point.y * dim.x + point.x) as usize
    }

    /// Convert an index to a point.
    /// 
    /// Useful if you store the grid in a one-dimensional array.
    fn index_to_point2d(&self, index: usize) -> Point2D {
        let dim = self.dimensions();
        let x = index as i32 % dim.x;
        let y = index as i32 / dim.x;
        Point2D {
            x,
            y,
        }
    }

    /// Check if a point is traversable.
    /// 
    /// Defaults to always, so you may want to implement
    /// this.
    #[allow(unused_variables)]
    fn is_opaque(&self, point: Point2D) -> bool {
        true
    }

    /// Get all possible neighbors of the point, regardless if the
    /// point or its neighbors is in bounds, opaque, or neither.
    fn get_possible_neighbors(&self, point: Point2D) -> [Point2D; 8] {
        [
            point + Point2D {
                x: 0,
                y: 1,
            },
            point + Point2D {
                x: 1,
                y: 0,
            },
            point + Point2D {
                x: 0,
                y: -1,
            },
            point + Point2D {
                x: -1,
                y: 0,
            },
            point + Point2D {
                x: 1,
                y: 1,
            },
            point + Point2D {
                x: 1,
                y: -1,
            },
            point + Point2D {
                x: -1,
                y: -1,
            },
            point + Point2D {
                x: -1,
                y: 1,
            },
        ]
    }

    /// Check if two points are possible neighbors.
    /// 
    /// Does not check if either points are inbounds or non-opaque.
    fn is_possible_neighbor(&self, p1: Point2D, p2: Point2D) -> bool {
        self.get_possible_neighbors(p1)
            .iter()
            .any(|&x| x == p2)
    }

    /// Get the neighbors that is in bounds and not opaque.
    fn get_neighbors(&self, point: Point2D) -> [Option<Point2D>; 8] {
        let mut arr: [Option<Point2D>; 8] = [None; 8];
        let possible_neighbors = self.get_possible_neighbors(point);
        for (i, n) in possible_neighbors.iter().enumerate() {
            if !self.is_opaque(*n) && self.in_bounds(*n) {
                arr[i] = Some(*n);
            }
        }
        arr
    }

    /// Check if two points are neighbors.
    /// 
    /// Checks if either points are inbounds or non-opaque.
    fn is_neighbor(&self, p1: Point2D, p2: Point2D) -> bool {
        self.get_neighbors(p1)
            .iter()
            .any(|&x| x == Some(p2))
    }

    /// Get the neighbors with the associated cost.
    /// 
    /// Defaults to all eight neighbors having a cost of 1.0
    /// if the neighbor is valid.
    /// 
    /// If you want the diagonals to cost sqrt(2), reimplement
    /// this method yourself.
    fn get_neighbors_with_cost(&self, point: Point2D) -> [(Option<Point2D>, BigRational); 8] {
        let mut arr: [(Option<Point2D>, BigRational); 8] = [
            (None, BigRational::from_float(0.0).unwrap()),
            (None, BigRational::from_float(0.0).unwrap()),
            (None, BigRational::from_float(0.0).unwrap()),
            (None, BigRational::from_float(0.0).unwrap()),
            (None, BigRational::from_float(0.0).unwrap()),
            (None, BigRational::from_float(0.0).unwrap()),
            (None, BigRational::from_float(0.0).unwrap()),
            (None, BigRational::from_float(0.0).unwrap()),
        ];
        let neighbors = self.get_neighbors(point);
        for (i, n) in neighbors.iter().enumerate() {
            let one = BigRational::from_float(1.0).unwrap();
            if n != &None {
                arr[i] = (*n, one);
            }
        }
        arr
    }
}

/// Algorithms to get the distance of two points.
pub enum Distance2D {
    Pythagoras,
    Manhattan,
    Chebyshev,
}

impl Distance2D {
    pub fn distance(&self, start: Point2D, end: Point2D) -> f64 {
        use Distance2D::*;
        match self {
            Pythagoras => {
                let dx = (max(start.x, end.x) - min(start.x, end.x)) as f64;
                let dy = (max(start.y, end.y) - min(start.y, end.y)) as f64;
                libm::sqrt((dx * dx) + (dy * dy))
            }
            Manhattan => {
                let dx = (max(start.x, end.x) - min(start.x, end.x)) as f64;
                let dy = (max(start.y, end.y) - min(start.y, end.y)) as f64;
                dx + dy
            }
            Chebyshev => {
                let dx = (max(start.x, end.x) - min(start.x, end.x)) as f64;
                let dy = (max(start.y, end.y) - min(start.y, end.y)) as f64;
                if dx > dy {
                    (dx - dy) + 1.0 * dy
                } else {
                    (dy - dx) + 1.0 * dx
                }
            }
        }
    }
}

pub fn breadth_first_search(graph: &dyn Grid2D, start: Point2D, goal: Point2D) -> Map<Point2D, Option<Point2D>> {
    let mut frontier: Queue<Point2D, U16> = Queue::new();
    frontier.enqueue(start).expect("Failed to enqueue");
    let mut came_from = Map::<Point2D, Option<Point2D>>::new();
    came_from.insert(start, None);
    while !frontier.is_empty() {
        let current = frontier.dequeue().unwrap();
        if current == goal { break }
        for next in &graph.get_neighbors(current) {
            let next = next.unwrap();
            if !came_from.contains_key(&next) {
                frontier.enqueue(next).expect("Failed to enqueue");
                came_from.insert(next, Some(current));
            }
        }
    }
    came_from
}

pub fn dijkstra_search(
    graph: &dyn Grid2D,
    start: Point2D,
    goal: Point2D
) -> (
    Map<Point2D, Option<Point2D>>,
    Map<Point2D, BigRational>,
) {
    let mut frontier: BinaryHeap<(BigRational, Point2D), U16, Max> = BinaryHeap::new();
    
    let zero1 = BigRational::from_float(0.0).unwrap();
    let zero2 = BigRational::from_float(0.0).unwrap();
    
    frontier.push((zero1, start)).expect("fail to push to heap");
    let mut came_from = Map::<Point2D, Option<Point2D>>::new();
    let mut cost_so_far = Map::<Point2D, BigRational>::new();
    
    came_from.insert(start, None);
    cost_so_far.insert(start, zero2);
    
    while !frontier.is_empty() {
        let current = frontier.pop().unwrap();
        let point = current.1;
        
        if current.1 == goal { break }
        
        for (next, cost) in &graph.get_neighbors_with_cost(point) {
            let next = next.unwrap();
            let new_cost1 = cost_so_far.get(&point).unwrap() + cost;
            let new_cost2 = cost_so_far.get(&point).unwrap() + cost;
            
            if !cost_so_far.contains_key(&next) || new_cost1 < *cost_so_far.get(&next).unwrap() {
                cost_so_far.insert(next, new_cost1);
                
                let priority = new_cost2;
                
                frontier.push((priority, next)).expect("fail to push to heap");
                came_from.insert(next, Some(point));
            }
        }
    }
    (came_from, cost_so_far)
}

pub fn a_star_search(
    graph: &dyn Grid2D,
    start: Point2D,
    goal: Point2D
) -> (
    Map<Point2D, Option<Point2D>>,
    Map<Point2D, BigRational>,
) {
    use num_bigint::ToBigInt;
    
    let mut frontier: BinaryHeap<(BigRational, Point2D), U16, Max> = BinaryHeap::new();
    
    let zero1 = BigRational::from_float(0.0).unwrap();
    let zero2 = BigRational::from_float(0.0).unwrap();
    
    frontier.push((zero1, start)).expect("fail to push to heap");
    
    let mut came_from = Map::<Point2D, Option<Point2D>>::new();
    let mut cost_so_far = Map::<Point2D, BigRational>::new();
    
    came_from.insert(start, None);
    cost_so_far.insert(start, zero2);
    
    while !frontier.is_empty() {
        let current = frontier.pop().unwrap();
        let point = current.1;
        
        if current.1 == goal { break }
        
        for (next, cost) in &graph.get_neighbors_with_cost(point) {
            let next = next.unwrap();
            
            let new_cost1 = cost_so_far.get(&point).unwrap() + cost;
            let new_cost2 = cost_so_far.get(&point).unwrap() + cost;
            
            if !cost_so_far.contains_key(&next) || new_cost1 < *cost_so_far.get(&next).unwrap() {
                cost_so_far.insert(next, new_cost1);
                
                let h = BigRational::from_integer(
                    heuristic(goal, next).to_bigint().unwrap()
                );
                let priority = new_cost2 + h;
                
                frontier.push((priority, next)).expect("fail to push to heap");
                came_from.insert(next, Some(point));
            }
        }
    }
    (came_from, cost_so_far)
}


fn heuristic(p1: Point2D, p2: Point2D) -> i32 {
    (p1.x - p2.x).abs() + (p1.y - p2.y).abs()
}

pub fn reconstruct_path(
    came_from: Map<Point2D, Option<Point2D>>,
    start: Point2D,
    goal: Point2D
) -> Vec<Point2D, U16> {
    let mut current = goal;
    let mut path = Vec::<Point2D, U16>::new();
    while current != start {
        path.push(current).expect("Cannot push to vector");
        current = came_from.get(&current).unwrap().unwrap();
    }
    path.push(start).expect("Cannot push to vector");
    #[cfg(feature = "reverse_path")]
    let path = path.iter()
        .cloned()
        .rev()
        .collect::<Vec<_, U16>>();
    path
}

// TODO: Create some tests
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}