Locations in the 2D plane will be represented using the provided (immutable) Point
class:
/**
* A location in a 2D plane's coordinate system. Immutable.
*/
public class Point {
/**
* Horizontal displacement from the y-axis. Finite.
*/
private final double x;
/**
* Vertical displacement from the x-axis. Finite.
*/
private final double y;
/**
* Construct a point at the origin of the coordinate system.
*/
public Point() {
// Final fields have no default value, so must assign explicitly.
x = 0.0;
y = 0.0;
}
/**
* Construct a point with given Cartesian coordinates.
*
* @param x Point's horizontal displacement from the y-axis. Finite.
* @param y Point's vertical displacement from the x-axis. Finite.
*/
public Point(double x, double y) {
assert Double.isFinite(x);
assert Double.isFinite(y);
this.x = x;
this.y = y;
}
/**
* The point's horizontal displacement from the y-axis. Finite.
*/
public double x() {
return x;
}
/**
* The point's vertical displacement from the x-axis. Finite.
*/
public double y() {
return y;
}
/**
* Return a point displaced from this point by the provided amounts.
*
* @param dx Horizontal displacement of result relative to this point. Finite.
* @param dy Vertical displacement of result relative to this point. Finite.
* @return Point at displaced location.
*/
public Point shifted(double dx, double dy) {
assert Double.isFinite(x);
assert Double.isFinite(y);
// Bug: Sum could overflow, violating constructor precondition.
return new Point(x + dx, y + dy);
}
/**
* A String representation of this point's coordinates, which are surrounded by parentheses and
* separated by a comma.
*/
@Override
public String toString() {
return "(" + x + "," + y + ")";
}
/**
* True iff obj is a Point at the same location as this point.
*/
@Override
public boolean equals(Object obj) {
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
Point p = (Point) obj;
return (x == p.x) && (y == p.y);
}
// Must override hashCode when overriding equals.
@Override
public int hashCode() {
return java.util.Objects.hash(x, y);
}
}
Given the functionality requirements, an immutable interface for a bounding box could look something like this:
/**
* An axis-aligned rectangle in a 2D plane.
*/
public interface BoundingBox {
/**
* The width of the box (in units of the coordinate system). Finite and non-negative.
*/
double width();
/**
* The height of the box (in units of the coordinate system). Finite and non-negative.
*/
double height();
/**
* The location of the centroid (center of area) of the box in the coordinate system. Non-null.
*/
Point centroid();
/**
* The locations of the corners of the box in the coordinate system. Corners are enumerated
* counter-clockwise, starting with the lower-left corner (minimum x & y coordinates).
*
* @return Length-4 array containing the (non-null) locations of the corners.
*/
Point[] corners();
/**
* The area enclosed by the box (in squared units of the coordinate system). Non-negative and
* non-NaN.
*/
// BUG: What if area overflows? Is Inf allowed?
double area();
/**
* Determine whether the specified point is contained strictly within the interior of this box.
* Points on the boundary of the box are not considered to be contained within its interior.
*
* @param p The point to perform the interior test on. Non-null.
* @return True if <code>p</code> is strictly within the interior of this box; otherwise false.
*/
boolean contains(Point p);
/**
* Determine whether the interior of a specified box is a subset of the interior of this box.
*
* @param b The box to perform the interior test on. Non-null.
* @return True if the interior of <code>b</code> is a subset of the interior of this box;
* otherwise false.
*/
boolean contains(BoundingBox b);
/**
* Compute the intersection of this box with another box. If the two boxes do not intersect,
* return a BoundingBox with zero area.
*
* @param b The box to intersect this box with. Non-null.
* @return A (non-null) BoundingBox representing the area common to this and <code>b</code>.
*/
// Note: This behavior is only sensible for "open" boxes; otherwise, it implies that
// non-intersecting boxes actually intersect at a point. It would be better to have a special
// value for representing empty boxes.
BoundingBox intersect(BoundingBox b);
/**
* Compute the result of scaling the linear dimensions of this box by the specified factor while
* preserving the box's centroid.
*
* @param s Factor by which box's linear dimensions should be scaled. Finite and non-negative.
* @return A (non-null) BoundingBox representing the result of the scaling.
*/
// BUG: How is overflow handled?
BoundingBox scale(double s);
/**
* Compute the result of shifting this box by a specified displacement.
*
* @param dx Coordinate distance to shift box by in x-direction. Finite.
* @param dy Coordinate distance to shift box by in y-direction. Finite.
* @return A (non-null) BoundingBox representing the result of the shift.
*/
// BUG: How is overflow handled?
BoundingBox shifted(double dx, double dy);
}
Here is an example of an implementation of this interface using a two-point representation:
/**
* A BoundingBox represented by the locations of its lower-left and upper-right corners.
*/
public class TwoPointBoundingBox implements BoundingBox {
/**
* Location of the lower-left corner of this box (point with minimum x-coordinate and minimum
* y-coordinate). Non-null.
*/
private final Point lower;
/**
* Location of the upper-right corner of this box (point with maximum x-coordinate and maximum
* y-coordinate). Non-null. Invariant: upper.x >= lower.x AND upper.y >= lower.y.
*/
private final Point upper;
/**
* Determine whether the class invariant is satisfied.
*
* @return True iff the class invariant is satisfied.
*/
private boolean checkInvariant() {
return (lower.x() <= upper.x()) &&
(lower.y() <= upper.y());
}
/**
* Construct a new BoundingBox with the specified lower-left and upper-right corner locations.
*
* @param lower Location of the lower-left corner. Non-null.
* @param upper Location of the upper-right corner. Non-null, and upper.x >= lower.x and
* upper.y >= lower.y.
*/
public TwoPointBoundingBox(Point lower, Point upper) {
assert lower.x() <= upper.x();
assert lower.y() <= upper.y();
this.lower = lower;
this.upper = upper;
assert checkInvariant();
}
@Override
public double width() {
// Because all fields are final and immutable and invariants are checked at the end of all
// constructors, these checks are redundant. We've written them anyway to illustrate good
// habits.
assert checkInvariant();
// Box width is equal to the horizontal (x) distance between the lower and upper points.
return upper.x() - lower.x();
}
@Override
public double height() {
assert checkInvariant();
// Box height is equal to the vertical (y) distance between the lower and upper points.
return upper.y() - lower.y();
}
@Override
public Point centroid() {
assert checkInvariant();
// The centroid is the average of the lower and upper points.
return new Point(0.5 * (upper.x() + lower.x()),
0.5 * (upper.y() + lower.y()));
}
@Override
public Point[] corners() {
assert checkInvariant();
// Allocate return value (arrays are mutable, so memoization is not an option).
Point[] ans = new Point[4];
// Lower-left
ans[0] = lower;
// Lower-right
ans[1] = lower.shifted(width(), 0);
// Upper-right
ans[2] = upper;
// Upper-left
ans[3] = lower.shifted(0, height());
return ans;
}
@Override
public double area() {
assert checkInvariant();
// The area of a rectangle is the product of its width and height.
return width() * height();
}
@Override
public boolean contains(Point p) {
assert checkInvariant();
// Check if `p`'s coordinates are strictly between our lower and upper coordinates.
return (p.x() > lower.x()) && (p.x() < upper.x()) &&
(p.y() > lower.y()) && (p.y() < upper.y());
}
@Override
public boolean contains(BoundingBox b) {
assert checkInvariant();
// `b` is contained iff all of its corners are contained.
Point[] corners = b.corners();
for (int i = 0; i < corners.length; ++i) {
if (!contains(corners[i])) {
return false;
}
}
return true;
}
@Override
public BoundingBox intersect(BoundingBox b) {
assert checkInvariant();
// Determine lower and upper points of `b`. Must extract from `corners` (wasteful) because
// `b` may not be a TwoPointBoundingBox.
Point[] bCorners = b.corners();
Point bLower = bCorners[0];
Point bUpper = bCorners[2];
// Compute smallest upper and largest lower coordinate in both dimensions to get
// intersection bounds.
double xMax = Math.min(upper.x(), bUpper.x());
double xMin = Math.max(lower.x(), bLower.x());
double yMax = Math.min(upper.y(), bUpper.y());
double yMin = Math.max(lower.y(), bLower.y());
// Check if intersection bounds are inverted.
if ((xMin > xMax) || (yMin > yMax)) {
// Intersection is empty set; return zero-area box.
return new TwoPointBoundingBox(new Point(), new Point());
}
return new TwoPointBoundingBox(new Point(xMin, yMin), new Point(xMax, yMax));
}
@Override
public BoundingBox scale(double s) {
assert s >= 0.0 && Double.isFinite(s);
assert checkInvariant();
// Compute difference between new and old dimensions.
double dw = (s - 1.0) * width();
double dh = (s - 1.0) * height();
// Shift corners by half the change in dimensions (in opposite directions).
return new TwoPointBoundingBox(lower.shifted(-0.5 * dw, -0.5 * dh),
upper.shifted(0.5 * dw, 0.5 * dh));
}
@Override
public BoundingBox shifted(double dx, double dy) {
assert Double.isFinite(dx);
assert Double.isFinite(dy);
assert checkInvariant();
// Shift both corners by the specified displacement.
return new TwoPointBoundingBox(lower.shifted(dx, dy),
upper.shifted(dx, dy));
}
@Override
public String toString() {
return "TwoPointBoundingBox(" + lower + " -> " + upper + ")";
}
@Override
public boolean equals(Object obj) {
assert checkInvariant();
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
TwoPointBoundingBox b = (TwoPointBoundingBox) obj;
return lower.equals(b.lower) && upper.equals(b.upper);
}
@Override
public int hashCode() {
return java.util.Objects.hash(lower, upper);
}
}
And here is an example of an implementation of the interface using a center & extents implementation:
/**
* A BoundingBox represented by the location of its centroid and its horizontal and vertical
* extents.
*/
public class CenterExtentsBoundingBox implements BoundingBox {
/**
* Location of box's centroid. Non-null.
*/
private final Point center;
/**
* Width of box (in coordinate system units). Finite and non-negative.
*/
private final double width;
/**
* Height of box (in coordinate system units). Finite and non-negative.
*/
private final double height;
/**
* Determine whether the class invariant is satisfied.
*
* @return True iff the class invariant is satisfied.
*/
private boolean checkInvariant() {
return center != null &&
width >= 0.0 && Double.isFinite(width) &&
height >= 0.0 && Double.isFinite(height);
}
public CenterExtentsBoundingBox(Point center, double width, double height) {
assert center != null;
assert width >= 0.0 && Double.isFinite(width);
assert height >= 0.0 && Double.isFinite(height);
this.center = center;
this.width = width;
this.height = height;
assert checkInvariant();
}
@Override
public double width() {
// Because all fields are final and immutable and invariants are checked at the end of all
// constructors, these checks are redundant. We've written them anyway to illustrate good
// habits.
assert checkInvariant();
return width;
}
@Override
public double height() {
assert checkInvariant();
return height;
}
@Override
public Point centroid() {
assert checkInvariant();
return center;
}
@Override
public Point[] corners() {
assert checkInvariant();
// Allocate return value (arrays are mutable, so memoization is not an option).
Point[] ans = new Point[4];
// Lower-left
ans[0] = center.shifted(-width / 2, -height / 2);
// Lower-right
ans[1] = center.shifted(width / 2, -height / 2);
// Upper-right
ans[2] = center.shifted(width / 2, height / 2);
// Upper-left
ans[3] = center.shifted(-width / 2, height / 2);
return ans;
}
@Override
public double area() {
assert checkInvariant();
// The area of a rectangle is the product of its width and height.
return width() * height();
}
@Override
public boolean contains(Point p) {
assert checkInvariant();
// Check whether point is within half-extents of center.
return (Math.abs(p.x() - center.x()) < width / 2) &&
(Math.abs(p.y() - center.y()) < height / 2);
}
@Override
public boolean contains(BoundingBox b) {
assert checkInvariant();
// `b` is contained iff all of its corners are contained.
Point[] corners = b.corners();
for (int i = 0; i < corners.length; ++i) {
if (!contains(corners[i])) {
return false;
}
}
return true;
}
@Override
public BoundingBox intersect(BoundingBox b) {
assert checkInvariant();
// Compute locations of our lower-left and upper-right corners.
Point lower = center.shifted(-width / 2, -height / 2);
Point upper = center.shifted(width / 2, height / 2);
// Determine lower and upper points of `b`. Must request all corners (wasteful) because we
// don't know the representation of `b`.
Point[] bCorners = b.corners();
Point bLower = bCorners[0];
Point bUpper = bCorners[2];
// Compute smallest upper and largest lower coordinate in both dimensions to get
// intersection bounds.
double xMax = Math.min(upper.x(), bUpper.x());
double xMin = Math.max(lower.x(), bLower.x());
double yMax = Math.min(upper.y(), bUpper.y());
double yMin = Math.max(lower.y(), bLower.y());
// Check if intersection bounds are inverted.
if ((xMin > xMax) || (yMin > yMax)) {
// Intersection is empty set; return zero-area box.
return new CenterExtentsBoundingBox(new Point(), 0.0, 0.0);
}
// Convert to center+extents representation.
return new CenterExtentsBoundingBox(new Point(0.5 * (xMin + xMax), 0.5 * (yMin + yMax)),
xMax - xMin, yMax - yMin);
}
@Override
public BoundingBox scale(double s) {
assert s >= 0.0 && Double.isFinite(s);
assert checkInvariant();
// Scale extents;
return new CenterExtentsBoundingBox(center, s * width, s * height);
}
@Override
public BoundingBox shifted(double dx, double dy) {
assert Double.isFinite(dx);
assert Double.isFinite(dy);
assert checkInvariant();
// Shift center.
return new CenterExtentsBoundingBox(center.shifted(dx, dy), width, height);
}
@Override
public String toString() {
return "CenterExtentsBoundingBox(" + center + ", " + width + "x" + height + ")";
}
@Override
public boolean equals(Object obj) {
assert checkInvariant();
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
CenterExtentsBoundingBox b = (CenterExtentsBoundingBox) obj;
return center.equals(b.center) && (width == b.width) && (height == b.height);
}
@Override
public int hashCode() {
return java.util.Objects.hash(center, width, height);
}
}
Finally, client code to use these interfaces might look something like this:
public class Main {
public static void main(String[] args) {
BoundingBox bb;
if ("".equals(args[0])) {
bb = new TwoPointBoundingBox(new Point(3, 1), new Point(4, 5));
} else if ("".equals(args[0])) {
bb = new CenterExtentsBoundingBox(new Point(3.5, 3), 1, 4);
} else {
System.exit(1);
}
// Shift bounding box to origin
Point c = bb.centroid();
BoundingBox bc = bb.shifted(-c.x(), -c.y());
System.out.println(bc.centroid());
// Compute area of intersection
System.out.println(areaOfIntersection(bb, bc));
}
public static double areaOfIntersection(BoundingBox b1, BoundingBox b2) {
return b1.intersect(b2).area();
}
}