aprs_parser/
position.rs

1use std::convert::TryFrom;
2use std::io::Write;
3use std::ops::RangeInclusive;
4
5use lonlat::{Latitude, Longitude};
6use AprsCompressedCs;
7use AprsCompressionType;
8use Callsign;
9use DecodeError;
10use EncodeError;
11use Timestamp;
12
13#[derive(PartialEq, Debug, Clone)]
14pub enum AprsCst {
15    CompressedSome {
16        cs: AprsCompressedCs,
17        t: AprsCompressionType,
18    },
19    CompressedNone,
20    Uncompressed,
21}
22
23#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq)]
24pub enum Precision {
25    TenDegree,
26    OneDegree,
27    TenMinute,
28    OneMinute,
29    TenthMinute,
30    HundredthMinute,
31}
32
33impl Precision {
34    /// Returns the width of the precision in degrees.
35    /// For example, `Precision::OneDegree` would return 1.0.
36    pub fn width(&self) -> f64 {
37        match self {
38            Precision::HundredthMinute => 1.0 / 6000.0,
39            Precision::TenthMinute => 1.0 / 600.0,
40            Precision::OneMinute => 1.0 / 60.0,
41            Precision::TenMinute => 1.0 / 6.0,
42            Precision::OneDegree => 1.0,
43            Precision::TenDegree => 10.0,
44        }
45    }
46
47    fn range(&self, center: f64) -> RangeInclusive<f64> {
48        let width = self.width();
49
50        (center - (width / 2.0))..=(center + (width / 2.0))
51    }
52
53    pub(crate) fn num_digits(&self) -> u8 {
54        match self {
55            Precision::HundredthMinute => 0,
56            Precision::TenthMinute => 1,
57            Precision::OneMinute => 2,
58            Precision::TenMinute => 3,
59            Precision::OneDegree => 4,
60            Precision::TenDegree => 5,
61        }
62    }
63
64    pub(crate) fn from_num_digits(digits: u8) -> Option<Self> {
65        let res = match digits {
66            0 => Precision::HundredthMinute,
67            1 => Precision::TenthMinute,
68            2 => Precision::OneMinute,
69            3 => Precision::TenMinute,
70            4 => Precision::OneDegree,
71            5 => Precision::TenDegree,
72            _ => return None,
73        };
74
75        Some(res)
76    }
77}
78
79impl Default for Precision {
80    fn default() -> Self {
81        Self::HundredthMinute
82    }
83}
84
85#[derive(PartialEq, Debug, Clone)]
86pub struct AprsPosition {
87    pub to: Callsign,
88
89    pub timestamp: Option<Timestamp>,
90    pub messaging_supported: bool,
91
92    /// Latitudes aren't specified precisely in APRS and have ambiguity built in. This value stores the center, but you can also call `AprsPosition::latitude_bounding()` to get the entire range that the actual latitude could be in.
93    pub latitude: Latitude,
94
95    /// Longitudes aren't specified precisely in APRS and have ambiguity built in. This value stores the center, but you can also call `AprsPosition::longitude_bounding()` to get the entire range that the actual longitude could be in.
96    pub longitude: Longitude,
97    pub precision: Precision,
98    pub symbol_table: char,
99    pub symbol_code: char,
100    pub comment: Vec<u8>,
101    pub cst: AprsCst,
102}
103
104impl AprsPosition {
105    /// Latitudes in APRS aren't perfectly precise - they have a configurable level of ambiguity. This is stored in the `precision` field on the `AprsPosition` struct. This method returns a range of what the actual latitude value might be.
106    pub fn latitude_bounding(&self) -> RangeInclusive<f64> {
107        self.precision.range(self.latitude.value())
108    }
109
110    /// Longitudes in APRS aren't perfectly precise - they have a configurable level of ambiguity. This is stored in the `precision` field on the `AprsPosition` struct. This method returns a range of what the actual longitude value might be.
111    pub fn longitude_bounding(&self) -> RangeInclusive<f64> {
112        self.precision.range(self.longitude.value())
113    }
114
115    pub fn decode(b: &[u8], to: Callsign) -> Result<Self, DecodeError> {
116        let first = *b
117            .first()
118            .ok_or_else(|| DecodeError::InvalidPosition(vec![]))?;
119        let messaging_supported = first == b'=' || first == b'@';
120
121        // parse timestamp if necessary
122        let has_timestamp = first == b'@' || first == b'/';
123        let timestamp = if has_timestamp {
124            Some(Timestamp::try_from(
125                b.get(1..8)
126                    .ok_or_else(|| DecodeError::InvalidPosition(b.to_vec()))?,
127            )?)
128        } else {
129            None
130        };
131
132        // strip leading type symbol and potential timestamp
133        let b = if has_timestamp { &b[8..] } else { &b[1..] };
134
135        // check for compressed position format
136        let is_uncompressed_position = (*b.first().unwrap_or(&0) as char).is_numeric();
137        match is_uncompressed_position {
138            true => Self::parse_uncompressed(b, to, timestamp, messaging_supported),
139            false => Self::parse_compressed(b, to, timestamp, messaging_supported),
140        }
141    }
142
143    fn parse_compressed(
144        b: &[u8],
145        to: Callsign,
146        timestamp: Option<Timestamp>,
147        messaging_supported: bool,
148    ) -> Result<Self, DecodeError> {
149        if b.len() < 13 {
150            return Err(DecodeError::InvalidPosition(b.to_owned()));
151        }
152
153        let symbol_table = b[0] as char;
154        let comp_lat = &b[1..5];
155        let comp_lon = &b[5..9];
156        let symbol_code = b[9] as char;
157        let course_speed = &b[10..12];
158        let comp_type = b[12];
159
160        let latitude = Latitude::parse_compressed(comp_lat)?;
161        let longitude = Longitude::parse_compressed(comp_lon)?;
162
163        // From the APRS spec - if the c value is a space,
164        // the csT doesn't matter
165        let cst = match course_speed[0] {
166            b' ' => AprsCst::CompressedNone,
167            _ => {
168                let t = comp_type
169                    .checked_sub(33)
170                    .ok_or_else(|| DecodeError::InvalidPosition(b.to_owned()))?
171                    .into();
172                let cs = AprsCompressedCs::parse(course_speed[0], course_speed[1], t)?;
173                AprsCst::CompressedSome { cs, t }
174            }
175        };
176
177        let comment = b[13..].to_owned();
178
179        Ok(Self {
180            to,
181            timestamp,
182            messaging_supported,
183            latitude,
184            longitude,
185            precision: Precision::default(),
186            symbol_table,
187            symbol_code,
188            comment,
189            cst,
190        })
191    }
192
193    fn parse_uncompressed(
194        b: &[u8],
195        to: Callsign,
196        timestamp: Option<Timestamp>,
197        messaging_supported: bool,
198    ) -> Result<Self, DecodeError> {
199        if b.len() < 19 {
200            return Err(DecodeError::InvalidPosition(b.to_owned()));
201        }
202
203        // parse position
204        let (latitude, precision) = Latitude::parse_uncompressed(&b[0..8])?;
205        let longitude = Longitude::parse_uncompressed(&b[9..18], precision)?;
206
207        let symbol_table = b[8] as char;
208        let symbol_code = b[18] as char;
209
210        let comment = b[19..].to_owned();
211
212        Ok(Self {
213            to,
214            timestamp,
215            messaging_supported,
216            latitude,
217            longitude,
218            precision,
219            symbol_table,
220            symbol_code,
221            comment,
222            cst: AprsCst::Uncompressed,
223        })
224    }
225
226    pub fn encode<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
227        let sym = match (self.timestamp.is_some(), self.messaging_supported) {
228            (true, true) => '@',
229            (true, false) => '/',
230            (false, true) => '=',
231            (false, false) => '!',
232        };
233
234        write!(buf, "{}", sym)?;
235
236        if let Some(ts) = &self.timestamp {
237            ts.encode(buf)?;
238        }
239
240        match self.cst {
241            AprsCst::Uncompressed => self.encode_uncompressed(buf),
242            AprsCst::CompressedSome { cs, t } => self.encode_compressed(buf, Some((cs, t))),
243            AprsCst::CompressedNone => self.encode_compressed(buf, None),
244        }
245    }
246
247    pub fn encode_uncompressed<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
248        self.latitude.encode_uncompressed(buf, self.precision)?;
249        write!(buf, "{}", self.symbol_table)?;
250        self.longitude.encode_uncompressed(buf)?;
251        write!(buf, "{}", self.symbol_code)?;
252
253        buf.write_all(&self.comment)?;
254
255        Ok(())
256    }
257
258    pub fn encode_compressed<W: Write>(
259        &self,
260        buf: &mut W,
261        extra: Option<(AprsCompressedCs, AprsCompressionType)>,
262    ) -> Result<(), EncodeError> {
263        write!(buf, "{}", self.symbol_table)?;
264
265        self.latitude.encode_compressed(buf)?;
266        self.longitude.encode_compressed(buf)?;
267
268        write!(buf, "{}", self.symbol_code)?;
269
270        match extra {
271            Some((cs, t)) => {
272                cs.encode(buf, t)?;
273            }
274            None => write!(buf, " sT")?,
275        };
276
277        buf.write_all(&self.comment)?;
278
279        Ok(())
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use compression_type::{GpsFix, NmeaSource, Origin};
287    use AprsAltitude;
288    use AprsCourseSpeed;
289    use AprsRadioRange;
290
291    fn default_callsign() -> Callsign {
292        Callsign::new_no_ssid("VE9")
293    }
294
295    #[test]
296    fn precision_e2e() {
297        for i in 0..6 {
298            assert_eq!(i, Precision::from_num_digits(i).unwrap().num_digits());
299        }
300    }
301
302    #[test]
303    fn parse_compressed_without_timestamp_or_messaging() {
304        let result = AprsPosition::decode(&b"!/ABCD#$%^- >C"[..], default_callsign()).unwrap();
305
306        assert_eq!(result.to, default_callsign());
307        assert_eq!(result.timestamp, None);
308        assert!(!result.messaging_supported);
309        assert_relative_eq!(*result.latitude, 25.97004667573229);
310        assert_relative_eq!(*result.longitude, -171.95429033460567);
311        assert_eq!(result.symbol_table, '/');
312        assert_eq!(result.symbol_code, '-');
313        assert_eq!(result.comment, []);
314        assert_eq!(result.cst, AprsCst::CompressedNone);
315    }
316
317    #[test]
318    fn parse_compressed_with_comment() {
319        let result =
320            AprsPosition::decode(&b"!/ABCD#$%^-X>DHello/A=001000"[..], default_callsign()).unwrap();
321
322        assert_eq!(result.to, default_callsign());
323        assert_eq!(result.timestamp, None);
324        assert_relative_eq!(*result.latitude, 25.97004667573229);
325        assert_relative_eq!(*result.longitude, -171.95429033460567);
326        assert_eq!(result.symbol_table, '/');
327        assert_eq!(result.symbol_code, '-');
328        assert_eq!(result.comment, b"Hello/A=001000");
329        assert_eq!(
330            result.cst,
331            AprsCst::CompressedSome {
332                cs: AprsCompressedCs::CourseSpeed(AprsCourseSpeed::new(220, 8.317274897290226,)),
333                t: AprsCompressionType {
334                    gps_fix: GpsFix::Current,
335                    nmea_source: NmeaSource::Other,
336                    origin: Origin::Tbd,
337                }
338            }
339        );
340    }
341
342    #[test]
343    fn parse_compressed_with_timestamp_without_messaging() {
344        let result = AprsPosition::decode(
345            &br"/074849h\ABCD#$%^^{?C322/103/A=003054"[..],
346            default_callsign(),
347        )
348        .unwrap();
349
350        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
351        assert!(!result.messaging_supported);
352        assert_relative_eq!(*result.latitude, 25.97004667573229);
353        assert_relative_eq!(*result.longitude, -171.95429033460567);
354        assert_eq!(result.symbol_table, '\\');
355        assert_eq!(result.symbol_code, '^');
356        assert_eq!(result.comment, b"322/103/A=003054");
357        assert_eq!(
358            result.cst,
359            AprsCst::CompressedSome {
360                cs: AprsCompressedCs::RadioRange(AprsRadioRange::new(20.12531377814689)),
361                t: AprsCompressionType {
362                    gps_fix: GpsFix::Current,
363                    nmea_source: NmeaSource::Other,
364                    origin: Origin::Software,
365                }
366            }
367        );
368    }
369
370    #[test]
371    fn parse_compressed_without_timestamp_with_messaging() {
372        let result = AprsPosition::decode(&b"=/ABCD#$%^-S]1"[..], default_callsign()).unwrap();
373
374        assert_eq!(result.to, default_callsign());
375        assert_eq!(result.timestamp, None);
376        assert!(result.messaging_supported);
377        assert_relative_eq!(*result.latitude, 25.97004667573229);
378        assert_relative_eq!(*result.longitude, -171.95429033460567);
379        assert_eq!(result.symbol_table, '/');
380        assert_eq!(result.symbol_code, '-');
381        assert_eq!(result.comment, []);
382        assert_eq!(
383            result.cst,
384            AprsCst::CompressedSome {
385                cs: AprsCompressedCs::Altitude(AprsAltitude::new(10004.520050700292)),
386                t: AprsCompressionType {
387                    gps_fix: GpsFix::Old,
388                    nmea_source: NmeaSource::Gga,
389                    origin: Origin::Compressed,
390                }
391            }
392        );
393    }
394
395    #[test]
396    fn parse_compressed_with_timestamp_and_messaging() {
397        let result = AprsPosition::decode(
398            &br"@074849h\ABCD#$%^^ >C322/103/A=003054"[..],
399            default_callsign(),
400        )
401        .unwrap();
402
403        assert_eq!(result.to, default_callsign());
404        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
405        assert!(result.messaging_supported);
406        assert_relative_eq!(*result.latitude, 25.97004667573229);
407        assert_relative_eq!(*result.longitude, -171.95429033460567);
408        assert_eq!(result.symbol_table, '\\');
409        assert_eq!(result.symbol_code, '^');
410        assert_eq!(result.comment, b"322/103/A=003054");
411        assert_eq!(result.cst, AprsCst::CompressedNone);
412    }
413
414    #[test]
415    fn parse_without_timestamp_or_messaging() {
416        let result =
417            AprsPosition::decode(&b"!4903.50N/07201.75W-"[..], default_callsign()).unwrap();
418
419        assert_eq!(result.to, default_callsign());
420        assert_eq!(result.timestamp, None);
421        assert!(!result.messaging_supported);
422        assert_relative_eq!(*result.latitude, 49.05833333333333);
423        assert_relative_eq!(*result.longitude, -72.02916666666667);
424        assert_eq!(result.symbol_table, '/');
425        assert_eq!(result.symbol_code, '-');
426        assert_eq!(result.comment, []);
427        assert_eq!(result.cst, AprsCst::Uncompressed);
428    }
429
430    #[test]
431    fn parse_with_comment() {
432        let result = AprsPosition::decode(
433            &b"!4903.5 N/07201.75W-Hello/A=001000"[..],
434            default_callsign(),
435        )
436        .unwrap();
437
438        assert_eq!(result.to, default_callsign());
439        assert_eq!(result.timestamp, None);
440        assert_eq!(*result.latitude, 49.05833333333333);
441        assert_eq!(*result.longitude, -72.02833333333334);
442        assert_eq!(Precision::TenthMinute, result.precision);
443        assert_eq!(49.0575..=49.05916666666666, result.latitude_bounding());
444        assert_eq!(-72.02916666666667..=-72.0275, result.longitude_bounding());
445        assert_eq!(result.symbol_table, '/');
446        assert_eq!(result.symbol_code, '-');
447        assert_eq!(result.comment, b"Hello/A=001000");
448        assert_eq!(result.cst, AprsCst::Uncompressed);
449    }
450
451    #[test]
452    fn parse_with_timestamp_without_messaging() {
453        let result = AprsPosition::decode(
454            &br"/074849h4821.61N\01224.49E^322/103/A=003054"[..],
455            default_callsign(),
456        )
457        .unwrap();
458
459        assert_eq!(result.to, default_callsign());
460        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
461        assert!(!result.messaging_supported);
462        assert_relative_eq!(*result.latitude, 48.36016666666667);
463        assert_relative_eq!(*result.longitude, 12.408166666666666);
464        assert_eq!(result.symbol_table, '\\');
465        assert_eq!(result.symbol_code, '^');
466        assert_eq!(result.comment, b"322/103/A=003054");
467        assert_eq!(result.cst, AprsCst::Uncompressed);
468    }
469
470    #[test]
471    fn parse_without_timestamp_with_messaging() {
472        let result =
473            AprsPosition::decode(&b"=4903.50N/07201.75W-"[..], default_callsign()).unwrap();
474
475        assert_eq!(result.to, default_callsign());
476        assert_eq!(result.timestamp, None);
477        assert!(result.messaging_supported);
478        assert_relative_eq!(*result.latitude, 49.05833333333333);
479        assert_relative_eq!(*result.longitude, -72.02916666666667);
480        assert_eq!(result.symbol_table, '/');
481        assert_eq!(result.symbol_code, '-');
482        assert_eq!(result.comment, []);
483        assert_eq!(result.cst, AprsCst::Uncompressed);
484    }
485
486    #[test]
487    fn parse_with_timestamp_and_messaging() {
488        let result = AprsPosition::decode(
489            &br"@074849h4821.61N\01224.49E^322/103/A=003054"[..],
490            default_callsign(),
491        )
492        .unwrap();
493
494        assert_eq!(result.to, default_callsign());
495        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
496        assert!(result.messaging_supported);
497        assert_relative_eq!(*result.latitude, 48.36016666666667);
498        assert_relative_eq!(*result.longitude, 12.408166666666666);
499        assert_eq!(result.symbol_table, '\\');
500        assert_eq!(result.symbol_code, '^');
501        assert_eq!(result.comment, b"322/103/A=003054");
502        assert_eq!(result.cst, AprsCst::Uncompressed);
503    }
504
505    #[test]
506    fn parse_and_reencode_positions() {
507        let positions = vec![
508            &b"!/ABCD#$%^- sT"[..],
509            &b"!/ABCD#$%^-A>CHello/A=001000"[..],
510            &b"/074849h/ABCD#$%^-{>C322/103/A=001000"[..],
511            &b"=/ABCD#$%^-2>1"[..],
512            &b"@074849h/ABCD#$%^- sT"[..],
513            &b"!4903.50N/07201.75W-"[..],
514            &b"!4903.50N/07201.75W-Hello/A=001000"[..],
515            &br"/074849h4821.61N\01224.49E^322/103/A=003054"[..],
516            &b"=4903.50N/07201.75W-"[..],
517            &br"@074849h4821.61N\01224.49E^322/103/A=003054"[..],
518            &br"@074849h4821.  N\01224.00E^322/103/A=003054"[..],
519        ];
520
521        for p in positions {
522            let pos = AprsPosition::decode(p, default_callsign()).unwrap();
523            let mut buf = vec![];
524            pos.encode(&mut buf).unwrap();
525
526            assert_eq!(
527                p,
528                buf,
529                "Expected '{}', got '{}'",
530                String::from_utf8_lossy(p),
531                String::from_utf8_lossy(&buf)
532            );
533        }
534    }
535}