updates for 0.9.4
[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.Coordinate;
29 import net.sf.openrocket.util.LineStyle;
30 import net.sf.openrocket.util.MathUtil;
31 import net.sf.openrocket.util.Prefs;
32 import net.sf.openrocket.util.Reflection;
33 import net.sf.openrocket.util.Transformation;
34
35 /**
36  * A <code>ScaleFigure</code> that draws a complete rocket.  Extra information can
37  * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)},
38  * {@link #clearRelativeExtra()}.
39  * 
40  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
41  */
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 final 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                 calculateSize();
99                 updateFigure();
100         }
101         
102         
103         
104         public Dimension getOrigin() {
105                 return new Dimension((int)translateX, (int)translateY);
106         }
107         
108         @Override
109         public double getFigureHeight() {
110                 return figureHeight;
111         }
112
113         @Override
114         public double getFigureWidth() {
115                 return figureWidth;
116         }
117
118         
119         public RocketComponent[] getSelection() {
120                 return selection;
121         }
122         
123         public void setSelection(RocketComponent[] selection) {
124                 if (selection == null) {
125                         this.selection = new RocketComponent[0];
126                 } else {
127                         this.selection = selection;
128                 }
129                 updateFigure();
130         }
131         
132         
133         public double getRotation() {
134                 return rotation;
135         }
136         
137         public Transformation getRotateTransformation() {
138                 return transformation;
139         }
140         
141         public void setRotation(double rot) {
142                 if (MathUtil.equals(rotation, rot))
143                         return;
144                 this.rotation = rot;
145                 this.transformation = Transformation.rotate_x(rotation);
146                 updateFigure();
147         }
148         
149         
150         public int getType() {
151                 return type;
152         }
153         
154         public void setType(int type) {
155                 if (type != TYPE_BACK && type != TYPE_SIDE) {
156                         throw new IllegalArgumentException("Illegal type: "+type);
157                 }
158                 if (this.type == type)
159                         return;
160                 this.type = type;
161                 updateFigure();
162         }
163
164         
165         
166         
167
168
169         /**
170          * Updates the figure shapes and figure size.
171          */
172         @Override
173         public void updateFigure() {
174                 figureShapes.clear();
175                 figureComponents.clear();
176                 
177                 calculateSize();
178
179                 // Get shapes for all active components
180                 for (RocketComponent c: configuration) {
181                         Shape[] s = getShapes(c);
182                         for (int i=0; i < s.length; i++) {
183                                 figureShapes.add(s[i]);
184                                 figureComponents.add(c);
185                         }
186                 }
187                 
188                 repaint();
189                 fireChangeEvent();
190         }
191
192         
193         public void addRelativeExtra(FigureElement p) {
194                 relativeExtra.add(p);
195         }
196         
197         public void removeRelativeExtra(FigureElement p) {
198                 relativeExtra.remove(p);
199         }
200         
201         public void clearRelativeExtra() {
202                 relativeExtra.clear();
203         }
204         
205         
206         public void addAbsoluteExtra(FigureElement p) {
207                 absoluteExtra.add(p);
208         }
209         
210         public void removeAbsoluteExtra(FigureElement p) {
211                 absoluteExtra.remove(p);
212         }
213         
214         public void clearAbsoluteExtra() {
215                 absoluteExtra.clear();
216         }
217         
218
219         /**
220          * Paints the rocket on to the Graphics element.
221          * <p>
222          * Warning:  If paintComponent is used outside the normal Swing usage, some Swing
223          * dependent parameters may be left wrong (mainly transformation).  If it is used,
224          * the RocketFigure should be repainted immediately afterwards.
225          */
226         @Override
227         public void paintComponent(Graphics g) {
228                 super.paintComponent(g);
229                 Graphics2D g2 = (Graphics2D)g;
230                 
231                 
232                 AffineTransform baseTransform = g2.getTransform();
233                 
234                 // Update figure shapes if necessary
235                 if (figureShapes == null)
236                         updateFigure();
237
238
239                 double tx, ty;
240                 // Calculate translation for figure centering
241                 if (figureWidthPx + 2*BORDER_PIXELS_WIDTH < getWidth()) {
242
243                         // Figure fits in the viewport
244                         if (type == TYPE_BACK)
245                                 tx = getWidth()/2;
246                         else 
247                                 tx = (getWidth()-figureWidthPx)/2 - minX*scale;
248
249                 } else {
250
251                         // Figure does not fit in viewport
252                         if (type == TYPE_BACK)
253                                 tx = BORDER_PIXELS_WIDTH + figureWidthPx/2;
254                         else 
255                                 tx = BORDER_PIXELS_WIDTH - minX*scale;
256                         
257                 }
258                 
259                 if (figureHeightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
260                          ty = getHeight()/2;
261                 } else {
262                          ty = BORDER_PIXELS_HEIGHT + figureHeightPx/2;
263                 }
264                 
265                 if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
266                         // Origin has changed, fire event
267                         translateX = tx;
268                         translateY = ty;
269                         fireChangeEvent();
270                 }
271                 
272
273                 // Calculate and store the transformation used
274                 // (inverse is used in detecting clicks on objects)
275                 g2transformation = new AffineTransform();
276                 g2transformation.translate(translateX, translateY);
277                 // Mirror position Y-axis upwards
278                 g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
279
280                 g2.transform(g2transformation);
281
282                 // Set rendering hints appropriately
283                 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
284                                 RenderingHints.VALUE_STROKE_NORMALIZE);
285                 g2.setRenderingHint(RenderingHints.KEY_RENDERING, 
286                                 RenderingHints.VALUE_RENDER_QUALITY);
287                 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
288                                 RenderingHints.VALUE_ANTIALIAS_ON);
289
290
291                 // Draw all shapes
292                 
293                 for (int i=0; i < figureShapes.size(); i++) {
294                         RocketComponent c = figureComponents.get(i);
295                         Shape s = figureShapes.get(i);
296                         boolean selected = false;
297                         
298                         // Check if component is in the selection
299                         for (int j=0; j < selection.length; j++) {
300                                 if (c == selection[j]) {
301                                         selected = true;
302                                         break;
303                                 }
304                         }
305                         
306                         // Set component color and line style
307                         Color color = c.getColor();
308                         if (color == null) {
309                                 color = Prefs.getDefaultColor(c.getClass());
310                         }
311                         g2.setColor(color);
312
313                         LineStyle style = c.getLineStyle();
314                         if (style == null)
315                                 style = Prefs.getDefaultLineStyle(c.getClass());
316                         
317                         float[] dashes = style.getDashes();
318                         for (int j=0; j<dashes.length; j++) {
319                                 dashes[j] *= EXTRA_SCALE / scale;
320                         }
321                         
322                         if (selected) {
323                                 g2.setStroke(new BasicStroke((float)(SELECTED_WIDTH*EXTRA_SCALE/scale),
324                                                 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
325                                 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
326                                                 RenderingHints.VALUE_STROKE_PURE);
327                         } else {
328                                 g2.setStroke(new BasicStroke((float)(NORMAL_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_NORMALIZE);
332                         }
333                         g2.draw(s);
334                         
335                 }
336                 
337                 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
338                                 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
339                 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
340                                 RenderingHints.VALUE_STROKE_NORMALIZE);
341
342                 
343                 // Draw motors
344                 String motorID = configuration.getMotorConfigurationID();
345                 Color fillColor = Prefs.getMotorFillColor();
346                 Color borderColor = Prefs.getMotorBorderColor();
347                 Iterator<MotorMount> iterator = configuration.motorIterator();
348                 while (iterator.hasNext()) {
349                         MotorMount mount = iterator.next();
350                         Motor motor = mount.getMotor(motorID);
351                         double length = motor.getLength();
352                         double radius = motor.getDiameter() / 2;
353                         
354                         Coordinate[] position = ((RocketComponent)mount).toAbsolute(
355                                         new Coordinate(((RocketComponent)mount).getLength() + 
356                                                         mount.getMotorOverhang() - length));
357                         
358                         for (int i=0; i < position.length; i++) {
359                                 position[i] = transformation.transform(position[i]);
360                         }
361                         
362                         for (Coordinate coord: position) {
363                                 Shape s;
364                                 if (type == TYPE_SIDE) {
365                                         s = new Rectangle2D.Double(EXTRA_SCALE*coord.x,
366                                                         EXTRA_SCALE*(coord.y - radius), EXTRA_SCALE*length, 
367                                                         EXTRA_SCALE*2*radius);
368                                 } else {
369                                         s = new Ellipse2D.Double(EXTRA_SCALE*(coord.z-radius),
370                                                         EXTRA_SCALE*(coord.y-radius), EXTRA_SCALE*2*radius,
371                                                         EXTRA_SCALE*2*radius);
372                                 }
373                                 g2.setColor(fillColor);
374                                 g2.fill(s);
375                                 g2.setColor(borderColor);
376                                 g2.draw(s);
377                         }
378                 }
379                 
380                 
381                 
382                 // Draw relative extras
383                 for (FigureElement e: relativeExtra) {
384                         e.paint(g2, scale/EXTRA_SCALE);
385                 }
386
387                 // Draw absolute extras
388                 g2.setTransform(baseTransform);
389                 Rectangle rect = this.getVisibleRect();
390                 
391                 for (FigureElement e: absoluteExtra) {
392                         e.paint(g2, 1.0, rect);
393                 }
394
395         }
396         
397         
398         public RocketComponent[] getComponentsByPoint(double x, double y) {
399                 // Calculate point in shapes' coordinates
400                 Point2D.Double p = new Point2D.Double(x,y);
401                 try {
402                         g2transformation.inverseTransform(p,p);
403                 } catch (NoninvertibleTransformException e) {
404                         return new RocketComponent[0];
405                 }
406                 
407                 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
408
409                 for (int i=0; i<figureShapes.size(); i++) {
410                         if (figureShapes.get(i).contains(p))
411                                 l.add(figureComponents.get(i));
412                 }
413                 return l.toArray(new RocketComponent[0]);
414         }
415         
416         
417         
418         /**
419          * Gets the shapes required to draw the component.
420          * 
421          * @param component
422          * @param params
423          * @return
424          */
425         private Shape[] getShapes(RocketComponent component) {
426                 Reflection.Method m;
427
428                 // Find the appropriate method
429                 switch (type) {
430                 case TYPE_SIDE:
431                         m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide", 
432                                         RocketComponent.class, Transformation.class);
433                         break;
434                         
435                 case TYPE_BACK:
436                         m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack", 
437                                         RocketComponent.class, Transformation.class);
438                         break;
439                         
440                 default:
441                         throw new RuntimeException("Unknown figure type = "+type);
442                 }
443                 
444                 if (m == null) {
445                         ExceptionHandler.handleErrorCondition("ERROR: Rocket figure paint method not found for " 
446                                         + component);
447                         return new Shape[0];
448                 }
449
450                 return (Shape[])m.invokeStatic(component,transformation);
451         }
452         
453         
454         
455         /**
456          * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
457          * The bounds are stored in the variables minX, maxX and maxR.
458          */
459         private void calculateFigureBounds() {
460                 Collection<Coordinate> bounds = configuration.getBounds();
461                 
462                 if (bounds.isEmpty()) {
463                         minX = 0;
464                         maxX = 0;
465                         maxR = 0;
466                         return;
467                 }
468
469                 minX = Double.MAX_VALUE;
470                 maxX = Double.MIN_VALUE;
471                 maxR = 0;
472                 for (Coordinate c: bounds) {
473                         double x = c.x, r = MathUtil.hypot(c.y, c.z);
474                         if (x < minX)
475                                 minX = x;
476                         if (x > maxX)
477                                 maxX = x;
478                         if (r > maxR)
479                                 maxR = r;
480                 }
481         }
482         
483         
484         public double getBestZoom(Rectangle2D bounds) {
485                 double zh=1, zv=1;
486                 if (bounds.getWidth() > 0.0001)
487                         zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth();
488                 if (bounds.getHeight() > 0.0001)
489                         zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight();
490                 return Math.min(zh, zv);
491         }
492
493         
494         /**
495          * Calculates the necessary size of the figure and set the PreferredSize 
496          * property accordingly.
497          */
498         private void calculateSize() {
499                 calculateFigureBounds();
500                 
501                 switch (type) {
502                 case TYPE_SIDE:
503                         figureWidth = maxX-minX;
504                         figureHeight = 2*maxR;
505                         break;
506                         
507                 case TYPE_BACK:
508                         figureWidth = 2*maxR;
509                         figureHeight = 2*maxR;
510                         break;
511                         
512                 default:
513                         assert(false): "Should not occur, type="+type;
514                         figureWidth = 0;
515                         figureHeight = 0;
516                 }
517                 
518                 figureWidthPx = (int)(figureWidth * scale);
519                 figureHeightPx = (int)(figureHeight * scale);
520
521                 Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
522                                 figureHeightPx+2*BORDER_PIXELS_HEIGHT);
523                 
524                 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
525                         setPreferredSize(d);
526                         setMinimumSize(d);
527                         revalidate();
528                 }
529         }
530         
531         public Rectangle2D getDimensions() {
532                 switch (type) {
533                 case TYPE_SIDE:
534                         return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
535                         
536                 case TYPE_BACK:
537                         return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
538                         
539                 default:
540                         throw new RuntimeException("Illegal figure type = "+type);
541                 }
542         }
543
544 }