[#2525] Implement component analysis plotting
This commit is contained in:
parent
0fd1a93f5f
commit
3312cb4f6c
@ -948,6 +948,35 @@ componentanalysisdlg.TabStability.Col.Component = Component
|
||||
componentanalysisdlg.TOTAL = Total (Rocket)
|
||||
componentanalysisdlg.noWarnings = <html><i><font color=\"gray\">No warnings.</font></i>
|
||||
|
||||
! CAPlotExportDialog
|
||||
CAPlotExportDialog.title = Plot / Export Component Analysis
|
||||
CAPlotExportDialog.XAxisConfiguration = X axis configuration
|
||||
CAPlotExportDialog.lbl.XAxis = X axis:
|
||||
CAPlotExportDialog.lbl.XAxis.ttip = The X axis (domain type) for the parameter sweep.
|
||||
CAPlotExportDialog.lbl.MinValue = Minimum:
|
||||
CAPlotExportDialog.lbl.MinValue.ttip = Minimum value for the parameter sweep.
|
||||
CAPlotExportDialog.lbl.MaxValue = Maximum:
|
||||
CAPlotExportDialog.lbl.MaxValue.ttip = Maximum value for the parameter sweep.
|
||||
CAPlotExportDialog.lbl.Delta = Step size:
|
||||
CAPlotExportDialog.lbl.Delta.ttip = Step size (increments) for the parameter sweep.
|
||||
CAPlotExportDialog.tab.Plot = Plot
|
||||
CAPlotExportDialog.tab.Export = Export
|
||||
|
||||
! CADataTypeGroup
|
||||
CADataTypeGroup.DOMAIN = Domain Parameter
|
||||
CADataTypeGroup.DRAG = Drag Characteristics
|
||||
CADataTypeGroup.STABILITY = Stability
|
||||
CADataTypeGroup.ROLL = Roll Dynamics
|
||||
|
||||
! CAPlotConfiguration
|
||||
CAPlotConfiguration.TotalCD = <html>Total C<sub>D</sub> vs. Mach number</html>
|
||||
|
||||
! CAPlotTypeSelector
|
||||
CAPlotTypeSelector.lbl.component = Component:
|
||||
|
||||
! CAPlotPanel
|
||||
CAPlotPanel.lbl.PlotTitle = Component Analysis Plot
|
||||
|
||||
! Custom Material dialog
|
||||
custmatdlg.title.Custommaterial = Custom material
|
||||
custmatdlg.lbl.Materialname = Material name:
|
||||
|
@ -0,0 +1,181 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.simulation.DataBranch;
|
||||
import info.openrocket.core.util.ArrayList;
|
||||
import info.openrocket.core.util.ModID;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* DataBranch for storing component analysis data.
|
||||
*/
|
||||
public class CADataBranch extends DataBranch<CADataType> {
|
||||
// Map to store values for each CADataType-RocketComponent pair
|
||||
private final Map<CADataType, Map<RocketComponent, ArrayList<Double>>> componentValues = new HashMap<>();
|
||||
// Maps to store min and max values for each CADataType-RocketComponent pair
|
||||
private final Map<CADataType, Map<RocketComponent, Double>> componentMinValues = new HashMap<>();
|
||||
private final Map<CADataType, Map<RocketComponent, Double>> componentMaxValues = new HashMap<>();
|
||||
|
||||
public CADataBranch(String name, CADataType... types) {
|
||||
super(name);
|
||||
for (CADataType type : types) {
|
||||
addType(type);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addType(CADataType type) {
|
||||
super.addType(type);
|
||||
if (!(type instanceof CADomainDataType)) {
|
||||
componentValues.put(type, new HashMap<>());
|
||||
componentMinValues.put(type, new HashMap<>());
|
||||
componentMaxValues.put(type, new HashMap<>());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPoint() {
|
||||
mutable.check();
|
||||
|
||||
for (Map.Entry<CADataType, ArrayList<Double>> entry : values.entrySet()) {
|
||||
entry.getValue().add(Double.NaN);
|
||||
}
|
||||
|
||||
for (Map<RocketComponent, ArrayList<Double>> componentMap : componentValues.values()) {
|
||||
for (ArrayList<Double> list : componentMap.values()) {
|
||||
list.add(Double.NaN);
|
||||
}
|
||||
}
|
||||
|
||||
modID = new ModID();
|
||||
}
|
||||
|
||||
public void setValue(CADataType type, RocketComponent component, double value) {
|
||||
mutable.check();
|
||||
|
||||
if (type instanceof CADomainDataType) {
|
||||
throw new IllegalArgumentException("Use setDomainValue for CADomainDataType");
|
||||
}
|
||||
|
||||
// Ensure the type exists
|
||||
if (!componentValues.containsKey(type)) {
|
||||
addType(type);
|
||||
}
|
||||
|
||||
Map<RocketComponent, ArrayList<Double>> typeMap = componentValues.get(type);
|
||||
ArrayList<Double> list = typeMap.computeIfAbsent(component, k -> {
|
||||
ArrayList<Double> newList = new ArrayList<>();
|
||||
int n = getLength();
|
||||
for (int i = 0; i < n; i++) {
|
||||
newList.add(Double.NaN);
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
|
||||
if (list.size() > 0) {
|
||||
list.set(list.size() - 1, value);
|
||||
}
|
||||
|
||||
// Update min and max values
|
||||
updateMinMaxValues(type, component, value);
|
||||
|
||||
modID = new ModID();
|
||||
}
|
||||
|
||||
public void setDomainValue(CADomainDataType domainType, double value) {
|
||||
mutable.check();
|
||||
|
||||
// Use the existing DataBranch functionality for domain values
|
||||
super.setValue(domainType, value);
|
||||
|
||||
modID = new ModID();
|
||||
}
|
||||
|
||||
private void updateMinMaxValues(CADataType type, RocketComponent component, double value) {
|
||||
Map<RocketComponent, Double> minMap = componentMinValues.get(type);
|
||||
Map<RocketComponent, Double> maxMap = componentMaxValues.get(type);
|
||||
|
||||
double min = minMap.getOrDefault(component, Double.NaN);
|
||||
double max = maxMap.getOrDefault(component, Double.NaN);
|
||||
|
||||
if (Double.isNaN(min) || (value < min)) {
|
||||
minMap.put(component, value);
|
||||
}
|
||||
if (Double.isNaN(max) || (value > max)) {
|
||||
maxMap.put(component, value);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Double> get(CADataType type, RocketComponent component) {
|
||||
if (type instanceof CADomainDataType) {
|
||||
return super.get(type);
|
||||
}
|
||||
|
||||
Map<RocketComponent, ArrayList<Double>> typeMap = componentValues.get(type);
|
||||
if (typeMap == null) return null;
|
||||
|
||||
ArrayList<Double> list = typeMap.get(component);
|
||||
if (list == null) return null;
|
||||
|
||||
return list.clone();
|
||||
}
|
||||
|
||||
public Double getByIndex(CADataType type, RocketComponent component, int index) {
|
||||
if (index < 0 || index >= getLength()) {
|
||||
throw new IllegalArgumentException("Index out of bounds");
|
||||
}
|
||||
|
||||
if (type instanceof CADomainDataType) {
|
||||
return super.getByIndex(type, index);
|
||||
}
|
||||
|
||||
Map<RocketComponent, ArrayList<Double>> typeMap = componentValues.get(type);
|
||||
if (typeMap == null) return null;
|
||||
|
||||
ArrayList<Double> list = typeMap.get(component);
|
||||
if (list == null) return null;
|
||||
|
||||
return list.get(index);
|
||||
}
|
||||
|
||||
public double getLast(CADataType type, RocketComponent component) {
|
||||
if (type instanceof CADomainDataType) {
|
||||
return super.getLast(type);
|
||||
}
|
||||
|
||||
Map<RocketComponent, ArrayList<Double>> typeMap = componentValues.get(type);
|
||||
if (typeMap == null) return Double.NaN;
|
||||
|
||||
ArrayList<Double> list = typeMap.get(component);
|
||||
if (list == null || list.isEmpty()) return Double.NaN;
|
||||
|
||||
return list.get(list.size() - 1);
|
||||
}
|
||||
|
||||
public double getMinimum(CADataType type, RocketComponent component) {
|
||||
if (type instanceof CADomainDataType) {
|
||||
return super.getMinimum(type);
|
||||
}
|
||||
|
||||
Map<RocketComponent, Double> minMap = componentMinValues.get(type);
|
||||
if (minMap == null) return Double.NaN;
|
||||
|
||||
Double min = minMap.get(component);
|
||||
return (min != null) ? min : Double.NaN;
|
||||
}
|
||||
|
||||
public double getMaximum(CADataType type, RocketComponent component) {
|
||||
if (type instanceof CADomainDataType) {
|
||||
return super.getMaximum(type);
|
||||
}
|
||||
|
||||
Map<RocketComponent, Double> maxMap = componentMaxValues.get(type);
|
||||
if (maxMap == null) return Double.NaN;
|
||||
|
||||
Double max = maxMap.get(component);
|
||||
return (max != null) ? max : Double.NaN;
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.rocketcomponent.ComponentAssembly;
|
||||
import info.openrocket.core.rocketcomponent.FinSet;
|
||||
import info.openrocket.core.rocketcomponent.Rocket;
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.simulation.DataType;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.unit.UnitGroup;
|
||||
import info.openrocket.core.util.Groupable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static info.openrocket.core.util.Chars.ALPHA;
|
||||
|
||||
public class CADataType implements Comparable<CADataType>, Groupable<CADataTypeGroup>, DataType {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
private final String name;
|
||||
private final String symbol;
|
||||
private final UnitGroup units;
|
||||
private final CADataTypeGroup group;
|
||||
private final int priority;
|
||||
private final int hashCode;
|
||||
|
||||
//// Stability
|
||||
public static final CADataType CP_X = new CADataType(trans.get("componentanalysisdlg.TabStability.Col.CP"),
|
||||
"CP", UnitGroup.UNITS_NONE, CADataTypeGroup.STABILITY, 10);
|
||||
public static final CADataType CNa = new CADataType("<html>C<sub>N<sub>" + ALPHA + "</sub></sub>",
|
||||
"CNa", UnitGroup.UNITS_NONE, CADataTypeGroup.STABILITY, 11);
|
||||
|
||||
//// Drag
|
||||
public static final CADataType PRESSURE_CD = new CADataType(trans.get("componentanalysisdlg.dragTableModel.Col.Pressure"),
|
||||
"CD,pressure", UnitGroup.UNITS_NONE, CADataTypeGroup.DRAG, 20);
|
||||
public static final CADataType BASE_CD = new CADataType(trans.get("componentanalysisdlg.dragTableModel.Col.Base"),
|
||||
"CD,base", UnitGroup.UNITS_NONE, CADataTypeGroup.DRAG, 21);
|
||||
public static final CADataType FRICTION_CD = new CADataType(trans.get("componentanalysisdlg.dragTableModel.Col.friction"),
|
||||
"CD,friction", UnitGroup.UNITS_NONE, CADataTypeGroup.DRAG, 22);
|
||||
public static final CADataType TOTAL_CD = new CADataType(trans.get("componentanalysisdlg.dragTableModel.Col.total"),
|
||||
"CD,total", UnitGroup.UNITS_NONE, CADataTypeGroup.DRAG, 23);
|
||||
|
||||
//// Roll
|
||||
public static final CADataType ROLL_FORCING_COEFFICIENT = new CADataType(trans.get("componentanalysisdlg.rollTableModel.Col.rollforc"),
|
||||
"Clf", UnitGroup.UNITS_NONE, CADataTypeGroup.ROLL, 30);
|
||||
public static final CADataType ROLL_DAMPING_COEFFICIENT = new CADataType(trans.get("componentanalysisdlg.rollTableModel.Col.rolldamp"),
|
||||
"Cld", UnitGroup.UNITS_NONE, CADataTypeGroup.ROLL, 31);
|
||||
public static final CADataType TOTAL_ROLL_COEFFICIENT = new CADataType(trans.get("componentanalysisdlg.rollTableModel.Col.total"),
|
||||
"Cl,tot", UnitGroup.UNITS_NONE, CADataTypeGroup.ROLL, 32);
|
||||
|
||||
|
||||
public static final CADataType[] ALL_TYPES = {
|
||||
CP_X, CNa,
|
||||
PRESSURE_CD, BASE_CD, FRICTION_CD, TOTAL_CD,
|
||||
ROLL_FORCING_COEFFICIENT, ROLL_DAMPING_COEFFICIENT, TOTAL_ROLL_COEFFICIENT
|
||||
};
|
||||
|
||||
protected CADataType(String typeName, String symbol, UnitGroup units, CADataTypeGroup group, int priority) {
|
||||
if (typeName == null)
|
||||
throw new IllegalArgumentException("typeName is null");
|
||||
if (units == null)
|
||||
throw new IllegalArgumentException("units is null");
|
||||
this.name = typeName;
|
||||
this.symbol = symbol;
|
||||
this.units = units;
|
||||
this.group = group;
|
||||
this.priority = priority;
|
||||
this.hashCode = this.name.toLowerCase(Locale.ENGLISH).hashCode();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSymbol() {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnitGroup getUnitGroup() {
|
||||
return units;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CADataTypeGroup getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate all components that are relevant for a given CADataType.
|
||||
* @param rocket the rocket to search in
|
||||
* @param type the CADataType to search for
|
||||
* @return a list of all components that are relevant for the given CADataType
|
||||
*/
|
||||
public static List<RocketComponent> calculateComponentsForType(Rocket rocket, CADataType type) {
|
||||
List<RocketComponent> components = new ArrayList<>();
|
||||
|
||||
// Iterate through all components in the rocket
|
||||
for (RocketComponent component : rocket.getSelectedConfiguration().getAllComponents()) {
|
||||
// Check if this component is relevant for the given CADataType
|
||||
if (isComponentRelevantForType(component, type)) {
|
||||
components.add(component);
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a component is relevant for a given CADataType.
|
||||
* @param component the component to check
|
||||
* @param type the CADataType to check
|
||||
* @return true if the component is relevant for the given CADataType, false otherwise
|
||||
*/
|
||||
public static boolean isComponentRelevantForType(RocketComponent component, CADataType type) {
|
||||
// Only aerodynamic and rockets are relevant for any CADataType
|
||||
if (!component.isAerodynamic() && !(component instanceof Rocket)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type.equals(CADataType.CP_X) || type.equals(CADataType.CNa) ||
|
||||
type.equals(CADataType.PRESSURE_CD) || type.equals(CADataType.BASE_CD) ||
|
||||
type.equals(CADataType.FRICTION_CD) || type.equals(CADataType.TOTAL_CD)) {
|
||||
return true;
|
||||
} else if (type.equals(CADataType.ROLL_FORCING_COEFFICIENT) || type.equals(CADataType.ROLL_DAMPING_COEFFICIENT) ||
|
||||
type.equals(CADataType.TOTAL_ROLL_COEFFICIENT)) {
|
||||
return component instanceof FinSet;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name; // +" ("+symbol+") "+units.getDefaultUnit().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof CADataType))
|
||||
return false;
|
||||
return this.name.compareToIgnoreCase(((CADataType)o).name) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CADataType o) {
|
||||
final int groupCompare = this.getGroup().compareTo(o.getGroup());
|
||||
if (groupCompare != 0) {
|
||||
return groupCompare;
|
||||
}
|
||||
|
||||
return this.priority - o.priority;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.util.Group;
|
||||
|
||||
public class CADataTypeGroup implements Comparable<CADataTypeGroup>, Group {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
public static final CADataTypeGroup DOMAIN_PARAMETER = new CADataTypeGroup(trans.get("CADataTypeGroup.DOMAIN"), 0);
|
||||
public static final CADataTypeGroup DRAG = new CADataTypeGroup(trans.get("CADataTypeGroup.DRAG"), 10);
|
||||
public static final CADataTypeGroup STABILITY = new CADataTypeGroup(trans.get("CADataTypeGroup.STABILITY"), 20);
|
||||
public static final CADataTypeGroup ROLL = new CADataTypeGroup(trans.get("CADataTypeGroup.ROLL"), 30);
|
||||
|
||||
private final String name;
|
||||
private final int priority;
|
||||
|
||||
private CADataTypeGroup(String groupName, int priority) {
|
||||
this.name = groupName;
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof CADataTypeGroup))
|
||||
return false;
|
||||
return this.compareTo((CADataTypeGroup) o) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CADataTypeGroup o) {
|
||||
return this.priority - o.priority;
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.unit.UnitGroup;
|
||||
|
||||
public class CADomainDataType extends CADataType {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
private double min;
|
||||
private double max;
|
||||
private double delta;
|
||||
private final double minDelta;
|
||||
|
||||
public static final CADomainDataType MACH = new CADomainDataType(trans.get("componentanalysisdlg.lbl.machnumber"),
|
||||
"M", UnitGroup.UNITS_NONE, CADataTypeGroup.DOMAIN_PARAMETER, 0.0, 3.0, 0.1, 0.001, 0);
|
||||
|
||||
public static final CADomainDataType[] ALL_DOMAIN_TYPES = {
|
||||
MACH
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param typeName the name of the data type
|
||||
* @param symbol the (mathematical) symbol of the data type
|
||||
* @param units the unit group of the data type
|
||||
* @param group the group of the data type
|
||||
* @param min the minimum value of the data type
|
||||
* @param max the maximum value of the data type
|
||||
* @param delta the step size of the data type
|
||||
* @param minDelta the minimum step size of the data type
|
||||
* @param priority the priority of the data type
|
||||
*/
|
||||
private CADomainDataType(String typeName, String symbol, UnitGroup units, CADataTypeGroup group,
|
||||
double min, double max, double delta, double minDelta, int priority) {
|
||||
super(typeName, symbol, units, group, priority);
|
||||
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.delta = delta;
|
||||
this.minDelta = minDelta;
|
||||
}
|
||||
|
||||
public double getMin() {
|
||||
return min;
|
||||
}
|
||||
|
||||
public void setMin(double min) {
|
||||
this.min = min;
|
||||
}
|
||||
|
||||
public double getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
public void setMax(double max) {
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public double getDelta() {
|
||||
return delta;
|
||||
}
|
||||
|
||||
public void setDelta(double delta) {
|
||||
this.delta = delta;
|
||||
}
|
||||
|
||||
public double getMinDelta() {
|
||||
return minDelta;
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.unit.Unit;
|
||||
import info.openrocket.swing.gui.components.CsvOptionPanel;
|
||||
import info.openrocket.swing.gui.util.FileHelper;
|
||||
import info.openrocket.swing.gui.util.SwingPreferences;
|
||||
import info.openrocket.swing.gui.widgets.CSVExportPanel;
|
||||
import info.openrocket.swing.gui.widgets.SaveFileChooser;
|
||||
|
||||
import javax.swing.JFileChooser;
|
||||
import java.awt.Component;
|
||||
import java.io.File;
|
||||
|
||||
public class CAExportPanel extends CSVExportPanel<CADataType> {
|
||||
private static final long serialVersionUID = 4423905472892675964L;
|
||||
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
private static final int OPTION_FIELD_DESCRIPTIONS = 0;
|
||||
|
||||
private CAExportPanel(CADataType[] types,
|
||||
boolean[] selected, CsvOptionPanel csvOptions, Component... extraComponents) {
|
||||
super(types, selected, csvOptions, extraComponents);
|
||||
}
|
||||
|
||||
public static CAExportPanel create(CADataType[] types) {
|
||||
boolean[] selected = new boolean[types.length];
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
selected[i] = ((SwingPreferences) Application.getPreferences()).isComponentAnalysisDataTypeExportSelected(types[i]);
|
||||
}
|
||||
|
||||
CsvOptionPanel csvOptions = new CsvOptionPanel(CAExportPanel.class,
|
||||
trans.get("SimExpPan.checkbox.Includefielddesc"),
|
||||
trans.get("SimExpPan.checkbox.ttip.Includefielddesc"));
|
||||
|
||||
return new CAExportPanel(types, selected, csvOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doExport() {
|
||||
JFileChooser chooser = new SaveFileChooser();
|
||||
chooser.setFileFilter(FileHelper.CSV_FILTER);
|
||||
chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory());
|
||||
|
||||
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION)
|
||||
return false;
|
||||
|
||||
File file = chooser.getSelectedFile();
|
||||
if (file == null)
|
||||
return false;
|
||||
|
||||
file = FileHelper.forceExtension(file, "csv");
|
||||
if (!FileHelper.confirmWrite(file, this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
String commentChar = csvOptions.getCommentCharacter();
|
||||
String fieldSep = csvOptions.getFieldSeparator();
|
||||
int decimalPlaces = csvOptions.getDecimalPlaces();
|
||||
boolean isExponentialNotation = csvOptions.isExponentialNotation();
|
||||
boolean fieldComment = csvOptions.getSelectionOption(OPTION_FIELD_DESCRIPTIONS);
|
||||
csvOptions.storePreferences();
|
||||
|
||||
// Store preferences and export
|
||||
int n = 0;
|
||||
((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory());
|
||||
for (int i = 0; i < selected.length; i++) {
|
||||
((SwingPreferences) Application.getPreferences()).setComponentAnalysisExportSelected(types[i], selected[i]);
|
||||
if (selected[i])
|
||||
n++;
|
||||
}
|
||||
|
||||
|
||||
CADataType[] fieldTypes = new CADataType[n];
|
||||
Unit[] fieldUnits = new Unit[n];
|
||||
int pos = 0;
|
||||
for (int i = 0; i < selected.length; i++) {
|
||||
if (selected[i]) {
|
||||
fieldTypes[pos] = types[i];
|
||||
fieldUnits[pos] = units[i];
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldSep.equals(SPACE)) {
|
||||
fieldSep = " ";
|
||||
} else if (fieldSep.equals(TAB)) {
|
||||
fieldSep = "\t";
|
||||
}
|
||||
|
||||
|
||||
/*SaveCSVWorker.export(file, simulation, branch, fieldTypes, fieldUnits, fieldSep, decimalPlaces,
|
||||
isExponentialNotation, commentChar, simulationComment, fieldComment, eventComment,
|
||||
SwingUtilities.getWindowAncestor(this));*/
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.aerodynamics.AerodynamicCalculator;
|
||||
import info.openrocket.core.aerodynamics.AerodynamicForces;
|
||||
import info.openrocket.core.aerodynamics.FlightConditions;
|
||||
import info.openrocket.core.logging.WarningSet;
|
||||
import info.openrocket.core.rocketcomponent.FinSet;
|
||||
import info.openrocket.core.rocketcomponent.Rocket;
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CAParameterSweep {
|
||||
private final CAParameters parameters;
|
||||
private final AerodynamicCalculator aerodynamicCalculator;
|
||||
private final Rocket rocket;
|
||||
|
||||
public CAParameterSweep(CAParameters parameters, AerodynamicCalculator aerodynamicCalculator, Rocket rocket) {
|
||||
this.parameters = parameters.clone();
|
||||
this.aerodynamicCalculator = aerodynamicCalculator;
|
||||
this.rocket = rocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a parameter sweep over the specified parameter type.
|
||||
* @param sweepParameter the parameter to sweep (e.g. MACH)
|
||||
* @param min the minimum value of the parameter
|
||||
* @param max the maximum value of the parameter
|
||||
* @param delta the step size of the parameter
|
||||
* @return a data branch containing the results of the sweep
|
||||
*/
|
||||
public CADataBranch sweep(CADomainDataType sweepParameter, double min, double max, double delta, double initialValue) {
|
||||
List<Double> sweepValues = generateSweepValues(min, max, delta);
|
||||
CADataBranch dataBranch = new CADataBranch("Parameter Sweep");
|
||||
dataBranch.addType(sweepParameter);
|
||||
|
||||
for (Double value : sweepValues) {
|
||||
setParameterValue(sweepParameter, value);
|
||||
FlightConditions conditions = createFlightConditions();
|
||||
Map<RocketComponent, AerodynamicForces> aeroData = aerodynamicCalculator.getForceAnalysis(rocket.getSelectedConfiguration(), conditions, new WarningSet());
|
||||
|
||||
dataBranch.addPoint();
|
||||
addDomainData(dataBranch, sweepParameter, value);
|
||||
|
||||
addComponentData(dataBranch, aeroData, value);
|
||||
addStabilityData(dataBranch, aeroData, value);
|
||||
addDragData(dataBranch, aeroData, value);
|
||||
addRollData(dataBranch, aeroData, value);
|
||||
}
|
||||
|
||||
// Reset the parameter to its original value
|
||||
setParameterValue(sweepParameter, initialValue);
|
||||
|
||||
return dataBranch;
|
||||
}
|
||||
|
||||
private List<Double> generateSweepValues(double min, double max, double delta) {
|
||||
List<Double> values = new ArrayList<>();
|
||||
int scale = determineScale(delta);
|
||||
double multiplier = Math.pow(10, scale);
|
||||
|
||||
for (double value = min; value <= max; value += delta) {
|
||||
double roundedValue = Math.round(value * multiplier) / multiplier;
|
||||
values.add(roundedValue);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private int determineScale(double delta) {
|
||||
String deltaStr = Double.toString(Math.abs(delta));
|
||||
int indexOfDecimal = deltaStr.indexOf(".");
|
||||
if (indexOfDecimal == -1) {
|
||||
return 0;
|
||||
}
|
||||
return deltaStr.length() - indexOfDecimal - 1;
|
||||
}
|
||||
|
||||
private void setParameterValue(CADomainDataType parameterType, double value) {
|
||||
if (parameterType.equals(CADomainDataType.MACH)) {
|
||||
parameters.setMach(value);
|
||||
}
|
||||
// Add more cases here as more parameter types are implemented
|
||||
else {
|
||||
throw new IllegalArgumentException("Unsupported parameter type: " + parameterType);
|
||||
}
|
||||
}
|
||||
|
||||
private FlightConditions createFlightConditions() {
|
||||
FlightConditions conditions = new FlightConditions(rocket.getSelectedConfiguration());
|
||||
conditions.setAOA(parameters.getAOA());
|
||||
conditions.setTheta(parameters.getTheta());
|
||||
conditions.setMach(parameters.getMach());
|
||||
conditions.setRollRate(parameters.getRollRate());
|
||||
return conditions;
|
||||
}
|
||||
|
||||
private static void addDomainData(CADataBranch dataBranch, CADomainDataType sweepParameter, Double value) {
|
||||
dataBranch.setDomainValue(sweepParameter, value);
|
||||
}
|
||||
|
||||
private void addComponentData(CADataBranch dataBranch, Map<RocketComponent, AerodynamicForces> aeroData, double sweepValue) {
|
||||
for (Map.Entry<RocketComponent, AerodynamicForces> entry : aeroData.entrySet()) {
|
||||
RocketComponent component = entry.getKey();
|
||||
AerodynamicForces forces = entry.getValue();
|
||||
|
||||
if (forces == null) {
|
||||
dataBranch.setValue(CADataType.CP_X, component, 0.0);
|
||||
dataBranch.setValue(CADataType.CNa, component, 0.0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (forces.getCP() != null) {
|
||||
double cpx = (component instanceof Rocket && forces.getCP().weight < MathUtil.EPSILON) ?
|
||||
Double.NaN : forces.getCP().x;
|
||||
dataBranch.setValue(CADataType.CP_X, component, cpx);
|
||||
dataBranch.setValue(CADataType.CNa, component, forces.getCNa());
|
||||
}
|
||||
|
||||
if (!Double.isNaN(forces.getCD())) {
|
||||
dataBranch.setValue(CADataType.PRESSURE_CD, component, forces.getPressureCD());
|
||||
dataBranch.setValue(CADataType.BASE_CD, component, forces.getBaseCD());
|
||||
dataBranch.setValue(CADataType.FRICTION_CD, component, forces.getFrictionCD());
|
||||
dataBranch.setValue(CADataType.TOTAL_CD, component, forces.getCD());
|
||||
}
|
||||
|
||||
if (component instanceof FinSet) {
|
||||
dataBranch.setValue(CADataType.ROLL_FORCING_COEFFICIENT, component, forces.getCrollForce());
|
||||
dataBranch.setValue(CADataType.ROLL_DAMPING_COEFFICIENT, component, forces.getCrollDamp());
|
||||
dataBranch.setValue(CADataType.TOTAL_ROLL_COEFFICIENT, component, forces.getCrollForce() + forces.getCrollDamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addStabilityData(CADataBranch dataBranch, Map<RocketComponent, AerodynamicForces> aeroData, double sweepValue) {
|
||||
AerodynamicForces totalForces = aeroData.get(rocket);
|
||||
if (totalForces != null && totalForces.getCP() != null) {
|
||||
dataBranch.setValue(CADataType.CP_X, rocket, totalForces.getCP().x);
|
||||
dataBranch.setValue(CADataType.CNa, rocket, totalForces.getCNa());
|
||||
}
|
||||
}
|
||||
|
||||
private void addDragData(CADataBranch dataBranch, Map<RocketComponent, AerodynamicForces> aeroData, double sweepValue) {
|
||||
AerodynamicForces totalForces = aeroData.get(rocket);
|
||||
if (totalForces != null) {
|
||||
dataBranch.setValue(CADataType.PRESSURE_CD, rocket, totalForces.getPressureCD());
|
||||
dataBranch.setValue(CADataType.BASE_CD, rocket, totalForces.getBaseCD());
|
||||
dataBranch.setValue(CADataType.FRICTION_CD, rocket, totalForces.getFrictionCD());
|
||||
dataBranch.setValue(CADataType.TOTAL_CD, rocket, totalForces.getCD());
|
||||
}
|
||||
}
|
||||
|
||||
private void addRollData(CADataBranch dataBranch, Map<RocketComponent, AerodynamicForces> aeroData, double sweepValue) {
|
||||
double totalRollForce = 0;
|
||||
double totalRollDamping = 0;
|
||||
|
||||
for (Map.Entry<RocketComponent, AerodynamicForces> entry : aeroData.entrySet()) {
|
||||
if (entry.getKey() instanceof FinSet) {
|
||||
AerodynamicForces forces = entry.getValue();
|
||||
totalRollForce += forces.getCrollForce();
|
||||
totalRollDamping += forces.getCrollDamp();
|
||||
}
|
||||
}
|
||||
|
||||
dataBranch.setValue(CADataType.ROLL_FORCING_COEFFICIENT, rocket, totalRollForce);
|
||||
dataBranch.setValue(CADataType.ROLL_DAMPING_COEFFICIENT, rocket, totalRollDamping);
|
||||
dataBranch.setValue(CADataType.TOTAL_ROLL_COEFFICIENT, rocket, totalRollForce + totalRollDamping);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.util.Mutable;
|
||||
import info.openrocket.swing.gui.scalefigure.RocketPanel;
|
||||
|
||||
public class ComponentAnalysisParameters implements Cloneable {
|
||||
public class CAParameters implements Cloneable {
|
||||
private final Mutable mutable = new Mutable();
|
||||
|
||||
private final Rocket rocket;
|
||||
@ -18,7 +18,7 @@ public class ComponentAnalysisParameters implements Cloneable {
|
||||
private double mach;
|
||||
private double rollRate;
|
||||
|
||||
public ComponentAnalysisParameters(Rocket rocket, RocketPanel rocketPanel) {
|
||||
public CAParameters(Rocket rocket, RocketPanel rocketPanel) {
|
||||
this.rocket = rocket;
|
||||
this.rocketPanel = rocketPanel;
|
||||
|
||||
@ -86,9 +86,9 @@ public class ComponentAnalysisParameters implements Cloneable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentAnalysisParameters clone() {
|
||||
public CAParameters clone() {
|
||||
try {
|
||||
return (ComponentAnalysisParameters) super.clone();
|
||||
return (CAParameters) super.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.unit.Unit;
|
||||
import info.openrocket.swing.gui.plot.Plot;
|
||||
import org.jfree.data.xy.XYSeries;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CAPlot extends Plot<CADataType, CADataBranch, CAPlotConfiguration> {
|
||||
public CAPlot(String name, CADataBranch mainBranch, CAPlotConfiguration config,
|
||||
List<CADataBranch> allBranches, boolean initialShowPoints) {
|
||||
super(name, mainBranch, config, allBranches, initialShowPoints);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<XYSeries> createSeriesForType(int dataIndex, int startIndex, CADataType type, Unit unit,
|
||||
CADataBranch branch, String baseName) {
|
||||
// Default implementation for regular DataBranch
|
||||
MetadataXYSeries series = new MetadataXYSeries(startIndex, false, true, unit.getUnit());
|
||||
|
||||
// Get the component name
|
||||
String componentName = filledConfig.getComponentName(dataIndex);
|
||||
|
||||
// Create a new description that includes the component name
|
||||
String description = baseName;
|
||||
if (!componentName.isEmpty()) {
|
||||
description += " (" + componentName + ")";
|
||||
}
|
||||
|
||||
series.setDescription(description);
|
||||
|
||||
List<Double> plotx = branch.get(filledConfig.getDomainAxisType());
|
||||
List<Double> ploty = branch.get(type, filledConfig.getComponent(dataIndex));
|
||||
|
||||
int pointCount = plotx.size();
|
||||
for (int j = 0; j < pointCount; j++) {
|
||||
double x = filledConfig.getDomainAxisUnit().toUnit(plotx.get(j));
|
||||
double y = unit.toUnit(ploty.get(j));
|
||||
series.add(x, y);
|
||||
}
|
||||
|
||||
return Collections.singletonList(series);
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.unit.Unit;
|
||||
import info.openrocket.core.util.ArrayList;
|
||||
import info.openrocket.swing.gui.plot.Axis;
|
||||
import info.openrocket.swing.gui.plot.PlotConfiguration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class CAPlotConfiguration extends PlotConfiguration<CADataType, CADataBranch> {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
private ArrayList<RocketComponent> plotDataComponents = new ArrayList<>();
|
||||
|
||||
public static final CAPlotConfiguration[] DEFAULT_CONFIGURATIONS;
|
||||
static {
|
||||
ArrayList<CAPlotConfiguration> configs = new ArrayList<>();
|
||||
CAPlotConfiguration config;
|
||||
|
||||
//// Total CD vs Mach
|
||||
config = new CAPlotConfiguration(trans.get("CAPlotConfiguration.TotalCD"),
|
||||
CADomainDataType.MACH);
|
||||
config.addPlotDataType(CADataType.TOTAL_CD, 0);
|
||||
configs.add(config);
|
||||
|
||||
DEFAULT_CONFIGURATIONS = configs.toArray(new CAPlotConfiguration[0]);
|
||||
}
|
||||
|
||||
public CAPlotConfiguration(String name, CADomainDataType domainType) {
|
||||
super(name, domainType);
|
||||
}
|
||||
|
||||
public CAPlotConfiguration(String name) {
|
||||
super(name, CADomainDataType.MACH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPlotDataType(CADataType type, int axis) {
|
||||
super.addPlotDataType(type, axis);
|
||||
plotDataComponents.add(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPlotDataType(CADataType type) {
|
||||
super.addPlotDataType(type);
|
||||
plotDataComponents.add(null);
|
||||
}
|
||||
|
||||
public RocketComponent getComponent(int index) {
|
||||
return plotDataComponents.get(index);
|
||||
}
|
||||
|
||||
public void setPlotDataComponent(int index, RocketComponent component) {
|
||||
plotDataComponents.set(index, component);
|
||||
}
|
||||
|
||||
public String getComponentName(int dataIndex) {
|
||||
RocketComponent component = getComponent(dataIndex);
|
||||
return component != null ? component.getName() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void calculateAxisBounds(List<CADataBranch> data) {
|
||||
int length = plotDataTypes.size();
|
||||
for (int i = 0; i < length; i++) {
|
||||
CADataType type = plotDataTypes.get(i);
|
||||
Unit unit = plotDataUnits.get(i);
|
||||
int index = plotDataAxes.get(i);
|
||||
if (index < 0) {
|
||||
throw new IllegalStateException("fitAxes called with auto-selected axis");
|
||||
}
|
||||
Axis axis = allAxes.get(index);
|
||||
|
||||
double min = unit.toUnit(data.get(0).getMinimum(type, getComponent(i)));
|
||||
double max = unit.toUnit(data.get(0).getMaximum(type, getComponent(i)));
|
||||
|
||||
for (int j = 1; j < data.size(); j++) {
|
||||
// Ignore empty data
|
||||
if (data.get(j).getLength() == 0) {
|
||||
continue;
|
||||
}
|
||||
min = Math.min(min, unit.toUnit(data.get(j).getMinimum(type)));
|
||||
max = Math.max(max, unit.toUnit(data.get(j).getMaximum(type)));
|
||||
}
|
||||
|
||||
axis.addBound(min);
|
||||
axis.addBound(max);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public CAPlotConfiguration cloneConfiguration() {
|
||||
CAPlotConfiguration clone = super.cloneConfiguration();
|
||||
clone.plotDataComponents = this.plotDataComponents.clone();
|
||||
return clone;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.preferences.ApplicationPreferences;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.swing.gui.plot.PlotDialog;
|
||||
|
||||
import java.awt.Window;
|
||||
import java.util.List;
|
||||
|
||||
public class CAPlotDialog extends PlotDialog<CADataType, CADataBranch, CAPlotConfiguration, CAPlot> {
|
||||
private CAPlotDialog(Window parent, String name, CAPlot plot, CAPlotConfiguration config, List<CADataBranch> allBranches, boolean initialShowPoints) {
|
||||
super(parent, name, plot, config, allBranches, initialShowPoints);
|
||||
}
|
||||
|
||||
public static CAPlotDialog create(Window parent, String name, CAPlotConfiguration config, List<CADataBranch> allBranches) {
|
||||
final boolean initialShowPoints = Application.getPreferences().getBoolean(ApplicationPreferences.PLOT_SHOW_POINTS, false);
|
||||
final CAPlot plot = new CAPlot(name, allBranches.get(0), config, allBranches, initialShowPoints);
|
||||
|
||||
return new CAPlotDialog(parent, name, plot, config, allBranches, initialShowPoints);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.unit.Unit;
|
||||
import info.openrocket.swing.gui.plot.PlotPanel;
|
||||
|
||||
import javax.swing.JDialog;
|
||||
import java.awt.Component;
|
||||
import java.awt.Window;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CAPlotPanel extends PlotPanel<CADataType, CADataBranch, CADataTypeGroup,
|
||||
CAPlotConfiguration, CAPlotTypeSelector> {
|
||||
/** The "Custom" configuration - not to be used for anything other than the title. */
|
||||
private static final CAPlotConfiguration CUSTOM_CONFIGURATION;
|
||||
|
||||
/** The array of presets for the combo box. */
|
||||
private static final CAPlotConfiguration[] PRESET_ARRAY;
|
||||
|
||||
|
||||
/** The current default configuration, set each time a plot is made. */
|
||||
private static CAPlotConfiguration DEFAULT_CONFIGURATION =
|
||||
CAPlotConfiguration.DEFAULT_CONFIGURATIONS[0].resetUnits();
|
||||
|
||||
private final ComponentAnalysisPlotExportDialog parent;
|
||||
|
||||
static {
|
||||
CUSTOM_CONFIGURATION = new CAPlotConfiguration(CUSTOM);
|
||||
|
||||
PRESET_ARRAY = Arrays.copyOf(CAPlotConfiguration.DEFAULT_CONFIGURATIONS,
|
||||
CAPlotConfiguration.DEFAULT_CONFIGURATIONS.length + 1);
|
||||
PRESET_ARRAY[PRESET_ARRAY.length - 1] = CUSTOM_CONFIGURATION;
|
||||
}
|
||||
|
||||
private CAPlotPanel(ComponentAnalysisPlotExportDialog parent,
|
||||
CADomainDataType[] typesX, CADataType[] typesY) {
|
||||
super(typesX, typesY, CUSTOM_CONFIGURATION, PRESET_ARRAY, DEFAULT_CONFIGURATION, null, null);
|
||||
|
||||
this.parent = parent;
|
||||
updatePlots();
|
||||
}
|
||||
|
||||
public static CAPlotPanel create(ComponentAnalysisPlotExportDialog parent) {
|
||||
CADomainDataType[] typesX = new CADomainDataType[] { parent.getSelectedParameter() };
|
||||
|
||||
return new CAPlotPanel(parent, typesX, CADataType.ALL_TYPES);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addXAxisSelector(CADataType[] typesX, Component[] extraWidgetsX) {
|
||||
// Don't add the X axis selector (this is added in the ComponentAnalysisPlotExportDialog)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addSelectionListeners(CAPlotTypeSelector selector, final int idx) {
|
||||
super.addSelectionListeners(selector, idx);
|
||||
|
||||
selector.addComponentSelectionListener(e -> {
|
||||
if (modifying > 0) return;
|
||||
RocketComponent component = selector.getSelectedComponent();
|
||||
configuration.setPlotDataComponent(idx, component);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CAPlotTypeSelector createSelector(int i, CADataType type, Unit unit, int axis) {
|
||||
return new CAPlotTypeSelector(parent, i, type, unit, axis, List.of(CADataType.ALL_TYPES),
|
||||
parent.getComponentsForType(type), configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setDefaultConfiguration(CAPlotConfiguration newConfiguration) {
|
||||
super.setDefaultConfiguration(newConfiguration);
|
||||
DEFAULT_CONFIGURATION = newConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JDialog doPlot(Window parentWindow) {
|
||||
CADataBranch branch = this.parent.runParameterSweep();
|
||||
CAPlotConfiguration config = this.getConfiguration();
|
||||
return CAPlotDialog.create(parent, trans.get("CAPlotPanel.lbl.PlotTitle"), config, Collections.singletonList(branch));
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.unit.Unit;
|
||||
import info.openrocket.swing.gui.plot.PlotTypeSelector;
|
||||
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JLabel;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.util.List;
|
||||
|
||||
public class CAPlotTypeSelector extends PlotTypeSelector<CADataType, CADataTypeGroup> {
|
||||
private final JComboBox<RocketComponent> componentSelector;
|
||||
|
||||
public CAPlotTypeSelector(final ComponentAnalysisPlotExportDialog parent, int plotIndex,
|
||||
CADataType type, Unit unit, int position, List<CADataType> availableTypes,
|
||||
List<RocketComponent> componentsForType, CAPlotConfiguration configuration) {
|
||||
super(plotIndex, type, unit, position, availableTypes, false);
|
||||
|
||||
if (componentsForType.isEmpty()) {
|
||||
throw new IllegalArgumentException("No components for type " + type);
|
||||
}
|
||||
|
||||
// Component selector
|
||||
this.add(new JLabel(trans.get("CAPlotTypeSelector.lbl.component")));
|
||||
componentSelector = new JComboBox<>(componentsForType.toArray(new RocketComponent[0]));
|
||||
configuration.setPlotDataComponent(plotIndex, componentsForType.get(0));
|
||||
this.add(componentSelector, "gapright para");
|
||||
|
||||
addRemoveButton();
|
||||
|
||||
typeSelector.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
CADataType type = (CADataType) typeSelector.getSelectedItem();
|
||||
List<RocketComponent> componentsForType = parent.getComponentsForType(type);
|
||||
componentSelector.removeAllItems();
|
||||
for (RocketComponent component : componentsForType) {
|
||||
componentSelector.addItem(component);
|
||||
}
|
||||
componentSelector.setSelectedIndex(0);
|
||||
configuration.setPlotDataComponent(plotIndex, (RocketComponent) componentSelector.getSelectedItem());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void addComponentSelectionListener(ItemListener listener) {
|
||||
componentSelector.addItemListener(listener);
|
||||
}
|
||||
|
||||
public RocketComponent getSelectedComponent() {
|
||||
return (RocketComponent) componentSelector.getSelectedItem();
|
||||
}
|
||||
}
|
@ -87,7 +87,7 @@ public class ComponentAnalysisDialog extends JDialog implements StateChangeListe
|
||||
private final JToggleButton worstToggle;
|
||||
private boolean fakeChange = false;
|
||||
private AerodynamicCalculator aerodynamicCalculator;
|
||||
private ComponentAnalysisParameters parameters;
|
||||
private CAParameters parameters;
|
||||
|
||||
private final ColumnTableModel longitudeStabilityTableModel;
|
||||
private final ColumnTableModel dragTableModel;
|
||||
@ -116,8 +116,8 @@ public class ComponentAnalysisDialog extends JDialog implements StateChangeListe
|
||||
|
||||
conditions = new FlightConditions(rkt.getSelectedConfiguration());
|
||||
|
||||
// Create ComponentAnalysisParameters
|
||||
parameters = new ComponentAnalysisParameters(rkt, rocketPanel);
|
||||
// Create CAParameters
|
||||
parameters = new CAParameters(rkt, rocketPanel);
|
||||
|
||||
aoa = new DoubleModel(parameters, "AOA", UnitGroup.UNITS_ANGLE, 0, Math.PI);
|
||||
mach = new DoubleModel(parameters, "Mach", UnitGroup.UNITS_COEFFICIENT, 0);
|
||||
@ -470,9 +470,12 @@ public class ComponentAnalysisDialog extends JDialog implements StateChangeListe
|
||||
plotExportBtn.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
// TODO: open plot/export dialog
|
||||
ComponentAnalysisPlotExportDialog dialog = new ComponentAnalysisPlotExportDialog(ComponentAnalysisDialog.this,
|
||||
parameters, aerodynamicCalculator, rkt);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
});
|
||||
panel.add(plotExportBtn, "tag plotexport");
|
||||
|
||||
// TODO: LOW: printing
|
||||
// button = new JButton("Print");
|
||||
|
@ -0,0 +1,270 @@
|
||||
package info.openrocket.swing.gui.dialogs.componentanalysis;
|
||||
|
||||
import info.openrocket.core.aerodynamics.AerodynamicCalculator;
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.rocketcomponent.ComponentAssembly;
|
||||
import info.openrocket.core.rocketcomponent.FinSet;
|
||||
import info.openrocket.core.rocketcomponent.Rocket;
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.swing.gui.adaptors.DoubleModel;
|
||||
import info.openrocket.swing.gui.components.EditableSpinner;
|
||||
import info.openrocket.swing.gui.util.GUIUtil;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTabbedPane;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ComponentAnalysisPlotExportDialog extends JDialog {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
private static final Logger log = LoggerFactory.getLogger(ComponentAnalysisPlotExportDialog.class);
|
||||
|
||||
private final Rocket rocket;
|
||||
|
||||
private final JTabbedPane tabbedPane;
|
||||
JComboBox<CADomainDataType> parameterSelector;
|
||||
private JButton okButton;
|
||||
|
||||
private static final int PLOT_IDX = 0;
|
||||
private static final int EXPORT_IDX = 1;
|
||||
|
||||
private final CAPlotPanel plotTab;
|
||||
private final CAExportPanel exportTab;
|
||||
|
||||
private DoubleModel minModel;
|
||||
private DoubleModel maxModel;
|
||||
private DoubleModel deltaModel;
|
||||
|
||||
private final CAParameters parameters;
|
||||
private final CAParameterSweep parameterSweep;
|
||||
|
||||
private final Map<CADataType, List<RocketComponent>> componentCache;
|
||||
private boolean isCacheValid;
|
||||
|
||||
public ComponentAnalysisPlotExportDialog(Window parent, CAParameters parameters,
|
||||
AerodynamicCalculator aerodynamicCalculator, Rocket rocket) {
|
||||
super(parent, trans.get("CAPlotExportDialog.title"), JDialog.ModalityType.DOCUMENT_MODAL);
|
||||
|
||||
final JPanel contentPanel = new JPanel(new MigLayout("fill, height 500px"));
|
||||
|
||||
this.rocket = rocket;
|
||||
this.parameters = parameters;
|
||||
this.parameterSweep = new CAParameterSweep(parameters, aerodynamicCalculator, rocket);
|
||||
this.componentCache = new HashMap<>();
|
||||
this.isCacheValid = false;
|
||||
|
||||
// ======== Top panel ========
|
||||
addTopPanel(contentPanel);
|
||||
|
||||
// ======== Tabbed pane ========
|
||||
this.tabbedPane = new JTabbedPane();
|
||||
|
||||
//// Plot data
|
||||
this.plotTab = CAPlotPanel.create(this);
|
||||
this.tabbedPane.addTab(trans.get("CAPlotExportDialog.tab.Plot"), this.plotTab);
|
||||
|
||||
//// Export data
|
||||
this.exportTab = CAExportPanel.create(CADataType.ALL_TYPES);
|
||||
this.tabbedPane.addTab(trans.get("CAPlotExportDialog.tab.Export"), this.exportTab);
|
||||
|
||||
contentPanel.add(tabbedPane, "grow, wrap");
|
||||
|
||||
// ======== Bottom panel ========
|
||||
addBottomPanel(contentPanel);
|
||||
|
||||
// Update the OK button text based on the selected tab
|
||||
tabbedPane.addChangeListener(new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
if (okButton == null) {
|
||||
return;
|
||||
}
|
||||
int selectedIndex = tabbedPane.getSelectedIndex();
|
||||
switch (selectedIndex) {
|
||||
case PLOT_IDX:
|
||||
okButton.setText(trans.get("SimulationConfigDialog.btn.plot"));
|
||||
break;
|
||||
case EXPORT_IDX:
|
||||
okButton.setText(trans.get("SimulationConfigDialog.btn.export"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.add(contentPanel);
|
||||
this.validate();
|
||||
this.pack();
|
||||
|
||||
// Add listeners for events that would invalidate the cache
|
||||
rocket.addComponentChangeListener(e -> invalidateCache());
|
||||
|
||||
this.setLocationByPlatform(true);
|
||||
GUIUtil.setDisposableDialogOptions(this, null);
|
||||
}
|
||||
|
||||
private void addTopPanel(JPanel contentPanel) {
|
||||
JPanel topPanel = new JPanel(new MigLayout("fill"));
|
||||
TitledBorder border = BorderFactory.createTitledBorder(trans.get("CAPlotExportDialog.XAxisConfiguration"));
|
||||
topPanel.setBorder(border);
|
||||
|
||||
// Domain parameter selector
|
||||
topPanel.add(new JLabel(trans.get("CAPlotExportDialog.lbl.XAxis")), "top, split 2");
|
||||
this.parameterSelector = new JComboBox<>(CADomainDataType.ALL_DOMAIN_TYPES);
|
||||
this.parameterSelector.setToolTipText(trans.get("CAPlotExportDialog.lbl.XAxis.ttip"));
|
||||
parameterSelector.setSelectedItem(CADomainDataType.MACH);
|
||||
topPanel.add(parameterSelector, "top, growx");
|
||||
|
||||
// Update the models
|
||||
updateModels(getSelectedParameter());
|
||||
|
||||
JPanel minMaxPanel = new JPanel(new MigLayout("fill, ins 0"));
|
||||
|
||||
// Min value
|
||||
minMaxPanel.add(new JLabel(trans.get("CAPlotExportDialog.lbl.MinValue")));
|
||||
final EditableSpinner minSpinner = new EditableSpinner(minModel.getSpinnerModel());
|
||||
minSpinner.setToolTipText(trans.get("CAPlotExportDialog.lbl.MinValue.ttip"));
|
||||
minMaxPanel.add(minSpinner, "growx, wrap");
|
||||
|
||||
// Max value
|
||||
minMaxPanel.add(new JLabel(trans.get("CAPlotExportDialog.lbl.MaxValue")));
|
||||
final EditableSpinner maxSpinner = new EditableSpinner(maxModel.getSpinnerModel());
|
||||
maxSpinner.setToolTipText(trans.get("CAPlotExportDialog.lbl.MaxValue.ttip"));
|
||||
minMaxPanel.add(maxSpinner, "growx, wrap");
|
||||
|
||||
topPanel.add(minMaxPanel, "growx");
|
||||
|
||||
// Step size
|
||||
topPanel.add(new JLabel(trans.get("CAPlotExportDialog.lbl.Delta")), "top, split 2");
|
||||
final EditableSpinner deltaSpinner = new EditableSpinner(deltaModel.getSpinnerModel());
|
||||
deltaSpinner.setToolTipText(trans.get("CAPlotExportDialog.lbl.Delta.ttip"));
|
||||
topPanel.add(deltaSpinner, "top, growx");
|
||||
|
||||
// Update the models and spinners when the parameter selector changes
|
||||
parameterSelector.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
updateModels(getSelectedParameter());
|
||||
minSpinner.setModel(minModel.getSpinnerModel());
|
||||
maxSpinner.setModel(maxModel.getSpinnerModel());
|
||||
deltaSpinner.setModel(deltaModel.getSpinnerModel());
|
||||
}
|
||||
});
|
||||
|
||||
contentPanel.add(topPanel, "growx, wrap");
|
||||
}
|
||||
|
||||
private void addBottomPanel(JPanel contentPanel) {
|
||||
JPanel bottomPanel = new JPanel(new MigLayout("fill, ins 0"));
|
||||
|
||||
// Close button
|
||||
JButton closeButton = new JButton(trans.get("dlg.but.close"));
|
||||
closeButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
ComponentAnalysisPlotExportDialog.this.dispose();
|
||||
}
|
||||
});
|
||||
bottomPanel.add(closeButton, "gapbefore push, split 2, right");
|
||||
|
||||
// OK button
|
||||
this.okButton = new JButton(trans.get("SimulationConfigDialog.btn.plot"));
|
||||
okButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (tabbedPane.getSelectedIndex() == PLOT_IDX) {
|
||||
JDialog dialog = ComponentAnalysisPlotExportDialog.this.plotTab.doPlot(ComponentAnalysisPlotExportDialog.this);
|
||||
if (dialog != null) {
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
} else if (tabbedPane.getSelectedIndex() == EXPORT_IDX) {
|
||||
ComponentAnalysisPlotExportDialog.this.exportTab.doExport();
|
||||
}
|
||||
}
|
||||
});
|
||||
bottomPanel.add(okButton, "wrap");
|
||||
|
||||
contentPanel.add(bottomPanel, "growx, wrap");
|
||||
}
|
||||
|
||||
public CADomainDataType getSelectedParameter() {
|
||||
return (CADomainDataType) parameterSelector.getSelectedItem();
|
||||
}
|
||||
|
||||
private void updateModels(CADomainDataType type) {
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("CADomainDataType cannot be null");
|
||||
}
|
||||
// TODO: use the maxModel for the max value of minModel and vice versa?
|
||||
this.minModel = new DoubleModel(type, "Min", 0);
|
||||
this.maxModel = new DoubleModel(type, "Max", 0);
|
||||
this.deltaModel = new DoubleModel(type, "Delta", type.getMinDelta());
|
||||
}
|
||||
|
||||
private void invalidateCache() {
|
||||
this.isCacheValid = false;
|
||||
this.componentCache.clear();
|
||||
}
|
||||
|
||||
public List<RocketComponent> getComponentsForType(CADataType type) {
|
||||
if (!isCacheValid || !componentCache.containsKey(type)) {
|
||||
updateCacheForType(type);
|
||||
}
|
||||
return new ArrayList<>(componentCache.get(type));
|
||||
}
|
||||
|
||||
private void updateCacheForType(CADataType type) {
|
||||
if (!isCacheValid) {
|
||||
componentCache.clear();
|
||||
}
|
||||
|
||||
if (!componentCache.containsKey(type)) {
|
||||
List<RocketComponent> components = CADataType.calculateComponentsForType(rocket, type);
|
||||
componentCache.put(type, components);
|
||||
}
|
||||
|
||||
isCacheValid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the parameter sweep and return the data branch.
|
||||
* @return the data branch containing the results of the parameter sweep
|
||||
*/
|
||||
public CADataBranch runParameterSweep() {
|
||||
double min = minModel.getValue();
|
||||
double max = maxModel.getValue();
|
||||
double delta = deltaModel.getValue();
|
||||
|
||||
CADomainDataType domainType = getSelectedParameter();
|
||||
CADataBranch dataBranch = parameterSweep.sweep(domainType, min, max, delta, getParameterValue(domainType));
|
||||
log.info("Parameter sweep completed. Data stored in dataBranch.");
|
||||
return dataBranch;
|
||||
}
|
||||
|
||||
private double getParameterValue(CADomainDataType parameterType) {
|
||||
if (parameterType.equals(CADomainDataType.MACH)) {
|
||||
return parameters.getMach();
|
||||
}
|
||||
// Add more cases here as more parameter types are implemented
|
||||
else {
|
||||
throw new IllegalArgumentException("Unsupported parameter type: " + parameterType);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -50,6 +50,7 @@ import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -87,7 +88,7 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
|
||||
/*tooltips*/true,
|
||||
/*urls*/false
|
||||
);
|
||||
this.chart.addSubtitle(new TextTitle(config.getName()));
|
||||
this.chart.addSubtitle(new TextTitle(Util.formatHTMLString(config.getName())));
|
||||
this.chart.getTitle().setFont(new Font("Dialog", Font.BOLD, 23));
|
||||
this.chart.setBackgroundPaint(new Color(240, 240, 240));
|
||||
this.legendItems = new LegendItems();
|
||||
@ -114,7 +115,7 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
|
||||
}
|
||||
|
||||
// Get plot length (ignore trailing NaN's)
|
||||
int typeCount = filledConfig.getTypeCount();
|
||||
int dataCount = filledConfig.getDataCount();
|
||||
|
||||
int seriesCount = 0;
|
||||
|
||||
@ -125,7 +126,7 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
|
||||
|
||||
// Create the XYSeries objects from the flight data and store into the collections
|
||||
String[] axisLabel = new String[2];
|
||||
for (int i = 0; i < typeCount; i++) {
|
||||
for (int i = 0; i < dataCount; i++) {
|
||||
// Get info
|
||||
T type = postProcessType(filledConfig.getType(i));
|
||||
Unit unit = filledConfig.getUnit(i);
|
||||
@ -133,54 +134,28 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
|
||||
String name = getLabel(type, unit);
|
||||
|
||||
// Populate data for each branch.
|
||||
|
||||
// The primary branch (branchIndex = 0) is easy since all the data is copied
|
||||
{
|
||||
int branchIndex = 0;
|
||||
B thisBranch = allBranches.get(branchIndex);
|
||||
// Store data in provided units
|
||||
List<Double> plotx = thisBranch.get(domainType);
|
||||
List<Double> ploty = thisBranch.get(type);
|
||||
XYSeries series = new XYSeries(seriesCount++, false, true);
|
||||
series.setDescription(name);
|
||||
int pointCount = plotx.size();
|
||||
for (int j = 0; j < pointCount; j++) {
|
||||
series.add(domainUnit.toUnit(plotx.get(j)), unit.toUnit(ploty.get(j)));
|
||||
}
|
||||
data[axis].addSeries(series);
|
||||
}
|
||||
// Secondary branches
|
||||
for (int branchIndex = 1; branchIndex < branchCount; branchIndex++) {
|
||||
for (int branchIndex = 0; branchIndex < branchCount; branchIndex++) {
|
||||
B thisBranch = allBranches.get(branchIndex);
|
||||
|
||||
// Ignore empty branches
|
||||
if (thisBranch.getLength() == 0) {
|
||||
// Add an empty series to keep the series count consistent
|
||||
XYSeries series = new XYSeries(seriesCount++, false, true);
|
||||
series.setDescription(thisBranch.getName() + ": " + name);
|
||||
data[axis].addSeries(series);
|
||||
continue;
|
||||
}
|
||||
|
||||
XYSeries series = new XYSeries(seriesCount++, false, true);
|
||||
series.setDescription(thisBranch.getName() + ": " + name);
|
||||
List<XYSeries> seriesList = createSeriesForType(i, seriesCount, type, unit, thisBranch,
|
||||
(branchIndex == 0 ? name : thisBranch.getName() + ": " + name));
|
||||
|
||||
// Copy all the data from the secondary branch
|
||||
List<Double> plotx = thisBranch.get(domainType);
|
||||
List<Double> ploty = thisBranch.get(type);
|
||||
|
||||
int pointCount = plotx.size();
|
||||
for (int j = 0; j < pointCount; j++) {
|
||||
series.add(domainUnit.toUnit(plotx.get(j)), unit.toUnit(ploty.get(j)));
|
||||
}
|
||||
for (XYSeries series : seriesList) {
|
||||
data[axis].addSeries(series);
|
||||
seriesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update axis label
|
||||
if (axisLabel[axis] == null)
|
||||
axisLabel[axis] = type.getName();
|
||||
axisLabel[axis] = Util.formatHTMLString(type.getName());
|
||||
else
|
||||
axisLabel[axis] += "; " + type.getName();
|
||||
axisLabel[axis] += "; " + Util.formatHTMLString(type.getName());
|
||||
}
|
||||
|
||||
// Add the data and formatting to the plot
|
||||
@ -292,13 +267,32 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
|
||||
}
|
||||
|
||||
private String getLabel(T type, Unit unit) {
|
||||
String name = type.getName();
|
||||
String name = Util.formatHTMLString(type.getName());
|
||||
if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) &&
|
||||
!UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0)
|
||||
name += " (" + unit.getUnit() + ")";
|
||||
return name;
|
||||
}
|
||||
|
||||
protected List<XYSeries> createSeriesForType(int dataIndex, int startIndex, T type, Unit unit, B branch,
|
||||
String baseName) {
|
||||
// Default implementation for regular DataBranch
|
||||
XYSeries series = new XYSeries(startIndex, false, true);
|
||||
series.setDescription(baseName);
|
||||
|
||||
List<Double> plotx = branch.get(filledConfig.getDomainAxisType());
|
||||
List<Double> ploty = branch.get(type);
|
||||
|
||||
int pointCount = plotx.size();
|
||||
for (int j = 0; j < pointCount; j++) {
|
||||
double x = filledConfig.getDomainAxisUnit().toUnit(plotx.get(j));
|
||||
double y = unit.toUnit(ploty.get(j));
|
||||
series.add(x, y);
|
||||
}
|
||||
|
||||
return Collections.singletonList(series);
|
||||
}
|
||||
|
||||
protected T postProcessType(T type) {
|
||||
return type;
|
||||
}
|
||||
|
@ -130,7 +130,11 @@ public class PlotConfiguration<T extends DataType, B extends DataBranch<T>> impl
|
||||
return plotDataAxes.get(index);
|
||||
}
|
||||
|
||||
public int getTypeCount() {
|
||||
/**
|
||||
* Returns the number of data types in this configuration.
|
||||
* @return the number of data types in this configuration.
|
||||
*/
|
||||
public int getDataCount() {
|
||||
return plotDataTypes.size();
|
||||
}
|
||||
|
||||
@ -242,31 +246,7 @@ public class PlotConfiguration<T extends DataType, B extends DataBranch<T>> impl
|
||||
}
|
||||
|
||||
// Add full range to the axes
|
||||
int length = plotDataTypes.size();
|
||||
for (int i = 0; i < length; i++) {
|
||||
T type = plotDataTypes.get(i);
|
||||
Unit unit = plotDataUnits.get(i);
|
||||
int index = plotDataAxes.get(i);
|
||||
if (index < 0) {
|
||||
throw new IllegalStateException("fitAxes called with auto-selected axis");
|
||||
}
|
||||
Axis axis = allAxes.get(index);
|
||||
|
||||
double min = unit.toUnit(data.get(0).getMinimum(type));
|
||||
double max = unit.toUnit(data.get(0).getMaximum(type));
|
||||
|
||||
for (int j = 1; j < data.size(); j++) {
|
||||
// Ignore empty data
|
||||
if (data.get(j).getLength() == 0) {
|
||||
continue;
|
||||
}
|
||||
min = Math.min(min, unit.toUnit(data.get(j).getMinimum(type)));
|
||||
max = Math.max(max, unit.toUnit(data.get(j).getMaximum(type)));
|
||||
}
|
||||
|
||||
axis.addBound(min);
|
||||
axis.addBound(max);
|
||||
}
|
||||
calculateAxisBounds(data);
|
||||
|
||||
// Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
|
||||
for (Axis a : allAxes) {
|
||||
@ -323,6 +303,34 @@ public class PlotConfiguration<T extends DataType, B extends DataBranch<T>> impl
|
||||
right.addBound(max2);
|
||||
}
|
||||
|
||||
protected void calculateAxisBounds(List<B> data) {
|
||||
int length = plotDataTypes.size();
|
||||
for (int i = 0; i < length; i++) {
|
||||
T type = plotDataTypes.get(i);
|
||||
Unit unit = plotDataUnits.get(i);
|
||||
int index = plotDataAxes.get(i);
|
||||
if (index < 0) {
|
||||
throw new IllegalStateException("fitAxes called with auto-selected axis");
|
||||
}
|
||||
Axis axis = allAxes.get(index);
|
||||
|
||||
double min = unit.toUnit(data.get(0).getMinimum(type));
|
||||
double max = unit.toUnit(data.get(0).getMaximum(type));
|
||||
|
||||
for (int j = 1; j < data.size(); j++) {
|
||||
// Ignore empty data
|
||||
if (data.get(j).getLength() == 0) {
|
||||
continue;
|
||||
}
|
||||
min = Math.min(min, unit.toUnit(data.get(j).getMinimum(type)));
|
||||
max = Math.max(max, unit.toUnit(data.get(j).getMaximum(type)));
|
||||
}
|
||||
|
||||
axis.addBound(min);
|
||||
axis.addBound(max);
|
||||
}
|
||||
}
|
||||
|
||||
//// Helper methods
|
||||
/**
|
||||
* Fits the axis ranges to the data and returns the "goodness value" of this
|
||||
|
@ -28,9 +28,12 @@ import java.awt.event.ItemEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class PlotPanel<T extends DataType & Groupable<G>, B extends DataBranch<T>, G extends Group,
|
||||
C extends PlotConfiguration<T, B>, S extends PlotTypeSelector<G, T>> extends JPanel {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
public class PlotPanel<T extends DataType & Groupable<G>,
|
||||
B extends DataBranch<T>,
|
||||
G extends Group,
|
||||
C extends PlotConfiguration<T, B>,
|
||||
S extends PlotTypeSelector<T, G>> extends JPanel {
|
||||
protected static final Translator trans = Application.getTranslator();
|
||||
|
||||
//// Custom
|
||||
protected static final String CUSTOM = trans.get("simplotpanel.CUSTOM");
|
||||
@ -193,7 +196,7 @@ public class PlotPanel<T extends DataType & Groupable<G>, B extends DataBranch<T
|
||||
newYAxisBtn.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (configuration.getTypeCount() >= 15) {
|
||||
if (configuration.getDataCount() >= 15) {
|
||||
JOptionPane.showMessageDialog(PlotPanel.this,
|
||||
//// A maximum of 15 plots is allowed.
|
||||
//// Cannot add plot
|
||||
@ -211,7 +214,7 @@ public class PlotPanel<T extends DataType & Groupable<G>, B extends DataBranch<T
|
||||
if (configuration.getDomainAxisType().equals(t)) {
|
||||
used = true;
|
||||
} else {
|
||||
for (int i = 0; i < configuration.getTypeCount(); i++) {
|
||||
for (int i = 0; i < configuration.getDataCount(); i++) {
|
||||
if (configuration.getType(i).equals(t)) {
|
||||
used = true;
|
||||
break;
|
||||
@ -238,7 +241,7 @@ public class PlotPanel<T extends DataType & Groupable<G>, B extends DataBranch<T
|
||||
return typeSelectorPanel;
|
||||
}
|
||||
|
||||
protected C getConfiguration() {
|
||||
public C getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
@ -251,7 +254,7 @@ public class PlotPanel<T extends DataType & Groupable<G>, B extends DataBranch<T
|
||||
modified = true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < configuration.getTypeCount(); i++) {
|
||||
for (int i = 0; i < configuration.getDataCount(); i++) {
|
||||
if (!Utils.contains(typesY, configuration.getType(i))) {
|
||||
configuration.removePlotDataType(i);
|
||||
i--;
|
||||
@ -288,7 +291,7 @@ public class PlotPanel<T extends DataType & Groupable<G>, B extends DataBranch<T
|
||||
}
|
||||
|
||||
typeSelectorPanel.removeAll();
|
||||
for (int i = 0; i < configuration.getTypeCount(); i++) {
|
||||
for (int i = 0; i < configuration.getDataCount(); i++) {
|
||||
T type = configuration.getType(i);
|
||||
Unit unit = configuration.getUnit(i);
|
||||
int axis = configuration.getAxis(i);
|
||||
@ -308,7 +311,7 @@ public class PlotPanel<T extends DataType & Groupable<G>, B extends DataBranch<T
|
||||
return (S) new PlotTypeSelector<>(i, type, unit, axis, Arrays.asList(typesY));
|
||||
}
|
||||
|
||||
private void addSelectionListeners(S selector, final int idx) {
|
||||
protected void addSelectionListeners(S selector, final int idx) {
|
||||
// Type
|
||||
selector.addTypeSelectionListener(e -> {
|
||||
if (modifying > 0) return;
|
||||
|
@ -22,7 +22,7 @@ import javax.swing.JComboBox;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
public class PlotTypeSelector<G extends Group, T extends Groupable<G> & UnitValue> extends JPanel {
|
||||
public class PlotTypeSelector<T extends Groupable<G> & UnitValue, G extends Group> extends JPanel {
|
||||
protected static final Translator trans = Application.getTranslator();
|
||||
private static final long serialVersionUID = 9056324972817542570L;
|
||||
|
||||
|
@ -28,7 +28,6 @@ import org.jfree.chart.renderer.xy.XYItemRenderer;
|
||||
import org.jfree.chart.title.TextTitle;
|
||||
import org.jfree.chart.ui.HorizontalAlignment;
|
||||
import org.jfree.chart.ui.VerticalAlignment;
|
||||
import org.jfree.data.xy.XYSeriesCollection;
|
||||
import org.jfree.chart.ui.RectangleAnchor;
|
||||
import org.jfree.chart.ui.RectangleEdge;
|
||||
import org.jfree.chart.ui.RectangleInsets;
|
||||
@ -228,7 +227,7 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
|
||||
|
||||
double xcoord = domainInterpolator.getValue(t);
|
||||
|
||||
for (int index = 0; index < config.getTypeCount(); index++) {
|
||||
for (int index = 0; index < config.getDataCount(); index++) {
|
||||
FlightDataType type = config.getType(index);
|
||||
List<Double> range = dataBranch.get(type);
|
||||
|
||||
|
@ -5,7 +5,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import info.openrocket.core.document.Simulation;
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.simulation.DataBranch;
|
||||
import info.openrocket.core.simulation.DataType;
|
||||
@ -55,28 +54,34 @@ public abstract class Util {
|
||||
};
|
||||
|
||||
public static <T extends DataType, B extends DataBranch<T>> List<String> generateSeriesLabels(List<B> branches) {
|
||||
List<String> stages = new ArrayList<>(branches.size());
|
||||
List<String> series = new ArrayList<>(branches.size());
|
||||
// We need to generate unique strings for each of the branches. Since the branch names are based
|
||||
// on the stage name there is no guarantee they are unique. In order to address this, we first assume
|
||||
// all the names are unique, then go through them looking for duplicates.
|
||||
for (B branch : branches) {
|
||||
stages.add(branch.getName());
|
||||
series.add(formatHTMLString(branch.getName()));
|
||||
}
|
||||
// check for duplicates:
|
||||
for (int i = 0; i < stages.size(); i++) {
|
||||
String stagename = stages.get(i);
|
||||
int numberDuplicates = Collections.frequency(stages, stagename);
|
||||
for (int i = 0; i < series.size(); i++) {
|
||||
String stagename = series.get(i);
|
||||
int numberDuplicates = Collections.frequency(series, stagename);
|
||||
if (numberDuplicates > 1) {
|
||||
int index = i;
|
||||
int count = 1;
|
||||
while (count <= numberDuplicates) {
|
||||
stages.set(index, stagename + "(" + count + ")");
|
||||
series.set(index, stagename + "(" + count + ")");
|
||||
count ++;
|
||||
for (index++; index < stages.size() && !stagename.equals(stages.get(index)); index++);
|
||||
for (index++; index < series.size() && !stagename.equals(series.get(index)); index++);
|
||||
}
|
||||
}
|
||||
}
|
||||
return stages;
|
||||
return series;
|
||||
}
|
||||
|
||||
public static String formatHTMLString(String input) {
|
||||
// TODO: Use AttributeString to format the string
|
||||
// Remove the HTML-like tags from the final string
|
||||
return input.replaceAll("<sub>|</sub>|<sup>|</sup>|<html>|</html>", "");
|
||||
}
|
||||
|
||||
public static Color getPlotColor(int index) {
|
||||
|
@ -49,7 +49,7 @@ import info.openrocket.swing.gui.theme.UITheme;
|
||||
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
|
||||
*/
|
||||
public class SimulationPlotPanel extends PlotPanel<FlightDataType, FlightDataBranch, FlightDataTypeGroup,
|
||||
SimulationPlotConfiguration, PlotTypeSelector<FlightDataTypeGroup, FlightDataType>> {
|
||||
SimulationPlotConfiguration, PlotTypeSelector<FlightDataType, FlightDataTypeGroup>> {
|
||||
@Serial
|
||||
private static final long serialVersionUID = -2227129713185477998L;
|
||||
|
||||
@ -262,7 +262,7 @@ public class SimulationPlotPanel extends PlotPanel<FlightDataType, FlightDataBra
|
||||
|
||||
@Override
|
||||
public JDialog doPlot(Window parent) {
|
||||
if (configuration.getTypeCount() == 0) {
|
||||
if (configuration.getDataCount() == 0) {
|
||||
JOptionPane.showMessageDialog(SimulationPlotPanel.this,
|
||||
trans.get("error.noPlotSelected"),
|
||||
trans.get("error.noPlotSelected.title"),
|
||||
|
@ -24,6 +24,7 @@ import java.util.prefs.Preferences;
|
||||
import info.openrocket.core.database.Databases;
|
||||
import info.openrocket.core.preferences.ApplicationPreferences;
|
||||
import info.openrocket.core.rocketcomponent.NoseCone;
|
||||
import info.openrocket.swing.gui.dialogs.componentanalysis.CADataType;
|
||||
import info.openrocket.swing.gui.theme.UITheme;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -646,6 +647,16 @@ public class SwingPreferences extends ApplicationPreferences implements Simulati
|
||||
prefs.putBoolean(type.getName(), selected);
|
||||
}
|
||||
|
||||
public boolean isComponentAnalysisDataTypeExportSelected(CADataType type) {
|
||||
Preferences prefs = PREFNODE.node("exportsComponentAnalysis");
|
||||
return prefs.getBoolean(type.getName(), false);
|
||||
}
|
||||
|
||||
public void setComponentAnalysisExportSelected(CADataType type, boolean selected) {
|
||||
Preferences prefs = PREFNODE.node("exportsComponentAnalysis");
|
||||
prefs.putBoolean(type.getName(), selected);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////// Default unit storage
|
||||
|
@ -30,6 +30,7 @@ open module info.openrocket.swing {
|
||||
requires com.formdev.flatlaf;
|
||||
requires com.formdev.flatlaf.extras;
|
||||
requires com.formdev.flatlaf.intellijthemes;
|
||||
requires org.checkerframework.checker.qual;
|
||||
|
||||
// Service providers
|
||||
// Also edit swing/src/main/resources/META-INF/services !! (until gradle-modules-plugin supports service
|
||||
|
Loading…
x
Reference in New Issue
Block a user