View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import org.djunits.value.vdouble.scalar.Angle;
4   import org.djutils.draw.line.PolyLine2d;
5   import org.djutils.draw.point.OrientedPoint2d;
6   import org.djutils.draw.point.Point2d;
7   import org.djutils.exceptions.Throw;
8   
9   /**
10   * Continuous definition of an arc.
11   * <p>
12   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
13   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
14   * </p>
15   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
16   */
17  public class ContinuousArc implements ContinuousLine
18  {
19  
20      /** Starting point. */
21      private final OrientedPoint2d startPoint;
22  
23      /** Curve radius. */
24      private final double radius;
25  
26      /** Angle of the curve. */
27      private final Angle angle;
28  
29      /** Sign to use for offsets and angles, which depends on the left/right direction. */
30      private double sign;
31  
32      /** Center point of circle, as calculated in constructor. */
33      private final Point2d center;
34  
35      /**
36       * Define arc by starting point, radius, curve direction, and length.
37       * @param startPoint starting point.
38       * @param radius radius (must be positive).
39       * @param left left curve, or right.
40       * @param length arc length.
41       */
42      public ContinuousArc(final OrientedPoint2d startPoint, final double radius, final boolean left, final double length)
43      {
44          this(startPoint, radius, left, Angle.instantiateSI(
45                  Throw.when(length, length <= 0.0, IllegalArgumentException.class, "Length must be above 0.") / radius));
46      }
47  
48      /**
49       * Define arc by starting point, radius, curve direction, and angle.
50       * @param startPoint starting point.
51       * @param radius radius (must be positive).
52       * @param left left curve, or right.
53       * @param angle angle of arc (must be positive).
54       */
55      public ContinuousArc(final OrientedPoint2d startPoint, final double radius, final boolean left, final Angle angle)
56      {
57          Throw.whenNull(startPoint, "Start point may not be null.");
58          Throw.when(radius < 0.0, IllegalArgumentException.class, "Radius must be positive.");
59          Throw.when(angle.si < 0.0, IllegalArgumentException.class, "Angle must be positive.");
60          this.startPoint = startPoint;
61          this.radius = radius;
62          this.sign = left ? 1.0 : -1.0;
63          this.angle = angle;
64          double dx = Math.cos(startPoint.dirZ) * this.sign * radius;
65          double dy = Math.sin(startPoint.dirZ) * this.sign * radius;
66  
67          this.center = new Point2d(startPoint.x - dy, startPoint.y + dx);
68      }
69  
70      @Override
71      public OrientedPoint2d getStartPoint()
72      {
73          return this.startPoint;
74      }
75  
76      @Override
77      public OrientedPoint2d getEndPoint()
78      {
79          Point2d point = getPoint(1.0, 0.0);
80          double dirZ = this.startPoint.dirZ + this.sign * this.angle.si;
81          dirZ = dirZ > Math.PI ? dirZ - 2.0 * Math.PI : (dirZ < -Math.PI ? dirZ + 2.0 * Math.PI : 0.0);
82          return new OrientedPoint2d(point.x, point.y, dirZ);
83      }
84  
85      @Override
86      public double getStartCurvature()
87      {
88          return 1.0 / this.radius;
89      }
90  
91      @Override
92      public double getEndCurvature()
93      {
94          return getStartCurvature();
95      }
96  
97      @Override
98      public double getStartRadius()
99      {
100         return this.radius;
101     }
102 
103     @Override
104     public double getEndRadius()
105     {
106         return this.radius;
107     }
108 
109     /**
110      * Returns a point on the arc at a fraction along the arc.
111      * @param fraction fraction along the arc.
112      * @param offset offset relative to radius.
113      * @return point on the arc at a fraction along the arc.
114      */
115     private Point2d getPoint(final double fraction, final double offset)
116     {
117         double len = this.radius - this.sign * offset;
118         double ang = this.startPoint.dirZ + this.sign * (this.angle.si * fraction);
119         double dx = this.sign * Math.cos(ang) * len;
120         double dy = this.sign * Math.sin(ang) * len;
121         return new Point2d(this.center.x + dy, this.center.y - dx);
122     }
123 
124     /**
125      * Returns the direction at given fraction along the arc.
126      * @param fraction fraction along the arc.
127      * @return direction at given fraction along the arc.
128      */
129     private double getDirection(final double fraction)
130     {
131         double d = this.startPoint.dirZ + this.sign * (this.angle.si) * fraction;
132         while (d > Math.PI)
133         {
134             d -= (2 * Math.PI);
135         }
136         while (d < -Math.PI)
137         {
138             d += (2 * Math.PI);
139         }
140         return d;
141     }
142 
143     @Override
144     public PolyLine2d flatten(final Flattener flattener)
145     {
146         Throw.whenNull(flattener, "Flattener may not be null.");
147         return flattener.flatten(new FlattableLine()
148         {
149             @Override
150             public Point2d get(final double fraction)
151             {
152                 return getPoint(fraction, 0.0);
153             }
154 
155             @Override
156             public double getDirection(final double fraction)
157             {
158                 return ContinuousArc.this.getDirection(fraction);
159             }
160         });
161     }
162 
163     @Override
164     public PolyLine2d flattenOffset(final ContinuousDoubleFunction offset, final Flattener flattener)
165     {
166         Throw.whenNull(offset, "Offset may not be null.");
167         Throw.whenNull(flattener, "Flattener may not be null.");
168         return flattener.flatten(new FlattableLine()
169         {
170             @Override
171             public Point2d get(final double fraction)
172             {
173                 return getPoint(fraction, offset.apply(fraction));
174             }
175 
176             @Override
177             public double getDirection(final double fraction)
178             {
179                 /*-
180                  * x = cos(phi) * (r - s(phi))
181                  * y = sin(phi) * (r - s(phi)) 
182                  * 
183                  * with,
184                  *   phi    = angle of circle arc point at fraction, relative to circle center
185                  *   r      = radius
186                  *   s(phi) = offset at phi (or at fraction)
187                  * 
188                  * then using the product rule: 
189                  * 
190                  * x' = -sin(phi) * (r - s(phi)) - cos(phi) * s'(phi)
191                  * y' = cos(phi) * (r - s(phi)) - sin(phi) * s'(phi)
192                  */
193                 double phi = (ContinuousArc.this.startPoint.dirZ
194                         + ContinuousArc.this.sign * (ContinuousArc.this.angle.si * fraction - Math.PI / 2));
195                 double sinPhi = Math.sin(phi);
196                 double cosPhi = Math.cos(phi);
197                 double sPhi = ContinuousArc.this.sign * offset.apply(fraction);
198                 double sPhiD = offset.getDerivative(fraction) / ContinuousArc.this.angle.si;
199                 double dx = -sinPhi * (ContinuousArc.this.radius - sPhi) - cosPhi * sPhiD;
200                 double dy = cosPhi * (ContinuousArc.this.radius - sPhi) - sinPhi * sPhiD;
201                 return Math.atan2(ContinuousArc.this.sign * dy, ContinuousArc.this.sign * dx);
202             }
203         });
204     }
205 
206     @Override
207     public double getLength()
208     {
209         return this.angle.si * this.radius;
210     }
211 
212     @Override
213     public String toString()
214     {
215         return "ContinuousArc [startPoint=" + this.startPoint + ", radius=" + this.radius + ", angle=" + this.angle + ", left="
216                 + (this.sign > 0.0) + "]";
217     }
218 
219 }