package net.sf.openrocket.rocketcomponent; import java.util.Collection; import java.util.EventObject; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import net.sf.openrocket.appearance.Appearance; import net.sf.openrocket.appearance.Decal; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.ArrayList; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.ChangeSource; import net.sf.openrocket.util.Color; import net.sf.openrocket.util.ComponentChangeAdapter; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.Invalidator; import net.sf.openrocket.util.LineStyle; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.SafetyMutex; import net.sf.openrocket.util.SimpleStack; import net.sf.openrocket.util.StateChangeListener; import net.sf.openrocket.util.UniqueID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class RocketComponent implements ChangeSource, Cloneable, Iterable<RocketComponent> { private static final Logger log = LoggerFactory.getLogger(RocketComponent.class); // Because of changes to Java 1.7.0-45's mechanism to construct DataFlavor objects (used in Drag and Drop) // We cannot access static members of the Application object in this class. Instead of holding // on to the Translator object, we'll just use when we need it. //private static final Translator trans = Application.getTranslator(); /* * Text is suitable to the form * Position relative to: <title> */ public enum Position { /** Position relative to the top of the parent component. */ //// Top of the parent component TOP(Application.getTranslator().get("RocketComponent.Position.TOP")), /** Position relative to the middle of the parent component. */ //// Middle of the parent component MIDDLE(Application.getTranslator().get("RocketComponent.Position.MIDDLE")), /** Position relative to the bottom of the parent component. */ //// Bottom of the parent component BOTTOM(Application.getTranslator().get("RocketComponent.Position.BOTTOM")), /** Position after the parent component (for body components). */ //// After the parent component AFTER(Application.getTranslator().get("RocketComponent.Position.AFTER")), /** Specify an absolute X-coordinate position. */ //// Tip of the nose cone ABSOLUTE(Application.getTranslator().get("RocketComponent.Position.ABSOLUTE")); private String title; Position(String title) { this.title = title; } @Override public String toString() { return title; } } /** * A safety mutex that can be used to prevent concurrent access to this component. */ protected SafetyMutex mutex = SafetyMutex.newInstance(); //////// Parent/child trees /** * Parent component of the current component, or null if none exists. */ private RocketComponent parent = null; /** * List of child components of this component. */ private ArrayList<RocketComponent> children = new ArrayList<RocketComponent>(); //////// Parameters common to all components: /** * Characteristic length of the component. This is used in calculating the coordinate * transformations and positions of other components in reference to this component. * This may and should be used as the "true" length of the component, where applicable. * By default it is zero, i.e. no translation. */ protected double length = 0; /** * Positioning of this component relative to the parent component. */ protected Position relativePosition; /** * Offset of the position of this component relative to the normal position given by * relativePosition. By default zero, i.e. no position change. */ 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; private double overrideCGX = 0; private boolean cgOverriden = false; private boolean overrideSubcomponents = false; // User-given name of the component private String name = null; // User-specified comment private String comment = ""; // Unique ID of the component private String id = null; // Preset component this component is based upon private ComponentPreset presetComponent = null; // The realistic appearance of this component private Appearance appearance = null; /** * Used to invalidate the component after calling {@link #copyFrom(RocketComponent)}. */ private Invalidator invalidator = new Invalidator(this); //// 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. */ public RocketComponent(Position relativePosition) { // These must not fire any events, due to Rocket undo system initialization this.name = getComponentName(); this.relativePosition = relativePosition; newID(); } //////////// 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 /** * Return the component mass (regardless of mass overriding). */ 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 /** * Return the longitudinal (around the y- or z-axis) unitary moment of inertia. * The unitary moment of inertia is the moment of inertia with the assumption that * the mass of the component is one kilogram. The inertia is measured in * respect to the non-overridden CG. * * @return the longitudinal unitary moment of inertia of this component. */ public abstract double getLongitudinalUnitInertia(); /** * Return the rotational (around the x-axis) unitary moment of inertia. * The unitary moment of inertia is the moment of inertia with the assumption that * the mass of the component is one kilogram. The inertia is measured in * respect to the non-overridden CG. * * @return the rotational unitary moment of inertia of this component. */ 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 * is enforced by the <code>addChild()</code> methods. The return value of this method * may change to reflect the current state of this component (e.g. two components of some * type cannot be placed as children). * * @param type The RocketComponent class type to add. * @return Whether such a component can be added. */ public abstract boolean isCompatible(Class<? extends RocketComponent> type); /* Non-abstract helper method */ /** * Test whether the given component can be added to this component. This is equivalent * to calling <code>isCompatible(c.getClass())</code>. * * @param c Component to test. * @return Whether the component can be added. * @see #isCompatible(Class) */ public final boolean isCompatible(RocketComponent c) { mutex.verify(); 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 //////////// /** * 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. * If the component is clustered, then a new array must be returned with a * coordinate for each cluster. * <p> * The default implementation simply returns the array, and thus produces no shift. * * @param c an array of coordinates to shift. * @return an array of shifted coordinates. The method may modify the contents * of the passed array and return the array itself. */ public Coordinate[] shiftCoordinates(Coordinate[] c) { checkState(); return c; } /** * Called when any component in the tree fires a ComponentChangeEvent. This is by * default a no-op, but subclasses may override this method to e.g. invalidate * cached data. The overriding method *must* call * <code>super.componentChanged(e)</code> at some point. * * @param e The event fired */ protected void componentChanged(ComponentChangeEvent e) { // No-op checkState(); } /** * Return the user-provided name of the component, or the component base * name if the user-provided name is empty. This can be used in the UI. * * @return A string describing the component. */ @Override public final String toString() { mutex.verify(); if (name.length() == 0) return getComponentName(); else return name; } /** * Create a string describing the basic component structure from this component downwards. * @return a string containing the rocket structure */ public final String toDebugString() { mutex.lock("toDebugString"); try { StringBuilder sb = new StringBuilder(); toDebugString(sb); return sb.toString(); } finally { mutex.unlock("toDebugString"); } } private void toDebugString(StringBuilder sb) { sb.append(this.getClass().getSimpleName()).append('@').append(System.identityHashCode(this)); sb.append("[\"").append(this.getName()).append('"'); for (RocketComponent c : this.children) { sb.append("; "); c.toDebugString(sb); } sb.append(']'); } /** * 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.iterator(true); while (iterator.hasNext()) { iterator.next().newID(); } return clone; } /** * Make a deep copy of the rocket component tree structure from this component * 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.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. */ protected RocketComponent copyWithOriginalID() { mutex.lock("copyWithOriginalID"); try { checkState(); RocketComponent clone; try { clone = (RocketComponent) this.clone(); } catch (CloneNotSupportedException e) { throw new BugException("CloneNotSupportedException encountered, report a bug!", e); } // Reset the mutex clone.mutex = SafetyMutex.newInstance(); // 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.copyWithOriginalID(); // Don't use add method since it fires events clone.children.add(childCopy); childCopy.parent = clone; } this.checkComponentStructure(); clone.checkComponentStructure(); return clone; } finally { mutex.unlock("copyWithOriginalID"); } } ////////////// Methods that may not be overridden //////////// ////////// Common parameter setting/getting ////////// /** * Get the realistic appearance of this component. * <code>null</code> = use the default for this material * * @return The component's realistic appearance, or <code>null</code> */ public Appearance getAppearance() { return appearance; } /** * Set the realistic appearance of this component. * Use <code>null</code> for default. * * @param appearance */ public void setAppearance(Appearance appearance) { this.appearance = appearance; if (this.appearance != null) { Decal d = this.appearance.getTexture(); if (d != null) { d.getImage().addChangeListener(new StateChangeListener() { @Override public void stateChanged(EventObject e) { fireComponentChangeEvent(ComponentChangeEvent.TEXTURE_CHANGE); } }); } } // CHECK - should this be a TEXTURE_CHANGE and not NONFUNCTIONAL_CHANGE? fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } /** * Return the color of the object to use in 2D figures, or <code>null</code> * to use the default color. */ public final Color getColor() { mutex.verify(); return color; } /** * Set the color of the object to use in 2D figures. */ public final void setColor(Color c) { if ((color == null && c == null) || (color != null && color.equals(c))) return; checkState(); this.color = c; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } public final LineStyle getLineStyle() { mutex.verify(); return lineStyle; } public final void setLineStyle(LineStyle style) { if (this.lineStyle == style) return; checkState(); this.lineStyle = style; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } /** * Get the current override mass. The mass is not necessarily in use * at the moment. * * @return the override mass */ public final double getOverrideMass() { mutex.verify(); return overrideMass; } /** * Set the current override mass. The mass is not set to use by this * method. * * @param m the override mass */ public final void setOverrideMass(double m) { if (MathUtil.equals(m, overrideMass)) return; checkState(); overrideMass = Math.max(m, 0); if (massOverriden) fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } /** * Return whether mass override is active for this component. This does NOT * take into account whether a parent component is overriding the mass. * * @return whether the mass is overridden */ public final boolean isMassOverridden() { mutex.verify(); return massOverriden; } /** * Set whether the mass is currently overridden. * * @param o whether the mass is overridden */ public final void setMassOverridden(boolean o) { if (massOverriden == o) { return; } checkState(); 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() { mutex.verify(); 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() { mutex.verify(); return overrideCGX; } /** * Set the current override CG to (x,0,0). * * @param x the x-coordinate of the override CG to set. */ public final void setOverrideCGX(double x) { if (MathUtil.equals(overrideCGX, x)) return; checkState(); this.overrideCGX = x; if (isCGOverridden()) fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); else fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } /** * Return whether the CG is currently overridden. * * @return whether the CG is overridden */ public final boolean isCGOverridden() { mutex.verify(); return cgOverriden; } /** * Set whether the CG is currently overridden. * * @param o whether the CG is overridden */ public final void setCGOverridden(boolean o) { if (cgOverriden == o) { return; } checkState(); cgOverriden = o; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } /** * Return whether the mass and/or CG override overrides all subcomponent values * as well. The default implementation is a normal getter/setter implementation, * however, subclasses are allowed to override this behavior if some subclass * always or never overrides subcomponents. In this case the subclass should * also override {@link #isOverrideSubcomponentsEnabled()} to return * <code>false</code>. * * @return whether the current mass and/or CG override overrides subcomponents as well. */ public boolean getOverrideSubcomponents() { mutex.verify(); return overrideSubcomponents; } /** * Set whether the mass and/or CG override overrides all subcomponent values * as well. See {@link #getOverrideSubcomponents()} for details. * * @param override whether the mass and/or CG override overrides all subcomponent. */ public void setOverrideSubcomponents(boolean override) { if (overrideSubcomponents == override) { return; } checkState(); overrideSubcomponents = override; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } /** * Return whether the option to override all subcomponents is enabled or not. * The default implementation returns <code>false</code> if neither mass nor * CG is overridden, <code>true</code> otherwise. * <p> * This method may be overridden if the setting of overriding subcomponents * cannot be set. * * @return whether the option to override subcomponents is currently enabled. */ public boolean isOverrideSubcomponentsEnabled() { mutex.verify(); return isCGOverridden() || isMassOverridden(); } /** * Get the user-defined name of the component. */ public final String getName() { mutex.verify(); return name; } /** * Set the user-defined name of the component. If name==null, sets the name to * the default name, currently the component name. */ public final void setName(String name) { if (this.name.equals(name)) { return; } checkState(); if (name == null || name.matches("^\\s*$")) this.name = getComponentName(); else this.name = name; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } /** * Return the comment of the component. The component may contain multiple lines * using \n as a newline separator. * * @return the comment of the component. */ public final String getComment() { mutex.verify(); return comment; } /** * Set the comment of the component. * * @param comment the comment of the component. */ public final void setComment(String comment) { if (this.comment.equals(comment)) return; checkState(); if (comment == null) this.comment = ""; else this.comment = comment; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } /** * Return the preset component that this component is based upon. * * @return the preset component, or <code>null</code> if this is not based on a preset. */ public final ComponentPreset getPresetComponent() { return presetComponent; } /** * Return the most compatible preset type for this component. * This method should be overridden by components which have presets * * @return the most compatible ComponentPreset.Type or <code>null</code> if no presets are compatible. */ public ComponentPreset.Type getPresetType() { return null; } /** * Set the preset component this component is based upon and load all of the * preset values. * * @param preset the preset component to load, or <code>null</code> to clear the preset. */ public final void loadPreset(ComponentPreset preset) { if (presetComponent == preset) { return; } if (preset == null) { clearPreset(); return; } // TODO - do we need to this compatibility check? /* if (preset.getComponentClass() != this.getClass()) { throw new IllegalArgumentException("Attempting to load preset of type " + preset.getComponentClass() + " into component of type " + this.getClass()); } */ RocketComponent root = getRoot(); final Rocket rocket; if (root instanceof Rocket) { rocket = (Rocket) root; } else { rocket = null; } try { if (rocket != null) { rocket.freeze(); } loadFromPreset(preset); this.presetComponent = preset; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } finally { if (rocket != null) { rocket.thaw(); } } } /** * Load component properties from the specified preset. The preset is guaranteed * to be of the correct type. * <p> * This method should fire the appropriate events related to the changes. The rocket * is frozen by the caller, so the events will be automatically combined. * <p> * This method must FIRST perform the preset loading and THEN call super.loadFromPreset(). * This is because mass setting requires the dimensions to be set beforehand. * * @param preset the preset to load from */ protected void loadFromPreset(ComponentPreset preset) { if (preset.has(ComponentPreset.LENGTH)) { this.length = preset.get(ComponentPreset.LENGTH); } } /** * Clear the current component preset. This does not affect the component properties * otherwise. */ public final void clearPreset() { if (presetComponent == null) return; presetComponent = null; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } /** * Returns the unique ID of the component. * * @return the ID of the component. */ public final String getID() { return id; } /** * Generate a new ID for this component. */ private final void newID() { mutex.verify(); 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 * relative to its parent. * * If the length of a component is settable, the class must define the setter method * itself. */ public final double getLength() { mutex.verify(); 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() { mutex.verify(); return relativePosition; } /** * Set the positioning of the component relative to its parent component. * The actual position of the component is maintained to the best ability. * <p> * The default implementation is of protected visibility, since many components * do not support setting the relative position. A component that does support * it should override this with a public method that simply calls this * supermethod AND fire a suitable ComponentChangeEvent. * * @param position the relative positioning. */ protected void setRelativePosition(RocketComponent.Position position) { if (this.relativePosition == position) return; checkState(); // Update position so as not to move the component if (this.parent != null) { double thisPos = this.toRelative(Coordinate.NUL, this.parent)[0].x; 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; break; case BOTTOM: this.position = thisPos - (this.parent.length - this.length); break; default: throw new BugException("Unknown position type: " + position); } } this.relativePosition = position; fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } /** * Determine position relative to given position argument. Note: This is a side-effect free method. No state * is modified. * * @param thePosition the relative position to be used as the basis for the computation * @param relativeTo the position is computed relative the the given component * * @return double position of the component relative to the parent, with respect to <code>position</code> */ public double asPositionValue(Position thePosition, RocketComponent relativeTo) { double result = this.position; if (relativeTo != null) { double thisPos = this.toRelative(Coordinate.NUL, relativeTo)[0].x; switch (thePosition) { case ABSOLUTE: result = this.toAbsolute(Coordinate.NUL)[0].x; break; case TOP: result = thisPos; break; case MIDDLE: result = thisPos - (relativeTo.length - this.length) / 2; break; case BOTTOM: result = thisPos - (relativeTo.length - this.length); break; default: throw new BugException("Unknown position type: " + thePosition); } } return result; } /** * 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() { mutex.verify(); return position; } /** * Set the position value of the component. The exact meaning of the value * depends on the current relative positioning. * <p> * The default implementation is of protected visibility, since many components * do not support setting the relative position. A component that does support * it should override this with a public method that simply calls this * supermethod AND fire a suitable ComponentChangeEvent. * * @param value the position value of the component. */ public void setPositionValue(double value) { if (MathUtil.equals(this.position, value)) return; checkState(); this.position = value; } /////////// Coordinate changes /////////// /** * Returns coordinate c in absolute coordinates. Equivalent to toComponent(c,null). */ public Coordinate[] toAbsolute(Coordinate c) { 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 * absolute coordinates. * <p> * This method returns an array of coordinates, each of which represents a * position of the coordinate in clustered cases. The array is guaranteed * to contain at least one element. * <p> * The current implementation does not support rotating components. * * @param c Coordinate in the component's coordinate system. * @param dest Destination component coordinate system. * @return an array of coordinates describing <code>c</code> in coordinates * relative to <code>dest</code>. */ public final Coordinate[] toRelative(Coordinate c, RocketComponent dest) { checkState(); mutex.lock("toRelative"); try { double absoluteX = Double.NaN; RocketComponent search = dest; Coordinate[] array = new Coordinate[1]; array[0] = c; 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); } 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); } 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); } break; case AFTER: // Add length of all previous brother-components with POSITION_RELATIVE_AFTER int index = component.parent.children.indexOf(component); assert (index >= 0); for (index--; index >= 0; index--) { RocketComponent comp = component.parent.children.get(index); double componentLength = comp.getTotalLength(); for (int i = 0; i < array.length; i++) { array[i] = array[i].add(componentLength, 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 if (Double.isNaN(absoluteX)) { absoluteX = component.position; } break; default: throw new BugException("Unknown relative positioning type of component" + component + ": " + component.relativePosition); } component = component.parent; // parent != null } if (!Double.isNaN(absoluteX)) { 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++) { array[i] = array[i].sub(origin[0]); } } return array; } finally { mutex.unlock("toRelative"); } } /** * Recursively sum the lengths of all subcomponents that have position * Position.AFTER. * * @return Sum of the lengths. */ private final double getTotalLength() { checkState(); this.checkComponentStructure(); mutex.lock("getTotalLength"); try { double l = 0; if (relativePosition == Position.AFTER) l = length; for (int i = 0; i < children.size(); i++) l += children.get(i).getTotalLength(); return l; } finally { mutex.unlock("getTotalLength"); } } /////////// 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() { mutex.verify(); if (massOverriden) return overrideMass; return getComponentMass(); } /** * Return the mass of this component and all of its subcomponents. */ public final double getSectionMass() { Double massSubtotal = getMass(); mutex.verify(); for (RocketComponent rc : children) { massSubtotal += rc.getSectionMass(); } return massSubtotal; } /** * Return the (possibly overridden) center of gravity and mass. * * Returns the CG with the weight of the coordinate set to the weight of the component. * Both CG and mass may be separately overridden. * * @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 longitudinal (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 * and is relative to the non-overridden CG. * * @return the longitudinal moment of inertia of this component. */ public final double getLongitudinalInertia() { checkState(); return getLongitudinalUnitInertia() * getMass(); } /** * Return the rotational (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 * and is relative to the non-overridden CG. * * @return the rotational moment of inertia of this component. */ public final double getRotationalInertia() { checkState(); return getRotationalUnitInertia() * getMass(); } /////////// Children handling /////////// /** * Adds a child to the rocket component tree. The component is added to the end * of the component's child list. This is a helper method that calls * {@link #addChild(RocketComponent,int)}. * * @param component The component to add. * @throws IllegalArgumentException if the component is already part of some * component tree. * @see #addChild(RocketComponent,int) */ public final void addChild(RocketComponent component) { checkState(); addChild(component, children.size()); } /** * Adds a child to the rocket component tree. The component is added to * the given position of the component's child list. * <p> * This method may be overridden to enforce more strict component addition rules. * The tests should be performed first and then this method called. * * @param component The component to add. * @param index Position to add component to. * @throws IllegalArgumentException If the component is already part of * some component tree. */ public void addChild(RocketComponent component, int index) { checkState(); if (component.parent != null) { throw new IllegalArgumentException("component " + component.getComponentName() + " is already in a tree"); } // Ensure that the no loops are created in component tree [A -> X -> Y -> B, B.addChild(A)] if (this.getRoot().equals(component)) { throw new IllegalStateException("Component " + component.getComponentName() + " is a parent of " + this.getComponentName() + ", attempting to create cycle in tree."); } if (!isCompatible(component)) { throw new IllegalStateException("Component " + component.getComponentName() + " not currently compatible with component " + getComponentName()); } children.add(index, component); component.parent = this; this.checkComponentStructure(); component.checkComponentStructure(); fireAddRemoveEvent(component); } /** * Removes a child from the rocket component tree. * * @param n remove the n'th child. * @throws IndexOutOfBoundsException if n is out of bounds */ public final void removeChild(int n) { checkState(); RocketComponent component = children.remove(n); component.parent = null; this.checkComponentStructure(); component.checkComponentStructure(); 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 * @return whether the component was a child */ public final boolean removeChild(RocketComponent component) { checkState(); component.checkComponentStructure(); if (children.remove(component)) { component.parent = null; this.checkComponentStructure(); component.checkComponentStructure(); fireAddRemoveEvent(component); return true; } return false; } /** * Move a child to another position. * * @param component the component to move * @param index the component's new position * @throws IllegalArgumentException If an illegal placement was attempted. */ public final void moveChild(RocketComponent component, int index) { checkState(); if (children.remove(component)) { children.add(index, component); this.checkComponentStructure(); component.checkComponentStructure(); fireAddRemoveEvent(component); } } /** * Fires an AERODYNAMIC_CHANGE, MASS_CHANGE or OTHER_CHANGE event depending on the * type of component removed. */ private void fireAddRemoveEvent(RocketComponent component) { Iterator<RocketComponent> iter = component.iterator(true); int type = ComponentChangeEvent.TREE_CHANGE; while (iter.hasNext()) { RocketComponent c = iter.next(); if (c.isAerodynamic()) type |= ComponentChangeEvent.AERODYNAMIC_CHANGE; if (c.isMassive()) type |= ComponentChangeEvent.MASS_CHANGE; } fireComponentChangeEvent(type); } public final int getChildCount() { checkState(); this.checkComponentStructure(); return children.size(); } public final RocketComponent getChild(int n) { checkState(); this.checkComponentStructure(); return children.get(n); } public final List<RocketComponent> getChildren() { checkState(); this.checkComponentStructure(); return children.clone(); } /** * Returns the position of the child in this components child list, or -1 if the * component is not a child of this component. * * @param child The child to search for. * @return Position in the list or -1 if not found. */ public final int getChildPosition(RocketComponent child) { checkState(); this.checkComponentStructure(); return children.indexOf(child); } /** * Get the parent component of this component. Returns <code>null</code> if the component * has no parent. * * @return The parent of this component or <code>null</code>. */ public final RocketComponent getParent() { checkState(); return parent; } /** * Get the root component of the component tree. * * @return The root component of the component tree. */ public final RocketComponent getRoot() { checkState(); RocketComponent gp = this; while (gp.parent != null) gp = gp.parent; return gp; } /** * Returns the root Rocket component of this component tree. Throws an * IllegalStateException if the root component is not a Rocket. * * @return The root Rocket component of the component tree. * @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; throw new IllegalStateException("getRocket() called with root component " + r.getComponentName()); } /** * Return the Stage component that this component belongs to. Throws an * IllegalStateException if a Stage is not in the parentage of this component. * * @return The Stage component this component belongs to. * @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; c = c.getParent(); } throw new IllegalStateException("getStage() called without Stage as a parent."); } /** * Return the stage number of the stage this component belongs to. The stages * are numbered from zero upwards. * * @return the stage number this component belongs to. */ public final int getStageNumber() { checkState(); if (parent == null) { throw new IllegalArgumentException("getStageNumber() called for root component"); } RocketComponent stage = this; while (!(stage instanceof Stage)) { stage = stage.parent; if (stage == null || stage.parent == null) { throw new IllegalStateException("getStageNumber() could not find parent " + "stage."); } } return stage.parent.getChildPosition(stage); } /** * Find a component with the given ID. The component tree is searched from this component * down (including this component) for the ID and the corresponding component is returned, * or null if not found. * * @param idToFind ID to search for. * @return The component with the ID, or null if not found. */ public final RocketComponent findComponent(String idToFind) { checkState(); Iterator<RocketComponent> iter = this.iterator(true); while (iter.hasNext()) { RocketComponent c = iter.next(); if (c.getID().equals(idToFind)) return c; } return null; } // TODO: Move these methods elsewhere (used only in SymmetricComponent) public final RocketComponent getPreviousComponent() { checkState(); this.checkComponentStructure(); if (parent == null) return null; int pos = parent.getChildPosition(this); if (pos < 0) { StringBuffer sb = new StringBuffer(); sb.append("Inconsistent internal state: "); sb.append("this=").append(this).append('[') .append(System.identityHashCode(this)).append(']'); sb.append(" parent.children=["); 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) sb.append(", "); } sb.append(']'); throw new IllegalStateException(sb.toString()); } assert (pos >= 0); if (pos == 0) return parent; RocketComponent c = parent.getChild(pos - 1); while (c.getChildCount() > 0) c = c.getChild(c.getChildCount() - 1); return c; } // TODO: Move these methods elsewhere (used only in SymmetricComponent) public final RocketComponent getNextComponent() { checkState(); if (getChildCount() > 0) return getChild(0); RocketComponent current = this; RocketComponent nextParent = this.parent; while (nextParent != null) { int pos = nextParent.getChildPosition(current); if (pos < nextParent.getChildCount() - 1) return nextParent.getChild(pos + 1); current = nextParent; nextParent = current.parent; } return null; } /////////// Event handling ////////// // // Listener lists are provided by the root Rocket component, // a single listener list for the whole rocket. // /** * Adds a ComponentChangeListener to the rocket tree. The listener is added to the root * component, which must be of type Rocket (which overrides this method). Events of all * subcomponents are sent to all listeners. * * @throws IllegalStateException - if the root component is not a Rocket */ public void addComponentChangeListener(ComponentChangeListener l) { checkState(); getRocket().addComponentChangeListener(l); } /** * Removes a ComponentChangeListener from the rocket tree. The listener is removed from * the root component, which must be of type Rocket (which overrides this method). * Does nothing if the root component is not a Rocket. (The asymmetry is so * that listeners can always be removed just in case.) * * @param l Listener to remove */ public void removeComponentChangeListener(ComponentChangeListener l) { if (parent != null) { getRoot().removeComponentChangeListener(l); } } /** * Adds a <code>ChangeListener</code> to the rocket tree. This is identical to * <code>addComponentChangeListener()</code> except that it uses a * <code>ChangeListener</code>. The same events are dispatched to the * <code>ChangeListener</code>, as <code>ComponentChangeEvent</code> is a subclass * of <code>ChangeEvent</code>. * * @throws IllegalStateException - if the root component is not a <code>Rocket</code> */ @Override public final void addChangeListener(StateChangeListener l) { addComponentChangeListener(new ComponentChangeAdapter(l)); } /** * Removes a ChangeListener from the rocket tree. This is identical to * removeComponentChangeListener() except it uses a ChangeListener. * Does nothing if the root component is not a Rocket. (The asymmetry is so * that listeners can always be removed just in case.) * * @param l Listener to remove */ @Override public final void removeChangeListener(StateChangeListener l) { removeComponentChangeListener(new ComponentChangeAdapter(l)); } /** * Fires a ComponentChangeEvent on the rocket structure. The call is passed to the * root component, which must be of type Rocket (which overrides this method). * Events of all subcomponents are sent to all listeners. * * If the component tree root is not a Rocket, the event is ignored. This is the * case when constructing components not in any Rocket tree. In this case it * would be impossible for the component to have listeners in any case. * * @param e Event to send */ protected void fireComponentChangeEvent(ComponentChangeEvent e) { 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 * this component. * * @param type Type of event * @see #fireComponentChangeEvent(ComponentChangeEvent) */ protected void fireComponentChangeEvent(int type) { fireComponentChangeEvent(new ComponentChangeEvent(this, type)); } /** * Checks whether this component has been invalidated and should no longer be used. * This is a safety check that in-place replaced components are no longer used. * All non-trivial methods (with the exception of methods simply getting a property) * should call this method before changing or computing anything. * * @throws BugException if this component has been invalidated by {@link #copyFrom(RocketComponent)}. */ protected void checkState() { invalidator.check(true); mutex.verify(); } /** * Check that the local component structure is correct. This can be called after changing * the component structure in order to verify the integrity. * <p> * TODO: Remove this after the "inconsistent internal state" bug has been corrected */ public void checkComponentStructure() { if (this.parent != null) { // Test that this component is found in parent's children with == operator if (!containsExact(this.parent.children, this)) { throw new BugException("Inconsistent component structure detected, parent does not contain this " + "component as a child, parent=" + parent.toDebugString() + " this=" + this.toDebugString()); } } for (RocketComponent child : this.children) { if (child.parent != this) { throw new BugException("Inconsistent component structure detected, child does not have this component " + "as the parent, this=" + this.toDebugString() + " child=" + child.toDebugString() + " child.parent=" + (child.parent == null ? "null" : child.parent.toDebugString())); } } } // Check whether the list contains exactly the searched-for component (with == operator) private boolean containsExact(List<RocketComponent> haystack, RocketComponent needle) { for (RocketComponent c : haystack) { if (needle == c) { return true; } } return false; } /////////// Iterators ////////// /** * Returns an iterator that iterates over all children and sub-children. * <p> * The iterator iterates through all children below this object, including itself if * <code>returnSelf</code> is true. The order of the iteration is not specified * (it may be specified in the future). * <p> * If an iterator iterating over only the direct children of the component is required, * use <code>component.getChildren().iterator()</code>. * * @param returnSelf boolean value specifying whether the component itself should be * returned * @return An iterator for the children and sub-children. */ public final Iterator<RocketComponent> iterator(boolean returnSelf) { checkState(); return new RocketComponentIterator(this, returnSelf); } /** * Returns an iterator that iterates over this component, its children and sub-children. * <p> * This method is equivalent to <code>iterator(true)</code>. * * @return An iterator for this component, its children and sub-children. */ @Override public final Iterator<RocketComponent> iterator() { return iterator(true); } /** * 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(); } ///////////// Visitor pattern implementation public <R> R accept(RocketComponentVisitor<R> visitor) { visitor.visit(this); return visitor.getResult(); } //////////// 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)); } 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)); } protected static final double ringVolume(double outerRadius, double innerRadius, double length) { return ringMass(outerRadius, innerRadius, length, 1.0); } protected static final double ringMass(double outerRadius, double innerRadius, double length, double density) { return Math.PI * (MathUtil.pow2(outerRadius) - MathUtil.pow2(innerRadius)) * length * density; } protected static final double ringLongitudinalUnitInertia(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; } protected static final double ringRotationalUnitInertia(double outerRadius, double innerRadius) { // 1/2 * (r1^2 + r2^2) return (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) / 2; } //////////// 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 * mechanism and when converting a finset into a freeform fin set. * This component must not have a parent, otherwise this method will fail. * <p> * The child components in the source tree are copied into the current tree, however, * the original components should not be used since they represent old copies of the * components. It is recommended to invalidate them by calling {@link #invalidate()}. * <p> * This method returns a list of components that should be invalidated after references * to them have been removed (for example by firing appropriate events). The list contains * all children and sub-children of the current component and the entire component * tree of <code>src</code>. * * @return a list of components that should not be used after this call. */ protected List<RocketComponent> copyFrom(RocketComponent src) { checkState(); List<RocketComponent> toInvalidate = new ArrayList<RocketComponent>(); if (this.parent != null) { throw new UnsupportedOperationException("copyFrom called for non-root component, parent=" + this.parent.toDebugString() + ", this=" + this.toDebugString()); } // Add current structure to be invalidated Iterator<RocketComponent> iterator = this.iterator(false); while (iterator.hasNext()) { toInvalidate.add(iterator.next()); } // Remove previous components for (RocketComponent child : this.children) { child.parent = null; } this.children.clear(); // Copy new children to this component for (RocketComponent c : src.children) { RocketComponent copy = c.copyWithOriginalID(); this.children.add(copy); copy.parent = this; } this.checkComponentStructure(); src.checkComponentStructure(); // Set all parameters this.length = src.length; this.relativePosition = src.relativePosition; this.position = src.position; this.color = src.color; this.lineStyle = src.lineStyle; this.overrideMass = src.overrideMass; this.massOverriden = src.massOverriden; this.overrideCGX = src.overrideCGX; this.cgOverriden = src.cgOverriden; this.overrideSubcomponents = src.overrideSubcomponents; this.name = src.name; this.comment = src.comment; this.id = src.id; // Add source components to invalidation tree for (RocketComponent c : src) { toInvalidate.add(c); } return toInvalidate; } protected void invalidate() { invalidator.invalidate(); } ////////// Iterator implementation /////////// /** * Private inner class to implement the Iterator. * * This iterator is fail-fast if the root of the structure is a Rocket. */ private static class RocketComponentIterator implements Iterator<RocketComponent> { // Stack holds iterators which still have some components left. private final SimpleStack<Iterator<RocketComponent>> iteratorStack = new SimpleStack<Iterator<RocketComponent>>(); private final Rocket root; private final int treeModID; private final RocketComponent original; 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; treeModID = root.getTreeModID(); } else { root = null; treeModID = -1; } Iterator<RocketComponent> i = c.children.iterator(); if (i.hasNext()) iteratorStack.push(i); this.original = c; this.returnSelf = returnSelf; } @Override public boolean hasNext() { checkID(); if (returnSelf) return true; return !iteratorStack.isEmpty(); // Elements remain if stack is not empty } @Override public RocketComponent next() { Iterator<RocketComponent> i; checkID(); // Return original component first if (returnSelf) { returnSelf = false; return original; } // Peek first iterator from stack, throw exception if empty i = iteratorStack.peek(); if (i == null) { throw new NoSuchElementException("No further elements in RocketComponent iterator"); } // Retrieve next component of the iterator, remove iterator from stack if empty RocketComponent c = i.next(); if (!i.hasNext()) iteratorStack.pop(); // Add iterator of component children to stack if it has children i = c.children.iterator(); if (i.hasNext()) iteratorStack.push(i); return c; } private void checkID() { if (root != null) { if (root.getTreeModID() != treeModID) { throw new IllegalStateException("Rocket modified while being iterated"); } } } @Override public void remove() { throw new UnsupportedOperationException("remove() not supported by " + "RocketComponent iterator"); } } }