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///
8/// This enum is non-exhaustive because GRIB grid templates are open-ended:
9/// WMO can add standard templates and centers can use local templates. Prefer
10/// the query helpers on this type for common behavior, or keep a wildcard arm
11/// when matching specific grid families.
12#[derive(Debug, Clone, PartialEq)]
13#[non_exhaustive]
14pub enum GridDefinition {
15    /// Template 3.0: Regular latitude/longitude (equidistant cylindrical).
16    LatLon(LatLonGrid),
17    /// Template 3.30: Lambert conformal.
18    LambertConformal(LambertConformalGrid),
19    /// Template 3.20: Polar stereographic projection.
20    PolarStereographic(PolarStereographicGrid),
21    /// Unsupported template (stored for diagnostics).
22    Unsupported(u16),
23}
24
25/// Template 3.0: Regular latitude/longitude grid.
26#[derive(Debug, Clone, PartialEq)]
27pub struct LatLonGrid {
28    pub ni: u32,
29    pub nj: u32,
30    pub lat_first: i32,
31    pub lon_first: i32,
32    pub lat_last: i32,
33    pub lon_last: i32,
34    pub di: u32,
35    pub dj: u32,
36    pub scanning_mode: u8,
37}
38
39/// Template 3.30: Lambert conformal grid.
40#[derive(Debug, Clone, PartialEq)]
41pub struct LambertConformalGrid {
42    pub number_of_points: u32,
43    pub shape_of_earth: u8,
44    pub scale_factor_radius: u8,
45    pub scaled_value_radius: u32,
46    pub scale_factor_major_axis: u8,
47    pub scaled_value_major_axis: u32,
48    pub scale_factor_minor_axis: u8,
49    pub scaled_value_minor_axis: u32,
50    pub nx: u32,
51    pub ny: u32,
52    pub lat_first: i32,
53    pub lon_first: u32,
54    pub resolution_and_component_flags: u8,
55    pub lat_d: i32,
56    pub lon_v: u32,
57    pub dx: u32,
58    pub dy: u32,
59    pub projection_center_flag: u8,
60    pub scanning_mode: u8,
61    pub latin1: i32,
62    pub latin2: i32,
63    pub lat_southern_pole: i32,
64    pub lon_southern_pole: u32,
65}
66
67/// Template 3.20: Polar stereographic projection.
68#[derive(Debug, Clone, PartialEq)]
69pub struct PolarStereographicGrid {
70    pub number_of_points: u32,
71    pub shape_of_earth: u8,
72    pub scale_factor_radius: u8,
73    pub scaled_value_radius: u32,
74    pub scale_factor_major_axis: u8,
75    pub scaled_value_major_axis: u32,
76    pub scale_factor_minor_axis: u8,
77    pub scaled_value_minor_axis: u32,
78    pub nx: u32,
79    pub ny: u32,
80    pub lat_first: i32,
81    pub lon_first: u32,
82    pub resolution_and_component_flags: u8,
83    pub lat_d: i32,
84    pub lon_v: u32,
85    pub dx: u32,
86    pub dy: u32,
87    pub projection_center_flag: u8,
88    pub scanning_mode: u8,
89}
90
91impl GridDefinition {
92    /// GRIB2 grid definition template number for typed templates.
93    ///
94    /// `Unsupported` stores the unhandled template number from the source
95    /// message for diagnostics.
96    pub fn template_number(&self) -> u16 {
97        match self {
98            Self::LatLon(_) => 0,
99            Self::PolarStereographic(_) => 20,
100            Self::LambertConformal(_) => 30,
101            Self::Unsupported(template) => *template,
102        }
103    }
104
105    pub fn as_lat_lon(&self) -> Option<&LatLonGrid> {
106        match self {
107            Self::LatLon(grid) => Some(grid),
108            _ => None,
109        }
110    }
111
112    pub fn as_polar_stereographic(&self) -> Option<&PolarStereographicGrid> {
113        match self {
114            Self::PolarStereographic(grid) => Some(grid),
115            _ => None,
116        }
117    }
118
119    pub fn as_lambert_conformal(&self) -> Option<&LambertConformalGrid> {
120        match self {
121            Self::LambertConformal(grid) => Some(grid),
122            _ => None,
123        }
124    }
125
126    pub fn unsupported_template(&self) -> Option<u16> {
127        match self {
128            Self::Unsupported(template) => Some(*template),
129            _ => None,
130        }
131    }
132
133    pub fn shape(&self) -> (usize, usize) {
134        match self {
135            Self::LatLon(g) => (g.ni as usize, g.nj as usize),
136            Self::PolarStereographic(g) => (g.nx as usize, g.ny as usize),
137            Self::LambertConformal(g) => (g.nx as usize, g.ny as usize),
138            Self::Unsupported(_) => (0, 0),
139        }
140    }
141
142    pub fn ndarray_shape(&self) -> Vec<usize> {
143        let (ni, nj) = self.shape();
144        match self {
145            Self::LatLon(_) | Self::PolarStereographic(_) | Self::LambertConformal(_)
146                if ni > 0 && nj > 0 =>
147            {
148                vec![nj, ni]
149            }
150            _ => Vec::new(),
151        }
152    }
153
154    pub fn num_points(&self) -> usize {
155        match self {
156            Self::LatLon(_) => {
157                let (ni, nj) = self.shape();
158                ni.saturating_mul(nj)
159            }
160            Self::PolarStereographic(g) => g.number_of_points as usize,
161            Self::LambertConformal(g) => g.number_of_points as usize,
162            Self::Unsupported(_) => 0,
163        }
164    }
165
166    pub fn validate_supported_scan_order(&self) -> Result<()> {
167        match self {
168            Self::LatLon(grid) => grid.validate_supported_scan_order(),
169            Self::PolarStereographic(grid) => grid.validate_supported_scan_order(),
170            Self::LambertConformal(grid) => grid.validate_supported_scan_order(),
171            Self::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
172        }
173    }
174
175    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
176        match self {
177            Self::LatLon(grid) => grid.reorder_for_ndarray_in_place(values),
178            Self::PolarStereographic(grid) => grid.reorder_for_ndarray_in_place(values),
179            Self::LambertConformal(grid) => grid.reorder_for_ndarray_in_place(values),
180            Self::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
181        }
182    }
183
184    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
185        if section_bytes.len() < 14 {
186            return Err(Error::InvalidSection {
187                section: 3,
188                reason: format!("expected at least 14 bytes, got {}", section_bytes.len()),
189            });
190        }
191        if section_bytes[4] != 3 {
192            return Err(Error::InvalidSection {
193                section: section_bytes[4],
194                reason: "not a grid definition section".into(),
195            });
196        }
197
198        let template = u16::from_be_bytes(section_bytes[12..14].try_into().unwrap());
199        match template {
200            0 => parse_latlon(section_bytes),
201            20 => parse_polar_stereographic(section_bytes),
202            30 => parse_lambert_conformal(section_bytes),
203            _ => Ok(Self::Unsupported(template)),
204        }
205    }
206}
207
208impl LatLonGrid {
209    pub fn longitudes(&self) -> Vec<f64> {
210        let step = self.di as f64 / 1_000_000.0;
211        let signed_step = if self.i_scans_positive() { step } else { -step };
212        let start = self.lon_first as f64 / 1_000_000.0;
213        (0..self.ni)
214            .map(|index| start + signed_step * index as f64)
215            .collect()
216    }
217
218    pub fn latitudes(&self) -> Vec<f64> {
219        let step = self.dj as f64 / 1_000_000.0;
220        let signed_step = if self.j_scans_positive() { step } else { -step };
221        let start = self.lat_first as f64 / 1_000_000.0;
222        (0..self.nj)
223            .map(|index| start + signed_step * index as f64)
224            .collect()
225    }
226
227    pub fn reorder_for_ndarray<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
228        self.reorder_grib_scan_to_ndarray_in_place(&mut values)?;
229        Ok(values)
230    }
231
232    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
233        self.reorder_grib_scan_to_ndarray_in_place(values)
234    }
235
236    pub fn reorder_grib_scan_to_ndarray<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
237        self.reorder_grib_scan_to_ndarray_in_place(&mut values)?;
238        Ok(values)
239    }
240
241    pub fn reorder_grib_scan_to_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
242        transform_supported_scan_order_in_place(
243            values,
244            self.ni as usize,
245            self.nj as usize,
246            self.scanning_mode,
247        )
248    }
249
250    pub fn reorder_ndarray_to_grib_scan<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
251        self.reorder_ndarray_to_grib_scan_in_place(&mut values)?;
252        Ok(values)
253    }
254
255    pub fn reorder_ndarray_to_grib_scan_in_place<T>(&self, values: &mut [T]) -> Result<()> {
256        transform_supported_scan_order_in_place(
257            values,
258            self.ni as usize,
259            self.nj as usize,
260            self.scanning_mode,
261        )
262    }
263
264    pub fn validate_supported_scan_order(&self) -> Result<()> {
265        validate_supported_scan_order(self.scanning_mode)
266    }
267
268    fn i_scans_positive(&self) -> bool {
269        i_scans_positive(self.scanning_mode)
270    }
271
272    fn j_scans_positive(&self) -> bool {
273        j_scans_positive(self.scanning_mode)
274    }
275}
276
277impl LambertConformalGrid {
278    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
279        transform_supported_scan_order_in_place(
280            values,
281            self.nx as usize,
282            self.ny as usize,
283            self.scanning_mode,
284        )
285    }
286
287    pub fn validate_supported_scan_order(&self) -> Result<()> {
288        validate_supported_scan_order(self.scanning_mode)
289    }
290}
291
292impl PolarStereographicGrid {
293    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
294        transform_supported_scan_order_in_place(
295            values,
296            self.nx as usize,
297            self.ny as usize,
298            self.scanning_mode,
299        )
300    }
301
302    pub fn validate_supported_scan_order(&self) -> Result<()> {
303        validate_supported_scan_order(self.scanning_mode)
304    }
305}
306
307fn transform_supported_scan_order_in_place<T>(
308    values: &mut [T],
309    ni: usize,
310    nj: usize,
311    scanning_mode: u8,
312) -> Result<()> {
313    validate_supported_scan_order(scanning_mode)?;
314    if values.len() != ni * nj {
315        return Err(Error::DataLengthMismatch {
316            expected: ni * nj,
317            actual: values.len(),
318        });
319    }
320
321    if adjacent_rows_alternate_direction(scanning_mode) {
322        reverse_alternating_rows(values, ni, nj, i_scans_positive(scanning_mode));
323    }
324
325    Ok(())
326}
327
328fn validate_supported_scan_order(scanning_mode: u8) -> Result<()> {
329    if i_points_are_consecutive(scanning_mode) {
330        Ok(())
331    } else {
332        Err(Error::UnsupportedScanningMode(scanning_mode))
333    }
334}
335
336fn i_scans_positive(scanning_mode: u8) -> bool {
337    scanning_mode & 0b1000_0000 == 0
338}
339
340fn j_scans_positive(scanning_mode: u8) -> bool {
341    scanning_mode & 0b0100_0000 != 0
342}
343
344fn i_points_are_consecutive(scanning_mode: u8) -> bool {
345    scanning_mode & 0b0010_0000 == 0
346}
347
348fn adjacent_rows_alternate_direction(scanning_mode: u8) -> bool {
349    scanning_mode & 0b0001_0000 != 0
350}
351
352fn reverse_alternating_rows<T>(values: &mut [T], ni: usize, nj: usize, i_scans_positive: bool) {
353    for row in 0..nj {
354        let reverse = if i_scans_positive {
355            row % 2 == 1
356        } else {
357            row % 2 == 0
358        };
359        if reverse {
360            values[row * ni..(row + 1) * ni].reverse();
361        }
362    }
363}
364
365fn parse_latlon(data: &[u8]) -> Result<GridDefinition> {
366    if data.len() < 72 {
367        return Err(Error::InvalidSection {
368            section: 3,
369            reason: format!("template 3.0 requires 72 bytes, got {}", data.len()),
370        });
371    }
372
373    let ni = u32::from_be_bytes(data[30..34].try_into().unwrap());
374    let nj = u32::from_be_bytes(data[34..38].try_into().unwrap());
375    let lat_first = grib_i32(&data[46..50]).unwrap();
376    let lon_first = grib_i32(&data[50..54]).unwrap();
377    let lat_last = grib_i32(&data[55..59]).unwrap();
378    let lon_last = grib_i32(&data[59..63]).unwrap();
379    let di = u32::from_be_bytes(data[63..67].try_into().unwrap());
380    let dj = u32::from_be_bytes(data[67..71].try_into().unwrap());
381    let scanning_mode = data[71];
382
383    Ok(GridDefinition::LatLon(LatLonGrid {
384        ni,
385        nj,
386        lat_first,
387        lon_first,
388        lat_last,
389        lon_last,
390        di,
391        dj,
392        scanning_mode,
393    }))
394}
395
396fn parse_polar_stereographic(data: &[u8]) -> Result<GridDefinition> {
397    if data.len() < 65 {
398        return Err(Error::InvalidSection {
399            section: 3,
400            reason: format!("template 3.20 requires 65 bytes, got {}", data.len()),
401        });
402    }
403
404    Ok(GridDefinition::PolarStereographic(PolarStereographicGrid {
405        number_of_points: u32::from_be_bytes(data[6..10].try_into().unwrap()),
406        shape_of_earth: data[14],
407        scale_factor_radius: data[15],
408        scaled_value_radius: u32::from_be_bytes(data[16..20].try_into().unwrap()),
409        scale_factor_major_axis: data[20],
410        scaled_value_major_axis: u32::from_be_bytes(data[21..25].try_into().unwrap()),
411        scale_factor_minor_axis: data[25],
412        scaled_value_minor_axis: u32::from_be_bytes(data[26..30].try_into().unwrap()),
413        nx: u32::from_be_bytes(data[30..34].try_into().unwrap()),
414        ny: u32::from_be_bytes(data[34..38].try_into().unwrap()),
415        lat_first: grib_i32(&data[38..42]).unwrap(),
416        lon_first: u32::from_be_bytes(data[42..46].try_into().unwrap()),
417        resolution_and_component_flags: data[46],
418        lat_d: grib_i32(&data[47..51]).unwrap(),
419        lon_v: u32::from_be_bytes(data[51..55].try_into().unwrap()),
420        dx: u32::from_be_bytes(data[55..59].try_into().unwrap()),
421        dy: u32::from_be_bytes(data[59..63].try_into().unwrap()),
422        projection_center_flag: data[63],
423        scanning_mode: data[64],
424    }))
425}
426
427fn parse_lambert_conformal(data: &[u8]) -> Result<GridDefinition> {
428    if data.len() < 81 {
429        return Err(Error::InvalidSection {
430            section: 3,
431            reason: format!("template 3.30 requires 81 bytes, got {}", data.len()),
432        });
433    }
434
435    Ok(GridDefinition::LambertConformal(LambertConformalGrid {
436        number_of_points: u32::from_be_bytes(data[6..10].try_into().unwrap()),
437        shape_of_earth: data[14],
438        scale_factor_radius: data[15],
439        scaled_value_radius: u32::from_be_bytes(data[16..20].try_into().unwrap()),
440        scale_factor_major_axis: data[20],
441        scaled_value_major_axis: u32::from_be_bytes(data[21..25].try_into().unwrap()),
442        scale_factor_minor_axis: data[25],
443        scaled_value_minor_axis: u32::from_be_bytes(data[26..30].try_into().unwrap()),
444        nx: u32::from_be_bytes(data[30..34].try_into().unwrap()),
445        ny: u32::from_be_bytes(data[34..38].try_into().unwrap()),
446        lat_first: grib_i32(&data[38..42]).unwrap(),
447        lon_first: u32::from_be_bytes(data[42..46].try_into().unwrap()),
448        resolution_and_component_flags: data[46],
449        lat_d: grib_i32(&data[47..51]).unwrap(),
450        lon_v: u32::from_be_bytes(data[51..55].try_into().unwrap()),
451        dx: u32::from_be_bytes(data[55..59].try_into().unwrap()),
452        dy: u32::from_be_bytes(data[59..63].try_into().unwrap()),
453        projection_center_flag: data[63],
454        scanning_mode: data[64],
455        latin1: grib_i32(&data[65..69]).unwrap(),
456        latin2: grib_i32(&data[69..73]).unwrap(),
457        lat_southern_pole: grib_i32(&data[73..77]).unwrap(),
458        lon_southern_pole: u32::from_be_bytes(data[77..81].try_into().unwrap()),
459    }))
460}
461
462#[cfg(test)]
463mod tests {
464    use super::{GridDefinition, LambertConformalGrid, LatLonGrid, PolarStereographicGrid};
465    use crate::binary::encode_wmo_i32;
466
467    #[test]
468    fn reports_latlon_shape() {
469        let grid = GridDefinition::LatLon(LatLonGrid {
470            ni: 3,
471            nj: 2,
472            lat_first: 50_000_000,
473            lon_first: -120_000_000,
474            lat_last: 49_000_000,
475            lon_last: -118_000_000,
476            di: 1_000_000,
477            dj: 1_000_000,
478            scanning_mode: 0,
479        });
480
481        assert_eq!(grid.shape(), (3, 2));
482        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
483        assert_eq!(grid.template_number(), 0);
484        assert!(grid.as_lat_lon().is_some());
485        assert!(grid.as_polar_stereographic().is_none());
486        assert!(grid.as_lambert_conformal().is_none());
487        assert_eq!(grid.unsupported_template(), None);
488        match grid {
489            GridDefinition::LatLon(ref latlon) => {
490                assert_eq!(latlon.longitudes(), vec![-120.0, -119.0, -118.0]);
491                assert_eq!(latlon.latitudes(), vec![50.0, 49.0]);
492            }
493            other => panic!("expected lat/lon grid, got {other:?}"),
494        }
495    }
496
497    #[test]
498    fn parses_polar_stereographic_template() {
499        let section = build_polar_stereographic_section(0);
500        let grid = GridDefinition::parse(&section).unwrap();
501
502        assert_eq!(grid.shape(), (3, 2));
503        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
504        assert_eq!(grid.num_points(), 6);
505        assert_eq!(grid.template_number(), 20);
506        assert!(grid.as_lat_lon().is_none());
507        assert!(grid.as_polar_stereographic().is_some());
508        assert!(grid.as_lambert_conformal().is_none());
509        assert_eq!(grid.unsupported_template(), None);
510        match grid {
511            GridDefinition::PolarStereographic(polar) => {
512                assert_eq!(polar.number_of_points, 6);
513                assert_eq!(polar.shape_of_earth, 6);
514                assert_eq!(polar.nx, 3);
515                assert_eq!(polar.ny, 2);
516                assert_eq!(polar.lat_first, 41_612_949);
517                assert_eq!(polar.lon_first, 185_117_126);
518                assert_eq!(polar.resolution_and_component_flags, 0x08);
519                assert_eq!(polar.lat_d, 60_000_000);
520                assert_eq!(polar.lon_v, 225_000_000);
521                assert_eq!(polar.dx, 3_000_000);
522                assert_eq!(polar.dy, 3_000_000);
523                assert_eq!(polar.projection_center_flag, 0);
524                assert_eq!(polar.scanning_mode, 0);
525            }
526            other => panic!("expected polar stereographic grid, got {other:?}"),
527        }
528    }
529
530    #[test]
531    fn parses_lambert_conformal_template() {
532        let section = build_lambert_section();
533        let grid = GridDefinition::parse(&section).unwrap();
534
535        assert_eq!(grid.shape(), (3, 2));
536        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
537        assert_eq!(grid.num_points(), 6);
538        assert_eq!(grid.template_number(), 30);
539        assert!(grid.as_lat_lon().is_none());
540        assert!(grid.as_lambert_conformal().is_some());
541        assert_eq!(grid.unsupported_template(), None);
542        match grid {
543            GridDefinition::LambertConformal(lambert) => {
544                assert_eq!(lambert.number_of_points, 6);
545                assert_eq!(lambert.shape_of_earth, 1);
546                assert_eq!(lambert.scaled_value_radius, 6_371_200);
547                assert_eq!(lambert.nx, 3);
548                assert_eq!(lambert.ny, 2);
549                assert_eq!(lambert.lat_first, 12_190_000);
550                assert_eq!(lambert.lon_first, 226_541_000);
551                assert_eq!(lambert.resolution_and_component_flags, 0x08);
552                assert_eq!(lambert.lat_d, 25_000_000);
553                assert_eq!(lambert.lon_v, 265_000_000);
554                assert_eq!(lambert.dx, 2_539_703);
555                assert_eq!(lambert.dy, 2_539_703);
556                assert_eq!(lambert.projection_center_flag, 0);
557                assert_eq!(lambert.scanning_mode, 0);
558                assert_eq!(lambert.latin1, 25_000_000);
559                assert_eq!(lambert.latin2, 25_000_000);
560                assert_eq!(lambert.lat_southern_pole, -90_000_000);
561                assert_eq!(lambert.lon_southern_pole, 0);
562            }
563            other => panic!("expected Lambert conformal grid, got {other:?}"),
564        }
565    }
566
567    #[test]
568    fn reports_unsupported_template_helpers() {
569        let grid = GridDefinition::Unsupported(3_276);
570
571        assert_eq!(grid.template_number(), 3_276);
572        assert!(grid.as_lat_lon().is_none());
573        assert!(grid.as_polar_stereographic().is_none());
574        assert!(grid.as_lambert_conformal().is_none());
575        assert_eq!(grid.unsupported_template(), Some(3_276));
576    }
577
578    #[test]
579    fn normalizes_alternating_row_scan() {
580        let grid = LatLonGrid {
581            ni: 3,
582            nj: 2,
583            lat_first: 0,
584            lon_first: 0,
585            lat_last: 0,
586            lon_last: 0,
587            di: 1,
588            dj: 1,
589            scanning_mode: 0b0001_0000,
590        };
591
592        let ordered = grid
593            .reorder_for_ndarray(vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0])
594            .unwrap();
595        assert_eq!(ordered, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
596    }
597
598    #[test]
599    fn converts_ndarray_order_to_alternating_scan_order() {
600        let grid = LatLonGrid {
601            ni: 3,
602            nj: 2,
603            lat_first: 0,
604            lon_first: 0,
605            lat_last: 0,
606            lon_last: 0,
607            di: 1,
608            dj: 1,
609            scanning_mode: 0b0001_0000,
610        };
611
612        let scan_order = grid
613            .reorder_ndarray_to_grib_scan(vec![1, 2, 3, 4, 5, 6])
614            .unwrap();
615        assert_eq!(scan_order, vec![1, 2, 3, 6, 5, 4]);
616    }
617
618    #[test]
619    fn normalizes_polar_stereographic_alternating_row_scan() {
620        let grid = PolarStereographicGrid {
621            number_of_points: 6,
622            shape_of_earth: 6,
623            scale_factor_radius: 0,
624            scaled_value_radius: 0,
625            scale_factor_major_axis: 0,
626            scaled_value_major_axis: 0,
627            scale_factor_minor_axis: 0,
628            scaled_value_minor_axis: 0,
629            nx: 3,
630            ny: 2,
631            lat_first: 0,
632            lon_first: 0,
633            resolution_and_component_flags: 0,
634            lat_d: 0,
635            lon_v: 0,
636            dx: 1,
637            dy: 1,
638            projection_center_flag: 0,
639            scanning_mode: 0b0001_0000,
640        };
641
642        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
643        grid.reorder_for_ndarray_in_place(&mut values).unwrap();
644        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
645    }
646
647    #[test]
648    fn normalizes_lambert_alternating_row_scan() {
649        let grid = LambertConformalGrid {
650            number_of_points: 6,
651            shape_of_earth: 1,
652            scale_factor_radius: 0,
653            scaled_value_radius: 6_371_200,
654            scale_factor_major_axis: 0,
655            scaled_value_major_axis: 0,
656            scale_factor_minor_axis: 0,
657            scaled_value_minor_axis: 0,
658            nx: 3,
659            ny: 2,
660            lat_first: 0,
661            lon_first: 0,
662            resolution_and_component_flags: 0,
663            lat_d: 0,
664            lon_v: 0,
665            dx: 1,
666            dy: 1,
667            projection_center_flag: 0,
668            scanning_mode: 0b0001_0000,
669            latin1: 0,
670            latin2: 0,
671            lat_southern_pole: 0,
672            lon_southern_pole: 0,
673        };
674
675        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
676        grid.reorder_for_ndarray_in_place(&mut values).unwrap();
677        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
678    }
679
680    #[test]
681    fn preserves_non_alternating_scan_modes_in_current_reader_order() {
682        for scanning_mode in [0b0000_0000, 0b1000_0000, 0b0100_0000, 0b1100_0000] {
683            let grid = LatLonGrid {
684                ni: 3,
685                nj: 2,
686                lat_first: 0,
687                lon_first: 0,
688                lat_last: 0,
689                lon_last: 0,
690                di: 1,
691                dj: 1,
692                scanning_mode,
693            };
694
695            let values = vec![1, 2, 3, 4, 5, 6];
696            assert_eq!(
697                grid.reorder_grib_scan_to_ndarray(values.clone()).unwrap(),
698                values
699            );
700            assert_eq!(
701                grid.reorder_ndarray_to_grib_scan(values.clone()).unwrap(),
702                values
703            );
704        }
705    }
706
707    #[test]
708    fn rejects_j_consecutive_scan_order() {
709        let grid = LatLonGrid {
710            ni: 3,
711            nj: 2,
712            lat_first: 0,
713            lon_first: 0,
714            lat_last: 0,
715            lon_last: 0,
716            di: 1,
717            dj: 1,
718            scanning_mode: 0b0010_0000,
719        };
720
721        let err = grid
722            .reorder_ndarray_to_grib_scan(vec![1, 2, 3, 4, 5, 6])
723            .unwrap_err();
724        assert!(matches!(
725            err,
726            crate::Error::UnsupportedScanningMode(0b0010_0000)
727        ));
728    }
729
730    fn build_polar_stereographic_section(scanning_mode: u8) -> Vec<u8> {
731        let mut section = vec![0u8; 65];
732        section[..4].copy_from_slice(&65u32.to_be_bytes());
733        section[4] = 3;
734        section[6..10].copy_from_slice(&6u32.to_be_bytes());
735        section[12..14].copy_from_slice(&20u16.to_be_bytes());
736        section[14] = 6;
737        section[30..34].copy_from_slice(&3u32.to_be_bytes());
738        section[34..38].copy_from_slice(&2u32.to_be_bytes());
739        section[38..42].copy_from_slice(&encode_wmo_i32(41_612_949).unwrap());
740        section[42..46].copy_from_slice(&185_117_126u32.to_be_bytes());
741        section[46] = 0x08;
742        section[47..51].copy_from_slice(&encode_wmo_i32(60_000_000).unwrap());
743        section[51..55].copy_from_slice(&225_000_000u32.to_be_bytes());
744        section[55..59].copy_from_slice(&3_000_000u32.to_be_bytes());
745        section[59..63].copy_from_slice(&3_000_000u32.to_be_bytes());
746        section[64] = scanning_mode;
747        section
748    }
749
750    fn build_lambert_section() -> Vec<u8> {
751        let mut section = vec![0u8; 81];
752        section[..4].copy_from_slice(&81u32.to_be_bytes());
753        section[4] = 3;
754        section[6..10].copy_from_slice(&6u32.to_be_bytes());
755        section[12..14].copy_from_slice(&30u16.to_be_bytes());
756        section[14] = 1;
757        section[16..20].copy_from_slice(&6_371_200u32.to_be_bytes());
758        section[30..34].copy_from_slice(&3u32.to_be_bytes());
759        section[34..38].copy_from_slice(&2u32.to_be_bytes());
760        section[38..42].copy_from_slice(&encode_wmo_i32(12_190_000).unwrap());
761        section[42..46].copy_from_slice(&226_541_000u32.to_be_bytes());
762        section[46] = 0x08;
763        section[47..51].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
764        section[51..55].copy_from_slice(&265_000_000u32.to_be_bytes());
765        section[55..59].copy_from_slice(&2_539_703u32.to_be_bytes());
766        section[59..63].copy_from_slice(&2_539_703u32.to_be_bytes());
767        section[65..69].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
768        section[69..73].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
769        section[73..77].copy_from_slice(&encode_wmo_i32(-90_000_000).unwrap());
770        section
771    }
772}