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