use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;
const MAX_DEGREE: usize = 13;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let gen_path = Path::new(&out_dir).join("igrf_generated.rs");
#[cfg(feature = "fetch-igrf")]
{
const IGRF_URL: &str = "https://www.ngdc.noaa.gov/IAGA/vmod/coeffs/igrf14coeffs.txt";
eprintln!("cargo:warning=Downloading IGRF coefficients from {IGRF_URL}");
match download_and_generate(IGRF_URL, &gen_path) {
Ok(()) => {
}
Err(e) => {
eprintln!("cargo:warning=Download failed ({e}), falling back to vendored data");
copy_vendored(&gen_path);
}
}
}
#[cfg(not(feature = "fetch-igrf"))]
copy_vendored(&gen_path);
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=data/igrf14.rs");
}
fn copy_vendored(dest: &Path) {
let vendored = Path::new("data/igrf14.rs");
fs::copy(vendored, dest)
.unwrap_or_else(|e| panic!("Failed to copy vendored {}: {e}", vendored.display()));
}
#[cfg(feature = "fetch-igrf")]
fn download_and_generate(url: &str, dest: &Path) -> Result<(), String> {
let body: String = ureq::get(url)
.call()
.map_err(|e| format!("{e}"))?
.body_mut()
.read_to_string()
.map_err(|e| format!("{e}"))?;
let parsed = parse_igrf(&body);
if parsed.columns.is_empty() || parsed.rows.is_empty() {
return Err("Downloaded data does not appear to be valid IGRF coefficients".into());
}
generate_rust(dest, &parsed);
Ok(())
}
struct ParsedIgrf {
columns: Vec<String>,
rows: Vec<CoeffRow>,
}
struct CoeffRow {
gh: char,
n: usize,
m: usize,
values: Vec<f64>,
}
fn parse_igrf(raw: &str) -> ParsedIgrf {
let mut columns = Vec::new();
let mut rows = Vec::new();
for line in raw.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with("g/h") || line.starts_with("c/s") {
let parts: Vec<&str> = line.split_whitespace().collect();
columns = parts[3..].iter().map(|s| s.to_string()).collect();
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
continue;
}
let gh = match parts[0].chars().next() {
Some(c @ ('g' | 'h')) => c,
_ => continue,
};
let n: usize = parts[1].parse().unwrap();
let m: usize = parts[2].parse().unwrap();
if n > MAX_DEGREE {
continue;
}
let values: Vec<f64> = parts[3..]
.iter()
.map(|s| s.parse::<f64>().unwrap())
.collect();
rows.push(CoeffRow { gh, n, m, values });
}
ParsedIgrf { columns, rows }
}
fn coeff_index(n: usize, m: usize) -> usize {
(n - 1) * n / 2 + (n - 1) + m
}
fn total_coeffs() -> usize {
MAX_DEGREE * (MAX_DEGREE + 3) / 2
}
fn extract_array(rows: &[CoeffRow], col_idx: usize, is_g: bool) -> Vec<f64> {
let n = total_coeffs();
let mut arr = vec![0.0_f64; n];
let target = if is_g { 'g' } else { 'h' };
for row in rows {
if row.gh != target || row.n > MAX_DEGREE {
continue;
}
let idx = coeff_index(row.n, row.m);
if idx < n && col_idx < row.values.len() {
arr[idx] = row.values[col_idx];
}
}
arr
}
fn parse_year(label: &str) -> Option<f64> {
if label.contains('-') {
return None;
}
label.parse::<f64>().ok()
}
fn generate_rust(path: &Path, parsed: &ParsedIgrf) {
let n = total_coeffs();
let mut out = fs::File::create(path).unwrap();
writeln!(out, "// Auto-generated by build.rs — do not edit manually.").unwrap();
writeln!(
out,
"// Source: IGRF-14, IAGA/NOAA (https://www.ngdc.noaa.gov/IAGA/vmod/coeffs/igrf14coeffs.txt)"
)
.unwrap();
writeln!(out).unwrap();
writeln!(out, "pub const IGRF_REFERENCE_RADIUS: f64 = 6371.2;").unwrap();
writeln!(out, "pub const IGRF_MAX_DEGREE: usize = {MAX_DEGREE};").unwrap();
writeln!(out, "pub const N_COEFFS: usize = {n};").unwrap();
writeln!(out).unwrap();
let mut epoch_years: Vec<(usize, f64)> = Vec::new();
let mut sv_col_idx: Option<usize> = None;
for (i, label) in parsed.columns.iter().enumerate() {
if let Some(year) = parse_year(label) {
epoch_years.push((i, year));
} else {
sv_col_idx = Some(i);
}
}
let num_epochs = epoch_years.len();
writeln!(out, "pub const NUM_EPOCHS: usize = {num_epochs};").unwrap();
writeln!(out).unwrap();
writeln!(out, "#[rustfmt::skip]").unwrap();
write!(out, "pub const EPOCH_YEARS: [f64; {num_epochs}] = [").unwrap();
for (i, (_, year)) in epoch_years.iter().enumerate() {
if i > 0 {
write!(out, ", ").unwrap();
}
write!(out, "{year:.1}").unwrap();
}
writeln!(out, "];").unwrap();
writeln!(out).unwrap();
writeln!(out, "#[rustfmt::skip]").unwrap();
writeln!(out, "pub const G_EPOCHS: [[f64; {n}]; {num_epochs}] = [").unwrap();
for (col_idx, _) in &epoch_years {
write_inner_array(&mut out, &extract_array(&parsed.rows, *col_idx, true));
}
writeln!(out, "];").unwrap();
writeln!(out).unwrap();
writeln!(out, "#[rustfmt::skip]").unwrap();
writeln!(out, "pub const H_EPOCHS: [[f64; {n}]; {num_epochs}] = [").unwrap();
for (col_idx, _) in &epoch_years {
write_inner_array(&mut out, &extract_array(&parsed.rows, *col_idx, false));
}
writeln!(out, "];").unwrap();
writeln!(out).unwrap();
if let Some(sv_idx) = sv_col_idx {
write_flat_array(
&mut out,
"DG_SV",
&extract_array(&parsed.rows, sv_idx, true),
);
write_flat_array(
&mut out,
"DH_SV",
&extract_array(&parsed.rows, sv_idx, false),
);
}
writeln!(out, "#[inline]").unwrap();
writeln!(
out,
"pub const fn coeff_index(n: usize, m: usize) -> usize {{"
)
.unwrap();
writeln!(out, " (n - 1) * n / 2 + (n - 1) + m").unwrap();
writeln!(out, "}}").unwrap();
}
fn write_inner_array(out: &mut fs::File, arr: &[f64]) {
write!(out, " [").unwrap();
for (i, val) in arr.iter().enumerate() {
if i > 0 && i % 8 == 0 {
write!(out, "\n ").unwrap();
}
write!(out, "{val:>12.1},").unwrap();
}
writeln!(out, "],").unwrap();
}
fn write_flat_array(out: &mut fs::File, name: &str, arr: &[f64]) {
let n = arr.len();
writeln!(out, "#[rustfmt::skip]").unwrap();
writeln!(out, "pub const {name}: [f64; {n}] = [").unwrap();
for (i, val) in arr.iter().enumerate() {
if i % 7 == 0 {
write!(out, " ").unwrap();
}
write!(out, "{val:>12.1},").unwrap();
if i % 7 == 6 || i == n - 1 {
writeln!(out).unwrap();
}
}
writeln!(out, "];").unwrap();
writeln!(out).unwrap();
}