/* * Rectangle.java * * Copyright (C) 2009-12 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.core.client; public class Rectangle { public Rectangle(int x, int y, int width, int height) { super() ; this.x = x ; this.y = y ; this.width = width ; this.height = height ; } // Eclipse auto-generated @Override public int hashCode() { final int prime = 31 ; int result = 1 ; result = prime * result + height ; result = prime * result + width ; result = prime * result + x ; result = prime * result + y ; return result ; } // Eclipse auto-generated @Override public boolean equals(Object obj) { if (this == obj) return true ; if (obj == null) return false ; if (getClass() != obj.getClass()) return false ; Rectangle other = (Rectangle) obj ; if (height != other.height) return false ; if (width != other.width) return false ; if (x != other.x) return false ; if (y != other.y) return false ; return true ; } @Override public String toString() { return "Rectangle(x=" + x + ",y=" + y + ",w=" + width + ",h=" + height + ")"; } public int getLeft() { return x ; } public int getTop() { return y ; } public int getWidth() { return width ; } public int getHeight() { return height ; } public int getRight() { return x + width ; } public int getBottom() { return y + height ; } public Point getLocation() { return new Point(x, y) ; } public Size getSize() { return new Size(width, height) ; } public Point getCorner(boolean left, boolean top) { return new Point(left ? getLeft() : getRight(), top ? getTop() : getBottom()) ; } public Rectangle move(int x, int y) { return new Rectangle(x, y, getWidth(), getHeight()); } /** * Returns the rectangular intersection of the two rectangles, or null if * the rectangles do not touch anywhere. * @param other The rectangle to intersect with this. * @param canReturnEmptyRect If true, in cases where one of the * rectangles has zero width or height OR cases where the rectangles share * an edge, it's possible for a zero-width or zero-height rectangle to be * returned. If false, then it's guaranteed that the return value will * either be null or a rectangle with a positive area. Note that * regardless of true or false, null can always be returned. * @return The intersection, or null if none. */ public Rectangle intersect(Rectangle other, boolean canReturnEmptyRect) { int left = Math.max(x, other.x); int right = Math.min(getRight(), other.getRight()); int top = Math.max(getTop(), other.getTop()); int bottom = Math.min(getBottom(), other.getBottom()); if ((canReturnEmptyRect && left <= right && top <= bottom) || (!canReturnEmptyRect && left < right && top < bottom)) { return new Rectangle(left, top, right-left, bottom-top); } else return null; } /** * Enlarge the rectangle in the given directions by the given amounts. */ public Rectangle inflate(int left, int top, int right, int bottom) { return new Rectangle(x - left, y - top, width + left + right, height + top + bottom); } /** * Enlarge each side of the rectangle by the given amount. Note that e.g. * 10 will result in 20-unit-greater width and height. * @param inflateBy * @return */ public Rectangle inflate(int inflateBy) { return inflate(inflateBy, inflateBy, inflateBy, inflateBy); } /** * Return the center point */ public Point center() { return new Point((getLeft() + getRight()) / 2, (getTop() + getBottom()) / 2); } /** * Create a new rectangle of the given width and height that is centered * relative to this rectangle. */ public Rectangle createCenteredRect(int width, int height) { return new Rectangle( (getWidth() - width) / 2, (getHeight() - height) / 2, width, height); } /** * Returns true if this rectangle ENTIRELY contains the given rectangle. */ public boolean contains(Rectangle other) { return getLeft() <= other.getLeft() && getTop() <= other.getTop() && getRight() >= other.getRight() && getBottom() >= other.getBottom(); } /** * Intelligently figures out where to move this rectangle within a container * to avoid another rectangle (the avoidee). * @param avoidee The rectangle we're trying to avoid * @param container The rectangle we need to try to stay within, if possible * @return The new location for the rectangle */ public Point avoidBounds(final Rectangle avoidee, final Rectangle container) { // Check for nothing to avoid if (avoidee == null) return this.getLocation(); // Check for no collision if (this.intersect(avoidee, false) == null) return this.getLocation(); // Figure out whether the avoidee is in the top or bottom half of the // container. vertDir < 0 means top half, vertDir > 0 means bottom half. int vertDir = avoidee.center().getY() - container.center().getY(); // Create new bounds that are just below or just above the avoidee, // depending on whether the avoidee is in the top or bottom half of // the container, respectively. Rectangle vertShift = this.move( this.getLeft(), vertDir > 0 ? avoidee.getTop() - this.getHeight() : avoidee.getBottom()); // If that resulted in bounds that fit entirely in the container, then // use it. (We prefer vertical shifting to horizontal shifting.) if (container.contains(vertShift)) return vertShift.getLocation(); // Now repeat the algorithm in the horizontal dimension. int horizDir = avoidee.center().getX() - container.center().getX(); Rectangle horizShift = this.move( horizDir > 0 ? avoidee.getLeft() - this.getWidth() : avoidee.getRight(), this.getTop()); if (container.contains(horizShift)) return horizShift.getLocation(); // Both vertical and horizontal options go off the container. Combine // their effects, then move to within the screen, if possible; or if all // else fails, just center. Rectangle hvShift = new Rectangle(horizShift.getLeft(), vertShift.getTop(), this.getWidth(), this.getHeight()); return hvShift.attemptToMoveInto(container, FailureMode.CENTER).getLocation(); } public enum FailureMode { /** * Center the rect's position in this dimension */ CENTER, /** * Don't change the rect's position in this dimension */ NO_CHANGE } /** * Attempt to move this rectangle so that it fits inside the given container. * If this rectangle is taller and/or wider than the container, then the * failureMode parameter can be used to dictate what the fallback behavior * should be. */ public Rectangle attemptToMoveInto(Rectangle container, FailureMode failureMode) { int newX = this.x; int newY = this.y; if (getWidth() <= container.getWidth()) { newX = Math.min(Math.max(newX, container.getLeft()), container.getRight() - getWidth()); } else if (failureMode == FailureMode.CENTER) { newX = container.getLeft() - (getWidth()-container.getWidth())/2; } if (getHeight() <= container.getHeight()) { newY = Math.min(Math.max(newY, container.getTop()), container.getBottom() - getHeight()); } else if (failureMode == FailureMode.CENTER) { newY = container.getTop() - (getHeight()-container.getHeight())/2; } return new Rectangle(newX, newY, getWidth(), getHeight()); } private final int x; private final int y; private final int width; private final int height; }