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