Skip to main content

grib_core/
grid.rs

1//! Grid Definition Section (Section 3) parsing.
2
3use crate::binary::decode_wmo_i32;
4use crate::error::{Error, Result};
5use crate::{ensure_limit, filled_vec};
6
7/// Grid definition extracted from Section 3.
8///
9/// This enum is non-exhaustive because GRIB grid templates are open-ended:
10/// WMO can add standard templates and centers can use local templates. Prefer
11/// the query helpers on this type for common behavior, or keep a wildcard arm
12/// when matching specific grid families.
13#[derive(Debug, Clone, PartialEq)]
14#[non_exhaustive]
15pub enum GridDefinition {
16    /// Template 3.0: Regular latitude/longitude (equidistant cylindrical).
17    LatLon(LatLonGrid),
18    /// Template 3.1: Rotated latitude/longitude.
19    RotatedLatLon(RotatedLatLonGrid),
20    /// Template 3.10: Mercator.
21    Mercator(MercatorGrid),
22    /// Template 3.31: Albers equal-area.
23    AlbersEqualArea(AlbersEqualAreaGrid),
24    /// Template 3.30: Lambert conformal.
25    LambertConformal(LambertConformalGrid),
26    /// Template 3.20: Polar stereographic projection.
27    PolarStereographic(PolarStereographicGrid),
28    /// Template 3.40: Regular Gaussian latitude/longitude.
29    RegularGaussian(RegularGaussianGrid),
30    /// Unsupported template (stored for diagnostics).
31    Unsupported(u16),
32}
33
34/// Template 3.0: Regular latitude/longitude grid.
35#[derive(Debug, Clone, PartialEq)]
36pub struct LatLonGrid {
37    pub ni: u32,
38    pub nj: u32,
39    pub lat_first: i32,
40    pub lon_first: i32,
41    pub lat_last: i32,
42    pub lon_last: i32,
43    pub di: u32,
44    pub dj: u32,
45    pub scanning_mode: u8,
46}
47
48/// Template 3.1: rotated latitude/longitude grid.
49#[derive(Debug, Clone, PartialEq)]
50pub struct RotatedLatLonGrid {
51    /// Grid coordinates expressed in the rotated coordinate system.
52    pub grid: LatLonGrid,
53    pub lat_southern_pole: i32,
54    pub lon_southern_pole: u32,
55    pub angle_of_rotation: f32,
56}
57
58/// Template 3.40: regular Gaussian latitude/longitude grid.
59#[derive(Debug, Clone, PartialEq)]
60pub struct RegularGaussianGrid {
61    pub ni: u32,
62    pub nj: u32,
63    pub lat_first: i32,
64    pub lon_first: i32,
65    pub lat_last: i32,
66    pub lon_last: i32,
67    pub di: u32,
68    /// Number of Gaussian parallels between a pole and the equator.
69    pub number_of_parallels: u32,
70    pub scanning_mode: u8,
71}
72
73/// Template 3.10: Mercator grid.
74#[derive(Debug, Clone, PartialEq)]
75pub struct MercatorGrid {
76    pub core: ProjectedGridCore,
77    pub lat_d: i32,
78    pub lat_last: i32,
79    pub lon_last: i32,
80    pub orientation_of_grid: i32,
81}
82
83/// Fields shared by all supported projected grid templates.
84///
85/// Longitudes use signed microdegrees consistently. Positive values follow
86/// the WMO east-longitude convention; negative west-longitude inputs are
87/// preserved rather than forced into a 0..360 representation.
88#[derive(Debug, Clone, PartialEq)]
89pub struct ProjectedGridCore {
90    pub number_of_points: u32,
91    pub shape_of_earth: u8,
92    pub scale_factor_radius: u8,
93    pub scaled_value_radius: u32,
94    pub scale_factor_major_axis: u8,
95    pub scaled_value_major_axis: u32,
96    pub scale_factor_minor_axis: u8,
97    pub scaled_value_minor_axis: u32,
98    pub nx: u32,
99    pub ny: u32,
100    pub lat_first: i32,
101    pub lon_first: i32,
102    pub resolution_and_component_flags: u8,
103    pub dx: u32,
104    pub dy: u32,
105    pub scanning_mode: u8,
106}
107
108/// Template 3.30: Lambert conformal grid.
109#[derive(Debug, Clone, PartialEq)]
110pub struct LambertConformalGrid {
111    pub core: ProjectedGridCore,
112    pub lat_d: i32,
113    pub lon_v: i32,
114    pub projection_center_flag: u8,
115    pub latin1: i32,
116    pub latin2: i32,
117    pub lat_southern_pole: i32,
118    pub lon_southern_pole: i32,
119}
120
121/// Template 3.31: Albers equal-area grid.
122#[derive(Debug, Clone, PartialEq)]
123pub struct AlbersEqualAreaGrid {
124    pub core: ProjectedGridCore,
125    pub lat_d: i32,
126    pub lon_v: i32,
127    pub projection_center_flag: u8,
128    pub latin1: i32,
129    pub latin2: i32,
130    pub lat_southern_pole: i32,
131    pub lon_southern_pole: i32,
132}
133
134/// Template 3.20: Polar stereographic projection.
135#[derive(Debug, Clone, PartialEq)]
136pub struct PolarStereographicGrid {
137    pub core: ProjectedGridCore,
138    pub lat_d: i32,
139    pub lon_v: i32,
140    pub projection_center_flag: u8,
141}
142
143impl GridDefinition {
144    /// GRIB2 grid definition template number for typed templates.
145    ///
146    /// `Unsupported` stores the unhandled template number from the source
147    /// message for diagnostics.
148    pub fn template_number(&self) -> u16 {
149        match self {
150            Self::LatLon(_) => 0,
151            Self::RotatedLatLon(_) => 1,
152            Self::Mercator(_) => 10,
153            Self::PolarStereographic(_) => 20,
154            Self::LambertConformal(_) => 30,
155            Self::AlbersEqualArea(_) => 31,
156            Self::RegularGaussian(_) => 40,
157            Self::Unsupported(template) => *template,
158        }
159    }
160
161    pub fn as_lat_lon(&self) -> Option<&LatLonGrid> {
162        match self {
163            Self::LatLon(grid) => Some(grid),
164            _ => None,
165        }
166    }
167
168    pub fn as_rotated_lat_lon(&self) -> Option<&RotatedLatLonGrid> {
169        match self {
170            Self::RotatedLatLon(grid) => Some(grid),
171            _ => None,
172        }
173    }
174
175    pub fn as_regular_gaussian(&self) -> Option<&RegularGaussianGrid> {
176        match self {
177            Self::RegularGaussian(grid) => Some(grid),
178            _ => None,
179        }
180    }
181
182    pub fn as_polar_stereographic(&self) -> Option<&PolarStereographicGrid> {
183        match self {
184            Self::PolarStereographic(grid) => Some(grid),
185            _ => None,
186        }
187    }
188
189    pub fn as_mercator(&self) -> Option<&MercatorGrid> {
190        match self {
191            Self::Mercator(grid) => Some(grid),
192            _ => None,
193        }
194    }
195
196    pub fn as_lambert_conformal(&self) -> Option<&LambertConformalGrid> {
197        match self {
198            Self::LambertConformal(grid) => Some(grid),
199            _ => None,
200        }
201    }
202
203    pub fn as_albers_equal_area(&self) -> Option<&AlbersEqualAreaGrid> {
204        match self {
205            Self::AlbersEqualArea(grid) => Some(grid),
206            _ => None,
207        }
208    }
209
210    pub fn projected_core(&self) -> Option<&ProjectedGridCore> {
211        match self {
212            Self::Mercator(grid) => Some(&grid.core),
213            Self::PolarStereographic(grid) => Some(&grid.core),
214            Self::LambertConformal(grid) => Some(&grid.core),
215            Self::AlbersEqualArea(grid) => Some(&grid.core),
216            Self::LatLon(_)
217            | Self::RotatedLatLon(_)
218            | Self::RegularGaussian(_)
219            | Self::Unsupported(_) => None,
220        }
221    }
222
223    pub fn unsupported_template(&self) -> Option<u16> {
224        match self {
225            Self::Unsupported(template) => Some(*template),
226            _ => None,
227        }
228    }
229
230    pub fn shape(&self) -> (usize, usize) {
231        match self {
232            Self::LatLon(g) => (g.ni as usize, g.nj as usize),
233            Self::RotatedLatLon(g) => (g.grid.ni as usize, g.grid.nj as usize),
234            Self::Mercator(g) => (g.core.nx as usize, g.core.ny as usize),
235            Self::PolarStereographic(g) => (g.core.nx as usize, g.core.ny as usize),
236            Self::LambertConformal(g) => (g.core.nx as usize, g.core.ny as usize),
237            Self::AlbersEqualArea(g) => (g.core.nx as usize, g.core.ny as usize),
238            Self::RegularGaussian(g) => (g.ni as usize, g.nj as usize),
239            Self::Unsupported(_) => (0, 0),
240        }
241    }
242
243    /// Returns the grid's Code Table 3.4 scanning-mode byte.
244    pub fn scanning_mode(&self) -> Option<u8> {
245        match self {
246            Self::LatLon(g) => Some(g.scanning_mode),
247            Self::RotatedLatLon(g) => Some(g.grid.scanning_mode),
248            Self::Mercator(g) => Some(g.core.scanning_mode),
249            Self::PolarStereographic(g) => Some(g.core.scanning_mode),
250            Self::LambertConformal(g) => Some(g.core.scanning_mode),
251            Self::AlbersEqualArea(g) => Some(g.core.scanning_mode),
252            Self::RegularGaussian(g) => Some(g.scanning_mode),
253            Self::Unsupported(_) => None,
254        }
255    }
256
257    /// Returns `(points_per_scan_line, number_of_scan_lines)`.
258    ///
259    /// This is the grouping geometry used by GRIB1 row-by-row second-order
260    /// packing and GRIB2 complex-packing group splitting method 0.
261    pub fn scan_line_shape(&self) -> Result<(usize, usize)> {
262        let (ni, nj) = self.shape();
263        let scanning_mode = self
264            .scanning_mode()
265            .ok_or_else(|| Error::UnsupportedGridTemplate(self.template_number()))?;
266        if i_points_are_consecutive(scanning_mode) {
267            Ok((ni, nj))
268        } else {
269            Ok((nj, ni))
270        }
271    }
272
273    pub fn shape_num_points(&self) -> Result<usize> {
274        match self {
275            Self::LatLon(g) => checked_grid_point_count(g.ni, g.nj),
276            Self::RotatedLatLon(g) => checked_grid_point_count(g.grid.ni, g.grid.nj),
277            Self::Mercator(g) => checked_grid_point_count(g.core.nx, g.core.ny),
278            Self::PolarStereographic(g) => checked_grid_point_count(g.core.nx, g.core.ny),
279            Self::LambertConformal(g) => checked_grid_point_count(g.core.nx, g.core.ny),
280            Self::AlbersEqualArea(g) => checked_grid_point_count(g.core.nx, g.core.ny),
281            Self::RegularGaussian(g) => checked_grid_point_count(g.ni, g.nj),
282            Self::Unsupported(_) => Ok(0),
283        }
284    }
285
286    pub fn ndarray_shape(&self) -> Vec<usize> {
287        let (ni, nj) = self.shape();
288        match self {
289            Self::LatLon(_)
290            | Self::RotatedLatLon(_)
291            | Self::RegularGaussian(_)
292            | Self::Mercator(_)
293            | Self::PolarStereographic(_)
294            | Self::LambertConformal(_)
295            | Self::AlbersEqualArea(_)
296                if ni > 0 && nj > 0 =>
297            {
298                vec![nj, ni]
299            }
300            _ => Vec::new(),
301        }
302    }
303
304    pub fn num_points(&self) -> usize {
305        self.checked_num_points().unwrap_or(usize::MAX)
306    }
307
308    pub fn checked_num_points(&self) -> Result<usize> {
309        match self {
310            Self::LatLon(_) => self.shape_num_points(),
311            Self::RotatedLatLon(_) => self.shape_num_points(),
312            Self::Mercator(g) => Ok(g.core.number_of_points as usize),
313            Self::PolarStereographic(g) => Ok(g.core.number_of_points as usize),
314            Self::LambertConformal(g) => Ok(g.core.number_of_points as usize),
315            Self::AlbersEqualArea(g) => Ok(g.core.number_of_points as usize),
316            Self::RegularGaussian(_) => self.shape_num_points(),
317            Self::Unsupported(_) => Ok(0),
318        }
319    }
320
321    pub fn declared_num_points(&self) -> Option<usize> {
322        match self {
323            Self::Mercator(g) => Some(g.core.number_of_points as usize),
324            Self::PolarStereographic(g) => Some(g.core.number_of_points as usize),
325            Self::LambertConformal(g) => Some(g.core.number_of_points as usize),
326            Self::AlbersEqualArea(g) => Some(g.core.number_of_points as usize),
327            Self::LatLon(_)
328            | Self::RotatedLatLon(_)
329            | Self::RegularGaussian(_)
330            | Self::Unsupported(_) => None,
331        }
332    }
333
334    /// One-dimensional latitude axis for unrotated geographic grids.
335    pub fn latitudes(&self) -> Result<Option<Vec<f64>>> {
336        self.latitudes_with_limit(None)
337    }
338
339    pub fn latitudes_with_limit(&self, max_axis_points: Option<usize>) -> Result<Option<Vec<f64>>> {
340        match self {
341            Self::LatLon(grid) => Ok(Some(grid.latitudes_with_limit(max_axis_points)?)),
342            Self::RegularGaussian(grid) => Ok(Some(grid.latitudes_with_limit(max_axis_points)?)),
343            _ => Ok(None),
344        }
345    }
346
347    /// One-dimensional longitude axis for unrotated geographic grids.
348    pub fn longitudes(&self) -> Result<Option<Vec<f64>>> {
349        self.longitudes_with_limit(None)
350    }
351
352    pub fn longitudes_with_limit(
353        &self,
354        max_axis_points: Option<usize>,
355    ) -> Result<Option<Vec<f64>>> {
356        match self {
357            Self::LatLon(grid) => Ok(Some(grid.longitudes_with_limit(max_axis_points)?)),
358            Self::RegularGaussian(grid) => Ok(Some(grid.longitudes_with_limit(max_axis_points)?)),
359            _ => Ok(None),
360        }
361    }
362
363    /// Projected x coordinates in metres relative to the first grid point.
364    ///
365    /// Regular latitude/longitude grids return `None`; use
366    /// [`LatLonGrid::longitudes`] for those grids.
367    pub fn projected_x_coordinates(&self) -> Result<Option<Vec<f64>>> {
368        self.projected_x_coordinates_with_limit(None)
369    }
370
371    pub fn projected_x_coordinates_with_limit(
372        &self,
373        max_axis_points: Option<usize>,
374    ) -> Result<Option<Vec<f64>>> {
375        match self {
376            Self::Mercator(grid) => Ok(Some(grid.core.x_coordinates_with_limit(max_axis_points)?)),
377            Self::PolarStereographic(grid) => {
378                Ok(Some(grid.core.x_coordinates_with_limit(max_axis_points)?))
379            }
380            Self::LambertConformal(grid) => {
381                Ok(Some(grid.core.x_coordinates_with_limit(max_axis_points)?))
382            }
383            Self::AlbersEqualArea(grid) => {
384                Ok(Some(grid.core.x_coordinates_with_limit(max_axis_points)?))
385            }
386            _ => Ok(None),
387        }
388    }
389
390    /// Projected y coordinates in metres relative to the first grid point.
391    ///
392    /// Regular latitude/longitude grids return `None`; use
393    /// [`LatLonGrid::latitudes`] for those grids.
394    pub fn projected_y_coordinates(&self) -> Result<Option<Vec<f64>>> {
395        self.projected_y_coordinates_with_limit(None)
396    }
397
398    pub fn projected_y_coordinates_with_limit(
399        &self,
400        max_axis_points: Option<usize>,
401    ) -> Result<Option<Vec<f64>>> {
402        match self {
403            Self::Mercator(grid) => Ok(Some(grid.core.y_coordinates_with_limit(max_axis_points)?)),
404            Self::PolarStereographic(grid) => {
405                Ok(Some(grid.core.y_coordinates_with_limit(max_axis_points)?))
406            }
407            Self::LambertConformal(grid) => {
408                Ok(Some(grid.core.y_coordinates_with_limit(max_axis_points)?))
409            }
410            Self::AlbersEqualArea(grid) => {
411                Ok(Some(grid.core.y_coordinates_with_limit(max_axis_points)?))
412            }
413            _ => Ok(None),
414        }
415    }
416
417    pub fn validate_supported_scan_order(&self) -> Result<()> {
418        match self {
419            Self::LatLon(grid) => grid.validate_supported_scan_order(),
420            Self::RotatedLatLon(grid) => grid.grid.validate_supported_scan_order(),
421            Self::Mercator(grid) => grid.core.validate_supported_scan_order(),
422            Self::PolarStereographic(grid) => grid.core.validate_supported_scan_order(),
423            Self::LambertConformal(grid) => grid.core.validate_supported_scan_order(),
424            Self::AlbersEqualArea(grid) => grid.core.validate_supported_scan_order(),
425            Self::RegularGaussian(grid) => grid.validate_supported_scan_order(),
426            Self::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
427        }
428    }
429
430    /// Converts GRIB scan traversal into row-major order by undoing alternating
431    /// row scans. The encoded i/j direction is deliberately preserved.
432    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
433        match self {
434            Self::LatLon(grid) => grid.reorder_for_ndarray_in_place(values),
435            Self::RotatedLatLon(grid) => grid.grid.reorder_for_ndarray_in_place(values),
436            Self::Mercator(grid) => grid.core.reorder_for_ndarray_in_place(values),
437            Self::PolarStereographic(grid) => grid.core.reorder_for_ndarray_in_place(values),
438            Self::LambertConformal(grid) => grid.core.reorder_for_ndarray_in_place(values),
439            Self::AlbersEqualArea(grid) => grid.core.reorder_for_ndarray_in_place(values),
440            Self::RegularGaussian(grid) => transform_supported_scan_order_in_place(
441                values,
442                grid.ni as usize,
443                grid.nj as usize,
444                grid.scanning_mode,
445            ),
446            Self::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
447        }
448    }
449
450    /// Converts GRIB scan traversal to deterministic row-major orientation:
451    /// columns increase in +i order and rows increase in -j order (north to
452    /// south for conventional geographic grids).
453    pub fn normalize_north_up_in_place<T>(&self, values: &mut [T]) -> Result<()> {
454        match self {
455            Self::LatLon(grid) => normalize_north_up_in_place(
456                values,
457                grid.ni as usize,
458                grid.nj as usize,
459                grid.scanning_mode,
460            ),
461            Self::RotatedLatLon(grid) => normalize_north_up_in_place(
462                values,
463                grid.grid.ni as usize,
464                grid.grid.nj as usize,
465                grid.grid.scanning_mode,
466            ),
467            Self::Mercator(grid) => normalize_north_up_in_place(
468                values,
469                grid.core.nx as usize,
470                grid.core.ny as usize,
471                grid.core.scanning_mode,
472            ),
473            Self::PolarStereographic(grid) => normalize_north_up_in_place(
474                values,
475                grid.core.nx as usize,
476                grid.core.ny as usize,
477                grid.core.scanning_mode,
478            ),
479            Self::LambertConformal(grid) => normalize_north_up_in_place(
480                values,
481                grid.core.nx as usize,
482                grid.core.ny as usize,
483                grid.core.scanning_mode,
484            ),
485            Self::AlbersEqualArea(grid) => normalize_north_up_in_place(
486                values,
487                grid.core.nx as usize,
488                grid.core.ny as usize,
489                grid.core.scanning_mode,
490            ),
491            Self::RegularGaussian(grid) => normalize_north_up_in_place(
492                values,
493                grid.ni as usize,
494                grid.nj as usize,
495                grid.scanning_mode,
496            ),
497            Self::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
498        }
499    }
500
501    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
502        if section_bytes.len() < 14 {
503            return Err(Error::InvalidSection {
504                section: 3,
505                reason: format!("expected at least 14 bytes, got {}", section_bytes.len()),
506            });
507        }
508        if section_bytes[4] != 3 {
509            return Err(Error::InvalidSection {
510                section: section_bytes[4],
511                reason: "not a grid definition section".into(),
512            });
513        }
514
515        let template = u16::from_be_bytes(section_bytes[12..14].try_into().unwrap());
516        match template {
517            0 => parse_latlon(section_bytes),
518            1 => parse_rotated_latlon(section_bytes),
519            10 => parse_mercator(section_bytes),
520            20 => parse_polar_stereographic(section_bytes),
521            30 => parse_lambert_conformal(section_bytes),
522            31 => parse_albers_equal_area(section_bytes),
523            40 => parse_regular_gaussian(section_bytes),
524            _ => Ok(Self::Unsupported(template)),
525        }
526    }
527}
528
529impl LatLonGrid {
530    pub fn longitudes(&self) -> Result<Vec<f64>> {
531        self.longitudes_with_limit(None)
532    }
533
534    pub fn longitudes_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
535        let step = self.di as f64 / 1_000_000.0;
536        let signed_step = if self.i_scans_positive() { step } else { -step };
537        let start = self.lon_first as f64 / 1_000_000.0;
538        linear_axis(
539            self.ni,
540            start,
541            signed_step,
542            max_axis_points,
543            "longitude axis",
544        )
545    }
546
547    pub fn latitudes(&self) -> Result<Vec<f64>> {
548        self.latitudes_with_limit(None)
549    }
550
551    pub fn latitudes_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
552        let step = self.dj as f64 / 1_000_000.0;
553        let signed_step = if self.j_scans_positive() { step } else { -step };
554        let start = self.lat_first as f64 / 1_000_000.0;
555        linear_axis(
556            self.nj,
557            start,
558            signed_step,
559            max_axis_points,
560            "latitude axis",
561        )
562    }
563
564    pub fn reorder_for_ndarray<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
565        self.reorder_grib_scan_to_ndarray_in_place(&mut values)?;
566        Ok(values)
567    }
568
569    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
570        self.reorder_grib_scan_to_ndarray_in_place(values)
571    }
572
573    pub fn reorder_grib_scan_to_ndarray<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
574        self.reorder_grib_scan_to_ndarray_in_place(&mut values)?;
575        Ok(values)
576    }
577
578    pub fn reorder_grib_scan_to_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
579        transform_supported_scan_order_in_place(
580            values,
581            self.ni as usize,
582            self.nj as usize,
583            self.scanning_mode,
584        )
585    }
586
587    pub fn reorder_ndarray_to_grib_scan<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
588        self.reorder_ndarray_to_grib_scan_in_place(&mut values)?;
589        Ok(values)
590    }
591
592    pub fn reorder_ndarray_to_grib_scan_in_place<T>(&self, values: &mut [T]) -> Result<()> {
593        transform_supported_scan_order_in_place(
594            values,
595            self.ni as usize,
596            self.nj as usize,
597            self.scanning_mode,
598        )
599    }
600
601    pub fn validate_supported_scan_order(&self) -> Result<()> {
602        validate_supported_scan_order(self.scanning_mode)
603    }
604
605    fn i_scans_positive(&self) -> bool {
606        i_scans_positive(self.scanning_mode)
607    }
608
609    fn j_scans_positive(&self) -> bool {
610        j_scans_positive(self.scanning_mode)
611    }
612}
613
614impl RotatedLatLonGrid {
615    pub fn rotated_longitudes(&self) -> Result<Vec<f64>> {
616        self.grid.longitudes()
617    }
618
619    pub fn rotated_longitudes_with_limit(
620        &self,
621        max_axis_points: Option<usize>,
622    ) -> Result<Vec<f64>> {
623        self.grid.longitudes_with_limit(max_axis_points)
624    }
625
626    pub fn rotated_latitudes(&self) -> Result<Vec<f64>> {
627        self.grid.latitudes()
628    }
629
630    pub fn rotated_latitudes_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
631        self.grid.latitudes_with_limit(max_axis_points)
632    }
633}
634
635impl RegularGaussianGrid {
636    pub fn longitudes(&self) -> Result<Vec<f64>> {
637        self.longitudes_with_limit(None)
638    }
639
640    pub fn longitudes_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
641        let step = self.longitude_increment_degrees()?;
642        let signed_step = if i_scans_positive(self.scanning_mode) {
643            step
644        } else {
645            -step
646        };
647        linear_axis(
648            self.ni,
649            f64::from(self.lon_first) / 1_000_000.0,
650            signed_step,
651            max_axis_points,
652            "Gaussian longitude axis",
653        )
654    }
655
656    pub fn latitudes(&self) -> Result<Vec<f64>> {
657        self.latitudes_with_limit(None)
658    }
659
660    pub fn latitudes_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
661        let requested = self.nj as usize;
662        ensure_limit("Gaussian latitude axis", requested, max_axis_points)?;
663        let all_latitudes = gaussian_latitudes(self.number_of_parallels, max_axis_points)?;
664        if requested > all_latitudes.len() {
665            return Err(Error::InvalidSection {
666                section: 3,
667                reason: format!(
668                    "Gaussian grid requests {} rows but N={} defines only {} parallels",
669                    self.nj,
670                    self.number_of_parallels,
671                    all_latitudes.len()
672                ),
673            });
674        }
675        if requested == 0 {
676            return Ok(Vec::new());
677        }
678
679        let first = f64::from(self.lat_first) / 1_000_000.0;
680        let first_index = all_latitudes
681            .iter()
682            .enumerate()
683            .min_by(|(_, left), (_, right)| {
684                (*left - first).abs().total_cmp(&(*right - first).abs())
685            })
686            .map(|(index, _)| index)
687            .ok_or_else(|| Error::InvalidSection {
688                section: 3,
689                reason: "Gaussian grid has no latitude parallels".into(),
690            })?;
691        if (all_latitudes[first_index] - first).abs() > 0.001 {
692            return Err(Error::InvalidSection {
693                section: 3,
694                reason: format!(
695                    "first latitude {first} is not a Gaussian parallel for N={}",
696                    self.number_of_parallels
697                ),
698            });
699        }
700
701        let ascending = j_scans_positive(self.scanning_mode);
702        let last_index = if ascending {
703            first_index.checked_sub(requested - 1)
704        } else {
705            first_index.checked_add(requested - 1)
706        }
707        .filter(|index| *index < all_latitudes.len())
708        .ok_or_else(|| Error::InvalidSection {
709            section: 3,
710            reason: "Gaussian latitude scan extends beyond the defined parallels".into(),
711        })?;
712
713        let mut result = Vec::new();
714        result.try_reserve_exact(requested).map_err(|error| {
715            Error::allocation("Gaussian latitude coordinates", requested, error)
716        })?;
717        if ascending {
718            result.extend(
719                (last_index..=first_index)
720                    .rev()
721                    .map(|index| all_latitudes[index]),
722            );
723        } else {
724            result.extend(all_latitudes[first_index..=last_index].iter().copied());
725        }
726
727        let declared_last = f64::from(self.lat_last) / 1_000_000.0;
728        if (result[requested - 1] - declared_last).abs() > 0.001 {
729            return Err(Error::InvalidSection {
730                section: 3,
731                reason: format!(
732                    "last latitude {declared_last} does not match the Gaussian scan endpoint {}",
733                    result[requested - 1]
734                ),
735            });
736        }
737        Ok(result)
738    }
739
740    pub fn validate_supported_scan_order(&self) -> Result<()> {
741        validate_supported_scan_order(self.scanning_mode)
742    }
743
744    fn longitude_increment_degrees(&self) -> Result<f64> {
745        if self.di != u32::MAX {
746            let increment = f64::from(self.di) / 1_000_000.0;
747            self.validate_longitude_endpoint(increment)?;
748            return Ok(increment);
749        }
750        if self.ni == 0 {
751            return Err(Error::InvalidSection {
752                section: 3,
753                reason: "Gaussian grid has missing Di and zero Ni".into(),
754            });
755        }
756        let inferred = 360.0 / f64::from(self.ni);
757        self.validate_longitude_endpoint(inferred)?;
758        Ok(inferred)
759    }
760
761    fn validate_longitude_endpoint(&self, increment: f64) -> Result<()> {
762        if self.ni == 0 {
763            return Err(Error::InvalidSection {
764                section: 3,
765                reason: "Gaussian grid has zero Ni".into(),
766            });
767        }
768        let expected_last = f64::from(self.lon_first) / 1_000_000.0
769            + if i_scans_positive(self.scanning_mode) {
770                increment * f64::from(self.ni - 1)
771            } else {
772                -increment * f64::from(self.ni - 1)
773            };
774        let declared_last = f64::from(self.lon_last) / 1_000_000.0;
775        let wrapped_difference = (expected_last - declared_last + 180.0).rem_euclid(360.0) - 180.0;
776        if wrapped_difference.abs() > 0.001 {
777            return Err(Error::InvalidSection {
778                section: 3,
779                reason: "Gaussian longitude endpoint does not match Ni, Di, and scanning mode"
780                    .into(),
781            });
782        }
783        Ok(())
784    }
785}
786
787fn gaussian_latitudes(
788    number_of_parallels: u32,
789    max_axis_points: Option<usize>,
790) -> Result<Vec<f64>> {
791    if number_of_parallels == 0 {
792        return Err(Error::InvalidSection {
793            section: 3,
794            reason: "Gaussian grid N must be nonzero".into(),
795        });
796    }
797    let order = usize::try_from(u64::from(number_of_parallels) * 2)
798        .map_err(|_| Error::ValueOutOfRange("Gaussian parallel count does not fit usize".into()))?;
799    ensure_limit("Gaussian parallel calculation", order, max_axis_points)?;
800    let mut latitudes = filled_vec(order, 0.0, "Gaussian latitude coordinates")?;
801    let order_f64 = order as f64;
802
803    for index in 0..order / 2 {
804        let mut root = (std::f64::consts::PI * (index as f64 + 0.75) / (order_f64 + 0.5)).cos();
805        let mut converged = false;
806        for _ in 0..32 {
807            let (polynomial, previous) = legendre_pair(order, root);
808            let derivative = order_f64 * (root * polynomial - previous) / (root * root - 1.0);
809            let next = root - polynomial / derivative;
810            if (next - root).abs() <= 4.0 * f64::EPSILON * next.abs().max(1.0) {
811                root = next;
812                converged = true;
813                break;
814            }
815            root = next;
816        }
817        if !converged || !root.is_finite() {
818            return Err(Error::InvalidSection {
819                section: 3,
820                reason: format!(
821                    "failed to compute Gaussian latitude root {index} for N={number_of_parallels}"
822                ),
823            });
824        }
825        let latitude = root.asin().to_degrees();
826        latitudes[index] = latitude;
827        latitudes[order - index - 1] = -latitude;
828    }
829    Ok(latitudes)
830}
831
832fn legendre_pair(order: usize, value: f64) -> (f64, f64) {
833    let mut previous = 0.0;
834    let mut current = 1.0;
835    for degree in 1..=order {
836        let older = previous;
837        previous = current;
838        current = ((2 * degree - 1) as f64 * value * previous - (degree - 1) as f64 * older)
839            / degree as f64;
840    }
841    (current, previous)
842}
843
844impl ProjectedGridCore {
845    pub fn x_coordinates(&self) -> Result<Vec<f64>> {
846        self.x_coordinates_with_limit(None)
847    }
848
849    pub fn x_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
850        projected_x_coordinates(self.nx, self.dx, self.scanning_mode, max_axis_points)
851    }
852
853    pub fn y_coordinates(&self) -> Result<Vec<f64>> {
854        self.y_coordinates_with_limit(None)
855    }
856
857    pub fn y_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
858        projected_y_coordinates(self.ny, self.dy, self.scanning_mode, max_axis_points)
859    }
860
861    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
862        transform_supported_scan_order_in_place(
863            values,
864            self.nx as usize,
865            self.ny as usize,
866            self.scanning_mode,
867        )
868    }
869
870    pub fn validate_supported_scan_order(&self) -> Result<()> {
871        validate_supported_scan_order(self.scanning_mode)
872    }
873}
874
875fn transform_supported_scan_order_in_place<T>(
876    values: &mut [T],
877    ni: usize,
878    nj: usize,
879    scanning_mode: u8,
880) -> Result<()> {
881    validate_supported_scan_order(scanning_mode)?;
882    let expected = ni.checked_mul(nj).ok_or(Error::ArithmeticOverflow {
883        operation: "computing grid point count",
884    })?;
885    if values.len() != expected {
886        return Err(Error::DataLengthMismatch {
887            expected,
888            actual: values.len(),
889        });
890    }
891
892    if adjacent_rows_alternate_direction(scanning_mode) {
893        reverse_alternating_rows(values, ni, nj);
894    }
895
896    Ok(())
897}
898
899fn normalize_north_up_in_place<T>(
900    values: &mut [T],
901    ni: usize,
902    nj: usize,
903    scanning_mode: u8,
904) -> Result<()> {
905    transform_supported_scan_order_in_place(values, ni, nj, scanning_mode)?;
906    if ni == 0 {
907        return Ok(());
908    }
909
910    if !i_scans_positive(scanning_mode) {
911        for row in values.chunks_exact_mut(ni) {
912            row.reverse();
913        }
914    }
915    if j_scans_positive(scanning_mode) {
916        // Reversing the whole slice reverses both row order and each row;
917        // reverse each row again to preserve column orientation.
918        values.reverse();
919        for row in values.chunks_exact_mut(ni) {
920            row.reverse();
921        }
922    }
923
924    Ok(())
925}
926
927fn validate_supported_scan_order(scanning_mode: u8) -> Result<()> {
928    if i_points_are_consecutive(scanning_mode) {
929        Ok(())
930    } else {
931        Err(Error::UnsupportedScanningMode(scanning_mode))
932    }
933}
934
935fn i_scans_positive(scanning_mode: u8) -> bool {
936    scanning_mode & 0b1000_0000 == 0
937}
938
939fn j_scans_positive(scanning_mode: u8) -> bool {
940    scanning_mode & 0b0100_0000 != 0
941}
942
943fn i_points_are_consecutive(scanning_mode: u8) -> bool {
944    scanning_mode & 0b0010_0000 == 0
945}
946
947fn adjacent_rows_alternate_direction(scanning_mode: u8) -> bool {
948    scanning_mode & 0b0001_0000 != 0
949}
950
951fn projected_x_coordinates(
952    count: u32,
953    spacing_millimetres: u32,
954    scanning_mode: u8,
955    max_axis_points: Option<usize>,
956) -> Result<Vec<f64>> {
957    projected_axis(
958        count,
959        spacing_millimetres,
960        i_scans_positive(scanning_mode),
961        max_axis_points,
962        "projected x axis",
963    )
964}
965
966fn projected_y_coordinates(
967    count: u32,
968    spacing_millimetres: u32,
969    scanning_mode: u8,
970    max_axis_points: Option<usize>,
971) -> Result<Vec<f64>> {
972    projected_axis(
973        count,
974        spacing_millimetres,
975        j_scans_positive(scanning_mode),
976        max_axis_points,
977        "projected y axis",
978    )
979}
980
981fn projected_axis(
982    count: u32,
983    spacing_millimetres: u32,
984    scans_positive: bool,
985    max_axis_points: Option<usize>,
986    what: &'static str,
987) -> Result<Vec<f64>> {
988    let step = f64::from(spacing_millimetres) / 1_000.0;
989    let signed_step = if scans_positive { step } else { -step };
990    linear_axis(count, 0.0, signed_step, max_axis_points, what)
991}
992
993fn linear_axis(
994    count: u32,
995    start: f64,
996    signed_step: f64,
997    max_axis_points: Option<usize>,
998    what: &'static str,
999) -> Result<Vec<f64>> {
1000    let count = count as usize;
1001    ensure_limit(what, count, max_axis_points)?;
1002    let mut axis = Vec::new();
1003    axis.try_reserve(count)
1004        .map_err(|error| Error::allocation(format!("{what} coordinates"), count, error))?;
1005    axis.extend((0..count).map(|index| start + signed_step * index as f64));
1006    Ok(axis)
1007}
1008
1009fn checked_grid_point_count(nx: u32, ny: u32) -> Result<usize> {
1010    let count = u64::from(nx) * u64::from(ny);
1011    usize::try_from(count).map_err(|_| {
1012        Error::ValueOutOfRange(format!("grid point count {count} does not fit in usize"))
1013    })
1014}
1015
1016/// Code Table 3.4 flag 4: the first row scans in the flag-1 direction and
1017/// each following row reverses it, so odd rows are flipped regardless of the
1018/// i direction.
1019fn reverse_alternating_rows<T>(values: &mut [T], ni: usize, nj: usize) {
1020    for row in (1..nj).step_by(2) {
1021        values[row * ni..(row + 1) * ni].reverse();
1022    }
1023}
1024
1025fn parse_latlon(data: &[u8]) -> Result<GridDefinition> {
1026    if data.len() < 72 {
1027        return Err(Error::InvalidSection {
1028            section: 3,
1029            reason: format!("template 3.0 requires 72 bytes, got {}", data.len()),
1030        });
1031    }
1032
1033    reject_quasi_regular_grid(data, 0)?;
1034    require_microdegree_angular_unit(data, 0)?;
1035    let grid = parse_latlon_fields(data);
1036    validate_geographic_coordinates(
1037        grid.lat_first,
1038        grid.lon_first,
1039        grid.lat_last,
1040        grid.lon_last,
1041        0,
1042    )?;
1043    Ok(GridDefinition::LatLon(grid))
1044}
1045
1046fn parse_rotated_latlon(data: &[u8]) -> Result<GridDefinition> {
1047    if data.len() < 84 {
1048        return Err(Error::InvalidSection {
1049            section: 3,
1050            reason: format!("template 3.1 requires 84 bytes, got {}", data.len()),
1051        });
1052    }
1053
1054    reject_quasi_regular_grid(data, 1)?;
1055    require_microdegree_angular_unit(data, 1)?;
1056    let grid = parse_latlon_fields(data);
1057    validate_geographic_coordinates(
1058        grid.lat_first,
1059        grid.lon_first,
1060        grid.lat_last,
1061        grid.lon_last,
1062        1,
1063    )?;
1064
1065    let lat_southern_pole = decode_wmo_i32(&data[72..76]).unwrap();
1066    let lon_southern_pole = u32::from_be_bytes(data[76..80].try_into().unwrap());
1067    if lat_southern_pole.unsigned_abs() > 90_000_000 || lon_southern_pole > 360_000_000 {
1068        return Err(Error::InvalidSection {
1069            section: 3,
1070            reason: "rotated-grid southern pole is outside geographic bounds".into(),
1071        });
1072    }
1073    let angle_of_rotation = f32::from_be_bytes(data[80..84].try_into().unwrap());
1074    if !angle_of_rotation.is_finite() {
1075        return Err(Error::InvalidSection {
1076            section: 3,
1077            reason: "rotated-grid angle of rotation must be finite".into(),
1078        });
1079    }
1080
1081    Ok(GridDefinition::RotatedLatLon(RotatedLatLonGrid {
1082        grid,
1083        lat_southern_pole,
1084        lon_southern_pole,
1085        angle_of_rotation,
1086    }))
1087}
1088
1089fn parse_regular_gaussian(data: &[u8]) -> Result<GridDefinition> {
1090    if data.len() < 72 {
1091        return Err(Error::InvalidSection {
1092            section: 3,
1093            reason: format!("template 3.40 requires 72 bytes, got {}", data.len()),
1094        });
1095    }
1096
1097    reject_quasi_regular_grid(data, 40)?;
1098    require_microdegree_angular_unit(data, 40)?;
1099    let grid = RegularGaussianGrid {
1100        ni: u32::from_be_bytes(data[30..34].try_into().unwrap()),
1101        nj: u32::from_be_bytes(data[34..38].try_into().unwrap()),
1102        lat_first: decode_wmo_i32(&data[46..50]).unwrap(),
1103        lon_first: decode_wmo_i32(&data[50..54]).unwrap(),
1104        lat_last: decode_wmo_i32(&data[55..59]).unwrap(),
1105        lon_last: decode_wmo_i32(&data[59..63]).unwrap(),
1106        di: u32::from_be_bytes(data[63..67].try_into().unwrap()),
1107        number_of_parallels: u32::from_be_bytes(data[67..71].try_into().unwrap()),
1108        scanning_mode: data[71],
1109    };
1110    validate_geographic_coordinates(
1111        grid.lat_first,
1112        grid.lon_first,
1113        grid.lat_last,
1114        grid.lon_last,
1115        40,
1116    )?;
1117    if grid.number_of_parallels == 0 {
1118        return Err(Error::InvalidSection {
1119            section: 3,
1120            reason: "template 3.40 number of parallels N must be nonzero".into(),
1121        });
1122    }
1123    let available_rows = u64::from(grid.number_of_parallels) * 2;
1124    if u64::from(grid.nj) > available_rows {
1125        return Err(Error::InvalidSection {
1126            section: 3,
1127            reason: format!(
1128                "template 3.40 Nj={} exceeds the {available_rows} parallels defined by N={}",
1129                grid.nj, grid.number_of_parallels
1130            ),
1131        });
1132    }
1133    Ok(GridDefinition::RegularGaussian(grid))
1134}
1135
1136fn parse_latlon_fields(data: &[u8]) -> LatLonGrid {
1137    LatLonGrid {
1138        ni: u32::from_be_bytes(data[30..34].try_into().unwrap()),
1139        nj: u32::from_be_bytes(data[34..38].try_into().unwrap()),
1140        lat_first: decode_wmo_i32(&data[46..50]).unwrap(),
1141        lon_first: decode_wmo_i32(&data[50..54]).unwrap(),
1142        lat_last: decode_wmo_i32(&data[55..59]).unwrap(),
1143        lon_last: decode_wmo_i32(&data[59..63]).unwrap(),
1144        di: u32::from_be_bytes(data[63..67].try_into().unwrap()),
1145        dj: u32::from_be_bytes(data[67..71].try_into().unwrap()),
1146        scanning_mode: data[71],
1147    }
1148}
1149
1150fn reject_quasi_regular_grid(data: &[u8], template: u16) -> Result<()> {
1151    let ni = u32::from_be_bytes(data[30..34].try_into().unwrap());
1152    let nj = u32::from_be_bytes(data[34..38].try_into().unwrap());
1153    if ni == u32::MAX || nj == u32::MAX || data[10] != 0 {
1154        return Err(Error::UnsupportedQuasiRegularGrid { template });
1155    }
1156    Ok(())
1157}
1158
1159fn require_microdegree_angular_unit(data: &[u8], template: u16) -> Result<()> {
1160    let encoded_basic_angle = u32::from_be_bytes(data[38..42].try_into().unwrap());
1161    let encoded_subdivisions = u32::from_be_bytes(data[42..46].try_into().unwrap());
1162    let basic_angle = match encoded_basic_angle {
1163        0 | u32::MAX => 1,
1164        value => value,
1165    };
1166    let subdivisions = match encoded_subdivisions {
1167        0 | u32::MAX => 1_000_000,
1168        value => value,
1169    };
1170    if u64::from(basic_angle) * 1_000_000 != u64::from(subdivisions) {
1171        return Err(Error::UnsupportedGridAngularUnit {
1172            template,
1173            basic_angle,
1174            subdivisions,
1175        });
1176    }
1177    Ok(())
1178}
1179
1180fn validate_geographic_coordinates(
1181    lat_first: i32,
1182    lon_first: i32,
1183    lat_last: i32,
1184    lon_last: i32,
1185    template: u16,
1186) -> Result<()> {
1187    if lat_first.unsigned_abs() > 90_000_000
1188        || lat_last.unsigned_abs() > 90_000_000
1189        || lon_first.unsigned_abs() > 360_000_000
1190        || lon_last.unsigned_abs() > 360_000_000
1191    {
1192        return Err(Error::InvalidSection {
1193            section: 3,
1194            reason: format!("template 3.{template} coordinates are outside geographic bounds"),
1195        });
1196    }
1197    Ok(())
1198}
1199
1200fn parse_mercator(data: &[u8]) -> Result<GridDefinition> {
1201    if data.len() < 72 {
1202        return Err(Error::InvalidSection {
1203            section: 3,
1204            reason: format!("template 3.10 requires 72 bytes, got {}", data.len()),
1205        });
1206    }
1207
1208    Ok(GridDefinition::Mercator(MercatorGrid {
1209        core: parse_projected_core(data, 64, 59),
1210        lat_d: decode_wmo_i32(&data[47..51]).unwrap(),
1211        lat_last: decode_wmo_i32(&data[51..55]).unwrap(),
1212        lon_last: decode_wmo_i32(&data[55..59]).unwrap(),
1213        orientation_of_grid: decode_wmo_i32(&data[60..64]).unwrap(),
1214    }))
1215}
1216
1217fn parse_polar_stereographic(data: &[u8]) -> Result<GridDefinition> {
1218    if data.len() < 65 {
1219        return Err(Error::InvalidSection {
1220            section: 3,
1221            reason: format!("template 3.20 requires 65 bytes, got {}", data.len()),
1222        });
1223    }
1224
1225    Ok(GridDefinition::PolarStereographic(PolarStereographicGrid {
1226        core: parse_projected_core(data, 55, 64),
1227        lat_d: decode_wmo_i32(&data[47..51]).unwrap(),
1228        lon_v: decode_wmo_i32(&data[51..55]).unwrap(),
1229        projection_center_flag: data[63],
1230    }))
1231}
1232
1233fn parse_albers_equal_area(data: &[u8]) -> Result<GridDefinition> {
1234    if data.len() < 81 {
1235        return Err(Error::InvalidSection {
1236            section: 3,
1237            reason: format!("template 3.31 requires 81 bytes, got {}", data.len()),
1238        });
1239    }
1240
1241    Ok(GridDefinition::AlbersEqualArea(AlbersEqualAreaGrid {
1242        core: parse_projected_core(data, 55, 64),
1243        lat_d: decode_wmo_i32(&data[47..51]).unwrap(),
1244        lon_v: decode_wmo_i32(&data[51..55]).unwrap(),
1245        projection_center_flag: data[63],
1246        latin1: decode_wmo_i32(&data[65..69]).unwrap(),
1247        latin2: decode_wmo_i32(&data[69..73]).unwrap(),
1248        lat_southern_pole: decode_wmo_i32(&data[73..77]).unwrap(),
1249        lon_southern_pole: decode_wmo_i32(&data[77..81]).unwrap(),
1250    }))
1251}
1252
1253fn parse_lambert_conformal(data: &[u8]) -> Result<GridDefinition> {
1254    if data.len() < 81 {
1255        return Err(Error::InvalidSection {
1256            section: 3,
1257            reason: format!("template 3.30 requires 81 bytes, got {}", data.len()),
1258        });
1259    }
1260
1261    Ok(GridDefinition::LambertConformal(LambertConformalGrid {
1262        core: parse_projected_core(data, 55, 64),
1263        lat_d: decode_wmo_i32(&data[47..51]).unwrap(),
1264        lon_v: decode_wmo_i32(&data[51..55]).unwrap(),
1265        projection_center_flag: data[63],
1266        latin1: decode_wmo_i32(&data[65..69]).unwrap(),
1267        latin2: decode_wmo_i32(&data[69..73]).unwrap(),
1268        lat_southern_pole: decode_wmo_i32(&data[73..77]).unwrap(),
1269        lon_southern_pole: decode_wmo_i32(&data[77..81]).unwrap(),
1270    }))
1271}
1272
1273fn parse_projected_core(
1274    data: &[u8],
1275    spacing_offset: usize,
1276    scanning_mode_offset: usize,
1277) -> ProjectedGridCore {
1278    ProjectedGridCore {
1279        number_of_points: u32::from_be_bytes(data[6..10].try_into().unwrap()),
1280        shape_of_earth: data[14],
1281        scale_factor_radius: data[15],
1282        scaled_value_radius: u32::from_be_bytes(data[16..20].try_into().unwrap()),
1283        scale_factor_major_axis: data[20],
1284        scaled_value_major_axis: u32::from_be_bytes(data[21..25].try_into().unwrap()),
1285        scale_factor_minor_axis: data[25],
1286        scaled_value_minor_axis: u32::from_be_bytes(data[26..30].try_into().unwrap()),
1287        nx: u32::from_be_bytes(data[30..34].try_into().unwrap()),
1288        ny: u32::from_be_bytes(data[34..38].try_into().unwrap()),
1289        lat_first: decode_wmo_i32(&data[38..42]).unwrap(),
1290        lon_first: decode_wmo_i32(&data[42..46]).unwrap(),
1291        resolution_and_component_flags: data[46],
1292        dx: u32::from_be_bytes(data[spacing_offset..spacing_offset + 4].try_into().unwrap()),
1293        dy: u32::from_be_bytes(
1294            data[spacing_offset + 4..spacing_offset + 8]
1295                .try_into()
1296                .unwrap(),
1297        ),
1298        scanning_mode: data[scanning_mode_offset],
1299    }
1300}
1301
1302#[cfg(test)]
1303mod tests {
1304    use super::{
1305        AlbersEqualAreaGrid, GridDefinition, LambertConformalGrid, LatLonGrid, MercatorGrid,
1306        PolarStereographicGrid, ProjectedGridCore, RegularGaussianGrid,
1307    };
1308    use crate::binary::encode_wmo_i32;
1309
1310    fn projected_core(nx: u32, ny: u32, dx: u32, dy: u32, scanning_mode: u8) -> ProjectedGridCore {
1311        ProjectedGridCore {
1312            number_of_points: nx * ny,
1313            shape_of_earth: 6,
1314            scale_factor_radius: 0,
1315            scaled_value_radius: 0,
1316            scale_factor_major_axis: 0,
1317            scaled_value_major_axis: 0,
1318            scale_factor_minor_axis: 0,
1319            scaled_value_minor_axis: 0,
1320            nx,
1321            ny,
1322            lat_first: 0,
1323            lon_first: 0,
1324            resolution_and_component_flags: 0,
1325            dx,
1326            dy,
1327            scanning_mode,
1328        }
1329    }
1330
1331    #[test]
1332    fn reports_latlon_shape() {
1333        let grid = GridDefinition::LatLon(LatLonGrid {
1334            ni: 3,
1335            nj: 2,
1336            lat_first: 50_000_000,
1337            lon_first: -120_000_000,
1338            lat_last: 49_000_000,
1339            lon_last: -118_000_000,
1340            di: 1_000_000,
1341            dj: 1_000_000,
1342            scanning_mode: 0,
1343        });
1344
1345        assert_eq!(grid.shape(), (3, 2));
1346        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
1347        assert_eq!(grid.template_number(), 0);
1348        assert!(grid.as_lat_lon().is_some());
1349        assert!(grid.as_mercator().is_none());
1350        assert!(grid.as_polar_stereographic().is_none());
1351        assert!(grid.as_lambert_conformal().is_none());
1352        assert!(grid.as_albers_equal_area().is_none());
1353        assert_eq!(grid.unsupported_template(), None);
1354        match grid {
1355            GridDefinition::LatLon(ref latlon) => {
1356                assert_eq!(latlon.longitudes().unwrap(), vec![-120.0, -119.0, -118.0]);
1357                assert_eq!(latlon.latitudes().unwrap(), vec![50.0, 49.0]);
1358            }
1359            other => panic!("expected lat/lon grid, got {other:?}"),
1360        }
1361    }
1362
1363    #[test]
1364    fn parses_mercator_template() {
1365        let section = build_mercator_section(0);
1366        let grid = GridDefinition::parse(&section).unwrap();
1367
1368        assert_eq!(grid.shape(), (3, 2));
1369        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
1370        assert_eq!(grid.num_points(), 6);
1371        assert_eq!(grid.template_number(), 10);
1372        assert!(grid.as_lat_lon().is_none());
1373        assert!(grid.as_mercator().is_some());
1374        assert!(grid.as_lambert_conformal().is_none());
1375        assert_eq!(grid.unsupported_template(), None);
1376        match grid {
1377            GridDefinition::Mercator(mercator) => {
1378                assert_eq!(mercator.core.number_of_points, 6);
1379                assert_eq!(mercator.core.shape_of_earth, 6);
1380                assert_eq!(mercator.core.nx, 3);
1381                assert_eq!(mercator.core.ny, 2);
1382                assert_eq!(mercator.core.lat_first, -20_000_000);
1383                assert_eq!(mercator.core.lon_first, -100_000_000);
1384                assert_eq!(mercator.core.resolution_and_component_flags, 0x08);
1385                assert_eq!(mercator.lat_d, 0);
1386                assert_eq!(mercator.lat_last, 20_000_000);
1387                assert_eq!(mercator.lon_last, -98_000_000);
1388                assert_eq!(mercator.core.scanning_mode, 0);
1389                assert_eq!(mercator.orientation_of_grid, 0);
1390                assert_eq!(mercator.core.dx, 1_000_000);
1391                assert_eq!(mercator.core.dy, 2_000_000);
1392            }
1393            other => panic!("expected Mercator grid, got {other:?}"),
1394        }
1395    }
1396
1397    #[test]
1398    fn parses_rotated_latlon_template() {
1399        let section = build_rotated_latlon_section();
1400        let grid = GridDefinition::parse(&section).unwrap();
1401
1402        assert_eq!(grid.shape(), (3, 2));
1403        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
1404        assert_eq!(grid.template_number(), 1);
1405        assert!(grid.as_lat_lon().is_none());
1406        let rotated = grid.as_rotated_lat_lon().unwrap();
1407        assert_eq!(rotated.lat_southern_pole, -30_000_000);
1408        assert_eq!(rotated.lon_southern_pole, 10_000_000);
1409        assert_eq!(rotated.angle_of_rotation, 15.5);
1410        assert_eq!(
1411            rotated.rotated_longitudes().unwrap(),
1412            vec![-20.0, 0.0, 20.0]
1413        );
1414        assert_eq!(rotated.rotated_latitudes().unwrap(), vec![-10.0, 0.0]);
1415        assert_eq!(grid.latitudes().unwrap(), None);
1416    }
1417
1418    #[test]
1419    fn rejects_non_finite_rotated_grid_angle() {
1420        let mut section = build_rotated_latlon_section();
1421        section[80..84].copy_from_slice(&f32::NAN.to_be_bytes());
1422        assert!(matches!(
1423            GridDefinition::parse(&section),
1424            Err(crate::Error::InvalidSection { section: 3, .. })
1425        ));
1426    }
1427
1428    #[test]
1429    fn parses_regular_gaussian_and_computes_latitudes() {
1430        let section = build_regular_gaussian_section(90_000_000);
1431        let grid = GridDefinition::parse(&section).unwrap();
1432
1433        assert_eq!(grid.shape(), (4, 4));
1434        assert_eq!(grid.ndarray_shape(), vec![4, 4]);
1435        assert_eq!(grid.template_number(), 40);
1436        let gaussian = grid.as_regular_gaussian().unwrap();
1437        assert_eq!(
1438            gaussian,
1439            &RegularGaussianGrid {
1440                ni: 4,
1441                nj: 4,
1442                lat_first: 59_444_408,
1443                lon_first: 0,
1444                lat_last: -59_444_408,
1445                lon_last: 270_000_000,
1446                di: 90_000_000,
1447                number_of_parallels: 2,
1448                scanning_mode: 0,
1449            }
1450        );
1451        let latitudes = gaussian.latitudes().unwrap();
1452        let expected = [
1453            59.444_408_289_166_77,
1454            19.875_719_147_440_904,
1455            -19.875_719_147_440_904,
1456            -59.444_408_289_166_77,
1457        ];
1458        for (actual, expected) in latitudes.iter().zip(expected) {
1459            assert!((actual - expected).abs() < 1e-12);
1460        }
1461        assert_eq!(
1462            gaussian.longitudes().unwrap(),
1463            vec![0.0, 90.0, 180.0, 270.0]
1464        );
1465        assert_eq!(grid.latitudes().unwrap().unwrap(), latitudes);
1466    }
1467
1468    #[test]
1469    fn infers_missing_gaussian_increment_for_global_grid() {
1470        let section = build_regular_gaussian_section(u32::MAX);
1471        let GridDefinition::RegularGaussian(grid) = GridDefinition::parse(&section).unwrap() else {
1472            panic!("expected regular Gaussian grid");
1473        };
1474        assert_eq!(grid.longitudes().unwrap(), vec![0.0, 90.0, 180.0, 270.0]);
1475    }
1476
1477    #[test]
1478    fn rejects_inconsistent_gaussian_longitude_endpoint() {
1479        let mut section = build_regular_gaussian_section(90_000_000);
1480        section[59..63].copy_from_slice(&encode_wmo_i32(180_000_000).unwrap());
1481        let GridDefinition::RegularGaussian(grid) = GridDefinition::parse(&section).unwrap() else {
1482            panic!("expected regular Gaussian grid");
1483        };
1484        assert!(matches!(
1485            grid.longitudes(),
1486            Err(crate::Error::InvalidSection { section: 3, .. })
1487        ));
1488    }
1489
1490    #[test]
1491    fn rejects_quasi_regular_geographic_grids_with_typed_error() {
1492        for (template, mut section) in [
1493            (1, build_rotated_latlon_section()),
1494            (40, build_regular_gaussian_section(90_000_000)),
1495        ] {
1496            section[30..34].copy_from_slice(&u32::MAX.to_be_bytes());
1497            section[10] = 2;
1498            assert!(matches!(
1499                GridDefinition::parse(&section),
1500                Err(crate::Error::UnsupportedQuasiRegularGrid { template: actual })
1501                    if actual == template
1502            ));
1503        }
1504    }
1505
1506    #[test]
1507    fn rejects_non_microdegree_geographic_angle_units_explicitly() {
1508        let mut section = build_regular_gaussian_section(90_000_000);
1509        section[38..42].copy_from_slice(&1u32.to_be_bytes());
1510        section[42..46].copy_from_slice(&1_000u32.to_be_bytes());
1511        assert!(matches!(
1512            GridDefinition::parse(&section),
1513            Err(crate::Error::UnsupportedGridAngularUnit {
1514                template: 40,
1515                basic_angle: 1,
1516                subdivisions: 1_000,
1517            })
1518        ));
1519    }
1520
1521    #[test]
1522    fn parses_polar_stereographic_template() {
1523        let section = build_polar_stereographic_section(0);
1524        let grid = GridDefinition::parse(&section).unwrap();
1525
1526        assert_eq!(grid.shape(), (3, 2));
1527        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
1528        assert_eq!(grid.num_points(), 6);
1529        assert_eq!(grid.template_number(), 20);
1530        assert!(grid.as_lat_lon().is_none());
1531        assert!(grid.as_mercator().is_none());
1532        assert!(grid.as_polar_stereographic().is_some());
1533        assert!(grid.as_lambert_conformal().is_none());
1534        assert!(grid.as_albers_equal_area().is_none());
1535        assert_eq!(grid.unsupported_template(), None);
1536        match grid {
1537            GridDefinition::PolarStereographic(polar) => {
1538                assert_eq!(polar.core.number_of_points, 6);
1539                assert_eq!(polar.core.shape_of_earth, 6);
1540                assert_eq!(polar.core.nx, 3);
1541                assert_eq!(polar.core.ny, 2);
1542                assert_eq!(polar.core.lat_first, 41_612_949);
1543                assert_eq!(polar.core.lon_first, 185_117_126);
1544                assert_eq!(polar.core.resolution_and_component_flags, 0x08);
1545                assert_eq!(polar.lat_d, 60_000_000);
1546                assert_eq!(polar.lon_v, 225_000_000);
1547                assert_eq!(polar.core.dx, 3_000_000);
1548                assert_eq!(polar.core.dy, 3_000_000);
1549                assert_eq!(polar.projection_center_flag, 0);
1550                assert_eq!(polar.core.scanning_mode, 0);
1551            }
1552            other => panic!("expected polar stereographic grid, got {other:?}"),
1553        }
1554    }
1555
1556    #[test]
1557    fn parses_albers_equal_area_template() {
1558        let section = build_albers_section();
1559        let grid = GridDefinition::parse(&section).unwrap();
1560
1561        assert_eq!(grid.shape(), (3, 2));
1562        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
1563        assert_eq!(grid.num_points(), 6);
1564        assert_eq!(grid.template_number(), 31);
1565        assert!(grid.as_lat_lon().is_none());
1566        assert!(grid.as_albers_equal_area().is_some());
1567        assert_eq!(grid.unsupported_template(), None);
1568        match grid {
1569            GridDefinition::AlbersEqualArea(albers) => {
1570                assert_eq!(albers.core.number_of_points, 6);
1571                assert_eq!(albers.core.shape_of_earth, 6);
1572                assert_eq!(albers.core.nx, 3);
1573                assert_eq!(albers.core.ny, 2);
1574                assert_eq!(albers.core.lat_first, 23_000_000);
1575                assert_eq!(albers.core.lon_first, 240_000_000);
1576                assert_eq!(albers.core.resolution_and_component_flags, 0x08);
1577                assert_eq!(albers.lat_d, 25_000_000);
1578                assert_eq!(albers.lon_v, 265_000_000);
1579                assert_eq!(albers.core.dx, 4_000_000);
1580                assert_eq!(albers.core.dy, 5_000_000);
1581                assert_eq!(albers.projection_center_flag, 0);
1582                assert_eq!(albers.core.scanning_mode, 0);
1583                assert_eq!(albers.latin1, 29_500_000);
1584                assert_eq!(albers.latin2, 45_500_000);
1585                assert_eq!(albers.lat_southern_pole, -90_000_000);
1586                assert_eq!(albers.lon_southern_pole, 0);
1587            }
1588            other => panic!("expected Albers equal-area grid, got {other:?}"),
1589        }
1590    }
1591
1592    #[test]
1593    fn parses_lambert_conformal_template() {
1594        let section = build_lambert_section();
1595        let grid = GridDefinition::parse(&section).unwrap();
1596
1597        assert_eq!(grid.shape(), (3, 2));
1598        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
1599        assert_eq!(grid.num_points(), 6);
1600        assert_eq!(grid.template_number(), 30);
1601        assert!(grid.as_lat_lon().is_none());
1602        assert!(grid.as_lambert_conformal().is_some());
1603        assert!(grid.as_albers_equal_area().is_none());
1604        assert_eq!(grid.unsupported_template(), None);
1605        match grid {
1606            GridDefinition::LambertConformal(lambert) => {
1607                assert_eq!(lambert.core.number_of_points, 6);
1608                assert_eq!(lambert.core.shape_of_earth, 1);
1609                assert_eq!(lambert.core.scaled_value_radius, 6_371_200);
1610                assert_eq!(lambert.core.nx, 3);
1611                assert_eq!(lambert.core.ny, 2);
1612                assert_eq!(lambert.core.lat_first, 12_190_000);
1613                assert_eq!(lambert.core.lon_first, 226_541_000);
1614                assert_eq!(lambert.core.resolution_and_component_flags, 0x08);
1615                assert_eq!(lambert.lat_d, 25_000_000);
1616                assert_eq!(lambert.lon_v, 265_000_000);
1617                assert_eq!(lambert.core.dx, 2_539_703);
1618                assert_eq!(lambert.core.dy, 2_539_703);
1619                assert_eq!(lambert.projection_center_flag, 0);
1620                assert_eq!(lambert.core.scanning_mode, 0);
1621                assert_eq!(lambert.latin1, 25_000_000);
1622                assert_eq!(lambert.latin2, 25_000_000);
1623                assert_eq!(lambert.lat_southern_pole, -90_000_000);
1624                assert_eq!(lambert.lon_southern_pole, 0);
1625            }
1626            other => panic!("expected Lambert conformal grid, got {other:?}"),
1627        }
1628    }
1629
1630    #[test]
1631    fn reports_unsupported_template_helpers() {
1632        let grid = GridDefinition::Unsupported(3_276);
1633
1634        assert_eq!(grid.template_number(), 3_276);
1635        assert!(grid.as_lat_lon().is_none());
1636        assert!(grid.as_mercator().is_none());
1637        assert!(grid.as_polar_stereographic().is_none());
1638        assert!(grid.as_lambert_conformal().is_none());
1639        assert!(grid.as_albers_equal_area().is_none());
1640        assert_eq!(grid.unsupported_template(), Some(3_276));
1641    }
1642
1643    #[test]
1644    fn normalizes_alternating_row_scan() {
1645        let grid = LatLonGrid {
1646            ni: 3,
1647            nj: 2,
1648            lat_first: 0,
1649            lon_first: 0,
1650            lat_last: 0,
1651            lon_last: 0,
1652            di: 1,
1653            dj: 1,
1654            scanning_mode: 0b0001_0000,
1655        };
1656
1657        let ordered = grid
1658            .reorder_for_ndarray(vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0])
1659            .unwrap();
1660        assert_eq!(ordered, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1661    }
1662
1663    #[test]
1664    fn converts_ndarray_order_to_alternating_scan_order() {
1665        let grid = LatLonGrid {
1666            ni: 3,
1667            nj: 2,
1668            lat_first: 0,
1669            lon_first: 0,
1670            lat_last: 0,
1671            lon_last: 0,
1672            di: 1,
1673            dj: 1,
1674            scanning_mode: 0b0001_0000,
1675        };
1676
1677        let scan_order = grid
1678            .reorder_ndarray_to_grib_scan(vec![1, 2, 3, 4, 5, 6])
1679            .unwrap();
1680        assert_eq!(scan_order, vec![1, 2, 3, 6, 5, 4]);
1681    }
1682
1683    #[test]
1684    fn preserves_negative_i_direction_for_alternating_row_scan() {
1685        // Row 0 scans -i from lon_first; row 1 scans back in +i.
1686        let grid = GridDefinition::LatLon(LatLonGrid {
1687            ni: 3,
1688            nj: 2,
1689            lat_first: 0,
1690            lon_first: 0,
1691            lat_last: 0,
1692            lon_last: 0,
1693            di: 1,
1694            dj: 1,
1695            scanning_mode: 0b1001_0000,
1696        });
1697
1698        let mut reader_order = vec![1, 2, 3, 6, 5, 4];
1699        grid.reorder_for_ndarray_in_place(&mut reader_order)
1700            .unwrap();
1701        assert_eq!(reader_order, vec![1, 2, 3, 4, 5, 6]);
1702
1703        let mut north_up = vec![1, 2, 3, 6, 5, 4];
1704        grid.normalize_north_up_in_place(&mut north_up).unwrap();
1705        assert_eq!(north_up, vec![3, 2, 1, 6, 5, 4]);
1706    }
1707
1708    #[test]
1709    fn normalizes_polar_stereographic_alternating_row_scan() {
1710        let grid = PolarStereographicGrid {
1711            core: projected_core(3, 2, 1, 1, 0b0001_0000),
1712            lat_d: 0,
1713            lon_v: 0,
1714            projection_center_flag: 0,
1715        };
1716
1717        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1718        grid.core.reorder_for_ndarray_in_place(&mut values).unwrap();
1719        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1720    }
1721
1722    #[test]
1723    fn normalizes_mercator_alternating_row_scan() {
1724        let grid = MercatorGrid {
1725            core: projected_core(3, 2, 1, 1, 0b0001_0000),
1726            lat_d: 0,
1727            lat_last: 0,
1728            lon_last: 0,
1729            orientation_of_grid: 0,
1730        };
1731
1732        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1733        grid.core.reorder_for_ndarray_in_place(&mut values).unwrap();
1734        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1735    }
1736
1737    #[test]
1738    fn normalizes_lambert_alternating_row_scan() {
1739        let grid = LambertConformalGrid {
1740            core: ProjectedGridCore {
1741                shape_of_earth: 1,
1742                scaled_value_radius: 6_371_200,
1743                ..projected_core(3, 2, 1, 1, 0b0001_0000)
1744            },
1745            lat_d: 0,
1746            lon_v: 0,
1747            projection_center_flag: 0,
1748            latin1: 0,
1749            latin2: 0,
1750            lat_southern_pole: 0,
1751            lon_southern_pole: 0,
1752        };
1753
1754        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1755        grid.core.reorder_for_ndarray_in_place(&mut values).unwrap();
1756        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1757    }
1758
1759    #[test]
1760    fn normalizes_albers_alternating_row_scan() {
1761        let grid = AlbersEqualAreaGrid {
1762            core: projected_core(3, 2, 1, 1, 0b0001_0000),
1763            lat_d: 0,
1764            lon_v: 0,
1765            projection_center_flag: 0,
1766            latin1: 0,
1767            latin2: 0,
1768            lat_southern_pole: 0,
1769            lon_southern_pole: 0,
1770        };
1771
1772        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1773        grid.core.reorder_for_ndarray_in_place(&mut values).unwrap();
1774        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1775    }
1776
1777    #[test]
1778    fn generates_projected_coordinate_offsets() {
1779        let mercator = GridDefinition::Mercator(MercatorGrid {
1780            core: projected_core(3, 2, 1_000_000, 2_000_000, 0),
1781            lat_d: 0,
1782            lat_last: 0,
1783            lon_last: 0,
1784            orientation_of_grid: 0,
1785        });
1786        assert_eq!(
1787            mercator.projected_x_coordinates().unwrap().unwrap(),
1788            vec![0.0, 1_000.0, 2_000.0]
1789        );
1790        assert_eq!(
1791            mercator.projected_y_coordinates().unwrap().unwrap(),
1792            vec![-0.0, -2_000.0]
1793        );
1794
1795        let polar = GridDefinition::PolarStereographic(PolarStereographicGrid {
1796            core: projected_core(3, 2, 3_000_000, 6_000_000, 0),
1797            lat_d: 0,
1798            lon_v: 0,
1799            projection_center_flag: 0,
1800        });
1801        assert_eq!(
1802            polar.projected_x_coordinates().unwrap().unwrap(),
1803            vec![0.0, 3_000.0, 6_000.0]
1804        );
1805        assert_eq!(
1806            polar.projected_y_coordinates().unwrap().unwrap(),
1807            vec![-0.0, -6_000.0]
1808        );
1809
1810        let lambert = GridDefinition::LambertConformal(LambertConformalGrid {
1811            core: ProjectedGridCore {
1812                shape_of_earth: 1,
1813                scaled_value_radius: 6_371_200,
1814                ..projected_core(3, 2, 2_539_703, 2_539_703, 0b1100_0000)
1815            },
1816            lat_d: 0,
1817            lon_v: 0,
1818            projection_center_flag: 0,
1819            latin1: 0,
1820            latin2: 0,
1821            lat_southern_pole: 0,
1822            lon_southern_pole: 0,
1823        });
1824        assert_eq!(
1825            lambert.projected_x_coordinates().unwrap().unwrap(),
1826            vec![-0.0, -2_539.703, -5_079.406]
1827        );
1828        assert_eq!(
1829            lambert.projected_y_coordinates().unwrap().unwrap(),
1830            vec![0.0, 2_539.703]
1831        );
1832
1833        let albers = GridDefinition::AlbersEqualArea(AlbersEqualAreaGrid {
1834            core: projected_core(3, 2, 4_000_000, 5_000_000, 0b1100_0000),
1835            lat_d: 0,
1836            lon_v: 0,
1837            projection_center_flag: 0,
1838            latin1: 0,
1839            latin2: 0,
1840            lat_southern_pole: 0,
1841            lon_southern_pole: 0,
1842        });
1843        assert_eq!(
1844            albers.projected_x_coordinates().unwrap().unwrap(),
1845            vec![-0.0, -4_000.0, -8_000.0]
1846        );
1847        assert_eq!(
1848            albers.projected_y_coordinates().unwrap().unwrap(),
1849            vec![0.0, 5_000.0]
1850        );
1851    }
1852
1853    #[test]
1854    fn preserves_non_alternating_scan_modes_in_current_reader_order() {
1855        for scanning_mode in [0b0000_0000, 0b1000_0000, 0b0100_0000, 0b1100_0000] {
1856            let grid = LatLonGrid {
1857                ni: 3,
1858                nj: 2,
1859                lat_first: 0,
1860                lon_first: 0,
1861                lat_last: 0,
1862                lon_last: 0,
1863                di: 1,
1864                dj: 1,
1865                scanning_mode,
1866            };
1867
1868            let values = vec![1, 2, 3, 4, 5, 6];
1869            assert_eq!(
1870                grid.reorder_grib_scan_to_ndarray(values.clone()).unwrap(),
1871                values
1872            );
1873            assert_eq!(
1874                grid.reorder_ndarray_to_grib_scan(values.clone()).unwrap(),
1875                values
1876            );
1877        }
1878    }
1879
1880    #[test]
1881    fn normalizes_scan_directions_to_north_up_west_first_order() {
1882        let grid = GridDefinition::LatLon(LatLonGrid {
1883            ni: 3,
1884            nj: 2,
1885            lat_first: 0,
1886            lon_first: 0,
1887            lat_last: 0,
1888            lon_last: 0,
1889            di: 1,
1890            dj: 1,
1891            scanning_mode: 0b1100_0000,
1892        });
1893        let mut values = vec![1, 2, 3, 4, 5, 6];
1894
1895        grid.normalize_north_up_in_place(&mut values).unwrap();
1896
1897        assert_eq!(values, vec![6, 5, 4, 3, 2, 1]);
1898    }
1899
1900    #[test]
1901    fn rejects_j_consecutive_scan_order() {
1902        let grid = LatLonGrid {
1903            ni: 3,
1904            nj: 2,
1905            lat_first: 0,
1906            lon_first: 0,
1907            lat_last: 0,
1908            lon_last: 0,
1909            di: 1,
1910            dj: 1,
1911            scanning_mode: 0b0010_0000,
1912        };
1913
1914        let err = grid
1915            .reorder_ndarray_to_grib_scan(vec![1, 2, 3, 4, 5, 6])
1916            .unwrap_err();
1917        assert!(matches!(
1918            err,
1919            crate::Error::UnsupportedScanningMode(0b0010_0000)
1920        ));
1921    }
1922
1923    fn build_polar_stereographic_section(scanning_mode: u8) -> Vec<u8> {
1924        let mut section = vec![0u8; 65];
1925        section[..4].copy_from_slice(&65u32.to_be_bytes());
1926        section[4] = 3;
1927        section[6..10].copy_from_slice(&6u32.to_be_bytes());
1928        section[12..14].copy_from_slice(&20u16.to_be_bytes());
1929        section[14] = 6;
1930        section[30..34].copy_from_slice(&3u32.to_be_bytes());
1931        section[34..38].copy_from_slice(&2u32.to_be_bytes());
1932        section[38..42].copy_from_slice(&encode_wmo_i32(41_612_949).unwrap());
1933        section[42..46].copy_from_slice(&185_117_126u32.to_be_bytes());
1934        section[46] = 0x08;
1935        section[47..51].copy_from_slice(&encode_wmo_i32(60_000_000).unwrap());
1936        section[51..55].copy_from_slice(&225_000_000u32.to_be_bytes());
1937        section[55..59].copy_from_slice(&3_000_000u32.to_be_bytes());
1938        section[59..63].copy_from_slice(&3_000_000u32.to_be_bytes());
1939        section[64] = scanning_mode;
1940        section
1941    }
1942
1943    fn build_rotated_latlon_section() -> Vec<u8> {
1944        let mut section = vec![0u8; 84];
1945        section[..4].copy_from_slice(&84u32.to_be_bytes());
1946        section[4] = 3;
1947        section[6..10].copy_from_slice(&6u32.to_be_bytes());
1948        section[12..14].copy_from_slice(&1u16.to_be_bytes());
1949        section[30..34].copy_from_slice(&3u32.to_be_bytes());
1950        section[34..38].copy_from_slice(&2u32.to_be_bytes());
1951        section[46..50].copy_from_slice(&encode_wmo_i32(-10_000_000).unwrap());
1952        section[50..54].copy_from_slice(&encode_wmo_i32(-20_000_000).unwrap());
1953        section[55..59].copy_from_slice(&encode_wmo_i32(0).unwrap());
1954        section[59..63].copy_from_slice(&encode_wmo_i32(20_000_000).unwrap());
1955        section[63..67].copy_from_slice(&20_000_000u32.to_be_bytes());
1956        section[67..71].copy_from_slice(&10_000_000u32.to_be_bytes());
1957        section[71] = 0b0100_0000;
1958        section[72..76].copy_from_slice(&encode_wmo_i32(-30_000_000).unwrap());
1959        section[76..80].copy_from_slice(&10_000_000u32.to_be_bytes());
1960        section[80..84].copy_from_slice(&15.5f32.to_be_bytes());
1961        section
1962    }
1963
1964    fn build_regular_gaussian_section(di: u32) -> Vec<u8> {
1965        let mut section = vec![0u8; 72];
1966        section[..4].copy_from_slice(&72u32.to_be_bytes());
1967        section[4] = 3;
1968        section[6..10].copy_from_slice(&16u32.to_be_bytes());
1969        section[12..14].copy_from_slice(&40u16.to_be_bytes());
1970        section[30..34].copy_from_slice(&4u32.to_be_bytes());
1971        section[34..38].copy_from_slice(&4u32.to_be_bytes());
1972        section[46..50].copy_from_slice(&encode_wmo_i32(59_444_408).unwrap());
1973        section[50..54].copy_from_slice(&encode_wmo_i32(0).unwrap());
1974        section[55..59].copy_from_slice(&encode_wmo_i32(-59_444_408).unwrap());
1975        section[59..63].copy_from_slice(&encode_wmo_i32(270_000_000).unwrap());
1976        section[63..67].copy_from_slice(&di.to_be_bytes());
1977        section[67..71].copy_from_slice(&2u32.to_be_bytes());
1978        section
1979    }
1980
1981    fn build_mercator_section(scanning_mode: u8) -> Vec<u8> {
1982        let mut section = vec![0u8; 72];
1983        section[..4].copy_from_slice(&72u32.to_be_bytes());
1984        section[4] = 3;
1985        section[6..10].copy_from_slice(&6u32.to_be_bytes());
1986        section[12..14].copy_from_slice(&10u16.to_be_bytes());
1987        section[14] = 6;
1988        section[30..34].copy_from_slice(&3u32.to_be_bytes());
1989        section[34..38].copy_from_slice(&2u32.to_be_bytes());
1990        section[38..42].copy_from_slice(&encode_wmo_i32(-20_000_000).unwrap());
1991        section[42..46].copy_from_slice(&encode_wmo_i32(-100_000_000).unwrap());
1992        section[46] = 0x08;
1993        section[47..51].copy_from_slice(&encode_wmo_i32(0).unwrap());
1994        section[51..55].copy_from_slice(&encode_wmo_i32(20_000_000).unwrap());
1995        section[55..59].copy_from_slice(&encode_wmo_i32(-98_000_000).unwrap());
1996        section[59] = scanning_mode;
1997        section[64..68].copy_from_slice(&1_000_000u32.to_be_bytes());
1998        section[68..72].copy_from_slice(&2_000_000u32.to_be_bytes());
1999        section
2000    }
2001
2002    fn build_lambert_section() -> Vec<u8> {
2003        let mut section = vec![0u8; 81];
2004        section[..4].copy_from_slice(&81u32.to_be_bytes());
2005        section[4] = 3;
2006        section[6..10].copy_from_slice(&6u32.to_be_bytes());
2007        section[12..14].copy_from_slice(&30u16.to_be_bytes());
2008        section[14] = 1;
2009        section[16..20].copy_from_slice(&6_371_200u32.to_be_bytes());
2010        section[30..34].copy_from_slice(&3u32.to_be_bytes());
2011        section[34..38].copy_from_slice(&2u32.to_be_bytes());
2012        section[38..42].copy_from_slice(&encode_wmo_i32(12_190_000).unwrap());
2013        section[42..46].copy_from_slice(&226_541_000u32.to_be_bytes());
2014        section[46] = 0x08;
2015        section[47..51].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
2016        section[51..55].copy_from_slice(&265_000_000u32.to_be_bytes());
2017        section[55..59].copy_from_slice(&2_539_703u32.to_be_bytes());
2018        section[59..63].copy_from_slice(&2_539_703u32.to_be_bytes());
2019        section[65..69].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
2020        section[69..73].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
2021        section[73..77].copy_from_slice(&encode_wmo_i32(-90_000_000).unwrap());
2022        section
2023    }
2024
2025    fn build_albers_section() -> Vec<u8> {
2026        let mut section = vec![0u8; 81];
2027        section[..4].copy_from_slice(&81u32.to_be_bytes());
2028        section[4] = 3;
2029        section[6..10].copy_from_slice(&6u32.to_be_bytes());
2030        section[12..14].copy_from_slice(&31u16.to_be_bytes());
2031        section[14] = 6;
2032        section[30..34].copy_from_slice(&3u32.to_be_bytes());
2033        section[34..38].copy_from_slice(&2u32.to_be_bytes());
2034        section[38..42].copy_from_slice(&encode_wmo_i32(23_000_000).unwrap());
2035        section[42..46].copy_from_slice(&240_000_000u32.to_be_bytes());
2036        section[46] = 0x08;
2037        section[47..51].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
2038        section[51..55].copy_from_slice(&265_000_000u32.to_be_bytes());
2039        section[55..59].copy_from_slice(&4_000_000u32.to_be_bytes());
2040        section[59..63].copy_from_slice(&5_000_000u32.to_be_bytes());
2041        section[65..69].copy_from_slice(&encode_wmo_i32(29_500_000).unwrap());
2042        section[69..73].copy_from_slice(&encode_wmo_i32(45_500_000).unwrap());
2043        section[73..77].copy_from_slice(&encode_wmo_i32(-90_000_000).unwrap());
2044        section
2045    }
2046}