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 if (!has_timestamp && s.len() < 19) || (has_timestamp && s.len() < 26) {
34 return Err(AprsError::InvalidPosition(s.to_owned()));
35 };
36
37 let (timestamp, s) = if has_timestamp {
39 (Some(s[1..8].parse()?), &s[8..])
40 } else {
41 (None, &s[1..])
42 };
43
44 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 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 let ogn = comment.parse::<PositionComment>().unwrap();
61
62 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}