1 package net.sf.openrocket.rocketcomponent;
3 import java.util.Collection;
4 import java.util.Collections;
5 import java.util.HashMap;
6 import java.util.Iterator;
7 import java.util.LinkedList;
11 import javax.swing.event.ChangeListener;
12 import javax.swing.event.EventListenerList;
14 import net.sf.openrocket.gui.main.ExceptionHandler;
15 import net.sf.openrocket.l10n.Translator;
16 import net.sf.openrocket.logging.LogHelper;
17 import net.sf.openrocket.motor.Motor;
18 import net.sf.openrocket.startup.Application;
19 import net.sf.openrocket.util.ArrayList;
20 import net.sf.openrocket.util.Chars;
21 import net.sf.openrocket.util.Coordinate;
22 import net.sf.openrocket.util.MathUtil;
23 import net.sf.openrocket.util.UniqueID;
27 * Base for all rocket components. This is the "starting point" for all rocket trees.
28 * It provides the actual implementations of several methods defined in RocketComponent
29 * (eg. the rocket listener lists) and the methods defined in RocketComponent call these.
30 * It also defines some other methods that concern the whole rocket, and helper methods
31 * that keep information about the program state.
33 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
36 public class Rocket extends RocketComponent {
37 private static final LogHelper log = Application.getLogger();
38 private static final Translator trans = Application.getTranslator();
40 public static final double DEFAULT_REFERENCE_LENGTH = 0.01;
44 * List of component change listeners.
46 private EventListenerList listenerList = new EventListenerList();
49 * When freezeList != null, events are not dispatched but stored in the list.
50 * When the structure is thawed, a single combined event will be fired.
52 private List<ComponentChangeEvent> freezeList = null;
56 private int massModID;
57 private int aeroModID;
58 private int treeModID;
59 private int functionalModID;
62 private ReferenceType refType = ReferenceType.MAXIMUM; // Set in constructor
63 private double customReferenceLength = DEFAULT_REFERENCE_LENGTH;
66 // The default configuration used in dialogs
67 private final Configuration defaultConfiguration;
70 private String designer = "";
71 private String revision = "";
74 // Motor configuration list
75 private ArrayList<String> motorConfigurationIDs = new ArrayList<String>();
76 private HashMap<String, String> motorConfigurationNames = new HashMap<String, String>();
78 motorConfigurationIDs.add(null);
82 // Does the rocket have a perfect finish (a notable amount of laminar flow)
83 private boolean perfectFinish = false;
87 ///////////// Constructor /////////////
90 super(RocketComponent.Position.AFTER);
91 modID = UniqueID.next();
95 functionalModID = modID;
96 defaultConfiguration = new Configuration(this);
101 public String getDesigner() {
106 public void setDesigner(String s) {
110 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
114 public String getRevision() {
119 public void setRevision(String s) {
123 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
130 * Return the number of stages in this rocket.
132 * @return the number of stages in this rocket.
134 public int getStageCount() {
136 return this.getChildCount();
141 * Return the non-negative modification ID of this rocket. The ID is changed
142 * every time any change occurs in the rocket. This can be used to check
143 * whether it is necessary to void cached data in cases where listeners can not
144 * or should not be used.
146 * Three other modification IDs are also available, {@link #getMassModID()},
147 * {@link #getAerodynamicModID()} {@link #getTreeModID()}, which change every time
148 * a mass change, aerodynamic change, or tree change occur. Even though the values
149 * of the different modification ID's may be equal, they should be treated totally
152 * Note that undo events restore the modification IDs that were in use at the
153 * corresponding undo level. Subsequent modifications, however, produce modIDs
154 * distinct from those already used.
156 * @return a unique ID number for this modification state.
158 public int getModID() {
163 * Return the non-negative mass modification ID of this rocket. See
164 * {@link #getModID()} for details.
166 * @return a unique ID number for this mass-modification state.
168 public int getMassModID() {
173 * Return the non-negative aerodynamic modification ID of this rocket. See
174 * {@link #getModID()} for details.
176 * @return a unique ID number for this aerodynamic-modification state.
178 public int getAerodynamicModID() {
183 * Return the non-negative tree modification ID of this rocket. See
184 * {@link #getModID()} for details.
186 * @return a unique ID number for this tree-modification state.
188 public int getTreeModID() {
193 * Return the non-negative functional modificationID of this rocket.
194 * This changes every time a functional change occurs.
196 * @return a unique ID number for this functional modification state.
198 public int getFunctionalModID() {
199 return functionalModID;
205 public ReferenceType getReferenceType() {
210 public void setReferenceType(ReferenceType type) {
214 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
218 public double getCustomReferenceLength() {
220 return customReferenceLength;
223 public void setCustomReferenceLength(double length) {
224 if (MathUtil.equals(customReferenceLength, length))
227 this.customReferenceLength = Math.max(length, 0.001);
229 if (refType == ReferenceType.CUSTOM) {
230 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
239 * Set whether the rocket has a perfect finish. This will affect whether the
240 * boundary layer is assumed to be fully turbulent or not.
242 * @param perfectFinish whether the finish is perfect.
244 public void setPerfectFinish(boolean perfectFinish) {
245 if (this.perfectFinish == perfectFinish)
247 this.perfectFinish = perfectFinish;
248 fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
254 * Get whether the rocket has a perfect finish.
256 * @return the perfectFinish
258 public boolean isPerfectFinish() {
259 return perfectFinish;
267 * Make a deep copy of the Rocket structure. This method is exposed as public to allow
268 * for undo/redo system functionality.
270 @SuppressWarnings("unchecked")
272 public Rocket copyWithOriginalID() {
273 Rocket copy = (Rocket) super.copyWithOriginalID();
274 copy.motorConfigurationIDs = this.motorConfigurationIDs.clone();
275 copy.motorConfigurationNames =
276 (HashMap<String, String>) this.motorConfigurationNames.clone();
277 copy.resetListeners();
283 * Load the rocket structure from the source. The method loads the fields of this
284 * Rocket object and copies the references to siblings from the <code>source</code>.
285 * The object <code>source</code> should not be used after this call, as it is in
288 * This method is meant to be used in conjunction with undo/redo functionality,
289 * and therefore fires an UNDO_EVENT, masked with all applicable mass/aerodynamic/tree
292 @SuppressWarnings("unchecked")
293 public void loadFrom(Rocket r) {
295 // Store list of components to invalidate after event has been fired
296 List<RocketComponent> toInvalidate = this.copyFrom(r);
298 int type = ComponentChangeEvent.UNDO_CHANGE | ComponentChangeEvent.NONFUNCTIONAL_CHANGE;
299 if (this.massModID != r.massModID)
300 type |= ComponentChangeEvent.MASS_CHANGE;
301 if (this.aeroModID != r.aeroModID)
302 type |= ComponentChangeEvent.AERODYNAMIC_CHANGE;
303 // Loading a rocket is always a tree change since the component objects change
304 type |= ComponentChangeEvent.TREE_CHANGE;
306 this.modID = r.modID;
307 this.massModID = r.massModID;
308 this.aeroModID = r.aeroModID;
309 this.treeModID = r.treeModID;
310 this.functionalModID = r.functionalModID;
311 this.refType = r.refType;
312 this.customReferenceLength = r.customReferenceLength;
314 this.motorConfigurationIDs = r.motorConfigurationIDs.clone();
315 this.motorConfigurationNames =
316 (HashMap<String, String>) r.motorConfigurationNames.clone();
317 this.perfectFinish = r.perfectFinish;
319 String id = defaultConfiguration.getMotorConfigurationID();
320 if (!this.motorConfigurationIDs.contains(id))
321 defaultConfiguration.setMotorConfigurationID(null);
323 this.checkComponentStructure();
325 fireComponentChangeEvent(type);
327 // Invalidate obsolete components after event
328 for (RocketComponent c : toInvalidate) {
336 /////// Implement the ComponentChangeListener lists
339 * Creates a new EventListenerList for this component. This is necessary when cloning
342 public void resetListeners() {
343 // System.out.println("RESETTING LISTENER LIST of Rocket "+this);
344 listenerList = new EventListenerList();
348 public void printListeners() {
349 System.out.println("" + this + " has " + listenerList.getListenerCount() + " listeners:");
350 Object[] list = listenerList.getListenerList();
351 for (int i = 1; i < list.length; i += 2)
352 System.out.println(" " + ((i + 1) / 2) + ": " + list[i]);
356 public void addComponentChangeListener(ComponentChangeListener l) {
358 listenerList.add(ComponentChangeListener.class, l);
359 log.verbose("Added ComponentChangeListener " + l + ", current number of listeners is " +
360 listenerList.getListenerCount());
364 public void removeComponentChangeListener(ComponentChangeListener l) {
365 listenerList.remove(ComponentChangeListener.class, l);
366 log.verbose("Removed ComponentChangeListener " + l + ", current number of listeners is " +
367 listenerList.getListenerCount());
372 public void addChangeListener(ChangeListener l) {
374 listenerList.add(ChangeListener.class, l);
375 log.verbose("Added ChangeListener " + l + ", current number of listeners is " +
376 listenerList.getListenerCount());
380 public void removeChangeListener(ChangeListener l) {
381 listenerList.remove(ChangeListener.class, l);
382 log.verbose("Removed ChangeListener " + l + ", current number of listeners is " +
383 listenerList.getListenerCount());
388 protected void fireComponentChangeEvent(ComponentChangeEvent e) {
389 mutex.lock("fireComponentChangeEvent");
393 // Update modification ID's only for normal (not undo/redo) events
394 if (!e.isUndoChange()) {
395 modID = UniqueID.next();
396 if (e.isMassChange())
398 if (e.isAerodynamicChange())
400 if (e.isTreeChange())
402 if (e.getType() != ComponentChangeEvent.NONFUNCTIONAL_CHANGE)
403 functionalModID = modID;
406 // Check whether frozen
407 if (freezeList != null) {
408 log.debug("Rocket is in frozen state, adding event " + e + " info freeze list");
413 log.debug("Firing rocket change event " + e);
415 // Notify all components first
416 Iterator<RocketComponent> iterator = this.iterator(true);
417 while (iterator.hasNext()) {
418 iterator.next().componentChanged(e);
421 // Notify all listeners
422 Object[] listeners = listenerList.getListenerList();
423 for (int i = listeners.length - 2; i >= 0; i -= 2) {
424 if (listeners[i] == ComponentChangeListener.class) {
425 ((ComponentChangeListener) listeners[i + 1]).componentChanged(e);
426 } else if (listeners[i] == ChangeListener.class) {
427 ((ChangeListener) listeners[i + 1]).stateChanged(e);
431 mutex.unlock("fireComponentChangeEvent");
437 * Freezes the rocket structure from firing any events. This may be performed to
438 * combine several actions on the structure into a single large action.
439 * <code>thaw()</code> must always be called afterwards.
441 * NOTE: Always use a try/finally to ensure <code>thaw()</code> is called:
443 * Rocket r = c.getRocket();
454 public void freeze() {
456 if (freezeList == null) {
457 freezeList = new LinkedList<ComponentChangeEvent>();
458 log.debug("Freezing Rocket");
460 ExceptionHandler.handleErrorCondition("Attempting to freeze Rocket when it is already frozen, " +
461 "freezeList=" + freezeList);
466 * Thaws a frozen rocket structure and fires a combination of the events fired during
467 * the freeze. The event type is a combination of those fired and the source is the
468 * last component to have been an event source.
474 if (freezeList == null) {
475 ExceptionHandler.handleErrorCondition("Attempting to thaw Rocket when it is not frozen");
478 if (freezeList.size() == 0) {
479 log.warn("Thawing rocket with no changes made");
484 log.debug("Thawing rocket, freezeList=" + freezeList);
488 for (ComponentChangeEvent e : freezeList) {
489 type = type | e.getType();
494 fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent) c, type));
500 //////// Motor configurations ////////
504 * Return the default configuration. This should be used in the user interface
505 * to ensure a consistent rocket configuration between dialogs. It should NOT
506 * be used in simulations not relating to the UI.
508 * @return the default {@link Configuration}.
510 public Configuration getDefaultConfiguration() {
512 return defaultConfiguration;
517 * Return an array of the motor configuration IDs. This array is guaranteed
518 * to contain the <code>null</code> ID as the first element.
520 * @return an array of the motor configuration IDs.
522 public String[] getMotorConfigurationIDs() {
524 return motorConfigurationIDs.toArray(new String[0]);
528 * Add a new motor configuration ID to the motor configurations. The new ID
531 * @return the new motor configuration ID.
533 public String newMotorConfigurationID() {
535 String id = UUID.randomUUID().toString();
536 motorConfigurationIDs.add(id);
537 fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
542 * Add a specified motor configuration ID to the motor configurations.
544 * @param id the motor configuration ID.
545 * @return true if successful, false if the ID was already used.
547 public boolean addMotorConfigurationID(String id) {
549 if (id == null || motorConfigurationIDs.contains(id))
551 motorConfigurationIDs.add(id);
552 fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
557 * Remove a motor configuration ID from the configuration IDs. The <code>null</code>
558 * ID cannot be removed, and an attempt to remove it will be silently ignored.
560 * @param id the motor configuration ID to remove
562 public void removeMotorConfigurationID(String id) {
566 motorConfigurationIDs.remove(id);
567 fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
572 * Check whether <code>id</code> is a valid motor configuration ID.
574 * @param id the configuration ID.
575 * @return whether a motor configuration with that ID exists.
577 public boolean isMotorConfigurationID(String id) {
579 return motorConfigurationIDs.contains(id);
585 * Check whether the given motor configuration ID has motors defined for it.
587 * @param id the motor configuration ID (may be invalid).
588 * @return whether any motors are defined for it.
590 public boolean hasMotors(String id) {
595 Iterator<RocketComponent> iterator = this.iterator();
596 while (iterator.hasNext()) {
597 RocketComponent c = iterator.next();
599 if (c instanceof MotorMount) {
600 MotorMount mount = (MotorMount) c;
601 if (!mount.isMotorMount())
603 if (mount.getMotor(id) != null) {
613 * Return the user-set name of the motor configuration. If no name has been set,
614 * returns an empty string (not null).
616 * @param id the motor configuration id
617 * @return the configuration name
619 public String getMotorConfigurationName(String id) {
621 if (!isMotorConfigurationID(id))
623 String s = motorConfigurationNames.get(id);
631 * Set the name of the motor configuration. A name can be unset by passing
632 * <code>null</code> or an empty string.
634 * @param id the motor configuration id
635 * @param name the name for the motor configuration
637 public void setMotorConfigurationName(String id, String name) {
639 motorConfigurationNames.put(id, name);
640 fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
645 * Return either the motor configuration name (if set) or its description.
647 * @param id the motor configuration ID.
648 * @return a textual representation of the configuration
650 public String getMotorConfigurationNameOrDescription(String id) {
654 name = getMotorConfigurationName(id);
655 if (name != null && !name.equals(""))
658 return getMotorConfigurationDescription(id);
662 * Return a description for the motor configuration, generated from the motor
663 * designations of the components.
665 * @param id the motor configuration ID.
666 * @return a textual representation of the configuration
668 @SuppressWarnings("null")
669 public String getMotorConfigurationDescription(String id) {
674 // Generate the description
676 // First iterate over each stage and store the designations of each motor
677 List<List<String>> list = new ArrayList<List<String>>();
678 List<String> currentList = null;
680 Iterator<RocketComponent> iterator = this.iterator();
681 while (iterator.hasNext()) {
682 RocketComponent c = iterator.next();
684 if (c instanceof Stage) {
686 currentList = new ArrayList<String>();
687 list.add(currentList);
689 } else if (c instanceof MotorMount) {
691 MotorMount mount = (MotorMount) c;
692 Motor motor = mount.getMotor(id);
694 if (mount.isMotorMount() && motor != null) {
695 String designation = motor.getDesignation(mount.getMotorDelay(id));
697 for (int i = 0; i < mount.getMotorCount(); i++) {
698 currentList.add(designation);
706 if (motorCount == 0) {
708 return trans.get("Rocket.motorCount.Nomotor");
711 // Change multiple occurrences of a motor to n x motor
712 List<String> stages = new ArrayList<String>();
714 for (List<String> stage : list) {
715 String stageName = "";
716 String previous = null;
719 Collections.sort(stage);
720 for (String current : stage) {
721 if (current.equals(previous)) {
727 if (previous != null) {
730 s = "" + count + Chars.TIMES + previous;
735 if (stageName.equals(""))
738 stageName = stageName + "," + s;
746 if (previous != null) {
749 s = "" + count + Chars.TIMES + previous;
754 if (stageName.equals(""))
757 stageName = stageName + "," + s;
760 stages.add(stageName);
764 for (int i = 0; i < stages.size(); i++) {
765 String s = stages.get(i);
771 name = name + "; " + s;
779 //////// Obligatory component information
783 public String getComponentName() {
785 return trans.get("Rocket.compname.Rocket");
789 public Coordinate getComponentCG() {
790 return new Coordinate(0, 0, 0, 0);
794 public double getComponentMass() {
799 public double getLongitudinalUnitInertia() {
804 public double getRotationalUnitInertia() {
809 public Collection<Coordinate> getComponentBounds() {
810 return Collections.emptyList();
814 public boolean isAerodynamic() {
819 public boolean isMassive() {
824 public boolean allowsChildren() {
829 * Allows only <code>Stage</code> components to be added to the type Rocket.
832 public boolean isCompatible(Class<? extends RocketComponent> type) {
833 return (Stage.class.isAssignableFrom(type));
837 * Accept a visitor to this Rocket in the component hierarchy.
839 * @param theVisitor the visitor that will be called back with a reference to this Rocket
842 public void accept(final ComponentVisitor theVisitor) {
843 theVisitor.visit(this);