use geonative_core::{Coord, Crs, Geometry};
use crate::epsg::{kind_for, CrsKind};
use crate::error::{ProjError, Result};
use crate::spherical_mercator;
use crate::tm;
#[derive(Debug, Clone)]
pub struct Transformer {
source: Crs,
target: Crs,
inverse: Op,
forward: Op,
}
#[derive(Debug, Clone, Copy)]
enum Op {
Identity,
SphericalMercatorForward,
SphericalMercatorInverse,
TmForward(tm::TmParams),
TmInverse(tm::TmParams),
}
impl Transformer {
pub fn from_epsg(from: u32, to: u32) -> Result<Self> {
Self::build(Crs::Epsg(from), Crs::Epsg(to))
}
pub fn from_crs(from: &Crs, to: &Crs) -> Result<Self> {
match (from, to) {
(Crs::Epsg(f), Crs::Epsg(t)) => Self::build(Crs::Epsg(*f), Crs::Epsg(*t)),
(Crs::Unknown, _) | (_, Crs::Unknown) => Err(ProjError::UnsupportedCrs(
"CRS::Unknown cannot be transformed; declare an EPSG code".into(),
)),
(Crs::Wkt(_), _) | (_, Crs::Wkt(_)) => Err(ProjError::UnsupportedCrs(
"CRS::Wkt — WKT parsing arrives in v0.2".into(),
)),
(Crs::Projjson(_), _) | (_, Crs::Projjson(_)) => Err(ProjError::UnsupportedCrs(
"CRS::Projjson — PROJJSON parsing arrives in v0.2".into(),
)),
_ => Err(ProjError::UnsupportedCrs(format!(
"unsupported CRS pair: {from:?} → {to:?}"
))),
}
}
fn build(source: Crs, target: Crs) -> Result<Self> {
let src_code = epsg_code(&source)?;
let tgt_code = epsg_code(&target)?;
let inverse = inverse_op(kind_for(src_code)?);
let forward = forward_op(kind_for(tgt_code)?);
Ok(Self {
source,
target,
inverse,
forward,
})
}
pub fn source_crs(&self) -> &Crs {
&self.source
}
pub fn target_crs(&self) -> &Crs {
&self.target
}
pub fn is_identity(&self) -> bool {
matches!(self.inverse, Op::Identity) && matches!(self.forward, Op::Identity)
}
pub fn transform(&self, c: &mut Coord) -> Result<()> {
apply(self.inverse, c)?;
apply(self.forward, c)?;
Ok(())
}
pub fn transform_slice(&self, coords: &mut [Coord]) -> Result<()> {
for c in coords {
self.transform(c)?;
}
Ok(())
}
pub fn transform_geometry(&self, g: &mut Geometry) -> Result<()> {
visit_coords(g, &mut |c| self.transform(c))
}
}
fn epsg_code(crs: &Crs) -> Result<u32> {
match crs {
Crs::Epsg(n) => Ok(*n),
other => Err(ProjError::UnsupportedCrs(format!("{other:?}"))),
}
}
fn inverse_op(kind: CrsKind) -> Op {
match kind {
CrsKind::Geographic4326 => Op::Identity,
CrsKind::WebMercator => Op::SphericalMercatorInverse,
CrsKind::TransverseMercator(p) => Op::TmInverse(p),
}
}
fn forward_op(kind: CrsKind) -> Op {
match kind {
CrsKind::Geographic4326 => Op::Identity,
CrsKind::WebMercator => Op::SphericalMercatorForward,
CrsKind::TransverseMercator(p) => Op::TmForward(p),
}
}
fn apply(op: Op, c: &mut Coord) -> Result<()> {
match op {
Op::Identity => {}
Op::SphericalMercatorForward => spherical_mercator::forward(c),
Op::SphericalMercatorInverse => spherical_mercator::inverse(c),
Op::TmForward(p) => tm::forward(&p, c),
Op::TmInverse(p) => tm::inverse(&p, c),
}
Ok(())
}
fn visit_coords<F>(g: &mut Geometry, f: &mut F) -> Result<()>
where
F: FnMut(&mut Coord) -> Result<()>,
{
use geonative_core::Geometry as G;
match g {
G::Point(c) => f(c),
G::LineString(ls) => {
for c in &mut ls.coords {
f(c)?;
}
Ok(())
}
G::Polygon(p) => {
for c in &mut p.exterior.coords {
f(c)?;
}
for ring in &mut p.holes {
for c in &mut ring.coords {
f(c)?;
}
}
Ok(())
}
G::MultiPoint(pts) => {
for c in pts {
f(c)?;
}
Ok(())
}
G::MultiLineString(lines) => {
for ls in lines {
for c in &mut ls.coords {
f(c)?;
}
}
Ok(())
}
G::MultiPolygon(polys) => {
for p in polys {
for c in &mut p.exterior.coords {
f(c)?;
}
for ring in &mut p.holes {
for c in &mut ring.coords {
f(c)?;
}
}
}
Ok(())
}
G::GeometryCollection(items) => {
for item in items {
visit_coords(item, f)?;
}
Ok(())
}
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use geonative_core::LineString;
fn approx(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() < eps
}
#[test]
fn identity_4326_to_4326() {
let t = Transformer::from_epsg(4326, 4326).unwrap();
assert!(t.is_identity());
let mut c = Coord::xy(145.0, -37.0);
t.transform(&mut c).unwrap();
assert_eq!(c.x, 145.0);
assert_eq!(c.y, -37.0);
}
#[test]
fn identity_gda2020_and_4326_equivalent() {
let t = Transformer::from_epsg(7844, 4326).unwrap();
assert!(t.is_identity());
}
#[test]
fn web_mercator_round_trip_via_pipeline() {
let to_3857 = Transformer::from_epsg(4326, 3857).unwrap();
let to_4326 = Transformer::from_epsg(3857, 4326).unwrap();
let mut c = Coord::xy(144.9631, -37.8136);
to_3857.transform(&mut c).unwrap();
to_4326.transform(&mut c).unwrap();
assert!(approx(c.x, 144.9631, 1e-9));
assert!(approx(c.y, -37.8136, 1e-9));
}
#[test]
fn mga55_to_4326_melbourne_round_trip() {
let to_mga = Transformer::from_epsg(4326, 7855).unwrap();
let to_wgs = Transformer::from_epsg(7855, 4326).unwrap();
let original = Coord::xy(144.9631, -37.8136);
let mut c = original;
to_mga.transform(&mut c).unwrap();
to_wgs.transform(&mut c).unwrap();
assert!(approx(c.x, original.x, 1e-12), "got {}", c.x);
assert!(approx(c.y, original.y, 1e-12), "got {}", c.y);
}
#[test]
fn mga56_to_utm56_two_projected_crses() {
let to_utm56 = Transformer::from_epsg(7856, 32756).unwrap();
let to_mga56 = Transformer::from_epsg(32756, 7856).unwrap();
let original = Coord::xy(500_000.0, 6_250_000.0);
let mut c = original;
to_utm56.transform(&mut c).unwrap();
to_mga56.transform(&mut c).unwrap();
assert!(approx(c.x, original.x, 0.01), "got {}", c.x);
assert!(approx(c.y, original.y, 0.01), "got {}", c.y);
}
#[test]
fn transform_geometry_walks_polygon() {
let mut g = Geometry::LineString(LineString::new(vec![
Coord::xy(144.0, -37.0),
Coord::xy(145.0, -38.0),
Coord::xy(146.0, -37.0),
]));
let t = Transformer::from_epsg(4326, 3857).unwrap();
t.transform_geometry(&mut g).unwrap();
if let Geometry::LineString(ls) = g {
for c in &ls.coords {
assert!(c.x.is_finite() && c.x.abs() > 1e6);
assert!(c.y.is_finite() && c.y.abs() > 1e6);
}
} else {
panic!("type changed");
}
}
#[test]
fn unsupported_epsg_errors() {
assert!(matches!(
Transformer::from_epsg(99999, 4326).unwrap_err(),
ProjError::UnsupportedEpsg(99999)
));
}
#[test]
fn unknown_crs_errors() {
assert!(matches!(
Transformer::from_crs(&Crs::Unknown, &Crs::Epsg(4326)).unwrap_err(),
ProjError::UnsupportedCrs(_)
));
}
}