use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Affiliation {
Friendly,
Hostile,
Unknown,
Neutral,
AssumedFriendly,
Suspect,
Pending,
}
impl Affiliation {
pub fn cot_char(&self) -> char {
match self {
Self::Friendly | Self::AssumedFriendly => 'f',
Self::Hostile | Self::Suspect => 'h',
Self::Neutral => 'n',
Self::Unknown | Self::Pending => 'u',
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EntityClassification {
Person,
Vehicle,
Aircraft,
Uav,
Vessel,
Ugv,
Sensor,
Unit,
Unknown,
Custom(String),
}
impl EntityClassification {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().as_str() {
"person" | "personnel" | "human" | "dismount" => Self::Person,
"vehicle" | "car" | "truck" | "tank" => Self::Vehicle,
"aircraft" | "plane" | "helicopter" | "helo" => Self::Aircraft,
"uav" | "drone" | "uas" => Self::Uav,
"vessel" | "ship" | "boat" | "maritime" => Self::Vessel,
"ugv" | "robot" | "ground_robot" => Self::Ugv,
"sensor" | "equipment" => Self::Sensor,
"unit" | "team" | "cell" | "squad" => Self::Unit,
"unknown" | "" => Self::Unknown,
other => Self::Custom(other.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CotType(String);
impl CotType {
pub fn new(type_code: &str) -> Self {
Self(type_code.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_atom(&self) -> bool {
self.0.starts_with("a-")
}
pub fn is_tasking(&self) -> bool {
self.0.starts_with("t-")
}
pub fn is_drawing(&self) -> bool {
self.0.starts_with("u-d-")
}
}
impl std::fmt::Display for CotType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct CotTypeMapper {
custom_mappings: std::collections::HashMap<String, String>,
}
impl Default for CotTypeMapper {
fn default() -> Self {
Self::new()
}
}
impl CotTypeMapper {
pub fn new() -> Self {
Self {
custom_mappings: std::collections::HashMap::new(),
}
}
pub fn add_mapping(&mut self, classification: &str, cot_type: &str) {
self.custom_mappings
.insert(classification.to_lowercase(), cot_type.to_string());
}
pub fn map(&self, classification: &str, affiliation: Affiliation) -> CotType {
if let Some(custom) = self.custom_mappings.get(&classification.to_lowercase()) {
return CotType::new(custom);
}
let entity = EntityClassification::parse(classification);
self.map_entity(&entity, affiliation)
}
pub fn map_entity(&self, entity: &EntityClassification, affiliation: Affiliation) -> CotType {
let aff = affiliation.cot_char();
let type_code = match entity {
EntityClassification::Person => format!("a-{}-G-E-S", aff),
EntityClassification::Vehicle => format!("a-{}-G-E-V", aff),
EntityClassification::Aircraft => format!("a-{}-A", aff),
EntityClassification::Uav => format!("a-{}-A-M-F-Q", aff),
EntityClassification::Vessel => format!("a-{}-S", aff),
EntityClassification::Ugv => format!("a-{}-G-U-C", aff),
EntityClassification::Sensor => format!("a-{}-G-E-S", aff),
EntityClassification::Unit => format!("a-{}-G-U-C", aff),
EntityClassification::Unknown => format!("a-{}-G", aff),
EntityClassification::Custom(_) => format!("a-{}-G", aff),
};
CotType::new(&type_code)
}
pub fn map_platform(&self, platform_type: &str, affiliation: Affiliation) -> CotType {
let aff = affiliation.cot_char();
let type_code = match platform_type.to_lowercase().as_str() {
"uav" | "drone" | "uas" => format!("a-{}-A-M-F-Q", aff), "ugv" | "robot" | "ground_robot" => format!("a-{}-G-U-C", aff), "soldier" | "operator" | "dismount" => format!("a-{}-G-U-C-I", aff), "vehicle" | "humvee" | "mrap" => format!("a-{}-G-U-C-V", aff), "command_vehicle" | "toc" => format!("a-{}-G-U-C-V-H", aff), "sensor_platform" => format!("a-{}-G-E-S", aff), _ => format!("a-{}-G-U-C", aff), };
CotType::new(&type_code)
}
pub fn handoff_type() -> CotType {
CotType::new("a-x-h-h")
}
pub fn geofence_type() -> CotType {
CotType::new("u-d-r")
}
pub fn mission_tasking_type() -> CotType {
CotType::new("t-x-m-c")
}
pub fn cell_marker_type(affiliation: Affiliation) -> CotType {
let aff = affiliation.cot_char();
CotType::new(&format!("a-{}-G-U-C", aff))
}
pub fn formation_marker_type(affiliation: Affiliation) -> CotType {
let aff = affiliation.cot_char();
CotType::new(&format!("a-{}-G-U-C", aff))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CotRelation {
Parent,
Handoff,
Sibling,
Observing,
}
impl CotRelation {
pub fn as_str(&self) -> &'static str {
match self {
Self::Parent => "p-p",
Self::Handoff => "h-h",
Self::Sibling => "s-s",
Self::Observing => "o-o",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_affiliation_cot_char() {
assert_eq!(Affiliation::Friendly.cot_char(), 'f');
assert_eq!(Affiliation::Hostile.cot_char(), 'h');
assert_eq!(Affiliation::Unknown.cot_char(), 'u');
assert_eq!(Affiliation::Neutral.cot_char(), 'n');
assert_eq!(Affiliation::AssumedFriendly.cot_char(), 'f');
assert_eq!(Affiliation::Suspect.cot_char(), 'h');
}
#[test]
fn test_entity_classification_parse() {
assert_eq!(
EntityClassification::parse("person"),
EntityClassification::Person
);
assert_eq!(
EntityClassification::parse("VEHICLE"),
EntityClassification::Vehicle
);
assert_eq!(
EntityClassification::parse("uav"),
EntityClassification::Uav
);
assert_eq!(
EntityClassification::parse("unknown"),
EntityClassification::Unknown
);
assert!(matches!(
EntityClassification::parse("custom_thing"),
EntityClassification::Custom(_)
));
}
#[test]
fn test_cot_type_mapper_person() {
let mapper = CotTypeMapper::new();
let friendly_person = mapper.map("person", Affiliation::Friendly);
assert_eq!(friendly_person.as_str(), "a-f-G-E-S");
let hostile_person = mapper.map("person", Affiliation::Hostile);
assert_eq!(hostile_person.as_str(), "a-h-G-E-S");
let unknown_person = mapper.map("person", Affiliation::Unknown);
assert_eq!(unknown_person.as_str(), "a-u-G-E-S");
}
#[test]
fn test_cot_type_mapper_vehicle() {
let mapper = CotTypeMapper::new();
let vehicle = mapper.map("vehicle", Affiliation::Friendly);
assert_eq!(vehicle.as_str(), "a-f-G-E-V");
}
#[test]
fn test_cot_type_mapper_uav() {
let mapper = CotTypeMapper::new();
let uav = mapper.map("uav", Affiliation::Friendly);
assert_eq!(uav.as_str(), "a-f-A-M-F-Q");
}
#[test]
fn test_cot_type_mapper_platform() {
let mapper = CotTypeMapper::new();
let ugv = mapper.map_platform("UGV", Affiliation::Friendly);
assert_eq!(ugv.as_str(), "a-f-G-U-C");
let operator = mapper.map_platform("operator", Affiliation::Friendly);
assert_eq!(operator.as_str(), "a-f-G-U-C-I");
}
#[test]
fn test_cot_type_mapper_custom() {
let mut mapper = CotTypeMapper::new();
mapper.add_mapping("special_target", "a-h-G-I-T");
let custom = mapper.map("special_target", Affiliation::Hostile);
assert_eq!(custom.as_str(), "a-h-G-I-T");
}
#[test]
fn test_cot_type_is_atom() {
let atom = CotType::new("a-f-G-E-S");
assert!(atom.is_atom());
let tasking = CotType::new("t-x-m-c");
assert!(!tasking.is_atom());
}
#[test]
fn test_cot_type_is_tasking() {
let tasking = CotType::new("t-x-m-c");
assert!(tasking.is_tasking());
let atom = CotType::new("a-f-G-E-S");
assert!(!atom.is_tasking());
}
#[test]
fn test_cot_relation_strings() {
assert_eq!(CotRelation::Parent.as_str(), "p-p");
assert_eq!(CotRelation::Handoff.as_str(), "h-h");
assert_eq!(CotRelation::Sibling.as_str(), "s-s");
assert_eq!(CotRelation::Observing.as_str(), "o-o");
}
#[test]
fn test_special_cot_types() {
assert_eq!(CotTypeMapper::handoff_type().as_str(), "a-x-h-h");
assert_eq!(CotTypeMapper::geofence_type().as_str(), "u-d-r");
assert_eq!(CotTypeMapper::mission_tasking_type().as_str(), "t-x-m-c");
}
#[test]
fn test_cell_and_formation_markers() {
let cell = CotTypeMapper::cell_marker_type(Affiliation::Friendly);
assert_eq!(cell.as_str(), "a-f-G-U-C");
let formation = CotTypeMapper::formation_marker_type(Affiliation::Friendly);
assert_eq!(formation.as_str(), "a-f-G-U-C");
}
}