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(lat_deg.to_radians(), lon_deg.to_radians(), height_m)
166 }
167
168 pub fn latitude_degrees(&self) -> f64 {
170 self.latitude.to_degrees()
171 }
172
173 pub fn longitude_degrees(&self) -> f64 {
175 self.longitude.to_degrees()
176 }
177
178 pub fn latitude_angle(&self) -> crate::Angle {
180 crate::Angle::from_radians(self.latitude)
181 }
182
183 pub fn longitude_angle(&self) -> crate::Angle {
185 crate::Angle::from_radians(self.longitude)
186 }
187
188 pub fn greenwich() -> Self {
192 Self::from_degrees(0.0, 0.0, 0.0).expect("Greenwich coordinates should always be valid")
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_location_creation() {
202 let loc = Location::new(0.5, 1.0, 100.0).unwrap();
203 assert_eq!(loc.latitude, 0.5);
204 assert_eq!(loc.longitude, 1.0);
205 assert_eq!(loc.height, 100.0);
206 }
207
208 #[test]
209 fn test_from_degrees() {
210 let loc = Location::from_degrees(45.0, 90.0, 1000.0).unwrap();
211 assert!((loc.latitude - 45.0_f64.to_radians()).abs() < 1e-15);
212 assert!((loc.longitude - 90.0_f64.to_radians()).abs() < 1e-15);
213 assert_eq!(loc.height, 1000.0);
214 }
215
216 #[test]
217 fn test_longitude_degrees_conversion_returns_degrees() {
218 let loc = Location::from_degrees(0.0, 180.0, 0.0).unwrap();
219 assert_eq!(loc.longitude_degrees(), 180.0);
220 }
221
222 #[test]
223 fn test_longitude_degrees_conversion_handles_negative() {
224 let loc = Location::from_degrees(0.0, -90.0, 0.0).unwrap();
225 assert_eq!(loc.longitude_degrees(), -90.0);
226 }
227
228 #[test]
229 fn test_longitude_angle_returns_angle_object() {
230 let loc = Location::from_degrees(0.0, 45.0, 0.0).unwrap();
231 let angle = loc.longitude_angle();
232 crate::test_helpers::assert_float_eq(angle.degrees(), 45.0, 1);
233 }
234
235 #[test]
236 fn test_longitude_angle_handles_wraparound() {
237 let loc = Location::from_degrees(0.0, -180.0, 0.0).unwrap();
238 let angle = loc.longitude_angle();
239 crate::test_helpers::assert_float_eq(angle.degrees(), -180.0, 1);
240 }
241
242 #[test]
243 fn test_location_validation_errors() {
244 let result = Location::new(f64::NAN, 0.0, 0.0);
245 assert!(result.is_err());
246 assert!(result
247 .unwrap_err()
248 .to_string()
249 .contains("Latitude must be finite"));
250
251 let result = Location::new(0.0, f64::NAN, 0.0);
252 assert!(result.is_err());
253 assert!(result
254 .unwrap_err()
255 .to_string()
256 .contains("Longitude must be finite"));
257
258 let result = Location::new(0.0, 0.0, f64::NAN);
259 assert!(result.is_err());
260 assert!(result
261 .unwrap_err()
262 .to_string()
263 .contains("Height must be finite"));
264
265 let result = Location::new(f64::INFINITY, 0.0, 0.0);
266 assert!(result.is_err());
267 assert!(result
268 .unwrap_err()
269 .to_string()
270 .contains("Latitude must be finite"));
271
272 let result = Location::new(0.0, f64::INFINITY, 0.0);
273 assert!(result.is_err());
274 assert!(result
275 .unwrap_err()
276 .to_string()
277 .contains("Longitude must be finite"));
278
279 let result = Location::new(0.0, 0.0, f64::INFINITY);
280 assert!(result.is_err());
281 assert!(result
282 .unwrap_err()
283 .to_string()
284 .contains("Height must be finite"));
285
286 let result = Location::new(crate::constants::PI, 0.0, 0.0);
287 assert!(result.is_err());
288 assert!(result
289 .unwrap_err()
290 .to_string()
291 .contains("outside valid range"));
292
293 let result = Location::new(-crate::constants::PI, 0.0, 0.0);
294 assert!(result.is_err());
295 assert!(result
296 .unwrap_err()
297 .to_string()
298 .contains("outside valid range"));
299
300 let result = Location::new(0.0, crate::constants::TWOPI, 0.0);
301 assert!(result.is_err());
302 assert!(result
303 .unwrap_err()
304 .to_string()
305 .contains("outside valid range"));
306
307 let result = Location::new(0.0, -crate::constants::TWOPI, 0.0);
308 assert!(result.is_err());
309 assert!(result
310 .unwrap_err()
311 .to_string()
312 .contains("outside valid range"));
313
314 let result = Location::new(0.0, 0.0, 200000.0);
315 assert!(result.is_err());
316 assert!(result
317 .unwrap_err()
318 .to_string()
319 .contains("outside reasonable range"));
320
321 let result = Location::new(0.0, 0.0, -20000.0);
322 assert!(result.is_err());
323 assert!(result
324 .unwrap_err()
325 .to_string()
326 .contains("outside reasonable range"));
327 }
328
329 #[test]
330 fn test_from_degrees_validation_errors() {
331 let result = Location::from_degrees(f64::NAN, 0.0, 0.0);
332 assert!(result.is_err());
333 assert!(result
334 .unwrap_err()
335 .to_string()
336 .contains("Latitude degrees must be finite"));
337
338 let result = Location::from_degrees(0.0, f64::NAN, 0.0);
339 assert!(result.is_err());
340 assert!(result
341 .unwrap_err()
342 .to_string()
343 .contains("Longitude degrees must be finite"));
344
345 let result = Location::from_degrees(95.0, 0.0, 0.0);
346 assert!(result.is_err());
347 assert!(result
348 .unwrap_err()
349 .to_string()
350 .contains("outside valid range [-90, 90]"));
351
352 let result = Location::from_degrees(-95.0, 0.0, 0.0);
353 assert!(result.is_err());
354 assert!(result
355 .unwrap_err()
356 .to_string()
357 .contains("outside valid range [-90, 90]"));
358
359 let result = Location::from_degrees(0.0, 185.0, 0.0);
360 assert!(result.is_err());
361 assert!(result
362 .unwrap_err()
363 .to_string()
364 .contains("outside valid range [-180, 180]"));
365
366 let result = Location::from_degrees(0.0, -185.0, 0.0);
367 assert!(result.is_err());
368 assert!(result
369 .unwrap_err()
370 .to_string()
371 .contains("outside valid range [-180, 180]"));
372 }
373}