1 package org.djutils.draw.point;
2
3 import java.util.Arrays;
4 import java.util.Iterator;
5 import java.util.Locale;
6 import java.util.Objects;
7
8 import org.djutils.draw.Directed3d;
9 import org.djutils.draw.Direction3d;
10 import org.djutils.exceptions.Throw;
11 import org.djutils.math.AngleUtil;
12
13 /**
14 * A DirectedPoint3d is a point in 3d space that additionally carries a direction in 3d i.c. dirY (similar to tilt; measured as
15 * an angle from the positive z-direction) and dirZ (similar to pan; measured as an angle from the positive x-direction).
16 * <p>
17 * Copyright (c) 2023-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
18 * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
19 * distributed under a three-clause BSD-style license, which can be found at
20 * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
21 * </p>
22 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
23 * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
24 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
25 */
26 public class DirectedPoint3d extends Point3d implements Directed3d
27 {
28 /** The direction as rotation around the x-axis. */
29 @SuppressWarnings("checkstyle:visibilitymodifier")
30 public final double dirY;
31
32 /** The direction as rotation from the positive z-axis towards the x-y plane. */
33 @SuppressWarnings("checkstyle:visibilitymodifier")
34 public final double dirZ;
35
36 /**
37 * Create a new DirectedPoint3d with x, y, and z coordinates and direction dirY,dirZ.
38 * @param x the x coordinate
39 * @param y the y coordinate
40 * @param z the z coordinate
41 * @param dirY the complement of the slope
42 * @param dirZ the counter-clockwise rotation around the point in radians
43 * @throws ArithmeticException when <code>x</code>, <code>y</code>, <code>z</code>, <code>dirY</code>, or <code>dirZ</code>
44 * is <code>NaN</code>
45 */
46 public DirectedPoint3d(final double x, final double y, final double z, final double dirY, final double dirZ)
47 {
48 super(x, y, z);
49 Throw.whenNaN(dirY, "dirY");
50 Throw.whenNaN(dirZ, "dirZ");
51 this.dirZ = dirZ;
52 this.dirY = dirY;
53 }
54
55 /**
56 * Create a new DirectedPoint3d with x, y and z coordinates and direction specified using a double array of two elements
57 * (containing dirY,dirZ in that order).
58 * @param x the x coordinate
59 * @param y the y coordinate
60 * @param z the z coordinate
61 * @param directionVector the two direction angles (dirY and dirZ) in a double array containing dirY and dirZ in that order.
62 * DirY is the rotation from the positive z-axis to the direction. DirZ is the angle from the positive x-axis to
63 * the projection of the direction in the x-y-plane.
64 * @throws NullPointerException when <code>directionVector</code> is <code>null</code>
65 * @throws ArithmeticException when <code>x</code>, <code>y</code>, <code>z</code> is <code>NaN</code>, or
66 * <code>directionVector</code> contains a <code>NaN</code> value
67 * @throws IllegalArgumentException when the length of the <code>directionVector</code> array is not 2, or contains a
68 * <code>NaN</code> value
69 */
70 public DirectedPoint3d(final double x, final double y, final double z, final double[] directionVector)
71 {
72 this(x, y, z, checkDirectionVector(directionVector), directionVector[1]);
73 }
74
75 /**
76 * Create a new DirectedPoint3d with x, y, and z coordinates and Direction3d.
77 * @param x the x coordinate
78 * @param y the y coordinate
79 * @param z the z coordinate
80 * @param dir the direction
81 * @throws NullPointerException when <code>dir></code> is <code>null</code>
82 * @throws ArithmeticException when <code>x</code>, <code>y</code>, <code>z</code>, <code>dirY</code>, or <code>dirZ</code>
83 * is <code>NaN</code>
84 */
85 public DirectedPoint3d(final double x, final double y, final double z, final Direction3d dir)
86 {
87 super(x, y, z);
88 Throw.whenNull(dir, "dir");
89 this.dirZ = dir.dirZ;
90 this.dirY = dir.dirY;
91 }
92
93 /**
94 * Construct a new DirectedPoint3d from three coordinates and the coordinates of a point that the direction goes through.
95 * @param x the x coordinate of the new DirectedPoint
96 * @param y the y coordinate of the new DirectedPoint
97 * @param z the z coordinate of the new DirectedPoint
98 * @param throughX the x-coordinate of a point that the direction goes through
99 * @param throughY the y-coordinate of a point that the direction goes through
100 * @param throughZ the z-coordinate of a point that the direction goes through
101 * @throws ArithmeticException when <code>z</code>, <code>y</code>, <code>z</code>, <code>throughX</code>,
102 * <code>throughY</code>, or <code>throughZ</code> is <code>NaN</code>
103 * @throws IllegalArgumentException when <code>throughX</code> == <code>x</code> and <code>throughY</code> == <code>y</code>
104 * and <code>throughZ</code> == <code>z</code>
105 */
106 public DirectedPoint3d(final double x, final double y, final double z, final double throughX, final double throughY,
107 final double throughZ)
108 {
109 this(x, y, z, buildDirectionVector(throughX - x, throughY - y, throughZ - z));
110 }
111
112 /**
113 * Construct a new DirectedPoint3d from x, y and z coordinates and a point that the direction goes through.
114 * @param x the x coordinate of the new DirectedPoint3d
115 * @param y the y coordinate of the new DirectedPoint3d
116 * @param z the z coordinate of the new DirectedPoint3d
117 * @param throughPoint a point that the direction goes through
118 * @throws NullPointerException when <code>throughPoint</code> is <code>null</code>
119 * @throws ArithmeticException when <code>x</code>, <code>y</code>, or <code>z</code> is <code>NaN</code>
120 * @throws IllegalArgumentException when <code>throughPoint</code> is exactly at <code>(x y,z)</code>
121 */
122 public DirectedPoint3d(final double x, final double y, final double z, final Point3d throughPoint)
123 {
124 this(x, y, z, Throw.whenNull(throughPoint, "througPoint").x, throughPoint.y, throughPoint.z);
125 }
126
127 /**
128 * Create a new DirectedPoint3d with x, y, and z coordinates in a double[] and direction dirY,dirZ.
129 * @param xyz the x, y and z coordinates
130 * @param dirY the complement of the slope
131 * @param dirZ the counter-clockwise rotation around the point in radians
132 * @throws NullPointerException when <code>xyx</code> is <code>null</code>
133 * @throws IllegalArgumentException when the length of the <code>xyz</code> array is not 3, or contains a <code>NaN</code>
134 * value, or <code>dirY</code>, or <code>dirZ</code> is <code>NaN</code>
135 */
136 public DirectedPoint3d(final double[] xyz, final double dirY, final double dirZ)
137 {
138 super(xyz);
139 Throw.whenNaN(dirY, "dirY");
140 Throw.whenNaN(dirZ, "dirZ");
141 this.dirY = dirY;
142 this.dirZ = dirZ;
143 }
144
145 /**
146 * Create a new OrientedPoint3d from x, y and z coordinates packed in a double array of three elements and a direction
147 * specified using a double array of two elements.
148 * @param xyz the <code>x</code>, <code>y</code> and <code>z</code> coordinates in that order
149 * @param directionVector the two direction angles <code>dirY</code> and <code>dirZ</code> in that order
150 * @throws NullPointerException when <code>xyz</code>, or <code>directionVector</code> is <code>null</code>
151 * @throws ArithmeticException when <code>xyz</code>, or <code>directionVector</code> contains a <code>NaN</code> value
152 * @throws IllegalArgumentException when the length of the <code>xyx</code> is not 3 or the length of the
153 * <code>directionVector</code> is not 2
154 */
155 public DirectedPoint3d(final double[] xyz, final double[] directionVector)
156 {
157 this(xyz, checkDirectionVector(directionVector), directionVector[1]);
158 }
159
160 /**
161 * Create a new DirectedPoint3d with x, y, and z coordinates in a double[] and a Direction3d.
162 * @param xyz the x, y and z coordinates
163 * @param dir the direction
164 * @throws NullPointerException when <code>xyx</code> is <code>null</code>, or <code>dir</code> is <code>null</code>
165 * @throws IllegalArgumentException when the length of the <code>xyz</code> array is not 3, or contains a <code>NaN</code>
166 * value
167 */
168 public DirectedPoint3d(final double[] xyz, final Direction3d dir)
169 {
170 super(xyz);
171 Throw.whenNull(dir, "dir");
172 this.dirY = dir.dirY;
173 this.dirZ = dir.dirZ;
174 }
175
176 /**
177 * Create a new DirectedPoint3d from another Point3d and and direction dirY,dirZ.
178 * @param point the point from which this OrientedPoint3d will be instantiated
179 * @param dirY the complement of the slope
180 * @param dirZ the counter-clockwise rotation around the point in radians
181 * @throws ArithmeticException when <code>dirY</code>, or <code>dirZ</code> is <code>NaN</code>
182 */
183 public DirectedPoint3d(final Point3d point, final double dirY, final double dirZ)
184 {
185 this(point.x, point.y, point.z, dirY, dirZ);
186 }
187
188 /**
189 * Create a new DirectedPoint3d from another Point3d and a Direction3d.
190 * @param point the point from which this OrientedPoint3d will be instantiated
191 * @param direction the direction
192 * @throws NullPointerException when <code>point</code>, or <code>direction</code> is <code>null</code>
193 */
194 public DirectedPoint3d(final Point3d point, final Direction3d direction)
195 {
196 this(point.x, point.y, point.z, Throw.whenNull(direction, "direction").dirY, direction.dirZ);
197 }
198
199 /**
200 * Construct a new DirectedPoint3d form a Point3d and the coordinates that the direction goes through.
201 * @param point the point
202 * @param throughX the x coordinate of a point that the direction goes through
203 * @param throughY the y coordinate of a point that the direction goes through
204 * @param throughZ the z coordinate of a point that the direction goes through
205 * @throws NullPointerException when <code>point</code> is <code>null</code>
206 * @throws ArithmeticException when <code>throughX</code>, or <code>throughY</code>, or <code>throughZ</code> is
207 * <code>NaN</code>
208 * @throws IllegalArgumentException when <code>throughX</code> == <code>point.x</code> and <code>throughY</code> ==
209 * <code>point.y</code> and <code>throughZ</code> == <code>point.z</code>
210 */
211 public DirectedPoint3d(final Point3d point, final double throughX, final double throughY, final double throughZ)
212 {
213 this(Throw.whenNull(point, "point").x, point.y, point.z, throughX, throughY, throughZ);
214 }
215
216 /**
217 * Construct a new DirectedPoint3d.
218 * @param point the location of the new DirectedPoint3d
219 * @param throughPoint another point that the direction goes through
220 * @throws NullPointerException when <code>point</code> is <code>null</code> or <code>throughPoint</code> is
221 * <code>null</code>
222 * @throws IllegalArgumentException when <code>throughPoint</code> is exactly at <code>point</code>
223 */
224 public DirectedPoint3d(final Point3d point, final Point3d throughPoint)
225 {
226 this(Throw.whenNull(point, "point").x, point.y, point.z, Throw.whenNull(throughPoint, "throughPoint").x, throughPoint.y,
227 throughPoint.z);
228 }
229
230 /**
231 * Build the direction vector.
232 * @param dX x difference
233 * @param dY y difference
234 * @param dZ z difference
235 * @return a two-element array containing dirY and dirZ
236 * @throws IllegalArgumentException when <code>dX</code> == <code>0.0</code> and <code>dY</code> == <code>0.0</code> and
237 * <code>dZ</code> == <code>0.0</code>
238 */
239 private static double[] buildDirectionVector(final double dX, final double dY, final double dZ)
240 {
241 Throw.when(0 == dX && 0 == dY && 0 == dZ, IllegalArgumentException.class, "Through point may not be equal to point");
242 return new double[] {Math.atan2(Math.hypot(dX, dY), dZ), Math.atan2(dY, dX)};
243 }
244
245 /**
246 * Verify that a double array is not null, has two elements.
247 * @param direction the array to check
248 * @return the first element of the argument
249 * @throws NullPointerException when <code>direction</code> is <code>null</code>
250 * @throws IllegalArgumentException when the length of the <code>direction</code> array is not 2
251 */
252 private static double checkDirectionVector(final double[] direction)
253 {
254 Throw.when(direction.length != 2, IllegalArgumentException.class, "length of direction array must be 2");
255 return direction[0];
256 }
257
258 @Override
259 public DirectedPoint3d translate(final double dX, final double dY)
260 {
261 Throw.whenNaN(dX, "dX");
262 Throw.whenNaN(dY, "dY");
263 return new DirectedPoint3d(this.x + dX, this.y + dY, this.z, this.dirY, this.dirZ);
264 }
265
266 @Override
267 public DirectedPoint3d translate(final double dX, final double dY, final double dZ)
268 {
269 Throw.whenNaN(dX, "dX");
270 Throw.whenNaN(dY, "dY");
271 Throw.whenNaN(dZ, "dZ");
272 return new DirectedPoint3d(this.x + dX, this.y + dY, this.z + dZ, this.dirY, this.dirZ);
273 }
274
275 @Override
276 public DirectedPoint3d scale(final double factor)
277 {
278 return new DirectedPoint3d(this.x * factor, this.y * factor, this.z * factor, this.dirY, this.dirZ);
279 }
280
281 @Override
282 public DirectedPoint3d neg()
283 {
284 return new DirectedPoint3d(-this.x, -this.y, -this.z, AngleUtil.normalizeAroundZero(this.dirY + Math.PI),
285 AngleUtil.normalizeAroundZero(this.dirZ + Math.PI));
286 }
287
288 @Override
289 public DirectedPoint3d abs()
290 {
291 return new DirectedPoint3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z), this.dirY, this.dirZ);
292 }
293
294 @Override
295 public DirectedPoint3d normalize() throws IllegalArgumentException
296 {
297 double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
298 Throw.when(length == 0.0, IllegalArgumentException.class, "cannot normalize (0.0, 0.0, 0.0)");
299 return new DirectedPoint3d(this.x / length, this.y / length, this.z / length, this.dirY, this.dirZ);
300 }
301
302 /**
303 * Interpolate towards another DirectedPoint3d with a fraction. It is allowed for fraction to be less than zero or larger
304 * than 1. In that case the interpolation turns into an extrapolation. DirY and dirZ are interpolated/extrapolated using the
305 * interpolateShortest method.
306 * @param otherPoint the other point
307 * @param fraction the factor for interpolation towards the other point. When <code>fraction</code> is between 0
308 * and 1, it is an interpolation, otherwise an extrapolation. If <code>fraction</code> is 0; <code>this</code>
309 * Point is returned; if <code>fraction</code> is 1, the <code>otherPoint</code> is returned
310 * @return a new <code>DirectedPoint3d</code> at the requested <code>fraction</code>
311 * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
312 * @throws ArithmeticException when <code>fraction</code> is <code>NaN</code>
313 */
314 public DirectedPoint3d interpolate(final DirectedPoint3d otherPoint, final double fraction)
315 {
316 Throw.whenNull(otherPoint, "otherPoint");
317 Throw.whenNaN(fraction, "fraction");
318 if (0.0 == fraction)
319 {
320 return this;
321 }
322 if (1.0 == fraction)
323 {
324 return otherPoint;
325 }
326 return new DirectedPoint3d((1.0 - fraction) * this.x + fraction * otherPoint.x,
327 (1.0 - fraction) * this.y + fraction * otherPoint.y, (1.0 - fraction) * this.z + fraction * otherPoint.z,
328 AngleUtil.interpolateShortest(this.dirY, otherPoint.dirY, fraction),
329 AngleUtil.interpolateShortest(this.dirZ, otherPoint.dirZ, fraction));
330 }
331
332 /**
333 * Return a new DirectedPoint3d with an in-place rotation around the z-axis by the provided rotateZ. The resulting rotation
334 * will be normalized between -π and π.
335 * @param rotateZ the rotation around the z-axis
336 * @return a new point with the same coordinates, <code>dirY</code> and modified <code>dirZ</code>
337 * @throws ArithmeticException when <code>rotateZ</code> is <code>NaN</code>
338 */
339 public DirectedPoint3d rotate(final double rotateZ)
340 {
341 Throw.whenNaN(rotateZ, "rotateZ");
342 return new DirectedPoint3d(this.x, this.y, this.z, this.dirY, AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
343 }
344
345 /**
346 * Return a new DirectedPoint3d point with an in-place rotation by the provided rotateY, and rotateZ. The resulting
347 * rotations will be normalized between -π and π.
348 * @param rotateY the rotation around the y-axis
349 * @param rotateZ the rotation around the z-axis
350 * @return a new point with the same coordinates and applied rotations
351 * @throws ArithmeticException when <code>rotateY</code>, or <code>rotateZ</code> is <code>NaN</code>
352 */
353 public DirectedPoint3d rotate(final double rotateY, final double rotateZ)
354 {
355 Throw.whenNaN(rotateY, "rotateY");
356 Throw.whenNaN(rotateZ, "rotateZ");
357 return new DirectedPoint3d(this.x, this.y, this.z, AngleUtil.normalizeAroundZero(this.dirY + rotateY),
358 AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
359 }
360
361 @Override
362 public double getDirZ()
363 {
364 return this.dirZ;
365 }
366
367 @Override
368 public double getDirY()
369 {
370 return this.dirY;
371 }
372
373 @Override
374 public Iterator<Point3d> iterator()
375 {
376 return Arrays.stream(new Point3d[] {this}).iterator();
377 }
378
379 @Override
380 public String toString()
381 {
382 return toString("%f", false);
383 }
384
385 @Override
386 public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
387 {
388 String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2%s, dirY=%2$s, dirZ=%2$s]",
389 doNotIncludeClassName ? "" : "DirectedPoint3d ", doubleFormat);
390 return String.format(Locale.US, format, this.x, this.y, this.z, this.dirY, this.dirZ);
391 }
392
393 /**
394 * Compare this Directed with another Directed with specified tolerances in the coordinates and the angles.
395 * @param other the Directed to compare to
396 * @param epsilonCoordinate the upper bound of difference for one of the coordinates; use Double.POSITIVE_INFINITY if you do
397 * not want to check the coordinates
398 * @param epsilonDirection the upper bound of difference for the direction(s); use Double.POSITIVE_INFINITY if you do not
399 * want to check the angles
400 * @return boolean;<code>true</code> if <code>x</code>, <code>y</code>, and possibly <code>z</code> are less than
401 * <code>epsilonCoordinate</code> apart, and <code>rotZ</code> and possibly <code>rotX</code>, and possibly
402 * <code>rotY</code>are less than <code>epsilonDirection</code> apart, otherwise <code>false</code>
403 * @throws NullPointerException when <code>other</code> is <code>null</code>
404 * @throws ArithmeticException when <code>epsilonCoordinate</code> or <code>epsilonDirection</code> is <code>NaN</code>
405 * @throws IllegalArgumentException <code>epsilonCoordinate</code> or <code>epsilonDirection</code> is <code>negative</code>
406 */
407 public boolean epsilonEquals(final DirectedPoint3d other, final double epsilonCoordinate, final double epsilonDirection)
408 throws NullPointerException, IllegalArgumentException
409 {
410 Throw.whenNull(other, "other");
411 Throw.when(epsilonCoordinate < 0 || epsilonDirection < 0, IllegalArgumentException.class,
412 "epsilonCoordinate and epsilonDirection may not be negative");
413 Throw.whenNaN(epsilonCoordinate, "epsilonCoordinate");
414 Throw.whenNaN(epsilonDirection, "epsilonDirection");
415 if (Math.abs(this.x - other.x) > epsilonCoordinate)
416 {
417 return false;
418 }
419 if (Math.abs(this.y - other.y) > epsilonCoordinate)
420 {
421 return false;
422 }
423 if (Math.abs(this.z - other.z) > epsilonCoordinate)
424 {
425 return false;
426 }
427 if (Math.abs(AngleUtil.normalizeAroundZero(this.dirZ - other.dirZ)) > epsilonDirection)
428 {
429 return false;
430 }
431 if (Math.abs(AngleUtil.normalizeAroundZero(this.dirY - other.dirY)) > epsilonDirection)
432 {
433 return false;
434 }
435 return true;
436 }
437
438 @Override
439 public int hashCode()
440 {
441 final int prime = 31;
442 int result = super.hashCode();
443 result = prime * result + Objects.hash(this.dirZ, this.dirY);
444 return result;
445 }
446
447 @Override
448 @SuppressWarnings("checkstyle:needbraces")
449 public boolean equals(final Object obj)
450 {
451 if (this == obj)
452 return true;
453 if (!super.equals(obj))
454 return false;
455 if (getClass() != obj.getClass())
456 return false;
457 DirectedPoint3d other = (DirectedPoint3d) obj;
458 return Double.doubleToLongBits(this.dirZ) == Double.doubleToLongBits(other.dirZ)
459 && Double.doubleToLongBits(this.dirY) == Double.doubleToLongBits(other.dirY);
460 }
461
462 }