diff --git a/core/src/main/java/info/openrocket/core/simulation/FlightEvent.java b/core/src/main/java/info/openrocket/core/simulation/FlightEvent.java index 921dfdf05..37b703f5a 100644 --- a/core/src/main/java/info/openrocket/core/simulation/FlightEvent.java +++ b/core/src/main/java/info/openrocket/core/simulation/FlightEvent.java @@ -183,6 +183,13 @@ public class FlightEvent implements Comparable { // finally, sort on event type 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() { diff --git a/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java index ce9c532c3..d6730b401 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java +++ b/swing/src/main/java/info/openrocket/swing/gui/plot/Plot.java @@ -101,8 +101,8 @@ public abstract class Plot, 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, C extend protected List 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 plotx = branch.get(filledConfig.getDomainAxisType()); List ploty = branch.get(type); @@ -327,7 +327,7 @@ public abstract class Plot, 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, 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, C extend return branchIdx; } + public int getDataIdx() { + return dataIdx; + } + public String getBranchName() { return branchName; } 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 21dec76a6..041278370 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 @@ -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 { @@ -106,32 +111,30 @@ public class SimulationPlot extends Plot eventTimes = new ArrayList<>(); - List eventLabels = new ArrayList<>(); List eventColors = new ArrayList<>(); List eventImages = new ArrayList<>(); + List> 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 eventTimes, List eventLabels, - List eventColors, List eventImages) { + private void fillEventLists(int branch, + List eventColors, List eventImages, List> eventSets) { + Set eventSet = new HashSet<>(); Set 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 eventTimes, List eventLabels, List eventColors) { + private static String constructEventLabels(Set events) { + String text = ""; + + for (FlightEvent event : events) { + if (text != "") { + text += ", "; + } + text += event.getType().toString(); + } + + return text; + } + + private static void plotVerticalLineMarkers(XYPlot plot, List eventColors, List> 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 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,58 +255,133 @@ public class SimulationPlot extends Plot eventTimes, - List eventLabels, List eventImages) { + private void plotIconMarkers(XYPlot plot, Simulation simulation, int branch, List eventImages, List> eventSets) { + + FlightDataBranch dataBranch = simulation.getSimulatedData().getBranch(branch); + List time = dataBranch.get(FlightDataType.TYPE_TIME); List 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); - Image image = eventImages.get(i); + List minMaxAxes = filledConfig.getAllAxes(); - if (image == null) { - continue; - } + 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 xcoord = domainInterpolator.getValue(t); + double minThis = minMaxAxes.get(axisno).getMinValue(); + double maxThis = minMaxAxes.get(axisno).getMaxValue(); - for (int index = 0; index < config.getDataCount(); index++) { - FlightDataType type = config.getType(index); - List range = dataBranch.get(type); + double slope = (maxLeft - minLeft)/(maxThis - minThis); + double intercept = (maxThis * minLeft - maxLeft * minThis)/(maxThis - minThis); + + XYSeriesCollection collection = data[axisno]; + for (XYSeries series : (List)(collection.getSeries())) { + Plot.MetadataXYSeries metaSeries = (Plot.MetadataXYSeries) series; - 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()) { + if (metaSeries.getBranchIdx() != branch) { continue; } - double ycoord = rangeInterpolator.getValue(t); - if (!Double.isNaN(ycoord)) { - // Convert units + int dataTypeIdx = metaSeries.getDataIdx(); + FlightDataType type = config.getType(dataTypeIdx); + String yName = type.toString(); + List range = dataBranch.get(type); + LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range); + + for (int i = 0; i < eventSets.size(); i++) { + Set 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); - ycoord = config.getUnit(index).toUnit(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; - Optional closestSample = time.stream() - .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) ; - - XYImageAnnotation annotation = - new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER); - annotation.setToolTipText(tooltipText); - plot.addAnnotation(annotation); + 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; + Optional closestSample = time.stream() + .min(Comparator.comparingDouble(sample -> Math.abs(sample - t))); + sampleIdx = closestSample.map(time::indexOf).orElse(-1); + + // 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, yloc, image, RectangleAnchor.CENTER); + annotation.setToolTipText(tooltipText); + plot.addAnnotation(annotation); + } } } } } + protected String formatEventTooltip(String dataName, Set 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(""); + + // Branchname(s) + sb.append(String.format("%s
", 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("Warning: " + ((Warning) event.getData()).toString() + "
"); + } + } + + // 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 + "
"); + } + + sb.append(String.format("%s: %s %s
", xName, df_x.format(dataX), unitX)); + sb.append(String.format("%s: %s %s
", yName, df_y.format(dataY), unitY)); + sb.append(String.format("%d%s sample", sampleIdx, ord_end)); + + // End tooltip + sb.append(""); + + return sb.toString(); + } + private List buildEventInfo() { ArrayList eventList = new ArrayList<>();