View Javadoc
1   package org.djutils.draw.curve;
2   
3   import org.djutils.draw.function.ContinuousPiecewiseLinearFunction;
4   import org.djutils.draw.line.PolyLine2d;
5   import org.djutils.draw.point.DirectedPoint2d;
6   import org.djutils.draw.point.Point2d;
7   import org.djutils.exceptions.Throw;
8   import org.djutils.exceptions.Try;
9   import org.djutils.math.AngleUtil;
10  
11  /**
12   * Continuous definition of a clothoid in 2d. The following definitions are available:
13   * <ul>
14   * <li>A clothoid between two <code>DirectedPoint2d</code>s.</li>
15   * <li>A clothoid originating from a <code>DirectedPoint2d</code> with start curvature, end curvature, and <code>length</code>
16   * specified.</li>
17   * <li>A clothoid originating from a <code>DirectedPoint2d</code> with start curvature, end curvature, and <code>A-value</code>
18   * specified.</li>
19   * </ul>
20   * This class is based on:
21   * <ul>
22   * <li>Dale Connor and Lilia Krivodonova (2014) "Interpolation of two-dimensional curves with Euler spirals", Journal of
23   * Computational and Applied Mathematics, Volume 261, 1 May 2014, pp. 320-332.</li>
24   * <li>D.J. Waltona and D.S. Meek (2009) "G<sup>1</sup> interpolation with a single Cornu spiral segment", Journal of
25   * Computational and Applied Mathematics, Volume 223, Issue 1, 1 January 2009, pp. 86-96.</li>
26   * </ul>
27   * <p>
28   * Copyright (c) 2023-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
29   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
30   * </p>
31   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
32   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
33   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
34   * @see <a href="https://www.sciencedirect.com/science/article/pii/S0377042713006286">Connor and Krivodonova (2014)</a>
35   * @see <a href="https://www.sciencedirect.com/science/article/pii/S0377042704000925">Waltona and Meek (2009)</a>
36   */
37  public class Clothoid2d implements Curve2d, OffsetCurve2d
38  {
39  
40      /** Threshold to consider input to be a trivial straight or circle arc. The value is 1/10th of a degree. */
41      private static final double ANGLE_TOLERANCE = 2.0 * Math.PI / 3600.0;
42  
43      /** Stopping tolerance for the Secant method to find optimal theta values. */
44      private static final double SECANT_TOLERANCE = 1e-8;
45  
46      /** Start point with direction. */
47      private final DirectedPoint2d startPoint;
48  
49      /** End point with direction. */
50      private final DirectedPoint2d endPoint;
51  
52      /** Start curvature. */
53      private final double startCurvature;
54  
55      /** End curvature. */
56      private final double endCurvature;
57  
58      /** Length. */
59      private final double length;
60  
61      /**
62       * A-value; for scaling the Fresnel integral. The regular clothoid A-parameter is obtained by dividing by
63       * {@code Math.sqrt(Math.PI)}.
64       */
65      private final double a;
66  
67      /** Minimum alpha value of line to draw. */
68      private final double alphaMin;
69  
70      /** Maximum alpha value of line to draw. */
71      private final double alphaMax;
72  
73      /** Unit vector from the origin of the clothoid, towards the positive side. */
74      private final double[] t0;
75  
76      /** Normal unit vector to t0. */
77      private final double[] n0;
78  
79      /** Whether the line needs to be flipped. */
80      private final boolean opposite;
81  
82      /** Whether the line is reflected. */
83      private final boolean reflected;
84  
85      /** Simplification to straight when valid. */
86      private final Straight2d straight;
87  
88      /** Simplification to arc when valid. */
89      private final Arc2d arc;
90  
91      /** Whether the shift was determined. */
92      private boolean shiftDetermined;
93  
94      /** Shift in x-coordinate of start point. */
95      private double shiftX;
96  
97      /** Shift in y-coordinate of start point. */
98      private double shiftY;
99  
100     /** Additional shift in x-coordinate towards end point. */
101     private double dShiftX;
102 
103     /** Additional shift in y-coordinate towards end point. */
104     private double dShiftY;
105 
106     /**
107      * Create clothoid between two directed points. This constructor is based on the procedure in:<br>
108      * Dale Connor and Lilia Krivodonova (2014) "Interpolation of two-dimensional curves with Euler spirals", Journal of
109      * Computational and Applied Mathematics, Volume 261, 1 May 2014, pp. 320-332.<br>
110      * Which applies the theory proven in:<br>
111      * D.J. Waltona and D.S. Meek (2009) "G<sup>1</sup> interpolation with a single Cornu spiral segment", Journal of
112      * Computational and Applied Mathematics, Volume 223, Issue 1, 1 January 2009, pp. 86-96.<br>
113      * This procedure guarantees that the resulting line has the minimal angle rotation that is required to connect the points.
114      * If the points approximate a straight line or circle, with a tolerance of up 1/10th of a degree, those respective lines
115      * are created. The numerical approximation of the underlying Fresnel integral is different from the paper. See
116      * {@code Clothoid.fresnel()}.
117      * @param startPoint start point
118      * @param endPoint end point
119      * @throws NullPointerException when <code>startPoint</code>, or <code>endPoint</code> is <code>null</code>
120      * @see <a href="https://www.sciencedirect.com/science/article/pii/S0377042713006286">Connor and Krivodonova (2014)</a>
121      * @see <a href="https://www.sciencedirect.com/science/article/pii/S0377042704000925">Waltona and Meek (2009)</a>
122      */
123     public Clothoid2d(final DirectedPoint2d startPoint, final DirectedPoint2d endPoint)
124     {
125         Throw.whenNull(startPoint, "startPoint");
126         Throw.whenNull(endPoint, "endPoint");
127         this.startPoint = startPoint;
128         this.endPoint = endPoint;
129 
130         double dx = endPoint.x - startPoint.x;
131         double dy = endPoint.y - startPoint.y;
132         double d2 = Math.hypot(dx, dy); // length of straight line from start to end
133         double d = Math.atan2(dy, dx); // angle of line through start and end points
134 
135         double phi1 = AngleUtil.normalizeAroundZero(d - startPoint.dirZ);
136         double phi2 = AngleUtil.normalizeAroundZero(endPoint.dirZ - d);
137         double phi1Abs = Math.abs(phi1);
138         double phi2Abs = Math.abs(phi2);
139 
140         if (phi1Abs < ANGLE_TOLERANCE && phi2Abs < ANGLE_TOLERANCE)
141         {
142             // Straight
143             this.length = Math.hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
144             this.a = Double.POSITIVE_INFINITY;
145             this.startCurvature = 0.0;
146             this.endCurvature = 0.0;
147             this.straight = new Straight2d(startPoint, this.length);
148             this.arc = null;
149             this.alphaMin = 0.0;
150             this.alphaMax = 0.0;
151             this.t0 = null;
152             this.n0 = null;
153             this.opposite = false;
154             this.reflected = false;
155             return;
156         }
157         else if (Math.abs(phi2 - phi1) < ANGLE_TOLERANCE)
158         {
159             // Arc
160             double r = .5 * d2 / Math.sin(phi1);
161             double cosStartDirection = Math.cos(startPoint.dirZ);
162             double sinStartDirection = Math.sin(startPoint.dirZ);
163             double ang = Math.PI / 2.0;
164             double cosAng = Math.cos(ang); // =0
165             double sinAng = Math.sin(ang); // =1
166             double x0 = startPoint.x - r * (cosStartDirection * cosAng + sinStartDirection * sinAng);
167             double y0 = startPoint.y - r * (cosStartDirection * -sinAng + sinStartDirection * cosAng);
168             double from = Math.atan2(startPoint.y - y0, startPoint.x - x0);
169             double to = Math.atan2(endPoint.y - y0, endPoint.x - x0);
170             if (r < 0 && to > from)
171             {
172                 to = to - 2.0 * Math.PI;
173             }
174             else if (r > 0 && to < from)
175             {
176                 to = to + 2.0 * Math.PI;
177             }
178             double angle = Math.abs(to - from);
179             this.length = angle * Math.abs(r);
180             this.a = 0.0;
181             this.startCurvature = 1.0 / r;
182             this.endCurvature = 1.0 / r;
183             this.straight = null;
184             this.arc = new Arc2d(startPoint, Math.abs(r), r > 0.0, angle);
185             this.alphaMin = 0.0;
186             this.alphaMax = 0.0;
187             this.t0 = null;
188             this.n0 = null;
189             this.opposite = false;
190             this.reflected = false;
191             return;
192         }
193         this.straight = null;
194         this.arc = null;
195 
196         // The algorithm assumes |phi2| to be larger than |phi1|. If this is not the case, the clothoid is created in the
197         // opposite direction.
198         if (phi2Abs < phi1Abs)
199         {
200             this.opposite = true;
201             double phi3 = phi1;
202             phi1 = -phi2;
203             phi2 = -phi3;
204             dx = -dx;
205             dy = -dy;
206         }
207         else
208         {
209             this.opposite = false;
210         }
211 
212         // The algorithm assumes 0 < phi2 < pi. If this is not the case, the input and output are reflected on 'd'.
213         this.reflected = phi2 < 0 || phi2 > Math.PI;
214         if (this.reflected)
215         {
216             phi1 = -phi1;
217             phi2 = -phi2;
218         }
219 
220         // h(phi1, phi2) guarantees for negative values along with 0 < phi1 < phi2 < pi, that a C-shaped clothoid exists.
221         double[] cs = Fresnel.fresnel(alphaToT(phi1 + phi2));
222         double h = cs[1] * Math.cos(phi1) - cs[0] * Math.sin(phi1);
223         boolean cShape = 0 < phi1 && phi1 < phi2 && phi2 < Math.PI && h < 0; // otherwise, S-shape
224         double theta = getTheta(phi1, phi2, cShape);
225         double aSign = cShape ? -1.0 : 1.0;
226         double thetaSign = -aSign;
227 
228         double v1 = theta + phi1 + phi2;
229         double v2 = theta + phi1;
230         double[] cs0 = Fresnel.fresnel(alphaToT(theta));
231         double[] cs1 = Fresnel.fresnel(alphaToT(v1));
232         this.a = d2 / ((cs1[1] + aSign * cs0[1]) * Math.sin(v2) + (cs1[0] + aSign * cs0[0]) * Math.cos(v2));
233 
234         dx /= d2; // normalized
235         dy /= d2;
236         if (this.reflected)
237         {
238             // reflect t0 and n0 on 'd' so that the created output clothoid is reflected back after input was reflected
239             this.t0 = new double[] {Math.cos(-v2) * dx + Math.sin(-v2) * dy, -Math.sin(-v2) * dx + Math.cos(-v2) * dy};
240             this.n0 = new double[] {-this.t0[1], this.t0[0]};
241         }
242         else
243         {
244             this.t0 = new double[] {Math.cos(v2) * dx + Math.sin(v2) * dy, -Math.sin(v2) * dx + Math.cos(v2) * dy};
245             this.n0 = new double[] {this.t0[1], -this.t0[0]};
246         }
247 
248         this.alphaMin = thetaSign * theta;
249         this.alphaMax = v1; // alphaMax = theta + phi1 + phi2, which is v1
250         double sign = (this.reflected ? -1.0 : 1.0);
251         double curveMin = Math.PI * alphaToT(this.alphaMin) / this.a;
252         double curveMax = Math.PI * alphaToT(v1) / this.a;
253         this.startCurvature = sign * (this.opposite ? -curveMax : curveMin);
254         this.endCurvature = sign * (this.opposite ? -curveMin : curveMax);
255         this.length = this.a * (alphaToT(v1) - alphaToT(this.alphaMin));
256     }
257 
258     /**
259      * Create clothoid from one point based on curvature and A-value.
260      * @param startPoint start point
261      * @param a A-value
262      * @param startCurvature start curvature
263      * @param endCurvature end curvature
264      * @throws NullPointerException when <code>startPoint</code> is <code>null</code>
265      * @throws IllegalArgumentException when <code>a &le; 0.0</code>
266      */
267     public Clothoid2d(final DirectedPoint2d startPoint, final double a, final double startCurvature, final double endCurvature)
268     {
269         Throw.whenNull(startPoint, "startPoint");
270         Throw.when(a <= 0.0, IllegalArgumentException.class, "A value must be above 0.");
271         this.startPoint = startPoint;
272         // Scale 'a', due to parameter conversion between C(alpha)/S(alpha) and C(t)/S(t); t = sqrt(2*alpha/pi).
273         this.a = a * Math.sqrt(Math.PI);
274         this.length = a * a * Math.abs(endCurvature - startCurvature);
275         this.startCurvature = startCurvature;
276         this.endCurvature = endCurvature;
277 
278         double l1 = a * a * startCurvature;
279         double l2 = a * a * endCurvature;
280         this.alphaMin = Math.abs(l1) * startCurvature / 2.0;
281         this.alphaMax = Math.abs(l2) * endCurvature / 2.0;
282 
283         double ang = AngleUtil.normalizeAroundZero(startPoint.dirZ) - Math.abs(this.alphaMin);
284         this.t0 = new double[] {Math.cos(ang), Math.sin(ang)};
285         this.n0 = new double[] {this.t0[1], -this.t0[0]};
286         double endDirection = ang + Math.abs(this.alphaMax);
287         if (startCurvature > endCurvature)
288         {
289             // In these cases the algorithm works in the negative direction. We need to flip over the line through the start
290             // point that runs perpendicular to the start direction.
291             double m = Math.tan(startPoint.dirZ + Math.PI / 2.0);
292 
293             // Linear algebra flipping, see: https://math.stackexchange.com/questions/525082/reflection-across-a-line
294             double onePlusMm = 1.0 + m * m;
295             double oneMinusMm = 1.0 - m * m;
296             double mmMinusOne = m * m - 1.0;
297             double twoM = 2.0 * m;
298             double t00 = this.t0[0];
299             double t01 = this.t0[1];
300             double n00 = this.n0[0];
301             double n01 = this.n0[1];
302             this.t0[0] = (oneMinusMm * t00 + 2 * m * t01) / onePlusMm;
303             this.t0[1] = (twoM * t00 + mmMinusOne * t01) / onePlusMm;
304             this.n0[0] = (oneMinusMm * n00 + 2 * m * n01) / onePlusMm;
305             this.n0[1] = (twoM * n00 + mmMinusOne * n01) / onePlusMm;
306 
307             double ang2 = Math.atan2(this.t0[1], this.t0[0]);
308             endDirection = ang2 - Math.abs(this.alphaMax) + Math.PI;
309         }
310         PolyLine2d line = toPolyLine(new Flattener2d.NumSegments(1));
311         Point2d end = Try.assign(() -> line.get(line.size() - 1), "Line does not have an end point.");
312         this.endPoint = new DirectedPoint2d(end.x, end.y, endDirection);
313 
314         // Fields not relevant for definition with curvatures
315         this.straight = null;
316         this.arc = null;
317         this.opposite = false;
318         this.reflected = false;
319     }
320 
321     /**
322      * Create clothoid from one point based on curvature and length. This method calculates the A-value as
323      * <i>sqrt(L/|k2-k1|)</i>, where <i>L</i> is the length of the resulting clothoid, and <i>k2</i> and <i>k1</i> are the end
324      * and start curvature.
325      * @param startPoint start point.
326      * @param length Length of the resulting clothoid.
327      * @param startCurvature start curvature.
328      * @param endCurvature end curvature
329      * @return clothoid based on curvature and length.
330      * @throws NullPointerException when <code>startPoint</code> is <code>null</code>
331      * @throws IllegalArgumentException when <code>length &le; 0.0</code>
332      */
333     public static Clothoid2d withLength(final DirectedPoint2d startPoint, final double length, final double startCurvature,
334             final double endCurvature)
335     {
336         Throw.when(length <= 0.0, IllegalArgumentException.class, "Length must be above 0.");
337         double a = Math.sqrt(length / Math.abs(endCurvature - startCurvature));
338         return new Clothoid2d(startPoint, a, startCurvature, endCurvature);
339     }
340 
341     /**
342      * Performs alpha to t variable change.
343      * @param alpha alpha value, must be positive
344      * @return t value (length along the Fresnel integral, also known as x)
345      */
346     private static double alphaToT(final double alpha)
347     {
348         return alpha >= 0 ? Math.sqrt(alpha * 2.0 / Math.PI) : -Math.sqrt(-alpha * 2.0 / Math.PI);
349     }
350 
351     /**
352      * Returns theta value given shape to use. If no such value is found, the other shape may be attempted.
353      * @param phi1 phi1.
354      * @param phi2 phi2.
355      * @param cShape C-shaped, or S-shaped otherwise.
356      * @return the number of radians that is moved on to a side of the full clothoid.
357      */
358     private static double getTheta(final double phi1, final double phi2, final boolean cShape)
359     {
360         double sign, phiMin, phiMax;
361         if (cShape)
362         {
363             double lambda = (1 - Math.cos(phi1)) / (1 - Math.cos(phi2));
364             phiMin = 0.0;
365             phiMax = (lambda * lambda * (phi1 + phi2)) / (1 - (lambda * lambda));
366             sign = -1.0;
367         }
368         else
369         {
370             phiMin = Math.max(0, -phi1);
371             phiMax = Math.PI / 2 - phi1;
372             sign = 1;
373         }
374 
375         double fMin = fTheta(phiMin, phi1, phi2, sign);
376         double fMax = fTheta(phiMax, phi1, phi2, sign);
377         if (fMin * fMax > 0)
378         {
379             throw new IllegalArgumentException(
380                     "f(phiMin) and f(phiMax) have the same sign, we cant find f(theta) = 0 between them.");
381         }
382 
383         // Find optimum using Secant method, see https://en.wikipedia.org/wiki/Secant_method
384         double x0 = phiMin;
385         double x1 = phiMax;
386         double x2 = 0;
387         for (int i = 0; i < 100; i++) // max 100 iterations, otherwise use latest x2 value
388         {
389             double f1 = fTheta(x1, phi1, phi2, sign);
390             x2 = x1 - f1 * (x1 - x0) / (f1 - fTheta(x0, phi1, phi2, sign));
391             x2 = Math.max(Math.min(x2, phiMax), phiMin); // this line is an essential addition to keep the algorithm at bay
392             x0 = x1;
393             x1 = x2;
394             if (Math.abs(x0 - x1) < SECANT_TOLERANCE || Math.abs(x0 / x1 - 1) < SECANT_TOLERANCE
395                     || Math.abs(f1) < SECANT_TOLERANCE)
396             {
397                 return x2;
398             }
399         }
400 
401         return x2;
402     }
403 
404     /**
405      * Function who's solution <i>f</i>(<i>theta</i>) = 0 for the given value of <i>phi1</i> and <i>phi2</i> gives the angle
406      * that solves fitting a C-shaped clothoid through two points. This assumes that <i>sign</i> = -1. If <i>sign</i> = 1, this
407      * changes to <i>g</i>(<i>theta</i>) = 0 being a solution for an S-shaped clothoid.
408      * @param theta angle defining the curvature of the resulting clothoid.
409      * @param phi1 angle between the line through both end points, and the direction of the first point.
410      * @param phi2 angle between the line through both end points, and the direction of the last point.
411      * @param sign 1 for C-shaped, -1 for S-shaped.
412      * @return <i>f</i>(<i>theta</i>) for <i>sign</i> = -1, or <i>g</i>(<i>theta</i>) for <i>sign</i> = 1.
413      */
414     private static double fTheta(final double theta, final double phi1, final double phi2, final double sign)
415     {
416         double thetaPhi1 = theta + phi1;
417         double[] cs0 = Fresnel.fresnel(alphaToT(theta));
418         double[] cs1 = Fresnel.fresnel(alphaToT(thetaPhi1 + phi2));
419         return (cs1[1] + sign * cs0[1]) * Math.cos(thetaPhi1) - (cs1[0] + sign * cs0[0]) * Math.sin(thetaPhi1);
420     }
421 
422     @Override
423     public DirectedPoint2d getStartPoint()
424     {
425         return this.startPoint;
426     }
427 
428     @Override
429     public DirectedPoint2d getEndPoint()
430     {
431         return this.endPoint;
432     }
433 
434     /**
435      * Start curvature of this Clothoid.
436      * @return start curvature of this Clothoid
437      */
438     public double getStartCurvature()
439     {
440         return this.startCurvature;
441     }
442 
443     /**
444      * End curvature of this Clothoid.
445      * @return end curvature of this Clothoid
446      */
447     public double getEndCurvature()
448     {
449         return this.endCurvature;
450     }
451 
452     /**
453      * Start radius of this Clothoid.
454      * @return start radius of this Clothoid
455      */
456     public double getStartRadius()
457     {
458         return 1.0 / this.startCurvature;
459     }
460 
461     /**
462      * End radius of this Clothoid.
463      * @return end radius of this Clothoid
464      */
465     public double getEndRadius()
466     {
467         return 1.0 / this.endCurvature;
468     }
469 
470     /**
471      * Return A, the clothoid scaling parameter.
472      * @return a, the clothoid scaling parameter.
473      */
474     public double getA()
475     {
476         // Scale 'a', due to parameter conversion between C(alpha)/S(alpha) and C(t)/S(t); t = sqrt(2*alpha/pi).
477         // The value of 'this.a' is used when scaling the Fresnel integral, which is why this is stored.
478         return this.a / Math.sqrt(Math.PI);
479     }
480 
481     /**
482      * Calculates shifts if these have not yet been calculated.
483      */
484     private void assureShift()
485     {
486         if (this.shiftDetermined)
487         {
488             return;
489         }
490 
491         DirectedPoint2d p1 = this.opposite ? this.endPoint : this.startPoint;
492         DirectedPoint2d p2 = this.opposite ? this.startPoint : this.endPoint;
493 
494         // Create first point to figure out the required overall shift
495         double[] csMin = Fresnel.fresnel(alphaToT(this.alphaMin));
496         double xMin = this.a * (csMin[0] * this.t0[0] - csMin[1] * this.n0[0]);
497         double yMin = this.a * (csMin[0] * this.t0[1] - csMin[1] * this.n0[1]);
498         this.shiftX = p1.x - xMin;
499         this.shiftY = p1.y - yMin;
500 
501         // Due to numerical precision, we linearly scale over alpha such that the final point is exactly on p2
502         if (p2 != null)
503         {
504             double[] csMax = Fresnel.fresnel(alphaToT(this.alphaMax));
505             double xMax = this.a * (csMax[0] * this.t0[0] - csMax[1] * this.n0[0]);
506             double yMax = this.a * (csMax[0] * this.t0[1] - csMax[1] * this.n0[1]);
507             this.dShiftX = p2.x - (xMax + this.shiftX);
508             this.dShiftY = p2.y - (yMax + this.shiftY);
509         }
510         else
511         {
512             this.dShiftX = 0.0;
513             this.dShiftY = 0.0;
514         }
515 
516         this.shiftDetermined = true;
517     }
518 
519     /**
520      * Returns a point on the clothoid at a fraction of curvature along the clothoid.
521      * @param fraction fraction of curvature along the clothoid
522      * @param offset offset relative to radius
523      * @return point on the clothoid at a fraction of curvature along the clothoid
524      */
525     private Point2d getPoint(final double fraction, final double offset)
526     {
527         double f = this.opposite ? 1.0 - fraction : fraction;
528         double alpha = this.alphaMin + f * (this.alphaMax - this.alphaMin);
529         double[] cs = Fresnel.fresnel(alphaToT(alpha));
530         double x = this.shiftX + this.a * (cs[0] * this.t0[0] - cs[1] * this.n0[0]) + f * this.dShiftX;
531         double y = this.shiftY + this.a * (cs[0] * this.t0[1] - cs[1] * this.n0[1]) + f * this.dShiftY;
532         double d = getDirectionForAlpha(alpha) + Math.PI / 2;
533         return new Point2d(x + Math.cos(d) * offset, y + Math.sin(d) * offset);
534     }
535 
536     @Override
537     public Point2d getPoint(final double fraction)
538     {
539         if (this.arc != null)
540         {
541             return this.arc.getPoint(fraction);
542         }
543         else if (this.straight != null)
544         {
545             return this.straight.getPoint(fraction);
546         }
547         return getPoint(fraction, 0);
548     }
549 
550     @Override
551     public Point2d getPoint(final double fraction, final ContinuousPiecewiseLinearFunction of)
552     {
553         if (this.arc != null)
554         {
555             return this.arc.getPoint(fraction, of);
556         }
557         else if (this.straight != null)
558         {
559             return this.straight.getPoint(fraction, of);
560         }
561         return getPoint(fraction, of.get(fraction));
562     }
563 
564     @Override
565     public Double getDirection(final double fraction)
566     {
567         if (this.arc != null)
568         {
569             return this.arc.getDirection(fraction);
570         }
571         else if (this.straight != null)
572         {
573             return this.straight.getDirection(fraction);
574         }
575         return getDirectionForAlpha(this.alphaMin + fraction * (this.alphaMax - this.alphaMin));
576     }
577 
578     @Override
579     public double getDirection(final double fraction, final ContinuousPiecewiseLinearFunction of)
580     {
581         double derivativeOffset = of.getDerivative(fraction) / this.length;
582         return getDirection(this.alphaMin + fraction * (this.alphaMax - this.alphaMin)) + Math.atan(derivativeOffset);
583     }
584 
585     /**
586      * Returns the direction at given alpha.
587      * @param alpha alpha
588      * @return direction at given alpha
589      */
590     private double getDirectionForAlpha(final double alpha)
591     {
592         double rot = Math.atan2(this.t0[1], this.t0[0]);
593         // abs because alpha = -3deg has the same direction as alpha = 3deg in an S-curve where alpha = 0 is the middle
594         rot += this.reflected ? -Math.abs(alpha) : Math.abs(alpha);
595         if (this.opposite)
596         {
597             rot += Math.PI;
598         }
599         return AngleUtil.normalizeAroundZero(rot);
600     }
601 
602     @Override
603     public PolyLine2d toPolyLine(final Flattener2d flattener)
604     {
605         if (this.straight != null)
606         {
607             return this.straight.toPolyLine(flattener);
608         }
609         if (this.arc != null)
610         {
611             return this.arc.toPolyLine(flattener);
612         }
613         assureShift();
614         return flattener.flatten(this);
615     }
616 
617     @Override
618     public PolyLine2d toPolyLine(final OffsetFlattener2d flattener, final ContinuousPiecewiseLinearFunction offsets)
619     {
620         Throw.whenNull(offsets, "offsets");
621         if (this.straight != null)
622         {
623             return this.straight.toPolyLine(flattener, offsets);
624         }
625         if (this.arc != null)
626         {
627             return this.arc.toPolyLine(flattener, offsets);
628         }
629         assureShift();
630         return flattener.flatten(this, offsets);
631     }
632 
633     @Override
634     public double getLength()
635     {
636         return this.length;
637     }
638 
639     /**
640      * Returns whether the shape was applied as a Clothoid, an Arc, or as a Straight, depending on start and end position and
641      * direction.
642      * @return "Clothoid", "Arc" or "Straight"
643      */
644     public String getAppliedShape()
645     {
646         return this.straight == null ? (this.arc == null ? "Clothoid" : "Arc") : "Straight";
647     }
648 
649     @Override
650     public String toString()
651     {
652         return "Clothoid [startPoint=" + this.startPoint + ", endPoint=" + this.endPoint + ", startCurvature="
653                 + this.startCurvature + ", endCurvature=" + this.endCurvature + ", length=" + this.length + "]";
654     }
655 
656 }
657 
658 /**
659  * Utility class to create clothoid lines, in particular the Fresnel integral based on:
660  * <ul>
661  * <li>W.J. Cody (1968) Chebyshev approximations for the Fresnel integrals. Mathematics of Computation, Vol. 22, Issue 102, pp.
662  * 450–453.</li>
663  * </ul>
664  * <p>
665  * Copyright (c) 2023-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
666  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
667  * </p>
668  * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
669  * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
670  * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
671  * @see <a href="https://www.ams.org/journals/mcom/1985-44-170/S0025-5718-1985-0777277-6/S0025-5718-1985-0777277-6.pdf">Cody
672  *      (1968)</a>
673  */
674 final class Fresnel
675 {
676 
677     // {@formatter:off}
678     /** Numerator coefficients to calculate C(t) in region 1. */
679     private static final double[] CN1 = new double[] {
680             9.999999999999999421E-01,
681             -1.994608988261842706E-01, 
682             1.761939525434914045E-02,
683             -5.280796513726226960E-04, 
684             5.477113856826871660E-06
685     };
686 
687     /** Denominator coefficients to calculate C(t) in region 1. */
688     private static final double[] CD1 = new double[] {
689             1.000000000000000000E+00,
690             4.727921120104532689E-02,
691             1.099572150256418851E-03,
692             1.552378852769941331E-05,
693             1.189389014228757184E-07
694     };
695 
696     /** Numerator coefficients to calculate C(t) in region 2. */
697     private static final double[] CN2 = new double[] {
698             1.00000000000111043640E+00,
699             -2.07073360335323894245E-01,
700             1.91870279431746926505E-02,
701             -6.71376034694922109230E-04,
702             1.02365435056105864908E-05,
703             -5.68293310121870728343E-08
704     };
705 
706     /** Denominator coefficients to calculate C(t) in region 3. */
707     private static final double[] CD2 = new double[] {
708             1.00000000000000000000E+00,
709             3.96667496952323433510E-02,
710             7.88905245052359907842E-04,
711             1.01344630866749406081E-05,
712             8.77945377892369265356E-08,
713             4.41701374065009620393E-10
714     };
715 
716     /** Numerator coefficients to calculate S(t) in region 1. */
717     private static final double[] SN1 = new double[] {
718             5.2359877559829887021E-01,
719             -7.0748991514452302596E-02,
720             3.8778212346368287939E-03,
721             -8.4555728435277680591E-05,
722             6.7174846662514086196E-07
723     };
724 
725     /** Denominator coefficients to calculate S(t) in region 1. */
726     private static final double[] SD1 = new double[] {
727             1.0000000000000000000E+00,
728             4.1122315114238422205E-02,
729             8.1709194215213447204E-04,
730             9.6269087593903403370E-06,
731             5.9528122767840998345E-08
732     };
733 
734     /** Numerator coefficients to calculate S(t) in region 2. */
735     private static final double[] SN2 = new double[] {
736             5.23598775598344165913E-01,
737             -7.37766914010191323867E-02,
738             4.30730526504366510217E-03,
739             -1.09540023911434994566E-04,
740             1.28531043742724820610E-06,
741             -5.76765815593088804567E-09
742     };
743 
744     /** Denominator coefficients to calculate S(t) in region 2. */
745     private static final double[] SD2 = new double[] {
746             1.00000000000000000000E+00,
747             3.53398342167472162540E-02,
748             6.18224620195473216538E-04,
749             6.87086265718620117905E-06,
750             5.03090581246612375866E-08,
751             2.05539124458579596075E-10
752     };
753 
754     /** Numerator coefficients to calculate f(t) in region 3. */
755     private static final double[] FN3 = new double[] {
756             3.1830975293580985290E-01,
757             1.2226000551672961219E+01,
758             1.2924886131901657025E+02,
759             4.3886367156695547655E+02,
760             4.1466722177958961672E+02,
761             5.6771463664185116454E+01
762     };
763 
764     /** Denominator coefficients to calculate f(t) in region 3. */
765     private static final double[] FD3 = new double[] {
766             1.0000000000000000000E+00,
767             3.8713003365583442831E+01,
768             4.1674359830705629745E+02,
769             1.4740030733966610568E+03,
770             1.5371675584895759916E+03,
771             2.9113088788847831515E+02
772     };
773 
774     /** Numerator coefficients to calculate f(t) in region 4. */
775     private static final double[] FN4 = new double[] {
776             3.183098818220169217E-01,
777             1.958839410219691002E+01,
778             3.398371349269842400E+02,
779             1.930076407867157531E+03,
780             3.091451615744296552E+03,
781             7.177032493651399590E+02
782     };
783 
784     /** Denominator coefficients to calculate f(t) in region 4. */
785     private static final double[] FD4 = new double[] {
786             1.000000000000000000E+00,
787             6.184271381728873709E+01,
788             1.085350675006501251E+03,
789             6.337471558511437898E+03,
790             1.093342489888087888E+04,
791             3.361216991805511494E+03
792     };
793 
794     /** Numerator coefficients to calculate f(t) in region 5. */
795     private static final double[] FN5 = new double[] {
796             -9.675460329952532343E-02,
797             -2.431275407194161683E+01,
798             -1.947621998306889176E+03,
799             -6.059852197160773639E+04,
800             -7.076806952837779823E+05,
801             -2.417656749061154155E+06,
802             -7.834914590078311336E+05
803     };
804 
805     /** Denominator coefficients to calculate f(t) in region 5. */
806     private static final double[] FD5 = new double[] {
807             1.000000000000000000E+00,
808             2.548289012949732752E+02,
809             2.099761536857815105E+04,
810             6.924122509827708985E+05,
811             9.178823229918143780E+06,
812             4.292733255630186679E+07,
813             4.803294184260528342E+07
814     };
815 
816     /** Numerator coefficients to calculate g(t) in region 3. */
817     private static final double[] GN3 = new double[] {
818             1.013206188102747985E-01,
819             4.445338275505123778E+00,
820             5.311228134809894481E+01,
821             1.991828186789025318E+02,
822             1.962320379716626191E+02,
823             2.054214324985006303E+01
824     };
825 
826     /** Denominator coefficients to calculate g(t) in region 3. */
827     private static final double[] GD3 = new double[] {
828             1.000000000000000000E+00,
829             4.539250196736893605E+01,
830             5.835905757164290666E+02,
831             2.544731331818221034E+03,
832             3.481121478565452837E+03,
833             1.013794833960028555E+03
834     };
835 
836     /** Numerator coefficients to calculate g(t) in region 4. */
837     private static final double[] GN4 = new double[] {
838             1.01321161761804586E-01,
839             7.11205001789782823E+00,
840             1.40959617911315524E+02,
841             9.08311749529593938E+02,
842             1.59268006085353864E+03,
843             3.13330163068755950E+02
844     };
845 
846     /** Denominator coefficients to calculate g(t) in region 4. */
847     private static final double[] GD4 = new double[] {
848             1.00000000000000000E+00,
849             7.17128596939302198E+01,
850             1.49051922797329229E+03,
851             1.06729678030583897E+04,
852             2.41315567213369742E+04,
853             1.15149832376260604E+04
854     };
855 
856     /** Numerator coefficients to calculate g(t) in region 5. */
857     private static final double[] GN5 = new double[] {
858             -1.53989733819769316E-01,
859             -4.31710157823357568E+01,
860             -3.87754141746378493E+03,
861             -1.35678867813756347E+05,
862             -1.77758950838029676E+06,
863             -6.66907061668636416E+06,
864             -1.72590224654836845E+06
865     };
866     
867     /** Denominator coefficients to calculate g(t) in region 5. */
868     private static final double[] GD5 = new double[] {
869             1.00000000000000000E+00,
870             2.86733194975899483E+02,
871             2.69183180396242536E+04,
872             1.02878693056687506E+06,
873             1.62095600500231646E+07,
874             9.38695862531635179E+07,
875             1.40622441123580005E+08
876     };
877     // {@formatter:on}
878 
879     /** Utility class. */
880     private Fresnel()
881     {
882         // do not instantiate
883     }
884 
885     /**
886      * Approximate the Fresnel integral. The method used is based on Cody (1968). This method applies rational approximation to
887      * approximate the clothoid. For clothoid rotation beyond 1.6 rad, this occurs in polar form. The polar form is robust for
888      * arbitrary large numbers, unlike polynomial expansion, and will at a large threshold converge to (0.5, 0.5). There are 5
889      * regions with different fitted values for the rational approximations, in Cartesian or polar form.<br>
890      * <br>
891      * W.J. Cody (1968) Chebyshev approximations for the Fresnel integrals. Mathematics of Computation, Vol. 22, Issue 102, pp.
892      * 450–453.
893      * @param x length along the standard Fresnel integral (no scaling).
894      * @return array with two double values c and s
895      * @see <a href="https://www.ams.org/journals/mcom/1968-22-102/S0025-5718-68-99871-2/S0025-5718-68-99871-2.pdf">Cody
896      *      (1968)</a>
897      */
898     public static double[] fresnel(final double x)
899     {
900         final double t = Math.abs(x);
901         double cc, ss;
902         if (t < 1.2)
903         {
904             cc = t * ratioEval(t, CN1, +1) / ratioEval(t, CD1, +1);
905             ss = t * t * t * ratioEval(t, SN1, +1) / ratioEval(t, SD1, +1);
906         }
907         else if (t < 1.6)
908         {
909             cc = t * ratioEval(t, CN2, +1) / ratioEval(t, CD2, +1);
910             ss = t * t * t * ratioEval(t, SN2, +1) / ratioEval(t, SD2, +1);
911         }
912         else if (t < 1.9)
913         {
914             double pitt2 = Math.PI * t * t / 2;
915             double sinpitt2 = Math.sin(pitt2);
916             double cospitt2 = Math.cos(pitt2);
917             double ft = (1 / t) * ratioEval(t, FN3, -1) / ratioEval(t, FD3, -1);
918             double gt = (1 / (t * t * t)) * ratioEval(t, GN3, -1) / ratioEval(t, GD3, -1);
919             cc = .5 + ft * sinpitt2 - gt * cospitt2;
920             ss = .5 - ft * cospitt2 - gt * sinpitt2;
921         }
922         else if (t < 2.4)
923         {
924             double pitt2 = Math.PI * t * t / 2;
925             double sinpitt2 = Math.sin(pitt2);
926             double cospitt2 = Math.cos(pitt2);
927             double tinv = 1 / t;
928             double tttinv = tinv * tinv * tinv;
929             double ft = tinv * ratioEval(t, FN4, -1) / ratioEval(t, FD4, -1);
930             double gt = tttinv * ratioEval(t, GN4, -1) / ratioEval(t, GD4, -1);
931             cc = .5 + ft * sinpitt2 - gt * cospitt2;
932             ss = .5 - ft * cospitt2 - gt * sinpitt2;
933         }
934         else
935         {
936             double pitt2 = Math.PI * t * t / 2;
937             double sinpitt2 = Math.sin(pitt2);
938             double cospitt2 = Math.cos(pitt2);
939             double piinv = 1 / Math.PI;
940             double tinv = 1 / t;
941             double tttinv = tinv * tinv * tinv;
942             double ttttinv = tttinv * tinv;
943             double ft = tinv * (piinv + (ttttinv * ratioEval(t, FN5, -1) / ratioEval(t, FD5, -1)));
944             double gt = tttinv * ((piinv * piinv) + (ttttinv * ratioEval(t, GN5, -1) / ratioEval(t, GD5, -1)));
945             cc = .5 + ft * sinpitt2 - gt * cospitt2;
946             ss = .5 - ft * cospitt2 - gt * sinpitt2;
947         }
948         if (x < 0)
949         {
950             cc = -cc;
951             ss = -ss;
952         }
953 
954         return new double[] {cc, ss};
955     }
956 
957     /**
958      * Evaluate numerator or denominator of rational approximation.
959      * @param t value along the clothoid
960      * @param coef rational approximation coefficients
961      * @param sign sign of exponent, +1 for Cartesian rational approximation, -1 for polar approximation
962      * @return numerator or denominator of rational approximation
963      */
964     private static double ratioEval(final double t, final double[] coef, final double sign)
965     {
966         double value = 0;
967         for (int s = 0; s < coef.length; s++)
968         {
969             value += coef[s] * Math.pow(t, sign * 4 * s);
970         }
971         return value;
972     }
973 
974 }