use crate::error::AprsError;
use crate::types::lonlat::{Latitude, Longitude};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AprsGridLocator {
pub grid: Vec<u8>,
pub comment: Vec<u8>,
}
impl AprsGridLocator {
pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
let body = info.get(1..).unwrap_or_default();
let end = body.iter().position(|&c| c == b']')
.ok_or(AprsError::UnsupportedPositionFormat)?;
let grid = body[..end].to_vec();
if grid.len() != 4 && grid.len() != 6 {
return Err(AprsError::UnsupportedPositionFormat);
}
if !grid[0].is_ascii_uppercase()
|| !grid[1].is_ascii_uppercase()
|| !grid[2].is_ascii_digit()
|| !grid[3].is_ascii_digit()
{
return Err(AprsError::UnsupportedPositionFormat);
}
if grid.len() == 6 && (!grid[4].is_ascii_alphabetic() || !grid[5].is_ascii_alphabetic()) {
return Err(AprsError::UnsupportedPositionFormat);
}
let comment = body.get(end + 1..).unwrap_or_default().to_vec();
Ok(Self { grid, comment })
}
pub fn to_position(&self) -> Option<(Latitude, Longitude)> {
if self.grid.len() < 4 { return None; }
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 {
let ssl = (self.grid[4].to_ascii_lowercase() - b'a') as f64;
let ssa = (self.grid[5].to_ascii_lowercase() - b'a') as f64;
let lon = fl * 20.0 + sl * 2.0 + ssl * (2.0 / 24.0) + (1.0 / 24.0) - 180.0;
let lat = fa * 10.0 + sa * 1.0 + ssa * (1.0 / 24.0) + (0.5 / 24.0) - 90.0;
(lon, lat)
} else {
let lon = fl * 20.0 + sl * 2.0 + 1.0 - 180.0;
let lat = fa * 10.0 + sa * 1.0 + 0.5 - 90.0;
(lon, lat)
};
Some((Latitude::new(lat)?, Longitude::new(lon)?))
}
pub fn encode(&self) -> Vec<u8> {
let mut out = vec![b'['];
out.extend_from_slice(&self.grid);
out.push(b']');
out.extend_from_slice(&self.comment);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn parse_4char() {
let g = AprsGridLocator::parse(b"[IO91]").unwrap();
assert_eq!(g.grid, b"IO91");
assert!(g.comment.is_empty());
}
#[test]
fn parse_6char_with_comment() {
let g = AprsGridLocator::parse(b"[IO91SX]comment here").unwrap();
assert_eq!(g.grid, b"IO91SX");
assert_eq!(g.comment, b"comment here");
}
#[test]
fn parse_missing_bracket() {
assert!(AprsGridLocator::parse(b"[IO91SX").is_err());
}
#[test]
fn parse_bad_length() {
assert!(AprsGridLocator::parse(b"[IO9]").is_err());
}
#[test]
fn to_position_4char() {
let g = AprsGridLocator { grid: b"JO22".to_vec(), comment: vec![] };
let (lat, lon) = g.to_position().unwrap();
assert_relative_eq!(lat.value(), 52.5, epsilon = 0.01);
assert_relative_eq!(lon.value(), 5.0, epsilon = 0.01);
}
#[test]
fn to_position_6char() {
let g = AprsGridLocator { grid: b"FN31pr".to_vec(), comment: vec![] };
let (lat, lon) = g.to_position().unwrap();
assert!(lat.value() > 41.0 && lat.value() < 42.0);
assert!(lon.value() > -73.0 && lon.value() < -72.0);
}
#[test]
fn encode_round_trip() {
let raw = b"[IO91SX]Hello";
let g = AprsGridLocator::parse(raw).unwrap();
assert_eq!(g.encode().as_slice(), raw.as_slice());
}
}