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}