1use chrono::{DateTime, Utc};
2use chrono_tz::Tz;
3use spa::{solar_position, SpaError, StdFloatOps};
4use crate::location::Location;
5
6use std::f64::consts::PI;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct SolarPosition {
11 pub zenith: f64,
13 pub azimuth: f64,
15 pub elevation: f64,
17}
18
19pub fn get_solarposition(location: &Location, time: DateTime<Tz>) -> Result<SolarPosition, SpaError> {
22 let utc_time: DateTime<Utc> = time.with_timezone(&Utc);
23
24 let result = solar_position::<StdFloatOps>(utc_time, location.latitude, location.longitude)?;
25
26 Ok(SolarPosition {
27 zenith: result.zenith_angle,
28 azimuth: result.azimuth,
29 elevation: 90.0 - result.zenith_angle,
30 })
31}
32
33fn calculate_simple_day_angle(day_of_year: f64, offset: f64) -> f64 {
38 (2.0 * PI / 365.0) * (day_of_year - offset)
39}
40
41pub fn equation_of_time_spencer71(day_of_year: f64) -> f64 {
47 let day_angle = calculate_simple_day_angle(day_of_year, 1.0);
48 (1440.0 / (2.0 * PI))
49 * (0.0000075
50 + 0.001868 * day_angle.cos()
51 - 0.032077 * day_angle.sin()
52 - 0.014615 * (2.0 * day_angle).cos()
53 - 0.040849 * (2.0 * day_angle).sin())
54}
55
56pub fn equation_of_time_pvcdrom(day_of_year: f64) -> f64 {
60 let bday = calculate_simple_day_angle(day_of_year, 1.0) - (2.0 * PI / 365.0) * 80.0;
61 9.87 * (2.0 * bday).sin() - 7.53 * bday.cos() - 1.5 * bday.sin()
62}
63
64pub fn declination_spencer71(day_of_year: f64) -> f64 {
71 let day_angle = calculate_simple_day_angle(day_of_year, 1.0);
72 0.006918
73 - 0.399912 * day_angle.cos()
74 + 0.070257 * day_angle.sin()
75 - 0.006758 * (2.0 * day_angle).cos()
76 + 0.000907 * (2.0 * day_angle).sin()
77 - 0.002697 * (3.0 * day_angle).cos()
78 + 0.00148 * (3.0 * day_angle).sin()
79}
80
81pub fn declination_cooper69(day_of_year: f64) -> f64 {
85 let day_angle = calculate_simple_day_angle(day_of_year, 1.0);
86 (23.45_f64).to_radians() * (day_angle + (2.0 * PI / 365.0) * 285.0).sin()
87}
88
89pub fn hour_angle(hours_from_midnight_utc: f64, longitude: f64, equation_of_time: f64) -> f64 {
98 15.0 * (hours_from_midnight_utc - 12.0) + longitude + equation_of_time / 4.0
99}
100
101pub fn solar_zenith_analytical(latitude: f64, hour_angle: f64, declination: f64) -> f64 {
107 let cos_zenith = declination.cos() * latitude.cos() * hour_angle.cos()
108 + declination.sin() * latitude.sin();
109 cos_zenith.clamp(-1.0, 1.0).acos()
111}
112
113pub fn solar_azimuth_analytical(
117 latitude: f64,
118 hour_angle: f64,
119 declination: f64,
120 zenith: f64,
121) -> f64 {
122 let numer = zenith.cos() * latitude.sin() - declination.sin();
123 let denom = zenith.sin() * latitude.cos();
124
125 let mut cos_azi = if denom.abs() < 1e-8 {
126 1.0
127 } else {
128 numer / denom
129 };
130
131 if (cos_azi - 1.0).abs() < 1e-8 {
133 cos_azi = 1.0;
134 }
135 if (cos_azi + 1.0).abs() < 1e-8 {
136 cos_azi = -1.0;
137 }
138 cos_azi = cos_azi.clamp(-1.0, 1.0);
139
140 let sign_ha = if hour_angle > 0.0 {
141 1.0
142 } else if hour_angle < 0.0 {
143 -1.0
144 } else {
145 0.0
146 };
147
148 sign_ha * cos_azi.acos() + PI
149}
150
151#[derive(Debug, Clone, PartialEq)]
155pub struct SunRiseSetTransit {
156 pub sunrise: f64,
158 pub sunset: f64,
160 pub transit: f64,
162}
163
164pub fn sun_rise_set_transit_geometric(
175 latitude: f64,
176 longitude: f64,
177 declination: f64,
178 equation_of_time: f64,
179 utc_offset_hours: f64,
180) -> Option<SunRiseSetTransit> {
181 let latitude_rad = latitude.to_radians();
182 let tan_product = -declination.tan() * latitude_rad.tan();
183
184 if tan_product.abs() > 1.0 {
186 return None;
187 }
188
189 let sunset_angle = tan_product.acos().to_degrees(); let sunrise_angle = -sunset_angle;
191
192 let sunrise_hour =
194 (sunrise_angle - longitude - equation_of_time / 4.0) / 15.0 + 12.0 + utc_offset_hours;
195 let sunset_hour =
196 (sunset_angle - longitude - equation_of_time / 4.0) / 15.0 + 12.0 + utc_offset_hours;
197 let transit_hour =
198 (0.0 - longitude - equation_of_time / 4.0) / 15.0 + 12.0 + utc_offset_hours;
199
200 Some(SunRiseSetTransit {
201 sunrise: sunrise_hour,
202 sunset: sunset_hour,
203 transit: transit_hour,
204 })
205}
206
207pub fn nrel_earthsun_distance(day_of_year: f64) -> f64 {
213 let day_angle = calculate_simple_day_angle(day_of_year, 1.0);
214 let r_over_r0_squared = 1.000110
215 + 0.034221 * day_angle.cos()
216 + 0.001280 * day_angle.sin()
217 + 0.000719 * (2.0 * day_angle).cos()
218 + 0.000077 * (2.0 * day_angle).sin();
219 1.0 / r_over_r0_squared.sqrt()
221}