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