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