rust_robotics 0.1.0

Umbrella crate for the RustRobotics workspace - feature-gated re-exports of all domain crates
Documentation
use eyre::{bail, Context, Result};
use rust_robotics::prelude::*;
use serde::{Deserialize, Serialize};

const PAYLOAD_VERSION: u32 = 1;
const COORD_EPSILON: f64 = 1e-9;

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct PathWaypoint {
    pub x: f64,
    pub y: f64,
}

impl PathWaypoint {
    pub fn from_point(point: Point2D) -> Self {
        Self {
            x: point.x,
            y: point.y,
        }
    }

    fn approx_eq(self, other: Self) -> bool {
        (self.x - other.x).abs() <= COORD_EPSILON && (self.y - other.y).abs() <= COORD_EPSILON
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathPlanningReport {
    pub version: u32,
    pub planner: String,
    pub start: PathWaypoint,
    pub goal: PathWaypoint,
    pub waypoint_count: usize,
    pub path_length: f64,
    pub waypoints: Vec<PathWaypoint>,
}

#[allow(dead_code)]
impl PathPlanningReport {
    pub fn from_path(path: &Path2D, start: Point2D, goal: Point2D) -> Self {
        Self {
            version: PAYLOAD_VERSION,
            planner: "A*".to_owned(),
            start: PathWaypoint::from_point(start),
            goal: PathWaypoint::from_point(goal),
            waypoint_count: path.len(),
            path_length: path.total_length(),
            waypoints: path
                .points
                .iter()
                .copied()
                .map(PathWaypoint::from_point)
                .collect(),
        }
    }

    pub fn to_json(&self) -> Result<String> {
        self.validate()?;
        serde_json::to_string(self).context("failed to serialize path planning report")
    }

    pub fn from_json(raw: &str) -> Result<Self> {
        let report: Self =
            serde_json::from_str(raw).context("failed to deserialize path planning report")?;
        report.validate()?;
        Ok(report)
    }

    pub fn validate(&self) -> Result<()> {
        if self.version != PAYLOAD_VERSION {
            bail!(
                "unsupported path planning report version {}, expected {}",
                self.version,
                PAYLOAD_VERSION
            );
        }
        if self.planner.is_empty() {
            bail!("path planning report is missing planner name");
        }
        if !self.path_length.is_finite() || self.path_length < 0.0 {
            bail!(
                "path planning report has invalid path_length {}",
                self.path_length
            );
        }
        if self.waypoints.is_empty() {
            bail!("path planning report contains no waypoints");
        }
        if self.waypoint_count != self.waypoints.len() {
            bail!(
                "path planning report waypoint_count {} does not match payload length {}",
                self.waypoint_count,
                self.waypoints.len()
            );
        }
        let first = self.waypoints[0];
        if !first.approx_eq(self.start) {
            bail!("path planning report start does not match first waypoint");
        }
        let last = self.waypoints[self.waypoints.len() - 1];
        if !last.approx_eq(self.goal) {
            bail!("path planning report goal does not match last waypoint");
        }
        Ok(())
    }

    pub fn segment_count(&self) -> usize {
        self.waypoint_count.saturating_sub(1)
    }

    pub fn direct_distance(&self) -> f64 {
        ((self.goal.x - self.start.x).powi(2) + (self.goal.y - self.start.y).powi(2)).sqrt()
    }
}