1 package org.djutils.draw.point;
2
3 import java.util.Arrays;
4 import java.util.Iterator;
5 import java.util.Locale;
6
7 import org.djutils.draw.Oriented3d;
8 import org.djutils.exceptions.Throw;
9 import org.djutils.math.AngleUtil;
10
11 /**
12 * A OrientedPoint3d is a point with an x, y, and z coordinate, plus a 3d orientation. The orientation is specified by the
13 * rotations around the x, y, and z-axis. A number of constructors and methods are provided for cases where only the rotation
14 * around the z-axis is of importance. Orientation in 3D is stored as three double values dirX,dirY,dirZ. This class does
15 * <b>not</b> prescribe a particular order in which these rotations are to be applied. (Applying rotations is <b>not</b>
16 * commutative, so this <i>is</i> important.)
17 * <p>
18 * Copyright (c) 2020-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
19 * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
20 * </p>
21 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
22 * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
23 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
24 */
25 public class OrientedPoint3d extends DirectedPoint3d implements Oriented3d
26 {
27 /** The direction as rotation around the x-axis. */
28 @SuppressWarnings("checkstyle:visibilitymodifier")
29 public final double dirX;
30
31 /**
32 * Create a new OrientedPoint3d with x, y, and z coordinates and direction 0,0,0.
33 * @param x the x coordinate
34 * @param y the y coordinate
35 * @param z the z coordinate
36 * @throws IllegalArgumentException when <code>x</code>, <code>y</code>, or <code>z</code> is <code>NaN</code>
37 */
38 public OrientedPoint3d(final double x, final double y, final double z)
39 {
40 this(x, y, z, 0.0, 0.0, 0.0);
41 }
42
43 /**
44 * Create a new OrientedPoint3d with x, y, and z coordinates and orientation dirX,dirY,dirZ.
45 * @param x the x coordinate
46 * @param y the y coordinate
47 * @param z the z coordinate
48 * @param dirX the direction as rotation around the x-axis with the point as the center
49 * @param dirY the direction as rotation around the y-axis with the point as the center
50 * @param dirZ the direction as rotation around the z-axis with the point as the center
51 * @throws ArithmeticException when <code>x</code>, <code>y</code>, <code>z</code>, <code>dirX</code>, <code>dirY</code>, or
52 * <code>dirZ</code> is <code>NaN</code>
53 */
54 public OrientedPoint3d(final double x, final double y, final double z, final double dirX, final double dirY,
55 final double dirZ)
56 {
57 super(x, y, z, dirY, dirZ);
58 Throw.whenNaN(dirX, "dirX");
59 this.dirX = dirX;
60 }
61
62 /**
63 * Create a new OrientedPoint3d with x, y, and z coordinates and direction 0,0,0.
64 * @param xyz the x, y and z coordinates
65 * @throws NullPointerException when <code>xyz</code> is <code>null</code>
66 * @throws IllegalArgumentException when the length of the <code>xyx</code> array is not 3
67 * @throws IllegalArgumentException when the <code>xyx</code> array contains a <code>NaN</code> value
68 */
69 public OrientedPoint3d(final double[] xyz)
70 {
71 super(xyz, 0, 0);
72 this.dirX = 0.0;
73 }
74
75 /**
76 * Create a new OrientedPoint3d with x, y, and z coordinates and orientation dirX,dirY,dirZ.
77 * @param xyz the x, y and z coordinates
78 * @param dirX the direction as rotation around the x-axis with the point as the center
79 * @param dirY the direction as rotation around the y-axis with the point as the center
80 * @param dirZ the direction as rotation around the z-axis with the point as the center
81 * @throws NullPointerException when <code>xyx</code> is <code>null</code>
82 * @throws IllegalArgumentException when the length of the xyz array is not 3
83 * @throws ArithmeticException when <code>xyz</code> contains a <code>NaN</code> value, or <code>dirX</code>,
84 * <code>dirY</code>, or <code>dirZ</code> is <code>NaN</code>
85 */
86 public OrientedPoint3d(final double[] xyz, final double dirX, final double dirY, final double dirZ)
87 throws NullPointerException, IllegalArgumentException
88 {
89 super(xyz, dirY, dirZ);
90 Throw.whenNaN(dirX, "dirX");
91 this.dirX = dirX;
92 }
93
94 /**
95 * Create a new OrientedPoint3d from another point and specified orientation dirX,dirY,dirZ.
96 * @param point the point from which this OrientedPoint3d will be instantiated
97 * @param dirX the direction as rotation around the x-axis with the point as the center
98 * @param dirY the direction as rotation around the y-axis with the point as the center
99 * @param dirZ the direction as rotation around the z-axis with the point as the center
100 * @throws NullPointerException when <code>point</code> is <code>null</code>
101 * @throws ArithmeticException when <code>dirX</code>, <code>dirY</code>, or <code>dirZ</code> is <code>NaN</code>
102 */
103 public OrientedPoint3d(final Point3d point, final double dirX, final double dirY, final double dirZ)
104 {
105 this(point.x, point.y, point.z, dirX, dirY, dirZ);
106 }
107
108 /**
109 * Verify that a double array is not null, has three elements.
110 * @param orientation the array to check
111 * @return the first element of the argument
112 * @throws NullPointerException when <code>orientation</code> is <code>null</code>
113 * @throws IllegalArgumentException when the length of the <code>orientation</code> array is not 3
114 */
115 private static double checkOrientationVector(final double[] orientation)
116 {
117 Throw.when(orientation.length != 3, IllegalArgumentException.class, "length of orientation array must be 3");
118 return orientation[0];
119 }
120
121 /**
122 * Create a new OrientedPoint3d with x, y, and z coordinates and orientation specified using a double array of three
123 * elements (containing dirX,dirY,dirZ in that order).
124 * @param x the x coordinate
125 * @param y the y coordinate
126 * @param z the z coordinate
127 * @param orientation the three orientation values as rotations around the x,y,z-axes in a double array containing
128 * dirX,dirY,dirZ in that order
129 * @throws NullPointerException when <code>rotation</code> is <code>null</code>
130 * @throws IllegalArgumentException when the length of the <code>direction</code> array is not 3
131 */
132 public OrientedPoint3d(final double x, final double y, final double z, final double[] orientation)
133 {
134 this(x, y, z, checkOrientationVector(orientation), orientation[1], orientation[2]);
135 }
136
137 /**
138 * Create a new OrientedPoint3d with x, y, and z coordinates packed in a double array and orientation specified using a
139 * double array of three elements (containing dirX,dirY,dirZ in that order).
140 * @param xyz the x, y and z coordinates in that order
141 * @param orientation the three orientation values as rotations around the x,y,z-axes in a double array containing
142 * dirX,dirY,dirZ in that order
143 * @throws NullPointerException when <code>xyx</code> or <code>direction</code> is <code>null</code>
144 * @throws IllegalArgumentException when the length of the <code>xyx</code> array or the length of the
145 * <code>orientation</code> array is not 3
146 */
147 public OrientedPoint3d(final double[] xyz, final double[] orientation)
148 {
149 this(xyz, checkOrientationVector(orientation), orientation[1], orientation[2]);
150 }
151
152 @Override
153 public OrientedPoint3d translate(final double dX, final double dY)
154 {
155 Throw.whenNaN(dX, "dX");
156 Throw.whenNaN(dY, "dY");
157 return new OrientedPoint3d(this.x + dX, this.y + dY, this.z, this.dirX, this.dirY, this.dirZ);
158 }
159
160 @Override
161 public OrientedPoint3d translate(final double dX, final double dY, final double dZ)
162 {
163 Throw.whenNaN(dX, "dX");
164 Throw.whenNaN(dY, "dY");
165 Throw.whenNaN(dZ, "dZ");
166 return new OrientedPoint3d(this.x + dX, this.y + dY, this.z + dZ, this.dirX, this.dirY, this.dirZ);
167 }
168
169 @Override
170 public OrientedPoint3d scale(final double factor)
171 {
172 return new OrientedPoint3d(this.x * factor, this.y * factor, this.z * factor, this.dirX, this.dirY, this.dirZ);
173 }
174
175 @Override
176 public OrientedPoint3d neg()
177 {
178 return new OrientedPoint3d(-this.x, -this.y, -this.z, AngleUtil.normalizeAroundZero(this.dirX + Math.PI),
179 AngleUtil.normalizeAroundZero(this.dirY + Math.PI), AngleUtil.normalizeAroundZero(this.dirZ + Math.PI));
180 }
181
182 @Override
183 public OrientedPoint3d abs()
184 {
185 return new OrientedPoint3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z), this.dirX, this.dirY, this.dirZ);
186 }
187
188 @Override
189 public OrientedPoint3d normalize() throws IllegalArgumentException
190 {
191 double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
192 Throw.when(length == 0.0, IllegalArgumentException.class, "cannot normalize (0.0, 0.0, 0.0)");
193 return new OrientedPoint3d(this.x / length, this.y / length, this.z / length, this.dirX, this.dirY, this.dirZ);
194 }
195
196 /**
197 * Interpolate towards another OrientedPoint3d with a fraction. It is allowed for fraction to be less than zero or larger
198 * than 1. In that case the interpolation turns into an extrapolation. DirX, dirY and dirZ are interpolated/extrapolated
199 * using the interpolateShortest method.
200 * @param otherPoint the other point
201 * @param fraction the factor for interpolation towards the other point. When <code>fraction</code> is between 0
202 * and 1, it is an interpolation, otherwise an extrapolation. If <code>fraction</code> is 0; <code>this</code>
203 * Point is returned; if <code>fraction</code> is 1, the <code>otherPoint</code> is returned
204 * @return a new <code>OrientedPoint3d</code> at the requested <code>fraction</code>
205 * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
206 * @throws ArithmeticException when <code>fraction</code> is <code>NaN</code>
207 */
208 public OrientedPoint3d interpolate(final OrientedPoint3d otherPoint, final double fraction)
209 {
210 Throw.whenNull(otherPoint, "otherPoint");
211 Throw.whenNaN(fraction, "fraction");
212 if (0.0 == fraction)
213 {
214 return this;
215 }
216 if (1.0 == fraction)
217 {
218 return otherPoint;
219 }
220 return new OrientedPoint3d((1.0 - fraction) * this.x + fraction * otherPoint.x,
221 (1.0 - fraction) * this.y + fraction * otherPoint.y, (1.0 - fraction) * this.z + fraction * otherPoint.z,
222 AngleUtil.interpolateShortest(this.dirX, otherPoint.dirX, fraction),
223 AngleUtil.interpolateShortest(this.dirY, otherPoint.dirY, fraction),
224 AngleUtil.interpolateShortest(this.dirZ, otherPoint.dirZ, fraction));
225 }
226
227 /**
228 * Return a new OrientedPoint3d with an in-place rotation around the z-axis by the provided rotateZ. The resulting rotation
229 * will be normalized between -π and π.
230 * @param rotateZ the rotation around the z-axis
231 * @return a new point with the same coordinates, <code>dirX</code> and <code>dirY</code> and modified <code>dirZ</code>
232 * @throws ArithmeticException when <code>rotateZ</code> is <code>NaN</code>
233 */
234 @Override
235 public OrientedPoint3d rotate(final double rotateZ)
236 {
237 Throw.whenNaN(rotateZ, "rotZ");
238 return new OrientedPoint3d(this.x, this.y, this.z, this.dirX, this.dirY,
239 AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
240 }
241
242 /**
243 * Return a new OrientedPoint3d point with an in-place rotation by the provided rotateX, rotateY, and rotateZ. The resulting
244 * rotations will be normalized between -π and π.
245 * @param rotateX the rotation around the x-axis
246 * @param rotateY the rotation around the y-axis
247 * @param rotateZ the rotation around the z-axis
248 * @return a new point with the same coordinates and applied rotations
249 * @throws ArithmeticException when any of the rotations is <code>NaN</code>
250 */
251 public OrientedPoint3d rotate(final double rotateX, final double rotateY, final double rotateZ)
252 {
253 Throw.whenNaN(rotateX, "rotateX");
254 Throw.whenNaN(rotateY, "rotateY");
255 Throw.whenNaN(rotateZ, "rotateZ");
256 return new OrientedPoint3d(this.x, this.y, this.z, AngleUtil.normalizeAroundZero(this.dirX + rotateX),
257 AngleUtil.normalizeAroundZero(this.dirY + rotateY), AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
258 }
259
260 @Override
261 public double getDirX()
262 {
263 return this.dirX;
264 }
265
266 @Override
267 public double getDirY()
268 {
269 return this.dirY;
270 }
271
272 @Override
273 public double getDirZ()
274 {
275 return this.dirZ;
276 }
277
278 @Override
279 public Iterator<Point3d> iterator()
280 {
281 return Arrays.stream(new Point3d[] {this}).iterator();
282 }
283
284 @Override
285 public String toString()
286 {
287 return toString("%f", false);
288 }
289
290 @Override
291 public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
292 {
293 String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2$s, rotX=%2$s, rotY=%2$s, rotZ=%2$s]",
294 doNotIncludeClassName ? "" : "OrientedPoint3d ", doubleFormat);
295 return String.format(Locale.US, format, this.x, this.y, this.z, this.dirX, this.dirY, this.dirZ);
296 }
297
298 /**
299 * Compare this Directed with another Directed with specified tolerances in the coordinates and the angles.
300 * @param other the Directed to compare to
301 * @param epsilonCoordinate the upper bound of difference for one of the coordinates; use Double.POSITIVE_INFINITY if you do
302 * not want to check the coordinates
303 * @param epsilonRotation the upper bound of difference for the rotation(s); use Double.POSITIVE_INFINITY if you do not want
304 * to check the angles
305 * @return boolean;<code>true</code> if <code>x</code>, <code>y</code>, and possibly <code>z</code> are less than
306 * <code>epsilonCoordinate</code> apart, and <code>rotZ</code> and possibly <code>rotX</code>, and possibly
307 * <code>rotY</code>are less than <code>epsilonDirection</code> apart, otherwise <code>false</code>
308 * @throws NullPointerException when <code>other</code> is <code>null</code>
309 * @throws ArithmeticException when <code>epsilonCoordinate</code> or <code>epsilonDirection</code> is <code>NaN</code>
310 * @throws IllegalArgumentException <code>epsilonCoordinate</code> or <code>epsilonDirection</code> is <code>negative</code>
311 */
312 public boolean epsilonEquals(final OrientedPoint3d other, final double epsilonCoordinate, final double epsilonRotation)
313 throws NullPointerException, IllegalArgumentException
314 {
315 Throw.whenNull(other, "other");
316 if (Math.abs(this.x - other.x) > epsilonCoordinate)
317 {
318 return false;
319 }
320 if (Math.abs(this.y - other.y) > epsilonCoordinate)
321 {
322 return false;
323 }
324 if (Math.abs(this.z - other.z) > epsilonCoordinate)
325 {
326 return false;
327 }
328 if (Math.abs(AngleUtil.normalizeAroundZero(this.dirX - other.dirX)) > epsilonRotation)
329 {
330 return false;
331 }
332 if (Math.abs(AngleUtil.normalizeAroundZero(this.dirY - other.dirY)) > epsilonRotation)
333 {
334 return false;
335 }
336 if (Math.abs(AngleUtil.normalizeAroundZero(this.dirZ - other.dirZ)) > epsilonRotation)
337 {
338 return false;
339 }
340 return true;
341 }
342
343 @Override
344 public int hashCode()
345 {
346 final int prime = 31;
347 int result = super.hashCode();
348 long temp;
349 temp = Double.doubleToLongBits(this.dirX);
350 result = prime * result + (int) (temp ^ (temp >>> 32));
351 temp = Double.doubleToLongBits(this.dirY);
352 result = prime * result + (int) (temp ^ (temp >>> 32));
353 temp = Double.doubleToLongBits(this.dirZ);
354 result = prime * result + (int) (temp ^ (temp >>> 32));
355 return result;
356 }
357
358 @Override
359 @SuppressWarnings("checkstyle:needbraces")
360 public boolean equals(final Object obj)
361 {
362 if (this == obj)
363 return true;
364 if (!super.equals(obj))
365 return false;
366 OrientedPoint3d other = (OrientedPoint3d) obj;
367 if (Double.doubleToLongBits(this.dirX) != Double.doubleToLongBits(other.dirX))
368 return false;
369 if (Double.doubleToLongBits(this.dirY) != Double.doubleToLongBits(other.dirY))
370 return false;
371 if (Double.doubleToLongBits(this.dirZ) != Double.doubleToLongBits(other.dirZ))
372 return false;
373 return true;
374 }
375
376 }