View Javadoc
1   package org.djutils.draw.curve;
2   
3   import static org.junit.jupiter.api.Assertions.assertEquals;
4   import static org.junit.jupiter.api.Assertions.assertFalse;
5   import static org.junit.jupiter.api.Assertions.assertNotEquals;
6   import static org.junit.jupiter.api.Assertions.assertTrue;
7   import static org.junit.jupiter.api.Assertions.fail;
8   
9   import java.util.Iterator;
10  import java.util.NavigableMap;
11  import java.util.TreeMap;
12  
13  import org.djutils.draw.Direction3d;
14  import org.djutils.draw.Export;
15  import org.djutils.draw.function.ContinuousPiecewiseLinearFunction;
16  import org.djutils.draw.line.LineSegment2d;
17  import org.djutils.draw.line.LineSegment3d;
18  import org.djutils.draw.line.PolyLine2d;
19  import org.djutils.draw.line.PolyLine3d;
20  import org.djutils.draw.line.Ray2d;
21  import org.djutils.draw.line.Ray3d;
22  import org.djutils.draw.point.DirectedPoint2d;
23  import org.djutils.draw.point.Point2d;
24  import org.djutils.draw.point.Point3d;
25  import org.djutils.math.AngleUtil;
26  import org.junit.jupiter.api.Test;
27  
28  /**
29   * TestCurves.java.
30   * <p>
31   * Copyright (c) 2024-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
32   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
33   * distributed under a three-clause BSD-style license, which can be found at
34   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
35   * </p>
36   * <p>
37   * TODO test flattener Beziers that are not based on just two DirectedPoint objects.
38   * </p>
39   * <p>
40   * TODO also with maxAngle flattener and non-declared knot.
41   * </p>
42   * <p>
43   * TODO test flattener with curve that has a knot.
44   * </p>
45   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
46   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
47   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
48   */
49  public class TestCurves
50  {
51  
52      /**
53       * Test the ContinuousStraight class.
54       */
55      @Test
56      public void testStraight()
57      {
58          NavigableMap<Double, Double> transition = new TreeMap<>();
59          transition.put(0.0, 0.0);
60          transition.put(0.2, 1.0);
61          transition.put(1.0, 2.0);
62          int steps = 100;
63          for (double x : new double[] {-10, -1, -0.1, 0, 0.1, 1, 10})
64          {
65              for (double y : new double[] {-30, -3, -0.3, 0, 0.3, 3, 30})
66              {
67                  for (double dirZ : new double[] {-3, -2, -1, 0, 1, 2, 3})
68                  {
69                      DirectedPoint2d dp = new DirectedPoint2d(x, y, dirZ);
70                      for (double length : new double[] {0.3, 3, 30})
71                      {
72                          Straight2d cs = new Straight2d(dp, length);
73                          assertEquals(x, cs.getStartPoint().x, 0.0, "start x");
74                          assertEquals(y, cs.getStartPoint().y, 0.0, "start y");
75                          assertEquals(dirZ, cs.getStartPoint().dirZ, 0.00001, "start dirZ");
76                          assertEquals(cs.getStartCurvature(), 0, 0, "start curvature");
77                          assertEquals(x + Math.cos(dirZ) * length, cs.getEndPoint().x, 0.00001, "end x");
78                          assertEquals(y + Math.sin(dirZ) * length, cs.getEndPoint().y, 0.00001, "end y");
79                          assertEquals(dirZ, cs.getEndPoint().dirZ, 0.00001, "end dirZ");
80                          assertEquals(cs.getEndCurvature(), 0, 0, "end curvature");
81                          assertEquals(length, cs.getLength(), 0.00001, "length");
82                          PolyLine2d flattened = cs.toPolyLine(null);
83                          assertEquals(2, flattened.size(), "size of flattened is 2 points");
84                          assertEquals(x, flattened.get(0).x, 0, "start of flattened x");
85                          assertEquals(y, flattened.get(0).y, 0, "start of flattened y");
86                          assertEquals(x + Math.cos(dirZ) * length, flattened.get(1).x, 0.00001, "end of flattened x");
87                          assertEquals(y + Math.sin(dirZ) * length, flattened.get(1).y, 0.00001, "end of flattened y");
88                          for (int step = 0; step <= steps; step++)
89                          {
90                              double fraction = 1.0 * step / steps;
91                              Point2d position = cs.getPoint(fraction);
92                              double direction = cs.getDirection(fraction);
93                              Point2d closest = flattened.closestPointOnPolyLine(position);
94                              assertEquals(0, position.distance(closest), 0.01, "Point at fraction lies on flattened line");
95                              assertEquals(flattened.get(0).directionTo(flattened.get(1)), direction, 0.00001,
96                                      "Direction matches");
97                          }
98                          ContinuousPiecewiseLinearFunction of = new ContinuousPiecewiseLinearFunction(transition);
99                          flattened = cs.toPolyLine(null, of);
100                         assertEquals(3, flattened.size(),
101                                 "size of flattened line with one offset knot along the way is 3 points");
102                         assertEquals(x, flattened.get(0).x, 0, "start of flattened x");
103                         assertEquals(y, flattened.get(0).y, 0, "start of flattened y");
104                         assertEquals(x + length * 0.2 * Math.cos(dirZ) - of.get(0.2) * Math.sin(dirZ), flattened.getX(1),
105                                 0.0001, "x of intermediate point");
106                         assertEquals(y + length * 0.2 * Math.sin(dirZ) + of.get(0.2) * Math.cos(dirZ), flattened.getY(1),
107                                 0.0001, "x of intermediate point");
108                         assertEquals(x + length * 1.0 * Math.cos(dirZ) - of.get(1.0) * Math.sin(dirZ), flattened.getX(2),
109                                 0.0001, "x of intermediate point");
110                         assertEquals(y + length * 1.0 * Math.sin(dirZ) + of.get(1.0) * Math.cos(dirZ), flattened.getY(2),
111                                 0.0001, "x of intermediate point");
112                         for (int step = 0; step <= steps; step++)
113                         {
114                             double fraction = 1.0 * step / steps;
115                             Point2d position = cs.getPoint(fraction, of);
116                             double direction = cs.getDirection(fraction, of);
117                             Point2d closest = flattened.closestPointOnPolyLine(position);
118                             assertEquals(0, position.distance(closest), 0.01, "Point at fraction lies on flattened line");
119                             if (fraction < 0.2)
120                             {
121                                 assertEquals(flattened.get(0).directionTo(flattened.get(1)), direction, 0.00001,
122                                         "Direction matches");
123                             }
124                             if (fraction > 0.2)
125                             {
126                                 assertEquals(flattened.get(1).directionTo(flattened.get(2)), direction, 0.00001,
127                                         "Direction matches");
128                             }
129                         }
130                     }
131                 }
132             }
133         }
134         try
135         {
136             new Straight2d(new DirectedPoint2d(1, 2, 3), -0.2);
137             fail("negative length should have thrown an IllegalArgumentException");
138         }
139         catch (IllegalArgumentException iae)
140         {
141             // Ignore expected exception
142         }
143         try
144         {
145             new Straight2d(new DirectedPoint2d(1, 2, 3), 0.0);
146             fail("zero length should have thrown an IllegalArgumentException");
147         }
148         catch (IllegalArgumentException iae)
149         {
150             // Ignore expected exception
151         }
152         assertTrue(new Straight2d(new DirectedPoint2d(2, 5, 1), 3).toString().startsWith("Straight ["),
153                 "toString returns something descriptive");
154     }
155 
156     /**
157      * Test the ContinuousArc class.
158      */
159     @Test
160     public void testArc()
161     {
162         NavigableMap<Double, Double> transition = new TreeMap<>();
163         transition.put(0.0, 0.0);
164         transition.put(0.2, 1.0);
165         transition.put(1.0, 2.0);
166         for (double x : new double[] {0, -10, -1, -0.1, 0.1, 1, 10})
167         {
168             for (double y : new double[] {0, -30, -3, -0.3, 0.3, 3, 30})
169             {
170                 for (double dirZ : new double[] {-3, -2, -1, 0, Math.PI / 2, 1, 2, 3})
171                 {
172                     DirectedPoint2d dp = new DirectedPoint2d(x, y, dirZ);
173                     for (double radius : new double[] {3.0, 0.3, 30})
174                     {
175                         for (boolean left : new Boolean[] {false, true})
176                         {
177                             for (double a : new double[] {1, 0.1, 2, 5})
178                             {
179                                 Arc2d ca = new Arc2d(dp, radius, left, a);
180                                 assertEquals(x, ca.getStartPoint().x, 0.00001, "start x");
181                                 assertEquals(y, ca.getStartPoint().y, 0.00001, "start y");
182                                 assertEquals(dirZ, ca.getStartPoint().dirZ, 0, "start dirZ");
183                                 assertEquals(radius, ca.getStartRadius(), 0.000001, "start radius");
184                                 assertEquals(radius, ca.getEndRadius(), 0.000001, "end radius");
185                                 assertEquals(1 / radius, ca.getStartCurvature(), 0.00001, "start curvature");
186                                 assertEquals(1 / radius, ca.getEndCurvature(), 0.00001, "end curvature");
187                                 assertEquals(dirZ, ca.getStartDirection(), 0, "start direction");
188                                 assertEquals(AngleUtil.normalizeAroundZero(dirZ + (left ? a : -a)), ca.getEndDirection(),
189                                         0.00001, "end direction");
190                                 int sign = left ? 1 : -1;
191                                 Point2d center =
192                                         new Point2d(x - Math.sin(dirZ) * radius * sign, y + Math.cos(dirZ) * radius * sign);
193                                 DirectedPoint2d expectedEnd =
194                                         new DirectedPoint2d(center.x + Math.sin(dirZ + a * sign) * radius * sign,
195                                                 center.y - Math.cos(dirZ + a * sign) * radius * sign,
196                                                 AngleUtil.normalizeAroundZero(dirZ + a * sign));
197                                 assertTrue(expectedEnd.epsilonEquals(ca.getEndPoint(), 0.001, 0.00001), " end point");
198                                 assertEquals(radius * a, ca.getLength(), 0.00001, "length");
199                                 // Test the NumSegments flattener without offsets
200                                 PolyLine2d flattened = ca.toPolyLine(new Flattener2d.NumSegments(20));
201                                 verifyNumSegments(ca, flattened, 20);
202                                 // Test the MaxDeviation flattener without offsets
203                                 double precision = 0.1;
204                                 flattened = ca.toPolyLine(new Flattener2d.MaxDeviation(precision));
205                                 verifyMaxDeviation(ca, flattened, precision);
206                                 // Test the MaxAngle flattener without offsets
207                                 double anglePrecision = 0.01;
208                                 flattened = ca.toPolyLine(new Flattener2d.MaxAngle(anglePrecision));
209                                 verifyMaxAngleDeviation(flattened, ca, anglePrecision);
210                                 // Test the MaxDeviationAndAngle flattener without offsets
211                                 flattened = ca.toPolyLine(new Flattener2d.MaxDeviationAndAngle(precision, anglePrecision));
212                                 verifyMaxDeviation(ca, flattened, precision);
213                                 verifyMaxAngleDeviation(flattened, ca, anglePrecision);
214                                 // Only check transitions for radius of arc > 2 and length of arc > 2
215                                 if (radius > 2 && ca.getLength() > 2)
216                                 {
217                                     ContinuousPiecewiseLinearFunction of = new ContinuousPiecewiseLinearFunction(transition);
218                                     // Test the NumSegments flattener with offsets
219                                     flattened = ca.toPolyLine(new OffsetFlattener2d.NumSegments(30), of);
220                                     verifyNumSegments(ca, of, flattened, 30);
221                                     // Test the MaxDeviation flattener with offsets
222                                     flattened = ca.toPolyLine(new OffsetFlattener2d.MaxDeviation(precision), of);
223                                     verifyMaxDeviation(ca, of, flattened, precision);
224                                     // Test the MaxAngle flattener with offsets
225                                     flattened = ca.toPolyLine(new OffsetFlattener2d.MaxAngle(anglePrecision), of);
226                                     verifyMaxAngleDeviation(flattened, ca, of, anglePrecision);
227                                     // Test the MaxDeviationAndAngle flattener with offsets
228                                     flattened = ca.toPolyLine(
229                                             new OffsetFlattener2d.MaxDeviationAndAngle(precision, anglePrecision), of);
230                                     verifyMaxDeviation(ca, of, flattened, precision);
231                                     verifyMaxAngleDeviation(flattened, ca, of, anglePrecision);
232                                 }
233                             }
234                         }
235                     }
236                 }
237             }
238         }
239         try
240         {
241             new Arc2d(new DirectedPoint2d(1, 2, 3), -0.01, true, 1);
242             fail("negative radius should have thrown an IllegalArgumentException");
243         }
244         catch (IllegalArgumentException iae)
245         {
246             // Ignore expected exception
247         }
248         new Arc2d(new DirectedPoint2d(1, 2, 3), 0, true, 1); // is allowed
249         try
250         {
251             new Arc2d(new DirectedPoint2d(1, 2, 3), 10, true, -0.1);
252             fail("negative angle should have thrown an IllegalArgumentException");
253         }
254         catch (IllegalArgumentException iae)
255         {
256             // Ignore expected exception
257         }
258         new Arc2d(new DirectedPoint2d(1, 2, 3), 10, true, 0); // is allowed
259         assertTrue(new Arc2d(new DirectedPoint2d(1, 2, 3), 10, true, 1).toString().startsWith("Arc ["),
260                 "toString returns something descriptive");
261 
262         Arc2d arc2d = new Arc2d(new DirectedPoint2d(1, 2, 3), 10, true, 1.5);
263         assertEquals(1.5, arc2d.getAngle(), 0.00001, "Angle is returned");
264         assertTrue(arc2d.isLeft(), "arc is left");
265         arc2d = new Arc2d(new DirectedPoint2d(1, 2, 3), 10, false, 1.5);
266         assertFalse(arc2d.isLeft(), "arc is right");
267     }
268 
269     /**
270      * Verify the number of segments and the location of the points on a flattened FlattableLine2d.
271      * @param curve FlattableLine2d
272      * @param flattened PolyLine2d
273      * @param numSegments the number of segments that the flattened FlattableLine2d should have
274      */
275     private static void verifyNumSegments(final Curve2d curve, final PolyLine2d flattened, final int numSegments)
276     {
277         assertEquals(numSegments, flattened.size() - 1, "Number of segments");
278         for (int i = 0; i <= numSegments; i++)
279         {
280             double fraction = i * 1.0 / numSegments;
281             Point2d expectedPoint = curve.getPoint(fraction);
282             Point2d actualPoint = flattened.get(i);
283             assertEquals(expectedPoint, actualPoint, "Point in flattened line matches point generated by the continuous arc");
284         }
285     }
286 
287     /**
288      * Verify the number of segments and the location of the points on a flattened OffsetFlattableLine2d.
289      * @param curve OffsetFlattableLine2d
290      * @param of ContinuousPiecewiseLinearFunction (may be null)
291      * @param flattened PolyLine2d
292      * @param numSegments the number of segments that the flattened OffsetFlattableLine2d should have
293      */
294     private static void verifyNumSegments(final OffsetCurve2d curve, final ContinuousPiecewiseLinearFunction of,
295             final PolyLine2d flattened, final int numSegments)
296     {
297         assertEquals(numSegments, flattened.size() - 1, "Number of segments");
298         for (int i = 0; i <= numSegments; i++)
299         {
300             double fraction = i * 1.0 / numSegments;
301             Point2d expectedPoint = curve.getPoint(fraction, of);
302             Point2d actualPoint = flattened.get(i);
303             assertEquals(expectedPoint, actualPoint, "Point in flattened line matches point generated by the continuous arc");
304         }
305     }
306 
307     /**
308      * Verify the number of segments and the location of the points on a flattened FlattableLine2d.
309      * @param curve FlattableLine3d
310      * @param flattened PolyLine3d
311      * @param numSegments the number of segments that the flattened FlattableLine2d should have
312      */
313     private static void verifyNumSegments(final Curve3d curve, final PolyLine3d flattened, final int numSegments)
314     {
315         assertEquals(numSegments, flattened.size() - 1, "Number of segments");
316         for (int i = 0; i <= numSegments; i++)
317         {
318             double fraction = i * 1.0 / numSegments;
319             Point3d expectedPoint = curve.getPoint(fraction);
320             Point3d actualPoint = flattened.get(i);
321             assertEquals(expectedPoint, actualPoint, "Point in flattened line matches point generated by the continuous arc");
322         }
323     }
324 
325     /** Maximum permissible exceeding of precision. Needed due to the simple-minded way that the Flattener works. */
326     public static final double FUDGE_FACTOR = 1.4;
327 
328     /**
329      * Verify the lateral precision of a flattened FlattableLine2d.
330      * @param curve FlattableLine2d
331      * @param flattened PolyLine2d
332      * @param precision double
333      */
334     private static void verifyMaxDeviation(final Curve2d curve, final PolyLine2d flattened, final double precision)
335     {
336         int steps = 100;
337         for (int step = 0; step <= steps; step++)
338         {
339             double fraction = 1.0 * step / steps;
340             Point2d curvePoint = curve.getPoint(fraction);
341             Point2d polyLinePoint = flattened.closestPointOnPolyLine(curvePoint);
342             if (curvePoint.distance(polyLinePoint) > precision * FUDGE_FACTOR)
343             {
344                 printSituation(-1, 0.5, flattened, curve, fraction, null);
345                 curve.toPolyLine(new Flattener2d.MaxDeviation(precision));
346             }
347             assertEquals(0, curvePoint.distance(polyLinePoint), precision * FUDGE_FACTOR,
348                     "point on Curve2d is close to PolyLine2d");
349         }
350     }
351 
352     /**
353      * Verify the lateral precision of a flattened continuous FlattableLine2d.
354      * @param curve FlattableLine2d
355      * @param of ContinuousPiecewiseLinearFunction
356      * @param flattened PolyLine2d
357      * @param precision double
358      */
359     private static void verifyMaxDeviation(final OffsetCurve2d curve, final ContinuousPiecewiseLinearFunction of,
360             final PolyLine2d flattened, final double precision)
361     {
362         double fraction = 0.0;
363         int steps = flattened.size() - 1;
364         for (int step = 0; step < steps; step++)
365         {
366             double fractionAtStartOfSegment = Double.NaN;
367             double fractionAtEndOfSegment = Double.NaN;
368             LineSegment2d lineSegment = flattened.getSegment(step);
369             // Bisect to find fraction for start and end of segment and use middle of those fractions as THE fraction
370             for (double positionOnSegment : new double[] {0.01, 0.99})
371             {
372                 Point2d pointOnSegment = lineSegment.getLocation(positionOnSegment * lineSegment.getLength());
373                 double flattenedDir = lineSegment.getStartPoint().directionTo(lineSegment.getEndPoint());
374                 // Find a fraction on fa2d that results in a point very close to flattenPoint
375                 double veryClose = 0.1 / flattened.size() / 5; // Don't know why that / 5 was needed
376                 // Use bisection to encroach on the fraction
377                 double highFraction = Math.min(1.0, fraction + Math.min(20.0 / steps, 0.5));
378                 while (highFraction - fraction > veryClose)
379                 {
380                     double midFraction = (fraction + highFraction) / 2;
381                     Point2d midPoint = curve.getPoint(midFraction, of);
382                     double dir = midPoint.directionTo(pointOnSegment);
383                     double dirDifference = Math.abs(AngleUtil.normalizeAroundZero(flattenedDir - dir));
384                     if (dirDifference < Math.PI / 4)
385                     {
386                         fraction = midFraction;
387                     }
388                     else
389                     {
390                         highFraction = midFraction;
391                     }
392                 }
393                 if (positionOnSegment < 0.5)
394                 {
395                     fractionAtStartOfSegment = fraction;
396                 }
397                 else
398                 {
399                     fractionAtEndOfSegment = fraction;
400                 }
401             }
402             fraction = (fractionAtStartOfSegment + fractionAtEndOfSegment) / 2; // Take the middle
403             Point2d curvePoint = curve.getPoint(fraction, of);
404             double actualDistance = curvePoint.distance(lineSegment.closestPointOnSegment(curvePoint));
405             if (actualDistance > precision * FUDGE_FACTOR)
406             {
407                 printSituation(step, 0.5, flattened, curve, fraction, of);
408                 curve.toPolyLine(new OffsetFlattener2d.MaxDeviation(precision), of);
409             }
410             assertEquals(0, actualDistance, precision * FUDGE_FACTOR, "point on OffsetCurve2d is close to PolyLine2d");
411             fraction = fractionAtEndOfSegment;
412         }
413     }
414 
415     /**
416      * Verify the lateral precision of a flattened FlattableLine2d.
417      * @param curve FlattableLine3d
418      * @param flattened PolyLine3d
419      * @param precision double
420      */
421     private static void verifyMaxDeviation(final Curve3d curve, final PolyLine3d flattened, final double precision)
422     {
423         int steps = 100;
424         for (int step = 0; step <= steps; step++)
425         {
426             double fraction = 1.0 * step / steps;
427             Point3d curvePoint = curve.getPoint(fraction);
428             Point3d polyLinePoint = flattened.closestPointOnPolyLine(curvePoint);
429             if (curvePoint.distance(polyLinePoint) > precision * FUDGE_FACTOR)
430             {
431                 printSituation(-1, 0.5, flattened, curve, fraction);
432                 curve.toPolyLine(new Flattener3d.MaxDeviation(precision));
433             }
434             assertEquals(0, curvePoint.distance(polyLinePoint), precision * FUDGE_FACTOR,
435                     "point on Curve3d is close to PolyLine2d");
436         }
437     }
438 
439     /**
440      * Print things for debugging.
441      * @param segment the step along the curve or the polyLine2d
442      * @param positionOnSegment double
443      * @param flattened PolyLine2d
444      * @param curve Object
445      * @param fraction double
446      * @param of ContinuousPiecewiseLinearFunction (may be null)
447      */
448     public static void printSituation(final int segment, final double positionOnSegment, final PolyLine2d flattened,
449             final Object curve, final double fraction, final ContinuousPiecewiseLinearFunction of)
450     {
451         System.out.println("# " + curve);
452         System.out.print(Export.toPlot(flattened));
453         if (null != of)
454         {
455             System.out.print("c0,1,0 "
456                     + Export.toPlot(((OffsetCurve2d) curve).toPolyLine(new OffsetFlattener2d.MaxDeviation(0.01), of)));
457         }
458         System.out.print("c0,1,1 " + Export.toPlot(((Curve2d) curve).toPolyLine(new Flattener2d.MaxDeviation(0.01))));
459         Point2d pointAtFraction =
460                 null != of ? ((OffsetCurve2d) curve).getPoint(fraction, of) : ((Curve2d) curve).getPoint(fraction);
461         double faDir =
462                 null != of ? ((OffsetCurve2d) curve).getDirection(fraction, of) : ((Curve2d) curve).getDirection(fraction);
463         System.out.print("# curveDirection=" + faDir);
464         if (segment >= 0)
465         {
466             double flattenedDir = flattened.get(segment).directionTo(flattened.get(segment + 1));
467             System.out.println(", segment direction=" + flattenedDir + " directionDifference=" + (flattenedDir - faDir));
468             System.out.println("# segment=" + segment + ", positionOnSegment=" + positionOnSegment);
469             System.out.println("sw0.1c1,0.6,0.6 " + Export.toPlot(flattened.getSegment(segment)) + " r");
470             LineSegment2d lineSegment = flattened.getSegment(segment);
471             Point2d closestPointOnSegment = lineSegment.closestPointOnSegment(pointAtFraction);
472             System.out.println("# closestPointOnSegment=" + closestPointOnSegment + " distance from pointAtFraction to segment="
473                     + pointAtFraction.distance(closestPointOnSegment));
474         }
475         else
476         {
477             System.out.println();
478         }
479         Point2d closestPointOnFlattened = flattened.closestPointOnPolyLine(pointAtFraction);
480         System.out.println("# fraction=" + fraction + " pointAtFraction=" + pointAtFraction + ", closestPointOnFlattened="
481                 + closestPointOnFlattened + ", distance=" + pointAtFraction.distance(closestPointOnFlattened));
482         System.out.print("# segments: ");
483         for (int i = 0; i < flattened.size() - 1; i++)
484         {
485             System.out.print(String.format("%s%3d%s ", i == segment ? "##" : "  ", i, i == segment ? "##" : "  "));
486         }
487         System.out.print("\n# angles:  ");
488         for (int i = 0; i < flattened.size() - 1; i++)
489         {
490             System.out.print(String.format(" %7.4f", flattened.get(i).directionTo(flattened.get(i + 1))));
491         }
492         System.out.print("\n# lengths: ");
493         for (int i = 0; i < flattened.size() - 1; i++)
494         {
495             System.out.print(String.format(" %7.4f", flattened.get(i).distance(flattened.get(i + 1))));
496         }
497         System.out.print("\n# x:    ");
498         for (int i = 0; i < flattened.size(); i++)
499         {
500             System.out.print(String.format(" %7.4f", flattened.get(i).x));
501         }
502         System.out.print("\n# y:    ");
503         for (int i = 0; i < flattened.size(); i++)
504         {
505             System.out.print(String.format(" %7.4f", flattened.get(i).y));
506         }
507         System.out.println("\nc0,0,1 M0,0L " + pointAtFraction.x + "," + pointAtFraction.y);
508         if (null != of)
509         {
510             System.out.print("# Knots in ofl2d domain:");
511             OffsetCurve2d ofl2d = (OffsetCurve2d) curve;
512             for (Iterator<ContinuousPiecewiseLinearFunction.TupleSt> iterator = of.iterator(); iterator.hasNext();)
513             {
514                 double knot = iterator.next().s();
515                 if (knot != 0.0 && knot != 1.0)
516                 {
517                     double t = ofl2d.getT(knot * ofl2d.getLength());
518                     System.out.println("\tknot at " + knot + " -> fraction " + t + " point " + ofl2d.getPoint(t, of));
519                 }
520             }
521         }
522 
523         System.out.println("break here");
524     }
525 
526     /**
527      * Print things for debugging.
528      * @param segment the step along the curve or the polyLine2d
529      * @param positionOnSegment double
530      * @param flattened PolyLine3d
531      * @param curve Curve3d
532      * @param fraction double
533      */
534     public static void printSituation(final int segment, final double positionOnSegment, final PolyLine3d flattened,
535             final Curve3d curve, final double fraction)
536     {
537         System.out.println("# " + curve);
538         System.out.print(Export.toPlot(flattened.project()));
539         System.out.print("c0,1,1 " + Export.toPlot(curve.toPolyLine(new Flattener3d.NumSegments(500)).project()));
540         Point3d pointAtFraction = curve.getPoint(fraction);
541         Direction3d faDir = curve.getDirection(fraction);
542         System.out.print("# curveDirection=" + faDir);
543         if (segment >= 0)
544         {
545             Direction3d flattenedDir = flattened.get(segment).directionTo(flattened.get(segment + 1));
546             System.out.println(
547                     ", segment direction=" + flattenedDir + " directionDifference=" + flattenedDir.directionDifference(faDir));
548             System.out.println("# segment=" + segment + ", positionOnSegment=" + positionOnSegment);
549             System.out.println("sw0.1c1,0.6,0.6 " + Export.toPlot(flattened.getSegment(segment).project()) + " r");
550             LineSegment3d lineSegment = flattened.getSegment(segment);
551             Point3d closestPointOnSegment = lineSegment.closestPointOnSegment(pointAtFraction);
552             System.out.println("# closestPointOnSegment=" + closestPointOnSegment + " distance from pointAtFraction to segment="
553                     + pointAtFraction.distance(closestPointOnSegment));
554         }
555         else
556         {
557             System.out.println();
558         }
559         Point3d closestPointOnFlattened = flattened.closestPointOnPolyLine(pointAtFraction);
560         System.out.println("# fraction=" + fraction + " pointAtFraction=" + pointAtFraction + "\n# closestPointOnFlattened="
561                 + closestPointOnFlattened + ", distance=" + pointAtFraction.distance(closestPointOnFlattened));
562         System.out.print("# segments: ");
563         for (int i = 0; i < flattened.size() - 1; i++)
564         {
565             System.out.print(String.format("%s%3d%s ", i == segment ? "##" : "  ", i, i == segment ? "##" : "  "));
566         }
567         System.out.print("\n# dirY:    ");
568         for (int i = 0; i < flattened.size() - 1; i++)
569         {
570             Direction3d segmentDirection = flattened.get(i).directionTo(flattened.get(i + 1));
571             System.out.print(String.format(" %7.4f", segmentDirection.dirY));
572         }
573         System.out.print("\n# dirZ:    ");
574         for (int i = 0; i < flattened.size() - 1; i++)
575         {
576             Direction3d segmentDirection = flattened.get(i).directionTo(flattened.get(i + 1));
577             System.out.print(String.format(" %7.4f", segmentDirection.dirZ));
578         }
579         System.out.print("\n# lengths: ");
580         for (int i = 0; i < flattened.size() - 1; i++)
581         {
582             System.out.print(String.format(" %7.4f", flattened.get(i).distance(flattened.get(i + 1))));
583         }
584         System.out.print("\n# x:    ");
585         for (int i = 0; i < flattened.size(); i++)
586         {
587             System.out.print(String.format(" %7.4f", flattened.get(i).x));
588         }
589         System.out.print("\n# y:    ");
590         for (int i = 0; i < flattened.size(); i++)
591         {
592             System.out.print(String.format(" %7.4f", flattened.get(i).y));
593         }
594         System.out.print("\n# z:    ");
595         for (int i = 0; i < flattened.size(); i++)
596         {
597             System.out.print(String.format(" %7.4f", flattened.get(i).z));
598         }
599         System.out.println("\nc0,0,1 M0,0L " + pointAtFraction.x + "," + pointAtFraction.y);
600 
601         System.out.println("break here");
602     }
603 
604     /**
605      * Verify that a flattened FlattableLine2d has matches direction with the flattableLine2d.
606      * @param flattened PolyLine2d
607      * @param curve FlattableLine2d
608      * @param anglePrecision double
609      */
610     public static void verifyMaxAngleDeviation(final PolyLine2d flattened, final Curve2d curve, final double anglePrecision)
611     {
612         double fraction = 0.0;
613         for (int step = 0; step < flattened.size() - 1; step++)
614         {
615             double flattenedDir = flattened.get(step).directionTo(flattened.get(step + 1));
616             for (double positionOnSegment : new double[] {0.1, 0.9})
617             {
618                 Point2d flattenPoint = flattened.get(step).interpolate(flattened.get(step + 1), positionOnSegment);
619                 // Find a fraction on fa2d that results in a point very close to flattenPoint
620                 double veryClose = 0.1 / flattened.size() / 5; // Don't know why that / 5 was needed
621                 // Use bisection to encroach on the fraction
622                 double highFraction = Math.min(1.0, fraction + Math.min(20.0 / flattened.size(), 0.5));
623                 while (highFraction - fraction > veryClose)
624                 {
625                     double midFraction = (fraction + highFraction) / 2;
626                     Point2d midPoint = curve.getPoint(midFraction);
627                     double dir = flattenPoint.directionTo(midPoint);
628                     double dirDifference = Math.abs(AngleUtil.normalizeAroundZero(flattenedDir - dir));
629                     if (dirDifference < Math.PI / 2)
630                     {
631                         highFraction = midFraction;
632                     }
633                     else
634                     {
635                         fraction = midFraction;
636                     }
637                 }
638                 double faDir = curve.getDirection(fraction);
639                 if (Math.abs(AngleUtil.normalizeAroundZero(flattenedDir - faDir)) > anglePrecision * FUDGE_FACTOR)
640                 {
641                     printSituation(step, positionOnSegment, flattened, curve, fraction, null);
642                     curve.toPolyLine(new Flattener2d.MaxAngle(anglePrecision));
643                 }
644                 assertEquals(0, AngleUtil.normalizeAroundZero(flattenedDir - faDir), anglePrecision * FUDGE_FACTOR,
645                         "direction difference should be less than anglePrecision");
646             }
647         }
648     }
649 
650     /**
651      * Verify that a flattened FlattableLine2d has no knots sharper than specified, except at the boundary points in the
652      * ContinuousPiecewiseLinearFunction.
653      * @param flattened PolyLine2d
654      * @param curve OffsetFlattableLine2d
655      * @param of ContinuousPiecewiseLinearFunction
656      * @param anglePrecision double
657      */
658     public static void verifyMaxAngleDeviation(final PolyLine2d flattened, final OffsetCurve2d curve,
659             final ContinuousPiecewiseLinearFunction of, final double anglePrecision)
660     {
661         double fraction = 0.0;
662         for (int step = 0; step < flattened.size() - 1; step++)
663         {
664             double flattenedDir = flattened.get(step).directionTo(flattened.get(step + 1));
665             for (double positionOnSegment : new double[] {0.1, 0.9})
666             {
667                 Point2d flattenPoint = flattened.get(step).interpolate(flattened.get(step + 1), positionOnSegment);
668                 // Find a fraction on fa2d that results in a point very close to flattenPoint
669                 double veryClose = 0.1 / flattened.size() / 5; // Don't know why that / 5 was needed
670                 // Use bisection to encroach on the fraction
671                 double highFraction = Math.min(1.0, fraction + 0.1);
672                 while (highFraction - fraction > veryClose)
673                 {
674                     double midFraction = (fraction + highFraction) / 2;
675                     Point2d midPoint = curve.getPoint(midFraction, of);
676                     double dir = flattenPoint.directionTo(midPoint);
677                     double dirDifference = Math.abs(AngleUtil.normalizeAroundZero(flattenedDir - dir));
678                     if (dirDifference < Math.PI / 2)
679                     {
680                         highFraction = midFraction;
681                     }
682                     else
683                     {
684                         fraction = midFraction;
685                     }
686                 }
687                 // Check if there is a knot very close to fraction
688                 Double knot = null;
689                 for (Iterator<ContinuousPiecewiseLinearFunction.TupleSt> iterator = of.iterator(); iterator.hasNext();)
690                 {
691                     knot = iterator.next().s();
692                     if (knot != 0.0 && knot != 1.0 && Math.abs(curve.getT(knot * curve.getLength()) - fraction) <= veryClose)
693                     {
694                         break;
695                     }
696                     knot = null;
697                 }
698                 if (knot == null)
699                 {
700                     double faDir = curve.getDirection(fraction, of);
701                     if (Math.abs(AngleUtil.normalizeAroundZero(flattenedDir - faDir)) > anglePrecision * FUDGE_FACTOR)
702                     {
703                         printSituation(step, positionOnSegment, flattened, curve, fraction, of);
704                         curve.getDirection(fraction, of);
705                         curve.toPolyLine(new OffsetFlattener2d.MaxAngle(anglePrecision), of);
706                     }
707                     assertEquals(0, AngleUtil.normalizeAroundZero(flattenedDir - faDir), anglePrecision * FUDGE_FACTOR,
708                             "direction difference should be less than anglePrecision");
709                 }
710             }
711         }
712     }
713 
714     /**
715      * Verify that a flattened FlattableLine2d has matches direction with the flattableLine2d.
716      * @param flattened PolyLine3d
717      * @param curve FlattableLine3d
718      * @param anglePrecision double
719      */
720     public static void verifyMaxAngleDeviation(final PolyLine3d flattened, final Curve3d curve, final double anglePrecision)
721     {
722         double fraction = 0.0;
723         for (int step = 0; step < flattened.size() - 1; step++)
724         {
725             Direction3d flattenedDir = flattened.get(step).directionTo(flattened.get(step + 1));
726             for (double positionOnSegment : new double[] {0.1, 0.9})
727             {
728                 Point3d flattenPoint = flattened.get(step).interpolate(flattened.get(step + 1), positionOnSegment);
729                 // Find a fraction on fa2d that results in a point very close to flattenPoint
730                 double veryClose = 0.1 / flattened.size() / 5; // Don't know why that / 5 was needed
731                 // Use bisection to encroach on the fraction
732                 double highFraction = Math.min(1.0, fraction + Math.min(20.0 / flattened.size(), 0.5));
733                 while (highFraction - fraction > veryClose)
734                 {
735                     double midFraction = (fraction + highFraction) / 2;
736                     Point3d midPoint = curve.getPoint(midFraction);
737                     Direction3d dir = flattenPoint.directionTo(midPoint);
738                     double dirDifference = flattenedDir.directionDifference(dir);
739                     if (dirDifference < Math.PI / 2)
740                     {
741                         highFraction = midFraction;
742                     }
743                     else
744                     {
745                         fraction = midFraction;
746                     }
747                 }
748                 Direction3d faDir = curve.getDirection(fraction);
749                 if (flattenedDir.directionDifference(faDir) > anglePrecision * FUDGE_FACTOR)
750                 {
751                     printSituation(step, positionOnSegment, flattened, curve, fraction);
752                 }
753                 assertEquals(0, flattenedDir.directionDifference(faDir), anglePrecision * FUDGE_FACTOR,
754                         "direction difference should be less than anglePrecision");
755             }
756         }
757     }
758 
759     /**
760      * Test the Bezier2d and BezierCubic2d classes.
761      */
762     @Test
763     public void testBezier2d()
764     {
765         NavigableMap<Double, Double> transition = new TreeMap<>();
766         transition.put(0.0, 0.0);
767         transition.put(0.2, 1.0);
768         transition.put(1.0, 2.0);
769         for (double x : new double[] {0, -1, -0.1, 10})
770         {
771             for (double y : new double[] {0, -3, -0.3, 30})
772             {
773                 for (double dirZ : new double[] {-3, -2, -1, 0, Math.PI / 2, 1, 2, 3})
774                 {
775                     Ray2d dp = new Ray2d(x, y, dirZ);
776                     for (double x2 : new double[] {10.5, 30})
777                     {
778                         for (double y2 : new double[] {30.5, 70})
779                         {
780                             for (double dirZ2 : new double[] {1, 0.1, 2.5, 5})
781                             {
782                                 Ray2d dp2 = new Ray2d(x2, y2, dirZ2);
783                                 BezierCubic2d cbc = new BezierCubic2d(dp, dp2);
784                                 assertEquals(x, cbc.getStartPoint().x, 0, "start x");
785                                 assertEquals(y, cbc.getStartPoint().y, 0, "start y");
786                                 assertEquals(dirZ, cbc.getStartPoint().dirZ, 0.00001, "start dirZ");
787                                 assertEquals(dirZ, cbc.getStartDirection(), 0.00001, "start direction");
788                                 assertEquals(x2, cbc.getEndPoint().x, 0.000001, "end x");
789                                 assertEquals(y2, cbc.getEndPoint().y, 0.000001, "end y");
790                                 assertEquals(AngleUtil.normalizeAroundZero(dirZ2), cbc.getEndPoint().dirZ, 0.00001, "end dirZ");
791                                 assertEquals(AngleUtil.normalizeAroundZero(dirZ2), cbc.getEndDirection(), 0.00001,
792                                         "end direction");
793                                 // Test the NumSegments flattener without offsets
794                                 PolyLine2d flattened = cbc.toPolyLine(new Flattener2d.NumSegments(20));
795                                 verifyNumSegments(cbc, flattened, 20);
796                                 // Test the MaxDeviation flattener without offsets
797                                 double precision = 0.1;
798                                 flattened = cbc.toPolyLine(new Flattener2d.MaxDeviation(precision));
799                                 verifyMaxDeviation(cbc, flattened, precision);
800                                 double anglePrecision = 0.01;
801                                 double meanDir = dp.directionTo(dp2);
802                                 if (Math.abs(AngleUtil.normalizeAroundZero(meanDir - dirZ)) < 2.5
803                                         && Math.abs(AngleUtil.normalizeAroundZero(meanDir - dirZ2)) < 2.5)
804                                 {
805                                     // Test the MaxAngle flattener without offsets
806                                     flattened = cbc.toPolyLine(new Flattener2d.MaxAngle(anglePrecision));
807                                     verifyMaxAngleDeviation(flattened, cbc, anglePrecision);
808                                     // Test the MaxDeviationAndAngle flattener without offsets
809                                     flattened = cbc.toPolyLine(new Flattener2d.MaxDeviationAndAngle(precision, anglePrecision));
810                                     verifyMaxDeviation(cbc, flattened, precision);
811                                     verifyMaxAngleDeviation(flattened, cbc, anglePrecision);
812                                 }
813                                 // Only check transitions for radius of arc > 2 and length of arc > 2
814                                 if (cbc.getStartRadius() > 2 && cbc.getLength() > 2)
815                                 {
816                                     ContinuousPiecewiseLinearFunction of = new ContinuousPiecewiseLinearFunction(transition);
817                                     // Test the NumSegments flattener with offsets
818                                     flattened = cbc.toPolyLine(new OffsetFlattener2d.NumSegments(30), of);
819                                     verifyNumSegments(cbc, of, flattened, 30);
820                                     // Test the MaxDeviation flattener with offsets
821                                     flattened = cbc.toPolyLine(new OffsetFlattener2d.MaxDeviation(precision), of);
822                                     if (Math.abs(AngleUtil.normalizeAroundZero(meanDir - dirZ)) < 2
823                                             && Math.abs(AngleUtil.normalizeAroundZero(meanDir - dirZ2)) < 2)
824                                     {
825                                         // Test with offsets only for shapes that we expect to be smooth
826                                         verifyMaxDeviation(cbc, of, flattened, precision);
827                                         flattened = cbc.toPolyLine(new OffsetFlattener2d.MaxAngle(anglePrecision), of);
828                                         verifyMaxAngleDeviation(flattened, cbc, of, anglePrecision);
829                                         // Test the MaxDeviationAndAngle flattener with offsets
830                                         flattened = cbc.toPolyLine(
831                                                 new OffsetFlattener2d.MaxDeviationAndAngle(precision, anglePrecision), of);
832                                         verifyMaxDeviation(cbc, of, flattened, precision);
833                                         verifyMaxAngleDeviation(flattened, cbc, of, anglePrecision);
834                                     }
835                                 }
836                             }
837                         }
838                     }
839                 }
840             }
841         }
842     }
843 
844     /**
845      * Check the startRadius and endRadius of CubicBezier2d and getT.
846      */
847     @Test
848     public void testCubicbezierRadiusAndSome()
849     {
850         // Check that the curvature functions return something sensible
851         // https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves
852         double controlDistance = (4.0 / 3) * Math.tan(Math.PI / 8);
853         BezierCubic2d bcb = new BezierCubic2d(new Point2d(1, 0), new Point2d(1, controlDistance),
854                 new Point2d(controlDistance, 1), new Point2d(0, 1));
855         assertEquals(1.0, bcb.getStartRadius(), 0.03, "start radius of cubic bezier approximation of unit circle");
856         assertEquals(1.0, bcb.getEndRadius(), 0.03, "end radius of cubic bezier approximation of unit circle");
857         bcb = new BezierCubic2d(new Point2d(1, 0), new Point2d(1, -controlDistance), new Point2d(controlDistance, -1),
858                 new Point2d(0, -1));
859         assertEquals(-1.0, bcb.getStartRadius(), 0.03, "start radius of cubic bezier approximation of unit circle");
860         assertEquals(-1.0, bcb.getEndRadius(), 0.03, "end radius of cubic bezier approximation of unit circle");
861         assertEquals(0.0, bcb.getT(0.0), 0.0, "getT is exact at 0.0");
862         assertEquals(0.0, bcb.getT(0.001 * bcb.getLength()), 0.1, "getT is close to 0.0 for small input");
863         assertEquals(0.5, bcb.getT(0.5 * bcb.getLength()), 0.01, "getT is close to 0.5 halfway on symmetrical Bezier");
864         assertEquals(1.0, bcb.getT(0.999 * bcb.getLength()), 0.1, "getT is close to 1.0 for input close to 1.0");
865         assertEquals(1.0, bcb.getT(bcb.getLength()), 0.0, "getT is exact at 1.0");
866 
867         try
868         {
869             bcb.split(-0.0001);
870             fail("Negative split point should have thrown IllegalArgumentException");
871         }
872         catch (IllegalArgumentException iae)
873         {
874             // Ignore expected exception
875         }
876 
877         try
878         {
879             bcb.split(1.0001);
880             fail("Split point beyond 1.0 should have thrown IllegalArgumentException");
881         }
882         catch (IllegalArgumentException iae)
883         {
884             // Ignore expected exception
885         }
886 
887     }
888 
889     /**
890      * Test the Bezier3d and CubicBezier3d classes.
891      */
892     @Test
893     public void testBezier3d()
894     {
895         NavigableMap<Double, Double> transition = new TreeMap<>();
896         transition.put(0.0, 0.0);
897         transition.put(0.2, 1.0);
898         transition.put(1.0, 2.0);
899         for (double x : new double[] {0, 10})
900         {
901             for (double y : new double[] {0, 30})
902             {
903                 for (double z : new double[] {0, 5})
904                 {
905                     for (double dirY : new double[] {Math.PI / 2, 0.3, 3})
906                     {
907                         for (double dirZ : new double[] {0, -2, Math.PI / 2, 2})
908                         {
909                             Ray3d dp = new Ray3d(x, y, z, dirY, dirZ);
910                             for (double x2 : new double[] {10.5, 30})
911                             {
912                                 for (double y2 : new double[] {30.5, 70})
913                                 {
914                                     for (double z2 : new double[] {3, 6})
915                                     {
916                                         for (double dirY2 : new double[] {Math.PI / 2, 1, 2.5})
917                                         {
918                                             for (double dirZ2 : new double[] {0, 0.1, 2.5, 5})
919                                             {
920                                                 Ray3d dp2 = new Ray3d(x2, y2, z2, dirY2, dirZ2);
921                                                 BezierCubic3d cbc = new BezierCubic3d(dp, dp2);
922                                                 assertEquals(x, cbc.getStartPoint().x, 0, "start x");
923                                                 assertEquals(y, cbc.getStartPoint().y, 0, "start y");
924                                                 assertEquals(z, cbc.getStartPoint().z, 0, "start y");
925                                                 assertEquals(dirZ, cbc.getStartPoint().dirZ, 0.0001, "start dirZ");
926                                                 assertEquals(dirY, cbc.getStartDirection().dirY, 0.0001, "start direction");
927                                                 assertEquals(dirZ, cbc.getStartDirection().dirZ, 0.0001, "start direction");
928                                                 assertEquals(x2, cbc.getEndPoint().x, 0.000001, "end x");
929                                                 assertEquals(y2, cbc.getEndPoint().y, 0.000001, "end y");
930                                                 assertEquals(z2, cbc.getEndPoint().z, 0.000001, "end y");
931                                                 assertEquals(AngleUtil.normalizeAroundZero(dirY2), cbc.getEndPoint().dirY,
932                                                         0.00001, "end dirY");
933                                                 assertEquals(AngleUtil.normalizeAroundZero(dirY2), cbc.getEndDirection().dirY,
934                                                         0.00001, "end dirY");
935                                                 assertEquals(AngleUtil.normalizeAroundZero(dirZ2), cbc.getEndPoint().dirZ,
936                                                         0.00001, "end dirZ");
937                                                 assertEquals(AngleUtil.normalizeAroundZero(dirZ2), cbc.getEndDirection().dirZ,
938                                                         0.00001, "end dirZ");
939                                                 // Test the NumSegments flattener
940                                                 PolyLine3d flattened = cbc.toPolyLine(new Flattener3d.NumSegments(20));
941                                                 verifyNumSegments(cbc, flattened, 20);
942                                                 // Test the MaxDeviation flattener
943                                                 double precision = 0.1;
944                                                 flattened = cbc.toPolyLine(new Flattener3d.MaxDeviation(precision));
945                                                 verifyMaxDeviation(cbc, flattened, precision);
946                                                 // Only verify angles for curves that are not too crooked.
947                                                 Direction3d meanDir = dp.directionTo(dp2);
948                                                 if (dp.getDir().directionDifference(dp2.getDir()) < 2
949                                                         && dp.getDir().directionDifference(meanDir) < 2
950                                                         && dp2.getDir().directionDifference(meanDir) < 2)
951                                                 {
952                                                     // System.out.println("dirDiff="
953                                                     // + dp.getDir().directionDifference(dp2.getDir()) + " " + cbc);
954                                                     double anglePrecision = 0.01;
955                                                     // Test the MaxAngle flattener without offsets
956                                                     flattened = cbc.toPolyLine(new Flattener3d.MaxAngle(anglePrecision));
957                                                     verifyMaxAngleDeviation(flattened, cbc, anglePrecision);
958                                                     // Test the MaxDeviationAndAngle flattener without offsets
959                                                     flattened = cbc.toPolyLine(
960                                                             new Flattener3d.MaxDeviationAndAngle(precision, anglePrecision));
961                                                     verifyMaxDeviation(cbc, flattened, precision);
962                                                     verifyMaxAngleDeviation(flattened, cbc, anglePrecision);
963                                                 }
964                                             }
965                                         }
966                                     }
967                                 }
968                             }
969                         }
970                     }
971                 }
972             }
973         }
974     }
975 
976     /**
977      * Test the various exceptions of the flatteners.
978      */
979     @Test
980     public void testFlattenerExceptions()
981     {
982         for (int badAmount : new int[] {0, -1})
983         {
984             try
985             {
986                 new Flattener2d.NumSegments(badAmount);
987                 fail("fewer than 1 segments should have thrown an IllegalArgumentException");
988             }
989             catch (IllegalArgumentException e)
990             {
991                 // Ignore expected exception
992             }
993 
994             try
995             {
996                 new OffsetFlattener2d.NumSegments(badAmount);
997                 fail("fewer than 1 segments should have thrown an IllegalArgumentException");
998             }
999             catch (IllegalArgumentException e)
1000             {
1001                 // Ignore expected exception
1002             }
1003 
1004             try
1005             {
1006                 new Flattener3d.NumSegments(badAmount);
1007                 fail("fewer than 1 segments should have thrown an IllegalArgumentException");
1008             }
1009             catch (IllegalArgumentException e)
1010             {
1011                 // Ignore expected exception
1012             }
1013         }
1014         for (double badAmount : new double[] {0.0, -0.1})
1015         {
1016             try
1017             {
1018                 new Flattener2d.MaxAngle(badAmount);
1019                 fail("angle tolerance <= 0 should have thrown an IllegalArgumentException");
1020             }
1021             catch (IllegalArgumentException e)
1022             {
1023                 // Ignore expected exception
1024             }
1025 
1026             try
1027             {
1028                 new OffsetFlattener2d.MaxAngle(badAmount);
1029                 fail("angle tolerance <= 0 should have thrown an IllegalArgumentException");
1030             }
1031             catch (IllegalArgumentException e)
1032             {
1033                 // Ignore expected exception
1034             }
1035 
1036             try
1037             {
1038                 new Flattener3d.MaxAngle(badAmount);
1039                 fail("angle tolerance <= 0 should have thrown an IllegalArgumentException");
1040             }
1041             catch (IllegalArgumentException e)
1042             {
1043                 // Ignore expected exception
1044             }
1045 
1046             try
1047             {
1048                 new Flattener2d.MaxDeviationAndAngle(1.0, badAmount);
1049                 fail("angle tolerance <= 0 should have thrown an IllegalArgumentException");
1050             }
1051             catch (IllegalArgumentException e)
1052             {
1053                 // Ignore expected exception
1054             }
1055 
1056             try
1057             {
1058                 new OffsetFlattener2d.MaxDeviationAndAngle(1.0, badAmount);
1059                 fail("angle tolerance <= 0 should have thrown an IllegalArgumentException");
1060             }
1061             catch (IllegalArgumentException e)
1062             {
1063                 // Ignore expected exception
1064             }
1065 
1066             try
1067             {
1068                 new Flattener3d.MaxDeviationAndAngle(1.0, badAmount);
1069                 fail("angle tolerance <= 0 should have thrown an IllegalArgumentException");
1070             }
1071             catch (IllegalArgumentException e)
1072             {
1073                 // Ignore expected exception
1074             }
1075 
1076             try
1077             {
1078                 new Flattener2d.MaxDeviationAndAngle(badAmount, 0.1);
1079                 fail("deviation tolerance <= 0 should have thrown an IllegalArgumentException");
1080             }
1081             catch (IllegalArgumentException e)
1082             {
1083                 // Ignore expected exception
1084             }
1085 
1086             try
1087             {
1088                 new OffsetFlattener2d.MaxDeviationAndAngle(badAmount, 0.1);
1089                 fail("deviation tolerance <= 0 should have thrown an IllegalArgumentException");
1090             }
1091             catch (IllegalArgumentException e)
1092             {
1093                 // Ignore expected exception
1094             }
1095 
1096             try
1097             {
1098                 new Flattener3d.MaxDeviationAndAngle(badAmount, 0.1);
1099                 fail("deviation tolerance <= 0 should have thrown an IllegalArgumentException");
1100             }
1101             catch (IllegalArgumentException e)
1102             {
1103                 // Ignore expected exception
1104             }
1105 
1106             try
1107             {
1108                 new Flattener2d.MaxDeviation(badAmount);
1109                 fail("deviation tolerance <= 0 should have thrown an IllegalArgumentException");
1110             }
1111             catch (IllegalArgumentException e)
1112             {
1113                 // Ignore expected exception
1114             }
1115 
1116             try
1117             {
1118                 new OffsetFlattener2d.MaxDeviation(badAmount);
1119                 fail("deviation tolerance <= 0 should have thrown an IllegalArgumentException");
1120             }
1121             catch (IllegalArgumentException e)
1122             {
1123                 // Ignore expected exception
1124             }
1125 
1126             try
1127             {
1128                 new Flattener3d.MaxDeviation(badAmount);
1129                 fail("deviation tolerance <= 0 should have thrown an IllegalArgumentException");
1130             }
1131             catch (IllegalArgumentException e)
1132             {
1133                 // Ignore expected exception
1134             }
1135 
1136         }
1137         try
1138         {
1139             new Flattener2d.MaxAngle(Double.NaN);
1140             fail("angle tolerance NaN should have thrown an ArithmeticException");
1141         }
1142         catch (ArithmeticException e)
1143         {
1144             // Ignore expected exception
1145         }
1146 
1147         try
1148         {
1149             new OffsetFlattener2d.MaxAngle(Double.NaN);
1150             fail("angle tolerance NaN should have thrown an ArithmeticException");
1151         }
1152         catch (ArithmeticException e)
1153         {
1154             // Ignore expected exception
1155         }
1156 
1157         try
1158         {
1159             new Flattener3d.MaxAngle(Double.NaN);
1160             fail("angle tolerance NaN should have thrown an ArithmeticException");
1161         }
1162         catch (ArithmeticException e)
1163         {
1164             // Ignore expected exception
1165         }
1166 
1167         try
1168         {
1169             new Flattener2d.MaxDeviationAndAngle(1.0, Double.NaN);
1170             fail("angle tolerance NaN should have thrown an ArithmeticException");
1171         }
1172         catch (ArithmeticException e)
1173         {
1174             // Ignore expected exception
1175         }
1176 
1177         try
1178         {
1179             new OffsetFlattener2d.MaxDeviationAndAngle(1.0, Double.NaN);
1180             fail("angle tolerance NaN should have thrown an ArithmeticException");
1181         }
1182         catch (ArithmeticException e)
1183         {
1184             // Ignore expected exception
1185         }
1186 
1187         try
1188         {
1189             new Flattener3d.MaxDeviationAndAngle(1.0, Double.NaN);
1190             fail("angle tolerance NaN should have thrown an ArithmeticException");
1191         }
1192         catch (ArithmeticException e)
1193         {
1194             // Ignore expected exception
1195         }
1196 
1197         try
1198         {
1199             new Flattener2d.MaxDeviationAndAngle(Double.NaN, 0.1);
1200             fail("angle tolerance NaN should have thrown an ArithmeticException");
1201         }
1202         catch (ArithmeticException e)
1203         {
1204             // Ignore expected exception
1205         }
1206 
1207         try
1208         {
1209             new OffsetFlattener2d.MaxDeviationAndAngle(Double.NaN, 0.1);
1210             fail("angle tolerance NaN should have thrown an ArithmeticException");
1211         }
1212         catch (ArithmeticException e)
1213         {
1214             // Ignore expected exception
1215         }
1216 
1217         try
1218         {
1219             new Flattener3d.MaxDeviationAndAngle(Double.NaN, 0.1);
1220             fail("angle tolerance NaN should have thrown an ArithmeticException");
1221         }
1222         catch (ArithmeticException e)
1223         {
1224             // Ignore expected exception
1225         }
1226 
1227         try
1228         {
1229             new Flattener2d.MaxDeviation(Double.NaN);
1230             fail("deviation tolerance NaN should have thrown an ArithmeticException");
1231         }
1232         catch (ArithmeticException e)
1233         {
1234             // Ignore expected exception
1235         }
1236 
1237         try
1238         {
1239             new OffsetFlattener2d.MaxDeviation(Double.NaN);
1240             fail("deviation tolerance NaN should have thrown an ArithmeticException");
1241         }
1242         catch (ArithmeticException e)
1243         {
1244             // Ignore expected exception
1245         }
1246 
1247         try
1248         {
1249             new Flattener3d.MaxDeviation(Double.NaN);
1250             fail("deviation tolerance NaN should have thrown an ArithmeticException");
1251         }
1252         catch (ArithmeticException e)
1253         {
1254             // Ignore expected exception
1255         }
1256 
1257         try
1258         {
1259             new Bezier3d(new double[] {}, new double[] {}, new double[] {});
1260             fail("No points for a Bezier3d should have thrown an IllegalArgumentException");
1261         }
1262         catch (IllegalArgumentException e)
1263         {
1264             // Ignore expected exception
1265         }
1266 
1267         try
1268         {
1269             new Bezier3d(new double[] {1, 2, 3}, new double[] {2, 3, 4}, new double[] {3, 4});
1270             fail("Non equal length arrays for a Bezier3d should have thrown an IllegalArgumentException");
1271         }
1272         catch (IllegalArgumentException e)
1273         {
1274             // Ignore expected exception
1275         }
1276 
1277         try
1278         {
1279             new Bezier3d(new double[] {1, 2, 3}, new double[] {2, 3}, new double[] {3, 4, 5});
1280             fail("Non equal length arrays for a Bezier3d should have thrown an IllegalArgumentException");
1281         }
1282         catch (IllegalArgumentException e)
1283         {
1284             // Ignore expected exception
1285         }
1286     }
1287 
1288     /**
1289      * Test the various constructors of Bezier2d.
1290      */
1291     @Test
1292     public void testBezier2dConstructors()
1293     {
1294         try
1295         {
1296             new Bezier2d(new double[] {1, 2}, new double[] {2, 3, 4});
1297             fail("Non equal length arrays for a Bezier2d should have thrown an IllegalArgumentException");
1298         }
1299         catch (IllegalArgumentException e)
1300         {
1301             // Ignore expected exception
1302         }
1303 
1304         try
1305         {
1306             new Bezier2d(new Point2d(1, 2));
1307             fail("Too short array of Point2s for a Bezier2d should have thrown an IllegalArgumentException");
1308         }
1309         catch (IllegalArgumentException e)
1310         {
1311             // Ignore expected exception
1312         }
1313 
1314         try
1315         {
1316             new Bezier2d(new double[] {1}, new double[] {2});
1317             fail("Too short arrays for a Bezier2d should have thrown an IllegalArgumentException");
1318         }
1319         catch (IllegalArgumentException e)
1320         {
1321             // Ignore expected exception
1322         }
1323 
1324         new Bezier2d(new double[] {1, 2}, new double[] {2, 3}); // Should succeed
1325 
1326         Bezier2d b2d = new Bezier2d(new Point2d(1, 2), new Point2d(12, 13));
1327         assertEquals(2, b2d.size(), "Size is reported");
1328         assertEquals(1, b2d.getX(0), "x[0]");
1329         assertEquals(12, b2d.getX(1), "x[1]");
1330         assertEquals(2, b2d.getY(0), "y[0]");
1331         assertEquals(13, b2d.getY(1), "y[1]");
1332         assertEquals(Math.sqrt(2 * 11 * 11), b2d.getLength(), 0.00001, "Length is reported");
1333         assertEquals(Math.sqrt(2 * 11 * 11), b2d.getLength(), 0.00001, "Length is reported from the cache");
1334         assertTrue(b2d.toString().startsWith("Bezier2d ["), "toString returns something descriptive");
1335         assertEquals(Math.sqrt(11 * 11 + 11 * 11), b2d.getLength(), 0.0001, "Length of 2-point (degenerate) Bezier");
1336         Bezier2d derivative = b2d.derivative();
1337         assertEquals(0, derivative.getLength(), 0.0, "Length of 1st derivative");
1338         Bezier2d derivative2 = derivative.derivative();
1339         assertEquals(0, derivative2.getLength(), 0.0, "Length of 2nd derivative");
1340         Bezier2d derivative3 = derivative2.derivative();
1341         assertEquals(derivative2, derivative3, "No more change");
1342         // Hash code and equals
1343         assertTrue(b2d.equals(b2d));
1344         assertFalse(b2d.equals(null));
1345         assertFalse(b2d.equals("not a bezier"));
1346         assertFalse(b2d.equals(new Bezier2d(new Point2d(1, 2), new Point2d(12, 14))));
1347         assertFalse(b2d.equals(new Bezier2d(new Point2d(3, 2), new Point2d(12, 13))));
1348         assertTrue(b2d.equals(new Bezier2d(new Point2d(1, 2), new Point2d(12, 13))));
1349         assertNotEquals(b2d.hashCode(), new Bezier2d(new Point2d(1, 2), new Point2d(12, 14)).hashCode());
1350         assertNotEquals(b2d.hashCode(), new Bezier2d(new Point2d(3, 2), new Point2d(12, 13)).hashCode());
1351     }
1352 
1353     /**
1354      * Test the various constructors of Bezier3d.
1355      */
1356     @Test
1357     public void testBezier3dConstructors()
1358     {
1359         try
1360         {
1361             new Bezier3d(new double[] {1, 2}, new double[] {2, 3, 4}, new double[] {3, 4, 5});
1362             fail("Non equal length arrays for a Bezier3d should have thrown an IllegalArgumentException");
1363         }
1364         catch (IllegalArgumentException e)
1365         {
1366             // Ignore expected exception
1367         }
1368 
1369         Bezier3d b3d = new Bezier3d(new Point3d(1, 2, 3), new Point3d(12, 13, 14));
1370         assertEquals(2, b3d.size(), "Size is reported");
1371         assertEquals(1, b3d.getX(0), "x[0]");
1372         assertEquals(12, b3d.getX(1), "x[1]");
1373         assertEquals(2, b3d.getY(0), "y[0]");
1374         assertEquals(13, b3d.getY(1), "y[1]");
1375         assertEquals(3, b3d.getZ(0), "z[0]");
1376         assertEquals(14, b3d.getZ(1), "z[1]");
1377         assertEquals(Math.sqrt(3 * 11 * 11), b3d.getLength(), 0.00001, "Length is reported");
1378         assertEquals(Math.sqrt(3 * 11 * 11), b3d.getLength(), 0.00001, "Length is reported from the cache");
1379         assertTrue(b3d.toString().startsWith("Bezier3d ["), "toString returns something descriptive");
1380         assertEquals(Math.sqrt(11 * 11 + 11 * 11 + 11 * 11), b3d.getLength(), 0.0001, "Length of 2-point (degenerate) Bezier");
1381         Bezier3d derivative = b3d.derivative();
1382         assertEquals(0, derivative.getLength(), 0.0, "Length of 1st derivative");
1383         Bezier3d derivative2 = derivative.derivative();
1384         assertEquals(0, derivative2.getLength(), 0.0, "Length of 2nd derivative");
1385         Bezier3d derivative3 = derivative2.derivative();
1386         assertEquals(derivative2, derivative3, "No more change");
1387         // Hash code and equals
1388         assertTrue(b3d.equals(b3d));
1389         assertFalse(b3d.equals(null));
1390         assertFalse(b3d.equals("not a bezier"));
1391         assertFalse(b3d.equals(new Bezier3d(new Point3d(1, 2, 3), new Point3d(12, 14, 14))));
1392         assertFalse(b3d.equals(new Bezier3d(new Point3d(3, 2, 3), new Point3d(12, 13, 14))));
1393         assertFalse(b3d.equals(new Bezier3d(new Point3d(1, 2, 5), new Point3d(12, 13, 14))));
1394         assertTrue(b3d.equals(new Bezier3d(new Point3d(1, 2, 3), new Point3d(12, 13, 14))));
1395         assertNotEquals(b3d.hashCode(), new Bezier3d(new Point3d(1, 2, 3), new Point3d(12, 14, 14)).hashCode());
1396         assertNotEquals(b3d.hashCode(), new Bezier3d(new Point3d(3, 2, 3), new Point3d(12, 13, 14)).hashCode());
1397         assertNotEquals(b3d.hashCode(), new Bezier3d(new Point3d(1, 2, 5), new Point3d(12, 13, 14)).hashCode());
1398     }
1399 
1400 }