From 3312cb4f6cfcaa0e3dd7176c372e158b3e4c011e Mon Sep 17 00:00:00 2001 From: SiboVG Date: Thu, 22 Aug 2024 03:21:47 +0200 Subject: [PATCH] [#2525] Implement component analysis plotting --- .../main/resources/l10n/messages.properties | 29 ++ .../componentanalysis/CADataBranch.java | 181 ++++++++++++ .../dialogs/componentanalysis/CADataType.java | 164 +++++++++++ .../componentanalysis/CADataTypeGroup.java | 47 +++ .../componentanalysis/CADomainDataType.java | 71 +++++ .../componentanalysis/CAExportPanel.java | 101 +++++++ .../componentanalysis/CAParameterSweep.java | 171 +++++++++++ ...lysisParameters.java => CAParameters.java} | 8 +- .../gui/dialogs/componentanalysis/CAPlot.java | 45 +++ .../CAPlotConfiguration.java | 101 +++++++ .../componentanalysis/CAPlotDialog.java | 21 ++ .../componentanalysis/CAPlotPanel.java | 85 ++++++ .../componentanalysis/CAPlotTypeSelector.java | 56 ++++ .../ComponentAnalysisDialog.java | 15 +- .../ComponentAnalysisPlotExportDialog.java | 270 ++++++++++++++++++ .../info/openrocket/swing/gui/plot/Plot.java | 70 +++-- .../swing/gui/plot/PlotConfiguration.java | 60 ++-- .../openrocket/swing/gui/plot/PlotPanel.java | 21 +- .../swing/gui/plot/PlotTypeSelector.java | 2 +- .../swing/gui/plot/SimulationPlot.java | 3 +- .../info/openrocket/swing/gui/plot/Util.java | 23 +- .../gui/simulation/SimulationPlotPanel.java | 4 +- .../swing/gui/util/SwingPreferences.java | 11 + swing/src/main/java/module-info.java | 1 + 24 files changed, 1463 insertions(+), 97 deletions(-) create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataBranch.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataType.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADataTypeGroup.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CADomainDataType.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAExportPanel.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAParameterSweep.java rename swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/{ComponentAnalysisParameters.java => CAParameters.java} (88%) create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlot.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotConfiguration.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotDialog.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotPanel.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAPlotTypeSelector.java create mode 100644 swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisPlotExportDialog.java 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