1 package net.sf.openrocket.gui.scalefigure;
4 import net.sf.openrocket.gui.figureelements.FigureElement;
5 import net.sf.openrocket.gui.main.ExceptionHandler;
6 import net.sf.openrocket.motor.Motor;
7 import net.sf.openrocket.rocketcomponent.Configuration;
8 import net.sf.openrocket.rocketcomponent.MotorMount;
9 import net.sf.openrocket.rocketcomponent.RocketComponent;
10 import net.sf.openrocket.util.BugException;
11 import net.sf.openrocket.util.Coordinate;
12 import net.sf.openrocket.util.LineStyle;
13 import net.sf.openrocket.util.MathUtil;
14 import net.sf.openrocket.util.Prefs;
15 import net.sf.openrocket.util.Reflection;
16 import net.sf.openrocket.util.Transformation;
18 import java.awt.BasicStroke;
19 import java.awt.Color;
20 import java.awt.Dimension;
21 import java.awt.Graphics;
22 import java.awt.Graphics2D;
23 import java.awt.Rectangle;
24 import java.awt.RenderingHints;
25 import java.awt.Shape;
26 import java.awt.geom.AffineTransform;
27 import java.awt.geom.Ellipse2D;
28 import java.awt.geom.NoninvertibleTransformException;
29 import java.awt.geom.Point2D;
30 import java.awt.geom.Rectangle2D;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Iterator;
34 import java.util.LinkedHashSet;
37 * A <code>ScaleFigure</code> that draws a complete rocket. Extra information can
38 * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)},
39 * {@link #clearRelativeExtra()}.
41 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
44 public class RocketFigure extends AbstractScaleFigure {
45 private static final long serialVersionUID = 1L;
47 private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure";
48 private static final String ROCKET_FIGURE_SUFFIX = "Shapes";
50 public static final int TYPE_SIDE = 1;
51 public static final int TYPE_BACK = 2;
53 // Width for drawing normal and selected components
54 public static final double NORMAL_WIDTH = 1.0;
55 public static final double SELECTED_WIDTH = 2.0;
58 private final Configuration configuration;
59 private RocketComponent[] selection = new RocketComponent[0];
61 private int type = TYPE_SIDE;
63 private double rotation;
64 private Transformation transformation;
66 private double translateX, translateY;
71 * figureComponents contains the corresponding RocketComponents of the figureShapes
73 private final ArrayList<Shape> figureShapes = new ArrayList<Shape>();
74 private final ArrayList<RocketComponent> figureComponents =
75 new ArrayList<RocketComponent>();
77 private double minX=0, maxX=0, maxR=0;
78 // Figure width and height in SI-units and pixels
79 private double figureWidth=0, figureHeight=0;
80 private int figureWidthPx=0, figureHeightPx=0;
82 private AffineTransform g2transformation = null;
84 private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
85 private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
89 * Creates a new rocket figure.
91 public RocketFigure(Configuration configuration) {
94 this.configuration = configuration;
97 this.transformation = Transformation.rotate_x(0.0);
105 public Dimension getOrigin() {
106 return new Dimension((int)translateX, (int)translateY);
110 public double getFigureHeight() {
115 public double getFigureWidth() {
120 public RocketComponent[] getSelection() {
124 public void setSelection(RocketComponent[] selection) {
125 if (selection == null) {
126 this.selection = new RocketComponent[0];
128 this.selection = selection;
134 public double getRotation() {
138 public Transformation getRotateTransformation() {
139 return transformation;
142 public void setRotation(double rot) {
143 if (MathUtil.equals(rotation, rot))
146 this.transformation = Transformation.rotate_x(rotation);
151 public int getType() {
155 public void setType(int type) {
156 if (type != TYPE_BACK && type != TYPE_SIDE) {
157 throw new IllegalArgumentException("Illegal type: "+type);
159 if (this.type == type)
171 * Updates the figure shapes and figure size.
174 public void updateFigure() {
175 figureShapes.clear();
176 figureComponents.clear();
180 // Get shapes for all active components
181 for (RocketComponent c: configuration) {
182 Shape[] s = getShapes(c);
183 for (int i=0; i < s.length; i++) {
184 figureShapes.add(s[i]);
185 figureComponents.add(c);
194 public void addRelativeExtra(FigureElement p) {
195 relativeExtra.add(p);
198 public void removeRelativeExtra(FigureElement p) {
199 relativeExtra.remove(p);
202 public void clearRelativeExtra() {
203 relativeExtra.clear();
207 public void addAbsoluteExtra(FigureElement p) {
208 absoluteExtra.add(p);
211 public void removeAbsoluteExtra(FigureElement p) {
212 absoluteExtra.remove(p);
215 public void clearAbsoluteExtra() {
216 absoluteExtra.clear();
221 * Paints the rocket on to the Graphics element.
223 * Warning: If paintComponent is used outside the normal Swing usage, some Swing
224 * dependent parameters may be left wrong (mainly transformation). If it is used,
225 * the RocketFigure should be repainted immediately afterwards.
228 public void paintComponent(Graphics g) {
229 super.paintComponent(g);
230 Graphics2D g2 = (Graphics2D)g;
233 AffineTransform baseTransform = g2.getTransform();
235 // Update figure shapes if necessary
236 if (figureShapes == null)
241 // Calculate translation for figure centering
242 if (figureWidthPx + 2*BORDER_PIXELS_WIDTH < getWidth()) {
244 // Figure fits in the viewport
245 if (type == TYPE_BACK)
248 tx = (getWidth()-figureWidthPx)/2 - minX*scale;
252 // Figure does not fit in viewport
253 if (type == TYPE_BACK)
254 tx = BORDER_PIXELS_WIDTH + figureWidthPx/2;
256 tx = BORDER_PIXELS_WIDTH - minX*scale;
260 ty = computeTy(figureHeightPx);
262 if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
263 // Origin has changed, fire event
270 // Calculate and store the transformation used
271 // (inverse is used in detecting clicks on objects)
272 g2transformation = new AffineTransform();
273 g2transformation.translate(translateX, translateY);
274 // Mirror position Y-axis upwards
275 g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
277 g2.transform(g2transformation);
279 // Set rendering hints appropriately
280 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
281 RenderingHints.VALUE_STROKE_NORMALIZE);
282 g2.setRenderingHint(RenderingHints.KEY_RENDERING,
283 RenderingHints.VALUE_RENDER_QUALITY);
284 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
285 RenderingHints.VALUE_ANTIALIAS_ON);
290 for (int i=0; i < figureShapes.size(); i++) {
291 RocketComponent c = figureComponents.get(i);
292 Shape s = figureShapes.get(i);
293 boolean selected = false;
295 // Check if component is in the selection
296 for (int j=0; j < selection.length; j++) {
297 if (c == selection[j]) {
303 // Set component color and line style
304 Color color = c.getColor();
306 color = Prefs.getDefaultColor(c.getClass());
310 LineStyle style = c.getLineStyle();
312 style = Prefs.getDefaultLineStyle(c.getClass());
314 float[] dashes = style.getDashes();
315 for (int j=0; j<dashes.length; j++) {
316 dashes[j] *= EXTRA_SCALE / scale;
320 g2.setStroke(new BasicStroke((float)(SELECTED_WIDTH*EXTRA_SCALE/scale),
321 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
322 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
323 RenderingHints.VALUE_STROKE_PURE);
325 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
326 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
327 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
328 RenderingHints.VALUE_STROKE_NORMALIZE);
334 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
335 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
336 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
337 RenderingHints.VALUE_STROKE_NORMALIZE);
341 String motorID = configuration.getMotorConfigurationID();
342 Color fillColor = Prefs.getMotorFillColor();
343 Color borderColor = Prefs.getMotorBorderColor();
344 Iterator<MotorMount> iterator = configuration.motorIterator();
345 while (iterator.hasNext()) {
346 MotorMount mount = iterator.next();
347 Motor motor = mount.getMotor(motorID);
348 double length = motor.getLength();
349 double radius = motor.getDiameter() / 2;
351 Coordinate[] position = ((RocketComponent)mount).toAbsolute(
352 new Coordinate(((RocketComponent)mount).getLength() +
353 mount.getMotorOverhang() - length));
355 for (int i=0; i < position.length; i++) {
356 position[i] = transformation.transform(position[i]);
359 for (Coordinate coord: position) {
361 if (type == TYPE_SIDE) {
362 s = new Rectangle2D.Double(EXTRA_SCALE*coord.x,
363 EXTRA_SCALE*(coord.y - radius), EXTRA_SCALE*length,
364 EXTRA_SCALE*2*radius);
366 s = new Ellipse2D.Double(EXTRA_SCALE*(coord.z-radius),
367 EXTRA_SCALE*(coord.y-radius), EXTRA_SCALE*2*radius,
368 EXTRA_SCALE*2*radius);
370 g2.setColor(fillColor);
372 g2.setColor(borderColor);
379 // Draw relative extras
380 for (FigureElement e: relativeExtra) {
381 e.paint(g2, scale/EXTRA_SCALE);
384 // Draw absolute extras
385 g2.setTransform(baseTransform);
386 Rectangle rect = this.getVisibleRect();
388 for (FigureElement e: absoluteExtra) {
389 e.paint(g2, 1.0, rect);
394 protected double computeTy (int heightPx) {
396 if (heightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
399 ty = BORDER_PIXELS_HEIGHT + heightPx/2;
405 public RocketComponent[] getComponentsByPoint(double x, double y) {
406 // Calculate point in shapes' coordinates
407 Point2D.Double p = new Point2D.Double(x,y);
409 g2transformation.inverseTransform(p,p);
410 } catch (NoninvertibleTransformException e) {
411 return new RocketComponent[0];
414 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
416 for (int i=0; i<figureShapes.size(); i++) {
417 if (figureShapes.get(i).contains(p))
418 l.add(figureComponents.get(i));
420 return l.toArray(new RocketComponent[0]);
426 * Gets the shapes required to draw the component.
432 private Shape[] getShapes(RocketComponent component) {
435 // Find the appropriate method
438 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide",
439 RocketComponent.class, Transformation.class);
443 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack",
444 RocketComponent.class, Transformation.class);
448 throw new BugException("Unknown figure type = "+type);
452 ExceptionHandler.handleErrorCondition("ERROR: Rocket figure paint method not found for "
457 return (Shape[])m.invokeStatic(component,transformation);
463 * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
464 * The bounds are stored in the variables minX, maxX and maxR.
466 private void calculateFigureBounds() {
467 Collection<Coordinate> bounds = configuration.getBounds();
469 if (bounds.isEmpty()) {
476 minX = Double.MAX_VALUE;
477 maxX = Double.MIN_VALUE;
479 for (Coordinate c: bounds) {
480 double x = c.x, r = MathUtil.hypot(c.y, c.z);
491 public double getBestZoom(Rectangle2D bounds) {
493 if (bounds.getWidth() > 0.0001)
494 zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth();
495 if (bounds.getHeight() > 0.0001)
496 zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight();
497 return Math.min(zh, zv);
502 * Calculates the necessary size of the figure and set the PreferredSize
503 * property accordingly.
505 private void calculateSize() {
506 calculateFigureBounds();
510 figureWidth = maxX-minX;
511 figureHeight = 2*maxR;
515 figureWidth = 2*maxR;
516 figureHeight = 2*maxR;
520 assert(false): "Should not occur, type="+type;
525 figureWidthPx = (int)(figureWidth * scale);
526 figureHeightPx = (int)(figureHeight * scale);
528 Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
529 figureHeightPx+2*BORDER_PIXELS_HEIGHT);
531 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
538 public Rectangle2D getDimensions() {
541 return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
544 return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
547 throw new BugException("Illegal figure type = "+type);