geo/algorithm/sweep/
line_or_point.rs

1use std::{cmp::Ordering, ops::Deref};
2
3use super::SweepPoint;
4use crate::{
5    line_intersection::line_intersection, Coord, GeoFloat, GeoNum, Kernel, Line, LineIntersection,
6};
7
8/// Either a line segment or a point.
9///
10/// The coordinates are ordered (see [`SweepPoint`]) and a line
11/// segment must have distinct points (use the `Point` variant if the
12/// coordinates are the equal).
13#[derive(Clone, Copy)]
14pub struct LineOrPoint<T: GeoNum> {
15    left: SweepPoint<T>,
16    right: SweepPoint<T>,
17}
18
19impl<T: GeoNum> std::fmt::Debug for LineOrPoint<T> {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        f.debug_tuple(if self.is_line() { "LPt" } else { "Pt" })
22            .field(&self.left.x_y())
23            .field(&self.right.x_y())
24            .finish()
25    }
26}
27
28impl<T: GeoNum> From<SweepPoint<T>> for LineOrPoint<T> {
29    fn from(pt: SweepPoint<T>) -> Self {
30        Self {
31            left: pt,
32            right: pt,
33        }
34    }
35}
36
37impl<T: GeoNum> From<(SweepPoint<T>, SweepPoint<T>)> for LineOrPoint<T> {
38    fn from(pt: (SweepPoint<T>, SweepPoint<T>)) -> Self {
39        let (start, end) = pt;
40        match start.cmp(&end) {
41            Ordering::Less => Self {
42                left: start,
43                right: end,
44            },
45            _ => Self {
46                left: end,
47                right: start,
48            },
49        }
50    }
51}
52
53/// Convert from a [`Line`] ensuring end point ordering.
54impl<T: GeoNum> From<Line<T>> for LineOrPoint<T> {
55    fn from(l: Line<T>) -> Self {
56        let start: SweepPoint<T> = l.start.into();
57        let end = l.end.into();
58        (start, end).into()
59    }
60}
61
62/// Convert from a [`Coord`]
63impl<T: GeoNum> From<Coord<T>> for LineOrPoint<T> {
64    fn from(c: Coord<T>) -> Self {
65        Self {
66            left: c.into(),
67            right: c.into(),
68        }
69    }
70}
71
72impl<T: GeoNum> LineOrPoint<T> {
73    /// Checks if the variant is a line.
74    #[inline]
75    pub fn is_line(&self) -> bool {
76        self.left != self.right
77    }
78
79    /// Return a [`Line`] representation of self.
80    #[inline]
81    pub fn line(&self) -> Line<T> {
82        Line::new(*self.left, *self.right)
83    }
84
85    #[inline]
86    pub fn left(&self) -> SweepPoint<T> {
87        self.left
88    }
89
90    #[inline]
91    pub fn right(&self) -> SweepPoint<T> {
92        self.right
93    }
94
95    #[cfg(test)]
96    pub fn coords_equal(&self, other: &LineOrPoint<T>) -> bool {
97        self.is_line() == other.is_line() && self.end_points() == other.end_points()
98    }
99
100    #[inline]
101    pub fn end_points(&self) -> (SweepPoint<T>, SweepPoint<T>) {
102        (self.left, self.right)
103    }
104
105    pub fn new(left: SweepPoint<T>, right: SweepPoint<T>) -> Self {
106        Self { left, right }
107    }
108}
109
110/// Equality based on ordering defined for segments as per algorithm.
111impl<T: GeoNum> PartialEq for LineOrPoint<T> {
112    #[inline]
113    fn eq(&self, other: &Self) -> bool {
114        self.partial_cmp(other) == Some(Ordering::Equal)
115    }
116}
117
118/// Ordering defined for segments as per algorithm.
119///
120/// Requires the following conditions:
121///
122/// 1. If comparing two lines, both the left ends must be strictly
123/// smaller than both right ends.
124///
125/// 2. A point is treated as a infinitesimal small vertical segment
126/// centered at its coordinates.
127impl<T: GeoNum> PartialOrd for LineOrPoint<T> {
128    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
129        match (self.is_line(), other.is_line()) {
130            (false, false) => {
131                if self.left == other.left {
132                    Some(Ordering::Equal)
133                } else {
134                    // Unequal points do not satisfy pre-condition and
135                    // can't be ordered.
136                    None
137                }
138            }
139            (false, true) => other.partial_cmp(self).map(Ordering::reverse),
140            (true, false) => {
141                let (p, q) = self.end_points();
142                let r = other.left;
143                if r > q || p > r {
144                    return None;
145                }
146                Some(
147                    T::Ker::orient2d(*p, *q, *r)
148                        .as_ordering()
149                        .then(Ordering::Greater),
150                )
151            }
152            (true, true) => {
153                let (p1, q1) = self.end_points();
154                let (p2, q2) = other.end_points();
155                if p1 > p2 {
156                    return other.partial_cmp(self).map(Ordering::reverse);
157                }
158                if p1 >= q2 || p2 >= q1 {
159                    return None;
160                }
161
162                // Assertion: p1 <= p2
163                // Assertion: pi < q_j
164                Some(
165                    T::Ker::orient2d(*p1, *q1, *p2)
166                        .as_ordering()
167                        .then_with(|| T::Ker::orient2d(*p1, *q1, *q2).as_ordering()),
168                )
169            }
170        }
171    }
172}
173
174impl<T: GeoFloat> LineOrPoint<T> {
175    /// Intersect a line with self and return a point, a overlapping segment or `None`.
176    ///
177    /// The `other` argument must be a line variant (debug builds will panic otherwise).
178    pub fn intersect_line(&self, other: &Self) -> Option<Self> {
179        debug_assert!(other.is_line(), "tried to intersect with a point variant!");
180
181        let line = other.line();
182        if !self.is_line() {
183            let p = self.left;
184            use crate::Intersects;
185            if line.intersects(&*p) {
186                Some(*self)
187            } else {
188                None
189            }
190        } else {
191            line_intersection(self.line(), line).map(|l| match l {
192                LineIntersection::SinglePoint {
193                    intersection,
194                    is_proper,
195                } => {
196                    let mut pt = intersection;
197                    if is_proper && (&pt == self.left.deref()) {
198                        if self.left.x == self.right.x {
199                            pt.y = pt.y.next_after(T::infinity());
200                        } else {
201                            pt.x = pt.x.next_after(T::infinity());
202                        }
203                    }
204                    pt.into()
205                }
206                LineIntersection::Collinear { intersection } => intersection.into(),
207            })
208        }
209    }
210
211    pub fn intersect_line_ordered(&self, other: &Self) -> Option<Self> {
212        let ord = self.partial_cmp(other);
213        match self.intersect_line(other) {
214            Some(lp) if !lp.is_line() => {
215                // NOTE: A key issue with using non-exact numbers (f64, etc.) in
216                // this algo. is that line-intersection may return
217                // counter-intuitive points.
218                //
219                // Specifically, this causes two issues:
220                //
221                // 1. The point of intersection r lies between the end-points in
222                // the lexicographic ordering. However, with finite repr., the
223                // line (1, 1) - (1 + eps, -1), where eps is ulp(1), does not
224                // admit r that lies between the end-points. Further, the
225                // end-points may be a very bad approximation to the actual
226                // intersection points (eg. intersect with x-axis).
227                //
228                // We detect and force r to be greater than both end-points; the
229                // other case is not easy to handle as the sweep has already
230                // progressed to a p strictly > r already.
231                //
232                // 2. The more severe issue is that in general r may not lie
233                // exactly on the line. Thus, with the segment stored on the
234                // active-segments tree (B-Tree / Splay), this may in adverse
235                // cases, cause the ordering between the segments to be
236                // incorrect, hence invalidating the segments. This is not easy
237                // to correct without a intrusive data-structure built
238                // specifically for this algo., that can track the neighbors of
239                // tree-nodes, and fix / report this issue. The crate
240                // `btree-slab` seems like a great starting point.
241                let pt = lp.left;
242                let (mut x, y) = pt.x_y();
243
244                let c = self.left;
245                if x == c.x && y < c.y {
246                    x = x.next_after(T::infinity());
247                }
248
249                let pt: SweepPoint<_> = Coord { x, y }.into();
250                debug_assert!(
251                    pt >= self.left,
252                    "line intersection before first line: {pt:?}\n\tLine({lp1:?} - {lp2:?}) X Line({lp3:?} - {lp4:?})",
253                    lp1 = self.left,
254                    lp2 = self.right,
255                    lp3 = other.left,
256                    lp4 = other.right,
257                );
258                debug_assert!(
259                    pt >= other.left,
260                    "line intersection before second line: {pt:?}\n\tLine({lp1:?} - {lp2:?}) X Line({lp3:?} - {lp4:?})",
261                    lp1 = self.left,
262                    lp2 = self.right,
263                    lp3 = other.left,
264                    lp4 = other.right,
265                );
266
267                if let Some(ord) = ord {
268                    let l1 = LineOrPoint::from((self.left, pt));
269                    let l2 = LineOrPoint {
270                        left: other.left,
271                        right: pt,
272                    };
273                    let cmp = l1.partial_cmp(&l2).unwrap();
274                    if l1.is_line() && l2.is_line() && cmp.then(ord) != ord {
275                        debug!(
276                            "ordering changed by intersection: {l1:?} {ord:?} {l2:?}",
277                            l1 = self,
278                            l2 = other
279                        );
280                        debug!("\tparts: {l1:?}, {l2:?}");
281                        debug!("\tintersection: {pt:?} {cmp:?}");
282
283                        // RM: This is a complicated intersection that is changing the ordering.
284                        // Heuristic: approximate with a trivial intersection point that preserves the topology.
285                        return Some(if self.left > other.left {
286                            self.left.into()
287                        } else {
288                            other.left.into()
289                        });
290                    }
291                }
292                Some((*pt).into())
293            }
294            e => e,
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use std::cmp::Ordering;
302
303    use geo_types::{Coord, LineString};
304    use wkt::ToWkt;
305
306    use crate::{GeoFloat, GeoNum, HasKernel, Kernel};
307
308    use super::LineOrPoint;
309
310    // Used for debugging sweep fp issues
311    #[test]
312    #[ignore]
313    fn check_ordering() {
314        let pt_7 = Coord::from((-32.57812499999999, 241.33427773853316));
315        let pt_8 = Coord::from((-36.11348070978957, 237.7989220287436));
316        let pt_13 = Coord::from((-25.507080078124993, 248.40532266040816));
317        let pt_14 = Coord::from((-36.48784219165816, 237.424560546875));
318        let _pt_15 = Coord::from((4.4929199218750036, 196.44379843334184));
319        let pt_16 = Coord::from((-36.048578439260666, 237.8638242992725));
320        let pt_17 = Coord::from((3.545624214480127, 197.39109414073673));
321
322        fn check_isection<T: GeoFloat>(abcd: [Coord<T>; 4]) -> Option<LineOrPoint<T>> {
323            let l1 = LineOrPoint::from((abcd[0].into(), abcd[1].into()));
324            let l2 = LineOrPoint::from((abcd[2].into(), abcd[3].into()));
325            l1.intersect_line_ordered(&l2)
326        }
327        fn check_lines<T: GeoNum>(abcd: [Coord<T>; 4]) -> Ordering {
328            let l1 = LineOrPoint::from((abcd[0].into(), abcd[1].into()));
329            let l2 = LineOrPoint::from((abcd[2].into(), abcd[3].into()));
330            l1.partial_cmp(&l2).unwrap()
331        }
332
333        eprintln!(
334            "(14-17) {cmp:?} (14-16)",
335            cmp = check_lines([pt_14, pt_17, pt_14, pt_16])
336        );
337        eprintln!(
338            "(8-16) {cmp:?} (14-16)",
339            cmp = check_lines([pt_8, pt_16, pt_14, pt_16]),
340        );
341        eprintln!(
342            "(8-7) {cmp:?} (14-16)",
343            cmp = check_lines([pt_8, pt_7, pt_14, pt_16]),
344        );
345        eprintln!(
346            "(8-7) {cmp:?} (14-13)",
347            cmp = check_lines([pt_8, pt_7, pt_14, pt_13]),
348        );
349        eprintln!(
350            "(8-7) {isect:?} (14-13)",
351            isect = check_isection([pt_8, pt_7, pt_14, pt_13]),
352        );
353        let l87 = LineString::new(vec![pt_8, pt_16, pt_7]);
354        let lo = LineString::new(vec![pt_14, pt_16, pt_13]);
355        eprintln!("l1: {}", l87.to_wkt());
356        eprintln!("lo: {}", lo.to_wkt());
357
358        eprintln!(
359            "pred: {:?}",
360            <f64 as HasKernel>::Ker::orient2d(pt_8, pt_7, pt_17)
361        );
362        eprintln!(
363            "pred: {:?}",
364            <f64 as HasKernel>::Ker::orient2d(pt_8, pt_14, pt_16)
365        );
366    }
367}