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