Skip to main content

geometry_algorithm/
num_segments.rs

1//! `num_segments(&g)` — total edge count.
2//!
3//! Mirrors `boost::geometry::num_segments` from
4//! `boost/geometry/algorithms/num_segments.hpp`.
5//!
6//! | Kind | Segment count |
7//! |---|---|
8//! | `Point` / `MultiPoint` | `0` |
9//! | `Segment` | `1` |
10//! | `Box` | `4` (an axis-aligned rectangle has four edges) |
11//! | `Linestring` with `n` points | `n - 1` (`0` if `n < 2`) |
12//! | `Ring` with `n` points | `n - 1` if closed, `n` if open |
13//! | `Polygon` | Σ over exterior + interior rings |
14//! | `MultiLinestring` / `MultiPolygon` | sum over members |
15
16use geometry_model::{
17    Box, Linestring, MultiLinestring, MultiPoint, MultiPolygon, Point, Polygon, Ring, Segment,
18};
19use geometry_trait::{Closure, Linestring as _, Polygon as _, Ring as _};
20
21/// Total number of segments (edges) in `g`.
22///
23/// Mirrors `boost::geometry::num_segments(g)` from
24/// `boost/geometry/algorithms/num_segments.hpp`.
25#[inline]
26#[must_use]
27pub fn num_segments<G: NumSegments>(g: &G) -> usize {
28    g.num_segments()
29}
30
31/// Public-but-implementation-detail trait dispatching by concrete
32/// model type (per the LA0.T2 coherence note). One impl per
33/// `geometry-model` struct; users call [`num_segments`].
34#[doc(hidden)]
35pub trait NumSegments {
36    /// Total number of segments (edges) in `self`.
37    fn num_segments(&self) -> usize;
38}
39
40impl<T, const D: usize, Cs> NumSegments for Point<T, D, Cs>
41where
42    T: geometry_coords::CoordinateScalar,
43    Cs: geometry_cs::CoordinateSystem,
44{
45    fn num_segments(&self) -> usize {
46        0
47    }
48}
49
50impl<P: geometry_trait::Point> NumSegments for MultiPoint<P> {
51    fn num_segments(&self) -> usize {
52        0
53    }
54}
55
56impl<P: geometry_trait::Point> NumSegments for Segment<P> {
57    fn num_segments(&self) -> usize {
58        1
59    }
60}
61
62impl<P: geometry_trait::Point> NumSegments for Box<P> {
63    fn num_segments(&self) -> usize {
64        4
65    }
66}
67
68impl<P: geometry_trait::Point> NumSegments for Linestring<P> {
69    fn num_segments(&self) -> usize {
70        let n = self.points().count();
71        if n < 2 { 0 } else { n - 1 }
72    }
73}
74
75impl<P: geometry_trait::Point, const CW: bool, const CL: bool> NumSegments for Ring<P, CW, CL> {
76    fn num_segments(&self) -> usize {
77        let n = self.points().count();
78        // Fewer than two points → no segments, regardless of closure.
79        // Mirrors `num_segments.hpp:46-48` (`if (n <= 1) return 0;`).
80        if n <= 1 {
81            0
82        } else if matches!(self.closure(), Closure::Closed) {
83            n - 1
84        } else {
85            n
86        }
87    }
88}
89
90impl<P: geometry_trait::Point, const CW: bool, const CL: bool> NumSegments for Polygon<P, CW, CL> {
91    fn num_segments(&self) -> usize {
92        let mut n = NumSegments::num_segments(self.exterior());
93        for inner in self.interiors() {
94            n += NumSegments::num_segments(inner);
95        }
96        n
97    }
98}
99
100impl<L: geometry_trait::Linestring + NumSegments> NumSegments for MultiLinestring<L> {
101    fn num_segments(&self) -> usize {
102        self.0.iter().map(NumSegments::num_segments).sum()
103    }
104}
105
106impl<Pg: geometry_trait::Polygon + NumSegments> NumSegments for MultiPolygon<Pg> {
107    fn num_segments(&self) -> usize {
108        self.0.iter().map(NumSegments::num_segments).sum()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    //! Reference values from
115    //! `geometry/test/algorithms/num_segments.cpp`.
116
117    use super::num_segments;
118    use geometry_cs::Cartesian;
119    use geometry_model::{
120        Box, Linestring, MultiPoint, Point2D, Polygon, Ring, Segment, linestring, polygon,
121    };
122
123    type Pt = Point2D<f64, Cartesian>;
124    type Ls = Linestring<Pt>;
125    type Poly = Polygon<Pt>;
126
127    /// `num_segments.cpp:104` — `POINT(0 0)` → 0.
128    #[test]
129    fn point_has_no_segments() {
130        assert_eq!(num_segments(&Pt::new(0.0, 0.0)), 0);
131    }
132
133    /// `num_segments.cpp:139` — a multipoint has no edges → 0.
134    #[test]
135    fn multi_point_has_no_segments() {
136        let mp = MultiPoint(vec![Pt::new(0.0, 0.0), Pt::new(1.0, 1.0)]);
137        assert_eq!(num_segments(&mp), 0);
138    }
139
140    /// `num_segments.cpp:109` — `SEGMENT(0 0,1 1)` → 1.
141    #[test]
142    fn segment_is_one() {
143        let s = Segment::new(Pt::new(0.0, 0.0), Pt::new(1.0, 1.0));
144        assert_eq!(num_segments(&s), 1);
145    }
146
147    /// `num_segments.cpp:114` — `BOX(0 0,1 1)` → 4.
148    #[test]
149    fn box_is_four() {
150        let b = Box::from_corners(Pt::new(0.0, 0.0), Pt::new(1.0, 1.0));
151        assert_eq!(num_segments(&b), 4);
152    }
153
154    /// `num_segments.cpp:128` — `LINESTRING(0 0,0 0)` (2 points) → 1.
155    #[test]
156    fn linestring_two_points_is_one_segment() {
157        let ls: Ls = linestring![(0.0, 0.0), (1.0, 1.0)];
158        assert_eq!(num_segments(&ls), 1);
159    }
160
161    /// `num_segments.cpp:130` — a 4-point linestring → 3.
162    #[test]
163    fn linestring_n_points_is_n_minus_one() {
164        let ls: Ls = linestring![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0)];
165        assert_eq!(num_segments(&ls), 3);
166    }
167
168    /// `num_segments.cpp:177` — a closed ring stores the closing vertex
169    /// explicitly, so `n` points → `n - 1` edges. 5 points → 4 edges.
170    #[test]
171    fn closed_ring_rectangle_has_4_edges() {
172        let mut r = Ring::<Pt>::new();
173        r.push(Pt::new(0.0, 0.0));
174        r.push(Pt::new(4.0, 0.0));
175        r.push(Pt::new(4.0, 3.0));
176        r.push(Pt::new(0.0, 3.0));
177        r.push(Pt::new(0.0, 0.0));
178        assert_eq!(num_segments(&r), 4);
179    }
180
181    /// `num_segments.cpp:164` — an open ring leaves the closing edge
182    /// implicit, so `n` points → `n` edges. 4 points → 4 edges.
183    #[test]
184    fn open_ring_rectangle_has_4_edges() {
185        let mut r = Ring::<Pt, true, false>::new();
186        r.push(Pt::new(0.0, 0.0));
187        r.push(Pt::new(4.0, 0.0));
188        r.push(Pt::new(4.0, 3.0));
189        r.push(Pt::new(0.0, 3.0));
190        assert_eq!(num_segments(&r), 4);
191    }
192
193    /// `num_segments.cpp:221` —
194    /// `POLYGON((0 0,10 0,10 10,0 10,0 0),(1 1,2 1,1 2,1 1))` → 7
195    /// (closed outer 5 pts = 4 edges, closed hole 4 pts = 3 edges).
196    #[test]
197    fn polygon_with_one_hole_sums() {
198        let pg: Poly = polygon![
199            [(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0), (0.0, 0.0)],
200            [(1.0, 1.0), (2.0, 1.0), (1.0, 2.0), (1.0, 1.0)],
201        ];
202        assert_eq!(num_segments(&pg), 7);
203    }
204
205    /// `num_segments.cpp:126-127` — an empty or 1-point linestring has
206    /// no edges (the `n < 2` guard).
207    #[test]
208    fn short_linestrings_have_no_segments() {
209        let empty: Ls = linestring![];
210        assert_eq!(num_segments(&empty), 0);
211        let single: Ls = linestring![(0.0, 0.0)];
212        assert_eq!(num_segments(&single), 0);
213    }
214
215    /// `num_segments.cpp:143-156` — a multi-linestring sums its members.
216    #[test]
217    fn multi_linestring_sums_members() {
218        let mls = geometry_model::MultiLinestring::<Ls>(vec![
219            linestring![(0.0, 0.0), (1.0, 1.0)], // 1 edge
220            linestring![(2.0, 2.0), (3.0, 3.0), (4.0, 4.0), (5.0, 5.0)], // 3 edges
221        ]);
222        assert_eq!(num_segments(&mls), 4);
223    }
224
225    /// `num_segments.cpp:242-262` — a multi-polygon sums its members.
226    #[test]
227    fn multi_polygon_sums_members() {
228        let plain: Poly = polygon![[(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0), (0.0, 0.0)]]; // 4
229        let holed: Poly = polygon![
230            [(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0), (0.0, 0.0)], // 4
231            [(1.0, 1.0), (2.0, 1.0), (1.0, 2.0), (1.0, 1.0)],             // 3
232        ];
233        let mpg = geometry_model::MultiPolygon(vec![plain, holed]);
234        assert_eq!(num_segments(&mpg), 11);
235    }
236
237    #[test]
238    fn single_point_open_ring_has_no_segments() {
239        // Regression: a 1-point ring has zero segments regardless of
240        // closure. Boost guards `if (n <= 1) return 0;`
241        // (num_segments.hpp:46-48). The port previously returned 1 for
242        // the open case.
243        use geometry_model::Ring;
244        let open_one: Ring<Point2D<f64, Cartesian>, true, false> =
245            Ring::from_vec(vec![Point2D::new(0.0, 0.0)]);
246        assert_eq!(num_segments(&open_one), 0);
247    }
248}