Skip to main content

geometry_proj/
reproject.rs

1//! Reproject a geometry from one [`Crs`] to another, in place.
2//!
3//! Bridges the geometry kernel's mutable point access to
4//! [`proj4rs::transform::transform`]. `proj4rs` drives the conversion
5//! through its [`proj4rs::transform::Transform`] trait, which
6//! hands each coordinate to a closure; this module implements that trait
7//! over any geometry that can enumerate its mutable points
8//! ([`ReprojectPoints`]).
9//!
10//! # Units
11//!
12//! `proj4rs` carries **geographic coordinates in radians**. When the
13//! source or destination CRS is geographic, callers work in radians on
14//! that side — matching `proj4rs`'s own convention. Use
15//! [`f64::to_radians`] / [`f64::to_degrees`] at the boundary if your
16//! data is in degrees.
17
18use proj4rs::transform::{Transform, TransformClosure};
19
20use geometry_cs::CoordinateSystem;
21use geometry_model::{Linestring, MultiPolygon, Point2D, Polygon, Ring};
22use geometry_trait::{Point, PointMut};
23
24use crate::crs::{Crs, CrsError};
25
26/// A geometry whose points can be enumerated and rewritten — the hook
27/// reprojection needs.
28///
29/// Implemented for the kernel's mutable areal / linear / point model
30/// types. The single method visits every point, letting the caller
31/// replace its `(x, y)`.
32pub trait ReprojectPoints {
33    /// Visit each point, replacing it with `f(x, y)`. `f` may fail; the
34    /// first failure stops the walk and is propagated.
35    ///
36    /// # Errors
37    ///
38    /// Whatever error `f` returns for the first point it rejects.
39    fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
40    where
41        F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>;
42}
43
44/// Reproject `geometry` in place from `from` to `to`.
45///
46/// # Errors
47///
48/// [`CrsError::Proj`] if `proj4rs` cannot transform a coordinate (out
49/// of the projection's valid domain, missing inverse, …). On error the
50/// geometry is **unchanged** — coordinates are transformed into a
51/// scratch buffer first and written back only after every point has
52/// succeeded.
53///
54/// # Panics
55///
56/// Panics if the write-back walk visits a different number of points
57/// than the collection walk did — unreachable in practice, since both
58/// walks traverse the same exclusively-borrowed geometry via the same
59/// [`ReprojectPoints::map_points`] impl.
60///
61/// # Examples
62///
63/// ```
64/// use geometry_cs::Cartesian;
65/// use geometry_model::Point2D;
66/// use geometry_proj::{reproject, Crs};
67/// use geometry_trait::Point as _;
68///
69/// // WGS84 lon/lat (radians) → Web Mercator metres.
70/// let wgs84 = Crs::from_epsg(4326).unwrap();
71/// let mercator = Crs::from_epsg(3857).unwrap();
72///
73/// // 0°,0° is the origin in both systems.
74/// let mut p = Point2D::<f64, Cartesian>::new(0.0, 0.0);
75/// reproject(&mut p, &wgs84, &mercator).unwrap();
76/// assert!(p.get::<0>().abs() < 1e-6);
77/// assert!(p.get::<1>().abs() < 1e-6);
78/// ```
79pub fn reproject<G: ReprojectPoints>(
80    geometry: &mut G,
81    from: &Crs,
82    to: &Crs,
83) -> Result<(), CrsError> {
84    // Phase 1: collect every coordinate without changing anything —
85    // the closure writes back the values it read.
86    let mut buf = CoordBuffer(alloc::vec::Vec::new());
87    geometry.map_points(&mut |x, y| {
88        buf.0.push((x, y));
89        Ok((x, y))
90    })?;
91
92    // Phase 2: transform the scratch buffer. On error the geometry has
93    // not been modified — the all-or-nothing guarantee.
94    proj4rs::transform::transform(from.proj(), to.proj(), &mut buf).map_err(CrsError::Proj)?;
95
96    // Phase 3: write the transformed pairs back in the same visit
97    // order. `map_points` walks a `&mut` geometry the same way both
98    // times, so the counts match by construction.
99    let mut it = buf.0.into_iter();
100    geometry.map_points(&mut |_x, _y| {
101        let (nx, ny) = it.next().expect(
102            "reproject: point count changed between walks over an exclusively borrowed geometry",
103        );
104        Ok((nx, ny))
105    })
106}
107
108/// Scratch coordinate buffer bridging `proj4rs`'s [`Transform`] trait.
109/// Phase 2 of the transactional walk: `proj4rs` transforms these pairs
110/// in place; the source geometry is not touched until every pair has
111/// succeeded.
112struct CoordBuffer(alloc::vec::Vec<(f64, f64)>);
113
114impl Transform for CoordBuffer {
115    fn transform_coordinates<F: TransformClosure>(
116        &mut self,
117        f: &mut F,
118    ) -> proj4rs::errors::Result<()> {
119        for pair in &mut self.0 {
120            let (nx, ny, _nz) = f(pair.0, pair.1, 0.0)?;
121            *pair = (nx, ny);
122        }
123        Ok(())
124    }
125}
126
127// ---- ReprojectPoints impls for the model geometries -----------------
128
129impl<Cs: CoordinateSystem> ReprojectPoints for Point2D<f64, Cs> {
130    fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
131    where
132        F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
133    {
134        let (nx, ny) = f(self.get::<0>(), self.get::<1>())?;
135        self.set::<0>(nx);
136        self.set::<1>(ny);
137        Ok(())
138    }
139}
140
141impl<Cs: CoordinateSystem> ReprojectPoints for Linestring<Point2D<f64, Cs>> {
142    fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
143    where
144        F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
145    {
146        for p in &mut self.0 {
147            p.map_points(f)?;
148        }
149        Ok(())
150    }
151}
152
153impl<Cs: CoordinateSystem> ReprojectPoints for Ring<Point2D<f64, Cs>> {
154    fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
155    where
156        F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
157    {
158        for p in &mut self.0 {
159            p.map_points(f)?;
160        }
161        Ok(())
162    }
163}
164
165impl<Cs: CoordinateSystem> ReprojectPoints for Polygon<Point2D<f64, Cs>> {
166    fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
167    where
168        F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
169    {
170        self.outer.map_points(f)?;
171        for hole in &mut self.inners {
172            hole.map_points(f)?;
173        }
174        Ok(())
175    }
176}
177
178impl<Cs: CoordinateSystem> ReprojectPoints for MultiPolygon<Polygon<Point2D<f64, Cs>>> {
179    fn map_points<F>(&mut self, f: &mut F) -> Result<(), CrsError>
180    where
181        F: FnMut(f64, f64) -> Result<(f64, f64), CrsError>,
182    {
183        for pg in &mut self.0 {
184            pg.map_points(f)?;
185        }
186        Ok(())
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::reproject;
193    use crate::crs::Crs;
194    use geometry_cs::Cartesian;
195    use geometry_model::{Linestring, Point2D};
196    use geometry_trait::Point as _;
197
198    type P = Point2D<f64, Cartesian>;
199
200    fn wgs84() -> Crs {
201        Crs::from_epsg(4326).unwrap()
202    }
203    fn mercator() -> Crs {
204        Crs::from_epsg(3857).unwrap()
205    }
206
207    #[test]
208    fn origin_maps_to_origin() {
209        let mut p = P::new(0.0, 0.0);
210        reproject(&mut p, &wgs84(), &mercator()).unwrap();
211        assert!(p.get::<0>().abs() < 1e-6);
212        assert!(p.get::<1>().abs() < 1e-6);
213    }
214
215    #[test]
216    fn known_point_web_mercator() {
217        // 45° E, 0° N in radians → Web Mercator x = R · λ.
218        // R = 6378137, λ = π/4 → x ≈ 5_009_377.
219        let mut p = P::new(45.0_f64.to_radians(), 0.0);
220        reproject(&mut p, &wgs84(), &mercator()).unwrap();
221        let expected_x = 6_378_137.0 * core::f64::consts::FRAC_PI_4;
222        assert!((p.get::<0>() - expected_x).abs() < 1.0);
223        assert!(p.get::<1>().abs() < 1e-3, "y = {}", p.get::<1>());
224    }
225
226    #[test]
227    fn round_trip_returns_original() {
228        let mut p = P::new(0.3_f64, 0.8);
229        let orig = (p.get::<0>(), p.get::<1>());
230        reproject(&mut p, &wgs84(), &mercator()).unwrap();
231        reproject(&mut p, &mercator(), &wgs84()).unwrap();
232        assert!((p.get::<0>() - orig.0).abs() < 1e-9);
233        assert!((p.get::<1>() - orig.1).abs() < 1e-9);
234    }
235
236    #[test]
237    fn reproject_a_linestring() {
238        let mut ls: Linestring<P> =
239            Linestring(vec![P::new(0.0, 0.0), P::new(0.1, 0.1), P::new(0.2, 0.05)]);
240        reproject(&mut ls, &wgs84(), &mercator()).unwrap();
241        // Origin vertex stays at the origin; the others move outward.
242        assert!(ls.0[0].get::<0>().abs() < 1e-6);
243        assert!(ls.0[1].get::<0>() > 1000.0);
244    }
245
246    /// A `Ring`'s every vertex is reprojected; the origin stays put and
247    /// the rest move outward under Web Mercator.
248    #[test]
249    fn reproject_a_ring() {
250        use geometry_model::Ring;
251        let mut ring: Ring<P> = Ring::from_vec(vec![
252            P::new(0.0, 0.0),
253            P::new(0.1, 0.0),
254            P::new(0.1, 0.1),
255            P::new(0.0, 0.0),
256        ]);
257        reproject(&mut ring, &wgs84(), &mercator()).unwrap();
258        assert!(ring.0[0].get::<0>().abs() < 1e-6); // origin fixed
259        assert!(ring.0[1].get::<0>() > 1000.0); // moved outward
260    }
261
262    /// A `Polygon`'s exterior *and* interior rings are all reprojected.
263    #[test]
264    fn reproject_a_polygon_with_hole() {
265        use geometry_model::{Polygon, Ring};
266        let outer: Ring<P> = Ring::from_vec(vec![
267            P::new(0.0, 0.0),
268            P::new(0.2, 0.0),
269            P::new(0.2, 0.2),
270            P::new(0.0, 0.0),
271        ]);
272        let hole: Ring<P> = Ring::from_vec(vec![
273            P::new(0.05, 0.05),
274            P::new(0.1, 0.05),
275            P::new(0.1, 0.1),
276            P::new(0.05, 0.05),
277        ]);
278        let mut pg = Polygon::with_inners(outer, vec![hole]);
279        reproject(&mut pg, &wgs84(), &mercator()).unwrap();
280        // Exterior origin vertex fixed at (0,0); the hole's first vertex
281        // moved outward (was 0.05 rad in each axis).
282        assert!(pg.outer.0[0].get::<0>().abs() < 1e-6);
283        assert!(pg.inners[0].0[0].get::<0>() > 1000.0);
284    }
285
286    /// A `MultiPolygon` reprojects each member polygon.
287    #[test]
288    fn reproject_a_multipolygon() {
289        use geometry_model::{MultiPolygon, Polygon, Ring};
290        let member = Polygon::with_inners(
291            Ring::from_vec(vec![
292                P::new(0.1, 0.1),
293                P::new(0.2, 0.1),
294                P::new(0.2, 0.2),
295                P::new(0.1, 0.1),
296            ]),
297            vec![],
298        );
299        let mut mpg: MultiPolygon<Polygon<P>> = MultiPolygon(vec![member.clone(), member]);
300        reproject(&mut mpg, &wgs84(), &mercator()).unwrap();
301        // Both members' first vertices moved to the same reprojected x
302        // (they were identical before), well away from the origin.
303        let x0 = mpg.0[0].outer.0[0].get::<0>();
304        let x1 = mpg.0[1].outer.0[0].get::<0>();
305        assert!(x0 > 1000.0);
306        assert!((x0 - x1).abs() < 1e-9);
307    }
308
309    #[test]
310    fn failed_reproject_leaves_geometry_unchanged() {
311        // lat = π/2 rad is the Mercator singularity: proj4rs refuses
312        // it. The FIRST point is transformable, so the old
313        // point-at-a-time walk would have rewritten it before hitting
314        // the error — the transactional walk must leave BOTH points
315        // bitwise-untouched.
316        let mut ls: Linestring<P> = Linestring(vec![
317            P::new(0.1, 0.1),
318            P::new(0.0, core::f64::consts::FRAC_PI_2),
319        ]);
320        let before: Vec<(u64, u64)> =
321            ls.0.iter()
322                .map(|p| (p.get::<0>().to_bits(), p.get::<1>().to_bits()))
323                .collect();
324        let result = reproject(&mut ls, &wgs84(), &mercator());
325        assert!(result.is_err(), "π/2 latitude must be rejected by proj4rs");
326        let after: Vec<(u64, u64)> =
327            ls.0.iter()
328                .map(|p| (p.get::<0>().to_bits(), p.get::<1>().to_bits()))
329                .collect();
330        assert_eq!(before, after, "geometry must be unchanged on Err");
331    }
332}