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