View Javadoc
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 &lt;code&gt;fraction&lt;/code&gt; 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 -&pi; and &pi;.
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 -&pi; and &pi;.
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 }