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, Drawable3d>
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 may not be null");
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 may not be null")).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 may not be null").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, "Array may not be null");
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     /** {@inheritDoc} */
275     @Override
276     public Iterator<Point3d> getPoints()
277     {
278         Point3d[] array =
279                 new Point3d[] { new Point3d(this.minX, this.minY, this.minZ), new Point3d(this.minX, this.minY, this.maxZ),
280                         new Point3d(this.minX, this.maxY, this.minZ), new Point3d(this.minX, this.maxY, this.maxZ),
281                         new Point3d(this.maxX, this.minY, this.minZ), new Point3d(this.maxX, this.minY, this.maxZ),
282                         new Point3d(this.maxX, this.maxY, this.minZ), new Point3d(this.maxX, this.maxY, this.maxZ) };
283         return Arrays.stream(array).iterator();
284     }
285 
286     /** {@inheritDoc} */
287     @Override
288     public int size()
289     {
290         return 8;
291     }
292 
293     /**
294      * Check if this Bounds3d contains a point. Contains returns false when the point is on the surface of this Bounds3d.
295      * @param x double; the x-coordinate of the point
296      * @param y double; the y-coordinate of the point
297      * @param z double; the z-coordinate of the point
298      * @return boolean; whether this Bounds3d contains the point
299      * @throws IllegalArgumentException when any of the coordinates is NaN
300      */
301     public boolean contains(final double x, final double y, final double z) throws IllegalArgumentException
302     {
303         Throw.when(Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z), IllegalArgumentException.class,
304                 "coordinates must be numbers (not NaN)");
305         return x > this.minX && x < this.maxX && y > this.minY && y < this.maxY && z > this.minZ && z < this.maxZ;
306     }
307 
308     /** {@inheritDoc} */
309     @Override
310     public boolean contains(final Point3d point)
311     {
312         Throw.whenNull(point, "point cannot be null");
313         return contains(point.x, point.y, point.z);
314     }
315 
316     /** {@inheritDoc} */
317     @Override
318     public boolean contains(final Drawable3d drawable) throws NullPointerException
319     {
320         Throw.whenNull(drawable, "drawable cannot be null");
321         Bounds3d bounds = drawable.getBounds();
322         return contains(bounds.minX, bounds.minY, bounds.minZ) && contains(bounds.maxX, bounds.maxY, bounds.maxZ);
323     }
324 
325     /**
326      * Check if this Bounds3d contains a point. Covers returns true when the point is on a face of this Bounds3d.
327      * @param x double; the x-coordinate of the point
328      * @param y double; the y-coordinate of the point
329      * @param z double; the z-coordinate of the point
330      * @return boolean; whether the bounding box contains the point, including the faces
331      * @throws IllegalArgumentException when any of the coordinates is NaN
332      */
333     public boolean covers(final double x, final double y, final double z) throws IllegalArgumentException
334     {
335         Throw.when(Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z), IllegalArgumentException.class,
336                 "coordinates must be numbers (not NaN)");
337         return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY && z >= this.minZ && z <= this.maxZ;
338     }
339 
340     /** {@inheritDoc} */
341     @Override
342     public boolean covers(final Point3d point)
343     {
344         Throw.whenNull(point, "point cannot be null");
345         return covers(point.x, point.y, point.z);
346     }
347 
348     /** {@inheritDoc} */
349     @Override
350     public boolean covers(final Drawable3d drawable)
351     {
352         Throw.whenNull(drawable, "drawable cannot be null");
353         Bounds3d bounds = drawable.getBounds();
354         return covers(bounds.minX, bounds.minY, bounds.minZ) && covers(bounds.maxX, bounds.maxY, bounds.maxZ);
355     }
356 
357     /** {@inheritDoc} */
358     @Override
359     public boolean disjoint(final Drawable3d drawable)
360     {
361         Throw.whenNull(drawable, "drawable cannot be null");
362         Bounds3d bounds = drawable.getBounds();
363         return bounds.minX > this.maxX || bounds.maxX < this.minX || bounds.minY > this.maxY || bounds.maxY < this.minY
364                 || bounds.minZ > this.maxZ || bounds.maxZ < this.minZ;
365     }
366 
367     /** {@inheritDoc} */
368     @Override
369     public boolean intersects(final Bounds3d otherBounds3d)
370     {
371         return !disjoint(otherBounds3d);
372     }
373 
374     /** {@inheritDoc} */
375     @Override
376     public Bounds3d intersection(final Bounds3d otherBounds3d)
377     {
378         Throw.whenNull(otherBounds3d, "otherBounds3d cannot be null");
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     /** {@inheritDoc} */
389     @Override
390     public Bounds2d project()
391     {
392         return new Bounds2d(this.minX, this.maxX, this.minY, this.maxY);
393     }
394 
395     /**
396      * Return the extent of this Bounds3d in the z-direction.
397      * @return double; the extent of this Bounds3d in the z-direction
398      */
399     public double getDeltaZ()
400     {
401         return getMaxZ() - getMinZ();
402     }
403 
404     /**
405      * Return the volume of this Bounds3d.
406      * @return double; the volume of this Bounds3d
407      */
408     public double getVolume()
409     {
410         return getDeltaX() * getDeltaY() * getDeltaZ();
411     }
412 
413     /** {@inheritDoc} */
414     @Override
415     public double getMinX()
416     {
417         return this.minX;
418     }
419 
420     /** {@inheritDoc} */
421     @Override
422     public double getMaxX()
423     {
424         return this.maxX;
425     }
426 
427     /** {@inheritDoc} */
428     @Override
429     public double getMinY()
430     {
431         return this.minY;
432     }
433 
434     /** {@inheritDoc} */
435     @Override
436     public double getMaxY()
437     {
438         return this.maxY;
439     }
440 
441     /**
442      * Return the lower bound for z.
443      * @return double; the lower bound for z
444      */
445     public double getMinZ()
446     {
447         return this.minZ;
448     }
449 
450     /**
451      * Return the upper bound for z.
452      * @return double; the upper bound for z
453      */
454     public double getMaxZ()
455     {
456         return this.maxZ;
457     }
458 
459     /** {@inheritDoc} */
460     @Override
461     public Point3d midPoint()
462     {
463         return new Point3d((this.minX + this.maxX) / 2, (this.minY + this.maxY) / 2, (this.minZ + this.maxZ) / 2);
464     }
465 
466     /** {@inheritDoc} */
467     @Override
468     public Bounds3d getBounds()
469     {
470         return this;
471     }
472 
473     /** {@inheritDoc} */
474     @Override
475     public String toString()
476     {
477         return toString("%f", false);
478     }
479 
480     /** {@inheritDoc} */
481     @Override
482     public String toString(final String doubleFormat, final boolean doNotIncludeClassName)
483     {
484         String format = String.format("%1$s[x[%2$s : %2$s], y[%2$s : %2$s, z[%2$s : %2$s]]",
485                 doNotIncludeClassName ? "" : "Bounds3d ", doubleFormat);
486         return String.format(Locale.US, format, this.minX, this.maxX, this.minY, this.maxY, this.minZ, this.maxZ);
487     }
488 
489     /** {@inheritDoc} */
490     @Override
491     public int hashCode()
492     {
493         final int prime = 31;
494         int result = 1;
495         long temp;
496         temp = Double.doubleToLongBits(this.maxX);
497         result = prime * result + (int) (temp ^ (temp >>> 32));
498         temp = Double.doubleToLongBits(this.maxY);
499         result = prime * result + (int) (temp ^ (temp >>> 32));
500         temp = Double.doubleToLongBits(this.maxZ);
501         result = prime * result + (int) (temp ^ (temp >>> 32));
502         temp = Double.doubleToLongBits(this.minX);
503         result = prime * result + (int) (temp ^ (temp >>> 32));
504         temp = Double.doubleToLongBits(this.minY);
505         result = prime * result + (int) (temp ^ (temp >>> 32));
506         temp = Double.doubleToLongBits(this.minZ);
507         result = prime * result + (int) (temp ^ (temp >>> 32));
508         return result;
509     }
510 
511     /** {@inheritDoc} */
512     @SuppressWarnings("checkstyle:needbraces")
513     @Override
514     public boolean equals(final Object obj)
515     {
516         if (this == obj)
517             return true;
518         if (obj == null)
519             return false;
520         if (getClass() != obj.getClass())
521             return false;
522         Bounds3d other = (Bounds3d) obj;
523         if (Double.doubleToLongBits(this.maxX) != Double.doubleToLongBits(other.maxX))
524             return false;
525         if (Double.doubleToLongBits(this.maxY) != Double.doubleToLongBits(other.maxY))
526             return false;
527         if (Double.doubleToLongBits(this.maxZ) != Double.doubleToLongBits(other.maxZ))
528             return false;
529         if (Double.doubleToLongBits(this.minX) != Double.doubleToLongBits(other.minX))
530             return false;
531         if (Double.doubleToLongBits(this.minY) != Double.doubleToLongBits(other.minY))
532             return false;
533         if (Double.doubleToLongBits(this.minZ) != Double.doubleToLongBits(other.minZ))
534             return false;
535         return true;
536     }
537 
538 }