use crate::error::{Error, Result};
struct OmercParams {
cos_lc: f64,
sin_lc: f64,
cos_l0: f64,
sin_l0: f64,
cos_th: f64,
sin_th: f64,
rk: f64,
}
fn omerc_params(
lat_0: f64,
lon_c: f64,
alpha_c: f64,
k0: f64,
semi_major: f64,
) -> Result<OmercParams> {
if !lat_0.is_finite() || !lon_c.is_finite() || !alpha_c.is_finite() {
return Err(Error::invalid_parameter("omerc", "non-finite parameter"));
}
if lat_0.abs() > std::f64::consts::FRAC_PI_2 - 1e-10 {
return Err(Error::invalid_parameter(
"omerc",
"projection centre cannot be at a pole",
));
}
if k0 <= 0.0 || !k0.is_finite() {
return Err(Error::invalid_parameter(
"omerc",
"scale factor must be positive and finite",
));
}
let theta = alpha_c - std::f64::consts::FRAC_PI_2;
Ok(OmercParams {
cos_lc: lon_c.cos(),
sin_lc: lon_c.sin(),
cos_l0: lat_0.cos(),
sin_l0: lat_0.sin(),
cos_th: theta.cos(),
sin_th: theta.sin(),
rk: semi_major * k0,
})
}
fn rotate_forward(lon: f64, lat: f64, p: &OmercParams) -> (f64, f64, f64) {
let (sin_lat, cos_lat) = lat.sin_cos();
let (sin_lon, cos_lon) = lon.sin_cos();
let px = cos_lat * cos_lon;
let py = cos_lat * sin_lon;
let pz = sin_lat;
let rx = px * p.cos_lc + py * p.sin_lc;
let ry = -px * p.sin_lc + py * p.cos_lc;
let rz = pz;
let sx = rx * p.cos_l0 + rz * p.sin_l0;
let sy = ry;
let sz = -rx * p.sin_l0 + rz * p.cos_l0;
let fx = sx;
let fy = sy * p.cos_th - sz * p.sin_th;
let fz = sy * p.sin_th + sz * p.cos_th;
(fx, fy, fz)
}
fn rotate_inverse(fx: f64, fy: f64, fz: f64, p: &OmercParams) -> (f64, f64) {
let sx = fx;
let sy = fy * p.cos_th + fz * p.sin_th;
let sz = -fy * p.sin_th + fz * p.cos_th;
let rx = sx * p.cos_l0 - sz * p.sin_l0;
let ry = sy;
let rz = sx * p.sin_l0 + sz * p.cos_l0;
let px = rx * p.cos_lc - ry * p.sin_lc;
let py = rx * p.sin_lc + ry * p.cos_lc;
let pz = rz;
let lat = pz.clamp(-1.0, 1.0).asin();
let lon = py.atan2(px);
(lon, lat)
}
pub fn oblique_mercator_forward(
lon: f64,
lat: f64,
lat_0: f64,
lon_c: f64,
alpha_c: f64,
k0: f64,
semi_major: f64,
) -> Result<(f64, f64)> {
if !lon.is_finite() || !lat.is_finite() {
return Err(Error::invalid_coordinate("omerc: non-finite input"));
}
let p = omerc_params(lat_0, lon_c, alpha_c, k0, semi_major)?;
let (fx, fy, fz) = rotate_forward(lon, lat, &p);
let lon_r = fy.atan2(fx);
let lat_r = fz.clamp(-1.0, 1.0).asin();
let x = p.rk * lon_r;
let half_pi = std::f64::consts::FRAC_PI_2;
let lat_clamped = lat_r.clamp(-half_pi + 1e-10, half_pi - 1e-10);
let y = p.rk * (half_pi * 0.5 + lat_clamped * 0.5).tan().ln();
Ok((x, y))
}
pub fn oblique_mercator_inverse(
x: f64,
y: f64,
lat_0: f64,
lon_c: f64,
alpha_c: f64,
k0: f64,
semi_major: f64,
) -> Result<(f64, f64)> {
if !x.is_finite() || !y.is_finite() {
return Err(Error::invalid_coordinate("omerc: non-finite input"));
}
let p = omerc_params(lat_0, lon_c, alpha_c, k0, semi_major)?;
let lon_r = x / p.rk;
let t = y / p.rk;
let lat_r = 2.0 * t.exp().atan() - std::f64::consts::FRAC_PI_2;
let (sin_lr, cos_lr) = lat_r.sin_cos();
let (sin_lonr, cos_lonr) = lon_r.sin_cos();
let fx = cos_lr * cos_lonr;
let fy = cos_lr * sin_lonr;
let fz = sin_lr;
let (lon, lat) = rotate_inverse(fx, fy, fz, &p);
Ok((lon, lat))
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
const R: f64 = 6_371_000.0;
const K0: f64 = 1.0;
fn deg(d: f64) -> f64 {
d.to_radians()
}
#[test]
fn test_omerc_at_centre() {
let lat_0 = deg(4.0);
let lon_c = deg(115.0);
let alpha = deg(53.31582); let (x, y) =
oblique_mercator_forward(lon_c, lat_0, lat_0, lon_c, alpha, K0, R).expect("forward ok");
assert!(x.abs() < 1.0, "x at centre: {x}");
assert!(y.abs() < 1.0, "y at centre: {y}");
}
#[test]
fn test_omerc_roundtrip() {
let lat_0 = deg(4.0);
let lon_c = deg(115.0);
let alpha = deg(53.31582);
let cases = [
(deg(115.0), deg(4.0)),
(deg(116.0), deg(5.0)),
(deg(113.5), deg(2.5)),
(deg(118.0), deg(6.0)),
(deg(110.0), deg(1.0)),
];
for (lon, lat) in cases {
let (x, y) =
oblique_mercator_forward(lon, lat, lat_0, lon_c, alpha, K0, R).expect("fwd");
let (lon2, lat2) =
oblique_mercator_inverse(x, y, lat_0, lon_c, alpha, K0, R).expect("inv");
assert!(
(lon - lon2).abs() < 1e-9,
"lon roundtrip: {:.6}° vs {:.6}° (diff: {:.2e})",
lon.to_degrees(),
lon2.to_degrees(),
(lon - lon2).abs()
);
assert!(
(lat - lat2).abs() < 1e-9,
"lat roundtrip: {:.6}° vs {:.6}° (diff: {:.2e})",
lat.to_degrees(),
lat2.to_degrees(),
(lat - lat2).abs()
);
}
}
#[test]
fn test_omerc_alpha_90_reduces_to_regular_mercator_like() {
let lat_0 = deg(0.0);
let lon_c = deg(0.0);
let alpha = deg(90.0);
let (x, y) =
oblique_mercator_forward(deg(10.0), deg(0.0), lat_0, lon_c, alpha, K0, R).expect("fwd");
assert!(x > 0.0, "x should be positive: {x}");
assert!(y.abs() < 100.0, "y should be near 0 on equator: {y}");
let (lon2, lat2) = oblique_mercator_inverse(x, y, lat_0, lon_c, alpha, K0, R).expect("inv");
assert!((deg(10.0) - lon2).abs() < 1e-9);
assert!(lat2.abs() < 1e-9);
}
#[test]
fn test_omerc_nonfinite_coord() {
let r = oblique_mercator_forward(f64::NAN, 0.0, 0.0, 0.0, 0.5, 1.0, R);
assert!(r.is_err());
let r = oblique_mercator_inverse(f64::INFINITY, 0.0, 0.0, 0.0, 0.5, 1.0, R);
assert!(r.is_err());
}
#[test]
fn test_omerc_polar_centre_rejected() {
let r = oblique_mercator_forward(0.0, 0.0, deg(90.0), 0.0, 0.5, 1.0, R);
assert!(r.is_err());
}
#[test]
fn test_omerc_negative_scale_rejected() {
let r = oblique_mercator_forward(0.0, 0.0, 0.0, 0.0, 0.5, -1.0, R);
assert!(r.is_err());
}
#[test]
fn test_omerc_symmetry_about_centre() {
let lat_0 = deg(4.0);
let lon_c = deg(115.0);
let alpha = deg(53.0);
let (x1, y1) = oblique_mercator_forward(deg(116.0), deg(5.0), lat_0, lon_c, alpha, K0, R)
.expect("fwd1");
let (x2, y2) = oblique_mercator_forward(deg(114.0), deg(3.0), lat_0, lon_c, alpha, K0, R)
.expect("fwd2");
let (l1, p1) = oblique_mercator_inverse(x1, y1, lat_0, lon_c, alpha, K0, R).expect("inv1");
let (l2, p2) = oblique_mercator_inverse(x2, y2, lat_0, lon_c, alpha, K0, R).expect("inv2");
assert!((deg(116.0) - l1).abs() < 1e-9);
assert!((deg(5.0) - p1).abs() < 1e-9);
assert!((deg(114.0) - l2).abs() < 1e-9);
assert!((deg(3.0) - p2).abs() < 1e-9);
}
#[test]
fn test_omerc_various_azimuths_roundtrip() {
let lat_0 = deg(30.0);
let lon_c = deg(0.0);
let lon = deg(5.0);
let lat = deg(32.0);
for alpha_deg in [0.0, 15.0, 30.0, 45.0, 60.0, 75.0, 90.0] {
let alpha = deg(alpha_deg);
let (x, y) =
oblique_mercator_forward(lon, lat, lat_0, lon_c, alpha, K0, R).expect("fwd");
let (lon2, lat2) =
oblique_mercator_inverse(x, y, lat_0, lon_c, alpha, K0, R).expect("inv");
assert!(
(lon - lon2).abs() < 1e-8,
"α={alpha_deg}° lon: {:.6}° vs {:.6}°",
lon.to_degrees(),
lon2.to_degrees()
);
assert!(
(lat - lat2).abs() < 1e-8,
"α={alpha_deg}° lat: {:.6}° vs {:.6}°",
lat.to_degrees(),
lat2.to_degrees()
);
}
}
}