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
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use csv::WriterBuilder;
126    use std::io::stdout;
127
128    #[test]
129    fn parse_without_timestamp_or_messaging() {
130        let result = r"!4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
131        assert_eq!(result.timestamp, None);
132        assert_eq!(result.messaging_supported, false);
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, PositionComment::default());
138    }
139
140    #[test]
141    fn parse_with_comment() {
142        let result = r"!4903.50N/07201.75W-Hello/A=001000"
143            .parse::<AprsPosition>()
144            .unwrap();
145        assert_eq!(result.timestamp, None);
146        assert_relative_eq!(*result.latitude, 49.05833333333333);
147        assert_relative_eq!(*result.longitude, -72.02916666666667);
148        assert_eq!(result.symbol_table, '/');
149        assert_eq!(result.symbol_code, '-');
150        assert_eq!(result.comment.unparsed.unwrap(), "Hello/A=001000");
151    }
152
153    #[test]
154    fn parse_with_timestamp_without_messaging() {
155        let result = r"/074849h4821.61N\01224.49E^322/103/A=003054"
156            .parse::<AprsPosition>()
157            .unwrap();
158        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
159        assert_eq!(result.messaging_supported, false);
160        assert_relative_eq!(*result.latitude, 48.36016666666667);
161        assert_relative_eq!(*result.longitude, 12.408166666666666);
162        assert_eq!(result.symbol_table, '\\');
163        assert_eq!(result.symbol_code, '^');
164        assert_eq!(result.comment.altitude.unwrap(), 003054);
165        assert_eq!(result.comment.course.unwrap(), 322);
166        assert_eq!(result.comment.speed.unwrap(), 103);
167    }
168
169    #[test]
170    fn parse_without_timestamp_with_messaging() {
171        let result = r"=4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
172        assert_eq!(result.timestamp, None);
173        assert_eq!(result.messaging_supported, true);
174        assert_relative_eq!(*result.latitude, 49.05833333333333);
175        assert_relative_eq!(*result.longitude, -72.02916666666667);
176        assert_eq!(result.symbol_table, '/');
177        assert_eq!(result.symbol_code, '-');
178        assert_eq!(result.comment, PositionComment::default());
179    }
180
181    #[test]
182    fn parse_with_timestamp_and_messaging() {
183        let result = r"@074849h4821.61N\01224.49E^322/103/A=003054"
184            .parse::<AprsPosition>()
185            .unwrap();
186        assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
187        assert_eq!(result.messaging_supported, true);
188        assert_relative_eq!(*result.latitude, 48.36016666666667);
189        assert_relative_eq!(*result.longitude, 12.408166666666666);
190        assert_eq!(result.symbol_table, '\\');
191        assert_eq!(result.symbol_code, '^');
192        assert_eq!(result.comment.altitude.unwrap(), 003054);
193        assert_eq!(result.comment.course.unwrap(), 322);
194        assert_eq!(result.comment.speed.unwrap(), 103);
195    }
196
197    #[ignore = "position_comment serialization not implemented"]
198    #[test]
199    fn test_serialize() {
200        let aprs_position = r"@074849h4821.61N\01224.49E^322/103/A=003054"
201            .parse::<AprsPosition>()
202            .unwrap();
203        let mut wtr = WriterBuilder::new().from_writer(stdout());
204        wtr.serialize(aprs_position).unwrap();
205        wtr.flush().unwrap();
206    }
207
208    #[test]
209    fn test_input_string_too_short() {
210        let result = "/13244".parse::<AprsPosition>();
211        assert!(result.is_err(), "Short input string should return an error");
212    }
213}