use std::collections::BTreeMap;
use kinavis_colregs::VesselCategory;
use kinavis_kernel::angle::TrueCourse;
use kinavis_kernel::position::Position;
use kinavis_kernel::time::{Instant, Utc};
use kinavis_kernel::units::Speed;
use serde_json::Value;
use crate::delta::Delta;
use crate::time::parse_timestamp;
pub const SELF_CONTEXT: &str = "vessels.self";
#[derive(Debug, Clone, Default, PartialEq)]
pub struct VesselState {
pub name: Option<String>,
pub mmsi: Option<String>,
pub position: Option<Position>,
pub course: Option<TrueCourse>,
pub speed: Option<Speed>,
pub heading: Option<TrueCourse>,
pub state: Option<String>,
pub ship_type: Option<u32>,
pub last_seen: Option<Instant<Utc>>,
}
impl VesselState {
#[must_use]
pub fn label<'a>(&'a self, context: &'a str) -> &'a str {
self.name
.as_deref()
.or(self.mmsi.as_deref())
.unwrap_or(context)
}
#[must_use]
pub fn category(&self) -> Option<Category> {
if let Some(state) = self.state.as_deref() {
if let Some(category) = category_of_state(state) {
return Some(category);
}
}
match self.ship_type? {
30 => Some(Category::UnderWay(VesselCategory::Fishing)),
36 => Some(Category::UnderWay(VesselCategory::Sailing { tack: None })),
_ => None,
}
}
fn apply(&mut self, path: &str, value: &Value) {
match path {
"" => {
if let Some(name) = value.get("name").and_then(Value::as_str) {
self.name = Some(name.trim().to_owned());
}
if let Some(mmsi) = value.get("mmsi").and_then(Value::as_str) {
self.mmsi = Some(mmsi.to_owned());
}
}
"name" => self.name = value.as_str().map(|name| name.trim().to_owned()),
"mmsi" => self.mmsi = value.as_str().map(str::to_owned),
"navigation.position" => {
self.position = value
.get("latitude")
.and_then(Value::as_f64)
.zip(value.get("longitude").and_then(Value::as_f64))
.and_then(|(latitude, longitude)| {
Position::from_degrees(latitude, longitude).ok()
});
}
"navigation.courseOverGroundTrue" => self.course = value.as_f64().and_then(direction),
"navigation.headingTrue" => self.heading = value.as_f64().and_then(direction),
"navigation.speedOverGround" => {
self.speed = value
.as_f64()
.filter(|speed| *speed >= 0.0)
.and_then(|speed| Speed::from_metres_per_second(speed).ok());
}
"navigation.state" => self.state = value.as_str().map(str::to_owned),
"design.aisShipType" => {
self.ship_type = value
.get("id")
.and_then(Value::as_u64)
.and_then(|id| u32::try_from(id).ok());
}
_ => {}
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Category {
UnderWay(VesselCategory),
NotUnderWay,
}
fn category_of_state(state: &str) -> Option<Category> {
let category = match state {
"motoring" | "towing < 200m" | "towing > 200m" | "pushing" | "pilotage" => {
VesselCategory::PowerDriven
}
"sailing" => VesselCategory::Sailing { tack: None },
"fishing" | "fishing-hampered" | "trawling" | "trawling-shooting" | "trawling-hauling" => {
VesselCategory::Fishing
}
"constrained by draft" => VesselCategory::ConstrainedByDraught,
"not under command" => VesselCategory::NotUnderCommand,
"anchored" | "moored" | "aground" | "not-under-way" => return Some(Category::NotUnderWay),
state if state.starts_with("restricted manouverability") || state == "mine clearance" => {
VesselCategory::RestrictedInAbilityToManoeuvre
}
_ => return None,
};
Some(Category::UnderWay(category))
}
fn direction(radians: f64) -> Option<TrueCourse> {
radians
.is_finite()
.then(|| TrueCourse::from_degrees_wrapped(radians.to_degrees()))
}
#[derive(Debug, Clone, Default)]
pub struct Picture {
vessels: BTreeMap<String, VesselState>,
own: Option<String>,
}
impl Picture {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_own_context(&mut self, context: &str) {
self.own = Some(context.to_owned());
if let Some(held) = self.vessels.remove(SELF_CONTEXT) {
self.vessels.entry(context.to_owned()).or_insert(held);
}
}
#[must_use]
pub const fn own_is_named(&self) -> bool {
self.own.is_some()
}
#[must_use]
pub fn own_context(&self) -> &str {
self.own.as_deref().unwrap_or(SELF_CONTEXT)
}
#[must_use]
pub fn is_own(&self, context: &str) -> bool {
context == SELF_CONTEXT || context == self.own_context()
}
pub fn apply(&mut self, delta: &Delta) {
if self.own.is_none() {
if let Some(context) = delta.context.as_deref() {
let own_position = delta.updates.iter().any(|update| {
update.is_own_sensor()
&& update
.values
.iter()
.any(|value| value.path == "navigation.position")
});
if own_position && context.starts_with("vessels.") && context != SELF_CONTEXT {
self.set_own_context(context);
}
}
}
let context = match delta.context.as_deref() {
None => self.own_context().to_owned(),
Some(context) if self.is_own(context) => self.own_context().to_owned(),
Some(context) => context.to_owned(),
};
if !context.starts_with("vessels.") {
return;
}
let vessel = self.vessels.entry(context).or_default();
for update in &delta.updates {
let at = update.timestamp.as_deref().and_then(parse_timestamp);
if let Some(at) = at {
vessel.last_seen = Some(vessel.last_seen.map_or(at, |seen| seen.max(at)));
}
for value in &update.values {
vessel.apply(&value.path, &value.value);
}
}
}
#[must_use]
pub fn own(&self) -> Option<&VesselState> {
self.vessels.get(self.own_context())
}
pub fn targets(&self) -> impl Iterator<Item = (&str, &VesselState)> + '_ {
self.vessels
.iter()
.filter(|(context, _)| !self.is_own(context))
.map(|(context, vessel)| (context.as_str(), vessel))
}
#[must_use]
pub fn len(&self) -> usize {
self.vessels.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.vessels.is_empty()
}
}