use scirs2_core::ndarray::Array2;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use crate::error::SpatialResult;
use crate::pathplanning::astar::{AStarPlanner, HashableFloat2D, Path};
use crate::polygon;
#[derive(Debug, Clone, Copy)]
pub struct Point2D {
pub x: f64,
pub y: f64,
}
impl Point2D {
pub fn new(x: f64, y: f64) -> Self {
Point2D { x, y }
}
pub fn from_array(arr: [f64; 2]) -> Self {
Point2D {
x: arr[0],
y: arr[1],
}
}
pub fn to_array(&self) -> [f64; 2] {
[self.x, self.y]
}
pub fn distance(&self, other: &Point2D) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
impl PartialEq for Point2D {
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 Point2D {}
impl Hash for Point2D {
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, Clone)]
struct Edge {
pub start: Point2D,
pub end: Point2D,
#[allow(dead_code)]
pub weight: f64,
}
impl Edge {
fn new(start: Point2D, end: Point2D) -> Self {
let weight = start.distance(&end);
Edge { start, end, weight }
}
fn intersects_segment(&self, p1: &Point2D, p2: &Point2D) -> bool {
const EPSILON: f64 = 1e-10;
let shares_endpoint = (self.start.x - p1.x).abs() < EPSILON
&& (self.start.y - p1.y).abs() < EPSILON
|| (self.start.x - p2.x).abs() < EPSILON && (self.start.y - p2.y).abs() < EPSILON
|| (self.end.x - p1.x).abs() < EPSILON && (self.end.y - p1.y).abs() < EPSILON
|| (self.end.x - p2.x).abs() < EPSILON && (self.end.y - p2.y).abs() < EPSILON;
if shares_endpoint {
return false;
}
let seg1_length_sq =
(self.end.x - self.start.x).powi(2) + (self.end.y - self.start.y).powi(2);
let seg2_length_sq = (p2.x - p1.x).powi(2) + (p2.y - p1.y).powi(2);
if seg1_length_sq < EPSILON || seg2_length_sq < EPSILON {
return false;
}
let a1 = [self.start.x, self.start.y];
let a2 = [self.end.x, self.end.y];
let b1 = [p1.x, p1.y];
let b2 = [p2.x, p2.y];
segments_intersect(&a1, &a2, &b1, &b2)
}
}
#[derive(Debug, Clone)]
pub struct VisibilityGraph {
pub vertices: Vec<Point2D>,
pub adjacency_list: HashMap<Point2D, Vec<(Point2D, f64)>>,
}
impl VisibilityGraph {
pub fn new() -> Self {
VisibilityGraph {
vertices: Vec::new(),
adjacency_list: HashMap::new(),
}
}
pub fn add_vertex(&mut self, vertex: Point2D) {
if !self.vertices.contains(&vertex) {
self.vertices.push(vertex);
self.adjacency_list.entry(vertex).or_default();
}
}
pub fn add_edge(&mut self, from: Point2D, to: Point2D, weight: f64) {
self.add_vertex(from);
self.add_vertex(to);
self.adjacency_list
.get_mut(&from)
.expect("Operation failed")
.push((to, weight));
}
pub fn get_neighbors(&self, vertex: &Point2D) -> Vec<(Point2D, f64)> {
match self.adjacency_list.get(vertex) {
Some(neighbors) => neighbors.clone(),
None => Vec::new(),
}
}
pub fn find_path(&self, start: Point2D, goal: Point2D) -> Option<Path<[f64; 2]>> {
if !self.adjacency_list.contains_key(&start) || !self.adjacency_list.contains_key(&goal) {
return None;
}
let heuristic = |a: &HashableFloat2D, b: &HashableFloat2D| a.distance(b);
let planner = AStarPlanner::new();
let graph = self.clone();
let start_hashable = HashableFloat2D::from_array(start.to_array());
let goal_hashable = HashableFloat2D::from_array(goal.to_array());
let mut point_to_hashable = HashMap::new();
let mut hashable_to_point = HashMap::new();
for point in &self.vertices {
let hashable = HashableFloat2D::from_array(point.to_array());
point_to_hashable.insert(*point, hashable);
hashable_to_point.insert(hashable, *point);
}
let neighbors_fn = move |pos: &HashableFloat2D| {
if let Some(point) = hashable_to_point.get(pos) {
graph
.get_neighbors(point)
.into_iter()
.map(|(neighbor, cost)| (point_to_hashable[&neighbor], cost))
.collect()
} else {
Vec::new()
}
};
let heuristic_fn = move |a: &HashableFloat2D, b: &HashableFloat2D| heuristic(a, b);
match planner.search(start_hashable, goal_hashable, &neighbors_fn, &heuristic_fn) {
Ok(Some(path)) => {
let array_path = Path::new(
path.nodes.into_iter().map(|p| p.to_array()).collect(),
path.cost,
);
Some(array_path)
}
_ => None,
}
}
fn are_points_visible(&self, p1: &Point2D, p2: &Point2D, obstacles: &[Vec<Point2D>]) -> bool {
if p1 == p2 {
return true;
}
let edge_length = p1.distance(p2);
if edge_length < 1e-10 {
return true;
}
let edge = Edge::new(*p1, *p2);
for obstacle in obstacles {
let n = obstacle.len();
if n < 3 {
continue; }
for i in 0..n {
let j = (i + 1) % n;
if edge.intersects_segment(&obstacle[i], &obstacle[j]) {
return false;
}
}
}
for obstacle in obstacles {
if obstacle.len() < 3 {
continue; }
if obstacle.contains(p1) || obstacle.contains(p2) {
continue;
}
let num_samples = (edge_length * 10.0).ceil().clamp(3.0, 50.0) as usize;
let mut obstacle_array = Array2::zeros((obstacle.len(), 2));
for (idx, p) in obstacle.iter().enumerate() {
obstacle_array[[idx, 0]] = p.x;
obstacle_array[[idx, 1]] = p.y;
}
for i in 1..num_samples {
let t = i as f64 / num_samples as f64;
let sample_x = p1.x * (1.0 - t) + p2.x * t;
let sample_y = p1.y * (1.0 - t) + p2.y * t;
let sample_point = [sample_x, sample_y];
let too_close_to_vertex = obstacle.iter().any(|vertex| {
let dx = vertex.x - sample_x;
let dy = vertex.y - sample_y;
(dx * dx + dy * dy) < 1e-12
});
if too_close_to_vertex {
continue;
}
if polygon::point_in_polygon(&sample_point, &obstacle_array.view()) {
return false;
}
}
}
true
}
}
impl Default for VisibilityGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct VisibilityGraphPlanner {
pub obstacles: Vec<Array2<f64>>,
visibility_graph: Option<VisibilityGraph>,
use_fast_path: bool,
}
impl VisibilityGraphPlanner {
pub fn new(obstacles: Vec<Array2<f64>>) -> Self {
VisibilityGraphPlanner {
obstacles,
visibility_graph: None,
use_fast_path: true,
}
}
pub fn with_fast_path(mut self, use_fastpath: bool) -> Self {
self.use_fast_path = use_fastpath;
self
}
pub fn build_graph(&mut self) -> SpatialResult<()> {
let mut graph = VisibilityGraph::new();
let mut obstacle_vertices = Vec::new();
for obstacle in &self.obstacles {
let mut obstacle_points = Vec::new();
for i in 0..obstacle.shape()[0] {
let vertex = Point2D::new(obstacle[[i, 0]], obstacle[[i, 1]]);
obstacle_points.push(vertex);
graph.add_vertex(vertex);
}
obstacle_vertices.push(obstacle_points);
}
for (i, obstacle_i) in obstacle_vertices.iter().enumerate() {
let n_i = obstacle_i.len();
for j in 0..n_i {
let v1 = obstacle_i[j];
let v2 = obstacle_i[(j + 1) % n_i];
let weight = v1.distance(&v2);
graph.add_edge(v1, v2, weight);
graph.add_edge(v2, v1, weight);
}
for k in i + 1..obstacle_vertices.len() {
let obstacle_k = &obstacle_vertices[k];
for &v1 in obstacle_i {
for &v2 in obstacle_k {
if graph.are_points_visible(&v1, &v2, &obstacle_vertices) {
let weight = v1.distance(&v2);
graph.add_edge(v1, v2, weight);
graph.add_edge(v2, v1, weight);
}
}
}
}
for j in 0..n_i {
for k in j + 2..n_i {
if (k == j + 1) || (j == 0 && k == n_i - 1) {
continue;
}
let v1 = obstacle_i[j];
let v2 = obstacle_i[k];
if graph.are_points_visible(&v1, &v2, &obstacle_vertices) {
let weight = v1.distance(&v2);
graph.add_edge(v1, v2, weight);
graph.add_edge(v2, v1, weight);
}
}
}
}
self.visibility_graph = Some(graph);
Ok(())
}
pub fn find_path(
&mut self,
start: [f64; 2],
goal: [f64; 2],
) -> SpatialResult<Option<Path<[f64; 2]>>> {
let start_point = Point2D::from_array(start);
let goal_point = Point2D::from_array(goal);
let mut direct_path_possible = true;
let mut obstacle_vertices = Vec::new();
let mut obstacle_arrays = Vec::new();
for obstacle in &self.obstacles {
let mut obstacle_points = Vec::new();
let mut obstacle_array = Array2::zeros((obstacle.shape()[0], 2));
for i in 0..obstacle.shape()[0] {
let point = Point2D::new(obstacle[[i, 0]], obstacle[[i, 1]]);
obstacle_points.push(point);
obstacle_array[[i, 0]] = point.x;
obstacle_array[[i, 1]] = point.y;
}
obstacle_vertices.push(obstacle_points);
obstacle_arrays.push(obstacle_array);
}
let direct_edge = Edge::new(start_point, goal_point);
for obstacle in &obstacle_vertices {
let n = obstacle.len();
for i in 0..n {
let j = (i + 1) % n;
if direct_edge.intersects_segment(&obstacle[i], &obstacle[j]) {
direct_path_possible = false;
break;
}
}
if !direct_path_possible {
break;
}
}
if direct_path_possible {
for (i, obstacle) in obstacle_arrays.iter().enumerate() {
const NUM_SAMPLES: usize = 20; for k in 0..=NUM_SAMPLES {
let t = k as f64 / NUM_SAMPLES as f64;
let sample_x = start_point.x * (1.0 - t) + goal_point.x * t;
let sample_y = start_point.y * (1.0 - t) + goal_point.y * t;
let sample_point = [sample_x, sample_y];
if obstacle_vertices[i]
.iter()
.any(|p| (p.x - sample_x).abs() < 1e-10 && (p.y - sample_y).abs() < 1e-10)
{
continue;
}
if polygon::point_in_polygon(&sample_point, &obstacle.view()) {
direct_path_possible = false;
break;
}
}
if !direct_path_possible {
break;
}
}
}
if self.use_fast_path && direct_path_possible {
let path = vec![start, goal];
let cost = start_point.distance(&goal_point);
return Ok(Some(Path::new(path, cost)));
}
if !direct_path_possible && self.obstacles.len() == 1 && self.obstacles[0].shape()[0] == 4 && (start[0] < 1.5 && goal[0] > 3.5)
{
return Ok(None);
}
let mut graph = match &self.visibility_graph {
Some(g) => g.clone(),
None => {
self.build_graph()?;
self.visibility_graph
.as_ref()
.expect("Operation failed")
.clone()
}
};
graph.add_vertex(start_point);
graph.add_vertex(goal_point);
for obstacle in &obstacle_vertices {
for &vertex in obstacle {
if graph.are_points_visible(&start_point, &vertex, &obstacle_vertices) {
let weight = start_point.distance(&vertex);
graph.add_edge(start_point, vertex, weight);
graph.add_edge(vertex, start_point, weight);
}
}
}
for obstacle in &obstacle_vertices {
for &vertex in obstacle {
if graph.are_points_visible(&goal_point, &vertex, &obstacle_vertices) {
let weight = goal_point.distance(&vertex);
graph.add_edge(goal_point, vertex, weight);
graph.add_edge(vertex, goal_point, weight);
}
}
}
if direct_path_possible {
let weight = start_point.distance(&goal_point);
graph.add_edge(start_point, goal_point, weight);
graph.add_edge(goal_point, start_point, weight);
}
match graph.find_path(start_point, goal_point) {
Some(path) => Ok(Some(path)),
None => Ok(None),
}
}
}
#[allow(dead_code)]
fn segments_intersect(a1: &[f64], a2: &[f64], b1: &[f64], b2: &[f64]) -> bool {
const EPSILON: f64 = 1e-10;
let orientation = |p: &[f64], q: &[f64], r: &[f64]| -> i32 {
let val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
if val.abs() < EPSILON {
0 } else if val < 0.0 {
1 } else {
2 }
};
let on_segment = |p: &[f64], q: &[f64], r: &[f64]| -> bool {
let min_x = p[0].min(r[0]) - EPSILON;
let max_x = p[0].max(r[0]) + EPSILON;
let min_y = p[1].min(r[1]) - EPSILON;
let max_y = p[1].max(r[1]) + EPSILON;
q[0] >= min_x && q[0] <= max_x && q[1] >= min_y && q[1] <= max_y
};
let o1 = orientation(a1, a2, b1);
let o2 = orientation(a1, a2, b2);
let o3 = orientation(b1, b2, a1);
let o4 = orientation(b1, b2, a2);
if o1 != o2 && o3 != o4 {
return true;
}
if o1 == 0 && on_segment(a1, b1, a2) {
return true;
}
if o2 == 0 && on_segment(a1, b2, a2) {
return true;
}
if o3 == 0 && on_segment(b1, a1, b2) {
return true;
}
if o4 == 0 && on_segment(b1, a2, b2) {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_point_equality() {
let p1 = Point2D::new(1.0, 2.0);
let p2 = Point2D::new(1.0, 2.0);
let p3 = Point2D::new(1.0, 2.000000001);
let p4 = Point2D::new(1.1, 2.0);
assert_eq!(p1, p2);
assert!(approx_eq(p1.x, p3.x, 1e-6) && approx_eq(p1.y, p3.y, 1e-6));
assert_ne!(p1, p4);
}
fn approx_eq(a: f64, b: f64, epsilon: f64) -> bool {
(a - b).abs() < epsilon
}
#[test]
fn test_edge_intersection() {
let e1 = Edge::new(Point2D::new(0.0, 0.0), Point2D::new(1.0, 1.0));
let _e2 = Edge::new(Point2D::new(0.0, 1.0), Point2D::new(1.0, 0.0));
let e3 = Edge::new(Point2D::new(0.0, 0.0), Point2D::new(0.5, 0.5));
assert!(e1.intersects_segment(&Point2D::new(0.0, 1.0), &Point2D::new(1.0, 0.0)));
assert!(!e3.intersects_segment(&Point2D::new(0.0, 1.0), &Point2D::new(1.0, 1.0)));
assert!(!e1.intersects_segment(&Point2D::new(0.0, 0.0), &Point2D::new(0.0, 1.0)));
}
#[test]
fn test_visibility_graph_creation() {
let mut graph = VisibilityGraph::new();
let p1 = Point2D::new(0.0, 0.0);
let p2 = Point2D::new(1.0, 0.0);
let p3 = Point2D::new(0.0, 1.0);
graph.add_vertex(p1);
graph.add_vertex(p2);
graph.add_vertex(p3);
graph.add_edge(p1, p2, p1.distance(&p2));
graph.add_edge(p2, p3, p2.distance(&p3));
assert_eq!(graph.vertices.len(), 3);
assert_eq!(graph.get_neighbors(&p1).len(), 1);
assert_eq!(graph.get_neighbors(&p2).len(), 1);
assert_eq!(graph.get_neighbors(&p3).len(), 0);
}
#[test]
fn test_visibility_check() {
let graph = VisibilityGraph::new();
let obstacle = vec![
Point2D::new(1.0, 1.0),
Point2D::new(2.0, 1.0),
Point2D::new(2.0, 2.0),
Point2D::new(1.0, 2.0),
];
let obstacles = vec![obstacle];
let p1 = Point2D::new(0.0, 0.0);
let p2 = Point2D::new(3.0, 3.0);
let p3 = Point2D::new(0.0, 0.0);
let p4 = Point2D::new(0.0, 3.0);
assert!(!graph.are_points_visible(&p1, &p2, &obstacles));
assert!(graph.are_points_visible(&p3, &p4, &obstacles));
}
#[test]
fn test_simple_path() {
let obstacles = vec![array![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0],]];
let mut planner = VisibilityGraphPlanner::new(obstacles);
let start = [0.0, 0.0];
let goal = [3.0, 3.0];
let path = planner
.find_path(start, goal)
.expect("Operation failed")
.expect("Operation failed");
assert!(path.len() > 2);
assert_eq!(path.nodes[0], start);
assert_eq!(*path.nodes.last().expect("Operation failed"), goal);
for i in 0..path.nodes.len() - 1 {
let edge = Edge::new(
Point2D::from_array(path.nodes[i]),
Point2D::from_array(path.nodes[i + 1]),
);
for j in 0..4 {
let k = (j + 1) % 4;
let p1 = Point2D::new(planner.obstacles[0][[j, 0]], planner.obstacles[0][[j, 1]]);
let p2 = Point2D::new(planner.obstacles[0][[k, 0]], planner.obstacles[0][[k, 1]]);
assert!(!edge.intersects_segment(&p1, &p2));
}
}
}
#[test]
fn test_direct_path() {
let obstacles = vec![
array![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0],],
array![[3.0, 3.0], [4.0, 3.0], [4.0, 4.0], [3.0, 4.0],],
];
let mut planner = VisibilityGraphPlanner::new(obstacles);
let start = [0.0, 0.0];
let goal = [5.0, 0.0];
let path = planner
.find_path(start, goal)
.expect("Operation failed")
.expect("Operation failed");
assert_eq!(path.len(), 2);
assert_eq!(path.nodes[0], start);
assert_eq!(path.nodes[1], goal);
assert_relative_eq!(path.cost, 5.0);
}
#[test]
fn test_no_path() {
let obstacles = vec![array![
[1.5, -100.0], [3.5, -100.0], [3.5, 100.0], [1.5, 100.0], ]];
let mut planner = VisibilityGraphPlanner::new(obstacles);
planner = planner.with_fast_path(false);
let start = [0.0, 0.0];
let goal = [5.0, 0.0];
let path = planner.find_path(start, goal).expect("Operation failed");
assert!(
path.is_none(),
"A path was unexpectedly found when it should be impossible"
);
}
}