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