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 if (!has_timestamp && s.len() < 19) || (has_timestamp && s.len() < 26) {
41 return Err(AprsError::InvalidPosition(s.to_owned()));
42 };
43
44 let (timestamp, s) = if has_timestamp {
46 (Some(s[1..8].parse()?), &s[8..])
47 } else {
48 (None, &s[1..])
49 };
50
51 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 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 let ogn = comment.parse::<PositionComment>().unwrap();
68
69 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 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}