use serde::{Deserialize, Serialize};
pub const KN_TO_MPS: f64 = 1852.0 / 3600.0;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PlatformKind {
#[default]
Static,
Vessel,
}
pub const DEFAULT_MAX_SPEED_KN: f64 = 30.0;
pub const DEFAULT_MAX_ACCEL_MPS2: f64 = 0.5;
pub const DEFAULT_MAX_TURN_RATE_DPS: f64 = 6.0;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PlatformCfg {
#[serde(default)]
pub kind: PlatformKind,
#[serde(default)]
pub max_speed_kn: Option<f64>,
#[serde(default)]
pub max_accel_mps2: Option<f64>,
#[serde(default)]
pub max_turn_rate_dps: Option<f64>,
#[serde(default)]
pub antenna_height_m: Option<f64>,
#[serde(default)]
pub heading_sensor: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct VesselLimits {
pub max_speed_mps: f64,
pub max_accel_mps2: f64,
pub max_turn_rate_dps: f64,
pub antenna_height_m: Option<f64>,
pub heading_sensor: bool,
}
impl PlatformCfg {
pub fn is_static(&self) -> bool {
*self == PlatformCfg::default()
}
pub fn is_vessel(&self) -> bool {
self.kind == PlatformKind::Vessel
}
pub fn limits(&self) -> Option<VesselLimits> {
self.is_vessel().then(|| VesselLimits {
max_speed_mps: self.max_speed_kn.unwrap_or(DEFAULT_MAX_SPEED_KN) * KN_TO_MPS,
max_accel_mps2: self.max_accel_mps2.unwrap_or(DEFAULT_MAX_ACCEL_MPS2),
max_turn_rate_dps: self.max_turn_rate_dps.unwrap_or(DEFAULT_MAX_TURN_RATE_DPS),
antenna_height_m: self.antenna_height_m,
heading_sensor: self.heading_sensor,
})
}
pub fn validate(&self) -> Result<(), String> {
let bounded = |name: &str, v: Option<f64>, lo: f64, hi: f64| -> Result<(), String> {
match v {
Some(x) if !(x.is_finite() && x > lo && x <= hi) => Err(format!(
"platform: {name} must be in ({lo}, {hi}] (got {x})"
)),
_ => Ok(()),
}
};
if !self.is_vessel() {
let stray = [
("max_speed_kn", self.max_speed_kn.is_some()),
("max_accel_mps2", self.max_accel_mps2.is_some()),
("max_turn_rate_dps", self.max_turn_rate_dps.is_some()),
("antenna_height_m", self.antenna_height_m.is_some()),
("heading_sensor", self.heading_sensor),
];
if let Some((name, _)) = stray.iter().find(|(_, set)| *set) {
return Err(format!(
"platform: {name} applies only to kind = \"vessel\" (kind is \"static\")"
));
}
return Ok(());
}
bounded("max_speed_kn", self.max_speed_kn, 0.0, 100.0)?;
bounded("max_accel_mps2", self.max_accel_mps2, 0.0, 20.0)?;
bounded("max_turn_rate_dps", self.max_turn_rate_dps, 0.0, 180.0)?;
bounded("antenna_height_m", self.antenna_height_m, 0.0, 200.0)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(s: &str) -> Result<PlatformCfg, toml::de::Error> {
toml::from_str(s)
}
#[test]
fn default_is_static_and_has_no_limits() {
let p = PlatformCfg::default();
assert!(p.is_static() && !p.is_vessel());
assert!(p.limits().is_none());
assert!(p.validate().is_ok());
}
#[test]
fn vessel_defaults_are_the_documented_ones() {
let p = parse("kind = \"vessel\"").unwrap();
p.validate().unwrap();
let l = p.limits().unwrap();
assert!((l.max_speed_mps - 30.0 * KN_TO_MPS).abs() < 1e-12);
assert_eq!(l.max_accel_mps2, DEFAULT_MAX_ACCEL_MPS2);
assert_eq!(l.max_turn_rate_dps, DEFAULT_MAX_TURN_RATE_DPS);
assert_eq!(l.antenna_height_m, None);
assert!(!l.heading_sensor);
}
#[test]
fn stated_limits_override_defaults() {
let p = parse(
"kind = \"vessel\"\nmax_speed_kn = 20.0\nmax_accel_mps2 = 0.2\n\
max_turn_rate_dps = 3.0\nantenna_height_m = 18.0\nheading_sensor = true",
)
.unwrap();
p.validate().unwrap();
let l = p.limits().unwrap();
assert!((l.max_speed_mps - 20.0 * KN_TO_MPS).abs() < 1e-12);
assert_eq!(
(l.max_accel_mps2, l.max_turn_rate_dps, l.antenna_height_m),
(0.2, 3.0, Some(18.0))
);
assert!(l.heading_sensor);
}
#[test]
fn impossible_limits_and_stray_keys_are_rejected() {
for bad in [
"kind = \"vessel\"\nmax_speed_kn = 0.0",
"kind = \"vessel\"\nmax_speed_kn = 250.0",
"kind = \"vessel\"\nmax_accel_mps2 = -1.0",
"kind = \"vessel\"\nmax_turn_rate_dps = 400.0",
"kind = \"vessel\"\nantenna_height_m = 0.0",
"kind = \"static\"\nmax_speed_kn = 20.0",
"heading_sensor = true",
] {
assert!(parse(bad).unwrap().validate().is_err(), "{bad}");
}
assert!(parse("kind = \"submarine\"").is_err());
assert!(parse("kind = \"vessel\"\nspeed = 3").is_err());
}
#[test]
fn a_vessel_is_not_static_for_serialisation() {
assert!(!parse("kind = \"vessel\"").unwrap().is_static());
}
}