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