#![cfg_attr(not(any(feature = "std", test)), no_std)]
use chrono::prelude::Utc;
use chrono::DateTime;
use chrono::TimeZone;
use chrono::Timelike;
use core::f64::consts::PI;
const PI2: f64 = PI * 2.0;
const RAD: f64 = 0.017453292519943295769236907684886;
const EARTH_MEAN_RADIUS: f64 = 6371.01;
const ASTRONOMICAL_UNIT: f64 = 149597890.0;
const JD2000: f64 = 2451545.0;
pub trait FloatOps {
fn sin(x: f64) -> f64;
fn cos(x: f64) -> f64;
fn tan(x: f64) -> f64;
fn asin(x: f64) -> f64;
fn acos(x: f64) -> f64;
fn atan(x: f64) -> f64;
fn atan2(y: f64, x: f64) -> f64;
fn trunc(x: f64) -> f64;
}
#[cfg(any(feature = "std", test))]
pub enum StdFloatOps {}
#[cfg(any(feature = "std", test))]
impl FloatOps for StdFloatOps {
fn sin(x: f64) -> f64 {
f64::sin(x)
}
fn cos(x: f64) -> f64 {
x.cos()
}
fn tan(x: f64) -> f64 {
x.tan()
}
fn asin(x: f64) -> f64 {
x.asin()
}
fn acos(x: f64) -> f64 {
x.acos()
}
fn atan(x: f64) -> f64 {
x.atan()
}
fn atan2(y: f64, x: f64) -> f64 {
y.atan2(x)
}
fn trunc(x: f64) -> f64 {
x.trunc()
}
}
#[derive(Debug, Clone)]
pub enum SunriseAndSet {
PolarNight,
PolarDay,
Daylight(DateTime<Utc>, DateTime<Utc>),
}
#[derive(Debug, Clone)]
pub struct SolarPos {
pub azimuth: f64,
pub zenith_angle: f64,
}
#[cfg(not(any(feature = "std", test)))]
#[derive(Debug, Clone)]
pub enum SpaError {
BadParam,
}
#[cfg(not(any(feature = "std", test)))]
impl core::fmt::Display for SpaError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
SpaError::BadParam => write!(f, "Latitude or longitude are not within valid ranges."),
}
}
}
#[cfg(any(feature = "std", test))]
#[derive(Debug, Clone, thiserror::Error)]
pub enum SpaError {
#[error("Latitude or longitude are not within valid ranges.")]
BadParam,
}
fn to_julian(utc: DateTime<Utc>) -> f64 {
let seconds_since_epoch: i64 = utc.timestamp();
((seconds_since_epoch as f64) / 86400.0) + 2440587.5
}
fn to_utc(jd: f64) -> DateTime<Utc> {
let secs_since_epoch = (jd - 2440587.5) * 86400.0;
let nanos = secs_since_epoch * (1000.0 * 1000.0 * 1000.0);
Utc.timestamp_nanos(nanos as i64)
}
fn in_pi(x: f64) -> f64 {
let n = (x / PI2) as i64;
let result = x - (n as f64 * PI2);
if result < 0.0 {
result + PI2
} else {
result
}
}
fn eps(t: f64) -> f64 {
return RAD
* (23.43929111 + ((-46.8150) * t - 0.00059 * t * t + 0.001813 * t * t * t) / 3600.0);
}
fn berechne_zeitgleichung<T: FloatOps>(t: f64) -> (f64, f64) {
let mut ra_mittel = 18.71506921 + 2400.0513369 * t + (2.5862e-5 - 1.72e-9 * t) * t * t;
let m = in_pi(PI2 * (0.993133 + 99.997361 * t));
let l = in_pi(
PI2 * (0.7859453
+ m / PI2
+ (6893.0 * T::sin(m) + 72.0 * T::sin(2.0 * m) + 6191.2 * t) / 1296.0e3),
);
let e = eps(t);
let mut ra = T::atan(T::tan(l) * T::cos(e));
if ra < 0.0 {
ra += PI;
}
if l > PI {
ra += PI;
}
ra = 24.0 * ra / PI2;
let dk = T::asin(T::sin(e) * T::sin(l));
ra_mittel = 24.0 * in_pi(PI2 * ra_mittel / 24.0) / PI2;
let mut d_ra = ra_mittel - ra;
if d_ra < -12.0 {
d_ra += 24.0;
}
if d_ra > 12.0 {
d_ra -= 24.0;
}
d_ra = d_ra * 1.0027379;
return (d_ra, dk);
}
pub fn sunrise_and_set<F: FloatOps>(
utc: DateTime<Utc>,
lat: f64,
lon: f64,
) -> Result<SunriseAndSet, SpaError> {
if -90.0 > lat || 90.0 < lat || -180.0 > lon || 180.0 < lon {
return Err(SpaError::BadParam);
}
let jd = to_julian(utc);
let t = (jd - JD2000) / 36525.0;
let h = -50.0 / 60.0 * RAD;
let b = lat * RAD; let geographische_laenge = lon;
let (ra_d, dk) = berechne_zeitgleichung::<F>(t);
let aux = (F::sin(h) - F::sin(b) * F::sin(dk)) / (F::cos(b) * F::cos(dk));
if aux >= 1.0 {
Result::Ok(SunriseAndSet::PolarNight)
} else if aux <= -1.0 {
Result::Ok(SunriseAndSet::PolarDay)
} else {
let zeitdifferenz = 12.0 * F::acos(aux) / PI;
let aufgang_lokal = 12.0 - zeitdifferenz - ra_d;
let untergang_lokal = 12.0 + zeitdifferenz - ra_d;
let aufgang_welt = aufgang_lokal - geographische_laenge / 15.0;
let untergang_welt = untergang_lokal - geographische_laenge / 15.0;
let jd_start = F::trunc(jd);
let aufgang_jd = (jd_start as f64) - 0.5 + (aufgang_welt / 24.0);
let untergang_jd = (jd_start as f64) - 0.5 + (untergang_welt / 24.0);
let sunriseset = SunriseAndSet::Daylight(to_utc(aufgang_jd), to_utc(untergang_jd));
Result::Ok(sunriseset)
}
}
pub fn solar_position<F: FloatOps>(
utc: DateTime<Utc>,
lat: f64,
lon: f64,
) -> Result<SolarPos, SpaError> {
if -90.0 > lat || 90.0 < lat || -180.0 > lon || 180.0 < lon {
return Err(SpaError::BadParam);
}
let decimal_hours =
(utc.hour() as f64) + ((utc.minute() as f64) + (utc.second() as f64) / 60.0) / 60.0;
let elapsed_julian_days = to_julian(utc) - JD2000;
let (ecliptic_longitude, ecliptic_obliquity) = {
let omega = 2.1429 - (0.0010394594 * elapsed_julian_days);
let mean_longitude = 4.8950630 + (0.017202791698 * elapsed_julian_days); let mean_anomaly = 6.2400600 + (0.0172019699 * elapsed_julian_days);
let ecliptic_longitude = mean_longitude
+ 0.03341607 * F::sin(mean_anomaly)
+ 0.00034894 * F::sin(2.0 * mean_anomaly)
- 0.0001134
- 0.0000203 * F::sin(omega);
let ecliptic_obliquity =
0.4090928 - 6.2140e-9 * elapsed_julian_days + 0.0000396 * F::cos(omega);
(ecliptic_longitude, ecliptic_obliquity)
};
let (declination, right_ascension) = {
let sin_ecliptic_longitude = F::sin(ecliptic_longitude);
let dy = F::cos(ecliptic_obliquity) * sin_ecliptic_longitude;
let dx = F::cos(ecliptic_longitude);
let mut right_ascension = F::atan2(dy, dx);
if right_ascension < 0.0 {
right_ascension = right_ascension + PI2;
}
let declination = F::asin(F::sin(ecliptic_obliquity) * sin_ecliptic_longitude);
(declination, right_ascension)
};
let (azimuth, zenith_angle) = {
let greenwich_mean_sidereal_time =
6.6974243242 + 0.0657098283 * elapsed_julian_days + decimal_hours;
let local_mean_sidereal_time = (greenwich_mean_sidereal_time * 15.0 + lon) * RAD;
let hour_angle = local_mean_sidereal_time - right_ascension;
let latitude_in_radians = lat * RAD;
let cos_latitude = F::cos(latitude_in_radians);
let sin_latitude = F::sin(latitude_in_radians);
let cos_hour_angle = F::cos(hour_angle);
let mut zenith_angle = F::acos(
cos_latitude * cos_hour_angle * F::cos(declination)
+ F::sin(declination) * sin_latitude,
);
let dy = -F::sin(hour_angle);
let dx = F::tan(declination) * cos_latitude - sin_latitude * cos_hour_angle;
let mut azimuth = F::atan2(dy, dx);
if azimuth < 0.0 {
azimuth = azimuth + PI2;
}
azimuth = azimuth / RAD;
let parallax = (EARTH_MEAN_RADIUS / ASTRONOMICAL_UNIT) * F::sin(zenith_angle);
zenith_angle = (zenith_angle + parallax) / RAD;
(azimuth, zenith_angle)
};
let solpos = SolarPos {
azimuth: azimuth,
zenith_angle: zenith_angle,
};
Result::Ok(solpos)
}
#[cfg(test)]
mod tests {
use chrono::{Datelike, TimeZone, Timelike, Utc};
use super::berechne_zeitgleichung;
use super::eps;
use super::in_pi;
use super::solar_position;
use super::sunrise_and_set;
use super::to_julian;
use super::to_utc;
use super::StdFloatOps;
use super::SunriseAndSet;
use super::JD2000;
use super::PI2;
use core::f64::consts::FRAC_PI_2;
use core::f64::consts::PI;
const LAT_DEG: f64 = 48.1;
const LON_DEG: f64 = 11.6;
#[test]
fn test_pi() {
assert!(FRAC_PI_2 < PI);
assert!(in_pi(LAT_DEG) < PI2);
assert!(in_pi(LON_DEG) < PI2);
}
#[test]
fn test_datetime() {
let unix_time_secs: i64 = 1128057420; let utc = Utc.timestamp_nanos(unix_time_secs * (1000 * 1000 * 1000));
assert_eq!(utc.year(), 2005);
assert_eq!(utc.month(), 9);
assert_eq!(utc.day(), 30);
assert_eq!(utc.hour(), 5);
assert_eq!(utc.minute(), 17);
}
#[test]
fn test_julian_day() {
let dt = Utc.with_ymd_and_hms(2006, 8, 6, 6, 0, 0).single().unwrap();
let jd = to_julian(dt);
assert_eq!(jd, 2453953.75);
assert_eq!(jd - JD2000, 2408.75);
assert_eq!(dt, to_utc(jd));
}
#[test]
fn test_zeitgleichung() {
let exp_jd = 2453644.0;
let exp_t = 0.057467488021902803;
let exp_e = 0.40907976105657956;
let exp_d_ra = 0.18539782794253773;
let exp_dk = -0.05148602985190724;
let dt = Utc
.with_ymd_and_hms(2005, 9, 30, 12, 0, 0)
.single()
.unwrap();
let jd = to_julian(dt);
assert_eq!(exp_jd, jd);
let t = (jd - JD2000) / 36525.0;
assert_eq!(exp_t, t);
assert_eq!(exp_e, eps(t));
let (d_ra, dk) = berechne_zeitgleichung::<StdFloatOps>(t);
assert_eq!(exp_d_ra, d_ra);
assert_eq!(exp_dk, dk);
}
#[test]
fn test_sunrise_and_set() {
let dt = Utc
.with_ymd_and_hms(2005, 9, 30, 12, 0, 0)
.single()
.unwrap();
let lat = 50.0;
let lon = 10.0;
let sunriseandset = sunrise_and_set::<StdFloatOps>(dt, lat, lon).unwrap();
match sunriseandset {
SunriseAndSet::PolarDay => assert!(false),
SunriseAndSet::PolarNight => assert!(false),
SunriseAndSet::Daylight(sunrise, sunset) => {
assert_eq!(sunrise.year(), 2005);
assert_eq!(sunrise.month(), 9);
assert_eq!(sunrise.day(), 30);
assert_eq!(sunrise.hour(), 5);
assert_eq!(sunrise.minute(), 17);
assert_eq!(sunset.year(), 2005);
assert_eq!(sunset.month(), 9);
assert_eq!(sunset.day(), 30);
assert_eq!(sunset.hour(), 16);
assert_eq!(sunset.minute(), 59);
}
}
}
#[test]
fn test_sunrise_and_set_polarday() {
let dt = Utc
.with_ymd_and_hms(2005, 6, 30, 12, 0, 0)
.single()
.unwrap();
let lat = 67.4;
let lon = 10.0;
let sunriseandset = sunrise_and_set::<StdFloatOps>(dt, lat, lon).unwrap();
match sunriseandset {
SunriseAndSet::PolarDay => assert!(true),
SunriseAndSet::PolarNight => assert!(false),
_ => assert!(false),
}
}
#[test]
fn test_sunrise_and_set_polarnight() {
let dt = Utc
.with_ymd_and_hms(2005, 6, 30, 12, 0, 0)
.single()
.unwrap();
let lat = -68.0;
let lon = 10.0;
let sunriseandset = sunrise_and_set::<StdFloatOps>(dt, lat, lon).unwrap();
match sunriseandset {
SunriseAndSet::PolarDay => assert!(false),
SunriseAndSet::PolarNight => assert!(true),
_ => assert!(false),
}
}
#[test]
fn test_solar_position() {
let exp_azimuth = 195.51003782406534;
let exp_zenith_angle = 54.03653683638118;
let dt = Utc
.with_ymd_and_hms(2005, 9, 30, 12, 0, 0)
.single()
.unwrap();
let lat = 50.0;
let lon = 10.0;
let solpos = solar_position::<StdFloatOps>(dt, lat, lon).unwrap();
assert_eq!(exp_azimuth, solpos.azimuth);
assert_eq!(exp_zenith_angle, solpos.zenith_angle);
}
}