use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Position {
pub lat: f64,
pub lon: f64,
pub cep_m: Option<f64>,
pub hae: Option<f64>,
}
impl Position {
pub fn new(lat: f64, lon: f64) -> Self {
Self {
lat,
lon,
cep_m: None,
hae: None,
}
}
pub fn with_accuracy(lat: f64, lon: f64, cep_m: f64) -> Self {
Self {
lat,
lon,
cep_m: Some(cep_m),
hae: None,
}
}
pub fn with_altitude(lat: f64, lon: f64, hae: f64, cep_m: Option<f64>) -> Self {
Self {
lat,
lon,
cep_m,
hae: Some(hae),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Velocity {
pub bearing: f64,
pub speed_mps: f64,
}
impl Velocity {
pub fn new(bearing: f64, speed_mps: f64) -> Self {
Self { bearing, speed_mps }
}
pub fn is_stationary(&self, threshold_mps: f64) -> bool {
self.speed_mps < threshold_mps
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TrackUpdate {
pub track_id: String,
pub classification: String,
pub confidence: f64,
pub position: Position,
pub velocity: Option<Velocity>,
pub attributes: HashMap<String, serde_json::Value>,
pub source_platform: String,
pub source_model: String,
pub model_version: String,
pub timestamp: DateTime<Utc>,
pub cell_id: Option<String>,
pub formation_id: Option<String>,
}
impl TrackUpdate {
pub fn new(
track_id: String,
classification: String,
confidence: f64,
position: Position,
source_platform: String,
source_model: String,
model_version: String,
) -> Self {
Self {
track_id,
classification,
confidence: confidence.clamp(0.0, 1.0),
position,
velocity: None,
attributes: HashMap::new(),
source_platform,
source_model,
model_version,
timestamp: Utc::now(),
cell_id: None,
formation_id: None,
}
}
pub fn with_attribute(mut self, key: &str, value: serde_json::Value) -> Self {
self.attributes.insert(key.to_string(), value);
self
}
pub fn with_velocity(mut self, velocity: Velocity) -> Self {
self.velocity = Some(velocity);
self
}
pub fn with_cell(mut self, cell_id: String) -> Self {
self.cell_id = Some(cell_id);
self
}
pub fn with_formation(mut self, formation_id: String) -> Self {
self.formation_id = Some(formation_id);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OperationalStatus {
Ready,
Active,
Degraded,
Offline,
Loading,
}
impl OperationalStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Ready => "READY",
Self::Active => "ACTIVE",
Self::Degraded => "DEGRADED",
Self::Offline => "OFFLINE",
Self::Loading => "LOADING",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CapabilityAdvertisement {
pub platform_id: String,
pub platform_type: String,
pub position: Position,
pub status: OperationalStatus,
pub readiness: f64,
pub capabilities: Vec<CapabilityInfo>,
pub cell_id: Option<String>,
pub formation_id: Option<String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CapabilityInfo {
pub capability_type: String,
pub model_name: String,
pub version: String,
pub precision: f64,
pub status: OperationalStatus,
}
impl CapabilityAdvertisement {
pub fn new(
platform_id: String,
platform_type: String,
position: Position,
status: OperationalStatus,
readiness: f64,
) -> Self {
Self {
platform_id,
platform_type,
position,
status,
readiness: readiness.clamp(0.0, 1.0),
capabilities: Vec::new(),
cell_id: None,
formation_id: None,
timestamp: Utc::now(),
}
}
pub fn with_capability(mut self, capability: CapabilityInfo) -> Self {
self.capabilities.push(capability);
self
}
pub fn with_cell(mut self, cell_id: String) -> Self {
self.cell_id = Some(cell_id);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HandoffState {
Initiated,
Accepted,
Transferred,
Completed,
Failed,
}
impl HandoffState {
pub fn as_str(&self) -> &'static str {
match self {
Self::Initiated => "INITIATED",
Self::Accepted => "ACCEPTED",
Self::Transferred => "TRANSFERRED",
Self::Completed => "COMPLETED",
Self::Failed => "FAILED",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HandoffMessage {
pub track_id: String,
pub position: Position,
pub source_cell: String,
pub target_cell: String,
pub state: HandoffState,
pub reason: String,
pub priority: u8,
pub timestamp: DateTime<Utc>,
}
impl HandoffMessage {
pub fn new(
track_id: String,
position: Position,
source_cell: String,
target_cell: String,
reason: String,
) -> Self {
Self {
track_id,
position,
source_cell,
target_cell,
state: HandoffState::Initiated,
reason,
priority: 3, timestamp: Utc::now(),
}
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority.clamp(1, 5);
self
}
pub fn with_state(mut self, state: HandoffState) -> Self {
self.state = state;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FormationCapabilitySummary {
pub formation_id: String,
pub callsign: String,
pub center_position: Position,
pub platform_count: u32,
pub cell_count: u32,
pub capabilities: Vec<AggregatedCapability>,
pub readiness: f64,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AggregatedCapability {
pub capability_type: String,
pub count: u32,
pub avg_precision: f64,
pub availability: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MissionTaskType {
TrackTarget,
SearchArea,
MonitorZone,
Abort,
}
impl MissionTaskType {
pub fn as_str(&self) -> &'static str {
match self {
Self::TrackTarget => "TRACK_TARGET",
Self::SearchArea => "SEARCH_AREA",
Self::MonitorZone => "MONITOR_ZONE",
Self::Abort => "ABORT",
}
}
pub fn from_cot_type(cot_type: &str) -> Option<Self> {
match cot_type {
"t-x-m-c-c" => Some(Self::TrackTarget),
"t-x-m-c-s" => Some(Self::SearchArea),
"t-x-m-c-a" => Some(Self::Abort),
_ if cot_type.starts_with("t-x-m-c") => Some(Self::TrackTarget), _ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum MissionPriority {
Critical,
High,
#[default]
Normal,
Low,
}
impl MissionPriority {
pub fn as_str(&self) -> &'static str {
match self {
Self::Critical => "CRITICAL",
Self::High => "HIGH",
Self::Normal => "NORMAL",
Self::Low => "LOW",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MissionTarget {
pub description: String,
pub last_known_position: Option<Position>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MissionBoundary {
pub boundary_type: BoundaryType,
pub coordinates: Vec<Position>,
pub radius_m: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BoundaryType {
Polygon,
Circle,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MissionTask {
pub task_id: String,
pub task_type: MissionTaskType,
pub issued_at: DateTime<Utc>,
pub issued_by: String,
pub expires_at: DateTime<Utc>,
pub target: Option<MissionTarget>,
pub boundary: Option<MissionBoundary>,
pub priority: MissionPriority,
pub objective_position: Option<Position>,
pub remarks: Option<String>,
}
impl MissionTask {
pub fn new(
task_id: String,
task_type: MissionTaskType,
issued_by: String,
expires_at: DateTime<Utc>,
) -> Self {
Self {
task_id,
task_type,
issued_at: Utc::now(),
issued_by,
expires_at,
target: None,
boundary: None,
priority: MissionPriority::Normal,
objective_position: None,
remarks: None,
}
}
pub fn from_cot_event(event: &super::CotEvent) -> Result<Self, MissionTaskError> {
let task_type = MissionTaskType::from_cot_type(event.cot_type.as_str()).ok_or(
MissionTaskError::InvalidCotType(event.cot_type.as_str().to_string()),
)?;
let mut task = Self {
task_id: event.uid.clone(),
task_type,
issued_at: event.time,
issued_by: "TAK-Server".to_string(), expires_at: event.stale,
target: None,
boundary: None,
priority: MissionPriority::Normal,
objective_position: Some(Position::with_altitude(
event.point.lat,
event.point.lon,
event.point.hae,
Some(event.point.ce),
)),
remarks: event.detail.remarks.clone(),
};
if let Some(ref remarks) = event.detail.remarks {
task.target = Some(MissionTarget {
description: remarks.clone(),
last_known_position: task.objective_position.clone(),
});
}
Ok(task)
}
pub fn with_target(mut self, target: MissionTarget) -> Self {
self.target = Some(target);
self
}
pub fn with_boundary(mut self, boundary: MissionBoundary) -> Self {
self.boundary = Some(boundary);
self
}
pub fn with_priority(mut self, priority: MissionPriority) -> Self {
self.priority = priority;
self
}
pub fn with_objective_position(mut self, position: Position) -> Self {
self.objective_position = Some(position);
self
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub fn is_mission_cot_type(cot_type: &str) -> bool {
cot_type.starts_with("t-x-m-c")
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MissionTaskError {
InvalidCotType(String),
MissingField(&'static str),
}
impl std::fmt::Display for MissionTaskError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidCotType(t) => write!(f, "Invalid CoT type for mission task: {}", t),
Self::MissingField(field) => write!(f, "Missing required field: {}", field),
}
}
}
impl std::error::Error for MissionTaskError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_creation() {
let pos = Position::new(33.7749, -84.3958);
assert_eq!(pos.lat, 33.7749);
assert_eq!(pos.lon, -84.3958);
assert!(pos.cep_m.is_none());
assert!(pos.hae.is_none());
}
#[test]
fn test_position_with_accuracy() {
let pos = Position::with_accuracy(33.7749, -84.3958, 2.5);
assert_eq!(pos.cep_m, Some(2.5));
}
#[test]
fn test_position_with_altitude() {
let pos = Position::with_altitude(33.7749, -84.3958, 100.0, Some(2.5));
assert_eq!(pos.hae, Some(100.0));
assert_eq!(pos.cep_m, Some(2.5));
}
#[test]
fn test_velocity_stationary() {
let moving = Velocity::new(45.0, 5.0);
assert!(!moving.is_stationary(0.5));
let stationary = Velocity::new(0.0, 0.1);
assert!(stationary.is_stationary(0.5));
}
#[test]
fn test_track_update_creation() {
let track = TrackUpdate::new(
"TRACK-001".to_string(),
"person".to_string(),
0.89,
Position::new(33.7749, -84.3958),
"Alpha-2".to_string(),
"object_tracker".to_string(),
"1.3.0".to_string(),
);
assert_eq!(track.track_id, "TRACK-001");
assert_eq!(track.confidence, 0.89);
}
#[test]
fn test_track_update_confidence_clamped() {
let track = TrackUpdate::new(
"TRACK-001".to_string(),
"person".to_string(),
1.5, Position::new(0.0, 0.0),
"platform".to_string(),
"model".to_string(),
"1.0".to_string(),
);
assert_eq!(track.confidence, 1.0);
}
#[test]
fn test_track_update_with_attributes() {
let track = TrackUpdate::new(
"TRACK-001".to_string(),
"person".to_string(),
0.89,
Position::new(0.0, 0.0),
"platform".to_string(),
"model".to_string(),
"1.0".to_string(),
)
.with_attribute("jacket_color", serde_json::json!("blue"))
.with_attribute("has_backpack", serde_json::json!(true));
assert_eq!(track.attributes.len(), 2);
assert_eq!(track.attributes["jacket_color"], "blue");
}
#[test]
fn test_capability_advertisement() {
let cap = CapabilityAdvertisement::new(
"Alpha-3".to_string(),
"UGV".to_string(),
Position::new(33.7749, -84.3958),
OperationalStatus::Active,
0.91,
)
.with_capability(CapabilityInfo {
capability_type: "OBJECT_TRACKING".to_string(),
model_name: "object_tracker".to_string(),
version: "1.3.0".to_string(),
precision: 0.94,
status: OperationalStatus::Active,
});
assert_eq!(cap.capabilities.len(), 1);
assert_eq!(cap.status.as_str(), "ACTIVE");
}
#[test]
fn test_handoff_message() {
let handoff = HandoffMessage::new(
"TRACK-001".to_string(),
Position::new(33.78, -84.40),
"Alpha-Team".to_string(),
"Bravo-Team".to_string(),
"boundary_crossing".to_string(),
)
.with_priority(2);
assert_eq!(handoff.state, HandoffState::Initiated);
assert_eq!(handoff.priority, 2);
}
#[test]
fn test_handoff_priority_clamped() {
let handoff = HandoffMessage::new(
"TRACK-001".to_string(),
Position::new(0.0, 0.0),
"source".to_string(),
"target".to_string(),
"test".to_string(),
)
.with_priority(10);
assert_eq!(handoff.priority, 5);
}
#[test]
fn test_operational_status_strings() {
assert_eq!(OperationalStatus::Ready.as_str(), "READY");
assert_eq!(OperationalStatus::Active.as_str(), "ACTIVE");
assert_eq!(OperationalStatus::Degraded.as_str(), "DEGRADED");
assert_eq!(OperationalStatus::Offline.as_str(), "OFFLINE");
assert_eq!(OperationalStatus::Loading.as_str(), "LOADING");
}
#[test]
fn test_handoff_state_strings() {
assert_eq!(HandoffState::Initiated.as_str(), "INITIATED");
assert_eq!(HandoffState::Accepted.as_str(), "ACCEPTED");
assert_eq!(HandoffState::Transferred.as_str(), "TRANSFERRED");
assert_eq!(HandoffState::Completed.as_str(), "COMPLETED");
assert_eq!(HandoffState::Failed.as_str(), "FAILED");
}
#[test]
fn test_mission_task_type_from_cot() {
assert_eq!(
MissionTaskType::from_cot_type("t-x-m-c-c"),
Some(MissionTaskType::TrackTarget)
);
assert_eq!(
MissionTaskType::from_cot_type("t-x-m-c-s"),
Some(MissionTaskType::SearchArea)
);
assert_eq!(
MissionTaskType::from_cot_type("t-x-m-c-a"),
Some(MissionTaskType::Abort)
);
assert_eq!(
MissionTaskType::from_cot_type("t-x-m-c-z"),
Some(MissionTaskType::TrackTarget)
);
assert_eq!(MissionTaskType::from_cot_type("a-f-G-U-C"), None);
}
#[test]
fn test_mission_task_type_as_str() {
assert_eq!(MissionTaskType::TrackTarget.as_str(), "TRACK_TARGET");
assert_eq!(MissionTaskType::SearchArea.as_str(), "SEARCH_AREA");
assert_eq!(MissionTaskType::MonitorZone.as_str(), "MONITOR_ZONE");
assert_eq!(MissionTaskType::Abort.as_str(), "ABORT");
}
#[test]
fn test_mission_priority_default() {
let priority = MissionPriority::default();
assert_eq!(priority, MissionPriority::Normal);
}
#[test]
fn test_mission_priority_as_str() {
assert_eq!(MissionPriority::Critical.as_str(), "CRITICAL");
assert_eq!(MissionPriority::High.as_str(), "HIGH");
assert_eq!(MissionPriority::Normal.as_str(), "NORMAL");
assert_eq!(MissionPriority::Low.as_str(), "LOW");
}
#[test]
fn test_mission_task_new() {
let expires = Utc::now() + chrono::Duration::hours(2);
let task = MissionTask::new(
"MISSION-001".to_string(),
MissionTaskType::TrackTarget,
"CMD-ALPHA".to_string(),
expires,
);
assert_eq!(task.task_id, "MISSION-001");
assert_eq!(task.task_type, MissionTaskType::TrackTarget);
assert_eq!(task.issued_by, "CMD-ALPHA");
assert_eq!(task.priority, MissionPriority::Normal);
assert!(task.target.is_none());
assert!(task.boundary.is_none());
}
#[test]
fn test_mission_task_with_builder_methods() {
let expires = Utc::now() + chrono::Duration::hours(1);
let task = MissionTask::new(
"MISSION-002".to_string(),
MissionTaskType::SearchArea,
"CMD-BRAVO".to_string(),
expires,
)
.with_priority(MissionPriority::High)
.with_objective_position(Position::new(33.7749, -84.3958))
.with_target(MissionTarget {
description: "Suspicious vehicle".to_string(),
last_known_position: Some(Position::new(33.77, -84.39)),
});
assert_eq!(task.priority, MissionPriority::High);
assert!(task.objective_position.is_some());
assert!(task.target.is_some());
assert_eq!(
task.target.as_ref().unwrap().description,
"Suspicious vehicle"
);
}
#[test]
fn test_mission_task_is_expired() {
let past_expires = Utc::now() - chrono::Duration::hours(1);
let task = MissionTask::new(
"EXPIRED-001".to_string(),
MissionTaskType::TrackTarget,
"CMD".to_string(),
past_expires,
);
assert!(task.is_expired());
let future_expires = Utc::now() + chrono::Duration::hours(1);
let active_task = MissionTask::new(
"ACTIVE-001".to_string(),
MissionTaskType::TrackTarget,
"CMD".to_string(),
future_expires,
);
assert!(!active_task.is_expired());
}
#[test]
fn test_mission_task_is_mission_cot_type() {
assert!(MissionTask::is_mission_cot_type("t-x-m-c-c"));
assert!(MissionTask::is_mission_cot_type("t-x-m-c-s"));
assert!(MissionTask::is_mission_cot_type("t-x-m-c-a"));
assert!(!MissionTask::is_mission_cot_type("a-f-G-U-C"));
assert!(!MissionTask::is_mission_cot_type("b-m-p-s-p-l"));
}
#[test]
fn test_mission_task_json_roundtrip() {
let expires = Utc::now() + chrono::Duration::hours(2);
let task = MissionTask::new(
"JSON-001".to_string(),
MissionTaskType::SearchArea,
"CMD".to_string(),
expires,
)
.with_priority(MissionPriority::Critical)
.with_objective_position(Position::new(33.7749, -84.3958));
let json = task.to_json().expect("should serialize");
let restored = MissionTask::from_json(&json).expect("should deserialize");
assert_eq!(restored.task_id, task.task_id);
assert_eq!(restored.task_type, task.task_type);
assert_eq!(restored.priority, task.priority);
}
#[test]
fn test_mission_task_error_display() {
let err = MissionTaskError::InvalidCotType("a-f-G-U-C".to_string());
assert!(err.to_string().contains("Invalid CoT type"));
let err = MissionTaskError::MissingField("target");
assert!(err.to_string().contains("Missing required field"));
}
#[test]
fn test_mission_task_from_cot_event() {
use crate::cot::{CotEvent, CotPoint, CotType};
let event = CotEvent {
version: "2.0".to_string(),
uid: "MISSION-TAK-001".to_string(),
cot_type: CotType::new("t-x-m-c-c"), time: Utc::now(),
start: Utc::now(),
stale: Utc::now() + chrono::Duration::hours(2),
how: "m-g".to_string(),
point: CotPoint {
lat: 33.7749,
lon: -84.3958,
hae: 300.0,
ce: 10.0,
le: 10.0,
},
detail: crate::cot::CotDetail {
contact_callsign: Some("CMD-001".to_string()),
remarks: Some("Track suspicious vehicle in sector alpha".to_string()),
..Default::default()
},
};
let task = MissionTask::from_cot_event(&event).expect("should convert");
assert_eq!(task.task_id, "MISSION-TAK-001");
assert_eq!(task.task_type, MissionTaskType::TrackTarget);
assert!(task.target.is_some());
assert_eq!(
task.target.as_ref().unwrap().description,
"Track suspicious vehicle in sector alpha"
);
assert!(task.objective_position.is_some());
let pos = task.objective_position.as_ref().unwrap();
assert_eq!(pos.lat, 33.7749);
assert_eq!(pos.lon, -84.3958);
}
#[test]
fn test_mission_task_from_cot_event_invalid_type() {
use crate::cot::{CotEvent, CotPoint, CotType};
let event = CotEvent {
version: "2.0".to_string(),
uid: "UNIT-001".to_string(),
cot_type: CotType::new("a-f-G-U-C"), time: Utc::now(),
start: Utc::now(),
stale: Utc::now() + chrono::Duration::hours(1),
how: "m-g".to_string(),
point: CotPoint::new(0.0, 0.0),
detail: Default::default(),
};
let result = MissionTask::from_cot_event(&event);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
MissionTaskError::InvalidCotType(_)
));
}
#[test]
fn test_mission_task_from_xml_end_to_end() {
use crate::cot::CotEvent;
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<event uid="TASK-20251208-001" type="t-x-m-c-c" time="2025-12-08T14:05:00Z"
start="2025-12-08T14:05:00Z" stale="2025-12-08T16:05:00Z" how="h-g-i-g-o">
<point lat="33.7756" lon="-84.3963" hae="300" ce="50" le="50"/>
<detail>
<contact callsign="CMD-ALPHA"/>
<remarks>Track suspicious vehicle in sector bravo, heading north on Main St</remarks>
</detail>
</event>"#;
let event = CotEvent::from_xml(xml).expect("should parse XML");
assert_eq!(event.uid, "TASK-20251208-001");
assert_eq!(event.cot_type.as_str(), "t-x-m-c-c");
let task = MissionTask::from_cot_event(&event).expect("should convert to MissionTask");
assert_eq!(task.task_id, "TASK-20251208-001");
assert_eq!(task.task_type, MissionTaskType::TrackTarget);
assert_eq!(task.issued_by, "TAK-Server");
assert!(task.target.is_some());
assert!(task
.target
.as_ref()
.unwrap()
.description
.contains("suspicious vehicle"));
assert!(task.objective_position.is_some());
let pos = task.objective_position.as_ref().unwrap();
assert!((pos.lat - 33.7756).abs() < 0.0001);
assert!((pos.lon - (-84.3963)).abs() < 0.0001);
}
#[test]
fn test_mission_task_search_area_from_xml() {
use crate::cot::CotEvent;
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<event uid="SEARCH-001" type="t-x-m-c-s" time="2025-12-08T14:00:00Z"
start="2025-12-08T14:00:00Z" stale="2025-12-08T18:00:00Z" how="h-g-i-g-o">
<point lat="33.80" lon="-84.40" hae="0" ce="500" le="500"/>
<detail>
<remarks>Search grid sector 7 for missing hiker</remarks>
</detail>
</event>"#;
let event = CotEvent::from_xml(xml).expect("should parse");
let task = MissionTask::from_cot_event(&event).expect("should convert");
assert_eq!(task.task_type, MissionTaskType::SearchArea);
assert_eq!(task.task_id, "SEARCH-001");
}
#[test]
fn test_mission_task_abort_from_xml() {
use crate::cot::CotEvent;
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<event uid="ABORT-001" type="t-x-m-c-a" time="2025-12-08T14:00:00Z"
start="2025-12-08T14:00:00Z" stale="2025-12-08T14:30:00Z" how="h-g-i-g-o">
<point lat="0" lon="0" hae="0" ce="999999" le="999999"/>
<detail>
<remarks>Abort current mission - RTB immediately</remarks>
</detail>
</event>"#;
let event = CotEvent::from_xml(xml).expect("should parse");
let task = MissionTask::from_cot_event(&event).expect("should convert");
assert_eq!(task.task_type, MissionTaskType::Abort);
assert!(task.remarks.as_ref().unwrap().contains("RTB"));
}
}