ogn_parser/
position.rs

1use std::fmt::Write;
2use std::str::FromStr;
3
4use flat_projection::FlatPoint;
5use flat_projection::FlatProjection;
6use serde::Serialize;
7
8use crate::AprsError;
9use crate::EncodeError;
10use crate::Timestamp;
11use crate::lonlat::{Latitude, Longitude, encode_latitude, encode_longitude};
12use crate::position_comment::PositionComment;
13
14#[derive(PartialEq, Debug, Clone, Serialize)]
15pub struct AprsPosition {
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub timestamp: Option<Timestamp>,
18    pub messaging_supported: bool,
19    pub latitude: Latitude,
20    pub longitude: Longitude,
21    pub symbol_table: char,
22    pub symbol_code: char,
23    #[serde(flatten)]
24    pub comment: PositionComment,
25
26    #[serde(skip_serializing)]
27    pub flat_projection: FlatProjection<f64>,
28    #[serde(skip_serializing)]
29    pub flat_point: FlatPoint<f64>,
30}
31
32impl FromStr for AprsPosition {
33    type Err = AprsError;
34
35    fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
36        let messaging_supported = s.starts_with('=') || s.starts_with('@');
37        let has_timestamp = s.starts_with('@') || s.starts_with('/');
38
39        // check for minimal message length
40        if (!has_timestamp && s.len() < 19) || (has_timestamp && s.len() < 26) {
41            return Err(AprsError::InvalidPosition(s.to_owned()));
42        };
43
44        // Extract timestamp and remaining string
45        let (timestamp, s) = if has_timestamp {
46            (Some(s[1..8].parse()?), &s[8..])
47        } else {
48            (None, &s[1..])
49        };
50
51        // check for compressed position format
52        let is_uncompressed_position = s.chars().take(1).all(|c| c.is_numeric());
53        if !is_uncompressed_position {
54            return Err(AprsError::UnsupportedPositionFormat(s.to_owned()));
55        }
56
57        // parse position
58        let mut latitude: Latitude = s[0..8].parse()?;
59        let mut longitude: Longitude = s[9..18].parse()?;
60
61        let symbol_table = s.chars().nth(8).unwrap();
62        let symbol_code = s.chars().nth(18).unwrap();
63
64        let comment = &s[19..s.len()];
65
66        // parse the comment
67        let ogn = comment.parse::<PositionComment>().unwrap();
68
69        // The comment may contain additional position precision information that will be added to the current position
70        if let Some(precision) = &ogn.additional_precision {
71            *latitude += precision.lat as f64 / 60_000.;
72            *longitude += precision.lon as f64 / 60_000.;
73        }
74
75        // For fast distance calculations, we need to create a flat projection of the position
76        let flat_projection = FlatProjection::new(*longitude, *latitude);
77        let flat_point = flat_projection.project(*longitude, *latitude);
78
79        Ok(AprsPosition {
80            timestamp,
81            messaging_supported,
82            latitude,
83            longitude,
84            symbol_table,
85            symbol_code,
86            comment: ogn,
87            flat_projection,
88            flat_point,
89        })
90    }
91}
92
93impl AprsPosition {
94    pub fn encode<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
95        let sym = match (self.timestamp.is_some(), self.messaging_supported) {
96            (true, true) => '@',
97            (true, false) => '/',
98            (false, true) => '=',
99            (false, false) => '!',
100        };
101
102        write!(buf, "{}", sym)?;
103
104        if let Some(ts) = &self.timestamp {
105            write!(buf, "{}", ts)?;
106        }
107
108        write!(
109            buf,
110            "{}{}{}{}{:#?}",
111            encode_latitude(self.latitude)?,
112            self.symbol_table,
113            encode_longitude(self.longitude)?,
114            self.symbol_code,
115            self.comment,
116        )?;
117
118        Ok(())
119    }
120
121    pub fn get_bearing(&self, other: &Self) -> f64 {
122        self.flat_point.bearing(&other.flat_point)
123    }
124
125    pub fn get_distance(&self, other: &Self) -> f64 {
126        self.flat_point.distance(&other.flat_point)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use csv::WriterBuilder;
134    use std::io::stdout;
135
136    #[test]
137    fn parse_without_timestamp_or_messaging() {
138        let result = r"!4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
139        assert_eq!(result.timestamp, None);
140        assert_eq!(result.messaging_supported, false);
141        assert_relative_eq!(*result.latitude, 49.05833333333333);
142        assert_relative_eq!(*result.longitude, -72.02916666666667);
143        assert_eq!(result.symbol_table, '/');
144        assert_eq!(result.symbol_code, '-');
145        assert_eq!(result.comment, PositionComment::default());
146    }
147
148    #[test]
149    fn parse_with_comment() {
150        let result = r"!4903.50N/07201.75W-Hello/A=001000"
151            .parse::<AprsPosition>()
152            .unwrap();
153        assert_eq!(result.timestamp, None);
154        assert_relative_eq!(*result.latitude, 49.05833333333333);
155        assert_relative_eq!(*result.longitude, -72.02916666666667);
156        assert_eq!(result.symbol_table, '/');
157        assert_eq!(result.symbol_code, '-');
158        assert_eq!(result.comment.unparsed.unwrap(), "Hello/A=001000");
159    }
160
161    #[test]
162    fn parse_with_timestamp_without_messaging() {
163        let result = r"/074849h4821.61N\01224.49E^322/103/A=003054"
164            .parse::<AprsPosition>()
165            .unwrap();
166        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
167        assert_eq!(result.messaging_supported, false);
168        assert_relative_eq!(*result.latitude, 48.36016666666667);
169        assert_relative_eq!(*result.longitude, 12.408166666666666);
170        assert_eq!(result.symbol_table, '\\');
171        assert_eq!(result.symbol_code, '^');
172        assert_eq!(result.comment.altitude.unwrap(), 003054);
173        assert_eq!(result.comment.course.unwrap(), 322);
174        assert_eq!(result.comment.speed.unwrap(), 103);
175    }
176
177    #[test]
178    fn parse_without_timestamp_with_messaging() {
179        let result = r"=4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
180        assert_eq!(result.timestamp, None);
181        assert_eq!(result.messaging_supported, true);
182        assert_relative_eq!(*result.latitude, 49.05833333333333);
183        assert_relative_eq!(*result.longitude, -72.02916666666667);
184        assert_eq!(result.symbol_table, '/');
185        assert_eq!(result.symbol_code, '-');
186        assert_eq!(result.comment, PositionComment::default());
187    }
188
189    #[test]
190    fn parse_with_timestamp_and_messaging() {
191        let result = r"@074849h4821.61N\01224.49E^322/103/A=003054"
192            .parse::<AprsPosition>()
193            .unwrap();
194        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
195        assert_eq!(result.messaging_supported, true);
196        assert_relative_eq!(*result.latitude, 48.36016666666667);
197        assert_relative_eq!(*result.longitude, 12.408166666666666);
198        assert_eq!(result.symbol_table, '\\');
199        assert_eq!(result.symbol_code, '^');
200        assert_eq!(result.comment.altitude.unwrap(), 003054);
201        assert_eq!(result.comment.course.unwrap(), 322);
202        assert_eq!(result.comment.speed.unwrap(), 103);
203    }
204
205    #[ignore = "position_comment serialization not implemented"]
206    #[test]
207    fn test_serialize() {
208        let aprs_position = r"@074849h4821.61N\01224.49E^322/103/A=003054"
209            .parse::<AprsPosition>()
210            .unwrap();
211        let mut wtr = WriterBuilder::new().from_writer(stdout());
212        wtr.serialize(aprs_position).unwrap();
213        wtr.flush().unwrap();
214    }
215
216    #[test]
217    fn test_input_string_too_short() {
218        let result = "/13244".parse::<AprsPosition>();
219        assert!(result.is_err(), "Short input string should return an error");
220    }
221}