[#2525] Implement component analysis plotting

This commit is contained in:
SiboVG 2024-08-22 03:21:47 +02:00
parent 0fd1a93f5f
commit 3312cb4f6c
24 changed files with 1463 additions and 97 deletions

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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");
@ -542,7 +545,7 @@ public class ComponentAnalysisDialog extends JDialog implements StateChangeListe
dragData.clear();
rollData.clear();
for(final RocketComponent comp: configuration.getAllComponents()) {
for (final RocketComponent comp: configuration.getAllComponents()) {
CMAnalysisEntry cmEntry = cmMap.get(comp.hashCode());
if (null == cmEntry) {
log.warn("Could not find massData entry for component: " + comp.getName());
@ -595,7 +598,7 @@ public class ComponentAnalysisDialog extends JDialog implements StateChangeListe
// }
}
for(final MotorConfiguration config: configuration.getActiveMotors()) {
for (final MotorConfiguration config: configuration.getActiveMotors()) {
CMAnalysisEntry cmEntry = cmMap.get(config.getMotor().getDesignation().hashCode());
if (null == cmEntry) {
continue;

View File

@ -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);
}
}
}

View File

@ -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++;
}
data[axis].addSeries(series);
}
// 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;
}

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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) {

View File

@ -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"),

View File

@ -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;
@ -645,6 +646,16 @@ public class SwingPreferences extends ApplicationPreferences implements Simulati
Preferences prefs = PREFNODE.node("exports");
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);
}

View File

@ -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