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.draw.DrawRuntimeException;
8   import org.djutils.draw.Oriented3d;
9   import org.djutils.exceptions.Throw;
10  import org.djutils.math.AngleUtil;
11  
12  /**
13   * A OrientedPoint3d is a point with an x, y, and z coordinate, plus a 3d orientation. The orientation is specified by the
14   * rotations around the x, y, and z-axis. A number of constructors and methods are provided for cases where only the rotation
15   * around the z-axis is of importance. Orientation in 3D is stored as three double values dirX,dirY,dirZ. This class does
16   * <b>not</b> prescribe a particular order in which these rotations are to be applied. (Applying rotations is <b>not</b>
17   * commutative, so this <i>is</i> important.)
18   * <p>
19   * Copyright (c) 2020-2025 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://github.com/peter-knoppers">Peter Knoppers</a>
24   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
25   */
26  public class OrientedPoint3d extends DirectedPoint3d implements Oriented3d<OrientedPoint3d>
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 dirX;
34  
35      /**
36       * Create a new OrientedPoint3d with x, y, and z coordinates and direction 0,0,0.
37       * @param x the x coordinate
38       * @param y the y coordinate
39       * @param z the z coordinate
40       * @throws IllegalArgumentException when <code>x</code>, <code>y</code>, or <code>z</code> is <code>NaN</code>
41       */
42      public OrientedPoint3d(final double x, final double y, final double z)
43      {
44          this(x, y, z, 0.0, 0.0, 0.0);
45      }
46  
47      /**
48       * Create a new OrientedPoint3d with x, y, and z coordinates and orientation dirX,dirY,dirZ.
49       * @param x the x coordinate
50       * @param y the y coordinate
51       * @param z the z coordinate
52       * @param dirX the direction as rotation around the x-axis with the point as the center
53       * @param dirY the direction as rotation around the y-axis with the point as the center
54       * @param dirZ the direction as rotation around the z-axis with the point as the center
55       * @throws ArithmeticException when <code>x</code>, <code>y</code>, <code>z</code>, <code>dirX</code>, <code>dirY</code>, or
56       *             <code>dirZ</code> is <code>NaN</code>
57       */
58      public OrientedPoint3d(final double x, final double y, final double z, final double dirX, final double dirY,
59              final double dirZ)
60      {
61          super(x, y, z, dirY, dirZ);
62          Throw.whenNaN(dirX, "dirX");
63          this.dirX = dirX;
64      }
65  
66      /**
67       * Create a new OrientedPoint3d with x, y, and z coordinates and direction 0,0,0.
68       * @param xyz the x, y and z coordinates
69       * @throws NullPointerException when <code>xyz</code> is <code>null</code>
70       * @throws IllegalArgumentException when the length of the <code>xyx</code> array is not 3
71       * @throws IllegalArgumentException when the <code>xyx</code> array contains a <code>NaN</code> value
72       */
73      public OrientedPoint3d(final double[] xyz)
74      {
75          super(xyz, 0, 0);
76          this.dirX = 0.0;
77      }
78  
79      /**
80       * Create a new OrientedPoint3d with x, y, and z coordinates and orientation dirX,dirY,dirZ.
81       * @param xyz the x, y and z coordinates
82       * @param dirX the direction as rotation around the x-axis with the point as the center
83       * @param dirY the direction as rotation around the y-axis with the point as the center
84       * @param dirZ the direction as rotation around the z-axis with the point as the center
85       * @throws NullPointerException when <code>xyx</code> is <code>null</code>
86       * @throws IllegalArgumentException when the length of the xyz array is not 3
87       * @throws ArithmeticException when <code>xyz</code> contains a <code>NaN</code> value, or <code>dirX</code>,
88       *             <code>dirY</code>, or <code>dirZ</code> is <code>NaN</code>
89       */
90      public OrientedPoint3d(final double[] xyz, final double dirX, final double dirY, final double dirZ)
91              throws NullPointerException, IllegalArgumentException
92      {
93          super(xyz, dirY, dirZ);
94          Throw.whenNaN(dirX, "dirX");
95          this.dirX = dirX;
96      }
97  
98      /**
99       * Create a new OrientedPoint3d from another point and specified orientation dirX,dirY,dirZ.
100      * @param point the point from which this OrientedPoint3d will be instantiated
101      * @param dirX the direction as rotation around the x-axis with the point as the center
102      * @param dirY the direction as rotation around the y-axis with the point as the center
103      * @param dirZ the direction as rotation around the z-axis with the point as the center
104      * @throws NullPointerException when <code>point</code> is <code>null</code>
105      * @throws ArithmeticException when <code>dirX</code>, <code>dirY</code>, or <code>dirZ</code> is <code>NaN</code>
106      */
107     public OrientedPoint3d(final Point3d point, final double dirX, final double dirY, final double dirZ)
108     {
109         this(point.x, point.y, point.z, dirX, dirY, dirZ);
110     }
111 
112     /**
113      * Verify that a double array is not null, has three elements.
114      * @param orientation the array to check
115      * @return the first element of the argument
116      * @throws NullPointerException when <code>orientation</code> is <code>null</code>
117      * @throws IllegalArgumentException when the length of the <code>orientation</code> array is not 3
118      */
119     private static double checkOrientationVector(final double[] orientation)
120     {
121         Throw.when(orientation.length != 3, IllegalArgumentException.class, "length of orientation array must be 3");
122         return orientation[0];
123     }
124 
125     /**
126      * Create a new OrientedPoint3d with x, y, and z coordinates and orientation specified using a double array of three
127      * elements (containing dirX,dirY,dirZ in that order).
128      * @param x the x coordinate
129      * @param y the y coordinate
130      * @param z the z coordinate
131      * @param orientation the three orientation values as rotations around the x,y,z-axes in a double array containing
132      *            dirX,dirY,dirZ in that order
133      * @throws NullPointerException when <code>rotation</code> is <code>null</code>
134      * @throws IllegalArgumentException when the length of the <code>direction</code> array is not 3
135      */
136     public OrientedPoint3d(final double x, final double y, final double z, final double[] orientation)
137     {
138         this(x, y, z, checkOrientationVector(orientation), orientation[1], orientation[2]);
139     }
140 
141     /**
142      * Create a new OrientedPoint3d with x, y, and z coordinates packed in a double array and orientation specified using a
143      * double array of three elements (containing dirX,dirY,dirZ in that order).
144      * @param xyz the x, y and z coordinates in that order
145      * @param orientation the three orientation values as rotations around the x,y,z-axes in a double array containing
146      *            dirX,dirY,dirZ in that order
147      * @throws NullPointerException when <code>xyx</code> or <code>direction</code> is <code>null</code>
148      * @throws IllegalArgumentException when the length of the <code>xyx</code> array or the length of the
149      *             <code>orientation</code> array is not 3
150      */
151     public OrientedPoint3d(final double[] xyz, final double[] orientation)
152     {
153         this(xyz, checkOrientationVector(orientation), orientation[1], orientation[2]);
154     }
155 
156     @Override
157     public OrientedPoint3d translate(final double dX, final double dY)
158     {
159         Throw.whenNaN(dX, "dX");
160         Throw.whenNaN(dY, "dY");
161         return new OrientedPoint3d(this.x + dX, this.y + dY, this.z, this.dirX, this.dirY, this.dirZ);
162     }
163 
164     @Override
165     public OrientedPoint3d translate(final double dX, final double dY, final double dZ)
166     {
167         Throw.whenNaN(dX, "dX");
168         Throw.whenNaN(dY, "dY");
169         Throw.whenNaN(dZ, "dZ");
170         return new OrientedPoint3d(this.x + dX, this.y + dY, this.z + dZ, this.dirX, this.dirY, this.dirZ);
171     }
172 
173     @Override
174     public OrientedPoint3d scale(final double factor)
175     {
176         return new OrientedPoint3d(this.x * factor, this.y * factor, this.z * factor, this.dirX, this.dirY, this.dirZ);
177     }
178 
179     @Override
180     public OrientedPoint3d neg()
181     {
182         return new OrientedPoint3d(-this.x, -this.y, -this.z, AngleUtil.normalizeAroundZero(this.dirX + Math.PI),
183                 AngleUtil.normalizeAroundZero(this.dirY + Math.PI), AngleUtil.normalizeAroundZero(this.dirZ + Math.PI));
184     }
185 
186     @Override
187     public OrientedPoint3d abs()
188     {
189         return new OrientedPoint3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z), this.dirX, this.dirY, this.dirZ);
190     }
191 
192     @Override
193     public OrientedPoint3d normalize() throws DrawRuntimeException
194     {
195         double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
196         Throw.when(length == 0.0, DrawRuntimeException.class, "cannot normalize (0.0, 0.0, 0.0)");
197         return new OrientedPoint3d(this.x / length, this.y / length, this.z / length, this.dirX, this.dirY, this.dirZ);
198     }
199 
200     /**
201      * Interpolate towards another OrientedPoint3d with a fraction. It is allowed for fraction to be less than zero or larger
202      * than 1. In that case the interpolation turns into an extrapolation. DirX, dirY and dirZ are interpolated/extrapolated
203      * using the interpolateShortest method.
204      * @param otherPoint the other point
205      * @param fraction the factor for interpolation towards the other point. When &lt;code&gt;fraction&lt;/code&gt; is
206      *            between 0 and 1, it is an interpolation, otherwise an extrapolation. If <code>fraction</code> is 0;
207      *            <code>this</code> Point is returned; if <code>fraction</code> is 1, the <code>otherPoint</code> is returned
208      * @return a new <code>OrientedPoint3d</code> at the requested <code>fraction</code>
209      * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
210      * @throws ArithmeticException when <code>fraction</code> is <code>NaN</code>
211      */
212     public OrientedPoint3d interpolate(final OrientedPoint3d otherPoint, final double fraction)
213     {
214         Throw.whenNull(otherPoint, "otherPoint");
215         Throw.whenNaN(fraction, "fraction");
216         if (0.0 == fraction)
217         {
218             return this;
219         }
220         if (1.0 == fraction)
221         {
222             return otherPoint;
223         }
224         return new OrientedPoint3d((1.0 - fraction) * this.x + fraction * otherPoint.x,
225                 (1.0 - fraction) * this.y + fraction * otherPoint.y, (1.0 - fraction) * this.z + fraction * otherPoint.z,
226                 AngleUtil.interpolateShortest(this.dirX, otherPoint.dirX, fraction),
227                 AngleUtil.interpolateShortest(this.dirY, otherPoint.dirY, fraction),
228                 AngleUtil.interpolateShortest(this.dirZ, otherPoint.dirZ, fraction));
229     }
230 
231     /**
232      * Return a new OrientedPoint3d with an in-place rotation around the z-axis by the provided rotateZ. The resulting rotation
233      * will be normalized between -&pi; and &pi;.
234      * @param rotateZ the rotation around the z-axis
235      * @return a new point with the same coordinates, <code>dirX</code> and <code>dirY</code> and modified
236      *         <code>dirZ</code>
237      * @throws ArithmeticException when <code>rotateZ</code> is <code>NaN</code>
238      */
239     @Override
240     public OrientedPoint3d rotate(final double rotateZ)
241     {
242         Throw.whenNaN(rotateZ, "rotZ");
243         return new OrientedPoint3d(this.x, this.y, this.z, this.dirX, this.dirY,
244                 AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
245     }
246 
247     /**
248      * Return a new OrientedPoint3d point with an in-place rotation by the provided rotateX, rotateY, and rotateZ. The resulting
249      * rotations will be normalized between -&pi; and &pi;.
250      * @param rotateX the rotation around the x-axis
251      * @param rotateY the rotation around the y-axis
252      * @param rotateZ the rotation around the z-axis
253      * @return a new point with the same coordinates and applied rotations
254      * @throws ArithmeticException when any of the rotations is <code>NaN</code>
255      */
256     public OrientedPoint3d rotate(final double rotateX, final double rotateY, final double rotateZ)
257     {
258         Throw.whenNaN(rotateX, "rotateX");
259         Throw.whenNaN(rotateY, "rotateY");
260         Throw.whenNaN(rotateZ, "rotateZ");
261         return new OrientedPoint3d(this.x, this.y, this.z, AngleUtil.normalizeAroundZero(this.dirX + rotateX),
262                 AngleUtil.normalizeAroundZero(this.dirY + rotateY), AngleUtil.normalizeAroundZero(this.dirZ + rotateZ));
263     }
264 
265     @Override
266     public double getDirX()
267     {
268         return this.dirX;
269     }
270 
271     @Override
272     public double getDirY()
273     {
274         return this.dirY;
275     }
276 
277     @Override
278     public double getDirZ()
279     {
280         return this.dirZ;
281     }
282 
283     @Override
284     public Iterator<Point3d> iterator()
285     {
286         return Arrays.stream(new Point3d[] {this}).iterator();
287     }
288 
289     @Override
290     public String toString()
291     {
292         return toString("%f", false);
293     }
294 
295     @Override
296     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
297     {
298         String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2$s, rotX=%2$s, rotY=%2$s, rotZ=%2$s]",
299                 doNotIncludeClassName ? "" : "OrientedPoint3d ", doubleFormat);
300         return String.format(Locale.US, format, this.x, this.y, this.z, this.dirX, this.dirY, this.dirZ);
301     }
302 
303     @Override
304     public boolean epsilonEquals(final OrientedPoint3d other, final double epsilonCoordinate, final double epsilonRotation)
305             throws NullPointerException, IllegalArgumentException
306     {
307         Throw.whenNull(other, "other");
308         if (Math.abs(this.x - other.x) > epsilonCoordinate)
309         {
310             return false;
311         }
312         if (Math.abs(this.y - other.y) > epsilonCoordinate)
313         {
314             return false;
315         }
316         if (Math.abs(this.z - other.z) > epsilonCoordinate)
317         {
318             return false;
319         }
320         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirX - other.dirX)) > epsilonRotation)
321         {
322             return false;
323         }
324         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirY - other.dirY)) > epsilonRotation)
325         {
326             return false;
327         }
328         if (Math.abs(AngleUtil.normalizeAroundZero(this.dirZ - other.dirZ)) > epsilonRotation)
329         {
330             return false;
331         }
332         return true;
333     }
334 
335     @Override
336     public int hashCode()
337     {
338         final int prime = 31;
339         int result = super.hashCode();
340         long temp;
341         temp = Double.doubleToLongBits(this.dirX);
342         result = prime * result + (int) (temp ^ (temp >>> 32));
343         temp = Double.doubleToLongBits(this.dirY);
344         result = prime * result + (int) (temp ^ (temp >>> 32));
345         temp = Double.doubleToLongBits(this.dirZ);
346         result = prime * result + (int) (temp ^ (temp >>> 32));
347         return result;
348     }
349 
350     @Override
351     @SuppressWarnings("checkstyle:needbraces")
352     public boolean equals(final Object obj)
353     {
354         if (this == obj)
355             return true;
356         if (!super.equals(obj))
357             return false;
358         OrientedPoint3d other = (OrientedPoint3d) obj;
359         if (Double.doubleToLongBits(this.dirX) != Double.doubleToLongBits(other.dirX))
360             return false;
361         if (Double.doubleToLongBits(this.dirY) != Double.doubleToLongBits(other.dirY))
362             return false;
363         if (Double.doubleToLongBits(this.dirZ) != Double.doubleToLongBits(other.dirZ))
364             return false;
365         return true;
366     }
367 
368 }