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.math.AngleUtil;
9   
10  /**
11   * Continuous definition of an arc in 2d.
12   * <p>
13   * Copyright (c) 2023-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
14   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
15   * </p>
16   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
17   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
18   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
19   */
20  public class Arc2d implements Curvature, Curve2d, OffsetCurve2d
21  {
22  
23      /** Starting point. */
24      private final DirectedPoint2d startPoint;
25  
26      /** Curve radius. */
27      private final double radius;
28  
29      /** Angle of the curve. */
30      private final double angle;
31  
32      /** Sign to use for offsets and angles, which depends on the left/right direction. */
33      private final double sign;
34  
35      /** Center point of circle, as calculated in constructor. */
36      private final Point2d center;
37  
38      /**
39       * Define arc by starting point, radius, curve direction, and angle.
40       * @param startPoint starting point
41       * @param radius radius (must be positive)
42       * @param left <code>true</code>for left turning curve; <code>false</code> for for right turning curve
43       * @param angle angle of arc (must be positive)
44       */
45      public Arc2d(final DirectedPoint2d startPoint, final double radius, final boolean left, final double angle)
46      {
47          Throw.whenNull(startPoint, "startPoint");
48          Throw.when(radius < 0.0, IllegalArgumentException.class, "radius must be positive");
49          Throw.when(angle < 0.0, IllegalArgumentException.class, "angle must be positive");
50          this.startPoint = startPoint;
51          this.radius = radius;
52          this.sign = left ? 1.0 : -1.0;
53          this.angle = angle;
54          double dx = Math.cos(startPoint.dirZ) * this.sign * radius;
55          double dy = Math.sin(startPoint.dirZ) * this.sign * radius;
56          this.center = new Point2d(startPoint.x - dy, startPoint.y + dx);
57      }
58  
59      @Override
60      public double getStartCurvature()
61      {
62          return 1.0 / this.radius;
63      }
64  
65      @Override
66      public double getEndCurvature()
67      {
68          return getStartCurvature();
69      }
70  
71      @Override
72      public double getStartRadius()
73      {
74          return this.radius;
75      }
76  
77      @Override
78      public double getEndRadius()
79      {
80          return this.radius;
81      }
82  
83      /**
84       * Does this arc bend to the left?
85       * @return <code>true</code> if this Arc bends to the left; <code>false</code> if this Arc bends to the right
86       */
87      public boolean isLeft()
88      {
89          return this.sign > 0;
90      }
91  
92      /**
93       * Retrieve the total change of direction on this Arc.
94       * @return the total change of direction on this Arc
95       */
96      public double getAngle()
97      {
98          return this.angle;
99      }
100 
101     /**
102      * Compute the point at the provided fraction of this Arc while applying the provided lateral offset.
103      * @param fraction the fraction along this Arc
104      * @param offset the lateral offset to apply
105      * @return the point at the provided fraction of this Arc with the provided lateral offset applied
106      */
107     private Point2d getPoint(final double fraction, final double offset)
108     {
109         double len = this.radius - this.sign * offset;
110         double a = this.startPoint.dirZ + this.sign * this.angle * fraction;
111         double dx = this.sign * Math.cos(a) * len;
112         double dy = this.sign * Math.sin(a) * len;
113         return new Point2d(this.center.x + dy, this.center.y - dx);
114     }
115 
116     @Override
117     public Point2d getPoint(final double fraction)
118     {
119         return getPoint(fraction, 0);
120     }
121 
122     @Override
123     public Point2d getPoint(final double fraction, final ContinuousPiecewiseLinearFunction of)
124     {
125         return getPoint(fraction, of.get(fraction));
126     }
127 
128     @Override
129     public Double getDirection(final double fraction)
130     {
131         return AngleUtil.normalizeAroundZero(this.startPoint.dirZ + this.sign * this.angle * fraction);
132     }
133 
134     @Override
135     public double getDirection(final double fraction, final ContinuousPiecewiseLinearFunction of)
136     {
137         /*-
138          * x = cos(phi) * (r - s(phi))
139          * y = sin(phi) * (r - s(phi)) 
140          * 
141          * with,
142          *   phi    = angle of circle arc point at fraction, relative to circle center
143          *   r      = radius
144          *   s(phi) = offset at phi (or at fraction)
145          * 
146          * then using the product rule: 
147          * 
148          * x' = -sin(phi) * (r - s(phi)) - cos(phi) * s'(phi)
149          * y' = cos(phi) * (r - s(phi)) - sin(phi) * s'(phi)
150          */
151         double phi = (Arc2d.this.startPoint.dirZ + Arc2d.this.sign * (Arc2d.this.angle * fraction - Math.PI / 2));
152         double sinPhi = Math.sin(phi);
153         double cosPhi = Math.cos(phi);
154         double sPhi = Arc2d.this.sign * of.get(fraction);
155         double sPhiD = of.getDerivative(fraction) / Arc2d.this.angle;
156         double dx = -sinPhi * (Arc2d.this.radius - sPhi) - cosPhi * sPhiD;
157         double dy = cosPhi * (Arc2d.this.radius - sPhi) - sinPhi * sPhiD;
158         double direction = Math.atan2(Arc2d.this.sign * dy, Arc2d.this.sign * dx);
159         return direction;
160     }
161 
162     @Override
163     public PolyLine2d toPolyLine(final Flattener2d flattener)
164     {
165         Throw.whenNull(flattener, "Flattener");
166         return flattener.flatten(this);
167     }
168 
169     @Override
170     public PolyLine2d toPolyLine(final OffsetFlattener2d flattener, final ContinuousPiecewiseLinearFunction offsets)
171     {
172         Throw.whenNull(offsets, "Offsets");
173         return flattener.flatten(this, offsets);
174     }
175 
176     @Override
177     public double getLength()
178     {
179         return this.angle * this.radius;
180     }
181 
182     @Override
183     public String toString()
184     {
185         return "Arc [startPoint=" + this.startPoint + ", radius=" + this.radius + ", angle=" + this.angle + ", left="
186                 + (this.sign > 0.0) + "]";
187     }
188 
189 }