import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.SymmetricComponent;
import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.customexpression.CustomExpression;
+import net.sf.openrocket.simulation.customexpression.CustomExpressionSimulationListener;
import net.sf.openrocket.simulation.listeners.SimulationListener;
import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
import net.sf.openrocket.simulation.listeners.system.InterruptListener;
private boolean is3d;
private final RocketFigure figure;
private final RocketFigure3d figure3d;
-
-
+
+
private final ScaleScrollPane scrollPane;
-
+
private final JPanel figureHolder;
-
+
private JLabel infoMessage;
-
+
private TreeSelectionModel selectionModel = null;
-
+
private BasicSlider rotationSlider;
ScaleSelector scaleSelector;
-
+
/* Calculation of CP and CG */
private AerodynamicCalculator aerodynamicCalculator;
private MassCalculator massCalculator;
-
+
private final OpenRocketDocument document;
private final Configuration configuration;
-
+
private Caret extraCP = null;
private Caret extraCG = null;
private RocketInfo extraText = null;
-
+
private double cpAOA = Double.NaN;
private double cpTheta = Double.NaN;
private double cpMach = Double.NaN;
private double cpRoll = Double.NaN;
-
+
// The functional ID of the rocket that was simulated
private int flightDataFunctionalID = -1;
private String flightDataMotorID = null;
-
+
private SimulationWorker backgroundSimulationWorker = null;
-
+
private List<EventListener> listeners = new ArrayList<EventListener>();
-
+
/**
* The executor service used for running the background simulations.
static {
backgroundSimulationExecutor = Executors.newFixedThreadPool(SwingPreferences.getMaxThreadCount(),
new ThreadFactory() {
- private ThreadFactory factory = Executors.defaultThreadFactory();
-
- @Override
- public Thread newThread(Runnable r) {
- Thread t = factory.newThread(r);
- t.setDaemon(true);
- t.setPriority(Thread.MIN_PRIORITY);
- return t;
- }
- });
+ private ThreadFactory factory = Executors.defaultThreadFactory();
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = factory.newThread(r);
+ t.setDaemon(true);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ });
}
-
-
+
+
public RocketPanel(OpenRocketDocument document) {
-
+
this.document = document;
configuration = document.getDefaultConfiguration();
-
+
// TODO: FUTURE: calculator selection
aerodynamicCalculator = new BarrowmanCalculator();
massCalculator = new BasicMassCalculator();
-
+
// Create figure and custom scroll pane
figure = new RocketFigure(configuration);
figure3d = new RocketFigure3d(configuration);
-
+
figureHolder = new JPanel(new BorderLayout());
-
+
scrollPane = new ScaleScrollPane(figure) {
private static final long serialVersionUID = 1L;
};
scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
scrollPane.setFitting(true);
-
+
createPanel();
-
+
is3d = true;
go2D();
-
+
configuration.addChangeListener(new StateChangeListener() {
@Override
public void stateChanged(EventObject e) {
updateFigures();
}
});
-
+
figure3d.addComponentSelectionListener(new RocketFigure3d.ComponentSelectionListener() {
@Override
public void componentClicked(RocketComponent clicked[], MouseEvent event) {
}
});
}
-
+
private void updateFigures() {
if (!is3d)
figure.updateFigure();
figureHolder.revalidate();
figure.repaint();
}
-
+
/**
* Creates the layout and components of the panel.
*/
private void createPanel() {
setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
-
+
setPreferredSize(new Dimension(800, 300));
-
+
//// Create toolbar
-
+
ButtonGroup bg = new ButtonGroup();
-
+
// Side/back buttons
FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
//// Side view
JToggleButton toggle = new JToggleButton(action);
bg.add(toggle);
add(toggle, "spanx, split");
-
+
action = new FigureTypeAction(RocketFigure.TYPE_BACK);
//// Back view
action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Backview"));
toggle = new JToggleButton(action);
bg.add(toggle);
add(toggle, "gap rel");
-
+
//// 3d Toggle
final JToggleButton toggle3d = new JToggleButton(new AbstractAction("3D") {
private static final long serialVersionUID = 1L;
}
});
bg.add(toggle3d);
+ toggle3d.setEnabled(RocketFigure3d.is3dEnabled());
add(toggle3d, "gap rel");
-
// Zoom level selector
scaleSelector = new ScaleSelector(scrollPane);
add(scaleSelector);
-
+
// Stage selector
StageSelector stageSelector = new StageSelector(configuration);
add(stageSelector, "");
-
+
// Motor configuration selector
label.setHorizontalAlignment(JLabel.RIGHT);
add(label, "growx, right");
add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap");
-
+
// Create slider and scroll pane
-
+
DoubleModel theta = new DoubleModel(figure, "Rotation",
UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
UnitSelector us = new UnitSelector(theta, true);
us.setHorizontalAlignment(JLabel.CENTER);
add(us, "alignx 50%, growx");
-
+
// Add the rocket figure
add(figureHolder, "grow, spany 2, wmin 300lp, hmin 100lp, wrap");
-
+
// Add rotation slider
// Minimum size to fit "360deg"
JLabel l = new JLabel("360" + Chars.DEGREE);
Dimension d = l.getPreferredSize();
-
+
add(rotationSlider = new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true),
"ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy");
-
+
//// <html>Click to select Shift+click to select other Double-click to edit Click+drag to move
infoMessage = new JLabel(trans.get("RocketPanel.lbl.infoMessage"));
infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
add(infoMessage, "skip, span, gapleft 25, wrap");
-
+
addExtras();
}
-
-
+
+
public RocketFigure getFigure() {
return figure;
}
-
+
public AerodynamicCalculator getAerodynamicCalculator() {
return aerodynamicCalculator;
}
-
+
public Configuration getConfiguration() {
return configuration;
}
-
+
/**
* Get the center of pressure figure element.
*
public Caret getExtraCP() {
return extraCP;
}
-
+
/**
* Get the center of gravity figure element.
*
public Caret getExtraCG() {
return extraCG;
}
-
+
/**
* Get the extra text figure element.
*
public RocketInfo getExtraText() {
return extraText;
}
-
+
public void setSelectionModel(TreeSelectionModel m) {
if (selectionModel != null) {
selectionModel.removeTreeSelectionListener(this);
selectionModel.addTreeSelectionListener(this);
valueChanged((TreeSelectionEvent) null); // updates FigureParameters
}
-
-
+
+
/**
* Return the angle of attack used in CP calculation. NaN signifies the default value
public double getCPAOA() {
return cpAOA;
}
-
+
/**
* Set the angle of attack to be used in CP calculation. A value of NaN signifies that
* the default AOA (zero) should be used.
updateFigures();
fireChangeEvent();
}
-
+
public double getCPTheta() {
return cpTheta;
}
-
+
public void setCPTheta(double theta) {
if (MathUtil.equals(theta, cpTheta) ||
(Double.isNaN(theta) && Double.isNaN(cpTheta)))
updateFigures();
fireChangeEvent();
}
-
+
public double getCPMach() {
return cpMach;
}
-
+
public void setCPMach(double mach) {
if (MathUtil.equals(mach, cpMach) ||
(Double.isNaN(mach) && Double.isNaN(cpMach)))
updateFigures();
fireChangeEvent();
}
-
+
public double getCPRoll() {
return cpRoll;
}
-
+
public void setCPRoll(double roll) {
if (MathUtil.equals(roll, cpRoll) ||
(Double.isNaN(roll) && Double.isNaN(cpRoll)))
updateFigures();
fireChangeEvent();
}
-
-
+
+
@Override
public void addChangeListener(EventListener listener) {
listeners.add(0, listener);
}
-
+
@Override
public void removeChangeListener(EventListener listener) {
listeners.remove(listener);
}
-
+
protected void fireChangeEvent() {
EventObject e = new EventObject(this);
for (EventListener l : listeners) {
}
}
}
-
-
+
+
/**
* the next component. Otherwise select the first component in the list.
*/
public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
-
+
private void handleMouseClick(MouseEvent event) {
if (event.getButton() != MouseEvent.BUTTON1)
return;
Point p1 = scrollPane.getViewport().getViewPosition();
int x = p0.x + p1.x;
int y = p0.y + p1.y;
-
+
RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
-
+
handleComponentClick(clicked, event);
}
-
+
private void handleComponentClick(RocketComponent[] clicked, MouseEvent event){
-
+
// If no component is clicked, do nothing
if (clicked.length == 0)
return;
-
+
// Check whether the currently selected component is in the clicked components.
TreePath path = selectionModel.getSelectionPath();
if (path != null) {
}
}
}
-
+
// Currently selected component not clicked
if (path == null) {
if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
path = ComponentTreeModel.makeTreePath(clicked[0]);
}
}
-
+
// Set selection and check for double-click
selectionModel.setSelectionPath(path);
if (event.getClickCount() == 2) {
RocketComponent component = (RocketComponent) path.getLastPathComponent();
-
+
ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
document, component);
}
}
-
-
+
+
/**
* the CP and CG carets.
*/
private WarningSet warnings = new WarningSet();
-
+
private void updateExtras() {
Coordinate cp, cg;
double cpx, cgx;
-
+
// TODO: MEDIUM: User-definable conditions
FlightConditions conditions = new FlightConditions(configuration);
warnings.clear();
-
+
if (!Double.isNaN(cpMach)) {
conditions.setMach(cpMach);
extraText.setMach(cpMach);
conditions.setMach(Application.getPreferences().getDefaultMach());
extraText.setMach(Application.getPreferences().getDefaultMach());
}
-
+
if (!Double.isNaN(cpAOA)) {
conditions.setAOA(cpAOA);
} else {
conditions.setAOA(0);
}
extraText.setAOA(cpAOA);
-
+
if (!Double.isNaN(cpRoll)) {
conditions.setRollRate(cpRoll);
} else {
conditions.setRollRate(0);
}
-
+
if (!Double.isNaN(cpTheta)) {
conditions.setTheta(cpTheta);
cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
}
extraText.setTheta(cpTheta);
-
+
cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
// System.out.println("CG computed as "+cg+ " CP as "+cp);
-
+
if (cp.weight > 0.000001)
cpx = cp.x;
else
cpx = Double.NaN;
-
+
if (cg.weight > 0.000001)
cgx = cg.x;
else
cgx = Double.NaN;
-
+
figure3d.setCG(cg);
figure3d.setCP(cp);
-
+
// Length bound is assumed to be tight
double length = 0, diameter = 0;
Collection<Coordinate> bounds = configuration.getBounds();
}
length = maxX - minX;
}
-
+
for (RocketComponent c : configuration) {
if (c instanceof SymmetricComponent) {
double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
diameter = MathUtil.max(diameter, d1, d2);
}
}
-
+
extraText.setCG(cgx);
extraText.setCP(cpx);
extraText.setLength(length);
extraText.setDiameter(diameter);
extraText.setMass(cg.weight);
extraText.setWarnings(warnings);
-
+
if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
-
+
// TODO: LOW: Y-coordinate and rotation
extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
-
+
} else {
-
+
extraCP.setPosition(Double.NaN, Double.NaN);
extraCG.setPosition(Double.NaN, Double.NaN);
-
+
}
-
+
//////// Flight simulation in background
-
+
// Check whether to compute or not
if (!((SwingPreferences) Application.getPreferences()).computeFlightInBackground()) {
extraText.setFlightData(null);
stopBackgroundSimulation();
return;
}
-
+
// Check whether data is already up to date
if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
flightDataMotorID == configuration.getMotorConfigurationID()) {
return;
}
-
+
flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
flightDataMotorID = configuration.getMotorConfigurationID();
-
+
// Stop previous computation (if any)
stopBackgroundSimulation();
-
+
// Check that configuration has motors
if (!configuration.hasMotors()) {
extraText.setFlightData(FlightData.NaN_DATA);
extraText.setCalculatingData(false);
return;
}
-
+
// Start calculation process
extraText.setCalculatingData(true);
-
+
Rocket duplicate = (Rocket) configuration.getRocket().copy();
Simulation simulation = ((SwingPreferences)Application.getPreferences()).getBackgroundSimulation(duplicate);
simulation.getOptions().setMotorConfigurationID(
configuration.getMotorConfigurationID());
-
- backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
+
+ backgroundSimulationWorker = new BackgroundSimulationWorker(document, simulation);
backgroundSimulationExecutor.execute(backgroundSimulationWorker);
}
-
+
/**
* Cancels the current background simulation worker, if any.
*/
backgroundSimulationWorker = null;
}
}
-
-
+
+
/**
* A SimulationWorker that simulates the rocket flight in the background and
* sets the results to the extra text when finished. The worker can be cancelled
* if necessary.
*/
private class BackgroundSimulationWorker extends SimulationWorker {
-
- public BackgroundSimulationWorker(Simulation sim) {
+
+ private final CustomExpressionSimulationListener exprListener;
+
+ public BackgroundSimulationWorker(OpenRocketDocument doc, Simulation sim) {
super(sim);
+ List<CustomExpression> exprs = doc.getCustomExpressions();
+ exprListener = new CustomExpressionSimulationListener(exprs);
}
-
+
@Override
protected FlightData doInBackground() {
-
+
// Pause a little while to allow faster UI reaction
try {
Thread.sleep(300);
}
if (isCancelled() || backgroundSimulationWorker != this)
return null;
-
+
return super.doInBackground();
}
-
+
@Override
protected void simulationDone() {
// Do nothing if cancelled
if (isCancelled() || backgroundSimulationWorker != this)
return;
-
+
backgroundSimulationWorker = null;
extraText.setFlightData(simulation.getSimulatedData());
extraText.setCalculatingData(false);
figure.repaint();
figure3d.repaint();
}
-
+
@Override
protected SimulationListener[] getExtraListeners() {
return new SimulationListener[] {
InterruptListener.INSTANCE,
- ApogeeEndListener.INSTANCE };
+ ApogeeEndListener.INSTANCE,
+ exprListener};
+
}
-
+
@Override
protected void simulationInterrupted(Throwable t) {
// Do nothing on cancel, set N/A data otherwise
if (isCancelled() || backgroundSimulationWorker != this) // Double-check
return;
-
+
backgroundSimulationWorker = null;
extraText.setFlightData(FlightData.NaN_DATA);
extraText.setCalculatingData(false);
figure3d.repaint();
}
}
-
-
+
+
/**
* Adds the extra data to the figure. Currently this includes the CP and CG carets.
extraCP = new CPCaret(0, 0);
extraText = new RocketInfo(configuration);
updateExtras();
-
+
figure.clearRelativeExtra();
figure.addRelativeExtra(extraCP);
figure.addRelativeExtra(extraCG);
figure.addAbsoluteExtra(extraText);
-
-
+
+
figure3d.clearRelativeExtra();
//figure3d.addRelativeExtra(extraCP);
//figure3d.addRelativeExtra(extraCG);
figure3d.addAbsoluteExtra(extraText);
-
+
}
-
-
+
+
/**
* Updates the selection in the FigureParameters and repaints the figure.
* Ignores the event itself.
figure.setSelection(null);
return;
}
-
+
RocketComponent[] components = new RocketComponent[paths.length];
for (int i = 0; i < paths.length; i++)
components[i] = (RocketComponent) paths[i].getLastPathComponent();
figure.setSelection(components);
-
+
figure3d.setSelection(components);
}
-
-
+
+
/**
* An <code>Action</code> that shows whether the figure type is the type
private class FigureTypeAction extends AbstractAction implements StateChangeListener {
private static final long serialVersionUID = 1L;
private final int type;
-
+
public FigureTypeAction(int type) {
this.type = type;
stateChanged(null);
figure.addChangeListener(this);
}
-
+
@Override
public void actionPerformed(ActionEvent e) {
boolean state = (Boolean) getValue(Action.SELECTED_KEY);
}
stateChanged(null);
}
-
+
@Override
public void stateChanged(EventObject e) {
putValue(Action.SELECTED_KEY, figure.getType() == type && !is3d);
}
}
-
+
}