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