Skip to main content

grib_core/
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_grib_scan_to_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        self.reorder_grib_scan_to_ndarray_in_place(values)
98    }
99
100    pub fn reorder_grib_scan_to_ndarray<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
101        self.reorder_grib_scan_to_ndarray_in_place(&mut values)?;
102        Ok(values)
103    }
104
105    pub fn reorder_grib_scan_to_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
106        self.transform_supported_scan_order_in_place(values)
107    }
108
109    pub fn reorder_ndarray_to_grib_scan<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
110        self.reorder_ndarray_to_grib_scan_in_place(&mut values)?;
111        Ok(values)
112    }
113
114    pub fn reorder_ndarray_to_grib_scan_in_place<T>(&self, values: &mut [T]) -> Result<()> {
115        self.transform_supported_scan_order_in_place(values)
116    }
117
118    pub fn validate_supported_scan_order(&self) -> Result<()> {
119        if self.i_points_are_consecutive() {
120            Ok(())
121        } else {
122            Err(Error::UnsupportedScanningMode(self.scanning_mode))
123        }
124    }
125
126    fn transform_supported_scan_order_in_place<T>(&self, values: &mut [T]) -> Result<()> {
127        self.validate_supported_scan_order()?;
128        let ni = self.ni as usize;
129        let nj = self.nj as usize;
130        if values.len() != ni * nj {
131            return Err(Error::DataLengthMismatch {
132                expected: ni * nj,
133                actual: values.len(),
134            });
135        }
136
137        if self.adjacent_rows_alternate_direction() {
138            reverse_alternating_rows(values, ni, nj, self.i_scans_positive());
139        }
140
141        Ok(())
142    }
143
144    fn i_scans_positive(&self) -> bool {
145        self.scanning_mode & 0b1000_0000 == 0
146    }
147
148    fn j_scans_positive(&self) -> bool {
149        self.scanning_mode & 0b0100_0000 != 0
150    }
151
152    fn i_points_are_consecutive(&self) -> bool {
153        self.scanning_mode & 0b0010_0000 == 0
154    }
155
156    fn adjacent_rows_alternate_direction(&self) -> bool {
157        self.scanning_mode & 0b0001_0000 != 0
158    }
159}
160
161fn reverse_alternating_rows<T>(values: &mut [T], ni: usize, nj: usize, i_scans_positive: bool) {
162    for row in 0..nj {
163        let reverse = if i_scans_positive {
164            row % 2 == 1
165        } else {
166            row % 2 == 0
167        };
168        if reverse {
169            values[row * ni..(row + 1) * ni].reverse();
170        }
171    }
172}
173
174fn parse_latlon(data: &[u8]) -> Result<GridDefinition> {
175    if data.len() < 72 {
176        return Err(Error::InvalidSection {
177            section: 3,
178            reason: format!("template 3.0 requires 72 bytes, got {}", data.len()),
179        });
180    }
181
182    let ni = u32::from_be_bytes(data[30..34].try_into().unwrap());
183    let nj = u32::from_be_bytes(data[34..38].try_into().unwrap());
184    let lat_first = grib_i32(&data[46..50]).unwrap();
185    let lon_first = grib_i32(&data[50..54]).unwrap();
186    let lat_last = grib_i32(&data[55..59]).unwrap();
187    let lon_last = grib_i32(&data[59..63]).unwrap();
188    let di = u32::from_be_bytes(data[63..67].try_into().unwrap());
189    let dj = u32::from_be_bytes(data[67..71].try_into().unwrap());
190    let scanning_mode = data[71];
191
192    Ok(GridDefinition::LatLon(LatLonGrid {
193        ni,
194        nj,
195        lat_first,
196        lon_first,
197        lat_last,
198        lon_last,
199        di,
200        dj,
201        scanning_mode,
202    }))
203}
204
205#[cfg(test)]
206mod tests {
207    use super::{GridDefinition, LatLonGrid};
208
209    #[test]
210    fn reports_latlon_shape() {
211        let grid = GridDefinition::LatLon(LatLonGrid {
212            ni: 3,
213            nj: 2,
214            lat_first: 50_000_000,
215            lon_first: -120_000_000,
216            lat_last: 49_000_000,
217            lon_last: -118_000_000,
218            di: 1_000_000,
219            dj: 1_000_000,
220            scanning_mode: 0,
221        });
222
223        assert_eq!(grid.shape(), (3, 2));
224        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
225        match grid {
226            GridDefinition::LatLon(ref latlon) => {
227                assert_eq!(latlon.longitudes(), vec![-120.0, -119.0, -118.0]);
228                assert_eq!(latlon.latitudes(), vec![50.0, 49.0]);
229            }
230            GridDefinition::Unsupported(_) => panic!("expected lat/lon grid"),
231        }
232    }
233
234    #[test]
235    fn normalizes_alternating_row_scan() {
236        let grid = LatLonGrid {
237            ni: 3,
238            nj: 2,
239            lat_first: 0,
240            lon_first: 0,
241            lat_last: 0,
242            lon_last: 0,
243            di: 1,
244            dj: 1,
245            scanning_mode: 0b0001_0000,
246        };
247
248        let ordered = grid
249            .reorder_for_ndarray(vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0])
250            .unwrap();
251        assert_eq!(ordered, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
252    }
253
254    #[test]
255    fn converts_ndarray_order_to_alternating_scan_order() {
256        let grid = LatLonGrid {
257            ni: 3,
258            nj: 2,
259            lat_first: 0,
260            lon_first: 0,
261            lat_last: 0,
262            lon_last: 0,
263            di: 1,
264            dj: 1,
265            scanning_mode: 0b0001_0000,
266        };
267
268        let scan_order = grid
269            .reorder_ndarray_to_grib_scan(vec![1, 2, 3, 4, 5, 6])
270            .unwrap();
271        assert_eq!(scan_order, vec![1, 2, 3, 6, 5, 4]);
272    }
273
274    #[test]
275    fn preserves_non_alternating_scan_modes_in_current_reader_order() {
276        for scanning_mode in [0b0000_0000, 0b1000_0000, 0b0100_0000, 0b1100_0000] {
277            let grid = LatLonGrid {
278                ni: 3,
279                nj: 2,
280                lat_first: 0,
281                lon_first: 0,
282                lat_last: 0,
283                lon_last: 0,
284                di: 1,
285                dj: 1,
286                scanning_mode,
287            };
288
289            let values = vec![1, 2, 3, 4, 5, 6];
290            assert_eq!(
291                grid.reorder_grib_scan_to_ndarray(values.clone()).unwrap(),
292                values
293            );
294            assert_eq!(
295                grid.reorder_ndarray_to_grib_scan(values.clone()).unwrap(),
296                values
297            );
298        }
299    }
300
301    #[test]
302    fn rejects_j_consecutive_scan_order() {
303        let grid = LatLonGrid {
304            ni: 3,
305            nj: 2,
306            lat_first: 0,
307            lon_first: 0,
308            lat_last: 0,
309            lon_last: 0,
310            di: 1,
311            dj: 1,
312            scanning_mode: 0b0010_0000,
313        };
314
315        let err = grid
316            .reorder_ndarray_to_grib_scan(vec![1, 2, 3, 4, 5, 6])
317            .unwrap_err();
318        assert!(matches!(
319            err,
320            crate::Error::UnsupportedScanningMode(0b0010_0000)
321        ));
322    }
323}