Skip to main content

navcore_ffi/
route.rs

1//! The chart-backed router, across the FFI boundary.
2//!
3//! Deliberately four parameters and nothing else: a chart path once, then
4//! two positions and the vessel's own safety numbers per query. No
5//! corridor-escalation, margin or lattice-spacing knobs cross this
6//! boundary -- `route_find::find_route` picks all of that for itself now,
7//! which is the whole reason a caller on the other side of the boundary
8//! can use this at all without first learning what a compilation scale is.
9
10use std::sync::Mutex;
11
12use enc_store::{ChartStore, StoreError};
13use route_find::{FindError, FindOptions, find_route};
14
15use crate::FfiPosition;
16
17/// Everything that can go wrong opening a chart or finding a route,
18/// flattened to one message for the foreign side. See
19/// [`enc_store::StoreError`] and [`route_find::FindError`] for what
20/// actually failed; UniFFI's `flat_error` only ever lowers a `Display`
21/// string across the boundary, not the structured original.
22#[derive(Debug, uniffi::Error)]
23#[uniffi(flat_error)]
24pub enum FfiRouteError {
25    /// Reading the chart, or finding a route across it, failed.
26    Failed(String),
27}
28
29impl std::fmt::Display for FfiRouteError {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            Self::Failed(reason) => write!(f, "{reason}"),
33        }
34    }
35}
36
37impl From<StoreError> for FfiRouteError {
38    fn from(error: StoreError) -> Self {
39        Self::Failed(error.to_string())
40    }
41}
42
43impl From<FindError> for FfiRouteError {
44    fn from(error: FindError) -> Self {
45        Self::Failed(error.to_string())
46    }
47}
48
49/// A chart, opened once and reused for as many route queries as the
50/// foreign side wants.
51///
52/// Wraps the store in a mutex for the same reason [`crate::FfiSimulator`]
53/// wraps its own state in one: a UniFFI object is handed to the foreign
54/// side as one shared reference, and `ChartStore`'s `rusqlite::Connection`
55/// is not `Sync` -- nothing here mutates the chart, but more than one
56/// thread reaching into the same connection at once is still unsound.
57#[derive(uniffi::Object)]
58pub struct FfiChartStore {
59    inner: Mutex<ChartStore>,
60}
61
62#[uniffi::export]
63impl FfiChartStore {
64    /// Opens a chart GeoPackage.
65    ///
66    /// # Errors
67    ///
68    /// If the file cannot be opened, or is not a GeoPackage.
69    #[uniffi::constructor]
70    pub fn new(chart_path: String) -> Result<Self, FfiRouteError> {
71        let store = ChartStore::open(std::path::Path::new(&chart_path))?;
72        Ok(Self { inner: Mutex::new(store) })
73    }
74
75    /// The shortest route between `from` and `to` this chart's own safety
76    /// check calls safe, for a vessel needing `safety_contour_m` of water
77    /// and `corridor_nm` of cross-track room either side of the track --
78    /// `None` if no such route exists within what
79    /// [`route_find::find_route`]'s own auto-widening search settles for.
80    ///
81    /// # Errors
82    ///
83    /// If the chart cannot be read.
84    pub fn find_route(
85        &self,
86        from: FfiPosition,
87        to: FfiPosition,
88        safety_contour_m: f64,
89        corridor_nm: f64,
90    ) -> Result<Option<Vec<FfiPosition>>, FfiRouteError> {
91        let store = self.inner.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
92        let options = FindOptions {
93            safety_contour_m,
94            port_xtd_nm: corridor_nm,
95            starboard_xtd_nm: corridor_nm,
96            margin_nm: None,
97            cell_nm: None,
98            avoid_restricted_areas: true,
99        };
100
101        let waypoints = find_route(&store, from.into(), to.into(), options)?;
102        Ok(waypoints
103            .map(|waypoints| waypoints.into_iter().map(|waypoint| waypoint.position.into()).collect()))
104    }
105}