source_map_gen/light/
time.rs

1use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
2use rgb::RGB;
3use spa::{SolarPos, SpaError};
4
5use crate::{
6    // light::{pitch_to_rgb, Angles, GlobalLighting},
7    source::{ColorBrightness}, light::{GlobalLighting, pitch_to_rgb}, map::Angles,
8};
9
10// lat
11// long
12// localdate
13// localtime
14
15// TODO: need:
16// r/g/b or temp over time
17// actually working temp to rgb
18// amb color over time
19// brightness overtime
20
21// TODO: solarpos to angles
22// TODO: to global light thingy
23// TODO: clouds/fire/adjust
24
25// TODO: no consensus start/end/middle of seasons
26// TODO: dateext
27// march 24.5
28// june 25
29// sep 25
30// dec 24.5
31
32impl Angles {
33    // Assumes +Y is north.
34    // seems good, checked in hammer and irl (scary)
35    pub(crate) fn from_solar_pos(pos: SolarPos) -> Self {
36        let pitch = pos.zenith_angle - 90.0;
37        // angle right from north (azimuth) to angle left from +X/east (yaw)
38        // rem_euclid() means slam into 0..360 range
39        let yaw = (270.0 - pos.azimuth).rem_euclid(360.0);
40        let roll = 0.0;
41        Angles { pitch, yaw, roll }
42    }
43}
44
45pub trait DateTimeUtcExt {
46    /// Calculates the timezone offset to a second from a longitude east, and
47    /// uses that as the offset for a [`DateTime<Utc>`].
48    fn from_longitude(longitude_east: f64, datetime: NaiveDateTime) -> Option<DateTime<Utc>> {
49        let offset = lon_to_offset(longitude_east)?;
50        let datetime_fixed = DateTime::<FixedOffset>::from_local(datetime, offset);
51        Some(datetime_fixed.into())
52    }
53}
54
55impl DateTimeUtcExt for DateTime<Utc> {}
56
57pub trait NaiveDateTimeExt {
58    fn from_season(season: Season, time: NaiveTime) -> Option<NaiveDateTime> {
59        let year = 2023;
60        let (month, day) = match season {
61            Season::Spring => (3, 24),  // March 24.5
62            Season::Summer => (6, 25),  // June 25
63            Season::Fall => (9, 25),    // Sep 25
64            Season::Winter => (12, 24), // Dec 24/5
65        };
66        Some(NaiveDate::from_ymd_opt(year, month, day)?.and_time(time))
67    }
68}
69
70impl NaiveDateTimeExt for NaiveDateTime {}
71
72#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
73pub enum Season {
74    Spring,
75    Summer,
76    Fall,
77    Winter,
78}
79
80/// Outputs closest `FixedOffset` "timezone" to the second.
81/// Longitudes outside of the bounds -180 to 180 give erroneous results.
82pub(crate) fn lon_to_offset(longitude_east: f64) -> Option<FixedOffset> {
83    let tz_hour = longitude_east / 15.0;
84    let tz_secs = (tz_hour * 3600.0).round() as i32;
85    // casting f64 to i32 truncates and does clamping and stuff
86    FixedOffset::east_opt(tz_secs)
87}
88
89// TODO: SolarPos to
90/// Calculate the sun position from world position and a local date and time.
91pub fn calc_solar_position_local(
92    lat: f64,
93    lon: f64,
94    datetime: NaiveDateTime,
95) -> Result<SolarPos, SpaError> {
96    let utc = DateTime::from_longitude(lon, datetime).ok_or(SpaError::BadParam)?;
97    spa::calc_solar_position(utc, lat, lon)
98}
99// TODO:LOC: TODO: make a lot better
100/// Get the mao lighting for a location and time
101pub fn loc_time_to_sun(
102    lat: f64,
103    lon: f64,
104    datetime: NaiveDateTime,
105) -> Result<GlobalLighting, SpaError> {
106    let solar_pos = calc_solar_position_local(lat, lon, datetime)?;
107    let mut sun_dir = Angles::from_solar_pos(solar_pos);
108    dbg!(sun_dir.pitch);
109    let RGB { r, g, b } = pitch_to_rgb(-sun_dir.pitch);
110    sun_dir.pitch = -sun_dir.pitch.abs();
111
112    Ok(GlobalLighting {
113        sun_color: ColorBrightness::new(r, g, b, 255), // TODO: brightness
114        sun_dir: sun_dir.clone(),
115        amb_color: ColorBrightness::new(171, 206, 220, 50), // default l4d2
116        amb_dir: sun_dir,
117        dir_lights: Vec::new(),
118    })
119}
120
121// Property::new("origin", "0 0 0"),
122// Property::new("SunSpreadAngle", "0"),
123// Property::new("pitch", "-14"),
124// Property::new("angles", "0 30 0"),
125// Property::new("_lightscaleHDR", "1"),
126// Property::new("_lightHDR", "-1 -1 -1 1"),
127// Property::new("_light", "228 215 192 400"),
128// Property::new("_AmbientScaleHDR", "1"),
129// Property::new("_ambientHDR", "-1 -1 -1 1"),
130// Property::new("_ambient", "171 206 220 50"),
131// Property::new("classname", "light_environment"),
132
133#[cfg(test)]
134mod tests {
135    use approx::assert_relative_eq;
136
137    use super::*;
138
139    #[test]
140    fn loc_time() {
141        let datetime =
142            NaiveDate::from_ymd_opt(2023, 4, 21).unwrap().and_hms_opt(14, 42, 0).unwrap();
143        // let datetime = chrono::Local::now().naive_local();
144        println!("datetime: {}", datetime);
145        // Las Vegas
146        let lighting = loc_time_to_sun(36.188110, -115.176468, datetime).unwrap();
147        let dir = &lighting.sun_dir;
148        dbg!(dir);
149        // verified in hammer/l4d2 and irl (scary)
150        assert_relative_eq!(-46.0, dir.pitch, epsilon = 3.0);
151        assert_relative_eq!(22.0, dir.yaw, epsilon = 3.0);
152        assert_relative_eq!(0.0, dir.roll);
153    }
154}