Skip to main content

geometry_algorithm/
transform.rs

1//! `transform(&g, &strategy)` — return a fresh geometry whose every
2//! point has been mapped through `strategy`.
3//!
4//! Mirrors `boost::geometry::transform(g_src, g_dst, strategy)` from
5//! `boost/geometry/algorithms/transform.hpp`. The Boost overload
6//! mutates `g_dst` through an out-parameter; the Rust port returns the
7//! result by value (same observable behaviour).
8
9use geometry_model::{Linestring, MultiLinestring, MultiPoint, MultiPolygon, Point, Polygon, Ring};
10use geometry_strategy::TransformStrategy;
11use geometry_trait::{
12    Linestring as LinestringTrait, MultiPoint as MultiPointTrait, Point as PointTrait,
13    Polygon as PolygonTrait, Ring as RingTrait,
14};
15
16/// Map every point of `g` through `s`, returning a new geometry of the
17/// same kind (with the strategy's output point type).
18///
19/// Mirrors `boost::geometry::transform(src, dst, strategy)` from
20/// `boost/geometry/algorithms/transform.hpp`.
21pub fn transform<G, S>(g: &G, s: &S) -> G::Output
22where
23    G: Transform<S>,
24{
25    g.transform(s)
26}
27
28/// Per-kind transform dispatch.
29#[doc(hidden)]
30pub trait Transform<S> {
31    type Output;
32    fn transform(&self, s: &S) -> Self::Output;
33}
34
35impl<T, const D: usize, Cs, S> Transform<S> for Point<T, D, Cs>
36where
37    T: geometry_coords::CoordinateScalar,
38    Cs: geometry_cs::CoordinateSystem,
39    Self: PointTrait,
40    S: TransformStrategy<Self>,
41{
42    type Output = S::Output;
43    fn transform(&self, s: &S) -> Self::Output {
44        s.transform(self)
45    }
46}
47
48impl<P, S> Transform<S> for Linestring<P>
49where
50    P: PointTrait,
51    S: TransformStrategy<P>,
52{
53    type Output = Linestring<S::Output>;
54    fn transform(&self, s: &S) -> Self::Output {
55        Linestring(self.points().map(|p| s.transform(p)).collect())
56    }
57}
58
59impl<P, S, const CW: bool, const CL: bool> Transform<S> for Ring<P, CW, CL>
60where
61    P: PointTrait,
62    S: TransformStrategy<P>,
63{
64    type Output = Ring<S::Output, CW, CL>;
65    fn transform(&self, s: &S) -> Self::Output {
66        Ring::from_vec(self.points().map(|p| s.transform(p)).collect())
67    }
68}
69
70impl<P, S, const CW: bool, const CL: bool> Transform<S> for Polygon<P, CW, CL>
71where
72    P: PointTrait,
73    S: TransformStrategy<P>,
74{
75    type Output = Polygon<S::Output, CW, CL>;
76    fn transform(&self, s: &S) -> Self::Output {
77        Polygon::with_inners(
78            self.exterior().transform(s),
79            self.interiors().map(|r| r.transform(s)).collect(),
80        )
81    }
82}
83
84impl<P, S> Transform<S> for MultiPoint<P>
85where
86    P: PointTrait,
87    S: TransformStrategy<P>,
88{
89    type Output = MultiPoint<S::Output>;
90    fn transform(&self, s: &S) -> Self::Output {
91        MultiPoint(self.points().map(|p| s.transform(p)).collect())
92    }
93}
94
95impl<L, S> Transform<S> for MultiLinestring<L>
96where
97    L: LinestringTrait + Transform<S>,
98    <L as Transform<S>>::Output: LinestringTrait,
99{
100    type Output = MultiLinestring<<L as Transform<S>>::Output>;
101    fn transform(&self, s: &S) -> Self::Output {
102        MultiLinestring(self.0.iter().map(|l| l.transform(s)).collect())
103    }
104}
105
106impl<Pg, S> Transform<S> for MultiPolygon<Pg>
107where
108    Pg: PolygonTrait + Transform<S>,
109    <Pg as Transform<S>>::Output: PolygonTrait,
110{
111    type Output = MultiPolygon<<Pg as Transform<S>>::Output>;
112    fn transform(&self, s: &S) -> Self::Output {
113        MultiPolygon(self.0.iter().map(|p| p.transform(s)).collect())
114    }
115}
116
117#[cfg(test)]
118#[allow(
119    clippy::float_cmp,
120    reason = "Affine outputs of integer inputs are exact."
121)]
122mod tests {
123    //! Reference behaviour from
124    //! `boost/geometry/test/algorithms/transform.cpp`.
125
126    use super::transform;
127    use geometry_cs::Cartesian;
128    use geometry_model::{Linestring, Point2D, linestring};
129    use geometry_strategy::Affine2;
130    use geometry_trait::{Linestring as _, Point as _};
131
132    type Pt = Point2D<f64, Cartesian>;
133
134    #[test]
135    fn point_identity_is_unchanged() {
136        let s = Affine2::<f64>::identity();
137        let q = transform(&Pt::new(3.0, 4.0), &s);
138        assert_eq!((q.get::<0>(), q.get::<1>()), (3.0, 4.0));
139    }
140
141    #[test]
142    fn linestring_translated() {
143        let ls: Linestring<Pt> = linestring![(0.0, 0.0), (1.0, 1.0)];
144        let s = Affine2::translation(10.0, 20.0);
145        let out = transform(&ls, &s);
146        let pts: Vec<(f64, f64)> = out.points().map(|p| (p.get::<0>(), p.get::<1>())).collect();
147        assert_eq!(pts, vec![(10.0, 20.0), (11.0, 21.0)]);
148    }
149
150    use geometry_model::{MultiLinestring, MultiPoint, MultiPolygon, Polygon, Ring, polygon};
151    use geometry_trait::{
152        MultiLinestring as _, MultiPoint as _, MultiPolygon as _, Polygon as _, Ring as _,
153    };
154
155    /// A `Ring` transforms every vertex through the strategy, preserving
156    /// order.
157    #[test]
158    fn ring_scaled() {
159        let r: Ring<Pt> = Ring::from_vec(vec![Pt::new(1.0, 1.0), Pt::new(2.0, 3.0)]);
160        let out = transform(&r, &Affine2::scale(2.0, 10.0));
161        let pts: Vec<(f64, f64)> = out.points().map(|p| (p.get::<0>(), p.get::<1>())).collect();
162        assert_eq!(pts, vec![(2.0, 10.0), (4.0, 30.0)]);
163    }
164
165    /// A `Polygon` transforms its exterior *and* every interior ring.
166    #[test]
167    fn polygon_translated_exterior_and_holes() {
168        let pg: Polygon<Pt> = polygon![
169            [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0)],
170            [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 1.0)]
171        ];
172        let out = transform(&pg, &Affine2::translation(100.0, 0.0));
173        // Exterior first vertex shifted by +100 in x.
174        let ext0 = out.exterior().points().next().unwrap();
175        assert_eq!((ext0.get::<0>(), ext0.get::<1>()), (100.0, 0.0));
176        // The hole is present and also shifted.
177        let hole0 = out.interiors().next().unwrap().points().next().unwrap();
178        assert_eq!((hole0.get::<0>(), hole0.get::<1>()), (101.0, 1.0));
179    }
180
181    /// A `MultiPoint` transforms each member point.
182    #[test]
183    fn multipoint_scaled() {
184        let mp = MultiPoint(vec![Pt::new(1.0, 1.0), Pt::new(2.0, 2.0)]);
185        let out = transform(&mp, &Affine2::scale(3.0, 3.0));
186        let pts: Vec<(f64, f64)> = out.points().map(|p| (p.get::<0>(), p.get::<1>())).collect();
187        assert_eq!(pts, vec![(3.0, 3.0), (6.0, 6.0)]);
188    }
189
190    /// A `MultiLinestring` transforms each member line string.
191    #[test]
192    fn multilinestring_translated() {
193        let mls: MultiLinestring<Linestring<Pt>> = MultiLinestring(vec![
194            linestring![(0.0, 0.0), (1.0, 0.0)],
195            linestring![(0.0, 1.0), (1.0, 1.0)],
196        ]);
197        let out = transform(&mls, &Affine2::translation(0.0, 5.0));
198        let first = out.linestrings().next().unwrap().points().next().unwrap();
199        assert_eq!((first.get::<0>(), first.get::<1>()), (0.0, 5.0));
200        assert_eq!(out.linestrings().count(), 2);
201    }
202
203    /// A `MultiPolygon` transforms each member polygon.
204    #[test]
205    fn multipolygon_scaled() {
206        let member: Polygon<Pt> = polygon![[(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 1.0)]];
207        let mpg: MultiPolygon<Polygon<Pt>> = MultiPolygon(vec![member.clone(), member]);
208        let out = transform(&mpg, &Affine2::scale(10.0, 10.0));
209        assert_eq!(out.polygons().count(), 2);
210        let v = out
211            .polygons()
212            .next()
213            .unwrap()
214            .exterior()
215            .points()
216            .next()
217            .unwrap();
218        assert_eq!((v.get::<0>(), v.get::<1>()), (10.0, 10.0));
219    }
220
221    // ---- Affine3: the 4×4 homogeneous 3D transforms ------------------
222
223    use geometry_model::Point3D;
224    use geometry_strategy::Affine3;
225
226    type P3 = Point3D<f64, Cartesian>;
227
228    /// The 3D identity leaves every ordinate untouched.
229    #[test]
230    fn affine3_identity_is_a_noop() {
231        let out = transform(&P3::new(2.0, 3.0, 4.0), &Affine3::identity());
232        assert_eq!(out.get::<0>(), 2.0);
233        assert_eq!(out.get::<1>(), 3.0);
234        assert_eq!(out.get::<2>(), 4.0);
235    }
236
237    /// 3D translation adds `(tx, ty, tz)` to the point.
238    #[test]
239    fn affine3_translation_shifts_all_three_axes() {
240        let t = Affine3::translation(1.0, 2.0, 3.0);
241        let out = transform(&P3::new(10.0, 20.0, 30.0), &t);
242        assert_eq!(out.get::<0>(), 11.0);
243        assert_eq!(out.get::<1>(), 22.0);
244        assert_eq!(out.get::<2>(), 33.0);
245    }
246
247    /// 3D non-uniform scale multiplies each axis independently.
248    #[test]
249    fn affine3_scale_multiplies_each_axis() {
250        let s = Affine3::scale(2.0, 3.0, 4.0);
251        let out = transform(&P3::new(1.0, 1.0, 1.0), &s);
252        assert_eq!(out.get::<0>(), 2.0);
253        assert_eq!(out.get::<1>(), 3.0);
254        assert_eq!(out.get::<2>(), 4.0);
255    }
256
257    /// A hand-built 4×4 matrix combining scale and translation applies
258    /// the full `M·[x y z 1]ᵀ` computation across all rows.
259    #[test]
260    fn affine3_full_matrix_applies_scale_and_translation() {
261        // Scale by (2,2,2) then translate by (1,2,3): the row-major
262        // matrix has diagonal 2s and the translation in the last column.
263        let mut a = Affine3::scale(2.0, 2.0, 2.0);
264        a.m[3] = 1.0;
265        a.m[7] = 2.0;
266        a.m[11] = 3.0;
267        let out = transform(&P3::new(1.0, 1.0, 1.0), &a);
268        assert_eq!(out.get::<0>(), 3.0); // 2*1 + 1
269        assert_eq!(out.get::<1>(), 4.0); // 2*1 + 2
270        assert_eq!(out.get::<2>(), 5.0); // 2*1 + 3
271    }
272}