1use crate::error::AprsError;
2use crate::util::parse_bytes;
3use std::ops::{Deref, RangeInclusive};
4
5#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Default)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub enum Precision {
9 TenDegree,
10 OneDegree,
11 TenMinute,
12 OneMinute,
13 TenthMinute,
14 #[default]
15 HundredthMinute,
16}
17
18impl Precision {
19 pub fn width(self) -> f64 {
21 match self {
22 Precision::HundredthMinute => 1.0 / 6000.0,
23 Precision::TenthMinute => 1.0 / 600.0,
24 Precision::OneMinute => 1.0 / 60.0,
25 Precision::TenMinute => 1.0 / 6.0,
26 Precision::OneDegree => 1.0,
27 Precision::TenDegree => 10.0,
28 }
29 }
30
31 pub fn range(self, center: f64) -> RangeInclusive<f64> {
32 let w = self.width();
33 (center - w / 2.0)..=(center + w / 2.0)
34 }
35
36 fn num_blank_digits(self) -> u8 {
37 match self {
38 Precision::HundredthMinute => 0,
39 Precision::TenthMinute => 1,
40 Precision::OneMinute => 2,
41 Precision::TenMinute => 3,
42 Precision::OneDegree => 4,
43 Precision::TenDegree => 5,
44 }
45 }
46
47 fn from_blank_digits(blanks: u8) -> Option<Self> {
48 Some(match blanks {
49 0 => Precision::HundredthMinute,
50 1 => Precision::TenthMinute,
51 2 => Precision::OneMinute,
52 3 => Precision::TenMinute,
53 4 => Precision::OneDegree,
54 5 => Precision::TenDegree,
55 _ => return None,
56 })
57 }
58}
59
60
61#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Default)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64#[cfg_attr(feature = "serde", serde(transparent))]
65pub struct Latitude(f64);
66
67impl Deref for Latitude {
68 type Target = f64;
69 fn deref(&self) -> &f64 {
70 &self.0
71 }
72}
73
74impl Latitude {
75 pub fn new(value: f64) -> Option<Self> {
76 if value.is_nan() || !(-90.0..=90.0).contains(&value) {
77 None
78 } else {
79 Some(Self(value))
80 }
81 }
82
83 pub fn value(self) -> f64 {
84 self.0
85 }
86
87 pub fn dmh(self) -> (u32, u32, u32, bool) {
89 let (is_north, v) = if self.0 >= 0.0 { (true, self.0) } else { (false, -self.0) };
90 let deg = v as u32;
91 let min = ((v - deg as f64) * 60.0) as u32;
92 let mut hdths = ((v - deg as f64 - min as f64 / 60.0) * 6000.0).round() as u32;
93 let mut min = min;
94 let mut deg = deg;
95 if hdths >= 100 { hdths = 0; min += 1; }
96 if min >= 60 { min = 0; deg += 1; }
97 (deg, min, hdths, is_north)
98 }
99
100 pub(crate) fn parse_uncompressed(b: &[u8]) -> Result<(Self, Precision), AprsError> {
103 if b.len() != 8 || b[4] != b'.' {
104 return Err(AprsError::InvalidLatitude { raw: b.to_vec() });
105 }
106 let is_north = match b[7] {
107 b'N' => true,
108 b'S' => false,
109 _ => return Err(AprsError::InvalidLatitude { raw: b.to_vec() }),
110 };
111 let (deg, b0) = parse_pair_ambiguous(&[b[0], b[1]], false)
112 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
113 let (min, b1) = parse_pair_ambiguous(&[b[2], b[3]], b0 > 0)
114 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
115 let (hdths, b2) = parse_pair_ambiguous(&[b[5], b[6]], b1 > 0)
116 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
117 let blanks = b0 + b1 + b2;
118 let precision = Precision::from_blank_digits(blanks)
119 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
120 let value = deg as f64 + min as f64 / 60.0 + hdths as f64 / 6000.0;
121 let value = if is_north { value } else { -value };
122 let lat = Latitude::new(value)
123 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
124 Ok((lat, precision))
125 }
126
127 pub(crate) fn parse_compressed(b: &[u8]) -> Result<Self, AprsError> {
129 let enc = base91_decode4(b)
130 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
131 let value = 90.0 - enc / 380926.0;
132 Latitude::new(value).ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })
133 }
134
135 pub(crate) fn encode_uncompressed(&self, out: &mut Vec<u8>, precision: Precision) {
136 let (deg, min, hdths, is_north) = self.dmh();
137 let dir = if is_north { b'N' } else { b'S' };
138 let blanks = precision.num_blank_digits() as usize;
139 let mut digits = [0u8; 6];
141 let _ = write_digits_6(&mut digits, deg, min, hdths);
142 let end = 6usize.saturating_sub(blanks);
143 let mut buf = [b' '; 6];
144 buf[..end].copy_from_slice(&digits[..end]);
145 out.extend_from_slice(&buf[..4]);
146 out.push(b'.');
147 out.extend_from_slice(&buf[4..6]);
148 out.push(dir);
149 }
150
151 pub(crate) fn encode_compressed(&self, out: &mut Vec<u8>) {
152 let value = (90.0 - self.0) * 380926.0;
153 base91_encode4(value.round() as u32, out);
154 }
155}
156
157#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Default)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160#[cfg_attr(feature = "serde", serde(transparent))]
161pub struct Longitude(f64);
162
163impl Deref for Longitude {
164 type Target = f64;
165 fn deref(&self) -> &f64 {
166 &self.0
167 }
168}
169
170impl Longitude {
171 pub fn new(value: f64) -> Option<Self> {
172 if value.is_nan() || !(-180.0..=180.0).contains(&value) {
173 None
174 } else {
175 Some(Self(value))
176 }
177 }
178
179 pub fn value(self) -> f64 {
180 self.0
181 }
182
183 pub fn dmh(self) -> (u32, u32, u32, bool) {
185 let (is_east, v) = if self.0 >= 0.0 { (true, self.0) } else { (false, -self.0) };
186 let deg = v as u32;
187 let min = ((v - deg as f64) * 60.0) as u32;
188 let mut hdths = ((v - deg as f64 - min as f64 / 60.0) * 6000.0).round() as u32;
189 let mut min = min;
190 let mut deg = deg;
191 if hdths >= 100 { hdths = 0; min += 1; }
192 if min >= 60 { min = 0; deg += 1; }
193 (deg, min, hdths, is_east)
194 }
195
196 pub(crate) fn parse_uncompressed(b: &[u8], precision: Precision) -> Result<Self, AprsError> {
199 if b.len() != 9 || b[5] != b'.' {
200 return Err(AprsError::InvalidLongitude { raw: b.to_vec() });
201 }
202 let is_east = match b[8] {
203 b'E' => true,
204 b'W' => false,
205 _ => return Err(AprsError::InvalidLongitude { raw: b.to_vec() }),
206 };
207 let mut digits = [0u8; 7];
209 digits[0..5].copy_from_slice(&b[0..5]);
210 digits[5..7].copy_from_slice(&b[6..8]);
211 let blanks = precision.num_blank_digits() as usize;
213 for d in digits.iter_mut().skip(7usize.saturating_sub(blanks)) {
214 *d = b'0';
215 }
216 let deg = parse_bytes::<u32>(&digits[0..3])
217 .ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })?;
218 let min = parse_bytes::<u32>(&digits[3..5])
219 .ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })?;
220 let hdths = parse_bytes::<u32>(&digits[5..7])
221 .ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })?;
222 let value = deg as f64 + min as f64 / 60.0 + hdths as f64 / 6000.0;
223 let value = if is_east { value } else { -value };
224 Longitude::new(value).ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })
225 }
226
227 pub(crate) fn parse_compressed(b: &[u8]) -> Result<Self, AprsError> {
229 let enc = base91_decode4(b)
230 .ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })?;
231 let value = enc / 190463.0 - 180.0;
232 Longitude::new(value).ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })
233 }
234
235 pub(crate) fn encode_uncompressed(&self, out: &mut Vec<u8>) {
236 let (deg, min, hdths, is_east) = self.dmh();
237 let dir = if is_east { b'E' } else { b'W' };
238 out.extend_from_slice(format!("{:03}{:02}.{:02}{}", deg, min, hdths, dir as char).as_bytes());
239 }
240
241 pub(crate) fn encode_compressed(&self, out: &mut Vec<u8>) {
242 let value = (180.0 + self.0) * 190463.0;
243 base91_encode4(value.round() as u32, out);
244 }
245}
246
247pub(crate) fn base91_decode4(b: &[u8]) -> Option<f64> {
250 if b.len() < 4 { return None; }
251 let mut val = 0.0f64;
252 for &byte in &b[..4] {
253 let d = byte.checked_sub(33)?;
254 if d > 90 { return None; }
255 val = val * 91.0 + d as f64;
256 }
257 Some(val)
258}
259
260pub(crate) fn base91_encode4(mut val: u32, out: &mut Vec<u8>) {
261 let mut buf = [33u8; 4]; for i in (0..4).rev() {
263 buf[i] = (val % 91) as u8 + 33;
264 val /= 91;
265 }
266 out.extend_from_slice(&buf);
267}
268
269pub(crate) fn base91_decode1(b: u8) -> Option<u8> {
270 b.checked_sub(33)
271}
272
273pub(crate) fn base91_encode1(v: u8) -> u8 {
274 v + 33
275}
276
277fn parse_pair_ambiguous(b: &[u8; 2], must_be_spaces: bool) -> Option<(u32, u8)> {
282 if must_be_spaces {
283 return if b == b" " { Some((0, 2)) } else { None };
284 }
285 match (b[0], b[1]) {
286 (b' ', b' ') => Some((0, 2)),
287 (d, b' ') if d.is_ascii_digit() => Some(((d - b'0') as u32 * 10, 1)),
288 (d0, d1) if d0.is_ascii_digit() && d1.is_ascii_digit() => {
289 Some(((d0 - b'0') as u32 * 10 + (d1 - b'0') as u32, 0))
290 }
291 _ => None,
292 }
293}
294
295fn write_digits_6(buf: &mut [u8; 6], deg: u32, min: u32, hdths: u32) -> Option<()> {
297 let s = format!("{:02}{:02}{:02}", deg, min, hdths);
298 if s.len() != 6 { return None; }
299 buf.copy_from_slice(s.as_bytes());
300 Some(())
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use approx::assert_relative_eq;
307
308 #[test]
309 fn lat_uncompressed_basic() {
310 let (lat, prec) = Latitude::parse_uncompressed(b"4903.50N").unwrap();
311 assert_relative_eq!(lat.value(), 49.05833333333333, epsilon = 1e-9);
312 assert_eq!(prec, Precision::HundredthMinute);
313 }
314
315 #[test]
316 fn lat_uncompressed_south() {
317 let (lat, _) = Latitude::parse_uncompressed(b"4903.50S").unwrap();
318 assert_relative_eq!(lat.value(), -49.05833333333333, epsilon = 1e-9);
319 }
320
321 #[test]
322 fn lat_ambiguity_one_tenth() {
323 let (lat, prec) = Latitude::parse_uncompressed(b"4903.5 N").unwrap();
324 assert_eq!(prec, Precision::TenthMinute);
325 assert_relative_eq!(lat.value(), 49.05833333333333, epsilon = 1e-4);
326 }
327
328 #[test]
329 fn lat_ambiguity_one_minute() {
330 let (lat, prec) = Latitude::parse_uncompressed(b"4903. N").unwrap();
331 assert_eq!(prec, Precision::OneMinute);
332 assert_relative_eq!(lat.value(), 49.05, epsilon = 1e-4);
333 }
334
335 #[test]
336 fn lat_invalid_direction() {
337 assert!(Latitude::parse_uncompressed(b"4903.50W").is_err());
338 }
339
340 #[test]
341 fn lat_out_of_range() {
342 assert!(Latitude::new(90.1).is_none());
343 assert!(Latitude::new(-90.1).is_none());
344 }
345
346 #[test]
347 fn lon_uncompressed_east() {
348 let lon = Longitude::parse_uncompressed(b"07201.75E", Precision::default()).unwrap();
349 assert_relative_eq!(lon.value(), 72.02916666666667, epsilon = 1e-9);
350 }
351
352 #[test]
353 fn lon_uncompressed_west() {
354 let lon = Longitude::parse_uncompressed(b"07201.75W", Precision::default()).unwrap();
355 assert_relative_eq!(lon.value(), -72.02916666666667, epsilon = 1e-9);
356 }
357
358 #[test]
359 fn lon_invalid_direction() {
360 assert!(Longitude::parse_uncompressed(b"07201.75N", Precision::default()).is_err());
361 }
362
363 #[test]
364 fn lat_encode_round_trip() {
365 let (lat, prec) = Latitude::parse_uncompressed(b"4903.50N").unwrap();
366 let mut out = Vec::new();
367 lat.encode_uncompressed(&mut out, prec);
368 assert_eq!(out, b"4903.50N");
369 }
370
371 #[test]
372 fn lon_encode_round_trip() {
373 let lon = Longitude::parse_uncompressed(b"07201.75W", Precision::default()).unwrap();
374 let mut out = Vec::new();
375 lon.encode_uncompressed(&mut out);
376 assert_eq!(out, b"07201.75W");
377 }
378
379 #[test]
380 fn compressed_lat_round_trip() {
381 let original = Latitude::new(49.05833).unwrap();
382 let mut enc = Vec::new();
383 original.encode_compressed(&mut enc);
384 assert_eq!(enc.len(), 4);
385 let decoded = Latitude::parse_compressed(&enc).unwrap();
386 assert_relative_eq!(decoded.value(), original.value(), epsilon = 0.001);
387 }
388
389 #[test]
390 fn compressed_lon_round_trip() {
391 let original = Longitude::new(-72.029).unwrap();
392 let mut enc = Vec::new();
393 original.encode_compressed(&mut enc);
394 assert_eq!(enc.len(), 4);
395 let decoded = Longitude::parse_compressed(&enc).unwrap();
396 assert_relative_eq!(decoded.value(), original.value(), epsilon = 0.001);
397 }
398
399 #[test]
400 fn base91_decode4_known() {
401 let val = base91_decode4(b"#$%^").unwrap();
403 assert_relative_eq!(val, 1532410.0, epsilon = 0.5);
404 }
405}