Bounds.java
package swingtree.layout;
import com.google.errorprone.annotations.Immutable;
import java.awt.Rectangle;
import java.util.Objects;
/**
* An immutable value object that represents the position and size of a component
* in the form of an x and y coordinate modeled by a {@link Position} object
* and a width and height modeled by a {@link Size} object.
* Note the rectangular bounds object is positioned in a coordinate system
* where the y-axis is growing positively downwards and the x-axis is growing
* positively to the right. <br>
* The bounds object may also be incomplete in the sense that the width and height
* may not be defined, in which case the {@link Size#unknown()} object is used.
* A bounds object with an unknown size located at the origin is considered
* the null object for this class and can be accessed using the {@link #none()} method.
* You may use this object instead of {@code null} to represent a missing bounds object.
* <p>
* Also note that the {@link #equals(Object)} and {@link #hashCode()} methods
* are implemented to compare the {@link Position} and {@link Size} objects
* for value based equality instead of reference based equality.
*/
@Immutable
public final class Bounds
{
private final static Bounds EMPTY = new Bounds(Position.origin(), Size.unknown());
/**
* Returns an empty bounds object, which is the null object for this class.
* <p>
* The returned bounds object has a location of {@link Position#origin()}
* and a size of {@link Size#unknown()}.
*
* @return an empty bounds object that is the null object for this class.
*/
public static Bounds none() {
return EMPTY;
}
private final Position _position;
private final Size _size;
/**
* Returns a bounds object with the specified location and size.
* <p>
* If the location is {@link Position#origin()} and the size is
* {@link Size#unknown()} then the {@link #none()} object is returned.
*
* @param position the location of the bounds object.
* @param size the size of the bounds object.
* @return a bounds object with the specified location and size.
*/
public static Bounds of(Position position, Size size ) {
Objects.requireNonNull(position);
Objects.requireNonNull(size);
if ( position.equals(Position.origin()) && size.equals(Size.unknown()) )
return EMPTY;
return new Bounds(position, size);
}
/**
* Returns a bounds object with the specified location and size
* in the form of x and y coordinates, width and height.
* <p>
* If the width or height is less than zero then the {@link #none()}
* object is returned.
*
* @param x the x coordinate of the location of the bounds object.
* @param y the y coordinate of the location of the bounds object.
* @param width the width of the bounds object.
* @param height the height of the bounds object.
* @return a bounds object with the specified location and size
* in the form of x and y coordinates, width and height.
*/
public static Bounds of( int x, int y, int width, int height ) {
if ( width < 0 || height < 0 )
return EMPTY;
return new Bounds(Position.of(x, y), Size.of(width, height));
}
/**
* Returns a bounds object with the specified location and size
* in the form of x and y coordinates, width and height.
* <p>
* If the width or height is less than zero then the {@link #none()}
* object is returned.
*
* @param x the x coordinate of the location of the bounds object.
* @param y the y coordinate of the location of the bounds object.
* @param width the width of the bounds object.
* @param height the height of the bounds object.
* @return a bounds object with the specified location and size
* in the form of x and y coordinates, width and height.
*/
public static Bounds of( float x, float y, float width, float height ) {
if ( width < 0 || height < 0 )
return EMPTY;
return new Bounds(Position.of(x, y), Size.of(width, height));
}
/**
* Creates a bounds object from an AWT {@link Rectangle} object.
* <p>
* If the width or height is less than zero then the {@link #none()}
* object is returned.
*
* @param rectangle an AWT rectangle object to create a bounds object from.
* @return a bounds object with the location and size of the AWT rectangle object.
*/
public static Bounds of( java.awt.Rectangle rectangle ) {
return of(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
}
private Bounds(Position position, Size size ) {
_position = Objects.requireNonNull(position);
_size = Objects.requireNonNull(size);
}
/**
* If you think of the bounds object as a rectangle, then the
* {@link Position} defines the top left corner and the {@link Size}
* defines the width and height of the rectangle.
* Note that the y-axis is growing positively downwards and the x-axis
* is growing positively to the right.
*
* @return The {@link Position} of this bounds object,
* which contains the x and y coordinates.
*/
public Position location() {
return _position;
}
/**
* Allows you to check weather the bounds object has a width
* that is greater than zero.
*
* @return The truth value of whether this bounds object has a width,
* which is true if the width is greater than zero.
*/
public boolean hasWidth() { return _size._width > 0; }
/**
* Allows you to check weather the bounds object has a height
* that is greater than zero.
*
* @return The truth value of whether this bounds object has a height,
* which is true if the height is greater than zero.
*/
public boolean hasHeight() {
return _size._height > 0;
}
/**
* The {@link Size} of define the width and height of the bounds
* starting from the x and y coordinates of the {@link Position}.
* Note that the {@link Position} is always the top left corner
* of the bounds object where the y-axis is growing positively downwards
* and the x-axis is growing positively to the right.
*
* @return The {@link Size} of this bounds object,
* which contains the width and height.
*/
public Size size() {
return _size;
}
/**
* Allows you to create an updated version of this bounds object with the
* specified x-coordinate and the same y-coordinate and size as this bounds instance.
* See also {@link #withY(int)}, {@link #withWidth(int)}, and {@link #withHeight(int)}.
*
* @param x A new x coordinate for the location of this bounds object.
* @return A new bounds object with a new location that has the specified x coordinate.
*/
public Bounds withX( int x ) {
return new Bounds(_position.withX(x), _size);
}
/**
* Allows you to create an updated version of this bounds object with the
* specified y-coordinate and the same x-coordinate and size as this bounds instance.
* See also {@link #withX(int)}, {@link #withWidth(int)}, and {@link #withHeight(int)}.
*
* @param y A new y coordinate for the location of this bounds object.
* @return A new bounds object with a new location that has the specified y coordinate.
*/
public Bounds withY( int y ) {
return new Bounds(_position.withY(y), _size);
}
/**
* Allows you to create an updated version of this bounds object with the
* specified width and the same x and y coordinates as well as height as this bounds instance.
* See also {@link #withX(int)}, {@link #withY(int)}, and {@link #withHeight(int)}, and {@link #withSize(int, int)}.
*
* @param width A new width for the size of this bounds object.
* @return A new bounds object with a new size that has the specified width.
*/
public Bounds withWidth( int width ) {
return new Bounds(_position, _size.withWidth(width));
}
/**
* Allows you to create an updated version of this bounds object with the
* specified height and the same x and y coordinates as well as width as this bounds instance.
* See also {@link #withX(int)}, {@link #withY(int)}, and {@link #withWidth(int)}, and {@link #withSize(int, int)}.
*
* @param height A new height for the size of this bounds object.
* @return A new bounds object with a new size that has the specified height.
*/
public Bounds withHeight( int height ) {
return new Bounds(_position, _size.withHeight(height));
}
/**
* Allows you to create an updated version of this bounds object with the
* specified width and height and the same x and y coordinates as this bounds instance.
* See also {@link #withX(int)}, {@link #withY(int)}, {@link #withWidth(int)}, and {@link #withHeight(int)}.
*
* @param width A new width for the size of this bounds object.
* @param height A new height for the size of this bounds object.
* @return A new bounds object with a new size that has the specified width and height.
*/
public Bounds withSize( int width, int height ) {
return new Bounds(_position, Size.of(width, height));
}
/**
* Creates a new bounds object which tightly fits around this bounds object
* and the specified bounds object, effectively merging the two bounds objects.
* This is done by finding the minimum x and y coordinates and
* the maximum width and height of the two bounds objects.
*
* @param other The bounds object to merge with this bounds object.
* @return A new bounds object that tightly fits around this bounds object and the specified bounds object.
*/
public Bounds merge( Bounds other ) {
Objects.requireNonNull(other);
if ( this.equals(other) )
return this;
final float thisLeft = _position.x();
final float thisTop = _position.y();
final float thisRight = thisLeft + _size._width;
final float thisBottom = thisTop + _size._height;
final float otherLeft = other._position.x();
final float otherTop = other._position.y();
final float otherRight = otherLeft + other._size._width;
final float otherBottom = otherTop + other._size._height;
final float left = Math.min( thisLeft, otherLeft );
final float top = Math.min( thisTop, otherTop );
final float right = Math.max( thisRight, otherRight );
final float bottom = Math.max( thisBottom, otherBottom );
return Bounds.of(left, top, right - left, bottom - top);
}
/**
* Allows you to check weather the bounds object has the specified
* width and height, which is true if the width and height are equal
* to the width and height of the {@link Size} of this bounds object
* (see {@link #size()}).
*
* @param width A new width for the size of this bounds object.
* @param height A new height for the size of this bounds object.
* @return A new bounds object with a new size that has the specified width and height.
*/
public boolean hasSize( int width, int height ) {
return _size._width == width && _size._height == height;
}
/**
* Allows you to check weather the bounds object has the specified
* width, which is true if the width is equal to the width of the
* {@link Size} of this bounds object (see {@link #size()}).
*
* @param width An integer value to compare to the width of this bounds object.
* @return The truth value of whether the specified width is equal to the width of this bounds object.
*/
public boolean hasWidth( int width ) {
return _size._width == width;
}
/**
* Allows you to check weather the bounds object has the specified
* height, which is true if the height is equal to the height of the
* {@link Size} of this bounds object (see {@link #size()}).
*
* @param height An integer value to compare to the height of this bounds object.
* @return The truth value of whether the specified height is equal to the height of this bounds object.
*/
public boolean hasHeight( int height ) {
return _size._height == height;
}
/**
* The bounds object has a location and size which form a rectangular area
* exposed by this method as a float value defined by the width multiplied by the height.
*
* @return The area of this bounds object, which is the width multiplied by the height.
*/
public float area() {
return _size._width * _size._height;
}
/**
* The bounds object has a location and size which form a rectangular area
* which can easily be converted to a {@link Rectangle} object using this method.
*
* @return A {@link Rectangle} object with the same location and size as this bounds object.
*/
public Rectangle toRectangle() {
return new Rectangle((int) _position.x(), (int) _position.y(), (int) _size._width, (int) _size._height);
}
@Override
public String toString() {
return this.getClass().getSimpleName()+"[" +
"location=" + _position + ", "+
"size=" + _size +
"]";
}
/**
* A convent method to check if the specified x and y coordinates and width and height
* are equal to the location and size of this bounds object.
* This is equivalent to calling {@link #equals(Object)} with
* a new bounds object created with the specified x and y coordinates and width and height
* like so: {@code equals(Bounds.of(x, y, width, height))}.
*
* @param x An integer value to compare to the x coordinate of the location of this bounds object.
* @param y An integer value to compare to the y coordinate of the location of this bounds object.
* @param width An integer value to compare to the width of this bounds object.
* @param height An integer value to compare to the height of this bounds object.
* @return The truth value of whether the specified x and y coordinates and width and height
* are equal to the location and size of this bounds object.
*/
public boolean equals( int x, int y, int width, int height ) {
return _position.x() == x && _position.y() == y && _size._width == width && _size._height == height;
}
@Override
public boolean equals( Object o ) {
if ( o == this ) return true;
if ( o == null ) return false;
if ( o.getClass() != this.getClass() ) return false;
Bounds that = (Bounds)o;
return Objects.equals(this._position, that._position) &&
Objects.equals(this._size, that._size);
}
@Override
public int hashCode() {
return Objects.hash(_position, _size);
}
}