diff --git a/.gitignore b/.gitignore
index 3b0822153..d7052274e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -97,3 +97,6 @@ openrocket.log
prime/*
swing/resources/datafiles/presets/system.ser
swing/resources/datafiles/presets
+
+# Eclipse - because some of us are stubborn
+.project
diff --git a/core/.classpath b/core/.classpath
index b21c5e70c..7b26f669d 100644
--- a/core/.classpath
+++ b/core/.classpath
@@ -27,7 +27,6 @@
-
diff --git a/core/OpenRocket Core.iml b/core/OpenRocket Core.iml
index b81bd7c08..ece044f8a 100644
--- a/core/OpenRocket Core.iml
+++ b/core/OpenRocket Core.iml
@@ -242,6 +242,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/core/lib/commons-lang3-3.12.0.jar b/core/lib/commons-lang3-3.12.0.jar
deleted file mode 100644
index 4d434a2a4..000000000
Binary files a/core/lib/commons-lang3-3.12.0.jar and /dev/null differ
diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties
index ebe011ef2..82be3c83b 100644
--- a/core/resources/l10n/messages.properties
+++ b/core/resources/l10n/messages.properties
@@ -536,6 +536,9 @@ simpanel.pop.plot = Plot / Export
simpanel.pop.run = Run
simpanel.pop.delete = Delete
simpanel.pop.duplicate = Duplicate
+simpanel.pop.exportToCSV = Export table as CSV file
+simpanel.pop.exportToCSV.save.dialog.title = Save as CSV file
+simpanel.dlg.no.simulation.table.rows = Simulation table has no entries\u2026 Please run a simulation first.
simpanel.checkbox.donotask = Do not ask me again
simpanel.lbl.defpref = You can change the default operation in the preferences.
simpanel.dlg.lbl.DeleteSim1 = Delete the selected simulations?
@@ -1440,6 +1443,7 @@ main.menu.file.quit = Quit
main.menu.file.quit.desc = Quit the program
main.menu.file.exportDecal = Save decal image\u2026
main.menu.file.exportDecal.desc = Save a decal from the current rocket design to a file for editing.
+main.menu.file.table.exportToCSV = Export simulations table as CSV file
main.menu.edit = Edit
main.menu.edit.desc = Rocket editing
diff --git a/core/resources/pix/icons/sim_table_export.png b/core/resources/pix/icons/sim_table_export.png
new file mode 100644
index 000000000..25b74d18f
Binary files /dev/null and b/core/resources/pix/icons/sim_table_export.png differ
diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java b/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java
index 629d3be25..11f4af969 100644
--- a/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java
+++ b/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java
@@ -20,7 +20,7 @@ import net.sf.openrocket.simulation.extension.SimulationExtension;
import net.sf.openrocket.simulation.extension.SimulationExtensionProvider;
import net.sf.openrocket.simulation.extension.impl.JavaCode;
import net.sf.openrocket.startup.Application;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
import com.google.inject.Key;
@@ -85,7 +85,7 @@ class SingleSimulationHandler extends AbstractElementHandler {
}
} else if (element.equals("listener") && content.trim().length() > 0) {
extensions.add(compatibilityExtension(content.trim()));
- } else if (element.equals("extension") && !StringUtil.isEmpty(attributes.get("extensionid"))) {
+ } else if (element.equals("extension") && !StringUtils.isEmpty(attributes.get("extensionid"))) {
String id = attributes.get("extensionid");
SimulationExtension extension = null;
Set extensionProviders = Application.getInjector().getInstance(new Key>() {
diff --git a/core/src/net/sf/openrocket/preset/loader/DoubleUnitColumnParser.java b/core/src/net/sf/openrocket/preset/loader/DoubleUnitColumnParser.java
index 00a0f4d61..1ffd75ea4 100644
--- a/core/src/net/sf/openrocket/preset/loader/DoubleUnitColumnParser.java
+++ b/core/src/net/sf/openrocket/preset/loader/DoubleUnitColumnParser.java
@@ -4,7 +4,7 @@ import net.sf.openrocket.preset.TypedKey;
import net.sf.openrocket.preset.TypedPropertyMap;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
public class DoubleUnitColumnParser extends BaseUnitColumnParser {
@@ -19,7 +19,7 @@ public class DoubleUnitColumnParser extends BaseUnitColumnParser {
@Override
protected void doParse(String columnData, String[] data, TypedPropertyMap props) {
try {
- if (StringUtil.isEmpty(columnData)) {
+ if (StringUtils.isEmpty(columnData)) {
return;
}
double value = Double.valueOf(columnData);
diff --git a/core/src/net/sf/openrocket/preset/loader/LineMaterialColumnParser.java b/core/src/net/sf/openrocket/preset/loader/LineMaterialColumnParser.java
index 9ba4ecb86..af29dcace 100644
--- a/core/src/net/sf/openrocket/preset/loader/LineMaterialColumnParser.java
+++ b/core/src/net/sf/openrocket/preset/loader/LineMaterialColumnParser.java
@@ -4,7 +4,7 @@ import net.sf.openrocket.database.Databases;
import net.sf.openrocket.material.Material;
import net.sf.openrocket.preset.TypedKey;
import net.sf.openrocket.preset.TypedPropertyMap;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
public class LineMaterialColumnParser extends BaseColumnParser {
@@ -22,7 +22,7 @@ public class LineMaterialColumnParser extends BaseColumnParser {
@Override
protected void doParse(String columnData, String[] data, TypedPropertyMap props) {
- if (StringUtil.isEmpty(columnData)) {
+ if (StringUtils.isEmpty(columnData)) {
return;
}
diff --git a/core/src/net/sf/openrocket/preset/loader/MassColumnParser.java b/core/src/net/sf/openrocket/preset/loader/MassColumnParser.java
index ba624758c..e2f06cca7 100644
--- a/core/src/net/sf/openrocket/preset/loader/MassColumnParser.java
+++ b/core/src/net/sf/openrocket/preset/loader/MassColumnParser.java
@@ -2,7 +2,7 @@ package net.sf.openrocket.preset.loader;
import net.sf.openrocket.preset.ComponentPreset;
import net.sf.openrocket.preset.TypedPropertyMap;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
/**
* Special DoubleUnitColumnParser for Mass column. Here we assume that if a mass of 0 is
@@ -18,7 +18,7 @@ public class MassColumnParser extends DoubleUnitColumnParser {
@Override
protected void doParse(String columnData, String[] data, TypedPropertyMap props) {
- if ( StringUtil.isEmpty(columnData) || "?".equals(columnData.trim())) {
+ if ( StringUtils.isEmpty(columnData) || "?".equals(columnData.trim())) {
return;
}
double d = Double.valueOf(columnData);
diff --git a/core/src/net/sf/openrocket/preset/loader/MaterialColumnParser.java b/core/src/net/sf/openrocket/preset/loader/MaterialColumnParser.java
index f8e274d2e..6633ccf44 100644
--- a/core/src/net/sf/openrocket/preset/loader/MaterialColumnParser.java
+++ b/core/src/net/sf/openrocket/preset/loader/MaterialColumnParser.java
@@ -5,7 +5,7 @@ import net.sf.openrocket.material.Material;
import net.sf.openrocket.preset.ComponentPreset;
import net.sf.openrocket.preset.TypedKey;
import net.sf.openrocket.preset.TypedPropertyMap;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
public class MaterialColumnParser extends BaseColumnParser {
@@ -27,7 +27,7 @@ public class MaterialColumnParser extends BaseColumnParser {
@Override
protected void doParse(String columnData, String[] data, TypedPropertyMap props) {
- if (StringUtil.isEmpty(columnData)) {
+ if (StringUtils.isEmpty(columnData)) {
return;
}
diff --git a/core/src/net/sf/openrocket/preset/loader/RockSimComponentFileLoader.java b/core/src/net/sf/openrocket/preset/loader/RockSimComponentFileLoader.java
index 4aac519f1..a2854f31c 100644
--- a/core/src/net/sf/openrocket/preset/loader/RockSimComponentFileLoader.java
+++ b/core/src/net/sf/openrocket/preset/loader/RockSimComponentFileLoader.java
@@ -18,7 +18,7 @@ import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.ArrayList;
import net.sf.openrocket.util.BugException;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
import com.opencsv.CSVReader;
/**
@@ -138,7 +138,7 @@ public abstract class RockSimComponentFileLoader {
if (data.length == 0) {
continue;
}
- if (data.length == 1 && StringUtil.isEmpty(data[0])) {
+ if (data.length == 1 && StringUtils.isEmpty(data[0])) {
continue;
}
parseData(data);
diff --git a/core/src/net/sf/openrocket/preset/loader/SurfaceMaterialColumnParser.java b/core/src/net/sf/openrocket/preset/loader/SurfaceMaterialColumnParser.java
index 690d6f455..6391ad87a 100644
--- a/core/src/net/sf/openrocket/preset/loader/SurfaceMaterialColumnParser.java
+++ b/core/src/net/sf/openrocket/preset/loader/SurfaceMaterialColumnParser.java
@@ -4,7 +4,7 @@ import net.sf.openrocket.database.Databases;
import net.sf.openrocket.material.Material;
import net.sf.openrocket.preset.TypedKey;
import net.sf.openrocket.preset.TypedPropertyMap;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
public class SurfaceMaterialColumnParser extends BaseColumnParser {
@@ -22,7 +22,7 @@ public class SurfaceMaterialColumnParser extends BaseColumnParser {
@Override
protected void doParse(String columnData, String[] data, TypedPropertyMap props) {
- if (StringUtil.isEmpty(columnData)) {
+ if (StringUtils.isEmpty(columnData)) {
return;
}
diff --git a/core/src/net/sf/openrocket/simulation/FlightDataType.java b/core/src/net/sf/openrocket/simulation/FlightDataType.java
index bea253490..950b0c728 100644
--- a/core/src/net/sf/openrocket/simulation/FlightDataType.java
+++ b/core/src/net/sf/openrocket/simulation/FlightDataType.java
@@ -10,7 +10,7 @@ import org.slf4j.LoggerFactory;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.UnitGroup;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
/**
* A class defining a storable simulation variable type. This class defined numerous ready
@@ -272,7 +272,7 @@ public class FlightDataType implements Comparable {
// found it from symbol
// if name was not given (empty string), can use the one we found
- if ( s == null || StringUtil.isEmpty(s)){
+ if ( s == null || StringUtils.isEmpty(s)){
s = type.getName();
}
if ( u == null ){
diff --git a/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java b/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java
index 0a1e4bd14..95bbf6726 100644
--- a/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java
+++ b/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java
@@ -11,7 +11,7 @@ import net.sf.openrocket.simulation.SimulationStatus;
import net.sf.openrocket.unit.FixedUnitGroup;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.ArrayList;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -227,7 +227,7 @@ public class CustomExpression implements Cloneable {
}
public boolean checkSymbol() {
- if (StringUtil.isEmpty(symbol)) {
+ if (StringUtils.isEmpty(symbol)) {
return false;
}
@@ -254,7 +254,7 @@ public class CustomExpression implements Cloneable {
}
public boolean checkName() {
- if (StringUtil.isEmpty(name)) {
+ if (StringUtils.isEmpty(name)) {
return false;
}
@@ -304,7 +304,7 @@ public class CustomExpression implements Cloneable {
* building the expression.
*/
public boolean checkExpression() {
- if (StringUtil.isEmpty(expression)) {
+ if (StringUtils.isEmpty(expression)) {
return false;
}
diff --git a/core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java b/core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java
index c2bfa66f6..e01dbfef7 100644
--- a/core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java
+++ b/core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java
@@ -14,13 +14,12 @@ import de.congrace.exp4j.ExpressionBuilder;
import de.congrace.exp4j.Variable;
import net.sf.openrocket.document.OpenRocketDocument;
import net.sf.openrocket.logging.Markers;
-import net.sf.openrocket.simulation.customexpression.CustomExpression;
import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.SimulationStatus;
import net.sf.openrocket.util.ArrayUtils;
import net.sf.openrocket.util.LinearInterpolator;
import net.sf.openrocket.util.MathUtil;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
public class RangeExpression extends CustomExpression {
private static final Logger log = LoggerFactory.getLogger(RangeExpression.class);
@@ -30,10 +29,10 @@ public class RangeExpression extends CustomExpression {
public RangeExpression(OpenRocketDocument doc, String startTime, String endTime, String variableType) {
super(doc);
- if (StringUtil.isEmpty(startTime)){
+ if (StringUtils.isEmpty(startTime)){
startTime = "0";
}
- if (StringUtil.isEmpty(endTime)){
+ if (StringUtils.isEmpty(endTime)){
endTime = "t";
}
diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java b/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java
index 8a1acfbfa..20796dc8d 100644
--- a/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java
+++ b/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java
@@ -5,7 +5,7 @@ import net.sf.openrocket.simulation.SimulationConditions;
import net.sf.openrocket.simulation.exception.SimulationException;
import net.sf.openrocket.simulation.extension.AbstractSimulationExtension;
import net.sf.openrocket.simulation.listeners.SimulationListener;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Injector;
@@ -19,7 +19,7 @@ public class JavaCode extends AbstractSimulationExtension {
public void initialize(SimulationConditions conditions) throws SimulationException {
String className = getClassName();
try {
- if (!StringUtil.isEmpty(className)) {
+ if (!StringUtils.isEmpty(className)) {
Class> clazz = Class.forName(className);
if (!SimulationListener.class.isAssignableFrom(clazz)) {
throw new SimulationException("Class " + className + " does not implement SimulationListener");
@@ -40,7 +40,7 @@ public class JavaCode extends AbstractSimulationExtension {
public String getName() {
String name = trans.get("SimulationExtension.javacode.name") + ": ";
String className = getClassName();
- if (!StringUtil.isEmpty(className)) {
+ if (!StringUtils.isEmpty(className)) {
name = name + className;
} else {
name = name + trans.get("SimulationExtension.javacode.name.none");
diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java
index 74e6126bf..b4741790e 100644
--- a/core/src/net/sf/openrocket/unit/UnitGroup.java
+++ b/core/src/net/sf/openrocket/unit/UnitGroup.java
@@ -18,7 +18,7 @@ import java.util.regex.Pattern;
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
import net.sf.openrocket.rocketcomponent.Rocket;
-import net.sf.openrocket.util.StringUtil;
+import net.sf.openrocket.util.StringUtils;
/**
@@ -672,7 +672,7 @@ public class UnitGroup {
throw new NumberFormatException("string did not match required pattern");
}
- double value = StringUtil.convertToDouble(matcher.group(1));
+ double value = StringUtils.convertToDouble(matcher.group(1));
String unit = matcher.group(2).trim();
if (unit.equals("")) {
diff --git a/core/src/net/sf/openrocket/util/StringUtil.java b/core/src/net/sf/openrocket/util/StringUtil.java
deleted file mode 100644
index 27be8ac44..000000000
--- a/core/src/net/sf/openrocket/util/StringUtil.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.sf.openrocket.util;
-
-public class StringUtil {
-
- /**
- * Returns true if the argument is null or empty.
- *
- * This is implemented without using String.isEmpty() because that method
- * is not available in Froyo.
- *
- * @param s string to check
- * @return true iff s is null or trims to
- * an empty string, where trim is defined
- * by {@link java.lang.String#trim}
- */
- public static boolean isEmpty( String s ) {
- if ( s == null ) {
- return true;
- }
- return "".equals(s.trim());
- }
-
- /**
- * Converts a string to a double, but with a more robust locale handling.
- * Some systems use a comma as a decimal separator, some a dot. This method
- * should work for both cases
- * @param input string to convert
- * @return double converted from string
- * @throws NumberFormatException if the string cannot be parsed.
- */
- public static double convertToDouble(String input) {
- input = input.replace(',', '.');
- int decimalSeparator = input.lastIndexOf('.');
-
- if (decimalSeparator > -1) {
- input = input.substring(0, decimalSeparator).replace(".", "") + input.substring(decimalSeparator);
- }
-
- return Double.parseDouble(input);
- }
-
-}
diff --git a/core/src/net/sf/openrocket/util/StringUtils.java b/core/src/net/sf/openrocket/util/StringUtils.java
new file mode 100644
index 000000000..92e866f78
--- /dev/null
+++ b/core/src/net/sf/openrocket/util/StringUtils.java
@@ -0,0 +1,101 @@
+package net.sf.openrocket.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class StringUtils {
+
+ public static String join(String sep, Object[] values) {
+ if ( values == null || values.length == 0 ) {
+ return "";
+ }
+ StringBuilder value = new StringBuilder();
+ for( Object v : values ) {
+ if( value.length() > 0 ) {
+ value.append(sep);
+ }
+ value.append(String.valueOf(v));
+ }
+ return value.toString();
+ }
+
+ /**
+ * Join starting with a list of strings rather than an array
+ * @param sep separator
+ * @param listValues list of values
+ * @return joined string
+ */
+ public static String join(String sep, List listValues) {
+ String[] values = listValues.toArray(new String[0]);
+ return join(sep, values);
+ }
+
+ /**
+ * Returns true if the argument is null or empty.
+ *
+ * This is implemented without using String.isEmpty() because that method
+ * is not available in Froyo.
+ *
+ * @param s string to check
+ * @return true iff s is null or trims to
+ * an empty string, where trim is defined
+ * by {@link java.lang.String#trim}
+ */
+ public static boolean isEmpty( String s ) {
+ if ( s == null ) {
+ return true;
+ }
+ return "".equals(s.trim());
+ }
+
+ /**
+ * Converts a string to a double, but with a more robust locale handling.
+ * Some systems use a comma as a decimal separator, some a dot. This method
+ * should work for both cases
+ * @param input string to convert
+ * @return double converted from string
+ * @throws NumberFormatException if the string cannot be parsed.
+ */
+ public static double convertToDouble(String input) {
+ input = input.replace(',', '.');
+ int decimalSeparator = input.lastIndexOf('.');
+
+ if (decimalSeparator > -1) {
+ input = input.substring(0, decimalSeparator).replace(".", "") + input.substring(decimalSeparator);
+ }
+
+ return Double.parseDouble(input);
+ }
+
+ /**
+ * Returns an escaped version of the String so that it can be safely used as a value in a CSV file.
+ * The goal is to surround the input string in double quotes if it contains any double quotes, commas,
+ * newlines, or carriage returns, and to escape any double quotes within the string by doubling them up.
+ * @param input the string to escape
+ * @return the escaped string that can be safely used in a CSV file
+ */
+ public static String escapeCSV(String input) {
+ final List CSV_SEARCH_CHARS = new ArrayList<>(Arrays.asList(',', '"', '\r', '\n'));
+
+ StringBuilder sb = new StringBuilder();
+ boolean quoted = false;
+ for (int i = 0; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (CSV_SEARCH_CHARS.contains(c)) {
+ quoted = true;
+ sb.append('\"');
+ }
+ if (c == '\"') {
+ sb.append('\"');
+ }
+ sb.append(c);
+ }
+ if (quoted) {
+ sb.insert(0, '\"');
+ sb.append('\"');
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/core/test/net/sf/openrocket/util/StringUtilTest.java b/core/test/net/sf/openrocket/util/StringUtilTest.java
index 7b1543d4c..0147921aa 100644
--- a/core/test/net/sf/openrocket/util/StringUtilTest.java
+++ b/core/test/net/sf/openrocket/util/StringUtilTest.java
@@ -8,40 +8,40 @@ import static org.junit.Assert.assertEquals;
/**
* A class that tests
- * {@link net.sf.openrocket.util.StringUtil}.
+ * {@link StringUtils}.
*/
public class StringUtilTest {
@Test
public void testStrings() {
- assertTrue(StringUtil.isEmpty(""));
- assertTrue(StringUtil.isEmpty(new StringBuilder().toString())); // ""
- assertTrue(StringUtil.isEmpty(" "));
- assertTrue(StringUtil.isEmpty(" "));
- assertTrue(StringUtil.isEmpty(" "));
- assertTrue(StringUtil.isEmpty(null));
+ assertTrue(StringUtils.isEmpty(""));
+ assertTrue(StringUtils.isEmpty(new StringBuilder().toString())); // ""
+ assertTrue(StringUtils.isEmpty(" "));
+ assertTrue(StringUtils.isEmpty(" "));
+ assertTrue(StringUtils.isEmpty(" "));
+ assertTrue(StringUtils.isEmpty(null));
- assertFalse(StringUtil.isEmpty("A"));
- assertFalse(StringUtil.isEmpty(" . "));
+ assertFalse(StringUtils.isEmpty("A"));
+ assertFalse(StringUtils.isEmpty(" . "));
}
@Test
public void testConvertToDouble() {
- assertEquals(0.2, StringUtil.convertToDouble(".2"), MathUtil.EPSILON);
- assertEquals(0.2, StringUtil.convertToDouble(",2"), MathUtil.EPSILON);
- assertEquals(1, StringUtil.convertToDouble("1,"), MathUtil.EPSILON);
- assertEquals(2, StringUtil.convertToDouble("2."), MathUtil.EPSILON);
- assertEquals(1, StringUtil.convertToDouble("1"), MathUtil.EPSILON);
- assertEquals(1.52, StringUtil.convertToDouble("1.52"), MathUtil.EPSILON);
- assertEquals(1.52, StringUtil.convertToDouble("1,52"), MathUtil.EPSILON);
- assertEquals(1.5, StringUtil.convertToDouble("1.500"), MathUtil.EPSILON);
- assertEquals(1.5, StringUtil.convertToDouble("1,500"), MathUtil.EPSILON);
- assertEquals(1500.61, StringUtil.convertToDouble("1.500,61"), MathUtil.EPSILON);
- assertEquals(1500.61, StringUtil.convertToDouble("1,500.61"), MathUtil.EPSILON);
- assertEquals(1500.2, StringUtil.convertToDouble("1,500,200"), MathUtil.EPSILON);
- assertEquals(1500.2, StringUtil.convertToDouble("1.500.200"), MathUtil.EPSILON);
- assertEquals(1500200.23, StringUtil.convertToDouble("1500200.23"), MathUtil.EPSILON);
- assertEquals(1500200.23, StringUtil.convertToDouble("1500200,23"), MathUtil.EPSILON);
- assertEquals(1500200.23, StringUtil.convertToDouble("1,500,200.23"), MathUtil.EPSILON);
- assertEquals(1500200.23, StringUtil.convertToDouble("1.500.200,23"), MathUtil.EPSILON);
+ assertEquals(0.2, StringUtils.convertToDouble(".2"), MathUtil.EPSILON);
+ assertEquals(0.2, StringUtils.convertToDouble(",2"), MathUtil.EPSILON);
+ assertEquals(1, StringUtils.convertToDouble("1,"), MathUtil.EPSILON);
+ assertEquals(2, StringUtils.convertToDouble("2."), MathUtil.EPSILON);
+ assertEquals(1, StringUtils.convertToDouble("1"), MathUtil.EPSILON);
+ assertEquals(1.52, StringUtils.convertToDouble("1.52"), MathUtil.EPSILON);
+ assertEquals(1.52, StringUtils.convertToDouble("1,52"), MathUtil.EPSILON);
+ assertEquals(1.5, StringUtils.convertToDouble("1.500"), MathUtil.EPSILON);
+ assertEquals(1.5, StringUtils.convertToDouble("1,500"), MathUtil.EPSILON);
+ assertEquals(1500.61, StringUtils.convertToDouble("1.500,61"), MathUtil.EPSILON);
+ assertEquals(1500.61, StringUtils.convertToDouble("1,500.61"), MathUtil.EPSILON);
+ assertEquals(1500.2, StringUtils.convertToDouble("1,500,200"), MathUtil.EPSILON);
+ assertEquals(1500.2, StringUtils.convertToDouble("1.500.200"), MathUtil.EPSILON);
+ assertEquals(1500200.23, StringUtils.convertToDouble("1500200.23"), MathUtil.EPSILON);
+ assertEquals(1500200.23, StringUtils.convertToDouble("1500200,23"), MathUtil.EPSILON);
+ assertEquals(1500200.23, StringUtils.convertToDouble("1,500,200.23"), MathUtil.EPSILON);
+ assertEquals(1500200.23, StringUtils.convertToDouble("1.500.200,23"), MathUtil.EPSILON);
}
}
diff --git a/swing/.classpath b/swing/.classpath
index e2bf65e9a..d57e69a4c 100644
--- a/swing/.classpath
+++ b/swing/.classpath
@@ -32,7 +32,6 @@
-
diff --git a/swing/build.xml b/swing/build.xml
index e242f8ea7..d1945c272 100644
--- a/swing/build.xml
+++ b/swing/build.xml
@@ -117,7 +117,6 @@
-
diff --git a/swing/src/net/sf/openrocket/file/SimulationTableCSVExport.java b/swing/src/net/sf/openrocket/file/SimulationTableCSVExport.java
new file mode 100644
index 000000000..0906b22ed
--- /dev/null
+++ b/swing/src/net/sf/openrocket/file/SimulationTableCSVExport.java
@@ -0,0 +1,181 @@
+package net.sf.openrocket.file;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import javax.swing.JOptionPane;
+import javax.swing.JTable;
+
+import net.sf.openrocket.util.StringUtils;
+
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.adaptors.Column;
+import net.sf.openrocket.gui.adaptors.ColumnTableModel;
+import net.sf.openrocket.gui.adaptors.ValueColumn;
+import net.sf.openrocket.unit.Value;
+import net.sf.openrocket.util.TextUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SimulationTableCSVExport {
+ private final OpenRocketDocument document;
+ private final JTable simulationTable;
+ private final ColumnTableModel simulationTableModel;
+ private final HashMap valueColumnToUnitString = new HashMap<>();
+
+ private static final Logger log = LoggerFactory.getLogger(SimulationTableCSVExport.class);
+
+ public SimulationTableCSVExport (OpenRocketDocument document, JTable simulationTable,
+ ColumnTableModel simulationTableModel) {
+ this.document = document;
+ this.simulationTable = simulationTable;
+ this.simulationTableModel = simulationTableModel;
+ }
+
+ /**
+ * To make a lookup of table header to units. For those columns which are of type Value, the
+ * units will be added to the header...
+ */
+ private void populateColumnNameToUnitsHashTable() {
+ valueColumnToUnitString.clear(); // Necessary if units changed during session
+ if (simulationTableModel == null) {
+ return;
+ }
+ for (int i = 0; i < simulationTableModel.getColumnCount(); i++) {
+ Column c = simulationTableModel.getColumn(i);
+ if (c instanceof ValueColumn) {
+ // Only value columns seem to have units that are not zero length strings... These are
+ // the ones we actually want in our lookup table.
+ valueColumnToUnitString.put(c.toString(), c.getUnits().getDefaultUnit().getUnit());
+ }
+
+ }
+ }
+ /**
+ * Dump data from sim table to file for run simulations
+ * @param data The CSV data as one string block.
+ * @param CSVFile The file to dump the data to.
+ */
+ private void dumpDataToFile(String data, File CSVFile) {
+ BufferedWriter bufferedWriter = null;
+ try {
+ CSVFile.createNewFile();
+ bufferedWriter = new BufferedWriter(new FileWriter(CSVFile));
+ bufferedWriter.write(data);
+ } catch (IOException e) {
+ String msg = e.getMessage();
+ JOptionPane.showMessageDialog(simulationTable.getParent(), msg);
+ } finally {
+ if (bufferedWriter != null) {
+ try {
+ bufferedWriter.close();
+ } catch (IOException e) {
+ String msg = e.getMessage();
+ JOptionPane.showMessageDialog(simulationTable.getParent(), msg);
+ }
+ }
+ }
+ }
+
+ /**
+ * Generate the CSV data from the simulation table
+ * @param fieldSep The field separator to use in the CSV file.
+ * @param precision The number of decimal places to use in the CSV file.
+ * @return
+ */
+ public String generateCSVDate(String fieldSep, int precision) {
+ int modelColumnCount = simulationTableModel.getColumnCount();
+ int modelRowCount = simulationTableModel.getRowCount();
+ populateColumnNameToUnitsHashTable();
+
+ String CSVSimResultString;
+ // Obtain the column titles for the first row of the CSV
+ ArrayList rowColumnElement = new ArrayList<>();
+ for (int j = 1; j 0!
+ for (int j = 1; j < modelColumnCount ; j++) { // skip first column
+ Object o = simulationTableModel.getValueAt(idx, j);
+ if (o != null) {
+ final String valueString;
+ if (o instanceof Value) {
+ double value = ((Value) o).getUnitValue();
+ valueString = TextUtil.doubleToString(value, precision);
+ } else {
+ valueString = o.toString();
+ }
+ rowColumnElement.add(StringUtils.escapeCSV(valueString));
+ } else {
+ rowColumnElement.add("");
+ nullCnt++;
+ }
+ }
+
+ // Current "unstable" will have a populated sim table EXCEPT for the optimum delay column on a restart
+ // after a save. That means any row that WAS simulated will have exactly one null column in it... so we'll
+ // skip row export for the case where there are MORE than one nulls. Either way the user should run sims.
+ if (nullCnt > 1) { // ignore rows that have null column fields 1 through 8...
+ continue;
+ }
+
+ // Create the column data comma separated string for the ith row...
+ CSVSimResultString = StringUtils.join(fieldSep, rowColumnElement);
+
+ // Piece together all rows into one big ginormous string, adding any warnings to the item
+ fullOutputResult.append("\n").append(CSVSimResultString);
+ fullOutputResult.append(fieldSep).append(warningsText);
+ }
+
+ return fullOutputResult.toString();
+ }
+
+ public void export(File file, String fieldSep, int precision) {
+ if (file == null) {
+ log.warn("No file selected for export");
+ return;
+ }
+
+ String CSVData = generateCSVDate(fieldSep, precision);
+ this.dumpDataToFile(CSVData, file);
+ log.info("Simulation table data exported to " + file.getAbsolutePath());
+ }
+}
diff --git a/swing/src/net/sf/openrocket/gui/adaptors/Column.java b/swing/src/net/sf/openrocket/gui/adaptors/Column.java
index 92df4b54f..d0fd9ffbc 100644
--- a/swing/src/net/sf/openrocket/gui/adaptors/Column.java
+++ b/swing/src/net/sf/openrocket/gui/adaptors/Column.java
@@ -4,6 +4,8 @@ import java.util.Comparator;
import javax.swing.table.TableColumnModel;
+import net.sf.openrocket.unit.UnitGroup;
+
public abstract class Column {
private final String name;
private final String toolTip;
@@ -59,7 +61,9 @@ public abstract class Column {
return 0;
}
-
+ public UnitGroup getUnits() {
+ return UnitGroup.UNITS_NONE;
+ }
/**
* Return the column type class. This is necessary for example for numerical
* sorting of Value objects, showing booleans as checkboxes etc.
diff --git a/swing/src/net/sf/openrocket/gui/adaptors/ValueColumn.java b/swing/src/net/sf/openrocket/gui/adaptors/ValueColumn.java
index 052f9b975..7b546b6ae 100644
--- a/swing/src/net/sf/openrocket/gui/adaptors/ValueColumn.java
+++ b/swing/src/net/sf/openrocket/gui/adaptors/ValueColumn.java
@@ -31,6 +31,10 @@ public abstract class ValueColumn extends Column {
return ValueComparator.INSTANCE;
}
+ @Override
+ public UnitGroup getUnits() {
+ return this.unit;
+ }
/**
* Returns the double value to show in the Value object
*
diff --git a/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorInformationPanel.java b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorInformationPanel.java
index be698ae6e..dcbf04aa0 100644
--- a/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorInformationPanel.java
+++ b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorInformationPanel.java
@@ -16,6 +16,7 @@ import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
+import net.sf.openrocket.util.StringUtils;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
@@ -33,7 +34,6 @@ import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.motor.ThrustCurveMotor;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.UnitGroup;
-import net.sf.openrocket.utils.StringUtils;
@SuppressWarnings("serial")
class MotorInformationPanel extends JPanel {
diff --git a/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java
index c963ae877..5485c60b1 100644
--- a/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java
+++ b/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java
@@ -51,6 +51,7 @@ import javax.swing.table.TableColumnModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
+import net.sf.openrocket.arch.SystemInfo;
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -1157,6 +1158,14 @@ public class GeneralOptimizationDialog extends JDialog {
chooser.setFileFilter(FileHelper.CSV_FILTER);
chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory());
chooser.setAccessory(csvOptions);
+
+ // TODO: update this dynamically instead of hard-coded values
+ // The macOS file chooser has an issue where it does not update its size when the accessory is added.
+ if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) {
+ Dimension currentSize = chooser.getPreferredSize();
+ Dimension newSize = new Dimension((int) (1.5 * currentSize.width), (int) (1.3 * currentSize.height));
+ chooser.setPreferredSize(newSize);
+ }
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION)
return;
diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
index 82a6fb014..699d4375b 100644
--- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
+++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
+import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JCheckBox;
@@ -566,6 +567,11 @@ public class BasicFrame extends JFrame {
//// END CREATE and implement File > "Encode 3D" menu and submenu
*/
+ // export sim table...
+ AbstractAction simTableExportAction = simulationPanel.getSimulationTableAsCSVExportAction();
+ JMenuItem exportSimTableToCSVMenuItem = new JMenuItem(simTableExportAction);
+ menu.add(exportSimTableToCSVMenuItem);
+
menu.addSeparator();
//// Close
@@ -1430,7 +1436,6 @@ public class BasicFrame extends JFrame {
}
// END ROCKSIM Export Action
-
/**
* Perform the writing of the design to the given file in RockSim format.
*
diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
index 3c8f45839..5cc7ca8ce 100644
--- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
+++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
@@ -3,6 +3,8 @@ package net.sf.openrocket.gui.main;
import java.awt.Color;
import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
@@ -13,6 +15,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
+import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
@@ -21,7 +24,9 @@ import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
+import javax.swing.JFileChooser;
import javax.swing.JLabel;
+import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
@@ -34,8 +39,10 @@ import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
-import net.sf.openrocket.gui.widgets.IconButton;
-import net.sf.openrocket.utils.TableRowTraversalPolicy;
+import net.sf.openrocket.arch.SystemInfo;
+import net.sf.openrocket.gui.components.CsvOptionPanel;
+import net.sf.openrocket.gui.util.FileHelper;
+import net.sf.openrocket.gui.util.SwingPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -59,20 +66,22 @@ import net.sf.openrocket.gui.simulation.SimulationEditDialog;
import net.sf.openrocket.gui.simulation.SimulationRunDialog;
import net.sf.openrocket.gui.simulation.SimulationWarningDialog;
import net.sf.openrocket.gui.util.Icons;
+import net.sf.openrocket.gui.widgets.IconButton;
import net.sf.openrocket.l10n.Translator;
-import net.sf.openrocket.rocketcomponent.Rocket;
-import net.sf.openrocket.rocketcomponent.FlightConfigurationId;
import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.FlightConfigurationId;
+import net.sf.openrocket.rocketcomponent.Rocket;
import net.sf.openrocket.simulation.FlightData;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.startup.Preferences;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.AlphanumComparator;
+import net.sf.openrocket.file.SimulationTableCSVExport;
+import net.sf.openrocket.utils.TableRowTraversalPolicy;
@SuppressWarnings("serial")
public class SimulationPanel extends JPanel {
-
private static final Logger log = LoggerFactory.getLogger(SimulationPanel.class);
private static final Translator trans = Application.getTranslator();
@@ -96,6 +105,7 @@ public class SimulationPanel extends JPanel {
private final JButton runButton;
private final JButton deleteButton;
private final JButton plotButton;
+ private final JButton simTableExportButton;
private final JPopupMenu pm;
private final SimulationAction editSimulationAction;
@@ -103,8 +113,10 @@ public class SimulationPanel extends JPanel {
private final SimulationAction plotSimulationAction;
private final SimulationAction duplicateSimulationAction;
private final SimulationAction deleteSimulationAction;
+ private final SimulationAction simTableExportAction;
private int[] previousSelection = null;
+ private JMenuItem exportSimTableToCSVMenuItem;
public SimulationPanel(OpenRocketDocument doc) {
super(new MigLayout("fill", "[grow][][][][][][grow]"));
@@ -119,6 +131,7 @@ public class SimulationPanel extends JPanel {
plotSimulationAction = new PlotSimulationAction();
duplicateSimulationAction = new DuplicateSimulationAction();
deleteSimulationAction = new DeleteSimulationAction();
+ simTableExportAction = new ExportSimulationTableAsCSVAction();
//////// The simulation action buttons ////////
@@ -139,7 +152,7 @@ public class SimulationPanel extends JPanel {
RocketActions.tieActionToButton(runButton, runSimulationAction, trans.get("simpanel.but.runsimulations"));
runButton.setToolTipText(trans.get("simpanel.but.ttip.runsimu"));
this.add(runButton, "gapright para");
-
+
//// Delete simulations button
deleteButton = new IconButton();
RocketActions.tieActionToButton(deleteButton, deleteSimulationAction, trans.get("simpanel.but.deletesimulations"));
@@ -151,6 +164,9 @@ public class SimulationPanel extends JPanel {
RocketActions.tieActionToButton(plotButton, plotSimulationAction, trans.get("simpanel.but.plotexport"));
this.add(plotButton, "wrap para");
+ //// Run then Dump simulations
+ simTableExportButton = new IconButton();
+ RocketActions.tieActionToButton(simTableExportButton, simTableExportAction, trans.get("simpanel.but.runsimulations"));
//////// The simulation table
@@ -174,6 +190,7 @@ public class SimulationPanel extends JPanel {
pm.addSeparator();
pm.add(runSimulationAction);
pm.add(plotSimulationAction);
+ pm.add(simTableExportAction);
// The normal left/right and tab/shift-tab key action traverses each cell/column of the table instead of going to the next row.
TableRowTraversalPolicy.setTableRowTraversalPolicy(simulationTable);
@@ -437,6 +454,14 @@ public class SimulationPanel extends JPanel {
}
+ /**
+ * Return the action for exporting the simulation table data to a CSV file.
+ * @return
+ */
+ public AbstractAction getSimulationTableAsCSVExportAction() {
+ return simTableExportAction;
+ }
+
protected void doPopup(MouseEvent e) {
pm.show(e.getComponent(), e.getX(), e.getY());
}
@@ -447,6 +472,7 @@ public class SimulationPanel extends JPanel {
runSimulationAction.updateEnabledState();
plotSimulationAction.updateEnabledState();
duplicateSimulationAction.updateEnabledState();
+ simTableExportAction.updateEnabledState();
}
/// when the simulation tab is selected this run outdated simulated if appropriate.
@@ -598,6 +624,90 @@ public class SimulationPanel extends JPanel {
}
}
+ class ExportSimulationTableAsCSVAction extends SimulationAction {
+
+ public ExportSimulationTableAsCSVAction() {
+ putValue(NAME, trans.get("simpanel.pop.exportToCSV"));
+ putValue(SMALL_ICON, Icons.SIM_TABLE_EXPORT);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ Container tableParent = simulationTable.getParent();
+ int rowCount = simulationTableModel.getRowCount();
+
+ // I'm pretty sure with the enablement/disablement of the menu item under the File dropdown,
+ // that this would no longer be needed because if there is no sim table yet, the context menu
+ // won't show up. But I'm going to leave this in just in case....
+ if (rowCount <= 0) {
+ log.info("No simulation table rows to export");
+ JOptionPane.showMessageDialog(tableParent, trans.get("simpanel.dlg.no.simulation.table.rows"));
+ return;
+ }
+
+ JFileChooser fch = this.setUpFileChooser();
+ int selectionStatus = fch.showSaveDialog(tableParent);
+ if (selectionStatus != JFileChooser.APPROVE_OPTION) {
+ log.info("User cancelled CSV export");
+ return;
+ }
+
+ // Fetch the info from the file chooser
+ File CSVFile = fch.getSelectedFile();
+ CSVFile = FileHelper.forceExtension(CSVFile, "csv");
+ String separator = ((CsvOptionPanel) fch.getAccessory()).getFieldSeparator();
+ int precision = ((CsvOptionPanel) fch.getAccessory()).getDecimalPlaces();
+ ((CsvOptionPanel) fch.getAccessory()).storePreferences();
+
+ // Handle some special separator options from CsvOptionPanel
+ if (separator.equals(trans.get("CsvOptionPanel.separator.space"))) {
+ separator = " ";
+ } else if (separator.equals(trans.get("CsvOptionPanel.separator.tab"))) {
+ separator = "\t";
+ }
+
+ SimulationTableCSVExport exporter = new SimulationTableCSVExport(document, simulationTable, simulationTableModel);
+ exporter.export(CSVFile, separator, precision);
+ }
+
+ /**
+ * Create the file chooser to save the CSV file.
+ * @return The file chooser.
+ */
+ private JFileChooser setUpFileChooser() {
+ JFileChooser fch = new JFileChooser();
+ fch.setDialogTitle(trans.get("simpanel.pop.exportToCSV.save.dialog.title"));
+ fch.setFileFilter(FileHelper.CSV_FILTER);
+ fch.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory());
+ fch.setAcceptAllFileFilterUsed(false);
+
+ // Default output CSV to same name as the document's rocket name.
+ String fileName = document.getRocket().getName() + ".csv";
+ fch.setSelectedFile(new File(fileName));
+
+ // Add CSV options to FileChooser
+ CsvOptionPanel CSVOptions = new CsvOptionPanel(SimulationTableCSVExport.class);
+ fch.setAccessory(CSVOptions);
+ fch.revalidate();
+
+ // TODO: update this dynamically instead of hard-coded values
+ // The macOS file chooser has an issue where it does not update its size when the accessory is added.
+ if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) {
+ Dimension currentSize = fch.getPreferredSize();
+ Dimension newSize = new Dimension((int) (1.5 * currentSize.width), (int) (1.3 * currentSize.height));
+ fch.setPreferredSize(newSize);
+ }
+
+ return fch;
+ }
+
+ @Override
+ public void updateEnabledState() {
+ setEnabled(simulationTableModel != null && simulationTableModel.getRowCount() > 0);
+ }
+
+ }
+
class PlotSimulationAction extends SimulationAction {
public PlotSimulationAction() {
putValue(NAME, trans.get("simpanel.pop.plot"));
diff --git a/swing/src/net/sf/openrocket/gui/util/Icons.java b/swing/src/net/sf/openrocket/gui/util/Icons.java
index bbbe1b052..4fc17a319 100644
--- a/swing/src/net/sf/openrocket/gui/util/Icons.java
+++ b/swing/src/net/sf/openrocket/gui/util/Icons.java
@@ -56,6 +56,7 @@ public class Icons {
public static final Icon FILE_PRINT = loadImageIcon("pix/icons/print-design.specs.png", "Print specifications");
// public static final Icon FILE_IMPORT = loadImageIcon("pix/icons/model_import.png", "Import");
public static final Icon FILE_EXPORT_AS = loadImageIcon("pix/icons/model_export.png", "Export model as");
+ public static final Icon SIM_TABLE_EXPORT = loadImageIcon("pix/icons/sim_table_export.png", "Export simulation table");
public static final Icon ENCODE_3D = loadImageIcon("pix/icons/model_encode3d.png", "Encode 3D");
public static final Icon FILE_CLOSE = loadImageIcon("pix/icons/document-close.png", "Close document");
public static final Icon FILE_QUIT = loadImageIcon("pix/icons/application-exit.png", "Quit OpenRocket");
diff --git a/swing/src/net/sf/openrocket/utils/StringUtils.java b/swing/src/net/sf/openrocket/utils/StringUtils.java
deleted file mode 100644
index e362252c4..000000000
--- a/swing/src/net/sf/openrocket/utils/StringUtils.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package net.sf.openrocket.utils;
-
-public class StringUtils {
-
- public static String join(String sep, Object[] values) {
- if ( values == null || values.length == 0 ) {
- return "";
- }
- StringBuilder value = new StringBuilder();
- for( Object v : values ) {
- if( value.length() > 0 ) {
- value.append(sep);
- }
- value.append(String.valueOf(v));
- }
- return value.toString();
- }
-
-}