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.BasicSlider;
41 import net.sf.openrocket.gui.StageSelector;
42 import net.sf.openrocket.gui.UnitSelector;
43 import net.sf.openrocket.gui.adaptors.DoubleModel;
44 import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
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.Coordinate;
63 import net.sf.openrocket.util.MathUtil;
64 import net.sf.openrocket.util.Prefs;
67 * A JPanel that contains a RocketFigure and buttons to manipulate the figure.
69 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
71 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
73 private final RocketFigure figure;
74 private final ScaleScrollPane scrollPane;
76 private JLabel infoMessage;
78 private TreeSelectionModel selectionModel = null;
81 /* Calculation of CP and CG */
82 private AerodynamicCalculator calculator;
85 private final OpenRocketDocument document;
86 private final Configuration configuration;
88 private Caret extraCP = null;
89 private Caret extraCG = null;
90 private RocketInfo extraText = null;
93 private double cpAOA = Double.NaN;
94 private double cpTheta = Double.NaN;
95 private double cpMach = Double.NaN;
96 private double cpRoll = Double.NaN;
98 // The functional ID of the rocket that was simulated
99 private int flightDataFunctionalID = -1;
100 private String flightDataMotorID = null;
103 private SimulationWorker backgroundSimulationWorker = null;
106 private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
110 * The executor service used for running the background simulations.
111 * This uses a fixed-sized thread pool for all background simulations
112 * with all threads in daemon mode and with minimum priority.
114 private static final Executor backgroundSimulationExecutor;
116 backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
117 new ThreadFactory() {
118 private ThreadFactory factory = Executors.defaultThreadFactory();
120 public Thread newThread(Runnable r) {
121 Thread t = factory.newThread(r);
123 t.setPriority(Thread.MIN_PRIORITY);
130 public RocketPanel(OpenRocketDocument document) {
132 this.document = document;
133 configuration = document.getDefaultConfiguration();
135 // TODO: FUTURE: calculator selection
136 calculator = new BarrowmanCalculator(configuration);
138 // Create figure and custom scroll pane
139 figure = new RocketFigure(configuration);
141 scrollPane = new ScaleScrollPane(figure) {
143 public void mouseClicked(MouseEvent event) {
144 handleMouseClick(event);
147 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
148 scrollPane.setFitting(true);
152 configuration.addChangeListener(new ChangeListener() {
154 public void stateChanged(ChangeEvent e) {
155 System.out.println("Configuration changed, calling updateFigure");
157 figure.updateFigure();
164 * Creates the layout and components of the panel.
166 private void createPanel() {
167 setLayout(new MigLayout("","[shrink][grow]","[shrink][shrink][grow][shrink]"));
169 setPreferredSize(new Dimension(800,300));
175 FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
176 action.putValue(Action.NAME, "Side view");
177 action.putValue(Action.SHORT_DESCRIPTION, "Side view");
178 JToggleButton toggle = new JToggleButton(action);
179 add(toggle,"spanx, split");
181 action = new FigureTypeAction(RocketFigure.TYPE_BACK);
182 action.putValue(Action.NAME, "Back view");
183 action.putValue(Action.SHORT_DESCRIPTION, "Rear view");
184 toggle = new JToggleButton(action);
185 add(toggle,"gap rel");
188 // Zoom level selector
189 ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
195 StageSelector stageSelector = new StageSelector(configuration);
196 add(stageSelector,"");
200 // Motor configuration selector
202 JLabel label = new JLabel("Motor configuration:");
203 label.setHorizontalAlignment(JLabel.RIGHT);
204 add(label,"growx, right");
205 add(new JComboBox(new MotorConfigurationModel(configuration)),"wrap");
211 // Create slider and scroll pane
213 DoubleModel theta = new DoubleModel(figure,"Rotation",
214 UnitGroup.UNITS_ANGLE,0,2*Math.PI);
215 UnitSelector us = new UnitSelector(theta,true);
216 us.setHorizontalAlignment(JLabel.CENTER);
217 add(us,"alignx 50%, growx");
219 // Add the rocket figure
220 add(scrollPane,"grow, spany 2, wmin 300lp, hmin 100lp, wrap");
223 // Add rotation slider
224 // Minimum size to fit "360deg"
225 JLabel l = new JLabel("360\u00b0");
226 Dimension d = l.getPreferredSize();
228 add(new BasicSlider(theta.getSliderModel(0,2*Math.PI),JSlider.VERTICAL,true),
229 "ax 50%, wrap, width "+(d.width+6)+"px:null:null, growy");
232 infoMessage = new JLabel("<html>" +
233 "Click to select " +
234 "Shift+click to select other " +
235 "Double-click to edit " +
236 "Click+drag to move");
237 infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
238 add(infoMessage,"skip, span, gapleft 25, wrap");
245 public RocketFigure getFigure() {
249 public AerodynamicCalculator getCalculator() {
253 public Configuration getConfiguration() {
254 return configuration;
257 public void setSelectionModel(TreeSelectionModel m) {
258 if (selectionModel != null) {
259 selectionModel.removeTreeSelectionListener(this);
262 selectionModel.addTreeSelectionListener(this);
263 valueChanged((TreeSelectionEvent)null); // updates FigureParameters
269 * Return the angle of attack used in CP calculation. NaN signifies the default value
271 * @return the angle of attack used, or NaN.
273 public double getCPAOA() {
278 * Set the angle of attack to be used in CP calculation. A value of NaN signifies that
279 * the default AOA (zero) should be used.
280 * @param aoa the angle of attack to use, or NaN
282 public void setCPAOA(double aoa) {
283 if (MathUtil.equals(aoa, cpAOA) ||
284 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
288 figure.updateFigure();
292 public double getCPTheta() {
296 public void setCPTheta(double theta) {
297 if (MathUtil.equals(theta, cpTheta) ||
298 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
301 if (!Double.isNaN(theta))
302 figure.setRotation(theta);
304 figure.updateFigure();
308 public double getCPMach() {
312 public void setCPMach(double mach) {
313 if (MathUtil.equals(mach, cpMach) ||
314 (Double.isNaN(mach) && Double.isNaN(cpMach)))
318 figure.updateFigure();
322 public double getCPRoll() {
326 public void setCPRoll(double roll) {
327 if (MathUtil.equals(roll, cpRoll) ||
328 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
332 figure.updateFigure();
339 public void addChangeListener(ChangeListener listener) {
340 listeners.add(0,listener);
343 public void removeChangeListener(ChangeListener listener) {
344 listeners.remove(listener);
347 protected void fireChangeEvent() {
348 ChangeEvent e = new ChangeEvent(this);
349 ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
350 for (ChangeListener l: list) {
359 * Handle clicking on figure shapes. The functioning is the following:
361 * Get the components clicked.
362 * If no component is clicked, do nothing.
363 * If the primary currently selected component is in the set, keep it,
364 * unless the selector specified is pressed. If it is pressed, cycle to
365 * the next component. Otherwise select the first component in the list.
367 public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
369 private void handleMouseClick(MouseEvent event) {
370 if (event.getButton() != MouseEvent.BUTTON1)
372 Point p0 = event.getPoint();
373 Point p1 = scrollPane.getViewport().getViewPosition();
377 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
379 // If no component is clicked, do nothing
380 if (clicked.length == 0)
383 // Check whether the currently selected component is in the clicked components.
384 TreePath path = selectionModel.getSelectionPath();
386 RocketComponent current = (RocketComponent)path.getLastPathComponent();
388 for (int i=0; i<clicked.length; i++) {
389 if (clicked[i] == current) {
390 if (event.isShiftDown() && (event.getClickCount()==1)) {
391 path = ComponentTreeModel.makeTreePath(clicked[(i+1)%clicked.length]);
393 path = ComponentTreeModel.makeTreePath(clicked[i]);
400 // Currently selected component not clicked
402 path = ComponentTreeModel.makeTreePath(clicked[0]);
405 // Set selection and check for double-click
406 selectionModel.setSelectionPath(path);
407 if (event.getClickCount() == 2) {
408 RocketComponent component = (RocketComponent)path.getLastPathComponent();
410 ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
411 document, component);
419 * Updates the extra data included in the figure. Currently this includes
420 * the CP and CG carets.
422 private WarningSet warnings = new WarningSet();
423 private void updateExtras() {
427 // TODO: MEDIUM: User-definable conditions
428 FlightConditions conditions = new FlightConditions(configuration);
431 if (!Double.isNaN(cpMach)) {
432 conditions.setMach(cpMach);
433 extraText.setMach(cpMach);
435 conditions.setMach(Prefs.getDefaultMach());
436 extraText.setMach(Prefs.getDefaultMach());
439 if (!Double.isNaN(cpAOA)) {
440 conditions.setAOA(cpAOA);
442 conditions.setAOA(0);
444 extraText.setAOA(cpAOA);
446 if (!Double.isNaN(cpRoll)) {
447 conditions.setRollRate(cpRoll);
449 conditions.setRollRate(0);
452 if (!Double.isNaN(cpTheta)) {
453 conditions.setTheta(cpTheta);
454 cp = calculator.getCP(conditions, warnings);
456 cp = calculator.getWorstCP(conditions, warnings);
458 extraText.setTheta(cpTheta);
461 cg = calculator.getCG(0);
462 // System.out.println("CG computed as "+cg+ " CP as "+cp);
464 if (cp.weight > 0.000001)
469 if (cg.weight > 0.000001)
474 // Length bound is assumed to be tight
475 double length = 0, diameter = 0;
476 Collection<Coordinate> bounds = configuration.getBounds();
477 if (!bounds.isEmpty()) {
478 double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
479 for (Coordinate c: bounds) {
485 length = maxX - minX;
488 for (RocketComponent c: configuration) {
489 if (c instanceof SymmetricComponent) {
490 double d1 = ((SymmetricComponent)c).getForeRadius() * 2;
491 double d2 = ((SymmetricComponent)c).getAftRadius() * 2;
492 diameter = MathUtil.max(diameter, d1, d2);
496 extraText.setCG(cgx);
497 extraText.setCP(cpx);
498 extraText.setLength(length);
499 extraText.setDiameter(diameter);
500 extraText.setMass(cg.weight);
501 extraText.setWarnings(warnings);
504 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
506 // TODO: LOW: Y-coordinate and rotation
507 extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
508 extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
512 extraCP.setPosition(Double.NaN, Double.NaN);
513 extraCG.setPosition(Double.NaN, Double.NaN);
518 //////// Flight simulation in background
520 // Check whether to compute or not
521 if (!Prefs.computeFlightInBackground()) {
522 extraText.setFlightData(null);
523 extraText.setCalculatingData(false);
524 stopBackgroundSimulation();
528 // Check whether data is already up to date
529 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
530 flightDataMotorID == configuration.getMotorConfigurationID()) {
534 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
535 flightDataMotorID = configuration.getMotorConfigurationID();
537 // Stop previous computation (if any)
538 stopBackgroundSimulation();
540 // Check that configuration has motors
541 if (!configuration.hasMotors()) {
542 extraText.setFlightData(FlightData.NaN_DATA);
543 extraText.setCalculatingData(false);
547 // Start calculation process
548 extraText.setCalculatingData(true);
550 Rocket duplicate = configuration.getRocket().copy();
551 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
552 simulation.getConditions().setMotorConfigurationID(
553 configuration.getMotorConfigurationID());
555 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
556 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
560 * Cancels the current background simulation worker, if any.
562 private void stopBackgroundSimulation() {
563 if (backgroundSimulationWorker != null) {
564 backgroundSimulationWorker.cancel(true);
565 backgroundSimulationWorker = null;
571 * A SimulationWorker that simulates the rocket flight in the background and
572 * sets the results to the extra text when finished. The worker can be cancelled
575 private class BackgroundSimulationWorker extends SimulationWorker {
577 public BackgroundSimulationWorker(Simulation sim) {
582 protected FlightData doInBackground() {
584 // Pause a little while to allow faster UI reaction
587 } catch (InterruptedException ignore) { }
588 if (isCancelled() || backgroundSimulationWorker != this)
591 return super.doInBackground();
595 protected void simulationDone() {
596 // Do nothing if cancelled
597 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
600 backgroundSimulationWorker = null;
601 extraText.setFlightData(simulation.getSimulatedData());
602 extraText.setCalculatingData(false);
607 protected SimulationListener[] getExtraListeners() {
608 return new SimulationListener[] {
609 InterruptListener.INSTANCE,
610 ApogeeEndListener.INSTANCE
615 protected void simulationInterrupted(Throwable t) {
616 // Do nothing on cancel, set N/A data otherwise
617 if (isCancelled() || backgroundSimulationWorker != this) // Double-check
620 backgroundSimulationWorker = null;
621 extraText.setFlightData(FlightData.NaN_DATA);
622 extraText.setCalculatingData(false);
630 * Adds the extra data to the figure. Currently this includes the CP and CG carets.
632 private void addExtras() {
633 figure.clearRelativeExtra();
634 extraCG = new CGCaret(0,0);
635 extraCP = new CPCaret(0,0);
636 extraText = new RocketInfo(configuration);
638 figure.addRelativeExtra(extraCP);
639 figure.addRelativeExtra(extraCG);
640 figure.addAbsoluteExtra(extraText);
645 * Updates the selection in the FigureParameters and repaints the figure.
646 * Ignores the event itself.
648 public void valueChanged(TreeSelectionEvent e) {
649 TreePath[] paths = selectionModel.getSelectionPaths();
651 figure.setSelection(null);
655 RocketComponent[] components = new RocketComponent[paths.length];
656 for (int i=0; i<paths.length; i++)
657 components[i] = (RocketComponent)paths[i].getLastPathComponent();
658 figure.setSelection(components);
664 * An <code>Action</code> that shows whether the figure type is the type
665 * given in the constructor.
667 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
669 private class FigureTypeAction extends AbstractAction implements ChangeListener {
670 private final int type;
672 public FigureTypeAction(int type) {
675 figure.addChangeListener(this);
678 public void actionPerformed(ActionEvent e) {
679 boolean state = (Boolean)getValue(Action.SELECTED_KEY);
681 // This view has been selected
682 figure.setType(type);
688 public void stateChanged(ChangeEvent e) {
689 putValue(Action.SELECTED_KEY,figure.getType() == type);