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-2021 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 Drawable3de3d">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 Point3doint3d.html#Point3d">Point3d[] { new Point3d(Point3dord">this.minX, this.minY, Point3ds="jxr_keyword">this.minZ), new Point3d(this.minX, this.minY, this.maxZ),
280                         new Point3d(this.minX, this.maxY, Point3ds="jxr_keyword">this.minZ), new Point3d(this.minX, this.maxY, this.maxZ),
281                         new Point3d(this.maxX, this.minY, Point3ds="jxr_keyword">this.minZ), new Point3d(this.maxX, this.minY, this.maxZ),
282                         new Point3d(this.maxX, this.maxY, Point3ds="jxr_keyword">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 Bounds3dl#Bounds3d">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./../../../org/djutils/draw/bounds/Bounds3d.html#Bounds3d">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 }