create changelog entry
[debian/openrocket] / core / src / net / sf / openrocket / gui / scalefigure / RocketFigure.java
1 package net.sf.openrocket.gui.scalefigure;
2
3
4 import java.awt.BasicStroke;
5 import java.awt.Color;
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;
21
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;
36
37 /**
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()}.
41  * 
42  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
43  */
44 public class RocketFigure extends AbstractScaleFigure {
45         private static final long serialVersionUID = 1L;
46         
47         private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure";
48         private static final String ROCKET_FIGURE_SUFFIX = "Shapes";
49         
50         public static final int TYPE_SIDE = 1;
51         public static final int TYPE_BACK = 2;
52         
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;
56         
57
58         private Configuration configuration;
59         private RocketComponent[] selection = new RocketComponent[0];
60         
61         private int type = TYPE_SIDE;
62         
63         private double rotation;
64         private Transformation transformation;
65         
66         private double translateX, translateY;
67         
68
69
70         /*
71          * figureComponents contains the corresponding RocketComponents of the figureShapes
72          */
73         private final ArrayList<Shape> figureShapes = new ArrayList<Shape>();
74         private final ArrayList<RocketComponent> figureComponents =
75                         new ArrayList<RocketComponent>();
76         
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;
81         
82         private AffineTransform g2transformation = null;
83         
84         private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
85         private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
86         
87         
88         /**
89          * Creates a new rocket figure.
90          */
91         public RocketFigure(Configuration configuration) {
92                 super();
93                 
94                 this.configuration = configuration;
95                 
96                 this.rotation = 0.0;
97                 this.transformation = Transformation.rotate_x(0.0);
98                 
99                 updateFigure();
100         }
101         
102         
103         /**
104          * Set the configuration displayed by the figure.  It may use the same or different rocket.
105          * 
106          * @param configuration         the configuration to display.
107          */
108         public void setConfiguration(Configuration configuration) {
109                 this.configuration = configuration;
110                 updateFigure();
111         }
112         
113         
114         @Override
115         public Dimension getOrigin() {
116                 return new Dimension((int) translateX, (int) translateY);
117         }
118         
119         @Override
120         public double getFigureHeight() {
121                 return figureHeight;
122         }
123         
124         @Override
125         public double getFigureWidth() {
126                 return figureWidth;
127         }
128         
129         
130         public RocketComponent[] getSelection() {
131                 return selection;
132         }
133         
134         public void setSelection(RocketComponent[] selection) {
135                 if (selection == null) {
136                         this.selection = new RocketComponent[0];
137                 } else {
138                         this.selection = selection;
139                 }
140                 updateFigure();
141         }
142         
143         
144         public double getRotation() {
145                 return rotation;
146         }
147         
148         public Transformation getRotateTransformation() {
149                 return transformation;
150         }
151         
152         public void setRotation(double rot) {
153                 if (MathUtil.equals(rotation, rot))
154                         return;
155                 this.rotation = rot;
156                 this.transformation = Transformation.rotate_x(rotation);
157                 updateFigure();
158         }
159         
160         
161         public int getType() {
162                 return type;
163         }
164         
165         public void setType(int type) {
166                 if (type != TYPE_BACK && type != TYPE_SIDE) {
167                         throw new IllegalArgumentException("Illegal type: " + type);
168                 }
169                 if (this.type == type)
170                         return;
171                 this.type = type;
172                 updateFigure();
173         }
174         
175         
176
177
178
179         /**
180          * Updates the figure shapes and figure size.
181          */
182         @Override
183         public void updateFigure() {
184                 figureShapes.clear();
185                 figureComponents.clear();
186                 
187                 calculateSize();
188                 
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);
195                         }
196                 }
197                 
198                 repaint();
199                 fireChangeEvent();
200         }
201         
202         
203         public void addRelativeExtra(FigureElement p) {
204                 relativeExtra.add(p);
205         }
206         
207         public void removeRelativeExtra(FigureElement p) {
208                 relativeExtra.remove(p);
209         }
210         
211         public void clearRelativeExtra() {
212                 relativeExtra.clear();
213         }
214         
215         
216         public void addAbsoluteExtra(FigureElement p) {
217                 absoluteExtra.add(p);
218         }
219         
220         public void removeAbsoluteExtra(FigureElement p) {
221                 absoluteExtra.remove(p);
222         }
223         
224         public void clearAbsoluteExtra() {
225                 absoluteExtra.clear();
226         }
227         
228         
229         /**
230          * Paints the rocket on to the Graphics element.
231          * <p>
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.
235          */
236         @Override
237         public void paintComponent(Graphics g) {
238                 super.paintComponent(g);
239                 Graphics2D g2 = (Graphics2D) g;
240                 
241
242                 AffineTransform baseTransform = g2.getTransform();
243                 
244                 // Update figure shapes if necessary
245                 if (figureShapes == null)
246                         updateFigure();
247                 
248
249                 double tx, ty;
250                 // Calculate translation for figure centering
251                 if (figureWidthPx + 2 * borderPixelsWidth < getWidth()) {
252                         
253                         // Figure fits in the viewport
254                         if (type == TYPE_BACK)
255                                 tx = getWidth() / 2;
256                         else
257                                 tx = (getWidth() - figureWidthPx) / 2 - minX * scale;
258                         
259                 } else {
260                         
261                         // Figure does not fit in viewport
262                         if (type == TYPE_BACK)
263                                 tx = borderPixelsWidth + figureWidthPx / 2;
264                         else
265                                 tx = borderPixelsWidth - minX * scale;
266                         
267                 }
268                 
269                 ty = computeTy(figureHeightPx);
270                 
271                 if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) {
272                         // Origin has changed, fire event
273                         translateX = tx;
274                         translateY = ty;
275                         fireChangeEvent();
276                 }
277                 
278
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);
285                 
286                 g2.transform(g2transformation);
287                 
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);
295                 
296
297                 // Draw all shapes
298                 
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;
303                         
304                         // Check if component is in the selection
305                         for (int j = 0; j < selection.length; j++) {
306                                 if (c == selection[j]) {
307                                         selected = true;
308                                         break;
309                                 }
310                         }
311                         
312                         // Set component color and line style
313                         net.sf.openrocket.util.Color color = c.getColor();
314                         if (color == null) {
315                                 color = Application.getPreferences().getDefaultColor(c.getClass());
316                         }
317                         g2.setColor(ColorConversion.toAwtColor(color));
318                         
319                         LineStyle style = c.getLineStyle();
320                         if (style == null)
321                                 style = Application.getPreferences().getDefaultLineStyle(c.getClass());
322                         
323                         float[] dashes = style.getDashes();
324                         for (int j = 0; j < dashes.length; j++) {
325                                 dashes[j] *= EXTRA_SCALE / scale;
326                         }
327                         
328                         if (selected) {
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);
333                         } else {
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);
338                         }
339                         g2.draw(s);
340                         
341                 }
342                 
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);
347                 
348
349                 // Draw motors
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;
359                         
360                         Coordinate[] position = ((RocketComponent) mount).toAbsolute(
361                                         new Coordinate(((RocketComponent) mount).getLength() +
362                                                         mount.getMotorOverhang() - length));
363                         
364                         for (int i = 0; i < position.length; i++) {
365                                 position[i] = transformation.transform(position[i]);
366                         }
367                         
368                         for (Coordinate coord : position) {
369                                 Shape s;
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);
374                                 } else {
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);
378                                 }
379                                 g2.setColor(fillColor);
380                                 g2.fill(s);
381                                 g2.setColor(borderColor);
382                                 g2.draw(s);
383                         }
384                 }
385                 
386
387
388                 // Draw relative extras
389                 for (FigureElement e : relativeExtra) {
390                         e.paint(g2, scale / EXTRA_SCALE);
391                 }
392                 
393                 // Draw absolute extras
394                 g2.setTransform(baseTransform);
395                 Rectangle rect = this.getVisibleRect();
396                 
397                 for (FigureElement e : absoluteExtra) {
398                         e.paint(g2, 1.0, rect);
399                 }
400                 
401         }
402         
403         protected double computeTy(int heightPx) {
404                 final double ty;
405                 if (heightPx + 2 * borderPixelsHeight < getHeight()) {
406                         ty = getHeight() / 2;
407                 } else {
408                         ty = borderPixelsHeight + heightPx / 2;
409                 }
410                 return ty;
411         }
412         
413         
414         public RocketComponent[] getComponentsByPoint(double x, double y) {
415                 // Calculate point in shapes' coordinates
416                 Point2D.Double p = new Point2D.Double(x, y);
417                 try {
418                         g2transformation.inverseTransform(p, p);
419                 } catch (NoninvertibleTransformException e) {
420                         return new RocketComponent[0];
421                 }
422                 
423                 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
424                 
425                 for (int i = 0; i < figureShapes.size(); i++) {
426                         if (figureShapes.get(i).contains(p))
427                                 l.add(figureComponents.get(i));
428                 }
429                 return l.toArray(new RocketComponent[0]);
430         }
431         
432         
433
434         /**
435          * Gets the shapes required to draw the component.
436          * 
437          * @param component
438          * @param params
439          * @return
440          */
441         private Shape[] getShapes(RocketComponent component) {
442                 Reflection.Method m;
443                 
444                 // Find the appropriate method
445                 switch (type) {
446                 case TYPE_SIDE:
447                         m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide",
448                                         RocketComponent.class, Transformation.class);
449                         break;
450                 
451                 case TYPE_BACK:
452                         m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack",
453                                         RocketComponent.class, Transformation.class);
454                         break;
455                 
456                 default:
457                         throw new BugException("Unknown figure type = " + type);
458                 }
459                 
460                 if (m == null) {
461                         Application.getExceptionHandler().handleErrorCondition("ERROR: Rocket figure paint method not found for "
462                                         + component);
463                         return new Shape[0];
464                 }
465                 
466                 return (Shape[]) m.invokeStatic(component, transformation);
467         }
468         
469         
470
471         /**
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.
474          */
475         private void calculateFigureBounds() {
476                 Collection<Coordinate> bounds = configuration.getBounds();
477                 
478                 if (bounds.isEmpty()) {
479                         minX = 0;
480                         maxX = 0;
481                         maxR = 0;
482                         return;
483                 }
484                 
485                 minX = Double.MAX_VALUE;
486                 maxX = Double.MIN_VALUE;
487                 maxR = 0;
488                 for (Coordinate c : bounds) {
489                         double x = c.x, r = MathUtil.hypot(c.y, c.z);
490                         if (x < minX)
491                                 minX = x;
492                         if (x > maxX)
493                                 maxX = x;
494                         if (r > maxR)
495                                 maxR = r;
496                 }
497         }
498         
499         
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);
507         }
508         
509         
510
511         /**
512          * Calculates the necessary size of the figure and set the PreferredSize 
513          * property accordingly.
514          */
515         private void calculateSize() {
516                 calculateFigureBounds();
517                 
518                 switch (type) {
519                 case TYPE_SIDE:
520                         figureWidth = maxX - minX;
521                         figureHeight = 2 * maxR;
522                         break;
523                 
524                 case TYPE_BACK:
525                         figureWidth = 2 * maxR;
526                         figureHeight = 2 * maxR;
527                         break;
528                 
529                 default:
530                         assert (false) : "Should not occur, type=" + type;
531                         figureWidth = 0;
532                         figureHeight = 0;
533                 }
534                 
535                 figureWidthPx = (int) (figureWidth * scale);
536                 figureHeightPx = (int) (figureHeight * scale);
537                 
538                 Dimension d = new Dimension(figureWidthPx + 2 * borderPixelsWidth,
539                                 figureHeightPx + 2 * borderPixelsHeight);
540                 
541                 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
542                         setPreferredSize(d);
543                         setMinimumSize(d);
544                         revalidate();
545                 }
546         }
547         
548         public Rectangle2D getDimensions() {
549                 switch (type) {
550                 case TYPE_SIDE:
551                         return new Rectangle2D.Double(minX, -maxR, maxX - minX, 2 * maxR);
552                         
553                 case TYPE_BACK:
554                         return new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR);
555                         
556                 default:
557                         throw new BugException("Illegal figure type = " + type);
558                 }
559         }
560         
561 }