use crate::polygonal::{Point2, PolygonScene, PolygonSearchRequest};
use condor_core::{BudgetExhausted, SearchOutcome, SearchPathCost, SearchVisitStats};
use std::{error::Error, fmt};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum PolygonPathBuildError {
#[error("polygon paths must contain at least one point")]
Empty,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PolygonPath {
points: Vec<Point2>,
cost: f64,
}
impl PolygonPath {
pub fn from_points(points: Vec<Point2>) -> Result<Self, PolygonPathBuildError> {
if points.is_empty() {
return Err(PolygonPathBuildError::Empty);
}
let cost = points
.windows(2)
.map(|pair| pair[0].distance_to(pair[1]))
.sum();
Ok(Self { points, cost })
}
pub fn from_points_with_cost(
points: Vec<Point2>,
cost: f64,
) -> Result<Self, PolygonPathBuildError> {
if points.is_empty() {
return Err(PolygonPathBuildError::Empty);
}
Ok(Self { points, cost })
}
#[must_use]
pub fn points(&self) -> &[Point2] {
&self.points
}
#[must_use]
pub fn len(&self) -> usize {
self.points.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
#[must_use]
pub fn start(&self) -> Point2 {
self.points
.first()
.copied()
.expect("polygon paths must contain at least one point")
}
#[must_use]
pub fn goal(&self) -> Point2 {
self.points
.last()
.copied()
.expect("polygon paths must contain at least one point")
}
#[must_use]
pub const fn cost(&self) -> f64 {
self.cost
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct PolygonSearchStats {
pub visited_nodes: usize,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PolygonSearchError {
InvalidStart {
point: Point2,
},
InvalidGoal {
point: Point2,
},
BudgetExhausted(BudgetExhausted),
}
impl fmt::Display for PolygonSearchError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidStart { point } => write!(formatter, "invalid polygon start: {point:?}"),
Self::InvalidGoal { point } => write!(formatter, "invalid polygon goal: {point:?}"),
Self::BudgetExhausted(reason) => write!(formatter, "{reason}"),
}
}
}
impl Error for PolygonSearchError {}
pub type PolygonSearchResult =
Result<SearchOutcome<PolygonPath, PolygonSearchStats>, PolygonSearchError>;
pub(crate) const fn found(path: PolygonPath, visited_nodes: usize) -> PolygonSearchResult {
Ok(SearchOutcome::found(
path,
PolygonSearchStats { visited_nodes },
))
}
pub(crate) const fn not_found(visited_nodes: usize) -> PolygonSearchResult {
Ok(SearchOutcome::no_path(PolygonSearchStats { visited_nodes }))
}
impl SearchPathCost for PolygonPath {
type Cost = f64;
fn path_cost(&self) -> Self::Cost {
self.cost()
}
}
impl SearchVisitStats for PolygonSearchStats {
fn visited_nodes(&self) -> usize {
self.visited_nodes
}
}
pub trait PolygonPathfinder {
fn name(&self) -> &'static str;
fn search(&self, scene: &PolygonScene, request: PolygonSearchRequest) -> PolygonSearchResult;
}
#[doc(hidden)]
pub const fn budget_error(reason: BudgetExhausted) -> PolygonSearchError {
PolygonSearchError::BudgetExhausted(reason)
}
#[cfg(test)]
mod tests {
use super::PolygonPath;
use crate::polygonal::Point2;
#[test]
fn polygon_path_computes_euclidean_cost_from_points() {
let path = PolygonPath::from_points(vec![
Point2::new(1.0, 1.0),
Point2::new(4.0, 5.0),
Point2::new(7.0, 5.0),
])
.expect("polygon path contains at least one point");
assert_eq!(path.start(), Point2::new(1.0, 1.0));
assert_eq!(path.goal(), Point2::new(7.0, 5.0));
assert!((path.cost() - 8.0).abs() <= 1e-9);
assert_eq!(path.len(), 3);
assert!(!path.is_empty());
}
}