Updates for 0.9.5
[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
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 final 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         private 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                 calculateSize();
100                 updateFigure();
101         }
102         
103         
104         
105         public Dimension getOrigin() {
106                 return new Dimension((int)translateX, (int)translateY);
107         }
108         
109         @Override
110         public double getFigureHeight() {
111                 return figureHeight;
112         }
113
114         @Override
115         public double getFigureWidth() {
116                 return figureWidth;
117         }
118
119         
120         public RocketComponent[] getSelection() {
121                 return selection;
122         }
123         
124         public void setSelection(RocketComponent[] selection) {
125                 if (selection == null) {
126                         this.selection = new RocketComponent[0];
127                 } else {
128                         this.selection = selection;
129                 }
130                 updateFigure();
131         }
132         
133         
134         public double getRotation() {
135                 return rotation;
136         }
137         
138         public Transformation getRotateTransformation() {
139                 return transformation;
140         }
141         
142         public void setRotation(double rot) {
143                 if (MathUtil.equals(rotation, rot))
144                         return;
145                 this.rotation = rot;
146                 this.transformation = Transformation.rotate_x(rotation);
147                 updateFigure();
148         }
149         
150         
151         public int getType() {
152                 return type;
153         }
154         
155         public void setType(int type) {
156                 if (type != TYPE_BACK && type != TYPE_SIDE) {
157                         throw new IllegalArgumentException("Illegal type: "+type);
158                 }
159                 if (this.type == type)
160                         return;
161                 this.type = type;
162                 updateFigure();
163         }
164
165         
166         
167         
168
169
170         /**
171          * Updates the figure shapes and figure size.
172          */
173         @Override
174         public void updateFigure() {
175                 figureShapes.clear();
176                 figureComponents.clear();
177                 
178                 calculateSize();
179
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);
186                         }
187                 }
188                 
189                 repaint();
190                 fireChangeEvent();
191         }
192
193         
194         public void addRelativeExtra(FigureElement p) {
195                 relativeExtra.add(p);
196         }
197         
198         public void removeRelativeExtra(FigureElement p) {
199                 relativeExtra.remove(p);
200         }
201         
202         public void clearRelativeExtra() {
203                 relativeExtra.clear();
204         }
205         
206         
207         public void addAbsoluteExtra(FigureElement p) {
208                 absoluteExtra.add(p);
209         }
210         
211         public void removeAbsoluteExtra(FigureElement p) {
212                 absoluteExtra.remove(p);
213         }
214         
215         public void clearAbsoluteExtra() {
216                 absoluteExtra.clear();
217         }
218         
219
220         /**
221          * Paints the rocket on to the Graphics element.
222          * <p>
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.
226          */
227         @Override
228         public void paintComponent(Graphics g) {
229                 super.paintComponent(g);
230                 Graphics2D g2 = (Graphics2D)g;
231                 
232                 
233                 AffineTransform baseTransform = g2.getTransform();
234                 
235                 // Update figure shapes if necessary
236                 if (figureShapes == null)
237                         updateFigure();
238
239
240                 double tx, ty;
241                 // Calculate translation for figure centering
242                 if (figureWidthPx + 2*BORDER_PIXELS_WIDTH < getWidth()) {
243
244                         // Figure fits in the viewport
245                         if (type == TYPE_BACK)
246                                 tx = getWidth()/2;
247                         else 
248                                 tx = (getWidth()-figureWidthPx)/2 - minX*scale;
249
250                 } else {
251
252                         // Figure does not fit in viewport
253                         if (type == TYPE_BACK)
254                                 tx = BORDER_PIXELS_WIDTH + figureWidthPx/2;
255                         else 
256                                 tx = BORDER_PIXELS_WIDTH - minX*scale;
257                         
258                 }
259                 
260                 if (figureHeightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
261                          ty = getHeight()/2;
262                 } else {
263                          ty = BORDER_PIXELS_HEIGHT + figureHeightPx/2;
264                 }
265                 
266                 if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
267                         // Origin has changed, fire event
268                         translateX = tx;
269                         translateY = ty;
270                         fireChangeEvent();
271                 }
272                 
273
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);
280
281                 g2.transform(g2transformation);
282
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);
290
291
292                 // Draw all shapes
293                 
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;
298                         
299                         // Check if component is in the selection
300                         for (int j=0; j < selection.length; j++) {
301                                 if (c == selection[j]) {
302                                         selected = true;
303                                         break;
304                                 }
305                         }
306                         
307                         // Set component color and line style
308                         Color color = c.getColor();
309                         if (color == null) {
310                                 color = Prefs.getDefaultColor(c.getClass());
311                         }
312                         g2.setColor(color);
313
314                         LineStyle style = c.getLineStyle();
315                         if (style == null)
316                                 style = Prefs.getDefaultLineStyle(c.getClass());
317                         
318                         float[] dashes = style.getDashes();
319                         for (int j=0; j<dashes.length; j++) {
320                                 dashes[j] *= EXTRA_SCALE / scale;
321                         }
322                         
323                         if (selected) {
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);
328                         } else {
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);
333                         }
334                         g2.draw(s);
335                         
336                 }
337                 
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);
342
343                 
344                 // Draw motors
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;
354                         
355                         Coordinate[] position = ((RocketComponent)mount).toAbsolute(
356                                         new Coordinate(((RocketComponent)mount).getLength() + 
357                                                         mount.getMotorOverhang() - length));
358                         
359                         for (int i=0; i < position.length; i++) {
360                                 position[i] = transformation.transform(position[i]);
361                         }
362                         
363                         for (Coordinate coord: position) {
364                                 Shape s;
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);
369                                 } else {
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);
373                                 }
374                                 g2.setColor(fillColor);
375                                 g2.fill(s);
376                                 g2.setColor(borderColor);
377                                 g2.draw(s);
378                         }
379                 }
380                 
381                 
382                 
383                 // Draw relative extras
384                 for (FigureElement e: relativeExtra) {
385                         e.paint(g2, scale/EXTRA_SCALE);
386                 }
387
388                 // Draw absolute extras
389                 g2.setTransform(baseTransform);
390                 Rectangle rect = this.getVisibleRect();
391                 
392                 for (FigureElement e: absoluteExtra) {
393                         e.paint(g2, 1.0, rect);
394                 }
395
396         }
397         
398         
399         public RocketComponent[] getComponentsByPoint(double x, double y) {
400                 // Calculate point in shapes' coordinates
401                 Point2D.Double p = new Point2D.Double(x,y);
402                 try {
403                         g2transformation.inverseTransform(p,p);
404                 } catch (NoninvertibleTransformException e) {
405                         return new RocketComponent[0];
406                 }
407                 
408                 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
409
410                 for (int i=0; i<figureShapes.size(); i++) {
411                         if (figureShapes.get(i).contains(p))
412                                 l.add(figureComponents.get(i));
413                 }
414                 return l.toArray(new RocketComponent[0]);
415         }
416         
417         
418         
419         /**
420          * Gets the shapes required to draw the component.
421          * 
422          * @param component
423          * @param params
424          * @return
425          */
426         private Shape[] getShapes(RocketComponent component) {
427                 Reflection.Method m;
428
429                 // Find the appropriate method
430                 switch (type) {
431                 case TYPE_SIDE:
432                         m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide", 
433                                         RocketComponent.class, Transformation.class);
434                         break;
435                         
436                 case TYPE_BACK:
437                         m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack", 
438                                         RocketComponent.class, Transformation.class);
439                         break;
440                         
441                 default:
442                         throw new BugException("Unknown figure type = "+type);
443                 }
444                 
445                 if (m == null) {
446                         ExceptionHandler.handleErrorCondition("ERROR: Rocket figure paint method not found for " 
447                                         + component);
448                         return new Shape[0];
449                 }
450
451                 return (Shape[])m.invokeStatic(component,transformation);
452         }
453         
454         
455         
456         /**
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.
459          */
460         private void calculateFigureBounds() {
461                 Collection<Coordinate> bounds = configuration.getBounds();
462                 
463                 if (bounds.isEmpty()) {
464                         minX = 0;
465                         maxX = 0;
466                         maxR = 0;
467                         return;
468                 }
469
470                 minX = Double.MAX_VALUE;
471                 maxX = Double.MIN_VALUE;
472                 maxR = 0;
473                 for (Coordinate c: bounds) {
474                         double x = c.x, r = MathUtil.hypot(c.y, c.z);
475                         if (x < minX)
476                                 minX = x;
477                         if (x > maxX)
478                                 maxX = x;
479                         if (r > maxR)
480                                 maxR = r;
481                 }
482         }
483         
484         
485         public double getBestZoom(Rectangle2D bounds) {
486                 double zh=1, zv=1;
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);
492         }
493
494         
495         /**
496          * Calculates the necessary size of the figure and set the PreferredSize 
497          * property accordingly.
498          */
499         private void calculateSize() {
500                 calculateFigureBounds();
501                 
502                 switch (type) {
503                 case TYPE_SIDE:
504                         figureWidth = maxX-minX;
505                         figureHeight = 2*maxR;
506                         break;
507                         
508                 case TYPE_BACK:
509                         figureWidth = 2*maxR;
510                         figureHeight = 2*maxR;
511                         break;
512                         
513                 default:
514                         assert(false): "Should not occur, type="+type;
515                         figureWidth = 0;
516                         figureHeight = 0;
517                 }
518                 
519                 figureWidthPx = (int)(figureWidth * scale);
520                 figureHeightPx = (int)(figureHeight * scale);
521
522                 Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
523                                 figureHeightPx+2*BORDER_PIXELS_HEIGHT);
524                 
525                 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
526                         setPreferredSize(d);
527                         setMinimumSize(d);
528                         revalidate();
529                 }
530         }
531         
532         public Rectangle2D getDimensions() {
533                 switch (type) {
534                 case TYPE_SIDE:
535                         return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
536                         
537                 case TYPE_BACK:
538                         return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
539                         
540                 default:
541                         throw new BugException("Illegal figure type = "+type);
542                 }
543         }
544
545 }