use super::angle::SiderealAngle;
use super::gast::GAST;
use super::gmst::GMST;
use super::lmst::LMST;
use crate::scales::{TT, UT1};
use crate::transforms::nutation::NutationCalculator;
use crate::TimeResult;
use celestial_core::angle::wrap_0_2pi;
use celestial_core::Location;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LAST {
angle: SiderealAngle,
location: Location,
}
impl LAST {
pub fn from_ut1_tt_and_location(ut1: &UT1, tt: &TT, location: &Location) -> TimeResult<Self> {
let gast = GAST::from_ut1_and_tt(ut1, tt)?;
let last_rad = gast.radians() + location.longitude;
let last_normalized = wrap_0_2pi(last_rad);
let angle = SiderealAngle::from_radians_exact(last_normalized);
Ok(Self {
angle,
location: *location,
})
}
pub fn from_lmst_and_equation_of_equinoxes(
ut1: &UT1,
tt: &TT,
location: &Location,
) -> TimeResult<Self> {
Self::from_ut1_tt_and_location(ut1, tt, location)
}
pub fn from_hours(hours: f64, location: &Location) -> Self {
Self {
angle: SiderealAngle::from_hours(hours),
location: *location,
}
}
pub fn from_degrees(degrees: f64, location: &Location) -> Self {
Self {
angle: SiderealAngle::from_degrees(degrees),
location: *location,
}
}
pub fn from_radians(radians: f64, location: &Location) -> Self {
Self {
angle: SiderealAngle::from_radians(radians),
location: *location,
}
}
pub fn j2000(location: &Location) -> TimeResult<Self> {
let ut1 = UT1::j2000();
let tt = TT::j2000();
Self::from_ut1_tt_and_location(&ut1, &tt, location)
}
pub fn angle(&self) -> SiderealAngle {
self.angle
}
pub fn location(&self) -> Location {
self.location
}
pub fn hours(&self) -> f64 {
self.angle.hours()
}
pub fn degrees(&self) -> f64 {
self.angle.degrees()
}
pub fn radians(&self) -> f64 {
self.angle.radians()
}
pub fn hour_angle_to_target(&self, target_ra_hours: f64) -> f64 {
self.angle.hour_angle_to_target(target_ra_hours)
}
pub fn to_gast(&self) -> GAST {
let longitude_hours = self.location.longitude * 12.0 / celestial_core::constants::PI;
let gast_hours = self.hours() - longitude_hours;
GAST::from_hours(gast_hours)
}
pub fn to_lmst(&self, tt: &TT) -> TimeResult<LMST> {
let nutation = tt.nutation_iau2006a()?;
let jd = tt.to_julian_date();
let mean_obliquity = celestial_core::obliquity::iau_2006_mean_obliquity(jd.jd1(), jd.jd2());
let ee_rad = nutation.nutation_longitude() * libm::cos(mean_obliquity);
let ee_hours = ee_rad * 12.0 / celestial_core::constants::PI;
let lmst_hours = self.hours() - ee_hours;
Ok(LMST::from_hours(lmst_hours, &self.location))
}
pub fn to_gmst(&self, tt: &TT) -> TimeResult<GMST> {
let lmst = self.to_lmst(tt)?;
Ok(lmst.to_gmst())
}
}
impl std::fmt::Display for LAST {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let lat_deg = self.location.latitude * celestial_core::constants::RAD_TO_DEG;
let lon_deg = self.location.longitude * celestial_core::constants::RAD_TO_DEG;
write!(
f,
"LAST {} at ({:.4}°, {:.4}°)",
self.angle, lat_deg, lon_deg
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mauna_kea() -> Location {
Location::from_degrees(19.8283, -155.4783, 4145.0).unwrap()
}
fn greenwich() -> Location {
Location::greenwich()
}
#[test]
fn test_last_at_greenwich_equals_gast() {
let ut1 = UT1::j2000();
let tt = TT::j2000();
let location = greenwich();
let gast = GAST::from_ut1_and_tt(&ut1, &tt).unwrap();
let last = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
assert!(
(last.hours() - gast.hours()).abs() < 1e-14,
"LAST at Greenwich should equal GAST: LAST={}, GAST={}",
last.hours(),
gast.hours()
);
}
#[test]
fn test_last_method_consistency() {
let ut1 = UT1::j2000();
let tt = TT::j2000();
let location = mauna_kea();
let last_method1 = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
let last_method2 = LAST::from_lmst_and_equation_of_equinoxes(&ut1, &tt, &location).unwrap();
assert!(
(last_method1.hours() - last_method2.hours()).abs() < 1e-14,
"LAST calculation methods should match: method1={}, method2={}",
last_method1.hours(),
last_method2.hours()
);
}
#[test]
fn test_last_longitude_correction() {
let ut1 = UT1::j2000();
let tt = TT::j2000();
let greenwich_loc = greenwich();
let east_15deg = Location::from_degrees(0.0, 15.0, 0.0).unwrap(); let west_15deg = Location::from_degrees(0.0, -15.0, 0.0).unwrap();
let last_greenwich = LAST::from_ut1_tt_and_location(&ut1, &tt, &greenwich_loc).unwrap();
let last_east = LAST::from_ut1_tt_and_location(&ut1, &tt, &east_15deg).unwrap();
let last_west = LAST::from_ut1_tt_and_location(&ut1, &tt, &west_15deg).unwrap();
let diff_east = last_east.hours() - last_greenwich.hours();
assert!(
(diff_east - 1.0).abs() < 1e-12,
"15°E should be +1 hour: {}",
diff_east
);
let diff_west = last_west.hours() - last_greenwich.hours();
assert!(
(diff_west + 1.0).abs() < 1e-12,
"15°W should be -1 hour: {}",
diff_west
);
}
#[test]
fn test_last_vs_gast_longitude() {
let ut1 = UT1::j2000();
let tt = TT::j2000();
let location = mauna_kea();
let last = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
let gast = GAST::from_ut1_and_tt(&ut1, &tt).unwrap();
let longitude_hours = location.longitude * 12.0 / celestial_core::constants::PI;
let expected_last = gast.hours() + longitude_hours;
let expected_last_normalized = ((expected_last % 24.0) + 24.0) % 24.0;
assert!(
(last.hours() - expected_last_normalized).abs() < 1e-14,
"LAST = GAST + longitude: LAST={}, GAST={}, longitude={}, expected={}",
last.hours(),
gast.hours(),
longitude_hours,
expected_last_normalized
);
}
#[test]
fn test_last_mauna_kea() {
let ut1 = UT1::j2000();
let tt = TT::j2000();
let location = mauna_kea();
let gast = GAST::from_ut1_and_tt(&ut1, &tt).unwrap();
let last = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
let expected_offset = -155.4783 / 15.0; let actual_offset = last.hours() - gast.hours();
assert!(
(actual_offset - expected_offset).abs() < 1e-10,
"Mauna Kea LAST offset incorrect: expected={}, actual={}",
expected_offset,
actual_offset
);
}
#[test]
fn test_last_j2000() {
let location = mauna_kea();
let last = LAST::j2000(&location).unwrap();
let hours = last.hours();
assert!(
(0.0..24.0).contains(&hours),
"LAST should be in [0, 24) hours: {}",
hours
);
}
#[test]
fn test_last_hour_angle_calculation() {
let location = mauna_kea();
let last = LAST::from_hours(12.0, &location);
let target_ra = 6.0;
let hour_angle = last.hour_angle_to_target(target_ra);
assert_eq!(hour_angle, 6.0);
}
#[test]
fn test_last_to_gast_roundtrip() {
let location = mauna_kea();
let original_gast = GAST::from_hours(15.5);
let longitude_hours = location.longitude * 12.0 / celestial_core::constants::PI;
let last_hours = original_gast.hours() + longitude_hours;
let last = LAST::from_hours(last_hours, &location);
let recovered_gast = last.to_gast();
assert!(
(recovered_gast.hours() - original_gast.hours()).abs() < 1e-14,
"GAST->LAST->GAST roundtrip failed: original={}, recovered={}",
original_gast.hours(),
recovered_gast.hours()
);
}
#[test]
fn test_last_to_lmst_conversion() {
let ut1 = UT1::j2000();
let tt = TT::j2000();
let location = mauna_kea();
let original_lmst = LMST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
let last = LAST::from_lmst_and_equation_of_equinoxes(&ut1, &tt, &location).unwrap();
let recovered_lmst = last.to_lmst(&tt).unwrap();
assert!(
(recovered_lmst.hours() - original_lmst.hours()).abs() < 1e-7,
"LMST->LAST->LMST roundtrip failed: original={}, recovered={}",
original_lmst.hours(),
recovered_lmst.hours()
);
}
#[test]
fn test_last_from_constructors() {
let location = mauna_kea();
let hours = 14.5;
let degrees = hours * 15.0;
let radians = hours * celestial_core::constants::PI / 12.0;
let last_hours = LAST::from_hours(hours, &location);
let last_degrees = LAST::from_degrees(degrees, &location);
let last_radians = LAST::from_radians(radians, &location);
assert!((last_hours.hours() - last_degrees.hours()).abs() < 1e-14);
assert!((last_hours.hours() - last_radians.hours()).abs() < 1e-14);
assert_eq!(last_hours.location(), location);
assert_eq!(last_degrees.location(), location);
assert_eq!(last_radians.location(), location);
}
#[test]
fn test_last_display() {
let location = mauna_kea();
let last = LAST::from_hours(12.5, &location);
let display = format!("{}", last);
assert!(display.contains("LAST"));
assert!(display.contains("19.8283")); assert!(display.contains("-155.4783")); }
#[test]
fn test_last_location_enforcement() {
let location = mauna_kea();
let last = LAST::from_hours(12.0, &location);
let stored_location = last.location();
assert_eq!(stored_location.latitude, location.latitude);
assert_eq!(stored_location.longitude, location.longitude);
assert_eq!(stored_location.height, location.height);
}
#[test]
fn test_extreme_longitudes() {
let ut1 = UT1::j2000();
let tt = TT::j2000();
let east_extreme = Location::from_degrees(0.0, 180.0, 0.0).unwrap(); let west_extreme = Location::from_degrees(0.0, -180.0, 0.0).unwrap();
let gast = GAST::from_ut1_and_tt(&ut1, &tt).unwrap();
let last_east = LAST::from_ut1_tt_and_location(&ut1, &tt, &east_extreme).unwrap();
let last_west = LAST::from_ut1_tt_and_location(&ut1, &tt, &west_extreme).unwrap();
let diff_east = last_east.hours() - gast.hours();
let expected_east = if diff_east < 0.0 {
diff_east + 24.0
} else {
diff_east
};
assert!(
(expected_east - 12.0).abs() < 1e-12,
"180°E should be +12 hours"
);
let diff_west = last_west.hours() - gast.hours();
let expected_west = if diff_west > 12.0 {
diff_west - 24.0
} else {
diff_west
};
assert!(
(expected_west + 12.0).abs() < 1e-12,
"180°W should be -12 hours"
);
}
#[test]
fn test_last_equation_of_equinoxes_range() {
let ut1 = UT1::j2000();
let tt = TT::j2000();
let location = mauna_kea();
let last = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
let lmst = LMST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
let ee_hours = last.hours() - lmst.hours();
let ee_hours_normalized = if ee_hours > 12.0 {
ee_hours - 24.0
} else if ee_hours < -12.0 {
ee_hours + 24.0
} else {
ee_hours
};
assert!(
ee_hours_normalized.abs() < 0.001, "Equation of equinoxes too large: {} hours = {} seconds",
ee_hours_normalized,
ee_hours_normalized * 3600.0
);
}
#[test]
fn test_last_constructors_and_accessors() {
let location = greenwich();
let last_deg = LAST::from_degrees(45.0, &location);
assert_eq!(last_deg.degrees(), 45.0);
assert_eq!(last_deg.hours(), 3.0);
let last_rad = LAST::from_radians(celestial_core::constants::PI / 4.0, &location);
assert!((last_rad.radians() - celestial_core::constants::PI / 4.0).abs() < 1e-15);
assert_eq!(last_rad.hours(), 3.0);
let angle = last_deg.angle();
assert_eq!(angle.degrees(), 45.0);
let stored_location = last_deg.location();
assert_eq!(stored_location.latitude, location.latitude);
assert_eq!(stored_location.longitude, location.longitude);
assert_eq!(stored_location.height, location.height);
let degrees = last_deg.degrees();
assert_eq!(degrees, 45.0);
}
}