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.Space2d; 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 2d objects. The Bounds2d is an immutable * object. *

* Copyright (c) 2020-2021 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, Bounds { /** */ private static final long serialVersionUID = 20200829L; /** The lower bound for x. */ private final double minAbsoluteX; /** The lower bound for y. */ private final double minAbsoluteY; /** The upper bound for x. */ private final double maxAbsoluteX; /** The upper bound for y. */ private final double maxAbsoluteY; /** * Construct a Bounds2d by providing its lower and upper bounds in both dimensions. * @param minAbsoluteX double; the lower bound for x * @param maxAbsoluteX double; the upper bound for x * @param minAbsoluteY double; the lower bound for y * @param maxAbsoluteY 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 minAbsoluteX, final double maxAbsoluteX, final double minAbsoluteY, final double maxAbsoluteY) throws IllegalArgumentException { Throw.when(Double.isNaN(minAbsoluteX) || Double.isNaN(maxAbsoluteX) || Double.isNaN(minAbsoluteY) || Double.isNaN(maxAbsoluteY), IllegalArgumentException.class, "bounds must be numbers (not NaN)"); Throw.when(minAbsoluteX > maxAbsoluteX || minAbsoluteY > maxAbsoluteY, IllegalArgumentException.class, "lower bound for each dimension should be less than or equal to its upper bound"); this.minAbsoluteX = minAbsoluteX; this.minAbsoluteY = minAbsoluteY; this.maxAbsoluteX = maxAbsoluteX; this.maxAbsoluteY = maxAbsoluteY; } /** * 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<? extends 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.minAbsoluteX = tempMinX; this.maxAbsoluteX = tempMaxX; this.minAbsoluteY = tempMinY; this.maxAbsoluteY = 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(this.minAbsoluteX, this.minAbsoluteY), new Point2d(this.minAbsoluteX, this.maxAbsoluteY), new Point2d(this.maxAbsoluteX, this.minAbsoluteY), new Point2d(this.maxAbsoluteX, this.maxAbsoluteY) }; 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 Point2d; 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.minAbsoluteX && x < this.maxAbsoluteX && y > this.minAbsoluteY && y < this.maxAbsoluteY; } /** * 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.minAbsoluteX && x <= this.maxAbsoluteX && y >= this.minAbsoluteY && y <= this.maxAbsoluteY; } /** * Check if this Bounds2d contains a point. Covers returns true when the point is on, or within the border of this Bounds2d. * @param point Point2d; 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); } /** {@inheritDoc} */ @Override public boolean covers(final Bounds2d otherBounds2d) throws NullPointerException { Throw.whenNull(otherBounds2d, "otherBounds2d cannot be null"); return covers(otherBounds2d.minAbsoluteX, otherBounds2d.minAbsoluteY) && covers(otherBounds2d.maxAbsoluteX, otherBounds2d.maxAbsoluteY); } /** {@inheritDoc} */ @Override public boolean disjoint(final Bounds2d otherBounds2d) throws NullPointerException { Throw.whenNull(otherBounds2d, "otherBounds2d cannot be null"); return otherBounds2d.minAbsoluteX >= this.maxAbsoluteX || otherBounds2d.maxAbsoluteX <= this.minAbsoluteX || otherBounds2d.minAbsoluteY >= this.maxAbsoluteY || otherBounds2d.maxAbsoluteY <= this.minAbsoluteY; } /** {@inheritDoc} */ @Override public boolean intersects(final Bounds2d otherBounds2d) throws NullPointerException { return !disjoint(otherBounds2d); } /** {@inheritDoc} */ @Override public Bounds2d intersection(final Bounds2d otherBounds2d) { Throw.whenNull(otherBounds2d, "otherBounds2d cannot be null"); if (disjoint(otherBounds2d)) { return null; } return new Bounds2d(Math.max(this.minAbsoluteX, otherBounds2d.minAbsoluteX), Math.min(this.maxAbsoluteX, otherBounds2d.maxAbsoluteX), Math.max(this.minAbsoluteY, otherBounds2d.minAbsoluteY), Math.min(this.maxAbsoluteY, otherBounds2d.maxAbsoluteY)); } /** * 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.minAbsoluteX, this.minAbsoluteY, this.maxAbsoluteX - this.minAbsoluteX, this.maxAbsoluteY - this.minAbsoluteY); } /** {@inheritDoc} */ @Override public double getAbsoluteMinX() { return this.minAbsoluteX; } /** {@inheritDoc} */ @Override public double getAbsoluteMaxX() { return this.maxAbsoluteX; } /** {@inheritDoc} */ @Override public double getAbsoluteMinY() { return this.minAbsoluteY; } /** {@inheritDoc} */ @Override public double getAbsoluteMaxY() { return this.maxAbsoluteY; } /** {@inheritDoc} */ @Override public Point2d midPoint() { return new Point2d((this.minAbsoluteX + this.maxAbsoluteX) / 2, (this.minAbsoluteY + this.maxAbsoluteY) / 2); } /** * 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 toString("%f"); } /** {@inheritDoc} */ @Override public String toString(final String doubleFormat, final boolean doNotIncludeClassName) { String format = String.format("%1$s[absoluteX[%2$s : %2$s], absoluteY[%2$s : %2$s]]", doNotIncludeClassName ? "" : "Bounds2d ", doubleFormat); return String.format(format, this.minAbsoluteX, this.maxAbsoluteX, this.minAbsoluteY, this.maxAbsoluteY); } /** {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; long temp; temp = Double.doubleToLongBits(this.maxAbsoluteX); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(this.maxAbsoluteY); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(this.minAbsoluteX); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(this.minAbsoluteY); 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.maxAbsoluteX) != Double.doubleToLongBits(other.maxAbsoluteX)) return false; if (Double.doubleToLongBits(this.maxAbsoluteY) != Double.doubleToLongBits(other.maxAbsoluteY)) return false; if (Double.doubleToLongBits(this.minAbsoluteX) != Double.doubleToLongBits(other.minAbsoluteX)) return false; if (Double.doubleToLongBits(this.minAbsoluteY) != Double.doubleToLongBits(other.minAbsoluteY)) return false; return true; } }