package org.djutils.draw.bounds; import java.awt.geom.Rectangle2D; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import org.djutils.draw.Drawable2d; import org.djutils.draw.point.Point2d; import org.djutils.exceptions.Throw; /** * A Bounds2d stores the rectangular 2D bounds of a 2d object, or a collection of 2dobjects. The Bounds2d is an immutable * object. *

* Copyright (c) 2020-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See DJUTILS License. *

* @author Alexander Verbraeck * @author Peter Knoppers */ public class Bounds2d implements Drawable2d { /** */ private static final long serialVersionUID = 20200829L; /** The lower bound for x. */ private final double minX; /** The lower bound for y. */ private final double minY; /** The upper bound for x. */ private final double maxX; /** The upper bound for y. */ private final double maxY; /** * Construct a Bounds2d by providing its lower and upper bounds in both dimensions. * @param minX double; the lower bound for x * @param maxX double; the upper bound for x * @param minY double; the lower bound for y * @param maxY double; the upper bound for y * @throws IllegalArgumentException when a lower bound is larger than the corresponding upper bound, or any of the bounds is * NaN */ public Bounds2d(final double minX, final double maxX, final double minY, final double maxY) throws IllegalArgumentException { Throw.when(Double.isNaN(minX) || Double.isNaN(maxX) || Double.isNaN(minY) || Double.isNaN(maxY), IllegalArgumentException.class, "bounds must be numbers (not NaN)"); Throw.when(minX > maxX || minY > maxY, IllegalArgumentException.class, "lower bound for each dimension should be less than or equal to its upper bound"); this.minX = minX; this.minY = minY; this.maxX = maxX; this.maxY = maxY; } /** * Constructs a new Bounds2d around the origin (0, 0). * @param deltaX double; the deltaX value around the origin * @param deltaY double; the deltaY value around the origin * @throws IllegalArgumentException when one of the delta values is less than zero */ public Bounds2d(final double deltaX, final double deltaY) { this(-0.5 * deltaX, 0.5 * deltaX, -0.5 * deltaY, 0.5 * deltaY); } /** * Construct a Bounds2d from some collection of points, finding the lowest and highest x and y coordinates. * @param points Iterator<Point2d>; Iterator that will generate all the points for which to construct a Bounds2d * @throws NullPointerException when points is null * @throws IllegalArgumentException when the iterator provides zero points */ public Bounds2d(final Iterator points) { Throw.whenNull(points, "points may not be null"); Throw.when(!points.hasNext(), IllegalArgumentException.class, "need at least one point"); Point2d point = points.next(); double tempMinX = point.x; double tempMaxX = point.x; double tempMinY = point.y; double tempMaxY = point.y; while (points.hasNext()) { point = points.next(); tempMinX = Math.min(tempMinX, point.x); tempMaxX = Math.max(tempMaxX, point.x); tempMinY = Math.min(tempMinY, point.y); tempMaxY = Math.max(tempMaxY, point.y); } this.minX = tempMinX; this.maxX = tempMaxX; this.minY = tempMinY; this.maxY = tempMaxY; } /** * Construct a Bounds2d from an array of Point2d, finding the lowest and highest x and y coordinates. * @param points Point2d[]; the points to construct a Bounds2d from * @throws NullPointerException when points is null * @throws IllegalArgumentException when zero points are provided */ public Bounds2d(final Point2d[] points) throws NullPointerException, IllegalArgumentException { this(Arrays.stream(Throw.whenNull(points, "points may not be null")).iterator()); } /** * Construct a Bounds2d for a Drawable2d. * @param drawable2d Drawable2d; any object that implements the Drawable2d interface * @throws NullPointerException when drawable2d is null */ public Bounds2d(final Drawable2d drawable2d) throws NullPointerException { this(Throw.whenNull(drawable2d, "drawable2d may not be null").getPoints()); } /** * Construct a Bounds2d from a collection of Point2d, finding the lowest and highest x and y coordinates. * @param points Collection<Point2d>; the collection of points to construct a Bounds2d from * @throws NullPointerException when points is null * @throws IllegalArgumentException when the collection is empty */ public Bounds2d(final Collection points) { this(Throw.whenNull(points, "points may not be null").iterator()); } /** {@inheritDoc} */ @Override public Iterator getPoints() { Point2d[] array = new Point2d[] { new Point2d(getMinX(), getMinY()), new Point2d(getMinX(), getMaxY()), new Point2d(getMaxX(), getMinY()), new Point2d(getMaxX(), getMaxY()) }; return Arrays.stream(array).iterator(); } /** {@inheritDoc} */ @Override public int size() { return 4; } /** * Check if this Bounds2d contains a given point. Contains considers a point on the border of this Bounds2d to be * outside. * @param point Point<Space2d>; the point * @return boolean; true this Bounds2d contains the point; false if this Bounds2d does not contain the point * @throws NullPointerException when point is null */ public boolean contains(final Point2d point) { Throw.whenNull(point, "point cannot be null"); return contains(point.x, point.y); } /** * Check if this Bounds2d contains a point. Contains considers a point on the border of this Bounds2d to be outside. * @param x double; the x-coordinate of the point * @param y double; the y-coordinate of the point * @return boolean; whether this Bounds2d contains the point * @throws IllegalArgumentException when any of the coordinates is NaN */ public boolean contains(final double x, final double y) throws IllegalArgumentException { Throw.when(Double.isNaN(x) || Double.isNaN(y), IllegalArgumentException.class, "coordinates must be numbers (not NaN)"); return x > this.minX && x < this.maxX && y > this.minY && y < this.maxY; } /** * Check if this Bounds2d completely contains a Drawable2d. * @param drawable Drawable2d; the object for which to check if it is completely contained within this Bounds2d. * @return boolean; false if any point of the Drawable2d is on or outside one of the borders of this Bounds2d; true when all * points of the Drawable2d are contained within this Bounds2d. * @throws NullPointerException when drawable2d is null */ public boolean contains(final Drawable2d drawable) throws NullPointerException { Throw.whenNull(drawable, "drawable cannot be null"); for (Iterator iterator = drawable.getPoints(); iterator.hasNext();) { if (!contains(iterator.next())) { return false; } } return true; } /** * Check if this Bounds2d contains a point. Covers returns true when the point is on, or within the border of this Bounds2d. * @param x double; the x-coordinate of the point * @param y double; the y-coordinate of the point * @return boolean; whether this Bounds2d, including its borders, contains the point */ public boolean covers(final double x, final double y) { Throw.when(Double.isNaN(x) || Double.isNaN(y), IllegalArgumentException.class, "coordinates must be numbers (not NaN)"); return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; } /** * Check if this Bounds2d contains a point. Covers returns true when the point is on, or within the border of this Bounds2d. * @param point Point; the point * @return boolean; whether this Bounds2d, including its borders, contains the point * @throws NullPointerException when point is null */ public boolean covers(final Point2d point) { Throw.whenNull(point, "point cannot be null"); return covers(point.x, point.y); } /** * Check if this Bounds2d contains another Bounds2d. Covers returns true when one of the edges of the other Bounds2d * (partly) overlaps a border of this Bounds2d. * @param otherBounds2d Bounds2d; the Bounds2d for which to check if it is contained within this Bounds2d * @return boolean; whether this Bounds2d contains the provided Bounds2d, including overlapping borders * @throws NullPointerException when otherBounds2d is null */ public boolean covers(final Bounds2d otherBounds2d) throws NullPointerException { Throw.whenNull(otherBounds2d, "otherBounds2d cannot be null"); return covers(otherBounds2d.minX, otherBounds2d.minY) && covers(otherBounds2d.maxX, otherBounds2d.maxY); } /** * Return whether this Bounds2d is disjoint from another Bounds2d. Only touching at an edge is considered disjoint. * @param otherBounds2d Bounds2d; the other Bounds2d * @return boolean; whether this Bounds2d is disjoint from another Bounds2d * @throws NullPointerException when bounds2d is null */ public boolean disjoint(final Bounds2d otherBounds2d) throws NullPointerException { Throw.whenNull(otherBounds2d, "otherBounds2d cannot be null"); return otherBounds2d.minX >= this.maxX || otherBounds2d.maxX <= this.minX || otherBounds2d.minY >= this.maxY || otherBounds2d.maxY <= this.minY; } /** * Return whether this Bounds2d intersects another Bounds2d. Only touching at an edge is not seen as intersecting. * @param otherBounds2d Bounds2d; the other Bounds2d * @return boolean; whether this bounding rectangle intersects the other Bounds2d * @throws NullPointerException when otherBounds2d is null */ public boolean intersects(final Bounds2d otherBounds2d) throws NullPointerException { return !disjoint(otherBounds2d); } /** * Return the intersecting Bounds2d of this Bounds2d and another Bounds2d. Touching at the edge is not seen as intersecting. * In case there is no intersection, null is returned. * @param otherBounds2d Bounds2d; the other Bounds2d * @return Bounds2d; the intersecting Bounds2d of this Bounds2d and another Bounds2d. Touching at the edge is not seen as * intersecting. If not intersecting; null is returned * @throws NullPointerException when otherBounds2d is null */ public Bounds2d intersection(final Bounds2d otherBounds2d) { Throw.whenNull(otherBounds2d, "otherBounds2d cannot be null"); if (disjoint(otherBounds2d)) { return null; } return new Bounds2d(Math.max(this.getMinX(), otherBounds2d.getMinX()), Math.min(this.getMaxX(), otherBounds2d.getMaxX()), Math.max(this.getMinY(), otherBounds2d.getMinY()), Math.min(this.getMaxY(), otherBounds2d.getMaxY())); } /** * Return the extent of this Bounds2d in the x-direction. * @return double; the extent of this Bounds2d in the x-direction */ public double getDeltaX() { return getMaxX() - getMinX(); } /** * Return the extent of this Bounds2d in the y-direction. * @return double; the extent of this Bounds2d in the y-direction */ public double getDeltaY() { return getMaxY() - getMinY(); } /** * Return an AWT Rectangle2D that covers the same area as this Bounds2d. * @return Rectangle2D; the rectangle that covers the same area as this Bounds2d */ public Rectangle2D toRectangle2D() { return new Rectangle2D.Double(this.minX, this.minY, this.maxX - this.minX, this.maxY - this.minY); } /** * Return the lower bound for x. * @return double; the lower bound for x */ public double getMinX() { return this.minX; } /** * Return the upper bound for x. * @return double; the upper bound for x */ public double getMaxX() { return this.maxX; } /** * Return the lower bound for y. * @return double; the lower bound for y */ public double getMinY() { return this.minY; } /** * Return the upper bound for y. * @return double; the upper bound for y */ public double getMaxY() { return this.maxY; } /** * Return the area of this Bounds2d. * @return double; the area of this Bounds2d */ public double getArea() { return getDeltaX() * getDeltaY(); } /** {@inheritDoc} */ @Override public Bounds2d getBounds() { return this; } /** {@inheritDoc} */ @Override public String toString() { return "Bounds2d [x[" + this.minX + " : " + this.maxX + "], y[" + this.minY + " : " + this.maxY + "]]"; } /** {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; long temp; temp = Double.doubleToLongBits(this.maxX); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(this.maxY); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(this.minX); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(this.minY); result = prime * result + (int) (temp ^ (temp >>> 32)); return result; } /** {@inheritDoc} */ @SuppressWarnings("checkstyle:needbraces") @Override public boolean equals(final Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Bounds2d other = (Bounds2d) obj; if (Double.doubleToLongBits(this.maxX) != Double.doubleToLongBits(other.maxX)) return false; if (Double.doubleToLongBits(this.maxY) != Double.doubleToLongBits(other.maxY)) return false; if (Double.doubleToLongBits(this.minX) != Double.doubleToLongBits(other.minX)) return false; if (Double.doubleToLongBits(this.minY) != Double.doubleToLongBits(other.minY)) return false; return true; } }