use std::time::Duration;
use uuid::Uuid;
use crate::base::{
block::{Block, BlockDesc, BlockProps, BlockState},
input::{InputProps, input_reader::InputReader},
output::Output,
};
use crate::blocks::utils::{input_as_number, input_as_number_in};
use crate::tokio_impl::sleep::current_time_millis;
use libhaystack::units::units_generated::MINUTE;
use libhaystack::val::{Bool, kind::HaystackKind};
use crate::{blocks::InputImpl, blocks::OutputImpl};
#[block]
#[derive(BlockProps, Debug)]
#[category = "time"]
pub struct Sun {
#[input(kind = "Number")]
pub lat: InputImpl,
#[input(kind = "Number")]
pub lon: InputImpl,
#[input(name = "tzOffset", kind = "Number")]
pub tz_offset: InputImpl,
#[output(kind = "Number")]
pub sunrise: OutputImpl,
#[output(kind = "Number")]
pub sunset: OutputImpl,
#[output(name = "isDay", kind = "Bool")]
pub is_day: OutputImpl,
initialized: bool,
}
impl Block for Sun {
async fn execute(&mut self) {
if self.initialized {
let now_ms = current_time_millis() as i64;
let until_next_minute_ms = (60_000 - now_ms.rem_euclid(60_000)) as u64;
self.wait_on_inputs(Duration::from_millis(until_next_minute_ms))
.await;
}
let lat = match input_as_number(&self.lat) {
Some(n) => n.value,
None => {
self.wait_on_inputs(Duration::from_millis(250)).await;
return;
}
};
let lon = match input_as_number(&self.lon) {
Some(n) => n.value,
None => {
self.wait_on_inputs(Duration::from_millis(250)).await;
return;
}
};
let tz_offset_min = input_as_number_in(&self.tz_offset, &MINUTE)
.map(|v| v as i64)
.unwrap_or(0);
let now_ms = current_time_millis() as i64;
let local_secs = (now_ms + tz_offset_min * 60_000).div_euclid(1000);
let now_minutes = (local_secs.rem_euclid(86_400) / 60) as f64;
match compute_sun_times(now_ms, lat, lon, tz_offset_min) {
SunState::Normal {
rise_minutes,
set_minutes,
} => {
self.sunrise.set(rise_minutes.into());
self.sunset.set(set_minutes.into());
let is_day = if rise_minutes <= set_minutes {
now_minutes >= rise_minutes && now_minutes < set_minutes
} else {
now_minutes >= rise_minutes || now_minutes < set_minutes
};
self.is_day.set(Bool { value: is_day }.into());
}
SunState::PolarDay => {
self.is_day.set(Bool { value: true }.into());
}
SunState::PolarNight => {
self.is_day.set(Bool { value: false }.into());
}
}
self.initialized = true;
}
}
#[derive(Debug, PartialEq)]
enum SunState {
Normal { rise_minutes: f64, set_minutes: f64 },
PolarDay,
PolarNight,
}
const DAYS_1970_TO_2000: i64 = 10_957;
fn compute_sun_times(now_utc_ms: i64, lat_deg: f64, lon_deg: f64, tz_offset_min: i64) -> SunState {
let local_secs = (now_utc_ms + tz_offset_min * 60_000).div_euclid(1000);
let local_days = local_secs.div_euclid(86_400);
let n = (local_days - DAYS_1970_TO_2000) as f64 + 0.0008;
let j_star = n - lon_deg / 360.0;
let m_deg = (357.5291 + 0.985_600_28 * j_star).rem_euclid(360.0);
let m_rad = m_deg.to_radians();
let c_deg = 1.9148 * m_rad.sin() + 0.0200 * (2.0 * m_rad).sin() + 0.0003 * (3.0 * m_rad).sin();
let lambda_rad = (m_deg + c_deg + 180.0 + 102.9372)
.rem_euclid(360.0)
.to_radians();
let j_transit = j_star + 0.0053 * m_rad.sin() - 0.0069 * (2.0 * lambda_rad).sin();
let sin_decl = lambda_rad.sin() * 23.44_f64.to_radians().sin();
let cos_decl = (1.0 - sin_decl * sin_decl).sqrt();
let lat_rad = lat_deg.to_radians();
let cos_omega =
((-0.83_f64).to_radians().sin() - lat_rad.sin() * sin_decl) / (lat_rad.cos() * cos_decl);
if cos_omega > 1.0 {
return SunState::PolarNight;
}
if cos_omega < -1.0 {
return SunState::PolarDay;
}
let omega_frac = cos_omega.acos() / std::f64::consts::TAU;
let j_rise_n = j_transit - omega_frac;
let j_set_n = j_transit + omega_frac;
let to_minutes = |jn: f64| -> f64 {
let unix_days = jn + 10_957.5;
let local_days_frac = unix_days + tz_offset_min as f64 / 1440.0;
local_days_frac.rem_euclid(1.0) * 1440.0
};
SunState::Normal {
rise_minutes: to_minutes(j_rise_n),
set_minutes: to_minutes(j_set_n),
}
}
#[cfg(test)]
mod test {
use super::{SunState, compute_sun_times};
const EQUINOX_2024_MS: i64 = 19_802 * 86_400_000;
const SOLSTICE_2024_MS: i64 = 19_894 * 86_400_000;
fn assert_close(actual: f64, expected: f64, tol_minutes: f64, label: &str) {
assert!(
(actual - expected).abs() <= tol_minutes,
"{}: expected {} ± {}, got {}",
label,
expected,
tol_minutes,
actual
);
}
#[test]
fn test_sun_equator_equinox_is_about_six_to_six() {
let s = compute_sun_times(EQUINOX_2024_MS + 12 * 3_600_000, 0.0, 0.0, 0);
match s {
SunState::Normal {
rise_minutes,
set_minutes,
} => {
assert_close(rise_minutes, 6.0 * 60.0, 15.0, "equator equinox sunrise");
assert_close(set_minutes, 18.0 * 60.0, 15.0, "equator equinox sunset");
}
_ => panic!("expected Normal, got {:?}", s),
}
}
#[test]
fn test_sun_nyc_solstice() {
let s = compute_sun_times(SOLSTICE_2024_MS + 12 * 3_600_000, 40.7128, -74.0060, -240);
match s {
SunState::Normal {
rise_minutes,
set_minutes,
} => {
assert_close(
rise_minutes,
5.0 * 60.0 + 25.0,
20.0,
"NYC solstice sunrise",
);
assert_close(set_minutes, 20.0 * 60.0 + 31.0, 20.0, "NYC solstice sunset");
}
_ => panic!("expected Normal, got {:?}", s),
}
}
#[test]
fn test_sun_polar_day_in_arctic_summer() {
let s = compute_sun_times(SOLSTICE_2024_MS + 12 * 3_600_000, 80.0, 0.0, 0);
assert_eq!(s, SunState::PolarDay);
}
#[test]
fn test_sun_polar_night_in_arctic_winter() {
let winter_ms = 20_078_i64 * 86_400_000 + 12 * 3_600_000;
let s = compute_sun_times(winter_ms, 80.0, 0.0, 0);
assert_eq!(s, SunState::PolarNight);
}
#[test]
fn test_sun_day_length_decreases_toward_winter_in_north() {
let summer = compute_sun_times(SOLSTICE_2024_MS + 12 * 3_600_000, 50.0, 0.0, 0);
let winter_ms = 20_078_i64 * 86_400_000 + 12 * 3_600_000;
let winter = compute_sun_times(winter_ms, 50.0, 0.0, 0);
let summer_len = match summer {
SunState::Normal {
rise_minutes,
set_minutes,
} => set_minutes - rise_minutes,
_ => panic!("expected Normal in summer"),
};
let winter_len = match winter {
SunState::Normal {
rise_minutes,
set_minutes,
} => set_minutes - rise_minutes,
_ => panic!("expected Normal in winter"),
};
assert!(
summer_len > winter_len,
"summer day ({} min) should be longer than winter ({} min)",
summer_len,
winter_len
);
}
}