Skip to main content

grib_reader/
grid.rs

1//! Grid Definition Section (Section 3) parsing.
2
3use crate::error::{Error, Result};
4use crate::util::grib_i32;
5
6/// Grid definition extracted from Section 3.
7#[derive(Debug, Clone, PartialEq)]
8pub enum GridDefinition {
9    /// Template 3.0: Regular latitude/longitude (equidistant cylindrical).
10    LatLon(LatLonGrid),
11    /// Unsupported template (stored for diagnostics).
12    Unsupported(u16),
13}
14
15/// Template 3.0: Regular latitude/longitude grid.
16#[derive(Debug, Clone, PartialEq)]
17pub struct LatLonGrid {
18    pub ni: u32,
19    pub nj: u32,
20    pub lat_first: i32,
21    pub lon_first: i32,
22    pub lat_last: i32,
23    pub lon_last: i32,
24    pub di: u32,
25    pub dj: u32,
26    pub scanning_mode: u8,
27}
28
29impl GridDefinition {
30    pub fn shape(&self) -> (usize, usize) {
31        match self {
32            Self::LatLon(g) => (g.ni as usize, g.nj as usize),
33            Self::Unsupported(_) => (0, 0),
34        }
35    }
36
37    pub fn ndarray_shape(&self) -> Vec<usize> {
38        let (ni, nj) = self.shape();
39        match self {
40            Self::LatLon(_) if ni > 0 && nj > 0 => vec![nj, ni],
41            _ => Vec::new(),
42        }
43    }
44
45    pub fn num_points(&self) -> usize {
46        let (ni, nj) = self.shape();
47        ni.saturating_mul(nj)
48    }
49
50    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
51        if section_bytes.len() < 14 {
52            return Err(Error::InvalidSection {
53                section: 3,
54                reason: format!("expected at least 14 bytes, got {}", section_bytes.len()),
55            });
56        }
57        if section_bytes[4] != 3 {
58            return Err(Error::InvalidSection {
59                section: section_bytes[4],
60                reason: "not a grid definition section".into(),
61            });
62        }
63
64        let template = u16::from_be_bytes(section_bytes[12..14].try_into().unwrap());
65        match template {
66            0 => parse_latlon(section_bytes),
67            _ => Ok(Self::Unsupported(template)),
68        }
69    }
70}
71
72impl LatLonGrid {
73    pub fn longitudes(&self) -> Vec<f64> {
74        let step = self.di as f64 / 1_000_000.0;
75        let signed_step = if self.i_scans_positive() { step } else { -step };
76        let start = self.lon_first as f64 / 1_000_000.0;
77        (0..self.ni)
78            .map(|index| start + signed_step * index as f64)
79            .collect()
80    }
81
82    pub fn latitudes(&self) -> Vec<f64> {
83        let step = self.dj as f64 / 1_000_000.0;
84        let signed_step = if self.j_scans_positive() { step } else { -step };
85        let start = self.lat_first as f64 / 1_000_000.0;
86        (0..self.nj)
87            .map(|index| start + signed_step * index as f64)
88            .collect()
89    }
90
91    pub fn reorder_for_ndarray<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
92        self.reorder_for_ndarray_in_place(&mut values)?;
93        Ok(values)
94    }
95
96    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
97        let ni = self.ni as usize;
98        let nj = self.nj as usize;
99        if values.len() != ni * nj {
100            return Err(Error::DataLengthMismatch {
101                expected: ni * nj,
102                actual: values.len(),
103            });
104        }
105
106        if !self.i_points_are_consecutive() {
107            return Err(Error::UnsupportedScanningMode(self.scanning_mode));
108        }
109
110        if self.adjacent_rows_alternate_direction() {
111            for row in 0..nj {
112                let reverse = if self.i_scans_positive() {
113                    row % 2 == 1
114                } else {
115                    row % 2 == 0
116                };
117                if reverse {
118                    values[row * ni..(row + 1) * ni].reverse();
119                }
120            }
121        }
122
123        Ok(())
124    }
125
126    fn i_scans_positive(&self) -> bool {
127        self.scanning_mode & 0b1000_0000 == 0
128    }
129
130    fn j_scans_positive(&self) -> bool {
131        self.scanning_mode & 0b0100_0000 != 0
132    }
133
134    fn i_points_are_consecutive(&self) -> bool {
135        self.scanning_mode & 0b0010_0000 == 0
136    }
137
138    fn adjacent_rows_alternate_direction(&self) -> bool {
139        self.scanning_mode & 0b0001_0000 != 0
140    }
141}
142
143fn parse_latlon(data: &[u8]) -> Result<GridDefinition> {
144    if data.len() < 72 {
145        return Err(Error::InvalidSection {
146            section: 3,
147            reason: format!("template 3.0 requires 72 bytes, got {}", data.len()),
148        });
149    }
150
151    let ni = u32::from_be_bytes(data[30..34].try_into().unwrap());
152    let nj = u32::from_be_bytes(data[34..38].try_into().unwrap());
153    let lat_first = grib_i32(&data[46..50]).unwrap();
154    let lon_first = grib_i32(&data[50..54]).unwrap();
155    let lat_last = grib_i32(&data[55..59]).unwrap();
156    let lon_last = grib_i32(&data[59..63]).unwrap();
157    let di = u32::from_be_bytes(data[63..67].try_into().unwrap());
158    let dj = u32::from_be_bytes(data[67..71].try_into().unwrap());
159    let scanning_mode = data[71];
160
161    Ok(GridDefinition::LatLon(LatLonGrid {
162        ni,
163        nj,
164        lat_first,
165        lon_first,
166        lat_last,
167        lon_last,
168        di,
169        dj,
170        scanning_mode,
171    }))
172}
173
174#[cfg(test)]
175mod tests {
176    use super::{GridDefinition, LatLonGrid};
177
178    #[test]
179    fn reports_latlon_shape() {
180        let grid = GridDefinition::LatLon(LatLonGrid {
181            ni: 3,
182            nj: 2,
183            lat_first: 50_000_000,
184            lon_first: -120_000_000,
185            lat_last: 49_000_000,
186            lon_last: -118_000_000,
187            di: 1_000_000,
188            dj: 1_000_000,
189            scanning_mode: 0,
190        });
191
192        assert_eq!(grid.shape(), (3, 2));
193        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
194        match grid {
195            GridDefinition::LatLon(ref latlon) => {
196                assert_eq!(latlon.longitudes(), vec![-120.0, -119.0, -118.0]);
197                assert_eq!(latlon.latitudes(), vec![50.0, 49.0]);
198            }
199            GridDefinition::Unsupported(_) => panic!("expected lat/lon grid"),
200        }
201    }
202
203    #[test]
204    fn normalizes_alternating_row_scan() {
205        let grid = LatLonGrid {
206            ni: 3,
207            nj: 2,
208            lat_first: 0,
209            lon_first: 0,
210            lat_last: 0,
211            lon_last: 0,
212            di: 1,
213            dj: 1,
214            scanning_mode: 0b0001_0000,
215        };
216
217        let ordered = grid
218            .reorder_for_ndarray(vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0])
219            .unwrap();
220        assert_eq!(ordered, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
221    }
222}