+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
--- /dev/null
+<?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 > 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=<-</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=<-
+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=<-
+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=<-
+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=<-
+</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 < 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=<-
+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=<-
+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=<-
+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=<-
+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=<-
+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=<-
+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=<-
+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=<-
+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
cond.atmosphericConditions = atmosphericConditions.clone();
return cond;
} catch (CloneNotSupportedException e) {
- throw new BugException("BUG: clone not supported!", e);
+ throw new BugException("clone not supported!", e);
}
}
}
- assert (r1 < r2); // Tube and boattail have been checked already
-
-
// All nose cones and shoulders from pre-calculated and interpolating
if (interpolator == null) {
calculateNoseInterpolator();
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
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;
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;
package net.sf.openrocket.document;
-//TODO: LOW: move class somewhere else?
import java.awt.event.ActionEvent;
import java.io.File;
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.
*/
* 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();
}
-
-
+
+
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;
}
-
-
-
+
+
+
@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));
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);
}
* @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()}
* @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;
}
/**
* 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);
}
* 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;
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();
}
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;
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;
}
-
+ /**
+ * 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());
}
}
-
+ /**
+ * 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
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.
*/
// 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);
}
}
}
}
-
-
+ /**
+ * Return the rocket associated with this simulation.
+ *
+ * @return the rocket.
+ */
+ public Rocket getRocket() {
+ return rocket;
+ }
+
+
/**
* Return a newly created Configuration for this simulation. The configuration
* has the motor ID set and all stages active.
public abstract class RocketLoader {
protected final WarningSet warnings = new WarningSet();
-
+
/**
* Loads a rocket from the specified File object.
public final OpenRocketDocument load(File source) throws RocketLoadException {
warnings.clear();
InputStream stream = null;
-
+
try {
stream = new BufferedInputStream(new FileInputStream(source));
}
}
}
-
+
/**
* 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.
*/
protected abstract OpenRocketDocument loadFromStream(InputStream source) throws IOException,
RocketLoadException;
-
-
+
+
public final WarningSet getWarnings() {
return warnings;
*/
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;
* 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;
+ }
}
public Column(String name) {
this.name = name;
}
-
+
/**
* Return the caption of the column.
*/
public Class<?> getColumnClass() {
return Object.class;
}
-
+
/**
* Return the value in this column at the specified row.
*
* @return the value at the specified position.
*/
public abstract Object getValueAt(int row);
+
}
}
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();
}
}
}
-
+
@Override
public int getColumnCount() {
return columns.length;
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);
}
+
}
try {
return (Double)getMethod.invoke(source)*multiplier;
} catch (IllegalArgumentException e) {
- throw new BugException("BUG: Unable to invoke getMethod of "+this, e);
+ throw new BugException("Unable to invoke getMethod of "+this, e);
} catch (IllegalAccessException e) {
- throw new BugException("BUG: Unable to invoke getMethod of "+this, e);
+ throw new BugException("Unable to invoke getMethod of "+this, e);
} catch (InvocationTargetException e) {
throw Reflection.handleWrappedException(e);
}
try {
setMethod.invoke(source, v/multiplier);
} catch (IllegalArgumentException e) {
- throw new BugException("BUG: Unable to invoke setMethod of "+this, e);
+ throw new BugException("Unable to invoke setMethod of "+this, e);
} catch (IllegalAccessException e) {
- throw new BugException("BUG: Unable to invoke setMethod of "+this, e);
+ throw new BugException("Unable to invoke setMethod of "+this, e);
} catch (InvocationTargetException e) {
throw Reflection.handleWrappedException(e);
}
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);
}
--- /dev/null
+package net.sf.openrocket.gui.dialogs;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTable;
+import javax.swing.JTextArea;
+import javax.swing.ListSelectionModel;
+import javax.swing.RowFilter;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableModel;
+import javax.swing.table.TableRowSorter;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.adaptors.Column;
+import net.sf.openrocket.gui.adaptors.ColumnTableModel;
+import net.sf.openrocket.gui.components.SelectableLabel;
+import net.sf.openrocket.logging.DelegatorLogger;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.logging.LogLevel;
+import net.sf.openrocket.logging.LogLevelBufferLogger;
+import net.sf.openrocket.logging.LogLine;
+import net.sf.openrocket.logging.StackTraceWriter;
+import net.sf.openrocket.logging.TraceException;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.NumericComparator;
+
+public class DebugLogDialog extends JDialog {
+ private static final LogHelper log = Application.getLogger();
+
+ private static final int POLL_TIME = 250;
+ private static final String STACK_TRACE_MARK = "\uFF01";
+
+ private static final EnumMap<LogLevel, Color> backgroundColors = new EnumMap<LogLevel, Color>(LogLevel.class);
+ static {
+ for (LogLevel l : LogLevel.values()) {
+ // Just to ensure every level has a bg color
+ backgroundColors.put(l, Color.ORANGE);
+ }
+ final int hi = 255;
+ final int lo = 150;
+ backgroundColors.put(LogLevel.ERROR, new Color(hi, lo, lo));
+ backgroundColors.put(LogLevel.WARN, new Color(hi, (hi + lo) / 2, lo));
+ backgroundColors.put(LogLevel.USER, new Color(lo, lo, hi));
+ backgroundColors.put(LogLevel.INFO, new Color(hi, hi, lo));
+ backgroundColors.put(LogLevel.DEBUG, new Color(lo, hi, lo));
+ backgroundColors.put(LogLevel.VBOSE, new Color(lo, hi, (hi + lo) / 2));
+ }
+
+ /** Buffer containing the log lines displayed */
+ private final List<LogLine> buffer = new ArrayList<LogLine>();
+
+ /** Queue of log lines to be added to the displayed buffer */
+ private final Queue<LogLine> queue = new ConcurrentLinkedQueue<LogLine>();
+
+ private final DelegatorLogger delegator;
+ private final LogListener logListener;
+
+ private final EnumMap<LogLevel, JCheckBox> filterButtons = new EnumMap<LogLevel, JCheckBox>(LogLevel.class);
+ private final JCheckBox followBox;
+ private final Timer timer;
+
+
+ private final JTable table;
+ private final ColumnTableModel model;
+ private final TableRowSorter<TableModel> sorter;
+
+ private final SelectableLabel numberLabel;
+ private final SelectableLabel timeLabel;
+ private final SelectableLabel levelLabel;
+ private final SelectableLabel locationLabel;
+ private final SelectableLabel messageLabel;
+ private final JTextArea stackTraceLabel;
+
+ public DebugLogDialog(Window parent) {
+ super(parent, "OpenRocket debug log");
+
+ // Start listening to log lines
+ LogHelper applicationLog = Application.getLogger();
+ if (applicationLog instanceof DelegatorLogger) {
+ log.info("Adding log listener");
+ delegator = (DelegatorLogger) applicationLog;
+ logListener = new LogListener();
+ delegator.addLogger(logListener);
+ } else {
+ log.warn("Application log is not a DelegatorLogger");
+ delegator = null;
+ logListener = null;
+ }
+
+ // Fetch old log lines
+ LogLevelBufferLogger bufferLogger = Application.getLogBuffer();
+ if (bufferLogger != null) {
+ buffer.addAll(bufferLogger.getLogs());
+ } else {
+ log.warn("Application does not have a log buffer");
+ }
+
+
+ // Create the UI
+ JPanel mainPanel = new JPanel(new MigLayout("fill"));
+ this.add(mainPanel);
+
+
+ JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+ split.setDividerLocation(0.7);
+ mainPanel.add(split, "grow");
+
+ // Top panel
+ JPanel panel = new JPanel(new MigLayout("fill"));
+ split.add(panel);
+
+ panel.add(new JLabel("Display log lines:"), "gapright para, split");
+ for (LogLevel l : LogLevel.values()) {
+ JCheckBox box = new JCheckBox(l.toString());
+ box.setSelected(true);
+ box.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ sorter.setRowFilter(new LogFilter());
+ }
+ });
+ panel.add(box, "gapright unrel");
+ filterButtons.put(l, box);
+ }
+
+ followBox = new JCheckBox("Follow");
+ followBox.setSelected(true);
+ panel.add(followBox, "skip, gapright para, right");
+
+ JButton clear = new JButton("Clear");
+ clear.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ log.user("Clearing log buffer");
+ buffer.clear();
+ queue.clear();
+ model.fireTableDataChanged();
+ }
+ });
+ panel.add(clear, "right, wrap");
+
+
+
+ // Create the table model
+ model = new ColumnTableModel(
+
+ new Column("#") {
+ @Override
+ public Object getValueAt(int row) {
+ return buffer.get(row).getLogCount();
+ }
+
+ @Override
+ public int getDefaultWidth() {
+ return 60;
+ }
+ },
+ new Column("Time") {
+ @Override
+ public Object getValueAt(int row) {
+ return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0);
+ }
+
+ @Override
+ public int getDefaultWidth() {
+ return 60;
+ }
+ },
+ new Column("Level") {
+ @Override
+ public Object getValueAt(int row) {
+ return buffer.get(row).getLevel();
+ }
+
+ @Override
+ public int getDefaultWidth() {
+ return 60;
+ }
+ },
+ new Column("") {
+ @Override
+ public Object getValueAt(int row) {
+ if (buffer.get(row).getCause() != null) {
+ return STACK_TRACE_MARK;
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ public int getExactWidth() {
+ return 16;
+ }
+ },
+ new Column("Location") {
+ @Override
+ public Object getValueAt(int row) {
+ TraceException e = buffer.get(row).getTrace();
+ if (e != null) {
+ return e.getMessage();
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ public int getDefaultWidth() {
+ return 200;
+ }
+ },
+ new Column("Message") {
+ @Override
+ public Object getValueAt(int row) {
+ return buffer.get(row).getMessage();
+ }
+
+ @Override
+ public int getDefaultWidth() {
+ return 580;
+ }
+ }
+
+ ) {
+ @Override
+ public int getRowCount() {
+ return buffer.size();
+ }
+ };
+
+ table = new JTable(model);
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ table.setSelectionBackground(Color.LIGHT_GRAY);
+ table.setSelectionForeground(Color.BLACK);
+ model.setColumnWidths(table.getColumnModel());
+ table.setDefaultRenderer(Object.class, new Renderer());
+
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ int row = table.getSelectedRow();
+ if (row >= 0) {
+ row = sorter.convertRowIndexToModel(row);
+ }
+ updateSelected(row);
+ }
+ });
+
+ sorter = new TableRowSorter<TableModel>(model);
+ sorter.setComparator(0, NumericComparator.INSTANCE);
+ sorter.setComparator(1, NumericComparator.INSTANCE);
+ sorter.setComparator(4, new LocationComparator());
+ table.setRowSorter(sorter);
+
+
+ panel.add(new JScrollPane(table), "span, grow, width " +
+ (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) +
+ "px, height 400px");
+
+
+ panel = new JPanel(new MigLayout("fill"));
+ split.add(panel);
+
+ panel.add(new JLabel("Log line number:"), "split, gapright rel");
+ numberLabel = new SelectableLabel();
+ panel.add(numberLabel, "width 70lp, gapright para");
+
+ panel.add(new JLabel("Time:"), "split, gapright rel");
+ timeLabel = new SelectableLabel();
+ panel.add(timeLabel, "width 70lp, gapright para");
+
+ panel.add(new JLabel("Level:"), "split, gapright rel");
+ levelLabel = new SelectableLabel();
+ panel.add(levelLabel, "width 70lp, gapright para");
+
+ panel.add(new JLabel("Location:"), "split, gapright rel");
+ locationLabel = new SelectableLabel();
+ panel.add(locationLabel, "growx, wrap unrel");
+
+ panel.add(new JLabel("Log message:"), "split, gapright rel");
+ messageLabel = new SelectableLabel();
+ panel.add(messageLabel, "growx, wrap para");
+
+ panel.add(new JLabel("Stack trace:"), "wrap rel");
+ stackTraceLabel = new JTextArea(8, 80);
+ stackTraceLabel.setEditable(false);
+ GUIUtil.changeFontSize(stackTraceLabel, -2);
+ panel.add(new JScrollPane(stackTraceLabel), "grow");
+
+
+ JButton close = new JButton("Close");
+ close.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ DebugLogDialog.this.dispose();
+ }
+ });
+ mainPanel.add(close, "newline para, right, tag ok");
+
+
+ // Use timer to purge the queue so as not to overwhelm the EDT with events
+ timer = new Timer(POLL_TIME, new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ purgeQueue();
+ }
+ });
+ timer.setRepeats(true);
+ timer.start();
+
+ this.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosed(WindowEvent e) {
+ log.user("Closing debug log dialog");
+ timer.stop();
+ if (delegator != null) {
+ log.info("Removing log listener");
+ delegator.removeLogger(logListener);
+ }
+ }
+ });
+
+ GUIUtil.setDisposableDialogOptions(this, close);
+ followBox.requestFocus();
+ }
+
+
+
+ private void updateSelected(int row) {
+ if (row < 0) {
+
+ numberLabel.setText("");
+ timeLabel.setText("");
+ levelLabel.setText("");
+ locationLabel.setText("");
+ messageLabel.setText("");
+ stackTraceLabel.setText("");
+
+ } else {
+
+ LogLine line = buffer.get(row);
+ numberLabel.setText("" + line.getLogCount());
+ timeLabel.setText(String.format("%.3f s", line.getTimestamp() / 1000.0));
+ levelLabel.setText(line.getLevel().toString());
+ TraceException e = line.getTrace();
+ if (e != null) {
+ locationLabel.setText(e.getMessage());
+ } else {
+ locationLabel.setText("-");
+ }
+ messageLabel.setText(line.getMessage());
+ Throwable t = line.getCause();
+ if (t != null) {
+ StackTraceWriter stw = new StackTraceWriter();
+ PrintWriter pw = new PrintWriter(stw);
+ t.printStackTrace(pw);
+ pw.flush();
+ stackTraceLabel.setText(stw.toString());
+ stackTraceLabel.setCaretPosition(0);
+ } else {
+ stackTraceLabel.setText("");
+ }
+
+ }
+ }
+
+
+ /**
+ * Check whether a row signifies a number of missing rows. This check is "heuristic"
+ * and checks whether the timestamp is zero and the message starts with "---".
+ */
+ private boolean isExcludedRow(int row) {
+ LogLine line = buffer.get(row);
+ return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---"));
+ }
+
+
+ /**
+ * Purge the queue of incoming log lines. This is called periodically from the EDT, and
+ * it adds any lines in the queue to the buffer, and fires a table event.
+ */
+ private void purgeQueue() {
+ int start = buffer.size();
+
+ LogLine line;
+ while ((line = queue.poll()) != null) {
+ buffer.add(line);
+ }
+
+ int end = buffer.size() - 1;
+ if (end >= start) {
+ model.fireTableRowsInserted(start, end);
+ if (followBox.isSelected()) {
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ Rectangle rect = table.getCellRect(1000000000, 1, true);
+ table.scrollRectToVisible(rect);
+ }
+ });
+ }
+ }
+ }
+
+
+ /**
+ * A logger that adds log lines to the queue. This method may be called from any
+ * thread, and therefore must be thread-safe.
+ */
+ private class LogListener extends LogHelper {
+ @Override
+ public void log(LogLine line) {
+ queue.add(line);
+ }
+ }
+
+ private class LogFilter extends RowFilter<TableModel, Integer> {
+
+ @Override
+ public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
+ int index = entry.getIdentifier();
+ LogLine line = buffer.get(index);
+ return filterButtons.get(line.getLevel()).isSelected();
+ }
+
+ }
+
+
+ private class Renderer extends JLabel implements TableCellRenderer {
+ @Override
+ public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus,
+ int row, int column) {
+ Color fg, bg;
+
+ row = sorter.convertRowIndexToModel(row);
+
+ if (STACK_TRACE_MARK.equals(value)) {
+ fg = Color.RED;
+ } else {
+ fg = table1.getForeground();
+ }
+ bg = backgroundColors.get(buffer.get(row).getLevel());
+
+ if (isSelected) {
+ bg = bg.darker();
+ } else if (isExcludedRow(row)) {
+ bg = bg.brighter();
+ }
+
+ this.setForeground(fg);
+ this.setBackground(bg);
+
+ this.setOpaque(true);
+ this.setText(value.toString());
+
+ return this;
+ }
+ }
+
+
+ private class LocationComparator implements Comparator<Object> {
+ private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$");
+
+ @Override
+ public int compare(Object o1, Object o2) {
+ String s1 = o1.toString();
+ String s2 = o2.toString();
+
+ Matcher m1 = splitPattern.matcher(s1);
+ Matcher m2 = splitPattern.matcher(s2);
+
+ if (m1.matches() && m2.matches()) {
+ String class1 = m1.group(1);
+ String pos1 = m1.group(2);
+ String class2 = m2.group(1);
+ String pos2 = m2.group(2);
+
+ if (class1.equals(class2)) {
+ return NumericComparator.INSTANCE.compare(pos1, pos2);
+ } else {
+ return class1.compareTo(class2);
+ }
+ }
+
+ return s1.compareTo(s2);
+ }
+
+ }
+
+}
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;
* @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;
/** 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) {
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) {
* <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) {
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;
}
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;
}
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");
+++ /dev/null
-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);
- }
-
-}
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
-import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import javax.swing.Action;
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;
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;
import net.sf.openrocket.gui.dialogs.AboutDialog;
import net.sf.openrocket.gui.dialogs.BugReportDialog;
import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog;
+import net.sf.openrocket.gui.dialogs.DebugLogDialog;
import net.sf.openrocket.gui.dialogs.ExampleDesignDialog;
import net.sf.openrocket.gui.dialogs.LicenseDialog;
import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog;
import net.sf.openrocket.gui.dialogs.SwingWorkerDialog;
-import net.sf.openrocket.gui.dialogs.UpdateInfoDialog;
import net.sf.openrocket.gui.dialogs.WarningDialog;
import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog;
+import net.sf.openrocket.gui.main.componenttree.ComponentTree;
import net.sf.openrocket.gui.scalefigure.RocketPanel;
import net.sf.openrocket.logging.LogHelper;
import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
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
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);
item.setIcon(Icons.FILE_OPEN);
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
+ log.user("Open... selected");
openAction();
}
});
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);
}
}
item.setIcon(Icons.FILE_SAVE);
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
+ log.user("Save selected");
saveAction();
}
});
item.setIcon(Icons.FILE_SAVE_AS);
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
+ log.user("Save as... selected");
saveAsAction();
}
});
item.setIcon(Icons.FILE_CLOSE);
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
+ log.user("Close selected");
closeAction();
}
});
item.setIcon(Icons.FILE_QUIT);
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
+ log.user("Quit selected");
quitAction();
}
});
"preferences");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
+ log.user("Preferences selected");
PreferencesDialog.showPreferences();
}
});
"separately");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
+ log.user("Component analysis selected");
ComponentAnalysisDialog.showDialog(rocketpanel);
}
});
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);
}
});
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 " +
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:",
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);
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);
});
menu.add(item);
-
-
menu.addSeparator();
item = new JMenuItem("Exception here");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
+ log.user("Exception here selected");
throw new RuntimeException("Testing exception from menu action listener");
}
});
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() {
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() {
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;
}
}
-
+ /**
+ * 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();
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)) {
}
}
} 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);
* @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;
}
if (cause instanceof FileNotFoundException) {
+ log.warn("File not found", cause);
JOptionPane.showMessageDialog(parent,
"File not found: " + filename,
"Error opening file", JOptionPane.ERROR_MESSAGE);
} else if (cause instanceof RocketLoadException) {
+ log.warn("Error loading the file", cause);
JOptionPane.showMessageDialog(parent,
"Unable to open file '" + filename + "': "
+ cause.getMessage(),
}
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 + ".",
doc.setSaved(true);
// Open the frame
+ log.debug("Opening new frame with the document");
BasicFrame frame = new BasicFrame(doc);
frame.setVisible(true);
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);
}
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;
}
"Writing " + file.getName() + "...", worker)) {
// User cancelled the save
+ log.user("User cancelled the save, deleting the file");
file.delete();
return false;
}
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);
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. " +
JOptionPane.QUESTION_MESSAGE);
if (result == JOptionPane.YES_OPTION) {
// Save
- if (!saveAction())
- return false; // If save was interrupted
+ log.user("User requested file save");
+ if (!saveAction()) {
+ log.info("File save was interrupted, not closing");
+ return false;
+ }
} else if (result == JOptionPane.NO_OPTION) {
// Don't save: No-op
+ log.user("User requested to discard design");
} else {
// Cancel or close
+ log.user("User cancelled closing, result=" + result);
return false;
}
}
// Rocket has been saved or discarded
+ log.debug("Disposing window");
this.dispose();
- // TODO: LOW: Close only dialogs that have this frame as their parent
ComponentConfigDialog.hideDialog();
ComponentAnalysisDialog.hideDialog();
frames.remove(this);
- if (frames.isEmpty())
+ if (frames.isEmpty()) {
+ log.info("Last frame closed, exiting");
System.exit(0);
+ }
return true;
}
- /**
- * Closes this frame if it is replaceable.
- */
- public void closeIfReplaceable() {
- if (this.replaceable && document.isSaved()) {
- closeAction();
- }
- }
-
+
/**
* Open a new design window with a basic rocket+stage.
*/
public static void newAction() {
- log.debug("New action initiated");
+ log.info("New action initiated");
+
Rocket rocket = new Rocket();
Stage stage = new Stage();
stage.setName("Sustainer");
* 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);
}
*/
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;
}
* @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;
}
-
}
import net.sf.openrocket.document.OpenRocketDocument;
import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
import net.sf.openrocket.rocketcomponent.BodyComponent;
import net.sf.openrocket.rocketcomponent.BodyTube;
import net.sf.openrocket.rocketcomponent.Bulkhead;
+++ /dev/null
-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;
- }
- }
-
-}
-
-
+++ /dev/null
-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());
- }
-
-}
+++ /dev/null
-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;
- }
-
-}
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 {
import net.sf.openrocket.gui.dialogs.BugReportDialog;
import net.sf.openrocket.logging.LogHelper;
import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.BugException;
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
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)) {
return;
}
- log.warn("Handling uncaught exception on thread=" + thread, throwable);
+ log.error("Handling uncaught exception on thread=" + thread, throwable);
throwable.printStackTrace();
if (handling) {
if (!(exception instanceof InternalException)) {
log.error(1, "Error occurred", exception);
}
- final ExceptionHandler handler = instance;
final Thread thread = Thread.currentThread();
+ final ExceptionHandler handler = instance;
+
+ if (handler == null) {
+ // Not initialized, throw the exception
+ throw new BugException("Error condition before exception handling has been initialized", exception);
+ }
try {
-
- if (handler == null) {
- // Not initialized, simply print the exception
- log.error("Exception handler not initialized");
- exception.printStackTrace();
- return;
- }
-
if (SwingUtilities.isEventDispatchThread()) {
log.info("Running in EDT, showing dialog");
handler.showDialog(thread, exception);
--- /dev/null
+package net.sf.openrocket.gui.main.componenttree;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+
+import javax.swing.DropMode;
+import javax.swing.Icon;
+import javax.swing.JTree;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+
+
+public class ComponentTree extends JTree {
+ private static final long serialVersionUID = 1L;
+
+ public ComponentTree(OpenRocketDocument document) {
+ super();
+ this.setModel(new ComponentTreeModel(document.getRocket(), this));
+ this.setToggleClickCount(0);
+
+ javax.swing.plaf.basic.BasicTreeUI ui = new javax.swing.plaf.basic.BasicTreeUI();
+ this.setUI(ui);
+
+ ui.setExpandedIcon(TreeIcon.MINUS);
+ ui.setCollapsedIcon(TreeIcon.PLUS);
+
+ ui.setLeftChildIndent(15);
+
+
+ setBackground(Color.WHITE);
+ setShowsRootHandles(false);
+
+ setCellRenderer(new ComponentTreeRenderer());
+
+ this.setDragEnabled(true);
+ this.setDropMode(DropMode.INSERT);
+ this.setTransferHandler(new ComponentTreeTransferHandler(document));
+
+ // Expand whole tree by default
+ expandTree();
+ }
+
+ public void expandTree() {
+ for (int i = 0; i < getRowCount(); i++)
+ expandRow(i);
+
+ }
+
+ private static class TreeIcon implements Icon {
+ public static final Icon PLUS = new TreeIcon(true);
+ public static final Icon MINUS = new TreeIcon(false);
+
+ // Implementation:
+
+ private final static int width = 9;
+ private final static int height = 9;
+ private final static BasicStroke stroke = new BasicStroke(2);
+ private boolean plus;
+
+ private TreeIcon(boolean plus) {
+ this.plus = plus;
+ }
+
+ public void paintIcon(Component c, Graphics g, int x, int y) {
+ Graphics2D g2 = (Graphics2D) g.create();
+
+ g2.setColor(Color.WHITE);
+ g2.fillRect(x, y, width, height);
+
+ g2.setColor(Color.DARK_GRAY);
+ g2.drawRect(x, y, width, height);
+
+ g2.setStroke(stroke);
+ g2.drawLine(x + 3, y + (height + 1) / 2, x + width - 2, y + (height + 1) / 2);
+ if (plus)
+ g2.drawLine(x + (width + 1) / 2, y + 3, x + (width + 1) / 2, y + height - 2);
+
+ g2.dispose();
+ }
+
+ public int getIconWidth() {
+ return width;
+ }
+
+ public int getIconHeight() {
+ return height;
+ }
+ }
+
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.main.componenttree;
+
+
+
+import java.awt.Component;
+
+import javax.swing.JTree;
+import javax.swing.tree.DefaultTreeCellRenderer;
+
+import net.sf.openrocket.gui.main.ComponentIcons;
+
+public class ComponentTreeRenderer extends DefaultTreeCellRenderer {
+
+ @Override
+ public Component getTreeCellRendererComponent(
+ JTree tree,
+ Object value,
+ boolean sel,
+ boolean expanded,
+ boolean leaf,
+ int row,
+ boolean hasFocus) {
+
+ super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+
+ setIcon(ComponentIcons.getSmallIcon(value.getClass()));
+
+ return this;
+ }
+
+}
--- /dev/null
+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;
+ }
+
+ }
+}
--- /dev/null
+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);
+ }
+
+}
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)) {
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;
// 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());
/**
* 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.
*
/**
* Log a LogLine object. This method needs to be able to cope with multiple threads
- * accessing it concurrently (for example being synchronized).
+ * accessing it concurrently (for example by being synchronized).
*
* @param line the LogLine to log.
*/
public abstract void log(LogLine line);
+ /**
+ * Log using VBOSE level.
+ *
+ * @param message the logged message (may be null).
+ */
+ public void verbose(String message) {
+ log(createLogLine(0, LogLevel.VBOSE, message, null));
+ }
+
+ /**
+ * Log using VBOSE level.
+ *
+ * @param message the logged message (may be null).
+ * @param cause the causing exception (may be null).
+ */
+ public void verbose(String message, Throwable cause) {
+ log(createLogLine(0, LogLevel.VBOSE, message, cause));
+ }
+
+ /**
+ * Log using VBOSE level.
+ *
+ * @param levels number of additional levels of stack trace to include.
+ * @param message the logged message (may be null).
+ */
+ public void verbose(int levels, String message) {
+ log(createLogLine(levels, LogLevel.VBOSE, message, null));
+ }
+
+ /**
+ * Log using VBOSE level.
+ *
+ * @param levels number of additional levels of stack trace to include.
+ * @param message the logged message (may be null).
+ * @param cause the causing exception (may be null).
+ */
+ public void verbose(int levels, String message, Throwable cause) {
+ log(createLogLine(levels, LogLevel.VBOSE, message, cause));
+ }
+
/**
* Log using DEBUG level.
}
-
+
/**
* Log using the provided log level.
*
}
-
+
/**
* Instantiates, logs and throws a BugException. The message is logged at
* ERROR level.
-
+
/**
* Create a LogLine object from the provided information. This method must be
* called directly from the called method in order for the trace position
*
* @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)) {
* 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
* user actions from a bounded log buffer.
*/
USER,
+
/**
* Level for indicating general level actions the software is performing and
* other notable events during execution (dialogs shown, simulations run etc).
*/
INFO,
+
/**
- * Level for indicating mid-results and other debugging information. No debug
- * logging should be performed in performance-intensive places to avoid slowing
- * the system. On the other hand for example cached results should be logged.
+ * Level for indicating mid-results, outcomes of methods and other debugging
+ * information. The data logged should be of value when analyzing error
+ * conditions and what has caused them. Places that are called repeatedly
+ * during e.g. flight simulation should use the VBOSE level instead.
*/
- DEBUG;
+ DEBUG,
+ /**
+ * Level of verbose debug logging to be used in areas which are called repeatedly,
+ * such as computational methods used in simulations. This level is separated to
+ * allow filtering out the verbose logs generated during simulations, DnD etc.
+ * from the normal debug logs.
+ */
+ VBOSE;
+
/** The maximum length of a level textual description */
public static final int LENGTH;
static {
int length = 0;
- for (LogLevel l: LogLevel.values()) {
+ for (LogLevel l : LogLevel.values()) {
length = Math.max(length, l.toString().length());
}
LENGTH = length;
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>.
* @return the corresponding log level, of defaultLevel.
*/
public static LogLevel fromString(String value, LogLevel defaultLevel) {
-
+
// Normalize the string
if (value == null) {
return defaultLevel;
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);
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;
*/
public TraceException(int minLevel, int maxLevel) {
if (minLevel > maxLevel || minLevel < 0) {
- throw new IllegalArgumentException("minLevel="+minLevel+" maxLevel="+maxLevel);
+ throw new IllegalArgumentException("minLevel=" + minLevel + " maxLevel=" + maxLevel);
}
this.minLevel = minLevel;
this.maxLevel = maxLevel;
}
-
-
+
+
/**
* Get the description of the code position as provided in the constructor.
*/
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]));
}
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(')');
try {
return (AtmosphericConditions) super.clone();
} catch (CloneNotSupportedException e) {
- throw new BugException("BUG: CloneNotSupportedException encountered!");
+ throw new BugException("CloneNotSupportedException encountered!");
}
}
+++ /dev/null
-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);
-
-}
+++ /dev/null
-package net.sf.openrocket.optimization;
-
-public interface FunctionCache {
-
- public double getValue(Point point);
-
- public void clearCache();
-
- public Function getFunction();
-
- public void setFunction(Function function);
-
-}
+++ /dev/null
-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);
- }
-
-}
+++ /dev/null
-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);
- }
-
-}
+++ /dev/null
-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);
- }
-
-}
+++ /dev/null
-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);
-
-
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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);
-
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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);
-}
+++ /dev/null
-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);
- }
-}
+++ /dev/null
-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;
- }
-}
--- /dev/null
+package net.sf.openrocket.optimization.general;
+
+/**
+ * An interface defining an optimizable function.
+ * <p>
+ * Some function optimizers require that the function is thread-safe.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface Function {
+
+ /**
+ * Evaluate the function at the specified point.
+ * <p>
+ * If the function evaluation is slow, then this method should abort the computation if
+ * the thread is interrupted.
+ *
+ * @param point the point at which to evaluate the function.
+ * @return the function value.
+ * @throws InterruptedException if the thread was interrupted before function evaluation was completed.
+ */
+ public double evaluate(Point point) throws InterruptedException;
+
+
+ /**
+ * Return a cached value of the function at the specified point. This allows efficient
+ * caching of old values even between calls to optimization methods. This method should
+ * NOT evaluate the function except in special cases (e.g. the point is outside of the
+ * function domain).
+ * <p>
+ * Note that it is allowed to always allowed to return <code>Double.NaN</code>, especially
+ * for functions that are fast to evaluate.
+ *
+ * @param point the point of function evaluation.
+ * @return the function value, or <code>Double.NaN</code> if the function value has not been
+ * evaluated at this point.
+ */
+ public double preComputed(Point point);
+
+}
--- /dev/null
+package net.sf.openrocket.optimization.general;
+
+/**
+ * A storage of cached values of a function. The purpose of this class is to
+ * cache function values
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface FunctionCache {
+
+ public double getValue(Point point);
+
+ public void clearCache();
+
+ public Function getFunction();
+
+ public void setFunction(Function function);
+
+}
--- /dev/null
+package net.sf.openrocket.optimization.general;
+
+/**
+ * An interface for a function optimization algorithm. The function is evaluated
+ * via a function cache.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface FunctionOptimizer {
+
+ /**
+ * Perform optimization on the function. The optimization control is called to control
+ * when optimization is stopped.
+ *
+ * @param initial the initial start point of the optimization.
+ * @param control the optimization control.
+ */
+ public void optimize(Point initial, OptimizationController control);
+
+
+ /**
+ * Return the optimum point computed by {@link #optimize(Point, OptimizationController)}.
+ *
+ * @return the optimum point value.
+ * @throws IllegalStateException if {@link #optimize(Point, OptimizationController)} has not been called.
+ */
+ public Point getOptimumPoint();
+
+ /**
+ * Return the function value at the optimum point.
+ *
+ * @return the value at the optimum point.
+ * @throws IllegalStateException if {@link #optimize(Point, OptimizationController)} has not been called.
+ */
+ public double getOptimumValue();
+
+
+ /**
+ * Return the function cache used by this optimization algorithm.
+ *
+ * @return the function cache.
+ */
+ public FunctionCache getFunctionCache();
+
+ /**
+ * Set the function cache that provides the function values for this algorithm.
+ * Some algorithms may require the function cache to be an instance of
+ * ParallelFunctionCache.
+ *
+ * @param functionCache the function cache.
+ */
+ public void setFunctionCache(FunctionCache functionCache);
+
+
+}
--- /dev/null
+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);
+
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.optimization.general;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An implementation of a ParallelFunctionCache that evaluates function values
+ * in parallel and caches them. This allows pre-calculating possibly required
+ * function values beforehand. If values are not required after all, the
+ * computation can be aborted assuming the function evaluation supports it.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ParallelExecutorCache implements ParallelFunctionCache {
+
+ private final Map<Point, Double> functionCache = new HashMap<Point, Double>();
+ private final Map<Point, Future<Double>> futureMap = new HashMap<Point, Future<Double>>();
+
+ private ExecutorService executor;
+
+ private Function function;
+
+
+
+ public ParallelExecutorCache() {
+ this(Runtime.getRuntime().availableProcessors());
+ }
+
+ public ParallelExecutorCache(int threadCount) {
+ executor = new ThreadPoolExecutor(threadCount, threadCount, 60, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<Runnable>(),
+ new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setDaemon(true);
+ return t;
+ }
+ });
+ }
+
+ public ParallelExecutorCache(ExecutorService executor) {
+ this.executor = executor;
+ }
+
+
+
+ /**
+ * Queue a list of function evaluations at the specified points.
+ *
+ * @param points the points at which to evaluate the function.
+ */
+ public void compute(Collection<Point> points) {
+ for (Point p : points) {
+ compute(p);
+ }
+ }
+
+
+ /**
+ * Queue function evaluation for the specified point.
+ *
+ * @param point the point at which to evaluate the function.
+ */
+ public void compute(Point point) {
+ if (functionCache.containsKey(point)) {
+ // Function has already been evaluated at the point
+ return;
+ }
+
+ if (futureMap.containsKey(point)) {
+ // Function is being evaluated at the point
+ return;
+ }
+
+ double value = function.preComputed(point);
+ if (!Double.isNaN(value)) {
+ // Function value was in function cache
+ functionCache.put(point, value);
+ return;
+ }
+
+ // Submit point for evaluation
+ FunctionCallable callable = new FunctionCallable(function, point);
+ Future<Double> future = executor.submit(callable);
+ futureMap.put(point, future);
+ }
+
+
+ /**
+ * Wait for a collection of points to be computed. After calling this method
+ * the function values are available by calling XXX
+ *
+ * @param points the points to wait for.
+ * @throws InterruptedException if this thread was interrupted while waiting.
+ */
+ public void waitFor(Collection<Point> points) throws InterruptedException {
+ for (Point p : points) {
+ waitFor(p);
+ }
+ }
+
+ /**
+ * Wait for a point to be computed. After calling this method
+ * the function values are available by calling XXX
+ *
+ * @param point the point to wait for.
+ * @throws InterruptedException if this thread was interrupted while waiting.
+ */
+ public void waitFor(Point point) throws InterruptedException {
+ if (functionCache.containsKey(point)) {
+ return;
+ }
+
+ Future<Double> future = futureMap.get(point);
+ if (future == null) {
+ throw new IllegalStateException("waitFor called for " + point + " but it is not being computed");
+ }
+
+ try {
+ double value = future.get();
+ functionCache.put(point, value);
+ } catch (ExecutionException e) {
+ throw new IllegalStateException("Function threw exception while processing", e.getCause());
+ }
+ }
+
+
+ /**
+ * Abort the computation of the specified point. If computation has ended,
+ * the result is stored in the function cache anyway.
+ *
+ * @param points the points to abort.
+ * @return a list of the points that have been computed anyway
+ */
+ public List<Point> abort(Collection<Point> points) {
+ List<Point> computed = new ArrayList<Point>(Math.min(points.size(), 10));
+
+ for (Point p : points) {
+ if (abort(p)) {
+ computed.add(p);
+ }
+ }
+
+ return computed;
+ }
+
+
+ /**
+ * Abort the computation of the specified point. If computation has ended,
+ * the result is stored in the function cache anyway.
+ *
+ * @param point the point to abort.
+ * @return <code>true</code> if the point has been computed anyway, <code>false</code> if not.
+ */
+ public boolean abort(Point point) {
+ if (functionCache.containsKey(point)) {
+ return true;
+ }
+
+ Future<Double> future = futureMap.remove(point);
+ if (future == null) {
+ throw new IllegalStateException("abort called for " + point + " but it is not being computed");
+ }
+
+ if (future.isDone()) {
+ // Evaluation has been completed, store value in cache
+ try {
+ double value = future.get();
+ functionCache.put(point, value);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ } else {
+ // Cancel the evaluation
+ future.cancel(true);
+ return false;
+ }
+ }
+
+
+ public double getValue(Point point) {
+ Double d = functionCache.get(point);
+ if (d == null) {
+ throw new IllegalStateException(point.toString() + " is not in function cache. " +
+ "functionCache=" + functionCache + " futureMap=" + futureMap);
+ }
+ return d;
+ }
+
+
+
+ @Override
+ public Function getFunction() {
+ return function;
+ }
+
+ @Override
+ public void setFunction(Function function) {
+ this.function = function;
+ clearCache();
+ }
+
+ @Override
+ public void clearCache() {
+ List<Point> list = new ArrayList<Point>(futureMap.keySet());
+ abort(list);
+ functionCache.clear();
+ }
+
+ public ExecutorService getExecutor() {
+ return executor;
+ }
+
+
+
+ /**
+ * A Callable that evaluates a function at a specific point and returns the result.
+ */
+ private class FunctionCallable implements Callable<Double> {
+ private final Function calledFunction;
+ private final Point point;
+
+ public FunctionCallable(Function function, Point point) {
+ this.calledFunction = function;
+ this.point = point;
+ }
+
+ @Override
+ public Double call() throws InterruptedException {
+ return calledFunction.evaluate(point);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.optimization.general;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A FunctionCache that allows queuing points to be computed in the background,
+ * waiting for specific points to become computed or aborting the computation of
+ * points.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface ParallelFunctionCache extends FunctionCache {
+
+ /**
+ * Queue a list of function evaluations at the specified points.
+ *
+ * @param points the points at which to evaluate the function.
+ */
+ public void compute(Collection<Point> points);
+
+ /**
+ * Queue function evaluation for the specified point.
+ *
+ * @param point the point at which to evaluate the function.
+ */
+ public void compute(Point point);
+
+ /**
+ * Wait for a collection of points to be computed. After calling this method
+ * the function values are available by calling XXX
+ *
+ * @param points the points to wait for.
+ * @throws InterruptedException if this thread was interrupted while waiting.
+ */
+ public void waitFor(Collection<Point> points) throws InterruptedException;
+
+ /**
+ * Wait for a point to be computed. After calling this method
+ * the function values are available by calling XXX
+ *
+ * @param point the point to wait for.
+ * @throws InterruptedException if this thread was interrupted while waiting.
+ */
+ public void waitFor(Point point) throws InterruptedException;
+
+
+ /**
+ * Abort the computation of the specified point. If computation has ended,
+ * the result is stored in the function cache anyway.
+ *
+ * @param points the points to abort.
+ * @return a list of the points that have been computed anyway
+ */
+ public List<Point> abort(Collection<Point> points);
+
+
+ /**
+ * Abort the computation of the specified point. If computation has ended,
+ * the result is stored in the function cache anyway.
+ *
+ * @param point the point to abort.
+ * @return <code>true</code> if the point has been computed anyway, <code>false</code> if not.
+ */
+ public boolean abort(Point point);
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.optimization.general.multidim;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.optimization.general.FunctionCache;
+import net.sf.openrocket.optimization.general.FunctionOptimizer;
+import net.sf.openrocket.optimization.general.OptimizationController;
+import net.sf.openrocket.optimization.general.ParallelFunctionCache;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.Statistics;
+
+/**
+ * A customized implementation of the parallel multidirectional search algorithm by Dennis and Torczon.
+ * <p>
+ * This is a parallel pattern search optimization algorithm. The function evaluations are performed
+ * using an ExecutorService. By default a ThreadPoolExecutor is used that has as many thread defined
+ * as the system has processors.
+ */
+public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Statistics {
+ private static final LogHelper log = Application.getLogger();
+
+ private List<Point> simplex = new ArrayList<Point>();
+
+ private ParallelFunctionCache functionExecutor;
+
+ private boolean useExpansion = false;
+
+ private int stepCount = 0;
+ private int reflectionAcceptance = 0;
+ private int expansionAcceptance = 0;
+ private int coordinateAcceptance = 0;
+ private int reductionFallback = 0;
+
+
+ public MultidirectionalSearchOptimizer() {
+ // No-op
+ }
+
+ public MultidirectionalSearchOptimizer(ParallelFunctionCache functionCache) {
+ this.functionExecutor = functionCache;
+ }
+
+
+
+ @Override
+ public void optimize(Point initial, OptimizationController control) {
+ FunctionCacheComparator comparator = new FunctionCacheComparator(functionExecutor);
+
+ final List<Point> pattern = SearchPattern.square(initial.dim());
+ log.info("Starting optimization at " + initial + " with pattern " + pattern);
+
+ try {
+
+ boolean simplexComputed = false;
+ double step = 0.5;
+
+ // Set up the current simplex
+ simplex.clear();
+ simplex.add(initial);
+ for (Point p : pattern) {
+ simplex.add(initial.add(p.mul(step)));
+ }
+
+ // Normal iterations
+ List<Point> reflection = new ArrayList<Point>(simplex.size());
+ List<Point> expansion = new ArrayList<Point>(simplex.size());
+ List<Point> coordinateSearch = new ArrayList<Point>(simplex.size());
+ Point current;
+ double currentValue;
+ do {
+
+ log.debug("Starting optimization step with simplex " + simplex +
+ (simplexComputed ? "" : " (not computed)"));
+ stepCount++;
+
+ if (!simplexComputed) {
+ // TODO: Could something be computed in parallel?
+ functionExecutor.compute(simplex);
+ functionExecutor.waitFor(simplex);
+ Collections.sort(simplex, comparator);
+ simplexComputed = true;
+ }
+
+ current = simplex.get(0);
+ currentValue = functionExecutor.getValue(current);
+
+ /*
+ * Compute and queue the next points in likely order of usefulness.
+ * Expansion is unlikely as we're mainly dealing with bounded optimization.
+ */
+ createReflection(simplex, reflection);
+ createCoordinateSearch(current, step, coordinateSearch);
+ if (useExpansion)
+ createExpansion(simplex, expansion);
+
+ functionExecutor.compute(reflection);
+ functionExecutor.compute(coordinateSearch);
+ if (useExpansion)
+ functionExecutor.compute(expansion);
+
+ // Check reflection acceptance
+ log.debug("Computing reflection");
+ functionExecutor.waitFor(reflection);
+
+ if (accept(reflection, currentValue)) {
+
+ log.debug("Reflection was successful, aborting coordinate search, " +
+ (useExpansion ? "computing" : "skipping") + " expansion");
+
+ functionExecutor.abort(coordinateSearch);
+
+ simplex.clear();
+ simplex.add(current);
+ simplex.addAll(reflection);
+ Collections.sort(simplex, comparator);
+
+ if (useExpansion) {
+
+ /*
+ * Assume expansion to be unsuccessful, queue next reflection while computing expansion.
+ */
+ createReflection(simplex, reflection);
+
+ functionExecutor.compute(reflection);
+ functionExecutor.waitFor(expansion);
+
+ if (accept(expansion, currentValue)) {
+ log.debug("Expansion was successful, aborting reflection");
+ functionExecutor.abort(reflection);
+
+ simplex.clear();
+ simplex.add(current);
+ simplex.addAll(expansion);
+ step *= 2;
+ Collections.sort(simplex, comparator);
+ expansionAcceptance++;
+ } else {
+ log.debug("Expansion failed");
+ reflectionAcceptance++;
+ }
+
+ } else {
+ reflectionAcceptance++;
+ }
+
+ } else {
+
+ log.debug("Reflection was unsuccessful, aborting expansion, computing coordinate search");
+
+ functionExecutor.abort(expansion);
+
+ /*
+ * Assume coordinate search to be unsuccessful, queue contraction step while computing.
+ */
+ halveStep(simplex);
+ functionExecutor.compute(simplex);
+ functionExecutor.waitFor(coordinateSearch);
+
+ if (accept(coordinateSearch, currentValue)) {
+
+ log.debug("Coordinate search successful, reseting simplex");
+ List<Point> toAbort = new LinkedList<Point>(simplex);
+ simplex.clear();
+ simplex.add(current);
+ for (Point p : pattern) {
+ simplex.add(current.add(p.mul(step)));
+ }
+ toAbort.removeAll(simplex);
+ functionExecutor.abort(toAbort);
+ simplexComputed = false;
+ coordinateAcceptance++;
+
+ } else {
+ log.debug("Coordinate search unsuccessful, halving step.");
+ step /= 2;
+ reductionFallback++;
+ }
+
+ }
+
+ log.debug("Ending optimization step with simplex " + simplex);
+
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+
+ } while (control.stepTaken(current, currentValue, simplex.get(0),
+ functionExecutor.getValue(simplex.get(0)), step));
+
+ } catch (InterruptedException e) {
+ log.info("Optimization was interrupted with InterruptedException");
+ }
+
+ log.info("Finishing optimization at point " + simplex.get(0) + " value = " +
+ functionExecutor.getValue(simplex.get(0)));
+ }
+
+
+
+ private void createReflection(List<Point> base, List<Point> reflection) {
+ Point current = base.get(0);
+ reflection.clear();
+ for (int i = 1; i < base.size(); i++) {
+ Point p = current.mul(2).sub(base.get(i));
+ reflection.add(p);
+ }
+ }
+
+ private void createExpansion(List<Point> base, List<Point> expansion) {
+ Point current = base.get(0);
+ expansion.clear();
+ for (int i = 1; i < base.size(); i++) {
+ Point p = current.mul(3).sub(base.get(i).mul(2));
+ expansion.add(p);
+ }
+ }
+
+ private void halveStep(List<Point> base) {
+ Point current = base.get(0);
+ for (int i = 1; i < base.size(); i++) {
+ Point p = base.get(i);
+ p = p.add(current).mul(0.5);
+ base.set(i, p);
+ }
+ }
+
+ private void createCoordinateSearch(Point current, double step, List<Point> coordinateDirections) {
+ coordinateDirections.clear();
+ for (int i = 0; i < current.dim(); i++) {
+ Point p = new Point(current.dim());
+ p = p.set(i, step);
+ coordinateDirections.add(current.add(p));
+ coordinateDirections.add(current.sub(p));
+ }
+ }
+
+
+ private boolean accept(List<Point> points, double currentValue) {
+ for (Point p : points) {
+ if (functionExecutor.getValue(p) < currentValue) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+
+ @Override
+ public Point getOptimumPoint() {
+ if (simplex.size() == 0) {
+ throw new IllegalStateException("Optimization has not been called, simplex is empty");
+ }
+ return simplex.get(0);
+ }
+
+ @Override
+ public double getOptimumValue() {
+ return functionExecutor.getValue(getOptimumPoint());
+ }
+
+ @Override
+ public FunctionCache getFunctionCache() {
+ return functionExecutor;
+ }
+
+ @Override
+ public void setFunctionCache(FunctionCache functionCache) {
+ if (!(functionCache instanceof ParallelFunctionCache)) {
+ throw new IllegalArgumentException("Function cache needs to be a ParallelFunctionCache: " + functionCache);
+ }
+ this.functionExecutor = (ParallelFunctionCache) functionCache;
+ }
+
+ @Override
+ public String getStatistics() {
+ return "MultidirectionalSearchOptimizer[stepCount=" + stepCount +
+ ", reflectionAcceptance=" + reflectionAcceptance +
+ ", expansionAcceptance=" + expansionAcceptance +
+ ", coordinateAcceptance=" + coordinateAcceptance +
+ ", reductionFallback=" + reductionFallback;
+ }
+
+ @Override
+ public void resetStatistics() {
+ stepCount = 0;
+ reflectionAcceptance = 0;
+ expansionAcceptance = 0;
+ coordinateAcceptance = 0;
+ reductionFallback = 0;
+ }
+
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+
+}
--- /dev/null
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.optimization.general.Function;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.startup.Application;
+
+/**
+ * A Function that optimizes a specific RocketOptimizationParameter to some goal
+ * by modifying a base simulation using SimulationModifiers.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketOptimizationFunction implements Function {
+ private static final LogHelper log = Application.getLogger();
+
+ /*
+ * NOTE: This class must be thread-safe!!!
+ */
+
+ private final Simulation baseSimulation;
+ private final RocketOptimizationParameter parameter;
+ private final OptimizationGoal goal;
+ private final SimulationModifier[] modifiers;
+
+ private final Map<Point, Double> parameterValueCache = new ConcurrentHashMap<Point, Double>();
+ private final Map<Point, Double> goalValueCache = new ConcurrentHashMap<Point, Double>();
+
+
+ /**
+ * Sole constructor.
+ * <p>
+ * The dimensionality of the resulting function is the same as the length of the
+ * modifiers array.
+ *
+ * @param baseSimulation the base simulation to modify
+ * @param parameter the rocket parameter to optimize
+ * @param goal the goal of the rocket parameter
+ * @param modifiers the modifiers that modify the simulation
+ */
+ public RocketOptimizationFunction(Simulation baseSimulation, RocketOptimizationParameter parameter,
+ OptimizationGoal goal, SimulationModifier... modifiers) {
+ this.baseSimulation = baseSimulation;
+ this.parameter = parameter;
+ this.goal = goal;
+ this.modifiers = modifiers.clone();
+ if (modifiers.length == 0) {
+ throw new IllegalArgumentException("No SimulationModifiers specified");
+ }
+ }
+
+
+ @Override
+ public double evaluate(Point point) throws InterruptedException {
+
+ // Check for precomputed value
+ double value = preComputed(point);
+ if (!Double.isNaN(value)) {
+ return value;
+ }
+
+ // Create the new simulation based on the point
+ double[] p = point.asArray();
+ if (p.length != modifiers.length) {
+ throw new IllegalArgumentException("Point has length " + p.length + " while function has " +
+ modifiers.length + " simulation modifiers");
+ }
+ Simulation simulation = newSimulationInstance();
+ for (int i = 0; i < modifiers.length; i++) {
+ modifiers[i].modify(simulation, p[i]);
+ }
+
+ // Compute the optimization value
+ value = parameter.computeValue(simulation);
+ parameterValueCache.put(point, value);
+
+ value = goal.getMinimizationParameter(value);
+ if (Double.isNaN(value)) {
+ log.warn("Computed value was NaN, baseSimulation=" + baseSimulation + " parameter=" + parameter +
+ " goal=" + goal + " modifiers=" + Arrays.toString(modifiers) + " simulation=" + simulation);
+ value = Double.MAX_VALUE;
+ }
+ goalValueCache.put(point, value);
+
+ return value;
+ }
+
+ @Override
+ public double preComputed(Point point) {
+ Double value = goalValueCache.get(point);
+ if (value != null) {
+ return value;
+ }
+
+ // TODO: CRITICAL: is in domain?
+ return 0;
+ }
+
+
+ /**
+ * Return the parameter value at a point that has been computed. The purpose is
+ * to allow retrieving the parameter value corresponding to the found minimum value.
+ *
+ * @param point the point to use.
+ * @return the parameter value at that point, or NaN if the value at this point has not been computed.
+ */
+ public double getComputedParameterValue(Point point) {
+ Double value = parameterValueCache.get(point);
+ if (value != null) {
+ return value;
+ } else {
+ return Double.NaN;
+ }
+ }
+
+
+ /**
+ * Returns a new deep copy of the simulation and rocket. This methods performs
+ * synchronization on the simulation for thread protection.
+ *
+ * @return
+ */
+ private Simulation newSimulationInstance() {
+ synchronized (baseSimulation) {
+ Rocket newRocket = (Rocket) baseSimulation.getRocket().copy();
+ Simulation newSimulation = baseSimulation.duplicateSimulation(newRocket);
+ return newSimulation;
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import net.sf.openrocket.document.Simulation;
+
+/**
+ * A parameter of a rocket or simulation that can be optimized
+ * (for example max. altitude or velocity).
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface RocketOptimizationParameter {
+
+ /**
+ * Return the label name for this optimization parameter.
+ *
+ * @return the name for the optimization parameter (e.g. "Flight altitude")
+ */
+ public String getName();
+
+ /**
+ * Compute the value for this optimization parameter for the simulation.
+ * The return value can be any double value.
+ *
+ * @param simulation the simulation
+ * @return the parameter value (any double value)
+ */
+ public double computeValue(Simulation simulation);
+
+}
--- /dev/null
+package net.sf.openrocket.optimization.rocketoptimization;
+
+import net.sf.openrocket.document.Simulation;
+
+/**
+ * An interface what modifies a single parameter in a rocket simulation
+ * based on a double value in the range [0...1].
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface SimulationModifier {
+
+ /**
+ * Modify the specified simulation to the corresponding parameter value.
+ *
+ * @param simulation the simulation to modify
+ * @param value a value in the range [0...1]
+ */
+ public void modify(Simulation simulation, double value);
+
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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);
+ }
+
+}
*/
public abstract class BodyComponent extends ExternalComponent {
-
+
/**
* Default constructor. Sets the relative position to POSITION_RELATIVE_AFTER,
* i.e. body components come after one another.
}
-
+
/**
* Get the outer radius of the component at cylindrical coordinate (x,theta).
*
* @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).
* @return Distance to the inner edge of the object
*/
public abstract double getInnerRadius(double x, double theta);
-
+
/**
public void setLength(double length) {
if (this.length == length)
return;
- this.length = Math.max(length,0);
+ this.length = Math.max(length, 0);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
+ @Override
+ public boolean allowsChildren() {
+ return true;
+ }
/**
* Check whether the given type can be added to this component. BodyComponents allow any
if (InternalComponent.class.isAssignableFrom(type))
return true;
if (ExternalComponent.class.isAssignableFrom(type) &&
- !BodyComponent.class.isAssignableFrom(type))
+ !BodyComponent.class.isAssignableFrom(type))
return true;
return false;
}
*/
public class BodyTube extends SymmetricComponent implements MotorMount {
-
- private double radius=0;
- private boolean autoRadius = false; // Radius chosen automatically based on parent component
+
+ private double radius = 0;
+ private boolean autoRadius = false; // Radius chosen automatically based on parent component
// When changing the inner radius, thickness is modified
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.
*/
* This method sets the automatic radius off.
*/
public void setRadius(double radius) {
- if ((this.radius == radius) && (autoRadius==false))
+ if ((this.radius == radius) && (autoRadius == false))
return;
this.autoRadius = false;
- this.radius = Math.max(radius,0);
-
+ this.radius = Math.max(radius, 0);
+
if (this.thickness > this.radius)
this.thickness = this.radius;
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
-
+
+
/**
* Returns whether the radius is selected automatically or not.
* Returns false also in case automatic radius selection is not possible.
public boolean isRadiusAutomatic() {
return autoRadius;
}
-
+
/**
* Sets whether the radius is selected automatically or not.
*/
@Override
- public double getAftRadius() { return getRadius(); }
- @Override
- public double getForeRadius() { return getRadius(); }
+ public double getAftRadius() {
+ return getRadius();
+ }
+
@Override
- public boolean isAftRadiusAutomatic() { return isRadiusAutomatic(); }
+ public double getForeRadius() {
+ return getRadius();
+ }
+
@Override
- public boolean isForeRadiusAutomatic() { return isRadiusAutomatic(); }
+ public boolean isAftRadiusAutomatic() {
+ return isRadiusAutomatic();
+ }
+ @Override
+ public boolean isForeRadiusAutomatic() {
+ return isRadiusAutomatic();
+ }
+
@Override
protected double getFrontAutoRadius() {
if (isRadiusAutomatic()) {
}
return getRadius();
}
-
+
@Override
protected double getRearAutoRadius() {
if (isRadiusAutomatic()) {
}
return getRadius();
}
-
-
-
-
-
+
+
+
public double getInnerRadius() {
if (filled)
return 0;
- return Math.max(getRadius()-thickness, 0);
+ return Math.max(getRadius() - thickness, 0);
}
public void setInnerRadius(double r) {
- setThickness(getRadius()-r);
+ setThickness(getRadius() - r);
}
-
-
+
+
/**
* Return the component name.
*/
public String getComponentName() {
return "Body tube";
}
-
-
+
+
/************ Component calculations ***********/
// From SymmetricComponent
public double getRadius(double x) {
return getRadius();
}
-
+
/**
* Returns the inner radius at the position x. If the tube is filled, returns always zero.
*/
if (filled)
return 0.0;
else
- return Math.max(getRadius()-thickness,0);
+ return Math.max(getRadius() - thickness, 0);
}
-
+
/**
* Returns the body tube's center of gravity.
*/
@Override
public Coordinate getComponentCG() {
- return new Coordinate(length/2,0,0,getComponentMass());
+ return new Coordinate(length / 2, 0, 0, getComponentMass());
}
/**
public double getComponentVolume() {
double r = getRadius();
if (filled)
- return getFilledVolume(r,length);
+ return getFilledVolume(r, length);
else
- return getFilledVolume(r,length) - getFilledVolume(getInnerRadius(0),length);
+ return getFilledVolume(r, length) - getFilledVolume(getInnerRadius(0), length);
}
@Override
public double getLongitudalUnitInertia() {
// 1/12 * (3 * (r1^2 + r2^2) + h^2)
- return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
- MathUtil.pow2(getLength())) / 12;
+ return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) + MathUtil.pow2(getLength())) / 12;
}
-
+
@Override
public double getRotationalUnitInertia() {
// 1/2 * (r1^2 + r2^2)
- return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+ return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius())) / 2;
}
-
-
+
+
/**
* Helper function for cylinder volume.
*/
private static double getFilledVolume(double r, double l) {
- return Math.PI * r*r * l;
+ return Math.PI * r * r * l;
}
-
+
/**
* Adds bounding coordinates to the given set. The body tube will fit within the
public Collection<Coordinate> getComponentBounds() {
Collection<Coordinate> bounds = new ArrayList<Coordinate>(8);
double r = getRadius();
- addBound(bounds,0,r);
- addBound(bounds,length,r);
+ addBound(bounds, 0, r);
+ addBound(bounds, length, r);
return bounds;
}
-
-
+
+
//////////////// Motor mount /////////////////
@Override
public boolean isMotorMount() {
return motorMount;
}
-
+
@Override
public void setMotorMount(boolean mount) {
if (motorMount == mount)
return motors.get(id);
}
-
+
@Override
public void setMotor(String id, Motor motor) {
if (id == null) {
motors.put(id, motor);
fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
}
-
+
@Override
public double getMotorDelay(String id) {
Double delay = ejectionDelays.get(id);
return Motor.PLUGGED;
return delay;
}
-
+
@Override
public void setMotorDelay(String id, double delay) {
ejectionDelays.put(id, delay);
@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)
ignitionEvent = event;
fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
}
-
+
@Override
public double getIgnitionDelay() {
return ignitionDelay;
}
-
+
@Override
public void setIgnitionDelay(double delay) {
if (MathUtil.equals(delay, ignitionDelay))
ignitionDelay = delay;
fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
}
-
+
@Override
public double getMotorOverhang() {
this.overhang = overhang;
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
-
+
+
@Override
public Coordinate getMotorPosition(String id) {
Motor motor = motors.get(id);
return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang());
}
-
-
+
+
/*
* (non-Javadoc)
* Copy the motor and ejection delay HashMaps.
*/
@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;
}
}
public class Bulkhead extends RadiusRingComponent {
-
+
public Bulkhead() {
setOuterRadiusAutomatic(true);
setLength(0.002);
}
-
+
@Override
public double getInnerRadius() {
return 0;
public String getComponentName() {
return "Bulkhead";
}
-
+
+ @Override
+ public boolean allowsChildren() {
+ return false;
+ }
+
@Override
public boolean isCompatible(Class<? extends RocketComponent> type) {
return false;
}
-
+
}
public class CenteringRing extends RadiusRingComponent {
-
+
public CenteringRing() {
setOuterRadiusAutomatic(true);
setInnerRadiusAutomatic(true);
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;
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());
}
return super.getInnerRadius();
}
-
+
@Override
public void setOuterRadiusAutomatic(boolean auto) {
public String getComponentName() {
return "Centering ring";
}
-
+
+ @Override
+ public boolean allowsChildren() {
+ return false;
+ }
+
@Override
public boolean isCompatible(Class<? extends RocketComponent> type) {
return false;
}
-
+
}
rocket.addComponentChangeListener(config);
return config;
} catch (CloneNotSupportedException e) {
- throw new BugException("BUG: clone not supported!", e);
+ throw new BugException("clone not supported!", e);
}
}
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);
public String getComponentName() {
return "Engine block";
}
-
+
+ @Override
+ public boolean allowsChildren() {
+ return false;
+ }
+
@Override
public boolean isCompatible(Class<? extends RocketComponent> type) {
return false;
}
-
+
}
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;
public double getRelativeVolume() {
return volume;
}
+
@Override
public String toString() {
return name;
END("Root chord trailing edge");
private final String name;
+
TabRelativePosition(String name) {
this.name = name;
}
/**
* 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.
*/
protected Transformation baseRotation = Transformation.rotate_x(rotation);
-
+
/**
* Cant angle of fins.
*/
*/
protected double thickness = 0.003;
-
+
/**
* The cross-section shape of the fins.
*/
protected CrossSection crossSection = CrossSection.SQUARE;
-
+
/*
* Fin tab properties.
*/
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;
public FinSet() {
super(RocketComponent.Position.BOTTOM);
}
-
+
/**
* Return the number of fins in the set.
* @return The number of fins.
public int getFinCount() {
return fins;
}
-
+
/**
* Sets the number of fins in the set.
* @param n The number of fins, greater of equal to one.
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.
baseRotation = Transformation.rotate_x(rotation);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
+
public Transformation getBaseRotationTransformation() {
return baseRotation;
}
-
+
public double getCantAngle() {
return cantAngle;
}
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;
}
}
public void setThickness(double r) {
if (thickness == r)
return;
- thickness = Math.max(r,0);
+ thickness = Math.max(r, 0);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
}
-
-
+
+
@Override
public void setRelativePosition(RocketComponent.Position position) {
super.setRelativePosition(position);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
+
@Override
public void setPositionValue(double value) {
-
+
public double getTabHeight() {
return tabHeight;
}
-
+
public void setTabHeight(double height) {
height = MathUtil.max(height, 0);
if (MathUtil.equals(this.tabHeight, height))
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))
this.tabLength = length;
fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
}
-
-
+
+
public double getTabShift() {
return tabShift;
}
-
+
public void setTabShift(double shift) {
this.tabShift = shift;
fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
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.
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.
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();
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;
}
}
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;
}
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;
}
}
// 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));
}
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));
}
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
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()) {
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.
*
}
-
-
+
+
/**
* 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,
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.
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;
public class FreeformFinSet extends FinSet {
-
+
private ArrayList<Coordinate> points = new ArrayList<Coordinate>();
public FreeformFinSet() {
points.add(Coordinate.NUL);
- points.add(new Coordinate(0.025,0.05));
- points.add(new Coordinate(0.075,0.05));
- points.add(new Coordinate(0.05,0));
+ points.add(new Coordinate(0.025, 0.05));
+ points.add(new Coordinate(0.075, 0.05));
+ points.add(new Coordinate(0.05, 0));
this.length = 0.05;
}
-
+
public FreeformFinSet(Coordinate[] finpoints) throws IllegalFinPointException {
setPoints(finpoints);
}
-
+
/*
public FreeformFinSet(FinSet finset) {
Coordinate[] finpoints = finset.getFinPoints();
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
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;
} 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.
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);
}
*/
@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");
}
copy.remove(index);
validate(copy);
this.points = copy;
-
+
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
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>
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");
}
}
if (index == 0) {
- System.out.println("Set point zero to x:"+x);
- for (int i=1; i < points.size(); i++) {
+ System.out.println("Set point zero to x:" + x);
+ for (int i = 1; i < points.size(); i++) {
Coordinate c = points.get(i);
points.set(i, c.setX(c.x - x));
}
} else {
- points.set(index,new Coordinate(x,y));
+ points.set(index, new Coordinate(x, y));
}
- if (index == 0 || index == points.size()-1) {
- this.length = points.get(points.size()-1).x;
+ if (index == 0 || index == points.size() - 1) {
+ this.length = points.get(points.size() - 1).x;
}
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
+
private boolean intersects(double ax0, double ay0, double ax1, double ay1,
double bx0, double by0, double bx1, double by1) {
- double d = ((by1-by0)*(ax1-ax0) - (bx1-bx0)*(ay1-ay0));
+ double d = ((by1 - by0) * (ax1 - ax0) - (bx1 - bx0) * (ay1 - ay0));
- double ua = ((bx1-bx0)*(ay0-by0) - (by1-by0)*(ax0-bx0)) / d;
- double ub = ((ax1-ax0)*(ay0-by0) - (ay1-ay0)*(ax0-bx0)) / d;
+ double ua = ((bx1 - bx0) * (ay0 - by0) - (by1 - by0) * (ax0 - bx0)) / d;
+ double ub = ((ax1 - ax0) * (ay0 - by0) - (ay1 - ay0) * (ax0 - bx0)) / d;
return (ua >= 0) && (ua <= 1) && (ub >= 0) && (ub <= 1);
}
-
+
@Override
public Coordinate[] getFinPoints() {
return points.toArray(new Coordinate[0]);
}
-
+
@Override
public double getSpan() {
double max = 0;
- for (Coordinate c: points) {
+ for (Coordinate c : points) {
if (c.y > max)
max = c.y;
}
return max;
}
-
+
@Override
public String getComponentName() {
return "Freeform fin set";
}
-
-
+
+
@SuppressWarnings("unchecked")
@Override
- public RocketComponent copy() {
- RocketComponent c = super.copy();
- ((FreeformFinSet)c).points = (ArrayList<Coordinate>) this.points.clone();
+ protected RocketComponent copyWithOriginalID() {
+ RocketComponent c = super.copyWithOriginalID();
+ ((FreeformFinSet) c).points = (ArrayList<Coordinate>) this.points.clone();
return c;
}
private void validate(ArrayList<Coordinate> points) throws IllegalFinPointException {
final int n = points.size();
- if (points.get(0).x != 0 || points.get(0).y != 0 ||
- points.get(n-1).x < 0 || points.get(n-1).y != 0) {
+ if (points.get(0).x != 0 || points.get(0).y != 0 ||
+ points.get(n - 1).x < 0 || points.get(n - 1).y != 0) {
throw new IllegalFinPointException("Start or end point illegal.");
}
- for (int i=0; i < n-1; i++) {
- for (int j=i+2; j < n-1; j++) {
- if (intersects(points.get(i).x, points.get(i).y, points.get(i+1).x, points.get(i+1).y,
- points.get(j).x, points.get(j).y, points.get(j+1).x, points.get(j+1).y)) {
+ for (int i = 0; i < n - 1; i++) {
+ for (int j = i + 2; j < n - 1; j++) {
+ if (intersects(points.get(i).x, points.get(i).y, points.get(i + 1).x, points.get(i + 1).y,
+ points.get(j).x, points.get(j).y, points.get(j + 1).x, points.get(j + 1).y)) {
throw new IllegalFinPointException("segments intersect");
}
}
}
}
}
-
+
}
import java.util.List;
import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
*
* @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);
}
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.
*/
public boolean isCompatible(Class<? extends RocketComponent> type) {
return InternalComponent.class.isAssignableFrom(type);
}
-
+
///////////// Cluster methods //////////////
/**
this.cluster = cluster;
fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
}
-
+
/**
* Return the number of tubes in the cluster.
* @return Number of tubes in the current cluster.
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));
}
public double getClusterRotation() {
return clusterRotation;
}
-
-
+
+
/**
* @param rotation the clusterRotation to set
*/
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;
}
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;
}
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 /////////////////
public boolean isMotorMount() {
return motorMount;
}
-
+
@Override
public void setMotorMount(boolean mount) {
if (motorMount == mount)
return motors.get(id);
}
-
+
@Override
public void setMotor(String id, Motor motor) {
if (id == null) {
motors.put(id, motor);
fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
}
-
+
@Override
public double getMotorDelay(String id) {
Double delay = ejectionDelays.get(id);
return Motor.PLUGGED;
return delay;
}
-
+
@Override
public void setMotorDelay(String id, double delay) {
ejectionDelays.put(id, delay);
@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)
ignitionEvent = event;
fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
}
-
+
@Override
public double getIgnitionDelay() {
return ignitionDelay;
}
-
+
@Override
public void setIgnitionDelay(double delay) {
if (MathUtil.equals(delay, ignitionDelay))
ignitionDelay = delay;
fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
}
-
+
@Override
public double getMotorOverhang() {
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
+
@Override
public Coordinate getMotorPosition(String id) {
Motor motor = motors.get(id);
return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang());
}
-
-
+
+
/*
* (non-Javadoc)
* Copy the motor and ejection delay HashMaps.
*/
@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
public class LaunchLug extends ExternalComponent {
-
+
private double radius;
private double thickness;
private double shiftY, shiftZ;
-
+
public LaunchLug() {
super(Position.MIDDLE);
- radius = 0.01/2;
+ radius = 0.01 / 2;
thickness = 0.001;
length = 0.03;
}
public double getRadius() {
return radius;
}
-
+
public void setRadius(double radius) {
if (MathUtil.equals(this.radius, radius))
return;
this.thickness = Math.min(this.thickness, this.radius);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
+
public double getInnerRadius() {
- return radius-thickness;
+ return radius - thickness;
}
-
+
public void setInnerRadius(double innerRadius) {
setRadius(innerRadius + thickness);
}
-
+
public double getThickness() {
return thickness;
}
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))
this.radialDirection = direction;
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
-
+
+
public void setLength(double length) {
if (MathUtil.equals(this.length, length))
return;
}
-
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);
}
*/
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>();
addBound(set, length, radius);
return set;
}
-
+
@Override
public Coordinate getComponentCG() {
- return new Coordinate(length/2, 0, 0, getComponentMass());
+ return new Coordinate(length / 2, 0, 0, getComponentMass());
}
-
+
@Override
public String getComponentName() {
return "Launch lug";
}
-
+
@Override
public double getLongitudalUnitInertia() {
// 1/12 * (3 * (r1^2 + r2^2) + h^2)
- return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
- MathUtil.pow2(getLength())) / 12;
+ return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) + MathUtil.pow2(getLength())) / 12;
}
-
+
@Override
public double getRotationalUnitInertia() {
// 1/2 * (r1^2 + r2^2)
- return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+ return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius())) / 2;
}
-
-
+
+ @Override
+ public boolean allowsChildren() {
+ return false;
+ }
+
@Override
public boolean isCompatible(Class<? extends RocketComponent> type) {
// Allow nothing to be attached to a LaunchLug
return false;
}
-
+
}
public class MassComponent extends MassObject {
private double mass = 0;
-
+
public MassComponent() {
super();
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))
public String getComponentName() {
return "Mass component";
}
-
+
+ @Override
+ public boolean allowsChildren() {
+ return false;
+ }
@Override
public boolean isCompatible(Class<? extends RocketComponent> type) {
import net.sf.openrocket.util.Prefs;
public class Parachute extends RecoveryDevice {
-
+
public static final double DEFAULT_CD = 0.8;
private double diameter;
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;
}
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;
fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
}
-
+
public final int getLineCount() {
return lineCount;
}
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) {
@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;
}
-
+
}
import net.sf.openrocket.util.Chars;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.UniqueID;
/**
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.
*/
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>();
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;
}
-
+
public String getDesigner() {
+ checkState();
return designer;
}
fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
}
-
+
public String getRevision() {
+ checkState();
return revision;
}
}
-
+
/**
* Return the number of stages in this rocket.
* @return the number of stages in this rocket.
*/
public int getStageCount() {
+ checkState();
return this.getChildCount();
}
-
+
/**
* Return the non-negative modification ID of this rocket. The ID is changed
* every time any change occurs in the rocket. This can be used to check
}
-
-
+
+
public ReferenceType getReferenceType() {
+ checkState();
return refType;
}
public double getCustomReferenceLength() {
+ checkState();
return customReferenceLength;
}
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);
}
-
-
-
+
+
+
/**
* Set whether the rocket has a perfect finish. This will affect whether the
* boundary layer is assumed to be fully turbulent or not.
this.perfectFinish = perfectFinish;
fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
}
-
-
+
+
/**
* Get whether the rocket has a perfect finish.
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;
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();
fireComponentChangeEvent(type);
}
-
-
+
+
/////// Implement the ComponentChangeListener lists
/**
* the structure.
*/
public void resetListeners() {
-// System.out.println("RESETTING LISTENER LIST of Rocket "+this);
+ // System.out.println("RESETTING LISTENER LIST of Rocket "+this);
listenerList = new EventListenerList();
}
public void printListeners() {
- System.out.println(""+this+" has "+listenerList.getListenerCount()+" listeners:");
+ System.out.println("" + this + " has " + listenerList.getListenerCount() + " listeners:");
Object[] list = listenerList.getListenerList();
- for (int i=1; i<list.length; i+=2)
- System.out.println(" "+((i+1)/2)+": "+list[i]);
+ for (int i = 1; i < list.length; i += 2)
+ System.out.println(" " + ((i + 1) / 2) + ": " + list[i]);
}
@Override
public void addComponentChangeListener(ComponentChangeListener l) {
- listenerList.add(ComponentChangeListener.class,l);
+ checkState();
+ listenerList.add(ComponentChangeListener.class, l);
if (DEBUG_LISTENERS)
- System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
- " listeners): "+l);
+ System.out.println(this + ": Added listner (now " + listenerList.getListenerCount() +
+ " listeners): " + l);
}
+
@Override
public void removeComponentChangeListener(ComponentChangeListener l) {
listenerList.remove(ComponentChangeListener.class, l);
if (DEBUG_LISTENERS)
- System.out.println(this+": Removed listner (now "+listenerList.getListenerCount()+
- " listeners): "+l);
+ System.out.println(this + ": Removed listner (now " + listenerList.getListenerCount() +
+ " listeners): " + l);
}
-
+
@Override
public void addChangeListener(ChangeListener l) {
- listenerList.add(ChangeListener.class,l);
+ checkState();
+ listenerList.add(ChangeListener.class, l);
if (DEBUG_LISTENERS)
- System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
- " listeners): "+l);
+ System.out.println(this + ": Added listner (now " + listenerList.getListenerCount() +
+ " listeners): " + l);
}
+
@Override
public void removeChangeListener(ChangeListener l) {
listenerList.remove(ChangeListener.class, l);
if (DEBUG_LISTENERS)
- System.out.println(this+": Removed listner (now "+listenerList.getListenerCount()+
- " listeners): "+l);
+ System.out.println(this + ": Removed listner (now " + listenerList.getListenerCount() +
+ " listeners): " + l);
}
-
+
@Override
protected void fireComponentChangeEvent(ComponentChangeEvent e) {
-
+ checkState();
+
// Update modification ID's only for normal (not undo/redo) events
if (!e.isUndoChange()) {
- modID = getNextModID();
+ modID = UniqueID.next();
if (e.isMassChange())
massModID = modID;
if (e.isAerodynamicChange())
}
if (DEBUG_LISTENERS)
- System.out.println("FIRING "+e);
+ System.out.println("FIRING " + e);
// Check whether frozen
if (freezeList != null) {
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.
* @see #thaw()
*/
public void freeze() {
- if (freezeList == null)
+ checkState();
+ if (freezeList == null) {
freezeList = new LinkedList<ComponentChangeEvent>();
+ }
}
/**
* @see #freeze()
*/
public void thaw() {
+ checkState();
if (freezeList == null)
return;
- if (freezeList.size()==0) {
+ if (freezeList.size() == 0) {
freezeList = null;
return;
}
int type = 0;
Object c = null;
- for (ComponentChangeEvent e: freezeList) {
+ for (ComponentChangeEvent e : freezeList) {
type = type | e.getType();
c = e.getSource();
}
freezeList = null;
- fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent)c,type));
+ fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent) c, type));
}
-
+
//////// Motor configurations ////////
-
+
/**
* Return the default configuration. This should be used in the user interface
* to ensure a consistent rocket configuration between dialogs. It should NOT
* @return the default {@link Configuration}.
*/
public Configuration getDefaultConfiguration() {
+ checkState();
return defaultConfiguration;
}
* @return an array of the motor configuration IDs.
*/
public String[] getMotorConfigurationIDs() {
+ checkState();
return motorConfigurationIDs.toArray(new String[0]);
}
* @return the new motor configuration ID.
*/
public String newMotorConfigurationID() {
+ checkState();
String id = UUID.randomUUID().toString();
motorConfigurationIDs.add(id);
fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
* @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.
* @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.
* @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.
*
* @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())
* @return the configuration name
*/
public String getMotorConfigurationName(String id) {
+ checkState();
if (!isMotorConfigurationID(id))
return "";
String s = motorConfigurationNames.get(id);
* @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);
}
* @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);
}
*/
@SuppressWarnings("null")
public String getMotorConfigurationDescription(String id) {
+ checkState();
String name;
int motorCount = 0;
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++;
}
// 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++;
}
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;
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;
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.
*/
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Stack;
-import java.util.UUID;
import javax.swing.event.ChangeListener;
+import net.sf.openrocket.logging.TraceException;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.ChangeSource;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.LineStyle;
import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.UniqueID;
-public abstract class RocketComponent implements ChangeSource, Cloneable,
+public abstract class RocketComponent implements ChangeSource, Cloneable,
Iterable<RocketComponent> {
-
+
/*
* Text is suitable to the form
* Position relative to: <title>
ABSOLUTE("Tip of the nose cone");
private String title;
+
Position(String title) {
this.title = title;
}
* List of child components of this component.
*/
private List<RocketComponent> children = new ArrayList<RocketComponent>();
-
+
//////// Parameters common to all components:
/**
* By default it is zero, i.e. no translation.
*/
protected double length = 0;
-
+
/**
* Positioning of this component relative to the parent component.
*/
*/
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;
// User-specified comment
private String comment = "";
-
+
// Unique ID of the component
+ // TODO: CRITICAL: Sort out usage of ID and undo defect
private String id = null;
- //// NOTE !!! All fields must be copied in the method copyFrom()! ////
-
+ /**
+ * When invalidated is non-null this component cannot be used anymore.
+ * This is a safety mechanism to prevent accidental use after calling {@link #copyFrom(RocketComponent)}.
+ */
+ private TraceException invalidated = null;
+ //// NOTE !!! All fields must be copied in the method copyFrom()! ////
+
+
/**
* Default constructor. Sets the name of the component to the component's static name
* and the relative position of the component.
// These must not fire any events, due to Rocket undo system initialization
this.name = getComponentName();
this.relativePosition = relativePosition;
- this.id = UUID.randomUUID().toString();
+ newID();
}
-
-
-
- //////////// Methods that must be implemented ////////////
+
+ //////////// Methods that must be implemented ////////////
+
+
/**
* Static component name. The name may not vary of the parameters, it must be static.
*/
- public abstract String getComponentName(); // Static component type name
-
+ public abstract String getComponentName(); // Static component type name
+
/**
* Return the component mass (regardless of mass overriding).
*/
- public abstract double getComponentMass(); // Mass of non-overridden component
-
+ public abstract double getComponentMass(); // Mass of non-overridden component
+
/**
* Return the component CG and mass (regardless of CG or mass overriding).
*/
- public abstract Coordinate getComponentCG(); // CG of non-overridden component
-
+ public abstract Coordinate getComponentCG(); // CG of non-overridden component
+
/**
* Return the longitudal (around the y- or z-axis) unitary moment of inertia.
* The unitary moment of inertia is the moment of inertia with the assumption that
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
* @see #isCompatible(Class)
*/
public final boolean isCompatible(RocketComponent c) {
+ checkState();
return isCompatible(c.getClass());
}
-
+
/**
* Return a collection of bounding coordinates. The coordinates must be such that
* the component is fully enclosed in their convex hull.
* @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.
* of the passed array and return the array itself.
*/
public Coordinate[] shiftCoordinates(Coordinate[] c) {
+ checkState();
return c;
}
*/
protected void componentChanged(ComponentChangeEvent e) {
// No-op
+ checkState();
}
-
-
+
+
/**
* Return a descriptive name of the component.
*
else
return name;
}
-
+
public final void printStructure() {
- System.out.println("Rocket structure from '"+this.toString()+"':");
+ System.out.println("Rocket structure from '" + this.toString() + "':");
printStructure(0);
}
private void printStructure(int level) {
String s = "";
- for (int i=0; i < level; i++) {
+ for (int i = 0; i < level; i++) {
s += " ";
}
- s += this.toString() + " (" + this.getComponentName()+")";
+ s += this.toString() + " (" + this.getComponentName() + ")";
System.out.println(s);
- for (RocketComponent c: children) {
- c.printStructure(level+1);
+ for (RocketComponent c : children) {
+ c.printStructure(level + 1);
+ }
+ }
+
+
+ /**
+ * Make a deep copy of the rocket component tree structure from this component
+ * downwards for copying purposes. Each component in the copy will be assigned
+ * a new component ID, making it a safe copy. This method does not fire any events.
+ *
+ * @return A deep copy of the structure.
+ */
+ public final RocketComponent copy() {
+ RocketComponent clone = copyWithOriginalID();
+
+ Iterator<RocketComponent> iterator = clone.deepIterator(true);
+ while (iterator.hasNext()) {
+ iterator.next().newID();
}
+ return clone;
}
+
/**
* Make a deep copy of the rocket component tree structure from this component
- * downwards. This method does not fire any events.
+ * downwards while maintaining the component ID's. The purpose of this method is
+ * to allow copies to be created with the original ID's for the purpose of the
+ * undo/redo mechanism. This method should not be used for other purposes,
+ * such as copy/paste. This method does not fire any events.
* <p>
* This method must be overridden by any component that refers to mutable objects,
* or if some fields should not be copied. This should be performed by
- * <code>RocketComponent c = super.copy();</code> and then cloning/modifying the
- * appropriate fields.
+ * <code>RocketComponent c = super.copyWithOriginalID();</code> and then cloning/modifying
+ * the appropriate fields.
* <p>
* This is not performed as serializing/deserializing for performance reasons.
*
* @return A deep copy of the structure.
*/
- public RocketComponent copy() {
+ protected RocketComponent copyWithOriginalID() {
+ checkState();
RocketComponent clone;
try {
- clone = (RocketComponent)this.clone();
+ clone = (RocketComponent) this.clone();
} catch (CloneNotSupportedException e) {
throw new BugException("CloneNotSupportedException encountered, " +
- "report a bug!",e);
+ "report a bug!", e);
}
-
+
// Reset all parent/child information
clone.parent = null;
clone.children = new ArrayList<RocketComponent>();
-
+
// Add copied children to the structure without firing events.
- for (RocketComponent child: this.children) {
- RocketComponent childCopy = child.copy();
+ for (RocketComponent child : this.children) {
+ RocketComponent childCopy = child.copyWithOriginalID();
// Don't use add method since it fires events
clone.children.add(childCopy);
childCopy.parent = clone;
}
-
+
return clone;
}
-
-
- ////////////// Methods that may not be overridden ////////////
+ ////////////// Methods that may not be overridden ////////////
+
+
////////// Common parameter setting/getting //////////
* to use the default color.
*/
public final Color getColor() {
+ checkState();
return color;
}
* Set the color of the object to use in 2D figures.
*/
public final void setColor(Color c) {
+ checkState();
if ((color == null && c == null) ||
(color != null && color.equals(c)))
return;
public final LineStyle getLineStyle() {
+ checkState();
return lineStyle;
}
public final void setLineStyle(LineStyle style) {
+ checkState();
if (this.lineStyle == style)
return;
this.lineStyle = style;
fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
}
-
-
+
+
/**
* Get the current override mass. The mass is not necessarily in use
* at the moment.
* @return the override mass
*/
public final double getOverrideMass() {
+ checkState();
return overrideMass;
}
* @param m the override mass
*/
public final void setOverrideMass(double m) {
- overrideMass = Math.max(m,0);
+ checkState();
+ overrideMass = Math.max(m, 0);
if (massOverriden)
fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
}
* @return whether the mass is overridden
*/
public final boolean isMassOverridden() {
+ checkState();
return massOverriden;
}
* @param o whether the mass is overridden
*/
public final void setMassOverridden(boolean o) {
+ checkState();
if (massOverriden != o) {
massOverriden = o;
fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
}
}
-
-
-
+
+
+
/**
* Return the current override CG. The CG is not necessarily overridden.
*
* @return the override CG
*/
public final Coordinate getOverrideCG() {
+ checkState();
return getComponentCG().setX(overrideCGX);
}
-
+
/**
* Return the x-coordinate of the current override CG.
*
* @return the x-coordinate of the override CG.
*/
public final double getOverrideCGX() {
+ checkState();
return overrideCGX;
}
* @param x the x-coordinate of the override CG to set.
*/
public final void setOverrideCGX(double x) {
+ checkState();
if (MathUtil.equals(overrideCGX, x))
return;
this.overrideCGX = x;
* @return whether the CG is overridden
*/
public final boolean isCGOverridden() {
+ checkState();
return cgOverriden;
}
* @param o whether the CG is overridden
*/
public final void setCGOverridden(boolean o) {
+ checkState();
if (cgOverriden != o) {
cgOverriden = o;
fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
}
}
-
+
/**
* Return whether the mass and/or CG override overrides all subcomponent values
* as well. The default implementation is a normal getter/setter implementation,
* @return whether the current mass and/or CG override overrides subcomponents as well.
*/
public boolean getOverrideSubcomponents() {
+ checkState();
return overrideSubcomponents;
}
* @param override whether the mass and/or CG override overrides all subcomponent.
*/
public void setOverrideSubcomponents(boolean override) {
+ checkState();
if (overrideSubcomponents != override) {
overrideSubcomponents = override;
fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
* @return whether the option to override subcomponents is currently enabled.
*/
public boolean isOverrideSubcomponentsEnabled() {
+ checkState();
return isCGOverridden() || isMassOverridden();
}
-
-
+
+
/**
* Get the user-defined name of the component.
*/
* the default name, currently the component name.
*/
public final void setName(String name) {
-// System.out.println("Set name called:"+name+" orig:"+this.name);
- if (name==null || name.matches("^\\s*$"))
+ checkState();
+ if (name == null || name.matches("^\\s*$"))
this.name = getComponentName();
else
this.name = name;
* @return the comment of the component.
*/
public final String getComment() {
+ checkState();
return comment;
}
* @param comment the comment of the component.
*/
public final void setComment(String comment) {
+ checkState();
if (this.comment.equals(comment))
return;
if (comment == null)
fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
}
-
+
/**
* Returns the unique ID of the component.
*
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
* itself.
*/
public final double getLength() {
+ checkState();
return length;
}
-
+
/**
* Get the positioning of the component relative to its parent component.
* This is one of the enums of {@link Position}. A setter method is not provided,
* but can be provided by a subclass.
*/
public final Position getRelativePosition() {
+ checkState();
return relativePosition;
}
* @param position the relative positioning.
*/
protected void setRelativePosition(RocketComponent.Position position) {
+ checkState();
if (this.relativePosition == position)
return;
// Update position so as not to move the component
if (this.parent != null) {
- double thisPos = this.toRelative(Coordinate.NUL,this.parent)[0].x;
-
+ double thisPos = this.toRelative(Coordinate.NUL, this.parent)[0].x;
+
switch (position) {
case ABSOLUTE:
this.position = this.toAbsolute(Coordinate.NUL)[0].x;
break;
-
+
case TOP:
this.position = thisPos;
break;
-
+
case MIDDLE:
- this.position = thisPos - (this.parent.length - this.length)/2;
+ this.position = thisPos - (this.parent.length - this.length) / 2;
break;
-
+
case BOTTOM:
this.position = thisPos - (this.parent.length - this.length);
break;
-
+
default:
- assert(false): "Should not occur";
+ throw new BugException("Unknown position type: " + position);
}
}
this.relativePosition = position;
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
-
+
+
/**
* Get the position value of the component. The exact meaning of the value is
* dependent on the current relative positioning.
* @return the positional value.
*/
public final double getPositionValue() {
+ checkState();
return position;
}
-
+
/**
* Set the position value of the component. The exact meaning of the value
* depends on the current relative positioning.
* @param value the position value of the component.
*/
public void setPositionValue(double value) {
+ checkState();
if (MathUtil.equals(this.position, value))
return;
this.position = value;
}
-
- /////////// Coordinate changes ///////////
+ /////////// Coordinate changes ///////////
+
/**
* Returns coordinate c in absolute coordinates. Equivalent to toComponent(c,null).
*/
public Coordinate[] toAbsolute(Coordinate c) {
- return toRelative(c,null);
+ checkState();
+ return toRelative(c, null);
}
-
+
/**
* Return coordinate <code>c</code> described in the coordinate system of
* <code>dest</code>. If <code>dest</code> is <code>null</code> returns
* 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];
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]);
}
}
* @return Sum of the lengths.
*/
private final double getTotalLength() {
- double l=0;
+ checkState();
+ double l = 0;
if (relativePosition == Position.AFTER)
l = length;
- for (int i=0; i<children.size(); i++)
+ for (int i = 0; i < children.size(); i++)
l += children.get(i).getTotalLength();
return l;
}
-
- /////////// Total mass and CG calculation ////////////
+ /////////// Total mass and CG calculation ////////////
+
/**
* Return the (possibly overridden) mass of component.
*
* @return The mass of the component or the given override mass.
*/
public final double getMass() {
+ checkState();
if (massOverriden)
return overrideMass;
return getComponentMass();
* @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
* @return the longitudal moment of inertia of this component.
*/
public final double getLongitudalInertia() {
+ checkState();
return getLongitudalUnitInertia() * getMass();
}
* @return the rotational moment of inertia of this component.
*/
public final double getRotationalInertia() {
+ checkState();
return getRotationalUnitInertia() * getMass();
}
-
+
/////////// Children handling ///////////
* @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
* 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);
* @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);
* 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.
*
* @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);
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]);
}
* @return Position in the list or -1 if not found.
*/
public final int getChildPosition(RocketComponent child) {
+ checkState();
return children.indexOf(child);
}
* @return The parent of this component or <code>null</code>.
*/
public final RocketComponent getParent() {
+ checkState();
return parent;
}
* @return The root component of the component tree.
*/
public final RocketComponent getRoot() {
+ checkState();
RocketComponent gp = this;
while (gp.parent != null)
gp = gp.parent;
* @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());
}
* @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.");
* @return the stage number this component belongs to.
*/
public final int getStageNumber() {
+ checkState();
if (parent == null) {
throw new IllegalArgumentException("getStageNumber() called for root component");
}
* 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);
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);
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;
}
* @throws IllegalStateException - if the root component is not a Rocket
*/
public void addComponentChangeListener(ComponentChangeListener l) {
+ checkState();
getRocket().addComponentChangeListener(l);
}
}
}
-
+
/**
* Adds a <code>ChangeListener</code> to the rocket tree. This is identical to
* <code>addComponentChangeListener()</code> except that it uses a
* @throws IllegalStateException - if the root component is not a <code>Rocket</code>
*/
public void addChangeListener(ChangeListener l) {
+ checkState();
getRocket().addChangeListener(l);
}
* @param e Event to send
*/
protected void fireComponentChangeEvent(ComponentChangeEvent e) {
- if (parent==null) {
+ checkState();
+ if (parent == null) {
/* Ignore if root invalid. */
return;
}
getRoot().fireComponentChangeEvent(e);
}
-
+
/**
* Fires a ComponentChangeEvent of the given type. The source of the event is set to
* @see #fireComponentChangeEvent(ComponentChangeEvent)
*/
protected void fireComponentChangeEvent(int type) {
- fireComponentChangeEvent(new ComponentChangeEvent(this,type));
+ fireComponentChangeEvent(new ComponentChangeEvent(this, type));
+ }
+
+
+ /**
+ * Checks whether this component has been invalidated and should no longer be used.
+ * This is a safety check that in-place replaced components are no longer used.
+ * All non-trivial methods should call this method as the first thing, unless the
+ * method may be used in debugging cases.
+ *
+ * @throws BugException if this component has been invalidated by {@link #copyFrom(RocketComponent)}.
+ */
+ protected void checkState() {
+ if (invalidated != null) {
+ throw new BugException("This component has been invalidated. Cause is the point of invalidation.",
+ invalidated);
+ }
}
-
/////////// Iterator implementation //////////
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;
}
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;
}
return c;
}
-
+
private void checkID() {
if (root != null) {
if (root.getTreeModID() != treeModID) {
"RocketComponent iterator");
}
}
-
+
/**
* Returns an iterator that iterates over all children and sub-children.
*
* @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);
}
/**
* @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);
}
* @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
* 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);
}
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;
this.name = src.name;
this.comment = src.comment;
this.id = src.id;
+
+ src.invalidated = new TraceException();
}
}
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;
}
}
-
+
@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;
}
-
+
}
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
public class Sleeve extends RingComponent {
-
+
protected double innerRadius = 0;
protected double thickness = 0;
-
+
public Sleeve() {
super();
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;
@Override
public void setInnerRadius(double r) {
- r = Math.max(r,0);
+ r = Math.max(r, 0);
if (MathUtil.equals(innerRadius, r))
return;
innerRadius = r;
-
+
@Override
public void setInnerRadiusAutomatic(boolean auto) {
super.setOuterRadiusAutomatic(auto);
public String getComponentName() {
return "Sleeve";
}
-
+
+ @Override
+ public boolean allowsChildren() {
+ return false;
+ }
+
@Override
public boolean isCompatible(Class<? extends RocketComponent> type) {
return false;
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
public boolean isCompatible(Class<? extends RocketComponent> type) {
return BodyComponent.class.isAssignableFrom(type);
}
-
+
}
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;
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;
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);
fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
}
-
+
@Override
public double getArea() {
return stripWidth * stripLength;
}
-
+
public void setArea(double area) {
if (MathUtil.equals(getArea(), area))
return;
}
-
+
@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;
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;
public class TubeCoupler extends ThicknessRingComponent implements RadialParent {
-
+
public TubeCoupler() {
setOuterRadiusAutomatic(true);
setThickness(0.002);
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.
*/
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();
}
}
-
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);
}
if (event.getType() != FlightEvent.Type.ALTITUDE) {
- log.debug("BasicEventSimulationEngine: Handling event " + event);
+ log.verbose("BasicEventSimulationEngine: Handling event " + event);
}
if (event.getType() == FlightEvent.Type.IGNITION) {
maxAcceleration = Double.NaN;
}
- log.info("Computed flight values:" +
+ log.debug("Computed flight values:" +
" maxAltitude=" + maxAltitude +
" maxVelocity=" + maxVelocity +
" maxAcceleration=" + maxAcceleration +
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);
// 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 +
", 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 +
SimulationStatus clone = (SimulationStatus) super.clone();
return clone;
} catch (CloneNotSupportedException e) {
- throw new BugException("BUG: CloneNotSupportedException?!?", e);
+ throw new BugException("CloneNotSupportedException?!?", e);
}
}
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);
}
}
setWindowIcons(dialog);
addModelNullingListener(dialog);
dialog.setLocationByPlatform(true);
+ dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ dialog.pack();
if (defaultButton != null) {
setDefaultButton(defaultButton);
}
public static void installEscapeCloseOperation(final JDialog dialog) {
Action dispatchClosing = new AbstractAction() {
public void actionPerformed(ActionEvent event) {
+ log.user("Closing dialog " + dialog);
dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
}
};
* @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.");
--- /dev/null
+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;
+ }
+ }
+
+}
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;
package net.sf.openrocket.utils;
-import net.sf.openrocket.optimization.Function;
-import net.sf.openrocket.optimization.FunctionOptimizer;
-import net.sf.openrocket.optimization.MultidirectionalSearchOptimizer;
-import net.sf.openrocket.optimization.OptimizationController;
-import net.sf.openrocket.optimization.ParallelExecutorCache;
-import net.sf.openrocket.optimization.ParallelFunctionCache;
-import net.sf.openrocket.optimization.Point;
+import net.sf.openrocket.optimization.general.Function;
+import net.sf.openrocket.optimization.general.FunctionOptimizer;
+import net.sf.openrocket.optimization.general.OptimizationController;
+import net.sf.openrocket.optimization.general.ParallelExecutorCache;
+import net.sf.openrocket.optimization.general.ParallelFunctionCache;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
-import net.sf.openrocket.optimization.Function;
-import net.sf.openrocket.optimization.FunctionOptimizer;
-import net.sf.openrocket.optimization.MultidirectionalSearchOptimizer;
-import net.sf.openrocket.optimization.OptimizationController;
-import net.sf.openrocket.optimization.ParallelExecutorCache;
-import net.sf.openrocket.optimization.Point;
+import net.sf.openrocket.optimization.general.Function;
+import net.sf.openrocket.optimization.general.FunctionOptimizer;
+import net.sf.openrocket.optimization.general.OptimizationController;
+import net.sf.openrocket.optimization.general.ParallelExecutorCache;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer;
import net.sf.openrocket.util.MathUtil;
--- /dev/null
+<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
--- /dev/null
+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);
+ }
+
+}
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 {
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"
};
}
-
+
public static void assertDeepEquality(RocketComponent c1, RocketComponent c2) {
assertEquality(c1, c2);
assertFalse("iterator end", i2.hasNext());
}
-
+
public static void assertDeepSimilarity(RocketComponent c1, RocketComponent c2,
boolean allowNameDifference) {
assertFalse("iterator end", i2.hasNext());
}
-
+
/**
* Check whether the two components are <em>similar</em>. Two components are similar
* @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 {
continue;
}
-// System.out.println("Testing results of method " + name);
+ // System.out.println("Testing results of method " + name);
// Run the methods
Object result1, result2;
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);
}
}
}
-
+
}
public class FinSetTest {
-
+
@Test
public void testFreeformConvert() {
testFreeformConvert(new TrapezoidFinSet());
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();
assertTrue(l1.changed);
assertEquals(ComponentChangeEvent.NONFUNCTIONAL_CHANGE, l1.changetype);
-
+
// Create copy
RocketComponent rocketcopy = rocket.copy();
FreeformFinSet.convertFinSet(fincopy);
assertTrue(l2.changed);
- assertEquals(ComponentChangeEvent.TREE_CHANGE,
+ assertEquals(ComponentChangeEvent.TREE_CHANGE,
l2.changetype & ComponentChangeEvent.TREE_CHANGE);
}
@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();
}
}
-
+
}
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);
ComponentCompare.assertDeepEquality(r1, r2);
}
-
+
}
$orjava = "";
$orcountry = "";
$orcores = "";
+$orlocale = "";
foreach (getallheaders() as $header => $value) {
if (preg_match("/^[a-zA-Z0-9 !$%&()*+,.\\/:=?@_~-]{1,40}$/", $value)) {
$h = strtolower($header);
$orcountry = $value;
} else if ($h == 'x-openrocket-cpus') {
$orcores = $value;
+ } else if ($h == 'x-openrocket-locale') {
+ $orlocale = $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) {