1use chrono::{DateTime, Duration, Utc};
6use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
7use quick_xml::Reader;
8use quick_xml::Writer;
9use serde::{Deserialize, Serialize};
10use std::io::Cursor;
11
12use super::peat_extension::PeatExtension;
13use super::type_mapper::{CotRelation, CotType};
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct CotEvent {
18 pub version: String,
20 pub uid: String,
22 pub cot_type: CotType,
24 pub time: DateTime<Utc>,
26 pub start: DateTime<Utc>,
28 pub stale: DateTime<Utc>,
30 pub how: String,
32 pub point: CotPoint,
34 pub detail: CotDetail,
36}
37
38impl CotEvent {
39 pub fn builder() -> CotEventBuilder {
41 CotEventBuilder::new()
42 }
43
44 pub fn to_xml(&self) -> Result<String, CotError> {
46 let mut writer = Writer::new(Cursor::new(Vec::new()));
47
48 writer
50 .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
51 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
52
53 let time_str = self.format_time(&self.time);
55 let start_str = self.format_time(&self.start);
56 let stale_str = self.format_time(&self.stale);
57
58 let mut event_elem = BytesStart::new("event");
59 event_elem.push_attribute(("version", self.version.as_str()));
60 event_elem.push_attribute(("uid", self.uid.as_str()));
61 event_elem.push_attribute(("type", self.cot_type.as_str()));
62 event_elem.push_attribute(("time", time_str.as_str()));
63 event_elem.push_attribute(("start", start_str.as_str()));
64 event_elem.push_attribute(("stale", stale_str.as_str()));
65 event_elem.push_attribute(("how", self.how.as_str()));
66
67 writer
68 .write_event(Event::Start(event_elem))
69 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
70
71 self.write_point(&mut writer)?;
73
74 self.write_detail(&mut writer)?;
76
77 writer
79 .write_event(Event::End(BytesEnd::new("event")))
80 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
81
82 let result = writer.into_inner().into_inner();
83 String::from_utf8(result).map_err(|e| CotError::Encoding(e.to_string()))
84 }
85
86 pub fn from_xml(xml: &str) -> Result<Self, CotError> {
90 let mut reader = Reader::from_str(xml);
91 reader.config_mut().trim_text(true);
92
93 let mut uid = None;
94 let mut cot_type = None;
95 let mut time = None;
96 let mut start = None;
97 let mut stale = None;
98 let mut how = String::from("m-g");
99 let mut point = None;
100 let mut detail = CotDetail::default();
101
102 let mut buf = Vec::new();
103 let mut in_detail = false;
104 let mut in_remarks = false;
105 let mut remarks_text = String::new();
106
107 loop {
108 match reader.read_event_into(&mut buf) {
109 Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
110 let name = e.name();
111 match name.as_ref() {
112 b"event" => {
113 for attr in e.attributes().flatten() {
115 match attr.key.as_ref() {
116 b"uid" => {
117 uid =
118 Some(String::from_utf8_lossy(&attr.value).into_owned());
119 }
120 b"type" => {
121 cot_type = Some(CotType::new(&String::from_utf8_lossy(
122 &attr.value,
123 )));
124 }
125 b"time" => {
126 time = Self::parse_time(&attr.value);
127 }
128 b"start" => {
129 start = Self::parse_time(&attr.value);
130 }
131 b"stale" => {
132 stale = Self::parse_time(&attr.value);
133 }
134 b"how" => {
135 how = String::from_utf8_lossy(&attr.value).into_owned();
136 }
137 _ => {}
138 }
139 }
140 }
141 b"point" => {
142 let mut lat = 0.0;
143 let mut lon = 0.0;
144 let mut hae = 0.0;
145 let mut ce = 9999999.0;
146 let mut le = 9999999.0;
147
148 for attr in e.attributes().flatten() {
149 match attr.key.as_ref() {
150 b"lat" => {
151 lat = String::from_utf8_lossy(&attr.value)
152 .parse()
153 .unwrap_or(0.0);
154 }
155 b"lon" => {
156 lon = String::from_utf8_lossy(&attr.value)
157 .parse()
158 .unwrap_or(0.0);
159 }
160 b"hae" => {
161 hae = String::from_utf8_lossy(&attr.value)
162 .parse()
163 .unwrap_or(0.0);
164 }
165 b"ce" => {
166 ce = String::from_utf8_lossy(&attr.value)
167 .parse()
168 .unwrap_or(9999999.0);
169 }
170 b"le" => {
171 le = String::from_utf8_lossy(&attr.value)
172 .parse()
173 .unwrap_or(9999999.0);
174 }
175 _ => {}
176 }
177 }
178 point = Some(CotPoint::with_full(lat, lon, hae, ce, le));
179 }
180 b"detail" => {
181 in_detail = true;
182 }
183 b"track" if in_detail => {
184 let mut course = 0.0;
185 let mut speed = 0.0;
186 for attr in e.attributes().flatten() {
187 match attr.key.as_ref() {
188 b"course" => {
189 course = String::from_utf8_lossy(&attr.value)
190 .parse()
191 .unwrap_or(0.0);
192 }
193 b"speed" => {
194 speed = String::from_utf8_lossy(&attr.value)
195 .parse()
196 .unwrap_or(0.0);
197 }
198 _ => {}
199 }
200 }
201 detail.track = Some(CotTrack { course, speed });
202 }
203 b"contact" if in_detail => {
204 for attr in e.attributes().flatten() {
205 if attr.key.as_ref() == b"callsign" {
206 detail.contact_callsign =
207 Some(String::from_utf8_lossy(&attr.value).into_owned());
208 }
209 }
210 }
211 b"remarks" if in_detail => {
212 in_remarks = true;
213 remarks_text.clear();
214 }
215 b"link" if in_detail => {
216 let mut link_uid = String::new();
217 let mut link_type = String::new();
218 let mut relation = String::new();
219 let mut link_remarks = None;
220
221 for attr in e.attributes().flatten() {
222 match attr.key.as_ref() {
223 b"uid" => {
224 link_uid =
225 String::from_utf8_lossy(&attr.value).into_owned();
226 }
227 b"type" => {
228 link_type =
229 String::from_utf8_lossy(&attr.value).into_owned();
230 }
231 b"relation" => {
232 relation =
233 String::from_utf8_lossy(&attr.value).into_owned();
234 }
235 b"remarks" => {
236 link_remarks =
237 Some(String::from_utf8_lossy(&attr.value).into_owned());
238 }
239 _ => {}
240 }
241 }
242 if !link_uid.is_empty() {
243 detail.links.push(CotLink {
244 uid: link_uid,
245 cot_type: link_type,
246 relation,
247 remarks: link_remarks,
248 });
249 }
250 }
251 _ => {}
252 }
253 }
254 Ok(Event::Text(ref e)) if in_remarks => {
255 remarks_text.push_str(&e.unescape().unwrap_or_default());
256 }
257 Ok(Event::End(ref e)) => match e.name().as_ref() {
258 b"detail" => in_detail = false,
259 b"remarks" => {
260 in_remarks = false;
261 if !remarks_text.is_empty() {
262 detail.remarks = Some(remarks_text.clone());
263 }
264 }
265 _ => {}
266 },
267 Ok(Event::Eof) => break,
268 Err(e) => {
269 return Err(CotError::XmlRead(format!(
270 "XML parse error at position {}: {:?}",
271 reader.buffer_position(),
272 e
273 )));
274 }
275 _ => {}
276 }
277 buf.clear();
278 }
279
280 let uid = uid.ok_or(CotError::MissingField("uid"))?;
281 let cot_type = cot_type.ok_or(CotError::MissingField("type"))?;
282 let point = point.ok_or(CotError::MissingField("point"))?;
283 let time = time.unwrap_or_else(Utc::now);
284 let start = start.unwrap_or(time);
285 let stale = stale.unwrap_or(time + Duration::minutes(5));
286
287 Ok(CotEvent {
288 version: "2.0".to_string(),
289 uid,
290 cot_type,
291 time,
292 start,
293 stale,
294 how,
295 point,
296 detail,
297 })
298 }
299
300 fn parse_time(value: &[u8]) -> Option<DateTime<Utc>> {
302 let s = String::from_utf8_lossy(value);
303 DateTime::parse_from_rfc3339(&s)
304 .ok()
305 .map(|dt| dt.with_timezone(&Utc))
306 .or_else(|| {
307 chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.fZ")
309 .ok()
310 .map(|ndt| ndt.and_utc())
311 })
312 .or_else(|| {
313 chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%SZ")
315 .ok()
316 .map(|ndt| ndt.and_utc())
317 })
318 }
319
320 fn format_time(&self, time: &DateTime<Utc>) -> String {
321 time.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
322 }
323
324 fn write_point(&self, writer: &mut Writer<Cursor<Vec<u8>>>) -> Result<(), CotError> {
325 let lat_str = self.point.lat.to_string();
326 let lon_str = self.point.lon.to_string();
327 let hae_str = self.point.hae.to_string();
328 let ce_str = self.point.ce.to_string();
329 let le_str = self.point.le.to_string();
330
331 let mut point_elem = BytesStart::new("point");
332 point_elem.push_attribute(("lat", lat_str.as_str()));
333 point_elem.push_attribute(("lon", lon_str.as_str()));
334 point_elem.push_attribute(("hae", hae_str.as_str()));
335 point_elem.push_attribute(("ce", ce_str.as_str()));
336 point_elem.push_attribute(("le", le_str.as_str()));
337
338 writer
339 .write_event(Event::Empty(point_elem))
340 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
341
342 Ok(())
343 }
344
345 fn write_detail(&self, writer: &mut Writer<Cursor<Vec<u8>>>) -> Result<(), CotError> {
346 writer
347 .write_event(Event::Start(BytesStart::new("detail")))
348 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
349
350 if let Some(ref track) = self.detail.track {
352 let course_str = track.course.to_string();
353 let speed_str = track.speed.to_string();
354
355 let mut track_elem = BytesStart::new("track");
356 track_elem.push_attribute(("course", course_str.as_str()));
357 track_elem.push_attribute(("speed", speed_str.as_str()));
358
359 writer
360 .write_event(Event::Empty(track_elem))
361 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
362 }
363
364 if let Some(ref callsign) = self.detail.contact_callsign {
366 let mut contact_elem = BytesStart::new("contact");
367 contact_elem.push_attribute(("callsign", callsign.as_str()));
368
369 writer
370 .write_event(Event::Empty(contact_elem))
371 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
372 }
373
374 if let Some(ref group) = self.detail.group {
376 let mut group_elem = BytesStart::new("__group");
377 group_elem.push_attribute(("name", group.name.as_str()));
378 group_elem.push_attribute(("role", group.role.as_str()));
379
380 writer
381 .write_event(Event::Empty(group_elem))
382 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
383 }
384
385 if let Some(ref remarks) = self.detail.remarks {
387 writer
388 .write_event(Event::Start(BytesStart::new("remarks")))
389 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
390 writer
391 .write_event(Event::Text(BytesText::new(remarks)))
392 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
393 writer
394 .write_event(Event::End(BytesEnd::new("remarks")))
395 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
396 }
397
398 if let Some(ref peat) = self.detail.peat_extension {
400 peat.write_xml(writer)?;
401 }
402
403 for link in &self.detail.links {
405 let mut link_elem = BytesStart::new("link");
406 link_elem.push_attribute(("uid", link.uid.as_str()));
407 link_elem.push_attribute(("type", link.cot_type.as_str()));
408 link_elem.push_attribute(("relation", link.relation.as_str()));
409 if let Some(ref remarks) = link.remarks {
410 link_elem.push_attribute(("remarks", remarks.as_str()));
411 }
412
413 writer
414 .write_event(Event::Empty(link_elem))
415 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
416 }
417
418 if let Some(ref priority) = self.detail.flow_priority {
420 let mut flow_elem = BytesStart::new("_flow-tags_");
421 flow_elem.push_attribute(("priority", priority.as_str()));
422
423 writer
424 .write_event(Event::Empty(flow_elem))
425 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
426 }
427
428 writer
429 .write_event(Event::End(BytesEnd::new("detail")))
430 .map_err(|e| CotError::XmlWrite(e.to_string()))?;
431
432 Ok(())
433 }
434}
435
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
438pub struct CotPoint {
439 pub lat: f64,
441 pub lon: f64,
443 pub hae: f64,
445 pub ce: f64,
447 pub le: f64,
449}
450
451impl CotPoint {
452 pub fn new(lat: f64, lon: f64) -> Self {
454 Self {
455 lat,
456 lon,
457 hae: 0.0,
458 ce: 9999999.0, le: 9999999.0, }
461 }
462
463 pub fn with_full(lat: f64, lon: f64, hae: f64, ce: f64, le: f64) -> Self {
465 Self {
466 lat,
467 lon,
468 hae,
469 ce,
470 le,
471 }
472 }
473}
474
475#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
477pub struct CotDetail {
478 pub track: Option<CotTrack>,
480 pub contact_callsign: Option<String>,
482 pub group: Option<CotGroup>,
484 pub remarks: Option<String>,
486 pub peat_extension: Option<PeatExtension>,
488 pub links: Vec<CotLink>,
490 pub flow_priority: Option<String>,
492}
493
494#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496pub struct CotTrack {
497 pub course: f64,
499 pub speed: f64,
501}
502
503#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
505pub struct CotGroup {
506 pub name: String,
508 pub role: String,
510}
511
512#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
514pub struct CotLink {
515 pub uid: String,
517 pub cot_type: String,
519 pub relation: String,
521 pub remarks: Option<String>,
523}
524
525impl CotLink {
526 pub fn new(uid: &str, cot_type: &str, relation: CotRelation) -> Self {
528 Self {
529 uid: uid.to_string(),
530 cot_type: cot_type.to_string(),
531 relation: relation.as_str().to_string(),
532 remarks: None,
533 }
534 }
535
536 pub fn with_remarks(mut self, remarks: &str) -> Self {
538 self.remarks = Some(remarks.to_string());
539 self
540 }
541}
542
543#[derive(Debug, Default)]
545pub struct CotEventBuilder {
546 uid: Option<String>,
547 cot_type: Option<CotType>,
548 time: Option<DateTime<Utc>>,
549 stale_duration: Duration,
550 how: String,
551 point: Option<CotPoint>,
552 detail: CotDetail,
553}
554
555impl CotEventBuilder {
556 pub fn new() -> Self {
558 Self {
559 uid: None,
560 cot_type: None,
561 time: None,
562 stale_duration: Duration::seconds(30),
563 how: "m-g".to_string(), point: None,
565 detail: CotDetail::default(),
566 }
567 }
568
569 pub fn uid(mut self, uid: &str) -> Self {
571 self.uid = Some(uid.to_string());
572 self
573 }
574
575 pub fn cot_type(mut self, cot_type: CotType) -> Self {
577 self.cot_type = Some(cot_type);
578 self
579 }
580
581 pub fn time(mut self, time: DateTime<Utc>) -> Self {
583 self.time = Some(time);
584 self
585 }
586
587 pub fn stale_duration(mut self, duration: Duration) -> Self {
589 self.stale_duration = duration;
590 self
591 }
592
593 pub fn how(mut self, how: &str) -> Self {
595 self.how = how.to_string();
596 self
597 }
598
599 pub fn point(mut self, point: CotPoint) -> Self {
601 self.point = Some(point);
602 self
603 }
604
605 pub fn track(mut self, course: f64, speed: f64) -> Self {
607 self.detail.track = Some(CotTrack { course, speed });
608 self
609 }
610
611 pub fn callsign(mut self, callsign: &str) -> Self {
613 self.detail.contact_callsign = Some(callsign.to_string());
614 self
615 }
616
617 pub fn group(mut self, name: &str, role: &str) -> Self {
619 self.detail.group = Some(CotGroup {
620 name: name.to_string(),
621 role: role.to_string(),
622 });
623 self
624 }
625
626 pub fn remarks(mut self, remarks: &str) -> Self {
628 self.detail.remarks = Some(remarks.to_string());
629 self
630 }
631
632 pub fn peat_extension(mut self, extension: PeatExtension) -> Self {
634 self.detail.peat_extension = Some(extension);
635 self
636 }
637
638 pub fn link(mut self, link: CotLink) -> Self {
640 self.detail.links.push(link);
641 self
642 }
643
644 pub fn flow_priority(mut self, priority: &str) -> Self {
646 self.detail.flow_priority = Some(priority.to_string());
647 self
648 }
649
650 pub fn build(self) -> Result<CotEvent, CotError> {
652 let uid = self.uid.ok_or(CotError::MissingField("uid"))?;
653 let cot_type = self.cot_type.ok_or(CotError::MissingField("cot_type"))?;
654 let point = self.point.ok_or(CotError::MissingField("point"))?;
655 let time = self.time.unwrap_or_else(Utc::now);
656
657 Ok(CotEvent {
658 version: "2.0".to_string(),
659 uid,
660 cot_type,
661 time,
662 start: time,
663 stale: time + self.stale_duration,
664 how: self.how,
665 point,
666 detail: self.detail,
667 })
668 }
669}
670
671#[derive(Debug, Clone, PartialEq)]
673pub enum CotError {
674 MissingField(&'static str),
676 XmlWrite(String),
678 XmlRead(String),
680 Encoding(String),
682}
683
684impl std::fmt::Display for CotError {
685 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686 match self {
687 Self::MissingField(field) => write!(f, "Missing required field: {}", field),
688 Self::XmlWrite(msg) => write!(f, "XML write error: {}", msg),
689 Self::XmlRead(msg) => write!(f, "XML read error: {}", msg),
690 Self::Encoding(msg) => write!(f, "Encoding error: {}", msg),
691 }
692 }
693}
694
695impl std::error::Error for CotError {}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn test_cot_event_builder() {
703 let event = CotEvent::builder()
704 .uid("TRACK-001")
705 .cot_type(CotType::new("a-f-G-E-S"))
706 .point(CotPoint::new(33.7749, -84.3958))
707 .remarks("Test track")
708 .build()
709 .unwrap();
710
711 assert_eq!(event.uid, "TRACK-001");
712 assert_eq!(event.cot_type.as_str(), "a-f-G-E-S");
713 assert_eq!(event.point.lat, 33.7749);
714 }
715
716 #[test]
717 fn test_cot_event_missing_uid() {
718 let result = CotEvent::builder()
719 .cot_type(CotType::new("a-f-G"))
720 .point(CotPoint::new(0.0, 0.0))
721 .build();
722
723 assert!(matches!(result, Err(CotError::MissingField("uid"))));
724 }
725
726 #[test]
727 fn test_cot_event_to_xml() {
728 let event = CotEvent::builder()
729 .uid("TEST-001")
730 .cot_type(CotType::new("a-f-G-E-S"))
731 .point(CotPoint::new(33.7749, -84.3958))
732 .remarks("Test event")
733 .build()
734 .unwrap();
735
736 let xml = event.to_xml().unwrap();
737
738 assert!(xml.contains("<?xml version=\"1.0\""));
739 assert!(xml.contains("uid=\"TEST-001\""));
740 assert!(xml.contains("type=\"a-f-G-E-S\""));
741 assert!(xml.contains("lat=\"33.7749\""));
742 assert!(xml.contains("<remarks>Test event</remarks>"));
743 }
744
745 #[test]
746 fn test_cot_event_with_track() {
747 let event = CotEvent::builder()
748 .uid("TRACK-001")
749 .cot_type(CotType::new("a-f-G-E-S"))
750 .point(CotPoint::new(33.7749, -84.3958))
751 .track(45.0, 5.0)
752 .build()
753 .unwrap();
754
755 let xml = event.to_xml().unwrap();
756 assert!(xml.contains("course=\"45\""));
757 assert!(xml.contains("speed=\"5\""));
758 }
759
760 #[test]
761 fn test_cot_event_with_links() {
762 let event = CotEvent::builder()
763 .uid("TRACK-001")
764 .cot_type(CotType::new("a-f-G-E-S"))
765 .point(CotPoint::new(33.7749, -84.3958))
766 .link(
767 CotLink::new("Alpha-Team", "a-f-G-U-C", CotRelation::Parent)
768 .with_remarks("parent-cell"),
769 )
770 .build()
771 .unwrap();
772
773 let xml = event.to_xml().unwrap();
774 assert!(xml.contains("relation=\"p-p\""));
775 assert!(xml.contains("remarks=\"parent-cell\""));
776 }
777
778 #[test]
779 fn test_cot_point_defaults() {
780 let point = CotPoint::new(0.0, 0.0);
781 assert_eq!(point.hae, 0.0);
782 assert_eq!(point.ce, 9999999.0);
783 assert_eq!(point.le, 9999999.0);
784 }
785
786 #[test]
787 fn test_cot_link_creation() {
788 let link = CotLink::new("target-uid", "a-f-G-U-C", CotRelation::Observing);
789 assert_eq!(link.relation, "o-o");
790 }
791
792 #[test]
793 fn test_cot_event_with_group() {
794 let event = CotEvent::builder()
795 .uid("PLATFORM-001")
796 .cot_type(CotType::new("a-f-G-U-C"))
797 .point(CotPoint::new(33.7749, -84.3958))
798 .group("Alpha-Team", "Team Member")
799 .build()
800 .unwrap();
801
802 let xml = event.to_xml().unwrap();
803 assert!(xml.contains("__group"));
804 assert!(xml.contains("name=\"Alpha-Team\""));
805 }
806
807 #[test]
812 fn test_cot_event_from_xml_basic() {
813 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
814 <event version="2.0" uid="TEST-001" type="a-f-G-E-S"
815 time="2025-12-08T14:10:00Z" start="2025-12-08T14:10:00Z"
816 stale="2025-12-08T14:15:00Z" how="m-g">
817 <point lat="33.7749" lon="-84.3958" hae="10.0" ce="5.0" le="3.0"/>
818 <detail>
819 <remarks>Test event</remarks>
820 </detail>
821 </event>"#;
822
823 let event = CotEvent::from_xml(xml).unwrap();
824
825 assert_eq!(event.uid, "TEST-001");
826 assert_eq!(event.cot_type.as_str(), "a-f-G-E-S");
827 assert_eq!(event.how, "m-g");
828 assert_eq!(event.point.lat, 33.7749);
829 assert_eq!(event.point.lon, -84.3958);
830 assert_eq!(event.point.hae, 10.0);
831 assert_eq!(event.point.ce, 5.0);
832 assert_eq!(event.point.le, 3.0);
833 assert_eq!(event.detail.remarks.as_deref(), Some("Test event"));
834 }
835
836 #[test]
837 fn test_cot_event_from_xml_roundtrip() {
838 let original = CotEvent::builder()
840 .uid("ROUNDTRIP-001")
841 .cot_type(CotType::new("a-f-G-U-C"))
842 .point(CotPoint::with_full(38.8977, -77.0365, 50.0, 10.0, 5.0))
843 .remarks("Roundtrip test")
844 .track(90.0, 5.5)
845 .build()
846 .unwrap();
847
848 let xml = original.to_xml().unwrap();
849 let parsed = CotEvent::from_xml(&xml).unwrap();
850
851 assert_eq!(parsed.uid, original.uid);
852 assert_eq!(parsed.cot_type.as_str(), original.cot_type.as_str());
853 assert_eq!(parsed.point.lat, original.point.lat);
854 assert_eq!(parsed.point.lon, original.point.lon);
855 assert_eq!(parsed.detail.remarks, original.detail.remarks);
856 assert!(parsed.detail.track.is_some());
857 assert_eq!(parsed.detail.track.as_ref().unwrap().course, 90.0);
858 assert_eq!(parsed.detail.track.as_ref().unwrap().speed, 5.5);
859 }
860
861 #[test]
862 fn test_cot_event_from_xml_mission_task() {
863 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
865 <event uid="MISSION-001" type="t-x-m-c-c" time="2025-12-08T14:05:00Z"
866 start="2025-12-08T14:05:00Z" stale="2025-12-08T15:05:00Z" how="h-g-i-g-o">
867 <point lat="33.7756" lon="-84.3963" hae="0" ce="100" le="100"/>
868 <detail>
869 <remarks>Track POI within designated area</remarks>
870 </detail>
871 </event>"#;
872
873 let event = CotEvent::from_xml(xml).unwrap();
874
875 assert_eq!(event.uid, "MISSION-001");
876 assert_eq!(event.cot_type.as_str(), "t-x-m-c-c");
877 assert_eq!(event.how, "h-g-i-g-o");
878 assert_eq!(event.point.lat, 33.7756);
879 assert_eq!(event.point.lon, -84.3963);
880 }
881
882 #[test]
883 fn test_cot_event_from_xml_with_contact() {
884 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
885 <event uid="ALPHA-3" type="a-f-G-U-C" time="2025-12-08T14:00:00Z"
886 start="2025-12-08T14:00:00Z" stale="2025-12-08T14:01:00Z" how="m-g">
887 <point lat="38.0" lon="-77.0" hae="0" ce="10" le="10"/>
888 <detail>
889 <contact callsign="Alpha-3"/>
890 </detail>
891 </event>"#;
892
893 let event = CotEvent::from_xml(xml).unwrap();
894
895 assert_eq!(event.detail.contact_callsign.as_deref(), Some("Alpha-3"));
896 }
897
898 #[test]
899 fn test_cot_event_from_xml_missing_uid() {
900 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
901 <event type="a-f-G" time="2025-12-08T14:00:00Z"
902 start="2025-12-08T14:00:00Z" stale="2025-12-08T14:01:00Z" how="m-g">
903 <point lat="0" lon="0" hae="0" ce="10" le="10"/>
904 <detail/>
905 </event>"#;
906
907 let result = CotEvent::from_xml(xml);
908 assert!(matches!(result, Err(CotError::MissingField("uid"))));
909 }
910
911 #[test]
912 fn test_cot_event_from_xml_missing_point() {
913 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
914 <event uid="TEST" type="a-f-G" time="2025-12-08T14:00:00Z"
915 start="2025-12-08T14:00:00Z" stale="2025-12-08T14:01:00Z" how="m-g">
916 <detail/>
917 </event>"#;
918
919 let result = CotEvent::from_xml(xml);
920 assert!(matches!(result, Err(CotError::MissingField("point"))));
921 }
922}