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