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.Direction3d;
8 import org.djutils.draw.Drawable3d;
9 import org.djutils.draw.InvalidProjectionException;
10 import org.djutils.draw.bounds.Bounds3d;
11 import org.djutils.exceptions.Throw;
12
13 /**
14 * A Point3d is an immutable point with an x, y, and z coordinate, stored with double precision. It differs from many Point
15 * implementations by being immutable.
16 * <p>
17 * Copyright (c) 2020-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
18 * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
19 * </p>
20 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
21 * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
22 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
23 */
24 public class Point3d implements Drawable3d, Point<Point3d>
25 {
26 /** The x-coordinate. */
27 @SuppressWarnings("checkstyle:visibilitymodifier")
28 public final double x;
29
30 /** The y-coordinate. */
31 @SuppressWarnings("checkstyle:visibilitymodifier")
32 public final double y;
33
34 /** The z-coordinate. */
35 @SuppressWarnings("checkstyle:visibilitymodifier")
36 public final double z;
37
38 /**
39 * Create a new Point from x, y and z coordinates provided as double arguments.
40 * @param x the x coordinate
41 * @param y the y coordinate
42 * @param z the z coordinate
43 * @throws ArithmeticException when <code>x</code> or <code>y</code> or <code>z</code> is <code>NaN</code>
44 */
45 public Point3d(final double x, final double y, final double z)
46 {
47 Throw.whenNaN(x, "x");
48 Throw.whenNaN(y, "y");
49 Throw.whenNaN(z, "z");
50 this.x = x;
51 this.y = y;
52 this.z = z;
53 }
54
55 /**
56 * Create a new Point3d from x, y and z coordinates provided as values in a double array.
57 * @param xyz the x, y and z coordinates
58 * @throws NullPointerException when <code>xyz</code> is <code>null</code>
59 * @throws IllegalArgumentException when the length of <code>xyz</code> is not 3
60 * @throws ArithmeticException when <code>xyz</code> contains a <code>NaN</code> value
61 */
62 public Point3d(final double[] xyz) throws NullPointerException, IllegalArgumentException
63 {
64 this(checkLengthIsThree(Throw.whenNull(xyz, "xyz"))[0], xyz[1], xyz[2]);
65 }
66
67 /**
68 * Create a new Point3d from x, y stored in a java.awt.geom.Point2D and double z.
69 * @param point a java.awt.geom.Point2D
70 * @param z the z coordinate
71 * @throws NullPointerException when <code>point</code> is <code>null</code>
72 * @throws ArithmeticException when <code>z</code> is <code>NaN</code>
73 */
74 public Point3d(final Point2d point, final double z)
75 {
76 Throw.whenNull(point, "point");
77 Throw.whenNaN(z, "z");
78 this.x = point.x;
79 this.y = point.y;
80 this.z = z;
81 }
82
83 /**
84 * Create an immutable point from x, y obtained from a AWT Point2D and double z.
85 * @param point a java.awt.geom.Point2D
86 * @param z the z coordinate
87 * @throws NullPointerException when <code>point</code> is <code>null</code>
88 * @throws ArithmeticException when <code>point</code> has a <code>NaN</code> coordinate, or <code>z</code> is
89 * <code>NaN</code>
90 */
91 public Point3d(final java.awt.geom.Point2D point, final double z)
92 {
93 Throw.whenNull(point, "point");
94 Throw.whenNaN(point.getX(), "point.getX()");
95 Throw.whenNaN(point.getY(), "point.getY()");
96 Throw.whenNaN(z, "z");
97 this.x = point.getX();
98 this.y = point.getY();
99 this.z = z;
100 }
101
102 /**
103 * Throw an IllegalArgumentException if the length of the provided array is not three.
104 * @param xyz the provided array
105 * @return the provided array
106 * @throws NullPointerException when <code>xyz</code> is <code>null</code>
107 * @throws IllegalArgumentException when length of <code>xyz</code> is not 3
108 */
109 private static double[] checkLengthIsThree(final double[] xyz)
110 {
111 Throw.when(xyz.length != 3, IllegalArgumentException.class, "Length of xyz-array must be 3");
112 return xyz;
113 }
114
115 @Override
116 public final double getX()
117 {
118 return this.x;
119 }
120
121 @Override
122 public final double getY()
123 {
124 return this.y;
125 }
126
127 /**
128 * Return the z-coordinate.
129 * @return the z-coordinate
130 */
131 public final double getZ()
132 {
133 return this.z;
134 }
135
136 @Override
137 public double distanceSquared(final Point3d otherPoint) throws NullPointerException
138 {
139 Throw.whenNull(otherPoint, "otherPoint");
140 double dx = this.x - otherPoint.x;
141 double dy = this.y - otherPoint.y;
142 double dz = this.z - otherPoint.z;
143 return dx * dx + dy * dy + dz * dz;
144 }
145
146 @Override
147 public double distance(final Point3d otherPoint) throws NullPointerException
148 {
149 Throw.whenNull(otherPoint, "otherPoint");
150 return Math.sqrt(distanceSquared(otherPoint));
151 }
152
153 @Override
154 public int size()
155 {
156 return 1;
157 }
158
159 @Override
160 public Iterator<Point3d> iterator()
161 {
162 return Arrays.stream(new Point3d[] {this}).iterator();
163 }
164
165 @Override
166 public Point2d project() throws InvalidProjectionException
167 {
168 return new Point2d(this.x, this.y);
169 }
170
171 /**
172 * Return a new Point3d with a translation by the provided dX and dY and preserved z value.
173 * @param dX the x translation
174 * @param dY the y translation
175 * @return a new point with the translated coordinates and the same <code>z</code> value
176 * @throws ArithmeticException when <code>dX</code>, or <code>dY</code> is <code>NaN</code>
177 */
178 public Point3d translate(final double dX, final double dY) throws ArithmeticException
179 {
180 Throw.whenNaN(dX, "dX");
181 Throw.whenNaN(dY, "dY");
182 return new Point3d(this.x + dX, this.y + dY, this.z);
183 }
184
185 /**
186 * Return a new Point3d with a translation by the provided dx, dy and dz.
187 * @param dX the x translation
188 * @param dY the y translation
189 * @param dZ the z translation
190 * @return a new point with the translated coordinates
191 * @throws ArithmeticException when <code>dX</code>, <code>dY</code>, or <code>dZ</code> is <code>NaN</code>
192 */
193 public Point3d translate(final double dX, final double dY, final double dZ)
194 {
195 Throw.whenNaN(dX, "dX");
196 Throw.whenNaN(dY, "dY");
197 Throw.whenNaN(dZ, "dZ");
198 return new Point3d(this.x + dX, this.y + dY, this.z + dZ);
199 }
200
201 @Override
202 public Point3d scale(final double factor)
203 {
204 Throw.whenNaN(factor, "factor");
205 return new Point3d(this.x * factor, this.y * factor, this.z * factor);
206 }
207
208 @Override
209 public Point3d neg()
210 {
211 return scale(-1.0);
212 }
213
214 @Override
215 public Point3d abs()
216 {
217 return new Point3d(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z));
218 }
219
220 @Override
221 public Point3d normalize() throws IllegalArgumentException
222 {
223 double length = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
224 Throw.when(length == 0.0, IllegalArgumentException.class, "cannot normalize (0.0, 0.0, 0.0)");
225 return this.scale(1.0 / length);
226 }
227
228 @Override
229 public Point3d interpolate(final Point3d point, final double fraction)
230 {
231 Throw.whenNull(point, "point");
232 Throw.whenNaN(fraction, "fraction");
233 return new Point3d((1.0 - fraction) * this.x + fraction * point.x, (1.0 - fraction) * this.y + fraction * point.y,
234 (1.0 - fraction) * this.z + fraction * point.z);
235
236 }
237
238 @Override
239 public boolean epsilonEquals(final Point3d otherPoint, final double epsilon)
240 {
241 Throw.whenNull(otherPoint, "otherPoint");
242 if (Math.abs(this.x - otherPoint.x) > epsilon)
243 {
244 return false;
245 }
246 if (Math.abs(this.y - otherPoint.y) > epsilon)
247 {
248 return false;
249 }
250 if (Math.abs(this.z - otherPoint.z) > epsilon)
251 {
252 return false;
253 }
254 return true;
255 }
256
257 @Override
258 public Bounds3d getAbsoluteBounds()
259 {
260 return new Bounds3d(this);
261 }
262
263 /**
264 * Return the direction to another Point3d.
265 * @param otherPoint the other point
266 * @return the direction to the other point in Radians (towards infinite X is 0; towards infinite Y is π / 2; etc.). If
267 * the points are identical; this method returns <code>NaN</code>.
268 */
269 public Direction3d directionTo(final Point3d otherPoint)
270 {
271 return new Direction3d(Math.atan2(Math.hypot(otherPoint.x - this.x, otherPoint.y - this.y), otherPoint.z - this.z),
272 Math.atan2(otherPoint.y - this.y, otherPoint.x - this.x));
273 }
274
275 @Override
276 public final Point3d closestPointOnSegment(final Point3d segmentPoint1, final Point3d segmentPoint2)
277 {
278 Throw.whenNull(segmentPoint1, "segmentPoint1");
279 Throw.whenNull(segmentPoint2, "segmentPoint2");
280 return closestPointOnSegment(segmentPoint1.x, segmentPoint1.y, segmentPoint1.z, segmentPoint2.x, segmentPoint2.y,
281 segmentPoint2.z);
282 }
283
284 /**
285 * Compute the closest point on a line with optional limiting of the result on either end.
286 * @param p1X the x coordinate of the first point on the line
287 * @param p1Y the y coordinate of the first point on the line
288 * @param p1Z the z coordinate of the first point on the line
289 * @param p2X the x coordinate of the second point on the line
290 * @param p2Y the y coordinate of the second point on the line
291 * @param p2Z the z coordinate of the second point on the line
292 * @param lowLimitHandling controls handling of results that lie before the first point of the line. If <code>null</code>;
293 * this method returns <code>null</code>; else if <code>true</code>; this method returns (p1X,p1Y); else
294 * (lowLimitHandling is <code>false</code>); this method will return the closest point on the line
295 * @param highLimitHandling controls the handling of results that lie beyond the second point of the line. If
296 * <code>null</code>; this method returns <code>null</code>; else if <code>true</code>; this method returns
297 * (p2X,p2Y); else (highLimitHandling is <code>false</code>); this method will return the closest point on the
298 * line
299 * @return the closest point on the line after applying the indicated limit handling; so the result can be <code>null</code>
300 * @throws ArithmeticException when any of the arguments is <code>NaN</code>
301 */
302 @SuppressWarnings("checkstyle:parameternumber")
303 public Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X, final double p2Y,
304 final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling)
305 {
306 double fraction = fractionalPositionOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, lowLimitHandling, highLimitHandling);
307 if (Double.isNaN(fraction))
308 {
309 return null;
310 }
311 if (fraction == 1.0)
312 {
313 return new Point3d(p2X, p2Y, p2Z); // Maximize precision in case fraction == 1.0
314 }
315 return new Point3d(p1X + fraction * (p2X - p1X), p1Y + fraction * (p2Y - p1Y), p1Z + fraction * (p2Z - p1Z));
316 }
317
318 /**
319 * Compute the fractional position of the closest point on a line with optional limiting of the result on either end. If the
320 * line has length 0; this method returns 0.0.
321 * @param p1X the x coordinate of the first point on the line
322 * @param p1Y the y coordinate of the first point on the line
323 * @param p1Z the z coordinate of the first point on the line
324 * @param p2X the x coordinate of the second point on the line
325 * @param p2Y the y coordinate of the second point on the line
326 * @param p2Z the z coordinate of the second point on the line
327 * @param lowLimitHandling controls handling of results that lie before the first point of the line. If null; this method
328 * returns <code>NaN</code>; else if <code>true</code>; this method returns 0.0; else (lowLimitHandling is
329 * false); this results < 0.0 are returned
330 * @param highLimitHandling controls the handling of results that lie beyond the second point of the line. If null; this
331 * method returns <code>NaN</code>; else if <code>true</code>; this method returns 1.0; else (highLimitHandling
332 * is <code>false</code>); results > 1.0 are returned
333 * @return the fractional position of the closest point on the line. Results within the range 0.0 .. 1.0 are always returned
334 * as is.. A result < 0.0 is subject to lowLimitHandling. A result > 1.0 is subject to highLimitHandling
335 * @throws IllegalArgumentException when any of the arguments is <code>NaN</code>
336 */
337 @SuppressWarnings("checkstyle:parameternumber")
338 public double fractionalPositionOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
339 final double p2Y, final double p2Z, final Boolean lowLimitHandling, final Boolean highLimitHandling)
340 {
341 double dX = p2X - p1X;
342 double dY = p2Y - p1Y;
343 double dZ = p2Z - p1Z;
344 Throw.whenNaN(dX, "dX");
345 Throw.whenNaN(dY, "dY");
346 Throw.whenNaN(dZ, "dZ");
347 if (0 == dX && 0 == dY && 0 == dZ)
348 {
349 return 0.0;
350 }
351 double fraction = ((this.x - p1X) * dX + (this.y - p1Y) * dY + (this.z - p1Z) * dZ) / (dX * dX + dY * dY + dZ * dZ);
352 if (fraction < 0.0)
353 {
354 if (lowLimitHandling == null)
355 {
356 return Double.NaN;
357 }
358 if (lowLimitHandling)
359 {
360 fraction = 0.0;
361 }
362 }
363 else if (fraction > 1.0)
364 {
365 if (highLimitHandling == null)
366 {
367 return Double.NaN;
368 }
369 if (highLimitHandling)
370 {
371 fraction = 1.0;
372 }
373 }
374 return fraction;
375 }
376
377 /**
378 * Project a point on a line segment. If the the projected points lies outside the line segment, the nearest end point of
379 * the line segment is returned. Otherwise the returned point lies between the end points of the line segment. <br>
380 * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
381 * Bourke</a>.
382 * @param p1X the x coordinate of the start point of the line segment
383 * @param p1Y the y coordinate of the start point of the line segment
384 * @param p1Z the z coordinate of the start point of the line segment
385 * @param p2X the x coordinate of the end point of the line segment
386 * @param p2Y the y coordinate of the end point of the line segment
387 * @param p2Z the y coordinate of the end point of the line segment
388 * @return either <code>segmentPoint1</code>, or <code>segmentPoint2</code> or a new Point2d that lies somewhere in between
389 * those two.
390 * @throws ArithmeticException when any of the parameters is <code>NaN</code>
391 */
392 public final Point3d closestPointOnSegment(final double p1X, final double p1Y, final double p1Z, final double p2X,
393 final double p2Y, final double p2Z)
394 {
395 return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, true, true);
396 }
397
398 @Override
399 public final Point3d closestPointOnLine(final Point3d linePoint1, final Point3d linePoint2)
400 {
401 Throw.whenNull(linePoint1, "linePoint1");
402 Throw.whenNull(linePoint2, "linePoint2");
403 return closestPointOnLine(linePoint1.x, linePoint1.y, linePoint1.z, linePoint2.x, linePoint2.y, linePoint2.z);
404 }
405
406 /**
407 * Project a point on a line. <br>
408 * Adapted from <a href="http://paulbourke.net/geometry/pointlineplane/DistancePoint.java">example code provided by Paul
409 * Bourke</a>.
410 * @param p1X the x coordinate of a point of the line
411 * @param p1Y the y coordinate of a point of the line
412 * @param p1Z the z coordinate of a point on the line
413 * @param p2X the x coordinate of another point on the line
414 * @param p2Y the y coordinate of another point on the line
415 * @param p2Z the z coordinate of another point on the line
416 * @return a point on the line that goes through the points
417 * @throws IllegalArgumentException when the points on the line are identical
418 */
419 public final Point3d closestPointOnLine(final double p1X, final double p1Y, final double p1Z, final double p2X,
420 final double p2Y, final double p2Z)
421 {
422 Throw.when(p1X == p2X && p1Y == p2Y && p1Z == p2Z, IllegalArgumentException.class, "degenerate line not allowed");
423 return closestPointOnLine(p1X, p1Y, p1Z, p2X, p2Y, p2Z, false, false);
424 }
425
426 /**
427 * Return the direction of the point in radians with respect to the origin, ignoring the z-coordinate.
428 * @return the direction of the projection of the point in the x-y plane with respect to the origin, in radians
429 */
430 final double horizontalDirection()
431 {
432 return Math.atan2(this.y, this.x);
433 }
434
435 /**
436 * Return the direction to another point, in radians, ignoring the z-coordinate.
437 * @param otherPoint the other point
438 * @return the direction of the projection of the point in the x-y plane to another point, in radians
439 * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
440 */
441 final double horizontalDirection(final Point3d otherPoint)
442 {
443 Throw.whenNull(otherPoint, "otherPoint");
444 return Math.atan2(otherPoint.y - this.y, otherPoint.x - this.x);
445 }
446
447 /**
448 * Return the direction with respect to the Z axis to another point, in radians.
449 * @param otherPoint the other point
450 * @return the direction with respect to the Z axis to another point, in radians
451 * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
452 */
453 final double verticalDirection(final Point3d otherPoint)
454 {
455 Throw.whenNull(otherPoint, "otherPoint");
456 return Math.atan2(Math.hypot(otherPoint.y - this.y, otherPoint.x - this.x), otherPoint.z - this.z);
457 }
458
459 /**
460 * Return the squared distance between the coordinates of this point and the provided point, ignoring the z-coordinate.
461 * @param otherPoint the other point
462 * @return the squared distance between this point and the other point, ignoring the z-coordinate
463 * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
464 */
465 final double horizontalDistanceSquared(final Point3d otherPoint)
466 {
467 Throw.whenNull(otherPoint, "otherPoint");
468 double dx = this.x - otherPoint.x;
469 double dy = this.y - otherPoint.y;
470 return dx * dx + dy * dy;
471 }
472
473 /**
474 * Return the Euclidean distance between this point and the provided point, ignoring the z-coordinate.
475 * @param otherPoint the other point
476 * @return the Euclidean distance between this point and the other point, ignoring the z-coordinate
477 * @throws NullPointerException when <code>otherPoint</code> is <code>null</code>
478 */
479 final double horizontalDistance(final Point3d otherPoint)
480 {
481 return Math.sqrt(horizontalDistanceSquared(otherPoint));
482 }
483
484 @Override
485 @SuppressWarnings("checkstyle:designforextension")
486 public String toString()
487 {
488 return toString("%f");
489 }
490
491 @Override
492 public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
493 {
494 String format = String.format("%1$s[x=%2$s, y=%2$s, z=%2$s]", doNotIncludeClassName ? "" : "Point3d ", doubleFormat);
495 return String.format(Locale.US, format, this.x, this.y, this.z);
496 }
497
498 @Override
499 public int hashCode()
500 {
501 final int prime = 31;
502 int result = 1;
503 long temp;
504 temp = Double.doubleToLongBits(this.x);
505 result = prime * result + (int) (temp ^ (temp >>> 32));
506 temp = Double.doubleToLongBits(this.y);
507 result = prime * result + (int) (temp ^ (temp >>> 32));
508 temp = Double.doubleToLongBits(this.z);
509 result = prime * result + (int) (temp ^ (temp >>> 32));
510 return result;
511 }
512
513 @SuppressWarnings("checkstyle:needbraces")
514 @Override
515 public boolean equals(final Object obj)
516 {
517 if (this == obj)
518 return true;
519 if (obj == null)
520 return false;
521 if (getClass() != obj.getClass())
522 return false;
523 Point3d other = (Point3d) obj;
524 if (Double.doubleToLongBits(this.x) != Double.doubleToLongBits(other.x))
525 return false;
526 if (Double.doubleToLongBits(this.y) != Double.doubleToLongBits(other.y))
527 return false;
528 if (Double.doubleToLongBits(this.z) != Double.doubleToLongBits(other.z))
529 return false;
530 return true;
531 }
532
533 }