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