1use crate::error::{Error, Result};
4use crate::util::grib_i32;
5
6#[derive(Debug, Clone, PartialEq)]
8pub enum GridDefinition {
9 LatLon(LatLonGrid),
11 Unsupported(u16),
13}
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct LatLonGrid {
18 pub ni: u32,
19 pub nj: u32,
20 pub lat_first: i32,
21 pub lon_first: i32,
22 pub lat_last: i32,
23 pub lon_last: i32,
24 pub di: u32,
25 pub dj: u32,
26 pub scanning_mode: u8,
27}
28
29impl GridDefinition {
30 pub fn shape(&self) -> (usize, usize) {
31 match self {
32 Self::LatLon(g) => (g.ni as usize, g.nj as usize),
33 Self::Unsupported(_) => (0, 0),
34 }
35 }
36
37 pub fn ndarray_shape(&self) -> Vec<usize> {
38 let (ni, nj) = self.shape();
39 match self {
40 Self::LatLon(_) if ni > 0 && nj > 0 => vec![nj, ni],
41 _ => Vec::new(),
42 }
43 }
44
45 pub fn num_points(&self) -> usize {
46 let (ni, nj) = self.shape();
47 ni.saturating_mul(nj)
48 }
49
50 pub fn parse(section_bytes: &[u8]) -> Result<Self> {
51 if section_bytes.len() < 14 {
52 return Err(Error::InvalidSection {
53 section: 3,
54 reason: format!("expected at least 14 bytes, got {}", section_bytes.len()),
55 });
56 }
57 if section_bytes[4] != 3 {
58 return Err(Error::InvalidSection {
59 section: section_bytes[4],
60 reason: "not a grid definition section".into(),
61 });
62 }
63
64 let template = u16::from_be_bytes(section_bytes[12..14].try_into().unwrap());
65 match template {
66 0 => parse_latlon(section_bytes),
67 _ => Ok(Self::Unsupported(template)),
68 }
69 }
70}
71
72impl LatLonGrid {
73 pub fn longitudes(&self) -> Vec<f64> {
74 let step = self.di as f64 / 1_000_000.0;
75 let signed_step = if self.i_scans_positive() { step } else { -step };
76 let start = self.lon_first as f64 / 1_000_000.0;
77 (0..self.ni)
78 .map(|index| start + signed_step * index as f64)
79 .collect()
80 }
81
82 pub fn latitudes(&self) -> Vec<f64> {
83 let step = self.dj as f64 / 1_000_000.0;
84 let signed_step = if self.j_scans_positive() { step } else { -step };
85 let start = self.lat_first as f64 / 1_000_000.0;
86 (0..self.nj)
87 .map(|index| start + signed_step * index as f64)
88 .collect()
89 }
90
91 pub fn reorder_for_ndarray(&self, values: Vec<f64>) -> Result<Vec<f64>> {
92 let ni = self.ni as usize;
93 let nj = self.nj as usize;
94 if values.len() != ni * nj {
95 return Err(Error::DataLengthMismatch {
96 expected: ni * nj,
97 actual: values.len(),
98 });
99 }
100
101 if !self.i_points_are_consecutive() {
102 return Err(Error::UnsupportedScanningMode(self.scanning_mode));
103 }
104
105 let mut ordered = values;
106 if self.adjacent_rows_alternate_direction() {
107 for row in 0..nj {
108 let reverse = if self.i_scans_positive() {
109 row % 2 == 1
110 } else {
111 row % 2 == 0
112 };
113 if reverse {
114 ordered[row * ni..(row + 1) * ni].reverse();
115 }
116 }
117 }
118
119 Ok(ordered)
120 }
121
122 fn i_scans_positive(&self) -> bool {
123 self.scanning_mode & 0b1000_0000 == 0
124 }
125
126 fn j_scans_positive(&self) -> bool {
127 self.scanning_mode & 0b0100_0000 != 0
128 }
129
130 fn i_points_are_consecutive(&self) -> bool {
131 self.scanning_mode & 0b0010_0000 == 0
132 }
133
134 fn adjacent_rows_alternate_direction(&self) -> bool {
135 self.scanning_mode & 0b0001_0000 != 0
136 }
137}
138
139fn parse_latlon(data: &[u8]) -> Result<GridDefinition> {
140 if data.len() < 72 {
141 return Err(Error::InvalidSection {
142 section: 3,
143 reason: format!("template 3.0 requires 72 bytes, got {}", data.len()),
144 });
145 }
146
147 let ni = u32::from_be_bytes(data[30..34].try_into().unwrap());
148 let nj = u32::from_be_bytes(data[34..38].try_into().unwrap());
149 let lat_first = grib_i32(&data[46..50]).unwrap();
150 let lon_first = grib_i32(&data[50..54]).unwrap();
151 let lat_last = grib_i32(&data[55..59]).unwrap();
152 let lon_last = grib_i32(&data[59..63]).unwrap();
153 let di = u32::from_be_bytes(data[63..67].try_into().unwrap());
154 let dj = u32::from_be_bytes(data[67..71].try_into().unwrap());
155 let scanning_mode = data[71];
156
157 Ok(GridDefinition::LatLon(LatLonGrid {
158 ni,
159 nj,
160 lat_first,
161 lon_first,
162 lat_last,
163 lon_last,
164 di,
165 dj,
166 scanning_mode,
167 }))
168}
169
170#[cfg(test)]
171mod tests {
172 use super::{GridDefinition, LatLonGrid};
173
174 #[test]
175 fn reports_latlon_shape() {
176 let grid = GridDefinition::LatLon(LatLonGrid {
177 ni: 3,
178 nj: 2,
179 lat_first: 50_000_000,
180 lon_first: -120_000_000,
181 lat_last: 49_000_000,
182 lon_last: -118_000_000,
183 di: 1_000_000,
184 dj: 1_000_000,
185 scanning_mode: 0,
186 });
187
188 assert_eq!(grid.shape(), (3, 2));
189 assert_eq!(grid.ndarray_shape(), vec![2, 3]);
190 match grid {
191 GridDefinition::LatLon(ref latlon) => {
192 assert_eq!(latlon.longitudes(), vec![-120.0, -119.0, -118.0]);
193 assert_eq!(latlon.latitudes(), vec![50.0, 49.0]);
194 }
195 GridDefinition::Unsupported(_) => panic!("expected lat/lon grid"),
196 }
197 }
198
199 #[test]
200 fn normalizes_alternating_row_scan() {
201 let grid = LatLonGrid {
202 ni: 3,
203 nj: 2,
204 lat_first: 0,
205 lon_first: 0,
206 lat_last: 0,
207 lon_last: 0,
208 di: 1,
209 dj: 1,
210 scanning_mode: 0b0001_0000,
211 };
212
213 let ordered = grid
214 .reorder_for_ndarray(vec![1.0, 2.0, 3.0, 6.0, 5.0, 4.0])
215 .unwrap();
216 assert_eq!(ordered, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
217 }
218}