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