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(); - } - -}