use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use crate::{Grib2Bbox, TimedWindMap, grib2, wind_av1};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Format {
Grib2,
WindAv1,
}
impl Format {
pub fn from_path(path: &Path) -> Result<Self, LoadError> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase);
match ext.as_deref() {
Some("grib2" | "grb2" | "grib") => Ok(Self::Grib2),
Some("wcav") => Ok(Self::WindAv1),
_ => Err(LoadError::UnknownExtension(path.to_path_buf())),
}
}
pub fn name(self) -> &'static str {
match self {
Self::Grib2 => "GRIB2",
Self::WindAv1 => "wind_av1",
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum LoadError {
UnknownExtension(PathBuf),
OptionNotApplicable(&'static str),
Io {
path: PathBuf,
source: std::io::Error,
},
Grib(grib2::LoadError),
WindAv1(wind_av1::DecodeError),
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownExtension(p) => write!(
f,
"cannot infer wind-map format from extension of {}: \
known extensions are .grib2 / .grb2 / .grib / .wcav",
p.display(),
),
Self::OptionNotApplicable(msg) => f.write_str(msg),
Self::Io { path, source } => {
write!(f, "I/O error on {}: {source}", path.display())
}
Self::Grib(e) => write!(f, "{e}"),
Self::WindAv1(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for LoadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
Self::Grib(e) => Some(e),
Self::WindAv1(e) => Some(e),
_ => None,
}
}
}
impl From<grib2::LoadError> for LoadError {
fn from(e: grib2::LoadError) -> Self {
Self::Grib(e)
}
}
impl From<wind_av1::DecodeError> for LoadError {
fn from(e: wind_av1::DecodeError) -> Self {
Self::WindAv1(e)
}
}
pub fn load(
path: &Path,
grib_stride: usize,
grib_bbox: Option<Grib2Bbox>,
) -> Result<TimedWindMap, LoadError> {
let fmt = Format::from_path(path)?;
if !matches!(fmt, Format::Grib2) && (grib_stride > 1 || grib_bbox.is_some()) {
return Err(LoadError::OptionNotApplicable(
"grib_stride / grib_bbox apply only to GRIB2 input",
));
}
match fmt {
Format::Grib2 => load_grib2(path, grib_stride, grib_bbox),
Format::WindAv1 => load_wcav(path),
}
}
pub fn load_grib2(
path: &Path,
stride: usize,
bbox: Option<Grib2Bbox>,
) -> Result<TimedWindMap, LoadError> {
let file = File::open(path).map_err(|source| LoadError::Io {
path: path.to_path_buf(),
source,
})?;
let reader = BufReader::new(file);
Ok(TimedWindMap::from_grib2_reader(reader, stride, bbox)?)
}
pub fn load_wcav(path: &Path) -> Result<TimedWindMap, LoadError> {
let file = File::open(path).map_err(|source| LoadError::Io {
path: path.to_path_buf(),
source,
})?;
let reader = BufReader::new(file);
Ok(wind_av1::decode(reader)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_from_path_detects_known_extensions() {
assert_eq!(
Format::from_path(Path::new("a.grib2")).unwrap(),
Format::Grib2
);
assert_eq!(
Format::from_path(Path::new("a.GRB2")).unwrap(),
Format::Grib2
);
assert_eq!(
Format::from_path(Path::new("a.grib")).unwrap(),
Format::Grib2
);
assert_eq!(
Format::from_path(Path::new("a.wcav")).unwrap(),
Format::WindAv1
);
}
#[test]
fn format_from_path_rejects_unknown_extensions() {
assert!(matches!(
Format::from_path(Path::new("a.json")),
Err(LoadError::UnknownExtension(_)),
));
assert!(matches!(
Format::from_path(Path::new("a.csv")),
Err(LoadError::UnknownExtension(_)),
));
assert!(matches!(
Format::from_path(Path::new("noext")),
Err(LoadError::UnknownExtension(_)),
));
}
#[test]
fn format_name_labels_match_module_doc() {
assert_eq!(Format::Grib2.name(), "GRIB2");
assert_eq!(Format::WindAv1.name(), "wind_av1");
}
#[test]
fn load_rejects_grib_options_with_wcav_input() {
let bbox = Grib2Bbox {
lat_min: 0.0,
lon_min: 0.0,
lat_max: 1.0,
lon_max: 1.0,
};
assert!(matches!(
load(Path::new("a.wcav"), 1, Some(bbox)),
Err(LoadError::OptionNotApplicable(_)),
));
assert!(matches!(
load(Path::new("a.wcav"), 2, None),
Err(LoadError::OptionNotApplicable(_)),
));
}
}