geo/algorithm/
line_intersection.rs

1use crate::{Coord, GeoFloat, Line};
2use geo_types::coord;
3
4use crate::BoundingRect;
5use crate::Intersects;
6
7#[derive(PartialEq, Eq, Clone, Copy, Debug)]
8pub enum LineIntersection<F: GeoFloat> {
9    /// Lines intersect in a single point
10    SinglePoint {
11        intersection: Coord<F>,
12        /// For Lines which intersect in a single point, that point may be either an endpoint
13        /// or in the interior of each Line.
14        /// If the point lies in the interior of both Lines, we call it a _proper_ intersection.
15        ///
16        /// # Note
17        ///
18        /// Due to the limited precision of most float data-types, the
19        /// calculated intersection point may be snapped to one of the
20        /// end-points even though all the end-points of the two
21        /// lines are distinct points. In such cases, this field is
22        /// still set to `true`. Please refer test_case:
23        /// `test_central_endpoint_heuristic_failure_1` for such an
24        /// example.
25        is_proper: bool,
26    },
27
28    /// Overlapping Lines intersect in a line segment
29    Collinear { intersection: Line<F> },
30}
31
32impl<F: GeoFloat> LineIntersection<F> {
33    pub fn is_proper(&self) -> bool {
34        match self {
35            Self::Collinear { .. } => false,
36            Self::SinglePoint { is_proper, .. } => *is_proper,
37        }
38    }
39}
40
41/// Returns the intersection between two [`Lines`](Line).
42///
43/// Lines can intersect in a Point or, for Collinear lines, in a Line. See [`LineIntersection`]
44/// for more details about the result.
45///
46/// # Examples
47///
48/// ```
49/// use geo_types::coord;
50/// use geo::{Line, Coord};
51/// use geo::line_intersection::{line_intersection, LineIntersection};
52///
53/// let line_1 = Line::new(coord! {x: 0.0, y: 0.0}, coord! { x: 5.0, y: 5.0 } );
54/// let line_2 = Line::new(coord! {x: 0.0, y: 5.0}, coord! { x: 5.0, y: 0.0 } );
55/// let expected = LineIntersection::SinglePoint { intersection: coord! { x: 2.5, y: 2.5 }, is_proper: true };
56/// assert_eq!(line_intersection(line_1, line_2), Some(expected));
57///
58/// let line_1 = Line::new(coord! {x: 0.0, y: 0.0}, coord! { x: 5.0, y: 5.0 } );
59/// let line_2 = Line::new(coord! {x: 0.0, y: 1.0}, coord! { x: 5.0, y: 6.0 } );
60/// assert_eq!(line_intersection(line_1, line_2), None);
61///
62/// let line_1 = Line::new(coord! {x: 0.0, y: 0.0}, coord! { x: 5.0, y: 5.0 } );
63/// let line_2 = Line::new(coord! {x: 5.0, y: 5.0}, coord! { x: 5.0, y: 0.0 } );
64/// let expected = LineIntersection::SinglePoint { intersection: coord! { x: 5.0, y: 5.0 }, is_proper: false };
65/// assert_eq!(line_intersection(line_1, line_2), Some(expected));
66///
67/// let line_1 = Line::new(coord! {x: 0.0, y: 0.0}, coord! { x: 5.0, y: 5.0 } );
68/// let line_2 = Line::new(coord! {x: 3.0, y: 3.0}, coord! { x: 6.0, y: 6.0 } );
69/// let expected = LineIntersection::Collinear { intersection: Line::new(coord! { x: 3.0, y: 3.0 }, coord! { x: 5.0, y: 5.0 })};
70/// assert_eq!(line_intersection(line_1, line_2), Some(expected));
71/// ```
72/// Strongly inspired by, and meant to produce the same results as, [JTS's RobustLineIntersector](https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java#L26).
73pub fn line_intersection<F>(p: Line<F>, q: Line<F>) -> Option<LineIntersection<F>>
74where
75    F: GeoFloat,
76{
77    if !p.bounding_rect().intersects(&q.bounding_rect()) {
78        return None;
79    }
80
81    use crate::kernels::{Kernel, Orientation::*, RobustKernel};
82    let p_q1 = RobustKernel::orient2d(p.start, p.end, q.start);
83    let p_q2 = RobustKernel::orient2d(p.start, p.end, q.end);
84    if matches!(
85        (p_q1, p_q2),
86        (Clockwise, Clockwise) | (CounterClockwise, CounterClockwise)
87    ) {
88        return None;
89    }
90
91    let q_p1 = RobustKernel::orient2d(q.start, q.end, p.start);
92    let q_p2 = RobustKernel::orient2d(q.start, q.end, p.end);
93    if matches!(
94        (q_p1, q_p2),
95        (Clockwise, Clockwise) | (CounterClockwise, CounterClockwise)
96    ) {
97        return None;
98    }
99
100    if matches!(
101        (p_q1, p_q2, q_p1, q_p2),
102        (Collinear, Collinear, Collinear, Collinear)
103    ) {
104        return collinear_intersection(p, q);
105    }
106
107    // At this point we know that there is a single intersection point (since the lines are not
108    // collinear).
109    //
110    // Check if the intersection is an endpoint. If it is, copy the endpoint as the
111    // intersection point. Copying the point rather than computing it ensures the point has the
112    // exact value, which is important for robustness. It is sufficient to simply check for an
113    // endpoint which is on the other line, since at this point we know that the inputLines
114    // must intersect.
115    if p_q1 == Collinear || p_q2 == Collinear || q_p1 == Collinear || q_p2 == Collinear {
116        // Check for two equal endpoints.
117        // This is done explicitly rather than by the orientation tests below in order to improve
118        // robustness.
119        //
120        // [An example where the orientation tests fail to be consistent is the following (where
121        // the true intersection is at the shared endpoint
122        // POINT (19.850257749638203 46.29709338043669)
123        //
124        // LINESTRING ( 19.850257749638203 46.29709338043669, 20.31970698357233 46.76654261437082 )
125        // and
126        // LINESTRING ( -48.51001596420236 -22.063180333403878, 19.850257749638203 46.29709338043669 )
127        //
128        // which used to produce the INCORRECT result: (20.31970698357233, 46.76654261437082, NaN)
129
130        let intersection: Coord<F>;
131        // false positives for this overzealous clippy https://github.com/rust-lang/rust-clippy/issues/6747
132        #[allow(clippy::suspicious_operation_groupings)]
133        if p.start == q.start || p.start == q.end {
134            intersection = p.start;
135        } else if p.end == q.start || p.end == q.end {
136            intersection = p.end;
137            // Now check to see if any endpoint lies on the interior of the other segment.
138        } else if p_q1 == Collinear {
139            intersection = q.start;
140        } else if p_q2 == Collinear {
141            intersection = q.end;
142        } else if q_p1 == Collinear {
143            intersection = p.start;
144        } else {
145            assert_eq!(q_p2, Collinear);
146            intersection = p.end;
147        }
148        Some(LineIntersection::SinglePoint {
149            intersection,
150            is_proper: false,
151        })
152    } else {
153        let intersection = proper_intersection(p, q);
154        Some(LineIntersection::SinglePoint {
155            intersection,
156            is_proper: true,
157        })
158    }
159}
160
161fn collinear_intersection<F: GeoFloat>(p: Line<F>, q: Line<F>) -> Option<LineIntersection<F>> {
162    fn collinear<F: GeoFloat>(intersection: Line<F>) -> LineIntersection<F> {
163        LineIntersection::Collinear { intersection }
164    }
165
166    fn improper<F: GeoFloat>(intersection: Coord<F>) -> LineIntersection<F> {
167        LineIntersection::SinglePoint {
168            intersection,
169            is_proper: false,
170        }
171    }
172
173    let p_bounds = p.bounding_rect();
174    let q_bounds = q.bounding_rect();
175    Some(
176        match (
177            p_bounds.intersects(&q.start),
178            p_bounds.intersects(&q.end),
179            q_bounds.intersects(&p.start),
180            q_bounds.intersects(&p.end),
181        ) {
182            (true, true, _, _) => collinear(q),
183            (_, _, true, true) => collinear(p),
184            (true, false, true, false) if q.start == p.start => improper(q.start),
185            (true, _, true, _) => collinear(Line::new(q.start, p.start)),
186            (true, false, false, true) if q.start == p.end => improper(q.start),
187            (true, _, _, true) => collinear(Line::new(q.start, p.end)),
188            (false, true, true, false) if q.end == p.start => improper(q.end),
189            (_, true, true, _) => collinear(Line::new(q.end, p.start)),
190            (false, true, false, true) if q.end == p.end => improper(q.end),
191            (_, true, _, true) => collinear(Line::new(q.end, p.end)),
192            _ => return None,
193        },
194    )
195}
196
197/// Finds the endpoint of the segments P and Q which is closest to the other segment.  This is
198/// a reasonable surrogate for the true intersection points in ill-conditioned cases (e.g.
199/// where two segments are nearly coincident, or where the endpoint of one segment lies almost
200/// on the other segment).
201///
202/// This replaces the older CentralEndpoint heuristic, which chose the wrong endpoint in some
203/// cases where the segments had very distinct slopes and one endpoint lay almost on the other
204/// segment.
205///
206/// `returns` the nearest endpoint to the other segment
207fn nearest_endpoint<F: GeoFloat>(p: Line<F>, q: Line<F>) -> Coord<F> {
208    use geo_types::private_utils::point_line_euclidean_distance;
209
210    let mut nearest_pt = p.start;
211    let mut min_dist = point_line_euclidean_distance(p.start, q);
212
213    let dist = point_line_euclidean_distance(p.end, q);
214    if dist < min_dist {
215        min_dist = dist;
216        nearest_pt = p.end;
217    }
218    let dist = point_line_euclidean_distance(q.start, p);
219    if dist < min_dist {
220        min_dist = dist;
221        nearest_pt = q.start;
222    }
223    let dist = point_line_euclidean_distance(q.end, p);
224    if dist < min_dist {
225        nearest_pt = q.end;
226    }
227    nearest_pt
228}
229
230fn raw_line_intersection<F: GeoFloat>(p: Line<F>, q: Line<F>) -> Option<Coord<F>> {
231    let p_min_x = p.start.x.min(p.end.x);
232    let p_min_y = p.start.y.min(p.end.y);
233    let p_max_x = p.start.x.max(p.end.x);
234    let p_max_y = p.start.y.max(p.end.y);
235
236    let q_min_x = q.start.x.min(q.end.x);
237    let q_min_y = q.start.y.min(q.end.y);
238    let q_max_x = q.start.x.max(q.end.x);
239    let q_max_y = q.start.y.max(q.end.y);
240
241    let int_min_x = p_min_x.max(q_min_x);
242    let int_max_x = p_max_x.min(q_max_x);
243    let int_min_y = p_min_y.max(q_min_y);
244    let int_max_y = p_max_y.min(q_max_y);
245
246    let two = F::one() + F::one();
247    let mid_x = (int_min_x + int_max_x) / two;
248    let mid_y = (int_min_y + int_max_y) / two;
249
250    // condition ordinate values by subtracting midpoint
251    let p1x = p.start.x - mid_x;
252    let p1y = p.start.y - mid_y;
253    let p2x = p.end.x - mid_x;
254    let p2y = p.end.y - mid_y;
255    let q1x = q.start.x - mid_x;
256    let q1y = q.start.y - mid_y;
257    let q2x = q.end.x - mid_x;
258    let q2y = q.end.y - mid_y;
259
260    // unrolled computation using homogeneous coordinates eqn
261    let px = p1y - p2y;
262    let py = p2x - p1x;
263    let pw = p1x * p2y - p2x * p1y;
264
265    let qx = q1y - q2y;
266    let qy = q2x - q1x;
267    let qw = q1x * q2y - q2x * q1y;
268
269    let xw = py * qw - qy * pw;
270    let yw = qx * pw - px * qw;
271    let w = px * qy - qx * py;
272
273    let x_int = xw / w;
274    let y_int = yw / w;
275
276    // check for parallel lines
277    if (x_int.is_nan() || x_int.is_infinite()) || (y_int.is_nan() || y_int.is_infinite()) {
278        None
279    } else {
280        // de-condition intersection point
281        Some(coord! {
282            x: x_int + mid_x,
283            y: y_int + mid_y,
284        })
285    }
286}
287
288/// This method computes the actual value of the intersection point.
289/// To obtain the maximum precision from the intersection calculation,
290/// the coordinates are normalized by subtracting the minimum
291/// ordinate values (in absolute value).  This has the effect of
292/// removing common significant digits from the calculation to
293/// maintain more bits of precision.
294fn proper_intersection<F: GeoFloat>(p: Line<F>, q: Line<F>) -> Coord<F> {
295    // Computes a segment intersection using homogeneous coordinates.
296    // Round-off error can cause the raw computation to fail,
297    // (usually due to the segments being approximately parallel).
298    // If this happens, a reasonable approximation is computed instead.
299    let mut int_pt = raw_line_intersection(p, q).unwrap_or_else(|| nearest_endpoint(p, q));
300
301    // NOTE: At this point, JTS does a `Envelope::contains(coord)` check, but confusingly,
302    // Envelope::contains(coord) in JTS is actually an *intersection* check, not a true SFS
303    // `contains`, because it includes the boundary of the rect.
304    if !(p.bounding_rect().intersects(&int_pt) && q.bounding_rect().intersects(&int_pt)) {
305        // compute a safer result
306        // copy the coordinate, since it may be rounded later
307        int_pt = nearest_endpoint(p, q);
308    }
309    int_pt
310}
311
312#[cfg(test)]
313mod test {
314    use super::*;
315    use crate::geo_types::coord;
316
317    /// Based on JTS test `testCentralEndpointHeuristicFailure`
318    /// > Following cases were failures when using the CentralEndpointIntersector heuristic.
319    /// > This is because one segment lies at a significant angle to the other,
320    /// > with only one endpoint is close to the other segment.
321    /// > The CE heuristic chose the wrong endpoint to return.
322    /// > The fix is to use a new heuristic which out of the 4 endpoints
323    /// > chooses the one which is closest to the other segment.
324    /// > This works in all known failure cases.
325    #[test]
326    fn test_central_endpoint_heuristic_failure_1() {
327        let line_1 = Line::new(
328            coord! {
329                x: 163.81867067,
330                y: -211.31840378,
331            },
332            coord! {
333                x: 165.9174252,
334                y: -214.1665075,
335            },
336        );
337        let line_2 = Line::new(
338            coord! {
339                x: 2.84139601,
340                y: -57.95412726,
341            },
342            coord! {
343                x: 469.59990601,
344                y: -502.63851732,
345            },
346        );
347        let actual = line_intersection(line_1, line_2);
348        let expected = LineIntersection::SinglePoint {
349            intersection: coord! {
350                x: 163.81867067,
351                y: -211.31840378,
352            },
353            is_proper: true,
354        };
355        assert_eq!(actual, Some(expected));
356    }
357
358    /// Based on JTS test `testCentralEndpointHeuristicFailure2`
359    /// > Test from Tomas Fa - JTS list 6/13/2012
360    /// >
361    /// > Fails using original JTS DeVillers determine orientation test.
362    /// > Succeeds using DD and Shewchuk orientation
363    #[test]
364    fn test_central_endpoint_heuristic_failure_2() {
365        let line_1 = Line::new(
366            coord! {
367                x: -58.00593335955,
368                y: -1.43739086465,
369            },
370            coord! {
371                x: -513.86101637525,
372                y: -457.29247388035,
373            },
374        );
375        let line_2 = Line::new(
376            coord! {
377                x: -215.22279674875,
378                y: -158.65425425385,
379            },
380            coord! {
381                x: -218.1208801283,
382                y: -160.68343590235,
383            },
384        );
385        let actual = line_intersection(line_1, line_2);
386        let expected = LineIntersection::SinglePoint {
387            intersection: coord! {
388                x: -215.22279674875,
389                y: -158.65425425385,
390            },
391            is_proper: true,
392        };
393        assert_eq!(actual, Some(expected));
394    }
395
396    /// Based on JTS test `testTomasFa_1`
397    /// > Test from Tomas Fa - JTS list 6/13/2012
398    /// >
399    /// > Fails using original JTS DeVillers determine orientation test.
400    /// > Succeeds using DD and Shewchuk orientation
401    #[test]
402    fn test_tomas_fa_1() {
403        let line_1 = Line::new(coord! { x: -42.0, y: 163.2 }, coord! { x: 21.2, y: 265.2 });
404        let line_2 = Line::new(coord! { x: -26.2, y: 188.7 }, coord! { x: 37.0, y: 290.7 });
405        let actual = line_intersection(line_1, line_2);
406        let expected = None;
407        assert_eq!(actual, expected);
408    }
409
410    /// Based on JTS test `testTomasFa_2`
411    ///
412    /// > Test from Tomas Fa - JTS list 6/13/2012
413    /// >
414    /// > Fails using original JTS DeVillers determine orientation test.
415    #[test]
416    fn test_tomas_fa_2() {
417        let line_1 = Line::new(coord! { x: -5.9, y: 163.1 }, coord! { x: 76.1, y: 250.7 });
418        let line_2 = Line::new(coord! { x: 14.6, y: 185.0 }, coord! { x: 96.6, y: 272.6 });
419        let actual = line_intersection(line_1, line_2);
420        let expected = None;
421        assert_eq!(actual, expected);
422    }
423
424    /// Based on JTS test `testLeduc_1`
425    ///
426    /// > Test involving two non-almost-parallel lines.
427    /// > Does not seem to cause problems with basic line intersection algorithm.
428    #[test]
429    fn test_leduc_1() {
430        let line_1 = Line::new(
431            coord! {
432                x: 305690.0434123494,
433                y: 254176.46578338774,
434            },
435            coord! {
436                x: 305601.9999843455,
437                y: 254243.19999846347,
438            },
439        );
440        let line_2 = Line::new(
441            coord! {
442                x: 305689.6153764265,
443                y: 254177.33102743194,
444            },
445            coord! {
446                x: 305692.4999844298,
447                y: 254171.4999983967,
448            },
449        );
450        let actual = line_intersection(line_1, line_2);
451        let expected = LineIntersection::SinglePoint {
452            intersection: coord! {
453                x: 305690.0434123494,
454                y: 254176.46578338774,
455            },
456            is_proper: true,
457        };
458        assert_eq!(actual, Some(expected));
459    }
460
461    /// Based on JTS test `testGEOS_1()`
462    ///
463    /// > Test from strk which is bad in GEOS (2009-04-14).
464    #[test]
465    fn test_geos_1() {
466        let line_1 = Line::new(
467            coord! {
468                x: 588750.7429703881,
469                y: 4518950.493668233,
470            },
471            coord! {
472                x: 588748.2060409798,
473                y: 4518933.9452804085,
474            },
475        );
476        let line_2 = Line::new(
477            coord! {
478                x: 588745.824857241,
479                y: 4518940.742239175,
480            },
481            coord! {
482                x: 588748.2060437313,
483                y: 4518933.9452791475,
484            },
485        );
486        let actual = line_intersection(line_1, line_2);
487        let expected = LineIntersection::SinglePoint {
488            intersection: coord! {
489                x: 588748.2060416829,
490                y: 4518933.945284994,
491            },
492            is_proper: true,
493        };
494        assert_eq!(actual, Some(expected));
495    }
496
497    /// Based on JTS test `testGEOS_2()`
498    ///
499    /// > Test from strk which is bad in GEOS (2009-04-14).
500    #[test]
501    fn test_geos_2() {
502        let line_1 = Line::new(
503            coord! {
504                x: 588743.626135934,
505                y: 4518924.610969561,
506            },
507            coord! {
508                x: 588732.2822865889,
509                y: 4518925.4314047815,
510            },
511        );
512        let line_2 = Line::new(
513            coord! {
514                x: 588739.1191384895,
515                y: 4518927.235700594,
516            },
517            coord! {
518                x: 588731.7854614238,
519                y: 4518924.578370095,
520            },
521        );
522        let actual = line_intersection(line_1, line_2);
523        let expected = LineIntersection::SinglePoint {
524            intersection: coord! {
525                x: 588733.8306132929,
526                y: 4518925.319423238,
527            },
528            is_proper: true,
529        };
530        assert_eq!(actual, Some(expected));
531    }
532
533    /// Based on JTS test `testDaveSkeaCase()`
534    ///
535    /// > This used to be a failure case (exception), but apparently works now.
536    /// > Possibly normalization has fixed this?
537    #[test]
538    fn test_dave_skea_case() {
539        let line_1 = Line::new(
540            coord! {
541                x: 2089426.5233462777,
542                y: 1180182.387733969,
543            },
544            coord! {
545                x: 2085646.6891757075,
546                y: 1195618.7333999649,
547            },
548        );
549        let line_2 = Line::new(
550            coord! {
551                x: 1889281.8148903656,
552                y: 1997547.0560044837,
553            },
554            coord! {
555                x: 2259977.3672236,
556                y: 483675.17050843034,
557            },
558        );
559        let actual = line_intersection(line_1, line_2);
560        let expected = LineIntersection::SinglePoint {
561            intersection: coord! {
562                x: 2087536.6062609926,
563                y: 1187900.560566967,
564            },
565            is_proper: true,
566        };
567        assert_eq!(actual, Some(expected));
568    }
569
570    /// Based on JTS test `testCmp5CaseWKT()`
571    ///
572    /// > Outside envelope using HCoordinate method.
573    #[test]
574    fn test_cmp_5_cask_wkt() {
575        let line_1 = Line::new(
576            coord! {
577                x: 4348433.262114629,
578                y: 5552595.478385733,
579            },
580            coord! {
581                x: 4348440.849387404,
582                y: 5552599.272022122,
583            },
584        );
585        let line_2 = Line::new(
586            coord! {
587                x: 4348433.26211463,
588                y: 5552595.47838573,
589            },
590            coord! {
591                x: 4348440.8493874,
592                y: 5552599.27202212,
593            },
594        );
595        let actual = line_intersection(line_1, line_2);
596        let expected = LineIntersection::SinglePoint {
597            intersection: coord! {
598                x: 4348440.8493874,
599                y: 5552599.27202212,
600            },
601            is_proper: true,
602        };
603        assert_eq!(actual, Some(expected));
604    }
605}