-
Notifications
You must be signed in to change notification settings - Fork 21.1k
feat(geometry): add line segment intersection utility #7376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
DenizAltunkapan
merged 4 commits into
TheAlgorithms:master
from
nickzerjeski:feat-geometry-line-intersection-6856
Apr 13, 2026
+206
−0
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f01ad41
feat(geometry): add line segment intersection utility
nickzerjeski d3e8eb2
test(geometry): cover more line intersection edge cases
nickzerjeski 83eca10
Address line intersection edge cases from review
nickzerjeski 481c2d0
Apply clang-format fixes for line intersection
nickzerjeski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
105 changes: 105 additions & 0 deletions
105
src/main/java/com/thealgorithms/geometry/LineIntersection.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| package com.thealgorithms.geometry; | ||
|
|
||
| import java.awt.geom.Point2D; | ||
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * Utility methods for checking and computing 2D line segment intersections. | ||
| */ | ||
| public final class LineIntersection { | ||
| private LineIntersection() { | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether two line segments intersect. | ||
| * | ||
| * @param p1 first endpoint of segment 1 | ||
| * @param p2 second endpoint of segment 1 | ||
| * @param q1 first endpoint of segment 2 | ||
| * @param q2 second endpoint of segment 2 | ||
| * @return true when the segments intersect (including touching endpoints) | ||
| */ | ||
| public static boolean intersects(Point p1, Point p2, Point q1, Point q2) { | ||
| int o1 = orientation(p1, p2, q1); | ||
| int o2 = orientation(p1, p2, q2); | ||
| int o3 = orientation(q1, q2, p1); | ||
| int o4 = orientation(q1, q2, p2); | ||
|
|
||
| if (o1 != o2 && o3 != o4) { | ||
| return true; | ||
| } | ||
|
|
||
| if (o1 == 0 && onSegment(p1, q1, p2)) { | ||
| return true; | ||
| } | ||
| if (o2 == 0 && onSegment(p1, q2, p2)) { | ||
| return true; | ||
| } | ||
| if (o3 == 0 && onSegment(q1, p1, q2)) { | ||
| return true; | ||
| } | ||
| if (o4 == 0 && onSegment(q1, p2, q2)) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Computes the single geometric intersection point between two non-parallel | ||
| * segments when it exists. | ||
| * | ||
| * <p>For parallel/collinear overlap, this method returns {@code Optional.empty()}. | ||
| * | ||
| * @param p1 first endpoint of segment 1 | ||
| * @param p2 second endpoint of segment 1 | ||
| * @param q1 first endpoint of segment 2 | ||
| * @param q2 second endpoint of segment 2 | ||
| * @return the intersection point when uniquely defined and on both segments | ||
| */ | ||
| public static Optional<Point2D.Double> intersectionPoint(Point p1, Point p2, Point q1, Point q2) { | ||
| if (!intersects(p1, p2, q1, q2)) { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| long x1 = p1.x(); | ||
| long y1 = p1.y(); | ||
| long x2 = p2.x(); | ||
| long y2 = p2.y(); | ||
| long x3 = q1.x(); | ||
| long y3 = q1.y(); | ||
| long x4 = q2.x(); | ||
| long y4 = q2.y(); | ||
|
|
||
| long denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); | ||
| if (denominator == 0L) { | ||
| return sharedEndpoint(p1, p2, q1, q2); | ||
| } | ||
|
|
||
| long determinant1 = x1 * y2 - y1 * x2; | ||
| long determinant2 = x3 * y4 - y3 * x4; | ||
| long numeratorX = determinant1 * (x3 - x4) - (x1 - x2) * determinant2; | ||
| long numeratorY = determinant1 * (y3 - y4) - (y1 - y2) * determinant2; | ||
|
|
||
| return Optional.of(new Point2D.Double(numeratorX / (double) denominator, numeratorY / (double) denominator)); | ||
| } | ||
|
|
||
| private static int orientation(Point a, Point b, Point c) { | ||
| long cross = ((long) b.x() - a.x()) * ((long) c.y() - a.y()) - ((long) b.y() - a.y()) * ((long) c.x() - a.x()); | ||
| return Long.compare(cross, 0L); | ||
| } | ||
|
|
||
| private static Optional<Point2D.Double> sharedEndpoint(Point p1, Point p2, Point q1, Point q2) { | ||
| if (p1.equals(q1) || p1.equals(q2)) { | ||
| return Optional.of(new Point2D.Double(p1.x(), p1.y())); | ||
| } | ||
| if (p2.equals(q1) || p2.equals(q2)) { | ||
| return Optional.of(new Point2D.Double(p2.x(), p2.y())); | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| private static boolean onSegment(Point a, Point b, Point c) { | ||
| return b.x() >= Math.min(a.x(), c.x()) && b.x() <= Math.max(a.x(), c.x()) && b.y() >= Math.min(a.y(), c.y()) && b.y() <= Math.max(a.y(), c.y()); | ||
| } | ||
| } |
101 changes: 101 additions & 0 deletions
101
src/test/java/com/thealgorithms/geometry/LineIntersectionTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| package com.thealgorithms.geometry; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| import java.awt.geom.Point2D; | ||
| import java.util.Optional; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class LineIntersectionTest { | ||
|
|
||
| @Test | ||
| void testCrossingSegments() { | ||
| Point p1 = new Point(0, 0); | ||
| Point p2 = new Point(4, 4); | ||
| Point q1 = new Point(0, 4); | ||
| Point q2 = new Point(4, 0); | ||
|
|
||
| assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); | ||
| Optional<Point2D.Double> intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); | ||
| assertTrue(intersection.isPresent()); | ||
| assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); | ||
| assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); | ||
| } | ||
|
|
||
| @Test | ||
| void testParallelSegments() { | ||
| Point p1 = new Point(0, 0); | ||
| Point p2 = new Point(3, 3); | ||
| Point q1 = new Point(0, 1); | ||
| Point q2 = new Point(3, 4); | ||
|
|
||
| assertFalse(LineIntersection.intersects(p1, p2, q1, q2)); | ||
| assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); | ||
| } | ||
|
|
||
| @Test | ||
| void testTouchingAtEndpoint() { | ||
| Point p1 = new Point(0, 0); | ||
| Point p2 = new Point(2, 2); | ||
| Point q1 = new Point(2, 2); | ||
| Point q2 = new Point(4, 0); | ||
|
|
||
| assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); | ||
| Optional<Point2D.Double> intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); | ||
| assertTrue(intersection.isPresent()); | ||
| assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); | ||
| assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); | ||
| } | ||
|
|
||
| @Test | ||
| void testCollinearOverlapHasNoUniquePoint() { | ||
| Point p1 = new Point(0, 0); | ||
| Point p2 = new Point(4, 4); | ||
| Point q1 = new Point(2, 2); | ||
| Point q2 = new Point(6, 6); | ||
|
|
||
| assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); | ||
| assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); | ||
| } | ||
|
|
||
| @Test | ||
| void testCollinearDisjointSegments() { | ||
| Point p1 = new Point(0, 0); | ||
| Point p2 = new Point(2, 2); | ||
| Point q1 = new Point(3, 3); | ||
| Point q2 = new Point(5, 5); | ||
|
|
||
| assertFalse(LineIntersection.intersects(p1, p2, q1, q2)); | ||
| assertTrue(LineIntersection.intersectionPoint(p1, p2, q1, q2).isEmpty()); | ||
| } | ||
|
|
||
| @Test | ||
| void testCollinearSegmentsTouchingAtEndpointHaveUniquePoint() { | ||
| Point p1 = new Point(0, 0); | ||
| Point p2 = new Point(2, 2); | ||
| Point q1 = new Point(2, 2); | ||
| Point q2 = new Point(4, 4); | ||
|
|
||
| assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); | ||
| Optional<Point2D.Double> intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); | ||
| assertTrue(intersection.isPresent()); | ||
| assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); | ||
| assertEquals(2.0, intersection.orElseThrow().getY(), 1e-9); | ||
| } | ||
|
|
||
| @Test | ||
| void testVerticalAndHorizontalCrossingSegments() { | ||
| Point p1 = new Point(2, 0); | ||
| Point p2 = new Point(2, 5); | ||
| Point q1 = new Point(0, 3); | ||
| Point q2 = new Point(4, 3); | ||
|
|
||
| assertTrue(LineIntersection.intersects(p1, p2, q1, q2)); | ||
| Optional<Point2D.Double> intersection = LineIntersection.intersectionPoint(p1, p2, q1, q2); | ||
| assertTrue(intersection.isPresent()); | ||
| assertEquals(2.0, intersection.orElseThrow().getX(), 1e-9); | ||
| assertEquals(3.0, intersection.orElseThrow().getY(), 1e-9); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.