use anyhow::{Context, Result, anyhow, bail};
use chrono::{DateTime, Datelike, TimeZone, Timelike, Utc};
use ureq::Agent;
use crate::decode::{self, WindField};
#[derive(Debug)]
pub struct NotFound;
impl std::fmt::Display for NotFound {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("not found")
}
}
impl std::error::Error for NotFound {}
pub fn is_not_found(err: &anyhow::Error) -> bool {
err.chain().any(|e| e.is::<NotFound>())
}
#[derive(Clone, Copy)]
pub struct GfsCycle(DateTime<Utc>);
impl GfsCycle {
pub fn new(t: DateTime<Utc>) -> Self {
GfsCycle(t)
}
fn fields(&self) -> (i32, u32, u32, u32) {
let t = self.0;
(t.year(), t.month(), t.day(), t.hour() / 6 * 6)
}
pub fn prev(&self) -> GfsCycle {
let (y, m, d, c) = self.fields();
let base = Utc
.with_ymd_and_hms(y, m, d, c, 0, 0)
.single()
.expect("cycle fields not a valid utc time");
GfsCycle(base - chrono::Duration::hours(6))
}
}
impl std::fmt::Display for GfsCycle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (y, m, d, c) = self.fields();
write!(f, "{y:04}{m:02}{d:02}.{c:02}")
}
}
pub fn prec2(prec: f64) -> Result<i64> {
let prec2 = (prec * 100.0) as i64;
if prec * 100.0 - prec2 as f64 != 0.0 {
bail!("precision must be at most two decimal places");
}
if prec2 <= 0 || 360 * 100 % prec2 != 0 {
bail!("precision must divide evenly");
}
Ok(prec2)
}
pub fn gfs_path(cycle: &GfsCycle, prec: f64) -> Result<String> {
let prec2 = prec2(prec)?;
let (year, month, day, cyc) = cycle.fields();
let (model, collection, variant, forecast) = ("gfs", "atmos", "pgrb2", "anl");
Ok(format!(
"{model}.{year:04}{month:02}{day:02}/{cyc:02}/{collection}/{model}.t{cyc:02}z.{variant}.{p0}p{p1:02}.{forecast}",
p0 = prec2 / 100,
p1 = prec2 % 100,
))
}
pub fn get_wind_grib(agent: &Agent, base: &str, prec: f64, level: &str) -> Result<WindField> {
prec2(prec)?;
let components = [("UGRD", level), ("VGRD", level)];
let names = ["wind u-component", "wind v-component"];
let idx = grib_index(agent, base, &components)
.with_context(|| format!("read grib index for {base:?}"))?;
let mut blob = Vec::new();
for (c, &(code, lvl)) in components.iter().enumerate() {
let range = idx[c].ok_or_else(|| {
anyhow!(
"read grib index for {base:?}: {} ({code} {lvl:?}) not found",
names[c]
)
})?;
let data = grib_data(agent, base, range)
.with_context(|| format!("read grib {} ({code} {lvl:?}) from {base:?}", names[c]))?;
blob.extend_from_slice(&data);
}
decode::decode_wind(&blob, prec as f32).with_context(|| format!("decode grib from {base:?}"))
}
type Range = (usize, Option<usize>);
fn grib_index(
agent: &Agent,
base: &str,
components: &[(&str, &str)],
) -> Result<Vec<Option<Range>>> {
let resp = agent
.get(format!("{base}.idx"))
.call()
.context("fetch index")?;
match resp.status().as_u16() {
200 => {}
404 => return Err(NotFound.into()),
s => bail!("response status {s}"),
}
let body = resp.into_body().read_to_string().context("read index")?;
let mut idx: Vec<Option<Range>> = vec![None; components.len()];
let mut cur: Option<usize> = None;
for line in body.lines() {
if line.is_empty() {
continue;
}
let f: Vec<&str> = line.split(':').collect();
if f.len() != 7 {
bail!("parse: unexpected number of fields in line {line:?}");
}
let off: usize = f[1]
.parse()
.with_context(|| format!("parse: invalid offset {:?}", f[1]))?;
if let Some(c) = cur.take()
&& let Some(range) = idx[c].as_mut()
{
range.1 = Some(off.saturating_sub(1));
}
for (i, c) in components.iter().enumerate() {
if c.0 == f[3] && c.1 == f[4] {
idx[i] = Some((off, None));
cur = Some(i);
}
}
}
Ok(idx)
}
fn grib_data(agent: &Agent, base: &str, range: Range) -> Result<Vec<u8>> {
let value = match range {
(start, Some(end)) => format!("bytes={start}-{end}"),
(start, None) => format!("bytes={start}-"),
};
let resp = agent
.get(base)
.header("Range", &value)
.call()
.context("get grib")?;
match resp.status().as_u16() {
206 => {}
404 => return Err(NotFound.into()),
200 => bail!("get grib: server did not send the requested range"),
s => bail!("get grib: response status {s}"),
}
let buf = resp.into_body().read_to_vec().context("get grib")?;
if !buf.starts_with(b"GRIB") {
bail!("get grib: response does not start with grib magic");
}
Ok(buf)
}