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 <code>fraction</code> 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 -π and π. 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 -π and π. 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 }