Skip to main content

peat_protocol/cot/
type_mapper.rs

1//! MIL-STD-2525 symbol type mappings for CoT
2//!
3//! Maps Peat entity classifications to CoT type codes following MIL-STD-2525
4//! military symbology standards.
5
6use serde::{Deserialize, Serialize};
7
8/// Entity affiliation (friend/hostile/unknown/neutral)
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum Affiliation {
11    /// Friendly force
12    Friendly,
13    /// Hostile force
14    Hostile,
15    /// Unknown affiliation
16    Unknown,
17    /// Neutral entity
18    Neutral,
19    /// Assumed friendly
20    AssumedFriendly,
21    /// Suspect (assumed hostile)
22    Suspect,
23    /// Pending determination
24    Pending,
25}
26
27impl Affiliation {
28    /// Get the CoT affiliation character
29    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/// Entity classification for type mapping
40#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
41pub enum EntityClassification {
42    /// Tracked person/personnel
43    Person,
44    /// Ground vehicle
45    Vehicle,
46    /// Aircraft (fixed or rotary wing)
47    Aircraft,
48    /// UAV/Drone
49    Uav,
50    /// Maritime vessel
51    Vessel,
52    /// UGV/Ground robot
53    Ugv,
54    /// Sensor/Equipment
55    Sensor,
56    /// Military unit/team
57    Unit,
58    /// Unknown entity
59    Unknown,
60    /// Custom classification
61    Custom(String),
62}
63
64impl EntityClassification {
65    /// Parse from classification string
66    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/// CoT type code
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct CotType(String);
85
86impl CotType {
87    /// Create a new CoT type
88    pub fn new(type_code: &str) -> Self {
89        Self(type_code.to_string())
90    }
91
92    /// Get the type code string
93    pub fn as_str(&self) -> &str {
94        &self.0
95    }
96
97    /// Check if this is an atom (position/location) type
98    pub fn is_atom(&self) -> bool {
99        self.0.starts_with("a-")
100    }
101
102    /// Check if this is a tasking type
103    pub fn is_tasking(&self) -> bool {
104        self.0.starts_with("t-")
105    }
106
107    /// Check if this is a drawing/shape type
108    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/// Mapper for converting Peat classifications to CoT types
120#[derive(Debug, Clone)]
121pub struct CotTypeMapper {
122    /// Custom type overrides
123    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    /// Create a new mapper with default MIL-STD-2525 mappings
134    pub fn new() -> Self {
135        Self {
136            custom_mappings: std::collections::HashMap::new(),
137        }
138    }
139
140    /// Add a custom mapping
141    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    /// Map a classification string to CoT type
147    pub fn map(&self, classification: &str, affiliation: Affiliation) -> CotType {
148        // Check custom mappings first
149        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    /// Map an entity classification to CoT type
158    pub fn map_entity(&self, entity: &EntityClassification, affiliation: Affiliation) -> CotType {
159        let aff = affiliation.cot_char();
160
161        let type_code = match entity {
162            // Tracked person - Ground Equipment - Sensor (tracking a person)
163            EntityClassification::Person => format!("a-{}-G-E-S", aff),
164
165            // Ground vehicle
166            EntityClassification::Vehicle => format!("a-{}-G-E-V", aff),
167
168            // Aircraft (generic)
169            EntityClassification::Aircraft => format!("a-{}-A", aff),
170
171            // UAV - Air - Military - Fixed Wing - UAV
172            EntityClassification::Uav => format!("a-{}-A-M-F-Q", aff),
173
174            // Maritime vessel
175            EntityClassification::Vessel => format!("a-{}-S", aff),
176
177            // UGV - Ground Unit - Combat
178            EntityClassification::Ugv => format!("a-{}-G-U-C", aff),
179
180            // Sensor/Equipment
181            EntityClassification::Sensor => format!("a-{}-G-E-S", aff),
182
183            // Military unit/team
184            EntityClassification::Unit => format!("a-{}-G-U-C", aff),
185
186            // Unknown ground
187            EntityClassification::Unknown => format!("a-{}-G", aff),
188
189            // Custom - default to ground unknown
190            EntityClassification::Custom(_) => format!("a-{}-G", aff),
191        };
192
193        CotType::new(&type_code)
194    }
195
196    /// Map a Peat platform type to CoT type
197    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), // UAV
202            "ugv" | "robot" | "ground_robot" => format!("a-{}-G-U-C", aff), // UGV
203            "soldier" | "operator" | "dismount" => format!("a-{}-G-U-C-I", aff), // Infantry
204            "vehicle" | "humvee" | "mrap" => format!("a-{}-G-U-C-V", aff), // Combat Vehicle
205            "command_vehicle" | "toc" => format!("a-{}-G-U-C-V-H", aff), // HQ Vehicle
206            "sensor_platform" => format!("a-{}-G-E-S", aff),         // Sensor
207            _ => format!("a-{}-G-U-C", aff),                         // Default to ground unit
208        };
209
210        CotType::new(&type_code)
211    }
212
213    /// Get the CoT type for a handoff event
214    pub fn handoff_type() -> CotType {
215        CotType::new("a-x-h-h")
216    }
217
218    /// Get the CoT type for a geofence/ROZ
219    pub fn geofence_type() -> CotType {
220        CotType::new("u-d-r")
221    }
222
223    /// Get the CoT type for a mission tasking
224    pub fn mission_tasking_type() -> CotType {
225        CotType::new("t-x-m-c")
226    }
227
228    /// Get the CoT type for a cell/team marker
229    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    /// Get the CoT type for a formation marker
235    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/// CoT relation types for link elements
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub enum CotRelation {
244    /// Parent (hierarchical ownership)
245    Parent,
246    /// Handoff (track transfer)
247    Handoff,
248    /// Sibling (same echelon)
249    Sibling,
250    /// Observing (sensor relationship)
251    Observing,
252}
253
254impl CotRelation {
255    /// Get the CoT relation code
256    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}