From f21dd554525c0b6ed428e21e6e65ed4b621b22a1 Mon Sep 17 00:00:00 2001 From: SiboVG Date: Tue, 20 Aug 2024 13:31:05 +0200 Subject: [PATCH] Refactor SimulationPlot to more generic Plot --- .../core/simulation/DataBranch.java | 2 +- .../openrocket/core/simulation/DataType.java | 1 + .../info/openrocket/swing/gui/plot/Plot.java | 529 ++++++++++++++++ .../swing/gui/plot/PlotConfiguration.java | 2 +- .../swing/gui/plot/SimulationPlot.java | 579 ++---------------- .../gui/plot/SimulationPlotConfiguration.java | 1 - .../swing/gui/plot/SimulationPlotDialog.java | 5 +- .../info/openrocket/swing/gui/plot/Util.java | 23 +- .../gui/simulation/SimulationExportPanel.java | 2 +- 9 files changed, 591 insertions(+), 553 deletions(-) create mode 100644 swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java diff --git a/core/src/main/java/info/openrocket/core/simulation/DataBranch.java b/core/src/main/java/info/openrocket/core/simulation/DataBranch.java index 7c4913eb7..74f4ead69 100644 --- a/core/src/main/java/info/openrocket/core/simulation/DataBranch.java +++ b/core/src/main/java/info/openrocket/core/simulation/DataBranch.java @@ -49,7 +49,7 @@ public abstract class DataBranch implements Monitorable { this.name = name; } - protected void addType(T type) { + public void addType(T type) { if (values.containsKey(type)) { throw new IllegalArgumentException("Value type " + type + " already exists."); } diff --git a/core/src/main/java/info/openrocket/core/simulation/DataType.java b/core/src/main/java/info/openrocket/core/simulation/DataType.java index 99e5b5738..ff62116da 100644 --- a/core/src/main/java/info/openrocket/core/simulation/DataType.java +++ b/core/src/main/java/info/openrocket/core/simulation/DataType.java @@ -6,4 +6,5 @@ import info.openrocket.core.util.UnitValue; * A type of data that can be stored in a {@link DataBranch}. */ public interface DataType extends UnitValue { + String getName(); } 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 new file mode 100644 index 000000000..b553dcef5 --- /dev/null +++ b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java @@ -0,0 +1,529 @@ +package info.openrocket.swing.gui.plot; + +import info.openrocket.core.l10n.Translator; +import info.openrocket.core.simulation.DataBranch; +import info.openrocket.core.simulation.DataType; +import info.openrocket.core.startup.Application; +import info.openrocket.core.unit.Unit; +import info.openrocket.core.unit.UnitGroup; +import info.openrocket.swing.gui.util.SwingPreferences; +import info.openrocket.swing.utils.DecimalFormatter; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.LegendItem; +import org.jfree.chart.LegendItemCollection; +import org.jfree.chart.LegendItemSource; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.block.BlockBorder; +import org.jfree.chart.labels.StandardXYToolTipGenerator; +import org.jfree.chart.plot.DefaultDrawingSupplier; +import org.jfree.chart.plot.Marker; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.ValueMarker; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.chart.title.LegendTitle; +import org.jfree.chart.title.TextTitle; +import org.jfree.chart.ui.LengthAdjustmentType; +import org.jfree.chart.ui.RectangleAnchor; +import org.jfree.chart.ui.RectangleEdge; +import org.jfree.chart.ui.RectangleInsets; +import org.jfree.data.Range; +import org.jfree.data.xy.XYDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +import org.jfree.text.TextUtilities; +import org.jfree.ui.TextAnchor; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class Plot, C extends PlotConfiguration> { + protected static final Translator trans = Application.getTranslator(); + protected static final SwingPreferences preferences = (SwingPreferences) Application.getPreferences(); + + protected static final float PLOT_STROKE_WIDTH = 1.5f; + + protected int branchCount; + protected final List renderers = new ArrayList<>(); + protected final LegendItems legendItems; + protected final XYSeriesCollection[] data; + protected final C filledConfig; // Configuration after using 'fillAutoAxes' + private final List allBranches; + + protected final JFreeChart chart; + + protected Plot(JFreeChart chart, B mainBranch, C config, List allBranches, boolean initialShowPoints) { + this.chart = chart; + this.allBranches = allBranches; + this.branchCount = allBranches.size(); + + this.chart.addSubtitle(new TextTitle(config.getName())); + this.chart.getTitle().setFont(new Font("Dialog", Font.BOLD, 23)); + this.chart.setBackgroundPaint(new Color(240, 240, 240)); + this.legendItems = new LegendItems(); + LegendTitle legend = new LegendTitle(this.legendItems); + legend.setMargin(new RectangleInsets(1.0, 1.0, 1.0, 1.0)); + legend.setFrame(BlockBorder.NONE); + legend.setBackgroundPaint(new Color(240, 240, 240)); + legend.setPosition(RectangleEdge.BOTTOM); + chart.addSubtitle(legend); + + // Create the data series for both axes + this.data = new XYSeriesCollection[2]; + this.data[0] = new XYSeriesCollection(); + this.data[1] = new XYSeriesCollection(); + + // Fill the auto-selections based on first branch selected. + this.filledConfig = config.fillAutoAxes(mainBranch); + + // Get the domain axis type + final T domainType = filledConfig.getDomainAxisType(); + final Unit domainUnit = filledConfig.getDomainAxisUnit(); + if (domainType == null) { + throw new IllegalArgumentException("Domain axis type not specified."); + } + + // Get plot length (ignore trailing NaN's) + int typeCount = filledConfig.getTypeCount(); + + int seriesCount = 0; + + // Compute the axes based on the min and max value of all branches + C plotConfig = filledConfig.cloneConfiguration(); + plotConfig.fitAxes(allBranches); + List minMaxAxes = plotConfig.getAllAxes(); + + // 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++) { + // Get info + T type = postProcessType(filledConfig.getType(i)); + Unit unit = filledConfig.getUnit(i); + int axis = filledConfig.getAxis(i); + 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++) { + 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); + + // 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))); + } + data[axis].addSeries(series); + } + + // Update axis label + if (axisLabel[axis] == null) + axisLabel[axis] = type.getName(); + else + axisLabel[axis] += "; " + type.getName(); + } + + // Add the data and formatting to the plot + XYPlot plot = chart.getXYPlot(); + plot.setDomainPannable(true); + plot.setRangePannable(true); + + // Plot appearance + plot.setBackgroundPaint(Color.white); + plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0)); + plot.setRangeGridlinesVisible(true); + plot.setRangeGridlinePaint(Color.lightGray); + + plot.setDomainGridlinesVisible(true); + plot.setDomainGridlinePaint(Color.lightGray); + + int cumulativeSeriesCount = 0; + + for (int axisno = 0; axisno < 2; axisno++) { + // Check whether axis has any data + if (data[axisno].getSeriesCount() > 0) { + // Create and set axis + double min = minMaxAxes.get(axisno).getMinValue(); + double max = minMaxAxes.get(axisno).getMaxValue(); + + NumberAxis axis = new PresetNumberAxis(min, max); + axis.setLabel(axisLabel[axisno]); + plot.setRangeAxis(axisno, axis); + axis.setLabelFont(new Font("Dialog", Font.BOLD, 14)); + + double domainMin = data[axisno].getDomainLowerBound(true); + double domainMax = data[axisno].getDomainUpperBound(true); + + plot.setDomainAxis(new PresetNumberAxis(domainMin, domainMax)); + + // Custom tooltip generator + int finalAxisno = axisno; + StandardXYToolTipGenerator tooltipGenerator = new StandardXYToolTipGenerator() { + @Override + public String generateToolTip(XYDataset dataset, int series, int item) { + XYSeriesCollection collection = data[finalAxisno]; + if (collection.getSeriesCount() == 0) { + return null; + } + XYSeries ser = collection.getSeries(series); + String name = ser.getDescription(); + + // Extract the unit from the last part of the series description, between parenthesis + Matcher m = Pattern.compile(".*\\((.*?)\\)").matcher(name); + String unitY = ""; + if (m.find()) { + unitY = m.group(1); + } + String unitX = domainUnit.getUnit(); + + double dataY = dataset.getYValue(series, item); + double dataX = dataset.getXValue(series, item); + + return formatSampleTooltip(name, dataX, unitX, dataY, unitY, item); + } + }; + + // Add data and map to the axis + plot.setDataset(axisno, data[axisno]); + ModifiedXYItemRenderer r = new ModifiedXYItemRenderer(branchCount); + renderers.add(r); + r.setDefaultToolTipGenerator(tooltipGenerator); + plot.setRenderer(axisno, r); + r.setDefaultShapesVisible(initialShowPoints); + r.setDefaultShapesFilled(true); + + // Set colors for all series of the current axis + for (int seriesIndex = 0; seriesIndex < data[axisno].getSeriesCount(); seriesIndex++) { + int colorIndex = cumulativeSeriesCount + seriesIndex; + r.setSeriesPaint(seriesIndex, Util.getPlotColor(colorIndex)); + + Stroke lineStroke = new BasicStroke(PLOT_STROKE_WIDTH); + r.setSeriesStroke(seriesIndex, lineStroke); + } + + // Update the cumulative count for the next axis + cumulativeSeriesCount += data[axisno].getSeriesCount(); + + // Now we pull the colors for the legend. + for (int j = 0; j < data[axisno].getSeriesCount(); j += branchCount) { + String name = data[axisno].getSeries(j).getDescription(); + this.legendItems.lineLabels.add(name); + Paint linePaint = r.lookupSeriesPaint(j); + this.legendItems.linePaints.add(linePaint); + Shape itemShape = r.lookupSeriesShape(j); + this.legendItems.pointShapes.add(itemShape); + Stroke lineStroke = r.getSeriesStroke(j); + this.legendItems.lineStrokes.add(lineStroke); + } + + plot.mapDatasetToRangeAxis(axisno, axisno); + } + } + + plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit)); + plot.addDomainMarker(new ValueMarker(0)); + plot.addRangeMarker(new ValueMarker(0)); + + plot.getDomainAxis().setLabelFont(new Font("Dialog", Font.BOLD, 14)); + } + + public JFreeChart getJFreeChart() { + return chart; + } + + private String getLabel(T type, Unit unit) { + String name = 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 T postProcessType(T type) { + return type; + } + + protected String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx, boolean addYValue) { + String ord_end = "th"; // Ordinal number ending (1'st', 2'nd'...) + if (sampleIdx % 10 == 1) { + ord_end = "st"; + } else if (sampleIdx % 10 == 2) { + ord_end = "nd"; + } else if (sampleIdx % 10 == 3) { + ord_end = "rd"; + } + + DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false); + DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false); + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("" + + "%s
", dataName)); + + if (addYValue) { + sb.append(String.format("Y: %s %s
", df_y.format(dataY), unitY)); + } + + sb.append(String.format("X: %s %s
" + + "%d%s sample" + + "", df_x.format(dataX), unitX, sampleIdx, ord_end)); + + return sb.toString(); + } + + protected String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx) { + return formatSampleTooltip(dataName, dataX, unitX, dataY, unitY, sampleIdx, true); + } + + protected String formatSampleTooltip(String dataName, double dataX, String unitX, int sampleIdx) { + return formatSampleTooltip(dataName, dataX, unitX, 0, "", sampleIdx, false); + } + + protected static class LegendItems implements LegendItemSource { + protected final List lineLabels = new ArrayList<>(); + protected final List linePaints = new ArrayList<>(); + protected final List lineStrokes = new ArrayList<>(); + protected final List pointShapes = new ArrayList<>(); + + @Override + public LegendItemCollection getLegendItems() { + LegendItemCollection c = new LegendItemCollection(); + int i = 0; + for (String s : lineLabels) { + String label = s; + String description = s; + String toolTipText = null; + String urlText = null; + boolean shapeIsVisible = false; + Shape shape = pointShapes.get(i); + boolean shapeIsFilled = false; + Paint fillPaint = linePaints.get(i); + boolean shapeOutlineVisible = false; + Paint outlinePaint = linePaints.get(i); + Stroke outlineStroke = lineStrokes.get(i); + boolean lineVisible = true; + Stroke lineStroke = lineStrokes.get(i); + Paint linePaint = linePaints.get(i); + + Shape legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); + + LegendItem result = new LegendItem(label, description, toolTipText, + urlText, shapeIsVisible, shape, shapeIsFilled, fillPaint, + shapeOutlineVisible, outlinePaint, outlineStroke, lineVisible, + legendLine, lineStroke, linePaint); + + c.add(result); + i++; + } + return c; + } + } + + public void setShowPoints(boolean showPoints) { + for (ModifiedXYItemRenderer r : renderers) { + r.setDefaultShapesVisible(showPoints); + } + } + + /** + * A modification to the standard renderer that renders the domain marker + * labels vertically instead of horizontally. + * + * This class is special in that it assumes the data series are added to it + * in a specific order. In particular they must be "by parameter by stage". + * Assuming that three series are chosen (a, b, c) and the rocket has 2 stages, the + * data series are added in this order: + * + * series a stage 0 + * series a stage 1 + * series b stage 0 + * series b stage 1 + * series c stage 0 + * series c stage 1 + */ + protected static class ModifiedXYItemRenderer extends XYLineAndShapeRenderer { + + private final int branchCount; + + protected ModifiedXYItemRenderer(int branchCount) { + this.branchCount = branchCount; + } + + @Override + public Paint lookupSeriesPaint(int series) { + return super.lookupSeriesPaint(series / branchCount); + } + + @Override + public Paint lookupSeriesFillPaint(int series) { + return super.lookupSeriesFillPaint(series / branchCount); + } + + @Override + public Paint lookupSeriesOutlinePaint(int series) { + return super.lookupSeriesOutlinePaint(series / branchCount); + } + + @Override + public Stroke lookupSeriesStroke(int series) { + return super.lookupSeriesStroke(series / branchCount); + } + + @Override + public Stroke lookupSeriesOutlineStroke(int series) { + return super.lookupSeriesOutlineStroke(series / branchCount); + } + + @Override + public Shape lookupSeriesShape(int series) { + return DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE[series % branchCount % DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE.length]; + } + + @Override + public Shape lookupLegendShape(int series) { + return DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE[series % branchCount % DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE.length]; + } + + @Override + public Font lookupLegendTextFont(int series) { + return super.lookupLegendTextFont(series / branchCount); + } + + @Override + public Paint lookupLegendTextPaint(int series) { + return super.lookupLegendTextPaint(series / branchCount); + } + + @Override + public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis, + Marker marker, Rectangle2D dataArea) { + + if (!(marker instanceof ValueMarker)) { + // Use parent for all others + super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea); + return; + } + + /* + * Draw the normal marker, but with rotated text. + * Copied from the overridden method. + */ + ValueMarker vm = (ValueMarker) marker; + double value = vm.getValue(); + Range range = domainAxis.getRange(); + if (!range.contains(value)) { + return; + } + + double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge()); + + PlotOrientation orientation = plot.getOrientation(); + Line2D line; + if (orientation == PlotOrientation.HORIZONTAL) { + line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v); + } else { + line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY()); + } + + final Composite originalComposite = g2.getComposite(); + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker + .getAlpha())); + g2.setPaint(marker.getPaint()); + g2.setStroke(marker.getStroke()); + g2.draw(line); + + String label = marker.getLabel(); + RectangleAnchor anchor = marker.getLabelAnchor(); + if (label != null) { + Font labelFont = marker.getLabelFont(); + g2.setFont(labelFont); + g2.setPaint(marker.getLabelPaint()); + Point2D coordinates = calculateDomainMarkerTextAnchorPoint(g2, + orientation, dataArea, line.getBounds2D(), marker + .getLabelOffset(), LengthAdjustmentType.EXPAND, anchor); + + // Changed: + TextAnchor textAnchor = TextAnchor.TOP_RIGHT; + TextUtilities.drawRotatedString(label, g2, (float) coordinates.getX() + 2, + (float) coordinates.getY(), textAnchor, + -Math.PI / 2, textAnchor); + } + g2.setComposite(originalComposite); + } + } + + protected static class PresetNumberAxis extends NumberAxis { + private final double min; + private final double max; + + public PresetNumberAxis(double min, double max) { + this.min = min; + this.max = max; + autoAdjustRange(); + } + + @Override + protected void autoAdjustRange() { + this.setRange(min, max); + } + + @Override + public void setRange(Range range) { + double lowerValue = range.getLowerBound(); + double upperValue = range.getUpperBound(); + if (lowerValue < min || upperValue > max) { + // Don't blow past the min & max of the range this is important to keep + // panning constrained within the current bounds. + return; + } + super.setRange(new Range(lowerValue, upperValue)); + } + + } +} 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 bb5ee2fe1..c5b425f5f 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 @@ -512,7 +512,7 @@ public class PlotConfiguration> impl } @SuppressWarnings("unchecked") - protected > C cloneConfiguration() { + public > C cloneConfiguration() { try { C copy = (C) super.clone(); 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 bc00b5b56..7fa36a3be 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 @@ -1,19 +1,8 @@ package info.openrocket.swing.gui.plot; -import java.awt.AlphaComposite; -import java.awt.BasicStroke; import java.awt.Color; -import java.awt.Composite; import java.awt.Font; -import java.awt.Graphics2D; import java.awt.Image; -import java.awt.Paint; -import java.awt.Shape; -import java.awt.Stroke; -import java.awt.geom.Line2D; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; @@ -21,90 +10,87 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import info.openrocket.core.document.Simulation; -import info.openrocket.core.l10n.Translator; import info.openrocket.core.logging.SimulationAbort; import info.openrocket.core.simulation.FlightDataBranch; import info.openrocket.core.simulation.FlightDataType; import info.openrocket.core.simulation.FlightEvent; -import info.openrocket.core.startup.Application; import info.openrocket.core.preferences.ApplicationPreferences; -import info.openrocket.core.unit.Unit; -import info.openrocket.core.unit.UnitGroup; import info.openrocket.core.util.LinearInterpolator; -import info.openrocket.swing.gui.simulation.SimulationPlotPanel; -import info.openrocket.swing.gui.util.SwingPreferences; - -import info.openrocket.swing.utils.DecimalFormatter; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; -import org.jfree.chart.LegendItem; -import org.jfree.chart.LegendItemCollection; -import org.jfree.chart.LegendItemSource; import org.jfree.chart.annotations.XYImageAnnotation; import org.jfree.chart.annotations.XYTitleAnnotation; -import org.jfree.chart.axis.NumberAxis; -import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.block.BlockBorder; -import org.jfree.chart.labels.StandardXYToolTipGenerator; -import org.jfree.chart.plot.DefaultDrawingSupplier; -import org.jfree.chart.plot.Marker; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.ValueMarker; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYItemRenderer; -import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; -import org.jfree.chart.title.LegendTitle; import org.jfree.chart.title.TextTitle; import org.jfree.chart.ui.HorizontalAlignment; import org.jfree.chart.ui.VerticalAlignment; -import org.jfree.data.Range; -import org.jfree.data.xy.XYDataset; -import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; -import org.jfree.text.TextUtilities; -import org.jfree.chart.ui.LengthAdjustmentType; import org.jfree.chart.ui.RectangleAnchor; import org.jfree.chart.ui.RectangleEdge; import org.jfree.chart.ui.RectangleInsets; -import org.jfree.ui.TextAnchor; /* * TODO: It should be possible to simplify this code quite a bit by using a single Renderer instance for - * both datasets and the legend. But for now, the renderers are queried for the line color information - * and this is held in the Legend. + * both datasets and the legend. But for now, the renderers are queried for the line color information + * and this is held in the Legend. */ @SuppressWarnings("serial") -public class SimulationPlot { - private static final Translator trans = Application.getTranslator(); - - private static final SwingPreferences preferences = (SwingPreferences) Application.getPreferences(); - - private static final float PLOT_STROKE_WIDTH = 1.5f; - - private final JFreeChart chart; - +public class SimulationPlot extends Plot { private final SimulationPlotConfiguration config; private final Simulation simulation; - private final SimulationPlotConfiguration filled; private final List eventList; - private final List renderers = new ArrayList<>(); - - private final LegendItems legendItems; - private ErrorAnnotationSet errorAnnotations = null; - private int branchCount; + SimulationPlot(Simulation simulation, SimulationPlotConfiguration config, boolean initialShowPoints, + JFreeChart chart, FlightDataBranch mainBranch, List allBranches) { + super(chart, mainBranch, config, allBranches, initialShowPoints); - void setShowPoints(boolean showPoints) { - for (ModifiedXYItemRenderer r : renderers) { - r.setDefaultShapesVisible(showPoints); + this.simulation = simulation; + this.config = config; + this.branchCount = simulation.getSimulatedData().getBranchCount(); + + // Create list of events to show (combine event too close to each other) + this.eventList = buildEventInfo(); + + // Create the event markers + drawDomainMarkers(-1); + + errorAnnotations = new ErrorAnnotationSet(branchCount); + } + + public static SimulationPlot create(Simulation simulation, SimulationPlotConfiguration config, boolean initialShowPoints) { + JFreeChart chart = ChartFactory.createXYLineChart( + //// Simulated flight + /*title*/simulation.getName(), + /*xAxisLabel*/null, + /*yAxisLabel*/null, + /*dataset*/null, + /*orientation*/PlotOrientation.VERTICAL, + /*legend*/false, + /*tooltips*/true, + /*urls*/false + ); + + FlightDataBranch mainBranch = simulation.getSimulatedData().getBranch(0); + + return new SimulationPlot(simulation, config, initialShowPoints, chart, mainBranch, + simulation.getSimulatedData().getBranches()); + } + + @Override + protected FlightDataType postProcessType(FlightDataType type) { + if (Objects.equals(type.getName(), "Position upwind")) { + type = FlightDataType.TYPE_POSITION_Y; } + return type; } void setShowErrors(boolean showErrors) { @@ -128,286 +114,6 @@ public class SimulationPlot { errorAnnotations.setCurrent(branch); } - SimulationPlot(Simulation simulation, SimulationPlotConfiguration config, boolean initialShowPoints) { - this.simulation = simulation; - this.config = config; - this.branchCount = simulation.getSimulatedData().getBranchCount(); - - this.chart = ChartFactory.createXYLineChart( - //// Simulated flight - /*title*/simulation.getName(), - /*xAxisLabel*/null, - /*yAxisLabel*/null, - /*dataset*/null, - /*orientation*/PlotOrientation.VERTICAL, - /*legend*/false, - /*tooltips*/true, - /*urls*/false - ); - - chart.getTitle().setFont(new Font("Dialog", Font.BOLD, 23)); - chart.setBackgroundPaint(new Color(240, 240, 240)); - this.legendItems = new LegendItems(); - LegendTitle legend = new LegendTitle(legendItems); - legend.setMargin(new RectangleInsets(1.0, 1.0, 1.0, 1.0)); - legend.setFrame(BlockBorder.NONE); - legend.setBackgroundPaint(new Color(240, 240, 240)); - legend.setPosition(RectangleEdge.BOTTOM); - chart.addSubtitle(legend); - - chart.addSubtitle(new TextTitle(config.getName())); - - // Fill the auto-selections based on first branch selected. - FlightDataBranch mainBranch = simulation.getSimulatedData().getBranch(0); - this.filled = config.fillAutoAxes(mainBranch); - - // Compute the axes based on the min and max value of all branches - SimulationPlotConfiguration plotConfig = filled.clone(); - plotConfig.fitAxes(simulation.getSimulatedData().getBranches()); - List minMaxAxes = plotConfig.getAllAxes(); - - // Create the data series for both axes - XYSeriesCollection[] data = new XYSeriesCollection[2]; - data[0] = new XYSeriesCollection(); - data[1] = new XYSeriesCollection(); - - // Get the domain axis type - final FlightDataType domainType = filled.getDomainAxisType(); - final Unit domainUnit = filled.getDomainAxisUnit(); - if (domainType == null) { - throw new IllegalArgumentException("Domain axis type not specified."); - } - - // Get plot length (ignore trailing NaN's) - int typeCount = filled.getTypeCount(); - - int seriesCount = 0; - - // 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++) { - // Get info - FlightDataType type = filled.getType(i); - if (Objects.equals(type.getName(), "Position upwind")) { - type = FlightDataType.TYPE_POSITION_Y; - } - Unit unit = filled.getUnit(i); - int axis = filled.getAxis(i); - String name = getLabel(type, unit); - - List seriesNames = Util.generateSeriesLabels(simulation); - - // Populate data for each branch. - - // The primary branch (branchIndex = 0) is easy since all the data is copied - { - int branchIndex = 0; - FlightDataBranch thisBranch = simulation.getSimulatedData().getBranch(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++) { - FlightDataBranch thisBranch = simulation.getSimulatedData().getBranch(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); - - // 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))); - } - data[axis].addSeries(series); - } - - // Update axis label - if (axisLabel[axis] == null) - axisLabel[axis] = type.getName(); - else - axisLabel[axis] += "; " + type.getName(); - } - - // Add the data and formatting to the plot - XYPlot plot = chart.getXYPlot(); - plot.setDomainPannable(true); - plot.setRangePannable(true); - - // Plot appearance - plot.setBackgroundPaint(Color.white); - plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0)); - plot.setRangeGridlinesVisible(true); - plot.setRangeGridlinePaint(Color.lightGray); - - plot.setDomainGridlinesVisible(true); - plot.setDomainGridlinePaint(Color.lightGray); - - int cumulativeSeriesCount = 0; - - for (int axisno = 0; axisno < 2; axisno++) { - // Check whether axis has any data - if (data[axisno].getSeriesCount() > 0) { - // Create and set axis - double min = minMaxAxes.get(axisno).getMinValue(); - double max = minMaxAxes.get(axisno).getMaxValue(); - - NumberAxis axis = new PresetNumberAxis(min, max); - axis.setLabel(axisLabel[axisno]); - plot.setRangeAxis(axisno, axis); - axis.setLabelFont(new Font("Dialog", Font.BOLD, 14)); - - double domainMin = data[axisno].getDomainLowerBound(true); - double domainMax = data[axisno].getDomainUpperBound(true); - - plot.setDomainAxis(new PresetNumberAxis(domainMin, domainMax)); - - // Custom tooltip generator - int finalAxisno = axisno; - StandardXYToolTipGenerator tooltipGenerator = new StandardXYToolTipGenerator() { - @Override - public String generateToolTip(XYDataset dataset, int series, int item) { - XYSeriesCollection collection = data[finalAxisno]; - if (collection.getSeriesCount() == 0) { - return null; - } - XYSeries ser = collection.getSeries(series); - String name = ser.getDescription(); - - // Extract the unit from the last part of the series description, between parenthesis - Matcher m = Pattern.compile(".*\\((.*?)\\)").matcher(name); - String unitY = ""; - if (m.find()) { - unitY = m.group(1); - } - String unitX = domainUnit.getUnit(); - - double dataY = dataset.getYValue(series, item); - double dataX = dataset.getXValue(series, item); - - return formatSampleTooltip(name, dataX, unitX, dataY, unitY, item); - } - }; - - // Add data and map to the axis - plot.setDataset(axisno, data[axisno]); - ModifiedXYItemRenderer r = new ModifiedXYItemRenderer(branchCount); - renderers.add(r); - r.setDefaultToolTipGenerator(tooltipGenerator); - plot.setRenderer(axisno, r); - r.setDefaultShapesVisible(initialShowPoints); - r.setDefaultShapesFilled(true); - - // Set colors for all series of the current axis - for (int seriesIndex = 0; seriesIndex < data[axisno].getSeriesCount(); seriesIndex++) { - int colorIndex = cumulativeSeriesCount + seriesIndex; - r.setSeriesPaint(seriesIndex, Util.getPlotColor(colorIndex)); - - Stroke lineStroke = new BasicStroke(PLOT_STROKE_WIDTH); - r.setSeriesStroke(seriesIndex, lineStroke); - } - - // Update the cumulative count for the next axis - cumulativeSeriesCount += data[axisno].getSeriesCount(); - - // Now we pull the colors for the legend. - for (int j = 0; j < data[axisno].getSeriesCount(); j += branchCount) { - String name = data[axisno].getSeries(j).getDescription(); - this.legendItems.lineLabels.add(name); - Paint linePaint = r.lookupSeriesPaint(j); - this.legendItems.linePaints.add(linePaint); - Shape itemShape = r.lookupSeriesShape(j); - this.legendItems.pointShapes.add(itemShape); - Stroke lineStroke = r.getSeriesStroke(j); - this.legendItems.lineStrokes.add(lineStroke); - } - - plot.mapDatasetToRangeAxis(axisno, axisno); - } - } - - plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit)); - plot.addDomainMarker(new ValueMarker(0)); - plot.addRangeMarker(new ValueMarker(0)); - - plot.getDomainAxis().setLabelFont(new Font("Dialog", Font.BOLD, 14)); - - // Create list of events to show (combine event too close to each other) - this.eventList = buildEventInfo(); - - // Create the event markers - drawDomainMarkers(-1); - - errorAnnotations = new ErrorAnnotationSet(branchCount); - } - - JFreeChart getJFreeChart() { - return chart; - } - - private String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx, boolean addYValue) { - String ord_end = "th"; // Ordinal number ending (1'st', 2'nd'...) - if (sampleIdx % 10 == 1) { - ord_end = "st"; - } else if (sampleIdx % 10 == 2) { - ord_end = "nd"; - } else if (sampleIdx % 10 == 3) { - ord_end = "rd"; - } - - DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false); - DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false); - - StringBuilder sb = new StringBuilder(); - sb.append(String.format("" + - "%s
", dataName)); - - if (addYValue) { - sb.append(String.format("Y: %s %s
", df_y.format(dataY), unitY)); - } - - sb.append(String.format("X: %s %s
" + - "%d%s sample" + - "", df_x.format(dataX), unitX, sampleIdx, ord_end)); - - return sb.toString(); - } - - private String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx) { - return formatSampleTooltip(dataName, dataX, unitX, dataY, unitY, sampleIdx, true); - } - - private String formatSampleTooltip(String dataName, double dataX, String unitX, int sampleIdx) { - return formatSampleTooltip(dataName, dataX, unitX, 0, "", sampleIdx, false); - } - - private String getLabel(FlightDataType type, Unit unit) { - String name = type.getName(); - if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) && - !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0) - name += " (" + unit.getUnit() + ")"; - return name; - } - /** * Draw the domain markers for a certain branch. Draws all the markers if the branch is -1. * @param branch branch to draw, or -1 to draw all @@ -549,7 +255,7 @@ public class SimulationPlot { LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range); // Image annotations are not supported on the right-side axis // TODO: LOW: Can this be achieved by JFreeChart? - if (filled.getAxis(index) != Util.PlotAxisSelection.LEFT.getValue()) { + if (filledConfig.getAxis(index) != Util.PlotAxisSelection.LEFT.getValue()) { continue; } @@ -611,203 +317,6 @@ public class SimulationPlot { } - private static class LegendItems implements LegendItemSource { - - private final List lineLabels = new ArrayList<>(); - private final List linePaints = new ArrayList<>(); - private final List lineStrokes = new ArrayList<>(); - private final List pointShapes = new ArrayList<>(); - - @Override - public LegendItemCollection getLegendItems() { - LegendItemCollection c = new LegendItemCollection(); - int i = 0; - for (String s : lineLabels) { - String label = s; - String description = s; - String toolTipText = null; - String urlText = null; - boolean shapeIsVisible = false; - Shape shape = pointShapes.get(i); - boolean shapeIsFilled = false; - Paint fillPaint = linePaints.get(i); - boolean shapeOutlineVisible = false; - Paint outlinePaint = linePaints.get(i); - Stroke outlineStroke = lineStrokes.get(i); - boolean lineVisible = true; - Stroke lineStroke = lineStrokes.get(i); - Paint linePaint = linePaints.get(i); - - Shape legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); - - LegendItem result = new LegendItem(label, description, toolTipText, - urlText, shapeIsVisible, shape, shapeIsFilled, fillPaint, - shapeOutlineVisible, outlinePaint, outlineStroke, lineVisible, - legendLine, lineStroke, linePaint); - - c.add(result); - i++; - } - return c; - } - } - - /** - * A modification to the standard renderer that renders the domain marker - * labels vertically instead of horizontally. - * - * This class is special in that it assumes the data series are added to it - * in a specific order. In particular they must be "by parameter by stage". - * Assuming that three series are chosen (a, b, c) and the rocket has 2 stages, the - * data series are added in this order: - * - * series a stage 0 - * series a stage 1 - * series b stage 0 - * series b stage 1 - * series c stage 0 - * series c stage 1 - */ - private static class ModifiedXYItemRenderer extends XYLineAndShapeRenderer { - - private final int branchCount; - - private ModifiedXYItemRenderer(int branchCount) { - this.branchCount = branchCount; - } - - @Override - public Paint lookupSeriesPaint(int series) { - return super.lookupSeriesPaint(series / branchCount); - } - - @Override - public Paint lookupSeriesFillPaint(int series) { - return super.lookupSeriesFillPaint(series / branchCount); - } - - @Override - public Paint lookupSeriesOutlinePaint(int series) { - return super.lookupSeriesOutlinePaint(series / branchCount); - } - - @Override - public Stroke lookupSeriesStroke(int series) { - return super.lookupSeriesStroke(series / branchCount); - } - - @Override - public Stroke lookupSeriesOutlineStroke(int series) { - return super.lookupSeriesOutlineStroke(series / branchCount); - } - - @Override - public Shape lookupSeriesShape(int series) { - return DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE[series % branchCount % DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE.length]; - } - - @Override - public Shape lookupLegendShape(int series) { - return DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE[series % branchCount % DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE.length]; - } - - @Override - public Font lookupLegendTextFont(int series) { - return super.lookupLegendTextFont(series / branchCount); - } - - @Override - public Paint lookupLegendTextPaint(int series) { - return super.lookupLegendTextPaint(series / branchCount); - } - - @Override - public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis, - Marker marker, Rectangle2D dataArea) { - - if (!(marker instanceof ValueMarker)) { - // Use parent for all others - super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea); - return; - } - - /* - * Draw the normal marker, but with rotated text. - * Copied from the overridden method. - */ - ValueMarker vm = (ValueMarker) marker; - double value = vm.getValue(); - Range range = domainAxis.getRange(); - if (!range.contains(value)) { - return; - } - - double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge()); - - PlotOrientation orientation = plot.getOrientation(); - Line2D line = null; - if (orientation == PlotOrientation.HORIZONTAL) { - line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v); - } else { - line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY()); - } - - final Composite originalComposite = g2.getComposite(); - g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker - .getAlpha())); - g2.setPaint(marker.getPaint()); - g2.setStroke(marker.getStroke()); - g2.draw(line); - - String label = marker.getLabel(); - RectangleAnchor anchor = marker.getLabelAnchor(); - if (label != null) { - Font labelFont = marker.getLabelFont(); - g2.setFont(labelFont); - g2.setPaint(marker.getLabelPaint()); - Point2D coordinates = calculateDomainMarkerTextAnchorPoint(g2, - orientation, dataArea, line.getBounds2D(), marker - .getLabelOffset(), LengthAdjustmentType.EXPAND, anchor); - - // Changed: - TextAnchor textAnchor = TextAnchor.TOP_RIGHT; - TextUtilities.drawRotatedString(label, g2, (float) coordinates.getX() + 2, - (float) coordinates.getY(), textAnchor, - -Math.PI / 2, textAnchor); - } - g2.setComposite(originalComposite); - } - - } - - private static class PresetNumberAxis extends NumberAxis { - private final double min; - private final double max; - - public PresetNumberAxis(double min, double max) { - this.min = min; - this.max = max; - autoAdjustRange(); - } - - @Override - protected void autoAdjustRange() { - this.setRange(min, max); - } - - @Override - public void setRange(Range range) { - double lowerValue = range.getLowerBound(); - double upperValue = range.getUpperBound(); - if (lowerValue < min || upperValue > max) { - // Don't blow past the min & max of the range this is important to keep - // panning constrained within the current bounds. - return; - } - super.setRange(new Range(lowerValue, upperValue)); - } - - } private static class EventDisplayInfo { int stage; diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java index 88f14d643..258ad08cf 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java +++ b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotConfiguration.java @@ -12,7 +12,6 @@ import info.openrocket.core.util.ArrayList; public class SimulationPlotConfiguration extends PlotConfiguration { - private static final Translator trans = Application.getTranslator(); public static final SimulationPlotConfiguration[] DEFAULT_CONFIGURATIONS; diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotDialog.java b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotDialog.java index 5732c3c64..7062ac4a4 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotDialog.java +++ b/swing/src/main/java/info/openrocket/swing/gui/plot/SimulationPlotDialog.java @@ -58,8 +58,7 @@ public class SimulationPlotDialog extends JDialog { this.setModalityType(ModalityType.DOCUMENT_MODAL); final boolean initialShowPoints = Application.getPreferences().getBoolean(ApplicationPreferences.PLOT_SHOW_POINTS, false); - - final SimulationPlot myPlot = new SimulationPlot(simulation, config, initialShowPoints); + final SimulationPlot myPlot = SimulationPlot.create(simulation, config, initialShowPoints); // Create the dialog JPanel panel = new JPanel(new MigLayout("fill, hidemode 3","[]","[grow][]")); @@ -115,7 +114,7 @@ public class SimulationPlotDialog extends JDialog { //// Add series selection box ArrayList stages = new ArrayList<>(); stages.add(trans.get("PlotDialog.StageDropDown.allStages")); - stages.addAll(Util.generateSeriesLabels(simulation)); + stages.addAll(Util.generateSeriesLabels(simulation.getSimulatedData().getBranches())); final JComboBox stageSelection = new JComboBox<>(stages.toArray(new String[0])); stageSelection.addItemListener(new ItemListener() { 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 fe9e1438a..599bba12c 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 @@ -7,6 +7,8 @@ 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; import info.openrocket.core.startup.Application; public abstract class Util { @@ -52,26 +54,25 @@ public abstract class Util { new Color(85, 107, 47), }; - public static List generateSeriesLabels( Simulation simulation ) { - int size = simulation.getSimulatedData().getBranchCount(); - ArrayList stages = new ArrayList<>(size); - // we need to generate unique strings for each of the branches. Since the branch names are based + public static > List generateSeriesLabels(List branches) { + List stages = 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 (int i = 0; i < simulation.getSimulatedData().getBranchCount(); i++) { - stages.add(simulation.getSimulatedData().getBranch(i).getName()); + for (B branch : branches) { + stages.add(branch.getName()); } // check for duplicates: - for( int i = 0; i< stages.size(); i++ ) { + for (int i = 0; i < stages.size(); i++) { String stagename = stages.get(i); int numberDuplicates = Collections.frequency(stages, stagename); - if ( numberDuplicates > 1 ) { + if (numberDuplicates > 1) { int index = i; int count = 1; - while( count <= numberDuplicates ) { - stages.set(index, stagename + "(" + count + ")" ); + while (count <= numberDuplicates) { + stages.set(index, stagename + "(" + count + ")"); count ++; - for( index++; index < stages.size() && !stagename.equals(stages.get(index)); index++ ); + for (index++; index < stages.size() && !stagename.equals(stages.get(index)); index++); } } } diff --git a/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationExportPanel.java b/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationExportPanel.java index 7476bdcad..c27d53eff 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationExportPanel.java +++ b/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationExportPanel.java @@ -77,7 +77,7 @@ public class SimulationExportPanel extends CSVExportPanel { trans.get("SimExpPan.checkbox.ttip.Incflightevents")); //// Add series selection box - ArrayList stages = new ArrayList<>(Util.generateSeriesLabels(simulation)); + ArrayList stages = new ArrayList<>(Util.generateSeriesLabels(simulation.getSimulatedData().getBranches())); if (stages.size() > 1) { final JComboBox stageSelection = new JComboBox<>(stages.toArray(new String[0]));