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.10: Mercator.
18    Mercator(MercatorGrid),
19    /// Template 3.31: Albers equal-area.
20    AlbersEqualArea(AlbersEqualAreaGrid),
21    /// Template 3.30: Lambert conformal.
22    LambertConformal(LambertConformalGrid),
23    /// Template 3.20: Polar stereographic projection.
24    PolarStereographic(PolarStereographicGrid),
25    /// Unsupported template (stored for diagnostics).
26    Unsupported(u16),
27}
28
29/// Template 3.0: Regular latitude/longitude grid.
30#[derive(Debug, Clone, PartialEq)]
31pub struct LatLonGrid {
32    pub ni: u32,
33    pub nj: u32,
34    pub lat_first: i32,
35    pub lon_first: i32,
36    pub lat_last: i32,
37    pub lon_last: i32,
38    pub di: u32,
39    pub dj: u32,
40    pub scanning_mode: u8,
41}
42
43/// Template 3.10: Mercator grid.
44#[derive(Debug, Clone, PartialEq)]
45pub struct MercatorGrid {
46    pub number_of_points: u32,
47    pub shape_of_earth: u8,
48    pub scale_factor_radius: u8,
49    pub scaled_value_radius: u32,
50    pub scale_factor_major_axis: u8,
51    pub scaled_value_major_axis: u32,
52    pub scale_factor_minor_axis: u8,
53    pub scaled_value_minor_axis: u32,
54    pub ni: u32,
55    pub nj: u32,
56    pub lat_first: i32,
57    pub lon_first: i32,
58    pub resolution_and_component_flags: u8,
59    pub lat_d: i32,
60    pub lat_last: i32,
61    pub lon_last: i32,
62    pub scanning_mode: u8,
63    pub orientation_of_grid: u32,
64    pub di: u32,
65    pub dj: u32,
66}
67
68/// Template 3.30: Lambert conformal grid.
69#[derive(Debug, Clone, PartialEq)]
70pub struct LambertConformalGrid {
71    pub number_of_points: u32,
72    pub shape_of_earth: u8,
73    pub scale_factor_radius: u8,
74    pub scaled_value_radius: u32,
75    pub scale_factor_major_axis: u8,
76    pub scaled_value_major_axis: u32,
77    pub scale_factor_minor_axis: u8,
78    pub scaled_value_minor_axis: u32,
79    pub nx: u32,
80    pub ny: u32,
81    pub lat_first: i32,
82    pub lon_first: u32,
83    pub resolution_and_component_flags: u8,
84    pub lat_d: i32,
85    pub lon_v: u32,
86    pub dx: u32,
87    pub dy: u32,
88    pub projection_center_flag: u8,
89    pub scanning_mode: u8,
90    pub latin1: i32,
91    pub latin2: i32,
92    pub lat_southern_pole: i32,
93    pub lon_southern_pole: u32,
94}
95
96/// Template 3.31: Albers equal-area grid.
97#[derive(Debug, Clone, PartialEq)]
98pub struct AlbersEqualAreaGrid {
99    pub number_of_points: u32,
100    pub shape_of_earth: u8,
101    pub scale_factor_radius: u8,
102    pub scaled_value_radius: u32,
103    pub scale_factor_major_axis: u8,
104    pub scaled_value_major_axis: u32,
105    pub scale_factor_minor_axis: u8,
106    pub scaled_value_minor_axis: u32,
107    pub nx: u32,
108    pub ny: u32,
109    pub lat_first: i32,
110    pub lon_first: u32,
111    pub resolution_and_component_flags: u8,
112    pub lat_d: i32,
113    pub lon_v: u32,
114    pub dx: u32,
115    pub dy: u32,
116    pub projection_center_flag: u8,
117    pub scanning_mode: u8,
118    pub latin1: i32,
119    pub latin2: i32,
120    pub lat_southern_pole: i32,
121    pub lon_southern_pole: u32,
122}
123
124/// Template 3.20: Polar stereographic projection.
125#[derive(Debug, Clone, PartialEq)]
126pub struct PolarStereographicGrid {
127    pub number_of_points: u32,
128    pub shape_of_earth: u8,
129    pub scale_factor_radius: u8,
130    pub scaled_value_radius: u32,
131    pub scale_factor_major_axis: u8,
132    pub scaled_value_major_axis: u32,
133    pub scale_factor_minor_axis: u8,
134    pub scaled_value_minor_axis: u32,
135    pub nx: u32,
136    pub ny: u32,
137    pub lat_first: i32,
138    pub lon_first: u32,
139    pub resolution_and_component_flags: u8,
140    pub lat_d: i32,
141    pub lon_v: u32,
142    pub dx: u32,
143    pub dy: u32,
144    pub projection_center_flag: u8,
145    pub scanning_mode: u8,
146}
147
148impl GridDefinition {
149    /// GRIB2 grid definition template number for typed templates.
150    ///
151    /// `Unsupported` stores the unhandled template number from the source
152    /// message for diagnostics.
153    pub fn template_number(&self) -> u16 {
154        match self {
155            Self::LatLon(_) => 0,
156            Self::Mercator(_) => 10,
157            Self::PolarStereographic(_) => 20,
158            Self::LambertConformal(_) => 30,
159            Self::AlbersEqualArea(_) => 31,
160            Self::Unsupported(template) => *template,
161        }
162    }
163
164    pub fn as_lat_lon(&self) -> Option<&LatLonGrid> {
165        match self {
166            Self::LatLon(grid) => Some(grid),
167            _ => None,
168        }
169    }
170
171    pub fn as_polar_stereographic(&self) -> Option<&PolarStereographicGrid> {
172        match self {
173            Self::PolarStereographic(grid) => Some(grid),
174            _ => None,
175        }
176    }
177
178    pub fn as_mercator(&self) -> Option<&MercatorGrid> {
179        match self {
180            Self::Mercator(grid) => Some(grid),
181            _ => None,
182        }
183    }
184
185    pub fn as_lambert_conformal(&self) -> Option<&LambertConformalGrid> {
186        match self {
187            Self::LambertConformal(grid) => Some(grid),
188            _ => None,
189        }
190    }
191
192    pub fn as_albers_equal_area(&self) -> Option<&AlbersEqualAreaGrid> {
193        match self {
194            Self::AlbersEqualArea(grid) => Some(grid),
195            _ => None,
196        }
197    }
198
199    pub fn unsupported_template(&self) -> Option<u16> {
200        match self {
201            Self::Unsupported(template) => Some(*template),
202            _ => None,
203        }
204    }
205
206    pub fn shape(&self) -> (usize, usize) {
207        match self {
208            Self::LatLon(g) => (g.ni as usize, g.nj as usize),
209            Self::Mercator(g) => (g.ni as usize, g.nj as usize),
210            Self::PolarStereographic(g) => (g.nx as usize, g.ny as usize),
211            Self::LambertConformal(g) => (g.nx as usize, g.ny as usize),
212            Self::AlbersEqualArea(g) => (g.nx as usize, g.ny as usize),
213            Self::Unsupported(_) => (0, 0),
214        }
215    }
216
217    pub fn shape_num_points(&self) -> Result<usize> {
218        match self {
219            Self::LatLon(g) => checked_grid_point_count(g.ni, g.nj),
220            Self::Mercator(g) => checked_grid_point_count(g.ni, g.nj),
221            Self::PolarStereographic(g) => checked_grid_point_count(g.nx, g.ny),
222            Self::LambertConformal(g) => checked_grid_point_count(g.nx, g.ny),
223            Self::AlbersEqualArea(g) => checked_grid_point_count(g.nx, g.ny),
224            Self::Unsupported(_) => Ok(0),
225        }
226    }
227
228    pub fn ndarray_shape(&self) -> Vec<usize> {
229        let (ni, nj) = self.shape();
230        match self {
231            Self::LatLon(_)
232            | Self::Mercator(_)
233            | Self::PolarStereographic(_)
234            | Self::LambertConformal(_)
235            | Self::AlbersEqualArea(_)
236                if ni > 0 && nj > 0 =>
237            {
238                vec![nj, ni]
239            }
240            _ => Vec::new(),
241        }
242    }
243
244    pub fn num_points(&self) -> usize {
245        self.checked_num_points().unwrap_or(usize::MAX)
246    }
247
248    pub fn checked_num_points(&self) -> Result<usize> {
249        match self {
250            Self::LatLon(_) => self.shape_num_points(),
251            Self::Mercator(g) => Ok(g.number_of_points as usize),
252            Self::PolarStereographic(g) => Ok(g.number_of_points as usize),
253            Self::LambertConformal(g) => Ok(g.number_of_points as usize),
254            Self::AlbersEqualArea(g) => Ok(g.number_of_points as usize),
255            Self::Unsupported(_) => Ok(0),
256        }
257    }
258
259    pub fn declared_num_points(&self) -> Option<usize> {
260        match self {
261            Self::Mercator(g) => Some(g.number_of_points as usize),
262            Self::PolarStereographic(g) => Some(g.number_of_points as usize),
263            Self::LambertConformal(g) => Some(g.number_of_points as usize),
264            Self::AlbersEqualArea(g) => Some(g.number_of_points as usize),
265            Self::LatLon(_) | Self::Unsupported(_) => None,
266        }
267    }
268
269    /// Projected x coordinates in metres relative to the first grid point.
270    ///
271    /// Regular latitude/longitude grids return `None`; use
272    /// [`LatLonGrid::longitudes`] for those grids.
273    pub fn projected_x_coordinates(&self) -> Result<Option<Vec<f64>>> {
274        self.projected_x_coordinates_with_limit(None)
275    }
276
277    pub fn projected_x_coordinates_with_limit(
278        &self,
279        max_axis_points: Option<usize>,
280    ) -> Result<Option<Vec<f64>>> {
281        match self {
282            Self::Mercator(grid) => Ok(Some(grid.x_coordinates_with_limit(max_axis_points)?)),
283            Self::PolarStereographic(grid) => {
284                Ok(Some(grid.x_coordinates_with_limit(max_axis_points)?))
285            }
286            Self::LambertConformal(grid) => {
287                Ok(Some(grid.x_coordinates_with_limit(max_axis_points)?))
288            }
289            Self::AlbersEqualArea(grid) => {
290                Ok(Some(grid.x_coordinates_with_limit(max_axis_points)?))
291            }
292            _ => Ok(None),
293        }
294    }
295
296    /// Projected y coordinates in metres relative to the first grid point.
297    ///
298    /// Regular latitude/longitude grids return `None`; use
299    /// [`LatLonGrid::latitudes`] for those grids.
300    pub fn projected_y_coordinates(&self) -> Result<Option<Vec<f64>>> {
301        self.projected_y_coordinates_with_limit(None)
302    }
303
304    pub fn projected_y_coordinates_with_limit(
305        &self,
306        max_axis_points: Option<usize>,
307    ) -> Result<Option<Vec<f64>>> {
308        match self {
309            Self::Mercator(grid) => Ok(Some(grid.y_coordinates_with_limit(max_axis_points)?)),
310            Self::PolarStereographic(grid) => {
311                Ok(Some(grid.y_coordinates_with_limit(max_axis_points)?))
312            }
313            Self::LambertConformal(grid) => {
314                Ok(Some(grid.y_coordinates_with_limit(max_axis_points)?))
315            }
316            Self::AlbersEqualArea(grid) => {
317                Ok(Some(grid.y_coordinates_with_limit(max_axis_points)?))
318            }
319            _ => Ok(None),
320        }
321    }
322
323    pub fn validate_supported_scan_order(&self) -> Result<()> {
324        match self {
325            Self::LatLon(grid) => grid.validate_supported_scan_order(),
326            Self::Mercator(grid) => grid.validate_supported_scan_order(),
327            Self::PolarStereographic(grid) => grid.validate_supported_scan_order(),
328            Self::LambertConformal(grid) => grid.validate_supported_scan_order(),
329            Self::AlbersEqualArea(grid) => grid.validate_supported_scan_order(),
330            Self::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
331        }
332    }
333
334    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
335        match self {
336            Self::LatLon(grid) => grid.reorder_for_ndarray_in_place(values),
337            Self::Mercator(grid) => grid.reorder_for_ndarray_in_place(values),
338            Self::PolarStereographic(grid) => grid.reorder_for_ndarray_in_place(values),
339            Self::LambertConformal(grid) => grid.reorder_for_ndarray_in_place(values),
340            Self::AlbersEqualArea(grid) => grid.reorder_for_ndarray_in_place(values),
341            Self::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
342        }
343    }
344
345    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
346        if section_bytes.len() < 14 {
347            return Err(Error::InvalidSection {
348                section: 3,
349                reason: format!("expected at least 14 bytes, got {}", section_bytes.len()),
350            });
351        }
352        if section_bytes[4] != 3 {
353            return Err(Error::InvalidSection {
354                section: section_bytes[4],
355                reason: "not a grid definition section".into(),
356            });
357        }
358
359        let template = u16::from_be_bytes(section_bytes[12..14].try_into().unwrap());
360        match template {
361            0 => parse_latlon(section_bytes),
362            10 => parse_mercator(section_bytes),
363            20 => parse_polar_stereographic(section_bytes),
364            30 => parse_lambert_conformal(section_bytes),
365            31 => parse_albers_equal_area(section_bytes),
366            _ => Ok(Self::Unsupported(template)),
367        }
368    }
369}
370
371impl LatLonGrid {
372    pub fn longitudes(&self) -> Result<Vec<f64>> {
373        self.longitudes_with_limit(None)
374    }
375
376    pub fn longitudes_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
377        let step = self.di as f64 / 1_000_000.0;
378        let signed_step = if self.i_scans_positive() { step } else { -step };
379        let start = self.lon_first as f64 / 1_000_000.0;
380        linear_axis(
381            self.ni,
382            start,
383            signed_step,
384            max_axis_points,
385            "longitude axis",
386        )
387    }
388
389    pub fn latitudes(&self) -> Result<Vec<f64>> {
390        self.latitudes_with_limit(None)
391    }
392
393    pub fn latitudes_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
394        let step = self.dj as f64 / 1_000_000.0;
395        let signed_step = if self.j_scans_positive() { step } else { -step };
396        let start = self.lat_first as f64 / 1_000_000.0;
397        linear_axis(
398            self.nj,
399            start,
400            signed_step,
401            max_axis_points,
402            "latitude axis",
403        )
404    }
405
406    pub fn reorder_for_ndarray<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
407        self.reorder_grib_scan_to_ndarray_in_place(&mut values)?;
408        Ok(values)
409    }
410
411    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
412        self.reorder_grib_scan_to_ndarray_in_place(values)
413    }
414
415    pub fn reorder_grib_scan_to_ndarray<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
416        self.reorder_grib_scan_to_ndarray_in_place(&mut values)?;
417        Ok(values)
418    }
419
420    pub fn reorder_grib_scan_to_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
421        transform_supported_scan_order_in_place(
422            values,
423            self.ni as usize,
424            self.nj as usize,
425            self.scanning_mode,
426        )
427    }
428
429    pub fn reorder_ndarray_to_grib_scan<T>(&self, mut values: Vec<T>) -> Result<Vec<T>> {
430        self.reorder_ndarray_to_grib_scan_in_place(&mut values)?;
431        Ok(values)
432    }
433
434    pub fn reorder_ndarray_to_grib_scan_in_place<T>(&self, values: &mut [T]) -> Result<()> {
435        transform_supported_scan_order_in_place(
436            values,
437            self.ni as usize,
438            self.nj as usize,
439            self.scanning_mode,
440        )
441    }
442
443    pub fn validate_supported_scan_order(&self) -> Result<()> {
444        validate_supported_scan_order(self.scanning_mode)
445    }
446
447    fn i_scans_positive(&self) -> bool {
448        i_scans_positive(self.scanning_mode)
449    }
450
451    fn j_scans_positive(&self) -> bool {
452        j_scans_positive(self.scanning_mode)
453    }
454}
455
456impl MercatorGrid {
457    pub fn x_coordinates(&self) -> Result<Vec<f64>> {
458        self.x_coordinates_with_limit(None)
459    }
460
461    pub fn x_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
462        projected_x_coordinates(self.ni, self.di, self.scanning_mode, max_axis_points)
463    }
464
465    pub fn y_coordinates(&self) -> Result<Vec<f64>> {
466        self.y_coordinates_with_limit(None)
467    }
468
469    pub fn y_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
470        projected_y_coordinates(self.nj, self.dj, self.scanning_mode, max_axis_points)
471    }
472
473    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
474        transform_supported_scan_order_in_place(
475            values,
476            self.ni as usize,
477            self.nj as usize,
478            self.scanning_mode,
479        )
480    }
481
482    pub fn validate_supported_scan_order(&self) -> Result<()> {
483        validate_supported_scan_order(self.scanning_mode)
484    }
485}
486
487impl LambertConformalGrid {
488    pub fn x_coordinates(&self) -> Result<Vec<f64>> {
489        self.x_coordinates_with_limit(None)
490    }
491
492    pub fn x_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
493        projected_x_coordinates(self.nx, self.dx, self.scanning_mode, max_axis_points)
494    }
495
496    pub fn y_coordinates(&self) -> Result<Vec<f64>> {
497        self.y_coordinates_with_limit(None)
498    }
499
500    pub fn y_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
501        projected_y_coordinates(self.ny, self.dy, self.scanning_mode, max_axis_points)
502    }
503
504    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
505        transform_supported_scan_order_in_place(
506            values,
507            self.nx as usize,
508            self.ny as usize,
509            self.scanning_mode,
510        )
511    }
512
513    pub fn validate_supported_scan_order(&self) -> Result<()> {
514        validate_supported_scan_order(self.scanning_mode)
515    }
516}
517
518impl AlbersEqualAreaGrid {
519    pub fn x_coordinates(&self) -> Result<Vec<f64>> {
520        self.x_coordinates_with_limit(None)
521    }
522
523    pub fn x_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
524        projected_x_coordinates(self.nx, self.dx, self.scanning_mode, max_axis_points)
525    }
526
527    pub fn y_coordinates(&self) -> Result<Vec<f64>> {
528        self.y_coordinates_with_limit(None)
529    }
530
531    pub fn y_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
532        projected_y_coordinates(self.ny, self.dy, self.scanning_mode, max_axis_points)
533    }
534
535    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
536        transform_supported_scan_order_in_place(
537            values,
538            self.nx as usize,
539            self.ny as usize,
540            self.scanning_mode,
541        )
542    }
543
544    pub fn validate_supported_scan_order(&self) -> Result<()> {
545        validate_supported_scan_order(self.scanning_mode)
546    }
547}
548
549impl PolarStereographicGrid {
550    pub fn x_coordinates(&self) -> Result<Vec<f64>> {
551        self.x_coordinates_with_limit(None)
552    }
553
554    pub fn x_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
555        projected_x_coordinates(self.nx, self.dx, self.scanning_mode, max_axis_points)
556    }
557
558    pub fn y_coordinates(&self) -> Result<Vec<f64>> {
559        self.y_coordinates_with_limit(None)
560    }
561
562    pub fn y_coordinates_with_limit(&self, max_axis_points: Option<usize>) -> Result<Vec<f64>> {
563        projected_y_coordinates(self.ny, self.dy, self.scanning_mode, max_axis_points)
564    }
565
566    pub fn reorder_for_ndarray_in_place<T>(&self, values: &mut [T]) -> Result<()> {
567        transform_supported_scan_order_in_place(
568            values,
569            self.nx as usize,
570            self.ny as usize,
571            self.scanning_mode,
572        )
573    }
574
575    pub fn validate_supported_scan_order(&self) -> Result<()> {
576        validate_supported_scan_order(self.scanning_mode)
577    }
578}
579
580fn transform_supported_scan_order_in_place<T>(
581    values: &mut [T],
582    ni: usize,
583    nj: usize,
584    scanning_mode: u8,
585) -> Result<()> {
586    validate_supported_scan_order(scanning_mode)?;
587    let expected = ni
588        .checked_mul(nj)
589        .ok_or_else(|| Error::Other("grid point count overflow".into()))?;
590    if values.len() != expected {
591        return Err(Error::DataLengthMismatch {
592            expected,
593            actual: values.len(),
594        });
595    }
596
597    if adjacent_rows_alternate_direction(scanning_mode) {
598        reverse_alternating_rows(values, ni, nj, i_scans_positive(scanning_mode));
599    }
600
601    Ok(())
602}
603
604fn validate_supported_scan_order(scanning_mode: u8) -> Result<()> {
605    if i_points_are_consecutive(scanning_mode) {
606        Ok(())
607    } else {
608        Err(Error::UnsupportedScanningMode(scanning_mode))
609    }
610}
611
612fn i_scans_positive(scanning_mode: u8) -> bool {
613    scanning_mode & 0b1000_0000 == 0
614}
615
616fn j_scans_positive(scanning_mode: u8) -> bool {
617    scanning_mode & 0b0100_0000 != 0
618}
619
620fn i_points_are_consecutive(scanning_mode: u8) -> bool {
621    scanning_mode & 0b0010_0000 == 0
622}
623
624fn adjacent_rows_alternate_direction(scanning_mode: u8) -> bool {
625    scanning_mode & 0b0001_0000 != 0
626}
627
628fn projected_x_coordinates(
629    count: u32,
630    spacing_millimetres: u32,
631    scanning_mode: u8,
632    max_axis_points: Option<usize>,
633) -> Result<Vec<f64>> {
634    projected_axis(
635        count,
636        spacing_millimetres,
637        i_scans_positive(scanning_mode),
638        max_axis_points,
639        "projected x axis",
640    )
641}
642
643fn projected_y_coordinates(
644    count: u32,
645    spacing_millimetres: u32,
646    scanning_mode: u8,
647    max_axis_points: Option<usize>,
648) -> Result<Vec<f64>> {
649    projected_axis(
650        count,
651        spacing_millimetres,
652        j_scans_positive(scanning_mode),
653        max_axis_points,
654        "projected y axis",
655    )
656}
657
658fn projected_axis(
659    count: u32,
660    spacing_millimetres: u32,
661    scans_positive: bool,
662    max_axis_points: Option<usize>,
663    what: &'static str,
664) -> Result<Vec<f64>> {
665    let step = f64::from(spacing_millimetres) / 1_000.0;
666    let signed_step = if scans_positive { step } else { -step };
667    linear_axis(count, 0.0, signed_step, max_axis_points, what)
668}
669
670fn linear_axis(
671    count: u32,
672    start: f64,
673    signed_step: f64,
674    max_axis_points: Option<usize>,
675    what: &'static str,
676) -> Result<Vec<f64>> {
677    let count = count as usize;
678    ensure_limit(what, count, max_axis_points)?;
679    let mut axis = Vec::new();
680    axis.try_reserve(count)
681        .map_err(|e| Error::Other(format!("failed to reserve {count} {what} coordinates: {e}")))?;
682    axis.extend((0..count).map(|index| start + signed_step * index as f64));
683    Ok(axis)
684}
685
686fn checked_grid_point_count(nx: u32, ny: u32) -> Result<usize> {
687    let count = u64::from(nx) * u64::from(ny);
688    usize::try_from(count)
689        .map_err(|_| Error::Other(format!("grid point count {count} does not fit in usize")))
690}
691
692fn ensure_limit(what: &'static str, requested: usize, limit: Option<usize>) -> Result<()> {
693    if let Some(limit) = limit {
694        if requested > limit {
695            return Err(Error::LimitExceeded {
696                what,
697                requested,
698                limit,
699            });
700        }
701    }
702    Ok(())
703}
704
705fn reverse_alternating_rows<T>(values: &mut [T], ni: usize, nj: usize, i_scans_positive: bool) {
706    for row in 0..nj {
707        let reverse = if i_scans_positive {
708            row % 2 == 1
709        } else {
710            row % 2 == 0
711        };
712        if reverse {
713            values[row * ni..(row + 1) * ni].reverse();
714        }
715    }
716}
717
718fn parse_latlon(data: &[u8]) -> Result<GridDefinition> {
719    if data.len() < 72 {
720        return Err(Error::InvalidSection {
721            section: 3,
722            reason: format!("template 3.0 requires 72 bytes, got {}", data.len()),
723        });
724    }
725
726    let ni = u32::from_be_bytes(data[30..34].try_into().unwrap());
727    let nj = u32::from_be_bytes(data[34..38].try_into().unwrap());
728    let lat_first = grib_i32(&data[46..50]).unwrap();
729    let lon_first = grib_i32(&data[50..54]).unwrap();
730    let lat_last = grib_i32(&data[55..59]).unwrap();
731    let lon_last = grib_i32(&data[59..63]).unwrap();
732    let di = u32::from_be_bytes(data[63..67].try_into().unwrap());
733    let dj = u32::from_be_bytes(data[67..71].try_into().unwrap());
734    let scanning_mode = data[71];
735
736    Ok(GridDefinition::LatLon(LatLonGrid {
737        ni,
738        nj,
739        lat_first,
740        lon_first,
741        lat_last,
742        lon_last,
743        di,
744        dj,
745        scanning_mode,
746    }))
747}
748
749fn parse_mercator(data: &[u8]) -> Result<GridDefinition> {
750    if data.len() < 72 {
751        return Err(Error::InvalidSection {
752            section: 3,
753            reason: format!("template 3.10 requires 72 bytes, got {}", data.len()),
754        });
755    }
756
757    Ok(GridDefinition::Mercator(MercatorGrid {
758        number_of_points: u32::from_be_bytes(data[6..10].try_into().unwrap()),
759        shape_of_earth: data[14],
760        scale_factor_radius: data[15],
761        scaled_value_radius: u32::from_be_bytes(data[16..20].try_into().unwrap()),
762        scale_factor_major_axis: data[20],
763        scaled_value_major_axis: u32::from_be_bytes(data[21..25].try_into().unwrap()),
764        scale_factor_minor_axis: data[25],
765        scaled_value_minor_axis: u32::from_be_bytes(data[26..30].try_into().unwrap()),
766        ni: u32::from_be_bytes(data[30..34].try_into().unwrap()),
767        nj: u32::from_be_bytes(data[34..38].try_into().unwrap()),
768        lat_first: grib_i32(&data[38..42]).unwrap(),
769        lon_first: grib_i32(&data[42..46]).unwrap(),
770        resolution_and_component_flags: data[46],
771        lat_d: grib_i32(&data[47..51]).unwrap(),
772        lat_last: grib_i32(&data[51..55]).unwrap(),
773        lon_last: grib_i32(&data[55..59]).unwrap(),
774        scanning_mode: data[59],
775        orientation_of_grid: u32::from_be_bytes(data[60..64].try_into().unwrap()),
776        di: u32::from_be_bytes(data[64..68].try_into().unwrap()),
777        dj: u32::from_be_bytes(data[68..72].try_into().unwrap()),
778    }))
779}
780
781fn parse_polar_stereographic(data: &[u8]) -> Result<GridDefinition> {
782    if data.len() < 65 {
783        return Err(Error::InvalidSection {
784            section: 3,
785            reason: format!("template 3.20 requires 65 bytes, got {}", data.len()),
786        });
787    }
788
789    Ok(GridDefinition::PolarStereographic(PolarStereographicGrid {
790        number_of_points: u32::from_be_bytes(data[6..10].try_into().unwrap()),
791        shape_of_earth: data[14],
792        scale_factor_radius: data[15],
793        scaled_value_radius: u32::from_be_bytes(data[16..20].try_into().unwrap()),
794        scale_factor_major_axis: data[20],
795        scaled_value_major_axis: u32::from_be_bytes(data[21..25].try_into().unwrap()),
796        scale_factor_minor_axis: data[25],
797        scaled_value_minor_axis: u32::from_be_bytes(data[26..30].try_into().unwrap()),
798        nx: u32::from_be_bytes(data[30..34].try_into().unwrap()),
799        ny: u32::from_be_bytes(data[34..38].try_into().unwrap()),
800        lat_first: grib_i32(&data[38..42]).unwrap(),
801        lon_first: u32::from_be_bytes(data[42..46].try_into().unwrap()),
802        resolution_and_component_flags: data[46],
803        lat_d: grib_i32(&data[47..51]).unwrap(),
804        lon_v: u32::from_be_bytes(data[51..55].try_into().unwrap()),
805        dx: u32::from_be_bytes(data[55..59].try_into().unwrap()),
806        dy: u32::from_be_bytes(data[59..63].try_into().unwrap()),
807        projection_center_flag: data[63],
808        scanning_mode: data[64],
809    }))
810}
811
812fn parse_albers_equal_area(data: &[u8]) -> Result<GridDefinition> {
813    if data.len() < 81 {
814        return Err(Error::InvalidSection {
815            section: 3,
816            reason: format!("template 3.31 requires 81 bytes, got {}", data.len()),
817        });
818    }
819
820    Ok(GridDefinition::AlbersEqualArea(AlbersEqualAreaGrid {
821        number_of_points: u32::from_be_bytes(data[6..10].try_into().unwrap()),
822        shape_of_earth: data[14],
823        scale_factor_radius: data[15],
824        scaled_value_radius: u32::from_be_bytes(data[16..20].try_into().unwrap()),
825        scale_factor_major_axis: data[20],
826        scaled_value_major_axis: u32::from_be_bytes(data[21..25].try_into().unwrap()),
827        scale_factor_minor_axis: data[25],
828        scaled_value_minor_axis: u32::from_be_bytes(data[26..30].try_into().unwrap()),
829        nx: u32::from_be_bytes(data[30..34].try_into().unwrap()),
830        ny: u32::from_be_bytes(data[34..38].try_into().unwrap()),
831        lat_first: grib_i32(&data[38..42]).unwrap(),
832        lon_first: u32::from_be_bytes(data[42..46].try_into().unwrap()),
833        resolution_and_component_flags: data[46],
834        lat_d: grib_i32(&data[47..51]).unwrap(),
835        lon_v: u32::from_be_bytes(data[51..55].try_into().unwrap()),
836        dx: u32::from_be_bytes(data[55..59].try_into().unwrap()),
837        dy: u32::from_be_bytes(data[59..63].try_into().unwrap()),
838        projection_center_flag: data[63],
839        scanning_mode: data[64],
840        latin1: grib_i32(&data[65..69]).unwrap(),
841        latin2: grib_i32(&data[69..73]).unwrap(),
842        lat_southern_pole: grib_i32(&data[73..77]).unwrap(),
843        lon_southern_pole: u32::from_be_bytes(data[77..81].try_into().unwrap()),
844    }))
845}
846
847fn parse_lambert_conformal(data: &[u8]) -> Result<GridDefinition> {
848    if data.len() < 81 {
849        return Err(Error::InvalidSection {
850            section: 3,
851            reason: format!("template 3.30 requires 81 bytes, got {}", data.len()),
852        });
853    }
854
855    Ok(GridDefinition::LambertConformal(LambertConformalGrid {
856        number_of_points: u32::from_be_bytes(data[6..10].try_into().unwrap()),
857        shape_of_earth: data[14],
858        scale_factor_radius: data[15],
859        scaled_value_radius: u32::from_be_bytes(data[16..20].try_into().unwrap()),
860        scale_factor_major_axis: data[20],
861        scaled_value_major_axis: u32::from_be_bytes(data[21..25].try_into().unwrap()),
862        scale_factor_minor_axis: data[25],
863        scaled_value_minor_axis: u32::from_be_bytes(data[26..30].try_into().unwrap()),
864        nx: u32::from_be_bytes(data[30..34].try_into().unwrap()),
865        ny: u32::from_be_bytes(data[34..38].try_into().unwrap()),
866        lat_first: grib_i32(&data[38..42]).unwrap(),
867        lon_first: u32::from_be_bytes(data[42..46].try_into().unwrap()),
868        resolution_and_component_flags: data[46],
869        lat_d: grib_i32(&data[47..51]).unwrap(),
870        lon_v: u32::from_be_bytes(data[51..55].try_into().unwrap()),
871        dx: u32::from_be_bytes(data[55..59].try_into().unwrap()),
872        dy: u32::from_be_bytes(data[59..63].try_into().unwrap()),
873        projection_center_flag: data[63],
874        scanning_mode: data[64],
875        latin1: grib_i32(&data[65..69]).unwrap(),
876        latin2: grib_i32(&data[69..73]).unwrap(),
877        lat_southern_pole: grib_i32(&data[73..77]).unwrap(),
878        lon_southern_pole: u32::from_be_bytes(data[77..81].try_into().unwrap()),
879    }))
880}
881
882#[cfg(test)]
883mod tests {
884    use super::{
885        AlbersEqualAreaGrid, GridDefinition, LambertConformalGrid, LatLonGrid, MercatorGrid,
886        PolarStereographicGrid,
887    };
888    use crate::binary::encode_wmo_i32;
889
890    #[test]
891    fn reports_latlon_shape() {
892        let grid = GridDefinition::LatLon(LatLonGrid {
893            ni: 3,
894            nj: 2,
895            lat_first: 50_000_000,
896            lon_first: -120_000_000,
897            lat_last: 49_000_000,
898            lon_last: -118_000_000,
899            di: 1_000_000,
900            dj: 1_000_000,
901            scanning_mode: 0,
902        });
903
904        assert_eq!(grid.shape(), (3, 2));
905        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
906        assert_eq!(grid.template_number(), 0);
907        assert!(grid.as_lat_lon().is_some());
908        assert!(grid.as_mercator().is_none());
909        assert!(grid.as_polar_stereographic().is_none());
910        assert!(grid.as_lambert_conformal().is_none());
911        assert!(grid.as_albers_equal_area().is_none());
912        assert_eq!(grid.unsupported_template(), None);
913        match grid {
914            GridDefinition::LatLon(ref latlon) => {
915                assert_eq!(latlon.longitudes().unwrap(), vec![-120.0, -119.0, -118.0]);
916                assert_eq!(latlon.latitudes().unwrap(), vec![50.0, 49.0]);
917            }
918            other => panic!("expected lat/lon grid, got {other:?}"),
919        }
920    }
921
922    #[test]
923    fn parses_mercator_template() {
924        let section = build_mercator_section(0);
925        let grid = GridDefinition::parse(&section).unwrap();
926
927        assert_eq!(grid.shape(), (3, 2));
928        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
929        assert_eq!(grid.num_points(), 6);
930        assert_eq!(grid.template_number(), 10);
931        assert!(grid.as_lat_lon().is_none());
932        assert!(grid.as_mercator().is_some());
933        assert!(grid.as_lambert_conformal().is_none());
934        assert_eq!(grid.unsupported_template(), None);
935        match grid {
936            GridDefinition::Mercator(mercator) => {
937                assert_eq!(mercator.number_of_points, 6);
938                assert_eq!(mercator.shape_of_earth, 6);
939                assert_eq!(mercator.ni, 3);
940                assert_eq!(mercator.nj, 2);
941                assert_eq!(mercator.lat_first, -20_000_000);
942                assert_eq!(mercator.lon_first, -100_000_000);
943                assert_eq!(mercator.resolution_and_component_flags, 0x08);
944                assert_eq!(mercator.lat_d, 0);
945                assert_eq!(mercator.lat_last, 20_000_000);
946                assert_eq!(mercator.lon_last, -98_000_000);
947                assert_eq!(mercator.scanning_mode, 0);
948                assert_eq!(mercator.orientation_of_grid, 0);
949                assert_eq!(mercator.di, 1_000_000);
950                assert_eq!(mercator.dj, 2_000_000);
951            }
952            other => panic!("expected Mercator grid, got {other:?}"),
953        }
954    }
955
956    #[test]
957    fn parses_polar_stereographic_template() {
958        let section = build_polar_stereographic_section(0);
959        let grid = GridDefinition::parse(&section).unwrap();
960
961        assert_eq!(grid.shape(), (3, 2));
962        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
963        assert_eq!(grid.num_points(), 6);
964        assert_eq!(grid.template_number(), 20);
965        assert!(grid.as_lat_lon().is_none());
966        assert!(grid.as_mercator().is_none());
967        assert!(grid.as_polar_stereographic().is_some());
968        assert!(grid.as_lambert_conformal().is_none());
969        assert!(grid.as_albers_equal_area().is_none());
970        assert_eq!(grid.unsupported_template(), None);
971        match grid {
972            GridDefinition::PolarStereographic(polar) => {
973                assert_eq!(polar.number_of_points, 6);
974                assert_eq!(polar.shape_of_earth, 6);
975                assert_eq!(polar.nx, 3);
976                assert_eq!(polar.ny, 2);
977                assert_eq!(polar.lat_first, 41_612_949);
978                assert_eq!(polar.lon_first, 185_117_126);
979                assert_eq!(polar.resolution_and_component_flags, 0x08);
980                assert_eq!(polar.lat_d, 60_000_000);
981                assert_eq!(polar.lon_v, 225_000_000);
982                assert_eq!(polar.dx, 3_000_000);
983                assert_eq!(polar.dy, 3_000_000);
984                assert_eq!(polar.projection_center_flag, 0);
985                assert_eq!(polar.scanning_mode, 0);
986            }
987            other => panic!("expected polar stereographic grid, got {other:?}"),
988        }
989    }
990
991    #[test]
992    fn parses_albers_equal_area_template() {
993        let section = build_albers_section();
994        let grid = GridDefinition::parse(&section).unwrap();
995
996        assert_eq!(grid.shape(), (3, 2));
997        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
998        assert_eq!(grid.num_points(), 6);
999        assert_eq!(grid.template_number(), 31);
1000        assert!(grid.as_lat_lon().is_none());
1001        assert!(grid.as_albers_equal_area().is_some());
1002        assert_eq!(grid.unsupported_template(), None);
1003        match grid {
1004            GridDefinition::AlbersEqualArea(albers) => {
1005                assert_eq!(albers.number_of_points, 6);
1006                assert_eq!(albers.shape_of_earth, 6);
1007                assert_eq!(albers.nx, 3);
1008                assert_eq!(albers.ny, 2);
1009                assert_eq!(albers.lat_first, 23_000_000);
1010                assert_eq!(albers.lon_first, 240_000_000);
1011                assert_eq!(albers.resolution_and_component_flags, 0x08);
1012                assert_eq!(albers.lat_d, 25_000_000);
1013                assert_eq!(albers.lon_v, 265_000_000);
1014                assert_eq!(albers.dx, 4_000_000);
1015                assert_eq!(albers.dy, 5_000_000);
1016                assert_eq!(albers.projection_center_flag, 0);
1017                assert_eq!(albers.scanning_mode, 0);
1018                assert_eq!(albers.latin1, 29_500_000);
1019                assert_eq!(albers.latin2, 45_500_000);
1020                assert_eq!(albers.lat_southern_pole, -90_000_000);
1021                assert_eq!(albers.lon_southern_pole, 0);
1022            }
1023            other => panic!("expected Albers equal-area grid, got {other:?}"),
1024        }
1025    }
1026
1027    #[test]
1028    fn parses_lambert_conformal_template() {
1029        let section = build_lambert_section();
1030        let grid = GridDefinition::parse(&section).unwrap();
1031
1032        assert_eq!(grid.shape(), (3, 2));
1033        assert_eq!(grid.ndarray_shape(), vec![2, 3]);
1034        assert_eq!(grid.num_points(), 6);
1035        assert_eq!(grid.template_number(), 30);
1036        assert!(grid.as_lat_lon().is_none());
1037        assert!(grid.as_lambert_conformal().is_some());
1038        assert!(grid.as_albers_equal_area().is_none());
1039        assert_eq!(grid.unsupported_template(), None);
1040        match grid {
1041            GridDefinition::LambertConformal(lambert) => {
1042                assert_eq!(lambert.number_of_points, 6);
1043                assert_eq!(lambert.shape_of_earth, 1);
1044                assert_eq!(lambert.scaled_value_radius, 6_371_200);
1045                assert_eq!(lambert.nx, 3);
1046                assert_eq!(lambert.ny, 2);
1047                assert_eq!(lambert.lat_first, 12_190_000);
1048                assert_eq!(lambert.lon_first, 226_541_000);
1049                assert_eq!(lambert.resolution_and_component_flags, 0x08);
1050                assert_eq!(lambert.lat_d, 25_000_000);
1051                assert_eq!(lambert.lon_v, 265_000_000);
1052                assert_eq!(lambert.dx, 2_539_703);
1053                assert_eq!(lambert.dy, 2_539_703);
1054                assert_eq!(lambert.projection_center_flag, 0);
1055                assert_eq!(lambert.scanning_mode, 0);
1056                assert_eq!(lambert.latin1, 25_000_000);
1057                assert_eq!(lambert.latin2, 25_000_000);
1058                assert_eq!(lambert.lat_southern_pole, -90_000_000);
1059                assert_eq!(lambert.lon_southern_pole, 0);
1060            }
1061            other => panic!("expected Lambert conformal grid, got {other:?}"),
1062        }
1063    }
1064
1065    #[test]
1066    fn reports_unsupported_template_helpers() {
1067        let grid = GridDefinition::Unsupported(3_276);
1068
1069        assert_eq!(grid.template_number(), 3_276);
1070        assert!(grid.as_lat_lon().is_none());
1071        assert!(grid.as_mercator().is_none());
1072        assert!(grid.as_polar_stereographic().is_none());
1073        assert!(grid.as_lambert_conformal().is_none());
1074        assert!(grid.as_albers_equal_area().is_none());
1075        assert_eq!(grid.unsupported_template(), Some(3_276));
1076    }
1077
1078    #[test]
1079    fn normalizes_alternating_row_scan() {
1080        let grid = LatLonGrid {
1081            ni: 3,
1082            nj: 2,
1083            lat_first: 0,
1084            lon_first: 0,
1085            lat_last: 0,
1086            lon_last: 0,
1087            di: 1,
1088            dj: 1,
1089            scanning_mode: 0b0001_0000,
1090        };
1091
1092        let ordered = grid
1093            .reorder_for_ndarray(vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0])
1094            .unwrap();
1095        assert_eq!(ordered, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1096    }
1097
1098    #[test]
1099    fn converts_ndarray_order_to_alternating_scan_order() {
1100        let grid = LatLonGrid {
1101            ni: 3,
1102            nj: 2,
1103            lat_first: 0,
1104            lon_first: 0,
1105            lat_last: 0,
1106            lon_last: 0,
1107            di: 1,
1108            dj: 1,
1109            scanning_mode: 0b0001_0000,
1110        };
1111
1112        let scan_order = grid
1113            .reorder_ndarray_to_grib_scan(vec![1, 2, 3, 4, 5, 6])
1114            .unwrap();
1115        assert_eq!(scan_order, vec![1, 2, 3, 6, 5, 4]);
1116    }
1117
1118    #[test]
1119    fn normalizes_polar_stereographic_alternating_row_scan() {
1120        let grid = PolarStereographicGrid {
1121            number_of_points: 6,
1122            shape_of_earth: 6,
1123            scale_factor_radius: 0,
1124            scaled_value_radius: 0,
1125            scale_factor_major_axis: 0,
1126            scaled_value_major_axis: 0,
1127            scale_factor_minor_axis: 0,
1128            scaled_value_minor_axis: 0,
1129            nx: 3,
1130            ny: 2,
1131            lat_first: 0,
1132            lon_first: 0,
1133            resolution_and_component_flags: 0,
1134            lat_d: 0,
1135            lon_v: 0,
1136            dx: 1,
1137            dy: 1,
1138            projection_center_flag: 0,
1139            scanning_mode: 0b0001_0000,
1140        };
1141
1142        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1143        grid.reorder_for_ndarray_in_place(&mut values).unwrap();
1144        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1145    }
1146
1147    #[test]
1148    fn normalizes_mercator_alternating_row_scan() {
1149        let grid = MercatorGrid {
1150            number_of_points: 6,
1151            shape_of_earth: 6,
1152            scale_factor_radius: 0,
1153            scaled_value_radius: 0,
1154            scale_factor_major_axis: 0,
1155            scaled_value_major_axis: 0,
1156            scale_factor_minor_axis: 0,
1157            scaled_value_minor_axis: 0,
1158            ni: 3,
1159            nj: 2,
1160            lat_first: 0,
1161            lon_first: 0,
1162            resolution_and_component_flags: 0,
1163            lat_d: 0,
1164            lat_last: 0,
1165            lon_last: 0,
1166            scanning_mode: 0b0001_0000,
1167            orientation_of_grid: 0,
1168            di: 1,
1169            dj: 1,
1170        };
1171
1172        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1173        grid.reorder_for_ndarray_in_place(&mut values).unwrap();
1174        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1175    }
1176
1177    #[test]
1178    fn normalizes_lambert_alternating_row_scan() {
1179        let grid = LambertConformalGrid {
1180            number_of_points: 6,
1181            shape_of_earth: 1,
1182            scale_factor_radius: 0,
1183            scaled_value_radius: 6_371_200,
1184            scale_factor_major_axis: 0,
1185            scaled_value_major_axis: 0,
1186            scale_factor_minor_axis: 0,
1187            scaled_value_minor_axis: 0,
1188            nx: 3,
1189            ny: 2,
1190            lat_first: 0,
1191            lon_first: 0,
1192            resolution_and_component_flags: 0,
1193            lat_d: 0,
1194            lon_v: 0,
1195            dx: 1,
1196            dy: 1,
1197            projection_center_flag: 0,
1198            scanning_mode: 0b0001_0000,
1199            latin1: 0,
1200            latin2: 0,
1201            lat_southern_pole: 0,
1202            lon_southern_pole: 0,
1203        };
1204
1205        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1206        grid.reorder_for_ndarray_in_place(&mut values).unwrap();
1207        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1208    }
1209
1210    #[test]
1211    fn normalizes_albers_alternating_row_scan() {
1212        let grid = AlbersEqualAreaGrid {
1213            number_of_points: 6,
1214            shape_of_earth: 6,
1215            scale_factor_radius: 0,
1216            scaled_value_radius: 0,
1217            scale_factor_major_axis: 0,
1218            scaled_value_major_axis: 0,
1219            scale_factor_minor_axis: 0,
1220            scaled_value_minor_axis: 0,
1221            nx: 3,
1222            ny: 2,
1223            lat_first: 0,
1224            lon_first: 0,
1225            resolution_and_component_flags: 0,
1226            lat_d: 0,
1227            lon_v: 0,
1228            dx: 1,
1229            dy: 1,
1230            projection_center_flag: 0,
1231            scanning_mode: 0b0001_0000,
1232            latin1: 0,
1233            latin2: 0,
1234            lat_southern_pole: 0,
1235            lon_southern_pole: 0,
1236        };
1237
1238        let mut values = vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1239        grid.reorder_for_ndarray_in_place(&mut values).unwrap();
1240        assert_eq!(values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1241    }
1242
1243    #[test]
1244    fn generates_projected_coordinate_offsets() {
1245        let mercator = GridDefinition::Mercator(MercatorGrid {
1246            number_of_points: 6,
1247            shape_of_earth: 6,
1248            scale_factor_radius: 0,
1249            scaled_value_radius: 0,
1250            scale_factor_major_axis: 0,
1251            scaled_value_major_axis: 0,
1252            scale_factor_minor_axis: 0,
1253            scaled_value_minor_axis: 0,
1254            ni: 3,
1255            nj: 2,
1256            lat_first: 0,
1257            lon_first: 0,
1258            resolution_and_component_flags: 0,
1259            lat_d: 0,
1260            lat_last: 0,
1261            lon_last: 0,
1262            scanning_mode: 0,
1263            orientation_of_grid: 0,
1264            di: 1_000_000,
1265            dj: 2_000_000,
1266        });
1267        assert_eq!(
1268            mercator.projected_x_coordinates().unwrap().unwrap(),
1269            vec![0.0, 1_000.0, 2_000.0]
1270        );
1271        assert_eq!(
1272            mercator.projected_y_coordinates().unwrap().unwrap(),
1273            vec![-0.0, -2_000.0]
1274        );
1275
1276        let polar = GridDefinition::PolarStereographic(PolarStereographicGrid {
1277            number_of_points: 6,
1278            shape_of_earth: 6,
1279            scale_factor_radius: 0,
1280            scaled_value_radius: 0,
1281            scale_factor_major_axis: 0,
1282            scaled_value_major_axis: 0,
1283            scale_factor_minor_axis: 0,
1284            scaled_value_minor_axis: 0,
1285            nx: 3,
1286            ny: 2,
1287            lat_first: 0,
1288            lon_first: 0,
1289            resolution_and_component_flags: 0,
1290            lat_d: 0,
1291            lon_v: 0,
1292            dx: 3_000_000,
1293            dy: 6_000_000,
1294            projection_center_flag: 0,
1295            scanning_mode: 0,
1296        });
1297        assert_eq!(
1298            polar.projected_x_coordinates().unwrap().unwrap(),
1299            vec![0.0, 3_000.0, 6_000.0]
1300        );
1301        assert_eq!(
1302            polar.projected_y_coordinates().unwrap().unwrap(),
1303            vec![-0.0, -6_000.0]
1304        );
1305
1306        let lambert = GridDefinition::LambertConformal(LambertConformalGrid {
1307            number_of_points: 6,
1308            shape_of_earth: 1,
1309            scale_factor_radius: 0,
1310            scaled_value_radius: 6_371_200,
1311            scale_factor_major_axis: 0,
1312            scaled_value_major_axis: 0,
1313            scale_factor_minor_axis: 0,
1314            scaled_value_minor_axis: 0,
1315            nx: 3,
1316            ny: 2,
1317            lat_first: 0,
1318            lon_first: 0,
1319            resolution_and_component_flags: 0,
1320            lat_d: 0,
1321            lon_v: 0,
1322            dx: 2_539_703,
1323            dy: 2_539_703,
1324            projection_center_flag: 0,
1325            scanning_mode: 0b1100_0000,
1326            latin1: 0,
1327            latin2: 0,
1328            lat_southern_pole: 0,
1329            lon_southern_pole: 0,
1330        });
1331        assert_eq!(
1332            lambert.projected_x_coordinates().unwrap().unwrap(),
1333            vec![-0.0, -2_539.703, -5_079.406]
1334        );
1335        assert_eq!(
1336            lambert.projected_y_coordinates().unwrap().unwrap(),
1337            vec![0.0, 2_539.703]
1338        );
1339
1340        let albers = GridDefinition::AlbersEqualArea(AlbersEqualAreaGrid {
1341            number_of_points: 6,
1342            shape_of_earth: 6,
1343            scale_factor_radius: 0,
1344            scaled_value_radius: 0,
1345            scale_factor_major_axis: 0,
1346            scaled_value_major_axis: 0,
1347            scale_factor_minor_axis: 0,
1348            scaled_value_minor_axis: 0,
1349            nx: 3,
1350            ny: 2,
1351            lat_first: 0,
1352            lon_first: 0,
1353            resolution_and_component_flags: 0,
1354            lat_d: 0,
1355            lon_v: 0,
1356            dx: 4_000_000,
1357            dy: 5_000_000,
1358            projection_center_flag: 0,
1359            scanning_mode: 0b1100_0000,
1360            latin1: 0,
1361            latin2: 0,
1362            lat_southern_pole: 0,
1363            lon_southern_pole: 0,
1364        });
1365        assert_eq!(
1366            albers.projected_x_coordinates().unwrap().unwrap(),
1367            vec![-0.0, -4_000.0, -8_000.0]
1368        );
1369        assert_eq!(
1370            albers.projected_y_coordinates().unwrap().unwrap(),
1371            vec![0.0, 5_000.0]
1372        );
1373    }
1374
1375    #[test]
1376    fn preserves_non_alternating_scan_modes_in_current_reader_order() {
1377        for scanning_mode in [0b0000_0000, 0b1000_0000, 0b0100_0000, 0b1100_0000] {
1378            let grid = LatLonGrid {
1379                ni: 3,
1380                nj: 2,
1381                lat_first: 0,
1382                lon_first: 0,
1383                lat_last: 0,
1384                lon_last: 0,
1385                di: 1,
1386                dj: 1,
1387                scanning_mode,
1388            };
1389
1390            let values = vec![1, 2, 3, 4, 5, 6];
1391            assert_eq!(
1392                grid.reorder_grib_scan_to_ndarray(values.clone()).unwrap(),
1393                values
1394            );
1395            assert_eq!(
1396                grid.reorder_ndarray_to_grib_scan(values.clone()).unwrap(),
1397                values
1398            );
1399        }
1400    }
1401
1402    #[test]
1403    fn rejects_j_consecutive_scan_order() {
1404        let grid = LatLonGrid {
1405            ni: 3,
1406            nj: 2,
1407            lat_first: 0,
1408            lon_first: 0,
1409            lat_last: 0,
1410            lon_last: 0,
1411            di: 1,
1412            dj: 1,
1413            scanning_mode: 0b0010_0000,
1414        };
1415
1416        let err = grid
1417            .reorder_ndarray_to_grib_scan(vec![1, 2, 3, 4, 5, 6])
1418            .unwrap_err();
1419        assert!(matches!(
1420            err,
1421            crate::Error::UnsupportedScanningMode(0b0010_0000)
1422        ));
1423    }
1424
1425    fn build_polar_stereographic_section(scanning_mode: u8) -> Vec<u8> {
1426        let mut section = vec![0u8; 65];
1427        section[..4].copy_from_slice(&65u32.to_be_bytes());
1428        section[4] = 3;
1429        section[6..10].copy_from_slice(&6u32.to_be_bytes());
1430        section[12..14].copy_from_slice(&20u16.to_be_bytes());
1431        section[14] = 6;
1432        section[30..34].copy_from_slice(&3u32.to_be_bytes());
1433        section[34..38].copy_from_slice(&2u32.to_be_bytes());
1434        section[38..42].copy_from_slice(&encode_wmo_i32(41_612_949).unwrap());
1435        section[42..46].copy_from_slice(&185_117_126u32.to_be_bytes());
1436        section[46] = 0x08;
1437        section[47..51].copy_from_slice(&encode_wmo_i32(60_000_000).unwrap());
1438        section[51..55].copy_from_slice(&225_000_000u32.to_be_bytes());
1439        section[55..59].copy_from_slice(&3_000_000u32.to_be_bytes());
1440        section[59..63].copy_from_slice(&3_000_000u32.to_be_bytes());
1441        section[64] = scanning_mode;
1442        section
1443    }
1444
1445    fn build_mercator_section(scanning_mode: u8) -> Vec<u8> {
1446        let mut section = vec![0u8; 72];
1447        section[..4].copy_from_slice(&72u32.to_be_bytes());
1448        section[4] = 3;
1449        section[6..10].copy_from_slice(&6u32.to_be_bytes());
1450        section[12..14].copy_from_slice(&10u16.to_be_bytes());
1451        section[14] = 6;
1452        section[30..34].copy_from_slice(&3u32.to_be_bytes());
1453        section[34..38].copy_from_slice(&2u32.to_be_bytes());
1454        section[38..42].copy_from_slice(&encode_wmo_i32(-20_000_000).unwrap());
1455        section[42..46].copy_from_slice(&encode_wmo_i32(-100_000_000).unwrap());
1456        section[46] = 0x08;
1457        section[47..51].copy_from_slice(&encode_wmo_i32(0).unwrap());
1458        section[51..55].copy_from_slice(&encode_wmo_i32(20_000_000).unwrap());
1459        section[55..59].copy_from_slice(&encode_wmo_i32(-98_000_000).unwrap());
1460        section[59] = scanning_mode;
1461        section[64..68].copy_from_slice(&1_000_000u32.to_be_bytes());
1462        section[68..72].copy_from_slice(&2_000_000u32.to_be_bytes());
1463        section
1464    }
1465
1466    fn build_lambert_section() -> Vec<u8> {
1467        let mut section = vec![0u8; 81];
1468        section[..4].copy_from_slice(&81u32.to_be_bytes());
1469        section[4] = 3;
1470        section[6..10].copy_from_slice(&6u32.to_be_bytes());
1471        section[12..14].copy_from_slice(&30u16.to_be_bytes());
1472        section[14] = 1;
1473        section[16..20].copy_from_slice(&6_371_200u32.to_be_bytes());
1474        section[30..34].copy_from_slice(&3u32.to_be_bytes());
1475        section[34..38].copy_from_slice(&2u32.to_be_bytes());
1476        section[38..42].copy_from_slice(&encode_wmo_i32(12_190_000).unwrap());
1477        section[42..46].copy_from_slice(&226_541_000u32.to_be_bytes());
1478        section[46] = 0x08;
1479        section[47..51].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
1480        section[51..55].copy_from_slice(&265_000_000u32.to_be_bytes());
1481        section[55..59].copy_from_slice(&2_539_703u32.to_be_bytes());
1482        section[59..63].copy_from_slice(&2_539_703u32.to_be_bytes());
1483        section[65..69].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
1484        section[69..73].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
1485        section[73..77].copy_from_slice(&encode_wmo_i32(-90_000_000).unwrap());
1486        section
1487    }
1488
1489    fn build_albers_section() -> Vec<u8> {
1490        let mut section = vec![0u8; 81];
1491        section[..4].copy_from_slice(&81u32.to_be_bytes());
1492        section[4] = 3;
1493        section[6..10].copy_from_slice(&6u32.to_be_bytes());
1494        section[12..14].copy_from_slice(&31u16.to_be_bytes());
1495        section[14] = 6;
1496        section[30..34].copy_from_slice(&3u32.to_be_bytes());
1497        section[34..38].copy_from_slice(&2u32.to_be_bytes());
1498        section[38..42].copy_from_slice(&encode_wmo_i32(23_000_000).unwrap());
1499        section[42..46].copy_from_slice(&240_000_000u32.to_be_bytes());
1500        section[46] = 0x08;
1501        section[47..51].copy_from_slice(&encode_wmo_i32(25_000_000).unwrap());
1502        section[51..55].copy_from_slice(&265_000_000u32.to_be_bytes());
1503        section[55..59].copy_from_slice(&4_000_000u32.to_be_bytes());
1504        section[59..63].copy_from_slice(&5_000_000u32.to_be_bytes());
1505        section[65..69].copy_from_slice(&encode_wmo_i32(29_500_000).unwrap());
1506        section[69..73].copy_from_slice(&encode_wmo_i32(45_500_000).unwrap());
1507        section[73..77].copy_from_slice(&encode_wmo_i32(-90_000_000).unwrap());
1508        section
1509    }
1510}