Skip to main content

geometry_algorithm/
convert.rs

1//! `convert(src) -> Dst` — typed conversions between equivalent
2//! geometry kinds.
3//!
4//! Mirrors `boost::geometry::convert(src, dst)` from
5//! `boost/geometry/algorithms/convert.hpp`. Boost writes the
6//! destination through an out-parameter; the Rust port returns by
7//! value. A dedicated [`Convert`] trait (rather than
8//! [`core::convert::Into`]) keeps geometry conversions opt-in and
9//! sidesteps the orphan-rule clash a blanket `From`/`Into` would hit
10//! the moment a user adapts their own types.
11//!
12//! Supported pairs:
13//!
14//! * `Box`     → `Polygon`     — the rectangle's 5-point closed ring
15//! * `Ring`    → `Polygon`     — the ring becomes the exterior
16//! * `Ring`    → `Linestring`  — copy the point sequence
17//! * `Segment` → `Linestring`  — two-point `(start, end)`
18//! * `Point`, `Linestring`, `Polygon` → single-member multi
19//!
20//! `Linestring` → `Ring` is intentionally NOT shipped — the caller
21//! must assert closure and orientation for the destination, which
22//! Boost forces through template arguments anyway. Use
23//! `Ring::from_vec(ls.0.clone())` (then [`correct`](fn@crate::correct))
24//! if you need it.
25
26use geometry_model::{
27    Box, Linestring, MultiLinestring, MultiPoint, MultiPolygon, Polygon, Ring, Segment,
28};
29use geometry_trait::{Point as PointTrait, PointMut};
30
31/// Convert `src` into the destination kind `Dst`.
32///
33/// Mirrors `boost::geometry::convert(src, dst)` from
34/// `boost/geometry/algorithms/convert.hpp`. The destination is
35/// inferred from the call-site annotation:
36///
37/// ```ignore
38/// let pg: Polygon<P> = convert(&bx);
39/// ```
40#[must_use]
41pub fn convert<Src, Dst>(src: &Src) -> Dst
42where
43    Src: Convert<Dst>,
44{
45    src.convert()
46}
47
48/// Per-pair conversion dispatch. One impl per `(Src, Dst)` pair the
49/// port supports; the free [`convert`] function is the public entry.
50#[doc(hidden)]
51pub trait Convert<Dst> {
52    fn convert(&self) -> Dst;
53}
54
55/// `Box` → `Polygon`: the rectangle's four corners, wound in the
56/// destination's declared order from the minimum corner and closed back
57/// onto it when the destination declares closure — a 5-point ring for
58/// the default `Polygon`, 4 points for an open one.
59///
60/// Mirrors the `box → polygon` arm of
61/// `boost/geometry/algorithms/convert.hpp` (`box_to_range`, which
62/// honours the target's order and closure). Only the first two
63/// dimensions participate — a box is a planar rectangle — so a further
64/// dimension of each corner keeps the point type's default value.
65impl<P, const CW: bool, const CL: bool> Convert<Polygon<P, CW, CL>> for Box<P>
66where
67    P: PointMut + Default,
68{
69    fn convert(&self) -> Polygon<P, CW, CL> {
70        let min_x = self.min().get::<0>();
71        let min_y = self.min().get::<1>();
72        let max_x = self.max().get::<0>();
73        let max_y = self.max().get::<1>();
74        let corner = |x: P::Scalar, y: P::Scalar| {
75            let mut p = P::default();
76            p.set::<0>(x);
77            p.set::<1>(y);
78            p
79        };
80        // Clockwise: (minx, miny) → (minx, maxy) → (maxx, maxy) → (maxx, miny).
81        let mut ring = if CW {
82            alloc::vec![
83                corner(min_x, min_y),
84                corner(min_x, max_y),
85                corner(max_x, max_y),
86                corner(max_x, min_y),
87            ]
88        } else {
89            alloc::vec![
90                corner(min_x, min_y),
91                corner(max_x, min_y),
92                corner(max_x, max_y),
93                corner(min_x, max_y),
94            ]
95        };
96        if CL {
97            ring.push(corner(min_x, min_y));
98        }
99        Polygon::new(Ring::<P, CW, CL>::from_vec(ring))
100    }
101}
102
103/// `Ring` → `Polygon`: the ring becomes the exterior, no holes.
104///
105/// Mirrors the `ring → polygon` arm of
106/// `boost/geometry/algorithms/convert.hpp`.
107impl<P, const CW: bool, const CL: bool> Convert<Polygon<P, CW, CL>> for Ring<P, CW, CL>
108where
109    P: PointTrait + Copy,
110{
111    fn convert(&self) -> Polygon<P, CW, CL> {
112        Polygon::new(Ring::from_vec(self.0.clone()))
113    }
114}
115
116/// `Ring` → `Linestring`: copy the point sequence, dropping the
117/// closure / orientation type information.
118///
119/// Mirrors the `ring → linestring` arm of
120/// `boost/geometry/algorithms/convert.hpp`.
121impl<P, const CW: bool, const CL: bool> Convert<Linestring<P>> for Ring<P, CW, CL>
122where
123    P: PointTrait + Copy,
124{
125    fn convert(&self) -> Linestring<P> {
126        Linestring(self.0.clone())
127    }
128}
129
130/// `Segment` → `Linestring`: a two-point `(start, end)` linestring.
131///
132/// Mirrors the `segment → linestring` arm of
133/// `boost/geometry/algorithms/convert.hpp`.
134impl<P> Convert<Linestring<P>> for Segment<P>
135where
136    P: PointTrait + Copy,
137{
138    fn convert(&self) -> Linestring<P> {
139        Linestring(alloc::vec![*self.start(), *self.end()])
140    }
141}
142
143/// `Point` → `MultiPoint`: wrap as a single-member multi.
144///
145/// Mirrors the `point → multi_point` arm of
146/// `boost/geometry/algorithms/convert.hpp`.
147impl<P> Convert<MultiPoint<P>> for P
148where
149    P: PointTrait + Copy,
150{
151    fn convert(&self) -> MultiPoint<P> {
152        MultiPoint(alloc::vec![*self])
153    }
154}
155
156/// `Linestring` → `MultiLinestring`: wrap as a single-member multi.
157///
158/// Mirrors the `linestring → multi_linestring` arm of
159/// `boost/geometry/algorithms/convert.hpp`.
160impl<P> Convert<MultiLinestring<Linestring<P>>> for Linestring<P>
161where
162    P: PointTrait + Copy,
163{
164    fn convert(&self) -> MultiLinestring<Linestring<P>> {
165        MultiLinestring(alloc::vec![self.clone()])
166    }
167}
168
169/// `Polygon` → `MultiPolygon`: wrap as a single-member multi.
170///
171/// Mirrors the `polygon → multi_polygon` arm of
172/// `boost/geometry/algorithms/convert.hpp`.
173impl<P, const CW: bool, const CL: bool> Convert<MultiPolygon<Polygon<P, CW, CL>>>
174    for Polygon<P, CW, CL>
175where
176    P: PointTrait + Copy,
177{
178    fn convert(&self) -> MultiPolygon<Polygon<P, CW, CL>> {
179        MultiPolygon(alloc::vec![self.clone()])
180    }
181}
182
183#[cfg(test)]
184#[allow(
185    clippy::float_cmp,
186    reason = "Converted corner coordinates are exact literals."
187)]
188mod tests {
189    //! Reference behaviour from
190    //! `boost/geometry/test/algorithms/convert.cpp` — a box becomes a
191    //! 5-point closed polygon over the same corners; a segment becomes
192    //! a two-point linestring; a point wraps into a single-member
193    //! multi-point.
194
195    use super::convert;
196    use geometry_cs::Cartesian;
197    use geometry_model::{Box, Linestring, MultiPoint, Point2D, Polygon, Segment};
198    use geometry_trait::{Point as _, Polygon as _, Ring as _};
199
200    type Pt = Point2D<f64, Cartesian>;
201
202    // convert.cpp — Box → Polygon yields the rectangle's closed ring.
203    #[test]
204    fn box_to_polygon_five_points_and_corners() {
205        let b: Box<Pt> = Box::from_corners(Pt::new(0., 0.), Pt::new(4., 3.));
206        let pg: Polygon<Pt> = convert(&b);
207        let pts: alloc::vec::Vec<(f64, f64)> = pg
208            .exterior()
209            .points()
210            .map(|p| (p.get::<0>(), p.get::<1>()))
211            .collect();
212        // Closed ring: 5 points, first == last, all four corners present.
213        assert_eq!(pts.len(), 5);
214        assert_eq!(pts[0], (0., 0.));
215        assert_eq!(pts[4], (0., 0.));
216        assert!(pts.contains(&(0., 3.)));
217        assert!(pts.contains(&(4., 3.)));
218        assert!(pts.contains(&(4., 0.)));
219    }
220
221    // convert.cpp — Segment → Linestring keeps both endpoints.
222    #[test]
223    fn segment_to_linestring_endpoints() {
224        let s = Segment::new(Pt::new(0., 0.), Pt::new(3., 4.));
225        let ls: Linestring<Pt> = convert(&s);
226        assert_eq!(ls.0.len(), 2);
227        assert_eq!(ls.0[0].get::<0>(), 0.);
228        assert_eq!(ls.0[1].get::<0>(), 3.);
229    }
230
231    #[test]
232    fn point_to_multi_point_single_member() {
233        let mp: MultiPoint<Pt> = convert(&Pt::new(1., 2.));
234        assert_eq!(mp.0.len(), 1);
235        assert_eq!(mp.0[0].get::<0>(), 1.);
236    }
237
238    // convert.cpp — Ring → Polygon: the ring becomes a hole-free
239    // exterior, vertices copied verbatim.
240    #[test]
241    fn ring_to_polygon_becomes_hole_free_exterior() {
242        use geometry_model::Ring;
243        let ring: Ring<Pt> = Ring::from_vec(vec![
244            Pt::new(0., 0.),
245            Pt::new(1., 0.),
246            Pt::new(1., 1.),
247            Pt::new(0., 0.),
248        ]);
249        let pg: Polygon<Pt> = convert(&ring);
250        assert_eq!(pg.exterior().points().count(), 4);
251        assert_eq!(pg.interiors().count(), 0);
252        let first = pg.exterior().points().next().unwrap();
253        assert_eq!((first.get::<0>(), first.get::<1>()), (0., 0.));
254    }
255
256    // convert.cpp — Ring → Linestring copies the point sequence.
257    #[test]
258    fn ring_to_linestring_copies_points() {
259        use geometry_model::Ring;
260        let ring: Ring<Pt> =
261            Ring::from_vec(vec![Pt::new(2., 3.), Pt::new(4., 5.), Pt::new(2., 3.)]);
262        let ls: Linestring<Pt> = convert(&ring);
263        assert_eq!(ls.0.len(), 3);
264        assert_eq!((ls.0[1].get::<0>(), ls.0[1].get::<1>()), (4., 5.));
265    }
266
267    // convert.cpp — Linestring → MultiLinestring wraps a single member.
268    #[test]
269    fn linestring_to_multi_linestring_single_member() {
270        use geometry_model::MultiLinestring;
271        let ls: Linestring<Pt> = Linestring(vec![Pt::new(0., 0.), Pt::new(1., 1.)]);
272        let mls: MultiLinestring<Linestring<Pt>> = convert(&ls);
273        assert_eq!(mls.0.len(), 1);
274        assert_eq!(mls.0[0].0.len(), 2);
275    }
276
277    // convert.cpp — Polygon → MultiPolygon wraps a single member.
278    #[test]
279    fn polygon_to_multi_polygon_single_member() {
280        use geometry_model::{MultiPolygon, Ring};
281        let pg: Polygon<Pt> = Polygon::new(Ring::from_vec(vec![
282            Pt::new(0., 0.),
283            Pt::new(1., 0.),
284            Pt::new(1., 1.),
285            Pt::new(0., 0.),
286        ]));
287        let mpg: MultiPolygon<Polygon<Pt>> = convert(&pg);
288        assert_eq!(mpg.0.len(), 1);
289        assert_eq!(mpg.0[0].exterior().points().count(), 4);
290    }
291
292    /// The ring is wound clockwise from the minimum corner exactly as
293    /// Boost emits it (`(0 0,0 3,4 3,4 0,0 0)`), not merely over the
294    /// same corners.
295    #[test]
296    fn box_to_polygon_is_wound_clockwise() {
297        let b: Box<Pt> = Box::from_corners(Pt::new(0., 0.), Pt::new(4., 3.));
298        let pg: Polygon<Pt> = convert(&b);
299        let pts: alloc::vec::Vec<(f64, f64)> = pg
300            .exterior()
301            .points()
302            .map(|p| (p.get::<0>(), p.get::<1>()))
303            .collect();
304        assert_eq!(
305            pts,
306            alloc::vec![(0., 0.), (0., 3.), (4., 3.), (4., 0.), (0., 0.)]
307        );
308        assert_eq!(crate::area::ring_area(pg.exterior()), 12.0);
309    }
310
311    /// A counter-clockwise-declared destination receives a
312    /// counter-clockwise ring (positive area under its own convention).
313    #[test]
314    fn box_to_ccw_polygon_is_wound_counter_clockwise() {
315        let b: Box<Pt> = Box::from_corners(Pt::new(0., 0.), Pt::new(4., 3.));
316        let pg: Polygon<Pt, false, true> = convert(&b);
317        let pts: alloc::vec::Vec<(f64, f64)> = pg
318            .exterior()
319            .points()
320            .map(|p| (p.get::<0>(), p.get::<1>()))
321            .collect();
322        assert_eq!(
323            pts,
324            alloc::vec![(0., 0.), (4., 0.), (4., 3.), (0., 3.), (0., 0.)]
325        );
326        assert_eq!(crate::area::ring_area(pg.exterior()), 12.0);
327    }
328
329    /// An open-declared destination receives the four corners without a
330    /// closing duplicate.
331    #[test]
332    fn box_to_open_polygon_has_four_stored_points() {
333        let b: Box<Pt> = Box::from_corners(Pt::new(0., 0.), Pt::new(4., 3.));
334        let pg: Polygon<Pt, true, false> = convert(&b);
335        assert_eq!(pg.exterior().points().count(), 4);
336        assert_eq!(crate::area::ring_area(pg.exterior()), 12.0);
337    }
338
339    /// A 3-D box converts too: the planar rectangle is drawn in `x`/`y`
340    /// and the third ordinate keeps the point's default.
341    #[test]
342    fn three_d_box_to_polygon_uses_the_first_two_dimensions() {
343        use geometry_model::Point3D;
344        type P3 = Point3D<f64, Cartesian>;
345        let b: Box<P3> = Box::from_corners(P3::new(0., 0., 0.), P3::new(4., 3., 1.));
346        let pg: Polygon<P3> = convert(&b);
347        assert_eq!(pg.exterior().points().count(), 5);
348        assert!(pg.exterior().points().all(|p| p.get::<2>() == 0.0));
349        assert_eq!(crate::area::ring_area(pg.exterior()), 12.0);
350    }
351}