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 OrientedPoint2d; starting point.
38       * @param radius double; radius (must be positive).
39       * @param left boolean; left curve, or right.
40       * @param length double; 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 OrientedPoint2d; starting point.
51       * @param radius double; radius (must be positive).
52       * @param left boolean; left curve, or right.
53       * @param angle 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      /** {@inheritDoc} */
71      @Override
72      public OrientedPoint2d getStartPoint()
73      {
74          return this.startPoint;
75      }
76  
77      /** {@inheritDoc} */
78      @Override
79      public OrientedPoint2d getEndPoint()
80      {
81          Point2d point = getPoint(1.0, 0.0);
82          double dirZ = this.startPoint.dirZ + this.sign * this.angle.si;
83          dirZ = dirZ > Math.PI ? dirZ - 2.0 * Math.PI : (dirZ < -Math.PI ? dirZ + 2.0 * Math.PI : 0.0);
84          return new OrientedPoint2d(point.x, point.y, dirZ);
85      }
86  
87      /** {@inheritDoc} */
88      @Override
89      public double getStartCurvature()
90      {
91          return 1.0 / this.radius;
92      }
93  
94      /** {@inheritDoc} */
95      @Override
96      public double getEndCurvature()
97      {
98          return getStartCurvature();
99      }
100 
101     /** {@inheritDoc} */
102     @Override
103     public double getStartRadius()
104     {
105         return this.radius;
106     }
107 
108     /** {@inheritDoc} */
109     @Override
110     public double getEndRadius()
111     {
112         return this.radius;
113     }
114 
115     /**
116      * Returns a point on the arc at a fraction along the arc.
117      * @param fraction double; fraction along the arc.
118      * @param offset double; offset relative to radius.
119      * @return Point2d; point on the arc at a fraction along the arc.
120      */
121     private Point2d getPoint(final double fraction, final double offset)
122     {
123         double len = this.radius - this.sign * offset;
124         double angle = this.startPoint.dirZ + this.sign * (this.angle.si * fraction);
125         double dx = this.sign * Math.cos(angle) * len;
126         double dy = this.sign * Math.sin(angle) * len;
127         return new Point2d(this.center.x + dy, this.center.y - dx);
128     }
129 
130     /**
131      * Returns the direction at given fraction along the arc.
132      * @param fraction double; fraction along the arc.
133      * @return double; direction at given fraction along the arc.
134      */
135     private double getDirection(final double fraction)
136     {
137         double d = this.startPoint.dirZ + this.sign * (this.angle.si) * fraction;
138         while (d > Math.PI)
139         {
140             d -= (2 * Math.PI);
141         }
142         while (d < -Math.PI)
143         {
144             d += (2 * Math.PI);
145         }
146         return d;
147     }
148 
149     /** {@inheritDoc} */
150     @Override
151     public PolyLine2d flatten(final Flattener flattener)
152     {
153         Throw.whenNull(flattener, "Flattener may not be null.");
154         return flattener.flatten(new FlattableLine()
155         {
156             /** {@inheritDoc} */
157             @Override
158             public Point2d get(final double fraction)
159             {
160                 return getPoint(fraction, 0.0);
161             }
162 
163             /** {@inheritDoc} */
164             @Override
165             public double getDirection(final double fraction)
166             {
167                 return ContinuousArc.this.getDirection(fraction);
168             }
169         });
170     }
171 
172     /** {@inheritDoc} */
173     @Override
174     public PolyLine2d flattenOffset(final FractionalLengthData offsets, final Flattener flattener)
175     {
176         Throw.whenNull(offsets, "Offsets may not be null.");
177         Throw.whenNull(flattener, "Flattener may not be null.");
178         return flattener.flatten(new FlattableLine()
179         {
180             /** {@inheritDoc} */
181             @Override
182             public Point2d get(final double fraction)
183             {
184                 return getPoint(fraction, offsets.get(fraction));
185             }
186 
187             /** {@inheritDoc} */
188             @Override
189             public double getDirection(final double fraction)
190             {
191                 /*-
192                  * x = cos(phi) * (r - s(phi))
193                  * y = sin(phi) * (r - s(phi)) 
194                  * 
195                  * with,
196                  *   phi    = angle of circle arc point at fraction, relative to circle center
197                  *   r      = radius
198                  *   s(phi) = offset at phi (or at fraction)
199                  * 
200                  * then using the product rule: 
201                  * 
202                  * x' = -sin(phi) * (r - s(phi)) - cos(phi) * s'(phi)
203                  * y' = cos(phi) * (r - s(phi)) - sin(phi) * s'(phi)
204                  */
205                 double phi = (ContinuousArc.this.startPoint.dirZ
206                         + ContinuousArc.this.sign * (ContinuousArc.this.angle.si * fraction - Math.PI / 2));
207                 double sinPhi = Math.sin(phi);
208                 double cosPhi = Math.cos(phi);
209                 double sPhi = ContinuousArc.this.sign * offsets.get(fraction);
210                 double sPhiD = offsets.getDerivative(fraction) / ContinuousArc.this.angle.si;
211                 double dx = -sinPhi * (ContinuousArc.this.radius - sPhi) - cosPhi * sPhiD;
212                 double dy = cosPhi * (ContinuousArc.this.radius - sPhi) - sinPhi * sPhiD;
213                 return Math.atan2(ContinuousArc.this.sign * dy, ContinuousArc.this.sign * dx);
214             }
215         });
216     }
217 
218     /** {@inheritDoc} */
219     @Override
220     public double getLength()
221     {
222         return this.angle.si * this.radius;
223     }
224 
225     /** {@inheritDoc} */
226     @Override
227     public String toString()
228     {
229         return "ContinuousArc [startPoint=" + this.startPoint + ", radius=" + this.radius + ", angle=" + this.angle + ", left="
230                 + (this.sign > 0.0) + "]";
231     }
232 
233 }