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 Location} 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.
 *  <p>
 *  Also note that the {@link #equals(Object)} and {@link #hashCode()} methods
 *  are implemented to compare the {@link Location} 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(Location.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 Location#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 Location _location;
    private final Size     _size;

    /**
     *  Returns a bounds object with the specified location and size.
     *  <p>
     *  If the location is {@link Location#origin()} and the size is
     *  {@link Size#unknown()} then the {@link #none()} object is returned.
     *
     *  @param location 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( Location location, Size size ) {
        Objects.requireNonNull(location);
        Objects.requireNonNull(size);
        if ( location.equals(Location.origin()) && size.equals(Size.unknown()) )
            return EMPTY;

        return new Bounds(location, 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(Location.of(x, y), Size.of(width, height));
    }

    public static Bounds of( float x, float y, float width, float height ) {
        if ( width < 0 || height < 0 )
            return EMPTY;

        return new Bounds(Location.of(x, y), Size.of(width, height));
    }

    private Bounds( Location location, Size size ) {
        _location = Objects.requireNonNull(location);
        _size     = Objects.requireNonNull(size);
    }

    /**
     *  If you think of the bounds object as a rectangle, then the
     *  {@link Location} 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 Location} of this bounds object,
     *         which contains the x and y coordinates.
     */
    public Location location() {
        return _location;
    }

    /**
     *  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 Location}.
     *  Note that the {@link Location} 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(_location.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(_location.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)}.
     *
     * @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(_location, _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)}.
     *
     * @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(_location, _size.withHeight(height));
    }

    /**
     *  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) _location.x(), (int) _location.y(), (int) _size._width, (int) _size._height);
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName()+"[" +
                    "location=" + _location + ", "+
                    "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 _location.x() == x && _location.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._location, that._location) &&
               Objects.equals(this._size,     that._size);
    }

    @Override
    public int hashCode() {
        return Objects.hash(_location, _size);
    }
}