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