1use crate::angle::TrueCourse;
32use crate::error::{ensure_finite, KernelError, Result};
33use crate::observation::{ObservationStatus, Observed, Quality};
34use crate::position::Position;
35use crate::time::{Instant, Utc};
36use crate::units::{Distance, Speed};
37
38#[non_exhaustive]
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub enum FixType {
49 None,
51 Autonomous,
53 Differential,
55 Precise,
57 RtkFixed,
59 RtkFloat,
61 Estimated,
63 Manual,
65 Simulated,
67}
68
69impl FixType {
70 #[must_use]
75 pub const fn is_position_fix(self) -> bool {
76 matches!(
77 self,
78 Self::Autonomous | Self::Differential | Self::Precise | Self::RtkFixed | Self::RtkFloat
79 )
80 }
81
82 const fn nominal_uere_metres(self) -> Option<f64> {
87 match self {
88 Self::Autonomous => Some(4.0),
89 Self::Differential => Some(1.0),
90 Self::Precise => Some(3.0),
91 Self::RtkFixed => Some(0.02),
92 Self::RtkFloat => Some(0.5),
93 Self::None | Self::Estimated | Self::Manual | Self::Simulated => None,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
102#[cfg_attr(
103 feature = "serde",
104 derive(serde::Serialize, serde::Deserialize),
105 serde(try_from = "f64", into = "f64")
106)]
107pub struct Dop(f64);
108
109impl Dop {
110 pub fn new(value: f64) -> Result<Self> {
117 ensure_finite("dilution of precision", value)?;
118 if value <= 0.0 {
119 return Err(KernelError::OutOfRange {
120 parameter: "dilution of precision",
121 value,
122 min: f64::MIN_POSITIVE,
123 max: f64::MAX,
124 });
125 }
126 Ok(Self(value))
127 }
128
129 #[must_use]
131 pub const fn value(self) -> f64 {
132 self.0
133 }
134}
135
136impl TryFrom<f64> for Dop {
137 type Error = KernelError;
138
139 fn try_from(value: f64) -> Result<Self> {
140 Self::new(value)
141 }
142}
143
144impl From<Dop> for f64 {
145 fn from(dop: Dop) -> Self {
146 dop.0
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct GnssQuality {
154 fix_type: FixType,
155 satellites: Option<u8>,
156 hdop: Option<Dop>,
157 vdop: Option<Dop>,
158 pdop: Option<Dop>,
159}
160
161impl GnssQuality {
162 #[must_use]
164 pub const fn fix_type(&self) -> FixType {
165 self.fix_type
166 }
167
168 #[must_use]
170 pub const fn satellites(&self) -> Option<u8> {
171 self.satellites
172 }
173
174 #[must_use]
176 pub const fn hdop(&self) -> Option<Dop> {
177 self.hdop
178 }
179
180 #[must_use]
182 pub const fn vdop(&self) -> Option<Dop> {
183 self.vdop
184 }
185
186 #[must_use]
188 pub const fn pdop(&self) -> Option<Dop> {
189 self.pdop
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199pub struct GnssFix {
200 taken_at: Instant<Utc>,
201 position: Position,
202 course_over_ground: Option<TrueCourse>,
203 speed_over_ground: Option<Speed>,
204 quality: GnssQuality,
205}
206
207impl GnssFix {
208 pub const fn builder(taken_at: Instant<Utc>, position: Position) -> GnssFixBuilder {
212 GnssFixBuilder {
213 fix: Self {
214 taken_at,
215 position,
216 course_over_ground: None,
217 speed_over_ground: None,
218 quality: GnssQuality {
219 fix_type: FixType::Autonomous,
220 satellites: None,
221 hdop: None,
222 vdop: None,
223 pdop: None,
224 },
225 },
226 }
227 }
228
229 #[must_use]
231 pub const fn taken_at(&self) -> Instant<Utc> {
232 self.taken_at
233 }
234
235 #[must_use]
237 pub const fn position(&self) -> Position {
238 self.position
239 }
240
241 #[must_use]
245 pub const fn course_over_ground(&self) -> Option<TrueCourse> {
246 self.course_over_ground
247 }
248
249 #[must_use]
251 pub const fn speed_over_ground(&self) -> Option<Speed> {
252 self.speed_over_ground
253 }
254
255 #[must_use]
257 pub const fn quality(&self) -> &GnssQuality {
258 &self.quality
259 }
260
261 #[must_use]
267 pub fn horizontal_accuracy(&self) -> Option<Distance> {
268 let uere = self.quality.fix_type.nominal_uere_metres()?;
269 self.horizontal_accuracy_with(Distance::from_metres(uere).ok()?)
271 }
272
273 #[must_use]
277 pub fn horizontal_accuracy_with(&self, uere: Distance) -> Option<Distance> {
278 self.quality.hdop.map(|hdop| uere * hdop.value())
279 }
280
281 #[must_use]
287 pub fn observed_position(&self) -> Observed<Position, Distance> {
288 let status = match self.quality.fix_type {
289 FixType::None => ObservationStatus::Invalid,
290 FixType::Estimated | FixType::Manual | FixType::Simulated => ObservationStatus::Suspect,
291 _ => ObservationStatus::Valid,
292 };
293 let mut quality = Quality::new(status);
294 if let Some(sigma) = self.horizontal_accuracy() {
295 quality = quality.with_sigma(sigma);
296 }
297 Observed::new(self.position, self.taken_at, quality)
298 }
299}
300
301#[derive(Debug, Clone, Copy)]
307#[must_use = "a builder does nothing until `build` is called"]
308pub struct GnssFixBuilder {
309 fix: GnssFix,
310}
311
312impl GnssFixBuilder {
313 pub const fn fix_type(mut self, fix_type: FixType) -> Self {
315 self.fix.quality.fix_type = fix_type;
316 self
317 }
318
319 pub const fn course_over_ground(mut self, course: TrueCourse) -> Self {
321 self.fix.course_over_ground = Some(course);
322 self
323 }
324
325 pub const fn speed_over_ground(mut self, speed: Speed) -> Self {
327 self.fix.speed_over_ground = Some(speed);
328 self
329 }
330
331 pub const fn satellites(mut self, count: u8) -> Self {
333 self.fix.quality.satellites = Some(count);
334 self
335 }
336
337 pub const fn hdop(mut self, hdop: Dop) -> Self {
339 self.fix.quality.hdop = Some(hdop);
340 self
341 }
342
343 pub const fn vdop(mut self, vdop: Dop) -> Self {
345 self.fix.quality.vdop = Some(vdop);
346 self
347 }
348
349 pub const fn pdop(mut self, pdop: Dop) -> Self {
351 self.fix.quality.pdop = Some(pdop);
352 self
353 }
354
355 #[must_use]
357 pub const fn build(self) -> GnssFix {
358 self.fix
359 }
360}
361
362#[cfg(test)]
363#[allow(clippy::unwrap_used, clippy::float_cmp)]
364mod tests {
365 use super::*;
366
367 fn somewhere() -> Position {
368 "50°45.3'N 001°20.0'W".parse().unwrap()
369 }
370
371 fn at(seconds: i64) -> Instant<Utc> {
372 Instant::from_unix_seconds(seconds)
373 }
374
375 #[test]
376 fn a_bare_fix_is_autonomous_with_nothing_else_known() {
377 let fix = GnssFix::builder(at(100), somewhere()).build();
378 assert_eq!(fix.taken_at(), at(100));
379 assert_eq!(fix.position(), somewhere());
380 assert_eq!(fix.quality().fix_type(), FixType::Autonomous);
381 assert_eq!(fix.course_over_ground(), None);
382 assert_eq!(fix.speed_over_ground(), None);
383 assert_eq!(fix.quality().satellites(), None);
384 assert_eq!(fix.horizontal_accuracy(), None);
385 }
386
387 #[test]
388 fn accuracy_is_hdop_times_the_range_error() {
389 let fix = GnssFix::builder(at(0), somewhere())
390 .hdop(Dop::new(2.0).unwrap())
391 .build();
392 assert!((fix.horizontal_accuracy().unwrap().metres() - 8.0).abs() < 1e-9);
394 let own = Distance::from_metres(1.5).unwrap();
395 assert!((fix.horizontal_accuracy_with(own).unwrap().metres() - 3.0).abs() < 1e-9);
396 let estimated = GnssFix::builder(at(0), somewhere())
398 .fix_type(FixType::Estimated)
399 .hdop(Dop::new(2.0).unwrap())
400 .build();
401 assert_eq!(estimated.horizontal_accuracy(), None);
402 assert!(estimated.horizontal_accuracy_with(own).is_some());
403 }
404
405 #[test]
406 fn the_observed_position_follows_the_fix_type() {
407 let cases = [
408 (FixType::RtkFixed, ObservationStatus::Valid),
409 (FixType::Autonomous, ObservationStatus::Valid),
410 (FixType::Estimated, ObservationStatus::Suspect),
411 (FixType::Simulated, ObservationStatus::Suspect),
412 (FixType::None, ObservationStatus::Invalid),
413 ];
414 for (fix_type, status) in cases {
415 let fix = GnssFix::builder(at(7), somewhere())
416 .fix_type(fix_type)
417 .hdop(Dop::new(1.0).unwrap())
418 .build();
419 let observed = fix.observed_position();
420 assert_eq!(observed.quality().status(), status, "{fix_type:?}");
421 assert_eq!(observed.taken_at(), at(7));
422 assert_eq!(*observed.value(), somewhere());
423 assert_eq!(
424 observed.quality().sigma().is_some(),
425 fix_type.is_position_fix(),
426 "{fix_type:?}"
427 );
428 }
429 }
430
431 #[test]
432 fn a_dilution_of_precision_is_positive_and_finite() {
433 assert!(Dop::new(0.0).is_err());
434 assert!(Dop::new(-1.0).is_err());
435 assert!(Dop::new(f64::NAN).is_err());
436 assert!(Dop::new(f64::INFINITY).is_err());
437 assert_eq!(Dop::new(1.5).unwrap().value(), 1.5);
438 }
439
440 #[cfg(feature = "serde")]
441 #[test]
442 fn serde_round_trips_and_validates_the_dop() {
443 let fix = GnssFix::builder(at(0), somewhere())
444 .fix_type(FixType::Differential)
445 .speed_over_ground(Speed::from_knots(3.0).unwrap())
446 .satellites(12)
447 .pdop(Dop::new(1.7).unwrap())
448 .build();
449 let json = serde_json::to_string(&fix).unwrap();
450 assert_eq!(serde_json::from_str::<GnssFix>(&json).unwrap(), fix);
451 assert!(serde_json::from_str::<Dop>("-1.0").is_err());
452 }
453}