use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use scirs2_core::ndarray::ArrayView1;
use crate::error::{SpatialError, SpatialResult};
#[derive(Debug, Clone)]
pub struct Path<N> {
pub nodes: Vec<N>,
pub cost: f64,
}
impl<N> Path<N> {
pub fn new(nodes: Vec<N>, cost: f64) -> Self {
Path { nodes, cost }
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn len(&self) -> usize {
self.nodes.len()
}
}
#[derive(Debug, Clone)]
pub struct Node<N: Clone + Eq + Hash> {
pub state: N,
pub parent: Option<Rc<Node<N>>>,
pub g: f64,
pub h: f64,
}
impl<N: Clone + Eq + Hash> Node<N> {
pub fn new(state: N, parent: Option<Rc<Node<N>>>, g: f64, h: f64) -> Self {
Node {
state,
parent,
g,
h,
}
}
pub fn f(&mut self) -> f64 {
self.g + self.h
}
}
impl<N: Clone + Eq + Hash> PartialEq for Node<N> {
fn eq(&self, other: &Self) -> bool {
self.state == other.state
}
}
impl<N: Clone + Eq + Hash> Eq for Node<N> {}
impl<N: Clone + Eq + Hash> Ord for Node<N> {
fn cmp(&self, other: &Self) -> Ordering {
let self_f = self.g + self.h;
let other_f = other.g + other.h;
other_f.partial_cmp(&self_f).unwrap_or(Ordering::Equal)
}
}
impl<N: Clone + Eq + Hash> PartialOrd for Node<N> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub type NeighborFn<N> = dyn Fn(&N) -> Vec<(N, f64)>;
pub type HeuristicFn<N> = dyn Fn(&N, &N) -> f64;
#[derive(Debug, Clone, Copy)]
pub struct HashableFloat2D {
pub x: f64,
pub y: f64,
}
impl HashableFloat2D {
pub fn new(x: f64, y: f64) -> Self {
HashableFloat2D { x, y }
}
pub fn from_array(arr: [f64; 2]) -> Self {
HashableFloat2D {
x: arr[0],
y: arr[1],
}
}
pub fn to_array(&self) -> [f64; 2] {
[self.x, self.y]
}
pub fn distance(&self, other: &HashableFloat2D) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
impl PartialEq for HashableFloat2D {
fn eq(&self, other: &Self) -> bool {
const EPSILON: f64 = 1e-10;
(self.x - other.x).abs() < EPSILON && (self.y - other.y).abs() < EPSILON
}
}
impl Eq for HashableFloat2D {}
impl Hash for HashableFloat2D {
fn hash<H: Hasher>(&self, state: &mut H) {
let precision = 1_000_000.0; let x_rounded = (self.x * precision).round() as i64;
let y_rounded = (self.y * precision).round() as i64;
x_rounded.hash(state);
y_rounded.hash(state);
}
}
#[derive(Debug)]
pub struct AStarPlanner {
max_iterations: Option<usize>,
weight: f64,
}
impl Default for AStarPlanner {
fn default() -> Self {
Self::new()
}
}
impl AStarPlanner {
pub fn new() -> Self {
AStarPlanner {
max_iterations: None,
weight: 1.0,
}
}
pub fn with_max_iterations(mut self, maxiterations: usize) -> Self {
self.max_iterations = Some(maxiterations);
self
}
pub fn with_weight(mut self, weight: f64) -> Self {
if weight < 0.0 {
self.weight = 0.0;
} else {
self.weight = weight;
}
self
}
pub fn search<N: Clone + Eq + Hash>(
&self,
start: N,
goal: N,
neighbors_fn: &dyn Fn(&N) -> Vec<(N, f64)>,
heuristic_fn: &dyn Fn(&N, &N) -> f64,
) -> SpatialResult<Option<Path<N>>> {
let mut open_set = BinaryHeap::new();
let mut closed_set = HashMap::new();
let h_start = heuristic_fn(&start, &goal);
let start_node = Rc::new(Node::new(start, None, 0.0, self.weight * h_start));
open_set.push(Rc::clone(&start_node));
let mut g_values = HashMap::new();
g_values.insert(start_node.state.clone(), 0.0);
let mut iterations = 0;
while let Some(current) = open_set.pop() {
if current.state == goal {
return Ok(Some(AStarPlanner::reconstruct_path(goal.clone(), current)));
}
if let Some(max_iter) = self.max_iterations {
iterations += 1;
if iterations > max_iter {
return Ok(None);
}
}
if closed_set.contains_key(¤t.state) {
continue;
}
closed_set.insert(current.state.clone(), Rc::clone(¤t));
for (neighbor_state, cost) in neighbors_fn(¤t.state) {
if closed_set.contains_key(&neighbor_state) {
continue;
}
let tentative_g = current.g + cost;
let in_open_set = g_values.contains_key(&neighbor_state);
if in_open_set
&& tentative_g >= *g_values.get(&neighbor_state).expect("Operation failed")
{
continue;
}
g_values.insert(neighbor_state.clone(), tentative_g);
let h = self.weight * heuristic_fn(&neighbor_state, &goal);
let neighbor_node = Rc::new(Node::new(
neighbor_state,
Some(Rc::clone(¤t)),
tentative_g,
h,
));
open_set.push(neighbor_node);
}
}
Ok(None)
}
fn reconstruct_path<N: Clone + Eq + Hash>(goal: N, node: Rc<Node<N>>) -> Path<N> {
let mut path = Vec::new();
let mut current = Some(node);
let mut cost = 0.0;
while let Some(_node) = current {
path.push(_node.state.clone());
cost = _node.g;
current = _node.parent.clone();
}
path.reverse();
Path::new(path, cost)
}
}
#[allow(dead_code)]
pub fn manhattan_distance(a: &[i32; 2], b: &[i32; 2]) -> f64 {
((a[0] - b[0]).abs() + (a[1] - b[1]).abs()) as f64
}
#[allow(dead_code)]
pub fn euclidean_distance_2d(a: &[f64; 2], b: &[f64; 2]) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
(dx * dx + dy * dy).sqrt()
}
#[allow(dead_code)]
pub fn euclidean_distance(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> SpatialResult<f64> {
if a.len() != b.len() {
return Err(SpatialError::DimensionError(format!(
"Mismatched dimensions: {} and {}",
a.len(),
b.len()
)));
}
let mut sum = 0.0;
for i in 0..a.len() {
let diff = a[i] - b[i];
sum += diff * diff;
}
Ok(sum.sqrt())
}
#[derive(Clone)]
pub struct GridAStarPlanner {
pub grid: Vec<Vec<bool>>, pub diagonalsallowed: bool,
}
impl GridAStarPlanner {
pub fn new(grid: Vec<Vec<bool>>, diagonalsallowed: bool) -> Self {
GridAStarPlanner {
grid,
diagonalsallowed,
}
}
pub fn height(&self) -> usize {
self.grid.len()
}
pub fn width(&self) -> usize {
if self.grid.is_empty() {
0
} else {
self.grid[0].len()
}
}
pub fn is_valid(&self, pos: &[i32; 2]) -> bool {
let (rows, cols) = (self.height() as i32, self.width() as i32);
if pos[0] < 0 || pos[0] >= rows || pos[1] < 0 || pos[1] >= cols {
return false;
}
!self.grid[pos[0] as usize][pos[1] as usize]
}
fn get_neighbors(&self, pos: &[i32; 2]) -> Vec<([i32; 2], f64)> {
let mut neighbors = Vec::new();
let directions = if self.diagonalsallowed {
vec![
[-1, 0],
[1, 0],
[0, -1],
[0, 1], [-1, -1],
[-1, 1],
[1, -1],
[1, 1], ]
} else {
vec![[-1, 0], [1, 0], [0, -1], [0, 1]]
};
for dir in directions {
let neighbor = [pos[0] + dir[0], pos[1] + dir[1]];
if self.is_valid(&neighbor) {
let cost = if dir[0] != 0 && dir[1] != 0 {
std::f64::consts::SQRT_2
} else {
1.0
};
neighbors.push((neighbor, cost));
}
}
neighbors
}
pub fn find_path(
&self,
start: [i32; 2],
goal: [i32; 2],
) -> SpatialResult<Option<Path<[i32; 2]>>> {
if !self.is_valid(&start) {
return Err(SpatialError::ValueError(
"Start position is invalid or an obstacle".to_string(),
));
}
if !self.is_valid(&goal) {
return Err(SpatialError::ValueError(
"Goal position is invalid or an obstacle".to_string(),
));
}
let planner = AStarPlanner::new();
let grid_clone = self.clone();
let neighbors_fn = move |pos: &[i32; 2]| grid_clone.get_neighbors(pos);
let heuristic_fn = |a: &[i32; 2], b: &[i32; 2]| manhattan_distance(a, b);
planner.search(start, goal, &neighbors_fn, &heuristic_fn)
}
}
#[derive(Clone)]
pub struct ContinuousAStarPlanner {
pub obstacles: Vec<Vec<[f64; 2]>>,
pub step_size: f64,
pub collisionthreshold: f64,
}
impl ContinuousAStarPlanner {
pub fn new(obstacles: Vec<Vec<[f64; 2]>>, step_size: f64, collisionthreshold: f64) -> Self {
ContinuousAStarPlanner {
obstacles,
step_size,
collisionthreshold,
}
}
pub fn is_in_collision(&self, point: &[f64; 2]) -> bool {
for obstacle in &self.obstacles {
if Self::point_in_polygon(point, obstacle) {
return true;
}
}
false
}
pub fn line_in_collision(&self, start: &[f64; 2], end: &[f64; 2]) -> bool {
let dx = end[0] - start[0];
let dy = end[1] - start[1];
let distance = (dx * dx + dy * dy).sqrt();
let steps = (distance / self.step_size).ceil() as usize;
if steps == 0 {
return self.is_in_collision(start) || self.is_in_collision(end);
}
for i in 0..=steps {
let t = i as f64 / steps as f64;
let x = start[0] + dx * t;
let y = start[1] + dy * t;
if self.is_in_collision(&[x, y]) {
return true;
}
}
false
}
fn point_in_polygon(point: &[f64; 2], polygon: &[[f64; 2]]) -> bool {
if polygon.len() < 3 {
return false;
}
let mut inside = false;
let mut j = polygon.len() - 1;
for i in 0..polygon.len() {
let xi = polygon[i][0];
let yi = polygon[i][1];
let xj = polygon[j][0];
let yj = polygon[j][1];
let intersect = ((yi > point[1]) != (yj > point[1]))
&& (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi);
if intersect {
inside = !inside;
}
j = i;
}
inside
}
fn get_neighbors(&self, pos: &[f64; 2], radius: f64) -> Vec<([f64; 2], f64)> {
let mut neighbors = Vec::new();
let num_samples = 8;
for i in 0..num_samples {
let angle = 2.0 * std::f64::consts::PI * (i as f64) / (num_samples as f64);
let nx = pos[0] + radius * angle.cos();
let ny = pos[1] + radius * angle.sin();
let neighbor = [nx, ny];
if !self.line_in_collision(pos, &neighbor) {
let cost = radius; neighbors.push((neighbor, cost));
}
}
neighbors
}
pub fn find_path(
&self,
start: [f64; 2],
goal: [f64; 2],
neighbor_radius: f64,
) -> SpatialResult<Option<Path<[f64; 2]>>> {
#[derive(Clone, Hash, PartialEq, Eq)]
struct Point2D {
x: i64,
y: i64,
}
let precision = 1000.0; let to_point = |p: [f64; 2]| -> Point2D {
Point2D {
x: (p[0] * precision).round() as i64,
y: (p[1] * precision).round() as i64,
}
};
let start_point = to_point(start);
let goal_point = to_point(goal);
if self.is_in_collision(&start) {
return Err(SpatialError::ValueError(
"Start position is in collision with an obstacle".to_string(),
));
}
if self.is_in_collision(&goal) {
return Err(SpatialError::ValueError(
"Goal position is in collision with an obstacle".to_string(),
));
}
if !self.line_in_collision(&start, &goal) {
let path = vec![start, goal];
let cost = euclidean_distance_2d(&start, &goal);
return Ok(Some(Path::new(path, cost)));
}
let planner = AStarPlanner::new();
let radius = neighbor_radius;
let planner_clone = self.clone();
let neighbors_fn = move |pos: &Point2D| {
let float_pos = [pos.x as f64 / precision, pos.y as f64 / precision];
planner_clone
.get_neighbors(&float_pos, radius)
.into_iter()
.map(|(neighbor, cost)| (to_point(neighbor), cost))
.collect()
};
let heuristic_fn = |a: &Point2D, b: &Point2D| {
let a_float = [a.x as f64 / precision, a.y as f64 / precision];
let b_float = [b.x as f64 / precision, b.y as f64 / precision];
euclidean_distance_2d(&a_float, &b_float)
};
let result = planner.search(start_point, goal_point, &neighbors_fn, &heuristic_fn)?;
if let Some(path) = result {
let float_path = path
.nodes
.into_iter()
.map(|p| [p.x as f64 / precision, p.y as f64 / precision])
.collect();
Ok(Some(Path::new(float_path, path.cost)))
} else {
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_astar_grid_no_obstacles() {
let grid = vec![
vec![false, false, false, false, false],
vec![false, false, false, false, false],
vec![false, false, false, false, false],
vec![false, false, false, false, false],
vec![false, false, false, false, false],
];
let planner = GridAStarPlanner::new(grid, false);
let start = [0, 0];
let goal = [4, 4];
let path = planner
.find_path(start, goal)
.expect("Operation failed")
.expect("Operation failed");
assert!(!path.is_empty());
assert_eq!(path.nodes.first().expect("Operation failed"), &start);
assert_eq!(path.nodes.last().expect("Operation failed"), &goal);
assert_eq!(path.len(), 9);
}
#[test]
fn test_astar_grid_with_obstacles() {
let grid = vec![
vec![false, false, false, false, false],
vec![false, false, false, false, false],
vec![false, true, true, true, false],
vec![false, false, false, false, false],
vec![false, false, false, false, false],
];
let planner = GridAStarPlanner::new(grid, false);
let start = [1, 1];
let goal = [4, 3];
let path = planner
.find_path(start, goal)
.expect("Operation failed")
.expect("Operation failed");
assert!(!path.is_empty());
assert_eq!(path.nodes.first().expect("Operation failed"), &start);
assert_eq!(path.nodes.last().expect("Operation failed"), &goal);
for node in &path.nodes {
assert!(!planner.grid[node[0] as usize][node[1] as usize]);
}
}
#[test]
fn test_astar_grid_no_path() {
let grid = vec![
vec![false, false, false, false, false],
vec![false, false, false, false, false],
vec![true, true, true, true, true],
vec![false, false, false, false, false],
vec![false, false, false, false, false],
];
let planner = GridAStarPlanner::new(grid, false);
let start = [1, 1];
let goal = [4, 1];
let path = planner.find_path(start, goal).expect("Operation failed");
assert!(path.is_none());
}
#[test]
fn test_astar_grid_with_diagonals() {
let grid = vec![
vec![false, false, false, false, false],
vec![false, false, false, false, false],
vec![false, false, false, false, false],
vec![false, false, false, false, false],
vec![false, false, false, false, false],
];
let planner = GridAStarPlanner::new(grid, true);
let start = [0, 0];
let goal = [4, 4];
let path = planner
.find_path(start, goal)
.expect("Operation failed")
.expect("Operation failed");
assert!(!path.is_empty());
assert_eq!(path.nodes.first().expect("Operation failed"), &start);
assert_eq!(path.nodes.last().expect("Operation failed"), &goal);
assert_eq!(path.len(), 5);
}
}