1use crate::errors::{AstroError, AstroResult, MathErrorKind};
31
32#[cfg(feature = "serde")]
33use serde::{Deserialize, Serialize};
34
35#[derive(Debug, Clone, Copy, PartialEq)]
40#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41pub struct Location {
42 pub latitude: f64,
44 pub longitude: f64,
46 pub height: f64,
48}
49
50impl Location {
51 pub fn new(latitude: f64, longitude: f64, height: f64) -> AstroResult<Self> {
64 if !latitude.is_finite() {
65 return Err(AstroError::math_error(
66 "location_validation",
67 MathErrorKind::InvalidInput,
68 "Latitude must be finite",
69 ));
70 }
71 if !longitude.is_finite() {
72 return Err(AstroError::math_error(
73 "location_validation",
74 MathErrorKind::InvalidInput,
75 "Longitude must be finite",
76 ));
77 }
78 if !height.is_finite() {
79 return Err(AstroError::math_error(
80 "location_validation",
81 MathErrorKind::InvalidInput,
82 "Height must be finite",
83 ));
84 }
85
86 if latitude.abs() > crate::constants::HALF_PI {
87 return Err(AstroError::math_error(
88 "location_validation",
89 MathErrorKind::InvalidInput,
90 "Latitude outside valid range [-π/2, π/2]",
91 ));
92 }
93 if longitude.abs() > crate::constants::PI {
94 return Err(AstroError::math_error(
95 "location_validation",
96 MathErrorKind::InvalidInput,
97 "Longitude outside valid range [-π, π]",
98 ));
99 }
100 if !(-12000.0..=100000.0).contains(&height) {
101 return Err(AstroError::math_error(
102 "location_validation",
103 MathErrorKind::InvalidInput,
104 "Height outside reasonable range [-12000, 100000] meters",
105 ));
106 }
107
108 Ok(Self {
109 latitude,
110 longitude,
111 height,
112 })
113 }
114
115 pub fn from_degrees(lat_deg: f64, lon_deg: f64, height_m: f64) -> AstroResult<Self> {
136 if !lat_deg.is_finite() {
137 return Err(AstroError::math_error(
138 "location_validation",
139 MathErrorKind::InvalidInput,
140 "Latitude degrees must be finite",
141 ));
142 }
143 if !lon_deg.is_finite() {
144 return Err(AstroError::math_error(
145 "location_validation",
146 MathErrorKind::InvalidInput,
147 "Longitude degrees must be finite",
148 ));
149 }
150 if lat_deg.abs() > 90.0 {
151 return Err(AstroError::math_error(
152 "location_validation",
153 MathErrorKind::InvalidInput,
154 "Latitude outside valid range [-90, 90] degrees",
155 ));
156 }
157 if lon_deg.abs() > 180.0 {
158 return Err(AstroError::math_error(
159 "location_validation",
160 MathErrorKind::InvalidInput,
161 "Longitude outside valid range [-180, 180] degrees",
162 ));
163 }
164
165 Self::new(
166 lat_deg * crate::constants::DEG_TO_RAD,
167 lon_deg * crate::constants::DEG_TO_RAD,
168 height_m,
169 )
170 }
171
172 pub fn latitude_degrees(&self) -> f64 {
174 self.latitude * crate::constants::RAD_TO_DEG
175 }
176
177 pub fn longitude_degrees(&self) -> f64 {
179 self.longitude * crate::constants::RAD_TO_DEG
180 }
181
182 pub fn latitude_angle(&self) -> crate::Angle {
184 crate::Angle::from_radians(self.latitude)
185 }
186
187 pub fn longitude_angle(&self) -> crate::Angle {
189 crate::Angle::from_radians(self.longitude)
190 }
191
192 pub fn greenwich() -> Self {
196 Self::from_degrees(0.0, 0.0, 0.0).expect("Greenwich coordinates should always be valid")
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_location_creation() {
206 let loc = Location::new(0.5, 1.0, 100.0).unwrap();
207 assert_eq!(loc.latitude, 0.5);
208 assert_eq!(loc.longitude, 1.0);
209 assert_eq!(loc.height, 100.0);
210 }
211
212 #[test]
213 fn test_from_degrees() {
214 let loc = Location::from_degrees(45.0, 90.0, 1000.0).unwrap();
215 assert!((loc.latitude - 45.0_f64.to_radians()).abs() < 1e-15);
216 assert!((loc.longitude - 90.0_f64.to_radians()).abs() < 1e-15);
217 assert_eq!(loc.height, 1000.0);
218 }
219
220 #[test]
221 fn test_longitude_degrees_conversion_returns_degrees() {
222 let loc = Location::from_degrees(0.0, 180.0, 0.0).unwrap();
223 assert_eq!(loc.longitude_degrees(), 180.0);
224 }
225
226 #[test]
227 fn test_longitude_degrees_conversion_handles_negative() {
228 let loc = Location::from_degrees(0.0, -90.0, 0.0).unwrap();
229 assert_eq!(loc.longitude_degrees(), -90.0);
230 }
231
232 #[test]
233 fn test_longitude_angle_returns_angle_object() {
234 let loc = Location::from_degrees(0.0, 45.0, 0.0).unwrap();
235 let angle = loc.longitude_angle();
236 crate::test_helpers::assert_float_eq(angle.degrees(), 45.0, 1);
237 }
238
239 #[test]
240 fn test_longitude_angle_handles_wraparound() {
241 let loc = Location::from_degrees(0.0, -180.0, 0.0).unwrap();
242 let angle = loc.longitude_angle();
243 crate::test_helpers::assert_float_eq(angle.degrees(), -180.0, 1);
244 }
245
246 #[test]
247 fn test_location_validation_errors() {
248 let result = Location::new(f64::NAN, 0.0, 0.0);
249 assert!(result.is_err());
250 assert!(result
251 .unwrap_err()
252 .to_string()
253 .contains("Latitude must be finite"));
254
255 let result = Location::new(0.0, f64::NAN, 0.0);
256 assert!(result.is_err());
257 assert!(result
258 .unwrap_err()
259 .to_string()
260 .contains("Longitude must be finite"));
261
262 let result = Location::new(0.0, 0.0, f64::NAN);
263 assert!(result.is_err());
264 assert!(result
265 .unwrap_err()
266 .to_string()
267 .contains("Height must be finite"));
268
269 let result = Location::new(f64::INFINITY, 0.0, 0.0);
270 assert!(result.is_err());
271 assert!(result
272 .unwrap_err()
273 .to_string()
274 .contains("Latitude must be finite"));
275
276 let result = Location::new(0.0, f64::INFINITY, 0.0);
277 assert!(result.is_err());
278 assert!(result
279 .unwrap_err()
280 .to_string()
281 .contains("Longitude must be finite"));
282
283 let result = Location::new(0.0, 0.0, f64::INFINITY);
284 assert!(result.is_err());
285 assert!(result
286 .unwrap_err()
287 .to_string()
288 .contains("Height must be finite"));
289
290 let result = Location::new(crate::constants::PI, 0.0, 0.0);
291 assert!(result.is_err());
292 assert!(result
293 .unwrap_err()
294 .to_string()
295 .contains("outside valid range"));
296
297 let result = Location::new(-crate::constants::PI, 0.0, 0.0);
298 assert!(result.is_err());
299 assert!(result
300 .unwrap_err()
301 .to_string()
302 .contains("outside valid range"));
303
304 let result = Location::new(0.0, crate::constants::TWOPI, 0.0);
305 assert!(result.is_err());
306 assert!(result
307 .unwrap_err()
308 .to_string()
309 .contains("outside valid range"));
310
311 let result = Location::new(0.0, -crate::constants::TWOPI, 0.0);
312 assert!(result.is_err());
313 assert!(result
314 .unwrap_err()
315 .to_string()
316 .contains("outside valid range"));
317
318 let result = Location::new(0.0, 0.0, 200000.0);
319 assert!(result.is_err());
320 assert!(result
321 .unwrap_err()
322 .to_string()
323 .contains("outside reasonable range"));
324
325 let result = Location::new(0.0, 0.0, -20000.0);
326 assert!(result.is_err());
327 assert!(result
328 .unwrap_err()
329 .to_string()
330 .contains("outside reasonable range"));
331 }
332
333 #[test]
334 fn test_from_degrees_validation_errors() {
335 let result = Location::from_degrees(f64::NAN, 0.0, 0.0);
336 assert!(result.is_err());
337 assert!(result
338 .unwrap_err()
339 .to_string()
340 .contains("Latitude degrees must be finite"));
341
342 let result = Location::from_degrees(0.0, f64::NAN, 0.0);
343 assert!(result.is_err());
344 assert!(result
345 .unwrap_err()
346 .to_string()
347 .contains("Longitude degrees must be finite"));
348
349 let result = Location::from_degrees(95.0, 0.0, 0.0);
350 assert!(result.is_err());
351 assert!(result
352 .unwrap_err()
353 .to_string()
354 .contains("outside valid range [-90, 90]"));
355
356 let result = Location::from_degrees(-95.0, 0.0, 0.0);
357 assert!(result.is_err());
358 assert!(result
359 .unwrap_err()
360 .to_string()
361 .contains("outside valid range [-90, 90]"));
362
363 let result = Location::from_degrees(0.0, 185.0, 0.0);
364 assert!(result.is_err());
365 assert!(result
366 .unwrap_err()
367 .to_string()
368 .contains("outside valid range [-180, 180]"));
369
370 let result = Location::from_degrees(0.0, -185.0, 0.0);
371 assert!(result.is_err());
372 assert!(result
373 .unwrap_err()
374 .to_string()
375 .contains("outside valid range [-180, 180]"));
376 }
377}