1 package net.sf.openrocket.gui.scalefigure;
4 import java.awt.Dimension;
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;
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;
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.SimulationWorker;
51 import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
52 import net.sf.openrocket.masscalc.BasicMassCalculator;
53 import net.sf.openrocket.masscalc.MassCalculator;
54 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
55 import net.sf.openrocket.rocketcomponent.Configuration;
56 import net.sf.openrocket.rocketcomponent.Rocket;
57 import net.sf.openrocket.rocketcomponent.RocketComponent;
58 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
59 import net.sf.openrocket.simulation.FlightData;
60 import net.sf.openrocket.simulation.listeners.SimulationListener;
61 import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
62 import net.sf.openrocket.simulation.listeners.system.InterruptListener;
63 import net.sf.openrocket.unit.UnitGroup;
64 import net.sf.openrocket.util.ChangeSource;
65 import net.sf.openrocket.util.Chars;
66 import net.sf.openrocket.util.Coordinate;
67 import net.sf.openrocket.util.MathUtil;
68 import net.sf.openrocket.util.Prefs;
71 * A JPanel that contains a RocketFigure and buttons to manipulate the figure.
73 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
75 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
77 private final RocketFigure figure;
78 private final ScaleScrollPane scrollPane;
80 private JLabel infoMessage;
82 private TreeSelectionModel selectionModel = null;
85 /* Calculation of CP and CG */
86 private AerodynamicCalculator aerodynamicCalculator;
87 private MassCalculator massCalculator;
90 private final OpenRocketDocument document;
91 private final Configuration configuration;
93 private Caret extraCP = null;
94 private Caret extraCG = null;
95 private RocketInfo extraText = null;
98 private double cpAOA = Double.NaN;
99 private double cpTheta = Double.NaN;
100 private double cpMach = Double.NaN;
101 private double cpRoll = Double.NaN;
103 // The functional ID of the rocket that was simulated
104 private int flightDataFunctionalID = -1;
105 private String flightDataMotorID = null;
108 private SimulationWorker backgroundSimulationWorker = null;
111 private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
115 * The executor service used for running the background simulations.
116 * This uses a fixed-sized thread pool for all background simulations
117 * with all threads in daemon mode and with minimum priority.
119 private static final Executor backgroundSimulationExecutor;
121 backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
122 new ThreadFactory() {
123 private ThreadFactory factory = Executors.defaultThreadFactory();
126 public Thread newThread(Runnable r) {
127 Thread t = factory.newThread(r);
129 t.setPriority(Thread.MIN_PRIORITY);
136 public RocketPanel(OpenRocketDocument document) {
138 this.document = document;
139 configuration = document.getDefaultConfiguration();
141 // TODO: FUTURE: calculator selection
142 aerodynamicCalculator = new BarrowmanCalculator();
143 massCalculator = new BasicMassCalculator();
145 // Create figure and custom scroll pane
146 figure = new RocketFigure(configuration);
148 scrollPane = new ScaleScrollPane(figure) {
150 public void mouseClicked(MouseEvent event) {
151 handleMouseClick(event);
154 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
155 scrollPane.setFitting(true);
159 configuration.addChangeListener(new ChangeListener() {
161 public void stateChanged(ChangeEvent e) {
162 System.out.println("Configuration changed, calling updateFigure");
164 figure.updateFigure();
171 * Creates the layout and components of the panel.
173 private void createPanel() {
174 setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
176 setPreferredSize(new Dimension(800, 300));
182 FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
183 action.putValue(Action.NAME, "Side view");
184 action.putValue(Action.SHORT_DESCRIPTION, "Side view");
185 JToggleButton toggle = new JToggleButton(action);
186 add(toggle, "spanx, split");
188 action = new FigureTypeAction(RocketFigure.TYPE_BACK);
189 action.putValue(Action.NAME, "Back view");
190 action.putValue(Action.SHORT_DESCRIPTION, "Rear view");
191 toggle = new JToggleButton(action);
192 add(toggle, "gap rel");
195 // Zoom level selector
196 ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
202 StageSelector stageSelector = new StageSelector(configuration);
203 add(stageSelector, "");
207 // Motor configuration selector
209 JLabel label = new JLabel("Motor configuration:");
210 label.setHorizontalAlignment(JLabel.RIGHT);
211 add(label, "growx, right");
212 add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap");
218 // Create slider and scroll pane
220 DoubleModel theta = new DoubleModel(figure, "Rotation",
221 UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
222 UnitSelector us = new UnitSelector(theta, true);
223 us.setHorizontalAlignment(JLabel.CENTER);
224 add(us, "alignx 50%, growx");
226 // Add the rocket figure
227 add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap");
230 // Add rotation slider
231 // Minimum size to fit "360deg"
232 JLabel l = new JLabel("360" + Chars.DEGREE);
233 Dimension d = l.getPreferredSize();
235 add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true),
236 "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy");
239 infoMessage = new JLabel("<html>" +
240 "Click to select " +
241 "Shift+click to select other " +
242 "Double-click to edit " +
243 "Click+drag to move");
244 infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
245 add(infoMessage, "skip, span, gapleft 25, wrap");
252 public RocketFigure getFigure() {
256 public AerodynamicCalculator getAerodynamicCalculator() {
257 return aerodynamicCalculator;
260 public Configuration getConfiguration() {
261 return configuration;
264 public void setSelectionModel(TreeSelectionModel m) {
265 if (selectionModel != null) {
266 selectionModel.removeTreeSelectionListener(this);
269 selectionModel.addTreeSelectionListener(this);
270 valueChanged((TreeSelectionEvent) null); // updates FigureParameters
276 * Return the angle of attack used in CP calculation. NaN signifies the default value
278 * @return the angle of attack used, or NaN.
280 public double getCPAOA() {
285 * Set the angle of attack to be used in CP calculation. A value of NaN signifies that
286 * the default AOA (zero) should be used.
287 * @param aoa the angle of attack to use, or NaN
289 public void setCPAOA(double aoa) {
290 if (MathUtil.equals(aoa, cpAOA) ||
291 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
295 figure.updateFigure();
299 public double getCPTheta() {
303 public void setCPTheta(double theta) {
304 if (MathUtil.equals(theta, cpTheta) ||
305 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
308 if (!Double.isNaN(theta))
309 figure.setRotation(theta);
311 figure.updateFigure();
315 public double getCPMach() {
319 public void setCPMach(double mach) {
320 if (MathUtil.equals(mach, cpMach) ||
321 (Double.isNaN(mach) && Double.isNaN(cpMach)))
325 figure.updateFigure();
329 public double getCPRoll() {
333 public void setCPRoll(double roll) {
334 if (MathUtil.equals(roll, cpRoll) ||
335 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
339 figure.updateFigure();
346 public void addChangeListener(ChangeListener listener) {
347 listeners.add(0, listener);
351 public void removeChangeListener(ChangeListener listener) {
352 listeners.remove(listener);
355 protected void fireChangeEvent() {
356 ChangeEvent e = new ChangeEvent(this);
357 ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
358 for (ChangeListener l : list) {
367 * Handle clicking on figure shapes. The functioning is the following:
369 * Get the components clicked.
370 * If no component is clicked, do nothing.
371 * If the currently selected component is in the set, keep it,
372 * unless the selector specified is pressed. If it is pressed, cycle to
373 * the next component. Otherwise select the first component in the list.
375 public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
377 private void handleMouseClick(MouseEvent event) {
378 if (event.getButton() != MouseEvent.BUTTON1)
380 Point p0 = event.getPoint();
381 Point p1 = scrollPane.getViewport().getViewPosition();
385 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
387 // If no component is clicked, do nothing
388 if (clicked.length == 0)
391 // Check whether the currently selected component is in the clicked components.
392 TreePath path = selectionModel.getSelectionPath();
394 RocketComponent current = (RocketComponent) path.getLastPathComponent();
396 for (int i = 0; i < clicked.length; i++) {
397 if (clicked[i] == current) {
398 if (event.isShiftDown() && (event.getClickCount() == 1)) {
399 path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]);
401 path = ComponentTreeModel.makeTreePath(clicked[i]);
408 // Currently selected component not clicked
410 if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
411 path = ComponentTreeModel.makeTreePath(clicked[1]);
413 path = ComponentTreeModel.makeTreePath(clicked[0]);
417 // Set selection and check for double-click
418 selectionModel.setSelectionPath(path);
419 if (event.getClickCount() == 2) {
420 RocketComponent component = (RocketComponent) path.getLastPathComponent();
422 ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
423 document, component);
431 * Updates the extra data included in the figure. Currently this includes
432 * the CP and CG carets.
434 private WarningSet warnings = new WarningSet();
436 private void updateExtras() {
440 // TODO: MEDIUM: User-definable conditions
441 FlightConditions conditions = new FlightConditions(configuration);
444 if (!Double.isNaN(cpMach)) {
445 conditions.setMach(cpMach);
446 extraText.setMach(cpMach);
448 conditions.setMach(Prefs.getDefaultMach());
449 extraText.setMach(Prefs.getDefaultMach());
452 if (!Double.isNaN(cpAOA)) {
453 conditions.setAOA(cpAOA);
455 conditions.setAOA(0);
457 extraText.setAOA(cpAOA);
459 if (!Double.isNaN(cpRoll)) {
460 conditions.setRollRate(cpRoll);
462 conditions.setRollRate(0);
465 if (!Double.isNaN(cpTheta)) {
466 conditions.setTheta(cpTheta);
467 cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
469 cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
471 extraText.setTheta(cpTheta);
474 cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
475 // System.out.println("CG computed as "+cg+ " CP as "+cp);
477 if (cp.weight > 0.000001)
482 if (cg.weight > 0.000001)
487 // Length bound is assumed to be tight
488 double length = 0, diameter = 0;
489 Collection<Coordinate> bounds = configuration.getBounds();
490 if (!bounds.isEmpty()) {
491 double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
492 for (Coordinate c : bounds) {
498 length = maxX - minX;
501 for (RocketComponent c : configuration) {
502 if (c instanceof SymmetricComponent) {
503 double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
504 double d2 = ((SymmetricComponent) c).getAftRadius() * 2;
505 diameter = MathUtil.max(diameter, d1, d2);
509 extraText.setCG(cgx);
510 extraText.setCP(cpx);
511 extraText.setLength(length);
512 extraText.setDiameter(diameter);
513 extraText.setMass(cg.weight);
514 extraText.setWarnings(warnings);
517 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
519 // TODO: LOW: Y-coordinate and rotation
520 extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
521 extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
525 extraCP.setPosition(Double.NaN, Double.NaN);
526 extraCG.setPosition(Double.NaN, Double.NaN);
531 //////// Flight simulation in background
533 // Check whether to compute or not
534 if (!Prefs.computeFlightInBackground()) {
535 extraText.setFlightData(null);
536 extraText.setCalculatingData(false);
537 stopBackgroundSimulation();
541 // Check whether data is already up to date
542 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
543 flightDataMotorID == configuration.getMotorConfigurationID()) {
547 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
548 flightDataMotorID = configuration.getMotorConfigurationID();
550 // Stop previous computation (if any)
551 stopBackgroundSimulation();
553 // Check that configuration has motors
554 if (!configuration.hasMotors()) {
555 extraText.setFlightData(FlightData.NaN_DATA);
556 extraText.setCalculatingData(false);
560 // Start calculation process
561 extraText.setCalculatingData(true);
563 Rocket duplicate = (Rocket) configuration.getRocket().copy();
564 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
565 simulation.getConditions().setMotorConfigurationID(
566 configuration.getMotorConfigurationID());
568 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
569 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
573 * Cancels the current background simulation worker, if any.
575 private void stopBackgroundSimulation() {
576 if (backgroundSimulationWorker != null) {
577 backgroundSimulationWorker.cancel(true);
578 backgroundSimulationWorker = null;
584 * A SimulationWorker that simulates the rocket flight in the background and
585 * sets the results to the extra text when finished. The worker can be cancelled
588 private class BackgroundSimulationWorker extends SimulationWorker {
590 public BackgroundSimulationWorker(Simulation sim) {
595 protected FlightData doInBackground() {
597 // Pause a little while to allow faster UI reaction
600 } catch (InterruptedException ignore) {
602 if (isCancelled() || backgroundSimulationWorker != this)
605 return super.doInBackground();
609 protected void simulationDone() {
610 // Do nothing if cancelled
611 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
614 backgroundSimulationWorker = null;
615 extraText.setFlightData(simulation.getSimulatedData());
616 extraText.setCalculatingData(false);
621 protected SimulationListener[] getExtraListeners() {
622 return new SimulationListener[] {
623 InterruptListener.INSTANCE,
624 ApogeeEndListener.INSTANCE };
628 protected void simulationInterrupted(Throwable t) {
629 // Do nothing on cancel, set N/A data otherwise
630 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
633 backgroundSimulationWorker = null;
634 extraText.setFlightData(FlightData.NaN_DATA);
635 extraText.setCalculatingData(false);
643 * Adds the extra data to the figure. Currently this includes the CP and CG carets.
645 private void addExtras() {
646 figure.clearRelativeExtra();
647 extraCG = new CGCaret(0, 0);
648 extraCP = new CPCaret(0, 0);
649 extraText = new RocketInfo(configuration);
651 figure.addRelativeExtra(extraCP);
652 figure.addRelativeExtra(extraCG);
653 figure.addAbsoluteExtra(extraText);
658 * Updates the selection in the FigureParameters and repaints the figure.
659 * Ignores the event itself.
661 public void valueChanged(TreeSelectionEvent e) {
662 TreePath[] paths = selectionModel.getSelectionPaths();
664 figure.setSelection(null);
668 RocketComponent[] components = new RocketComponent[paths.length];
669 for (int i = 0; i < paths.length; i++)
670 components[i] = (RocketComponent) paths[i].getLastPathComponent();
671 figure.setSelection(components);
677 * An <code>Action</code> that shows whether the figure type is the type
678 * given in the constructor.
680 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
682 private class FigureTypeAction extends AbstractAction implements ChangeListener {
683 private final int type;
685 public FigureTypeAction(int type) {
688 figure.addChangeListener(this);
691 public void actionPerformed(ActionEvent e) {
692 boolean state = (Boolean) getValue(Action.SELECTED_KEY);
694 // This view has been selected
695 figure.setType(type);
701 public void stateChanged(ChangeEvent e) {
702 putValue(Action.SELECTED_KEY, figure.getType() == type);