#![forbid(unsafe_code)]
use crate::internal::cartesian_to_geodetic;
use crate::{TransBuild, TransParams};
use oxiproj_core::{Coord, Ellipsoid, IoUnits, Lpz, Operation, ProjError, ProjResult, Xyz};
use oxiproj_grids::{read_geotiff, read_geotiff_gdal_metadata, sample_grid, GridSet};
const MAX_ITERATIONS: usize = 10;
const TOL: f64 = 1e-8;
#[derive(Debug)]
struct Deformation {
grid: GridSet,
ell: Ellipsoid,
sample_e: usize,
sample_n: usize,
sample_u: usize,
fixed_dt: Option<f64>,
t_epoch: Option<f64>,
}
impl Deformation {
fn dt_for(&self, t: f64) -> ProjResult<f64> {
if let Some(dt) = self.fixed_dt {
return Ok(dt);
}
match self.t_epoch {
Some(epoch) if t.is_finite() => Ok(t - epoch),
Some(_) => Err(ProjError::MissingTime),
None => Err(ProjError::MissingArg),
}
}
fn grid_shift(&self, x: f64, y: f64, z: f64) -> ProjResult<(f64, f64, f64)> {
let geod = cartesian_to_geodetic(Xyz::new(x, y, z), &self.ell);
let lon_deg = geod.lam.to_degrees();
let lat_deg = geod.phi.to_degrees();
let s = sample_grid(&self.grid, lat_deg, lon_deg).ok_or(ProjError::OutsideGrid)?;
let e = s
.get(self.sample_e)
.copied()
.ok_or(ProjError::OutsideGrid)?
/ 1000.0;
let n = s
.get(self.sample_n)
.copied()
.ok_or(ProjError::OutsideGrid)?
/ 1000.0;
let u = s
.get(self.sample_u)
.copied()
.ok_or(ProjError::OutsideGrid)?
/ 1000.0;
let sp = geod.phi.sin();
let cp = geod.phi.cos();
let sl = geod.lam.sin();
let cl = geod.lam.cos();
let tx = -sp * cl * n - sl * e + cp * cl * u;
let ty = -sp * sl * n + cl * e + cp * sl * u;
let tz = cp * n + sp * u;
Ok((tx, ty, tz))
}
fn forward_xyz(&self, x: f64, y: f64, z: f64, dt: f64) -> ProjResult<(f64, f64, f64)> {
let (sx, sy, sz) = self.grid_shift(x, y, z)?;
Ok((x + dt * sx, y + dt * sy, z + dt * sz))
}
fn reverse_xyz(&self, ix: f64, iy: f64, iz: f64, dt: f64) -> ProjResult<(f64, f64, f64)> {
let (dx, dy, dz) = self.grid_shift(ix, iy, iz)?;
let z0 = dz;
let mut ox = ix - dt * dx;
let mut oy = iy - dt * dy;
let mut oz = iz + dt * dz;
for _ in 0..MAX_ITERATIONS {
let (ex, ey, ez) = match self.grid_shift(ox, oy, oz) {
Ok(v) => v,
Err(_) => break,
};
let difx = ox + dt * ex - ix;
let dify = oy + dt * ey - iy;
let difz = oz - dt * ez - iz;
ox += difx;
oy += dify;
oz += difz;
if difx.hypot(dify) <= TOL {
break;
}
}
oz = iz - dt * z0;
Ok((ox, oy, oz))
}
}
impl Operation for Deformation {
fn forward_3d(&self, lpz: Lpz) -> ProjResult<Xyz> {
let dt = self.fixed_dt.ok_or(ProjError::MissingTime)?;
let (x, y, z) = self.forward_xyz(lpz.lam, lpz.phi, lpz.z, dt)?;
Ok(Xyz::new(x, y, z))
}
fn inverse_3d(&self, xyz: Xyz) -> ProjResult<Lpz> {
let dt = self.fixed_dt.ok_or(ProjError::MissingTime)?;
let (x, y, z) = self.reverse_xyz(xyz.x, xyz.y, xyz.z, dt)?;
Ok(Lpz::new(x, y, z))
}
fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let dt = self.dt_for(v[3])?;
let (x, y, z) = self.forward_xyz(v[0], v[1], v[2], dt)?;
Ok(Coord::new(x, y, z, v[3]))
}
fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let dt = self.dt_for(v[3])?;
let (x, y, z) = self.reverse_xyz(v[0], v[1], v[2], dt)?;
Ok(Coord::new(x, y, z, 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 dt = p.params.get_f64("dt");
let t_epoch = p.params.get_f64("t_epoch");
let t_obs = p.params.get_f64("t_obs");
let (fixed_dt, epoch) = if let Some(dt_val) = dt {
if t_epoch.is_some() {
return Err(ProjError::MutuallyExclusiveArgs);
}
(Some(dt_val), None)
} else if let Some(t_obs_val) = t_obs {
let epoch = t_epoch.ok_or(ProjError::MissingArg)?;
(Some(t_obs_val - epoch), None)
} else if let Some(epoch) = t_epoch {
(None, Some(epoch))
} else {
return Err(ProjError::MissingArg);
};
let data = super::gridshift::resolve_grid_bytes(p.registry, grid_name)?;
let grid = read_geotiff(&data, grid_name)?;
if grid.bands.len() < 3 {
return Err(ProjError::IllegalArgValue);
}
let mut sample_e = 0usize;
let mut sample_n = 1usize;
let mut sample_u = 2usize;
if let Some(md) = read_geotiff_gdal_metadata(&data)? {
for i in 0..grid.bands.len() {
match md.metadata_item("DESCRIPTION", Some(i as i32)) {
Some("east_velocity") => sample_e = i,
Some("north_velocity") => sample_n = i,
Some("up_velocity") => sample_u = i,
_ => {}
}
}
if let Some(unit) = md.metadata_item("UNITTYPE", Some(sample_e as i32)) {
if !unit.is_empty() && unit != "millimetres per year" {
return Err(ProjError::IllegalArgValue);
}
}
}
Ok(TransBuild::new(
Box::new(Deformation {
grid,
ell: *p.ellipsoid,
sample_e,
sample_n,
sample_u,
fixed_dt,
t_epoch: epoch,
}),
IoUnits::Cartesian,
IoUnits::Cartesian,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::internal::geodetic_to_cartesian;
use oxiproj_core::DEG_TO_RAD;
use oxiproj_grids::{GridBand, GridExtent, GridSource};
fn grs80() -> Ellipsoid {
Ellipsoid::named("GRS80").expect("GRS80 ellipsoid")
}
fn make_velocity_grid(band_values: &[f32]) -> GridSet {
let extent = GridExtent {
ll_lat: -1.0,
ll_lon: -1.0,
ur_lat: 1.0,
ur_lon: 1.0,
lat_inc: 2.0,
lon_inc: 2.0,
};
let bands = band_values
.iter()
.map(|&v| GridBand {
extent: extent.clone(),
values: vec![v; 4],
semantics: Default::default(),
})
.collect();
GridSet {
bands,
source: GridSource::Memory,
}
}
fn op_fixed(band_values: &[f32], dt: f64) -> Deformation {
Deformation {
grid: make_velocity_grid(band_values),
ell: grs80(),
sample_e: 0,
sample_n: 1,
sample_u: 2,
fixed_dt: Some(dt),
t_epoch: None,
}
}
#[test]
fn enu_rotation_at_prime_meridian_equator() {
let op = op_fixed(&[100.0, 200.0, 300.0], 10.0);
let p = geodetic_to_cartesian(Lpz::new(0.0, 0.0, 0.0), &grs80());
let out = op.forward_3d(Lpz::new(p.x, p.y, p.z)).expect("forward");
assert!((out.x - (p.x + 3.0)).abs() < 1e-9, "dX(u): {}", out.x - p.x);
assert!((out.y - (p.y + 1.0)).abs() < 1e-9, "dY(e): {}", out.y - p.y);
assert!((out.z - (p.z + 2.0)).abs() < 1e-9, "dZ(n): {}", out.z - p.z);
}
#[test]
fn forward_round_trips_through_inverse() {
let op = op_fixed(&[12.0, -34.0, 56.0], 25.0);
let p = geodetic_to_cartesian(
Lpz::new(0.3 * DEG_TO_RAD, 0.4 * DEG_TO_RAD, 100.0),
&grs80(),
);
let fwd = op.forward_3d(Lpz::new(p.x, p.y, p.z)).expect("forward");
let inv = op
.inverse_3d(Xyz::new(fwd.x, fwd.y, fwd.z))
.expect("inverse");
assert!((inv.lam - p.x).abs() < 1e-3, "X {} vs {}", inv.lam, p.x);
assert!((inv.phi - p.y).abs() < 1e-3, "Y {} vs {}", inv.phi, p.y);
assert!((inv.z - p.z).abs() < 1e-3, "Z {} vs {}", inv.z, p.z);
}
#[test]
fn zero_dt_is_identity() {
let op = op_fixed(&[1000.0, 1000.0, 1000.0], 0.0);
let p = geodetic_to_cartesian(Lpz::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0), &grs80());
let out = op.forward_3d(Lpz::new(p.x, p.y, p.z)).expect("forward");
assert!((out.x - p.x).abs() < 1e-12, "X");
assert!((out.y - p.y).abs() < 1e-12, "Y");
assert!((out.z - p.z).abs() < 1e-12, "Z");
}
#[test]
fn forward_4d_uses_epoch_to_form_span() {
let epoch_op = Deformation {
grid: make_velocity_grid(&[100.0, 200.0, 300.0]),
ell: grs80(),
sample_e: 0,
sample_n: 1,
sample_u: 2,
fixed_dt: None,
t_epoch: Some(2000.0),
};
let p = geodetic_to_cartesian(Lpz::new(0.0, 0.0, 0.0), &grs80());
let out = epoch_op
.forward_4d(Coord::new(p.x, p.y, p.z, 2010.0))
.expect("forward_4d");
let v = out.v();
assert!((v[3] - 2010.0).abs() < 1e-9, "time passthrough");
assert!((v[0] - (p.x + 3.0)).abs() < 1e-9, "dX(u)");
assert!((v[1] - (p.y + 1.0)).abs() < 1e-9, "dY(e)");
assert!((v[2] - (p.z + 2.0)).abs() < 1e-9, "dZ(n)");
}
#[test]
fn epoch_mode_requires_finite_time() {
let epoch_op = Deformation {
grid: make_velocity_grid(&[1.0, 2.0, 3.0]),
ell: grs80(),
sample_e: 0,
sample_n: 1,
sample_u: 2,
fixed_dt: None,
t_epoch: Some(2000.0),
};
let err = epoch_op
.forward_4d(Coord::new(0.0, 0.0, 0.0, f64::INFINITY))
.err();
assert_eq!(err, Some(ProjError::MissingTime));
}
#[test]
fn three_d_entry_requires_fixed_span() {
let epoch_op = Deformation {
grid: make_velocity_grid(&[1.0, 2.0, 3.0]),
ell: grs80(),
sample_e: 0,
sample_n: 1,
sample_u: 2,
fixed_dt: None,
t_epoch: Some(2000.0),
};
assert_eq!(
epoch_op.forward_3d(Lpz::new(0.0, 0.0, 0.0)).err(),
Some(ProjError::MissingTime)
);
}
#[test]
fn outside_grid_errors() {
let op = op_fixed(&[1.0, 2.0, 3.0], 10.0);
let far = geodetic_to_cartesian(
Lpz::new(45.0 * DEG_TO_RAD, 45.0 * DEG_TO_RAD, 0.0),
&grs80(),
);
assert_eq!(
op.forward_3d(Lpz::new(far.x, far.y, far.z)).err(),
Some(ProjError::OutsideGrid)
);
}
use crate::TransParamLookup;
#[derive(Default)]
struct Params {
grids: Option<String>,
dt: Option<f64>,
t_epoch: Option<f64>,
t_obs: Option<f64>,
}
impl TransParamLookup for Params {
fn get_dms(&self, _key: &str) -> Option<f64> {
None
}
fn get_f64(&self, key: &str) -> Option<f64> {
match key {
"dt" => self.dt,
"t_epoch" => self.t_epoch,
"t_obs" => self.t_obs,
_ => None,
}
}
fn get_int(&self, _key: &str) -> Option<i64> {
None
}
fn get_str(&self, key: &str) -> Option<&str> {
match key {
"grids" => self.grids.as_deref(),
_ => None,
}
}
fn get_bool(&self, _key: &str) -> bool {
false
}
fn exists(&self, key: &str) -> bool {
match key {
"grids" => self.grids.is_some(),
"dt" => self.dt.is_some(),
"t_epoch" => self.t_epoch.is_some(),
"t_obs" => self.t_obs.is_some(),
_ => false,
}
}
}
fn build_new(params: Params) -> ProjResult<TransBuild> {
let ell = grs80();
new(&TransParams {
ellipsoid: &ell,
params: ¶ms,
registry: None,
})
}
#[test]
fn missing_grids_errors() {
let err = build_new(Params {
dt: Some(1.0),
..Default::default()
})
.err();
assert_eq!(err, Some(ProjError::MissingArg));
}
#[test]
fn missing_time_span_errors() {
let err = build_new(Params {
grids: Some("whatever.tif".to_string()),
..Default::default()
})
.err();
assert_eq!(err, Some(ProjError::MissingArg));
}
#[test]
fn dt_and_epoch_mutually_exclusive() {
let err = build_new(Params {
grids: Some("whatever.tif".to_string()),
dt: Some(1.0),
t_epoch: Some(2000.0),
..Default::default()
})
.err();
assert_eq!(err, Some(ProjError::MutuallyExclusiveArgs));
}
#[test]
fn t_obs_without_epoch_errors() {
let err = build_new(Params {
grids: Some("whatever.tif".to_string()),
t_obs: Some(2020.0),
..Default::default()
})
.err();
assert_eq!(err, Some(ProjError::MissingArg));
}
}