use std::{
cmp::Ordering,
ops::{Add, Sub},
};
use jiff::{SignedDuration, Zoned, civil::Date, tz::TimeZone};
use crate::util::{geolocation::GeoLocation, math_helper::HOUR_NANOS, noaa_calculator};
pub const GEOMETRIC_ZENITH: f64 = 90.0;
pub const CIVIL_ZENITH: f64 = 96.0;
pub const NAUTICAL_ZENITH: f64 = 102.0;
pub const ASTRONOMICAL_ZENITH: f64 = 108.0;
#[must_use]
pub fn utc_sunrise(date: Date, zenith: f64, geo_location: &GeoLocation) -> Option<f64> {
noaa_calculator::utc_sunrise(date, geo_location, zenith, true)
}
#[must_use]
pub fn utc_sunset(date: Date, zenith: f64, geo_location: &GeoLocation) -> Option<f64> {
noaa_calculator::utc_sunset(date, geo_location, zenith, true)
}
#[must_use]
pub fn utc_sea_level_sunrise(date: Date, zenith: f64, geo_location: &GeoLocation) -> Option<f64> {
noaa_calculator::utc_sunrise(date, geo_location, zenith, false)
}
#[must_use]
pub fn utc_sea_level_sunset(date: Date, zenith: f64, geo_location: &GeoLocation) -> Option<f64> {
noaa_calculator::utc_sunset(date, geo_location, zenith, false)
}
#[must_use]
pub fn sunrise(date: Date, geo_location: &GeoLocation) -> Option<Zoned> {
date_time_from_time_of_day(
date,
noaa_calculator::utc_sunrise(date, geo_location, GEOMETRIC_ZENITH, true)?,
geo_location,
&SolarEvent::Sunrise,
)
}
#[must_use]
pub fn sunset(date: Date, geo_location: &GeoLocation) -> Option<Zoned> {
date_time_from_time_of_day(
date,
noaa_calculator::utc_sunset(date, geo_location, GEOMETRIC_ZENITH, true)?,
geo_location,
&SolarEvent::Sunset,
)
}
#[must_use]
pub fn sea_level_sunrise(date: Date, geo_location: &GeoLocation) -> Option<Zoned> {
sunrise_offset_by_degrees(date, geo_location, GEOMETRIC_ZENITH)
}
#[must_use]
pub fn sea_level_sunset(date: Date, geo_location: &GeoLocation) -> Option<Zoned> {
sunset_offset_by_degrees(date, geo_location, GEOMETRIC_ZENITH)
}
#[must_use]
pub fn sunrise_offset_by_degrees(
date: Date,
geo_location: &GeoLocation,
offset_zenith: f64,
) -> Option<Zoned> {
date_time_from_time_of_day(
date,
utc_sea_level_sunrise(date, offset_zenith, geo_location)?,
geo_location,
&SolarEvent::Sunrise,
)
}
#[must_use]
pub fn sunset_offset_by_degrees(
date: Date,
geo_location: &GeoLocation,
offset_zenith: f64,
) -> Option<Zoned> {
date_time_from_time_of_day(
date,
utc_sea_level_sunset(date, offset_zenith, geo_location)?,
geo_location,
&SolarEvent::Sunset,
)
}
#[must_use]
pub fn temporal_hour(sunrise: &Zoned, sunset: &Zoned) -> SignedDuration {
sunset.duration_since(sunrise) / 12
}
#[must_use]
pub fn solar_noon(date: Date, geo_location: &GeoLocation) -> Option<Zoned> {
date_time_from_time_of_day(
date,
noaa_calculator::utc_noon(date, geo_location)?,
geo_location,
&SolarEvent::Noon,
)
}
#[must_use]
pub fn solar_midnight(date: Date, geo_location: &GeoLocation) -> Option<Zoned> {
date_time_from_time_of_day(
date,
noaa_calculator::utc_midnight(date, geo_location)?,
geo_location,
&SolarEvent::Midnight,
)
}
#[must_use]
pub fn solar_azimuth(instant: &Zoned, geo_location: &GeoLocation) -> f64 {
noaa_calculator::solar_azimuth(instant, geo_location)
}
#[must_use]
pub fn solar_elevation(instant: &Zoned, geo_location: &GeoLocation) -> f64 {
noaa_calculator::solar_elevation(instant, geo_location)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Azimuth {
East,
West,
}
#[must_use]
pub fn time_at_azimuth(
date: Date,
geo_location: &GeoLocation,
azimuth: Azimuth,
) -> Option<Zoned> {
date_time_from_time_of_day(
date,
noaa_calculator::utc_time_at_azimuth(date, geo_location, azimuth)?,
geo_location,
match azimuth {
Azimuth::East => &SolarEvent::Sunrise,
Azimuth::West => &SolarEvent::Sunset,
},
)
}
enum SolarEvent {
Sunrise,
Sunset,
Noon,
Midnight,
}
fn date_time_from_time_of_day(
date: Date,
time_of_day: f64,
geo_location: &GeoLocation,
event: &SolarEvent,
) -> Option<Zoned> {
let anchor = noaa_calculator::antimeridian_adjusted_date(
&date.to_zoned(geo_location.timezone.clone()).ok()?,
geo_location.longitude,
);
let local_time_hours = geo_location.longitude / 15.0 + time_of_day;
let anchor = match event {
SolarEvent::Sunrise if local_time_hours > 18.0 => anchor.yesterday().ok()?,
SolarEvent::Sunset if local_time_hours < 6.0 => anchor.tomorrow().ok()?,
SolarEvent::Midnight if local_time_hours < 12.0 => anchor.tomorrow().ok()?,
SolarEvent::Noon if local_time_hours < 0.0 => anchor.tomorrow().ok()?,
SolarEvent::Noon if local_time_hours > 24.0 => anchor.yesterday().ok()?,
_ => anchor,
};
let total_nanos = (time_of_day * HOUR_NANOS).round() as i64;
let utc_dt = anchor
.to_zoned(TimeZone::UTC)
.ok()?
.add(SignedDuration::from_nanos(total_nanos));
Some(utc_dt.with_time_zone(geo_location.timezone.clone()))
}
#[must_use]
pub fn local_mean_time(date: Date, geo_location: &GeoLocation, hours: f64) -> Option<Zoned> {
if !(0.0..24.0).contains(&hours) {
return None;
}
let time_of_day = hours - geo_location.local_mean_time_offset();
let total_nanos = (time_of_day * HOUR_NANOS).round() as i64;
let utc_dt = date
.to_zoned(TimeZone::UTC)
.ok()?
.add(SignedDuration::from_nanos(total_nanos));
let local_dt = utc_dt.with_time_zone(geo_location.timezone.clone());
match local_dt.date().cmp(&date) {
Ordering::Less => Some(local_dt.add(SignedDuration::from_hours(24))),
Ordering::Greater => Some(local_dt.sub(SignedDuration::from_hours(24))),
Ordering::Equal => Some(local_dt),
}
}