use std::io::Cursor;
use anyhow::{Result, anyhow, bail};
use grib::{Grib2, Grib2Read, Grib2SubmessageDecoder, LatLons, SubMessage};
use image::{ImageBuffer, Rgb32FImage, imageops::FilterType};
pub struct WindField {
pub uv: Vec<(f32, f32)>,
pub width: usize,
pub height: usize,
}
pub fn decode_wind(data: &[u8], precision: f32) -> Result<WindField> {
let grib2 = grib::from_reader(Cursor::new(data))?;
extract_grib_wind(grib2, precision)
}
pub fn create_wind_texture(width: usize, height: usize, uv: &[(f32, f32)]) -> Result<Rgb32FImage> {
let pix = uv
.iter()
.flat_map(|&(u, v)| {
let s = f32::sqrt(u * u + v * v);
let u = u / s;
let v = v / s;
[
f32::clamp(u + 1.0, 0.0, 2.0) / 2.0,
f32::clamp(v + 1.0, 0.0, 2.0) / 2.0,
f32::clamp(s, 0.0, 30.0) / 30.0,
]
})
.collect();
ImageBuffer::from_vec(width as u32, height as u32, pix)
.ok_or_else(|| anyhow!("wind texture buffer size does not match {width}x{height}"))
}
pub fn filter_wind_texture(img: &Rgb32FImage) -> Rgb32FImage {
let resized = image::imageops::resize(
img,
img.width() / 4,
img.height() / 4,
FilterType::Triangle, );
image::imageops::blur(&resized, 1.0)
}
fn extract_grib_wind<T: Grib2Read>(grib2: Grib2<T>, precision: f32) -> Result<WindField> {
let mut ugrd = None;
let mut vgrd = None;
for (_, msg) in grib2.iter() {
let slot = match msg
.prod_def()
.parameter_category()
.zip(msg.prod_def().parameter_number())
{
Some((2, 2)) => &mut ugrd,
Some((2, 3)) => &mut vgrd,
_ => continue,
};
slot.replace(extract_grib_values(msg, precision)?);
}
let (ugrd, ugrd_dim) = ugrd.ok_or_else(|| anyhow!("missing UGRD"))?;
let (vgrd, vgrd_dim) = vgrd.ok_or_else(|| anyhow!("missing VGRD"))?;
if ugrd_dim != vgrd_dim {
bail!("mismatched grid size for UGRD ({ugrd_dim:?}) and VGRD ({vgrd_dim:?})");
}
let (width, height) = ugrd_dim;
Ok(WindField {
uv: std::iter::zip(ugrd, vgrd).collect(),
width,
height,
})
}
fn extract_grib_values<R: Grib2Read>(
submessage: SubMessage<R>,
precision: f32,
) -> Result<(Vec<f32>, (usize, usize))> {
let lat_dim = (180.0 / precision + 1.0) as usize;
let lon_dim = (360.0 / precision) as usize;
let latlon = submessage.latlons()?;
let values = Grib2SubmessageDecoder::from(submessage)?;
let mut res = vec![0_f32; lat_dim * lon_dim];
for ((lat, lon), val) in latlon.zip(values.dispatch()?) {
let lon = (lon + 180.0) % 360.0 - 180.0; let lat_idx = ((-lat + 90.0) / precision).round() as usize;
let lon_idx = ((lon + 180.0) / precision).round() as usize;
let idx = lat_idx * lon_dim + lon_idx;
let slot = res.get_mut(idx).ok_or_else(|| {
anyhow!("grib point ({lat}, {lon}) maps outside the {lon_dim}x{lat_dim} grid")
})?;
*slot = val;
}
Ok((res, (lon_dim, lat_dim)))
}