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ézier info</a>
20 */
21 public class Bezier2d implements Curve2d
22 {
23
24 /** The x-coordinates of the points of this Bézier. */
25 @SuppressWarnings("checkstyle:visibilitymodifier")
26 protected final double[] x;
27
28 /** The y-coordinates of the points of this Bézier. */
29 @SuppressWarnings("checkstyle:visibilitymodifier")
30 protected final double[] y;
31
32 /**
33 * Create a Bézier curve of any order.
34 * @param points Point2d... shape points that define the Bé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ézier curve of any order.
55 * @param x the x-coordinates of the points that define the Bézier curve
56 * @param y the y-coordinates of the points that define the Bé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é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ézier curve
71 * @param y the y-coordinates of the points that define the Bé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ézier, which is a Bézier of 1 order lower.
90 * @return derivative Bé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ézier curve is based on.
100 * @return the number of points (or x-y pairs) that this Bé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é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ézier curve.
128 * @param i the index
129 * @return the x-coordinate of the i'th point of this Bézier curve
130 * @throws IndexOutOfBoundsException when <code>i < 0</code>, or <code>i ≥ 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ézier curve.
139 * @param i the index
140 * @return the y-coordinate of the i'th point of this Bézier curve
141 * @throws IndexOutOfBoundsException when <code>i < 0</code>, or <code>i ≥ 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é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 }