Skip to main content

kinavis_colregs/
lib.rs

1//! COLREGs steering and sailing rules: encounter type, responsibility,
2//! permitted manoeuvre.
3//!
4//! CPA/TCPA is kinematics and lives in the collision assessment; this crate
5//! holds the rules. Their sector widths are conventions and their application
6//! depends on the waters (TSS, narrow channels), so they are versioned
7//! separately, the conventions are explicit in [`ColregsConfig`], and every
8//! answer names the deciding [`Rule`].
9//!
10//! # Rules applied
11//!
12//! Vessels in sight of one another ([`Visibility::InSight`]):
13//!
14//! - **Rule 13**, overtaking: a vessel coming up from more than 22.5° abaft the
15//!   other's beam keeps out of the way, whatever the categories. *Coming up*
16//!   requires a closing range; astern and opening is treated as crossing
17//!   geometry.
18//! - **Rule 18**, responsibilities: power-driven gives way to sailing, fishing,
19//!   RAM and NUC, in that order of precedence; a vessel constrained by her
20//!   draught is not to be impeded.
21//! - **Rule 14**, head-on: two power-driven vessels on reciprocal or nearly
22//!   reciprocal courses both alter to starboard.
23//! - **Rule 15**, crossing: the power-driven vessel with the other on her
24//!   starboard side gives way.
25//! - **Rule 12**, sailing vessels: port tack keeps clear of starboard tack; on
26//!   the same tack, windward keeps clear of leeward.
27//! - **Rule 17**, stand-on vessel: keeps course and speed; may act if the
28//!   give-way vessel does not; does not alter to port for a vessel on her port
29//!   side.
30//!
31//! Restricted visibility ([`Visibility::Restricted`]), **Rule 19**: no stand-on
32//! vessel; no alteration to port for a vessel forward of the beam, none towards
33//! a vessel abeam or abaft the beam.
34//!
35//! Rules 9 and 10 (narrow channels, TSS) are **not** applied. Between two
36//! vessels of the same category other than power-driven or sailing (e.g. two
37//! fishing vessels) the rules are silent; the head-on and crossing geometry is
38//! applied and reported as Rule 14 or 15.
39//!
40//! ```rust
41//! use kinavis::relative_motion::{Contact, Vessel};
42//! use kinavis_colregs::{
43//!     rule_of_the_road, ColregsConfig, Encounter, Party, Responsibility, Situation,
44//!     VesselCategory, Visibility,
45//! };
46//! use kinavis_kernel::{Distance, Side, Speed, TrueBearing, TrueCourse};
47//!
48//! // Steering north at twelve knots; a power-driven vessel five miles off
49//! // on the starboard bow, steering west at the same speed.
50//! let own = Party::new(
51//!     Vessel { course: TrueCourse::new(0.0)?, speed: Speed::from_knots(12.0)? },
52//!     VesselCategory::PowerDriven,
53//! );
54//! let target = Party::new(
55//!     Vessel { course: TrueCourse::new(270.0)?, speed: Speed::from_knots(12.0)? },
56//!     VesselCategory::PowerDriven,
57//! );
58//! let contact = Contact {
59//!     bearing: TrueBearing::new(45.0)?,
60//!     range: Distance::from_nautical_miles(5.0)?,
61//! };
62//! let situation = Situation::new(own, target, contact, Visibility::InSight);
63//!
64//! let ruling = rule_of_the_road(&situation, &ColregsConfig::STANDARD)?;
65//! assert_eq!(ruling.encounter(), Encounter::Crossing { target_on: Side::Starboard });
66//! assert_eq!(ruling.responsibility(), Responsibility::GiveWay);
67//! assert_eq!(format!("{}", ruling.rule()), "Rule 15");
68//! // Give way by altering to starboard and passing astern; not to port,
69//! // which would cross ahead.
70//! let may = ruling.manoeuvre();
71//! assert!(may.starboard && !may.port && may.slow_down && !may.hold);
72//!
73//! // A sailing vessel would stand on regardless of geometry.
74//! let sailing = Situation::new(
75//!     own,
76//!     Party::new(target.motion(), VesselCategory::Sailing { tack: None }),
77//!     contact,
78//!     Visibility::InSight,
79//! );
80//! let ruling = rule_of_the_road(&sailing, &ColregsConfig::STANDARD)?;
81//! assert_eq!(ruling.responsibility(), Responsibility::GiveWay);
82//! assert_eq!(format!("{}", ruling.rule()), "Rule 18");
83//! # Ok::<(), kinavis_kernel::KernelError>(())
84//! ```
85//!
86//! # Feature flags
87//!
88//! - `std` *(default)* — standard library maths in the kernel.
89//! - `libm` — for `no_std` targets: `--no-default-features --features libm`.
90//! - `serde` — serialisation of the value types.
91//!
92//! No allocation; builds for bare-metal targets.
93
94#![cfg_attr(not(feature = "std"), no_std)]
95
96// The crate does not allocate; tests use `format!`.
97#[cfg(test)]
98extern crate alloc;
99
100mod rules;
101
102use core::fmt;
103
104use kinavis::relative_motion::{Contact, Vessel};
105use kinavis_kernel::angle::{Side, TrueCourse};
106use kinavis_kernel::units::Angle;
107
108pub use rules::rule_of_the_road;
109
110/// Runs the `README.md` example as a doctest.
111#[cfg(doctest)]
112#[doc = include_str!("../README.md")]
113pub struct ReadmeExamples;
114
115/// Side of a sailing vessel the wind is on.
116///
117/// `#[non_exhaustive]`; match with a wildcard arm.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119#[non_exhaustive]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121pub enum Tack {
122    /// Wind on the port side; mainsail carried to starboard.
123    Port,
124    /// Wind on the starboard side.
125    Starboard,
126}
127
128/// Vessel category per Rule 3, ordered as in Rule 18.
129///
130/// `#[non_exhaustive]`; match with a wildcard arm.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
132#[non_exhaustive]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub enum VesselCategory {
135    /// Propelled by machinery; the case Rules 14 and 15 address.
136    PowerDriven,
137    /// Under sail, no machinery in use. The tack matters only between two
138    /// sailing vessels (Rule 12); `None` if unknown.
139    Sailing {
140        /// Wind side, if known.
141        tack: Option<Tack>,
142    },
143    /// Engaged in fishing with gear that restricts manoeuvrability.
144    Fishing,
145    /// Constrained by her draught: not to be impeded, but ranks below Rule 18
146    /// (a)–(c).
147    ConstrainedByDraught,
148    /// Restricted in her ability to manoeuvre (RAM).
149    RestrictedInAbilityToManoeuvre,
150    /// Not under command (NUC).
151    NotUnderCommand,
152}
153
154impl VesselCategory {
155    /// Rule 18 precedence: higher stands on, lower gives way; equal means Rule
156    /// 18 does not decide.
157    #[must_use]
158    pub const fn precedence(self) -> u8 {
159        match self {
160            Self::PowerDriven => 0,
161            Self::Sailing { .. } => 1,
162            Self::Fishing => 2,
163            Self::ConstrainedByDraught => 3,
164            Self::RestrictedInAbilityToManoeuvre => 4,
165            Self::NotUnderCommand => 5,
166        }
167    }
168}
169
170/// One vessel in an encounter: motion and category.
171#[derive(Debug, Clone, Copy, PartialEq)]
172#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173pub struct Party {
174    motion: Vessel,
175    heading: Option<TrueCourse>,
176    category: VesselCategory,
177}
178
179impl Party {
180    /// Vessel of `category` with ground motion `motion`; heading defaults to
181    /// the course over ground until set by [`Party::with_heading`].
182    #[must_use]
183    pub const fn new(motion: Vessel, category: VesselCategory) -> Self {
184        Self {
185            motion,
186            heading: None,
187            category,
188        }
189    }
190
191    /// Sets the heading. The rules refer to the vessel's head, which differs
192    /// from the course over ground under current or leeway.
193    #[must_use]
194    pub const fn with_heading(mut self, heading: TrueCourse) -> Self {
195        self.heading = Some(heading);
196        self
197    }
198
199    /// Course and speed over the ground.
200    #[must_use]
201    pub const fn motion(&self) -> Vessel {
202        self.motion
203    }
204
205    /// Heading: as set, or the course over ground.
206    #[must_use]
207    pub fn heading(&self) -> TrueCourse {
208        self.heading.unwrap_or(self.motion.course)
209    }
210
211    /// Category.
212    #[must_use]
213    pub const fn category(&self) -> VesselCategory {
214        self.category
215    }
216}
217
218/// Whether the vessels are in sight of one another.
219///
220/// `#[non_exhaustive]`; match with a wildcard arm.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
222#[non_exhaustive]
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224pub enum Visibility {
225    /// Visual contact: Section II applies.
226    InSight,
227    /// Fog, mist, falling snow, heavy rain: Rule 19 applies; no stand-on
228    /// vessel.
229    Restricted,
230}
231
232/// Encounter: own ship, the other vessel, its bearing and range, and
233/// conditions.
234#[derive(Debug, Clone, Copy, PartialEq)]
235#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
236pub struct Situation {
237    own: Party,
238    target: Party,
239    contact: Contact,
240    visibility: Visibility,
241    wind_from: Option<TrueCourse>,
242}
243
244impl Situation {
245    /// Own ship and the other vessel at `contact` from own ship.
246    #[must_use]
247    pub const fn new(own: Party, target: Party, contact: Contact, visibility: Visibility) -> Self {
248        Self {
249            own,
250            target,
251            contact,
252            visibility,
253            wind_from: None,
254        }
255    }
256
257    /// Sets the true wind; Rule 12 (a)(ii) needs it to tell windward from
258    /// leeward.
259    #[must_use]
260    pub const fn with_wind_from(mut self, from: TrueCourse) -> Self {
261        self.wind_from = Some(from);
262        self
263    }
264
265    /// Own ship.
266    #[must_use]
267    pub const fn own(&self) -> Party {
268        self.own
269    }
270
271    /// Other vessel.
272    #[must_use]
273    pub const fn target(&self) -> Party {
274        self.target
275    }
276
277    /// Bearing and range of the other vessel from own ship.
278    #[must_use]
279    pub const fn contact(&self) -> Contact {
280        self.contact
281    }
282
283    /// Visibility.
284    #[must_use]
285    pub const fn visibility(&self) -> Visibility {
286        self.visibility
287    }
288
289    /// Direction the true wind blows from, if known.
290    #[must_use]
291    pub const fn wind_from(&self) -> Option<TrueCourse> {
292        self.wind_from
293    }
294}
295
296/// Conventions used to apply the rules.
297///
298/// Rule 13 fixes the overtaking sector; "nearly reciprocal" is left to
299/// judgement. These values encode that judgement and may be set per vessel or
300/// area.
301#[derive(Debug, Clone, Copy, PartialEq)]
302#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
303pub struct ColregsConfig {
304    /// Start of the overtaking sector abaft the beam: 22.5° per Rule 13 (b),
305    /// the arc of the stern light.
306    pub overtaking_abaft_beam: Angle,
307    /// Half-width either side of dead ahead, for both relative bearings, within
308    /// which courses are nearly reciprocal under Rule 14. Customary value 6°.
309    pub head_on_half_width: Angle,
310}
311
312impl ColregsConfig {
313    /// Customary conventions: 22.5° and 6°.
314    pub const STANDARD: Self = Self {
315        overtaking_abaft_beam: Angle::from_degrees_unchecked(22.5),
316        head_on_half_width: Angle::from_degrees_unchecked(6.0),
317    };
318}
319
320/// Encounter type from own ship's side.
321///
322/// `#[non_exhaustive]`; match with a wildcard arm.
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
324#[non_exhaustive]
325#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
326pub enum Encounter {
327    /// Own ship is overtaking the other vessel.
328    Overtaking,
329    /// The other vessel is overtaking own ship.
330    BeingOvertaken,
331    /// Reciprocal or nearly reciprocal courses, each ahead of the other.
332    HeadOn,
333    /// Crossing courses; the other vessel on the given side.
334    Crossing {
335        /// Side of own ship the other vessel bears on.
336        target_on: Side,
337    },
338}
339
340/// Who keeps out of the way.
341///
342/// `#[non_exhaustive]`; match with a wildcard arm.
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
344#[non_exhaustive]
345#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
346pub enum Responsibility {
347    /// Own ship gives way: early and substantial action.
348    GiveWay,
349    /// Own ship stands on and monitors.
350    StandOn,
351    /// Both act: head-on, or restricted visibility.
352    Both,
353    /// Not decidable from the inputs: two sailing vessels on the same tack, no
354    /// wind given.
355    Undetermined,
356}
357
358/// Deciding rule.
359///
360/// `#[non_exhaustive]`; match with a wildcard arm.
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
362#[non_exhaustive]
363#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
364pub enum Rule {
365    /// Sailing vessels.
366    Rule12,
367    /// Overtaking.
368    Rule13,
369    /// Head-on situation.
370    Rule14,
371    /// Crossing situation.
372    Rule15,
373    /// Action by stand-on vessel.
374    Rule17,
375    /// Responsibilities between vessels.
376    Rule18,
377    /// Conduct of vessels in restricted visibility.
378    Rule19,
379}
380
381impl fmt::Display for Rule {
382    /// Formats as `Rule 15`.
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        let number = match self {
385            Self::Rule12 => 12,
386            Self::Rule13 => 13,
387            Self::Rule14 => 14,
388            Self::Rule15 => 15,
389            Self::Rule17 => 17,
390            Self::Rule18 => 18,
391            Self::Rule19 => 19,
392        };
393        write!(f, "Rule {number}")
394    }
395}
396
397/// Constraints the rules place on own ship's manoeuvre.
398///
399/// Not a manoeuvre (that is computed from the geometry): permitted alteration
400/// sides, speed reduction, and whether to hold course and speed.
401// Four independent permissions, not a state machine; hence the lint allowance.
402#[allow(clippy::struct_excessive_bools)]
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
404#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
405pub struct PermittedManoeuvre {
406    /// Alteration to starboard permitted.
407    pub starboard: bool,
408    /// Alteration to port permitted.
409    pub port: bool,
410    /// Speed reduction permitted.
411    pub slow_down: bool,
412    /// Hold course and speed; the permitted alterations apply only if the other
413    /// vessel fails to act.
414    pub hold: bool,
415}
416
417/// Result of applying the rules to a situation.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
419#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
420pub struct Ruling {
421    encounter: Encounter,
422    responsibility: Responsibility,
423    rule: Rule,
424    manoeuvre: PermittedManoeuvre,
425}
426
427impl Ruling {
428    /// Encounter type.
429    #[must_use]
430    pub const fn encounter(&self) -> Encounter {
431        self.encounter
432    }
433
434    /// Who keeps out of the way.
435    #[must_use]
436    pub const fn responsibility(&self) -> Responsibility {
437        self.responsibility
438    }
439
440    /// Rule that decided the responsibility.
441    #[must_use]
442    pub const fn rule(&self) -> Rule {
443        self.rule
444    }
445
446    /// Permitted manoeuvre for own ship.
447    #[must_use]
448    pub const fn manoeuvre(&self) -> PermittedManoeuvre {
449        self.manoeuvre
450    }
451}