use crate::errors::{AstroError, AstroResult, MathErrorKind};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Location {
pub latitude: f64,
pub longitude: f64,
pub height: f64,
}
impl Location {
pub fn new(latitude: f64, longitude: f64, height: f64) -> AstroResult<Self> {
if !latitude.is_finite() {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Latitude must be finite",
));
}
if !longitude.is_finite() {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Longitude must be finite",
));
}
if !height.is_finite() {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Height must be finite",
));
}
if latitude.abs() > crate::constants::HALF_PI {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Latitude outside valid range [-π/2, π/2]",
));
}
if longitude.abs() > crate::constants::PI {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Longitude outside valid range [-π, π]",
));
}
if !(-12000.0..=100000.0).contains(&height) {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Height outside reasonable range [-12000, 100000] meters",
));
}
Ok(Self {
latitude,
longitude,
height,
})
}
pub fn from_degrees(lat_deg: f64, lon_deg: f64, height_m: f64) -> AstroResult<Self> {
if !lat_deg.is_finite() {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Latitude degrees must be finite",
));
}
if !lon_deg.is_finite() {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Longitude degrees must be finite",
));
}
if lat_deg.abs() > 90.0 {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Latitude outside valid range [-90, 90] degrees",
));
}
if lon_deg.abs() > 180.0 {
return Err(AstroError::math_error(
"location_validation",
MathErrorKind::InvalidInput,
"Longitude outside valid range [-180, 180] degrees",
));
}
Self::new(lat_deg.to_radians(), lon_deg.to_radians(), height_m)
}
pub fn latitude_degrees(&self) -> f64 {
self.latitude.to_degrees()
}
pub fn longitude_degrees(&self) -> f64 {
self.longitude.to_degrees()
}
pub fn latitude_angle(&self) -> crate::Angle {
crate::Angle::from_radians(self.latitude)
}
pub fn longitude_angle(&self) -> crate::Angle {
crate::Angle::from_radians(self.longitude)
}
pub fn greenwich() -> Self {
Self::from_degrees(0.0, 0.0, 0.0).expect("Greenwich coordinates should always be valid")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_location_creation() {
let loc = Location::new(0.5, 1.0, 100.0).unwrap();
assert_eq!(loc.latitude, 0.5);
assert_eq!(loc.longitude, 1.0);
assert_eq!(loc.height, 100.0);
}
#[test]
fn test_from_degrees() {
let loc = Location::from_degrees(45.0, 90.0, 1000.0).unwrap();
assert!((loc.latitude - 45.0_f64.to_radians()).abs() < 1e-15);
assert!((loc.longitude - 90.0_f64.to_radians()).abs() < 1e-15);
assert_eq!(loc.height, 1000.0);
}
#[test]
fn test_longitude_degrees_conversion_returns_degrees() {
let loc = Location::from_degrees(0.0, 180.0, 0.0).unwrap();
assert_eq!(loc.longitude_degrees(), 180.0);
}
#[test]
fn test_longitude_degrees_conversion_handles_negative() {
let loc = Location::from_degrees(0.0, -90.0, 0.0).unwrap();
assert_eq!(loc.longitude_degrees(), -90.0);
}
#[test]
fn test_longitude_angle_returns_angle_object() {
let loc = Location::from_degrees(0.0, 45.0, 0.0).unwrap();
let angle = loc.longitude_angle();
crate::test_helpers::assert_float_eq(angle.degrees(), 45.0, 1);
}
#[test]
fn test_longitude_angle_handles_wraparound() {
let loc = Location::from_degrees(0.0, -180.0, 0.0).unwrap();
let angle = loc.longitude_angle();
crate::test_helpers::assert_float_eq(angle.degrees(), -180.0, 1);
}
#[test]
fn test_location_validation_errors() {
let result = Location::new(f64::NAN, 0.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Latitude must be finite"));
let result = Location::new(0.0, f64::NAN, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Longitude must be finite"));
let result = Location::new(0.0, 0.0, f64::NAN);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Height must be finite"));
let result = Location::new(f64::INFINITY, 0.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Latitude must be finite"));
let result = Location::new(0.0, f64::INFINITY, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Longitude must be finite"));
let result = Location::new(0.0, 0.0, f64::INFINITY);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Height must be finite"));
let result = Location::new(crate::constants::PI, 0.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside valid range"));
let result = Location::new(-crate::constants::PI, 0.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside valid range"));
let result = Location::new(0.0, crate::constants::TWOPI, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside valid range"));
let result = Location::new(0.0, -crate::constants::TWOPI, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside valid range"));
let result = Location::new(0.0, 0.0, 200000.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside reasonable range"));
let result = Location::new(0.0, 0.0, -20000.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside reasonable range"));
}
#[test]
fn test_from_degrees_validation_errors() {
let result = Location::from_degrees(f64::NAN, 0.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Latitude degrees must be finite"));
let result = Location::from_degrees(0.0, f64::NAN, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Longitude degrees must be finite"));
let result = Location::from_degrees(95.0, 0.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside valid range [-90, 90]"));
let result = Location::from_degrees(-95.0, 0.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside valid range [-90, 90]"));
let result = Location::from_degrees(0.0, 185.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside valid range [-180, 180]"));
let result = Location::from_degrees(0.0, -185.0, 0.0);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("outside valid range [-180, 180]"));
}
}