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}