gistools/readers/grib2/
mod.rs

1/// Section content
2pub mod sections;
3
4use crate::{
5    parsers::{BufferReader, FeatureReader, Reader},
6    util::fetch_url,
7};
8use alloc::{
9    format,
10    string::{String, ToString},
11    vec,
12    vec::Vec,
13};
14use core::cell::RefCell;
15use s2json::{BBox3D, MValue, Properties, VectorFeature, VectorGeometry, VectorMultiPoint};
16pub use sections::*;
17
18/// An GRIB2 Shaped Vector Feature
19pub type GRIB2VectorFeature = VectorFeature<Vec<Grib2ProductDefinition>>;
20
21/// GFS sources available for download
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Grib2GFSSource {
24    /// AWS
25    Aws,
26    /// FTPPRD
27    Ftpprd,
28    /// NOMADS
29    Nomads,
30    /// Google
31    Google,
32    /// Azure
33    Azure,
34    /// User defined server
35    Other(String),
36}
37impl From<&str> for Grib2GFSSource {
38    fn from(value: &str) -> Self {
39        match value {
40            "aws" => Grib2GFSSource::Aws,
41            "ftpprd" => Grib2GFSSource::Ftpprd,
42            "nomads" => Grib2GFSSource::Nomads,
43            "google" => Grib2GFSSource::Google,
44            "azure" => Grib2GFSSource::Azure,
45            _ => Grib2GFSSource::Other(value.into()),
46        }
47    }
48}
49impl Grib2GFSSource {
50    /// Convert the source to a URL
51    pub fn to_url(&self) -> String {
52        match self {
53            Grib2GFSSource::Aws => "https://noaa-gfs-bdp-pds.s3.amazonaws.com/".into(),
54            Grib2GFSSource::Ftpprd => "https://ftpprd.ncep.noaa.gov/data/nccf/com/gfs/prod/".into(),
55            Grib2GFSSource::Nomads => {
56                "https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod/".into()
57            }
58            Grib2GFSSource::Google => {
59                "https://storage.googleapis.com/global-forecast-system/".into()
60            }
61            Grib2GFSSource::Azure => "https://noaagfs.blob.core.windows.net/gfs/".into(),
62            Grib2GFSSource::Other(s) => s.into(),
63        }
64    }
65}
66
67/// GFS domains available for download
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum Grib2GFSDomain {
70    /// Atmospheric
71    Atmos,
72    /// Ocean
73    Wave,
74}
75
76/// GFS ATMOS products available for download
77/// - `pgrb2.0p25` - common fields, 0.25 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2.0p25.f000.shtml)
78/// - `pgrb2.0p50` - common fields, 0.50 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2.0p50.f000.shtml)
79/// - `pgrb2.1p00` - common fields, 1.00 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2.1p00.f000.shtml)
80/// - `pgrb2b.0p25` - uncommon fields, 0.25 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2b.0p25.f000.shtml)
81/// - `pgrb2b.0p50` - uncommon fields, 0.50 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2b.0p50.f000.shtml)
82/// - `pgrb2b.1p00` - uncommon fields, 1.00 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2b.1p00.f000.shtml)
83/// - `pgrb2full.0p50` - combined grids of 0.50 resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t12z.pgrb2full.0p50.f000.shtml)
84/// - `sfluxgrb` - surface flux fields, T1534 Semi-Lagrangian grid [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.sfluxgrbf000.grib2.shtml)
85/// - `goesimpgrb2.0p25` - 0.50 degree resolution for GOES-IMP [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.goessimpgrb2.0p25.f000.shtml)
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum Grib2AtmosGFSProduct {
88    /// `pgrb2.0p25` - common fields, 0.25 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2.0p25.f000.shtml)
89    Pgrb20p25,
90    /// - `pgrb2.0p50` - common fields, 0.50 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2.0p50.f000.shtml)
91    Pgrb20p50,
92    /// - `pgrb2.1p00` - common fields, 1.00 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2.1p00.f000.shtml)
93    Pgrb21p00,
94    /// - `pgrb2b.0p25` - uncommon fields, 0.25 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2b.0p25.f000.shtml)
95    Pgrb2b0p25,
96    /// - `pgrb2b.0p50` - uncommon fields, 0.50 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2b.0p50.f000.shtml)
97    Pgrb2b0p50,
98    /// - `pgrb2b.1p00` - uncommon fields, 1.00 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2b.1p00.f000.shtml)
99    Pgrb2b1p00,
100    /// - `pgrb2full.0p50` - combined grids of 0.50 resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t12z.pgrb2full.0p50.f000.shtml)
101    Pgrb2full0p50,
102    /// - `sfluxgrb` - surface flux fields, T1534 Semi-Lagrangian grid [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.sfluxgrbf000.grib2.shtml)
103    Sfluxgrb,
104    /// - `goesimpgrb2.0p25` - 0.50 degree resolution for GOES-IMP [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.goessimpgrb2.0p25.f000.shtml)
105    Goesimpgrb20p25,
106    /// - User defined product
107    Other(String),
108}
109impl From<&str> for Grib2AtmosGFSProduct {
110    fn from(value: &str) -> Self {
111        match value {
112            "pgrb2.0p25" => Self::Pgrb20p25,
113            "pgrb2.0p50" => Self::Pgrb20p50,
114            "pgrb2.1p00" => Self::Pgrb21p00,
115            "pgrb2b.0p25" => Self::Pgrb2b0p25,
116            "pgrb2b.0p50" => Self::Pgrb2b0p50,
117            "pgrb2b.1p00" => Self::Pgrb2b1p00,
118            "pgrb2full.0p50" => Self::Pgrb2full0p50,
119            "sfluxgrb" => Self::Sfluxgrb,
120            "goesimpgrb2.0p25" => Self::Goesimpgrb20p25,
121            _ => Self::Other(value.into()),
122        }
123    }
124}
125impl From<Grib2AtmosGFSProduct> for String {
126    fn from(value: Grib2AtmosGFSProduct) -> Self {
127        match value {
128            Grib2AtmosGFSProduct::Pgrb20p25 => "pgrb2.0p25".into(),
129            Grib2AtmosGFSProduct::Pgrb20p50 => "pgrb2.0p50".into(),
130            Grib2AtmosGFSProduct::Pgrb21p00 => "pgrb2.1p00".into(),
131            Grib2AtmosGFSProduct::Pgrb2b0p25 => "pgrb2b.0p25".into(),
132            Grib2AtmosGFSProduct::Pgrb2b0p50 => "pgrb2b.0p50".into(),
133            Grib2AtmosGFSProduct::Pgrb2b1p00 => "pgrb2b.1p00".into(),
134            Grib2AtmosGFSProduct::Pgrb2full0p50 => "pgrb2full.0p50".into(),
135            Grib2AtmosGFSProduct::Sfluxgrb => "sfluxgrb".into(),
136            Grib2AtmosGFSProduct::Goesimpgrb20p25 => "goesimpgrb2.0p25".into(),
137            Grib2AtmosGFSProduct::Other(v) => v,
138        }
139    }
140}
141
142/// GFS WAVE products available for download
143/// - `arctic.9km` - Arctic, 9km resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.arctic.9km.f003.grib2.shtml)
144/// - `atlocn.0p16` - Atlantic, 0.16 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.atlocn.0p16.f003.grib2.shtml)
145/// - `epacif.0p16` - Eastern Pacific, 0.16 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.epacif.0p16.f003.grib2.shtml)
146/// - `global.0p16` - Global, 0.16 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.global.0p16.f003.grib2.shtml)
147/// - `global.0p25` - Global, 0.25 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.global.0p25.f003.grib2.shtml)
148/// - `gsouth.0p25` - Gulf of South America, 0.25 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.gsouth.0p25.f003.grib2.shtml)
149/// - `wcoast.0p16` - West Coast, 0.16 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.wcoast.0p16.f003.grib2.shtml)
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum Grib2WaveGFSProduct {
152    /// - `arctic.9km` - Arctic, 9km resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.arctic.9km.f003.grib2.shtml)
153    Arctic9km,
154    /// - `atlocn.0p16` - Atlantic, 0.16 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.atlocn.0p16.f003.grib2.shtml)
155    Atlocn0p16,
156    /// - `epacif.0p16` - Eastern Pacific, 0.16 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.epacif.0p16.f003.grib2.shtml)
157    Epacif0p16,
158    /// - `global.0p16` - Global, 0.16 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.global.0p16.f003.grib2.shtml)
159    Global0p16,
160    /// - `global.0p25` - Global, 0.25 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.global.0p25.f003.grib2.shtml)
161    Global0p25,
162    /// - `gsouth.0p25` - Gulf of South America, 0.25 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.gsouth.0p25.f003.grib2.shtml)
163    Gsouth0p25,
164    /// - `wcoast.0p16` - West Coast, 0.16 degree resolution [Study Variables here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.wcoast.0p16.f003.grib2.shtml)
165    Wcoast0p16,
166    /// User defined product
167    Other(String),
168}
169impl From<&str> for Grib2WaveGFSProduct {
170    fn from(value: &str) -> Self {
171        match value {
172            "arctic.9km" => Grib2WaveGFSProduct::Arctic9km,
173            "atlocn.0p16" => Grib2WaveGFSProduct::Atlocn0p16,
174            "epacif.0p16" => Grib2WaveGFSProduct::Epacif0p16,
175            "global.0p16" => Grib2WaveGFSProduct::Global0p16,
176            "global.0p25" => Grib2WaveGFSProduct::Global0p25,
177            "gsouth.0p25" => Grib2WaveGFSProduct::Gsouth0p25,
178            "wcoast.0p16" => Grib2WaveGFSProduct::Wcoast0p16,
179            _ => Grib2WaveGFSProduct::Other(value.into()),
180        }
181    }
182}
183impl From<Grib2WaveGFSProduct> for String {
184    fn from(value: Grib2WaveGFSProduct) -> Self {
185        match value {
186            Grib2WaveGFSProduct::Arctic9km => "arctic.9km".into(),
187            Grib2WaveGFSProduct::Atlocn0p16 => "atlocn.0p16".into(),
188            Grib2WaveGFSProduct::Epacif0p16 => "epacif.0p16".into(),
189            Grib2WaveGFSProduct::Global0p16 => "global.0p16".into(),
190            Grib2WaveGFSProduct::Global0p25 => "global.0p25".into(),
191            Grib2WaveGFSProduct::Gsouth0p25 => "gsouth.0p25".into(),
192            Grib2WaveGFSProduct::Wcoast0p16 => "wcoast.0p16".into(),
193            Grib2WaveGFSProduct::Other(value) => value,
194        }
195    }
196}
197
198/// GFS Hour
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum Grib2GFSHour {
201    /// "00"
202    Hour0,
203    /// "06"
204    Hour6,
205    /// "12"
206    Hour12,
207    /// "18"
208    Hour18,
209}
210impl From<&str> for Grib2GFSHour {
211    fn from(value: &str) -> Self {
212        match value {
213            "00" => Grib2GFSHour::Hour0,
214            "06" => Grib2GFSHour::Hour6,
215            "12" => Grib2GFSHour::Hour12,
216            "18" => Grib2GFSHour::Hour18,
217            _ => panic!("Invalid hour"),
218        }
219    }
220}
221impl From<Grib2GFSHour> for String {
222    fn from(value: Grib2GFSHour) -> Self {
223        match value {
224            Grib2GFSHour::Hour0 => "00".into(),
225            Grib2GFSHour::Hour6 => "06".into(),
226            Grib2GFSHour::Hour12 => "12".into(),
227            Grib2GFSHour::Hour18 => "18".into(),
228        }
229    }
230}
231
232/// Description of a section in the GRIB2 file
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct Grib2SectionLocations {
235    /// Start/offset of section
236    pub start: u64,
237    /// If missing, assume the end is the end of the file
238    pub end: Option<u64>,
239    /// The entire line detailing the section
240    pub line: String,
241    /// The name of the filter
242    pub name: String,
243}
244
245#[doc(hidden)]
246/// # Fetch ATMOS or WAVE GFS data.
247///
248/// ## ATMOS
249/// You can find some data to reference what's available [here](https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod/).
250///
251/// An example of what variable data means can be found [here](https://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs.t00z.pgrb2.0p50.f000.shtml).
252///
253/// ## WAVE
254/// You can find some data to reference what's available [here](https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod/).
255///
256/// An example of what variable data means can be found [here](https://www.nco.ncep.noaa.gov/pmb/products/wave/gfswave.t12z.arctic.9km.f003.grib2.shtml).
257///
258/// ## Parameters
259///
260/// - `source`: The source of the data, `aws` | `ftpprd` | `nomads` | `google` | `azure` | or a user provided url
261/// - `product`: which product to fetch. Use [`Grib2AtmosGFSProduct`] or [`Grib2WaveGFSProduct`]
262/// - `domain`: The domain of the data, `atmos` or `wave`
263/// - `year`: The year to fetch given a 4 digit year
264/// - `month`: The month to fetch given a 2 digit month 01 is January and 12 is December
265/// - `day`: The day to fetch given a 2 digit day, e.g. '01' or '31'
266/// - `hour`: The forecast hour with 2 digits often in increments of 6 up to 18, e.g. '00' or '12'
267/// - `forecast`: The forecast hour with 3 digits often in increments of 3 up to 384, e.g. '000' or '003'
268/// - `filters`: The filters to apply by filtering lines in the .idx file
269///
270/// ## Returns
271///
272/// A [`GRIB2Reader`] of the specific sections
273///
274/// ## Example
275///
276/// ```rust
277/// use gistools::readers::{fetch_gfs_data, Grib2GFSSource, Grib2AtmosGFSProduct, Grib2GFSDomain};
278///
279/// async fn example() {
280///     let grib2_reader = fetch_gfs_data(
281///         Grib2GFSSource::Aws,
282///         Grib2AtmosGFSProduct::Pgrb2b1p00,
283///         Grib2GFSDomain::Atmos,
284///         "2024".into(),
285///         "12".into(),
286///         "14".into(),
287///         "12".into(),
288///         Some("003".into()),
289///         Some(vec!["TMP:2 m".into()]),
290///      )
291///     .await;
292///     assert_eq!(grib2_reader.idxs.len(), 1);
293/// }
294/// ```
295#[allow(clippy::too_many_arguments)]
296pub async fn fetch_gfs_data<P: Into<String>>(
297    source: Grib2GFSSource,
298    product: P,
299    domain: Grib2GFSDomain,
300    year: String,
301    month: String,
302    day: String,
303    hour: Grib2GFSHour,
304    forecast: Option<String>,
305    filters: Option<Vec<String>>,
306) -> GRIB2Reader {
307    // If year is not 4 chars, month not 2, day not 2, or forecast is not 3 chars, return error
308    let forecast = forecast.unwrap_or("000".into());
309    if year.len() != 4 || month.len() != 2 || day.len() != 2 || forecast.len() != 3 {
310        panic!("Year, month, day, and forecast must be 4, 2, 2, and 3 characters, respectively.",);
311    }
312    let link = get_gfs_link(source, product, domain, year, month, day, hour, forecast);
313    // pull .idx file FIRST
314    let idxs = parsed_idx_from_url(format!("{link}.idx"), filters.unwrap_or_default(), None).await;
315    let source_data = link_to_chunks(link, &idxs).await;
316
317    GRIB2Reader::new::<BufferReader>(source_data.into(), idxs)
318}
319
320/// Get the link to download GFS Atmos data relative to IDXs
321async fn link_to_chunks(link: String, idxs: &[Grib2SectionLocations]) -> Vec<BufferReader> {
322    let mut readers: Vec<BufferReader> = vec![];
323    for Grib2SectionLocations { start, end, .. } in idxs {
324        let end = end.map_or(String::new(), |e| e.to_string());
325        let chunk =
326            fetch_url::<()>(&link, &[("Range", &format!("bytes={start}-{end}"))], None, None)
327                .await
328                .unwrap();
329        readers.push(BufferReader::new(chunk));
330    }
331
332    readers
333}
334
335/// Get the link to download GFS Atmos data
336///
337/// ## Parameters
338///
339/// - `source`: The source of the data, `aws` | `ftpprd` | `nomads` | `google` | `azure` | or a user provided url
340/// - `product`: which product to fetch
341/// - `domain`: The domain of the data, either 'atmos' for atmospheric data or 'wave' for ocean wave data
342/// - `year`: The year to fetch given a 4 digit year
343/// - `month`: The month to fetch given a 2 digit month 01 is January and 12 is December
344/// - `day`: The day to fetch given a 2 digit day, e.g. '01' or '31'
345/// - `hour`: The forecast hour with 2 digits often in increments of 6 up to 18, e.g. '00' or '12'
346/// - `forecast`: The forecast hour with 3 digits often in increments of 3 up to 384, e.g. '000' or '003'
347///
348/// ## Returns
349///
350/// A [`String`] of the specific sections
351#[allow(clippy::too_many_arguments)]
352pub fn get_gfs_link<P: Into<String>>(
353    source: Grib2GFSSource,
354    product: P,
355    domain: Grib2GFSDomain,
356    year: String,
357    month: String,
358    day: String,
359    hour: Grib2GFSHour,
360    forecast: String,
361) -> String {
362    let mut link = source.to_url();
363    let domain_str = if domain == Grib2GFSDomain::Atmos { "atmos" } else { "wave/gridded" };
364    let start_name = if domain == Grib2GFSDomain::Atmos { "gfs" } else { "gfswave" };
365    let end_name = if domain == Grib2GFSDomain::Atmos { "" } else { ".grib2" };
366    let hour: String = hour.into();
367    let product: String = product.into();
368    link = format!(
369        "{link}gfs.{year}{month}{day}/{hour}/{domain_str}/{start_name}.t{hour}z.{product}.\
370         f{forecast}{end_name}",
371    );
372
373    link
374}
375
376/// Parse the .idx file for GRIB2 section details using a URL
377///
378/// ## Parameters
379/// - `url`: The URL of the .idx file
380/// - `filters`: The filters to apply
381/// - `offset_position`: The position of the offset in the ":" sequence
382///
383/// ## Returns
384/// An array of Grib2SectionLocations
385pub async fn parsed_idx_from_url(
386    url: String,
387    filters: Vec<String>,
388    offset_position: Option<usize>,
389) -> Vec<Grib2SectionLocations> {
390    let data = fetch_url::<()>(&url, &[], None, None).await.unwrap();
391    parse_idx(String::from_utf8_lossy(&data).into(), filters, offset_position)
392}
393
394/// Parse the .idx file for GRIB2 section details
395///
396/// ## Parameters
397/// - `data`: The contents of the .idx file
398/// - `filters`: The filters to apply
399/// - `offset_position`: The position of the offset in the ":" sequence
400///
401/// ## Returns
402/// An array of Grib2SectionLocations
403pub fn parse_idx(
404    data: String,
405    filters: Vec<String>,
406    offset_position: Option<usize>,
407) -> Vec<Grib2SectionLocations> {
408    let offset_position = offset_position.unwrap_or(1);
409    let mut res = vec![];
410    // split lines, parse information, and add to array
411    for line in data.lines() {
412        if line.is_empty() {
413            continue;
414        }
415        let offset = line
416            .split(':')
417            .nth(offset_position)
418            .and_then(|s| s.trim().parse::<u64>().ok())
419            .unwrap_or(0);
420        res.push(Grib2SectionLocations {
421            start: offset,
422            end: None,
423            line: line.into(),
424            name: line.into(),
425        });
426    }
427    // now add the "end"s
428    for i in 0..res.len() - 1 {
429        res[i].end = Some(res[i + 1].start);
430    }
431    // lastly add the filters
432    if !filters.is_empty() {
433        res = res
434            .iter()
435            .filter(|s_l| filters.iter().any(|f| s_l.line.contains(f)))
436            .cloned()
437            .collect();
438    }
439    // set names to filter names
440    for i in 0..res.len() {
441        res[i].name = filters[i].clone();
442    }
443
444    res
445}
446
447/// GRIB2 Reader inputs
448#[derive(Debug)]
449pub enum GRIB2ReaderInput<T: Reader> {
450    /// A single input reader (completely unparsed)
451    Reader(T),
452    /// A list of input readers, parsed into section chunks
453    SectionChunks(Vec<BufferReader>),
454}
455impl<T: Reader> From<T> for GRIB2ReaderInput<T> {
456    fn from(reader: T) -> Self {
457        GRIB2ReaderInput::Reader(reader)
458    }
459}
460impl<T: Reader> From<Vec<BufferReader>> for GRIB2ReaderInput<T> {
461    fn from(readers: Vec<BufferReader>) -> Self {
462        GRIB2ReaderInput::SectionChunks(readers)
463    }
464}
465
466/// # GRIB2 Reader
467///
468/// ## Description
469///
470/// This class reads a GRIB2 file and returns a list of GRIB2 products.
471///
472/// Implements the [`FeatureReader`] trait
473///
474/// ## Usage
475///
476/// The methods you have access to:
477/// - [`GRIB2Reader::new`]: Create a new GRIB2Reader
478/// - [`GRIB2Reader::from_idx`]: Create a GRIB2Reader with filtered .idx file data (see [`parse_idx`] and [`parsed_idx_from_url`])
479/// - [`GRIB2Reader::get_data`]: Get the Vector MultiPoint data
480/// - [`GRIB2Reader::get_feature`]: Get the VectorFeature data
481///
482/// Associated methods that are useful:
483/// - [`fetch_gfs_data`]: Fetch ATMOS or WAVE GFS data.
484/// - [`parsed_idx_from_url`]: Given an input URL pointing to an IDX file, parse the sections
485/// - [`parse_idx`]: Given an input string of an IDX file, parse the sections
486///
487/// ### The recommended way to parse grib files is to filter out what you want:
488/// ```rust
489/// use gistools::{parsers::{BufferReader, FeatureReader}, readers::{parse_idx, GRIB2Reader}};
490/// use std::{fs, path::PathBuf};
491///
492/// let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
493/// path.push("tests/readers/grib2/fixtures/ref_sec0.gdas.t12z.pgrb2.1p00.anl.75r.grib2.txt");
494///
495/// // parse the .idx file and apply a filter that we only need 3 sections
496/// let idx_data = fs::read_to_string(path).unwrap();
497/// let sections = parse_idx(
498///     idx_data,
499///     vec![":DZDT:0.01 mb:".into(), ":TMP:0.4 mb:".into(), ":ABSV:0.4 mb:anl:".into()],
500///     None,
501/// );
502///
503/// // grab the grib2 file itself building with the filtered IDX sections
504/// let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
505/// path.push("tests/readers/grib2/fixtures/ref_sec0.gdas.t12z.pgrb2.1p00.anl.75r.grib2");
506/// let bytes = std::fs::read(path.clone()).unwrap();
507/// let grib2_reader = GRIB2Reader::from_idx(&BufferReader::from(bytes), sections);
508///
509/// let features: Vec<_> = grib2_reader.iter().collect();
510/// assert_eq!(features.len(), 1);
511/// ```
512///
513/// ### Parsing the entire grib file:
514/// ```rust
515/// use gistools::{parsers::{BufferReader, FeatureReader}, readers::GRIB2Reader};
516/// use std::{fs, path::PathBuf};
517///
518/// let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
519/// path.push("tests/readers/grib2/fixtures/ref_simple_packing.grib2");
520///
521/// let bytes = fs::read(path.clone()).unwrap();
522/// let grib2_reader = GRIB2Reader::new(BufferReader::from(bytes).into(), vec![]);
523///
524/// let features: Vec<_> = grib2_reader.iter().collect();
525/// assert_eq!(features.len(), 1);
526/// ```
527///
528/// ## Links
529/// - <https://en.wikipedia.org/wiki/GRIB>
530/// - <https://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc/>
531#[derive(Debug, Clone)]
532pub struct GRIB2Reader {
533    /// The GRIB2 packets
534    pub packets: RefCell<Vec<Grib2Sections>>,
535    /// The list of section locations
536    pub idxs: Vec<Grib2SectionLocations>,
537}
538impl GRIB2Reader {
539    /// Create a GRIB2Reader
540    ///
541    /// ## Parameters
542    /// - `readers`: Reader(s) for entire GRIB file. If array, its grib chunks, otherwise it will be the entire file
543    /// - `idxs`: The list of section locations we will be parsing
544    ///
545    /// ## Returns
546    /// A [`GRIB2Reader`]
547    pub fn new<T: Reader>(readers: GRIB2ReaderInput<T>, idxs: Vec<Grib2SectionLocations>) -> Self {
548        let this = GRIB2Reader { packets: vec![].into(), idxs };
549        let grib_chunks = match readers {
550            GRIB2ReaderInput::Reader(reader) => split_grib_chunks(&reader),
551            GRIB2ReaderInput::SectionChunks(chunks) => chunks,
552        };
553        for grib_chunk in grib_chunks {
554            this.packets.borrow_mut().push(split_section_chunks(grib_chunk));
555        }
556
557        this
558    }
559
560    /// Create a GRIB2Reader from a .idx file
561    ///
562    /// ## Parameters
563    /// - `source`: Either the http path to the .idx file or the entire GRIB file
564    /// - `idxs`: The parsed .idx file with the locations of each section
565    ///
566    /// ## Returns
567    /// A GRIB2Reader of the specific sections
568    pub fn from_idx<T: Reader>(source: &T, idxs: Vec<Grib2SectionLocations>) -> GRIB2Reader {
569        let mut readers: Vec<BufferReader> = vec![];
570        for idx in &idxs {
571            readers.push(BufferReader::new(source.slice(Some(idx.start), idx.end)));
572        }
573        GRIB2Reader::new::<T>(readers.into(), idxs)
574    }
575
576    /// Get the Vector Point feature data
577    pub fn get_data(&self) -> Option<VectorMultiPoint> {
578        let geo_grid = self
579            .packets
580            .borrow_mut()
581            .get_mut(0)
582            .and_then(|p| Some(p.grid_definition.as_mut()?.values.build_grid()));
583        // setup geometry
584        if let Some(mut geometry) = geo_grid {
585            // add M-Values from each packet
586            for (i, packet) in self.packets.borrow().iter().enumerate() {
587                let name = self.idxs.get(i).map(|i| i.name.clone()).unwrap_or(i.to_string());
588                if let Some(data) = packet.data.as_ref().map(|d| d.data(packet)) {
589                    for (i, geo) in geometry.iter_mut().enumerate().take(data.len()) {
590                        if let Some(m_value) = data.get(i) {
591                            if geo.m.is_none() {
592                                geo.m = Some(MValue::new());
593                            }
594                            geo.m.as_mut().unwrap().insert((&name).into(), (*m_value).into());
595                        }
596                    }
597                }
598            }
599            Some(geometry)
600        } else {
601            None
602        }
603    }
604
605    /// Get the Vector Point feature
606    pub fn get_feature(&self) -> Option<GRIB2VectorFeature> {
607        if let Some(geometry) = self.get_data() {
608            // setup metadata
609            let product_metadata: Vec<Grib2ProductDefinition> = self
610                .packets
611                .borrow()
612                .iter()
613                .filter_map(|packet| Some(packet.product_definition.as_ref()?.values.clone()))
614                .collect();
615            // setup bbox
616            let bbox = BBox3D::from_linestring(&geometry);
617            Some(GRIB2VectorFeature::new_wm(
618                None,
619                Properties::default(),
620                VectorGeometry::new_multipoint(geometry, Some(bbox)),
621                Some(product_metadata),
622            ))
623        } else {
624            None
625        }
626    }
627}
628
629/// The GRIB2 Iterator tool
630#[derive(Debug)]
631pub struct GRIB2Iterator<'a> {
632    reader: &'a GRIB2Reader,
633    done: bool,
634}
635impl Iterator for GRIB2Iterator<'_> {
636    type Item = GRIB2VectorFeature;
637
638    fn next(&mut self) -> Option<Self::Item> {
639        if self.done {
640            return None;
641        }
642        self.done = true;
643        self.reader.get_feature()
644    }
645}
646/// A feature reader trait with a callback-based approach
647impl FeatureReader<Vec<Grib2ProductDefinition>, Properties, MValue> for GRIB2Reader {
648    type FeatureIterator<'a> = GRIB2Iterator<'a>;
649
650    fn iter(&self) -> Self::FeatureIterator<'_> {
651        GRIB2Iterator { reader: self, done: false }
652    }
653
654    fn par_iter(&self, _pool_size: usize, thread_id: usize) -> Self::FeatureIterator<'_> {
655        if thread_id == 0 { self.iter() } else { GRIB2Iterator { reader: self, done: true } }
656    }
657}
658
659/// Split the bytes of the GRIB file into individual GRIB chunks that represent sections
660///
661/// ## Parameters
662/// - `reader`: Reader for entire GRIB file
663///
664/// ## Returns
665/// Array of GRIB Chunk Buffers containing individual GRIB definitions in file
666fn split_grib_chunks<T: Reader>(reader: &T) -> Vec<BufferReader> {
667    if reader.len() == 0 {
668        return vec![];
669    }
670    let length = reader.uint64_be(Some(8));
671    let grib_data = BufferReader::new(reader.slice(Some(0), Some(length)));
672
673    let mut chunks: Vec<BufferReader> = vec![grib_data];
674    if length == reader.len() {
675        return chunks;
676    }
677    let rest = BufferReader::new(reader.slice(Some(length), None));
678    chunks.append(&mut split_grib_chunks(&rest));
679
680    chunks
681}