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.assertNotNull;
7   import static org.junit.Assert.assertNull;
8   import static org.junit.Assert.assertTrue;
9   import static org.junit.Assert.fail;
10  
11  import java.awt.geom.Path2D;
12  import java.awt.geom.PathIterator;
13  import java.lang.reflect.InvocationTargetException;
14  import java.util.ArrayList;
15  import java.util.Arrays;
16  import java.util.Iterator;
17  import java.util.List;
18  import java.util.NoSuchElementException;
19  
20  import org.djutils.draw.DrawRuntimeException;
21  import org.djutils.draw.bounds.Bounds3d;
22  import org.djutils.draw.line.PolyLine.TransitionFunction;
23  import org.djutils.draw.point.Point2d;
24  import org.djutils.draw.point.Point3d;
25  import org.junit.Test;
26  
27  /**
28   * TestLine3d.java.
29   * <p>
30   * Copyright (c) 2020-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
31   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
32   * </p>
33   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
34   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
35   */
36  public class PolyLine3dTest
37  {
38      /**
39       * Test the constructors of PolyLine3d.
40       * @throws DrawRuntimeException on failure
41       */
42      @Test
43      public final void constructorsTest() throws DrawRuntimeException
44      {
45          double[] values = { -999, 0, 99, 9999 }; // Keep this list short; execution time grows with 9th power of length
46          Point3d[] points = new Point3d[0]; // Empty array
47          try
48          {
49              runConstructors(points);
50              fail("Should have thrown a DrawRuntimeException");
51          }
52          catch (DrawRuntimeException exception)
53          {
54              // Ignore expected exception
55          }
56          for (double x0 : values)
57          {
58              for (double y0 : values)
59              {
60                  for (double z0 : values)
61                  {
62                      points = new Point3d[1]; // Degenerate array holding one point
63                      points[0] = new Point3d(x0, y0, z0);
64                      try
65                      {
66                          runConstructors(points);
67                          fail("Should have thrown a DrawRuntimeException");
68                      }
69                      catch (DrawRuntimeException exception)
70                      {
71                          // Ignore expected exception
72                      }
73                      for (double x1 : values)
74                      {
75                          for (double y1 : values)
76                          {
77                              for (double z1 : values)
78                              {
79                                  points = new Point3d[2]; // Straight line; two points
80                                  points[0] = new Point3d(x0, y0, z0);
81                                  points[1] = new Point3d(x1, y1, z1);
82                                  if (0 == points[0].distance(points[1]))
83                                  {
84                                      try
85                                      {
86                                          runConstructors(points);
87                                          fail("Should have thrown a DrawRuntimeException");
88                                      }
89                                      catch (DrawRuntimeException exception)
90                                      {
91                                          // Ignore expected exception
92                                      }
93                                  }
94                                  else
95                                  {
96                                      runConstructors(points);
97                                      for (double x2 : values)
98                                      {
99                                          for (double y2 : values)
100                                         {
101                                             for (double z2 : values)
102                                             {
103                                                 points = new Point3d[3]; // Line with intermediate point
104                                                 points[0] = new Point3d(x0, y0, z0);
105                                                 points[1] = new Point3d(x1, y1, z1);
106                                                 points[2] = new Point3d(x2, y2, z2);
107                                                 if (0 == points[1].distance(points[2]))
108                                                 {
109                                                     try
110                                                     {
111                                                         runConstructors(points);
112                                                         fail("Should have thrown a DrawRuntimeException");
113                                                     }
114                                                     catch (DrawRuntimeException exception)
115                                                     {
116                                                         // Ignore expected exception
117                                                     }
118                                                 }
119                                                 else
120                                                 {
121                                                     runConstructors(points);
122                                                 }
123                                             }
124                                         }
125                                     }
126                                 }
127                             }
128                         }
129                     }
130                 }
131             }
132         }
133     }
134 
135     /**
136      * Test all the constructors of PolyLine3d.
137      * @param points Point3d[]; array of Point3d to test with
138      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
139      */
140     private void runConstructors(final Point3d[] points) throws DrawRuntimeException
141     {
142         verifyPoints(new PolyLine3d(points), points);
143         List<Point3d> list = new ArrayList<>();
144         for (int i = 0; i < points.length; i++)
145         {
146             list.add(points[i]);
147         }
148         PolyLine3d line = new PolyLine3d(list);
149         verifyPoints(line, points);
150         // Convert it to Point3d[], create another Line3d from that and check that
151         verifyPoints(new PolyLine3d(line.getPoints()), points);
152         assertEquals("length at index 0", 0.0, line.lengthAtIndex(0), 0);
153         double length = 0;
154         for (int i = 1; i < points.length; i++)
155         {
156             length += Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2) + Math.pow(points[i].y - points[i - 1].y, 2)
157                     + Math.pow(points[i].z - points[i - 1].z, 2));
158             assertEquals("length at index", length, line.lengthAtIndex(i), 0.0001);
159         }
160         assertEquals("length", length, line.getLength(), 10 * Math.ulp(length));
161 
162         assertEquals("size", points.length, line.size());
163 
164         Bounds3d b3d = line.getBounds();
165         Bounds3d ref = new Bounds3d(points);
166         assertEquals("bounds is correct", ref, b3d);
167 
168         try
169         {
170             line.get(-1);
171             fail("Negative index should have thrown an IndexOutOfBoundsException");
172         }
173         catch (IndexOutOfBoundsException ioobe)
174         {
175             // Ignore expected exception
176         }
177 
178         try
179         {
180             line.get(line.size() + 1);
181             fail("Too large index should have thrown an IndexOutOfBoundsException");
182         }
183         catch (IndexOutOfBoundsException ioobe)
184         {
185             // Ignore expected exception
186         }
187 
188         try
189         {
190             new PolyLine3d((List<Point3d>) null);
191             fail("null list should have thrown a NullPointerException");
192         }
193         catch (NullPointerException npe)
194         {
195             // Ignore expected exception
196         }
197 
198         // Construct a Path3D.Double that contains the horizontal moveto or lineto
199         Path2D path = new Path2D.Double();
200         path.moveTo(points[0].x, points[0].y);
201         // System.out.print("path is "); printPath2D(path);
202         for (int i = 1; i < points.length; i++)
203         {
204             // Path3D is corrupt if same point is added twice in succession
205             if (points[i].x != points[i - 1].x || points[i].y != points[i - 1].y)
206             {
207                 path.lineTo(points[i].x, points[i].y);
208             }
209         }
210     }
211 
212     /**
213      * Print a Path2D to the console.
214      * @param path Path2D; the path
215      */
216     public final void printPath2D(final Path2D path)
217     {
218         PathIterator pi = path.getPathIterator(null);
219         double[] p = new double[6];
220         while (!pi.isDone())
221         {
222             int segType = pi.currentSegment(p);
223             if (segType == PathIterator.SEG_MOVETO)
224             {
225                 System.out.print(" move to " + new Point3d(p[0], p[1], 0.0));
226             }
227             if (segType == PathIterator.SEG_LINETO)
228             {
229                 System.out.print(" line to " + new Point3d(p[0], p[1], 0.0));
230             }
231             else if (segType == PathIterator.SEG_CLOSE)
232             {
233                 System.out.print(" close");
234             }
235             pi.next();
236         }
237         System.out.println("");
238     }
239 
240     /**
241      * Verify that a Line3d contains the same points as an array of Point3d.
242      * @param line Line3d; the OTS line
243      * @param points Point3d[]; the OTSPoint array
244      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
245      */
246     private void verifyPoints(final PolyLine3d line, final Point3d[] points) throws DrawRuntimeException
247     {
248         assertEquals("Line should have same number of points as point array", line.size(), points.length);
249         for (int i = 0; i < points.length; i++)
250         {
251             assertEquals("x of point i should match", points[i].x, line.get(i).x, Math.ulp(points[i].x));
252             assertEquals("y of point i should match", points[i].y, line.get(i).y, Math.ulp(points[i].y));
253             assertEquals("z of point i should match", points[i].z, line.get(i).z, Math.ulp(points[i].z));
254             assertEquals("x of point i should match", points[i].x, line.getX(i), Math.ulp(points[i].x));
255             assertEquals("y of point i should match", points[i].y, line.getY(i), Math.ulp(points[i].y));
256             assertEquals("z of point i should match", points[i].z, line.getZ(i), Math.ulp(points[i].z));
257             if (i < points.length - 1)
258             {
259                 LineSegment3d segment = line.getSegment(i);
260                 assertEquals("begin x of line segment i should match", points[i].x, segment.startX, Math.ulp(points[i].x));
261                 assertEquals("begin y of line segment i should match", points[i].y, segment.startY, Math.ulp(points[i].y));
262                 assertEquals("begin z of line segment i should match", points[i].z, segment.startZ, Math.ulp(points[i].z));
263                 assertEquals("end x of line segment i should match", points[i + 1].x, segment.endX, Math.ulp(points[i + 1].x));
264                 assertEquals("end y of line segment i should match", points[i + 1].y, segment.endY, Math.ulp(points[i + 1].y));
265                 assertEquals("end z of line segment i should match", points[i + 1].z, segment.endZ, Math.ulp(points[i + 1].z));
266             }
267             else
268             {
269                 try
270                 {
271                     line.getSegment(i);
272                     fail("Too large index should have thrown a DrawRuntimeException");
273                 }
274                 catch (DrawRuntimeException dre)
275                 {
276                     // Ignore expected exception
277                 }
278 
279                 try
280                 {
281                     line.getSegment(-1);
282                     fail("Negative index should have thrown a DrawRuntimeException");
283                 }
284                 catch (DrawRuntimeException dre)
285                 {
286                     // Ignore expected exception
287                 }
288 
289             }
290         }
291     }
292 
293     /**
294      * Test all constructors of a Line2d.
295      * @throws DrawRuntimeException if that happens uncaught; this test has failed
296      * @throws DrawRuntimeException if that happens uncaught; this test has failed
297      */
298     @Test
299     public void testConstructors() throws DrawRuntimeException, DrawRuntimeException
300     {
301         runConstructors(new Point3d[] { new Point3d(1.2, 3.4, 5.5), new Point3d(2.3, 4.5, 6.6), new Point3d(3.4, 5.6, 7.7) });
302 
303         try
304         {
305             new PolyLine3d(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 }, new double[] { 7, 8 });
306             fail("double arrays of unequal length should have thrown a DrawRuntimeException");
307         }
308         catch (DrawRuntimeException dre)
309         {
310             // Ignore expected exception
311         }
312 
313         try
314         {
315             new PolyLine3d(new double[] { 1, 2, 3 }, new double[] { 4, 5 }, new double[] { 7, 8, 9 });
316             fail("double arrays of unequal length should have thrown a DrawRuntimeException");
317         }
318         catch (DrawRuntimeException dre)
319         {
320             // Ignore expected exception
321         }
322 
323         try
324         {
325             new PolyLine3d(new double[] { 1, 2 }, new double[] { 4, 5, 6 }, new double[] { 7, 8, 9 });
326             fail("double arrays of unequal length should have thrown a DrawRuntimeException");
327         }
328         catch (DrawRuntimeException dre)
329         {
330             // Ignore expected exception
331         }
332 
333         try
334         {
335             new PolyLine3d(null, new double[] { 1, 2 }, new double[] { 3, 4 });
336             fail("null double array should have thrown a NullPointerException");
337         }
338         catch (NullPointerException npe)
339         {
340             // Ignore expected exception
341         }
342 
343         try
344         {
345             new PolyLine3d(new double[] { 1, 2 }, null, new double[] { 5, 6 });
346             fail("null double array should have thrown a NullPointerException");
347         }
348         catch (NullPointerException npe)
349         {
350             // Ignore expected exception
351         }
352 
353         try
354         {
355             new PolyLine3d(new double[] { 1, 2 }, new double[] { 3, 4 }, null);
356             fail("null double array should have thrown a NullPointerException");
357         }
358         catch (NullPointerException npe)
359         {
360             // Ignore expected exception
361         }
362 
363         try
364         {
365             new PolyLine3d((List<Point3d>) null);
366             fail("null list should have thrown a nullPointerException");
367         }
368         catch (NullPointerException npe)
369         {
370             // Ignore expected exception
371         }
372 
373         List<Point3d> shortList = new ArrayList<>();
374         try
375         {
376             new PolyLine3d(shortList);
377             fail("empty list should have thrown a DrawRuntimeException");
378         }
379         catch (DrawRuntimeException dre)
380         {
381             // Ignore expected exception
382         }
383 
384         shortList.add(new Point3d(1, 2, 3));
385         try
386         {
387             new PolyLine3d(shortList);
388             fail("one-point list should have thrown a DrawRuntimeException");
389         }
390         catch (DrawRuntimeException dre)
391         {
392             // Ignore expected exception
393         }
394 
395         Point3d p1 = new Point3d(1, 2, 3);
396         Point3d p2 = new Point3d(3, 4, 5);
397         PolyLine3d pl = new PolyLine3d(p1, p2);
398         assertEquals("two points", 2, pl.size());
399         assertEquals("p1", p1, pl.get(0));
400         assertEquals("p2", p2, pl.get(1));
401 
402         pl = new PolyLine3d(p1, p2, (Point3d[]) null);
403         assertEquals("two points", 2, pl.size());
404         assertEquals("p1", p1, pl.get(0));
405         assertEquals("p2", p2, pl.get(1));
406 
407         pl = new PolyLine3d(p1, p2, new Point3d[0]);
408         assertEquals("two points", 2, pl.size());
409         assertEquals("p1", p1, pl.get(0));
410         assertEquals("p2", p2, pl.get(1));
411 
412         try
413         {
414             new PolyLine3d(new Point3d[] {});
415             fail("empty array should have thrown a DrawRuntimeException");
416         }
417         catch (DrawRuntimeException dre)
418         {
419             // Ignore expected exception
420         }
421 
422         try
423         {
424             new PolyLine3d(new Point3d[] { new Point3d(1, 2, 3) });
425             fail("single point should have thrown a DrawRuntimeException");
426         }
427         catch (DrawRuntimeException dre)
428         {
429             // Ignore expected exception
430         }
431 
432         try
433         {
434             new PolyLine3d(new Point3d[] { new Point3d(1, 2, 3), new Point3d(1, 2, 3) });
435             fail("duplicate point should have thrown a DrawRuntimeException");
436         }
437         catch (DrawRuntimeException dre)
438         {
439             // Ignore expected exception
440         }
441 
442         try
443         {
444             new PolyLine3d(new Point3d[] { new Point3d(1, 2, 3), new Point3d(1, 2, 3), new Point3d(3, 4, 5) });
445             fail("duplicate point should have thrown a DrawRuntimeException");
446         }
447         catch (DrawRuntimeException dre)
448         {
449             // Ignore expected exception
450         }
451 
452         try
453         {
454             new PolyLine3d(new Point3d[] { new Point3d(-1, -2, -3), new Point3d(1, 2, 3), new Point3d(1, 2, 3),
455                     new Point3d(3, 4, 5) });
456             fail("duplicate point should have thrown a DrawRuntimeException");
457         }
458         catch (DrawRuntimeException dre)
459         {
460             // Ignore expected exception
461         }
462     }
463 
464     /**
465      * Test that exception is thrown when it should be.
466      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
467      */
468     @Test
469     public final void exceptionTest() throws DrawRuntimeException
470     {
471         PolyLine3d line = new PolyLine3d(new Point3d[] { new Point3d(1, 2, 3), new Point3d(4, 5, 6) });
472         try
473         {
474             line.get(-1);
475             fail("Should have thrown an IndexOutOfBoundsException");
476         }
477         catch (IndexOutOfBoundsException ioobe)
478         {
479             // Ignore expected exception
480         }
481 
482         try
483         {
484             line.get(2);
485             fail("Should have thrown an IndexOutOfBoundsException");
486         }
487         catch (IndexOutOfBoundsException ioobe)
488         {
489             // Ignore expected exception
490         }
491     }
492 
493     /**
494      * Test the getLocationExtended method and friends.
495      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
496      */
497     @Test
498     public final void locationExtendedTest() throws DrawRuntimeException
499     {
500         Point3d p0 = new Point3d(10, 20, 30);
501         Point3d p1 = new Point3d(40, 50, 60);
502         Point3d p2 = new Point3d(90, 80, 70);
503         PolyLine3d polyLine = new PolyLine3d(new Point3d[] { p0, p1, p2 });
504         double expectedPhi1 = Math.atan2(p1.y - p0.y, p1.x - p0.x);
505         double expectedTheta1 = Math.atan2(Math.hypot(p1.x - p0.x, p1.y - p0.y), p1.z - p0.z);
506         checkGetLocation(polyLine, -10, null, expectedPhi1, expectedTheta1);
507         checkGetLocation(polyLine, -0.0001, p0, expectedPhi1, expectedTheta1);
508         checkGetLocation(polyLine, 0, p0, expectedPhi1, expectedTheta1);
509         checkGetLocation(polyLine, 0.0001, p0, expectedPhi1, expectedTheta1);
510         double expectedPhi2 = Math.atan2(p2.y - p1.y, p2.x - p1.x);
511         double expectedTheta2 = Math.atan2(Math.hypot(p2.x - p1.x, p2.y - p1.y), p2.z - p1.z);
512         checkGetLocation(polyLine, 0.9999, p2, expectedPhi2, expectedTheta2);
513         checkGetLocation(polyLine, 1.0, p2, expectedPhi2, expectedTheta2);
514         checkGetLocation(polyLine, 1.0001, p2, expectedPhi2, expectedTheta2);
515         checkGetLocation(polyLine, 10, null, expectedPhi2, expectedTheta2);
516     }
517 
518     /**
519      * Check the location returned by the various location methods.
520      * @param line Line3d; the line
521      * @param fraction double; relative position to check
522      * @param expectedPoint Point3d; expected location of the result
523      * @param expectedPhi double; expected angle of the result from the X axis
524      * @param expectedTheta double; expected angle of the result from the Z axis
525      * @throws DrawRuntimeException on failure
526      */
527     private void checkGetLocation(final PolyLine3d line, final double fraction, final Point3d expectedPoint,
528             final double expectedPhi, final double expectedTheta) throws DrawRuntimeException
529     {
530         double length = line.getLength();
531         checkRay3d(line.getLocationExtended(fraction * length), expectedPoint, expectedPhi, expectedTheta);
532         if (fraction < 0 || fraction > 1)
533         {
534             try
535             {
536                 line.getLocation(fraction * length);
537                 fail("getLocation should have thrown a DrawRuntimeException");
538             }
539             catch (DrawRuntimeException dre)
540             {
541                 // Ignore expected exception
542             }
543             try
544             {
545                 line.getLocationFraction(fraction);
546                 fail("getLocation should have thrown a DrawRuntimeException");
547             }
548             catch (DrawRuntimeException ne)
549             {
550                 // Ignore expected exception
551             }
552         }
553         else
554         {
555             checkRay3d(line.getLocation(fraction * length), expectedPoint, expectedPhi, expectedTheta);
556             checkRay3d(line.getLocationFraction(fraction), expectedPoint, expectedPhi, expectedTheta);
557         }
558 
559     }
560 
561     /**
562      * Verify the location and direction of a DirectedPoint3d.
563      * @param dp DirectedPoint3d; the DirectedPoint3d that should be verified
564      * @param expectedPoint Point3d; the expected location (or null if location should not be checked)
565      * @param expectedPhi double; the expected angle from the X axis
566      * @param expectedTheta double; the expected angle from the Z axis
567      */
568     private void checkRay3d(final Ray3d dp, final Point3d expectedPoint, final double expectedPhi, final double expectedTheta)
569     {
570         if (null != expectedPoint)
571         {
572             Point3d p = new Point3d(dp.x, dp.y, dp.z);
573             assertEquals("locationExtended(0) returns approximately expected point", 0, expectedPoint.distance(p), 0.1);
574         }
575         assertEquals("Phi (rotation of projection from X axis)", expectedPhi, dp.getPhi(), 0.001);
576         assertEquals("Theta (rotation from Z axis)", expectedTheta, dp.getTheta(), 0.001);
577     }
578 
579     /**
580      * Test the filtering constructors.
581      * @throws DrawRuntimeException should never happen
582      */
583     @Test
584     public final void filterTest() throws DrawRuntimeException
585     {
586         Point3d[] tooShort = new Point3d[] {};
587         try
588         {
589             new PolyLine3d(true, tooShort);
590             fail("Array with no points should have thrown an exception");
591         }
592         catch (DrawRuntimeException dre)
593         {
594             // Ignore expected exception
595         }
596 
597         tooShort = new Point3d[] { new Point3d(1, 2, 3) };
598         try
599         {
600             new PolyLine3d(true, tooShort);
601             fail("Array with one point should have thrown an exception");
602         }
603         catch (DrawRuntimeException dre)
604         {
605             // Ignore expected exception
606         }
607 
608         Point3d p0 = new Point3d(1, 2, 3);
609         Point3d p1 = new Point3d(4, 5, 6);
610         Point3d[] points = new Point3d[] { p0, p1 };
611         PolyLine3d result = new PolyLine3d(true, points);
612         assertTrue("first point is p0", p0.equals(result.get(0)));
613         assertTrue("second point is p1", p1.equals(result.get(1)));
614         Point3d p1Same = new Point3d(4, 5, 6);
615         result = new PolyLine3d(true, new Point3d[] { p0, p0, p0, p0, p1Same, p0, p1, p1, p1Same, p1, p1 });
616         assertEquals("result should contain 4 points", 4, result.size());
617         assertTrue("first point is p0", p0.equals(result.get(0)));
618         assertTrue("second point is p1", p1.equals(result.get(1)));
619         assertTrue("third point is p0", p0.equals(result.get(0)));
620         assertTrue("last point is p1", p1.equals(result.get(1)));
621         new PolyLine3d(true, new Point3d[] { p0, new Point3d(1, 3, 4) });
622         new PolyLine3d(true, new Point3d[] { p0, new Point3d(1, 2, 4) });
623 
624         try
625         {
626             PolyLine3d.cleanPoints(true, null);
627             fail("null iterator should have thrown a NullPointerException");
628         }
629         catch (NullPointerException npe)
630         {
631             // Ignore expected exception
632         }
633 
634         try
635         {
636             PolyLine3d.cleanPoints(true, new Iterator<Point3d>()
637             {
638                 @Override
639                 public boolean hasNext()
640                 {
641                     return false;
642                 }
643 
644                 @Override
645                 public Point3d next()
646                 {
647                     return null;
648                 }
649             });
650             fail("Iterator that has no data should have thrown a DrawRuntimeException");
651         }
652         catch (DrawRuntimeException dre)
653         {
654             // Ignore expected exception
655         }
656 
657         Iterator<Point3d> iterator =
658                 PolyLine3d.cleanPoints(true, Arrays.stream(new Point3d[] { new Point3d(1, 2, 3) }).iterator());
659         iterator.next(); // should work
660         assertFalse("iterator should now be out of data", iterator.hasNext());
661         try
662         {
663             iterator.next();
664             fail("Iterator that has no nore data should have thrown a NoSuchElementException");
665         }
666         catch (NoSuchElementException nse)
667         {
668             // Ignore expected exception
669         }
670 
671         // Check that cleanPoints with false indeed does not filter
672         iterator = PolyLine3d.cleanPoints(false,
673                 Arrays.stream(new Point3d[] { new Point3d(1, 2, 3), new Point3d(1, 2, 3), new Point3d(1, 2, 3) }).iterator());
674         assertTrue("iterator has initial point", iterator.hasNext());
675         iterator.next();
676         assertTrue("iterator has second point", iterator.hasNext());
677         iterator.next();
678         assertTrue("iterator has second point", iterator.hasNext());
679         iterator.next();
680         assertFalse("iterator has no more data", iterator.hasNext());
681     }
682 
683     /**
684      * Test the equals method.
685      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
686      */
687     @Test
688     public final void equalsTest() throws DrawRuntimeException
689     {
690         Point3d p0 = new Point3d(1.1, 2.2, 3.3);
691         Point3d p1 = new Point3d(2.1, 2.2, 3.3);
692         Point3d p2 = new Point3d(3.1, 2.2, 3.3);
693 
694         PolyLine3d line = new PolyLine3d(new Point3d[] { p0, p1, p2 });
695         assertTrue("Line3d is equal to itself", line.equals(line));
696         assertFalse("Line3d is not equal to null", line.equals(null));
697         assertFalse("Line3d is not equals to some other kind of Object", line.equals(new Object()));
698         PolyLine3d line2 = new PolyLine3d(new Point3d[] { p0, p1, p2 });
699         assertTrue("Line3d is equal ot other Line3d that has the exact same list of Point3d", line.equals(line2));
700         Point3d p2Same = new Point3d(3.1, 2.2, 3.3);
701         line2 = new PolyLine3d(new Point3d[] { p0, p1, p2Same });
702         assertTrue("Line3d is equal ot other Line3d that has the exact same list of Point3d; even if some of "
703                 + "those point are different instances with the same coordinates", line.equals(line2));
704         Point3d p2NotSame = new Point3d(3.1, 2.2, 3.35);
705         line2 = new PolyLine3d(new Point3d[] { p0, p1, p2NotSame });
706         assertFalse("Line3d is not equal ot other Line3d that differs in one coordinate", line.equals(line2));
707         line2 = new PolyLine3d(new Point3d[] { p0, p1, p2, p2NotSame });
708         assertFalse("Line3d is not equal ot other Line3d that has more points (but is identical up to the common length)",
709                 line.equals(line2));
710         assertFalse("Line3d is not equal ot other Line3d that has fewer points  (but is identical up to the common length)",
711                 line2.equals(line));
712     }
713 
714     /**
715      * Test the concatenate method.
716      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
717      */
718     @Test
719     public final void concatenateTest() throws DrawRuntimeException
720     {
721         Point3d p0 = new Point3d(1.1, 2.2, 3.3);
722         Point3d p1 = new Point3d(2.1, 2.2, 3.3);
723         Point3d p2 = new Point3d(3.1, 2.2, 3.3);
724         Point3d p3 = new Point3d(4.1, 2.2, 3.3);
725         Point3d p4 = new Point3d(5.1, 2.2, 3.3);
726         Point3d p5 = new Point3d(6.1, 2.2, 3.3);
727 
728         PolyLine3d l0 = new PolyLine3d(p0, p1, p2);
729         PolyLine3d l1 = new PolyLine3d(p2, p3);
730         PolyLine3d l2 = new PolyLine3d(p3, p4, p5);
731         PolyLine3d ll = PolyLine3d.concatenate(l0, l1, l2);
732         assertEquals("size is 6", 6, ll.size());
733         assertEquals("point 0 is p0", p0, ll.get(0));
734         assertEquals("point 1 is p1", p1, ll.get(1));
735         assertEquals("point 2 is p2", p2, ll.get(2));
736         assertEquals("point 3 is p3", p3, ll.get(3));
737         assertEquals("point 4 is p4", p4, ll.get(4));
738         assertEquals("point 5 is p5", p5, ll.get(5));
739 
740         ll = PolyLine3d.concatenate(l1);
741         assertEquals("size is 2", 2, ll.size());
742         assertEquals("point 0 is p2", p2, ll.get(0));
743         assertEquals("point 1 is p3", p3, ll.get(1));
744 
745         try
746         {
747             PolyLine3d.concatenate(l0, l2);
748             fail("Gap should have throw an exception");
749         }
750         catch (DrawRuntimeException dre)
751         {
752             // Ignore expected exception
753         }
754         try
755         {
756             PolyLine3d.concatenate();
757             fail("concatenate of empty list should have thrown an exception");
758         }
759         catch (DrawRuntimeException dre)
760         {
761             // Ignore expected exception
762         }
763 
764         // Test concatenate methods with tolerance
765         PolyLine3d thirdLine = new PolyLine3d(p4, p5);
766         for (double tolerance : new double[] { 0.1, 0.01, 0.001, 0.0001, 0.00001 })
767         {
768             for (double actualError : new double[] { tolerance * 0.9, tolerance * 1.1 })
769             {
770                 int maxDirection = 10;
771                 for (int direction = 0; direction < maxDirection; direction++)
772                 {
773                     double dx = actualError * Math.cos(Math.PI * 2 * direction / maxDirection);
774                     double dy = actualError * Math.sin(Math.PI * 2 * direction / maxDirection);
775                     PolyLine3d otherLine = new PolyLine3d(new Point3d(p2.x + dx, p2.y + dy, p2.z), p3, p4);
776                     if (actualError < tolerance)
777                     {
778                         try
779                         {
780                             PolyLine3d.concatenate(tolerance, l0, otherLine);
781                         }
782                         catch (DrawRuntimeException dre)
783                         {
784                             PolyLine3d.concatenate(tolerance, l0, otherLine);
785                             fail("concatenation with error " + actualError + " and tolerance " + tolerance
786                                     + " should not have failed");
787                         }
788                         try
789                         {
790                             PolyLine3d.concatenate(tolerance, l0, otherLine, thirdLine);
791                         }
792                         catch (DrawRuntimeException dre)
793                         {
794                             fail("concatenation with error " + actualError + " and tolerance " + tolerance
795                                     + " should not have failed");
796                         }
797                     }
798                     else
799                     {
800                         try
801                         {
802                             PolyLine3d.concatenate(tolerance, l0, otherLine);
803                         }
804                         catch (DrawRuntimeException dre)
805                         {
806                             // Ignore expected exception
807                         }
808                         try
809                         {
810                             PolyLine3d.concatenate(tolerance, l0, otherLine, thirdLine);
811                         }
812                         catch (DrawRuntimeException dre)
813                         {
814                             // Ignore expected exception
815                         }
816                     }
817                 }
818             }
819         }
820     }
821 
822     /**
823      * Test the reverse and project methods.
824      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
825      */
826     @Test
827     public final void reverseAndProjectTest() throws DrawRuntimeException
828     {
829         Point3d p0 = new Point3d(1.1, 2.21, 3.1);
830         Point3d p1 = new Point3d(2.1, 2.22, 3.2);
831         Point3d p2 = new Point3d(2.1, 2.23, 3.3);
832         Point3d p2x = new Point3d(p2.x, p2.y, p2.z + 1);
833         Point3d p3 = new Point3d(4.1, 2.24, 3.4);
834         Point3d p4 = new Point3d(5.1, 2.25, 3.5);
835         Point3d p5 = new Point3d(6.1, 2.26, 3.6);
836 
837         PolyLine3d l01 = new PolyLine3d(p0, p1);
838         PolyLine3d r = l01.reverse();
839         assertEquals("result has size 2", 2, r.size());
840         assertEquals("point 0 is p1", p1, r.get(0));
841         assertEquals("point 1 is p0", p0, r.get(1));
842 
843         PolyLine3d l05 = new PolyLine3d(p0, p1, p2, p3, p4, p5);
844         r = l05.reverse();
845         assertEquals("result has size 6", 6, r.size());
846         assertEquals("point 0 is p5", p5, r.get(0));
847         assertEquals("point 1 is p4", p4, r.get(1));
848         assertEquals("point 2 is p3", p3, r.get(2));
849         assertEquals("point 3 is p2", p2, r.get(3));
850         assertEquals("point 4 is p1", p1, r.get(4));
851         assertEquals("point 5 is p0", p0, r.get(5));
852 
853         PolyLine2d l2d = l05.project();
854         assertEquals("result has size 6", 6, l2d.size());
855         assertEquals("point 0 is p5", p0.project(), l2d.get(0));
856         assertEquals("point 1 is p4", p1.project(), l2d.get(1));
857         assertEquals("point 2 is p3", p2.project(), l2d.get(2));
858         assertEquals("point 3 is p2", p3.project(), l2d.get(3));
859         assertEquals("point 4 is p1", p4.project(), l2d.get(4));
860         assertEquals("point 5 is p0", p5.project(), l2d.get(5));
861 
862         l05 = new PolyLine3d(p0, p1, p2, p2x, p3, p4, p5);
863         l2d = l05.project();
864         assertEquals("result has size 6", 6, l2d.size());
865         assertEquals("point 0 is p5", p0.project(), l2d.get(0));
866         assertEquals("point 1 is p4", p1.project(), l2d.get(1));
867         assertEquals("point 2 is p3", p2.project(), l2d.get(2));
868         assertEquals("point 3 is p2", p3.project(), l2d.get(3));
869         assertEquals("point 4 is p1", p4.project(), l2d.get(4));
870         assertEquals("point 5 is p0", p5.project(), l2d.get(5));
871 
872         PolyLine3d l22x = new PolyLine3d(p2, p2x);
873         try
874         {
875             l22x.project();
876             fail("Projecting a Polyline3d that entirely projects to one point should have thrown an exception");
877         }
878         catch (DrawRuntimeException dre)
879         {
880             // Ignore expected exception
881         }
882     }
883 
884     /**
885      * Test the extract and extractFraction methods.
886      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
887      */
888     @SuppressWarnings("checkstyle:methodlength")
889     @Test
890     public final void extractTest() throws DrawRuntimeException
891     {
892         Point3d p0 = new Point3d(1, 2, 3);
893         Point3d p1 = new Point3d(2, 3, 4);
894         Point3d p1a = new Point3d(2.01, 3.01, 4.01);
895         Point3d p1b = new Point3d(2.02, 3.02, 4.02);
896         Point3d p1c = new Point3d(2.03, 3.03, 4.03);
897         Point3d p2 = new Point3d(12, 13, 14);
898 
899         PolyLine3d l = new PolyLine3d(p0, p1);
900         PolyLine3d e = l.extractFractional(0, 1);
901         assertEquals("size of extraction is 2", 2, e.size());
902         assertEquals("point 0 is p0", p0, e.get(0));
903         assertEquals("point 1 is p1", p1, e.get(1));
904         try
905         {
906             l.extractFractional(-0.1, 1);
907             fail("negative start should have thrown an exception");
908         }
909         catch (DrawRuntimeException exception)
910         {
911             // Ignore expected exception
912         }
913         try
914         {
915             l.extractFractional(Double.NaN, 1);
916             fail("NaN start should have thrown an exception");
917         }
918         catch (DrawRuntimeException exception)
919         {
920             // Ignore expected exception
921         }
922         try
923         {
924             l.extractFractional(0, 1.1);
925             fail("end > 1 should have thrown an exception");
926         }
927         catch (DrawRuntimeException exception)
928         {
929             // Ignore expected exception
930         }
931         try
932         {
933             l.extractFractional(0, Double.NaN);
934             fail("NaN end should have thrown an exception");
935         }
936         catch (DrawRuntimeException exception)
937         {
938             // Ignore expected exception
939         }
940         try
941         {
942             l.extractFractional(0.6, 0.4);
943             fail("start > end should have thrown an exception");
944         }
945         catch (DrawRuntimeException exception)
946         {
947             // Ignore expected exception
948         }
949         try
950         {
951             l.extract(-0.1, 1);
952             fail("negative start should have thrown an exception");
953         }
954         catch (DrawRuntimeException exception)
955         {
956             // Ignore expected exception
957         }
958         try
959         {
960             l.extract(Double.NaN, 1);
961             fail("NaN start should have thrown an exception");
962         }
963         catch (DrawRuntimeException exception)
964         {
965             // Ignore expected exception
966         }
967         try
968         {
969             l.extract(0, l.getLength() + 0.1);
970             fail("end > length should have thrown an exception");
971         }
972         catch (DrawRuntimeException exception)
973         {
974             // Ignore expected exception
975         }
976         try
977         {
978             l.extract(0, Double.NaN);
979             fail("NaN end should have thrown an exception");
980         }
981         catch (DrawRuntimeException exception)
982         {
983             // Ignore expected exception
984         }
985         try
986         {
987             l.extract(0.6, 0.4);
988             fail("start > end should have thrown an exception");
989         }
990         catch (DrawRuntimeException exception)
991         {
992             // Ignore expected exception
993         }
994 
995         for (int i = 0; i < 10; i++)
996         {
997             for (int j = i + 1; j < 10; j++)
998             {
999                 double start = i * l.getLength() / 10;
1000                 double end = j * l.getLength() / 10;
1001                 // System.err.println("i=" + i + ", j=" + j);
1002                 for (PolyLine3d extractedLine : new PolyLine3d[] { l.extract(start, end),
1003                         l.extractFractional(1.0 * i / 10, 1.0 * j / 10) })
1004                 {
1005                     assertEquals("size of extract is 2", 2, extractedLine.size());
1006                     assertEquals("x of 0", p0.x + (p1.x - p0.x) * i / 10, extractedLine.get(0).x, 0.0001);
1007                     assertEquals("y of 0", p0.y + (p1.y - p0.y) * i / 10, extractedLine.get(0).y, 0.0001);
1008                     assertEquals("z of 0", p0.z + (p1.z - p0.z) * i / 10, extractedLine.get(0).z, 0.0001);
1009                     assertEquals("x of 1", p0.x + (p1.x - p0.x) * j / 10, extractedLine.get(1).x, 0.0001);
1010                     assertEquals("y of 1", p0.y + (p1.y - p0.y) * j / 10, extractedLine.get(1).y, 0.0001);
1011                     assertEquals("z of 1", p0.z + (p1.z - p0.z) * j / 10, extractedLine.get(1).z, 0.0001);
1012                 }
1013             }
1014         }
1015 
1016         for (PolyLine3d line : new PolyLine3d[] { new PolyLine3d(p0, p1, p2), new PolyLine3d(p0, p1, p1a, p1b, p1c, p2) })
1017         {
1018             for (int i = 0; i < 110; i++)
1019             {
1020                 if (10 == i)
1021                 {
1022                     continue; // results are not entirely predictable due to rounding errors
1023                 }
1024                 for (int j = i + 1; j < 110; j++)
1025                 {
1026                     if (10 == j)
1027                     {
1028                         continue; // results are not entirely predictable due to rounding errors
1029                     }
1030                     double start = i * line.getLength() / 110;
1031                     double end = j * line.getLength() / 110;
1032                     // System.err.println("first length is " + firstLength);
1033                     // System.err.println("second length is " + line.getLength());
1034                     // System.err.println("i=" + i + ", j=" + j);
1035                     for (PolyLine3d extractedLine : new PolyLine3d[] { line.extract(start, end),
1036                             line.extractFractional(1.0 * i / 110, 1.0 * j / 110) })
1037                     {
1038                         int expectedSize = i < 10 && j > 10 ? line.size() : 2;
1039                         assertEquals("size is " + expectedSize, expectedSize, extractedLine.size());
1040                         if (i < 10)
1041                         {
1042                             assertEquals("x of 0", p0.x + (p1.x - p0.x) * i / 10, extractedLine.get(0).x, 0.0001);
1043                             assertEquals("y of 0", p0.y + (p1.y - p0.y) * i / 10, extractedLine.get(0).y, 0.0001);
1044                             assertEquals("z of 0", p0.z + (p1.z - p0.z) * i / 10, extractedLine.get(0).z, 0.0001);
1045                         }
1046                         else
1047                         {
1048                             assertEquals("x of 0", p1.x + (p2.x - p1.x) * (i - 10) / 100, extractedLine.get(0).x, 0.0001);
1049                             assertEquals("y of 0", p1.y + (p2.y - p1.y) * (i - 10) / 100, extractedLine.get(0).y, 0.0001);
1050                             assertEquals("z of 0", p1.z + (p2.z - p1.z) * (i - 10) / 100, extractedLine.get(0).z, 0.0001);
1051                         }
1052                         if (j < 10)
1053                         {
1054                             assertEquals("x of 1", p0.x + (p1.x - p0.x) * j / 10, extractedLine.get(1).x, 0.0001);
1055                             assertEquals("y of 1", p0.y + (p1.y - p0.y) * j / 10, extractedLine.get(1).y, 0.0001);
1056                             assertEquals("z of 1", p0.z + (p1.z - p0.z) * j / 10, extractedLine.get(1).z, 0.0001);
1057                         }
1058                         else
1059                         {
1060                             assertEquals("x of last", p1.x + (p2.x - p1.x) * (j - 10) / 100, extractedLine.getLast().x, 0.0001);
1061                             assertEquals("y of last", p1.y + (p2.y - p1.y) * (j - 10) / 100, extractedLine.getLast().y, 0.0001);
1062                             assertEquals("z of last", p1.z + (p2.z - p1.z) * (j - 10) / 100, extractedLine.getLast().z, 0.0001);
1063                         }
1064                         if (extractedLine.size() > 2)
1065                         {
1066                             assertEquals("x of mid", p1.x, extractedLine.get(1).x, 0.0001);
1067                             assertEquals("y of mid", p1.y, extractedLine.get(1).y, 0.0001);
1068                             assertEquals("z of mid", p1.z, extractedLine.get(1).z, 0.0001);
1069                         }
1070                     }
1071                 }
1072             }
1073         }
1074     }
1075 
1076     /**
1077      * Test other methods of PolyLine3d.
1078      * @throws DrawRuntimeException should not happen (if it does, this test has failed)
1079      */
1080     @Test
1081     @SuppressWarnings("unlikely-arg-type")
1082     public final void testOtherMethods() throws DrawRuntimeException
1083     {
1084         Point3d[] array =
1085                 new Point3d[] { new Point3d(1, 2, 3), new Point3d(3, 4, 5), new Point3d(3.2, 4.1, 5.1), new Point3d(5, 6, 7) };
1086         PolyLine3d line = new PolyLine3d(Arrays.stream(array).iterator());
1087         assertEquals("size", array.length, line.size());
1088         for (int i = 0; i < array.length; i++)
1089         {
1090             assertEquals("i-th point", array[i], line.get(i));
1091         }
1092         int nextIndex = 0;
1093         for (Iterator<Point3d> iterator = line.getPoints(); iterator.hasNext();)
1094         {
1095             assertEquals("i-th point from line iterator", array[nextIndex++], iterator.next());
1096         }
1097         assertEquals("iterator returned all points", array.length, nextIndex);
1098 
1099         PolyLine3d filtered = line.noiseFilteredLine(0.0);
1100         assertEquals("filtered with 0 tolerance returns line", line, filtered);
1101         filtered = line.noiseFilteredLine(0.01);
1102         assertEquals("filtered with very low tolerance returns line", line, filtered);
1103         filtered = line.noiseFilteredLine(0.5);
1104         assertEquals("size of filtered line is 3", 3, filtered.size());
1105         assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
1106         assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
1107         assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
1108         filtered = line.noiseFilteredLine(10);
1109         assertEquals("size of filtered line is 2", 2, filtered.size());
1110         assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
1111         assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
1112 
1113         array = new Point3d[] { new Point3d(1, 2, 3), new Point3d(3, 4, 5), new Point3d(3.2, 4.1, 5.1), new Point3d(1, 2, 3) };
1114         line = new PolyLine3d(Arrays.stream(array).iterator());
1115         filtered = line.noiseFilteredLine(10);
1116         assertEquals("size of filtered line is 3", 3, filtered.size());
1117         assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
1118         assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
1119         assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
1120 
1121         array = new Point3d[] { new Point3d(1, 2, 3), new Point3d(3, 4, 5), new Point3d(1.1, 2.1, 3), new Point3d(1, 2, 3) };
1122         line = new PolyLine3d(Arrays.stream(array).iterator());
1123         filtered = line.noiseFilteredLine(0.5);
1124         assertEquals("size of filtered line is 3", 3, filtered.size());
1125         assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
1126         assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
1127         assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
1128 
1129         array = new Point3d[] { new Point3d(1, 2, 3), new Point3d(3, 4, 5) };
1130         line = new PolyLine3d(Arrays.stream(array).iterator());
1131         filtered = line.noiseFilteredLine(10);
1132         assertEquals("Filtering a two-point line returns that line", line, filtered);
1133 
1134         array = new Point3d[] { new Point3d(1, 2, 3), new Point3d(1, 2, 3), new Point3d(1, 2, 3), new Point3d(3, 4, 5) };
1135         line = new PolyLine3d(true, array);
1136         assertEquals("cleaned line has 2 points", 2, line.size());
1137         assertEquals("first point", array[0], line.getFirst());
1138         assertEquals("last point", array[array.length - 1], line.getLast());
1139 
1140         array = new Point3d[] { new Point3d(1, 2, 3), new Point3d(1, 2, 3), new Point3d(3, 4, 5), new Point3d(3, 4, 5) };
1141         line = new PolyLine3d(true, array);
1142         assertEquals("cleaned line has 2 points", 2, line.size());
1143         assertEquals("first point", array[0], line.getFirst());
1144         assertEquals("last point", array[array.length - 1], line.getLast());
1145 
1146         array = new Point3d[] { new Point3d(0, -1, 3), new Point3d(1, 2, 4), new Point3d(1, 2, 4), new Point3d(3, 4, 4) };
1147         line = new PolyLine3d(true, array);
1148         assertEquals("cleaned line has 2 points", 3, line.size());
1149         assertEquals("first point", array[0], line.getFirst());
1150         assertEquals("last point", array[array.length - 1], line.getLast());
1151 
1152         array = new Point3d[] { new Point3d(0, -1, 3), new Point3d(1, 2, 4), new Point3d(1, 2, 4), new Point3d(1, 2, 4),
1153                 new Point3d(3, 4, 5) };
1154         line = new PolyLine3d(true, array);
1155         assertEquals("cleaned line has 3 points", 3, line.size());
1156         assertEquals("first point", array[0], line.getFirst());
1157         assertEquals("mid point", array[1], line.get(1));
1158         assertEquals("last point", array[array.length - 1], line.getLast());
1159 
1160         try
1161         {
1162             new PolyLine3d(true, new Point3d[0]);
1163             fail("Too short array should have thrown a DrawRuntimeException");
1164         }
1165         catch (DrawRuntimeException dre)
1166         {
1167             // Ignore expected exception
1168         }
1169 
1170         try
1171         {
1172             new PolyLine3d(true, new Point3d[] { new Point3d(1, 2, 3) });
1173             fail("Too short array should have thrown a DrawRuntimeException");
1174         }
1175         catch (DrawRuntimeException dre)
1176         {
1177             // Ignore expected exception
1178         }
1179 
1180         try
1181         {
1182             new PolyLine3d(true, new Point3d[] { new Point3d(1, 2, 3), new Point3d(1, 2, 3) });
1183             fail("All duplicate points in array should have thrown a DrawRuntimeException");
1184         }
1185         catch (DrawRuntimeException dre)
1186         {
1187             // Ignore expected exception
1188         }
1189 
1190         try
1191         {
1192             new PolyLine3d(true, new Point3d[] { new Point3d(1, 2, 3), new Point3d(1, 2, 3), new Point3d(1, 2, 3) });
1193             fail("All duplicate points in array should have thrown a DrawRuntimeException");
1194         }
1195         catch (DrawRuntimeException dre)
1196         {
1197             // Ignore expected exception
1198         }
1199 
1200         array = new Point3d[] { new Point3d(1, 2, 3), new Point3d(4, 6, 9), new Point3d(8, 9, 15) };
1201         line = new PolyLine3d(array);
1202 
1203         try
1204         {
1205             line.getLocation(-0.1);
1206             fail("negative location should have thrown a DrawRuntimeException");
1207         }
1208         catch (DrawRuntimeException dre)
1209         {
1210             // Ignore expected exception
1211         }
1212 
1213         double length = line.getLength();
1214         assertEquals("Length of line is about 15.6", 15.6, length, 0.1);
1215 
1216         try
1217         {
1218             line.getLocation(length + 0.1);
1219             fail("location beyond length should have thrown a DrawRuntimeException");
1220         }
1221         catch (DrawRuntimeException dre)
1222         {
1223             // Ignore expected exception
1224         }
1225 
1226         try
1227         {
1228             line.getLocation(-0.1);
1229             fail("negative location should have thrown a DrawRuntimeException");
1230         }
1231         catch (DrawRuntimeException dre)
1232         {
1233             // Ignore expected exception
1234         }
1235 
1236         assertEquals("Length of line is 15.6", 15.6, length, 0.1);
1237 
1238         try
1239         {
1240             line.getLocationFraction(1.1);
1241             fail("location beyond length should have thrown a DrawRuntimeException");
1242         }
1243         catch (DrawRuntimeException dre)
1244         {
1245             // Ignore expected exception
1246         }
1247 
1248         try
1249         {
1250             line.getLocationFraction(-0.1);
1251             fail("negative location should have thrown a DrawRuntimeException");
1252         }
1253         catch (DrawRuntimeException dre)
1254         {
1255             // Ignore expected exception
1256         }
1257 
1258         for (double position : new double[] { -1, 0, 2.5, 4.9, 5.1, 7.5, 9.9, 10, 11 })
1259         {
1260             Ray3d ray = line.getLocationExtended(position);
1261             if (position < length / 2)
1262             {
1263                 Ray3d expected =
1264                         new Ray3d(array[0].interpolate(array[1], position / (length / 2)), Math.atan2(4, 3), Math.atan2(5, 6));
1265                 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
1266             }
1267             else
1268             {
1269                 Ray3d expected = new Ray3d(array[1].interpolate(array[2], (position - length / 2) / (length / 2)),
1270                         Math.atan2(3, 4), Math.atan2(5, 6));
1271                 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
1272             }
1273             ray = line.getLocationFractionExtended(position / line.getLength());
1274             if (position < length / 2)
1275             {
1276                 Ray3d expected =
1277                         new Ray3d(array[0].interpolate(array[1], position / (length / 2)), Math.atan2(4, 3), Math.atan2(5, 6));
1278                 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
1279             }
1280             else
1281             {
1282                 Ray3d expected = new Ray3d(array[1].interpolate(array[2], (position - length / 2) / (length / 2)),
1283                         Math.atan2(3, 4), Math.atan2(5, 6));
1284                 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
1285             }
1286         }
1287 
1288         // Test the projectOrthogonal methods
1289         array = new Point3d[] { new Point3d(1, 2, 3), new Point3d(4, 6, 8), new Point3d(8, 9, 13) };
1290         line = new PolyLine3d(array);
1291         for (double x = -15; x <= 20; x++)
1292         {
1293             for (double y = -15; y <= 20; y++)
1294             {
1295                 for (double z = -15; z <= 20; z++)
1296                 {
1297                     Point3d xyz = new Point3d(x, y, z);
1298                     // System.out.println("x=" + x + ", y=" + y);
1299                     double result = line.projectOrthogonalFractional(xyz);
1300                     if (!Double.isNaN(result))
1301                     {
1302                         assertTrue("result must be >= 0.0", result >= 0);
1303                         assertTrue("result must be <= 1.0", result <= 1.0);
1304                         Ray3d ray = line.getLocationFraction(result);
1305                         Point3d projected = line.projectOrthogonal(xyz);
1306                         assertEquals("if fraction is between 0 and 1; projectOrthogonal yiels point at that fraction", ray.x,
1307                                 projected.x, 00001);
1308                         assertEquals("if fraction is between 0 and 1; projectOrthogonal yiels point at that fraction", ray.y,
1309                                 projected.y, 00001);
1310                         assertEquals("if fraction is between 0 and 1; projectOrthogonal yiels point at that fraction", ray.z,
1311                                 projected.z, 00001);
1312                     }
1313                     else
1314                     {
1315                         assertNull("point projects outside line", line.projectOrthogonal(xyz));
1316                     }
1317                     result = line.projectOrthogonalFractionalExtended(xyz);
1318                     if (!Double.isNaN(result))
1319                     {
1320                         Point3d resultPoint = line.getLocationFractionExtended(result);
1321                         if (result >= 0.0 && result <= 1.0)
1322                         {
1323                             Point3d closestPointOnLine = line.closestPointOnPolyLine(xyz);
1324                             assertEquals("resultPoint is equal to closestPoint", resultPoint, closestPointOnLine);
1325                             assertEquals("getLocationFraction returns same as getLocationfractionExtended", resultPoint,
1326                                     line.getLocationFraction(result));
1327                         }
1328                         else
1329                         {
1330                             try
1331                             {
1332                                 line.getLocationFraction(result);
1333                                 fail("illegal fraction should have thrown a DrawRuntimeException");
1334                             }
1335                             catch (DrawRuntimeException dre)
1336                             {
1337                                 // Ignore expected exception
1338                             }
1339                             if (result < 0)
1340                             {
1341                                 assertEquals("resultPoint lies on extention of start segment",
1342                                         resultPoint.distance(line.get(1)) - resultPoint.distance(line.getFirst()),
1343                                         line.getFirst().distance(line.get(1)), 0.0001);
1344                             }
1345                             else
1346                             {
1347                                 // result > 1
1348                                 assertEquals("resultPoint lies on extention of end segment",
1349                                         resultPoint.distance(line.get(line.size() - 2)) - resultPoint.distance(line.getLast()),
1350                                         line.getLast().distance(line.get(line.size() - 2)), 0.0001);
1351                             }
1352                         }
1353                     }
1354                     else
1355                     {
1356                         assertNull("point projects outside extended line", line.projectOrthogonalExtended(xyz));
1357                         Point3d closestPointOnLine = line.closestPointOnPolyLine(xyz);
1358                         assertNotNull("closest point is never null", closestPointOnLine);
1359                         boolean found = false;
1360                         for (int index = 0; index < line.size(); index++)
1361                         {
1362                             Point3d linePoint = line.get(index);
1363                             if (linePoint.x == closestPointOnLine.x && linePoint.y == closestPointOnLine.y)
1364                             {
1365                                 found = true;
1366                             }
1367                         }
1368                         assertTrue("closestPointOnLine is one of the construction points of the line", found);
1369                     }
1370                     Point3d closestPointOnLine = line.closestPointOnPolyLine(xyz);
1371                     assertNotNull("closest point is never null", closestPointOnLine);
1372                 }
1373             }
1374         }
1375         Point3d toleranceResultPoint = line.getLocationFraction(-0.01, 0.01);
1376         assertEquals("tolerance result matches extended fraction result", line.getLocationFraction(0), toleranceResultPoint);
1377         toleranceResultPoint = line.getLocationFraction(1.01, 0.01);
1378         assertEquals("tolerance result matches extended fraction result", line.getLocationFraction(1), toleranceResultPoint);
1379 
1380         try
1381         {
1382             line.getLocationFraction(-.011, 0.01);
1383             fail("fraction outside tolerance should have thrown a DrawRuntimeException");
1384         }
1385         catch (DrawRuntimeException dre)
1386         {
1387             // Ignore expected exception
1388         }
1389 
1390         try
1391         {
1392             line.getLocationFraction(1.011, 0.01);
1393             fail("fraction outside tolerance should have thrown a DrawRuntimeException");
1394         }
1395         catch (DrawRuntimeException dre)
1396         {
1397             // Ignore expected exception
1398         }
1399 
1400         // Test the extract and truncate methods
1401         array = new Point3d[] { new Point3d(1, 2, 3), new Point3d(4, 6, 8), new Point3d(8, 9, 10) };
1402         line = new PolyLine3d(array);
1403         length = line.getLength();
1404         for (double to : new double[] { -10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20 })
1405         {
1406             if (to <= 0 || to > length)
1407             {
1408                 try
1409                 {
1410                     line.truncate(to);
1411                     fail("illegal truncate should have thrown a DrawRuntimeException");
1412                 }
1413                 catch (DrawRuntimeException dre)
1414                 {
1415                     // Ignore expected exception
1416                 }
1417             }
1418             else
1419             {
1420                 PolyLine3d truncated = line.truncate(to);
1421                 assertEquals("truncated line start with start point of line", line.getFirst(), truncated.getFirst());
1422                 assertEquals("Length of truncated line is truncate position", to, truncated.getLength(), 0.0001);
1423             }
1424             for (double from : new double[] { -10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20 })
1425             {
1426                 if (from >= to || from < 0 || to > length)
1427                 {
1428                     try
1429                     {
1430                         line.extract(from, to);
1431                         fail("Illegal range should have thrown a DrawRuntimeException");
1432                     }
1433                     catch (DrawRuntimeException dre)
1434                     {
1435                         // Ignore expected exception
1436                     }
1437                 }
1438                 else
1439                 {
1440                     PolyLine3d fragment = line.extract(from, to);
1441                     Point3d fromPoint = line.getLocation(from);
1442                     assertTrue("fragment starts at from", fromPoint.epsilonEquals(fragment.getFirst(), 0.00001));
1443                     Point3d toPoint = line.getLocation(to);
1444                     assertTrue("fragment ends at to", toPoint.epsilonEquals(fragment.getLast(), 0.00001));
1445                     assertEquals("Length of fragment", to - from, fragment.getLength(), 0.0001);
1446                     if (from == 0)
1447                     {
1448                         assertEquals("fragment starts at begin of line", line.getFirst(), fragment.getFirst());
1449                     }
1450                     if (to == length)
1451                     {
1452                         assertEquals("fragment ends at end of line", line.getLast(), fragment.getLast());
1453                     }
1454                 }
1455             }
1456         }
1457         try
1458         {
1459             line.extract(Double.NaN, 10.0);
1460             fail("NaN value should have thrown a DrawRuntimeException");
1461         }
1462         catch (DrawRuntimeException dre)
1463         {
1464             // Ignore expected exception
1465         }
1466 
1467         try
1468         {
1469             line.extract(0.0, Double.NaN);
1470             fail("NaN value should have thrown a DrawRuntimeException");
1471         }
1472         catch (DrawRuntimeException dre)
1473         {
1474             // Ignore expected exception
1475         }
1476 
1477         // Verify that hashCode. Check that the result depends on the actual coordinates.
1478         assertNotEquals("hash code takes x coordinate of first point into account",
1479                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1480                 new PolyLine3d(new Point3d(1, 0, 0), new Point3d(1, 1, 1)).hashCode());
1481         assertNotEquals("hash code takes y coordinate of first point into account",
1482                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1483                 new PolyLine3d(new Point3d(0, 1, 0), new Point3d(1, 1, 1)).hashCode());
1484         assertNotEquals("hash code takes z coordinate of first point into account",
1485                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1486                 new PolyLine3d(new Point3d(0, 0, 1), new Point3d(1, 1, 1)).hashCode());
1487         assertNotEquals("hash code takes x coordinate of second point into account",
1488                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1489                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(2, 1, 1)).hashCode());
1490         assertNotEquals("hash code takes y coordinate of second point into account",
1491                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1492                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 2, 1)).hashCode());
1493         assertNotEquals("hash code takes z coordinate of second point into account",
1494                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1495                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 2)).hashCode());
1496 
1497         // Verify the equals method.
1498         assertTrue("line is equal to itself", line.equals(line));
1499         assertFalse("line is not equal to a different line",
1500                 line.equals(new PolyLine3d(new Point3d(123, 456, 789), new Point3d(789, 101112, 987))));
1501         assertFalse("line is not equal to null", line.equals(null));
1502         assertFalse("line is not equal to a different kind of object", line.equals("unlikely"));
1503         assertTrue("Line is equal to line from same set of points", line.equals(new PolyLine3d(line.getPoints())));
1504         // Make a line that differs only in the very last point
1505         Point3d[] otherArray = Arrays.copyOf(array, array.length);
1506         otherArray[otherArray.length - 1] = new Point3d(otherArray[otherArray.length - 1].x,
1507                 otherArray[otherArray.length - 1].y + 5, otherArray[otherArray.length - 1].z);
1508         PolyLine3d other = new PolyLine3d(otherArray);
1509         assertFalse("PolyLine3d that differs in y of last point is different", line.equals(other));
1510     }
1511 
1512     /**
1513      * Test the find method.
1514      * @throws DrawRuntimeException if that happens uncaught; this test has failed
1515      * @throws SecurityException if that happens uncaught; this test has failed
1516      * @throws NoSuchMethodException if that happens uncaught; this test has failed
1517      * @throws InvocationTargetException if that happens uncaught; this test has failed
1518      * @throws IllegalArgumentException if that happens uncaught; this test has failed
1519      * @throws IllegalAccessException if that happens uncaught; this test has failed
1520      */
1521     @Test
1522     public final void testFind() throws DrawRuntimeException, NoSuchMethodException, SecurityException, IllegalAccessException,
1523             IllegalArgumentException, InvocationTargetException
1524     {
1525         // Construct a line with exponentially increasing distances
1526         List<Point3d> points = new ArrayList<>();
1527         for (int i = 0; i < 20; i++)
1528         {
1529             points.add(new Point3d(Math.pow(2, i) - 1, 10, 20));
1530         }
1531         PolyLine3d line = new PolyLine3d(points);
1532         double end = points.get(points.size() - 1).x;
1533         for (int i = 0; i < end; i++)
1534         {
1535             double pos = i + 0.5;
1536             int index = line.find(pos);
1537             assertTrue("segment starts before pos", line.get(index).x <= pos);
1538             assertTrue("next segment starts after pos", line.get(index + 1).x >= pos);
1539         }
1540         assertEquals("pos 0 returns index 0", 0, line.find(0.0));
1541     }
1542 
1543     /**
1544      * Test the truncate method.
1545      * @throws DrawRuntimeException if that happens uncaught; this test has failed
1546      */
1547     @Test
1548     public final void testTruncate() throws DrawRuntimeException
1549     {
1550         Point3d from = new Point3d(10, 20, 30);
1551         Point3d to = new Point3d(70, 80, 90);
1552         double length = from.distance(to);
1553         PolyLine3d line = new PolyLine3d(from, to);
1554         PolyLine3d truncatedLine = line.truncate(length);
1555         assertEquals("Start of line truncated at full length is the same as start of the input line", truncatedLine.get(0),
1556                 from);
1557         assertEquals("End of line truncated at full length is about the same as end of input line", 0,
1558                 truncatedLine.get(1).distance(to), 0.0001);
1559         try
1560         {
1561             line.truncate(-0.1);
1562             fail("truncate at negative length should have thrown DrawRuntimeException");
1563         }
1564         catch (DrawRuntimeException dre)
1565         {
1566             // Ignore expected exception
1567         }
1568         try
1569         {
1570             line.truncate(length + 0.1);
1571             fail("truncate at length beyond length of line should have thrown DrawExDrawRuntimeExceptionception");
1572         }
1573         catch (DrawRuntimeException dre)
1574         {
1575             // Ignore expected exception
1576         }
1577         truncatedLine = line.truncate(length / 2);
1578         assertEquals("Start of truncated line is the same as start of the input line", truncatedLine.get(0), from);
1579         Point3d halfWay = new Point3d((from.x + to.x) / 2, (from.y + to.y) / 2, (from.z + to.z) / 2);
1580         assertEquals("End of 50%, truncated 2-point line should be at the half way point", 0,
1581                 halfWay.distance(truncatedLine.get(1)), 0.0001);
1582         Point3d intermediatePoint = new Point3d(20, 20, 20);
1583         line = new PolyLine3d(from, intermediatePoint, to);
1584         length = from.distance(intermediatePoint) + intermediatePoint.distance(to);
1585         truncatedLine = line.truncate(length);
1586         assertEquals("Start of line truncated at full length is the same as start of the input line", truncatedLine.get(0),
1587                 from);
1588         assertEquals("End of line truncated at full length is about the same as end of input line", 0,
1589                 truncatedLine.get(2).distance(to), 0.0001);
1590         truncatedLine = line.truncate(from.distance(intermediatePoint));
1591         assertEquals("Start of line truncated at full length is the same as start of the input line", truncatedLine.get(0),
1592                 from);
1593         assertEquals("Line truncated at intermediate point ends at that intermediate point", 0,
1594                 truncatedLine.get(1).distance(intermediatePoint), 0.0001);
1595     }
1596 
1597     /**
1598      * Test the debugging output methods.
1599      */
1600     @Test
1601     public void testExports()
1602     {
1603         Point3d[] points = new Point3d[] { new Point3d(123.456, 345.678, 901.234), new Point3d(234.567, 456.789, 12.345),
1604                 new Point3d(-12.345, -34.567, 45.678) };
1605         PolyLine3d pl = new PolyLine3d(points);
1606         String[] out = pl.toExcel().split("\\n");
1607         assertEquals("Excel output consists of one line per point", points.length, out.length);
1608         for (int index = 0; index < points.length; index++)
1609         {
1610             String[] fields = out[index].split("\\t");
1611             assertEquals("each line consists of three fields", 3, fields.length);
1612             try
1613             {
1614                 double x = Double.parseDouble(fields[0].trim());
1615                 assertEquals("x matches", points[index].x, x, 0.001);
1616             }
1617             catch (NumberFormatException nfe)
1618             {
1619                 fail("First field " + fields[0] + " does not parse as a double");
1620             }
1621             try
1622             {
1623                 double y = Double.parseDouble(fields[1].trim());
1624                 assertEquals("y matches", points[index].y, y, 0.001);
1625             }
1626             catch (NumberFormatException nfe)
1627             {
1628                 fail("Second field " + fields[1] + " does not parse as a double");
1629             }
1630             try
1631             {
1632                 double z = Double.parseDouble(fields[2].trim());
1633                 assertEquals("z matches", points[index].z, z, 0.001);
1634             }
1635             catch (NumberFormatException nfe)
1636             {
1637                 fail("Second field " + fields[2] + " does not parse as a double");
1638             }
1639         }
1640     }
1641 
1642     /**
1643      * Test the hashCode and Equals methods.
1644      * @throws DrawRuntimeException when that happens uncaught; this test has failed
1645      * @throws NullPointerException when that happens uncaught; this test has failed
1646      */
1647     @SuppressWarnings("unlikely-arg-type")
1648     @Test
1649     public void testToStringHashCodeAndEquals() throws NullPointerException, DrawRuntimeException
1650     {
1651         PolyLine3d line = new PolyLine3d(new Point3d[] { new Point3d(1, 2, 3), new Point3d(4, 6, 8), new Point3d(8, 9, 10) });
1652         assertTrue("toString returns something descriptive", line.toString().startsWith("PolyLine3d ["));
1653         assertFalse("toString does not contain startPhi", line.toString().contains("startPhi"));
1654         assertFalse("toString does not contain startTheta", line.toString().contains("startTheta"));
1655         assertTrue("toString can suppress the class name", line.toString().indexOf(line.toString(true)) > 0);
1656 
1657         // Verify that hashCode. Check that the result depends on the actual coordinates.
1658         assertNotEquals("hash code takes x coordinate into account",
1659                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1660                 new PolyLine3d(new Point3d(1, 0, 0), new Point3d(1, 1, 1)).hashCode());
1661         assertNotEquals("hash code takes y coordinate into account",
1662                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1663                 new PolyLine3d(new Point3d(0, 1, 0), new Point3d(1, 1, 1)).hashCode());
1664         assertNotEquals("hash code takes z coordinate into account",
1665                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1666                 new PolyLine3d(new Point3d(0, 0, 1), new Point3d(1, 1, 1)).hashCode());
1667         assertNotEquals("hash code takes x coordinate into account",
1668                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1669                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(2, 1, 1)).hashCode());
1670         assertNotEquals("hash code takes y coordinate into account",
1671                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1672                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 2, 1)).hashCode());
1673         assertNotEquals("hash code takes z coordinate into account",
1674                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 1)).hashCode(),
1675                 new PolyLine3d(new Point3d(0, 0, 0), new Point3d(1, 1, 2)).hashCode());
1676 
1677         // Verify the equals method.
1678         assertTrue("line is equal to itself", line.equals(line));
1679         assertFalse("line is not equal to a different line",
1680                 line.equals(new PolyLine2d(new Point2d(123, 456), new Point2d(789, 101112))));
1681         assertFalse("line is not equal to null", line.equals(null));
1682         assertFalse("line is not equal to a different kind of object", line.equals("unlikely"));
1683         assertEquals("equals verbatim copy", line,
1684                 new PolyLine3d(new Point3d[] { new Point3d(1, 2, 3), new Point3d(4, 6, 8), new Point3d(8, 9, 10) }));
1685         assertNotEquals("equals checks x", line,
1686                 new PolyLine3d(new Point3d[] { new Point3d(2, 2, 3), new Point3d(4, 6, 8), new Point3d(8, 9, 10) }));
1687         assertNotEquals("equals checks y", line,
1688                 new PolyLine3d(new Point3d[] { new Point3d(1, 2, 3), new Point3d(4, 7, 8), new Point3d(8, 9, 10) }));
1689         assertNotEquals("equals checks z", line,
1690                 new PolyLine3d(new Point3d[] { new Point3d(1, 2, 3), new Point3d(4, 6, 8), new Point3d(8, 9, 11) }));
1691         assertTrue("Line is equal to line from same set of points", line.equals(new PolyLine3d(line.getPoints())));
1692     }
1693 
1694     /**
1695      * Test for a problem that occurred in OTS2.
1696      * @throws DrawRuntimeException when that happens, this test has failed
1697      */
1698     @Test
1699     public void testProjectProblem() throws DrawRuntimeException
1700     {
1701         PolyLine3d polyLine3d = new PolyLine3d(new Point3d(1, 1, 2), new Point3d(11, 1, 5), new Point3d(16, 6, 0),
1702                 new Point3d(21, 6, 0), new Point3d(21, 0, 0));
1703         double x = 11;
1704         double y = 1;
1705         Point2d point = new Point2d(x, y);
1706         // The difficult work is done with the line projected on the the Z=0 plane
1707         PolyLine2d projectedLine = polyLine3d.project();
1708         // Project (x, y) onto each segment of the projected line
1709         int bestSegmentIndex = -1;
1710         double bestDistance = Double.POSITIVE_INFINITY;
1711         double bestSegmentDirection = Double.NaN;
1712         Point2d prevPoint = null;
1713         // Find the nearest segment
1714         for (int index = 0; index < projectedLine.size(); index++)
1715         {
1716             Point2d nextPoint = projectedLine.get(index);
1717             if (null != prevPoint)
1718             {
1719                 Point2d closestOnSegment = point.closestPointOnSegment(prevPoint, nextPoint);
1720                 double distance = closestOnSegment.distance(point);
1721                 if (distance < bestDistance)
1722                 {
1723                     bestDistance = distance;
1724                     bestSegmentIndex = index;
1725                     bestSegmentDirection = prevPoint.directionTo(nextPoint);
1726                 }
1727             }
1728             prevPoint = nextPoint;
1729         }
1730         // bestSegmentIndex is the index of the point where the best segment ENDS
1731         // Make the rays that bisect the angles at the start and end of the segment
1732         double prevDirection = projectedLine.get(bestSegmentIndex - 1).directionTo(projectedLine.get(bestSegmentIndex));
1733         double nextDirection = bestSegmentIndex < projectedLine.size() - 1
1734                 ? projectedLine.get(bestSegmentIndex).directionTo(projectedLine.get(bestSegmentIndex + 1))
1735                 : projectedLine.get(projectedLine.size() - 2).directionTo(projectedLine.getLast());
1736         Ray2d prevRay =
1737                 new Ray2d(projectedLine.get(bestSegmentIndex - 1), (prevDirection + bestSegmentDirection) / 2 + Math.PI / 2);
1738         Ray2d nextRay =
1739                 new Ray2d(projectedLine.get(bestSegmentIndex), (bestSegmentDirection + nextDirection) / 2 + Math.PI / 2);
1740         // Project the point onto each ray
1741         Point2d prevRayProjection = prevRay.projectOrthogonalExtended(point);
1742         Point2d nextRayProjection = nextRay.projectOrthogonalExtended(point);
1743         Point2d projectionOnBestSegment =
1744                 prevRay.interpolate(nextRay, point.distance(prevRayProjection) / prevRayProjection.distance(nextRayProjection));
1745         // Find the corresponding fractional location on the input polyLine3d
1746         // Find the corresponding segment on the polyLine3d
1747         for (int index = 1; index < polyLine3d.size(); index++)
1748         {
1749             // Comparing double values; but that should work as the coordinates of the rays are exact copies of the x and y
1750             // coordinates of the polyLine3d
1751             if (polyLine3d.getX(index - 1) == prevRay.x && polyLine3d.getY(index - 1) == prevRay.y
1752                     && polyLine3d.getX(index) == nextRay.x && polyLine3d.getY(index) == nextRay.y)
1753             {
1754                 double lengthAtPrevRay = polyLine3d.lengthAtIndex(index - 1);
1755                 double fraction = (lengthAtPrevRay + prevRay.distance(projectionOnBestSegment) / prevRay.distance(nextRay)
1756                         * (polyLine3d.lengthAtIndex(index) - lengthAtPrevRay)) / polyLine3d.getLength();
1757 
1758                 polyLine3d.getLocationFraction(fraction); // This operation failed
1759             }
1760         }
1761     }
1762 
1763     /**
1764      * Test the transitionLine method.
1765      */
1766     @Test
1767     public void testTransitionLine()
1768     {
1769         // Create a Bezier with a 90 degree change of direction starting in X direction, ending in Y direction
1770         PolyLine3d bezier = Bezier.cubic(64, new Ray3d(-5, 0, 2, 0, 0, 2), new Ray3d(0, 5, 2, 0, 7, 2));
1771         // System.out.print("c1,0,0" + bezier1.project().toPlot());
1772         double length = bezier.getLength();
1773         double prevDir = Double.NaN;
1774         for (int step = 0; step <= 1000; step++)
1775         {
1776             double distance = length * step / 1000;
1777             Ray3d ray = bezier.getLocation(distance);
1778             double direction = Math.toDegrees(ray.phi);
1779             if (step > 0)
1780             {
1781                 assertEquals("phi changes very little at step " + step, prevDir, direction, 2);
1782             }
1783             prevDir = Math.toDegrees(ray.phi);
1784         }
1785         // Make a gradually transitioning offset line
1786         PolyLine3d transitioningOffsetLine = bezier.offsetLine(0, 2);
1787         // Verify that this curve is fairly smooth
1788         length = transitioningOffsetLine.getLength();
1789         prevDir = Double.NaN;
1790         for (int step = 0; step <= 1000; step++)
1791         {
1792             double distance = length * step / 1000;
1793             Ray3d ray = transitioningOffsetLine.getLocation(distance);
1794             double direction = Math.toDegrees(ray.phi);
1795             if (step > 0)
1796             {
1797                 assertEquals("phi changes very little at step " + step, prevDir, direction, 2);
1798             }
1799             prevDir = Math.toDegrees(ray.phi);
1800         }
1801         PolyLine3d endLine = bezier.offsetLine(-2);
1802         // System.out.print("c0,1,0" + endLine.project().toPlot());
1803         TransitionFunction transitionFunction = new TransitionFunction()
1804         {
1805             @Override
1806             public double function(final double fraction)
1807             {
1808                 return 0.5 - Math.cos(fraction * Math.PI) / 2;
1809             }
1810         };
1811         PolyLine3d cosineSmoothTransitioningLine = bezier.transitionLine(endLine, transitionFunction);
1812         // System.out.print("c0,0,0" + cosineSmoothTransitioningLine.project().toPlot());
1813         length = cosineSmoothTransitioningLine.getLength();
1814         prevDir = Double.NaN;
1815         for (int step = 0; step <= 1000; step++)
1816         {
1817             double distance = length * step / 1000;
1818             Ray3d ray = cosineSmoothTransitioningLine.getLocation(distance);
1819             double direction = Math.toDegrees(ray.phi);
1820             if (step > 0)
1821             {
1822                 assertEquals("phi changes very little at step " + step, prevDir, direction, 4);
1823             }
1824             prevDir = Math.toDegrees(ray.phi);
1825         }
1826         // System.out.print(
1827         // "c0,0,1" + Bezier.cubic(bezier1.getLocationFraction(0), endLine.getLocationFraction(1)).project().toPlot());
1828         // Reverse the lines
1829         PolyLine3d cosineSmoothTransitioningLine2 =
1830                 endLine.reverse().transitionLine(bezier.reverse(), transitionFunction).reverse();
1831         // Check that those lines are very similar
1832         assertEquals("Lengths are equal", cosineSmoothTransitioningLine.getLength(), cosineSmoothTransitioningLine2.getLength(),
1833                 0.001);
1834         for (int step = 0; step <= 1000; step++)
1835         {
1836             Ray3d ray1 = cosineSmoothTransitioningLine.getLocation(step * cosineSmoothTransitioningLine.getLength() / 1000);
1837             Ray3d ray2 = cosineSmoothTransitioningLine2.getLocation(step * cosineSmoothTransitioningLine2.getLength() / 1000);
1838             assertEquals("rays are almost equal in x", ray1.x, ray2.x, 0.001);
1839             assertEquals("rays are almost equal in y", ray1.y, ray2.y, 0.001);
1840             assertEquals("rays are almost equal in z", ray1.z, ray2.z, 0.001);
1841             assertEquals("rays are almost equal in phi", ray1.phi, ray2.phi, 0.0001);
1842             assertEquals("rays are almost equal in theta", ray1.theta, ray2.theta, 0.0001);
1843         }
1844 
1845         assertEquals("offset by zero returns original", bezier, bezier.offsetLine(0, 0));
1846         assertEquals("offset by constant with two arguments returns same as offset with one argument", bezier.offsetLine(3, 3),
1847                 bezier.offsetLine(3));
1848     }
1849 
1850     /**
1851      * Test the degenerate PolyLine3d.
1852      */
1853     @Test
1854     public void testDegenerate()
1855     {
1856         try
1857         {
1858             new PolyLine3d(Double.NaN, 2, 2.5, 3, -1);
1859             fail("NaN should have thrown a DrawRuntimeException");
1860         }
1861         catch (DrawRuntimeException dre)
1862         {
1863             // Ignore expected exception
1864         }
1865 
1866         try
1867         {
1868             new PolyLine3d(1, Double.NaN, 2.5, 3, -1);
1869             fail("NaN should have thrown a DrawRuntimeException");
1870         }
1871         catch (DrawRuntimeException dre)
1872         {
1873             // Ignore expected exception
1874         }
1875 
1876         try
1877         {
1878             new PolyLine3d(1, 2, Double.NaN, 3, -1);
1879             fail("NaN should have thrown a DrawRuntimeException");
1880         }
1881         catch (DrawRuntimeException dre)
1882         {
1883             // Ignore expected exception
1884         }
1885 
1886         try
1887         {
1888             new PolyLine3d(1, 2, 2.5, Double.NaN, -1);
1889             fail("NaN should have thrown a DrawRuntimeException");
1890         }
1891         catch (DrawRuntimeException dre)
1892         {
1893             // Ignore expected exception
1894         }
1895 
1896         try
1897         {
1898             new PolyLine3d(1, 2, 2.5, 3, Double.NaN);
1899             fail("NaN should have thrown a DrawRuntimeException");
1900         }
1901         catch (DrawRuntimeException dre)
1902         {
1903             // Ignore expected exception
1904         }
1905 
1906         try
1907         {
1908             new PolyLine3d(1, 2, 2.5, Double.POSITIVE_INFINITY, -1);
1909             fail("NaN should have thrown a DrawRuntimeException");
1910         }
1911         catch (DrawRuntimeException dre)
1912         {
1913             // Ignore expected exception
1914         }
1915 
1916         try
1917         {
1918             new PolyLine3d(1, 2, 2.5, 3, Double.POSITIVE_INFINITY);
1919             fail("NaN should have thrown a DrawRuntimeException");
1920         }
1921         catch (DrawRuntimeException dre)
1922         {
1923             // Ignore expected exception
1924         }
1925 
1926         try
1927         {
1928             new PolyLine3d(1, 2, 2.5, Double.NEGATIVE_INFINITY, -1);
1929             fail("NaN should have thrown a DrawRuntimeException");
1930         }
1931         catch (DrawRuntimeException dre)
1932         {
1933             // Ignore expected exception
1934         }
1935 
1936         try
1937         {
1938             new PolyLine3d(1, 2, 2.5, 3, Double.NEGATIVE_INFINITY);
1939             fail("NaN should have thrown a DrawRuntimeException");
1940         }
1941         catch (DrawRuntimeException dre)
1942         {
1943             // Ignore expected exception
1944         }
1945 
1946         PolyLine3d l = new PolyLine3d(1, 2, 2.5, 3, -1);
1947         assertEquals("length is 0", 0, l.getLength(), 0);
1948         assertEquals("size is 1", 1, l.size());
1949         assertEquals("getX(0) is 1", 1, l.getX(0), 0);
1950         assertEquals("getY(0) is 2", 2, l.getY(0), 0);
1951         Ray3d r = l.getLocation(0.0);
1952         assertEquals("heading at 0", 3, r.getPhi(), 0);
1953         assertEquals("x at 0 is 1", 1, r.getX(), 0);
1954         assertEquals("y at 0 is 2", 2, r.getY(), 0);
1955         assertEquals("bounds", new Bounds3d(l.get(0)), l.getBounds());
1956         try
1957         {
1958             l.getLocation(0.1);
1959             fail("location at position != 0 should have thrown a DrawRuntimeException");
1960         }
1961         catch (DrawRuntimeException dre)
1962         {
1963             // Ignore expected exception
1964         }
1965 
1966         try
1967         {
1968             l.getLocation(-0.1);
1969             fail("location at position != 0 should have thrown a DrawRuntimeException");
1970         }
1971         catch (DrawRuntimeException dre)
1972         {
1973             // Ignore expected exception
1974         }
1975 
1976         try
1977         {
1978             new PolyLine3d(new Point3d(1, 2, 2.5), Double.NaN, -1);
1979             fail("NaN should have thrown a DrawRuntimeException");
1980         }
1981         catch (DrawRuntimeException dre)
1982         {
1983             // Ignore expected exception
1984         }
1985 
1986         try
1987         {
1988             new PolyLine3d(new Point3d(1, 2, 2.5), 3, Double.NaN);
1989             fail("NaN should have thrown a DrawRuntimeException");
1990         }
1991         catch (DrawRuntimeException dre)
1992         {
1993             // Ignore expected exception
1994         }
1995 
1996         try
1997         {
1998             new PolyLine3d((Ray3d) null);
1999             fail("null pointer should have thrown a NullPointerException");
2000         }
2001         catch (NullPointerException npe)
2002         {
2003             // Ignore expected exception
2004         }
2005 
2006         assertEquals("closest point is the point", r, l.closestPointOnPolyLine(new Point3d(4, -2, 7)));
2007 
2008         PolyLine3d straightXisZ = new PolyLine3d(1, 2, 5, 0, 0);
2009         for (int x = -10; x <= 10; x += 1)
2010         {
2011             for (int y = -10; y <= 10; y += 1)
2012             {
2013                 for (int z = -10; z <= 10; z += 1)
2014                 {
2015                     Point3d testPoint = new Point3d(x, y, z);
2016                     assertEquals("closest point extended", r.projectOrthogonalExtended(testPoint),
2017                             l.projectOrthogonalExtended(testPoint));
2018                     assertEquals("closest point on degenerate line is the point of the degenerate line", l.getLocation(0.0),
2019                             l.closestPointOnPolyLine(testPoint));
2020                     if (z == 5)
2021                     {
2022                         assertEquals("projection on X==Z degenerate line hits", straightXisZ.get(0),
2023                                 straightXisZ.projectOrthogonal(testPoint));
2024                     }
2025                     else
2026                     {
2027                         assertNull("projection on X==Z degenerate line misses", straightXisZ.projectOrthogonal(testPoint));
2028                     }
2029                     if (x == 1 && y == 2 && z == 2.5)
2030                     {
2031                         assertEquals("NonExtended projection will return point for exact match", testPoint,
2032                                 l.projectOrthogonal(testPoint));
2033                         assertEquals("NonExtended fractional projection returns 0 for exact match", 0,
2034                                 l.projectOrthogonalFractional(testPoint), 0);
2035                         assertEquals("Extended fractional projection returns 0 for exact match", 0,
2036                                 l.projectOrthogonalFractionalExtended(testPoint), 0);
2037                     }
2038                     else
2039                     {
2040                         assertNull("For non-nice directions nonExtended projection will return null if point does not match",
2041                                 l.projectOrthogonal(testPoint));
2042                         assertTrue("For non-nice directions non-extended fractional projection will return NaN if point does "
2043                                 + "not match", Double.isNaN(l.projectOrthogonalFractional(testPoint)));
2044                         if (l.getLocation(0.0).projectOrthogonalFractional(testPoint) > 0)
2045                         {
2046                             assertTrue(
2047                                     "ProjectOrthogonalFractionalExtended returns POSITIVE_INFINITY of projection misses "
2048                                             + "along startHeading side",
2049                                     Double.POSITIVE_INFINITY == l.projectOrthogonalFractionalExtended(testPoint));
2050                         }
2051                         else
2052                         {
2053                             assertTrue(
2054                                     "ProjectOrthogonalFractionalExtended returns POSITIVE_INFINITY of projection misses "
2055                                             + ", but not along startHeading side",
2056                                     Double.NEGATIVE_INFINITY == l.projectOrthogonalFractionalExtended(testPoint));
2057                         }
2058                     }
2059                     if (z == 5)
2060                     {
2061                         assertEquals("Non-Extended projection will return point for matching X for line along X",
2062                                 straightXisZ.get(0), straightXisZ.projectOrthogonal(testPoint));
2063                     }
2064                     else
2065                     {
2066                         assertNull("Non-Extended projection will return null for non matching X for line along X",
2067                                 straightXisZ.projectOrthogonal(testPoint));
2068                     }
2069                 }
2070             }
2071         }
2072 
2073         l = new PolyLine3d(new Point3d(1, 2, 2.5), 3, -1);
2074         assertEquals("length is 0", 0, l.getLength(), 0);
2075         assertEquals("size is 1", 1, l.size());
2076         assertEquals("getX(0) is 1", 1, l.getX(0), 0);
2077         assertEquals("getY(0) is 2", 2, l.getY(0), 0);
2078         assertEquals("getZ(0) is 2.5", 2.5, l.getZ(0), 0);
2079         r = l.getLocation(0.0);
2080         assertEquals("phi at 0", 3, r.getPhi(), 0);
2081         assertEquals("theta at 0", -1, r.getTheta(), 0);
2082         assertEquals("x at 0 is 1", 1, r.getX(), 0);
2083         assertEquals("y at 0 is 2", 2, r.getY(), 0);
2084         assertEquals("z at 0 is 2.5", 2.5, r.getZ(), 0);
2085 
2086         l = new PolyLine3d(new Ray3d(1, 2, 2.5, 3, -1));
2087         assertEquals("length is 0", 0, l.getLength(), 0);
2088         assertEquals("size is 1", 1, l.size());
2089         assertEquals("getX(0) is 1", 1, l.getX(0), 0);
2090         assertEquals("getY(0) is 2", 2, l.getY(0), 0);
2091         assertEquals("getZ(0) is 2.5", 2.5, l.getZ(0), 0);
2092         r = l.getLocation(0.0);
2093         assertEquals("phi at 0", 3, r.getPhi(), 0);
2094         assertEquals("theta at 0", -1, r.getTheta(), 0);
2095         assertEquals("x at 0 is 1", 1, r.getX(), 0);
2096         assertEquals("y at 0 is 2", 2, r.getY(), 0);
2097         assertEquals("z at 0 is 2.5", 2.5, r.getZ(), 0);
2098 
2099         PolyLine3d notEqual = new PolyLine3d(1, 2, 2.5, 4, -1);
2100         assertNotEquals("Check that the equals method verifies the startPhi", l, notEqual);
2101         notEqual = new PolyLine3d(1, 2, 2.5, 3, -2);
2102         assertNotEquals("Check that the equals method verifies the startTheta", l, notEqual);
2103         
2104         assertTrue("toString contains startPhi", l.toString().contains("startPhi"));
2105         assertTrue("toString contains startTheta", l.toString().contains("startTheta"));
2106     }
2107 
2108     /**
2109      * Draw a X marker.
2110      * @param x double; x location
2111      * @param y double; y location
2112      * @return String
2113      */
2114     public static String marker(final double x, final double y)
2115     {
2116         final double markerSize = 0.05;
2117         return String.format("M%f,%f L%f,%f M%f,%f L%f,%f", x - markerSize / 2, y - markerSize / 2, x + markerSize / 2,
2118                 y + markerSize / 2, x - markerSize / 2, y + markerSize / 2, x + markerSize / 2, y - markerSize / 2);
2119     }
2120 
2121     /**
2122      * Problem with limited precision when getting location almost at end.
2123      * @throws DrawRuntimeException when that happens this test has triggered the problem
2124      */
2125     @Test
2126     public void testOTS2Problem() throws DrawRuntimeException
2127     {
2128         // Problem 1
2129         PolyLine3d line = new PolyLine3d(new Point3d(100, 0, 0), new Point3d(100.1, 0, 0));
2130         double length = line.getLength();
2131         line.getLocation(length - Math.ulp(length));
2132 
2133         // Problem 2
2134         line = new PolyLine3d(new Point3d(0, 0, 0), new Point3d(110.1, 0, 0), new Point3d(111, 0, 0));
2135         length = line.getLength();
2136         line.getLocation(length - Math.ulp(length));
2137 
2138         // Problem 3
2139         List<Point3d> list = new ArrayList<>();
2140         list.add(new Point3d(1, 2, 3));
2141         list.add(new Ray3d(2, 3, 4, 0, 0));
2142         new PolyLine3d(list);
2143     }
2144 
2145 }