use crate::field::{ProfileField, ProfileLevel};
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SoundingLevel {
pub altitude_m: f64,
pub speed_mps: f64,
pub direction_from_deg: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Sounding {
pub station: Option<String>,
pub valid: Option<String>,
pub levels: Vec<SoundingLevel>,
}
impl Sounding {
pub fn parse(text: &str) -> Result<Self> {
let mut station = None;
let mut valid = None;
let mut levels = Vec::new();
for (number, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(comment) = line.strip_prefix('#') {
let comment = comment.trim();
if let Some(value) = comment.strip_prefix("station:") {
station = Some(value.trim().to_string());
} else if let Some(value) = comment.strip_prefix("valid:") {
valid = Some(value.trim().to_string());
}
continue;
}
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() != 3 {
return Err(Error::invalid(
"sounding",
format!(
"line {}: expected 'altitude_m speed_mps direction_from_deg'",
number + 1
),
));
}
let parse = |token: &str| -> Result<f64> {
token.parse().map_err(|_| {
Error::invalid(
"sounding",
format!("line {}: '{token}' is not numeric", number + 1),
)
})
};
levels.push(SoundingLevel {
altitude_m: parse(fields[0])?,
speed_mps: parse(fields[1])?,
direction_from_deg: parse(fields[2])?,
});
}
if levels.is_empty() {
return Err(Error::invalid("sounding", "no levels"));
}
Ok(Self {
station,
valid,
levels,
})
}
pub fn surface_altitude_m(&self) -> Option<f64> {
self.levels.iter().map(|l| l.altitude_m).reduce(f64::min)
}
pub fn to_profile_field(&self) -> Result<ProfileField> {
ProfileField::new(
self.levels
.iter()
.map(|l| ProfileLevel {
altitude_m: l.altitude_m,
speed_mps: l.speed_mps,
direction_from_deg: l.direction_from_deg,
})
.collect(),
)
}
}