use crate::pace_model::{AnalysisOptions, WeatherConditions, WeatherLookup};
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WasmWeatherEntry {
name: String,
temperature_c: f64,
humidity_pct: f64,
wind_kmh: f64,
precip_prob_pct: f64,
}
fn weather_lookup_from(entries: &[WasmWeatherEntry]) -> WeatherLookup {
if entries.is_empty() {
return WeatherLookup::empty();
}
let mut names = Vec::with_capacity(entries.len());
let mut values = Vec::with_capacity(entries.len());
for w in entries {
names.push(w.name.clone());
values.push(WeatherConditions {
temperature_c: w.temperature_c,
humidity_pct: w.humidity_pct,
wind_kmh: w.wind_kmh,
precip_prob_pct: w.precip_prob_pct,
});
}
WeatherLookup::new(names, values)
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WasmAnalyzeOptions {
base_pace_s_per_km: f64,
k_fatigue: f64,
life_base_stop_s: u32,
#[serde(default)]
weather: Vec<WasmWeatherEntry>,
}
impl WasmAnalyzeOptions {
pub(crate) fn to_analysis_options(&self) -> AnalysisOptions {
AnalysisOptions {
base_pace_s_per_km: self.base_pace_s_per_km,
k_fatigue: self.k_fatigue,
life_base_stop_s: self.life_base_stop_s,
weather: weather_lookup_from(&self.weather),
}
}
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WasmRecalibrateOptions {
base_pace_s_per_km: f64,
k_fatigue: f64,
life_base_stop_s: u32,
current_index: u32,
actual_elapsed_s: f64,
#[serde(default)]
weather: Vec<WasmWeatherEntry>,
}
impl WasmRecalibrateOptions {
pub(crate) fn current_index(&self) -> usize {
self.current_index as usize
}
pub(crate) fn actual_elapsed_s(&self) -> f64 {
self.actual_elapsed_s
}
pub(crate) fn to_analysis_options(&self) -> AnalysisOptions {
AnalysisOptions {
base_pace_s_per_km: self.base_pace_s_per_km,
k_fatigue: self.k_fatigue,
life_base_stop_s: self.life_base_stop_s,
weather: weather_lookup_from(&self.weather),
}
}
}
#[cfg(test)]
impl WasmAnalyzeOptions {
pub(crate) fn sample() -> Self {
Self {
base_pace_s_per_km: 500.0,
k_fatigue: 0.002,
life_base_stop_s: 3600,
weather: Vec::new(),
}
}
}
#[cfg(test)]
impl WasmRecalibrateOptions {
pub(crate) fn sample(current_index: u32, actual_elapsed_s: f64) -> Self {
Self {
base_pace_s_per_km: 500.0,
k_fatigue: 0.002,
life_base_stop_s: 3600,
current_index,
actual_elapsed_s,
weather: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weather_lookup_empty_when_no_entries() {
let options = WasmAnalyzeOptions {
base_pace_s_per_km: 500.0,
k_fatigue: 0.002,
life_base_stop_s: 3600,
weather: Vec::new(),
};
assert!((options.to_analysis_options().weather.factor_for("anything") - 1.0).abs() < 1e-9);
}
#[test]
fn weather_lookup_converts_entries_and_matches_by_name() {
let options = WasmAnalyzeOptions {
base_pace_s_per_km: 500.0,
k_fatigue: 0.002,
life_base_stop_s: 3600,
weather: vec![WasmWeatherEntry {
name: "Chamonix".into(),
temperature_c: 32.0,
humidity_pct: 85.0,
wind_kmh: 35.0,
precip_prob_pct: 80.0,
}],
};
let lookup = &options.to_analysis_options().weather;
assert!(lookup.factor_for("Chamonix") > 1.0);
assert!((lookup.factor_for("Unknown") - 1.0).abs() < 1e-9);
}
}