package net.sf.openrocket.gui.dialogs;
import static net.sf.openrocket.unit.Unit.NOUNIT;
import static net.sf.openrocket.util.Chars.ALPHA;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JToggleButton;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.TableCellRenderer;
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
import net.sf.openrocket.aerodynamics.AerodynamicForces;
import net.sf.openrocket.aerodynamics.FlightConditions;
import net.sf.openrocket.aerodynamics.Warning;
import net.sf.openrocket.aerodynamics.WarningSet;
import net.sf.openrocket.gui.adaptors.Column;
import net.sf.openrocket.gui.adaptors.ColumnTable;
import net.sf.openrocket.gui.adaptors.ColumnTableModel;
import net.sf.openrocket.gui.adaptors.DoubleModel;
import net.sf.openrocket.gui.adaptors.FlightConfigurationModel;
import net.sf.openrocket.gui.components.BasicSlider;
import net.sf.openrocket.gui.components.StageSelector;
import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.gui.components.UnitSelector;
import net.sf.openrocket.gui.scalefigure.RocketPanel;
import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.masscalc.BasicMassCalculator;
import net.sf.openrocket.masscalc.MassCalculator;
import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
import net.sf.openrocket.rocketcomponent.Configuration;
import net.sf.openrocket.rocketcomponent.FinSet;
import net.sf.openrocket.rocketcomponent.Rocket;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.StateChangeListener;
public class ComponentAnalysisDialog extends JDialog implements StateChangeListener {
private static ComponentAnalysisDialog singletonDialog = null;
private static final Translator trans = Application.getTranslator();
private final FlightConditions conditions;
private final Configuration configuration;
private final DoubleModel theta, aoa, mach, roll;
private final JToggleButton worstToggle;
private boolean fakeChange = false;
private AerodynamicCalculator aerodynamicCalculator;
private final MassCalculator massCalculator = new BasicMassCalculator();
private final ColumnTableModel cpTableModel;
private final ColumnTableModel dragTableModel;
private final ColumnTableModel rollTableModel;
private final JList warningList;
private final List<AerodynamicForces> cpData = new ArrayList<AerodynamicForces>();
private final List<Coordinate> cgData = new ArrayList<Coordinate>();
private final List<AerodynamicForces> dragData = new ArrayList<AerodynamicForces>();
private double totalCD = 0;
private final List<AerodynamicForces> rollData = new ArrayList<AerodynamicForces>();
public ComponentAnalysisDialog(final RocketPanel rocketPanel) {
////Component analysis
super(SwingUtilities.getWindowAncestor(rocketPanel),
trans.get("componentanalysisdlg.componentanalysis"));
JTable table;
JPanel panel = new JPanel(new MigLayout("fill", "[][35lp::][fill][fill]"));
add(panel);
this.configuration = rocketPanel.getConfiguration();
this.aerodynamicCalculator = rocketPanel.getAerodynamicCalculator().newInstance();
conditions = new FlightConditions(configuration);
rocketPanel.setCPAOA(0);
aoa = new DoubleModel(rocketPanel, "CPAOA", UnitGroup.UNITS_ANGLE, 0, Math.PI);
rocketPanel.setCPMach(Application.getPreferences().getDefaultMach());
mach = new DoubleModel(rocketPanel, "CPMach", UnitGroup.UNITS_COEFFICIENT, 0);
rocketPanel.setCPTheta(rocketPanel.getFigure().getRotation());
theta = new DoubleModel(rocketPanel, "CPTheta", UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
rocketPanel.setCPRoll(0);
roll = new DoubleModel(rocketPanel, "CPRoll", UnitGroup.UNITS_ROLL);
//// Wind direction:
panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.winddir")), "width 120lp!");
panel.add(new UnitSelector(theta, true), "width 50lp!");
BasicSlider slider = new BasicSlider(theta.getSliderModel(0, 2 * Math.PI));
panel.add(slider, "growx, split 2");
//// Worst button
worstToggle = new JToggleButton(trans.get("componentanalysisdlg.ToggleBut.worst"));
worstToggle.setSelected(true);
worstToggle.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
stateChanged(null);
}
});
slider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (!fakeChange)
worstToggle.setSelected(false);
}
});
panel.add(worstToggle, "");
warningList = new JList();
JScrollPane scrollPane = new JScrollPane(warningList);
////Warnings:
scrollPane.setBorder(BorderFactory.createTitledBorder(trans.get("componentanalysisdlg.TitledBorder.warnings")));
panel.add(scrollPane, "gap paragraph, spany 4, width 300lp!, growy 1, height :100lp:, wrap");
////Angle of attack:
panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.angleofattack")), "width 120lp!");
panel.add(new UnitSelector(aoa, true), "width 50lp!");
panel.add(new BasicSlider(aoa.getSliderModel(0, Math.PI)), "growx, wrap");
//// Mach number:
panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.machnumber")), "width 120lp!");
panel.add(new UnitSelector(mach, true), "width 50lp!");
panel.add(new BasicSlider(mach.getSliderModel(0, 3)), "growx, wrap");
//// Roll rate:
panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.rollrate")), "width 120lp!");
panel.add(new UnitSelector(roll, true), "width 50lp!");
panel.add(new BasicSlider(roll.getSliderModel(-20 * 2 * Math.PI, 20 * 2 * Math.PI)),
"growx, wrap paragraph");
// Stage and motor selection:
//// Active stages:
panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.activestages")), "spanx, split, gapafter rel");
panel.add(new StageSelector(configuration), "gapafter paragraph");
//// Motor configuration:
JLabel label = new JLabel(trans.get("componentanalysisdlg.lbl.motorconf"));
label.setHorizontalAlignment(JLabel.RIGHT);
panel.add(label, "growx, right");
panel.add(new JComboBox(new FlightConfigurationModel(configuration)), "wrap");
// Tabbed pane
JTabbedPane tabbedPane = new JTabbedPane();
panel.add(tabbedPane, "spanx, growx, growy");
// Create the CP data table
cpTableModel = new ColumnTableModel(
//// Component
new Column(trans.get("componentanalysisdlg.TabStability.Col.Component")) {
@Override
public Object getValueAt(int row) {
RocketComponent c = cpData.get(row).getComponent();
if (c instanceof Rocket) {
return trans.get("componentanalysisdlg.TOTAL");
}
return c.toString();
}
@Override
public int getDefaultWidth() {
return 200;
}
},
new Column(trans.get("componentanalysisdlg.TabStability.Col.CG") + " / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) {
private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
@Override
public Object getValueAt(int row) {
return unit.toString(cgData.get(row).x);
}
},
new Column(trans.get("componentanalysisdlg.TabStability.Col.Mass") + " / " + UnitGroup.UNITS_MASS.getDefaultUnit().getUnit()) {
private Unit unit = UnitGroup.UNITS_MASS.getDefaultUnit();
@Override
public Object getValueAt(int row) {
return unit.toString(cgData.get(row).weight);
}
},
new Column(trans.get("componentanalysisdlg.TabStability.Col.CP") + " / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) {
private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
@Override
public Object getValueAt(int row) {
return unit.toString(cpData.get(row).getCP().x);
}
},
new Column("<html>C<sub>N<sub>" + ALPHA + "</sub></sub>") {
@Override
public Object getValueAt(int row) {
return NOUNIT.toString(cpData.get(row).getCP().weight);
}
}
) {
@Override
public int getRowCount() {
return cpData.size();
}
};
table = new ColumnTable(cpTableModel);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setSelectionBackground(Color.LIGHT_GRAY);
table.setSelectionForeground(Color.BLACK);
cpTableModel.setColumnWidths(table.getColumnModel());
table.setDefaultRenderer(Object.class, new CustomCellRenderer());
// table.setShowHorizontalLines(false);
// table.setShowVerticalLines(true);
JScrollPane scrollpane = new JScrollPane(table);
scrollpane.setPreferredSize(new Dimension(600, 200));
//// Stability and Stability information
tabbedPane.addTab(trans.get("componentanalysisdlg.TabStability"),
null, scrollpane, trans.get("componentanalysisdlg.TabStability.ttip"));
// Create the drag data table
dragTableModel = new ColumnTableModel(
//// Component
new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Component")) {
@Override
public Object getValueAt(int row) {
RocketComponent c = dragData.get(row).getComponent();
if (c instanceof Rocket) {
return trans.get("componentanalysisdlg.TOTAL");
}
return c.toString();
}
@Override
public int getDefaultWidth() {
return 200;
}
},
//// <html>Pressure C<sub>D</sub>
new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Pressure")) {
@Override
public Object getValueAt(int row) {
return dragData.get(row).getPressureCD();
}
},
//// <html>Base C<sub>D</sub>
new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Base")) {
@Override
public Object getValueAt(int row) {
return dragData.get(row).getBaseCD();
}
},
//// <html>Friction C<sub>D</sub>
new Column(trans.get("componentanalysisdlg.dragTableModel.Col.friction")) {
@Override
public Object getValueAt(int row) {
return dragData.get(row).getFrictionCD();
}
},
//// <html>Total C<sub>D</sub>
new Column(trans.get("componentanalysisdlg.dragTableModel.Col.total")) {
@Override
public Object getValueAt(int row) {
return dragData.get(row).getCD();
}
}
) {
@Override
public int getRowCount() {
return dragData.size();
}
};
table = new JTable(dragTableModel);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setSelectionBackground(Color.LIGHT_GRAY);
table.setSelectionForeground(Color.BLACK);
dragTableModel.setColumnWidths(table.getColumnModel());
table.setDefaultRenderer(Object.class, new DragCellRenderer(new Color(0.5f, 1.0f, 0.5f)));
// table.setShowHorizontalLines(false);
// table.setShowVerticalLines(true);
scrollpane = new JScrollPane(table);
scrollpane.setPreferredSize(new Dimension(600, 200));
//// Drag characteristics and Drag characteristics tooltip
tabbedPane.addTab(trans.get("componentanalysisdlg.dragTabchar"), null, scrollpane,
trans.get("componentanalysisdlg.dragTabchar.ttip"));
// Create the roll data table
rollTableModel = new ColumnTableModel(
//// Component
new Column(trans.get("componentanalysisdlg.rollTableModel.Col.component")) {
@Override
public Object getValueAt(int row) {
RocketComponent c = rollData.get(row).getComponent();
if (c instanceof Rocket) {
return trans.get("componentanalysisdlg.TOTAL");
}
return c.toString();
}
},
//// Roll forcing coefficient
new Column(trans.get("componentanalysisdlg.rollTableModel.Col.rollforc")) {
@Override
public Object getValueAt(int row) {
return rollData.get(row).getCrollForce();
}
},
//// Roll damping coefficient
new Column(trans.get("componentanalysisdlg.rollTableModel.Col.rolldamp")) {
@Override
public Object getValueAt(int row) {
return rollData.get(row).getCrollDamp();
}
},
//// <html>Total C<sub>l</sub>
new Column(trans.get("componentanalysisdlg.rollTableModel.Col.total")) {
@Override
public Object getValueAt(int row) {
return rollData.get(row).getCroll();
}
}
) {
@Override
public int getRowCount() {
return rollData.size();
}
};
table = new JTable(rollTableModel);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setSelectionBackground(Color.LIGHT_GRAY);
table.setSelectionForeground(Color.BLACK);
rollTableModel.setColumnWidths(table.getColumnModel());
scrollpane = new JScrollPane(table);
scrollpane.setPreferredSize(new Dimension(600, 200));
//// Roll dynamics and Roll dynamics tooltip
tabbedPane.addTab(trans.get("componentanalysisdlg.rollTableModel"), null, scrollpane,
trans.get("componentanalysisdlg.rollTableModel.ttip"));
// Add the data updater to listen to changes in aoa and theta
mach.addChangeListener(this);
theta.addChangeListener(this);
aoa.addChangeListener(this);
roll.addChangeListener(this);
configuration.addChangeListener(this);
this.stateChanged(null);
// Remove listeners when closing window
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
//System.out.println("Closing method called: " + this);
theta.removeChangeListener(ComponentAnalysisDialog.this);
aoa.removeChangeListener(ComponentAnalysisDialog.this);
mach.removeChangeListener(ComponentAnalysisDialog.this);
roll.removeChangeListener(ComponentAnalysisDialog.this);
configuration.removeChangeListener(ComponentAnalysisDialog.this);
//System.out.println("SETTING NAN VALUES");
rocketPanel.setCPAOA(Double.NaN);
rocketPanel.setCPTheta(Double.NaN);
rocketPanel.setCPMach(Double.NaN);
rocketPanel.setCPRoll(Double.NaN);
singletonDialog = null;
}
});
//// Reference length:
panel.add(new StyledLabel(trans.get("componentanalysisdlg.lbl.reflenght"), -1),
"span, split, gapleft para, gapright rel");
DoubleModel dm = new DoubleModel(conditions, "RefLength", UnitGroup.UNITS_LENGTH);
UnitSelector sel = new UnitSelector(dm, true);
sel.resizeFont(-1);
panel.add(sel, "gapright para");
//// Reference area:
panel.add(new StyledLabel(trans.get("componentanalysisdlg.lbl.refarea"), -1), "gapright rel");
dm = new DoubleModel(conditions, "RefArea", UnitGroup.UNITS_AREA);
sel = new UnitSelector(dm, true);
sel.resizeFont(-1);
panel.add(sel, "wrap");
// Buttons
JButton button;
// TODO: LOW: printing
// button = new JButton("Print");
// button.addActionListener(new ActionListener() {
// public void actionPerformed(ActionEvent e) {
// try {
// table.print();
// } catch (PrinterException e1) {
// JOptionPane.showMessageDialog(ComponentAnalysisDialog.this,
// "An error occurred while printing.", "Print error",
// JOptionPane.ERROR_MESSAGE);
// }
// }
// });
// panel.add(button,"tag ok");
//button = new JButton("Close");
//Close button
button = new JButton(trans.get("dlg.but.close"));
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ComponentAnalysisDialog.this.dispose();
}
});
panel.add(button, "span, split, tag cancel");
this.setLocationByPlatform(true);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
pack();
GUIUtil.setDisposableDialogOptions(this, null);
}
/**
* Updates the data in the table and fires a table data change event.
*/
@Override
public void stateChanged(EventObject e) {
AerodynamicForces forces;
WarningSet set = new WarningSet();
conditions.setAOA(aoa.getValue());
conditions.setTheta(theta.getValue());
conditions.setMach(mach.getValue());
conditions.setRollRate(roll.getValue());
conditions.setReference(configuration);
if (worstToggle.isSelected()) {
aerodynamicCalculator.getWorstCP(configuration, conditions, null);
if (!MathUtil.equals(conditions.getTheta(), theta.getValue())) {
fakeChange = true;
theta.setValue(conditions.getTheta()); // Fires a stateChanged event
fakeChange = false;
return;
}
}
Map<RocketComponent, AerodynamicForces> aeroData =
aerodynamicCalculator.getForceAnalysis(configuration, conditions, set);
Map<RocketComponent, Coordinate> massData =
massCalculator.getCGAnalysis(configuration, MassCalcType.LAUNCH_MASS);
cpData.clear();
cgData.clear();
dragData.clear();
rollData.clear();
for (RocketComponent c : configuration) {
forces = aeroData.get(c);
Coordinate cg = massData.get(c);
if (forces == null)
continue;
if (forces.getCP() != null) {
cpData.add(forces);
cgData.add(cg);
}
if (!Double.isNaN(forces.getCD())) {
dragData.add(forces);
}
if (c instanceof FinSet) {
rollData.add(forces);
}
}
forces = aeroData.get(configuration.getRocket());
if (forces != null) {
cpData.add(forces);
cgData.add(massData.get(configuration.getRocket()));
dragData.add(forces);
rollData.add(forces);
totalCD = forces.getCD();
} else {
totalCD = 0;
}
// Set warnings
if (set.isEmpty()) {
warningList.setListData(new String[] {
trans.get("componentanalysisdlg.noWarnings")
});
} else {
warningList.setListData(new Vector<Warning>(set));
}
cpTableModel.fireTableDataChanged();
dragTableModel.fireTableDataChanged();
rollTableModel.fireTableDataChanged();
}
private class CustomCellRenderer extends JLabel implements TableCellRenderer {
private final Font normalFont;
private final Font boldFont;
public CustomCellRenderer() {
super();
normalFont = getFont();
boldFont = normalFont.deriveFont(Font.BOLD);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
this.setText(value.toString());
if ((row < 0) || (row >= cpData.size()))
return this;
if (cpData.get(row).getComponent() instanceof Rocket) {
this.setFont(boldFont);
} else {
this.setFont(normalFont);
}
return this;
}
}
private class DragCellRenderer extends JLabel implements TableCellRenderer {
private final Font normalFont;
private final Font boldFont;
public DragCellRenderer(Color baseColor) {
super();
normalFont = getFont();
boldFont = normalFont.deriveFont(Font.BOLD);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
if (value instanceof Double) {
// A drag coefficient
double cd = (Double) value;
this.setText(String.format("%.2f (%.0f%%)", cd, 100 * cd / totalCD));
float r = (float) (cd / 1.5);
float hue = MathUtil.clamp(0.3333f * (1 - 2.0f * r), 0, 0.3333f);
float sat = MathUtil.clamp(0.8f * r + 0.1f * (1 - r), 0, 1);
float val = 1.0f;
this.setBackground(Color.getHSBColor(hue, sat, val));
this.setOpaque(true);
this.setHorizontalAlignment(SwingConstants.CENTER);
} else {
// Other
this.setText(value.toString());
this.setOpaque(false);
this.setHorizontalAlignment(SwingConstants.LEFT);
}
if ((row < 0) || (row >= dragData.size()))
return this;
if ((dragData.get(row).getComponent() instanceof Rocket) || (column == 4)) {
this.setFont(boldFont);
} else {
this.setFont(normalFont);
}
return this;
}
}
///////// Singleton implementation
public static void showDialog(RocketPanel rocketpanel) {
if (singletonDialog != null)
singletonDialog.dispose();
singletonDialog = new ComponentAnalysisDialog(rocketpanel);
singletonDialog.setVisible(true);
}
public static void hideDialog() {
if (singletonDialog != null)
singletonDialog.dispose();
}
}