1use std::fmt::Write;
2use std::str::FromStr;
3
4use flat_projection::FlatProjection;
5use serde::Serialize;
6
7use crate::AprsError;
8use crate::EncodeError;
9use crate::Timestamp;
10use crate::lonlat::{Latitude, Longitude, encode_latitude, encode_longitude};
11use crate::position_comment::PositionComment;
12
13pub struct Relation {
14 pub bearing: f64,
15 pub distance: f64,
16}
17
18#[derive(PartialEq, Debug, Clone, Serialize)]
19pub struct AprsPosition {
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub timestamp: Option<Timestamp>,
22 pub messaging_supported: bool,
23 pub latitude: Latitude,
24 pub longitude: Longitude,
25 pub symbol_table: char,
26 pub symbol_code: char,
27 #[serde(flatten)]
28 pub comment: PositionComment,
29}
30
31impl FromStr for AprsPosition {
32 type Err = AprsError;
33
34 fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
35 let messaging_supported = s.starts_with('=') || s.starts_with('@');
36 let has_timestamp = s.starts_with('@') || s.starts_with('/');
37
38 if (!has_timestamp && s.len() < 20) || (has_timestamp && s.len() < 27) {
40 return Err(AprsError::InvalidPosition(s.to_owned()));
41 };
42
43 let (timestamp, s) = if has_timestamp {
45 (Some(s[1..8].parse()?), &s[8..])
46 } else {
47 (None, &s[1..])
48 };
49
50 let is_uncompressed_position = s.chars().take(1).all(|c| c.is_numeric());
52 if !is_uncompressed_position {
53 return Err(AprsError::UnsupportedPositionFormat(s.to_owned()));
54 }
55
56 let mut latitude: Latitude = s[0..8].parse()?;
58 let mut longitude: Longitude = s[9..18].parse()?;
59
60 let symbol_table = s.chars().nth(8).unwrap();
61 let symbol_code = s.chars().nth(18).unwrap();
62
63 let comment = &s[19..s.len()];
64
65 let ogn = comment.parse::<PositionComment>().unwrap();
67
68 if let Some(additional_precision) = &ogn.additional_precision {
70 *latitude += latitude.signum() * additional_precision.lat as f64 / 60_000.;
71 *longitude += longitude.signum() * additional_precision.lon as f64 / 60_000.;
72 }
73
74 Ok(AprsPosition {
75 timestamp,
76 messaging_supported,
77 latitude,
78 longitude,
79 symbol_table,
80 symbol_code,
81 comment: ogn,
82 })
83 }
84}
85
86impl AprsPosition {
87 pub fn encode<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
88 let sym = match (self.timestamp.is_some(), self.messaging_supported) {
89 (true, true) => '@',
90 (true, false) => '/',
91 (false, true) => '=',
92 (false, false) => '!',
93 };
94
95 write!(buf, "{sym}")?;
96
97 if let Some(ts) = &self.timestamp {
98 write!(buf, "{ts}")?;
99 }
100
101 write!(
102 buf,
103 "{}{}{}{}{:#?}",
104 encode_latitude(self.latitude)?,
105 self.symbol_table,
106 encode_longitude(self.longitude)?,
107 self.symbol_code,
108 self.comment,
109 )?;
110
111 Ok(())
112 }
113
114 pub fn get_relation(&self, other: &Self) -> Relation {
115 let mean_longitude: f64 = (*self.longitude + *other.longitude) / 2.0;
116 let mean_latitude: f64 = (*self.latitude + *other.latitude) / 2.0;
117 let flat_projection = FlatProjection::new(mean_longitude, mean_latitude);
118
119 let p1 = flat_projection.project(*self.latitude, *self.longitude);
120 let p2 = flat_projection.project(*other.latitude, *other.longitude);
121
122 Relation {
123 bearing: (450.0 - p1.bearing(&p2)) % 360.0, distance: p1.distance(&p2) * 1000.0, }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use csv::WriterBuilder;
133 use std::io::stdout;
134
135 #[test]
136 fn parse_without_timestamp_or_messaging() {
137 let result = r"!4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
138 assert_eq!(result.timestamp, None);
139 assert_eq!(result.messaging_supported, false);
140 assert_relative_eq!(*result.latitude, 49.05833333333333);
141 assert_relative_eq!(*result.longitude, -72.02916666666667);
142 assert_eq!(result.symbol_table, '/');
143 assert_eq!(result.symbol_code, '-');
144 assert_eq!(result.comment, PositionComment::default());
145 }
146
147 #[test]
148 fn parse_with_comment() {
149 let result = r"!4903.50N/07201.75W-Hello/A=001000"
150 .parse::<AprsPosition>()
151 .unwrap();
152 assert_eq!(result.timestamp, None);
153 assert_relative_eq!(*result.latitude, 49.05833333333333);
154 assert_relative_eq!(*result.longitude, -72.02916666666667);
155 assert_eq!(result.symbol_table, '/');
156 assert_eq!(result.symbol_code, '-');
157 assert_eq!(result.comment.unparsed.unwrap(), "Hello/A=001000");
158 }
159
160 #[test]
161 fn parse_with_timestamp_without_messaging() {
162 let result = r"/074849h4821.61N\01224.49E^322/103/A=003054"
163 .parse::<AprsPosition>()
164 .unwrap();
165 assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
166 assert_eq!(result.messaging_supported, false);
167 assert_relative_eq!(*result.latitude, 48.36016666666667);
168 assert_relative_eq!(*result.longitude, 12.408166666666666);
169 assert_eq!(result.symbol_table, '\\');
170 assert_eq!(result.symbol_code, '^');
171 assert_eq!(result.comment.altitude.unwrap(), 003054);
172 assert_eq!(result.comment.course.unwrap(), 322);
173 assert_eq!(result.comment.speed.unwrap(), 103);
174 }
175
176 #[test]
177 fn parse_without_timestamp_with_messaging() {
178 let result = r"=4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
179 assert_eq!(result.timestamp, None);
180 assert_eq!(result.messaging_supported, true);
181 assert_relative_eq!(*result.latitude, 49.05833333333333);
182 assert_relative_eq!(*result.longitude, -72.02916666666667);
183 assert_eq!(result.symbol_table, '/');
184 assert_eq!(result.symbol_code, '-');
185 assert_eq!(result.comment, PositionComment::default());
186 }
187
188 #[test]
189 fn parse_with_timestamp_and_messaging() {
190 let result = r"@074849h4821.61N\01224.49E^322/103/A=003054"
191 .parse::<AprsPosition>()
192 .unwrap();
193 assert_eq!(result.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
194 assert_eq!(result.messaging_supported, true);
195 assert_relative_eq!(*result.latitude, 48.36016666666667);
196 assert_relative_eq!(*result.longitude, 12.408166666666666);
197 assert_eq!(result.symbol_table, '\\');
198 assert_eq!(result.symbol_code, '^');
199 assert_eq!(result.comment.altitude.unwrap(), 003054);
200 assert_eq!(result.comment.course.unwrap(), 322);
201 assert_eq!(result.comment.speed.unwrap(), 103);
202 }
203
204 #[test]
205 fn test_latitude_longitude() {
206 let result = r"/104337h5211.24N\00032.65W^124/081/A=004026 !W62!"
207 .parse::<AprsPosition>()
208 .unwrap();
209 assert_relative_eq!(*result.latitude, 52.18743333333334);
210 assert_relative_eq!(*result.longitude, -0.5442);
211 }
212
213 #[ignore = "position_comment serialization not implemented"]
214 #[test]
215 fn test_serialize() {
216 let aprs_position = r"@074849h4821.61N\01224.49E^322/103/A=003054"
217 .parse::<AprsPosition>()
218 .unwrap();
219 let mut wtr = WriterBuilder::new().from_writer(stdout());
220 wtr.serialize(aprs_position).unwrap();
221 wtr.flush().unwrap();
222 }
223
224 #[test]
225 fn test_input_string_with_timestamp_too_short() {
226 let result = r"/104337h5211.24N\00032.65W".parse::<AprsPosition>();
227 assert!(result.is_err(), "Short input string should return an error");
228 }
229
230 #[test]
231 fn test_input_string_without_timestamp_too_short() {
232 let result = r"!4903.50N/07201.75W".parse::<AprsPosition>();
233 assert!(result.is_err(), "Short input string should return an error");
234 }
235
236 #[test]
237 fn test_bearing() {
238 let receiver = r"!4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
239 let east = r"!4903.50N/07101.75W-".parse::<AprsPosition>().unwrap();
240 let north = r"!5004.50N/07201.75W-".parse::<AprsPosition>().unwrap();
241 let west = r"!4903.50N/07301.75W-".parse::<AprsPosition>().unwrap();
242 let south = r"!4803.50N/07201.75W-".parse::<AprsPosition>().unwrap();
243
244 let to_north = receiver.get_relation(&north);
245 assert_eq!(to_north.bearing, 0.0);
246
247 let to_east = receiver.get_relation(&east);
248 assert_eq!(to_east.bearing, 90.0);
249
250 let to_south = receiver.get_relation(&south);
251 assert_eq!(to_south.bearing, 180.0);
252
253 let to_west = receiver.get_relation(&west);
254 assert_eq!(to_west.bearing, 270.0);
255 }
256}