updates for 0.9.4
authorplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Sat, 26 Sep 2009 17:06:37 +0000 (17:06 +0000)
committerplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Sat, 26 Sep 2009 17:06:37 +0000 (17:06 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@25 180e2498-e6e9-4542-8430-84ac67f01cd8

73 files changed:
ChangeLog
build.xml
fileformat.txt [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java
src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java
src/net/sf/openrocket/communication/Communication.java [new file with mode: 0644]
src/net/sf/openrocket/communication/UpdateInfo.java [new file with mode: 0644]
src/net/sf/openrocket/file/OpenRocketLoader.java
src/net/sf/openrocket/file/OpenRocketSaver.java
src/net/sf/openrocket/file/openrocket/FinSetSaver.java
src/net/sf/openrocket/gui/adaptors/Column.java
src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java
src/net/sf/openrocket/gui/adaptors/DoubleModel.java
src/net/sf/openrocket/gui/adaptors/MaterialModel.java
src/net/sf/openrocket/gui/components/CollectionTable.java [new file with mode: 0644]
src/net/sf/openrocket/gui/components/UnitSelector.java
src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java
src/net/sf/openrocket/gui/configdialog/FinSetConfig.java
src/net/sf/openrocket/gui/configdialog/MotorConfig.java
src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java
src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java
src/net/sf/openrocket/gui/configdialog/StreamerConfig.java
src/net/sf/openrocket/gui/dialogs/AboutDialog.java
src/net/sf/openrocket/gui/dialogs/BugReportDialog.java
src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java
src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/dialogs/EditMotorConfigurationDialog.java
src/net/sf/openrocket/gui/dialogs/ExampleDesignDialog.java
src/net/sf/openrocket/gui/dialogs/LicenseDialog.java
src/net/sf/openrocket/gui/dialogs/MotorChooserDialog.java
src/net/sf/openrocket/gui/dialogs/PreferencesDialog.java [deleted file]
src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/BasicFrame.java
src/net/sf/openrocket/gui/main/ComponentAddButtons.java
src/net/sf/openrocket/gui/main/ComponentIcons.java
src/net/sf/openrocket/gui/main/ComponentTreeModel.java
src/net/sf/openrocket/gui/main/ExceptionHandler.java
src/net/sf/openrocket/gui/main/SimulationEditDialog.java
src/net/sf/openrocket/gui/main/SimulationRunDialog.java
src/net/sf/openrocket/gui/plot/PlotDialog.java
src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java
src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java
src/net/sf/openrocket/gui/scalefigure/RocketFigure.java
src/net/sf/openrocket/material/Material.java
src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java
src/net/sf/openrocket/rocketcomponent/ExternalComponent.java
src/net/sf/openrocket/rocketcomponent/FinSet.java
src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java
src/net/sf/openrocket/rocketcomponent/RocketComponent.java
src/net/sf/openrocket/rocketcomponent/TubeCoupler.java
src/net/sf/openrocket/unit/Unit.java
src/net/sf/openrocket/unit/UnitGroup.java
src/net/sf/openrocket/unit/Value.java [new file with mode: 0644]
src/net/sf/openrocket/util/ComparablePair.java [new file with mode: 0644]
src/net/sf/openrocket/util/GUIUtil.java
src/net/sf/openrocket/util/Icons.java
src/net/sf/openrocket/util/OpenFileWorker.java
src/net/sf/openrocket/util/Pair.java
src/net/sf/openrocket/util/Prefs.java
src/net/sf/openrocket/util/SaveCSVWorker.java
src/net/sf/openrocket/util/SaveFileWorker.java
src/net/sf/openrocket/util/Test.java [deleted file]
src/net/sf/openrocket/util/TestRockets.java [new file with mode: 0644]
src/net/sf/openrocket/util/Transformation.java
src/net/sf/openrocket/util/UniqueID.java
test/net/sf/openrocket/communication/CommunicationTest.java [new file with mode: 0644]
test/net/sf/openrocket/rocketcomponent/ComponentCompare.java [new file with mode: 0644]
test/net/sf/openrocket/rocketcomponent/ComponentCompareTest.java [new file with mode: 0644]
test/net/sf/openrocket/rocketcomponent/FinSetTest.java [new file with mode: 0644]
test/net/sf/openrocket/rocketcomponent/RocketTest.java [new file with mode: 0644]
test/net/sf/openrocket/unit/ValueTest.java [new file with mode: 0644]
test/net/sf/openrocket/util/UniqueIDTest.java [new file with mode: 0644]

index 2e74313c5533349ce8ec2fad9982d89d509cb892..45192f7ba52bc71c8a3e0bfcc161b37c65648397 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,34 @@
+2009-09-26  Sampo Niskanen
+
+       * Implemented custom material editing
+
+2009-09-20  Sampo Niskanen
+
+       * Implemented more unit tests, fixed bugs
+
+2009-09-19  Sampo Niskanen
+
+       * [BUG] Ignore Sun JRE bug 6828938 in ExceptionHandler
+       * Implemented non-exception throwing bug handling
+       * [BUG] Fixed unnecessary cropping for component tree names
+
+2009-09-10  Sampo Niskanen
+
+       * [BUG] Freeform fin set shape undo not working
+       * [BUG] Conversion to freeform fin set not working
+
+2009-09-08  Sampo Niskanen
+
+       * Allow components to be attached to tube coupler
+
+2009-09-07  Sampo Niskanen
+
+       * Implemented fin tab save/load
+
+2009-09-04  Sampo Niskanen
+
+       * Implemented through-the-wall fin tabs (excluding save/load)
+
 2009-09-01  Sampo Niskanen
 
        * Released version 0.9.3
index 386b5825ed3ca956b7f6ea2d0460ec5cd1979076..97f5a4e41c299b35b95b279870ba79b3288438aa 100644 (file)
--- a/build.xml
+++ b/build.xml
        <property name="main-dir" value="net/sf/openrocket/startup"/>
 
        
-       <!-- Classpath definition -->
+       <!-- Classpath definitions -->
        <path id="classpath">
                <fileset dir="${lib.dir}" includes="**/*.jar"/>
        </path>
        
+       <path id="test-classpath">
+               <path refid="classpath"/>
+               <pathelement location="${build-test.dir}"/>
+               <pathelement location="${classes.dir}"/>
+               <pathelement location="${ant.library.dir}/junit4.jar"/>
+       </path>
+       
 
        
        <!-- CLEAN -->
        <target name="clean">
                <delete dir="${build.dir}"/>
+               <delete dir="tmp/"/>
        </target>
                
        
                <javac srcdir="${src.dir}" destdir="${classes.dir}" excludes="${main-dir}/*" classpathref="classpath"/>
                <echo>Compiling startup classes</echo>
                <javac srcdir="${src.dir}/${main-dir}" destdir="${classes.dir}" source="1.4" classpathref="classpath"/>
+               <copy file="build.properties" todir="${dist.dir}"/>
        </target>
        
        
        <!-- JAR -->
        <target name="jar" depends="build">
                <copy todir="${dist.dir}/">
-                       <fileset dir="." includes="LICENSE.TXT README.TXT ChangeLog ReleaseNotes build.properties" />
+                       <fileset dir="." includes="LICENSE.TXT README.TXT ChangeLog ReleaseNotes build.properties fileformat.txt" />
                        <fileset dir="." includes="datafiles/ pix/" />
                </copy>
                <mkdir dir="${jar.dir}"/>
@@ -69,7 +78,7 @@
        
        
        <!-- DIST-SRC -->
-       <target name="dist-src" depends="checktodo,clean,unittest">
+       <target name="dist-src">
                <echo>                  
                Building source distribution
                </echo>
@@ -141,19 +150,19 @@ ${criticaltodos}</fail>
        <target name="unittest" description="Execute unit tests" depends="build">
                <echo>Building unit tests</echo>
                <mkdir dir="${build-test.dir}"/>
-               <javac srcdir="${src-test.dir}" destdir="${build-test.dir}" classpathref="classpath" classpath="${classes.dir}:${ant.library.dir}/junit4.jar"/>
+               <javac srcdir="${src-test.dir}" destdir="${build-test.dir}" classpathref="test-classpath"/>
                
                <echo>Running unit tests</echo>
                <mkdir dir="tmp/rawtestoutput"/>
                <junit printsummary="true" failureproperty="junit.failure">
-                       <classpath path="${build-test.dir}"/>
-                       <classpath path="${classes.dir}"/>
-                       <classpath path="${ant.library.dir}/junit4.jar"/>
-                 
+                       <classpath>
+                               <path refid="test-classpath"/>
+                               <path location="${basedir}"/>
+                       </classpath>
                        <batchtest todir="tmp/rawtestoutput">
-                               <fileset dir="${src-test.dir}">
-                                       <include name="**/*Test*.java" />
-                                       <exclude name="Test.java" />
+                               <fileset dir="${build-test.dir}">
+                                       <include name="**/*Test*.class" />
+                                       <exclude name="Test.class" />
                                </fileset>
                                <formatter type="xml"/>
                        </batchtest>
diff --git a/fileformat.txt b/fileformat.txt
new file mode 100644 (file)
index 0000000..a226d42
--- /dev/null
@@ -0,0 +1,35 @@
+
+The current OpenRocket file format is "documented" only as the
+reference implementation.  This will hopefully change in the future.
+
+
+The "version" attribute of the <openrocket> tag describes the file
+format version used, while the "creator" attribute MAY describe the
+software version used to write the document.
+
+The file format version is increased every time the format is
+changed.  The minor number is increased when changes are made that are
+mostly backward-compatible, meaning that older software versions
+should be able to read the design sans the new features.  The major
+number is increased when changes are made that render the design
+problematic or impossible to read for older software.  For maximum
+compatibility software should save a file in the oldest file format
+version that supports all the necessary design features.
+
+
+The following file format versions exist:
+
+
+0.9:  Used before the first public release of Openrocket; effectively
+      equivalent to 1.0.  Should not be used when writing documents.
+      OpenRocket accepts this version when reading files, but other
+      software need not.
+
+1.0:  File format version of the first public release (OpenRocket 0.9.0)
+
+1.1:  Introduced with OpenRocket 0.9.4.  Adds support for saving fin
+      tabs (<tabheight>, <tablength> and <tabposition> elements) and
+      allows attaching subcomponents to a tube coupler (previously
+      forbidden).
+
+
index 8ace04ac73b913df1f7cbbcc24784e73751ee5c9..10da9f4639e375a9a3d624e1c638b8554a73e0b3 100644 (file)
@@ -13,7 +13,6 @@ import net.sf.openrocket.aerodynamics.barrowman.RocketComponentCalc;
 import net.sf.openrocket.rocketcomponent.Configuration;
 import net.sf.openrocket.rocketcomponent.ExternalComponent;
 import net.sf.openrocket.rocketcomponent.FinSet;
-import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
 import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
@@ -21,7 +20,6 @@ import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.PolyInterpolator;
 import net.sf.openrocket.util.Reflection;
-import net.sf.openrocket.util.Test;
 
 /**
  * An aerodynamic calculator that uses the extended Barrowman method to 
@@ -809,89 +807,89 @@ public class BarrowmanCalculator extends AerodynamicCalculator {
        
        
        
-       public static void main(String[] arg) {
-               
-               PolyInterpolator interpolator;
-               
-               interpolator = new PolyInterpolator(
-                               new double[] { 0, 17*Math.PI/180 },
-                               new double[] { 0, 17*Math.PI/180 }
-               );
-               double[] poly1 = interpolator.interpolator(1, 1.3, 0, 0);
-               
-               interpolator = new PolyInterpolator(
-                               new double[] { 17*Math.PI/180, Math.PI/2 },
-                               new double[] { 17*Math.PI/180, Math.PI/2 },
-                               new double[] { Math.PI/2 }
-               );
-               double[] poly2 = interpolator.interpolator(1.3, 0, 0, 0, 0);
-                               
-               
-               for (double a=0; a<=180.1; a++) {
-                       double r = a*Math.PI/180;
-                       if (r > Math.PI/2)
-                               r = Math.PI - r;
-                       
-                       double value;
-                       if (r < 18*Math.PI/180)
-                               value = PolyInterpolator.eval(r, poly1);
-                       else
-                               value = PolyInterpolator.eval(r, poly2);
-                       
-                       System.out.println(""+a+" "+value);
-               }
-               
-               System.exit(0);
-               
-               
-               Rocket normal = Test.makeRocket();
-               Rocket perfect = Test.makeRocket();
-               normal.setPerfectFinish(false);
-               perfect.setPerfectFinish(true);
-               
-               Configuration confNormal = new Configuration(normal);
-               Configuration confPerfect = new Configuration(perfect);
-               
-               for (RocketComponent c: confNormal) {
-                       if (c instanceof ExternalComponent) {
-                               ((ExternalComponent)c).setFinish(Finish.NORMAL);
-                       }
-               }
-               for (RocketComponent c: confPerfect) {
-                       if (c instanceof ExternalComponent) {
-                               ((ExternalComponent)c).setFinish(Finish.NORMAL);
-                       }
-               }
-               
-               
-               confNormal.setToStage(0);
-               confPerfect.setToStage(0);
-               
-               
-               
-               BarrowmanCalculator calcNormal = new BarrowmanCalculator(confNormal);
-               BarrowmanCalculator calcPerfect = new BarrowmanCalculator(confPerfect);
-               
-               FlightConditions conditions = new FlightConditions(confNormal);
-               
-               for (double mach=0; mach < 3; mach += 0.1) {
-                       conditions.setMach(mach);
-
-                       Map<RocketComponent, AerodynamicForces> data = 
-                               calcNormal.getForceAnalysis(conditions, null);
-                       AerodynamicForces forcesNormal = data.get(normal);
-                       
-                       data = calcPerfect.getForceAnalysis(conditions, null);
-                       AerodynamicForces forcesPerfect = data.get(perfect);
-                       
-                       System.out.printf("%f %f %f %f %f %f %f\n",mach, 
-                                       forcesNormal.pressureCD, forcesPerfect.pressureCD, 
-                                       forcesNormal.frictionCD, forcesPerfect.frictionCD,
-                                       forcesNormal.CD, forcesPerfect.CD);
-               }
-               
-               
-               
-       }
+//     public static void main(String[] arg) {
+//             
+//             PolyInterpolator interpolator;
+//             
+//             interpolator = new PolyInterpolator(
+//                             new double[] { 0, 17*Math.PI/180 },
+//                             new double[] { 0, 17*Math.PI/180 }
+//             );
+//             double[] poly1 = interpolator.interpolator(1, 1.3, 0, 0);
+//             
+//             interpolator = new PolyInterpolator(
+//                             new double[] { 17*Math.PI/180, Math.PI/2 },
+//                             new double[] { 17*Math.PI/180, Math.PI/2 },
+//                             new double[] { Math.PI/2 }
+//             );
+//             double[] poly2 = interpolator.interpolator(1.3, 0, 0, 0, 0);
+//                             
+//             
+//             for (double a=0; a<=180.1; a++) {
+//                     double r = a*Math.PI/180;
+//                     if (r > Math.PI/2)
+//                             r = Math.PI - r;
+//                     
+//                     double value;
+//                     if (r < 18*Math.PI/180)
+//                             value = PolyInterpolator.eval(r, poly1);
+//                     else
+//                             value = PolyInterpolator.eval(r, poly2);
+//                     
+//                     System.out.println(""+a+" "+value);
+//             }
+//             
+//             System.exit(0);
+//             
+//             
+//             Rocket normal = TestRocket.makeRocket();
+//             Rocket perfect = TestRocket.makeRocket();
+//             normal.setPerfectFinish(false);
+//             perfect.setPerfectFinish(true);
+//             
+//             Configuration confNormal = new Configuration(normal);
+//             Configuration confPerfect = new Configuration(perfect);
+//             
+//             for (RocketComponent c: confNormal) {
+//                     if (c instanceof ExternalComponent) {
+//                             ((ExternalComponent)c).setFinish(Finish.NORMAL);
+//                     }
+//             }
+//             for (RocketComponent c: confPerfect) {
+//                     if (c instanceof ExternalComponent) {
+//                             ((ExternalComponent)c).setFinish(Finish.NORMAL);
+//                     }
+//             }
+//             
+//             
+//             confNormal.setToStage(0);
+//             confPerfect.setToStage(0);
+//             
+//             
+//             
+//             BarrowmanCalculator calcNormal = new BarrowmanCalculator(confNormal);
+//             BarrowmanCalculator calcPerfect = new BarrowmanCalculator(confPerfect);
+//             
+//             FlightConditions conditions = new FlightConditions(confNormal);
+//             
+//             for (double mach=0; mach < 3; mach += 0.1) {
+//                     conditions.setMach(mach);
+//
+//                     Map<RocketComponent, AerodynamicForces> data = 
+//                             calcNormal.getForceAnalysis(conditions, null);
+//                     AerodynamicForces forcesNormal = data.get(normal);
+//                     
+//                     data = calcPerfect.getForceAnalysis(conditions, null);
+//                     AerodynamicForces forcesPerfect = data.get(perfect);
+//                     
+//                     System.out.printf("%f %f %f %f %f %f %f\n",mach, 
+//                                     forcesNormal.pressureCD, forcesPerfect.pressureCD, 
+//                                     forcesNormal.frictionCD, forcesPerfect.frictionCD,
+//                                     forcesNormal.CD, forcesPerfect.CD);
+//             }
+//             
+//             
+//             
+//     }
 
 }
index 05144ef5dcad161cb0e1f00f7a9fdc78d0184056..13a36b67716df204f1a86021a00efe4a53ba2847 100644 (file)
@@ -1,26 +1,20 @@
 package net.sf.openrocket.aerodynamics.barrowman;
 
-import static java.lang.Math.pow;
-import static java.lang.Math.sqrt;
+import static java.lang.Math.*;
 import static net.sf.openrocket.util.MathUtil.pow2;
 
 import java.util.Arrays;
-import java.util.Iterator;
 
 import net.sf.openrocket.aerodynamics.AerodynamicForces;
 import net.sf.openrocket.aerodynamics.FlightConditions;
 import net.sf.openrocket.aerodynamics.Warning;
 import net.sf.openrocket.aerodynamics.WarningSet;
-import net.sf.openrocket.rocketcomponent.Configuration;
 import net.sf.openrocket.rocketcomponent.FinSet;
-import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
-import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.LinearInterpolator;
 import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.PolyInterpolator;
-import net.sf.openrocket.util.Test;
 
 
 public class FinSetCalc extends RocketComponentCalc {
@@ -603,38 +597,38 @@ public class FinSetCalc extends RocketComponentCalc {
        }
        
        
-       @SuppressWarnings("null")
-       public static void main(String arg[]) {
-               Rocket rocket = Test.makeRocket();
-               FinSet finset = null;
-               
-               Iterator<RocketComponent> iter = rocket.deepIterator();
-               while (iter.hasNext()) {
-                       RocketComponent c = iter.next();
-                       if (c instanceof FinSet) {
-                               finset = (FinSet)c;
-                               break;
-                       }
-               }
-               
-               ((TrapezoidFinSet)finset).setHeight(0.10);
-               ((TrapezoidFinSet)finset).setRootChord(0.10);
-               ((TrapezoidFinSet)finset).setTipChord(0.10);
-               ((TrapezoidFinSet)finset).setSweep(0.0);
-
-               
-               FinSetCalc calc = new FinSetCalc(finset);
-               
-               calc.calculateFinGeometry();
-               FlightConditions cond = new FlightConditions(new Configuration(rocket));
-               for (double m=0; m < 3; m+=0.05) {
-                       cond.setMach(m);
-                       cond.setAOA(0.0*Math.PI/180);
-                       double cna = calc.calculateFinCNa1(cond);
-                       System.out.printf("%5.2f "+cna+"\n", m);
-               }
-               
-       }
+//     @SuppressWarnings("null")
+//     public static void main(String arg[]) {
+//             Rocket rocket = TestRocket.makeRocket();
+//             FinSet finset = null;
+//             
+//             Iterator<RocketComponent> iter = rocket.deepIterator();
+//             while (iter.hasNext()) {
+//                     RocketComponent c = iter.next();
+//                     if (c instanceof FinSet) {
+//                             finset = (FinSet)c;
+//                             break;
+//                     }
+//             }
+//             
+//             ((TrapezoidFinSet)finset).setHeight(0.10);
+//             ((TrapezoidFinSet)finset).setRootChord(0.10);
+//             ((TrapezoidFinSet)finset).setTipChord(0.10);
+//             ((TrapezoidFinSet)finset).setSweep(0.0);
+//
+//             
+//             FinSetCalc calc = new FinSetCalc(finset);
+//             
+//             calc.calculateFinGeometry();
+//             FlightConditions cond = new FlightConditions(new Configuration(rocket));
+//             for (double m=0; m < 3; m+=0.05) {
+//                     cond.setMach(m);
+//                     cond.setAOA(0.0*Math.PI/180);
+//                     double cna = calc.calculateFinCNa1(cond);
+//                     System.out.printf("%5.2f "+cna+"\n", m);
+//             }
+//             
+//     }
 
 
        @Override
diff --git a/src/net/sf/openrocket/communication/Communication.java b/src/net/sf/openrocket/communication/Communication.java
new file mode 100644 (file)
index 0000000..df3b0ac
--- /dev/null
@@ -0,0 +1,262 @@
+package net.sf.openrocket.communication;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+
+import net.sf.openrocket.util.ComparablePair;
+import net.sf.openrocket.util.Prefs;
+
+public class Communication {
+
+       private static final String BUG_REPORT_URL = 
+               "http://openrocket.sourceforge.net/actions/reportbug";
+       private static final String UPDATE_INFO_URL =
+               "http://openrocket.sourceforge.net/actions/updates";
+
+       private static final String VERSION_PARAM = "version";
+       
+       
+       private static final String BUG_REPORT_PARAM = "content";
+       private static final int BUG_REPORT_RESPONSE_CODE = HttpURLConnection.HTTP_ACCEPTED;
+       private static final int CONNECTION_TIMEOUT = 10000;  // in milliseconds
+
+       private static final int UPDATE_INFO_UPDATE_AVAILABLE = HttpURLConnection.HTTP_OK;
+       private static final int UPDATE_INFO_NO_UPDATE_CODE = HttpURLConnection.HTTP_NO_CONTENT;
+       private static final String UPDATE_INFO_CONTENT_TYPE = "text/plain";
+
+       
+       private static UpdateInfoFetcher fetcher = null;
+       
+
+       /**
+        * Send the provided report to the OpenRocket bug report URL.  If the connection
+        * fails or the server does not respond with the correct response code, an
+        * exception is thrown.
+        * 
+        * @param report                the report to send.
+        * @throws IOException  if an error occurs while connecting to the server or
+        *                                              the server responds with a wrong response code.
+        */
+       public static void sendBugReport(String report) throws IOException {
+               URL url = new URL(BUG_REPORT_URL);
+               
+               HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+               
+               connection.setConnectTimeout(CONNECTION_TIMEOUT);
+               connection.setInstanceFollowRedirects(true);
+               connection.setRequestMethod("POST");
+               connection.setUseCaches(false);
+               connection.setRequestProperty("X-OpenRocket-Version", encode(Prefs.getVersion()));
+               
+               String post;
+               post = (VERSION_PARAM + "=" + encode(Prefs.getVersion())
+                               + "&" + BUG_REPORT_PARAM + "=" + encode(report));
+               
+               OutputStreamWriter wr = null;
+               try {
+                       // Send post information
+                       connection.setDoOutput(true);
+                       wr = new OutputStreamWriter(connection.getOutputStream(), "UTF-8");
+                       wr.write(post);
+                       wr.flush();
+                       
+                       if (connection.getResponseCode() != BUG_REPORT_RESPONSE_CODE) {
+                               throw new IOException("Server responded with code " + 
+                                               connection.getResponseCode() + ", expecting " + BUG_REPORT_RESPONSE_CODE);
+                       }
+               } finally {
+                       if (wr != null)
+                               wr.close();
+                       connection.disconnect();
+               }
+       }
+       
+       
+       
+       /**
+        * Start an asynchronous task that will fetch information about the latest
+        * OpenRocket version.  This will overwrite any previous fetching operation.
+        */
+       public static void startFetchUpdateInfo() {
+               fetcher = new UpdateInfoFetcher();
+               fetcher.start();
+       }
+       
+       
+       /**
+        * Check whether the update info fetching is still in progress.
+        * 
+        * @return      <code>true</code> if the communication is still in progress.
+        */
+       public static boolean isFetchUpdateInfoRunning() {
+               if (fetcher == null) {
+                       throw new IllegalStateException("startFetchUpdateInfo() has not been called");
+               }
+               return fetcher.isAlive();
+       }
+       
+       
+       /**
+        * Retrieve the result of the background update info fetcher.  This method returns 
+        * the result of the previous call to {@link #startFetchUpdateInfo()}. It must be
+        * called before calling this method.
+        * <p>
+        * This method will return <code>null</code> if the info fetcher is still running or
+        * if it encountered a problem in communicating with the server.  The difference can
+        * be checked using {@link #isFetchUpdateInfoRunning()}.
+        * 
+        * @return      the update result, or <code>null</code> if the fetching is still in progress
+        *                      or an error occurred while communicating with the server.
+        * @throws      IllegalStateException   if {@link #startFetchUpdateInfo()} has not been called.
+        */
+       public static UpdateInfo getUpdateInfo() {
+               if (fetcher == null) {
+                       throw new IllegalStateException("startFetchUpdateInfo() has not been called");
+               }
+               return fetcher.info;
+       }
+       
+       
+       
+       /**
+        * Parse the data received from the server.
+        * 
+        * @param r             the Reader from which to read.
+        * @return              an UpdateInfo construct, or <code>null</code> if the data was invalid.
+        * @throws IOException  if an I/O exception occurs.
+        */
+       /* package-private */
+       static UpdateInfo parseUpdateInput(Reader r) throws IOException {
+               BufferedReader reader;
+               if (r instanceof BufferedReader) {
+                       reader = (BufferedReader)r;
+               } else {
+                       reader = new BufferedReader(r);
+               }
+               
+               
+               String version = null;
+               ArrayList<ComparablePair<Integer,String>> updates = 
+                       new ArrayList<ComparablePair<Integer,String>>();
+               
+               String str = reader.readLine();
+               while (str != null) {
+                       if (str.matches("^Version: *[0-9]+\\.[0-9]+\\.[0-9]+[a-zA-Z0-9.-]* *$")) {
+                               version = str.substring(8).trim();
+                       } else if (str.matches("^[0-9]+:\\p{Print}+$")) {
+                               int index = str.indexOf(':');
+                               int value = Integer.parseInt(str.substring(0, index));
+                               String desc = str.substring(index+1).trim();
+                               if (!desc.equals("")) {
+                                       updates.add(new ComparablePair<Integer,String>(value, desc));
+                               }
+                       }
+                       // Ignore anything else
+                       str = reader.readLine();
+               }
+               
+               if (version != null) {
+                       return new UpdateInfo(version, updates);
+               } else {
+                       return null;
+               }
+       }
+       
+       
+       
+       
+       private static class UpdateInfoFetcher extends Thread {
+
+               private volatile UpdateInfo info = null;
+               
+               @Override
+               public void run() {
+                       try {
+                               doConnection();
+                       } catch (IOException e) {
+                               return;
+                       }
+               }
+               
+               
+               private void doConnection() throws IOException {
+                       URL url;
+                       url = new URL(UPDATE_INFO_URL + "?" + VERSION_PARAM + "=" + 
+                                       encode(Prefs.getVersion()));
+                       
+                       HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+                       
+                       connection.setConnectTimeout(CONNECTION_TIMEOUT);
+                       connection.setInstanceFollowRedirects(true);
+                       connection.setRequestMethod("GET");
+                       connection.setUseCaches(false);
+                       connection.setRequestProperty("X-OpenRocket-Version", encode(Prefs.getVersion()));
+                       connection.setRequestProperty("X-OpenRocket-ID", encode(Prefs.getUniqueID()));
+                       connection.setRequestProperty("X-OpenRocket-OS", encode(
+                                       System.getProperty("os.name") + " " + System.getProperty("os.arch")));
+                       connection.setRequestProperty("X-OpenRocket-Java", encode(
+                                       System.getProperty("java.vendor") + " " + System.getProperty("java.version")));
+                       connection.setRequestProperty("X-OpenRocket-Country", encode(
+                                       System.getProperty("user.country")));
+                       
+                       InputStream is = null;
+                       try {
+                               connection.connect();
+                               
+                               if (connection.getResponseCode() == UPDATE_INFO_NO_UPDATE_CODE) {
+                                       // No updates are available
+                                       info = new UpdateInfo();
+                                       return;
+                               }
+                               
+                               if (connection.getResponseCode() != UPDATE_INFO_UPDATE_AVAILABLE) {
+                                       // Error communicating with server
+                                       return;
+                               }
+                               
+                               if (!UPDATE_INFO_CONTENT_TYPE.equalsIgnoreCase(connection.getContentType())) {
+                                       // Unknown response type
+                                       return;
+                               }
+                               
+                               // Update is available, parse input
+                               is = connection.getInputStream();
+                               String encoding = connection.getContentEncoding();
+                               if (encoding == null)
+                                       encoding = "UTF-8";
+                               BufferedReader reader = new BufferedReader(new InputStreamReader(is, encoding));
+                               
+                               
+
+                       } finally {
+                               if (is != null)
+                                       is.close();
+                               connection.disconnect();
+                       }
+
+                       
+               }
+               
+       }
+       
+       
+       private static String encode(String str) {
+               if (str == null)
+                       return "null";
+               try {
+                       return URLEncoder.encode(str, "UTF-8");
+               } catch (UnsupportedEncodingException e) {
+                       throw new RuntimeException("Unsupported encoding UTF-8", e);
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/communication/UpdateInfo.java b/src/net/sf/openrocket/communication/UpdateInfo.java
new file mode 100644 (file)
index 0000000..73e3963
--- /dev/null
@@ -0,0 +1,50 @@
+package net.sf.openrocket.communication;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.util.ComparablePair;
+import net.sf.openrocket.util.Prefs;
+
+public class UpdateInfo {
+
+       private final String latestVersion;
+       
+       private final ArrayList<ComparablePair<Integer, String>> updates;
+       
+       
+       public UpdateInfo() {
+               this.latestVersion = Prefs.getVersion();
+               this.updates = new ArrayList<ComparablePair<Integer, String>>();
+       }
+       
+       public UpdateInfo(String version, List<ComparablePair<Integer, String>> updates) {
+               this.latestVersion = version;
+               this.updates = new ArrayList<ComparablePair<Integer, String>>(updates);
+       }
+
+
+
+       /**
+        * Get the latest OpenRocket version.  If it is the current version, then the value
+        * of {@link Prefs#getVersion()} is returned.
+        * 
+        * @return      the latest OpenRocket version.
+        */
+       public String getLatestVersion() {
+               return latestVersion;
+       }
+
+
+       /**
+        * Return a list of the new features/updates that are available.  The list has a
+        * priority for each update and a message text.  The returned list may be modified.
+        * 
+        * @return      a modifiable list of the updates.
+        */
+       @SuppressWarnings("unchecked")
+       public List<ComparablePair<Integer, String>> getUpdates() {
+               return (List<ComparablePair<Integer, String>>) updates.clone();
+       }
+       
+}
index 1d33fd18fb63bab5f9f07ffeea0d8241ca34fb64..c5d5f54c947c537d37cfc57de6fa2fadbc806341 100644 (file)
@@ -57,6 +57,7 @@ import net.sf.openrocket.rocketcomponent.Transition;
 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
 import net.sf.openrocket.rocketcomponent.TubeCoupler;
 import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.rocketcomponent.FinSet.TabRelativePosition;
 import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
 import net.sf.openrocket.simulation.FlightData;
 import net.sf.openrocket.simulation.FlightDataBranch;
@@ -144,7 +145,7 @@ public class OpenRocketLoader extends RocketLoader {
 class DocumentConfig {
 
        /* Remember to update OpenRocketSaver as well! */
-       public static final String[] SUPPORTED_VERSIONS = { "0.9", "1.0" };
+       public static final String[] SUPPORTED_VERSIONS = { "0.9", "1.0", "1.1" };
 
 
        ////////  Component constructors
@@ -289,6 +290,11 @@ class DocumentConfig {
                                FinSet.CrossSection.class));
                setters.put("FinSet:cant", new DoubleSetter(
                                Reflection.findMethodStatic(FinSet.class, "setCantAngle", double.class), Math.PI/180.0));
+               setters.put("FinSet:tabheight", new DoubleSetter(
+                               Reflection.findMethodStatic(FinSet.class, "setTabHeight", double.class)));
+               setters.put("FinSet:tablength", new DoubleSetter(
+                               Reflection.findMethodStatic(FinSet.class, "setTabLength", double.class)));
+               setters.put("FinSet:tabposition", new FinTabPositionSetter());
                
                // TrapezoidFinSet
                setters.put("TrapezoidFinSet:rootchord", new DoubleSetter(
@@ -462,7 +468,8 @@ class DocumentConfig {
         * @param enumClass             the class of the enum.
         * @return                              the found enum value, or <code>null</code>.
         */
-       public static <T extends Enum<T>> Enum<T> findEnum(String name, Class<? extends Enum<T>> enumClass) {
+       public static <T extends Enum<T>> Enum<T> findEnum(String name, 
+                       Class<? extends Enum<T>> enumClass) {
                
                if (name == null)
                        return null;
@@ -1901,6 +1908,43 @@ class PositionSetter implements Setter {
 }
 
 
+class FinTabPositionSetter extends DoubleSetter {
+
+       public FinTabPositionSetter() {
+               super(Reflection.findMethodStatic(FinSet.class, "setTabShift", double.class));
+       }
+
+       @Override
+       public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               
+               if (!(c instanceof FinSet)) {
+                       throw new IllegalStateException("FinTabPositionSetter called for component " + c);
+               }
+
+               String relative = attributes.get("relativeto");
+               FinSet.TabRelativePosition position = 
+                       (TabRelativePosition) DocumentConfig.findEnum(relative,
+                                       FinSet.TabRelativePosition.class);
+               
+               if (position != null) {
+                       
+                       ((FinSet)c).setTabRelativePosition(position);
+                       
+               } else {
+                       if (relative == null) {
+                               warnings.add("Required attribute 'relativeto' not found for fin tab position.");
+                       } else {
+                               warnings.add("Illegal attribute value '" + relative + "' encountered.");
+                       }
+               }
+               
+               super.set(c, s, attributes, warnings);
+       }
+
+       
+}
+
 
 class ClusterConfigurationSetter implements Setter {
 
index de41cb0ca4efc7a2194111eb1d3b6f1ff01eae97..312aa0bb311f1da0c61b830d256730258cc034d1 100644 (file)
@@ -14,12 +14,15 @@ import net.sf.openrocket.aerodynamics.Warning;
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.document.Simulation;
 import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.rocketcomponent.FinSet;
 import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
 import net.sf.openrocket.simulation.FlightData;
 import net.sf.openrocket.simulation.FlightDataBranch;
 import net.sf.openrocket.simulation.FlightEvent;
 import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.Pair;
 import net.sf.openrocket.util.Prefs;
 import net.sf.openrocket.util.Reflection;
@@ -27,8 +30,13 @@ import net.sf.openrocket.util.TextUtil;
 
 public class OpenRocketSaver extends RocketSaver {
        
-       /* Remember to update OpenRocketLoader as well! */
-       public static final String FILE_VERSION = "1.0";
+       /**
+        * Divisor used in converting an integer version to the point-represented version.
+        * The integer version divided by this value is the major version and the remainder is
+        * the minor version.  For example 101 corresponds to file version "1.1".
+        */
+       public static final int FILE_VERSION_DIVISOR = 100;
+
        
        private static final String OPENROCKET_CHARSET = "UTF-8";
        
@@ -59,14 +67,18 @@ public class OpenRocketSaver extends RocketSaver {
                
                dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET)); 
                
+               final int fileVersion = calculateNecessaryFileVersion(document, options);
+               final String fileVersionString = 
+                       (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR); 
+               
                
                this.indent = 0;
                
                System.out.println("Writing...");
                
                writeln("<?xml version='1.0' encoding='utf-8'?>");
-               writeln("<openrocket version=\""+FILE_VERSION+"\" creator=\"OpenRocket "
-                               +Prefs.getVersion()+ "\">");
+               writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket "
+                               + Prefs.getVersion() + "\">");
                indent++;
                
                // Recursively save the rocket structure
@@ -91,8 +103,9 @@ public class OpenRocketSaver extends RocketSaver {
                writeln("</openrocket>");
                
                dest.flush();
-               if (output instanceof GZIPOutputStream)
+               if (options.isCompressionEnabled()) {
                        ((GZIPOutputStream)output).finish();
+               }
        }
        
        
@@ -147,6 +160,51 @@ public class OpenRocketSaver extends RocketSaver {
        }
        
 
+       /**
+        * Determine which file version is required in order to store all the features of the
+        * current design.  By default the oldest version that supports all the necessary features
+        * will be used.
+        * 
+        * @param document      the document to output.
+        * @param opts          the storage options.
+        * @return                      the integer file version to use.
+        */
+       private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) {
+               /*
+                * File version 1.1 is required for:
+                *  - fin tabs
+                *  - components attached to tube coupler
+                * 
+                * Otherwise use version 1.0.
+                */
+               
+               // Check for fin tabs (version 1.1)
+               Iterator<RocketComponent> iterator = document.getRocket().deepIterator();
+               while (iterator.hasNext()) {
+                       RocketComponent c = iterator.next();
+                       
+                       // Check for fin tabs
+                       if (c instanceof FinSet) {
+                               FinSet fin = (FinSet)c;
+                               if (!MathUtil.equals(fin.getTabHeight(),0) &&
+                                               !MathUtil.equals(fin.getTabLength(), 0)) {
+                                       return FILE_VERSION_DIVISOR + 1;
+                               }
+                       }
+                       
+                       // Check for components attached to tube coupler
+                       if (c instanceof TubeCoupler) {
+                               if (c.getChildCount() > 0) {
+                                       return FILE_VERSION_DIVISOR + 1;
+                               }
+                       }
+               }
+               
+               // Default (version 1.0)
+               return FILE_VERSION_DIVISOR + 0;
+       }
+       
+       
        
        @SuppressWarnings("unchecked")
        private void saveComponent(RocketComponent component) throws IOException {
index 42f4d8967a141d922ca8435b3af10ae89e4e7932..756115e4044abd215bcef298b95a898def0c0880 100644 (file)
@@ -2,6 +2,8 @@ package net.sf.openrocket.file.openrocket;
 
 import java.util.List;
 
+import net.sf.openrocket.util.MathUtil;
+
 public class FinSetSaver extends ExternalComponentSaver {
 
        @Override
@@ -15,6 +17,18 @@ public class FinSetSaver extends ExternalComponentSaver {
                elements.add("<crosssection>" + fins.getCrossSection().name().toLowerCase()
                                + "</crosssection>");
                elements.add("<cant>" + (fins.getCantAngle() * 180.0 / Math.PI) + "</cant>");
+               
+               // Save fin tabs only if they exist (compatibility with file version < 1.1)
+               if (!MathUtil.equals(fins.getTabHeight(),0) &&
+                               !MathUtil.equals(fins.getTabLength(), 0)) {
+                       
+                       elements.add("<tabheight>" + fins.getTabHeight() + "</tabheight>");
+                       elements.add("<tablength>" + fins.getTabLength() + "</tablength>");
+                       elements.add("<tabposition relativeto=\"" +
+                                       fins.getTabRelativePosition().name().toLowerCase() + "\">" +
+                                       fins.getTabShift() + "</tabposition>");
+               
+               }
        }
 
 }
index 3b22d11b204450f03dde24bf8fc357bb26c57922..733413674887cd954c635a682b1d29999466f82f 100644 (file)
@@ -46,6 +46,12 @@ public abstract class Column {
        }
        
        
+       /**
+        * Return the column type class.  This is necessary for example for numerical
+        * sorting of Value objects, showing booleans as checkboxes etc.
+        * 
+        * @return      the object class of this column, by default <code>Object.class</code>.
+        */
        public Class<?> getColumnClass() {
                return Object.class;
        }
index 651738c18bdd1b5c6dfc23ef1e91827d05ab682b..53011f3750de58f82b89def4c9947193a43a789a 100644 (file)
@@ -4,6 +4,8 @@ import javax.swing.table.AbstractTableModel;
 import javax.swing.table.TableColumn;
 import javax.swing.table.TableColumnModel;
 
+import net.sf.openrocket.gui.main.ExceptionHandler;
+
 public abstract class ColumnTableModel extends AbstractTableModel {
        private final Column[] columns;
        
@@ -45,7 +47,8 @@ public abstract class ColumnTableModel extends AbstractTableModel {
        public Object getValueAt(int row, int col) {
                if ((row < 0) || (row >= getRowCount()) ||
                                (col < 0) || (col >= columns.length)) {
-                       System.err.println("Error:  Requested illegal column/row = "+col+"/"+row+".");
+                       ExceptionHandler.handleErrorCondition("Error:  Requested illegal column/row, " +
+                                       "col="+col+" row="+row);
                        assert(false);
                        return null;
                }
index f2125b630162f012ee6398fc4f0d8f1a1dc6367e..b39cb0104de62fb78ec5071aceb8358f65aedcdb 100644 (file)
@@ -37,6 +37,8 @@ import net.sf.openrocket.util.MathUtil;
 
 public class DoubleModel implements ChangeListener, ChangeSource {
        private static final boolean DEBUG_LISTENERS = false;
+       
+       public static final DoubleModel ZERO = new DoubleModel(0);
 
        //////////// JSpinner Model ////////////
        
@@ -706,7 +708,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
        
        
        /**
-        * Add a listener to the model.  Adds the model as a listener to the Component if this
+        * Add a listener to the model.  Adds the model as a listener to the value source if this
         * is the first listener.
         * @param l Listener to add.
         */
index 8834e48af25154ee3e1d8730d47235a612434290..be8df36eb4a762492949e8a71127c2dc1ca2f404 100644 (file)
@@ -1,32 +1,20 @@
 package net.sf.openrocket.gui.adaptors;
 
 
-import java.awt.Dialog;
-import java.awt.Window;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
+import java.awt.Component;
 
 import javax.swing.AbstractListModel;
 import javax.swing.ComboBoxModel;
-import javax.swing.JButton;
-import javax.swing.JCheckBox;
-import javax.swing.JDialog;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSpinner;
-import javax.swing.JTextField;
 import javax.swing.SwingUtilities;
 
-import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.database.Database;
 import net.sf.openrocket.database.DatabaseListener;
 import net.sf.openrocket.database.Databases;
-import net.sf.openrocket.gui.components.UnitSelector;
+import net.sf.openrocket.gui.dialogs.CustomMaterialDialog;
 import net.sf.openrocket.material.Material;
 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
-import net.sf.openrocket.unit.UnitGroup;
 import net.sf.openrocket.util.Reflection;
 
 public class MaterialModel extends AbstractListModel implements
@@ -35,6 +23,8 @@ public class MaterialModel extends AbstractListModel implements
        private static final String CUSTOM = "Custom";
 
        
+       private final Component parentComponent;
+       
        private final RocketComponent component;
        private final Material.Type type;
        private final Database<Material> database;
@@ -43,11 +33,13 @@ public class MaterialModel extends AbstractListModel implements
        private final Reflection.Method setMethod;
        
        
-       public MaterialModel(RocketComponent component, Material.Type type) {
-               this(component, type, "Material");
+       public MaterialModel(Component parent, RocketComponent component, Material.Type type) {
+               this(parent, component, type, "Material");
        }       
 
-       public MaterialModel(RocketComponent component, Material.Type type, String name) {
+       public MaterialModel(Component parent, RocketComponent component, Material.Type type, 
+                       String name) {
+               this.parentComponent = parent;
                this.component = component;
                this.type = type;
                
@@ -94,18 +86,20 @@ public class MaterialModel extends AbstractListModel implements
                        SwingUtilities.invokeLater(new Runnable() {
                                @Override
                                public void run() {
-                                       AddMaterialDialog dialog = new AddMaterialDialog();
+                                       CustomMaterialDialog dialog = new CustomMaterialDialog(
+                                                       SwingUtilities.getWindowAncestor(parentComponent), 
+                                                       (Material) getSelectedItem(), true,
+                                                       "Define custom material");
+
                                        dialog.setVisible(true);
                                        
-                                       if (!dialog.okClicked)
+                                       if (!dialog.getOkClicked())
                                                return;
                                        
-                                       Material material = Material.newMaterial(type, 
-                                                       dialog.nameField.getText().trim(),
-                                                       dialog.density.getValue(), true);
+                                       Material material = dialog.getMaterial();
                                        setMethod.invoke(component, material);
                                        
-                                       if (dialog.addBox.isSelected()) {
+                                       if (dialog.isAddSelected()) {
                                                database.add(material);
                                        }
                                }
@@ -156,65 +150,5 @@ public class MaterialModel extends AbstractListModel implements
        public void elementRemoved(Material element, Database<Material> source) {
                this.fireContentsChanged(this, 0, database.size());
        }
-
-
-
-
-       
        
-       private class AddMaterialDialog extends JDialog {
-               
-               private boolean okClicked = false;
-               private JTextField nameField;
-               private DoubleModel density;
-               private JCheckBox addBox;
-               
-               public AddMaterialDialog() {
-                       super((Window)null, "Custom material", Dialog.ModalityType.APPLICATION_MODAL);
-                       
-                       Material material = (Material) getSelectedItem();
-                       
-                       JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]"));
-                       
-                       panel.add(new JLabel("Material name:"));
-                       nameField = new JTextField(15);
-                       nameField.setText(material.getName());
-                       panel.add(nameField,"span 2, growx, wrap");
-                       
-                       panel.add(new JLabel("Material density:"));
-                       density = new DoubleModel(material.getDensity(),UnitGroup.UNITS_DENSITY_BULK,0);
-                       JSpinner spinner = new JSpinner(density.getSpinnerModel());
-                       panel.add(spinner, "growx");
-                       panel.add(new UnitSelector(density),"wrap");
-                       
-                       addBox = new JCheckBox("Add material to database");
-                       panel.add(addBox,"span, wrap");
-                       
-                       JButton button = new JButton("OK");
-                       button.addActionListener(new ActionListener() {
-                               @Override
-                               public void actionPerformed(ActionEvent e) {
-                                       okClicked = true;
-                                       AddMaterialDialog.this.setVisible(false);
-                               }
-                       });
-                       panel.add(button,"span, split, tag ok");
-                       
-                       button = new JButton("Cancel");
-                       button.addActionListener(new ActionListener() {
-                               @Override
-                               public void actionPerformed(ActionEvent e) {
-                                       AddMaterialDialog.this.setVisible(false);
-                               }
-                       });
-                       panel.add(button,"tag cancel");
-                       
-                       this.setContentPane(panel);
-                       this.pack();
-                       this.setAlwaysOnTop(true);
-                       this.setLocationRelativeTo(null);
-               }
-       }
-
-
 }
diff --git a/src/net/sf/openrocket/gui/components/CollectionTable.java b/src/net/sf/openrocket/gui/components/CollectionTable.java
new file mode 100644 (file)
index 0000000..ef14dc6
--- /dev/null
@@ -0,0 +1,75 @@
+package net.sf.openrocket.gui.components;
+
+import javax.swing.JTable;
+import javax.swing.table.AbstractTableModel;
+
+
+/*
+ * TODO: LOW:  This is currently unused.
+ */
+public abstract class CollectionTable<T> extends JTable {
+
+       private final String[] columnNames;
+       private CollectionTableModel model;
+       
+       
+       protected CollectionTable(String[] columnNames) {
+               this.columnNames = columnNames.clone();
+       }
+       
+       
+       protected void initializeTable() {
+               model = new CollectionTableModel();
+               this.setModel(model);
+       }
+       
+       
+       /**
+        * Retrieve the object for the specified row number.
+        * 
+        * @param row   the row number being queried.
+        * @return              the object at that row.
+        */
+       protected abstract T getModelObjectAt(int row);
+       
+       protected abstract int getModelRowCount();
+       
+       
+       
+       protected abstract Object getViewForModelObject(T object, int column);
+       
+       protected Class<?> getViewColumnClass(int column) {
+               return Object.class;
+       }
+       
+       
+       
+       private class CollectionTableModel extends AbstractTableModel {
+               @Override
+               public int getColumnCount() {
+                       return columnNames.length;
+               }
+               
+               @Override
+               public String getColumnName(int column) {
+                       return columnNames[column];
+               }
+
+               @Override
+               public Class<?> getColumnClass(int column) {
+                       return getViewColumnClass(column);
+               }
+
+
+               @Override
+               public int getRowCount() {
+                       return getModelRowCount();
+               }
+
+               @Override
+               public Object getValueAt(int row, int column) {
+                       T value = getModelObjectAt(row);
+                       return getViewForModelObject(value, column);
+               }
+       }
+}
index 962992e2bf5b4f63946b3b49569e99142b04a230..8a81a9adb5114ef7ca3227c371996f2cc2362343 100644 (file)
@@ -39,7 +39,7 @@ import net.sf.openrocket.unit.UnitGroup;
 public class UnitSelector extends ResizeLabel implements ChangeListener, MouseListener,
                ItemSelectable {
 
-       private final DoubleModel model;
+       private DoubleModel model;
        private final Action[] extraActions;
 
        private UnitGroup unitGroup;
@@ -73,9 +73,12 @@ public class UnitSelector extends ResizeLabel implements ChangeListener, MouseLi
                if (model != null) {
                        this.unitGroup = model.getUnitGroup();
                        this.currentUnit = model.getCurrentUnit();
-               } else {
+               } else if (group != null) {
                        this.unitGroup = group;
                        this.currentUnit = group.getDefaultUnit();
+               } else {
+                       this.unitGroup = UnitGroup.UNITS_NONE;
+                       this.currentUnit = UnitGroup.UNITS_NONE.getDefaultUnit();
                }
 
                this.extraActions = actions;
@@ -90,6 +93,10 @@ public class UnitSelector extends ResizeLabel implements ChangeListener, MouseLi
                withinBorder = new CompoundBorder(new LineBorder(new Color(0f, 0f, 0f, 0.6f)),
                                new EmptyBorder(1, 1, 1, 1));
 
+               // Add model listener if showing value
+               if (showValue)
+                       this.model.addChangeListener(this);
+               
                setBorder(normalBorder);
                updateText();
        }
@@ -102,9 +109,6 @@ public class UnitSelector extends ResizeLabel implements ChangeListener, MouseLi
 
        public UnitSelector(DoubleModel model, boolean showValue, Action... actions) {
                this(model, showValue, null, actions);
-
-               // Add model listener
-               this.model.addChangeListener(this);
        }
 
 
@@ -125,6 +129,26 @@ public class UnitSelector extends ResizeLabel implements ChangeListener, MouseLi
                return model;
        }
 
+       
+       /**
+        * Set the current double model.  
+        * 
+        * @param model         the model to set, <code>null</code> is NOT allowed.
+        */
+       public void setModel(DoubleModel model) {
+               if (this.model != null && showValue) {
+                       this.model.removeChangeListener(this);
+               }
+               this.model = model;
+               this.unitGroup = model.getUnitGroup();
+               this.currentUnit = model.getCurrentUnit();
+               if (showValue) {
+                       this.model.addChangeListener(this);
+               }
+               updateText();
+       }
+       
+       
 
        /**
         * Return the unit group that is being shown, or <code>null</code>.  Either this method
index e785e45e737be5b956d02d33c8d3459ded7f0e81..0ad6c93ac08d87f9b14699f8cbf148a8359a63fb 100644 (file)
@@ -1,8 +1,6 @@
 package net.sf.openrocket.gui.configdialog;
 
 
-import java.awt.Component;
-import java.awt.Container;
 import java.awt.Point;
 import java.awt.Window;
 import java.awt.event.ComponentAdapter;
@@ -10,14 +8,9 @@ import java.awt.event.ComponentEvent;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 
-import javax.swing.DefaultBoundedRangeModel;
 import javax.swing.JDialog;
-import javax.swing.JSlider;
-import javax.swing.JSpinner;
-import javax.swing.SpinnerNumberModel;
 
 import net.sf.openrocket.document.OpenRocketDocument;
-import net.sf.openrocket.gui.Resettable;
 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
@@ -25,7 +18,7 @@ import net.sf.openrocket.util.GUIUtil;
 import net.sf.openrocket.util.Prefs;
 
 /**
- * A JFrame dialog that contains the configuration elements of one component.
+ * A dialog that contains the configuration elements of one component.
  * The contents of the dialog are instantiated from CONFIGDIALOGPACKAGE according
  * to the current component.
  * 
@@ -69,9 +62,7 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis
                        }
                });
                
-               
-               // Install ESC listener
-               GUIUtil.installEscapeCloseOperation(this);
+               GUIUtil.setDisposableDialogOptions(this, null);
        }
        
 
@@ -88,9 +79,7 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis
 
                if (configurator != null) {
                        // Remove listeners by setting all applicable models to null
-                       setNullModels(configurator);  // null-safe
-
-//                     mainPanel.remove(configurator);
+                       GUIUtil.setNullModels(configurator);  // null-safe
                }
                
                this.document = document;
@@ -100,48 +89,15 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis
                configurator = getDialogContents();
                this.setContentPane(configurator);
                configurator.updateFields();
-//             mainPanel.add(configurator,"cell 0 0, growx, growy");
                
                setTitle(component.getComponentName()+" configuration");
 
 //             Dimension pref = getPreferredSize();
 //             Dimension real = getSize();
 //             if (pref.width > real.width || pref.height > real.height)
-               pack();
+               this.pack();
        }
        
-       /**
-        * Traverses recursively the component tree, and sets all applicable component 
-        * models to null, so as to remove the listener connections.
-        * 
-        * NOTE:  All components in the configuration dialogs that use custom models must be added
-        * to this method.
-        */
-       private void setNullModels(Component c) {
-               if (c==null)
-                       return;
-               
-               // Remove models for known components
-               //  Why the FSCK must this be so hard?!?!?
-
-               if (c instanceof JSpinner) {
-                       ((JSpinner)c).setModel(new SpinnerNumberModel());
-               } else if (c instanceof JSlider) {
-                       ((JSlider)c).setModel(new DefaultBoundedRangeModel());
-               } else if (c instanceof Resettable) {
-                       ((Resettable)c).resetModel();
-               }
-
-               
-               if (c instanceof Container) {
-                       Component[] cs = ((Container)c).getComponents();
-                       for (Component sub: cs)
-                               setNullModels(sub);
-               }
-
-       }
-       
-       
        /**
         * Return the configurator panel of the current component.
         */
index fb8e76cce77fd07f504b20a408e46a0081d0e2d7..efec1267965f97a6aa2ffaf6d8f10a9c88dc07c9 100644 (file)
@@ -4,11 +4,22 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 
 import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
 import javax.swing.SwingUtilities;
 
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.components.BasicSlider;
+import net.sf.openrocket.gui.components.UnitSelector;
 import net.sf.openrocket.rocketcomponent.FinSet;
 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
 
 
 public abstract class FinSetConfig extends RocketComponentConfig {
@@ -17,6 +28,8 @@ public abstract class FinSetConfig extends RocketComponentConfig {
        
        public FinSetConfig(RocketComponent component) {
                super(component);
+               
+               tabbedPane.insertTab("Fin tabs", null, finTabPanel(), "Through-the-wall fin tabs", 0);
        }
 
        
@@ -35,20 +48,9 @@ public abstract class FinSetConfig extends RocketComponentConfig {
                                        SwingUtilities.invokeLater(new Runnable() {
                                                @Override
                                                public void run() {
-                                                       FreeformFinSet freeform = new FreeformFinSet((FinSet)component);
-                                                       String name = component.getComponentName();
-                                                       
-                                                       if (freeform.getName().startsWith(name)) {
-                                                               freeform.setName(freeform.getComponentName() + 
-                                                                               freeform.getName().substring(name.length()));
-                                                       }
-                                                       
-                                                       RocketComponent parent = component.getParent();
-                                                       int index = parent.getChildPosition(component);
-
                                                        ComponentConfigDialog.addUndoPosition("Convert fin set");
-                                                       parent.removeChild(index);
-                                                       parent.addChild(freeform, index);
+                                                       RocketComponent freeform = 
+                                                               FreeformFinSet.convertFinSet((FinSet)component);
                                                        ComponentConfigDialog.showDialog(freeform);
                                                }
                                        });
@@ -98,6 +100,84 @@ public abstract class FinSetConfig extends RocketComponentConfig {
 
        }
 
+       public JPanel finTabPanel() {
+               JPanel panel = new JPanel(
+                               new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%",
+                               "[150lp::][65lp::][30lp::][200lp::]",""));
+//             JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
+//                             "[40lp][80lp::][30lp::][100lp::]",""));
+
+               panel.add(new JLabel("<html><b>Through-the-wall fin tabs:</b>"), "spanx, wrap 30lp");
+               
+               JLabel label;
+               DoubleModel m;
+               DoubleModel length;
+               DoubleModel length2;
+               DoubleModel length_2;
+               JSpinner spin;
+
+               length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0);
+               length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0);
+               length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0);
+               
+               ////  Tab length
+               label = new JLabel("Tab length:");
+               label.setToolTipText("The length of the fin tab.");
+               panel.add(label, "gapleft para, gapright 40lp, growx 1");
+               
+               m = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx 1");
+               
+               panel.add(new UnitSelector(m),"growx 1");
+               panel.add(new BasicSlider(m.getSliderModel(DoubleModel.ZERO, length)),
+                               "w 100lp, growx 5, wrap");
+
+               
+               ////  Tab length
+               label = new JLabel("Tab height:");
+               label.setToolTipText("The spanwise height of the fin tab.");
+               panel.add(label, "gapleft para");
+               
+               m = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(DoubleModel.ZERO, length2)),
+                               "w 100lp, growx 5, wrap para");
+
+               
+               ////  Tab position
+               label = new JLabel("Tab position:");
+               label.setToolTipText("The position of the fin tab.");
+               panel.add(label, "gapleft para");
+               
+               m = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(length_2, length2)),"w 100lp, growx 5, wrap");
+
+               
+               
+               label = new JLabel("relative to");
+               panel.add(label, "right, gapright unrel");
+               
+               EnumModel<FinSet.TabRelativePosition> em = 
+                       new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
+               
+               panel.add(new JComboBox(em), "spanx 3, growx");
+               
+               return panel;
+       }
 
        @Override
        public void updateFields() {
index c4210e6c2f36a1ab40f7b3ea377b48de67b4c078..7cfc1bd6fe6c8c859ea124c899413324ecefb46d 100644 (file)
@@ -152,7 +152,7 @@ public class MotorConfig extends JPanel {
                                updateFields();
                        }
                });
-               panel.add(button,"span, split, grow");
+               panel.add(button,"span, split, growx");
                
                button = new JButton("Remove motor");
                button.addActionListener(new ActionListener() {
@@ -162,7 +162,7 @@ public class MotorConfig extends JPanel {
                                updateFields();
                        }
                });
-               panel.add(button,"grow, wrap");
+               panel.add(button,"growx, wrap");
                
                
                
index c058725d95e7d68935e5030808d182d8147a64df..f41ca5ce2c0733db0b0a5edccf00e7ec6586f3e1 100644 (file)
@@ -52,7 +52,8 @@ public class ParachuteConfig extends RecoveryDeviceConfig {
                
                panel.add(new JLabel("Material:"));
                
-               JComboBox combo = new JComboBox(new MaterialModel(component, Material.Type.SURFACE));
+               JComboBox combo = new JComboBox(new MaterialModel(panel, component, 
+                               Material.Type.SURFACE));
                combo.setToolTipText("The component material affects the weight of the component.");
                panel.add(combo,"spanx 3, growx, wrap paragraph");
 
@@ -112,7 +113,7 @@ public class ParachuteConfig extends RecoveryDeviceConfig {
                
                panel.add(new JLabel("Material:"));
                
-               combo = new JComboBox(new MaterialModel(component, Material.Type.LINE, 
+               combo = new JComboBox(new MaterialModel(panel, component, Material.Type.LINE, 
                                "LineMaterial"));
                panel.add(combo,"spanx 3, growx, wrap");
 
index 5e017a4b43024645201a58f45f090634f1821703..0d2461b179d8804b12c8389f93a165a951f43d45 100644 (file)
@@ -174,7 +174,7 @@ public class RocketComponentConfig extends JPanel {
                label.setToolTipText("The component material affects the weight of the component.");
                panel.add(label,"spanx 4, wrap rel");
                
-               JComboBox combo = new JComboBox(new MaterialModel(component,type));
+               JComboBox combo = new JComboBox(new MaterialModel(panel, component, type));
                combo.setToolTipText("The component material affects the weight of the component.");
                panel.add(combo,"spanx 4, growx, wrap paragraph");
                
@@ -229,8 +229,8 @@ public class RocketComponentConfig extends JPanel {
                JPanel panel = new JPanel(new MigLayout("align 50% 20%, fillx, gap rel unrel",
                                "[][65lp::][30lp::][]",""));
                
-               panel.add(new JLabel("Override the mass or center of gravity of the " +
-                               component.getComponentName() + ":"),"spanx, wrap 20lp");
+               panel.add(new JLabel("<html><b>Override the mass or center of gravity of the " +
+                               component.getComponentName() + ":</b>"),"spanx, wrap 20lp");
 
                JCheckBox check;
                BooleanModel bm;
@@ -315,7 +315,8 @@ public class RocketComponentConfig extends JPanel {
        private JPanel commentTab() {
                JPanel panel = new JPanel(new MigLayout("fill"));
                
-               panel.add(new JLabel("Comments on the "+component.getComponentName()+":"), "wrap");
+               panel.add(new JLabel("<html><b>Comments on the "+component.getComponentName()+":</b>"), 
+                               "wrap");
                
                // TODO: LOW:  Changes in comment from other sources not reflected in component
                commentTextArea = new JTextArea(component.getComment());
@@ -325,7 +326,7 @@ public class RocketComponentConfig extends JPanel {
                GUIUtil.setTabToFocusing(commentTextArea);
                commentTextArea.addFocusListener(textFieldListener);
                
-               panel.add(new JScrollPane(commentTextArea), "growx, growy");
+               panel.add(new JScrollPane(commentTextArea), "width 10px, height 10px, growx, growy");
                
                return panel;
        }
@@ -335,7 +336,7 @@ public class RocketComponentConfig extends JPanel {
        private JPanel figureTab() {
                JPanel panel = new JPanel(new MigLayout("align 20% 20%"));
                
-               panel.add(new JLabel("Figure style:"), "wrap para");
+               panel.add(new JLabel("<html><b>Figure style:</b>"), "wrap para");
                
                
                panel.add(new JLabel("Component color:"), "gapleft para, gapright 10lp");
index 132f6a5e585d96d05049896143772eb811e49411..02011b71f37a01f145e26a72c186c67e7386153d 100644 (file)
@@ -85,7 +85,8 @@ public class StreamerConfig extends RecoveryDeviceConfig {
                
                panel.add(new JLabel("Material:"));
                
-               JComboBox combo = new JComboBox(new MaterialModel(component, Material.Type.SURFACE));
+               JComboBox combo = new JComboBox(new MaterialModel(panel, component, 
+                               Material.Type.SURFACE));
                combo.setToolTipText("The component material affects the weight of the component.");
                panel.add(combo,"spanx 3, growx, wrap 20lp");
 
index 3b151f70947b509745f95550310d8320922003fc..516be5ec3012b762647f4e3670e6d8957a51ab57 100644 (file)
@@ -62,8 +62,8 @@ public class AboutDialog extends JDialog {
                this.pack();
                this.setResizable(false);
                this.setLocationRelativeTo(parent);
-               GUIUtil.setDefaultButton(close);
-               GUIUtil.installEscapeCloseOperation(this);
+               
+               GUIUtil.setDisposableDialogOptions(this, close);
        }
        
        
index b58d48a356f371360205ac76190781489c307adf..6df6803148be506d544f9a3c51558f3ced382936 100644 (file)
@@ -7,14 +7,11 @@ import java.awt.Window;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.io.IOException;
-import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
-import java.net.HttpURLConnection;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.URL;
 import java.net.URLEncoder;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -28,6 +25,7 @@ import javax.swing.JScrollPane;
 import javax.swing.JTextArea;
 
 import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.communication.Communication;
 import net.sf.openrocket.gui.components.ResizeLabel;
 import net.sf.openrocket.gui.components.SelectableLabel;
 import net.sf.openrocket.util.GUIUtil;
@@ -38,14 +36,6 @@ public class BugReportDialog extends JDialog {
        
        private static final String REPORT_EMAIL = "openrocket-bugs@lists.sourceforge.net";
        
-       private static final String REPORT_URL = 
-               "http://openrocket.sourceforge.net/actions/reportbug";
-
-       private static final String REPORT_VERSION_PARAM = "version";
-       private static final String REPORT_PARAM = "content";
-       private static final int REPORT_RESPONSE_CODE = HttpURLConnection.HTTP_ACCEPTED;
-       private static final int REPORT_TIMEOUT = 10000;  // in milliseconds
-       
 
        public BugReportDialog(Window parent, String labelText, String message) {
                super(parent, "Bug report", Dialog.ModalityType.APPLICATION_MODAL);
@@ -117,7 +107,8 @@ public class BugReportDialog extends JDialog {
                                String text = textArea.getText();
                                try {
                                        
-                                       sendReport(text);
+                                       Communication.sendBugReport(text);
+
                                        // Success if we came here
                                        JOptionPane.showMessageDialog(BugReportDialog.this,
                                                        new Object[] { "Bug report successfully sent.",
@@ -142,8 +133,8 @@ public class BugReportDialog extends JDialog {
                this.pack();
                this.pack();
                this.setLocationRelativeTo(parent);
-               GUIUtil.installEscapeCloseOperation(this);
-               GUIUtil.setDefaultButton(send);
+               
+               GUIUtil.setDisposableDialogOptions(this, send);
        }
 
        
@@ -282,50 +273,6 @@ public class BugReportDialog extends JDialog {
        
        
        
-       /**
-        * Send the provided report to the OpenRocket bug report URL.  If the connection
-        * fails or the server does not respond with the correct response code, an
-        * exception is thrown.
-        * 
-        * @param report                the report to send.
-        * @throws IOException  if an error occurs while connecting to the server or
-        *                                              the server responds with a wrong response code.
-        */
-       private void sendReport(String report) throws IOException {
-               URL url = new URL(REPORT_URL);
-               
-               HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-               
-               connection.setConnectTimeout(REPORT_TIMEOUT);
-               connection.setInstanceFollowRedirects(true);
-               connection.setRequestMethod("POST");
-               connection.setUseCaches(false);
-               connection.setRequestProperty("X-OpenRocket-Version", Prefs.getVersion());
-               
-               String post;
-               post = (REPORT_VERSION_PARAM + "=" + URLEncoder.encode(Prefs.getVersion(), "UTF-8")
-                               + "&" + REPORT_PARAM + "=" + URLEncoder.encode(report, "UTF-8"));
-               
-               OutputStreamWriter wr = null;
-               try {
-                       // Send post information
-                       connection.setDoOutput(true);
-                       wr = new OutputStreamWriter(connection.getOutputStream(), "UTF-8");
-                       wr.write(post);
-                       wr.flush();
-                       
-                       if (connection.getResponseCode() != REPORT_RESPONSE_CODE) {
-                               throw new IOException("Server responded with code " + 
-                                               connection.getResponseCode() + ", expecting " + REPORT_RESPONSE_CODE);
-                       }
-               } finally {
-                       if (wr != null)
-                               wr.close();
-                       connection.disconnect();
-               }
-       }
-       
-
        /**
         * Open the default email client with the suitable bug report.
         * Note that this does not work on some systems even if Desktop.isSupported()
index a5a184aeb2138b0c0c03cf0ef774a0e10f2252d6..8157744344b55763e15868e0aa697546cf182ebe 100644 (file)
@@ -414,8 +414,9 @@ public class ComponentAnalysisDialog extends JDialog implements ChangeListener {
 
                this.setLocationByPlatform(true);
                setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
-               GUIUtil.installEscapeCloseOperation(this);
                pack();
+               
+               GUIUtil.setDisposableDialogOptions(this, null);
        }
        
        
diff --git a/src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java b/src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java
new file mode 100644 (file)
index 0000000..7fb7ad4
--- /dev/null
@@ -0,0 +1,175 @@
+package net.sf.openrocket.gui.dialogs;
+
+import java.awt.Dialog;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.JTextField;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.components.ResizeLabel;
+import net.sf.openrocket.gui.components.UnitSelector;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.util.GUIUtil;
+
+public class CustomMaterialDialog extends JDialog {
+
+       private final Material originalMaterial;
+       
+       private boolean okClicked = false;
+       private JComboBox typeBox;
+       private JTextField nameField;
+       private DoubleModel density;
+       private JSpinner densitySpinner;
+       private UnitSelector densityUnit;
+       private JCheckBox addBox;
+       
+       public CustomMaterialDialog(Window parent, Material material, boolean saveOption,
+                       String title) {
+               this(parent, material, saveOption, title, null);
+       }
+       
+               
+       public CustomMaterialDialog(Window parent, Material material, boolean saveOption,
+                       String title, String note) {
+               super(parent, "Custom material", Dialog.ModalityType.APPLICATION_MODAL);
+               
+               this.originalMaterial = material;
+               
+               JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel"));
+               
+               
+               // Add title and note
+               if (title != null) {
+                       panel.add(new JLabel("<html><b>" + title + ":"), 
+                                       "gapleft para, span, wrap" + (note == null ? " para":""));
+               }
+               if (note != null) {
+                       panel.add(new ResizeLabel(note, -1), "span, wrap para");
+               }
+               
+
+               // Material name
+               panel.add(new JLabel("Material name:"));
+               nameField = new JTextField(15);
+               if (material != null) {
+                       nameField.setText(material.getName());
+               }
+               panel.add(nameField, "span, growx, wrap");
+               
+               
+               // Material type (if not known)
+               panel.add(new JLabel("Material type:"));
+               if (material == null) {
+                       typeBox = new JComboBox(Material.Type.values());
+                       typeBox.setSelectedItem(Material.Type.BULK);
+                       typeBox.setEditable(false);
+                       typeBox.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       updateDensityModel();
+                               }
+                       });
+                       panel.add(typeBox, "span, growx, wrap");
+               } else {
+                       panel.add(new JLabel(material.getType().toString()), "span, growx, wrap");
+               }
+               
+               
+               // Material density
+               panel.add(new JLabel("Material density:"));
+               densitySpinner = new JSpinner();
+               panel.add(densitySpinner, "w 70lp");
+               densityUnit = new UnitSelector((DoubleModel)null);
+               panel.add(densityUnit, "w 30lp");
+               panel.add(new JPanel(), "growx, wrap");
+               updateDensityModel();
+               
+               
+               // Save option
+               if (saveOption) {
+                       addBox = new JCheckBox("Add material to database");
+                       panel.add(addBox,"span, wrap");
+               }
+                       
+               JButton okButton = new JButton("OK");
+               okButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               okClicked = true;
+                               CustomMaterialDialog.this.setVisible(false);
+                       }
+               });
+               panel.add(okButton,"span, split, tag ok");
+               
+               JButton closeButton = new JButton("Cancel");
+               closeButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               okClicked = false;
+                               CustomMaterialDialog.this.setVisible(false);
+                       }
+               });
+               panel.add(closeButton,"tag cancel");
+               
+               this.setContentPane(panel);
+               this.pack();
+               this.setLocationByPlatform(true);
+               GUIUtil.setDisposableDialogOptions(this, okButton);
+       }
+       
+       
+       public boolean getOkClicked() {
+               return okClicked;
+       }
+       
+       
+       public boolean isAddSelected() {
+               return addBox.isSelected();
+       }
+       
+       
+       public Material getMaterial() {
+               Material.Type type;
+               String name;
+               double density;
+               
+               if (typeBox != null) {
+                       type = (Material.Type) typeBox.getSelectedItem();
+               } else {
+                       type = originalMaterial.getType();
+               }
+               
+               name = nameField.getText().trim();
+               
+               density = this.density.getValue();
+               
+               return Material.newMaterial(type, name, density, true);
+       }
+       
+       
+       private void updateDensityModel() {
+               if (originalMaterial != null) {
+                       if (density == null) {
+                               density = new DoubleModel(originalMaterial.getDensity(),
+                                               originalMaterial.getType().getUnitGroup(), 0);
+                               densitySpinner.setModel(density.getSpinnerModel());
+                               densityUnit.setModel(density);
+                       }
+               } else {
+                       Material.Type type = (Material.Type) typeBox.getSelectedItem();
+                       density = new DoubleModel(0, type.getUnitGroup(), 0);
+                       densitySpinner.setModel(density.getSpinnerModel());
+                       densityUnit.setModel(density);
+               }
+       }
+}
index a42d8a944fcf17f521781ffb58b501521c3894ae..550dee56d4005554bbb1554a4e4a61b4d9ec8f83 100644 (file)
@@ -249,9 +249,8 @@ public class EditMotorConfigurationDialog extends JDialog {
                
                updateEnabled();
                
-               GUIUtil.installEscapeCloseOperation(this);
-               GUIUtil.setDefaultButton(close);
                this.setLocationByPlatform(true);
+               GUIUtil.setDisposableDialogOptions(this, close);
                
                // Undo description
                final OpenRocketDocument document = BasicFrame.findDocument(rocket);
index b6d1ec423e96cbc4ad36ed65810d32f885317aa6..13a80c075b1842f27796bddb7c9bf9bed91b892f 100644 (file)
@@ -88,8 +88,8 @@ public class ExampleDesignDialog extends JDialog {
                this.add(panel);
                this.pack();
                this.setLocationByPlatform(true);
-               GUIUtil.installEscapeCloseOperation(this);
-               GUIUtil.setDefaultButton(openButton);
+               
+               GUIUtil.setDisposableDialogOptions(this, openButton);
        }
        
        
index 92144db77766d0e9a66cc44d175ee0c43006e522..b58113da597ae4a89ebc159955ead6ca63d5e4b6 100644 (file)
@@ -72,8 +72,8 @@ public class LicenseDialog extends JDialog {
                this.setTitle("OpenRocket license");
                this.pack();
                this.setLocationRelativeTo(parent);
-               GUIUtil.setDefaultButton(close);
-               GUIUtil.installEscapeCloseOperation(this);
+               
+               GUIUtil.setDisposableDialogOptions(this, close);
        }
        
 }
index 95b2aad6dd25ae22c350a9ed9308b492e291a063..6893dccf596bad062a6c3c0324e738880f892956 100644 (file)
@@ -284,9 +284,8 @@ public class MotorChooserDialog extends JDialog {
                this.pack();
 //             this.setAlwaysOnTop(true);
 
-               GUIUtil.setDefaultButton(okButton);
-               GUIUtil.installEscapeCloseOperation(this);
                this.setLocationByPlatform(true);
+               GUIUtil.setDisposableDialogOptions(this, okButton);
                
                // Table can be scrolled only after pack() has been called
                setSelectionVisible();
diff --git a/src/net/sf/openrocket/gui/dialogs/PreferencesDialog.java b/src/net/sf/openrocket/gui/dialogs/PreferencesDialog.java
deleted file mode 100644 (file)
index c06cbbc..0000000
+++ /dev/null
@@ -1,391 +0,0 @@
-package net.sf.openrocket.gui.dialogs;
-
-import java.awt.Dialog;
-import java.awt.Window;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.WindowAdapter;
-import java.awt.event.WindowEvent;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.swing.AbstractListModel;
-import javax.swing.ComboBoxModel;
-import javax.swing.JButton;
-import javax.swing.JComboBox;
-import javax.swing.JDialog;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JTabbedPane;
-
-import net.miginfocom.swing.MigLayout;
-import net.sf.openrocket.gui.components.ResizeLabel;
-import net.sf.openrocket.unit.Unit;
-import net.sf.openrocket.unit.UnitGroup;
-import net.sf.openrocket.util.GUIUtil;
-import net.sf.openrocket.util.Prefs;
-
-public class PreferencesDialog extends JDialog {
-       
-       private final List<DefaultUnitSelector> unitSelectors = new ArrayList<DefaultUnitSelector>();
-
-       private PreferencesDialog() {
-               super((Window)null, "Preferences", Dialog.ModalityType.APPLICATION_MODAL);
-               
-               JPanel panel = new JPanel(new MigLayout("fill, gap unrel","[grow]","[grow][]"));
-                               
-               JTabbedPane tabbedPane = new JTabbedPane();
-               panel.add(tabbedPane,"grow, wrap");
-               
-
-               tabbedPane.addTab("Units", null, unitsPane(), "Default units");
-               tabbedPane.addTab("Confirmation", null, confirmationPane(), "Confirmation dialog settings");
-               
-               
-               
-               JButton close = new JButton("Close");
-               close.addActionListener(new ActionListener() {
-                       @Override
-                       public void actionPerformed(ActionEvent arg0) {
-                               PreferencesDialog.this.setVisible(false);
-                               PreferencesDialog.this.dispose();
-                       }
-               });
-               panel.add(close,"span, right, tag close");
-               
-               this.setContentPane(panel);
-               pack();
-               setAlwaysOnTop(true);
-               this.setLocationRelativeTo(null);
-               
-               this.addWindowListener(new WindowAdapter() {
-                       @Override
-                       public void windowClosed(WindowEvent e) {
-                               Prefs.storeDefaultUnits();
-                       }
-               });
-
-               GUIUtil.setDefaultButton(close);
-               GUIUtil.installEscapeCloseOperation(this);
-       }
-       
-       
-       private JPanel confirmationPane() {
-               JPanel panel = new JPanel(new MigLayout("fill"));
-               
-               panel.add(new JLabel("Position to insert new body components:"));
-               panel.add(new JComboBox(new PrefChoiseSelector(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY,
-                               "Always ask", "Insert in middle", "Add to end")), "wrap para, sg combos");
-               
-               panel.add(new JLabel("Confirm deletion of simulations:"));
-               panel.add(new JComboBox(new PrefBooleanSelector(Prefs.CONFIRM_DELETE_SIMULATION,
-                               "Delete", "Confirm", true)), "wrap para, sg combos");
-               
-               return panel;
-       }
-       
-       private JPanel unitsPane() {
-               JPanel panel = new JPanel(new MigLayout("", "[][]40lp[][]"));
-               JComboBox combo;
-               
-               panel.add(new JLabel("Select your preferred units:"), "span, wrap paragraph");
-               
-/*
-               public static final UnitGroup UNITS_LENGTH;
-               public static final UnitGroup UNITS_MOTOR_DIMENSIONS;
-               public static final UnitGroup UNITS_DISTANCE;
-               
-               public static final UnitGroup UNITS_VELOCITY;
-               public static final UnitGroup UNITS_ACCELERATION;
-               public static final UnitGroup UNITS_MASS;
-               public static final UnitGroup UNITS_FORCE;
-               public static final UnitGroup UNITS_IMPULSE;
-
-               public static final UnitGroup UNITS_STABILITY;
-               public static final UnitGroup UNITS_FLIGHT_TIME;
-               public static final UnitGroup UNITS_ROLL;
-               
-               public static final UnitGroup UNITS_AREA;
-               public static final UnitGroup UNITS_DENSITY_LINE;
-               public static final UnitGroup UNITS_DENSITY_SURFACE;
-               public static final UnitGroup UNITS_DENSITY_BULK;
-               public static final UnitGroup UNITS_ROUGHNESS;
-               
-               public static final UnitGroup UNITS_TEMPERATURE;
-               public static final UnitGroup UNITS_PRESSURE;
-               public static final UnitGroup UNITS_ANGLE;
-*/
-               
-               panel.add(new JLabel("Rocket dimensions:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_LENGTH));
-               panel.add(combo, "sizegroup boxes");
-               
-               panel.add(new JLabel("Line density:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_LINE));
-               panel.add(combo, "sizegroup boxes, wrap");
-               
-               
-               
-               panel.add(new JLabel("Motor dimensions:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MOTOR_DIMENSIONS));
-               panel.add(combo, "sizegroup boxes");
-               
-               panel.add(new JLabel("Surface density:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_SURFACE));
-               panel.add(combo, "sizegroup boxes, wrap");
-               
-
-               
-               panel.add(new JLabel("Distance:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DISTANCE));
-               panel.add(combo, "sizegroup boxes");
-               
-               panel.add(new JLabel("Bulk density::"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_BULK));
-               panel.add(combo, "sizegroup boxes, wrap");
-               
-
-               
-               panel.add(new JLabel("Velocity:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_VELOCITY));
-               panel.add(combo, "sizegroup boxes");
-
-               panel.add(new JLabel("Surface roughness:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROUGHNESS));
-               panel.add(combo, "sizegroup boxes, wrap");
-               
-               
-               
-               panel.add(new JLabel("Acceleration:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ACCELERATION));
-               panel.add(combo, "sizegroup boxes");
-
-               panel.add(new JLabel("Area:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_AREA));
-               panel.add(combo, "sizegroup boxes, wrap");
-               
-               
-
-               panel.add(new JLabel("Mass:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MASS));
-               panel.add(combo, "sizegroup boxes");
-               
-               panel.add(new JLabel("Angle:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ANGLE));
-               panel.add(combo, "sizegroup boxes, wrap");
-               
-
-               
-               panel.add(new JLabel("Force:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_FORCE));
-               panel.add(combo, "sizegroup boxes");
-               
-               panel.add(new JLabel("Roll rate:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROLL));
-               panel.add(combo, "sizegroup boxes, wrap");
-               
-
-               
-               panel.add(new JLabel("Total impulse:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_IMPULSE));
-               panel.add(combo, "sizegroup boxes");
-               
-               panel.add(new JLabel("Temperature:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_TEMPERATURE));
-               panel.add(combo, "sizegroup boxes, wrap");
-               
-
-               
-               panel.add(new JLabel("Stability:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_STABILITY));
-               panel.add(combo, "sizegroup boxes");
-
-               panel.add(new JLabel("Pressure:"));
-               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_PRESSURE));
-               panel.add(combo, "sizegroup boxes, wrap para");
-               
-               
-               
-               JButton button = new JButton("Default metric");
-               button.addActionListener(new ActionListener() {
-                       @Override
-                       public void actionPerformed(ActionEvent e) {
-                               UnitGroup.setDefaultMetricUnits();
-                               for (DefaultUnitSelector s: unitSelectors)
-                                       s.fireChange();
-                       }
-               });
-               panel.add(button, "spanx, split 2, grow");
-               
-               button = new JButton("Default imperial");
-               button.addActionListener(new ActionListener() {
-                       @Override
-                       public void actionPerformed(ActionEvent e) {
-                               UnitGroup.setDefaultImperialUnits();
-                               for (DefaultUnitSelector s: unitSelectors)
-                                       s.fireChange();
-                       }
-               });
-               panel.add(button, "grow, wrap para");
-               
-               
-               panel.add(new ResizeLabel("The effects will take place the next time you open a window.",-2),
-                               "spanx, wrap");
-               
-
-               return panel;
-       }
-       
-       
-       
-       
-       private class DefaultUnitSelector extends AbstractListModel implements ComboBoxModel {
-               
-               private final UnitGroup group;
-               public DefaultUnitSelector(UnitGroup group) {
-                       this.group = group;
-                       unitSelectors.add(this);
-               }
-               
-               @Override
-               public Object getSelectedItem() {
-                       return group.getDefaultUnit();
-               }
-               @Override
-               public void setSelectedItem(Object item) {
-                       if (!(item instanceof Unit)) {
-                               throw new IllegalArgumentException("Illegal argument "+item);
-                       }
-                       group.setDefaultUnit(group.getUnitIndex((Unit)item));
-               }
-               @Override
-               public Object getElementAt(int index) {
-                       return group.getUnit(index);
-               }
-               @Override
-               public int getSize() {
-                       return group.getUnitCount();
-               }
-               
-               
-               public void fireChange() {
-                       this.fireContentsChanged(this, 0, this.getSize());
-               }
-       }
-       
-
-       
-       private class PrefChoiseSelector extends AbstractListModel implements ComboBoxModel {
-               private final String preference;
-               private final String[] descriptions;
-               
-               public PrefChoiseSelector(String preference, String ... descriptions) {
-                       this.preference = preference;
-                       this.descriptions = descriptions;
-               }
-               
-               @Override
-               public Object getSelectedItem() {
-                       return descriptions[Prefs.getChoise(preference, descriptions.length, 0)];
-               }
-               
-               @Override
-               public void setSelectedItem(Object item) {
-                       if (!(item instanceof String)) {
-                               throw new IllegalArgumentException("Illegal argument "+item);
-                       }
-                       int index;
-                       for (index = 0; index < descriptions.length; index++) {
-                               if (((String)item).equalsIgnoreCase(descriptions[index]))
-                                       break;
-                       }
-                       if (index >= descriptions.length) {
-                               throw new IllegalArgumentException("Illegal argument "+item);
-                       }
-                       
-                       Prefs.putChoise(preference, index);
-               }
-               
-               @Override
-               public Object getElementAt(int index) {
-                       return descriptions[index];
-               }
-               @Override
-               public int getSize() {
-                       return descriptions.length;
-               }
-       }
-       
-
-       private class PrefBooleanSelector extends AbstractListModel implements ComboBoxModel {
-               private final String preference;
-               private final String trueDesc, falseDesc;
-               private final boolean def;
-               
-               public PrefBooleanSelector(String preference, String falseDescription, 
-                               String trueDescription, boolean defaultState) {
-                       this.preference = preference;
-                       this.trueDesc = trueDescription;
-                       this.falseDesc = falseDescription;
-                       this.def = defaultState;
-               }
-               
-               @Override
-               public Object getSelectedItem() {
-                       if (Prefs.NODE.getBoolean(preference, def)) {
-                               return trueDesc;
-                       } else {
-                               return falseDesc;
-                       }
-               }
-               
-               @Override
-               public void setSelectedItem(Object item) {
-                       if (!(item instanceof String)) {
-                               throw new IllegalArgumentException("Illegal argument "+item);
-                       }
-                       
-                       if (trueDesc.equals(item)) {
-                               Prefs.NODE.putBoolean(preference, true);
-                       } else if (falseDesc.equals(item)) {
-                               Prefs.NODE.putBoolean(preference, false);
-                       } else {
-                               throw new IllegalArgumentException("Illegal argument "+item);
-                       }
-               }
-               
-               @Override
-               public Object getElementAt(int index) {
-                       switch (index) {
-                       case 0:
-                               return def ? trueDesc : falseDesc;
-
-                       case 1:
-                               return def ? falseDesc: trueDesc;
-                               
-                       default:
-                               throw new IndexOutOfBoundsException("Boolean asked for index="+index);
-                       }
-               }
-               @Override
-               public int getSize() {
-                       return 2;
-               }
-       }
-       
-       
-       
-       ////////  Singleton implementation  ////////
-       
-       private static PreferencesDialog dialog = null;
-       
-       public static void showPreferences() {
-               if (dialog != null) {
-                       dialog.dispose();
-               }
-               dialog = new PreferencesDialog();
-               dialog.setVisible(true);
-       }
-       
-       
-}
diff --git a/src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java b/src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java
new file mode 100644 (file)
index 0000000..e6d66f4
--- /dev/null
@@ -0,0 +1,371 @@
+package net.sf.openrocket.gui.dialogs.preferences;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Iterator;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.DefaultTableCellRenderer;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.database.Database;
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.gui.adaptors.Column;
+import net.sf.openrocket.gui.adaptors.ColumnTableModel;
+import net.sf.openrocket.gui.dialogs.CustomMaterialDialog;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.unit.Value;
+
+public class MaterialEditPanel extends JPanel {
+
+       private final JTable table;
+       
+       private final JButton addButton;
+       private final JButton editButton;
+       private final JButton deleteButton;
+       private final JButton revertButton;
+       
+       
+       public MaterialEditPanel() {
+               super(new MigLayout("fill"));
+               
+               
+               // TODO: LOW: Create sorter that keeps material types always in order
+               final ColumnTableModel model = new ColumnTableModel(
+                               new Column("Material") {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               return getMaterial(row).getName();
+                                       }
+                               },
+                               
+                               new Column("Type") {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               return getMaterial(row).getType().toString();
+                                       }
+                                       @Override
+                                       public int getDefaultWidth() {
+                                               return 15;
+                                       }
+                               },
+                               
+                               new Column("Density") {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               Material m = getMaterial(row);
+                                               double d = m.getDensity();
+                                               switch (m.getType()) {
+                                               case LINE:
+                                                       return UnitGroup.UNITS_DENSITY_LINE.toValue(d);
+                                                       
+                                               case SURFACE:
+                                                       return UnitGroup.UNITS_DENSITY_SURFACE.toValue(d);
+                                                       
+                                               case BULK:
+                                                       return UnitGroup.UNITS_DENSITY_BULK.toValue(d);
+                                                       
+                                               default:
+                                                       throw new IllegalStateException("Material type " + m.getType());
+                                               }
+                                       }
+                                       @Override
+                                       public int getDefaultWidth() {
+                                               return 15;
+                                       }
+                                       @Override
+                                       public Class<?> getColumnClass() {
+                                               return Value.class;
+                                       }
+                               }
+               ) {
+                       @Override
+                       public int getRowCount() {
+                               return Databases.BULK_MATERIAL.size() + Databases.SURFACE_MATERIAL.size() +
+                                       Databases.LINE_MATERIAL.size();
+                       }
+               };
+               
+               table = new JTable(model);
+               model.setColumnWidths(table.getColumnModel());
+               table.setAutoCreateRowSorter(true);
+               table.setDefaultRenderer(Object.class, new MaterialCellRenderer());
+               this.add(new JScrollPane(table), "w 200px, h 100px, grow 100");
+               
+               
+               
+               addButton = new JButton("New");
+               addButton.setToolTipText("Add a new material");
+               addButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               CustomMaterialDialog dialog = new CustomMaterialDialog(
+                                                       SwingUtilities.getWindowAncestor(MaterialEditPanel.this),
+                                                       null, false, "Add a custom material");
+                               dialog.setVisible(true);
+                               if (dialog.getOkClicked()) {
+                                       Material mat = dialog.getMaterial();
+                                       getDatabase(mat).add(mat);
+                                       model.fireTableDataChanged();
+                                       setButtonStates();
+                               }
+                       }
+               });
+               this.add(addButton, "gap rel rel para para, w 70lp, split 5, flowy, growx 1, top");
+               
+               
+               editButton = new JButton("Edit");
+               editButton.setToolTipText("Edit an existing material");
+               editButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               int sel = table.getSelectedRow();
+                               if (sel < 0)
+                                       return;
+                               sel = table.convertRowIndexToModel(sel);
+                               Material m = getMaterial(sel);
+                               
+                               CustomMaterialDialog dialog;
+                               if (m.isUserDefined()) {
+                                       dialog = new CustomMaterialDialog(
+                                                       SwingUtilities.getWindowAncestor(MaterialEditPanel.this),
+                                                       m, false, "Edit material");
+                               } else {
+                                       dialog = new CustomMaterialDialog(
+                                                       SwingUtilities.getWindowAncestor(MaterialEditPanel.this),
+                                                       m, false, "Add a custom material", 
+                                                       "The built-in materials cannot be modified.");
+                               }
+                               
+                               dialog.setVisible(true);
+                               
+                               if (dialog.getOkClicked()) {
+                                       if (m.isUserDefined()) {
+                                               getDatabase(m).remove(m);
+                                       }
+                                       Material mat = dialog.getMaterial();
+                                       getDatabase(mat).add(mat);
+                                       model.fireTableDataChanged();
+                                       setButtonStates();
+                               }
+                       }
+               });
+               this.add(editButton, "gap rel rel para para, growx 1, top");
+               
+               
+               deleteButton = new JButton("Delete");
+               deleteButton.setToolTipText("Delete a user-defined material");
+               deleteButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               int sel = table.getSelectedRow();
+                               if (sel < 0)
+                                       return;
+                               sel = table.convertRowIndexToModel(sel);
+                               Material m = getMaterial(sel);
+                               if (!m.isUserDefined())
+                                       return;
+                               getDatabase(m).remove(m);
+                               model.fireTableDataChanged();
+                               setButtonStates();
+                       }
+               });
+               this.add(deleteButton, "gap rel rel para para, growx 1, top");
+               
+               
+               this.add(new JPanel(), "grow 1");
+               
+               revertButton = new JButton("Revert all");
+               revertButton.setToolTipText("Delete all user-defined materials");
+               revertButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               int sel = JOptionPane.showConfirmDialog(MaterialEditPanel.this, 
+                                               "Delete all user-defined materials?", "Revert all?", 
+                                               JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+                               if (sel == JOptionPane.YES_OPTION) {
+                                       Iterator<Material> iterator;
+
+                                       iterator = Databases.LINE_MATERIAL.iterator();
+                                       while (iterator.hasNext()) {
+                                               if (iterator.next().isUserDefined())
+                                                       iterator.remove();
+                                       }
+
+                                       iterator = Databases.SURFACE_MATERIAL.iterator();
+                                       while (iterator.hasNext()) {
+                                               if (iterator.next().isUserDefined())
+                                                       iterator.remove();
+                                       }
+
+                                       iterator = Databases.BULK_MATERIAL.iterator();
+                                       while (iterator.hasNext()) {
+                                               if (iterator.next().isUserDefined())
+                                                       iterator.remove();
+                                       }
+                                       model.fireTableDataChanged();
+                                       setButtonStates();
+                               }
+                       }
+               });
+               this.add(revertButton, "gap rel rel para para, growx 1, bottom, wrap");
+               
+               setButtonStates();
+               table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+                       @Override
+                       public void valueChanged(ListSelectionEvent e) {
+                               setButtonStates();
+                       }
+               });
+               table.addMouseListener(new MouseAdapter() {
+                       @Override
+                       public void mouseClicked(MouseEvent e) {
+                               if (e.getClickCount() == 2) {
+                                       editButton.doClick();
+                               }
+                       }
+               });
+               
+               
+               this.add(new JLabel("<html><i>Editing materials will not affect existing " +
+                               "rocket designs.</i>"), "span");
+               
+               
+       }
+       
+       
+       private Database<Material> getDatabase(Material m) {
+               switch (m.getType()) {
+               case BULK:
+                       return Databases.BULK_MATERIAL;
+                       
+               case SURFACE:
+                       return Databases.SURFACE_MATERIAL;
+                       
+               case LINE:
+                       return Databases.LINE_MATERIAL;
+
+               default:
+                       throw new IllegalArgumentException("Material type invalid, m="+m);
+               }
+       }
+       
+       
+       private void setButtonStates() {
+               int sel = table.getSelectedRow();
+               
+               // Add button always enabled
+               addButton.setEnabled(true);
+               
+               // Edit button enabled if a material is selected
+               editButton.setEnabled(sel >= 0);
+               
+               // Delete button enabled if a user-defined material is selected
+               if (sel >= 0) {
+                       int modelRow = table.convertRowIndexToModel(sel);
+                       deleteButton.setEnabled(getMaterial(modelRow).isUserDefined());
+               } else {
+                       deleteButton.setEnabled(false);
+               }
+
+               // Revert button enabled if any user-defined material exists
+               boolean found = false;
+               
+               for (Material m: Databases.BULK_MATERIAL) {
+                       if (m.isUserDefined()) {
+                               found = true;
+                               break;
+                       }
+               }
+               if (!found) {
+                       for (Material m: Databases.SURFACE_MATERIAL) {
+                               if (m.isUserDefined()) {
+                                       found = true;
+                                       break;
+                               }
+                       }
+               }
+               if (!found) {
+                       for (Material m: Databases.LINE_MATERIAL) {
+                               if (m.isUserDefined()) {
+                                       found = true;
+                                       break;
+                               }
+                       }
+               }
+               revertButton.setEnabled(found);
+               
+       }
+       
+       private Material getMaterial(int origRow) {
+               int row = origRow;
+               int n;
+               
+               n = Databases.BULK_MATERIAL.size();
+               if (row < n) {
+                       return Databases.BULK_MATERIAL.get(row);
+               }
+               row -= n;
+               
+               n = Databases.SURFACE_MATERIAL.size();
+               if (row < n) {
+                       return Databases.SURFACE_MATERIAL.get(row);
+               }
+               row -= n;
+               
+               n = Databases.LINE_MATERIAL.size();
+               if (row < n) {
+                       return Databases.LINE_MATERIAL.get(row);
+               }
+               throw new IndexOutOfBoundsException("row="+origRow+" while material count" +
+                               " bulk:" + Databases.BULK_MATERIAL.size() + 
+                               " surface:" + Databases.SURFACE_MATERIAL.size() +
+                               " line:" + Databases.LINE_MATERIAL.size());
+       }
+       
+       
+       private class MaterialCellRenderer extends DefaultTableCellRenderer {
+
+               /* (non-Javadoc)
+                * @see javax.swing.table.DefaultTableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
+                */
+               @Override
+               public Component getTableCellRendererComponent(JTable table, Object value,
+                               boolean isSelected, boolean hasFocus, int row, int column) {
+                       Component c = super.getTableCellRendererComponent(table, value, isSelected, 
+                                       hasFocus, row, column);
+                       if (c instanceof JLabel) {
+                               JLabel label = (JLabel)c;
+                               Material m = getMaterial(row);
+                               
+                               if (isSelected) {
+                                       if (m.isUserDefined())
+                                               label.setForeground(table.getSelectionForeground());
+                                       else
+                                               label.setForeground(Color.GRAY);
+                               } else {
+                                       if (m.isUserDefined())
+                                               label.setForeground(table.getForeground());
+                                       else
+                                               label.setForeground(Color.GRAY);
+                               }
+                       }
+                       return c;
+               }
+               
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java b/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java
new file mode 100644 (file)
index 0000000..2ef8e11
--- /dev/null
@@ -0,0 +1,367 @@
+package net.sf.openrocket.gui.dialogs.preferences;
+
+import java.awt.Dialog;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.components.ResizeLabel;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class PreferencesDialog extends JDialog {
+       
+       private final List<DefaultUnitSelector> unitSelectors = new ArrayList<DefaultUnitSelector>();
+
+       private PreferencesDialog() {
+               super((Window)null, "Preferences", Dialog.ModalityType.APPLICATION_MODAL);
+               
+               JPanel panel = new JPanel(new MigLayout("fill, gap unrel","[grow]","[grow][]"));
+                               
+               JTabbedPane tabbedPane = new JTabbedPane();
+               panel.add(tabbedPane,"grow, wrap");
+               
+
+               tabbedPane.addTab("Units", null, unitsPane(), "Default units");
+               tabbedPane.addTab("Materials", null, new MaterialEditPanel(), "Custom materials");
+               tabbedPane.addTab("Confirmation", null, confirmationPane(), "Confirmation dialog settings");
+               
+               
+               JButton close = new JButton("Close");
+               close.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent arg0) {
+                               PreferencesDialog.this.setVisible(false);
+                               PreferencesDialog.this.dispose();
+                       }
+               });
+               panel.add(close,"span, right, tag close");
+               
+               this.setContentPane(panel);
+               pack();
+               this.setLocationRelativeTo(null);
+               
+               this.addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosed(WindowEvent e) {
+                               Prefs.storeDefaultUnits();
+                       }
+               });
+
+               GUIUtil.setDisposableDialogOptions(this, close);
+       }
+       
+       
+       private JPanel confirmationPane() {
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               panel.add(new JLabel("Position to insert new body components:"));
+               panel.add(new JComboBox(new PrefChoiseSelector(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY,
+                               "Always ask", "Insert in middle", "Add to end")), "wrap para, sg combos");
+               
+               panel.add(new JLabel("Confirm deletion of simulations:"));
+               panel.add(new JComboBox(new PrefBooleanSelector(Prefs.CONFIRM_DELETE_SIMULATION,
+                               "Delete", "Confirm", true)), "wrap para, sg combos");
+               
+               return panel;
+       }
+       
+       
+       
+       private JPanel unitsPane() {
+               JPanel panel = new JPanel(new MigLayout("", "[][]40lp[][]"));
+               JComboBox combo;
+               
+               panel.add(new JLabel("Select your preferred units:"), "span, wrap paragraph");
+               
+               
+               panel.add(new JLabel("Rocket dimensions:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_LENGTH));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Line density:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_LINE));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+               
+               
+               panel.add(new JLabel("Motor dimensions:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MOTOR_DIMENSIONS));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Surface density:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_SURFACE));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+
+               
+               panel.add(new JLabel("Distance:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DISTANCE));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Bulk density::"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_BULK));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+
+               
+               panel.add(new JLabel("Velocity:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_VELOCITY));
+               panel.add(combo, "sizegroup boxes");
+
+               panel.add(new JLabel("Surface roughness:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROUGHNESS));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+               
+               
+               panel.add(new JLabel("Acceleration:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ACCELERATION));
+               panel.add(combo, "sizegroup boxes");
+
+               panel.add(new JLabel("Area:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_AREA));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+               
+
+               panel.add(new JLabel("Mass:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MASS));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Angle:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ANGLE));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+
+               
+               panel.add(new JLabel("Force:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_FORCE));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Roll rate:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROLL));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+
+               
+               panel.add(new JLabel("Total impulse:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_IMPULSE));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Temperature:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_TEMPERATURE));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+
+               
+               panel.add(new JLabel("Stability:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_STABILITY));
+               panel.add(combo, "sizegroup boxes");
+
+               panel.add(new JLabel("Pressure:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_PRESSURE));
+               panel.add(combo, "sizegroup boxes, wrap para");
+               
+               
+               
+               JButton button = new JButton("Default metric");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               UnitGroup.setDefaultMetricUnits();
+                               for (DefaultUnitSelector s: unitSelectors)
+                                       s.fireChange();
+                       }
+               });
+               panel.add(button, "spanx, split 2, grow");
+               
+               button = new JButton("Default imperial");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               UnitGroup.setDefaultImperialUnits();
+                               for (DefaultUnitSelector s: unitSelectors)
+                                       s.fireChange();
+                       }
+               });
+               panel.add(button, "grow, wrap para");
+               
+               
+               panel.add(new ResizeLabel("The effects will take place the next time you open a window.",-2),
+                               "spanx, wrap");
+               
+
+               return panel;
+       }
+       
+       
+       
+       
+       
+       private class DefaultUnitSelector extends AbstractListModel implements ComboBoxModel {
+               
+               private final UnitGroup group;
+               public DefaultUnitSelector(UnitGroup group) {
+                       this.group = group;
+                       unitSelectors.add(this);
+               }
+               
+               @Override
+               public Object getSelectedItem() {
+                       return group.getDefaultUnit();
+               }
+               @Override
+               public void setSelectedItem(Object item) {
+                       if (!(item instanceof Unit)) {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+                       group.setDefaultUnit(group.getUnitIndex((Unit)item));
+               }
+               @Override
+               public Object getElementAt(int index) {
+                       return group.getUnit(index);
+               }
+               @Override
+               public int getSize() {
+                       return group.getUnitCount();
+               }
+               
+               
+               public void fireChange() {
+                       this.fireContentsChanged(this, 0, this.getSize());
+               }
+       }
+       
+
+       
+       private class PrefChoiseSelector extends AbstractListModel implements ComboBoxModel {
+               private final String preference;
+               private final String[] descriptions;
+               
+               public PrefChoiseSelector(String preference, String ... descriptions) {
+                       this.preference = preference;
+                       this.descriptions = descriptions;
+               }
+               
+               @Override
+               public Object getSelectedItem() {
+                       return descriptions[Prefs.getChoise(preference, descriptions.length, 0)];
+               }
+               
+               @Override
+               public void setSelectedItem(Object item) {
+                       if (!(item instanceof String)) {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+                       int index;
+                       for (index = 0; index < descriptions.length; index++) {
+                               if (((String)item).equalsIgnoreCase(descriptions[index]))
+                                       break;
+                       }
+                       if (index >= descriptions.length) {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+                       
+                       Prefs.putChoise(preference, index);
+               }
+               
+               @Override
+               public Object getElementAt(int index) {
+                       return descriptions[index];
+               }
+               @Override
+               public int getSize() {
+                       return descriptions.length;
+               }
+       }
+       
+
+       private class PrefBooleanSelector extends AbstractListModel implements ComboBoxModel {
+               private final String preference;
+               private final String trueDesc, falseDesc;
+               private final boolean def;
+               
+               public PrefBooleanSelector(String preference, String falseDescription, 
+                               String trueDescription, boolean defaultState) {
+                       this.preference = preference;
+                       this.trueDesc = trueDescription;
+                       this.falseDesc = falseDescription;
+                       this.def = defaultState;
+               }
+               
+               @Override
+               public Object getSelectedItem() {
+                       if (Prefs.NODE.getBoolean(preference, def)) {
+                               return trueDesc;
+                       } else {
+                               return falseDesc;
+                       }
+               }
+               
+               @Override
+               public void setSelectedItem(Object item) {
+                       if (!(item instanceof String)) {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+                       
+                       if (trueDesc.equals(item)) {
+                               Prefs.NODE.putBoolean(preference, true);
+                       } else if (falseDesc.equals(item)) {
+                               Prefs.NODE.putBoolean(preference, false);
+                       } else {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+               }
+               
+               @Override
+               public Object getElementAt(int index) {
+                       switch (index) {
+                       case 0:
+                               return def ? trueDesc : falseDesc;
+
+                       case 1:
+                               return def ? falseDesc: trueDesc;
+                               
+                       default:
+                               throw new IndexOutOfBoundsException("Boolean asked for index="+index);
+                       }
+               }
+               @Override
+               public int getSize() {
+                       return 2;
+               }
+       }
+       
+       
+       
+       ////////  Singleton implementation  ////////
+       
+       private static PreferencesDialog dialog = null;
+       
+       public static void showPreferences() {
+               if (dialog != null) {
+                       dialog.dispose();
+               }
+               dialog = new PreferencesDialog();
+               dialog.setVisible(true);
+       }
+       
+       
+}
index 5357b3290add757453d6fc4dd93ad400c6d08eb7..8ad7601ad7692f52af35d24095e1e31413e0564a 100644 (file)
@@ -42,6 +42,7 @@ import javax.swing.JScrollPane;
 import javax.swing.JSeparator;
 import javax.swing.JSplitPane;
 import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
 import javax.swing.KeyStroke;
 import javax.swing.ListSelectionModel;
 import javax.swing.LookAndFeel;
@@ -72,9 +73,9 @@ import net.sf.openrocket.gui.dialogs.BugReportDialog;
 import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog;
 import net.sf.openrocket.gui.dialogs.ExampleDesignDialog;
 import net.sf.openrocket.gui.dialogs.LicenseDialog;
-import net.sf.openrocket.gui.dialogs.PreferencesDialog;
 import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
 import net.sf.openrocket.gui.dialogs.WarningDialog;
+import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog;
 import net.sf.openrocket.gui.scalefigure.RocketPanel;
 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
@@ -86,6 +87,7 @@ import net.sf.openrocket.util.Icons;
 import net.sf.openrocket.util.OpenFileWorker;
 import net.sf.openrocket.util.Prefs;
 import net.sf.openrocket.util.SaveFileWorker;
+import net.sf.openrocket.util.TestRockets;
 
 public class BasicFrame extends JFrame {
        private static final long serialVersionUID = 1L;
@@ -295,7 +297,7 @@ public class BasicFrame extends JFrame {
                                int selRow = tree.getRowForLocation(e.getX(), e.getY());
                                TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
                                if(selRow != -1) {
-                                       if(e.getClickCount() == 2) {
+                                       if((e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) {
                                                // Double-click
                                                RocketComponent c = (RocketComponent)selPath.getLastPathComponent();
                                                ComponentConfigDialog.showDialog(BasicFrame.this, 
@@ -631,6 +633,38 @@ public class BasicFrame extends JFrame {
                
                menu.addSeparator();
                
+               item = new JMenuItem("Create test rocket");
+               item.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               JTextField field = new JTextField();
+                               int sel = JOptionPane.showOptionDialog(BasicFrame.this, new Object[] {
+                                               "Input text key to generate random rocket:",
+                                               field
+                                       }, "Generate random test rocket", JOptionPane.DEFAULT_OPTION, 
+                                       JOptionPane.QUESTION_MESSAGE, null, new Object[] {
+                                               "Random", "OK"
+                               }, "OK");
+                               
+                               Rocket r;
+                               if (sel == 0) {
+                                       r = new TestRockets(null).makeTestRocket();
+                               } else if (sel == 1) {
+                                       r = new TestRockets(field.getText()).makeTestRocket();
+                               } else {
+                                       return;
+                               }
+                               
+                               OpenRocketDocument doc = new OpenRocketDocument(r);
+                               doc.setSaved(true);
+                               BasicFrame frame = new BasicFrame(doc);
+                               frame.setVisible(true);
+                       }
+               });
+               menu.add(item);
+               
+               menu.addSeparator();
+               
                item = new JMenuItem("Exception here");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
index 0b7454e78724117b61c0adf2467dd14464a95dd6..f140225669beb7dd687faeb435d61cacecf8e91f 100644 (file)
@@ -380,8 +380,7 @@ public class ComponentAddButtons extends JPanel implements Scrollable {
                        
                        if (c == null) {
                                // Should not occur
-                               System.err.println("ERROR:  Could not place new component.");
-                               Thread.dumpStack();
+                               ExceptionHandler.handleErrorCondition("ERROR:  Could not place new component.");
                                updateEnabled();
                                return;
                        }
@@ -492,8 +491,7 @@ public class ComponentAddButtons extends JPanel implements Scrollable {
                                // Insert at the end of the parent
                                return new Pair<RocketComponent,Integer>(parent, null);
                        default:
-                               System.err.println("ERROR:  Bad position type: "+pos);
-                               Thread.dumpStack();
+                               ExceptionHandler.handleErrorCondition("ERROR:  Bad position type: "+pos);
                                return null;
                        }
                }
@@ -533,8 +531,7 @@ public class ComponentAddButtons extends JPanel implements Scrollable {
                                sel = 2;
                                break;
                        default:
-                               System.err.println("ERROR:  JOptionPane returned "+sel);
-                               Thread.dumpStack();
+                               ExceptionHandler.handleErrorCondition("ERROR:  JOptionPane returned "+sel);
                                return 0;
                        }
                        
index 0c140b26bad9a01234675f00b06bbc9cc9228e19..d1e47a30f1db929223d5df3afd89b7b9823d2b44 100644 (file)
@@ -86,7 +86,7 @@ public class ComponentIcons {
        private static ImageIcon loadSmall(String file, String desc) {
                URL url = ClassLoader.getSystemResource(file);
                if (url==null) {
-               System.err.println("ERROR:  Couldn't find file: " + file);
+               ExceptionHandler.handleErrorCondition("ERROR:  Couldn't find file: " + file);
                        return null;
                }
                return new ImageIcon(url, desc);
@@ -103,8 +103,7 @@ public class ComponentIcons {
                                bi = ImageIO.read(url);
                                bi2 = ImageIO.read(url);   //  How the fsck can one duplicate a BufferedImage???
                        } catch (IOException e) {
-                               System.err.println("ERROR:  Couldn't read file: "+file);
-                               e.printStackTrace();
+                               ExceptionHandler.handleErrorCondition("ERROR:  Couldn't read file: "+file, e);
                        return new ImageIcon[]{null,null};
                        }
                        
@@ -138,7 +137,7 @@ public class ComponentIcons {
                
                return icons;
            } else {
-               System.err.println("ERROR:  Couldn't find file: " + file);
+               ExceptionHandler.handleErrorCondition("ERROR:  Couldn't find file: " + file);
                return new ImageIcon[]{null,null};
            }
        }
index f4428bfb032bd9cb3ff8639eb33ee422bd498ca0..7e7ba155ee50e754a17fb7c71e21ad4cf7836922 100644 (file)
@@ -90,6 +90,13 @@ public class ComponentTreeModel implements TreeModel, ComponentChangeListener {
                listeners.remove(l);
        }
        
+       private void fireTreeNodeChanged(RocketComponent node) {
+               TreeModelEvent e = new TreeModelEvent(this, makeTreePath(node), null, null);
+               Object[] l = listeners.toArray();
+               for (int i=0; i<l.length; i++)
+                       ((TreeModelListener)l[i]).treeNodesChanged(e);
+       }
+       
        private void fireTreeNodesChanged() {
                Object[] path = { root };
                TreeModelEvent e = new TreeModelEvent(this,path);
@@ -167,7 +174,7 @@ public class ComponentTreeModel implements TreeModel, ComponentChangeListener {
                                expandAll();
                        }
                } else if (e.isOtherChange()) {
-                       fireTreeNodesChanged();
+                       fireTreeNodeChanged(e.getSource());
                }
        }
 
index 93474b1c6b17262e96b0ac43fefcf9203570ed06..7b233b022b764c03d0771145411b169f76d781ab 100644 (file)
@@ -32,11 +32,12 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                        handling = false;
                }
 
+               e.printStackTrace();
+               
                try {
                        
                        if (handling) {
-                               System.err.println("Exception is currently being handled, " +
-                                               "dumping exception instead:");
+                               System.err.println("Exception is currently being handled, ignoring:");
                                e.printStackTrace();
                                return;
                        }
@@ -70,6 +71,63 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
        }
 
        
+       /**
+        * Handle an error condition programmatically without throwing an exception.
+        * This can be used in cases where recovery of the error is desirable.
+        * 
+        * @param message       the error message.
+        */
+       public static void handleErrorCondition(String message) {
+               handleErrorCondition(new InternalException(message));
+       }
+       
+
+       /**
+        * Handle an error condition programmatically without throwing an exception.
+        * This can be used in cases where recovery of the error is desirable.
+        * 
+        * @param message       the error message.
+        * @param exception     the exception that occurred.
+        */
+       public static void handleErrorCondition(String message, Exception exception) {
+               handleErrorCondition(new InternalException(message, exception));
+       }
+       
+       
+       /**
+        * Handle an error condition programmatically without throwing an exception.
+        * This can be used in cases where recovery of the error is desirable.
+        * 
+        * @param exception             the exception that occurred.
+        */
+       public static void handleErrorCondition(final Exception exception) {
+               final ExceptionHandler handler;
+
+               try {
+
+                       if (instance == null) {
+                               handler = new ExceptionHandler();
+                       } else {
+                               handler = instance;
+                       }
+
+                       final Thread thread = Thread.currentThread();
+
+                       if (SwingUtilities.isEventDispatchThread()) {
+                               handler.showDialog(thread, exception);
+                       } else {
+                               SwingUtilities.invokeAndWait(new Runnable() {
+                                       public void run() {
+                                               handler.showDialog(thread, exception);
+                                       }
+                               });
+                       }
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+       
+       
        /**
         * The actual handling routine.
         * 
@@ -103,10 +161,17 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                
                
                // Normal exception, show question dialog
+               String msg = e.getClass().getSimpleName() + ": " + e.getMessage();
+               if (msg.length() > 90) {
+                       msg = msg.substring(0, 90) + "...";
+               }
+               
                
                int selection = JOptionPane.showOptionDialog(null, new Object[] {
                                "OpenRocket encountered an uncaught exception.  This typically signifies " +
-                               "a bug in the software.", " ",
+                               "a bug in the software.", 
+                               "<html><em>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" + msg + "</em>",
+                               " ",
                                "Please take a moment to report this bug to the developers.",
                                "This can be done automatically if you have an Internet connection."
                                }, "Uncaught exception", JOptionPane.DEFAULT_OPTION, 
@@ -158,9 +223,45 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
         */
        public static class AwtHandler {
                public void handle(Throwable t) {
+                       
+                       /*
+                        * Detect and ignore bug 6828938 in Sun JRE 1.6.0_14 - 1.6.0_16.
+                        */
+                       if (t instanceof ArrayIndexOutOfBoundsException) {
+                               final String buggyClass = "sun.font.FontDesignMetrics";
+                               StackTraceElement[] elements = t.getStackTrace();
+                               if (elements.length >= 3 &&
+                                               (buggyClass.equals(elements[0].getClassName()) ||
+                                                buggyClass.equals(elements[1].getClassName()) ||
+                                                buggyClass.equals(elements[2].getClassName()))) {
+                                       System.err.println("Ignoring Sun JRE bug 6828938:  " + t);
+                                       return;
+                               }
+                       }
+                       
+                       
                        if (instance != null) {
                                instance.uncaughtException(Thread.currentThread(), t);
                        }
                }
        }
+       
+       
+       private static class InternalException extends Exception {
+               public InternalException() {
+                       super();
+               }
+
+               public InternalException(String message, Throwable cause) {
+                       super(message, cause);
+               }
+
+               public InternalException(String message) {
+                       super(message);
+               }
+
+               public InternalException(Throwable cause) {
+                       super(cause);
+               }
+       }
 }
index 4d92c1fb8fd6207be2a142f4a1be551f5ccfd4c5..81234d3eaf5d03482b0de717ff5562c352aa1166 100644 (file)
@@ -174,8 +174,8 @@ public class SimulationEditDialog extends JDialog {
                this.validate();
                this.pack();
                this.setLocationByPlatform(true);
-               GUIUtil.setDefaultButton(button);
-               GUIUtil.installEscapeCloseOperation(this);
+               
+               GUIUtil.setDisposableDialogOptions(this, button);
        }
        
        
@@ -931,8 +931,8 @@ public class SimulationEditDialog extends JDialog {
 
                dialog.setLocationByPlatform(true);
                dialog.pack();
-               GUIUtil.installEscapeCloseOperation(dialog);
-               GUIUtil.setDefaultButton(button);
+               
+               GUIUtil.setDisposableDialogOptions(dialog, button);
 
                dialog.setVisible(true);
        }
index e16486b7a8fe7d13cb6aff38dc791063be94b2cd..238ca3225e9ebc5ec5e21414c2a54ab563baa9ec 100644 (file)
@@ -142,7 +142,8 @@ public class SimulationRunDialog extends JDialog {
                this.setLocationByPlatform(true);
                this.validate();
                this.pack();
-               GUIUtil.installEscapeCloseOperation(this);
+               
+               GUIUtil.setDisposableDialogOptions(this, null);
 
                updateProgress();
        }
index a40c841a21f01c0dcf483282e2c6696142c92acc..d84fed9b55ebebf385bc0c0cc496abf88b32d5d9 100644 (file)
@@ -397,7 +397,7 @@ public class PlotDialog extends JDialog {
                
                panel.add(chartPanel, "grow, wrap 20lp");
                
-               final JCheckBox check = new JCheckBox("Show points");
+               final JCheckBox check = new JCheckBox("Show data points");
                check.setSelected(initialShowPoints);
                check.addActionListener(new ActionListener() {
                        @Override
@@ -424,8 +424,8 @@ public class PlotDialog extends JDialog {
 
                this.setLocationByPlatform(true);
                this.pack();
-               GUIUtil.installEscapeCloseOperation(this);
-               GUIUtil.setDefaultButton(button);
+               
+               GUIUtil.setDisposableDialogOptions(this, button);
        }
        
        
index 3d874a205acbbc1fdb648ad2dd7d44352a132eaa..0eb856f55f95b5fd312b9a52585c54c0bf496e32 100644 (file)
@@ -22,16 +22,16 @@ public class FinSetShapes extends RocketComponentShapes {
                Transformation baseRotation = finset.getBaseRotationTransformation();
                Transformation finRotation = finset.getFinRotationTransformation();
                
-               Coordinate c[] = finset.getFinPoints();
-
+               Coordinate finPoints[] = finset.getFinPointsWithTab();
+               
                
                // TODO: MEDIUM: sloping radius
                double radius = finset.getBodyRadius();
                
                // Translate & rotate the coordinates
-               for (int i=0; i<c.length; i++) {
-                       c[i] = cantRotation.transform(c[i]);
-                       c[i] = baseRotation.transform(c[i].add(0,radius,0));
+               for (int i=0; i<finPoints.length; i++) {
+                       finPoints[i] = cantRotation.transform(finPoints[i]);
+                       finPoints[i] = baseRotation.transform(finPoints[i].add(0,radius,0));
                }
                
                
@@ -43,19 +43,20 @@ public class FinSetShapes extends RocketComponentShapes {
 
                        // Make polygon
                        p = new Path2D.Float();
-                       for (int i=0; i<c.length; i++) {
-                               a = transformation.transform(finset.toAbsolute(c[i])[0]);
+                       for (int i=0; i<finPoints.length; i++) {
+                               a = transformation.transform(finset.toAbsolute(finPoints[i])[0]);
                                if (i==0)
                                        p.moveTo(a.x*S, a.y*S);
                                else
                                        p.lineTo(a.x*S, a.y*S);                 
                        }
+                       
                        p.closePath();
                        s[fin] = p;
 
                        // Rotate fin coordinates
-                       for (int i=0; i<c.length; i++)
-                               c[i] = finRotation.transform(c[i]);
+                       for (int i=0; i<finPoints.length; i++)
+                               finPoints[i] = finRotation.transform(finPoints[i]);
                }
                
                return s;
index 8311d0ae12f9ff5f8bc3026e7a243f6ba18f81f7..3ed7741d183fcb9d20a5ff3f9b0d215a31b430eb 100644 (file)
@@ -3,6 +3,7 @@ package net.sf.openrocket.gui.rocketfigure;
 
 import java.awt.Shape;
 
+import net.sf.openrocket.gui.main.ExceptionHandler;
 import net.sf.openrocket.gui.scalefigure.RocketFigure;
 import net.sf.openrocket.util.Transformation;
 
@@ -17,14 +18,16 @@ public class RocketComponentShapes {
        public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
                        Transformation t) {
                // no-op
-               System.err.println("ERROR:  RocketComponent.getShapesSide called with "+component);
+               ExceptionHandler.handleErrorCondition("ERROR:  RocketComponent.getShapesSide called with "
+                               + component);
                return new Shape[0];
        }
        
        public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component,
                        Transformation t) {
                // no-op
-               System.err.println("ERROR:  RocketComponent.getShapesBack called with "+component);
+               ExceptionHandler.handleErrorCondition("ERROR:  RocketComponent.getShapesBack called with "
+                               +component);
                return new Shape[0];
        }
        
index 35158def5fa5b7604cf67aa23ef9c9b611c5c2ee..776728dcfed53d2266ca08b433e25c7fe7582a92 100644 (file)
@@ -20,6 +20,7 @@ import java.util.Iterator;
 import java.util.LinkedHashSet;
 
 import net.sf.openrocket.gui.figureelements.FigureElement;
+import net.sf.openrocket.gui.main.ExceptionHandler;
 import net.sf.openrocket.motor.Motor;
 import net.sf.openrocket.rocketcomponent.Configuration;
 import net.sf.openrocket.rocketcomponent.MotorMount;
@@ -441,7 +442,8 @@ public class RocketFigure extends AbstractScaleFigure {
                }
                
                if (m == null) {
-                       System.err.println("ERROR: Rocket figure paint method not found for " + component);
+                       ExceptionHandler.handleErrorCondition("ERROR: Rocket figure paint method not found for " 
+                                       + component);
                        return new Shape[0];
                }
 
index 93db63aa0d106e836f7d37175e2ccd64fc0a8a00..8febc9480432cefa785ebc2c1b5a262ef51f9e7f 100644 (file)
@@ -17,21 +17,33 @@ import net.sf.openrocket.util.MathUtil;
 public abstract class Material implements Comparable<Material> {
 
        public enum Type {
-               LINE,
-               SURFACE,
-               BULK
+               LINE("Line", UnitGroup.UNITS_DENSITY_LINE),
+               SURFACE("Surface", UnitGroup.UNITS_DENSITY_SURFACE),
+               BULK("Bulk", UnitGroup.UNITS_DENSITY_BULK);
+               
+               private final String name;
+               private final UnitGroup units;
+               private Type(String name, UnitGroup units) {
+                       this.name = name;
+                       this.units = units;
+               }
+               public UnitGroup getUnitGroup() {
+                       return units;
+               }
+               @Override
+               public String toString() {
+                       return name;
+               }
        }
        
+       
+       /////  Definitions of different material types  /////
+       
        public static class Line extends Material {
                public Line(String name, double density, boolean userDefined) {
                        super(name, density, userDefined);
                }
 
-               @Override
-               public UnitGroup getUnitGroup() {
-                       return UnitGroup.UNITS_DENSITY_LINE;
-               }
-
                @Override
                public Type getType() {
                        return Type.LINE;
@@ -44,11 +56,6 @@ public abstract class Material implements Comparable<Material> {
                        super(name, density, userDefined);
                }
                
-               @Override
-               public UnitGroup getUnitGroup() {
-                       return UnitGroup.UNITS_DENSITY_SURFACE;
-               }
-
                @Override
                public Type getType() {
                        return Type.SURFACE;
@@ -65,11 +72,6 @@ public abstract class Material implements Comparable<Material> {
                        super(name, density, userDefined);
                }
 
-               @Override
-               public UnitGroup getUnitGroup() {
-                       return UnitGroup.UNITS_DENSITY_BULK;
-               }
-
                @Override
                public Type getType() {
                        return Type.BULK;
@@ -107,12 +109,11 @@ public abstract class Material implements Comparable<Material> {
                return userDefined;
        }
        
-       public abstract UnitGroup getUnitGroup();
        public abstract Type getType();
        
        @Override
        public String toString() {
-               return getName(getUnitGroup().getDefaultUnit());
+               return this.getName(this.getType().getUnitGroup().getDefaultUnit());
        }
        
 
index 5e7fea0763dbc18ae33432174f87a315d61da7d1..58d3297b5cee3b1be392bf110e62b93d5fe8f259 100644 (file)
@@ -39,6 +39,15 @@ public class ComponentChangeEvent extends ChangeEvent {
        }
        
        
+       /**
+        * Return the source component of this event as specified in the constructor.
+        */
+       @Override
+       public RocketComponent getSource() {
+               return (RocketComponent) super.getSource();
+       }
+
+
        public boolean isAerodynamicChange() {
                return (type & AERODYNAMIC_CHANGE) != 0;
        }
index 9f96bbdc9d740282e2f2834a8954af3b31e7be69..8e11c9140eefa1bbee588d3b4f97c663d7c247f8 100644 (file)
@@ -114,5 +114,16 @@ public abstract class ExternalComponent extends RocketComponent {
                this.finish = finish;
                fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
        }
+
+       
+       
+       @Override
+       protected void copyFrom(RocketComponent c) {
+               super.copyFrom(c);
+               
+               ExternalComponent src = (ExternalComponent)c;
+               this.finish = src.finish;
+               this.material = src.material;
+       }
        
 }
index be68570af48ab1989506215781d4171ba948b49c..96c85b26b8af5fb12def2b26a75087779b8d59ec 100644 (file)
@@ -1,6 +1,7 @@
 package net.sf.openrocket.rocketcomponent;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 
@@ -38,6 +39,22 @@ public abstract class FinSet extends ExternalComponent {
                }
        }
        
+       public enum TabRelativePosition {
+               FRONT("Root chord leading edge"),
+               CENTER("Root chord midpoint"),
+               END("Root chord trailing edge");
+               
+               private final String name;
+               TabRelativePosition(String name) {
+                       this.name = name;
+               }
+               
+               @Override
+               public String toString() {
+                       return name;
+               }
+       }
+       
        /**
         * Number of fins.
         */
@@ -80,7 +97,17 @@ public abstract class FinSet extends ExternalComponent {
        protected CrossSection crossSection = CrossSection.SQUARE;
        
        
+       /*
+        * Fin tab properties.
+        */
+       private double tabHeight = 0;
+       private double tabLength = 0.05;
+       private double tabShift = 0;
+       private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER;
+       
+       
        // Cached fin area & CG.  Validity of both must be checked using finArea!
+       // Fin area does not include fin tabs, CG does.
        private double finArea = -1;
        private double finCGx = -1;
        private double finCGy = -1;
@@ -220,16 +247,123 @@ public abstract class FinSet extends ExternalComponent {
                super.setPositionValue(value);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
+       
+       
 
        
+       public double getTabHeight() {
+               return tabHeight;
+       }
+
+       public void setTabHeight(double height) {
+               height = MathUtil.max(height, 0);
+               if (MathUtil.equals(this.tabHeight, height))
+                       return;
+               this.tabHeight = height;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
 
+
+       public double getTabLength() {
+               return tabLength;
+       }
+
+       public void setTabLength(double length) {
+               length = MathUtil.max(length, 0);
+               if (MathUtil.equals(this.tabLength, length))
+                       return;
+               this.tabLength = length;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+
+       public double getTabShift() {
+               return tabShift;
+       }
+
+       public void setTabShift(double shift) {
+               this.tabShift = shift;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
        
        
+       public TabRelativePosition getTabRelativePosition() {
+               return tabRelativePosition;
+       }
+       
+       public void setTabRelativePosition(TabRelativePosition position) {
+               if (this.tabRelativePosition == position)
+                       return;
+               
+
+               double front = getTabFrontEdge();
+               switch (position) {
+               case FRONT:
+                       this.tabShift = front;
+                       break;
+                       
+               case CENTER:
+                       this.tabShift = front + tabLength/2 - getLength()/2;
+                       break;
+                       
+               case END:
+                       this.tabShift = front + tabLength - getLength();
+                       break;
+                       
+               default:
+                       throw new IllegalArgumentException("position="+position);
+               }
+               this.tabRelativePosition = position;
+               
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+
+       
+       /**
+        * Return the tab front edge position from the front of the fin.
+        */
+       public double getTabFrontEdge() {
+               switch (this.tabRelativePosition) {
+               case FRONT:
+                       return tabShift;
+                       
+               case CENTER:
+                       return getLength()/2 - tabLength/2 + tabShift;
+                       
+               case END:
+                       return getLength() - tabLength + tabShift;
+                       
+               default:
+                       throw new IllegalStateException("tabRelativePosition="+tabRelativePosition);
+               }
+       }
+
+       /**
+        * Return the tab trailing edge position *from the front of the fin*.
+        */
+       public double getTabTrailingEdge() {
+               switch (this.tabRelativePosition) {
+               case FRONT:
+                       return tabLength + tabShift;                    
+               case CENTER:
+                       return getLength()/2 + tabLength/2 + tabShift;
+                       
+               case END:
+                       return getLength() + tabShift;
+                       
+               default:
+                       throw new IllegalStateException("tabRelativePosition="+tabRelativePosition);
+               }
+       }
+
+       
        
-       ///////////  Calculation methods  ///////////
        
+       ///////////  Calculation methods  ///////////
+
        /**
-        * Return the area of one side of one fin.
+        * Return the area of one side of one fin.  This does NOT include the area of
+        * the fin tab.
         * 
         * @return   the area of one side of one fin.
         */
@@ -240,6 +374,7 @@ public abstract class FinSet extends ExternalComponent {
                return finArea;
        }
        
+       
        /**
         * Return the unweighted CG of a single fin.  The X-coordinate is relative to
         * the root chord trailing edge and the Y-coordinate to the fin root chord.
@@ -257,7 +392,8 @@ public abstract class FinSet extends ExternalComponent {
 
        @Override
        public double getComponentVolume() {
-               return fins * getFinArea() * thickness * crossSection.getRelativeVolume();
+               return fins * (getFinArea() + tabHeight*tabLength) * thickness * 
+                       crossSection.getRelativeVolume();
        }
        
 
@@ -303,9 +439,21 @@ public abstract class FinSet extends ExternalComponent {
                if (finArea < 0)
                        finArea = 0;
                
-               if (finArea > 0) {
-                       finCGx /= finArea;
-                       finCGy /= finArea;
+               // Add effect of fin tabs to CG
+               double tabArea = tabLength * tabHeight;
+               if (!MathUtil.equals(tabArea, 0)) {
+                       
+                       double x = (getTabFrontEdge() + getTabTrailingEdge())/2;
+                       double y = -this.tabHeight/2;
+                       
+                       finCGx += x*tabArea;
+                       finCGy += y*tabArea;
+                       
+               }
+               
+               if ((finArea + tabArea) > 0) {
+                       finCGx /= (finArea + tabArea);
+                       finCGy /= (finArea + tabArea);
                } else {
                        finCGx = (points[0].x + points[points.length-1].x)/2;
                        finCGy = 0;
@@ -488,6 +636,38 @@ public abstract class FinSet extends ExternalComponent {
         */
        public abstract Coordinate[] getFinPoints();
        
+       
+       /**
+        * Return a list of coordinates defining the geometry of a single fin, including a
+        * possible fin tab.  The coordinates are the XY-coordinates of points defining the 
+        * shape of a single fin, where the origin is the leading root edge.  This implementation
+        * calls {@link #getFinPoints()} and adds the necessary points for the fin tab.
+        * The tab coordinates will have a negative y value.
+        * 
+        * @return  List of XY-coordinates.
+        */
+       public Coordinate[] getFinPointsWithTab() {
+               Coordinate[] points = getFinPoints();
+               
+               if (MathUtil.equals(getTabHeight(), 0) || 
+                               MathUtil.equals(getTabLength(), 0))
+                       return points;
+               
+               double x1 = getTabFrontEdge();
+               double x2 = getTabTrailingEdge();
+               double y = -getTabHeight();
+
+               int n = points.length;
+               points = Arrays.copyOf(points, points.length+4);
+               points[n] = new Coordinate(x2, 0);
+               points[n+1] = new Coordinate(x2, y);
+               points[n+2] = new Coordinate(x1, y);
+               points[n+3] = new Coordinate(x1, 0);
+               return points;
+       }
+       
+       
+       
        /**
         * Get the span of a single fin.  That is, the length from the root to the tip of the fin.
         * @return  Span of a single fin.
@@ -508,5 +688,9 @@ public abstract class FinSet extends ExternalComponent {
                this.cantRotation = src.cantRotation;
                this.thickness = src.thickness;
                this.crossSection = src.crossSection;
+               this.tabHeight = src.tabHeight;
+               this.tabLength = src.tabLength;
+               this.tabRelativePosition = src.tabRelativePosition;
+               this.tabShift = src.tabShift;
        }
 }
index 1312799fd36c1a8ebf51a0aa2498f3e0908aec86..d5275db88857c1072153fb7f326fa3047e0a87c6 100644 (file)
@@ -1,14 +1,13 @@
 package net.sf.openrocket.rocketcomponent;
 
 import java.util.ArrayList;
-import java.util.List;
 
 import net.sf.openrocket.util.Coordinate;
 
 
 public class FreeformFinSet extends FinSet {
 
-       private final List<Coordinate> points = new ArrayList<Coordinate>();
+       private ArrayList<Coordinate> points = new ArrayList<Coordinate>();
        
        public FreeformFinSet() {
                points.add(Coordinate.NUL);
@@ -19,7 +18,16 @@ public class FreeformFinSet extends FinSet {
                this.length = 0.05;
        }
        
-       
+
+       public FreeformFinSet(Coordinate[] finpoints) {
+               points.clear();
+               for (Coordinate c: finpoints) {
+                       points.add(c);
+               }
+               this.length = points.get(points.size()-1).x - points.get(0).x;
+       }
+
+       /*
        public FreeformFinSet(FinSet finset) {
                Coordinate[] finpoints = finset.getFinPoints();
                this.copyFrom(finset);
@@ -30,8 +38,69 @@ public class FreeformFinSet extends FinSet {
                }
                this.length = points.get(points.size()-1).x - points.get(0).x;
        }
+       */
        
        
+       /**
+        * Convert an existing fin set into a freeform fin set.  The specified
+        * fin set is taken out of the rocket tree (if any) and the new component
+        * inserted in its stead.
+        * <p>
+        * The specified fin set should not be used after the call!
+        * 
+        * @param finset        the fin set to convert.
+        * @return                      the new freeform fin set.
+        */
+       public static FreeformFinSet convertFinSet(FinSet finset) {
+               final RocketComponent root = finset.getRoot();
+               FreeformFinSet freeform;
+               
+               try {
+                       if (root instanceof Rocket) {
+                               ((Rocket)root).freeze();
+                       }
+
+                       // Get fin set position and remove fin set
+                       final RocketComponent parent = finset.getParent();
+                       final int position;
+                       if (parent != null) {
+                               position = parent.getChildPosition(finset);
+                               parent.removeChild(position);
+                       } else {
+                               position = -1;
+                       }
+
+                       
+                       // Create the freeform fin set
+                       Coordinate[] finpoints = finset.getFinPoints();
+                       freeform = new FreeformFinSet(finpoints);
+
+                       // Copy component attributes
+                       freeform.copyFrom(finset);
+                       
+                       // Set name
+                       final String componentTypeName = finset.getComponentName();
+                       final String name = freeform.getName();
+
+                       if (name.startsWith(componentTypeName)) {
+                               freeform.setName(freeform.getComponentName() + 
+                                               name.substring(componentTypeName.length()));
+                       }
+
+                       // Add freeform fin set to parent
+                       if (parent != null) {
+                               parent.addChild(freeform, position);
+                       }
+
+               } finally {
+                       if (root instanceof Rocket) {
+                               ((Rocket)root).thaw();
+                       }
+               }
+               return freeform;
+       }
+
+       
        
        /**
         * Add a fin point between indices <code>index-1</code> and <code>index</code>.
@@ -229,4 +298,13 @@ public class FreeformFinSet extends FinSet {
                return "Freeform fin set";
        }
 
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public RocketComponent copy() {
+               RocketComponent c = super.copy();
+               ((FreeformFinSet)c).points = (ArrayList<Coordinate>) this.points.clone();
+               return c;
+       }
+
 }
index 6cb230c4202979cdd62c25e8c3ef82b7cef943d4..29f146be617e9b68bbc1a48713001972e826106d 100644 (file)
@@ -1411,10 +1411,11 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
 
        
        /**
-        * Loads the RocketComponent fields from the given component.  This method may
-        * be called only for a Rocket component and is meant for use with the
-        * undo/redo mechanism.
-        *
+        * Loads the RocketComponent fields from the given component.  This method is meant
+        * for in-place replacement of a component.  It is used with the undo/redo
+        * mechanism and when converting a finset into a freeform fin set.
+        * This component must not have a parent, otherwise this method will fail.
+        * <p>
         * The fields are copied by reference, and the supplied component must not be used
         * after the call, as it is in an undefined state.
         * 
@@ -1422,9 +1423,6 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         */
        protected void copyFrom(RocketComponent src) {
                
-               if (!(this instanceof Rocket)) {
-                       throw new UnsupportedOperationException("copyFrom called for component " + this);
-               }
                if (this.parent != null) {
                        throw new UnsupportedOperationException("copyFrom called for non-root component " 
                                        + this);
index bb199219eb50e2e8c509f522a731867dfa6c4d04..d27d2cca25da176439519471a7b8c3918e4b522d 100644 (file)
@@ -1,7 +1,7 @@
 package net.sf.openrocket.rocketcomponent;
 
 
-public class TubeCoupler extends ThicknessRingComponent {
+public class TubeCoupler extends ThicknessRingComponent implements RadialParent {
 
        public TubeCoupler() {
                setOuterRadiusAutomatic(true);
@@ -22,9 +22,24 @@ public class TubeCoupler extends ThicknessRingComponent {
                return "Tube coupler";
        }
 
+       /**
+        * Allow all InternalComponents to be added to this component.
+        */
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
-               return false;
+               return InternalComponent.class.isAssignableFrom(type);
+       }
+
+
+       @Override
+       public double getInnerRadius(double x) {
+               return getInnerRadius();
+       }
+
+
+       @Override
+       public double getOuterRadius(double x) {
+               return getOuterRadius();
        }
 }
 
index 3686bedc38ca2a0bcee2e3a575944f5afe5916a7..55b4f160d786c5e88ab43d13b049e7a0e8572d2f 100644 (file)
@@ -167,6 +167,17 @@ public abstract class Unit {
        
        
        
+       /**
+        * Creates a new Value object with the specified value and this unit.
+        * 
+        * @param value the value to set.
+        * @return              a new Value object.
+        */
+       public Value toValue(double value) {
+               return new Value(value, this);
+       }
+       
+       
 
        /**
         * Round the value (in the current units) to a precision suitable for rough valuing
index 850cb925bf5a37d6147ff51db9b2ddcbd502876a..6b6d4997675052a5ad1ad33bf110f5ce17d6a799 100644 (file)
@@ -316,6 +316,26 @@ public class UnitGroup {
                defaultUnit = n;
        }
        
+       
+       
+       /**
+        * Find a unit by approximate unit name.  Only letters and (ordinary) numbers are
+        * considered in the matching.  This method is mainly means for testing, allowing
+        * a simple means to obtain a particular unit.
+        * 
+        * @param str   the unit name.
+        * @return              the corresponding unit, or <code>null</code> if not found.
+        */
+       public Unit findApproximate(String str) {
+               str = str.replaceAll("\\W", "").trim();
+               for (Unit u: units) {
+                       String name = u.getUnit().replaceAll("\\W", "").trim();
+                       if (str.equalsIgnoreCase(name))
+                               return u;
+               }
+               return null;
+       }
+       
        /**
         * Set the default unit based on the unit name.  Does nothing if the name
         * does not match any of the units.
@@ -393,6 +413,22 @@ public class UnitGroup {
        }
        
        
+
+       
+       
+       /**
+        * Creates a new Value object with the specified value and the default unit of this group.
+        * 
+        * @param value the value to set.
+        * @return              a new Value object.
+        */
+       public Value toValue(double value) {
+               return this.getDefaultUnit().toValue(value);
+       }
+       
+       
+       
+       
        private static final Pattern STRING_PATTERN = Pattern.compile("^\\s*([0-9.,-]+)(.*?)$");
        /**
         * Converts a string into an SI value.  If the string has one of the units in this
diff --git a/src/net/sf/openrocket/unit/Value.java b/src/net/sf/openrocket/unit/Value.java
new file mode 100644 (file)
index 0000000..54e1927
--- /dev/null
@@ -0,0 +1,135 @@
+package net.sf.openrocket.unit;
+
+/**
+ * A class representing an SI value and a unit.  The toString() method yields the
+ * current value in the current units.  This class may be used to encapsulate
+ * a sortable value for example for tables.  The sorting is performed by the
+ * value in the current units, ignoring the unit.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class Value implements Comparable<Value> {
+       
+       private double value;
+       private Unit unit;
+       
+       
+       /**
+        * Create a new Value object.
+        * 
+        * @param value         the value to set.
+        * @param unit          the unit to set (<code>null</code> not allowed)
+        */
+       public Value(double value, Unit unit) {
+               if (unit == null) {
+                       throw new IllegalArgumentException("unit is null");
+               }
+               this.value = value;
+               this.unit = unit;
+       }
+
+       
+       /**
+        * Get the value of this object.
+        * 
+        * @return the value
+        */
+       public double getValue() {
+               return value;
+       }
+
+       /**
+        * Set the value of this object.
+        * 
+        * @param value the value to set
+        */
+       public void setValue(double value) {
+               this.value = value;
+       }
+       
+       
+       /**
+        * Get the value of this object in the current units.
+        * 
+        * @return      the value in the current units.
+        */
+       public double getUnitValue() {
+               return unit.toUnit(value);
+       }
+       
+       
+       /**
+        * Set the value of this object in the current units.
+        * 
+        * @param value         the value in current units.
+        */
+       public void setUnitValue(double value) {
+               this.value = unit.fromUnit(value);
+       }
+       
+
+       /**
+        * Get the unit of this object.
+        * 
+        * @return the unit
+        */
+       public Unit getUnit() {
+               return unit;
+       }
+
+       /**
+        * Set the value of this object.
+        * 
+        * @param unit the unit to set (<code>null</code> not allowed)
+        */
+       public void setUnit(Unit unit) {
+               if (unit == null) {
+                       throw new IllegalArgumentException("unit is null");
+               }
+               this.unit = unit;
+       }
+       
+       
+       /**
+        * Return a string formatted using the {@link Unit#toStringUnit(double)} method
+        * of the current unit.  If the unit is <code>null</code> then the UNITS_NONE
+        * group is used.
+        */
+       @Override
+       public String toString() {
+               return unit.toStringUnit(value);
+       }
+
+       
+       
+       /**
+        * Compare this value to another value.  The comparison is performed primarily by
+        * the unit text, secondarily the value in the unit values.
+        */
+       @Override
+       public int compareTo(Value o) {
+               int n = this.getUnit().getUnit().compareTo(o.getUnit().getUnit());
+               if (n != 0)
+                       return n;
+               
+               double us = this.getUnitValue();
+               double them = o.getUnitValue();
+               
+               if (Double.isNaN(us)) {
+                       if (Double.isNaN(them))
+                               return 0;
+                       else
+                               return 1;
+               }
+               if (Double.isNaN(them))
+                       return -1;
+               
+               if (us < them)
+                       return -1;
+               else if (us > them)
+                       return 1;
+               else
+                       return 0;
+       }
+
+}
diff --git a/src/net/sf/openrocket/util/ComparablePair.java b/src/net/sf/openrocket/util/ComparablePair.java
new file mode 100644 (file)
index 0000000..7b87968
--- /dev/null
@@ -0,0 +1,28 @@
+package net.sf.openrocket.util;
+
+/**
+ * Sortable storage of a pair of objects.  A list of these objects can be sorted according
+ * to the first object.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ * @param <U>  the first object type, according to which comparisons are performed.
+ * @param <V>  the second object type.
+ */
+public class ComparablePair<U extends Comparable<U>, V> extends Pair<U, V> 
+       implements Comparable<ComparablePair<U, V>>{
+
+       public ComparablePair(U u, V v) {
+               super(u, v);
+       }
+       
+       
+       /**
+        * Compares the first objects.  If either of the objects is <code>null</code> this
+        * method throws <code>NullPointerException</code>.
+        */
+       @Override
+       public int compareTo(ComparablePair<U, V> other) {
+               return this.getU().compareTo(other.getU());
+       }
+
+}
index 27cf121c7a5ffad1ae201742e15c87cb7df226a4..2f50a9cf92d11c75d2c197d3d02c6764a8b48663 100644 (file)
@@ -1,36 +1,62 @@
 package net.sf.openrocket.util;
 
 import java.awt.Component;
+import java.awt.Container;
 import java.awt.Image;
 import java.awt.KeyboardFocusManager;
 import java.awt.Point;
 import java.awt.Window;
 import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentListener;
+import java.awt.event.FocusListener;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
+import java.beans.PropertyChangeListener;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.Vector;
 
 import javax.imageio.ImageIO;
 import javax.swing.AbstractAction;
+import javax.swing.AbstractButton;
 import javax.swing.Action;
+import javax.swing.DefaultBoundedRangeModel;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.DefaultListSelectionModel;
 import javax.swing.JButton;
+import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JDialog;
 import javax.swing.JRootPane;
+import javax.swing.JSlider;
+import javax.swing.JSpinner;
 import javax.swing.JTable;
+import javax.swing.JTree;
 import javax.swing.KeyStroke;
 import javax.swing.RootPaneContainer;
+import javax.swing.SpinnerNumberModel;
 import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeListener;
 import javax.swing.table.AbstractTableModel;
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.DefaultTableModel;
 import javax.swing.table.TableModel;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.DefaultTreeSelectionModel;
+import javax.swing.tree.TreeNode;
+
+import net.sf.openrocket.gui.Resettable;
 
 public class GUIUtil {
 
@@ -45,7 +71,6 @@ public class GUIUtil {
        loadImage("pix/icon/icon-032.png");
        loadImage("pix/icon/icon-016.png");
     }
-    
     private static void loadImage(String file) {
        InputStream is;
  
@@ -62,6 +87,26 @@ public class GUIUtil {
     }
     
 
+    
+    /**
+     * Set suitable options for a single-use disposable dialog.  This includes
+     * setting ESC to close the dialog and adding the appropriate window icons.
+     * If defaultButton is provided, it is set to the default button action.
+     * <p>
+     * The default button must be already attached to the dialog.
+     * 
+     * @param dialog           the dialog.
+     * @param defaultButton    the default button of the dialog, or <code>null</code>.
+     */
+    public static void setDisposableDialogOptions(JDialog dialog, JButton defaultButton) {
+       installEscapeCloseOperation(dialog);
+       setWindowIcons(dialog);
+       addModelNullingListener(dialog);
+       if (defaultButton != null) {
+               setDefaultButton(defaultButton);
+       }
+    }
+    
        
        
        /**
@@ -115,10 +160,165 @@ public class GUIUtil {
     }
 
     
+    
+    /**
+     * Set the OpenRocket icons to the window icons.
+     * 
+     * @param window   the window to set.
+     */
     public static void setWindowIcons(Window window) {
        window.setIconImages(images);
     }
     
+    /**
+     * Add a listener to the provided window that will call {@link #setNullModels(Component)}
+     * on the window once it is disposed.  This method may only be used on single-use
+     * windows and dialogs, that will never be shown again once closed!
+     * 
+     * @param window   the window to add the listener to.
+     */
+    public static void addModelNullingListener(final Window window) {
+       window.addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosed(WindowEvent e) {
+                               setNullModels(window);
+                       }
+       });
+    }
+
+
+       /**
+        * Traverses recursively the component tree, and sets all applicable component 
+        * models to null, so as to remove the listener connections.  After calling this
+        * method the component hierarchy should no longed be used.
+        * <p>
+        * All components that use custom models should be added to this method, as
+        * there exists no standard way of removing the model from a component.
+        * 
+        * @param c             the component (<code>null</code> is ok)
+        */
+       public static void setNullModels(Component c) {
+               if (c==null)
+                       return;
+               
+               // Remove various listeners
+               for (ComponentListener l: c.getComponentListeners()) {
+                       c.removeComponentListener(l);
+               }
+               for (FocusListener l: c.getFocusListeners()) {
+                       c.removeFocusListener(l);
+               }
+               for (MouseListener l: c.getMouseListeners()) {
+                       c.removeMouseListener(l);
+               }
+               for (PropertyChangeListener l: c.getPropertyChangeListeners()) {
+                       c.removePropertyChangeListener(l);
+               }
+               for (PropertyChangeListener l: c.getPropertyChangeListeners("model")) {
+                       c.removePropertyChangeListener("model", l);
+               }
+               for (PropertyChangeListener l: c.getPropertyChangeListeners("action")) {
+                       c.removePropertyChangeListener("action", l);
+               }
+               
+               // Remove models for known components
+               //  Why the FSCK must this be so hard?!?!?
+       
+               if (c instanceof JSpinner) {
+                       
+                       JSpinner spinner = (JSpinner)c;
+                       for (ChangeListener l: spinner.getChangeListeners()) {
+                               spinner.removeChangeListener(l);
+                       }
+                       spinner.setModel(new SpinnerNumberModel());
+                       
+               } else if (c instanceof JSlider) {
+                       
+                       JSlider slider = (JSlider)c;
+                       for (ChangeListener l: slider.getChangeListeners()) {
+                               slider.removeChangeListener(l);
+                       }
+                       slider.setModel(new DefaultBoundedRangeModel());
+                       
+               } else if (c instanceof JComboBox) {
+                       
+                       JComboBox combo = (JComboBox)c;
+                       for (ActionListener l: combo.getActionListeners()) {
+                               combo.removeActionListener(l);
+                       }
+                       combo.setModel(new DefaultComboBoxModel());
+                       
+               } else if (c instanceof AbstractButton) {
+                       
+                       AbstractButton button = (AbstractButton)c;
+                       for (ActionListener l: button.getActionListeners()) {
+                               button.removeActionListener(l);
+                       }
+                       button.setAction(new AbstractAction() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) { }
+                       });
+                       
+               } else if (c instanceof JTable) {
+                       
+                       JTable table = (JTable)c;
+                       table.setModel(new DefaultTableModel());
+                       table.setColumnModel(new DefaultTableColumnModel());
+                       table.setSelectionModel(new DefaultListSelectionModel());
+                       
+               } else if (c instanceof JTree) {
+                       
+                       JTree tree = (JTree)c;
+                       tree.setModel(new DefaultTreeModel(new TreeNode() {
+                               @SuppressWarnings("unchecked")
+                               @Override
+                               public Enumeration children() {
+                                       return new Vector().elements();
+                               }
+                               @Override
+                               public boolean getAllowsChildren() {
+                                       return false;
+                               }
+                               @Override
+                               public TreeNode getChildAt(int childIndex) {
+                                       return null;
+                               }
+                               @Override
+                               public int getChildCount() {
+                                       return 0;
+                               }
+                               @Override
+                               public int getIndex(TreeNode node) {
+                                       return 0;
+                               }
+                               @Override
+                               public TreeNode getParent() {
+                                       return null;
+                               }
+                               @Override
+                               public boolean isLeaf() {
+                                       return true;
+                               }
+                       }));
+                       tree.setSelectionModel(new DefaultTreeSelectionModel());
+                       
+               } else if (c instanceof Resettable) {
+                       
+                       ((Resettable)c).resetModel();
+                       
+               }
+       
+               // Recurse the component
+               if (c instanceof Container) {
+                       Component[] cs = ((Container)c).getComponents();
+                       for (Component sub: cs)
+                               setNullModels(sub);
+               }
+       
+       }
+       
+       
+
     
     /**
      * A mouse listener that toggles the state of a boolean value in a table model
@@ -184,5 +384,5 @@ public class GUIUtil {
                }
 
     }
-       
+    
 }
index fa2c25e778d6756a7c4013f81ac36646bf5060d3..a447009f1f3ee886375bffeab8262cd05ab89df5 100644 (file)
@@ -9,6 +9,7 @@ import javax.swing.Icon;
 import javax.swing.ImageIcon;
 
 import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.main.ExceptionHandler;
 
 
 public class Icons {
@@ -59,10 +60,19 @@ public class Icons {
        
        
        
+       /**
+        * Load an ImageIcon from the specified file.  The file is obtained as a system
+        * resource from the normal classpath.  If the file cannot be loaded a bug dialog
+        * is opened and <code>null</code> is returned.
+        * 
+        * @param file  the file to load.
+        * @param name  the description of the icon.
+        * @return              the ImageIcon, or null if could not be loaded (after the user closes the dialog)
+        */
        public static ImageIcon loadImageIcon(String file, String name) {
                URL url = ClassLoader.getSystemResource(file);
                if (url == null) {
-                       System.err.println("Resource "+file+" not found!  Ignoring...");
+                       ExceptionHandler.handleErrorCondition("Image file " + file + " not found, ignoring.");
                        return null;
                }
                return new ImageIcon(url, name);
index 4b765c3d4f0a972ba4f9d8d762799a85e87b8977..0246e91f337946b7f063720cc1d4dfa9b5ccb85d 100644 (file)
@@ -12,6 +12,7 @@ import javax.swing.SwingWorker;
 
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.file.RocketLoader;
+import net.sf.openrocket.gui.main.ExceptionHandler;
 
 
 /**
@@ -67,8 +68,7 @@ public class OpenFileWorker extends SwingWorker<OpenRocketDocument, Void> {
                        try {
                                is.close();
                        } catch (Exception e) {
-                               System.err.println("Error closing file: ");
-                               e.printStackTrace();
+                               ExceptionHandler.handleErrorCondition("Error closing file", e);
                        }
                }
        }
index 8a4cec289aabdfe3e87573f557e8e891c3994c30..e13b9b94660f854f095c54a37e82fe072f253093 100644 (file)
@@ -1,5 +1,12 @@
 package net.sf.openrocket.util;
 
+/**
+ * Storage for a pair of objects.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ * @param <U>  the first object type.
+ * @param <V>  the second object type.
+ */
 public class Pair<U,V> {
 
        private final U u;
index 049b546832ee5d657abebe15be1be8be50185cb6..2b7c4101ddfe1fc4a71d3af6b5da4a26f2733347 100644 (file)
@@ -18,6 +18,7 @@ import java.util.prefs.Preferences;
 
 import net.sf.openrocket.database.Databases;
 import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.main.ExceptionHandler;
 import net.sf.openrocket.material.Material;
 import net.sf.openrocket.rocketcomponent.BodyComponent;
 import net.sf.openrocket.rocketcomponent.FinSet;
@@ -62,7 +63,8 @@ public class Prefs {
                        InputStream is = ClassLoader.getSystemResourceAsStream("build.properties");
                        if (is == null) {
                                throw new MissingResourceException(
-                                               "build.properties not found, distribution built wrong",
+                                               "build.properties not found, distribution built wrong" + 
+                                               "   path:"+System.getProperty("java.class.path"),
                                                "build.properties", "build.version");
                        }
                        
@@ -172,6 +174,16 @@ public class Prefs {
        }
        
        
+       public static String getUniqueID() {
+               String id = PREFNODE.get("id", null);
+               if (id == null) {
+                       id = UniqueID.generateHashedID();
+                       PREFNODE.put("id", id);
+               }
+               return id;
+       }
+       
+       
        
        public static void storeVersion() {
                PREFNODE.put("OpenRocketVersion", getVersion());
@@ -471,8 +483,7 @@ public class Prefs {
                        }
                        
                } catch (BackingStoreException e) {
-                       System.err.println("BackingStoreException:");
-                       e.printStackTrace();
+                       ExceptionHandler.handleErrorCondition(e);
                }
        }
        
index 444c6926fd4d60212adeeb1c109f8b436777ffb8..a4a65c46c4da0c4284f437c00b42906838676613 100644 (file)
@@ -13,6 +13,7 @@ import javax.swing.SwingWorker;
 import net.sf.openrocket.document.Simulation;
 import net.sf.openrocket.file.CSVExport;
 import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
+import net.sf.openrocket.gui.main.ExceptionHandler;
 import net.sf.openrocket.simulation.FlightDataBranch;
 import net.sf.openrocket.simulation.FlightDataBranch.Type;
 import net.sf.openrocket.unit.Unit;
@@ -75,8 +76,7 @@ public class SaveCSVWorker extends SwingWorker<Void, Void> {
                        try {
                                os.close();
                        } catch (Exception e) {
-                               System.err.println("Error closing file: ");
-                               e.printStackTrace();
+                               ExceptionHandler.handleErrorCondition("Error closing file", e);
                        }
                }
                return null;
index 9f94d5105b91a8a4a2c5ef543ad2b4c6306f6ebb..fe5f70cfb51f23c4992b18d71d5278436e98af48 100644 (file)
@@ -8,6 +8,7 @@ import javax.swing.SwingWorker;
 
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.file.RocketSaver;
+import net.sf.openrocket.gui.main.ExceptionHandler;
 
 public class SaveFileWorker extends SwingWorker<Void, Void> {
 
@@ -46,8 +47,7 @@ public class SaveFileWorker extends SwingWorker<Void, Void> {
                        try {
                                os.close();
                        } catch (Exception e) {
-                               System.err.println("Error closing file: ");
-                               e.printStackTrace();
+                               ExceptionHandler.handleErrorCondition("Error closing file", e);
                        }
                }
                return null;
diff --git a/src/net/sf/openrocket/util/Test.java b/src/net/sf/openrocket/util/Test.java
deleted file mode 100644 (file)
index c0e37c7..0000000
+++ /dev/null
@@ -1,416 +0,0 @@
-package net.sf.openrocket.util;
-
-import net.sf.openrocket.database.Databases;
-import net.sf.openrocket.material.Material;
-import net.sf.openrocket.motor.Motor;
-import net.sf.openrocket.rocketcomponent.BodyTube;
-import net.sf.openrocket.rocketcomponent.Bulkhead;
-import net.sf.openrocket.rocketcomponent.CenteringRing;
-import net.sf.openrocket.rocketcomponent.FreeformFinSet;
-import net.sf.openrocket.rocketcomponent.IllegalFinPointException;
-import net.sf.openrocket.rocketcomponent.InnerTube;
-import net.sf.openrocket.rocketcomponent.LaunchLug;
-import net.sf.openrocket.rocketcomponent.MassComponent;
-import net.sf.openrocket.rocketcomponent.NoseCone;
-import net.sf.openrocket.rocketcomponent.Rocket;
-import net.sf.openrocket.rocketcomponent.Stage;
-import net.sf.openrocket.rocketcomponent.Transition;
-import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
-import net.sf.openrocket.rocketcomponent.TubeCoupler;
-import net.sf.openrocket.rocketcomponent.FinSet.CrossSection;
-import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
-
-public class Test {
-
-       public static double noseconeLength=0.10,noseconeRadius=0.01;
-       public static double bodytubeLength=0.20,bodytubeRadius=0.01,bodytubeThickness=0.001;
-       
-       public static int finCount=3;
-       public static double finRootChord=0.04,finTipChord=0.05,finSweep=0.01,finThickness=0.003, finHeight=0.03;
-       
-       public static double materialDensity=1000;  // kg/m3
-       
-
-       public static Rocket makeRocket() {
-               Rocket rocket;
-               Stage stage,stage2;
-               NoseCone nosecone;
-               BodyTube bodytube, bt2;
-               Transition transition;
-               TrapezoidFinSet finset;
-               
-               rocket = new Rocket();
-               stage = new Stage();
-               stage.setName("Stage1");
-               stage2 = new Stage();
-               stage2.setName("Stage2");
-               nosecone = new NoseCone(Transition.Shape.ELLIPSOID,noseconeLength,noseconeRadius);
-               bodytube = new BodyTube(bodytubeLength,bodytubeRadius,bodytubeThickness);
-               transition = new Transition();
-               bt2 = new BodyTube(bodytubeLength,bodytubeRadius*2,bodytubeThickness);
-               bt2.setMotorMount(true);
-
-               finset = new TrapezoidFinSet(finCount,finRootChord,finTipChord,finSweep,finHeight);
-               
-               
-               // Stage construction
-               rocket.addChild(stage);
-               rocket.addChild(stage2);
-
-               
-               // Component construction
-               stage.addChild(nosecone);
-               
-               stage.addChild(bodytube);
-
-               
-               stage2.addChild(transition);
-               
-               stage2.addChild(bt2);
-               
-               bodytube.addChild(finset);
-               
-               
-               rocket.getDefaultConfiguration().setAllStages();
-               
-               return rocket;
-       }
-       
-
-       public static Rocket makeSmallFlyable() {
-               Rocket rocket;
-               Stage stage;
-               NoseCone nosecone;
-               BodyTube bodytube;
-               TrapezoidFinSet finset;
-               
-               rocket = new Rocket();
-               stage = new Stage();
-               stage.setName("Stage1");
-
-               nosecone = new NoseCone(Transition.Shape.ELLIPSOID,noseconeLength,noseconeRadius);
-               bodytube = new BodyTube(bodytubeLength,bodytubeRadius,bodytubeThickness);
-
-               finset = new TrapezoidFinSet(finCount,finRootChord,finTipChord,finSweep,finHeight);
-               
-               
-               // Stage construction
-               rocket.addChild(stage);
-
-               
-               // Component construction
-               stage.addChild(nosecone);
-               stage.addChild(bodytube);
-
-               bodytube.addChild(finset);
-               
-               Material material = Prefs.getDefaultComponentMaterial(null, Material.Type.BULK);
-               nosecone.setMaterial(material);
-               bodytube.setMaterial(material);
-               finset.setMaterial(material);
-               
-               String id = rocket.newMotorConfigurationID();
-               bodytube.setMotorMount(true);
-               
-               for (Motor m: Databases.MOTOR) {
-                       if (m.getDesignation().equals("B4")) {
-                               bodytube.setMotor(id, m);
-                               break;
-                       }
-               }
-               bodytube.setMotorOverhang(0.005);
-               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
-               
-               rocket.getDefaultConfiguration().setAllStages();
-               
-               
-               return rocket;
-       }
-
-
-       public static Rocket makeBigBlue() {
-               Rocket rocket;
-               Stage stage;
-               NoseCone nosecone;
-               BodyTube bodytube;
-               FreeformFinSet finset;
-               MassComponent mcomp;
-               
-               rocket = new Rocket();
-               stage = new Stage();
-               stage.setName("Stage1");
-
-               nosecone = new NoseCone(Transition.Shape.ELLIPSOID,0.105,0.033);
-               nosecone.setThickness(0.001);
-               bodytube = new BodyTube(0.69,0.033,0.001);
-
-               finset = new FreeformFinSet();
-               try {
-                       finset.setPoints(new Coordinate[] {
-                                       new Coordinate(0, 0),
-                                       new Coordinate(0.115, 0.072),
-                                       new Coordinate(0.255, 0.072),
-                                       new Coordinate(0.255, 0.037),
-                                       new Coordinate(0.150, 0)
-                       });
-               } catch (IllegalFinPointException e) {
-                       e.printStackTrace();
-               }
-               finset.setThickness(0.003);
-               finset.setFinCount(4);
-               
-               finset.setCantAngle(0*Math.PI/180);
-               System.err.println("Fin cant angle: "+(finset.getCantAngle() * 180/Math.PI));
-               
-               mcomp = new MassComponent(0.2,0.03,0.045 + 0.060);
-               mcomp.setRelativePosition(Position.TOP);
-               mcomp.setPositionValue(0);
-               
-               // Stage construction
-               rocket.addChild(stage);
-               rocket.setPerfectFinish(false);
-
-               
-               // Component construction
-               stage.addChild(nosecone);
-               stage.addChild(bodytube);
-
-               bodytube.addChild(finset);
-               
-               bodytube.addChild(mcomp);
-               
-//             Material material = new Material("Test material", 500);
-//             nosecone.setMaterial(material);
-//             bodytube.setMaterial(material);
-//             finset.setMaterial(material);
-               
-               String id = rocket.newMotorConfigurationID();
-               bodytube.setMotorMount(true);
-               
-               for (Motor m: Databases.MOTOR) {
-                       if (m.getDesignation().equals("F12J")) {
-                               bodytube.setMotor(id, m);
-                               break;
-                       }
-               }
-               bodytube.setMotorOverhang(0.005);
-               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
-               
-               rocket.getDefaultConfiguration().setAllStages();
-               
-               
-               return rocket;
-       }
-       
-       
-
-       public static Rocket makeIsoHaisu() {
-               Rocket rocket;
-               Stage stage;
-               NoseCone nosecone;
-               BodyTube tube1, tube2, tube3;
-               TrapezoidFinSet finset;
-               TrapezoidFinSet auxfinset;
-               MassComponent mcomp;
-               
-               final double R = 0.07;
-               
-               rocket = new Rocket();
-               stage = new Stage();
-               stage.setName("Stage1");
-
-               nosecone = new NoseCone(Transition.Shape.OGIVE,0.53,R);
-               nosecone.setThickness(0.005);
-               nosecone.setMassOverridden(true);
-               nosecone.setOverrideMass(0.588);
-               stage.addChild(nosecone);
-               
-               tube1 = new BodyTube(0.505,R,0.005);
-               tube1.setMassOverridden(true);
-               tube1.setOverrideMass(0.366);
-               stage.addChild(tube1);
-               
-               tube2 = new BodyTube(0.605,R,0.005);
-               tube2.setMassOverridden(true);
-               tube2.setOverrideMass(0.427);
-               stage.addChild(tube2);
-               
-               tube3 = new BodyTube(1.065,R,0.005);
-               tube3.setMassOverridden(true);
-               tube3.setOverrideMass(0.730);
-               stage.addChild(tube3);
-               
-               
-               LaunchLug lug = new LaunchLug();
-               tube1.addChild(lug);
-               
-               TubeCoupler coupler = new TubeCoupler();
-               coupler.setOuterRadiusAutomatic(true);
-               coupler.setThickness(0.005);
-               coupler.setLength(0.28);
-               coupler.setMassOverridden(true);
-               coupler.setOverrideMass(0.360);
-               coupler.setRelativePosition(Position.BOTTOM);
-               coupler.setPositionValue(-0.14);
-               tube1.addChild(coupler);
-               
-               
-               // Parachute
-               MassComponent mass = new MassComponent(0.05, 0.05, 0.280);
-               mass.setRelativePosition(Position.TOP);
-               mass.setPositionValue(0.2);
-               tube1.addChild(mass);
-               
-               // Cord
-               mass = new MassComponent(0.05, 0.05, 0.125);
-               mass.setRelativePosition(Position.TOP);
-               mass.setPositionValue(0.2);
-               tube1.addChild(mass);
-               
-               // Payload
-               mass = new MassComponent(0.40, R, 1.500);
-               mass.setRelativePosition(Position.TOP);
-               mass.setPositionValue(0.25);
-               tube1.addChild(mass);
-               
-               
-               auxfinset = new TrapezoidFinSet();
-               auxfinset.setName("CONTROL");
-               auxfinset.setFinCount(2);
-               auxfinset.setRootChord(0.05);
-               auxfinset.setTipChord(0.05);
-               auxfinset.setHeight(0.10);
-               auxfinset.setSweep(0);
-               auxfinset.setThickness(0.008);
-               auxfinset.setCrossSection(CrossSection.AIRFOIL);
-               auxfinset.setRelativePosition(Position.TOP);
-               auxfinset.setPositionValue(0.28);
-               auxfinset.setBaseRotation(Math.PI/2);
-               tube1.addChild(auxfinset);
-               
-               
-               
-               
-               coupler = new TubeCoupler();
-               coupler.setOuterRadiusAutomatic(true);
-               coupler.setLength(0.28);
-               coupler.setRelativePosition(Position.TOP);
-               coupler.setPositionValue(0.47);
-               coupler.setMassOverridden(true);
-               coupler.setOverrideMass(0.360);
-               tube2.addChild(coupler);
-               
-               
-               
-               // Parachute
-               mass = new MassComponent(0.1, 0.05, 0.028);
-               mass.setRelativePosition(Position.TOP);
-               mass.setPositionValue(0.14);
-               tube2.addChild(mass);
-               
-               Bulkhead bulk = new Bulkhead();
-               bulk.setOuterRadiusAutomatic(true);
-               bulk.setMassOverridden(true);
-               bulk.setOverrideMass(0.050);
-               bulk.setRelativePosition(Position.TOP);
-               bulk.setPositionValue(0.27);
-               tube2.addChild(bulk);
-               
-               // Chord
-               mass = new MassComponent(0.1, 0.05, 0.125);
-               mass.setRelativePosition(Position.TOP);
-               mass.setPositionValue(0.19);
-               tube2.addChild(mass);
-               
-               
-               
-               InnerTube inner = new InnerTube();
-               inner.setOuterRadius(0.08/2);
-               inner.setInnerRadius(0.0762/2);
-               inner.setLength(0.86);
-               inner.setMassOverridden(true);
-               inner.setOverrideMass(0.388);
-               tube3.addChild(inner);
-               
-               
-               CenteringRing center = new CenteringRing();
-               center.setInnerRadiusAutomatic(true);
-               center.setOuterRadiusAutomatic(true);
-               center.setLength(0.005);
-               center.setMassOverridden(true);
-               center.setOverrideMass(0.038);
-               center.setRelativePosition(Position.BOTTOM);
-               center.setPositionValue(0);
-               tube3.addChild(center);
-               
-               
-               center = new CenteringRing();
-               center.setInnerRadiusAutomatic(true);
-               center.setOuterRadiusAutomatic(true);
-               center.setLength(0.005);
-               center.setMassOverridden(true);
-               center.setOverrideMass(0.038);
-               center.setRelativePosition(Position.TOP);
-               center.setPositionValue(0.28);
-               tube3.addChild(center);
-               
-               
-               center = new CenteringRing();
-               center.setInnerRadiusAutomatic(true);
-               center.setOuterRadiusAutomatic(true);
-               center.setLength(0.005);
-               center.setMassOverridden(true);
-               center.setOverrideMass(0.038);
-               center.setRelativePosition(Position.TOP);
-               center.setPositionValue(0.83);
-               tube3.addChild(center);
-               
-               
-               
-               
-               
-               finset = new TrapezoidFinSet();
-               finset.setRootChord(0.495);
-               finset.setTipChord(0.1);
-               finset.setHeight(0.185);
-               finset.setThickness(0.005);
-               finset.setSweep(0.3);
-               finset.setRelativePosition(Position.BOTTOM);
-               finset.setPositionValue(-0.03);
-               finset.setBaseRotation(Math.PI/2);
-               tube3.addChild(finset);
-               
-               
-               finset.setCantAngle(0*Math.PI/180);
-               System.err.println("Fin cant angle: "+(finset.getCantAngle() * 180/Math.PI));
-               
-               
-               // Stage construction
-               rocket.addChild(stage);
-               rocket.setPerfectFinish(false);
-
-               
-               
-               String id = rocket.newMotorConfigurationID();
-               tube3.setMotorMount(true);
-               
-               for (Motor m: Databases.MOTOR) {
-                       if (m.getDesignation().equals("L540")) {
-                               tube3.setMotor(id, m);
-                               break;
-                       }
-               }
-               tube3.setMotorOverhang(0.02);
-               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
-
-//             tube3.setIgnitionEvent(MotorMount.IgnitionEvent.NEVER);
-               
-               rocket.getDefaultConfiguration().setAllStages();
-               
-               
-               return rocket;
-       }
-       
-       
-       
-}
diff --git a/src/net/sf/openrocket/util/TestRockets.java b/src/net/sf/openrocket/util/TestRockets.java
new file mode 100644 (file)
index 0000000..6b5b83f
--- /dev/null
@@ -0,0 +1,583 @@
+package net.sf.openrocket.util;
+
+import java.awt.Color;
+import java.util.Random;
+
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.material.Material.Type;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.IllegalFinPointException;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.InternalComponent;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.ReferenceType;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.rocketcomponent.FinSet.CrossSection;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
+import net.sf.openrocket.rocketcomponent.Transition.Shape;
+
+public class TestRockets {
+       
+       private final String key;
+       private final Random rnd;
+       
+       
+       public TestRockets(String key) {
+
+               if (key == null) {
+                       Random rnd = new Random();
+                       StringBuilder sb = new StringBuilder();
+                       for (int i=0; i<6; i++) {
+                               int n = rnd.nextInt(62);
+                               if (n < 10) {
+                                       sb.append((char)('0'+n));
+                               } else if (n < 36) {
+                                       sb.append((char)('A'+n-10));
+                               } else {
+                                       sb.append((char)('a'+n-36));
+                               }
+                       }
+                       key = sb.toString();
+               }
+               
+               this.key = key;
+               this.rnd = new Random(key.hashCode());
+
+       }
+
+
+       /**
+        * Create a new test rocket based on the value 'key'.  The rocket utilizes most of the 
+        * properties and features available.  The same key always returns the same rocket,
+        * but different key values produce slightly different rockets.  A key value of
+        * <code>null</code> generates a rocket using a random key.
+        * <p>
+        * The rocket created by this method is not fly-worthy.  It is also NOT guaranteed
+        * that later versions would produce exactly the same rocket!
+        * 
+        * @return              a rocket design.
+        */
+       public Rocket makeTestRocket() {
+               
+               Rocket rocket = new Rocket();
+               setBasics(rocket);
+               rocket.setCustomReferenceLength(rnd(0.05));
+               rocket.setDesigner("Designer " + key);
+               rocket.setReferenceType((ReferenceType) randomEnum(ReferenceType.class));
+               rocket.setRevision("Rocket revision " + key);
+               rocket.setName(key);
+               
+               
+               Stage stage = new Stage();
+               setBasics(stage);
+               rocket.addChild(stage);
+               
+               
+               NoseCone nose = new NoseCone();
+               setBasics(stage);
+               nose.setAftRadius(rnd(0.03));
+               nose.setAftRadiusAutomatic(rnd.nextBoolean());
+               nose.setAftShoulderCapped(rnd.nextBoolean());
+               nose.setAftShoulderLength(rnd(0.02));
+               nose.setAftShoulderRadius(rnd(0.02));
+               nose.setAftShoulderThickness(rnd(0.002));
+               nose.setClipped(rnd.nextBoolean());
+               nose.setThickness(rnd(0.002));
+               nose.setFilled(rnd.nextBoolean());
+               nose.setForeRadius(rnd(0.1));  // Unset
+               nose.setLength(rnd(0.15));
+               nose.setShapeParameter(rnd(0.5));
+               nose.setType((Shape) randomEnum(Shape.class));
+               stage.addChild(nose);
+               
+               
+               Transition shoulder = new Transition();
+               setBasics(shoulder);
+               shoulder.setAftRadius(rnd(0.06));
+               shoulder.setAftRadiusAutomatic(rnd.nextBoolean());
+               shoulder.setAftShoulderCapped(rnd.nextBoolean());
+               shoulder.setAftShoulderLength(rnd(0.02));
+               shoulder.setAftShoulderRadius(rnd(0.05));
+               shoulder.setAftShoulderThickness(rnd(0.002));
+               shoulder.setClipped(rnd.nextBoolean());
+               shoulder.setThickness(rnd(0.002));
+               shoulder.setFilled(rnd.nextBoolean());
+               shoulder.setForeRadius(rnd(0.03));
+               shoulder.setForeRadiusAutomatic(rnd.nextBoolean());
+               shoulder.setForeShoulderCapped(rnd.nextBoolean());
+               shoulder.setForeShoulderLength(rnd(0.02));
+               shoulder.setForeShoulderRadius(rnd(0.02));
+               shoulder.setForeShoulderThickness(rnd(0.002));
+               shoulder.setLength(rnd(0.15));
+               shoulder.setShapeParameter(rnd(0.5));
+               shoulder.setThickness(rnd(0.003));
+               shoulder.setType((Shape) randomEnum(Shape.class));
+               stage.addChild(shoulder);
+               
+               
+               BodyTube body = new BodyTube();
+               setBasics(body);
+               body.setThickness(rnd(0.002));
+               body.setFilled(rnd.nextBoolean());
+               body.setIgnitionDelay(rnd.nextDouble()*3);
+               body.setIgnitionEvent((IgnitionEvent) randomEnum(IgnitionEvent.class));
+               body.setLength(rnd(0.3));
+               body.setMotorMount(rnd.nextBoolean());
+               body.setMotorOverhang(rnd.nextGaussian()*0.03);
+               body.setRadius(rnd(0.06));
+               body.setRadiusAutomatic(rnd.nextBoolean());
+               stage.addChild(body);
+
+               
+               Transition boattail = new Transition();
+               setBasics(boattail);
+               boattail.setAftRadius(rnd(0.03));
+               boattail.setAftRadiusAutomatic(rnd.nextBoolean());
+               boattail.setAftShoulderCapped(rnd.nextBoolean());
+               boattail.setAftShoulderLength(rnd(0.02));
+               boattail.setAftShoulderRadius(rnd(0.02));
+               boattail.setAftShoulderThickness(rnd(0.002));
+               boattail.setClipped(rnd.nextBoolean());
+               boattail.setThickness(rnd(0.002));
+               boattail.setFilled(rnd.nextBoolean());
+               boattail.setForeRadius(rnd(0.06));
+               boattail.setForeRadiusAutomatic(rnd.nextBoolean());
+               boattail.setForeShoulderCapped(rnd.nextBoolean());
+               boattail.setForeShoulderLength(rnd(0.02));
+               boattail.setForeShoulderRadius(rnd(0.05));
+               boattail.setForeShoulderThickness(rnd(0.002));
+               boattail.setLength(rnd(0.15));
+               boattail.setShapeParameter(rnd(0.5));
+               boattail.setThickness(rnd(0.003));
+               boattail.setType((Shape) randomEnum(Shape.class));
+               stage.addChild(boattail);
+               
+
+               MassComponent mass = new MassComponent();
+               setBasics(mass);
+               mass.setComponentMass(rnd(0.05));
+               mass.setLength(rnd(0.05));
+               mass.setRadialDirection(rnd(100));
+               mass.setRadialPosition(rnd(0.02));
+               mass.setRadius(rnd(0.05));
+               nose.addChild(mass);
+               
+               
+               
+               
+               return rocket;
+       }
+       
+       
+       private void setBasics(RocketComponent c) {
+               c.setComment(c.getComponentName() + " comment " + key);
+               c.setName(c.getComponentName() + " name " + key);
+
+               c.setCGOverridden(rnd.nextBoolean());
+               c.setMassOverridden(rnd.nextBoolean());
+               c.setOverrideCGX(rnd(0.2));
+               c.setOverrideMass(rnd(0.05));
+               c.setOverrideSubcomponents(rnd.nextBoolean());
+
+               if (c.isMassive()) {
+                       // Only massive components are drawn
+                       c.setColor(randomColor());
+                       c.setLineStyle((LineStyle) randomEnum(LineStyle.class));
+               }
+               
+               if (c instanceof ExternalComponent) {
+                       ExternalComponent e = (ExternalComponent)c;
+                       e.setFinish((Finish) randomEnum(Finish.class));
+                       double d = rnd(100);
+                       e.setMaterial(Material.newMaterial(Type.BULK, "Testmat "+d, d, rnd.nextBoolean()));
+               }
+               
+               if (c instanceof InternalComponent) {
+                       InternalComponent i = (InternalComponent)c;
+                       i.setRelativePosition((Position) randomEnum(Position.class));
+                       i.setPositionValue(rnd(0.3));
+               }
+       }
+       
+       
+       
+       private double rnd(double scale) {
+               return (rnd.nextDouble()*0.2+0.9) * scale;
+       }
+       
+       private Color randomColor() {
+               return new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
+       }
+       
+       private <T extends Enum<T>> Enum<T> randomEnum(Class<T> c) {
+               Enum<T>[] values = c.getEnumConstants();
+               if (values.length == 0)
+                       return null;
+               
+               return values[rnd.nextInt(values.length)];
+       }
+
+       
+       
+       
+       
+       
+       public Rocket makeSmallFlyable() {
+               double noseconeLength=0.10,noseconeRadius=0.01;
+               double bodytubeLength=0.20,bodytubeRadius=0.01,bodytubeThickness=0.001;
+               
+               int finCount=3;
+               double finRootChord=0.04,finTipChord=0.05,finSweep=0.01,finThickness=0.003, finHeight=0.03;
+               
+               
+               Rocket rocket;
+               Stage stage;
+               NoseCone nosecone;
+               BodyTube bodytube;
+               TrapezoidFinSet finset;
+               
+               rocket = new Rocket();
+               stage = new Stage();
+               stage.setName("Stage1");
+
+               nosecone = new NoseCone(Transition.Shape.ELLIPSOID,noseconeLength,noseconeRadius);
+               bodytube = new BodyTube(bodytubeLength,bodytubeRadius,bodytubeThickness);
+
+               finset = new TrapezoidFinSet(finCount,finRootChord,finTipChord,finSweep,finHeight);
+               
+               
+               // Stage construction
+               rocket.addChild(stage);
+
+               
+               // Component construction
+               stage.addChild(nosecone);
+               stage.addChild(bodytube);
+
+               bodytube.addChild(finset);
+               
+               Material material = Prefs.getDefaultComponentMaterial(null, Material.Type.BULK);
+               nosecone.setMaterial(material);
+               bodytube.setMaterial(material);
+               finset.setMaterial(material);
+               
+               String id = rocket.newMotorConfigurationID();
+               bodytube.setMotorMount(true);
+               
+               for (Motor m: Databases.MOTOR) {
+                       if (m.getDesignation().equals("B4")) {
+                               bodytube.setMotor(id, m);
+                               break;
+                       }
+               }
+               bodytube.setMotorOverhang(0.005);
+               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+               
+               rocket.getDefaultConfiguration().setAllStages();
+               
+               
+               return rocket;
+       }
+
+
+       public static Rocket makeBigBlue() {
+               Rocket rocket;
+               Stage stage;
+               NoseCone nosecone;
+               BodyTube bodytube;
+               FreeformFinSet finset;
+               MassComponent mcomp;
+               
+               rocket = new Rocket();
+               stage = new Stage();
+               stage.setName("Stage1");
+
+               nosecone = new NoseCone(Transition.Shape.ELLIPSOID,0.105,0.033);
+               nosecone.setThickness(0.001);
+               bodytube = new BodyTube(0.69,0.033,0.001);
+
+               finset = new FreeformFinSet();
+               try {
+                       finset.setPoints(new Coordinate[] {
+                                       new Coordinate(0, 0),
+                                       new Coordinate(0.115, 0.072),
+                                       new Coordinate(0.255, 0.072),
+                                       new Coordinate(0.255, 0.037),
+                                       new Coordinate(0.150, 0)
+                       });
+               } catch (IllegalFinPointException e) {
+                       e.printStackTrace();
+               }
+               finset.setThickness(0.003);
+               finset.setFinCount(4);
+               
+               finset.setCantAngle(0*Math.PI/180);
+               System.err.println("Fin cant angle: "+(finset.getCantAngle() * 180/Math.PI));
+               
+               mcomp = new MassComponent(0.2,0.03,0.045 + 0.060);
+               mcomp.setRelativePosition(Position.TOP);
+               mcomp.setPositionValue(0);
+               
+               // Stage construction
+               rocket.addChild(stage);
+               rocket.setPerfectFinish(false);
+
+               
+               // Component construction
+               stage.addChild(nosecone);
+               stage.addChild(bodytube);
+
+               bodytube.addChild(finset);
+               
+               bodytube.addChild(mcomp);
+               
+//             Material material = new Material("Test material", 500);
+//             nosecone.setMaterial(material);
+//             bodytube.setMaterial(material);
+//             finset.setMaterial(material);
+               
+               String id = rocket.newMotorConfigurationID();
+               bodytube.setMotorMount(true);
+               
+               for (Motor m: Databases.MOTOR) {
+                       if (m.getDesignation().equals("F12J")) {
+                               bodytube.setMotor(id, m);
+                               break;
+                       }
+               }
+               bodytube.setMotorOverhang(0.005);
+               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+               
+               rocket.getDefaultConfiguration().setAllStages();
+               
+               
+               return rocket;
+       }
+       
+       
+
+       public static Rocket makeIsoHaisu() {
+               Rocket rocket;
+               Stage stage;
+               NoseCone nosecone;
+               BodyTube tube1, tube2, tube3;
+               TrapezoidFinSet finset;
+               TrapezoidFinSet auxfinset;
+               MassComponent mcomp;
+               
+               final double R = 0.07;
+               
+               rocket = new Rocket();
+               stage = new Stage();
+               stage.setName("Stage1");
+
+               nosecone = new NoseCone(Transition.Shape.OGIVE,0.53,R);
+               nosecone.setThickness(0.005);
+               nosecone.setMassOverridden(true);
+               nosecone.setOverrideMass(0.588);
+               stage.addChild(nosecone);
+               
+               tube1 = new BodyTube(0.505,R,0.005);
+               tube1.setMassOverridden(true);
+               tube1.setOverrideMass(0.366);
+               stage.addChild(tube1);
+               
+               tube2 = new BodyTube(0.605,R,0.005);
+               tube2.setMassOverridden(true);
+               tube2.setOverrideMass(0.427);
+               stage.addChild(tube2);
+               
+               tube3 = new BodyTube(1.065,R,0.005);
+               tube3.setMassOverridden(true);
+               tube3.setOverrideMass(0.730);
+               stage.addChild(tube3);
+               
+               
+               LaunchLug lug = new LaunchLug();
+               tube1.addChild(lug);
+               
+               TubeCoupler coupler = new TubeCoupler();
+               coupler.setOuterRadiusAutomatic(true);
+               coupler.setThickness(0.005);
+               coupler.setLength(0.28);
+               coupler.setMassOverridden(true);
+               coupler.setOverrideMass(0.360);
+               coupler.setRelativePosition(Position.BOTTOM);
+               coupler.setPositionValue(-0.14);
+               tube1.addChild(coupler);
+               
+               
+               // Parachute
+               MassComponent mass = new MassComponent(0.05, 0.05, 0.280);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.2);
+               tube1.addChild(mass);
+               
+               // Cord
+               mass = new MassComponent(0.05, 0.05, 0.125);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.2);
+               tube1.addChild(mass);
+               
+               // Payload
+               mass = new MassComponent(0.40, R, 1.500);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.25);
+               tube1.addChild(mass);
+               
+               
+               auxfinset = new TrapezoidFinSet();
+               auxfinset.setName("CONTROL");
+               auxfinset.setFinCount(2);
+               auxfinset.setRootChord(0.05);
+               auxfinset.setTipChord(0.05);
+               auxfinset.setHeight(0.10);
+               auxfinset.setSweep(0);
+               auxfinset.setThickness(0.008);
+               auxfinset.setCrossSection(CrossSection.AIRFOIL);
+               auxfinset.setRelativePosition(Position.TOP);
+               auxfinset.setPositionValue(0.28);
+               auxfinset.setBaseRotation(Math.PI/2);
+               tube1.addChild(auxfinset);
+               
+               
+               
+               
+               coupler = new TubeCoupler();
+               coupler.setOuterRadiusAutomatic(true);
+               coupler.setLength(0.28);
+               coupler.setRelativePosition(Position.TOP);
+               coupler.setPositionValue(0.47);
+               coupler.setMassOverridden(true);
+               coupler.setOverrideMass(0.360);
+               tube2.addChild(coupler);
+               
+               
+               
+               // Parachute
+               mass = new MassComponent(0.1, 0.05, 0.028);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.14);
+               tube2.addChild(mass);
+               
+               Bulkhead bulk = new Bulkhead();
+               bulk.setOuterRadiusAutomatic(true);
+               bulk.setMassOverridden(true);
+               bulk.setOverrideMass(0.050);
+               bulk.setRelativePosition(Position.TOP);
+               bulk.setPositionValue(0.27);
+               tube2.addChild(bulk);
+               
+               // Chord
+               mass = new MassComponent(0.1, 0.05, 0.125);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.19);
+               tube2.addChild(mass);
+               
+               
+               
+               InnerTube inner = new InnerTube();
+               inner.setOuterRadius(0.08/2);
+               inner.setInnerRadius(0.0762/2);
+               inner.setLength(0.86);
+               inner.setMassOverridden(true);
+               inner.setOverrideMass(0.388);
+               tube3.addChild(inner);
+               
+               
+               CenteringRing center = new CenteringRing();
+               center.setInnerRadiusAutomatic(true);
+               center.setOuterRadiusAutomatic(true);
+               center.setLength(0.005);
+               center.setMassOverridden(true);
+               center.setOverrideMass(0.038);
+               center.setRelativePosition(Position.BOTTOM);
+               center.setPositionValue(0);
+               tube3.addChild(center);
+               
+               
+               center = new CenteringRing();
+               center.setInnerRadiusAutomatic(true);
+               center.setOuterRadiusAutomatic(true);
+               center.setLength(0.005);
+               center.setMassOverridden(true);
+               center.setOverrideMass(0.038);
+               center.setRelativePosition(Position.TOP);
+               center.setPositionValue(0.28);
+               tube3.addChild(center);
+               
+               
+               center = new CenteringRing();
+               center.setInnerRadiusAutomatic(true);
+               center.setOuterRadiusAutomatic(true);
+               center.setLength(0.005);
+               center.setMassOverridden(true);
+               center.setOverrideMass(0.038);
+               center.setRelativePosition(Position.TOP);
+               center.setPositionValue(0.83);
+               tube3.addChild(center);
+               
+               
+               
+               
+               
+               finset = new TrapezoidFinSet();
+               finset.setRootChord(0.495);
+               finset.setTipChord(0.1);
+               finset.setHeight(0.185);
+               finset.setThickness(0.005);
+               finset.setSweep(0.3);
+               finset.setRelativePosition(Position.BOTTOM);
+               finset.setPositionValue(-0.03);
+               finset.setBaseRotation(Math.PI/2);
+               tube3.addChild(finset);
+               
+               
+               finset.setCantAngle(0*Math.PI/180);
+               System.err.println("Fin cant angle: "+(finset.getCantAngle() * 180/Math.PI));
+               
+               
+               // Stage construction
+               rocket.addChild(stage);
+               rocket.setPerfectFinish(false);
+
+               
+               
+               String id = rocket.newMotorConfigurationID();
+               tube3.setMotorMount(true);
+               
+               for (Motor m: Databases.MOTOR) {
+                       if (m.getDesignation().equals("L540")) {
+                               tube3.setMotor(id, m);
+                               break;
+                       }
+               }
+               tube3.setMotorOverhang(0.02);
+               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+
+//             tube3.setIgnitionEvent(MotorMount.IgnitionEvent.NEVER);
+               
+               rocket.getDefaultConfiguration().setAllStages();
+               
+               
+               return rocket;
+       }
+       
+       
+       
+}
index d263c42ca49acf3ed343138e102c29ed5c620d87..2c54697409da1b9ab6cd8baba4a13ce7c0722238 100644 (file)
@@ -1,6 +1,8 @@
 package net.sf.openrocket.util;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
 
 /**
  * Defines an affine transformation of the form  A*x+c,  where x and c are Coordinates and
@@ -241,6 +243,20 @@ public class Transformation implements java.io.Serializable {
        }
        
        
+       @Override
+       public boolean equals(Object other) {
+               if (!(other instanceof Transformation))
+                       return false;
+               Transformation o = (Transformation)other;
+               for (int i=0; i<3; i++) {
+                       for (int j=0; j<3; j++) {
+                               if (!MathUtil.equals(this.rotation[i][j], o.rotation[i][j]))
+                                       return false;
+                       }
+               }
+               return this.translate.equals(o.translate);
+       }
+       
        public static void main(String[] arg) {
                Transformation t;
                
index 1cc082e816bf4ba1aaca592afe6703e76255856c..e7b56d4ac7dbef057d29ccfaa7b59c6a69c369ce 100644 (file)
@@ -1,8 +1,12 @@
 package net.sf.openrocket.util;
 
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import net.sf.openrocket.gui.main.ExceptionHandler;
+
 public class UniqueID {
        
        private static AtomicInteger nextId = new AtomicInteger(1);
@@ -30,4 +34,34 @@ public class UniqueID {
                return UUID.randomUUID().toString();
        }
        
+       
+       /**
+        * Return a hashed unique ID that contains no information whatsoever of the
+        * originating computer.
+        * 
+        * @return      a unique identifier string that contains no information about the computer.
+        */
+       public static String generateHashedID() {
+               String id = UUID.randomUUID().toString();
+               
+               try {
+                       MessageDigest algorithm = MessageDigest.getInstance("MD5");
+                       algorithm.reset();
+                       algorithm.update(id.getBytes());
+                       byte[] digest = algorithm.digest();
+                       
+                       StringBuilder sb = new StringBuilder();
+                       for (byte b: digest) {
+                               sb.append(String.format("%02X", 0xFF & b));
+                       }
+                       id = sb.toString();
+                       
+               } catch (NoSuchAlgorithmException e) {
+                       ExceptionHandler.handleErrorCondition(e);
+                       id = "" + id.hashCode();
+               }
+               
+               return id;
+       }
+       
 }
diff --git a/test/net/sf/openrocket/communication/CommunicationTest.java b/test/net/sf/openrocket/communication/CommunicationTest.java
new file mode 100644 (file)
index 0000000..8a2b9be
--- /dev/null
@@ -0,0 +1,120 @@
+package net.sf.openrocket.communication;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Random;
+
+import org.junit.Test;
+
+public class CommunicationTest {
+
+       @Test
+       public void testIllegalInputUpdateParsing() throws IOException {
+               
+               UpdateInfo info;
+               
+               info = Communication.parseUpdateInput(new StringReader(""));
+               assertNull(info);
+               
+               info = Communication.parseUpdateInput(new StringReader("vj\u00e4avdsads"));
+               assertNull(info);
+               
+               info = Communication.parseUpdateInput(new StringReader("\u0000\u0001\u0002"));
+               assertNull(info);
+               
+               info = Communication.parseUpdateInput(new StringReader("Version: 1.2"));
+               assertNull(info);
+               
+               info = Communication.parseUpdateInput(new StringReader("Version: 1.2pre"));
+               assertNull(info);
+               
+               info = Communication.parseUpdateInput(new StringReader("Version: 1.2.x"));
+               assertNull(info);
+               
+               info = Communication.parseUpdateInput(new StringReader("\u0000\u0001\u0002"));
+               assertNull(info);
+               
+               // Feed random bad input
+               Random rnd = new Random();
+               StringBuilder sb = new StringBuilder(10000);
+               for (int i=0; i<100; i++) {
+                       int length = rnd.nextInt(10000);
+                       sb.delete(0, sb.length());
+                       for (int j=0; j<length; j++) {
+                               sb.append((char)rnd.nextInt());
+                       }
+                       info = Communication.parseUpdateInput(new StringReader(sb.toString()));
+                       assertNull(info);
+               }
+               
+       }
+       
+       
+
+       @Test
+       public void testValidInputUpdateParsing() throws IOException {
+
+               UpdateInfo info;
+               
+               info = Communication.parseUpdateInput(new StringReader("Version: 1.2.3"));
+               assertNotNull(info);
+               assertEquals("1.2.3", info.getLatestVersion());
+               assertEquals(0, info.getUpdates().size());
+               
+               info = Communication.parseUpdateInput(new StringReader("Version: 1.2.3pre"));
+               assertNotNull(info);
+               assertEquals("1.2.3pre", info.getLatestVersion());
+               assertEquals(0, info.getUpdates().size());
+               
+               info = Communication.parseUpdateInput(new StringReader("Version: 1.2.3-build-3"));
+               assertNotNull(info);
+               assertEquals("1.2.3-build-3", info.getLatestVersion());
+               assertEquals(0, info.getUpdates().size());
+               
+               info = Communication.parseUpdateInput(new StringReader("Version: 1.2.3x\n\n"));
+               assertNotNull(info);
+               assertEquals("1.2.3x", info.getLatestVersion());
+               assertEquals(0, info.getUpdates().size());
+               
+               info = Communication.parseUpdateInput(new StringReader("Version:1.2.3\nfdsacd\u00e4fdsa"));
+               assertNotNull(info);
+               assertEquals("1.2.3", info.getLatestVersion());
+               assertEquals(0, info.getUpdates().size());
+               
+               info = Communication.parseUpdateInput(new StringReader(
+                               "Version:   1.2.3  \n" +
+                               "15: Fifteen\n" +
+                               "3: Three1  \r\n" +
+                               "3: Three2\r" +
+                               "1:One"));
+               assertNotNull(info);
+               assertEquals("1.2.3", info.getLatestVersion());
+               assertEquals(4, info.getUpdates().size());
+               assertEquals(15, info.getUpdates().get(0).getU());
+               assertEquals(3, info.getUpdates().get(1).getU());
+               assertEquals(3, info.getUpdates().get(2).getU());
+               assertEquals(1, info.getUpdates().get(3).getU());
+               assertEquals("Fifteen", info.getUpdates().get(0).getV());
+               assertEquals("Three1", info.getUpdates().get(1).getV());
+               assertEquals("Three2", info.getUpdates().get(2).getV());
+               assertEquals("One", info.getUpdates().get(3).getV());
+               
+
+               info = Communication.parseUpdateInput(new StringReader(
+                               "Version: 1.2.3\n" +
+                               "15:   (C) 1234 A&B %23 \\o/  \r\r\n" +
+                               "5: m\u00e4c\n" +
+                               "3: Invalid\u0000value\n" +
+                               "1: One\u0019two"));
+               assertNotNull(info);
+               assertEquals("1.2.3", info.getLatestVersion());
+               assertEquals(1, info.getUpdates().size());
+               assertEquals(15, info.getUpdates().get(0).getU());
+               assertEquals("(C) 1234 A&B %23 \\o/", info.getUpdates().get(0).getV());
+               
+               
+               
+       }
+}
diff --git a/test/net/sf/openrocket/rocketcomponent/ComponentCompare.java b/test/net/sf/openrocket/rocketcomponent/ComponentCompare.java
new file mode 100644 (file)
index 0000000..a9fb278
--- /dev/null
@@ -0,0 +1,146 @@
+package net.sf.openrocket.rocketcomponent;
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.regex.Pattern;
+
+public class ComponentCompare {
+       
+       private static final Pattern GETTER_PATTERN = Pattern.compile("^(is|get)[A-Z].*+");
+       
+       private static final String[] IGNORED_METHODS = {
+               "getClass", "getChildCount", "getChildren", "getNextComponent", "getID", 
+               "getPreviousComponent", "getParent", "getRocket", "getRoot", "getStage", 
+               "getStageNumber", "getComponentName",
+               // Rocket specific methods:
+               "getModID", "getMassModID",     "getAerodynamicModID", "getTreeModID", "getFunctionalModID",
+               "getMotorConfigurationIDs", "getDefaultConfiguration"
+       };
+       
+       
+       /**
+        * Check whether the two components are <em>equal</em>.  Two components are considered
+        * equal if they are of the same type and all of their getXXX() and isXXX() methods
+        * return equal values.
+        * 
+        * @param c1    the first component to compare.
+        * @param c2    the second component to compare.
+        */
+       public static void assertEquality(RocketComponent c1, RocketComponent c2) {
+               assertEquals(c1.getClass(), c2.getClass());
+               
+               // Same class + similar  ==  equal
+               assertSimilarity(c1, c2);
+       }
+       
+       
+       
+       public static void assertDeepEquality(RocketComponent c1, RocketComponent c2) {
+               assertEquality(c1, c2);
+               
+               Iterator<RocketComponent> i1 = c1.iterator();
+               Iterator<RocketComponent> i2 = c2.iterator();
+               while (i1.hasNext()) {
+                       assertTrue("iterator continues", i2.hasNext());
+                       RocketComponent comp1 = i1.next();
+                       RocketComponent comp2 = i2.next();
+                       assertDeepEquality(comp1, comp2);
+               }
+               assertFalse("iterator end", i2.hasNext());
+       }
+       
+
+
+       public static void assertDeepSimilarity(RocketComponent c1, RocketComponent c2,
+                       boolean allowNameDifference) {
+               assertSimilarity(c1, c2, allowNameDifference);
+               
+               Iterator<RocketComponent> i1 = c1.iterator();
+               Iterator<RocketComponent> i2 = c2.iterator();
+               while (i1.hasNext()) {
+                       assertTrue("iterator continues", i2.hasNext());
+                       RocketComponent comp1 = i1.next();
+                       RocketComponent comp2 = i2.next();
+                       assertDeepSimilarity(comp1, comp2, allowNameDifference);
+               }
+               assertFalse("iterator end", i2.hasNext());
+       }
+       
+
+
+       /**
+        * Check whether the two components are <em>similar</em>.  Two components are similar
+        * if each of the getXXX and isXXX methods that both object types have return
+        * equal values.  This does not check whether the two components are of the same type.
+        * 
+        * @param c1    the first component.
+        * @param c2    the second component.
+        */
+       public static void assertSimilarity(RocketComponent c1, RocketComponent c2) {
+               assertSimilarity(c1, c2, false);
+       }
+       
+       /**
+        * Check whether the two components are <em>similar</em>, allowing a name difference.
+        * 
+        * @param c1    the first component.
+        * @param c2    the second component.
+        * @param allowNameDifference   whether to allow the components to have different names.
+        */
+       public static void assertSimilarity(RocketComponent c1, RocketComponent c2, 
+                       boolean allowNameDifference) {
+               Class<? extends RocketComponent> class1 = c1.getClass();
+               Class<? extends RocketComponent> class2 = c2.getClass();
+               
+               mainloop:
+               for (Method m1: class1.getMethods()) {
+                       // Check for getter method
+                       String name = m1.getName();
+                       if (!GETTER_PATTERN.matcher(name).matches())
+                               continue;
+
+                       // Ignore methods that take parameters
+                       if (m1.getParameterTypes().length != 0)
+                               continue;
+                       
+                       // Ignore specific getters
+                       for (String ignore: IGNORED_METHODS) {
+                               if (name.equals(ignore))
+                                       continue mainloop;
+                       }
+                       if (allowNameDifference && name.equals("getName"))
+                               continue;
+                               
+                       
+                       // Check for method in other class
+                       Method m2;
+                       try {
+                               m2 = class2.getMethod(name);
+                       } catch (NoSuchMethodException e) {
+                               continue;
+                       }
+                       
+//                     System.out.println("Testing results of method " + name);
+                       
+                       // Run the methods
+                       Object result1, result2;
+                       try {
+                               result1 = m1.invoke(c1);
+                               result2 = m2.invoke(c2);
+                       } catch (Exception e) {
+                               throw new RuntimeException("Error executing method " + name, e);
+                       }
+                       
+                       if (result1 != null && result2 != null && 
+                                       result1.getClass().isArray() && result2.getClass().isArray()) {
+                               assertArrayEquals("Comparing result of method " + name,
+                                               (Object[])result1, (Object[])result2);
+                       } else {
+                               assertEquals("Comparing result of method " + name, result1, result2);
+                       }
+               }
+       }
+
+}
diff --git a/test/net/sf/openrocket/rocketcomponent/ComponentCompareTest.java b/test/net/sf/openrocket/rocketcomponent/ComponentCompareTest.java
new file mode 100644 (file)
index 0000000..d1f94d9
--- /dev/null
@@ -0,0 +1,131 @@
+package net.sf.openrocket.rocketcomponent;
+
+import static org.junit.Assert.*;
+
+import java.awt.Color;
+import java.util.Iterator;
+
+import net.sf.openrocket.util.Coordinate;
+
+import org.junit.Test;
+
+public class ComponentCompareTest {
+
+       @Test
+       public void testComponentEquality() {
+               Rocket r1 = net.sf.openrocket.util.TestRockets.makeBigBlue();
+               Rocket r2 = net.sf.openrocket.util.TestRockets.makeBigBlue();
+
+               Iterator<RocketComponent> i1 = r1.deepIterator(true);
+               Iterator<RocketComponent> i2 = r2.deepIterator(true);
+               while (i1.hasNext()) {
+                       assertTrue(i2.hasNext());
+                       
+                       RocketComponent c1 = i1.next();
+                       RocketComponent c2 = i2.next();
+                       
+                       ComponentCompare.assertEquality(c1, c2);
+                       ComponentCompare.assertSimilarity(c1, c2);
+               }
+               assertFalse(i2.hasNext());
+
+               
+               ComponentCompare.assertDeepEquality(r1, r2);
+               ComponentCompare.assertDeepSimilarity(r1, r2, false);
+               
+               
+               r1.setColor(Color.YELLOW);
+               try {
+                       ComponentCompare.assertEquality(r1, r2);
+                       fail();
+               } catch (AssertionError e) {
+                       // Correct behavior
+               }
+               
+               
+               i1 = r1.deepIterator(true);
+               i2 = r2.deepIterator(true);
+               boolean finsetfound = false;
+               while (i1.hasNext()) {
+                       RocketComponent c1 = i1.next();
+                       RocketComponent c2 = i2.next();
+                       
+                       if (c1 instanceof FinSet) {
+                               finsetfound = true;
+                               FinSet f1 = (FinSet)c1;
+                               f1.setTabHeight(0.001);
+                               
+                               try {
+                                       ComponentCompare.assertEquality(c1, c2);
+                                       fail();
+                               } catch (AssertionError e) {
+                                       // Correct behavior
+                               }
+                       }
+               }
+               assertTrue(finsetfound);
+       }
+       
+       
+       @Test
+       public void testComponentSimilarity() {
+               FinSet trap = new TrapezoidFinSet(
+                               5,   // fins
+                               5.0, // root
+                               3.0, // tip
+                               0.0, // sweep
+                               2.0); // height
+               FinSet free = new FreeformFinSet(new Coordinate[] {
+                               new Coordinate(0,0),
+                               new Coordinate(0,2),
+                               new Coordinate(3,2),
+                               new Coordinate(5,0)
+               });
+               free.setFinCount(5);
+               
+               ComponentCompare.assertSimilarity(trap, free, true);
+               
+               try {
+                       ComponentCompare.assertSimilarity(trap, free);
+                       fail();
+               } catch (AssertionError e) {
+                       // Correct behavior
+               }
+               
+               free.setName(trap.getName());
+               ComponentCompare.assertSimilarity(trap, free);
+               
+               try {
+                       ComponentCompare.assertEquality(trap, free);
+                       fail();
+               } catch (AssertionError e) {
+                       // Correct behavior
+               }
+               
+               
+               BodyTube t1 = new BodyTube();
+               BodyTube t2 = new BodyTube();
+               t1.addChild(free);
+               t2.addChild(trap);
+               
+               ComponentCompare.assertDeepSimilarity(t1, t2, false);
+
+               try {
+                       ComponentCompare.assertDeepEquality(t1, t2);
+                       fail();
+               } catch (AssertionError e) {
+                       // Correct behavior
+               }
+               
+               t1.addChild(new TrapezoidFinSet());
+
+               try {
+                       ComponentCompare.assertDeepSimilarity(t1, t2, true);
+                       fail();
+               } catch (AssertionError e) {
+                       // Correct behavior
+               }
+               
+       }
+       
+}
diff --git a/test/net/sf/openrocket/rocketcomponent/FinSetTest.java b/test/net/sf/openrocket/rocketcomponent/FinSetTest.java
new file mode 100644 (file)
index 0000000..ae0e665
--- /dev/null
@@ -0,0 +1,111 @@
+package net.sf.openrocket.rocketcomponent;
+
+import static org.junit.Assert.*;
+
+import java.awt.Color;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.material.Material.Type;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.rocketcomponent.FinSet.CrossSection;
+import net.sf.openrocket.rocketcomponent.FinSet.TabRelativePosition;
+import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
+import net.sf.openrocket.util.LineStyle;
+
+import org.junit.Test;
+
+public class FinSetTest {
+       
+       
+       @Test
+       public void testFreeformConvert() {
+               testFreeformConvert(new TrapezoidFinSet());
+               testFreeformConvert(new EllipticalFinSet());
+               testFreeformConvert(new FreeformFinSet());
+       }
+       
+       
+       private void testFreeformConvert(FinSet fin) {
+               FreeformFinSet converted;
+               Material mat = Material.newMaterial(Type.BULK, "foo", 0.1, true);
+               
+               fin.setBaseRotation(1.1);
+               fin.setCantAngle(0.001);
+               fin.setCGOverridden(true);
+               fin.setColor(Color.YELLOW);
+               fin.setComment("cmt");
+               fin.setCrossSection(CrossSection.ROUNDED);
+               fin.setFinCount(5);
+               fin.setFinish(Finish.ROUGH);
+               fin.setLineStyle(LineStyle.DASHDOT);
+               fin.setMassOverridden(true);
+               fin.setMaterial(mat);
+               fin.setOverrideCGX(0.012);
+               fin.setOverrideMass(0.0123);
+               fin.setOverrideSubcomponents(true);
+               fin.setPositionValue(0.1);
+               fin.setRelativePosition(Position.ABSOLUTE);
+               fin.setTabHeight(0.01);
+               fin.setTabLength(0.02);
+               fin.setTabRelativePosition(TabRelativePosition.END);
+               fin.setTabShift(0.015);
+               fin.setThickness(0.005);
+
+               converted = FreeformFinSet.convertFinSet(fin);
+               
+               ComponentCompare.assertSimilarity(fin, converted, true);
+               
+               assertEquals(converted.getComponentName(), converted.getName());
+               
+               
+               // Create test rocket
+               Rocket rocket = new Rocket();
+               Stage stage = new Stage();
+               BodyTube body = new BodyTube();
+               
+               rocket.addChild(stage);
+               stage.addChild(body);
+               body.addChild(fin);
+               
+               Listener l1 = new Listener("l1");
+               rocket.addComponentChangeListener(l1);
+               
+               fin.setName("Custom name");
+               assertTrue(l1.changed);
+               assertEquals(ComponentChangeEvent.NONFUNCTIONAL_CHANGE, l1.changetype);
+               
+               
+               // Create copy
+               RocketComponent rocketcopy = rocket.copy();
+               
+               Listener l2 = new Listener("l2");
+               rocketcopy.addComponentChangeListener(l2);
+               
+               FinSet fincopy = (FinSet) rocketcopy.getChild(0).getChild(0).getChild(0);
+               FreeformFinSet.convertFinSet(fincopy);
+               
+               assertTrue(l2.changed);
+               assertEquals(ComponentChangeEvent.TREE_CHANGE, 
+                               l2.changetype & ComponentChangeEvent.TREE_CHANGE);
+               
+       }
+       
+       
+       private static class Listener implements ComponentChangeListener {
+               private boolean changed = false;
+               private int changetype = 0;
+               private final String name;
+               
+               public Listener(String name) {
+                       this.name = name;
+               }
+               
+               @Override
+               public void componentChanged(ComponentChangeEvent e) {
+                       assertFalse("Ensuring listener "+name+" has not been called.", changed);
+                       changed = true;
+                       changetype = e.getType();
+               }
+       }
+
+}
diff --git a/test/net/sf/openrocket/rocketcomponent/RocketTest.java b/test/net/sf/openrocket/rocketcomponent/RocketTest.java
new file mode 100644 (file)
index 0000000..11c55dc
--- /dev/null
@@ -0,0 +1,21 @@
+package net.sf.openrocket.rocketcomponent;
+
+import org.junit.Test;
+
+public class RocketTest {
+       
+       @Test
+       public void testCopyFrom() {
+               Rocket r1 = net.sf.openrocket.util.TestRockets.makeIsoHaisu();
+               Rocket r2 = net.sf.openrocket.util.TestRockets.makeBigBlue();
+               
+               Rocket copy = r2.copy();
+               
+               ComponentCompare.assertDeepEquality(r2, copy);
+               
+               r1.copyFrom(copy);
+               
+               ComponentCompare.assertDeepEquality(r1, r2);
+       }
+
+}
diff --git a/test/net/sf/openrocket/unit/ValueTest.java b/test/net/sf/openrocket/unit/ValueTest.java
new file mode 100644 (file)
index 0000000..0c8c61f
--- /dev/null
@@ -0,0 +1,43 @@
+package net.sf.openrocket.unit;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class ValueTest {
+
+       @Test
+       public void testValues() {
+               Value v1, v2;
+               
+               v1 = new Value(273.15, UnitGroup.UNITS_TEMPERATURE.findApproximate("F"));
+               v2 = new Value(283.15, UnitGroup.UNITS_TEMPERATURE.findApproximate("C"));
+               
+               assertTrue(v1.compareTo(v2) > 0);
+               assertTrue(v2.compareTo(v1) < 0);
+               assertTrue(v1.compareTo(v1) == 0);
+               assertTrue(v2.compareTo(v2) == 0);
+               
+               v2.setUnit(UnitGroup.UNITS_TEMPERATURE.findApproximate("K"));
+               assertTrue(v1.compareTo(v2) > 0);
+               assertTrue(v2.compareTo(v1) < 0);
+               assertEquals("283 K", v2.toString());
+
+               v2.setUnit(UnitGroup.UNITS_TEMPERATURE.findApproximate("F"));
+               assertTrue(v1.compareTo(v2) < 0);
+               assertTrue(v2.compareTo(v1) > 0);
+               
+               
+               v1.setValue(Double.NaN);
+               assertTrue(v1.compareTo(v2) > 0);
+               assertTrue(v2.compareTo(v1) < 0);
+               
+               v2.setValue(Double.NaN);
+               assertTrue(v1.compareTo(v2) == 0);
+               assertTrue(v1.compareTo(v2) == 0);
+               assertEquals("N/A", v1.toString());
+               assertEquals("N/A", v2.toString());
+               
+       }
+       
+}
diff --git a/test/net/sf/openrocket/util/UniqueIDTest.java b/test/net/sf/openrocket/util/UniqueIDTest.java
new file mode 100644 (file)
index 0000000..eeec615
--- /dev/null
@@ -0,0 +1,50 @@
+package net.sf.openrocket.util;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class UniqueIDTest {
+
+       @Test
+       public void integerTest() {
+               
+               int n = UniqueID.next();
+               assertTrue(n > 0);
+               assertEquals(n+1, UniqueID.next());
+               assertEquals(n+2, UniqueID.next());
+               assertEquals(n+3, UniqueID.next());
+               
+       }
+       
+       
+       @Test
+       public void stringTest() {
+               String id = UniqueID.uuid();
+               assertNotNull(id);
+               assertNotSame(id, UniqueID.uuid());
+               assertNotSame(id, UniqueID.uuid());
+       }
+       
+       @Test
+       public void hashedTest() {
+               String id = UniqueID.generateHashedID();
+               assertNotNull(id);
+               
+               boolean matchhigh = false;
+               boolean matchlow = false;
+               for (int i=0; i<100; i++) {
+                       String newid = UniqueID.generateHashedID();
+                       assertNotNull(newid);
+                       assertNotSame(id, newid);
+                       assertTrue(newid.matches("^[0-9a-fA-F]{32}$"));
+                       
+                       // Check that both high and low values occur
+                       matchhigh = matchhigh || newid.matches("^([0-9a-fA-F][0-9a-fA-F])*[A-F].*");
+                       matchlow = matchlow || newid.matches("^([0-9a-fA-F][0-9a-fA-F])*[0-4].*");
+               }
+               assertTrue(matchhigh);
+               assertTrue(matchlow);
+       }
+       
+}