Undo/redo system enhancements, DnD for component tree, bug fixes
authorplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Sun, 3 Oct 2010 14:37:46 +0000 (14:37 +0000)
committerplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Sun, 3 Oct 2010 14:37:46 +0000 (14:37 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@86 180e2498-e6e9-4542-8430-84ac67f01cd8

107 files changed:
ChangeLog
doc/techdoc/techdoc.pdf
doc/undo-redo-flow.uxf [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/FlightConditions.java
src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java
src/net/sf/openrocket/communication/UpdateInfoRetriever.java
src/net/sf/openrocket/document/OpenRocketDocument.java
src/net/sf/openrocket/document/Simulation.java
src/net/sf/openrocket/file/RocketLoader.java
src/net/sf/openrocket/file/rocksim/RocksimLoader.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/components/URLLabel.java
src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java
src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java
src/net/sf/openrocket/gui/main/BareComponentTreeModel.java [deleted file]
src/net/sf/openrocket/gui/main/BasicFrame.java
src/net/sf/openrocket/gui/main/ComponentAddButtons.java
src/net/sf/openrocket/gui/main/ComponentTree.java [deleted file]
src/net/sf/openrocket/gui/main/ComponentTreeModel.java [deleted file]
src/net/sf/openrocket/gui/main/ComponentTreeRenderer.java [deleted file]
src/net/sf/openrocket/gui/main/DocumentSelectionModel.java
src/net/sf/openrocket/gui/main/ExceptionHandler.java
src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java [new file with mode: 0644]
src/net/sf/openrocket/gui/plot/PlotDialog.java
src/net/sf/openrocket/gui/scalefigure/RocketPanel.java
src/net/sf/openrocket/logging/LogHelper.java
src/net/sf/openrocket/logging/LogLevel.java
src/net/sf/openrocket/logging/TraceException.java
src/net/sf/openrocket/models/atmosphere/AtmosphericConditions.java
src/net/sf/openrocket/optimization/Function.java [deleted file]
src/net/sf/openrocket/optimization/FunctionCache.java [deleted file]
src/net/sf/openrocket/optimization/FunctionCacheComparator.java [deleted file]
src/net/sf/openrocket/optimization/FunctionCallable.java [deleted file]
src/net/sf/openrocket/optimization/FunctionDecorator.java [deleted file]
src/net/sf/openrocket/optimization/FunctionOptimizer.java [deleted file]
src/net/sf/openrocket/optimization/MultidirectionalSearchOptimizer.java [deleted file]
src/net/sf/openrocket/optimization/MultipleOptimizationController.java [deleted file]
src/net/sf/openrocket/optimization/OptimizationController.java [deleted file]
src/net/sf/openrocket/optimization/ParallelExecutorCache.java [deleted file]
src/net/sf/openrocket/optimization/ParallelFunctionCache.java [deleted file]
src/net/sf/openrocket/optimization/Point.java [deleted file]
src/net/sf/openrocket/optimization/SearchPattern.java [deleted file]
src/net/sf/openrocket/optimization/general/Function.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/FunctionCache.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/FunctionOptimizer.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/OptimizationController.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/Point.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationParameter.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/BodyComponent.java
src/net/sf/openrocket/rocketcomponent/BodyTube.java
src/net/sf/openrocket/rocketcomponent/Bulkhead.java
src/net/sf/openrocket/rocketcomponent/CenteringRing.java
src/net/sf/openrocket/rocketcomponent/Configuration.java
src/net/sf/openrocket/rocketcomponent/EngineBlock.java
src/net/sf/openrocket/rocketcomponent/FinSet.java
src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java
src/net/sf/openrocket/rocketcomponent/InnerTube.java
src/net/sf/openrocket/rocketcomponent/LaunchLug.java
src/net/sf/openrocket/rocketcomponent/MassComponent.java
src/net/sf/openrocket/rocketcomponent/Parachute.java
src/net/sf/openrocket/rocketcomponent/Rocket.java
src/net/sf/openrocket/rocketcomponent/RocketComponent.java
src/net/sf/openrocket/rocketcomponent/ShockCord.java
src/net/sf/openrocket/rocketcomponent/Sleeve.java
src/net/sf/openrocket/rocketcomponent/Stage.java
src/net/sf/openrocket/rocketcomponent/Streamer.java
src/net/sf/openrocket/rocketcomponent/Transition.java
src/net/sf/openrocket/rocketcomponent/TubeCoupler.java
src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java
src/net/sf/openrocket/simulation/FlightData.java
src/net/sf/openrocket/simulation/RK4SimulationStepper.java
src/net/sf/openrocket/simulation/SimulationStatus.java
src/net/sf/openrocket/util/BugException.java
src/net/sf/openrocket/util/GUIUtil.java
src/net/sf/openrocket/util/Icons.java
src/net/sf/openrocket/util/NumericComparator.java [new file with mode: 0644]
src/net/sf/openrocket/util/Reflection.java
src/net/sf/openrocket/utils/TestFunctionOptimizer.java
src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java
test/net/sf/openrocket/Estes_A8.rse [new file with mode: 0644]
test/net/sf/openrocket/IntegrationTest.java [new file with mode: 0644]
test/net/sf/openrocket/optimization/TestSearchPattern.java
test/net/sf/openrocket/rocketcomponent/ComponentCompare.java
test/net/sf/openrocket/rocketcomponent/FinSetTest.java
test/net/sf/openrocket/rocketcomponent/RocketTest.java
test/net/sf/openrocket/simplerocket.ork [new file with mode: 0644]
web/html/actions/updates.php
web/html/techdoc.pdf [new file with mode: 0644]

index a8ea7facf0209955599df64a4017adc2c87416d5..fa9cde2e40284fbdfee037083524517bdd7ed0eb 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,16 @@
+2010-10-03  Sampo Niskanen
+
+       * Added VBOSE logging level
+
+2010-10-02  Sampo Niskanen
+
+       * [BUG] Exception when undoing changes
+
+2010-09-27  Sampo Niskanen
+
+       * Implemented DnD for component tree
+       * Documented undo/redo functionality
+
 2010-09-07  Sampo Niskanen
 
        * Released version 1.1.2
index 90a824ccf3fb1f722d1179fea6e6a93215a5bf3e..2377e85ccc843cef1dade7ff5646a4380538c5bd 100644 (file)
Binary files a/doc/techdoc/techdoc.pdf and b/doc/techdoc/techdoc.pdf differ
diff --git a/doc/undo-redo-flow.uxf b/doc/undo-redo-flow.uxf
new file mode 100644 (file)
index 0000000..4aa4b77
--- /dev/null
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><umlet_diagram><help_text>// Uncomment the following line to change the fontsize:
+// fontsize=14
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////
+// Welcome to UMLet!
+//
+// Double-click on UML elements to add them to the diagram, or to copy them
+// Edit elements by modifying the text in this panel
+// Hold Ctrl to select multiple elements
+// Use Ctrl+mouse to select via lasso
+//
+// Use Â± or Ctrl+mouse wheel to zoom
+// Drag a whole relation at its central square icon
+//
+// Press Ctrl+C to copy the whole diagram to the system clipboard (then just paste it to, eg, Word)
+// Edit the files in the "palettes" directory to create your own element palettes
+//
+// Select "Custom Elements &gt; New..." to create new element types
+//////////////////////////////////////////////////////////////////////////////////////////////
+
+
+// This text will be stored with each diagram;  use it for notes.</help_text><zoom_level>10</zoom_level><element><type>com.umlet.element.custom.State</type><coordinates><x>670</x><y>540</y><w>280</w><h>140</h></coordinates><panel_attributes>Clean, at end of history
+--
+undoHistory.size = n
+undoPosition = n-1
+clean
+-.
+redo() not available</panel_attributes><additional_attributes/></element><element><type>com.umlet.element.custom.InitialState</type><coordinates><x>340</x><y>140</y><w>20</w><h>20</h></coordinates><panel_attributes/><additional_attributes/></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>320</x><y>120</y><w>50</w><h>160</h></coordinates><panel_attributes>lt=&lt;-</panel_attributes><additional_attributes>30;140;30;30</additional_attributes></element><element><type>com.umlet.element.custom.State</type><coordinates><x>200</x><y>540</y><w>280</w><h>140</h></coordinates><panel_attributes>Dirty, at end of history
+--
+undoHistory.size = n
+undoPosition = n-1
+dirty
+-.
+redo() not available</panel_attributes><additional_attributes/></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>230</x><y>370</y><w>214</w><h>190</h></coordinates><panel_attributes>lt=&lt;-
+Modify rocket:
+nextDescription stored
+in undoDescription</panel_attributes><additional_attributes>110;170;110;30</additional_attributes></element><element><type>com.umlet.element.custom.State</type><coordinates><x>200</x><y>260</y><w>280</w><h>140</h></coordinates><panel_attributes>Clean, no history
+--
+undoHistory.size = 1
+undoPosition = 0
+clean
+-.
+undo(), redo() not available</panel_attributes><additional_attributes/></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>60</x><y>350</y><w>210</w><h>124</h></coordinates><panel_attributes>lt=&lt;-
+addUndoPosition:
+nextDescription set</panel_attributes><additional_attributes>140;30;100;30;100;80;190;80;190;50</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>450</x><y>560</y><w>240</w><h>146</h></coordinates><panel_attributes>lt=&lt;-
+addUndoPosition():
+rocket copied to
+end of history,
+nextDescription set,
+undoPosition increased</panel_attributes><additional_attributes>220;50;30;50</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>310</x><y>410</y><w>510</w><h>150</h></coordinates><panel_attributes>lt=&lt;-
+</panel_attributes><additional_attributes>30;130;30;30;490;30;490;130</additional_attributes></element><element><type>com.umlet.element.custom.State</type><coordinates><x>190</x><y>850</y><w>280</w><h>140</h></coordinates><panel_attributes>Clean, at midpoint of history
+--
+undoHistory.size = n
+undoPosition = m &lt; n-1
+clean
+-.
+</panel_attributes><additional_attributes/></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>190</x><y>650</y><w>282</w><h>220</h></coordinates><panel_attributes>lt=&lt;-
+undo():
+rocket copied to end of history,
+rocket loaded from undoPosition
+</panel_attributes><additional_attributes>150;200;150;30</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>0</x><y>590</y><w>238</w><h>350</h></coordinates><panel_attributes>lt=&lt;-
+Modify rocket:
+redo information removed,
+nextDescription stored
+in undoDescription</panel_attributes><additional_attributes>200;30;120;30;120;330;190;330</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>0</x><y>940</y><w>286</w><h>190</h></coordinates><panel_attributes>lt=&lt;-
+undo():
+undoPosition decreased,
+Rocket loaded from undoPosition
+-
+addUndoPosition():
+nextDescription set</panel_attributes><additional_attributes>190;30;150;30;150;80;240;80;240;50</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>390</x><y>910</y><w>282</w><h>178</h></coordinates><panel_attributes>lt=&lt;-
+redo():
+undoPosition increased,
+rocket loaded from undoPosition</panel_attributes><additional_attributes>30;80;30;120;150;120;150;30;80;30</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>510</x><y>650</y><w>412</w><h>348</h></coordinates><panel_attributes>lt=&lt;-
+redo():
+undoPosition increased to n-1,
+rocket loaded from undoPosition</panel_attributes><additional_attributes>280;30;280;290;30;290</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>440</x><y>650</y><w>422</w><h>288</h></coordinates><panel_attributes>lt=&lt;-
+undo():
+undoPosition decreased,
+rocket loaded from undoPosition</panel_attributes><additional_attributes>30;230;290;230;290;30</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>70</x><y>450</y><w>200</w><h>160</h></coordinates><panel_attributes>lt=&lt;-
+Modify rocket
+</panel_attributes><additional_attributes>180;90;180;50;80;50;80;140;130;140</additional_attributes></element><element><type>com.umlet.element.base.Relation</type><coordinates><x>860</x><y>460</y><w>216</w><h>160</h></coordinates><panel_attributes>lt=&lt;-
+addUndoPosition()</panel_attributes><additional_attributes>30;80;30;50;130;50;130;140;90;140</additional_attributes></element><element><type>com.umlet.element.custom.State</type><coordinates><x>540</x><y>50</y><w>400</w><h>350</h></coordinates><panel_attributes>Variable description
+--
+undoHistory: a list of the undo positions.  Each position
+is a full copy of the rocket structure. Always contains at 
+least one entry.
+-.
+undoPosition: current location within the undoHistory.
+If at the last entry, the rocket can be "clean" (in the
+state of undoHistory.get(undoPosition)) or "dirty" (in 
+a modified state). 
+-.
+undoDescription:  the value at undoDescription.get(
+undoPosition) describes the action that would be
+undone if undo() was called at that moment.  The value
+at undoDescription.get(undoPosition+1) describes the
+action that would be redone if redo() were be called.
+-.
+nextDescription: the description of the actions that
+will be done next (or are already being done).
+-.
+storedDescription: temporary storage for
+nextDescription during a startUndo() ... stopUndo()
+action.
+fg=blue</panel_attributes><additional_attributes/></element></umlet_diagram>
\ No newline at end of file
index 99df61f14fa5dadb62894bbbf76b0ecdac6f46b2..64e814559f58c812801107c11b8c7ee4419c30dc 100644 (file)
@@ -413,7 +413,7 @@ public class FlightConditions implements Cloneable, ChangeSource, Monitorable {
                        cond.atmosphericConditions = atmosphericConditions.clone();
                        return cond;
                } catch (CloneNotSupportedException e) {
-                       throw new BugException("BUG: clone not supported!", e);
+                       throw new BugException("clone not supported!", e);
                }
        }
        
index 446b97ea455639dbd2d7e7651dbc0b2bb2d324b6..4f74bf1ecbe1d2600f7d2f59c830334c06269a6f 100644 (file)
@@ -203,9 +203,6 @@ public class SymmetricComponentCalc extends RocketComponentCalc {
                }
                
 
-               assert (r1 < r2); // Tube and boattail have been checked already
-               
-
                // All nose cones and shoulders from pre-calculated and interpolating 
                if (interpolator == null) {
                        calculateNoseInterpolator();
@@ -355,8 +352,9 @@ public class SymmetricComponentCalc extends RocketComponentCalc {
                        throw new UnsupportedOperationException("Unknown transition shape: " + shape);
                }
                
-               assert (p >= 0);
-               assert (p <= 1.001);
+               if (p < 0 || p > 1.00001) {
+                       throw new BugException("Inconsistent parameter value p=" + p + " shape=" + shape);
+               }
                
 
                // Check for parameterized shape and interpolate if necessary
index 772bc90c6097396317946d90e7e61ae224446f8d..6cd4e11393d0fcb68b3df87d125ac728a39dcb69 100644 (file)
@@ -7,6 +7,7 @@ import java.io.InputStreamReader;
 import java.io.Reader;
 import java.net.HttpURLConnection;
 import java.util.ArrayList;
+import java.util.Locale;
 
 import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.startup.Application;
@@ -157,6 +158,8 @@ public class UpdateInfoRetriever {
                        connection.setRequestProperty("X-OpenRocket-Country",
                                        Communicator.encode(System.getProperty("user.country") + " " +
                                                        System.getProperty("user.timezone")));
+                       connection.setRequestProperty("X-OpenRocket-Locale",
+                                       Communicator.encode(Locale.getDefault().toString()));
                        connection.setRequestProperty("X-OpenRocket-CPUs", "" + Runtime.getRuntime().availableProcessors());
                        
                        InputStream is = null;
index 83c4b0b7ebeba0c15f5860bafde4c75a161e07a8..02a91e82053d8d2f62a29013a750624434fc84f0 100644 (file)
@@ -1,5 +1,4 @@
 package net.sf.openrocket.document;
-//TODO: LOW: move class somewhere else?
 
 import java.awt.event.ActionEvent;
 import java.io.File;
@@ -13,15 +12,26 @@ import javax.swing.Action;
 import net.sf.openrocket.document.events.DocumentChangeEvent;
 import net.sf.openrocket.document.events.DocumentChangeListener;
 import net.sf.openrocket.document.events.SimulationChangeEvent;
+import net.sf.openrocket.gui.main.ExceptionHandler;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.logging.TraceException;
 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
 import net.sf.openrocket.rocketcomponent.Configuration;
 import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.Icons;
 
-
+/**
+ * Class describing an entire OpenRocket document, including a rocket and
+ * simulations.  This class also handles undo/redo operations for the rocket structure.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
 public class OpenRocketDocument implements ComponentChangeListener {
+       private static final LogHelper log = Application.getLogger();
+       
        /**
         * The minimum number of undo levels that are stored.
         */
@@ -31,43 +41,62 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * UNDO_LEVELS by this amount the undo is purged to that length.
         */
        public static final int UNDO_MARGIN = 10;
-
        
        public static final String SIMULATION_NAME_PREFIX = "Simulation ";
        
+       /** Whether an undo error has already been reported to the user */
+       private static boolean undoErrorReported = false;
+       
        private final Rocket rocket;
        private final Configuration configuration;
-
-       private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
        
+       private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
        
-       private int undoPosition = -1;  // Illegal position, init in constructor
+
+       /*
+        * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
+        */
+
+       /** 
+        * The undo history of the rocket.   Whenever a new undo position is created while the
+        * rocket is in "dirty" state, the rocket is copied here.
+        */
        private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
        private LinkedList<String> undoDescription = new LinkedList<String>();
        
+       /**
+        * The position in the undoHistory we are currently at.  If modifications have been
+        * made to the rocket, the rocket is in "dirty" state and this points to the previous
+        * "clean" state.
+        */
+       private int undoPosition = -1; // Illegal position, init in constructor
+       
+       /**
+        * The description of the next action that modifies this rocket.
+        */
        private String nextDescription = null;
        private String storedDescription = null;
        
-       
+
        private File file = null;
        private int savedID = -1;
        
        private final StorageOptions storageOptions = new StorageOptions();
        
-       
-       private final List<DocumentChangeListener> listeners = 
-               new ArrayList<DocumentChangeListener>();
+
+       private final List<DocumentChangeListener> listeners =
+                       new ArrayList<DocumentChangeListener>();
        
        /* These must be initialized after undo history is set up. */
        private final UndoRedoAction undoAction;
        private final UndoRedoAction redoAction;
-               
+       
        
        public OpenRocketDocument(Rocket rocket) {
                this(rocket.getDefaultConfiguration());
        }
        
-
+       
        private OpenRocketDocument(Configuration configuration) {
                this.configuration = configuration;
                this.rocket = configuration.getRocket();
@@ -81,31 +110,31 @@ public class OpenRocketDocument implements ComponentChangeListener {
        }
        
        
-       
-       
+
+
        public Rocket getRocket() {
                return rocket;
        }
-
+       
        
        public Configuration getDefaultConfiguration() {
                return configuration;
        }
-
-
+       
+       
        public File getFile() {
                return file;
        }
-
+       
        public void setFile(File file) {
                this.file = file;
        }
        
-
+       
        public boolean isSaved() {
                return rocket.getModID() == savedID;
        }
-
+       
        public void setSaved(boolean saved) {
                if (saved == false)
                        this.savedID = -1;
@@ -123,34 +152,41 @@ public class OpenRocketDocument implements ComponentChangeListener {
        }
        
        
-       
-       
-       
+
+
+
        @SuppressWarnings("unchecked")
        public List<Simulation> getSimulations() {
-               return (ArrayList<Simulation>)simulations.clone();
+               return (ArrayList<Simulation>) simulations.clone();
        }
+       
        public int getSimulationCount() {
                return simulations.size();
        }
+       
        public Simulation getSimulation(int n) {
                return simulations.get(n);
        }
+       
        public int getSimulationIndex(Simulation simulation) {
                return simulations.indexOf(simulation);
        }
+       
        public void addSimulation(Simulation simulation) {
                simulations.add(simulation);
                fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
        }
+       
        public void addSimulation(Simulation simulation, int n) {
                simulations.add(n, simulation);
                fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
        }
+       
        public void removeSimulation(Simulation simulation) {
                simulations.remove(simulation);
                fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
        }
+       
        public Simulation removeSimulation(int n) {
                Simulation simulation = simulations.remove(n);
                fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
@@ -167,16 +203,17 @@ public class OpenRocketDocument implements ComponentChangeListener {
        public String getNextSimulationName() {
                // Generate unique name for the simulation
                int maxValue = 0;
-               for (Simulation s: simulations) {
+               for (Simulation s : simulations) {
                        String name = s.getName();
                        if (name.startsWith(SIMULATION_NAME_PREFIX)) {
                                try {
-                                       maxValue = Math.max(maxValue, 
+                                       maxValue = Math.max(maxValue,
                                                        Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
-                               } catch (NumberFormatException ignore) { }
+                               } catch (NumberFormatException ignore) {
+                               }
                        }
                }
-               return SIMULATION_NAME_PREFIX + (maxValue+1);
+               return SIMULATION_NAME_PREFIX + (maxValue + 1);
        }
        
        
@@ -196,43 +233,52 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * @param description A short description of the following actions.
         */
        public void addUndoPosition(String description) {
-
+               
+               if (storedDescription != null) {
+                       logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
+                                       " description=" + description);
+               }
+               
                // Check whether modifications have been done since last call
                if (isCleanState()) {
                        // No modifications
+                       log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
                        nextDescription = description;
                        return;
                }
-
+               
+               log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
                
                /*
                 * Modifications have been made to the rocket.  We should be at the end of the
-                * undo history, but check for consistency.
+                * undo history, but check for consistency and try to recover.
                 */
-               assert(undoPosition == undoHistory.size()-1): "Undo inconsistency, report bug!";
-               while (undoPosition < undoHistory.size()-1) {
+               if (undoPosition != undoHistory.size() - 1) {
+                       logUndoError("undo position inconsistency");
+               }
+               while (undoPosition < undoHistory.size() - 1) {
                        undoHistory.removeLast();
                        undoDescription.removeLast();
                }
                
-               
+
                // Add the current state to the undo history
-               undoHistory.add(rocket.copy());
-               undoDescription.add(description);
+               undoHistory.add(rocket.copyWithOriginalID());
+               undoDescription.add(null);
                nextDescription = description;
                undoPosition++;
                
-               
+
                // Maintain maximum undo size
-               if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN) {
-                       for (int i=0; i < UNDO_MARGIN+1; i++) {
+               if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) {
+                       for (int i = 0; i < UNDO_MARGIN; i++) {
                                undoHistory.removeFirst();
                                undoDescription.removeFirst();
                                undoPosition--;
                        }
                }
        }
-
+       
        
        /**
         * Start a time-limited undoable operation.  After the operation {@link #stopUndo()}
@@ -244,8 +290,14 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * @param description   Description of the following undoable operations.
         */
        public void startUndo(String description) {
-               storedDescription = nextDescription;
+               if (storedDescription != null) {
+                       logUndoError("startUndo called while storedDescription=" + storedDescription +
+                                       " description=" + description);
+               }
+               log.info("Starting time-limited undoable operation '" + description + "' for " + this);
+               String store = nextDescription;
                addUndoPosition(description);
+               storedDescription = store;
        }
        
        /**
@@ -254,8 +306,11 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * performed.
         */
        public void stopUndo() {
-               addUndoPosition(storedDescription);
+               log.info("Ending time-limited undoable operation for " + this + " nextDescription=" +
+                               nextDescription + "     storedDescription=" + storedDescription);
+               String stored = storedDescription;
                storedDescription = null;
+               addUndoPosition(stored);
        }
        
        
@@ -273,10 +328,11 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * Clear the undo history.
         */
        public void clearUndo() {
+               log.info("Clearing undo history of " + this);
                undoHistory.clear();
                undoDescription.clear();
                
-               undoHistory.add(rocket.copy());
+               undoHistory.add(rocket.copyWithOriginalID());
                undoDescription.add(null);
                undoPosition = 0;
                
@@ -291,8 +347,13 @@ public class OpenRocketDocument implements ComponentChangeListener {
        public void componentChanged(ComponentChangeEvent e) {
                
                if (!e.isUndoChange()) {
+                       if (undoPosition < undoHistory.size() - 1) {
+                               log.info("Rocket changed while in undo history, removing redo information for " + this +
+                                               " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
+                                               " isClean=" + isCleanState());
+                       }
                        // Remove any redo information if available
-                       while (undoPosition < undoHistory.size()-1) {
+                       while (undoPosition < undoHistory.size() - 1) {
                                undoHistory.removeLast();
                                undoDescription.removeLast();
                        }
@@ -304,8 +365,12 @@ public class OpenRocketDocument implements ComponentChangeListener {
                undoAction.setAllValues();
                redoAction.setAllValues();
        }
-
        
+       
+       /**
+        * Return whether undo action is available.
+        * @return      <code>true</code> if undo can be performed
+        */
        public boolean isUndoAvailable() {
                if (undoPosition > 0)
                        return true;
@@ -313,22 +378,34 @@ public class OpenRocketDocument implements ComponentChangeListener {
                return !isCleanState();
        }
        
+       /**
+        * Return the description of what action would be undone if undo is called.
+        * @return      the description what would be undone, or <code>null</code> if description unavailable.
+        */
        public String getUndoDescription() {
                if (!isUndoAvailable())
                        return null;
                
                if (isCleanState()) {
-                       return undoDescription.get(undoPosition-1);
+                       return undoDescription.get(undoPosition - 1);
                } else {
                        return undoDescription.get(undoPosition);
                }
        }
-
        
+       
+       /**
+        * Return whether redo action is available.
+        * @return      <code>true</code> if redo can be performed
+        */
        public boolean isRedoAvailable() {
-               return undoPosition < undoHistory.size()-1;
+               return undoPosition < undoHistory.size() - 1;
        }
        
+       /**
+        * Return the description of what action would be redone if redo is called.
+        * @return      the description of what would be redone, or <code>null</code> if description unavailable.
+        */
        public String getRedoDescription() {
                if (!isRedoAvailable())
                        return null;
@@ -337,35 +414,59 @@ public class OpenRocketDocument implements ComponentChangeListener {
        }
        
        
-       
+       /**
+        * Perform undo operation on the rocket.
+        */
        public void undo() {
+               log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
+                               " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
                if (!isUndoAvailable()) {
-                       throw new IllegalStateException("Undo not available.");
+                       logUndoError("Undo not available");
+                       undoAction.setAllValues();
+                       redoAction.setAllValues();
+                       return;
                }
-
+               if (storedDescription != null) {
+                       logUndoError("undo() called with storedDescription=" + storedDescription);
+               }
+               
                // Update history position
                
                if (isCleanState()) {
                        // We are in a clean state, simply move backwards in history
                        undoPosition--;
                } else {
+                       if (undoPosition != undoHistory.size() - 1) {
+                               logUndoError("undo position inconsistency");
+                       }
                        // Modifications have been made, save the state and restore previous state
-                       undoHistory.add(rocket.copy());
+                       undoHistory.add(rocket.copyWithOriginalID());
                        undoDescription.add(null);
                }
                
-               rocket.loadFrom(undoHistory.get(undoPosition).copy());
+               rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
        }
        
        
+       /**
+        * Perform redo operation on the rocket.
+        */
        public void redo() {
+               log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
+                               " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
                if (!isRedoAvailable()) {
-                       throw new IllegalStateException("Redo not available.");
+                       logUndoError("Redo not available");
+                       undoAction.setAllValues();
+                       redoAction.setAllValues();
+                       return;
+               }
+               if (storedDescription != null) {
+                       logUndoError("redo() called with storedDescription=" + storedDescription);
                }
                
                undoPosition++;
                
-               rocket.loadFrom(undoHistory.get(undoPosition).copy());
+               rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
        }
        
        
@@ -374,7 +475,21 @@ public class OpenRocketDocument implements ComponentChangeListener {
        }
        
        
-       
+       /**
+        * Log a non-fatal undo/redo error or inconsistency.  Reports it to the user the first 
+        * time it occurs, but not on subsequent times.  Logs automatically the undo system state.
+        */
+       private void logUndoError(String error) {
+               log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
+                               " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
+                               " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
+                               new TraceException());
+               
+               if (!undoErrorReported) {
+                       undoErrorReported = true;
+                       ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error);
+               }
+       }
        
        ///////  Listeners
        
@@ -388,14 +503,14 @@ public class OpenRocketDocument implements ComponentChangeListener {
        
        protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
                DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
-               for (DocumentChangeListener l: array) {
+               for (DocumentChangeListener l : array) {
                        l.documentChanged(event);
                }
        }
        
        
-       
-       
+
+
        /**
         * Inner class to implement undo/redo actions.
         */
@@ -408,56 +523,58 @@ public class OpenRocketDocument implements ComponentChangeListener {
                // Sole constructor
                public UndoRedoAction(int type) {
                        if (type != UNDO && type != REDO) {
-                               throw new IllegalArgumentException("Unknown type = "+type);
+                               throw new IllegalArgumentException("Unknown type = " + type);
                        }
                        this.type = type;
                        setAllValues();
                }
-
+               
                
                // Actual action to make
                public void actionPerformed(ActionEvent e) {
                        switch (type) {
                        case UNDO:
+                               log.user("Performing undo, event=" + e);
                                undo();
                                break;
-                               
+                       
                        case REDO:
+                               log.user("Performing redo, event=" + e);
                                redo();
                                break;
                        }
                }
-
+               
                
                // Set all the values correctly (name and enabled/disabled status)
                public void setAllValues() {
-                       String name,desc;
-                       boolean enabled;
+                       String name, desc;
+                       boolean actionEnabled;
                        
                        switch (type) {
                        case UNDO:
                                name = "Undo";
                                desc = getUndoDescription();
-                               enabled = isUndoAvailable();
+                               actionEnabled = isUndoAvailable();
                                this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
                                break;
-                               
+                       
                        case REDO:
                                name = "Redo";
                                desc = getRedoDescription();
-                               enabled = isRedoAvailable();
+                               actionEnabled = isRedoAvailable();
                                this.putValue(SMALL_ICON, Icons.EDIT_REDO);
                                break;
-                               
+                       
                        default:
-                               throw new BugException("illegal type="+type);
+                               throw new BugException("illegal type=" + type);
                        }
                        
                        if (desc != null)
-                               name = name + " ("+desc+")";
+                               name = name + " (" + desc + ")";
                        
                        putValue(NAME, name);
-                       setEnabled(enabled);
+                       setEnabled(actionEnabled);
                }
        }
 }
index 3d0556b12f6b2c3a9b67f4631fec8dc6286ab99e..d7afa7c8ba3a33dd257f3d8a221945f533167ae1 100644 (file)
@@ -137,8 +137,16 @@ public class Simulation implements ChangeSource, Cloneable {
        }
        
        
-
-
+       /**
+        * Return the rocket associated with this simulation.
+        * 
+        * @return      the rocket.
+        */
+       public Rocket getRocket() {
+               return rocket;
+       }
+       
+       
        /**
         * Return a newly created Configuration for this simulation.  The configuration
         * has the motor ID set and all stages active.
index 113bc690eb74f48e316a36eda28aaa7ad69ac6dc..4fc8dada7cbd12e37c1ce6c3946e37336778e4a3 100644 (file)
@@ -13,7 +13,7 @@ import net.sf.openrocket.document.OpenRocketDocument;
 
 public abstract class RocketLoader {
        protected final WarningSet warnings = new WarningSet();
-
+       
        
        /**
         * Loads a rocket from the specified File object.
@@ -21,7 +21,7 @@ public abstract class RocketLoader {
        public final OpenRocketDocument load(File source) throws RocketLoadException {
                warnings.clear();
                InputStream stream = null;
-
+               
                try {
                        
                        stream = new BufferedInputStream(new FileInputStream(source));
@@ -39,24 +39,24 @@ public abstract class RocketLoader {
                        }
                }
        }
-
+       
        /**
         * Loads a rocket from the specified InputStream.
         */
        public final OpenRocketDocument load(InputStream source) throws RocketLoadException {
                warnings.clear();
-
+               
                try {
                        return loadFromStream(source);
                } catch (RocketLoadException e) {
                        throw e;
                } catch (IOException e) {
-                       throw new RocketLoadException("I/O error: " + e.getMessage());
+                       throw new RocketLoadException("I/O error: " + e.getMessage(), e);
                }
        }
-
        
        
+
        /**
         * This method is called by the default implementations of {@link #load(File)} 
         * and {@link #load(InputStream)} to load the rocket.
@@ -65,8 +65,8 @@ public abstract class RocketLoader {
         */
        protected abstract OpenRocketDocument loadFromStream(InputStream source) throws IOException,
                        RocketLoadException;
-
-
+       
+       
 
        public final WarningSet getWarnings() {
                return warnings;
index 709534154c205b849fb34f904c8547569a4025c3..a2258aa3154036b874bf36877d565a4022a29481 100644 (file)
@@ -3,13 +3,13 @@
  */
 package net.sf.openrocket.file.rocksim;
 
-import net.sf.openrocket.file.RocketLoader;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.file.RocketLoadException;
+import net.sf.openrocket.file.RocketLoader;
 import net.sf.openrocket.file.simplesax.SimpleSAX;
-import net.sf.openrocket.document.OpenRocketDocument;
-
-import java.io.InputStream;
-import java.io.IOException;
 
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
@@ -30,28 +30,29 @@ import org.xml.sax.SAXException;
  *          getMaterial
  */
 public class RocksimLoader extends RocketLoader {
-    /**
-     * This method is called by the default implementations of {@link #load(java.io.File)}
-     * and {@link #load(java.io.InputStream)} to load the rocket.
-     *
-     * @throws net.sf.openrocket.file.RocketLoadException
-     *          if an error occurs during loading.
-     */
-    @Override 
-    protected OpenRocketDocument loadFromStream(InputStream source) throws IOException, RocketLoadException {
-
-        InputSource xmlSource = new InputSource(source);
-
-        RocksimHandler handler = new RocksimHandler();
+       /**
+        * This method is called by the default implementations of {@link #load(java.io.File)}
+        * and {@link #load(java.io.InputStream)} to load the rocket.
+        *
+        * @throws net.sf.openrocket.file.RocketLoadException
+        *          if an error occurs during loading.
+        */
+       @Override
+       protected OpenRocketDocument loadFromStream(InputStream source) throws IOException, RocketLoadException {
                
-        try {
-            SimpleSAX.readXML(xmlSource, handler, warnings);
-        } catch (SAXException e) {
-            throw new RocketLoadException("Malformed XML in input.", e);
-        }
-
-        final OpenRocketDocument document = handler.getDocument();
-        document.setFile(null);
-        return document;
-    }
+               InputSource xmlSource = new InputSource(source);
+               
+               RocksimHandler handler = new RocksimHandler();
+               
+               try {
+                       SimpleSAX.readXML(xmlSource, handler, warnings);
+               } catch (SAXException e) {
+                       throw new RocketLoadException("Malformed XML in input.", e);
+               }
+               
+               final OpenRocketDocument document = handler.getDocument();
+               document.setFile(null);
+               document.clearUndo();
+               return document;
+       }
 }
index 733413674887cd954c635a682b1d29999466f82f..87997c484736884ac4a2a1a76b32b26b3660bf80 100644 (file)
@@ -14,7 +14,7 @@ public abstract class Column {
        public Column(String name) {
                this.name = name;
        }
-
+       
        /**
         * Return the caption of the column.
         */
@@ -55,7 +55,7 @@ public abstract class Column {
        public Class<?> getColumnClass() {
                return Object.class;
        }
-
+       
        /**
         * Return the value in this column at the specified row.
         * 
@@ -63,4 +63,5 @@ public abstract class Column {
         * @return              the value at the specified position.
         */
        public abstract Object getValueAt(int row);
+       
 }
index 53011f3750de58f82b89def4c9947193a43a789a..2e010f6e2f52cadc4a25e5bea10e827b78bd13d2 100644 (file)
@@ -14,7 +14,7 @@ public abstract class ColumnTableModel extends AbstractTableModel {
        }
        
        public void setColumnWidths(TableColumnModel model) {
-               for (int i=0; i < columns.length; i++) {
+               for (int i = 0; i < columns.length; i++) {
                        if (columns[i].getExactWidth() > 0) {
                                TableColumn col = model.getColumn(i);
                                int w = columns[i].getExactWidth();
@@ -27,7 +27,7 @@ public abstract class ColumnTableModel extends AbstractTableModel {
                        }
                }
        }
-
+       
        @Override
        public int getColumnCount() {
                return columns.length;
@@ -42,16 +42,15 @@ public abstract class ColumnTableModel extends AbstractTableModel {
        public Class<?> getColumnClass(int col) {
                return columns[col].getColumnClass();
        }
-
+       
        @Override
        public Object getValueAt(int row, int col) {
                if ((row < 0) || (row >= getRowCount()) ||
                                (col < 0) || (col >= columns.length)) {
-                       ExceptionHandler.handleErrorCondition("Error:  Requested illegal column/row, " +
-                                       "col="+col+" row="+row);
-                       assert(false);
+                       ExceptionHandler.handleErrorCondition("Error:  Requested illegal column/row, col=" + col + " row=" + row);
                        return null;
                }
                return columns[col].getValueAt(row);
        }
+       
 }
index 2f1946a44524fea97a9d3340bfa46109c3461948..20b2ad94349743aeae7c06b8e46347a22619350f 100644 (file)
@@ -591,9 +591,9 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                try {
                        return (Double)getMethod.invoke(source)*multiplier;
                } catch (IllegalArgumentException e) {
-                       throw new BugException("BUG: Unable to invoke getMethod of "+this, e);
+                       throw new BugException("Unable to invoke getMethod of "+this, e);
                } catch (IllegalAccessException e) {
-                       throw new BugException("BUG: Unable to invoke getMethod of "+this, e);
+                       throw new BugException("Unable to invoke getMethod of "+this, e);
                } catch (InvocationTargetException e) {
                        throw Reflection.handleWrappedException(e);
                }
@@ -617,9 +617,9 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                try {
                        setMethod.invoke(source, v/multiplier);
                } catch (IllegalArgumentException e) {
-                       throw new BugException("BUG: Unable to invoke setMethod of "+this, e);
+                       throw new BugException("Unable to invoke setMethod of "+this, e);
                } catch (IllegalAccessException e) {
-                       throw new BugException("BUG: Unable to invoke setMethod of "+this, e);
+                       throw new BugException("Unable to invoke setMethod of "+this, e);
                } catch (InvocationTargetException e) {
                        throw Reflection.handleWrappedException(e);
                }
index 52747d1b81334049305253778b44ef713e43e90e..b70cd4b106f1e5f6fe76714639cf58318ed90db0 100644 (file)
@@ -63,7 +63,7 @@ public class URLLabel extends SelectableLabel {
                                        try {
                                                d.browse(new URI(url));
                                        } catch (URISyntaxException e1) {
-                                               throw new BugException("BUG: Illegal URL: " + url, e1);
+                                               throw new BugException("Illegal URL: " + url, e1);
                                        } catch (IOException e1) {
                                                log.error("Unable to launch browser: " + e1.getMessage(), e1);
                                        }
diff --git a/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java b/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java
new file mode 100644 (file)
index 0000000..c67568f
--- /dev/null
@@ -0,0 +1,518 @@
+package net.sf.openrocket.gui.dialogs;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+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.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTable;
+import javax.swing.JTextArea;
+import javax.swing.ListSelectionModel;
+import javax.swing.RowFilter;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableModel;
+import javax.swing.table.TableRowSorter;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.adaptors.Column;
+import net.sf.openrocket.gui.adaptors.ColumnTableModel;
+import net.sf.openrocket.gui.components.SelectableLabel;
+import net.sf.openrocket.logging.DelegatorLogger;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.logging.LogLevel;
+import net.sf.openrocket.logging.LogLevelBufferLogger;
+import net.sf.openrocket.logging.LogLine;
+import net.sf.openrocket.logging.StackTraceWriter;
+import net.sf.openrocket.logging.TraceException;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.NumericComparator;
+
+public class DebugLogDialog extends JDialog {
+       private static final LogHelper log = Application.getLogger();
+       
+       private static final int POLL_TIME = 250;
+       private static final String STACK_TRACE_MARK = "\uFF01";
+       
+       private static final EnumMap<LogLevel, Color> backgroundColors = new EnumMap<LogLevel, Color>(LogLevel.class);
+       static {
+               for (LogLevel l : LogLevel.values()) {
+                       // Just to ensure every level has a bg color
+                       backgroundColors.put(l, Color.ORANGE);
+               }
+               final int hi = 255;
+               final int lo = 150;
+               backgroundColors.put(LogLevel.ERROR, new Color(hi, lo, lo));
+               backgroundColors.put(LogLevel.WARN, new Color(hi, (hi + lo) / 2, lo));
+               backgroundColors.put(LogLevel.USER, new Color(lo, lo, hi));
+               backgroundColors.put(LogLevel.INFO, new Color(hi, hi, lo));
+               backgroundColors.put(LogLevel.DEBUG, new Color(lo, hi, lo));
+               backgroundColors.put(LogLevel.VBOSE, new Color(lo, hi, (hi + lo) / 2));
+       }
+       
+       /** Buffer containing the log lines displayed */
+       private final List<LogLine> buffer = new ArrayList<LogLine>();
+       
+       /** Queue of log lines to be added to the displayed buffer */
+       private final Queue<LogLine> queue = new ConcurrentLinkedQueue<LogLine>();
+       
+       private final DelegatorLogger delegator;
+       private final LogListener logListener;
+       
+       private final EnumMap<LogLevel, JCheckBox> filterButtons = new EnumMap<LogLevel, JCheckBox>(LogLevel.class);
+       private final JCheckBox followBox;
+       private final Timer timer;
+       
+
+       private final JTable table;
+       private final ColumnTableModel model;
+       private final TableRowSorter<TableModel> sorter;
+       
+       private final SelectableLabel numberLabel;
+       private final SelectableLabel timeLabel;
+       private final SelectableLabel levelLabel;
+       private final SelectableLabel locationLabel;
+       private final SelectableLabel messageLabel;
+       private final JTextArea stackTraceLabel;
+       
+       public DebugLogDialog(Window parent) {
+               super(parent, "OpenRocket debug log");
+               
+               // Start listening to log lines
+               LogHelper applicationLog = Application.getLogger();
+               if (applicationLog instanceof DelegatorLogger) {
+                       log.info("Adding log listener");
+                       delegator = (DelegatorLogger) applicationLog;
+                       logListener = new LogListener();
+                       delegator.addLogger(logListener);
+               } else {
+                       log.warn("Application log is not a DelegatorLogger");
+                       delegator = null;
+                       logListener = null;
+               }
+               
+               // Fetch old log lines
+               LogLevelBufferLogger bufferLogger = Application.getLogBuffer();
+               if (bufferLogger != null) {
+                       buffer.addAll(bufferLogger.getLogs());
+               } else {
+                       log.warn("Application does not have a log buffer");
+               }
+               
+
+               // Create the UI
+               JPanel mainPanel = new JPanel(new MigLayout("fill"));
+               this.add(mainPanel);
+               
+
+               JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+               split.setDividerLocation(0.7);
+               mainPanel.add(split, "grow");
+               
+               // Top panel
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               split.add(panel);
+               
+               panel.add(new JLabel("Display log lines:"), "gapright para, split");
+               for (LogLevel l : LogLevel.values()) {
+                       JCheckBox box = new JCheckBox(l.toString());
+                       box.setSelected(true);
+                       box.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       sorter.setRowFilter(new LogFilter());
+                               }
+                       });
+                       panel.add(box, "gapright unrel");
+                       filterButtons.put(l, box);
+               }
+               
+               followBox = new JCheckBox("Follow");
+               followBox.setSelected(true);
+               panel.add(followBox, "skip, gapright para, right");
+               
+               JButton clear = new JButton("Clear");
+               clear.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Clearing log buffer");
+                               buffer.clear();
+                               queue.clear();
+                               model.fireTableDataChanged();
+                       }
+               });
+               panel.add(clear, "right, wrap");
+               
+
+
+               // Create the table model
+               model = new ColumnTableModel(
+
+               new Column("#") {
+                       @Override
+                       public Object getValueAt(int row) {
+                               return buffer.get(row).getLogCount();
+                       }
+                       
+                       @Override
+                       public int getDefaultWidth() {
+                               return 60;
+                       }
+               },
+                               new Column("Time") {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0);
+                                       }
+                                       
+                                       @Override
+                                       public int getDefaultWidth() {
+                                               return 60;
+                                       }
+                               },
+                               new Column("Level") {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               return buffer.get(row).getLevel();
+                                       }
+                                       
+                                       @Override
+                                       public int getDefaultWidth() {
+                                               return 60;
+                                       }
+                               },
+                               new Column("") {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               if (buffer.get(row).getCause() != null) {
+                                                       return STACK_TRACE_MARK;
+                                               } else {
+                                                       return "";
+                                               }
+                                       }
+                                       
+                                       @Override
+                                       public int getExactWidth() {
+                                               return 16;
+                                       }
+                               },
+                               new Column("Location") {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               TraceException e = buffer.get(row).getTrace();
+                                               if (e != null) {
+                                                       return e.getMessage();
+                                               } else {
+                                                       return "";
+                                               }
+                                       }
+                                       
+                                       @Override
+                                       public int getDefaultWidth() {
+                                               return 200;
+                                       }
+                               },
+                               new Column("Message") {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               return buffer.get(row).getMessage();
+                                       }
+                                       
+                                       @Override
+                                       public int getDefaultWidth() {
+                                               return 580;
+                                       }
+                               }
+
+               ) {
+                       @Override
+                       public int getRowCount() {
+                               return buffer.size();
+                       }
+               };
+               
+               table = new JTable(model);
+               table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               table.setSelectionBackground(Color.LIGHT_GRAY);
+               table.setSelectionForeground(Color.BLACK);
+               model.setColumnWidths(table.getColumnModel());
+               table.setDefaultRenderer(Object.class, new Renderer());
+               
+               table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+                       @Override
+                       public void valueChanged(ListSelectionEvent e) {
+                               int row = table.getSelectedRow();
+                               if (row >= 0) {
+                                       row = sorter.convertRowIndexToModel(row);
+                               }
+                               updateSelected(row);
+                       }
+               });
+               
+               sorter = new TableRowSorter<TableModel>(model);
+               sorter.setComparator(0, NumericComparator.INSTANCE);
+               sorter.setComparator(1, NumericComparator.INSTANCE);
+               sorter.setComparator(4, new LocationComparator());
+               table.setRowSorter(sorter);
+               
+
+               panel.add(new JScrollPane(table), "span, grow, width " +
+                               (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) +
+                               "px, height 400px");
+               
+
+               panel = new JPanel(new MigLayout("fill"));
+               split.add(panel);
+               
+               panel.add(new JLabel("Log line number:"), "split, gapright rel");
+               numberLabel = new SelectableLabel();
+               panel.add(numberLabel, "width 70lp, gapright para");
+               
+               panel.add(new JLabel("Time:"), "split, gapright rel");
+               timeLabel = new SelectableLabel();
+               panel.add(timeLabel, "width 70lp, gapright para");
+               
+               panel.add(new JLabel("Level:"), "split, gapright rel");
+               levelLabel = new SelectableLabel();
+               panel.add(levelLabel, "width 70lp, gapright para");
+               
+               panel.add(new JLabel("Location:"), "split, gapright rel");
+               locationLabel = new SelectableLabel();
+               panel.add(locationLabel, "growx, wrap unrel");
+               
+               panel.add(new JLabel("Log message:"), "split, gapright rel");
+               messageLabel = new SelectableLabel();
+               panel.add(messageLabel, "growx, wrap para");
+               
+               panel.add(new JLabel("Stack trace:"), "wrap rel");
+               stackTraceLabel = new JTextArea(8, 80);
+               stackTraceLabel.setEditable(false);
+               GUIUtil.changeFontSize(stackTraceLabel, -2);
+               panel.add(new JScrollPane(stackTraceLabel), "grow");
+               
+
+               JButton close = new JButton("Close");
+               close.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               DebugLogDialog.this.dispose();
+                       }
+               });
+               mainPanel.add(close, "newline para, right, tag ok");
+               
+
+               // Use timer to purge the queue so as not to overwhelm the EDT with events
+               timer = new Timer(POLL_TIME, new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               purgeQueue();
+                       }
+               });
+               timer.setRepeats(true);
+               timer.start();
+               
+               this.addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosed(WindowEvent e) {
+                               log.user("Closing debug log dialog");
+                               timer.stop();
+                               if (delegator != null) {
+                                       log.info("Removing log listener");
+                                       delegator.removeLogger(logListener);
+                               }
+                       }
+               });
+               
+               GUIUtil.setDisposableDialogOptions(this, close);
+               followBox.requestFocus();
+       }
+       
+       
+
+       private void updateSelected(int row) {
+               if (row < 0) {
+                       
+                       numberLabel.setText("");
+                       timeLabel.setText("");
+                       levelLabel.setText("");
+                       locationLabel.setText("");
+                       messageLabel.setText("");
+                       stackTraceLabel.setText("");
+                       
+               } else {
+                       
+                       LogLine line = buffer.get(row);
+                       numberLabel.setText("" + line.getLogCount());
+                       timeLabel.setText(String.format("%.3f s", line.getTimestamp() / 1000.0));
+                       levelLabel.setText(line.getLevel().toString());
+                       TraceException e = line.getTrace();
+                       if (e != null) {
+                               locationLabel.setText(e.getMessage());
+                       } else {
+                               locationLabel.setText("-");
+                       }
+                       messageLabel.setText(line.getMessage());
+                       Throwable t = line.getCause();
+                       if (t != null) {
+                               StackTraceWriter stw = new StackTraceWriter();
+                               PrintWriter pw = new PrintWriter(stw);
+                               t.printStackTrace(pw);
+                               pw.flush();
+                               stackTraceLabel.setText(stw.toString());
+                               stackTraceLabel.setCaretPosition(0);
+                       } else {
+                               stackTraceLabel.setText("");
+                       }
+                       
+               }
+       }
+       
+       
+       /**
+        * Check whether a row signifies a number of missing rows.  This check is "heuristic"
+        * and checks whether the timestamp is zero and the message starts with "---".
+        */
+       private boolean isExcludedRow(int row) {
+               LogLine line = buffer.get(row);
+               return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---"));
+       }
+       
+       
+       /**
+        * Purge the queue of incoming log lines.  This is called periodically from the EDT, and
+        * it adds any lines in the queue to the buffer, and fires a table event.
+        */
+       private void purgeQueue() {
+               int start = buffer.size();
+               
+               LogLine line;
+               while ((line = queue.poll()) != null) {
+                       buffer.add(line);
+               }
+               
+               int end = buffer.size() - 1;
+               if (end >= start) {
+                       model.fireTableRowsInserted(start, end);
+                       if (followBox.isSelected()) {
+                               SwingUtilities.invokeLater(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               Rectangle rect = table.getCellRect(1000000000, 1, true);
+                                               table.scrollRectToVisible(rect);
+                                       }
+                               });
+                       }
+               }
+       }
+       
+       
+       /**
+        * A logger that adds log lines to the queue.  This method may be called from any
+        * thread, and therefore must be thread-safe.
+        */
+       private class LogListener extends LogHelper {
+               @Override
+               public void log(LogLine line) {
+                       queue.add(line);
+               }
+       }
+       
+       private class LogFilter extends RowFilter<TableModel, Integer> {
+               
+               @Override
+               public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
+                       int index = entry.getIdentifier();
+                       LogLine line = buffer.get(index);
+                       return filterButtons.get(line.getLevel()).isSelected();
+               }
+               
+       }
+       
+       
+       private class Renderer extends JLabel implements TableCellRenderer {
+               @Override
+               public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus,
+                               int row, int column) {
+                       Color fg, bg;
+                       
+                       row = sorter.convertRowIndexToModel(row);
+                       
+                       if (STACK_TRACE_MARK.equals(value)) {
+                               fg = Color.RED;
+                       } else {
+                               fg = table1.getForeground();
+                       }
+                       bg = backgroundColors.get(buffer.get(row).getLevel());
+                       
+                       if (isSelected) {
+                               bg = bg.darker();
+                       } else if (isExcludedRow(row)) {
+                               bg = bg.brighter();
+                       }
+                       
+                       this.setForeground(fg);
+                       this.setBackground(bg);
+                       
+                       this.setOpaque(true);
+                       this.setText(value.toString());
+                       
+                       return this;
+               }
+       }
+       
+       
+       private class LocationComparator implements Comparator<Object> {
+               private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$");
+               
+               @Override
+               public int compare(Object o1, Object o2) {
+                       String s1 = o1.toString();
+                       String s2 = o2.toString();
+                       
+                       Matcher m1 = splitPattern.matcher(s1);
+                       Matcher m2 = splitPattern.matcher(s2);
+                       
+                       if (m1.matches() && m2.matches()) {
+                               String class1 = m1.group(1);
+                               String pos1 = m1.group(2);
+                               String class2 = m2.group(1);
+                               String pos2 = m2.group(2);
+                               
+                               if (class1.equals(class2)) {
+                                       return NumericComparator.INSTANCE.compare(pos1, pos2);
+                               } else {
+                                       return class1.compareTo(class2);
+                               }
+                       }
+                       
+                       return s1.compareTo(s2);
+               }
+               
+       }
+       
+}
index 70730ba86906d76c34c679254665554518b995ed..939514b050a7ffe0d3cd9eef46cd6cc32adaea40 100644 (file)
@@ -15,6 +15,8 @@ import javax.swing.JProgressBar;
 import javax.swing.SwingWorker;
 
 import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.MathUtil;
 
 
@@ -27,7 +29,8 @@ import net.sf.openrocket.util.MathUtil;
  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
  */
 public class SwingWorkerDialog extends JDialog implements PropertyChangeListener {
-
+       private static final LogHelper log = Application.getLogger();
+       
        /** Number of milliseconds to wait at a time between checking worker status */
        private static final int DELAY = 100;
        
@@ -39,21 +42,20 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener
        
        /** Open the dialog if estimated total time is longed than this */
        private static final int TOTAL_TIME_FOR_DIALOG = 2000;
-
        
-       private final SwingWorker<?,?> worker;
+
+       private final SwingWorker<?, ?> worker;
        private final JProgressBar progressBar;
        
        private boolean cancelled = false;
        
        
-       private SwingWorkerDialog(Window parent, String title, String label, 
-                       SwingWorker<?,?> w) {
+       private SwingWorkerDialog(Window parent, String title, String label, SwingWorker<?, ?> w) {
                super(parent, title, ModalityType.APPLICATION_MODAL);
                
                this.worker = w;
                w.addPropertyChangeListener(this);
-
+               
                JPanel panel = new JPanel(new MigLayout("fill"));
                
                if (label != null) {
@@ -67,18 +69,19 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener
                cancel.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               log.user("User cancelled SwingWorker operation");
                                cancel();
                        }
                });
                panel.add(cancel, "right");
                
                this.add(panel);
-               this.setMinimumSize(new Dimension(250,100));
+               this.setMinimumSize(new Dimension(250, 100));
                this.pack();
                this.setLocationRelativeTo(parent);
                this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
        }
-
+       
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
                if (worker.getState() == SwingWorker.StateValue.DONE) {
@@ -118,12 +121,14 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener
         *                                      <code>false</code> if the user cancelled the operation
         */
        public static boolean runWorker(Window parent, String title, String label,
-                       SwingWorker<?,?> worker) {
+                       SwingWorker<?, ?> worker) {
+               
+               log.info("Running SwingWorker " + worker);
                
                // Start timing the worker
                final long startTime = System.currentTimeMillis();
                worker.execute();
-
+               
                // Monitor worker thread before opening the dialog
                while (true) {
                        
@@ -131,11 +136,12 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener
                                Thread.sleep(DELAY);
                        } catch (InterruptedException e) {
                                // Should never occur
-                               e.printStackTrace();
+                               log.error("EDT was interrupted", e);
                        }
                        
                        if (worker.isDone()) {
                                // Worker has completed within time limits
+                               log.info("Worker completed before opening dialog");
                                return true;
                        }
                        
@@ -144,24 +150,28 @@ public class SwingWorkerDialog extends JDialog implements PropertyChangeListener
                        if (elapsed < ESTIMATION_DELAY)
                                continue;
                        
-                       
+
                        // Calculate and check estimated remaining time
                        int progress = MathUtil.clamp(worker.getProgress(), 1, 100); // Avoid div-by-zero
                        long estimate = elapsed * 100 / progress;
                        long remaining = estimate - elapsed;
                        
+                       log.debug("Estimated run time, estimate=" + estimate + " remaining=" + remaining);
+                       
                        if (estimate >= TOTAL_TIME_FOR_DIALOG)
                                break;
                        
                        if (remaining >= REMAINING_TIME_FOR_DIALOG)
                                break;
                }
-
                
+
                // Dialog is required
                
+               log.info("Opening dialog for SwingWorker " + worker);
                SwingWorkerDialog dialog = new SwingWorkerDialog(parent, title, label, worker);
                dialog.setVisible(true);
+               log.info("Worker done, cancelled=" + dialog.cancelled);
                
                return !dialog.cancelled;
        }
index 94f38a9758961f8410f4e917c3b97ba85ad9c7c2..d3d5e57f4547a6b635c9594af4b4996ec551d1de 100644 (file)
@@ -205,23 +205,23 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec
                                        sel = SHOW_ALL;
                                switch (sel) {
                                case SHOW_ALL:
-                                                       sorter.setRowFilter(new MotorRowFilterAll());
-                                                       break;
-                                               
-                                               case SHOW_SMALLER:
-                                                       sorter.setRowFilter(new MotorRowFilterSmaller());
-                                                       break;
-                                               
-                                               case SHOW_EXACT:
-                                                       sorter.setRowFilter(new MotorRowFilterExact());
-                                                       break;
-                                               
-                                               default:
-                                                       assert (false) : "Should not occur.";
-                                               }
-                                               Prefs.putChoise("MotorDiameterMatch", sel);
-                                               scrollSelectionVisible();
-                                       }
+                                       sorter.setRowFilter(new MotorRowFilterAll());
+                                       break;
+                               
+                               case SHOW_SMALLER:
+                                       sorter.setRowFilter(new MotorRowFilterSmaller());
+                                       break;
+                               
+                               case SHOW_EXACT:
+                                       sorter.setRowFilter(new MotorRowFilterExact());
+                                       break;
+                               
+                               default:
+                                       throw new BugException("Invalid selection mode sel=" + sel);
+                               }
+                               Prefs.putChoise("MotorDiameterMatch", sel);
+                               scrollSelectionVisible();
+                       }
                });
                panel.add(filterComboBox, "spanx, growx, wrap rel");
                
diff --git a/src/net/sf/openrocket/gui/main/BareComponentTreeModel.java b/src/net/sf/openrocket/gui/main/BareComponentTreeModel.java
deleted file mode 100644 (file)
index 5c36178..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-package net.sf.openrocket.gui.main;
-
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.Iterator;
-
-import javax.swing.JTree;
-import javax.swing.event.TreeModelEvent;
-import javax.swing.event.TreeModelListener;
-import javax.swing.tree.TreeModel;
-import javax.swing.tree.TreePath;
-
-import net.sf.openrocket.logging.LogHelper;
-import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
-import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
-import net.sf.openrocket.rocketcomponent.RocketComponent;
-import net.sf.openrocket.startup.Application;
-
-
-/**
- * A TreeModel that implements viewing of the rocket tree structure.
- * This class shows the internal structure of the tree, as opposed to the regular
- * user-side view.
- * 
- * @author Sampo Niskanen <sampo.niskanen@iki.fi>
- */
-
-public class BareComponentTreeModel implements TreeModel, ComponentChangeListener {
-       private static final LogHelper log = Application.getLogger();
-       
-       private ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
-
-       private final RocketComponent root;
-       private final JTree tree;
-
-       public BareComponentTreeModel(RocketComponent root, JTree tree) {
-               this.root = root;
-               this.tree = tree;
-               root.addComponentChangeListener(this);
-       }
-       
-       
-       public Object getChild(Object parent, int index) {
-               RocketComponent component = (RocketComponent)parent;
-               try {
-                       return component.getChild(index);
-               } catch (IndexOutOfBoundsException e) {
-                       return null;
-               }
-       }
-
-       public int getChildCount(Object parent) {
-               return ((RocketComponent)parent).getChildCount();
-       }
-
-       public int getIndexOfChild(Object parent, Object child) {
-               RocketComponent p = (RocketComponent)parent;
-               RocketComponent c = (RocketComponent)child;
-               return p.getChildPosition(c);
-       }
-
-       public Object getRoot() {
-               return root;
-       }
-
-       public boolean isLeaf(Object node) {
-               RocketComponent c = (RocketComponent)node;
-               return (c.getChildCount()==0);
-       }
-
-       public void addTreeModelListener(TreeModelListener l) {
-               listeners.add(l);
-       }
-
-       public void removeTreeModelListener(TreeModelListener l) {
-               listeners.remove(l);
-       }
-       
-       private void fireTreeNodesChanged() {
-               Object[] path = { root };
-               TreeModelEvent e = new TreeModelEvent(this,path);
-               Object[] l = listeners.toArray();
-               for (int i=0; i<l.length; i++)
-                       ((TreeModelListener)l[i]).treeNodesChanged(e);
-       }
-       
-       
-       private void printStructure(TreePath p, int level) {
-               String indent="";
-               for (int i=0; i<level; i++)
-                       indent += "  ";
-               System.out.println(indent+p+
-                               ": isVisible:"+tree.isVisible(p)+
-                               " isCollapsed:"+tree.isCollapsed(p)+
-                               " isExpanded:"+tree.isExpanded(p));
-               Object parent = p.getLastPathComponent();
-               for (int i=0; i<getChildCount(parent); i++) {
-                       Object child = getChild(parent,i);
-                       TreePath path = makeTreePath((RocketComponent)child);
-                       printStructure(path,level+1);
-               }
-       }
-       
-       
-       private void fireTreeStructureChanged(RocketComponent source) {
-               Object[] path = { root };
-               
-               
-               // Get currently expanded path IDs
-               Enumeration<TreePath> enumer = tree.getExpandedDescendants(new TreePath(path));
-               ArrayList<String> expanded = new ArrayList<String>();
-               if (enumer != null) {
-                       while (enumer.hasMoreElements()) {
-                               TreePath p = enumer.nextElement();
-                               expanded.add(((RocketComponent)p.getLastPathComponent()).getID());
-                       }
-               }
-               
-               // Send structure change event
-               TreeModelEvent e = new TreeModelEvent(this,path);
-               Object[] l = listeners.toArray();
-               for (int i=0; i<l.length; i++)
-                       ((TreeModelListener)l[i]).treeStructureChanged(e);
-               
-               // Re-expand the paths
-               Iterator<String> iter = expanded.iterator();
-               while (iter.hasNext()) {
-                       RocketComponent c = root.findComponent(iter.next());
-                       if (c==null)
-                               continue;
-                       tree.expandPath(makeTreePath(c));
-               }
-               if (source != null) {
-                       TreePath p = makeTreePath(source);
-                       tree.makeVisible(p);
-                       tree.expandPath(p);
-               }
-       }
-       
-       public void valueForPathChanged(TreePath path, Object newValue) {
-               log.error("ERROR: valueForPathChanged called?!");
-       }
-
-
-       public void componentChanged(ComponentChangeEvent e) {
-               if (e.isTreeChange() || e.isUndoChange()) {
-                       // Tree must be fully updated also in case of an undo change 
-                       fireTreeStructureChanged((RocketComponent)e.getSource());
-                       if (e.isTreeChange() && e.isUndoChange()) {
-                               // If the undo has changed the tree structure, some elements may be hidden unnecessarily
-                               // TODO: LOW: Could this be performed better?
-                               expandAll();
-                       }
-               } else if (e.isOtherChange()) {
-                       fireTreeNodesChanged();
-               }
-       }
-
-       public void expandAll() {
-               Iterator<RocketComponent> iterator = root.deepIterator();
-               while (iterator.hasNext()) {
-                       tree.makeVisible(makeTreePath(iterator.next()));
-               }
-       }
-       
-       public static TreePath makeTreePath(RocketComponent component) {
-               int count = 0;
-               RocketComponent c = component;
-               
-               while (c != null) {
-                       count++;
-                       c = c.getParent();
-               }
-               
-               Object[] list = new Object[count];
-               
-               count--;
-               c=component;
-               while (c!=null) {
-                       list[count] = c;
-                       count--;
-                       c = c.getParent();
-               }
-               
-               return new TreePath(list);
-       }
-       
-}
index aba5398635fdee8f248706c9b3f7c866a4f2e185..4031be8aabc259ec9a1d69ea1931d0f376b242fb 100644 (file)
@@ -19,12 +19,12 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
-import java.lang.reflect.InvocationTargetException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLDecoder;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.concurrent.ExecutionException;
 
 import javax.swing.Action;
@@ -47,8 +47,6 @@ import javax.swing.KeyStroke;
 import javax.swing.ListSelectionModel;
 import javax.swing.ScrollPaneConstants;
 import javax.swing.SwingUtilities;
-import javax.swing.Timer;
-import javax.swing.ToolTipManager;
 import javax.swing.border.TitledBorder;
 import javax.swing.event.TreeSelectionEvent;
 import javax.swing.event.TreeSelectionListener;
@@ -59,9 +57,6 @@ import javax.swing.tree.TreeSelectionModel;
 
 import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.aerodynamics.WarningSet;
-import net.sf.openrocket.communication.UpdateInfo;
-import net.sf.openrocket.communication.UpdateInfoRetriever;
-import net.sf.openrocket.database.Databases;
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.file.GeneralRocketLoader;
 import net.sf.openrocket.file.RocketLoadException;
@@ -73,13 +68,14 @@ import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
 import net.sf.openrocket.gui.dialogs.AboutDialog;
 import net.sf.openrocket.gui.dialogs.BugReportDialog;
 import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog;
+import net.sf.openrocket.gui.dialogs.DebugLogDialog;
 import net.sf.openrocket.gui.dialogs.ExampleDesignDialog;
 import net.sf.openrocket.gui.dialogs.LicenseDialog;
 import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog;
 import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
-import net.sf.openrocket.gui.dialogs.UpdateInfoDialog;
 import net.sf.openrocket.gui.dialogs.WarningDialog;
 import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog;
+import net.sf.openrocket.gui.main.componenttree.ComponentTree;
 import net.sf.openrocket.gui.scalefigure.RocketPanel;
 import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
@@ -283,7 +279,7 @@ public class BasicFrame extends JFrame {
                
                JPanel panel = new JPanel(new MigLayout("fill, flowy", "", "[grow]"));
                
-               tree = new ComponentTree(rocket);
+               tree = new ComponentTree(document);
                tree.setSelectionModel(componentSelectionModel);
                
                // Remove JTree key events that interfere with menu accelerators
@@ -404,9 +400,12 @@ public class BasicFrame extends JFrame {
                item.setIcon(Icons.FILE_NEW);
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("New... selected");
                                newAction();
-                               if (replaceable)
+                               if (replaceable) {
+                                       log.info("Closing previous window");
                                        closeAction();
+                               }
                        }
                });
                menu.add(item);
@@ -417,6 +416,7 @@ public class BasicFrame extends JFrame {
                item.setIcon(Icons.FILE_OPEN);
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Open... selected");
                                openAction();
                        }
                });
@@ -429,9 +429,11 @@ public class BasicFrame extends JFrame {
                item.setIcon(Icons.FILE_OPEN_EXAMPLE);
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Open example... selected");
                                URL[] urls = ExampleDesignDialog.selectExampleDesigns(BasicFrame.this);
                                if (urls != null) {
                                        for (URL u : urls) {
+                                               log.user("Opening example " + u);
                                                open(u, BasicFrame.this);
                                        }
                                }
@@ -447,6 +449,7 @@ public class BasicFrame extends JFrame {
                item.setIcon(Icons.FILE_SAVE);
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Save selected");
                                saveAction();
                        }
                });
@@ -460,6 +463,7 @@ public class BasicFrame extends JFrame {
                item.setIcon(Icons.FILE_SAVE_AS);
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Save as... selected");
                                saveAsAction();
                        }
                });
@@ -474,6 +478,7 @@ public class BasicFrame extends JFrame {
                item.setIcon(Icons.FILE_CLOSE);
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Close selected");
                                closeAction();
                        }
                });
@@ -487,6 +492,7 @@ public class BasicFrame extends JFrame {
                item.setIcon(Icons.FILE_QUIT);
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Quit selected");
                                quitAction();
                        }
                });
@@ -540,6 +546,7 @@ public class BasicFrame extends JFrame {
                                "preferences");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Preferences selected");
                                PreferencesDialog.showPreferences();
                        }
                });
@@ -559,6 +566,7 @@ public class BasicFrame extends JFrame {
                                "separately");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Component analysis selected");
                                ComponentAnalysisDialog.showDialog(rocketpanel);
                        }
                });
@@ -586,26 +594,42 @@ public class BasicFrame extends JFrame {
                item.getAccessibleContext().setAccessibleDescription("OpenRocket license information");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("License selected");
                                new LicenseDialog(BasicFrame.this).setVisible(true);
                        }
                });
                menu.add(item);
                
+               menu.addSeparator();
+               
                item = new JMenuItem("Bug report", KeyEvent.VK_B);
                item.getAccessibleContext().setAccessibleDescription("Information about reporting " +
                                "bugs in OpenRocket");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
-                               //                              new BugDialog(BasicFrame.this).setVisible(true);
+                               log.user("Bug report selected");
                                BugReportDialog.showBugReportDialog(BasicFrame.this);
                        }
                });
                menu.add(item);
                
+               item = new JMenuItem("Debug log");
+               item.getAccessibleContext().setAccessibleDescription("View the OpenRocket debug log");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Debug log selected");
+                               new DebugLogDialog(BasicFrame.this).setVisible(true);
+                       }
+               });
+               menu.add(item);
+               
+               menu.addSeparator();
+               
                item = new JMenuItem("About", KeyEvent.VK_A);
                item.getAccessibleContext().setAccessibleDescription("About OpenRocket");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("About selected");
                                new AboutDialog(BasicFrame.this).setVisible(true);
                        }
                });
@@ -627,6 +651,7 @@ public class BasicFrame extends JFrame {
                item = new JMenuItem("What is this menu?");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("What is this menu? selected");
                                JOptionPane.showMessageDialog(BasicFrame.this,
                                                new Object[] {
                                                                "The 'Debug' menu includes actions for testing and debugging " +
@@ -645,6 +670,7 @@ public class BasicFrame extends JFrame {
                item.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Create test rocket selected");
                                JTextField field = new JTextField();
                                int sel = JOptionPane.showOptionDialog(BasicFrame.this, new Object[] {
                                                "Input text key to generate random rocket:",
@@ -677,6 +703,7 @@ public class BasicFrame extends JFrame {
                item.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Create Iso-Haisu selected");
                                Rocket r = TestRockets.makeIsoHaisu();
                                OpenRocketDocument doc = new OpenRocketDocument(r);
                                doc.setSaved(true);
@@ -691,6 +718,7 @@ public class BasicFrame extends JFrame {
                item.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Create Big Blue selected");
                                Rocket r = TestRockets.makeBigBlue();
                                OpenRocketDocument doc = new OpenRocketDocument(r);
                                doc.setSaved(true);
@@ -700,13 +728,12 @@ public class BasicFrame extends JFrame {
                });
                menu.add(item);
                
-
-
                menu.addSeparator();
                
                item = new JMenuItem("Exception here");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Exception here selected");
                                throw new RuntimeException("Testing exception from menu action listener");
                        }
                });
@@ -715,6 +742,7 @@ public class BasicFrame extends JFrame {
                item = new JMenuItem("Exception from EDT");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Exception from EDT selected");
                                SwingUtilities.invokeLater(new Runnable() {
                                        @Override
                                        public void run() {
@@ -729,6 +757,7 @@ public class BasicFrame extends JFrame {
                item = new JMenuItem("Exception from other thread");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Exception from other thread selected");
                                new Thread() {
                                        @Override
                                        public void run() {
@@ -769,19 +798,24 @@ public class BasicFrame extends JFrame {
                chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
                chooser.setMultiSelectionEnabled(true);
                chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
-               if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION)
+               int option = chooser.showOpenDialog(this);
+               if (option != JFileChooser.APPROVE_OPTION) {
+                       log.user("Decided not to open files, option=" + option);
                        return;
+               }
                
                Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
                
                File[] files = chooser.getSelectedFiles();
+               log.user("Opening files " + Arrays.toString(files));
                
                for (File file : files) {
-                       System.out.println("Opening file: " + file);
+                       log.info("Opening file: " + file);
                        if (open(file, this)) {
                                
                                // Close previous window if replacing
                                if (replaceable && document.isSaved()) {
+                                       log.info("Closing window because it is replaceable");
                                        closeAction();
                                        replaceable = false;
                                }
@@ -790,10 +824,17 @@ public class BasicFrame extends JFrame {
        }
        
        
-
+       /**
+        * Open a file based on a URL.
+        * @param url           the file to open.
+        * @param parent        the parent window for dialogs.
+        * @return                      <code>true</code> if opened successfully.
+        */
        private static boolean open(URL url, BasicFrame parent) {
                String filename = null;
                
+               // First figure out the file name from the URL
+               
                // Try using URI.getPath();
                try {
                        URI uri = url.toURI();
@@ -819,6 +860,9 @@ public class BasicFrame extends JFrame {
                        filename = filename.substring(filename.lastIndexOf('/') + 1);
                }
                
+
+               // Open the file
+               log.info("Opening file from url=" + url + " filename=" + filename);
                try {
                        InputStream is = url.openStream();
                        if (open(is, filename, parent)) {
@@ -829,6 +873,7 @@ public class BasicFrame extends JFrame {
                                }
                        }
                } catch (IOException e) {
+                       log.warn("Error opening file" + e);
                        JOptionPane.showMessageDialog(parent,
                                        "An error occurred while opening the file " + filename,
                                        "Error loading file", JOptionPane.ERROR_MESSAGE);
@@ -876,16 +921,15 @@ public class BasicFrame extends JFrame {
         * @param parent
         * @return
         */
-       private static boolean open(OpenFileWorker worker, String filename, File file,
-                       Window parent) {
+       private static boolean open(OpenFileWorker worker, String filename, File file, Window parent) {
                
-               MotorDatabaseLoadingDialog.check(null);
+               MotorDatabaseLoadingDialog.check(parent);
                
                // Open the file in a Swing worker thread
-               if (!SwingWorkerDialog.runWorker(parent, "Opening file",
-                               "Reading " + filename + "...", worker)) {
-                       
+               log.info("Starting OpenFileWorker");
+               if (!SwingWorkerDialog.runWorker(parent, "Opening file", "Reading " + filename + "...", worker)) {
                        // User cancelled the operation
+                       log.info("User cancelled the OpenFileWorker");
                        return false;
                }
                
@@ -902,6 +946,7 @@ public class BasicFrame extends JFrame {
                        
                        if (cause instanceof FileNotFoundException) {
                                
+                               log.warn("File not found", cause);
                                JOptionPane.showMessageDialog(parent,
                                                "File not found: " + filename,
                                                "Error opening file", JOptionPane.ERROR_MESSAGE);
@@ -909,6 +954,7 @@ public class BasicFrame extends JFrame {
                                
                        } else if (cause instanceof RocketLoadException) {
                                
+                               log.warn("Error loading the file", cause);
                                JOptionPane.showMessageDialog(parent,
                                                "Unable to open file '" + filename + "': "
                                                                + cause.getMessage(),
@@ -926,13 +972,14 @@ public class BasicFrame extends JFrame {
                }
                
                if (doc == null) {
-                       throw new BugException("BUG: Document loader returned null");
+                       throw new BugException("Document loader returned null");
                }
                
 
                // Show warnings
                WarningSet warnings = worker.getRocketLoader().getWarnings();
                if (!warnings.isEmpty()) {
+                       log.info("Warnings while reading file: " + warnings);
                        WarningDialog.showWarnings(parent,
                                        new Object[] {
                                                        "The following problems were encountered while opening " + filename + ".",
@@ -947,6 +994,7 @@ public class BasicFrame extends JFrame {
                doc.setSaved(true);
                
                // Open the frame
+               log.debug("Opening new frame with the document");
                BasicFrame frame = new BasicFrame(doc);
                frame.setVisible(true);
                
@@ -960,21 +1008,26 @@ public class BasicFrame extends JFrame {
        private boolean saveAction() {
                File file = document.getFile();
                if (file == null) {
+                       log.info("Document does not contain file, opening save as dialog instead");
                        return saveAsAction();
                }
+               log.info("Saving document to " + file);
                
                // Saving RockSim designs is not supported
                if (ROCKSIM_DESIGN_FILTER.accept(file)) {
                        file = new File(file.getAbsolutePath().replaceAll(".[rR][kK][tT](.[gG][zZ])?$",
                                        ".ork"));
                        
+                       log.info("Attempting to save in RockSim format, renaming to " + file);
                        int option = JOptionPane.showConfirmDialog(this, new Object[] {
                                        "Saving designs in RockSim format is not supported.",
                                        "Save in OpenRocket format instead (" + file.getName() + ")?"
                                }, "Save " + file.getName(), JOptionPane.YES_NO_OPTION,
                                        JOptionPane.QUESTION_MESSAGE, null);
-                       if (option != JOptionPane.YES_OPTION)
+                       if (option != JOptionPane.YES_OPTION) {
+                               log.user("User chose not to save");
                                return false;
+                       }
                        
                        document.setFile(file);
                }
@@ -984,52 +1037,60 @@ public class BasicFrame extends JFrame {
        
        private boolean saveAsAction() {
                File file = null;
-               while (file == null) {
-                       // TODO: HIGH: what if *.rkt chosen?
-                       StorageOptionChooser storageChooser =
-                                       new StorageOptionChooser(document, document.getDefaultStorageOptions());
-                       JFileChooser chooser = new JFileChooser();
-                       chooser.setFileFilter(OPENROCKET_DESIGN_FILTER);
-                       chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
-                       chooser.setAccessory(storageChooser);
-                       if (document.getFile() != null)
-                               chooser.setSelectedFile(document.getFile());
-                       
-                       if (chooser.showSaveDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
-                               return false;
-                       
-                       file = chooser.getSelectedFile();
-                       if (file == null)
+               
+               // TODO: HIGH: what if *.rkt chosen?
+               StorageOptionChooser storageChooser =
+                               new StorageOptionChooser(document, document.getDefaultStorageOptions());
+               JFileChooser chooser = new JFileChooser();
+               chooser.setFileFilter(OPENROCKET_DESIGN_FILTER);
+               chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
+               chooser.setAccessory(storageChooser);
+               if (document.getFile() != null)
+                       chooser.setSelectedFile(document.getFile());
+               
+               int option = chooser.showSaveDialog(BasicFrame.this);
+               if (option != JFileChooser.APPROVE_OPTION) {
+                       log.user("User decided not to save, option=" + option);
+                       return false;
+               }
+               
+               file = chooser.getSelectedFile();
+               if (file == null) {
+                       log.user("User did not select a file");
+                       return false;
+               }
+               
+               Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
+               storageChooser.storeOptions(document.getDefaultStorageOptions());
+               
+               if (file.getName().indexOf('.') < 0) {
+                       log.debug("File name does not contain extension, adding .ork");
+                       String name = file.getAbsolutePath();
+                       name = name + ".ork";
+                       file = new File(name);
+               }
+               
+               if (file.exists()) {
+                       log.info("File " + file + " exists, confirming overwrite from user");
+                       int result = JOptionPane.showConfirmDialog(this,
+                                       "File '" + file.getName() + "' exists.  Do you want to overwrite it?",
+                                       "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
+                       if (result != JOptionPane.YES_OPTION) {
+                               log.user("User decided not to overwrite the file");
                                return false;
-                       
-                       Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
-                       storageChooser.storeOptions(document.getDefaultStorageOptions());
-                       
-                       if (file.getName().indexOf('.') < 0) {
-                               String name = file.getAbsolutePath();
-                               name = name + ".ork";
-                               file = new File(name);
-                       }
-                       
-                       if (file.exists()) {
-                               int result = JOptionPane.showConfirmDialog(this,
-                                               "File '" + file.getName() + "' exists.  Do you want to overwrite it?",
-                                               "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
-                               if (result != JOptionPane.YES_OPTION)
-                                       return false;
                        }
                }
-               saveAs(file);
-               return true;
+               
+               return saveAs(file);
        }
        
-       
        private boolean saveAs(File file) {
-               System.out.println("Saving to file: " + file.getName());
+               log.info("Saving document as " + file);
                boolean saved = false;
                
                if (!StorageOptionChooser.verifyStorageOptions(document, this)) {
                        // User cancelled the dialog
+                       log.user("User cancelled saving in storage options dialog");
                        return false;
                }
                
@@ -1040,6 +1101,7 @@ public class BasicFrame extends JFrame {
                                "Writing " + file.getName() + "...", worker)) {
                        
                        // User cancelled the save
+                       log.user("User cancelled the save, deleting the file");
                        file.delete();
                        return false;
                }
@@ -1055,6 +1117,7 @@ public class BasicFrame extends JFrame {
                        Throwable cause = e.getCause();
                        
                        if (cause instanceof IOException) {
+                               log.warn("An I/O error occurred while saving " + file, cause);
                                JOptionPane.showMessageDialog(this, new String[] {
                                                "An I/O error occurred while saving:",
                                                e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
@@ -1073,6 +1136,7 @@ public class BasicFrame extends JFrame {
        
        private boolean closeAction() {
                if (!document.isSaved()) {
+                       log.info("Confirming whether to save the design");
                        ComponentConfigDialog.hideDialog();
                        int result = JOptionPane.showConfirmDialog(this,
                                        "Design '" + rocket.getName() + "' has not been saved.  " +
@@ -1081,44 +1145,44 @@ public class BasicFrame extends JFrame {
                                        JOptionPane.QUESTION_MESSAGE);
                        if (result == JOptionPane.YES_OPTION) {
                                // Save
-                               if (!saveAction())
-                                       return false; // If save was interrupted
+                               log.user("User requested file save");
+                               if (!saveAction()) {
+                                       log.info("File save was interrupted, not closing");
+                                       return false;
+                               }
                        } else if (result == JOptionPane.NO_OPTION) {
                                // Don't save: No-op
+                               log.user("User requested to discard design");
                        } else {
                                // Cancel or close
+                               log.user("User cancelled closing, result=" + result);
                                return false;
                        }
                }
                
                // Rocket has been saved or discarded
+               log.debug("Disposing window");
                this.dispose();
                
-               // TODO: LOW: Close only dialogs that have this frame as their parent
                ComponentConfigDialog.hideDialog();
                ComponentAnalysisDialog.hideDialog();
                
                frames.remove(this);
-               if (frames.isEmpty())
+               if (frames.isEmpty()) {
+                       log.info("Last frame closed, exiting");
                        System.exit(0);
+               }
                return true;
        }
        
        
-       /**
-        * Closes this frame if it is replaceable.
-        */
-       public void closeIfReplaceable() {
-               if (this.replaceable && document.isSaved()) {
-                       closeAction();
-               }
-       }
-       
+
        /**
         * Open a new design window with a basic rocket+stage.
         */
        public static void newAction() {
-               log.debug("New action initiated");
+               log.info("New action initiated");
+               
                Rocket rocket = new Rocket();
                Stage stage = new Stage();
                stage.setName("Sustainer");
@@ -1136,13 +1200,17 @@ public class BasicFrame extends JFrame {
         * Quit the application.  Confirms saving unsaved designs.  The action of File->Quit.
         */
        public static void quitAction() {
+               log.info("Quit action initiated");
                for (int i = frames.size() - 1; i >= 0; i--) {
+                       log.debug("Closing frame " + frames.get(i));
                        if (!frames.get(i).closeAction()) {
                                // Close canceled
+                               log.info("Quit was cancelled");
                                return;
                        }
                }
                // Should not be reached, but just in case
+               log.error("Should already have exited application");
                System.exit(0);
        }
        
@@ -1177,9 +1245,12 @@ public class BasicFrame extends JFrame {
         */
        public static BasicFrame findFrame(Rocket rocket) {
                for (BasicFrame f : frames) {
-                       if (f.rocket == rocket)
+                       if (f.rocket == rocket) {
+                               log.debug("Found frame " + f + " for rocket " + rocket);
                                return f;
+                       }
                }
+               log.debug("Could not find frame for rocket " + rocket);
                return null;
        }
        
@@ -1191,142 +1262,11 @@ public class BasicFrame extends JFrame {
         * @return               the corresponding OpenRocketDocument, or <code>null</code> if not found.
         */
        public static OpenRocketDocument findDocument(Rocket rocket) {
-               for (BasicFrame f : frames) {
-                       if (f.rocket == rocket)
-                               return f.document;
-               }
-               return null;
-       }
-       
-       
-       public static void main(final String[] args) {
-               
-               // Run the actual startup method in the EDT since it can use progress dialogs etc.
-               try {
-                       SwingUtilities.invokeAndWait(new Runnable() {
-                               @Override
-                               public void run() {
-                                       runMain(args);
-                               }
-                       });
-               } catch (InterruptedException e) {
-                       e.printStackTrace();
-               } catch (InvocationTargetException e) {
-                       e.printStackTrace();
-               }
-               
-       }
-       
-       
-       private static void runMain(String[] args) {
-               
-               // Initialize the splash screen with version info
-               Splash.init();
-               
-
-               // Start update info fetching
-               final UpdateInfoRetriever updateInfo;
-               if (Prefs.getCheckUpdates()) {
-                       updateInfo = new UpdateInfoRetriever();
-                       updateInfo.start();
+               BasicFrame frame = findFrame(rocket);
+               if (frame != null) {
+                       return frame.document;
                } else {
-                       updateInfo = null;
+                       return null;
                }
-               
-
-               // Set the best available look-and-feel
-               GUIUtil.setBestLAF();
-               
-               // Set tooltip delay time.  Tooltips are used in MotorChooserDialog extensively.
-               ToolTipManager.sharedInstance().setDismissDelay(30000);
-               
-
-               // Setup the uncaught exception handler
-               ExceptionHandler.registerExceptionHandler();
-               
-
-               // Load defaults
-               Prefs.loadDefaultUnits();
-               
-
-               // Load motors etc.
-               Databases.fakeMethod();
-               
-               // Starting action (load files or open new document)
-               if (!handleCommandLine(args)) {
-                       newAction();
-               }
-               
-
-               // Check whether update info has been fetched or whether it needs more time
-               checkUpdateStatus(updateInfo);
-       }
-       
-       
-       private static void checkUpdateStatus(final UpdateInfoRetriever updateInfo) {
-               if (updateInfo == null)
-                       return;
-               
-               int delay = 1000;
-               if (!updateInfo.isRunning())
-                       delay = 100;
-               
-               final Timer timer = new Timer(delay, null);
-               
-               ActionListener listener = new ActionListener() {
-                       private int count = 5;
-                       
-                       @Override
-                       public void actionPerformed(ActionEvent e) {
-                               if (!updateInfo.isRunning()) {
-                                       timer.stop();
-                                       
-                                       String current = Prefs.getVersion();
-                                       String last = Prefs.getString(Prefs.LAST_UPDATE, "");
-                                       
-                                       UpdateInfo info = updateInfo.getUpdateInfo();
-                                       if (info != null && info.getLatestVersion() != null &&
-                                                       !current.equals(info.getLatestVersion()) &&
-                                                       !last.equals(info.getLatestVersion())) {
-                                               
-                                               UpdateInfoDialog infoDialog = new UpdateInfoDialog(info);
-                                               infoDialog.setVisible(true);
-                                               if (infoDialog.isReminderSelected()) {
-                                                       Prefs.putString(Prefs.LAST_UPDATE, "");
-                                               } else {
-                                                       Prefs.putString(Prefs.LAST_UPDATE, info.getLatestVersion());
-                                               }
-                                       }
-                               }
-                               count--;
-                               if (count <= 0)
-                                       timer.stop();
-                       }
-               };
-               timer.addActionListener(listener);
-               timer.start();
-       }
-       
-       
-       /**
-        * Handles arguments passed from the command line.  This may be used either
-        * when starting the first instance of OpenRocket or later when OpenRocket is
-        * executed again while running.
-        * 
-        * @param args  the command-line arguments.
-        * @return              whether a new frame was opened or similar user desired action was
-        *                              performed as a result.
-        */
-       public static boolean handleCommandLine(String[] args) {
-               
-               // Check command-line for files
-               boolean opened = false;
-               for (String file : args) {
-                       if (open(new File(file), null)) {
-                               opened = true;
-                       }
-               }
-               return opened;
        }
-       
 }
index 3fe592677d768a34d21b6c70f766da03a8d89018..af1d1bb103fd229a545244170b72aa7a12dcb43e 100644 (file)
@@ -29,6 +29,7 @@ import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.gui.components.StyledLabel;
 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
 import net.sf.openrocket.rocketcomponent.BodyComponent;
 import net.sf.openrocket.rocketcomponent.BodyTube;
 import net.sf.openrocket.rocketcomponent.Bulkhead;
diff --git a/src/net/sf/openrocket/gui/main/ComponentTree.java b/src/net/sf/openrocket/gui/main/ComponentTree.java
deleted file mode 100644 (file)
index 4164666..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-package net.sf.openrocket.gui.main;
-
-import java.awt.BasicStroke;
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-
-import javax.swing.Icon;
-import javax.swing.JTree;
-
-import net.sf.openrocket.rocketcomponent.RocketComponent;
-
-
-public class ComponentTree extends JTree {
-       private static final long serialVersionUID = 1L;
-
-       public ComponentTree(RocketComponent root) {
-               super();
-               this.setModel(new ComponentTreeModel(root,this));
-               this.setToggleClickCount(0);
-               
-               javax.swing.plaf.basic.BasicTreeUI ui = new javax.swing.plaf.basic.BasicTreeUI();
-               this.setUI(ui);
-
-               ui.setExpandedIcon(TreeIcon.MINUS);
-               ui.setCollapsedIcon(TreeIcon.PLUS);
-               
-               ui.setLeftChildIndent(15);
-               
-
-               setBackground(Color.WHITE);
-               setShowsRootHandles(false);
-               
-               setCellRenderer(new ComponentTreeRenderer());
-               
-               // Expand whole tree by default
-               expandTree();
-       }
-       
-       
-       public void expandTree() {
-               for (int i=0; i<getRowCount(); i++)
-                       expandRow(i);
-               
-       }
-       
-       private static class TreeIcon implements Icon{
-               public static final Icon PLUS = new TreeIcon(true);
-               public static final Icon MINUS = new TreeIcon(false);
-               
-               // Implementation:
-               
-           private final static int width = 9;
-           private final static int height = 9;
-           private final static BasicStroke stroke = new BasicStroke(2);
-           private boolean plus;
-           
-           private TreeIcon(boolean plus) {
-               this.plus = plus;
-           }
-           
-           public void paintIcon(Component c, Graphics g, int x, int y) {
-               Graphics2D g2 = (Graphics2D)g.create();
-               
-               g2.setColor(Color.WHITE);
-               g2.fillRect(x,y,width,height);
-               
-               g2.setColor(Color.DARK_GRAY);
-               g2.drawRect(x,y,width,height);
-               
-               g2.setStroke(stroke);
-               g2.drawLine(x+3, y+(height+1)/2, x+width-2, y+(height+1)/2);
-               if (plus)
-                       g2.drawLine(x+(width+1)/2, y+3, x+(width+1)/2, y+height-2);
-               
-               g2.dispose();
-           }
-           
-           public int getIconWidth() {
-               return width;
-           }
-           
-           public int getIconHeight() {
-               return height;
-           }
-       }
-       
-}
-
-
diff --git a/src/net/sf/openrocket/gui/main/ComponentTreeModel.java b/src/net/sf/openrocket/gui/main/ComponentTreeModel.java
deleted file mode 100644 (file)
index 7e7ba15..0000000
+++ /dev/null
@@ -1,202 +0,0 @@
-package net.sf.openrocket.gui.main;
-
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.Iterator;
-import java.util.List;
-
-import javax.swing.JTree;
-import javax.swing.event.TreeModelEvent;
-import javax.swing.event.TreeModelListener;
-import javax.swing.tree.TreeModel;
-import javax.swing.tree.TreePath;
-
-import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
-import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
-import net.sf.openrocket.rocketcomponent.RocketComponent;
-
-
-/**
- * A TreeModel that implements viewing of the rocket tree structure.
- * This transforms the internal view (which has nested Stages) into the user-view
- * (which has parallel Stages).
- * 
- * To view with the internal structure, switch to using BareComponentTreeModel in
- * ComponentTree.java.  NOTE: This class's makeTreePath will still be used, which
- * will create illegal paths, which results in problems with selections. 
- * 
- * TODO: MEDIUM: When converting a component to another component this model given 
- * outdated information, since it uses the components themselves as the nodes.
- * 
- * @author Sampo Niskanen <sampo.niskanen@iki.fi>
- */
-
-public class ComponentTreeModel implements TreeModel, ComponentChangeListener {
-       ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
-
-       private final RocketComponent root;
-       private final JTree tree;
-
-       public ComponentTreeModel(RocketComponent root, JTree tree) {
-               this.root = root;
-               this.tree = tree;
-               root.addComponentChangeListener(this);
-       }
-       
-       
-       public Object getChild(Object parent, int index) {
-               RocketComponent component = (RocketComponent)parent;
-               
-               try {
-                       return component.getChild(index);
-               } catch (IndexOutOfBoundsException e) {
-                       return null;
-               }
-       }
-       
-
-       public int getChildCount(Object parent) {
-               RocketComponent c = (RocketComponent)parent;
-
-               return c.getChildCount();
-       }
-
-       
-       public int getIndexOfChild(Object parent, Object child) {
-               if (parent==null || child==null)
-                       return -1;
-               
-               RocketComponent p = (RocketComponent)parent;
-               RocketComponent c = (RocketComponent)child;
-               
-               return p.getChildPosition(c);
-       }
-
-       public Object getRoot() {
-               return root;
-       }
-
-       public boolean isLeaf(Object node) {
-               RocketComponent c = (RocketComponent)node;
-
-               return (c.getChildCount() == 0);
-       }
-
-       public void addTreeModelListener(TreeModelListener l) {
-               listeners.add(l);
-       }
-
-       public void removeTreeModelListener(TreeModelListener l) {
-               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);
-               Object[] l = listeners.toArray();
-               for (int i=0; i<l.length; i++)
-                       ((TreeModelListener)l[i]).treeNodesChanged(e);
-       }
-       
-       
-       @SuppressWarnings("unused")
-       private void printStructure(TreePath p, int level) {
-               String indent="";
-               for (int i=0; i<level; i++)
-                       indent += "  ";
-               System.out.println(indent+p+
-                               ": isVisible:"+tree.isVisible(p)+
-                               " isCollapsed:"+tree.isCollapsed(p)+
-                               " isExpanded:"+tree.isExpanded(p));
-               Object parent = p.getLastPathComponent();
-               for (int i=0; i<getChildCount(parent); i++) {
-                       Object child = getChild(parent,i);
-                       TreePath path = makeTreePath((RocketComponent)child);
-                       printStructure(path,level+1);
-               }
-       }
-       
-       
-       private void fireTreeStructureChanged(RocketComponent source) {
-               Object[] path = { root };
-               
-               
-               // Get currently expanded path IDs
-               Enumeration<TreePath> enumer = tree.getExpandedDescendants(new TreePath(path));
-               ArrayList<String> expanded = new ArrayList<String>();
-               if (enumer != null) {
-                       while (enumer.hasMoreElements()) {
-                               TreePath p = enumer.nextElement();
-                               expanded.add(((RocketComponent)p.getLastPathComponent()).getID());
-                       }
-               }
-               
-               // Send structure change event
-               TreeModelEvent e = new TreeModelEvent(this,path);
-               Object[] l = listeners.toArray();
-               for (int i=0; i<l.length; i++)
-                       ((TreeModelListener)l[i]).treeStructureChanged(e);
-               
-               // Re-expand the paths
-               for (String id: expanded) {
-                       RocketComponent c = root.findComponent(id);
-                       if (c==null)
-                               continue;
-                       tree.expandPath(makeTreePath(c));
-               }
-               if (source != null) {
-                       TreePath p = makeTreePath(source);
-                       tree.makeVisible(p);
-                       tree.expandPath(p);
-               }
-       }
-       
-       public void valueForPathChanged(TreePath path, Object newValue) {
-               System.err.println("ERROR: valueForPathChanged called?!");
-       }
-
-
-       public void componentChanged(ComponentChangeEvent e) {
-               if (e.isTreeChange() || e.isUndoChange()) {
-                       // Tree must be fully updated also in case of an undo change 
-                       fireTreeStructureChanged((RocketComponent)e.getSource());
-                       if (e.isTreeChange() && e.isUndoChange()) {
-                               // If the undo has changed the tree structure, some elements may be hidden
-                               // unnecessarily
-                               // TODO: LOW: Could this be performed better?
-                               expandAll();
-                       }
-               } else if (e.isOtherChange()) {
-                       fireTreeNodeChanged(e.getSource());
-               }
-       }
-
-       public void expandAll() {
-               Iterator<RocketComponent> iterator = root.deepIterator();
-               while (iterator.hasNext()) {
-                       tree.makeVisible(makeTreePath(iterator.next()));
-               }
-       }
-
-       
-       public static TreePath makeTreePath(RocketComponent component) {
-               RocketComponent c = component;
-       
-               List<RocketComponent> list = new ArrayList<RocketComponent>();
-               
-               while (c != null) {
-                       list.add(0,c);
-                       c = c.getParent();
-               }
-               
-               return new TreePath(list.toArray());
-       }
-       
-}
diff --git a/src/net/sf/openrocket/gui/main/ComponentTreeRenderer.java b/src/net/sf/openrocket/gui/main/ComponentTreeRenderer.java
deleted file mode 100644 (file)
index e505d7a..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.sf.openrocket.gui.main;
-
-
-
-import java.awt.Component;
-
-import javax.swing.JTree;
-import javax.swing.tree.DefaultTreeCellRenderer;
-
-public class ComponentTreeRenderer extends DefaultTreeCellRenderer {
-
-    @Override
-       public Component getTreeCellRendererComponent(
-            JTree tree,
-            Object value,
-            boolean sel,
-            boolean expanded,
-            boolean leaf,
-            int row,
-            boolean hasFocus) {
-
-       super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
-       
-       setIcon(ComponentIcons.getSmallIcon(value.getClass()));
-       
-       return this;
-    }
-       
-}
index 746a2341fd9c43a1372816a913941988bf1c3fe9..409620cb9f7297b5a68d11bed0e5c01d137e02dd 100644 (file)
@@ -14,6 +14,7 @@ import javax.swing.tree.TreeSelectionModel;
 
 import net.sf.openrocket.document.OpenRocketDocument;
 import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
 
 public class DocumentSelectionModel {
index f484b2b156aedbe82a9f739b6c246df8ac1ca8d3..384aee185471c7969735eb2a8ff5c8e38fac8362 100644 (file)
@@ -6,6 +6,7 @@ import javax.swing.SwingUtilities;
 import net.sf.openrocket.gui.dialogs.BugReportDialog;
 import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.BugException;
 
 
 public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
@@ -36,7 +37,7 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                if (isOutOfMemoryError(throwable)) {
                        memoryReserve = null;
                        handling = false;
-                       log.warn("Out of memory error detected", throwable);
+                       log.error("Out of memory error detected", throwable);
                }
                
                if (isNonFatalJREBug(throwable)) {
@@ -44,7 +45,7 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                        return;
                }
                
-               log.warn("Handling uncaught exception on thread=" + thread, throwable);
+               log.error("Handling uncaught exception on thread=" + thread, throwable);
                throwable.printStackTrace();
                
                if (handling) {
@@ -121,18 +122,15 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                if (!(exception instanceof InternalException)) {
                        log.error(1, "Error occurred", exception);
                }
-               final ExceptionHandler handler = instance;
                final Thread thread = Thread.currentThread();
+               final ExceptionHandler handler = instance;
+               
+               if (handler == null) {
+                       // Not initialized, throw the exception
+                       throw new BugException("Error condition before exception handling has been initialized", exception);
+               }
                
                try {
-                       
-                       if (handler == null) {
-                               // Not initialized, simply print the exception
-                               log.error("Exception handler not initialized");
-                               exception.printStackTrace();
-                               return;
-                       }
-                       
                        if (SwingUtilities.isEventDispatchThread()) {
                                log.info("Running in EDT, showing dialog");
                                handler.showDialog(thread, exception);
diff --git a/src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java b/src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java
new file mode 100644 (file)
index 0000000..e680c85
--- /dev/null
@@ -0,0 +1,93 @@
+package net.sf.openrocket.gui.main.componenttree;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+
+import javax.swing.DropMode;
+import javax.swing.Icon;
+import javax.swing.JTree;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+
+
+public class ComponentTree extends JTree {
+       private static final long serialVersionUID = 1L;
+       
+       public ComponentTree(OpenRocketDocument document) {
+               super();
+               this.setModel(new ComponentTreeModel(document.getRocket(), this));
+               this.setToggleClickCount(0);
+               
+               javax.swing.plaf.basic.BasicTreeUI ui = new javax.swing.plaf.basic.BasicTreeUI();
+               this.setUI(ui);
+               
+               ui.setExpandedIcon(TreeIcon.MINUS);
+               ui.setCollapsedIcon(TreeIcon.PLUS);
+               
+               ui.setLeftChildIndent(15);
+               
+
+               setBackground(Color.WHITE);
+               setShowsRootHandles(false);
+               
+               setCellRenderer(new ComponentTreeRenderer());
+               
+               this.setDragEnabled(true);
+               this.setDropMode(DropMode.INSERT);
+               this.setTransferHandler(new ComponentTreeTransferHandler(document));
+               
+               // Expand whole tree by default
+               expandTree();
+       }
+       
+       public void expandTree() {
+               for (int i = 0; i < getRowCount(); i++)
+                       expandRow(i);
+               
+       }
+       
+       private static class TreeIcon implements Icon {
+               public static final Icon PLUS = new TreeIcon(true);
+               public static final Icon MINUS = new TreeIcon(false);
+               
+               // Implementation:
+               
+               private final static int width = 9;
+               private final static int height = 9;
+               private final static BasicStroke stroke = new BasicStroke(2);
+               private boolean plus;
+               
+               private TreeIcon(boolean plus) {
+                       this.plus = plus;
+               }
+               
+               public void paintIcon(Component c, Graphics g, int x, int y) {
+                       Graphics2D g2 = (Graphics2D) g.create();
+                       
+                       g2.setColor(Color.WHITE);
+                       g2.fillRect(x, y, width, height);
+                       
+                       g2.setColor(Color.DARK_GRAY);
+                       g2.drawRect(x, y, width, height);
+                       
+                       g2.setStroke(stroke);
+                       g2.drawLine(x + 3, y + (height + 1) / 2, x + width - 2, y + (height + 1) / 2);
+                       if (plus)
+                               g2.drawLine(x + (width + 1) / 2, y + 3, x + (width + 1) / 2, y + height - 2);
+                       
+                       g2.dispose();
+               }
+               
+               public int getIconWidth() {
+                       return width;
+               }
+               
+               public int getIconHeight() {
+                       return height;
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java b/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java
new file mode 100644 (file)
index 0000000..f35222c
--- /dev/null
@@ -0,0 +1,252 @@
+package net.sf.openrocket.gui.main.componenttree;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JTree;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.BugException;
+
+
+/**
+ * A TreeModel that implements viewing of the rocket tree structure.
+ * This transforms the internal view (which has nested Stages) into the user-view
+ * (which has parallel Stages).
+ * 
+ * To view with the internal structure, switch to using BareComponentTreeModel in
+ * ComponentTree.java.  NOTE: This class's makeTreePath will still be used, which
+ * will create illegal paths, which results in problems with selections. 
+ * 
+ * TODO: MEDIUM: When converting a component to another component this model given 
+ * outdated information, since it uses the components themselves as the nodes.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ComponentTreeModel implements TreeModel, ComponentChangeListener {
+       ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
+       
+       private final RocketComponent root;
+       private final JTree tree;
+       
+       public ComponentTreeModel(RocketComponent root, JTree tree) {
+               this.root = root;
+               this.tree = tree;
+               root.addComponentChangeListener(this);
+       }
+       
+       
+       public Object getChild(Object parent, int index) {
+               RocketComponent component = (RocketComponent) parent;
+               
+               try {
+                       return component.getChild(index);
+               } catch (IndexOutOfBoundsException e) {
+                       return null;
+               }
+       }
+       
+       
+       public int getChildCount(Object parent) {
+               RocketComponent c = (RocketComponent) parent;
+               
+               return c.getChildCount();
+       }
+       
+       
+       public int getIndexOfChild(Object parent, Object child) {
+               if (parent == null || child == null)
+                       return -1;
+               
+               RocketComponent p = (RocketComponent) parent;
+               RocketComponent c = (RocketComponent) child;
+               
+               return p.getChildPosition(c);
+       }
+       
+       public Object getRoot() {
+               return root;
+       }
+       
+       public boolean isLeaf(Object node) {
+               return !((RocketComponent) node).allowsChildren();
+       }
+       
+       public void addTreeModelListener(TreeModelListener l) {
+               listeners.add(l);
+       }
+       
+       public void removeTreeModelListener(TreeModelListener l) {
+               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);
+               Object[] l = listeners.toArray();
+               for (int i = 0; i < l.length; i++)
+                       ((TreeModelListener) l[i]).treeNodesChanged(e);
+       }
+       
+       
+       @SuppressWarnings("unused")
+       private void printStructure(TreePath p, int level) {
+               String indent = "";
+               for (int i = 0; i < level; i++)
+                       indent += "  ";
+               System.out.println(indent + p +
+                               ": isVisible:" + tree.isVisible(p) +
+                               " isCollapsed:" + tree.isCollapsed(p) +
+                               " isExpanded:" + tree.isExpanded(p));
+               Object parent = p.getLastPathComponent();
+               for (int i = 0; i < getChildCount(parent); i++) {
+                       Object child = getChild(parent, i);
+                       TreePath path = makeTreePath((RocketComponent) child);
+                       printStructure(path, level + 1);
+               }
+       }
+       
+       
+       private void fireTreeStructureChanged(RocketComponent source) {
+               Object[] path = { root };
+               
+
+               // Get currently expanded path IDs
+               Enumeration<TreePath> enumer = tree.getExpandedDescendants(new TreePath(path));
+               ArrayList<String> expanded = new ArrayList<String>();
+               if (enumer != null) {
+                       while (enumer.hasMoreElements()) {
+                               TreePath p = enumer.nextElement();
+                               expanded.add(((RocketComponent) p.getLastPathComponent()).getID());
+                       }
+               }
+               
+               // Send structure change event
+               TreeModelEvent e = new TreeModelEvent(this, path);
+               Object[] l = listeners.toArray();
+               for (int i = 0; i < l.length; i++)
+                       ((TreeModelListener) l[i]).treeStructureChanged(e);
+               
+               // Re-expand the paths
+               for (String id : expanded) {
+                       RocketComponent c = root.findComponent(id);
+                       if (c == null)
+                               continue;
+                       tree.expandPath(makeTreePath(c));
+               }
+               if (source != null) {
+                       TreePath p = makeTreePath(source);
+                       tree.makeVisible(p);
+                       tree.expandPath(p);
+               }
+       }
+       
+       public void valueForPathChanged(TreePath path, Object newValue) {
+               System.err.println("ERROR: valueForPathChanged called?!");
+       }
+       
+       
+       public void componentChanged(ComponentChangeEvent e) {
+               if (e.isTreeChange() || e.isUndoChange()) {
+                       // Tree must be fully updated also in case of an undo change 
+                       fireTreeStructureChanged((RocketComponent) e.getSource());
+                       if (e.isTreeChange() && e.isUndoChange()) {
+                               // If the undo has changed the tree structure, some elements may be hidden
+                               // unnecessarily
+                               // TODO: LOW: Could this be performed better?
+                               expandAll();
+                       }
+               } else if (e.isOtherChange()) {
+                       fireTreeNodeChanged(e.getSource());
+               }
+       }
+       
+       public void expandAll() {
+               Iterator<RocketComponent> iterator = root.deepIterator();
+               while (iterator.hasNext()) {
+                       tree.makeVisible(makeTreePath(iterator.next()));
+               }
+       }
+       
+       
+       /**
+        * Return the rocket component that a TreePath object is referring to.
+        * 
+        * @param path  the TreePath
+        * @return              the RocketComponent the path is referring to.
+        * @throws NullPointerException         if path is null
+        * @throws BugException                         if the path does not refer to a RocketComponent
+        */
+       public static RocketComponent componentFromPath(TreePath path) {
+               Object last = path.getLastPathComponent();
+               if (!(last instanceof RocketComponent)) {
+                       throw new BugException("Tree path does not refer to a RocketComponent: " + path.toString());
+               }
+               return (RocketComponent) last;
+       }
+       
+       
+       /**
+        * Return a TreePath corresponding to the specified rocket component.
+        * 
+        * @param component             the rocket component
+        * @return                              a TreePath corresponding to this RocketComponent
+        */
+       public static TreePath makeTreePath(RocketComponent component) {
+               if (component == null) {
+                       throw new NullPointerException();
+               }
+               
+               RocketComponent c = component;
+               
+               List<RocketComponent> list = new LinkedList<RocketComponent>();
+               
+               while (c != null) {
+                       list.add(0, c);
+                       c = c.getParent();
+               }
+               
+               return new TreePath(list.toArray());
+       }
+       
+       
+       /**
+        * Return a string describing the path, using component normal names.
+        * 
+        * @param treePath      the tree path
+        * @return                      a string representation
+        */
+       public static String pathToString(TreePath treePath) {
+               StringBuffer sb = new StringBuffer();
+               sb.append("[");
+               for (Object o : treePath.getPath()) {
+                       if (sb.length() > 1) {
+                               sb.append("; ");
+                       }
+                       if (o instanceof RocketComponent) {
+                               sb.append(((RocketComponent) o).getComponentName());
+                       } else {
+                               sb.append(o.toString());
+                       }
+               }
+               sb.append("]");
+               return sb.toString();
+       }
+}
diff --git a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java b/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java
new file mode 100644 (file)
index 0000000..75fb293
--- /dev/null
@@ -0,0 +1,31 @@
+package net.sf.openrocket.gui.main.componenttree;
+
+
+
+import java.awt.Component;
+
+import javax.swing.JTree;
+import javax.swing.tree.DefaultTreeCellRenderer;
+
+import net.sf.openrocket.gui.main.ComponentIcons;
+
+public class ComponentTreeRenderer extends DefaultTreeCellRenderer {
+
+    @Override
+       public Component getTreeCellRendererComponent(
+            JTree tree,
+            Object value,
+            boolean sel,
+            boolean expanded,
+            boolean leaf,
+            int row,
+            boolean hasFocus) {
+
+       super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+       
+       setIcon(ComponentIcons.getSmallIcon(value.getClass()));
+       
+       return this;
+    }
+       
+}
diff --git a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java b/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java
new file mode 100644 (file)
index 0000000..f225f5e
--- /dev/null
@@ -0,0 +1,357 @@
+package net.sf.openrocket.gui.main.componenttree;
+
+import java.awt.Point;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.swing.JComponent;
+import javax.swing.JTree;
+import javax.swing.SwingUtilities;
+import javax.swing.TransferHandler;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.main.ExceptionHandler;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.BugException;
+
+/**
+ * A TransferHandler that handles dragging components from and to a ComponentTree.
+ * Supports both moving and copying (only copying when dragging to a different rocket).
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ComponentTreeTransferHandler extends TransferHandler {
+       
+       private static final LogHelper log = Application.getLogger();
+       
+       private final OpenRocketDocument document;
+       
+       /**
+        * Sole constructor.
+        * 
+        * @param document      the document this handler will drop to, used for undo actions.
+        */
+       public ComponentTreeTransferHandler(OpenRocketDocument document) {
+               this.document = document;
+       }
+       
+       @Override
+       public int getSourceActions(JComponent comp) {
+               return COPY_OR_MOVE;
+       }
+       
+       @Override
+       public Transferable createTransferable(JComponent component) {
+               if (!(component instanceof JTree)) {
+                       throw new BugException("TransferHandler called with component " + component);
+               }
+               
+               JTree tree = (JTree) component;
+               TreePath path = tree.getSelectionPath();
+               if (path == null) {
+                       return null;
+               }
+               
+               RocketComponent c = ComponentTreeModel.componentFromPath(path);
+               if (c instanceof Rocket) {
+                       log.info("Attempting to create transferable from Rocket");
+                       return null;
+               }
+               
+               log.info("Creating transferable from component " + c.getComponentName());
+               return new RocketComponentTransferable(c);
+       }
+       
+       
+
+
+       @Override
+       public void exportDone(JComponent comp, Transferable trans, int action) {
+               // Removal from the old place is implemented already in import, so do nothing
+       }
+       
+       
+
+       @Override
+       public boolean canImport(TransferHandler.TransferSupport support) {
+               SourceTarget data = getSourceAndTarget(support);
+               
+               if (data == null) {
+                       return false;
+               }
+               
+               boolean allowed = data.destParent.isCompatible(data.child);
+               log.verbose("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed);
+               
+               // If drag-dropping to another rocket always copy
+               if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) {
+                       support.setDropAction(COPY);
+               }
+               
+               return allowed;
+       }
+       
+       
+
+       @Override
+       public boolean importData(TransferHandler.TransferSupport support) {
+               
+               // Sun JRE silently ignores any RuntimeExceptions in importData, yeech!
+               try {
+                       
+                       SourceTarget data = getSourceAndTarget(support);
+                       
+                       // Check what action to perform
+                       int action = support.getDropAction();
+                       if (data.srcParent.getRoot() != data.destParent.getRoot()) {
+                               // If drag-dropping to another rocket always copy
+                               log.info("Performing DnD between different rockets, forcing copy action");
+                               action = TransferHandler.COPY;
+                       }
+                       
+
+                       // Check whether move action would be a no-op
+                       if ((action == MOVE) && (data.srcParent == data.destParent) &&
+                                       (data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) {
+                               log.user("Dropped component at the same place as previously: " + data);
+                               return false;
+                       }
+                       
+
+                       switch (action) {
+                       case MOVE:
+                               log.user("Performing DnD move operation: " + data);
+                               
+                               // If parents are the same, check whether removing the child changes the insert position
+                               int index = data.destIndex;
+                               if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) {
+                                       index--;
+                               }
+                               
+                               // Mark undo and freeze rocket.  src and dest are in same rocket, need to freeze only one
+                               try {
+                                       document.startUndo("Move component");
+                                       try {
+                                               data.srcParent.getRocket().freeze();
+                                               data.srcParent.removeChild(data.srcIndex);
+                                               data.destParent.addChild(data.child, index);
+                                       } finally {
+                                               data.srcParent.getRocket().thaw();
+                                       }
+                               } finally {
+                                       document.stopUndo();
+                               }
+                               return true;
+                               
+                       case COPY:
+                               log.user("Performing DnD copy operation: " + data);
+                               RocketComponent copy = data.child.copy();
+                               try {
+                                       document.startUndo("Copy component");
+                                       data.destParent.addChild(copy, data.destIndex);
+                               } finally {
+                                       document.stopUndo();
+                               }
+                               return true;
+                               
+                       default:
+                               log.warn("Unknown transfer action " + action);
+                               return false;
+                       }
+                       
+               } catch (final RuntimeException e) {
+                       // Open error dialog later if an exception has occurred
+                       SwingUtilities.invokeLater(new Runnable() {
+                               @Override
+                               public void run() {
+                                       ExceptionHandler.handleErrorCondition(e);
+                               }
+                       });
+                       return false;
+               }
+       }
+       
+       
+
+       /**
+        * Fetch the source and target for the DnD action.  This method does not perform
+        * checks on whether this action is allowed based on component positioning rules.
+        * 
+        * @param support       the transfer support
+        * @return                      the source and targer, or <code>null</code> if invalid.
+        */
+       private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) {
+               // We currently only support drop, not paste
+               if (!support.isDrop()) {
+                       log.warn("Import action is not a drop action");
+                       return null;
+               }
+               
+               // we only import RocketComponentTransferable
+               if (!support.isDataFlavorSupported(RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR)) {
+                       log.debug("Attempting to import data with data flavors " +
+                                       Arrays.toString(support.getTransferable().getTransferDataFlavors()));
+                       return null;
+               }
+               
+               // Fetch the drop location and convert it to work around bug 6560955
+               JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
+               if (dl.getPath() == null) {
+                       log.debug("No drop path location available");
+                       return null;
+               }
+               MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl);
+               
+
+               // Fetch the transferred component (child component)
+               Transferable transferable = support.getTransferable();
+               RocketComponent child;
+               
+               try {
+                       child = (RocketComponent) transferable.getTransferData(
+                                       RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR);
+               } catch (IOException e) {
+                       throw new BugException(e);
+               } catch (UnsupportedFlavorException e) {
+                       throw new BugException(e);
+               }
+               
+
+               // Get the source component & index
+               RocketComponent srcParent = child.getParent();
+               if (srcParent == null) {
+                       log.debug("Attempting to drag root component");
+                       return null;
+               }
+               int srcIndex = srcParent.getChildPosition(child);
+               
+
+               // Get destination component & index
+               RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path);
+               int destIndex = location.index;
+               if (destIndex < 0) {
+                       destIndex = 0;
+               }
+               
+               return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child);
+       }
+       
+       private class SourceTarget {
+               private final RocketComponent srcParent;
+               private final int srcIndex;
+               private final RocketComponent destParent;
+               private final int destIndex;
+               private final RocketComponent child;
+               
+               public SourceTarget(RocketComponent srcParent, int srcIndex, RocketComponent destParent, int destIndex,
+                               RocketComponent child) {
+                       this.srcParent = srcParent;
+                       this.srcIndex = srcIndex;
+                       this.destParent = destParent;
+                       this.destIndex = destIndex;
+                       this.child = child;
+               }
+               
+               @Override
+               public String toString() {
+                       return "[" +
+                                       "srcParent=" + srcParent.getComponentName() +
+                                       ", srcIndex=" + srcIndex +
+                                       ", destParent=" + destParent.getComponentName() +
+                                       ", destIndex=" + destIndex +
+                                       ", child=" + child.getComponentName() +
+                                       "]";
+               }
+               
+       }
+       
+       
+
+       /**
+        * Convert the JTree drop location in order to work around bug 6560955
+        * (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955).
+        * <p>
+        * This method analyzes whether the user is dropping on top of the last component
+        * of a subtree or the next item in the tree.  The case to fix must fulfill the following
+        * requirements:
+        * <ul>
+        *      <li> The node before the current insertion node is not a leaf node
+        *  <li> The drop point is on top of the last node of that node
+        * </ul>
+        * <p>
+        * This does not fix the visual clue provided to the user, but fixes the actual drop location.
+        * 
+        * @param tree          the JTree in question
+        * @param location      the original drop location
+        * @return                      the updated drop location
+        */
+       private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) {
+               
+               final TreePath originalPath = location.getPath();
+               final int originalIndex = location.getChildIndex();
+               
+               if (originalPath == null || originalIndex <= 0) {
+                       return new MyDropLocation(location);
+               }
+               
+               // Check whether previous node is a leaf node
+               TreeModel model = tree.getModel();
+               Object previousNode = model.getChild(originalPath.getLastPathComponent(), originalIndex - 1);
+               if (model.isLeaf(previousNode)) {
+                       return new MyDropLocation(location);
+               }
+               
+               // Find node on top of which the drop occurred
+               Point point = location.getDropPoint();
+               TreePath dropPath = tree.getPathForLocation(point.x, point.y);
+               if (dropPath == null) {
+                       return new MyDropLocation(location);
+               }
+               
+               // Check whether previousNode is in the ancestry of the actual drop location
+               boolean inAncestry = false;
+               for (Object o : dropPath.getPath()) {
+                       if (o == previousNode) {
+                               inAncestry = true;
+                               break;
+                       }
+               }
+               if (!inAncestry) {
+                       return new MyDropLocation(location);
+               }
+               
+               // The bug has occurred - insert after the actual drop location
+               TreePath correctInsertPath = dropPath.getParentPath();
+               int correctInsertIndex = model.getIndexOfChild(correctInsertPath.getLastPathComponent(),
+                               dropPath.getLastPathComponent()) + 1;
+               
+               log.verbose("Working around Sun JRE bug 6560955: " +
+                               "converted path=" + ComponentTreeModel.pathToString(originalPath) + " index=" + originalIndex +
+                               " into path=" + ComponentTreeModel.pathToString(correctInsertPath) +
+                               " index=" + correctInsertIndex);
+               
+               return new MyDropLocation(correctInsertPath, correctInsertIndex);
+       }
+       
+       private class MyDropLocation {
+               private final TreePath path;
+               private final int index;
+               
+               public MyDropLocation(JTree.DropLocation location) {
+                       this(location.getPath(), location.getChildIndex());
+               }
+               
+               public MyDropLocation(TreePath path, int index) {
+                       this.path = path;
+                       this.index = index;
+               }
+               
+       }
+}
diff --git a/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java b/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java
new file mode 100644 (file)
index 0000000..e9379da
--- /dev/null
@@ -0,0 +1,47 @@
+package net.sf.openrocket.gui.main.componenttree;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+/**
+ * A transferable that provides a reference to a (JVM-local) RocketComponent object.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketComponentTransferable implements Transferable {
+       
+       public static final DataFlavor ROCKET_COMPONENT_DATA_FLAVOR = new DataFlavor(
+                       DataFlavor.javaJVMLocalObjectMimeType + "; class=" + RocketComponent.class.getCanonicalName(),
+                       "OpenRocket component");
+       
+
+       private final RocketComponent component;
+       
+       public RocketComponentTransferable(RocketComponent component) {
+               this.component = component;
+       }
+       
+       
+       @Override
+       public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
+               if (!isDataFlavorSupported(flavor)) {
+                       throw new UnsupportedFlavorException(flavor);
+               }
+               return component;
+       }
+       
+       @Override
+       public DataFlavor[] getTransferDataFlavors() {
+               return new DataFlavor[] { ROCKET_COMPONENT_DATA_FLAVOR };
+       }
+       
+       @Override
+       public boolean isDataFlavorSupported(DataFlavor flavor) {
+               return flavor.equals(ROCKET_COMPONENT_DATA_FLAVOR);
+       }
+       
+}
index 77d5338e35f585207e20f6965924145ccb1deaee..de60e12558c460ce3993c056f95bdeac4e9d0a7a 100644 (file)
@@ -342,13 +342,12 @@ public class PlotDialog extends JDialog {
                                        tindex = 0;
                                        a = 0;
                                } else {
-                                       assert (tindex > 0);
                                        tindex--;
                                        double t1 = time.get(tindex);
                                        double t2 = time.get(tindex + 1);
                                        
                                        if ((t1 > t) || (t2 < t)) {
-                                               throw new BugException("BUG: t1=" + t1 + " t2=" + t2 + " t=" + t);
+                                               throw new BugException("t1=" + t1 + " t2=" + t2 + " t=" + t);
                                        }
                                        
                                        if (MathUtil.equals(t1, t2)) {
index 6a8e68d035948d53c95129f4e8fd0da6577f3a12..71ac8a5921b99a0cd2aa47c6ccfd59e2d692ae33 100644 (file)
@@ -47,8 +47,8 @@ import net.sf.openrocket.gui.figureelements.CGCaret;
 import net.sf.openrocket.gui.figureelements.CPCaret;
 import net.sf.openrocket.gui.figureelements.Caret;
 import net.sf.openrocket.gui.figureelements.RocketInfo;
-import net.sf.openrocket.gui.main.ComponentTreeModel;
 import net.sf.openrocket.gui.main.SimulationWorker;
+import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
 import net.sf.openrocket.masscalc.BasicMassCalculator;
 import net.sf.openrocket.masscalc.MassCalculator;
 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
@@ -560,7 +560,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
                // Start calculation process
                extraText.setCalculatingData(true);
                
-               Rocket duplicate = configuration.getRocket().copy();
+               Rocket duplicate = (Rocket) configuration.getRocket().copy();
                Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
                simulation.getConditions().setMotorConfigurationID(
                                configuration.getMotorConfigurationID());
index a51c0a92199b6880f92fc71d3e7d16c57d96c689..77809e07de59334594ce8c502aaf923ecb9eebfe 100644 (file)
@@ -23,13 +23,13 @@ public abstract class LogHelper {
        /**
         * Level from which upward a TraceException is added to the log lines.
         */
-       private static final LogLevel TRACING_LOG_LEVEL = 
-               LogLevel.fromString(System.getProperty("openrocket.log.tracelevel"), LogLevel.INFO);
+       private static final LogLevel TRACING_LOG_LEVEL =
+                       LogLevel.fromString(System.getProperty("openrocket.log.tracelevel"), LogLevel.INFO);
        
        private static final DelegatorLogger delegator = new DelegatorLogger();
        
        
-       
+
        /**
         * Get the logger to be used in logging.
         * 
@@ -43,13 +43,53 @@ public abstract class LogHelper {
 
        /**
         * Log a LogLine object.  This method needs to be able to cope with multiple threads
-        * accessing it concurrently (for example being synchronized).
+        * accessing it concurrently (for example by being synchronized).
         * 
         * @param line  the LogLine to log.
         */
        public abstract void log(LogLine line);
        
        
+       /**
+        * Log using VBOSE level.
+        * 
+        * @param message       the logged message (may be null).
+        */
+       public void verbose(String message) {
+               log(createLogLine(0, LogLevel.VBOSE, message, null));
+       }
+       
+       /**
+        * Log using VBOSE level.
+        * 
+        * @param message       the logged message (may be null).
+        * @param cause         the causing exception (may be null).
+        */
+       public void verbose(String message, Throwable cause) {
+               log(createLogLine(0, LogLevel.VBOSE, message, cause));
+       }
+       
+       /**
+        * Log using VBOSE level.
+        * 
+        * @param levels        number of additional levels of stack trace to include.
+        * @param message       the logged message (may be null).
+        */
+       public void verbose(int levels, String message) {
+               log(createLogLine(levels, LogLevel.VBOSE, message, null));
+       }
+       
+       /**
+        * Log using VBOSE level.
+        * 
+        * @param levels        number of additional levels of stack trace to include.
+        * @param message       the logged message (may be null).
+        * @param cause         the causing exception (may be null).
+        */
+       public void verbose(int levels, String message, Throwable cause) {
+               log(createLogLine(levels, LogLevel.VBOSE, message, cause));
+       }
+       
        
        /**
         * Log using DEBUG level.
@@ -256,7 +296,7 @@ public abstract class LogHelper {
        }
        
        
-       
+
        /**
         * Log using the provided log level.
         * 
@@ -302,7 +342,7 @@ public abstract class LogHelper {
        }
        
        
-       
+
        /**
         * Instantiates, logs and throws a BugException.  The message is logged at
         * ERROR level.
@@ -336,7 +376,7 @@ public abstract class LogHelper {
        
        
 
-       
+
        /**
         * Create a LogLine object from the provided information.  This method must be
         * called directly from the called method in order for the trace position
@@ -349,7 +389,7 @@ public abstract class LogHelper {
         * 
         * @return                                      a LogLine populated with all necessary fields.
         */
-       private LogLine createLogLine(int additionalLevels, LogLevel level, String message, 
+       private LogLine createLogLine(int additionalLevels, LogLevel level, String message,
                        Throwable cause) {
                TraceException trace;
                if (level.atLeast(TRACING_LOG_LEVEL)) {
index 19842c390544bba3713d0289503e7328df02f454..74235b360f7d5027db9f3d82732b4494ab9a7d4f 100644 (file)
@@ -16,11 +16,13 @@ public enum LogLevel {
         * No ERROR level events _should_ occur while running the program.
         */
        ERROR,
+
        /** 
         * Level for indicating error conditions or atypical events that can occur during
         * normal operation (errors while loading files, weird computation results etc).
         */
        WARN,
+
        /** 
         * Level for logging user actions (adding and modifying components, running
         * simulations etc).  A user action should be logged as soon as possible on this
@@ -28,24 +30,35 @@ public enum LogLevel {
         * user actions from a bounded log buffer.
         */
        USER,
+
        /**
         * Level for indicating general level actions the software is performing and
         * other notable events during execution (dialogs shown, simulations run etc).
         */
        INFO,
+
        /**
-        * Level for indicating mid-results and other debugging information.  No debug
-        * logging should be performed in performance-intensive places to avoid slowing
-        * the system.  On the other hand for example cached results should be logged.
+        * Level for indicating mid-results, outcomes of methods and other debugging 
+        * information.  The data logged should be of value when analyzing error
+        * conditions and what has caused them.  Places that are called repeatedly
+        * during e.g. flight simulation should use the VBOSE level instead.
         */
-       DEBUG;
+       DEBUG,
 
+       /**
+        * Level of verbose debug logging to be used in areas which are called repeatedly,
+        * such as computational methods used in simulations.  This level is separated to
+        * allow filtering out the verbose logs generated during simulations, DnD etc.
+        * from the normal debug logs.
+        */
+       VBOSE;
        
+
        /** The maximum length of a level textual description */
        public static final int LENGTH;
        static {
                int length = 0;
-               for (LogLevel l: LogLevel.values()) {
+               for (LogLevel l : LogLevel.values()) {
                        length = Math.max(length, l.toString().length());
                }
                LENGTH = length;
@@ -58,7 +71,7 @@ public enum LogLevel {
        public boolean atLeast(LogLevel level) {
                return this.compareTo(level) <= 0;
        }
-
+       
        /**
         * Return true if this log level is of a priority greater than that of
         * <code>level</code>.
@@ -77,7 +90,7 @@ public enum LogLevel {
         * @return                              the corresponding log level, of defaultLevel.
         */
        public static LogLevel fromString(String value, LogLevel defaultLevel) {
-
+               
                // Normalize the string
                if (value == null) {
                        return defaultLevel;
@@ -88,7 +101,7 @@ public enum LogLevel {
                LogLevel level = defaultLevel;
                if (value.equals("ALL")) {
                        LogLevel[] values = LogLevel.values();
-                       level = values[values.length-1];
+                       level = values[values.length - 1];
                } else {
                        try {
                                level = LogLevel.valueOf(value);
index 752a09610d350575620cff4f1bd8c6a2735edd49..8468a1ed4fc8a9bb20230d54815293a92c1edd01 100644 (file)
@@ -18,7 +18,7 @@ package net.sf.openrocket.logging;
 public class TraceException extends Exception {
        
        private static final String STANDARD_PACKAGE_PREFIX = "net.sf.openrocket.";
-
+       
        private final int minLevel;
        private final int maxLevel;
        private volatile String message = null;
@@ -55,13 +55,13 @@ public class TraceException extends Exception {
         */
        public TraceException(int minLevel, int maxLevel) {
                if (minLevel > maxLevel || minLevel < 0) {
-                       throw new IllegalArgumentException("minLevel="+minLevel+" maxLevel="+maxLevel);
+                       throw new IllegalArgumentException("minLevel=" + minLevel + " maxLevel=" + maxLevel);
                }
                this.minLevel = minLevel;
                this.maxLevel = maxLevel;
        }
-
-
+       
+       
        /**
         * Get the description of the code position as provided in the constructor.
         */
@@ -69,13 +69,13 @@ public class TraceException extends Exception {
        public String getMessage() {
                if (message == null) {
                        StackTraceElement[] elements = this.getStackTrace();
-
+                       
                        StringBuilder sb = new StringBuilder();
                        if (minLevel < elements.length) {
                                
                                sb.append("(");
                                sb.append(toString(elements[minLevel]));
-                               for (int i=minLevel+1; i <= maxLevel; i++) {
+                               for (int i = minLevel + 1; i <= maxLevel; i++) {
                                        if (i < elements.length) {
                                                sb.append(' ').append(toString(elements[i]));
                                        }
@@ -87,10 +87,10 @@ public class TraceException extends Exception {
                                sb.append("(no stack trace)");
                                
                        } else {
-
+                               
                                sb.append('(');
                                sb.append(toString(elements[0]));
-                               for (int i=1; i < elements.length; i++) {
+                               for (int i = 1; i < elements.length; i++) {
                                        sb.append(' ').append(toString(elements[i]));
                                }
                                sb.append(" level=").append(minLevel).append(')');
index 056d3321ba235b6c3f923b4634fb7c4995ca4425..a988ac8edc87cc7c79a18fdf9009c6aa8ec3551d 100644 (file)
@@ -119,7 +119,7 @@ public class AtmosphericConditions implements Cloneable, Monitorable {
                try {
                        return (AtmosphericConditions) super.clone();
                } catch (CloneNotSupportedException e) {
-                       throw new BugException("BUG:  CloneNotSupportedException encountered!");
+                       throw new BugException("CloneNotSupportedException encountered!");
                }
        }
        
diff --git a/src/net/sf/openrocket/optimization/Function.java b/src/net/sf/openrocket/optimization/Function.java
deleted file mode 100644 (file)
index 636e8a4..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-package net.sf.openrocket.optimization;
-
-/**
- * An interface defining an optimizable function.
- * <p>
- * Some function optimizers require that the function is thread-safe.
- * 
- * @author Sampo Niskanen <sampo.niskanen@iki.fi>
- */
-public interface Function {
-       
-       /**
-        * Evaluate the function at the specified point.
-        * <p>
-        * If the function evaluation is slow, then this method should abort the computation if
-        * the thread is interrupted.
-        * 
-        * @param point         the point at which to evaluate the function.
-        * @return                      the function value.
-        * @throws InterruptedException         if the thread was interrupted before function evaluation was completed.
-        */
-       public double evaluate(Point point) throws InterruptedException;
-       
-       
-       /**
-        * Return a cached value of the function at the specified point.  This allows efficient
-        * caching of old values even between calls to optimization methods.  This method should
-        * NOT evaluate the function except in special cases (e.g. the point is outside of the
-        * function domain).
-        * <p>
-        * Note that it is allowed to always allowed to return <code>Double.NaN</code>, especially
-        * for functions that are fast to evaluate.
-        * 
-        * @param point         the point of function evaluation.
-        * @return                      the function value, or <code>Double.NaN</code> if the function value has not been
-        *                                      evaluated at this point.
-        */
-       public double preComputed(Point point);
-       
-}
diff --git a/src/net/sf/openrocket/optimization/FunctionCache.java b/src/net/sf/openrocket/optimization/FunctionCache.java
deleted file mode 100644 (file)
index faa080b..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.sf.openrocket.optimization;
-
-public interface FunctionCache {
-       
-       public double getValue(Point point);
-       
-       public void clearCache();
-       
-       public Function getFunction();
-       
-       public void setFunction(Function function);
-       
-}
diff --git a/src/net/sf/openrocket/optimization/FunctionCacheComparator.java b/src/net/sf/openrocket/optimization/FunctionCacheComparator.java
deleted file mode 100644 (file)
index 11e7229..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.sf.openrocket.optimization;
-
-import java.util.Comparator;
-
-/**
- * A comparator that orders Points in a function value order, smallest first.
- * 
- * @author Sampo Niskanen <sampo.niskanen@iki.fi>
- */
-public class FunctionCacheComparator implements Comparator<Point> {
-       
-       private final FunctionCache cache;
-       
-       public FunctionCacheComparator(FunctionCache cache) {
-               this.cache = cache;
-       }
-       
-       @Override
-       public int compare(Point o1, Point o2) {
-               double v1 = cache.getValue(o1);
-               double v2 = cache.getValue(o2);
-               
-               return Double.compare(v1, v2);
-       }
-       
-}
diff --git a/src/net/sf/openrocket/optimization/FunctionCallable.java b/src/net/sf/openrocket/optimization/FunctionCallable.java
deleted file mode 100644 (file)
index 3154665..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.sf.openrocket.optimization;
-
-import java.util.concurrent.Callable;
-
-/**
- * A Callable that computes the value of a function at a specific point.
- * 
- * @author Sampo Niskanen <sampo.niskanen@iki.fi>
- */
-public class FunctionCallable implements Callable<Double> {
-       
-       private final Function function;
-       private final Point point;
-       
-       /**
-        * Sole constructor.
-        * 
-        * @param function      the function to evaluate
-        * @param point         the point at which to evaluate the function
-        */
-       public FunctionCallable(Function function, Point point) {
-               this.function = function;
-               this.point = point;
-       }
-       
-       /**
-        * Evaluate the function and return the result.
-        */
-       @Override
-       public Double call() throws InterruptedException {
-               return function.evaluate(point);
-       }
-       
-}
diff --git a/src/net/sf/openrocket/optimization/FunctionDecorator.java b/src/net/sf/openrocket/optimization/FunctionDecorator.java
deleted file mode 100644 (file)
index 0214b2c..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-package net.sf.openrocket.optimization;
-
-public class FunctionDecorator implements Function {
-       
-       private final Function function;
-       
-       public FunctionDecorator(Function function) {
-               this.function = function;
-       }
-       
-       
-       @Override
-       public double evaluate(Point x) throws InterruptedException {
-               return function.evaluate(x);
-       }
-       
-       @Override
-       public double preComputed(Point x) {
-               return function.preComputed(x);
-       }
-       
-}
diff --git a/src/net/sf/openrocket/optimization/FunctionOptimizer.java b/src/net/sf/openrocket/optimization/FunctionOptimizer.java
deleted file mode 100644 (file)
index 8c151ea..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-package net.sf.openrocket.optimization;
-
-public interface FunctionOptimizer {
-       
-       public void optimize(Point initial, OptimizationController control);
-       
-       
-       public Point getOptimumPoint();
-       
-       public double getOptimumValue();
-       
-       
-       public FunctionCache getFunctionCache();
-       
-       public void setFunctionCache(FunctionCache functionCache);
-       
-
-}
diff --git a/src/net/sf/openrocket/optimization/MultidirectionalSearchOptimizer.java b/src/net/sf/openrocket/optimization/MultidirectionalSearchOptimizer.java
deleted file mode 100644 (file)
index 9125c32..0000000
+++ /dev/null
@@ -1,290 +0,0 @@
-package net.sf.openrocket.optimization;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-
-import net.sf.openrocket.logging.LogHelper;
-import net.sf.openrocket.startup.Application;
-import net.sf.openrocket.util.Statistics;
-
-/**
- * A customized implementation of the parallel multidirectional search algorithm by Dennis and Torczon.
- * <p>
- * This is a parallel pattern search optimization algorithm.  The function evaluations are performed
- * using an ExecutorService.  By default a ThreadPoolExecutor is used that has as many thread defined
- * as the system has processors.
- */
-public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Statistics {
-       private static final LogHelper log = Application.getLogger();
-       
-       private List<Point> simplex = new ArrayList<Point>();
-       
-       private ParallelFunctionCache functionExecutor;
-       
-       private boolean useExpansion = false;
-       
-       private int stepCount = 0;
-       private int reflectionAcceptance = 0;
-       private int expansionAcceptance = 0;
-       private int coordinateAcceptance = 0;
-       private int reductionFallback = 0;
-       
-       
-       public MultidirectionalSearchOptimizer() {
-               // No-op
-       }
-       
-       public MultidirectionalSearchOptimizer(ParallelFunctionCache functionCache) {
-               this.functionExecutor = functionCache;
-       }
-       
-       
-
-       @Override
-       public void optimize(Point initial, OptimizationController control) {
-               FunctionCacheComparator comparator = new FunctionCacheComparator(functionExecutor);
-               
-               final List<Point> pattern = SearchPattern.square(initial.dim());
-               log.info("Starting optimization at " + initial + " with pattern " + pattern);
-               
-               try {
-                       
-                       boolean simplexComputed = false;
-                       double step = 0.5;
-                       
-                       // Set up the current simplex
-                       simplex.clear();
-                       simplex.add(initial);
-                       for (Point p : pattern) {
-                               simplex.add(initial.add(p.mul(step)));
-                       }
-                       
-                       // Normal iterations
-                       List<Point> reflection = new ArrayList<Point>(simplex.size());
-                       List<Point> expansion = new ArrayList<Point>(simplex.size());
-                       List<Point> coordinateSearch = new ArrayList<Point>(simplex.size());
-                       Point current;
-                       double currentValue;
-                       do {
-                               
-                               log.debug("Starting optimization step with simplex " + simplex +
-                                               (simplexComputed ? "" : " (not computed)"));
-                               stepCount++;
-                               
-                               if (!simplexComputed) {
-                                       // TODO: Could something be computed in parallel?
-                                       functionExecutor.compute(simplex);
-                                       functionExecutor.waitFor(simplex);
-                                       Collections.sort(simplex, comparator);
-                                       simplexComputed = true;
-                               }
-                               
-                               current = simplex.get(0);
-                               currentValue = functionExecutor.getValue(current);
-                               
-                               /*
-                                * Compute and queue the next points in likely order of usefulness.
-                                * Expansion is unlikely as we're mainly dealing with bounded optimization.
-                                */
-                               createReflection(simplex, reflection);
-                               createCoordinateSearch(current, step, coordinateSearch);
-                               if (useExpansion)
-                                       createExpansion(simplex, expansion);
-                               
-                               functionExecutor.compute(reflection);
-                               functionExecutor.compute(coordinateSearch);
-                               if (useExpansion)
-                                       functionExecutor.compute(expansion);
-                               
-                               // Check reflection acceptance
-                               log.debug("Computing reflection");
-                               functionExecutor.waitFor(reflection);
-                               
-                               if (accept(reflection, currentValue)) {
-                                       
-                                       log.debug("Reflection was successful, aborting coordinate search, " +
-                                                       (useExpansion ? "computing" : "skipping") + " expansion");
-                                       
-                                       functionExecutor.abort(coordinateSearch);
-                                       
-                                       simplex.clear();
-                                       simplex.add(current);
-                                       simplex.addAll(reflection);
-                                       Collections.sort(simplex, comparator);
-                                       
-                                       if (useExpansion) {
-                                               
-                                               /*
-                                                * Assume expansion to be unsuccessful, queue next reflection while computing expansion.
-                                                */
-                                               createReflection(simplex, reflection);
-                                               
-                                               functionExecutor.compute(reflection);
-                                               functionExecutor.waitFor(expansion);
-                                               
-                                               if (accept(expansion, currentValue)) {
-                                                       log.debug("Expansion was successful, aborting reflection");
-                                                       functionExecutor.abort(reflection);
-                                                       
-                                                       simplex.clear();
-                                                       simplex.add(current);
-                                                       simplex.addAll(expansion);
-                                                       step *= 2;
-                                                       Collections.sort(simplex, comparator);
-                                                       expansionAcceptance++;
-                                               } else {
-                                                       log.debug("Expansion failed");
-                                                       reflectionAcceptance++;
-                                               }
-                                               
-                                       } else {
-                                               reflectionAcceptance++;
-                                       }
-                                       
-                               } else {
-                                       
-                                       log.debug("Reflection was unsuccessful, aborting expansion, computing coordinate search");
-                                       
-                                       functionExecutor.abort(expansion);
-                                       
-                                       /*
-                                        * Assume coordinate search to be unsuccessful, queue contraction step while computing.
-                                        */
-                                       halveStep(simplex);
-                                       functionExecutor.compute(simplex);
-                                       functionExecutor.waitFor(coordinateSearch);
-                                       
-                                       if (accept(coordinateSearch, currentValue)) {
-                                               
-                                               log.debug("Coordinate search successful, reseting simplex");
-                                               List<Point> toAbort = new LinkedList<Point>(simplex);
-                                               simplex.clear();
-                                               simplex.add(current);
-                                               for (Point p : pattern) {
-                                                       simplex.add(current.add(p.mul(step)));
-                                               }
-                                               toAbort.removeAll(simplex);
-                                               functionExecutor.abort(toAbort);
-                                               simplexComputed = false;
-                                               coordinateAcceptance++;
-                                               
-                                       } else {
-                                               log.debug("Coordinate search unsuccessful, halving step.");
-                                               step /= 2;
-                                               reductionFallback++;
-                                       }
-                                       
-                               }
-                               
-                               log.debug("Ending optimization step with simplex " + simplex);
-                               
-                               if (Thread.interrupted()) {
-                                       throw new InterruptedException();
-                               }
-                               
-                       } while (control.stepTaken(current, currentValue, simplex.get(0),
-                                       functionExecutor.getValue(simplex.get(0)), step));
-                       
-               } catch (InterruptedException e) {
-                       log.info("Optimization was interrupted with InterruptedException");
-               }
-               
-               log.info("Finishing optimization at point " + simplex.get(0) + " value = " +
-                               functionExecutor.getValue(simplex.get(0)));
-       }
-       
-       
-
-       private void createReflection(List<Point> base, List<Point> reflection) {
-               Point current = base.get(0);
-               reflection.clear();
-               for (int i = 1; i < base.size(); i++) {
-                       Point p = current.mul(2).sub(base.get(i));
-                       reflection.add(p);
-               }
-       }
-       
-       private void createExpansion(List<Point> base, List<Point> expansion) {
-               Point current = base.get(0);
-               expansion.clear();
-               for (int i = 1; i < base.size(); i++) {
-                       Point p = current.mul(3).sub(base.get(i).mul(2));
-                       expansion.add(p);
-               }
-       }
-       
-       private void halveStep(List<Point> base) {
-               Point current = base.get(0);
-               for (int i = 1; i < base.size(); i++) {
-                       Point p = base.get(i);
-                       p = p.add(current).mul(0.5);
-                       base.set(i, p);
-               }
-       }
-       
-       private void createCoordinateSearch(Point current, double step, List<Point> coordinateDirections) {
-               coordinateDirections.clear();
-               for (int i = 0; i < current.dim(); i++) {
-                       Point p = new Point(current.dim());
-                       p = p.set(i, step);
-                       coordinateDirections.add(current.add(p));
-                       coordinateDirections.add(current.sub(p));
-               }
-       }
-       
-       
-       private boolean accept(List<Point> points, double currentValue) {
-               for (Point p : points) {
-                       if (functionExecutor.getValue(p) < currentValue) {
-                               return true;
-                       }
-               }
-               return false;
-       }
-       
-       
-
-       @Override
-       public Point getOptimumPoint() {
-               return simplex.get(0);
-       }
-       
-       @Override
-       public double getOptimumValue() {
-               return functionExecutor.getValue(getOptimumPoint());
-       }
-       
-       @Override
-       public FunctionCache getFunctionCache() {
-               return functionExecutor;
-       }
-       
-       @Override
-       public void setFunctionCache(FunctionCache functionCache) {
-               if (!(functionCache instanceof ParallelFunctionCache)) {
-                       throw new IllegalArgumentException("Function cache needs to be a ParallelFunctionCache: " + functionCache);
-               }
-               this.functionExecutor = (ParallelFunctionCache) functionCache;
-       }
-       
-       @Override
-       public String getStatistics() {
-               return "MultidirectionalSearchOptimizer[stepCount=" + stepCount +
-                               ", reflectionAcceptance=" + reflectionAcceptance +
-                               ", expansionAcceptance=" + expansionAcceptance +
-                               ", coordinateAcceptance=" + coordinateAcceptance +
-                               ", reductionFallback=" + reductionFallback;
-       }
-       
-       @Override
-       public void resetStatistics() {
-               stepCount = 0;
-               reflectionAcceptance = 0;
-               expansionAcceptance = 0;
-               coordinateAcceptance = 0;
-               reductionFallback = 0;
-       }
-       
-}
diff --git a/src/net/sf/openrocket/optimization/MultipleOptimizationController.java b/src/net/sf/openrocket/optimization/MultipleOptimizationController.java
deleted file mode 100644 (file)
index 50e51b9..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-package net.sf.openrocket.optimization;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-/**
- * An OptimizationController that delegates control actions to multiple other controllers.
- * The optimization is stopped if any of the controllers stops it.
- * 
- * @author Sampo Niskanen <sampo.niskanen@iki.fi>
- */
-public class MultipleOptimizationController implements OptimizationController {
-       
-       private final List<OptimizationController> controllers = new ArrayList<OptimizationController>();
-       
-       public MultipleOptimizationController(OptimizationController... controllers) {
-               for (OptimizationController c : controllers) {
-                       this.controllers.add(c);
-               }
-       }
-       
-       public MultipleOptimizationController(Collection<OptimizationController> controllers) {
-               this.controllers.addAll(controllers);
-       }
-       
-       
-       @Override
-       public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) {
-               boolean ret = true;
-               
-               for (OptimizationController c : controllers) {
-                       if (!c.stepTaken(oldPoint, oldValue, newPoint, newValue, stepSize)) {
-                               ret = false;
-                       }
-               }
-               
-               return ret;
-       }
-       
-}
diff --git a/src/net/sf/openrocket/optimization/OptimizationController.java b/src/net/sf/openrocket/optimization/OptimizationController.java
deleted file mode 100644 (file)
index 3146243..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-package net.sf.openrocket.optimization;
-
-public interface OptimizationController {
-       
-       /**
-        * Control for whether to continue the optimization.  This method is called after
-        * every full step taken by the optimization algorithm.
-        * 
-        * @param oldPoint      the old position.
-        * @param oldValue      the value of the function at the old position.
-        * @param newPoint      the new position.
-        * @param newValue      the value of the function at the new position.
-        * @param stepSize      the step length that is used to search for smaller function values (when applicable).
-        * @return                      <code>true</code> to continue optimization, <code>false</code> to stop.
-        */
-       public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue,
-                       double stepSize);
-       
-}
diff --git a/src/net/sf/openrocket/optimization/ParallelExecutorCache.java b/src/net/sf/openrocket/optimization/ParallelExecutorCache.java
deleted file mode 100644 (file)
index d1c4f6b..0000000
+++ /dev/null
@@ -1,226 +0,0 @@
-package net.sf.openrocket.optimization;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A class that evaluates function values in parallel and caches them.
- * This allows pre-calculating possibly required function values beforehand.
- * If values are not required after all, the computation can be aborted assuming
- * the function evaluation supports it.
- * 
- * @author Sampo Niskanen <sampo.niskanen@iki.fi>
- */
-public class ParallelExecutorCache implements ParallelFunctionCache {
-       
-       private final Map<Point, Double> functionCache = new HashMap<Point, Double>();
-       private final Map<Point, Future<Double>> futureMap = new HashMap<Point, Future<Double>>();
-       
-       private ExecutorService executor;
-       
-       private Function function;
-       
-       
-
-       public ParallelExecutorCache() {
-               this(Runtime.getRuntime().availableProcessors());
-       }
-       
-       public ParallelExecutorCache(int threadCount) {
-               executor = new ThreadPoolExecutor(threadCount, threadCount, 60, TimeUnit.SECONDS,
-                               new LinkedBlockingQueue<Runnable>(),
-                               new ThreadFactory() {
-                                       @Override
-                                       public Thread newThread(Runnable r) {
-                                               Thread t = new Thread(r);
-                                               t.setDaemon(true);
-                                               return t;
-                                       }
-                               });
-       }
-       
-       public ParallelExecutorCache(ExecutorService executor) {
-               this.executor = executor;
-       }
-       
-       
-
-       /**
-        * Queue a list of function evaluations at the specified points.
-        * 
-        * @param points        the points at which to evaluate the function.
-        */
-       public void compute(Collection<Point> points) {
-               for (Point p : points) {
-                       compute(p);
-               }
-       }
-       
-       
-       /**
-        * Queue function evaluation for the specified point.
-        * 
-        * @param point         the point at which to evaluate the function.
-        */
-       public void compute(Point point) {
-               if (functionCache.containsKey(point)) {
-                       // Function has already been evaluated at the point
-                       return;
-               }
-               
-               if (futureMap.containsKey(point)) {
-                       // Function is being evaluated at the point
-                       return;
-               }
-               
-               double value = function.preComputed(point);
-               if (!Double.isNaN(value)) {
-                       // Function value was in function cache
-                       functionCache.put(point, value);
-                       return;
-               }
-               
-               // Submit point for evaluation
-               FunctionCallable callable = new FunctionCallable(function, point);
-               Future<Double> future = executor.submit(callable);
-               futureMap.put(point, future);
-       }
-       
-       
-       /**
-        * Wait for a collection of points to be computed.  After calling this method
-        * the function values are available by calling XXX
-        * 
-        * @param points        the points to wait for.
-        * @throws InterruptedException         if this thread was interrupted while waiting.
-        */
-       public void waitFor(Collection<Point> points) throws InterruptedException {
-               for (Point p : points) {
-                       waitFor(p);
-               }
-       }
-       
-       /**
-        * Wait for a point to be computed.  After calling this method
-        * the function values are available by calling XXX
-        * 
-        * @param point         the point to wait for.
-        * @throws InterruptedException         if this thread was interrupted while waiting.
-        */
-       public void waitFor(Point point) throws InterruptedException {
-               if (functionCache.containsKey(point)) {
-                       return;
-               }
-               
-               Future<Double> future = futureMap.get(point);
-               if (future == null) {
-                       throw new IllegalStateException("waitFor called for " + point + " but it is not being computed");
-               }
-               
-               try {
-                       double value = future.get();
-                       functionCache.put(point, value);
-               } catch (ExecutionException e) {
-                       throw new IllegalStateException("Function threw exception while processing", e.getCause());
-               }
-       }
-       
-       
-       /**
-        * Abort the computation of the specified point.  If computation has ended,
-        * the result is stored in the function cache anyway.
-        * 
-        * @param points        the points to abort.
-        * @return                      a list of the points that have been computed anyway
-        */
-       public List<Point> abort(Collection<Point> points) {
-               List<Point> computed = new ArrayList<Point>(Math.min(points.size(), 10));
-               
-               for (Point p : points) {
-                       if (abort(p)) {
-                               computed.add(p);
-                       }
-               }
-               
-               return computed;
-       }
-       
-       
-       /**
-        * Abort the computation of the specified point.  If computation has ended,
-        * the result is stored in the function cache anyway.
-        * 
-        * @param point         the point to abort.
-        * @return                      <code>true</code> if the point has been computed anyway, <code>false</code> if not.
-        */
-       public boolean abort(Point point) {
-               if (functionCache.containsKey(point)) {
-                       return true;
-               }
-               
-               Future<Double> future = futureMap.remove(point);
-               if (future == null) {
-                       throw new IllegalStateException("abort called for " + point + " but it is not being computed");
-               }
-               
-               if (future.isDone()) {
-                       // Evaluation has been completed, store value in cache
-                       try {
-                               double value = future.get();
-                               functionCache.put(point, value);
-                               return true;
-                       } catch (Exception e) {
-                               return false;
-                       }
-               } else {
-                       // Cancel the evaluation
-                       future.cancel(true);
-                       return false;
-               }
-       }
-       
-       
-       public double getValue(Point point) {
-               Double d = functionCache.get(point);
-               if (d == null) {
-                       throw new IllegalStateException(point.toString() + " is not in function cache.  " +
-                                       "functionCache=" + functionCache + "  futureMap=" + futureMap);
-               }
-               return d;
-       }
-       
-       
-
-       @Override
-       public Function getFunction() {
-               return function;
-       }
-       
-       @Override
-       public void setFunction(Function function) {
-               this.function = function;
-               clearCache();
-       }
-       
-       @Override
-       public void clearCache() {
-               List<Point> list = new ArrayList<Point>(futureMap.keySet());
-               abort(list);
-               functionCache.clear();
-       }
-       
-       public ExecutorService getExecutor() {
-               return executor;
-       }
-       
-}
diff --git a/src/net/sf/openrocket/optimization/ParallelFunctionCache.java b/src/net/sf/openrocket/optimization/ParallelFunctionCache.java
deleted file mode 100644 (file)
index c035d9e..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-package net.sf.openrocket.optimization;
-
-import java.util.Collection;
-import java.util.List;
-
-public interface ParallelFunctionCache extends FunctionCache {
-       
-       /**
-        * Queue a list of function evaluations at the specified points.
-        * 
-        * @param points        the points at which to evaluate the function.
-        */
-       public void compute(Collection<Point> points);
-       
-       /**
-        * Queue function evaluation for the specified point.
-        * 
-        * @param point         the point at which to evaluate the function.
-        */
-       public void compute(Point point);
-       
-       /**
-        * Wait for a collection of points to be computed.  After calling this method
-        * the function values are available by calling XXX
-        * 
-        * @param points        the points to wait for.
-        * @throws InterruptedException         if this thread was interrupted while waiting.
-        */
-       public void waitFor(Collection<Point> points) throws InterruptedException;
-       
-       /**
-        * Wait for a point to be computed.  After calling this method
-        * the function values are available by calling XXX
-        * 
-        * @param point         the point to wait for.
-        * @throws InterruptedException         if this thread was interrupted while waiting.
-        */
-       public void waitFor(Point point) throws InterruptedException;
-       
-       
-       /**
-        * Abort the computation of the specified point.  If computation has ended,
-        * the result is stored in the function cache anyway.
-        * 
-        * @param points        the points to abort.
-        * @return                      a list of the points that have been computed anyway
-        */
-       public List<Point> abort(Collection<Point> points);
-       
-       
-       /**
-        * Abort the computation of the specified point.  If computation has ended,
-        * the result is stored in the function cache anyway.
-        * 
-        * @param point         the point to abort.
-        * @return                      <code>true</code> if the point has been computed anyway, <code>false</code> if not.
-        */
-       public boolean abort(Point point);
-}
diff --git a/src/net/sf/openrocket/optimization/Point.java b/src/net/sf/openrocket/optimization/Point.java
deleted file mode 100644 (file)
index 41140f5..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-package net.sf.openrocket.optimization;
-
-import java.util.Arrays;
-
-import net.sf.openrocket.util.MathUtil;
-
-/**
- * An immutable n-dimensional coordinate point.
- * 
- * @author Sampo Niskanen <sampo.niskanen@iki.fi>
- */
-public final class Point {
-       
-       private final double[] point;
-       private double length = -1;
-       private double length2 = -1;
-       
-       
-       public Point(int dim) {
-               if (dim <= 0) {
-                       throw new IllegalArgumentException("Invalid dimensionality " + dim);
-               }
-               point = new double[dim];
-       }
-       
-       public Point(int dim, double value) {
-               this(dim);
-               Arrays.fill(point, value);
-       }
-       
-       public Point(double... value) {
-               if (value.length == 0) {
-                       throw new IllegalArgumentException("Zero-dimensional point not allowed");
-               }
-               point = value.clone();
-       }
-       
-       private Point(Point p) {
-               point = p.point.clone();
-       }
-       
-       
-
-       /**
-        * Return the point dimensionality.
-        * 
-        * @return      the point dimensionality
-        */
-       public int dim() {
-               return point.length;
-       }
-       
-       
-
-       public double get(int i) {
-               return point[i];
-       }
-       
-       public Point set(int i, double v) {
-               Point p = new Point(this);
-               p.point[i] = v;
-               return p;
-       }
-       
-       
-       /**
-        * Return a new point that is the sum of two points.
-        * 
-        * @param other         the point to add to this point.
-        * @return                      the sum of these points.
-        */
-       public Point add(Point other) {
-               Point p = new Point(this);
-               for (int i = 0; i < point.length; i++) {
-                       p.point[i] += other.point[i];
-               }
-               return p;
-       }
-       
-       
-       /**
-        * Return a new point that is the subtraction of two points.
-        * 
-        * @param other         the point to subtract from this point.
-        * @return                      the value of this - other.
-        */
-       public Point sub(Point other) {
-               Point p = new Point(this);
-               for (int i = 0; i < point.length; i++) {
-                       p.point[i] -= other.point[i];
-               }
-               return p;
-       }
-       
-       
-       /**
-        * Return this point multiplied by a scalar value.
-        * 
-        * @param v             the scalar to multiply with
-        * @return              the scaled point
-        */
-       public Point mul(double v) {
-               Point p = new Point(this);
-               for (int i = 0; i < point.length; i++) {
-                       p.point[i] *= v;
-               }
-               return p;
-       }
-       
-       
-       /**
-        * Return the length of this coordinate.
-        * 
-        * @return      the length.
-        */
-       public double length() {
-               if (length < 0) {
-                       length = Math.sqrt(length2());
-               }
-               return length;
-       }
-       
-       
-       /**
-        * Return the squared length of this coordinate.
-        * 
-        * @return      the square of the length of thie coordinate.
-        */
-       public double length2() {
-               if (length2 < 0) {
-                       length2 = 0;
-                       for (double p : point) {
-                               length2 += p * p;
-                       }
-               }
-               return length2;
-       }
-       
-       
-       /**
-        * Return the point as an array.
-        * 
-        * @return      the point as an array.
-        */
-       public double[] asArray() {
-               return point.clone();
-       }
-       
-       @Override
-       public boolean equals(Object obj) {
-               if (this == obj)
-                       return true;
-               
-               if (!(obj instanceof Point))
-                       return false;
-               
-               Point other = (Point) obj;
-               if (this.point.length != other.point.length)
-                       return false;
-               
-               for (int i = 0; i < point.length; i++) {
-                       if (!MathUtil.equals(this.point[i], other.point[i]))
-                               return false;
-               }
-               return true;
-       }
-       
-       @Override
-       public int hashCode() {
-               int n = 0;
-               for (double d : point) {
-                       n *= 37;
-                       n += (int) (d * 1000);
-               }
-               return n;
-       }
-       
-       @Override
-       public String toString() {
-               return "Point" + Arrays.toString(point);
-       }
-}
diff --git a/src/net/sf/openrocket/optimization/SearchPattern.java b/src/net/sf/openrocket/optimization/SearchPattern.java
deleted file mode 100644 (file)
index 8082928..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-package net.sf.openrocket.optimization;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import net.sf.openrocket.util.MathUtil;
-
-public class SearchPattern {
-       
-       /**
-        * Create a square search pattern with the specified dimensionality.
-        * 
-        * @param dimensionality        the dimensionality
-        */
-       public static List<Point> square(int dimensionality) {
-               List<Point> pattern = new ArrayList<Point>(dimensionality);
-               
-               for (int i = 0; i < dimensionality; i++) {
-                       double[] p = new double[dimensionality];
-                       p[i] = 1.0;
-                       pattern.add(new Point(p));
-               }
-               return pattern;
-       }
-       
-       
-
-       /**
-        * Create a regular simplex search pattern with the specified dimensionality.
-        * 
-        * @param dimensionality        the dimensionality
-        */
-       public static List<Point> regularSimplex(int dimensionality) {
-               if (dimensionality <= 0) {
-                       throw new IllegalArgumentException("Illegal dimensionality " + dimensionality);
-               }
-               
-               List<Point> pattern = new ArrayList<Point>(dimensionality);
-               
-               double[] coordinates = new double[dimensionality];
-               double dot = -1.0 / dimensionality;
-               
-               /*
-                * First construct an origin-centered regular simplex.
-                * http://en.wikipedia.org/wiki/Simplex#Cartesian_coordinates_for_regular_n-dimensional_simplex_in_Rn
-                */
-
-               for (int i = 0; i < dimensionality; i++) {
-                       // Compute the next point coordinate
-                       double value = 1;
-                       
-                       for (int j = 0; j < i; j++) {
-                               value -= MathUtil.pow2(coordinates[j]);
-                       }
-                       value = Math.sqrt(value);
-                       
-                       coordinates[i] = value;
-                       pattern.add(new Point(coordinates));
-                       
-                       // Compute the i-coordinate for all next points
-                       value = dot;
-                       for (int j = 0; j < i; j++) {
-                               value -= MathUtil.pow2(coordinates[j]);
-                       }
-                       value = value / coordinates[i];
-                       
-                       coordinates[i] = value;
-               }
-               
-               // Minimum point
-               Point min = pattern.get(dimensionality - 1);
-               min = min.set(dimensionality - 1, -min.get(dimensionality - 1));
-               
-
-               /*
-                * Shift simplex to have a corner at the origin and scale to unit length.
-                */
-               if (dimensionality > 1) {
-                       double scale = 1.0 / (pattern.get(1).sub(pattern.get(0)).length());
-                       for (int i = 0; i < dimensionality; i++) {
-                               Point p = pattern.get(i);
-                               p = p.sub(min).mul(scale);
-                               pattern.set(i, p);
-                       }
-               }
-               
-               return pattern;
-       }
-}
diff --git a/src/net/sf/openrocket/optimization/general/Function.java b/src/net/sf/openrocket/optimization/general/Function.java
new file mode 100644 (file)
index 0000000..97404e2
--- /dev/null
@@ -0,0 +1,40 @@
+package net.sf.openrocket.optimization.general;
+
+/**
+ * An interface defining an optimizable function.
+ * <p>
+ * Some function optimizers require that the function is thread-safe.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface Function {
+       
+       /**
+        * Evaluate the function at the specified point.
+        * <p>
+        * If the function evaluation is slow, then this method should abort the computation if
+        * the thread is interrupted.
+        * 
+        * @param point         the point at which to evaluate the function.
+        * @return                      the function value.
+        * @throws InterruptedException         if the thread was interrupted before function evaluation was completed.
+        */
+       public double evaluate(Point point) throws InterruptedException;
+       
+       
+       /**
+        * Return a cached value of the function at the specified point.  This allows efficient
+        * caching of old values even between calls to optimization methods.  This method should
+        * NOT evaluate the function except in special cases (e.g. the point is outside of the
+        * function domain).
+        * <p>
+        * Note that it is allowed to always allowed to return <code>Double.NaN</code>, especially
+        * for functions that are fast to evaluate.
+        * 
+        * @param point         the point of function evaluation.
+        * @return                      the function value, or <code>Double.NaN</code> if the function value has not been
+        *                                      evaluated at this point.
+        */
+       public double preComputed(Point point);
+       
+}
diff --git a/src/net/sf/openrocket/optimization/general/FunctionCache.java b/src/net/sf/openrocket/optimization/general/FunctionCache.java
new file mode 100644 (file)
index 0000000..c280fba
--- /dev/null
@@ -0,0 +1,19 @@
+package net.sf.openrocket.optimization.general;
+
+/**
+ * A storage of cached values of a function.  The purpose of this class is to
+ * cache function values 
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface FunctionCache {
+       
+       public double getValue(Point point);
+       
+       public void clearCache();
+       
+       public Function getFunction();
+       
+       public void setFunction(Function function);
+       
+}
diff --git a/src/net/sf/openrocket/optimization/general/FunctionOptimizer.java b/src/net/sf/openrocket/optimization/general/FunctionOptimizer.java
new file mode 100644 (file)
index 0000000..2caf7fa
--- /dev/null
@@ -0,0 +1,55 @@
+package net.sf.openrocket.optimization.general;
+
+/**
+ * An interface for a function optimization algorithm.  The function is evaluated
+ * via a function cache.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface FunctionOptimizer {
+       
+       /**
+        * Perform optimization on the function.  The optimization control is called to control
+        * when optimization is stopped.
+        * 
+        * @param initial       the initial start point of the optimization.
+        * @param control       the optimization control.
+        */
+       public void optimize(Point initial, OptimizationController control);
+       
+       
+       /**
+        * Return the optimum point computed by {@link #optimize(Point, OptimizationController)}.
+        * 
+        * @return      the optimum point value.
+        * @throws IllegalStateException        if {@link #optimize(Point, OptimizationController)} has not been called.
+        */
+       public Point getOptimumPoint();
+       
+       /**
+        * Return the function value at the optimum point.
+        * 
+        * @return      the value at the optimum point.
+        * @throws IllegalStateException        if {@link #optimize(Point, OptimizationController)} has not been called.
+        */
+       public double getOptimumValue();
+       
+       
+       /**
+        * Return the function cache used by this optimization algorithm.
+        * 
+        * @return      the function cache.
+        */
+       public FunctionCache getFunctionCache();
+       
+       /**
+        * Set the function cache that provides the function values for this algorithm.
+        * Some algorithms may require the function cache to be an instance of
+        * ParallelFunctionCache.
+        * 
+        * @param functionCache         the function cache.
+        */
+       public void setFunctionCache(FunctionCache functionCache);
+       
+
+}
diff --git a/src/net/sf/openrocket/optimization/general/OptimizationController.java b/src/net/sf/openrocket/optimization/general/OptimizationController.java
new file mode 100644 (file)
index 0000000..c91689c
--- /dev/null
@@ -0,0 +1,19 @@
+package net.sf.openrocket.optimization.general;
+
+public interface OptimizationController {
+       
+       /**
+        * Control for whether to continue the optimization.  This method is called after
+        * every full step taken by the optimization algorithm.
+        * 
+        * @param oldPoint      the old position.
+        * @param oldValue      the value of the function at the old position.
+        * @param newPoint      the new position.
+        * @param newValue      the value of the function at the new position.
+        * @param stepSize      the step length that is used to search for smaller function values when applicable, or NaN.
+        * @return                      <code>true</code> to continue optimization, <code>false</code> to stop.
+        */
+       public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue,
+                       double stepSize);
+       
+}
diff --git a/src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java b/src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java
new file mode 100644 (file)
index 0000000..a0a455e
--- /dev/null
@@ -0,0 +1,56 @@
+package net.sf.openrocket.optimization.general;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An OptimizationController that delegates control actions to multiple other controllers.
+ * The optimization is stopped if any of the controllers stops it.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class OptimizationControllerDelegator implements OptimizationController {
+       
+       private final List<OptimizationController> controllers = new ArrayList<OptimizationController>();
+       
+       /**
+        * Construct the controlled based on an array of controllers.
+        * 
+        * @param controllers   the controllers to use.
+        */
+       public OptimizationControllerDelegator(OptimizationController... controllers) {
+               for (OptimizationController c : controllers) {
+                       this.controllers.add(c);
+               }
+       }
+       
+       /**
+        * Construct the controller based on a collection of controllers.
+        * 
+        * @param controllers   the controllers to use.
+        */
+       public OptimizationControllerDelegator(Collection<OptimizationController> controllers) {
+               this.controllers.addAll(controllers);
+       }
+       
+       
+       /**
+        * Control whether to continue optimization.  This method returns false if any of the
+        * used controllers returns false.  However, all controllers will be called even if
+        * an earlier one stops the optimization.
+        */
+       @Override
+       public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) {
+               boolean ret = true;
+               
+               for (OptimizationController c : controllers) {
+                       if (!c.stepTaken(oldPoint, oldValue, newPoint, newValue, stepSize)) {
+                               ret = false;
+                       }
+               }
+               
+               return ret;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java b/src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java
new file mode 100644 (file)
index 0000000..4f84dcc
--- /dev/null
@@ -0,0 +1,247 @@
+package net.sf.openrocket.optimization.general;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An implementation of a ParallelFunctionCache that evaluates function values
+ * in parallel and caches them.  This allows pre-calculating possibly required
+ * function values beforehand.  If values are not required after all, the
+ * computation can be aborted assuming the function evaluation supports it.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ParallelExecutorCache implements ParallelFunctionCache {
+       
+       private final Map<Point, Double> functionCache = new HashMap<Point, Double>();
+       private final Map<Point, Future<Double>> futureMap = new HashMap<Point, Future<Double>>();
+       
+       private ExecutorService executor;
+       
+       private Function function;
+       
+       
+
+       public ParallelExecutorCache() {
+               this(Runtime.getRuntime().availableProcessors());
+       }
+       
+       public ParallelExecutorCache(int threadCount) {
+               executor = new ThreadPoolExecutor(threadCount, threadCount, 60, TimeUnit.SECONDS,
+                               new LinkedBlockingQueue<Runnable>(),
+                               new ThreadFactory() {
+                                       @Override
+                                       public Thread newThread(Runnable r) {
+                                               Thread t = new Thread(r);
+                                               t.setDaemon(true);
+                                               return t;
+                                       }
+                               });
+       }
+       
+       public ParallelExecutorCache(ExecutorService executor) {
+               this.executor = executor;
+       }
+       
+       
+
+       /**
+        * Queue a list of function evaluations at the specified points.
+        * 
+        * @param points        the points at which to evaluate the function.
+        */
+       public void compute(Collection<Point> points) {
+               for (Point p : points) {
+                       compute(p);
+               }
+       }
+       
+       
+       /**
+        * Queue function evaluation for the specified point.
+        * 
+        * @param point         the point at which to evaluate the function.
+        */
+       public void compute(Point point) {
+               if (functionCache.containsKey(point)) {
+                       // Function has already been evaluated at the point
+                       return;
+               }
+               
+               if (futureMap.containsKey(point)) {
+                       // Function is being evaluated at the point
+                       return;
+               }
+               
+               double value = function.preComputed(point);
+               if (!Double.isNaN(value)) {
+                       // Function value was in function cache
+                       functionCache.put(point, value);
+                       return;
+               }
+               
+               // Submit point for evaluation
+               FunctionCallable callable = new FunctionCallable(function, point);
+               Future<Double> future = executor.submit(callable);
+               futureMap.put(point, future);
+       }
+       
+       
+       /**
+        * Wait for a collection of points to be computed.  After calling this method
+        * the function values are available by calling XXX
+        * 
+        * @param points        the points to wait for.
+        * @throws InterruptedException         if this thread was interrupted while waiting.
+        */
+       public void waitFor(Collection<Point> points) throws InterruptedException {
+               for (Point p : points) {
+                       waitFor(p);
+               }
+       }
+       
+       /**
+        * Wait for a point to be computed.  After calling this method
+        * the function values are available by calling XXX
+        * 
+        * @param point         the point to wait for.
+        * @throws InterruptedException         if this thread was interrupted while waiting.
+        */
+       public void waitFor(Point point) throws InterruptedException {
+               if (functionCache.containsKey(point)) {
+                       return;
+               }
+               
+               Future<Double> future = futureMap.get(point);
+               if (future == null) {
+                       throw new IllegalStateException("waitFor called for " + point + " but it is not being computed");
+               }
+               
+               try {
+                       double value = future.get();
+                       functionCache.put(point, value);
+               } catch (ExecutionException e) {
+                       throw new IllegalStateException("Function threw exception while processing", e.getCause());
+               }
+       }
+       
+       
+       /**
+        * Abort the computation of the specified point.  If computation has ended,
+        * the result is stored in the function cache anyway.
+        * 
+        * @param points        the points to abort.
+        * @return                      a list of the points that have been computed anyway
+        */
+       public List<Point> abort(Collection<Point> points) {
+               List<Point> computed = new ArrayList<Point>(Math.min(points.size(), 10));
+               
+               for (Point p : points) {
+                       if (abort(p)) {
+                               computed.add(p);
+                       }
+               }
+               
+               return computed;
+       }
+       
+       
+       /**
+        * Abort the computation of the specified point.  If computation has ended,
+        * the result is stored in the function cache anyway.
+        * 
+        * @param point         the point to abort.
+        * @return                      <code>true</code> if the point has been computed anyway, <code>false</code> if not.
+        */
+       public boolean abort(Point point) {
+               if (functionCache.containsKey(point)) {
+                       return true;
+               }
+               
+               Future<Double> future = futureMap.remove(point);
+               if (future == null) {
+                       throw new IllegalStateException("abort called for " + point + " but it is not being computed");
+               }
+               
+               if (future.isDone()) {
+                       // Evaluation has been completed, store value in cache
+                       try {
+                               double value = future.get();
+                               functionCache.put(point, value);
+                               return true;
+                       } catch (Exception e) {
+                               return false;
+                       }
+               } else {
+                       // Cancel the evaluation
+                       future.cancel(true);
+                       return false;
+               }
+       }
+       
+       
+       public double getValue(Point point) {
+               Double d = functionCache.get(point);
+               if (d == null) {
+                       throw new IllegalStateException(point.toString() + " is not in function cache.  " +
+                                       "functionCache=" + functionCache + "  futureMap=" + futureMap);
+               }
+               return d;
+       }
+       
+       
+
+       @Override
+       public Function getFunction() {
+               return function;
+       }
+       
+       @Override
+       public void setFunction(Function function) {
+               this.function = function;
+               clearCache();
+       }
+       
+       @Override
+       public void clearCache() {
+               List<Point> list = new ArrayList<Point>(futureMap.keySet());
+               abort(list);
+               functionCache.clear();
+       }
+       
+       public ExecutorService getExecutor() {
+               return executor;
+       }
+       
+       
+
+       /**
+        * A Callable that evaluates a function at a specific point and returns the result.
+        */
+       private class FunctionCallable implements Callable<Double> {
+               private final Function calledFunction;
+               private final Point point;
+               
+               public FunctionCallable(Function function, Point point) {
+                       this.calledFunction = function;
+                       this.point = point;
+               }
+               
+               @Override
+               public Double call() throws InterruptedException {
+                       return calledFunction.evaluate(point);
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java b/src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java
new file mode 100644 (file)
index 0000000..9b4a0eb
--- /dev/null
@@ -0,0 +1,66 @@
+package net.sf.openrocket.optimization.general;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A FunctionCache that allows queuing points to be computed in the background,
+ * waiting for specific points to become computed or aborting the computation of
+ * points.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface ParallelFunctionCache extends FunctionCache {
+       
+       /**
+        * Queue a list of function evaluations at the specified points.
+        * 
+        * @param points        the points at which to evaluate the function.
+        */
+       public void compute(Collection<Point> points);
+       
+       /**
+        * Queue function evaluation for the specified point.
+        * 
+        * @param point         the point at which to evaluate the function.
+        */
+       public void compute(Point point);
+       
+       /**
+        * Wait for a collection of points to be computed.  After calling this method
+        * the function values are available by calling XXX
+        * 
+        * @param points        the points to wait for.
+        * @throws InterruptedException         if this thread was interrupted while waiting.
+        */
+       public void waitFor(Collection<Point> points) throws InterruptedException;
+       
+       /**
+        * Wait for a point to be computed.  After calling this method
+        * the function values are available by calling XXX
+        * 
+        * @param point         the point to wait for.
+        * @throws InterruptedException         if this thread was interrupted while waiting.
+        */
+       public void waitFor(Point point) throws InterruptedException;
+       
+       
+       /**
+        * Abort the computation of the specified point.  If computation has ended,
+        * the result is stored in the function cache anyway.
+        * 
+        * @param points        the points to abort.
+        * @return                      a list of the points that have been computed anyway
+        */
+       public List<Point> abort(Collection<Point> points);
+       
+       
+       /**
+        * Abort the computation of the specified point.  If computation has ended,
+        * the result is stored in the function cache anyway.
+        * 
+        * @param point         the point to abort.
+        * @return                      <code>true</code> if the point has been computed anyway, <code>false</code> if not.
+        */
+       public boolean abort(Point point);
+}
diff --git a/src/net/sf/openrocket/optimization/general/Point.java b/src/net/sf/openrocket/optimization/general/Point.java
new file mode 100644 (file)
index 0000000..908d445
--- /dev/null
@@ -0,0 +1,182 @@
+package net.sf.openrocket.optimization.general;
+
+import java.util.Arrays;
+
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * An immutable n-dimensional coordinate point.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public final class Point {
+       
+       private final double[] point;
+       private double length = -1;
+       private double length2 = -1;
+       
+       
+       public Point(int dim) {
+               if (dim <= 0) {
+                       throw new IllegalArgumentException("Invalid dimensionality " + dim);
+               }
+               point = new double[dim];
+       }
+       
+       public Point(int dim, double value) {
+               this(dim);
+               Arrays.fill(point, value);
+       }
+       
+       public Point(double... value) {
+               if (value.length == 0) {
+                       throw new IllegalArgumentException("Zero-dimensional point not allowed");
+               }
+               point = value.clone();
+       }
+       
+       private Point(Point p) {
+               point = p.point.clone();
+       }
+       
+       
+
+       /**
+        * Return the point dimensionality.
+        * 
+        * @return      the point dimensionality
+        */
+       public int dim() {
+               return point.length;
+       }
+       
+       
+
+       public double get(int i) {
+               return point[i];
+       }
+       
+       public Point set(int i, double v) {
+               Point p = new Point(this);
+               p.point[i] = v;
+               return p;
+       }
+       
+       
+       /**
+        * Return a new point that is the sum of two points.
+        * 
+        * @param other         the point to add to this point.
+        * @return                      the sum of these points.
+        */
+       public Point add(Point other) {
+               Point p = new Point(this);
+               for (int i = 0; i < point.length; i++) {
+                       p.point[i] += other.point[i];
+               }
+               return p;
+       }
+       
+       
+       /**
+        * Return a new point that is the subtraction of two points.
+        * 
+        * @param other         the point to subtract from this point.
+        * @return                      the value of this - other.
+        */
+       public Point sub(Point other) {
+               Point p = new Point(this);
+               for (int i = 0; i < point.length; i++) {
+                       p.point[i] -= other.point[i];
+               }
+               return p;
+       }
+       
+       
+       /**
+        * Return this point multiplied by a scalar value.
+        * 
+        * @param v             the scalar to multiply with
+        * @return              the scaled point
+        */
+       public Point mul(double v) {
+               Point p = new Point(this);
+               for (int i = 0; i < point.length; i++) {
+                       p.point[i] *= v;
+               }
+               return p;
+       }
+       
+       
+       /**
+        * Return the length of this coordinate.
+        * 
+        * @return      the length.
+        */
+       public double length() {
+               if (length < 0) {
+                       length = Math.sqrt(length2());
+               }
+               return length;
+       }
+       
+       
+       /**
+        * Return the squared length of this coordinate.
+        * 
+        * @return      the square of the length of thie coordinate.
+        */
+       public double length2() {
+               if (length2 < 0) {
+                       length2 = 0;
+                       for (double p : point) {
+                               length2 += p * p;
+                       }
+               }
+               return length2;
+       }
+       
+       
+       /**
+        * Return the point as an array.
+        * 
+        * @return      the point as an array.
+        */
+       public double[] asArray() {
+               return point.clone();
+       }
+       
+       @Override
+       public boolean equals(Object obj) {
+               if (this == obj)
+                       return true;
+               
+               if (!(obj instanceof Point))
+                       return false;
+               
+               Point other = (Point) obj;
+               if (this.point.length != other.point.length)
+                       return false;
+               
+               for (int i = 0; i < point.length; i++) {
+                       if (!MathUtil.equals(this.point[i], other.point[i]))
+                               return false;
+               }
+               return true;
+       }
+       
+       @Override
+       public int hashCode() {
+               int n = 0;
+               for (double d : point) {
+                       n *= 37;
+                       n += (int) (d * 1000);
+               }
+               return n;
+       }
+       
+       @Override
+       public String toString() {
+               return "Point" + Arrays.toString(point);
+       }
+}
diff --git a/src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java b/src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java
new file mode 100644 (file)
index 0000000..16a2fbc
--- /dev/null
@@ -0,0 +1,29 @@
+package net.sf.openrocket.optimization.general.multidim;
+
+import java.util.Comparator;
+
+import net.sf.openrocket.optimization.general.FunctionCache;
+import net.sf.openrocket.optimization.general.Point;
+
+/**
+ * A comparator that orders Points in a function value order, smallest first.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class FunctionCacheComparator implements Comparator<Point> {
+       
+       private final FunctionCache cache;
+       
+       public FunctionCacheComparator(FunctionCache cache) {
+               this.cache = cache;
+       }
+       
+       @Override
+       public int compare(Point o1, Point o2) {
+               double v1 = cache.getValue(o1);
+               double v2 = cache.getValue(o2);
+               
+               return Double.compare(v1, v2);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java b/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java
new file mode 100644 (file)
index 0000000..80a194b
--- /dev/null
@@ -0,0 +1,298 @@
+package net.sf.openrocket.optimization.general.multidim;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.optimization.general.FunctionCache;
+import net.sf.openrocket.optimization.general.FunctionOptimizer;
+import net.sf.openrocket.optimization.general.OptimizationController;
+import net.sf.openrocket.optimization.general.ParallelFunctionCache;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.Statistics;
+
+/**
+ * A customized implementation of the parallel multidirectional search algorithm by Dennis and Torczon.
+ * <p>
+ * This is a parallel pattern search optimization algorithm.  The function evaluations are performed
+ * using an ExecutorService.  By default a ThreadPoolExecutor is used that has as many thread defined
+ * as the system has processors.
+ */
+public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Statistics {
+       private static final LogHelper log = Application.getLogger();
+       
+       private List<Point> simplex = new ArrayList<Point>();
+       
+       private ParallelFunctionCache functionExecutor;
+       
+       private boolean useExpansion = false;
+       
+       private int stepCount = 0;
+       private int reflectionAcceptance = 0;
+       private int expansionAcceptance = 0;
+       private int coordinateAcceptance = 0;
+       private int reductionFallback = 0;
+       
+       
+       public MultidirectionalSearchOptimizer() {
+               // No-op
+       }
+       
+       public MultidirectionalSearchOptimizer(ParallelFunctionCache functionCache) {
+               this.functionExecutor = functionCache;
+       }
+       
+       
+
+       @Override
+       public void optimize(Point initial, OptimizationController control) {
+               FunctionCacheComparator comparator = new FunctionCacheComparator(functionExecutor);
+               
+               final List<Point> pattern = SearchPattern.square(initial.dim());
+               log.info("Starting optimization at " + initial + " with pattern " + pattern);
+               
+               try {
+                       
+                       boolean simplexComputed = false;
+                       double step = 0.5;
+                       
+                       // Set up the current simplex
+                       simplex.clear();
+                       simplex.add(initial);
+                       for (Point p : pattern) {
+                               simplex.add(initial.add(p.mul(step)));
+                       }
+                       
+                       // Normal iterations
+                       List<Point> reflection = new ArrayList<Point>(simplex.size());
+                       List<Point> expansion = new ArrayList<Point>(simplex.size());
+                       List<Point> coordinateSearch = new ArrayList<Point>(simplex.size());
+                       Point current;
+                       double currentValue;
+                       do {
+                               
+                               log.debug("Starting optimization step with simplex " + simplex +
+                                               (simplexComputed ? "" : " (not computed)"));
+                               stepCount++;
+                               
+                               if (!simplexComputed) {
+                                       // TODO: Could something be computed in parallel?
+                                       functionExecutor.compute(simplex);
+                                       functionExecutor.waitFor(simplex);
+                                       Collections.sort(simplex, comparator);
+                                       simplexComputed = true;
+                               }
+                               
+                               current = simplex.get(0);
+                               currentValue = functionExecutor.getValue(current);
+                               
+                               /*
+                                * Compute and queue the next points in likely order of usefulness.
+                                * Expansion is unlikely as we're mainly dealing with bounded optimization.
+                                */
+                               createReflection(simplex, reflection);
+                               createCoordinateSearch(current, step, coordinateSearch);
+                               if (useExpansion)
+                                       createExpansion(simplex, expansion);
+                               
+                               functionExecutor.compute(reflection);
+                               functionExecutor.compute(coordinateSearch);
+                               if (useExpansion)
+                                       functionExecutor.compute(expansion);
+                               
+                               // Check reflection acceptance
+                               log.debug("Computing reflection");
+                               functionExecutor.waitFor(reflection);
+                               
+                               if (accept(reflection, currentValue)) {
+                                       
+                                       log.debug("Reflection was successful, aborting coordinate search, " +
+                                                       (useExpansion ? "computing" : "skipping") + " expansion");
+                                       
+                                       functionExecutor.abort(coordinateSearch);
+                                       
+                                       simplex.clear();
+                                       simplex.add(current);
+                                       simplex.addAll(reflection);
+                                       Collections.sort(simplex, comparator);
+                                       
+                                       if (useExpansion) {
+                                               
+                                               /*
+                                                * Assume expansion to be unsuccessful, queue next reflection while computing expansion.
+                                                */
+                                               createReflection(simplex, reflection);
+                                               
+                                               functionExecutor.compute(reflection);
+                                               functionExecutor.waitFor(expansion);
+                                               
+                                               if (accept(expansion, currentValue)) {
+                                                       log.debug("Expansion was successful, aborting reflection");
+                                                       functionExecutor.abort(reflection);
+                                                       
+                                                       simplex.clear();
+                                                       simplex.add(current);
+                                                       simplex.addAll(expansion);
+                                                       step *= 2;
+                                                       Collections.sort(simplex, comparator);
+                                                       expansionAcceptance++;
+                                               } else {
+                                                       log.debug("Expansion failed");
+                                                       reflectionAcceptance++;
+                                               }
+                                               
+                                       } else {
+                                               reflectionAcceptance++;
+                                       }
+                                       
+                               } else {
+                                       
+                                       log.debug("Reflection was unsuccessful, aborting expansion, computing coordinate search");
+                                       
+                                       functionExecutor.abort(expansion);
+                                       
+                                       /*
+                                        * Assume coordinate search to be unsuccessful, queue contraction step while computing.
+                                        */
+                                       halveStep(simplex);
+                                       functionExecutor.compute(simplex);
+                                       functionExecutor.waitFor(coordinateSearch);
+                                       
+                                       if (accept(coordinateSearch, currentValue)) {
+                                               
+                                               log.debug("Coordinate search successful, reseting simplex");
+                                               List<Point> toAbort = new LinkedList<Point>(simplex);
+                                               simplex.clear();
+                                               simplex.add(current);
+                                               for (Point p : pattern) {
+                                                       simplex.add(current.add(p.mul(step)));
+                                               }
+                                               toAbort.removeAll(simplex);
+                                               functionExecutor.abort(toAbort);
+                                               simplexComputed = false;
+                                               coordinateAcceptance++;
+                                               
+                                       } else {
+                                               log.debug("Coordinate search unsuccessful, halving step.");
+                                               step /= 2;
+                                               reductionFallback++;
+                                       }
+                                       
+                               }
+                               
+                               log.debug("Ending optimization step with simplex " + simplex);
+                               
+                               if (Thread.interrupted()) {
+                                       throw new InterruptedException();
+                               }
+                               
+                       } while (control.stepTaken(current, currentValue, simplex.get(0),
+                                       functionExecutor.getValue(simplex.get(0)), step));
+                       
+               } catch (InterruptedException e) {
+                       log.info("Optimization was interrupted with InterruptedException");
+               }
+               
+               log.info("Finishing optimization at point " + simplex.get(0) + " value = " +
+                               functionExecutor.getValue(simplex.get(0)));
+       }
+       
+       
+
+       private void createReflection(List<Point> base, List<Point> reflection) {
+               Point current = base.get(0);
+               reflection.clear();
+               for (int i = 1; i < base.size(); i++) {
+                       Point p = current.mul(2).sub(base.get(i));
+                       reflection.add(p);
+               }
+       }
+       
+       private void createExpansion(List<Point> base, List<Point> expansion) {
+               Point current = base.get(0);
+               expansion.clear();
+               for (int i = 1; i < base.size(); i++) {
+                       Point p = current.mul(3).sub(base.get(i).mul(2));
+                       expansion.add(p);
+               }
+       }
+       
+       private void halveStep(List<Point> base) {
+               Point current = base.get(0);
+               for (int i = 1; i < base.size(); i++) {
+                       Point p = base.get(i);
+                       p = p.add(current).mul(0.5);
+                       base.set(i, p);
+               }
+       }
+       
+       private void createCoordinateSearch(Point current, double step, List<Point> coordinateDirections) {
+               coordinateDirections.clear();
+               for (int i = 0; i < current.dim(); i++) {
+                       Point p = new Point(current.dim());
+                       p = p.set(i, step);
+                       coordinateDirections.add(current.add(p));
+                       coordinateDirections.add(current.sub(p));
+               }
+       }
+       
+       
+       private boolean accept(List<Point> points, double currentValue) {
+               for (Point p : points) {
+                       if (functionExecutor.getValue(p) < currentValue) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+       
+       
+
+       @Override
+       public Point getOptimumPoint() {
+               if (simplex.size() == 0) {
+                       throw new IllegalStateException("Optimization has not been called, simplex is empty");
+               }
+               return simplex.get(0);
+       }
+       
+       @Override
+       public double getOptimumValue() {
+               return functionExecutor.getValue(getOptimumPoint());
+       }
+       
+       @Override
+       public FunctionCache getFunctionCache() {
+               return functionExecutor;
+       }
+       
+       @Override
+       public void setFunctionCache(FunctionCache functionCache) {
+               if (!(functionCache instanceof ParallelFunctionCache)) {
+                       throw new IllegalArgumentException("Function cache needs to be a ParallelFunctionCache: " + functionCache);
+               }
+               this.functionExecutor = (ParallelFunctionCache) functionCache;
+       }
+       
+       @Override
+       public String getStatistics() {
+               return "MultidirectionalSearchOptimizer[stepCount=" + stepCount +
+                               ", reflectionAcceptance=" + reflectionAcceptance +
+                               ", expansionAcceptance=" + expansionAcceptance +
+                               ", coordinateAcceptance=" + coordinateAcceptance +
+                               ", reductionFallback=" + reductionFallback;
+       }
+       
+       @Override
+       public void resetStatistics() {
+               stepCount = 0;
+               reflectionAcceptance = 0;
+               expansionAcceptance = 0;
+               coordinateAcceptance = 0;
+               reductionFallback = 0;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java b/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java
new file mode 100644 (file)
index 0000000..592f4ea
--- /dev/null
@@ -0,0 +1,95 @@
+package net.sf.openrocket.optimization.general.multidim;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * A helper class to create search patterns.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SearchPattern {
+       
+       /**
+        * Create a square search pattern with the specified dimensionality.
+        * 
+        * @param dimensionality        the dimensionality
+        */
+       public static List<Point> square(int dimensionality) {
+               List<Point> pattern = new ArrayList<Point>(dimensionality);
+               
+               for (int i = 0; i < dimensionality; i++) {
+                       double[] p = new double[dimensionality];
+                       p[i] = 1.0;
+                       pattern.add(new Point(p));
+               }
+               return pattern;
+       }
+       
+       
+
+       /**
+        * Create a regular simplex search pattern with the specified dimensionality.
+        * 
+        * @param dimensionality        the dimensionality
+        */
+       public static List<Point> regularSimplex(int dimensionality) {
+               if (dimensionality <= 0) {
+                       throw new IllegalArgumentException("Illegal dimensionality " + dimensionality);
+               }
+               
+               List<Point> pattern = new ArrayList<Point>(dimensionality);
+               
+               double[] coordinates = new double[dimensionality];
+               double dot = -1.0 / dimensionality;
+               
+               /*
+                * First construct an origin-centered regular simplex.
+                * http://en.wikipedia.org/wiki/Simplex#Cartesian_coordinates_for_regular_n-dimensional_simplex_in_Rn
+                */
+
+               for (int i = 0; i < dimensionality; i++) {
+                       // Compute the next point coordinate
+                       double value = 1;
+                       
+                       for (int j = 0; j < i; j++) {
+                               value -= MathUtil.pow2(coordinates[j]);
+                       }
+                       value = Math.sqrt(value);
+                       
+                       coordinates[i] = value;
+                       pattern.add(new Point(coordinates));
+                       
+                       // Compute the i-coordinate for all next points
+                       value = dot;
+                       for (int j = 0; j < i; j++) {
+                               value -= MathUtil.pow2(coordinates[j]);
+                       }
+                       value = value / coordinates[i];
+                       
+                       coordinates[i] = value;
+               }
+               
+               // Minimum point
+               Point min = pattern.get(dimensionality - 1);
+               min = min.set(dimensionality - 1, -min.get(dimensionality - 1));
+               
+
+               /*
+                * Shift simplex to have a corner at the origin and scale to unit length.
+                */
+               if (dimensionality > 1) {
+                       double scale = 1.0 / (pattern.get(1).sub(pattern.get(0)).length());
+                       for (int i = 0; i < dimensionality; i++) {
+                               Point p = pattern.get(i);
+                               p = p.sub(min).mul(scale);
+                               pattern.set(i, p);
+                       }
+               }
+               
+               return pattern;
+       }
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java b/src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java
new file mode 100644 (file)
index 0000000..f788404
--- /dev/null
@@ -0,0 +1,20 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+/**
+ * A goal for an optimization process, for example minimizing, maximizing or seeking
+ * a specific parameter value.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface OptimizationGoal {
+       
+       /**
+        * Compute a value which, when minimized, yields the desired goal of the 
+        * optimization problem.
+        * 
+        * @param value         the function actual value
+        * @return                      the value to minimize to reach the goal
+        */
+       double getMinimizationParameter(double value);
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java b/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java
new file mode 100644 (file)
index 0000000..7a8404b
--- /dev/null
@@ -0,0 +1,137 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.optimization.general.Function;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.startup.Application;
+
+/**
+ * A Function that optimizes a specific RocketOptimizationParameter to some goal
+ * by modifying a base simulation using SimulationModifiers.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketOptimizationFunction implements Function {
+       private static final LogHelper log = Application.getLogger();
+       
+       /*
+        * NOTE:  This class must be thread-safe!!!
+        */
+
+       private final Simulation baseSimulation;
+       private final RocketOptimizationParameter parameter;
+       private final OptimizationGoal goal;
+       private final SimulationModifier[] modifiers;
+       
+       private final Map<Point, Double> parameterValueCache = new ConcurrentHashMap<Point, Double>();
+       private final Map<Point, Double> goalValueCache = new ConcurrentHashMap<Point, Double>();
+       
+       
+       /**
+        * Sole constructor.
+        * <p>
+        * The dimensionality of the resulting function is the same as the length of the
+        * modifiers array.
+        * 
+        * @param baseSimulation        the base simulation to modify
+        * @param parameter                     the rocket parameter to optimize
+        * @param goal                          the goal of the rocket parameter
+        * @param modifiers                     the modifiers that modify the simulation
+        */
+       public RocketOptimizationFunction(Simulation baseSimulation, RocketOptimizationParameter parameter,
+                       OptimizationGoal goal, SimulationModifier... modifiers) {
+               this.baseSimulation = baseSimulation;
+               this.parameter = parameter;
+               this.goal = goal;
+               this.modifiers = modifiers.clone();
+               if (modifiers.length == 0) {
+                       throw new IllegalArgumentException("No SimulationModifiers specified");
+               }
+       }
+       
+       
+       @Override
+       public double evaluate(Point point) throws InterruptedException {
+               
+               // Check for precomputed value
+               double value = preComputed(point);
+               if (!Double.isNaN(value)) {
+                       return value;
+               }
+               
+               // Create the new simulation based on the point
+               double[] p = point.asArray();
+               if (p.length != modifiers.length) {
+                       throw new IllegalArgumentException("Point has length " + p.length + " while function has " +
+                                       modifiers.length + " simulation modifiers");
+               }
+               Simulation simulation = newSimulationInstance();
+               for (int i = 0; i < modifiers.length; i++) {
+                       modifiers[i].modify(simulation, p[i]);
+               }
+               
+               // Compute the optimization value
+               value = parameter.computeValue(simulation);
+               parameterValueCache.put(point, value);
+               
+               value = goal.getMinimizationParameter(value);
+               if (Double.isNaN(value)) {
+                       log.warn("Computed value was NaN, baseSimulation=" + baseSimulation + " parameter=" + parameter +
+                                       " goal=" + goal + " modifiers=" + Arrays.toString(modifiers) + " simulation=" + simulation);
+                       value = Double.MAX_VALUE;
+               }
+               goalValueCache.put(point, value);
+               
+               return value;
+       }
+       
+       @Override
+       public double preComputed(Point point) {
+               Double value = goalValueCache.get(point);
+               if (value != null) {
+                       return value;
+               }
+               
+               // TODO: CRITICAL: is in domain?
+               return 0;
+       }
+       
+       
+       /**
+        * Return the parameter value at a point that has been computed.  The purpose is
+        * to allow retrieving the parameter value corresponding to the found minimum value.
+        * 
+        * @param point         the point to use.
+        * @return                      the parameter value at that point, or NaN if the value at this point has not been computed.
+        */
+       public double getComputedParameterValue(Point point) {
+               Double value = parameterValueCache.get(point);
+               if (value != null) {
+                       return value;
+               } else {
+                       return Double.NaN;
+               }
+       }
+       
+       
+       /**
+        * Returns a new deep copy of the simulation and rocket.  This methods performs
+        * synchronization on the simulation for thread protection.
+        * 
+        * @return
+        */
+       private Simulation newSimulationInstance() {
+               synchronized (baseSimulation) {
+                       Rocket newRocket = (Rocket) baseSimulation.getRocket().copy();
+                       Simulation newSimulation = baseSimulation.duplicateSimulation(newRocket);
+                       return newSimulation;
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationParameter.java
new file mode 100644 (file)
index 0000000..61b1a80
--- /dev/null
@@ -0,0 +1,29 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import net.sf.openrocket.document.Simulation;
+
+/**
+ * A parameter of a rocket or simulation that can be optimized
+ * (for example max. altitude or velocity).
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface RocketOptimizationParameter {
+       
+       /**
+        * Return the label name for this optimization parameter.
+        * 
+        * @return      the name for the optimization parameter (e.g. "Flight altitude")
+        */
+       public String getName();
+       
+       /**
+        * Compute the value for this optimization parameter for the simulation.
+        * The return value can be any double value.
+        * 
+        * @param simulation    the simulation
+        * @return                              the parameter value (any double value)
+        */
+       public double computeValue(Simulation simulation);
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java
new file mode 100644 (file)
index 0000000..f0e1e7e
--- /dev/null
@@ -0,0 +1,21 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import net.sf.openrocket.document.Simulation;
+
+/**
+ * An interface what modifies a single parameter in a rocket simulation
+ * based on a double value in the range [0...1].
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface SimulationModifier {
+       
+       /**
+        * Modify the specified simulation to the corresponding parameter value.
+        * 
+        * @param simulation    the simulation to modify
+        * @param value                 a value in the range [0...1]
+        */
+       public void modify(Simulation simulation, double value);
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java b/src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java
new file mode 100644 (file)
index 0000000..e9576ee
--- /dev/null
@@ -0,0 +1,18 @@
+package net.sf.openrocket.optimization.rocketoptimization.goals;
+
+import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal;
+
+/**
+ * An optimization goal that maximizes a function value.  The method simply
+ * returns the opposite of the function value.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class MaximizationGoal implements OptimizationGoal {
+       
+       @Override
+       public double getMinimizationParameter(double value) {
+               return -value;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java b/src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java
new file mode 100644 (file)
index 0000000..c6f8fc6
--- /dev/null
@@ -0,0 +1,18 @@
+package net.sf.openrocket.optimization.rocketoptimization.goals;
+
+import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal;
+
+/**
+ * An optimization goal that minimizes a function value.  The method simply
+ * returns the function value.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class MinimizationGoal implements OptimizationGoal {
+       
+       @Override
+       public double getMinimizationParameter(double value) {
+               return value;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java b/src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java
new file mode 100644 (file)
index 0000000..34d433a
--- /dev/null
@@ -0,0 +1,30 @@
+package net.sf.openrocket.optimization.rocketoptimization.goals;
+
+import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal;
+
+
+/**
+ * An optimization goal that seeks for a specific function value.
+ * The method returns the Euclidic distance from the desired value.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ValueSeekGoal implements OptimizationGoal {
+       
+       private final double goal;
+       
+       /**
+        * Sole constructor.
+        * 
+        * @param goal  the function value to optimize towards.
+        */
+       public ValueSeekGoal(double goal) {
+               this.goal = goal;
+       }
+       
+       @Override
+       public double getMinimizationParameter(double value) {
+               return Math.abs(value - goal);
+       }
+       
+}
index f17bd3d9f6c2152426c6c15d7632782b537a0322..8c888320d487537bebc2ba926d6eef5338085a68 100644 (file)
@@ -13,7 +13,7 @@ package net.sf.openrocket.rocketcomponent;
  */
 
 public abstract class BodyComponent extends ExternalComponent {
-
+       
        /**
         * Default constructor.  Sets the relative position to POSITION_RELATIVE_AFTER,
         * i.e. body components come after one another.
@@ -23,7 +23,7 @@ public abstract class BodyComponent extends ExternalComponent {
        }
        
        
-       
+
        /**
         * Get the outer radius of the component at cylindrical coordinate (x,theta).
         * 
@@ -34,7 +34,7 @@ public abstract class BodyComponent extends ExternalComponent {
         * @return  Distance to the outer edge of the object
         */
        public abstract double getRadius(double x, double theta);
-
+       
        
        /**
         * Get the inner radius of the component at cylindrical coordinate (x,theta).
@@ -46,7 +46,7 @@ public abstract class BodyComponent extends ExternalComponent {
         * @return  Distance to the inner edge of the object
         */
        public abstract double getInnerRadius(double x, double theta);
-
+       
        
 
        /**
@@ -55,10 +55,14 @@ public abstract class BodyComponent extends ExternalComponent {
        public void setLength(double length) {
                if (this.length == length)
                        return;
-               this.length = Math.max(length,0);
+               this.length = Math.max(length, 0);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
+       @Override
+       public boolean allowsChildren() {
+               return true;
+       }
        
        /**
         * Check whether the given type can be added to this component.  BodyComponents allow any
@@ -72,7 +76,7 @@ public abstract class BodyComponent extends ExternalComponent {
                if (InternalComponent.class.isAssignableFrom(type))
                        return true;
                if (ExternalComponent.class.isAssignableFrom(type) &&
-                       !BodyComponent.class.isAssignableFrom(type))
+                               !BodyComponent.class.isAssignableFrom(type))
                        return true;
                return false;
        }
index 2e272f95e097da9dc0430406ccde01a3d6d11529..b84478e7dc7f6578bb72a2e08173615f38fd717a 100644 (file)
@@ -16,9 +16,9 @@ import net.sf.openrocket.util.MathUtil;
  */
 
 public class BodyTube extends SymmetricComponent implements MotorMount {
-
-       private double radius=0;
-       private boolean autoRadius = false;  // Radius chosen automatically based on parent component
+       
+       private double radius = 0;
+       private boolean autoRadius = false; // Radius chosen automatically based on parent component
        
        // When changing the inner radius, thickness is modified
        
@@ -29,36 +29,36 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        private double ignitionDelay = 0;
        private double overhang = 0;
        
-
        
+
        public BodyTube() {
                super();
-               this.length = 8*DEFAULT_RADIUS;
+               this.length = 8 * DEFAULT_RADIUS;
                this.radius = DEFAULT_RADIUS;
                this.autoRadius = true;
        }
        
        public BodyTube(double length, double radius) {
                super();
-               this.radius = Math.max(radius,0);
-               this.length = Math.max(length,0);
+               this.radius = Math.max(radius, 0);
+               this.length = Math.max(length, 0);
        }
        
        
        public BodyTube(double length, double radius, boolean filled) {
-               this(length,radius);
+               this(length, radius);
                this.filled = filled;
        }
        
        public BodyTube(double length, double radius, double thickness) {
-               this(length,radius);
+               this(length, radius);
                this.filled = false;
                this.thickness = thickness;
        }
        
-
+       
        /************  Get/set component parameter methods ************/
-
+       
        /**
         * Return the outer radius of the body tube.
         */
@@ -90,18 +90,18 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
         * This method sets the automatic radius off.
         */
        public void setRadius(double radius) {
-               if ((this.radius == radius) && (autoRadius==false))
+               if ((this.radius == radius) && (autoRadius == false))
                        return;
                
                this.autoRadius = false;
-               this.radius = Math.max(radius,0);
-
+               this.radius = Math.max(radius, 0);
+               
                if (this.thickness > this.radius)
                        this.thickness = this.radius;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
-
+       
+       
        /**
         * Returns whether the radius is selected automatically or not.
         * Returns false also in case automatic radius selection is not possible.
@@ -109,7 +109,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        public boolean isRadiusAutomatic() {
                return autoRadius;
        }
-
+       
        /**
         * Sets whether the radius is selected automatically or not.  
         */
@@ -123,16 +123,27 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        
        
        @Override
-       public double getAftRadius() {  return getRadius(); }
-       @Override
-       public double getForeRadius() { return getRadius(); }
+       public double getAftRadius() {
+               return getRadius();
+       }
+       
        @Override
-       public boolean isAftRadiusAutomatic() { return isRadiusAutomatic();     }
+       public double getForeRadius() {
+               return getRadius();
+       }
+       
        @Override
-       public boolean isForeRadiusAutomatic() { return isRadiusAutomatic(); }
+       public boolean isAftRadiusAutomatic() {
+               return isRadiusAutomatic();
+       }
        
+       @Override
+       public boolean isForeRadiusAutomatic() {
+               return isRadiusAutomatic();
+       }
        
        
+
        @Override
        protected double getFrontAutoRadius() {
                if (isRadiusAutomatic()) {
@@ -146,7 +157,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                }
                return getRadius();
        }
-
+       
        @Override
        protected double getRearAutoRadius() {
                if (isRadiusAutomatic()) {
@@ -160,26 +171,24 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                }
                return getRadius();
        }
-
-
-       
-       
-       
        
        
+
+
+
        public double getInnerRadius() {
                if (filled)
                        return 0;
-               return Math.max(getRadius()-thickness, 0);
+               return Math.max(getRadius() - thickness, 0);
        }
        
        public void setInnerRadius(double r) {
-               setThickness(getRadius()-r);
+               setThickness(getRadius() - r);
        }
        
        
-       
-       
+
+
        /**
         * Return the component name.
         */
@@ -187,8 +196,8 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        public String getComponentName() {
                return "Body tube";
        }
-
-
+       
+       
        /************ Component calculations ***********/
        
        // From SymmetricComponent
@@ -199,7 +208,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        public double getRadius(double x) {
                return getRadius();
        }
-
+       
        /**
         * Returns the inner radius at the position x.  If the tube is filled, returns always zero.
         */
@@ -208,16 +217,16 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                if (filled)
                        return 0.0;
                else
-                       return Math.max(getRadius()-thickness,0);
+                       return Math.max(getRadius() - thickness, 0);
        }
        
-
+       
        /**
         * Returns the body tube's center of gravity.
         */
        @Override
        public Coordinate getComponentCG() {
-               return new Coordinate(length/2,0,0,getComponentMass());
+               return new Coordinate(length / 2, 0, 0, getComponentMass());
        }
        
        /**
@@ -227,35 +236,34 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        public double getComponentVolume() {
                double r = getRadius();
                if (filled)
-                       return getFilledVolume(r,length);
+                       return getFilledVolume(r, length);
                else
-                       return getFilledVolume(r,length) - getFilledVolume(getInnerRadius(0),length);
+                       return getFilledVolume(r, length) - getFilledVolume(getInnerRadius(0), length);
        }
        
        
        @Override
        public double getLongitudalUnitInertia() {
                // 1/12 * (3 * (r1^2 + r2^2) + h^2)
-               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
-                               MathUtil.pow2(getLength())) / 12;
+               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) + MathUtil.pow2(getLength())) / 12;
        }
-
+       
        @Override
        public double getRotationalUnitInertia() {
                // 1/2 * (r1^2 + r2^2)
-               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius())) / 2;
        }
-
-
        
+       
+
 
        /**
         * Helper function for cylinder volume.
         */
        private static double getFilledVolume(double r, double l) {
-               return Math.PI * r*r * l;
+               return Math.PI * r * r * l;
        }
-
+       
        
        /**
         * Adds bounding coordinates to the given set.  The body tube will fit within the
@@ -267,19 +275,19 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        public Collection<Coordinate> getComponentBounds() {
                Collection<Coordinate> bounds = new ArrayList<Coordinate>(8);
                double r = getRadius();
-               addBound(bounds,0,r);
-               addBound(bounds,length,r);
+               addBound(bounds, 0, r);
+               addBound(bounds, length, r);
                return bounds;
        }
-
-
+       
+       
        ////////////////  Motor mount  /////////////////
        
        @Override
        public boolean isMotorMount() {
                return motorMount;
        }
-
+       
        @Override
        public void setMotorMount(boolean mount) {
                if (motorMount == mount)
@@ -302,7 +310,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                
                return motors.get(id);
        }
-
+       
        @Override
        public void setMotor(String id, Motor motor) {
                if (id == null) {
@@ -317,7 +325,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                motors.put(id, motor);
                fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
        }
-
+       
        @Override
        public double getMotorDelay(String id) {
                Double delay = ejectionDelays.get(id);
@@ -325,7 +333,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                        return Motor.PLUGGED;
                return delay;
        }
-
+       
        @Override
        public void setMotorDelay(String id, double delay) {
                ejectionDelays.put(id, delay);
@@ -339,14 +347,14 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
        
        @Override
        public double getMotorMountDiameter() {
-               return getInnerRadius()*2;
+               return getInnerRadius() * 2;
        }
-
+       
        @Override
        public IgnitionEvent getIgnitionEvent() {
                return ignitionEvent;
        }
-
+       
        @Override
        public void setIgnitionEvent(IgnitionEvent event) {
                if (ignitionEvent == event)
@@ -354,13 +362,13 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                ignitionEvent = event;
                fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
        }
-
+       
        
        @Override
        public double getIgnitionDelay() {
                return ignitionDelay;
        }
-
+       
        @Override
        public void setIgnitionDelay(double delay) {
                if (MathUtil.equals(delay, ignitionDelay))
@@ -368,7 +376,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                ignitionDelay = delay;
                fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
        }
-
+       
        
        @Override
        public double getMotorOverhang() {
@@ -382,8 +390,8 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                this.overhang = overhang;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
-
+       
+       
        @Override
        public Coordinate getMotorPosition(String id) {
                Motor motor = motors.get(id);
@@ -393,10 +401,10 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
                
                return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang());
        }
-
        
-
        
+
+
        /*
         * (non-Javadoc)
         * Copy the motor and ejection delay HashMaps.
@@ -405,10 +413,10 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
         */
        @SuppressWarnings("unchecked")
        @Override
-       public RocketComponent copy() {
-               RocketComponent c = super.copy();
-               ((BodyTube)c).motors = (HashMap<String,Motor>) motors.clone();
-               ((BodyTube)c).ejectionDelays = (HashMap<String,Double>) ejectionDelays.clone();
+       protected RocketComponent copyWithOriginalID() {
+               RocketComponent c = super.copyWithOriginalID();
+               ((BodyTube) c).motors = (HashMap<String, Motor>) motors.clone();
+               ((BodyTube) c).ejectionDelays = (HashMap<String, Double>) ejectionDelays.clone();
                return c;
        }
 }
index c82aa97caa1ece35b6021c9c923dc15d7a675c35..4c0e6c0e0cecdecb767705db72a0a3f681af08fd 100644 (file)
@@ -2,12 +2,12 @@ package net.sf.openrocket.rocketcomponent;
 
 
 public class Bulkhead extends RadiusRingComponent {
-
+       
        public Bulkhead() {
                setOuterRadiusAutomatic(true);
                setLength(0.002);
        }
-
+       
        @Override
        public double getInnerRadius() {
                return 0;
@@ -27,10 +27,15 @@ public class Bulkhead extends RadiusRingComponent {
        public String getComponentName() {
                return "Bulkhead";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return false;
        }
-
+       
 }
index b0f2aa931ce3ab14f3c96638ee5f4e769b7c06ac..944f286025694ae9bf5a4641fde102b01827910d 100644 (file)
@@ -4,7 +4,7 @@ import net.sf.openrocket.util.Coordinate;
 
 
 public class CenteringRing extends RadiusRingComponent {
-
+       
        public CenteringRing() {
                setOuterRadiusAutomatic(true);
                setInnerRadiusAutomatic(true);
@@ -19,12 +19,12 @@ public class CenteringRing extends RadiusRingComponent {
                        innerRadius = 0;
                        // Component can be parentless if disattached from rocket
                        if (this.getParent() != null) {
-                               for (RocketComponent sibling: this.getParent().getChildren()) {
+                               for (RocketComponent sibling : this.getParent().getChildren()) {
                                        /*
                                         * Only InnerTubes are considered when determining the automatic
                                         * inner radius (for now).
                                         */
-                                       if (!(sibling instanceof InnerTube))  // Excludes itself
+                                       if (!(sibling instanceof InnerTube)) // Excludes itself
                                                continue;
                                        
                                        double pos1 = this.toRelative(Coordinate.NUL, sibling)[0].x;
@@ -32,7 +32,7 @@ public class CenteringRing extends RadiusRingComponent {
                                        if (pos2 < 0 || pos1 > sibling.getLength())
                                                continue;
                                        
-                                       innerRadius = Math.max(innerRadius, ((InnerTube)sibling).getOuterRadius());
+                                       innerRadius = Math.max(innerRadius, ((InnerTube) sibling).getOuterRadius());
                                }
                                innerRadius = Math.min(innerRadius, getOuterRadius());
                        }
@@ -40,7 +40,7 @@ public class CenteringRing extends RadiusRingComponent {
                
                return super.getInnerRadius();
        }
-
+       
        
        @Override
        public void setOuterRadiusAutomatic(boolean auto) {
@@ -56,10 +56,15 @@ public class CenteringRing extends RadiusRingComponent {
        public String getComponentName() {
                return "Centering ring";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return false;
        }
-
+       
 }
index 837e9fea7803f468cc9bdd302e596ff7048c4745..ca3e58fba3183ddc7b03392a5a1fb624e4e30f08 100644 (file)
@@ -343,7 +343,7 @@ public class Configuration implements Cloneable, ChangeSource, ComponentChangeLi
                        rocket.addComponentChangeListener(config);
                        return config;
                } catch (CloneNotSupportedException e) {
-                       throw new BugException("BUG: clone not supported!", e);
+                       throw new BugException("clone not supported!", e);
                }
        }
        
index e6c06ae5bd8aa178a422fd072de6533e5ec2bb43..2c2c590b94c47843b251875060104a89c43e3932 100644 (file)
@@ -2,14 +2,14 @@ package net.sf.openrocket.rocketcomponent;
 
 
 public class EngineBlock extends ThicknessRingComponent {
-
+       
        public EngineBlock() {
                super();
                setOuterRadiusAutomatic(true);
                setThickness(0.005);
                setLength(0.005);
        }
-
+       
        @Override
        public void setOuterRadiusAutomatic(boolean auto) {
                super.setOuterRadiusAutomatic(auto);
@@ -19,10 +19,15 @@ public class EngineBlock extends ThicknessRingComponent {
        public String getComponentName() {
                return "Engine block";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return false;
        }
-
+       
 }
index 96c85b26b8af5fb12def2b26a75087779b8d59ec..ea60d417c0c956848b9db166dff5341e04416614 100644 (file)
@@ -19,12 +19,13 @@ public abstract class FinSet extends ExternalComponent {
        
        
        public enum CrossSection {
-               SQUARE("Square",   1.00),
+               SQUARE("Square", 1.00),
                ROUNDED("Rounded", 0.99),
                AIRFOIL("Airfoil", 0.85);
                
                private final String name;
                private final double volume;
+               
                CrossSection(String name, double volume) {
                        this.name = name;
                        this.volume = volume;
@@ -33,6 +34,7 @@ public abstract class FinSet extends ExternalComponent {
                public double getRelativeVolume() {
                        return volume;
                }
+               
                @Override
                public String toString() {
                        return name;
@@ -45,6 +47,7 @@ public abstract class FinSet extends ExternalComponent {
                END("Root chord trailing edge");
                
                private final String name;
+               
                TabRelativePosition(String name) {
                        this.name = name;
                }
@@ -63,7 +66,7 @@ public abstract class FinSet extends ExternalComponent {
        /**
         * Rotation about the x-axis by 2*PI/fins.
         */
-       protected Transformation finRotation = Transformation.rotate_x(2*Math.PI/fins);
+       protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins);
        
        /**
         * Rotation angle of the first fin.  Zero corresponds to the positive y-axis.
@@ -75,7 +78,7 @@ public abstract class FinSet extends ExternalComponent {
         */
        protected Transformation baseRotation = Transformation.rotate_x(rotation);
        
-       
+
        /**
         * Cant angle of fins.
         */
@@ -90,13 +93,13 @@ public abstract class FinSet extends ExternalComponent {
         */
        protected double thickness = 0.003;
        
-       
+
        /**
         * The cross-section shape of the fins.
         */
        protected CrossSection crossSection = CrossSection.SQUARE;
        
-       
+
        /*
         * Fin tab properties.
         */
@@ -105,7 +108,7 @@ public abstract class FinSet extends ExternalComponent {
        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;
@@ -121,9 +124,9 @@ public abstract class FinSet extends ExternalComponent {
        public FinSet() {
                super(RocketComponent.Position.BOTTOM);
        }
-
        
        
+
        /**
         * Return the number of fins in the set.
         * @return The number of fins.
@@ -131,7 +134,7 @@ public abstract class FinSet extends ExternalComponent {
        public int getFinCount() {
                return fins;
        }
-
+       
        /**
         * Sets the number of fins in the set.
         * @param n The number of fins, greater of equal to one.
@@ -144,14 +147,14 @@ public abstract class FinSet extends ExternalComponent {
                if (n > 8)
                        n = 8;
                fins = n;
-               finRotation = Transformation.rotate_x(2*Math.PI/fins);
+               finRotation = Transformation.rotate_x(2 * Math.PI / fins);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
        public Transformation getFinRotationTransformation() {
                return finRotation;
        }
-
+       
        /**
         * Gets the base rotation amount of the first fin.
         * @return The base rotation amount.
@@ -172,13 +175,13 @@ public abstract class FinSet extends ExternalComponent {
                baseRotation = Transformation.rotate_x(rotation);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        public Transformation getBaseRotationTransformation() {
                return baseRotation;
        }
        
        
-       
+
        public double getCantAngle() {
                return cantAngle;
        }
@@ -194,12 +197,12 @@ public abstract class FinSet extends ExternalComponent {
        
        public Transformation getCantRotation() {
                if (cantRotation == null) {
-                       if (MathUtil.equals(cantAngle,0)) {
+                       if (MathUtil.equals(cantAngle, 0)) {
                                cantRotation = Transformation.IDENTITY;
                        } else {
-                               Transformation t = new Transformation(-length/2,0,0);
+                               Transformation t = new Transformation(-length / 2, 0, 0);
                                t = Transformation.rotate_y(cantAngle).applyTransformation(t);
-                               t = new Transformation(length/2,0,0).applyTransformation(t);
+                               t = new Transformation(length / 2, 0, 0).applyTransformation(t);
                                cantRotation = t;
                        }
                }
@@ -215,7 +218,7 @@ public abstract class FinSet extends ExternalComponent {
        public void setThickness(double r) {
                if (thickness == r)
                        return;
-               thickness = Math.max(r,0);
+               thickness = Math.max(r, 0);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
@@ -232,15 +235,15 @@ public abstract class FinSet extends ExternalComponent {
        }
        
        
-       
-       
+
+
 
        @Override
        public void setRelativePosition(RocketComponent.Position position) {
                super.setRelativePosition(position);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        
        @Override
        public void setPositionValue(double value) {
@@ -250,11 +253,11 @@ public abstract class FinSet extends ExternalComponent {
        
        
 
-       
+
        public double getTabHeight() {
                return tabHeight;
        }
-
+       
        public void setTabHeight(double height) {
                height = MathUtil.max(height, 0);
                if (MathUtil.equals(this.tabHeight, height))
@@ -262,12 +265,12 @@ public abstract class FinSet extends ExternalComponent {
                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))
@@ -275,12 +278,12 @@ public abstract class FinSet extends ExternalComponent {
                this.tabLength = length;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
-
+       
+       
        public double getTabShift() {
                return tabShift;
        }
-
+       
        public void setTabShift(double shift) {
                this.tabShift = shift;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
@@ -301,23 +304,23 @@ public abstract class FinSet extends ExternalComponent {
                case FRONT:
                        this.tabShift = front;
                        break;
-                       
+               
                case CENTER:
-                       this.tabShift = front + tabLength/2 - getLength()/2;
+                       this.tabShift = front + tabLength / 2 - getLength() / 2;
                        break;
-                       
+               
                case END:
                        this.tabShift = front + tabLength - getLength();
                        break;
-                       
+               
                default:
-                       throw new IllegalArgumentException("position="+position);
+                       throw new IllegalArgumentException("position=" + position);
                }
                this.tabRelativePosition = position;
                
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
-
+       
        
        /**
         * Return the tab front edge position from the front of the fin.
@@ -328,39 +331,39 @@ public abstract class FinSet extends ExternalComponent {
                        return tabShift;
                        
                case CENTER:
-                       return getLength()/2 - tabLength/2 + tabShift;
+                       return getLength() / 2 - tabLength / 2 + tabShift;
                        
                case END:
                        return getLength() - tabLength + tabShift;
                        
                default:
-                       throw new IllegalStateException("tabRelativePosition="+tabRelativePosition);
+                       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;                    
+                       return tabLength + tabShift;
                case CENTER:
-                       return getLength()/2 + tabLength/2 + tabShift;
+                       return getLength() / 2 + tabLength / 2 + tabShift;
                        
                case END:
                        return getLength() + tabShift;
                        
                default:
-                       throw new IllegalStateException("tabRelativePosition="+tabRelativePosition);
+                       throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition);
                }
        }
-
        
        
-       
-       ///////////  Calculation methods  ///////////
 
+
+       ///////////  Calculation methods  ///////////
+       
        /**
         * Return the area of one side of one fin.  This does NOT include the area of
         * the fin tab.
@@ -385,33 +388,33 @@ public abstract class FinSet extends ExternalComponent {
                if (finArea < 0)
                        calculateAreaCG();
                
-               return new Coordinate(finCGx,finCGy,0);
+               return new Coordinate(finCGx, finCGy, 0);
        }
        
        
 
        @Override
        public double getComponentVolume() {
-               return fins * (getFinArea() + tabHeight*tabLength) * thickness * 
-                       crossSection.getRelativeVolume();
+               return fins * (getFinArea() + tabHeight * tabLength) * thickness *
+                               crossSection.getRelativeVolume();
        }
        
-
+       
        @Override
        public Coordinate getComponentCG() {
                if (finArea < 0)
                        calculateAreaCG();
                
-               double mass = getComponentMass();  // safe
+               double mass = getComponentMass(); // safe
                
                if (fins == 1) {
                        return baseRotation.transform(
-                                       new Coordinate(finCGx,finCGy + getBodyRadius(), 0, mass));
+                                       new Coordinate(finCGx, finCGy + getBodyRadius(), 0, mass));
                } else {
                        return new Coordinate(finCGx, 0, 0, mass);
                }
        }
-
+       
        
        private void calculateAreaCG() {
                Coordinate[] points = this.getFinPoints();
@@ -419,20 +422,20 @@ public abstract class FinSet extends ExternalComponent {
                finCGx = 0;
                finCGy = 0;
                
-               for (int i=0; i < points.length-1; i++) {
+               for (int i = 0; i < points.length - 1; i++) {
                        final double x0 = points[i].x;
-                       final double x1 = points[i+1].x;
+                       final double x1 = points[i + 1].x;
                        final double y0 = points[i].y;
-                       final double y1 = points[i+1].y;
+                       final double y1 = points[i + 1].y;
                        
-                       double da = (y0+y1)*(x1-x0) / 2;
+                       double da = (y0 + y1) * (x1 - x0) / 2;
                        finArea += da;
-                       if (Math.abs(y0-y1) < 0.00001) {
-                               finCGx += (x0+x1)/2 * da;
-                               finCGy += y0/2 * da;
+                       if (Math.abs(y0 - y1) < 0.00001) {
+                               finCGx += (x0 + x1) / 2 * da;
+                               finCGy += y0 / 2 * da;
                        } else {
-                               finCGx += (x0*(2*y0 + y1) + x1*(y0 + 2*y1)) / (3*(y0 + y1)) * da;
-                               finCGy += (y1 + y0*y0/(y0 + y1))/3 * da;
+                               finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da;
+                               finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da;
                        }
                }
                
@@ -443,11 +446,11 @@ public abstract class FinSet extends ExternalComponent {
                double tabArea = tabLength * tabHeight;
                if (!MathUtil.equals(tabArea, 0)) {
                        
-                       double x = (getTabFrontEdge() + getTabTrailingEdge())/2;
-                       double y = -this.tabHeight/2;
+                       double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2;
+                       double y = -this.tabHeight / 2;
                        
-                       finCGx += x*tabArea;
-                       finCGy += y*tabArea;
+                       finCGx += x * tabArea;
+                       finCGy += y * tabArea;
                        
                }
                
@@ -455,7 +458,7 @@ public abstract class FinSet extends ExternalComponent {
                        finCGx /= (finArea + tabArea);
                        finCGy /= (finArea + tabArea);
                } else {
-                       finCGx = (points[0].x + points[points.length-1].x)/2;
+                       finCGx = (points[0].x + points[points.length - 1].x) / 2;
                        finCGy = 0;
                }
        }
@@ -484,23 +487,23 @@ public abstract class FinSet extends ExternalComponent {
                // w2 and h2 are squares of the fin width and height
                double w = getLength();
                double h = getSpan();
-               double w2,h2;
+               double w2, h2;
                
-               if (MathUtil.equals(w*h,0)) {
+               if (MathUtil.equals(w * h, 0)) {
                        w2 = area;
                        h2 = area;
                } else {
-                       w2 = w*area/h;
-                       h2 = h*area/w;
+                       w2 = w * area / h;
+                       h2 = h * area / w;
                }
                
-               double inertia = (h2 + 2*w2)/24;
+               double inertia = (h2 + 2 * w2) / 24;
                
                if (fins == 1)
                        return inertia;
                
                double radius = getBodyRadius();
-
+               
                return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius));
        }
        
@@ -525,18 +528,18 @@ public abstract class FinSet extends ExternalComponent {
                double w = getLength();
                double h = getSpan();
                
-               if (MathUtil.equals(w*h,0)) {
+               if (MathUtil.equals(w * h, 0)) {
                        h = Math.sqrt(area);
                } else {
-                       h = Math.sqrt(h*area/w);
+                       h = Math.sqrt(h * area / w);
                }
                
                if (fins == 1)
-                       return h*h / 12;
+                       return h * h / 12;
                
                double radius = getBodyRadius();
                
-               return fins * (h*h/12 + MathUtil.pow2(h/2 + radius));
+               return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius));
        }
        
        
@@ -548,13 +551,13 @@ public abstract class FinSet extends ExternalComponent {
                List<Coordinate> bounds = new ArrayList<Coordinate>();
                double r = getBodyRadius();
                
-               for (Coordinate point: getFinPoints()) {
+               for (Coordinate point : getFinPoints()) {
                        addFinBound(bounds, point.x, point.y + r);
                }
                
                return bounds;
        }
-
+       
        
        /**
         * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
@@ -564,25 +567,25 @@ public abstract class FinSet extends ExternalComponent {
                Coordinate c;
                int i;
                
-               c = new Coordinate(x,y,thickness/2);
+               c = new Coordinate(x, y, thickness / 2);
                c = baseRotation.transform(c);
                set.add(c);
-               for (i=1; i<fins; i++) {
+               for (i = 1; i < fins; i++) {
                        c = finRotation.transform(c);
                        set.add(c);
                }
                
-               c = new Coordinate(x,y,-thickness/2);
+               c = new Coordinate(x, y, -thickness / 2);
                c = baseRotation.transform(c);
                set.add(c);
-               for (i=1; i<fins; i++) {
+               for (i = 1; i < fins; i++) {
                        c = finRotation.transform(c);
                        set.add(c);
                }
        }
-
        
        
+
        @Override
        public void componentChanged(ComponentChangeEvent e) {
                if (e.isAerodynamicChange()) {
@@ -603,16 +606,21 @@ public abstract class FinSet extends ExternalComponent {
                RocketComponent s;
                
                s = this.getParent();
-               while (s!=null) {
+               while (s != null) {
                        if (s instanceof SymmetricComponent) {
-                               double x = this.toRelative(new Coordinate(0,0,0), s)[0].x;
-                               return ((SymmetricComponent)s).getRadius(x);
+                               double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x;
+                               return ((SymmetricComponent) s).getRadius(x);
                        }
                        s = s.getParent();
                }
                return 0;
        }
        
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        /**
         * Allows nothing to be attached to a FinSet.
         * 
@@ -624,8 +632,8 @@ public abstract class FinSet extends ExternalComponent {
        }
        
        
-       
-       
+
+
        /**
         * Return a list of coordinates defining the geometry of a single fin.  
         * The coordinates are the XY-coordinates of points defining the shape of a single fin,
@@ -649,25 +657,25 @@ public abstract class FinSet extends ExternalComponent {
        public Coordinate[] getFinPointsWithTab() {
                Coordinate[] points = getFinPoints();
                
-               if (MathUtil.equals(getTabHeight(), 0) || 
+               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 = 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);
+               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.
@@ -679,7 +687,7 @@ public abstract class FinSet extends ExternalComponent {
        protected void copyFrom(RocketComponent c) {
                super.copyFrom(c);
                
-               FinSet src = (FinSet)c;
+               FinSet src = (FinSet) c;
                this.fins = src.fins;
                this.finRotation = src.finRotation;
                this.rotation = src.rotation;
index 19f9e0ada6e4be564a0458358893f0f70b731a78..3e76b2514206a2588a7d22414d95a26d0f88ddb9 100644 (file)
@@ -8,23 +8,23 @@ import net.sf.openrocket.util.Coordinate;
 
 
 public class FreeformFinSet extends FinSet {
-
+       
        private ArrayList<Coordinate> points = new ArrayList<Coordinate>();
        
        public FreeformFinSet() {
                points.add(Coordinate.NUL);
-               points.add(new Coordinate(0.025,0.05));
-               points.add(new Coordinate(0.075,0.05));
-               points.add(new Coordinate(0.05,0));
+               points.add(new Coordinate(0.025, 0.05));
+               points.add(new Coordinate(0.075, 0.05));
+               points.add(new Coordinate(0.05, 0));
                
                this.length = 0.05;
        }
        
-
+       
        public FreeformFinSet(Coordinate[] finpoints) throws IllegalFinPointException {
                setPoints(finpoints);
        }
-
+       
        /*
        public FreeformFinSet(FinSet finset) {
                Coordinate[] finpoints = finset.getFinPoints();
@@ -37,8 +37,8 @@ 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
@@ -55,9 +55,9 @@ public class FreeformFinSet extends FinSet {
                
                try {
                        if (root instanceof Rocket) {
-                               ((Rocket)root).freeze();
+                               ((Rocket) root).freeze();
                        }
-
+                       
                        // Get fin set position and remove fin set
                        final RocketComponent parent = finset.getParent();
                        final int position;
@@ -67,45 +67,45 @@ public class FreeformFinSet extends FinSet {
                        } else {
                                position = -1;
                        }
-
                        
+
                        // Create the freeform fin set
                        Coordinate[] finpoints = finset.getFinPoints();
                        try {
                                freeform = new FreeformFinSet(finpoints);
                        } catch (IllegalFinPointException e) {
                                throw new BugException("Illegal fin points when converting existing fin to " +
-                                               "freeform fin, fin=" + finset + " points="+Arrays.toString(finpoints),
+                                               "freeform fin, fin=" + finset + " points=" + Arrays.toString(finpoints),
                                                e);
                        }
-
+                       
                        // 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() + 
+                               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();
+                               ((Rocket) root).thaw();
                        }
                }
                return freeform;
        }
-
        
        
+
        /**
         * Add a fin point between indices <code>index-1</code> and <code>index</code>.
         * The point is placed at the midpoint of the current segment.
@@ -115,12 +115,12 @@ public class FreeformFinSet extends FinSet {
        public void addPoint(int index) {
                double x0, y0, x1, y1;
                
-               x0 = points.get(index-1).x;
-               y0 = points.get(index-1).y;
+               x0 = points.get(index - 1).x;
+               y0 = points.get(index - 1).y;
                x1 = points.get(index).x;
                y1 = points.get(index).y;
                
-               points.add(index, new Coordinate((x0+x1)/2, (y0+y1)/2));
+               points.add(index, new Coordinate((x0 + x1) / 2, (y0 + y1) / 2));
                // adding a point within the segment affects neither mass nor aerodynamics
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
@@ -136,7 +136,7 @@ public class FreeformFinSet extends FinSet {
         */
        @SuppressWarnings("unchecked")
        public void removePoint(int index) throws IllegalFinPointException {
-               if (index == 0  ||  index == points.size()-1) {
+               if (index == 0 || index == points.size() - 1) {
                        throw new IllegalFinPointException("cannot remove first or last point");
                }
                
@@ -144,7 +144,7 @@ public class FreeformFinSet extends FinSet {
                copy.remove(index);
                validate(copy);
                this.points = copy;
-
+               
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
@@ -155,17 +155,17 @@ public class FreeformFinSet extends FinSet {
        
        public void setPoints(Coordinate[] points) throws IllegalFinPointException {
                ArrayList<Coordinate> list = new ArrayList<Coordinate>(points.length);
-               for (Coordinate p: points) {
+               for (Coordinate p : points) {
                        list.add(p);
                }
                validate(list);
                this.points = list;
                
-               this.length = points[points.length-1].x;
+               this.length = points[points.length - 1].x;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
-
+       
        /**
         * Set the point at position <code>i</code> to coordinates (x,y).
         * <p>
@@ -187,54 +187,54 @@ public class FreeformFinSet extends FinSet {
                if (y < 0)
                        y = 0;
                
-               double x0,y0,x1,y1;
+               double x0, y0, x1, y1;
                
                if (index == 0) {
                        
                        // Restrict point
-                       x = Math.min(x, points.get(points.size()-1).x);
+                       x = Math.min(x, points.get(points.size() - 1).x);
                        y = 0;
                        x0 = Double.NaN;
                        y0 = Double.NaN;
                        x1 = points.get(1).x;
                        y1 = points.get(1).y;
                        
-               } else if (index == points.size()-1) {
+               } else if (index == points.size() - 1) {
                        
                        // Restrict point
                        x = Math.max(x, 0);
                        y = 0;
-                       x0 = points.get(index-1).x;
-                       y0 = points.get(index-1).y;
+                       x0 = points.get(index - 1).x;
+                       y0 = points.get(index - 1).y;
                        x1 = Double.NaN;
                        y1 = Double.NaN;
                        
                } else {
                        
-                       x0 = points.get(index-1).x;
-                       y0 = points.get(index-1).y;
-                       x1 = points.get(index+1).x;
-                       y1 = points.get(index+1).y;
+                       x0 = points.get(index - 1).x;
+                       y0 = points.get(index - 1).y;
+                       x1 = points.get(index + 1).x;
+                       y1 = points.get(index + 1).y;
                        
                }
                
-               
-               
+
+
                // Check for intersecting
                double px0, py0, px1, py1;
                px0 = 0;
                py0 = 0;
-               for (int i=1; i < points.size(); i++) {
+               for (int i = 1; i < points.size(); i++) {
                        px1 = points.get(i).x;
                        py1 = points.get(i).y;
                        
-                       if (i != index-1 && i != index && i != index+1) {
-                               if (intersects(x0,y0,x,y,px0,py0,px1,py1)) {
+                       if (i != index - 1 && i != index && i != index + 1) {
+                               if (intersects(x0, y0, x, y, px0, py0, px1, py1)) {
                                        throw new IllegalFinPointException("segments intersect");
                                }
                        }
-                       if (i != index && i != index+1 && i != index+2) {
-                               if (intersects(x,y,x1,y1,px0,py0,px1,py1)) {
+                       if (i != index && i != index + 1 && i != index + 2) {
+                               if (intersects(x, y, x1, y1, px0, py0, px1, py1)) {
                                        throw new IllegalFinPointException("segments intersect");
                                }
                        }
@@ -245,77 +245,77 @@ public class FreeformFinSet extends FinSet {
                
                if (index == 0) {
                        
-                       System.out.println("Set point zero to x:"+x);
-                       for (int i=1; i < points.size(); i++) {
+                       System.out.println("Set point zero to x:" + x);
+                       for (int i = 1; i < points.size(); i++) {
                                Coordinate c = points.get(i);
                                points.set(i, c.setX(c.x - x));
                        }
                        
                } else {
                        
-                       points.set(index,new Coordinate(x,y));
+                       points.set(index, new Coordinate(x, y));
                        
                }
-               if (index == 0 || index == points.size()-1) {
-                       this.length = points.get(points.size()-1).x;
+               if (index == 0 || index == points.size() - 1) {
+                       this.length = points.get(points.size() - 1).x;
                }
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
        
-       
+
        private boolean intersects(double ax0, double ay0, double ax1, double ay1,
                        double bx0, double by0, double bx1, double by1) {
                
-               double d = ((by1-by0)*(ax1-ax0) - (bx1-bx0)*(ay1-ay0));
+               double d = ((by1 - by0) * (ax1 - ax0) - (bx1 - bx0) * (ay1 - ay0));
                
-               double ua = ((bx1-bx0)*(ay0-by0) - (by1-by0)*(ax0-bx0)) / d;
-               double ub = ((ax1-ax0)*(ay0-by0) - (ay1-ay0)*(ax0-bx0)) / d;
+               double ua = ((bx1 - bx0) * (ay0 - by0) - (by1 - by0) * (ax0 - bx0)) / d;
+               double ub = ((ax1 - ax0) * (ay0 - by0) - (ay1 - ay0) * (ax0 - bx0)) / d;
                
                return (ua >= 0) && (ua <= 1) && (ub >= 0) && (ub <= 1);
        }
        
-
+       
        @Override
        public Coordinate[] getFinPoints() {
                return points.toArray(new Coordinate[0]);
        }
-
+       
        @Override
        public double getSpan() {
                double max = 0;
-               for (Coordinate c: points) {
+               for (Coordinate c : points) {
                        if (c.y > max)
                                max = c.y;
                }
                return max;
        }
-
+       
        @Override
        public String getComponentName() {
                return "Freeform fin set";
        }
-
-
+       
+       
        @SuppressWarnings("unchecked")
        @Override
-       public RocketComponent copy() {
-               RocketComponent c = super.copy();
-               ((FreeformFinSet)c).points = (ArrayList<Coordinate>) this.points.clone();
+       protected RocketComponent copyWithOriginalID() {
+               RocketComponent c = super.copyWithOriginalID();
+               ((FreeformFinSet) c).points = (ArrayList<Coordinate>) this.points.clone();
                return c;
        }
        
        
        private void validate(ArrayList<Coordinate> points) throws IllegalFinPointException {
                final int n = points.size();
-               if (points.get(0).x != 0 || points.get(0).y != 0 || 
-                               points.get(n-1).x < 0 || points.get(n-1).y != 0) {
+               if (points.get(0).x != 0 || points.get(0).y != 0 ||
+                               points.get(n - 1).x < 0 || points.get(n - 1).y != 0) {
                        throw new IllegalFinPointException("Start or end point illegal.");
                }
-               for (int i=0; i < n-1; i++) {
-                       for (int j=i+2; j < n-1; j++) {
-                               if (intersects(points.get(i).x, points.get(i).y, points.get(i+1).x, points.get(i+1).y,
-                                                      points.get(j).x, points.get(j).y, points.get(j+1).x, points.get(j+1).y)) {
+               for (int i = 0; i < n - 1; i++) {
+                       for (int j = i + 2; j < n - 1; j++) {
+                               if (intersects(points.get(i).x, points.get(i).y, points.get(i + 1).x, points.get(i + 1).y,
+                                                               points.get(j).x, points.get(j).y, points.get(j + 1).x, points.get(j + 1).y)) {
                                        throw new IllegalFinPointException("segments intersect");
                                }
                        }
@@ -324,5 +324,5 @@ public class FreeformFinSet extends FinSet {
                        }
                }
        }
-
+       
 }
index 8ef7d1f7bb9bf3b9250256cbebaddbec98834a06..10c0e7ee9acb726c29ab78ee0c15b5c1a739367c 100644 (file)
@@ -5,6 +5,7 @@ import java.util.HashMap;
 import java.util.List;
 
 import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
 
@@ -15,29 +16,29 @@ import net.sf.openrocket.util.MathUtil;
  * 
  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
  */
-public class InnerTube extends ThicknessRingComponent 
-implements Clusterable, RadialParent, MotorMount {
-
+public class InnerTube extends ThicknessRingComponent
+               implements Clusterable, RadialParent, MotorMount {
+       
        private ClusterConfiguration cluster = ClusterConfiguration.SINGLE;
        private double clusterScale = 1.0;
        private double clusterRotation = 0.0;
        
-       
+
        private boolean motorMount = false;
        private HashMap<String, Double> ejectionDelays = new HashMap<String, Double>();
        private HashMap<String, Motor> motors = new HashMap<String, Motor>();
        private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC;
        private double ignitionDelay = 0;
        private double overhang = 0;
-
+       
        
        /**
         * Main constructor.
         */
        public InnerTube() {
                // A-C motor size:
-               this.setOuterRadius(0.019/2);
-               this.setInnerRadius(0.018/2);
+               this.setOuterRadius(0.019 / 2);
+               this.setInnerRadius(0.018 / 2);
                this.setLength(0.070);
        }
        
@@ -46,19 +47,24 @@ implements Clusterable, RadialParent, MotorMount {
        public double getInnerRadius(double x) {
                return getInnerRadius();
        }
-
-
+       
+       
        @Override
        public double getOuterRadius(double x) {
                return getOuterRadius();
        }
        
-
+       
        @Override
        public String getComponentName() {
                return "Inner Tube";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return true;
+       }
+       
        /**
         * Allow all InternalComponents to be added to this component.
         */
@@ -66,9 +72,9 @@ implements Clusterable, RadialParent, MotorMount {
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return InternalComponent.class.isAssignableFrom(type);
        }
-
        
        
+
        /////////////  Cluster methods  //////////////
        
        /**
@@ -87,7 +93,7 @@ implements Clusterable, RadialParent, MotorMount {
                this.cluster = cluster;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
+       
        /**
         * Return the number of tubes in the cluster.
         * @return Number of tubes in the current cluster.
@@ -105,17 +111,17 @@ implements Clusterable, RadialParent, MotorMount {
        public double getClusterScale() {
                return clusterScale;
        }
-
+       
        /**
         * Set the cluster scaling.
         * @see #getClusterScale()
         */
        public void setClusterScale(double scale) {
-               scale = Math.max(scale,0);
+               scale = Math.max(scale, 0);
                if (MathUtil.equals(clusterScale, scale))
                        return;
                clusterScale = scale;
-               fireComponentChangeEvent(new ComponentChangeEvent(this,ComponentChangeEvent.MASS_CHANGE));
+               fireComponentChangeEvent(new ComponentChangeEvent(this, ComponentChangeEvent.MASS_CHANGE));
        }
        
        
@@ -126,8 +132,8 @@ implements Clusterable, RadialParent, MotorMount {
        public double getClusterRotation() {
                return clusterRotation;
        }
-
-
+       
+       
        /**
         * @param rotation the clusterRotation to set
         */
@@ -138,15 +144,15 @@ implements Clusterable, RadialParent, MotorMount {
                this.clusterRotation = rotation;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
-
+       
+       
        /**
         * Return the distance between the closest two cluster inner tube center points.
         * This is equivalent to the cluster scale multiplied by the tube diameter.
         */
        @Override
        public double getClusterSeparation() {
-               return 2*getOuterRadius()*clusterScale;
+               return 2 * getOuterRadius() * clusterScale;
        }
        
        
@@ -154,8 +160,8 @@ implements Clusterable, RadialParent, MotorMount {
                List<Coordinate> list = new ArrayList<Coordinate>(getClusterCount());
                List<Double> points = cluster.getPoints(clusterRotation - getRadialDirection());
                double separation = getClusterSeparation();
-               for (int i=0; i < points.size()/2; i++) {
-                       list.add(new Coordinate(0,points.get(2*i)*separation,points.get(2*i+1)*separation));
+               for (int i = 0; i < points.size() / 2; i++) {
+                       list.add(new Coordinate(0, points.get(2 * i) * separation, points.get(2 * i + 1) * separation));
                }
                return list;
        }
@@ -168,20 +174,23 @@ implements Clusterable, RadialParent, MotorMount {
                int count = getClusterCount();
                if (count == 1)
                        return array;
-
+               
                List<Coordinate> points = getClusterPoints();
-               assert(points.size() == count);
+               if (points.size() != count) {
+                       throw new BugException("Inconsistent cluster configuration, cluster count=" + count +
+                                       " point count=" + points.size());
+               }
                Coordinate[] newArray = new Coordinate[array.length * count];
-               for (int i=0; i < array.length; i++) {
-                       for (int j=0; j < count; j++) {
-                               newArray[i*count + j] = array[i].add(points.get(j));
+               for (int i = 0; i < array.length; i++) {
+                       for (int j = 0; j < count; j++) {
+                               newArray[i * count + j] = array[i].add(points.get(j));
                        }
                }
                
                return newArray;
        }
        
-
+       
 
 
        ////////////////  Motor mount  /////////////////
@@ -190,7 +199,7 @@ implements Clusterable, RadialParent, MotorMount {
        public boolean isMotorMount() {
                return motorMount;
        }
-
+       
        @Override
        public void setMotorMount(boolean mount) {
                if (motorMount == mount)
@@ -213,7 +222,7 @@ implements Clusterable, RadialParent, MotorMount {
                
                return motors.get(id);
        }
-
+       
        @Override
        public void setMotor(String id, Motor motor) {
                if (id == null) {
@@ -228,7 +237,7 @@ implements Clusterable, RadialParent, MotorMount {
                motors.put(id, motor);
                fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
        }
-
+       
        @Override
        public double getMotorDelay(String id) {
                Double delay = ejectionDelays.get(id);
@@ -236,7 +245,7 @@ implements Clusterable, RadialParent, MotorMount {
                        return Motor.PLUGGED;
                return delay;
        }
-
+       
        @Override
        public void setMotorDelay(String id, double delay) {
                ejectionDelays.put(id, delay);
@@ -251,14 +260,14 @@ implements Clusterable, RadialParent, MotorMount {
        
        @Override
        public double getMotorMountDiameter() {
-               return getInnerRadius()*2;
+               return getInnerRadius() * 2;
        }
-
+       
        @Override
        public IgnitionEvent getIgnitionEvent() {
                return ignitionEvent;
        }
-
+       
        @Override
        public void setIgnitionEvent(IgnitionEvent event) {
                if (ignitionEvent == event)
@@ -266,13 +275,13 @@ implements Clusterable, RadialParent, MotorMount {
                ignitionEvent = event;
                fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
        }
-
+       
        
        @Override
        public double getIgnitionDelay() {
                return ignitionDelay;
        }
-
+       
        @Override
        public void setIgnitionDelay(double delay) {
                if (MathUtil.equals(delay, ignitionDelay))
@@ -280,7 +289,7 @@ implements Clusterable, RadialParent, MotorMount {
                ignitionDelay = delay;
                fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
        }
-
+       
        
        @Override
        public double getMotorOverhang() {
@@ -295,7 +304,7 @@ implements Clusterable, RadialParent, MotorMount {
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
-
+       
        @Override
        public Coordinate getMotorPosition(String id) {
                Motor motor = motors.get(id);
@@ -305,10 +314,10 @@ implements Clusterable, RadialParent, MotorMount {
                
                return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang());
        }
-
-       
        
        
+
+
        /*
         * (non-Javadoc)
         * Copy the motor and ejection delay HashMaps.
@@ -317,11 +326,11 @@ implements Clusterable, RadialParent, MotorMount {
         */
        @SuppressWarnings("unchecked")
        @Override
-       public RocketComponent copy() {
-               RocketComponent c = super.copy();
-               ((InnerTube)c).motors = (HashMap<String,Motor>) motors.clone();
-               ((InnerTube)c).ejectionDelays = (HashMap<String,Double>) ejectionDelays.clone();
+       protected RocketComponent copyWithOriginalID() {
+               RocketComponent c = super.copyWithOriginalID();
+               ((InnerTube) c).motors = (HashMap<String, Motor>) motors.clone();
+               ((InnerTube) c).ejectionDelays = (HashMap<String, Double>) ejectionDelays.clone();
                return c;
        }
-
+       
 }
\ No newline at end of file
index 9fbfcae4856706fc68b963a7f35975bc7e21f24a..6e3f7b39d48331697454855f9ce1adbed5695a3c 100644 (file)
@@ -8,7 +8,7 @@ import net.sf.openrocket.util.MathUtil;
 
 
 public class LaunchLug extends ExternalComponent {
-
+       
        private double radius;
        private double thickness;
        
@@ -18,10 +18,10 @@ public class LaunchLug extends ExternalComponent {
        private double shiftY, shiftZ;
        
        
-       
+
        public LaunchLug() {
                super(Position.MIDDLE);
-               radius = 0.01/2;
+               radius = 0.01 / 2;
                thickness = 0.001;
                length = 0.03;
        }
@@ -30,7 +30,7 @@ public class LaunchLug extends ExternalComponent {
        public double getRadius() {
                return radius;
        }
-
+       
        public void setRadius(double radius) {
                if (MathUtil.equals(this.radius, radius))
                        return;
@@ -38,15 +38,15 @@ public class LaunchLug extends ExternalComponent {
                this.thickness = Math.min(this.thickness, this.radius);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        public double getInnerRadius() {
-               return radius-thickness;
+               return radius - thickness;
        }
-
+       
        public void setInnerRadius(double innerRadius) {
                setRadius(innerRadius + thickness);
        }
-
+       
        public double getThickness() {
                return thickness;
        }
@@ -57,12 +57,12 @@ public class LaunchLug extends ExternalComponent {
                this.thickness = MathUtil.clamp(thickness, 0, radius);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        
        public double getRadialDirection() {
                return radialDirection;
        }
-
+       
        public void setRadialDirection(double direction) {
                direction = MathUtil.reduce180(direction);
                if (MathUtil.equals(this.radialDirection, direction))
@@ -70,9 +70,9 @@ public class LaunchLug extends ExternalComponent {
                this.radialDirection = direction;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
-
        
+       
+
        public void setLength(double length) {
                if (MathUtil.equals(this.length, length))
                        return;
@@ -81,7 +81,6 @@ public class LaunchLug extends ExternalComponent {
        }
        
        
-       
 
 
 
@@ -90,21 +89,21 @@ public class LaunchLug extends ExternalComponent {
                super.setRelativePosition(position);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        
        @Override
        public void setPositionValue(double value) {
                super.setPositionValue(value);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
        
        
+
        @Override
        public Coordinate[] shiftCoordinates(Coordinate[] array) {
                array = super.shiftCoordinates(array);
                
-               for (int i=0; i < array.length; i++) {
+               for (int i = 0; i < array.length; i++) {
                        array[i] = array[i].add(0, shiftY, shiftZ);
                }
                
@@ -122,38 +121,38 @@ public class LaunchLug extends ExternalComponent {
                 */
                RocketComponent body;
                double parentRadius;
-
+               
                for (body = this.getParent(); body != null; body = body.getParent()) {
                        if (body instanceof SymmetricComponent)
                                break;
                }
-
+               
                if (body == null) {
                        parentRadius = 0;
                } else {
-                       SymmetricComponent s = (SymmetricComponent)body;
+                       SymmetricComponent s = (SymmetricComponent) body;
                        double x1, x2;
                        x1 = this.toRelative(Coordinate.NUL, body)[0].x;
-                       x2 = this.toRelative(new Coordinate(length,0,0), body)[0].x;
+                       x2 = this.toRelative(new Coordinate(length, 0, 0), body)[0].x;
                        x1 = MathUtil.clamp(x1, 0, body.getLength());
                        x2 = MathUtil.clamp(x2, 0, body.getLength());
                        parentRadius = Math.max(s.getRadius(x1), s.getRadius(x2));
                }
-
+               
                shiftY = Math.cos(radialDirection) * (parentRadius + radius);
                shiftZ = Math.sin(radialDirection) * (parentRadius + radius);
-
-//             System.out.println("Computed shift: y="+shiftY+" z="+shiftZ);
-}
-
+               
+               //              System.out.println("Computed shift: y="+shiftY+" z="+shiftZ);
+       }
        
        
 
+
        @Override
        public double getComponentVolume() {
-               return length * Math.PI * (MathUtil.pow2(radius) - MathUtil.pow2(radius-thickness));
+               return length * Math.PI * (MathUtil.pow2(radius) - MathUtil.pow2(radius - thickness));
        }
-
+       
        @Override
        public Collection<Coordinate> getComponentBounds() {
                ArrayList<Coordinate> set = new ArrayList<Coordinate>();
@@ -161,35 +160,38 @@ public class LaunchLug extends ExternalComponent {
                addBound(set, length, radius);
                return set;
        }
-
+       
        @Override
        public Coordinate getComponentCG() {
-               return new Coordinate(length/2, 0, 0, getComponentMass());
+               return new Coordinate(length / 2, 0, 0, getComponentMass());
        }
-
+       
        @Override
        public String getComponentName() {
                return "Launch lug";
        }
-
+       
        @Override
        public double getLongitudalUnitInertia() {
                // 1/12 * (3 * (r1^2 + r2^2) + h^2)
-               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
-                               MathUtil.pow2(getLength())) / 12;
+               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) + MathUtil.pow2(getLength())) / 12;
        }
-
+       
        @Override
        public double getRotationalUnitInertia() {
                // 1/2 * (r1^2 + r2^2)
-               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius())) / 2;
        }
-
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                // Allow nothing to be attached to a LaunchLug
                return false;
        }
-
+       
 }
index 951d4dc3f4d30d96040c2135e7fbdb4daba0a4bd..0a84e3fe9b91774d901522da985cd3f60fb41f48 100644 (file)
@@ -4,7 +4,7 @@ import net.sf.openrocket.util.MathUtil;
 
 public class MassComponent extends MassObject {
        private double mass = 0;
-
+       
        
        public MassComponent() {
                super();
@@ -14,13 +14,13 @@ public class MassComponent extends MassObject {
                super(length, radius);
                this.mass = mass;
        }
-
-
+       
+       
        @Override
        public double getComponentMass() {
                return mass;
        }
-
+       
        public void setComponentMass(double mass) {
                mass = Math.max(mass, 0);
                if (MathUtil.equals(this.mass, mass))
@@ -34,7 +34,11 @@ public class MassComponent extends MassObject {
        public String getComponentName() {
                return "Mass component";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
        
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
index 38ff5b3d64acb7401d0e9f3fdc8f222fcc9710b3..080ab6356317bc6cd97ee6a68179afc7e85e2731 100644 (file)
@@ -5,7 +5,7 @@ import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.Prefs;
 
 public class Parachute extends RecoveryDevice {
-
+       
        public static final double DEFAULT_CD = 0.8;
        
        private double diameter;
@@ -13,15 +13,15 @@ public class Parachute extends RecoveryDevice {
        private Material lineMaterial;
        private int lineCount = 6;
        private double lineLength = 0.3;
-
+       
        
        public Parachute() {
                this.diameter = 0.3;
                this.lineMaterial = Prefs.getDefaultComponentMaterial(Parachute.class, Material.Type.LINE);
                this.lineLength = 0.3;
        }
-
-
+       
+       
        public double getDiameter() {
                return diameter;
        }
@@ -40,7 +40,7 @@ public class Parachute extends RecoveryDevice {
        
        public final void setLineMaterial(Material mat) {
                if (mat.getType() != Material.Type.LINE) {
-                       throw new IllegalArgumentException("Attempted to set non-line material "+mat);
+                       throw new IllegalArgumentException("Attempted to set non-line material " + mat);
                }
                if (mat.equals(lineMaterial))
                        return;
@@ -51,7 +51,7 @@ public class Parachute extends RecoveryDevice {
                        fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
        
-               
+       
        public final int getLineCount() {
                return lineCount;
        }
@@ -76,16 +76,16 @@ public class Parachute extends RecoveryDevice {
                else
                        fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
-
-
+       
+       
        @Override
        public double getComponentCD(double mach) {
-               return DEFAULT_CD;  // TODO: HIGH:  Better parachute CD estimate?
+               return DEFAULT_CD; // TODO: HIGH:  Better parachute CD estimate?
        }
        
        @Override
        public double getArea() {
-               return Math.PI * MathUtil.pow2(diameter/2);
+               return Math.PI * MathUtil.pow2(diameter / 2);
        }
        
        public void setArea(double area) {
@@ -97,18 +97,23 @@ public class Parachute extends RecoveryDevice {
        
        @Override
        public double getComponentMass() {
-               return super.getComponentMass() + 
-                       getLineCount() * getLineLength() * getLineMaterial().getDensity();
+               return super.getComponentMass() +
+                               getLineCount() * getLineLength() * getLineMaterial().getDensity();
        }
-
+       
        @Override
        public String getComponentName() {
                return "Parachute";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return false;
        }
-
+       
 }
index 9ab110b49c75996e2a5efb337638d980a0cf43ef..a347bc07102b8f7d12a01a149c2ef3bbdd8c1613 100644 (file)
@@ -16,6 +16,7 @@ import net.sf.openrocket.motor.Motor;
 import net.sf.openrocket.util.Chars;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.UniqueID;
 
 
 /**
@@ -32,14 +33,7 @@ public class Rocket extends RocketComponent {
        public static final double DEFAULT_REFERENCE_LENGTH = 0.01;
        
        private static final boolean DEBUG_LISTENERS = false;
-
        
-       /**
-        * The next modification ID to use.  This variable may only be accessed via
-        * the synchronized {@link #getNextModID()} method!
-        */
-       private static int nextModID = 1;
-
 
        /**
         * List of component change listeners.
@@ -52,26 +46,26 @@ public class Rocket extends RocketComponent {
         */
        private List<ComponentChangeEvent> freezeList = null;
        
-       
+
        private int modID;
        private int massModID;
        private int aeroModID;
        private int treeModID;
        private int functionalModID;
        
-       
-       private ReferenceType refType = ReferenceType.MAXIMUM;  // Set in constructor
+
+       private ReferenceType refType = ReferenceType.MAXIMUM; // Set in constructor
        private double customReferenceLength = DEFAULT_REFERENCE_LENGTH;
        
-       
+
        // The default configuration used in dialogs
        private final Configuration defaultConfiguration;
        
-       
+
        private String designer = "";
        private String revision = "";
        
-       
+
        // Motor configuration list
        private ArrayList<String> motorConfigurationIDs = new ArrayList<String>();
        private HashMap<String, String> motorConfigurationNames = new HashMap<String, String>();
@@ -79,17 +73,17 @@ public class Rocket extends RocketComponent {
                motorConfigurationIDs.add(null);
        }
        
-       
+
        // Does the rocket have a perfect finish (a notable amount of laminar flow)
        private boolean perfectFinish = false;
        
        
-       
+
        /////////////  Constructor  /////////////
        
        public Rocket() {
                super(RocketComponent.Position.AFTER);
-               modID = getNextModID();
+               modID = UniqueID.next();
                massModID = modID;
                aeroModID = modID;
                treeModID = modID;
@@ -98,8 +92,9 @@ public class Rocket extends RocketComponent {
        }
        
        
-       
+
        public String getDesigner() {
+               checkState();
                return designer;
        }
        
@@ -110,8 +105,9 @@ public class Rocket extends RocketComponent {
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
        
-
+       
        public String getRevision() {
+               checkState();
                return revision;
        }
        
@@ -123,7 +119,7 @@ public class Rocket extends RocketComponent {
        }
        
        
-       
+
 
        /**
         * Return the number of stages in this rocket.
@@ -131,11 +127,12 @@ public class Rocket extends RocketComponent {
         * @return   the number of stages in this rocket.
         */
        public int getStageCount() {
+               checkState();
                return this.getChildCount();
        }
        
        
-       
+
        /**
         * Return the non-negative modification ID of this rocket.  The ID is changed
         * every time any change occurs in the rocket.  This can be used to check 
@@ -199,9 +196,10 @@ public class Rocket extends RocketComponent {
        }
        
        
-       
-       
+
+
        public ReferenceType getReferenceType() {
+               checkState();
                return refType;
        }
        
@@ -214,6 +212,7 @@ public class Rocket extends RocketComponent {
        
        
        public double getCustomReferenceLength() {
+               checkState();
                return customReferenceLength;
        }
        
@@ -221,7 +220,7 @@ public class Rocket extends RocketComponent {
                if (MathUtil.equals(customReferenceLength, length))
                        return;
                
-               this.customReferenceLength = Math.max(length,0.001);
+               this.customReferenceLength = Math.max(length, 0.001);
                
                if (refType == ReferenceType.CUSTOM) {
                        fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
@@ -229,9 +228,9 @@ public class Rocket extends RocketComponent {
        }
        
        
-       
-       
-       
+
+
+
        /**
         * Set whether the rocket has a perfect finish.  This will affect whether the
         * boundary layer is assumed to be fully turbulent or not.
@@ -244,8 +243,8 @@ public class Rocket extends RocketComponent {
                this.perfectFinish = perfectFinish;
                fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
        }
-
-
+       
+       
 
        /**
         * Get whether the rocket has a perfect finish.
@@ -255,33 +254,22 @@ public class Rocket extends RocketComponent {
        public boolean isPerfectFinish() {
                return perfectFinish;
        }
+       
+       
 
 
 
        /**
-        * Return a new unique modification ID.  This method is thread-safe.
-        * 
-        * @return  a new modification ID unique to this session.
-        */
-       private synchronized int getNextModID() {
-               return nextModID++;
-       }
-       
-
-       
-       
-       
-       /**
-        * Make a deep copy of the Rocket structure.  This is a helper method which simply 
-        * casts the result of the superclass method to a Rocket.
+        * Make a deep copy of the Rocket structure.  This method is exposed as public to allow
+        * for undo/redo system functionality.
         */
-       @SuppressWarnings("unchecked")
        @Override
-       public Rocket copy() {
-               Rocket copy = (Rocket)super.copy();
+       @SuppressWarnings("unchecked")
+       public Rocket copyWithOriginalID() {
+               Rocket copy = (Rocket) super.copyWithOriginalID();
                copy.motorConfigurationIDs = (ArrayList<String>) this.motorConfigurationIDs.clone();
-               copy.motorConfigurationNames = 
-                       (HashMap<String, String>) this.motorConfigurationNames.clone();
+               copy.motorConfigurationNames =
+                               (HashMap<String, String>) this.motorConfigurationNames.clone();
                copy.resetListeners();
                
                return copy;
@@ -318,8 +306,8 @@ public class Rocket extends RocketComponent {
                this.customReferenceLength = r.customReferenceLength;
                
                this.motorConfigurationIDs = (ArrayList<String>) r.motorConfigurationIDs.clone();
-               this.motorConfigurationNames = 
-                       (HashMap<String, String>) r.motorConfigurationNames.clone();
+               this.motorConfigurationNames =
+                               (HashMap<String, String>) r.motorConfigurationNames.clone();
                this.perfectFinish = r.perfectFinish;
                
                String id = defaultConfiguration.getMotorConfigurationID();
@@ -328,10 +316,10 @@ public class Rocket extends RocketComponent {
                
                fireComponentChangeEvent(type);
        }
-
-       
        
        
+
+
        ///////  Implement the ComponentChangeListener lists
        
        /**
@@ -339,56 +327,61 @@ public class Rocket extends RocketComponent {
         * the structure.
         */
        public void resetListeners() {
-//             System.out.println("RESETTING LISTENER LIST of Rocket "+this);
+               //              System.out.println("RESETTING LISTENER LIST of Rocket "+this);
                listenerList = new EventListenerList();
        }
        
        
        public void printListeners() {
-               System.out.println(""+this+" has "+listenerList.getListenerCount()+" listeners:");
+               System.out.println("" + this + " has " + listenerList.getListenerCount() + " listeners:");
                Object[] list = listenerList.getListenerList();
-               for (int i=1; i<list.length; i+=2)
-                       System.out.println("  "+((i+1)/2)+": "+list[i]);
+               for (int i = 1; i < list.length; i += 2)
+                       System.out.println("  " + ((i + 1) / 2) + ": " + list[i]);
        }
        
        @Override
        public void addComponentChangeListener(ComponentChangeListener l) {
-               listenerList.add(ComponentChangeListener.class,l);
+               checkState();
+               listenerList.add(ComponentChangeListener.class, l);
                if (DEBUG_LISTENERS)
-                       System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
-                                       " listeners): "+l);
+                       System.out.println(this + ": Added listner (now " + listenerList.getListenerCount() +
+                                       " listeners): " + l);
        }
+       
        @Override
        public void removeComponentChangeListener(ComponentChangeListener l) {
                listenerList.remove(ComponentChangeListener.class, l);
                if (DEBUG_LISTENERS)
-                       System.out.println(this+": Removed listner (now "+listenerList.getListenerCount()+
-                                       " listeners): "+l);
+                       System.out.println(this + ": Removed listner (now " + listenerList.getListenerCount() +
+                                       " listeners): " + l);
        }
        
-
+       
        @Override
        public void addChangeListener(ChangeListener l) {
-               listenerList.add(ChangeListener.class,l);
+               checkState();
+               listenerList.add(ChangeListener.class, l);
                if (DEBUG_LISTENERS)
-                       System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
-                                       " listeners): "+l);
+                       System.out.println(this + ": Added listner (now " + listenerList.getListenerCount() +
+                                       " listeners): " + l);
        }
+       
        @Override
        public void removeChangeListener(ChangeListener l) {
                listenerList.remove(ChangeListener.class, l);
                if (DEBUG_LISTENERS)
-                       System.out.println(this+": Removed listner (now "+listenerList.getListenerCount()+
-                                       " listeners): "+l);
+                       System.out.println(this + ": Removed listner (now " + listenerList.getListenerCount() +
+                                       " listeners): " + l);
        }
-
+       
        
        @Override
        protected void fireComponentChangeEvent(ComponentChangeEvent e) {
-
+               checkState();
+               
                // Update modification ID's only for normal (not undo/redo) events
                if (!e.isUndoChange()) {
-                       modID = getNextModID();
+                       modID = UniqueID.next();
                        if (e.isMassChange())
                                massModID = modID;
                        if (e.isAerodynamicChange())
@@ -400,7 +393,7 @@ public class Rocket extends RocketComponent {
                }
                
                if (DEBUG_LISTENERS)
-                       System.out.println("FIRING "+e);
+                       System.out.println("FIRING " + e);
                
                // Check whether frozen
                if (freezeList != null) {
@@ -413,19 +406,19 @@ public class Rocket extends RocketComponent {
                while (iterator.hasNext()) {
                        iterator.next().componentChanged(e);
                }
-
+               
                // Notify all listeners
                Object[] listeners = listenerList.getListenerList();
-               for (int i = listeners.length-2; i>=0; i-=2) {
+               for (int i = listeners.length - 2; i >= 0; i -= 2) {
                        if (listeners[i] == ComponentChangeListener.class) {
-                               ((ComponentChangeListener) listeners[i+1]).componentChanged(e);
+                               ((ComponentChangeListener) listeners[i + 1]).componentChanged(e);
                        } else if (listeners[i] == ChangeListener.class) {
-                               ((ChangeListener) listeners[i+1]).stateChanged(e);
+                               ((ChangeListener) listeners[i + 1]).stateChanged(e);
                        }
                }
        }
        
-               
+       
        /**
         * Freezes the rocket structure from firing any events.  This may be performed to
         * combine several actions on the structure into a single large action.
@@ -445,8 +438,10 @@ public class Rocket extends RocketComponent {
         * @see #thaw()
         */
        public void freeze() {
-               if (freezeList == null)
+               checkState();
+               if (freezeList == null) {
                        freezeList = new LinkedList<ComponentChangeEvent>();
+               }
        }
        
        /**
@@ -457,30 +452,31 @@ public class Rocket extends RocketComponent {
         * @see #freeze()
         */
        public void thaw() {
+               checkState();
                if (freezeList == null)
                        return;
-               if (freezeList.size()==0) {
+               if (freezeList.size() == 0) {
                        freezeList = null;
                        return;
                }
                
                int type = 0;
                Object c = null;
-               for (ComponentChangeEvent e: freezeList) {
+               for (ComponentChangeEvent e : freezeList) {
                        type = type | e.getType();
                        c = e.getSource();
                }
                freezeList = null;
                
-               fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent)c,type));
+               fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent) c, type));
        }
        
        
 
-       
+
        ////////  Motor configurations  ////////
        
-       
+
        /**
         * Return the default configuration.  This should be used in the user interface
         * to ensure a consistent rocket configuration between dialogs.  It should NOT
@@ -489,6 +485,7 @@ public class Rocket extends RocketComponent {
         * @return   the default {@link Configuration}.
         */
        public Configuration getDefaultConfiguration() {
+               checkState();
                return defaultConfiguration;
        }
        
@@ -500,6 +497,7 @@ public class Rocket extends RocketComponent {
         * @return  an array of the motor configuration IDs.
         */
        public String[] getMotorConfigurationIDs() {
+               checkState();
                return motorConfigurationIDs.toArray(new String[0]);
        }
        
@@ -510,6 +508,7 @@ public class Rocket extends RocketComponent {
         * @return  the new motor configuration ID.
         */
        public String newMotorConfigurationID() {
+               checkState();
                String id = UUID.randomUUID().toString();
                motorConfigurationIDs.add(id);
                fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
@@ -523,13 +522,14 @@ public class Rocket extends RocketComponent {
         * @return              true if successful, false if the ID was already used.
         */
        public boolean addMotorConfigurationID(String id) {
+               checkState();
                if (id == null || motorConfigurationIDs.contains(id))
                        return false;
                motorConfigurationIDs.add(id);
                fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
                return true;
        }
-
+       
        /**
         * Remove a motor configuration ID from the configuration IDs.  The <code>null</code>
         * ID cannot be removed, and an attempt to remove it will be silently ignored.
@@ -537,12 +537,13 @@ public class Rocket extends RocketComponent {
         * @param id   the motor configuration ID to remove
         */
        public void removeMotorConfigurationID(String id) {
+               checkState();
                if (id == null)
                        return;
                motorConfigurationIDs.remove(id);
                fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
        }
-
+       
        
        /**
         * Check whether <code>id</code> is a valid motor configuration ID.
@@ -551,11 +552,12 @@ public class Rocket extends RocketComponent {
         * @return              whether a motor configuration with that ID exists.
         */
        public boolean isMotorConfigurationID(String id) {
+               checkState();
                return motorConfigurationIDs.contains(id);
        }
        
        
-       
+
        /**
         * Check whether the given motor configuration ID has motors defined for it.
         * 
@@ -563,13 +565,14 @@ public class Rocket extends RocketComponent {
         * @return              whether any motors are defined for it.
         */
        public boolean hasMotors(String id) {
+               checkState();
                if (id == null)
                        return false;
                
                Iterator<RocketComponent> iterator = this.deepIterator();
                while (iterator.hasNext()) {
                        RocketComponent c = iterator.next();
-
+                       
                        if (c instanceof MotorMount) {
                                MotorMount mount = (MotorMount) c;
                                if (!mount.isMotorMount())
@@ -591,6 +594,7 @@ public class Rocket extends RocketComponent {
         * @return         the configuration name
         */
        public String getMotorConfigurationName(String id) {
+               checkState();
                if (!isMotorConfigurationID(id))
                        return "";
                String s = motorConfigurationNames.get(id);
@@ -608,7 +612,8 @@ public class Rocket extends RocketComponent {
         * @param name  the name for the motor configuration
         */
        public void setMotorConfigurationName(String id, String name) {
-               motorConfigurationNames.put(id,name);
+               checkState();
+               motorConfigurationNames.put(id, name);
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
        
@@ -620,12 +625,13 @@ public class Rocket extends RocketComponent {
         * @return    a textual representation of the configuration
         */
        public String getMotorConfigurationNameOrDescription(String id) {
+               checkState();
                String name;
                
                name = getMotorConfigurationName(id);
-               if (name != null  &&  !name.equals(""))
+               if (name != null && !name.equals(""))
                        return name;
-
+               
                return getMotorConfigurationDescription(id);
        }
        
@@ -638,6 +644,7 @@ public class Rocket extends RocketComponent {
         */
        @SuppressWarnings("null")
        public String getMotorConfigurationDescription(String id) {
+               checkState();
                String name;
                int motorCount = 0;
                
@@ -664,7 +671,7 @@ public class Rocket extends RocketComponent {
                                if (mount.isMotorMount() && motor != null) {
                                        String designation = motor.getDesignation(mount.getMotorDelay(id));
                                        
-                                       for (int i=0; i < mount.getMotorCount(); i++) {
+                                       for (int i = 0; i < mount.getMotorCount(); i++) {
                                                currentList.add(designation);
                                                motorCount++;
                                        }
@@ -680,13 +687,13 @@ public class Rocket extends RocketComponent {
                // Change multiple occurrences of a motor to n x motor 
                List<String> stages = new ArrayList<String>();
                
-               for (List<String> stage: list) {
+               for (List<String> stage : list) {
                        String stageName = "";
                        String previous = null;
                        int count = 0;
                        
                        Collections.sort(stage);
-                       for (String current: stage) {
+                       for (String current : stage) {
                                if (current.equals(previous)) {
                                        
                                        count++;
@@ -730,11 +737,11 @@ public class Rocket extends RocketComponent {
                }
                
                name = "[";
-               for (int i=0; i < stages.size(); i++) {
+               for (int i = 0; i < stages.size(); i++) {
                        String s = stages.get(i);
                        if (s.equals(""))
                                s = "None";
-                       if (i==0)
+                       if (i == 0)
                                name = name + s;
                        else
                                name = name + "; " + s;
@@ -743,31 +750,31 @@ public class Rocket extends RocketComponent {
                return name;
        }
        
-
+       
 
        ////////  Obligatory component information
        
-       
+
        @Override
        public String getComponentName() {
                return "Rocket";
        }
-
+       
        @Override
        public Coordinate getComponentCG() {
-               return new Coordinate(0,0,0,0);
+               return new Coordinate(0, 0, 0, 0);
        }
-
+       
        @Override
        public double getComponentMass() {
                return 0;
        }
-
+       
        @Override
        public double getLongitudalUnitInertia() {
                return 0;
        }
-
+       
        @Override
        public double getRotationalUnitInertia() {
                return 0;
@@ -777,17 +784,22 @@ public class Rocket extends RocketComponent {
        public Collection<Coordinate> getComponentBounds() {
                return Collections.emptyList();
        }
-
+       
        @Override
        public boolean isAerodynamic() {
                return false;
        }
-
+       
        @Override
        public boolean isMassive() {
                return false;
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return true;
+       }
+       
        /**
         * Allows only <code>Stage</code> components to be added to the type Rocket.
         */
index a945d7b2dd9ecf5a38d063c9a971a93f84216f79..1dd1b324a665165aedf25de99aba91364c233a4d 100644 (file)
@@ -9,20 +9,21 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Stack;
-import java.util.UUID;
 
 import javax.swing.event.ChangeListener;
 
+import net.sf.openrocket.logging.TraceException;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.ChangeSource;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.LineStyle;
 import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.UniqueID;
 
 
-public abstract class RocketComponent implements ChangeSource, Cloneable, 
+public abstract class RocketComponent implements ChangeSource, Cloneable,
                Iterable<RocketComponent> {
-
+       
        /*
         * Text is suitable to the form
         *    Position relative to:  <title>
@@ -40,6 +41,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                ABSOLUTE("Tip of the nose cone");
                
                private String title;
+               
                Position(String title) {
                        this.title = title;
                }
@@ -60,8 +62,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * List of child components of this component.
         */
        private List<RocketComponent> children = new ArrayList<RocketComponent>();
-
        
+
        ////////  Parameters common to all components:
        
        /**
@@ -71,7 +73,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * By default it is zero, i.e. no translation.
         */
        protected double length = 0;
-
+       
        /**
         * Positioning of this component relative to the parent component.
         */
@@ -83,12 +85,12 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         */
        protected double position = 0;
        
-       
+
        // Color of the component, null means to use the default color
        private Color color = null;
        private LineStyle lineStyle = null;
        
-       
+
        // Override mass/CG
        private double overrideMass = 0;
        private boolean massOverriden = false;
@@ -103,14 +105,21 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        
        // User-specified comment
        private String comment = "";
-
+       
        // Unique ID of the component
+       // TODO: CRITICAL: Sort out usage of ID and undo defect
        private String id = null;
        
-       ////  NOTE !!!  All fields must be copied in the method copyFrom()!  ////
-       
+       /**
+        * When invalidated is non-null this component cannot be used anymore.
+        * This is a safety mechanism to prevent accidental use after calling {@link #copyFrom(RocketComponent)}.
+        */
+       private TraceException invalidated = null;
        
+       ////  NOTE !!!  All fields must be copied in the method copyFrom()!  ////
        
+
+
        /**
         * Default constructor.  Sets the name of the component to the component's static name
         * and the relative position of the component.
@@ -119,32 +128,32 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                // These must not fire any events, due to Rocket undo system initialization
                this.name = getComponentName();
                this.relativePosition = relativePosition;
-               this.id = UUID.randomUUID().toString();
+               newID();
        }
        
        
-       
-       
-       
-       ////////////  Methods that must be implemented  ////////////
 
 
+
+       ////////////  Methods that must be implemented  ////////////
+       
+
        /**
         * Static component name.  The name may not vary of the parameters, it must be static.
         */
-       public abstract String getComponentName();  // Static component type name
-
+       public abstract String getComponentName(); // Static component type name
+       
        /**
         * Return the component mass (regardless of mass overriding).
         */
-       public abstract double getComponentMass();  // Mass of non-overridden component
-
+       public abstract double getComponentMass(); // Mass of non-overridden component
+       
        /**
         * Return the component CG and mass (regardless of CG or mass overriding).
         */
-       public abstract Coordinate getComponentCG();    // CG of non-overridden component
-       
+       public abstract Coordinate getComponentCG(); // CG of non-overridden component
        
+
        /**
         * Return the longitudal (around the y- or z-axis) unitary moment of inertia.  
         * The unitary moment of inertia is the moment of inertia with the assumption that
@@ -167,7 +176,14 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        public abstract double getRotationalUnitInertia();
        
        
-       
+       /**
+        * Test whether this component allows any children components.  This method must
+        * return true if and only if {@link #isCompatible(Class)} returns true for any
+        * rocket component class.
+        * 
+        * @return      <code>true</code> if children can be attached to this component, <code>false</code> otherwise.
+        */
+       public abstract boolean allowsChildren();
        
        /**
         * Test whether the given component type can be added to this component.  This type safety
@@ -191,11 +207,12 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @see #isCompatible(Class)
         */
        public final boolean isCompatible(RocketComponent c) {
+               checkState();
                return isCompatible(c.getClass());
        }
        
        
-       
+
        /**
         * Return a collection of bounding coordinates.  The coordinates must be such that
         * the component is fully enclosed in their convex hull.
@@ -203,24 +220,24 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return      a collection of coordinates that bound the component.
         */
        public abstract Collection<Coordinate> getComponentBounds();
-
+       
        /**
         * Return true if the component may have an aerodynamic effect on the rocket.
         */
        public abstract boolean isAerodynamic();
-
+       
        /**
         * Return true if the component may have an effect on the rocket's mass.
         */
        public abstract boolean isMassive();
        
        
-       
-       
 
-       ////////////  Methods that may be overridden  ////////////
 
+
+       ////////////  Methods that may be overridden  ////////////
        
+
        /**
         * Shift the coordinates in the array corresponding to radial movement.  A component
         * that has a radial position must shift the coordinates in this array suitably.
@@ -234,6 +251,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         *                        of the passed array and return the array itself.
         */
        public Coordinate[] shiftCoordinates(Coordinate[] c) {
+               checkState();
                return c;
        }
        
@@ -248,11 +266,12 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         */
        protected void componentChanged(ComponentChangeEvent e) {
                // No-op
+               checkState();
        }
        
-
-       
        
+
+
        /**
         * Return a descriptive name of the component.
         * 
@@ -268,69 +287,92 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                else
                        return name;
        }
-
+       
        
        public final void printStructure() {
-               System.out.println("Rocket structure from '"+this.toString()+"':");
+               System.out.println("Rocket structure from '" + this.toString() + "':");
                printStructure(0);
        }
        
        private void printStructure(int level) {
                String s = "";
                
-               for (int i=0; i < level; i++) {
+               for (int i = 0; i < level; i++) {
                        s += "  ";
                }
-               s += this.toString() + " (" + this.getComponentName()+")";
+               s += this.toString() + " (" + this.getComponentName() + ")";
                System.out.println(s);
                
-               for (RocketComponent c: children) {
-                       c.printStructure(level+1);
+               for (RocketComponent c : children) {
+                       c.printStructure(level + 1);
+               }
+       }
+       
+       
+       /**
+        * Make a deep copy of the rocket component tree structure from this component
+        * downwards for copying purposes.  Each component in the copy will be assigned
+        * a new component ID, making it a safe copy.  This method does not fire any events.
+        * 
+        * @return A deep copy of the structure.
+        */
+       public final RocketComponent copy() {
+               RocketComponent clone = copyWithOriginalID();
+               
+               Iterator<RocketComponent> iterator = clone.deepIterator(true);
+               while (iterator.hasNext()) {
+                       iterator.next().newID();
                }
+               return clone;
        }
        
        
+
        /**
         * Make a deep copy of the rocket component tree structure from this component
-        * downwards.  This method does not fire any events.
+        * downwards while maintaining the component ID's.  The purpose of this method is
+        * to allow copies to be created with the original ID's for the purpose of the
+        * undo/redo mechanism.  This method should not be used for other purposes,
+        * such as copy/paste.  This method does not fire any events.
         * <p>
         * This method must be overridden by any component that refers to mutable objects, 
         * or if some fields should not be copied.  This should be performed by
-        * <code>RocketComponent c = super.copy();</code> and then cloning/modifying the
-        * appropriate fields.
+        * <code>RocketComponent c = super.copyWithOriginalID();</code> and then cloning/modifying
+        * the appropriate fields.
         * <p>
         * This is not performed as serializing/deserializing for performance reasons.
         * 
         * @return A deep copy of the structure.
         */
-       public RocketComponent copy() {
+       protected RocketComponent copyWithOriginalID() {
+               checkState();
                RocketComponent clone;
                try {
-                       clone = (RocketComponent)this.clone();
+                       clone = (RocketComponent) this.clone();
                } catch (CloneNotSupportedException e) {
                        throw new BugException("CloneNotSupportedException encountered, " +
-                                       "report a bug!",e);
+                                       "report a bug!", e);
                }
-
+               
                // Reset all parent/child information
                clone.parent = null;
                clone.children = new ArrayList<RocketComponent>();
-
+               
                // Add copied children to the structure without firing events.
-               for (RocketComponent child: this.children) {
-                       RocketComponent childCopy = child.copy();
+               for (RocketComponent child : this.children) {
+                       RocketComponent childCopy = child.copyWithOriginalID();
                        // Don't use add method since it fires events
                        clone.children.add(childCopy);
                        childCopy.parent = clone;
                }
-
+               
                return clone;
        }
-
-
-       //////////////  Methods that may not be overridden  ////////////
        
        
+       //////////////  Methods that may not be overridden  ////////////
+       
+
 
        ////////// Common parameter setting/getting //////////
        
@@ -339,6 +381,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * to use the default color.
         */
        public final Color getColor() {
+               checkState();
                return color;
        }
        
@@ -346,6 +389,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * Set the color of the object to use in 2D figures.  
         */
        public final void setColor(Color c) {
+               checkState();
                if ((color == null && c == null) ||
                                (color != null && color.equals(c)))
                        return;
@@ -356,19 +400,21 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        
        
        public final LineStyle getLineStyle() {
+               checkState();
                return lineStyle;
        }
        
        public final void setLineStyle(LineStyle style) {
+               checkState();
                if (this.lineStyle == style)
                        return;
                this.lineStyle = style;
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
-
-       
        
        
+
+
        /**
         * Get the current override mass.  The mass is not necessarily in use
         * at the moment.
@@ -376,6 +422,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  the override mass
         */
        public final double getOverrideMass() {
+               checkState();
                return overrideMass;
        }
        
@@ -386,7 +433,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param m  the override mass
         */
        public final void setOverrideMass(double m) {
-               overrideMass = Math.max(m,0);
+               checkState();
+               overrideMass = Math.max(m, 0);
                if (massOverriden)
                        fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
@@ -398,6 +446,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  whether the mass is overridden
         */
        public final boolean isMassOverridden() {
+               checkState();
                return massOverriden;
        }
        
@@ -407,31 +456,34 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param o  whether the mass is overridden
         */
        public final void setMassOverridden(boolean o) {
+               checkState();
                if (massOverriden != o) {
                        massOverriden = o;
                        fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
                }
        }
-
-       
-       
        
        
+
+
+
        /**
         * Return the current override CG.  The CG is not necessarily overridden.
         * 
         * @return  the override CG
         */
        public final Coordinate getOverrideCG() {
+               checkState();
                return getComponentCG().setX(overrideCGX);
        }
-
+       
        /**
         * Return the x-coordinate of the current override CG.
         * 
         * @return      the x-coordinate of the override CG.
         */
        public final double getOverrideCGX() {
+               checkState();
                return overrideCGX;
        }
        
@@ -441,6 +493,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param x  the x-coordinate of the override CG to set.
         */
        public final void setOverrideCGX(double x) {
+               checkState();
                if (MathUtil.equals(overrideCGX, x))
                        return;
                this.overrideCGX = x;
@@ -456,6 +509,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  whether the CG is overridden
         */
        public final boolean isCGOverridden() {
+               checkState();
                return cgOverriden;
        }
        
@@ -465,14 +519,15 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param o  whether the CG is overridden
         */
        public final void setCGOverridden(boolean o) {
+               checkState();
                if (cgOverriden != o) {
                        cgOverriden = o;
                        fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
                }
        }
-
        
        
+
        /**
         * Return whether the mass and/or CG override overrides all subcomponent values
         * as well.  The default implementation is a normal getter/setter implementation,
@@ -484,6 +539,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return      whether the current mass and/or CG override overrides subcomponents as well.
         */
        public boolean getOverrideSubcomponents() {
+               checkState();
                return overrideSubcomponents;
        }
        
@@ -495,6 +551,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param override      whether the mass and/or CG override overrides all subcomponent.
         */
        public void setOverrideSubcomponents(boolean override) {
+               checkState();
                if (overrideSubcomponents != override) {
                        overrideSubcomponents = override;
                        fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
@@ -512,12 +569,13 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return      whether the option to override subcomponents is currently enabled.
         */
        public boolean isOverrideSubcomponentsEnabled() {
+               checkState();
                return isCGOverridden() || isMassOverridden();
        }
        
        
-       
-       
+
+
        /**
         * Get the user-defined name of the component.
         */
@@ -530,8 +588,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * the default name, currently the component name.
         */
        public final void setName(String name) {
-//             System.out.println("Set name called:"+name+" orig:"+this.name);
-               if (name==null || name.matches("^\\s*$"))
+               checkState();
+               if (name == null || name.matches("^\\s*$"))
                        this.name = getComponentName();
                else
                        this.name = name;
@@ -546,6 +604,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  the comment of the component.
         */
        public final String getComment() {
+               checkState();
                return comment;
        }
        
@@ -555,6 +614,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param comment  the comment of the component.
         */
        public final void setComment(String comment) {
+               checkState();
                if (this.comment.equals(comment))
                        return;
                if (comment == null)
@@ -564,8 +624,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
        
-
        
+
        /**
         * Returns the unique ID of the component.
         * 
@@ -575,28 +635,16 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                return id;
        }
        
-       
        /**
-        * Set the unique ID of the component.  If <code>id</code> in <code>null</code> then
-        * this method generates a new unique ID for the component.
-        * <p>
-        * This method should be used only in special cases, such as when creating database
-        * entries with empty IDs.
-        * 
-        * @param id    the ID to set.
+        * Generate a new ID for this component.
         */
-       public final void setID(String id) {
-               if (id == null) {
-                       this.id = UUID.randomUUID().toString();
-               } else {
-                       this.id = id;
-               }
-               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);     
+       private final void newID() {
+               this.id = UniqueID.uuid();
        }
        
        
-       
-       
+
+
        /**
         * Get the characteristic length of the component, for example the length of a body tube
         * of the length of the root chord of a fin.  This is used in positioning the component
@@ -606,15 +654,17 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * itself.
         */
        public final double getLength() {
+               checkState();
                return length;
        }
-
+       
        /**
         * Get the positioning of the component relative to its parent component.
         * This is one of the enums of {@link Position}.  A setter method is not provided,
         * but can be provided by a subclass.
         */
        public final Position getRelativePosition() {
+               checkState();
                return relativePosition;
        }
        
@@ -631,42 +681,43 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param position      the relative positioning.
         */
        protected void setRelativePosition(RocketComponent.Position position) {
+               checkState();
                if (this.relativePosition == position)
                        return;
                
                // Update position so as not to move the component
                if (this.parent != null) {
-                       double thisPos = this.toRelative(Coordinate.NUL,this.parent)[0].x;
-
+                       double thisPos = this.toRelative(Coordinate.NUL, this.parent)[0].x;
+                       
                        switch (position) {
                        case ABSOLUTE:
                                this.position = this.toAbsolute(Coordinate.NUL)[0].x;
                                break;
-                               
+                       
                        case TOP:
                                this.position = thisPos;
                                break;
-                               
+                       
                        case MIDDLE:
-                               this.position = thisPos - (this.parent.length - this.length)/2;
+                               this.position = thisPos - (this.parent.length - this.length) / 2;
                                break;
-                               
+                       
                        case BOTTOM:
                                this.position = thisPos - (this.parent.length - this.length);
                                break;
-                               
+                       
                        default:
-                               assert(false): "Should not occur";
+                               throw new BugException("Unknown position type: " + position);
                        }
                }
                
                this.relativePosition = position;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
-
        
        
+
+
        /**
         * Get the position value of the component.  The exact meaning of the value is
         * dependent on the current relative positioning.
@@ -674,10 +725,11 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  the positional value.
         */
        public final double getPositionValue() {
+               checkState();
                return position;
        }
        
-
+       
        /**
         * Set the position value of the component.  The exact meaning of the value
         * depends on the current relative positioning.
@@ -690,23 +742,25 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param value         the position value of the component.
         */
        public void setPositionValue(double value) {
+               checkState();
                if (MathUtil.equals(this.position, value))
                        return;
                this.position = value;
        }
-
        
        
-       ///////////  Coordinate changes  ///////////
 
+       ///////////  Coordinate changes  ///////////
+       
        /**
         * Returns coordinate c in absolute coordinates.  Equivalent to toComponent(c,null).
         */
        public Coordinate[] toAbsolute(Coordinate c) {
-               return toRelative(c,null);
+               checkState();
+               return toRelative(c, null);
        }
        
-
+       
        /**
         * Return coordinate <code>c</code> described in the coordinate system of 
         * <code>dest</code>.  If <code>dest</code> is <code>null</code> returns
@@ -724,6 +778,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         *                         relative to <code>dest</code>.
         */
        public final Coordinate[] toRelative(Coordinate c, RocketComponent dest) {
+               checkState();
                double absoluteX = Double.NaN;
                RocketComponent search = dest;
                Coordinate[] array = new Coordinate[1];
@@ -731,72 +786,72 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                
                RocketComponent component = this;
                while ((component != search) && (component.parent != null)) {
-
+                       
                        array = component.shiftCoordinates(array);
                        
                        switch (component.relativePosition) {
                        case TOP:
-                               for (int i=0; i < array.length; i++) {
-                                       array[i] = array[i].add(component.position,0,0);
+                               for (int i = 0; i < array.length; i++) {
+                                       array[i] = array[i].add(component.position, 0, 0);
                                }
                                break;
-                               
+                       
                        case MIDDLE:
-                               for (int i=0; i < array.length; i++) {
-                                       array[i] = array[i].add(component.position + 
-                                                       (component.parent.length-component.length)/2,0,0);
+                               for (int i = 0; i < array.length; i++) {
+                                       array[i] = array[i].add(component.position +
+                                                       (component.parent.length - component.length) / 2, 0, 0);
                                }
                                break;
-                               
+                       
                        case BOTTOM:
-                               for (int i=0; i < array.length; i++) {
-                                       array[i] = array[i].add(component.position + 
-                                                       (component.parent.length-component.length),0,0);
+                               for (int i = 0; i < array.length; i++) {
+                                       array[i] = array[i].add(component.position +
+                                                       (component.parent.length - component.length), 0, 0);
                                }
                                break;
-                               
+                       
                        case AFTER:
                                // Add length of all previous brother-components with POSITION_RELATIVE_AFTER
                                int index = component.parent.children.indexOf(component);
-                               assert(index >= 0);
+                               assert (index >= 0);
                                for (index--; index >= 0; index--) {
                                        RocketComponent comp = component.parent.children.get(index);
                                        double length = comp.getTotalLength();
-                                       for (int i=0; i < array.length; i++) {
-                                               array[i] = array[i].add(length,0,0);
+                                       for (int i = 0; i < array.length; i++) {
+                                               array[i] = array[i].add(length, 0, 0);
                                        }
                                }
-                               for (int i=0; i < array.length; i++) {
-                                       array[i] = array[i].add(component.position + component.parent.length,0,0);
+                               for (int i = 0; i < array.length; i++) {
+                                       array[i] = array[i].add(component.position + component.parent.length, 0, 0);
                                }
                                break;
-                               
+                       
                        case ABSOLUTE:
-                               search = null;  // Requires back-search if dest!=null
+                               search = null; // Requires back-search if dest!=null
                                if (Double.isNaN(absoluteX)) {
                                        absoluteX = component.position;
                                }
                                break;
-                               
+                       
                        default:
-                               throw new BugException("Unknown relative positioning type of component"+
-                                               component+": "+component.relativePosition);
+                               throw new BugException("Unknown relative positioning type of component" +
+                                               component + ": " + component.relativePosition);
                        }
-
-                       component = component.parent;  // parent != null
+                       
+                       component = component.parent; // parent != null
                }
-
+               
                if (!Double.isNaN(absoluteX)) {
-                       for (int i=0; i < array.length; i++) {
+                       for (int i = 0; i < array.length; i++) {
                                array[i] = array[i].setX(absoluteX + c.x);
                        }
                }
-
+               
                // Check whether destination has been found or whether to backtrack
                // TODO: LOW: Backtracking into clustered components uses only one component 
                if ((dest != null) && (component != dest)) {
                        Coordinate[] origin = dest.toAbsolute(Coordinate.NUL);
-                       for (int i=0; i < array.length; i++) {
+                       for (int i = 0; i < array.length; i++) {
                                array[i] = array[i].sub(origin[0]);
                        }
                }
@@ -812,24 +867,26 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  Sum of the lengths.
         */
        private final double getTotalLength() {
-               double l=0;
+               checkState();
+               double l = 0;
                if (relativePosition == Position.AFTER)
                        l = length;
-               for (int i=0; i<children.size(); i++)
+               for (int i = 0; i < children.size(); i++)
                        l += children.get(i).getTotalLength();
                return l;
        }
        
-
        
-       /////////// Total mass and CG calculation ////////////
 
+       /////////// Total mass and CG calculation ////////////
+       
        /**
         * Return the (possibly overridden) mass of component.
         * 
         * @return The mass of the component or the given override mass.
         */
        public final double getMass() {
+               checkState();
                if (massOverriden)
                        return overrideMass;
                return getComponentMass();
@@ -844,16 +901,17 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return The CG of the component or the given override CG.
         */
        public final Coordinate getCG() {
+               checkState();
                if (cgOverriden)
                        return getOverrideCG().setWeight(getMass());
-
+               
                if (massOverriden)
                        return getComponentCG().setWeight(getMass());
                
                return getComponentCG();
        }
        
-
+       
        /**
         * Return the longitudal (around the y- or z-axis) moment of inertia of this component.
         * The moment of inertia is scaled in reference to the (possibly overridden) mass
@@ -862,6 +920,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return    the longitudal moment of inertia of this component.
         */
        public final double getLongitudalInertia() {
+               checkState();
                return getLongitudalUnitInertia() * getMass();
        }
        
@@ -873,11 +932,12 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return    the rotational moment of inertia of this component.
         */
        public final double getRotationalInertia() {
+               checkState();
                return getRotationalUnitInertia() * getMass();
        }
        
        
-       
+
        ///////////  Children handling  ///////////
        
 
@@ -892,9 +952,10 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @see #addChild(RocketComponent,int)
         */
        public final void addChild(RocketComponent component) {
-               addChild(component,children.size());
+               checkState();
+               addChild(component, children.size());
        }
-
+       
        
        /**
         * Adds a child to the rocket component tree.  The component is added to 
@@ -909,16 +970,17 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         *                                                                       some component tree.
         */
        public void addChild(RocketComponent component, int position) {
+               checkState();
                if (component.parent != null) {
-                       throw new IllegalArgumentException("component "+component.getComponentName()+
+                       throw new IllegalArgumentException("component " + component.getComponentName() +
                                        " is already in a tree");
                }
                if (!isCompatible(component)) {
-                       throw new IllegalStateException("Component "+component.getComponentName()+
-                                       " not currently compatible with component "+getComponentName());
+                       throw new IllegalStateException("Component " + component.getComponentName() +
+                                       " not currently compatible with component " + getComponentName());
                }
                
-               children.add(position,component);
+               children.add(position, component);
                component.parent = this;
                
                fireAddRemoveEvent(component);
@@ -932,6 +994,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @throws IndexOutOfBoundsException  if n is out of bounds
         */
        public final void removeChild(int n) {
+               checkState();
                RocketComponent component = children.remove(n);
                component.parent = null;
                fireAddRemoveEvent(component);
@@ -941,19 +1004,22 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * Removes a child from the rocket component tree.  Does nothing if the component
         * is not present as a child.
         * 
-        * @param component  the component to remove
+        * @param component             the component to remove
+        * @return                              whether the component was a child
         */
-       public final void removeChild(RocketComponent component) {
+       public final boolean removeChild(RocketComponent component) {
+               checkState();
                if (children.remove(component)) {
                        component.parent = null;
-                       
                        fireAddRemoveEvent(component);
+                       return true;
                }
+               return false;
        }
-
        
-
        
+
+
        /**
         * Move a child to another position.
         * 
@@ -962,6 +1028,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @throws IllegalArgumentException If an illegal placement was attempted.
         */
        public final void moveChild(RocketComponent component, int position) {
+               checkState();
                if (children.remove(component)) {
                        children.add(position, component);
                        fireAddRemoveEvent(component);
@@ -989,14 +1056,17 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        
        
        public final int getChildCount() {
+               checkState();
                return children.size();
        }
        
        public final RocketComponent getChild(int n) {
+               checkState();
                return children.get(n);
        }
        
        public final RocketComponent[] getChildren() {
+               checkState();
                return children.toArray(new RocketComponent[0]);
        }
        
@@ -1009,6 +1079,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  Position in the list or -1 if not found.
         */
        public final int getChildPosition(RocketComponent child) {
+               checkState();
                return children.indexOf(child);
        }
        
@@ -1019,6 +1090,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  The parent of this component or <code>null</code>.
         */
        public final RocketComponent getParent() {
+               checkState();
                return parent;
        }
        
@@ -1028,6 +1100,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return  The root component of the component tree.
         */
        public final RocketComponent getRoot() {
+               checkState();
                RocketComponent gp = this;
                while (gp.parent != null)
                        gp = gp.parent;
@@ -1042,11 +1115,12 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @throws  IllegalStateException  If the root component is not a Rocket.
         */
        public final Rocket getRocket() {
+               checkState();
                RocketComponent r = getRoot();
                if (r instanceof Rocket)
-                       return (Rocket)r;
+                       return (Rocket) r;
                throw new IllegalStateException("getRocket() called with root component "
-                               +r.getComponentName());
+                               + r.getComponentName());
        }
        
        
@@ -1058,10 +1132,11 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @throws      IllegalStateException   if a Stage component is not in the parentage.
         */
        public final Stage getStage() {
+               checkState();
                RocketComponent c = this;
                while (c != null) {
                        if (c instanceof Stage)
-                               return (Stage)c;
+                               return (Stage) c;
                        c = c.getParent();
                }
                throw new IllegalStateException("getStage() called without Stage as a parent.");
@@ -1074,6 +1149,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return   the stage number this component belongs to.
         */
        public final int getStageNumber() {
+               checkState();
                if (parent == null) {
                        throw new IllegalArgumentException("getStageNumber() called for root component");
                }
@@ -1095,21 +1171,23 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * down (including this component) for the ID and the corresponding component is returned,
         * or null if not found.
         * 
-        * @param id  ID to search for.
+        * @param idToFind  ID to search for.
         * @return    The component with the ID, or null if not found.
         */
-       public final RocketComponent findComponent(String id) {
+       public final RocketComponent findComponent(String idToFind) {
+               checkState();
                Iterator<RocketComponent> iter = this.deepIterator(true);
                while (iter.hasNext()) {
                        RocketComponent c = iter.next();
-                       if (c.id.equals(id))
+                       if (c.getID().equals(idToFind))
                                return c;
                }
                return null;
        }
-
+       
        
        public final RocketComponent getPreviousComponent() {
+               checkState();
                if (parent == null)
                        return null;
                int pos = parent.getChildPosition(this);
@@ -1117,27 +1195,28 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                        StringBuffer sb = new StringBuffer();
                        sb.append("Inconsistent internal state: ");
                        sb.append("this=").append(this).append('[')
-                               .append(System.identityHashCode(this)).append(']');
+                                       .append(System.identityHashCode(this)).append(']');
                        sb.append(" parent.children=[");
-                       for (int i=0; i < parent.children.size(); i++) {
+                       for (int i = 0; i < parent.children.size(); i++) {
                                RocketComponent c = parent.children.get(i);
                                sb.append(c).append('[').append(System.identityHashCode(c)).append(']');
-                               if (i < parent.children.size()-1)
+                               if (i < parent.children.size() - 1)
                                        sb.append(", ");
                        }
                        sb.append(']');
                        throw new IllegalStateException(sb.toString());
                }
-               assert(pos >= 0);
+               assert (pos >= 0);
                if (pos == 0)
                        return parent;
-               RocketComponent c = parent.getChild(pos-1);
+               RocketComponent c = parent.getChild(pos - 1);
                while (c.getChildCount() > 0)
-                       c = c.getChild(c.getChildCount()-1);
+                       c = c.getChild(c.getChildCount() - 1);
                return c;
        }
        
        public final RocketComponent getNextComponent() {
+               checkState();
                if (getChildCount() > 0)
                        return getChild(0);
                
@@ -1146,9 +1225,9 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                
                while (parent != null) {
                        int pos = parent.getChildPosition(current);
-                       if (pos < parent.getChildCount()-1)
-                               return parent.getChild(pos+1);
-                               
+                       if (pos < parent.getChildCount() - 1)
+                               return parent.getChild(pos + 1);
+                       
                        current = parent;
                        parent = current.parent;
                }
@@ -1170,6 +1249,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @throws IllegalStateException - if the root component is not a Rocket
         */
        public void addComponentChangeListener(ComponentChangeListener l) {
+               checkState();
                getRocket().addComponentChangeListener(l);
        }
        
@@ -1187,7 +1267,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                }
        }
        
-
+       
        /**
         * Adds a <code>ChangeListener</code> to the rocket tree.  This is identical to 
         * <code>addComponentChangeListener()</code> except that it uses a 
@@ -1198,6 +1278,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @throws IllegalStateException - if the root component is not a <code>Rocket</code>
         */
        public void addChangeListener(ChangeListener l) {
+               checkState();
                getRocket().addChangeListener(l);
        }
        
@@ -1228,13 +1309,14 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param e  Event to send
         */
        protected void fireComponentChangeEvent(ComponentChangeEvent e) {
-               if (parent==null) {
+               checkState();
+               if (parent == null) {
                        /* Ignore if root invalid. */
                        return;
                }
                getRoot().fireComponentChangeEvent(e);
        }
-
+       
        
        /**
         * Fires a ComponentChangeEvent of the given type.  The source of the event is set to
@@ -1244,10 +1326,25 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @see #fireComponentChangeEvent(ComponentChangeEvent)
         */
        protected void fireComponentChangeEvent(int type) {
-               fireComponentChangeEvent(new ComponentChangeEvent(this,type));
+               fireComponentChangeEvent(new ComponentChangeEvent(this, type));
+       }
+       
+       
+       /**
+        * Checks whether this component has been invalidated and should no longer be used.
+        * This is a safety check that in-place replaced components are no longer used.
+        * All non-trivial methods should call this method as the first thing, unless the
+        * method may be used in debugging cases.
+        * 
+        * @throws      BugException    if this component has been invalidated by {@link #copyFrom(RocketComponent)}.
+        */
+       protected void checkState() {
+               if (invalidated != null) {
+                       throw new BugException("This component has been invalidated.  Cause is the point of invalidation.",
+                                       invalidated);
+               }
        }
        
-
        
        ///////////  Iterator implementation  //////////
        
@@ -1263,16 +1360,16 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                
                private final Rocket root;
                private final int treeModID;
-
+               
                private final RocketComponent original;
-               private boolean returnSelf=false;
+               private boolean returnSelf = false;
                
                // Construct iterator with component's child's iterator, if it has elements
                public RocketComponentIterator(RocketComponent c, boolean returnSelf) {
                        
                        RocketComponent gp = c.getRoot();
                        if (gp instanceof Rocket) {
-                               root = (Rocket)gp;
+                               root = (Rocket) gp;
                                treeModID = root.getTreeModID();
                        } else {
                                root = null;
@@ -1288,20 +1385,22 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                }
                
                public boolean hasNext() {
+                       checkState();
                        checkID();
                        if (returnSelf)
                                return true;
-                       return !iteratorstack.empty();  // Elements remain if stack is not empty
+                       return !iteratorstack.empty(); // Elements remain if stack is not empty
                }
-
+               
                public RocketComponent next() {
                        Iterator<RocketComponent> i;
-
+                       
+                       checkState();
                        checkID();
                        
                        // Return original component first
                        if (returnSelf) {
-                               returnSelf=false;
+                               returnSelf = false;
                                return original;
                        }
                        
@@ -1325,7 +1424,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                        
                        return c;
                }
-
+               
                private void checkID() {
                        if (root != null) {
                                if (root.getTreeModID() != treeModID) {
@@ -1339,7 +1438,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                                        "RocketComponent iterator");
                }
        }
-
+       
        /**
         * Returns an iterator that iterates over all children and sub-children.
         * 
@@ -1355,7 +1454,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return An iterator for the children and sub-children.
         */
        public final Iterator<RocketComponent> deepIterator(boolean returnSelf) {
-               return new RocketComponentIterator(this,returnSelf);
+               checkState();
+               return new RocketComponentIterator(this, returnSelf);
        }
        
        /**
@@ -1368,7 +1468,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return An iterator for the children and sub-children.
         */
        public final Iterator<RocketComponent> deepIterator() {
-               return new RocketComponentIterator(this,false);
+               checkState();
+               return new RocketComponentIterator(this, false);
        }
        
        
@@ -1379,54 +1480,85 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @return An iterator for the children.
         */
        public final Iterator<RocketComponent> iterator() {
+               checkState();
                return Collections.unmodifiableList(children).iterator();
        }
        
+       
+
+
+       /**
+        * Compare component equality based on the ID of this component.  Only the
+        * ID and class type is used for a basis of comparison.
+        */
+       @Override
+       public boolean equals(Object obj) {
+               if (this == obj)
+                       return true;
+               if (obj == null)
+                       return false;
+               if (this.getClass() != obj.getClass())
+                       return false;
+               RocketComponent other = (RocketComponent) obj;
+               return this.id.equals(other.id);
+       }
+       
+       
+
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+       
+       
+
        ////////////  Helper methods for subclasses
        
+
+
+
        /**
         * Helper method to add rotationally symmetric bounds at the specified coordinates.
         * The X-axis value is <code>x</code> and the radius at the specified position is
         * <code>r</code>. 
         */
        protected static final void addBound(Collection<Coordinate> bounds, double x, double r) {
-               bounds.add(new Coordinate(x,-r,-r));
-               bounds.add(new Coordinate(x, r,-r));
+               bounds.add(new Coordinate(x, -r, -r));
+               bounds.add(new Coordinate(x, r, -r));
                bounds.add(new Coordinate(x, r, r));
-               bounds.add(new Coordinate(x,-r, r));
+               bounds.add(new Coordinate(x, -r, r));
        }
        
        
-       protected static final Coordinate ringCG(double outerRadius, double innerRadius, 
+       protected static final Coordinate ringCG(double outerRadius, double innerRadius,
                        double x1, double x2, double density) {
-               return new Coordinate((x1+x2)/2, 0, 0, 
-                               ringMass(outerRadius, innerRadius, x2-x1, density));
+               return new Coordinate((x1 + x2) / 2, 0, 0,
+                               ringMass(outerRadius, innerRadius, x2 - x1, density));
        }
        
        protected static final double ringMass(double outerRadius, double innerRadius,
                        double length, double density) {
-               return Math.PI*(MathUtil.pow2(outerRadius) - MathUtil.pow2(innerRadius)) *
+               return Math.PI * (MathUtil.pow2(outerRadius) - MathUtil.pow2(innerRadius)) *
                                        length * density;
        }
-
-       protected static final double ringLongitudalUnitInertia(double outerRadius, 
+       
+       protected static final double ringLongitudalUnitInertia(double outerRadius,
                        double innerRadius, double length) {
                // 1/12 * (3 * (r1^2 + r2^2) + h^2)
-               return (3 * (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) +
-                               MathUtil.pow2(length)) / 12;
+               return (3 * (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) + MathUtil.pow2(length)) / 12;
        }
-
-       protected static final double ringRotationalUnitInertia(double outerRadius, 
+       
+       protected static final double ringRotationalUnitInertia(double outerRadius,
                        double innerRadius) {
                // 1/2 * (r1^2 + r2^2)
-               return (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius))/2;
+               return (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) / 2;
        }
-
-
        
-       ////////////  OTHER
+       
 
+       ////////////  OTHER
        
+
        /**
         * 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
@@ -1434,14 +1566,16 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * 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.
+        * after the call, as it is in an undefined state.  This is enforced by invalidating
+        * the source component.
         * 
         * TODO: MEDIUM: Make general to copy all private/protected fields...
         */
        protected void copyFrom(RocketComponent src) {
+               checkState();
                
                if (this.parent != null) {
-                       throw new UnsupportedOperationException("copyFrom called for non-root component " 
+                       throw new UnsupportedOperationException("copyFrom called for non-root component "
                                        + this);
                }
                
@@ -1449,10 +1583,10 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                this.children = src.children;
                src.children = new ArrayList<RocketComponent>();
                
-               for (RocketComponent c: this.children) {
+               for (RocketComponent c : this.children) {
                        c.parent = this;
                }
-
+               
                // Set all parameters
                this.length = src.length;
                this.relativePosition = src.relativePosition;
@@ -1467,6 +1601,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                this.name = src.name;
                this.comment = src.comment;
                this.id = src.id;
+               
+               src.invalidated = new TraceException();
        }
        
 }
index 6c048c6fe3b32b7b146e403d4197cca84e66781a..6a23a75b871e0026668bece076159d9b56edf9ee 100644 (file)
@@ -6,17 +6,17 @@ import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.Prefs;
 
 public class ShockCord extends MassObject {
-
+       
        private Material material;
        private double cordLength;
-
+       
        public ShockCord() {
                material = Prefs.getDefaultComponentMaterial(ShockCord.class, Material.Type.LINE);
                cordLength = 0.4;
        }
        
        
-       
+
        public Material getMaterial() {
                return material;
        }
@@ -44,20 +44,25 @@ public class ShockCord extends MassObject {
        }
        
        
-       
+
        @Override
        public double getComponentMass() {
                return material.getDensity() * cordLength;
        }
-
+       
        @Override
        public String getComponentName() {
                return "Shock cord";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return false;
        }
-
+       
 }
index fd0db53ef98308c25c42446f5928b061fa156332..ec244ee0ecc5f8000a26f53c46d3cbaffa700b8f 100644 (file)
@@ -12,10 +12,10 @@ import net.sf.openrocket.util.MathUtil;
  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
  */
 public class Sleeve extends RingComponent {
-
+       
        protected double innerRadius = 0;
        protected double thickness = 0;
-
+       
        
        public Sleeve() {
                super();
@@ -51,8 +51,8 @@ public class Sleeve extends RingComponent {
                        double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x;
                        pos1 = MathUtil.clamp(pos1, 0, parent.getLength());
                        pos2 = MathUtil.clamp(pos2, 0, parent.getLength());
-                       innerRadius = Math.max(((RadialParent)parent).getOuterRadius(pos1),
-                                       ((RadialParent)parent).getOuterRadius(pos2));
+                       innerRadius = Math.max(((RadialParent) parent).getOuterRadius(pos1),
+                                       ((RadialParent) parent).getOuterRadius(pos2));
                }
                
                return innerRadius;
@@ -60,7 +60,7 @@ public class Sleeve extends RingComponent {
        
        @Override
        public void setInnerRadius(double r) {
-               r = Math.max(r,0);
+               r = Math.max(r, 0);
                if (MathUtil.equals(innerRadius, r))
                        return;
                innerRadius = r;
@@ -83,7 +83,7 @@ public class Sleeve extends RingComponent {
        
        
 
-       
+
        @Override
        public void setInnerRadiusAutomatic(boolean auto) {
                super.setOuterRadiusAutomatic(auto);
@@ -93,7 +93,12 @@ public class Sleeve extends RingComponent {
        public String getComponentName() {
                return "Sleeve";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return false;
index 74a36a5ebb9bd6b394d370bfbe304b6689a65fb2..361f50232fcd1cd4c3b2d9aec694018e9c348763 100644 (file)
@@ -1,12 +1,17 @@
 package net.sf.openrocket.rocketcomponent;
 
 public class Stage extends ComponentAssembly {
-
+       
        @Override
        public String getComponentName() {
                return "Stage";
        }
-
+       
+       
+       @Override
+       public boolean allowsChildren() {
+               return true;
+       }
        
        /**
         * Check whether the given type can be added to this component.  A Stage allows 
@@ -19,5 +24,5 @@ public class Stage extends ComponentAssembly {
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return BodyComponent.class.isAssignableFrom(type);
        }
-
+       
 }
index 134f3ee9d24ed610f7c531258786e3407ec22cc4..dd071a40716481ef9d3abbd1806a27da1fb9c2ff 100644 (file)
@@ -3,12 +3,12 @@ package net.sf.openrocket.rocketcomponent;
 import net.sf.openrocket.util.MathUtil;
 
 public class Streamer extends RecoveryDevice {
-
+       
        public static final double DEFAULT_CD = 0.6;
        
        public static final double MAX_COMPUTED_CD = 0.4;
        
-       
+
        private double stripLength;
        private double stripWidth;
        
@@ -22,18 +22,18 @@ public class Streamer extends RecoveryDevice {
        public double getStripLength() {
                return stripLength;
        }
-
+       
        public void setStripLength(double stripLength) {
                if (MathUtil.equals(this.stripLength, stripLength))
                        return;
                this.stripLength = stripLength;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        public double getStripWidth() {
                return stripWidth;
        }
-
+       
        public void setStripWidth(double stripWidth) {
                if (MathUtil.equals(this.stripWidth, stripWidth))
                        return;
@@ -46,18 +46,18 @@ public class Streamer extends RecoveryDevice {
        public void setLength(double length) {
                setStripWidth(length);
        }
-
+       
        
        public double getAspectRatio() {
                if (stripWidth > 0.0001)
-                       return stripLength/stripWidth;
+                       return stripLength / stripWidth;
                return 1000;
        }
        
        public void setAspectRatio(double ratio) {
                if (MathUtil.equals(getAspectRatio(), ratio))
                        return;
-                               
+               
                ratio = Math.max(ratio, 0.01);
                double area = getArea();
                stripWidth = Math.sqrt(area / ratio);
@@ -65,12 +65,12 @@ public class Streamer extends RecoveryDevice {
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
-
+       
        @Override
        public double getArea() {
                return stripWidth * stripLength;
        }
-
+       
        public void setArea(double area) {
                if (MathUtil.equals(getArea(), area))
                        return;
@@ -82,22 +82,27 @@ public class Streamer extends RecoveryDevice {
        }
        
        
-       
+
        @Override
        public double getComponentCD(double mach) {
                double density = this.getMaterial().getDensity();
                double cd;
                
-               cd = 0.034 * ((density + 0.025)/0.105) * (stripLength+1) / stripLength;
+               cd = 0.034 * ((density + 0.025) / 0.105) * (stripLength + 1) / stripLength;
                cd = MathUtil.min(cd, MAX_COMPUTED_CD);
                return cd;
        }
-
+       
        @Override
        public String getComponentName() {
                return "Streamer";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return false;
+       }
+       
        @Override
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return false;
index f598ad1eeb6c5500a8be3305990f5c78245dbf44..af559b5ba086dc49d25cdf0858adf816972ee611 100644 (file)
@@ -172,7 +172,7 @@ public class Transition extends SymmetricComponent {
        
        public void setType(Shape type) {
                if (type == null) {
-                       throw new IllegalArgumentException("BUG:  setType called with null argument");
+                       throw new IllegalArgumentException("setType called with null argument");
                }
                if (this.type == type)
                        return;
index d27d2cca25da176439519471a7b8c3918e4b522d..540535e188682760b43e3871f0ac563100178711 100644 (file)
@@ -2,7 +2,7 @@ package net.sf.openrocket.rocketcomponent;
 
 
 public class TubeCoupler extends ThicknessRingComponent implements RadialParent {
-
+       
        public TubeCoupler() {
                setOuterRadiusAutomatic(true);
                setThickness(0.002);
@@ -15,13 +15,18 @@ public class TubeCoupler extends ThicknessRingComponent implements RadialParent
        public void setOuterRadiusAutomatic(boolean auto) {
                super.setOuterRadiusAutomatic(auto);
        }
-
+       
        
        @Override
        public String getComponentName() {
                return "Tube coupler";
        }
-
+       
+       @Override
+       public boolean allowsChildren() {
+               return true;
+       }
+       
        /**
         * Allow all InternalComponents to be added to this component.
         */
@@ -29,17 +34,16 @@ public class TubeCoupler extends ThicknessRingComponent implements RadialParent
        public boolean isCompatible(Class<? extends RocketComponent> type) {
                return InternalComponent.class.isAssignableFrom(type);
        }
-
-
+       
+       
        @Override
        public double getInnerRadius(double x) {
                return getInnerRadius();
        }
-
-
+       
+       
        @Override
        public double getOuterRadius(double x) {
                return getOuterRadius();
        }
 }
-
index 13122a7f10735e1c0b8425afcba07d6e943db3e5..ef80f4979f9b14cc6507383eb8ff2688de3b3513 100644 (file)
@@ -77,7 +77,7 @@ public class BasicEventSimulationEngine implements SimulationEngine {
                                        if (nextEvent != null) {
                                                maxStepTime = MathUtil.max(nextEvent.getTime() - status.getSimulationTime(), 0.001);
                                        }
-                                       log.debug("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime());
+                                       log.verbose("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime());
                                        currentStepper.step(status, maxStepTime);
                                }
                                SimulationListenerHelper.firePostStep(status);
@@ -294,7 +294,7 @@ public class BasicEventSimulationEngine implements SimulationEngine {
                        }
                        
                        if (event.getType() != FlightEvent.Type.ALTITUDE) {
-                               log.debug("BasicEventSimulationEngine:  Handling event " + event);
+                               log.verbose("BasicEventSimulationEngine:  Handling event " + event);
                        }
                        
                        if (event.getType() == FlightEvent.Type.IGNITION) {
index 57fd7c8d541b49ecc84cd84de781db5d742d7a7b..839c658465c7f6fc0084ad437434d5873165f8ac 100644 (file)
@@ -242,7 +242,7 @@ public class FlightData {
                        maxAcceleration = Double.NaN;
                }
                
-               log.info("Computed flight values:" +
+               log.debug("Computed flight values:" +
                                " maxAltitude=" + maxAltitude +
                                " maxVelocity=" + maxVelocity +
                                " maxAcceleration=" + maxAcceleration +
index 8ef74c8581c40791ffab558f99231ad74f8a1ed7..97d934cf5b9272fcf0e9608d966b1af47b091f19 100644 (file)
@@ -155,11 +155,11 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
                
                double minTimeStep = status.getSimulationConditions().getTimeStep() / 20;
                if (store.timestep < minTimeStep) {
-                       log.debug("Too small time step " + store.timestep + " (limiting factor " + limitingValue + "), using " +
+                       log.verbose("Too small time step " + store.timestep + " (limiting factor " + limitingValue + "), using " +
                                        minTimeStep + " instead.");
                        store.timestep = minTimeStep;
                } else {
-                       log.debug("Selected time step " + store.timestep + " (limiting factor " + limitingValue + ")");
+                       log.verbose("Selected time step " + store.timestep + " (limiting factor " + limitingValue + ")");
                }
                checkNaN(store.timestep);
                
@@ -175,7 +175,7 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
                // Log if difference over 1%, recompute if over 10%
                if (thrustDiff > 0.01 * thrustEstimate) {
                        if (thrustDiff > 0.1 * thrustEstimate + 0.001) {
-                               log.info("Thrust estimate differs from correct value by " +
+                               log.debug("Thrust estimate differs from correct value by " +
                                                (Math.rint(1000 * (thrustDiff + 0.000001) / thrustEstimate) / 10.0) + "%," +
                                                " estimate=" + thrustEstimate +
                                                " correct=" + store.thrustForce +
@@ -183,7 +183,7 @@ public class RK4SimulationStepper extends AbstractSimulationStepper {
                                                ", recomputing k1 parameters");
                                k1 = computeParameters(status, store);
                        } else {
-                               log.debug("Thrust estimate differs from correct value by " +
+                               log.verbose("Thrust estimate differs from correct value by " +
                                                (Math.rint(1000 * (thrustDiff + 0.000001) / thrustEstimate) / 10.0) + "%," +
                                                " estimate=" + thrustEstimate +
                                                " correct=" + store.thrustForce +
index 3738229a6769dc7abcf7652ee8c0342571e35465..cc77117ba9f8b791d0fd9835e33fd84a87fc6d14 100644 (file)
@@ -322,7 +322,7 @@ public class SimulationStatus implements Cloneable, Monitorable {
                        SimulationStatus clone = (SimulationStatus) super.clone();
                        return clone;
                } catch (CloneNotSupportedException e) {
-                       throw new BugException("BUG:  CloneNotSupportedException?!?", e);
+                       throw new BugException("CloneNotSupportedException?!?", e);
                }
        }
        
index 140862d0d48366ca9c2f1cc7dfb5746067ca7050..8d113add9251442d3db15d562a367ad470300c49 100644 (file)
@@ -8,15 +8,15 @@ package net.sf.openrocket.util;
 public class BugException extends FatalException {
        
        public BugException(String message) {
-               super(message);
+               super("BUG: " + message);
        }
        
        public BugException(Throwable cause) {
-               super(cause);
+               super("BUG: " + cause.getMessage(), cause);
        }
        
        public BugException(String message, Throwable cause) {
-               super(message, cause);
+               super("BUG: " + message, cause);
        }
        
 }
index 46b7893106985d9ebb07059844b596a21f802cc7..54047e35ec0c30f3949c8fff87dbdd8a0a801aab 100644 (file)
@@ -111,6 +111,8 @@ public class GUIUtil {
                setWindowIcons(dialog);
                addModelNullingListener(dialog);
                dialog.setLocationByPlatform(true);
+               dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+               dialog.pack();
                if (defaultButton != null) {
                        setDefaultButton(defaultButton);
                }
@@ -127,6 +129,7 @@ public class GUIUtil {
        public static void installEscapeCloseOperation(final JDialog dialog) {
                Action dispatchClosing = new AbstractAction() {
                        public void actionPerformed(ActionEvent event) {
+                               log.user("Closing dialog " + dialog);
                                dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
                        }
                };
index 52153e1488093b899c742e186c6691f738f7df26..6878163babf02446fbd524213393f5f5c5b38f28 100644 (file)
@@ -79,6 +79,10 @@ public class Icons {
         * @return              the ImageIcon, or null if could not be loaded (after the user closes the dialog)
         */
        public static ImageIcon loadImageIcon(String file, String name) {
+               if (System.getProperty("openrocket.unittest") != null) {
+                       return new ImageIcon();
+               }
+               
                URL url = ClassLoader.getSystemResource(file);
                if (url == null) {
                        ExceptionHandler.handleErrorCondition("Image file " + file + " not found, ignoring.");
diff --git a/src/net/sf/openrocket/util/NumericComparator.java b/src/net/sf/openrocket/util/NumericComparator.java
new file mode 100644 (file)
index 0000000..a337dee
--- /dev/null
@@ -0,0 +1,35 @@
+package net.sf.openrocket.util;
+
+import java.util.Comparator;
+
+public class NumericComparator implements Comparator<Object> {
+       
+       public static final NumericComparator INSTANCE = new NumericComparator();
+       
+       @Override
+       public int compare(Object o1, Object o2) {
+               double v1 = getValue(o1);
+               double v2 = getValue(o2);
+               
+               if (Double.isNaN(v1) || Double.isNaN(v2)) {
+                       String s1 = o1.toString();
+                       String s2 = o2.toString();
+                       return s1.compareTo(s2);
+               }
+               
+               return Double.compare(v1, v2);
+       }
+       
+       private double getValue(Object o) {
+               if (o instanceof Number) {
+                       return ((Number) o).doubleValue();
+               }
+               String s = o.toString();
+               try {
+                       return Double.parseDouble(s);
+               } catch (NumberFormatException e) {
+                       return Double.NaN;
+               }
+       }
+       
+}
index b48f68af8a26b51e5928f1bbc1650053563ffee0..b91f70473124568efc9f7205e54a9e96ef48f095 100644 (file)
@@ -69,7 +69,7 @@ public class Reflection {
        public static Error handleWrappedException(Exception e) {
                Throwable cause = e.getCause();
                if (cause == null) {
-                       throw new BugException("BUG: wrapped exception without cause", e);
+                       throw new BugException("wrapped exception without cause", e);
                }
                if (cause instanceof RuntimeException) {
                        throw (RuntimeException)cause;
index 4eacec33961e79507987dfda5514c9ffcc40c201..a57ea78a71125577eac7075514575acbc9be0455 100644 (file)
@@ -1,12 +1,12 @@
 package net.sf.openrocket.utils;
 
-import net.sf.openrocket.optimization.Function;
-import net.sf.openrocket.optimization.FunctionOptimizer;
-import net.sf.openrocket.optimization.MultidirectionalSearchOptimizer;
-import net.sf.openrocket.optimization.OptimizationController;
-import net.sf.openrocket.optimization.ParallelExecutorCache;
-import net.sf.openrocket.optimization.ParallelFunctionCache;
-import net.sf.openrocket.optimization.Point;
+import net.sf.openrocket.optimization.general.Function;
+import net.sf.openrocket.optimization.general.FunctionOptimizer;
+import net.sf.openrocket.optimization.general.OptimizationController;
+import net.sf.openrocket.optimization.general.ParallelExecutorCache;
+import net.sf.openrocket.optimization.general.ParallelFunctionCache;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer;
 
 
 
index 5a81d10f636953a44eb786d2477d60438f8eae69..b60bb3d83eaae9ccb3fe1e644d806054fb28e57b 100644 (file)
@@ -7,12 +7,12 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
-import net.sf.openrocket.optimization.Function;
-import net.sf.openrocket.optimization.FunctionOptimizer;
-import net.sf.openrocket.optimization.MultidirectionalSearchOptimizer;
-import net.sf.openrocket.optimization.OptimizationController;
-import net.sf.openrocket.optimization.ParallelExecutorCache;
-import net.sf.openrocket.optimization.Point;
+import net.sf.openrocket.optimization.general.Function;
+import net.sf.openrocket.optimization.general.FunctionOptimizer;
+import net.sf.openrocket.optimization.general.OptimizationController;
+import net.sf.openrocket.optimization.general.ParallelExecutorCache;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer;
 import net.sf.openrocket.util.MathUtil;
 
 
diff --git a/test/net/sf/openrocket/Estes_A8.rse b/test/net/sf/openrocket/Estes_A8.rse
new file mode 100644 (file)
index 0000000..1098ffe
--- /dev/null
@@ -0,0 +1,40 @@
+<engine-database>\r
+ <engine-list>\r
+<engine FDiv="10" FFix="1" FStep="-1." Isp="71.7" Itot="2.32" Type="single-use" auto-calc-cg="1" auto-calc-mass="1" avgThrust="3.178" burn-time="0.73" cgDiv="10" cgFix="1" cgStep="-1." code="A8" delays="3,5" dia="18." exitDia="0." initWt="16.35" len="70." mDiv="10" mFix="1" mStep="-1." massFrac="20.18" mfg="Estes" peakThrust="9.73" propWt="3.3" tDiv="10" tFix="1" tStep="-1." throatDia="0.">\r
+<comments>Estes A8 RASP.ENG file made from NAR published data\r
+File produced October 3, 2000\r
+The total impulse, peak thrust, average thrust and burn time are\r
+the same as the averaged static test data on the NAR web site in\r
+the certification file. The curve drawn with these data points is as\r
+close to the certification curve as can be with such a limited\r
+number of points (32) allowed with wRASP up to v1.6.\r
+</comments>\r
+<data>\r
+<eng-data cg="35." f="0." m="3.3" t="0."/>\r
+<eng-data cg="35." f="0.512" m="3.28507" t="0.041"/>\r
+<eng-data cg="35." f="2.115" m="3.20474" t="0.084"/>\r
+<eng-data cg="35." f="4.358" m="3.0068" t="0.127"/>\r
+<eng-data cg="35." f="6.794" m="2.6975" t="0.166"/>\r
+<eng-data cg="35." f="8.588" m="2.41309" t="0.192"/>\r
+<eng-data cg="35." f="9.294" m="2.23506" t="0.206"/>\r
+<eng-data cg="35." f="9.73" m="1.96448" t="0.226"/>\r
+<eng-data cg="35." f="8.845" m="1.83238" t="0.236"/>\r
+<eng-data cg="35." f="7.179" m="1.70703" t="0.247"/>\r
+<eng-data cg="35." f="5.063" m="1.58515" t="0.261"/>\r
+<eng-data cg="35." f="3.717" m="1.48525" t="0.277"/>\r
+<eng-data cg="35." f="3.205" m="1.3425" t="0.306"/>\r
+<eng-data cg="35." f="2.884" m="1.14764" t="0.351"/>\r
+<eng-data cg="35." f="2.499" m="0.94092" t="0.405"/>\r
+<eng-data cg="35." f="2.371" m="0.726196" t="0.467"/>\r
+<eng-data cg="35." f="2.307" m="0.509957" t="0.532"/>\r
+<eng-data cg="35." f="2.371" m="0.320333" t="0.589"/>\r
+<eng-data cg="35." f="2.371" m="0.175326" t="0.632"/>\r
+<eng-data cg="35." f="2.243" m="0.109701" t="0.652"/>\r
+<eng-data cg="35." f="1.794" m="0.0637665" t="0.668"/>\r
+<eng-data cg="35." f="1.153" m="0.0302344" t="0.684"/>\r
+<eng-data cg="35." f="0.448" m="0.00860204" t="0.703"/>\r
+<eng-data cg="35." f="0." m="0." t="0.73"/>\r
+</data>\r
+</engine>\r
+ </engine-list>\r
+</engine-database>\r
diff --git a/test/net/sf/openrocket/IntegrationTest.java b/test/net/sf/openrocket/IntegrationTest.java
new file mode 100644 (file)
index 0000000..fa60aaa
--- /dev/null
@@ -0,0 +1,315 @@
+package net.sf.openrocket;
+
+import static org.junit.Assert.*;
+
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.swing.Action;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.database.ThrustCurveMotorSetDatabase;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.file.GeneralRocketLoader;
+import net.sf.openrocket.file.RocketLoadException;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
+import net.sf.openrocket.masscalc.BasicMassCalculator;
+import net.sf.openrocket.masscalc.MassCalculator;
+import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.motor.ThrustCurveMotor;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.EngineBlock;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.FlightDataType;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.Coordinate;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * This class contains various integration tests that simulate user actions that
+ * might be performed.
+ */
+public class IntegrationTest {
+       
+       private OpenRocketDocument document;
+       private Action undoAction, redoAction;
+       
+       private AerodynamicCalculator aeroCalc = new BarrowmanCalculator();
+       private MassCalculator massCalc = new BasicMassCalculator();
+       private Configuration config;
+       private FlightConditions conditions;
+       
+       
+       @BeforeClass
+       public static void initialize() {
+               ThrustCurveMotorSetDatabase db = new ThrustCurveMotorSetDatabase(false) {
+                       @Override
+                       protected void loadMotors() {
+                               GeneralMotorLoader loader = new GeneralMotorLoader();
+                               InputStream is = this.getClass().getResourceAsStream("Estes_A8.rse");
+                               assertNotNull("Problem in unit test, cannot find Estes_A8.rse", is);
+                               try {
+                                       for (Motor m : loader.load(is, "Estes_A8.rse")) {
+                                               addMotor((ThrustCurveMotor) m);
+                                       }
+                                       is.close();
+                               } catch (IOException e) {
+                                       e.printStackTrace();
+                                       fail("IOException: " + e);
+                               }
+                       }
+               };
+               db.startLoading();
+               assertEquals(1, db.getMotorSets().size());
+               Application.setMotorSetDatabase(db);
+       }
+       
+       /**
+        * Tests loading a rocket design, modifying it, simulating it and the undo/redo
+        * mechanism in various combinations.
+        */
+       @Test
+       public void test1() throws RocketLoadException, IOException, SimulationException {
+               System.setProperty("openrocket.unittest", "true");
+               
+               // Load the rocket
+               GeneralRocketLoader loader = new GeneralRocketLoader();
+               InputStream is = this.getClass().getResourceAsStream("simplerocket.ork");
+               assertNotNull("Problem in unit test, cannot find simplerocket.ork", is);
+               document = loader.load(is);
+               is.close();
+               
+               undoAction = document.getUndoAction();
+               redoAction = document.getRedoAction();
+               config = document.getSimulation(0).getConfiguration();
+               conditions = new FlightConditions(config);
+               
+
+               // Test undo state
+               checkUndoState(null, null);
+               
+
+               // Compute cg+cp + altitude
+               checkCgCp(0.248, 0.0645, 0.320, 12.0);
+               checkAlt(48.2);
+               
+
+               // Mass modification
+               document.addUndoPosition("Modify mass");
+               checkUndoState(null, null);
+               massComponent().setComponentMass(0.01);
+               checkUndoState("Modify mass", null);
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.230, 0.0745, 0.320, 12.0);
+               checkAlt(37.2);
+               
+
+               // Non-change
+               document.addUndoPosition("No change");
+               checkUndoState("Modify mass", null);
+               
+
+               // Non-funcitonal change
+               document.addUndoPosition("Name change");
+               checkUndoState("Modify mass", null);
+               massComponent().setName("Foobar component");
+               checkUndoState("Name change", null);
+               
+
+               // Check cg+cp
+               checkCgCp(0.230, 0.0745, 0.320, 12.0);
+               
+
+               // Aerodynamic modification
+               document.addUndoPosition("Remove component");
+               checkUndoState("Name change", null);
+               document.getRocket().getChild(0).removeChild(0);
+               checkUndoState("Remove component", null);
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.163, 0.0613, 0.275, 9.95);
+               checkAlt(45.0);
+               
+
+               // Undo "Remove component" change
+               undoAction.actionPerformed(new ActionEvent(this, 0, "foo"));
+               assertTrue(document.getRocket().getChild(0).getChild(0) instanceof NoseCone);
+               checkUndoState("Name change", "Remove component");
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.230, 0.0745, 0.320, 12.0);
+               checkAlt(37.2);
+               
+
+               // Undo "Name change" change
+               undoAction.actionPerformed(new ActionEvent(this, 0, "foo"));
+               assertEquals("Extra mass", massComponent().getName());
+               checkUndoState("Modify mass", "Name change");
+               
+
+               // Check cg+cp
+               checkCgCp(0.230, 0.0745, 0.320, 12.0);
+               
+
+               // Undo "Modify mass" change
+               undoAction.actionPerformed(new ActionEvent(this, 0, "foo"));
+               assertEquals(0, massComponent().getComponentMass(), 0);
+               checkUndoState(null, "Modify mass");
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.248, 0.0645, 0.320, 12.0);
+               checkAlt(48.2);
+               
+
+               // Redo "Modify mass" change
+               redoAction.actionPerformed(new ActionEvent(this, 0, "foo"));
+               assertEquals(0.010, massComponent().getComponentMass(), 0.00001);
+               checkUndoState("Modify mass", "Name change");
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.230, 0.0745, 0.320, 12.0);
+               checkAlt(37.2);
+               
+
+               // Mass modification
+               document.addUndoPosition("Modify mass2");
+               checkUndoState("Modify mass", "Name change");
+               massComponent().setComponentMass(0.015);
+               checkUndoState("Modify mass2", null);
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.223, 0.0795, 0.320, 12.0);
+               checkAlt(32.7);
+               
+
+               // Perform component movement
+               document.startUndo("Move component");
+               document.getRocket().freeze();
+               RocketComponent bodytube = document.getRocket().getChild(0).getChild(1);
+               RocketComponent innertube = bodytube.getChild(2);
+               RocketComponent engineblock = innertube.getChild(0);
+               assertTrue(innertube.removeChild(engineblock));
+               bodytube.addChild(engineblock, 0);
+               checkUndoState("Modify mass2", null);
+               document.getRocket().thaw();
+               checkUndoState("Move component", null);
+               document.stopUndo();
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.221, 0.0797, 0.320, 12.0);
+               checkAlt(32.7);
+               
+
+               // Modify mass without setting undo description
+               massComponent().setComponentMass(0.020);
+               checkUndoState("Modify mass2", null);
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.215, 0.0847, 0.320, 12.0);
+               checkAlt(29.0);
+               
+
+               // Undo "Modify mass2" change
+               undoAction.actionPerformed(new ActionEvent(this, 0, "foo"));
+               assertEquals(0.015, massComponent().getComponentMass(), 0.0000001);
+               checkUndoState("Move component", "Modify mass2");
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.221, 0.0797, 0.320, 12.0);
+               checkAlt(32.7);
+               
+
+               // Undo "Move component" change
+               undoAction.actionPerformed(new ActionEvent(this, 0, "foo"));
+               assertTrue(document.getRocket().getChild(0).getChild(1).getChild(2).getChild(0) instanceof EngineBlock);
+               checkUndoState("Modify mass2", "Move component");
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.223, 0.0795, 0.320, 12.0);
+               checkAlt(32.7);
+               
+
+               // Redo "Move component" change
+               redoAction.actionPerformed(new ActionEvent(this, 0, "foo"));
+               assertTrue(document.getRocket().getChild(0).getChild(1).getChild(0) instanceof EngineBlock);
+               checkUndoState("Move component", "Modify mass2");
+               
+
+               // Check cg+cp + altitude
+               checkCgCp(0.221, 0.0797, 0.320, 12.0);
+               checkAlt(32.7);
+       }
+       
+       private String massComponentID = null;
+       
+       private MassComponent massComponent() {
+               if (massComponentID == null) {
+                       massComponentID = document.getRocket().getChild(0).getChild(1).getChild(0).getID();
+               }
+               return (MassComponent) document.getRocket().findComponent(massComponentID);
+       }
+       
+       
+       private void checkUndoState(String undoDesc, String redoDesc) {
+               if (undoDesc == null) {
+                       assertEquals("Undo", undoAction.getValue(Action.NAME));
+                       assertFalse(undoAction.isEnabled());
+               } else {
+                       assertEquals("Undo (" + undoDesc + ")", undoAction.getValue(Action.NAME));
+                       assertTrue(undoAction.isEnabled());
+               }
+               if (redoDesc == null) {
+                       assertEquals("Redo", redoAction.getValue(Action.NAME));
+                       assertFalse(redoAction.isEnabled());
+               } else {
+                       assertEquals("Redo (" + redoDesc + ")", redoAction.getValue(Action.NAME));
+                       assertTrue(redoAction.isEnabled());
+               }
+       }
+       
+       
+       private void checkCgCp(double cgx, double mass, double cpx, double cna) {
+               Coordinate cg, cp;
+               
+               cg = massCalc.getCG(config, MassCalcType.LAUNCH_MASS);
+               assertEquals(cgx, cg.x, 0.001);
+               assertEquals(mass, cg.weight, 0.0005);
+               
+               cp = aeroCalc.getWorstCP(config, conditions, null);
+               assertEquals(cpx, cp.x, 0.001);
+               assertEquals(cna, cp.weight, 0.1);
+       }
+       
+       
+       private void checkAlt(double expected) throws SimulationException {
+               Simulation simulation = document.getSimulation(0);
+               double actual;
+               
+               // Simulate + check altitude
+               simulation.simulate();
+               actual = simulation.getSimulatedData().getBranch(0).getMaximum(FlightDataType.TYPE_ALTITUDE);
+               assertEquals(expected, actual, 0.5);
+       }
+       
+}
index 7ba59656d8b4e195d47660ee9ee45358432eab54..fcb618b8baf0d3db9151828d9c99eb5031b0dc2a 100644 (file)
@@ -4,6 +4,9 @@ import static org.junit.Assert.*;
 
 import java.util.List;
 
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.optimization.general.multidim.SearchPattern;
+
 import org.junit.Test;
 
 public class TestSearchPattern {
index a9fb278a143dffe2ff15139b3a852797ff1468c3..d0a2374b18210af91ccf766b510337e3c3a623f0 100644 (file)
@@ -6,17 +6,19 @@ import java.lang.reflect.Method;
 import java.util.Iterator;
 import java.util.regex.Pattern;
 
+import net.sf.openrocket.util.BugException;
+
 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"
+                       "getClass", "getChildCount", "getChildren", "getNextComponent", "getID",
+                       "getPreviousComponent", "getParent", "getRocket", "getRoot", "getStage",
+                       "getStageNumber", "getComponentName",
+                       // Rocket specific methods:
+                       "getModID", "getMassModID", "getAerodynamicModID", "getTreeModID", "getFunctionalModID",
+                       "getMotorConfigurationIDs", "getDefaultConfiguration"
        };
        
        
@@ -36,7 +38,7 @@ public class ComponentCompare {
        }
        
        
-       
+
        public static void assertDeepEquality(RocketComponent c1, RocketComponent c2) {
                assertEquality(c1, c2);
                
@@ -51,7 +53,7 @@ public class ComponentCompare {
                assertFalse("iterator end", i2.hasNext());
        }
        
-
+       
 
        public static void assertDeepSimilarity(RocketComponent c1, RocketComponent c2,
                        boolean allowNameDifference) {
@@ -68,7 +70,7 @@ public class ComponentCompare {
                assertFalse("iterator end", i2.hasNext());
        }
        
-
+       
 
        /**
         * Check whether the two components are <em>similar</em>.  Two components are similar
@@ -89,31 +91,30 @@ public class ComponentCompare {
         * @param c2    the second component.
         * @param allowNameDifference   whether to allow the components to have different names.
         */
-       public static void assertSimilarity(RocketComponent c1, RocketComponent c2, 
+       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()) {
+               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) {
+                       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 {
@@ -122,7 +123,7 @@ public class ComponentCompare {
                                continue;
                        }
                        
-//                     System.out.println("Testing results of method " + name);
+                       //                      System.out.println("Testing results of method " + name);
                        
                        // Run the methods
                        Object result1, result2;
@@ -130,17 +131,17 @@ public class ComponentCompare {
                                result1 = m1.invoke(c1);
                                result2 = m2.invoke(c2);
                        } catch (Exception e) {
-                               throw new RuntimeException("Error executing method " + name, e);
+                               throw new BugException("Error executing method " + name, e);
                        }
                        
-                       if (result1 != null && result2 != null && 
+                       if (result1 != null && result2 != null &&
                                        result1.getClass().isArray() && result2.getClass().isArray()) {
                                assertArrayEquals("Comparing result of method " + name,
-                                               (Object[])result1, (Object[])result2);
+                                               (Object[]) result1, (Object[]) result2);
                        } else {
                                assertEquals("Comparing result of method " + name, result1, result2);
                        }
                }
        }
-
+       
 }
index ae0e665a6849b9ce24924a35d3470f4eb86a1711..2cf43903cb8561c9e9313e6569d3ae0b838e50f4 100644 (file)
@@ -16,7 +16,7 @@ import org.junit.Test;
 
 public class FinSetTest {
        
-       
+
        @Test
        public void testFreeformConvert() {
                testFreeformConvert(new TrapezoidFinSet());
@@ -50,14 +50,15 @@ public class FinSetTest {
                fin.setTabRelativePosition(TabRelativePosition.END);
                fin.setTabShift(0.015);
                fin.setThickness(0.005);
+               
 
-               converted = FreeformFinSet.convertFinSet(fin);
+               converted = FreeformFinSet.convertFinSet((FinSet) fin.copy());
                
                ComponentCompare.assertSimilarity(fin, converted, true);
                
                assertEquals(converted.getComponentName(), converted.getName());
                
-               
+
                // Create test rocket
                Rocket rocket = new Rocket();
                Stage stage = new Stage();
@@ -74,7 +75,7 @@ public class FinSetTest {
                assertTrue(l1.changed);
                assertEquals(ComponentChangeEvent.NONFUNCTIONAL_CHANGE, l1.changetype);
                
-               
+
                // Create copy
                RocketComponent rocketcopy = rocket.copy();
                
@@ -85,7 +86,7 @@ public class FinSetTest {
                FreeformFinSet.convertFinSet(fincopy);
                
                assertTrue(l2.changed);
-               assertEquals(ComponentChangeEvent.TREE_CHANGE, 
+               assertEquals(ComponentChangeEvent.TREE_CHANGE,
                                l2.changetype & ComponentChangeEvent.TREE_CHANGE);
                
        }
@@ -102,10 +103,10 @@ public class FinSetTest {
                
                @Override
                public void componentChanged(ComponentChangeEvent e) {
-                       assertFalse("Ensuring listener "+name+" has not been called.", changed);
+                       assertFalse("Ensuring listener " + name + " has not been called.", changed);
                        changed = true;
                        changetype = e.getType();
                }
        }
-
+       
 }
index 11c55dcad780a8129f382ff5bb5890fbfb5cad07..79a18fd1e7e864b14beaa94641b6d6b621738dad 100644 (file)
@@ -9,7 +9,7 @@ public class RocketTest {
                Rocket r1 = net.sf.openrocket.util.TestRockets.makeIsoHaisu();
                Rocket r2 = net.sf.openrocket.util.TestRockets.makeBigBlue();
                
-               Rocket copy = r2.copy();
+               Rocket copy = (Rocket) r2.copy();
                
                ComponentCompare.assertDeepEquality(r2, copy);
                
@@ -17,5 +17,5 @@ public class RocketTest {
                
                ComponentCompare.assertDeepEquality(r1, r2);
        }
-
+       
 }
diff --git a/test/net/sf/openrocket/simplerocket.ork b/test/net/sf/openrocket/simplerocket.ork
new file mode 100644 (file)
index 0000000..453cdec
Binary files /dev/null and b/test/net/sf/openrocket/simplerocket.ork differ
index 3ff71ef0a8c96863eab3fa9404240e30a2454ddd..fa109631e4db9f2de4bfb82d6c300b739b4794ad 100644 (file)
@@ -22,6 +22,7 @@ $oros = "";
 $orjava = "";
 $orcountry = "";
 $orcores = "";
+$orlocale = "";
 foreach (getallheaders() as $header => $value) {
     if (preg_match("/^[a-zA-Z0-9 !$%&()*+,.\\/:=?@_~-]{1,40}$/", $value)) {
        $h = strtolower($header);
@@ -37,6 +38,8 @@ foreach (getallheaders() as $header => $value) {
            $orcountry = $value;
        } else if ($h == 'x-openrocket-cpus') {
            $orcores = $value;
+       } else if ($h == 'x-openrocket-locale') {
+           $orlocale = $value;
        }
     }
 }
@@ -44,14 +47,15 @@ foreach (getallheaders() as $header => $value) {
 // Log the request
 if ((strlen($orversion) > 0 || strlen($orid) > 0 || strlen($oros) > 0
      || strlen($orjava) > 0 || strlen($orcountry) > 0 
-     || strlen($orcores) > 0) &&
+     || strlen($orcores) > 0 || strlen($orlocale) > 0) &&
     (strlen($orversion) < 20 && strlen($orid) < 50 && strlen($oros) < 50
      && strlen($orjava) < 50 && strlen($orcountry) < 50) 
-     && strlen($orcores) < 10) {
+     && strlen($orcores) < 10 && strlen($orlocale) < 20) {
 
     $file = $logfiles . gmdate("Y-m");
     $line = gmdate("Y-m-d H:i:s") . ";" . $orid . ";" . $orversion .
-       ";" . $oros . ";" . $orjava . ";" . $orcountry . ";" . $orcores . "\n";
+       ";" . $oros . ";" . $orjava . ";" . $orcountry . ";" . $orcores . 
+       ";" . $orlocale . "\n";
 
     $fp = fopen($file, 'a');
     if ($fp != FALSE) {
diff --git a/web/html/techdoc.pdf b/web/html/techdoc.pdf
new file mode 100644 (file)
index 0000000..2377e85
Binary files /dev/null and b/web/html/techdoc.pdf differ