1use crate::error::AprsError;
2use crate::types::lonlat::{Latitude, Longitude};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct AprsGridLocator {
14 pub grid: Vec<u8>,
16 pub comment: Vec<u8>,
17}
18
19impl AprsGridLocator {
20 pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
22 let body = info.get(1..).unwrap_or_default();
24
25 let end = body.iter().position(|&c| c == b']')
26 .ok_or(AprsError::UnsupportedPositionFormat)?;
27
28 let grid = body[..end].to_vec();
29
30 if grid.len() != 4 && grid.len() != 6 {
31 return Err(AprsError::UnsupportedPositionFormat);
32 }
33 if !grid[0].is_ascii_uppercase()
34 || !grid[1].is_ascii_uppercase()
35 || !grid[2].is_ascii_digit()
36 || !grid[3].is_ascii_digit()
37 {
38 return Err(AprsError::UnsupportedPositionFormat);
39 }
40 if grid.len() == 6 && (!grid[4].is_ascii_alphabetic() || !grid[5].is_ascii_alphabetic()) {
41 return Err(AprsError::UnsupportedPositionFormat);
42 }
43
44 let comment = body.get(end + 1..).unwrap_or_default().to_vec();
45 Ok(Self { grid, comment })
46 }
47
48 pub fn to_position(&self) -> Option<(Latitude, Longitude)> {
50 if self.grid.len() < 4 { return None; }
51
52 let fl = (self.grid[0] - b'A') as f64; let fa = (self.grid[1] - b'A') as f64; let sl = (self.grid[2] - b'0') as f64; let sa = (self.grid[3] - b'0') as f64; let (lon, lat) = if self.grid.len() >= 6 {
58 let ssl = (self.grid[4].to_ascii_lowercase() - b'a') as f64;
60 let ssa = (self.grid[5].to_ascii_lowercase() - b'a') as f64;
61 let lon = fl * 20.0 + sl * 2.0 + ssl * (2.0 / 24.0) + (1.0 / 24.0) - 180.0;
62 let lat = fa * 10.0 + sa * 1.0 + ssa * (1.0 / 24.0) + (0.5 / 24.0) - 90.0;
63 (lon, lat)
64 } else {
65 let lon = fl * 20.0 + sl * 2.0 + 1.0 - 180.0;
67 let lat = fa * 10.0 + sa * 1.0 + 0.5 - 90.0;
68 (lon, lat)
69 };
70
71 Some((Latitude::new(lat)?, Longitude::new(lon)?))
72 }
73
74 pub fn encode(&self) -> Vec<u8> {
75 let mut out = vec![b'['];
76 out.extend_from_slice(&self.grid);
77 out.push(b']');
78 out.extend_from_slice(&self.comment);
79 out
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use approx::assert_relative_eq;
87
88 #[test]
89 fn parse_4char() {
90 let g = AprsGridLocator::parse(b"[IO91]").unwrap();
91 assert_eq!(g.grid, b"IO91");
92 assert!(g.comment.is_empty());
93 }
94
95 #[test]
96 fn parse_6char_with_comment() {
97 let g = AprsGridLocator::parse(b"[IO91SX]comment here").unwrap();
98 assert_eq!(g.grid, b"IO91SX");
99 assert_eq!(g.comment, b"comment here");
100 }
101
102 #[test]
103 fn parse_missing_bracket() {
104 assert!(AprsGridLocator::parse(b"[IO91SX").is_err());
105 }
106
107 #[test]
108 fn parse_bad_length() {
109 assert!(AprsGridLocator::parse(b"[IO9]").is_err());
110 }
111
112 #[test]
113 fn to_position_4char() {
114 let g = AprsGridLocator { grid: b"JO22".to_vec(), comment: vec![] };
115 let (lat, lon) = g.to_position().unwrap();
116 assert_relative_eq!(lat.value(), 52.5, epsilon = 0.01);
118 assert_relative_eq!(lon.value(), 5.0, epsilon = 0.01);
119 }
120
121 #[test]
122 fn to_position_6char() {
123 let g = AprsGridLocator { grid: b"FN31pr".to_vec(), comment: vec![] };
124 let (lat, lon) = g.to_position().unwrap();
125 assert!(lat.value() > 41.0 && lat.value() < 42.0);
127 assert!(lon.value() > -73.0 && lon.value() < -72.0);
128 }
129
130 #[test]
131 fn encode_round_trip() {
132 let raw = b"[IO91SX]Hello";
133 let g = AprsGridLocator::parse(raw).unwrap();
134 assert_eq!(g.encode().as_slice(), raw.as_slice());
135 }
136}