View Javadoc
1   package org.djutils.draw.curve;
2   
3   import java.util.Arrays;
4   
5   import org.djutils.draw.line.PolyLine2d;
6   import org.djutils.draw.point.Point2d;
7   import org.djutils.exceptions.Throw;
8   
9   /**
10   * Continuous definition of a Bézier curve in 2d. This class is simply a helper class for (and a super of)
11   * {@code BezierCubic2d}, which uses this class to determine curvature, offset lines, etc.
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   * @see <a href="https://pomax.github.io/bezierinfo/">B&eacute;zier info</a>
20   */
21  public class Bezier2d implements Curve2d
22  {
23  
24      /** The x-coordinates of the points of this B&eacute;zier. */
25      @SuppressWarnings("checkstyle:visibilitymodifier")
26      protected final double[] x;
27  
28      /** The y-coordinates of the points of this B&eacute;zier. */
29      @SuppressWarnings("checkstyle:visibilitymodifier")
30      protected final double[] y;
31  
32      /**
33       * Create a B&eacute;zier curve of any order.
34       * @param points Point2d... shape points that define the B&eacute;zier curve
35       * @throws NullPointerException when <code>points</code> is <code>null</code>, or contains a <code>null</code> value
36       * @throws IllegalArgumentException when the length of <code>points</code> is less than <code>2</code>
37       */
38      public Bezier2d(final Point2d... points)
39      {
40          Throw.when(points.length < 2, IllegalArgumentException.class, "minimum number of points is 2");
41          this.x = new double[points.length];
42          this.y = new double[points.length];
43          int index = 0;
44          for (Point2d point : points)
45          {
46              Throw.whenNull(point, "One of the points is null");
47              this.x[index] = point.x;
48              this.y[index] = point.y;
49              index++;
50          }
51      }
52  
53      /**
54       * Create a B&eacute;zier curve of any order.
55       * @param x the x-coordinates of the points that define the B&eacute;zier curve
56       * @param y the y-coordinates of the points that define the B&eacute;zier curve
57       * @throws NullPointerException when <code>x</code>, or <code>y</code> is <code>null</code>
58       * @throws IllegalArgumentException when the length of the <code>x</code> array is not equal to the length of the
59       *             <code>y</code> array, or less than <code>2</code>
60       */
61      public Bezier2d(final double[] x, final double[] y)
62      {
63          this(true, x, y);
64      }
65  
66      /**
67       * Construct a B&eacute;zier curve of any order, optionally checking the lengths of the provided arrays.
68       * @param checkLengths if <code>true</code>; check the lengths of the <code>x</code> and <code>y</code> arrays; if
69       *            <code>false</code>; do not check those lengths
70       * @param x the x-coordinates of the points that define the B&eacute;zier curve
71       * @param y the y-coordinates of the points that define the B&eacute;zier curve
72       * @throws NullPointerException when <code>x</code>, or <code>y</code> is <code>null</code>
73       * @throws IllegalArgumentException when the length of <code>x</code> is not equal to the length of <code>y</code>, or less
74       *             than <code>2</code> and <code>checkLengths</code> is <code>true</code>
75       */
76      private Bezier2d(final boolean checkLengths, final double[] x, final double[] y)
77      {
78          if (checkLengths)
79          {
80              Throw.when(x.length < 2, IllegalArgumentException.class, "minimum number of points is 2");
81              Throw.when(x.length != y.length, IllegalArgumentException.class,
82                      "length of x-array must be same as length of y-array");
83          }
84          this.x = Arrays.copyOf(x, x.length);
85          this.y = Arrays.copyOf(y, y.length);
86      }
87  
88      /**
89       * Returns the derivative for a B&eacute;zier, which is a B&eacute;zier of 1 order lower.
90       * @return derivative B&eacute;zier
91       * @throws IllegalStateException when the order of <code>this Bezier2d</code> is <code>1</code>
92       */
93      public Bezier2d derivative()
94      {
95          return new Bezier2d(false, Bezier.derivative(this.x), Bezier.derivative(this.y));
96      }
97  
98      /**
99       * Return the number of points (or x-y pairs) that this B&eacute;zier curve is based on.
100      * @return the number of points (or x-y pairs) that this B&eacute;zier curve is based on
101      */
102     public int size()
103     {
104         return this.x.length;
105     }
106 
107     /**
108      * Returns the estimated path length of this B&eacute;zier curve using the method of numerical approach of Legendre-Gauss,
109      * which is quite accurate.
110      * @return estimated length.
111      */
112     private double length()
113     {
114         double len = 0.0;
115         Bezier2d derivativeBezier = derivative();
116         for (int i = 0; i < Bezier.T.length; i++)
117         {
118             double t = 0.5 * Bezier.T[i] + 0.5;
119             Point2d p = derivativeBezier.getPoint(t);
120             len += Bezier.C[i] * Math.hypot(p.x, p.y);
121         }
122         len *= 0.5;
123         return len;
124     }
125 
126     /**
127      * Retrieve the x-coordinate of the i'th point of this B&eacute;zier curve.
128      * @param i the index
129      * @return the x-coordinate of the i'th point of this B&eacute;zier curve
130      * @throws IndexOutOfBoundsException when <code>i &lt; 0</code>, or <code>i &ge; size()</code>
131      */
132     public double getX(final int i)
133     {
134         return this.x[i];
135     }
136 
137     /**
138      * Retrieve the y-coordinate of the i'th point of this B&eacute;zier curve.
139      * @param i the index
140      * @return the y-coordinate of the i'th point of this B&eacute;zier curve
141      * @throws IndexOutOfBoundsException when <code>i &lt; 0</code>, or <code>i &ge; size()</code>
142      */
143     public double getY(final int i)
144     {
145         return this.y[i];
146     }
147 
148     @Override
149     public Point2d getPoint(final double t)
150     {
151         return new Point2d(Bezier.Bn(t, this.x), Bezier.Bn(t, this.y));
152     }
153 
154     /** Cache the result of the getLength method. */
155     private double cachedLength = -1;
156 
157     @Override
158     public double getLength()
159     {
160         if (this.cachedLength < 0)
161         {
162             this.cachedLength = length();
163         }
164         return this.cachedLength;
165     }
166 
167     /**
168      * Returns the curvature at the given t value.
169      * @param t t value, moving from 0 to 1 along the B&eacute;zier.
170      * @return double curvature at the given t value.
171      */
172     public double curvature(final double t)
173     {
174         Bezier2d derivativeBezier = derivative();
175         Point2d d = derivativeBezier.getPoint(t);
176         double denominator = Math.pow(d.x * d.x + d.y * d.y, 3.0 / 2.0);
177         if (denominator == 0.0)
178         {
179             return Double.POSITIVE_INFINITY;
180         }
181         Point2d dd = derivativeBezier.derivative().getPoint(t);
182         double numerator = d.x * dd.y - dd.x * d.y;
183         return numerator / denominator;
184     }
185 
186     @Override
187     public PolyLine2d toPolyLine(final Flattener2d flattener)
188     {
189         return flattener.flatten(this);
190     }
191 
192     // If toString is regenerated, take care to remove cashedLength from the result.
193     @Override
194     public String toString()
195     {
196         return "Bezier2d [x=" + Arrays.toString(this.x) + ", y=" + Arrays.toString(this.y) + "]";
197     }
198 
199     @Override
200     public int hashCode()
201     {
202         final int prime = 31;
203         int result = 1;
204         result = prime * result + Arrays.hashCode(this.x);
205         result = prime * result + Arrays.hashCode(this.y);
206         return result;
207     }
208 
209     @Override
210     @SuppressWarnings("checkstyle:needbraces")
211     public boolean equals(final Object obj)
212     {
213         if (this == obj)
214             return true;
215         if (obj == null)
216             return false;
217         if (getClass() != obj.getClass())
218             return false;
219         Bezier2d other = (Bezier2d) obj;
220         return Arrays.equals(this.x, other.x) && Arrays.equals(this.y, other.y);
221     }
222 
223 }