DGP - merged printing support from branch
authorrodinia814 <rodinia814@180e2498-e6e9-4542-8430-84ac67f01cd8>
Fri, 12 Nov 2010 02:51:18 +0000 (02:51 +0000)
committerrodinia814 <rodinia814@180e2498-e6e9-4542-8430-84ac67f01cd8>
Fri, 12 Nov 2010 02:51:18 +0000 (02:51 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@95 180e2498-e6e9-4542-8430-84ac67f01cd8

152 files changed:
.classpath
ChangeLog
ReleaseNotes
build.properties
build.xml
doc/properties.txt
doc/techdoc/techdoc.pdf
doc/undo-redo-flow.uxf [new file with mode: 0644]
lib-test/hamcrest-core-1.3.0RC1.jar [new file with mode: 0644]
lib-test/hamcrest-library-1.3.0RC1.jar [new file with mode: 0644]
lib-test/jmock-2.6.0-RC2.jar [new file with mode: 0644]
lib-test/jmock-junit4-2.6.0-RC2.jar [new file with mode: 0644]
lib-test/junit-4.7.jar [deleted file]
lib-test/junit-dep-4.8.2.jar [new file with mode: 0644]
lib/iText-5.0.2.jar [deleted file]
releasing.txt
src/META-INF/services/net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationParameterService [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/LaunchLugHandler.java
src/net/sf/openrocket/file/rocksim/RocksimLoader.java
src/net/sf/openrocket/gui/adaptors/BooleanModel.java
src/net/sf/openrocket/gui/adaptors/Column.java
src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java
src/net/sf/openrocket/gui/adaptors/DoubleModel.java
src/net/sf/openrocket/gui/adaptors/IntegerModel.java
src/net/sf/openrocket/gui/components/URLLabel.java
src/net/sf/openrocket/gui/configdialog/FinSetConfig.java
src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/dialogs/DetailDialog.java
src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java
src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java
src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java [new file with mode: 0644]
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/SimulationPanel.java
src/net/sf/openrocket/gui/main/SimulationRunDialog.java
src/net/sf/openrocket/gui/main/SimulationWorker.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/print/DesignReport.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/LogLevelBufferLogger.java
src/net/sf/openrocket/logging/LogLine.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/OptimizationException.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/OptimizableParameter.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameterService.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/SimulationDomain.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/SimulationModifierService.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/optimization/rocketoptimization/modifiers/GenericModifier.java [new file with mode: 0644]
src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.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/startup/Startup.java
src/net/sf/openrocket/util/BugException.java
src/net/sf/openrocket/util/ConcurrencyException.java [new file with mode: 0644]
src/net/sf/openrocket/util/GUIUtil.java
src/net/sf/openrocket/util/Icons.java
src/net/sf/openrocket/util/MemoryManagement.java [new file with mode: 0644]
src/net/sf/openrocket/util/NumericComparator.java [new file with mode: 0644]
src/net/sf/openrocket/util/Reflection.java
src/net/sf/openrocket/util/SafetyMutex.java [new file with mode: 0644]
src/net/sf/openrocket/util/TextUtil.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/file/rocksim/RocksimLoaderTest.java
test/net/sf/openrocket/file/rocksim/rocksimTestRocket3.rkt
test/net/sf/openrocket/optimization/TestSearchPattern.java
test/net/sf/openrocket/optimization/rocketoptimization/TestRocketOptimizationFunction.java [new file with mode: 0644]
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]
test/net/sf/openrocket/util/TestMutex.java [new file with mode: 0644]
web/html/actions/updates.php
web/html/download.html
web/html/index.html
web/html/techdoc.pdf [new file with mode: 0644]
web/htp/htp.def
web/htp/news.htp

index d1b79c768119276a248cc797932dbb1631b49774..4a68d2c2c56e9eae26d6da2542cdcfb0598172fb 100644 (file)
@@ -10,7 +10,6 @@
                </accessrules>
        </classpathentry>
        <classpathentry kind="lib" path="lib-extra/RXTXcomm.jar"/>
-       <classpathentry kind="lib" path="lib-test/junit-4.7.jar"/>
        <classpathentry kind="lib" path="lib/jfreechart-1.0.13.jar" sourcepath="/home/sampo/Projects/lib/jfreechart-1.0.13/source"/>
        <classpathentry kind="lib" path="lib/jcommon-1.0.16.jar">
                <accessrules>
index fc0cab4f6f4d70b4c3032da2eb00ad74921c4b08..efbb97935316e44bb18b597160f7b61bc078867b 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,51 @@
+2010-10-25  Doug Pedrick
+
+       * [BUG] Take launch lug radial angle into account when loading rkt file
+
+2010-10-24  Sampo Niskane
+
+       * Added SafetyMutex and took into use in Simulation
+
+2010-10-18  Sampo Niskanen
+
+       * Ignore Sun JRE bug in D3D
+
+2010-10-09  Sampo Niskanen
+
+       * [BUG] Fixed conversion to freeform fin set
+       * Enhanced logging
+
+2010-10-08  Sampo Niskanen
+
+       * New components no longer look expandable in the component tree
+
+2010-10-06  Sampo Niskanen
+
+       * Released version 1.1.3
+
+2010-10-05  Sampo Niskanen
+
+       * Display comment as tooltip in component tree
+       * Limited allowed component attachments to those of the component
+         add buttons
+
+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
+
 2010-09-05  Sampo Niskanen
 
        * [BUG] Fixed bug that prevents adding stages to a rocket
index 67a353ae2761f07b36d09f5d1f9fd39b96cd7330..613117165ae332fbfe8638ee6c7d3ecc695f75de 100644 (file)
@@ -1,3 +1,16 @@
+OpenRocket 1.1.3  (2010-10-06):
+-------------------------------
+
+Support for drag-drop moving and copying of components.  Fixes a
+severe bug in the undo system.
+
+
+OpenRocket 1.1.2  (2010-09-07):
+-------------------------------
+
+Fixes a severe bug that prevented adding stages to rockets.
+
+
 OpenRocket 1.1.1  (2010-09-03):
 -------------------------------
 
index aeee4e68f2ae6e18bf1d9954f94dc2fe0fed88cb..fd13d5a763b6d91d2d4d7f9406e9e708c5512163 100644 (file)
@@ -1,7 +1,7 @@
 
 # The OpenRocket build version
 
-build.version=1.1.2pre
+build.version=1.1.4pre
 
 
 # The source of the package.  When building a package for a specific
index 6262bc0da7d1423f4f43e4e4c2924807febb814f..0540b3dc611aa451d74ee2562ad63465ee2834a2 100644 (file)
--- a/build.xml
+++ b/build.xml
                <pathelement location="${build-test.dir}"/>
                <pathelement location="${classes.dir}"/>
                <pathelement location="${src-test.dir}"/>
-<!--           <pathelement location="${ant.library.dir}/junit4.jar"/> -->
-               <pathelement location="lib-test/junit-4.7.jar"/>
+               <pathelement location="lib-test/junit-dep-4.8.2.jar"/>
+               <pathelement location="lib-test/hamcrest-core-1.3.0RC1.jar"/>
+               <pathelement location="lib-test/hamcrest-library-1.3.0RC1.jar"/>
+               <pathelement location="lib-test/jmock-2.6.0-RC2.jar"/>
+               <pathelement location="lib-test/jmock-junit4-2.6.0-RC2.jar"/>
        </path>
        
 
index ed0690416a760e28b6be8575e62a9a5d7ae05d09..cbddc55fbf6a8ab0ab5fe6a8b33cd72cd5389208 100644 (file)
@@ -25,6 +25,14 @@ openrocket.log.tracelevel
 Debugging options
 -----------------
 
+openrocket.debug
+       Turns on various options useful for debugging purposes.  The parameters defined are:
+               openrocket.log.stdout=VBOSE
+               openrocket.log.tracelevel=VBOSE
+               openrocket.debug.menu=true
+               openrocket.debug.motordigest=true
+
+
 openrocket.debug.menu
        If defined the "Debug" menu will be displayed in the main application window.
 
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
diff --git a/lib-test/hamcrest-core-1.3.0RC1.jar b/lib-test/hamcrest-core-1.3.0RC1.jar
new file mode 100644 (file)
index 0000000..1195cb7
Binary files /dev/null and b/lib-test/hamcrest-core-1.3.0RC1.jar differ
diff --git a/lib-test/hamcrest-library-1.3.0RC1.jar b/lib-test/hamcrest-library-1.3.0RC1.jar
new file mode 100644 (file)
index 0000000..8e6568b
Binary files /dev/null and b/lib-test/hamcrest-library-1.3.0RC1.jar differ
diff --git a/lib-test/jmock-2.6.0-RC2.jar b/lib-test/jmock-2.6.0-RC2.jar
new file mode 100644 (file)
index 0000000..a846450
Binary files /dev/null and b/lib-test/jmock-2.6.0-RC2.jar differ
diff --git a/lib-test/jmock-junit4-2.6.0-RC2.jar b/lib-test/jmock-junit4-2.6.0-RC2.jar
new file mode 100644 (file)
index 0000000..129e561
Binary files /dev/null and b/lib-test/jmock-junit4-2.6.0-RC2.jar differ
diff --git a/lib-test/junit-4.7.jar b/lib-test/junit-4.7.jar
deleted file mode 100644 (file)
index 700ad69..0000000
Binary files a/lib-test/junit-4.7.jar and /dev/null differ
diff --git a/lib-test/junit-dep-4.8.2.jar b/lib-test/junit-dep-4.8.2.jar
new file mode 100644 (file)
index 0000000..f28b4ef
Binary files /dev/null and b/lib-test/junit-dep-4.8.2.jar differ
diff --git a/lib/iText-5.0.2.jar b/lib/iText-5.0.2.jar
deleted file mode 100644 (file)
index ed95653..0000000
Binary files a/lib/iText-5.0.2.jar and /dev/null differ
index 68934ed2d97c2f6c06300b4f4310d3cabed35194..e48b765e43ffc987a0b6dd7d444b001c6447e0fa 100644 (file)
@@ -6,22 +6,22 @@ Steps for making a release:
 3.  Update ChangeLog
 4.  ant dist
 5.  Test new features (not in project directory)
-6.  Copy distribution files into dists/
-7.  Update Eclipse project and commit files to SVN
-8.  Tag the version in SVN, URL:
-    https://openrocket.svn.sourceforge.net/svnroot/openrocket/tags/Release_1.1.x
-9.  Update updates.php and test various versions using a different name:
+6.  Copy distribution files into OpenRocket-dists project
+7.  Update updates.php and test various versions using a different name:
     scp updates.php plaa,openrocket@web.sourceforge.net:htdocs/actions/testupdates.php
     java -Dopenrocket.debug.updateurl=http://openrocket.sourceforge.net/actions/testupdates.php -jar OpenRocket-1.1.0.jar
-10. Upload JAR and source distribution and ReleaseNotes to Sourceforge 
+8.  Update HTML: htp/htp.def (version number) htp/news.htp (release info)
+9.  Run "htp @" and check the web pages locally
+10. Update Eclipse project and commit files to SVN (OpenRocket and OpenRocket-dists)
+11. Tag the version in SVN, URL:
+    https://openrocket.svn.sourceforge.net/svnroot/openrocket/tags/Release_1.1.x
+12. Upload JAR and source distribution and ReleaseNotes to Sourceforge 
      - Project Admin -> File Manager
      - create new version directory under /openrocket
      - upload JAR, ZIP and ReleaseNotes
      - select ReleaseNotes properties, set as release notes
      - select JAR properties, set release note file and default downloads
      - select ZIP properties, set release note file
-11. Update HTML: htp/htp.def (version number) htp/news.htp (release info)
-12. Run "htp @" and check the web pages locally
 13. Update HTML to web server:
     scp * plaa,openrocket@web.sourceforge.net:htdocs/
 14. Test downloading from Sourceforge and web site
diff --git a/src/META-INF/services/net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationParameterService b/src/META-INF/services/net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationParameterService
new file mode 100644 (file)
index 0000000..e69de29
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..9d5d0cd35169c62b6ce1fcbeab492e0987943ead 100644 (file)
@@ -9,6 +9,7 @@ import javax.swing.event.ChangeListener;
 import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
 import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.masscalc.BasicMassCalculator;
 import net.sf.openrocket.masscalc.MassCalculator;
 import net.sf.openrocket.rocketcomponent.Configuration;
@@ -23,11 +24,21 @@ import net.sf.openrocket.simulation.SimulationStepper;
 import net.sf.openrocket.simulation.exception.SimulationException;
 import net.sf.openrocket.simulation.exception.SimulationListenerException;
 import net.sf.openrocket.simulation.listeners.SimulationListener;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.SafetyMutex;
 
-
+/**
+ * A class defining a simulation, its conditions and simulated data.
+ * <p>
+ * This class is not thread-safe and enforces single-threaded access with a
+ * SafetyMutex.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
 public class Simulation implements ChangeSource, Cloneable {
+       private static final LogHelper log = Application.getLogger();
        
        public static enum Status {
                /** Up-to-date */
@@ -46,6 +57,7 @@ public class Simulation implements ChangeSource, Cloneable {
                NOT_SIMULATED
        }
        
+       private final SafetyMutex mutex = new SafetyMutex();
        
        private final Rocket rocket;
        
@@ -137,8 +149,17 @@ public class Simulation implements ChangeSource, Cloneable {
        }
        
        
-
-
+       /**
+        * Return the rocket associated with this simulation.
+        * 
+        * @return      the rocket.
+        */
+       public Rocket getRocket() {
+               mutex.verify();
+               return rocket;
+       }
+       
+       
        /**
         * Return a newly created Configuration for this simulation.  The configuration
         * has the motor ID set and all stages active.
@@ -146,6 +167,7 @@ public class Simulation implements ChangeSource, Cloneable {
         * @return      a newly created Configuration of the launch conditions.
         */
        public Configuration getConfiguration() {
+               mutex.verify();
                Configuration c = new Configuration(rocket);
                c.setMotorConfigurationID(conditions.getMotorConfigurationID());
                c.setAllStages();
@@ -160,6 +182,7 @@ public class Simulation implements ChangeSource, Cloneable {
         * @return the simulation conditions.
         */
        public GUISimulationConditions getConditions() {
+               mutex.verify();
                return conditions;
        }
        
@@ -171,6 +194,7 @@ public class Simulation implements ChangeSource, Cloneable {
         * @return      the actual list of simulation listeners.
         */
        public List<String> getSimulationListeners() {
+               mutex.verify();
                return simulationListeners;
        }
        
@@ -181,6 +205,7 @@ public class Simulation implements ChangeSource, Cloneable {
         * @return      the name for the simulation.
         */
        public String getName() {
+               mutex.verify();
                return name;
        }
        
@@ -191,15 +216,20 @@ public class Simulation implements ChangeSource, Cloneable {
         * @param name  the name of the simulation.
         */
        public void setName(String name) {
-               if (this.name.equals(name))
-                       return;
-               
-               if (name == null)
-                       this.name = "";
-               else
-                       this.name = name;
-               
-               fireChangeEvent();
+               mutex.lock("setName");
+               try {
+                       if (this.name.equals(name))
+                               return;
+                       
+                       if (name == null)
+                               this.name = "";
+                       else
+                               this.name = name;
+                       
+                       fireChangeEvent();
+               } finally {
+                       mutex.unlock("setName");
+               }
        }
        
        
@@ -211,6 +241,8 @@ public class Simulation implements ChangeSource, Cloneable {
         * @see Status
         */
        public Status getStatus() {
+               mutex.verify();
+               
                if (status == Status.UPTODATE || status == Status.LOADED) {
                        if (rocket.getFunctionalModID() != simulatedRocketID ||
                                        !conditions.equals(simulatedConditions))
@@ -225,61 +257,59 @@ public class Simulation implements ChangeSource, Cloneable {
 
        public void simulate(SimulationListener... additionalListeners)
                                                throws SimulationException {
-               
-               if (this.status == Status.EXTERNAL) {
-                       throw new SimulationException("Cannot simulate imported simulation.");
-               }
-               
-               AerodynamicCalculator aerodynamicCalculator;
-               SimulationEngine simulator;
-               SimulationStepper stepper;
-               MassCalculator massCalculator;
-               
+               mutex.lock("simulate");
                try {
-                       aerodynamicCalculator = aerodynamicCalculatorClass.newInstance();
-                       simulator = simulationEngineClass.newInstance();
-                       stepper = simulationStepperClass.newInstance();
-                       massCalculator = massCalculatorClass.newInstance();
-               } catch (InstantiationException e) {
-                       throw new IllegalStateException("Cannot instantiate calculator/simulator.", e);
-               } catch (IllegalAccessException e) {
-                       throw new IllegalStateException("Cannot access calc/sim instance?! BUG!", e);
-               } catch (NullPointerException e) {
-                       throw new IllegalStateException("Calculator or simulator null", e);
-               }
-               
-               SimulationConditions simulationConditions = conditions.toSimulationConditions();
-               for (SimulationListener l : additionalListeners) {
-                       simulationConditions.getSimulationListenerList().add(l);
-               }
-               
-               for (String className : simulationListeners) {
-                       SimulationListener l = null;
+                       
+                       if (this.status == Status.EXTERNAL) {
+                               throw new SimulationException("Cannot simulate imported simulation.");
+                       }
+                       
+                       SimulationEngine simulator;
+                       
                        try {
-                               Class<?> c = Class.forName(className);
-                               l = (SimulationListener) c.newInstance();
-                       } catch (Exception e) {
-                               throw new SimulationListenerException("Could not instantiate listener of " +
-                                               "class: " + className, e);
+                               simulator = simulationEngineClass.newInstance();
+                       } catch (InstantiationException e) {
+                               throw new IllegalStateException("Cannot instantiate simulator.", e);
+                       } catch (IllegalAccessException e) {
+                               throw new IllegalStateException("Cannot access simulator instance?! BUG!", e);
+                       } catch (NullPointerException e) {
+                               throw new IllegalStateException("Simulator null", e);
+                       }
+                       
+                       SimulationConditions simulationConditions = conditions.toSimulationConditions();
+                       for (SimulationListener l : additionalListeners) {
+                               simulationConditions.getSimulationListenerList().add(l);
+                       }
+                       
+                       for (String className : simulationListeners) {
+                               SimulationListener l = null;
+                               try {
+                                       Class<?> c = Class.forName(className);
+                                       l = (SimulationListener) c.newInstance();
+                               } catch (Exception e) {
+                                       throw new SimulationListenerException("Could not instantiate listener of " +
+                                                       "class: " + className, e);
+                               }
+                               simulationConditions.getSimulationListenerList().add(l);
                        }
-                       simulationConditions.getSimulationListenerList().add(l);
+                       
+                       long t1, t2;
+                       log.debug("Simulation: calling simulator");
+                       t1 = System.currentTimeMillis();
+                       simulatedData = simulator.simulate(simulationConditions);
+                       t2 = System.currentTimeMillis();
+                       log.debug("Simulation: returning from simulator, simulation took " + (t2 - t1) + "ms");
+                       
+                       // Set simulated info after simulation, will not be set in case of exception
+                       simulatedConditions = conditions.clone();
+                       simulatedMotors = getConfiguration().getMotorConfigurationDescription();
+                       simulatedRocketID = rocket.getFunctionalModID();
+                       
+                       status = Status.UPTODATE;
+                       fireChangeEvent();
+               } finally {
+                       mutex.unlock("simulate");
                }
-               
-               long t1, t2;
-               System.out.println("Simulation: calling simulator");
-               t1 = System.currentTimeMillis();
-               simulatedData = simulator.simulate(simulationConditions);
-               t2 = System.currentTimeMillis();
-               System.out.println("Simulation: returning from simulator, " +
-                               "simulation took " + (t2 - t1) + "ms");
-               
-               // Set simulated info after simulation, will not be set in case of exception
-               simulatedConditions = conditions.clone();
-               simulatedMotors = getConfiguration().getMotorConfigurationDescription();
-               simulatedRocketID = rocket.getFunctionalModID();
-               
-               status = Status.UPTODATE;
-               fireChangeEvent();
        }
        
        
@@ -290,6 +320,7 @@ public class Simulation implements ChangeSource, Cloneable {
         * @return      the conditions used in the previous simulation, or <code>null</code>.
         */
        public GUISimulationConditions getSimulatedConditions() {
+               mutex.verify();
                return simulatedConditions;
        }
        
@@ -302,6 +333,7 @@ public class Simulation implements ChangeSource, Cloneable {
         * @see         FlightData#getWarningSet()
         */
        public WarningSet getSimulatedWarnings() {
+               mutex.verify();
                if (simulatedData == null)
                        return null;
                return simulatedData.getWarningSet();
@@ -317,6 +349,7 @@ public class Simulation implements ChangeSource, Cloneable {
         * @see         Rocket#getMotorConfigurationNameOrDescription(String)
         */
        public String getSimulatedMotorDescription() {
+               mutex.verify();
                return simulatedMotors;
        }
        
@@ -327,6 +360,7 @@ public class Simulation implements ChangeSource, Cloneable {
         * @return      the flight data of the previous simulation, or <code>null</code>.
         */
        public FlightData getSimulatedData() {
+               mutex.verify();
                return simulatedData;
        }
        
@@ -340,6 +374,7 @@ public class Simulation implements ChangeSource, Cloneable {
         */
        @SuppressWarnings("unchecked")
        public Simulation copy() {
+               mutex.lock("copy");
                try {
                        
                        Simulation copy = (Simulation) super.clone();
@@ -355,9 +390,10 @@ public class Simulation implements ChangeSource, Cloneable {
                        
                        return copy;
                        
-
                } catch (CloneNotSupportedException e) {
                        throw new BugException("Clone not supported, BUG", e);
+               } finally {
+                       mutex.unlock("copy");
                }
        }
        
@@ -371,26 +407,33 @@ public class Simulation implements ChangeSource, Cloneable {
         */
        @SuppressWarnings("unchecked")
        public Simulation duplicateSimulation(Rocket newRocket) {
-               Simulation copy = new Simulation(newRocket);
-               
-               copy.name = this.name;
-               copy.conditions.copyFrom(this.conditions);
-               copy.simulationListeners = (ArrayList<String>) this.simulationListeners.clone();
-               copy.simulationStepperClass = this.simulationStepperClass;
-               copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass;
-               
-               return copy;
+               mutex.lock("duplicateSimulation");
+               try {
+                       Simulation copy = new Simulation(newRocket);
+                       
+                       copy.name = this.name;
+                       copy.conditions.copyFrom(this.conditions);
+                       copy.simulationListeners = (ArrayList<String>) this.simulationListeners.clone();
+                       copy.simulationStepperClass = this.simulationStepperClass;
+                       copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass;
+                       
+                       return copy;
+               } finally {
+                       mutex.unlock("duplicateSimulation");
+               }
        }
        
        
 
        @Override
        public void addChangeListener(ChangeListener listener) {
+               mutex.verify();
                listeners.add(listener);
        }
        
        @Override
        public void removeChangeListener(ChangeListener listener) {
+               mutex.verify();
                listeners.remove(listener);
        }
        
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 6d981ae4a542e75ead7efe9ece19d2331d6b410e..60c5b494cf2b347aecef25d7194bef1d5836f211 100644 (file)
@@ -64,6 +64,9 @@ class LaunchLugHandler extends PositionDependentHandler<LaunchLug> {
             if ("Material".equals(element)) {
                 setMaterialName(content);
             }
+            if ("RadialAngle".equals(element)) {
+                lug.setRadialDirection(Double.parseDouble(content));
+            }
             if ("FinishCode".equals(element)) {
                 lug.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket());
             }
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 e474a9ee86dcfb9ea7ba8c107fa105016ee622fd..2aa651c642d670233499a2c8bded21997683d043 100644 (file)
@@ -11,6 +11,8 @@ import javax.swing.AbstractAction;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.ChangeSource;
 import net.sf.openrocket.util.Reflection;
@@ -33,7 +35,8 @@ import net.sf.openrocket.util.Reflection;
  */
 
 public class BooleanModel extends AbstractAction implements ChangeListener {
-
+       private static final LogHelper log = Application.getLogger();
+       
        private final ChangeSource source;
        private final String valueName;
        
@@ -44,6 +47,8 @@ public class BooleanModel extends AbstractAction implements ChangeListener {
        private final List<Component> components = new ArrayList<Component>();
        private final List<Boolean> componentEnableState = new ArrayList<Boolean>();
        
+       private String toString = null;
+       
        private int firing = 0;
        
        private boolean oldValue;
@@ -53,34 +58,38 @@ public class BooleanModel extends AbstractAction implements ChangeListener {
                this.source = source;
                this.valueName = valueName;
                
-               Method getter=null, setter=null;
-               
+               Method getter = null, setter = null;
                
+
                // Try get/is and set
                try {
                        getter = source.getClass().getMethod("is" + valueName);
-               } catch (NoSuchMethodException ignore) { }
+               } catch (NoSuchMethodException ignore) {
+               }
                if (getter == null) {
                        try {
                                getter = source.getClass().getMethod("get" + valueName);
-                       } catch (NoSuchMethodException ignore) { }
+                       } catch (NoSuchMethodException ignore) {
+                       }
                }
                try {
-                       setter = source.getClass().getMethod("set" + valueName,boolean.class);
-               } catch (NoSuchMethodException ignore) { }
+                       setter = source.getClass().getMethod("set" + valueName, boolean.class);
+               } catch (NoSuchMethodException ignore) {
+               }
                
-               if (getter==null || setter==null) {
-                       throw new IllegalArgumentException("get/is methods for boolean '"+valueName+
-                                       "' not present in class "+source.getClass().getCanonicalName());
+               if (getter == null || setter == null) {
+                       throw new IllegalArgumentException("get/is methods for boolean '" + valueName +
+                                       "' not present in class " + source.getClass().getCanonicalName());
                }
-
+               
                getMethod = getter;
                setMethod = setter;
                
                Method e = null;
                try {
                        e = source.getClass().getMethod("is" + valueName + "Enabled");
-               } catch (NoSuchMethodException ignore) { }
+               } catch (NoSuchMethodException ignore) {
+               }
                getEnabled = e;
                
                oldValue = getValue();
@@ -94,19 +103,20 @@ public class BooleanModel extends AbstractAction implements ChangeListener {
        
        public boolean getValue() {
                try {
-                       return (Boolean)getMethod.invoke(source);
+                       return (Boolean) getMethod.invoke(source);
                } catch (IllegalAccessException e) {
-                       throw new BugException("getMethod execution error for source "+source,e);
+                       throw new BugException("getMethod execution error for source " + source, e);
                } catch (InvocationTargetException e) {
                        throw Reflection.handleWrappedException(e);
                }
        }
        
        public void setValue(boolean b) {
+               log.debug("Setting value of " + this + " to " + b);
                try {
-                       setMethod.invoke(source, new Object[] { (Boolean)b });
+                       setMethod.invoke(source, new Object[] { (Boolean) b });
                } catch (IllegalAccessException e) {
-                       throw new BugException("setMethod execution error for source "+source,e);
+                       throw new BugException("setMethod execution error for source " + source, e);
                } catch (InvocationTargetException e) {
                        throw Reflection.handleWrappedException(e);
                }
@@ -141,7 +151,7 @@ public class BooleanModel extends AbstractAction implements ChangeListener {
        private void updateEnableStatus() {
                boolean state = getValue();
                
-               for (int i=0; i < components.size(); i++) {
+               for (int i = 0; i < components.size(); i++) {
                        Component c = components.get(i);
                        boolean b = componentEnableState.get(i);
                        c.setEnabled(state == b);
@@ -149,81 +159,54 @@ public class BooleanModel extends AbstractAction implements ChangeListener {
        }
        
        
-//     @Override
-//     public boolean isEnabled() {
-//             if (getEnabled == null)
-//                     return true;
-//             try {
-//                     return (Boolean)getEnabled.invoke(source);
-//             } catch (IllegalAccessException e) {
-//                     throw new RuntimeException("getEnabled execution error for source "+source,e);
-//             } catch (InvocationTargetException e) {
-//                     throw new RuntimeException("getEnabled execution error for source "+source,e);
-//             }
-//     }
-
 
        private boolean getIsEnabled() {
                if (getEnabled == null)
                        return true;
                try {
-                       return (Boolean)getEnabled.invoke(source);
+                       return (Boolean) getEnabled.invoke(source);
                } catch (IllegalAccessException e) {
-                       throw new BugException("getEnabled execution error for source "+source,e);
+                       throw new BugException("getEnabled execution error for source " + source, e);
                } catch (InvocationTargetException e) {
                        throw Reflection.handleWrappedException(e);
                }
        }
        
-//     @Override
-//     public Object getValue(String key) {
-//             if (key.equals(SELECTED_KEY)) {
-//                     return getValue();
-//             }
-//             return super.getValue(key);
-//     }
-//
-//     @Override
-//     public void putValue(String key, Object value) {
-//             if (firing > 0)  // Ignore if currently firing event
-//                     return;
-//             if (key.equals(SELECTED_KEY) && (value instanceof Boolean)) {
-//                     setValue((Boolean)value);
-//             } else {
-//                     super.putValue(key, value);
-//             }
-//             updateEnableStatus();
-//     }
-       
-       
        @Override
        public void stateChanged(ChangeEvent event) {
-               if (firing > 0)
+               if (firing > 0) {
+                       log.debug("Ignoring stateChanged of " + this + ", currently firing events");
                        return;
+               }
                
                boolean v = getValue();
                boolean e = getIsEnabled();
                if (oldValue != v) {
+                       log.debug("Value of " + this + " has changed to " + v + " oldValue=" + oldValue);
                        oldValue = v;
                        firing++;
                        this.putValue(SELECTED_KEY, getValue());
-//                     this.firePropertyChange(SELECTED_KEY, !v, v);
+                       //                      this.firePropertyChange(SELECTED_KEY, !v, v);
                        updateEnableStatus();
                        firing--;
                }
                if (oldEnabled != e) {
+                       log.debug("Enabled status of " + this + " has changed to " + e + " oldEnabled=" + oldEnabled);
                        oldEnabled = e;
                        setEnabled(e);
                }
        }
-
-
+       
+       
        @Override
        public void actionPerformed(ActionEvent e) {
-               if (firing > 0)
+               if (firing > 0) {
+                       log.debug("Ignoring actionPerformed of " + this + ", currently firing events");
                        return;
+               }
                
-               boolean v = (Boolean)this.getValue(SELECTED_KEY);
+               boolean v = (Boolean) this.getValue(SELECTED_KEY);
+               log.user("Value of " + this + " changed to " + v + " oldValue=" + oldValue);
                if (v != oldValue) {
                        firing++;
                        setValue(v);
@@ -238,6 +221,9 @@ public class BooleanModel extends AbstractAction implements ChangeListener {
        
        @Override
        public String toString() {
-               return "BooleanModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+               if (toString == null) {
+                       toString = "BooleanModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";
+               }
+               return toString;
        }
 }
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..a81381dca284088a41cd9ec4b6ff32feb0231474 100644 (file)
@@ -15,6 +15,8 @@ import javax.swing.SpinnerNumberModel;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.unit.Unit;
 import net.sf.openrocket.unit.UnitGroup;
 import net.sf.openrocket.util.BugException;
@@ -38,10 +40,11 @@ import net.sf.openrocket.util.Reflection;
  */
 
 public class DoubleModel implements ChangeListener, ChangeSource {
-       private static final boolean DEBUG_LISTENERS = false;
+       private static final LogHelper log = Application.getLogger();
        
-       public static final DoubleModel ZERO = new DoubleModel(0);
 
+       public static final DoubleModel ZERO = new DoubleModel(0);
+       
        //////////// JSpinner Model ////////////
        
        /**
@@ -53,56 +56,50 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                @Override
                public Object getValue() {
                        return currentUnit.toUnit(DoubleModel.this.getValue());
-//                     return makeString(currentUnit.toUnit(DoubleModel.this.getValue()));
                }
-
+               
                @Override
                public void setValue(Object value) {
-                       
-                       System.out.println("setValue("+value+") called, valueName="+valueName+
-                                       " firing="+firing);
-                       
-                       if (firing > 0)   // Ignore, if called when model is sending events
+                       if (firing > 0) {
+                               // Ignore, if called when model is sending events
+                               log.verbose("Ignoring call to SpinnerModel setValue for " + DoubleModel.this.toString() +
+                                               " value=" + value + ", currently firing events");
                                return;
-                       Number num = (Number)value;
+                       }
+                       Number num = (Number) value;
                        double newValue = num.doubleValue();
-                       DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
+                       double converted = currentUnit.fromUnit(newValue);
                        
+                       log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
+                                       " converted=" + converted);
+                       DoubleModel.this.setValue(converted);
                        
-//                     try {
-//                             double newValue = Double.parseDouble((String)value);
-//                             DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
-//                     } catch (NumberFormatException e) { 
-//                             DoubleModel.this.fireStateChanged();
-//                     };
                }
                
                @Override
                public Object getNextValue() {
                        double d = currentUnit.toUnit(DoubleModel.this.getValue());
                        double max = currentUnit.toUnit(maxValue);
-                       if (MathUtil.equals(d,max))
+                       if (MathUtil.equals(d, max))
                                return null;
                        d = currentUnit.getNextValue(d);
                        if (d > max)
                                d = max;
                        return d;
-//                     return makeString(d);
                }
-
+               
                @Override
                public Object getPreviousValue() {
                        double d = currentUnit.toUnit(DoubleModel.this.getValue());
                        double min = currentUnit.toUnit(minValue);
-                       if (MathUtil.equals(d,min))
+                       if (MathUtil.equals(d, min))
                                return null;
                        d = currentUnit.getPreviousValue(d);
                        if (d < min)
                                d = min;
                        return d;
-//                     return makeString(d);
                }
-
+               
                
                @Override
                public Comparable<Double> getMinimum() {
@@ -119,7 +116,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                public void addChangeListener(ChangeListener l) {
                        DoubleModel.this.addChangeListener(l);
                }
-
+               
                @Override
                public void removeChangeListener(ChangeListener l) {
                        DoubleModel.this.removeChangeListener(l);
@@ -137,9 +134,9 @@ public class DoubleModel implements ChangeListener, ChangeSource {
        }
        
        
-       
-       
-       
+
+
+
        ////////////  JSlider model  ////////////
        
        private class ValueSliderModel implements BoundedRangeModel, ChangeListener {
@@ -149,7 +146,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                 * Use linear scale  value = linear1 * x + linear0  when x < linearPosition
                 * Use quadratic scale  value = quad2 * x^2 + quad1 * x + quad0  otherwise
                 */
-               
+
                // Linear in range x <= linearPosition
                private final double linearPosition;
                
@@ -161,40 +158,40 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                //private final double linear0;
                
                // Non-linear multiplier, exponent and constant
-               private final double quad2,quad1,quad0;
-               
+               private final double quad2, quad1, quad0;
                
                
+
                public ValueSliderModel(DoubleModel min, DoubleModel max) {
                        linearPosition = 1.0;
-
+                       
                        this.min = min;
-                       this.mid = max;  // Never use exponential scale
+                       this.mid = max; // Never use exponential scale
                        this.max = max;
                        
                        min.addChangeListener(this);
                        max.addChangeListener(this);
-
-                       quad2 = quad1 = quad0 = 0;  // Not used
+                       
+                       quad2 = quad1 = quad0 = 0; // Not used
                }
                
                
-               
+
                /**
                 * Generate a linear model from min to max.
                 */
                public ValueSliderModel(double min, double max) {
                        linearPosition = 1.0;
-
+                       
                        this.min = new DoubleModel(min);
-                       this.mid = new DoubleModel(max);  // Never use exponential scale
+                       this.mid = new DoubleModel(max); // Never use exponential scale
                        this.max = new DoubleModel(max);
-
-                       quad2 = quad1 = quad0 = 0;  // Not used
+                       
+                       quad2 = quad1 = quad0 = 0; // Not used
                }
                
                public ValueSliderModel(double min, double mid, double max) {
-                       this(min,0.5,mid,max);
+                       this(min, 0.5, mid, max);
                }
                
                /*
@@ -208,15 +205,15 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                        this.min = new DoubleModel(min);
                        this.mid = new DoubleModel(mid);
                        this.max = new DoubleModel(max);
-
                        
+
                        linearPosition = pos;
                        //linear0 = min;
                        //linear1 = (mid-min)/pos;
                        
                        if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
-                               throw new IllegalArgumentException("Bad arguments for ValueSliderModel "+
-                                               "min="+min+" mid="+mid+" max="+max+" pos="+pos);
+                               throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
+                                               "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
                        }
                        
                        /*
@@ -225,16 +222,16 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                         *   f(1)    = max      - end point
                         *   f'(pos) = linear1  - continuity of derivative
                         */
-                       
-                       double delta = (mid-min)/pos;
-                       quad2 = (max - mid - delta + delta*pos) / pow2(pos-1);
-                       quad1 = (delta + 2*(mid-max)*pos - delta*pos*pos) / pow2(pos-1);
-                       quad0 = (mid - (2*mid+delta)*pos + (max+delta)*pos*pos) / pow2(pos-1);
+
+                       double delta = (mid - min) / pos;
+                       quad2 = (max - mid - delta + delta * pos) / pow2(pos - 1);
+                       quad1 = (delta + 2 * (mid - max) * pos - delta * pos * pos) / pow2(pos - 1);
+                       quad0 = (mid - (2 * mid + delta) * pos + (max + delta) * pos * pos) / pow2(pos - 1);
                        
                }
                
                private double pow2(double x) {
-                       return x*x;
+                       return x * x;
                }
                
                public int getValue() {
@@ -250,68 +247,94 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                                //linear0 = min;
                                //linear1 = (mid-min)/pos;
                                
-                               x = (value - min.getValue())*linearPosition/(mid.getValue()-min.getValue());
+                               x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
                        } else {
                                // Use quadratic scale
                                // Further solution of the quadratic equation
                                //   a*x^2 + b*x + c-value == 0
-                               x = (Math.sqrt(quad1*quad1 - 4*quad2*(quad0-value)) - quad1) / (2*quad2);
+                               x = (Math.sqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2);
                        }
-                       return (int)(x*MAX);
+                       return (int) (x * MAX);
                }
-
-
+               
+               
                public void setValue(int newValue) {
-                       if (firing > 0)   // Ignore loops
+                       if (firing > 0) {
+                               // Ignore loops
+                               log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() +
+                                               " value=" + newValue + ", currently firing events");
                                return;
+                       }
                        
-                       double x = (double)newValue/MAX;
-                       double value;
+                       double x = (double) newValue / MAX;
+                       double scaledValue;
                        
                        if (x <= linearPosition) {
                                // Use linear scale
                                //linear0 = min;
                                //linear1 = (mid-min)/pos;
-
-                               value = (mid.getValue()-min.getValue())/linearPosition*x + min.getValue();
+                               
+                               scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
                        } else {
                                // Use quadratic scale
-                               value = quad2*x*x + quad1*x + quad0;
+                               scaledValue = quad2 * x * x + quad1 * x + quad0;
                        }
                        
-                       DoubleModel.this.setValue(currentUnit.fromUnit(
-                                       currentUnit.round(currentUnit.toUnit(value))));
+                       double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
+                       log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
+                                       " scaledValue=" + scaledValue + " converted=" + converted);
+                       DoubleModel.this.setValue(converted);
                }
-
+               
                
                // Static get-methods
                private boolean isAdjusting;
-               public int getExtent() { return 0; }
-               public int getMaximum() { return MAX; }
-               public int getMinimum() { return 0; }
-               public boolean getValueIsAdjusting() { return isAdjusting; }
+               
+               public int getExtent() {
+                       return 0;
+               }
+               
+               public int getMaximum() {
+                       return MAX;
+               }
+               
+               public int getMinimum() {
+                       return 0;
+               }
+               
+               public boolean getValueIsAdjusting() {
+                       return isAdjusting;
+               }
                
                // Ignore set-values
-               public void setExtent(int newExtent) { }
-               public void setMaximum(int newMaximum) { }
-               public void setMinimum(int newMinimum) { }
-               public void setValueIsAdjusting(boolean b) { isAdjusting = b; }
-
+               public void setExtent(int newExtent) {
+               }
+               
+               public void setMaximum(int newMaximum) {
+               }
+               
+               public void setMinimum(int newMinimum) {
+               }
+               
+               public void setValueIsAdjusting(boolean b) {
+                       isAdjusting = b;
+               }
+               
                public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
                        setValueIsAdjusting(adjusting);
                        setValue(value);
                }
-
+               
                // Pass change listeners to the underlying model
                public void addChangeListener(ChangeListener l) {
                        DoubleModel.this.addChangeListener(l);
                }
-
+               
                public void removeChangeListener(ChangeListener l) {
                        DoubleModel.this.removeChangeListener(l);
                }
-
-
+               
+               
 
                public void stateChanged(ChangeEvent e) {
                        // Min or max range has changed.
@@ -323,24 +346,24 @@ public class DoubleModel implements ChangeListener, ChangeSource {
        
        
        public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
-               return new ValueSliderModel(min,max);
+               return new ValueSliderModel(min, max);
        }
        
        public BoundedRangeModel getSliderModel(double min, double max) {
-               return new ValueSliderModel(min,max);
+               return new ValueSliderModel(min, max);
        }
        
        public BoundedRangeModel getSliderModel(double min, double mid, double max) {
-               return new ValueSliderModel(min,mid,max);
+               return new ValueSliderModel(min, mid, max);
        }
        
        public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
-               return new ValueSliderModel(min,pos,mid,max);
+               return new ValueSliderModel(min, pos, mid, max);
        }
        
        
-       
-       
+
+
 
        ////////////  Action model  ////////////
        
@@ -352,10 +375,9 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                        addChangeListener(this);
                }
                
-
+               
                @Override
                public boolean isEnabled() {
-                       // TODO: LOW: does not reflect if component is currently able to support automatic setting
                        return isAutomaticAvailable();
                }
                
@@ -367,51 +389,61 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                        }
                        return super.getValue(key);
                }
-
+               
                @Override
                public void putValue(String key, Object value) {
-                       if (firing > 0)
+                       if (firing > 0) {
+                               log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() +
+                                               " key=" + key + " value=" + value + ", currently firing events");
                                return;
+                       }
                        if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
-                               oldValue = (Boolean)value;
-                               setAutomatic((Boolean)value);
+                               log.user("ActionModel putValue called for " + DoubleModel.this.toString() +
+                                               " key=" + key + " value=" + value);
+                               oldValue = (Boolean) value;
+                               setAutomatic((Boolean) value);
                        } else {
+                               log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() +
+                                               " key=" + key + " value=" + value);
                                super.putValue(key, value);
                        }
                }
-
+               
                // Implement a wrapper to the ChangeListeners
-               ArrayList<PropertyChangeListener> propertyChangeListeners = 
-                       new ArrayList<PropertyChangeListener>();
+               ArrayList<PropertyChangeListener> propertyChangeListeners =
+                               new ArrayList<PropertyChangeListener>();
+               
                @Override
                public void addPropertyChangeListener(PropertyChangeListener listener) {
                        propertyChangeListeners.add(listener);
                        DoubleModel.this.addChangeListener(this);
                }
+               
                @Override
                public void removePropertyChangeListener(PropertyChangeListener listener) {
                        propertyChangeListeners.remove(listener);
                        if (propertyChangeListeners.isEmpty())
                                DoubleModel.this.removeChangeListener(this);
                }
+               
                // If the value has changed, generate an event to the listeners
                public void stateChanged(ChangeEvent e) {
                        boolean newValue = isAutomatic();
                        if (oldValue == newValue)
                                return;
-                       PropertyChangeEvent event = new PropertyChangeEvent(this,Action.SELECTED_KEY,
-                                       oldValue,newValue);
+                       PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY,
+                                       oldValue, newValue);
                        oldValue = newValue;
                        Object[] l = propertyChangeListeners.toArray();
-                       for (int i=0; i<l.length; i++) {
-                               ((PropertyChangeListener)l[i]).propertyChange(event);
+                       for (int i = 0; i < l.length; i++) {
+                               ((PropertyChangeListener) l[i]).propertyChange(event);
                        }
                }
-
+               
                public void actionPerformed(ActionEvent e) {
                        // Setting performed in putValue
                }
-
+               
        }
        
        /**
@@ -425,16 +457,15 @@ public class DoubleModel implements ChangeListener, ChangeSource {
        }
        
        
-       
-       
 
 
-       ////////////  Main model  /////////////
 
+       ////////////  Main model  /////////////
+       
        /*
         * The main model handles all values in SI units, i.e. no conversion is made within the model.
         */
-       
+
        private final ChangeSource source;
        private final String valueName;
        private final double multiplier;
@@ -449,36 +480,38 @@ public class DoubleModel implements ChangeListener, ChangeSource {
        
        private final UnitGroup units;
        private Unit currentUnit;
-
+       
        private final double minValue;
        private final double maxValue;
-
        
-       private int firing = 0;  //  >0 when model itself is sending events
+       private String toString = null;
        
+
+       private int firing = 0; //  >0 when model itself is sending events
        
+
        // Used to differentiate changes in valueName and other changes in the component:
        private double lastValue = 0;
        private boolean lastAutomatic = false;
-               
+       
        
        public DoubleModel(double value) {
-               this(value, UnitGroup.UNITS_NONE,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+               this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
        }
        
        public DoubleModel(double value, UnitGroup unit) {
-               this(value,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+               this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
        }
        
        public DoubleModel(double value, UnitGroup unit, double min) {
-               this(value,unit,min,Double.POSITIVE_INFINITY);
+               this(value, unit, min, Double.POSITIVE_INFINITY);
        }
        
        public DoubleModel(double value, UnitGroup unit, double min, double max) {
                this.lastValue = value;
                this.minValue = min;
                this.maxValue = max;
-
+               
                source = null;
                valueName = "Constant value";
                multiplier = 1;
@@ -488,7 +521,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                units = unit;
                currentUnit = units.getDefaultUnit();
        }
-
+       
        
        /**
         * Generates a new DoubleModel that changes the values of the specified component.
@@ -505,7 +538,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                this.source = source;
                this.valueName = valueName;
                this.multiplier = multiplier;
-
+               
                this.units = unit;
                currentUnit = units.getDefaultUnit();
                
@@ -515,26 +548,28 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                try {
                        getMethod = source.getClass().getMethod("get" + valueName);
                } catch (NoSuchMethodException e) {
-                       throw new IllegalArgumentException("get method for value '"+valueName+
-                                       "' not present in class "+source.getClass().getCanonicalName());
+                       throw new IllegalArgumentException("get method for value '" + valueName +
+                                       "' not present in class " + source.getClass().getCanonicalName());
                }
-
-               Method s=null;
+               
+               Method s = null;
                try {
-                       s = source.getClass().getMethod("set" + valueName,double.class);
-               } catch (NoSuchMethodException e1) { }  // Ignore
+                       s = source.getClass().getMethod("set" + valueName, double.class);
+               } catch (NoSuchMethodException e1) {
+               } // Ignore
                setMethod = s;
                
                // Automatic selection methods
                
-               Method set=null,get=null;
+               Method set = null, get = null;
                
                try {
                        get = source.getClass().getMethod("is" + valueName + "Automatic");
-                       set = source.getClass().getMethod("set" + valueName + "Automatic",boolean.class);
-               } catch (NoSuchMethodException e) { } // ignore
+                       set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
+               } catch (NoSuchMethodException e) {
+               } // ignore
                
-               if (set!=null && get!=null) {
+               if (set != null && get != null) {
                        getAutoMethod = get;
                        setAutoMethod = set;
                } else {
@@ -543,57 +578,57 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                }
                
        }
-
+       
        public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
                        double min) {
-               this(source,valueName,multiplier,unit,min,Double.POSITIVE_INFINITY);
+               this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
        }
        
        public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
-               this(source,valueName,multiplier,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+               this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
        }
        
-       public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, 
+       public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
                        double min, double max) {
-               this(source,valueName,1.0,unit,min,max);
+               this(source, valueName, 1.0, unit, min, max);
        }
        
        public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
-               this(source,valueName,1.0,unit,min,Double.POSITIVE_INFINITY);
+               this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
        }
        
        public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
-               this(source,valueName,1.0,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+               this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
        }
-
+       
        public DoubleModel(ChangeSource source, String valueName) {
-               this(source,valueName,1.0,UnitGroup.UNITS_NONE,
-                               Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+               this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
+                               Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
        }
-
+       
        public DoubleModel(ChangeSource source, String valueName, double min) {
-               this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,Double.POSITIVE_INFINITY);
+               this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
        }
        
        public DoubleModel(ChangeSource source, String valueName, double min, double max) {
-               this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,max);
+               this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
        }
        
        
-       
+
        /**
         * Returns the value of the variable (in SI units).
         */
        public double getValue() {
-               if (getMethod==null)  // Constant value
+               if (getMethod == null) // Constant value
                        return lastValue;
-
+               
                try {
-                       return (Double)getMethod.invoke(source)*multiplier;
+                       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);
                }
@@ -604,27 +639,28 @@ public class DoubleModel implements ChangeListener, ChangeSource {
         * @param v New value for parameter in SI units.
         */
        public void setValue(double v) {
-               if (setMethod==null) {
+               log.debug("Setting value " + v + " for " + this);
+               if (setMethod == null) {
                        if (getMethod != null) {
-                               throw new RuntimeException("setMethod not available for variable '"+valueName+
-                                               "' in class "+source.getClass().getCanonicalName());
+                               throw new BugException("setMethod not available for variable '" + valueName +
+                                               "' in class " + source.getClass().getCanonicalName());
                        }
                        lastValue = v;
                        fireStateChanged();
                        return;
                }
-
+               
                try {
-                       setMethod.invoke(source, v/multiplier);
+                       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);
                }
        }
-
+       
        
        /**
         * Returns whether setting the value automatically is available.
@@ -632,7 +668,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
        public boolean isAutomaticAvailable() {
                return (getAutoMethod != null) && (setAutoMethod != null);
        }
-
+       
        /**
         * Returns whether the value is currently being set automatically.
         * Returns false if automatic setting is not available at all.
@@ -642,7 +678,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                        return false;
                
                try {
-                       return (Boolean)getAutoMethod.invoke(source);
+                       return (Boolean) getAutoMethod.invoke(source);
                } catch (IllegalArgumentException e) {
                        throw new BugException("Method call failed", e);
                } catch (IllegalAccessException e) {
@@ -658,10 +694,12 @@ public class DoubleModel implements ChangeListener, ChangeSource {
         */
        public void setAutomatic(boolean auto) {
                if (setAutoMethod == null) {
-                       fireStateChanged();  // in case something is out-of-sync
+                       log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
+                       fireStateChanged(); // in case something is out-of-sync
                        return;
                }
                
+               log.debug("Setting automatic to " + auto + " for " + this);
                lastAutomatic = auto;
                try {
                        setAutoMethod.invoke(source, auto);
@@ -674,7 +712,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                }
        }
        
-
+       
        /**
         * Returns the current Unit.  At the beginning it is the default unit of the UnitGroup.
         * @return The most recently set unit.
@@ -690,6 +728,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
        public void setCurrentUnit(Unit u) {
                if (currentUnit == u)
                        return;
+               log.debug("Setting unit for " + this + " to '" + u + "'");
                currentUnit = u;
                fireStateChanged();
        }
@@ -705,7 +744,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
        }
        
        
-       
+
        /**
         * Add a listener to the model.  Adds the model as a listener to the value source if this
         * is the first listener.
@@ -719,12 +758,11 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                                lastAutomatic = isAutomatic();
                        }
                }
-
+               
                listeners.add(l);
-               if (DEBUG_LISTENERS)
-                       System.out.println(this+" adding listener (total "+listeners.size()+"): "+l);
+               log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
        }
-
+       
        /**
         * Remove a listener from the model.  Removes the model from being a listener to the Component
         * if this was the last listener of the model.
@@ -735,10 +773,19 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                if (listeners.isEmpty() && source != null) {
                        source.removeChangeListener(this);
                }
-               if (DEBUG_LISTENERS)
-                       System.out.println(this+" removing listener (total "+listeners.size()+"): "+l);
+               log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
        }
        
+       
+       @Override
+       protected void finalize() throws Throwable {
+               super.finalize();
+               if (!listeners.isEmpty()) {
+                       log.warn(this + " being garbage-collected while having listeners " + listeners);
+               }
+       };
+       
+       
        /**
         * Fire a ChangeEvent to all listeners.
         */
@@ -746,11 +793,11 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                Object[] l = listeners.toArray();
                ChangeEvent event = new ChangeEvent(this);
                firing++;
-               for (int i=0; i<l.length; i++)
-                       ((ChangeListener)l[i]).stateChanged(event);
+               for (int i = 0; i < l.length; i++)
+                       ((ChangeListener) l[i]).stateChanged(event);
                firing--;
        }
-
+       
        /**
         * Called when the component changes.  Checks whether the modeled value has changed, and if
         * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
@@ -764,14 +811,20 @@ public class DoubleModel implements ChangeListener, ChangeSource {
                lastAutomatic = b;
                fireStateChanged();
        }
-
+       
+       
        /**
         * Explain the DoubleModel as a String.
         */
        @Override
        public String toString() {
-               if (source == null)
-                       return "DoubleModel[constant="+lastValue+"]";
-               return "DoubleModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+               if (toString == null) {
+                       if (source == null) {
+                               toString = "DoubleModel[constant=" + lastValue + "]";
+                       } else {
+                               toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";
+                       }
+               }
+               return toString;
        }
 }
index 35efe7fe260a4be5b1cc24c64f0e11e3e2519b64..8cf103652359ad4ab83bd925c7581b8e172bfc60 100644 (file)
@@ -9,14 +9,17 @@ import javax.swing.SpinnerNumberModel;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.ChangeSource;
 import net.sf.openrocket.util.Reflection;
 
 
 public class IntegerModel implements ChangeListener {
-
-
+       private static final LogHelper log = Application.getLogger();
+       
+       
        //////////// JSpinner Model ////////////
        
        private class IntegerSpinnerModel extends SpinnerNumberModel {
@@ -24,44 +27,43 @@ public class IntegerModel implements ChangeListener {
                public Object getValue() {
                        return IntegerModel.this.getValue();
                }
-
+               
                @Override
                public void setValue(Object value) {
-                       if (firing > 0)   // Ignore, if called when model is sending events
+                       if (firing > 0) {
+                               // Ignore, if called when model is sending events
+                               log.verbose("Ignoring call to SpinnerModel setValue for " + IntegerModel.this.toString() +
+                                               " value=" + value + ", currently firing events");
                                return;
-                       Number num = (Number)value;
+                               
+                       }
+                       Number num = (Number) value;
                        int newValue = num.intValue();
+                       log.user("SpinnerModel setValue called for " + IntegerModel.this.toString() + " newValue=" + newValue);
                        IntegerModel.this.setValue(newValue);
-                       
-//                     try {
-//                             int newValue = Integer.parseInt((String)value);
-//                             IntegerModel.this.setValue(newValue);
-//                     } catch (NumberFormatException e) { 
-//                             IntegerModel.this.fireStateChanged();
-//                     };
                }
-                       
+               
                @Override
                public Object getNextValue() {
                        int d = IntegerModel.this.getValue();
                        if (d >= maxValue)
                                return null;
-                       return (d+1);
+                       return (d + 1);
                }
-
+               
                @Override
                public Object getPreviousValue() {
                        int d = IntegerModel.this.getValue();
                        if (d <= minValue)
                                return null;
-                       return (d-1);
+                       return (d - 1);
                }
                
                @Override
                public void addChangeListener(ChangeListener l) {
                        IntegerModel.this.addChangeListener(l);
                }
-
+               
                @Override
                public void removeChangeListener(ChangeListener l) {
                        IntegerModel.this.removeChangeListener(l);
@@ -82,11 +84,11 @@ public class IntegerModel implements ChangeListener {
 
 
        ////////////  Main model  /////////////
-
+       
        /*
         * The main model handles all values in SI units, i.e. no conversion is made within the model.
         */
-       
+
        private final ChangeSource source;
        private final String valueName;
        
@@ -94,26 +96,27 @@ public class IntegerModel implements ChangeListener {
        private final Method setMethod;
        
        private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
-
+       
        private final int minValue;
        private final int maxValue;
-
        
-       private int firing = 0;  //  >0 when model itself is sending events
+       private String toString = null;
        
+
+       private int firing = 0; //  >0 when model itself is sending events
        
+
        // Used to differentiate changes in valueName and other changes in the source:
        private int lastValue = 0;
-               
-
        
+       
+
        /**
         * Generates a new DoubleModel that changes the values of the specified source.
         * The double value is read and written using the methods "get"/"set" + valueName.
         *  
         * @param source Component whose parameter to use.
-        * @param valueName Name of metods used to get/set the parameter.
-        * @param multiplier Value shown by the model is the value from source.getXXX * multiplier
+        * @param valueName Name of methods used to get/set the parameter.
         * @param min Minimum value allowed (in SI units)
         * @param max Maximum value allowed (in SI units)
         */
@@ -126,30 +129,30 @@ public class IntegerModel implements ChangeListener {
                
                try {
                        getMethod = source.getClass().getMethod("get" + valueName);
-                       setMethod = source.getClass().getMethod("set" + valueName,int.class);
+                       setMethod = source.getClass().getMethod("set" + valueName, int.class);
                } catch (NoSuchMethodException e) {
-                       throw new IllegalArgumentException("get/set methods for value '"+valueName+
-                                       "' not present in class "+source.getClass().getCanonicalName());
+                       throw new IllegalArgumentException("get/set methods for value '" + valueName +
+                                       "' not present in class " + source.getClass().getCanonicalName());
                }
        }
-
+       
        public IntegerModel(ChangeSource source, String valueName, int min) {
-               this(source,valueName,min,Integer.MAX_VALUE);
+               this(source, valueName, min, Integer.MAX_VALUE);
        }
        
        public IntegerModel(ChangeSource source, String valueName) {
-               this(source,valueName,Integer.MIN_VALUE,Integer.MAX_VALUE);
+               this(source, valueName, Integer.MIN_VALUE, Integer.MAX_VALUE);
        }
        
-
-       
        
+
+
        /**
         * Returns the value of the variable.
         */
        public int getValue() {
                try {
-                       return (Integer)getMethod.invoke(source);
+                       return (Integer) getMethod.invoke(source);
                } catch (IllegalArgumentException e) {
                        throw new BugException(e);
                } catch (IllegalAccessException e) {
@@ -163,6 +166,7 @@ public class IntegerModel implements ChangeListener {
         * Sets the value of the variable.
         */
        public void setValue(int v) {
+               log.debug("Setting value " + v + " for " + this);
                try {
                        setMethod.invoke(source, v);
                } catch (IllegalArgumentException e) {
@@ -173,7 +177,7 @@ public class IntegerModel implements ChangeListener {
                        throw Reflection.handleWrappedException(e);
                }
        }
-
+       
        
        /**
         * Add a listener to the model.  Adds the model as a listener to the Component if this
@@ -185,10 +189,11 @@ public class IntegerModel implements ChangeListener {
                        source.addChangeListener(this);
                        lastValue = getValue();
                }
-
+               
                listeners.add(l);
+               log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
        }
-
+       
        /**
         * Remove a listener from the model.  Removes the model from being a listener to the Component
         * if this was the last listener of the model.
@@ -199,17 +204,28 @@ public class IntegerModel implements ChangeListener {
                if (listeners.isEmpty()) {
                        source.removeChangeListener(this);
                }
+               log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
        }
        
+       
+       @Override
+       protected void finalize() throws Throwable {
+               super.finalize();
+               if (!listeners.isEmpty()) {
+                       log.warn(this + " being garbage-collected while having listeners " + listeners);
+               }
+       };
+       
+       
        public void fireStateChanged() {
                Object[] l = listeners.toArray();
                ChangeEvent event = new ChangeEvent(this);
                firing++;
-               for (int i=0; i<l.length; i++)
-                       ((ChangeListener)l[i]).stateChanged(event);
+               for (int i = 0; i < l.length; i++)
+                       ((ChangeListener) l[i]).stateChanged(event);
                firing--;
        }
-
+       
        /**
         * Called when the source changes.  Checks whether the modeled value has changed, and if
         * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
@@ -221,13 +237,16 @@ public class IntegerModel implements ChangeListener {
                lastValue = v;
                fireStateChanged();
        }
-
+       
        /**
         * Explain the DoubleModel as a String.
         */
        @Override
        public String toString() {
-               return "IntegerModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+               if (toString == null) {
+                       toString = "IntegerModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";
+               }
+               return toString;
        }
        
 }
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);
                                        }
index df5ce634fa42f71816e0188a57d9d489ac45e04e..ccc1174f49fa96b4a9dcf1407c9e9233d23b7303 100644 (file)
@@ -18,14 +18,17 @@ import net.sf.openrocket.gui.components.BasicSlider;
 import net.sf.openrocket.gui.components.StyledLabel;
 import net.sf.openrocket.gui.components.UnitSelector;
 import net.sf.openrocket.gui.components.StyledLabel.Style;
+import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.rocketcomponent.FinSet;
 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
 import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.unit.UnitGroup;
 
 
 public abstract class FinSetConfig extends RocketComponentConfig {
-
+       private static final LogHelper log = Application.getLogger();
+       
        private JButton split = null;
        
        public FinSetConfig(RocketComponent component) {
@@ -33,11 +36,11 @@ public abstract class FinSetConfig extends RocketComponentConfig {
                
                tabbedPane.insertTab("Fin tabs", null, finTabPanel(), "Through-the-wall fin tabs", 0);
        }
-
        
        
+
        protected void addFinSetButtons() {
-               JButton convert=null;
+               JButton convert = null;
                
                //// Convert buttons
                if (!(component instanceof FreeformFinSet)) {
@@ -46,70 +49,75 @@ public abstract class FinSetConfig extends RocketComponentConfig {
                        convert.addActionListener(new ActionListener() {
                                @Override
                                public void actionPerformed(ActionEvent e) {
+                                       log.user("Converting " + component.getComponentName() + " into freeform fin set");
+                                       
                                        // Do change in future for overall safety
                                        SwingUtilities.invokeLater(new Runnable() {
                                                @Override
                                                public void run() {
                                                        ComponentConfigDialog.addUndoPosition("Convert fin set");
-                                                       RocketComponent freeform = 
-                                                               FreeformFinSet.convertFinSet((FinSet)component);
+                                                       RocketComponent freeform =
+                                                                       FreeformFinSet.convertFinSet((FinSet) component);
                                                        ComponentConfigDialog.showDialog(freeform);
                                                }
                                        });
-
+                                       
                                        ComponentConfigDialog.hideDialog();
                                }
                        });
                }
-
+               
                split = new JButton("Split fins");
                split.setToolTipText("Split the fin set into separate fins");
                split.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" +
+                                               ((FinSet) component).getFinCount());
+                               
                                // Do change in future for overall safety
                                SwingUtilities.invokeLater(new Runnable() {
                                        @Override
                                        public void run() {
                                                RocketComponent parent = component.getParent();
                                                int index = parent.getChildPosition(component);
-                                               int count = ((FinSet)component).getFinCount();
-                                               double base = ((FinSet)component).getBaseRotation();
+                                               int count = ((FinSet) component).getFinCount();
+                                               double base = ((FinSet) component).getBaseRotation();
                                                if (count <= 1)
                                                        return;
                                                
                                                ComponentConfigDialog.addUndoPosition("Split fin set");
                                                parent.removeChild(index);
-                                               for (int i=0; i<count; i++) {
-                                                       FinSet copy = (FinSet)component.copy();
+                                               for (int i = 0; i < count; i++) {
+                                                       FinSet copy = (FinSet) component.copy();
                                                        copy.setFinCount(1);
-                                                       copy.setBaseRotation(base + i*2*Math.PI/count);
-                                                       copy.setName(copy.getName() + " #" + (i+1));
-                                                       parent.addChild(copy, index+i);
+                                                       copy.setBaseRotation(base + i * 2 * Math.PI / count);
+                                                       copy.setName(copy.getName() + " #" + (i + 1));
+                                                       parent.addChild(copy, index + i);
                                                }
                                        }
                                });
-
+                               
                                ComponentConfigDialog.hideDialog();
                        }
                });
-               split.setEnabled(((FinSet)component).getFinCount() > 1);
+               split.setEnabled(((FinSet) component).getFinCount() > 1);
                
-               if (convert==null)
+               if (convert == null)
                        addButtons(split);
                else
-                       addButtons(split,convert);
-
+                       addButtons(split, convert);
+               
        }
-
+       
        public JPanel finTabPanel() {
                JPanel panel = new JPanel(
                                new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%",
-                               "[150lp::][65lp::][30lp::][200lp::]",""));
-//             JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
-//                             "[40lp][80lp::][30lp::][100lp::]",""));
-
-               panel.add(new StyledLabel("Through-the-wall fin tabs:", Style.BOLD), 
+                                               "[150lp::][65lp::][30lp::][200lp::]", ""));
+               //              JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
+               //                              "[40lp][80lp::][30lp::][100lp::]",""));
+               
+               panel.add(new StyledLabel("Through-the-wall fin tabs:", Style.BOLD),
                                "spanx, wrap 30lp");
                
                JLabel label;
@@ -118,7 +126,7 @@ public abstract class FinSetConfig extends RocketComponentConfig {
                DoubleModel length2;
                DoubleModel length_2;
                JSpinner spin;
-
+               
                length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0);
                length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0);
                length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0);
@@ -132,13 +140,13 @@ public abstract class FinSetConfig extends RocketComponentConfig {
                
                spin = new JSpinner(m.getSpinnerModel());
                spin.setEditor(new SpinnerEditor(spin));
-               panel.add(spin,"growx 1");
+               panel.add(spin, "growx 1");
                
-               panel.add(new UnitSelector(m),"growx 1");
+               panel.add(new UnitSelector(m), "growx 1");
                panel.add(new BasicSlider(m.getSliderModel(DoubleModel.ZERO, length)),
                                "w 100lp, growx 5, wrap");
-
                
+
                ////  Tab length
                label = new JLabel("Tab height:");
                label.setToolTipText("The spanwise height of the fin tab.");
@@ -148,13 +156,13 @@ public abstract class FinSetConfig extends RocketComponentConfig {
                
                spin = new JSpinner(m.getSpinnerModel());
                spin.setEditor(new SpinnerEditor(spin));
-               panel.add(spin,"growx");
+               panel.add(spin, "growx");
                
-               panel.add(new UnitSelector(m),"growx");
+               panel.add(new UnitSelector(m), "growx");
                panel.add(new BasicSlider(m.getSliderModel(DoubleModel.ZERO, length2)),
                                "w 100lp, growx 5, wrap para");
-
                
+
                ////  Tab position
                label = new JLabel("Tab position:");
                label.setToolTipText("The position of the fin tab.");
@@ -164,28 +172,28 @@ public abstract class FinSetConfig extends RocketComponentConfig {
                
                spin = new JSpinner(m.getSpinnerModel());
                spin.setEditor(new SpinnerEditor(spin));
-               panel.add(spin,"growx");
-               
-               panel.add(new UnitSelector(m),"growx");
-               panel.add(new BasicSlider(m.getSliderModel(length_2, length2)),"w 100lp, growx 5, wrap");
-
+               panel.add(spin, "growx");
                
+               panel.add(new UnitSelector(m), "growx");
+               panel.add(new BasicSlider(m.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
                
+
+
                label = new JLabel("relative to");
                panel.add(label, "right, gapright unrel");
                
-               EnumModel<FinSet.TabRelativePosition> em = 
-                       new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
+               EnumModel<FinSet.TabRelativePosition> em =
+                               new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
                
                panel.add(new JComboBox(em), "spanx 3, growx");
                
                return panel;
        }
-
+       
        @Override
        public void updateFields() {
                super.updateFields();
                if (split != null)
-                       split.setEnabled(((FinSet)component).getFinCount() > 1);
+                       split.setEnabled(((FinSet) component).getFinCount() > 1);
        }
 }
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..927cc54
--- /dev/null
@@ -0,0 +1,520 @@
+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());
+                       // By default display DEBUG and above
+                       box.setSelected(l.atLeast(LogLevel.DEBUG));
+                       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);
+               sorter.setRowFilter(new LogFilter());
+               
+
+               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 05a19950dd56b4765579096dd479d99130f39c65..87e36e67852418682510f1367e992e63994f9881 100644 (file)
@@ -3,16 +3,31 @@ package net.sf.openrocket.gui.dialogs;
 import java.awt.Component;
 
 import javax.swing.JOptionPane;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
 
-public class DetailDialog {
+import net.sf.openrocket.util.GUIUtil;
 
-       public static void showDetailedMessageDialog(Component parentComponent, Object message, 
-                       String details, String title, int messageType)  {
+public class DetailDialog {
+       
+       public static void showDetailedMessageDialog(Component parentComponent, Object message,
+                       String details, String title, int messageType) {
                
-               // TODO: HIGH: Detailed dialog
-               JOptionPane.showMessageDialog(parentComponent, message, title, messageType, null);
+               if (details != null) {
+                       JTextArea textArea = null;
+                       textArea = new JTextArea(5, 40);
+                       textArea.setText(details);
+                       textArea.setCaretPosition(0);
+                       textArea.setEditable(false);
+                       GUIUtil.changeFontSize(textArea, -2);
+                       JOptionPane.showMessageDialog(parentComponent,
+                                       new Object[] { message, new JScrollPane(textArea) },
+                                       title, messageType, null);
+               } else {
+                       JOptionPane.showMessageDialog(parentComponent, message, title, messageType, null);
+               }
                
        }
        
-       
+
 }
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/dialogs/optimization/GeneralOptimizationDialog.java b/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java
new file mode 100644 (file)
index 0000000..ed55079
--- /dev/null
@@ -0,0 +1,86 @@
+package net.sf.openrocket.gui.dialogs.optimization;
+
+import java.awt.Window;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+import javax.swing.JDialog;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter;
+import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameterService;
+import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier;
+import net.sf.openrocket.optimization.rocketoptimization.SimulationModifierService;
+import net.sf.openrocket.util.BugException;
+
+public class GeneralOptimizationDialog extends JDialog {
+       
+       private final List<OptimizableParameter> optimizationParameters = new ArrayList<OptimizableParameter>();
+       private final Map<Object, List<SimulationModifier>> simulationModifiers =
+                       new HashMap<Object, List<SimulationModifier>>();
+       
+
+       private final OpenRocketDocument document;
+       
+       public GeneralOptimizationDialog(OpenRocketDocument document, Window parent) {
+               this.document = document;
+               
+               loadOptimizationParameters();
+               loadSimulationModifiers();
+       }
+       
+       
+       private void loadOptimizationParameters() {
+               ServiceLoader<OptimizableParameterService> loader =
+                               ServiceLoader.load(OptimizableParameterService.class);
+               
+               for (OptimizableParameterService g : loader) {
+                       optimizationParameters.addAll(g.getParameters(document));
+               }
+               
+               if (optimizationParameters.isEmpty()) {
+                       throw new BugException("No rocket optimization parameters found, distribution built wrong.");
+               }
+               
+               Collections.sort(optimizationParameters, new Comparator<OptimizableParameter>() {
+                       @Override
+                       public int compare(OptimizableParameter o1, OptimizableParameter o2) {
+                               return o1.getName().compareTo(o2.getName());
+                       }
+               });
+       }
+       
+       
+       private void loadSimulationModifiers() {
+               ServiceLoader<SimulationModifierService> loader = ServiceLoader.load(SimulationModifierService.class);
+               
+               for (SimulationModifierService g : loader) {
+                       for (SimulationModifier m : g.getModifiers(document)) {
+                               Object key = m.getRelatedObject();
+                               List<SimulationModifier> list = simulationModifiers.get(key);
+                               if (list == null) {
+                                       list = new ArrayList<SimulationModifier>();
+                                       simulationModifiers.put(key, list);
+                               }
+                               list.add(m);
+                       }
+               }
+               
+               for (Object key : simulationModifiers.keySet()) {
+                       List<SimulationModifier> list = simulationModifiers.get(key);
+                       Collections.sort(list, new Comparator<SimulationModifier>() {
+                               @Override
+                               public int compare(SimulationModifier o1, SimulationModifier o2) {
+                                       return o1.getName().compareTo(o2.getName());
+                               }
+                       });
+               }
+               
+       }
+       
+}
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 43c214c72e5eb31e2ff95e22bec2162b72fcfea0..3c8f4e2c7e76ed02230525f04147421db1776323 100644 (file)
@@ -2,9 +2,6 @@ package net.sf.openrocket.gui.main;
 
 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;
@@ -16,14 +13,16 @@ 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.DetailDialog;
 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.PrintDialog;
 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;
@@ -35,6 +34,8 @@ import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.GUIUtil;
 import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.MemoryManagement;
+import net.sf.openrocket.util.MemoryManagement.MemoryData;
 import net.sf.openrocket.util.OpenFileWorker;
 import net.sf.openrocket.util.Prefs;
 import net.sf.openrocket.util.Reflection;
@@ -61,8 +62,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;
@@ -89,12 +88,14 @@ 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.LinkedList;
+import java.util.List;
 import java.util.concurrent.ExecutionException;
 
 public class BasicFrame extends JFrame {
@@ -283,7 +284,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 +405,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 +421,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 +434,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 +454,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 +468,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 +483,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();
                        }
                });
@@ -498,6 +508,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();
                        }
                });
@@ -551,6 +562,7 @@ public class BasicFrame extends JFrame {
                                "preferences");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Preferences selected");
                                PreferencesDialog.showPreferences();
                        }
                });
@@ -570,6 +582,7 @@ public class BasicFrame extends JFrame {
                                "separately");
                item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Component analysis selected");
                                ComponentAnalysisDialog.showDialog(rocketpanel);
                        }
                });
@@ -597,26 +610,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);
                        }
                });
@@ -638,6 +667,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 " +
@@ -656,6 +686,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:",
@@ -688,6 +719,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);
@@ -702,6 +734,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);
@@ -711,13 +744,88 @@ public class BasicFrame extends JFrame {
                });
                menu.add(item);
                
+               menu.addSeparator();
+               
 
+               item = new JMenuItem("Memory statistics");
+               item.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Memory statistics selected");
+                               
+                               // Get discarded but remaining objects (this also runs System.gc multiple times)
+                               List<MemoryData> objects = MemoryManagement.getRemainingObjects();
+                               StringBuilder sb = new StringBuilder();
+                               sb.append("Objects that should have been garbage-collected but have not been:\n");
+                               int count = 0;
+                               for (MemoryData data : objects) {
+                                       Object o = data.getReference().get();
+                                       if (o == null)
+                                               continue;
+                                       sb.append("Age ").append(System.currentTimeMillis() - data.getRegistrationTime())
+                                                       .append(" ms:  ").append(o).append('\n');
+                                       count++;
+                                       // Explicitly null the strong reference to avoid possibility of invisible references
+                                       o = null;
+                               }
+                               sb.append("Total: " + count);
+                               
+                               // Get basic memory stats
+                               System.gc();
+                               long max = Runtime.getRuntime().maxMemory();
+                               long free = Runtime.getRuntime().freeMemory();
+                               long used = max - free;
+                               String[] stats = new String[4];
+                               stats[0] = "Memory usage:";
+                               stats[1] = String.format("   Max memory:  %.1f MB", max / 1024.0 / 1024.0);
+                               stats[2] = String.format("   Used memory: %.1f MB (%.0f%%)", used / 1024.0 / 1024.0, 100.0 * used / max);
+                               stats[3] = String.format("   Free memory: %.1f MB (%.0f%%)", free / 1024.0 / 1024.0, 100.0 * free / max);
+                               
+
+                               DetailDialog.showDetailedMessageDialog(BasicFrame.this, stats, sb.toString(),
+                                               "Memory statistics", JOptionPane.INFORMATION_MESSAGE);
+                       }
+               });
+               menu.add(item);
+               
+
+               item = new JMenuItem("Exhaust memory");
+               item.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Exhaust memory selected");
+                               LinkedList<byte[]> data = new LinkedList<byte[]>();
+                               int count = 0;
+                               final int bytesPerArray = 10240;
+                               try {
+                                       while (true) {
+                                               byte[] array = new byte[bytesPerArray];
+                                               for (int i = 0; i < bytesPerArray; i++) {
+                                                       array[i] = (byte) i;
+                                               }
+                                               data.add(array);
+                                               count++;
+                                       }
+                               } catch (OutOfMemoryError error) {
+                                       data = null;
+                                       long size = bytesPerArray * (long) count;
+                                       String s = String.format("OutOfMemory occurred after %d iterations (approx. %.1f MB consumed)",
+                                                       count, size / 1024.0 / 1024.0);
+                                       log.debug(s, error);
+                                       JOptionPane.showMessageDialog(BasicFrame.this, s);
+                               }
+                       }
+               });
+               menu.add(item);
+               
 
                menu.addSeparator();
                
                item = new JMenuItem("Exception here");
                item.addActionListener(new ActionListener() {
+                       @Override
                        public void actionPerformed(ActionEvent e) {
+                               log.user("Exception here selected");
                                throw new RuntimeException("Testing exception from menu action listener");
                        }
                });
