1use chrono::Duration;
6
7use super::event::{CotError, CotEvent, CotLink, CotPoint};
8use super::peat_extension::{
9 PeatCapability, PeatExtension, PeatHandoff, PeatHierarchy, PeatSource, PeatStatus,
10};
11use super::type_mapper::{Affiliation, CotRelation, CotTypeMapper};
12use super::types::{
13 CapabilityAdvertisement, FormationCapabilitySummary, HandoffMessage, TrackUpdate,
14};
15
16#[derive(Debug, Clone)]
18pub struct CotEncoderConfig {
19 pub track_stale_secs: i64,
21 pub capability_stale_secs: i64,
23 pub handoff_stale_secs: i64,
25 pub default_affiliation: Affiliation,
27 pub include_peat_extension: bool,
29}
30
31impl Default for CotEncoderConfig {
32 fn default() -> Self {
33 Self {
34 track_stale_secs: 30,
35 capability_stale_secs: 60,
36 handoff_stale_secs: 300,
37 default_affiliation: Affiliation::Friendly,
38 include_peat_extension: true,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct CotEncoder {
46 config: CotEncoderConfig,
48 type_mapper: CotTypeMapper,
50}
51
52impl Default for CotEncoder {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl CotEncoder {
59 pub fn new() -> Self {
61 Self {
62 config: CotEncoderConfig::default(),
63 type_mapper: CotTypeMapper::new(),
64 }
65 }
66
67 pub fn with_config(config: CotEncoderConfig) -> Self {
69 Self {
70 config,
71 type_mapper: CotTypeMapper::new(),
72 }
73 }
74
75 pub fn type_mapper_mut(&mut self) -> &mut CotTypeMapper {
77 &mut self.type_mapper
78 }
79
80 pub fn track_update_to_event(&self, track: &TrackUpdate) -> Result<CotEvent, CotError> {
82 let cot_type = self
83 .type_mapper
84 .map(&track.classification, self.config.default_affiliation);
85
86 let mut builder = CotEvent::builder()
87 .uid(&track.track_id)
88 .cot_type(cot_type)
89 .time(track.timestamp)
90 .stale_duration(Duration::seconds(self.config.track_stale_secs))
91 .point(CotPoint::with_full(
92 track.position.lat,
93 track.position.lon,
94 track.position.hae.unwrap_or(0.0),
95 track.position.cep_m.unwrap_or(9999999.0),
96 9999999.0, ))
98 .remarks(&self.format_track_remarks(track));
99
100 if let Some(ref vel) = track.velocity {
102 builder = builder.track(vel.bearing, vel.speed_mps);
103 }
104
105 if self.config.include_peat_extension {
107 let mut ext = PeatExtension::new()
108 .with_source(PeatSource::new(
109 &track.source_platform,
110 &track.source_model,
111 &track.model_version,
112 ))
113 .with_confidence(track.confidence, Some(0.70));
114
115 if track.cell_id.is_some() || track.formation_id.is_some() {
117 let mut hier = PeatHierarchy::new();
118 if let Some(ref cell_id) = track.cell_id {
119 hier = hier.with_cell(cell_id, Some("tracker"));
120 }
121 if let Some(ref formation_id) = track.formation_id {
122 hier = hier.with_formation(formation_id);
123 }
124 ext = ext.with_hierarchy(hier);
125 }
126
127 for (key, value) in &track.attributes {
129 let (val_str, type_str) = self.json_value_to_attr(value);
130 ext = ext.with_attribute(key, &val_str, &type_str);
131 }
132
133 builder = builder.peat_extension(ext);
134 }
135
136 builder = builder.link(
138 CotLink::new(&track.source_platform, "a-f-G-U-C", CotRelation::Observing)
139 .with_remarks("sensor-platform"),
140 );
141
142 if let Some(ref cell_id) = track.cell_id {
144 builder = builder.link(
145 CotLink::new(cell_id, "a-f-G-U-C", CotRelation::Parent).with_remarks("parent-cell"),
146 );
147 }
148
149 builder.build()
150 }
151
152 pub fn encode_track_update(&self, track: &TrackUpdate) -> Result<String, CotError> {
154 self.track_update_to_event(track)?.to_xml()
155 }
156
157 pub fn capability_to_event(&self, cap: &CapabilityAdvertisement) -> Result<CotEvent, CotError> {
159 let cot_type = self
160 .type_mapper
161 .map_platform(&cap.platform_type, self.config.default_affiliation);
162
163 let mut builder = CotEvent::builder()
164 .uid(&cap.platform_id)
165 .cot_type(cot_type)
166 .time(cap.timestamp)
167 .stale_duration(Duration::seconds(self.config.capability_stale_secs))
168 .point(CotPoint::with_full(
169 cap.position.lat,
170 cap.position.lon,
171 cap.position.hae.unwrap_or(0.0),
172 cap.position.cep_m.unwrap_or(9999999.0),
173 9999999.0,
174 ))
175 .callsign(&cap.platform_id)
176 .remarks(&self.format_capability_remarks(cap));
177
178 if let Some(ref cell_id) = cap.cell_id {
180 builder = builder.group(cell_id, "Team Member");
181 }
182
183 if self.config.include_peat_extension {
185 let mut ext =
186 PeatExtension::new().with_status(PeatStatus::new(cap.status, cap.readiness));
187
188 if cap.cell_id.is_some() || cap.formation_id.is_some() {
190 let mut hier = PeatHierarchy::new();
191 if let Some(ref cell_id) = cap.cell_id {
192 hier = hier.with_cell(cell_id, None);
193 }
194 if let Some(ref formation_id) = cap.formation_id {
195 hier = hier.with_formation(formation_id);
196 }
197 ext = ext.with_hierarchy(hier);
198 }
199
200 for cap_info in &cap.capabilities {
202 ext = ext.with_capability(PeatCapability::new(
203 &cap_info.capability_type,
204 &cap_info.version,
205 cap_info.precision,
206 cap_info.status,
207 ));
208 }
209
210 builder = builder.peat_extension(ext);
211 }
212
213 if let Some(ref cell_id) = cap.cell_id {
215 builder = builder.link(
216 CotLink::new(cell_id, "a-f-G-U-C", CotRelation::Parent).with_remarks("parent-cell"),
217 );
218 }
219
220 builder.build()
221 }
222
223 pub fn encode_capability_advertisement(
225 &self,
226 cap: &CapabilityAdvertisement,
227 ) -> Result<String, CotError> {
228 self.capability_to_event(cap)?.to_xml()
229 }
230
231 pub fn handoff_to_event(&self, handoff: &HandoffMessage) -> Result<CotEvent, CotError> {
233 let cot_type = CotTypeMapper::handoff_type();
234
235 let mut builder = CotEvent::builder()
236 .uid(&format!("HANDOFF-{}", handoff.track_id))
237 .cot_type(cot_type)
238 .time(handoff.timestamp)
239 .stale_duration(Duration::seconds(self.config.handoff_stale_secs))
240 .point(CotPoint::with_full(
241 handoff.position.lat,
242 handoff.position.lon,
243 handoff.position.hae.unwrap_or(0.0),
244 handoff.position.cep_m.unwrap_or(9999999.0),
245 9999999.0,
246 ))
247 .remarks(&format!(
248 "Track {} handoff: {} → {} ({})",
249 handoff.track_id, handoff.source_cell, handoff.target_cell, handoff.reason
250 ));
251
252 let flow_priority = match handoff.priority {
254 1 => "flash",
255 2 => "immediate",
256 3 => "routine",
257 4 => "deferred",
258 _ => "bulk",
259 };
260 builder = builder.flow_priority(flow_priority);
261
262 if self.config.include_peat_extension {
264 let ext = PeatExtension::new().with_handoff(PeatHandoff::new(
265 &handoff.source_cell,
266 &handoff.target_cell,
267 handoff.state.as_str(),
268 &handoff.reason,
269 ));
270
271 builder = builder.peat_extension(ext);
272 }
273
274 builder = builder
276 .link(
277 CotLink::new(&handoff.source_cell, "a-f-G-U-C", CotRelation::Handoff)
278 .with_remarks("handoff-source"),
279 )
280 .link(
281 CotLink::new(&handoff.target_cell, "a-f-G-U-C", CotRelation::Handoff)
282 .with_remarks("handoff-target"),
283 )
284 .link(
285 CotLink::new(&handoff.track_id, "a-f-G-E-S", CotRelation::Observing)
286 .with_remarks("handoff-track"),
287 );
288
289 builder.build()
290 }
291
292 pub fn encode_handoff(&self, handoff: &HandoffMessage) -> Result<String, CotError> {
294 self.handoff_to_event(handoff)?.to_xml()
295 }
296
297 pub fn formation_summary_to_event(
299 &self,
300 summary: &FormationCapabilitySummary,
301 ) -> Result<CotEvent, CotError> {
302 let cot_type = CotTypeMapper::formation_marker_type(self.config.default_affiliation);
303
304 let mut builder = CotEvent::builder()
305 .uid(&summary.formation_id)
306 .cot_type(cot_type)
307 .time(summary.timestamp)
308 .stale_duration(Duration::seconds(self.config.capability_stale_secs))
309 .point(CotPoint::with_full(
310 summary.center_position.lat,
311 summary.center_position.lon,
312 summary.center_position.hae.unwrap_or(0.0),
313 summary.center_position.cep_m.unwrap_or(9999999.0),
314 9999999.0,
315 ))
316 .callsign(&summary.callsign)
317 .remarks(&format!(
318 "Formation {} - {} platforms, {} cells, {:.0}% ready",
319 summary.callsign,
320 summary.platform_count,
321 summary.cell_count,
322 summary.readiness * 100.0
323 ));
324
325 if self.config.include_peat_extension {
327 let mut ext = PeatExtension::new()
328 .with_status(PeatStatus::new(
329 super::types::OperationalStatus::Active,
330 summary.readiness,
331 ))
332 .with_hierarchy(PeatHierarchy::new().with_formation(&summary.formation_id));
333
334 for agg_cap in &summary.capabilities {
336 ext = ext.with_capability(PeatCapability::new(
337 &agg_cap.capability_type,
338 &format!("{} units", agg_cap.count),
339 agg_cap.avg_precision,
340 if agg_cap.availability > 0.8 {
341 super::types::OperationalStatus::Active
342 } else if agg_cap.availability > 0.5 {
343 super::types::OperationalStatus::Degraded
344 } else {
345 super::types::OperationalStatus::Offline
346 },
347 ));
348 }
349
350 builder = builder.peat_extension(ext);
351 }
352
353 builder.build()
354 }
355
356 pub fn encode_formation_summary(
358 &self,
359 summary: &FormationCapabilitySummary,
360 ) -> Result<String, CotError> {
361 self.formation_summary_to_event(summary)?.to_xml()
362 }
363
364 fn format_track_remarks(&self, track: &TrackUpdate) -> String {
365 let mut remarks = format!(
366 "{}: {:.0}% confidence",
367 track.classification,
368 track.confidence * 100.0
369 );
370
371 for (key, value) in &track.attributes {
373 if let serde_json::Value::String(s) = value {
374 remarks.push_str(&format!(", {}={}", key, s));
375 } else if let serde_json::Value::Bool(b) = value {
376 if *b {
377 remarks.push_str(&format!(", {}", key));
378 }
379 }
380 }
381
382 remarks
383 }
384
385 fn format_capability_remarks(&self, cap: &CapabilityAdvertisement) -> String {
386 let cap_list: Vec<_> = cap
387 .capabilities
388 .iter()
389 .map(|c| c.capability_type.as_str())
390 .collect();
391
392 format!(
393 "{} ({}) - {} ({:.0}% ready)",
394 cap.platform_type,
395 cap_list.join(", "),
396 cap.status.as_str(),
397 cap.readiness * 100.0
398 )
399 }
400
401 fn json_value_to_attr(&self, value: &serde_json::Value) -> (String, String) {
402 match value {
403 serde_json::Value::String(s) => (s.clone(), "string".to_string()),
404 serde_json::Value::Bool(b) => (b.to_string(), "boolean".to_string()),
405 serde_json::Value::Number(n) => (n.to_string(), "number".to_string()),
406 _ => (value.to_string(), "json".to_string()),
407 }
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use crate::cot::types::{CapabilityInfo, OperationalStatus, Position, Velocity};
415
416 #[test]
417 fn test_encode_track_update() {
418 let encoder = CotEncoder::new();
419
420 let track = TrackUpdate::new(
421 "TRACK-001".to_string(),
422 "person".to_string(),
423 0.89,
424 Position::with_accuracy(33.7749, -84.3958, 2.5),
425 "Alpha-2".to_string(),
426 "object_tracker".to_string(),
427 "1.3.0".to_string(),
428 )
429 .with_velocity(Velocity::new(45.0, 1.2))
430 .with_attribute("jacket_color", serde_json::json!("blue"))
431 .with_cell("Alpha-Team".to_string());
432
433 let xml = encoder.encode_track_update(&track).unwrap();
434
435 assert!(xml.contains("uid=\"TRACK-001\""));
436 assert!(xml.contains("type=\"a-f-G-E-S\""));
437 assert!(xml.contains("lat=\"33.7749\""));
438 assert!(xml.contains("<_peat_"));
439 assert!(xml.contains("platform=\"Alpha-2\""));
440 assert!(xml.contains("jacket_color"));
441 }
442
443 #[test]
444 fn test_encode_capability_advertisement() {
445 let encoder = CotEncoder::new();
446
447 let cap = CapabilityAdvertisement::new(
448 "Alpha-3".to_string(),
449 "UGV".to_string(),
450 Position::new(33.7749, -84.3958),
451 OperationalStatus::Active,
452 0.91,
453 )
454 .with_capability(CapabilityInfo {
455 capability_type: "OBJECT_TRACKING".to_string(),
456 model_name: "object_tracker".to_string(),
457 version: "1.3.0".to_string(),
458 precision: 0.94,
459 status: OperationalStatus::Active,
460 })
461 .with_cell("Alpha-Team".to_string());
462
463 let xml = encoder.encode_capability_advertisement(&cap).unwrap();
464
465 assert!(xml.contains("uid=\"Alpha-3\""));
466 assert!(xml.contains("callsign=\"Alpha-3\""));
467 assert!(xml.contains("__group"));
468 assert!(xml.contains("<capability"));
469 }
470
471 #[test]
472 fn test_encode_handoff() {
473 let encoder = CotEncoder::new();
474
475 let handoff = HandoffMessage::new(
476 "TRACK-001".to_string(),
477 Position::new(33.78, -84.40),
478 "Alpha-Team".to_string(),
479 "Bravo-Team".to_string(),
480 "boundary_crossing".to_string(),
481 )
482 .with_priority(2);
483
484 let xml = encoder.encode_handoff(&handoff).unwrap();
485
486 assert!(xml.contains("uid=\"HANDOFF-TRACK-001\""));
487 assert!(xml.contains("type=\"a-x-h-h\""));
488 assert!(xml.contains("<handoff"));
489 assert!(xml.contains("priority=\"immediate\""));
490 }
491
492 #[test]
493 fn test_encoder_without_peat_extension() {
494 let config = CotEncoderConfig {
495 include_peat_extension: false,
496 ..Default::default()
497 };
498
499 let encoder = CotEncoder::with_config(config);
500
501 let track = TrackUpdate::new(
502 "TRACK-001".to_string(),
503 "person".to_string(),
504 0.89,
505 Position::new(0.0, 0.0),
506 "platform".to_string(),
507 "model".to_string(),
508 "1.0".to_string(),
509 );
510
511 let xml = encoder.encode_track_update(&track).unwrap();
512
513 assert!(!xml.contains("<_peat_"));
514 }
515
516 #[test]
517 fn test_priority_to_flow_tags() {
518 let encoder = CotEncoder::new();
519
520 for (priority, expected_tag) in [
521 (1u8, "flash"),
522 (2, "immediate"),
523 (3, "routine"),
524 (4, "deferred"),
525 (5, "bulk"),
526 ] {
527 let handoff = HandoffMessage::new(
528 "TRACK".to_string(),
529 Position::new(0.0, 0.0),
530 "src".to_string(),
531 "dst".to_string(),
532 "test".to_string(),
533 )
534 .with_priority(priority);
535
536 let xml = encoder.encode_handoff(&handoff).unwrap();
537 assert!(
538 xml.contains(&format!("priority=\"{}\"", expected_tag)),
539 "Priority {} should map to {}",
540 priority,
541 expected_tag
542 );
543 }
544 }
545
546 #[test]
547 fn test_custom_type_mapping() {
548 let mut encoder = CotEncoder::new();
549 encoder
550 .type_mapper_mut()
551 .add_mapping("special_target", "a-h-G-I-T");
552
553 let track = TrackUpdate::new(
554 "TRACK-001".to_string(),
555 "special_target".to_string(),
556 0.95,
557 Position::new(0.0, 0.0),
558 "platform".to_string(),
559 "model".to_string(),
560 "1.0".to_string(),
561 );
562
563 let xml = encoder.encode_track_update(&track).unwrap();
564 assert!(xml.contains("type=\"a-h-G-I-T\""));
565 }
566}