1use core::time::Duration;
4
5use kinavis::error::Result;
6use kinavis::relative_motion::Vessel;
7use kinavis::sailings::rhumb_destination;
8use kinavis_kernel::angle::{Direction, True, TrueCourse};
9use kinavis_kernel::event::TargetId;
10use kinavis_kernel::inline::Inline;
11use kinavis_kernel::math;
12use kinavis_kernel::position::{Latitude, Longitude, Position};
13use kinavis_kernel::snapshot::GroundTrack;
14use kinavis_kernel::time::{Instant, Utc};
15use kinavis_kernel::units::{Distance, Speed};
16
17use crate::observation::TargetObservation;
18
19pub const MAX_TRACK_HISTORY: usize = 12;
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27struct Fix {
28 position: Position,
29 at: Instant<Utc>,
30}
31
32type Fixes = Inline<Fix, MAX_TRACK_HISTORY>;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39#[non_exhaustive]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub enum TrackStatus {
42 Acquiring,
44 Tracking,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
55pub struct TargetTrack {
56 target: TargetId,
57 fixes: Fixes,
59 latest: Fix,
61 reported: Option<GroundTrack>,
62 heading: Option<TrueCourse>,
63 status: TrackStatus,
64}
65
66struct Fit {
69 north_knots: f64,
70 east_knots: f64,
71 north_offset: f64,
73 east_offset: f64,
74}
75
76impl TargetTrack {
77 pub(crate) fn placeholder() -> Self {
79 Self::started_by(&TargetObservation::new(
80 TargetId::new(0),
81 Position::new(Latitude::EQUATOR, Longitude::GREENWICH),
82 Instant::from_unix_seconds(0),
83 ))
84 }
85
86 pub(crate) fn started_by(observation: &TargetObservation) -> Self {
88 let fix = Fix {
89 position: observation.position(),
90 at: observation.at(),
91 };
92 let mut fixes = Fixes::new(fix);
93 let _ = fixes.push(fix);
95 Self {
96 target: observation.target(),
97 fixes,
98 latest: fix,
99 reported: observation.ground_track(),
100 heading: observation.heading(),
101 status: TrackStatus::Acquiring,
102 }
103 }
104
105 pub(crate) fn extend(&mut self, observation: &TargetObservation) {
108 let fix = Fix {
109 position: observation.position(),
110 at: observation.at(),
111 };
112 if self.fixes.push(fix).is_err() {
113 let mut kept = Fixes::new(fix);
114 for &old in self.fixes.iter().skip(1) {
115 let _ = kept.push(old);
116 }
117 let _ = kept.push(fix);
118 self.fixes = kept;
119 }
120 self.latest = fix;
121 if observation.ground_track().is_some() {
122 self.reported = observation.ground_track();
123 }
124 if observation.heading().is_some() {
125 self.heading = observation.heading();
126 }
127 }
128
129 pub(crate) fn acquire(&mut self) {
131 self.status = TrackStatus::Tracking;
132 }
133
134 #[must_use]
136 pub const fn target(&self) -> TargetId {
137 self.target
138 }
139
140 #[must_use]
142 pub const fn status(&self) -> TrackStatus {
143 self.status
144 }
145
146 #[must_use]
148 pub const fn fix_count(&self) -> usize {
149 self.fixes.len()
150 }
151
152 #[must_use]
154 pub fn last_seen(&self) -> Instant<Utc> {
155 self.last().at
156 }
157
158 #[must_use]
160 pub fn last_position(&self) -> Position {
161 self.last().position
162 }
163
164 #[must_use]
166 pub fn first_seen(&self) -> Instant<Utc> {
167 self.fixes
168 .first()
169 .map_or_else(|| self.last().at, |fix| fix.at)
170 }
171
172 #[must_use]
174 pub const fn reported_ground_track(&self) -> Option<GroundTrack> {
175 self.reported
176 }
177
178 #[must_use]
180 pub const fn heading(&self) -> Option<TrueCourse> {
181 self.heading
182 }
183
184 #[must_use]
186 pub fn age(&self, now: Instant<Utc>) -> Duration {
187 now.checked_duration_since(self.last_seen())
188 .unwrap_or_default()
189 }
190
191 #[must_use]
196 pub fn motion(&self) -> Option<GroundTrack> {
197 self.reported.or_else(|| self.fitted_motion())
198 }
199
200 #[must_use]
205 pub fn fitted_motion(&self) -> Option<GroundTrack> {
206 let fit = self.fit()?;
207 let speed = math::hypot(fit.north_knots, fit.east_knots);
208 if !speed.is_finite() || speed <= 0.0 {
209 return None;
210 }
211 Some(GroundTrack {
212 course_over_ground: Direction::<True>::from_degrees_wrapped(math::to_degrees(
213 math::atan2(fit.east_knots, fit.north_knots),
214 )),
215 speed_over_ground: Speed::from_knots_unchecked(speed),
216 })
217 }
218
219 pub fn position_at(&self, when: Instant<Utc>) -> Result<Position> {
227 let base = self.smoothed_position()?;
228 let Some(motion) = self.motion() else {
229 return Ok(base);
230 };
231 let last = self.last_seen();
232 let hours = match when.checked_duration_since(last) {
233 Some(ahead) => ahead.as_secs_f64() / 3600.0,
234 None => {
235 -last
236 .checked_duration_since(when)
237 .unwrap_or_default()
238 .as_secs_f64()
239 / 3600.0
240 }
241 };
242 let run = Distance::from_nautical_miles(motion.speed_over_ground.knots() * hours)?;
243 rhumb_destination(base, motion.course_over_ground, run)
244 }
245
246 #[must_use]
248 pub fn as_vessel(&self) -> Option<Vessel> {
249 self.motion().map(|motion| Vessel {
250 course: motion.course_over_ground,
251 speed: motion.speed_over_ground,
252 })
253 }
254
255 const fn last(&self) -> Fix {
257 self.latest
258 }
259
260 fn smoothed_position(&self) -> Result<Position> {
263 let last = self.last();
264 let Some(fit) = self.fit() else {
265 return Ok(last.position);
266 };
267 let latitude = last.position.latitude().degrees() + fit.north_offset / 60.0;
268 let stretch = math::cos(last.position.latitude().radians());
269 if stretch < 1e-6 {
270 return Ok(last.position);
271 }
272 let longitude = last.position.longitude().degrees() + fit.east_offset / (60.0 * stretch);
273 if !(-90.0..=90.0).contains(&latitude) {
274 return Ok(last.position);
275 }
276 Ok(Position::from_degrees(latitude, longitude)?)
277 }
278
279 fn fit(&self) -> Option<Fit> {
282 if self.fixes.len() < 2 {
283 return None;
284 }
285 let last = self.last();
286 let count = math::count_to_f64(self.fixes.len());
287 let stretch = math::cos(last.position.latitude().radians());
288
289 let mut sum_t = 0.0;
290 let mut sum_n = 0.0;
291 let mut sum_e = 0.0;
292 for fix in self.fixes.iter() {
293 let (t, n, e) = local(fix, last, stretch);
294 sum_t += t;
295 sum_n += n;
296 sum_e += e;
297 }
298 let (mean_t, mean_n, mean_e) = (sum_t / count, sum_n / count, sum_e / count);
299
300 let mut s_tt = 0.0;
301 let mut s_tn = 0.0;
302 let mut s_te = 0.0;
303 for fix in self.fixes.iter() {
304 let (t, n, e) = local(fix, last, stretch);
305 s_tt += (t - mean_t) * (t - mean_t);
306 s_tn += (t - mean_t) * (n - mean_n);
307 s_te += (t - mean_t) * (e - mean_e);
308 }
309 if s_tt <= 0.0 || !s_tt.is_finite() {
310 return None;
311 }
312 let north_knots = s_tn / s_tt;
313 let east_knots = s_te / s_tt;
314 Some(Fit {
315 north_knots,
316 east_knots,
317 north_offset: mean_n - north_knots * mean_t,
318 east_offset: mean_e - east_knots * mean_t,
319 })
320 }
321}
322
323fn local(fix: &Fix, last: Fix, stretch: f64) -> (f64, f64, f64) {
326 let hours = -last
327 .at
328 .checked_duration_since(fix.at)
329 .unwrap_or_default()
330 .as_secs_f64()
331 / 3600.0;
332 let north = last.position.latitude_difference(fix.position).degrees() * 60.0;
333 let east = last.position.longitude_difference(fix.position).degrees() * 60.0 * stretch;
334 (hours, north, east)
335}