use proj4rs::transform::{Transform, TransformClosure};
use geometry_cs::CoordinateSystem;
use geometry_model::{Linestring, MultiPolygon, Point2D, Polygon, Ring};
use geometry_trait::{Point, PointMut};
use crate::crs::{Crs, CrsError};
pub trait ReprojectPoints {
fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
where
F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>;
}
pub fn reproject<G: ReprojectPoints>(
geometry: &mut G,
from: &Crs,
to: &Crs,
) -> Result<(), CrsError> {
let mut buf = CoordBuffer(alloc::vec::Vec::new());
geometry.map_points(&mut |x, y| {
buf.0.push((x, y));
Ok((x, y))
})?;
proj4rs::transform::transform(from.proj(), to.proj(), &mut buf).map_err(CrsError::Proj)?;
let mut it = buf.0.into_iter();
geometry.map_points(&mut |_x, _y| {
let (nx, ny) = it.next().expect(
"reproject: point count changed between walks over an exclusively borrowed geometry",
);
Ok((nx, ny))
})
}
struct CoordBuffer(alloc::vec::Vec<(f64, f64)>);
impl Transform for CoordBuffer {
fn transform_coordinates<F: TransformClosure>(
&mut self,
f: &mut F,
) -> proj4rs::errors::Result<()> {
for pair in &mut self.0 {
let (nx, ny, _nz) = f(pair.0, pair.1, 0.0)?;
*pair = (nx, ny);
}
Ok(())
}
}
impl<Cs: CoordinateSystem> ReprojectPoints for Point2D<f64, Cs> {
fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
where
F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
{
let (nx, ny) = f(self.get::<0>(), self.get::<1>())?;
self.set::<0>(nx);
self.set::<1>(ny);
Ok(())
}
}
impl<Cs: CoordinateSystem> ReprojectPoints for Linestring<Point2D<f64, Cs>> {
fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
where
F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
{
for p in &mut self.0 {
p.map_points(f)?;
}
Ok(())
}
}
impl<Cs: CoordinateSystem> ReprojectPoints for Ring<Point2D<f64, Cs>> {
fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
where
F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
{
for p in &mut self.0 {
p.map_points(f)?;
}
Ok(())
}
}
impl<Cs: CoordinateSystem> ReprojectPoints for Polygon<Point2D<f64, Cs>> {
fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
where
F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
{
self.outer.map_points(f)?;
for hole in &mut self.inners {
hole.map_points(f)?;
}
Ok(())
}
}
impl<Cs: CoordinateSystem> ReprojectPoints for MultiPolygon<Polygon<Point2D<f64, Cs>>> {
fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
where
F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
{
for pg in &mut self.0 {
pg.map_points(f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::reproject;
use crate::crs::Crs;
use geometry_cs::Cartesian;
use geometry_model::{Linestring, Point2D};
use geometry_trait::Point as _;
type P = Point2D<f64, Cartesian>;
fn wgs84() -> Crs {
Crs::from_epsg(4326).unwrap()
}
fn mercator() -> Crs {
Crs::from_epsg(3857).unwrap()
}
#[test]
fn origin_maps_to_origin() {
let mut p = P::new(0.0, 0.0);
reproject(&mut p, &wgs84(), &mercator()).unwrap();
assert!(p.get::<0>().abs() < 1e-6);
assert!(p.get::<1>().abs() < 1e-6);
}
#[test]
fn known_point_web_mercator() {
let mut p = P::new(45.0_f64.to_radians(), 0.0);
reproject(&mut p, &wgs84(), &mercator()).unwrap();
let expected_x = 6_378_137.0 * core::f64::consts::FRAC_PI_4;
assert!(
(p.get::<0>() - expected_x).abs() < 1.0,
"x = {}",
p.get::<0>()
);
assert!(p.get::<1>().abs() < 1e-3, "y = {}", p.get::<1>());
}
#[test]
fn round_trip_returns_original() {
let mut p = P::new(0.3_f64, 0.8);
let orig = (p.get::<0>(), p.get::<1>());
reproject(&mut p, &wgs84(), &mercator()).unwrap();
reproject(&mut p, &mercator(), &wgs84()).unwrap();
assert!((p.get::<0>() - orig.0).abs() < 1e-9);
assert!((p.get::<1>() - orig.1).abs() < 1e-9);
}
#[test]
fn reproject_a_linestring() {
let mut ls: Linestring<P> =
Linestring(vec![P::new(0.0, 0.0), P::new(0.1, 0.1), P::new(0.2, 0.05)]);
reproject(&mut ls, &wgs84(), &mercator()).unwrap();
assert!(ls.0[0].get::<0>().abs() < 1e-6);
assert!(ls.0[1].get::<0>() > 1000.0);
}
#[test]
fn failed_reproject_leaves_geometry_unchanged() {
let mut ls: Linestring<P> = Linestring(vec![
P::new(0.1, 0.1),
P::new(0.0, core::f64::consts::FRAC_PI_2),
]);
let before: Vec<(u64, u64)> =
ls.0.iter()
.map(|p| (p.get::<0>().to_bits(), p.get::<1>().to_bits()))
.collect();
let result = reproject(&mut ls, &wgs84(), &mercator());
assert!(result.is_err(), "π/2 latitude must be rejected by proj4rs");
let after: Vec<(u64, u64)> =
ls.0.iter()
.map(|p| (p.get::<0>().to_bits(), p.get::<1>().to_bits()))
.collect();
assert_eq!(before, after, "geometry must be unchanged on Err");
}
}