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 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 pub latitude: Latitude,
94
95 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 pub fn latitude_bounding(&self) -> RangeInclusive<f64> {
107 self.precision.range(self.latitude.value())
108 }
109
110 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 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 let b = if has_timestamp { &b[8..] } else { &b[1..] };
134
135 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 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 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}