Add SIM_WARN events to plots.

Note that this required a lot of rewrite to the code adding event icons
This commit is contained in:
JoePfeiffer 2024-09-23 06:53:22 -06:00
parent 67f6aa191a
commit 38a3772eb0
3 changed files with 183 additions and 73 deletions

View File

@ -183,6 +183,13 @@ public class FlightEvent implements Comparable<FlightEvent> {
// finally, sort on event type // finally, sort on event type
return this.type.ordinal() - o.type.ordinal(); return this.type.ordinal() - o.type.ordinal();
} }
public boolean equals(FlightEvent o) {
if ((this.type == Type.SIM_WARN) && (o.type == Type.SIM_WARN))
return ((Warning)(this.data)).equals((Warning)(o.data));
return this.equals(0);
}
@Override @Override
public String toString() { public String toString() {

View File

@ -101,8 +101,8 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
// Create the data series for both axes // Create the data series for both axes
this.data = new XYSeriesCollection[2]; this.data = new XYSeriesCollection[2];
this.data[0] = new XYSeriesCollection(); this.data[Util.PlotAxisSelection.LEFT.getValue()] = new XYSeriesCollection();
this.data[1] = new XYSeriesCollection(); this.data[Util.PlotAxisSelection.RIGHT.getValue()] = new XYSeriesCollection();
// Fill the auto-selections based on first branch selected. // Fill the auto-selections based on first branch selected.
this.filledConfig = config.fillAutoAxes(mainBranch); this.filledConfig = config.fillAutoAxes(mainBranch);
@ -278,7 +278,7 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
protected List<XYSeries> createSeriesForType(int dataIndex, int startIndex, T type, Unit unit, B branch, protected List<XYSeries> createSeriesForType(int dataIndex, int startIndex, T type, Unit unit, B branch,
int branchIdx, String branchName, String baseName) { int branchIdx, String branchName, String baseName) {
// Default implementation for regular DataBranch // Default implementation for regular DataBranch
MetadataXYSeries series = new MetadataXYSeries(startIndex, false, true, branchIdx, unit.getUnit(), branchName, baseName); MetadataXYSeries series = new MetadataXYSeries(startIndex, false, true, branchIdx, dataIndex, unit.getUnit(), branchName, baseName);
List<Double> plotx = branch.get(filledConfig.getDomainAxisType()); List<Double> plotx = branch.get(filledConfig.getDomainAxisType());
List<Double> ploty = branch.get(type); List<Double> ploty = branch.get(type);
@ -327,7 +327,7 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
return formatSampleTooltip(dataName, dataX, unitX, 0, "", sampleIdx, false); return formatSampleTooltip(dataName, dataX, unitX, 0, "", sampleIdx, false);
} }
private String getOrdinalEnding(int n) { protected String getOrdinalEnding(int n) {
if (n % 100 == 11 || n % 100 == 12 || n % 100 == 13) return "th"; if (n % 100 == 11 || n % 100 == 12 || n % 100 == 13) return "th";
return switch (n % 10) { return switch (n % 10) {
case 1 -> "st"; case 1 -> "st";
@ -540,20 +540,27 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
protected static class MetadataXYSeries extends XYSeries { protected static class MetadataXYSeries extends XYSeries {
private final int branchIdx; private final int branchIdx;
private final int dataIdx;
private final String unit; private final String unit;
private final String branchName; private final String branchName;
private String baseName; private String baseName;
public MetadataXYSeries(Comparable key, boolean autoSort, boolean allowDuplicateXValues, int branchIdx, String unit, public MetadataXYSeries(Comparable key, boolean autoSort, boolean allowDuplicateXValues, int branchIdx, int dataIdx, String unit,
String branchName, String baseName) { String branchName, String baseName) {
super(key, autoSort, allowDuplicateXValues); super(key, autoSort, allowDuplicateXValues);
this.branchIdx = branchIdx; this.branchIdx = branchIdx;
this.dataIdx = dataIdx;
this.unit = unit; this.unit = unit;
this.branchName = branchName; this.branchName = branchName;
this.baseName = baseName; this.baseName = baseName;
updateDescription(); updateDescription();
} }
public MetadataXYSeries(Comparable key, boolean autoSort, boolean allowDuplicateXValues, int branchIdx, String unit,
String branchName, String baseName) {
this(key, autoSort, allowDuplicateXValues, branchIdx, -1, unit, branchName, baseName);
}
public String getUnit() { public String getUnit() {
return unit; return unit;
} }
@ -562,6 +569,10 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
return branchIdx; return branchIdx;
} }
public int getDataIdx() {
return dataIdx;
}
public String getBranchName() { public String getBranchName() {
return branchName; return branchName;
} }

View File

@ -5,6 +5,7 @@ import java.awt.Font;
import java.awt.Image; import java.awt.Image;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.text.DecimalFormat;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -13,11 +14,13 @@ import java.util.Set;
import info.openrocket.core.document.Simulation; import info.openrocket.core.document.Simulation;
import info.openrocket.core.logging.SimulationAbort; import info.openrocket.core.logging.SimulationAbort;
import info.openrocket.core.logging.Warning;
import info.openrocket.core.simulation.FlightDataBranch; import info.openrocket.core.simulation.FlightDataBranch;
import info.openrocket.core.simulation.FlightDataType; import info.openrocket.core.simulation.FlightDataType;
import info.openrocket.core.simulation.FlightEvent; import info.openrocket.core.simulation.FlightEvent;
import info.openrocket.core.preferences.ApplicationPreferences; import info.openrocket.core.preferences.ApplicationPreferences;
import info.openrocket.core.util.LinearInterpolator; import info.openrocket.core.util.LinearInterpolator;
import info.openrocket.swing.utils.DecimalFormatter;
import org.jfree.chart.annotations.XYImageAnnotation; import org.jfree.chart.annotations.XYImageAnnotation;
import org.jfree.chart.annotations.XYTitleAnnotation; import org.jfree.chart.annotations.XYTitleAnnotation;
@ -31,6 +34,8 @@ import org.jfree.chart.ui.VerticalAlignment;
import org.jfree.chart.ui.RectangleAnchor; import org.jfree.chart.ui.RectangleAnchor;
import org.jfree.chart.ui.RectangleEdge; import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets; import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, SimulationPlotConfiguration> { public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, SimulationPlotConfiguration> {
@ -106,32 +111,30 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
plot.clearAnnotations(); plot.clearAnnotations();
// Store flight event information // Store flight event information
List<Double> eventTimes = new ArrayList<>();
List<String> eventLabels = new ArrayList<>();
List<Color> eventColors = new ArrayList<>(); List<Color> eventColors = new ArrayList<>();
List<Image> eventImages = new ArrayList<>(); List<Image> eventImages = new ArrayList<>();
List<Set<FlightEvent>> eventSets = new ArrayList<>();
// Plot the markers // Plot the markers
if (config.getDomainAxisType() == FlightDataType.TYPE_TIME && !preferences.getBoolean(ApplicationPreferences.MARKER_STYLE_ICON, false)) { if (config.getDomainAxisType() == FlightDataType.TYPE_TIME && !preferences.getBoolean(ApplicationPreferences.MARKER_STYLE_ICON, false)) {
fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages); fillEventLists(branch, eventColors, eventImages, eventSets);
plotVerticalLineMarkers(plot, eventTimes, eventLabels, eventColors); plotVerticalLineMarkers(plot, eventColors, eventSets);
} else { // Other domains are plotted as image annotations } else { // Other domains are plotted as image annotations
if (branch == -1) { if (branch == -1) {
// For icon markers, we need to do the plotting separately, otherwise you can have icon markers from e.g. // For icon markers, we need to do the plotting separately, otherwise you can have icon markers from e.g.
// branch 1 be plotted on branch 0 // branch 1 be plotted on branch 0
for (int b = 0; b < simulation.getSimulatedData().getBranchCount(); b++) { for (int b = 0; b < simulation.getSimulatedData().getBranchCount(); b++) {
fillEventLists(b, eventTimes, eventLabels, eventColors, eventImages); fillEventLists(b, eventColors, eventImages, eventSets);
dataBranch = simulation.getSimulatedData().getBranch(b); dataBranch = simulation.getSimulatedData().getBranch(b);
plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages); plotIconMarkers(plot, simulation, b, eventImages, eventSets);
eventTimes.clear(); eventSets = new ArrayList<>();
eventLabels.clear();
eventColors.clear(); eventColors.clear();
eventImages.clear(); eventImages.clear();
} }
} else { } else {
fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages); fillEventLists(branch, eventColors, eventImages, eventSets);
plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages); plotIconMarkers(plot, simulation, branch, eventImages, eventSets);
} }
} }
} }
@ -160,72 +163,86 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
newBranchName.append(" + ").append(allBranches.get(i).getName()); newBranchName.append(" + ").append(allBranches.get(i).getName());
} }
} }
return newBranchName + ": " + ser.getBaseName(); return newBranchName + ": " + ser.getBaseName();
} }
private void fillEventLists(int branch, List<Double> eventTimes, List<String> eventLabels, private void fillEventLists(int branch,
List<Color> eventColors, List<Image> eventImages) { List<Color> eventColors, List<Image> eventImages, List<Set<FlightEvent>> eventSets) {
Set<FlightEvent> eventSet = new HashSet<>();
Set<FlightEvent.Type> typeSet = new HashSet<>(); Set<FlightEvent.Type> typeSet = new HashSet<>();
double prevTime = -100; double prevTime = -100;
String text = null;
Color color = null; Color color = null;
Image image = null; Image image = null;
int maxOrdinal = -1; int maxOrdinal = -1;
for (EventDisplayInfo info : eventList) { for (EventDisplayInfo info : eventList) {
if (branch >= 0 && branch != info.stage) { if (branch >= 0 && branch != info.stage) {
continue; continue;
} }
double t = info.time; double t = info.time;
FlightEvent.Type type = info.event.getType(); FlightEvent event = info.event;
FlightEvent.Type type = event.getType();
if (Math.abs(t - prevTime) <= 0.05) { if (Math.abs(t - prevTime) <= 0.05) {
if (!typeSet.contains(type)) { if (!typeSet.contains(type)) {
text = text + ", " + type.toString();
if (type.ordinal() > maxOrdinal) { if (type.ordinal() > maxOrdinal) {
color = EventGraphics.getEventColor(info.event); color = EventGraphics.getEventColor(event);
image = EventGraphics.getEventImage(info.event); image = EventGraphics.getEventImage(event);
maxOrdinal = type.ordinal(); maxOrdinal = type.ordinal();
} }
typeSet.add(type); typeSet.add(type);
eventSet.add(event);
} }
} else { } else {
if (text != null) { if (!eventSet.isEmpty()) {
eventTimes.add(prevTime);
eventLabels.add(text);
eventColors.add(color); eventColors.add(color);
eventImages.add(image); eventImages.add(image);
eventSets.add(eventSet);
} }
prevTime = t; prevTime = t;
text = type.toString(); color = EventGraphics.getEventColor(event);
color = EventGraphics.getEventColor(info.event); image = EventGraphics.getEventImage(event);
image = EventGraphics.getEventImage(info.event);
typeSet.clear(); typeSet.clear();
typeSet.add(type); typeSet.add(type);
eventSet = new HashSet<>();
eventSet.add(event);
maxOrdinal = type.ordinal(); maxOrdinal = type.ordinal();
} }
} }
if (text != null) { if (!eventSet.isEmpty()) {
eventTimes.add(prevTime);
eventLabels.add(text);
eventColors.add(color); eventColors.add(color);
eventImages.add(image); eventImages.add(image);
eventSets.add(eventSet);
} }
} }
private static void plotVerticalLineMarkers(XYPlot plot, List<Double> eventTimes, List<String> eventLabels, List<Color> eventColors) { private static String constructEventLabels(Set<FlightEvent> events) {
String text = "";
for (FlightEvent event : events) {
if (text != "") {
text += ", ";
}
text += event.getType().toString();
}
return text;
}
private static void plotVerticalLineMarkers(XYPlot plot, List<Color> eventColors, List<Set<FlightEvent>> eventSets) {
double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound(); double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound();
// Domain time is plotted as vertical lines // Domain time is plotted as vertical lines
for (int i = 0; i < eventTimes.size(); i++) { for (int i = 0; i < eventSets.size(); i++) {
double t = eventTimes.get(i); Set<FlightEvent> events = eventSets.get(i);
String event = eventLabels.get(i); double t = ((FlightEvent)events.toArray()[0]).getTime();
String eventLabel = constructEventLabels(events);
Color color = eventColors.get(i); Color color = eventColors.get(i);
ValueMarker m = new ValueMarker(t); ValueMarker m = new ValueMarker(t);
m.setLabel(event); m.setLabel(eventLabel);
m.setPaint(color); m.setPaint(color);
m.setLabelPaint(color); m.setLabelPaint(color);
m.setAlpha(0.7f); m.setAlpha(0.7f);
@ -238,58 +255,133 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
} }
} }
private void plotIconMarkers(XYPlot plot, FlightDataBranch dataBranch, List<Double> eventTimes, private void plotIconMarkers(XYPlot plot, Simulation simulation, int branch, List<Image> eventImages, List<Set<FlightEvent>> eventSets) {
List<String> eventLabels, List<Image> eventImages) {
FlightDataBranch dataBranch = simulation.getSimulatedData().getBranch(branch);
List<Double> time = dataBranch.get(FlightDataType.TYPE_TIME); List<Double> time = dataBranch.get(FlightDataType.TYPE_TIME);
List<Double> domain = dataBranch.get(config.getDomainAxisType()); List<Double> domain = dataBranch.get(config.getDomainAxisType());
LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain); LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain);
String xName = config.getDomainAxisType().getName();
for (int i = 0; i < eventTimes.size(); i++) { List<Axis> minMaxAxes = filledConfig.getAllAxes();
double t = eventTimes.get(i);
Image image = eventImages.get(i);
if (image == null) { for (int axisno = 0; axisno < data.length; axisno++) {
continue; // Image annotations are drawn using the data space defined by the left axis, so
} // the position of annotations on the right axis need to be mapped to the left axis
// dataspace.
double minLeft = minMaxAxes.get(0).getMinValue();
double maxLeft = minMaxAxes.get(0).getMaxValue();
double xcoord = domainInterpolator.getValue(t); double minThis = minMaxAxes.get(axisno).getMinValue();
double maxThis = minMaxAxes.get(axisno).getMaxValue();
for (int index = 0; index < config.getDataCount(); index++) { double slope = (maxLeft - minLeft)/(maxThis - minThis);
FlightDataType type = config.getType(index); double intercept = (maxThis * minLeft - maxLeft * minThis)/(maxThis - minThis);
List<Double> range = dataBranch.get(type);
XYSeriesCollection collection = data[axisno];
for (XYSeries series : (List<XYSeries>)(collection.getSeries())) {
Plot.MetadataXYSeries metaSeries = (Plot.MetadataXYSeries) series;
LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range); if (metaSeries.getBranchIdx() != branch) {
// Image annotations are not supported on the right-side axis
// TODO: LOW: Can this be achieved by JFreeChart?
if (filledConfig.getAxis(index) != Util.PlotAxisSelection.LEFT.getValue()) {
continue; continue;
} }
double ycoord = rangeInterpolator.getValue(t); int dataTypeIdx = metaSeries.getDataIdx();
if (!Double.isNaN(ycoord)) { FlightDataType type = config.getType(dataTypeIdx);
// Convert units String yName = type.toString();
List<Double> range = dataBranch.get(type);
LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range);
for (int i = 0; i < eventSets.size(); i++) {
Set<FlightEvent> events = eventSets.get(i);
double t = ((FlightEvent)events.toArray()[0]).getTime();
Image image = eventImages.get(i);
if (image == null) {
continue;
}
double xcoord = domainInterpolator.getValue(t);
double ycoord = rangeInterpolator.getValue(t);
xcoord = config.getDomainAxisUnit().toUnit(xcoord); xcoord = config.getDomainAxisUnit().toUnit(xcoord);
ycoord = config.getUnit(index).toUnit(ycoord); ycoord = config.getUnit(dataTypeIdx).toUnit(ycoord);
// Get the sample index of the flight event. Because this can be an interpolation between two samples, if (!Double.isNaN(ycoord)) {
// take the closest sample. // Get the sample index of the flight event. Because this can be an interpolation between two samples,
final int sampleIdx; // take the closest sample.
Optional<Double> closestSample = time.stream() final int sampleIdx;
.min(Comparator.comparingDouble(sample -> Math.abs(sample - t))); Optional<Double> closestSample = time.stream()
sampleIdx = closestSample.map(time::indexOf).orElse(-1); .min(Comparator.comparingDouble(sample -> Math.abs(sample - t)));
sampleIdx = closestSample.map(time::indexOf).orElse(-1);
String tooltipText = formatSampleTooltip(eventLabels.get(i), xcoord, config.getDomainAxisUnit().getUnit(), sampleIdx) ;
// Convert units
XYImageAnnotation annotation = String unitY = metaSeries.getUnit();
new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER); String unitX = config.getDomainAxisUnit().getUnit();
annotation.setToolTipText(tooltipText);
plot.addAnnotation(annotation);
String tooltipText = formatEventTooltip(getNameBasedOnIdxAndSeries(metaSeries, sampleIdx), events,
xName, xcoord, unitX,
yName, ycoord, unitY,
sampleIdx) ;
double yloc = slope * ycoord + intercept;
XYImageAnnotation annotation =
new XYImageAnnotation(xcoord, yloc, image, RectangleAnchor.CENTER);
annotation.setToolTipText(tooltipText);
plot.addAnnotation(annotation);
}
} }
} }
} }
} }
protected String formatEventTooltip(String dataName, Set<FlightEvent> events, String xName, double dataX, String unitX, String yName, double dataY, String unitY, int sampleIdx) {
String ord_end = getOrdinalEnding(sampleIdx);
DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false);
DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false);
StringBuilder sb = new StringBuilder();
// start tooltip
sb.append("<html>");
// Branchname(s)
sb.append(String.format("<b><i>%s</i></b><br>", dataName));
// Any events?
if ((null != events) && (events.size() != 0)) {
// Pass through and collect any warnings
for (FlightEvent event : events) {
if (event.getType() == FlightEvent.Type.SIM_WARN) {
sb.append("<b><i>Warning: " + ((Warning) event.getData()).toString() + "</b></i><br>");
}
}
// Now pass through and collect the other events
String eventStr = "";
for (FlightEvent event : events) {
if (event.getType() != FlightEvent.Type.SIM_WARN) {
if (eventStr != "") {
eventStr = eventStr + ", ";
}
eventStr = eventStr + event.getType();
}
}
sb.append(eventStr + "<br>");
}
sb.append(String.format("%s: %s %s<br>", xName, df_x.format(dataX), unitX));
sb.append(String.format("%s: %s %s<br>", yName, df_y.format(dataY), unitY));
sb.append(String.format("%d<sup>%s</sup> sample", sampleIdx, ord_end));
// End tooltip
sb.append("</html>");
return sb.toString();
}
private List<EventDisplayInfo> buildEventInfo() { private List<EventDisplayInfo> buildEventInfo() {
ArrayList<EventDisplayInfo> eventList = new ArrayList<>(); ArrayList<EventDisplayInfo> eventList = new ArrayList<>();