diff --git a/core/src/main/resources/l10n/messages.properties b/core/src/main/resources/l10n/messages.properties
index e4dd72f7e..545c807e5 100644
--- a/core/src/main/resources/l10n/messages.properties
+++ b/core/src/main/resources/l10n/messages.properties
@@ -948,6 +948,35 @@ componentanalysisdlg.TabStability.Col.Component = Component
componentanalysisdlg.TOTAL = Total (Rocket)
componentanalysisdlg.noWarnings = No warnings.
+! 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 = Total CD vs. Mach number
+
+! 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:
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataBranch.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataBranch.java
new file mode 100644
index 000000000..2ad1ba427
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataBranch.java
@@ -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 {
+ // Map to store values for each CADataType-RocketComponent pair
+ private final Map>> componentValues = new HashMap<>();
+ // Maps to store min and max values for each CADataType-RocketComponent pair
+ private final Map> componentMinValues = new HashMap<>();
+ private final Map> 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> entry : values.entrySet()) {
+ entry.getValue().add(Double.NaN);
+ }
+
+ for (Map> componentMap : componentValues.values()) {
+ for (ArrayList 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> typeMap = componentValues.get(type);
+ ArrayList list = typeMap.computeIfAbsent(component, k -> {
+ ArrayList 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 minMap = componentMinValues.get(type);
+ Map 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 get(CADataType type, RocketComponent component) {
+ if (type instanceof CADomainDataType) {
+ return super.get(type);
+ }
+
+ Map> typeMap = componentValues.get(type);
+ if (typeMap == null) return null;
+
+ ArrayList 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> typeMap = componentValues.get(type);
+ if (typeMap == null) return null;
+
+ ArrayList 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> typeMap = componentValues.get(type);
+ if (typeMap == null) return Double.NaN;
+
+ ArrayList 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 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 maxMap = componentMaxValues.get(type);
+ if (maxMap == null) return Double.NaN;
+
+ Double max = maxMap.get(component);
+ return (max != null) ? max : Double.NaN;
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataType.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataType.java
new file mode 100644
index 000000000..f3c474533
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataType.java
@@ -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, Groupable, 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("CN" + ALPHA + "",
+ "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 calculateComponentsForType(Rocket rocket, CADataType type) {
+ List 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;
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataTypeGroup.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataTypeGroup.java
new file mode 100644
index 000000000..a06d75f67
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataTypeGroup.java
@@ -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, 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;
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADomainDataType.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADomainDataType.java
new file mode 100644
index 000000000..0cae02a4a
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADomainDataType.java
@@ -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;
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAExportPanel.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAExportPanel.java
new file mode 100644
index 000000000..216a6f6b9
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAExportPanel.java
@@ -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 {
+ 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;
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAParameterSweep.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAParameterSweep.java
new file mode 100644
index 000000000..6debae837
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAParameterSweep.java
@@ -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 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 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 generateSweepValues(double min, double max, double delta) {
+ List 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 aeroData, double sweepValue) {
+ for (Map.Entry 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 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 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 aeroData, double sweepValue) {
+ double totalRollForce = 0;
+ double totalRollDamping = 0;
+
+ for (Map.Entry 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);
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisParameters.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAParameters.java
similarity index 88%
rename from swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisParameters.java
rename to swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAParameters.java
index ccf910719..3439a483b 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisParameters.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAParameters.java
@@ -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);
}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlot.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlot.java
new file mode 100644
index 000000000..28a36f671
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlot.java
@@ -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 {
+ public CAPlot(String name, CADataBranch mainBranch, CAPlotConfiguration config,
+ List allBranches, boolean initialShowPoints) {
+ super(name, mainBranch, config, allBranches, initialShowPoints);
+ }
+
+ @Override
+ protected List 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 plotx = branch.get(filledConfig.getDomainAxisType());
+ List 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);
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotConfiguration.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotConfiguration.java
new file mode 100644
index 000000000..45ff51245
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotConfiguration.java
@@ -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 {
+ private static final Translator trans = Application.getTranslator();
+
+ private ArrayList plotDataComponents = new ArrayList<>();
+
+ public static final CAPlotConfiguration[] DEFAULT_CONFIGURATIONS;
+ static {
+ ArrayList 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 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;
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotDialog.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotDialog.java
new file mode 100644
index 000000000..2ffaca89b
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotDialog.java
@@ -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 {
+ private CAPlotDialog(Window parent, String name, CAPlot plot, CAPlotConfiguration config, List allBranches, boolean initialShowPoints) {
+ super(parent, name, plot, config, allBranches, initialShowPoints);
+ }
+
+ public static CAPlotDialog create(Window parent, String name, CAPlotConfiguration config, List 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);
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotPanel.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotPanel.java
new file mode 100644
index 000000000..2c6af08fc
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotPanel.java
@@ -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 {
+ /** 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));
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotTypeSelector.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotTypeSelector.java
new file mode 100644
index 000000000..f6250b769
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotTypeSelector.java
@@ -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 {
+ private final JComboBox componentSelector;
+
+ public CAPlotTypeSelector(final ComponentAnalysisPlotExportDialog parent, int plotIndex,
+ CADataType type, Unit unit, int position, List availableTypes,
+ List 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 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();
+ }
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisDialog.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisDialog.java
index 953c0cec3..01f73656f 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisDialog.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisDialog.java
@@ -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;
diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisPlotExportDialog.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisPlotExportDialog.java
new file mode 100644
index 000000000..c8c9f13cf
--- /dev/null
+++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisPlotExportDialog.java
@@ -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 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> 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 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 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);
+ }
+ }
+
+}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java
index 948433eba..e4b29a4a1 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java
@@ -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, 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, 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, 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, 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 plotx = thisBranch.get(domainType);
- List 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 seriesList = createSeriesForType(i, seriesCount, type, unit, thisBranch,
+ (branchIndex == 0 ? name : thisBranch.getName() + ": " + name));
- // Copy all the data from the secondary branch
- List plotx = thisBranch.get(domainType);
- List 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, 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 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 plotx = branch.get(filledConfig.getDomainAxisType());
+ List 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;
}
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/PlotConfiguration.java b/swing/src/main/java/info/openrocket/swing/gui/plot/PlotConfiguration.java
index c5b425f5f..0dfab0ffe 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/PlotConfiguration.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/PlotConfiguration.java
@@ -130,7 +130,11 @@ public class PlotConfiguration> 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> 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> impl
right.addBound(max2);
}
+ protected void calculateAxisBounds(List 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
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/PlotPanel.java b/swing/src/main/java/info/openrocket/swing/gui/plot/PlotPanel.java
index 0ffd440f3..6d63b8a6c 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/PlotPanel.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/PlotPanel.java
@@ -28,9 +28,12 @@ import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.Arrays;
-public class PlotPanel, B extends DataBranch, G extends Group,
- C extends PlotConfiguration, S extends PlotTypeSelector> extends JPanel {
- private static final Translator trans = Application.getTranslator();
+public class PlotPanel,
+ B extends DataBranch,
+ G extends Group,
+ C extends PlotConfiguration,
+ S extends PlotTypeSelector> 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, B extends DataBranch= 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, B extends DataBranch, B extends DataBranch, B extends DataBranch, B extends DataBranch, B extends DataBranch(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;
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/PlotTypeSelector.java b/swing/src/main/java/info/openrocket/swing/gui/plot/PlotTypeSelector.java
index 60e821f1b..948b163ed 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/PlotTypeSelector.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/PlotTypeSelector.java
@@ -22,7 +22,7 @@ import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
-public class PlotTypeSelector & UnitValue> extends JPanel {
+public class PlotTypeSelector & UnitValue, G extends Group> extends JPanel {
protected static final Translator trans = Application.getTranslator();
private static final long serialVersionUID = 9056324972817542570L;
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlot.java b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlot.java
index c493ea4de..8a5756ef4 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlot.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlot.java
@@ -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 range = dataBranch.get(type);
diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/Util.java b/swing/src/main/java/info/openrocket/swing/gui/plot/Util.java
index 599bba12c..8daa64c55 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/plot/Util.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/plot/Util.java
@@ -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 > List generateSeriesLabels(List branches) {
- List stages = new ArrayList<>(branches.size());
+ List 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("|||||", "");
}
public static Color getPlotColor(int index) {
diff --git a/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationPlotPanel.java b/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationPlotPanel.java
index cc91db4bd..b1034e9c8 100644
--- a/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationPlotPanel.java
+++ b/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationPlotPanel.java
@@ -49,7 +49,7 @@ import info.openrocket.swing.gui.theme.UITheme;
* @author Sampo Niskanen
*/
public class SimulationPlotPanel extends PlotPanel> {
+ SimulationPlotConfiguration, PlotTypeSelector> {
@Serial
private static final long serialVersionUID = -2227129713185477998L;
@@ -262,7 +262,7 @@ public class SimulationPlotPanel extends PlotPanel