Initial commit
[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.BasicSlider;
41 import net.sf.openrocket.gui.StageSelector;
42 import net.sf.openrocket.gui.UnitSelector;
43 import net.sf.openrocket.gui.adaptors.DoubleModel;
44 import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
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 primary 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                         path = ComponentTreeModel.makeTreePath(clicked[0]);
403                 }
404                 
405                 // Set selection and check for double-click
406                 selectionModel.setSelectionPath(path);
407                 if (event.getClickCount() == 2) {
408                         RocketComponent component = (RocketComponent)path.getLastPathComponent();
409                         
410                         ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this), 
411                                         document, component);
412                 }
413         }
414         
415
416         
417
418         /**
419          * Updates the extra data included in the figure.  Currently this includes
420          * the CP and CG carets.
421          */
422         private WarningSet warnings = new WarningSet();
423         private void updateExtras() {
424                 Coordinate cp,cg;
425                 double cpx, cgx;
426
427                 // TODO: MEDIUM: User-definable conditions
428                 FlightConditions conditions = new FlightConditions(configuration);
429                 warnings.clear();
430
431                 if (!Double.isNaN(cpMach)) {
432                         conditions.setMach(cpMach);
433                         extraText.setMach(cpMach);
434                 } else {
435                         conditions.setMach(Prefs.getDefaultMach());
436                         extraText.setMach(Prefs.getDefaultMach());
437                 }
438                 
439                 if (!Double.isNaN(cpAOA)) {
440                         conditions.setAOA(cpAOA);
441                 } else {
442                         conditions.setAOA(0);
443                 }
444                 extraText.setAOA(cpAOA);
445                 
446                 if (!Double.isNaN(cpRoll)) {
447                         conditions.setRollRate(cpRoll);
448                 } else {
449                         conditions.setRollRate(0);
450                 }
451
452                 if (!Double.isNaN(cpTheta)) {
453                         conditions.setTheta(cpTheta);
454                         cp = calculator.getCP(conditions, warnings);
455                 } else {
456                         cp = calculator.getWorstCP(conditions, warnings);
457                 }
458                 extraText.setTheta(cpTheta);
459                 
460
461                 cg = calculator.getCG(0);
462 //              System.out.println("CG computed as "+cg+ " CP as "+cp);
463                 
464                 if (cp.weight > 0.000001)
465                         cpx = cp.x;
466                 else
467                         cpx = Double.NaN;
468                 
469                 if (cg.weight > 0.000001)
470                         cgx = cg.x;
471                 else
472                         cgx = Double.NaN;
473                 
474                 // Length bound is assumed to be tight
475                 double length = 0, diameter = 0;
476                 Collection<Coordinate> bounds = configuration.getBounds();
477                 if (!bounds.isEmpty()) {
478                         double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
479                         for (Coordinate c: bounds) {
480                                 if (c.x < minX)
481                                         minX = c.x;
482                                 if (c.x > maxX)
483                                         maxX = c.x;
484                         }
485                         length = maxX - minX;
486                 }
487                 
488                 for (RocketComponent c: configuration) {
489                         if (c instanceof SymmetricComponent) {
490                                 double d1 = ((SymmetricComponent)c).getForeRadius() * 2;
491                                 double d2 = ((SymmetricComponent)c).getAftRadius() * 2;
492                                 diameter = MathUtil.max(diameter, d1, d2);
493                         }
494                 }
495
496                 extraText.setCG(cgx);
497                 extraText.setCP(cpx);
498                 extraText.setLength(length);
499                 extraText.setDiameter(diameter);
500                 extraText.setMass(cg.weight);
501                 extraText.setWarnings(warnings);
502                         
503                 
504                 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
505
506                         // TODO: LOW: Y-coordinate and rotation
507                         extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
508                         extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
509
510                 } else {
511                         
512                         extraCP.setPosition(Double.NaN, Double.NaN);
513                         extraCG.setPosition(Double.NaN, Double.NaN);
514                         
515                 }
516                 
517                 
518                 ////////  Flight simulation in background
519                 
520                 // Check whether to compute or not
521                 if (!Prefs.computeFlightInBackground()) {
522                         extraText.setFlightData(null);
523                         extraText.setCalculatingData(false);
524                         stopBackgroundSimulation();
525                         return;
526                 }
527                 
528                 // Check whether data is already up to date
529                 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
530                                 flightDataMotorID == configuration.getMotorConfigurationID()) {
531                         return;
532                 }
533
534                 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
535                 flightDataMotorID = configuration.getMotorConfigurationID();
536                 
537                 // Stop previous computation (if any)
538                 stopBackgroundSimulation();
539                 
540                 // Check that configuration has motors
541                 if (!configuration.hasMotors()) {
542                         extraText.setFlightData(FlightData.NaN_DATA);
543                         extraText.setCalculatingData(false);
544                         return;
545                 }
546
547                 // Start calculation process
548                 extraText.setCalculatingData(true);
549                 
550                 Rocket duplicate = configuration.getRocket().copy();
551                 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
552                 simulation.getConditions().setMotorConfigurationID(
553                                 configuration.getMotorConfigurationID());
554
555                 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
556                 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
557         }
558         
559         /**
560          * Cancels the current background simulation worker, if any.
561          */
562         private void stopBackgroundSimulation() {
563                 if (backgroundSimulationWorker != null) {
564                         backgroundSimulationWorker.cancel(true);
565                         backgroundSimulationWorker = null;
566                 }
567         }
568         
569
570         /**
571          * A SimulationWorker that simulates the rocket flight in the background and
572          * sets the results to the extra text when finished.  The worker can be cancelled
573          * if necessary.
574          */
575         private class BackgroundSimulationWorker extends SimulationWorker {
576
577                 public BackgroundSimulationWorker(Simulation sim) {
578                         super(sim);
579                 }
580                 
581                 @Override
582                 protected FlightData doInBackground() {
583                         
584                         // Pause a little while to allow faster UI reaction
585                         try {
586                                 Thread.sleep(300);
587                         } catch (InterruptedException ignore) { }
588                         if (isCancelled() || backgroundSimulationWorker != this)
589                                 return null;
590                         
591                         return super.doInBackground();
592                 }
593
594                 @Override
595                 protected void simulationDone() {
596                         // Do nothing if cancelled
597                         if (isCancelled() || backgroundSimulationWorker != this)  // Double-check
598                                 return;
599                         
600                         backgroundSimulationWorker = null;
601                         extraText.setFlightData(simulation.getSimulatedData());
602                         extraText.setCalculatingData(false);
603                         figure.repaint();
604                 }
605
606                 @Override
607                 protected SimulationListener[] getExtraListeners() {
608                         return new SimulationListener[] {
609                                         InterruptListener.INSTANCE,
610                                         ApogeeEndListener.INSTANCE
611                         };
612                 }
613
614                 @Override
615                 protected void simulationInterrupted(Throwable t) {
616                         // Do nothing on cancel, set N/A data otherwise
617                         if (isCancelled() || backgroundSimulationWorker != this)  // Double-check
618                                 return;
619                         
620                         backgroundSimulationWorker = null;
621                         extraText.setFlightData(FlightData.NaN_DATA);
622                         extraText.setCalculatingData(false);
623                         figure.repaint();
624                 }
625         }
626         
627         
628         
629         /**
630          * Adds the extra data to the figure.  Currently this includes the CP and CG carets.
631          */
632         private void addExtras() {
633                 figure.clearRelativeExtra();
634                 extraCG = new CGCaret(0,0);
635                 extraCP = new CPCaret(0,0);
636                 extraText = new RocketInfo(configuration);
637                 updateExtras();
638                 figure.addRelativeExtra(extraCP);
639                 figure.addRelativeExtra(extraCG);
640                 figure.addAbsoluteExtra(extraText);
641         }
642
643         
644         /**
645          * Updates the selection in the FigureParameters and repaints the figure.  
646          * Ignores the event itself.
647          */
648         public void valueChanged(TreeSelectionEvent e) {
649                 TreePath[] paths = selectionModel.getSelectionPaths();
650                 if (paths==null) {
651                         figure.setSelection(null);
652                         return;
653                 }
654                 
655                 RocketComponent[] components = new RocketComponent[paths.length];
656                 for (int i=0; i<paths.length; i++)
657                         components[i] = (RocketComponent)paths[i].getLastPathComponent();
658                 figure.setSelection(components);
659         }
660
661         
662         
663         /**
664          * An <code>Action</code> that shows whether the figure type is the type
665          * given in the constructor.
666          * 
667          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
668          */
669         private class FigureTypeAction extends AbstractAction implements ChangeListener {
670                 private final int type;
671                 
672                 public FigureTypeAction(int type) {
673                         this.type = type;
674                         stateChanged(null);
675                         figure.addChangeListener(this);
676                 }
677                 
678                 public void actionPerformed(ActionEvent e) {
679                         boolean state = (Boolean)getValue(Action.SELECTED_KEY);
680                         if (state == true) {
681                                 // This view has been selected
682                                 figure.setType(type);
683                                 updateExtras();
684                         }
685                         stateChanged(null);
686                 }
687
688                 public void stateChanged(ChangeEvent e) {
689                         putValue(Action.SELECTED_KEY,figure.getType() == type);
690                 }
691         }
692
693 }