1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Position {
13 pub lat: f64,
15 pub lon: f64,
17 pub cep_m: Option<f64>,
19 pub hae: Option<f64>,
21}
22
23impl Position {
24 pub fn new(lat: f64, lon: f64) -> Self {
26 Self {
27 lat,
28 lon,
29 cep_m: None,
30 hae: None,
31 }
32 }
33
34 pub fn with_accuracy(lat: f64, lon: f64, cep_m: f64) -> Self {
36 Self {
37 lat,
38 lon,
39 cep_m: Some(cep_m),
40 hae: None,
41 }
42 }
43
44 pub fn with_altitude(lat: f64, lon: f64, hae: f64, cep_m: Option<f64>) -> Self {
46 Self {
47 lat,
48 lon,
49 cep_m,
50 hae: Some(hae),
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct Velocity {
58 pub bearing: f64,
60 pub speed_mps: f64,
62}
63
64impl Velocity {
65 pub fn new(bearing: f64, speed_mps: f64) -> Self {
67 Self { bearing, speed_mps }
68 }
69
70 pub fn is_stationary(&self, threshold_mps: f64) -> bool {
72 self.speed_mps < threshold_mps
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct TrackUpdate {
81 pub track_id: String,
83 pub classification: String,
85 pub confidence: f64,
87 pub position: Position,
89 pub velocity: Option<Velocity>,
91 pub attributes: HashMap<String, serde_json::Value>,
93 pub source_platform: String,
95 pub source_model: String,
97 pub model_version: String,
99 pub timestamp: DateTime<Utc>,
101 pub cell_id: Option<String>,
103 pub formation_id: Option<String>,
105}
106
107impl TrackUpdate {
108 pub fn new(
110 track_id: String,
111 classification: String,
112 confidence: f64,
113 position: Position,
114 source_platform: String,
115 source_model: String,
116 model_version: String,
117 ) -> Self {
118 Self {
119 track_id,
120 classification,
121 confidence: confidence.clamp(0.0, 1.0),
122 position,
123 velocity: None,
124 attributes: HashMap::new(),
125 source_platform,
126 source_model,
127 model_version,
128 timestamp: Utc::now(),
129 cell_id: None,
130 formation_id: None,
131 }
132 }
133
134 pub fn with_attribute(mut self, key: &str, value: serde_json::Value) -> Self {
136 self.attributes.insert(key.to_string(), value);
137 self
138 }
139
140 pub fn with_velocity(mut self, velocity: Velocity) -> Self {
142 self.velocity = Some(velocity);
143 self
144 }
145
146 pub fn with_cell(mut self, cell_id: String) -> Self {
148 self.cell_id = Some(cell_id);
149 self
150 }
151
152 pub fn with_formation(mut self, formation_id: String) -> Self {
154 self.formation_id = Some(formation_id);
155 self
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161pub enum OperationalStatus {
162 Ready,
164 Active,
166 Degraded,
168 Offline,
170 Loading,
172}
173
174impl OperationalStatus {
175 pub fn as_str(&self) -> &'static str {
177 match self {
178 Self::Ready => "READY",
179 Self::Active => "ACTIVE",
180 Self::Degraded => "DEGRADED",
181 Self::Offline => "OFFLINE",
182 Self::Loading => "LOADING",
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct CapabilityAdvertisement {
192 pub platform_id: String,
194 pub platform_type: String,
196 pub position: Position,
198 pub status: OperationalStatus,
200 pub readiness: f64,
202 pub capabilities: Vec<CapabilityInfo>,
204 pub cell_id: Option<String>,
206 pub formation_id: Option<String>,
208 pub timestamp: DateTime<Utc>,
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub struct CapabilityInfo {
215 pub capability_type: String,
217 pub model_name: String,
219 pub version: String,
221 pub precision: f64,
223 pub status: OperationalStatus,
225}
226
227impl CapabilityAdvertisement {
228 pub fn new(
230 platform_id: String,
231 platform_type: String,
232 position: Position,
233 status: OperationalStatus,
234 readiness: f64,
235 ) -> Self {
236 Self {
237 platform_id,
238 platform_type,
239 position,
240 status,
241 readiness: readiness.clamp(0.0, 1.0),
242 capabilities: Vec::new(),
243 cell_id: None,
244 formation_id: None,
245 timestamp: Utc::now(),
246 }
247 }
248
249 pub fn with_capability(mut self, capability: CapabilityInfo) -> Self {
251 self.capabilities.push(capability);
252 self
253 }
254
255 pub fn with_cell(mut self, cell_id: String) -> Self {
257 self.cell_id = Some(cell_id);
258 self
259 }
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
264pub enum HandoffState {
265 Initiated,
267 Accepted,
269 Transferred,
271 Completed,
273 Failed,
275}
276
277impl HandoffState {
278 pub fn as_str(&self) -> &'static str {
280 match self {
281 Self::Initiated => "INITIATED",
282 Self::Accepted => "ACCEPTED",
283 Self::Transferred => "TRANSFERRED",
284 Self::Completed => "COMPLETED",
285 Self::Failed => "FAILED",
286 }
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct HandoffMessage {
293 pub track_id: String,
295 pub position: Position,
297 pub source_cell: String,
299 pub target_cell: String,
301 pub state: HandoffState,
303 pub reason: String,
305 pub priority: u8,
307 pub timestamp: DateTime<Utc>,
309}
310
311impl HandoffMessage {
312 pub fn new(
314 track_id: String,
315 position: Position,
316 source_cell: String,
317 target_cell: String,
318 reason: String,
319 ) -> Self {
320 Self {
321 track_id,
322 position,
323 source_cell,
324 target_cell,
325 state: HandoffState::Initiated,
326 reason,
327 priority: 3, timestamp: Utc::now(),
329 }
330 }
331
332 pub fn with_priority(mut self, priority: u8) -> Self {
334 self.priority = priority.clamp(1, 5);
335 self
336 }
337
338 pub fn with_state(mut self, state: HandoffState) -> Self {
340 self.state = state;
341 self
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347pub struct FormationCapabilitySummary {
348 pub formation_id: String,
350 pub callsign: String,
352 pub center_position: Position,
354 pub platform_count: u32,
356 pub cell_count: u32,
358 pub capabilities: Vec<AggregatedCapability>,
360 pub readiness: f64,
362 pub timestamp: DateTime<Utc>,
364}
365
366#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
368pub struct AggregatedCapability {
369 pub capability_type: String,
371 pub count: u32,
373 pub avg_precision: f64,
375 pub availability: f64,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
387pub enum MissionTaskType {
388 TrackTarget,
390 SearchArea,
392 MonitorZone,
394 Abort,
396}
397
398impl MissionTaskType {
399 pub fn as_str(&self) -> &'static str {
401 match self {
402 Self::TrackTarget => "TRACK_TARGET",
403 Self::SearchArea => "SEARCH_AREA",
404 Self::MonitorZone => "MONITOR_ZONE",
405 Self::Abort => "ABORT",
406 }
407 }
408
409 pub fn from_cot_type(cot_type: &str) -> Option<Self> {
411 match cot_type {
412 "t-x-m-c-c" => Some(Self::TrackTarget),
413 "t-x-m-c-s" => Some(Self::SearchArea),
414 "t-x-m-c-a" => Some(Self::Abort),
415 _ if cot_type.starts_with("t-x-m-c") => Some(Self::TrackTarget), _ => None,
417 }
418 }
419}
420
421#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
423pub enum MissionPriority {
424 Critical,
425 High,
426 #[default]
427 Normal,
428 Low,
429}
430
431impl MissionPriority {
432 pub fn as_str(&self) -> &'static str {
433 match self {
434 Self::Critical => "CRITICAL",
435 Self::High => "HIGH",
436 Self::Normal => "NORMAL",
437 Self::Low => "LOW",
438 }
439 }
440}
441
442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
444pub struct MissionTarget {
445 pub description: String,
447 pub last_known_position: Option<Position>,
449}
450
451#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
453pub struct MissionBoundary {
454 pub boundary_type: BoundaryType,
456 pub coordinates: Vec<Position>,
458 pub radius_m: Option<f64>,
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
464pub enum BoundaryType {
465 Polygon,
466 Circle,
467}
468
469#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
479pub struct MissionTask {
480 pub task_id: String,
482 pub task_type: MissionTaskType,
484 pub issued_at: DateTime<Utc>,
486 pub issued_by: String,
488 pub expires_at: DateTime<Utc>,
490 pub target: Option<MissionTarget>,
492 pub boundary: Option<MissionBoundary>,
494 pub priority: MissionPriority,
496 pub objective_position: Option<Position>,
498 pub remarks: Option<String>,
500}
501
502impl MissionTask {
503 pub fn new(
505 task_id: String,
506 task_type: MissionTaskType,
507 issued_by: String,
508 expires_at: DateTime<Utc>,
509 ) -> Self {
510 Self {
511 task_id,
512 task_type,
513 issued_at: Utc::now(),
514 issued_by,
515 expires_at,
516 target: None,
517 boundary: None,
518 priority: MissionPriority::Normal,
519 objective_position: None,
520 remarks: None,
521 }
522 }
523
524 pub fn from_cot_event(event: &super::CotEvent) -> Result<Self, MissionTaskError> {
528 let task_type = MissionTaskType::from_cot_type(event.cot_type.as_str()).ok_or(
529 MissionTaskError::InvalidCotType(event.cot_type.as_str().to_string()),
530 )?;
531
532 let mut task = Self {
533 task_id: event.uid.clone(),
534 task_type,
535 issued_at: event.time,
536 issued_by: "TAK-Server".to_string(), expires_at: event.stale,
538 target: None,
539 boundary: None,
540 priority: MissionPriority::Normal,
541 objective_position: Some(Position::with_altitude(
542 event.point.lat,
543 event.point.lon,
544 event.point.hae,
545 Some(event.point.ce),
546 )),
547 remarks: event.detail.remarks.clone(),
548 };
549
550 if let Some(ref remarks) = event.detail.remarks {
552 task.target = Some(MissionTarget {
553 description: remarks.clone(),
554 last_known_position: task.objective_position.clone(),
555 });
556 }
557
558 Ok(task)
559 }
560
561 pub fn with_target(mut self, target: MissionTarget) -> Self {
563 self.target = Some(target);
564 self
565 }
566
567 pub fn with_boundary(mut self, boundary: MissionBoundary) -> Self {
569 self.boundary = Some(boundary);
570 self
571 }
572
573 pub fn with_priority(mut self, priority: MissionPriority) -> Self {
575 self.priority = priority;
576 self
577 }
578
579 pub fn with_objective_position(mut self, position: Position) -> Self {
581 self.objective_position = Some(position);
582 self
583 }
584
585 pub fn is_expired(&self) -> bool {
587 Utc::now() > self.expires_at
588 }
589
590 pub fn is_mission_cot_type(cot_type: &str) -> bool {
592 cot_type.starts_with("t-x-m-c")
593 }
594
595 pub fn to_json(&self) -> Result<String, serde_json::Error> {
597 serde_json::to_string(self)
598 }
599
600 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
602 serde_json::from_str(json)
603 }
604}
605
606#[derive(Debug, Clone, PartialEq)]
608pub enum MissionTaskError {
609 InvalidCotType(String),
611 MissingField(&'static str),
613}
614
615impl std::fmt::Display for MissionTaskError {
616 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617 match self {
618 Self::InvalidCotType(t) => write!(f, "Invalid CoT type for mission task: {}", t),
619 Self::MissingField(field) => write!(f, "Missing required field: {}", field),
620 }
621 }
622}
623
624impl std::error::Error for MissionTaskError {}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629
630 #[test]
631 fn test_position_creation() {
632 let pos = Position::new(33.7749, -84.3958);
633 assert_eq!(pos.lat, 33.7749);
634 assert_eq!(pos.lon, -84.3958);
635 assert!(pos.cep_m.is_none());
636 assert!(pos.hae.is_none());
637 }
638
639 #[test]
640 fn test_position_with_accuracy() {
641 let pos = Position::with_accuracy(33.7749, -84.3958, 2.5);
642 assert_eq!(pos.cep_m, Some(2.5));
643 }
644
645 #[test]
646 fn test_position_with_altitude() {
647 let pos = Position::with_altitude(33.7749, -84.3958, 100.0, Some(2.5));
648 assert_eq!(pos.hae, Some(100.0));
649 assert_eq!(pos.cep_m, Some(2.5));
650 }
651
652 #[test]
653 fn test_velocity_stationary() {
654 let moving = Velocity::new(45.0, 5.0);
655 assert!(!moving.is_stationary(0.5));
656
657 let stationary = Velocity::new(0.0, 0.1);
658 assert!(stationary.is_stationary(0.5));
659 }
660
661 #[test]
662 fn test_track_update_creation() {
663 let track = TrackUpdate::new(
664 "TRACK-001".to_string(),
665 "person".to_string(),
666 0.89,
667 Position::new(33.7749, -84.3958),
668 "Alpha-2".to_string(),
669 "object_tracker".to_string(),
670 "1.3.0".to_string(),
671 );
672
673 assert_eq!(track.track_id, "TRACK-001");
674 assert_eq!(track.confidence, 0.89);
675 }
676
677 #[test]
678 fn test_track_update_confidence_clamped() {
679 let track = TrackUpdate::new(
680 "TRACK-001".to_string(),
681 "person".to_string(),
682 1.5, Position::new(0.0, 0.0),
684 "platform".to_string(),
685 "model".to_string(),
686 "1.0".to_string(),
687 );
688
689 assert_eq!(track.confidence, 1.0);
690 }
691
692 #[test]
693 fn test_track_update_with_attributes() {
694 let track = TrackUpdate::new(
695 "TRACK-001".to_string(),
696 "person".to_string(),
697 0.89,
698 Position::new(0.0, 0.0),
699 "platform".to_string(),
700 "model".to_string(),
701 "1.0".to_string(),
702 )
703 .with_attribute("jacket_color", serde_json::json!("blue"))
704 .with_attribute("has_backpack", serde_json::json!(true));
705
706 assert_eq!(track.attributes.len(), 2);
707 assert_eq!(track.attributes["jacket_color"], "blue");
708 }
709
710 #[test]
711 fn test_capability_advertisement() {
712 let cap = CapabilityAdvertisement::new(
713 "Alpha-3".to_string(),
714 "UGV".to_string(),
715 Position::new(33.7749, -84.3958),
716 OperationalStatus::Active,
717 0.91,
718 )
719 .with_capability(CapabilityInfo {
720 capability_type: "OBJECT_TRACKING".to_string(),
721 model_name: "object_tracker".to_string(),
722 version: "1.3.0".to_string(),
723 precision: 0.94,
724 status: OperationalStatus::Active,
725 });
726
727 assert_eq!(cap.capabilities.len(), 1);
728 assert_eq!(cap.status.as_str(), "ACTIVE");
729 }
730
731 #[test]
732 fn test_handoff_message() {
733 let handoff = HandoffMessage::new(
734 "TRACK-001".to_string(),
735 Position::new(33.78, -84.40),
736 "Alpha-Team".to_string(),
737 "Bravo-Team".to_string(),
738 "boundary_crossing".to_string(),
739 )
740 .with_priority(2);
741
742 assert_eq!(handoff.state, HandoffState::Initiated);
743 assert_eq!(handoff.priority, 2);
744 }
745
746 #[test]
747 fn test_handoff_priority_clamped() {
748 let handoff = HandoffMessage::new(
749 "TRACK-001".to_string(),
750 Position::new(0.0, 0.0),
751 "source".to_string(),
752 "target".to_string(),
753 "test".to_string(),
754 )
755 .with_priority(10); assert_eq!(handoff.priority, 5);
758 }
759
760 #[test]
761 fn test_operational_status_strings() {
762 assert_eq!(OperationalStatus::Ready.as_str(), "READY");
763 assert_eq!(OperationalStatus::Active.as_str(), "ACTIVE");
764 assert_eq!(OperationalStatus::Degraded.as_str(), "DEGRADED");
765 assert_eq!(OperationalStatus::Offline.as_str(), "OFFLINE");
766 assert_eq!(OperationalStatus::Loading.as_str(), "LOADING");
767 }
768
769 #[test]
770 fn test_handoff_state_strings() {
771 assert_eq!(HandoffState::Initiated.as_str(), "INITIATED");
772 assert_eq!(HandoffState::Accepted.as_str(), "ACCEPTED");
773 assert_eq!(HandoffState::Transferred.as_str(), "TRANSFERRED");
774 assert_eq!(HandoffState::Completed.as_str(), "COMPLETED");
775 assert_eq!(HandoffState::Failed.as_str(), "FAILED");
776 }
777
778 #[test]
783 fn test_mission_task_type_from_cot() {
784 assert_eq!(
785 MissionTaskType::from_cot_type("t-x-m-c-c"),
786 Some(MissionTaskType::TrackTarget)
787 );
788 assert_eq!(
789 MissionTaskType::from_cot_type("t-x-m-c-s"),
790 Some(MissionTaskType::SearchArea)
791 );
792 assert_eq!(
793 MissionTaskType::from_cot_type("t-x-m-c-a"),
794 Some(MissionTaskType::Abort)
795 );
796 assert_eq!(
798 MissionTaskType::from_cot_type("t-x-m-c-z"),
799 Some(MissionTaskType::TrackTarget)
800 );
801 assert_eq!(MissionTaskType::from_cot_type("a-f-G-U-C"), None);
803 }
804
805 #[test]
806 fn test_mission_task_type_as_str() {
807 assert_eq!(MissionTaskType::TrackTarget.as_str(), "TRACK_TARGET");
808 assert_eq!(MissionTaskType::SearchArea.as_str(), "SEARCH_AREA");
809 assert_eq!(MissionTaskType::MonitorZone.as_str(), "MONITOR_ZONE");
810 assert_eq!(MissionTaskType::Abort.as_str(), "ABORT");
811 }
812
813 #[test]
814 fn test_mission_priority_default() {
815 let priority = MissionPriority::default();
816 assert_eq!(priority, MissionPriority::Normal);
817 }
818
819 #[test]
820 fn test_mission_priority_as_str() {
821 assert_eq!(MissionPriority::Critical.as_str(), "CRITICAL");
822 assert_eq!(MissionPriority::High.as_str(), "HIGH");
823 assert_eq!(MissionPriority::Normal.as_str(), "NORMAL");
824 assert_eq!(MissionPriority::Low.as_str(), "LOW");
825 }
826
827 #[test]
828 fn test_mission_task_new() {
829 let expires = Utc::now() + chrono::Duration::hours(2);
830 let task = MissionTask::new(
831 "MISSION-001".to_string(),
832 MissionTaskType::TrackTarget,
833 "CMD-ALPHA".to_string(),
834 expires,
835 );
836
837 assert_eq!(task.task_id, "MISSION-001");
838 assert_eq!(task.task_type, MissionTaskType::TrackTarget);
839 assert_eq!(task.issued_by, "CMD-ALPHA");
840 assert_eq!(task.priority, MissionPriority::Normal);
841 assert!(task.target.is_none());
842 assert!(task.boundary.is_none());
843 }
844
845 #[test]
846 fn test_mission_task_with_builder_methods() {
847 let expires = Utc::now() + chrono::Duration::hours(1);
848 let task = MissionTask::new(
849 "MISSION-002".to_string(),
850 MissionTaskType::SearchArea,
851 "CMD-BRAVO".to_string(),
852 expires,
853 )
854 .with_priority(MissionPriority::High)
855 .with_objective_position(Position::new(33.7749, -84.3958))
856 .with_target(MissionTarget {
857 description: "Suspicious vehicle".to_string(),
858 last_known_position: Some(Position::new(33.77, -84.39)),
859 });
860
861 assert_eq!(task.priority, MissionPriority::High);
862 assert!(task.objective_position.is_some());
863 assert!(task.target.is_some());
864 assert_eq!(
865 task.target.as_ref().unwrap().description,
866 "Suspicious vehicle"
867 );
868 }
869
870 #[test]
871 fn test_mission_task_is_expired() {
872 let past_expires = Utc::now() - chrono::Duration::hours(1);
873 let task = MissionTask::new(
874 "EXPIRED-001".to_string(),
875 MissionTaskType::TrackTarget,
876 "CMD".to_string(),
877 past_expires,
878 );
879 assert!(task.is_expired());
880
881 let future_expires = Utc::now() + chrono::Duration::hours(1);
882 let active_task = MissionTask::new(
883 "ACTIVE-001".to_string(),
884 MissionTaskType::TrackTarget,
885 "CMD".to_string(),
886 future_expires,
887 );
888 assert!(!active_task.is_expired());
889 }
890
891 #[test]
892 fn test_mission_task_is_mission_cot_type() {
893 assert!(MissionTask::is_mission_cot_type("t-x-m-c-c"));
894 assert!(MissionTask::is_mission_cot_type("t-x-m-c-s"));
895 assert!(MissionTask::is_mission_cot_type("t-x-m-c-a"));
896 assert!(!MissionTask::is_mission_cot_type("a-f-G-U-C"));
897 assert!(!MissionTask::is_mission_cot_type("b-m-p-s-p-l"));
898 }
899
900 #[test]
901 fn test_mission_task_json_roundtrip() {
902 let expires = Utc::now() + chrono::Duration::hours(2);
903 let task = MissionTask::new(
904 "JSON-001".to_string(),
905 MissionTaskType::SearchArea,
906 "CMD".to_string(),
907 expires,
908 )
909 .with_priority(MissionPriority::Critical)
910 .with_objective_position(Position::new(33.7749, -84.3958));
911
912 let json = task.to_json().expect("should serialize");
913 let restored = MissionTask::from_json(&json).expect("should deserialize");
914
915 assert_eq!(restored.task_id, task.task_id);
916 assert_eq!(restored.task_type, task.task_type);
917 assert_eq!(restored.priority, task.priority);
918 }
919
920 #[test]
921 fn test_mission_task_error_display() {
922 let err = MissionTaskError::InvalidCotType("a-f-G-U-C".to_string());
923 assert!(err.to_string().contains("Invalid CoT type"));
924
925 let err = MissionTaskError::MissingField("target");
926 assert!(err.to_string().contains("Missing required field"));
927 }
928
929 #[test]
930 fn test_mission_task_from_cot_event() {
931 use crate::cot::{CotEvent, CotPoint, CotType};
932
933 let event = CotEvent {
935 version: "2.0".to_string(),
936 uid: "MISSION-TAK-001".to_string(),
937 cot_type: CotType::new("t-x-m-c-c"), time: Utc::now(),
939 start: Utc::now(),
940 stale: Utc::now() + chrono::Duration::hours(2),
941 how: "m-g".to_string(),
942 point: CotPoint {
943 lat: 33.7749,
944 lon: -84.3958,
945 hae: 300.0,
946 ce: 10.0,
947 le: 10.0,
948 },
949 detail: crate::cot::CotDetail {
950 contact_callsign: Some("CMD-001".to_string()),
951 remarks: Some("Track suspicious vehicle in sector alpha".to_string()),
952 ..Default::default()
953 },
954 };
955
956 let task = MissionTask::from_cot_event(&event).expect("should convert");
957
958 assert_eq!(task.task_id, "MISSION-TAK-001");
959 assert_eq!(task.task_type, MissionTaskType::TrackTarget);
960 assert!(task.target.is_some());
961 assert_eq!(
962 task.target.as_ref().unwrap().description,
963 "Track suspicious vehicle in sector alpha"
964 );
965 assert!(task.objective_position.is_some());
966 let pos = task.objective_position.as_ref().unwrap();
967 assert_eq!(pos.lat, 33.7749);
968 assert_eq!(pos.lon, -84.3958);
969 }
970
971 #[test]
972 fn test_mission_task_from_cot_event_invalid_type() {
973 use crate::cot::{CotEvent, CotPoint, CotType};
974
975 let event = CotEvent {
977 version: "2.0".to_string(),
978 uid: "UNIT-001".to_string(),
979 cot_type: CotType::new("a-f-G-U-C"), time: Utc::now(),
981 start: Utc::now(),
982 stale: Utc::now() + chrono::Duration::hours(1),
983 how: "m-g".to_string(),
984 point: CotPoint::new(0.0, 0.0),
985 detail: Default::default(),
986 };
987
988 let result = MissionTask::from_cot_event(&event);
989 assert!(result.is_err());
990 assert!(matches!(
991 result.unwrap_err(),
992 MissionTaskError::InvalidCotType(_)
993 ));
994 }
995
996 #[test]
998 fn test_mission_task_from_xml_end_to_end() {
999 use crate::cot::CotEvent;
1000
1001 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1003 <event uid="TASK-20251208-001" type="t-x-m-c-c" time="2025-12-08T14:05:00Z"
1004 start="2025-12-08T14:05:00Z" stale="2025-12-08T16:05:00Z" how="h-g-i-g-o">
1005 <point lat="33.7756" lon="-84.3963" hae="300" ce="50" le="50"/>
1006 <detail>
1007 <contact callsign="CMD-ALPHA"/>
1008 <remarks>Track suspicious vehicle in sector bravo, heading north on Main St</remarks>
1009 </detail>
1010 </event>"#;
1011
1012 let event = CotEvent::from_xml(xml).expect("should parse XML");
1014 assert_eq!(event.uid, "TASK-20251208-001");
1015 assert_eq!(event.cot_type.as_str(), "t-x-m-c-c");
1016
1017 let task = MissionTask::from_cot_event(&event).expect("should convert to MissionTask");
1019
1020 assert_eq!(task.task_id, "TASK-20251208-001");
1022 assert_eq!(task.task_type, MissionTaskType::TrackTarget);
1023 assert_eq!(task.issued_by, "TAK-Server");
1024 assert!(task.target.is_some());
1025 assert!(task
1026 .target
1027 .as_ref()
1028 .unwrap()
1029 .description
1030 .contains("suspicious vehicle"));
1031 assert!(task.objective_position.is_some());
1032 let pos = task.objective_position.as_ref().unwrap();
1033 assert!((pos.lat - 33.7756).abs() < 0.0001);
1034 assert!((pos.lon - (-84.3963)).abs() < 0.0001);
1035 }
1036
1037 #[test]
1039 fn test_mission_task_search_area_from_xml() {
1040 use crate::cot::CotEvent;
1041
1042 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1043 <event uid="SEARCH-001" type="t-x-m-c-s" time="2025-12-08T14:00:00Z"
1044 start="2025-12-08T14:00:00Z" stale="2025-12-08T18:00:00Z" how="h-g-i-g-o">
1045 <point lat="33.80" lon="-84.40" hae="0" ce="500" le="500"/>
1046 <detail>
1047 <remarks>Search grid sector 7 for missing hiker</remarks>
1048 </detail>
1049 </event>"#;
1050
1051 let event = CotEvent::from_xml(xml).expect("should parse");
1052 let task = MissionTask::from_cot_event(&event).expect("should convert");
1053
1054 assert_eq!(task.task_type, MissionTaskType::SearchArea);
1055 assert_eq!(task.task_id, "SEARCH-001");
1056 }
1057
1058 #[test]
1060 fn test_mission_task_abort_from_xml() {
1061 use crate::cot::CotEvent;
1062
1063 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1064 <event uid="ABORT-001" type="t-x-m-c-a" time="2025-12-08T14:00:00Z"
1065 start="2025-12-08T14:00:00Z" stale="2025-12-08T14:30:00Z" how="h-g-i-g-o">
1066 <point lat="0" lon="0" hae="0" ce="999999" le="999999"/>
1067 <detail>
1068 <remarks>Abort current mission - RTB immediately</remarks>
1069 </detail>
1070 </event>"#;
1071
1072 let event = CotEvent::from_xml(xml).expect("should parse");
1073 let task = MissionTask::from_cot_event(&event).expect("should convert");
1074
1075 assert_eq!(task.task_type, MissionTaskType::Abort);
1076 assert!(task.remarks.as_ref().unwrap().contains("RTB"));
1077 }
1078}