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