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.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;
68 * A JPanel that contains a RocketFigure and buttons to manipulate the figure.
70 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
72 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
74 private final RocketFigure figure;
75 private final ScaleScrollPane scrollPane;
77 private JLabel infoMessage;
79 private TreeSelectionModel selectionModel = null;
82 /* Calculation of CP and CG */
83 private AerodynamicCalculator calculator;
86 private final OpenRocketDocument document;
87 private final Configuration configuration;
89 private Caret extraCP = null;
90 private Caret extraCG = null;
91 private RocketInfo extraText = null;
94 private double cpAOA = Double.NaN;
95 private double cpTheta = Double.NaN;
96 private double cpMach = Double.NaN;
97 private double cpRoll = Double.NaN;
99 // The functional ID of the rocket that was simulated
100 private int flightDataFunctionalID = -1;
101 private String flightDataMotorID = null;
104 private SimulationWorker backgroundSimulationWorker = null;
107 private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
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.
115 private static final Executor backgroundSimulationExecutor;
117 backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
118 new ThreadFactory() {
119 private ThreadFactory factory = Executors.defaultThreadFactory();
121 public Thread newThread(Runnable r) {
122 Thread t = factory.newThread(r);
124 t.setPriority(Thread.MIN_PRIORITY);
131 public RocketPanel(OpenRocketDocument document) {
133 this.document = document;
134 configuration = document.getDefaultConfiguration();
136 // TODO: FUTURE: calculator selection
137 calculator = new BarrowmanCalculator(configuration);
139 // Create figure and custom scroll pane
140 figure = new RocketFigure(configuration);
142 scrollPane = new ScaleScrollPane(figure) {
144 public void mouseClicked(MouseEvent event) {
145 handleMouseClick(event);
148 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
149 scrollPane.setFitting(true);
153 configuration.addChangeListener(new ChangeListener() {
155 public void stateChanged(ChangeEvent e) {
156 System.out.println("Configuration changed, calling updateFigure");
158 figure.updateFigure();
165 * Creates the layout and components of the panel.
167 private void createPanel() {
168 setLayout(new MigLayout("","[shrink][grow]","[shrink][shrink][grow][shrink]"));
170 setPreferredSize(new Dimension(800,300));
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");
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");
189 // Zoom level selector
190 ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
196 StageSelector stageSelector = new StageSelector(configuration);
197 add(stageSelector,"");
201 // Motor configuration selector
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");
212 // Create slider and scroll pane
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");
220 // Add the rocket figure
221 add(scrollPane,"grow, spany 2, wmin 300lp, hmin 100lp, wrap");
224 // Add rotation slider
225 // Minimum size to fit "360deg"
226 JLabel l = new JLabel("360" + Chars.DEGREE);
227 Dimension d = l.getPreferredSize();
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");
233 infoMessage = new JLabel("<html>" +
234 "Click to select " +
235 "Shift+click to select other " +
236 "Double-click to edit " +
237 "Click+drag to move");
238 infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
239 add(infoMessage,"skip, span, gapleft 25, wrap");
246 public RocketFigure getFigure() {
250 public AerodynamicCalculator getCalculator() {
254 public Configuration getConfiguration() {
255 return configuration;
258 public void setSelectionModel(TreeSelectionModel m) {
259 if (selectionModel != null) {
260 selectionModel.removeTreeSelectionListener(this);
263 selectionModel.addTreeSelectionListener(this);
264 valueChanged((TreeSelectionEvent)null); // updates FigureParameters
270 * Return the angle of attack used in CP calculation. NaN signifies the default value
272 * @return the angle of attack used, or NaN.
274 public double getCPAOA() {
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
283 public void setCPAOA(double aoa) {
284 if (MathUtil.equals(aoa, cpAOA) ||
285 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
289 figure.updateFigure();
293 public double getCPTheta() {
297 public void setCPTheta(double theta) {
298 if (MathUtil.equals(theta, cpTheta) ||
299 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
302 if (!Double.isNaN(theta))
303 figure.setRotation(theta);
305 figure.updateFigure();
309 public double getCPMach() {
313 public void setCPMach(double mach) {
314 if (MathUtil.equals(mach, cpMach) ||
315 (Double.isNaN(mach) && Double.isNaN(cpMach)))
319 figure.updateFigure();
323 public double getCPRoll() {
327 public void setCPRoll(double roll) {
328 if (MathUtil.equals(roll, cpRoll) ||
329 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
333 figure.updateFigure();
340 public void addChangeListener(ChangeListener listener) {
341 listeners.add(0,listener);
344 public void removeChangeListener(ChangeListener listener) {
345 listeners.remove(listener);
348 protected void fireChangeEvent() {
349 ChangeEvent e = new ChangeEvent(this);
350 ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
351 for (ChangeListener l: list) {
360 * Handle clicking on figure shapes. The functioning is the following:
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.
368 public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
370 private void handleMouseClick(MouseEvent event) {
371 if (event.getButton() != MouseEvent.BUTTON1)
373 Point p0 = event.getPoint();
374 Point p1 = scrollPane.getViewport().getViewPosition();
378 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
380 // If no component is clicked, do nothing
381 if (clicked.length == 0)
384 // Check whether the currently selected component is in the clicked components.
385 TreePath path = selectionModel.getSelectionPath();
387 RocketComponent current = (RocketComponent)path.getLastPathComponent();
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]);
394 path = ComponentTreeModel.makeTreePath(clicked[i]);
401 // Currently selected component not clicked
403 if (event.isShiftDown() && event.getClickCount()==1 && clicked.length>1) {
404 path = ComponentTreeModel.makeTreePath(clicked[1]);
406 path = ComponentTreeModel.makeTreePath(clicked[0]);
410 // Set selection and check for double-click
411 selectionModel.setSelectionPath(path);
412 if (event.getClickCount() == 2) {
413 RocketComponent component = (RocketComponent)path.getLastPathComponent();
415 ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
416 document, component);
424 * Updates the extra data included in the figure. Currently this includes
425 * the CP and CG carets.
427 private WarningSet warnings = new WarningSet();
428 private void updateExtras() {
432 // TODO: MEDIUM: User-definable conditions
433 FlightConditions conditions = new FlightConditions(configuration);
436 if (!Double.isNaN(cpMach)) {
437 conditions.setMach(cpMach);
438 extraText.setMach(cpMach);
440 conditions.setMach(Prefs.getDefaultMach());
441 extraText.setMach(Prefs.getDefaultMach());
444 if (!Double.isNaN(cpAOA)) {
445 conditions.setAOA(cpAOA);
447 conditions.setAOA(0);
449 extraText.setAOA(cpAOA);
451 if (!Double.isNaN(cpRoll)) {
452 conditions.setRollRate(cpRoll);
454 conditions.setRollRate(0);
457 if (!Double.isNaN(cpTheta)) {
458 conditions.setTheta(cpTheta);
459 cp = calculator.getCP(conditions, warnings);
461 cp = calculator.getWorstCP(conditions, warnings);
463 extraText.setTheta(cpTheta);
466 cg = calculator.getCG(0);
467 // System.out.println("CG computed as "+cg+ " CP as "+cp);
469 if (cp.weight > 0.000001)
474 if (cg.weight > 0.000001)
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) {
490 length = maxX - minX;
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);
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);
509 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
511 // TODO: LOW: Y-coordinate and rotation
512 extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
513 extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
517 extraCP.setPosition(Double.NaN, Double.NaN);
518 extraCG.setPosition(Double.NaN, Double.NaN);
523 //////// Flight simulation in background
525 // Check whether to compute or not
526 if (!Prefs.computeFlightInBackground()) {
527 extraText.setFlightData(null);
528 extraText.setCalculatingData(false);
529 stopBackgroundSimulation();
533 // Check whether data is already up to date
534 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
535 flightDataMotorID == configuration.getMotorConfigurationID()) {
539 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
540 flightDataMotorID = configuration.getMotorConfigurationID();
542 // Stop previous computation (if any)
543 stopBackgroundSimulation();
545 // Check that configuration has motors
546 if (!configuration.hasMotors()) {
547 extraText.setFlightData(FlightData.NaN_DATA);
548 extraText.setCalculatingData(false);
552 // Start calculation process
553 extraText.setCalculatingData(true);
555 Rocket duplicate = configuration.getRocket().copy();
556 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
557 simulation.getConditions().setMotorConfigurationID(
558 configuration.getMotorConfigurationID());
560 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
561 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
565 * Cancels the current background simulation worker, if any.
567 private void stopBackgroundSimulation() {
568 if (backgroundSimulationWorker != null) {
569 backgroundSimulationWorker.cancel(true);
570 backgroundSimulationWorker = null;
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
580 private class BackgroundSimulationWorker extends SimulationWorker {
582 public BackgroundSimulationWorker(Simulation sim) {
587 protected FlightData doInBackground() {
589 // Pause a little while to allow faster UI reaction
592 } catch (InterruptedException ignore) { }
593 if (isCancelled() || backgroundSimulationWorker != this)
596 return super.doInBackground();
600 protected void simulationDone() {
601 // Do nothing if cancelled
602 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
605 backgroundSimulationWorker = null;
606 extraText.setFlightData(simulation.getSimulatedData());
607 extraText.setCalculatingData(false);
612 protected SimulationListener[] getExtraListeners() {
613 return new SimulationListener[] {
614 InterruptListener.INSTANCE,
615 ApogeeEndListener.INSTANCE
620 protected void simulationInterrupted(Throwable t) {
621 // Do nothing on cancel, set N/A data otherwise
622 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
625 backgroundSimulationWorker = null;
626 extraText.setFlightData(FlightData.NaN_DATA);
627 extraText.setCalculatingData(false);
635 * Adds the extra data to the figure. Currently this includes the CP and CG carets.
637 private void addExtras() {
638 figure.clearRelativeExtra();
639 extraCG = new CGCaret(0,0);
640 extraCP = new CPCaret(0,0);
641 extraText = new RocketInfo(configuration);
643 figure.addRelativeExtra(extraCP);
644 figure.addRelativeExtra(extraCG);
645 figure.addAbsoluteExtra(extraText);
650 * Updates the selection in the FigureParameters and repaints the figure.
651 * Ignores the event itself.
653 public void valueChanged(TreeSelectionEvent e) {
654 TreePath[] paths = selectionModel.getSelectionPaths();
656 figure.setSelection(null);
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);
669 * An <code>Action</code> that shows whether the figure type is the type
670 * given in the constructor.
672 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
674 private class FigureTypeAction extends AbstractAction implements ChangeListener {
675 private final int type;
677 public FigureTypeAction(int type) {
680 figure.addChangeListener(this);
683 public void actionPerformed(ActionEvent e) {
684 boolean state = (Boolean)getValue(Action.SELECTED_KEY);
686 // This view has been selected
687 figure.setType(type);
693 public void stateChanged(ChangeEvent e) {
694 putValue(Action.SELECTED_KEY,figure.getType() == type);