use crate::{TransBuild, TransParams};
use oxiproj_core::{Coord, IoUnits, Lpz, Operation, ProjResult, Xyz, DEG_TO_RAD};
const ARCSEC_TO_RAD: f64 = DEG_TO_RAD / 3600.0;
#[derive(Debug)]
struct VertOffset {
lam0: f64,
phi0: f64,
slope_lat: f64,
slope_lon: f64,
zoff: f64,
rho0: f64,
nu0: f64,
}
impl VertOffset {
fn forward_offset(&self, phi: f64, lam: f64) -> f64 {
let dlam = lam - self.lam0;
self.zoff
+ self.slope_lat * self.rho0 * (phi - self.phi0)
+ self.slope_lon * self.nu0 * dlam * phi.cos()
}
}
impl Operation for VertOffset {
fn forward_3d(&self, lpz: Lpz) -> ProjResult<Xyz> {
let off = self.forward_offset(lpz.phi, lpz.lam);
Ok(Xyz::new(lpz.lam, lpz.phi, lpz.z + off))
}
fn inverse_3d(&self, xyz: Xyz) -> ProjResult<Lpz> {
let off = self.forward_offset(xyz.y, xyz.x);
Ok(Lpz::new(xyz.x, xyz.y, xyz.z - off))
}
fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let o = self.forward_3d(Lpz::new(v[0], v[1], v[2]))?;
Ok(Coord::new(o.x, o.y, o.z, v[3]))
}
fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
let o = self.inverse_3d(Xyz::new(v[0], v[1], v[2]))?;
Ok(Coord::new(o.lam, o.phi, o.z, v[3]))
}
fn has_inverse(&self) -> bool {
true
}
}
pub fn new(p: &TransParams) -> ProjResult<TransBuild> {
let pa = p.params;
let phi0 = pa.get_dms("lat_0").unwrap_or(0.0);
let lam0 = pa.get_dms("lon_0").unwrap_or(0.0);
let zoff = pa.get_f64("dh").unwrap_or(0.0);
let slope_lat = pa.get_f64("slope_lat").unwrap_or(0.0) * ARCSEC_TO_RAD;
let slope_lon = pa.get_f64("slope_lon").unwrap_or(0.0) * ARCSEC_TO_RAD;
let a = p.ellipsoid.a;
let es = p.ellipsoid.es;
let sinlat0 = phi0.sin();
let one_minus_es_sinlat0_square = 1.0 - es * (sinlat0 * sinlat0);
let rho0 = a * (1.0 - es) / (one_minus_es_sinlat0_square * one_minus_es_sinlat0_square.sqrt());
let nu0 = a / one_minus_es_sinlat0_square.sqrt();
let op = VertOffset {
lam0,
phi0,
slope_lat,
slope_lon,
zoff,
rho0,
nu0,
};
Ok(TransBuild::new(
Box::new(op),
IoUnits::Radians,
IoUnits::Radians,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TransParamLookup;
use oxiproj_core::{Ellipsoid, DEG_TO_RAD};
use std::collections::HashMap;
struct ProjParamLookup {
dms: HashMap<&'static str, f64>,
f64s: HashMap<&'static str, f64>,
}
impl TransParamLookup for ProjParamLookup {
fn get_dms(&self, key: &str) -> Option<f64> {
self.dms.get(key).copied()
}
fn get_f64(&self, key: &str) -> Option<f64> {
self.f64s.get(key).copied()
}
fn get_int(&self, _key: &str) -> Option<i64> {
None
}
fn get_str(&self, _key: &str) -> Option<&str> {
None
}
fn get_bool(&self, _key: &str) -> bool {
false
}
fn exists(&self, key: &str) -> bool {
self.dms.contains_key(key) || self.f64s.contains_key(key)
}
}
fn build(dms: &[(&'static str, f64)], f64s: &[(&'static str, f64)]) -> TransBuild {
let ell = Ellipsoid::named("WGS84").unwrap();
let params = ProjParamLookup {
dms: dms.iter().copied().collect(),
f64s: f64s.iter().copied().collect(),
};
let pp = TransParams {
ellipsoid: &ell,
params: ¶ms,
registry: None,
};
new(&pp).unwrap()
}
#[test]
fn cct_origin_point() {
let b = build(
&[("lat_0", 55.0 * DEG_TO_RAD), ("lon_0", 12.0 * DEG_TO_RAD)],
&[("dh", 1.5), ("slope_lat", 0.1), ("slope_lon", 0.2)],
);
let out = b
.operation
.forward_3d(Lpz::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 100.0))
.unwrap();
assert!((out.z - 101.5).abs() < 1e-6, "z = {}", out.z);
assert!((out.x - 12.0 * DEG_TO_RAD).abs() < 1e-12, "x = {}", out.x);
assert!((out.y - 55.0 * DEG_TO_RAD).abs() < 1e-12, "y = {}", out.y);
}
#[test]
fn slope_round_trip() {
let b = build(
&[("lat_0", 55.0 * DEG_TO_RAD), ("lon_0", 12.0 * DEG_TO_RAD)],
&[("dh", 1.5), ("slope_lat", 0.1), ("slope_lon", 0.2)],
);
let input = Lpz::new(12.0 * DEG_TO_RAD, 56.0 * DEG_TO_RAD, 100.0);
let fwd = b.operation.forward_3d(input).unwrap();
let offset = fwd.z - 100.0;
assert!(
(offset - 1.5).abs() > 1e-9,
"slope_lat term not exercised: offset = {offset}"
);
let back = b
.operation
.inverse_3d(Xyz::new(fwd.x, fwd.y, fwd.z))
.unwrap();
assert!((back.z - input.z).abs() < 1e-9, "z = {}", back.z);
}
}