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.gui.main.ExceptionHandler;
24 import net.sf.openrocket.motor.Motor;
25 import net.sf.openrocket.rocketcomponent.Configuration;
26 import net.sf.openrocket.rocketcomponent.MotorMount;
27 import net.sf.openrocket.rocketcomponent.RocketComponent;
28 import net.sf.openrocket.util.Coordinate;
29 import net.sf.openrocket.util.LineStyle;
30 import net.sf.openrocket.util.MathUtil;
31 import net.sf.openrocket.util.Prefs;
32 import net.sf.openrocket.util.Reflection;
33 import net.sf.openrocket.util.Transformation;
36 * A <code>ScaleFigure</code> that draws a complete rocket. Extra information can
37 * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)},
38 * {@link #clearRelativeExtra()}.
40 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
43 public class RocketFigure extends AbstractScaleFigure {
44 private static final long serialVersionUID = 1L;
46 private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure";
47 private static final String ROCKET_FIGURE_SUFFIX = "Shapes";
49 public static final int TYPE_SIDE = 1;
50 public static final int TYPE_BACK = 2;
52 // Width for drawing normal and selected components
53 public static final double NORMAL_WIDTH = 1.0;
54 public static final double SELECTED_WIDTH = 2.0;
57 private final Configuration configuration;
58 private RocketComponent[] selection = new RocketComponent[0];
60 private int type = TYPE_SIDE;
62 private double rotation;
63 private Transformation transformation;
65 private double translateX, translateY;
70 * figureComponents contains the corresponding RocketComponents of the figureShapes
72 private final ArrayList<Shape> figureShapes = new ArrayList<Shape>();
73 private final ArrayList<RocketComponent> figureComponents =
74 new ArrayList<RocketComponent>();
76 private double minX=0, maxX=0, maxR=0;
77 // Figure width and height in SI-units and pixels
78 private double figureWidth=0, figureHeight=0;
79 private int figureWidthPx=0, figureHeightPx=0;
81 private AffineTransform g2transformation = null;
83 private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
84 private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
88 * Creates a new rocket figure.
90 public RocketFigure(Configuration configuration) {
93 this.configuration = configuration;
96 this.transformation = Transformation.rotate_x(0.0);
104 public Dimension getOrigin() {
105 return new Dimension((int)translateX, (int)translateY);
109 public double getFigureHeight() {
114 public double getFigureWidth() {
119 public RocketComponent[] getSelection() {
123 public void setSelection(RocketComponent[] selection) {
124 if (selection == null) {
125 this.selection = new RocketComponent[0];
127 this.selection = selection;
133 public double getRotation() {
137 public Transformation getRotateTransformation() {
138 return transformation;
141 public void setRotation(double rot) {
142 if (MathUtil.equals(rotation, rot))
145 this.transformation = Transformation.rotate_x(rotation);
150 public int getType() {
154 public void setType(int type) {
155 if (type != TYPE_BACK && type != TYPE_SIDE) {
156 throw new IllegalArgumentException("Illegal type: "+type);
158 if (this.type == type)
170 * Updates the figure shapes and figure size.
173 public void updateFigure() {
174 figureShapes.clear();
175 figureComponents.clear();
179 // Get shapes for all active components
180 for (RocketComponent c: configuration) {
181 Shape[] s = getShapes(c);
182 for (int i=0; i < s.length; i++) {
183 figureShapes.add(s[i]);
184 figureComponents.add(c);
193 public void addRelativeExtra(FigureElement p) {
194 relativeExtra.add(p);
197 public void removeRelativeExtra(FigureElement p) {
198 relativeExtra.remove(p);
201 public void clearRelativeExtra() {
202 relativeExtra.clear();
206 public void addAbsoluteExtra(FigureElement p) {
207 absoluteExtra.add(p);
210 public void removeAbsoluteExtra(FigureElement p) {
211 absoluteExtra.remove(p);
214 public void clearAbsoluteExtra() {
215 absoluteExtra.clear();
220 * Paints the rocket on to the Graphics element.
222 * Warning: If paintComponent is used outside the normal Swing usage, some Swing
223 * dependent parameters may be left wrong (mainly transformation). If it is used,
224 * the RocketFigure should be repainted immediately afterwards.
227 public void paintComponent(Graphics g) {
228 super.paintComponent(g);
229 Graphics2D g2 = (Graphics2D)g;
232 AffineTransform baseTransform = g2.getTransform();
234 // Update figure shapes if necessary
235 if (figureShapes == null)
240 // Calculate translation for figure centering
241 if (figureWidthPx + 2*BORDER_PIXELS_WIDTH < getWidth()) {
243 // Figure fits in the viewport
244 if (type == TYPE_BACK)
247 tx = (getWidth()-figureWidthPx)/2 - minX*scale;
251 // Figure does not fit in viewport
252 if (type == TYPE_BACK)
253 tx = BORDER_PIXELS_WIDTH + figureWidthPx/2;
255 tx = BORDER_PIXELS_WIDTH - minX*scale;
259 if (figureHeightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
262 ty = BORDER_PIXELS_HEIGHT + figureHeightPx/2;
265 if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
266 // Origin has changed, fire event
273 // Calculate and store the transformation used
274 // (inverse is used in detecting clicks on objects)
275 g2transformation = new AffineTransform();
276 g2transformation.translate(translateX, translateY);
277 // Mirror position Y-axis upwards
278 g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
280 g2.transform(g2transformation);
282 // Set rendering hints appropriately
283 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
284 RenderingHints.VALUE_STROKE_NORMALIZE);
285 g2.setRenderingHint(RenderingHints.KEY_RENDERING,
286 RenderingHints.VALUE_RENDER_QUALITY);
287 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
288 RenderingHints.VALUE_ANTIALIAS_ON);
293 for (int i=0; i < figureShapes.size(); i++) {
294 RocketComponent c = figureComponents.get(i);
295 Shape s = figureShapes.get(i);
296 boolean selected = false;
298 // Check if component is in the selection
299 for (int j=0; j < selection.length; j++) {
300 if (c == selection[j]) {
306 // Set component color and line style
307 Color color = c.getColor();
309 color = Prefs.getDefaultColor(c.getClass());
313 LineStyle style = c.getLineStyle();
315 style = Prefs.getDefaultLineStyle(c.getClass());
317 float[] dashes = style.getDashes();
318 for (int j=0; j<dashes.length; j++) {
319 dashes[j] *= EXTRA_SCALE / scale;
323 g2.setStroke(new BasicStroke((float)(SELECTED_WIDTH*EXTRA_SCALE/scale),
324 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
325 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
326 RenderingHints.VALUE_STROKE_PURE);
328 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
329 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
330 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
331 RenderingHints.VALUE_STROKE_NORMALIZE);
337 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
338 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
339 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
340 RenderingHints.VALUE_STROKE_NORMALIZE);
344 String motorID = configuration.getMotorConfigurationID();
345 Color fillColor = Prefs.getMotorFillColor();
346 Color borderColor = Prefs.getMotorBorderColor();
347 Iterator<MotorMount> iterator = configuration.motorIterator();
348 while (iterator.hasNext()) {
349 MotorMount mount = iterator.next();
350 Motor motor = mount.getMotor(motorID);
351 double length = motor.getLength();
352 double radius = motor.getDiameter() / 2;
354 Coordinate[] position = ((RocketComponent)mount).toAbsolute(
355 new Coordinate(((RocketComponent)mount).getLength() +
356 mount.getMotorOverhang() - length));
358 for (int i=0; i < position.length; i++) {
359 position[i] = transformation.transform(position[i]);
362 for (Coordinate coord: position) {
364 if (type == TYPE_SIDE) {
365 s = new Rectangle2D.Double(EXTRA_SCALE*coord.x,
366 EXTRA_SCALE*(coord.y - radius), EXTRA_SCALE*length,
367 EXTRA_SCALE*2*radius);
369 s = new Ellipse2D.Double(EXTRA_SCALE*(coord.z-radius),
370 EXTRA_SCALE*(coord.y-radius), EXTRA_SCALE*2*radius,
371 EXTRA_SCALE*2*radius);
373 g2.setColor(fillColor);
375 g2.setColor(borderColor);
382 // Draw relative extras
383 for (FigureElement e: relativeExtra) {
384 e.paint(g2, scale/EXTRA_SCALE);
387 // Draw absolute extras
388 g2.setTransform(baseTransform);
389 Rectangle rect = this.getVisibleRect();
391 for (FigureElement e: absoluteExtra) {
392 e.paint(g2, 1.0, rect);
398 public RocketComponent[] getComponentsByPoint(double x, double y) {
399 // Calculate point in shapes' coordinates
400 Point2D.Double p = new Point2D.Double(x,y);
402 g2transformation.inverseTransform(p,p);
403 } catch (NoninvertibleTransformException e) {
404 return new RocketComponent[0];
407 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
409 for (int i=0; i<figureShapes.size(); i++) {
410 if (figureShapes.get(i).contains(p))
411 l.add(figureComponents.get(i));
413 return l.toArray(new RocketComponent[0]);
419 * Gets the shapes required to draw the component.
425 private Shape[] getShapes(RocketComponent component) {
428 // Find the appropriate method
431 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide",
432 RocketComponent.class, Transformation.class);
436 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack",
437 RocketComponent.class, Transformation.class);
441 throw new RuntimeException("Unknown figure type = "+type);
445 ExceptionHandler.handleErrorCondition("ERROR: Rocket figure paint method not found for "
450 return (Shape[])m.invokeStatic(component,transformation);
456 * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
457 * The bounds are stored in the variables minX, maxX and maxR.
459 private void calculateFigureBounds() {
460 Collection<Coordinate> bounds = configuration.getBounds();
462 if (bounds.isEmpty()) {
469 minX = Double.MAX_VALUE;
470 maxX = Double.MIN_VALUE;
472 for (Coordinate c: bounds) {
473 double x = c.x, r = MathUtil.hypot(c.y, c.z);
484 public double getBestZoom(Rectangle2D bounds) {
486 if (bounds.getWidth() > 0.0001)
487 zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth();
488 if (bounds.getHeight() > 0.0001)
489 zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight();
490 return Math.min(zh, zv);
495 * Calculates the necessary size of the figure and set the PreferredSize
496 * property accordingly.
498 private void calculateSize() {
499 calculateFigureBounds();
503 figureWidth = maxX-minX;
504 figureHeight = 2*maxR;
508 figureWidth = 2*maxR;
509 figureHeight = 2*maxR;
513 assert(false): "Should not occur, type="+type;
518 figureWidthPx = (int)(figureWidth * scale);
519 figureHeightPx = (int)(figureHeight * scale);
521 Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
522 figureHeightPx+2*BORDER_PIXELS_HEIGHT);
524 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
531 public Rectangle2D getDimensions() {
534 return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
537 return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
540 throw new RuntimeException("Illegal figure type = "+type);