use std::fmt::Write;
use std::str::FromStr;
use flat_projection::FlatProjection;
use serde::Serialize;
use crate::AprsError;
use crate::EncodeError;
use crate::Timestamp;
use crate::lonlat::{Latitude, Longitude, encode_latitude, encode_longitude};
use crate::position_comment::PositionComment;
pub struct Relation {
pub bearing: f64,
pub distance: f64,
}
#[derive(PartialEq, Debug, Clone, Serialize)]
pub struct AprsPosition {
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<Timestamp>,
pub messaging_supported: bool,
pub latitude: Latitude,
pub longitude: Longitude,
pub symbol_table: char,
pub symbol_code: char,
#[serde(flatten)]
pub comment: PositionComment,
}
impl FromStr for AprsPosition {
type Err = AprsError;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
let messaging_supported = s.starts_with('=') || s.starts_with('@');
let has_timestamp = s.starts_with('@') || s.starts_with('/');
if (!has_timestamp && s.len() < 20) || (has_timestamp && s.len() < 27) {
return Err(AprsError::InvalidPosition(s.to_owned()));
};
let (timestamp, s) = if has_timestamp {
(Some(s[1..8].parse()?), &s[8..])
} else {
(None, &s[1..])
};
let is_uncompressed_position = s.chars().take(1).all(|c| c.is_numeric());
if !is_uncompressed_position {
return Err(AprsError::UnsupportedPositionFormat(s.to_owned()));
}
let mut latitude: Latitude = s[0..8].parse()?;
let mut longitude: Longitude = s[9..18].parse()?;
let symbol_table = s.chars().nth(8).unwrap();
let symbol_code = s.chars().nth(18).unwrap();
let comment = &s[19..s.len()];
let ogn = comment.parse::<PositionComment>().unwrap();
if let Some(additional_precision) = &ogn.additional_precision {
*latitude += latitude.signum() * additional_precision.lat as f64 / 60_000.;
*longitude += longitude.signum() * additional_precision.lon as f64 / 60_000.;
}
Ok(AprsPosition {
timestamp,
messaging_supported,
latitude,
longitude,
symbol_table,
symbol_code,
comment: ogn,
})
}
}
impl AprsPosition {
pub fn encode<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
let sym = match (self.timestamp.is_some(), self.messaging_supported) {
(true, true) => '@',
(true, false) => '/',
(false, true) => '=',
(false, false) => '!',
};
write!(buf, "{sym}")?;
if let Some(ts) = &self.timestamp {
write!(buf, "{ts}")?;
}
write!(
buf,
"{}{}{}{}{:#?}",
encode_latitude(self.latitude)?,
self.symbol_table,
encode_longitude(self.longitude)?,
self.symbol_code,
self.comment,
)?;
Ok(())
}
pub fn get_relation(&self, other: &Self) -> Relation {
let mean_longitude: f64 = (*self.longitude + *other.longitude) / 2.0;
let mean_latitude: f64 = (*self.latitude + *other.latitude) / 2.0;
let flat_projection = FlatProjection::new(mean_longitude, mean_latitude);
let p1 = flat_projection.project(*self.latitude, *self.longitude);
let p2 = flat_projection.project(*other.latitude, *other.longitude);
Relation {
bearing: (450.0 - p1.bearing(&p2)) % 360.0, distance: p1.distance(&p2) * 1000.0, }
}
}
#[cfg(test)]
mod tests {
use super::*;
use csv::WriterBuilder;
use std::io::stdout;
#[test]
fn parse_without_timestamp_or_messaging() {
let result = r"!4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
assert_eq!(result.timestamp, None);
assert_eq!(result.messaging_supported, false);
assert_relative_eq!(*result.latitude, 49.05833333333333);
assert_relative_eq!(*result.longitude, -72.02916666666667);
assert_eq!(result.symbol_table, '/');
assert_eq!(result.symbol_code, '-');
assert_eq!(result.comment, PositionComment::default());
}
#[test]
fn parse_with_comment() {
let result = r"!4903.50N/07201.75W-Hello/A=001000"
.parse::<AprsPosition>()
.unwrap();
assert_eq!(result.timestamp, None);
assert_relative_eq!(*result.latitude, 49.05833333333333);
assert_relative_eq!(*result.longitude, -72.02916666666667);
assert_eq!(result.symbol_table, '/');
assert_eq!(result.symbol_code, '-');
assert_eq!(result.comment.unparsed.unwrap(), "Hello/A=001000");
}
#[test]
fn parse_with_timestamp_without_messaging() {
let result = r"/074849h4821.61N\01224.49E^322/103/A=003054"
.parse::<AprsPosition>()
.unwrap();
assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
assert_eq!(result.messaging_supported, false);
assert_relative_eq!(*result.latitude, 48.36016666666667);
assert_relative_eq!(*result.longitude, 12.408166666666666);
assert_eq!(result.symbol_table, '\\');
assert_eq!(result.symbol_code, '^');
assert_eq!(result.comment.altitude.unwrap(), 003054);
assert_eq!(result.comment.course.unwrap(), 322);
assert_eq!(result.comment.speed.unwrap(), 103);
}
#[test]
fn parse_without_timestamp_with_messaging() {
let result = r"=4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
assert_eq!(result.timestamp, None);
assert_eq!(result.messaging_supported, true);
assert_relative_eq!(*result.latitude, 49.05833333333333);
assert_relative_eq!(*result.longitude, -72.02916666666667);
assert_eq!(result.symbol_table, '/');
assert_eq!(result.symbol_code, '-');
assert_eq!(result.comment, PositionComment::default());
}
#[test]
fn parse_with_timestamp_and_messaging() {
let result = r"@074849h4821.61N\01224.49E^322/103/A=003054"
.parse::<AprsPosition>()
.unwrap();
assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
assert_eq!(result.messaging_supported, true);
assert_relative_eq!(*result.latitude, 48.36016666666667);
assert_relative_eq!(*result.longitude, 12.408166666666666);
assert_eq!(result.symbol_table, '\\');
assert_eq!(result.symbol_code, '^');
assert_eq!(result.comment.altitude.unwrap(), 003054);
assert_eq!(result.comment.course.unwrap(), 322);
assert_eq!(result.comment.speed.unwrap(), 103);
}
#[test]
fn test_latitude_longitude() {
let result = r"/104337h5211.24N\00032.65W^124/081/A=004026 !W62!"
.parse::<AprsPosition>()
.unwrap();
assert_relative_eq!(*result.latitude, 52.18743333333334);
assert_relative_eq!(*result.longitude, -0.5442);
}
#[ignore = "position_comment serialization not implemented"]
#[test]
fn test_serialize() {
let aprs_position = r"@074849h4821.61N\01224.49E^322/103/A=003054"
.parse::<AprsPosition>()
.unwrap();
let mut wtr = WriterBuilder::new().from_writer(stdout());
wtr.serialize(aprs_position).unwrap();
wtr.flush().unwrap();
}
#[test]
fn test_input_string_with_timestamp_too_short() {
let result = r"/104337h5211.24N\00032.65W".parse::<AprsPosition>();
assert!(result.is_err(), "Short input string should return an error");
}
#[test]
fn test_input_string_without_timestamp_too_short() {
let result = r"!4903.50N/07201.75W".parse::<AprsPosition>();
assert!(result.is_err(), "Short input string should return an error");
}
#[test]
fn test_bearing() {
let receiver = r"!4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
let east = r"!4903.50N/07101.75W-".parse::<AprsPosition>().unwrap();
let north = r"!5004.50N/07201.75W-".parse::<AprsPosition>().unwrap();
let west = r"!4903.50N/07301.75W-".parse::<AprsPosition>().unwrap();
let south = r"!4803.50N/07201.75W-".parse::<AprsPosition>().unwrap();
let to_north = receiver.get_relation(&north);
assert_eq!(to_north.bearing, 0.0);
let to_east = receiver.get_relation(&east);
assert_eq!(to_east.bearing, 90.0);
let to_south = receiver.get_relation(&south);
assert_eq!(to_south.bearing, 180.0);
let to_west = receiver.get_relation(&west);
assert_eq!(to_west.bearing, 270.0);
}
}