View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import org.djunits.value.vdouble.scalar.Angle;
4   import org.djunits.value.vdouble.scalar.Direction;
5   import org.djutils.base.AngleUtil;
6   import org.djutils.draw.line.PolyLine2d;
7   import org.djutils.draw.point.OrientedPoint2d;
8   import org.djutils.draw.point.Point2d;
9   import org.djutils.exceptions.Throw;
10  import org.djutils.exceptions.Try;
11  
12  /**
13   * Continuous definition of a clothoid. The following definitions are available:
14   * <ul>
15   * <li>A clothoid between two directed <i>points</i>.</li>
16   * <li>A clothoid originating from a directed point with start curvature, end curvature, and <i>length</i> specified.</li>
17   * <li>A clothoid originating from a directed point with start curvature, end curvature, and <i>A-value</i> specified.</li>
18   * </ul>
19   * This class is based on:
20   * <ul>
21   * <li>Dale Connor and Lilia Krivodonova (2014) "Interpolation of two-dimensional curves with Euler spirals", Journal of
22   * Computational and Applied Mathematics, Volume 261, 1 May 2014, pp. 320-332.</li>
23   * <li>D.J. Waltona and D.S. Meek (2009) "G<sup>1</sup> interpolation with a single Cornu spiral segment", Journal of
24   * Computational and Applied Mathematics, Volume 223, Issue 1, 1 January 2009, pp. 86-96.</li>
25   * </ul>
26   * <p>
27   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
28   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
29   * </p>
30   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
31   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
32   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
33   * @see <a href="https://www.sciencedirect.com/science/article/pii/S0377042713006286">Connor and Krivodonova (2014)</a>
34   * @see <a href="https://www.sciencedirect.com/science/article/pii/S0377042704000925">Waltona and Meek (2009)</a>
35   */
36  public class ContinuousClothoid implements ContinuousLine
37  {
38  
39      /** Threshold to consider input to be a trivial straight or circle arc. The value is 1/10th of a degree. */
40      private static final double ANGLE_TOLERANCE = 2.0 * Math.PI / 3600.0;
41  
42      /** Stopping tolerance for the Secant method to find optimal theta values. */
43      private static final double SECANT_TOLERANCE = 1e-8;
44  
45      /** Start point with direction. */
46      private final OrientedPoint2d startPoint;
47  
48      /** End point with direction. */
49      private final OrientedPoint2d endPoint;
50  
51      /** Start curvature. */
52      private final double startCurvature;
53  
54      /** End curvature. */
55      private final double endCurvature;
56  
57      /** Length. */
58      private final double length;
59  
60      /**
61       * A-value; for scaling the Fresnel integral. The regular clothoid A-parameter is obtained by dividing by
62       * {@code Math.sqrt(Math.PI)}.
63       */
64      private final double a;
65  
66      /** Minimum alpha value of line to draw. */
67      private final double alphaMin;
68  
69      /** Maximum alpha value of line to draw. */
70      private final double alphaMax;
71  
72      /** Unit vector from the origin of the clothoid, towards the positive side. */
73      private final double[] t0;
74  
75      /** Normal unit vector to t0. */
76      private final double[] n0;
77  
78      /** Whether the line needs to be flipped. */
79      private final boolean opposite;
80  
81      /** Whether the line is reflected. */
82      private final boolean reflected;
83  
84      /** Simplification to straight when valid. */
85      private final ContinuousStraight straight;
86  
87      /** Simplification to arc when valid. */
88      private final ContinuousArc arc;
89  
90      /** Whether the shift was determined. */
91      private boolean shiftDetermined;
92  
93      /** Shift in x-coordinate of start point. */
94      private double shiftX;
95  
96      /** Shift in y-coordinate of start point. */
97      private double shiftY;
98  
99      /** Additional shift in x-coordinate towards end point. */
100     private double dShiftX;
101 
102     /** Additional shift in y-coordinate towards end point. */
103     private double dShiftY;
104 
105     /**
106      * Create clothoid between two directed points. This constructor is based on the procedure in:<br>
107      * <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      * <br>
111      * Which applies the theory proven in:<br>
112      * <br>
113      * D.J. Waltona and D.S. Meek (2009) "G<sup>1</sup> interpolation with a single Cornu spiral segment", Journal of
114      * Computational and Applied Mathematics, Volume 223, Issue 1, 1 January 2009, pp. 86-96.<br>
115      * <br>
116      * This procedure guarantees that the resulting line has the minimal angle rotation that is required to connect the points.
117      * If the points approximate a straight line or circle, with a tolerance of up 1/10th of a degree, those respective lines
118      * are created. The numerical approximation of the underlying Fresnel integral is different from the paper. See
119      * {@code Clothoid.fresnal()}.
120      * @param startPoint start point.
121      * @param endPoint end point.
122      * @see <a href="https://www.sciencedirect.com/science/article/pii/S0377042713006286">Connor and Krivodonova (2014)</a>
123      * @see <a href="https://www.sciencedirect.com/science/article/pii/S0377042704000925">Waltona and Meek (2009)</a>
124      */
125     public ContinuousClothoid(final OrientedPoint2d startPoint, final OrientedPoint2d endPoint)
126     {
127         Throw.whenNull(startPoint, "Start point may not be null.");
128         Throw.whenNull(endPoint, "End point may not be null.");
129         this.startPoint = startPoint;
130         this.endPoint = endPoint;
131 
132         double dx = endPoint.x - startPoint.x;
133         double dy = endPoint.y - startPoint.y;
134         double d2 = Math.hypot(dx, dy); // length of straight line from start to end
135         double d = Math.atan2(dy, dx); // angle of line through start and end points
136 
137         double phi1 = AngleUtil.normalizeAroundZero(d - startPoint.dirZ);
138         double phi2 = AngleUtil.normalizeAroundZero(endPoint.dirZ - d);
139         double phi1Abs = Math.abs(phi1);
140         double phi2Abs = Math.abs(phi2);
141 
142         if (phi1Abs < ANGLE_TOLERANCE && phi2Abs < ANGLE_TOLERANCE)
143         {
144             // Straight
145             this.length = Math.hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
146             this.a = Double.POSITIVE_INFINITY;
147             this.startCurvature = 0.0;
148             this.endCurvature = 0.0;
149             this.straight = new ContinuousStraight(startPoint, this.length);
150             this.arc = null;
151             this.alphaMin = 0.0;
152             this.alphaMax = 0.0;
153             this.t0 = null;
154             this.n0 = null;
155             this.opposite = false;
156             this.reflected = false;
157             return;
158         }
159         else if (Math.abs(phi2 - phi1) < ANGLE_TOLERANCE)
160         {
161             // Arc
162             double r = .5 * d2 / Math.sin(phi1);
163             double cosStartDirection = Math.cos(startPoint.dirZ);
164             double sinStartDirection = Math.sin(startPoint.dirZ);
165             double ang = Math.PI / 2.0;
166             double cosAng = Math.cos(ang); // =0
167             double sinAng = Math.sin(ang); // =1
168             double x0 = startPoint.x - r * (cosStartDirection * cosAng + sinStartDirection * sinAng);
169             double y0 = startPoint.y - r * (cosStartDirection * -sinAng + sinStartDirection * cosAng);
170             double from = Math.atan2(startPoint.y - y0, startPoint.x - x0);
171             double to = Math.atan2(endPoint.y - y0, endPoint.x - x0);
172             if (r < 0 && to > from)
173             {
174                 to = to - 2.0 * Math.PI;
175             }
176             else if (r > 0 && to < from)
177             {
178                 to = to + 2.0 * Math.PI;
179             }
180             Angle angle = Angle.instantiateSI(Math.abs(to - from));
181             this.length = angle.si * Math.abs(r);
182             this.a = 0.0;
183             this.startCurvature = 1.0 / r;
184             this.endCurvature = 1.0 / r;
185             this.straight = null;
186             this.arc = new ContinuousArc(startPoint, Math.abs(r), r > 0.0, angle);
187             this.alphaMin = 0.0;
188             this.alphaMax = 0.0;
189             this.t0 = null;
190             this.n0 = null;
191             this.opposite = false;
192             this.reflected = false;
193             return;
194         }
195         this.straight = null;
196         this.arc = null;
197 
198         // The algorithm assumes |phi2| to be larger than |phi1|. If this is not the case, the clothoid is created in the
199         // opposite direction.
200         if (phi2Abs < phi1Abs)
201         {
202             this.opposite = true;
203             double phi3 = phi1;
204             phi1 = -phi2;
205             phi2 = -phi3;
206             dx = -dx;
207             dy = -dy;
208         }
209         else
210         {
211             this.opposite = false;
212         }
213 
214         // The algorithm assumes 0 < phi2 < pi. If this is not the case, the input and output are reflected on 'd'.
215         this.reflected = phi2 < 0 || phi2 > Math.PI;
216         if (this.reflected)
217         {
218             phi1 = -phi1;
219             phi2 = -phi2;
220         }
221 
222         // h(phi1, phi2) guarantees for negative values along with 0 < phi1 < phi2 < pi, that a C-shaped clothoid exists.
223         Fresnel cs = Fresnel.integral(alphaToT(phi1 + phi2));
224         double h = cs.s() * Math.cos(phi1) - cs.c() * Math.sin(phi1);
225         boolean cShape = 0 < phi1 && phi1 < phi2 && phi2 < Math.PI && h < 0; // otherwise, S-shape
226         double theta = getTheta(phi1, phi2, cShape);
227         double aSign = cShape ? -1.0 : 1.0;
228         double thetaSign = -aSign;
229 
230         double v1 = theta + phi1 + phi2;
231         double v2 = theta + phi1;
232         Fresnel cs0 = Fresnel.integral(alphaToT(theta));
233         Fresnel cs1 = Fresnel.integral(alphaToT(v1));
234         this.a = d2 / ((cs1.s() + aSign * cs0.s()) * Math.sin(v2) + (cs1.c() + aSign * cs0.c()) * Math.cos(v2));
235 
236         dx /= d2; // normalized
237         dy /= d2;
238         if (this.reflected)
239         {
240             // reflect t0 and n0 on 'd' so that the created output clothoid is reflected back after input was reflected
241             this.t0 = new double[] {Math.cos(-v2) * dx + Math.sin(-v2) * dy, -Math.sin(-v2) * dx + Math.cos(-v2) * dy};
242             this.n0 = new double[] {-this.t0[1], this.t0[0]};
243         }
244         else
245         {
246             this.t0 = new double[] {Math.cos(v2) * dx + Math.sin(v2) * dy, -Math.sin(v2) * dx + Math.cos(v2) * dy};
247             this.n0 = new double[] {this.t0[1], -this.t0[0]};
248         }
249 
250         this.alphaMin = thetaSign * theta;
251         this.alphaMax = v1; // alphaMax = theta + phi1 + phi2, which is v1
252         double sign = (this.reflected ? -1.0 : 1.0);
253         double curveMin = Math.PI * alphaToT(this.alphaMin) / this.a;
254         double curveMax = Math.PI * alphaToT(v1) / this.a;
255         this.startCurvature = sign * (this.opposite ? -curveMax : curveMin);
256         this.endCurvature = sign * (this.opposite ? -curveMin : curveMax);
257         this.length = this.a * (alphaToT(v1) - alphaToT(this.alphaMin));
258     }
259 
260     /**
261      * Create clothoid from one point based on curvature and A-value.
262      * @param startPoint start point.
263      * @param a A-value.
264      * @param startCurvature start curvature.
265      * @param endCurvature end curvature;
266      */
267     public ContinuousClothoid(final OrientedPoint2d startPoint, final double a, final double startCurvature,
268             final double endCurvature)
269     {
270         Throw.whenNull(startPoint, "Start point may not be null.");
271         Throw.when(a <= 0.0, IllegalArgumentException.class, "A value must be above 0.");
272         this.startPoint = startPoint;
273         // Scale 'a', due to parameter conversion between C(alpha)/S(alpha) and C(t)/S(t); t = sqrt(2*alpha/pi).
274         this.a = a * Math.sqrt(Math.PI);
275         this.length = a * a * Math.abs(endCurvature - startCurvature);
276         this.startCurvature = startCurvature;
277         this.endCurvature = endCurvature;
278 
279         double l1 = a * a * startCurvature;
280         double l2 = a * a * endCurvature;
281         this.alphaMin = Math.abs(l1) * startCurvature / 2.0;
282         this.alphaMax = Math.abs(l2) * endCurvature / 2.0;
283 
284         double ang = AngleUtil.normalizeAroundZero(startPoint.dirZ) - Math.abs(this.alphaMin);
285         this.t0 = new double[] {Math.cos(ang), Math.sin(ang)};
286         this.n0 = new double[] {this.t0[1], -this.t0[0]};
287         Direction endDirection = Direction.instantiateSI(ang + Math.abs(this.alphaMax));
288         if (startCurvature > endCurvature)
289         {
290             // In these cases the algorithm works in the negative direction. We need to flip over the line through the start
291             // point that runs perpendicular to the start direction.
292             double m = Math.tan(startPoint.dirZ + Math.PI / 2.0);
293 
294             // Linear algebra flipping, see: https://math.stackexchange.com/questions/525082/reflection-across-a-line
295             double onePlusMm = 1.0 + m * m;
296             double oneMinusMm = 1.0 - m * m;
297             double mmMinusOne = m * m - 1.0;
298             double twoM = 2.0 * m;
299             double t00 = this.t0[0];
300             double t01 = this.t0[1];
301             double n00 = this.n0[0];
302             double n01 = this.n0[1];
303             this.t0[0] = (oneMinusMm * t00 + 2 * m * t01) / onePlusMm;
304             this.t0[1] = (twoM * t00 + mmMinusOne * t01) / onePlusMm;
305             this.n0[0] = (oneMinusMm * n00 + 2 * m * n01) / onePlusMm;
306             this.n0[1] = (twoM * n00 + mmMinusOne * n01) / onePlusMm;
307 
308             double ang2 = Math.atan2(this.t0[1], this.t0[0]);
309             endDirection = Direction.instantiateSI(ang2 - Math.abs(this.alphaMax) + Math.PI);
310         }
311         PolyLine2d line = flatten(new Flattener.NumSegments(1));
312         Point2d end = Try.assign(() -> line.get(line.size() - 1), "Line does not have an end point.");
313         this.endPoint = new OrientedPoint2d(end.x, end.y, endDirection.si);
314 
315         // Fields not relevant for definition with curvatures
316         this.straight = null;
317         this.arc = null;
318         this.opposite = false;
319         this.reflected = false;
320     }
321 
322     /**
323      * Create clothoid from one point based on curvature and length. This method calculates the A-value as
324      * <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
325      * and start curvature.
326      * @param startPoint start point.
327      * @param length Length of the resulting clothoid.
328      * @param startCurvature start curvature.
329      * @param endCurvature end curvature;
330      * @return clothoid based on curvature and length.
331      */
332     public static ContinuousClothoid withLength(final OrientedPoint2d startPoint, final double length,
333             final double startCurvature, final double endCurvature)
334     {
335         Throw.when(length <= 0.0, IllegalArgumentException.class, "Length must be above 0.");
336         double a = Math.sqrt(length / Math.abs(endCurvature - startCurvature));
337         return new ContinuousClothoid(startPoint, a, startCurvature, endCurvature);
338     }
339 
340     /**
341      * Performs alpha to t variable change.
342      * @param alpha alpha value, must be positive.
343      * @return t value (length along the Fresnel integral, also known as x).
344      */
345     private static double alphaToT(final double alpha)
346     {
347         return alpha >= 0 ? Math.sqrt(alpha * 2.0 / Math.PI) : -Math.sqrt(-alpha * 2.0 / Math.PI);
348     }
349 
350     /**
351      * Returns theta value given shape to use. If no such value is found, the other shape may be attempted.
352      * @param phi1 phi1.
353      * @param phi2 phi2.
354      * @param cShape C-shaped, or S-shaped otherwise.
355      * @return theta value; the number of radians that is moved on to a side of the full clothoid.
356      */
357     private static double getTheta(final double phi1, final double phi2, final boolean cShape)
358     {
359         double sign, phiMin, phiMax;
360         if (cShape)
361         {
362             double lambda = (1 - Math.cos(phi1)) / (1 - Math.cos(phi2));
363             phiMin = 0.0;
364             phiMax = (lambda * lambda * (phi1 + phi2)) / (1 - (lambda * lambda));
365             sign = -1.0;
366         }
367         else
368         {
369             phiMin = Math.max(0, -phi1);
370             phiMax = Math.PI / 2 - phi1;
371             sign = 1;
372         }
373 
374         double fMin = fTheta(phiMin, phi1, phi2, sign);
375         double fMax = fTheta(phiMax, phi1, phi2, sign);
376         if (fMin * fMax > 0)
377         {
378             throw new RuntimeException("f(phiMin) and f(phiMax) have the same sign, we cant find f(theta) = 0 between them.");
379         }
380 
381         // Find optimum using Secant method, see https://en.wikipedia.org/wiki/Secant_method
382         double x0 = phiMin;
383         double x1 = phiMax;
384         double x2 = 0;
385         for (int i = 0; i < 100; i++) // max 100 iterations, otherwise use latest x2 value
386         {
387             double f1 = fTheta(x1, phi1, phi2, sign);
388             x2 = x1 - f1 * (x1 - x0) / (f1 - fTheta(x0, phi1, phi2, sign));
389             x2 = Math.max(Math.min(x2, phiMax), phiMin); // this line is an essential addition to keep the algorithm at bay
390             x0 = x1;
391             x1 = x2;
392             if (Math.abs(x0 - x1) < SECANT_TOLERANCE || Math.abs(x0 / x1 - 1) < SECANT_TOLERANCE
393                     || Math.abs(f1) < SECANT_TOLERANCE)
394             {
395                 return x2;
396             }
397         }
398 
399         return x2;
400     }
401 
402     /**
403      * 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
404      * that solves fitting a C-shaped clothoid through two points. This assumes that <i>sign</i> = -1. If <i>sign</i> = 1, this
405      * changes to <i>g</i>(<i>theta</i>) = 0 being a solution for an S-shaped clothoid.
406      * @param theta angle defining the curvature of the resulting clothoid.
407      * @param phi1 angle between the line through both end points, and the direction of the first point.
408      * @param phi2 angle between the line through both end points, and the direction of the last point.
409      * @param sign 1 for C-shaped, -1 for S-shaped.
410      * @return <i>f</i>(<i>theta</i>) for <i>sign</i> = -1, or <i>g</i>(<i>theta</i>) for <i>sign</i> = 1.
411      */
412     private static double fTheta(final double theta, final double phi1, final double phi2, final double sign)
413     {
414         double thetaPhi1 = theta + phi1;
415         Fresnel cs0 = Fresnel.integral(alphaToT(theta));
416         Fresnel cs1 = Fresnel.integral(alphaToT(thetaPhi1 + phi2));
417         return (cs1.s() + sign * cs0.s()) * Math.cos(thetaPhi1) - (cs1.c() + sign * cs0.c()) * Math.sin(thetaPhi1);
418     }
419 
420     @Override
421     public OrientedPoint2d getStartPoint()
422     {
423         return this.startPoint;
424     }
425 
426     @Override
427     public OrientedPoint2d getEndPoint()
428     {
429         return this.endPoint;
430     }
431 
432     @Override
433     public double getStartCurvature()
434     {
435         return this.startCurvature;
436     }
437 
438     @Override
439     public double getEndCurvature()
440     {
441         return this.endCurvature;
442     }
443 
444     @Override
445     public double getStartRadius()
446     {
447         return 1.0 / this.startCurvature;
448     }
449 
450     @Override
451     public double getEndRadius()
452     {
453         return 1.0 / this.endCurvature;
454     }
455 
456     /**
457      * Return A, the clothoid scaling parameter.
458      * @return a, the clothoid scaling parameter.
459      */
460     public double getA()
461     {
462         // Scale 'a', due to parameter conversion between C(alpha)/S(alpha) and C(t)/S(t); t = sqrt(2*alpha/pi).
463         // The value of 'this.a' is used when scaling the Fresnel integral, which is why this is stored.
464         return this.a / Math.sqrt(Math.PI);
465     }
466 
467     /**
468      * Calculates shifts if these have not yet been calculated.
469      */
470     private void assureShift()
471     {
472         if (this.shiftDetermined)
473         {
474             return;
475         }
476 
477         OrientedPoint2d p1 = this.opposite ? this.endPoint : this.startPoint;
478         OrientedPoint2d p2 = this.opposite ? this.startPoint : this.endPoint;
479 
480         // Create first point to figure out the required overall shift
481         Fresnel csMin = Fresnel.integral(alphaToT(this.alphaMin));
482         double xMin = this.a * (csMin.c() * this.t0[0] - csMin.s() * this.n0[0]);
483         double yMin = this.a * (csMin.c() * this.t0[1] - csMin.s() * this.n0[1]);
484         this.shiftX = p1.x - xMin;
485         this.shiftY = p1.y - yMin;
486 
487         // Due to numerical precision, we linearly scale over alpha such that the final point is exactly on p2
488         if (p2 != null)
489         {
490             Fresnel csMax = Fresnel.integral(alphaToT(this.alphaMax));
491             double xMax = this.a * (csMax.c() * this.t0[0] - csMax.s() * this.n0[0]);
492             double yMax = this.a * (csMax.c() * this.t0[1] - csMax.s() * this.n0[1]);
493             this.dShiftX = p2.x - (xMax + this.shiftX);
494             this.dShiftY = p2.y - (yMax + this.shiftY);
495         }
496         else
497         {
498             this.dShiftX = 0.0;
499             this.dShiftY = 0.0;
500         }
501 
502         this.shiftDetermined = true;
503     }
504 
505     /**
506      * Returns a point on the clothoid at a fraction of curvature along the clothoid.
507      * @param fraction fraction of curvature along the clothoid.
508      * @param offset offset relative to radius.
509      * @return point on the clothoid at a fraction of curvature along the clothoid.
510      */
511     private Point2d getPoint(final double fraction, final double offset)
512     {
513         double f = this.opposite ? 1.0 - fraction : fraction;
514         double alpha = this.alphaMin + f * (this.alphaMax - this.alphaMin);
515         Fresnel cs = Fresnel.integral(alphaToT(alpha));
516         double x = this.shiftX + this.a * (cs.c() * this.t0[0] - cs.s() * this.n0[0]) + f * this.dShiftX;
517         double y = this.shiftY + this.a * (cs.c() * this.t0[1] - cs.s() * this.n0[1]) + f * this.dShiftY;
518         double d = getDirection(alpha) + Math.PI / 2;
519         return new Point2d(x + Math.cos(d) * offset, y + Math.sin(d) * offset);
520     }
521 
522     /**
523      * Returns the direction at given alpha.
524      * @param alpha alpha.
525      * @return direction at given alpha.
526      */
527     private double getDirection(final double alpha)
528     {
529         double rot = Math.atan2(this.t0[1], this.t0[0]);
530         // abs because alpha = -3deg has the same direction as alpha = 3deg in an S-curve where alpha = 0 is the middle
531         rot += this.reflected ? -Math.abs(alpha) : Math.abs(alpha);
532         if (this.opposite)
533         {
534             rot += Math.PI;
535         }
536         return AngleUtil.normalizeAroundZero(rot);
537     }
538 
539     @Override
540     public PolyLine2d flatten(final Flattener flattener)
541     {
542         Throw.whenNull(flattener, "Flattener may not be null.");
543         if (this.straight != null)
544         {
545             return this.straight.flatten(flattener);
546         }
547         if (this.arc != null)
548         {
549             return this.arc.flatten(flattener);
550         }
551         assureShift();
552         return flattener.flatten(new FlattableLine()
553         {
554             @Override
555             public Point2d get(final double fraction)
556             {
557                 return getPoint(fraction, 0.0);
558             }
559 
560             @Override
561             public double getDirection(final double fraction)
562             {
563                 return ContinuousClothoid.this.getDirection(ContinuousClothoid.this.alphaMin
564                         + fraction * (ContinuousClothoid.this.alphaMax - ContinuousClothoid.this.alphaMin));
565             }
566         });
567     }
568 
569     @Override
570     public PolyLine2d flattenOffset(final ContinuousDoubleFunction offset, final Flattener flattener)
571     {
572         Throw.whenNull(offset, "Offsets may not be null.");
573         Throw.whenNull(flattener, "Flattener may not be null.");
574         if (this.straight != null)
575         {
576             return this.straight.flattenOffset(offset, flattener);
577         }
578         if (this.arc != null)
579         {
580             return this.arc.flattenOffset(offset, flattener);
581         }
582         assureShift();
583         return flattener.flatten(new FlattableLine()
584         {
585             @Override
586             public Point2d get(final double fraction)
587             {
588                 return getPoint(fraction, offset.apply(fraction));
589             }
590 
591             @Override
592             public double getDirection(final double fraction)
593             {
594                 double derivativeOffset = offset.getDerivative(fraction) / ContinuousClothoid.this.length;
595                 return ContinuousClothoid.this.getDirection(ContinuousClothoid.this.alphaMin
596                         + fraction * (ContinuousClothoid.this.alphaMax - ContinuousClothoid.this.alphaMin))
597                         + Math.atan(derivativeOffset);
598             }
599         });
600     }
601 
602     @Override
603     public double getLength()
604     {
605         return this.length;
606     }
607 
608     /**
609      * Returns whether the shape was applied as a Clothoid, an Arc, or as a Straight, depending on start and end position and
610      * direction.
611      * @return "Clothoid", "Arc" or "Straight".
612      */
613     public String getAppliedShape()
614     {
615         return this.straight == null ? (this.arc == null ? "Clothoid" : "Arc") : "Straight";
616     }
617 
618     @Override
619     public String toString()
620     {
621         return "ContinuousClothoid [startPoint=" + this.startPoint + ", endPoint=" + this.endPoint + ", startCurvature="
622                 + this.startCurvature + ", endCurvature=" + this.endCurvature + ", length=" + this.length + "]";
623     }
624 
625 }