package net.sf.openrocket.aerodynamics; import java.util.ArrayList; import java.util.EventListener; import java.util.EventObject; import java.util.List; import net.sf.openrocket.models.atmosphere.AtmosphericConditions; import net.sf.openrocket.rocketcomponent.Configuration; 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; import net.sf.openrocket.util.UniqueID; /** * A class defining the momentary flight conditions of a rocket, including * the angle of attack, lateral wind angle, atmospheric conditions etc. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public class FlightConditions implements Cloneable, ChangeSource, Monitorable { private List<EventListener> listenerList = new ArrayList<EventListener>(); private EventObject event = new EventObject(this); /** Reference length used in calculations. */ private double refLength = 1.0; /** Reference area used in calculations. */ private double refArea = Math.PI * 0.25; /** Angle of attack. */ private double aoa = 0; /** Sine of the angle of attack. */ private double sinAOA = 0; /** * The fraction <code>sin(aoa) / aoa</code>. At an AOA of zero this value * must be one. This value may be used in many cases to avoid checking for * division by zero. */ private double sincAOA = 1.0; /** Lateral wind direction. */ private double theta = 0; /** Current Mach speed. */ private double mach = 0.3; /** * Sqrt(1 - M^2) for M<1 * Sqrt(M^2 - 1) for M>1 */ private double beta = MathUtil.safeSqrt(1 - mach * mach); /** Current roll rate. */ private double rollRate = 0; /** Current pitch rate. */ private double pitchRate = 0; /** Current yaw rate. */ private double yawRate = 0; private Coordinate pitchCenter = Coordinate.NUL; private AtmosphericConditions atmosphericConditions = new AtmosphericConditions(); private int modID; private int modIDadd = 0; /** * Sole constructor. The reference length is initialized to the reference length * of the <code>Configuration</code>, and the reference area accordingly. * If <code>config</code> is <code>null</code>, then the reference length is set * to 1 meter. * * @param config the configuration of which the reference length is taken. */ public FlightConditions(Configuration config) { if (config != null) setRefLength(config.getReferenceLength()); this.modID = UniqueID.next(); } /** * Set the reference length from the given configuration. * @param config the configuration from which to get the reference length. */ public void setReference(Configuration config) { setRefLength(config.getReferenceLength()); } /** * Set the reference length and area. * fires change event */ public void setRefLength(double length) { refLength = length; refArea = Math.PI * MathUtil.pow2(length / 2); fireChangeEvent(); } /** * @return the reference length. */ public double getRefLength() { return refLength; } /** * Set the reference area and length. * fires change event */ public void setRefArea(double area) { refArea = area; refLength = MathUtil.safeSqrt(area / Math.PI) * 2; fireChangeEvent(); } /** * @return the reference area. */ public double getRefArea() { return refArea; } /** * Sets the angle of attack. It calculates values also for the methods * {@link #getSinAOA()} and {@link #getSincAOA()}. * fires change event if it's different from previous value * @param aoa the angle of attack. */ public void setAOA(double aoa) { aoa = MathUtil.clamp(aoa, 0, Math.PI); if (MathUtil.equals(this.aoa, aoa)) return; this.aoa = aoa; if (aoa < 0.001) { this.sinAOA = aoa; this.sincAOA = 1.0; } else { this.sinAOA = Math.sin(aoa); this.sincAOA = sinAOA / aoa; } fireChangeEvent(); } /** * Sets the angle of attack with the sine. The value <code>sinAOA</code> is assumed * to be the sine of <code>aoa</code> for cases in which this value is known. * The AOA must still be specified, as the sine is not unique in the range * of 0..180 degrees. * fires change event if it's different from previous value * * @param aoa the angle of attack in radians. * @param sinAOA the sine of the angle of attack. */ public void setAOA(double aoa, double sinAOA) { aoa = MathUtil.clamp(aoa, 0, Math.PI); sinAOA = MathUtil.clamp(sinAOA, 0, 1); if (MathUtil.equals(this.aoa, aoa)) return; assert (Math.abs(Math.sin(aoa) - sinAOA) < 0.0001) : "Illegal sine: aoa=" + aoa + " sinAOA=" + sinAOA; this.aoa = aoa; this.sinAOA = sinAOA; if (aoa < 0.001) { this.sincAOA = 1.0; } else { this.sincAOA = sinAOA / aoa; } fireChangeEvent(); } /** * @return the angle of attack. */ public double getAOA() { return aoa; } /** * @return the sine of the angle of attack. */ public double getSinAOA() { return sinAOA; } /** * @return the sinc of the angle of attack (sin(AOA) / AOA). This method returns * one if the angle of attack is zero. */ public double getSincAOA() { return sincAOA; } /** * Set the direction of the lateral airflow. * fires change event if it's different from previous value * */ public void setTheta(double theta) { if (MathUtil.equals(this.theta, theta)) return; this.theta = theta; fireChangeEvent(); } /** * @return the direction of the lateral airflow. */ public double getTheta() { return theta; } /** * Set the current Mach speed. This should be (but is not required to be) in * reference to the speed of sound of the atmospheric conditions. * * fires change event if it's different from previous value */ public void setMach(double mach) { mach = Math.max(mach, 0); if (MathUtil.equals(this.mach, mach)) return; this.mach = mach; if (mach < 1) this.beta = MathUtil.safeSqrt(1 - mach * mach); else this.beta = MathUtil.safeSqrt(mach * mach - 1); fireChangeEvent(); } /** * @return the current Mach speed. */ public double getMach() { return mach; } /** * Returns the current rocket velocity, calculated from the Mach number and the * speed of sound. If either of these parameters are changed, the velocity changes * as well. * * @return the velocity of the rocket. */ public double getVelocity() { return mach * atmosphericConditions.getMachSpeed(); } /** * Sets the Mach speed according to the given velocity and the current speed of sound. * * @param velocity the current velocity. */ public void setVelocity(double velocity) { setMach(velocity / atmosphericConditions.getMachSpeed()); } /** * @return sqrt(abs(1 - Mach^2)). This is calculated in the setting call and is * therefore fast. */ public double getBeta() { return beta; } /** * @return the current roll rate. */ public double getRollRate() { return rollRate; } /** * Set the current roll rate. * fires change event if it's different from previous */ public void setRollRate(double rate) { if (MathUtil.equals(this.rollRate, rate)) return; this.rollRate = rate; fireChangeEvent(); } /** * * @return current pitch rate */ public double getPitchRate() { return pitchRate; } /** * sets the pitch rate * fires change event if it's different from previous * @param pitchRate */ public void setPitchRate(double pitchRate) { if (MathUtil.equals(this.pitchRate, pitchRate)) return; this.pitchRate = pitchRate; fireChangeEvent(); } /** * * @return current yaw rate */ public double getYawRate() { return yawRate; } public void setYawRate(double yawRate) { if (MathUtil.equals(this.yawRate, yawRate)) return; this.yawRate = yawRate; fireChangeEvent(); } /** * @return the pitchCenter */ public Coordinate getPitchCenter() { return pitchCenter; } /** * @param pitchCenter the pitchCenter to set */ public void setPitchCenter(Coordinate pitchCenter) { if (this.pitchCenter.equals(pitchCenter)) return; this.pitchCenter = pitchCenter; fireChangeEvent(); } /** * Return the current atmospheric conditions. Note that this method returns a * reference to the {@link AtmosphericConditions} object used by this object. * Changes made to the object will modify the encapsulated object, but will NOT * generate change events. * * @return the current atmospheric conditions. */ public AtmosphericConditions getAtmosphericConditions() { return atmosphericConditions; } /** * Set the current atmospheric conditions. This method will fire a change event * if a change occurs. */ public void setAtmosphericConditions(AtmosphericConditions cond) { if (atmosphericConditions.equals(cond)) return; modIDadd += atmosphericConditions.getModID(); atmosphericConditions = cond; fireChangeEvent(); } /** * Retrieve the modification count of this object. Each time it is modified * the modification count is increased by one. * * @return the number of times this object has been modified since instantiation. */ @Override public int getModID() { return modID + modIDadd + this.atmosphericConditions.getModID(); } @Override public String toString() { return String.format("FlightConditions[" + "aoa=%.2f\u00b0," + "theta=%.2f\u00b0," + "mach=%.3f," + "rollRate=%.2f," + "pitchRate=%.2f," + "yawRate=%.2f," + "refLength=%.3f," + "pitchCenter=" + pitchCenter.toString() + "," + "atmosphericConditions=" + atmosphericConditions.toString() + "]", aoa * 180 / Math.PI, theta * 180 / Math.PI, mach, rollRate, pitchRate, yawRate, refLength); } /** * @return a copy of the flight conditions. The copy has no listeners. The * atmospheric conditions is also cloned. */ @Override public FlightConditions clone() { try { FlightConditions cond = (FlightConditions) super.clone(); cond.listenerList = new ArrayList<EventListener>(); cond.event = new EventObject(cond); cond.atmosphericConditions = atmosphericConditions.clone(); return cond; } catch (CloneNotSupportedException e) { throw new BugException("clone not supported!", e); } } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof FlightConditions)) return false; FlightConditions other = (FlightConditions) obj; return (MathUtil.equals(this.refLength, other.refLength) && MathUtil.equals(this.aoa, other.aoa) && MathUtil.equals(this.theta, other.theta) && MathUtil.equals(this.mach, other.mach) && MathUtil.equals(this.rollRate, other.rollRate) && MathUtil.equals(this.pitchRate, other.pitchRate) && MathUtil.equals(this.yawRate, other.yawRate) && this.pitchCenter.equals(other.pitchCenter) && this.atmosphericConditions.equals(other.atmosphericConditions)); } @Override public int hashCode() { return (int) (1000 * (refLength + aoa + theta + mach + rollRate + pitchRate + yawRate)); } @Override public void addChangeListener(StateChangeListener listener) { listenerList.add(0, listener); } @Override public void removeChangeListener(StateChangeListener listener) { listenerList.remove(listener); } /** * wake up call to listeners */ protected void fireChangeEvent() { modID = UniqueID.next(); // 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(event); } } } }