Updates for 0.9.5
[debian/openrocket] / src / net / sf / openrocket / rocketcomponent / Configuration.java
1 package net.sf.openrocket.rocketcomponent;
2
3 import java.util.ArrayList;
4 import java.util.BitSet;
5 import java.util.Collection;
6 import java.util.Collections;
7 import java.util.HashMap;
8 import java.util.Iterator;
9 import java.util.List;
10 import java.util.NoSuchElementException;
11
12 import javax.swing.event.ChangeEvent;
13 import javax.swing.event.ChangeListener;
14 import javax.swing.event.EventListenerList;
15
16 import net.sf.openrocket.util.BugException;
17 import net.sf.openrocket.util.ChangeSource;
18 import net.sf.openrocket.util.Coordinate;
19 import net.sf.openrocket.util.MathUtil;
20
21
22 public class Configuration implements Cloneable, ChangeSource, ComponentChangeListener, 
23                 Iterable<RocketComponent> {
24         
25         public static final double DEFAULT_IGNITION_TIME = Double.MAX_VALUE;
26         
27
28         private Rocket rocket;
29         private BitSet stages = new BitSet();
30         
31         private String motorConfiguration = null;
32         
33         private EventListenerList listenerList = new EventListenerList();
34         
35         private final HashMap<MotorMount, Double> ignitionTimes =
36                 new HashMap<MotorMount, Double>();
37         
38
39         /* Cached data */
40         private int boundsModID = -1;
41         private ArrayList<Coordinate> cachedBounds = new ArrayList<Coordinate>();
42         private double cachedLength = -1;
43
44         private int refLengthModID = -1;
45         private double cachedRefLength = -1;
46         
47         
48         
49         /**
50          * Create a new configuration with the specified <code>Rocket</code> with 
51          * <code>null</code> motor configuration.
52          * 
53          * @param rocket  the rocket
54          */
55         public Configuration(Rocket rocket) {
56                 this.rocket = rocket;
57                 setAllStages();
58                 rocket.addComponentChangeListener(this);
59         }
60
61         
62         /**
63          * Create a new configuration with the specified <code>Rocket</code> and motor
64          * configuration.
65          * 
66          * @param rocket        the rocket.
67          * @param motorID       the motor configuration ID to use.
68          */
69         public Configuration(Rocket rocket, String motorID) {
70                 this.rocket = rocket;
71                 this.motorConfiguration = motorID;
72                 setAllStages();
73                 rocket.addComponentChangeListener(this);
74         }
75         
76         
77         
78         
79         public Rocket getRocket() {
80                 return rocket;
81         }
82         
83         
84         public void setAllStages() {
85                 stages.clear();
86                 stages.set(0,rocket.getStageCount());
87                 fireChangeEvent();
88         }
89         
90         
91         /**
92          * Set all stages up to and including the given stage number.  For example,
93          * <code>setToStage(0)</code> will set only the first stage active.
94          * 
95          * @param stage         the stage number.
96          */
97         public void setToStage(int stage) {
98                 stages.clear();
99                 stages.set(0, stage+1, true);
100 //              stages.set(stage+1, rocket.getStageCount(), false);
101                 fireChangeEvent();
102         }
103         
104         
105         /**
106          * Check whether the up-most stage of the rocket is in this configuration.
107          * 
108          * @return      <code>true</code> if the first stage is active in this configuration.
109          */
110         public boolean isHead() {
111                 return isStageActive(0);
112         }
113         
114         public boolean isStageActive(RocketComponent stage) {
115                 if (!(stage instanceof Stage)) {
116                         throw new IllegalArgumentException("called with component "+stage);
117                 }
118                 return stages.get(stage.getParent().getChildPosition(stage));
119         }
120         
121         
122         public boolean isStageActive(int stage) {
123                 if (stage >= rocket.getStageCount())
124                         return false;
125                 return stages.get(stage);
126         }
127         
128         public int getStageCount() {
129                 return rocket.getStageCount();
130         }
131
132         public int getActiveStageCount() {
133                 int count = 0;
134                 int s = rocket.getStageCount();
135                 
136                 for (int i=0; i < s; i++) {
137                         if (stages.get(i))
138                                 count++;
139                 }
140                 return count;
141         }
142
143         public int[] getActiveStages() {
144                 int stageCount = rocket.getStageCount();
145                 List<Integer> active = new ArrayList<Integer>();
146                 int[] ret;
147
148                 for (int i=0; i < stageCount; i++) {
149                         if (stages.get(i)) {
150                                 active.add(i);
151                         }
152                 }
153                 
154                 ret = new int[active.size()];
155                 for (int i=0; i < ret.length; i++) {
156                         ret[i] = active.get(i);
157                 }
158                 
159                 return ret;
160         }
161         
162         
163         /**
164          * Return the reference length associated with the current configuration.  The 
165          * reference length type is retrieved from the <code>Rocket</code>.
166          * 
167          * @return  the reference length for this configuration.
168          */
169         public double getReferenceLength() {
170                 if (rocket.getModID() != refLengthModID) {
171                         refLengthModID = rocket.getModID();
172                         cachedRefLength = rocket.getReferenceType().getReferenceLength(this);
173                 }
174                 return cachedRefLength;
175         }
176         
177         
178         public double getReferenceArea() {
179                 return Math.PI * MathUtil.pow2(getReferenceLength()/2);
180         }
181         
182         
183         public String getMotorConfigurationID() {
184                 return motorConfiguration;
185         }
186         
187         public void setMotorConfigurationID(String id) {
188                 if ((motorConfiguration == null  &&  id == null) ||
189                                 (id != null && id.equals(motorConfiguration)))
190                         return;
191                 
192                 motorConfiguration = id;
193                 fireChangeEvent();
194         }
195         
196         public String getMotorConfigurationDescription() {
197                 return rocket.getMotorConfigurationNameOrDescription(motorConfiguration);
198         }
199         
200         
201         
202         /**
203          * Clear all motor ignition times.  All values are reset to their default of
204          * {@link #DEFAULT_IGNITION_TIME}.
205          */
206         public void resetIgnitionTimes() {
207                 ignitionTimes.clear();
208         }
209         
210         /**
211          * Set the ignition time of the motor in the specified motor mount.  Negative or NaN
212          * time values will cause an <code>IllegalArgumentException</code>.
213          * 
214          * @param mount the motor mount to specify.
215          * @param time  the time at which to ignite the motors.
216          * @throws IllegalArgumentException   if <code>time</code> is negative of NaN.
217          */
218         public void setIgnitionTime(MotorMount mount, double time) {
219                 if (time < 0) {
220                         throw new IllegalArgumentException("time is negative: "+time);
221                 }
222                 ignitionTimes.put(mount, time);
223                 // TODO: MEDIUM: Should this fire events?
224         }
225         
226         /**
227          * Return the ignition time of the motor in the specified motor mount.  If no time
228          * has been specified, returns {@link #DEFAULT_IGNITION_TIME} as the default.
229          * 
230          * @param mount   the motor mount.
231          * @return                ignition time of the motors in the mount.
232          */
233         public double getIgnitionTime(MotorMount mount) {
234                 Double d = ignitionTimes.get(mount);
235                 if (d == null)
236                         return DEFAULT_IGNITION_TIME;
237                 return d;
238         }
239         
240         
241
242         
243         /**
244          * Removes the listener connection to the rocket and listeners of this object.
245          * This configuration may not be used after a call to this method!
246          */
247         public void release() {
248                 rocket.removeComponentChangeListener(this);
249                 listenerList = null;
250                 rocket = null;
251         }
252         
253         
254         ////////////////  Listeners  ////////////////
255         
256         @Override
257         public void addChangeListener(ChangeListener listener) {
258                 listenerList.add(ChangeListener.class, listener);
259         }
260
261         @Override
262         public void removeChangeListener(ChangeListener listener) {
263                 listenerList.remove(ChangeListener.class, listener);
264         }
265         
266         protected void fireChangeEvent() {
267                 Object[] listeners = listenerList.getListenerList();
268                 ChangeEvent e = new ChangeEvent(this);
269                 
270                 boundsModID = -1;
271                 refLengthModID = -1;
272                 
273                 for (int i = listeners.length-2; i>=0; i-=2) {
274                         if (listeners[i] == ChangeListener.class) {
275                                 ((ChangeListener) listeners[i+1]).stateChanged(e);
276                         }
277                 }
278         }
279
280         
281         @Override
282         public void componentChanged(ComponentChangeEvent e) {
283                 fireChangeEvent();
284         }
285         
286         
287         ///////////////  Helper methods  ///////////////
288         
289         /**
290          * Return whether this configuration has any motors defined to it.
291          * 
292          * @return  true if this configuration has active motor mounts with motors defined to them.
293          */
294         public boolean hasMotors() {
295                 for (RocketComponent c: this) {
296                         if (c instanceof MotorMount) {
297                                 MotorMount mount = (MotorMount) c;
298                                 if (!mount.isMotorMount())
299                                         continue;
300                                 if (mount.getMotor(this.motorConfiguration) != null) {
301                                         return true;
302                                 }
303                         }
304                 }
305                 return false;
306         }
307         
308         
309         /**
310          * Return the bounds of the current configuration.  The bounds are cached.
311          * 
312          * @return      a <code>Collection</code> containing coordinates bouding the rocket.
313          */
314         @SuppressWarnings("unchecked")
315         public Collection<Coordinate> getBounds() {
316                 if (rocket.getModID() != boundsModID) {
317                         boundsModID = rocket.getModID();
318                         cachedBounds.clear();
319                         
320                         double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
321                         for (RocketComponent component: this) {
322                                 for (Coordinate c: component.getComponentBounds()) {
323                                         for (Coordinate coord: component.toAbsolute(c)) {
324                                                 cachedBounds.add(coord);
325                                                 if (coord.x < minX)
326                                                         minX = coord.x;
327                                                 if (coord.x > maxX)
328                                                         maxX = coord.x;
329                                         }
330                                 }
331                         }
332                         
333                         if (Double.isInfinite(minX) || Double.isInfinite(maxX)) {
334                                 cachedLength = 0;
335                         } else {
336                                 cachedLength = maxX - minX;
337                         }
338                 }
339                 return (ArrayList<Coordinate>) cachedBounds.clone();
340         }
341         
342         
343         /**
344          * Returns the length of the rocket configuration, from the foremost bound X-coordinate
345          * to the aft-most X-coordinate.  The value is cached.
346          * 
347          * @return      the length of the rocket in the X-direction.
348          */
349         public double getLength() {
350                 if (rocket.getModID() != boundsModID)
351                         getBounds();  // Calculates the length
352                 
353                 return cachedLength;
354         }
355         
356         
357         
358
359         /**
360          * Return an iterator that iterates over the currently active components.
361          * The <code>Rocket</code> and <code>Stage</code> components are not returned,
362          * but instead all components that are within currently active stages.
363          */
364         @Override
365         public Iterator<RocketComponent> iterator() {
366                 return new ConfigurationIterator();
367         }
368         
369         
370         /**
371          * Return an iterator that iterates over all <code>MotorMount</code>s within the
372          * current configuration that have an active motor.
373          * 
374          * @return  an iterator over active motor mounts.
375          */
376         public Iterator<MotorMount> motorIterator() {
377                 return new MotorIterator();
378         }
379         
380         
381         /**
382          * Perform a deep-clone.  The object references are also cloned and no
383          * listeners are listening on the cloned object.  
384          */
385         @Override
386         public Configuration clone() {
387                 try {
388                         Configuration config = (Configuration) super.clone();
389                         config.listenerList = new EventListenerList();
390                         config.stages = (BitSet) this.stages.clone();
391                         config.cachedBounds = new ArrayList<Coordinate>();
392                         config.boundsModID = -1;
393                         config.refLengthModID = -1;
394                         rocket.addComponentChangeListener(config);
395                         return config;
396                 } catch (CloneNotSupportedException e) {
397                         throw new BugException("BUG: clone not supported!",e);
398                 }
399         }
400
401         
402
403         /**
404          * A class that iterates over all currently active components.
405          * 
406          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
407          */
408         private class ConfigurationIterator implements Iterator<RocketComponent> {
409                 Iterator<Iterator<RocketComponent>> iterators;
410                 Iterator<RocketComponent> current = null;
411                 
412                 public ConfigurationIterator() {
413                         List<Iterator<RocketComponent>> list = new ArrayList<Iterator<RocketComponent>>();
414                         
415                         for (RocketComponent stage: rocket.getChildren()) {
416                                 if (isStageActive(stage)) {
417                                         list.add(stage.deepIterator());
418                                 }
419                         }
420                         
421                         // Get iterators and initialize current
422                         iterators = list.iterator();
423                         if (iterators.hasNext()) {
424                                 current = iterators.next();
425                         } else {
426                                 List<RocketComponent> l = Collections.emptyList();
427                                 current = l.iterator();
428                         }
429                 }
430                 
431                 
432                 @Override
433                 public boolean hasNext() {
434                         if (!current.hasNext())
435                                 getNextIterator();
436                         
437                         return current.hasNext();
438                 }
439
440                 @Override
441                 public RocketComponent next() {
442                         if (!current.hasNext())
443                                 getNextIterator();
444                         
445                         return current.next();
446                 }
447
448                 /**
449                  * Get the next iterator that has items.  If such an iterator does
450                  * not exist, current is left to an empty iterator.
451                  */
452                 private void getNextIterator() {
453                         while ((!current.hasNext()) && iterators.hasNext()) {
454                                 current = iterators.next();
455                         }
456                 }
457                 
458                 @Override
459                 public void remove() {
460                         throw new UnsupportedOperationException("remove unsupported");
461                 }
462         }
463         
464         private class MotorIterator implements Iterator<MotorMount> {
465                 private final Iterator<RocketComponent> iterator;
466                 private MotorMount next = null;
467                 
468                 public MotorIterator() {
469                         this.iterator = iterator();
470                 }
471
472                 @Override
473                 public boolean hasNext() {
474                         getNext();
475                         return (next != null);
476                 }
477
478                 @Override
479                 public MotorMount next() {
480                         getNext();
481                         if (next == null) {
482                                 throw new NoSuchElementException("iterator called for too long");
483                         }
484                         
485                         MotorMount ret = next;
486                         next = null;
487                         return ret;
488                 }
489
490                 @Override
491                 public void remove() {
492                         throw new UnsupportedOperationException("remove unsupported");
493                 }
494                 
495                 private void getNext() {
496                         if (next != null)
497                                 return;
498                         while (iterator.hasNext()) {
499                                 RocketComponent c = iterator.next();
500                                 if (c instanceof MotorMount) {
501                                         MotorMount mount = (MotorMount) c;
502                                         if (mount.isMotorMount() && mount.getMotor(motorConfiguration) != null) {
503                                                 next = mount;
504                                                 return;
505                                         }
506                                 }
507                         }
508                 }
509         }
510 }