geo/algorithm/relate/geomgraph/
edge_end_bundle.rs

1use super::{CoordPos, Direction, Edge, EdgeEnd, GeometryGraph, IntersectionMatrix, Label};
2use crate::{Coord, GeoFloat};
3
4/// A collection of [`EdgeEnds`](EdgeEnd) which obey the following invariant:
5/// They originate at the same node and have the same direction.
6///
7/// This is based on [JTS's `EdgeEndBundle` as of 1.18.1](https://github.com/locationtech/jts/blob/jts-1.18.1/modules/core/src/main/java/org/locationtech/jts/operation/relate/EdgeEndBundle.java)
8#[derive(Clone, Debug)]
9pub(crate) struct EdgeEndBundle<F>
10where
11    F: GeoFloat,
12{
13    coordinate: Coord<F>,
14    edge_ends: Vec<EdgeEnd<F>>,
15}
16
17impl<F> EdgeEndBundle<F>
18where
19    F: GeoFloat,
20{
21    pub(crate) fn new(coordinate: Coord<F>) -> Self {
22        Self {
23            coordinate,
24            edge_ends: vec![],
25        }
26    }
27
28    fn edge_ends_iter(&self) -> impl Iterator<Item = &EdgeEnd<F>> {
29        self.edge_ends.iter()
30    }
31
32    fn edge_ends_iter_mut(&mut self) -> impl Iterator<Item = &mut EdgeEnd<F>> {
33        self.edge_ends.iter_mut()
34    }
35
36    pub(crate) fn insert(&mut self, edge_end: EdgeEnd<F>) {
37        self.edge_ends.push(edge_end);
38    }
39
40    pub(crate) fn into_labeled(mut self) -> LabeledEdgeEndBundle<F> {
41        let is_area = self
42            .edge_ends_iter()
43            .any(|edge_end| edge_end.label().is_area());
44
45        let mut label = if is_area {
46            Label::empty_area()
47        } else {
48            Label::empty_line_or_point()
49        };
50
51        for i in 0..2 {
52            self.compute_label_on(&mut label, i);
53            if is_area {
54                self.compute_label_side(&mut label, i, Direction::Left);
55                self.compute_label_side(&mut label, i, Direction::Right);
56            }
57        }
58
59        LabeledEdgeEndBundle {
60            label,
61            edge_end_bundle: self,
62        }
63    }
64
65    /// Compute the overall ON position for the list of EdgeEnds.
66    /// (This is essentially equivalent to computing the self-overlay of a single Geometry)
67    ///
68    /// EdgeEnds can be either on the boundary (e.g. Polygon edge)
69    /// OR in the interior (e.g. segment of a LineString)
70    /// of their parent Geometry.
71    ///
72    /// In addition, GeometryCollections use a boundary node rule to determine whether a segment is
73    /// on the boundary or not.
74    ///
75    /// Finally, in GeometryCollections it can occur that an edge is both
76    /// on the boundary and in the interior (e.g. a LineString segment lying on
77    /// top of a Polygon edge.) In this case the Boundary is given precedence.
78    ///
79    /// These observations result in the following rules for computing the ON location:
80    /// - if there are an odd number of Bdy edges, the attribute is Bdy
81    /// - if there are an even number >= 2 of Bdy edges, the attribute is Int
82    /// - if there are any Int edges, the attribute is Int
83    /// - otherwise, the attribute is None
84    ///
85    fn compute_label_on(&mut self, label: &mut Label, geom_index: usize) {
86        let mut boundary_count = 0;
87        let mut found_interior = false;
88
89        for edge_end in self.edge_ends_iter() {
90            match edge_end.label().on_position(geom_index) {
91                Some(CoordPos::OnBoundary) => {
92                    boundary_count += 1;
93                }
94                Some(CoordPos::Inside) => {
95                    found_interior = true;
96                }
97                None | Some(CoordPos::Outside) => {}
98            }
99        }
100
101        let mut position = None;
102        if found_interior {
103            position = Some(CoordPos::Inside);
104        }
105
106        if boundary_count > 0 {
107            position = Some(GeometryGraph::<'_, F>::determine_boundary(boundary_count));
108        }
109
110        if let Some(location) = position {
111            label.set_on_position(geom_index, location);
112        } else {
113            // This is technically a diversion from JTS, but I don't think we'd ever
114            // get here, unless `l.on_location` was *already* None, in which cases this is a
115            // no-op, so assert that assumption.
116            // If this assert is rightfully triggered, we may need to add a method like
117            // `l.clear_on_location(geom_index)`
118            debug_assert!(
119                label.on_position(geom_index).is_none(),
120                "diverging from JTS, which would have replaced the existing Location with None"
121            );
122        }
123    }
124
125    /// To compute the summary label for a side, the algorithm is:
126    ///     FOR all edges
127    ///       IF any edge's location is INTERIOR for the side, side location = INTERIOR
128    ///       ELSE IF there is at least one EXTERIOR attribute, side location = EXTERIOR
129    ///       ELSE  side location = NULL
130    /// Note that it is possible for two sides to have apparently contradictory information
131    /// i.e. one edge side may indicate that it is in the interior of a geometry, while
132    /// another edge side may indicate the exterior of the same geometry.  This is
133    /// not an incompatibility - GeometryCollections may contain two Polygons that touch
134    /// along an edge.  This is the reason for Interior-primacy rule above - it
135    /// results in the summary label having the Geometry interior on _both_ sides.
136    fn compute_label_side(&mut self, label: &mut Label, geom_index: usize, side: Direction) {
137        let mut position = None;
138        for edge_end in self.edge_ends_iter_mut() {
139            if edge_end.label().is_area() {
140                match edge_end.label_mut().position(geom_index, side) {
141                    Some(CoordPos::Inside) => {
142                        position = Some(CoordPos::Inside);
143                        break;
144                    }
145                    Some(CoordPos::Outside) => {
146                        position = Some(CoordPos::Outside);
147                    }
148                    None | Some(CoordPos::OnBoundary) => {}
149                }
150            }
151        }
152
153        if let Some(position) = position {
154            label.set_position(geom_index, side, position);
155        }
156    }
157}
158
159/// An [`EdgeEndBundle`] whose topological relationships have been aggregated into a single
160/// [`Label`].
161///
162/// `update_intersection_matrix` applies this aggregated topology to an `IntersectionMatrix`.
163#[derive(Clone, Debug)]
164pub(crate) struct LabeledEdgeEndBundle<F>
165where
166    F: GeoFloat,
167{
168    label: Label,
169    edge_end_bundle: EdgeEndBundle<F>,
170}
171
172impl<F> LabeledEdgeEndBundle<F>
173where
174    F: GeoFloat,
175{
176    pub fn label(&self) -> &Label {
177        &self.label
178    }
179
180    pub fn label_mut(&mut self) -> &mut Label {
181        &mut self.label
182    }
183
184    pub fn update_intersection_matrix(&self, intersection_matrix: &mut IntersectionMatrix) {
185        Edge::<F>::update_intersection_matrix(self.label(), intersection_matrix);
186    }
187
188    pub fn coordinate(&self) -> &Coord<F> {
189        &self.edge_end_bundle.coordinate
190    }
191}