(Effectively a re-write of the MassCalculation code) - renamed some mass calculator accesor methods - MassData, InertiaMatrix refactored into 'RigidBody' class - refactors out cache code to separate wrapper class - calculations now use the Transform class to translate masses, CM, and MOI - new class: MassCalculation - contains relevant context for a given calculation: tree-root, type, time, config - contains most actual calculation code - calculations are tracked with a context class: MassCalculation
856 lines
24 KiB
Java
856 lines
24 KiB
Java
package net.sf.openrocket.gui.scalefigure;
|
|
|
|
|
|
import java.awt.BorderLayout;
|
|
import java.awt.Dimension;
|
|
import java.awt.Font;
|
|
import java.awt.Point;
|
|
import java.awt.event.InputEvent;
|
|
import java.awt.event.MouseEvent;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.EventListener;
|
|
import java.util.EventObject;
|
|
import java.util.List;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ThreadFactory;
|
|
|
|
import javax.swing.ComboBoxModel;
|
|
import javax.swing.DefaultComboBoxModel;
|
|
import javax.swing.JComboBox;
|
|
import javax.swing.JLabel;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JSlider;
|
|
import javax.swing.JViewport;
|
|
import javax.swing.SwingUtilities;
|
|
import javax.swing.event.TreeSelectionEvent;
|
|
import javax.swing.event.TreeSelectionListener;
|
|
import javax.swing.tree.TreePath;
|
|
import javax.swing.tree.TreeSelectionModel;
|
|
|
|
import net.miginfocom.swing.MigLayout;
|
|
import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
|
|
import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
|
|
import net.sf.openrocket.aerodynamics.FlightConditions;
|
|
import net.sf.openrocket.aerodynamics.WarningSet;
|
|
import net.sf.openrocket.document.OpenRocketDocument;
|
|
import net.sf.openrocket.document.Simulation;
|
|
import net.sf.openrocket.gui.adaptors.DoubleModel;
|
|
import net.sf.openrocket.gui.components.BasicSlider;
|
|
import net.sf.openrocket.gui.components.ConfigurationModel;
|
|
import net.sf.openrocket.gui.components.StageSelector;
|
|
import net.sf.openrocket.gui.components.UnitSelector;
|
|
import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
|
|
import net.sf.openrocket.gui.figure3d.RocketFigure3d;
|
|
import net.sf.openrocket.gui.figureelements.CGCaret;
|
|
import net.sf.openrocket.gui.figureelements.CPCaret;
|
|
import net.sf.openrocket.gui.figureelements.Caret;
|
|
import net.sf.openrocket.gui.figureelements.RocketInfo;
|
|
import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
|
|
import net.sf.openrocket.gui.simulation.SimulationWorker;
|
|
import net.sf.openrocket.gui.util.SwingPreferences;
|
|
import net.sf.openrocket.l10n.Translator;
|
|
import net.sf.openrocket.masscalc.MassCalculator;
|
|
import net.sf.openrocket.masscalc.RigidBody;
|
|
import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
|
|
import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
|
|
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
|
|
import net.sf.openrocket.rocketcomponent.FlightConfigurationId;
|
|
import net.sf.openrocket.rocketcomponent.Rocket;
|
|
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;
|
|
import net.sf.openrocket.startup.Application;
|
|
import net.sf.openrocket.unit.UnitGroup;
|
|
import net.sf.openrocket.util.ChangeSource;
|
|
import net.sf.openrocket.util.Chars;
|
|
import net.sf.openrocket.util.Coordinate;
|
|
import net.sf.openrocket.util.MathUtil;
|
|
import net.sf.openrocket.util.StateChangeListener;
|
|
|
|
|
|
/**
|
|
* A JPanel that contains a RocketFigure and buttons to manipulate the figure.
|
|
*
|
|
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
|
|
* @author Bill Kuker <bkuker@billkuker.com>
|
|
*/
|
|
@SuppressWarnings("serial")
|
|
public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
|
|
|
|
private static final Translator trans = Application.getTranslator();
|
|
|
|
public enum VIEW_TYPE {
|
|
SideView(false, RocketFigure.VIEW_SIDE),
|
|
BackView(false, RocketFigure.VIEW_BACK),
|
|
Figure3D(true, RocketFigure3d.TYPE_FIGURE),
|
|
Unfinished(true, RocketFigure3d.TYPE_UNFINISHED),
|
|
Finished(true, RocketFigure3d.TYPE_FINISHED);
|
|
|
|
public final boolean is3d;
|
|
private final int type;
|
|
|
|
VIEW_TYPE(final boolean is3d, final int type) {
|
|
this.is3d = is3d;
|
|
this.type = type;
|
|
};
|
|
|
|
@Override
|
|
public String toString() {
|
|
return trans.get("RocketPanel.FigTypeAct." + super.toString());
|
|
}
|
|
|
|
}
|
|
|
|
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;
|
|
private ScaleSelector scaleSelector;
|
|
|
|
/* Calculation of CP and CG */
|
|
private AerodynamicCalculator aerodynamicCalculator;
|
|
|
|
private final OpenRocketDocument document;
|
|
|
|
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 FlightConfigurationId flightDataMotorID = null;
|
|
|
|
private SimulationWorker backgroundSimulationWorker = null;
|
|
|
|
private List<EventListener> listeners = new ArrayList<EventListener>();
|
|
|
|
|
|
/**
|
|
* The executor service used for running the background simulations.
|
|
* This uses a fixed-sized thread pool for all background simulations
|
|
* with all threads in daemon mode and with minimum priority.
|
|
*/
|
|
private static final Executor backgroundSimulationExecutor;
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
public OpenRocketDocument getDocument(){
|
|
return this.document;
|
|
}
|
|
|
|
public RocketPanel(OpenRocketDocument document) {
|
|
this.document = document;
|
|
Rocket rkt = document.getRocket();
|
|
|
|
|
|
// TODO: FUTURE: calculator selection
|
|
aerodynamicCalculator = new BarrowmanCalculator();
|
|
|
|
// Create figure and custom scroll pane
|
|
figure = new RocketFigure(rkt);
|
|
figure3d = new RocketFigure3d(document);
|
|
|
|
figureHolder = new JPanel(new BorderLayout());
|
|
|
|
scrollPane = new ScaleScrollPane(figure) {
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
@Override
|
|
public void mouseClicked(MouseEvent event) {
|
|
handleMouseClick(event);
|
|
}
|
|
};
|
|
scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
|
|
scrollPane.setFitting(true);
|
|
|
|
createPanel();
|
|
|
|
is3d = true;
|
|
go2D();
|
|
|
|
rkt.addChangeListener(new StateChangeListener() {
|
|
@Override
|
|
public void stateChanged(EventObject e) {
|
|
updateExtras();
|
|
updateFigures();
|
|
}
|
|
});
|
|
|
|
rkt.addComponentChangeListener(new ComponentChangeListener() {
|
|
@Override
|
|
public void componentChanged(ComponentChangeEvent e) {
|
|
if (is3d) {
|
|
if (e.isTextureChange()) {
|
|
figure3d.flushTextureCaches();
|
|
}
|
|
}
|
|
updateFigures();
|
|
}
|
|
});
|
|
|
|
figure3d.addComponentSelectionListener(new RocketFigure3d.ComponentSelectionListener() {
|
|
@Override
|
|
public void componentClicked(RocketComponent clicked[], MouseEvent event) {
|
|
handleComponentClick(clicked, event);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void updateFigures() {
|
|
if (!is3d)
|
|
figure.updateFigure();
|
|
else
|
|
figure3d.updateFigure();
|
|
}
|
|
|
|
private void go3D() {
|
|
if (is3d)
|
|
return;
|
|
is3d = true;
|
|
figureHolder.remove(scrollPane);
|
|
figureHolder.add(figure3d, BorderLayout.CENTER);
|
|
rotationSlider.setEnabled(false);
|
|
scaleSelector.setEnabled(false);
|
|
|
|
revalidate();
|
|
figureHolder.revalidate();
|
|
|
|
figure3d.repaint();
|
|
}
|
|
|
|
private void go2D() {
|
|
if (!is3d)
|
|
return;
|
|
is3d = false;
|
|
figureHolder.remove(figure3d);
|
|
figureHolder.add(scrollPane, BorderLayout.CENTER);
|
|
rotationSlider.setEnabled(true);
|
|
scaleSelector.setEnabled(true);
|
|
revalidate();
|
|
figureHolder.revalidate();
|
|
figure.repaint();
|
|
}
|
|
|
|
/**
|
|
* Creates the layout and components of the panel.
|
|
*/
|
|
private void createPanel() {
|
|
final Rocket rkt = document.getRocket();
|
|
|
|
rkt.addChangeListener(new StateChangeListener(){
|
|
@Override
|
|
public void stateChanged(EventObject eo) {
|
|
updateExtras();
|
|
updateFigures();
|
|
}
|
|
});
|
|
|
|
setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
|
|
|
|
setPreferredSize(new Dimension(800, 300));
|
|
|
|
// View Type drop-down
|
|
ComboBoxModel<VIEW_TYPE> cm = new DefaultComboBoxModel<VIEW_TYPE>(VIEW_TYPE.values()) {
|
|
|
|
@Override
|
|
public void setSelectedItem(Object o) {
|
|
super.setSelectedItem(o);
|
|
VIEW_TYPE v = (VIEW_TYPE) o;
|
|
if (v.is3d) {
|
|
figure3d.setType(v.type);
|
|
go3D();
|
|
} else {
|
|
figure.setType(v);
|
|
updateExtras(); // when switching from side view to back view, need to clear CP & CG markers
|
|
go2D();
|
|
}
|
|
}
|
|
};
|
|
add(new JLabel(trans.get("RocketPanel.lbl.ViewType")), "spanx, split");
|
|
add(new JComboBox<VIEW_TYPE>(cm));
|
|
|
|
// Zoom level selector
|
|
scaleSelector = new ScaleSelector(scrollPane);
|
|
add(scaleSelector);
|
|
|
|
// Stage selector
|
|
StageSelector stageSelector = new StageSelector( rkt );
|
|
rkt.addChangeListener(stageSelector);
|
|
add(stageSelector);
|
|
|
|
// Flight configuration selector
|
|
//// Flight configuration:
|
|
JLabel label = new JLabel(trans.get("RocketPanel.lbl.Flightcfg"));
|
|
label.setHorizontalAlignment(JLabel.RIGHT);
|
|
add(label, "growx, right");
|
|
|
|
final JComboBox<FlightConfiguration> configComboBox = new JComboBox<>();
|
|
final ConfigurationModel configModel = new ConfigurationModel(rkt, configComboBox);
|
|
rkt.addChangeListener( configModel );
|
|
configComboBox.setModel(configModel);
|
|
add(configComboBox, "wrap, width 16%, wmin 100");
|
|
|
|
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Get the center of pressure figure element.
|
|
*
|
|
* @return center of pressure info
|
|
*/
|
|
public Caret getExtraCP() {
|
|
return extraCP;
|
|
}
|
|
|
|
/**
|
|
* Get the center of gravity figure element.
|
|
*
|
|
* @return center of gravity info
|
|
*/
|
|
public Caret getExtraCG() {
|
|
return extraCG;
|
|
}
|
|
|
|
/**
|
|
* Get the extra text figure element.
|
|
*
|
|
* @return extra text that contains info about the rocket design
|
|
*/
|
|
public RocketInfo getExtraText() {
|
|
return extraText;
|
|
}
|
|
|
|
public void setSelectionModel(TreeSelectionModel m) {
|
|
if (selectionModel != null) {
|
|
selectionModel.removeTreeSelectionListener(this);
|
|
}
|
|
selectionModel = m;
|
|
selectionModel.addTreeSelectionListener(this);
|
|
valueChanged((TreeSelectionEvent) null); // updates FigureParameters
|
|
}
|
|
|
|
/**
|
|
* Return the angle of attack used in CP calculation. NaN signifies the default value
|
|
* of zero.
|
|
* @return the angle of attack used, or NaN.
|
|
*/
|
|
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.
|
|
* @param aoa the angle of attack to use, or NaN
|
|
*/
|
|
public void setCPAOA(double aoa) {
|
|
if (MathUtil.equals(aoa, cpAOA) ||
|
|
(Double.isNaN(aoa) && Double.isNaN(cpAOA)))
|
|
return;
|
|
cpAOA = aoa;
|
|
updateExtras();
|
|
updateFigures();
|
|
fireChangeEvent();
|
|
}
|
|
|
|
public double getCPTheta() {
|
|
return cpTheta;
|
|
}
|
|
|
|
public void setCPTheta(double theta) {
|
|
if (MathUtil.equals(theta, cpTheta) ||
|
|
(Double.isNaN(theta) && Double.isNaN(cpTheta)))
|
|
return;
|
|
cpTheta = theta;
|
|
if (!Double.isNaN(theta))
|
|
figure.setRotation(theta);
|
|
updateExtras();
|
|
updateFigures();
|
|
fireChangeEvent();
|
|
}
|
|
|
|
public double getCPMach() {
|
|
return cpMach;
|
|
}
|
|
|
|
public void setCPMach(double mach) {
|
|
if (MathUtil.equals(mach, cpMach) ||
|
|
(Double.isNaN(mach) && Double.isNaN(cpMach)))
|
|
return;
|
|
cpMach = mach;
|
|
updateExtras();
|
|
updateFigures();
|
|
fireChangeEvent();
|
|
}
|
|
|
|
public double getCPRoll() {
|
|
return cpRoll;
|
|
}
|
|
|
|
public void setCPRoll(double roll) {
|
|
if (MathUtil.equals(roll, cpRoll) ||
|
|
(Double.isNaN(roll) && Double.isNaN(cpRoll)))
|
|
return;
|
|
cpRoll = roll;
|
|
updateExtras();
|
|
updateFigures();
|
|
fireChangeEvent();
|
|
}
|
|
|
|
@Override
|
|
public void addChangeListener(StateChangeListener listener) {
|
|
listeners.add(0, listener);
|
|
}
|
|
|
|
@Override
|
|
public void removeChangeListener(StateChangeListener listener) {
|
|
listeners.remove(listener);
|
|
}
|
|
|
|
protected void fireChangeEvent() {
|
|
EventObject e = new EventObject(this);
|
|
for (EventListener l : listeners) {
|
|
if (l instanceof StateChangeListener) {
|
|
((StateChangeListener) l).stateChanged(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle clicking on figure shapes. The functioning is the following:
|
|
*
|
|
* Get the components clicked.
|
|
* If no component is clicked, do nothing.
|
|
* If the currently selected component is in the set, keep it,
|
|
* unless the selector specified is pressed. If it is pressed, cycle to
|
|
* 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 p0 = event.getPoint();
|
|
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) {
|
|
selectionModel.setSelectionPath(null);
|
|
return;
|
|
}
|
|
|
|
// Check whether the currently selected component is in the clicked components.
|
|
TreePath path = selectionModel.getSelectionPath();
|
|
if (path != null) {
|
|
RocketComponent current = (RocketComponent) path.getLastPathComponent();
|
|
path = null;
|
|
for (int i = 0; i < clicked.length; i++) {
|
|
if (clicked[i] == current) {
|
|
if (event.isShiftDown() && (event.getClickCount() == 1)) {
|
|
path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]);
|
|
} else {
|
|
path = ComponentTreeModel.makeTreePath(clicked[i]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Currently selected component not clicked
|
|
if (path == null) {
|
|
if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
|
|
path = ComponentTreeModel.makeTreePath(clicked[1]);
|
|
} else {
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the extra data included in the figure. Currently this includes
|
|
* the CP and CG carets.
|
|
*/
|
|
private WarningSet warnings = new WarningSet();
|
|
|
|
private void updateExtras() {
|
|
Coordinate cp, cg;
|
|
double cpx, cgx;
|
|
|
|
FlightConfiguration curConfig = document.getSelectedConfiguration();
|
|
// TODO: MEDIUM: User-definable conditions
|
|
FlightConditions conditions = new FlightConditions(curConfig);
|
|
warnings.clear();
|
|
|
|
if (!Double.isNaN(cpMach)) {
|
|
conditions.setMach(cpMach);
|
|
extraText.setMach(cpMach);
|
|
} else {
|
|
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(curConfig, conditions, warnings);
|
|
} else {
|
|
cp = aerodynamicCalculator.getWorstCP(curConfig, conditions, warnings);
|
|
}
|
|
extraText.setTheta(cpTheta);
|
|
|
|
cg = MassCalculator.calculateLaunch( curConfig).getCM();
|
|
|
|
if (cp.weight > MassCalculator.MIN_MASS){
|
|
cpx = cp.x;
|
|
}else{
|
|
cpx = Double.NaN;
|
|
}
|
|
|
|
if (cg.weight > MassCalculator.MIN_MASS){
|
|
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 = curConfig.getBounds();
|
|
if (!bounds.isEmpty()) {
|
|
double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
|
|
for (Coordinate c : bounds) {
|
|
if (c.x < minX)
|
|
minX = c.x;
|
|
if (c.x > maxX)
|
|
maxX = c.x;
|
|
}
|
|
length = maxX - minX;
|
|
}
|
|
|
|
for (RocketComponent c : curConfig.getActiveComponents()) {
|
|
if (c instanceof SymmetricComponent) {
|
|
double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
|
|
double d2 = ((SymmetricComponent) c).getAftRadius() * 2;
|
|
diameter = MathUtil.max(diameter, d1, d2);
|
|
}
|
|
}
|
|
|
|
RigidBody emptyInfo = MassCalculator.calculateStructure( curConfig );
|
|
|
|
extraText.setCG(cgx);
|
|
extraText.setCP(cpx);
|
|
extraText.setLength(length);
|
|
extraText.setDiameter(diameter);
|
|
extraText.setMass(cg.weight);
|
|
extraText.setMassWithoutMotors( emptyInfo.getMass() );
|
|
extraText.setWarnings(warnings);
|
|
|
|
if (figure.getType() == RocketPanel.VIEW_TYPE.SideView && 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);
|
|
extraText.setCalculatingData(false);
|
|
stopBackgroundSimulation();
|
|
return;
|
|
}
|
|
|
|
// Check whether data is already up to date
|
|
if (flightDataFunctionalID == curConfig.getRocket().getFunctionalModID() &&
|
|
flightDataMotorID == curConfig.getId()) {
|
|
return;
|
|
}
|
|
|
|
flightDataFunctionalID = curConfig.getRocket().getFunctionalModID();
|
|
flightDataMotorID = curConfig.getId();
|
|
|
|
// Stop previous computation (if any)
|
|
stopBackgroundSimulation();
|
|
|
|
// Check that configuration has motors
|
|
if (!curConfig.hasMotors()){
|
|
extraText.setFlightData(FlightData.NaN_DATA);
|
|
extraText.setCalculatingData(false);
|
|
return;
|
|
}
|
|
|
|
// Start calculation process
|
|
if(((SwingPreferences) Application.getPreferences()).computeFlightInBackground()){
|
|
extraText.setCalculatingData(true);
|
|
|
|
Rocket duplicate = (Rocket) document.getRocket().copy();
|
|
Simulation simulation = ((SwingPreferences) Application.getPreferences()).getBackgroundSimulation(duplicate);
|
|
simulation.setFlightConfigurationId( document.getSelectedConfiguration().getId());
|
|
|
|
backgroundSimulationWorker = new BackgroundSimulationWorker(document, simulation);
|
|
backgroundSimulationExecutor.execute(backgroundSimulationWorker);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels the current background simulation worker, if any.
|
|
*/
|
|
private void stopBackgroundSimulation() {
|
|
if (backgroundSimulationWorker != null) {
|
|
backgroundSimulationWorker.cancel(true);
|
|
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 {
|
|
|
|
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);
|
|
} catch (InterruptedException ignore) {
|
|
}
|
|
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,
|
|
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);
|
|
figure.repaint();
|
|
figure3d.repaint();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the extra data to the figure. Currently this includes the CP and CG carets.
|
|
*/
|
|
private void addExtras() {
|
|
FlightConfiguration curConfig = document.getSelectedConfiguration();
|
|
extraCG = new CGCaret(0, 0);
|
|
extraCP = new CPCaret(0, 0);
|
|
extraText = new RocketInfo(curConfig);
|
|
|
|
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.
|
|
*/
|
|
@Override
|
|
public void valueChanged(TreeSelectionEvent e) {
|
|
TreePath[] paths = selectionModel.getSelectionPaths();
|
|
if (paths == null) {
|
|
figure.setSelection(null);
|
|
figure3d.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
|
|
// * given in the constructor.
|
|
// *
|
|
// * @author Sampo Niskanen <sampo.niskanen@iki.fi>
|
|
// */
|
|
// private class FigureTypeAction extends AbstractAction implements
|
|
// StateChangeListener {
|
|
// private static final long serialVersionUID = 1L;
|
|
// private final VIEW_TYPE type;
|
|
//
|
|
// public FigureTypeAction(VIEW_TYPE type) {
|
|
// this.type = type;
|
|
// stateChanged(null);
|
|
// figure.addChangeListener(this);
|
|
// }
|
|
//
|
|
// @Override
|
|
// public void actionPerformed(ActionEvent e) {
|
|
// boolean state = (Boolean) getValue(Action.SELECTED_KEY);
|
|
// if (state == true) {
|
|
// // This view has been selected
|
|
// figure.setType(type);
|
|
// go2D();
|
|
// updateExtras();
|
|
// }
|
|
// stateChanged(null);
|
|
// }
|
|
//
|
|
// @Override
|
|
// public void stateChanged(EventObject e) {
|
|
// putValue(Action.SELECTED_KEY, figure.getType() == type && !is3d);
|
|
// }
|
|
// }
|
|
|
|
}
|