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.util.ColorConversion;
24 import net.sf.openrocket.gui.util.SwingPreferences;
25 import net.sf.openrocket.motor.Motor;
26 import net.sf.openrocket.rocketcomponent.Configuration;
27 import net.sf.openrocket.rocketcomponent.MotorMount;
28 import net.sf.openrocket.rocketcomponent.RocketComponent;
29 import net.sf.openrocket.startup.Application;
30 import net.sf.openrocket.util.BugException;
31 import net.sf.openrocket.util.Coordinate;
32 import net.sf.openrocket.util.LineStyle;
33 import net.sf.openrocket.util.MathUtil;
34 import net.sf.openrocket.util.Reflection;
35 import net.sf.openrocket.util.Transformation;
38 * A <code>ScaleFigure</code> that draws a complete rocket. Extra information can
39 * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)},
40 * {@link #clearRelativeExtra()}.
42 * @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 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 protected 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);
104 * Set the configuration displayed by the figure. It may use the same or different rocket.
106 * @param configuration the configuration to display.
108 public void setConfiguration(Configuration configuration) {
109 this.configuration = configuration;
115 public Dimension getOrigin() {
116 return new Dimension((int) translateX, (int) translateY);
120 public double getFigureHeight() {
125 public double getFigureWidth() {
130 public RocketComponent[] getSelection() {
134 public void setSelection(RocketComponent[] selection) {
135 if (selection == null) {
136 this.selection = new RocketComponent[0];
138 this.selection = selection;
144 public double getRotation() {
148 public Transformation getRotateTransformation() {
149 return transformation;
152 public void setRotation(double rot) {
153 if (MathUtil.equals(rotation, rot))
156 this.transformation = Transformation.rotate_x(rotation);
161 public int getType() {
165 public void setType(int type) {
166 if (type != TYPE_BACK && type != TYPE_SIDE) {
167 throw new IllegalArgumentException("Illegal type: " + type);
169 if (this.type == type)
180 * Updates the figure shapes and figure size.
183 public void updateFigure() {
184 figureShapes.clear();
185 figureComponents.clear();
189 // Get shapes for all active components
190 for (RocketComponent c : configuration) {
191 Shape[] s = getShapes(c);
192 for (int i = 0; i < s.length; i++) {
193 figureShapes.add(s[i]);
194 figureComponents.add(c);
203 public void addRelativeExtra(FigureElement p) {
204 relativeExtra.add(p);
207 public void removeRelativeExtra(FigureElement p) {
208 relativeExtra.remove(p);
211 public void clearRelativeExtra() {
212 relativeExtra.clear();
216 public void addAbsoluteExtra(FigureElement p) {
217 absoluteExtra.add(p);
220 public void removeAbsoluteExtra(FigureElement p) {
221 absoluteExtra.remove(p);
224 public void clearAbsoluteExtra() {
225 absoluteExtra.clear();
230 * Paints the rocket on to the Graphics element.
232 * Warning: If paintComponent is used outside the normal Swing usage, some Swing
233 * dependent parameters may be left wrong (mainly transformation). If it is used,
234 * the RocketFigure should be repainted immediately afterwards.
237 public void paintComponent(Graphics g) {
238 super.paintComponent(g);
239 Graphics2D g2 = (Graphics2D) g;
242 AffineTransform baseTransform = g2.getTransform();
244 // Update figure shapes if necessary
245 if (figureShapes == null)
250 // Calculate translation for figure centering
251 if (figureWidthPx + 2 * borderPixelsWidth < getWidth()) {
253 // Figure fits in the viewport
254 if (type == TYPE_BACK)
257 tx = (getWidth() - figureWidthPx) / 2 - minX * scale;
261 // Figure does not fit in viewport
262 if (type == TYPE_BACK)
263 tx = borderPixelsWidth + figureWidthPx / 2;
265 tx = borderPixelsWidth - minX * scale;
269 ty = computeTy(figureHeightPx);
271 if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) {
272 // Origin has changed, fire event
279 // Calculate and store the transformation used
280 // (inverse is used in detecting clicks on objects)
281 g2transformation = new AffineTransform();
282 g2transformation.translate(translateX, translateY);
283 // Mirror position Y-axis upwards
284 g2transformation.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE);
286 g2.transform(g2transformation);
288 // Set rendering hints appropriately
289 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
290 RenderingHints.VALUE_STROKE_NORMALIZE);
291 g2.setRenderingHint(RenderingHints.KEY_RENDERING,
292 RenderingHints.VALUE_RENDER_QUALITY);
293 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
294 RenderingHints.VALUE_ANTIALIAS_ON);
299 for (int i = 0; i < figureShapes.size(); i++) {
300 RocketComponent c = figureComponents.get(i);
301 Shape s = figureShapes.get(i);
302 boolean selected = false;
304 // Check if component is in the selection
305 for (int j = 0; j < selection.length; j++) {
306 if (c == selection[j]) {
312 // Set component color and line style
313 net.sf.openrocket.util.Color color = c.getColor();
315 color = Application.getPreferences().getDefaultColor(c.getClass());
317 g2.setColor(ColorConversion.toAwtColor(color));
319 LineStyle style = c.getLineStyle();
321 style = Application.getPreferences().getDefaultLineStyle(c.getClass());
323 float[] dashes = style.getDashes();
324 for (int j = 0; j < dashes.length; j++) {
325 dashes[j] *= EXTRA_SCALE / scale;
329 g2.setStroke(new BasicStroke((float) (SELECTED_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_PURE);
334 g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale),
335 BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, dashes, 0));
336 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
337 RenderingHints.VALUE_STROKE_NORMALIZE);
343 g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale),
344 BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
345 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
346 RenderingHints.VALUE_STROKE_NORMALIZE);
350 String motorID = configuration.getMotorConfigurationID();
351 Color fillColor = ((SwingPreferences)Application.getPreferences()).getMotorFillColor();
352 Color borderColor = ((SwingPreferences)Application.getPreferences()).getMotorBorderColor();
353 Iterator<MotorMount> iterator = configuration.motorIterator();
354 while (iterator.hasNext()) {
355 MotorMount mount = iterator.next();
356 Motor motor = mount.getMotor(motorID);
357 double length = motor.getLength();
358 double radius = motor.getDiameter() / 2;
360 Coordinate[] position = ((RocketComponent) mount).toAbsolute(
361 new Coordinate(((RocketComponent) mount).getLength() +
362 mount.getMotorOverhang() - length));
364 for (int i = 0; i < position.length; i++) {
365 position[i] = transformation.transform(position[i]);
368 for (Coordinate coord : position) {
370 if (type == TYPE_SIDE) {
371 s = new Rectangle2D.Double(EXTRA_SCALE * coord.x,
372 EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * length,
373 EXTRA_SCALE * 2 * radius);
375 s = new Ellipse2D.Double(EXTRA_SCALE * (coord.z - radius),
376 EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * 2 * radius,
377 EXTRA_SCALE * 2 * radius);
379 g2.setColor(fillColor);
381 g2.setColor(borderColor);
388 // Draw relative extras
389 for (FigureElement e : relativeExtra) {
390 e.paint(g2, scale / EXTRA_SCALE);
393 // Draw absolute extras
394 g2.setTransform(baseTransform);
395 Rectangle rect = this.getVisibleRect();
397 for (FigureElement e : absoluteExtra) {
398 e.paint(g2, 1.0, rect);
403 protected double computeTy(int heightPx) {
405 if (heightPx + 2 * borderPixelsHeight < getHeight()) {
406 ty = getHeight() / 2;
408 ty = borderPixelsHeight + heightPx / 2;
414 public RocketComponent[] getComponentsByPoint(double x, double y) {
415 // Calculate point in shapes' coordinates
416 Point2D.Double p = new Point2D.Double(x, y);
418 g2transformation.inverseTransform(p, p);
419 } catch (NoninvertibleTransformException e) {
420 return new RocketComponent[0];
423 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
425 for (int i = 0; i < figureShapes.size(); i++) {
426 if (figureShapes.get(i).contains(p))
427 l.add(figureComponents.get(i));
429 return l.toArray(new RocketComponent[0]);
435 * Gets the shapes required to draw the component.
441 private Shape[] getShapes(RocketComponent component) {
444 // Find the appropriate method
447 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide",
448 RocketComponent.class, Transformation.class);
452 m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack",
453 RocketComponent.class, Transformation.class);
457 throw new BugException("Unknown figure type = " + type);
461 Application.getExceptionHandler().handleErrorCondition("ERROR: Rocket figure paint method not found for "
466 return (Shape[]) m.invokeStatic(component, transformation);
472 * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
473 * The bounds are stored in the variables minX, maxX and maxR.
475 private void calculateFigureBounds() {
476 Collection<Coordinate> bounds = configuration.getBounds();
478 if (bounds.isEmpty()) {
485 minX = Double.MAX_VALUE;
486 maxX = Double.MIN_VALUE;
488 for (Coordinate c : bounds) {
489 double x = c.x, r = MathUtil.hypot(c.y, c.z);
500 public double getBestZoom(Rectangle2D bounds) {
501 double zh = 1, zv = 1;
502 if (bounds.getWidth() > 0.0001)
503 zh = (getWidth() - 2 * borderPixelsWidth) / bounds.getWidth();
504 if (bounds.getHeight() > 0.0001)
505 zv = (getHeight() - 2 * borderPixelsHeight) / bounds.getHeight();
506 return Math.min(zh, zv);
512 * Calculates the necessary size of the figure and set the PreferredSize
513 * property accordingly.
515 private void calculateSize() {
516 calculateFigureBounds();
520 figureWidth = maxX - minX;
521 figureHeight = 2 * maxR;
525 figureWidth = 2 * maxR;
526 figureHeight = 2 * maxR;
530 assert (false) : "Should not occur, type=" + type;
535 figureWidthPx = (int) (figureWidth * scale);
536 figureHeightPx = (int) (figureHeight * scale);
538 Dimension d = new Dimension(figureWidthPx + 2 * borderPixelsWidth,
539 figureHeightPx + 2 * borderPixelsHeight);
541 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
548 public Rectangle2D getDimensions() {
551 return new Rectangle2D.Double(minX, -maxR, maxX - minX, 2 * maxR);
554 return new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR);
557 throw new BugException("Illegal figure type = " + type);