1 package net.sf.openrocket.gui.scalefigure;
4 import net.miginfocom.swing.MigLayout;
5 import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
6 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
7 import net.sf.openrocket.aerodynamics.FlightConditions;
8 import net.sf.openrocket.aerodynamics.WarningSet;
9 import net.sf.openrocket.document.OpenRocketDocument;
10 import net.sf.openrocket.document.Simulation;
11 import net.sf.openrocket.gui.adaptors.DoubleModel;
12 import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
13 import net.sf.openrocket.gui.components.BasicSlider;
14 import net.sf.openrocket.gui.components.StageSelector;
15 import net.sf.openrocket.gui.components.UnitSelector;
16 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
17 import net.sf.openrocket.gui.figureelements.CGCaret;
18 import net.sf.openrocket.gui.figureelements.CPCaret;
19 import net.sf.openrocket.gui.figureelements.Caret;
20 import net.sf.openrocket.gui.figureelements.RocketInfo;
21 import net.sf.openrocket.gui.main.SimulationWorker;
22 import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
23 import net.sf.openrocket.masscalc.BasicMassCalculator;
24 import net.sf.openrocket.masscalc.MassCalculator;
25 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
26 import net.sf.openrocket.rocketcomponent.Configuration;
27 import net.sf.openrocket.rocketcomponent.Rocket;
28 import net.sf.openrocket.rocketcomponent.RocketComponent;
29 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
30 import net.sf.openrocket.simulation.FlightData;
31 import net.sf.openrocket.simulation.listeners.SimulationListener;
32 import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
33 import net.sf.openrocket.simulation.listeners.system.InterruptListener;
34 import net.sf.openrocket.unit.UnitGroup;
35 import net.sf.openrocket.util.ChangeSource;
36 import net.sf.openrocket.util.Chars;
37 import net.sf.openrocket.util.Coordinate;
38 import net.sf.openrocket.util.MathUtil;
39 import net.sf.openrocket.util.Prefs;
41 import javax.swing.AbstractAction;
42 import javax.swing.Action;
43 import javax.swing.JComboBox;
44 import javax.swing.JLabel;
45 import javax.swing.JPanel;
46 import javax.swing.JSlider;
47 import javax.swing.JToggleButton;
48 import javax.swing.JViewport;
49 import javax.swing.SwingUtilities;
50 import javax.swing.event.ChangeEvent;
51 import javax.swing.event.ChangeListener;
52 import javax.swing.event.TreeSelectionEvent;
53 import javax.swing.event.TreeSelectionListener;
54 import javax.swing.tree.TreePath;
55 import javax.swing.tree.TreeSelectionModel;
56 import java.awt.Dimension;
58 import java.awt.Point;
59 import java.awt.event.ActionEvent;
60 import java.awt.event.InputEvent;
61 import java.awt.event.MouseEvent;
62 import java.util.ArrayList;
63 import java.util.Collection;
64 import java.util.List;
65 import java.util.concurrent.Executor;
66 import java.util.concurrent.Executors;
67 import java.util.concurrent.ThreadFactory;
70 * A JPanel that contains a RocketFigure and buttons to manipulate the figure.
72 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
74 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
76 private final RocketFigure figure;
77 private final ScaleScrollPane scrollPane;
79 private JLabel infoMessage;
81 private TreeSelectionModel selectionModel = null;
84 /* Calculation of CP and CG */
85 private AerodynamicCalculator aerodynamicCalculator;
86 private MassCalculator massCalculator;
89 private final OpenRocketDocument document;
90 private final Configuration configuration;
92 private Caret extraCP = null;
93 private Caret extraCG = null;
94 private RocketInfo extraText = null;
97 private double cpAOA = Double.NaN;
98 private double cpTheta = Double.NaN;
99 private double cpMach = Double.NaN;
100 private double cpRoll = Double.NaN;
102 // The functional ID of the rocket that was simulated
103 private int flightDataFunctionalID = -1;
104 private String flightDataMotorID = null;
107 private SimulationWorker backgroundSimulationWorker = null;
108 private boolean dirty = false;
110 private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
114 * The executor service used for running the background simulations.
115 * This uses a fixed-sized thread pool for all background simulations
116 * with all threads in daemon mode and with minimum priority.
118 private static final Executor backgroundSimulationExecutor;
120 backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
121 new ThreadFactory() {
122 private ThreadFactory factory = Executors.defaultThreadFactory();
125 public Thread newThread(Runnable r) {
126 Thread t = factory.newThread(r);
128 t.setPriority(Thread.MIN_PRIORITY);
135 public RocketPanel(OpenRocketDocument document) {
137 this.document = document;
138 configuration = document.getDefaultConfiguration();
140 // TODO: FUTURE: calculator selection
141 aerodynamicCalculator = new BarrowmanCalculator();
142 massCalculator = new BasicMassCalculator();
144 // Create figure and custom scroll pane
145 figure = new RocketFigure(configuration);
147 scrollPane = new ScaleScrollPane(figure) {
149 public void mouseClicked(MouseEvent event) {
150 handleMouseClick(event);
153 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
154 scrollPane.setFitting(true);
158 configuration.addChangeListener(new ChangeListener() {
160 public void stateChanged(ChangeEvent e) {
161 System.out.println("Configuration changed, calling updateFigure");
163 figure.updateFigure();
170 * Creates the layout and components of the panel.
172 private void createPanel() {
173 setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
175 setPreferredSize(new Dimension(800, 300));
181 FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
182 action.putValue(Action.NAME, "Side view");
183 action.putValue(Action.SHORT_DESCRIPTION, "Side view");
184 JToggleButton toggle = new JToggleButton(action);
185 add(toggle, "spanx, split");
187 action = new FigureTypeAction(RocketFigure.TYPE_BACK);
188 action.putValue(Action.NAME, "Back view");
189 action.putValue(Action.SHORT_DESCRIPTION, "Rear view");
190 toggle = new JToggleButton(action);
191 add(toggle, "gap rel");
194 // Zoom level selector
195 ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
201 StageSelector stageSelector = new StageSelector(configuration);
202 add(stageSelector, "");
206 // Motor configuration selector
208 JLabel label = new JLabel("Motor configuration:");
209 label.setHorizontalAlignment(JLabel.RIGHT);
210 add(label, "growx, right");
211 add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap");
217 // Create slider and scroll pane
219 DoubleModel theta = new DoubleModel(figure, "Rotation",
220 UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
221 UnitSelector us = new UnitSelector(theta, true);
222 us.setHorizontalAlignment(JLabel.CENTER);
223 add(us, "alignx 50%, growx");
225 // Add the rocket figure
226 add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap");
229 // Add rotation slider
230 // Minimum size to fit "360deg"
231 JLabel l = new JLabel("360" + Chars.DEGREE);
232 Dimension d = l.getPreferredSize();
234 add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true),
235 "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy");
238 infoMessage = new JLabel("<html>" +
239 "Click to select " +
240 "Shift+click to select other " +
241 "Double-click to edit " +
242 "Click+drag to move");
243 infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
244 add(infoMessage, "skip, span, gapleft 25, wrap");
251 public RocketFigure getFigure() {
255 public AerodynamicCalculator getAerodynamicCalculator() {
256 return aerodynamicCalculator;
259 public Configuration getConfiguration() {
260 return configuration;
264 * Get the center of pressure figure element.
266 * @return center of pressure info
268 public Caret getExtraCP () {
273 * Get the center of gravity figure element.
275 * @return center of gravity info
277 public Caret getExtraCG () {
282 * Get the extra text figure element.
284 * @return extra text that contains info about the rocket design
286 public RocketInfo getExtraText () {
290 public void setSelectionModel(TreeSelectionModel m) {
291 if (selectionModel != null) {
292 selectionModel.removeTreeSelectionListener(this);
295 selectionModel.addTreeSelectionListener(this);
296 valueChanged((TreeSelectionEvent) null); // updates FigureParameters
302 * Return the angle of attack used in CP calculation. NaN signifies the default value
304 * @return the angle of attack used, or NaN.
306 public double getCPAOA() {
311 * Set the angle of attack to be used in CP calculation. A value of NaN signifies that
312 * the default AOA (zero) should be used.
313 * @param aoa the angle of attack to use, or NaN
315 public void setCPAOA(double aoa) {
316 if (MathUtil.equals(aoa, cpAOA) ||
317 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
321 figure.updateFigure();
325 public double getCPTheta() {
329 public void setCPTheta(double theta) {
330 if (MathUtil.equals(theta, cpTheta) ||
331 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
334 if (!Double.isNaN(theta))
335 figure.setRotation(theta);
337 figure.updateFigure();
341 public double getCPMach() {
345 public void setCPMach(double mach) {
346 if (MathUtil.equals(mach, cpMach) ||
347 (Double.isNaN(mach) && Double.isNaN(cpMach)))
351 figure.updateFigure();
355 public double getCPRoll() {
359 public void setCPRoll(double roll) {
360 if (MathUtil.equals(roll, cpRoll) ||
361 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
365 figure.updateFigure();
372 public void addChangeListener(ChangeListener listener) {
373 listeners.add(0, listener);
377 public void removeChangeListener(ChangeListener listener) {
378 listeners.remove(listener);
381 protected void fireChangeEvent() {
382 ChangeEvent e = new ChangeEvent(this);
383 ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
384 for (ChangeListener l : list) {
393 * Handle clicking on figure shapes. The functioning is the following:
395 * Get the components clicked.
396 * If no component is clicked, do nothing.
397 * If the currently selected component is in the set, keep it,
398 * unless the selector specified is pressed. If it is pressed, cycle to
399 * the next component. Otherwise select the first component in the list.
401 public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
403 private void handleMouseClick(MouseEvent event) {
404 if (event.getButton() != MouseEvent.BUTTON1)
406 Point p0 = event.getPoint();
407 Point p1 = scrollPane.getViewport().getViewPosition();
411 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
413 // If no component is clicked, do nothing
414 if (clicked.length == 0)
417 // Check whether the currently selected component is in the clicked components.
418 TreePath path = selectionModel.getSelectionPath();
420 RocketComponent current = (RocketComponent) path.getLastPathComponent();
422 for (int i = 0; i < clicked.length; i++) {
423 if (clicked[i] == current) {
424 if (event.isShiftDown() && (event.getClickCount() == 1)) {
425 path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]);
427 path = ComponentTreeModel.makeTreePath(clicked[i]);
434 // Currently selected component not clicked
436 if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
437 path = ComponentTreeModel.makeTreePath(clicked[1]);
439 path = ComponentTreeModel.makeTreePath(clicked[0]);
443 // Set selection and check for double-click
444 selectionModel.setSelectionPath(path);
445 if (event.getClickCount() == 2) {
446 RocketComponent component = (RocketComponent) path.getLastPathComponent();
448 ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
449 document, component);
457 * Updates the extra data included in the figure. Currently this includes
458 * the CP and CG carets.
460 private WarningSet warnings = new WarningSet();
462 private void updateExtras() {
466 // TODO: MEDIUM: User-definable conditions
467 FlightConditions conditions = new FlightConditions(configuration);
470 if (!Double.isNaN(cpMach)) {
471 conditions.setMach(cpMach);
472 extraText.setMach(cpMach);
474 conditions.setMach(Prefs.getDefaultMach());
475 extraText.setMach(Prefs.getDefaultMach());
478 if (!Double.isNaN(cpAOA)) {
479 conditions.setAOA(cpAOA);
481 conditions.setAOA(0);
483 extraText.setAOA(cpAOA);
485 if (!Double.isNaN(cpRoll)) {
486 conditions.setRollRate(cpRoll);
488 conditions.setRollRate(0);
491 if (!Double.isNaN(cpTheta)) {
492 conditions.setTheta(cpTheta);
493 cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
495 cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
497 extraText.setTheta(cpTheta);
500 cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
501 // System.out.println("CG computed as "+cg+ " CP as "+cp);
503 if (cp.weight > 0.000001)
508 if (cg.weight > 0.000001)
513 // Length bound is assumed to be tight
514 double length = 0, diameter = 0;
515 Collection<Coordinate> bounds = configuration.getBounds();
516 if (!bounds.isEmpty()) {
517 double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
518 for (Coordinate c : bounds) {
524 length = maxX - minX;
527 for (RocketComponent c : configuration) {
528 if (c instanceof SymmetricComponent) {
529 double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
530 double d2 = ((SymmetricComponent) c).getAftRadius() * 2;
531 diameter = MathUtil.max(diameter, d1, d2);
535 extraText.setCG(cgx);
536 extraText.setCP(cpx);
537 extraText.setLength(length);
538 extraText.setDiameter(diameter);
539 extraText.setMass(cg.weight);
540 extraText.setWarnings(warnings);
543 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
545 // TODO: LOW: Y-coordinate and rotation
546 extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
547 extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
551 extraCP.setPosition(Double.NaN, Double.NaN);
552 extraCG.setPosition(Double.NaN, Double.NaN);
557 //////// Flight simulation in background
559 // Check whether to compute or not
560 if (!Prefs.computeFlightInBackground()) {
561 extraText.setFlightData(null);
562 extraText.setCalculatingData(false);
563 stopBackgroundSimulation();
567 // Check whether data is already up to date
568 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
569 flightDataMotorID == configuration.getMotorConfigurationID()) {
573 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
574 flightDataMotorID = configuration.getMotorConfigurationID();
576 // Stop previous computation (if any)
577 stopBackgroundSimulation();
579 // Check that configuration has motors
580 if (!configuration.hasMotors()) {
581 extraText.setFlightData(FlightData.NaN_DATA);
582 extraText.setCalculatingData(false);
586 // Start calculation process
587 extraText.setCalculatingData(true);
589 Rocket duplicate = (Rocket) configuration.getRocket().copy();
590 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
591 simulation.getConditions().setMotorConfigurationID(
592 configuration.getMotorConfigurationID());
594 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
595 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
599 * Cancels the current background simulation worker, if any.
601 private void stopBackgroundSimulation() {
602 if (backgroundSimulationWorker != null) {
603 backgroundSimulationWorker.cancel(true);
604 backgroundSimulationWorker = null;
610 * A SimulationWorker that simulates the rocket flight in the background and
611 * sets the results to the extra text when finished. The worker can be cancelled
614 private class BackgroundSimulationWorker extends SimulationWorker {
616 public BackgroundSimulationWorker(Simulation sim) {
621 protected FlightData doInBackground() {
623 // Pause a little while to allow faster UI reaction
626 } catch (InterruptedException ignore) {
628 if (isCancelled() || backgroundSimulationWorker != this)
631 return super.doInBackground();
635 protected void simulationDone() {
636 // Do nothing if cancelled
637 if (isCancelled() || backgroundSimulationWorker != this)
640 backgroundSimulationWorker = null;
641 extraText.setFlightData(simulation.getSimulatedData());
642 extraText.setCalculatingData(false);
647 protected SimulationListener[] getExtraListeners() {
648 return new SimulationListener[] {
649 InterruptListener.INSTANCE,
650 ApogeeEndListener.INSTANCE };
654 protected void simulationInterrupted(Throwable t) {
655 // Do nothing on cancel, set N/A data otherwise
656 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
659 backgroundSimulationWorker = null;
660 extraText.setFlightData(FlightData.NaN_DATA);
661 extraText.setCalculatingData(false);
669 * Adds the extra data to the figure. Currently this includes the CP and CG carets.
671 private void addExtras() {
672 figure.clearRelativeExtra();
673 extraCG = new CGCaret(0, 0);
674 extraCP = new CPCaret(0, 0);
675 extraText = new RocketInfo(configuration);
677 figure.addRelativeExtra(extraCP);
678 figure.addRelativeExtra(extraCG);
679 figure.addAbsoluteExtra(extraText);
684 * Updates the selection in the FigureParameters and repaints the figure.
685 * Ignores the event itself.
688 public void valueChanged(TreeSelectionEvent e) {
689 TreePath[] paths = selectionModel.getSelectionPaths();
691 figure.setSelection(null);
695 RocketComponent[] components = new RocketComponent[paths.length];
696 for (int i = 0; i < paths.length; i++)
697 components[i] = (RocketComponent) paths[i].getLastPathComponent();
698 figure.setSelection(components);
704 * An <code>Action</code> that shows whether the figure type is the type
705 * given in the constructor.
707 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
709 private class FigureTypeAction extends AbstractAction implements ChangeListener {
710 private final int type;
712 public FigureTypeAction(int type) {
715 figure.addChangeListener(this);
719 public void actionPerformed(ActionEvent e) {
720 boolean state = (Boolean) getValue(Action.SELECTED_KEY);
722 // This view has been selected
723 figure.setType(type);
730 public void stateChanged(ChangeEvent e) {
731 putValue(Action.SELECTED_KEY, figure.getType() == type);