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#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Default)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63#[cfg_attr(feature = "serde", serde(transparent))]
64pub struct Latitude(f64);
65
66impl Deref for Latitude {
67 type Target = f64;
68 fn deref(&self) -> &f64 {
69 &self.0
70 }
71}
72
73impl Latitude {
74 pub fn new(value: f64) -> Option<Self> {
75 if value.is_nan() || !(-90.0..=90.0).contains(&value) {
76 None
77 } else {
78 Some(Self(value))
79 }
80 }
81
82 pub fn value(self) -> f64 {
83 self.0
84 }
85
86 pub fn dmh(self) -> (u32, u32, u32, bool) {
88 let (is_north, v) = if self.0 >= 0.0 {
89 (true, self.0)
90 } else {
91 (false, -self.0)
92 };
93 let deg = v as u32;
94 let min = ((v - deg as f64) * 60.0) as u32;
95 let mut hdths = ((v - deg as f64 - min as f64 / 60.0) * 6000.0).round() as u32;
96 let mut min = min;
97 let mut deg = deg;
98 if hdths >= 100 {
99 hdths = 0;
100 min += 1;
101 }
102 if min >= 60 {
103 min = 0;
104 deg += 1;
105 }
106 (deg, min, hdths, is_north)
107 }
108
109 pub(crate) fn parse_uncompressed(b: &[u8]) -> Result<(Self, Precision), AprsError> {
112 if b.len() != 8 || b[4] != b'.' {
113 return Err(AprsError::InvalidLatitude { raw: b.to_vec() });
114 }
115 let is_north = match b[7] {
116 b'N' => true,
117 b'S' => false,
118 _ => return Err(AprsError::InvalidLatitude { raw: b.to_vec() }),
119 };
120 let (deg, b0) = parse_pair_ambiguous(&[b[0], b[1]], false)
121 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
122 let (min, b1) = parse_pair_ambiguous(&[b[2], b[3]], b0 > 0)
123 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
124 let (hdths, b2) = parse_pair_ambiguous(&[b[5], b[6]], b1 > 0)
125 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
126 let blanks = b0 + b1 + b2;
127 let precision = Precision::from_blank_digits(blanks)
128 .ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
129 let value = deg as f64 + min as f64 / 60.0 + hdths as f64 / 6000.0;
130 let value = if is_north { value } else { -value };
131 let lat =
132 Latitude::new(value).ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
133 Ok((lat, precision))
134 }
135
136 pub(crate) fn parse_compressed(b: &[u8]) -> Result<Self, AprsError> {
138 let enc =
139 base91_decode4(b).ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })?;
140 let value = 90.0 - enc / 380926.0;
141 Latitude::new(value).ok_or_else(|| AprsError::InvalidLatitude { raw: b.to_vec() })
142 }
143
144 pub(crate) fn encode_uncompressed(&self, out: &mut Vec<u8>, precision: Precision) {
145 let (deg, min, hdths, is_north) = self.dmh();
146 let dir = if is_north { b'N' } else { b'S' };
147 let blanks = precision.num_blank_digits() as usize;
148 let mut digits = [0u8; 6];
150 let _ = write_digits_6(&mut digits, deg, min, hdths);
151 let end = 6usize.saturating_sub(blanks);
152 let mut buf = [b' '; 6];
153 buf[..end].copy_from_slice(&digits[..end]);
154 out.extend_from_slice(&buf[..4]);
155 out.push(b'.');
156 out.extend_from_slice(&buf[4..6]);
157 out.push(dir);
158 }
159
160 pub(crate) fn encode_compressed(&self, out: &mut Vec<u8>) {
161 let value = (90.0 - self.0) * 380926.0;
162 base91_encode4(value.round() as u32, out);
163 }
164}
165
166#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Default)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
169#[cfg_attr(feature = "serde", serde(transparent))]
170pub struct Longitude(f64);
171
172impl Deref for Longitude {
173 type Target = f64;
174 fn deref(&self) -> &f64 {
175 &self.0
176 }
177}
178
179impl Longitude {
180 pub fn new(value: f64) -> Option<Self> {
181 if value.is_nan() || !(-180.0..=180.0).contains(&value) {
182 None
183 } else {
184 Some(Self(value))
185 }
186 }
187
188 pub fn value(self) -> f64 {
189 self.0
190 }
191
192 pub fn dmh(self) -> (u32, u32, u32, bool) {
194 let (is_east, v) = if self.0 >= 0.0 {
195 (true, self.0)
196 } else {
197 (false, -self.0)
198 };
199 let deg = v as u32;
200 let min = ((v - deg as f64) * 60.0) as u32;
201 let mut hdths = ((v - deg as f64 - min as f64 / 60.0) * 6000.0).round() as u32;
202 let mut min = min;
203 let mut deg = deg;
204 if hdths >= 100 {
205 hdths = 0;
206 min += 1;
207 }
208 if min >= 60 {
209 min = 0;
210 deg += 1;
211 }
212 (deg, min, hdths, is_east)
213 }
214
215 pub(crate) fn parse_uncompressed(b: &[u8], precision: Precision) -> Result<Self, AprsError> {
218 if b.len() != 9 || b[5] != b'.' {
219 return Err(AprsError::InvalidLongitude { raw: b.to_vec() });
220 }
221 let is_east = match b[8] {
222 b'E' => true,
223 b'W' => false,
224 _ => return Err(AprsError::InvalidLongitude { raw: b.to_vec() }),
225 };
226 let mut digits = [0u8; 7];
228 digits[0..5].copy_from_slice(&b[0..5]);
229 digits[5..7].copy_from_slice(&b[6..8]);
230 let blanks = precision.num_blank_digits() as usize;
232 for d in digits.iter_mut().skip(7usize.saturating_sub(blanks)) {
233 *d = b'0';
234 }
235 let deg = parse_bytes::<u32>(&digits[0..3])
236 .ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })?;
237 let min = parse_bytes::<u32>(&digits[3..5])
238 .ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })?;
239 let hdths = parse_bytes::<u32>(&digits[5..7])
240 .ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })?;
241 let value = deg as f64 + min as f64 / 60.0 + hdths as f64 / 6000.0;
242 let value = if is_east { value } else { -value };
243 Longitude::new(value).ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })
244 }
245
246 pub(crate) fn parse_compressed(b: &[u8]) -> Result<Self, AprsError> {
248 let enc =
249 base91_decode4(b).ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })?;
250 let value = enc / 190463.0 - 180.0;
251 Longitude::new(value).ok_or_else(|| AprsError::InvalidLongitude { raw: b.to_vec() })
252 }
253
254 pub(crate) fn encode_uncompressed(&self, out: &mut Vec<u8>) {
255 let (deg, min, hdths, is_east) = self.dmh();
256 let dir = if is_east { b'E' } else { b'W' };
257 out.extend_from_slice(
258 format!("{:03}{:02}.{:02}{}", deg, min, hdths, dir as char).as_bytes(),
259 );
260 }
261
262 pub(crate) fn encode_compressed(&self, out: &mut Vec<u8>) {
263 let value = (180.0 + self.0) * 190463.0;
264 base91_encode4(value.round() as u32, out);
265 }
266}
267
268pub(crate) fn base91_decode4(b: &[u8]) -> Option<f64> {
271 if b.len() < 4 {
272 return None;
273 }
274 let mut val = 0.0f64;
275 for &byte in &b[..4] {
276 let d = byte.checked_sub(33)?;
277 if d > 90 {
278 return None;
279 }
280 val = val * 91.0 + d as f64;
281 }
282 Some(val)
283}
284
285pub(crate) fn base91_encode4(mut val: u32, out: &mut Vec<u8>) {
286 let mut buf = [33u8; 4]; for i in (0..4).rev() {
288 buf[i] = (val % 91) as u8 + 33;
289 val /= 91;
290 }
291 out.extend_from_slice(&buf);
292}
293
294pub(crate) fn base91_decode1(b: u8) -> Option<u8> {
295 b.checked_sub(33)
296}
297
298pub(crate) fn base91_encode1(v: u8) -> u8 {
299 v + 33
300}
301
302fn parse_pair_ambiguous(b: &[u8; 2], must_be_spaces: bool) -> Option<(u32, u8)> {
307 if must_be_spaces {
308 return if b == b" " { Some((0, 2)) } else { None };
309 }
310 match (b[0], b[1]) {
311 (b' ', b' ') => Some((0, 2)),
312 (d, b' ') if d.is_ascii_digit() => Some(((d - b'0') as u32 * 10, 1)),
313 (d0, d1) if d0.is_ascii_digit() && d1.is_ascii_digit() => {
314 Some(((d0 - b'0') as u32 * 10 + (d1 - b'0') as u32, 0))
315 }
316 _ => None,
317 }
318}
319
320fn write_digits_6(buf: &mut [u8; 6], deg: u32, min: u32, hdths: u32) -> Option<()> {
322 let s = format!("{:02}{:02}{:02}", deg, min, hdths);
323 if s.len() != 6 {
324 return None;
325 }
326 buf.copy_from_slice(s.as_bytes());
327 Some(())
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use approx::assert_relative_eq;
334
335 #[test]
336 fn lat_uncompressed_basic() {
337 let (lat, prec) = Latitude::parse_uncompressed(b"4903.50N").unwrap();
338 assert_relative_eq!(lat.value(), 49.05833333333333, epsilon = 1e-9);
339 assert_eq!(prec, Precision::HundredthMinute);
340 }
341
342 #[test]
343 fn lat_uncompressed_south() {
344 let (lat, _) = Latitude::parse_uncompressed(b"4903.50S").unwrap();
345 assert_relative_eq!(lat.value(), -49.05833333333333, epsilon = 1e-9);
346 }
347
348 #[test]
349 fn lat_ambiguity_one_tenth() {
350 let (lat, prec) = Latitude::parse_uncompressed(b"4903.5 N").unwrap();
351 assert_eq!(prec, Precision::TenthMinute);
352 assert_relative_eq!(lat.value(), 49.05833333333333, epsilon = 1e-4);
353 }
354
355 #[test]
356 fn lat_ambiguity_one_minute() {
357 let (lat, prec) = Latitude::parse_uncompressed(b"4903. N").unwrap();
358 assert_eq!(prec, Precision::OneMinute);
359 assert_relative_eq!(lat.value(), 49.05, epsilon = 1e-4);
360 }
361
362 #[test]
363 fn lat_invalid_direction() {
364 assert!(Latitude::parse_uncompressed(b"4903.50W").is_err());
365 }
366
367 #[test]
368 fn lat_out_of_range() {
369 assert!(Latitude::new(90.1).is_none());
370 assert!(Latitude::new(-90.1).is_none());
371 }
372
373 #[test]
374 fn lon_uncompressed_east() {
375 let lon = Longitude::parse_uncompressed(b"07201.75E", Precision::default()).unwrap();
376 assert_relative_eq!(lon.value(), 72.02916666666667, epsilon = 1e-9);
377 }
378
379 #[test]
380 fn lon_uncompressed_west() {
381 let lon = Longitude::parse_uncompressed(b"07201.75W", Precision::default()).unwrap();
382 assert_relative_eq!(lon.value(), -72.02916666666667, epsilon = 1e-9);
383 }
384
385 #[test]
386 fn lon_invalid_direction() {
387 assert!(Longitude::parse_uncompressed(b"07201.75N", Precision::default()).is_err());
388 }
389
390 #[test]
391 fn lat_encode_round_trip() {
392 let (lat, prec) = Latitude::parse_uncompressed(b"4903.50N").unwrap();
393 let mut out = Vec::new();
394 lat.encode_uncompressed(&mut out, prec);
395 assert_eq!(out, b"4903.50N");
396 }
397
398 #[test]
399 fn lon_encode_round_trip() {
400 let lon = Longitude::parse_uncompressed(b"07201.75W", Precision::default()).unwrap();
401 let mut out = Vec::new();
402 lon.encode_uncompressed(&mut out);
403 assert_eq!(out, b"07201.75W");
404 }
405
406 #[test]
407 fn compressed_lat_round_trip() {
408 let original = Latitude::new(49.05833).unwrap();
409 let mut enc = Vec::new();
410 original.encode_compressed(&mut enc);
411 assert_eq!(enc.len(), 4);
412 let decoded = Latitude::parse_compressed(&enc).unwrap();
413 assert_relative_eq!(decoded.value(), original.value(), epsilon = 0.001);
414 }
415
416 #[test]
417 fn compressed_lon_round_trip() {
418 let original = Longitude::new(-72.029).unwrap();
419 let mut enc = Vec::new();
420 original.encode_compressed(&mut enc);
421 assert_eq!(enc.len(), 4);
422 let decoded = Longitude::parse_compressed(&enc).unwrap();
423 assert_relative_eq!(decoded.value(), original.value(), epsilon = 0.001);
424 }
425
426 #[test]
427 fn base91_decode4_known() {
428 let val = base91_decode4(b"#$%^").unwrap();
430 assert_relative_eq!(val, 1532410.0, epsilon = 0.5);
431 }
432}