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.EventListener;
13 import java.util.EventObject;
14 import java.util.List;
15 import java.util.concurrent.Executor;
16 import java.util.concurrent.Executors;
17 import java.util.concurrent.ThreadFactory;
19 import javax.swing.AbstractAction;
20 import javax.swing.Action;
21 import javax.swing.JComboBox;
22 import javax.swing.JLabel;
23 import javax.swing.JPanel;
24 import javax.swing.JSlider;
25 import javax.swing.JToggleButton;
26 import javax.swing.JViewport;
27 import javax.swing.SwingUtilities;
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.gui.util.SwingPreferences;
53 import net.sf.openrocket.l10n.Translator;
54 import net.sf.openrocket.masscalc.BasicMassCalculator;
55 import net.sf.openrocket.masscalc.MassCalculator;
56 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
57 import net.sf.openrocket.rocketcomponent.Configuration;
58 import net.sf.openrocket.rocketcomponent.Rocket;
59 import net.sf.openrocket.rocketcomponent.RocketComponent;
60 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
61 import net.sf.openrocket.simulation.FlightData;
62 import net.sf.openrocket.simulation.listeners.SimulationListener;
63 import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
64 import net.sf.openrocket.simulation.listeners.system.InterruptListener;
65 import net.sf.openrocket.startup.Application;
66 import net.sf.openrocket.unit.UnitGroup;
67 import net.sf.openrocket.util.ChangeSource;
68 import net.sf.openrocket.util.Chars;
69 import net.sf.openrocket.util.Coordinate;
70 import net.sf.openrocket.util.MathUtil;
71 import net.sf.openrocket.util.StateChangeListener;
74 * A JPanel that contains a RocketFigure and buttons to manipulate the figure.
76 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
78 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
80 private static final Translator trans = Application.getTranslator();
81 private final RocketFigure figure;
82 private final ScaleScrollPane scrollPane;
84 private JLabel infoMessage;
86 private TreeSelectionModel selectionModel = null;
89 /* Calculation of CP and CG */
90 private AerodynamicCalculator aerodynamicCalculator;
91 private MassCalculator massCalculator;
94 private final OpenRocketDocument document;
95 private final Configuration configuration;
97 private Caret extraCP = null;
98 private Caret extraCG = null;
99 private RocketInfo extraText = null;
102 private double cpAOA = Double.NaN;
103 private double cpTheta = Double.NaN;
104 private double cpMach = Double.NaN;
105 private double cpRoll = Double.NaN;
107 // The functional ID of the rocket that was simulated
108 private int flightDataFunctionalID = -1;
109 private String flightDataMotorID = null;
112 private SimulationWorker backgroundSimulationWorker = null;
114 private List<EventListener> listeners = new ArrayList<EventListener>();
118 * The executor service used for running the background simulations.
119 * This uses a fixed-sized thread pool for all background simulations
120 * with all threads in daemon mode and with minimum priority.
122 private static final Executor backgroundSimulationExecutor;
124 backgroundSimulationExecutor = Executors.newFixedThreadPool(SwingPreferences.getMaxThreadCount(),
125 new ThreadFactory() {
126 private ThreadFactory factory = Executors.defaultThreadFactory();
129 public Thread newThread(Runnable r) {
130 Thread t = factory.newThread(r);
132 t.setPriority(Thread.MIN_PRIORITY);
139 public RocketPanel(OpenRocketDocument document) {
141 this.document = document;
142 configuration = document.getDefaultConfiguration();
144 // TODO: FUTURE: calculator selection
145 aerodynamicCalculator = new BarrowmanCalculator();
146 massCalculator = new BasicMassCalculator();
148 // Create figure and custom scroll pane
149 figure = new RocketFigure(configuration);
151 scrollPane = new ScaleScrollPane(figure) {
153 public void mouseClicked(MouseEvent event) {
154 handleMouseClick(event);
157 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
158 scrollPane.setFitting(true);
162 configuration.addChangeListener(new StateChangeListener() {
164 public void stateChanged(EventObject e) {
165 // System.out.println("Configuration changed, calling updateFigure");
167 figure.updateFigure();
174 * Creates the layout and components of the panel.
176 private void createPanel() {
177 setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
179 setPreferredSize(new Dimension(800, 300));
185 FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
187 action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Sideview"));
189 action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Sideview"));
190 JToggleButton toggle = new JToggleButton(action);
191 add(toggle, "spanx, split");
193 action = new FigureTypeAction(RocketFigure.TYPE_BACK);
195 action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Backview"));
197 action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Backview"));
198 toggle = new JToggleButton(action);
199 add(toggle, "gap rel");
202 // Zoom level selector
203 ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
209 StageSelector stageSelector = new StageSelector(configuration);
210 add(stageSelector, "");
214 // Motor configuration selector
215 //// Motor configuration:
216 JLabel label = new JLabel(trans.get("RocketPanel.lbl.Motorcfg"));
217 label.setHorizontalAlignment(JLabel.RIGHT);
218 add(label, "growx, right");
219 add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap");
225 // Create slider and scroll pane
227 DoubleModel theta = new DoubleModel(figure, "Rotation",
228 UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
229 UnitSelector us = new UnitSelector(theta, true);
230 us.setHorizontalAlignment(JLabel.CENTER);
231 add(us, "alignx 50%, growx");
233 // Add the rocket figure
234 add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap");
237 // Add rotation slider
238 // Minimum size to fit "360deg"
239 JLabel l = new JLabel("360" + Chars.DEGREE);
240 Dimension d = l.getPreferredSize();
242 add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true),
243 "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy");
246 //// <html>Click to select Shift+click to select other Double-click to edit Click+drag to move
247 infoMessage = new JLabel(trans.get("RocketPanel.lbl.infoMessage"));
248 infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
249 add(infoMessage, "skip, span, gapleft 25, wrap");
257 public RocketFigure getFigure() {
261 public AerodynamicCalculator getAerodynamicCalculator() {
262 return aerodynamicCalculator;
265 public Configuration getConfiguration() {
266 return configuration;
270 * Get the center of pressure figure element.
272 * @return center of pressure info
274 public Caret getExtraCP() {
279 * Get the center of gravity figure element.
281 * @return center of gravity info
283 public Caret getExtraCG() {
288 * Get the extra text figure element.
290 * @return extra text that contains info about the rocket design
292 public RocketInfo getExtraText() {
296 public void setSelectionModel(TreeSelectionModel m) {
297 if (selectionModel != null) {
298 selectionModel.removeTreeSelectionListener(this);
301 selectionModel.addTreeSelectionListener(this);
302 valueChanged((TreeSelectionEvent) null); // updates FigureParameters
308 * Return the angle of attack used in CP calculation. NaN signifies the default value
310 * @return the angle of attack used, or NaN.
312 public double getCPAOA() {
317 * Set the angle of attack to be used in CP calculation. A value of NaN signifies that
318 * the default AOA (zero) should be used.
319 * @param aoa the angle of attack to use, or NaN
321 public void setCPAOA(double aoa) {
322 if (MathUtil.equals(aoa, cpAOA) ||
323 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
327 figure.updateFigure();
331 public double getCPTheta() {
335 public void setCPTheta(double theta) {
336 if (MathUtil.equals(theta, cpTheta) ||
337 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
340 if (!Double.isNaN(theta))
341 figure.setRotation(theta);
343 figure.updateFigure();
347 public double getCPMach() {
351 public void setCPMach(double mach) {
352 if (MathUtil.equals(mach, cpMach) ||
353 (Double.isNaN(mach) && Double.isNaN(cpMach)))
357 figure.updateFigure();
361 public double getCPRoll() {
365 public void setCPRoll(double roll) {
366 if (MathUtil.equals(roll, cpRoll) ||
367 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
371 figure.updateFigure();
378 public void addChangeListener(EventListener listener) {
379 listeners.add(0, listener);
383 public void removeChangeListener(EventListener listener) {
384 listeners.remove(listener);
387 protected void fireChangeEvent() {
388 EventObject e = new EventObject(this);
389 for (EventListener l : listeners) {
390 if ( l instanceof StateChangeListener ) {
391 ((StateChangeListener)l).stateChanged(e);
400 * Handle clicking on figure shapes. The functioning is the following:
402 * Get the components clicked.
403 * If no component is clicked, do nothing.
404 * If the currently selected component is in the set, keep it,
405 * unless the selector specified is pressed. If it is pressed, cycle to
406 * the next component. Otherwise select the first component in the list.
408 public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
410 private void handleMouseClick(MouseEvent event) {
411 if (event.getButton() != MouseEvent.BUTTON1)
413 Point p0 = event.getPoint();
414 Point p1 = scrollPane.getViewport().getViewPosition();
418 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
420 // If no component is clicked, do nothing
421 if (clicked.length == 0)
424 // Check whether the currently selected component is in the clicked components.
425 TreePath path = selectionModel.getSelectionPath();
427 RocketComponent current = (RocketComponent) path.getLastPathComponent();
429 for (int i = 0; i < clicked.length; i++) {
430 if (clicked[i] == current) {
431 if (event.isShiftDown() && (event.getClickCount() == 1)) {
432 path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]);
434 path = ComponentTreeModel.makeTreePath(clicked[i]);
441 // Currently selected component not clicked
443 if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
444 path = ComponentTreeModel.makeTreePath(clicked[1]);
446 path = ComponentTreeModel.makeTreePath(clicked[0]);
450 // Set selection and check for double-click
451 selectionModel.setSelectionPath(path);
452 if (event.getClickCount() == 2) {
453 RocketComponent component = (RocketComponent) path.getLastPathComponent();
455 ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
456 document, component);
464 * Updates the extra data included in the figure. Currently this includes
465 * the CP and CG carets.
467 private WarningSet warnings = new WarningSet();
469 private void updateExtras() {
473 // TODO: MEDIUM: User-definable conditions
474 FlightConditions conditions = new FlightConditions(configuration);
477 if (!Double.isNaN(cpMach)) {
478 conditions.setMach(cpMach);
479 extraText.setMach(cpMach);
481 conditions.setMach(Application.getPreferences().getDefaultMach());
482 extraText.setMach(Application.getPreferences().getDefaultMach());
485 if (!Double.isNaN(cpAOA)) {
486 conditions.setAOA(cpAOA);
488 conditions.setAOA(0);
490 extraText.setAOA(cpAOA);
492 if (!Double.isNaN(cpRoll)) {
493 conditions.setRollRate(cpRoll);
495 conditions.setRollRate(0);
498 if (!Double.isNaN(cpTheta)) {
499 conditions.setTheta(cpTheta);
500 cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
502 cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
504 extraText.setTheta(cpTheta);
507 cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
508 // System.out.println("CG computed as "+cg+ " CP as "+cp);
510 if (cp.weight > 0.000001)
515 if (cg.weight > 0.000001)
520 // Length bound is assumed to be tight
521 double length = 0, diameter = 0;
522 Collection<Coordinate> bounds = configuration.getBounds();
523 if (!bounds.isEmpty()) {
524 double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
525 for (Coordinate c : bounds) {
531 length = maxX - minX;
534 for (RocketComponent c : configuration) {
535 if (c instanceof SymmetricComponent) {
536 double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
537 double d2 = ((SymmetricComponent) c).getAftRadius() * 2;
538 diameter = MathUtil.max(diameter, d1, d2);
542 extraText.setCG(cgx);
543 extraText.setCP(cpx);
544 extraText.setLength(length);
545 extraText.setDiameter(diameter);
546 extraText.setMass(cg.weight);
547 extraText.setWarnings(warnings);
550 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
552 // TODO: LOW: Y-coordinate and rotation
553 extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
554 extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
558 extraCP.setPosition(Double.NaN, Double.NaN);
559 extraCG.setPosition(Double.NaN, Double.NaN);
564 //////// Flight simulation in background
566 // Check whether to compute or not
567 if (!((SwingPreferences) Application.getPreferences()).computeFlightInBackground()) {
568 extraText.setFlightData(null);
569 extraText.setCalculatingData(false);
570 stopBackgroundSimulation();
574 // Check whether data is already up to date
575 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
576 flightDataMotorID == configuration.getMotorConfigurationID()) {
580 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
581 flightDataMotorID = configuration.getMotorConfigurationID();
583 // Stop previous computation (if any)
584 stopBackgroundSimulation();
586 // Check that configuration has motors
587 if (!configuration.hasMotors()) {
588 extraText.setFlightData(FlightData.NaN_DATA);
589 extraText.setCalculatingData(false);
593 // Start calculation process
594 extraText.setCalculatingData(true);
596 Rocket duplicate = (Rocket) configuration.getRocket().copy();
597 Simulation simulation = ((SwingPreferences)Application.getPreferences()).getBackgroundSimulation(duplicate);
598 simulation.getOptions().setMotorConfigurationID(
599 configuration.getMotorConfigurationID());
601 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
602 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
606 * Cancels the current background simulation worker, if any.
608 private void stopBackgroundSimulation() {
609 if (backgroundSimulationWorker != null) {
610 backgroundSimulationWorker.cancel(true);
611 backgroundSimulationWorker = null;
617 * A SimulationWorker that simulates the rocket flight in the background and
618 * sets the results to the extra text when finished. The worker can be cancelled
621 private class BackgroundSimulationWorker extends SimulationWorker {
623 public BackgroundSimulationWorker(Simulation sim) {
628 protected FlightData doInBackground() {
630 // Pause a little while to allow faster UI reaction
633 } catch (InterruptedException ignore) {
635 if (isCancelled() || backgroundSimulationWorker != this)
638 return super.doInBackground();
642 protected void simulationDone() {
643 // Do nothing if cancelled
644 if (isCancelled() || backgroundSimulationWorker != this)
647 backgroundSimulationWorker = null;
648 extraText.setFlightData(simulation.getSimulatedData());
649 extraText.setCalculatingData(false);
654 protected SimulationListener[] getExtraListeners() {
655 return new SimulationListener[] {
656 InterruptListener.INSTANCE,
657 ApogeeEndListener.INSTANCE };
661 protected void simulationInterrupted(Throwable t) {
662 // Do nothing on cancel, set N/A data otherwise
663 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
666 backgroundSimulationWorker = null;
667 extraText.setFlightData(FlightData.NaN_DATA);
668 extraText.setCalculatingData(false);
676 * Adds the extra data to the figure. Currently this includes the CP and CG carets.
678 private void addExtras() {
679 figure.clearRelativeExtra();
680 extraCG = new CGCaret(0, 0);
681 extraCP = new CPCaret(0, 0);
682 extraText = new RocketInfo(configuration);
684 figure.addRelativeExtra(extraCP);
685 figure.addRelativeExtra(extraCG);
686 figure.addAbsoluteExtra(extraText);
691 * Updates the selection in the FigureParameters and repaints the figure.
692 * Ignores the event itself.
695 public void valueChanged(TreeSelectionEvent e) {
696 TreePath[] paths = selectionModel.getSelectionPaths();
698 figure.setSelection(null);
702 RocketComponent[] components = new RocketComponent[paths.length];
703 for (int i = 0; i < paths.length; i++)
704 components[i] = (RocketComponent) paths[i].getLastPathComponent();
705 figure.setSelection(components);
711 * An <code>Action</code> that shows whether the figure type is the type
712 * given in the constructor.
714 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
716 private class FigureTypeAction extends AbstractAction implements StateChangeListener {
717 private final int type;
719 public FigureTypeAction(int type) {
722 figure.addChangeListener(this);
726 public void actionPerformed(ActionEvent e) {
727 boolean state = (Boolean) getValue(Action.SELECTED_KEY);
729 // This view has been selected
730 figure.setType(type);
737 public void stateChanged(EventObject e) {
738 putValue(Action.SELECTED_KEY, figure.getType() == type);