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}