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