Skip to main content

navcore_ffi/
lib.rs

1//! The UniFFI adapter: nav-core's boundary to clients written in other
2//! languages.
3//!
4//! Everything UniFFI-shaped lives here, in the outermost layer, rather than
5//! as derives bolted onto `fix` or `nav-math`'s own types -- the workspace
6//! `Cargo.toml`'s own architecture comment is "nav-math must not know
7//! Signal K exists", and a `#[derive(uniffi::Record)]` on `fix::VesselState` would
8//! be exactly that. This crate converts, it does not redefine: the types
9//! it wraps stay exactly what they are elsewhere.
10//!
11//! Deliberately narrow for now: a client has nothing to draw without a
12//! source of vessel state, and [`FfiSimulator`] is the only one that
13//! exists yet, so it is the only one exposed. A real one (Signal K) joins
14//! later without this shape needing to change.
15//!
16//! `route` is the other side of that same growth: a client that can
17//! draw a vessel also needs somewhere to send it, and `route_find` is
18//! nav-core's answer to "where."
19
20mod route;
21
22use std::sync::Mutex;
23
24use fix::{Simulator, VesselState};
25use nav_math::Position;
26
27pub use route::{FfiChartStore, FfiRouteError};
28
29uniffi::setup_scaffolding!();
30
31/// A geographic position, crossing the FFI boundary.
32#[derive(uniffi::Record)]
33pub struct FfiPosition {
34    /// Degrees north of the equator.
35    pub lat_deg: f64,
36    /// Degrees east of Greenwich.
37    pub lon_deg: f64,
38}
39
40impl From<Position> for FfiPosition {
41    fn from(position: Position) -> Self {
42        Self {
43            lat_deg: position.lat_deg,
44            lon_deg: position.lon_deg,
45        }
46    }
47}
48
49impl From<FfiPosition> for Position {
50    fn from(position: FfiPosition) -> Self {
51        Self::new(position.lat_deg, position.lon_deg)
52    }
53}
54
55/// What a client draws about own ship at one instant.
56///
57/// `predictor_end`/`heading_line_end` are computed once here, via
58/// [`VesselState`]'s own methods, rather than left for a client to derive
59/// -- the whole reason this computation lives in `nav-math` rather than here.
60/// This shape is not incidental: it is exactly what a chart bridge --
61/// each client's own module for it, on whichever platform -- already
62/// expects to send onward.
63#[derive(uniffi::Record)]
64pub struct FfiVesselState {
65    /// Where the vessel is.
66    pub position: FfiPosition,
67    /// The far end of the course predictor.
68    pub predictor_end: FfiPosition,
69    /// The far end of the heading line, when the vessel has a compass.
70    pub heading_line_end: Option<FfiPosition>,
71    /// Course over ground, degrees true.
72    pub cog_deg: f64,
73    /// Speed over ground, knots.
74    pub sog_kn: f64,
75    /// Heading, degrees true, when the vessel has a compass.
76    pub heading_deg: Option<f64>,
77}
78
79impl From<VesselState> for FfiVesselState {
80    fn from(state: VesselState) -> Self {
81        Self {
82            position: state.position.into(),
83            predictor_end: state.predictor_end().into(),
84            heading_line_end: state.heading_line_end().map(Into::into),
85            cog_deg: state.cog_deg,
86            sog_kn: state.sog_kn,
87            heading_deg: state.heading_deg,
88        }
89    }
90}
91
92/// A vessel with nothing real behind it, for a client to have something to
93/// draw before any real source exists -- see [`Simulator`]'s own doc.
94///
95/// Wraps the state in a mutex rather than requiring `&mut self`: a UniFFI
96/// object is handed to the foreign side as one shared reference, not
97/// something a caller on that side can hold `mut`.
98#[derive(uniffi::Object)]
99pub struct FfiSimulator {
100    inner: Mutex<Simulator>,
101}
102
103#[uniffi::export]
104impl FfiSimulator {
105    /// A vessel under way in the Gulf of Trieste, where this project's
106    /// charts have coverage.
107    #[uniffi::constructor]
108    #[must_use]
109    pub fn new() -> Self {
110        Self {
111            inner: Mutex::new(Simulator::new()),
112        }
113    }
114
115    /// Advances the vessel by `dt_seconds` and returns the new state.
116    #[must_use]
117    pub fn tick(&self, dt_seconds: f64) -> FfiVesselState {
118        let mut simulator = self.inner.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
119        simulator.tick(std::time::Duration::from_secs_f64(dt_seconds)).into()
120    }
121}
122
123impl Default for FfiSimulator {
124    fn default() -> Self {
125        Self::new()
126    }
127}