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