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