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.BugException;
29 import net.sf.openrocket.util.Coordinate;
30 import net.sf.openrocket.util.LineStyle;
31 import net.sf.openrocket.util.MathUtil;
32 import net.sf.openrocket.util.Prefs;
33 import net.sf.openrocket.util.Reflection;
34 import net.sf.openrocket.util.Transformation;
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 if (figureHeightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
263 ty = BORDER_PIXELS_HEIGHT + figureHeightPx/2;
266 if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
267 // Origin has changed, fire event
274 // Calculate and store the transformation used
275 // (inverse is used in detecting clicks on objects)
276 g2transformation = new AffineTransform();
277 g2transformation.translate(translateX, translateY);
278 // Mirror position Y-axis upwards
279 g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
281 g2.transform(g2transformation);
283 // Set rendering hints appropriately
284 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
285 RenderingHints.VALUE_STROKE_NORMALIZE);
286 g2.setRenderingHint(RenderingHints.KEY_RENDERING,
287 RenderingHints.VALUE_RENDER_QUALITY);
288 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
289 RenderingHints.VALUE_ANTIALIAS_ON);
294 for (int i=0; i < figureShapes.size(); i++) {
295 RocketComponent c = figureComponents.get(i);
296 Shape s = figureShapes.get(i);
297 boolean selected = false;
299 // Check if component is in the selection
300 for (int j=0; j < selection.length; j++) {
301 if (c == selection[j]) {
307 // Set component color and line style
308 Color color = c.getColor();
310 color = Prefs.getDefaultColor(c.getClass());
314 LineStyle style = c.getLineStyle();
316 style = Prefs.getDefaultLineStyle(c.getClass());
318 float[] dashes = style.getDashes();
319 for (int j=0; j<dashes.length; j++) {
320 dashes[j] *= EXTRA_SCALE / scale;
324 g2.setStroke(new BasicStroke((float)(SELECTED_WIDTH*EXTRA_SCALE/scale),
325 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
326 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
327 RenderingHints.VALUE_STROKE_PURE);
329 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
330 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
331 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
332 RenderingHints.VALUE_STROKE_NORMALIZE);
338 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
339 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
340 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
341 RenderingHints.VALUE_STROKE_NORMALIZE);
345 String motorID = configuration.getMotorConfigurationID();
346 Color fillColor = Prefs.getMotorFillColor();
347 Color borderColor = Prefs.getMotorBorderColor();
348 Iterator<MotorMount> iterator = configuration.motorIterator();
349 while (iterator.hasNext()) {
350 MotorMount mount = iterator.next();
351 Motor motor = mount.getMotor(motorID);
352 double length = motor.getLength();
353 double radius = motor.getDiameter() / 2;
355 Coordinate[] position = ((RocketComponent)mount).toAbsolute(
356 new Coordinate(((RocketComponent)mount).getLength() +
357 mount.getMotorOverhang() - length));
359 for (int i=0; i < position.length; i++) {
360 position[i] = transformation.transform(position[i]);
363 for (Coordinate coord: position) {
365 if (type == TYPE_SIDE) {
366 s = new Rectangle2D.Double(EXTRA_SCALE*coord.x,
367 EXTRA_SCALE*(coord.y - radius), EXTRA_SCALE*length,
368 EXTRA_SCALE*2*radius);
370 s = new Ellipse2D.Double(EXTRA_SCALE*(coord.z-radius),
371 EXTRA_SCALE*(coord.y-radius), EXTRA_SCALE*2*radius,
372 EXTRA_SCALE*2*radius);
374 g2.setColor(fillColor);
376 g2.setColor(borderColor);
383 // Draw relative extras
384 for (FigureElement e: relativeExtra) {
385 e.paint(g2, scale/EXTRA_SCALE);
388 // Draw absolute extras
389 g2.setTransform(baseTransform);
390 Rectangle rect = this.getVisibleRect();
392 for (FigureElement e: absoluteExtra) {
393 e.paint(g2, 1.0, rect);
399 public RocketComponent[] getComponentsByPoint(double x, double y) {
400 // Calculate point in shapes' coordinates
401 Point2D.Double p = new Point2D.Double(x,y);
403 g2transformation.inverseTransform(p,p);
404 } catch (NoninvertibleTransformException e) {
405 return new RocketComponent[0];
408 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
410 for (int i=0; i<figureShapes.size(); i++) {
411 if (figureShapes.get(i).contains(p))
412 l.add(figureComponents.get(i));
414 return l.toArray(new RocketComponent[0]);
420 * Gets the shapes required to draw the component.
426 private Shape[] getShapes(RocketComponent component) {
429 // Find the appropriate method
432 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide",
433 RocketComponent.class, Transformation.class);
437 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack",
438 RocketComponent.class, Transformation.class);
442 throw new BugException("Unknown figure type = "+type);
446 ExceptionHandler.handleErrorCondition("ERROR: Rocket figure paint method not found for "
451 return (Shape[])m.invokeStatic(component,transformation);
457 * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
458 * The bounds are stored in the variables minX, maxX and maxR.
460 private void calculateFigureBounds() {
461 Collection<Coordinate> bounds = configuration.getBounds();
463 if (bounds.isEmpty()) {
470 minX = Double.MAX_VALUE;
471 maxX = Double.MIN_VALUE;
473 for (Coordinate c: bounds) {
474 double x = c.x, r = MathUtil.hypot(c.y, c.z);
485 public double getBestZoom(Rectangle2D bounds) {
487 if (bounds.getWidth() > 0.0001)
488 zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth();
489 if (bounds.getHeight() > 0.0001)
490 zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight();
491 return Math.min(zh, zv);
496 * Calculates the necessary size of the figure and set the PreferredSize
497 * property accordingly.
499 private void calculateSize() {
500 calculateFigureBounds();
504 figureWidth = maxX-minX;
505 figureHeight = 2*maxR;
509 figureWidth = 2*maxR;
510 figureHeight = 2*maxR;
514 assert(false): "Should not occur, type="+type;
519 figureWidthPx = (int)(figureWidth * scale);
520 figureHeightPx = (int)(figureHeight * scale);
522 Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
523 figureHeightPx+2*BORDER_PIXELS_HEIGHT);
525 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
532 public Rectangle2D getDimensions() {
535 return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
538 return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
541 throw new BugException("Illegal figure type = "+type);