Skip to content

Commit dd086bd

Browse files
committed
test(geometry): add comprehensive tests for Bentley-Ottmann algorithm
- 19 test cases covering typical, edge, and degenerate cases - Tests include: single/multiple intersections, parallel segments, grid patterns - Performance test with 100 random segments - All tests validate correctness of intersection detection
1 parent 5a7f27c commit dd086bd

File tree

1 file changed

+343
-0
lines changed

1 file changed

+343
-0
lines changed
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
package com.thealgorithms.geometry;
2+
3+
import org.junit.jupiter.api.Test;
4+
import java.awt.geom.Point2D;
5+
import java.util.*;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
/**
10+
* Comprehensive unit tests for {@link BentleyOttmann}.
11+
*
12+
* <p>This test suite validates the correctness of the Bentley–Ottmann algorithm
13+
* implementation by checking intersection points between multiple line segment configurations.</p>
14+
*
15+
* <p>Test cases include typical, edge, degenerate geometrical setups, and performance tests.</p>
16+
*/
17+
public class BentleyOttmannTest {
18+
19+
private static final double EPS = 1e-6;
20+
21+
@Test
22+
void testSingleIntersection() {
23+
List<Object> segments = List.of(
24+
newSegment(1, 1, 5, 5),
25+
newSegment(1, 5, 5, 1)
26+
);
27+
28+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
29+
assertEquals(1, intersections.size());
30+
assertTrue(containsPoint(intersections, 3.0, 3.0));
31+
}
32+
33+
@Test
34+
void testVerticalIntersection() {
35+
List<Object> segments = List.of(
36+
newSegment(3, 0, 3, 6),
37+
newSegment(1, 1, 5, 5)
38+
);
39+
40+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
41+
assertEquals(1, intersections.size());
42+
assertTrue(containsPoint(intersections, 3.0, 3.0));
43+
}
44+
45+
@Test
46+
void testNoIntersection() {
47+
List<Object> segments = List.of(
48+
newSegment(0, 0, 1, 1),
49+
newSegment(2, 2, 3, 3)
50+
);
51+
52+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
53+
assertTrue(intersections.isEmpty());
54+
}
55+
56+
@Test
57+
void testCoincidentSegments() {
58+
List<Object> segments = List.of(
59+
newSegment(1, 1, 5, 5),
60+
newSegment(1, 1, 5, 5)
61+
);
62+
63+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
64+
65+
assertEquals(2, intersections.size(), "Two identical segments should report 2 intersection points (both endpoints)");
66+
assertTrue(containsPoint(intersections, 1.0, 1.0));
67+
assertTrue(containsPoint(intersections, 5.0, 5.0));
68+
}
69+
70+
@Test
71+
void testHorizontalIntersection() {
72+
List<Object> segments = List.of(
73+
newSegment(0, 2, 4, 2),
74+
newSegment(2, 0, 2, 4)
75+
);
76+
77+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
78+
assertTrue(containsPoint(intersections, 2.0, 2.0));
79+
}
80+
81+
@Test
82+
void testEmptyList() {
83+
List<Object> segments = List.of();
84+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
85+
assertTrue(intersections.isEmpty());
86+
}
87+
88+
@Test
89+
void testSingleSegment() {
90+
List<Object> segments = List.of(
91+
newSegment(0, 0, 5, 5)
92+
);
93+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
94+
assertTrue(intersections.isEmpty());
95+
}
96+
97+
@Test
98+
void testNullListThrowsException() {
99+
assertThrows(IllegalArgumentException.class, () -> BentleyOttmann.findIntersections(null));
100+
}
101+
102+
@Test
103+
void testParallelSegments() {
104+
// Test 1: Parallel diagonal segments
105+
List<Object> diagonalSegments = List.of(
106+
newSegment(0, 0, 4, 4),
107+
newSegment(1, 0, 5, 4),
108+
newSegment(2, 0, 6, 4)
109+
);
110+
assertTrue(BentleyOttmann.findIntersections(cast(diagonalSegments)).isEmpty());
111+
112+
// Test 2: Parallel vertical segments
113+
List<Object> verticalSegments = List.of(
114+
newSegment(1, 0, 1, 5),
115+
newSegment(2, 0, 2, 5),
116+
newSegment(3, 0, 3, 5)
117+
);
118+
assertTrue(BentleyOttmann.findIntersections(cast(verticalSegments)).isEmpty());
119+
120+
// Test 3: Parallel horizontal segments
121+
List<Object> horizontalSegments = List.of(
122+
newSegment(0, 1, 5, 1),
123+
newSegment(0, 2, 5, 2),
124+
newSegment(0, 3, 5, 3)
125+
);
126+
assertTrue(BentleyOttmann.findIntersections(cast(horizontalSegments)).isEmpty());
127+
}
128+
129+
@Test
130+
void testTouchingEndpoints() {
131+
List<Object> segments = List.of(
132+
newSegment(0, 0, 2, 2),
133+
newSegment(2, 2, 4, 0)
134+
);
135+
136+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
137+
assertEquals(1, intersections.size());
138+
assertTrue(containsPoint(intersections, 2.0, 2.0));
139+
}
140+
141+
@Test
142+
void testOverlappingCollinearSegments() {
143+
List<Object> segments = List.of(
144+
newSegment(0, 0, 4, 4),
145+
newSegment(2, 2, 6, 6)
146+
);
147+
148+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
149+
// Overlapping collinear segments share the point (2,2) where second starts
150+
// and (4,4) where first ends - at least one should be detected
151+
assertFalse(intersections.isEmpty(), "Should find at least one overlap point");
152+
assertTrue(containsPoint(intersections, 2.0, 2.0) ||
153+
containsPoint(intersections, 4.0, 4.0),
154+
"Should contain either (2,2) or (4,4)");
155+
}
156+
157+
@Test
158+
void testMultipleSegmentsAtOnePoint() {
159+
// Star pattern: 4 segments meeting at (2, 2)
160+
List<Object> segments = List.of(
161+
newSegment(0, 2, 4, 2), // horizontal
162+
newSegment(2, 0, 2, 4), // vertical
163+
newSegment(0, 0, 4, 4), // diagonal /
164+
newSegment(0, 4, 4, 0) // diagonal \
165+
);
166+
167+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
168+
assertTrue(containsPoint(intersections, 2.0, 2.0));
169+
// All segments meet at (2, 2), so should be reported once
170+
assertEquals(1, intersections.size());
171+
}
172+
173+
@Test
174+
void testGridPattern() {
175+
// 3x3 grid: should have 9 intersection points
176+
List<Object> segments = new ArrayList<>();
177+
178+
// Vertical lines at x = 0, 1, 2
179+
for (int i = 0; i <= 2; i++) {
180+
segments.add(newSegment(i, 0, i, 2));
181+
}
182+
183+
// Horizontal lines at y = 0, 1, 2
184+
for (int i = 0; i <= 2; i++) {
185+
segments.add(newSegment(0, i, 2, i));
186+
}
187+
188+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
189+
190+
// Each vertical line crosses each horizontal line
191+
// 3 vertical × 3 horizontal = 9 intersections
192+
assertEquals(9, intersections.size(), "3x3 grid should have 9 intersections");
193+
194+
// Verify all grid points are present
195+
for (int x = 0; x <= 2; x++) {
196+
for (int y = 0; y <= 2; y++) {
197+
assertTrue(containsPoint(intersections, x, y),
198+
String.format("Grid point (%d, %d) should be present", x, y));
199+
}
200+
}
201+
}
202+
203+
@Test
204+
void testTriangleIntersections() {
205+
// Three segments forming a triangle
206+
List<Object> segments = List.of(
207+
newSegment(0, 0, 4, 0), // base
208+
newSegment(0, 0, 2, 3), // left side
209+
newSegment(4, 0, 2, 3) // right side
210+
);
211+
212+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
213+
// Triangle vertices are intersections
214+
assertTrue(containsPoint(intersections, 0.0, 0.0));
215+
assertTrue(containsPoint(intersections, 4.0, 0.0));
216+
assertTrue(containsPoint(intersections, 2.0, 3.0));
217+
assertEquals(3, intersections.size());
218+
}
219+
220+
@Test
221+
void testCrossingDiagonals() {
222+
// X pattern with multiple crossings
223+
List<Object> segments = List.of(
224+
newSegment(0, 0, 10, 10),
225+
newSegment(0, 10, 10, 0),
226+
newSegment(5, 0, 5, 10),
227+
newSegment(0, 5, 10, 5)
228+
);
229+
230+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
231+
assertTrue(containsPoint(intersections, 5.0, 5.0), "Center point should be present");
232+
assertEquals(1, intersections.size());
233+
}
234+
235+
236+
237+
@Test
238+
void testVerySmallSegments() {
239+
List<Object> segments = List.of(
240+
newSegment(0.001, 0.001, 0.002, 0.002),
241+
newSegment(0.001, 0.002, 0.002, 0.001)
242+
);
243+
244+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
245+
assertEquals(1, intersections.size());
246+
assertTrue(containsPoint(intersections, 0.0015, 0.0015));
247+
}
248+
249+
@Test
250+
void testSegmentsShareCommonPoint() {
251+
List<Object> segmentsSameStart = List.of(
252+
newSegment(0, 0, 4, 4),
253+
newSegment(0, 0, 4, -4),
254+
newSegment(0, 0, -4, 4)
255+
);
256+
257+
Set<Point2D.Double> intersectionsSameStart = BentleyOttmann.findIntersections(cast(segmentsSameStart));
258+
assertTrue(containsPoint(intersectionsSameStart, 0.0, 0.0));
259+
List<Object> segmentsSameEnd = List.of(
260+
newSegment(0, 0, 4, 4),
261+
newSegment(8, 4, 4, 4),
262+
newSegment(4, 8, 4, 4)
263+
);
264+
265+
Set<Point2D.Double> intersectionsSameEnd = BentleyOttmann.findIntersections(cast(segmentsSameEnd));
266+
assertTrue(containsPoint(intersectionsSameEnd, 4.0, 4.0));
267+
}
268+
269+
@Test
270+
void testSegmentsAtAngles() {
271+
// Segments at 45, 90, 135 degrees
272+
List<Object> segments = List.of(
273+
newSegment(0, 2, 4, 2), // horizontal
274+
newSegment(2, 0, 2, 4), // vertical
275+
newSegment(0, 0, 4, 4), // 45 degrees
276+
newSegment(0, 4, 4, 0) // 135 degrees
277+
);
278+
279+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
280+
assertTrue(containsPoint(intersections, 2.0, 2.0));
281+
}
282+
283+
@Test
284+
void testPerformanceWithManySegments() {
285+
// Generate 100 random segments
286+
Random random = new Random(42); // Fixed seed for reproducibility
287+
List<Object> segments = new ArrayList<>();
288+
289+
for (int i = 0; i < 100; i++) {
290+
double x1 = random.nextDouble() * 100;
291+
double y1 = random.nextDouble() * 100;
292+
double x2 = random.nextDouble() * 100;
293+
double y2 = random.nextDouble() * 100;
294+
segments.add(newSegment(x1, y1, x2, y2));
295+
}
296+
297+
long startTime = System.currentTimeMillis();
298+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
299+
long endTime = System.currentTimeMillis();
300+
301+
long duration = endTime - startTime;
302+
303+
// Should complete in reasonable time (< 1 second for 100 segments)
304+
assertTrue(duration < 1000,
305+
"Algorithm should complete in less than 1 second for 100 segments. Took: " + duration + "ms");
306+
307+
// Just verify it returns a valid result
308+
assertNotNull(intersections);
309+
System.out.println("Performance test: 100 segments processed in " + duration +
310+
"ms, found " + intersections.size() + " intersections");
311+
}
312+
313+
@Test
314+
void testIssueExample() {
315+
// Example from the GitHub issue
316+
List<Object> segments = List.of(
317+
newSegment(1, 1, 5, 5), // Segment A
318+
newSegment(1, 5, 5, 1), // Segment B
319+
newSegment(3, 0, 3, 6) // Segment C
320+
);
321+
322+
Set<Point2D.Double> intersections = BentleyOttmann.findIntersections(cast(segments));
323+
324+
// Expected output: [(3, 3)]
325+
assertEquals(1, intersections.size(), "Should find exactly one intersection");
326+
assertTrue(containsPoint(intersections, 3.0, 3.0),
327+
"Intersection should be at (3, 3)");
328+
}
329+
330+
private static Object newSegment(double x1, double y1, double x2, double y2) {
331+
return new BentleyOttmann.Segment(new Point2D.Double(x1, y1), new Point2D.Double(x2, y2));
332+
}
333+
334+
private static List<BentleyOttmann.Segment> cast(List<Object> objs) {
335+
List<BentleyOttmann.Segment> result = new ArrayList<>();
336+
for (Object o : objs) result.add((BentleyOttmann.Segment) o);
337+
return result;
338+
}
339+
340+
private static boolean containsPoint(Set<Point2D.Double> points, double x, double y) {
341+
return points.stream().anyMatch(p -> Math.abs(p.x - x) < EPS && Math.abs(p.y - y) < EPS);
342+
}
343+
}

0 commit comments

Comments
 (0)