1 package net.sf.openrocket.gui.scalefigure;
4 import java.awt.BasicStroke;
6 import java.awt.Dimension;
7 import java.awt.Graphics;
8 import java.awt.Graphics2D;
9 import java.awt.Rectangle;
10 import java.awt.RenderingHints;
11 import java.awt.Shape;
12 import java.awt.geom.AffineTransform;
13 import java.awt.geom.Ellipse2D;
14 import java.awt.geom.NoninvertibleTransformException;
15 import java.awt.geom.Point2D;
16 import java.awt.geom.Rectangle2D;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.Iterator;
20 import java.util.LinkedHashSet;
22 import net.sf.openrocket.gui.figureelements.FigureElement;
23 import net.sf.openrocket.rocketcomponent.Configuration;
24 import net.sf.openrocket.rocketcomponent.Motor;
25 import net.sf.openrocket.rocketcomponent.MotorMount;
26 import net.sf.openrocket.rocketcomponent.RocketComponent;
27 import net.sf.openrocket.util.Coordinate;
28 import net.sf.openrocket.util.LineStyle;
29 import net.sf.openrocket.util.MathUtil;
30 import net.sf.openrocket.util.Prefs;
31 import net.sf.openrocket.util.Reflection;
32 import net.sf.openrocket.util.Transformation;
35 * A <code>ScaleFigure</code> that draws a complete rocket. Extra information can
36 * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)},
37 * {@link #clearRelativeExtra()}.
39 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
42 public class RocketFigure extends AbstractScaleFigure {
43 private static final long serialVersionUID = 1L;
45 private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure";
46 private static final String ROCKET_FIGURE_SUFFIX = "Shapes";
48 public static final int TYPE_SIDE = 1;
49 public static final int TYPE_BACK = 2;
51 // Width for drawing normal and selected components
52 public static final double NORMAL_WIDTH = 1.0;
53 public static final double SELECTED_WIDTH = 2.0;
56 private final Configuration configuration;
57 private RocketComponent[] selection = new RocketComponent[0];
59 private int type = TYPE_SIDE;
61 private double rotation;
62 private Transformation transformation;
64 private double translateX, translateY;
69 * figureComponents contains the corresponding RocketComponents of the figureShapes
71 private final ArrayList<Shape> figureShapes = new ArrayList<Shape>();
72 private final ArrayList<RocketComponent> figureComponents =
73 new ArrayList<RocketComponent>();
75 private double minX=0, maxX=0, maxR=0;
76 // Figure width and height in SI-units and pixels
77 private double figureWidth=0, figureHeight=0;
78 private int figureWidthPx=0, figureHeightPx=0;
80 private AffineTransform g2transformation = null;
82 private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
83 private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
87 * Creates a new rocket figure.
89 public RocketFigure(Configuration configuration) {
92 this.configuration = configuration;
95 this.transformation = Transformation.rotate_x(0.0);
103 public Dimension getOrigin() {
104 return new Dimension((int)translateX, (int)translateY);
108 public double getFigureHeight() {
113 public double getFigureWidth() {
118 public RocketComponent[] getSelection() {
122 public void setSelection(RocketComponent[] selection) {
123 if (selection == null) {
124 selection = new RocketComponent[0];
126 this.selection = selection;
132 public double getRotation() {
136 public Transformation getRotateTransformation() {
137 return transformation;
140 public void setRotation(double rot) {
141 if (MathUtil.equals(rotation, rot))
144 this.transformation = Transformation.rotate_x(rotation);
149 public int getType() {
153 public void setType(int type) {
154 if (type != TYPE_BACK && type != TYPE_SIDE) {
155 throw new IllegalArgumentException("Illegal type: "+type);
157 if (this.type == type)
169 * Updates the figure shapes and figure size.
172 public void updateFigure() {
173 figureShapes.clear();
174 figureComponents.clear();
178 // Get shapes for all active components
179 for (RocketComponent c: configuration) {
180 Shape[] s = getShapes(c);
181 for (int i=0; i < s.length; i++) {
182 figureShapes.add(s[i]);
183 figureComponents.add(c);
192 public void addRelativeExtra(FigureElement p) {
193 relativeExtra.add(p);
196 public void removeRelativeExtra(FigureElement p) {
197 relativeExtra.remove(p);
200 public void clearRelativeExtra() {
201 relativeExtra.clear();
205 public void addAbsoluteExtra(FigureElement p) {
206 absoluteExtra.add(p);
209 public void removeAbsoluteExtra(FigureElement p) {
210 absoluteExtra.remove(p);
213 public void clearAbsoluteExtra() {
214 absoluteExtra.clear();
219 * Paints the rocket on to the Graphics element.
221 * Warning: If paintComponent is used outside the normal Swing usage, some Swing
222 * dependent parameters may be left wrong (mainly transformation). If it is used,
223 * the RocketFigure should be repainted immediately afterwards.
226 public void paintComponent(Graphics g) {
227 super.paintComponent(g);
228 Graphics2D g2 = (Graphics2D)g;
231 AffineTransform baseTransform = g2.getTransform();
233 // Update figure shapes if necessary
234 if (figureShapes == null)
239 // Calculate translation for figure centering
240 if (figureWidthPx + 2*BORDER_PIXELS_WIDTH < getWidth()) {
242 // Figure fits in the viewport
243 if (type == TYPE_BACK)
246 tx = (getWidth()-figureWidthPx)/2 - minX*scale;
250 // Figure does not fit in viewport
251 if (type == TYPE_BACK)
252 tx = BORDER_PIXELS_WIDTH + figureWidthPx/2;
254 tx = BORDER_PIXELS_WIDTH - minX*scale;
258 if (figureHeightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
261 ty = BORDER_PIXELS_HEIGHT + figureHeightPx/2;
264 if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
265 // Origin has changed, fire event
272 // Calculate and store the transformation used
273 // (inverse is used in detecting clicks on objects)
274 g2transformation = new AffineTransform();
275 g2transformation.translate(translateX, translateY);
276 // Mirror position Y-axis upwards
277 g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
279 g2.transform(g2transformation);
281 // Set rendering hints appropriately
282 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
283 RenderingHints.VALUE_STROKE_NORMALIZE);
284 g2.setRenderingHint(RenderingHints.KEY_RENDERING,
285 RenderingHints.VALUE_RENDER_QUALITY);
286 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
287 RenderingHints.VALUE_ANTIALIAS_ON);
292 for (int i=0; i < figureShapes.size(); i++) {
293 RocketComponent c = figureComponents.get(i);
294 Shape s = figureShapes.get(i);
295 boolean selected = false;
297 // Check if component is in the selection
298 for (int j=0; j < selection.length; j++) {
299 if (c == selection[j]) {
305 // Set component color and line style
306 Color color = c.getColor();
308 color = Prefs.getDefaultColor(c.getClass());
312 LineStyle style = c.getLineStyle();
314 style = Prefs.getDefaultLineStyle(c.getClass());
316 float[] dashes = style.getDashes();
317 for (int j=0; j<dashes.length; j++) {
318 dashes[j] *= EXTRA_SCALE / scale;
322 g2.setStroke(new BasicStroke((float)(SELECTED_WIDTH*EXTRA_SCALE/scale),
323 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
324 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
325 RenderingHints.VALUE_STROKE_PURE);
327 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
328 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
329 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
330 RenderingHints.VALUE_STROKE_NORMALIZE);
336 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
337 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
338 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
339 RenderingHints.VALUE_STROKE_NORMALIZE);
343 String motorID = configuration.getMotorConfigurationID();
344 Color fillColor = Prefs.getMotorFillColor();
345 Color borderColor = Prefs.getMotorBorderColor();
346 Iterator<MotorMount> iterator = configuration.motorIterator();
347 while (iterator.hasNext()) {
348 MotorMount mount = iterator.next();
349 Motor motor = mount.getMotor(motorID);
350 double length = motor.getLength();
351 double radius = motor.getDiameter() / 2;
353 Coordinate[] position = ((RocketComponent)mount).toAbsolute(
354 new Coordinate(((RocketComponent)mount).getLength() +
355 mount.getMotorOverhang() - length));
357 for (int i=0; i < position.length; i++) {
358 position[i] = transformation.transform(position[i]);
361 for (Coordinate coord: position) {
363 if (type == TYPE_SIDE) {
364 s = new Rectangle2D.Double(EXTRA_SCALE*coord.x,
365 EXTRA_SCALE*(coord.y - radius), EXTRA_SCALE*length,
366 EXTRA_SCALE*2*radius);
368 s = new Ellipse2D.Double(EXTRA_SCALE*(coord.z-radius),
369 EXTRA_SCALE*(coord.y-radius), EXTRA_SCALE*2*radius,
370 EXTRA_SCALE*2*radius);
372 g2.setColor(fillColor);
374 g2.setColor(borderColor);
381 // Draw relative extras
382 for (FigureElement e: relativeExtra) {
383 e.paint(g2, scale/EXTRA_SCALE);
386 // Draw absolute extras
387 g2.setTransform(baseTransform);
388 Rectangle rect = this.getVisibleRect();
390 for (FigureElement e: absoluteExtra) {
391 e.paint(g2, 1.0, rect);
397 public RocketComponent[] getComponentsByPoint(double x, double y) {
398 // Calculate point in shapes' coordinates
399 Point2D.Double p = new Point2D.Double(x,y);
401 g2transformation.inverseTransform(p,p);
402 } catch (NoninvertibleTransformException e) {
403 return new RocketComponent[0];
406 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
408 for (int i=0; i<figureShapes.size(); i++) {
409 if (figureShapes.get(i).contains(p))
410 l.add(figureComponents.get(i));
412 return l.toArray(new RocketComponent[0]);
418 * Gets the shapes required to draw the component.
424 private Shape[] getShapes(RocketComponent component) {
427 // Find the appropriate method
430 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide",
431 RocketComponent.class, Transformation.class);
435 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack",
436 RocketComponent.class, Transformation.class);
440 throw new RuntimeException("Unknown figure type = "+type);
444 System.err.println("ERROR: Rocket figure paint method not found for " + component);
448 return (Shape[])m.invokeStatic(component,transformation);
454 * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
455 * The bounds are stored in the variables minX, maxX and maxR.
457 private void calculateFigureBounds() {
458 Collection<Coordinate> bounds = configuration.getBounds();
460 if (bounds.isEmpty()) {
467 minX = Double.MAX_VALUE;
468 maxX = Double.MIN_VALUE;
470 for (Coordinate c: bounds) {
471 double x = c.x, r = MathUtil.hypot(c.y, c.z);
482 public double getBestZoom(Rectangle2D bounds) {
484 if (bounds.getWidth() > 0.0001)
485 zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth();
486 if (bounds.getHeight() > 0.0001)
487 zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight();
488 return Math.min(zh, zv);
493 * Calculates the necessary size of the figure and set the PreferredSize
494 * property accordingly.
496 private void calculateSize() {
497 calculateFigureBounds();
501 figureWidth = maxX-minX;
502 figureHeight = 2*maxR;
506 figureWidth = 2*maxR;
507 figureHeight = 2*maxR;
511 assert(false): "Should not occur, type="+type;
516 figureWidthPx = (int)(figureWidth * scale);
517 figureHeightPx = (int)(figureHeight * scale);
519 Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
520 figureHeightPx+2*BORDER_PIXELS_HEIGHT);
522 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
529 public Rectangle2D getDimensions() {
532 return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
535 return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
538 throw new RuntimeException("Illegal figure type = "+type);