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