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   
7   import org.djutils.base.AngleUtil;
8   import org.djutils.draw.DrawRuntimeException;
9   import org.djutils.draw.Oriented3d;
10  import org.djutils.exceptions.Throw;
11  
12  /**
13   * A OrientedPoint3d is an immutable point with an x, y, and z coordinate, stored with double precision plus a 3d orientation.
14   * The orientation is specified by the rotations around the x, y, and z-axis. A number of constructors and methods are provided
15   * for cases where only the rotation around the z-axis is of importance. Orientation in 3D is stored as three double values
16   * dirX,dirY,dirZ. This class does <b>not</b> prescribe a particular order in which these rotations are to be applied. (Applying
17   * rotations is <b>not</b> commutative, so this <i>is</i> important.)
18   * <p>
19   * Copyright (c) 2020-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
20   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
21   * </p>
22   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
23   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
24   */
25  public class OrientedPoint3d extends DirectedPoint3d implements Oriented3d<OrientedPoint3d>
26  {
27      /** */
28      private static final long serialVersionUID = 20200828L;
29  
30      /** The direction as rotation around the x-axis. */
31      @SuppressWarnings("checkstyle:visibilitymodifier")
32      public final double dirX;
33  
34      /**
35       * Create a new OrientedPoint3d with x, y, and z coordinates and direction 0,0,0.
36       * @param x double; the x coordinate
37       * @param y double; the y coordinate
38       * @param z double; the z coordinate
39       * @throws IllegalArgumentException when x, y, or z is NaN
40       */
41      public OrientedPoint3d(final double x, final double y, final double z) throws IllegalArgumentException
42      {
43          this(x, y, z, 0.0, 0.0, 0.0);
44      }
45  
46      /**
47       * Create a new OrientedPoint3d with x, y, and z coordinates and orientation dirX,dirY,dirZ.
48       * @param x double; the x coordinate
49       * @param y double; the y coordinate
50       * @param z double; the z coordinate
51       * @param dirX double; the direction as rotation around the x-axis with the point as the center
52       * @param dirY double; the direction as rotation around the y-axis with the point as the center
53       * @param dirZ double; the direction as rotation around the z-axis with the point as the center
54       * @throws IllegalArgumentException when x, y, z, dirX, dirY, or dirZ is NaN
55       */
56      public OrientedPoint3d(final double x, final double y, final double z, final double dirX, final double dirY,
57              final double dirZ) throws IllegalArgumentException
58      {
59          super(x, y, z, dirY, dirZ);
60          Throw.when(Double.isNaN(dirX), IllegalArgumentException.class, "dirX must be a numbers (not NaN)");
61          this.dirX = dirX;
62      }
63  
64      /**
65       * Create a new OrientedPoint3d with x, y, and z coordinates and direction 0,0,0.
66       * @param xyz double[]; the x, y and z coordinates
67       * @throws NullPointerException when xyx is null
68       * @throws IllegalArgumentException when the length of the xyx array is not 3, or contains a NaN value
69       */
70      public OrientedPoint3d(final double[] xyz) throws NullPointerException, IllegalArgumentException
71      {
72          super(xyz, 0, 0);
73          this.dirX = 0.0;
74      }
75  
76      /**
77       * Create a new OrientedPoint3d with x, y, and z coordinates and orientation dirX,dirY,dirZ.
78       * @param xyz double[]; the x, y and z coordinates
79       * @param dirX double; the direction as rotation around the x-axis with the point as the center
80       * @param dirY double; the direction as rotation around the y-axis with the point as the center
81       * @param dirZ double; the direction as rotation around the z-axis with the point as the center
82       * @throws NullPointerException when xyx is null
83       * @throws IllegalArgumentException when the length of the xyz array is not 3, or contains a NaN value, or dirX, dirY, or
84       *             dirZ is NaN
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.when(Double.isNaN(dirX), IllegalArgumentException.class, "dirX must be a number (not NaN)");
91          this.dirX = dirX;
92      }
93  
94      /**
95       * Create a new OrientedPoint3d from another point and specified orientation dirX,dirY,dirZ.
96       * @param point Point3d; the point from which this OrientedPoint3d will be instantiated
97       * @param dirX double; the direction as rotation around the x-axis with the point as the center
98       * @param dirY double; the direction as rotation around the y-axis with the point as the center
99       * @param dirZ double; the direction as rotation around the z-axis with the point as the center
100      * @throws IllegalArgumentException when dirX, dirY, or dirZ is NaN
101      */
102     public OrientedPoint3d(final Point3d point, final double dirX, final double dirY, final double dirZ)
103             throws IllegalArgumentException
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 double[]; the array to check
111      * @return double; the first element of the argument
112      * @throws NullPointerException when <code>orientation</code> is null
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             throws NullPointerException, IllegalArgumentException
117     {
118         Throw.when(orientation.length != 3, IllegalArgumentException.class, "length of orientation array must be 3");
119         return orientation[0];
120     }
121 
122     /**
123      * Create a new OrientedPoint3d with x, y, and z coordinates and orientation specified using a double array of three
124      * elements (containing dirX,dirY,dirZ in that order).
125      * @param x double; the x coordinate
126      * @param y double; the y coordinate
127      * @param z double; the z coordinate
128      * @param orientation double[]; the three orientation values as rotations around the x,y,z-axes in a double array containing
129      *            dirX,dirY,dirZ in that order
130      * @throws NullPointerException when <code>rotation</code> is null
131      * @throws IllegalArgumentException when the length of the <code>direction</code> array is not 3
132      */
133     public OrientedPoint3d(final double x, final double y, final double z, final double[] orientation)
134             throws NullPointerException, IllegalArgumentException
135     {
136         this(x, y, z, checkOrientationVector(orientation), orientation[1], orientation[2]);
137     }
138 
139     /**
140      * Create a new OrientedPoint3d with x, y, and z coordinates packed in a double array and orientation specified using a
141      * double array of three elements (containing dirX,dirY,dirZ in that order).
142      * @param xyz double[]; the x, y and z coordinates in that order
143      * @param orientation double[]; the three orientation values as rotations around the x,y,z-axes in a double array containing
144      *            dirX,dirY,dirZ in that order
145      * @throws NullPointerException when xyx or direction is null
146      * @throws IllegalArgumentException when the length of the xyx array or the length of the orientation array is not 3
147      */
148     public OrientedPoint3d(final double[] xyz, final double[] orientation) throws NullPointerException, IllegalArgumentException
149     {
150         this(xyz, checkOrientationVector(orientation), orientation[1], orientation[2]);
151     }
152 
153     @Override
154     public OrientedPoint3d translate(final double dx, final double dy) throws IllegalArgumentException
155     {
156         Throw.when(Double.isNaN(dx) || Double.isNaN(dy), IllegalArgumentException.class, "translation may not contain NaN");
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) throws IllegalArgumentException
162     {
163         Throw.when(Double.isNaN(dx) || Double.isNaN(dy) || Double.isNaN(dz), IllegalArgumentException.class,
164                 "Translation may not contain NaN");
165         return new OrientedPoint3d(this.x + dx, this.y + dy, this.z + dz, this.dirX, this.dirY, this.dirZ);
166     }
167 
168     @Override
169     public OrientedPoint3d scale(final double factor) throws IllegalArgumentException
170     {
171         return new OrientedPoint3d(this.x * factor, this.y * factor, this.z * factor, this.dirX, this.dirY, this.dirZ);
172     }
173 
174     @Override
175     public OrientedPoint3d neg()
176     {
177         return new OrientedPoint3d(-this.x, -this.y, -this.z, AngleUtil.normalizeAroundZero(this.dirX + Math.PI),
178                 AngleUtil.normalizeAroundZero(this.dirY + Math.PI), AngleUtil.normalizeAroundZero(this.dirZ + Math.PI));
179     }
180 
181     @Override
182     public OrientedPoint3d abs()
183     {
184         return new OrientedPoint3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z), this.dirX, this.dirY, this.dirZ);
185     }
186 
187     @Override
188     public OrientedPoint3d normalize() throws DrawRuntimeException
189     {
190         double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
191         Throw.when(length == 0.0, DrawRuntimeException.class, "cannot normalize (0.0, 0.0, 0.0)");
192         return new OrientedPoint3d(this.x / length, this.y / length, this.z / length, this.dirX, this.dirY, this.dirZ);
193     }
194 
195     /**
196      * Interpolate towards another OrientedPoint3d with a fraction. It is allowed for fraction to be less than zero or larger
197      * than 1. In that case the interpolation turns into an extrapolation. DirX, dirY and dirZ are interpolated/extrapolated
198      * using the interpolateShortest method.
199      * @param otherPoint OrientedPoint3d; the other point
200      * @param fraction double; the factor for interpolation towards the other point. When &lt;code&gt;fraction&lt;/code&gt; is
201      *            between 0 and 1, it is an interpolation, otherwise an extrapolation. If <code>fraction</code> is 0;
202      *            <code>this</code> Point is returned; if <code>fraction</code> is 1, the <code>otherPoint</code> is returned
203      * @return OrientedPoint3d; a new OrientedPoint3d at the requested fraction
204      * @throws NullPointerException when otherPoint is null
205      * @throws IllegalArgumentException when fraction is NaN
206      */
207     public OrientedPoint3d interpolate(final OrientedPoint3d otherPoint, final double fraction)
208             throws NullPointerException, IllegalArgumentException
209     {
210         Throw.whenNull(otherPoint, "otherPoint");
211         Throw.when(Double.isNaN(fraction), IllegalArgumentException.class, "fraction must be a number (not NaN)");
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 -&pi; and &pi;.
230      * @param rotateZ double; the rotation around the z-axis
231      * @return OrientedPoint3d; a new point with the same coordinates, dirX and dirY and modified dirZ
232      * @throws IllegalArgumentException when rotateZ is NaN
233      */
234     @Override
235     public OrientedPoint3d rotate(final double rotateZ) throws IllegalArgumentException
236     {
237         Throw.when(Double.isNaN(rotateZ), IllegalArgumentException.class, "rotateZ must be a number (not NaN)");
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 -&pi; and &pi;.
245      * @param rotateX double; the rotation around the x-axis
246      * @param rotateY double; the rotation around the y-axis
247      * @param rotateZ double; the rotation around the z-axis
248      * @return OrientedPoint3d; a new point with the same coordinates and applied rotations
249      * @throws IllegalArgumentException when any of the rotations is NaN
250      */
251     public OrientedPoint3d rotate(final double rotateX, final double rotateY, final double rotateZ)
252             throws IllegalArgumentException
253     {
254         Throw.when(Double.isNaN(rotateX) || Double.isNaN(rotateY) || Double.isNaN(rotateZ), IllegalArgumentException.class,
255                 "rotateX, rotateY and rotateZ must be a numbers (not NaN)");
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<OrientedPoint3d> getPoints()
280     {
281         return Arrays.stream(new OrientedPoint3d[] {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     @Override
299     public boolean epsilonEquals(final OrientedPoint3d other, final double epsilonCoordinate, final double epsilonRotation)
300             throws NullPointerException, IllegalArgumentException
301     {
302         Throw.whenNull(other, "other");
303         if (Math.abs(this.x - other.x) > epsilonCoordinate)
304         {
305             return false;
306         }
307         if (Math.abs(this.y - other.y) > epsilonCoordinate)
308         {
309             return false;
310         }
311         if (Math.abs(this.z - other.z) > epsilonCoordinate)
312         {
313             return false;
314         }
315         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirX - other.dirX)) > epsilonRotation)
316         {
317             return false;
318         }
319         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirY - other.dirY)) > epsilonRotation)
320         {
321             return false;
322         }
323         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirZ - other.dirZ)) > epsilonRotation)
324         {
325             return false;
326         }
327         return true;
328     }
329 
330     @Override
331     public int hashCode()
332     {
333         final int prime = 31;
334         int result = super.hashCode();
335         long temp;
336         temp = Double.doubleToLongBits(this.dirX);
337         result = prime * result + (int) (temp ^ (temp >>> 32));
338         temp = Double.doubleToLongBits(this.dirY);
339         result = prime * result + (int) (temp ^ (temp >>> 32));
340         temp = Double.doubleToLongBits(this.dirZ);
341         result = prime * result + (int) (temp ^ (temp >>> 32));
342         return result;
343     }
344 
345     @Override
346     @SuppressWarnings("checkstyle:needbraces")
347     public boolean equals(final Object obj)
348     {
349         if (this == obj)
350             return true;
351         if (!super.equals(obj))
352             return false;
353         OrientedPoint3d other = (OrientedPoint3d) obj;
354         if (Double.doubleToLongBits(this.dirX) != Double.doubleToLongBits(other.dirX))
355             return false;
356         if (Double.doubleToLongBits(this.dirY) != Double.doubleToLongBits(other.dirY))
357             return false;
358         if (Double.doubleToLongBits(this.dirZ) != Double.doubleToLongBits(other.dirZ))
359             return false;
360         return true;
361     }
362 
363 }