#![forbid(unsafe_code)]
use crate::{TransBuild, TransParams};
use oxiproj_core::{Coord, IoUnits, Operation, ProjError, ProjResult};
use oxiproj_grids::{sample_grid, GridSet};
const MAX_ITER: usize = 10;
const ITER_TOL: f64 = 1e-12;
#[derive(Debug)]
struct XyzGridShift {
grid: GridSet,
}
impl Operation for XyzGridShift {
fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let lon_deg = v[0].to_degrees();
let lat_deg = v[1].to_degrees();
let shifts = sample_grid(&self.grid, lat_deg, lon_deg).ok_or(ProjError::OutsideGrid)?;
let dx_rad = shifts[0].to_radians();
let dy_rad = shifts[1].to_radians();
let dz = if shifts.len() >= 3 { shifts[2] } else { 0.0 };
Ok(Coord::new(v[0] + dx_rad, v[1] + dy_rad, v[2] + dz, v[3]))
}
fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let mut lon_r = v[0];
let mut lat_r = v[1];
for _ in 0..MAX_ITER {
let lon_deg_try = lon_r.to_degrees();
let lat_deg_try = lat_r.to_degrees();
let shifts =
sample_grid(&self.grid, lat_deg_try, lon_deg_try).ok_or(ProjError::OutsideGrid)?;
let new_lon = v[0] - shifts[0].to_radians();
let new_lat = v[1] - shifts[1].to_radians();
let dlon = (new_lon - lon_r).abs();
let dlat = (new_lat - lat_r).abs();
lon_r = new_lon;
lat_r = new_lat;
if dlon < ITER_TOL && dlat < ITER_TOL {
break;
}
}
let shifts_at_src = sample_grid(&self.grid, lat_r.to_degrees(), lon_r.to_degrees())
.ok_or(ProjError::OutsideGrid)?;
let dz = if shifts_at_src.len() >= 3 {
shifts_at_src[2]
} else {
0.0
};
Ok(Coord::new(lon_r, lat_r, v[2] - dz, v[3]))
}
fn has_inverse(&self) -> bool {
true
}
}
pub fn new(p: &TransParams) -> ProjResult<TransBuild> {
let grid_name = p.params.get_str("grids").ok_or(ProjError::MissingArg)?;
let registry = p.registry.ok_or(ProjError::FileNotFound)?;
let data = registry
.get_grid(grid_name)
.ok_or(ProjError::FileNotFound)?;
let grid = oxiproj_grids::read_geotiff(data, grid_name)?;
if grid.bands.len() < 2 {
return Err(ProjError::FileNotFound);
}
Ok(TransBuild::new(
Box::new(XyzGridShift { grid }),
IoUnits::Radians,
IoUnits::Radians,
))
}
#[cfg(test)]
mod tests {
use super::*;
use oxiproj_core::DEG_TO_RAD;
use oxiproj_grids::{GridBand, GridExtent, GridSet, GridSource};
fn make_uniform_xyz_gridset(values: &[f32]) -> GridSet {
let extent = GridExtent {
ll_lat: 0.0,
ll_lon: 0.0,
ur_lat: 1.0,
ur_lon: 1.0,
lat_inc: 1.0,
lon_inc: 1.0,
};
let bands = values
.iter()
.map(|&v| GridBand {
extent: extent.clone(),
values: vec![v; 4],
})
.collect();
GridSet {
bands,
source: GridSource::Memory,
}
}
#[test]
fn test_xyzgridshift_forward_2band() {
let gs = make_uniform_xyz_gridset(&[0.1f32, 0.2]);
let op = XyzGridShift { grid: gs };
let input = Coord::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 100.0, 0.0);
let out = op.forward_4d(input).unwrap();
let v = out.v();
let expected_lon = (0.5 + 0.1) * DEG_TO_RAD;
let expected_lat = (0.5 + 0.2) * DEG_TO_RAD;
assert!((v[0] - expected_lon).abs() < 1e-9, "lon");
assert!((v[1] - expected_lat).abs() < 1e-9, "lat");
assert!((v[2] - 100.0).abs() < 1e-6, "z unchanged for 2-band");
}
#[test]
fn test_xyzgridshift_forward_3band() {
let gs = make_uniform_xyz_gridset(&[0.1f32, 0.2, 5.0]);
let op = XyzGridShift { grid: gs };
let input = Coord::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 100.0, 0.0);
let out = op.forward_4d(input).unwrap();
assert!((out.v()[2] - 105.0).abs() < 1e-6, "z += 5.0");
}
#[test]
fn test_xyzgridshift_round_trip() {
let gs = make_uniform_xyz_gridset(&[0.05f32, 0.03, 10.0]);
let op = XyzGridShift { grid: gs };
let input = Coord::new(0.4 * DEG_TO_RAD, 0.6 * DEG_TO_RAD, 50.0, 2020.0);
let fwd = op.forward_4d(input).unwrap();
let inv = op.inverse_4d(fwd).unwrap();
let vi = inv.v();
let vi0 = input.v();
assert!((vi[0] - vi0[0]).abs() < 1e-9, "lon round-trip");
assert!((vi[1] - vi0[1]).abs() < 1e-9, "lat round-trip");
assert!((vi[2] - vi0[2]).abs() < 1e-6, "z round-trip");
}
#[test]
fn test_xyzgridshift_outside_err() {
let gs = make_uniform_xyz_gridset(&[0.1f32, 0.2]);
let op = XyzGridShift { grid: gs };
let far = Coord::new(45.0 * DEG_TO_RAD, 45.0 * DEG_TO_RAD, 0.0, 0.0);
assert_eq!(op.forward_4d(far).err(), Some(ProjError::OutsideGrid));
}
}