Skip to content

Commit 7efe55e

Browse files
committed
feat(geometry): add line segment intersection utility
1 parent 169fcdd commit 7efe55e

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.thealgorithms.geometry;
2+
3+
import java.awt.geom.Point2D;
4+
import java.util.Optional;
5+
6+
/**
7+
* Utility methods for checking and computing 2D line segment intersections.
8+
*/
9+
public final class LineIntersection {
10+
private LineIntersection() {
11+
}
12+
13+
/**
14+
* Checks whether two line segments intersect.
15+
*
16+
* @param p1 first endpoint of segment 1
17+
* @param p2 second endpoint of segment 1
18+
* @param q1 first endpoint of segment 2
19+
* @param q2 second endpoint of segment 2
20+
* @return true when the segments intersect (including touching endpoints)
21+
*/
22+
public static boolean intersects(Point p1, Point p2, Point q1, Point q2) {
23+
int o1 = orientation(p1, p2, q1);
24+
int o2 = orientation(p1, p2, q2);
25+
int o3 = orientation(q1, q2, p1);
26+
int o4 = orientation(q1, q2, p2);
27+
28+
if (o1 != o2 && o3 != o4) {
29+
return true;
30+
}
31+
32+
if (o1 == 0 && onSegment(p1, q1, p2)) {
33+
return true;
34+
}
35+
if (o2 == 0 && onSegment(p1, q2, p2)) {
36+
return true;
37+
}
38+
if (o3 == 0 && onSegment(q1, p1, q2)) {
39+
return true;
40+
}
41+
if (o4 == 0 && onSegment(q1, p2, q2)) {
42+
return true;
43+
}
44+
45+
return false;
46+
}
47+
48+
/**
49+
* Computes the single geometric intersection point between two non-parallel
50+
* segments when it exists.
51+
*
52+
* <p>For parallel/collinear overlap, this method returns {@code Optional.empty()}.
53+
*
54+
* @param p1 first endpoint of segment 1
55+
* @param p2 second endpoint of segment 1
56+
* @param q1 first endpoint of segment 2
57+
* @param q2 second endpoint of segment 2
58+
* @return the intersection point when uniquely defined and on both segments
59+
*/
60+
public static Optional<Point2D.Double> intersectionPoint(Point p1, Point p2, Point q1, Point q2) {
61+
if (!intersects(p1, p2, q1, q2)) {
62+
return Optional.empty();
63+
}
64+
65+
double x1 = p1.x();
66+
double y1 = p1.y();
67+
double x2 = p2.x();
68+
double y2 = p2.y();
69+
double x3 = q1.x();
70+
double y3 = q1.y();
71+
double x4 = q2.x();
72+
double y4 = q2.y();
73+
74+
double denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
75+
if (denominator == 0.0) {
76+
return Optional.empty();
77+
}
78+
79+
double numeratorX = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4);
80+
double numeratorY = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4);
81+
82+
return Optional.of(new Point2D.Double(numeratorX / denominator, numeratorY / denominator));
83+
}
84+
85+
private static int orientation(Point a, Point b, Point c) {
86+
long cross = (long) (b.x() - a.x()) * (c.y() - a.y()) - (long) (b.y() - a.y()) * (c.x() - a.x());
87+
return Long.compare(cross, 0L);
88+
}
89+
90+
private static boolean onSegment(Point a, Point b, Point c) {
91+
return b.x() >= Math.min(a.x(), c.x()) && b.x() <= Math.max(a.x(), c.x())
92+
&& b.y() >= Math.min(a.y(), c.y()) && b.y() <= Math.max(a.y(), c.y());
93+
}
94+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.thealgorithms.geometry;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.awt.geom.Point2D;
8+
import java.util.Optional;
9+
import org.junit.jupiter.api.Test;
10+
11+
class LineIntersectionTest {
12+
13+
@Test
14+
void testCrossingSegments() {
15+
Point p1 = new Point(0, 0);
16+
Point p2 = new Point(4, 4);
17+
Point q1 = new Point(0, 4);
18+
Point q2 = new Point(4, 0);
19+
20+
assertTrue(LineIntersection.intersects(p1, p2, q1, q2));
21+
Optional<Point2D.Double> intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2);
22+
assertTrue(intersection.isPresent());
23+
assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9);
24+
assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9);
25+
}
26+
27+
@Test
28+
void testParallelSegments() {
29+
Point p1 = new Point(0, 0);
30+
Point p2 = new Point(3, 3);
31+
Point q1 = new Point(0, 1);
32+
Point q2 = new Point(3, 4);
33+
34+
assertFalse(LineIntersection.intersects(p1, p2, q1, q2));
35+
assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty());
36+
}
37+
38+
@Test
39+
void testTouchingAtEndpoint() {
40+
Point p1 = new Point(0, 0);
41+
Point p2 = new Point(2, 2);
42+
Point q1 = new Point(2, 2);
43+
Point q2 = new Point(4, 0);
44+
45+
assertTrue(LineIntersection.intersects(p1, p2, q1, q2));
46+
Optional<Point2D.Double> intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2);
47+
assertTrue(intersection.isPresent());
48+
assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9);
49+
assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9);
50+
}
51+
52+
@Test
53+
void testCollinearOverlapHasNoUniquePoint() {
54+
Point p1 = new Point(0, 0);
55+
Point p2 = new Point(4, 4);
56+
Point q1 = new Point(2, 2);
57+
Point q2 = new Point(6, 6);
58+
59+
assertTrue(LineIntersection.intersects(p1, p2, q1, q2));
60+
assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty());
61+
}
62+
}

0 commit comments

Comments
 (0)