package net.sf.openrocket.rocketcomponent; import java.util.Collection; import java.util.Collections; import java.util.EventListener; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.UUID; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.ArrayList; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.StateChangeListener; import net.sf.openrocket.util.UniqueID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base for all rocket components. This is the "starting point" for all rocket trees. * It provides the actual implementations of several methods defined in RocketComponent * (eg. the rocket listener lists) and the methods defined in RocketComponent call these. * It also defines some other methods that concern the whole rocket, and helper methods * that keep information about the program state. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public class Rocket extends RocketComponent { private static final Logger log = LoggerFactory.getLogger(Rocket.class); private static final Translator trans = Application.getTranslator(); public static final String DEFAULT_NAME = "[{motors}]"; public static final double DEFAULT_REFERENCE_LENGTH = 0.01; /** * List of component change listeners. */ private List<EventListener> listenerList = new ArrayList<EventListener>(); /** * When freezeList != null, events are not dispatched but stored in the list. * When the structure is thawed, a single combined event will be fired. */ 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 double customReferenceLength = DEFAULT_REFERENCE_LENGTH; // The default configuration used in dialogs private final Configuration defaultConfiguration; private String designer = ""; private String revision = ""; // Flight configuration list private ArrayList<String> flightConfigurationIDs = new ArrayList<String>(); private HashMap<String, String> flightConfigurationNames = new HashMap<String, String>(); { flightConfigurationIDs.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 = UniqueID.next(); massModID = modID; aeroModID = modID; treeModID = modID; functionalModID = modID; defaultConfiguration = new Configuration(this); } public String getDesigner() { checkState(); return designer; } public void setDesigner(String s) { if (s == null) s = ""; designer = s; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } public String getRevision() { checkState(); return revision; } public void setRevision(String s) { if (s == null) s = ""; revision = s; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } /** * 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 * whether it is necessary to void cached data in cases where listeners can not * or should not be used. * <p> * Three other modification IDs are also available, {@link #getMassModID()}, * {@link #getAerodynamicModID()} {@link #getTreeModID()}, which change every time * a mass change, aerodynamic change, or tree change occur. Even though the values * of the different modification ID's may be equal, they should be treated totally * separate. * <p> * Note that undo events restore the modification IDs that were in use at the * corresponding undo level. Subsequent modifications, however, produce modIDs * distinct from those already used. * * @return a unique ID number for this modification state. */ public int getModID() { return modID; } /** * Return the non-negative mass modification ID of this rocket. See * {@link #getModID()} for details. * * @return a unique ID number for this mass-modification state. */ public int getMassModID() { return massModID; } /** * Return the non-negative aerodynamic modification ID of this rocket. See * {@link #getModID()} for details. * * @return a unique ID number for this aerodynamic-modification state. */ public int getAerodynamicModID() { return aeroModID; } /** * Return the non-negative tree modification ID of this rocket. See * {@link #getModID()} for details. * * @return a unique ID number for this tree-modification state. */ public int getTreeModID() { return treeModID; } /** * Return the non-negative functional modificationID of this rocket. * This changes every time a functional change occurs. * * @return a unique ID number for this functional modification state. */ public int getFunctionalModID() { return functionalModID; } public ReferenceType getReferenceType() { checkState(); return refType; } public void setReferenceType(ReferenceType type) { if (refType == type) return; refType = type; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } public double getCustomReferenceLength() { checkState(); return customReferenceLength; } public void setCustomReferenceLength(double length) { if (MathUtil.equals(customReferenceLength, length)) return; 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. * * @param perfectFinish whether the finish is perfect. */ public void setPerfectFinish(boolean perfectFinish) { if (this.perfectFinish == perfectFinish) return; this.perfectFinish = perfectFinish; fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); } /** * Get whether the rocket has a perfect finish. * * @return the perfectFinish */ public boolean isPerfectFinish() { return perfectFinish; } /** * 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 copyWithOriginalID() { Rocket copy = (Rocket) super.copyWithOriginalID(); copy.flightConfigurationIDs = this.flightConfigurationIDs.clone(); copy.flightConfigurationNames = (HashMap<String, String>) this.flightConfigurationNames.clone(); copy.resetListeners(); return copy; } /** * Load the rocket structure from the source. The method loads the fields of this * Rocket object and copies the references to siblings from the <code>source</code>. * The object <code>source</code> should not be used after this call, as it is in * an illegal state! * <p> * This method is meant to be used in conjunction with undo/redo functionality, * and therefore fires an UNDO_EVENT, masked with all applicable mass/aerodynamic/tree * changes. */ @SuppressWarnings("unchecked") public void loadFrom(Rocket r) { // Store list of components to invalidate after event has been fired List<RocketComponent> toInvalidate = this.copyFrom(r); int type = ComponentChangeEvent.UNDO_CHANGE | ComponentChangeEvent.NONFUNCTIONAL_CHANGE; if (this.massModID != r.massModID) type |= ComponentChangeEvent.MASS_CHANGE; if (this.aeroModID != r.aeroModID) type |= ComponentChangeEvent.AERODYNAMIC_CHANGE; // Loading a rocket is always a tree change since the component objects change type |= ComponentChangeEvent.TREE_CHANGE; this.modID = r.modID; this.massModID = r.massModID; this.aeroModID = r.aeroModID; this.treeModID = r.treeModID; this.functionalModID = r.functionalModID; this.refType = r.refType; this.customReferenceLength = r.customReferenceLength; this.flightConfigurationIDs = r.flightConfigurationIDs.clone(); this.flightConfigurationNames = (HashMap<String, String>) r.flightConfigurationNames.clone(); this.perfectFinish = r.perfectFinish; String id = defaultConfiguration.getFlightConfigurationID(); if (!this.flightConfigurationIDs.contains(id)) defaultConfiguration.setFlightConfigurationID(null); this.checkComponentStructure(); fireComponentChangeEvent(type); // Invalidate obsolete components after event for (RocketComponent c : toInvalidate) { c.invalidate(); } } /////// Implement the ComponentChangeListener lists /** * Creates a new EventListenerList for this component. This is necessary when cloning * the structure. */ public void resetListeners() { // System.out.println("RESETTING LISTENER LIST of Rocket "+this); listenerList = new ArrayList<EventListener>(); } public void printListeners() { System.out.println("" + this + " has " + listenerList.size() + " listeners:"); int i = 0; for (EventListener l : listenerList) { System.out.println(" " + (i) + ": " + l); i++; } } @Override public void addComponentChangeListener(ComponentChangeListener l) { checkState(); listenerList.add(l); log.trace("Added ComponentChangeListener " + l + ", current number of listeners is " + listenerList.size()); } @Override public void removeComponentChangeListener(ComponentChangeListener l) { listenerList.remove(l); log.trace("Removed ComponentChangeListener " + l + ", current number of listeners is " + listenerList.size()); } @Override protected void fireComponentChangeEvent(ComponentChangeEvent e) { mutex.lock("fireComponentChangeEvent"); try { checkState(); // Update modification ID's only for normal (not undo/redo) events if (!e.isUndoChange()) { modID = UniqueID.next(); if (e.isMassChange()) massModID = modID; if (e.isAerodynamicChange()) aeroModID = modID; if (e.isTreeChange()) treeModID = modID; if (e.getType() != ComponentChangeEvent.NONFUNCTIONAL_CHANGE) functionalModID = modID; } // Check whether frozen if (freezeList != null) { log.debug("Rocket is in frozen state, adding event " + e + " info freeze list"); freezeList.add(e); return; } log.debug("Firing rocket change event " + e); // Notify all components first Iterator<RocketComponent> iterator = this.iterator(true); while (iterator.hasNext()) { iterator.next().componentChanged(e); } // Notify all listeners // Copy the list before iterating to prevent concurrent modification exceptions. EventListener[] list = listenerList.toArray(new EventListener[0]); for (EventListener l : list) { if (l instanceof ComponentChangeListener) { ((ComponentChangeListener) l).componentChanged(e); } else if (l instanceof StateChangeListener) { ((StateChangeListener) l).stateChanged(e); } } } finally { mutex.unlock("fireComponentChangeEvent"); } } /** * Freezes the rocket structure from firing any events. This may be performed to * combine several actions on the structure into a single large action. * <code>thaw()</code> must always be called afterwards. * * NOTE: Always use a try/finally to ensure <code>thaw()</code> is called: * <pre> * Rocket r = c.getRocket(); * try { * r.freeze(); * // do stuff * } finally { * r.thaw(); * } * </pre> * * @see #thaw() */ public void freeze() { checkState(); if (freezeList == null) { freezeList = new LinkedList<ComponentChangeEvent>(); log.debug("Freezing Rocket"); } else { Application.getExceptionHandler().handleErrorCondition("Attempting to freeze Rocket when it is already frozen, " + "freezeList=" + freezeList); } } /** * Thaws a frozen rocket structure and fires a combination of the events fired during * the freeze. The event type is a combination of those fired and the source is the * last component to have been an event source. * * @see #freeze() */ public void thaw() { checkState(); if (freezeList == null) { Application.getExceptionHandler().handleErrorCondition("Attempting to thaw Rocket when it is not frozen"); return; } if (freezeList.size() == 0) { log.warn("Thawing rocket with no changes made"); freezeList = null; return; } log.debug("Thawing rocket, freezeList=" + freezeList); int type = 0; Object c = null; for (ComponentChangeEvent e : freezeList) { type = type | e.getType(); c = e.getSource(); } freezeList = null; 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 * be used in simulations not relating to the UI. * * @return the default {@link Configuration}. */ public Configuration getDefaultConfiguration() { checkState(); return defaultConfiguration; } /** * Return an array of the flight configuration IDs. This array is guaranteed * to contain the <code>null</code> ID as the first element. * * @return an array of the flight configuration IDs. */ public String[] getFlightConfigurationIDs() { checkState(); return flightConfigurationIDs.toArray(new String[0]); } /** * Add a new flight configuration ID to the flight configurations. The new ID * is returned. * * @return the new flight configuration ID. */ public String newFlightConfigurationID() { checkState(); String id = UUID.randomUUID().toString(); flightConfigurationIDs.add(id); fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); return id; } /** * Add a specified motor configuration ID to the motor configurations. * * @param id the motor configuration ID. * @return true if successful, false if the ID was already used. */ public boolean addMotorConfigurationID(String id) { checkState(); if (id == null || flightConfigurationIDs.contains(id)) return false; flightConfigurationIDs.add(id); fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); return true; } /** * Remove a flight 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 flight configuration ID to remove */ public void removeFlightConfigurationID(String id) { checkState(); if (id == null) return; // Get current configuration: String currentId = getDefaultConfiguration().getFlightConfigurationID(); // If we're removing the current configuration, we need to switch to a different one first. if (currentId != null && currentId.equals(id)) { getDefaultConfiguration().setFlightConfigurationID(null); } flightConfigurationIDs.remove(id); fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); } /** * Check whether <code>id</code> is a valid motor configuration ID. * * @param id the configuration ID. * @return whether a motor configuration with that ID exists. */ public boolean isFlightConfigurationID(String id) { checkState(); return flightConfigurationIDs.contains(id); } /** * Check whether the given motor configuration ID has motors defined for it. * * @param id the motor configuration ID (may be invalid). * @return whether any motors are defined for it. */ public boolean hasMotors(String id) { checkState(); if (id == null) return false; Iterator<RocketComponent> iterator = this.iterator(); while (iterator.hasNext()) { RocketComponent c = iterator.next(); if (c instanceof MotorMount) { MotorMount mount = (MotorMount) c; if (!mount.isMotorMount()) continue; if (mount.getMotorConfiguration().get(id).getMotor() != null) { return true; } } } return false; } /** * Return the user-set name of the flight configuration. If no name has been set, * returns the default name ({@link #DEFAULT_NAME}). * * @param id the flight configuration id * @return the configuration name */ public String getFlightConfigurationName(String id) { checkState(); if (!isFlightConfigurationID(id)) return DEFAULT_NAME; String s = flightConfigurationNames.get(id); if (s == null) return DEFAULT_NAME; return s; } /** * Set the name of the flight configuration. A name can be unset by passing * <code>null</code> or an empty string. * * @param id the flight configuration id * @param name the name for the flight configuration */ public void setFlightConfigurationName(String id, String name) { checkState(); if (name == null || name.equals("") || DEFAULT_NAME.equals(name)) { flightConfigurationNames.remove(id); } else { flightConfigurationNames.put(id, name); } fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } //////// Obligatory component information @Override public String getComponentName() { //// Rocket return trans.get("Rocket.compname.Rocket"); } @Override public Coordinate getComponentCG() { return new Coordinate(0, 0, 0, 0); } @Override public double getComponentMass() { return 0; } @Override public double getLongitudinalUnitInertia() { return 0; } @Override public double getRotationalUnitInertia() { return 0; } @Override 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. */ @Override public boolean isCompatible(Class<? extends RocketComponent> type) { return (Stage.class.isAssignableFrom(type)); } }