use crate::field::{ProfileField, ProfileLevel};
use crate::{Error, Result};
use grib::{Grib2SubmessageDecoder, LatLons};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Variable {
Height,
Ugrd,
Vgrd,
}
impl Variable {
fn of(category: Option<u8>, number: Option<u8>) -> Option<Self> {
match (category, number) {
(Some(3), Some(5)) => Some(Self::Height),
(Some(2), Some(2)) => Some(Self::Ugrd),
(Some(2), Some(3)) => Some(Self::Vgrd),
_ => None,
}
}
}
pub fn profile_from_grib2(
data: &[u8],
latitude_deg: f64,
longitude_deg: f64,
launch_azimuth_deg: f64,
) -> Result<ProfileField> {
if !latitude_deg.is_finite() || latitude_deg.abs() > 90.0 {
return Err(Error::invalid(
"latitude_deg",
"must be finite within [-90, 90]",
));
}
if !longitude_deg.is_finite() || !launch_azimuth_deg.is_finite() {
return Err(Error::invalid("longitude_deg", "must be finite"));
}
let grib2 = grib::from_bytes(data)
.map_err(|e| Error::invalid("grib2", format!("could not decode: {e}")))?;
let mut levels: Vec<(i32, Variable, f64, f64, f64)> = Vec::new();
for (_index, submessage) in grib2.iter() {
let prod = submessage.prod_def();
let Some(variable) = Variable::of(prod.parameter_category(), prod.parameter_number())
else {
continue;
};
let Some((surface, _)) = prod.fixed_surfaces() else {
continue;
};
if surface.surface_type != 100 {
continue;
}
let latlons: Vec<(f64, f64)> = submessage
.latlons()
.map_err(|e| Error::invalid("grib2", format!("grid has no lat/lons: {e:?}")))?
.collect();
let values: Vec<f32> = Grib2SubmessageDecoder::from(submessage)
.map_err(|e| Error::invalid("grib2", format!("cannot decode values: {e}")))?
.dispatch()
.map_err(|e| Error::invalid("grib2", format!("cannot decode values: {e}")))?
.collect();
let mut best = usize::MAX;
let mut best_dist = f64::INFINITY;
for (i, &(lat, lon)) in latlons.iter().enumerate() {
let dlat = lat - latitude_deg;
let mut dlon = (lon - longitude_deg).abs();
if dlon > 180.0 {
dlon = 360.0 - dlon;
}
let dist = dlat * dlat + dlon * dlon;
if dist < best_dist {
best_dist = dist;
best = i;
}
}
if best == usize::MAX {
return Err(Error::invalid("grib2", "grid has no points"));
}
levels.push((
surface.scaled_value,
variable,
f64::from(values[best]),
latlons[best].0,
latlons[best].1,
));
}
type LevelParts = (i32, Option<f64>, Option<f64>, Option<f64>);
let mut assembled: Vec<LevelParts> = Vec::new();
for (level_pa, variable, value, _lat, _lon) in levels {
let entry = match assembled.iter_mut().find(|(l, _, _, _)| *l == level_pa) {
Some(entry) => entry,
None => {
assembled.push((level_pa, None, None, None));
assembled.last_mut().expect("just pushed")
}
};
match variable {
Variable::Height => entry.1 = Some(value),
Variable::Ugrd => entry.2 = Some(value),
Variable::Vgrd => entry.3 = Some(value),
}
}
let mut profile_levels = Vec::new();
for (_pa, height, u, v) in assembled {
let (Some(altitude_m), Some(u_mps), Some(v_mps)) = (height, u, v) else {
return Err(Error::invalid(
"grib2",
"levels lack geopotential height, UGRD or VGRD",
));
};
let speed_mps = u_mps.hypot(v_mps);
let from_met = (-u_mps).atan2(-v_mps).to_degrees().rem_euclid(360.0);
let direction_from_deg = (from_met - launch_azimuth_deg).rem_euclid(360.0);
profile_levels.push(ProfileLevel {
altitude_m,
speed_mps,
direction_from_deg,
});
}
ProfileField::new(profile_levels)
}