1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum Affiliation {
11 Friendly,
13 Hostile,
15 Unknown,
17 Neutral,
19 AssumedFriendly,
21 Suspect,
23 Pending,
25}
26
27impl Affiliation {
28 pub fn cot_char(&self) -> char {
30 match self {
31 Self::Friendly | Self::AssumedFriendly => 'f',
32 Self::Hostile | Self::Suspect => 'h',
33 Self::Neutral => 'n',
34 Self::Unknown | Self::Pending => 'u',
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
41pub enum EntityClassification {
42 Person,
44 Vehicle,
46 Aircraft,
48 Uav,
50 Vessel,
52 Ugv,
54 Sensor,
56 Unit,
58 Unknown,
60 Custom(String),
62}
63
64impl EntityClassification {
65 pub fn parse(s: &str) -> Self {
67 match s.to_lowercase().as_str() {
68 "person" | "personnel" | "human" | "dismount" => Self::Person,
69 "vehicle" | "car" | "truck" | "tank" => Self::Vehicle,
70 "aircraft" | "plane" | "helicopter" | "helo" => Self::Aircraft,
71 "uav" | "drone" | "uas" => Self::Uav,
72 "vessel" | "ship" | "boat" | "maritime" => Self::Vessel,
73 "ugv" | "robot" | "ground_robot" => Self::Ugv,
74 "sensor" | "equipment" => Self::Sensor,
75 "unit" | "team" | "cell" | "squad" => Self::Unit,
76 "unknown" | "" => Self::Unknown,
77 other => Self::Custom(other.to_string()),
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct CotType(String);
85
86impl CotType {
87 pub fn new(type_code: &str) -> Self {
89 Self(type_code.to_string())
90 }
91
92 pub fn as_str(&self) -> &str {
94 &self.0
95 }
96
97 pub fn is_atom(&self) -> bool {
99 self.0.starts_with("a-")
100 }
101
102 pub fn is_tasking(&self) -> bool {
104 self.0.starts_with("t-")
105 }
106
107 pub fn is_drawing(&self) -> bool {
109 self.0.starts_with("u-d-")
110 }
111}
112
113impl std::fmt::Display for CotType {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 write!(f, "{}", self.0)
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct CotTypeMapper {
122 custom_mappings: std::collections::HashMap<String, String>,
124}
125
126impl Default for CotTypeMapper {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl CotTypeMapper {
133 pub fn new() -> Self {
135 Self {
136 custom_mappings: std::collections::HashMap::new(),
137 }
138 }
139
140 pub fn add_mapping(&mut self, classification: &str, cot_type: &str) {
142 self.custom_mappings
143 .insert(classification.to_lowercase(), cot_type.to_string());
144 }
145
146 pub fn map(&self, classification: &str, affiliation: Affiliation) -> CotType {
148 if let Some(custom) = self.custom_mappings.get(&classification.to_lowercase()) {
150 return CotType::new(custom);
151 }
152
153 let entity = EntityClassification::parse(classification);
154 self.map_entity(&entity, affiliation)
155 }
156
157 pub fn map_entity(&self, entity: &EntityClassification, affiliation: Affiliation) -> CotType {
159 let aff = affiliation.cot_char();
160
161 let type_code = match entity {
162 EntityClassification::Person => format!("a-{}-G-E-S", aff),
164
165 EntityClassification::Vehicle => format!("a-{}-G-E-V", aff),
167
168 EntityClassification::Aircraft => format!("a-{}-A", aff),
170
171 EntityClassification::Uav => format!("a-{}-A-M-F-Q", aff),
173
174 EntityClassification::Vessel => format!("a-{}-S", aff),
176
177 EntityClassification::Ugv => format!("a-{}-G-U-C", aff),
179
180 EntityClassification::Sensor => format!("a-{}-G-E-S", aff),
182
183 EntityClassification::Unit => format!("a-{}-G-U-C", aff),
185
186 EntityClassification::Unknown => format!("a-{}-G", aff),
188
189 EntityClassification::Custom(_) => format!("a-{}-G", aff),
191 };
192
193 CotType::new(&type_code)
194 }
195
196 pub fn map_platform(&self, platform_type: &str, affiliation: Affiliation) -> CotType {
198 let aff = affiliation.cot_char();
199
200 let type_code = match platform_type.to_lowercase().as_str() {
201 "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), };
209
210 CotType::new(&type_code)
211 }
212
213 pub fn handoff_type() -> CotType {
215 CotType::new("a-x-h-h")
216 }
217
218 pub fn geofence_type() -> CotType {
220 CotType::new("u-d-r")
221 }
222
223 pub fn mission_tasking_type() -> CotType {
225 CotType::new("t-x-m-c")
226 }
227
228 pub fn cell_marker_type(affiliation: Affiliation) -> CotType {
230 let aff = affiliation.cot_char();
231 CotType::new(&format!("a-{}-G-U-C", aff))
232 }
233
234 pub fn formation_marker_type(affiliation: Affiliation) -> CotType {
236 let aff = affiliation.cot_char();
237 CotType::new(&format!("a-{}-G-U-C", aff))
238 }
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub enum CotRelation {
244 Parent,
246 Handoff,
248 Sibling,
250 Observing,
252}
253
254impl CotRelation {
255 pub fn as_str(&self) -> &'static str {
257 match self {
258 Self::Parent => "p-p",
259 Self::Handoff => "h-h",
260 Self::Sibling => "s-s",
261 Self::Observing => "o-o",
262 }
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_affiliation_cot_char() {
272 assert_eq!(Affiliation::Friendly.cot_char(), 'f');
273 assert_eq!(Affiliation::Hostile.cot_char(), 'h');
274 assert_eq!(Affiliation::Unknown.cot_char(), 'u');
275 assert_eq!(Affiliation::Neutral.cot_char(), 'n');
276 assert_eq!(Affiliation::AssumedFriendly.cot_char(), 'f');
277 assert_eq!(Affiliation::Suspect.cot_char(), 'h');
278 }
279
280 #[test]
281 fn test_entity_classification_parse() {
282 assert_eq!(
283 EntityClassification::parse("person"),
284 EntityClassification::Person
285 );
286 assert_eq!(
287 EntityClassification::parse("VEHICLE"),
288 EntityClassification::Vehicle
289 );
290 assert_eq!(
291 EntityClassification::parse("uav"),
292 EntityClassification::Uav
293 );
294 assert_eq!(
295 EntityClassification::parse("unknown"),
296 EntityClassification::Unknown
297 );
298 assert!(matches!(
299 EntityClassification::parse("custom_thing"),
300 EntityClassification::Custom(_)
301 ));
302 }
303
304 #[test]
305 fn test_cot_type_mapper_person() {
306 let mapper = CotTypeMapper::new();
307
308 let friendly_person = mapper.map("person", Affiliation::Friendly);
309 assert_eq!(friendly_person.as_str(), "a-f-G-E-S");
310
311 let hostile_person = mapper.map("person", Affiliation::Hostile);
312 assert_eq!(hostile_person.as_str(), "a-h-G-E-S");
313
314 let unknown_person = mapper.map("person", Affiliation::Unknown);
315 assert_eq!(unknown_person.as_str(), "a-u-G-E-S");
316 }
317
318 #[test]
319 fn test_cot_type_mapper_vehicle() {
320 let mapper = CotTypeMapper::new();
321
322 let vehicle = mapper.map("vehicle", Affiliation::Friendly);
323 assert_eq!(vehicle.as_str(), "a-f-G-E-V");
324 }
325
326 #[test]
327 fn test_cot_type_mapper_uav() {
328 let mapper = CotTypeMapper::new();
329
330 let uav = mapper.map("uav", Affiliation::Friendly);
331 assert_eq!(uav.as_str(), "a-f-A-M-F-Q");
332 }
333
334 #[test]
335 fn test_cot_type_mapper_platform() {
336 let mapper = CotTypeMapper::new();
337
338 let ugv = mapper.map_platform("UGV", Affiliation::Friendly);
339 assert_eq!(ugv.as_str(), "a-f-G-U-C");
340
341 let operator = mapper.map_platform("operator", Affiliation::Friendly);
342 assert_eq!(operator.as_str(), "a-f-G-U-C-I");
343 }
344
345 #[test]
346 fn test_cot_type_mapper_custom() {
347 let mut mapper = CotTypeMapper::new();
348 mapper.add_mapping("special_target", "a-h-G-I-T");
349
350 let custom = mapper.map("special_target", Affiliation::Hostile);
351 assert_eq!(custom.as_str(), "a-h-G-I-T");
352 }
353
354 #[test]
355 fn test_cot_type_is_atom() {
356 let atom = CotType::new("a-f-G-E-S");
357 assert!(atom.is_atom());
358
359 let tasking = CotType::new("t-x-m-c");
360 assert!(!tasking.is_atom());
361 }
362
363 #[test]
364 fn test_cot_type_is_tasking() {
365 let tasking = CotType::new("t-x-m-c");
366 assert!(tasking.is_tasking());
367
368 let atom = CotType::new("a-f-G-E-S");
369 assert!(!atom.is_tasking());
370 }
371
372 #[test]
373 fn test_cot_relation_strings() {
374 assert_eq!(CotRelation::Parent.as_str(), "p-p");
375 assert_eq!(CotRelation::Handoff.as_str(), "h-h");
376 assert_eq!(CotRelation::Sibling.as_str(), "s-s");
377 assert_eq!(CotRelation::Observing.as_str(), "o-o");
378 }
379
380 #[test]
381 fn test_special_cot_types() {
382 assert_eq!(CotTypeMapper::handoff_type().as_str(), "a-x-h-h");
383 assert_eq!(CotTypeMapper::geofence_type().as_str(), "u-d-r");
384 assert_eq!(CotTypeMapper::mission_tasking_type().as_str(), "t-x-m-c");
385 }
386
387 #[test]
388 fn test_cell_and_formation_markers() {
389 let cell = CotTypeMapper::cell_marker_type(Affiliation::Friendly);
390 assert_eq!(cell.as_str(), "a-f-G-U-C");
391
392 let formation = CotTypeMapper::formation_marker_type(Affiliation::Friendly);
393 assert_eq!(formation.as_str(), "a-f-G-U-C");
394 }
395}