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