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