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

@ -184,6 +184,13 @@ public class FlightEvent implements Comparable<FlightEvent> {
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
public String toString() {
return "FlightEvent[type=" + type.name() + ",time=" + time + ",source=" + source + ",data=" + String.valueOf(data) + "]";

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
this.data = new XYSeriesCollection[2];
this.data[0] = new XYSeriesCollection();
this.data[1] = new XYSeriesCollection();
this.data[Util.PlotAxisSelection.LEFT.getValue()] = new XYSeriesCollection();
this.data[Util.PlotAxisSelection.RIGHT.getValue()] = new XYSeriesCollection();
// Fill the auto-selections based on first branch selected.
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,
int branchIdx, String branchName, String baseName) {
// 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> 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);
}
private String getOrdinalEnding(int n) {
protected String getOrdinalEnding(int n) {
if (n % 100 == 11 || n % 100 == 12 || n % 100 == 13) return "th";
return switch (n % 10) {
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 {
private final int branchIdx;
private final int dataIdx;
private final String unit;
private final String branchName;
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) {
super(key, autoSort, allowDuplicateXValues);
this.branchIdx = branchIdx;
this.dataIdx = dataIdx;
this.unit = unit;
this.branchName = branchName;
this.baseName = baseName;
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() {
return unit;
}
@ -562,6 +569,10 @@ public abstract class Plot<T extends DataType, B extends DataBranch<T>, C extend
return branchIdx;
}
public int getDataIdx() {
return dataIdx;
}
public String getBranchName() {
return branchName;
}

View File

@ -5,6 +5,7 @@ import java.awt.Font;
import java.awt.Image;
import java.util.ArrayList;
import java.util.Comparator;
import java.text.DecimalFormat;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@ -13,11 +14,13 @@ import java.util.Set;
import info.openrocket.core.document.Simulation;
import info.openrocket.core.logging.SimulationAbort;
import info.openrocket.core.logging.Warning;
import info.openrocket.core.simulation.FlightDataBranch;
import info.openrocket.core.simulation.FlightDataType;
import info.openrocket.core.simulation.FlightEvent;
import info.openrocket.core.preferences.ApplicationPreferences;
import info.openrocket.core.util.LinearInterpolator;
import info.openrocket.swing.utils.DecimalFormatter;
import org.jfree.chart.annotations.XYImageAnnotation;
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.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
@SuppressWarnings("serial")
public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, SimulationPlotConfiguration> {
@ -106,32 +111,30 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
plot.clearAnnotations();
// Store flight event information
List<Double> eventTimes = new ArrayList<>();
List<String> eventLabels = new ArrayList<>();
List<Color> eventColors = new ArrayList<>();
List<Image> eventImages = new ArrayList<>();
List<Set<FlightEvent>> eventSets = new ArrayList<>();
// Plot the markers
if (config.getDomainAxisType() == FlightDataType.TYPE_TIME && !preferences.getBoolean(ApplicationPreferences.MARKER_STYLE_ICON, false)) {
fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages);
plotVerticalLineMarkers(plot, eventTimes, eventLabels, eventColors);
fillEventLists(branch, eventColors, eventImages, eventSets);
plotVerticalLineMarkers(plot, eventColors, eventSets);
} else { // Other domains are plotted as image annotations
if (branch == -1) {
// 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
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);
plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages);
eventTimes.clear();
eventLabels.clear();
plotIconMarkers(plot, simulation, b, eventImages, eventSets);
eventSets = new ArrayList<>();
eventColors.clear();
eventImages.clear();
}
} else {
fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages);
plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages);
fillEventLists(branch, eventColors, eventImages, eventSets);
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());
}
}
return newBranchName + ": " + ser.getBaseName();
}
private void fillEventLists(int branch, List<Double> eventTimes, List<String> eventLabels,
List<Color> eventColors, List<Image> eventImages) {
private void fillEventLists(int branch,
List<Color> eventColors, List<Image> eventImages, List<Set<FlightEvent>> eventSets) {
Set<FlightEvent> eventSet = new HashSet<>();
Set<FlightEvent.Type> typeSet = new HashSet<>();
double prevTime = -100;
String text = null;
Color color = null;
Image image = null;
int maxOrdinal = -1;
for (EventDisplayInfo info : eventList) {
if (branch >= 0 && branch != info.stage) {
continue;
}
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 (!typeSet.contains(type)) {
text = text + ", " + type.toString();
if (type.ordinal() > maxOrdinal) {
color = EventGraphics.getEventColor(info.event);
image = EventGraphics.getEventImage(info.event);
color = EventGraphics.getEventColor(event);
image = EventGraphics.getEventImage(event);
maxOrdinal = type.ordinal();
}
typeSet.add(type);
eventSet.add(event);
}
} else {
if (text != null) {
eventTimes.add(prevTime);
eventLabels.add(text);
if (!eventSet.isEmpty()) {
eventColors.add(color);
eventImages.add(image);
eventSets.add(eventSet);
}
prevTime = t;
text = type.toString();
color = EventGraphics.getEventColor(info.event);
image = EventGraphics.getEventImage(info.event);
color = EventGraphics.getEventColor(event);
image = EventGraphics.getEventImage(event);
typeSet.clear();
typeSet.add(type);
eventSet = new HashSet<>();
eventSet.add(event);
maxOrdinal = type.ordinal();
}
}
if (text != null) {
eventTimes.add(prevTime);
eventLabels.add(text);
if (!eventSet.isEmpty()) {
eventColors.add(color);
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();
// Domain time is plotted as vertical lines
for (int i = 0; i < eventTimes.size(); i++) {
double t = eventTimes.get(i);
String event = eventLabels.get(i);
for (int i = 0; i < eventSets.size(); i++) {
Set<FlightEvent> events = eventSets.get(i);
double t = ((FlightEvent)events.toArray()[0]).getTime();
String eventLabel = constructEventLabels(events);
Color color = eventColors.get(i);
ValueMarker m = new ValueMarker(t);
m.setLabel(event);
m.setLabel(eventLabel);
m.setPaint(color);
m.setLabelPaint(color);
m.setAlpha(0.7f);
@ -238,40 +255,60 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
}
}
private void plotIconMarkers(XYPlot plot, FlightDataBranch dataBranch, List<Double> eventTimes,
List<String> eventLabels, List<Image> eventImages) {
private void plotIconMarkers(XYPlot plot, Simulation simulation, int branch, List<Image> eventImages, List<Set<FlightEvent>> eventSets) {
FlightDataBranch dataBranch = simulation.getSimulatedData().getBranch(branch);
List<Double> time = dataBranch.get(FlightDataType.TYPE_TIME);
List<Double> domain = dataBranch.get(config.getDomainAxisType());
LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain);
String xName = config.getDomainAxisType().getName();
for (int i = 0; i < eventTimes.size(); i++) {
double t = eventTimes.get(i);
List<Axis> minMaxAxes = filledConfig.getAllAxes();
for (int axisno = 0; axisno < data.length; axisno++) {
// 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 minThis = minMaxAxes.get(axisno).getMinValue();
double maxThis = minMaxAxes.get(axisno).getMaxValue();
double slope = (maxLeft - minLeft)/(maxThis - minThis);
double intercept = (maxThis * minLeft - maxLeft * minThis)/(maxThis - minThis);
XYSeriesCollection collection = data[axisno];
for (XYSeries series : (List<XYSeries>)(collection.getSeries())) {
Plot.MetadataXYSeries metaSeries = (Plot.MetadataXYSeries) series;
if (metaSeries.getBranchIdx() != branch) {
continue;
}
int dataTypeIdx = metaSeries.getDataIdx();
FlightDataType type = config.getType(dataTypeIdx);
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);
for (int index = 0; index < config.getDataCount(); index++) {
FlightDataType type = config.getType(index);
List<Double> range = dataBranch.get(type);
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 (filledConfig.getAxis(index) != Util.PlotAxisSelection.LEFT.getValue()) {
continue;
}
double ycoord = rangeInterpolator.getValue(t);
if (!Double.isNaN(ycoord)) {
// Convert units
xcoord = config.getDomainAxisUnit().toUnit(xcoord);
ycoord = config.getUnit(index).toUnit(ycoord);
xcoord = config.getDomainAxisUnit().toUnit(xcoord);
ycoord = config.getUnit(dataTypeIdx).toUnit(ycoord);
if (!Double.isNaN(ycoord)) {
// Get the sample index of the flight event. Because this can be an interpolation between two samples,
// take the closest sample.
final int sampleIdx;
@ -279,16 +316,71 @@ public class SimulationPlot extends Plot<FlightDataType, FlightDataBranch, Simul
.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
String unitY = metaSeries.getUnit();
String unitX = config.getDomainAxisUnit().getUnit();
String tooltipText = formatEventTooltip(getNameBasedOnIdxAndSeries(metaSeries, sampleIdx), events,
xName, xcoord, unitX,
yName, ycoord, unitY,
sampleIdx) ;
double yloc = slope * ycoord + intercept;
XYImageAnnotation annotation =
new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER);
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() {
ArrayList<EventDisplayInfo> eventList = new ArrayList<>();