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() < 19) || (has_timestamp && s.len() < 26) {
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(precision) = &ogn.additional_precision {
70 *latitude += precision.lat as f64 / 60_000.;
71 *longitude += 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 #[ignore = "position_comment serialization not implemented"]
205 #[test]
206 fn test_serialize() {
207 let aprs_position = r"@074849h4821.61N\01224.49E^322/103/A=003054"
208 .parse::<AprsPosition>()
209 .unwrap();
210 let mut wtr = WriterBuilder::new().from_writer(stdout());
211 wtr.serialize(aprs_position).unwrap();
212 wtr.flush().unwrap();
213 }
214
215 #[test]
216 fn test_input_string_too_short() {
217 let result = "/13244".parse::<AprsPosition>();
218 assert!(result.is_err(), "Short input string should return an error");
219 }
220
221 #[test]
222 fn test_bearing() {
223 let receiver = r"!4903.50N/07201.75W-".parse::<AprsPosition>().unwrap();
224 let east = r"!4903.50N/07101.75W-".parse::<AprsPosition>().unwrap();
225 let north = r"!5004.50N/07201.75W-".parse::<AprsPosition>().unwrap();
226 let west = r"!4903.50N/07301.75W-".parse::<AprsPosition>().unwrap();
227 let south = r"!4803.50N/07201.75W-".parse::<AprsPosition>().unwrap();
228
229 let to_north = receiver.get_relation(&north);
230 assert_eq!(to_north.bearing, 0.0);
231
232 let to_east = receiver.get_relation(&east);
233 assert_eq!(to_east.bearing, 90.0);
234
235 let to_south = receiver.get_relation(&south);
236 assert_eq!(to_south.bearing, 180.0);
237
238 let to_west = receiver.get_relation(&west);
239 assert_eq!(to_west.bearing, 270.0);
240 }
241}