use core::fmt;
use kinavis_kernel::angle::TrueCourse;
use kinavis_kernel::event::TargetId;
use kinavis_kernel::position::Position;
use kinavis_kernel::snapshot::GroundTrack;
use kinavis_kernel::units::{RateOfTurn, Speed};
use kinavis_kernel::InlineStr;
use crate::aton::AidToNavigation;
use crate::bits::Bits;
use crate::error::AisError;
use crate::fields::{byte, course, heading, position, second, speed, value_of, Fields};
use crate::static_data::{StaticAndVoyageData, StaticDataReport};
use crate::vessel::{Dimensions, PositionFixingDevice, ShipType};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Message {
PositionReport(PositionReport),
StaticAndVoyageData(StaticAndVoyageData),
StaticDataReport(StaticDataReport),
AidToNavigation(AidToNavigation),
Unsupported {
kind: u8,
},
}
impl Message {
pub fn decode(bits: &Bits) -> Result<Self, AisError> {
let kind = bits.unsigned(0, 6).ok_or(AisError::TooShort {
bits: bits.len(),
needed: 6,
})?;
let kind = byte(kind);
match kind {
1..=3 => PositionReport::class_a(bits, kind).map(Self::PositionReport),
5 => StaticAndVoyageData::decode(bits).map(Self::StaticAndVoyageData),
18 => PositionReport::class_b(bits, ClassB::STANDARD).map(Self::PositionReport),
19 => PositionReport::class_b(bits, ClassB::EXTENDED).map(Self::PositionReport),
21 => AidToNavigation::decode(bits).map(Self::AidToNavigation),
24 => Ok(StaticDataReport::decode(bits)?
.map_or(Self::Unsupported { kind }, Self::StaticDataReport)),
_ => Ok(Self::Unsupported { kind }),
}
}
#[must_use]
pub const fn mmsi(&self) -> Option<TargetId> {
match self {
Self::PositionReport(report) => Some(report.mmsi),
Self::StaticAndVoyageData(data) => Some(data.mmsi),
Self::StaticDataReport(report) => Some(report.mmsi),
Self::AidToNavigation(aid) => Some(aid.mmsi),
Self::Unsupported { .. } => None,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum StationClass {
A,
B,
}
impl fmt::Display for StationClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::A => "class A",
Self::B => "class B",
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NavigationStatus {
UnderWayUsingEngine,
AtAnchor,
NotUnderCommand,
RestrictedManoeuvrability,
ConstrainedByDraught,
Moored,
Aground,
Fishing,
UnderWaySailing,
TowingAstern,
PushingAheadOrTowingAlongside,
SearchAndRescueTransmitter,
Reserved(u8),
}
impl NavigationStatus {
#[must_use]
pub const fn from_code(code: u8) -> Option<Self> {
Some(match code {
0 => Self::UnderWayUsingEngine,
1 => Self::AtAnchor,
2 => Self::NotUnderCommand,
3 => Self::RestrictedManoeuvrability,
4 => Self::ConstrainedByDraught,
5 => Self::Moored,
6 => Self::Aground,
7 => Self::Fishing,
8 => Self::UnderWaySailing,
11 => Self::TowingAstern,
12 => Self::PushingAheadOrTowingAlongside,
14 => Self::SearchAndRescueTransmitter,
9 | 10 | 13 => Self::Reserved(code),
_ => return None,
})
}
#[must_use]
pub const fn code(self) -> u8 {
match self {
Self::UnderWayUsingEngine => 0,
Self::AtAnchor => 1,
Self::NotUnderCommand => 2,
Self::RestrictedManoeuvrability => 3,
Self::ConstrainedByDraught => 4,
Self::Moored => 5,
Self::Aground => 6,
Self::Fishing => 7,
Self::UnderWaySailing => 8,
Self::TowingAstern => 11,
Self::PushingAheadOrTowingAlongside => 12,
Self::SearchAndRescueTransmitter => 14,
Self::Reserved(code) => code,
}
}
}
impl fmt::Display for NavigationStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnderWayUsingEngine => f.write_str("under way using engine"),
Self::AtAnchor => f.write_str("at anchor"),
Self::NotUnderCommand => f.write_str("not under command"),
Self::RestrictedManoeuvrability => f.write_str("restricted manoeuvrability"),
Self::ConstrainedByDraught => f.write_str("constrained by draught"),
Self::Moored => f.write_str("moored"),
Self::Aground => f.write_str("aground"),
Self::Fishing => f.write_str("engaged in fishing"),
Self::UnderWaySailing => f.write_str("under way sailing"),
Self::TowingAstern => f.write_str("towing astern"),
Self::PushingAheadOrTowingAlongside => f.write_str("pushing ahead or towing alongside"),
Self::SearchAndRescueTransmitter => f.write_str("search and rescue transmitter"),
Self::Reserved(code) => write!(f, "reserved status {code}"),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Turn {
Rate(RateOfTurn),
OffScale {
to_port: bool,
},
}
impl Turn {
const SCALE: f64 = 4.733;
fn from_field(value: i32) -> Result<Option<Self>, AisError> {
Ok(Some(match value {
-128 => return Ok(None),
127 => Self::OffScale { to_port: false },
-127 => Self::OffScale { to_port: true },
_ => {
let scaled = f64::from(value) / Self::SCALE;
let magnitude = scaled * scaled;
let rate = if value < 0 { -magnitude } else { magnitude };
Self::Rate(
RateOfTurn::from_degrees_per_minute(rate).map_err(value_of("rate of turn"))?,
)
}
}))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PositionReport {
pub kind: u8,
pub mmsi: TargetId,
pub status: Option<NavigationStatus>,
pub turn: Option<Turn>,
pub speed: Option<Speed>,
pub accurate: bool,
pub position: Option<Position>,
pub course: Option<TrueCourse>,
pub heading: Option<TrueCourse>,
pub second: Option<u8>,
pub raim: bool,
pub name: Option<InlineStr<20>>,
pub ship_type: Option<ShipType>,
pub dimensions: Option<Dimensions>,
pub fixing_device: Option<PositionFixingDevice>,
}
#[derive(Clone, Copy)]
struct ClassB {
needed: usize,
raim: usize,
}
impl ClassB {
const STANDARD: Self = Self {
needed: 168,
raim: 147,
};
const EXTENDED: Self = Self {
needed: 312,
raim: 305,
};
}
impl PositionReport {
fn class_a(bits: &Bits, kind: u8) -> Result<Self, AisError> {
let field = Fields::of(bits, 168)?;
Ok(Self {
kind,
mmsi: TargetId::new(field.unsigned(8, 30)),
status: NavigationStatus::from_code(byte(field.unsigned(38, 4))),
turn: Turn::from_field(field.signed(42, 8))?,
speed: speed(field.unsigned(50, 10))?,
accurate: field.bit(60),
position: position(field.signed(61, 28), field.signed(89, 27))?,
course: course(field.unsigned(116, 12))?,
heading: heading(field.unsigned(128, 9))?,
second: second(field.unsigned(137, 6)),
raim: field.bit(148),
name: None,
ship_type: None,
dimensions: None,
fixing_device: None,
})
}
fn class_b(bits: &Bits, layout: ClassB) -> Result<Self, AisError> {
let field = Fields::of(bits, layout.needed)?;
let extended = layout.needed == ClassB::EXTENDED.needed;
Ok(Self {
kind: byte(field.unsigned(0, 6)),
mmsi: TargetId::new(field.unsigned(8, 30)),
status: None,
turn: None,
speed: speed(field.unsigned(46, 10))?,
accurate: field.bit(56),
position: position(field.signed(57, 28), field.signed(85, 27))?,
course: course(field.unsigned(112, 12))?,
heading: heading(field.unsigned(124, 9))?,
second: second(field.unsigned(133, 6)),
raim: field.bit(layout.raim),
name: extended.then(|| field.text(143, 20)).flatten(),
ship_type: extended
.then(|| ShipType::from_code(byte(field.unsigned(263, 8))))
.flatten(),
dimensions: if extended {
Dimensions::from_fields(
field.unsigned(271, 9),
field.unsigned(280, 9),
field.unsigned(289, 6),
field.unsigned(295, 6),
)?
} else {
None
},
fixing_device: extended
.then(|| PositionFixingDevice::from_code(byte(field.unsigned(301, 4))))
.flatten(),
})
}
#[must_use]
pub const fn station_class(&self) -> StationClass {
match self.kind {
18 | 19 => StationClass::B,
_ => StationClass::A,
}
}
#[must_use]
pub fn ground_track(&self) -> Option<GroundTrack> {
Some(GroundTrack {
course_over_ground: self.course?,
speed_over_ground: self.speed?,
})
}
}