use std::{
f64::consts::PI,
ops::{Add, Sub},
};
use jiff::{Span, Zoned, civil::Date, tz::TimeZone};
use crate::{
astronomical_calculator::Azimuth,
util::{
geolocation::GeoLocation,
math_helper::{HOUR_MINUTES, HOUR_SECONDS, MINUTE_SECONDS, SECOND_NANOS},
zenith_adjustments::adjusted_zenith,
},
};
const JULIAN_DAY_JAN_1_2000: f64 = 2_451_545.0;
const JULIAN_DAYS_PER_CENTURY: f64 = 36_525.0;
pub(crate) fn antimeridian_adjusted_date(dt: &Zoned, longitude: f64) -> Date {
let local_hours_offset =
longitude / 15.0 - f64::from(dt.offset().seconds()) / HOUR_SECONDS;
if local_hours_offset >= 20.0 {
dt.add(Span::new().days(1)).date()
} else if local_hours_offset <= -20.0 {
dt.sub(Span::new().days(1)).date()
} else {
dt.date()
}
}
fn datetime_to_julian_day(dt: &Zoned, longitude: f64) -> f64 {
let date = antimeridian_adjusted_date(dt, longitude);
let mut year = f64::from(date.year());
let mut month = f64::from(date.month());
let day = f64::from(date.day());
if month <= 2.0 {
year -= 1.0;
month += 12.0;
}
let a = (year / 100.0).floor();
let b = (2.0 - a + a / 4.0).floor();
(365.25 * (year + 4716.0)).floor() + (30.6001 * (month + 1.0)).floor() + day + b - 1524.5
}
fn julian_centuries_from_julian_day(julian_day: f64) -> f64 {
(julian_day - JULIAN_DAY_JAN_1_2000) / JULIAN_DAYS_PER_CENTURY
}
fn julian_day_from_julian_centuries(julian_centuries: f64) -> f64 {
julian_centuries.mul_add(JULIAN_DAYS_PER_CENTURY, JULIAN_DAY_JAN_1_2000)
}
fn sun_hour_angle_at_horizon(latitude: f64, solar_dec: f64, zenith: f64, mode: &Mode) -> f64 {
let lat_r = latitude.to_radians();
let solar_dec_r = solar_dec.to_radians();
let zenith_r = zenith.to_radians();
let mut hour_angle = lat_r
.tan()
.mul_add(
-solar_dec_r.tan(),
zenith_r.cos() / (lat_r.cos() * solar_dec_r.cos()),
)
.acos();
if *mode == Mode::SunsetMidnight {
hour_angle *= -1.0;
}
hour_angle }
fn earth_orbit_eccentricity(julian_centuries: f64) -> f64 {
julian_centuries.mul_add(
-0.0000001267f64.mul_add(julian_centuries, 0.000042037),
0.016708634,
)
}
fn sun_geometric_mean_anomaly(julian_centuries: f64) -> f64 {
let anomaly = julian_centuries.mul_add(
0.0001537f64.mul_add(-julian_centuries, 35999.05029),
357.52911,
); anomaly % 360.0 }
fn sun_geometric_mean_longitude(julian_centuries: f64) -> f64 {
let longitude = julian_centuries.mul_add(
0.0003032f64.mul_add(julian_centuries, 36000.76983),
280.46646,
); longitude % 360.0 }
fn mean_obliquity_of_ecliptic(julian_centuries: f64) -> f64 {
let seconds = julian_centuries.mul_add(
-julian_centuries.mul_add(julian_centuries.mul_add(-0.001813, 0.00059), 46.8150),
21.448,
);
23.0 + ((26.0 + (seconds / 60.0)) / 60.0) }
fn obliquity_correction(julian_centuries: f64) -> f64 {
let obliquity_of_ecliptic = mean_obliquity_of_ecliptic(julian_centuries);
let omega = 1_934.136f64.mul_add(-julian_centuries, 125.04);
let correction = 0.00256f64.mul_add(omega.to_radians().cos(), obliquity_of_ecliptic);
correction % 360.0 }
fn equation_of_time(julian_centuries: f64) -> f64 {
let epsilon = obliquity_correction(julian_centuries).to_radians();
let mean_lon = sun_geometric_mean_longitude(julian_centuries).to_radians();
let mean_anom = sun_geometric_mean_anomaly(julian_centuries).to_radians();
let eoe = earth_orbit_eccentricity(julian_centuries);
let mut y = (epsilon / 2.0).tan();
y *= y;
let sin2l0 = (2.0 * mean_lon).sin();
let sin4l0 = (4.0 * mean_lon).sin();
let cos2l0 = (2.0 * mean_lon).cos();
let sin_anom = (mean_anom).sin();
let sin_2_anom = (2.0 * mean_anom).sin();
let eq_time = (1.25 * eoe * eoe).mul_add(
-sin_2_anom,
(0.5 * y * y).mul_add(
-sin4l0,
(4.0 * eoe * y * sin_anom).mul_add(cos2l0, (y * sin2l0) - (2.0 * eoe * sin_anom)),
),
);
eq_time.to_degrees() * 4.0 }
fn solar_noon_utc(julian_centuries: f64, longitude: f64) -> f64 {
let century_start = julian_day_from_julian_centuries(julian_centuries);
let approx_tnoon = julian_centuries_from_julian_day(century_start + (longitude / 360.0));
let approx_eq_time = equation_of_time(approx_tnoon);
let approx_sol_noon = longitude.mul_add(4.0, 720.0) - approx_eq_time;
let tnoon = julian_centuries_from_julian_day(century_start - 0.5 + (approx_sol_noon / 1_440.0));
let eq_time = equation_of_time(tnoon);
longitude.mul_add(4.0, 720.0) - eq_time
}
fn sun_equation_of_center(julian_centuries: f64) -> f64 {
let mrad = sun_geometric_mean_anomaly(julian_centuries).to_radians();
let sinm = mrad.sin();
let sin_2_anom = (2.0 * mrad).sin();
let sin_3_anom = (3.0 * mrad).sin();
sinm.mul_add(
julian_centuries.mul_add(-0.000014f64.mul_add(julian_centuries, 0.004817), 1.914602),
sin_2_anom * 0.000101f64.mul_add(-julian_centuries, 0.019993),
) + (sin_3_anom * 0.000289) }
fn sun_true_longitude(julian_centuries: f64) -> f64 {
let sgml = sun_geometric_mean_longitude(julian_centuries);
let center = sun_equation_of_center(julian_centuries);
sgml + center }
fn sun_apparent_longitude(julian_centuries: f64) -> f64 {
let true_longitude = sun_true_longitude(julian_centuries);
let omega = 1_934.136f64.mul_add(-julian_centuries, 125.04);
0.00478f64.mul_add(-omega.to_radians().sin(), true_longitude - 0.00569) }
fn solar_declination(julian_centuries: f64) -> f64 {
let correction = obliquity_correction(julian_centuries).to_radians();
let apparent_longitude = sun_apparent_longitude(julian_centuries).to_radians();
let sint = correction.sin() * apparent_longitude.sin();
sint.asin().to_degrees() }
fn approximate_utc_sun_position(
approx_julian_centuries: f64,
latitude: f64,
longitude: f64,
zenith: f64,
mode: &Mode,
) -> f64 {
let eq_time = equation_of_time(approx_julian_centuries);
let solar_dec = solar_declination(approx_julian_centuries);
let hour_angle = sun_hour_angle_at_horizon(latitude, solar_dec, zenith, mode);
let delta = longitude - hour_angle.to_degrees();
let time_delta = delta * 4.0;
720.0 + time_delta - eq_time
}
fn utc_sun_rise_set(
date: Date,
geo_location: &GeoLocation,
zenith: f64,
adjust_for_elevation: bool,
mode: &Mode,
) -> Option<f64> {
let elevation = if adjust_for_elevation {
geo_location.elevation
} else {
0.0
};
let zoned = date.to_zoned(geo_location.timezone.clone()).ok()?;
let adjusted_date = antimeridian_adjusted_date(&zoned, geo_location.longitude);
let adjusted_zenith = adjusted_zenith(zenith, elevation, adjusted_date);
let julian_day = datetime_to_julian_day(&zoned, geo_location.longitude);
let noonmin = solar_noon_utc(
julian_centuries_from_julian_day(julian_day),
-geo_location.longitude,
);
let tnoon = julian_centuries_from_julian_day(julian_day + (noonmin / 1_440.0));
let first_pass = approximate_utc_sun_position(
tnoon,
geo_location.latitude,
-geo_location.longitude,
adjusted_zenith,
mode,
);
let trefinement = julian_centuries_from_julian_day(julian_day + (first_pass / 1_440.0));
let time = approximate_utc_sun_position(
trefinement,
geo_location.latitude,
-geo_location.longitude,
adjusted_zenith,
mode,
);
normalize_time(time)
}
fn normalize_time(minutes: f64) -> Option<f64> {
let time = minutes / 60.0;
if time.is_nan() {
return None;
}
let wrapped = time % 24.0;
Some(if wrapped < 0.0 { wrapped + 24.0 } else { wrapped })
}
fn utc_solar_noon_midnight(date: Date, geo_location: &GeoLocation, mode: &Mode) -> Option<f64> {
let julian_day = datetime_to_julian_day(
&date.to_zoned(geo_location.timezone.clone()).ok()?,
geo_location.longitude,
);
let longitude = -geo_location.longitude;
let base_minutes = if *mode == Mode::SunriseNoon {
720.0
} else {
1440.0
};
let tnoon = julian_centuries_from_julian_day(julian_day + longitude / 360.0);
let eot = equation_of_time(tnoon);
let mut sol_noon_utc = longitude.mul_add(4.0, -eot);
for _ in 0..2 {
let newt = julian_centuries_from_julian_day(julian_day + sol_noon_utc / 1440.0);
let eot = equation_of_time(newt);
if eot.is_nan() {
return None;
}
sol_noon_utc = longitude.mul_add(4.0, base_minutes) - eot;
}
normalize_time(sol_noon_utc)
}
fn sin_deg(deg: f64) -> f64 {
deg.to_radians().sin()
}
fn cos_deg(deg: f64) -> f64 {
deg.to_radians().cos()
}
fn tan_deg(deg: f64) -> f64 {
deg.to_radians().tan()
}
fn acos_deg(x: f64) -> f64 {
x.acos().to_degrees()
}
fn solar_position(instant: &Zoned, loc: &GeoLocation) -> (f64, f64, f64) {
let utc = instant.with_time_zone(TimeZone::UTC);
let fractional_day = (f64::from(utc.hour())
+ (f64::from(utc.minute())
+ (f64::from(utc.second()) + f64::from(utc.subsec_nanosecond()) / SECOND_NANOS)
/ MINUTE_SECONDS)
/ HOUR_MINUTES)
/ 24.0;
let julian_day = datetime_to_julian_day(&utc, loc.longitude) + fractional_day;
let julian_centuries = julian_centuries_from_julian_day(julian_day);
let declination = solar_declination(julian_centuries);
let eq_time = equation_of_time(julian_centuries);
let true_solar_time =
((fractional_day + eq_time / 1_440.0 + loc.longitude / 360.0) + 2.0) % 1.0;
let hour_angle = true_solar_time.mul_add(2.0 * PI, -PI);
let cos_zenith = sin_deg(loc.latitude).mul_add(
sin_deg(declination),
cos_deg(loc.latitude) * cos_deg(declination) * hour_angle.cos(),
);
let zenith = acos_deg(cos_zenith.clamp(-1.0, 1.0));
(zenith, hour_angle, declination)
}
fn adjust_elevation_for_refraction(elevation: f64) -> f64 {
if elevation > 85.0 {
return 0.0;
}
let te = tan_deg(elevation);
let correction = if elevation > 5.0 {
(58.1 / te) - (0.07 / te.powi(3)) + (0.000086 / te.powi(5))
} else if elevation > -0.575 {
elevation.mul_add(
elevation.mul_add(
elevation.mul_add(0.711f64.mul_add(elevation, -12.79), 103.4),
-518.2,
),
1735.0,
)
} else {
-20.774 / te
};
correction / 3_600.0
}
#[must_use]
pub fn utc_time_at_azimuth(
date: Date,
geo_location: &GeoLocation,
target_azimuth: Azimuth,
) -> Option<f64> {
let julian_day = datetime_to_julian_day(
&date.to_zoned(geo_location.timezone.clone()).ok()?,
geo_location.longitude,
);
let solar_noon_base = 0.5 - (geo_location.longitude / 360.0);
let quarter = match target_azimuth {
Azimuth::East => 0.25,
Azimuth::West => 0.75,
};
let mut date_time = solar_noon_base + quarter;
for _ in 0..3 {
let julian_centuries = julian_centuries_from_julian_day(julian_day + date_time);
let ratio = tan_deg(solar_declination(julian_centuries)) / tan_deg(geo_location.latitude);
if ratio.is_nan() || !(-1.0..=1.0).contains(&ratio) {
return None;
}
let sign = match target_azimuth {
Azimuth::East => -1.0,
Azimuth::West => 1.0,
};
let offset = sign * (acos_deg(ratio) / 360.0);
date_time = solar_noon_base + offset - (equation_of_time(julian_centuries) / 1_440.0);
}
Some((date_time * 24.0 % 24.0 + 24.0) % 24.0)
}
#[must_use]
pub fn utc_sunrise(
date: Date,
geo_location: &GeoLocation,
zenith: f64,
adjust_for_elevation: bool,
) -> Option<f64> {
utc_sun_rise_set(
date,
geo_location,
zenith,
adjust_for_elevation,
&Mode::SunriseNoon,
)
}
#[must_use]
pub fn utc_sunset(
date: Date,
geo_location: &GeoLocation,
zenith: f64,
adjust_for_elevation: bool,
) -> Option<f64> {
utc_sun_rise_set(
date,
geo_location,
zenith,
adjust_for_elevation,
&Mode::SunsetMidnight,
)
}
#[must_use]
pub fn utc_noon(date: Date, geo_location: &GeoLocation) -> Option<f64> {
utc_solar_noon_midnight(date, geo_location, &Mode::SunriseNoon)
}
#[must_use]
pub fn utc_midnight(date: Date, geo_location: &GeoLocation) -> Option<f64> {
utc_solar_noon_midnight(date, geo_location, &Mode::SunsetMidnight)
}
#[must_use]
pub fn solar_elevation(instant: &Zoned, geo_location: &GeoLocation) -> f64 {
let (zenith, _, _) = solar_position(instant, geo_location);
let elevation = 90.0 - zenith;
elevation + adjust_elevation_for_refraction(elevation)
}
#[must_use]
pub fn solar_azimuth(instant: &Zoned, geo_location: &GeoLocation) -> f64 {
let (zenith, hour_angle, declination) = solar_position(instant, geo_location);
let az_denominator = cos_deg(geo_location.latitude) * sin_deg(zenith);
let azimuth = if az_denominator.abs() > 0.001 {
let az = sin_deg(geo_location.latitude).mul_add(cos_deg(zenith), -sin_deg(declination))
/ az_denominator;
let sign = if hour_angle > 0.0 { -1.0 } else { 1.0 };
acos_deg(az.clamp(-1.0, 1.0)).mul_add(-sign, 180.0)
} else if geo_location.latitude > 0.0 {
180.0
} else {
0.0
};
(azimuth + 360.0) % 360.0
}
#[derive(PartialEq)]
enum Mode {
SunriseNoon,
SunsetMidnight,
}