1 package net.sf.openrocket.rocketcomponent;
3 import java.util.ArrayList;
4 import java.util.Collection;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.Iterator;
8 import java.util.LinkedList;
10 import java.util.UUID;
12 import javax.swing.event.ChangeListener;
13 import javax.swing.event.EventListenerList;
15 import net.sf.openrocket.motor.Motor;
16 import net.sf.openrocket.util.Chars;
17 import net.sf.openrocket.util.Coordinate;
18 import net.sf.openrocket.util.MathUtil;
19 import net.sf.openrocket.util.UniqueID;
23 * Base for all rocket components. This is the "starting point" for all rocket trees.
24 * It provides the actual implementations of several methods defined in RocketComponent
25 * (eg. the rocket listener lists) and the methods defined in RocketComponent call these.
26 * It also defines some other methods that concern the whole rocket, and helper methods
27 * that keep information about the program state.
29 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
32 public class Rocket extends RocketComponent {
33 public static final double DEFAULT_REFERENCE_LENGTH = 0.01;
35 private static final boolean DEBUG_LISTENERS = false;
39 * List of component change listeners.
41 private EventListenerList listenerList = new EventListenerList();
44 * When freezeList != null, events are not dispatched but stored in the list.
45 * When the structure is thawed, a single combined event will be fired.
47 private List<ComponentChangeEvent> freezeList = null;
51 private int massModID;
52 private int aeroModID;
53 private int treeModID;
54 private int functionalModID;
57 private ReferenceType refType = ReferenceType.MAXIMUM; // Set in constructor
58 private double customReferenceLength = DEFAULT_REFERENCE_LENGTH;
61 // The default configuration used in dialogs
62 private final Configuration defaultConfiguration;
65 private String designer = "";
66 private String revision = "";
69 // Motor configuration list
70 private ArrayList<String> motorConfigurationIDs = new ArrayList<String>();
71 private HashMap<String, String> motorConfigurationNames = new HashMap<String, String>();
73 motorConfigurationIDs.add(null);
77 // Does the rocket have a perfect finish (a notable amount of laminar flow)
78 private boolean perfectFinish = false;
82 ///////////// Constructor /////////////
85 super(RocketComponent.Position.AFTER);
86 modID = UniqueID.next();
90 functionalModID = modID;
91 defaultConfiguration = new Configuration(this);
96 public String getDesigner() {
101 public void setDesigner(String s) {
105 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
109 public String getRevision() {
114 public void setRevision(String s) {
118 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
125 * Return the number of stages in this rocket.
127 * @return the number of stages in this rocket.
129 public int getStageCount() {
131 return this.getChildCount();
137 * Return the non-negative modification ID of this rocket. The ID is changed
138 * every time any change occurs in the rocket. This can be used to check
139 * whether it is necessary to void cached data in cases where listeners can not
140 * or should not be used.
142 * Three other modification IDs are also available, {@link #getMassModID()},
143 * {@link #getAerodynamicModID()} {@link #getTreeModID()}, which change every time
144 * a mass change, aerodynamic change, or tree change occur. Even though the values
145 * of the different modification ID's may be equal, they should be treated totally
148 * Note that undo events restore the modification IDs that were in use at the
149 * corresponding undo level. Subsequent modifications, however, produce modIDs
150 * distinct from those already used.
152 * @return a unique ID number for this modification state.
154 public int getModID() {
159 * Return the non-negative mass modification ID of this rocket. See
160 * {@link #getModID()} for details.
162 * @return a unique ID number for this mass-modification state.
164 public int getMassModID() {
169 * Return the non-negative aerodynamic modification ID of this rocket. See
170 * {@link #getModID()} for details.
172 * @return a unique ID number for this aerodynamic-modification state.
174 public int getAerodynamicModID() {
179 * Return the non-negative tree modification ID of this rocket. See
180 * {@link #getModID()} for details.
182 * @return a unique ID number for this tree-modification state.
184 public int getTreeModID() {
189 * Return the non-negative functional modificationID of this rocket.
190 * This changes every time a functional change occurs.
192 * @return a unique ID number for this functional modification state.
194 public int getFunctionalModID() {
195 return functionalModID;
201 public ReferenceType getReferenceType() {
206 public void setReferenceType(ReferenceType type) {
210 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
214 public double getCustomReferenceLength() {
216 return customReferenceLength;
219 public void setCustomReferenceLength(double length) {
220 if (MathUtil.equals(customReferenceLength, length))
223 this.customReferenceLength = Math.max(length, 0.001);
225 if (refType == ReferenceType.CUSTOM) {
226 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
235 * Set whether the rocket has a perfect finish. This will affect whether the
236 * boundary layer is assumed to be fully turbulent or not.
238 * @param perfectFinish whether the finish is perfect.
240 public void setPerfectFinish(boolean perfectFinish) {
241 if (this.perfectFinish == perfectFinish)
243 this.perfectFinish = perfectFinish;
244 fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
250 * Get whether the rocket has a perfect finish.
252 * @return the perfectFinish
254 public boolean isPerfectFinish() {
255 return perfectFinish;
263 * Make a deep copy of the Rocket structure. This method is exposed as public to allow
264 * for undo/redo system functionality.
267 @SuppressWarnings("unchecked")
268 public Rocket copyWithOriginalID() {
269 Rocket copy = (Rocket) super.copyWithOriginalID();
270 copy.motorConfigurationIDs = (ArrayList<String>) this.motorConfigurationIDs.clone();
271 copy.motorConfigurationNames =
272 (HashMap<String, String>) this.motorConfigurationNames.clone();
273 copy.resetListeners();
279 * Load the rocket structure from the source. The method loads the fields of this
280 * Rocket object and copies the references to siblings from the <code>source</code>.
281 * The object <code>source</code> should not be used after this call, as it is in
284 * This method is meant to be used in conjunction with undo/redo functionality,
285 * and therefore fires an UNDO_EVENT, masked with all applicable mass/aerodynamic/tree
288 @SuppressWarnings("unchecked")
289 public void loadFrom(Rocket r) {
292 int type = ComponentChangeEvent.UNDO_CHANGE | ComponentChangeEvent.NONFUNCTIONAL_CHANGE;
293 if (this.massModID != r.massModID)
294 type |= ComponentChangeEvent.MASS_CHANGE;
295 if (this.aeroModID != r.aeroModID)
296 type |= ComponentChangeEvent.AERODYNAMIC_CHANGE;
297 if (this.treeModID != r.treeModID)
298 type |= ComponentChangeEvent.TREE_CHANGE;
300 this.modID = r.modID;
301 this.massModID = r.massModID;
302 this.aeroModID = r.aeroModID;
303 this.treeModID = r.treeModID;
304 this.functionalModID = r.functionalModID;
305 this.refType = r.refType;
306 this.customReferenceLength = r.customReferenceLength;
308 this.motorConfigurationIDs = (ArrayList<String>) r.motorConfigurationIDs.clone();
309 this.motorConfigurationNames =
310 (HashMap<String, String>) r.motorConfigurationNames.clone();
311 this.perfectFinish = r.perfectFinish;
313 String id = defaultConfiguration.getMotorConfigurationID();
314 if (!this.motorConfigurationIDs.contains(id))
315 defaultConfiguration.setMotorConfigurationID(null);
317 fireComponentChangeEvent(type);
323 /////// Implement the ComponentChangeListener lists
326 * Creates a new EventListenerList for this component. This is necessary when cloning
329 public void resetListeners() {
330 // System.out.println("RESETTING LISTENER LIST of Rocket "+this);
331 listenerList = new EventListenerList();
335 public void printListeners() {
336 System.out.println("" + this + " has " + listenerList.getListenerCount() + " listeners:");
337 Object[] list = listenerList.getListenerList();
338 for (int i = 1; i < list.length; i += 2)
339 System.out.println(" " + ((i + 1) / 2) + ": " + list[i]);
343 public void addComponentChangeListener(ComponentChangeListener l) {
345 listenerList.add(ComponentChangeListener.class, l);
347 System.out.println(this + ": Added listner (now " + listenerList.getListenerCount() +
348 " listeners): " + l);
352 public void removeComponentChangeListener(ComponentChangeListener l) {
353 listenerList.remove(ComponentChangeListener.class, l);
355 System.out.println(this + ": Removed listner (now " + listenerList.getListenerCount() +
356 " listeners): " + l);
361 public void addChangeListener(ChangeListener l) {
363 listenerList.add(ChangeListener.class, l);
365 System.out.println(this + ": Added listner (now " + listenerList.getListenerCount() +
366 " listeners): " + l);
370 public void removeChangeListener(ChangeListener l) {
371 listenerList.remove(ChangeListener.class, l);
373 System.out.println(this + ": Removed listner (now " + listenerList.getListenerCount() +
374 " listeners): " + l);
379 protected void fireComponentChangeEvent(ComponentChangeEvent e) {
382 // Update modification ID's only for normal (not undo/redo) events
383 if (!e.isUndoChange()) {
384 modID = UniqueID.next();
385 if (e.isMassChange())
387 if (e.isAerodynamicChange())
389 if (e.isTreeChange())
391 if (e.getType() != ComponentChangeEvent.NONFUNCTIONAL_CHANGE)
392 functionalModID = modID;
396 System.out.println("FIRING " + e);
398 // Check whether frozen
399 if (freezeList != null) {
404 // Notify all components first
405 Iterator<RocketComponent> iterator = this.deepIterator(true);
406 while (iterator.hasNext()) {
407 iterator.next().componentChanged(e);
410 // Notify all listeners
411 Object[] listeners = listenerList.getListenerList();
412 for (int i = listeners.length - 2; i >= 0; i -= 2) {
413 if (listeners[i] == ComponentChangeListener.class) {
414 ((ComponentChangeListener) listeners[i + 1]).componentChanged(e);
415 } else if (listeners[i] == ChangeListener.class) {
416 ((ChangeListener) listeners[i + 1]).stateChanged(e);
423 * Freezes the rocket structure from firing any events. This may be performed to
424 * combine several actions on the structure into a single large action.
425 * <code>thaw()</code> must always be called afterwards.
427 * NOTE: Always use a try/finally to ensure <code>thaw()</code> is called:
429 * Rocket r = c.getRocket();
440 public void freeze() {
442 if (freezeList == null) {
443 freezeList = new LinkedList<ComponentChangeEvent>();
448 * Thaws a frozen rocket structure and fires a combination of the events fired during
449 * the freeze. The event type is a combination of those fired and the source is the
450 * last component to have been an event source.
456 if (freezeList == null)
458 if (freezeList.size() == 0) {
465 for (ComponentChangeEvent e : freezeList) {
466 type = type | e.getType();
471 fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent) c, type));
477 //////// Motor configurations ////////
481 * Return the default configuration. This should be used in the user interface
482 * to ensure a consistent rocket configuration between dialogs. It should NOT
483 * be used in simulations not relating to the UI.
485 * @return the default {@link Configuration}.
487 public Configuration getDefaultConfiguration() {
489 return defaultConfiguration;
494 * Return an array of the motor configuration IDs. This array is guaranteed
495 * to contain the <code>null</code> ID as the first element.
497 * @return an array of the motor configuration IDs.
499 public String[] getMotorConfigurationIDs() {
501 return motorConfigurationIDs.toArray(new String[0]);
505 * Add a new motor configuration ID to the motor configurations. The new ID
508 * @return the new motor configuration ID.
510 public String newMotorConfigurationID() {
512 String id = UUID.randomUUID().toString();
513 motorConfigurationIDs.add(id);
514 fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
519 * Add a specified motor configuration ID to the motor configurations.
521 * @param id the motor configuration ID.
522 * @return true if successful, false if the ID was already used.
524 public boolean addMotorConfigurationID(String id) {
526 if (id == null || motorConfigurationIDs.contains(id))
528 motorConfigurationIDs.add(id);
529 fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
534 * Remove a motor configuration ID from the configuration IDs. The <code>null</code>
535 * ID cannot be removed, and an attempt to remove it will be silently ignored.
537 * @param id the motor configuration ID to remove
539 public void removeMotorConfigurationID(String id) {
543 motorConfigurationIDs.remove(id);
544 fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
549 * Check whether <code>id</code> is a valid motor configuration ID.
551 * @param id the configuration ID.
552 * @return whether a motor configuration with that ID exists.
554 public boolean isMotorConfigurationID(String id) {
556 return motorConfigurationIDs.contains(id);
562 * Check whether the given motor configuration ID has motors defined for it.
564 * @param id the motor configuration ID (may be invalid).
565 * @return whether any motors are defined for it.
567 public boolean hasMotors(String id) {
572 Iterator<RocketComponent> iterator = this.deepIterator();
573 while (iterator.hasNext()) {
574 RocketComponent c = iterator.next();
576 if (c instanceof MotorMount) {
577 MotorMount mount = (MotorMount) c;
578 if (!mount.isMotorMount())
580 if (mount.getMotor(id) != null) {
590 * Return the user-set name of the motor configuration. If no name has been set,
591 * returns an empty string (not null).
593 * @param id the motor configuration id
594 * @return the configuration name
596 public String getMotorConfigurationName(String id) {
598 if (!isMotorConfigurationID(id))
600 String s = motorConfigurationNames.get(id);
608 * Set the name of the motor configuration. A name can be unset by passing
609 * <code>null</code> or an empty string.
611 * @param id the motor configuration id
612 * @param name the name for the motor configuration
614 public void setMotorConfigurationName(String id, String name) {
616 motorConfigurationNames.put(id, name);
617 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
622 * Return either the motor configuration name (if set) or its description.
624 * @param id the motor configuration ID.
625 * @return a textual representation of the configuration
627 public String getMotorConfigurationNameOrDescription(String id) {
631 name = getMotorConfigurationName(id);
632 if (name != null && !name.equals(""))
635 return getMotorConfigurationDescription(id);
639 * Return a description for the motor configuration, generated from the motor
640 * designations of the components.
642 * @param id the motor configuration ID.
643 * @return a textual representation of the configuration
645 @SuppressWarnings("null")
646 public String getMotorConfigurationDescription(String id) {
651 // Generate the description
653 // First iterate over each stage and store the designations of each motor
654 List<List<String>> list = new ArrayList<List<String>>();
655 List<String> currentList = null;
657 Iterator<RocketComponent> iterator = this.deepIterator();
658 while (iterator.hasNext()) {
659 RocketComponent c = iterator.next();
661 if (c instanceof Stage) {
663 currentList = new ArrayList<String>();
664 list.add(currentList);
666 } else if (c instanceof MotorMount) {
668 MotorMount mount = (MotorMount) c;
669 Motor motor = mount.getMotor(id);
671 if (mount.isMotorMount() && motor != null) {
672 String designation = motor.getDesignation(mount.getMotorDelay(id));
674 for (int i = 0; i < mount.getMotorCount(); i++) {
675 currentList.add(designation);
683 if (motorCount == 0) {
684 return "[No motors]";
687 // Change multiple occurrences of a motor to n x motor
688 List<String> stages = new ArrayList<String>();
690 for (List<String> stage : list) {
691 String stageName = "";
692 String previous = null;
695 Collections.sort(stage);
696 for (String current : stage) {
697 if (current.equals(previous)) {
703 if (previous != null) {
706 s = "" + count + Chars.TIMES + previous;
711 if (stageName.equals(""))
714 stageName = stageName + "," + s;
722 if (previous != null) {
725 s = "" + count + Chars.TIMES + previous;
730 if (stageName.equals(""))
733 stageName = stageName + "," + s;
736 stages.add(stageName);
740 for (int i = 0; i < stages.size(); i++) {
741 String s = stages.get(i);
747 name = name + "; " + s;
755 //////// Obligatory component information
759 public String getComponentName() {
764 public Coordinate getComponentCG() {
765 return new Coordinate(0, 0, 0, 0);
769 public double getComponentMass() {
774 public double getLongitudalUnitInertia() {
779 public double getRotationalUnitInertia() {
784 public Collection<Coordinate> getComponentBounds() {
785 return Collections.emptyList();
789 public boolean isAerodynamic() {
794 public boolean isMassive() {
799 public boolean allowsChildren() {
804 * Allows only <code>Stage</code> components to be added to the type Rocket.
807 public boolean isCompatible(Class<? extends RocketComponent> type) {
808 return (Stage.class.isAssignableFrom(type));