use super::ais::Message;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Country {
pub country: String,
pub flag: String,
#[serde(rename = "iso-3166-1")]
pub iso_3166_1: String,
}
impl Default for Country {
fn default() -> Self {
Self {
country: "Unknown".to_string(),
flag: "🏳️".to_string(),
iso_3166_1: "XX".to_string(),
}
}
}
pub static DEFAULT_COUNTRY: Lazy<Country> = Lazy::new(Country::default);
const MID_JSON: &str = include_str!("../../data/country_mid.json");
pub static COUNTRY_MID: Lazy<HashMap<String, Country>> =
Lazy::new(|| serde_json::from_str(MID_JSON).unwrap());
fn country_from_mid(mid: u16) -> &'static Country {
if let Some(country) = COUNTRY_MID.get(&mid.to_string()) {
country
} else {
&DEFAULT_COUNTRY
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum MmsiType {
#[serde(rename = "Coast Station")]
CoastStation,
#[serde(rename = "Group of Ships")]
GroupOfShips,
#[serde(rename = "SAR Aircraft")]
SarAircraft,
#[serde(rename = "AIS AtoN")]
AisAton,
#[serde(rename = "AIS SART/MOB/EPIRB")]
AisSartMobEpirb,
#[serde(rename = "Ship")]
StandardShipStation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MmsiInfo {
pub mmsi_type: MmsiType,
#[serde(flatten)]
pub country: Country,
}
impl MmsiInfo {
pub fn new(mmsi_type: MmsiType, country: Country) -> Self {
Self { mmsi_type, country }
}
pub fn from_message(msg: &Message) -> Result<Self, String> {
Self::from_mmsi(msg.mmsi())
}
pub fn from_mmsi(mmsi_num: u32) -> Result<Self, String> {
let mmsi_str = format!("{:09}", mmsi_num);
let prefix3: u16 = mmsi_str[0..3].parse().unwrap_or(0);
if mmsi_str.starts_with("00") {
let mid: u16 = mmsi_str[2..5].parse().unwrap_or(0);
if (200..800).contains(&mid) {
return Ok(MmsiInfo::new(
MmsiType::CoastStation,
country_from_mid(mid).clone(),
));
} else {
return Err(format!("Invalid MID ({}) for coast station", mid));
}
}
if mmsi_str.starts_with('0') && !mmsi_str.starts_with("00") {
let mid: u16 = mmsi_str[1..4].parse().unwrap_or(0);
if (200..800).contains(&mid) {
return Ok(MmsiInfo::new(
MmsiType::GroupOfShips,
country_from_mid(mid).clone(),
));
} else {
return Err(format!("Invalid MID ({}) for group", mid));
}
}
if mmsi_str.starts_with("111") {
let mid: u16 = mmsi_str[3..6].parse().unwrap_or(0);
if (200..800).contains(&mid) {
return Ok(MmsiInfo::new(
MmsiType::SarAircraft,
country_from_mid(mid).clone(),
));
} else {
return Err(format!("Invalid MID ({}) for SAR aircraft", mid));
}
}
if mmsi_str.starts_with("99") {
let mid: u16 = mmsi_str[2..5].parse().unwrap_or(0);
let country = country_from_mid(mid);
if country.country == "Unknown" {
return Err(format!("Invalid MID ({}) for AtoN", mid));
}
return Ok(MmsiInfo::new(MmsiType::AisAton, country.clone()));
}
if (970..980).contains(&prefix3) {
return Ok(MmsiInfo::new(
MmsiType::AisSartMobEpirb,
DEFAULT_COUNTRY.clone(),
));
}
let mid: u16 = prefix3;
if (200..800).contains(&mid) {
return Ok(MmsiInfo::new(
MmsiType::StandardShipStation,
country_from_mid(mid).clone(),
));
}
Err(format!(
"Invalid or unrecognized MMSI (prefix = {:?})",
prefix3
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_mmsi_various_formats() {
if MmsiInfo::from_mmsi(0).is_ok() {
panic!("Expected Err for MMSI 0, got Ok");
}
if let Ok(info) = MmsiInfo::from_mmsi(227456789u32) {
assert_eq!(info.country.country, "France");
assert_eq!(info.mmsi_type, MmsiType::StandardShipStation);
} else {
panic!("Expected Ok for MMSI 227456789, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(2270001u32) {
assert_eq!(info.country.country, "France");
assert_eq!(info.mmsi_type, MmsiType::CoastStation);
} else {
panic!("Expected Ok for MMSI 2270001, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(111227001u32) {
assert_eq!(info.country.country, "France");
assert_eq!(info.mmsi_type, MmsiType::SarAircraft);
} else {
panic!("Expected Ok for MMSI 111227001, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(992270001u32) {
assert_eq!(info.country.country, "France");
assert_eq!(info.mmsi_type, MmsiType::AisAton);
} else {
panic!("Expected Ok for MMSI 992270001, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(974123456u32) {
assert_eq!(info.country.country, "Unknown");
assert_eq!(info.mmsi_type, MmsiType::AisSartMobEpirb);
} else {
panic!("Expected Ok for MMSI 974123456, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(22745600u32) {
assert_eq!(info.country.country, "France");
assert_eq!(info.mmsi_type, MmsiType::GroupOfShips);
} else {
panic!("Expected Ok for MMSI 22745600, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(338123456u32) {
assert_eq!(info.country.country, "United States");
assert_eq!(info.mmsi_type, MmsiType::StandardShipStation);
} else {
panic!("Expected Ok for MMSI 338123456, got Err");
}
if MmsiInfo::from_mmsi(123456789u32).is_err() {
} else {
panic!("Expected Err for MMSI 123456789, got Ok");
}
if MmsiInfo::from_mmsi(999999999u32).is_err() {
} else {
panic!("Expected Err for MMSI 999999999, got Ok");
}
if let Ok(info) = MmsiInfo::from_mmsi(200000000u32) {
assert_eq!(info.mmsi_type, MmsiType::StandardShipStation);
} else {
panic!("Expected Ok for MMSI 200000000, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(799999999u32) {
assert_eq!(info.mmsi_type, MmsiType::StandardShipStation);
} else {
panic!("Expected Ok for MMSI 799999999, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(970000001u32) {
assert_eq!(info.mmsi_type, MmsiType::AisSartMobEpirb);
} else {
panic!("Expected Ok for MMSI 970000001, got Err");
}
if let Ok(info) = MmsiInfo::from_mmsi(979999999u32) {
assert_eq!(info.mmsi_type, MmsiType::AisSartMobEpirb);
} else {
panic!("Expected Ok for MMSI 979999999, got Err");
}
if MmsiInfo::from_mmsi(800000000u32).is_err() {
} else {
panic!("Expected Err for MMSI 800000000, got Ok");
}
if MmsiInfo::from_mmsi(199999999u32).is_err() {
} else {
panic!("Expected Err for MMSI 199999999, got Ok");
}
}
}