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,
}
}
}
fn quantize(deg: f64) -> i64 {
(deg * 100_000.0).round() as i64
}
fn bilinear_sample(
latlons: &[(f64, f64)],
values: &[f32],
latitude_deg: f64,
longitude_deg: f64,
) -> Result<f32> {
use std::collections::HashMap;
const WINDOW_DEG: f64 = 0.6;
let mut by_point: HashMap<(i64, i64), f32> = HashMap::new();
let mut lats: Vec<f64> = Vec::new();
let mut lons: Vec<f64> = Vec::new();
for (i, &(lat, lon)) in latlons.iter().enumerate() {
let mut dlon = (lon - longitude_deg).abs();
if dlon > 180.0 {
dlon = 360.0 - dlon;
}
if (lat - latitude_deg).abs() > WINDOW_DEG || dlon > WINDOW_DEG {
continue;
}
by_point.insert((quantize(lat), quantize(lon)), values[i]);
lats.push(lat);
lons.push(lon);
}
lats.sort_by(f64::total_cmp);
lats.dedup();
lons.sort_by(f64::total_cmp);
lons.dedup();
let cell = |axis: &[f64], x: f64| -> Option<(usize, usize, f64)> {
if x < axis[0] || x > axis[axis.len() - 1] {
return None;
}
let hi = axis
.iter()
.position(|&v| v >= x)
.unwrap_or(axis.len() - 1)
.max(1);
let (lo_v, hi_v) = (axis[hi - 1], axis[hi]);
let f = if hi_v > lo_v {
(x - lo_v) / (hi_v - lo_v)
} else {
0.0
};
Some((hi - 1, hi, f))
};
let nearest = |by_point: &HashMap<(i64, i64), f32>| -> Result<f32> {
if by_point.is_empty() {
let mut best = f32::NAN;
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 = values[i];
}
}
if best_dist.is_infinite() {
return Err(Error::invalid("grib2", "grid has no points"));
}
return Ok(best);
}
let mut best = f32::NAN;
let mut best_dist = f64::INFINITY;
for &(lat, lon) in latlons {
let key = (quantize(lat), quantize(lon));
let Some(&value) = by_point.get(&key) else {
continue;
};
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 = value;
}
}
if best_dist.is_infinite() {
return Err(Error::invalid("grib2", "grid has no points"));
}
Ok(best)
};
let (Some((j0, j1, fj)), Some((i0, i1, fi))) =
(cell(&lats, latitude_deg), cell(&lons, longitude_deg))
else {
return nearest(&by_point);
};
let corner = |j: usize, i: usize| {
by_point
.get(&(quantize(lats[j]), quantize(lons[i])))
.copied()
};
let (Some(v00), Some(v01), Some(v10), Some(v11)) = (
corner(j0, i0),
corner(j0, i1),
corner(j1, i0),
corner(j1, i1),
) else {
return nearest(&by_point);
};
Ok(v00 * (1.0 - fi) as f32 * (1.0 - fj) as f32
+ v01 * fi as f32 * (1.0 - fj) as f32
+ v10 * (1.0 - fi) as f32 * fj as f32
+ v11 * fi as f32 * fj as f32)
}
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 value = bilinear_sample(&latlons, &values, latitude_deg, longitude_deg)?;
levels.push((surface.scaled_value, variable, f64::from(value), 0.0, 0.0));
}
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)
}