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