diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties
index 540d2d2a6..0270b37c8 100644
--- a/core/resources/l10n/messages.properties
+++ b/core/resources/l10n/messages.properties
@@ -720,6 +720,11 @@ simplotpanel.RIGHT_NAME = Right
simplotpanel.CUSTOM = Custom
SimulationPlotPanel.error.noPlotSelected = Please add one or more variables to plot on the Y-axis.
SimulationPlotPanel.error.noPlotSelected.title = Nothing to plot
+simplotpanel.MarkerStyle.lbl.MarkerStyle = Marker style:
+simplotpanel.MarkerStyle.lbl.MarkerStyle.ttip = Style of the flight event marker (how it's drawn in the simulation plot)
+simplotpanel.MarkerStyle.btn.VerticalMarker = Vertical line
+simplotpanel.MarkerStyle.btn.Icon = Icon
+simplotpanel.MarkerStyle.OnlyInTime = Only available for time domain, other domains only support icon markers
! Component add buttons
compaddbuttons.AxialStage = Stage
diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java
index 26b9b1095..f9c31bd82 100644
--- a/core/src/net/sf/openrocket/startup/Preferences.java
+++ b/core/src/net/sf/openrocket/startup/Preferences.java
@@ -76,6 +76,7 @@ public abstract class Preferences implements ChangeSource {
public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors";
private static final String AUTO_OPEN_LAST_DESIGN = "AUTO_OPEN_LAST_DESIGN";
private static final String OPEN_LEFTMOST_DESIGN_TAB = "OPEN_LEFTMOST_DESIGN_TAB";
+ public static final String MARKER_STYLE_ICON = "MARKER_STYLE_ICON";
private static final String SHOW_MARKERS = "SHOW_MARKERS";
private static final String SHOW_ROCKSIM_FORMAT_WARNING = "SHOW_ROCKSIM_FORMAT_WARNING";
diff --git a/core/src/net/sf/openrocket/util/LinearInterpolator.java b/core/src/net/sf/openrocket/util/LinearInterpolator.java
index e16726bb5..8a9e786ac 100644
--- a/core/src/net/sf/openrocket/util/LinearInterpolator.java
+++ b/core/src/net/sf/openrocket/util/LinearInterpolator.java
@@ -82,7 +82,7 @@ public class LinearInterpolator implements Cloneable {
if ( y1 != null ) {
// Wow, x was a key in the map. Such luck.
- return y1.doubleValue();
+ return y1;
}
// we now know that x is not in the map, so we need to find the lower and higher keys.
@@ -96,16 +96,16 @@ public class LinearInterpolator implements Cloneable {
Double firstKey = sortMap.firstKey();
// x is smaller than the first entry in the map.
- if ( x < firstKey.doubleValue() ) {
+ if ( x < firstKey) {
y1 = sortMap.get(firstKey);
- return y1.doubleValue();
+ return y1;
}
// floor key is the largest key smaller than x - since we have at least one key,
// and x>=firstKey, we know that floorKey != null.
Double floorKey = sortMap.subMap(firstKey, x).lastKey();
- x1 = floorKey.doubleValue();
+ x1 = floorKey;
y1 = sortMap.get(floorKey);
// Now we need to find the key that is greater or equal to x
@@ -113,16 +113,16 @@ public class LinearInterpolator implements Cloneable {
// Check if x is bigger than all the entries.
if ( tailMap.isEmpty() ) {
- return y1.doubleValue();
+ return y1;
}
Double ceilKey = tailMap.firstKey();
// Check if x is bigger than all the entries.
if ( ceilKey == null ) {
- return y1.doubleValue();
+ return y1;
}
- x2 = ceilKey.doubleValue();
+ x2 = ceilKey;
y2 = sortMap.get(ceilKey);
return (x - x1)/(x2-x1) * (y2-y1) + y1;
diff --git a/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java b/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java
index 1714fbd6f..57f45c09c 100644
--- a/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java
+++ b/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java
@@ -20,9 +20,12 @@ import java.util.regex.Pattern;
import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.gui.simulation.SimulationPlotPanel;
+import net.sf.openrocket.gui.util.SwingPreferences;
import net.sf.openrocket.simulation.FlightDataBranch;
import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.startup.Preferences;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.LinearInterpolator;
@@ -65,6 +68,8 @@ import org.jfree.ui.TextAnchor;
*/
@SuppressWarnings("serial")
public class SimulationPlot {
+ private static final SwingPreferences preferences = (SwingPreferences) Application.getPreferences();
+
private static final float PLOT_STROKE_WIDTH = 1.5f;
@@ -79,7 +84,7 @@ public class SimulationPlot {
private final LegendItems legendItems;
- int branchCount;
+ private int branchCount;
void setShowPoints(boolean showPoints) {
for (ModifiedXYItemRenderer r : renderers) {
@@ -286,32 +291,19 @@ public class SimulationPlot {
}
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 unit_y = "";
+ String unitY = "";
if (m.find()) {
- unit_y = m.group(1);
+ unitY = m.group(1);
}
- String unit_x = domainUnit.getUnit();
- String ord_end = "th"; // Ordinal number ending (1'st', 2'nd'...)
- if (item % 10 == 1)
- ord_end = "st";
- else if (item % 10 == 2)
- ord_end = "nd";
- else if (item % 10 == 3)
- ord_end = "rd";
- double data_y = dataset.getYValue(series, item);
- double data_x = dataset.getXValue(series, item);
- DecimalFormat df_y = DecimalFormatter.df(data_y, 2, false);
- DecimalFormat df_x = DecimalFormatter.df(data_x, 2, false);
- return String.format("" +
- "%s
" +
- "Y: %s %s
" +
- "X: %s %s
" +
- "%d%s sample" +
- "",
- name, df_y.format(data_y), unit_y,
- df_x.format(data_x), unit_x, item, ord_end);
+ String unitX = domainUnit.getUnit();
+
+ double dataY = dataset.getYValue(series, item);
+ double dataX = dataset.getXValue(series, item);
+
+ return formatSampleTooltip(name, dataX, unitX, dataY, unitY, item);
}
};
@@ -364,6 +356,42 @@ public class SimulationPlot {
return chart;
}
+ private String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx, boolean addYValue) {
+ String ord_end = "th"; // Ordinal number ending (1'st', 2'nd'...)
+ if (sampleIdx % 10 == 1) {
+ ord_end = "st";
+ } else if (sampleIdx % 10 == 2) {
+ ord_end = "nd";
+ } else if (sampleIdx % 10 == 3) {
+ ord_end = "rd";
+ }
+
+ DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false);
+ DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("" +
+ "%s
", dataName));
+
+ if (addYValue) {
+ sb.append(String.format("Y: %s %s
", df_y.format(dataY), unitY));
+ }
+
+ sb.append(String.format("X: %s %s
" +
+ "%d%s sample" +
+ "", df_x.format(dataX), unitX, sampleIdx, ord_end));
+
+ return sb.toString();
+ }
+
+ private String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx) {
+ return formatSampleTooltip(dataName, dataX, unitX, dataY, unitY, sampleIdx, true);
+ }
+
+ private String formatSampleTooltip(String dataName, double dataX, String unitX, int sampleIdx) {
+ return formatSampleTooltip(dataName, dataX, unitX, 0, "", sampleIdx, false);
+ }
+
private String getLabel(FlightDataType type, Unit unit) {
String name = type.getName();
if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) &&
@@ -372,129 +400,166 @@ public class SimulationPlot {
return name;
}
- private void drawDomainMarkers(int stage) {
+ /**
+ * 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
+ */
+ private void drawDomainMarkers(int branch) {
XYPlot plot = chart.getXYPlot();
- FlightDataBranch mainBranch = simulation.getSimulatedData().getBranch(0);
+ FlightDataBranch dataBranch = simulation.getSimulatedData().getBranch(Math.max(branch, 0));
- // Clear existing domain markers
+ // Clear existing domain markers and annotations
plot.clearDomainMarkers();
+ plot.clearAnnotations();
- // Construct domain marker lists collapsing based on time.
+ // Store flight event information
+ List eventTimes = new ArrayList<>();
+ List eventLabels = new ArrayList<>();
+ List eventColors = new ArrayList<>();
+ List eventImages = new ArrayList<>();
- List eventTimes = new ArrayList();
- List eventLabels = new ArrayList();
- List eventColors = new ArrayList();
- List eventImages = new ArrayList();
- {
- HashSet typeSet = new HashSet();
- double prevTime = -100;
- String text = null;
- Color color = null;
- Image image = null;
- for (EventDisplayInfo info : eventList) {
- if (stage >= 0 && stage != info.stage) {
- continue;
+ // Plot the markers
+ if (config.getDomainAxisType() == FlightDataType.TYPE_TIME && !preferences.getBoolean(Preferences.MARKER_STYLE_ICON, false)) {
+ fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages);
+ plotVerticalLineMarkers(plot, eventTimes, eventLabels, eventColors);
+
+ } 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);
+ dataBranch = simulation.getSimulatedData().getBranch(b);
+ plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages);
+ eventTimes.clear();
+ eventLabels.clear();
+ eventColors.clear();
+ eventImages.clear();
}
+ } else {
+ fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages);
+ plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages);
+ }
+ }
+ }
- double t = info.time;
- FlightEvent.Type type = info.event.getType();
+ private void fillEventLists(int branch, List eventTimes, List eventLabels,
+ List eventColors, List eventImages) {
+ HashSet typeSet = new HashSet<>();
+ double prevTime = -100;
+ String text = null;
+ Color color = null;
+ Image image = null;
+ for (EventDisplayInfo info : eventList) {
+ if (branch >= 0 && branch != info.stage) {
+ continue;
+ }
- if (Math.abs(t - prevTime) <= 0.05) {
+ double t = info.time;
+ FlightEvent.Type type = info.event.getType();
- if (!typeSet.contains(type)) {
- text = text + ", " + type.toString();
- color = EventGraphics.getEventColor(type);
- image = EventGraphics.getEventImage(type);
- typeSet.add(type);
- }
-
- } else {
-
- if (text != null) {
- eventTimes.add(prevTime);
- eventLabels.add(text);
- eventColors.add(color);
- eventImages.add(image);
- }
- prevTime = t;
- text = type.toString();
+ if (Math.abs(t - prevTime) <= 0.05) {
+ if (!typeSet.contains(type)) {
+ text = text + ", " + type.toString();
color = EventGraphics.getEventColor(type);
image = EventGraphics.getEventImage(type);
- typeSet.clear();
typeSet.add(type);
}
+ } else {
+ if (text != null) {
+ eventTimes.add(prevTime);
+ eventLabels.add(text);
+ eventColors.add(color);
+ eventImages.add(image);
+ }
+ prevTime = t;
+ text = type.toString();
+ color = EventGraphics.getEventColor(type);
+ image = EventGraphics.getEventImage(type);
+ typeSet.clear();
+ typeSet.add(type);
}
- if (text != null) {
- eventTimes.add(prevTime);
- eventLabels.add(text);
- eventColors.add(color);
- eventImages.add(image);
+
+ }
+ if (text != null) {
+ eventTimes.add(prevTime);
+ eventLabels.add(text);
+ eventColors.add(color);
+ eventImages.add(image);
+ }
+ }
+
+ private static void plotVerticalLineMarkers(XYPlot plot, List eventTimes, List eventLabels, List eventColors) {
+ 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);
+ Color color = eventColors.get(i);
+
+ ValueMarker m = new ValueMarker(t);
+ m.setLabel(event);
+ m.setPaint(color);
+ m.setLabelPaint(color);
+ m.setAlpha(0.7f);
+ m.setLabelFont(new Font("Dialog", Font.PLAIN, 13));
+ plot.addDomainMarker(m);
+
+ if (t > plot.getDomainAxis().getUpperBound() - markerWidth) {
+ plot.setDomainAxis(new PresetNumberAxis(plot.getDomainAxis().getLowerBound(), t + markerWidth));
}
}
+ }
- // Plot the markers
- if (config.getDomainAxisType() == FlightDataType.TYPE_TIME) {
- double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound();
+ private void plotIconMarkers(XYPlot plot, FlightDataBranch dataBranch, List eventTimes,
+ List eventLabels, List eventImages) {
+ List time = dataBranch.get(FlightDataType.TYPE_TIME);
+ List domain = dataBranch.get(config.getDomainAxisType());
- // Domain time is plotted as vertical markers
- for (int i = 0; i < eventTimes.size(); i++) {
- double t = eventTimes.get(i);
- String event = eventLabels.get(i);
- Color color = eventColors.get(i);
+ LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain);
- ValueMarker m = new ValueMarker(t);
- m.setLabel(event);
- m.setPaint(color);
- m.setLabelPaint(color);
- m.setAlpha(0.7f);
- m.setLabelFont(new Font("Dialog", Font.PLAIN, 13));
- plot.addDomainMarker(m);
+ for (int i = 0; i < eventTimes.size(); i++) {
+ double t = eventTimes.get(i);
+ Image image = eventImages.get(i);
- if (t > plot.getDomainAxis().getUpperBound() - markerWidth) {
- plot.setDomainAxis(new PresetNumberAxis(plot.getDomainAxis().getLowerBound(), t + markerWidth));
- }
+ if (image == null) {
+ continue;
}
- } else {
+ double xcoord = domainInterpolator.getValue(t);
- // Other domains are plotted as image annotations
- List time = mainBranch.get(FlightDataType.TYPE_TIME);
- List domain = mainBranch.get(config.getDomainAxisType());
+ for (int index = 0; index < config.getTypeCount(); index++) {
+ FlightDataType type = config.getType(index);
+ List range = dataBranch.get(type);
- LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain);
-
- for (int i = 0; i < eventTimes.size(); i++) {
- double t = eventTimes.get(i);
- String event = eventLabels.get(i);
- Image image = eventImages.get(i);
-
- if (image == null)
+ 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) != SimulationPlotPanel.LEFT) {
continue;
-
- double xcoord = domainInterpolator.getValue(t);
- for (int index = 0; index < config.getTypeCount(); index++) {
- FlightDataType type = config.getType(index);
- List range = mainBranch.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 (filled.getAxis(index) != SimulationPlotPanel.LEFT) {
- continue;
- }
-
- double ycoord = rangeInterpolator.getValue(t);
-
- // Convert units
- xcoord = config.getDomainAxisUnit().toUnit(xcoord);
- ycoord = config.getUnit(index).toUnit(ycoord);
-
- XYImageAnnotation annotation =
- new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER);
- annotation.setToolTipText(event);
- plot.addAnnotation(annotation);
}
+
+ double ycoord = rangeInterpolator.getValue(t);
+
+ // Convert units
+ 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);
}
}
}
diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java
index da516379c..c28cc606c 100644
--- a/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java
+++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java
@@ -9,12 +9,15 @@ import java.util.Arrays;
import java.util.EnumSet;
import javax.swing.BorderFactory;
+import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
+import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
+import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
@@ -29,11 +32,13 @@ import net.sf.openrocket.gui.plot.PlotConfiguration;
import net.sf.openrocket.gui.plot.SimulationPlotDialog;
import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.gui.util.Icons;
+import net.sf.openrocket.gui.util.SwingPreferences;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.simulation.FlightDataBranch;
import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.FlightEvent;
import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.startup.Preferences;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.util.Utils;
import net.sf.openrocket.gui.widgets.SelectColorButton;
@@ -47,6 +52,7 @@ public class SimulationPlotPanel extends JPanel {
private static final long serialVersionUID = -2227129713185477998L;
private static final Translator trans = Application.getTranslator();
+ private static final SwingPreferences preferences = (SwingPreferences) Application.getPreferences();
// TODO: LOW: Should these be somewhere else?
public static final int AUTO = -1;
@@ -201,7 +207,7 @@ public class SimulationPlotPanel extends JPanel {
typeSelectorPanel = new JPanel(new MigLayout("gapy rel"));
JScrollPane scroll = new JScrollPane(typeSelectorPanel);
- this.add(scroll, "spany 2, height 10px, wmin 400lp, grow 100, gapright para");
+ this.add(scroll, "spany 3, height 10px, wmin 400lp, grow 100, gapright para");
//// Flight events
@@ -244,10 +250,46 @@ public class SimulationPlotPanel extends JPanel {
eventTableModel.fireTableDataChanged();
}
});
- this.add(button, "gapleft para, gapright para, growx, sizegroup buttons, wrap para");
-
-
+ this.add(button, "gapleft para, gapright para, growx, sizegroup buttons, wrap");
+
+ //// Style event marker
+ JLabel styleEventMarker = new JLabel(trans.get("simplotpanel.MarkerStyle.lbl.MarkerStyle"));
+ JRadioButton radioVerticalMarker = new JRadioButton(trans.get("simplotpanel.MarkerStyle.btn.VerticalMarker"));
+ JRadioButton radioIcon = new JRadioButton(trans.get("simplotpanel.MarkerStyle.btn.Icon"));
+ ButtonGroup bg = new ButtonGroup();
+ bg.add(radioVerticalMarker);
+ bg.add(radioIcon);
+
+ boolean useIcon = preferences.getBoolean(Preferences.MARKER_STYLE_ICON, false);
+ if (useIcon) {
+ radioIcon.setSelected(true);
+ } else {
+ radioVerticalMarker.setSelected(true);
+ }
+
+ radioIcon.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (modifying > 0)
+ return;
+ preferences.putBoolean(Preferences.MARKER_STYLE_ICON, radioIcon.isSelected());
+ }
+ });
+
+ domainTypeSelector.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ updateStyleEventWidgets(styleEventMarker, radioVerticalMarker, radioIcon);
+ }
+ });
+ updateStyleEventWidgets(styleEventMarker, radioVerticalMarker, radioIcon);
+
+ this.add(styleEventMarker, "split 3, growx");
+ this.add(radioVerticalMarker);
+ this.add(radioIcon, "wrap para");
+
+
//// New Y axis plot type
button = new SelectColorButton(trans.get("simplotpanel.but.NewYaxisplottype"));
button.addActionListener(new ActionListener() {
@@ -322,6 +364,19 @@ public class SimulationPlotPanel extends JPanel {
*/
updatePlots();
}
+
+ private void updateStyleEventWidgets(JLabel styleEventMarker, JRadioButton radioVerticalMarker, JRadioButton radioIcon) {
+ if (modifying > 0)
+ return;
+ FlightDataType type = (FlightDataType) domainTypeSelector.getSelectedItem();
+ boolean isTime = type == FlightDataType.TYPE_TIME;
+ styleEventMarker.setEnabled(isTime);
+ radioVerticalMarker.setEnabled(isTime);
+ radioIcon.setEnabled(isTime);
+ styleEventMarker.setToolTipText(isTime ? trans.get("simplotpanel.MarkerStyle.lbl.MarkerStyle.ttip") : trans.get("simplotpanel.MarkerStyle.OnlyInTime"));
+ radioVerticalMarker.setToolTipText(isTime ? null : trans.get("simplotpanel.MarkerStyle.OnlyInTime"));
+ radioIcon.setToolTipText(isTime ? null : trans.get("simplotpanel.MarkerStyle.OnlyInTime"));
+ }
public JDialog doPlot(Window parent) {
if (configuration.getTypeCount() == 0) {