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