/* Copyright 2006 by Sean Luke and George Mason University Licensed under the Academic Free License version 3.0 See the file "LICENSE" for more information */ package sim.portrayal.simple; import sim.portrayal.*; import java.awt.*; import sim.display.*; import java.awt.geom.*; import java.awt.event.*; /** A wrapper for other Portrayal2Ds which provides some kind of pointing object (typically a line) along the object's specified orientation angle. This is a very simple way to show orientation. <p>For the line to be drawn, the underlying object must adhere to the Oriented2D interface, which provides the orientation2D() method. The line starts at the origin and is of length: <pre><tt> length: (int)(scale * max(info.draw.width,info.draw.height)) + offset; </tt></pre> <p>... that is, or is a value which scales when you zoom in, and dr adds additional fixed pixels. The default is scale = 1.0, offset = 0, with a red color. Note that though the scale is 1.0, the shape is actually drawn effectively with a scale of 2.0 (that is, filling a 2x2 square rather than a 1x1 square) so as to be seen to some degree outside the underlying child portrayal. <p>You can specify other shapes than a simple line. We provide several others: a line arrow, a kite, compass, triangle, inverted T dagger shape, and arrow. <p><b>Note: </b> One oddity of OrientedPortrayal2D is due to the fact that the line is only drawn if the object is being drawn. While most FieldPortrayals ask objects just off-screen to draw themselves just to be careful, if an object is significantly off-screen, it may not be asked to draw itself, and so the orientation line will not be drawn -- even though part of the orientation line could be on-screen at the time! C'est la vie. */ public class OrientedPortrayal2D extends SimplePortrayal2D { public static final double DEFAULT_SCALE = 1.0; public static final int DEFAULT_OFFSET = 0; public static final int SHAPE_LINE = 0; public static final int SHAPE_LINE_ARROW = 1; public static final int SHAPE_KITE = 2; public static final int SHAPE_COMPASS = 3; public static final int SHAPE_TRIANGLE = 4; public static final int SHAPE_INVERTED_T = 5; public static final int SHAPE_ARROW = 6; /** The type of the oriented shape */ int shape = SHAPE_LINE; public void setShape(int val) { if (val >= SHAPE_LINE && val <= SHAPE_ARROW) shape = val; path = null; } public int getShape() { return shape; } /** The pre-scaling length */ public double scale; /** The post-scaling length offset */ public int offset; /** The Paint or Color of the line */ public Paint paint; public SimplePortrayal2D child; public boolean drawFilled = true; public void setDrawFilled(boolean val) { drawFilled = val; } public boolean isDrawFilled() { return drawFilled; } /** Overrides all drawing. */ boolean showOrientation = true; public boolean isOrientationShowing() { return showOrientation; } public void setOrientationShowing(boolean val) { showOrientation = val; } /** @deprecated use isOrientationShowing() */ public boolean isLineShowing() { return showOrientation; } /** @deprecated use setOrientationShowing() */ public void setLineShowing(boolean val) { showOrientation = val; } Shape path = null; Shape buildPolygon(double[] xpoints, double[] ypoints, int npoints) { GeneralPath path = new GeneralPath(); // general paths are only floats and not doubles in Java 1.4, 1.5 // in 1.6 it's been changed to doubles finally but we're not there yet. if (xpoints.length > 0) path.moveTo((float)xpoints[0], (float)ypoints[0]); for(int i=npoints-1; i >= 0; i--) path.lineTo((float)xpoints[i], (float)ypoints[i]); return path; } boolean onlyDrawWhenSelected = false; public void setOnlyDrawWhenSelected(boolean val) { onlyDrawWhenSelected = val; } public boolean getOnlyDrawWhenSelected() { return onlyDrawWhenSelected; } public OrientedPortrayal2D(SimplePortrayal2D child, int offset, double scale, Paint paint, int shape) { this.offset = offset; this.scale = scale; this.child = child; this.paint = paint; setShape(shape); } /** If child is null, then the underlying model object is presumed to be a Portrayal2D and will be used. */ public OrientedPortrayal2D(SimplePortrayal2D child, int offset, double scale, Paint paint) { this(child,offset,scale,paint,SHAPE_LINE); } /** Draw a line of length scale = 0.5, offset = 0, in red. If child is null, then the underlying model object is presumed to be a Portrayal2D and will be used. */ public OrientedPortrayal2D(SimplePortrayal2D child) { this(child, DEFAULT_OFFSET, DEFAULT_SCALE, Color.red); } /** Draw a line of the given length in red. If child is null, then the underlying model object is presumed to be a Portrayal2D and will be used. */ public OrientedPortrayal2D(SimplePortrayal2D child, int offset, double scale) { this(child, offset, scale, Color.red); } /** Draw a line of length scale = 0.5, offset = 0. If child is null, then the underlying model object is presumed to be a Portrayal2D and will be used. */ public OrientedPortrayal2D(SimplePortrayal2D child, Paint paint) { this(child, DEFAULT_OFFSET, DEFAULT_SCALE, paint); } public SimplePortrayal2D getChild(Object object) { if (child!=null) return child; else { if (!(object instanceof SimplePortrayal2D)) throw new RuntimeException("Object provided to OrientedPortrayal2D is not a SimplePortrayal2D: " + object); return (SimplePortrayal2D) object; } } int[] simplePolygonX = new int[7]; int[] simplePolygonY = new int[7]; double[] simplePolygonXd = new double[7]; double[] simplePolygonYd = new double[7]; double lastLength = Double.NaN; AffineTransform transform = new AffineTransform(); Stroke stroke = new BasicStroke(); /** Returns the orientation of the underlying object, or NaN if there is no such orientation. The default implementation assumes that the object is non-null and is an instance of Oriented2D, and calls orientation2D() on it; else it returns NaN. */ public double getOrientation(Object object, DrawInfo2D info) { if (object != null && object instanceof Oriented2D) return ((Oriented2D)object).orientation2D(); else return Double.NaN; } public void draw(Object object, Graphics2D graphics, DrawInfo2D info) { // draw the underlying object first? if (shape <= SHAPE_LINE_ARROW || !drawFilled) getChild(object).draw(object,graphics,info); if (showOrientation && (info.selected || !onlyDrawWhenSelected)) { double theta = getOrientation(object, info); if (theta == theta) // NaN != NaN { double length = (scale * (info.draw.width < info.draw.height ? info.draw.width : info.draw.height)) + offset; // fit in smallest dimension if (length != lastLength) { lastLength = length; path = null; } // redo shape graphics.setPaint(paint); if (info.precise) // real-valued drawing, slightly slower { transform.setToTranslation(info.draw.x, info.draw.y); transform.rotate(theta); final double lenx = 1.0 * length; // oriented forwards final double leny = 0.0 * length; switch(shape) { default: // NOTE FALL THRU case SHAPE_LINE: if (path == null) { path = new Line2D.Double(0,0,length,0); } graphics.setStroke(stroke); graphics.draw(transform.createTransformedShape(path)); break; case SHAPE_LINE_ARROW: if (path == null) { GeneralPath p = new GeneralPath(); //[-1, 0] p.moveTo((float) -lenx, (float) -leny); // [1, 0] p.lineTo((float) lenx, (float) leny); // [1/2, 1/2] p.moveTo((float) (lenx/2 - leny/2), (float) (leny/2 + lenx/2)); // [1, 0] p.lineTo((float) lenx, (float) leny); // [1/2, -1/2] p.lineTo((float) (lenx/2 + leny/2), (float) (leny/2 - lenx/2)); path = p; } graphics.setStroke(stroke); graphics.draw(transform.createTransformedShape(path)); break; case SHAPE_KITE: if (path == null) { // [1, 0] simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); // [-1, 1] simplePolygonXd[1] = (0 + -leny + -lenx); simplePolygonYd[1] = (0 + lenx + -leny); // [-1/2, 0] simplePolygonXd[2] = (0 + -lenx/2); simplePolygonYd[2] = (0 + -leny/2); // [-1, -1] simplePolygonXd[3] = (0 + leny + -lenx); simplePolygonYd[3] = (0 + -lenx + -leny); path = buildPolygon(simplePolygonXd, simplePolygonYd, 4); } if (drawFilled) graphics.fill(transform.createTransformedShape(path)); else { graphics.setStroke(stroke); graphics.draw(transform.createTransformedShape(path)); } break; case SHAPE_COMPASS: if (path == null) { // [1, 0] simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); // [0, 1/2] simplePolygonXd[1] = (0 + -leny/2); simplePolygonYd[1] = (0 + lenx/2); // [-1/2, 0] simplePolygonXd[2] = (0 + -lenx/2); simplePolygonYd[2] = (0 + -leny/2); // [0, -1/2] simplePolygonXd[3] = (0 + leny/2); simplePolygonYd[3] = (0 + -lenx/2); path = buildPolygon(simplePolygonXd, simplePolygonYd, 4); } if (drawFilled) graphics.fill(transform.createTransformedShape(path)); else { graphics.setStroke(stroke); graphics.draw(transform.createTransformedShape(path)); } break; case SHAPE_TRIANGLE: if (path == null) { // [1, 0] simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); // [-1, 1/2] simplePolygonXd[1] = (0 + -lenx + -leny/2); simplePolygonYd[1] = (0 + -leny + lenx/2); // [-1, -1/2] simplePolygonXd[2] = (0 + -lenx + leny/2); simplePolygonYd[2] = (0 + -leny + -lenx/2); path = buildPolygon(simplePolygonXd, simplePolygonYd, 3); } if (drawFilled) graphics.fill(transform.createTransformedShape(path)); else { graphics.setStroke(stroke); graphics.draw(transform.createTransformedShape(path)); } break; case SHAPE_INVERTED_T: if (path == null) { // [1, 0] simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); // [-1/2, 1/2] simplePolygonXd[1] = (0 + -lenx/2 + -leny/2); simplePolygonYd[1] = (0 + -leny/2 + lenx/2); // [-1, 1] simplePolygonXd[2] = (0 + -lenx + -leny); simplePolygonYd[2] = (0 + -leny + lenx); // [-1, -1] simplePolygonXd[3] = (0 + -lenx + leny); simplePolygonYd[3] = (0 + -leny + -lenx); // [-1/2, -1/2] simplePolygonXd[4] = (0 + -lenx/2 + leny/2); simplePolygonYd[4] = (0 + -leny/2 + -lenx/2); path = buildPolygon(simplePolygonXd, simplePolygonYd, 5); } if (drawFilled) graphics.fill(transform.createTransformedShape(path)); else { graphics.setStroke(stroke); graphics.draw(transform.createTransformedShape(path)); } break; case SHAPE_ARROW: if (path == null) { // [1, 0] simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); // [0, 1] simplePolygonXd[1] = (0 + -leny); simplePolygonYd[1] = (0 + lenx); // [0, 1/2] simplePolygonXd[2] = (0 + -leny/2); simplePolygonYd[2] = (0 + lenx/2); // [-1, 1/2] simplePolygonXd[3] = (0 + -lenx + -leny/2); simplePolygonYd[3] = (0 + -leny + lenx/2); // [-1, -1/2] simplePolygonXd[4] = (0 + -lenx + leny/2); simplePolygonYd[4] = (0 + -leny + -lenx/2); // [0, -1/2] simplePolygonXd[5] = (0 + leny/2); simplePolygonYd[5] = (0 + -lenx/2); // [0, -1] simplePolygonXd[6] = (0 + leny); simplePolygonYd[6] = (0 + -lenx); path = buildPolygon(simplePolygonXd, simplePolygonYd, 7); } if (drawFilled) graphics.fill(transform.createTransformedShape(path)); else { graphics.setStroke(stroke); graphics.draw(transform.createTransformedShape(path)); } break; } } else // integer drawing { final double lenx = Math.cos(theta)*length; final double leny = Math.sin(theta)*length; switch(shape) { default: // NOTE FALL THRU case SHAPE_LINE: graphics.drawLine((int)info.draw.x, (int)info.draw.y, (int)(info.draw.x + lenx), (int)(info.draw.y + leny)); break; case SHAPE_LINE_ARROW: // [1, 0] to [-1, 0] graphics.drawLine((int)(info.draw.x + lenx), (int)(info.draw.y + leny), (int)(info.draw.x - lenx), (int)(info.draw.y - leny)); // [1, 0] to [1/2, 1/2] graphics.drawLine((int)(info.draw.x + lenx), (int)(info.draw.y + leny), (int)(info.draw.x + lenx/2 - leny/2), (int)(info.draw.y + leny/2 + lenx/2)); // [1, 0] to [1/2, -1/2] graphics.drawLine((int)(info.draw.x + lenx), (int)(info.draw.y + leny), (int)(info.draw.x + lenx/2 + leny/2), (int)(info.draw.y + leny/2 - lenx/2)); break; case SHAPE_KITE: simplePolygonX[0] = (int)(info.draw.x + lenx); simplePolygonY[0] = (int)(info.draw.y + leny); simplePolygonX[1] = (int)(info.draw.x + -leny + -lenx); simplePolygonY[1] = (int)(info.draw.y + lenx + -leny); simplePolygonX[2] = (int)(info.draw.x + -lenx/2); simplePolygonY[2] = (int)(info.draw.y + -leny/2); simplePolygonX[3] = (int)(info.draw.x + leny + -lenx); simplePolygonY[3] = (int)(info.draw.y + -lenx + -leny); if (drawFilled) graphics.fillPolygon(simplePolygonX, simplePolygonY, 4); else graphics.drawPolygon(simplePolygonX, simplePolygonY, 4); break; case SHAPE_COMPASS: simplePolygonX[0] = (int)(info.draw.x + lenx); simplePolygonY[0] = (int)(info.draw.y + leny); simplePolygonX[1] = (int)(info.draw.x + -leny/2); simplePolygonY[1] = (int)(info.draw.y + lenx/2); simplePolygonX[2] = (int)(info.draw.x + -lenx/2); simplePolygonY[2] = (int)(info.draw.y + -leny/2); simplePolygonX[3] = (int)(info.draw.x + leny/2); simplePolygonY[3] = (int)(info.draw.y + -lenx/2); if (drawFilled) graphics.fillPolygon(simplePolygonX, simplePolygonY, 4); else graphics.drawPolygon(simplePolygonX, simplePolygonY, 4); break; case SHAPE_TRIANGLE: simplePolygonX[0] = (int)(info.draw.x + lenx); simplePolygonY[0] = (int)(info.draw.y + leny); simplePolygonX[1] = (int)(info.draw.x + -lenx + -leny/2); simplePolygonY[1] = (int)(info.draw.y + -leny + lenx/2); simplePolygonX[2] = (int)(info.draw.x + -lenx + leny/2); simplePolygonY[2] = (int)(info.draw.y + -leny + -lenx/2); if (drawFilled) graphics.fillPolygon(simplePolygonX, simplePolygonY, 3); else graphics.drawPolygon(simplePolygonX, simplePolygonY, 3); break; case SHAPE_INVERTED_T: simplePolygonX[0] = (int)(info.draw.x + lenx); simplePolygonY[0] = (int)(info.draw.y + leny); simplePolygonX[1] = (int)(info.draw.x + -lenx/2 + -leny/2); simplePolygonY[1] = (int)(info.draw.y + -leny/2 + lenx/2); simplePolygonX[2] = (int)(info.draw.x + -lenx + -leny); simplePolygonY[2] = (int)(info.draw.y + -leny + lenx); simplePolygonX[3] = (int)(info.draw.x + -lenx + leny); simplePolygonY[3] = (int)(info.draw.y + -leny + -lenx); simplePolygonX[4] = (int)(info.draw.x + -lenx/2 + leny/2); simplePolygonY[4] = (int)(info.draw.y + -leny/2 + -lenx/2); if (drawFilled) graphics.fillPolygon(simplePolygonX, simplePolygonY, 5); else graphics.drawPolygon(simplePolygonX, simplePolygonY, 5); break; case SHAPE_ARROW: simplePolygonX[0] = (int)(info.draw.x + lenx); simplePolygonY[0] = (int)(info.draw.y + leny); simplePolygonX[1] = (int)(info.draw.x + -leny); simplePolygonY[1] = (int)(info.draw.y + lenx); simplePolygonX[2] = (int)(info.draw.x + -leny/2); simplePolygonY[2] = (int)(info.draw.y + lenx/2); simplePolygonX[3] = (int)(info.draw.x + -lenx + -leny/2); simplePolygonY[3] = (int)(info.draw.y + -leny + lenx/2); simplePolygonX[4] = (int)(info.draw.x + -lenx + leny/2); simplePolygonY[4] = (int)(info.draw.y + -leny + -lenx/2); simplePolygonX[5] = (int)(info.draw.x + leny/2); simplePolygonY[5] = (int)(info.draw.y + -lenx/2); simplePolygonX[6] = (int)(info.draw.x + leny); simplePolygonY[6] = (int)(info.draw.y + -lenx); if (drawFilled) graphics.fillPolygon(simplePolygonX, simplePolygonY, 7); else graphics.drawPolygon(simplePolygonX, simplePolygonY, 7); break; } } } } // draw the underlying object last? if (shape > SHAPE_LINE_ARROW && drawFilled) getChild(object).draw(object,graphics,info); } boolean orientationHittable = true; /** Returns true if the orientation marker can be hit as part of the object. By default the answer is YES. */ public boolean isOrientationHittable() { return orientationHittable; } /** Sets whether or not the orientation marker can be hit as part of the object. */ public void setOrientationHittable(boolean val) { orientationHittable = val; } public boolean hitObject(Object object, DrawInfo2D range) { if (getChild(object).hitObject(object,range)) return true; if (!orientationHittable) return false; // now additionally determine if I was hit if (showOrientation && (object!=null) && (object instanceof Oriented2D)) { final double theta = ((Oriented2D)object).orientation2D(); final double length = ((scale * (range.draw.width < range.draw.height ? range.draw.width : range.draw.height)) + offset); // fit in smallest dimension // we'll always do precise hitting transform.setToTranslation(range.draw.x, range.draw.y); transform.rotate(theta); final double lenx = 1.0 * length; // oriented forwards final double leny = 0.0 * length; switch(shape) { default: // NOTE FALL THRU case SHAPE_LINE: { break; } // hard to hit a line case SHAPE_LINE_ARROW: { break; } // hard to hit a line arrow case SHAPE_KITE: if (path == null) { simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); simplePolygonXd[1] = (0 + -leny + -lenx); simplePolygonYd[1] = (0 + lenx + -leny); simplePolygonXd[2] = (0 + -lenx/2); simplePolygonYd[2] = (0 + -leny/2); simplePolygonXd[3] = (0 + leny + -lenx); simplePolygonYd[3] = (0 + -lenx + -leny); path = buildPolygon(simplePolygonXd, simplePolygonYd, 4); } return transform.createTransformedShape(path).intersects(range.clip.x, range.clip.y, range.clip.width, range.clip.height); //break; case SHAPE_COMPASS: if (path == null) { simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); simplePolygonXd[1] = (0 + -leny/2); simplePolygonYd[1] = (0 + lenx/2); simplePolygonXd[2] = (0 + -lenx/2); simplePolygonYd[2] = (0 + -leny/2); simplePolygonXd[3] = (0 + leny/2); simplePolygonYd[3] = (0 + -lenx/2); path = buildPolygon(simplePolygonXd, simplePolygonYd, 4); } return transform.createTransformedShape(path).intersects(range.clip.x, range.clip.y, range.clip.width, range.clip.height); //break; case SHAPE_TRIANGLE: if (path == null) { simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); simplePolygonXd[1] = (0 + -lenx + -leny/2); simplePolygonYd[1] = (0 + -leny + lenx/2); simplePolygonXd[2] = (0 + -lenx + leny/2); simplePolygonYd[2] = (0 + -leny + -lenx/2); path = buildPolygon(simplePolygonXd, simplePolygonYd, 3); } return transform.createTransformedShape(path).intersects(range.clip.x, range.clip.y, range.clip.width, range.clip.height); // break; case SHAPE_INVERTED_T: if (path == null) { simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); simplePolygonXd[1] = (0 + -lenx/2 + -leny/2); simplePolygonYd[1] = (0 + -leny/2 + lenx/2); simplePolygonXd[2] = (0 + -lenx + -leny); simplePolygonYd[2] = (0 + -leny + lenx); simplePolygonXd[3] = (0 + -lenx + leny); simplePolygonYd[3] = (0 + -leny + -lenx); simplePolygonXd[4] = (0 + -lenx/2 + leny/2); simplePolygonYd[4] = (0 + -leny/2 + -lenx/2); path = buildPolygon(simplePolygonXd, simplePolygonYd, 5); } return transform.createTransformedShape(path).intersects(range.clip.x, range.clip.y, range.clip.width, range.clip.height); // break; case SHAPE_ARROW: if (path == null) { simplePolygonXd[0] = (0 + lenx); simplePolygonYd[0] = (0 + leny); simplePolygonXd[1] = (0 + -leny); simplePolygonYd[1] = (0 + lenx); simplePolygonXd[2] = (0 + -leny/2); simplePolygonYd[2] = (0 + lenx/2); simplePolygonXd[3] = (0 + -lenx + -leny/2); simplePolygonYd[3] = (0 + -leny + lenx/2); simplePolygonXd[4] = (0 + -lenx + leny/2); simplePolygonYd[4] = (0 + -leny + -lenx/2); simplePolygonXd[5] = (0 + leny/2); simplePolygonYd[5] = (0 + -lenx/2); simplePolygonXd[6] = (0 + leny); simplePolygonYd[6] = (0 + -lenx); path = buildPolygon(simplePolygonXd, simplePolygonYd, 7); } return transform.createTransformedShape(path).intersects(range.clip.x, range.clip.y, range.clip.width, range.clip.height); // break; } } return false; } public boolean setSelected(LocationWrapper wrapper, boolean selected) { return getChild(wrapper.getObject()).setSelected(wrapper, selected); } public Inspector getInspector(LocationWrapper wrapper, GUIState state) { return getChild(wrapper.getObject()).getInspector(wrapper,state); } public String getName(LocationWrapper wrapper) { return getChild(wrapper.getObject()).getName(wrapper); } public boolean handleMouseEvent(GUIState guistate, Manipulating2D manipulating, LocationWrapper wrapper, MouseEvent event, DrawInfo2D fieldPortrayalDrawInfo, int type) { return getChild(wrapper.getObject()).handleMouseEvent(guistate, manipulating, wrapper, event, fieldPortrayalDrawInfo, type); // let someone else have it } }