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.util.ArrayList;
13  import java.util.Arrays;
14  import java.util.Iterator;
15  import java.util.List;
16  import java.util.NoSuchElementException;
17  
18  import org.djutils.draw.DrawRuntimeException;
19  import org.djutils.draw.Transform2d;
20  import org.djutils.draw.bounds.Bounds2d;
21  import org.djutils.draw.line.PolyLine.TransitionFunction;
22  import org.djutils.draw.point.Point2d;
23  import org.djutils.draw.point.Point3d;
24  import org.djutils.exceptions.Try;
25  import org.junit.Test;
26  
27  /**
28   * TestLine2d.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 PolyLine2dTest
37  {
38  
39      /**
40       * Test the constructors of PolyLine2d.
41       * @throws DrawRuntimeException on failure
42       */
43      @Test
44      public final void constructorsTest() throws DrawRuntimeException
45      {
46          double[] values = { -999, 0, 99, 9999 }; // Keep this list short; execution time grows with 6th power of length
47          Point2d[] points = new Point2d[0]; // Empty array
48          try
49          {
50              runConstructors(points);
51              fail("Should have thrown a DrawRuntimeException");
52          }
53          catch (DrawRuntimeException exception)
54          {
55              // Ignore expected exception
56          }
57          for (double x0 : values)
58          {
59              for (double y0 : values)
60              {
61                  points = new Point2d[1]; // Degenerate array holding one point
62                  points[0] = new Point2d(x0, y0);
63                  try
64                  {
65                      runConstructors(points);
66                      fail("Should have thrown a DrawRuntimeException");
67                  }
68                  catch (DrawRuntimeException exception)
69                  {
70                      // Ignore expected exception
71                  }
72                  for (double x1 : values)
73                  {
74                      for (double y1 : values)
75                      {
76                          points = new Point2d[2]; // Straight line; two points
77                          points[0] = new Point2d(x0, y0);
78                          points[1] = new Point2d(x1, y1);
79                          if (0 == points[0].distance(points[1]))
80                          {
81                              try
82                              {
83                                  runConstructors(points);
84                                  fail("Should have thrown a DrawRuntimeException");
85                              }
86                              catch (DrawRuntimeException exception)
87                              {
88                                  // Ignore expected exception
89                              }
90                          }
91                          else
92                          {
93                              runConstructors(points);
94                              for (double x2 : values)
95                              {
96                                  for (double y2 : values)
97                                  {
98                                      points = new Point2d[3]; // Line with intermediate point
99                                      points[0] = new Point2d(x0, y0);
100                                     points[1] = new Point2d(x1, y1);
101                                     points[2] = new Point2d(x2, y2);
102                                     if (0 == points[1].distance(points[2]))
103                                     {
104                                         try
105                                         {
106                                             runConstructors(points);
107                                             fail("Should have thrown a DrawRuntimeException");
108                                         }
109                                         catch (DrawRuntimeException exception)
110                                         {
111                                             // Ignore expected exception
112                                         }
113                                     }
114                                     else
115                                     {
116                                         runConstructors(points);
117                                     }
118                                 }
119                             }
120                         }
121                     }
122                 }
123             }
124         }
125     }
126 
127     /**
128      * Test all the constructors of PolyLine2d.
129      * @param points Point2d[]; array of Point2d to test with
130      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
131      */
132     private void runConstructors(final Point2d[] points) throws DrawRuntimeException
133     {
134         verifyPointsAndSegments(new PolyLine2d(points), points);
135         List<Point2d> list = new ArrayList<>();
136         for (int i = 0; i < points.length; i++)
137         {
138             list.add(points[i]);
139         }
140         PolyLine2d line = new PolyLine2d(list);
141         verifyPointsAndSegments(line, points);
142         verifyPointsAndSegments(new PolyLine2d(line.getPoints()), points);
143         assertEquals("length at index 0", 0.0, line.lengthAtIndex(0), 0);
144         double length = 0;
145         for (int i = 1; i < points.length; i++)
146         {
147             length += Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2) + Math.pow(points[i].y - points[i - 1].y, 2));
148             assertEquals("length at index", length, line.lengthAtIndex(i), 0.0001);
149         }
150         assertEquals("length", length, line.getLength(), 10 * Math.ulp(length));
151 
152         assertEquals("size", points.length, line.size());
153 
154         Bounds2d b2d = line.getBounds();
155         Bounds2d ref = new Bounds2d(points);
156         assertEquals("bounds is correct", ref, b2d);
157 
158         try
159         {
160             line.get(-1);
161             fail("Negative index should have thrown an IndexOutOfBoundsException");
162         }
163         catch (IndexOutOfBoundsException ioobe)
164         {
165             // Ignore expected exception
166         }
167 
168         try
169         {
170             line.get(line.size() + 1);
171             fail("Too large index should have thrown an IndexOutOfBoundsException");
172         }
173         catch (IndexOutOfBoundsException ioobe)
174         {
175             // Ignore expected exception
176         }
177 
178         try
179         {
180             new PolyLine2d((List<Point2d>) null);
181             fail("null list should have thrown a NullPointerException");
182         }
183         catch (NullPointerException npe)
184         {
185             // Ignore expected exception
186         }
187 
188         int horizontalMoves = 0;
189         Path2D path = new Path2D.Double();
190         path.moveTo(points[0].x, points[0].y);
191         // System.out.print("path is "); printPath2D(path);
192         for (int i = 1; i < points.length; i++)
193         {
194             // Path2D is corrupt if same point is added twice in succession
195             if (points[i].x != points[i - 1].x || points[i].y != points[i - 1].y)
196             {
197                 path.lineTo(points[i].x, points[i].y);
198                 horizontalMoves++;
199             }
200         }
201 
202         try
203         {
204             new PolyLine2d((Path2D) null);
205             fail("null path should have thrown a NullPointerException");
206         }
207         catch (NullPointerException npe)
208         {
209             // Ignore expected exception
210         }
211 
212         try
213         {
214             line = new PolyLine2d(path);
215             if (0 == horizontalMoves)
216             {
217                 fail("Construction of Line2d from path with degenerate projection should have failed");
218             }
219             assertEquals("number of points should match", horizontalMoves + 1, line.size());
220             int indexInLine = 0;
221             for (int i = 0; i < points.length; i++)
222             {
223                 if (i > 0 && (points[i].x != points[i - 1].x || points[i].y != points[i - 1].y))
224                 {
225                     indexInLine++;
226                 }
227                 assertEquals("x in line", points[i].x, line.get(indexInLine).x, 0.001);
228                 assertEquals("y in line", points[i].y, line.get(indexInLine).y, 0.001);
229             }
230         }
231         catch (DrawRuntimeException e)
232         {
233             if (0 != horizontalMoves)
234             {
235                 fail("Construction of Line2d from path with non-degenerate projection should not have failed");
236             }
237         }
238     }
239 
240     /**
241      * Test construction of a Line2d from a Path2D with SEG_CLOSE.
242      * @throws DrawRuntimeException on unexpected error
243      */
244     @Test
245     public void testPathWithClose() throws DrawRuntimeException
246     {
247         Path2D path = new Path2D.Double();
248         path.moveTo(1, 2);
249         path.lineTo(4, 5);
250         path.lineTo(4, 8);
251         path.closePath();
252         PolyLine2d line = new PolyLine2d(path);
253         assertEquals("line has 4 points", 4, line.size());
254         assertEquals("first point equals last point", line.getFirst(), line.getLast());
255         // Now the case that the path was already closed
256         path = new Path2D.Double();
257         path.moveTo(1, 2);
258         path.lineTo(4, 5);
259         path.lineTo(1, 2);
260         path.closePath();
261         line = new PolyLine2d(path);
262         assertEquals("line has 4 points", 3, line.size());
263         assertEquals("first point equals last point", line.getFirst(), line.getLast());
264         path = new Path2D.Double();
265         path.moveTo(1, 2);
266         path.lineTo(4, 5);
267         path.lineTo(4, 8);
268         path.curveTo(1, 2, 3, 4, 5, 6);
269         try
270         {
271             new PolyLine2d(path);
272             fail("unsupported SEG_CUBICTO should have thrown an exception");
273         }
274         catch (DrawRuntimeException dre)
275         {
276             // Ignore expected exception
277         }
278     }
279 
280     /**
281      * Test all constructors of a Line2d.
282      * @throws DrawRuntimeException if that happens uncaught; this test has failed
283      * @throws DrawRuntimeException if that happens uncaught; this test has failed
284      */
285     @Test
286     public void testConstructors() throws DrawRuntimeException, DrawRuntimeException
287     {
288         runConstructors(new Point2d[] { new Point2d(1.2, 3.4), new Point2d(2.3, 4.5), new Point2d(3.4, 5.6) });
289 
290         Try.testFail(new Try.Execution()
291         {
292             @Override
293             public void execute() throws Throwable
294             {
295                 new PolyLine2d(new double[] { 1, 2, 3 }, new double[] { 4, 5 });
296             }
297         }, "double arrays of unequal length should have thrown a DrawRuntimeException", DrawRuntimeException.class);
298 
299         Try.testFail(new Try.Execution()
300         {
301             @Override
302             public void execute() throws Throwable
303             {
304                 new PolyLine2d(new double[] { 1, 2 }, new double[] { 3, 4, 5 });
305             }
306         }, "double arrays of unequal length should have thrown a DrawRuntimeException", DrawRuntimeException.class);
307 
308         Try.testFail(new Try.Execution()
309         {
310             @Override
311             public void execute() throws Throwable
312             {
313                 new PolyLine2d(null, new double[] { 1, 2 });
314             }
315         }, "null double array should have thrown a NullPointerException", NullPointerException.class);
316 
317         Try.testFail(new Try.Execution()
318         {
319             @Override
320             public void execute() throws Throwable
321             {
322                 new PolyLine2d(new double[] { 1, 2 }, null);
323             }
324         }, "null double array should have thrown a NullPointerException", NullPointerException.class);
325 
326         Try.testFail(new Try.Execution()
327         {
328             @Override
329             public void execute() throws Throwable
330             {
331                 new PolyLine2d((List<Point2d>) null);
332             }
333         }, "null list should have thrown a nullPointerException", NullPointerException.class);
334 
335         List<Point2d> shortList = new ArrayList<>();
336         Try.testFail(new Try.Execution()
337         {
338             @Override
339             public void execute() throws Throwable
340             {
341                 new PolyLine2d(shortList);
342             }
343         }, "empty list should have thrown a DrawRuntimeException", DrawRuntimeException.class);
344 
345         shortList.add(new Point2d(1, 2));
346         Try.testFail(new Try.Execution()
347         {
348             @Override
349             public void execute() throws Throwable
350             {
351                 new PolyLine2d(shortList);
352             }
353         }, "one-point list should have thrown a DrawRuntimeException", DrawRuntimeException.class);
354 
355         Point2d p1 = new Point2d(1, 2);
356         Point2d p2 = new Point2d(3, 4);
357         PolyLine2d pl = new PolyLine2d(p1, p2);
358         assertEquals("two points", 2, pl.size());
359         assertEquals("p1", p1, pl.get(0));
360         assertEquals("p2", p2, pl.get(1));
361 
362         pl = new PolyLine2d(p1, p2, (Point2d[]) null);
363         assertEquals("two points", 2, pl.size());
364         assertEquals("p1", p1, pl.get(0));
365         assertEquals("p2", p2, pl.get(1));
366 
367         pl = new PolyLine2d(p1, p2, new Point2d[0]);
368         assertEquals("two points", 2, pl.size());
369         assertEquals("p1", p1, pl.get(0));
370         assertEquals("p2", p2, pl.get(1));
371 
372         Try.testFail(new Try.Execution()
373         {
374             @Override
375             public void execute() throws Throwable
376             {
377                 new PolyLine2d(new Point2d[] {});
378             }
379         }, "empty array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
380 
381         Try.testFail(new Try.Execution()
382         {
383             @Override
384             public void execute() throws Throwable
385             {
386                 new PolyLine2d(new Point2d[] { new Point2d(1, 2) });
387             }
388         }, "single point should have thrown a DrawRuntimeException", DrawRuntimeException.class);
389 
390         Try.testFail(new Try.Execution()
391         {
392             @Override
393             public void execute() throws Throwable
394             {
395                 new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(1, 2) });
396             }
397         }, "duplicate point should have thrown a DrawRuntimeException", DrawRuntimeException.class);
398 
399         Try.testFail(new Try.Execution()
400         {
401             @Override
402             public void execute() throws Throwable
403             {
404                 new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4) });
405             }
406         }, "duplicate point should have thrown a DrawRuntimeException", DrawRuntimeException.class);
407 
408         Try.testFail(new Try.Execution()
409         {
410             @Override
411             public void execute() throws Throwable
412             {
413                 new PolyLine2d(new Point2d[] { new Point2d(-1, -2), new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4) });
414             }
415         }, "duplicate point should have thrown a DrawRuntimeException", DrawRuntimeException.class);
416     }
417 
418     /**
419      * Test the other methods of PolyLine2d.
420      * @throws DrawRuntimeException if that happens uncaught; this test has failed
421      * @throws NullPointerException if that happens uncaught; this test has failed
422      */
423     @SuppressWarnings("unlikely-arg-type")
424     @Test
425     public void testOtherMethods() throws NullPointerException, DrawRuntimeException
426     {
427         Point2d[] array = new Point2d[] { new Point2d(1, 2), new Point2d(3, 4), new Point2d(3.2, 4.1), new Point2d(5, 6) };
428         PolyLine2d line = new PolyLine2d(Arrays.stream(array).iterator());
429         assertEquals("size", array.length, line.size());
430         for (int i = 0; i < array.length; i++)
431         {
432             assertEquals("i-th point", array[i], line.get(i));
433         }
434         int nextIndex = 0;
435         for (Iterator<Point2d> iterator = line.getPoints(); iterator.hasNext();)
436         {
437             assertEquals("i-th point from line iterator", array[nextIndex++], iterator.next());
438         }
439         assertEquals("iterator returned all points", array.length, nextIndex);
440 
441         PolyLine2d filtered = line.noiseFilteredLine(0.0);
442         assertEquals("filtered with 0 tolerance returns line", line, filtered);
443         filtered = line.noiseFilteredLine(0.01);
444         assertEquals("filtered with very low tolerance returns line", line, filtered);
445         filtered = line.noiseFilteredLine(0.5);
446         assertEquals("size of filtered line is 3", 3, filtered.size());
447         assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
448         assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
449         assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
450         filtered = line.noiseFilteredLine(10);
451         assertEquals("size of filtered line is 2", 2, filtered.size());
452         assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
453         assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
454 
455         array = new Point2d[] { new Point2d(1, 2), new Point2d(3, 4), new Point2d(3.2, 4.1), new Point2d(1, 2) };
456         line = new PolyLine2d(Arrays.stream(array).iterator());
457         filtered = line.noiseFilteredLine(10);
458         assertEquals("size of filtered line is 3", 3, filtered.size());
459         assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
460         assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
461         assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
462 
463         array = new Point2d[] { new Point2d(1, 2), new Point2d(3, 4), new Point2d(1.1, 2.1), new Point2d(1, 2) };
464         line = new PolyLine2d(Arrays.stream(array).iterator());
465         filtered = line.noiseFilteredLine(0.5);
466         assertEquals("size of filtered line is 3", 3, filtered.size());
467         assertEquals("first point of filtered line matches", line.getFirst(), filtered.getFirst());
468         assertEquals("last point of filtered line matches", line.getLast(), filtered.getLast());
469         assertEquals("mid point of filtered line is point 1 of unfiltered line", line.get(1), filtered.get(1));
470 
471         array = new Point2d[] { new Point2d(1, 2), new Point2d(3, 4) };
472         line = new PolyLine2d(Arrays.stream(array).iterator());
473         filtered = line.noiseFilteredLine(10);
474         assertEquals("Filtering a two-point line returns that line", line, filtered);
475 
476         array = new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4) };
477         line = new PolyLine2d(true, array);
478         assertEquals("cleaned line has 2 points", 2, line.size());
479         assertEquals("first point", array[0], line.getFirst());
480         assertEquals("last point", array[array.length - 1], line.getLast());
481 
482         array = new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4), new Point2d(3, 4) };
483         line = new PolyLine2d(true, array);
484         assertEquals("cleaned line has 2 points", 2, line.size());
485         assertEquals("first point", array[0], line.getFirst());
486         assertEquals("last point", array[array.length - 1], line.getLast());
487 
488         array = new Point2d[] { new Point2d(0, -1), new Point2d(1, 2), new Point2d(1, 2), new Point2d(3, 4) };
489         line = new PolyLine2d(true, array);
490         assertEquals("cleaned line has 2 points", 3, line.size());
491         assertEquals("first point", array[0], line.getFirst());
492         assertEquals("last point", array[array.length - 1], line.getLast());
493 
494         array = new Point2d[] { new Point2d(0, -1), new Point2d(1, 2), new Point2d(1, 2), new Point2d(1, 2),
495                 new Point2d(3, 4) };
496         line = new PolyLine2d(true, array);
497         assertEquals("cleaned line has 3 points", 3, line.size());
498         assertEquals("first point", array[0], line.getFirst());
499         assertEquals("mid point", array[1], line.get(1));
500         assertEquals("last point", array[array.length - 1], line.getLast());
501 
502         Try.testFail(new Try.Execution()
503         {
504             @Override
505             public void execute() throws Throwable
506             {
507                 new PolyLine2d(true, new Point2d[0]);
508             }
509         }, "Too short array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
510 
511         Try.testFail(new Try.Execution()
512         {
513             @Override
514             public void execute() throws Throwable
515             {
516                 new PolyLine2d(true, new Point2d[] { new Point2d(1, 2) });
517             }
518         }, "Too short array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
519 
520         Try.testFail(new Try.Execution()
521         {
522             @Override
523             public void execute() throws Throwable
524             {
525                 new PolyLine2d(true, new Point2d[] { new Point2d(1, 2), new Point2d(1, 2) });
526             }
527         }, "All duplicate points in array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
528 
529         Try.testFail(new Try.Execution()
530         {
531             @Override
532             public void execute() throws Throwable
533             {
534                 new PolyLine2d(true, new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(1, 2) });
535             }
536         }, "All duplicate points in array should have thrown a DrawRuntimeException", DrawRuntimeException.class);
537 
538         array = new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) };
539         line = new PolyLine2d(array);
540 
541         try
542         {
543             line.getLocation(-0.1);
544             fail("negative location should have thrown a DrawRuntimeException");
545         }
546         catch (DrawRuntimeException dre)
547         {
548             // Ignore expected exception
549         }
550 
551         double length = line.getLength();
552         assertEquals("Length of line is 10", 10, length, 0.000001);
553 
554         try
555         {
556             line.getLocation(length + 0.1);
557             fail("location beyond length should have thrown a DrawRuntimeException");
558         }
559         catch (DrawRuntimeException dre)
560         {
561             // Ignore expected exception
562         }
563 
564         try
565         {
566             line.getLocation(-0.1);
567             fail("negative location should have thrown a DrawRuntimeException");
568         }
569         catch (DrawRuntimeException dre)
570         {
571             // Ignore expected exception
572         }
573 
574         assertEquals("Length of line is 10", 10, length, 0.000001);
575 
576         try
577         {
578             line.getLocationFraction(1.1);
579             fail("location beyond length should have thrown a DrawRuntimeException");
580         }
581         catch (DrawRuntimeException de)
582         {
583             // Ignore expected exception
584         }
585 
586         try
587         {
588             line.getLocationFraction(-0.1);
589             fail("negative location should have thrown a DrawRuntimeException");
590         }
591         catch (DrawRuntimeException dre)
592         {
593             // Ignore expected exception
594         }
595 
596         for (double position : new double[] { -1, 0, 2.5, 4.9, 5.1, 7.5, 9.9, 10, 11 })
597         {
598             Ray2d ray = line.getLocationExtended(position);
599             if (position < 5)
600             {
601                 Ray2d expected = new Ray2d(array[0].interpolate(array[1], position / 5), Math.atan2(4, 3));
602                 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
603             }
604             else
605             {
606                 Ray2d expected = new Ray2d(array[1].interpolate(array[2], (position - 5) / 5), Math.atan2(3, 4));
607                 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
608             }
609             ray = line.getLocationFractionExtended(position / line.getLength());
610             if (position < 5)
611             {
612                 Ray2d expected = new Ray2d(array[0].interpolate(array[1], position / 5), Math.atan2(4, 3));
613                 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
614             }
615             else
616             {
617                 Ray2d expected = new Ray2d(array[1].interpolate(array[2], (position - 5) / 5), Math.atan2(3, 4));
618                 assertTrue("interpolated/extrapolated point", expected.epsilonEquals(ray, 0.0001, 0.00001));
619             }
620         }
621 
622         // Test the projectOrthogonal methods
623         array = new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) };
624         line = new PolyLine2d(array);
625         // System.out.println(line.toPlot());
626         for (double x = -15; x <= 20; x++)
627         {
628             for (double y = -15; y <= 20; y++)
629             {
630                 Point2d xy = new Point2d(x, y);
631                 // System.out.println("x=" + x + ", y=" + y);
632                 double result = line.projectOrthogonalFractional(xy);
633                 if (!Double.isNaN(result))
634                 {
635                     assertTrue("result must be >= 0.0", result >= 0);
636                     assertTrue("result must be <= 1.0", result <= 1.0);
637                     Ray2d ray = line.getLocationFraction(result);
638                     Point2d projected = line.projectOrthogonal(xy);
639                     assertEquals("if fraction is between 0 and 1; projectOrthogonal yiels point at that fraction", ray.x,
640                             projected.x, 00001);
641                     assertEquals("if fraction is between 0 and 1; projectOrthogonal yiels point at that fraction", ray.y,
642                             projected.y, 00001);
643                 }
644                 else
645                 {
646                     assertNull("point projects outside line", line.projectOrthogonal(xy));
647                 }
648                 result = line.projectOrthogonalFractionalExtended(xy);
649                 if (!Double.isNaN(result))
650                 {
651                     Point2d resultPoint = line.getLocationFractionExtended(result);
652                     if (result >= 0.0 && result <= 1.0)
653                     {
654                         Point2d closestPointOnLine = line.closestPointOnPolyLine(xy);
655                         assertEquals("resultPoint is equal to closestPoint", resultPoint, closestPointOnLine);
656                         assertEquals("getLocationFraction returns same as getLocationfractionExtended", resultPoint,
657                                 line.getLocationFraction(result));
658                     }
659                     else
660                     {
661                         try
662                         {
663                             line.getLocationFraction(result);
664                             fail("illegal fraction should have thrown a DrawRuntimeException");
665                         }
666                         catch (DrawRuntimeException dre)
667                         {
668                             // Ignore expected exception
669                         }
670                         if (result < 0)
671                         {
672                             assertEquals("resultPoint lies on extention of start segment",
673                                     resultPoint.distance(line.get(1)) - resultPoint.distance(line.getFirst()),
674                                     line.getFirst().distance(line.get(1)), 0.0001);
675                         }
676                         else
677                         {
678                             // result > 1
679                             assertEquals("resultPoint lies on extention of end segment",
680                                     resultPoint.distance(line.get(line.size() - 2)) - resultPoint.distance(line.getLast()),
681                                     line.getLast().distance(line.get(line.size() - 2)), 0.0001);
682                         }
683                     }
684                 }
685                 else
686                 {
687                     assertNull("point projects outside extended line", line.projectOrthogonalExtended(xy));
688                     Point2d closestPointOnLine = line.closestPointOnPolyLine(xy);
689                     assertNotNull("closest point is never null", closestPointOnLine);
690                     boolean found = false;
691                     for (int index = 0; index < line.size(); index++)
692                     {
693                         Point2d linePoint = line.get(index);
694                         if (linePoint.x == closestPointOnLine.x && linePoint.y == closestPointOnLine.y)
695                         {
696                             found = true;
697                         }
698                     }
699                     assertTrue("closestPointOnLine is one of the construction points of the line", found);
700                 }
701                 Point2d closestPointOnLine = line.closestPointOnPolyLine(xy);
702                 assertNotNull("closest point is never null", closestPointOnLine);
703             }
704         }
705         Point2d toleranceResultPoint = line.getLocationFraction(-0.01, 0.01);
706         assertEquals("tolerance result matches extended fraction result", line.getLocationFraction(0), toleranceResultPoint);
707         toleranceResultPoint = line.getLocationFraction(1.01, 0.01);
708         assertEquals("tolerance result matches extended fraction result", line.getLocationFraction(1), toleranceResultPoint);
709 
710         try
711         {
712             line.getLocationFraction(-.011, 0.01);
713             fail("fraction outside tolerance should have thrown a DrawRuntimeException");
714         }
715         catch (DrawRuntimeException dre)
716         {
717             // Ignore expected exception
718         }
719 
720         try
721         {
722             line.getLocationFraction(1.011, 0.01);
723             fail("fraction outside tolerance should have thrown a DrawRuntimeException");
724         }
725         catch (DrawRuntimeException dre)
726         {
727             // Ignore expected exception
728         }
729 
730         // Test the extract and truncate methods
731         array = new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) };
732         line = new PolyLine2d(array);
733         length = line.getLength();
734         for (double to : new double[] { -10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20 })
735         {
736             if (to <= 0 || to > length)
737             {
738                 try
739                 {
740                     line.truncate(to);
741                     fail("illegal truncate should have thrown a DrawRuntimeException");
742                 }
743                 catch (DrawRuntimeException dre)
744                 {
745                     // Ignore expected exception
746                 }
747             }
748             else
749             {
750                 PolyLine2d truncated = line.truncate(to);
751                 assertEquals("truncated line start with start point of line", line.getFirst(), truncated.getFirst());
752                 assertEquals("Length of truncated line is truncate position", to, truncated.getLength(), 0.0001);
753             }
754             for (double from : new double[] { -10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20 })
755             {
756                 if (from >= to || from < 0 || to > length)
757                 {
758                     try
759                     {
760                         line.extract(from, to);
761                         fail("Illegal range should have thrown a DrawRuntimeException");
762                     }
763                     catch (DrawRuntimeException dre)
764                     {
765                         // Ignore expected exception
766                     }
767                 }
768                 else
769                 {
770                     PolyLine2d fragment = line.extract(from, to);
771                     Point2d fromPoint = line.getLocation(from);
772                     assertTrue("fragment starts at from", fromPoint.epsilonEquals(fragment.getFirst(), 0.00001));
773                     Point2d toPoint = line.getLocation(to);
774                     assertTrue("fragment ends at to", toPoint.epsilonEquals(fragment.getLast(), 0.00001));
775                     assertEquals("Length of fragment", to - from, fragment.getLength(), 0.0001);
776                     if (from == 0)
777                     {
778                         assertEquals("fragment starts at begin of line", line.getFirst(), fragment.getFirst());
779                     }
780                     if (to == length)
781                     {
782                         assertEquals("fragment ends at end of line", line.getLast(), fragment.getLast());
783                     }
784                 }
785             }
786         }
787         try
788         {
789             line.extract(Double.NaN, 10.0);
790             fail("NaN value should have thrown a DrawRuntimeException");
791         }
792         catch (DrawRuntimeException dre)
793         {
794             // Ignore expected exception
795         }
796 
797         try
798         {
799             line.extract(0.0, Double.NaN);
800             fail("NaN value should have thrown a DrawRuntimeException");
801         }
802         catch (DrawRuntimeException dre)
803         {
804             // Ignore expected exception
805         }
806 
807         // Verify that hashCode. Check that the result depends on the actual coordinates.
808         assertNotEquals("hash code takes x coordinate of first point into account",
809                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
810                 new PolyLine2d(new Point2d(1, 0), new Point2d(1, 1)).hashCode());
811         assertNotEquals("hash code takes y coordinate of first point into account",
812                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
813                 new PolyLine2d(new Point2d(0, 1), new Point2d(1, 1)).hashCode());
814         assertNotEquals("hash code takes x coordinate of second point into account",
815                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
816                 new PolyLine2d(new Point2d(0, 0), new Point2d(2, 1)).hashCode());
817         assertNotEquals("hash code takes y coordinate of second point into account",
818                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
819                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 2)).hashCode());
820 
821         // Verify the equals method.
822         assertTrue("line is equal to itself", line.equals(line));
823         assertFalse("line is not equal to a different line",
824                 line.equals(new PolyLine2d(new Point2d(123, 456), new Point2d(789, 101112))));
825         assertFalse("line is not equal to null", line.equals(null));
826         assertFalse("line is not equal to a different kind of object", line.equals("unlikely"));
827         assertTrue("Line is equal to line from same set of points", line.equals(new PolyLine2d(line.getPoints())));
828         // Make a line that differs only in the very last point
829         Point2d[] otherArray = Arrays.copyOf(array, array.length);
830         otherArray[otherArray.length - 1] =
831                 new Point2d(otherArray[otherArray.length - 1].x, otherArray[otherArray.length - 1].y + 5);
832         PolyLine2d other = new PolyLine2d(otherArray);
833         assertFalse("PolyLine2d that differs in y of last point is different", line.equals(other));
834     }
835 
836     /**
837      * Test the concatenate method.
838      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
839      */
840     @Test
841     public final void concatenateTest() throws DrawRuntimeException
842     {
843         Point2d p0 = new Point2d(1.1, 2.2);
844         Point2d p1 = new Point2d(2.1, 2.2);
845         Point2d p2 = new Point2d(3.1, 2.2);
846         Point2d p3 = new Point2d(4.1, 2.2);
847         Point2d p4 = new Point2d(5.1, 2.2);
848         Point2d p5 = new Point2d(6.1, 2.2);
849 
850         PolyLine2d l0 = new PolyLine2d(p0, p1, p2);
851         PolyLine2d l1 = new PolyLine2d(p2, p3);
852         PolyLine2d l2 = new PolyLine2d(p3, p4, p5);
853         PolyLine2d ll = PolyLine2d.concatenate(l0, l1, l2);
854         assertEquals("size is 6", 6, ll.size());
855         assertEquals("point 0 is p0", p0, ll.get(0));
856         assertEquals("point 1 is p1", p1, ll.get(1));
857         assertEquals("point 2 is p2", p2, ll.get(2));
858         assertEquals("point 3 is p3", p3, ll.get(3));
859         assertEquals("point 4 is p4", p4, ll.get(4));
860         assertEquals("point 5 is p5", p5, ll.get(5));
861 
862         ll = PolyLine2d.concatenate(l1);
863         assertEquals("size is 2", 2, ll.size());
864         assertEquals("point 0 is p2", p2, ll.get(0));
865         assertEquals("point 1 is p3", p3, ll.get(1));
866 
867         try
868         {
869             PolyLine2d.concatenate(l0, l2);
870             fail("Gap should have throw a DrawRuntimeException");
871         }
872         catch (DrawRuntimeException dre)
873         {
874             // Ignore expected exception
875         }
876         try
877         {
878             PolyLine2d.concatenate();
879             fail("concatenate of empty list should have thrown a DrawRuntimeException");
880         }
881         catch (DrawRuntimeException dre)
882         {
883             // Ignore expected exception
884         }
885 
886         // Test concatenate methods with tolerance
887         PolyLine2d thirdLine = new PolyLine2d(p4, p5);
888         for (double tolerance : new double[] { 0.1, 0.01, 0.001, 0.0001, 0.00001 })
889         {
890             for (double actualError : new double[] { tolerance * 0.9, tolerance * 1.1 })
891             {
892                 int maxDirection = 10;
893                 for (int direction = 0; direction < maxDirection; direction++)
894                 {
895                     double dx = actualError * Math.cos(Math.PI * 2 * direction / maxDirection);
896                     double dy = actualError * Math.sin(Math.PI * 2 * direction / maxDirection);
897                     PolyLine2d otherLine = new PolyLine2d(new Point2d(p2.x + dx, p2.y + dy), p3, p4);
898                     if (actualError < tolerance)
899                     {
900                         try
901                         {
902                             PolyLine2d.concatenate(tolerance, l0, otherLine);
903                         }
904                         catch (DrawRuntimeException dre)
905                         {
906                             PolyLine2d.concatenate(tolerance, l0, otherLine);
907                             fail("concatenation with error " + actualError + " and tolerance " + tolerance
908                                     + " should not have failed");
909                         }
910                         try
911                         {
912                             PolyLine2d.concatenate(tolerance, l0, otherLine, thirdLine);
913                         }
914                         catch (DrawRuntimeException dre)
915                         {
916                             fail("concatenation with error " + actualError + " and tolerance " + tolerance
917                                     + " should not have failed");
918                         }
919                     }
920                     else
921                     {
922                         try
923                         {
924                             PolyLine2d.concatenate(tolerance, l0, otherLine);
925                         }
926                         catch (DrawRuntimeException dre)
927                         {
928                             // Ignore expected exception
929                         }
930                         try
931                         {
932                             PolyLine2d.concatenate(tolerance, l0, otherLine, thirdLine);
933                         }
934                         catch (DrawRuntimeException dre)
935                         {
936                             // Ignore expected exception
937                         }
938                     }
939                 }
940             }
941         }
942     }
943 
944     /**
945      * Test the offsetLine methods.
946      * @throws DrawRuntimeException when that happens uncaught; this test has failed
947      */
948     @Test
949     public void testOffsetLine() throws DrawRuntimeException
950     {
951         for (Point2d[] points : new Point2d[][] { { new Point2d(1, 2), new Point2d(3, 50) },
952                 { new Point2d(-40, -20), new Point2d(5, -2), new Point2d(3, 50) },
953                 { new Point2d(-40, -20), new Point2d(5, -2), new Point2d(3, -50) } })
954         {
955             for (double angle = 0; angle < 2 * Math.PI; angle += Math.PI / 360)
956             {
957                 Transform2d transform = new Transform2d().rotation(angle);
958                 Point2d[] transformed = new Point2d[points.length];
959                 for (int index = 0; index < points.length; index++)
960                 {
961                     transformed[index] = transform.transform(points[index]);
962                 }
963                 final PolyLine2d line = new PolyLine2d(transformed);
964                 // System.out.println("angle " + Math.toDegrees(angle) + " line " + line);
965                 Try.testFail(new Try.Execution()
966                 {
967                     @Override
968                     public void execute() throws Throwable
969                     {
970                         line.offsetLine(Double.NaN);
971                     }
972                 }, "NaN offset should have thrown an IllegalArgumentException", IllegalArgumentException.class);
973 
974                 assertEquals("offset 0 yields the reference line", line, line.offsetLine(0));
975                 // System.out.print("reference line " + line.toPlot());
976                 for (double offset : new double[] { 1, 10, 0.1, -0.1, -10 })
977                 {
978                     PolyLine2d offsetLine = line.offsetLine(offset);
979                     // System.out.print("angle " + angle + ", offset " + offset + ": " + offsetLine.toPlot());
980                     if (points.length == 2)
981                     {
982                         assertEquals("two-point line should have a two-point offset line", 2, offsetLine.size());
983                         assertEquals("length of offset line of two-point reference line equals length of reference line",
984                                 line.getLength(), offsetLine.getLength(), 0.01);
985                     }
986                     assertEquals("offset at start", Math.abs(offset), line.getFirst().distance(offsetLine.getFirst()), 0.01);
987                     assertEquals("offset at end", Math.abs(offset), line.getLast().distance(offsetLine.getLast()), 0.01);
988                     // Verify that negative offset works in the direction opposite to positive
989                     assertEquals("offset to the left vs to the right differs by twice the offset", Math.abs(2 * offset),
990                             offsetLine.getFirst().distance(line.offsetLine(-offset).getFirst()), 0.001);
991                     // The following four may be false if the offset is not small comparable to the lenght of the first or last
992                     // segment of the line
993                     assertEquals("projection of first point of line onto offset line is (almost) first point of offset line", 0,
994                             offsetLine.getLocationExtended(
995                                     offsetLine.projectOrthogonalFractionalExtended(line.getFirst()) * offsetLine.getLength())
996                                     .distance(offsetLine.getFirst()),
997                             0.01);
998                     double fraction = offsetLine.projectOrthogonalFractionalExtended(line.getLast());
999                     assertEquals("fraction should be 1 with maximum error a few ULP", 1, fraction, 0.000001);
1000                     if (fraction > 1.0)
1001                     {
1002                         fraction = 1.0;
1003                     }
1004                     assertEquals("projection of last point of line onto offset line is (almost) last point of offset line", 0,
1005                             offsetLine.getLocation(fraction * offsetLine.getLength()).distance(offsetLine.getLast()), 0.01);
1006                     assertEquals("projection of first point of offset line onto line is (almost) first point of line", 0,
1007                             line.getLocationExtended(
1008                                     line.projectOrthogonalFractionalExtended(offsetLine.getFirst()) * line.getLength())
1009                                     .distance(line.getFirst()),
1010                             0.01);
1011                     fraction = line.projectOrthogonalFractionalExtended(offsetLine.getLast());
1012                     assertEquals("fraction should be 1 with maximum error a few ULP", 1, fraction, 0.000001);
1013                     if (fraction > 1.0)
1014                     {
1015                         fraction = 1.0;
1016                     }
1017                     assertEquals("projection of last point of offset line onto line is (almost) last point of line", 0,
1018                             line.getLocation(fraction * line.getLength()).distance(line.getLast()), 0.01);
1019                 }
1020             }
1021         }
1022 
1023         final PolyLine2d line = new PolyLine2d(new Point2d(1, 2), new Point2d(3, 4));
1024         Try.testFail(new Try.Execution()
1025         {
1026             @Override
1027             public void execute() throws Throwable
1028             {
1029                 line.offsetLine(1, 0, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1030                         PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO,
1031                         PolyLine.DEFAULT_OFFSET_PRECISION);
1032             }
1033         }, "zero circle precision should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1034 
1035         Try.testFail(new Try.Execution()
1036         {
1037             @Override
1038             public void execute() throws Throwable
1039             {
1040                 line.offsetLine(1, Double.NaN, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1041                         PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO,
1042                         PolyLine.DEFAULT_OFFSET_PRECISION);
1043             }
1044         }, "NaN circle precision should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1045 
1046         Try.testFail(new Try.Execution()
1047         {
1048             @Override
1049             public void execute() throws Throwable
1050             {
1051                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, 0, PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE,
1052                         PolyLine.DEFAULT_OFFSET_FILTER_RATIO, PolyLine.DEFAULT_OFFSET_PRECISION);
1053             }
1054         }, "zero offsetMinimumFilterValue should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1055 
1056         Try.testFail(new Try.Execution()
1057         {
1058             @Override
1059             public void execute() throws Throwable
1060             {
1061                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, Double.NaN, PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE,
1062                         PolyLine.DEFAULT_OFFSET_FILTER_RATIO, PolyLine.DEFAULT_OFFSET_PRECISION);
1063             }
1064         }, "NaN offsetMinimumFilterValue should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1065 
1066         Try.testFail(new Try.Execution()
1067         {
1068             @Override
1069             public void execute() throws Throwable
1070             {
1071                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE,
1072                         PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO,
1073                         PolyLine.DEFAULT_OFFSET_PRECISION);
1074             }
1075         }, "offsetMinimumFilterValue not less than offsetMaximumFilterValue should have thrown an IllegalArgumentException",
1076                 IllegalArgumentException.class);
1077 
1078         Try.testFail(new Try.Execution()
1079         {
1080             @Override
1081             public void execute() throws Throwable
1082             {
1083                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE, 0,
1084                         PolyLine.DEFAULT_OFFSET_FILTER_RATIO, PolyLine.DEFAULT_OFFSET_PRECISION);
1085             }
1086         }, "zero offsetMaximumfilterValue should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1087 
1088         Try.testFail(new Try.Execution()
1089         {
1090             @Override
1091             public void execute() throws Throwable
1092             {
1093                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE, Double.NaN,
1094                         PolyLine.DEFAULT_OFFSET_FILTER_RATIO, PolyLine.DEFAULT_OFFSET_PRECISION);
1095             }
1096         }, "NaN offsetMaximumfilterValue should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1097 
1098         Try.testFail(new Try.Execution()
1099         {
1100             @Override
1101             public void execute() throws Throwable
1102             {
1103                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1104                         PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, 0, PolyLine.DEFAULT_OFFSET_PRECISION);
1105             }
1106         }, "zero offsetFilterRatio should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1107 
1108         Try.testFail(new Try.Execution()
1109         {
1110             @Override
1111             public void execute() throws Throwable
1112             {
1113                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1114                         PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, Double.NaN, PolyLine.DEFAULT_OFFSET_PRECISION);
1115             }
1116         }, "NaN offsetFilterRatio should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1117 
1118         Try.testFail(new Try.Execution()
1119         {
1120             @Override
1121             public void execute() throws Throwable
1122             {
1123                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1124                         PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO, 0);
1125             }
1126         }, "zero offsetPrecision should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1127 
1128         Try.testFail(new Try.Execution()
1129         {
1130             @Override
1131             public void execute() throws Throwable
1132             {
1133                 line.offsetLine(1, PolyLine.DEFAULT_CIRCLE_PRECISION, PolyLine.DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
1134                         PolyLine.DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, PolyLine.DEFAULT_OFFSET_FILTER_RATIO, Double.NaN);
1135             }
1136         }, "NaN offsetPrecision should have thrown an IllegalArgumentException", IllegalArgumentException.class);
1137     }
1138 
1139     /**
1140      * Test the projectRay method.
1141      * @throws DrawRuntimeException cannot happen
1142      */
1143     @Test
1144     public void testProjectRayTransition() throws DrawRuntimeException
1145     {
1146         List<Point2d> innerDesignLinePoints = new ArrayList<>();
1147         List<Point2d> outerDesignLinePoints = new ArrayList<>();
1148         // Approximate a quarter circle with radius 5
1149         double innerRadius = 5;
1150         // Approximate a quarter circle with radius 8
1151         double outerRadius = 8;
1152         for (int degree = 0; degree <= 90; degree++)
1153         {
1154             innerDesignLinePoints.add(new Point2d(innerRadius * Math.sin(Math.toRadians(degree)),
1155                     innerRadius * Math.cos(Math.toRadians(degree))));
1156             outerDesignLinePoints.add(new Point2d(outerRadius * Math.sin(Math.toRadians(degree)),
1157                     outerRadius * Math.cos(Math.toRadians(degree))));
1158         }
1159         PolyLine2d innerDesignLine = new PolyLine2d(innerDesignLinePoints);
1160         PolyLine2d outerDesignLine = new PolyLine2d(outerDesignLinePoints);
1161         List<Point2d> transitionLinePoints = new ArrayList<>();
1162         int degree = 0;
1163         Point2d prevPoint = innerDesignLinePoints.get(0);
1164         while (degree < 10)
1165         {
1166             double x = innerRadius * Math.sin(Math.toRadians(degree));
1167             double y = innerRadius * Math.cos(Math.toRadians(degree));
1168             double direction = prevPoint.directionTo(new Point2d(x, y));
1169             Ray2d ray = new Ray2d(x, y, direction);
1170             transitionLinePoints.add(ray);
1171             prevPoint = ray;
1172             degree++;
1173         }
1174         while (degree <= 80)
1175         {
1176             double phase = Math.PI * (degree - 10) / 70;
1177             double radius = innerRadius + (outerRadius - innerRadius) * (1 - Math.cos(phase) / 2 - 0.5);
1178             double x = radius * Math.sin(Math.toRadians(degree));
1179             double y = radius * Math.cos(Math.toRadians(degree));
1180             double direction = prevPoint.directionTo(new Point2d(x, y));
1181             Ray2d ray = new Ray2d(x, y, direction);
1182             transitionLinePoints.add(ray);
1183             prevPoint = ray;
1184             degree++;
1185         }
1186         while (degree < 90)
1187         {
1188             double x = outerRadius * Math.sin(Math.toRadians(degree));
1189             double y = outerRadius * Math.cos(Math.toRadians(degree));
1190             double direction = prevPoint.directionTo(new Point2d(x, y));
1191             Ray2d ray = new Ray2d(x, y, direction);
1192             transitionLinePoints.add(ray);
1193             prevPoint = ray;
1194             degree++;
1195         }
1196         PolyLine2d transitionLine = new PolyLine2d(transitionLinePoints);
1197         // System.out.print("inner design line: " + innerDesignLine.toPlot());
1198         // System.out.print("outer design line: " + outerDesignLine.toPlot());
1199         // System.out.print("transition line: " + transitionLine.toPlot());
1200         List<Point2d> projections = new ArrayList<>();
1201         for (Iterator<Point2d> iterator = transitionLine.getPoints(); iterator.hasNext();)
1202         {
1203             Point2d p = iterator.next();
1204             if (p instanceof Ray2d)
1205             {
1206                 Ray2d ray = (Ray2d) p;
1207                 Point2d transitionLinePoint = new Point2d(ray.x, ray.y);
1208                 projections.add(transitionLinePoint);
1209                 double location = innerDesignLine.projectRay(ray);
1210                 if (!Double.isNaN(location))
1211                 {
1212                     Point2d projection = innerDesignLine.getLocation(location);
1213                     projections.add(new Point2d(projection.x, projection.y));
1214                     projections.add(transitionLinePoint);
1215                 }
1216                 location = outerDesignLine.projectRay(ray);
1217                 if (!Double.isNaN(location))
1218                 {
1219                     Point2d projection = outerDesignLine.getLocation(location);
1220                     projections.add(new Point2d(projection.x, projection.y));
1221                     projections.add(transitionLinePoint);
1222                 }
1223             }
1224         }
1225         // System.out.print("cosine projections: " + PolyLine2d.createAndCleanPolyLine2d(projections).toPlot());
1226         Ray2d from = new Ray2d(outerDesignLine.get(10).x, outerDesignLine.get(10).y,
1227                 outerDesignLine.get(10).directionTo(outerDesignLine.get(11)));
1228         Ray2d to = new Ray2d(innerDesignLine.get(80).x, innerDesignLine.get(80).y,
1229                 innerDesignLine.get(80).directionTo(innerDesignLine.get(81)));
1230         transitionLine = Bezier.cubic(from, to);
1231         // System.out.print("Bezier: " + transitionLine.toPlot());
1232         projections = new ArrayList<>();
1233         Point2d prev = null;
1234         for (Iterator<Point2d> iterator = transitionLine.getPoints(); iterator.hasNext();)
1235         {
1236             Point2d p = iterator.next();
1237             if (prev != null)
1238             {
1239                 Ray2d ray = new Ray2d(prev, prev.directionTo(p));
1240                 Point2d transitionLinePoint = new Point2d(ray.x, ray.y);
1241                 projections.add(transitionLinePoint);
1242                 double location = innerDesignLine.projectRay(ray);
1243                 if (!Double.isNaN(location))
1244                 {
1245                     innerDesignLine.getLocation(location);
1246                     Point2d projection = innerDesignLine.getLocation(location);
1247                     projections.add(new Point2d(projection.x, projection.y));
1248                     projections.add(transitionLinePoint);
1249                 }
1250                 location = outerDesignLine.projectRay(ray);
1251                 if (!Double.isNaN(location))
1252                 {
1253                     outerDesignLine.getLocation(location);
1254                     Point2d projection = outerDesignLine.getLocation(location);
1255                     projections.add(new Point2d(projection.x, projection.y));
1256                     projections.add(transitionLinePoint);
1257                 }
1258             }
1259             prev = p;
1260         }
1261         // System.out.print("Bezier projections: " + PolyLine2d.createAndCleanPolyLine2d(projections).toPlot());
1262     }
1263 
1264     /**
1265      * Test the projectRay method.
1266      */
1267     @Test
1268     public void testProjectRay()
1269     {
1270         PolyLine2d reference = new PolyLine2d(new Point2d(0, 1), new Point2d(5, 1), new Point2d(10, 6), new Point2d(20, 6));
1271         // System.out.print("reference line is " + reference.toPlot());
1272         PolyLine2d offsetLine = reference.offsetLine(-10);
1273         // Now we have a line with a somewhat smooth 45 degree curve around (5, 1) with radius 10
1274         // System.out.print("offset line is " + offsetLine.toPlot());
1275         double slope = 0.25;
1276         double slopeAngle = Math.atan2(slope, 1);
1277         double prevProjection = -100;
1278         List<Point2d> projections = new ArrayList<>();
1279         for (double x = -0.5; x < 19; x += 0.25)
1280         {
1281             double y = -5 + x * slope;
1282             Ray2d ray = new Ray2d(x, y, slopeAngle);
1283             projections.add(ray);
1284             double projectionLocation = offsetLine.projectRay(ray);
1285             if (Double.isNaN(projectionLocation))
1286             {
1287                 offsetLine.projectRay(ray);
1288             }
1289             assertFalse("There is a projection", Double.isNaN(projectionLocation));
1290             Point2d projectedPoint = offsetLine.getLocation(projectionLocation);
1291             // System.out.println(String.format("DirectedPoint %s projects on line at %.3f. which is at %s", ray,
1292             // projectionLocation, projectedPoint));
1293             projections.add(projectedPoint);
1294             projections.add(ray); // And back to ray
1295             assertTrue("projection increases monotonous", projectionLocation > prevProjection);
1296             prevProjection = projectionLocation;
1297         }
1298         // System.out.print("projections: " + new PolyLine2d(projections).toPlot());
1299         projections.clear();
1300         prevProjection = -100;
1301         for (double x = 1.5; x < 21; x += 0.25)
1302         {
1303             double y = -15 + x * slope;
1304             Ray2d ray = new Ray2d(x, y, slopeAngle);
1305             double projectionLocation = offsetLine.projectRay(ray);
1306             if (Double.isNaN(projectionLocation))
1307             {
1308                 System.out.println("x " + x + " gives NaN result");
1309                 continue;
1310             }
1311             projections.add(ray);
1312             Point2d projectedPoint = offsetLine.getLocation(projectionLocation);
1313             // System.out.println(String.format("DirectedPoint %s projects on line at %.3f. which is at %s", ray,
1314             // projectionLocation, projectedPoint));
1315             projections.add(projectedPoint);
1316             projections.add(ray); // And back to ray
1317             assertTrue("projection increases monotonous", projectionLocation > prevProjection);
1318             prevProjection = projectionLocation;
1319         }
1320         // System.out.print("projections: " + new PolyLine2d(projections).toPlot());
1321     }
1322 
1323     /**
1324      * Test the debugging output methods.
1325      */
1326     @Test
1327     public void testExports()
1328     {
1329         Point2d[] points =
1330                 new Point2d[] { new Point2d(123.456, 345.678), new Point2d(234.567, 456.789), new Point2d(-12.345, -34.567) };
1331         PolyLine2d pl = new PolyLine2d(points);
1332         String[] out = pl.toExcel().split("\\n");
1333         assertEquals("Excel output consists of one line per point", points.length, out.length);
1334         for (int index = 0; index < points.length; index++)
1335         {
1336             String[] fields = out[index].split("\\t");
1337             assertEquals("each line consists of two fields", 2, fields.length);
1338             try
1339             {
1340                 double x = Double.parseDouble(fields[0].trim());
1341                 assertEquals("x matches", points[index].x, x, 0.001);
1342             }
1343             catch (NumberFormatException nfe)
1344             {
1345                 fail("First field " + fields[0] + " does not parse as a double");
1346             }
1347             try
1348             {
1349                 double y = Double.parseDouble(fields[1].trim());
1350                 assertEquals("y matches", points[index].y, y, 0.001);
1351             }
1352             catch (NumberFormatException nfe)
1353             {
1354                 fail("Second field " + fields[1] + " does not parse as a double");
1355             }
1356         }
1357 
1358         out = pl.toPlot().split(" L");
1359         assertEquals("Plotter output consists of one coordinate pair per point", points.length, out.length);
1360         for (int index = 0; index < points.length; index++)
1361         {
1362             String[] fields = out[index].split(",");
1363             assertEquals("each line consists of two fields", 2, fields.length);
1364             if (index == 0)
1365             {
1366                 assertTrue(fields[0].startsWith("M"));
1367                 fields[0] = fields[0].substring(1);
1368             }
1369             try
1370             {
1371                 double x = Double.parseDouble(fields[0].trim());
1372                 assertEquals("x matches", points[index].x, x, 0.001);
1373             }
1374             catch (NumberFormatException nfe)
1375             {
1376                 fail("First field " + fields[0] + " does not parse as a double");
1377             }
1378             try
1379             {
1380                 double y = Double.parseDouble(fields[1].trim());
1381                 assertEquals("y matches", points[index].y, y, 0.001);
1382             }
1383             catch (NumberFormatException nfe)
1384             {
1385                 fail("Second field " + fields[1] + " does not parse as a double");
1386             }
1387         }
1388     }
1389 
1390     /**
1391      * Verify that a Line2d contains the same points as an array of Point2d.
1392      * @param line Line2d; the OTS line
1393      * @param points Point2d[]; the OTSPoint array
1394      * @throws DrawRuntimeException should not happen; this test has failed if it does happen
1395      */
1396     private void verifyPointsAndSegments(final PolyLine2d line, final Point2d[] points) throws DrawRuntimeException
1397     {
1398         assertEquals("Line should have same number of points as point array", line.size(), points.length);
1399         for (int i = 0; i < points.length; i++)
1400         {
1401             assertEquals("x of point i should match", points[i].x, line.get(i).x, Math.ulp(points[i].x));
1402             assertEquals("y of point i should match", points[i].y, line.get(i).y, Math.ulp(points[i].y));
1403             assertEquals("x of point i should match", points[i].x, line.getX(i), Math.ulp(points[i].x));
1404             assertEquals("y of point i should match", points[i].y, line.getY(i), Math.ulp(points[i].y));
1405             if (i < points.length - 1)
1406             {
1407                 LineSegment2d segment = line.getSegment(i);
1408                 assertEquals("begin x of line segment i should match", points[i].x, segment.startX, Math.ulp(points[i].x));
1409                 assertEquals("begin y of line segment i should match", points[i].y, segment.startY, Math.ulp(points[i].y));
1410                 assertEquals("end x of line segment i should match", points[i + 1].x, segment.endX, Math.ulp(points[i + 1].x));
1411                 assertEquals("end y of line segment i should match", points[i + 1].y, segment.endY, Math.ulp(points[i + 1].y));
1412             }
1413             else
1414             {
1415                 try
1416                 {
1417                     line.getSegment(i);
1418                     fail("Too large index should have thrown a DrawRuntimeException");
1419                 }
1420                 catch (DrawRuntimeException dre)
1421                 {
1422                     // Ignore expected exception
1423                 }
1424 
1425                 try
1426                 {
1427                     line.getSegment(-1);
1428                     fail("Negative index should have thrown a DrawRuntimeException");
1429                 }
1430                 catch (DrawRuntimeException dre)
1431                 {
1432                     // Ignore expected exception
1433                 }
1434 
1435             }
1436         }
1437     }
1438 
1439     /**
1440      * Test the transitionLine method.
1441      */
1442     @Test
1443     public void testTransitionLine()
1444     {
1445         // Create a Bezier with a 90 degree change of direction starting in X direction, ending in Y direction
1446         PolyLine2d bezier = Bezier.cubic(64, new Ray2d(-5, 0, 0, 0), new Ray2d(0, 5, 0, 7));
1447         // System.out.print("c1,0,0" + bezier1.project().toPlot());
1448         double length = bezier.getLength();
1449         double prevDir = Double.NaN;
1450         for (int step = 0; step <= 1000; step++)
1451         {
1452             double distance = length * step / 1000;
1453             Ray2d ray = bezier.getLocation(distance);
1454             double direction = Math.toDegrees(ray.phi);
1455             if (step > 0)
1456             {
1457                 assertEquals("phi changes very little at step " + step, prevDir, direction, 2);
1458             }
1459             prevDir = Math.toDegrees(ray.phi);
1460         }
1461         // Make a gradually transitioning offset line
1462         PolyLine2d transitioningOffsetLine = bezier.offsetLine(0, 2);
1463         // Verify that this curve is fairly smooth
1464         length = transitioningOffsetLine.getLength();
1465         prevDir = Double.NaN;
1466         for (int step = 0; step <= 1000; step++)
1467         {
1468             double distance = length * step / 1000;
1469             Ray2d ray = transitioningOffsetLine.getLocation(distance);
1470             double direction = Math.toDegrees(ray.phi);
1471             if (step > 0)
1472             {
1473                 assertEquals("phi changes very little at step " + step, prevDir, direction, 2);
1474             }
1475             prevDir = Math.toDegrees(ray.phi);
1476         }
1477         PolyLine2d endLine = bezier.offsetLine(-2);
1478         // System.out.print("c0,1,0" + endLine.project().toPlot());
1479         TransitionFunction transitionFunction = new TransitionFunction()
1480         {
1481             @Override
1482             public double function(final double fraction)
1483             {
1484                 return 0.5 - Math.cos(fraction * Math.PI) / 2;
1485             }
1486         };
1487         PolyLine2d cosineSmoothTransitioningLine = bezier.transitionLine(endLine, transitionFunction);
1488         // System.out.print("c0,0,0" + cosineSmoothTransitioningLine.project().toPlot());
1489         length = cosineSmoothTransitioningLine.getLength();
1490         prevDir = Double.NaN;
1491         for (int step = 0; step <= 1000; step++)
1492         {
1493             double distance = length * step / 1000;
1494             Ray2d ray = cosineSmoothTransitioningLine.getLocation(distance);
1495             double direction = Math.toDegrees(ray.phi);
1496             if (step > 0)
1497             {
1498                 assertEquals("phi changes very little at step " + step, prevDir, direction, 4);
1499             }
1500             prevDir = Math.toDegrees(ray.phi);
1501         }
1502         // System.out.print(
1503         // "c0,0,1" + Bezier.cubic(bezier1.getLocationFraction(0), endLine.getLocationFraction(1)).project().toPlot());
1504         // Reverse the lines
1505         PolyLine2d cosineSmoothTransitioningLine2 =
1506                 endLine.reverse().transitionLine(bezier.reverse(), transitionFunction).reverse();
1507         // Check that those lines are very similar
1508         assertEquals("Lengths are equal", cosineSmoothTransitioningLine.getLength(), cosineSmoothTransitioningLine2.getLength(),
1509                 0.001);
1510         for (int step = 0; step <= 1000; step++)
1511         {
1512             Ray2d ray1 = cosineSmoothTransitioningLine.getLocation(step * cosineSmoothTransitioningLine.getLength() / 1000);
1513             Ray2d ray2 = cosineSmoothTransitioningLine2.getLocation(step * cosineSmoothTransitioningLine2.getLength() / 1000);
1514             assertEquals("rays are almost equal in x", ray1.x, ray2.x, 0.001);
1515             assertEquals("rays are almost equal in y", ray1.y, ray2.y, 0.001);
1516             assertEquals("rays are almost equal in phi", ray1.phi, ray2.phi, 0.0001);
1517         }
1518 
1519         assertEquals("offset by zero returns original", bezier, bezier.offsetLine(0, 0));
1520         assertEquals("offset by constant with two arguments returns same as offset with one argument", bezier.offsetLine(3, 3),
1521                 bezier.offsetLine(3));
1522     }
1523 
1524     /**
1525      * Test the filtering constructors.
1526      * @throws DrawRuntimeException should never happen
1527      */
1528     @Test
1529     public final void filterTest() throws DrawRuntimeException
1530     {
1531         Point2d[] tooShort = new Point2d[] {};
1532         try
1533         {
1534             new PolyLine2d(true, tooShort);
1535             fail("Array with no points should have thrown an exception");
1536         }
1537         catch (DrawRuntimeException dre)
1538         {
1539             // Ignore expected exception
1540         }
1541 
1542         tooShort = new Point2d[] { new Point2d(1, 2) };
1543         try
1544         {
1545             new PolyLine2d(true, tooShort);
1546             fail("Array with one point should have thrown an exception");
1547         }
1548         catch (DrawRuntimeException dre)
1549         {
1550             // Ignore expected exception
1551         }
1552 
1553         Point2d p0 = new Point2d(1, 2);
1554         Point2d p1 = new Point2d(4, 5);
1555         Point2d[] points = new Point2d[] { p0, p1 };
1556         PolyLine2d result = new PolyLine2d(true, points);
1557         assertTrue("first point is p0", p0.equals(result.get(0)));
1558         assertTrue("second point is p1", p1.equals(result.get(1)));
1559         Point2d p1Same = new Point2d(4, 5);
1560         result = new PolyLine2d(true, new Point2d[] { p0, p0, p0, p0, p1Same, p0, p1, p1, p1Same, p1, p1 });
1561         assertEquals("result should contain 4 points", 4, result.size());
1562         assertTrue("first point is p0", p0.equals(result.get(0)));
1563         assertTrue("second point is p1", p1.equals(result.get(1)));
1564         assertTrue("third point is p0", p0.equals(result.get(0)));
1565         assertTrue("last point is p1", p1.equals(result.get(1)));
1566         new PolyLine2d(true, new Point2d[] { p0, new Point2d(1, 3) });
1567 
1568         try
1569         {
1570             PolyLine2d.cleanPoints(true, null);
1571             fail("null iterator should have thrown a NullPointerException");
1572         }
1573         catch (NullPointerException npe)
1574         {
1575             // Ignore expected exception
1576         }
1577 
1578         try
1579         {
1580             PolyLine2d.cleanPoints(true, new Iterator<Point2d>()
1581             {
1582                 @Override
1583                 public boolean hasNext()
1584                 {
1585                     return false;
1586                 }
1587 
1588                 @Override
1589                 public Point2d next()
1590                 {
1591                     return null;
1592                 }
1593             });
1594             fail("Iterator that has no data should have thrown a DrawRuntimeException");
1595         }
1596         catch (DrawRuntimeException dre)
1597         {
1598             // Ignore expected exception
1599         }
1600 
1601         Iterator<Point2d> iterator =
1602                 PolyLine2d.cleanPoints(true, Arrays.stream(new Point2d[] { new Point2d(1, 2) }).iterator());
1603         iterator.next(); // should work
1604         assertFalse("iterator should now be out of data", iterator.hasNext());
1605         try
1606         {
1607             iterator.next();
1608             fail("Iterator that has no nore data should have thrown a NoSuchElementException");
1609         }
1610         catch (NoSuchElementException nse)
1611         {
1612             // Ignore expected exception
1613         }
1614 
1615         // Check that cleanPoints with false indeed does not filter
1616         iterator = PolyLine2d.cleanPoints(false,
1617                 Arrays.stream(new Point2d[] { new Point2d(1, 2), new Point2d(1, 2), new Point2d(1, 2) }).iterator());
1618         assertTrue("iterator has initial point", iterator.hasNext());
1619         iterator.next();
1620         assertTrue("iterator has second point", iterator.hasNext());
1621         iterator.next();
1622         assertTrue("iterator has second point", iterator.hasNext());
1623         iterator.next();
1624         assertFalse("iterator has no more data", iterator.hasNext());
1625     }
1626 
1627     /**
1628      * Test the hashCode and Equals methods.
1629      * @throws DrawRuntimeException when that happens uncaught; this test has failed
1630      * @throws NullPointerException when that happens uncaught; this test has failed
1631      */
1632     @SuppressWarnings("unlikely-arg-type")
1633     @Test
1634     public void testToStringHashCodeAndEquals() throws NullPointerException, DrawRuntimeException
1635     {
1636         PolyLine2d line = new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) });
1637         assertTrue("toString returns something descriptive", line.toString().startsWith("PolyLine2d ["));
1638         assertFalse("toString does not startHeading", line.toString().contains("startHeading"));
1639         assertTrue("toString can suppress the class name", line.toString().indexOf(line.toString(true)) > 0);
1640 
1641         // Verify that hashCode. Check that the result depends on the actual coordinates.
1642         assertNotEquals("hash code takes x coordinate into account",
1643                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
1644                 new PolyLine2d(new Point2d(1, 0), new Point2d(1, 1)).hashCode());
1645         assertNotEquals("hash code takes y coordinate into account",
1646                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
1647                 new PolyLine2d(new Point2d(0, 1), new Point2d(1, 1)).hashCode());
1648         assertNotEquals("hash code takes x coordinate into account",
1649                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
1650                 new PolyLine2d(new Point2d(0, 0), new Point2d(2, 1)).hashCode());
1651         assertNotEquals("hash code takes y coordinate into account",
1652                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 1)).hashCode(),
1653                 new PolyLine2d(new Point2d(0, 0), new Point2d(1, 2)).hashCode());
1654 
1655         // Verify the equals method.
1656         assertTrue("line is equal to itself", line.equals(line));
1657         assertFalse("line is not equal to a different line",
1658                 line.equals(new PolyLine3d(new Point3d(123, 456, 789), new Point3d(789, 101112, 2))));
1659         assertFalse("line is not equal to null", line.equals(null));
1660         assertFalse("line is not equal to a different kind of object", line.equals("unlikely"));
1661         assertEquals("equals verbatim copy", line,
1662                 new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(4, 6), new Point2d(8, 9) }));
1663         assertNotEquals("equals checks x", line,
1664                 new PolyLine2d(new Point2d[] { new Point2d(2, 2), new Point2d(4, 6), new Point2d(8, 9) }));
1665         assertNotEquals("equals checks y", line,
1666                 new PolyLine2d(new Point2d[] { new Point2d(1, 2), new Point2d(4, 7), new Point2d(8, 9) }));
1667         assertTrue("Line is equal to line from same set of points", line.equals(new PolyLine2d(line.getPoints())));
1668     }
1669 
1670     /**
1671      * Test the degenerate PolyLine2d.
1672      */
1673     @Test
1674     public void testDegenerate()
1675     {
1676         try
1677         {
1678             new PolyLine2d(Double.NaN, 2, 3);
1679             fail("NaN should have thrown a DrawRuntimeException");
1680         }
1681         catch (DrawRuntimeException dre)
1682         {
1683             // Ignore expected exception
1684         }
1685 
1686         try
1687         {
1688             new PolyLine2d(1, Double.NaN, 3);
1689             fail("NaN should have thrown a DrawRuntimeException");
1690         }
1691         catch (DrawRuntimeException dre)
1692         {
1693             // Ignore expected exception
1694         }
1695 
1696         try
1697         {
1698             new PolyLine2d(1, 2, Double.NaN);
1699             fail("NaN should have thrown a DrawRuntimeException");
1700         }
1701         catch (DrawRuntimeException dre)
1702         {
1703             // Ignore expected exception
1704         }
1705 
1706         try
1707         {
1708             new PolyLine2d(1, 2, Double.POSITIVE_INFINITY);
1709             fail("NaN should have thrown a DrawRuntimeException");
1710         }
1711         catch (DrawRuntimeException dre)
1712         {
1713             // Ignore expected exception
1714         }
1715 
1716         try
1717         {
1718             new PolyLine2d(1, 2, Double.NEGATIVE_INFINITY);
1719             fail("NaN should have thrown a DrawRuntimeException");
1720         }
1721         catch (DrawRuntimeException dre)
1722         {
1723             // Ignore expected exception
1724         }
1725 
1726         PolyLine2d l = new PolyLine2d(1, 2, 3);
1727         assertEquals("length is 0", 0, l.getLength(), 0);
1728         assertEquals("size is 1", 1, l.size());
1729         assertEquals("getX(0) is 1", 1, l.getX(0), 0);
1730         assertEquals("getY(0) is 2", 2, l.getY(0), 0);
1731         Ray2d r = l.getLocation(0.0);
1732         assertEquals("heading at 0", 3, r.getPhi(), 0);
1733         assertEquals("x at 0 is 1", 1, r.getX(), 0);
1734         assertEquals("y at 0 is 2", 2, r.getY(), 0);
1735         assertEquals("bounds", new Bounds2d(l.get(0)), l.getBounds());
1736         try
1737         {
1738             l.getLocation(0.1);
1739             fail("location at position != 0 should have thrown a DrawRuntimeException");
1740         }
1741         catch (DrawRuntimeException dre)
1742         {
1743             // Ignore expected exception
1744         }
1745 
1746         try
1747         {
1748             l.getLocation(-0.1);
1749             fail("location at position != 0 should have thrown a DrawRuntimeException");
1750         }
1751         catch (DrawRuntimeException dre)
1752         {
1753             // Ignore expected exception
1754         }
1755 
1756         try
1757         {
1758             new PolyLine2d(new Point2d(1, 2), Double.NaN);
1759             fail("NaN should have thrown a DrawRuntimeException");
1760         }
1761         catch (DrawRuntimeException dre)
1762         {
1763             // Ignore expected exception
1764         }
1765 
1766         try
1767         {
1768             new PolyLine2d((Ray2d) null);
1769             fail("null pointer should have thrown a NullPointerException");
1770         }
1771         catch (NullPointerException npe)
1772         {
1773             // Ignore expected exception
1774         }
1775 
1776         assertEquals("closest point is the point", r, l.closestPointOnPolyLine(new Point2d(4, -2)));
1777 
1778         PolyLine2d straightX = new PolyLine2d(1, 2, 0);
1779         for (int x = -10; x <= 10; x += 1)
1780         {
1781             for (int y = -10; y <= 10; y += 1)
1782             {
1783                 Point2d testPoint = new Point2d(x, y);
1784                 assertEquals("closest point extended", r.projectOrthogonalExtended(testPoint),
1785                         l.projectOrthogonalExtended(testPoint));
1786                 assertEquals("closest point on degenerate line is the point of the degenerate line", l.getLocation(0.0),
1787                         l.closestPointOnPolyLine(testPoint));
1788                 if (x == 1)
1789                 {
1790                     assertEquals("projection on horizontal degenerate line hits", straightX.get(0),
1791                             straightX.projectOrthogonal(testPoint));
1792                 }
1793                 else
1794                 {
1795                     assertNull("projection on horizontal degenerate line misses", straightX.projectOrthogonal(testPoint));
1796                 }
1797                 if (x == 1 && y == 2)
1798                 {
1799                     assertEquals("NonExtended projection will return point for exact match", testPoint,
1800                             l.projectOrthogonal(testPoint));
1801                     assertEquals("NonExtended fractional projection returns 0 for exact match", 0,
1802                             l.projectOrthogonalFractional(testPoint), 0);
1803                     assertEquals("Extended fractional projection returns 0 for exact match", 0,
1804                             l.projectOrthogonalFractionalExtended(testPoint), 0);
1805                 }
1806                 else
1807                 {
1808                     assertNull("For non-nice directions nonExtended projection will return null if point does not match",
1809                             l.projectOrthogonal(testPoint));
1810                     assertTrue("For non-nice directions non-extended fractional projection will return NaN if point does "
1811                             + "not match", Double.isNaN(l.projectOrthogonalFractional(testPoint)));
1812                     if (l.getLocation(0.0).projectOrthogonalFractional(testPoint) > 0)
1813                     {
1814                         assertTrue(
1815                                 "ProjectOrthogonalFractionalExtended returns POSITIVE_INFINITY of projection misses "
1816                                         + "along startHeading side",
1817                                 Double.POSITIVE_INFINITY == l.projectOrthogonalFractionalExtended(testPoint));
1818                     }
1819                     else
1820                     {
1821                         assertTrue(
1822                                 "ProjectOrthogonalFractionalExtended returns POSITIVE_INFINITY of projection misses "
1823                                         + ", but not along startHeading side",
1824                                 Double.NEGATIVE_INFINITY == l.projectOrthogonalFractionalExtended(testPoint));
1825                     }
1826                 }
1827                 if (x == 1)
1828                 {
1829                     assertEquals("Non-Extended projection will return point for matching X for line along X", straightX.get(0),
1830                             straightX.projectOrthogonal(testPoint));
1831                 }
1832                 else
1833                 {
1834                     assertNull("Non-Extended projection will return null for non matching X for line along X",
1835                             straightX.projectOrthogonal(testPoint));
1836                 }
1837             }
1838         }
1839 
1840         l = new PolyLine2d(new Point2d(1, 2), 3);
1841         assertEquals("length is 0", 0, l.getLength(), 0);
1842         assertEquals("size is 1", 1, l.size());
1843         assertEquals("getX(0) is 1", 1, l.getX(0), 0);
1844         assertEquals("getY(0) is 2", 2, l.getY(0), 0);
1845         r = l.getLocation(0.0);
1846         assertEquals("heading at 0", 3, r.getPhi(), 0);
1847         assertEquals("x at 0 is 1", 1, r.getX(), 0);
1848         assertEquals("y at 0 is 2", 2, r.getY(), 0);
1849 
1850         l = new PolyLine2d(new Ray2d(1, 2, 3));
1851         assertEquals("length is 0", 0, l.getLength(), 0);
1852         assertEquals("size is 1", 1, l.size());
1853         assertEquals("getX(0) is 1", 1, l.getX(0), 0);
1854         assertEquals("getY(0) is 2", 2, l.getY(0), 0);
1855         r = l.getLocation(0.0);
1856         assertEquals("heading at 0", 3, r.getPhi(), 0);
1857         assertEquals("x at 0 is 1", 1, r.getX(), 0);
1858         assertEquals("y at 0 is 2", 2, r.getY(), 0);
1859 
1860         PolyLine2d notEqual = new PolyLine2d(1, 2, 4);
1861         assertNotEquals("Check that the equals method verifies the startHeading", l, notEqual);
1862         
1863         assertTrue("toString contains startHeading", l.toString().contains("startHeading"));
1864     }
1865 
1866     /**
1867      * Problem with limited precision when getting location almost at end.
1868      * @throws DrawRuntimeException when that happens this test has triggered the problem
1869      */
1870     @Test
1871     public void testOTS2Problem() throws DrawRuntimeException
1872     {
1873         PolyLine2d line = new PolyLine2d(new Point2d(100, 0), new Point2d(100.1, 0));
1874         double length = line.getLength();
1875         line.getLocation(length - Math.ulp(length));
1876     }
1877 
1878 }