clean up template leftovers in copyright file
[debian/openrocket] / src / net / sf / openrocket / gui / scalefigure / RocketPanel.java
1 package net.sf.openrocket.gui.scalefigure;
2
3
4 import net.miginfocom.swing.MigLayout;
5 import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
6 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
7 import net.sf.openrocket.aerodynamics.FlightConditions;
8 import net.sf.openrocket.aerodynamics.WarningSet;
9 import net.sf.openrocket.document.OpenRocketDocument;
10 import net.sf.openrocket.document.Simulation;
11 import net.sf.openrocket.gui.adaptors.DoubleModel;
12 import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
13 import net.sf.openrocket.gui.components.BasicSlider;
14 import net.sf.openrocket.gui.components.StageSelector;
15 import net.sf.openrocket.gui.components.UnitSelector;
16 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
17 import net.sf.openrocket.gui.figureelements.CGCaret;
18 import net.sf.openrocket.gui.figureelements.CPCaret;
19 import net.sf.openrocket.gui.figureelements.Caret;
20 import net.sf.openrocket.gui.figureelements.RocketInfo;
21 import net.sf.openrocket.gui.main.SimulationWorker;
22 import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
23 import net.sf.openrocket.l10n.Translator;
24 import net.sf.openrocket.masscalc.BasicMassCalculator;
25 import net.sf.openrocket.masscalc.MassCalculator;
26 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
27 import net.sf.openrocket.rocketcomponent.Configuration;
28 import net.sf.openrocket.rocketcomponent.Rocket;
29 import net.sf.openrocket.rocketcomponent.RocketComponent;
30 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
31 import net.sf.openrocket.simulation.FlightData;
32 import net.sf.openrocket.simulation.listeners.SimulationListener;
33 import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
34 import net.sf.openrocket.simulation.listeners.system.InterruptListener;
35 import net.sf.openrocket.startup.Application;
36 import net.sf.openrocket.unit.UnitGroup;
37 import net.sf.openrocket.util.ChangeSource;
38 import net.sf.openrocket.util.Chars;
39 import net.sf.openrocket.util.Coordinate;
40 import net.sf.openrocket.util.MathUtil;
41 import net.sf.openrocket.util.Prefs;
42
43 import javax.swing.AbstractAction;
44 import javax.swing.Action;
45 import javax.swing.JComboBox;
46 import javax.swing.JLabel;
47 import javax.swing.JPanel;
48 import javax.swing.JSlider;
49 import javax.swing.JToggleButton;
50 import javax.swing.JViewport;
51 import javax.swing.SwingUtilities;
52 import javax.swing.event.ChangeEvent;
53 import javax.swing.event.ChangeListener;
54 import javax.swing.event.TreeSelectionEvent;
55 import javax.swing.event.TreeSelectionListener;
56 import javax.swing.tree.TreePath;
57 import javax.swing.tree.TreeSelectionModel;
58 import java.awt.Dimension;
59 import java.awt.Font;
60 import java.awt.Point;
61 import java.awt.event.ActionEvent;
62 import java.awt.event.InputEvent;
63 import java.awt.event.MouseEvent;
64 import java.util.ArrayList;
65 import java.util.Collection;
66 import java.util.List;
67 import java.util.concurrent.Executor;
68 import java.util.concurrent.Executors;
69 import java.util.concurrent.ThreadFactory;
70
71 /**
72  * A JPanel that contains a RocketFigure and buttons to manipulate the figure. 
73  * 
74  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
75  */
76 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
77         
78         private static final Translator trans = Application.getTranslator();
79         private final RocketFigure figure;
80         private final ScaleScrollPane scrollPane;
81         
82         private JLabel infoMessage;
83         
84         private TreeSelectionModel selectionModel = null;
85         
86
87         /* Calculation of CP and CG */
88         private AerodynamicCalculator aerodynamicCalculator;
89         private MassCalculator massCalculator;
90         
91
92         private final OpenRocketDocument document;
93         private final Configuration configuration;
94         
95         private Caret extraCP = null;
96         private Caret extraCG = null;
97         private RocketInfo extraText = null;
98         
99
100         private double cpAOA = Double.NaN;
101         private double cpTheta = Double.NaN;
102         private double cpMach = Double.NaN;
103         private double cpRoll = Double.NaN;
104         
105         // The functional ID of the rocket that was simulated
106         private int flightDataFunctionalID = -1;
107         private String flightDataMotorID = null;
108         
109
110         private SimulationWorker backgroundSimulationWorker = null;
111         private boolean dirty = false;
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                 addExtras();
251         }
252         
253         
254
255         public RocketFigure getFigure() {
256                 return figure;
257         }
258         
259         public AerodynamicCalculator getAerodynamicCalculator() {
260                 return aerodynamicCalculator;
261         }
262         
263         public Configuration getConfiguration() {
264                 return configuration;
265         }
266
267     /**
268      * Get the center of pressure figure element.
269      * 
270      * @return center of pressure info
271      */
272     public Caret getExtraCP () {
273         return extraCP;
274     }
275
276     /**
277      * Get the center of gravity figure element.
278      * 
279      * @return center of gravity info
280      */
281     public Caret getExtraCG () {
282         return extraCG;
283     }
284
285     /**
286      * Get the extra text figure element.
287      * 
288      * @return extra text that contains info about the rocket design
289      */
290     public RocketInfo getExtraText () {
291         return extraText;
292     }
293
294     public void setSelectionModel(TreeSelectionModel m) {
295                 if (selectionModel != null) {
296                         selectionModel.removeTreeSelectionListener(this);
297                 }
298                 selectionModel = m;
299                 selectionModel.addTreeSelectionListener(this);
300                 valueChanged((TreeSelectionEvent) null); // updates FigureParameters
301         }
302         
303         
304
305         /**
306          * Return the angle of attack used in CP calculation.  NaN signifies the default value
307          * of zero.
308          * @return   the angle of attack used, or NaN.
309          */
310         public double getCPAOA() {
311                 return cpAOA;
312         }
313         
314         /**
315          * Set the angle of attack to be used in CP calculation.  A value of NaN signifies that
316          * the default AOA (zero) should be used.
317          * @param aoa   the angle of attack to use, or NaN
318          */
319         public void setCPAOA(double aoa) {
320                 if (MathUtil.equals(aoa, cpAOA) ||
321                                 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
322                         return;
323                 cpAOA = aoa;
324                 updateExtras();
325                 figure.updateFigure();
326                 fireChangeEvent();
327         }
328         
329         public double getCPTheta() {
330                 return cpTheta;
331         }
332         
333         public void setCPTheta(double theta) {
334                 if (MathUtil.equals(theta, cpTheta) ||
335                                 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
336                         return;
337                 cpTheta = theta;
338                 if (!Double.isNaN(theta))
339                         figure.setRotation(theta);
340                 updateExtras();
341                 figure.updateFigure();
342                 fireChangeEvent();
343         }
344         
345         public double getCPMach() {
346                 return cpMach;
347         }
348         
349         public void setCPMach(double mach) {
350                 if (MathUtil.equals(mach, cpMach) ||
351                                 (Double.isNaN(mach) && Double.isNaN(cpMach)))
352                         return;
353                 cpMach = mach;
354                 updateExtras();
355                 figure.updateFigure();
356                 fireChangeEvent();
357         }
358         
359         public double getCPRoll() {
360                 return cpRoll;
361         }
362         
363         public void setCPRoll(double roll) {
364                 if (MathUtil.equals(roll, cpRoll) ||
365                                 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
366                         return;
367                 cpRoll = roll;
368                 updateExtras();
369                 figure.updateFigure();
370                 fireChangeEvent();
371         }
372         
373         
374
375         @Override
376         public void addChangeListener(ChangeListener listener) {
377                 listeners.add(0, listener);
378         }
379         
380         @Override
381         public void removeChangeListener(ChangeListener listener) {
382                 listeners.remove(listener);
383         }
384         
385         protected void fireChangeEvent() {
386                 ChangeEvent e = new ChangeEvent(this);
387                 ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
388                 for (ChangeListener l : list) {
389                         l.stateChanged(e);
390                 }
391         }
392         
393         
394
395
396         /**
397          * Handle clicking on figure shapes.  The functioning is the following:
398          * 
399          * Get the components clicked.
400          * If no component is clicked, do nothing.
401          * If the currently selected component is in the set, keep it, 
402          * unless the selector specified is pressed.  If it is pressed, cycle to 
403          * the next component. Otherwise select the first component in the list. 
404          */
405         public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
406         
407         private void handleMouseClick(MouseEvent event) {
408                 if (event.getButton() != MouseEvent.BUTTON1)
409                         return;
410                 Point p0 = event.getPoint();
411                 Point p1 = scrollPane.getViewport().getViewPosition();
412                 int x = p0.x + p1.x;
413                 int y = p0.y + p1.y;
414                 
415                 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
416                 
417                 // If no component is clicked, do nothing
418                 if (clicked.length == 0)
419                         return;
420                 
421                 // Check whether the currently selected component is in the clicked components.
422                 TreePath path = selectionModel.getSelectionPath();
423                 if (path != null) {
424                         RocketComponent current = (RocketComponent) path.getLastPathComponent();
425                         path = null;
426                         for (int i = 0; i < clicked.length; i++) {
427                                 if (clicked[i] == current) {
428                                         if (event.isShiftDown() && (event.getClickCount() == 1)) {
429                                                 path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]);
430                                         } else {
431                                                 path = ComponentTreeModel.makeTreePath(clicked[i]);
432                                         }
433                                         break;
434                                 }
435                         }
436                 }
437                 
438                 // Currently selected component not clicked
439                 if (path == null) {
440                         if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
441                                 path = ComponentTreeModel.makeTreePath(clicked[1]);
442                         } else {
443                                 path = ComponentTreeModel.makeTreePath(clicked[0]);
444                         }
445                 }
446                 
447                 // Set selection and check for double-click
448                 selectionModel.setSelectionPath(path);
449                 if (event.getClickCount() == 2) {
450                         RocketComponent component = (RocketComponent) path.getLastPathComponent();
451                         
452                         ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
453                                         document, component);
454                 }
455         }
456         
457         
458
459
460         /**
461          * Updates the extra data included in the figure.  Currently this includes
462          * the CP and CG carets.
463          */
464         private WarningSet warnings = new WarningSet();
465         
466         private void updateExtras() {
467                 Coordinate cp, cg;
468                 double cpx, cgx;
469                 
470                 // TODO: MEDIUM: User-definable conditions
471                 FlightConditions conditions = new FlightConditions(configuration);
472                 warnings.clear();
473                 
474                 if (!Double.isNaN(cpMach)) {
475                         conditions.setMach(cpMach);
476                         extraText.setMach(cpMach);
477                 } else {
478                         conditions.setMach(Prefs.getDefaultMach());
479                         extraText.setMach(Prefs.getDefaultMach());
480                 }
481                 
482                 if (!Double.isNaN(cpAOA)) {
483                         conditions.setAOA(cpAOA);
484                 } else {
485                         conditions.setAOA(0);
486                 }
487                 extraText.setAOA(cpAOA);
488                 
489                 if (!Double.isNaN(cpRoll)) {
490                         conditions.setRollRate(cpRoll);
491                 } else {
492                         conditions.setRollRate(0);
493                 }
494                 
495                 if (!Double.isNaN(cpTheta)) {
496                         conditions.setTheta(cpTheta);
497                         cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
498                 } else {
499                         cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
500                 }
501                 extraText.setTheta(cpTheta);
502                 
503
504                 cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
505                 //              System.out.println("CG computed as "+cg+ " CP as "+cp);
506                 
507                 if (cp.weight > 0.000001)
508                         cpx = cp.x;
509                 else
510                         cpx = Double.NaN;
511                 
512                 if (cg.weight > 0.000001)
513                         cgx = cg.x;
514                 else
515                         cgx = Double.NaN;
516                 
517                 // Length bound is assumed to be tight
518                 double length = 0, diameter = 0;
519                 Collection<Coordinate> bounds = configuration.getBounds();
520                 if (!bounds.isEmpty()) {
521                         double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
522                         for (Coordinate c : bounds) {
523                                 if (c.x < minX)
524                                         minX = c.x;
525                                 if (c.x > maxX)
526                                         maxX = c.x;
527                         }
528                         length = maxX - minX;
529                 }
530                 
531                 for (RocketComponent c : configuration) {
532                         if (c instanceof SymmetricComponent) {
533                                 double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
534                                 double d2 = ((SymmetricComponent) c).getAftRadius() * 2;
535                                 diameter = MathUtil.max(diameter, d1, d2);
536                         }
537                 }
538                 
539                 extraText.setCG(cgx);
540                 extraText.setCP(cpx);
541                 extraText.setLength(length);
542                 extraText.setDiameter(diameter);
543                 extraText.setMass(cg.weight);
544                 extraText.setWarnings(warnings);
545                 
546
547                 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
548                         
549                         // TODO: LOW: Y-coordinate and rotation
550                         extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
551                         extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
552                         
553                 } else {
554                         
555                         extraCP.setPosition(Double.NaN, Double.NaN);
556                         extraCG.setPosition(Double.NaN, Double.NaN);
557                         
558                 }
559                 
560
561                 ////////  Flight simulation in background
562                 
563                 // Check whether to compute or not
564                 if (!Prefs.computeFlightInBackground()) {
565                         extraText.setFlightData(null);
566                         extraText.setCalculatingData(false);
567                         stopBackgroundSimulation();
568                         return;
569                 }
570                 
571                 // Check whether data is already up to date
572                 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
573                                 flightDataMotorID == configuration.getMotorConfigurationID()) {
574                         return;
575                 }
576                 
577                 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
578                 flightDataMotorID = configuration.getMotorConfigurationID();
579                 
580                 // Stop previous computation (if any)
581                 stopBackgroundSimulation();
582                 
583                 // Check that configuration has motors
584                 if (!configuration.hasMotors()) {
585                         extraText.setFlightData(FlightData.NaN_DATA);
586                         extraText.setCalculatingData(false);
587                         return;
588                 }
589                 
590                 // Start calculation process
591                 extraText.setCalculatingData(true);
592                 
593                 Rocket duplicate = (Rocket) configuration.getRocket().copy();
594                 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
595                 simulation.getOptions().setMotorConfigurationID(
596                                 configuration.getMotorConfigurationID());
597                 
598                 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
599                 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
600         }
601         
602         /**
603          * Cancels the current background simulation worker, if any.
604          */
605         private void stopBackgroundSimulation() {
606                 if (backgroundSimulationWorker != null) {
607                         backgroundSimulationWorker.cancel(true);
608                         backgroundSimulationWorker = null;
609                 }
610         }
611         
612         
613         /**
614          * A SimulationWorker that simulates the rocket flight in the background and
615          * sets the results to the extra text when finished.  The worker can be cancelled
616          * if necessary.
617          */
618         private class BackgroundSimulationWorker extends SimulationWorker {
619                 
620                 public BackgroundSimulationWorker(Simulation sim) {
621                         super(sim);
622                 }
623                 
624                 @Override
625                 protected FlightData doInBackground() {
626                         
627                         // Pause a little while to allow faster UI reaction
628                         try {
629                                 Thread.sleep(300);
630                         } catch (InterruptedException ignore) {
631                         }
632                         if (isCancelled() || backgroundSimulationWorker != this)
633                                 return null;
634                         
635                         return super.doInBackground();
636                 }
637                 
638                 @Override
639                 protected void simulationDone() {
640                         // Do nothing if cancelled
641                         if (isCancelled() || backgroundSimulationWorker != this)
642                                 return;
643                         
644                         backgroundSimulationWorker = null;
645                         extraText.setFlightData(simulation.getSimulatedData());
646                         extraText.setCalculatingData(false);
647                         figure.repaint();
648                 }
649                 
650                 @Override
651                 protected SimulationListener[] getExtraListeners() {
652                         return new SimulationListener[] {
653                                         InterruptListener.INSTANCE,
654                                         ApogeeEndListener.INSTANCE };
655                 }
656                 
657                 @Override
658                 protected void simulationInterrupted(Throwable t) {
659                         // Do nothing on cancel, set N/A data otherwise
660                         if (isCancelled() || backgroundSimulationWorker != this) // Double-check
661                                 return;
662                         
663                         backgroundSimulationWorker = null;
664                         extraText.setFlightData(FlightData.NaN_DATA);
665                         extraText.setCalculatingData(false);
666                         figure.repaint();
667                 }
668         }
669         
670         
671
672         /**
673          * Adds the extra data to the figure.  Currently this includes the CP and CG carets.
674          */
675         private void addExtras() {
676                 figure.clearRelativeExtra();
677                 extraCG = new CGCaret(0, 0);
678                 extraCP = new CPCaret(0, 0);
679                 extraText = new RocketInfo(configuration);
680                 updateExtras();
681                 figure.addRelativeExtra(extraCP);
682                 figure.addRelativeExtra(extraCG);
683                 figure.addAbsoluteExtra(extraText);
684         }
685         
686         
687         /**
688          * Updates the selection in the FigureParameters and repaints the figure.  
689          * Ignores the event itself.
690          */
691         @Override
692         public void valueChanged(TreeSelectionEvent e) {
693                 TreePath[] paths = selectionModel.getSelectionPaths();
694                 if (paths == null) {
695                         figure.setSelection(null);
696                         return;
697                 }
698                 
699                 RocketComponent[] components = new RocketComponent[paths.length];
700                 for (int i = 0; i < paths.length; i++)
701                         components[i] = (RocketComponent) paths[i].getLastPathComponent();
702                 figure.setSelection(components);
703         }
704         
705         
706
707         /**
708          * An <code>Action</code> that shows whether the figure type is the type
709          * given in the constructor.
710          * 
711          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
712          */
713         private class FigureTypeAction extends AbstractAction implements ChangeListener {
714                 private final int type;
715                 
716                 public FigureTypeAction(int type) {
717                         this.type = type;
718                         stateChanged(null);
719                         figure.addChangeListener(this);
720                 }
721                 
722                 @Override
723                 public void actionPerformed(ActionEvent e) {
724                         boolean state = (Boolean) getValue(Action.SELECTED_KEY);
725                         if (state == true) {
726                                 // This view has been selected
727                                 figure.setType(type);
728                                 updateExtras();
729                         }
730                         stateChanged(null);
731                 }
732                 
733                 @Override
734                 public void stateChanged(ChangeEvent e) {
735                         putValue(Action.SELECTED_KEY, figure.getType() == type);
736                 }
737         }
738         
739 }