use crate::calendar::{gmst_degrees, gregorian_to_jd};
use crate::coords::{deg_to_rad, mean_obliquity, normalize_degrees, rad_to_deg};
use crate::error::Result;
use crate::planet::Planet;
const REFRACTION_CORRECTION: f64 = -0.5667;
const SUN_RISE_SET_ALTITUDE: f64 = -0.8333;
const MOON_RISE_SET_ALTITUDE: f64 = 0.125;
fn standard_altitude(planet: Planet) -> f64 {
match planet {
Planet::Sun => SUN_RISE_SET_ALTITUDE,
Planet::Moon => MOON_RISE_SET_ALTITUDE,
_ => REFRACTION_CORRECTION,
}
}
fn equatorial_position(planet: Planet, jd: f64) -> Result<(f64, f64)> {
let (lon, lat) = match planet {
Planet::Sun => (crate::sun::solar_longitude(jd), 0.0),
Planet::Moon => (
crate::moon::lunar_longitude(jd),
crate::moon::lunar_latitude(jd),
),
_ => {
let pos = crate::planetary::compute_position(planet, jd)?;
(pos.longitude_deg, pos.latitude_deg)
}
};
let t = crate::calendar::julian_centuries(jd);
let eps = mean_obliquity(t);
let (ra, dec) = crate::coords::ecliptic_to_equatorial(lon, lat, eps);
Ok((ra, dec))
}
#[derive(Debug, Clone, Copy)]
pub struct RiseSetTransit {
pub rise: Option<f64>,
pub transit: Option<f64>,
pub set: Option<f64>,
}
pub fn rise_set_transit(
planet: Planet,
year: i32,
month: u32,
day: u32,
lat_deg: f64,
lon_deg: f64,
) -> Result<RiseSetTransit> {
let h0 = standard_altitude(planet);
let jd0 = gregorian_to_jd(year, month, day, 0, 0, 0.0)?;
let (ra1, dec1) = equatorial_position(planet, jd0 - 1.0)?;
let (ra2, dec2) = equatorial_position(planet, jd0)?;
let (ra3, dec3) = equatorial_position(planet, jd0 + 1.0)?;
let theta0 = gmst_degrees(jd0);
let lat_rad = deg_to_rad(lat_deg);
let h0_rad = deg_to_rad(h0);
let dec2_rad = deg_to_rad(dec2);
let cos_h = (h0_rad.sin() - lat_rad.sin() * dec2_rad.sin()) / (lat_rad.cos() * dec2_rad.cos());
let (rise_jd, set_jd) = if cos_h < -1.0 {
(None, None)
} else if cos_h > 1.0 {
(None, None)
} else {
let h_deg = rad_to_deg(cos_h.acos());
let m0 = (ra2 + lon_deg - theta0) / 360.0;
let m1 = m0 - h_deg / 360.0;
let m2 = m0 + h_deg / 360.0;
let m1 = normalize_m(m1);
let m2 = normalize_m(m2);
let interp = InterpData {
ra: [ra1, ra2, ra3],
dec: [dec1, dec2, dec3],
};
let rise = refine_rise_set(m1, theta0, lon_deg, lat_deg, h0, &interp);
let set = refine_rise_set(m2, theta0, lon_deg, lat_deg, h0, &interp);
(Some(jd0 + rise), Some(jd0 + set))
};
let m0 = normalize_m((ra2 + lon_deg - theta0) / 360.0);
let transit_jd = Some(jd0 + m0);
Ok(RiseSetTransit {
rise: rise_jd,
transit: transit_jd,
set: set_jd,
})
}
fn normalize_m(m: f64) -> f64 {
let r = m % 1.0;
if r < 0.0 { r + 1.0 } else { r }
}
struct InterpData {
ra: [f64; 3],
dec: [f64; 3],
}
fn refine_rise_set(
m: f64,
theta0: f64,
lon_deg: f64,
lat_deg: f64,
h0: f64,
interp: &InterpData,
) -> f64 {
let mut m_refined = m;
for _ in 0..4 {
let ra = interpolate_angle(interp.ra[0], interp.ra[1], interp.ra[2], m_refined);
let dec = interpolate(interp.dec[0], interp.dec[1], interp.dec[2], m_refined);
let theta = theta0 + 360.985_647 * m_refined;
let h = normalize_degrees(theta - lon_deg - ra);
let h = if h > 180.0 { h - 360.0 } else { h };
let lat_r = deg_to_rad(lat_deg);
let dec_r = deg_to_rad(dec);
let h_r = deg_to_rad(h);
let alt =
rad_to_deg((lat_r.sin() * dec_r.sin() + lat_r.cos() * dec_r.cos() * h_r.cos()).asin());
let denom = 360.0 * dec_r.cos() * lat_r.cos() * h_r.sin();
if denom.abs() < 1e-12 {
break;
}
let dm = (alt - h0) / denom;
m_refined += dm;
if dm.abs() < 1e-8 {
break;
}
}
m_refined
}
fn interpolate(y1: f64, y2: f64, y3: f64, n: f64) -> f64 {
let a = y2 - y1;
let b = y3 - y2;
let c = b - a;
y2 + n * (a + b + n * c) / 2.0
}
fn interpolate_angle(y1: f64, y2: f64, y3: f64, n: f64) -> f64 {
let mut a = y2 - y1;
let mut b = y3 - y2;
if a > 180.0 {
a -= 360.0;
} else if a < -180.0 {
a += 360.0;
}
if b > 180.0 {
b -= 360.0;
} else if b < -180.0 {
b += 360.0;
}
let c = b - a;
normalize_degrees(y2 + n * (a + b + n * c) / 2.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calendar::jd_to_gregorian;
#[test]
fn sun_rise_set_london_j2000() {
let rst = rise_set_transit(Planet::Sun, 2000, 1, 1, 51.5, -0.1).unwrap();
assert!(rst.rise.is_some());
assert!(rst.transit.is_some());
assert!(rst.set.is_some());
let rise = rst.rise.unwrap();
let transit = rst.transit.unwrap();
let set = rst.set.unwrap();
assert!(
rise < transit,
"rise {rise} should be before transit {transit}"
);
assert!(
transit < set,
"transit {transit} should be before set {set}"
);
let (_, _, _, h, m, _) = jd_to_gregorian(rise);
assert!(h == 8 || h == 7, "rise hour = {h}:{m}");
}
#[test]
fn sun_rise_set_equator() {
let rst = rise_set_transit(Planet::Sun, 2000, 3, 20, 0.0, 0.0).unwrap();
assert!(rst.rise.is_some());
assert!(rst.set.is_some());
let rise = rst.rise.unwrap();
let set = rst.set.unwrap();
let day_length = (set - rise) * 24.0;
assert!(
(day_length - 12.0).abs() < 1.0,
"day length = {day_length}h"
);
}
#[test]
fn moon_rise_set() {
let rst = rise_set_transit(Planet::Moon, 2000, 1, 1, 51.5, -0.1).unwrap();
assert!(rst.transit.is_some());
}
#[test]
fn mars_rise_set() {
let rst = rise_set_transit(Planet::Mars, 2000, 1, 1, 51.5, -0.1).unwrap();
assert!(rst.transit.is_some());
}
#[test]
fn normalize_m_range() {
assert!((normalize_m(0.5) - 0.5).abs() < 1e-10);
assert!((normalize_m(-0.3) - 0.7).abs() < 1e-10);
assert!((normalize_m(1.3) - 0.3).abs() < 1e-10);
}
}