use std::error::Error;
use std::path::Path;
use std::sync::Mutex;
use crate::cli::DepthArgs;
use crate::config::{resolve, Settings};
use crate::pipeline::{run_module, Enricher, OutputKind, OutputSpec, Value};
struct Axis {
origin: f64,
step: f64,
len: usize,
}
impl Axis {
fn from_coords(coords: &[f64]) -> Result<Self, Box<dyn Error>> {
if coords.len() < 2 {
return Err("axis needs at least two coordinates".into());
}
let origin = coords[0];
let step = (coords[coords.len() - 1] - coords[0]) / (coords.len() - 1) as f64;
Ok(Axis { origin, step, len: coords.len() })
}
fn index(&self, q: f64) -> Option<usize> {
if self.step == 0.0 {
return None;
}
let f = (q - self.origin) / self.step; if f < -0.5 || f > self.len as f64 - 0.5 {
return None;
}
let i = (f.round() as isize).clamp(0, self.len as isize - 1);
Some(i as usize)
}
}
pub fn silence_hdf5_diagnostics() {
thread_local! {
static SILENCED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
SILENCED.with(|done| {
if done.get() {
return;
}
extern "C" {
fn H5Eset_auto2(
estack_id: i64,
func: *const std::ffi::c_void,
client_data: *mut std::ffi::c_void,
) -> i32;
}
unsafe {
H5Eset_auto2(0, std::ptr::null(), std::ptr::null_mut());
}
done.set(true);
});
}
fn normalize_lon(lon: f64) -> f64 {
if !lon.is_finite() {
return lon;
}
((lon + 180.0).rem_euclid(360.0)) - 180.0
}
pub struct DepthEnricher {
file: Mutex<netcdf::File>,
var_name: String,
lat: Axis,
lon: Axis,
column: String,
positive: bool,
}
impl DepthEnricher {
pub fn open(path: &Path, column: String, positive: bool) -> Result<Self, Box<dyn Error>> {
silence_hdf5_diagnostics();
let file = netcdf::open(path)
.map_err(|e| format!("cannot open GEBCO file {}: {e}", path.display()))?;
let lat_coords: Vec<f64> = file
.variable("lat")
.ok_or("GEBCO file has no 'lat' variable")?
.get_values(..)
.map_err(|e| format!("reading 'lat': {e}"))?;
let lon_coords: Vec<f64> = file
.variable("lon")
.ok_or("GEBCO file has no 'lon' variable")?
.get_values(..)
.map_err(|e| format!("reading 'lon': {e}"))?;
if file.variable("elevation").is_none() {
return Err("GEBCO file has no 'elevation' variable".into());
}
let lat = Axis::from_coords(&lat_coords)?;
let lon = Axis::from_coords(&lon_coords)?;
Ok(Self {
file: Mutex::new(file),
var_name: "elevation".to_string(),
lat,
lon,
column,
positive,
})
}
}
impl Enricher for DepthEnricher {
fn outputs(&self) -> Vec<OutputSpec> {
Vec::from([OutputSpec {
name: self.column.clone(),
kind: OutputKind::Float,
}])
}
fn enrich(&self, lon: f64, lat: f64) -> Vec<Value> {
silence_hdf5_diagnostics();
let value = match (self.lat.index(lat), self.lon.index(normalize_lon(lon))) {
(Some(i), Some(j)) => {
let file = self.file.lock().expect("depth file mutex poisoned");
let var = file
.variable(&self.var_name)
.expect("elevation presence checked in open");
match var.get_value::<f64, _>([i, j]) {
Ok(v) if self.positive => -v,
Ok(v) => v,
Err(_) => f64::NAN,
}
}
_ => f64::NAN,
};
Vec::from([Value::Float(value)])
}
}
pub fn run(args: DepthArgs) -> Result<(), Box<dyn Error>> {
let s: Settings = resolve(&args.common, None)?;
let data = args
.data
.ok_or("depth requires --data <GEBCO NetCDF file>")?;
let df = crate::io::read_frame(&args.common.input, args.common.in_format)?;
let out_path = args
.common
.output
.clone()
.unwrap_or_else(|| super::default_output(&args.common.input, "depth", args.common.in_format));
let enr = DepthEnricher::open(&data, args.column, args.positive)?;
run_module(&enr, df, &s, &out_path, args.common.out_format)
}