View Javadoc
1   package org.djutils.draw.bounds;
2   
3   import java.util.Arrays;
4   import java.util.Collection;
5   import java.util.Iterator;
6   import java.util.Locale;
7   
8   import org.djutils.draw.Drawable3d;
9   import org.djutils.draw.point.Point3d;
10  import org.djutils.exceptions.Throw;
11  
12  /**
13   * Bounds3d is the generic class for the 3D extent of an object. It is an immutable object.
14   * <p>
15   * Copyright (c) 2020-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
16   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
17   * </p>
18   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
19   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
20   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
21   */
22  public class Bounds3d implements Drawable3d, Bounds<Bounds3d, Point3d>
23  {
24      /** The lower bound for x. */
25      private final double minX;
26  
27      /** The lower bound for y. */
28      private final double minY;
29  
30      /** The lower bound for z. */
31      private final double minZ;
32  
33      /** The upper bound for x. */
34      private final double maxX;
35  
36      /** The upper bound for y. */
37      private final double maxY;
38  
39      /** The upper bound for z. */
40      private final double maxZ;
41  
42      /**
43       * Construct a Bounds3d by providing all lower and upper bounds.
44       * @param minX the lower bound for x
45       * @param maxX the upper bound for x
46       * @param minY the lower bound for y
47       * @param maxY the upper bound for y
48       * @param minZ the lower bound for z
49       * @param maxZ the upper bound for z
50       * @throws ArithmeticException when <code>minX</code>, <code>maxX</code>, <code>minY</code>,
51       *             <code>maxY</code>,<code>minZ</code>, or <code>maxZ</code> is <code>NaN</code>
52       * @throws IllegalArgumentException when <code>minX</code> &gt; <code>maxX</code>, <code>minY</code> &gt; <code>maxY</code>,
53       *             or <code>minZ</code> &gt; <code>maxZ</code>
54       */
55      public Bounds3d(final double minX, final double maxX, final double minY, final double maxY, final double minZ,
56              final double maxZ)
57      {
58          Throw.whenNaN(minX, "minX");
59          Throw.whenNaN(maxX, "maxX");
60          Throw.whenNaN(minY, "minY");
61          Throw.whenNaN(maxY, "maxY");
62          Throw.whenNaN(minZ, "minZ");
63          Throw.whenNaN(maxZ, "maxZ");
64          Throw.when(minX > maxX || minY > maxY || minZ > maxZ, IllegalArgumentException.class,
65                  "lower bound for each dimension should be less than or equal to its upper bound");
66          this.minX = minX;
67          this.minY = minY;
68          this.minZ = minZ;
69          this.maxX = maxX;
70          this.maxY = maxY;
71          this.maxZ = maxZ;
72      }
73  
74      /**
75       * Constructs a new Bounds3d around the origin (0, 0, 0).
76       * @param deltaX the deltaX value around the origin
77       * @param deltaY the deltaY value around the origin
78       * @param deltaZ the deltaZ value around the origin
79       * @throws ArithmeticException when <code>deltaX</code>, <code>deltaY</code>, or <code>deltaZ</code> is <code>NaN</code>
80       * @throws IllegalArgumentException when <code>deltaX &lt; 0.0</code>, <code>deltaY &lt; 0.0</code>, or <code>deltaZ &lt;
81       *             0.0</code>
82       */
83      public Bounds3d(final double deltaX, final double deltaY, final double deltaZ)
84      {
85          this(-0.5 * deltaX, 0.5 * deltaX, -0.5 * deltaY, 0.5 * deltaY, -0.5 * deltaZ, 0.5 * deltaZ);
86      }
87  
88      /**
89       * Construct a Bounds3d from some collection of points, finding the lowest and highest x and y coordinates.
90       * @param points the array of points to construct a Bounds3d from
91       * @throws NullPointerException when <code>points</code> is <code>null</code>
92       * @throws IllegalArgumentException when the <code>points</code> iterator provides zero points
93       */
94      public Bounds3d(final Iterator<? extends Point3d> points)
95      {
96          Throw.whenNull(points, "points");
97          Throw.when(!points.hasNext(), IllegalArgumentException.class, "need at least one point");
98          Point3d point = points.next();
99          double tempMinX = point.x;
100         double tempMaxX = point.x;
101         double tempMinY = point.y;
102         double tempMaxY = point.y;
103         double tempMinZ = point.z;
104         double tempMaxZ = point.z;
105         while (points.hasNext())
106         {
107             point = points.next();
108             tempMinX = Math.min(tempMinX, point.x);
109             tempMaxX = Math.max(tempMaxX, point.x);
110             tempMinY = Math.min(tempMinY, point.y);
111             tempMaxY = Math.max(tempMaxY, point.y);
112             tempMinZ = Math.min(tempMinZ, point.z);
113             tempMaxZ = Math.max(tempMaxZ, point.z);
114         }
115         this.minX = tempMinX;
116         this.maxX = tempMaxX;
117         this.minY = tempMinY;
118         this.maxY = tempMaxY;
119         this.minZ = tempMinZ;
120         this.maxZ = tempMaxZ;
121     }
122 
123     /**
124      * Construct a Bounds3d from an array of Point3d, finding the lowest and highest x, y and z coordinates.
125      * @param points the points to construct a Bounds3d from
126      * @throws NullPointerException when <code>points</code> is <code>null</code>
127      * @throws IllegalArgumentException when zero points are provided
128      */
129     public Bounds3d(final Point3d[] points)
130     {
131         this(Arrays.stream(Throw.whenNull(points, "points")).iterator());
132     }
133 
134     /**
135      * Construct a Bounds3d for a Drawable3d.
136      * @param drawable3d any object that implements the Drawable2d interface
137      * @throws NullPointerException when <code>drawable3d</code> is <code>null</code>
138      */
139     public Bounds3d(final Drawable3d drawable3d) throws NullPointerException
140     {
141         this(Throw.whenNull(drawable3d, "drawable3d").iterator());
142     }
143 
144     /**
145      * Construct a Bounds3d for several Drawable2d objects.
146      * @param drawable3d the Drawable2d objects
147      * @throws NullPointerException when the <code>drawable3d</code> array is <code>null</code>, or contains a <code>null</code>
148      *             value
149      * @throws IllegalArgumentException when the length of the <code>drawable3d</code> array is 0
150      */
151     public Bounds3d(final Drawable3d... drawable3d)
152     {
153         this(pointsOf(drawable3d));
154     }
155 
156     /**
157      * Return an iterator that will return all points of one or more Drawable objects.
158      * @param drawable3d the Drawable objects
159      * @return iterator that will return all points of the Drawable objects
160      * @throws NullPointerException when <code>drawable3darray</code> is <code>null</code>
161      * @throws IllegalArgumentException when the <code>drawable3dArray</code> contains 0 elements
162      */
163     public static Iterator<Point3d> pointsOf(final Drawable3d... drawable3d)
164     {
165         return new Iterator<Point3d>()
166         {
167             /** Index in the argument array. */
168             private int nextArgument = 0;
169 
170             /** Iterator over the Point2d objects in the current Drawable2d. */
171             private Iterator<? extends Point3d> currentIterator = ensureHasOne(drawable3d)[0].iterator();
172 
173             @Override
174             public boolean hasNext()
175             {
176                 return this.nextArgument < drawable3d.length - 1 || this.currentIterator.hasNext();
177             }
178 
179             @Override
180             public Point3d next()
181             {
182                 if (this.currentIterator.hasNext())
183                 {
184                     return this.currentIterator.next();
185                 }
186                 // Move to next Drawable2d
187                 this.nextArgument++;
188                 this.currentIterator = drawable3d[this.nextArgument].iterator();
189                 return this.currentIterator.next(); // Cannot fail because every Drawable has at least one point
190             }
191         };
192     }
193 
194     /**
195      * Verify that the array contains at least one entry.
196      * @param drawable3dArray array of Drawable2d objects
197      * @return the array
198      * @throws NullPointerException when the <code>drawable3dArray</code> is <code>null</code>
199      * @throws IllegalArgumentException when the <code>drawable3dArray</code> contains 0 elements
200      */
201     static Drawable3d[] ensureHasOne(final Drawable3d[] drawable3dArray)
202     {
203         Throw.whenNull(drawable3dArray, "drawable3dArray");
204         Throw.when(drawable3dArray.length == 0, IllegalArgumentException.class, "Array must contain at least one value");
205         return drawable3dArray;
206     }
207 
208     /**
209      * Construct a Bounds3d for a Collection of Drawable2d objects.
210      * @param drawableCollection the collection
211      * @throws NullPointerException when the <code>drawableCollection</code> is <code>null</code>, or contains <code>null</code>
212      *             values
213      * @throws IllegalArgumentException when the <code>drawableCollection</code> is empty
214      */
215     public Bounds3d(final Collection<Drawable3d> drawableCollection)
216     {
217         this(pointsOf(drawableCollection));
218     }
219 
220     /**
221      * Return an iterator that will return all points of one or more Drawable3d objects.
222      * @param drawableCollection the collection of Drawable2d objects
223      * @return iterator that will return all points of the Drawable objects
224      * @throws NullPointerException when the <code>drawableCollection</code> is <code>null</code>, or contains a
225      *             <code>null</code> value
226      * @throws IllegalArgumentException when the <code>drawableCollection</code> is empty
227      */
228     public static Iterator<Point3d> pointsOf(final Collection<Drawable3d> drawableCollection)
229     {
230         return new Iterator<Point3d>()
231         {
232             /** Iterator that iterates over the collection. */
233             private Iterator<Drawable3d> collectionIterator = ensureHasOne(drawableCollection.iterator());
234 
235             /** Iterator that generates Point2d objects for the currently selected element of the collection. */
236             private Iterator<? extends Point3d> currentIterator = this.collectionIterator.next().iterator();
237 
238             @Override
239             public boolean hasNext()
240             {
241                 if (this.currentIterator == null)
242                 {
243                     return false;
244                 }
245                 return this.currentIterator.hasNext();
246             }
247 
248             @Override
249             public Point3d next()
250             {
251                 Point3d result = this.currentIterator.next();
252                 if (!this.currentIterator.hasNext())
253                 {
254                     if (this.collectionIterator.hasNext())
255                     {
256                         this.currentIterator = this.collectionIterator.next().iterator();
257                     }
258                     else
259                     {
260                         this.currentIterator = null;
261                     }
262                 }
263                 return result;
264             }
265         };
266     }
267 
268     /**
269      * Verify that the iterator has something to return.
270      * @param iterator the iterator
271      * @return the iterator
272      * @throws NullPointerException when the <code>iterator</code> is <code>null</code>
273      * @throws IllegalArgumentException when the <code>hasNext</code> method of the <code>iterator</code> returns
274      *             <code>false</code> before even one <code>Drawable2d</code> was delivered
275      */
276     static Iterator<Drawable3d> ensureHasOne(final Iterator<Drawable3d> iterator)
277     {
278         Throw.when(!iterator.hasNext(), IllegalArgumentException.class, "Collection may not be empty");
279         return iterator;
280     }
281 
282     @Override
283     public Iterator<Point3d> iterator()
284     {
285         Point3d[] array =
286                 new Point3d[] {new Point3d(this.minX, this.minY, this.minZ), new Point3d(this.maxX, this.minY, this.minZ),
287                         new Point3d(this.maxX, this.maxY, this.minZ), new Point3d(this.minX, this.maxY, this.minZ),
288                         new Point3d(this.minX, this.maxY, this.maxZ), new Point3d(this.maxX, this.maxY, this.maxZ),
289                         new Point3d(this.maxX, this.minY, this.maxZ), new Point3d(this.minX, this.minY, this.maxZ)};
290         return Arrays.stream(array).iterator();
291     }
292 
293     @Override
294     public int size()
295     {
296         return 8;
297     }
298 
299     /**
300      * Check if this Bounds3d contains a point. Contains returns false when the point is on the surface of this Bounds3d.
301      * @param x the x-coordinate of the point
302      * @param y the y-coordinate of the point
303      * @param z the z-coordinate of the point
304      * @return whether this Bounds3d contains the point
305      * @throws ArithmeticException when <code>x</code>, or <code>y</code> is <code>NaN</code>
306      */
307     public boolean contains(final double x, final double y, final double z)
308     {
309         Throw.whenNaN(x, "x");
310         Throw.whenNaN(y, "y");
311         Throw.whenNaN(z, "z");
312         return x > this.minX && x < this.maxX && y > this.minY && y < this.maxY && z > this.minZ && z < this.maxZ;
313     }
314 
315     @Override
316     public boolean contains(final Point3d point)
317     {
318         Throw.whenNull(point, "point");
319         return contains(point.x, point.y, point.z);
320     }
321 
322     @Override
323     public boolean contains(final Bounds3d otherBounds) throws NullPointerException
324     {
325         Throw.whenNull(otherBounds, "otherBounds");
326         return contains(otherBounds.minX, otherBounds.minY, otherBounds.minZ)
327                 && contains(otherBounds.maxX, otherBounds.maxY, otherBounds.maxZ);
328     }
329 
330     /**
331      * Check if this Bounds3d contains a point. Covers returns <code>true</code> when the point is on a face of this Bounds3d.
332      * @param x the x-coordinate of the point
333      * @param y the y-coordinate of the point
334      * @param z the z-coordinate of the point
335      * @return whether the bounding box contains the point, including the faces
336      * @throws ArithmeticException when <code>x</code>, <code>y</code>, or <code>z</code> is <code>NaN</code>
337      */
338     public boolean covers(final double x, final double y, final double z)
339     {
340         Throw.whenNaN(x, "x");
341         Throw.whenNaN(y, "y");
342         Throw.whenNaN(z, "z");
343         return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY && z >= this.minZ && z <= this.maxZ;
344     }
345 
346     @Override
347     public boolean covers(final Point3d point)
348     {
349         Throw.whenNull(point, "point");
350         return covers(point.x, point.y, point.z);
351     }
352 
353     @Override
354     public boolean covers(final Bounds3d otherBounds)
355     {
356         Throw.whenNull(otherBounds, "otherBounds");
357         return covers(otherBounds.minX, otherBounds.minY, otherBounds.minZ)
358                 && covers(otherBounds.maxX, otherBounds.maxY, otherBounds.maxZ);
359     }
360 
361     @Override
362     public boolean disjoint(final Bounds3d otherBounds)
363     {
364         Throw.whenNull(otherBounds, "otherBounds");
365         return otherBounds.minX > this.maxX || otherBounds.maxX < this.minX || otherBounds.minY > this.maxY
366                 || otherBounds.maxY < this.minY || otherBounds.minZ > this.maxZ || otherBounds.maxZ < this.minZ;
367     }
368 
369     @Override
370     public boolean intersects(final Bounds3d otherBounds3d)
371     {
372         return !disjoint(otherBounds3d);
373     }
374 
375     @Override
376     public Bounds3d intersection(final Bounds3d otherBounds3d)
377     {
378         Throw.whenNull(otherBounds3d, "otherBounds3d");
379         if (disjoint(otherBounds3d))
380         {
381             return null;
382         }
383         return new Bounds3d(Math.max(this.minX, otherBounds3d.minX), Math.min(this.maxX, otherBounds3d.maxX),
384                 Math.max(this.minY, otherBounds3d.minY), Math.min(this.maxY, otherBounds3d.maxY),
385                 Math.max(this.minZ, otherBounds3d.minZ), Math.min(this.maxZ, otherBounds3d.maxZ));
386     }
387 
388     @Override
389     public Bounds2d project()
390     {
391         return new Bounds2d(this.minX, this.maxX, this.minY, this.maxY);
392     }
393 
394     /**
395      * Return the extent of this Bounds3d in the z-direction.
396      * @return the extent of this Bounds3d in the z-direction
397      */
398     public double getDeltaZ()
399     {
400         return getMaxZ() - getMinZ();
401     }
402 
403     /**
404      * Return the volume of this Bounds3d.
405      * @return the volume of this Bounds3d
406      */
407     public double getVolume()
408     {
409         return getDeltaX() * getDeltaY() * getDeltaZ();
410     }
411 
412     @Override
413     public double getMinX()
414     {
415         return this.minX;
416     }
417 
418     @Override
419     public double getMaxX()
420     {
421         return this.maxX;
422     }
423 
424     @Override
425     public double getMinY()
426     {
427         return this.minY;
428     }
429 
430     @Override
431     public double getMaxY()
432     {
433         return this.maxY;
434     }
435 
436     /**
437      * Return the lower bound for z.
438      * @return the lower bound for z
439      */
440     public double getMinZ()
441     {
442         return this.minZ;
443     }
444 
445     /**
446      * Return the upper bound for z.
447      * @return the upper bound for z
448      */
449     public double getMaxZ()
450     {
451         return this.maxZ;
452     }
453 
454     @Override
455     public Point3d midPoint()
456     {
457         return new Point3d((this.minX + this.maxX) / 2, (this.minY + this.maxY) / 2, (this.minZ + this.maxZ) / 2);
458     }
459 
460     @Override
461     public Bounds3d getAbsoluteBounds()
462     {
463         return this;
464     }
465 
466     @Override
467     public String toString()
468     {
469         return toString("%f", false);
470     }
471 
472     @Override
473     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
474     {
475         String format = String.format("%1$s[x[%2$s : %2$s], y[%2$s : %2$s, z[%2$s : %2$s]]",
476                 doNotIncludeClassName ? "" : "Bounds3d ", doubleFormat);
477         return String.format(Locale.US, format, this.minX, this.maxX, this.minY, this.maxY, this.minZ, this.maxZ);
478     }
479 
480     @Override
481     public int hashCode()
482     {
483         final int prime = 31;
484         int result = 1;
485         long temp;
486         temp = Double.doubleToLongBits(this.maxX);
487         result = prime * result + (int) (temp ^ (temp >>> 32));
488         temp = Double.doubleToLongBits(this.maxY);
489         result = prime * result + (int) (temp ^ (temp >>> 32));
490         temp = Double.doubleToLongBits(this.maxZ);
491         result = prime * result + (int) (temp ^ (temp >>> 32));
492         temp = Double.doubleToLongBits(this.minX);
493         result = prime * result + (int) (temp ^ (temp >>> 32));
494         temp = Double.doubleToLongBits(this.minY);
495         result = prime * result + (int) (temp ^ (temp >>> 32));
496         temp = Double.doubleToLongBits(this.minZ);
497         result = prime * result + (int) (temp ^ (temp >>> 32));
498         return result;
499     }
500 
501     @SuppressWarnings("checkstyle:needbraces")
502     @Override
503     public boolean equals(final Object obj)
504     {
505         if (this == obj)
506             return true;
507         if (obj == null)
508             return false;
509         if (getClass() != obj.getClass())
510             return false;
511         Bounds3d other = (Bounds3d) obj;
512         if (Double.doubleToLongBits(this.maxX) != Double.doubleToLongBits(other.maxX))
513             return false;
514         if (Double.doubleToLongBits(this.maxY) != Double.doubleToLongBits(other.maxY))
515             return false;
516         if (Double.doubleToLongBits(this.maxZ) != Double.doubleToLongBits(other.maxZ))
517             return false;
518         if (Double.doubleToLongBits(this.minX) != Double.doubleToLongBits(other.minX))
519             return false;
520         if (Double.doubleToLongBits(this.minY) != Double.doubleToLongBits(other.minY))
521             return false;
522         if (Double.doubleToLongBits(this.minZ) != Double.doubleToLongBits(other.minZ))
523             return false;
524         return true;
525     }
526 
527 }