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