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.draw.DrawRuntimeException;
14  import org.djutils.draw.bounds.Bounds3d;
15  import org.djutils.draw.point.OrientedPoint3d;
16  import org.djutils.draw.point.Point3d;
17  import org.junit.Test;
18  
19  /**
20   * Ray3dTest.java.
21   * <p>
22   * Copyright (c) 2021-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
23   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
24   * </p>
25   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
26   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
27   */
28  public class Ray3dTest
29  {
30      /**
31       * Test the various constructors of a Ray3d.
32       */
33      @Test
34      public void testConstructors()
35      {
36          // Verify theta and phi for the six basic directions.
37          verifyRay("positive x", new Ray3d(0, 0, 0, 1, 0, 0), 0, 0, 0, 0, Math.PI / 2);
38          verifyRay("positive y", new Ray3d(0, 0, 0, 0, 1, 0), 0, 0, 0, Math.PI / 2, Math.PI / 2);
39          verifyRay("positive z", new Ray3d(0, 0, 0, 0, 0, 1), 0, 0, 0, 0, 0);
40          verifyRay("negative x", new Ray3d(0, 0, 0, -1, 0, 0), 0, 0, 0, Math.PI, Math.PI / 2);
41          verifyRay("negative y", new Ray3d(0, 0, 0, 0, -1, 0), 0, 0, 0, -Math.PI / 2, Math.PI / 2);
42          verifyRay("negative z", new Ray3d(0, 0, 0, 0, 0, -1), 0, 0, 0, 0, Math.PI);
43          verifyRay("Constructor from x, y, phi, theta", new Ray3d(1, 2, 3, 4, 5), 1, 2, 3, 4, 5);
44          verifyRay("Constructor from Point3d, phi, theta", new Ray3d(new Point3d(0.1, 0.2, 0.3), -0.4, -0.5), 0.1, 0.2, 0.3,
45                  -0.4, -0.5);
46          verifyRay("Constructor from x, y, z, throughX, throughY, throughZ", new Ray3d(1, 2, 3, 4, 6, 15), 1, 2, 3,
47                  Math.atan2(4, 3), Math.atan2(5, 12));
48          verifyRay("Constructor from x, y, z, throughX, throughY, throughZ", new Ray3d(1, 2, 3, 1, 6, 15), 1, 2, 3,
49                  Math.atan2(4, 0), Math.atan2(4, 12));
50          verifyRay("Constructor from x, y, z, throughX, throughY, throughZ", new Ray3d(1, 2, 3, 1, 2, 15), 1, 2, 3,
51                  Math.atan2(0, 0), Math.atan2(0, 12));
52          verifyRay("Constructor from Point3d, throughX, throughY, throughZ", new Ray3d(new Point3d(1, 2, 3), 4, 6, 15), 1, 2, 3,
53                  Math.atan2(4, 3), Math.atan2(5, 12));
54          verifyRay("Constructor from Point3d, throughX, throughY, throughZ", new Ray3d(new Point3d(1, 2, 3), 1, 6, 15), 1, 2, 3,
55                  Math.atan2(4, 0), Math.atan2(4, 12));
56          verifyRay("Constructor from Point3d, throughX, throughY, throughZ", new Ray3d(new Point3d(1, 2, 3), 1, 2, 15), 1, 2, 3,
57                  Math.atan2(0, 0), Math.atan2(0, 12));
58          verifyRay("Constructor from x, y, z, Point3d", new Ray3d(1, 2, 3, new Point3d(4, 6, 15)), 1, 2, 3, Math.atan2(4, 3),
59                  Math.atan2(5, 12));
60          verifyRay("Constructor from x, y, z, Point3d", new Ray3d(1, 2, 3, new Point3d(1, 6, 15)), 1, 2, 3, Math.atan2(4, 0),
61                  Math.atan2(4, 12));
62          verifyRay("Constructor from x, y, z, Point3d", new Ray3d(1, 2, 3, new Point3d(1, 2, 15)), 1, 2, 3, Math.atan2(0, 0),
63                  Math.atan2(0, 12));
64          verifyRay("Constructor from Point3d, Point3d", new Ray3d(new Point3d(1, 2, 3), new Point3d(4, 6, 15)), 1, 2, 3,
65                  Math.atan2(4, 3), Math.atan2(5, 12));
66          verifyRay("Constructor from Point3d, Point3d", new Ray3d(new Point3d(1, 2, 3), new Point3d(1, 6, 15)), 1, 2, 3,
67                  Math.atan2(4, 0), Math.atan2(4, 12));
68          verifyRay("Constructor from Point3d, Point3d", new Ray3d(new Point3d(1, 2, 3), new Point3d(1, 2, 15)), 1, 2, 3,
69                  Math.atan2(0, 0), Math.atan2(0, 12));
70  
71          try
72          {
73              new Ray3d(1, 2, 3, Double.NaN, 0);
74              fail("NaN for phy should have thrown a DrawRuntimeException");
75          }
76          catch (DrawRuntimeException dre)
77          {
78              // Ignore expected exception
79          }
80  
81          try
82          {
83              new Ray3d(1, 2, 3, 0, Double.NaN);
84              fail("NaN for theta should have thrown a DrawRuntimeException");
85          }
86          catch (DrawRuntimeException dre)
87          {
88              // Ignore expected exception
89          }
90  
91          try
92          {
93              new Ray3d(null, 1, 2);
94              fail("null for point should have thrown a NullPointerException");
95          }
96          catch (NullPointerException dre)
97          {
98              // Ignore expected exception
99          }
100 
101         try
102         {
103             new Ray3d(1, 2, 3, 1, 2, 3);
104             fail("Same coordinates for through point should have thrown a DrawRuntimeException");
105         }
106         catch (DrawRuntimeException dre)
107         {
108             // Ignore expected exception
109         }
110 
111         try
112         {
113             new Ray3d(1, 2, 3, new Point3d(1, 2, 3));
114             fail("Same coordinates for through point should have thrown a DrawRuntimeException");
115         }
116         catch (DrawRuntimeException dre)
117         {
118             // Ignore expected exception
119         }
120 
121         try
122         {
123             new Ray3d(new Point3d(1, 2, 3), 1, 2, 3);
124             fail("Same coordinates for through point should have thrown a DrawRuntimeException");
125         }
126         catch (DrawRuntimeException dre)
127         {
128             // Ignore expected exception
129         }
130 
131         try
132         {
133             new Ray3d(1, 2, 3, null);
134             fail("null for through point should have thrown a NullPointerException");
135         }
136         catch (NullPointerException dre)
137         {
138             // Ignore expected exception
139         }
140 
141         try
142         {
143             new Ray3d(null, new Point3d(4, 5, 6));
144             fail("null for point should have thrown a NullPointerException");
145         }
146         catch (NullPointerException dre)
147         {
148             // Ignore expected exception
149         }
150 
151         try
152         {
153             new Ray3d(new Point3d(1, 2, 3), null);
154             fail("null for through point should have thrown a NullPointerException");
155         }
156         catch (NullPointerException dre)
157         {
158             // Ignore expected exception
159         }
160 
161         Ray3d ray = new Ray3d(1, 2, 3, 0.2, 0.3);
162         assertTrue("toString returns something descriptive", ray.toString().startsWith("Ray3d"));
163         assertTrue("toString can suppress the class name", ray.toString().indexOf(ray.toString(true)) > 0);
164     }
165 
166     /**
167      * Verify all fields of a Ray3d with a tolerance of 0.0001.
168      * @param description String; description of the test
169      * @param ray Ray3d; the Ray3d
170      * @param expectedX double; the expected x value
171      * @param expectedY double; the expected y value
172      * @param expectedZ double; the expected z value
173      * @param expectedPhi double; the expected phi value
174      * @param expectedTheta double; the expected theta value
175      */
176     private void verifyRay(final String description, final Ray3d ray, final double expectedX, final double expectedY,
177             final double expectedZ, final double expectedPhi, final double expectedTheta)
178     {
179         assertEquals(description + " getX", expectedX, ray.getX(), 0.0001);
180         assertEquals(description + " x", expectedX, ray.x, 0.0001);
181         assertEquals(description + " getY", expectedY, ray.getY(), 0.0001);
182         assertEquals(description + " y", expectedY, ray.y, 0.0001);
183         assertEquals(description + " getZ", expectedZ, ray.getZ(), 0.0001);
184         assertEquals(description + " z", expectedZ, ray.z, 0.0001);
185         assertEquals(description + " getPhi", expectedPhi, ray.getPhi(), 0.0001);
186         assertEquals(description + " phi", expectedPhi, ray.phi, 0.0001);
187         assertEquals(description + " getTheta", expectedTheta, ray.getTheta(), 0.0001);
188         assertEquals(description + " theta", expectedTheta, ray.theta, 0.0001);
189         Point3d startPoint = ray.getEndPoint();
190         assertEquals(description + " getStartPoint x", expectedX, startPoint.x, 0.0001);
191         assertEquals(description + " getStartPoint y", expectedY, startPoint.y, 0.0001);
192         assertEquals(description + " getStartPoint z", expectedZ, startPoint.z, 0.0001);
193         Ray3d negated = ray.neg();
194         assertEquals(description + " neg x", -expectedX, negated.x, 0.0001);
195         assertEquals(description + " neg y", -expectedY, negated.y, 0.0001);
196         assertEquals(description + " neg z", -expectedZ, negated.z, 0.0001);
197         assertEquals(description + " neg phi", expectedPhi + Math.PI, negated.phi, 0.0001);
198         assertEquals(description + " neg theta", expectedTheta + Math.PI, negated.theta, 0.0001);
199         Ray3d flipped = ray.flip();
200         assertEquals(description + " getX", expectedX, flipped.getX(), 0.0001);
201         assertEquals(description + " x", expectedX, flipped.x, 0.0001);
202         assertEquals(description + " getY", expectedY, flipped.getY(), 0.0001);
203         assertEquals(description + " y", expectedY, flipped.y, 0.0001);
204         assertEquals(description + " getZ", expectedZ, flipped.getZ(), 0.0001);
205         assertEquals(description + " z", expectedZ, flipped.z, 0.0001);
206         assertEquals(description + " getPhi", expectedPhi + Math.PI, flipped.getPhi(), 0.0001);
207         assertEquals(description + " phi", expectedPhi + Math.PI, flipped.phi, 0.0001);
208         assertEquals(description + " getTheta", Math.PI - expectedTheta, flipped.getTheta(), 0.0001);
209         assertEquals(description + " theta", Math.PI - expectedTheta, flipped.theta, 0.0001);
210         assertEquals(description + " size", 2, ray.size());
211         Iterator<Point3d> iterator = ray.getPoints();
212         // First result of iterator is the finite end point (but this is not a hard promise)
213         assertTrue(iterator.hasNext());
214         Point3d point = iterator.next();
215         assertEquals(description + " iterator first point x", expectedX, point.x, 0.0001);
216         assertEquals(description + " iterator first point y", expectedY, point.y, 0.0001);
217         assertEquals(description + " iterator first point z", expectedZ, point.z, 0.0001);
218         assertTrue(iterator.hasNext());
219         point = iterator.next();
220         // We only check that the point is infinite in at least one direction; the boundTest covers the rest
221         assertTrue(description + " iterator second point is at infinity",
222                 Double.isInfinite(point.x) || Double.isInfinite(point.y) || Double.isInfinite(point.z));
223         assertFalse(iterator.hasNext());
224         try
225         {
226             iterator.next();
227             fail("Should have thrown a NoSuchElementException");
228         }
229         catch (NoSuchElementException nsee)
230         {
231             // Ignore expected exception
232         }
233     }
234 
235     /**
236      * Test the result of the getBounds method.
237      */
238     @Test
239     public void boundsTest()
240     {
241         // X direction
242         // Angle of 0 is exact; bounds should be infinite in only the positive X direction
243         verifyBounds(new Ray3d(1, 2, 3, 0, 1).getBounds(), 1, 2, 3, Double.POSITIVE_INFINITY, 2, Double.POSITIVE_INFINITY);
244 
245         // Z direction
246         // Angle of 0 is exact; bounds should be infinite in only the positive X direction
247         verifyBounds(new Ray3d(1, 2, 3, 0, 0).getBounds(), 1, 2, 3, 1, 2, Double.POSITIVE_INFINITY);
248 
249         // first quadrant in XY, pointing up (positive Z)
250         verifyBounds(new Ray3d(1, 2, 3, 0.2, 1.1).getBounds(), 1, 2, 3, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY,
251                 Double.POSITIVE_INFINITY);
252 
253         // Math.PI / 2 is in first quadrant due to finite precision of a double
254         verifyBounds(new Ray3d(1, 2, 3, Math.PI / 2, 1).getBounds(), 1, 2, 3, Double.POSITIVE_INFINITY,
255                 Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
256 
257         // second quadrant in XY, pointing up
258         verifyBounds(new Ray3d(1, 2, 3, 2, 1).getBounds(), Double.NEGATIVE_INFINITY, 2, 3, 1, Double.POSITIVE_INFINITY,
259                 Double.POSITIVE_INFINITY);
260 
261         // Math.PI is in second quadrant due to finite precision of a double
262         verifyBounds(new Ray3d(1, 2, 3, Math.PI, 1).getBounds(), Double.NEGATIVE_INFINITY, 2, 3, 1, Double.POSITIVE_INFINITY,
263                 Double.POSITIVE_INFINITY);
264 
265         // third quadrant
266         verifyBounds(new Ray3d(1, 2, 3, 4, 1).getBounds(), Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, 3, 1, 2,
267                 Double.POSITIVE_INFINITY);
268 
269         // fourth quadrant
270         verifyBounds(new Ray3d(1, 2, 3, -1, 1).getBounds(), 1, Double.NEGATIVE_INFINITY, 3, Double.POSITIVE_INFINITY, 2,
271                 Double.POSITIVE_INFINITY);
272 
273         // -Math.PI / 2 is in fourth quadrant due to finite precision of a double
274         verifyBounds(new Ray3d(1, 2, 3, -Math.PI / 2, 1).getBounds(), 1, Double.NEGATIVE_INFINITY, 3, Double.POSITIVE_INFINITY,
275                 2, Double.POSITIVE_INFINITY);
276 
277         // first quadrant in XY, pointing down (negative Z)
278         verifyBounds(new Ray3d(1, 2, 3, 0.2, 3).getBounds(), 1, 2, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
279                 Double.POSITIVE_INFINITY, 3);
280 
281         // second quadrant in XY, pointing down
282         verifyBounds(new Ray3d(1, 2, 3, 2, 3).getBounds(), Double.NEGATIVE_INFINITY, 2, Double.NEGATIVE_INFINITY, 1,
283                 Double.POSITIVE_INFINITY, 3);
284 
285         // Math.PI is in second quadrant due to finite precision of a double
286         verifyBounds(new Ray3d(1, 2, 3, Math.PI, 3).getBounds(), Double.NEGATIVE_INFINITY, 2, Double.NEGATIVE_INFINITY, 1,
287                 Double.POSITIVE_INFINITY, 3);
288 
289         // third quadrant
290         verifyBounds(new Ray3d(1, 2, 3, 4, 3).getBounds(), Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY,
291                 Double.NEGATIVE_INFINITY, 1, 2, 3);
292 
293         // fourth quadrant
294         verifyBounds(new Ray3d(1, 2, 3, -1, 3).getBounds(), 1, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY,
295                 Double.POSITIVE_INFINITY, 2, 3);
296 
297         // -Math.PI / 2 is in fourth quadrant due to finite precision of a double
298         verifyBounds(new Ray3d(1, 2, 3, -Math.PI / 2, 3).getBounds(), 1, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY,
299                 Double.POSITIVE_INFINITY, 2, 3);
300 
301         // first quadrant in XY, pointing up (positive Z)
302         verifyBounds(new Ray3d(1, 2, 3, 0.2, -1.1).getBounds(), Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, 3, 1, 2,
303                 Double.POSITIVE_INFINITY);
304 
305         // Math.PI / 2 is in first quadrant due to finite precision of a double
306         verifyBounds(new Ray3d(1, 2, 3, Math.PI / 2, -1).getBounds(), Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, 3, 1,
307                 2, Double.POSITIVE_INFINITY);
308 
309         // second quadrant in XY, pointing up
310         verifyBounds(new Ray3d(1, 2, 3, 2, -1).getBounds(), 1, Double.NEGATIVE_INFINITY, 3, Double.POSITIVE_INFINITY, 2,
311                 Double.POSITIVE_INFINITY);
312 
313         // Math.PI is in second quadrant due to finite precision of a double
314         verifyBounds(new Ray3d(1, 2, 3, Math.PI, -1).getBounds(), 1, Double.NEGATIVE_INFINITY, 3, Double.POSITIVE_INFINITY, 2,
315                 Double.POSITIVE_INFINITY);
316 
317         // third quadrant
318         verifyBounds(new Ray3d(1, 2, 3, 4, -1).getBounds(), 1, 2, 3, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY,
319                 Double.POSITIVE_INFINITY);
320 
321         // fourth quadrant
322         verifyBounds(new Ray3d(1, 2, 3, -1, -1).getBounds(), Double.NEGATIVE_INFINITY, 2, 3, 1, Double.POSITIVE_INFINITY,
323                 Double.POSITIVE_INFINITY);
324 
325         // TODO theta values at boundaries and outside of [0..PI/2]
326     }
327 
328     /**
329      * Verify a Bounds object.
330      * @param bounds Bounds3d; the Bounds object to verify
331      * @param expectedMinX double; the expected minimum x value
332      * @param expectedMinY double; the expected minimum y value
333      * @param expectedMinZ double; the expected minimum z value
334      * @param expectedMaxX double; the expected maximum x value
335      * @param expectedMaxY double; the expected maximum y value
336      * @param expectedMaxZ double; the expected maximum z value
337      */
338     private void verifyBounds(final Bounds3d bounds, final double expectedMinX, final double expectedMinY,
339             final double expectedMinZ, final double expectedMaxX, final double expectedMaxY, final double expectedMaxZ)
340     {
341         assertEquals("Bounds minX", expectedMinX, bounds.getMinX(), 0.0001);
342         assertEquals("Bounds minY", expectedMinY, bounds.getMinY(), 0.0001);
343         assertEquals("Bounds minZ", expectedMinZ, bounds.getMinZ(), 0.0001);
344         assertEquals("Bounds maxX", expectedMaxX, bounds.getMaxX(), 0.0001);
345         assertEquals("Bounds maxY", expectedMaxY, bounds.getMaxY(), 0.0001);
346         assertEquals("Bounds maxZ", expectedMaxZ, bounds.getMaxZ(), 0.0001);
347     }
348 
349     /**
350      * Test the getLocation and getLocationExtended methods.
351      */
352     @Test
353     public void testLocation()
354     {
355         try
356         {
357             new Ray3d(1, 2, 3, 1, 0.5).getLocation(Double.NaN);
358             fail("NaN position should have thrown a DrawRuntimeException");
359         }
360         catch (DrawRuntimeException dre)
361         {
362             // Ignore expected exception
363         }
364 
365         try
366         {
367             new Ray3d(1, 2, 3, 1, 0.5).getLocation(-1);
368             fail("Negative position should have thrown a DrawRuntimeException");
369         }
370         catch (DrawRuntimeException dre)
371         {
372             // Ignore expected exception
373         }
374 
375         try
376         {
377             new Ray3d(1, 2, 3, 1, 0.5).getLocation(Double.POSITIVE_INFINITY);
378             fail("Infited position should have thrown a DrawRuntimeException");
379         }
380         catch (DrawRuntimeException dre)
381         {
382             // Ignore expected exception
383         }
384 
385         try
386         {
387             new Ray3d(1, 2, 3, 1, 0.5).getLocation(Double.NEGATIVE_INFINITY);
388             fail("Infinte position should have thrown a DrawRuntimeException");
389         }
390         catch (DrawRuntimeException dre)
391         {
392             // Ignore expected exception
393         }
394 
395         try
396         {
397             new Ray3d(1, 2, 3, 1, 0.5).getLocationExtended(Double.POSITIVE_INFINITY);
398             fail("Infinite position should have thrown a DrawRuntimeException");
399         }
400         catch (DrawRuntimeException dre)
401         {
402             // Ignore expected exception
403         }
404 
405         try
406         {
407             new Ray3d(1, 2, 3, 1, 0.5).getLocationExtended(Double.NEGATIVE_INFINITY);
408             fail("Infinite position should have thrown a DrawRuntimeException");
409         }
410         catch (DrawRuntimeException dre)
411         {
412             // Ignore expected exception
413         }
414 
415         try
416         {
417             new Ray3d(1, 2, 3, 1, 0.5).getLocationExtended(Double.NaN);
418             fail("NaN position should have thrown a DrawRuntimeException");
419         }
420         catch (DrawRuntimeException dre)
421         {
422             // Ignore expected exception
423         }
424 
425         for (double phi : new double[] { 0, 1, 2, 3, 4, 5, -1, -2, Math.PI })
426         {
427             for (double theta : new double[] { 0, 1, 2, 3, 4, 5, -1, -2, Math.PI })
428             {
429                 Ray3d ray = new Ray3d(1, 2, 3, phi, theta);
430                 for (double position : new double[] { 0, 10, 0.1, -2 })
431                 {
432                     Ray3d result = ray.getLocationExtended(position);
433                     assertEquals("result is position distance away from base of ray", Math.abs(position), ray.distance(result),
434                             0.001);
435                     assertEquals("result has same phi as ray", ray.phi, result.phi, 0.00001);
436                     assertTrue("Reverse position on result yields ray",
437                             ray.epsilonEquals(result.getLocationExtended(-position), 0.0001));
438                     if (position > 0)
439                     {
440                         // TODO verify that it is on positive side of ray
441                         assertEquals("result lies in on ray (phi)", ray.phi, result.phi, 0.0001);
442                         assertEquals("result lies on ray (theta)", ray.theta, result.theta, 0.0001);
443                     }
444                     if (position < 0)
445                     {
446                         assertEquals("ray lies on result (phi)", result.phi, ray.phi, 0.0001);
447                         assertEquals("ray lies on result (theta)", result.theta, ray.theta, 0.0001);
448                     }
449                 }
450             }
451         }
452     }
453 
454     /**
455      * Test the closestPointOnRay and the projectOrthogonal methods.
456      */
457     @Test
458     public void testClosestPointAndProjectOrthogonal()
459     {
460         Ray3d ray = new Ray3d(1, 2, 3, 0.4, 0.5);
461         try
462         {
463             ray.closestPointOnRay(null);
464             fail("Null for point should have thrown a NullPointerException");
465         }
466         catch (NullPointerException npe)
467         {
468             // Ignore expected exception
469         }
470 
471         Point3d result = ray.closestPointOnRay(new Point3d(1, 2, 0));
472         assertEquals("result is start point", ray.x, result.x, 0);
473         assertEquals("result is start point", ray.y, result.y, 0);
474         assertEquals("result is start point", ray.z, result.z, 0);
475         result = ray.closestPointOnRay(new Point3d(1, 2, 0));
476         assertEquals("result is start point", ray.x, result.x, 0);
477         assertEquals("result is start point", ray.y, result.y, 0);
478         assertEquals("result is start point", ray.z, result.z, 0);
479         result = ray.closestPointOnRay(new Point3d(0, 2, 3));
480         assertEquals("result is start point", ray.x, result.x, 0);
481         assertEquals("result is start point", ray.y, result.y, 0);
482         assertEquals("result is start point", ray.z, result.z, 0);
483         result = ray.closestPointOnRay(new Point3d(1, 2, 3));
484         assertEquals("result is start point", ray.x, result.x, 0);
485         assertEquals("result is start point", ray.y, result.y, 0);
486         assertEquals("result is start point", ray.z, result.z, 0);
487 
488         assertNull("projection misses the ray", ray.projectOrthogonal(new Point3d(1, 0, 3)));
489         assertNull("projection misses the ray", ray.projectOrthogonal(new Point3d(0, 2, 3)));
490         assertNull("projection misses the ray", ray.projectOrthogonal(new Point3d(1, 2, 2)));
491         assertEquals("projection hits start point of ray", new Point3d(1, 2, 3), ray.projectOrthogonal(new Point3d(1, 2, 3)));
492         assertEquals("extended projection returns same point as projection on sufficiently long line segment", 0,
493                 new LineSegment3d(ray.getLocationExtended(-100), ray.getLocation(100))
494                         .closestPointOnSegment(new Point3d(1, 0, -1))
495                         .distance(ray.projectOrthogonalExtended(new Point3d(1, 0, -1))),
496                 0.0001);
497 
498         Point3d projectingPoint = new Point3d(10, 10, 10);
499         result = ray.closestPointOnRay(projectingPoint); // Projects at a point along the ray
500         double distance = result.distance(ray.getEndPoint());
501         assertTrue("distance from start is > 0", distance > 0);
502         // Check that points on the ray slightly closer to start point or slightly further are indeed further from
503         // projectingPoint
504         assertTrue("Point on ray closer than result is further from projectingPoint",
505                 ray.getLocation(distance - 0.1).distance(projectingPoint) < distance);
506         assertTrue("Point on ray further than result is further from projectingPoint",
507                 ray.getLocation(distance + 0.1).distance(projectingPoint) < distance);
508         assertEquals("projectOrthogonalExtended returns same result as long as orthogonal projection exists", 0,
509                 result.distance(ray.projectOrthogonalExtended(projectingPoint)), 0.0001);
510     }
511 
512     /**
513      * Test the project methods.
514      */
515     @Test
516     public void testProject()
517     {
518         Ray3d ray = new Ray3d(1, 2, 3, 20, 10, 5);
519         assertTrue("projects outside", Double.isNaN(ray.projectOrthogonalFractional(new Point3d(1, 1, 1))));
520         assertTrue("projects before start", ray.projectOrthogonalFractionalExtended(new Point3d(1, 1, 1)) < 0);
521         assertEquals("projects at", -new Point3d(1 - 19 - 19, 2 - 8 - 8, 3 - 2 - 2).distance(ray),
522                 ray.projectOrthogonalFractionalExtended(new Point3d(1 - 19 - 19 + 8, 2 - 8 - 8 - 19, 3 - 2 - 2)), 0.0001);
523         // Projection of projection is projection
524         for (int x = -2; x < 5; x++)
525         {
526             for (int y = -2; y < 5; y++)
527             {
528                 for (int z = -2; z < 5; z++)
529                 {
530                     Point3d point = new Point3d(x, y, z);
531                     double fraction = ray.projectOrthogonalFractionalExtended(point);
532                     if (fraction < 0)
533                     {
534                         assertTrue("non extended version yields NaN", Double.isNaN(ray.projectOrthogonalFractional(point)));
535                         assertNull("non extended projectOrthogonal yields null", ray.projectOrthogonal(point));
536                     }
537                     else
538                     {
539                         assertEquals("non extended version yields same", fraction, ray.projectOrthogonalFractional(point),
540                                 0.00001);
541                         assertEquals("non extended version yields same as extended version", ray.projectOrthogonal(point),
542                                 ray.projectOrthogonalExtended(point));
543                     }
544                     Point3d projected = ray.projectOrthogonalExtended(point);
545                     assertEquals("projecting projected point yields same", fraction,
546                             ray.projectOrthogonalFractionalExtended(projected), 0.00001);
547                 }
548             }
549         }
550     }
551 
552     /**
553      * Test the epsilonEquals method.
554      */
555     @Test
556     public void epsilonEqualsTest()
557     {
558         Ray3d ray = new Ray3d(1, 2, 3, 0.5, -0.5);
559         try
560         {
561             ray.epsilonEquals((Ray3d) null, 1, 1);
562             fail("Null pointer should have thrown a NullPointerException");
563         }
564         catch (NullPointerException npe)
565         {
566             // Ignore expected exception
567         }
568 
569         try
570         {
571             ray.epsilonEquals(ray, -0.1, 1);
572             fail("Negative epsilonCoordinate should have thrown an IllegalArgumentException");
573         }
574         catch (IllegalArgumentException npe)
575         {
576             // Ignore expected exception
577         }
578 
579         try
580         {
581             ray.epsilonEquals(ray, 1, -0.1);
582             fail("Negative epsilonDirection should have thrown an IllegalArgumentException");
583         }
584         catch (IllegalArgumentException npe)
585         {
586             // Ignore expected exception
587         }
588 
589         try
590         {
591             ray.epsilonEquals(ray, Double.NaN, 1);
592             fail("NaN epsilonCoordinate should have thrown an IllegalArgumentException");
593         }
594         catch (IllegalArgumentException npe)
595         {
596             // Ignore expected exception
597         }
598 
599         try
600         {
601             ray.epsilonEquals(ray, 1, Double.NaN);
602             fail("NaN epsilonDirection should have thrown an IllegalArgumentException");
603         }
604         catch (IllegalArgumentException npe)
605         {
606             // Ignore expected exception
607         }
608 
609         double[] deltas = new double[] { 0.0, -0.125, 0.125, -1, 1 }; // Use values that can be represented exactly in a double
610         for (double dX : deltas)
611         {
612             for (double dY : deltas)
613             {
614                 for (double dZ : deltas)
615                 {
616                     for (double dPhi : deltas)
617                     {
618                         for (double dTheta : deltas)
619                         {
620                             for (double epsilon : new double[] { 0, 0.125, 0.5, 0.9, 1.0, 1.1 })
621                             {
622                                 Ray3d other = new Ray3d(ray.x + dX, ray.y + dY, ray.z + dZ, ray.phi + dPhi, ray.theta + dTheta);
623                                 // System.out.println(String.format("dX=%f, dY=%f, dZ=%f, dPhi=%f, dTheta=%f, epsilon=%f", dX,
624                                 // dY,
625                                 // dZ, dPhi, dTheta, epsilon));
626                                 boolean result = ray.epsilonEquals(other, epsilon, Double.POSITIVE_INFINITY);
627                                 boolean expected =
628                                         Math.abs(dX) <= epsilon && Math.abs(dY) <= epsilon && Math.abs(dZ) <= epsilon;
629                                 assertEquals("result of epsilonEquals checking x, y, z", expected, result);
630 
631                                 result = ray.epsilonEquals(other, Double.POSITIVE_INFINITY, epsilon);
632                                 expected = Math.abs(dPhi) <= epsilon && Math.abs(dTheta) <= epsilon;
633                                 assertEquals("result of epsilonEquals checking phi and theta", expected, result);
634                                 // Create an equivalent alternative ray
635                                 other = new Ray3d(ray.x + dX, ray.y + dY, ray.z + dZ, Math.PI + ray.phi + dPhi,
636                                         Math.PI - ray.theta + dTheta);
637                                 result = ray.epsilonEquals(other, epsilon, Double.POSITIVE_INFINITY);
638                                 expected = Math.abs(dX) <= epsilon && Math.abs(dY) <= epsilon && Math.abs(dZ) <= epsilon;
639                                 assertEquals("result of epsilonEquals checking x, y, z", expected, result);
640 
641                                 result = ray.epsilonEquals(other, Double.POSITIVE_INFINITY, epsilon);
642                                 expected = Math.abs(dPhi) <= epsilon && Math.abs(dTheta) <= epsilon;
643                                 assertEquals("result of epsilonEquals checking phi and theta", expected, result);
644                             }
645                         }
646                     }
647                 }
648             }
649         }
650     }
651 
652     /**
653      * Test the equals and hasCode methods.
654      */
655     @Test
656     public void equalsAndHashCodeTest()
657     {
658         Ray3d ray = new Ray3d(1, 2, 3, 11, 12, 13);
659         assertEquals("equal to itself", ray, ray);
660         assertNotEquals("not equal to null", ray, null);
661         assertNotEquals("not equal to different object with same parent class", ray, new OrientedPoint3d(1, 2, 3));
662         assertNotEquals("not equal to ray with different phi", ray, new Ray3d(1, 2, 3, 11, 10, 13));
663         assertNotEquals("not equal to ray with different theta", ray, new Ray3d(1, 2, 3, 11, 12, 10));
664         assertNotEquals("not equal to ray with different start x", ray, new Ray3d(2, 2, 3, 12, 12, 13));
665         assertNotEquals("not equal to ray with different start y", ray, new Ray3d(1, 3, 3, 11, 13, 13));
666         assertEquals("equal to ray with same x, y and direction", ray, new Ray3d(1, 2, 3, 21, 22, 23));
667 
668         assertNotEquals("hashCode depends on x", ray.hashCode(), new Ray3d(2, 2, 3, 12, 12, 13));
669         assertNotEquals("hashCode depends on y", ray.hashCode(), new Ray3d(1, 3, 3, 11, 13, 13));
670         assertNotEquals("hashCode depends on y", ray.hashCode(), new Ray3d(1, 2, 4, 11, 12, 14));
671         assertNotEquals("hashCode depends on phi", ray.hashCode(), new Ray3d(1, 2, 3, 11, 10, 13));
672         assertNotEquals("hashCode depends on theta", ray.hashCode(), new Ray3d(1, 2, 3, 11, 12, 10));
673     }
674 
675 }