Merge remote-tracking branch 'origin/unstable' into add-open-airframe-warning

This commit is contained in:
JoePfeiffer 2023-01-03 18:46:26 -07:00
commit b3d2b92edd
34 changed files with 320 additions and 133 deletions

View File

@ -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
@ -1150,6 +1155,8 @@ NoseConeCfg.tab.General = General
NoseConeCfg.tab.ttip.General = General properties
NoseConeCfg.tab.Shoulder = Shoulder
NoseConeCfg.tab.ttip.Shoulder = Shoulder properties
NoseConeCfg.checkbox.Flip = Flip to tail cone
NoseConeCfg.checkbox.Flip.ttip = Flips the nose cone direction to a tail cone.
! ParachuteConfig
Parachute.Parachute = Parachute

View File

@ -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";

View File

@ -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;

View File

@ -24,6 +24,8 @@ import static org.junit.Assert.assertSame;
* Tests to verify that simulations contain all the expected flight events.
*/
public class FlightEventsTest extends BaseTestCase {
private static final double EPSILON = 0.005;
/**
* Tests for a single stage design.
*/
@ -66,7 +68,7 @@ public class FlightEventsTest extends BaseTestCase {
// Test that the event times are correct
for (int i = 0; i < expectedEventTimes.length; i++) {
assertEquals(" Flight type " + expectedEventTypes[i] + " has wrong time",
expectedEventTimes[i], eventList.get(i).getTime(), 0.002);
expectedEventTimes[i], eventList.get(i).getTime(), EPSILON);
}
@ -142,7 +144,7 @@ public class FlightEventsTest extends BaseTestCase {
// Test that the event times are correct
for (int i = 0; i < expectedEventTimes.length; i++) {
assertEquals(" Flight type " + expectedEventTypes[i] + " has wrong time",
expectedEventTimes[i], eventList.get(i).getTime(), 0.002);
expectedEventTimes[i], eventList.get(i).getTime(), EPSILON);
}
// Test that the event sources are correct

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,8 @@
package net.sf.openrocket.gui.main;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
@ -18,6 +20,35 @@ public final class ExampleDesignFileAction extends JMenu {
*/
private final BasicFrame parent;
/**
* Order in which the example files should be displayed in the menu.
* A null items means there should be a separator.
* <p>
* NOTE: update this list if you add a new example file, or update the name of an existing one!!.
*/
private static final String[] exampleFileOrder = {
// Examples of basic rockets
"A simple model rocket",
"Two-stage rocket",
"Three-stage rocket",
"TARC payload rocket",
"Tube fin rocket",
null,
// Examples demonstrating complex rocket features
"Airstart timing",
"Chute release",
"Dual parachute deployment",
"Clustered motors",
"Parallel booster staging",
"Pods--airframes and winglets",
"Pods--powered with recovery deployment",
null,
// Examples demonstrating customized functionality
"Presets",
"Simulation extensions",
"Simulation extensions and scripting"
};
/**
* Constructor.
*
@ -38,11 +69,37 @@ public final class ExampleDesignFileAction extends JMenu {
private void updateMenu() {
removeAll();
ExampleDesignFile[] examples = ExampleDesignFile.getExampleDesigns();
List<JMenuItem> itemList = new ArrayList<>();
// First create the menu items
for (ExampleDesignFile file : examples) {
Action action = createAction(file);
action.putValue(Action.NAME, file.toString());
JMenuItem menuItem = new JMenuItem(action);
add(menuItem);
itemList.add(menuItem);
}
// Then add them according to their order
for (String s : exampleFileOrder) {
if (s == null) {
addSeparator();
} else {
for (JMenuItem item : itemList) {
if (item.getText().equals(s)) {
add(item);
itemList.remove(item);
break;
}
}
}
}
// Add the remaining (unordered) items to the end
if (itemList.size() > 0) {
addSeparator();
for (JMenuItem item : itemList) {
add(item);
}
}
}

View File

@ -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("<html>" +
"<b><i>%s</i></b><br>" +
"Y: %s %s<br>" +
"X: %s %s<br>" +
"%d<sup>%s</sup> sample" +
"</html>",
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("<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) &&
@ -372,27 +400,58 @@ 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<Double> eventTimes = new ArrayList<>();
List<String> eventLabels = new ArrayList<>();
List<Color> eventColors = new ArrayList<>();
List<Image> eventImages = new ArrayList<>();
List<Double> eventTimes = new ArrayList<Double>();
List<String> eventLabels = new ArrayList<String>();
List<Color> eventColors = new ArrayList<Color>();
List<Image> eventImages = new ArrayList<Image>();
{
HashSet<FlightEvent.Type> typeSet = new HashSet<FlightEvent.Type>();
// 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);
}
}
}
private void fillEventLists(int branch, List<Double> eventTimes, List<String> eventLabels,
List<Color> eventColors, List<Image> eventImages) {
HashSet<FlightEvent.Type> 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) {
if (branch >= 0 && branch != info.stage) {
continue;
}
@ -400,7 +459,6 @@ public class SimulationPlot {
FlightEvent.Type type = info.event.getType();
if (Math.abs(t - prevTime) <= 0.05) {
if (!typeSet.contains(type)) {
text = text + ", " + type.toString();
color = EventGraphics.getEventColor(type);
@ -409,7 +467,6 @@ public class SimulationPlot {
}
} else {
if (text != null) {
eventTimes.add(prevTime);
eventLabels.add(text);
@ -433,11 +490,10 @@ public class SimulationPlot {
}
}
// Plot the markers
if (config.getDomainAxisType() == FlightDataType.TYPE_TIME) {
private static void plotVerticalLineMarkers(XYPlot plot, List<Double> eventTimes, List<String> eventLabels, List<Color> eventColors) {
double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound();
// Domain time is plotted as vertical markers
// 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);
@ -455,27 +511,28 @@ public class SimulationPlot {
plot.setDomainAxis(new PresetNumberAxis(plot.getDomainAxis().getLowerBound(), t + markerWidth));
}
}
}
} else {
// Other domains are plotted as image annotations
List<Double> time = mainBranch.get(FlightDataType.TYPE_TIME);
List<Double> domain = mainBranch.get(config.getDomainAxisType());
private void plotIconMarkers(XYPlot plot, FlightDataBranch dataBranch, List<Double> eventTimes,
List<String> eventLabels, List<Image> eventImages) {
List<Double> time = dataBranch.get(FlightDataType.TYPE_TIME);
List<Double> domain = dataBranch.get(config.getDomainAxisType());
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)
if (image == null) {
continue;
}
double xcoord = domainInterpolator.getValue(t);
for (int index = 0; index < config.getTypeCount(); index++) {
FlightDataType type = config.getType(index);
List<Double> range = mainBranch.get(type);
List<Double> range = dataBranch.get(type);
LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range);
// Image annotations are not supported on the right-side axis
@ -490,14 +547,22 @@ public class SimulationPlot {
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<Double> 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(event);
annotation.setToolTipText(tooltipText);
plot.addAnnotation(annotation);
}
}
}
}
private List<EventDisplayInfo> buildEventInfo() {
ArrayList<EventDisplayInfo> eventList = new ArrayList<EventDisplayInfo>();

View File

@ -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,9 +250,45 @@ 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"));
@ -323,6 +365,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) {
JOptionPane.showMessageDialog(SimulationPlotPanel.this,