@@ -726,6 +834,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() {
@@ -740,6 +849,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() {
@@ -751,13 +861,22 @@ public class BasicFrame extends JFrame {
                });
                menu.add(item);
                
+               item = new JMenuItem("OutOfMemoryError here");
+               item.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("OutOfMemoryError here selected");
+                               throw new OutOfMemoryError("Testing OutOfMemoryError from menu action listener");
+                       }
+               });
+               menu.add(item);
+               
 
 
                return menu;
        }
        
        
-
        /**
         * Select the tab on the main pane.
         * 
@@ -780,19 +899,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;
                                }
@@ -801,10 +925,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();
@@ -830,6 +961,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)) {
@@ -840,6 +974,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);
@@ -887,16 +1022,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;
                }
                
@@ -913,6 +1047,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);
@@ -920,6 +1055,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(),
@@ -937,13 +1073,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 + ".",
@@ -958,6 +1095,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);
                
@@ -971,21 +1109,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);
                }
@@ -995,52 +1138,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;
                }
                
@@ -1051,6 +1202,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;
                }
@@ -1066,6 +1218,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);
@@ -1084,6 +1237,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.  " +
@@ -1092,38 +1246,37 @@ 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();
-               }
-       }
 
     /**
      * 
@@ -1136,7 +1289,8 @@ public class BasicFrame extends JFrame {
         * 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");
@@ -1154,13 +1308,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);
        }
        
@@ -1195,9 +1353,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;
        }
        
@@ -1209,142 +1370,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..e5429e3aec1b4a694783b7425e7feb5985f58b5b 100644 (file)
@@ -29,6 +29,8 @@ 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.logging.LogHelper;
 import net.sf.openrocket.rocketcomponent.BodyComponent;
 import net.sf.openrocket.rocketcomponent.BodyTube;
 import net.sf.openrocket.rocketcomponent.Bulkhead;
@@ -48,6 +50,7 @@ import net.sf.openrocket.rocketcomponent.Streamer;
 import net.sf.openrocket.rocketcomponent.Transition;
 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
 import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.Pair;
 import net.sf.openrocket.util.Prefs;
@@ -62,6 +65,7 @@ import net.sf.openrocket.util.Reflection;
  */
 
 public class ComponentAddButtons extends JPanel implements Scrollable {
+       private static final LogHelper log = Application.getLogger();
        
        private static final int ROWS = 3;
        private static final int MAXCOLS = 6;
@@ -363,6 +367,7 @@ public class ComponentAddButtons extends JPanel implements Scrollable {
                @Override
                protected void fireActionPerformed(ActionEvent event) {
                        super.fireActionPerformed(event);
+                       log.user("Adding component of type " + componentClass.getSimpleName());
                        RocketComponent c = null;
                        Integer position = null;
                        
@@ -373,6 +378,7 @@ public class ComponentAddButtons extends JPanel implements Scrollable {
                        Pair<RocketComponent, Integer> pos = getAdditionPosition(c);
                        if (pos == null) {
                                // Cancel addition
+                               log.info("No position to add component");
                                return;
                        }
                        c = pos.getU();
@@ -395,11 +401,9 @@ public class ComponentAddButtons extends JPanel implements Scrollable {
                        try {
                                component = (RocketComponent) constructor.newInstance();
                        } catch (InstantiationException e) {
-                               throw new BugException("Could not construct new instance of class " +
-                                               constructor, e);
+                               throw new BugException("Could not construct new instance of class " + constructor, e);
                        } catch (IllegalAccessException e) {
-                               throw new BugException("Could not construct new instance of class " +
-                                               constructor, e);
+                               throw new BugException("Could not construct new instance of class " + constructor, e);
                        } catch (InvocationTargetException e) {
                                throw Reflection.handleWrappedException(e);
                        }
@@ -407,7 +411,9 @@ public class ComponentAddButtons extends JPanel implements Scrollable {
                        // Next undo position is set by opening the configuration dialog
                        document.addUndoPosition("Add " + component.getComponentName());
                        
-
+                       log.info("Adding component " + component.getComponentName() + " to component " + c.getComponentName() +
+                                       " position=" + position);
+                       
                        if (position == null)
                                c.addChild(component);
                        else
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..9fbc9a848f6b184ed797a252d0f8c2401c67c222 100644 (file)
@@ -5,6 +5,7 @@ import javax.swing.SwingUtilities;
 
 import net.sf.openrocket.gui.dialogs.BugReportDialog;
 import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.logging.TraceException;
 import net.sf.openrocket.startup.Application;
 
 
@@ -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) {
@@ -62,6 +63,7 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                        } else {
                                log.info("Exception handler not on EDT, invoking dialog on EDT");
                                SwingUtilities.invokeAndWait(new Runnable() {
+                                       @Override
                                        public void run() {
                                                showDialog(thread, throwable);
                                        }
@@ -75,7 +77,7 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                                log.error("Caught exception while handling exception", ex);
                                System.err.println("Exception in exception handler, dumping exception:");
                                ex.printStackTrace();
-                       } catch (Throwable ignore) {
+                       } catch (Exception ignore) {
                        }
                        
                } finally {
@@ -89,11 +91,14 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
        /**
         * Handle an error condition programmatically without throwing an exception.
         * This can be used in cases where recovery of the error is desirable.
+        * <p>
+        * This method is guaranteed never to throw an exception, and can thus be safely
+        * used in finally blocks.
         * 
         * @param message       the error message.
         */
        public static void handleErrorCondition(String message) {
-               log.error(1, message);
+               log.error(1, message, new TraceException());
                handleErrorCondition(new InternalException(message));
        }
        
@@ -101,6 +106,9 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
        /**
         * Handle an error condition programmatically without throwing an exception.
         * This can be used in cases where recovery of the error is desirable.
+        * <p>
+        * This method is guaranteed never to throw an exception, and can thus be safely
+        * used in finally blocks.
         * 
         * @param message       the error message.
         * @param exception     the exception that occurred.
@@ -114,22 +122,22 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
        /**
         * Handle an error condition programmatically without throwing an exception.
         * This can be used in cases where recovery of the error is desirable.
+        * <p>
+        * This method is guaranteed never to throw an exception, and can thus be safely
+        * used in finally blocks.
         * 
         * @param exception             the exception that occurred.
         */
        public static void handleErrorCondition(final Exception exception) {
-               if (!(exception instanceof InternalException)) {
-                       log.error(1, "Error occurred", exception);
-               }
-               final ExceptionHandler handler = instance;
-               final Thread thread = Thread.currentThread();
-               
                try {
+                       if (!(exception instanceof InternalException)) {
+                               log.error(1, "Error occurred", exception);
+                       }
+                       final Thread thread = Thread.currentThread();
+                       final ExceptionHandler handler = instance;
                        
                        if (handler == null) {
-                               // Not initialized, simply print the exception
-                               log.error("Exception handler not initialized");
-                               exception.printStackTrace();
+                               log.error("Error condition occurred before exception handling has been initialized", exception);
                                return;
                        }
                        
@@ -139,14 +147,14 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                        } else {
                                log.info("Not in EDT, invoking and waiting for dialog");
                                SwingUtilities.invokeAndWait(new Runnable() {
+                                       @Override
                                        public void run() {
                                                handler.showDialog(thread, exception);
                                        }
                                });
                        }
                } catch (Exception e) {
-                       log.error("Exception occurred while showing error dialog", e);
-                       e.printStackTrace();
+                       log.error("Exception occurred in error handler", e);
                }
        }
        
@@ -291,6 +299,8 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
         */
        private static boolean isNonFatalJREBug(Throwable t) {
                
+               // NOTE:  Calling method logs the entire throwable, so log only message here
+               
                /*
                 * Detect and ignore bug 6828938 in Sun JRE 1.6.0_14 - 1.6.0_16.
                 */
@@ -348,6 +358,18 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                                return true;
                        }
                }
+               
+               /*
+                * Detect Sun JRE bug in D3D
+                */
+               if (t instanceof ClassCastException) {
+                       if (t.getMessage().equals("sun.awt.Win32GraphicsConfig cannot be cast to sun.java2d.d3d.D3DGraphicsConfig")) {
+                               log.warn("Ignoring Sun JRE bug " +
+                                               "(see http://forums.sun.com/thread.jspa?threadID=5440525): " + t);
+                               return true;
+                       }
+               }
+               
                return false;
        }
        
index 5fd4e1a92732af313cb05ee990030aa29f56abe3..644605db8fd1529b1a599c5e3510322dbb8fd7a0 100644 (file)
@@ -226,7 +226,6 @@ public class SimulationPanel extends JPanel {
                                                
                                                // Set simulation status icon
                                                Simulation.Status status = document.getSimulation(row).getStatus();
-                                               System.out.println("status=" + status);
                                                label.setIcon(Icons.SIMULATION_STATUS_ICON_MAP.get(status));
                                                
 
index c37ceef22d6b8ba9c05182a1e35b5a858ad515bf..9b94ec615c58ae282b792121109b816e3a0cb651 100644 (file)
@@ -69,7 +69,13 @@ public class SimulationRunDialog extends JDialog {
        private final JProgressBar progressBar;
        
 
+       /*
+        * NOTE:  Care must be used when accessing the simulation parameters, since they
+        * are being run in another thread.  Mutexes are used to avoid concurrent usage, which
+        * will result in an exception being thrown!
+        */
        private final Simulation[] simulations;
+       private final String[] simulationNames;
        private final SimulationWorker[] simulationWorkers;
        private final SimulationStatus[] simulationStatuses;
        private final double[] simulationMaxAltitude;
@@ -87,6 +93,7 @@ public class SimulationRunDialog extends JDialog {
                
                // Initialize the simulations
                int n = simulations.length;
+               simulationNames = new String[n];
                simulationWorkers = new SimulationWorker[n];
                simulationStatuses = new SimulationStatus[n];
                simulationMaxAltitude = new double[n];
@@ -94,6 +101,7 @@ public class SimulationRunDialog extends JDialog {
                simulationDone = new boolean[n];
                
                for (int i = 0; i < n; i++) {
+                       simulationNames[i] = simulations[i].getName();
                        simulationWorkers[i] = new InteractiveSimulationWorker(simulations[i], i);
                        executor.execute(simulationWorkers[i]);
                }
@@ -201,7 +209,7 @@ public class SimulationRunDialog extends JDialog {
                log.debug("Progressbar value " + progress);
                
                // Update the simulation fields
-               simLabel.setText("Running " + simulations[index].getName());
+               simLabel.setText("Running " + simulationNames[index]);
                if (simulationStatuses[index] == null) {
                        log.debug("No simulation status data available, setting empty labels");
                        timeLabel.setText("");
index 9e6f62bc1ca5b05765a9e1e78219211759593a43..f348fbcdbe0d22306aaac190416a878abe458b80 100644 (file)
@@ -54,8 +54,6 @@ public abstract class SimulationWorker extends SwingWorker<FlightData, Simulatio
                try {
                        simulation.simulate(listeners);
                } catch (Throwable e) {
-                       //                      System.out.println("Simulation interrupted:");
-                       //                      e.printStackTrace();
                        throwable = e;
                        return null;
                }
@@ -77,8 +75,6 @@ public abstract class SimulationWorker extends SwingWorker<FlightData, Simulatio
        /**
         * Called after a simulation is successfully simulated.  This method is not
         * called if the simulation ends in an exception.
-        * 
-        * @param sim   the simulation including the flight data
         */
        protected abstract void simulationDone();
        
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..ef13629
--- /dev/null
@@ -0,0 +1,132 @@
+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 javax.swing.ToolTipManager;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+
+
+public class ComponentTree extends JTree {
+       
+       public ComponentTree(OpenRocketDocument document) {
+               super();
+               this.setModel(new ComponentTreeModel(document.getRocket(), this));
+               this.setToggleClickCount(0);
+               
+               javax.swing.plaf.basic.BasicTreeUI plainUI = new javax.swing.plaf.basic.BasicTreeUI();
+               this.setUI(plainUI);
+               plainUI.setExpandedIcon(TreeIcon.MINUS);
+               plainUI.setCollapsedIcon(TreeIcon.PLUS);
+               plainUI.setLeftChildIndent(15);
+               
+
+               this.setBackground(Color.WHITE);
+               this.setShowsRootHandles(false);
+               
+               this.setCellRenderer(new ComponentTreeRenderer());
+               
+               this.setDragEnabled(true);
+               this.setDropMode(DropMode.INSERT);
+               this.setTransferHandler(new ComponentTreeTransferHandler(document));
+               
+               // Expand whole tree by default
+               expandTree();
+               
+               // Enable tooltips for this component
+               ToolTipManager.sharedInstance().registerComponent(this);
+               
+       }
+       
+       
+       public void expandTree() {
+               for (int i = 0; i < getRowCount(); i++)
+                       expandRow(i);
+       }
+       
+       @Override
+       public void treeDidChange() {
+               super.treeDidChange();
+               expandChildlessNodes();
+       }
+       
+       /**
+        * Expand all nodes in the tree that are visible and have no children.  This can be used
+        * to avoid the situation where a non-leaf node is marked as being expandable, but when
+        * expanding it it has no children.
+        */
+       private void expandChildlessNodes() {
+               TreeModel model = this.getModel();
+               if (model == null) {
+                       return;
+               }
+               Object root = model.getRoot();
+               expandChildlessNodes(model, new TreePath(root));
+       }
+       
+       private void expandChildlessNodes(TreeModel model, TreePath path) {
+               Object object = path.getLastPathComponent();
+               if (this.isVisible(path)) {
+                       int count = model.getChildCount(object);
+                       if (count == 0) {
+                               this.expandPath(path);
+                       }
+                       for (int i = 0; i < count; i++) {
+                               expandChildlessNodes(model, path.pathByAddingChild(model.getChild(object, 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..a7e8386
--- /dev/null
@@ -0,0 +1,45 @@
+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;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.TextUtil;
+
+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);
+               
+               // Set icon
+               setIcon(ComponentIcons.getSmallIcon(value.getClass()));
+               
+               // Set tooltip
+               RocketComponent c = (RocketComponent) value;
+               String comment = c.getComment().trim();
+               if (comment.length() > 0) {
+                       comment = TextUtil.htmlEncode(comment);
+                       comment = "<html>" + comment.replace("\n", "<br>");
+                       this.setToolTipText(comment);
+               } else {
+                       this.setToolTipText(null);
+               }
+               
+               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 1da0bd00ed885e4a63536c27393548339c1d9fae..247881e8071f9b9f0050bec8a2d975cb45d762a1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * DrawingsPrintable.java
+ * DesignReport.java
  */
 package net.sf.openrocket.gui.print;
 
@@ -330,7 +330,7 @@ public class DesignReport extends BaseVisitorStrategy {
     private void addFlightData (final Rocket theRocket, final String mid, final PdfPTable parent, int leading) {
         FlightData flight = null;
         if (theRocket.getMotorConfigurationIDs().length > 1) {
-            Rocket duplicate = theRocket.copy();
+            Rocket duplicate= theRocket.copyWithOriginalID();
             Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
             simulation.getConditions().setMotorConfigurationID(mid);
 
index c75fa072d4ff04ee795dd87c28cb977bf54d3897..ed1293e0a0f5d1acad5516a0e235727b6ba407e5 100644 (file)
@@ -18,8 +18,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;
@@ -586,7 +586,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());
@@ -634,7 +634,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
                @Override
                protected void simulationDone() {
                        // Do nothing if cancelled
-                       if (isCancelled() || backgroundSimulationWorker != this) // Double-check
+                       if (isCancelled() || backgroundSimulationWorker != this)
                                return;
                        
                        backgroundSimulationWorker = null;
@@ -684,6 +684,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
         * Updates the selection in the FigureParameters and repaints the figure.  
         * Ignores the event itself.
         */
+       @Override
        public void valueChanged(TreeSelectionEvent e) {
                TreePath[] paths = selectionModel.getSelectionPaths();
                if (paths == null) {
@@ -714,6 +715,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
                        figure.addChangeListener(this);
                }
                
+               @Override
                public void actionPerformed(ActionEvent e) {
                        boolean state = (Boolean) getValue(Action.SELECTED_KEY);
                        if (state == true) {
@@ -724,6 +726,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
                        stateChanged(null);
                }
                
+               @Override
                public void stateChanged(ChangeEvent e) {
                        putValue(Action.SELECTED_KEY, figure.getType() == type);
                }
index a51c0a92199b6880f92fc71d3e7d16c57d96c689..d6b52104ba8307966504c489f886556d008c73b3 100644 (file)
@@ -16,6 +16,9 @@ import net.sf.openrocket.util.BugException;
  *  <li><code>message</code>   the String message (may be null).
  *  <li><code>cause</code>             the exception that caused this log (may be null).
  * </ul>
+ * <p>
+ * The logging methods are guaranteed never to throw an exception, and can thus be safely
+ * used in finally blocks.
  * 
  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
  */
@@ -23,13 +26,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 +46,69 @@ 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) {
+               try {
+                       log(createLogLine(0, LogLevel.VBOSE, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+       
+       /**
+        * 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) {
+               try {
+                       log(createLogLine(0, LogLevel.VBOSE, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+       
+       /**
+        * 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) {
+               try {
+                       log(createLogLine(levels, LogLevel.VBOSE, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+       
+       /**
+        * 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) {
+               try {
+                       log(createLogLine(levels, LogLevel.VBOSE, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+       
        
        /**
         * Log using DEBUG level.
@@ -57,7 +116,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void debug(String message) {
-               log(createLogLine(0, LogLevel.DEBUG, message, null));
+               try {
+                       log(createLogLine(0, LogLevel.DEBUG, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -67,7 +130,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void debug(String message, Throwable cause) {
-               log(createLogLine(0, LogLevel.DEBUG, message, cause));
+               try {
+                       log(createLogLine(0, LogLevel.DEBUG, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -77,7 +144,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void debug(int levels, String message) {
-               log(createLogLine(levels, LogLevel.DEBUG, message, null));
+               try {
+                       log(createLogLine(levels, LogLevel.DEBUG, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -88,7 +159,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void debug(int levels, String message, Throwable cause) {
-               log(createLogLine(levels, LogLevel.DEBUG, message, cause));
+               try {
+                       log(createLogLine(levels, LogLevel.DEBUG, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        
@@ -98,7 +173,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void info(String message) {
-               log(createLogLine(0, LogLevel.INFO, message, null));
+               try {
+                       log(createLogLine(0, LogLevel.INFO, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -108,7 +187,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void info(String message, Throwable cause) {
-               log(createLogLine(0, LogLevel.INFO, message, cause));
+               try {
+                       log(createLogLine(0, LogLevel.INFO, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -118,7 +201,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void info(int levels, String message) {
-               log(createLogLine(levels, LogLevel.INFO, message, null));
+               try {
+                       log(createLogLine(levels, LogLevel.INFO, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -129,7 +216,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void info(int levels, String message, Throwable cause) {
-               log(createLogLine(levels, LogLevel.INFO, message, cause));
+               try {
+                       log(createLogLine(levels, LogLevel.INFO, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        
@@ -139,7 +230,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void user(String message) {
-               log(createLogLine(0, LogLevel.USER, message, null));
+               try {
+                       log(createLogLine(0, LogLevel.USER, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -149,7 +244,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void user(String message, Throwable cause) {
-               log(createLogLine(0, LogLevel.USER, message, cause));
+               try {
+                       log(createLogLine(0, LogLevel.USER, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -159,7 +258,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void user(int levels, String message) {
-               log(createLogLine(levels, LogLevel.USER, message, null));
+               try {
+                       log(createLogLine(levels, LogLevel.USER, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -170,7 +273,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void user(int levels, String message, Throwable cause) {
-               log(createLogLine(levels, LogLevel.USER, message, cause));
+               try {
+                       log(createLogLine(levels, LogLevel.USER, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        
@@ -180,7 +287,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void warn(String message) {
-               log(createLogLine(0, LogLevel.WARN, message, null));
+               try {
+                       log(createLogLine(0, LogLevel.WARN, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -190,7 +301,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void warn(String message, Throwable cause) {
-               log(createLogLine(0, LogLevel.WARN, message, cause));
+               try {
+                       log(createLogLine(0, LogLevel.WARN, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -200,7 +315,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void warn(int levels, String message) {
-               log(createLogLine(levels, LogLevel.WARN, message, null));
+               try {
+                       log(createLogLine(levels, LogLevel.WARN, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -211,7 +330,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void warn(int levels, String message, Throwable cause) {
-               log(createLogLine(levels, LogLevel.WARN, message, cause));
+               try {
+                       log(createLogLine(levels, LogLevel.WARN, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        
@@ -221,7 +344,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void error(String message) {
-               log(createLogLine(0, LogLevel.ERROR, message, null));
+               try {
+                       log(createLogLine(0, LogLevel.ERROR, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -231,7 +358,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void error(String message, Throwable cause) {
-               log(createLogLine(0, LogLevel.ERROR, message, cause));
+               try {
+                       log(createLogLine(0, LogLevel.ERROR, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -241,7 +372,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void error(int levels, String message) {
-               log(createLogLine(levels, LogLevel.ERROR, message, null));
+               try {
+                       log(createLogLine(levels, LogLevel.ERROR, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -252,11 +387,15 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void error(int levels, String message, Throwable cause) {
-               log(createLogLine(levels, LogLevel.ERROR, message, cause));
+               try {
+                       log(createLogLine(levels, LogLevel.ERROR, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        
-       
+
        /**
         * Log using the provided log level.
         * 
@@ -264,7 +403,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void log(LogLevel level, String message) {
-               log(createLogLine(0, level, message, null));
+               try {
+                       log(createLogLine(0, level, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -275,7 +418,11 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void log(LogLevel level, String message, Throwable cause) {
-               log(createLogLine(0, level, message, cause));
+               try {
+                       log(createLogLine(0, level, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -286,7 +433,11 @@ public abstract class LogHelper {
         * @param message       the logged message (may be null).
         */
        public void log(int levels, LogLevel level, String message) {
-               log(createLogLine(levels, level, message, null));
+               try {
+                       log(createLogLine(levels, level, message, null));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        /**
@@ -298,11 +449,15 @@ public abstract class LogHelper {
         * @param cause         the causing exception (may be null).
         */
        public void log(int levels, LogLevel level, String message, Throwable cause) {
-               log(createLogLine(levels, level, message, cause));
+               try {
+                       log(createLogLine(levels, level, message, cause));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
        }
        
        
-       
+
        /**
         * Instantiates, logs and throws a BugException.  The message is logged at
         * ERROR level.
@@ -336,7 +491,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 +504,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..b52d14536a383c6bf1aa18d1092b8517298d127a 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,27 +30,46 @@ 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 log level with highest priority */
+       public static final LogLevel HIGHEST;
+       /** The log level with lowest priority */
+       public static final LogLevel LOWEST;
        /** 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;
+               
+               LogLevel[] values = LogLevel.values();
+               HIGHEST = values[0];
+               LOWEST = values[values.length - 1];
        }
        
        /**
@@ -58,7 +79,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 +98,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 +109,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 3b2a2c4341abc67b9a53a913a6520ce06116c0b9..d902106563fa5f7e23b3b44d91fabf4bdd05d526 100644 (file)
@@ -54,12 +54,12 @@ public class LogLevelBufferLogger extends LogHelper {
                        
                        if (misses > 0) {
                                if (logs.isEmpty()) {
-                                       result.add(new LogLine(level, 0, 0, new TraceException(),
-                                                       "--- " + misses + " " + level + " lines removed but log is empty! ---",
+                                       result.add(new LogLine(level, 0, 0, null,
+                                                       "===== " + misses + " " + level + " lines removed but log is empty! =====",
                                                        null));
                                } else {
-                                       result.add(new LogLine(level, logs.get(0).getLogCount(), 0, new TraceException(),
-                                                       "--- " + misses + " " + level + " lines removed ---", null));
+                                       result.add(new LogLine(level, logs.get(0).getLogCount(), 0, null,
+                                                       "===== " + misses + " " + level + " lines removed =====", null));
                                }
                        }
                        result.addAll(logs);
index 71b92d48f8452db2e24f477ae5c104c6b397e630..c2dc4fa6c6323e779d9da001b3021203c280d95c 100644 (file)
@@ -31,13 +31,32 @@ public class LogLine implements Comparable<LogLine> {
        
        private volatile String formattedMessage = null;
        
-
+       
+       /**
+        * Construct a LogLine at the current moment.  The next log line count number is selected
+        * and the current run time set to the timestamp.
+        * 
+        * @param level         the logging level
+        * @param trace         the trace exception for the log line, <code>null</code> permitted
+        * @param message       the log message
+        * @param cause         the causing throwable, <code>null</code> permitted
+        */
        public LogLine(LogLevel level, TraceException trace, String message, Throwable cause) {
                this(level, logCount.getAndIncrement(), System.currentTimeMillis() - startTime, trace, message, cause);
        }
        
-       
-       public LogLine(LogLevel level, int count, long timestamp, 
+       /**
+        * Construct a LogLine with all parameters.  This should only be used in special conditions,
+        * for example to insert a log line at a specific point within normal logs.
+        * 
+        * @param level         the logging level
+        * @param count         the log line count number
+        * @param timestamp     the log line timestamp
+        * @param trace         the trace exception for the log line, <code>null</code> permitted
+        * @param message       the log message
+        * @param cause         the causing throwable, <code>null</code> permitted
+        */
+       public LogLine(LogLevel level, int count, long timestamp,
                        TraceException trace, String message, Throwable cause) {
                this.level = level;
                this.count = count;
@@ -46,57 +65,57 @@ public class LogLine implements Comparable<LogLine> {
                this.message = message;
                this.cause = cause;
        }
-
        
        
+
        /**
         * @return the level
         */
        public LogLevel getLevel() {
                return level;
        }
-
-
+       
+       
        /**
         * @return the count
         */
        public int getLogCount() {
                return count;
        }
-
-
+       
+       
        /**
         * @return the timestamp
         */
        public long getTimestamp() {
                return timestamp;
        }
-
-
+       
+       
        /**
         * @return the trace
         */
        public TraceException getTrace() {
                return trace;
        }
-
-
+       
+       
        /**
         * @return the message
         */
        public String getMessage() {
                return message;
        }
-
-
+       
+       
        /**
         * @return the error
         */
        public Throwable getCause() {
                return cause;
        }
-
-
+       
+       
 
 
        /**
@@ -109,7 +128,7 @@ public class LogLine implements Comparable<LogLine> {
                if (formattedMessage == null) {
                        String str;
                        str = String.format("%4d %10.3f %-" + LogLevel.LENGTH + "s %s %s",
-                                       count, timestamp/1000.0, (level != null) ? level.toString() : "NULL",
+                                       count, timestamp / 1000.0, (level != null) ? level.toString() : "NULL",
                                        (trace != null) ? trace.getMessage() : "(-)",
                                        message);
                        if (cause != null) {
@@ -123,8 +142,8 @@ public class LogLine implements Comparable<LogLine> {
                }
                return formattedMessage;
        }
-
-
+       
+       
        /**
         * Compare against another log line based on the log line count number.
         */
index 752a09610d350575620cff4f1bd8c6a2735edd49..b08109a35f1645ce187fe20bba2393c1d13cb55f 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,24 @@ 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;
        }
-
-
+       
+       
+       /**
+        * Construct an exception with the specified message.
+        * 
+        * @param message       the message for the exception.
+        */
+       public TraceException(String message) {
+               this(0, 0);
+               this.message = message;
+       }
+       
+       
        /**
         * Get the description of the code position as provided in the constructor.
         */
@@ -69,13 +80,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 +98,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..a7be566
--- /dev/null
@@ -0,0 +1,25 @@
+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.
+        * @throws OptimizationException        if an error occurs that prevents the optimization
+        */
+       public double evaluate(Point point) throws InterruptedException, OptimizationException;
+       
+}
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..69c42d7
--- /dev/null
@@ -0,0 +1,39 @@
+package net.sf.openrocket.optimization.general;
+
+/**
+ * A storage of cached values of a function.  The purpose of this class is to
+ * cache function values between optimization runs.  Subinterfaces may provide
+ * additional functionality.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface FunctionCache {
+       
+       /**
+        * Compute and return the value of the function at the specified point.
+        * 
+        * @param point         the point at which to evaluate.
+        * @return                      the value of the function at that point.
+        */
+       public double getValue(Point point);
+       
+       /**
+        * Clear the cache.
+        */
+       public void clearCache();
+       
+       /**
+        * Return the function that is evaluated by this cache implementation.
+        * 
+        * @return      the function that is being evaluated.
+        */
+       public Function getFunction();
+       
+       /**
+        * Set the function that is evaluated by this cache implementation.
+        * 
+        * @param function      the function that is being evaluated.
+        */
+       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..485843f
--- /dev/null
@@ -0,0 +1,56 @@
+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.
+        * @throws OptimizationException        if an error occurs that prevents optimization
+        */
+       public void optimize(Point initial, OptimizationController control) throws OptimizationException;
+       
+       
+       /**
+        * 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/OptimizationException.java b/src/net/sf/openrocket/optimization/general/OptimizationException.java
new file mode 100644 (file)
index 0000000..7c0c067
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.optimization.general;
+
+/**
+ * An exception that prevents optimization from continuing.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class OptimizationException extends Exception {
+       
+       public OptimizationException(String message) {
+               super(message);
+       }
+       
+       public OptimizationException(Throwable cause) {
+               super(cause);
+       }
+       
+       public OptimizationException(String message, Throwable cause) {
+               super(message, cause);
+       }
+       
+}
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..7807860
--- /dev/null
@@ -0,0 +1,244 @@
+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;
+
+import net.sf.openrocket.util.BugException;
+
+/**
+ * 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.
+ * <p>
+ * Note that while this class handles threads and abstracts background execution,
+ * the public methods themselves are NOT thread-safe and should be called from
+ * only one thread at a time.
+ * 
+ * @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;
+       
+       
+       /**
+        * Construct a cache that uses the same number of computational threads as there are
+        * processors available.
+        */
+       public ParallelExecutorCache() {
+               this(Runtime.getRuntime().availableProcessors());
+       }
+       
+       /**
+        * Construct a cache that uses the specified number of computational threads for background
+        * computation.  The threads that are created are marked as daemon threads.
+        * 
+        * @param threadCount   the number of threads to use in the executor.
+        */
+       public ParallelExecutorCache(int threadCount) {
+               this(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;
+                                       }
+                               }));
+       }
+       
+       /**
+        * Construct a cache that uses the specified ExecutorService for managing
+        * computational threads.
+        * 
+        * @param executor      the executor to use for function evaluations.
+        */
+       public ParallelExecutorCache(ExecutorService executor) {
+               this.executor = executor;
+       }
+       
+       
+
+       @Override
+       public void compute(Collection<Point> points) {
+               for (Point p : points) {
+                       compute(p);
+               }
+       }
+       
+       
+       @Override
+       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;
+               }
+               
+               // Submit point for evaluation
+               FunctionCallable callable = new FunctionCallable(function, point);
+               Future<Double> future = executor.submit(callable);
+               futureMap.put(point, future);
+       }
+       
+       
+       @Override
+       public void waitFor(Collection<Point> points) throws InterruptedException, OptimizationException {
+               for (Point p : points) {
+                       waitFor(p);
+               }
+       }
+       
+       
+       @Override
+       public void waitFor(Point point) throws InterruptedException, OptimizationException {
+               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) {
+                       Throwable cause = e.getCause();
+                       if (cause instanceof InterruptedException) {
+                               throw (InterruptedException) cause;
+                       }
+                       if (cause instanceof OptimizationException) {
+                               throw (OptimizationException) cause;
+                       }
+                       if (cause instanceof RuntimeException) {
+                               throw (RuntimeException) cause;
+                       }
+                       
+                       throw new BugException("Function threw unknown exception while processing", e);
+               }
+       }
+       
+       
+
+       @Override
+       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;
+       }
+       
+       
+
+       @Override
+       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;
+               }
+       }
+       
+       
+       @Override
+       public double getValue(Point point) {
+               Double d = functionCache.get(point);
+               if (d == null) {
+                       throw new IllegalStateException(point + " 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, OptimizationException {
+                       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..c5b34a5
--- /dev/null
@@ -0,0 +1,70 @@
+package net.sf.openrocket.optimization.general;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A FunctionCache that allows scheduling 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 {
+       
+       /**
+        * Schedule a list of function evaluations at the specified points.
+        * The points are added to the end of the computation queue in the order
+        * they are returned by the iterator.
+        * 
+        * @param points        the points at which to evaluate the function.
+        */
+       public void compute(Collection<Point> points);
+       
+       /**
+        * Schedule function evaluation for the specified point.  The point is
+        * added to the end of the computation queue.
+        * 
+        * @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 {@link #getValue(Point)}.
+        * 
+        * @param points        the points to wait for.
+        * @throws InterruptedException         if this thread or the computing thread was interrupted while waiting.
+        */
+       public void waitFor(Collection<Point> points) throws InterruptedException, OptimizationException;
+       
+       /**
+        * Wait for a point to be computed.  After calling this method
+        * the function value is available by calling {@link #getValue(Point)}.
+        * 
+        * @param point         the point to wait for.
+        * @throws InterruptedException         if this thread or the computing thread was interrupted while waiting.
+        * @throws OptimizationException 
+        */
+       public void waitFor(Point point) throws InterruptedException, OptimizationException;
+       
+       
+       /**
+        * Abort the computation of the specified points.  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..0e70bf1
--- /dev/null
@@ -0,0 +1,299 @@
+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.OptimizationException;
+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) throws OptimizationException {
+               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/OptimizableParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java
new file mode 100644 (file)
index 0000000..e8506e8
--- /dev/null
@@ -0,0 +1,33 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.optimization.general.OptimizationException;
+
+/**
+ * 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 OptimizableParameter {
+       
+       /**
+        * 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.
+        * <p>
+        * This method can return NaN in case of a problem computing
+        * 
+        * @param simulation    the simulation
+        * @return                              the parameter value (any double value)
+        * @throws OptimizationException        if an error occurs preventing the optimization from continuing
+        */
+       public double computeValue(Simulation simulation) throws OptimizationException;
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameterService.java b/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameterService.java
new file mode 100644 (file)
index 0000000..b227917
--- /dev/null
@@ -0,0 +1,23 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import java.util.Collection;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+
+/**
+ * A service for generating rocket optimization parameters.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface OptimizableParameterService {
+       
+       /**
+        * Return all available rocket optimization parameters for this document.
+        * These should be new instances unless the parameter implementation is stateless.
+        * 
+        * @param document      the design document
+        * @return                      a collection of the rocket optimization parameters.
+        */
+       public Collection<OptimizableParameter> getParameters(OpenRocketDocument document);
+       
+}
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..a9fb500
--- /dev/null
@@ -0,0 +1,163 @@
+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.OptimizationException;
+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();
+       
+       private static final double OUTSIDE_DOMAIN_SCALE = 1.0e200;
+       
+       /*
+        * NOTE:  This class must be thread-safe!!!
+        */
+
+       private final Simulation baseSimulation;
+       private final OptimizableParameter parameter;
+       private final OptimizationGoal goal;
+       private final SimulationDomain domain;
+       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, OptimizableParameter parameter,
+                       OptimizationGoal goal, SimulationDomain domain, SimulationModifier... modifiers) {
+               this.baseSimulation = baseSimulation;
+               this.parameter = parameter;
+               this.goal = goal;
+               this.domain = domain;
+               this.modifiers = modifiers.clone();
+               if (modifiers.length == 0) {
+                       throw new IllegalArgumentException("No SimulationModifiers specified");
+               }
+       }
+       
+       
+       @Override
+       public double evaluate(Point point) throws InterruptedException, OptimizationException {
+               /*
+                * parameterValue is the computed parameter value (e.g. altitude)
+                * goalValue is the value that needs to be minimized
+                */
+               double goalValue, parameterValue;
+               
+               // Check for precomputed value
+               Double d = goalValueCache.get(point);
+               if (d != null && !Double.isNaN(d)) {
+                       log.verbose("Optimization function value at point " + point + " was found in cache: " + d);
+                       return d;
+               }
+               
+               log.verbose("Computing optimization function value at point " + point);
+               
+               // 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(baseSimulation);
+               for (int i = 0; i < modifiers.length; i++) {
+                       modifiers[i].modify(simulation, p[i]);
+               }
+               
+
+               // Check whether the point is within the simulation domain
+               double distance = domain.getDistanceToDomain(simulation);
+               if (distance > 0 || Double.isNaN(distance)) {
+                       if (Double.isNaN(distance)) {
+                               goalValue = Double.MAX_VALUE;
+                       } else {
+                               goalValue = (distance + 1) * OUTSIDE_DOMAIN_SCALE;
+                       }
+                       parameterValueCache.put(point, Double.NaN);
+                       goalValueCache.put(point, goalValue);
+                       log.verbose("Optimization point is outside of domain, distance=" + distance + " goal function value=" + goalValue);
+                       return goalValue;
+               }
+               
+
+               // Compute the optimization value
+               parameterValue = parameter.computeValue(simulation);
+               parameterValueCache.put(point, parameterValue);
+               
+               goalValue = goal.getMinimizationParameter(parameterValue);
+               if (Double.isNaN(goalValue)) {
+                       log.warn("Computed goal value was NaN, baseSimulation=" + baseSimulation + " parameter=" + parameter +
+                                       " goal=" + goal + " modifiers=" + Arrays.toString(modifiers) + " simulation=" + simulation +
+                                       " parameter value=" + parameterValue);
+                       goalValue = Double.MAX_VALUE;
+               }
+               goalValueCache.put(point, goalValue);
+               
+               log.verbose("Parameter value at point " + point + " is " + goalValue + ", goal function value=" + goalValue);
+               
+               return goalValue;
+       }
+       
+       
+
+
+       /**
+        * 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.
+        * <p>
+        * Note:  This method is package-private for unit testing purposes.
+        * 
+        * @return      a new deep copy of the simulation and rocket
+        */
+       Simulation newSimulationInstance(Simulation simulation) {
+               synchronized (baseSimulation) {
+                       Rocket newRocket = (Rocket) simulation.getRocket().copy();
+                       Simulation newSimulation = simulation.duplicateSimulation(newRocket);
+                       return newSimulation;
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java b/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java
new file mode 100644 (file)
index 0000000..b03be65
--- /dev/null
@@ -0,0 +1,25 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import net.sf.openrocket.document.Simulation;
+
+/**
+ * An interface defining a function domain which limits allowed function values.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface SimulationDomain {
+       
+       /**
+        * Return a value determining whether the simulation is within the domain limits
+        * of an optimization process.  If the returned value is negative or zero, the
+        * simulation is within the domain; if the value is positive, the returned value
+        * is an indication of how far from the domain the value is; if the returned value
+        * is NaN, the simulation is outside of the domain.
+        * 
+        * @param simulation    the simulation to check.
+        * @return                              a negative value or zero if the simulation is in the domain;
+        *                                              a positive value or NaN if not.
+        */
+       public double getDistanceToDomain(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..147ac0d
--- /dev/null
@@ -0,0 +1,89 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.ChangeSource;
+
+/**
+ * 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 extends ChangeSource {
+       
+       /**
+        * Return a name describing this modifier.
+        * @return      a name describing this modifier.
+        */
+       public String getName();
+       
+       
+       /**
+        * Return the object this modifier is related to.  This is for example the
+        * rocket component this modifier is modifying.  This object can be used by a
+        * UI to group related modifiers.
+        * 
+        * @return      the object this modifier is related to, or <code>null</code>.
+        */
+       public Object getRelatedObject();
+       
+       
+       /**
+        * Return the current value of the modifier in SI units.
+        * @return      the current value of this parameter in SI units.
+        */
+       public double getCurrentValue();
+       
+       
+       /**
+        * Return the minimum value (corresponding to scaled value 0) in SI units.
+        * @return      the value corresponding to scaled value 0.
+        */
+       public double getMinValue();
+       
+       /**
+        * Set the minimum value (corresponding to scaled value 0) in SI units.
+        * @param value the value corresponding to scaled value 0.
+        */
+       public void setMinValue(double value);
+       
+       
+       /**
+        * Return the maximum value (corresponding to scaled value 1) in SI units.
+        * @return      the value corresponding to scaled value 1.
+        */
+       public double getMaxValue();
+       
+       /**
+        * Set the maximum value (corresponding to scaled value 1) in SI units.
+        * @param value the value corresponding to scaled value 1.
+        */
+       public void setMaxValue(double value);
+       
+       
+       /**
+        * Return the unit group used for the values returned by {@link #getCurrentValue()} etc.
+        * @return      the unit group
+        */
+       public UnitGroup getUnitGroup();
+       
+       
+       /**
+        * Return the current scaled value.  This is normally within the range [0...1], but
+        * can be outside the range if the current value is outside of the min and max values.
+        * @return      the current value of this parameter (normally between [0 ... 1])
+        */
+       public double getCurrentScaledValue();
+       
+       
+
+       /**
+        * Modify the specified simulation to the corresponding parameter value.
+        * 
+        * @param simulation    the simulation to modify
+        * @param scaledValue   the scaled value in the range [0...1]
+        */
+       public void modify(Simulation simulation, double scaledValue);
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifierService.java b/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifierService.java
new file mode 100644 (file)
index 0000000..43b6196
--- /dev/null
@@ -0,0 +1,23 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import java.util.Collection;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+
+/**
+ * A service for generating simulation modifiers.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface SimulationModifierService {
+       
+       /**
+        * Return all available simulation modifiers for this document.
+        * 
+        * @param document      the design document
+        * @return                      a collection of the rocket optimization parameters.
+        */
+       public Collection<SimulationModifier> getModifiers(OpenRocketDocument document);
+       
+
+}
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);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java
new file mode 100644 (file)
index 0000000..ac62dc6
--- /dev/null
@@ -0,0 +1,162 @@
+package net.sf.openrocket.optimization.rocketoptimization.modifiers;
+
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.BugException;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Reflection.Method;
+
+public class GenericModifier implements SimulationModifier {
+       
+       private final String name;
+       private final Object relatedObject;
+       private final UnitGroup unitGroup;
+       private final double multiplier;
+       private final Object modifiable;
+       
+       private final Method getter;
+       private final Method setter;
+       
+       private double minValue;
+       private double maxValue;
+       
+       
+
+
+
+       public GenericModifier(String modifierName, Object relatedObject, UnitGroup unitGroup, double multiplier,
+                       Object modifiable, String methodName) {
+               this.name = modifierName;
+               this.relatedObject = relatedObject;
+               this.unitGroup = unitGroup;
+               this.multiplier = multiplier;
+               this.modifiable = modifiable;
+               
+               if (MathUtil.equals(multiplier, 0)) {
+                       throw new IllegalArgumentException("multiplier is zero");
+               }
+               
+               try {
+                       methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
+                       getter = new Method(modifiable.getClass().getMethod("get" + methodName));
+                       setter = new Method(modifiable.getClass().getMethod("set" + methodName, double.class));
+               } catch (SecurityException e) {
+                       throw new BugException("Trying to find method get/set" + methodName + " in class " + modifiable.getClass(), e);
+               } catch (NoSuchMethodException e) {
+                       throw new BugException("Trying to find method get/set" + methodName + " in class " + modifiable.getClass(), e);
+               }
+       }
+       
+       
+       @Override
+       public String getName() {
+               return name;
+       }
+       
+       @Override
+       public Object getRelatedObject() {
+               return relatedObject;
+       }
+       
+       @Override
+       public double getCurrentValue() {
+               return ((Double) getter.invoke(modifiable)) * multiplier;
+       }
+       
+       
+       @Override
+       public double getCurrentScaledValue() {
+               double value = getCurrentValue();
+               return toScaledValue(value);
+       }
+       
+       @Override
+       public void modify(Simulation simulation, double scaledValue) {
+               double siValue = toBaseValue(scaledValue) / multiplier;
+               setter.invoke(modifiable, siValue);
+       }
+       
+       
+       /**
+        * Returns the scaled value (normally within [0...1]).
+        */
+       private double toScaledValue(double value) {
+               if (MathUtil.equals(minValue, maxValue)) {
+                       if (value > maxValue)
+                               return 1.0;
+                       if (value < minValue)
+                               return 0.0;
+                       return 0.5;
+               }
+               
+               return MathUtil.map(value, minValue, maxValue, 0.0, 1.0);
+       }
+       
+       
+       /**
+        * Returns the base value (in SI units).
+        */
+       private double toBaseValue(double value) {
+               return MathUtil.map(value, 0.0, 1.0, minValue, maxValue);
+       }
+       
+       
+
+       @Override
+       public double getMinValue() {
+               return minValue;
+       }
+       
+       @Override
+       public void setMinValue(double value) {
+               if (MathUtil.equals(minValue, value))
+                       return;
+               this.minValue = value;
+               if (maxValue < minValue)
+                       maxValue = minValue;
+               fireChangeEvent();
+       }
+       
+       @Override
+       public double getMaxValue() {
+               return maxValue;
+       }
+       
+       @Override
+       public void setMaxValue(double value) {
+               if (MathUtil.equals(maxValue, value))
+                       return;
+               this.maxValue = value;
+               if (minValue > maxValue)
+                       minValue = maxValue;
+               fireChangeEvent();
+       }
+       
+       @Override
+       public UnitGroup getUnitGroup() {
+               return unitGroup;
+       }
+       
+       
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               // TODO Auto-generated method stub
+               
+       }
+       
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               // TODO Auto-generated method stub
+               
+       }
+       
+       
+       private void fireChangeEvent() {
+               // TODO Auto-generated method stub
+               
+       }
+       
+}
diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java
new file mode 100644 (file)
index 0000000..d8a2543
--- /dev/null
@@ -0,0 +1,32 @@
+package net.sf.openrocket.optimization.rocketoptimization.parameters;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.optimization.general.OptimizationException;
+import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter;
+import net.sf.openrocket.simulation.FlightDataType;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
+
+/**
+ * An optimization parameter that computes the maximum altitude of a simulated flight.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class MaximumAltitudeParameter implements OptimizableParameter {
+       
+       @Override
+       public String getName() {
+               return "Maximum altitude";
+       }
+       
+       @Override
+       public double computeValue(Simulation simulation) throws OptimizationException {
+               try {
+                       simulation.simulate(new ApogeeEndListener());
+                       return simulation.getSimulatedData().getBranch(0).getMaximum(FlightDataType.TYPE_ALTITUDE);
+               } catch (SimulationException e) {
+                       throw new OptimizationException(e);
+               }
+       }
+       
+}
index 9eb970c610ab8c2f1c11b93d27dfe54861cea4ec..97a12406f4e764d00cce6abf580b796f14d56b0e 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.
@@ -33,7 +33,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).
@@ -45,7 +45,7 @@ public abstract class BodyComponent extends ExternalComponent {
         * @return  Distance to the inner edge of the object
         */
        public abstract double getInnerRadius(double x, double theta);
-
+       
        
 
        /**
@@ -54,26 +54,13 @@ 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);
        }
        
-       
-       /**
-        * Check whether the given type can be added to this component.  BodyComponents allow any
-        * InternalComponents or ExternalComponents, excluding BodyComponents, to be added.
-        * 
-        * @param type  The RocketComponent class type to add.
-        * @return      Whether such a component can be added.
-        */
        @Override
-       public boolean isCompatible(Class<? extends RocketComponent> type) {
-               if (InternalComponent.class.isAssignableFrom(type))
-                       return true;
-               if (ExternalComponent.class.isAssignableFrom(type) &&
-                       !BodyComponent.class.isAssignableFrom(type))
-                       return true;
-               return false;
+       public boolean allowsChildren() {
+               return true;
        }
     
     /**
index fd5eb4a48cb995a79f7733a720deebe27b43a1ce..5656c72b32557bcc73a092242a6949e9180688c6 100644 (file)
@@ -16,9 +16,9 @@ import java.util.HashMap;
  */
 
 public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial {
-
-       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, Coaxial
        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.
      * 
@@ -96,18 +96,18 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
         */
     @Override
        public void setOuterRadius (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.
@@ -115,7 +115,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
        public boolean isRadiusAutomatic() {
                return autoRadius;
        }
-
+       
        /**
         * Sets whether the radius is selected automatically or not.  
         */
@@ -133,12 +133,17 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
        @Override
        public double getForeRadius() { return getOuterRadius(); }
        @Override
-       public boolean isAftRadiusAutomatic() { return isRadiusAutomatic();     }
-       @Override
-       public boolean isForeRadiusAutomatic() { return isRadiusAutomatic(); }
+       public boolean isAftRadiusAutomatic() {
+               return isRadiusAutomatic();
+       }
        
+       @Override
+       public boolean isForeRadiusAutomatic() {
+               return isRadiusAutomatic();
+       }
        
        
+
        @Override
        protected double getFrontAutoRadius() {
                if (isRadiusAutomatic()) {
@@ -152,7 +157,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                }
                return getOuterRadius();
        }
-
+       
        @Override
        protected double getRearAutoRadius() {
                if (isRadiusAutomatic()) {
@@ -166,6 +171,8 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                }
                return getOuterRadius();
        }
+       
+       
 
 
        
@@ -185,8 +192,8 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
        }
        
        
-       
-       
+
+
        /**
         * Return the component name.
         */
@@ -216,7 +223,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
        public double getRadius(double x) {
                return getOuterRadius();
        }
-
+       
        /**
         * Returns the inner radius at the position x.  If the tube is filled, returns always zero.
         */
@@ -228,13 +235,13 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                        return Math.max(getOuterRadius()-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());
        }
        
        /**
@@ -244,9 +251,9 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
        public double getComponentVolume() {
                double r = getOuterRadius();
                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);
        }
        
        
@@ -256,23 +263,23 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) +
                                MathUtil.pow2(getLength())) / 12;
        }
-
+       
        @Override
        public double getRotationalUnitInertia() {
                // 1/2 * (r1^2 + r2^2)
                return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius()))/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
@@ -284,19 +291,37 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
        public Collection<Coordinate> getComponentBounds() {
                Collection<Coordinate> bounds = new ArrayList<Coordinate>(8);
                double r = getOuterRadius();
-               addBound(bounds,0,r);
-               addBound(bounds,length,r);
+               addBound(bounds, 0, r);
+               addBound(bounds, length, r);
                return bounds;
        }
+       
+       
 
-
+       /**
+        * Check whether the given type can be added to this component.  BodyTubes allow any
+        * InternalComponents or ExternalComponents, excluding BodyComponents, to be added.
+        * 
+        * @param type  The RocketComponent class type to add.
+        * @return      Whether such a component can be added.
+        */
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               if (InternalComponent.class.isAssignableFrom(type))
+                       return true;
+               if (ExternalComponent.class.isAssignableFrom(type) &&
+                               !BodyComponent.class.isAssignableFrom(type))
+                       return true;
+               return false;
+       }
+       
        ////////////////  Motor mount  /////////////////
        
        @Override
        public boolean isMotorMount() {
                return motorMount;
        }
-
+       
        @Override
        public void setMotorMount(boolean mount) {
                if (motorMount == mount)
@@ -319,7 +344,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                
                return motors.get(id);
        }
-
+       
        @Override
        public void setMotor(String id, Motor motor) {
                if (id == null) {
@@ -334,7 +359,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                motors.put(id, motor);
                fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
        }
-
+       
        @Override
        public double getMotorDelay(String id) {
                Double delay = ejectionDelays.get(id);
@@ -342,7 +367,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                        return Motor.PLUGGED;
                return delay;
        }
-
+       
        @Override
        public void setMotorDelay(String id, double delay) {
                ejectionDelays.put(id, delay);
@@ -356,14 +381,14 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
        
        @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)
@@ -371,13 +396,13 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                ignitionEvent = event;
                fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
        }
-
+       
        
        @Override
        public double getIgnitionDelay() {
                return ignitionDelay;
        }
-
+       
        @Override
        public void setIgnitionDelay(double delay) {
                if (MathUtil.equals(delay, ignitionDelay))
@@ -385,7 +410,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                ignitionDelay = delay;
                fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
        }
-
+       
        
        @Override
        public double getMotorOverhang() {
@@ -399,8 +424,8 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                this.overhang = overhang;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
-
+       
+       
        @Override
        public Coordinate getMotorPosition(String id) {
                Motor motor = motors.get(id);
@@ -410,10 +435,10 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                
                return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang());
        }
-
        
-
        
+
+
        /*
         * (non-Javadoc)
         * Copy the motor and ejection delay HashMaps.
@@ -422,10 +447,10 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
         */
        @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 1621042c6551c61c928b759383d2513e88c20e24..3abcd44a213c0c28a648202c3f3875010b88cf99 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,7 +56,12 @@ 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 1b80e42641fb167faf391355440ec4862fbf39b7..ce6402151bc646972bd055b34235c9e63f4cc378 100644 (file)
@@ -1,5 +1,7 @@
 package net.sf.openrocket.rocketcomponent;
 
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.Coordinate;
 
@@ -8,23 +10,24 @@ import java.util.Arrays;
 
 
 public class FreeformFinSet extends FinSet {
-
+       private static final LogHelper log = Application.getLogger();
+       
        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 +40,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
@@ -50,14 +53,15 @@ public class FreeformFinSet extends FinSet {
         * @return                      the new freeform fin set.
         */
        public static FreeformFinSet convertFinSet(FinSet finset) {
+               log.info("Converting " + finset.getComponentName() + " into freeform fin set");
                final RocketComponent root = finset.getRoot();
                FreeformFinSet freeform;
                
                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 +71,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 +119,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 +140,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 +148,7 @@ public class FreeformFinSet extends FinSet {
                copy.remove(index);
                validate(copy);
                this.points = copy;
-
+               
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
@@ -155,17 +159,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 +191,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,63 +249,63 @@ 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;
        }
        
@@ -317,14 +321,14 @@ public class FreeformFinSet extends FinSet {
        
        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");
                                }
                        }
@@ -333,5 +337,5 @@ public class FreeformFinSet extends FinSet {
                        }
                }
        }
-
+       
 }
index 0a0b6327fdfd9f7b86238ca2304955c77ceeb8be..036daef64add96280cf60ece88e1ac3bda41b2dd 100644 (file)
@@ -1,6 +1,7 @@
 package net.sf.openrocket.rocketcomponent;
 
 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 java.util.List;
  * 
  * @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,7 +314,6 @@ implements Clusterable, RadialParent, MotorMount {
                
                return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang());
        }
-
        
     /**
      * Accept a visitor to an InnerTube object in the component hierarchy.
@@ -317,7 +325,8 @@ implements Clusterable, RadialParent, MotorMount {
         theVisitor.visit(this);
     }
        
-       
+
+
        /*
         * (non-Javadoc)
         * Copy the motor and ejection delay HashMaps.
@@ -326,11 +335,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 2670bc6063741d925b82dbd47926bb3b4df69d4d..500e9063bd34b31675b3ba88197b29e7dd40ae5f 100644 (file)
@@ -7,6 +7,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 
 
+
 public class LaunchLug extends ExternalComponent implements Coaxial {
 
        private double radius;
@@ -18,10 +19,10 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
        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 +31,7 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
        public double getOuterRadius () {
                return radius;
        }
-
+       
        public void setOuterRadius (double radius) {
                if (MathUtil.equals(this.radius, radius))
                        return;
@@ -38,15 +39,15 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
                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) {
                setOuterRadius(innerRadius + thickness);
        }
-
+       
        public double getThickness() {
                return thickness;
        }
@@ -57,12 +58,12 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
                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 +71,9 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
                this.radialDirection = direction;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
-
        
+       
+
        public void setLength(double length) {
                if (MathUtil.equals(this.length, length))
                        return;
@@ -81,7 +82,6 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
        }
        
        
-       
 
 
 
@@ -90,21 +90,21 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
                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 +122,38 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
                 */
                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,37 +161,41 @@ public class LaunchLug extends ExternalComponent implements Coaxial {
                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(getOuterRadius()) +
                                MathUtil.pow2(getLength())) / 12;
        }
-
+       
        @Override
        public double getRotationalUnitInertia() {
                // 1/2 * (r1^2 + r2^2)
                return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius()))/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;
        }
-
+       
     /**
      * Accept a visitor to this LaunchLug in the component hierarchy.
      * 
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 a0c923ed2592219aaa14c0b4264cb4bf63db0b05..509c9ff545e98e869b6f1f7601dd771d280a53a7 100644 (file)
@@ -1,9 +1,13 @@
 package net.sf.openrocket.rocketcomponent;
 
+import net.sf.openrocket.gui.main.ExceptionHandler;
+import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.Chars;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.UniqueID;
 
 import javax.swing.event.ChangeListener;
 import javax.swing.event.EventListenerList;
@@ -28,17 +32,10 @@ import java.util.UUID;
  */
 
 public class Rocket extends RocketComponent {
+       private static final LogHelper log = Application.getLogger();
+       
        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.
@@ -51,26 +48,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>();
@@ -78,17 +75,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;
@@ -97,8 +94,9 @@ public class Rocket extends RocketComponent {
        }
        
        
-       
+
        public String getDesigner() {
+               checkState();
                return designer;
        }
        
@@ -109,8 +107,9 @@ public class Rocket extends RocketComponent {
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
        
-
+       
        public String getRevision() {
+               checkState();
                return revision;
        }
        
@@ -122,7 +121,7 @@ public class Rocket extends RocketComponent {
        }
        
        
-       
+
 
        /**
         * Return the number of stages in this rocket.
@@ -130,6 +129,7 @@ public class Rocket extends RocketComponent {
         * @return   the number of stages in this rocket.
         */
        public int getStageCount() {
+               checkState();
                return this.getChildCount();
        }
        
@@ -197,9 +197,10 @@ public class Rocket extends RocketComponent {
        }
        
        
-       
-       
+
+
        public ReferenceType getReferenceType() {
+               checkState();
                return refType;
        }
        
@@ -212,6 +213,7 @@ public class Rocket extends RocketComponent {
        
        
        public double getCustomReferenceLength() {
+               checkState();
                return customReferenceLength;
        }
        
@@ -219,7 +221,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);
@@ -227,9 +229,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.
@@ -242,8 +244,8 @@ public class Rocket extends RocketComponent {
                this.perfectFinish = perfectFinish;
                fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
        }
-
-
+       
+       
 
        /**
         * Get whether the rocket has a perfect finish.
@@ -253,33 +255,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;
@@ -316,8 +307,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();
@@ -326,10 +317,10 @@ public class Rocket extends RocketComponent {
                
                fireComponentChangeEvent(type);
        }
-
-       
        
        
+
+
        ///////  Implement the ComponentChangeListener lists
        
        /**
@@ -337,56 +328,57 @@ 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);
-               if (DEBUG_LISTENERS)
-                       System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
-                                       " listeners): "+l);
+               checkState();
+               listenerList.add(ComponentChangeListener.class, l);
+               log.verbose("Added ComponentChangeListener " + l + ", current number of listeners is " +
+                               listenerList.getListenerCount());
        }
+       
        @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);
+               log.verbose("Removed ComponentChangeListener " + l + ", current number of listeners is " +
+                               listenerList.getListenerCount());
        }
        
-
+       
        @Override
        public void addChangeListener(ChangeListener l) {
-               listenerList.add(ChangeListener.class,l);
-               if (DEBUG_LISTENERS)
-                       System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
-                                       " listeners): "+l);
+               checkState();
+               listenerList.add(ChangeListener.class, l);
+               log.verbose("Added ChangeListener " + l + ", current number of listeners is " +
+                               listenerList.getListenerCount());
        }
+       
        @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);
+               log.verbose("Removed ChangeListener " + l + ", current number of listeners is " +
+                               listenerList.getListenerCount());
        }
-
+       
        
        @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())
@@ -397,33 +389,33 @@ public class Rocket extends RocketComponent {
                                functionalModID = modID;
                }
                
-               if (DEBUG_LISTENERS)
-                       System.out.println("FIRING "+e);
-               
                // Check whether frozen
                if (freezeList != null) {
+                       log.debug("Rocket is in frozen state, adding event " + e + " info freeze list");
                        freezeList.add(e);
                        return;
                }
                
+               log.debug("Firing rocket change event " + e);
+               
                // Notify all components first
                Iterator<RocketComponent> iterator = this.deepIterator(true);
                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.
@@ -443,8 +435,14 @@ public class Rocket extends RocketComponent {
         * @see #thaw()
         */
        public void freeze() {
-               if (freezeList == null)
+               checkState();
+               if (freezeList == null) {
                        freezeList = new LinkedList<ComponentChangeEvent>();
+                       log.debug("Freezing Rocket");
+               } else {
+                       ExceptionHandler.handleErrorCondition("Attempting to freeze Rocket when it is already frozen, " +
+                                       "freezeList=" + freezeList);
+               }
        }
        
        /**
@@ -455,30 +453,36 @@ public class Rocket extends RocketComponent {
         * @see #freeze()
         */
        public void thaw() {
-               if (freezeList == null)
+               checkState();
+               if (freezeList == null) {
+                       ExceptionHandler.handleErrorCondition("Attempting to thaw Rocket when it is not frozen");
                        return;
-               if (freezeList.size()==0) {
+               }
+               if (freezeList.size() == 0) {
+                       log.warn("Thawing rocket with no changes made");
                        freezeList = null;
                        return;
                }
                
+               log.debug("Thawing rocket, freezeList=" + freezeList);
+               
                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
@@ -487,6 +491,7 @@ public class Rocket extends RocketComponent {
         * @return   the default {@link Configuration}.
         */
        public Configuration getDefaultConfiguration() {
+               checkState();
                return defaultConfiguration;
        }
        
@@ -498,6 +503,7 @@ public class Rocket extends RocketComponent {
         * @return  an array of the motor configuration IDs.
         */
        public String[] getMotorConfigurationIDs() {
+               checkState();
                return motorConfigurationIDs.toArray(new String[0]);
        }
        
@@ -508,6 +514,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);
@@ -521,13 +528,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.
@@ -535,12 +543,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.
@@ -549,11 +558,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.
         * 
@@ -561,13 +571,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())
@@ -589,6 +600,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);
@@ -606,7 +618,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);
        }
        
@@ -618,12 +631,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);
        }
        
@@ -636,6 +650,7 @@ public class Rocket extends RocketComponent {
         */
        @SuppressWarnings("null")
        public String getMotorConfigurationDescription(String id) {
+               checkState();
                String name;
                int motorCount = 0;
                
@@ -662,7 +677,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++;
                                        }
@@ -678,13 +693,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++;
@@ -728,11 +743,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;
@@ -741,31 +756,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;
@@ -775,17 +790,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 71d2c1390821d122ab9c966b30b572313e025e2c..02d4c1d02b2084df927434dfdf21c9a664b9f2a7 100644 (file)
@@ -1,10 +1,14 @@
 package net.sf.openrocket.rocketcomponent;
 
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.logging.TraceException;
+import net.sf.openrocket.startup.Application;
 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;
 
 import javax.swing.event.ChangeListener;
 import java.awt.Color;
@@ -16,12 +20,12 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Stack;
-import java.util.UUID;
 
 
-public abstract class RocketComponent implements ChangeSource, Cloneable, 
+public abstract class RocketComponent implements ChangeSource, Cloneable,
                Iterable<RocketComponent> , Visitable<ComponentVisitor, RocketComponent> {
-
+       private static final LogHelper log = Application.getLogger();
+       
        /*
         * Text is suitable to the form
         *    Position relative to:  <title>
@@ -39,6 +43,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                ABSOLUTE("Tip of the nose cone");
                
                private String title;
+               
                Position(String title) {
                        this.title = title;
                }
@@ -59,8 +64,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:
        
        /**
@@ -70,7 +75,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.
         */
@@ -82,12 +87,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;
@@ -102,14 +107,20 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        
        // User-specified comment
        private String comment = "";
-
+       
        // Unique ID of the component
        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.
@@ -118,7 +129,7 @@ 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  ////////////
@@ -127,19 +138,19 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        /**
         * 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
@@ -162,7 +173,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
@@ -190,7 +208,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        }
        
        
-       
+
        /**
         * Return a collection of bounding coordinates.  The coordinates must be such that
         * the component is fully enclosed in their convex hull.
@@ -198,24 +216,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.
@@ -229,6 +247,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;
        }
        
@@ -243,11 +262,12 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         */
        protected void componentChanged(ComponentChangeEvent e) {
                // No-op
+               checkState();
        }
        
-
-       
        
+
+
        /**
         * Return a descriptive name of the component.
         * 
@@ -263,66 +283,89 @@ 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;
        }
-
-
+       
+       
     /**
      * Accept a visitor to this RocketComponent in the component hierarchy.
      * 
@@ -335,7 +378,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
 
        //////////////  Methods that may not be overridden  ////////////
        
-       
+
 
        ////////// Common parameter setting/getting //////////
        
@@ -355,6 +398,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                                (color != null && color.equals(c)))
                        return;
                
+               checkState();
                this.color = c;
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
@@ -367,13 +411,14 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        public final void setLineStyle(LineStyle style) {
                if (this.lineStyle == style)
                        return;
+               checkState();
                this.lineStyle = style;
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
-
-       
        
        
+
+
        /**
         * Get the current override mass.  The mass is not necessarily in use
         * at the moment.
@@ -391,7 +436,10 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param m  the override mass
         */
        public final void setOverrideMass(double m) {
-               overrideMass = Math.max(m,0);
+               if (MathUtil.equals(m, overrideMass))
+                       return;
+               checkState();
+               overrideMass = Math.max(m, 0);
                if (massOverriden)
                        fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
@@ -412,16 +460,18 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param o  whether the mass is overridden
         */
        public final void setMassOverridden(boolean o) {
-               if (massOverriden != o) {
-                       massOverriden = o;
-                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               if (massOverriden == o) {
+                       return;
                }
+               checkState();
+               massOverriden = o;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
-       
-       
        
        
+
+
+
        /**
         * Return the current override CG.  The CG is not necessarily overridden.
         * 
@@ -430,7 +480,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        public final Coordinate getOverrideCG() {
                return getComponentCG().setX(overrideCGX);
        }
-
+       
        /**
         * Return the x-coordinate of the current override CG.
         * 
@@ -448,6 +498,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        public final void setOverrideCGX(double x) {
                if (MathUtil.equals(overrideCGX, x))
                        return;
+               checkState();
                this.overrideCGX = x;
                if (isCGOverridden())
                        fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
@@ -470,14 +521,16 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param o  whether the CG is overridden
         */
        public final void setCGOverridden(boolean o) {
-               if (cgOverriden != o) {
-                       cgOverriden = o;
-                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               if (cgOverriden == o) {
+                       return;
                }
+               checkState();
+               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,
@@ -500,10 +553,12 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
         * @param override      whether the mass and/or CG override overrides all subcomponent.
         */
        public void setOverrideSubcomponents(boolean override) {
-               if (overrideSubcomponents != override) {
-                       overrideSubcomponents = override;
-                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               if (overrideSubcomponents == override) {
+                       return;
                }
+               checkState();
+               overrideSubcomponents = override;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
        
        /**
@@ -521,8 +576,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        }
        
        
-       
-       
+
+
        /**
         * Get the user-defined name of the component.
         */
@@ -535,8 +590,11 @@ 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*$"))
+               if (this.name.equals(name)) {
+                       return;
+               }
+               checkState();
+               if (name == null || name.matches("^\\s*$"))
                        this.name = getComponentName();
                else
                        this.name = name;
@@ -562,6 +620,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        public final void setComment(String comment) {
                if (this.comment.equals(comment))
                        return;
+               checkState();
                if (comment == null)
                        this.comment = "";
                else
@@ -569,8 +628,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
        }
        
-
        
+
        /**
         * Returns the unique ID of the component.
         * 
@@ -580,28 +639,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
@@ -613,7 +660,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        public final double getLength() {
                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,
@@ -638,40 +685,41 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        protected void setRelativePosition(RocketComponent.Position position) {
                if (this.relativePosition == position)
                        return;
+               checkState();
                
                // 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.
@@ -682,7 +730,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                return position;
        }
        
-
+       
        /**
         * Set the position value of the component.  The exact meaning of the value
         * depends on the current relative positioning.
@@ -697,21 +745,23 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
        public void setPositionValue(double value) {
                if (MathUtil.equals(this.position, value))
                        return;
+               checkState();
                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
@@ -729,6 +779,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];
@@ -736,72 +787,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]);
                        }
                }
@@ -817,18 +868,19 @@ 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.
         * 
@@ -849,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
@@ -867,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();
        }
        
@@ -878,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  ///////////
        
 
@@ -897,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 
@@ -914,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);
@@ -937,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);
@@ -946,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.
         * 
@@ -967,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);
@@ -994,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]);
        }
        
@@ -1014,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);
        }
        
@@ -1024,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;
        }
        
@@ -1033,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;
@@ -1047,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());
        }
        
        
@@ -1063,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.");
@@ -1079,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");
                }
@@ -1100,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);
@@ -1122,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);
                
@@ -1151,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;
                }
@@ -1175,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);
        }
        
@@ -1192,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 
@@ -1203,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);
        }
        
@@ -1233,13 +1309,15 @@ 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. */
+                       log.debug("Attempted firing event " + e + " with root " + this.getComponentName() + ", ignoring event");
                        return;
                }
                getRoot().fireComponentChangeEvent(e);
        }
-
+       
        
        /**
         * Fires a ComponentChangeEvent of the given type.  The source of the event is set to
@@ -1249,10 +1327,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 (with the exception of methods simply getting a property)
+        * should call this method before changing or computing anything.
+        * 
+        * @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  //////////
        
@@ -1268,16 +1361,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;
@@ -1293,20 +1386,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;
                        }
                        
@@ -1330,7 +1425,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                        
                        return c;
                }
-
+               
                private void checkID() {
                        if (root != null) {
                                if (root.getTreeModID() != treeModID) {
@@ -1344,7 +1439,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable,
                                        "RocketComponent iterator");
                }
        }
-
+       
        /**
         * Returns an iterator that iterates over all children and sub-children.
         * 
@@ -1360,7 +1455,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);
        }
        
        /**
@@ -1373,7 +1469,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);
        }
        
        
@@ -1384,54 +1481,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
@@ -1439,14 +1567,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);
                }
                
@@ -1454,10 +1584,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;
@@ -1472,6 +1602,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 ed2fc7a9129d5f20b69ffcf8b90fc12b7148a5e8..b16cd4b660e96a39c42d99533f66061cea79080f 100644 (file)
@@ -1,15 +1,21 @@
 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 only BodyComponents to be added.
+        * Check whether the given type can be added to this component.  A Stage allows 
+        * only BodyComponents to be added.
      *
      * @param type The RocketComponent class type to add.
      *
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 f26666ae9079415722dedf1420f9419bd2b2fde6..81140a66dc60b208c821d433d2adcaaf279b233c 100644 (file)
@@ -16,15 +16,15 @@ import static net.sf.openrocket.util.MathUtil.pow3;
 public class Transition extends SymmetricComponent {
        private static final double CLIP_PRECISION = 0.0001;
        
-       
+
        private Shape type;
        private double shapeParameter;
-       private boolean clipped;    // Not to be read - use isClipped(), which may be overriden
+       private boolean clipped; // Not to be read - use isClipped(), which may be overriden
        
        private double radius1, radius2;
-       private boolean autoRadius1, autoRadius2;   // Whether the start radius is automatic
-       
-       
+       private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic
+                       
+
        private double foreShoulderRadius;
        private double foreShoulderThickness;
        private double foreShoulderLength;
@@ -34,9 +34,9 @@ public class Transition extends SymmetricComponent {
        private double aftShoulderLength;
        private boolean aftShoulderCapped;
        
-       
+
        // Used to cache the clip length
-       private double clipLength=-1;
+       private double clipLength = -1;
        
        public Transition() {
                super();
@@ -53,11 +53,11 @@ public class Transition extends SymmetricComponent {
        }
        
        
-       
-       
+
+
        ////////  Fore radius  ////////
        
-       
+
        @Override
        public double getForeRadius() {
                if (isForeRadiusAutomatic()) {
@@ -79,8 +79,8 @@ public class Transition extends SymmetricComponent {
                        return;
                
                this.autoRadius1 = false;
-               this.radius1 = Math.max(radius,0);
-
+               this.radius1 = Math.max(radius, 0);
+               
                if (this.thickness > this.radius1 && this.thickness > this.radius2)
                        this.thickness = Math.max(this.radius1, this.radius2);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
@@ -99,7 +99,7 @@ public class Transition extends SymmetricComponent {
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
        
-
+       
        ////////  Aft radius  /////////
        
        @Override
@@ -119,19 +119,19 @@ public class Transition extends SymmetricComponent {
        }
        
        
-       
+
        public void setAftRadius(double radius) {
                if ((this.radius2 == radius) && (autoRadius2 == false))
                        return;
                
                this.autoRadius2 = false;
-               this.radius2 = Math.max(radius,0);
-
+               this.radius2 = Math.max(radius, 0);
+               
                if (this.thickness > this.radius1 && this.thickness > this.radius2)
                        this.thickness = Math.max(this.radius1, this.radius2);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        @Override
        public boolean isAftRadiusAutomatic() {
                return autoRadius2;
@@ -146,7 +146,7 @@ public class Transition extends SymmetricComponent {
        }
        
        
-       
+
        //// Radius automatics
        
        @Override
@@ -155,18 +155,18 @@ public class Transition extends SymmetricComponent {
                        return -1;
                return getAftRadius();
        }
-
-
+       
+       
        @Override
        protected double getRearAutoRadius() {
                if (isForeRadiusAutomatic())
                        return -1;
                return getForeRadius();
        }
-
-
        
        
+
+
        ////////  Type & shape  /////////
        
        public Shape getType() {
@@ -175,7 +175,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;
@@ -195,7 +195,7 @@ public class Transition extends SymmetricComponent {
                this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        public boolean isClipped() {
                if (!type.isClippable())
                        return false;
@@ -212,7 +212,7 @@ public class Transition extends SymmetricComponent {
        public boolean isClippedEnabled() {
                return type.isClippable();
        }
-
+       
        public double getShapeParameterMin() {
                return type.minParameter();
        }
@@ -227,29 +227,29 @@ public class Transition extends SymmetricComponent {
        public double getForeShoulderRadius() {
                return foreShoulderRadius;
        }
-
+       
        public void setForeShoulderRadius(double foreShoulderRadius) {
                if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
                        return;
                this.foreShoulderRadius = foreShoulderRadius;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
+       
        public double getForeShoulderThickness() {
                return foreShoulderThickness;
        }
-
+       
        public void setForeShoulderThickness(double foreShoulderThickness) {
                if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
                        return;
                this.foreShoulderThickness = foreShoulderThickness;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
+       
        public double getForeShoulderLength() {
                return foreShoulderLength;
        }
-
+       
        public void setForeShoulderLength(double foreShoulderLength) {
                if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
                        return;
@@ -267,36 +267,36 @@ public class Transition extends SymmetricComponent {
                this.foreShoulderCapped = capped;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
-
        
        
+
+
        public double getAftShoulderRadius() {
                return aftShoulderRadius;
        }
-
+       
        public void setAftShoulderRadius(double aftShoulderRadius) {
                if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
                        return;
                this.aftShoulderRadius = aftShoulderRadius;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
+       
        public double getAftShoulderThickness() {
                return aftShoulderThickness;
        }
-
+       
        public void setAftShoulderThickness(double aftShoulderThickness) {
                if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
                        return;
                this.aftShoulderThickness = aftShoulderThickness;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
+       
        public double getAftShoulderLength() {
                return aftShoulderLength;
        }
-
+       
        public void setAftShoulderLength(double aftShoulderLength) {
                if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
                        return;
@@ -314,10 +314,10 @@ public class Transition extends SymmetricComponent {
                this.aftShoulderCapped = capped;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
        }
-
-
        
        
+
+
        ///////////   Shape implementations   ////////////
        
 
@@ -327,17 +327,17 @@ public class Transition extends SymmetricComponent {
         */
        @Override
        public double getRadius(double x) {
-               if (x<0 || x>length)
+               if (x < 0 || x > length)
                        return 0;
                
-               double r1=getForeRadius();
-               double r2=getAftRadius();
-
+               double r1 = getForeRadius();
+               double r2 = getAftRadius();
+               
                if (r1 == r2)
                        return r1;
                
                if (r1 > r2) {
-                       x = length-x;
+                       x = length - x;
                        double tmp = r1;
                        r1 = r2;
                        r2 = tmp;
@@ -346,29 +346,29 @@ public class Transition extends SymmetricComponent {
                if (isClipped()) {
                        // Check clip calculation
                        if (clipLength < 0)
-                               calculateClip(r1,r2);
-                       return type.getRadius(clipLength+x, r2, clipLength+length, shapeParameter);
+                               calculateClip(r1, r2);
+                       return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter);
                } else {
                        // Not clipped
-                       return r1 + type.getRadius(x, r2-r1, length, shapeParameter);
+                       return r1 + type.getRadius(x, r2 - r1, length, shapeParameter);
                }
        }
-
+       
        /**
         * Numerically solve clipLength from the equation
         *     r1 == type.getRadius(clipLength,r2,clipLength+length)
         * using a binary search.  It assumes getOuterRadius() to be monotonically increasing.
         */
        private void calculateClip(double r1, double r2) {
-               double min=0, max=length;
+               double min = 0, max = length;
                
                if (r1 >= r2) {
-                       double tmp=r1;
+                       double tmp = r1;
                        r1 = r2;
                        r2 = tmp;
                }
                
-               if (r1==0) {
+               if (r1 == 0) {
                        clipLength = 0;
                        return;
                }
@@ -381,22 +381,22 @@ public class Transition extends SymmetricComponent {
                // Required:
                //    getR(min,min+length,r2) - r1 < 0
                //    getR(max,max+length,r2) - r1 > 0
-
-               int n=0;
-               while (type.getRadius(max, r2, max+length, shapeParameter) - r1 < 0) {
+               
+               int n = 0;
+               while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) {
                        min = max;
                        max *= 2;
                        n++;
-                       if (n>10)
+                       if (n > 10)
                                break;
                }
-
+               
                while (true) {
-                       clipLength = (min+max)/2;
-                       if ((max-min)<CLIP_PRECISION)
+                       clipLength = (min + max) / 2;
+                       if ((max - min) < CLIP_PRECISION)
                                return;
-                       double val = type.getRadius(clipLength, r2, clipLength+length, shapeParameter);
-                       if (val-r1 > 0) {
+                       double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter);
+                       if (val - r1 > 0) {
                                max = clipLength;
                        } else {
                                min = clipLength;
@@ -407,9 +407,9 @@ public class Transition extends SymmetricComponent {
        
        @Override
        public double getInnerRadius(double x) {
-               return Math.max(getRadius(x)-thickness,0);
+               return Math.max(getRadius(x) - thickness, 0);
        }
-               
+       
        
 
        @Override
@@ -447,7 +447,7 @@ public class Transition extends SymmetricComponent {
                
                return mass;
        }
-
+       
        @Override
        public Coordinate getComponentCG() {
                Coordinate cg = super.getComponentCG();
@@ -458,25 +458,25 @@ public class Transition extends SymmetricComponent {
                }
                if (isForeShoulderCapped()) {
                        final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
-                       cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(), 
-                                       getForeShoulderThickness()-getForeShoulderLength(),
+                       cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
+                                       getForeShoulderThickness() - getForeShoulderLength(),
                                        getMaterial().getDensity()));
                }
                
                if (getAftShoulderLength() > 0.001) {
                        final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
-                       cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(), 
-                                       getLength()+getAftShoulderLength(), getMaterial().getDensity()));
+                       cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
+                                       getLength() + getAftShoulderLength(), getMaterial().getDensity()));
                }
                if (isAftShoulderCapped()) {
                        final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
-                       cg = cg.average(ringCG(ir, 0, 
-                                       getLength()+getAftShoulderLength()-getAftShoulderThickness(), 
-                                       getLength()+getAftShoulderLength(), getMaterial().getDensity()));
+                       cg = cg.average(ringCG(ir, 0,
+                                       getLength() + getAftShoulderLength() - getAftShoulderThickness(),
+                                       getLength() + getAftShoulderLength(), getMaterial().getDensity()));
                }
                return cg;
        }
-
+       
        
        /*
         * The moments of inertia are not explicitly corrected for the shoulders.
@@ -510,14 +510,29 @@ public class Transition extends SymmetricComponent {
         theVisitor.visit(this);
     }
        
+       /**
+        * Check whether the given type can be added to this component.  Transitions allow any
+        * InternalComponents to be added.
+        * 
+        * @param type  The RocketComponent class type to add.
+        * @return      Whether such a component can be added.
+        */
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               if (InternalComponent.class.isAssignableFrom(type))
+                       return true;
+               return false;
+       }
+       
        
+
        /**
         * An enumeration listing the possible shapes of transitions.
         * 
         * @author Sampo Niskanen <sampo.niskanen@iki.fi>
         */
        public static enum Shape {
-
+               
                /**
                 * Conical shape.
                 */
@@ -529,10 +544,10 @@ public class Transition extends SymmetricComponent {
                                assert x >= 0;
                                assert x <= length;
                                assert radius >= 0;
-                               return radius*x/length;
+                               return radius * x / length;
                        }
                },
-
+               
                /**
                 * Ogive shape.  The shape parameter is the portion of an extended tangent ogive
                 * that will be used.  That is, for param==1 a tangent ogive will be produced, and
@@ -540,21 +555,23 @@ public class Transition extends SymmetricComponent {
                 */
                OGIVE("Ogive",
                                "An ogive nose cone has a profile that is a segment of a circle.  " +
-                               "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
-                               "a smooth transition to the body tube, values less than 1 produce "+
-                               "<b>secant ogives</b>.",
+                                               "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
+                                               "a smooth transition to the body tube, values less than 1 produce " +
+                                               "<b>secant ogives</b>.",
                                "An ogive transition has a profile that is a segment of a circle.  " +
-                               "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
-                               "a smooth transition to the body tube at the aft end, values less than 1 " +
-                               "produce <b>secant ogives</b>.") {
+                                               "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
+                                               "a smooth transition to the body tube at the aft end, values less than 1 " +
+                                               "produce <b>secant ogives</b>.") {
                        @Override
                        public boolean usesParameter() {
-                               return true;   // Range 0...1 is default
+                               return true; // Range 0...1 is default
                        }
+                       
                        @Override
                        public double defaultParameter() {
-                               return 1.0;    // Tangent ogive by default
+                               return 1.0; // Tangent ogive by default
                        }
+                       
                        @Override
                        public double getRadius(double x, double radius, double length, double param) {
                                assert x >= 0;
@@ -574,56 +591,58 @@ public class Transition extends SymmetricComponent {
                                        return CONICAL.getRadius(x, radius, length, param);
                                
                                // Radius of circle is:
-                               double R = sqrt((pow2(length)+pow2(radius)) *
-                                               (pow2((2-param)*length) + pow2(param*radius))/(4*pow2(param*radius)));
-                               double L = length/param;
-//                             double R = (radius + length*length/(radius*param*param))/2;
-                               double y0 = sqrt(R*R - L*L);
-                               return sqrt(R*R - (L-x)*(L-x)) - y0;
+                               double R = sqrt((pow2(length) + pow2(radius)) *
+                                               (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius)));
+                               double L = length / param;
+                               //                              double R = (radius + length*length/(radius*param*param))/2;
+                               double y0 = sqrt(R * R - L * L);
+                               return sqrt(R * R - (L - x) * (L - x)) - y0;
                        }
                },
-
+               
                /**
                 * Ellipsoidal shape.
                 */
                ELLIPSOID("Ellipsoid",
-                               "An ellipsoidal nose cone has a profile of a half-ellipse "+
-                               "with major axes of lengths 2&times;<i>Length</i> and <i>Diameter</i>.",
-                               "An ellipsoidal transition has a profile of a half-ellipse "+
-                               "with major axes of lengths 2&times;<i>Length</i> and <i>Diameter</i>.  If the "+
-                               "transition is not clipped, then the profile is extended at the center by the "+
-                               "corresponding radius.",true) {
+                               "An ellipsoidal nose cone has a profile of a half-ellipse " +
+                                               "with major axes of lengths 2&times;<i>Length</i> and <i>Diameter</i>.",
+                               "An ellipsoidal transition has a profile of a half-ellipse " +
+                                               "with major axes of lengths 2&times;<i>Length</i> and <i>Diameter</i>.  If the " +
+                                               "transition is not clipped, then the profile is extended at the center by the " +
+                                               "corresponding radius.", true) {
                        @Override
                        public double getRadius(double x, double radius, double length, double param) {
                                assert x >= 0;
                                assert x <= length;
                                assert radius >= 0;
-                               x = x*radius/length;
-                               return sqrt(2*radius*x-x*x);  // radius/length * sphere
+                               x = x * radius / length;
+                               return sqrt(2 * radius * x - x * x); // radius/length * sphere
                        }
                },
-
+               
                POWER("Power series",
-                               "A power series nose cone has a profile of "+
-                               "<i>Radius</i>&nbsp;&times;&nbsp;(<i>x</i>&nbsp;/&nbsp;<i>Length</i>)" +
-                               "<sup><i>k</i></sup> "+
-                               "where <i>k</i> is the shape parameter.  For <i>k</i>=0.5 this is a "+
-                               "<b>" + FRAC12 +"-power</b> or <b>parabolic</b> nose cone, for <i>k</i>=0.75 a "+
-                               "<b>" + FRAC34 +"-power</b>, and for <i>k</i>=1 a <b>conical</b> nose cone.",
-                               "A power series transition has a profile of "+
-                               "<i>Radius</i>&nbsp;&times;&nbsp;(<i>x</i>&nbsp;/&nbsp;<i>Length</i>)" +
-                               "<sup><i>k</i></sup> "+
-                               "where <i>k</i> is the shape parameter.  For <i>k</i>=0.5 the transition is "+
-                               "<b>" + FRAC12 + "-power</b> or <b>parabolic</b>, for <i>k</i>=0.75 a " +
-                               "<b>" + FRAC34 + "-power</b>, and for <i>k</i>=1 <b>conical</b>.",true) {
+                               "A power series nose cone has a profile of " +
+                                               "<i>Radius</i>&nbsp;&times;&nbsp;(<i>x</i>&nbsp;/&nbsp;<i>Length</i>)" +
+                                               "<sup><i>k</i></sup> " +
+                                               "where <i>k</i> is the shape parameter.  For <i>k</i>=0.5 this is a " +
+                                               "<b>" + FRAC12 + "-power</b> or <b>parabolic</b> nose cone, for <i>k</i>=0.75 a " +
+                                               "<b>" + FRAC34 + "-power</b>, and for <i>k</i>=1 a <b>conical</b> nose cone.",
+                               "A power series transition has a profile of " +
+                                               "<i>Radius</i>&nbsp;&times;&nbsp;(<i>x</i>&nbsp;/&nbsp;<i>Length</i>)" +
+                                               "<sup><i>k</i></sup> " +
+                                               "where <i>k</i> is the shape parameter.  For <i>k</i>=0.5 the transition is " +
+                                               "<b>" + FRAC12 + "-power</b> or <b>parabolic</b>, for <i>k</i>=0.75 a " +
+                                               "<b>" + FRAC34 + "-power</b>, and for <i>k</i>=1 <b>conical</b>.", true) {
                        @Override
-                       public boolean usesParameter() {  // Range 0...1
+                       public boolean usesParameter() { // Range 0...1
                                return true;
                        }
+                       
                        @Override
                        public double defaultParameter() {
                                return 0.5;
                        }
+                       
                        @Override
                        public double getRadius(double x, double radius, double length, double param) {
                                assert x >= 0;
@@ -631,40 +650,42 @@ public class Transition extends SymmetricComponent {
                                assert radius >= 0;
                                assert param >= 0;
                                assert param <= 1;
-                               if (param<=0.00001) {
-                                       if (x<=0.00001)
+                               if (param <= 0.00001) {
+                                       if (x <= 0.00001)
                                                return 0;
                                        else
                                                return radius;
                                }
-                               return radius*Math.pow(x/length, param);
+                               return radius * Math.pow(x / length, param);
                        }
                        
                },
                
                PARABOLIC("Parabolic series",
-                               "A parabolic series nose cone has a profile of a parabola.  The shape "+
-                               "parameter defines the segment of the parabola to utilize.  The shape " +
-                               "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
-                               "tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
-                               "<b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone.",
-                               "A parabolic series transition has a profile of a parabola.  The shape "+
-                               "parameter defines the segment of the parabola to utilize.  The shape " +
-                               "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
-                               "tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
-                               "<b>1/2 parabola</b> and 0 produces a <b>conical</b> transition.") {
+                               "A parabolic series nose cone has a profile of a parabola.  The shape " +
+                                               "parameter defines the segment of the parabola to utilize.  The shape " +
+                                               "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
+                                               "tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
+                                               "<b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone.",
+                               "A parabolic series transition has a profile of a parabola.  The shape " +
+                                               "parameter defines the segment of the parabola to utilize.  The shape " +
+                                               "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
+                                               "tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
+                                               "<b>1/2 parabola</b> and 0 produces a <b>conical</b> transition.") {
                        
                        // In principle a parabolic transition is clippable, but the difference is
                        // negligible.
                        
                        @Override
-                       public boolean usesParameter() {  // Range 0...1
+                       public boolean usesParameter() { // Range 0...1
                                return true;
                        }
+                       
                        @Override
                        public double defaultParameter() {
                                return 1.0;
                        }
+                       
                        @Override
                        public double getRadius(double x, double radius, double length, double param) {
                                assert x >= 0;
@@ -672,31 +693,33 @@ public class Transition extends SymmetricComponent {
                                assert radius >= 0;
                                assert param >= 0;
                                assert param <= 1;
-
-                               return radius * ((2*x/length - param*pow2(x/length))/(2-param));
+                               
+                               return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param));
                        }
                },
                
-               
-               
+
+
                HAACK("Haack series",
                                "The Haack series nose cones are designed to minimize drag.  The shape parameter " +
-                               "0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes " +
-                               "drag for fixed length and diameter, while a value of 0.333 produces an " +
-                               "<b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume.",
+                                               "0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes " +
+                                               "drag for fixed length and diameter, while a value of 0.333 produces an " +
+                                               "<b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume.",
                                "The Haack series <i>nose cones</i> are designed to minimize drag.  " +
-                               "These transition shapes are their equivalents, but do not necessarily produce " +
-                               "optimal drag for transitions.  " +
-                               "The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, " +
-                               "while a value of 0.333 produces an <b>LV-Haack</b> shape.",true) {
+                                               "These transition shapes are their equivalents, but do not necessarily produce " +
+                                               "optimal drag for transitions.  " +
+                                               "The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, " +
+                                               "while a value of 0.333 produces an <b>LV-Haack</b> shape.", true) {
                        @Override
                        public boolean usesParameter() {
                                return true;
                        }
-                       @Override 
+                       
+                       @Override
                        public double maxParameter() {
-                               return 1.0/3.0;  // Range 0...1/3
+                               return 1.0 / 3.0; // Range 0...1/3
                        }
+                       
                        @Override
                        public double getRadius(double x, double radius, double length, double param) {
                                assert x >= 0;
@@ -704,58 +727,58 @@ public class Transition extends SymmetricComponent {
                                assert radius >= 0;
                                assert param >= 0;
                                assert param <= 2;
-
-                               double theta = Math.acos(1-2*x/length); 
-                               if (param==0) {
-                                       return radius*sqrt((theta-sin(2*theta)/2)/Math.PI);
+                               
+                               double theta = Math.acos(1 - 2 * x / length);
+                               if (param == 0) {
+                                       return radius * sqrt((theta - sin(2 * theta) / 2) / Math.PI);
                                }
-                               return radius*sqrt((theta-sin(2*theta)/2+param*pow3(sin(theta)))/Math.PI);
+                               return radius * sqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI);
                        }
                },
                
-//             POLYNOMIAL("Smooth polynomial",
-//                             "A polynomial is fitted such that the nose cone profile is horizontal "+
-//                             "at the aft end of the transition.  The angle at the tip is defined by "+
-//                             "the shape parameter.",
-//                             "A polynomial is fitted such that the transition profile is horizontal "+
-//                             "at the aft end of the transition.  The angle at the fore end is defined "+
-//                             "by the shape parameter.") {
-//                     @Override
-//                     public boolean usesParameter() {
-//                             return true;
-//                     }
-//                     @Override
-//                     public double maxParameter() {
-//                             return 3.0;   //  Range 0...3
-//                     }
-//                     @Override
-//                     public double defaultParameter() {
-//                             return 0.0;
-//                     }
-//                     public double getRadius(double x, double radius, double length, double param) {
-//                             assert x >= 0;
-//                             assert x <= length;
-//                             assert radius >= 0;
-//                             assert param >= 0;
-//                             assert param <= 3;
-//                             // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
-//                             x = x/length;
-//                             return radius*((((param-2)*x + (3-2*param))*x + param)*x);
-//                     }
-//             }
+               //              POLYNOMIAL("Smooth polynomial",
+               //                              "A polynomial is fitted such that the nose cone profile is horizontal "+
+               //                              "at the aft end of the transition.  The angle at the tip is defined by "+
+               //                              "the shape parameter.",
+               //                              "A polynomial is fitted such that the transition profile is horizontal "+
+               //                              "at the aft end of the transition.  The angle at the fore end is defined "+
+               //                              "by the shape parameter.") {
+               //                      @Override
+               //                      public boolean usesParameter() {
+               //                              return true;
+               //                      }
+               //                      @Override
+               //                      public double maxParameter() {
+               //                              return 3.0;   //  Range 0...3
+               //                      }
+               //                      @Override
+               //                      public double defaultParameter() {
+               //                              return 0.0;
+               //                      }
+               //                      public double getRadius(double x, double radius, double length, double param) {
+               //                              assert x >= 0;
+               //                              assert x <= length;
+               //                              assert radius >= 0;
+               //                              assert param >= 0;
+               //                              assert param <= 3;
+               //                              // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
+               //                              x = x/length;
+               //                              return radius*((((param-2)*x + (3-2*param))*x + param)*x);
+               //                      }
+               //              }
                ;
-                                               
+               
                // Privete fields of the shapes
                private final String name;
                private final String transitionDesc;
                private final String noseconeDesc;
                private final boolean canClip;
-
+               
                // Non-clippable constructor
                Shape(String name, String noseconeDesc, String transitionDesc) {
-                       this(name,noseconeDesc,transitionDesc,false);
+                       this(name, noseconeDesc, transitionDesc, false);
                }
-
+               
                // Clippable constructor
                Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
                        this.name = name;
@@ -785,7 +808,7 @@ public class Transition extends SymmetricComponent {
                public String getNoseConeDescription() {
                        return noseconeDesc;
                }
-
+               
                /**
                 * Check whether the shape differs in clipped mode.  The clipping should be
                 * enabled by default if possible.
@@ -800,7 +823,7 @@ public class Transition extends SymmetricComponent {
                public boolean usesParameter() {
                        return false;
                }
-
+               
                /**
                 * Return the minimum value of the shape parameter.  (Default 0.)
                 */
@@ -821,7 +844,7 @@ public class Transition extends SymmetricComponent {
                public double defaultParameter() {
                        return 0.0;
                }
-
+               
                /**
                 * Calculate the basic radius of a transition with the given radius, length and
                 * shape parameter at the point x from the tip of the component.  It is assumed
@@ -835,7 +858,7 @@ public class Transition extends SymmetricComponent {
                 * @return       The basic radius at the given position.
                 */
                public abstract double getRadius(double x, double radius, double length, double param);
-
+               
                
                /**
                 * Returns the name of the shape (same as getName()).
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..655cdb52b780b3d2e0132c319be9ae884c4d2735 100644 (file)
@@ -41,6 +41,7 @@ public class BasicEventSimulationEngine implements SimulationEngine {
        private SimulationStatus status;
        
        
+       @Override
        public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException {
                Set<MotorId> motorBurntOut = new HashSet<MotorId>();
                
@@ -77,7 +78,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 +295,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) {
@@ -409,7 +410,7 @@ public class BasicEventSimulationEngine implements SimulationEngine {
                                
                        case STAGE_SEPARATION: {
                                // TODO: HIGH: Store lower stages to be simulated later
-                               RocketComponent stage = (RocketComponent) event.getSource();
+                               RocketComponent stage = event.getSource();
                                int n = stage.getStageNumber();
                                status.getConfiguration().setToStage(n);
                                status.getFlightData().addEvent(event);
@@ -542,6 +543,14 @@ public class BasicEventSimulationEngine implements SimulationEngine {
                d += status.getEffectiveLaunchRodLength();
                
                if (Double.isNaN(d) || b) {
+                       log.error("Simulation resulted in NaN value:" +
+                                       " simulationTime=" + status.getSimulationTime() +
+                                       " previousTimeStep=" + status.getPreviousTimeStep() +
+                                       " rocketPosition=" + status.getRocketPosition() +
+                                       " rocketVelocity=" + status.getRocketVelocity() +
+                                       " rocketOrientationQuaternion=" + status.getRocketOrientationQuaternion() +
+                                       " rocketRotationVelocity=" + status.getRocketRotationVelocity() +
+                                       " effectiveLaunchRodLength=" + status.getEffectiveLaunchRodLength());
                        throw new SimulationException("Simulation resulted in not-a-number (NaN) value, please report a bug.");
                }
        }
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 2d2d492e0e9d8c09c81f210b51ef196cc88a4349..658f70c0b05a42d6ecd47765302eac39d13bc556 100644 (file)
@@ -62,6 +62,9 @@ public class Startup {
        
        public static void main(final String[] args) throws Exception {
                
+               // Check for "openrocket.debug" property before anything else
+               checkDebugStatus();
+               
                // Initialize logging first so we can use it
                initializeLogging();
                
@@ -91,6 +94,18 @@ public class Startup {
        
 
 
+       private static void checkDebugStatus() {
+               if (System.getProperty("openrocket.debug") != null) {
+                       System.setProperty("openrocket.log.stdout", "VBOSE");
+                       System.setProperty("openrocket.log.tracelevel", "VBOSE");
+                       System.setProperty("openrocket.debug.menu", "true");
+                       System.setProperty("openrocket.debug.motordigest", "true");
+               }
+       }
+       
+       
+
+
        private static void runMain(String[] args) {
                
                // Initialize the splash screen with version info
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);
        }
        
 }
diff --git a/src/net/sf/openrocket/util/ConcurrencyException.java b/src/net/sf/openrocket/util/ConcurrencyException.java
new file mode 100644 (file)
index 0000000..252267c
--- /dev/null
@@ -0,0 +1,26 @@
+package net.sf.openrocket.util;
+
+/**
+ * An exception that indicates a concurrency bug in the software.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ConcurrencyException extends FatalException {
+       
+       public ConcurrencyException() {
+               super();
+       }
+       
+       public ConcurrencyException(String message, Throwable cause) {
+               super(message, cause);
+       }
+       
+       public ConcurrencyException(String message) {
+               super(message);
+       }
+       
+       public ConcurrencyException(Throwable cause) {
+               super(cause);
+       }
+       
+}
index 46b7893106985d9ebb07059844b596a21f802cc7..ff97d9a8e1853df824d396f81654e74ebb05459b 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);
                }
@@ -126,7 +128,9 @@ public class GUIUtil {
         */
        public static void installEscapeCloseOperation(final JDialog dialog) {
                Action dispatchClosing = new AbstractAction() {
+                       @Override
                        public void actionPerformed(ActionEvent event) {
+                               log.user("Closing dialog " + dialog);
                                dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
                        }
                };
@@ -191,6 +195,7 @@ public class GUIUtil {
                        @Override
                        public void windowClosed(WindowEvent e) {
                                setNullModels(window);
+                               MemoryManagement.collectable(window);
                        }
                });
        }
@@ -338,7 +343,7 @@ public class GUIUtil {
                        
                        JTree tree = (JTree) c;
                        tree.setModel(new DefaultTreeModel(new TreeNode() {
-                               @SuppressWarnings("unchecked")
+                               @SuppressWarnings("rawtypes")
                                @Override
                                public Enumeration children() {
                                        return new Vector().elements();
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/MemoryManagement.java b/src/net/sf/openrocket/util/MemoryManagement.java
new file mode 100644 (file)
index 0000000..34589b5
--- /dev/null
@@ -0,0 +1,137 @@
+package net.sf.openrocket.util;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
+
+/**
+ * A class that performs certain memory-management operations for debugging purposes.
+ * For example, complex objects that are being discarded and that should be garbage-collectable
+ * (such as dialog windows) should be registered for monitoring by calling
+ * {@link #collectable(Object)}.  This will allow monitoring whether the object really is
+ * garbage-collected or whether it is retained in memory due to a memory leak.
+ * Only complex objects should be registered due to the overhead of the monitoring.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public final class MemoryManagement {
+       private static final LogHelper log = Application.getLogger();
+       
+       /** Purge cleared references every this many calls to {@link #collectable(Object)} */
+       private static final int PURGE_CALL_COUNT = 100;
+       
+
+       /**
+        * Storage of the objects.  This is basically a mapping from the objects (using weak references)
+        * to 
+        */
+       private static List<MemoryData> objects = new LinkedList<MemoryData>();
+       private static int callCount = 0;
+       
+       
+       private MemoryManagement() {
+       }
+       
+       
+       /**
+        * Mark an object that should be garbage-collectable by the GC.  This class will monitor
+        * whether the object actually gets garbage-collected or not by holding a weak reference
+        * to the object.
+        * 
+        * @param o             the object to monitor.
+        */
+       public static synchronized void collectable(Object o) {
+               if (o == null) {
+                       throw new IllegalArgumentException("object is null");
+               }
+               log.debug("Adding object into collectable list: " + o);
+               objects.add(new MemoryData(o));
+               callCount++;
+               if (callCount % PURGE_CALL_COUNT == 0) {
+                       purge();
+               }
+       }
+       
+       
+       /**
+        * Return the number of times {@link #collectable(Object)} has been called.
+        * @return      the number of times {@link #collectable(Object)} has been called.
+        */
+       public static synchronized int getCallCount() {
+               return callCount;
+       }
+       
+       
+       /**
+        * Return a list of MemoryData objects corresponding to the objects that have been
+        * registered by {@link #collectable(Object)} and have not been garbage-collected properly.
+        * This method first calls <code>System.gc()</code> multiple times to attempt to
+        * force any remaining garbage collection.
+        * 
+        * @return      a list of MemoryData objects for objects that have not yet been garbage-collected.
+        */
+       public static synchronized ArrayList<MemoryData> getRemainingObjects() {
+               for (int i = 0; i < 5; i++) {
+                       System.runFinalization();
+                       System.gc();
+                       try {
+                               Thread.sleep(1);
+                       } catch (InterruptedException e) {
+                       }
+               }
+               purge();
+               return new ArrayList<MemoryData>(objects);
+       }
+       
+       
+
+       /**
+        * Purge all cleared references from the object list.
+        */
+       private static void purge() {
+               int origCount = objects.size();
+               Iterator<MemoryData> iterator = objects.iterator();
+               while (iterator.hasNext()) {
+                       MemoryData data = iterator.next();
+                       if (data.getReference().get() == null) {
+                               iterator.remove();
+                       }
+               }
+               log.debug(objects.size() + " of " + origCount + " objects remaining in discarded objects list after purge.");
+       }
+       
+       
+       /**
+        * A value object class containing data of a discarded object reference.
+        */
+       public static final class MemoryData {
+               private final WeakReference<Object> reference;
+               private final long registrationTime;
+               
+               private MemoryData(Object object) {
+                       this.reference = new WeakReference<Object>(object);
+                       this.registrationTime = System.currentTimeMillis();
+               }
+               
+               /**
+                * Return the weak reference to the discarded object.
+                */
+               public WeakReference<Object> getReference() {
+                       return reference;
+               }
+               
+               /**
+                * Return the time when the object was discarded.
+                * @return      a millisecond timestamp of when the object was discarded.
+                */
+               public long getRegistrationTime() {
+                       return registrationTime;
+               }
+       }
+       
+}
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;
diff --git a/src/net/sf/openrocket/util/SafetyMutex.java b/src/net/sf/openrocket/util/SafetyMutex.java
new file mode 100644 (file)
index 0000000..fb202a9
--- /dev/null
@@ -0,0 +1,171 @@
+package net.sf.openrocket.util;
+
+import java.util.LinkedList;
+
+import net.sf.openrocket.gui.main.ExceptionHandler;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
+
+/**
+ * A mutex that can be used for verifying thread safety.  This class cannot be
+ * used to perform synchronization, only to detect concurrency issues.  This
+ * class can be used by the main methods of non-thread-safe classes to ensure
+ * the class is not wrongly used from multiple threads concurrently.
+ * <p>
+ * This mutex is not reentrant even for the same thread that has locked it.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SafetyMutex {
+       private static final LogHelper log = Application.getLogger();
+       
+       // Package-private for unit testing
+       static volatile boolean errorReported = false;
+       
+       // lockingThread is set when this mutex is locked.
+       Thread lockingThread = null;
+       // Stack of places that have locked this mutex
+       final LinkedList<String> locations = new LinkedList<String>();
+       
+       
+       /**
+        * Verify that this mutex is unlocked, but don't lock it.  This has the same effect
+        * as <code>mutex.lock(); mutex.unlock();</code> and is useful for methods that return
+        * immediately (e.g. getters).
+        * 
+        * @throws ConcurrencyException if this mutex is already locked.
+        */
+       public synchronized void verify() {
+               checkState(true);
+               if (lockingThread != null && lockingThread != Thread.currentThread()) {
+                       error("Mutex is already locked", true);
+               }
+       }
+       
+       
+       /**
+        * Lock this mutex.  If this mutex is already locked an error is raised and
+        * a ConcurrencyException is thrown.  The location parameter is used to distinguish
+        * the locking location, and it should be e.g. the method name.
+        * 
+        * @param location      a string describing the location where this mutex was locked (cannot be null).
+        * 
+        * @throws ConcurrencyException         if this mutex is already locked.
+        */
+       public synchronized void lock(String location) {
+               if (location == null) {
+                       throw new IllegalArgumentException("location is null");
+               }
+               checkState(true);
+               
+               Thread currentThread = Thread.currentThread();
+               if (lockingThread != null && lockingThread != currentThread) {
+                       error("Mutex is already locked", true);
+               }
+               
+               lockingThread = currentThread;
+               locations.push(location);
+       }
+       
+       
+
+       /**
+        * Unlock this mutex.  If this mutex is not locked at the position of the parameter
+        * or was locked by another thread than the current thread an error is raised,
+        * but an exception is not thrown.
+        * <p>
+        * This method is guaranteed never to throw an exception, so it can safely be used in finally blocks.
+        * 
+        * @param location      a location string matching that which locked the mutex
+        * @return                      whether the unlocking was successful (this normally doesn't need to be checked)
+        */
+       public synchronized boolean unlock(String location) {
+               try {
+                       
+                       if (location == null) {
+                               ExceptionHandler.handleErrorCondition("location is null");
+                               location = "";
+                       }
+                       checkState(false);
+                       
+
+                       // Check that the mutex is locked
+                       if (lockingThread == null) {
+                               error("Mutex was not locked", false);
+                               return false;
+                       }
+                       
+                       // Check that the mutex is locked by the current thread
+                       if (lockingThread != Thread.currentThread()) {
+                               error("Mutex is being unlocked from differerent thread than where it was locked", false);
+                               return false;
+                       }
+                       
+                       // Check that the unlock location is correct
+                       String lastLocation = locations.pop();
+                       if (!location.equals(lastLocation)) {
+                               locations.push(lastLocation);
+                               error("Mutex unlocking location does not match locking location, location=" + location, false);
+                               return false;
+                       }
+                       
+                       // Unlock the mutex if the last one
+                       if (locations.isEmpty()) {
+                               lockingThread = null;
+                       }
+                       return true;
+               } catch (Exception e) {
+                       ExceptionHandler.handleErrorCondition("An exception occurred while unlocking a mutex, " +
+                                       "locking thread=" + lockingThread + " locations=" + locations, e);
+                       return false;
+               }
+       }
+       
+       
+
+       /**
+        * Check that the internal state of the mutex (lockingThread vs. locations) is correct.
+        */
+       private void checkState(boolean throwException) {
+               /*
+                * Disallowed states:
+                *   lockingThread == null  &&  !locations.isEmpty()
+                *   lockingThread != null  &&  locations.isEmpty()
+                */
+               if ((lockingThread == null) ^ (locations.isEmpty())) {
+                       // Clear the mutex only after error() has executed (and possibly thrown an exception)
+                       try {
+                               error("Mutex data inconsistency occurred - unlocking mutex", throwException);
+                       } finally {
+                               lockingThread = null;
+                               locations.clear();
+                       }
+               }
+       }
+       
+       
+       /**
+        * Raise an error.  The first occurrence is passed directly to the exception handler,
+        * later errors are simply logged.
+        */
+       private void error(String message, boolean throwException) {
+               message = message +
+                               ", current thread = " + Thread.currentThread() +
+                               ", locking thread=" + lockingThread +
+                               ", locking locations=" + locations;
+               
+               ConcurrencyException ex = new ConcurrencyException(message);
+               
+               if (!errorReported) {
+                       errorReported = true;
+                       ExceptionHandler.handleErrorCondition(ex);
+               } else {
+                       log.error(message, ex);
+               }
+               
+               if (throwException) {
+                       throw ex;
+               }
+       }
+       
+}
index c6a1bb835c6e99c09983e4f60d0347bd58864c9b..32dffd849a4eb0c13cc92d8c1eb613df76e5b327 100644 (file)
@@ -2,11 +2,11 @@ package net.sf.openrocket.util;
 
 
 public class TextUtil {
-       private static final char[] HEX = { 
-               '0','1','2','3','4','5','6','7',
-               '8','9','a','b','c','d','e','f' 
+       private static final char[] HEX = {
+                       '0', '1', '2', '3', '4', '5', '6', '7',
+                       '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
        };
-
+       
        
        /**
         * Return the bytes formatted as a hexadecimal string.  The length of the
@@ -18,13 +18,13 @@ public class TextUtil {
         */
        public static final String hexString(byte[] bytes) {
                StringBuilder sb = new StringBuilder(bytes.length * 2);
-               for (byte b: bytes) {
+               for (byte b : bytes) {
                        sb.append(HEX[(b >>> 4) & 0xF]);
                        sb.append(HEX[b & 0xF]);
                }
                return sb.toString();
        }
-
+       
        /**
         * Return a string of the double value with suitable precision (5 digits).
         * The string is the shortest representation of the value including the
@@ -49,7 +49,7 @@ public class TextUtil {
                                return "Inf";
                }
                
-               
+
                final String sign = (d < 0) ? "-" : "";
                double abs = Math.abs(d);
                
@@ -95,7 +95,7 @@ public class TextUtil {
         */
        private static String decimalFormat(double value) {
                if (value >= 10000)
-                       return "" + (int)(value + 0.5);
+                       return "" + (int) (value + 0.5);
                
                int decimals = 1;
                double v = value;
@@ -108,8 +108,8 @@ public class TextUtil {
        }
        
        
-       
-       
+
+
        /*
         * value must be positive!
         */
@@ -118,42 +118,50 @@ public class TextUtil {
                // Calculate rounding and limit values (rounding slightly smaller)
                int rounding = 1;
                double limit = 0.5;
-               for (int i=0; i<decimals; i++) {
+               for (int i = 0; i < decimals; i++) {
                        rounding *= 10;
                        limit /= 10;
                }
-
-               // Round value
-               value = (Math.rint(value * rounding)+0.1) / rounding;
                
+               // Round value
+               value = (Math.rint(value * rounding) + 0.1) / rounding;
                
-               int whole = (int)value;
+
+               int whole = (int) value;
                value -= whole;
                
-               
+
                if (value < limit)
-                       return "" + whole; 
+                       return "" + whole;
                limit *= 10;
-
+               
                StringBuilder sb = new StringBuilder();
                sb.append("" + whole);
                sb.append('.');
-
                
-               for (int i = 0; i<decimals; i++) {
+
+               for (int i = 0; i < decimals; i++) {
                        
                        value *= 10;
-                       whole = (int)value;
+                       whole = (int) value;
                        value -= whole;
-                       sb.append((char)('0' + whole));
+                       sb.append((char) ('0' + whole));
                        
                        if (value < limit)
                                return sb.toString();
                        limit *= 10;
-
+                       
                }
-
+               
                return sb.toString();
        }
-
+       
+       
+       public static String htmlEncode(String s) {
+               s = s.replace("&", "&amp;");
+               s = s.replace("\"", "&quot;");
+               s = s.replace("<", "&lt;");
+               s = s.replace(">", "&gt;");
+               return s;
+       }
 }
index 4eacec33961e79507987dfda5514c9ffcc40c201..e5f9da71ae7287fed84935ed6be85c52c0a56deb 100644 (file)
@@ -1,12 +1,13 @@
 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.OptimizationException;
+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;
 
 
 
@@ -22,7 +23,7 @@ public class TestFunctionOptimizer {
        
 
        private void go(final ParallelFunctionCache functionCache,
-                       final FunctionOptimizer optimizer, final Point optimum, final int maxSteps) {
+                       final FunctionOptimizer optimizer, final Point optimum, final int maxSteps) throws OptimizationException {
                
                Function function = new Function() {
                        @Override
@@ -35,15 +36,6 @@ public class TestFunctionOptimizer {
                                        return Double.NaN;
                                }
                        }
-                       
-                       @Override
-                       public double preComputed(Point p) {
-                               for (double d : p.asArray()) {
-                                       if (d < 0 || d > 1)
-                                               return Double.MAX_VALUE;
-                               }
-                               return Double.NaN;
-                       }
                };
                
                OptimizationController control = new OptimizationController() {
@@ -83,7 +75,7 @@ public class TestFunctionOptimizer {
        }
        
        
-       public static void main(String[] args) throws InterruptedException {
+       public static void main(String[] args) throws InterruptedException, OptimizationException {
                
                System.err.println("Number of processors: " + Runtime.getRuntime().availableProcessors());
                
index 5a81d10f636953a44eb786d2477d60438f8eae69..e09a0ee85f21ff52966a27f27e3b6236293e1446 100644 (file)
@@ -7,12 +7,13 @@ 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.OptimizationException;
+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;
 
 
@@ -26,7 +27,7 @@ public class TestFunctionOptimizerLoop {
        
        
 
-       private void go(final FunctionOptimizer optimizer, final Point optimum, final int maxSteps, ExecutorService executor) {
+       private void go(final FunctionOptimizer optimizer, final Point optimum, final int maxSteps, ExecutorService executor) throws OptimizationException {
                
                Function function = new Function() {
                        @Override
@@ -34,15 +35,6 @@ public class TestFunctionOptimizerLoop {
                                evaluations++;
                                return p.sub(optimum).length2();
                        }
-                       
-                       @Override
-                       public double preComputed(Point p) {
-                               for (double d : p.asArray()) {
-                                       if (d < 0 || d > 1)
-                                               return Double.MAX_VALUE;
-                               }
-                               return Double.NaN;
-                       }
                };
                
                OptimizationController control = new OptimizationController() {
@@ -67,7 +59,7 @@ public class TestFunctionOptimizerLoop {
        }
        
        
-       public static void main(String[] args) {
+       public static void main(String[] args) throws OptimizationException {
                
                System.err.println("PRECISION = " + PRECISION);
                
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..c864ec9
--- /dev/null
@@ -0,0 +1,317 @@
+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 9b33319d5aff4412ce1adf70ec56c7630557d48e..1ea1f98934eb8af5c7f2ddee7b2bcb9f8fed7edd 100644 (file)
@@ -8,6 +8,8 @@ import junit.framework.Test;
 import junit.framework.TestCase;
 import junit.framework.TestSuite;
 import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
 import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.rocketcomponent.Stage;
 
@@ -156,6 +158,10 @@ public class RocksimLoaderTest extends TestCase {
         assertTrue(stage2.isCGOverridden());
         assertEquals(0.4d, stage2.getOverrideCG().x);
         
+        BodyTube bt = (BodyTube)stage2.getChild(0);
+        LaunchLug ll = (LaunchLug)bt.getChild(6);
+        assertEquals(1.22d, ll.getRadialDirection());
+        
         assertEquals(2, stage3.getChildCount());
         assertEquals("Transition", stage3.getChild(0).getName());
         assertEquals("Body tube", stage3.getChild(1).getName());
index a7c46dae2c1f005e305ed4f4d7712dd93925a95b..df1788c2b2c73f370a6aa4d7fe9b3e88203bb32c 100644 (file)
 <PartNo>13056</PartNo>
 <PartDesc><![CDATA[1/4" X 3]]></PartDesc>
 <RadialLoc>37.185</RadialLoc>
-<RadialAngle>0.</RadialAngle>
+<RadialAngle>3.14</RadialAngle>
 <Texture>file=()|position=(0,0,0)|origin=(0.5,0.5,0.5)|scale=(1,1,1)|repeat=(1)|interpolate=(0)|flipr(0)|flips(0)|flipt=(0)|preventseam=(1)</Texture>
 <Opacity>1.</Opacity>
 <Specular>0.</Specular>
 <PartNo>13056</PartNo>
 <PartDesc><![CDATA[1/4" X 3]]></PartDesc>
 <RadialLoc>37.185</RadialLoc>
-<RadialAngle>0.</RadialAngle>
+<RadialAngle>1.22</RadialAngle>
 <Texture>file=()|position=(0,0,0)|origin=(0.5,0.5,0.5)|scale=(1,1,1)|repeat=(1)|interpolate=(0)|flipr(0)|flips(0)|flipt=(0)|preventseam=(1)</Texture>
 <Opacity>1.</Opacity>
 <Specular>0.</Specular>
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 {
diff --git a/test/net/sf/openrocket/optimization/rocketoptimization/TestRocketOptimizationFunction.java b/test/net/sf/openrocket/optimization/rocketoptimization/TestRocketOptimizationFunction.java
new file mode 100644 (file)
index 0000000..53ee37d
--- /dev/null
@@ -0,0 +1,214 @@
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import static org.junit.Assert.*;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.optimization.general.OptimizationException;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.rocketcomponent.Rocket;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.auto.Mock;
+import org.jmock.integration.junit4.JMock;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+@RunWith(JMock.class)
+public class TestRocketOptimizationFunction {
+       Mockery context = new JUnit4Mockery();
+       
+       @Mock
+       OptimizableParameter parameter;
+       @Mock
+       OptimizationGoal goal;
+       @Mock
+       SimulationDomain domain;
+       @Mock
+       SimulationModifier modifier1;
+       @Mock
+       SimulationModifier modifier2;
+       
+       
+       @Test
+       public void testNormalEvaluation() throws InterruptedException, OptimizationException {
+               final Rocket rocket = new Rocket();
+               final Simulation simulation = new Simulation(rocket);
+               
+               final double p1 = 0.4;
+               final double p2 = 0.7;
+               final double ddist = -0.43;
+               final double pvalue = 9.81;
+               final double gvalue = 8.81;
+               
+               // @formatter:off
+               context.checking(new Expectations() {{
+                               oneOf(modifier1).modify(simulation, p1);
+                               oneOf(modifier2).modify(simulation, p2);
+                               oneOf(domain).getDistanceToDomain(simulation); will(returnValue(ddist));
+                               oneOf(parameter).computeValue(simulation); will(returnValue(pvalue));
+                               oneOf(goal).getMinimizationParameter(pvalue); will(returnValue(gvalue));
+               }});
+               // @formatter:on
+               
+
+               RocketOptimizationFunction function = new RocketOptimizationFunction(simulation,
+                               parameter, goal, domain, modifier1, modifier2) {
+                       @Override
+                       Simulation newSimulationInstance(Simulation sim) {
+                               return sim;
+                       }
+               };
+               
+
+               assertEquals(Double.NaN, function.getComputedParameterValue(new Point(p1, p2)), 0);
+               
+               double value = function.evaluate(new Point(p1, p2));
+               assertEquals(gvalue, value, 0);
+               
+               assertEquals(pvalue, function.getComputedParameterValue(new Point(p1, p2)), 0);
+               
+               // Re-evaluate the point to verify parameter is not recomputed
+               value = function.evaluate(new Point(p1, p2));
+               assertEquals(gvalue, value, 0);
+       }
+       
+       
+       @Test
+       public void testNaNValue() throws InterruptedException, OptimizationException {
+               final Rocket rocket = new Rocket();
+               final Simulation simulation = new Simulation(rocket);
+               
+               final double p1 = 0.4;
+               final double p2 = 0.7;
+               final double ddist = -0.43;
+               final double pvalue = 9.81;
+               
+               // @formatter:off
+               context.checking(new Expectations() {{
+                               oneOf(modifier1).modify(simulation, p1);
+                               oneOf(modifier2).modify(simulation, p2);
+                               oneOf(domain).getDistanceToDomain(simulation); will(returnValue(ddist));
+                               oneOf(parameter).computeValue(simulation); will(returnValue(pvalue));
+                               oneOf(goal).getMinimizationParameter(pvalue); will(returnValue(Double.NaN));
+               }});
+               // @formatter:on
+               
+
+               RocketOptimizationFunction function = new RocketOptimizationFunction(simulation,
+                               parameter, goal, domain, modifier1, modifier2) {
+                       @Override
+                       Simulation newSimulationInstance(Simulation sim) {
+                               return sim;
+                       }
+               };
+               
+
+               assertEquals(Double.NaN, function.getComputedParameterValue(new Point(p1, p2)), 0);
+               
+               double value = function.evaluate(new Point(p1, p2));
+               assertEquals(Double.MAX_VALUE, value, 0);
+               
+               assertEquals(pvalue, function.getComputedParameterValue(new Point(p1, p2)), 0);
+               
+               value = function.evaluate(new Point(p1, p2));
+               assertEquals(Double.MAX_VALUE, value, 0);
+       }
+       
+       
+       @Test
+       public void testOutsideDomain() throws InterruptedException, OptimizationException {
+               final Rocket rocket = new Rocket();
+               final Simulation simulation = new Simulation(rocket);
+               
+               final double p1 = 0.4;
+               final double p2 = 0.7;
+               final double ddist = 0.98;
+               
+               // @formatter:off
+               context.checking(new Expectations() {{
+                               oneOf(modifier1).modify(simulation, p1);
+                               oneOf(modifier2).modify(simulation, p2);
+                               oneOf(domain).getDistanceToDomain(simulation); will(returnValue(ddist));
+               }});
+               // @formatter:on
+               
+
+               RocketOptimizationFunction function = new RocketOptimizationFunction(simulation,
+                               parameter, goal, domain, modifier1, modifier2) {
+                       @Override
+                       Simulation newSimulationInstance(Simulation sim) {
+                               return sim;
+                       }
+               };
+               
+
+               assertEquals(Double.NaN, function.getComputedParameterValue(new Point(p1, p2)), 0);
+               
+               double value = function.evaluate(new Point(p1, p2));
+               assertTrue(value > 1e100);
+               
+               assertEquals(Double.NaN, function.getComputedParameterValue(new Point(p1, p2)), 0);
+               
+               value = function.evaluate(new Point(p1, p2));
+               assertTrue(value > 1e100);
+       }
+       
+       @Test
+       public void testOutsideDomain2() throws InterruptedException, OptimizationException {
+               final Rocket rocket = new Rocket();
+               final Simulation simulation = new Simulation(rocket);
+               
+               final double p1 = 0.4;
+               final double p2 = 0.7;
+               final double ddist = Double.NaN;
+               
+               // @formatter:off
+               context.checking(new Expectations() {{
+                               oneOf(modifier1).modify(simulation, p1);
+                               oneOf(modifier2).modify(simulation, p2);
+                               oneOf(domain).getDistanceToDomain(simulation); will(returnValue(ddist));
+               }});
+               // @formatter:on
+               
+
+               RocketOptimizationFunction function = new RocketOptimizationFunction(simulation,
+                               parameter, goal, domain, modifier1, modifier2) {
+                       @Override
+                       Simulation newSimulationInstance(Simulation sim) {
+                               return sim;
+                       }
+               };
+               
+
+               assertEquals(Double.NaN, function.getComputedParameterValue(new Point(p1, p2)), 0);
+               
+               double value = function.evaluate(new Point(p1, p2));
+               assertEquals(Double.MAX_VALUE, value, 0);
+               
+               assertEquals(Double.NaN, function.getComputedParameterValue(new Point(p1, p2)), 0);
+               
+               value = function.evaluate(new Point(p1, p2));
+               assertEquals(Double.MAX_VALUE, value, 0);
+       }
+       
+       
+       @Test
+       public void testNewSimulationInstance() {
+               final Rocket rocket = new Rocket();
+               rocket.setName("Foobar");
+               final Simulation simulation = new Simulation(rocket);
+               simulation.setName("MySim");
+               
+               RocketOptimizationFunction function = new RocketOptimizationFunction(simulation,
+                               parameter, goal, domain, modifier1, modifier2);
+               
+               Simulation sim = function.newSimulationInstance(simulation);
+               assertFalse(simulation == sim);
+               assertEquals("MySim", sim.getName());
+               assertFalse(rocket == sim.getRocket());
+               assertEquals("Foobar", sim.getRocket().getName());
+       }
+       
+}
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
diff --git a/test/net/sf/openrocket/util/TestMutex.java b/test/net/sf/openrocket/util/TestMutex.java
new file mode 100644 (file)
index 0000000..dd01848
--- /dev/null
@@ -0,0 +1,156 @@
+package net.sf.openrocket.util;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class TestMutex {
+       
+       @Test
+       public void testSingleLocking() {
+               SafetyMutex m = new SafetyMutex();
+               
+               // Test single locking
+               assertNull(m.lockingThread);
+               m.verify();
+               m.lock("here");
+               assertNotNull(m.lockingThread);
+               assertTrue(m.unlock("here"));
+               
+       }
+       
+       @Test
+       public void testDoubleLocking() {
+               SafetyMutex m = new SafetyMutex();
+               
+               // Test double locking
+               m.verify();
+               m.lock("foobar");
+               m.verify();
+               m.lock("bazqux");
+               m.verify();
+               assertTrue(m.unlock("bazqux"));
+               m.verify();
+               assertTrue(m.unlock("foobar"));
+               m.verify();
+       }
+       
+       @Test
+       public void testDoubleUnlocking() {
+               SafetyMutex m = new SafetyMutex();
+               // Mark error reported to not init exception handler
+               SafetyMutex.errorReported = true;
+               
+               m.lock("here");
+               assertTrue(m.unlock("here"));
+               assertFalse(m.unlock("here"));
+       }
+       
+       
+
+       private volatile int testState = 0;
+       private volatile String failure = null;
+       
+       @Test(timeout = 1000)
+       public void testThreadingErrors() {
+               final SafetyMutex m = new SafetyMutex();
+               
+               // Initialize and start the thread
+               Thread thread = new Thread() {
+                       @Override
+                       public void run() {
+                               try {
+                                       
+                                       // Test locking a locked mutex
+                                       waitFor(1);
+                                       try {
+                                               m.lock("in thread one");
+                                               failure = "Succeeded in locking a mutex locked by a different thread";
+                                               return;
+                                       } catch (ConcurrencyException e) {
+                                               // OK
+                                       }
+                                       
+                                       // Test unlocking a mutex locked by a different thread
+                                       if (m.unlock("in thread two")) {
+                                               failure = "Succeeded in unlocking a mutex locked by a different thread";
+                                               return;
+                                       }
+                                       
+                                       // Test verifying a locked mutex that already has an error
+                                       try {
+                                               m.verify();
+                                               failure = "Succeeded in verifying a mutex locked by a different thread";
+                                               return;
+                                       } catch (ConcurrencyException e) {
+                                               // OK
+                                       }
+                                       
+                                       // Test locking a mutex after it's been unlocked
+                                       testState = 2;
+                                       waitFor(3);
+                                       m.lock("in thread three");
+                                       m.verify();
+                                       
+                                       // Wait for other side to test
+                                       testState = 4;
+                                       waitFor(5);
+                                       
+                                       // Exit code
+                                       testState = 6;
+                                       
+                               } catch (Exception e) {
+                                       failure = "Exception occurred in thread: " + e;
+                                       return;
+                               }
+                               
+                       }
+               };
+               thread.setDaemon(true);
+               thread.start();
+               
+               m.lock("one");
+               testState = 1;
+               
+               waitFor(2);
+               assertNull("Thread error: " + failure, failure);
+               
+               m.verify();
+               m.unlock("one");
+               testState = 3;
+               
+               waitFor(4);
+               assertNull("Thread error: " + failure, failure);
+               
+               try {
+                       m.lock("two");
+                       fail("Succeeded in locking a locked mutex in main thread");
+               } catch (ConcurrencyException e) {
+                       // OK
+               }
+               
+               // Test unlocking a mutex locked by a different thread
+               assertFalse(m.unlock("here"));
+               
+               try {
+                       m.verify();
+                       fail("Succeeded in verifying a locked mutex in main thread");
+               } catch (ConcurrencyException e) {
+                       // OK
+               }
+               
+               testState = 5;
+               waitFor(6);
+               assertNull("Thread error: " + failure, failure);
+       }
+       
+       private void waitFor(int state) {
+               while (testState != state && failure == null) {
+                       try {
+                               Thread.sleep(1);
+                       } catch (InterruptedException e) {
+                       }
+               }
+       }
+       
+}
index d1936eccc5b4b517e7e5a4d7a25a5cdd000056b7..ea686483385befd13350f799d0d41bee72fccb64 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) {
@@ -76,17 +80,25 @@ header("Content-type: text/plain");
 $version = $_GET["version"];
 $updates = "";
 
-if (preg_match("/^1\.1\.0/", $version)) {
-  $updates = "Version: 1.1.1\n" .
+$unstable = "1.1.3";
+$stable = "1.0.0";
+
+if (preg_match("/^1\.1\.[12]/", $version)) {
+  $updates = "Version: " . $unstable . "\n" .
+    "5: Initial drag-and-drop support\n" .
+    "4: Bug fixes\n";
+} else if (preg_match("/^1\.1\.0/", $version)) {
+  $updates = "Version: " . $unstable . "\n" .
     "6: Enhanced motor selection\n" .
     "5: Rewritten simulation code\n" .
+    "5: Drag-and-drop support\n" .
     "4: Bug fixes";
 } else if (preg_match("/^0\.9\.6/", $version)) {
-  $updates = "Version: 1.0.0\n" .
+  $updates = "Version: " . $stable . "\n" .
     "6: Hundreds of new thrustcurves\n" .
     "5: Bug fixes";
 } else if (preg_match("/^0\.9\.[45]/", $version)) {
-  $updates = "Version: 1.0.0\n" .
+  $updates = "Version: " . $stable . "\n" .
     "7: Hundreds of new thrustcurves\n" .
     "6: Aerodynamic computation updates\n" .
     "5: Numerous bug fixes";
index cc5e575dc4c7acce1b0b6fb5c0dc0a06d08bf8b7..ddb2b5be3bead929bc9e683dde699f41782978c6 100644 (file)
   <div class="content">
 <div class="news">
       <h2>Recent news:</h2>
+  <p><span class="date">6.10.2010:</span> Version 1.1.3 is
+    <a href="download.html">released</a>!</p>
+  <p>This release includes support for moving and copying components
+    in the component tree using drag-and-drop.  Use normal DnD for
+    moving, and control-drag for copy.  This release also fixes a
+    severe bug in the undo system.</p>
+  <p><span class="date">7.9.2010:</span> A bug-fix version 1.1.2 is
+    <a href="download.html">released</a>!</p>
+  <p>This release fixes a severe bug in 1.1.1 that prevented adding stages
+    to rocket designs.  Users are recommended to upgrade.</p>
   <p><span class="date">3.9.2010:</span> Version 1.1.1 is
     <a href="download.html">released</a>!</p>
   <p>For this release a major part of the simulator code has been
     rocket design files (.RKT), thanks to contributions by Doug
     Pedrick.  It's also the kick-off of the 1.1 development branch of
     OpenRocket.</p>
-  <p><span class="date">10.3.2010:</span> Version 1.0.0 is
-    <a href="download.html">released</a>!</p>
-  <p>This release includes several hundred new thrustcurves from
-    <a href="http://www.thrustcurve.org/">thrustcurve.org</a> and some
-    bug fixes.  It is also a stable milestone, after which development
-    on larger features will be started in a 1.1 branch.  Look out
-    for it!  :-)</p>
 </div>
     <div class="contentholder">
       <h2>Ready packages</h2>
         <a href="http://sourceforge.net/donate/index.php?group_id=260357"><img src="project-support.jpg" width="88" height="32" alt="Support This Project" /></a>
       </div>
         <div class="downloadbox">
-    <a class="main" href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.1/OpenRocket-1.1.1.jar/download">
+    <a class="main" href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.3/OpenRocket-1.1.3.jar/download">
       <strong>Download now!</strong>
-      <span>OpenRocket-1.1.1.jar</span>
+      <span>OpenRocket-1.1.3.jar</span>
     </a>
     <span class="alternative">
-      <a href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.1/ReleaseNotes/view">Release notes</a> |
-            <a href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.1/OpenRocket-1.1.1-src.zip/download">Source code</a>
+      <a href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.3/ReleaseNotes/view">Release notes</a> |
+            <a href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.3/OpenRocket-1.1.3-src.zip/download">Source code</a>
     </span>
   </div>
       <h3>Stable release</h3>
        Windows) by double-clicking the package icon.  No installation is
        required.</p>
       <p>From the command line OpenRocket can be started by
-      <span class="command">java -jar OpenRocket-1.1.1.jar</span></p>
+      <span class="command">java -jar OpenRocket-1.1.3.jar</span></p>
        
     </div>
     <div class="clear"></div>
index d6346e517d7e12f02a9f9e294f7830329ce8ac80..aa10d745a8342bac831c6d0a04ade368a986e602 100644 (file)
     <h2>Introduction</h2>
     <div class="rightpane">
         <div class="downloadbox">
-    <a class="main" href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.1/OpenRocket-1.1.1.jar/download">
+    <a class="main" href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.3/OpenRocket-1.1.3.jar/download">
       <strong>Download now!</strong>
-      <span>OpenRocket-1.1.1.jar</span>
+      <span>OpenRocket-1.1.3.jar</span>
     </a>
     <span class="alternative">
-      <a href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.1/ReleaseNotes/view">Release notes</a> |
+      <a href="https://sourceforge.net/projects/openrocket/files/openrocket/1.1.3/ReleaseNotes/view">Release notes</a> |
             <a href="download.html">Other versions</a>
     </span>
   </div>
     <div class="clear"></div>
 <div class="news">
       <h2>News</h2>
+  <p><span class="date">6.10.2010:</span> Version 1.1.3 is
+    <a href="download.html">released</a>!</p>
+  <p>This release includes support for moving and copying components
+    in the component tree using drag-and-drop.  Use normal DnD for
+    moving, and control-drag for copy.  This release also fixes a
+    severe bug in the undo system.</p>
+  <p><span class="date">7.9.2010:</span> A bug-fix version 1.1.2 is
+    <a href="download.html">released</a>!</p>
+  <p>This release fixes a severe bug in 1.1.1 that prevented adding stages
+    to rocket designs.  Users are recommended to upgrade.</p>
   <p><span class="date">3.9.2010:</span> Version 1.1.1 is
     <a href="download.html">released</a>!</p>
   <p>For this release a major part of the simulator code has been
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
index c432d350826987be908cb973351d85b8451a5ad3..8fd48dea324333f44d93f8f76881ac133e41ff6f 100644 (file)
@@ -1,5 +1,5 @@
 <set stableversion="1.0.0">
-<set developmentversion="1.1.1">
+<set developmentversion="1.1.3">
 <set version="${developmentversion}">
 
 <def name="downloadbox">
index 5257bbcf6a50a4021ccd568308862aaf860aaf58..03938a0d666045fe7742c4fa4aaa4988e32ca0cf 100644 (file)
@@ -9,6 +9,20 @@
 
   <!--- Remember to move the position of "onlyrecent" below! --->
 
+  <p><span class="date">6.10.2010:</span> Version 1.1.3 is 
+    <a href="download.html">released</a>!</p>
+
+  <p>This release includes support for moving and copying components
+    in the component tree using drag-and-drop.  Use normal DnD for
+    moving, and control-drag for copy.  This release also fixes a
+    severe bug in the undo system.</p>
+
+  <p><span class="date">7.9.2010:</span> A bug-fix version 1.1.2 is 
+    <a href="download.html">released</a>!</p>
+
+  <p>This release fixes a severe bug in 1.1.1 that prevented adding stages
+    to rocket designs.  Users are recommended to upgrade.</p>
+
   <p><span class="date">3.9.2010:</span> Version 1.1.1 is 
     <a href="download.html">released</a>!</p>
 
     Pedrick.  It's also the kick-off of the 1.1 development branch of
     OpenRocket.</p>
 
+
+  <if not onlyrecent><!--- Older items not shown on download page:  --->
+
+
   <p><span class="date">10.3.2010:</span> Version 1.0.0 is 
     <a href="download.html">released</a>!</p>
 
     on larger features will be started in a 1.1 branch.  Look out
     for it!  :-)</p>
 
-
-  <if not onlyrecent><!--- Older items not shown on download page:  --->
-
-
   <p><span class="date">17.2.2010:</span> Version 0.9.6 is
     <a href="download.html">released</a>!</p>