1 package net.sf.openrocket.gui.scalefigure;
4 import java.awt.BorderLayout;
5 import java.awt.Dimension;
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;
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;
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;
79 * A JPanel that contains a RocketFigure and buttons to manipulate the figure.
81 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
82 * @author Bill Kuker <bkuker@billkuker.com>
84 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
85 private static final long serialVersionUID = 1L;
87 private static final Translator trans = Application.getTranslator();
90 private final RocketFigure figure;
91 private final RocketFigure3d figure3d;
94 private final ScaleScrollPane scrollPane;
96 private final JPanel figureHolder;
98 private JLabel infoMessage;
100 private TreeSelectionModel selectionModel = null;
102 private BasicSlider rotationSlider;
103 ScaleSelector scaleSelector;
106 /* Calculation of CP and CG */
107 private AerodynamicCalculator aerodynamicCalculator;
108 private MassCalculator massCalculator;
111 private final OpenRocketDocument document;
112 private final Configuration configuration;
114 private Caret extraCP = null;
115 private Caret extraCG = null;
116 private RocketInfo extraText = null;
119 private double cpAOA = Double.NaN;
120 private double cpTheta = Double.NaN;
121 private double cpMach = Double.NaN;
122 private double cpRoll = Double.NaN;
124 // The functional ID of the rocket that was simulated
125 private int flightDataFunctionalID = -1;
126 private String flightDataMotorID = null;
129 private SimulationWorker backgroundSimulationWorker = null;
131 private List<EventListener> listeners = new ArrayList<EventListener>();
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.
139 private static final Executor backgroundSimulationExecutor;
141 backgroundSimulationExecutor = Executors.newFixedThreadPool(SwingPreferences.getMaxThreadCount(),
142 new ThreadFactory() {
143 private ThreadFactory factory = Executors.defaultThreadFactory();
146 public Thread newThread(Runnable r) {
147 Thread t = factory.newThread(r);
149 t.setPriority(Thread.MIN_PRIORITY);
156 public RocketPanel(OpenRocketDocument document) {
158 this.document = document;
159 configuration = document.getDefaultConfiguration();
161 // TODO: FUTURE: calculator selection
162 aerodynamicCalculator = new BarrowmanCalculator();
163 massCalculator = new BasicMassCalculator();
165 // Create figure and custom scroll pane
166 figure = new RocketFigure(configuration);
167 figure3d = new RocketFigure3d(configuration);
169 figureHolder = new JPanel(new BorderLayout());
171 scrollPane = new ScaleScrollPane(figure) {
172 private static final long serialVersionUID = 1L;
175 public void mouseClicked(MouseEvent event) {
176 handleMouseClick(event);
179 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
180 scrollPane.setFitting(true);
187 configuration.addChangeListener(new StateChangeListener() {
189 public void stateChanged(EventObject e) {
190 // System.out.println("Configuration changed, calling updateFigure");
196 figure3d.addComponentSelectionListener(new RocketFigure3d.ComponentSelectionListener() {
198 public void componentClicked(RocketComponent clicked[], MouseEvent event) {
199 handleComponentClick(clicked, event);
204 private void updateFigures() {
206 figure.updateFigure();
208 figure3d.updateFigure();
211 private void go3D() {
215 figureHolder.remove(scrollPane);
216 figureHolder.add(figure3d, BorderLayout.CENTER);
217 rotationSlider.setEnabled(false);
218 scaleSelector.setEnabled(false);
221 figureHolder.revalidate();
226 private void go2D() {
230 figureHolder.remove(figure3d);
231 figureHolder.add(scrollPane, BorderLayout.CENTER);
232 rotationSlider.setEnabled(true);
233 scaleSelector.setEnabled(true);
235 figureHolder.revalidate();
240 * Creates the layout and components of the panel.
242 private void createPanel() {
243 setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
245 setPreferredSize(new Dimension(800, 300));
250 ButtonGroup bg = new ButtonGroup();
253 FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
255 action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Sideview"));
257 action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Sideview"));
258 JToggleButton toggle = new JToggleButton(action);
260 add(toggle, "spanx, split");
262 action = new FigureTypeAction(RocketFigure.TYPE_BACK);
264 action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Backview"));
266 action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Backview"));
267 toggle = new JToggleButton(action);
269 add(toggle, "gap rel");
272 final JToggleButton toggle3d = new JToggleButton(new AbstractAction("3D") {
273 private static final long serialVersionUID = 1L;
275 putValue(Action.NAME, "3D");//TODO
276 putValue(Action.SHORT_DESCRIPTION, "3D"); //TODO
279 public void actionPerformed(ActionEvent e) {
280 if ( ((JToggleButton)e.getSource()).isSelected() ){
288 toggle3d.setEnabled(RocketFigure3d.is3dEnabled());
289 add(toggle3d, "gap rel");
291 // Zoom level selector
292 scaleSelector = new ScaleSelector(scrollPane);
298 StageSelector stageSelector = new StageSelector(configuration);
299 add(stageSelector, "");
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");
314 // Create slider and scroll pane
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");
322 // Add the rocket figure
323 add(figureHolder, "grow, spany 2, wmin 300lp, hmin 100lp, wrap");
326 // Add rotation slider
327 // Minimum size to fit "360deg"
328 JLabel l = new JLabel("360" + Chars.DEGREE);
329 Dimension d = l.getPreferredSize();
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");
335 //// <html>Click to select Shift+click to select other Double-click to edit 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");
346 public RocketFigure getFigure() {
350 public AerodynamicCalculator getAerodynamicCalculator() {
351 return aerodynamicCalculator;
354 public Configuration getConfiguration() {
355 return configuration;
359 * Get the center of pressure figure element.
361 * @return center of pressure info
363 public Caret getExtraCP() {
368 * Get the center of gravity figure element.
370 * @return center of gravity info
372 public Caret getExtraCG() {
377 * Get the extra text figure element.
379 * @return extra text that contains info about the rocket design
381 public RocketInfo getExtraText() {
385 public void setSelectionModel(TreeSelectionModel m) {
386 if (selectionModel != null) {
387 selectionModel.removeTreeSelectionListener(this);
390 selectionModel.addTreeSelectionListener(this);
391 valueChanged((TreeSelectionEvent) null); // updates FigureParameters
397 * Return the angle of attack used in CP calculation. NaN signifies the default value
399 * @return the angle of attack used, or NaN.
401 public double getCPAOA() {
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
410 public void setCPAOA(double aoa) {
411 if (MathUtil.equals(aoa, cpAOA) ||
412 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
420 public double getCPTheta() {
424 public void setCPTheta(double theta) {
425 if (MathUtil.equals(theta, cpTheta) ||
426 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
429 if (!Double.isNaN(theta))
430 figure.setRotation(theta);
436 public double getCPMach() {
440 public void setCPMach(double mach) {
441 if (MathUtil.equals(mach, cpMach) ||
442 (Double.isNaN(mach) && Double.isNaN(cpMach)))
450 public double getCPRoll() {
454 public void setCPRoll(double roll) {
455 if (MathUtil.equals(roll, cpRoll) ||
456 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
467 public void addChangeListener(EventListener listener) {
468 listeners.add(0, listener);
472 public void removeChangeListener(EventListener listener) {
473 listeners.remove(listener);
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);
489 * Handle clicking on figure shapes. The functioning is the following:
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.
497 public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
499 private void handleMouseClick(MouseEvent event) {
500 if (event.getButton() != MouseEvent.BUTTON1)
502 Point p0 = event.getPoint();
503 Point p1 = scrollPane.getViewport().getViewPosition();
507 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
509 handleComponentClick(clicked, event);
512 private void handleComponentClick(RocketComponent[] clicked, MouseEvent event){
514 // If no component is clicked, do nothing
515 if (clicked.length == 0)
518 // Check whether the currently selected component is in the clicked components.
519 TreePath path = selectionModel.getSelectionPath();
521 RocketComponent current = (RocketComponent) path.getLastPathComponent();
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]);
528 path = ComponentTreeModel.makeTreePath(clicked[i]);
535 // Currently selected component not clicked
537 if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
538 path = ComponentTreeModel.makeTreePath(clicked[1]);
540 path = ComponentTreeModel.makeTreePath(clicked[0]);
544 // Set selection and check for double-click
545 selectionModel.setSelectionPath(path);
546 if (event.getClickCount() == 2) {
547 RocketComponent component = (RocketComponent) path.getLastPathComponent();
549 ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
550 document, component);
558 * Updates the extra data included in the figure. Currently this includes
559 * the CP and CG carets.
561 private WarningSet warnings = new WarningSet();
563 private void updateExtras() {
567 // TODO: MEDIUM: User-definable conditions
568 FlightConditions conditions = new FlightConditions(configuration);
571 if (!Double.isNaN(cpMach)) {
572 conditions.setMach(cpMach);
573 extraText.setMach(cpMach);
575 conditions.setMach(Application.getPreferences().getDefaultMach());
576 extraText.setMach(Application.getPreferences().getDefaultMach());
579 if (!Double.isNaN(cpAOA)) {
580 conditions.setAOA(cpAOA);
582 conditions.setAOA(0);
584 extraText.setAOA(cpAOA);
586 if (!Double.isNaN(cpRoll)) {
587 conditions.setRollRate(cpRoll);
589 conditions.setRollRate(0);
592 if (!Double.isNaN(cpTheta)) {
593 conditions.setTheta(cpTheta);
594 cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
596 cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
598 extraText.setTheta(cpTheta);
601 cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
602 // System.out.println("CG computed as "+cg+ " CP as "+cp);
604 if (cp.weight > 0.000001)
609 if (cg.weight > 0.000001)
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) {
628 length = maxX - minX;
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);
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);
647 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
649 // TODO: LOW: Y-coordinate and rotation
650 extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
651 extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
655 extraCP.setPosition(Double.NaN, Double.NaN);
656 extraCG.setPosition(Double.NaN, Double.NaN);
661 //////// Flight simulation in background
663 // Check whether to compute or not
664 if (!((SwingPreferences) Application.getPreferences()).computeFlightInBackground()) {
665 extraText.setFlightData(null);
666 extraText.setCalculatingData(false);
667 stopBackgroundSimulation();
671 // Check whether data is already up to date
672 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
673 flightDataMotorID == configuration.getMotorConfigurationID()) {
677 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
678 flightDataMotorID = configuration.getMotorConfigurationID();
680 // Stop previous computation (if any)
681 stopBackgroundSimulation();
683 // Check that configuration has motors
684 if (!configuration.hasMotors()) {
685 extraText.setFlightData(FlightData.NaN_DATA);
686 extraText.setCalculatingData(false);
690 // Start calculation process
691 extraText.setCalculatingData(true);
693 Rocket duplicate = (Rocket) configuration.getRocket().copy();
694 Simulation simulation = ((SwingPreferences)Application.getPreferences()).getBackgroundSimulation(duplicate);
695 simulation.getOptions().setMotorConfigurationID(
696 configuration.getMotorConfigurationID());
698 backgroundSimulationWorker = new BackgroundSimulationWorker(document, simulation);
699 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
703 * Cancels the current background simulation worker, if any.
705 private void stopBackgroundSimulation() {
706 if (backgroundSimulationWorker != null) {
707 backgroundSimulationWorker.cancel(true);
708 backgroundSimulationWorker = null;
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
718 private class BackgroundSimulationWorker extends SimulationWorker {
720 private final CustomExpressionSimulationListener exprListener;
722 public BackgroundSimulationWorker(OpenRocketDocument doc, Simulation sim) {
724 List<CustomExpression> exprs = doc.getCustomExpressions();
725 exprListener = new CustomExpressionSimulationListener(exprs);
729 protected FlightData doInBackground() {
731 // Pause a little while to allow faster UI reaction
734 } catch (InterruptedException ignore) {
736 if (isCancelled() || backgroundSimulationWorker != this)
739 return super.doInBackground();
743 protected void simulationDone() {
744 // Do nothing if cancelled
745 if (isCancelled() || backgroundSimulationWorker != this)
748 backgroundSimulationWorker = null;
749 extraText.setFlightData(simulation.getSimulatedData());
750 extraText.setCalculatingData(false);
756 protected SimulationListener[] getExtraListeners() {
757 return new SimulationListener[] {
758 InterruptListener.INSTANCE,
759 ApogeeEndListener.INSTANCE,
765 protected void simulationInterrupted(Throwable t) {
766 // Do nothing on cancel, set N/A data otherwise
767 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
770 backgroundSimulationWorker = null;
771 extraText.setFlightData(FlightData.NaN_DATA);
772 extraText.setCalculatingData(false);
781 * Adds the extra data to the figure. Currently this includes the CP and CG carets.
783 private void addExtras() {
784 extraCG = new CGCaret(0, 0);
785 extraCP = new CPCaret(0, 0);
786 extraText = new RocketInfo(configuration);
789 figure.clearRelativeExtra();
790 figure.addRelativeExtra(extraCP);
791 figure.addRelativeExtra(extraCG);
792 figure.addAbsoluteExtra(extraText);
795 figure3d.clearRelativeExtra();
796 //figure3d.addRelativeExtra(extraCP);
797 //figure3d.addRelativeExtra(extraCG);
798 figure3d.addAbsoluteExtra(extraText);
804 * Updates the selection in the FigureParameters and repaints the figure.
805 * Ignores the event itself.
808 public void valueChanged(TreeSelectionEvent e) {
809 TreePath[] paths = selectionModel.getSelectionPaths();
811 figure.setSelection(null);
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);
820 figure3d.setSelection(components);
826 * An <code>Action</code> that shows whether the figure type is the type
827 * given in the constructor.
829 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
831 private class FigureTypeAction extends AbstractAction implements StateChangeListener {
832 private static final long serialVersionUID = 1L;
833 private final int type;
835 public FigureTypeAction(int type) {
838 figure.addChangeListener(this);
842 public void actionPerformed(ActionEvent e) {
843 boolean state = (Boolean) getValue(Action.SELECTED_KEY);
845 // This view has been selected
846 figure.setType(type);
854 public void stateChanged(EventObject e) {
855 putValue(Action.SELECTED_KEY, figure.getType() == type && !is3d);