ogn_parser/
position.rs

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