package net.sf.openrocket.rocketcomponent;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.EventObject;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import net.sf.openrocket.util.ArrayList;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.ChangeSource;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.Monitorable;
import net.sf.openrocket.util.StateChangeListener;
/**
* A class defining a rocket configuration, including motors and which stages are active.
*
* TODO: HIGH: Remove motor ignition times from this class.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
public class Configuration implements Cloneable, ChangeSource, ComponentChangeListener,
Iterable<RocketComponent>, Monitorable {
private Rocket rocket;
private BitSet stages = new BitSet();
private String flightConfigurationId = null;
private List<EventListener> listenerList = new ArrayList<EventListener>();
/* Cached data */
private int boundsModID = -1;
private ArrayList<Coordinate> cachedBounds = new ArrayList<Coordinate>();
private double cachedLength = -1;
private int refLengthModID = -1;
private double cachedRefLength = -1;
private int modID = 0;
/**
* Create a new configuration with the specified <code>Rocket</code> with
* <code>null</code> motor configuration.
*
* @param rocket the rocket
*/
public Configuration(Rocket rocket) {
this.rocket = rocket;
setAllStages();
rocket.addComponentChangeListener(this);
}
public Rocket getRocket() {
return rocket;
}
public void setAllStages() {
stages.clear();
stages.set(0, rocket.getStageCount());
fireChangeEvent();
}
/**
* Set all stages up to and including the given stage number. For example,
* <code>setToStage(0)</code> will set only the first stage active.
*
* @param stage the stage number.
*/
public void setToStage(int stage) {
stages.clear();
stages.set(0, stage + 1, true);
// stages.set(stage+1, rocket.getStageCount(), false);
fireChangeEvent();
}
public void setOnlyStage(int stage) {
stages.clear();
stages.set(stage, stage + 1, true);
fireChangeEvent();
}
/**
* Check whether the up-most stage of the rocket is in this configuration.
*
* @return <code>true</code> if the first stage is active in this configuration.
*/
public boolean isHead() {
return isStageActive(0);
}
/**
* Check whether the stage specified by the index is active.
*/
public boolean isStageActive(int stage) {
if (stage >= rocket.getStageCount())
return false;
return stages.get(stage);
}
public int getStageCount() {
return rocket.getStageCount();
}
public int getActiveStageCount() {
int count = 0;
int s = rocket.getStageCount();
for (int i = 0; i < s; i++) {
if (stages.get(i))
count++;
}
return count;
}
public int[] getActiveStages() {
int stageCount = rocket.getStageCount();
List<Integer> active = new ArrayList<Integer>();
int[] ret;
for (int i = 0; i < stageCount; i++) {
if (stages.get(i)) {
active.add(i);
}
}
ret = new int[active.size()];
for (int i = 0; i < ret.length; i++) {
ret[i] = active.get(i);
}
return ret;
}
/**
* Return the reference length associated with the current configuration. The
* reference length type is retrieved from the <code>Rocket</code>.
*
* @return the reference length for this configuration.
*/
public double getReferenceLength() {
if (rocket.getModID() != refLengthModID) {
refLengthModID = rocket.getModID();
cachedRefLength = rocket.getReferenceType().getReferenceLength(this);
}
return cachedRefLength;
}
public double getReferenceArea() {
return Math.PI * MathUtil.pow2(getReferenceLength() / 2);
}
public String getFlightConfigurationID() {
return flightConfigurationId;
}
public void setFlightConfigurationID(String id) {
if ((flightConfigurationId == null && id == null) ||
(id != null && id.equals(flightConfigurationId)))
return;
flightConfigurationId = id;
fireChangeEvent();
}
/**
* Removes the listener connection to the rocket and listeners of this object.
* This configuration may not be used after a call to this method!
*/
public void release() {
rocket.removeComponentChangeListener(this);
listenerList = new ArrayList<EventListener>();
rocket = null;
}
//////////////// Listeners ////////////////
@Override
public void addChangeListener(StateChangeListener listener) {
listenerList.add(listener);
}
@Override
public void removeChangeListener(StateChangeListener listener) {
listenerList.remove(listener);
}
protected void fireChangeEvent() {
EventObject e = new EventObject(this);
this.modID++;
boundsModID = -1;
refLengthModID = -1;
// Copy the list before iterating to prevent concurrent modification exceptions.
EventListener[] listeners = listenerList.toArray(new EventListener[0]);
for (EventListener l : listeners) {
if (l instanceof StateChangeListener) {
((StateChangeListener) l).stateChanged(e);
}
}
}
@Override
public void componentChanged(ComponentChangeEvent e) {
fireChangeEvent();
}
/////////////// Helper methods ///////////////
/**
* Return whether this configuration has any motors defined to it.
*
* @return true if this configuration has active motor mounts with motors defined to them.
*/
public boolean hasMotors() {
for (RocketComponent c : this) {
if (c instanceof MotorMount) {
MotorMount mount = (MotorMount) c;
if (!mount.isMotorMount())
continue;
if (mount.getMotor(this.flightConfigurationId) != null) {
return true;
}
}
}
return false;
}
/**
* Return whether a component is in the currently active stages.
*/
public boolean isComponentActive(final RocketComponent c) {
int stage = c.getStageNumber();
return isStageActive(stage);
}
/**
* Return the bounds of the current configuration. The bounds are cached.
*
* @return a <code>Collection</code> containing coordinates bouding the rocket.
*/
public Collection<Coordinate> getBounds() {
if (rocket.getModID() != boundsModID) {
boundsModID = rocket.getModID();
cachedBounds.clear();
double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
for (RocketComponent component : this) {
for (Coordinate c : component.getComponentBounds()) {
for (Coordinate coord : component.toAbsolute(c)) {
cachedBounds.add(coord);
if (coord.x < minX)
minX = coord.x;
if (coord.x > maxX)
maxX = coord.x;
}
}
}
if (Double.isInfinite(minX) || Double.isInfinite(maxX)) {
cachedLength = 0;
} else {
cachedLength = maxX - minX;
}
}
return cachedBounds.clone();
}
/**
* Returns the length of the rocket configuration, from the foremost bound X-coordinate
* to the aft-most X-coordinate. The value is cached.
*
* @return the length of the rocket in the X-direction.
*/
public double getLength() {
if (rocket.getModID() != boundsModID)
getBounds(); // Calculates the length
return cachedLength;
}
/**
* Return an iterator that iterates over the currently active components.
* The <code>Rocket</code> and <code>Stage</code> components are not returned,
* but instead all components that are within currently active stages.
*/
@Override
public Iterator<RocketComponent> iterator() {
return new ConfigurationIterator();
}
/**
* Return an iterator that iterates over all <code>MotorMount</code>s within the
* current configuration that have an active motor.
*
* @return an iterator over active motor mounts.
*/
public Iterator<MotorMount> motorIterator() {
return new MotorIterator();
}
/**
* Perform a deep-clone. The object references are also cloned and no
* listeners are listening on the cloned object. The rocket instance remains the same.
*/
@Override
public Configuration clone() {
try {
Configuration config = (Configuration) super.clone();
config.listenerList = new ArrayList<EventListener>();
config.stages = (BitSet) this.stages.clone();
config.cachedBounds = new ArrayList<Coordinate>();
config.boundsModID = -1;
config.refLengthModID = -1;
rocket.addComponentChangeListener(config);
return config;
} catch (CloneNotSupportedException e) {
throw new BugException("clone not supported!", e);
}
}
@Override
public int getModID() {
return modID + rocket.getModID();
}
/**
* A class that iterates over all currently active components.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
private class ConfigurationIterator implements Iterator<RocketComponent> {
Iterator<Iterator<RocketComponent>> iterators;
Iterator<RocketComponent> current = null;
public ConfigurationIterator() {
List<Iterator<RocketComponent>> list = new ArrayList<Iterator<RocketComponent>>();
for (RocketComponent stage : rocket.getChildren()) {
if (isComponentActive(stage)) {
list.add(stage.iterator(false));
}
}
// Get iterators and initialize current
iterators = list.iterator();
if (iterators.hasNext()) {
current = iterators.next();
} else {
List<RocketComponent> l = Collections.emptyList();
current = l.iterator();
}
}
@Override
public boolean hasNext() {
if (!current.hasNext())
getNextIterator();
return current.hasNext();
}
@Override
public RocketComponent next() {
if (!current.hasNext())
getNextIterator();
return current.next();
}
/**
* Get the next iterator that has items. If such an iterator does
* not exist, current is left to an empty iterator.
*/
private void getNextIterator() {
while ((!current.hasNext()) && iterators.hasNext()) {
current = iterators.next();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove unsupported");
}
}
private class MotorIterator implements Iterator<MotorMount> {
private final Iterator<RocketComponent> iterator;
private MotorMount next = null;
public MotorIterator() {
this.iterator = iterator();
}
@Override
public boolean hasNext() {
getNext();
return (next != null);
}
@Override
public MotorMount next() {
getNext();
if (next == null) {
throw new NoSuchElementException("iterator called for too long");
}
MotorMount ret = next;
next = null;
return ret;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove unsupported");
}
private void getNext() {
if (next != null)
return;
while (iterator.hasNext()) {
RocketComponent c = iterator.next();
if (c instanceof MotorMount) {
MotorMount mount = (MotorMount) c;
if (mount.isMotorMount() && mount.getMotor(flightConfigurationId) != null) {
next = mount;
return;
}
}
}
}
}
}