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>
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 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);
103 * Set the configuration displayed by the figure. It may use the same or different rocket.
105 * @param configuration the configuration to display.
107 public void setConfiguration(Configuration configuration) {
108 this.configuration = configuration;
114 public Dimension getOrigin() {
115 return new Dimension((int) translateX, (int) translateY);
119 public double getFigureHeight() {
124 public double getFigureWidth() {
129 public RocketComponent[] getSelection() {
133 public void setSelection(RocketComponent[] selection) {
134 if (selection == null) {
135 this.selection = new RocketComponent[0];
137 this.selection = selection;
143 public double getRotation() {
147 public Transformation getRotateTransformation() {
148 return transformation;
151 public void setRotation(double rot) {
152 if (MathUtil.equals(rotation, rot))
155 this.transformation = Transformation.rotate_x(rotation);
160 public int getType() {
164 public void setType(int type) {
165 if (type != TYPE_BACK && type != TYPE_SIDE) {
166 throw new IllegalArgumentException("Illegal type: " + type);
168 if (this.type == type)
179 * Updates the figure shapes and figure size.
182 public void updateFigure() {
183 figureShapes.clear();
184 figureComponents.clear();
188 // Get shapes for all active components
189 for (RocketComponent c : configuration) {
190 Shape[] s = getShapes(c);
191 for (int i = 0; i < s.length; i++) {
192 figureShapes.add(s[i]);
193 figureComponents.add(c);
202 public void addRelativeExtra(FigureElement p) {
203 relativeExtra.add(p);
206 public void removeRelativeExtra(FigureElement p) {
207 relativeExtra.remove(p);
210 public void clearRelativeExtra() {
211 relativeExtra.clear();
215 public void addAbsoluteExtra(FigureElement p) {
216 absoluteExtra.add(p);
219 public void removeAbsoluteExtra(FigureElement p) {
220 absoluteExtra.remove(p);
223 public void clearAbsoluteExtra() {
224 absoluteExtra.clear();
229 * Paints the rocket on to the Graphics element.
231 * Warning: If paintComponent is used outside the normal Swing usage, some Swing
232 * dependent parameters may be left wrong (mainly transformation). If it is used,
233 * the RocketFigure should be repainted immediately afterwards.
236 public void paintComponent(Graphics g) {
237 super.paintComponent(g);
238 Graphics2D g2 = (Graphics2D) g;
241 AffineTransform baseTransform = g2.getTransform();
243 // Update figure shapes if necessary
244 if (figureShapes == null)
249 // Calculate translation for figure centering
250 if (figureWidthPx + 2 * borderPixelsWidth < getWidth()) {
252 // Figure fits in the viewport
253 if (type == TYPE_BACK)
256 tx = (getWidth() - figureWidthPx) / 2 - minX * scale;
260 // Figure does not fit in viewport
261 if (type == TYPE_BACK)
262 tx = borderPixelsWidth + figureWidthPx / 2;
264 tx = borderPixelsWidth - minX * scale;
268 ty = computeTy(figureHeightPx);
270 if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) {
271 // Origin has changed, fire event
278 // Calculate and store the transformation used
279 // (inverse is used in detecting clicks on objects)
280 g2transformation = new AffineTransform();
281 g2transformation.translate(translateX, translateY);
282 // Mirror position Y-axis upwards
283 g2transformation.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE);
285 g2.transform(g2transformation);
287 // Set rendering hints appropriately
288 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
289 RenderingHints.VALUE_STROKE_NORMALIZE);
290 g2.setRenderingHint(RenderingHints.KEY_RENDERING,
291 RenderingHints.VALUE_RENDER_QUALITY);
292 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
293 RenderingHints.VALUE_ANTIALIAS_ON);
298 for (int i = 0; i < figureShapes.size(); i++) {
299 RocketComponent c = figureComponents.get(i);
300 Shape s = figureShapes.get(i);
301 boolean selected = false;
303 // Check if component is in the selection
304 for (int j = 0; j < selection.length; j++) {
305 if (c == selection[j]) {
311 // Set component color and line style
312 Color color = c.getColor();
314 color = Prefs.getDefaultColor(c.getClass());
318 LineStyle style = c.getLineStyle();
320 style = Prefs.getDefaultLineStyle(c.getClass());
322 float[] dashes = style.getDashes();
323 for (int j = 0; j < dashes.length; j++) {
324 dashes[j] *= EXTRA_SCALE / scale;
328 g2.setStroke(new BasicStroke((float) (SELECTED_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_PURE);
333 g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale),
334 BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, dashes, 0));
335 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
336 RenderingHints.VALUE_STROKE_NORMALIZE);
342 g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale),
343 BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
344 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
345 RenderingHints.VALUE_STROKE_NORMALIZE);
349 String motorID = configuration.getMotorConfigurationID();
350 Color fillColor = Prefs.getMotorFillColor();
351 Color borderColor = Prefs.getMotorBorderColor();
352 Iterator<MotorMount> iterator = configuration.motorIterator();
353 while (iterator.hasNext()) {
354 MotorMount mount = iterator.next();
355 Motor motor = mount.getMotor(motorID);
356 double length = motor.getLength();
357 double radius = motor.getDiameter() / 2;
359 Coordinate[] position = ((RocketComponent) mount).toAbsolute(
360 new Coordinate(((RocketComponent) mount).getLength() +
361 mount.getMotorOverhang() - length));
363 for (int i = 0; i < position.length; i++) {
364 position[i] = transformation.transform(position[i]);
367 for (Coordinate coord : position) {
369 if (type == TYPE_SIDE) {
370 s = new Rectangle2D.Double(EXTRA_SCALE * coord.x,
371 EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * length,
372 EXTRA_SCALE * 2 * radius);
374 s = new Ellipse2D.Double(EXTRA_SCALE * (coord.z - radius),
375 EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * 2 * radius,
376 EXTRA_SCALE * 2 * radius);
378 g2.setColor(fillColor);
380 g2.setColor(borderColor);
387 // Draw relative extras
388 for (FigureElement e : relativeExtra) {
389 e.paint(g2, scale / EXTRA_SCALE);
392 // Draw absolute extras
393 g2.setTransform(baseTransform);
394 Rectangle rect = this.getVisibleRect();
396 for (FigureElement e : absoluteExtra) {
397 e.paint(g2, 1.0, rect);
402 protected double computeTy(int heightPx) {
404 if (heightPx + 2 * borderPixelsHeight < getHeight()) {
405 ty = getHeight() / 2;
407 ty = borderPixelsHeight + heightPx / 2;
413 public RocketComponent[] getComponentsByPoint(double x, double y) {
414 // Calculate point in shapes' coordinates
415 Point2D.Double p = new Point2D.Double(x, y);
417 g2transformation.inverseTransform(p, p);
418 } catch (NoninvertibleTransformException e) {
419 return new RocketComponent[0];
422 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
424 for (int i = 0; i < figureShapes.size(); i++) {
425 if (figureShapes.get(i).contains(p))
426 l.add(figureComponents.get(i));
428 return l.toArray(new RocketComponent[0]);
434 * Gets the shapes required to draw the component.
440 private Shape[] getShapes(RocketComponent component) {
443 // Find the appropriate method
446 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide",
447 RocketComponent.class, Transformation.class);
451 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack",
452 RocketComponent.class, Transformation.class);
456 throw new BugException("Unknown figure type = " + type);
460 ExceptionHandler.handleErrorCondition("ERROR: Rocket figure paint method not found for "
465 return (Shape[]) m.invokeStatic(component, transformation);
471 * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
472 * The bounds are stored in the variables minX, maxX and maxR.
474 private void calculateFigureBounds() {
475 Collection<Coordinate> bounds = configuration.getBounds();
477 if (bounds.isEmpty()) {
484 minX = Double.MAX_VALUE;
485 maxX = Double.MIN_VALUE;
487 for (Coordinate c : bounds) {
488 double x = c.x, r = MathUtil.hypot(c.y, c.z);
499 public double getBestZoom(Rectangle2D bounds) {
500 double zh = 1, zv = 1;
501 if (bounds.getWidth() > 0.0001)
502 zh = (getWidth() - 2 * borderPixelsWidth) / bounds.getWidth();
503 if (bounds.getHeight() > 0.0001)
504 zv = (getHeight() - 2 * borderPixelsHeight) / bounds.getHeight();
505 return Math.min(zh, zv);
511 * Calculates the necessary size of the figure and set the PreferredSize
512 * property accordingly.
514 private void calculateSize() {
515 calculateFigureBounds();
519 figureWidth = maxX - minX;
520 figureHeight = 2 * maxR;
524 figureWidth = 2 * maxR;
525 figureHeight = 2 * maxR;
529 assert (false) : "Should not occur, type=" + type;
534 figureWidthPx = (int) (figureWidth * scale);
535 figureHeightPx = (int) (figureHeight * scale);
537 Dimension d = new Dimension(figureWidthPx + 2 * borderPixelsWidth,
538 figureHeightPx + 2 * borderPixelsHeight);
540 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
547 public Rectangle2D getDimensions() {
550 return new Rectangle2D.Double(minX, -maxR, maxX - minX, 2 * maxR);
553 return new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR);
556 throw new BugException("Illegal figure type = " + type);