Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/apis/open_meteo/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub struct HourlyUnits {
#[serde(rename = "precipitation_probability")]
pub precipitation_probability: String,
pub precipitation: String,
pub snowfall: String,
#[serde(rename = "uv_index")]
pub uv_index: String,
#[serde(rename = "wind_speed_10m")]
Expand All @@ -92,6 +93,7 @@ pub struct Hourly {
#[serde(rename = "precipitation_probability")]
pub precipitation_probability: Vec<u16>,
pub precipitation: Vec<f32>,
pub snowfall: Vec<f32>,
#[serde(rename = "uv_index")]
pub uv_index: Vec<f32>,
#[serde(rename = "wind_speed_10m")]
Expand All @@ -118,6 +120,8 @@ pub struct DailyUnits {
pub precipitation_sum: String,
#[serde(rename = "precipitation_probability_max")]
pub precipitation_probability_max: String,
#[serde(rename = "snowfall_sum")]
pub snowfall_sum: String,
}

#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
Expand All @@ -140,6 +144,8 @@ pub struct Daily {
pub precipitation_sum: Vec<f32>,
#[serde(rename = "precipitation_probability_max")]
pub precipitation_probability_max: Vec<u16>,
#[serde(rename = "snowfall_sum")]
pub snowfall_sum: Vec<f32>,
#[serde(rename = "cloud_cover_mean")]
pub cloud_cover_mean: Vec<Option<u16>>,
}
Expand Down Expand Up @@ -182,10 +188,11 @@ impl From<OpenMeteoHourlyResponse> for Vec<crate::domain::models::HourlyForecast
hourly_data.wind_gusts_10m[i].round() as u16,
);

let precipitation = Precipitation::new(
let precipitation = Precipitation::new_with_snowfall(
Some(hourly_data.precipitation_probability[i]),
None,
Some(hourly_data.precipitation[i].round() as u16),
Some(hourly_data.snowfall[i].round() as u16),
);

let uv_index = hourly_data.uv_index[i].round() as u16;
Expand Down Expand Up @@ -251,9 +258,15 @@ impl From<OpenMeteoDailyResponse> for Vec<crate::domain::models::DailyForecast>
let precipitation = {
let amount_max = response.daily.precipitation_sum[i].round() as u16;
let chance = response.daily.precipitation_probability_max[i];

if amount_max > 0 || chance > 0 {
Some(Precipitation::new(Some(chance), None, Some(amount_max)))
let snowfall_amount = response.daily.snowfall_sum[i].round() as u16;

if amount_max > 0 || chance > 0 || snowfall_amount > 0 {
Some(Precipitation::new_with_snowfall(
Some(chance),
None,
Some(amount_max),
Some(snowfall_amount),
))
} else {
None
}
Expand Down
5 changes: 3 additions & 2 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub static OPEN_METEO_HOURLY_ENDPOINT: Lazy<Url> = Lazy::new(|| {
"{}/v1/forecast?\
latitude={}&\
longitude={}&\
hourly=temperature_2m,apparent_temperature,precipitation_probability,precipitation,uv_index,wind_speed_10m,wind_gusts_10m,relative_humidity_2m,cloud_cover&\
hourly=temperature_2m,apparent_temperature,precipitation_probability,precipitation,uv_index,wind_speed_10m,wind_gusts_10m,relative_humidity_2m,snowfall,cloud_cover,weather_code&\
current=is_day&\
forecast_days=14&\
timezone=UTC",
Expand Down Expand Up @@ -79,7 +79,8 @@ pub static OPEN_METEO_DAILY_ENDPOINT: Lazy<Url> = Lazy::new(|| {
"{}/v1/forecast?\
latitude={}&\
longitude={}&\
daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,cloud_cover_mean&\
daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,snowfall_sum,cloud_cover_mean,weather_code&\
current=is_day&\
forecast_days=14&\
past_days=1&\
timezone=auto",
Expand Down
71 changes: 46 additions & 25 deletions src/domain/icons.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use super::models::{DailyForecast, HourlyForecast, Precipitation, Wind};
use crate::logger;
use crate::weather::icons::{
DayNight, HumidityIconName, Icon, RainAmountIcon, RainAmountName, RainChanceName, UVIndexIcon,
WindIconName,
DayNight, HumidityIconName, Icon, PrecipitationChanceName, RainAmountIcon, RainAmountName,
UVIndexIcon, WindIconName,
};
use crate::weather::utils::get_moon_phase_icon_name;
use crate::CONFIG;
Expand Down Expand Up @@ -39,44 +39,52 @@ impl Precipitation {
if is_hourly {
median *= 24.0;
}

// If primarily snow, return snow variant instead of rain
if self.is_primarily_snow() {
return match median {
0.0..1.4 => RainAmountName::None,
_ => RainAmountName::Snow,
};
}

match median {
0.0..=2.0 => RainAmountName::None,
0.0..3.0 => RainAmountName::None,
3.0..=20.0 => RainAmountName::Drizzle,
21.0.. => RainAmountName::Rain,
_ => RainAmountName::None,
_ => RainAmountName::Rain,
}
}

/// Converts the precipitation chance (percentage) to a corresponding `RainChanceName`.
/// Converts the precipitation chance (percentage) to a corresponding `PrecipitationChanceName`.
///
/// # Returns
///
/// * A `RainChanceName` variant representing the precipitation chance.
pub fn chance_to_name(&self) -> RainChanceName {
/// * A `PrecipitationChanceName` variant representing the precipitation chance.
pub fn chance_to_name(&self) -> PrecipitationChanceName {
match self.chance.unwrap_or(0) {
0..=25 => RainChanceName::Clear,
26..=50 => RainChanceName::PartlyCloudy,
51..=75 => RainChanceName::Overcast,
76.. => RainChanceName::Extreme,
0..=25 => PrecipitationChanceName::Clear,
26..=50 => PrecipitationChanceName::PartlyCloudy,
51..=75 => PrecipitationChanceName::Overcast,
76.. => PrecipitationChanceName::Extreme,
}
}
}

/// Converts cloud cover percentage to a corresponding `RainChanceName`.
/// Converts cloud cover percentage to a corresponding `PrecipitationChanceName`.
///
/// # Arguments
///
/// * `cloud_cover` - Cloud cover percentage (0-100)
///
/// # Returns
///
/// * A `RainChanceName` variant representing the cloud cover level
fn cloud_cover_to_name(cloud_cover: u16) -> RainChanceName {
/// * A `PrecipitationChanceName` variant representing the cloud cover level
fn cloud_cover_to_name(cloud_cover: u16) -> PrecipitationChanceName {
match cloud_cover {
0..=25 => RainChanceName::Clear,
26..=50 => RainChanceName::PartlyCloudy,
51..=75 => RainChanceName::Overcast,
76.. => RainChanceName::Extreme,
0..=25 => PrecipitationChanceName::Clear,
26..=50 => PrecipitationChanceName::PartlyCloudy,
51..=75 => PrecipitationChanceName::Overcast,
76.. => PrecipitationChanceName::Extreme,
}
}

Expand All @@ -92,22 +100,31 @@ fn cloud_cover_to_name(cloud_cover: u16) -> RainChanceName {
///
/// * Adjusted cloud level ensuring consistency with precipitation amount
fn apply_precipitation_override(
cloud_name: RainChanceName,
cloud_name: PrecipitationChanceName,
amount_name: RainAmountName,
) -> RainChanceName {
) -> PrecipitationChanceName {
match amount_name {
RainAmountName::None => cloud_name,
RainAmountName::Drizzle => {
// Drizzle requires at least partly cloudy
match cloud_name {
RainChanceName::Clear => RainChanceName::PartlyCloudy,
PrecipitationChanceName::Clear => PrecipitationChanceName::PartlyCloudy,
_ => cloud_name,
}
}
RainAmountName::Rain => {
// Heavy rain requires at least overcast
match cloud_name {
RainChanceName::Clear | RainChanceName::PartlyCloudy => RainChanceName::Overcast,
PrecipitationChanceName::Clear | PrecipitationChanceName::PartlyCloudy => {
PrecipitationChanceName::Overcast
}
_ => cloud_name,
}
}
RainAmountName::Snow => {
// Snow requires at least partly cloudy
match cloud_name {
PrecipitationChanceName::Clear => PrecipitationChanceName::PartlyCloudy,
_ => cloud_name,
}
}
Expand Down Expand Up @@ -139,7 +156,7 @@ impl Icon for DailyForecast {
format!("{adjusted_chance_name}{}{amount_name}.svg", DayNight::Day)
} else {
// Default to clear day if no precipitation data
format!("{}{}.svg", RainChanceName::Clear, DayNight::Day)
format!("{}{}.svg", PrecipitationChanceName::Clear, DayNight::Day)
}
}
}
Expand Down Expand Up @@ -167,7 +184,11 @@ impl Icon for HourlyForecast {
let mut icon_name = format!("{adjusted_chance_name}{day_night}{amount_name}.svg");

if CONFIG.render_options.use_moon_phase_instead_of_clear_night
&& icon_name.ends_with(&format!("{}{}.svg", RainChanceName::Clear, DayNight::Night))
&& icon_name.ends_with(&format!(
"{}{}.svg",
PrecipitationChanceName::Clear,
DayNight::Night
))
{
logger::detail("Using moon phase icon instead of clear night");
icon_name = get_moon_phase_icon_name().to_string();
Expand Down
43 changes: 43 additions & 0 deletions src/domain/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ pub struct Precipitation {
pub chance: Option<u16>,
pub amount_min: Option<u16>,
pub amount_max: Option<u16>,
pub snowfall_amount: Option<u16>,
}

impl Precipitation {
Expand All @@ -142,6 +143,21 @@ impl Precipitation {
chance,
amount_min,
amount_max,
snowfall_amount: None,
}
}

pub fn new_with_snowfall(
chance: Option<u16>,
amount_min: Option<u16>,
amount_max: Option<u16>,
snowfall_amount: Option<u16>,
) -> Self {
Self {
chance,
amount_min,
amount_max,
snowfall_amount,
}
}

Expand All @@ -150,6 +166,33 @@ impl Precipitation {
let max = self.amount_max.unwrap_or(min);
(min + max) as f32 / 2.0
}

/// Check if this precipitation includes snowfall
pub fn has_snow(&self) -> bool {
self.snowfall_amount.unwrap_or(0) > 0
}

/// Determine if precipitation is primarily snow based on water equivalent ratio
/// Using Open-Meteo's ratio: 7 cm snow ≈ 10 mm water (0.7 density)
pub fn is_primarily_snow(&self) -> bool {
let snow_cm = self.snowfall_amount.unwrap_or(0) as f32;
let precip_mm = self.calculate_median();

if snow_cm == 0.0 {
return false;
}

// Convert snow to water equivalent (7cm snow = 10mm water, so multiply by ~1.43), from open meteo docs
let snow_water_equivalent = snow_cm * 1.43;

// If snow water equivalent is more than 60% of total precipitation, it's primarily snow
snow_water_equivalent > (precip_mm * 0.6)
}

/// Get snowfall amount in cm
pub fn get_snowfall_cm(&self) -> f32 {
self.snowfall_amount.unwrap_or(0) as f32
}
}

/// Domain model for astronomical data
Expand Down
6 changes: 4 additions & 2 deletions src/weather/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use strum_macros::Display;
use crate::CONFIG;

#[derive(Debug, Display, Copy, Clone)]
pub enum RainChanceName {
pub enum PrecipitationChanceName {
#[strum(to_string = "clear")]
Clear,
#[strum(to_string = "partly-cloudy")]
Expand All @@ -16,14 +16,16 @@ pub enum RainChanceName {
Extreme,
}

#[derive(Debug, Display, Copy, Clone)]
#[derive(Debug, Display, Copy, Clone, PartialEq)]
pub enum RainAmountName {
#[strum(to_string = "")]
None,
#[strum(to_string = "-drizzle")]
Drizzle,
#[strum(to_string = "-rain")]
Rain,
#[strum(to_string = "-snow")]
Snow,
}

#[derive(Debug, Display, Copy, Clone)]
Expand Down
Loading