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