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