View Javadoc
1   package org.djutils.draw.line;
2   
3   import static org.junit.Assert.assertEquals;
4   import static org.junit.Assert.assertFalse;
5   import static org.junit.Assert.assertNotEquals;
6   import static org.junit.Assert.assertNull;
7   import static org.junit.Assert.assertTrue;
8   import static org.junit.Assert.fail;
9   
10  import java.util.Iterator;
11  import java.util.NoSuchElementException;
12  
13  import org.djutils.base.AngleUtil;
14  import org.djutils.draw.DrawRuntimeException;
15  import org.djutils.draw.bounds.Bounds2d;
16  import org.djutils.draw.point.OrientedPoint2d;
17  import org.djutils.draw.point.Point2d;
18  import org.junit.Test;
19  
20  /**
21   * Ray2dTest.java.
22   * <p>
23   * Copyright (c) 2021-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
24   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
25   * </p>
26   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
27   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
28   */
29  public class Ray2dTest
30  {
31      /**
32       * Test the various constructors of a Ray2d.
33       */
34      @Test
35      public void testConstructors()
36      {
37          verifyRay("Constructor from x, y, phi", new Ray2d(1, 2, 3), 1, 2, 3);
38          verifyRay("Constructor from Point2d, phi", new Ray2d(new Point2d(0.1, 0.2), -0.3), 0.1, 0.2, -0.3);
39          verifyRay("Constructor from x, y, throughX, throughY", new Ray2d(1, 2, 3, 5), 1, 2, Math.atan2(3, 2));
40          verifyRay("Constructor from x, y, throughX, throughY", new Ray2d(1, 2, 1, 5), 1, 2, Math.atan2(3, 0));
41          verifyRay("Constructor from x, y, throughX, throughY", new Ray2d(1, 2, 3, 2), 1, 2, Math.atan2(0, 2));
42          verifyRay("Constructor from Point2d, throughX, throughY", new Ray2d(new Point2d(1, 2), 3, 5), 1, 2, Math.atan2(3, 2));
43          verifyRay("Constructor from Point2d, throughX, throughY", new Ray2d(new Point2d(1, 2), 1, 5), 1, 2, Math.atan2(3, 0));
44          verifyRay("Constructor from Point2d, throughX, throughY", new Ray2d(new Point2d(1, 2), 3, 2), 1, 2, Math.atan2(0, 2));
45          verifyRay("Constructor from x, y, Point2d", new Ray2d(1, 2, new Point2d(3, 5)), 1, 2, Math.atan2(3, 2));
46          verifyRay("Constructor from x, y, Point2d", new Ray2d(1, 2, new Point2d(1, 5)), 1, 2, Math.atan2(3, 0));
47          verifyRay("Constructor from x, y, Point2d", new Ray2d(1, 2, new Point2d(3, 2)), 1, 2, Math.atan2(0, 2));
48          verifyRay("Constructor from Point2d, Point2d", new Ray2d(new Point2d(1, 2), new Point2d(3, 5)), 1, 2, Math.atan2(3, 2));
49          verifyRay("Constructor from Point2d, Point2d", new Ray2d(new Point2d(1, 2), new Point2d(1, 5)), 1, 2, Math.atan2(3, 0));
50          verifyRay("Constructor from Point2d, Point2d", new Ray2d(new Point2d(1, 2), new Point2d(3, 2)), 1, 2, Math.atan2(0, 2));
51  
52          try
53          {
54              new Ray2d(1, 2, Double.NaN);
55              fail("NaN for phy should have thrown a DrawRuntimeException");
56          }
57          catch (DrawRuntimeException dre)
58          {
59              // Ignore expected exception
60          }
61  
62          try
63          {
64              new Ray2d(null, 1);
65              fail("null for point should have thrown a NullPointerException");
66          }
67          catch (NullPointerException dre)
68          {
69              // Ignore expected exception
70          }
71  
72          try
73          {
74              new Ray2d(1, 2, 1, 2);
75              fail("Same coordinates for through point should have thrown a DrawRuntimeException");
76          }
77          catch (DrawRuntimeException dre)
78          {
79              // Ignore expected exception
80          }
81  
82          try
83          {
84              new Ray2d(1, 2, new Point2d(1, 2));
85              fail("Same coordinates for through point should have thrown a DrawRuntimeException");
86          }
87          catch (DrawRuntimeException dre)
88          {
89              // Ignore expected exception
90          }
91  
92          try
93          {
94              new Ray2d(new Point2d(1, 2), 1, 2);
95              fail("Same coordinates for through point should have thrown a DrawRuntimeException");
96          }
97          catch (DrawRuntimeException dre)
98          {
99              // Ignore expected exception
100         }
101 
102         try
103         {
104             new Ray2d(1, 2, null);
105             fail("null for through point should have thrown a NullPointerException");
106         }
107         catch (NullPointerException dre)
108         {
109             // Ignore expected exception
110         }
111 
112         try
113         {
114             new Ray2d(null, new Point2d(3, 4));
115             fail("null for point should have thrown a NullPointerException");
116         }
117         catch (NullPointerException dre)
118         {
119             // Ignore expected exception
120         }
121 
122         try
123         {
124             new Ray2d(new Point2d(1, 2), null);
125             fail("null for through point should have thrown a NullPointerException");
126         }
127         catch (NullPointerException dre)
128         {
129             // Ignore expected exception
130         }
131 
132         Ray2d ray = new Ray2d(1, 2, 3);
133         assertTrue("toString returns something descriptive", ray.toString().startsWith("Ray2d"));
134         assertTrue("toString can suppress the class name", ray.toString().indexOf(ray.toString(true)) > 0);
135     }
136 
137     /**
138      * Verify all fields of a Ray2d with a tolerance of 0.0001.
139      * @param description String; description of the test
140      * @param ray Ray2d; the Ray2d
141      * @param expectedX double; the expected x value
142      * @param expectedY double; the expected y value
143      * @param expectedPhi double; the expected phi value
144      */
145     private void verifyRay(final String description, final Ray2d ray, final double expectedX, final double expectedY,
146             final double expectedPhi)
147     {
148         assertEquals(description + " getX", expectedX, ray.getX(), 0.0001);
149         assertEquals(description + " x", expectedX, ray.x, 0.0001);
150         assertEquals(description + " getY", expectedY, ray.getY(), 0.0001);
151         assertEquals(description + " y", expectedY, ray.y, 0.0001);
152         assertEquals(description + " getPhi", expectedPhi, ray.getPhi(), 0.0001);
153         assertEquals(description + " phi", expectedPhi, ray.phi, 0.0001);
154         Point2d startPoint = ray.getEndPoint();
155         assertEquals(description + " getStartPoint x", expectedX, startPoint.x, 0.0001);
156         assertEquals(description + " getStartPoint y", expectedY, startPoint.y, 0.0001);
157         Ray2d negated = ray.neg();
158         assertEquals(description + " neg x", -expectedX, negated.x, 0.0001);
159         assertEquals(description + " neg y", -expectedY, negated.y, 0.0001);
160         assertEquals(description + " neg phi", expectedPhi + Math.PI, negated.phi, 0.0001);
161         Ray2d flipped = ray.flip();
162         assertEquals(description + " getX", expectedX, flipped.getX(), 0.0001);
163         assertEquals(description + " x", expectedX, flipped.x, 0.0001);
164         assertEquals(description + " getY", expectedY, flipped.getY(), 0.0001);
165         assertEquals(description + " y", expectedY, flipped.y, 0.0001);
166         assertEquals(description + " getPhi", expectedPhi + Math.PI, flipped.getPhi(), 0.0001);
167         assertEquals(description + " phi", expectedPhi + Math.PI, flipped.phi, 0.0001);
168         assertEquals(description + " size", 2, ray.size());
169         Iterator<Point2d> iterator = ray.getPoints();
170         // First result of iterator is the finite end point (but this is not a hard promise)
171         assertTrue(iterator.hasNext());
172         Point2d point = iterator.next();
173         assertEquals(description + " iterator first point x", expectedX, point.x, 0.0001);
174         assertEquals(description + " iterator first point y", expectedY, point.y, 0.0001);
175         assertTrue(iterator.hasNext());
176         point = iterator.next();
177         // We only check that the point is infinite in at least one direction; the boundTest covers the rest
178         assertTrue(description + " iterator second point is at infinity",
179                 Double.isInfinite(point.x) || Double.isInfinite(point.y));
180         assertFalse(iterator.hasNext());
181         try
182         {
183             iterator.next();
184             fail("Should have thrown a NoSuchElementException");
185         }
186         catch (NoSuchElementException nsee)
187         {
188             // Ignore expected exception
189         }
190     }
191 
192     /**
193      * Test the result of the getBounds method.
194      */
195     @Test
196     public void boundsTest()
197     {
198         // X direction
199         // Angle of 0 is exact; bounds should be infinite in only the positive X direction
200         verifyBounds(new Ray2d(1, 2, 0).getBounds(), 1, 2, Double.POSITIVE_INFINITY, 2);
201 
202         // first quadrant
203         verifyBounds(new Ray2d(1, 2, 0.2).getBounds(), 1, 2, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
204 
205         // Math.PI / 2 is in first quadrant due to finite precision of a double
206         verifyBounds(new Ray2d(1, 2, Math.PI / 2).getBounds(), 1, 2, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
207 
208         // second quadrant
209         verifyBounds(new Ray2d(1, 2, 2).getBounds(), Double.NEGATIVE_INFINITY, 2, 1, Double.POSITIVE_INFINITY);
210 
211         // Math.PI is in second quadrant due to finite precision of a double
212         verifyBounds(new Ray2d(1, 2, Math.PI).getBounds(), Double.NEGATIVE_INFINITY, 2, 1, Double.POSITIVE_INFINITY);
213 
214         // third quadrant
215         verifyBounds(new Ray2d(1, 2, 4).getBounds(), Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, 1, 2);
216 
217         // fourth quadrant
218         verifyBounds(new Ray2d(1, 2, -1).getBounds(), 1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 2);
219 
220         // -Math.PI / 2 is in fourth quadrant due to finite precision of a double
221         verifyBounds(new Ray2d(1, 2, -Math.PI / 2).getBounds(), 1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 2);
222 
223     }
224 
225     /**
226      * Verify a Bounds object.
227      * @param bounds Bounds2d; the Bounds object to verify
228      * @param expectedMinX double; the expected minimum x value
229      * @param expectedMinY double; the expected minimum y value
230      * @param expectedMaxX double; the expected maximum x value
231      * @param expectedMaxY double; the expected maximum y value
232      */
233     private void verifyBounds(final Bounds2d bounds, final double expectedMinX, final double expectedMinY,
234             final double expectedMaxX, final double expectedMaxY)
235     {
236         assertEquals("Bounds minX", expectedMinX, bounds.getMinX(), 0.0001);
237         assertEquals("Bounds minY", expectedMinY, bounds.getMinY(), 0.0001);
238         assertEquals("Bounds maxX", expectedMaxX, bounds.getMaxX(), 0.0001);
239         assertEquals("Bounds maxY", expectedMaxY, bounds.getMaxY(), 0.0001);
240     }
241 
242     /**
243      * Test the getLocation and getLocationExtended methods.
244      */
245     @Test
246     public void testLocation()
247     {
248         try
249         {
250             new Ray2d(1, 2, 1).getLocation(Double.NaN);
251             fail("NaN position should have thrown a DrawRuntimeException");
252         }
253         catch (DrawRuntimeException dre)
254         {
255             // Ignore expected exception
256         }
257 
258         try
259         {
260             new Ray2d(1, 2, 1).getLocation(-1);
261             fail("Negative position should have thrown a DrawRuntimeException");
262         }
263         catch (DrawRuntimeException dre)
264         {
265             // Ignore expected exception
266         }
267 
268         try
269         {
270             new Ray2d(1, 2, 1).getLocation(Double.POSITIVE_INFINITY);
271             fail("Infinite position should have thrown a DrawRuntimeException");
272         }
273         catch (DrawRuntimeException dre)
274         {
275             // Ignore expected exception
276         }
277 
278         try
279         {
280             new Ray2d(1, 2, 1).getLocation(Double.NEGATIVE_INFINITY);
281             fail("Infinite position should have thrown a DrawRuntimeException");
282         }
283         catch (DrawRuntimeException dre)
284         {
285             // Ignore expected exception
286         }
287 
288         try
289         {
290             new Ray2d(1, 2, 1).getLocationExtended(Double.POSITIVE_INFINITY);
291             fail("Infinite position should have thrown a DrawRuntimeException");
292         }
293         catch (DrawRuntimeException dre)
294         {
295             // Ignore expected exception
296         }
297 
298         try
299         {
300             new Ray2d(1, 2, 1).getLocationExtended(Double.NEGATIVE_INFINITY);
301             fail("Infinite position should have thrown a DrawRuntimeException");
302         }
303         catch (DrawRuntimeException dre)
304         {
305             // Ignore expected exception
306         }
307 
308         try
309         {
310             new Ray2d(1, 2, 1).getLocationExtended(Double.NaN);
311             fail("NaN position should have thrown a DrawRuntimeException");
312         }
313         catch (DrawRuntimeException dre)
314         {
315             // Ignore expected exception
316         }
317 
318         for (double phi : new double[] { 0, 1, 2, 3, 4, 5, -1, -2, Math.PI })
319         {
320             Ray2d ray = new Ray2d(1, 2, phi);
321             for (double position : new double[] { 0, 10, 0.1, -2 })
322             {
323                 Ray2d result = ray.getLocationExtended(position);
324                 assertEquals("result is position distance away from base of ray", Math.abs(position), ray.distance(result),
325                         0.001);
326                 assertEquals("result has same phi as ray", ray.phi, result.phi, 0.00001);
327                 assertTrue("Reverse position on result yields ray",
328                         ray.epsilonEquals(result.getLocationExtended(-position), 0.0001));
329                 if (position > 0)
330                 {
331                     assertEquals("result lies in on ray", AngleUtil.normalizeAroundZero(ray.phi), ray.directionTo(result),
332                             0.0001);
333                 }
334                 if (position < 0)
335                 {
336                     assertEquals("ray lies on result", AngleUtil.normalizeAroundZero(result.phi), result.directionTo(ray),
337                             0.0001);
338                 }
339             }
340         }
341     }
342 
343     /**
344      * Test the closestPointOnRay and the projectOrthogonal methods.
345      */
346     @Test
347     public void testClosestPointAndProjectOrthogonal()
348     {
349         Ray2d ray = new Ray2d(1, 2, 1);
350         try
351         {
352             ray.closestPointOnRay(null);
353             fail("Null for point should have thrown a NullPointerException");
354         }
355         catch (NullPointerException npe)
356         {
357             // Ignore expected exception
358         }
359 
360         Point2d result = ray.closestPointOnRay(new Point2d(1, 0));
361         assertEquals("result is start point", ray.x, result.x, 0);
362         assertEquals("result is start point", ray.y, result.y, 0);
363         result = ray.closestPointOnRay(new Point2d(0, 2));
364         assertEquals("result is start point", ray.x, result.x, 0);
365         assertEquals("result is start point", ray.y, result.y, 0);
366         result = ray.closestPointOnRay(new Point2d(1, 2));
367         assertEquals("result is start point", ray.x, result.x, 0);
368         assertEquals("result is start point", ray.y, result.y, 0);
369 
370         assertNull("projection misses the ray", ray.projectOrthogonal(new Point2d(1, 0)));
371         assertNull("projection misses the ray", ray.projectOrthogonal(new Point2d(0, 2)));
372         assertEquals("projection hits start point of ray", new Point2d(1, 2), ray.projectOrthogonal(new Point2d(1, 2)));
373         assertEquals("extended projection returns same point as projection on sufficiently long line segment", 0,
374                 new LineSegment2d(ray.getLocationExtended(-100), ray.getLocation(100)).closestPointOnSegment(new Point2d(1, 0))
375                         .distance(ray.projectOrthogonalExtended(new Point2d(1, 0))),
376                 0.0001);
377 
378         Point2d projectingPoint = new Point2d(10, 10);
379         result = ray.closestPointOnRay(projectingPoint); // Projects at a point along the ray
380         double distance = result.distance(ray.getEndPoint());
381         assertTrue("distance from start is > 0", distance > 0);
382         // Angle startPoint-result-test-projectingPoint should be 90 degrees
383         double angle = ray.getPhi() - result.directionTo(projectingPoint);
384         assertEquals("angle should be about 90 degrees", Math.PI / 2, Math.abs(AngleUtil.normalizeAroundZero(angle)), 0.0001);
385         assertEquals("projection hits closest point on the ray", 0, result.distance(ray.projectOrthogonal(projectingPoint)),
386                 0.0001);
387         assertEquals("projectOrthogonalExtended returns same result as long as orthogonal projection exists", 0,
388                 result.distance(ray.projectOrthogonalExtended(projectingPoint)), 0.0001);
389     }
390 
391     /**
392      * Test the project methods.
393      */
394     @Test
395     public void testProject()
396     {
397         Ray2d ray = new Ray2d(1, 2, 20, 10);
398         assertTrue("projects outside", Double.isNaN(ray.projectOrthogonalFractional(new Point2d(1, 1))));
399         assertTrue("projects before start", ray.projectOrthogonalFractionalExtended(new Point2d(1, 1)) < 0);
400         assertEquals("projects at", -new Point2d(1 - 19 - 19, 2 - 8 - 8).distance(ray),
401                 ray.projectOrthogonalFractionalExtended(new Point2d(1 - 19 - 19 + 8, 2 - 8 - 8 - 19)), 0.0001);
402         // Projection of projection is projection
403         for (int x = -2; x < 5; x++)
404         {
405             for (int y = -2; y < 5; y++)
406             {
407                 Point2d point = new Point2d(x, y);
408                 double fraction = ray.projectOrthogonalFractionalExtended(point);
409                 if (fraction < 0)
410                 {
411                     assertTrue("non extended version yields NaN", Double.isNaN(ray.projectOrthogonalFractional(point)));
412                     assertNull("non extended projectOrthogonal yields null", ray.projectOrthogonal(point));
413                 }
414                 else
415                 {
416                     assertEquals("non extended version yields same", fraction, ray.projectOrthogonalFractional(point), 0.00001);
417                     assertEquals("non extended version yields same as extended version", ray.projectOrthogonal(point),
418                             ray.projectOrthogonalExtended(point));
419                 }
420                 Point2d projected = ray.projectOrthogonalExtended(point);
421                 assertEquals("projecting projected point yields same", fraction,
422                         ray.projectOrthogonalFractionalExtended(projected), 0.00001);
423             }
424         }
425     }
426 
427     /**
428      * Test the epsilonEquals method.
429      */
430     @Test
431     public void epsilonEqualsTest()
432     {
433         Ray2d ray = new Ray2d(1, 2, -1);
434         try
435         {
436             ray.epsilonEquals((Ray2d) null, 1, 1);
437             fail("Null pointer should have thrown a NullPointerException");
438         }
439         catch (NullPointerException npe)
440         {
441             // Ignore expected exception
442         }
443 
444         try
445         {
446             ray.epsilonEquals(ray, -0.1, 1);
447             fail("Negative epsilonCoordinate should have thrown an IllegalArgumentException");
448         }
449         catch (IllegalArgumentException npe)
450         {
451             // Ignore expected exception
452         }
453 
454         try
455         {
456             ray.epsilonEquals(ray, 1, -0.1);
457             fail("Negative epsilonDirection should have thrown an IllegalArgumentException");
458         }
459         catch (IllegalArgumentException npe)
460         {
461             // Ignore expected exception
462         }
463 
464         try
465         {
466             ray.epsilonEquals(ray, Double.NaN, 1);
467             fail("NaN epsilonCoordinate should have thrown an IllegalArgumentException");
468         }
469         catch (IllegalArgumentException npe)
470         {
471             // Ignore expected exception
472         }
473 
474         try
475         {
476             ray.epsilonEquals(ray, 1, Double.NaN);
477             fail("NaN epsilonDirection should have thrown an IllegalArgumentException");
478         }
479         catch (IllegalArgumentException npe)
480         {
481             // Ignore expected exception
482         }
483 
484         double[] deltas = new double[] { 0.0, -0.125, 0.125, -1, 1 }; // Use values that can be represented exactly in a double
485         for (double dX : deltas)
486         {
487             for (double dY : deltas)
488             {
489                 for (double dPhi : deltas)
490                 {
491                     Ray2d other = new Ray2d(ray.x + dX, ray.y + dY, ray.phi + dPhi);
492                     for (double epsilon : new double[] { 0, 0.125, 0.5, 0.9, 1.0, 1.1 })
493                     {
494                         // System.out.println(String.format("dX=%f, dY=%f, dPhi=%f, epsilon=%f", dX, dY, dPhi, epsilon));
495                         boolean result = ray.epsilonEquals(other, epsilon, Double.POSITIVE_INFINITY);
496                         boolean expected = Math.abs(dX) <= epsilon && Math.abs(dY) <= epsilon;
497                         assertEquals("result of epsilonEquals checking x, y, z", expected, result);
498 
499                         result = ray.epsilonEquals(other, Double.POSITIVE_INFINITY, epsilon);
500                         expected = Math.abs(dPhi) <= epsilon;
501                         assertEquals("result of epsilonEquals checking phi", expected, result);
502                     }
503                 }
504             }
505         }
506     }
507 
508     /**
509      * Test the equals and hasCode methods.
510      */
511     @Test
512     public void equalsAndHashCodeTest()
513     {
514         Ray2d ray = new Ray2d(1, 2, 11, 12);
515         assertEquals("equal to itself", ray, ray);
516         assertNotEquals("not equal to null", ray, null);
517         assertNotEquals("not equal to different object with same parent class", ray, new OrientedPoint2d(1, 2));
518         assertNotEquals("not equal to ray with different direction", ray, new Ray2d(1, 2, 11, 10));
519         assertNotEquals("not equal to ray with different start x", ray, new Ray2d(2, 2, 12, 12));
520         assertNotEquals("not equal to ray with different start y", ray, new Ray2d(1, 3, 12, 13));
521         assertEquals("equal to ray with same x, y and direction", ray, new Ray2d(1, 2, 21, 22));
522 
523         assertNotEquals("hashCode depends on x", ray.hashCode(), new Ray2d(2, 2, 12, 12));
524         assertNotEquals("hashCode depends on y", ray.hashCode(), new Ray2d(1, 3, 11, 13));
525         assertNotEquals("hashCode depends on phi", ray.hashCode(), new Ray2d(1, 2, 11, 10));
526     }
527 
528 }