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