#![forbid(unsafe_code)]
use crate::{TransBuild, TransParams};
use oxiproj_core::{Coord, IoUnits, Operation, ProjError, ProjResult};
use oxiproj_grids::{read_gtx, read_ntv2, sample_grid, GridSet};
#[derive(Debug)]
struct Deformation {
grid: GridSet,
t_epoch: f64,
}
impl Operation for Deformation {
fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let t = v[3];
let dt = t - self.t_epoch;
let lon_deg = v[0].to_degrees();
let lat_deg = v[1].to_degrees();
let velocities = sample_grid(&self.grid, lat_deg, lon_deg).ok_or(ProjError::OutsideGrid)?;
let dvlon_deg = velocities[0] * dt;
let dvlat_deg = if velocities.len() >= 2 {
velocities[1] * dt
} else {
0.0
};
let dvz = if velocities.len() >= 3 {
velocities[2] * dt
} else {
0.0
};
Ok(Coord::new(
v[0] + dvlon_deg.to_radians(),
v[1] + dvlat_deg.to_radians(),
v[2] + dvz,
v[3],
))
}
fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let t = v[3];
let dt = t - self.t_epoch;
let lon_deg = v[0].to_degrees();
let lat_deg = v[1].to_degrees();
let velocities = sample_grid(&self.grid, lat_deg, lon_deg).ok_or(ProjError::OutsideGrid)?;
let dvlon_deg = velocities[0] * dt;
let dvlat_deg = if velocities.len() >= 2 {
velocities[1] * dt
} else {
0.0
};
let dvz = if velocities.len() >= 3 {
velocities[2] * dt
} else {
0.0
};
Ok(Coord::new(
v[0] - dvlon_deg.to_radians(),
v[1] - dvlat_deg.to_radians(),
v[2] - dvz,
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 t_epoch = p.params.get_f64("t_epoch").ok_or(ProjError::MissingArg)?;
let registry = p.registry.ok_or(ProjError::FileNotFound)?;
let data = registry
.get_grid(grid_name)
.ok_or(ProjError::FileNotFound)?;
if let Ok(grids) = read_ntv2(data, grid_name) {
if let Some(grid) = grids.into_iter().next() {
return Ok(TransBuild::new(
Box::new(Deformation { grid, t_epoch }),
IoUnits::Radians,
IoUnits::Radians,
));
}
}
let grid = read_gtx(data, grid_name)?;
Ok(TransBuild::new(
Box::new(Deformation { grid, t_epoch }),
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_velocity_grid(band_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 = band_values
.iter()
.map(|&v| GridBand {
extent: extent.clone(),
values: vec![v; 4],
})
.collect();
GridSet {
bands,
source: GridSource::Memory,
}
}
#[test]
fn test_deformation_forward_2band() {
let grid = make_velocity_grid(&[0.1f32, 0.2]);
let op = Deformation {
grid,
t_epoch: 2000.0,
};
let input = Coord::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0, 2010.0);
let out = op.forward_4d(input).unwrap();
let v = out.v();
let expected_lon = (0.5 + 1.0) * DEG_TO_RAD;
let expected_lat = (0.5 + 2.0) * DEG_TO_RAD;
assert!((v[0] - expected_lon).abs() < 1e-9, "lon");
assert!((v[1] - expected_lat).abs() < 1e-9, "lat");
}
#[test]
fn test_deformation_forward_3band() {
let grid = make_velocity_grid(&[0.0f32, 0.0, 0.5]);
let op = Deformation {
grid,
t_epoch: 2000.0,
};
let input = Coord::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 100.0, 2010.0);
let out = op.forward_4d(input).unwrap();
assert!((out.v()[2] - 105.0).abs() < 1e-6, "z should be 105");
}
#[test]
fn test_deformation_zero_dt() {
let grid = make_velocity_grid(&[1.0f32, 1.0]);
let op = Deformation {
grid,
t_epoch: 2000.0,
};
let input = Coord::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0, 2000.0);
let out = op.forward_4d(input).unwrap();
let v = out.v();
assert!((v[0] - input.v()[0]).abs() < 1e-12, "no lon shift");
assert!((v[1] - input.v()[1]).abs() < 1e-12, "no lat shift");
}
#[test]
fn test_deformation_inverse() {
let grid = make_velocity_grid(&[0.0001f32, 0.0002]);
let op = Deformation {
grid,
t_epoch: 2000.0,
};
let input = Coord::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0, 2010.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");
}
#[test]
fn test_deformation_outside_returns_err() {
let grid = make_velocity_grid(&[0.1f32, 0.2]);
let op = Deformation {
grid,
t_epoch: 2000.0,
};
let far = Coord::new(45.0 * DEG_TO_RAD, 45.0 * DEG_TO_RAD, 0.0, 2010.0);
assert_eq!(op.forward_4d(far).err(), Some(ProjError::OutsideGrid));
}
}