version 1.1.9
[debian/openrocket] / src / net / sf / openrocket / gui / scalefigure / RocketPanel.java
1 package net.sf.openrocket.gui.scalefigure;
2
3
4 import java.awt.Dimension;
5 import java.awt.Font;
6 import java.awt.Point;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.InputEvent;
9 import java.awt.event.MouseEvent;
10 import java.util.ArrayList;
11 import java.util.Collection;
12 import java.util.List;
13 import java.util.concurrent.Executor;
14 import java.util.concurrent.Executors;
15 import java.util.concurrent.ThreadFactory;
16
17 import javax.swing.AbstractAction;
18 import javax.swing.Action;
19 import javax.swing.JComboBox;
20 import javax.swing.JLabel;
21 import javax.swing.JPanel;
22 import javax.swing.JSlider;
23 import javax.swing.JToggleButton;
24 import javax.swing.JViewport;
25 import javax.swing.SwingUtilities;
26 import javax.swing.event.ChangeEvent;
27 import javax.swing.event.ChangeListener;
28 import javax.swing.event.TreeSelectionEvent;
29 import javax.swing.event.TreeSelectionListener;
30 import javax.swing.tree.TreePath;
31 import javax.swing.tree.TreeSelectionModel;
32
33 import net.miginfocom.swing.MigLayout;
34 import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
35 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
36 import net.sf.openrocket.aerodynamics.FlightConditions;
37 import net.sf.openrocket.aerodynamics.WarningSet;
38 import net.sf.openrocket.document.OpenRocketDocument;
39 import net.sf.openrocket.document.Simulation;
40 import net.sf.openrocket.gui.adaptors.DoubleModel;
41 import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
42 import net.sf.openrocket.gui.components.BasicSlider;
43 import net.sf.openrocket.gui.components.StageSelector;
44 import net.sf.openrocket.gui.components.UnitSelector;
45 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
46 import net.sf.openrocket.gui.figureelements.CGCaret;
47 import net.sf.openrocket.gui.figureelements.CPCaret;
48 import net.sf.openrocket.gui.figureelements.Caret;
49 import net.sf.openrocket.gui.figureelements.RocketInfo;
50 import net.sf.openrocket.gui.main.SimulationWorker;
51 import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
52 import net.sf.openrocket.l10n.Translator;
53 import net.sf.openrocket.masscalc.BasicMassCalculator;
54 import net.sf.openrocket.masscalc.MassCalculator;
55 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
56 import net.sf.openrocket.rocketcomponent.Configuration;
57 import net.sf.openrocket.rocketcomponent.Rocket;
58 import net.sf.openrocket.rocketcomponent.RocketComponent;
59 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
60 import net.sf.openrocket.simulation.FlightData;
61 import net.sf.openrocket.simulation.listeners.SimulationListener;
62 import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
63 import net.sf.openrocket.simulation.listeners.system.InterruptListener;
64 import net.sf.openrocket.startup.Application;
65 import net.sf.openrocket.unit.UnitGroup;
66 import net.sf.openrocket.util.ChangeSource;
67 import net.sf.openrocket.util.Chars;
68 import net.sf.openrocket.util.Coordinate;
69 import net.sf.openrocket.util.MathUtil;
70 import net.sf.openrocket.util.Prefs;
71
72 /**
73  * A JPanel that contains a RocketFigure and buttons to manipulate the figure. 
74  * 
75  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
76  */
77 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
78         
79         private static final Translator trans = Application.getTranslator();
80         private final RocketFigure figure;
81         private final ScaleScrollPane scrollPane;
82         
83         private JLabel infoMessage;
84         
85         private TreeSelectionModel selectionModel = null;
86         
87
88         /* Calculation of CP and CG */
89         private AerodynamicCalculator aerodynamicCalculator;
90         private MassCalculator massCalculator;
91         
92
93         private final OpenRocketDocument document;
94         private final Configuration configuration;
95         
96         private Caret extraCP = null;
97         private Caret extraCG = null;
98         private RocketInfo extraText = null;
99         
100
101         private double cpAOA = Double.NaN;
102         private double cpTheta = Double.NaN;
103         private double cpMach = Double.NaN;
104         private double cpRoll = Double.NaN;
105         
106         // The functional ID of the rocket that was simulated
107         private int flightDataFunctionalID = -1;
108         private String flightDataMotorID = null;
109         
110
111         private SimulationWorker backgroundSimulationWorker = null;
112         
113         private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
114         
115
116         /**
117          * The executor service used for running the background simulations.
118          * This uses a fixed-sized thread pool for all background simulations
119          * with all threads in daemon mode and with minimum priority.
120          */
121         private static final Executor backgroundSimulationExecutor;
122         static {
123                 backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
124                                 new ThreadFactory() {
125                                         private ThreadFactory factory = Executors.defaultThreadFactory();
126                                         
127                                         @Override
128                                         public Thread newThread(Runnable r) {
129                                                 Thread t = factory.newThread(r);
130                                                 t.setDaemon(true);
131                                                 t.setPriority(Thread.MIN_PRIORITY);
132                                                 return t;
133                                         }
134                                 });
135         }
136         
137         
138         public RocketPanel(OpenRocketDocument document) {
139                 
140                 this.document = document;
141                 configuration = document.getDefaultConfiguration();
142                 
143                 // TODO: FUTURE: calculator selection
144                 aerodynamicCalculator = new BarrowmanCalculator();
145                 massCalculator = new BasicMassCalculator();
146                 
147                 // Create figure and custom scroll pane
148                 figure = new RocketFigure(configuration);
149                 
150                 scrollPane = new ScaleScrollPane(figure) {
151                         @Override
152                         public void mouseClicked(MouseEvent event) {
153                                 handleMouseClick(event);
154                         }
155                 };
156                 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
157                 scrollPane.setFitting(true);
158                 
159                 createPanel();
160                 
161                 configuration.addChangeListener(new ChangeListener() {
162                         @Override
163                         public void stateChanged(ChangeEvent e) {
164                                 System.out.println("Configuration changed, calling updateFigure");
165                                 updateExtras();
166                                 figure.updateFigure();
167                         }
168                 });
169         }
170         
171         
172         /**
173          * Creates the layout and components of the panel.
174          */
175         private void createPanel() {
176                 setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
177                 
178                 setPreferredSize(new Dimension(800, 300));
179                 
180
181                 //// Create toolbar
182                 
183                 // Side/back buttons
184                 FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
185                 //// Side view
186                 action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Sideview"));
187                 //// Side view
188                 action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Sideview"));
189                 JToggleButton toggle = new JToggleButton(action);
190                 add(toggle, "spanx, split");
191                 
192                 action = new FigureTypeAction(RocketFigure.TYPE_BACK);
193                 //// Back view
194                 action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Backview"));
195                 //// Back view
196                 action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Backview"));
197                 toggle = new JToggleButton(action);
198                 add(toggle, "gap rel");
199                 
200
201                 // Zoom level selector
202                 ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
203                 add(scaleSelector);
204                 
205
206
207                 // Stage selector
208                 StageSelector stageSelector = new StageSelector(configuration);
209                 add(stageSelector, "");
210                 
211
212
213                 // Motor configuration selector
214                 //// Motor configuration:
215                 JLabel label = new JLabel(trans.get("RocketPanel.lbl.Motorcfg"));
216                 label.setHorizontalAlignment(JLabel.RIGHT);
217                 add(label, "growx, right");
218                 add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap");
219                 
220
221
222
223
224                 // Create slider and scroll pane
225                 
226                 DoubleModel theta = new DoubleModel(figure, "Rotation",
227                                 UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
228                 UnitSelector us = new UnitSelector(theta, true);
229                 us.setHorizontalAlignment(JLabel.CENTER);
230                 add(us, "alignx 50%, growx");
231                 
232                 // Add the rocket figure
233                 add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap");
234                 
235
236                 // Add rotation slider
237                 // Minimum size to fit "360deg"
238                 JLabel l = new JLabel("360" + Chars.DEGREE);
239                 Dimension d = l.getPreferredSize();
240                 
241                 add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true),
242                                 "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy");
243                 
244
245                 //// <html>Click to select &nbsp;&nbsp; Shift+click to select other &nbsp;&nbsp; Double-click to edit &nbsp;&nbsp; Click+drag to move
246                 infoMessage = new JLabel(trans.get("RocketPanel.lbl.infoMessage"));
247                 infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
248                 add(infoMessage, "skip, span, gapleft 25, wrap");
249                 
250
251                 addExtras();
252         }
253         
254         
255
256         public RocketFigure getFigure() {
257                 return figure;
258         }
259         
260         public AerodynamicCalculator getAerodynamicCalculator() {
261                 return aerodynamicCalculator;
262         }
263         
264         public Configuration getConfiguration() {
265                 return configuration;
266         }
267         
268         /**
269          * Get the center of pressure figure element.
270          * 
271          * @return center of pressure info
272          */
273         public Caret getExtraCP() {
274                 return extraCP;
275         }
276         
277         /**
278          * Get the center of gravity figure element.
279          * 
280          * @return center of gravity info
281          */
282         public Caret getExtraCG() {
283                 return extraCG;
284         }
285         
286         /**
287          * Get the extra text figure element.
288          * 
289          * @return extra text that contains info about the rocket design
290          */
291         public RocketInfo getExtraText() {
292                 return extraText;
293         }
294         
295         public void setSelectionModel(TreeSelectionModel m) {
296                 if (selectionModel != null) {
297                         selectionModel.removeTreeSelectionListener(this);
298                 }
299                 selectionModel = m;
300                 selectionModel.addTreeSelectionListener(this);
301                 valueChanged((TreeSelectionEvent) null); // updates FigureParameters
302         }
303         
304         
305
306         /**
307          * Return the angle of attack used in CP calculation.  NaN signifies the default value
308          * of zero.
309          * @return   the angle of attack used, or NaN.
310          */
311         public double getCPAOA() {
312                 return cpAOA;
313         }
314         
315         /**
316          * Set the angle of attack to be used in CP calculation.  A value of NaN signifies that
317          * the default AOA (zero) should be used.
318          * @param aoa   the angle of attack to use, or NaN
319          */
320         public void setCPAOA(double aoa) {
321                 if (MathUtil.equals(aoa, cpAOA) ||
322                                 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
323                         return;
324                 cpAOA = aoa;
325                 updateExtras();
326                 figure.updateFigure();
327                 fireChangeEvent();
328         }
329         
330         public double getCPTheta() {
331                 return cpTheta;
332         }
333         
334         public void setCPTheta(double theta) {
335                 if (MathUtil.equals(theta, cpTheta) ||
336                                 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
337                         return;
338                 cpTheta = theta;
339                 if (!Double.isNaN(theta))
340                         figure.setRotation(theta);
341                 updateExtras();
342                 figure.updateFigure();
343                 fireChangeEvent();
344         }
345         
346         public double getCPMach() {
347                 return cpMach;
348         }
349         
350         public void setCPMach(double mach) {
351                 if (MathUtil.equals(mach, cpMach) ||
352                                 (Double.isNaN(mach) && Double.isNaN(cpMach)))
353                         return;
354                 cpMach = mach;
355                 updateExtras();
356                 figure.updateFigure();
357                 fireChangeEvent();
358         }
359         
360         public double getCPRoll() {
361                 return cpRoll;
362         }
363         
364         public void setCPRoll(double roll) {
365                 if (MathUtil.equals(roll, cpRoll) ||
366                                 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
367                         return;
368                 cpRoll = roll;
369                 updateExtras();
370                 figure.updateFigure();
371                 fireChangeEvent();
372         }
373         
374         
375
376         @Override
377         public void addChangeListener(ChangeListener listener) {
378                 listeners.add(0, listener);
379         }
380         
381         @Override
382         public void removeChangeListener(ChangeListener listener) {
383                 listeners.remove(listener);
384         }
385         
386         protected void fireChangeEvent() {
387                 ChangeEvent e = new ChangeEvent(this);
388                 ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
389                 for (ChangeListener l : list) {
390                         l.stateChanged(e);
391                 }
392         }
393         
394         
395
396
397         /**
398          * Handle clicking on figure shapes.  The functioning is the following:
399          * 
400          * Get the components clicked.
401          * If no component is clicked, do nothing.
402          * If the currently selected component is in the set, keep it, 
403          * unless the selector specified is pressed.  If it is pressed, cycle to 
404          * the next component. Otherwise select the first component in the list. 
405          */
406         public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
407         
408         private void handleMouseClick(MouseEvent event) {
409                 if (event.getButton() != MouseEvent.BUTTON1)
410                         return;
411                 Point p0 = event.getPoint();
412                 Point p1 = scrollPane.getViewport().getViewPosition();
413                 int x = p0.x + p1.x;
414                 int y = p0.y + p1.y;
415                 
416                 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
417                 
418                 // If no component is clicked, do nothing
419                 if (clicked.length == 0)
420                         return;
421                 
422                 // Check whether the currently selected component is in the clicked components.
423                 TreePath path = selectionModel.getSelectionPath();
424                 if (path != null) {
425                         RocketComponent current = (RocketComponent) path.getLastPathComponent();
426                         path = null;
427                         for (int i = 0; i < clicked.length; i++) {
428                                 if (clicked[i] == current) {
429                                         if (event.isShiftDown() && (event.getClickCount() == 1)) {
430                                                 path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]);
431                                         } else {
432                                                 path = ComponentTreeModel.makeTreePath(clicked[i]);
433                                         }
434                                         break;
435                                 }
436                         }
437                 }
438                 
439                 // Currently selected component not clicked
440                 if (path == null) {
441                         if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
442                                 path = ComponentTreeModel.makeTreePath(clicked[1]);
443                         } else {
444                                 path = ComponentTreeModel.makeTreePath(clicked[0]);
445                         }
446                 }
447                 
448                 // Set selection and check for double-click
449                 selectionModel.setSelectionPath(path);
450                 if (event.getClickCount() == 2) {
451                         RocketComponent component = (RocketComponent) path.getLastPathComponent();
452                         
453                         ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
454                                         document, component);
455                 }
456         }
457         
458         
459
460
461         /**
462          * Updates the extra data included in the figure.  Currently this includes
463          * the CP and CG carets.
464          */
465         private WarningSet warnings = new WarningSet();
466         
467         private void updateExtras() {
468                 Coordinate cp, cg;
469                 double cpx, cgx;
470                 
471                 // TODO: MEDIUM: User-definable conditions
472                 FlightConditions conditions = new FlightConditions(configuration);
473                 warnings.clear();
474                 
475                 if (!Double.isNaN(cpMach)) {
476                         conditions.setMach(cpMach);
477                         extraText.setMach(cpMach);
478                 } else {
479                         conditions.setMach(Prefs.getDefaultMach());
480                         extraText.setMach(Prefs.getDefaultMach());
481                 }
482                 
483                 if (!Double.isNaN(cpAOA)) {
484                         conditions.setAOA(cpAOA);
485                 } else {
486                         conditions.setAOA(0);
487                 }
488                 extraText.setAOA(cpAOA);
489                 
490                 if (!Double.isNaN(cpRoll)) {
491                         conditions.setRollRate(cpRoll);
492                 } else {
493                         conditions.setRollRate(0);
494                 }
495                 
496                 if (!Double.isNaN(cpTheta)) {
497                         conditions.setTheta(cpTheta);
498                         cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
499                 } else {
500                         cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
501                 }
502                 extraText.setTheta(cpTheta);
503                 
504
505                 cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
506                 //              System.out.println("CG computed as "+cg+ " CP as "+cp);
507                 
508                 if (cp.weight > 0.000001)
509                         cpx = cp.x;
510                 else
511                         cpx = Double.NaN;
512                 
513                 if (cg.weight > 0.000001)
514                         cgx = cg.x;
515                 else
516                         cgx = Double.NaN;
517                 
518                 // Length bound is assumed to be tight
519                 double length = 0, diameter = 0;
520                 Collection<Coordinate> bounds = configuration.getBounds();
521                 if (!bounds.isEmpty()) {
522                         double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
523                         for (Coordinate c : bounds) {
524                                 if (c.x < minX)
525                                         minX = c.x;
526                                 if (c.x > maxX)
527                                         maxX = c.x;
528                         }
529                         length = maxX - minX;
530                 }
531                 
532                 for (RocketComponent c : configuration) {
533                         if (c instanceof SymmetricComponent) {
534                                 double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
535                                 double d2 = ((SymmetricComponent) c).getAftRadius() * 2;
536                                 diameter = MathUtil.max(diameter, d1, d2);
537                         }
538                 }
539                 
540                 extraText.setCG(cgx);
541                 extraText.setCP(cpx);
542                 extraText.setLength(length);
543                 extraText.setDiameter(diameter);
544                 extraText.setMass(cg.weight);
545                 extraText.setWarnings(warnings);
546                 
547
548                 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
549                         
550                         // TODO: LOW: Y-coordinate and rotation
551                         extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
552                         extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
553                         
554                 } else {
555                         
556                         extraCP.setPosition(Double.NaN, Double.NaN);
557                         extraCG.setPosition(Double.NaN, Double.NaN);
558                         
559                 }
560                 
561
562                 ////////  Flight simulation in background
563                 
564                 // Check whether to compute or not
565                 if (!Prefs.computeFlightInBackground()) {
566                         extraText.setFlightData(null);
567                         extraText.setCalculatingData(false);
568                         stopBackgroundSimulation();
569                         return;
570                 }
571                 
572                 // Check whether data is already up to date
573                 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
574                                 flightDataMotorID == configuration.getMotorConfigurationID()) {
575                         return;
576                 }
577                 
578                 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
579                 flightDataMotorID = configuration.getMotorConfigurationID();
580                 
581                 // Stop previous computation (if any)
582                 stopBackgroundSimulation();
583                 
584                 // Check that configuration has motors
585                 if (!configuration.hasMotors()) {
586                         extraText.setFlightData(FlightData.NaN_DATA);
587                         extraText.setCalculatingData(false);
588                         return;
589                 }
590                 
591                 // Start calculation process
592                 extraText.setCalculatingData(true);
593                 
594                 Rocket duplicate = (Rocket) configuration.getRocket().copy();
595                 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
596                 simulation.getOptions().setMotorConfigurationID(
597                                 configuration.getMotorConfigurationID());
598                 
599                 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
600                 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
601         }
602         
603         /**
604          * Cancels the current background simulation worker, if any.
605          */
606         private void stopBackgroundSimulation() {
607                 if (backgroundSimulationWorker != null) {
608                         backgroundSimulationWorker.cancel(true);
609                         backgroundSimulationWorker = null;
610                 }
611         }
612         
613         
614         /**
615          * A SimulationWorker that simulates the rocket flight in the background and
616          * sets the results to the extra text when finished.  The worker can be cancelled
617          * if necessary.
618          */
619         private class BackgroundSimulationWorker extends SimulationWorker {
620                 
621                 public BackgroundSimulationWorker(Simulation sim) {
622                         super(sim);
623                 }
624                 
625                 @Override
626                 protected FlightData doInBackground() {
627                         
628                         // Pause a little while to allow faster UI reaction
629                         try {
630                                 Thread.sleep(300);
631                         } catch (InterruptedException ignore) {
632                         }
633                         if (isCancelled() || backgroundSimulationWorker != this)
634                                 return null;
635                         
636                         return super.doInBackground();
637                 }
638                 
639                 @Override
640                 protected void simulationDone() {
641                         // Do nothing if cancelled
642                         if (isCancelled() || backgroundSimulationWorker != this)
643                                 return;
644                         
645                         backgroundSimulationWorker = null;
646                         extraText.setFlightData(simulation.getSimulatedData());
647                         extraText.setCalculatingData(false);
648                         figure.repaint();
649                 }
650                 
651                 @Override
652                 protected SimulationListener[] getExtraListeners() {
653                         return new SimulationListener[] {
654                                         InterruptListener.INSTANCE,
655                                         ApogeeEndListener.INSTANCE };
656                 }
657                 
658                 @Override
659                 protected void simulationInterrupted(Throwable t) {
660                         // Do nothing on cancel, set N/A data otherwise
661                         if (isCancelled() || backgroundSimulationWorker != this) // Double-check
662                                 return;
663                         
664                         backgroundSimulationWorker = null;
665                         extraText.setFlightData(FlightData.NaN_DATA);
666                         extraText.setCalculatingData(false);
667                         figure.repaint();
668                 }
669         }
670         
671         
672
673         /**
674          * Adds the extra data to the figure.  Currently this includes the CP and CG carets.
675          */
676         private void addExtras() {
677                 figure.clearRelativeExtra();
678                 extraCG = new CGCaret(0, 0);
679                 extraCP = new CPCaret(0, 0);
680                 extraText = new RocketInfo(configuration);
681                 updateExtras();
682                 figure.addRelativeExtra(extraCP);
683                 figure.addRelativeExtra(extraCG);
684                 figure.addAbsoluteExtra(extraText);
685         }
686         
687         
688         /**
689          * Updates the selection in the FigureParameters and repaints the figure.  
690          * Ignores the event itself.
691          */
692         @Override
693         public void valueChanged(TreeSelectionEvent e) {
694                 TreePath[] paths = selectionModel.getSelectionPaths();
695                 if (paths == null) {
696                         figure.setSelection(null);
697                         return;
698                 }
699                 
700                 RocketComponent[] components = new RocketComponent[paths.length];
701                 for (int i = 0; i < paths.length; i++)
702                         components[i] = (RocketComponent) paths[i].getLastPathComponent();
703                 figure.setSelection(components);
704         }
705         
706         
707
708         /**
709          * An <code>Action</code> that shows whether the figure type is the type
710          * given in the constructor.
711          * 
712          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
713          */
714         private class FigureTypeAction extends AbstractAction implements ChangeListener {
715                 private final int type;
716                 
717                 public FigureTypeAction(int type) {
718                         this.type = type;
719                         stateChanged(null);
720                         figure.addChangeListener(this);
721                 }
722                 
723                 @Override
724                 public void actionPerformed(ActionEvent e) {
725                         boolean state = (Boolean) getValue(Action.SELECTED_KEY);
726                         if (state == true) {
727                                 // This view has been selected
728                                 figure.setType(type);
729                                 updateExtras();
730                         }
731                         stateChanged(null);
732                 }
733                 
734                 @Override
735                 public void stateChanged(ChangeEvent e) {
736                         putValue(Action.SELECTED_KEY, figure.getType() == type);
737                 }
738         }
739         
740 }