ea1f98c7180c4bf3d705f1c86d8c70b737a218a2
[debian/openrocket] / src / net / sf / openrocket / gui / scalefigure / RocketFigure.java
1 package net.sf.openrocket.gui.scalefigure;
2
3
4 import net.sf.openrocket.gui.figureelements.FigureElement;
5 import net.sf.openrocket.gui.main.ExceptionHandler;
6 import net.sf.openrocket.motor.Motor;
7 import net.sf.openrocket.rocketcomponent.Configuration;
8 import net.sf.openrocket.rocketcomponent.MotorMount;
9 import net.sf.openrocket.rocketcomponent.RocketComponent;
10 import net.sf.openrocket.util.BugException;
11 import net.sf.openrocket.util.Coordinate;
12 import net.sf.openrocket.util.LineStyle;
13 import net.sf.openrocket.util.MathUtil;
14 import net.sf.openrocket.util.Prefs;
15 import net.sf.openrocket.util.Reflection;
16 import net.sf.openrocket.util.Transformation;
17
18 import java.awt.BasicStroke;
19 import java.awt.Color;
20 import java.awt.Dimension;
21 import java.awt.Graphics;
22 import java.awt.Graphics2D;
23 import java.awt.Rectangle;
24 import java.awt.RenderingHints;
25 import java.awt.Shape;
26 import java.awt.geom.AffineTransform;
27 import java.awt.geom.Ellipse2D;
28 import java.awt.geom.NoninvertibleTransformException;
29 import java.awt.geom.Point2D;
30 import java.awt.geom.Rectangle2D;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Iterator;
34 import java.util.LinkedHashSet;
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         ty = computeTy(figureHeightPx);
261
262         if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
263                         // Origin has changed, fire event
264                         translateX = tx;
265                         translateY = ty;
266                         fireChangeEvent();
267                 }
268                 
269
270                 // Calculate and store the transformation used
271                 // (inverse is used in detecting clicks on objects)
272                 g2transformation = new AffineTransform();
273                 g2transformation.translate(translateX, translateY);
274                 // Mirror position Y-axis upwards
275                 g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
276
277                 g2.transform(g2transformation);
278
279                 // Set rendering hints appropriately
280                 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
281                                 RenderingHints.VALUE_STROKE_NORMALIZE);
282                 g2.setRenderingHint(RenderingHints.KEY_RENDERING, 
283                                 RenderingHints.VALUE_RENDER_QUALITY);
284                 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
285                                 RenderingHints.VALUE_ANTIALIAS_ON);
286
287
288                 // Draw all shapes
289                 
290                 for (int i=0; i < figureShapes.size(); i++) {
291                         RocketComponent c = figureComponents.get(i);
292                         Shape s = figureShapes.get(i);
293                         boolean selected = false;
294                         
295                         // Check if component is in the selection
296                         for (int j=0; j < selection.length; j++) {
297                                 if (c == selection[j]) {
298                                         selected = true;
299                                         break;
300                                 }
301                         }
302                         
303                         // Set component color and line style
304                         Color color = c.getColor();
305                         if (color == null) {
306                                 color = Prefs.getDefaultColor(c.getClass());
307                         }
308                         g2.setColor(color);
309
310                         LineStyle style = c.getLineStyle();
311                         if (style == null)
312                                 style = Prefs.getDefaultLineStyle(c.getClass());
313                         
314                         float[] dashes = style.getDashes();
315                         for (int j=0; j<dashes.length; j++) {
316                                 dashes[j] *= EXTRA_SCALE / scale;
317                         }
318                         
319                         if (selected) {
320                                 g2.setStroke(new BasicStroke((float)(SELECTED_WIDTH*EXTRA_SCALE/scale),
321                                                 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
322                                 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
323                                                 RenderingHints.VALUE_STROKE_PURE);
324                         } else {
325                                 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
326                                                 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
327                                 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
328                                                 RenderingHints.VALUE_STROKE_NORMALIZE);
329                         }
330                         g2.draw(s);
331                         
332                 }
333                 
334                 g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
335                                 BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
336                 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
337                                 RenderingHints.VALUE_STROKE_NORMALIZE);
338
339                 
340                 // Draw motors
341                 String motorID = configuration.getMotorConfigurationID();
342                 Color fillColor = Prefs.getMotorFillColor();
343                 Color borderColor = Prefs.getMotorBorderColor();
344                 Iterator<MotorMount> iterator = configuration.motorIterator();
345                 while (iterator.hasNext()) {
346                         MotorMount mount = iterator.next();
347                         Motor motor = mount.getMotor(motorID);
348                         double length = motor.getLength();
349                         double radius = motor.getDiameter() / 2;
350                         
351                         Coordinate[] position = ((RocketComponent)mount).toAbsolute(
352                                         new Coordinate(((RocketComponent)mount).getLength() + 
353                                                         mount.getMotorOverhang() - length));
354                         
355                         for (int i=0; i < position.length; i++) {
356                                 position[i] = transformation.transform(position[i]);
357                         }
358                         
359                         for (Coordinate coord: position) {
360                                 Shape s;
361                                 if (type == TYPE_SIDE) {
362                                         s = new Rectangle2D.Double(EXTRA_SCALE*coord.x,
363                                                         EXTRA_SCALE*(coord.y - radius), EXTRA_SCALE*length, 
364                                                         EXTRA_SCALE*2*radius);
365                                 } else {
366                                         s = new Ellipse2D.Double(EXTRA_SCALE*(coord.z-radius),
367                                                         EXTRA_SCALE*(coord.y-radius), EXTRA_SCALE*2*radius,
368                                                         EXTRA_SCALE*2*radius);
369                                 }
370                                 g2.setColor(fillColor);
371                                 g2.fill(s);
372                                 g2.setColor(borderColor);
373                                 g2.draw(s);
374                         }
375                 }
376                 
377                 
378                 
379                 // Draw relative extras
380                 for (FigureElement e: relativeExtra) {
381                         e.paint(g2, scale/EXTRA_SCALE);
382                 }
383
384                 // Draw absolute extras
385                 g2.setTransform(baseTransform);
386                 Rectangle rect = this.getVisibleRect();
387                 
388                 for (FigureElement e: absoluteExtra) {
389                         e.paint(g2, 1.0, rect);
390                 }
391
392         }
393
394     protected double computeTy (int heightPx) {
395         final double ty;
396         if (heightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
397              ty = getHeight()/2;
398         } else {
399              ty = BORDER_PIXELS_HEIGHT + heightPx/2;
400         }
401         return ty;
402     }
403
404
405     public RocketComponent[] getComponentsByPoint(double x, double y) {
406                 // Calculate point in shapes' coordinates
407                 Point2D.Double p = new Point2D.Double(x,y);
408                 try {
409                         g2transformation.inverseTransform(p,p);
410                 } catch (NoninvertibleTransformException e) {
411                         return new RocketComponent[0];
412                 }
413                 
414                 LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
415
416                 for (int i=0; i<figureShapes.size(); i++) {
417                         if (figureShapes.get(i).contains(p))
418                                 l.add(figureComponents.get(i));
419                 }
420                 return l.toArray(new RocketComponent[0]);
421         }
422         
423         
424         
425         /**
426          * Gets the shapes required to draw the component.
427          * 
428          * @param component
429          * @param params
430          * @return
431          */
432         private Shape[] getShapes(RocketComponent component) {
433                 Reflection.Method m;
434
435                 // Find the appropriate method
436                 switch (type) {
437                 case TYPE_SIDE:
438                         m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide", 
439                                         RocketComponent.class, Transformation.class);
440                         break;
441                         
442                 case TYPE_BACK:
443                         m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack", 
444                                         RocketComponent.class, Transformation.class);
445                         break;
446                         
447                 default:
448                         throw new BugException("Unknown figure type = "+type);
449                 }
450                 
451                 if (m == null) {
452                         ExceptionHandler.handleErrorCondition("ERROR: Rocket figure paint method not found for " 
453                                         + component);
454                         return new Shape[0];
455                 }
456
457                 return (Shape[])m.invokeStatic(component,transformation);
458         }
459         
460         
461         
462         /**
463          * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
464          * The bounds are stored in the variables minX, maxX and maxR.
465          */
466         private void calculateFigureBounds() {
467                 Collection<Coordinate> bounds = configuration.getBounds();
468                 
469                 if (bounds.isEmpty()) {
470                         minX = 0;
471                         maxX = 0;
472                         maxR = 0;
473                         return;
474                 }
475
476                 minX = Double.MAX_VALUE;
477                 maxX = Double.MIN_VALUE;
478                 maxR = 0;
479                 for (Coordinate c: bounds) {
480                         double x = c.x, r = MathUtil.hypot(c.y, c.z);
481                         if (x < minX)
482                                 minX = x;
483                         if (x > maxX)
484                                 maxX = x;
485                         if (r > maxR)
486                                 maxR = r;
487                 }
488         }
489         
490         
491         public double getBestZoom(Rectangle2D bounds) {
492                 double zh=1, zv=1;
493                 if (bounds.getWidth() > 0.0001)
494                         zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth();
495                 if (bounds.getHeight() > 0.0001)
496                         zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight();
497                 return Math.min(zh, zv);
498         }
499
500         
501         /**
502          * Calculates the necessary size of the figure and set the PreferredSize 
503          * property accordingly.
504          */
505         private void calculateSize() {
506                 calculateFigureBounds();
507                 
508                 switch (type) {
509                 case TYPE_SIDE:
510                         figureWidth = maxX-minX;
511                         figureHeight = 2*maxR;
512                         break;
513                         
514                 case TYPE_BACK:
515                         figureWidth = 2*maxR;
516                         figureHeight = 2*maxR;
517                         break;
518                         
519                 default:
520                         assert(false): "Should not occur, type="+type;
521                         figureWidth = 0;
522                         figureHeight = 0;
523                 }
524                 
525                 figureWidthPx = (int)(figureWidth * scale);
526                 figureHeightPx = (int)(figureHeight * scale);
527
528                 Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
529                                 figureHeightPx+2*BORDER_PIXELS_HEIGHT);
530                 
531                 if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
532                         setPreferredSize(d);
533                         setMinimumSize(d);
534                         revalidate();
535                 }
536         }
537         
538         public Rectangle2D getDimensions() {
539                 switch (type) {
540                 case TYPE_SIDE:
541                         return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
542                         
543                 case TYPE_BACK:
544                         return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
545                         
546                 default:
547                         throw new BugException("Illegal figure type = "+type);
548                 }
549         }
550
551 }