Skip to main content

kinavis_ais/
aton.rs

1//! Aid-to-navigation report, message 21: buoys, beacons, lights, and virtual
2//! aids broadcast by a shore station.
3
4use core::fmt;
5
6use kinavis_kernel::event::TargetId;
7use kinavis_kernel::position::Position;
8use kinavis_kernel::InlineStr;
9
10use crate::bits::Bits;
11use crate::error::AisError;
12use crate::fields::{byte, position, second, Fields};
13use crate::vessel::{Dimensions, PositionFixingDevice};
14
15/// Name field: 20 characters; name extension at the end of the message: up to
16/// 14 more.
17const NAME_CHARS: usize = 20;
18const EXTENSION_CHARS: usize = 14;
19const ALL_NAME_CHARS: usize = NAME_CHARS + EXTENSION_CHARS;
20
21/// Aid-to-navigation report.
22// Independent flags, each a field of the standard's table; not a state machine.
23#[allow(clippy::struct_excessive_bools)]
24#[derive(Debug, Clone, Copy, PartialEq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct AidToNavigation {
27    /// MMSI.
28    pub mmsi: TargetId,
29    /// Aid type.
30    pub aid_type: Option<AidType>,
31    /// Name, up to 34 characters: 20 in the name field plus the extension.
32    pub name: Option<InlineStr<ALL_NAME_CHARS>>,
33    /// Position accuracy flag: DGNSS, better than 10 m.
34    pub accurate: bool,
35    /// Position, 1/10 000 minute resolution.
36    pub position: Option<Position>,
37    /// Dimensions around the position; none for a virtual aid.
38    pub dimensions: Option<Dimensions>,
39    /// Position fixing device; `Surveyed` for a fixed aid.
40    pub fixing_device: Option<PositionFixingDevice>,
41    /// UTC second of the report, `0..=59`; `None` if no time is available or
42    /// the position is dead-reckoned or manual.
43    pub second: Option<u8>,
44    /// Off-position flag for a floating aid. Defined only when the second is
45    /// valid; otherwise `None`.
46    pub off_position: Option<bool>,
47    /// 8 bits reserved for regional use.
48    pub regional: u8,
49    /// RAIM flag.
50    pub raim: bool,
51    /// Virtual aid: broadcast from elsewhere, nothing physical at the position.
52    pub is_virtual: bool,
53    /// Assigned mode: transmitting on a schedule set by a competent authority.
54    pub assigned: bool,
55}
56
57impl AidToNavigation {
58    /// Fixed part: 272 bits, followed by up to 14 characters of name extension.
59    const NEEDED: usize = 272;
60
61    pub(crate) fn decode(bits: &Bits) -> Result<Self, AisError> {
62        let field = Fields::of(bits, Self::NEEDED)?;
63        let second = second(field.unsigned(253, 6));
64        Ok(Self {
65            mmsi: TargetId::new(field.unsigned(8, 30)),
66            aid_type: AidType::from_code(byte(field.unsigned(38, 5))),
67            name: name(&field),
68            accurate: field.bit(163),
69            position: position(field.signed(164, 28), field.signed(192, 27))?,
70            dimensions: Dimensions::from_fields(
71                field.unsigned(219, 9),
72                field.unsigned(228, 9),
73                field.unsigned(237, 6),
74                field.unsigned(243, 6),
75            )?,
76            fixing_device: PositionFixingDevice::from_code(byte(field.unsigned(249, 4))),
77            second,
78            off_position: second.map(|_| field.bit(259)),
79            regional: byte(field.unsigned(260, 8)),
80            raim: field.bit(268),
81            is_virtual: field.bit(269),
82            assigned: field.bit(270),
83        })
84    }
85}
86
87/// Name from the name field plus any whole characters past the fixed part.
88fn name(field: &Fields<'_>) -> Option<InlineStr<ALL_NAME_CHARS>> {
89    let extension = ((field.len() - AidToNavigation::NEEDED) / 6).min(EXTENSION_CHARS);
90    let in_field = (0..NAME_CHARS).map(|index| 43 + index * 6);
91    let in_extension = (0..extension).map(|index| AidToNavigation::NEEDED + index * 6);
92    field.text_at(in_field.chain(in_extension))
93}
94
95/// Aid-to-navigation type.
96///
97/// `#[non_exhaustive]`; match with a wildcard arm.
98#[non_exhaustive]
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub enum AidType {
102    /// Reference point, 1.
103    ReferencePoint,
104    /// RACON, 2.
105    Racon,
106    /// Fixed offshore structure (platform, wind farm), 3.
107    FixedStructureOffshore,
108    /// Light, with or without sectors, 5 and 6.
109    Light {
110        /// Sector light.
111        sectored: bool,
112    },
113    /// Leading light, front or rear, 7 and 8.
114    LeadingLight {
115        /// Rear light of the pair.
116        rear: bool,
117    },
118    /// Fixed beacon, `9..=19`.
119    Beacon(Mark),
120    /// Floating mark (buoy), `20..=30`.
121    Floating(Mark),
122    /// Light vessel, LANBY or rig, 31.
123    LightVessel,
124    /// Reserved code, kept as received.
125    Reserved(u8),
126}
127
128impl AidType {
129    /// Type for a code; `None` for 0 ("not specified").
130    #[must_use]
131    pub const fn from_code(code: u8) -> Option<Self> {
132        Some(match code {
133            0 => return None,
134            1 => Self::ReferencePoint,
135            2 => Self::Racon,
136            3 => Self::FixedStructureOffshore,
137            5 => Self::Light { sectored: false },
138            6 => Self::Light { sectored: true },
139            7 => Self::LeadingLight { rear: false },
140            8 => Self::LeadingLight { rear: true },
141            9..=19 => Self::Beacon(Mark::from_offset(code - 9)),
142            20..=30 => Self::Floating(Mark::from_offset(code - 20)),
143            31 => Self::LightVessel,
144            _ => Self::Reserved(code),
145        })
146    }
147
148    /// Code for the type.
149    #[must_use]
150    pub const fn code(self) -> u8 {
151        match self {
152            Self::ReferencePoint => 1,
153            Self::Racon => 2,
154            Self::FixedStructureOffshore => 3,
155            Self::Light { sectored: false } => 5,
156            Self::Light { sectored: true } => 6,
157            Self::LeadingLight { rear: false } => 7,
158            Self::LeadingLight { rear: true } => 8,
159            Self::Beacon(mark) => 9 + mark.offset(),
160            Self::Floating(mark) => 20 + mark.offset(),
161            Self::LightVessel => 31,
162            Self::Reserved(code) => code,
163        }
164    }
165}
166
167impl fmt::Display for AidType {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            Self::ReferencePoint => f.write_str("reference point"),
171            Self::Racon => f.write_str("RACON"),
172            Self::FixedStructureOffshore => f.write_str("fixed structure off shore"),
173            Self::Light { sectored: false } => f.write_str("light without sectors"),
174            Self::Light { sectored: true } => f.write_str("light with sectors"),
175            Self::LeadingLight { rear: false } => f.write_str("leading light, front"),
176            Self::LeadingLight { rear: true } => f.write_str("leading light, rear"),
177            Self::Beacon(mark) => write!(f, "beacon, {mark}"),
178            Self::Floating(mark) => write!(f, "floating mark, {mark}"),
179            Self::LightVessel => f.write_str("light vessel"),
180            Self::Reserved(code) => write!(f, "reserved aid type {code}"),
181        }
182    }
183}
184
185/// IALA mark type of a beacon or buoy.
186///
187/// `#[non_exhaustive]`; match with a wildcard arm.
188#[non_exhaustive]
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
190#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
191pub enum Mark {
192    /// Cardinal mark: safe water on the named side.
193    Cardinal(Quadrant),
194    /// Port-hand lateral mark.
195    PortHand,
196    /// Starboard-hand lateral mark.
197    StarboardHand,
198    /// Preferred channel to starboard: pass as a port-hand mark.
199    PreferredChannelPortHand,
200    /// Preferred channel to port: pass as a starboard-hand mark.
201    PreferredChannelStarboardHand,
202    /// Isolated danger mark.
203    IsolatedDanger,
204    /// Safe water mark: fairway or landfall.
205    SafeWater,
206    /// Special mark.
207    Special,
208}
209
210impl Mark {
211    /// Mark at an offset into either code range, `0..=10`.
212    const fn from_offset(offset: u8) -> Self {
213        match offset {
214            0 => Self::Cardinal(Quadrant::North),
215            1 => Self::Cardinal(Quadrant::East),
216            2 => Self::Cardinal(Quadrant::South),
217            3 => Self::Cardinal(Quadrant::West),
218            4 => Self::PortHand,
219            5 => Self::StarboardHand,
220            6 => Self::PreferredChannelPortHand,
221            7 => Self::PreferredChannelStarboardHand,
222            8 => Self::IsolatedDanger,
223            9 => Self::SafeWater,
224            _ => Self::Special,
225        }
226    }
227
228    const fn offset(self) -> u8 {
229        match self {
230            Self::Cardinal(Quadrant::North) => 0,
231            Self::Cardinal(Quadrant::East) => 1,
232            Self::Cardinal(Quadrant::South) => 2,
233            Self::Cardinal(Quadrant::West) => 3,
234            Self::PortHand => 4,
235            Self::StarboardHand => 5,
236            Self::PreferredChannelPortHand => 6,
237            Self::PreferredChannelStarboardHand => 7,
238            Self::IsolatedDanger => 8,
239            Self::SafeWater => 9,
240            Self::Special => 10,
241        }
242    }
243}
244
245impl fmt::Display for Mark {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        match self {
248            Self::Cardinal(quadrant) => write!(f, "cardinal {quadrant}"),
249            Self::PortHand => f.write_str("port hand"),
250            Self::StarboardHand => f.write_str("starboard hand"),
251            Self::PreferredChannelPortHand => f.write_str("preferred channel, port hand"),
252            Self::PreferredChannelStarboardHand => f.write_str("preferred channel, starboard hand"),
253            Self::IsolatedDanger => f.write_str("isolated danger"),
254            Self::SafeWater => f.write_str("safe water"),
255            Self::Special => f.write_str("special"),
256        }
257    }
258}
259
260/// Cardinal quadrant: the side of the danger where safe water lies.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
262#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
263pub enum Quadrant {
264    /// Pass north of the mark.
265    North,
266    /// Pass east.
267    East,
268    /// Pass south.
269    South,
270    /// Pass west.
271    West,
272}
273
274impl fmt::Display for Quadrant {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        f.write_str(match self {
277            Self::North => "north",
278            Self::East => "east",
279            Self::South => "south",
280            Self::West => "west",
281        })
282    }
283}