1#![cfg_attr(not(feature = "std"), no_std)]
86
87#[cfg(test)]
89extern crate alloc;
90
91mod assessment;
92mod avoidance;
93mod event;
94mod observation;
95mod track;
96
97#[cfg(doctest)]
99#[doc = include_str!("../README.md")]
100pub struct ReadmeExamples;
101
102use core::time::Duration;
103
104use kinavis::error::{ensure_range, KernelError, NavigationError, Result};
105use kinavis::sailings::great_circle;
106use kinavis_kernel::angle::TrueCourse;
107use kinavis_kernel::event::{EventList, RejectionReason, TargetId};
108use kinavis_kernel::inline::Inline;
109#[cfg(test)]
110use kinavis_kernel::math;
111use kinavis_kernel::position::Position;
112use kinavis_kernel::snapshot::GroundTrack;
113use kinavis_kernel::time::{Instant, Utc};
114use kinavis_kernel::units::Speed;
115
116pub use assessment::{
117 assess, assess_track, assess_traffic, CollisionAssessment, CollisionPicture, CollisionRisk,
118 CpaPolicy, TargetAssessment,
119};
120pub use avoidance::{
121 avoid, avoid_all, AvoidanceManoeuvre, ManoeuvreConstraints, PermittedSides,
122 ALTERATION_STEP_DEG, MAX_ALTERATION_DEG,
123};
124pub use event::TrafficEvent;
125pub use observation::TargetObservation;
126pub use track::{TargetTrack, TrackStatus, MAX_TRACK_HISTORY};
127
128pub const MAX_TARGETS: usize = 32;
134
135#[non_exhaustive]
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143pub enum WhenFull {
144 #[default]
147 Refuse,
148 EvictStalest,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq)]
159#[cfg_attr(
160 feature = "serde",
161 derive(serde::Serialize, serde::Deserialize),
162 serde(try_from = "StoredTrackingPolicy", into = "StoredTrackingPolicy")
163)]
164pub struct TrackingPolicy {
165 fixes_to_acquire: u8,
166 stale_after: Duration,
167 lost_after: Duration,
168 max_speed: Speed,
169 when_full: WhenFull,
170}
171
172impl TrackingPolicy {
173 pub fn new(
191 fixes_to_acquire: u8,
192 stale_after: Duration,
193 lost_after: Duration,
194 max_speed: Speed,
195 ) -> Result<Self> {
196 ensure_range(
197 "fixes to acquire",
198 f64::from(fixes_to_acquire),
199 1.0,
200 f64::from(u8::MAX),
201 )?;
202 ensure_range(
203 "lost after",
204 lost_after.as_secs_f64(),
205 stale_after.as_secs_f64(),
206 f64::MAX,
207 )?;
208 ensure_range("max speed", max_speed.knots(), f64::MIN_POSITIVE, f64::MAX)?;
209 Ok(Self {
210 fixes_to_acquire,
211 stale_after,
212 lost_after,
213 max_speed,
214 when_full: WhenFull::Refuse,
215 })
216 }
217
218 #[must_use]
220 pub const fn when_full(mut self, when_full: WhenFull) -> Self {
221 self.when_full = when_full;
222 self
223 }
224
225 #[must_use]
227 pub const fn fixes_to_acquire(&self) -> u8 {
228 self.fixes_to_acquire
229 }
230
231 #[must_use]
233 pub const fn stale_after(&self) -> Duration {
234 self.stale_after
235 }
236
237 #[must_use]
239 pub const fn lost_after(&self) -> Duration {
240 self.lost_after
241 }
242
243 #[must_use]
245 pub const fn max_speed(&self) -> Speed {
246 self.max_speed
247 }
248
249 #[must_use]
251 pub const fn on_full(&self) -> WhenFull {
252 self.when_full
253 }
254}
255
256#[cfg(feature = "serde")]
258#[derive(serde::Serialize, serde::Deserialize)]
259struct StoredTrackingPolicy {
260 fixes_to_acquire: u8,
261 stale_after: Duration,
262 lost_after: Duration,
263 max_speed: Speed,
264 when_full: WhenFull,
265}
266
267#[cfg(feature = "serde")]
268impl TryFrom<StoredTrackingPolicy> for TrackingPolicy {
269 type Error = NavigationError;
270
271 fn try_from(stored: StoredTrackingPolicy) -> Result<Self> {
272 Ok(Self::new(
273 stored.fixes_to_acquire,
274 stored.stale_after,
275 stored.lost_after,
276 stored.max_speed,
277 )?
278 .when_full(stored.when_full))
279 }
280}
281
282#[cfg(feature = "serde")]
283impl From<TrackingPolicy> for StoredTrackingPolicy {
284 fn from(policy: TrackingPolicy) -> Self {
285 Self {
286 fixes_to_acquire: policy.fixes_to_acquire,
287 stale_after: policy.stale_after,
288 lost_after: policy.lost_after,
289 max_speed: policy.max_speed,
290 when_full: policy.when_full,
291 }
292 }
293}
294
295#[derive(Debug, Clone, PartialEq)]
308pub struct Traffic<const N: usize = MAX_TARGETS> {
309 policy: TrackingPolicy,
310 tracks: Inline<TargetTrack, N>,
311}
312
313impl Traffic<MAX_TARGETS> {
314 #[must_use]
316 pub fn new(policy: TrackingPolicy) -> Self {
317 Self::with_capacity(policy)
318 }
319}
320
321impl<const N: usize> Traffic<N> {
322 #[must_use]
325 pub fn with_capacity(policy: TrackingPolicy) -> Self {
326 Self {
327 policy,
328 tracks: Inline::new(TargetTrack::placeholder()),
329 }
330 }
331
332 #[must_use]
334 pub const fn policy(&self) -> TrackingPolicy {
335 self.policy
336 }
337
338 #[must_use]
340 pub const fn capacity() -> usize {
341 N
342 }
343
344 #[must_use]
346 pub const fn len(&self) -> usize {
347 self.tracks.len()
348 }
349
350 #[must_use]
352 pub const fn is_empty(&self) -> bool {
353 self.tracks.is_empty()
354 }
355
356 #[must_use]
358 pub fn tracks(&self) -> &[TargetTrack] {
359 &self.tracks
360 }
361
362 #[must_use]
364 pub fn track(&self, target: TargetId) -> Option<&TargetTrack> {
365 self.tracks.iter().find(|track| track.target() == target)
366 }
367
368 pub fn ingest(&mut self, observation: TargetObservation) -> Result<EventList<TrafficEvent>> {
386 let mut events = EventList::new();
387 let index = self
388 .tracks
389 .iter()
390 .position(|track| track.target() == observation.target());
391
392 let Some(index) = index else {
393 if self.tracks.len() >= N {
394 self.make_room(&observation, &mut events)?;
395 }
396 let mut track = TargetTrack::started_by(&observation);
397 if self.policy.fixes_to_acquire <= 1 {
398 track.acquire();
399 events.push(TrafficEvent::TargetAcquired {
400 target: observation.target(),
401 at: observation.at(),
402 });
403 }
404 self.tracks.push(track).map_err(|full| {
405 NavigationError::Kernel(KernelError::CapacityExceeded {
406 context: "the traffic picture",
407 needed: full.capacity.saturating_add(1),
408 capacity: full.capacity,
409 })
410 })?;
411 return Ok(events);
412 };
413
414 let Some(track) = self.tracks.as_mut_slice().get_mut(index) else {
415 return Ok(events);
416 };
417 let Some(elapsed) = observation
418 .at()
419 .checked_duration_since(track.last_seen())
420 .filter(|elapsed| !elapsed.is_zero())
421 else {
422 events.push(TrafficEvent::ObservationRejected {
423 target: observation.target(),
424 reason: RejectionReason::OutOfOrder,
425 at: observation.at(),
426 });
427 return Ok(events);
428 };
429 let jump = great_circle(track.last_position(), observation.position())?.distance;
430 let implied = jump.nautical_miles() / (elapsed.as_secs_f64() / 3600.0);
431 if !implied.is_finite() || implied > self.policy.max_speed.knots() {
432 events.push(TrafficEvent::ObservationRejected {
433 target: observation.target(),
434 reason: RejectionReason::ImplausibleJump {
435 implied_speed: Speed::from_knots_unchecked(implied.min(f64::MAX)),
436 },
437 at: observation.at(),
438 });
439 return Ok(events);
440 }
441
442 track.extend(&observation);
443 if track.status() == TrackStatus::Acquiring
444 && track.fix_count() >= usize::from(self.policy.fixes_to_acquire)
445 {
446 track.acquire();
447 events.push(TrafficEvent::TargetAcquired {
448 target: observation.target(),
449 at: observation.at(),
450 });
451 }
452 Ok(events)
453 }
454
455 fn make_room(
457 &mut self,
458 newcomer: &TargetObservation,
459 events: &mut EventList<TrafficEvent>,
460 ) -> Result<()> {
461 match self.policy.when_full {
462 WhenFull::Refuse => Err(NavigationError::Kernel(KernelError::CapacityExceeded {
463 context: "the traffic picture",
464 needed: N.saturating_add(1),
465 capacity: N,
466 })),
467 WhenFull::EvictStalest => {
468 let stalest = self
469 .tracks
470 .iter()
471 .enumerate()
472 .min_by_key(|(_, track)| track.last_seen())
473 .map(|(index, _)| index);
474 if let Some(evicted) = stalest.and_then(|index| self.tracks.remove(index)) {
475 events.push(TrafficEvent::TargetEvicted {
476 target: evicted.target(),
477 last_seen: evicted.last_seen(),
478 for_target: newcomer.target(),
479 });
480 }
481 Ok(())
482 }
483 }
484 }
485
486 pub fn sweep(&mut self, now: Instant<Utc>) -> EventList<TrafficEvent, N> {
492 let mut events = EventList::with_capacity();
493 let mut kept = Inline::<TargetTrack, N>::new(TargetTrack::placeholder());
494 for track in self.tracks.iter() {
495 if track.age(now) > self.policy.lost_after {
496 events.push(TrafficEvent::TargetLost {
497 target: track.target(),
498 last_seen: track.last_seen(),
499 });
500 } else {
501 let _ = kept.push(*track);
503 }
504 }
505 self.tracks = kept;
506 events
507 }
508
509 #[must_use]
512 pub fn view(&self, now: Instant<Utc>) -> TrafficView<N> {
513 let mut targets = Inline::<TargetView, N>::new(TargetView::PLACEHOLDER);
514 for track in self.tracks.iter() {
515 let age = track.age(now);
516 let position = track.position_at(now).unwrap_or(track.last_position());
519 let _ = targets.push(TargetView {
520 target: track.target(),
521 status: track.status(),
522 position,
523 motion: track.motion(),
524 heading: track.heading(),
525 age,
526 stale: age > self.policy.stale_after,
527 });
528 }
529 TrafficView { at: now, targets }
530 }
531}
532
533#[derive(Debug, Clone, Copy, PartialEq)]
535#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
536pub struct TargetView {
537 pub target: TargetId,
539 pub status: TrackStatus,
541 pub position: Position,
543 pub motion: Option<GroundTrack>,
545 pub heading: Option<TrueCourse>,
547 pub age: Duration,
549 pub stale: bool,
551}
552
553impl TargetView {
554 const PLACEHOLDER: Self = Self {
556 target: TargetId::new(0),
557 status: TrackStatus::Acquiring,
558 position: Position::new(
559 kinavis_kernel::position::Latitude::EQUATOR,
560 kinavis_kernel::position::Longitude::GREENWICH,
561 ),
562 motion: None,
563 heading: None,
564 age: Duration::ZERO,
565 stale: false,
566 };
567}
568
569#[derive(Debug, Clone, Copy, PartialEq)]
572pub struct TrafficView<const N: usize = MAX_TARGETS> {
573 at: Instant<Utc>,
574 targets: Inline<TargetView, N>,
575}
576
577impl<const N: usize> TrafficView<N> {
578 #[must_use]
580 pub const fn at(&self) -> Instant<Utc> {
581 self.at
582 }
583
584 #[must_use]
586 pub fn targets(&self) -> &[TargetView] {
587 &self.targets
588 }
589
590 #[must_use]
592 pub const fn len(&self) -> usize {
593 self.targets.len()
594 }
595
596 #[must_use]
598 pub const fn is_empty(&self) -> bool {
599 self.targets.is_empty()
600 }
601
602 #[must_use]
604 pub fn target(&self, target: TargetId) -> Option<&TargetView> {
605 self.targets.iter().find(|view| view.target == target)
606 }
607
608 pub fn current(&self) -> impl Iterator<Item = &TargetView> + '_ {
610 self.targets.iter().filter(|view| !view.stale)
611 }
612}
613
614#[cfg(test)]
615#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::indexing_slicing)]
616mod tests {
617 use super::*;
618 use kinavis::relative_motion::Contact;
619 use kinavis_kernel::angle::TrueBearing;
620 use kinavis_kernel::units::Distance;
621
622 fn at(latitude: f64, longitude: f64) -> Position {
623 Position::from_degrees(latitude, longitude).unwrap()
624 }
625
626 fn start() -> Instant<Utc> {
627 Instant::from_unix_seconds(1_789_000_000)
628 }
629
630 fn after(seconds: u64) -> Instant<Utc> {
631 start().checked_add(Duration::from_secs(seconds)).unwrap()
632 }
633
634 fn knots(value: f64) -> Speed {
635 Speed::from_knots(value).unwrap()
636 }
637
638 fn policy() -> TrackingPolicy {
639 TrackingPolicy::new(
640 3,
641 Duration::from_secs(30),
642 Duration::from_secs(180),
643 knots(60.0),
644 )
645 .unwrap()
646 }
647
648 fn northbound(target: u32, n: u64, interval: u64) -> TargetObservation {
651 let miles = 12.0 * f64::from(u32::try_from(n * interval).unwrap()) / 3600.0;
652 TargetObservation::new(
653 TargetId::new(target),
654 at(50.0 + miles / 60.0, -1.0),
655 after(n * interval),
656 )
657 }
658
659 fn kinds(events: &[TrafficEvent]) -> alloc::vec::Vec<&'static str> {
660 events
661 .iter()
662 .map(|event| match event {
663 TrafficEvent::TargetAcquired { .. } => "acquired",
664 TrafficEvent::TargetLost { .. } => "lost",
665 TrafficEvent::ObservationRejected { .. } => "rejected",
666 _ => "other",
667 })
668 .collect()
669 }
670
671 #[test]
672 fn a_target_is_acquired_on_the_policys_fix_and_not_before() {
673 let mut traffic = Traffic::new(policy());
674 assert!(traffic.is_empty());
675
676 assert!(traffic.ingest(northbound(7, 0, 60)).unwrap().is_empty());
677 assert_eq!(traffic.len(), 1);
678 assert_eq!(
679 traffic.track(TargetId::new(7)).unwrap().status(),
680 TrackStatus::Acquiring
681 );
682 assert!(traffic.ingest(northbound(7, 1, 60)).unwrap().is_empty());
683 let events = traffic.ingest(northbound(7, 2, 60)).unwrap();
684 assert_eq!(kinds(&events), ["acquired"]);
685 assert!(matches!(
686 events[0],
687 TrafficEvent::TargetAcquired { target, at } if target == TargetId::new(7) && at == after(120)
688 ));
689 assert_eq!(
690 traffic.track(TargetId::new(7)).unwrap().status(),
691 TrackStatus::Tracking
692 );
693 assert!(traffic.ingest(northbound(7, 3, 60)).unwrap().is_empty());
695 }
696
697 #[test]
698 fn a_policy_of_one_fix_acquires_on_sight() {
699 let mut traffic = Traffic::new(
700 TrackingPolicy::new(
701 1,
702 policy().stale_after(),
703 policy().lost_after(),
704 policy().max_speed(),
705 )
706 .unwrap(),
707 );
708 let events = traffic.ingest(northbound(9, 0, 10)).unwrap();
709 assert_eq!(kinds(&events), ["acquired"]);
710 }
711
712 #[test]
713 fn the_fitted_motion_is_the_targets_course_and_speed() {
714 let mut traffic = Traffic::new(policy());
715 for n in 0..5 {
716 let _ = traffic.ingest(northbound(7, n, 60)).unwrap();
717 }
718 let track = traffic.track(TargetId::new(7)).unwrap();
719 assert_eq!(track.fix_count(), 5);
720 assert_eq!(track.first_seen(), start());
721 assert_eq!(track.last_seen(), after(240));
722 let motion = track.fitted_motion().unwrap();
723 assert!(
724 motion.course_over_ground.degrees() < 0.01
725 || motion.course_over_ground.degrees() > 359.99
726 );
727 assert!((motion.speed_over_ground.knots() - 12.0).abs() < 0.01);
728 assert_eq!(track.motion(), Some(motion));
729 assert_eq!(track.reported_ground_track(), None);
730 assert_eq!(track.as_vessel().unwrap().speed, motion.speed_over_ground);
731
732 let ahead = track.position_at(after(300)).unwrap();
734 assert!((ahead.latitude().degrees() - (50.0 + 1.0 / 60.0)).abs() < 1e-4);
735 let behind = track.position_at(after(180)).unwrap();
736 assert!((behind.latitude().degrees() - (50.0 + 0.6 / 60.0)).abs() < 1e-4);
737 }
738
739 #[test]
740 fn the_fit_smooths_a_noisy_plot() {
741 let mut traffic = Traffic::new(policy());
742 for n in 0..8 {
744 let mut observation = northbound(7, n, 60);
745 if n % 2 == 1 {
746 observation = TargetObservation::new(
747 observation.target(),
748 at(
749 observation.position().latitude().degrees(),
750 -1.0 + 0.1 / 60.0 / math::cos(50.0_f64.to_radians()),
751 ),
752 observation.at(),
753 );
754 }
755 let _ = traffic.ingest(observation).unwrap();
756 }
757 let motion = traffic
758 .track(TargetId::new(7))
759 .unwrap()
760 .fitted_motion()
761 .unwrap();
762 let course = motion.course_over_ground.degrees();
764 assert!(course < 2.0 || course > 358.0, "{course}");
765 assert!((motion.speed_over_ground.knots() - 12.0).abs() < 0.5);
766 }
767
768 #[test]
769 fn a_reported_track_beats_the_fitted_one() {
770 let mut traffic = Traffic::new(policy());
771 let reported = GroundTrack {
772 course_over_ground: TrueCourse::new(3.0).unwrap(),
773 speed_over_ground: knots(11.5),
774 };
775 let _ = traffic
776 .ingest(northbound(7, 0, 60).with_ground_track(reported))
777 .unwrap();
778 let track = traffic.track(TargetId::new(7)).unwrap();
780 assert_eq!(track.fitted_motion(), None);
781 assert_eq!(track.motion(), Some(reported));
782
783 let _ = traffic
784 .ingest(northbound(7, 1, 60).with_heading(TrueCourse::new(5.0).unwrap()))
785 .unwrap();
786 let track = traffic.track(TargetId::new(7)).unwrap();
787 assert_eq!(track.motion(), Some(reported));
790 assert_eq!(track.heading(), Some(TrueCourse::new(5.0).unwrap()));
791 assert!(track.fitted_motion().is_some());
792 }
793
794 #[test]
795 fn the_window_keeps_the_latest_fixes() {
796 let mut traffic = Traffic::new(policy());
797 for n in 0..20 {
798 let _ = traffic.ingest(northbound(7, n, 30)).unwrap();
799 }
800 let track = traffic.track(TargetId::new(7)).unwrap();
801 assert_eq!(track.fix_count(), MAX_TRACK_HISTORY);
802 assert_eq!(track.last_seen(), after(19 * 30));
803 assert_eq!(
804 track.first_seen(),
805 after((20 - MAX_TRACK_HISTORY as u64) * 30)
806 );
807 let motion = track.fitted_motion().unwrap();
808 assert!((motion.speed_over_ground.knots() - 12.0).abs() < 0.01);
809 }
810
811 #[test]
812 fn an_observation_out_of_order_or_too_fast_is_turned_away() {
813 let mut traffic = Traffic::new(policy());
814 let _ = traffic.ingest(northbound(7, 1, 60)).unwrap();
815
816 let same = traffic.ingest(northbound(7, 1, 60)).unwrap();
818 assert!(matches!(
819 same[0],
820 TrafficEvent::ObservationRejected {
821 reason: RejectionReason::OutOfOrder,
822 ..
823 }
824 ));
825 let earlier = traffic.ingest(northbound(7, 0, 60)).unwrap();
826 assert_eq!(kinds(&earlier), ["rejected"]);
827
828 let jump = traffic
830 .ingest(TargetObservation::new(
831 TargetId::new(7),
832 at(50.0 + 10.0 / 60.0, -1.0),
833 after(120),
834 ))
835 .unwrap();
836 assert!(matches!(
837 jump[0],
838 TrafficEvent::ObservationRejected {
839 target,
840 reason: RejectionReason::ImplausibleJump { implied_speed },
841 at
842 } if implied_speed.knots() > 500.0 && at == after(120) && target == TargetId::new(7)
843 ));
844 let track = traffic.track(TargetId::new(7)).unwrap();
846 assert_eq!(track.fix_count(), 1);
847 assert_eq!(track.last_seen(), after(60));
848 }
849
850 #[test]
851 fn a_silent_target_goes_stale_and_then_is_lost() {
852 let mut traffic = Traffic::new(policy());
853 for n in 0..3 {
854 let _ = traffic.ingest(northbound(7, n, 60)).unwrap();
855 }
856 let _ = traffic.ingest(northbound(8, 0, 60)).unwrap();
857
858 let fresh = traffic.view(after(130));
859 assert_eq!(fresh.len(), 2);
860 assert!(!fresh.target(TargetId::new(7)).unwrap().stale);
861 assert_eq!(fresh.current().count(), 1);
862 assert!(fresh.target(TargetId::new(8)).unwrap().stale);
864 assert_eq!(fresh.at(), after(130));
865
866 let stale = traffic.view(after(160));
867 assert!(stale.target(TargetId::new(7)).unwrap().stale);
868 assert_eq!(
869 stale.target(TargetId::new(7)).unwrap().age,
870 Duration::from_secs(40)
871 );
872 assert_eq!(stale.current().count(), 0);
873
874 assert!(traffic.sweep(after(170)).is_empty());
876 assert_eq!(traffic.len(), 2);
877 let events = traffic.sweep(after(181));
878 assert_eq!(traffic.len(), 1);
879 assert!(matches!(
880 events[0],
881 TrafficEvent::TargetLost { target, last_seen }
882 if target == TargetId::new(8) && last_seen == start()
883 ));
884 let events = traffic.sweep(after(120 + 181));
885 assert_eq!(kinds(&events), ["lost"]);
886 assert!(traffic.is_empty());
887 assert!(traffic.view(after(1000)).is_empty());
888 }
889
890 #[test]
891 fn the_view_carries_the_target_forward() {
892 let mut traffic = Traffic::new(policy());
893 for n in 0..3 {
894 let _ = traffic.ingest(northbound(7, n, 60)).unwrap();
895 }
896 let view = traffic.view(after(180));
897 let seen = view.targets()[0];
898 assert_eq!(seen.target, TargetId::new(7));
899 assert_eq!(seen.status, TrackStatus::Tracking);
900 assert!((seen.position.latitude().degrees() - (50.0 + 0.6 / 60.0)).abs() < 1e-4);
902 assert!(seen.motion.is_some());
903 assert_eq!(seen.heading, None);
904 assert_eq!(seen.age, Duration::from_secs(60));
905 }
906
907 #[test]
908 fn a_full_picture_refuses_a_new_target_and_keeps_the_old() {
909 let mut traffic = Traffic::new(policy());
910 for target in 0..MAX_TARGETS {
911 let _ = traffic
912 .ingest(northbound(u32::try_from(target).unwrap(), 0, 60))
913 .unwrap();
914 }
915 assert_eq!(traffic.len(), MAX_TARGETS);
916 assert!(matches!(
917 traffic
918 .ingest(northbound(u32::try_from(MAX_TARGETS).unwrap(), 0, 60))
919 .unwrap_err(),
920 NavigationError::Kernel(KernelError::CapacityExceeded {
921 context: "the traffic picture",
922 ..
923 })
924 ));
925 assert_eq!(traffic.len(), MAX_TARGETS);
926 assert!(traffic.ingest(northbound(3, 1, 60)).is_ok());
928 }
929
930 #[test]
931 fn a_radar_contact_becomes_a_position() {
932 let own = at(50.0, -1.0);
933 let observation = TargetObservation::from_contact(
934 TargetId::new(4),
935 own,
936 Contact {
937 bearing: TrueBearing::new(90.0).unwrap(),
938 range: Distance::from_nautical_miles(6.0).unwrap(),
939 },
940 start(),
941 )
942 .unwrap();
943 assert_eq!(observation.target(), TargetId::new(4));
944 assert_eq!(observation.at(), start());
945 assert!((observation.position().latitude().degrees() - 50.0).abs() < 1e-9);
946 assert!(observation.position().longitude().degrees() > -1.0 + 0.15);
947 assert_eq!(observation.ground_track(), None);
948 assert_eq!(observation.heading(), None);
949 }
950
951 #[test]
952 fn a_policy_must_make_sense() {
953 let (stale, lost) = (Duration::from_secs(30), Duration::from_secs(180));
954 assert!(TrackingPolicy::new(0, stale, lost, knots(60.0)).is_err());
955 assert!(TrackingPolicy::new(3, stale, Duration::from_secs(10), knots(60.0)).is_err());
956 assert!(TrackingPolicy::new(3, stale, lost, Speed::ZERO).is_err());
957 assert_eq!(Traffic::new(policy()).policy(), policy());
958 assert_eq!(policy().on_full(), WhenFull::Refuse);
959 assert_eq!(
960 policy().when_full(WhenFull::EvictStalest).on_full(),
961 WhenFull::EvictStalest
962 );
963 }
964
965 #[test]
966 fn a_full_picture_told_to_evict_drops_the_stalest_and_says_so() {
967 let mut traffic = Traffic::<4>::with_capacity(policy().when_full(WhenFull::EvictStalest));
968 for target in 0..4_u64 {
971 let observation = TargetObservation::new(
972 TargetId::new(u32::try_from(target).unwrap()),
973 at(50.0, -1.0),
974 after(10 * target),
975 );
976 let _ = traffic.ingest(observation).unwrap();
977 }
978 let _ = traffic
979 .ingest(TargetObservation::new(
980 TargetId::new(1),
981 at(50.01, -1.0),
982 after(40),
983 ))
984 .unwrap();
985 let events = traffic
986 .ingest(TargetObservation::new(
987 TargetId::new(9),
988 at(50.0, -1.0),
989 after(50),
990 ))
991 .unwrap();
992 assert_eq!(traffic.len(), 4);
993 assert!(traffic.track(TargetId::new(0)).is_none());
994 assert!(traffic.track(TargetId::new(9)).is_some());
995 assert_eq!(
996 events.as_slice(),
997 [TrafficEvent::TargetEvicted {
998 target: TargetId::new(0),
999 last_seen: start(),
1000 for_target: TargetId::new(9),
1001 }]
1002 );
1003 for flood in 100..200_u64 {
1006 let seen = after(60 + flood);
1007 let _ = traffic
1008 .ingest(TargetObservation::new(
1009 TargetId::new(1),
1010 at(50.02, -1.0),
1011 seen,
1012 ))
1013 .unwrap();
1014 let newcomer = TargetId::new(u32::try_from(flood).unwrap());
1015 let _ = traffic
1016 .ingest(TargetObservation::new(newcomer, at(50.0, -1.0), seen))
1017 .unwrap();
1018 }
1019 assert_eq!(traffic.len(), 4);
1020 assert!(traffic.track(TargetId::new(1)).is_some());
1021 }
1022}