diff --git a/src/blocks/weather.rs b/src/blocks/weather.rs index 5373ca0d8e..4ce61ab04e 100644 --- a/src/blocks/weather.rs +++ b/src/blocks/weather.rs @@ -179,14 +179,25 @@ pub enum WeatherService { Nws(nws::Config), } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] enum WeatherIcon { - Clear { is_night: bool }, - Clouds { is_night: bool }, - Fog { is_night: bool }, - Rain { is_night: bool }, + Clear { + is_night: bool, + }, + Clouds { + is_night: bool, + }, + Fog { + is_night: bool, + }, + Rain { + is_night: bool, + }, Snow, - Thunder { is_night: bool }, + Thunder { + is_night: bool, + }, + #[default] Default, } @@ -209,23 +220,7 @@ impl WeatherIcon { } } -#[derive(Debug)] -struct Wind { - speed: f64, - degrees: Option, -} - -impl PartialEq for Wind { - fn eq(&self, other: &Self) -> bool { - (self.speed - other.speed).abs() < 0.001 - && match (self.degrees, other.degrees) { - (Some(degrees0), Some(degrees1)) => (degrees0 - degrees1).abs() < 0.001, - (None, None) => true, - _ => false, - } - } -} - +#[derive(Default)] struct WeatherMoment { icon: WeatherIcon, weather: String, @@ -237,6 +232,7 @@ struct WeatherMoment { wind_kmh: f64, wind_direction: Option, } + struct ForecastAggregate { temp: f64, apparent: f64, @@ -246,19 +242,21 @@ struct ForecastAggregate { wind_direction: Option, } +struct ForecastAggregateSegment { + temp: Option, + apparent: Option, + humidity: Option, + wind: Option, + wind_kmh: Option, + wind_direction: Option, +} + struct WeatherResult { location: String, current_weather: WeatherMoment, forecast: Option, } -struct Forecast { - avg: ForecastAggregate, - min: ForecastAggregate, - max: ForecastAggregate, - fin: WeatherMoment, -} - impl WeatherResult { fn into_values(self) -> Values { let mut values = map! { @@ -307,6 +305,118 @@ impl WeatherResult { } } +struct Forecast { + avg: ForecastAggregate, + min: ForecastAggregate, + max: ForecastAggregate, + fin: WeatherMoment, +} + +impl Forecast { + fn new(data: &[ForecastAggregateSegment], fin: WeatherMoment) -> Self { + let mut temp_avg = 0.0; + let mut temp_count = 0.0; + let mut apparent_avg = 0.0; + let mut apparent_count = 0.0; + let mut humidity_avg = 0.0; + let mut humidity_count = 0.0; + let mut wind_north_avg = 0.0; + let mut wind_east_avg = 0.0; + let mut wind_kmh_north_avg = 0.0; + let mut wind_kmh_east_avg = 0.0; + let mut wind_count = 0.0; + let mut max = ForecastAggregate { + temp: f64::MIN, + apparent: f64::MIN, + humidity: f64::MIN, + wind: f64::MIN, + wind_kmh: f64::MIN, + wind_direction: None, + }; + let mut min = ForecastAggregate { + temp: f64::MAX, + apparent: f64::MAX, + humidity: f64::MAX, + wind: f64::MAX, + wind_kmh: f64::MAX, + wind_direction: None, + }; + for val in data { + if let Some(temp) = val.temp { + temp_avg += temp; + max.temp = max.temp.max(temp); + min.temp = min.temp.min(temp); + temp_count += 1.0; + } + if let Some(apparent) = val.apparent { + apparent_avg += apparent; + max.apparent = max.apparent.max(apparent); + min.apparent = min.apparent.min(apparent); + apparent_count += 1.0; + } + if let Some(humidity) = val.humidity { + humidity_avg += humidity; + max.humidity = max.humidity.max(humidity); + min.humidity = min.humidity.min(humidity); + humidity_count += 1.0; + } + + if let (Some(wind), Some(wind_kmh)) = (val.wind, val.wind_kmh) { + if let Some(degrees) = val.wind_direction { + let (sin, cos) = degrees.to_radians().sin_cos(); + wind_north_avg += wind * cos; + wind_east_avg += wind * sin; + wind_kmh_north_avg += wind_kmh * cos; + wind_kmh_east_avg += wind_kmh * sin; + wind_count += 1.0; + } + + if wind > max.wind { + max.wind_direction = val.wind_direction; + max.wind = wind; + max.wind_kmh = wind_kmh; + } + + if wind < min.wind { + min.wind_direction = val.wind_direction; + min.wind = wind; + min.wind_kmh = wind_kmh; + } + } + } + + temp_avg /= temp_count; + humidity_avg /= humidity_count; + apparent_avg /= apparent_count; + + // Calculate the wind results separately, discarding invalid wind values + let (wind_avg, wind_kmh_avg, wind_direction_avg) = if wind_count == 0.0 { + (0.0, 0.0, None) + } else { + ( + wind_east_avg.hypot(wind_north_avg) / wind_count, + wind_kmh_east_avg.hypot(wind_kmh_north_avg) / wind_count, + Some( + wind_east_avg + .atan2(wind_north_avg) + .to_degrees() + .rem_euclid(360.0), + ), + ) + }; + + let avg = ForecastAggregate { + temp: temp_avg, + apparent: apparent_avg, + humidity: humidity_avg, + wind: wind_avg, + wind_kmh: wind_kmh_avg, + wind_direction: wind_direction_avg, + }; + Self { avg, min, max, fin } + } +} + pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let mut actions = api.get_actions()?; api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?; @@ -490,32 +600,6 @@ fn convert_wind_direction(direction_opt: Option) -> &'static str { } } -// Compute the average wind speed and direction -fn average_wind(winds: &[Wind]) -> Wind { - let mut north = 0.0; - let mut east = 0.0; - let mut count = 0.0; - for wind in winds { - if let Some(degrees) = wind.degrees { - let (sin, cos) = degrees.to_radians().sin_cos(); - north += wind.speed * cos; - east += wind.speed * sin; - count += 1.0; - } - } - if count == 0.0 { - Wind { - speed: 0.0, - degrees: None, - } - } else { - Wind { - speed: east.hypot(north) / count, - degrees: Some(east.atan2(north).to_degrees().rem_euclid(360.0)), - } - } -} - /// Compute the Australian Apparent Temperature from metric units fn australian_apparent_temp(temp: f64, humidity: f64, wind_speed: f64) -> f64 { let exponent = 17.27 * temp / (237.7 + temp); @@ -528,77 +612,106 @@ mod tests { use super::*; #[test] - fn test_average_wind_speed() { + fn test_new_forecast_average_wind_speed() { let mut degrees = 0.0; while degrees < 360.0 { - let averaged = average_wind(&[ - Wind { - speed: 1.0, - degrees: Some(degrees), - }, - Wind { - speed: 2.0, - degrees: Some(degrees), - }, - ]); - assert_eq!( - averaged, - Wind { - speed: 1.5, - degrees: Some(degrees) - } + let forecast = Forecast::new( + &[ + ForecastAggregateSegment { + temp: None, + apparent: None, + humidity: None, + wind: Some(1.0), + wind_kmh: Some(3.6), + wind_direction: Some(degrees), + }, + ForecastAggregateSegment { + temp: None, + apparent: None, + humidity: None, + wind: Some(2.0), + wind_kmh: Some(7.2), + wind_direction: Some(degrees), + }, + ], + WeatherMoment::default(), ); + assert!((forecast.avg.wind - 1.5).abs() < 0.1); + assert!((forecast.avg.wind_kmh - 5.4).abs() < 0.1); + assert!((forecast.avg.wind_direction.unwrap() - degrees).abs() < 0.1); degrees += 15.0; } } #[test] - fn test_average_wind_degrees() { + fn test_new_forecast_average_wind_degrees() { let mut degrees = 0.0; while degrees < 360.0 { let low = degrees - 1.0; let high = degrees + 1.0; - let averaged = average_wind(&[ - Wind { - speed: 1.0, - degrees: Some(low), - }, - Wind { - speed: 1.0, - degrees: Some(high), - }, - ]); + let forecast = Forecast::new( + &[ + ForecastAggregateSegment { + temp: None, + apparent: None, + humidity: None, + wind: Some(1.0), + wind_kmh: Some(3.6), + wind_direction: Some(low), + }, + ForecastAggregateSegment { + temp: None, + apparent: None, + humidity: None, + wind: Some(1.0), + wind_kmh: Some(3.6), + wind_direction: Some(high), + }, + ], + WeatherMoment::default(), + ); // For winds of equal strength the direction should will be the // average of the low and high degrees - assert!((averaged.degrees.unwrap() - degrees).abs() < 0.1); + assert!((forecast.avg.wind_direction.unwrap() - degrees).abs() < 0.1); degrees += 15.0; } } #[test] - fn test_average_wind_speed_and_degrees() { + fn test_new_forecast_average_wind_speed_and_degrees() { let mut degrees = 0.0; while degrees < 360.0 { let low = degrees - 1.0; let high = degrees + 1.0; - let averaged = average_wind(&[ - Wind { - speed: 1.0, - degrees: Some(low), - }, - Wind { - speed: 2.0, - degrees: Some(high), - }, - ]); + let forecast = Forecast::new( + &[ + ForecastAggregateSegment { + temp: None, + apparent: None, + humidity: None, + wind: Some(1.0), + wind_kmh: Some(3.6), + wind_direction: Some(low), + }, + ForecastAggregateSegment { + temp: None, + apparent: None, + humidity: None, + wind: Some(2.0), + wind_kmh: Some(7.2), + wind_direction: Some(high), + }, + ], + WeatherMoment::default(), + ); // Wind degree will be higher than the centerpoint of the low // and high winds since the high wind is stronger and will be // less than high // (low+high)/2 < average.degrees < high - assert!((low + high) / 2.0 < averaged.degrees.unwrap()); - assert!(averaged.degrees.unwrap() < high); + assert!((low + high) / 2.0 < forecast.avg.wind_direction.unwrap()); + assert!(forecast.avg.wind_direction.unwrap() < high); degrees += 15.0; } } diff --git a/src/blocks/weather/met_no.rs b/src/blocks/weather/met_no.rs index eacf704cca..f670b33af3 100644 --- a/src/blocks/weather/met_no.rs +++ b/src/blocks/weather/met_no.rs @@ -26,41 +26,16 @@ impl<'a> Service<'a> { }) } - fn get_weather_instant(&self, forecast_data: &ForecastData) -> WeatherMoment { - let instant = &forecast_data.instant.details; - - let mut symbol_code_split = forecast_data - .next_1_hours - .as_ref() - .unwrap() - .summary - .symbol_code - .split('_'); - - let summary = symbol_code_split.next().unwrap(); - - // Times of day can be day, night, and polartwilight - let is_night = symbol_code_split - .next() - .map_or(false, |time_of_day| time_of_day == "night"); - - let translated = translate(self.legend, summary, &self.config.lang); - - let temp = instant.air_temperature.unwrap_or_default(); - let humidity = instant.relative_humidity.unwrap_or_default(); - let wind_speed = instant.wind_speed.unwrap_or_default(); - - WeatherMoment { - temp, - apparent: australian_apparent_temp(temp, humidity, wind_speed), - humidity, - weather: translated.clone(), - weather_verbose: translated, - wind: wind_speed, - wind_kmh: wind_speed * 3.6, - wind_direction: instant.wind_from_direction, - icon: weather_to_icon(summary, is_night), - } + fn translate(&self, summary: &str) -> String { + self.legend + .get(summary) + .map(|res| match self.config.lang { + ApiLanguage::English => res.desc_en.as_str(), + ApiLanguage::NorwegianBokmaal => res.desc_nb.as_str(), + ApiLanguage::NorwegianNynorsk => res.desc_nn.as_str(), + }) + .unwrap_or(summary) + .into() } } @@ -98,6 +73,73 @@ struct ForecastTimeStep { // time: String, } +impl ForecastTimeStep { + fn to_moment(&self, service: &Service) -> WeatherMoment { + let instant = &self.data.instant.details; + + let mut symbol_code_split = self + .data + .next_1_hours + .as_ref() + .unwrap() + .summary + .symbol_code + .split('_'); + + let summary = symbol_code_split.next().unwrap(); + + // Times of day can be day, night, and polartwilight + let is_night = symbol_code_split + .next() + .map_or(false, |time_of_day| time_of_day == "night"); + + let translated = service.translate(summary); + + let temp = instant.air_temperature.unwrap_or_default(); + let humidity = instant.relative_humidity.unwrap_or_default(); + let wind_speed = instant.wind_speed.unwrap_or_default(); + + WeatherMoment { + temp, + apparent: australian_apparent_temp(temp, humidity, wind_speed), + humidity, + weather: translated.clone(), + weather_verbose: translated, + wind: wind_speed, + wind_kmh: wind_speed * 3.6, + wind_direction: instant.wind_from_direction, + icon: weather_to_icon(summary, is_night), + } + } + + fn to_aggregate(&self) -> ForecastAggregateSegment { + let instant = &self.data.instant.details; + + let apparent = if let (Some(air_temperature), Some(relative_humidity), Some(wind_speed)) = ( + instant.air_temperature, + instant.relative_humidity, + instant.wind_speed, + ) { + Some(australian_apparent_temp( + air_temperature, + relative_humidity, + wind_speed, + )) + } else { + None + }; + + ForecastAggregateSegment { + temp: instant.air_temperature, + apparent, + humidity: instant.relative_humidity, + wind: instant.wind_speed, + wind_kmh: instant.wind_speed.map(|t| t * 3.6), + wind_direction: instant.wind_from_direction, + } + } +} + #[derive(Deserialize, Debug)] struct ForecastData { instant: ForecastModelInstant, @@ -134,18 +176,6 @@ static LEGENDS: Lazy> = const FORECAST_URL: &str = "https://api.met.no/weatherapi/locationforecast/2.0/compact"; -fn translate(legend: &LegendsStore, summary: &str, lang: &ApiLanguage) -> String { - legend - .get(summary) - .map(|res| match lang { - ApiLanguage::English => res.desc_en.as_str(), - ApiLanguage::NorwegianBokmaal => res.desc_nb.as_str(), - ApiLanguage::NorwegianNynorsk => res.desc_nn.as_str(), - }) - .unwrap_or(summary) - .into() -} - #[async_trait] impl WeatherProvider for Service<'_> { async fn get_weather( @@ -177,116 +207,39 @@ impl WeatherProvider for Service<'_> { .error("Forecast request failed")?; let forecast_hours = self.config.forecast_hours; + let location_name = location.map_or("Unknown".to_string(), |c| c.city.clone()); - let forecast = if !need_forecast || forecast_hours == 0 { - None - } else { - let mut temp_avg = 0.0; - let mut temp_min = f64::MAX; - let mut temp_max = f64::MIN; - let mut temp_count = 0.0; - let mut humidity_avg = 0.0; - let mut humidity_min = f64::MAX; - let mut humidity_max = f64::MIN; - let mut humidity_count = 0.0; - let mut wind_forecasts = Vec::new(); - let mut apparent_avg = 0.0; - let mut apparent_min = f64::MAX; - let mut apparent_max = f64::MIN; - let mut apparent_count = 0.0; - if data.properties.timeseries.len() < forecast_hours { - Err(Error::new( + let current_weather = data.properties.timeseries.first().unwrap().to_moment(self); + + if !need_forecast || forecast_hours == 0 { + return Ok(WeatherResult { + location: location_name, + current_weather, + forecast: None, + }); + } + + if data.properties.timeseries.len() < forecast_hours { + return Err(Error::new( format!("Unable to fetch the specified number of forecast_hours specified {}, only {} hours available", forecast_hours, data.properties.timeseries.len()), ))?; - } - for forecast_time_step in data.properties.timeseries.iter().take(forecast_hours) { - let forecast_instant = &forecast_time_step.data.instant.details; - if let Some(air_temperature) = forecast_instant.air_temperature { - temp_avg += air_temperature; - temp_min = temp_min.min(air_temperature); - temp_max = temp_max.max(air_temperature); - temp_count += 1.0; - } - if let Some(relative_humidity) = forecast_instant.relative_humidity { - humidity_avg += relative_humidity; - humidity_min = humidity_min.min(relative_humidity); - humidity_max = humidity_max.max(relative_humidity); - humidity_count += 1.0; - } - if let Some(wind_speed) = forecast_instant.wind_speed { - wind_forecasts.push(Wind { - speed: wind_speed, - degrees: forecast_instant.wind_from_direction, - }); - } - if let (Some(air_temperature), Some(relative_humidity), Some(wind_speed)) = ( - forecast_instant.air_temperature, - forecast_instant.relative_humidity, - forecast_instant.wind_speed, - ) { - let apparent = - australian_apparent_temp(air_temperature, relative_humidity, wind_speed); - apparent_avg += apparent; - apparent_min = apparent_min.min(apparent); - apparent_max = apparent_max.max(apparent); - apparent_count += 1.0; - } - } - temp_avg /= temp_count; - humidity_avg /= humidity_count; - apparent_avg /= apparent_count; - let Wind { - speed: wind_avg, - degrees: direction_avg, - } = average_wind(&wind_forecasts); - let Wind { - speed: wind_min, - degrees: direction_min, - } = wind_forecasts - .iter() - .min_by(|x, y| x.speed.total_cmp(&y.speed)) - .error("No min wind")?; - let Wind { - speed: wind_max, - degrees: direction_max, - } = wind_forecasts - .iter() - .min_by(|x, y| x.speed.total_cmp(&y.speed)) - .error("No max wind")?; - - Some(Forecast { - avg: ForecastAggregate { - temp: temp_avg, - apparent: apparent_avg, - humidity: humidity_avg, - wind: wind_avg, - wind_kmh: wind_avg * 3.6, - wind_direction: direction_avg, - }, - min: ForecastAggregate { - temp: temp_min, - apparent: apparent_min, - humidity: humidity_min, - wind: *wind_min, - wind_kmh: wind_min * 3.6, - wind_direction: *direction_min, - }, - max: ForecastAggregate { - temp: temp_max, - apparent: apparent_max, - humidity: humidity_max, - wind: *wind_max, - wind_kmh: wind_max * 3.6, - wind_direction: *direction_max, - }, - fin: self.get_weather_instant(&data.properties.timeseries[forecast_hours - 1].data), - }) - }; + } + + let data_agg: Vec = data + .properties + .timeseries + .iter() + .take(forecast_hours) + .map(|f| f.to_aggregate()) + .collect(); + + let fin = data.properties.timeseries[forecast_hours - 1].to_moment(self); + + let forecast = Some(Forecast::new(&data_agg, fin)); Ok(WeatherResult { - location: location.map_or("Unknown".to_string(), |c| c.city.clone()), - current_weather: self - .get_weather_instant(&data.properties.timeseries.first().unwrap().data), + location: location_name, + current_weather, forecast, }) } diff --git a/src/blocks/weather/nws.rs b/src/blocks/weather/nws.rs index 7d947fe027..f221e960c7 100644 --- a/src/blocks/weather/nws.rs +++ b/src/blocks/weather/nws.rs @@ -213,102 +213,18 @@ impl ApiForecast { } } - fn to_aggregate(&self) -> ForecastAggregate { - ForecastAggregate { - temp: self.temperature.value, - apparent: self.apparent_temp(), - humidity: self.relative_humidity.value, - wind: self.wind_speed(), - wind_kmh: self.wind_kmh(), + fn to_aggregate(&self) -> ForecastAggregateSegment { + ForecastAggregateSegment { + temp: Some(self.temperature.value), + apparent: Some(self.apparent_temp()), + humidity: Some(self.relative_humidity.value), + wind: Some(self.wind_speed()), + wind_kmh: Some(self.wind_kmh()), wind_direction: self.wind_direction(), } } } -fn combine_forecasts(data: &[ForecastAggregate], fin: WeatherMoment) -> Forecast { - let mut temp = 0.0; - let mut apparent = 0.0; - let mut humidity = 0.0; - let mut wind_north = 0.0; - let mut wind_east = 0.0; - let mut wind_kmh_north = 0.0; - let mut wind_kmh_east = 0.0; - let mut wind_count = 0.0; - let mut max = ForecastAggregate { - temp: f64::MIN, - apparent: f64::MIN, - humidity: f64::MIN, - wind: f64::MIN, - wind_kmh: f64::MIN, - wind_direction: None, - }; - let mut min = ForecastAggregate { - temp: f64::MAX, - apparent: f64::MAX, - humidity: f64::MAX, - wind: f64::MAX, - wind_kmh: f64::MAX, - wind_direction: None, - }; - for val in data { - // Summations for averaging - temp += val.temp; - apparent += val.apparent; - humidity += val.humidity; - if let Some(degrees) = val.wind_direction { - let (sin, cos) = degrees.to_radians().sin_cos(); - wind_north += val.wind * cos; - wind_east += val.wind * sin; - wind_kmh_north += val.wind_kmh * cos; - wind_kmh_east += val.wind_kmh * sin; - wind_count += 1.0; - } - - // Max - max.temp = max.temp.max(val.temp); - max.apparent = max.apparent.max(val.apparent); - max.humidity = max.humidity.max(val.humidity); - if val.wind > max.wind { - max.wind_direction = val.wind_direction; - max.wind = val.wind; - max.wind_kmh = val.wind_kmh; - } - - // Min - min.temp = min.temp.min(val.temp); - min.apparent = min.apparent.min(val.apparent); - min.humidity = min.humidity.min(val.humidity); - if val.wind < min.wind { - min.wind_direction = val.wind_direction; - min.wind = val.wind; - min.wind_kmh = val.wind_kmh; - } - } - - let count = data.len() as f64; - - // Calculate the wind results separately, discarding invalid wind values - let (wind, wind_kmh, wind_direction) = if wind_count == 0.0 { - (0.0, 0.0, None) - } else { - ( - wind_east.hypot(wind_north) / wind_count, - wind_kmh_east.hypot(wind_kmh_north) / wind_count, - Some(wind_east.atan2(wind_north).to_degrees().rem_euclid(360.0)), - ) - }; - - let avg = ForecastAggregate { - temp: temp / count, - apparent: apparent / count, - humidity: humidity / count, - wind, - wind_kmh, - wind_direction, - }; - Forecast { avg, min, max, fin } -} - #[async_trait] impl WeatherProvider for Service<'_> { async fn get_weather( @@ -341,11 +257,9 @@ impl WeatherProvider for Service<'_> { .error("parsing weather data failed")?; let data = data.properties.periods; - let current = data.first().error("No current weather")?; - - let current_weather = current.to_moment(); + let current_weather = data.first().error("No current weather")?.to_moment(); - if !need_forecast { + if !need_forecast || self.config.forecast_hours == 0 { return Ok(WeatherResult { location: location.name, current_weather, @@ -353,18 +267,15 @@ impl WeatherProvider for Service<'_> { }); } - let data_agg: Vec = data + let data_agg: Vec = data .iter() .take(self.config.forecast_hours) .map(|f| f.to_aggregate()) .collect(); - let fin = data - .get(self.config.forecast_hours.min(data.len() - 1)) - .error("no weather available")? - .to_moment(); + let fin = data.last().error("no weather available")?.to_moment(); - let forecast = Some(combine_forecasts(&data_agg, fin)); + let forecast = Some(Forecast::new(&data_agg, fin)); Ok(WeatherResult { location: location.name, diff --git a/src/blocks/weather/open_weather_map.rs b/src/blocks/weather/open_weather_map.rs index 40f8043a94..b34446dc1d 100644 --- a/src/blocks/weather/open_weather_map.rs +++ b/src/blocks/weather/open_weather_map.rs @@ -157,14 +157,55 @@ struct ApiInstantResponse { dt: i64, } +impl ApiInstantResponse { + fn wind_kmh(&self, units: &UnitSystem) -> f64 { + self.wind.speed + * match units { + UnitSystem::Metric => 3.6, + UnitSystem::Imperial => 3.6 * 0.447, + } + } + + fn to_moment(&self, units: &UnitSystem, current_data: &ApiCurrentResponse) -> WeatherMoment { + let is_night = current_data.sys.sunrise >= self.dt || self.dt >= current_data.sys.sunset; + + WeatherMoment { + icon: weather_to_icon(self.weather[0].main.as_str(), is_night), + weather: self.weather[0].main.clone(), + weather_verbose: self.weather[0].description.clone(), + temp: self.main.temp, + apparent: self.main.feels_like, + humidity: self.main.humidity, + wind: self.wind.speed, + wind_kmh: self.wind_kmh(units), + wind_direction: self.wind.deg, + } + } + + fn to_aggregate(&self, units: &UnitSystem) -> ForecastAggregateSegment { + ForecastAggregateSegment { + temp: Some(self.main.temp), + apparent: Some(self.main.feels_like), + humidity: Some(self.main.humidity), + wind: Some(self.wind.speed), + wind_kmh: Some(self.wind_kmh(units)), + wind_direction: self.wind.deg, + } + } +} + #[derive(Deserialize, Debug)] struct ApiCurrentResponse { - weather: Vec, - main: ApiMain, - wind: ApiWind, + #[serde(flatten)] + instant: ApiInstantResponse, sys: ApiSys, name: String, - dt: i64, +} + +impl ApiCurrentResponse { + fn to_moment(&self, units: &UnitSystem) -> WeatherMoment { + self.instant.to_moment(units, self) + } } #[derive(Deserialize, Debug)] @@ -231,160 +272,51 @@ impl WeatherProvider for Service<'_> { .await .error("Current weather request failed")?; - let current_weather = { - let is_night = current_data.sys.sunrise >= current_data.dt - || current_data.dt >= current_data.sys.sunset; - WeatherMoment { - temp: current_data.main.temp, - apparent: current_data.main.feels_like, - humidity: current_data.main.humidity, - weather: current_data.weather[0].main.clone(), - weather_verbose: current_data.weather[0].description.clone(), - wind: current_data.wind.speed, - wind_kmh: current_data.wind.speed - * match self.units { - UnitSystem::Metric => 3.6, - UnitSystem::Imperial => 3.6 * 0.447, - }, - wind_direction: current_data.wind.deg, - icon: weather_to_icon(current_data.weather[0].main.as_str(), is_night), - } - }; + let current_weather = current_data.to_moment(self.units); - let forecast = if !need_forecast || self.forecast_hours == 0 { - None - } else { - // Refer to https://openweathermap.org/forecast5 - let forecast_url = format!( - "{FORECAST_URL}?{location_query}&appid={api_key}&units={units}&lang={lang}&cnt={cnt}", - api_key = self.api_key, - units = match self.units { - UnitSystem::Metric => "metric", - UnitSystem::Imperial => "imperial", - }, - lang = self.lang, - cnt = self.forecast_hours / 3, - ); - - let forecast_data: ApiForecastResponse = REQWEST_CLIENT - .get(forecast_url) - .send() - .await - .error("Forecast weather request failed")? - .json() - .await - .error("Forecast weather request failed")?; - - let mut temp_avg = 0.0; - let mut temp_min = f64::MAX; - let mut temp_max = f64::MIN; - let mut apparent_avg = 0.0; - let mut apparent_min = f64::MAX; - let mut apparent_max = f64::MIN; - let mut humidity_avg = 0.0; - let mut humidity_min = f64::MAX; - let mut humidity_max = f64::MIN; - let mut wind_forecasts = Vec::new(); - let mut forecast_count = 0.0; - for forecast_instant in &forecast_data.list { - let instant_main = &forecast_instant.main; - temp_avg += instant_main.temp; - temp_min = temp_min.min(instant_main.temp); - temp_max = temp_max.max(instant_main.temp); - apparent_avg += instant_main.feels_like; - apparent_min = apparent_min.min(instant_main.feels_like); - apparent_max = apparent_max.max(instant_main.feels_like); - humidity_avg += instant_main.humidity; - humidity_min = humidity_min.min(instant_main.humidity); - humidity_max = humidity_max.max(instant_main.humidity); - forecast_count += 1.0; - - let instant_wind = &forecast_instant.wind; - wind_forecasts.push(Wind { - speed: instant_wind.speed, - degrees: instant_wind.deg, - }); - } - temp_avg /= forecast_count; - apparent_avg /= forecast_count; - humidity_avg /= forecast_count; - let Wind { - speed: wind_avg, - degrees: direction_avg, - } = average_wind(&wind_forecasts); - let Wind { - speed: wind_min, - degrees: direction_min, - } = wind_forecasts - .iter() - .min_by(|x, y| x.speed.total_cmp(&y.speed)) - .error("No min wind")?; - let Wind { - speed: wind_max, - degrees: direction_max, - } = wind_forecasts - .iter() - .min_by(|x, y| x.speed.total_cmp(&y.speed)) - .error("No max wind")?; - - let fin_data = forecast_data.list.last().unwrap(); - let fin_is_night = - current_data.sys.sunrise >= fin_data.dt || fin_data.dt >= current_data.sys.sunset; - - Some(Forecast { - avg: ForecastAggregate { - temp: temp_avg, - apparent: apparent_avg, - humidity: humidity_avg, - wind: wind_avg, - wind_kmh: wind_avg - * match self.units { - UnitSystem::Metric => 3.6, - UnitSystem::Imperial => 3.6 * 0.447, - }, - wind_direction: direction_avg, - }, - min: ForecastAggregate { - temp: temp_min, - apparent: apparent_min, - humidity: humidity_min, - wind: *wind_min, - wind_kmh: wind_min - * match self.units { - UnitSystem::Metric => 3.6, - UnitSystem::Imperial => 3.6 * 0.447, - }, - wind_direction: *direction_min, - }, - max: ForecastAggregate { - temp: temp_max, - apparent: apparent_max, - humidity: humidity_max, - wind: *wind_max, - wind_kmh: wind_max - * match self.units { - UnitSystem::Metric => 3.6, - UnitSystem::Imperial => 3.6 * 0.447, - }, - wind_direction: *direction_max, - }, - fin: WeatherMoment { - icon: weather_to_icon(fin_data.weather[0].main.as_str(), fin_is_night), - weather: fin_data.weather[0].main.clone(), - weather_verbose: fin_data.weather[0].description.clone(), - temp: fin_data.main.temp, - apparent: fin_data.main.feels_like, - humidity: fin_data.main.humidity, - wind: fin_data.wind.speed, - wind_kmh: fin_data.wind.speed - * match self.units { - UnitSystem::Metric => 3.6, - UnitSystem::Imperial => 3.6 * 0.447, - }, - wind_direction: fin_data.wind.deg, - }, - }) - }; + if !need_forecast || self.forecast_hours == 0 { + return Ok(WeatherResult { + location: current_data.name, + current_weather, + forecast: None, + }); + } + + // Refer to https://openweathermap.org/forecast5 + let forecast_url = format!( + "{FORECAST_URL}?{location_query}&appid={api_key}&units={units}&lang={lang}&cnt={cnt}", + api_key = self.api_key, + units = match self.units { + UnitSystem::Metric => "metric", + UnitSystem::Imperial => "imperial", + }, + lang = self.lang, + cnt = self.forecast_hours / 3, + ); + + let forecast_data: ApiForecastResponse = REQWEST_CLIENT + .get(forecast_url) + .send() + .await + .error("Forecast weather request failed")? + .json() + .await + .error("Forecast weather request failed")?; + + let data_agg: Vec = forecast_data + .list + .iter() + .take(self.forecast_hours) + .map(|f| f.to_aggregate(self.units)) + .collect(); + + let fin = forecast_data + .list + .last() + .error("no weather available")? + .to_moment(self.units, ¤t_data); + + let forecast = Some(Forecast::new(&data_agg, fin)); Ok(WeatherResult { location: current_data.name,