Refactor SimulationPlot to more generic Plot
This commit is contained in:
parent
49f3c12a70
commit
f21dd55452
@ -49,7 +49,7 @@ public abstract class DataBranch<T extends DataType> 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.");
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
529
swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java
Normal file
529
swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java
Normal file
@ -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<T extends DataType, B extends DataBranch<T>, C extends PlotConfiguration<T, B>> {
|
||||
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<ModifiedXYItemRenderer> renderers = new ArrayList<>();
|
||||
protected final LegendItems legendItems;
|
||||
protected final XYSeriesCollection[] data;
|
||||
protected final C filledConfig; // Configuration after using 'fillAutoAxes'
|
||||
private final List<B> allBranches;
|
||||
|
||||
protected final JFreeChart chart;
|
||||
|
||||
protected Plot(JFreeChart chart, B mainBranch, C config, List<B> 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<Axis> 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<Double> plotx = thisBranch.get(domainType);
|
||||
List<Double> ploty = thisBranch.get(type);
|
||||
XYSeries series = new XYSeries(seriesCount++, false, true);
|
||||
series.setDescription(name);
|
||||
int pointCount = plotx.size();
|
||||
for (int j = 0; j < pointCount; j++) {
|
||||
series.add(domainUnit.toUnit(plotx.get(j)), unit.toUnit(ploty.get(j)));
|
||||
}
|
||||
data[axis].addSeries(series);
|
||||
}
|
||||
// Secondary branches
|
||||
for (int branchIndex = 1; branchIndex < branchCount; branchIndex++) {
|
||||
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<Double> plotx = thisBranch.get(domainType);
|
||||
List<Double> ploty = thisBranch.get(type);
|
||||
|
||||
int pointCount = plotx.size();
|
||||
for (int j = 0; j < pointCount; j++) {
|
||||
series.add(domainUnit.toUnit(plotx.get(j)), unit.toUnit(ploty.get(j)));
|
||||
}
|
||||
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("<html>" +
|
||||
"<b><i>%s</i></b><br>", dataName));
|
||||
|
||||
if (addYValue) {
|
||||
sb.append(String.format("Y: %s %s<br>", df_y.format(dataY), unitY));
|
||||
}
|
||||
|
||||
sb.append(String.format("X: %s %s<br>" +
|
||||
"%d<sup>%s</sup> sample" +
|
||||
"</html>", 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<String> lineLabels = new ArrayList<>();
|
||||
protected final List<Paint> linePaints = new ArrayList<>();
|
||||
protected final List<Stroke> lineStrokes = new ArrayList<>();
|
||||
protected final List<Shape> 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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -512,7 +512,7 @@ public class PlotConfiguration<T extends DataType, B extends DataBranch<T>> impl
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <C extends PlotConfiguration<T, B>> C cloneConfiguration() {
|
||||
public <C extends PlotConfiguration<T, B>> C cloneConfiguration() {
|
||||
try {
|
||||
C copy = (C) super.clone();
|
||||
|
||||
|
@ -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<FlightDataType, FlightDataBranch, SimulationPlotConfiguration> {
|
||||
private final SimulationPlotConfiguration config;
|
||||
private final Simulation simulation;
|
||||
private final SimulationPlotConfiguration filled;
|
||||
|
||||
private final List<EventDisplayInfo> eventList;
|
||||
private final List<ModifiedXYItemRenderer> 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<FlightDataBranch> 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<Axis> 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<String> 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<Double> plotx = thisBranch.get(domainType);
|
||||
List<Double> ploty = thisBranch.get(type);
|
||||
XYSeries series = new XYSeries(seriesCount++, false, true);
|
||||
series.setDescription(name);
|
||||
int pointCount = plotx.size();
|
||||
for (int j = 0; j < pointCount; j++) {
|
||||
series.add(domainUnit.toUnit(plotx.get(j)), unit.toUnit(ploty.get(j)));
|
||||
}
|
||||
data[axis].addSeries(series);
|
||||
}
|
||||
// Secondary branches
|
||||
for (int branchIndex = 1; branchIndex < branchCount; branchIndex++) {
|
||||
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<Double> plotx = thisBranch.get(domainType);
|
||||
List<Double> ploty = thisBranch.get(type);
|
||||
|
||||
int pointCount = plotx.size();
|
||||
for (int j = 0; j < pointCount; j++) {
|
||||
series.add(domainUnit.toUnit(plotx.get(j)), unit.toUnit(ploty.get(j)));
|
||||
}
|
||||
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("<html>" +
|
||||
"<b><i>%s</i></b><br>", dataName));
|
||||
|
||||
if (addYValue) {
|
||||
sb.append(String.format("Y: %s %s<br>", df_y.format(dataY), unitY));
|
||||
}
|
||||
|
||||
sb.append(String.format("X: %s %s<br>" +
|
||||
"%d<sup>%s</sup> sample" +
|
||||
"</html>", 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<String> lineLabels = new ArrayList<>();
|
||||
private final List<Paint> linePaints = new ArrayList<>();
|
||||
private final List<Stroke> lineStrokes = new ArrayList<>();
|
||||
private final List<Shape> 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;
|
||||
|
@ -12,7 +12,6 @@ import info.openrocket.core.util.ArrayList;
|
||||
|
||||
|
||||
public class SimulationPlotConfiguration extends PlotConfiguration<FlightDataType, FlightDataBranch> {
|
||||
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
public static final SimulationPlotConfiguration[] DEFAULT_CONFIGURATIONS;
|
||||
|
@ -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<String> stages = new ArrayList<>();
|
||||
stages.add(trans.get("PlotDialog.StageDropDown.allStages"));
|
||||
stages.addAll(Util.generateSeriesLabels(simulation));
|
||||
stages.addAll(Util.generateSeriesLabels(simulation.getSimulatedData().getBranches()));
|
||||
|
||||
final JComboBox<String> stageSelection = new JComboBox<>(stages.toArray(new String[0]));
|
||||
stageSelection.addItemListener(new ItemListener() {
|
||||
|
@ -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<String> generateSeriesLabels( Simulation simulation ) {
|
||||
int size = simulation.getSimulatedData().getBranchCount();
|
||||
ArrayList<String> stages = new ArrayList<>(size);
|
||||
// we need to generate unique strings for each of the branches. Since the branch names are based
|
||||
public static <T extends DataType, B extends DataBranch<T>> List<String> generateSeriesLabels(List<B> branches) {
|
||||
List<String> 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++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ public class SimulationExportPanel extends CSVExportPanel<FlightDataType> {
|
||||
trans.get("SimExpPan.checkbox.ttip.Incflightevents"));
|
||||
|
||||
//// Add series selection box
|
||||
ArrayList<String> stages = new ArrayList<>(Util.generateSeriesLabels(simulation));
|
||||
ArrayList<String> stages = new ArrayList<>(Util.generateSeriesLabels(simulation.getSimulatedData().getBranches()));
|
||||
if (stages.size() > 1) {
|
||||
final JComboBox<String> stageSelection = new JComboBox<>(stages.toArray(new String[0]));
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user