rust_grib_decoder/
lib.rs

1//! rust-grib-decoder
2//!
3//! Small helper library extracted from atmosGUI for decoding GRIB2 CCSDS/AEC (template 5.0=42) payloads
4//! and extracting Section 7 payloads grouped by GRIB message.
5
6use std::path::Path;
7
8/// Message-level summary for diagnostics and matching
9#[derive(Debug, Clone)]
10pub struct MessageSummary {
11    pub message_index: usize,
12    pub key: String,
13    pub forecast_time: Option<i32>,
14    pub repr_template: Option<u16>,
15    pub payload_len: usize,
16    pub decode_ok: bool,
17    pub decoded_samples_len: Option<usize>,
18}
19
20/// File-level summary
21#[derive(Debug, Clone)]
22pub struct FileSummary {
23    pub path: std::path::PathBuf,
24    pub n_messages: usize,
25    pub messages: Vec<MessageSummary>,
26}
27
28/// Match reason when a message matches a parameter alias
29#[derive(Debug, Clone, Copy)]
30pub enum MatchReason {
31    Token,
32    NormParam,
33    Section4,
34}
35
36// Export parameter alias mapping for higher-level consumers (CLI/UI).
37pub mod param_alias;
38
39// Helper: extract tag value lines from a description-like string
40#[allow(dead_code)]
41fn extract_tag_value_case_insensitive(desc: &str, tags: &[&str]) -> Option<String> {
42    for line in desc.lines() {
43        let s = line.trim();
44        for tag in tags {
45            let t = tag.to_lowercase();
46            let key = t.clone() + ":";
47            let key_eq = t.clone() + "=";
48            let s_l = s.to_lowercase();
49            if s_l.starts_with(&key) || s_l.starts_with(&key_eq) {
50                if let Some(idx) = s.find(':') {
51                    let v = s[idx + 1..].trim();
52                    return Some(v.to_string());
53                }
54                if let Some(idx) = s.find('=') {
55                    let v = s[idx + 1..].trim();
56                    return Some(v.to_string());
57                }
58            }
59            // Also support 'tag value' format e.g. 'discipline 0'
60            if s_l.starts_with(&format!("{} ", t)) {
61                let v = s[t.len()..].trim();
62                return Some(v.to_string());
63            }
64            // Also try 'tag = value' with spaces
65            let patterns = [format!("{} =", t.clone()), format!("{}:", t)];
66            for pat in patterns.iter() {
67                if let Some(pos) = s_l.find(pat) {
68                    let v = s[pos + pat.len()..].trim();
69                    return Some(v.to_string());
70                }
71            }
72        }
73    }
74    None
75}
76
77/// Parse and return the first integer found in a string, used for forecast times and numeric tags.
78#[allow(dead_code)]
79fn parse_first_int_from_str(s: &str) -> Option<i32> {
80    let s = s.trim();
81    let mut num_str = String::new();
82    for c in s.chars() {
83        if c.is_ascii_digit() || (num_str.is_empty() && (c == '+' || c == '-')) {
84            num_str.push(c);
85        } else if !num_str.is_empty() {
86            break;
87        }
88    }
89    if num_str.is_empty() { None } else { num_str.parse::<i32>().ok() }
90}
91
92/// Read Section 7 payloads grouped by GRIB message (message order).
93/// Returns Vec of payload bytes found for each message in order (messages with no Section7 are skipped).
94pub fn read_grib2_section7_payloads_by_message(path: &Path) -> Result<Vec<Vec<u8>>, String> {
95    use std::fs;
96    use memchr::memmem;
97
98    let data = fs::read(path).map_err(|e| format!("打开文件失败:{e}"))?;
99    let mut out: Vec<Vec<u8>> = Vec::new();
100
101    let mut pos = 0usize;
102    while pos + 16 <= data.len() {
103        // find next "GRIB" marker at or after pos
104        if &data[pos..pos + 4] != b"GRIB" {
105            if let Some(next) = memmem::find(&data[pos + 1..], b"GRIB") {
106                pos = pos + 1 + next;
107            } else {
108                break;
109            }
110            continue;
111        }
112        // sanity check for GRIB2
113        if data[pos + 7] != 2 {
114            pos += 4;
115            continue;
116        }
117        // parse sections for this message
118        let mut off = pos + 16;
119        let mut payload_found = false;
120        loop {
121            if off + 5 > data.len() {
122                break; // truncated
123            }
124            let len = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
125            let sect_num = data[off + 4];
126            if len < 5 { break; }
127            let payload_len = len - 5;
128            let payload_off = off + 5;
129            if payload_off + payload_len > data.len() { break; }
130            if sect_num == 7 {
131                out.push(data[payload_off..payload_off + payload_len].to_vec());
132                payload_found = true;
133            }
134            off = payload_off + payload_len;
135            // check for end marker '7777'
136            if off + 4 <= data.len() && &data[off..off + 4] == b"7777" {
137                off += 4;
138                break;
139            }
140        }
141        // If this message had no section7, push empty vector to keep message order alignment
142        if !payload_found {
143            out.push(Vec::new());
144        }
145        // advance pos to end of this message (or move forward a bit)
146        pos = if off > pos { off } else { pos + 4 };
147    }
148
149    Ok(out)
150}
151
152/// Template42 params extracted from a Section5 payload
153pub struct Template42Params {
154    pub num_points: usize,
155    pub bits_per_sample: u8,
156    pub block_size: u32,
157    pub rsi: u32,
158    pub flags_mask: u32,
159    pub exp: i32,
160    pub dec: i32,
161    pub ref_val: f32,
162}
163
164/// Read Section5 template 5.0=42 parameters per GRIB message in file order.
165/// Returns a Vec where each entry corresponds to a GRIB message; `None` if the
166/// message does not contain a template 5.0=42 in its Section5 or if Section5 is missing.
167pub fn read_grib2_section5_template42_params_by_message(path: &Path) -> Result<Vec<Option<Template42Params>>, String> {
168    // Implement as a single compile-time cfg block so grib-dependent code is not compiled
169    // when the `grib-support` feature is disabled.
170    #[cfg(feature = "grib-support")]
171    {
172        use std::fs::File;
173        use std::io::BufReader;
174        use grib::def::grib2::DataRepresentationTemplate;
175
176        let f = File::open(path).map_err(|e| format!("open file failed: {e}"))?;
177        let r = BufReader::new(f);
178        let grib2 = grib::from_reader(r).map_err(|e| format!("GRIB parse failed: {e}"))?;
179
180        let mut out: Vec<Option<Template42Params>> = Vec::new();
181        for ((_msg_idx, _submsg_idx), submsg) in grib2.iter() {
182            match submsg.section5() {
183                Ok(s5) => match s5.payload.template {
184                    DataRepresentationTemplate::_5_42(t) => {
185                        let params = Template42Params {
186                            num_points: s5.payload.num_encoded_points as usize,
187                            bits_per_sample: t.simple.num_bits as u8,
188                            block_size: t.block_size as u32,
189                            rsi: t.ref_sample_interval as u32,
190                            flags_mask: t.mask as u32,
191                            exp: t.simple.exp as i32,
192                            dec: t.simple.dec as i32,
193                            ref_val: t.simple.ref_val as f32,
194                        };
195                        out.push(Some(params));
196                    }
197                    _ => out.push(None),
198                },
199                Err(_) => out.push(None),
200            }
201        }
202        Ok(out)
203    }
204    #[cfg(not(feature = "grib-support"))]
205    {                                    
206        use std::fs;
207
208        let data = fs::read(path).map_err(|e| format!("打开文件失败:{e}"))?;
209        let mut out: Vec<Option<Template42Params>> = Vec::new();
210
211        let mut pos = 0usize;
212        while pos + 16 <= data.len() {
213            if &data[pos..pos + 4] != b"GRIB" {
214                // find next GRIB
215                if let Some(next) = memchr::memmem::find(&data[pos + 1..], b"GRIB") {
216                    pos = pos + 1 + next;
217                } else {
218                    break;
219                }
220                continue;
221            }
222            if data[pos + 7] != 2 {
223                pos += 4;
224                continue;
225            }
226            // Walk sections until Section5 or end of message
227            let mut off = pos + 16;
228            let mut found_s5 = false;
229            let mut s5_params: Option<Template42Params> = None;
230            loop {
231                if off + 5 > data.len() { break; }
232                let len = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
233                let sect_num = data[off + 4];
234                if len < 5 { break; }
235                let payload_len = len - 5;
236                let payload_off = off + 5;
237                if payload_off + payload_len > data.len() { break; }
238                if sect_num == 5 {
239                    // Conservative full parse according to GRIB2 Section 5 layout:
240                    // Section 5 (Data Representation) payload layout (simplified):
241                    // - number of data points (4 bytes)
242                    // - data representation template number (2 bytes)
243                    // - then template-specific payload
244                    if payload_len >= 6 {
245                        let _num_points = u32::from_be_bytes([
246                            data[payload_off],
247                            data[payload_off + 1],
248                            data[payload_off + 2],
249                            data[payload_off + 3],
250                        ]) as usize;
251                        let tmpl_num = u16::from_be_bytes([data[payload_off + 4], data[payload_off + 5]]);
252                        if tmpl_num == 42 {
253                            // Parse template 5.0=42 (CCSDS/AEC) simple section layout (simple packing):
254                            // Parse defensively with bounds checks.
255                            let mut cursor = payload_off + 6;
256                            if cursor + 4 <= payload_off + payload_len {
257                                let num_encoded_points = u32::from_be_bytes([
258                                    data[cursor],
259                                    data[cursor + 1],
260                                    data[cursor + 2],
261                                    data[cursor + 3],
262                                ]) as usize;
263                                cursor += 4;
264                                // num_bits
265                                if cursor + 1 > payload_off + payload_len { break; }
266                                let num_bits = data[cursor] as u8; cursor += 1;
267                                // block_size
268                                if cursor + 4 > payload_off + payload_len { break; }
269                                let block_size = u32::from_be_bytes([data[cursor], data[cursor + 1], data[cursor + 2], data[cursor + 3]]); cursor += 4;
270                                // ref_sample_interval (rsi)
271                                if cursor + 4 > payload_off + payload_len { break; }
272                                let rsi = u32::from_be_bytes([data[cursor], data[cursor + 1], data[cursor + 2], data[cursor + 3]]); cursor += 4;
273                                // mask
274                                if cursor + 4 > payload_off + payload_len { break; }
275                                let mask = u32::from_be_bytes([data[cursor], data[cursor + 1], data[cursor + 2], data[cursor + 3]]); cursor += 4;
276                                // simple packing params: exp (i16), dec (i16), ref_val (f32)
277                                if cursor + 2 + 2 + 4 > payload_off + payload_len { break; }
278                                let exp = i16::from_be_bytes([data[cursor], data[cursor + 1]]) as i32; cursor += 2;
279                                let dec = i16::from_be_bytes([data[cursor], data[cursor + 1]]) as i32; cursor += 2;
280                                let ref_val = f32::from_bits(u32::from_be_bytes([data[cursor], data[cursor + 1], data[cursor + 2], data[cursor + 3]]));
281
282                                // Construct params
283                                s5_params = Some(Template42Params {
284                                    num_points: num_encoded_points,
285                                    bits_per_sample: num_bits,
286                                    block_size,
287                                    rsi,
288                                    flags_mask: mask,
289                                    exp,
290                                    dec,
291                                    ref_val,
292                                });
293                            }
294                        }
295                    }
296                    found_s5 = true;
297                }
298                off = payload_off + payload_len;
299                // check for end marker
300                if off + 4 <= data.len() && &data[off..off + 4] == b"7777" { off += 4; break; }
301            }
302            if found_s5 {
303                out.push(s5_params);
304            } else {
305                out.push(None);
306            }
307
308            // advance pos to end of this message
309            pos = if off > pos { off } else { pos + 4 };
310        }
311
312        return Ok(out);
313    }
314}
315
316/// Read Section4 (Product Definition Section) parameter identifiers per GRIB message in file order.
317/// Returns Vec where each entry is Some((discipline, category, number)) if Section4 present and fields可解析.
318pub fn read_grib2_section4_parameter_ids_by_message(path: &Path) -> Result<Vec<Option<(u8,u8,u8)>>, String> {
319    // Prefer using the `grib` crate when available for reliable extraction.
320    #[cfg(feature = "grib-support")]
321    {
322        use std::fs::File;
323        use std::io::BufReader;
324        use grib::from_reader;
325
326        let f = File::open(path).map_err(|e| format!("open file failed: {e}"))?;
327        let r = BufReader::new(f);
328        let grib2 = from_reader(r).map_err(|e| format!("GRIB parse failed: {e}"))?;
329        let mut out: Vec<Option<(u8,u8,u8)>> = Vec::new();
330        for ((_msg_idx, _submsg_idx), submsg) in grib2.iter() {
331            // Use submessage.describe() for Section4 extraction (some Section4 types may not implement dedicated accessors).
332            let desc = submsg.describe();
333            let discipline = extract_tag_value_case_insensitive(&desc, &["discipline"]).and_then(|s| s.parse::<u8>().ok());
334            let category = extract_tag_value_case_insensitive(&desc, &["parametercategory", "parameter category", "paramcategory", "category"]).and_then(|s| s.parse::<u8>().ok());
335            let number = extract_tag_value_case_insensitive(&desc, &["parameternumber", "parameter number", "paramnumber", "number"]).and_then(|s| s.parse::<u8>().ok());
336            if discipline.is_some() || category.is_some() || number.is_some() {
337                let d = discipline.unwrap_or(0);
338                let c = category.unwrap_or(0);
339                let n = number.unwrap_or(0);
340                out.push(Some((d,c,n)));
341            } else {
342                out.push(None);
343            }
344        }
345        return Ok(out);
346    }
347
348    // Fallback raw-byte scan: scan for Section 4 payload and try simple heuristic parse.
349    #[cfg(not(feature = "grib-support"))]
350    {
351        use std::fs;
352        let data = fs::read(path).map_err(|e| format!("打开文件失败:{e}"))?;
353        let mut out: Vec<Option<(u8,u8,u8)>> = Vec::new();
354        let mut pos = 0usize;
355        while pos + 16 <= data.len() {
356            if &data[pos..pos + 4] != b"GRIB" {
357                if let Some(next) = memchr::memmem::find(&data[pos + 1..], b"GRIB") {
358                    pos = pos + 1 + next;
359                } else { break; }
360                continue;
361            }
362            if data[pos + 7] != 2 {
363                pos += 4;
364                continue;
365            }
366            let mut off = pos + 16;
367            let mut found_s4: Option<(u8,u8,u8)> = None;
368            loop {
369                if off + 5 > data.len() { break; }
370                let len = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) as usize;
371                let sect_num = data[off + 4];
372                if len < 5 { break; }
373                let payload_len = len - 5;
374                let payload_off = off + 5;
375                if payload_off + payload_len > data.len() { break; }
376                if sect_num == 4 {
377                    // Heuristic: within Section4 payload, discipline is at payload[0], parameterCategory at payload[8], parameterNumber at payload[9]
378                    // These offsets are best-effort and may not be universally correct, but work for common PDS templates.
379                    if payload_len >= 10 {
380                        let d = data[payload_off];
381                        let c = data[payload_off + 8];
382                        let n = data[payload_off + 9];
383                        found_s4 = Some((d, c, n));
384                    }
385                }
386                off = payload_off + payload_len;
387                if off + 4 <= data.len() && &data[off..off+4] == b"7777" { off += 4; break; }
388            }
389            out.push(found_s4);
390            pos = if off > pos { off } else { pos + 4 };
391        }
392        return Ok(out);
393    }
394}
395
396/// Decode template 5.0=42 using rust-aec given a Section7 payload and template params
397pub fn decode_template42_rust_from_params_with_payload(
398    payload: &[u8],
399    width: usize,
400    height: usize,
401    num_points: usize,
402    bits_per_sample: u8,
403    block_size: u32,
404    rsi: u32,
405    flags_mask: u32,
406    exp: i32,
407    dec: i32,
408    ref_val: f32,
409) -> Result<Vec<f32>, String> {
410    use rust_aec::{flags_from_grib2_ccsds_flags, params::AecParams};
411
412    let expected_points = width.saturating_mul(height);
413    if expected_points != 0 && num_points != expected_points {
414        return Err(format!(
415            "template42(rust-aec) only supports non-bitmap regular grids: num_points={} grid={}x{}={}",
416            num_points, width, height, expected_points
417        ));
418    }
419
420    let ccsds_flags: u8 = (flags_mask & 0xff) as u8;
421    let flags = flags_from_grib2_ccsds_flags(ccsds_flags);
422    let params = AecParams::new(bits_per_sample, block_size, rsi, flags);
423    let decoded_bytes = rust_aec::decode(payload, params, num_points)
424        .map_err(|e| format!("rust-aec decode failed: {e}"))?;
425
426    let bits_u32 = bits_per_sample as u32;
427    let bytes_per_sample = ((bits_u32 + 7) / 8) as usize;
428    let msb = flags.contains(rust_aec::params::AecFlags::MSB);
429    let mask: u32 = if bits_u32 >= 32 { u32::MAX } else { (1u32 << bits_u32) - 1 };
430    let bscale = 2.0f32.powi(exp);
431    let dscale = 10.0f32.powi(-dec);
432
433    let mut values = Vec::with_capacity(num_points);
434    for i in 0..num_points {
435        let start = i * bytes_per_sample;
436        let sample_bytes = &decoded_bytes[start..start + bytes_per_sample];
437        let raw = if msb {
438            sample_bytes.iter().fold(0u32, |acc, &b| (acc << 8) | (b as u32))
439        } else {
440            sample_bytes
441                .iter()
442                .enumerate()
443                .fold(0u32, |acc, (j, &b)| acc | ((b as u32) << (j * 8)))
444        } & mask;
445        let v = (ref_val + (raw as f32) * bscale) * dscale;
446        values.push(v);
447    }
448
449    Ok(values)
450}
451
452/// Read first Section7 payload and decode template 5.0=42 using it (convenience wrapper)
453pub fn decode_template42_rust_from_params(
454    path: &Path,
455    width: usize,
456    height: usize,
457    num_points: usize,
458    bits_per_sample: u8,
459    block_size: u32,
460    rsi: u32,
461    flags_mask: u32,
462    exp: i32,
463    dec: i32,
464    ref_val: f32,
465) -> Result<Vec<f32>, String> {
466    // Convenience wrapper that reads the *first* Section 7 payload and decodes it.
467    let payload = read_first_grib2_section7_payload(path)?;
468    decode_template42_rust_from_params_with_payload(
469        &payload,
470        width,
471        height,
472        num_points,
473        bits_per_sample,
474        block_size,
475        rsi,
476        flags_mask,
477        exp,
478        dec,
479        ref_val,
480    )
481}
482
483/// Read first Section7 payload (returns payload of the first Section7 found in the file)
484pub fn read_first_grib2_section7_payload(path: &Path) -> Result<Vec<u8>, String> {
485    use std::fs::File;
486    use std::io::Read;
487
488    let mut f = File::open(path).map_err(|e| format!("open file failed: {e}"))?;
489
490    // Section 0 is 16 bytes.
491    let mut s0 = [0u8; 16];
492    f.read_exact(&mut s0).map_err(|e| format!("read GRIB2 section0 failed: {e}"))?;
493    if &s0[0..4] != b"GRIB" {
494        return Err("not a GRIB file".to_string());
495    }
496    if s0[7] != 2 {
497        return Err(format!("not GRIB2 (edition={})", s0[7]));
498    }
499
500    loop {
501        let mut header = [0u8; 5];
502        f.read_exact(&mut header).map_err(|e| format!("read section header failed: {e}"))?;
503        let len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
504        let sect_num = header[4];
505        if len < 5 {
506            return Err(format!("invalid section length: {len} (section {sect_num})"));
507        }
508        let payload_len = len - 5;
509        if sect_num == 7 {
510            let mut payload = vec![0u8; payload_len];
511            f.read_exact(&mut payload).map_err(|e| format!("read Section7 payload failed: {e}"))?;
512            return Ok(payload);
513        }
514        let mut skip = vec![0u8; payload_len];
515        f.read_exact(&mut skip).map_err(|e| format!("skip failed: {e}"))?;
516    }
517}
518
519/// Detect GRIB edition: returns 1 or 2
520pub fn detect_grib_edition(path: &Path) -> Result<u8, String> {
521    use std::fs::File;
522    use std::io::Read;
523
524    let mut f = File::open(path).map_err(|e| format!("open file failed: {e}"))?;
525    let mut s0 = [0u8; 16];
526    f.read_exact(&mut s0).map_err(|e| format!("read header failed: {e}"))?;
527    if &s0[0..4] != b"GRIB" {
528        return Err("not a GRIB file".to_string());
529    }
530    Ok(s0[7])
531}
532
533/// Return whether this crate was compiled with `grib-support` (enables using the `grib` crate paths)
534pub fn has_grib_support() -> bool {
535    cfg!(feature = "grib-support")
536}
537
538/// GRIB2 regular lat/lon grid metadata (template 3.0)
539#[derive(Debug, Clone)]
540pub struct Grib2LatLonGrid {
541    pub ni: usize,
542    pub nj: usize,
543    pub lat1_deg: f64,
544    pub lon1_deg: f64,
545    pub di_deg: f64,
546    pub dj_deg: f64,
547    pub scan_mode: u8,
548}
549
550/// Read the first GRIB2 regular lat/lon grid (template 3.0) from the file and return metadata.
551pub fn read_first_grib2_latlon_grid(path: &Path) -> Result<Grib2LatLonGrid, String> {
552    use std::fs::File;
553    use std::io::Read;
554
555    let mut f = File::open(path).map_err(|e| format!("open file failed: {e}"))?;
556
557    let mut s0 = [0u8; 16];
558    f.read_exact(&mut s0).map_err(|e| format!("read GRIB2 section0 failed: {e}"))?;
559    if &s0[0..4] != b"GRIB" {
560        return Err("not a GRIB file".to_string());
561    }
562    if s0[7] != 2 {
563        return Err(format!("not GRIB2 (edition={})", s0[7]));
564    }
565
566    loop {
567        let mut header = [0u8; 5];
568        f.read_exact(&mut header).map_err(|e| format!("read section header failed: {e}"))?;
569        let len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
570        let sect_num = header[4];
571        if len < 5 {
572            return Err(format!("invalid section length: {len} (section {sect_num})"));
573        }
574        let payload_len = len - 5;
575        let mut payload = vec![0u8; payload_len];
576        f.read_exact(&mut payload).map_err(|e| format!("read payload failed: {e}"))?;
577
578        if sect_num == 3 {
579            // Section 3: grid definition
580            if payload.len() < 9 {
581                return Err("GRIB2: Section3 too short".to_string());
582            }
583            let grid_tmpl = u16::from_be_bytes([payload[7], payload[8]]);
584            if grid_tmpl != 0 {
585                return Err(format!("GRIB2: only grid template 3.0 (regular lat/lon) supported; got 3.{grid_tmpl}"));
586            }
587            let t = &payload[9..];
588            if t.len() < 58 {
589                return Err(format!("GRIB2: grid template 3.0 too short (need >=58 bytes, got {})", t.len()));
590            }
591            const MISSING_U32: u32 = 0xFFFF_FFFF;
592            let ni_u32 = u32::from_be_bytes([t[16], t[17], t[18], t[19]]);
593            let nj_u32 = u32::from_be_bytes([t[20], t[21], t[22], t[23]]);
594            if ni_u32 == MISSING_U32 || nj_u32 == MISSING_U32 {
595                return Err("GRIB2: template 3.0 has missing Ni/Nj or irregular grid".to_string());
596            }
597            let ni = ni_u32 as usize;
598            let nj = nj_u32 as usize;
599            let basic_angle = u32::from_be_bytes([t[24], t[25], t[26], t[27]]);
600            let subdivisions = u32::from_be_bytes([t[28], t[29], t[30], t[31]]);
601            let unit_deg: f64 = if (basic_angle == 0 && subdivisions == 0) || (basic_angle == MISSING_U32 && subdivisions == MISSING_U32) {
602                1e-6
603            } else {
604                (basic_angle as f64) / (subdivisions as f64)
605            };
606            let mut lat1 = i32::from_be_bytes([t[32], t[33], t[34], t[35]]) as f64 * unit_deg;
607            let mut lon1 = i32::from_be_bytes([t[36], t[37], t[38], t[39]]) as f64 * unit_deg;
608            let lat2 = i32::from_be_bytes([t[41], t[42], t[43], t[44]]) as f64 * unit_deg;
609            let lon2 = i32::from_be_bytes([t[45], t[46], t[47], t[48]]) as f64 * unit_deg;
610            let mut di = u32::from_be_bytes([t[49], t[50], t[51], t[52]]) as f64 * unit_deg;
611            let mut dj = u32::from_be_bytes([t[53], t[54], t[55], t[56]]) as f64 * unit_deg;
612            let scan_mode = t[57];
613
614            // Fallback: compute increments from corner coordinates if encoded Di/Dj are zero
615            if (di == 0.0 || dj == 0.0) && (ni > 1 && nj > 1) {
616                // compute dj from lat1 and lat2 (lat1 - lat2) / (nj-1)
617                if dj == 0.0 {
618                    dj = (lat1 - lat2) / ((nj.saturating_sub(1)) as f64);
619                }
620                // compute di from lon1/lon2, handle wrapping
621                if di == 0.0 {
622                    let mut dlon = lon2 - lon1;
623                    while dlon <= -180.0 { dlon += 360.0; }
624                    while dlon > 180.0 { dlon -= 360.0; }
625                    di = dlon / ((ni.saturating_sub(1)) as f64);
626                }
627            }
628
629            // Last-resort fallback: if di/dj still zero (some datasets omit corner coords), assume a global regular grid
630            if (di == 0.0 || dj == 0.0) && (ni > 1 && nj > 1) {
631                // Conservative default spanning full globe: lon range 360, lat range 180
632                di = 360.0 / (ni as f64);
633                dj = 180.0 / (nj as f64);
634                // If encoded corner points are missing, use conventional top-left (lat1=90, lon1=-180)
635                if lat1 == 0.0 && lon1 == 0.0 {
636                    // choose lat1=90 so that lat2 ~ -90
637                    lat1 = 90.0;
638                    lon1 = -180.0;
639                }
640            }
641
642            // suggested center point (approx)
643            let _suggested_lat = lat1 - ((nj.saturating_sub(1) as f64) * dj / 2.0);
644            let _suggested_lon = normalize_lon_deg(lon1 + ((ni.saturating_sub(1) as f64) * di / 2.0));
645            return Ok(Grib2LatLonGrid {
646                ni,
647                nj,
648                lat1_deg: lat1,
649                lon1_deg: normalize_lon_deg(lon1),
650                di_deg: di,
651                dj_deg: dj,
652                scan_mode,
653            });
654        }
655
656        // skip other sections and continue
657    }
658}
659
660fn normalize_lon_deg(mut lon: f64) -> f64 {
661    while lon <= -180.0 { lon += 360.0; }
662    while lon > 180.0 { lon -= 360.0; }
663    lon
664}
665
666/// Read all Section7 payloads and return them in discovery order (may be empty)
667pub fn read_all_grib2_section7_payloads(path: &Path) -> Result<Vec<Vec<u8>>, String> {
668    use std::fs::File;
669    use std::io::Read;
670
671    let mut f = File::open(path).map_err(|e| format!("open file failed: {e}"))?;
672
673    // Section 0 is 16 bytes.
674    let mut s0 = [0u8; 16];
675    f.read_exact(&mut s0).map_err(|e| format!("read GRIB2 section0 failed: {e}"))?;
676    if &s0[0..4] != b"GRIB" {
677        return Err("not a GRIB file".to_string());
678    }
679    if s0[7] != 2 {
680        return Err(format!("not GRIB2 (edition={})", s0[7]));
681    }
682
683    let mut out = Vec::new();
684    loop {
685        let mut header = [0u8; 5];
686        match f.read_exact(&mut header) {
687            Ok(()) => {}
688            Err(e) => {
689                if e.kind() == std::io::ErrorKind::UnexpectedEof {
690                    break;
691                }
692                return Err(format!("read section header failed: {e}"));
693            }
694        }
695        let len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
696        let sect_num = header[4];
697        if len < 5 {
698            return Err(format!("invalid section length: {len} (section {sect_num})"));
699        }
700        let payload_len = len - 5;
701
702        if sect_num == 7 {
703            let mut payload = vec![0u8; payload_len];
704            match f.read_exact(&mut payload) {
705                Ok(()) => {
706                    out.push(payload);
707                    continue;
708                }
709                Err(e) => {
710                    return Err(format!("read Section7 payload failed: {e}"));
711                }
712            }
713        }
714
715        // Skip other sections.
716        let mut skip = vec![0u8; payload_len];
717        match f.read_exact(&mut skip) {
718            Ok(()) => {}
719            Err(_) => {
720                // broken/truncated, return what we collected so far
721                break;
722            }
723        }
724    }
725
726    Ok(out)
727}
728
729/// Batch decode a list of Section7 payload slices using corresponding template42 params.
730/// Returns a Vec of Option<Vec<f32>> where None indicates no params or decode skipped/failed.
731pub fn decode_template42_from_payload_list(
732    payloads: &[&[u8]],
733    params_list: &[Option<Template42Params>],
734    width: usize,
735    height: usize,
736) -> Result<Vec<Option<Vec<f32>>>, String> {
737    if payloads.len() != params_list.len() {
738        return Err("payloads and params_list length mismatch".to_string());
739    }
740    let mut out: Vec<Option<Vec<f32>>> = Vec::with_capacity(payloads.len());
741    for (payload, params_opt) in payloads.iter().zip(params_list.iter()) {
742        if let Some(params) = params_opt {
743            // call existing decoder (rust-aec is a required dependency of this crate)
744            match decode_template42_rust_from_params_with_payload(
745                payload,
746                width,
747                height,
748                params.num_points,
749                params.bits_per_sample,
750                params.block_size,
751                params.rsi,
752                params.flags_mask,
753                params.exp,
754                params.dec,
755                params.ref_val,
756            ) {
757                Ok(v) => out.push(Some(v)),
758                Err(_) => out.push(None),
759            }
760        } else {
761            out.push(None);
762        }
763    }
764    Ok(out)
765}
766
767/// High-level helper: read Section7 payloads and Section5 template42 params for the file,
768/// then batch decode template42 messages and return per-message decoded arrays (or None).
769pub fn decode_template42_for_file_by_message(
770    path: &Path,
771    width: usize,
772    height: usize,
773) -> Result<Vec<Option<Vec<f32>>>, String> {
774    let payloads_vec = read_grib2_section7_payloads_by_message(path)?;
775    let params_vec = read_grib2_section5_template42_params_by_message(path)?;
776    // prepare slices
777    let payload_slices: Vec<&[u8]> = payloads_vec.iter().map(|v| v.as_slice()).collect();
778    let params_ref: Vec<Option<Template42Params>> = params_vec.into_iter().collect();
779    decode_template42_from_payload_list(&payload_slices, &params_ref, width, height)
780}
781
782/// Convenience helper: decode the *first* template42 message in the file (if any).
783/// Returns Ok(Some(Vec<f32>)) if a template42 message was found and decoded successfully,
784/// Ok(None) if no suitable message was found, or Err on file/IO errors.
785pub fn decode_template42_first_message(
786    path: &Path,
787    width: usize,
788    height: usize,
789) -> Result<Option<Vec<f32>>, String> {
790    let payloads_vec = read_grib2_section7_payloads_by_message(path)?;
791    let params_vec = read_grib2_section5_template42_params_by_message(path)?;
792
793    let n = payloads_vec.len().min(params_vec.len());
794    for i in 0..n {
795        let payload = &payloads_vec[i];
796        if payload.is_empty() {
797            continue;
798        }
799        if let Some(ref params) = params_vec[i] {
800            match decode_template42_rust_from_params_with_payload(
801                payload.as_slice(),
802                width,
803                height,
804                params.num_points,
805                params.bits_per_sample,
806                params.block_size,
807                params.rsi,
808                params.flags_mask,
809                params.exp,
810                params.dec,
811                params.ref_val,
812            ) {
813                Ok(v) => return Ok(Some(v)),
814                Err(_) => continue, // try next
815            }
816        }
817    }
818    Ok(None)
819}
820
821/// Try to decode a specific message index using its Section7 payload if available; otherwise
822/// fall back to using the first Section7 payload in the file. Useful when per-message payload
823/// discovery may be unreliable.
824pub fn decode_template42_try_message_payload(
825    path: &Path,
826    message_index: usize,
827    width: usize,
828    height: usize,
829    params: &Template42Params,
830) -> Result<Vec<f32>, String> {
831    // Try to read per-message payloads and use the indexed payload if present and non-empty.
832    match read_grib2_section7_payloads_by_message(path) {
833        Ok(payloads) => {
834            if let Some(p) = payloads.get(message_index) {
835                if !p.is_empty() {
836                    return decode_template42_rust_from_params_with_payload(
837                        p.as_slice(),
838                        width,
839                        height,
840                        params.num_points,
841                        params.bits_per_sample,
842                        params.block_size,
843                        params.rsi,
844                        params.flags_mask,
845                        params.exp,
846                        params.dec,
847                        params.ref_val,
848                    );
849                }
850            }
851        }
852        Err(_) => {
853            // ignore; we'll fall back to first payload
854        }
855    }
856
857    // Fall back to decode using the *first* Section7 payload in the file.
858    let payload = read_first_grib2_section7_payload(path)?;
859    decode_template42_rust_from_params_with_payload(
860        &payload,
861        width,
862        height,
863        params.num_points,
864        params.bits_per_sample,
865        params.block_size,
866        params.rsi,
867        params.flags_mask,
868        params.exp,
869        params.dec,
870        params.ref_val,
871    )
872}
873
874/// Probe all GRIB2 fields at a lat/lon and return Vec<ProbeField>.
875/// Uses the grib crate (requires `grib-support`) and the crate's decoding helpers to fetch/compute values.
876pub fn probe_all_grib2_latlon(_path: &Path, _lat_deg: f64, _lon_deg: f64) -> Result<Vec<ProbeField>, String> {
877    #[cfg(not(feature = "grib-support"))]
878    {
879        return Err("probe_all_grib2_latlon requires the 'grib-support' feature to be enabled".to_string());
880    }
881
882    #[cfg(feature = "grib-support")]
883    {
884        use std::fs::File;
885        use std::io::BufReader;
886        use grib::{from_reader, Grib2SubmessageDecoder};
887
888        let grid = read_first_grib2_latlon_grid(path)?;
889        if grid.scan_mode != 0 {
890            return Err(format!("unsupported scanning_mode=0x{:02x}", grid.scan_mode));
891        }
892
893        let f = File::open(path).map_err(|e| format!("open file failed: {e}"))?;
894        let r = BufReader::new(f);
895        let grib2 = from_reader(r).map_err(|e| format!("GRIB parse failed: {e}"))?;
896
897        // Pre-decode template42 messages in batch when possible
898        let decoded_template42_by_message = match decode_template42_for_file_by_message(path, grid.ni, grid.nj) {
899            Ok(v) => v,
900            Err(_) => Vec::new(),
901        };
902
903        let mut out: Vec<ProbeField> = Vec::new();
904
905        for (idx, ((_msg_idx, _submsg_idx), submessage)) in grib2.iter().enumerate() {
906            let desc = submessage.describe();
907            let key = if let Some(sn) = crate::extract_tag_value_case_insensitive(&desc, &["shortname", "short_name", "short name"]) {
908                sn.trim_matches(|c: char| c == '"' || c == '\'' || c.is_whitespace()).to_string()
909            } else {
910                desc.lines().next().unwrap_or("<desc>").to_string()
911            };
912            let ft = crate::extract_tag_value_case_insensitive(&desc, &["forecast time", "forecasttime", "forecast_time", "forecast"]).and_then(|s| crate::parse_first_int_from_str(&s));
913
914            let (width, height) = match submessage.grid_shape() {
915                Ok((w,h)) => (w,h),
916                Err(_) => continue,
917            };
918            if width != grid.ni || height != grid.nj { continue; }
919
920            // get decoded values
921            let values_opt = decoded_template42_by_message.get(idx).and_then(|o| o.clone());
922            let values: Vec<f32> = if let Some(v) = values_opt { v } else {
923                // Extract Section5 params ahead of consuming the submessage
924                let template42_params_opt: Option<Template42Params> = match submessage.section5() {
925                    Ok(s5) => match s5.payload.template {
926                        grib::def::grib2::DataRepresentationTemplate::_5_42(t) => Some(Template42Params {
927                            num_points: s5.payload.num_encoded_points as usize,
928                            bits_per_sample: t.simple.num_bits as u8,
929                            block_size: t.block_size as u32,
930                            rsi: t.ref_sample_interval as u32,
931                            flags_mask: t.mask as u32,
932                            exp: t.simple.exp as i32,
933                            dec: t.simple.dec as i32,
934                            ref_val: t.simple.ref_val as f32,
935                        }),
936                        _ => None,
937                    },
938                    Err(_) => None,
939                };
940
941                let decoder = match Grib2SubmessageDecoder::from(submessage) {
942                    Ok(d) => d,
943                    Err(_) => continue,
944                };
945                match decoder.dispatch() {
946                    Ok(decoded) => decoded.collect::<Vec<f32>>(),
947                    Err(_) => {
948                        // Try rust-aec fallback for template42 using Section5 params and per-message payload
949                        if let Some(params) = template42_params_opt {
950                            match decode_template42_try_message_payload(path, idx, width, height, &params) {
951                                Ok(vv) => vv,
952                                Err(_) => continue,
953                            }
954                        } else {
955                            continue;
956                        }
957                    }
958                }
959            };
960
961            // interpolation
962            let mut dlon = lon_deg - grid.lon1_deg;
963            while dlon < 0.0 { dlon += 360.0; }
964            while dlon >= 360.0 { dlon -= 360.0; }
965            let i_f = dlon / grid.di_deg;
966            let j_f = (grid.lat1_deg - lat_deg) / grid.dj_deg;
967            if !i_f.is_finite() || !j_f.is_finite() { continue; }
968            let ni = width as isize; let nj = height as isize;
969            let i0 = i_f.floor() as isize; let j0 = j_f.floor() as isize; let i1 = i0 + 1; let j1 = j0 + 1;
970            let dx = i_f - (i0 as f64); let dy = j_f - (j0 as f64);
971            let i0w = (((i0 % ni) + ni) % ni) as usize; let i1w = (((i1 % ni) + ni) % ni) as usize;
972            let j0c = j0.clamp(0, nj - 1) as usize; let j1c = j1.clamp(0, nj - 1) as usize;
973            let idx00 = j0c.saturating_mul(width).saturating_add(i0w);
974            let idx10 = j0c.saturating_mul(width).saturating_add(i1w);
975            let idx01 = j1c.saturating_mul(width).saturating_add(i0w);
976            let idx11 = j1c.saturating_mul(width).saturating_add(i1w);
977            let s00 = values.get(idx00).copied().unwrap_or(f32::NAN);
978            let s10 = values.get(idx10).copied().unwrap_or(f32::NAN);
979            let s01 = values.get(idx01).copied().unwrap_or(f32::NAN);
980            let s11 = values.get(idx11).copied().unwrap_or(f32::NAN);
981            if !s00.is_finite() || !s10.is_finite() || !s01.is_finite() || !s11.is_finite() { continue; }
982            let v0 = s00 * (1.0 - dx as f32) + s10 * (dx as f32);
983            let v1 = s01 * (1.0 - dx as f32) + s11 * (dx as f32);
984            let value = v0 * (1.0 - dy as f32) + v1 * (dy as f32);
985
986            // grid center coords
987            let glat = grid.lat1_deg - j_f * grid.dj_deg;
988            let glon = normalize_lon_deg(grid.lon1_deg + i_f * grid.di_deg);
989
990            out.push(ProbeField { key, forecast_time: ft, i: i0w, j: j0c, lat: glat, lon: glon, value });
991        }
992
993        Ok(out)
994    }
995}
996
997/// Probe all fields (GRIB1 and GRIB2) at a lat/lon and return Vec<ProbeField>.
998/// For GRIB2 this delegates to `probe_all_grib2_latlon`. For GRIB1 a message-by-message scan is performed.
999pub fn probe_all_fields_at_lat_lon(path: &Path, lat_deg: f64, lon_deg: f64) -> Result<Vec<ProbeField>, String> {
1000    match detect_grib_edition(path)? {
1001        2 => probe_all_grib2_latlon(path, lat_deg, lon_deg),
1002        1 => {
1003            // Minimal GRIB1 probe: decode first message and return a single ProbeField
1004            let msg = read_grib1_first_message(path)?;
1005            let (grid, _drt) = parse_grib1_latlon_grid(&msg)?;
1006            let decoded = decode_grib1_first_message(&msg)?;
1007            let (i, j, glat, glon) = nearest_latlon_index_for_grib1(lat_deg, lon_deg, &grid)?;
1008            let idx = j.saturating_mul(decoded.width).saturating_add(i);
1009            let val = decoded.values.get(idx).copied().unwrap_or(f32::NAN);
1010            let key = extract_grib1_parameter_key(&msg).unwrap_or_else(|| "<unknown>".to_string());
1011            Ok(vec![ProbeField { key, forecast_time: None, i, j, lat: glat, lon: glon, value: val }])
1012        }
1013        other => Err(format!("unsupported GRIB edition {} (only 1/2 supported)", other)),
1014    }
1015}
1016
1017/// Summarize a GRIB file into `FileSummary` / `MessageSummary` for diagnostics.
1018pub fn summarize_file(path: &Path) -> Result<FileSummary, String> {
1019    match detect_grib_edition(path)? {
1020        2 => {
1021            #[cfg(not(feature = "grib-support"))]
1022            return Err("summarize_file requires the 'grib-support' feature".to_string());
1023
1024            #[cfg(feature = "grib-support")]
1025            {
1026                use std::fs::File;
1027                use std::io::BufReader;
1028                use grib::from_reader;
1029
1030                let f = File::open(path).map_err(|e| format!("open file failed: {}", e))?;
1031                let r = BufReader::new(f);
1032                let grib2 = from_reader(r).map_err(|e| format!("GRIB parse failed: {}", e))?;
1033
1034                // prefetch section7 payloads and section5 params to avoid reparsing
1035                let payloads = read_grib2_section7_payloads_by_message(path).unwrap_or_default();
1036                let params_vec = read_grib2_section5_template42_params_by_message(path).unwrap_or_default();
1037
1038                let mut messages: Vec<MessageSummary> = Vec::new();
1039                for (idx, ((_msg_idx, _submsg_idx), sub)) in grib2.iter().enumerate() {
1040                    let desc = sub.describe();
1041                    let key = if let Some(sn) = extract_tag_value_case_insensitive(&desc, &["shortname", "short_name", "short name"]) {
1042                        sn.trim_matches(|c: char| c == '"' || c == '\'' || c.is_whitespace()).to_string()
1043                    } else {
1044                        desc.lines().next().unwrap_or("<desc>").to_string()
1045                    };
1046                    let ft = extract_tag_value_case_insensitive(&desc, &["forecast time", "forecasttime", "forecast_time", "forecast"]).and_then(|s| parse_first_int_from_str(&s));
1047                    let repr = sub.repr_def().repr_tmpl_num() as u16;
1048                    let payload_len = payloads.get(idx).map(|v| v.len()).unwrap_or(0);
1049
1050                    // Try to read grid dims up-front for potential rust-aec use
1051                    let (width, height) = match sub.grid_shape() {
1052                        Ok((w,h)) => (w,h),
1053                        Err(_) => continue,
1054                    };
1055                    // Try a fast decode check: prefer grib crate dispatch if available, otherwise try rust-aec params/payload
1056                    let mut decode_ok = false;
1057                    let mut decoded_samples_len: Option<usize> = None;
1058
1059                    // First try grib crate decoder
1060                    {
1061                        use grib::Grib2SubmessageDecoder;
1062                        // Move sub into decoder (we already captured width/height above)
1063                        if let Ok(decoder) = Grib2SubmessageDecoder::from(sub) {
1064                            if let Ok(decoded_iter) = decoder.dispatch() {
1065                                let collected: Vec<f32> = decoded_iter.collect();
1066                                decoded_samples_len = Some(collected.len());
1067                                decode_ok = !collected.is_empty();
1068                            }
1069                        }
1070                    }
1071
1072                    // If decode failed and Section5 params exist, try rust-aec quick attempt using payload and grid dims
1073                    if !decode_ok {
1074                        if let Some(params_opt) = params_vec.get(idx) {
1075                            if let Some(params) = params_opt.as_ref() {
1076                                if payload_len > 0 {
1077                                    if let Ok(vv) = decode_template42_rust_from_params_with_payload(payloads.get(idx).map(|v| v.as_slice()).unwrap_or(&[]), width, height, params.num_points, params.bits_per_sample, params.block_size, params.rsi, params.flags_mask, params.exp, params.dec, params.ref_val) {
1078                                        decode_ok = !vv.is_empty();
1079                                        decoded_samples_len = Some(vv.len());
1080                                    }
1081                                }
1082                            }
1083                        }
1084                    }
1085
1086                    messages.push(MessageSummary {
1087                        message_index: idx,
1088                        key,
1089                        forecast_time: ft,
1090                        repr_template: Some(repr),
1091                        payload_len,
1092                        decode_ok,
1093                        decoded_samples_len,
1094                    });
1095                }
1096
1097                Ok(FileSummary { path: path.to_path_buf(), n_messages: messages.len(), messages })
1098            }
1099        }
1100        1 => {
1101            use std::fs::File;
1102            use std::io::Read;
1103            let mut f = File::open(path).map_err(|e| format!("open file failed: {}", e))?;
1104            let mut messages: Vec<MessageSummary> = Vec::new();
1105            let mut msg_idx = 0usize;
1106            loop {
1107                let mut header = [0u8; 8];
1108                match f.read_exact(&mut header) {
1109                    Ok(()) => {}
1110                    Err(e) => {
1111                        if e.kind() == std::io::ErrorKind::UnexpectedEof { break; }
1112                        return Err(format!("read GRIB1 header failed: {}", e));
1113                    }
1114                }
1115                if &header[0..4] != b"GRIB" || header[7] != 1 { return Err("not a GRIB1 file or header error".to_string()); }
1116                let total_len = read_u24_be(&header[4..7]) as usize;
1117                if total_len < 8 { return Err(format!("invalid message length: {}", total_len)); }
1118                let mut msg = vec![0u8; total_len];
1119                msg[0..8].copy_from_slice(&header);
1120                f.read_exact(&mut msg[8..]).map_err(|e| format!("read GRIB1 message failed: {}", e))?;
1121                let key = extract_grib1_parameter_key(&msg).unwrap_or_else(|| "<unknown>".to_string());
1122                let payload_len = total_len;
1123                let mut decode_ok = false;
1124                let mut decoded_samples_len = None;
1125                if let Ok(d) = decode_grib1_first_message(&msg) {
1126                    decode_ok = !d.values.is_empty();
1127                    decoded_samples_len = Some(d.values.len());
1128                }
1129                messages.push(MessageSummary {
1130                    message_index: msg_idx,
1131                    key,
1132                    forecast_time: None,
1133                    repr_template: None,
1134                    payload_len,
1135                    decode_ok,
1136                    decoded_samples_len,
1137                });
1138                msg_idx += 1;
1139            }
1140            Ok(FileSummary { path: path.to_path_buf(), n_messages: messages.len(), messages })
1141        }
1142        other => Err(format!("unsupported GRIB edition={} (only 1/2 supported)", other)),
1143    }
1144}
1145
1146/// Find candidate messages for a parameter (using the existing alias matching heuristics).
1147pub fn find_candidates_for_param(path: &Path, param: &str) -> Result<Vec<(usize, MatchReason, String, Option<i32>)>, String> {
1148    let alias = crate::param_alias::ParamAlias::for_param(param);
1149    let fs = summarize_file(path)?;
1150    let mut out: Vec<(usize, MatchReason, String, Option<i32>)> = Vec::new();
1151    for m in fs.messages.into_iter() {
1152        let key_l = m.key.to_lowercase();
1153        // token check
1154        for t in alias.tokens.iter() {
1155            if key_l.contains(t) {
1156                out.push((m.message_index, MatchReason::Token, m.key.clone(), m.forecast_time));
1157                continue;
1158            }
1159        }
1160        // normalized param contains
1161        let norm = |s: &str| s.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>();
1162        let nparam = norm(param);
1163        let nkey = norm(&key_l);
1164        if !nparam.is_empty() && nkey.contains(&nparam) {
1165            out.push((m.message_index, MatchReason::NormParam, m.key.clone(), m.forecast_time));
1166            continue;
1167        }
1168    }
1169    Ok(out)
1170}
1171
1172fn nearest_latlon_index_for_grib1(lat_deg: f64, lon_deg: f64, grid: &Grib1LatLonGrid) -> Result<(usize, usize, f64, f64), String> {
1173    if grid.di_deg == 0.0 || grid.dj_deg == 0.0 {
1174        return Err("grid increments are zero".to_string());
1175    }
1176
1177    let lon = normalize_lon_deg(lon_deg);
1178    let lon1 = normalize_lon_deg(grid.lon1_deg);
1179
1180    let mut i = ((lon - lon1) / grid.di_deg).round() as isize;
1181    let mut j = ((grid.lat1_deg - lat_deg) / grid.dj_deg).round() as isize;
1182
1183    if grid.ni > 0 {
1184        let ni = grid.ni as isize;
1185        i = ((i % ni) + ni) % ni;
1186    }
1187    j = j.clamp(0, grid.nj.saturating_sub(1) as isize);
1188
1189    let i_usize = i as usize;
1190    let j_usize = j as usize;
1191    let glat = grid.lat1_deg - (j_usize as f64) * grid.dj_deg;
1192    let glon = normalize_lon_deg(lon1 + (i_usize as f64) * grid.di_deg);
1193    Ok((i_usize, j_usize, glat, glon))
1194}
1195
1196fn extract_grib1_parameter_key(msg: &[u8]) -> Option<String> {
1197    if msg.len() < 8 { return None; }
1198    let offset = 8;
1199    let pds_len = read_u24_be(msg.get(offset..offset + 3)? ) as usize;
1200    if msg.len() < offset + pds_len { return None; }
1201    let pds = &msg[offset..offset + pds_len];
1202    if pds.len() < 9 { return None; }
1203    let param = pds.get(8).copied()?;
1204    Some(format!("p{}", param))
1205}
1206
1207/// Decoded field helper returned by high-level decoders.
1208#[derive(Debug, Clone)]
1209pub struct DecodedField {
1210    pub width: usize,
1211    pub height: usize,
1212    pub values: Vec<f32>,
1213    /// For template 5.0=42 decoding, which backend was used: Some("rust-aec-first" | "rust-aec") or None if grib crate returned decoded data.
1214    pub aec_backend: Option<&'static str>,
1215}
1216
1217/// Probe result entry returned by `probe_all_fields_at_lat_lon`.
1218#[derive(Debug, Clone)]
1219pub struct ProbeField {
1220    /// Parameter key, typically shortname or first line of description.
1221    pub key: String,
1222    /// Optional forecast time (hours) extracted from description (if available)
1223    pub forecast_time: Option<i32>,
1224    /// Grid fractional indices (i, j) rounded to nearest integer used for interpolation
1225    pub i: usize,
1226    pub j: usize,
1227    /// Grid center latitude/longitude in degrees
1228    pub lat: f64,
1229    pub lon: f64,
1230    /// Value at the location (units as-present in GRIB field)
1231    pub value: f32,
1232}
1233
1234/// Decode the first available field in the GRIB2 file.
1235///
1236/// Strategy:
1237/// 1. Try a fast-path: detect regular lat/lon grid and attempt to decode the first template 5.0=42 message via the pure-Rust AEC helper.
1238/// 2. If the fast-path fails and the crate is built with `grib-support`, use the `grib` crate to decode the first submessage; if that fails due to unsupported template 42, try the rust-aec fallback using Section5 params and per-message/first payload.
1239/// 3. If neither approach succeeds, return an Err.
1240pub fn decode_grib2_first_field(path: &Path) -> Result<DecodedField, String> {
1241    // Fast-path using the pure-Rust AEC decoder (if a grid can be determined)
1242    if let Ok(grid) = read_first_grib2_latlon_grid(path) {
1243        match decode_template42_first_message(path, grid.ni, grid.nj) {
1244            Ok(Some(values)) => return Ok(DecodedField { width: grid.ni, height: grid.nj, values, aec_backend: Some("rust-aec-first") }),
1245            Ok(None) | Err(_) => {}
1246        }
1247    }
1248
1249    // If the grib crate is available, try to use it to decode the first submessage and fall back to rust-aec when needed.
1250    #[cfg(feature = "grib-support")]
1251    {
1252        use grib::{from_reader, Grib2SubmessageDecoder, GribError, DecodeError};
1253        use std::fs::File;
1254        use std::io::BufReader;
1255
1256        let f = File::open(path).map_err(|e| format!("open file failed: {e}"))?;
1257        let r = BufReader::new(f);
1258        let grib2 = from_reader(r).map_err(|e| format!("GRIB parse failed: {e}"))?;
1259        // Get first submessage and optionally the Section5 payload (we may need it for template 42 fallback).
1260        let ((_msg_idx, _submsg_idx), submessage) = grib2.iter().next().ok_or_else(|| "No GRIB2 submessages found in file".to_string())?;
1261        let (width, height) = submessage.grid_shape().map_err(|e| format!("grid_shape read failed: {e}"))?;
1262
1263        // Pull Section5 if available before we move/consume the submessage.
1264        let s5_opt = match submessage.section5() {
1265            Ok(s5) => Some(s5),
1266            Err(_) => None,
1267        };
1268
1269        let decoder = Grib2SubmessageDecoder::from(submessage).map_err(|e| format!("Decoder creation failed: {e}"))?;
1270
1271        match decoder.dispatch() {
1272            Ok(decoded) => return Ok(DecodedField { width, height, values: decoded.collect(), aec_backend: None }),
1273            Err(err) => {
1274                // If template 42 not supported, try rust-aec fallback using Section5 params and payload(s).
1275                if matches!(err, GribError::DecodeError(DecodeError::NotSupported(
1276                    "GRIB2 code table 5.0 (data representation template number)", 42
1277                ))) {
1278                    if let Some(s5) = s5_opt {
1279                        use grib::def::grib2::DataRepresentationTemplate;
1280                        match s5.payload.template {
1281                            DataRepresentationTemplate::_5_42(t) => {
1282                                let params = Template42Params {
1283                                    num_points: s5.payload.num_encoded_points as usize,
1284                                    bits_per_sample: t.simple.num_bits as u8,
1285                                    block_size: t.block_size as u32,
1286                                    rsi: t.ref_sample_interval as u32,
1287                                    flags_mask: t.mask as u32,
1288                                    exp: t.simple.exp as i32,
1289                                    dec: t.simple.dec as i32,
1290                                    ref_val: t.simple.ref_val as f32,
1291                                };
1292                                match decode_template42_try_message_payload(path, 0, width, height, &params) {
1293                                    Ok(values) => return Ok(DecodedField { width, height, values, aec_backend: Some("rust-aec") }),
1294                                    Err(e) => return Err(format!("rust-aec decode failed: {e}")),
1295                                }
1296                            }
1297                            _ => return Err("template 42 params missing or unexpected template".to_string()),
1298                        }
1299                    } else {
1300                        return Err("template 42 params missing (no Section5)".to_string());
1301                    }
1302                } else {
1303                    return Err(format!("decode error: {err}"));
1304                }
1305            }
1306        }
1307    }
1308
1309    #[cfg(not(feature = "grib-support"))]
1310    {
1311        return Err("grib-support feature disabled and fast-path did not decode first field".to_string());
1312    }
1313
1314}
1315// ----------------------------- GRIB1 helpers -----------------------------
1316
1317/// GRIB1 regular lat/lon grid metadata (similar layout to GRIB2 helper)
1318#[derive(Debug, Clone)]
1319pub struct Grib1LatLonGrid {
1320    pub ni: usize,
1321    pub nj: usize,
1322    pub lat1_deg: f64,
1323    pub lon1_deg: f64,
1324    pub di_deg: f64,
1325    pub dj_deg: f64,
1326    pub scan_mode: u8,
1327}
1328
1329#[derive(Debug, Clone)]
1330pub struct Grib1Decoded {
1331    pub width: usize,
1332    pub height: usize,
1333    pub values: Vec<f32>,
1334}
1335
1336/// Read the first GRIB1 message from file (returns the raw message bytes)
1337pub fn read_grib1_first_message(path: &Path) -> Result<Vec<u8>, String> {
1338    use std::fs::File;
1339    use std::io::Read;
1340
1341    let mut f = File::open(path).map_err(|e| format!("打开文件失败:{e}"))?;
1342    let mut header = [0u8; 8];
1343    f.read_exact(&mut header)
1344        .map_err(|e| format!("读取 GRIB1 头失败:{e}"))?;
1345
1346    if &header[0..4] != b"GRIB" {
1347        return Err("文件头不是 GRIB".to_string());
1348    }
1349    if header[7] != 1 {
1350        return Err(format!("不是 GRIB1(edition={})", header[7]));
1351    }
1352
1353    let msg_len = ((header[4] as u32) << 16 | (header[5] as u32) << 8 | (header[6] as u32)) as usize;
1354    if msg_len < 8 {
1355        return Err(format!("GRIB1 message length 异常:{msg_len}"));
1356    }
1357
1358    let mut msg = vec![0u8; msg_len];
1359    msg[0..8].copy_from_slice(&header);
1360    f.read_exact(&mut msg[8..])
1361        .map_err(|e| format!("读取 GRIB1 第一个 message 失败:{e}"))?;
1362    Ok(msg)
1363}
1364
1365/// Parse a GRIB1 message and extract a lat/lon grid (returns (grid, data_representation_type))
1366pub fn parse_grib1_latlon_grid(msg: &[u8]) -> Result<(Grib1LatLonGrid, u8), String> {
1367    if msg.len() < 8 {
1368        return Err("GRIB1 message 太短".to_string());
1369    }
1370    let mut offset = 8;
1371
1372    let pds_len = read_u24_be(msg.get(offset..offset + 3).ok_or_else(|| "PDS 长度越界".to_string())?) as usize;
1373    if pds_len < 28 {
1374        return Err(format!("PDS length 太小:{pds_len}"));
1375    }
1376    let pds = msg.get(offset..offset + pds_len).ok_or_else(|| "PDS 越界".to_string())?;
1377    let flags = pds.get(7).copied().ok_or_else(|| "PDS flags 越界".to_string())?;
1378    let has_gds = (flags & 0x80) != 0;
1379    offset += pds_len;
1380    if !has_gds {
1381        return Err("GRIB1: 缺少 GDS(无法获取网格信息)".to_string());
1382    }
1383
1384    let gds_len = read_u24_be(msg.get(offset..offset + 3).ok_or_else(|| "GDS 长度越界".to_string())?) as usize;
1385    if gds_len < 28 {
1386        return Err(format!("GDS length 太小:{gds_len}"));
1387    }
1388    let gds = msg.get(offset..offset + gds_len).ok_or_else(|| "GDS 越界".to_string())?;
1389
1390    let grid_type = gds.get(5).copied().ok_or_else(|| "GDS data_representation_type 越界".to_string())?;
1391    let ni = read_u16_be(gds.get(6..8).ok_or_else(|| "GDS Ni 越界".to_string())?) as usize;
1392    let nj = read_u16_be(gds.get(8..10).ok_or_else(|| "GDS Nj 越界".to_string())?) as usize;
1393
1394    // GRIB1 lat/lon coordinates are in millidegrees, encoded as signed sign-magnitude 24-bit.
1395    let la1_md = read_i24_grib1(gds.get(10..13).ok_or_else(|| "GDS La1 越界".to_string())?);
1396    let lo1_md = read_i24_grib1(gds.get(13..16).ok_or_else(|| "GDS Lo1 越界".to_string())?);
1397    let di_md = read_u16_be(gds.get(23..25).ok_or_else(|| "GDS Di 越界".to_string())?) as i32;
1398    let dj_md = read_u16_be(gds.get(25..27).ok_or_else(|| "GDS Dj 越界".to_string())?) as i32;
1399    let scan_mode = gds.get(27).copied().ok_or_else(|| "GDS scanning_mode 越界".to_string())?;
1400
1401    let grid = Grib1LatLonGrid {
1402        ni: ni.max(1),
1403        nj: nj.max(1),
1404        lat1_deg: (la1_md as f64) / 1000.0,
1405        lon1_deg: (lo1_md as f64) / 1000.0,
1406        di_deg: (di_md as f64) / 1000.0,
1407        dj_deg: (dj_md as f64) / 1000.0,
1408        scan_mode,
1409    };
1410    Ok((grid, grid_type))
1411}
1412
1413fn read_i24_grib1(bytes: &[u8]) -> i32 {
1414    let b0 = bytes.get(0).copied().unwrap_or(0);
1415    let b1 = bytes.get(1).copied().unwrap_or(0);
1416    let b2 = bytes.get(2).copied().unwrap_or(0);
1417    let magnitude: i32 = ((b0 as i32 & 0x7f) << 16) | ((b1 as i32) << 8) | (b2 as i32);
1418    if (b0 & 0x80) != 0 { -magnitude } else { magnitude }
1419}
1420
1421fn read_i16_grib1(bytes: &[u8]) -> i16 {
1422    let b0 = bytes.get(0).copied().unwrap_or(0);
1423    let b1 = bytes.get(1).copied().unwrap_or(0);
1424    let magnitude = (b1 as i16) + (((b0 & 0x7f) as i16) << 8);
1425    if (b0 & 0x80) != 0 { -magnitude } else { magnitude }
1426}
1427
1428fn read_f32_ibm(bytes: &[u8]) -> f32 {
1429    if bytes.len() < 4 {
1430        return f32::NAN;
1431    }
1432    let sign = if (bytes[0] & 0x80) != 0 { -1.0 } else { 1.0 };
1433    let exponent = (bytes[0] & 0x7f) as i32;
1434    let mantissa = (((bytes[1] as i32) << 16) | ((bytes[2] as i32) << 8) | (bytes[3] as i32)) as f32;
1435    sign * 2.0f32.powi(-24) * mantissa * 16.0f32.powi(exponent - 64)
1436}
1437
1438/// Decode simple-packed GRIB1 BDS packed values (supports optional bitmap)
1439pub fn decode_grib1_simple_packed_values(
1440    packed: &[u8],
1441    npoints: usize,
1442    bitmap: Option<&[u8]>,
1443    bits_per_value: usize,
1444    ref_value: f32,
1445    binary_scale: i16,
1446) -> Result<Vec<f32>, String> {
1447    let factor = 2.0f32.powi(binary_scale as i32);
1448
1449    let mut values = Vec::with_capacity(npoints);
1450
1451    let mut data_bit = BitCursor::new(packed);
1452    let mut bmp_bit = bitmap.map(BitCursor::new);
1453
1454    for _ in 0..npoints {
1455        if let Some(bmp) = bmp_bit.as_mut() {
1456            let present = bmp.read_bit().ok_or_else(|| "Bitmap 读取越界".to_string())?;
1457            if !present {
1458                values.push(f32::NAN);
1459                continue;
1460            }
1461        }
1462
1463        if bits_per_value == 0 {
1464            values.push(ref_value);
1465            continue;
1466        }
1467
1468        if bits_per_value > 32 {
1469            return Err(format!("bits_per_value 太大:{bits_per_value}(>32 暂不支持)"));
1470        }
1471
1472        let x = data_bit
1473            .read_bits(bits_per_value)
1474            .ok_or_else(|| "BDS packed data 读取越界".to_string())?;
1475
1476        values.push(ref_value + (x as f32) * factor);
1477    }
1478
1479    Ok(values)
1480}
1481
1482struct BitCursor<'a> {
1483    data: &'a [u8],
1484    bit_pos: usize,
1485}
1486
1487impl<'a> BitCursor<'a> {
1488    fn new(data: &'a [u8]) -> Self {
1489        Self { data, bit_pos: 0 }
1490    }
1491
1492    fn read_bit(&mut self) -> Option<bool> {
1493        let v = self.read_bits(1)?;
1494        Some(v != 0)
1495    }
1496
1497    fn read_bits(&mut self, nbits: usize) -> Option<u32> {
1498        if nbits == 0 {
1499            return Some(0);
1500        }
1501
1502        let mut out: u32 = 0;
1503        for _ in 0..nbits {
1504            let byte_idx = self.bit_pos / 8;
1505            let bit_in_byte = self.bit_pos % 8;
1506            let byte = *self.data.get(byte_idx)?;
1507            let bit = (byte >> (7 - bit_in_byte)) & 1;
1508            out = (out << 1) | (bit as u32);
1509            self.bit_pos += 1;
1510        }
1511        Some(out)
1512    }
1513}
1514
1515/// Decode the first GRIB1 message into an array of floats (supports simple packing & optional bitmap)
1516pub fn decode_grib1_first_message(msg: &[u8]) -> Result<Grib1Decoded, String> {
1517    if msg.len() < 8 {
1518        return Err("GRIB1 message 太短".to_string());
1519    }
1520    if &msg[0..4] != b"GRIB" {
1521        return Err("GRIB1 header 不正确".to_string());
1522    }
1523    if msg[7] != 1 {
1524        return Err(format!("不是 GRIB1(edition={})", msg[7]));
1525    }
1526
1527    let total_len = ((msg[4] as u32) << 16 | (msg[5] as u32) << 8 | (msg[6] as u32)) as usize;
1528    if total_len != msg.len() {
1529        if total_len > msg.len() {
1530            return Err(format!("GRIB1 指示的 message 长度={total_len},但实际读取到 {} 字节", msg.len()));
1531        }
1532    }
1533
1534    let mut offset = 8;
1535
1536    // --- PDS ---
1537    let pds_len = read_u24_be(msg.get(offset..offset + 3).ok_or_else(|| "PDS 长度越界".to_string())?) as usize;
1538    if pds_len < 28 {
1539        return Err(format!("PDS length 太小:{pds_len}"));
1540    }
1541    let pds = msg.get(offset..offset + pds_len).ok_or_else(|| "PDS 越界".to_string())?;
1542    let flags = pds.get(7).copied().ok_or_else(|| "PDS flags 越界".to_string())?;
1543    let has_gds = (flags & 0x80) != 0;
1544    let has_bms = (flags & 0x40) != 0;
1545    offset += pds_len;
1546
1547    // --- GDS ---
1548    let (width, height, data_representation_type) = if has_gds {
1549        let gds_len = read_u24_be(msg.get(offset..offset + 3).ok_or_else(|| "GDS 长度越界".to_string())?) as usize;
1550        if gds_len < 11 {
1551            return Err(format!("GDS length 太小:{gds_len}"));
1552        }
1553        let gds = msg.get(offset..offset + gds_len).ok_or_else(|| "GDS 越界".to_string())?;
1554        let drt = gds.get(5).copied().ok_or_else(|| "GDS data_representation_type 越界".to_string())?;
1555        let ni = read_u16_be(gds.get(6..8).ok_or_else(|| "GDS Ni 越界".to_string())?) as usize;
1556        let nj = read_u16_be(gds.get(8..10).ok_or_else(|| "GDS Nj 越界".to_string())?) as usize;
1557        offset += gds_len;
1558        match drt {
1559            0 | 10 => (ni.max(1), nj.max(1), drt),
1560            other => return Err(format!("GRIB1: 暂不支持 data_representation_type={other}(目前支持 0=LatLon, 10=RotatedLatLon)")),
1561        }
1562    } else {
1563        return Err("GRIB1: 缺少 GDS(无法获取网格尺寸)".to_string());
1564    };
1565
1566    // --- BMS (Bitmap Section) ---
1567    let bitmap: Option<Vec<u8>> = if has_bms {
1568        let bms_len = read_u24_be(msg.get(offset..offset + 3).ok_or_else(|| "BMS 长度越界".to_string())?) as usize;
1569        if bms_len < 6 {
1570            return Err(format!("BMS length 太小:{bms_len}"));
1571        }
1572        let bms = msg.get(offset..offset + bms_len).ok_or_else(|| "BMS 越界".to_string())?;
1573        let table_ref = read_u16_be(bms.get(4..6).ok_or_else(|| "BMS table_reference 越界".to_string())?);
1574        if table_ref != 0 {
1575            return Err(format!("GRIB1: BMS 使用了预定义 bitmap(table_reference={table_ref}),暂不支持"));
1576        }
1577        let bmp = bms.get(6..).unwrap_or(&[]).to_vec();
1578        offset += bms_len;
1579        Some(bmp)
1580    } else {
1581        None
1582    };
1583
1584    // --- BDS ---
1585    let bds_len = read_u24_be(msg.get(offset..offset + 3).ok_or_else(|| "BDS 长度越界".to_string())?) as usize;
1586    if bds_len < 11 {
1587        return Err(format!("BDS length 太小:{bds_len}"));
1588    }
1589    let bds = msg.get(offset..offset + bds_len).ok_or_else(|| "BDS 越界".to_string())?;
1590    let bds_flags = bds.get(3).copied().ok_or_else(|| "BDS flags 越界".to_string())?;
1591
1592    let has_complex_packing = (bds_flags & 0b0100_0000) != 0;
1593    if has_complex_packing {
1594        return Err(format!("GRIB1: 暂不支持复杂 packing(BDS flags=0x{bds_flags:02x},grid_type={data_representation_type})"));
1595    }
1596
1597    let binary_scale = read_i16_grib1(bds.get(4..6).ok_or_else(|| "BDS binary_scale 越界".to_string())?);
1598    let ref_value = read_f32_ibm(bds.get(6..10).ok_or_else(|| "BDS ref_value 越界".to_string())?);
1599    let bits_per_value = bds.get(10).copied().ok_or_else(|| "BDS bits_per_value 越界".to_string())? as usize;
1600    let packed = bds.get(11..).unwrap_or(&[]);
1601
1602    let npoints = width
1603        .checked_mul(height)
1604        .ok_or_else(|| "width*height 溢出".to_string())?;
1605
1606    let values = decode_grib1_simple_packed_values(
1607        packed,
1608        npoints,
1609        bitmap.as_deref(),
1610        bits_per_value,
1611        ref_value,
1612        binary_scale,
1613    )?;
1614
1615    Ok(Grib1Decoded { width, height, values })
1616}
1617
1618// small helper to read 3-byte big-endian integer
1619fn read_u24_be(bytes: &[u8]) -> u32 {
1620    let b0 = bytes.get(0).copied().unwrap_or(0) as u32;
1621    let b1 = bytes.get(1).copied().unwrap_or(0) as u32;
1622    let b2 = bytes.get(2).copied().unwrap_or(0) as u32;
1623    (b0 << 16) | (b1 << 8) | b2
1624}
1625
1626fn read_u16_be(bytes: &[u8]) -> u16 {
1627    let b0 = bytes.get(0).copied().unwrap_or(0) as u16;
1628    let b1 = bytes.get(1).copied().unwrap_or(0) as u16;
1629    (b0 << 8) | b1
1630}
1631
1632
1633