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