use kinavis::relative_motion::Vessel;
use kinavis_kernel::angle::{wrap180, Side};
use kinavis_kernel::error::{ensure_range, Result};
use kinavis_kernel::math;
use crate::{
ColregsConfig, Encounter, PermittedManoeuvre, Responsibility, Rule, Ruling, Situation, Tack,
VesselCategory, Visibility,
};
const DEAD_AHEAD_DEG: f64 = 1e-9;
struct Geometry {
relative: f64,
aspect: f64,
}
pub fn rule_of_the_road(situation: &Situation, config: &ColregsConfig) -> Result<Ruling> {
ensure_range(
"overtaking sector",
config.overtaking_abaft_beam.degrees(),
0.0,
90.0,
)?;
ensure_range(
"head-on half-width",
config.head_on_half_width.degrees(),
0.0,
90.0,
)?;
let geometry = geometry_of(situation);
let encounter = encounter_of(&geometry, situation, config);
if situation.visibility() == Visibility::Restricted {
return Ok(Ruling {
encounter,
responsibility: Responsibility::Both,
rule: Rule::Rule19,
manoeuvre: restricted_visibility(encounter, geometry.relative),
});
}
let (responsibility, rule) = responsibility_of(encounter, situation);
let manoeuvre = in_sight(encounter, responsibility, rule);
Ok(Ruling {
encounter,
responsibility,
rule,
manoeuvre,
})
}
fn geometry_of(situation: &Situation) -> Geometry {
let contact = situation.contact();
let relative = wrap180(contact.bearing.degrees() - situation.own().heading().degrees());
let from_target = contact.bearing.degrees() + 180.0;
let aspect = wrap180(from_target - situation.target().heading().degrees());
Geometry { relative, aspect }
}
fn encounter_of(geometry: &Geometry, situation: &Situation, config: &ColregsConfig) -> Encounter {
let own = situation.own().motion();
let target = situation.target().motion();
let overtaking_from = 90.0 + config.overtaking_abaft_beam.degrees();
let closing = is_closing(situation);
let own_astern_of_target = closing && math::abs(geometry.aspect) > overtaking_from;
let target_astern_of_own = closing && math::abs(geometry.relative) > overtaking_from;
match (own_astern_of_target, target_astern_of_own) {
(true, false) => return Encounter::Overtaking,
(false, true) => return Encounter::BeingOvertaken,
(true, true) => {
return if own.speed.knots() > target.speed.knots() {
Encounter::Overtaking
} else {
Encounter::BeingOvertaken
};
}
(false, false) => {}
}
let half = config.head_on_half_width.degrees();
if math::abs(geometry.relative) <= half && math::abs(geometry.aspect) <= half {
return Encounter::HeadOn;
}
let starboard = if math::abs(geometry.relative) < DEAD_AHEAD_DEG {
geometry.aspect < 0.0
} else {
geometry.relative > 0.0
};
Encounter::Crossing {
target_on: if starboard {
Side::Starboard
} else {
Side::Port
},
}
}
fn is_closing(situation: &Situation) -> bool {
let contact = situation.contact();
let (north, east) = (
math::cos(contact.bearing.radians()),
math::sin(contact.bearing.radians()),
);
let (own_north, own_east) = components(situation.own().motion());
let (target_north, target_east) = components(situation.target().motion());
let closing_rate = north * (target_north - own_north) + east * (target_east - own_east);
let scale = math::hypot(own_north, own_east).max(math::hypot(target_north, target_east));
closing_rate < 0.0 && !math::is_effectively_zero(closing_rate, scale)
}
fn components(vessel: Vessel) -> (f64, f64) {
let radians = vessel.course.radians();
(
vessel.speed.knots() * math::cos(radians),
vessel.speed.knots() * math::sin(radians),
)
}
fn responsibility_of(encounter: Encounter, situation: &Situation) -> (Responsibility, Rule) {
match encounter {
Encounter::Overtaking => return (Responsibility::GiveWay, Rule::Rule13),
Encounter::BeingOvertaken => return (Responsibility::StandOn, Rule::Rule13),
_ => {}
}
let own = situation.own().category();
let target = situation.target().category();
match own.precedence().cmp(&target.precedence()) {
core::cmp::Ordering::Less => return (Responsibility::GiveWay, Rule::Rule18),
core::cmp::Ordering::Greater => return (Responsibility::StandOn, Rule::Rule18),
core::cmp::Ordering::Equal => {}
}
if let (
VesselCategory::Sailing { tack: own_tack },
VesselCategory::Sailing { tack: target_tack },
) = (own, target)
{
return (
sailing_vessels(own_tack, target_tack, situation),
Rule::Rule12,
);
}
match encounter {
Encounter::HeadOn => (Responsibility::Both, Rule::Rule14),
Encounter::Crossing {
target_on: Side::Starboard,
} => (Responsibility::GiveWay, Rule::Rule15),
Encounter::Crossing { .. } => (Responsibility::StandOn, Rule::Rule17),
Encounter::Overtaking | Encounter::BeingOvertaken => {
(Responsibility::Undetermined, Rule::Rule13)
}
}
}
fn sailing_vessels(
own: Option<Tack>,
target: Option<Tack>,
situation: &Situation,
) -> Responsibility {
match (own, target) {
(Some(Tack::Port), Some(Tack::Starboard)) => Responsibility::GiveWay,
(Some(Tack::Starboard), Some(Tack::Port)) => Responsibility::StandOn,
(Some(_), Some(_)) => match situation.wind_from() {
Some(from) => {
let towards = from.degrees() + 180.0;
let downwind = wrap180(situation.contact().bearing.degrees() - towards);
if math::abs(downwind) < 90.0 {
Responsibility::GiveWay
} else {
Responsibility::StandOn
}
}
None => Responsibility::Undetermined,
},
_ => Responsibility::Undetermined,
}
}
fn in_sight(
encounter: Encounter,
responsibility: Responsibility,
rule: Rule,
) -> PermittedManoeuvre {
match (responsibility, rule, encounter) {
(Responsibility::GiveWay, Rule::Rule15, _) | (Responsibility::Both, Rule::Rule14, _) => {
PermittedManoeuvre {
starboard: true,
port: false,
slow_down: true,
hold: false,
}
}
(Responsibility::StandOn, _, Encounter::Crossing { .. }) => PermittedManoeuvre {
starboard: true,
port: false,
slow_down: true,
hold: true,
},
(Responsibility::StandOn, _, _) => PermittedManoeuvre {
starboard: true,
port: true,
slow_down: true,
hold: true,
},
(Responsibility::GiveWay | Responsibility::Both | Responsibility::Undetermined, _, _) => {
PermittedManoeuvre {
starboard: true,
port: true,
slow_down: true,
hold: false,
}
}
}
}
fn restricted_visibility(encounter: Encounter, relative: f64) -> PermittedManoeuvre {
let forward_of_beam = math::abs(relative) < 90.0;
let (starboard, port) = if forward_of_beam {
(true, encounter == Encounter::Overtaking)
} else if relative >= 0.0 {
(false, true)
} else {
(true, false)
};
PermittedManoeuvre {
starboard,
port,
slow_down: true,
hold: false,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::Party;
use kinavis::relative_motion::Contact;
use kinavis_kernel::angle::{TrueBearing, TrueCourse};
use kinavis_kernel::units::{Angle, Distance, Speed};
fn vessel(course: f64, knots: f64) -> Vessel {
Vessel {
course: TrueCourse::new(course).unwrap(),
speed: Speed::from_knots(knots).unwrap(),
}
}
fn power(course: f64, knots: f64) -> Party {
Party::new(vessel(course, knots), VesselCategory::PowerDriven)
}
fn bearing(degrees: f64) -> Contact {
Contact {
bearing: TrueBearing::new(degrees).unwrap(),
range: Distance::from_nautical_miles(5.0).unwrap(),
}
}
fn in_sight(own: Party, target: Party, contact: Contact) -> Ruling {
rule_of_the_road(
&Situation::new(own, target, contact, Visibility::InSight),
&ColregsConfig::STANDARD,
)
.unwrap()
}
#[test]
fn crossing_the_vessel_with_the_other_to_starboard_gives_way() {
let ruling = in_sight(power(0.0, 12.0), power(270.0, 12.0), bearing(45.0));
assert_eq!(
ruling.encounter(),
Encounter::Crossing {
target_on: Side::Starboard
}
);
assert_eq!(ruling.responsibility(), Responsibility::GiveWay);
assert_eq!(ruling.rule(), Rule::Rule15);
let may = ruling.manoeuvre();
assert!(may.starboard && !may.port && may.slow_down && !may.hold);
let ruling = in_sight(power(0.0, 12.0), power(90.0, 12.0), bearing(315.0));
assert_eq!(
ruling.encounter(),
Encounter::Crossing {
target_on: Side::Port
}
);
assert_eq!(ruling.responsibility(), Responsibility::StandOn);
assert_eq!(ruling.rule(), Rule::Rule17);
let may = ruling.manoeuvre();
assert!(may.hold && may.starboard && !may.port);
}
#[test]
fn head_on_is_nearly_reciprocal_and_both_go_to_starboard() {
let ruling = in_sight(power(0.0, 12.0), power(180.0, 12.0), bearing(0.0));
assert_eq!(ruling.encounter(), Encounter::HeadOn);
assert_eq!(ruling.responsibility(), Responsibility::Both);
assert_eq!(ruling.rule(), Rule::Rule14);
assert!(ruling.manoeuvre().starboard && !ruling.manoeuvre().port);
let ruling = in_sight(power(0.0, 12.0), power(185.0, 12.0), bearing(5.0));
assert_eq!(ruling.encounter(), Encounter::HeadOn);
let ruling = in_sight(power(0.0, 12.0), power(190.0, 12.0), bearing(10.0));
assert!(matches!(ruling.encounter(), Encounter::Crossing { .. }));
let wide = ColregsConfig {
head_on_half_width: Angle::from_degrees(12.0).unwrap(),
..ColregsConfig::STANDARD
};
let ruling = rule_of_the_road(
&Situation::new(
power(0.0, 12.0),
power(190.0, 12.0),
bearing(10.0),
Visibility::InSight,
),
&wide,
)
.unwrap();
assert_eq!(ruling.encounter(), Encounter::HeadOn);
}
#[test]
fn overtaking_keeps_clear_whatever_the_vessels_are() {
let sailing = Party::new(vessel(0.0, 15.0), VesselCategory::Sailing { tack: None });
let ruling = in_sight(sailing, power(0.0, 8.0), bearing(0.0));
assert_eq!(ruling.encounter(), Encounter::Overtaking);
assert_eq!(ruling.responsibility(), Responsibility::GiveWay);
assert_eq!(ruling.rule(), Rule::Rule13);
assert!(ruling.manoeuvre().starboard && ruling.manoeuvre().port);
let ruling = in_sight(power(0.0, 8.0), power(0.0, 15.0), bearing(150.0));
assert_eq!(ruling.encounter(), Encounter::BeingOvertaken);
assert_eq!(ruling.responsibility(), Responsibility::StandOn);
assert_eq!(ruling.rule(), Rule::Rule13);
assert!(ruling.manoeuvre().hold);
let ruling = in_sight(power(0.0, 8.0), power(0.0, 15.0), bearing(110.0));
assert!(matches!(ruling.encounter(), Encounter::Crossing { .. }));
let ruling = in_sight(power(0.0, 15.0), power(180.0, 8.0), bearing(180.0));
assert!(matches!(ruling.encounter(), Encounter::Crossing { .. }));
let ruling = in_sight(power(0.0, 6.0), power(0.0, 15.0), bearing(0.0));
assert!(matches!(ruling.encounter(), Encounter::Crossing { .. }));
}
#[test]
fn rule_18_ranks_the_vessels() {
let fishing = Party::new(vessel(90.0, 4.0), VesselCategory::Fishing);
let ruling = in_sight(power(0.0, 12.0), fishing, bearing(315.0));
assert_eq!(ruling.responsibility(), Responsibility::GiveWay);
assert_eq!(ruling.rule(), Rule::Rule18);
assert!(ruling.manoeuvre().port && ruling.manoeuvre().starboard);
let ram = Party::new(
vessel(0.0, 3.0),
VesselCategory::RestrictedInAbilityToManoeuvre,
);
let ruling = in_sight(ram, fishing, bearing(45.0));
assert_eq!(ruling.responsibility(), Responsibility::StandOn);
assert_eq!(ruling.rule(), Rule::Rule18);
let nuc = Party::new(vessel(0.0, 0.0), VesselCategory::NotUnderCommand);
assert_eq!(
in_sight(ram, nuc, bearing(45.0)).responsibility(),
Responsibility::GiveWay
);
assert!(
VesselCategory::NotUnderCommand.precedence()
> VesselCategory::ConstrainedByDraught.precedence()
);
let ruling = in_sight(
Party::new(vessel(0.0, 4.0), VesselCategory::Fishing),
fishing,
bearing(45.0),
);
assert_eq!(ruling.responsibility(), Responsibility::GiveWay);
assert_eq!(ruling.rule(), Rule::Rule15);
}
#[test]
fn rule_12_between_sailing_vessels() {
let on = |tack| VesselCategory::Sailing { tack: Some(tack) };
let port_tack = Party::new(vessel(0.0, 6.0), on(Tack::Port));
let starboard_tack = Party::new(vessel(90.0, 6.0), on(Tack::Starboard));
let southbound = Party::new(vessel(180.0, 6.0), on(Tack::Port));
let ruling = in_sight(port_tack, starboard_tack, bearing(315.0));
assert_eq!(ruling.responsibility(), Responsibility::GiveWay);
assert_eq!(ruling.rule(), Rule::Rule12);
let ruling = in_sight(starboard_tack, southbound, bearing(45.0));
assert_eq!(ruling.responsibility(), Responsibility::StandOn);
let same = Party::new(vessel(90.0, 6.0), on(Tack::Port));
let situation = Situation::new(port_tack, same, bearing(90.0), Visibility::InSight)
.with_wind_from(TrueCourse::WEST);
let ruling = rule_of_the_road(&situation, &ColregsConfig::STANDARD).unwrap();
assert_eq!(ruling.responsibility(), Responsibility::GiveWay);
let situation = situation.with_wind_from(TrueCourse::EAST);
let ruling = rule_of_the_road(&situation, &ColregsConfig::STANDARD).unwrap();
assert_eq!(ruling.responsibility(), Responsibility::StandOn);
let ruling = in_sight(port_tack, same, bearing(90.0));
assert_eq!(ruling.responsibility(), Responsibility::Undetermined);
let unknown = Party::new(vessel(90.0, 6.0), VesselCategory::Sailing { tack: None });
assert_eq!(
in_sight(port_tack, unknown, bearing(90.0)).responsibility(),
Responsibility::Undetermined
);
assert!(!in_sight(port_tack, unknown, bearing(90.0)).manoeuvre().hold);
}
#[test]
fn in_restricted_visibility_nobody_stands_on() {
let situation = |contact| {
Situation::new(
power(0.0, 12.0),
power(90.0, 12.0),
contact,
Visibility::Restricted,
)
};
let ruling =
rule_of_the_road(&situation(bearing(315.0)), &ColregsConfig::STANDARD).unwrap();
assert_eq!(ruling.responsibility(), Responsibility::Both);
assert_eq!(ruling.rule(), Rule::Rule19);
let may = ruling.manoeuvre();
assert!(!may.hold && may.starboard && !may.port && may.slow_down);
let ruling =
rule_of_the_road(&situation(bearing(150.0)), &ColregsConfig::STANDARD).unwrap();
assert!(!ruling.manoeuvre().starboard && ruling.manoeuvre().port);
let ruling =
rule_of_the_road(&situation(bearing(210.0)), &ColregsConfig::STANDARD).unwrap();
assert!(ruling.manoeuvre().starboard && !ruling.manoeuvre().port);
let overtaking = Situation::new(
power(0.0, 15.0),
power(0.0, 8.0),
bearing(0.0),
Visibility::Restricted,
);
let ruling = rule_of_the_road(&overtaking, &ColregsConfig::STANDARD).unwrap();
assert_eq!(ruling.encounter(), Encounter::Overtaking);
assert!(ruling.manoeuvre().port && ruling.manoeuvre().starboard);
}
#[test]
fn the_head_is_the_heading_when_it_is_known() {
let own = power(0.0, 12.0).with_heading(TrueCourse::new(20.0).unwrap());
let ruling = in_sight(own, power(180.0, 12.0), bearing(10.0));
assert_eq!(
ruling.encounter(),
Encounter::Crossing {
target_on: Side::Port
}
);
assert_eq!(own.heading(), TrueCourse::new(20.0).unwrap());
assert_eq!(power(0.0, 12.0).heading(), TrueCourse::NORTH);
}
#[test]
fn a_configuration_the_rules_could_not_mean_is_refused() {
let situation = Situation::new(
power(0.0, 12.0),
power(270.0, 12.0),
bearing(45.0),
Visibility::InSight,
);
for (abaft, half) in [(100.0, 6.0), (-1.0, 6.0), (22.5, 91.0), (22.5, -6.0)] {
let config = ColregsConfig {
overtaking_abaft_beam: Angle::from_degrees(abaft).unwrap(),
head_on_half_width: Angle::from_degrees(half).unwrap(),
};
assert!(rule_of_the_road(&situation, &config).is_err());
}
assert_eq!(alloc::format!("{}", Rule::Rule19), "Rule 19");
}
}