/*
* Open Source Physics software is free software as described near the bottom of this code file.
*
* For additional information and documentation on Open Source Physics please see:
* <http://www.opensourcephysics.org/>
*/
package org.opensourcephysics.display;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
public class BoundedShape extends InteractiveShape implements Selectable {
static int CENTER = 0;
static int BOTTOM = 1;
static int LEFT = 2;
static int TOP = 3;
static int RIGHT = 4;
static int CORNER = 5;
static int NONE = 6;
int hotspot = NONE;
int delta = 3;
int deltaSqr = delta*delta;
int d2 = 2*delta+1;
boolean selected = false;
boolean hideBounds = false;
Color boundsColor = new Color(128, 128, 255);
boolean widthDrag = false;
boolean heightDrag = false;
boolean xyDrag = true;
boolean rotateDrag = false;
Shape pixelBounds = new Rectangle2D.Double(0, 0, 0, 0); // bounding rectangle in pixel coordinates.
Point2D[] hotSpots = new Point2D[6];
XYDelegate xyDelegate = new XYDelegate();
/**
* Constructs a BoundedShape object for the given shape.
*
* @param s Shape
* @param x double
* @param y double
*/
public BoundedShape(Shape s, double x, double y) {
super(s, x, y);
for(int i = 0, n = hotSpots.length; i<n; i++) {
hotSpots[i] = new Point2D.Float(0, 0);
}
}
/**
* Creates a bounded rectangle.
* @param x
* @param y
* @param w
* @param h
* @return the interactive rectangle
*/
public static BoundedShape createBoundedRectangle(double x, double y, double w, double h) {
Shape shape = new Rectangle2D.Double(-w/2, -h/2, w, h);
return new BoundedShape(shape, x, y);
}
/**
* Creates a bounded rectangle.
* @param x
* @param y
* @param b base
* @param h height
* @return the rectangle
*/
public static BoundedShape createBoundedTriangle(double x, double y, double b, double h) {
GeneralPath path = new GeneralPath();
path.moveTo((float) (-b/2), (float) (-h/2));
path.lineTo((float) (+b/2), (float) (-h/2));
path.lineTo(0, (float) (h/2));
path.closePath();
Shape shape = path;
return new BoundedShape(shape, x, y);
}
/**
* Creates a bounded arrow.
* @param x
* @param y
* @param w base
* @param h height
* @return the arrow
*/
public static BoundedShape createBoundedArrow(double x, double y, double w, double h) {
InteractiveArrow ia = new InteractiveArrow(x, y, w, h);
ia.hideBounds = false;
return ia;
}
/**
* Creates a bounded arrow.
* @param x
* @param y
* @param w base
* @param h height
* @return the arrow
*/
public static BoundedShape createBoundedCenteredArrow(double x, double y, double w, double h) {
InteractiveCenteredArrow ica = new InteractiveCenteredArrow(x, y, w, h);
ica.hideBounds = false;
return ica;
}
/**
* Creates a bounded image.
* @param x
* @param y
* @param image
* @return the rectangle
*/
public static BoundedShape createBoundedImage(Image image, double x, double y) {
return new BoundedImage(image, x, y);
}
/**
* Creates a bounded ellipse.
*
* @param x
* @param y
* @param w
* @param h
* @return BoundedShape
*/
public static BoundedShape createBoundedEllipse(double x, double y, double w, double h) {
Shape shape = new Ellipse2D.Double(-w/2, -h/2, w, h);
return new BoundedShape(shape, x, y);
}
/**
* Creates a bounded circle.
*
* @param x
* @param y
* @param d the diameter
* @return the circle
*/
public static BoundedShape createBoundedCircle(double x, double y, double d) {
Shape shape = new Ellipse2D.Double(-d/2, -d/2, d, d);
return new BoundedShape(shape, x, y);
}
public void setSelected(boolean selected) {
this.selected = selected;
}
public boolean isSelected() {
return selected;
}
/**
* Sets the xy drag option.
*
* @param enable boolean
*/
public void setXYDrag(boolean enable) {
xyDrag = enable;
}
/**
* Gets the xy drag boolean.
*
* @return boolean true if center can be dragged
*/
public boolean isXYDrag() {
return xyDrag;
}
/**
* Sets the rotate drag option.
*
* @param enable boolean
*/
public void setRotateDrag(boolean enable) {
rotateDrag = enable;
}
/**
* Gets the rotate drag option.
* @return boolean
*/
public boolean isRotateDrag() {
return rotateDrag;
}
/**
* Sets the width drag option.
* @param enable boolean
*/
public void setWidthDrag(boolean enable) {
widthDrag = enable;
}
/**
* Gets the width width drag option.
*
* @return boolean true if center can be dragged
*/
public boolean isWidthDrag() {
return widthDrag;
}
/**
* Sets the height drag option.
* @param enable boolean
*/
public void setHeightDrag(boolean enable) {
heightDrag = enable;
}
/**
* Gets the height drag option.
*
* @return boolean true if center can be dragged
*/
public boolean isHeightDrag() {
return heightDrag;
}
public java.awt.Cursor getPreferredCursor() {
if(xyDrag&&(hotspot==CENTER)) {
return java.awt.Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
} else if(rotateDrag&&(hotspot==CORNER)) { // need better cursors!
return java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
} else if(widthDrag&&(hotspot==LEFT)) {
return(theta==0) ? java.awt.Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR) : java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
} else if(widthDrag&&(hotspot==RIGHT)) {
return(theta==0) ? java.awt.Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR) : java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
} else if(heightDrag&&(hotspot==TOP)) {
return(theta==0) ? java.awt.Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR) : java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
} else if(heightDrag&&(hotspot==BOTTOM)) {
return(theta==0) ? java.awt.Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR) : java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
} else if(selected) {
return java.awt.Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
} else {
return java.awt.Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
}
}
public void toggleSelected() {
selected = !selected;
}
public Interactive findInteractive(DrawingPanel panel, int xpix, int ypix) {
if(isInside(panel, xpix, ypix)) {
return xyDelegate;
}
return null;
}
/**
* Determines if the shape is enabled and if the given pixel coordinates are within the shape.
*
* @param panel DrawingPanel
* @param xpix int
* @param ypix int
* @return boolean
*/
public boolean isInside(DrawingPanel panel, int xpix, int ypix) {
hotspot = NONE;
if(!enabled) {
return false;
}
if(pixelBounds.contains(xpix, ypix)&&!selected) {
return true;
}
if(selected) {
hotspot = getHotSpotIndex(xpix, ypix, hotSpots);
return true;
}
return false;
}
int getHotSpotIndex(int xpix, int ypix, Point2D[] hotSpots) {
for(int i = 0, n = hotSpots.length; i<n; i++) {
double dx = xpix-hotSpots[i].getX();
double dy = ypix-hotSpots[i].getY();
if(dx*dx+dy*dy<=deltaSqr) {
return i;
}
}
return NONE;
}
void computeScaledHotSpots(Rectangle2D rect, double ar) {
double sin = Math.sin(theta);
double cos = Math.cos(theta);
double centerX = rect.getCenterX()-xoff*toPixels.getScaleX();
double centerY = rect.getCenterY()-yoff*toPixels.getScaleY();
double right = rect.getWidth()/2+xoff*toPixels.getScaleX();
double left = rect.getWidth()/2-xoff*toPixels.getScaleX();
double bottom = rect.getHeight()/2+yoff*toPixels.getScaleY();
double top = rect.getHeight()/2-yoff*toPixels.getScaleY();
hotSpots[CENTER].setLocation(centerX, centerY); // center
hotSpots[BOTTOM].setLocation(centerX+xoff*toPixels.getScaleX()*cos+bottom*sin/ar, centerY-xoff*toPixels.getScaleX()*sin+bottom*cos); // bottom
hotSpots[LEFT].setLocation(centerX-left*cos+yoff*toPixels.getScaleY()*sin, centerY+ar*left*sin+yoff*toPixels.getScaleY()*cos); // left
hotSpots[TOP].setLocation(centerX+xoff*toPixels.getScaleX()*cos-top*sin/ar, centerY-xoff*toPixels.getScaleX()*sin-top*cos); // top
hotSpots[RIGHT].setLocation(centerX+right*cos+yoff*toPixels.getScaleY()*sin, centerY-ar*right*sin+yoff*toPixels.getScaleY()*cos); // right
hotSpots[CORNER].setLocation(centerX+right*cos-top*sin/ar, centerY-right*sin*ar-top*cos); // corner
}
void computeFixedHotSpots(Rectangle2D rect) {
double sin = Math.sin(theta);
double cos = Math.cos(theta);
double cx = rect.getCenterX()-xoff; // center x
double cy = rect.getCenterY()+yoff; // center y
double right = rect.getWidth()/2+xoff;
double left = xoff-rect.getWidth()/2;
double bottom = yoff-rect.getHeight()/2;
double top = rect.getHeight()/2+yoff;
hotSpots[0].setLocation(cx, cy); // center
hotSpots[1].setLocation(cx+xoff*cos-bottom*sin, cy-bottom*cos-xoff*sin); // bottom
hotSpots[2].setLocation(cx+left*cos-yoff*sin, cy-left*sin-yoff*cos); // left
hotSpots[3].setLocation(cx+xoff*cos-top*sin, cy-top*cos-xoff*sin); // top
hotSpots[4].setLocation(cx+right*cos-yoff*sin, cy-right*sin-yoff*cos); // right
hotSpots[5].setLocation(cx+right*cos-top*sin, cy-right*sin-top*cos); // corner
}
/**
* Sets the x and y coordinates using hotspots.
*
* @param y
*/
void setHotSpotXY(double x, double y) {
if(hideBounds) {
setXY(x, y);
return;
}
if(xyDrag&&selected&&(hotspot==CENTER)) {
setXY(x, y);
} else if(rotateDrag&&selected&&(hotspot==CORNER)) {
if(pixelSized) {
double r = -toPixels.getScaleY()/toPixels.getScaleX();
double dx = x-this.x;
double dy = y-this.y;
theta = Math.atan2(r*dy, dx)-Math.atan2(height/2+yoff, (width/2+xoff));
} else {
double dx = x-this.x;
double dy = y-this.y;
double theta1 = Math.atan2(height/2+yoff, width/2+xoff);
double theta2 = Math.atan2(dy, dx);
setTheta(theta2-theta1);
}
} else if(widthDrag&&selected&&((hotspot==LEFT)||(hotspot==RIGHT))) {
if(pixelSized) {
double dx = toPixels.getScaleX()*(x-this.x)-xoff;
double dy = toPixels.getScaleY()*(y-this.y)+yoff;
BoundedShape.this.setWidth(2*Math.sqrt(dx*dx+dy*dy));
} else {
double dx = (x-this.x-xoff);
double dy = (y-this.y-yoff);
setWidth(2*Math.sqrt(dx*dx+dy*dy));
}
} else if(heightDrag&&selected&&((hotspot==TOP)||(hotspot==BOTTOM))) {
if(pixelSized) {
double dx = toPixels.getScaleX()*(x-this.x)-xoff;
double dy = toPixels.getScaleY()*(y-this.y)+yoff;
BoundedShape.this.setHeight(2*Math.sqrt(dx*dx+dy*dy));
} else {
double dx = (x-this.x-xoff);
double dy = (y-this.y-yoff);
setHeight(2*Math.sqrt(dx*dx+dy*dy));
}
}
}
/**
* Draws the shape.
*
* @param panel the drawing panel
* @param g the graphics context
*/
public void draw(DrawingPanel panel, Graphics g) {
super.draw(panel, g);
if(pixelSized) {
drawFixedBounds(panel, g);
} else {
drawScaledBounds(panel, g);
}
}
/**
* Draws the shape.
*
* @param panel the drawing panel
* @param g the graphics context
*/
private void drawScaledBounds(DrawingPanel panel, Graphics g) {
double r = -toPixels.getScaleY()/toPixels.getScaleX();
if(theta==0) {
Shape temp = toPixels.createTransformedShape(shape.getBounds2D());
computeScaledHotSpots(temp.getBounds2D(), r);
pixelBounds = temp.getBounds2D();
} else {
// rotate the shape into standard position to get correct x-y bounds
Shape temp = AffineTransform.getRotateInstance(-theta, x, y).createTransformedShape(shape);
// the following alternate should also give the correct bounds in world coordinates
// Shape temp = new Rectangle2D.Double(x-width/2, y-height/2, width, height);
temp = toPixels.createTransformedShape(temp);
computeScaledHotSpots(temp.getBounds2D(), r);
pixelBounds = temp.getBounds2D();
if(panel.isSquareAspect()) {
pixelBounds = AffineTransform.getRotateInstance(-theta, ((Rectangle2D) pixelBounds).getCenterX()-xoff*toPixels.getScaleX(), ((Rectangle2D) pixelBounds).getCenterY()-yoff*toPixels.getScaleY()).createTransformedShape(pixelBounds);
} else {
double px = ((Rectangle2D) pixelBounds).getCenterX()-xoff*toPixels.getScaleX();
double py = ((Rectangle2D) pixelBounds).getCenterY()-yoff*toPixels.getScaleY();
pixelBounds = AffineTransform.getTranslateInstance(-px, -py).createTransformedShape(pixelBounds);
pixelBounds = AffineTransform.getScaleInstance(1, 1.0/r).createTransformedShape(pixelBounds);
pixelBounds = AffineTransform.getRotateInstance(-theta).createTransformedShape(pixelBounds);
pixelBounds = AffineTransform.getScaleInstance(1, r).createTransformedShape(pixelBounds);
pixelBounds = AffineTransform.getTranslateInstance(px, py).createTransformedShape(pixelBounds);
}
}
if(!selected||hideBounds) {
return;
}
Graphics2D g2 = ((Graphics2D) g);
g2.setPaint(boundsColor);
g2.draw(pixelBounds);
if(rotateDrag) {
g2.fillOval((int) hotSpots[CORNER].getX()-delta, (int) hotSpots[CORNER].getY()-delta, d2, d2);
}
if(heightDrag) {
g2.fillRect((int) hotSpots[TOP].getX()-delta, (int) hotSpots[TOP].getY()-delta, d2, d2);
g2.fillRect((int) hotSpots[BOTTOM].getX()-delta, (int) hotSpots[BOTTOM].getY()-delta, d2, d2);
}
if(widthDrag) {
g2.fillRect((int) hotSpots[LEFT].getX()-delta, (int) hotSpots[LEFT].getY()-delta, d2, d2);
g2.fillRect((int) hotSpots[RIGHT].getX()-delta, (int) hotSpots[RIGHT].getY()-delta, d2, d2);
}
if(xyDrag) {
g2.fillRect((int) hotSpots[CENTER].getX()-delta, (int) hotSpots[CENTER].getY()-delta, d2, d2);
g2.setColor(edgeColor);
g2.fillOval((int) hotSpots[CENTER].getX()-1, (int) hotSpots[CENTER].getY()-1, 3, 3);
g2.setPaint(boundsColor);
}
g.setColor(Color.BLACK);
}
/**
* Draws the shape.
*
* @param panel the drawing panel
* @param g the graphics context
*/
private void drawFixedBounds(DrawingPanel panel, Graphics g) {
if(theta==0) {
Point2D pt = new Point2D.Double(x, y);
pt = toPixels.transform(pt, pt);
Shape temp = AffineTransform.getTranslateInstance(-x+pt.getX()+xoff, -y+pt.getY()-yoff).createTransformedShape(shape.getBounds2D());
computeFixedHotSpots(temp.getBounds2D());
pixelBounds = temp.getBounds2D();
} else {
// rotate the shape into standard position to get correct x-y bounds
// Shape temp = AffineTransform.getRotateInstance(-theta, x, y).createTransformedShape(shape);
Point2D pt = new Point2D.Double(x, y);
pt = toPixels.transform(pt, pt);
Shape temp = AffineTransform.getTranslateInstance(-x+pt.getX()+xoff, -y+pt.getY()-yoff).createTransformedShape(shape);
// temp = AffineTransform.getTranslateInstance(pt.getX(), pt.getY()).createTransformedShape(temp);
computeFixedHotSpots(temp.getBounds2D());
pixelBounds = temp.getBounds2D();
pixelBounds = AffineTransform.getRotateInstance(-theta, pt.getX(), pt.getY()).createTransformedShape(pixelBounds);
}
if(!selected||hideBounds) {
return;
}
Graphics2D g2 = ((Graphics2D) g);
g2.setPaint(boundsColor);
g2.draw(pixelBounds);
if(rotateDrag) {
g2.fillOval((int) hotSpots[CORNER].getX()-delta, (int) hotSpots[CORNER].getY()-delta, d2, d2);
}
if(heightDrag) {
g2.fillRect((int) hotSpots[TOP].getX()-delta, (int) hotSpots[TOP].getY()-delta, d2, d2);
g2.fillRect((int) hotSpots[BOTTOM].getX()-delta, (int) hotSpots[BOTTOM].getY()-delta, d2, d2);
}
if(widthDrag) {
g2.fillRect((int) hotSpots[LEFT].getX()-delta, (int) hotSpots[LEFT].getY()-delta, d2, d2);
g2.fillRect((int) hotSpots[RIGHT].getX()-delta, (int) hotSpots[RIGHT].getY()-delta, d2, d2);
}
if(xyDrag) {
g2.fillRect((int) hotSpots[CENTER].getX()-delta, (int) hotSpots[CENTER].getY()-delta, d2, d2);
g2.setColor(edgeColor);
g2.fillOval((int) hotSpots[CENTER].getX()-1, (int) hotSpots[CENTER].getY()-1, 3, 3);
g2.setPaint(boundsColor);
}
g.setColor(Color.BLACK);
}
/**
* Gets a description of this object.
* @return String
*/
public String toString() {
return "BoundedShape:"+"\n \t shape="+shapeClass+"\n \t x="+x+"\n \t y="+y+"\n \t width="+width+"\n \t height="+height+"\n \t theta="+theta; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
}
/**
* Gets the XML object loader for this class.
* @return ObjectLoader
*/
public static XML.ObjectLoader getLoader() {
return new BoundedShapeLoader();
}
/**
* A class to save and load BoundedShape in an XMLControl.
*/
protected static class BoundedShapeLoader extends InteractiveShapeLoader {
public void saveObject(XMLControl control, Object obj) {
super.saveObject(control, obj);
BoundedShape boundedShape = (BoundedShape) obj;
control.setValue("xy drag", boundedShape.isXYDrag()); //$NON-NLS-1$
control.setValue("width drag", boundedShape.isWidthDrag()); //$NON-NLS-1$
control.setValue("height drag", boundedShape.isHeightDrag()); //$NON-NLS-1$
control.setValue("rotate drag", boundedShape.isRotateDrag()); //$NON-NLS-1$
}
public Object createObject(XMLControl control) {
return new BoundedShape(new Rectangle2D.Double(0, 0, 0, 0), 0, 0); // default shape is a rectangle for now
}
public Object loadObject(XMLControl control, Object obj) {
BoundedShape boundedShape = (BoundedShape) obj;
boundedShape.setXYDrag(control.getBoolean("xy drag")); //$NON-NLS-1$
boundedShape.setWidthDrag(control.getBoolean("width drag")); //$NON-NLS-1$
boundedShape.setHeightDrag(control.getBoolean("height drag")); //$NON-NLS-1$
boundedShape.setRotateDrag(control.getBoolean("rotate drag")); //$NON-NLS-1$
super.loadObject(control, obj);
return boundedShape;
}
}
class XYDelegate extends AbstractInteractive implements Selectable {
public void draw(DrawingPanel panel, Graphics g) {}
public boolean isInside(DrawingPanel panel, int xpix, int ypix) {
return BoundedShape.this.isInside(panel, xpix, ypix);
}
public void setXY(double x, double y) {
BoundedShape.this.setHotSpotXY(x, y);
}
public void setSelected(boolean selectable) {
BoundedShape.this.setSelected(selectable);
}
public void toggleSelected() {
BoundedShape.this.toggleSelected();
}
public boolean isSelected() {
return BoundedShape.this.isSelected();
}
public Cursor getPreferredCursor() {
return BoundedShape.this.getPreferredCursor();
}
}
}
/*
* Open Source Physics software is free software; you can redistribute
* it and/or modify it under the terms of the GNU General Public License (GPL) as
* published by the Free Software Foundation; either version 2 of the License,
* or(at your option) any later version.
* Code that uses any portion of the code in the org.opensourcephysics package
* or any subpackage (subdirectory) of this package must must also be be released
* under the GNU GPL license.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
* or view the license online at http://www.gnu.org/copyleft/gpl.html
*
* Copyright (c) 2007 The Open Source Physics project
* http://www.opensourcephysics.org
*/