package net.sf.openrocket.gui.scalefigure; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import net.sf.openrocket.gui.figureelements.FigureElement; import net.sf.openrocket.gui.util.ColorConversion; import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.LineStyle; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Reflection; import net.sf.openrocket.util.Transformation; /** * A <code>ScaleFigure</code> that draws a complete rocket. Extra information can * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)}, * {@link #clearRelativeExtra()}. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public class RocketFigure extends AbstractScaleFigure { private static final long serialVersionUID = 1L; private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure"; private static final String ROCKET_FIGURE_SUFFIX = "Shapes"; public static final int TYPE_SIDE = 1; public static final int TYPE_BACK = 2; // Width for drawing normal and selected components public static final double NORMAL_WIDTH = 1.0; public static final double SELECTED_WIDTH = 2.0; private Configuration configuration; private RocketComponent[] selection = new RocketComponent[0]; private int type = TYPE_SIDE; private double rotation; private Transformation transformation; private double translateX, translateY; /* * figureComponents contains the corresponding RocketComponents of the figureShapes */ private final ArrayList<Shape> figureShapes = new ArrayList<Shape>(); private final ArrayList<RocketComponent> figureComponents = new ArrayList<RocketComponent>(); private double minX = 0, maxX = 0, maxR = 0; // Figure width and height in SI-units and pixels private double figureWidth = 0, figureHeight = 0; protected int figureWidthPx = 0, figureHeightPx = 0; private AffineTransform g2transformation = null; private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>(); private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>(); /** * Creates a new rocket figure. */ public RocketFigure(Configuration configuration) { super(); this.configuration = configuration; this.rotation = 0.0; this.transformation = Transformation.rotate_x(0.0); updateFigure(); } /** * Set the configuration displayed by the figure. It may use the same or different rocket. * * @param configuration the configuration to display. */ public void setConfiguration(Configuration configuration) { this.configuration = configuration; updateFigure(); } @Override public Dimension getOrigin() { return new Dimension((int) translateX, (int) translateY); } @Override public double getFigureHeight() { return figureHeight; } @Override public double getFigureWidth() { return figureWidth; } public RocketComponent[] getSelection() { return selection; } public void setSelection(RocketComponent[] selection) { if (selection == null) { this.selection = new RocketComponent[0]; } else { this.selection = selection; } updateFigure(); } public double getRotation() { return rotation; } public Transformation getRotateTransformation() { return transformation; } public void setRotation(double rot) { if (MathUtil.equals(rotation, rot)) return; this.rotation = rot; this.transformation = Transformation.rotate_x(rotation); updateFigure(); } public int getType() { return type; } public void setType(int type) { if (type != TYPE_BACK && type != TYPE_SIDE) { throw new IllegalArgumentException("Illegal type: " + type); } if (this.type == type) return; this.type = type; updateFigure(); } /** * Updates the figure shapes and figure size. */ @Override public void updateFigure() { figureShapes.clear(); figureComponents.clear(); calculateSize(); // Get shapes for all active components for (RocketComponent c : configuration) { Shape[] s = getShapes(c); for (int i = 0; i < s.length; i++) { figureShapes.add(s[i]); figureComponents.add(c); } } repaint(); fireChangeEvent(); } public void addRelativeExtra(FigureElement p) { relativeExtra.add(p); } public void removeRelativeExtra(FigureElement p) { relativeExtra.remove(p); } public void clearRelativeExtra() { relativeExtra.clear(); } public void addAbsoluteExtra(FigureElement p) { absoluteExtra.add(p); } public void removeAbsoluteExtra(FigureElement p) { absoluteExtra.remove(p); } public void clearAbsoluteExtra() { absoluteExtra.clear(); } /** * Paints the rocket on to the Graphics element. * <p> * Warning: If paintComponent is used outside the normal Swing usage, some Swing * dependent parameters may be left wrong (mainly transformation). If it is used, * the RocketFigure should be repainted immediately afterwards. */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; AffineTransform baseTransform = g2.getTransform(); // Update figure shapes if necessary if (figureShapes == null) updateFigure(); double tx, ty; // Calculate translation for figure centering if (figureWidthPx + 2 * borderPixelsWidth < getWidth()) { // Figure fits in the viewport if (type == TYPE_BACK) tx = getWidth() / 2; else tx = (getWidth() - figureWidthPx) / 2 - minX * scale; } else { // Figure does not fit in viewport if (type == TYPE_BACK) tx = borderPixelsWidth + figureWidthPx / 2; else tx = borderPixelsWidth - minX * scale; } ty = computeTy(figureHeightPx); if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { // Origin has changed, fire event translateX = tx; translateY = ty; fireChangeEvent(); } // Calculate and store the transformation used // (inverse is used in detecting clicks on objects) g2transformation = new AffineTransform(); g2transformation.translate(translateX, translateY); // Mirror position Y-axis upwards g2transformation.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE); g2.transform(g2transformation); // Set rendering hints appropriately g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Draw all shapes for (int i = 0; i < figureShapes.size(); i++) { RocketComponent c = figureComponents.get(i); Shape s = figureShapes.get(i); boolean selected = false; // Check if component is in the selection for (int j = 0; j < selection.length; j++) { if (c == selection[j]) { selected = true; break; } } // Set component color and line style net.sf.openrocket.util.Color color = c.getColor(); if (color == null) { color = Application.getPreferences().getDefaultColor(c.getClass()); } g2.setColor(ColorConversion.toAwtColor(color)); LineStyle style = c.getLineStyle(); if (style == null) style = Application.getPreferences().getDefaultLineStyle(c.getClass()); float[] dashes = style.getDashes(); for (int j = 0; j < dashes.length; j++) { dashes[j] *= EXTRA_SCALE / scale; } if (selected) { g2.setStroke(new BasicStroke((float) (SELECTED_WIDTH * EXTRA_SCALE / scale), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, dashes, 0)); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); } else { g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, dashes, 0)); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); } g2.draw(s); } g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); // Draw motors String motorID = configuration.getFlightConfigurationID(); Color fillColor = ((SwingPreferences)Application.getPreferences()).getMotorFillColor(); Color borderColor = ((SwingPreferences)Application.getPreferences()).getMotorBorderColor(); Iterator<MotorMount> iterator = configuration.motorIterator(); while (iterator.hasNext()) { MotorMount mount = iterator.next(); Motor motor = mount.getMotor(motorID); double length = motor.getLength(); double radius = motor.getDiameter() / 2; Coordinate[] position = ((RocketComponent) mount).toAbsolute( new Coordinate(((RocketComponent) mount).getLength() + mount.getMotorOverhang() - length)); for (int i = 0; i < position.length; i++) { position[i] = transformation.transform(position[i]); } for (Coordinate coord : position) { Shape s; if (type == TYPE_SIDE) { s = new Rectangle2D.Double(EXTRA_SCALE * coord.x, EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * length, EXTRA_SCALE * 2 * radius); } else { s = new Ellipse2D.Double(EXTRA_SCALE * (coord.z - radius), EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * 2 * radius, EXTRA_SCALE * 2 * radius); } g2.setColor(fillColor); g2.fill(s); g2.setColor(borderColor); g2.draw(s); } } // Draw relative extras for (FigureElement e : relativeExtra) { e.paint(g2, scale / EXTRA_SCALE); } // Draw absolute extras g2.setTransform(baseTransform); Rectangle rect = this.getVisibleRect(); for (FigureElement e : absoluteExtra) { e.paint(g2, 1.0, rect); } } protected double computeTy(int heightPx) { final double ty; if (heightPx + 2 * borderPixelsHeight < getHeight()) { ty = getHeight() / 2; } else { ty = borderPixelsHeight + heightPx / 2; } return ty; } public RocketComponent[] getComponentsByPoint(double x, double y) { // Calculate point in shapes' coordinates Point2D.Double p = new Point2D.Double(x, y); try { g2transformation.inverseTransform(p, p); } catch (NoninvertibleTransformException e) { return new RocketComponent[0]; } LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>(); for (int i = 0; i < figureShapes.size(); i++) { if (figureShapes.get(i).contains(p)) l.add(figureComponents.get(i)); } return l.toArray(new RocketComponent[0]); } /** * Gets the shapes required to draw the component. * * @param component * @param params * @return */ private Shape[] getShapes(RocketComponent component) { Reflection.Method m; // Find the appropriate method switch (type) { case TYPE_SIDE: m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide", RocketComponent.class, Transformation.class); break; case TYPE_BACK: m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack", RocketComponent.class, Transformation.class); break; default: throw new BugException("Unknown figure type = " + type); } if (m == null) { Application.getExceptionHandler().handleErrorCondition("ERROR: Rocket figure paint method not found for " + component); return new Shape[0]; } return (Shape[]) m.invokeStatic(component, transformation); } /** * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions. * The bounds are stored in the variables minX, maxX and maxR. */ private void calculateFigureBounds() { Collection<Coordinate> bounds = configuration.getBounds(); if (bounds.isEmpty()) { minX = 0; maxX = 0; maxR = 0; return; } minX = Double.MAX_VALUE; maxX = Double.MIN_VALUE; maxR = 0; for (Coordinate c : bounds) { double x = c.x, r = MathUtil.hypot(c.y, c.z); if (x < minX) minX = x; if (x > maxX) maxX = x; if (r > maxR) maxR = r; } } public double getBestZoom(Rectangle2D bounds) { double zh = 1, zv = 1; if (bounds.getWidth() > 0.0001) zh = (getWidth() - 2 * borderPixelsWidth) / bounds.getWidth(); if (bounds.getHeight() > 0.0001) zv = (getHeight() - 2 * borderPixelsHeight) / bounds.getHeight(); return Math.min(zh, zv); } /** * Calculates the necessary size of the figure and set the PreferredSize * property accordingly. */ private void calculateSize() { calculateFigureBounds(); switch (type) { case TYPE_SIDE: figureWidth = maxX - minX; figureHeight = 2 * maxR; break; case TYPE_BACK: figureWidth = 2 * maxR; figureHeight = 2 * maxR; break; default: assert (false) : "Should not occur, type=" + type; figureWidth = 0; figureHeight = 0; } figureWidthPx = (int) (figureWidth * scale); figureHeightPx = (int) (figureHeight * scale); Dimension d = new Dimension(figureWidthPx + 2 * borderPixelsWidth, figureHeightPx + 2 * borderPixelsHeight); if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) { setPreferredSize(d); setMinimumSize(d); revalidate(); } } public Rectangle2D getDimensions() { switch (type) { case TYPE_SIDE: return new Rectangle2D.Double(minX, -maxR, maxX - minX, 2 * maxR); case TYPE_BACK: return new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR); default: throw new BugException("Illegal figure type = " + type); } } }