1use crate::constants::{
40 WGS84_ECCENTRICITY_SQUARED, WGS84_SEMI_MAJOR_AXIS, WGS84_SEMI_MAJOR_AXIS_KM,
41};
42use crate::errors::{AstroError, AstroResult, MathErrorKind};
43
44use super::Location;
45
46impl Location {
47 pub fn to_geocentric_km(&self) -> AstroResult<(f64, f64)> {
77 let lat = self.latitude;
78 let height_km = self.height / 1000.0;
79
80 let (sin_lat, cos_lat) = libm::sincos(lat);
81
82 let denominator = 1.0 - WGS84_ECCENTRICITY_SQUARED * sin_lat * sin_lat;
83 if denominator <= f64::EPSILON {
84 return Err(AstroError::math_error(
85 "geocentric_conversion",
86 MathErrorKind::DivisionByZero,
87 "Latitude too close to critical value causing division by zero",
88 ));
89 }
90
91 let n = WGS84_SEMI_MAJOR_AXIS_KM / libm::sqrt(denominator);
92
93 let u = (n + height_km) * cos_lat;
94
95 let v = (n * (1.0 - WGS84_ECCENTRICITY_SQUARED) + height_km) * sin_lat;
96
97 Ok((u, v))
98 }
99
100 pub fn to_geocentric_meters(&self) -> AstroResult<(f64, f64)> {
129 let height_m = self.height;
130
131 let (phi_sin, phi_cos) = libm::sincos(self.latitude);
132
133 let wgs_flattened = 1.0 / 298.257223563;
134 let axis_ratio = 1.0 - wgs_flattened;
135 let axis_ratio_sq = axis_ratio * axis_ratio;
136
137 let norm_sq = phi_cos * phi_cos + axis_ratio_sq * phi_sin * phi_sin;
138 if norm_sq <= f64::EPSILON {
139 return Err(AstroError::math_error(
140 "geocentric_conversion",
141 MathErrorKind::DivisionByZero,
142 "Latitude too close to critical value causing division by zero",
143 ));
144 }
145
146 let prime_vertical_radius = WGS84_SEMI_MAJOR_AXIS / libm::sqrt(norm_sq);
147 let as_val = axis_ratio_sq * prime_vertical_radius;
148
149 let equatorial_radius = (prime_vertical_radius + height_m) * phi_cos;
150 let z_coordinate = (as_val + height_m) * phi_sin;
151
152 Ok((equatorial_radius, z_coordinate))
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_geocentric_at_equator() {
162 let loc = Location::from_degrees(0.0, 0.0, 0.0).unwrap();
163 let (u, v) = loc.to_geocentric_km().unwrap();
164
165 assert_eq!(
166 u,
167 crate::constants::WGS84_SEMI_MAJOR_AXIS_KM,
168 "u = {} km, expected ~6378.137 km",
169 u
170 );
171 assert_eq!(v, 0.0, "v = {} km, expected ~0 km", v);
172 }
173
174 #[test]
175 fn test_geocentric_at_north_pole() {
176 let loc = Location::from_degrees(90.0, 0.0, 0.0).unwrap();
177 let (u, v) = loc.to_geocentric_km().unwrap();
178
179 assert!(u.abs() < 1e-10, "u = {} km, expected very close to 0 km", u);
180 let expected_polar_radius =
181 crate::constants::WGS84_SEMI_MAJOR_AXIS_KM * (1.0 - 1.0 / 298.257223563);
182 assert_eq!(
183 v, expected_polar_radius,
184 "v = {} km, expected ~{} km",
185 v, expected_polar_radius
186 );
187 }
188
189 #[test]
190 fn test_geocentric_km_rejects_degenerate_latitude() {
191 use crate::constants::{HALF_PI, PI};
192 let critical_lat = HALF_PI - 1e-10;
193 let critical_lat_deg = critical_lat * 180.0 / PI;
194
195 let mut test_lat = critical_lat_deg;
196 while test_lat < 90.0 {
197 let loc = Location::from_degrees(test_lat, 0.0, 0.0).unwrap();
198 if loc.to_geocentric_km().is_err() {
199 return;
200 }
201 test_lat += 1e-12;
202 }
203 }
204
205 #[test]
206 fn test_geocentric_meters_rejects_degenerate_latitude() {
207 use crate::constants::{HALF_PI, PI};
208 let critical_lat = HALF_PI - 1e-10;
209 let critical_lat_deg = critical_lat * 180.0 / PI;
210
211 let mut test_lat = critical_lat_deg;
212 while test_lat < 90.0 {
213 let loc = Location::from_degrees(test_lat, 0.0, 0.0).unwrap();
214 if loc.to_geocentric_meters().is_err() {
215 return;
216 }
217 test_lat += 1e-12;
218 }
219 }
220
221 #[test]
222 fn test_geocentric_meters_handles_equator() {
223 let loc = Location::from_degrees(0.0, 0.0, 0.0).unwrap();
224 let (u, v) = loc.to_geocentric_meters().unwrap();
225 crate::test_helpers::assert_float_eq(u, WGS84_SEMI_MAJOR_AXIS, 1);
226 crate::test_helpers::assert_float_eq(v, 0.0, 1);
227 }
228
229 #[test]
230 fn test_geocentric_meters_handles_north_pole() {
231 let loc = Location::from_degrees(90.0, 0.0, 0.0).unwrap();
232 let (u, v) = loc.to_geocentric_meters().unwrap();
233 assert!(u.abs() < 1e-9);
234 crate::test_helpers::assert_ulp_le(v, 6356752.314245179, 1, "v at pole");
235 }
236
237 #[test]
238 fn test_geocentric_at_45_degrees() {
239 let loc = Location::from_degrees(45.0, 0.0, 0.0).unwrap();
240 let (u, v) = loc.to_geocentric_km().unwrap();
241
242 assert!(u > 4000.0 && u < 5000.0, "u = {} km, expected ~4500 km", u);
243 assert!(v > 4000.0 && v < 5000.0, "v = {} km, expected ~4500 km", v);
244
245 assert!(
246 u > v,
247 "u should be larger than v due to Earth's oblate shape: u={}, v={}",
248 u,
249 v
250 );
251 assert!(
252 (u - v).abs() < 100.0,
253 "At 45°, u and v should be similar: u={}, v={}",
254 u,
255 v
256 );
257 }
258
259 #[test]
260 fn test_geocentric_with_height() {
261 let loc_sea_level = Location::from_degrees(0.0, 0.0, 0.0).unwrap();
262 let loc_elevated = Location::from_degrees(0.0, 0.0, 1000.0).unwrap();
263
264 let (u1, v1) = loc_sea_level.to_geocentric_km().unwrap();
265 let (u2, v2) = loc_elevated.to_geocentric_km().unwrap();
266
267 assert!(
268 (u2 - u1 - 1.0).abs() < 0.001,
269 "1km elevation should increase u by ~1km"
270 );
271 assert!(
272 (v2 - v1).abs() < 0.001,
273 "At equator, elevation shouldn't affect v much"
274 );
275 }
276
277 #[test]
278 fn test_negative_latitude() {
279 let loc = Location::from_degrees(-45.0, 0.0, 0.0).unwrap();
280 let (u, v) = loc.to_geocentric_km().unwrap();
281
282 assert!(u > 0.0, "u should be positive: {}", u);
283 assert!(
284 v < 0.0,
285 "v should be negative in southern hemisphere: {}",
286 v
287 );
288 }
289
290 #[test]
291 fn test_geocentric_division_by_zero() {
292 let north_pole = Location {
293 latitude: crate::constants::HALF_PI,
294 longitude: 0.0,
295 height: 0.0,
296 };
297
298 let result = north_pole.to_geocentric_km();
299 assert!(result.is_ok());
300 }
301}