1 package net.sf.openrocket.aerodynamics;
3 import java.util.ArrayList;
4 import java.util.EventListener;
5 import java.util.EventObject;
8 import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
9 import net.sf.openrocket.rocketcomponent.Configuration;
10 import net.sf.openrocket.util.BugException;
11 import net.sf.openrocket.util.ChangeSource;
12 import net.sf.openrocket.util.Coordinate;
13 import net.sf.openrocket.util.MathUtil;
14 import net.sf.openrocket.util.Monitorable;
15 import net.sf.openrocket.util.StateChangeListener;
16 import net.sf.openrocket.util.UniqueID;
19 * A class defining the momentary flight conditions of a rocket, including
20 * the angle of attack, lateral wind angle, atmospheric conditions etc.
22 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
24 public class FlightConditions implements Cloneable, ChangeSource, Monitorable {
26 private List<EventListener> listenerList = new ArrayList<EventListener>();
27 private EventObject event = new EventObject(this);
30 /** Reference length used in calculations. */
31 private double refLength = 1.0;
33 /** Reference area used in calculations. */
34 private double refArea = Math.PI * 0.25;
37 /** Angle of attack. */
38 private double aoa = 0;
40 /** Sine of the angle of attack. */
41 private double sinAOA = 0;
44 * The fraction <code>sin(aoa) / aoa</code>. At an AOA of zero this value
45 * must be one. This value may be used in many cases to avoid checking for
48 private double sincAOA = 1.0;
50 /** Lateral wind direction. */
51 private double theta = 0;
53 /** Current Mach speed. */
54 private double mach = 0.3;
57 * Sqrt(1 - M^2) for M<1
58 * Sqrt(M^2 - 1) for M>1
60 private double beta = MathUtil.safeSqrt(1 - mach * mach);
63 /** Current roll rate. */
64 private double rollRate = 0;
66 private double pitchRate = 0;
67 private double yawRate = 0;
69 private Coordinate pitchCenter = Coordinate.NUL;
72 private AtmosphericConditions atmosphericConditions = new AtmosphericConditions();
76 private int modIDadd = 0;
80 * Sole constructor. The reference length is initialized to the reference length
81 * of the <code>Configuration</code>, and the reference area accordingly.
82 * If <code>config</code> is <code>null</code>, then the reference length is set
85 * @param config the configuration of which the reference length is taken.
87 public FlightConditions(Configuration config) {
89 setRefLength(config.getReferenceLength());
90 this.modID = UniqueID.next();
95 * Set the reference length from the given configuration.
96 * @param config the configuration from which to get the reference length.
98 public void setReference(Configuration config) {
99 setRefLength(config.getReferenceLength());
104 * Set the reference length and area.
106 public void setRefLength(double length) {
109 refArea = Math.PI * MathUtil.pow2(length / 2);
114 * Return the reference length.
116 public double getRefLength() {
121 * Set the reference area and length.
123 public void setRefArea(double area) {
125 refLength = MathUtil.safeSqrt(area / Math.PI) * 2;
130 * Return the reference area.
132 public double getRefArea() {
138 * Sets the angle of attack. It calculates values also for the methods
139 * {@link #getSinAOA()} and {@link #getSincAOA()}.
141 * @param aoa the angle of attack.
143 public void setAOA(double aoa) {
144 aoa = MathUtil.clamp(aoa, 0, Math.PI);
145 if (MathUtil.equals(this.aoa, aoa))
153 this.sinAOA = Math.sin(aoa);
154 this.sincAOA = sinAOA / aoa;
161 * Sets the angle of attack with the sine. The value <code>sinAOA</code> is assumed
162 * to be the sine of <code>aoa</code> for cases in which this value is known.
163 * The AOA must still be specified, as the sine is not unique in the range
166 * @param aoa the angle of attack in radians.
167 * @param sinAOA the sine of the angle of attack.
169 public void setAOA(double aoa, double sinAOA) {
170 aoa = MathUtil.clamp(aoa, 0, Math.PI);
171 sinAOA = MathUtil.clamp(sinAOA, 0, 1);
172 if (MathUtil.equals(this.aoa, aoa))
175 assert (Math.abs(Math.sin(aoa) - sinAOA) < 0.0001) : "Illegal sine: aoa=" + aoa + " sinAOA=" + sinAOA;
178 this.sinAOA = sinAOA;
182 this.sincAOA = sinAOA / aoa;
189 * Return the angle of attack.
191 public double getAOA() {
196 * Return the sine of the angle of attack.
198 public double getSinAOA() {
203 * Return the sinc of the angle of attack (sin(AOA) / AOA). This method returns
204 * one if the angle of attack is zero.
206 public double getSincAOA() {
212 * Set the direction of the lateral airflow.
214 public void setTheta(double theta) {
215 if (MathUtil.equals(this.theta, theta))
222 * Return the direction of the lateral airflow.
224 public double getTheta() {
230 * Set the current Mach speed. This should be (but is not required to be) in
231 * reference to the speed of sound of the atmospheric conditions.
233 public void setMach(double mach) {
234 mach = Math.max(mach, 0);
235 if (MathUtil.equals(this.mach, mach))
240 this.beta = MathUtil.safeSqrt(1 - mach * mach);
242 this.beta = MathUtil.safeSqrt(mach * mach - 1);
247 * Return the current Mach speed.
249 public double getMach() {
254 * Returns the current rocket velocity, calculated from the Mach number and the
255 * speed of sound. If either of these parameters are changed, the velocity changes
258 * @return the velocity of the rocket.
260 public double getVelocity() {
261 return mach * atmosphericConditions.getMachSpeed();
265 * Sets the Mach speed according to the given velocity and the current speed of sound.
267 * @param velocity the current velocity.
269 public void setVelocity(double velocity) {
270 setMach(velocity / atmosphericConditions.getMachSpeed());
275 * Return sqrt(abs(1 - Mach^2)). This is calculated in the setting call and is
278 public double getBeta() {
284 * Return the current roll rate.
286 public double getRollRate() {
292 * Set the current roll rate.
294 public void setRollRate(double rate) {
295 if (MathUtil.equals(this.rollRate, rate))
298 this.rollRate = rate;
303 public double getPitchRate() {
308 public void setPitchRate(double pitchRate) {
309 if (MathUtil.equals(this.pitchRate, pitchRate))
311 this.pitchRate = pitchRate;
316 public double getYawRate() {
321 public void setYawRate(double yawRate) {
322 if (MathUtil.equals(this.yawRate, yawRate))
324 this.yawRate = yawRate;
332 * @return the pitchCenter
334 public Coordinate getPitchCenter() {
340 * @param pitchCenter the pitchCenter to set
342 public void setPitchCenter(Coordinate pitchCenter) {
343 if (this.pitchCenter.equals(pitchCenter))
345 this.pitchCenter = pitchCenter;
351 * Return the current atmospheric conditions. Note that this method returns a
352 * reference to the {@link AtmosphericConditions} object used by this object.
353 * Changes made to the object will modify the encapsulated object, but will NOT
354 * generate change events.
356 * @return the current atmospheric conditions.
358 public AtmosphericConditions getAtmosphericConditions() {
359 return atmosphericConditions;
363 * Set the current atmospheric conditions. This method will fire a change event
364 * if a change occurs.
366 public void setAtmosphericConditions(AtmosphericConditions cond) {
367 if (atmosphericConditions.equals(cond))
369 modIDadd += atmosphericConditions.getModID();
370 atmosphericConditions = cond;
376 * Retrieve the modification count of this object. Each time it is modified
377 * the modification count is increased by one.
379 * @return the number of times this object has been modified since instantiation.
382 public int getModID() {
383 return modID + modIDadd + this.atmosphericConditions.getModID();
388 public String toString() {
389 return String.format("FlightConditions[" +
391 "theta=%.2f\u00b0," +
397 "pitchCenter=" + pitchCenter.toString() + "," +
398 "atmosphericConditions=" + atmosphericConditions.toString() +
400 aoa * 180 / Math.PI, theta * 180 / Math.PI, mach, rollRate, pitchRate, yawRate, refLength);
405 * Return a copy of the flight conditions. The copy has no listeners. The
406 * atmospheric conditions is also cloned.
409 public FlightConditions clone() {
411 FlightConditions cond = (FlightConditions) super.clone();
412 cond.listenerList = new ArrayList<EventListener>();
413 cond.event = new EventObject(cond);
414 cond.atmosphericConditions = atmosphericConditions.clone();
416 } catch (CloneNotSupportedException e) {
417 throw new BugException("clone not supported!", e);
423 public boolean equals(Object obj) {
426 if (!(obj instanceof FlightConditions))
429 FlightConditions other = (FlightConditions) obj;
431 return (MathUtil.equals(this.refLength, other.refLength) &&
432 MathUtil.equals(this.aoa, other.aoa) &&
433 MathUtil.equals(this.theta, other.theta) &&
434 MathUtil.equals(this.mach, other.mach) &&
435 MathUtil.equals(this.rollRate, other.rollRate) &&
436 MathUtil.equals(this.pitchRate, other.pitchRate) &&
437 MathUtil.equals(this.yawRate, other.yawRate) &&
438 this.pitchCenter.equals(other.pitchCenter) && this.atmosphericConditions.equals(other.atmosphericConditions));
442 public int hashCode() {
443 return (int) (1000 * (refLength + aoa + theta + mach + rollRate + pitchRate + yawRate));
448 public void addChangeListener(EventListener listener) {
449 listenerList.add(0, listener);
453 public void removeChangeListener(EventListener listener) {
454 listenerList.remove(listener);
457 protected void fireChangeEvent() {
458 modID = UniqueID.next();
459 // Copy the list before iterating to prevent concurrent modification exceptions.
460 EventListener[] listeners = listenerList.toArray(new EventListener[0]);
461 for (EventListener l : listeners) {
462 if ( l instanceof StateChangeListener ) {
463 ((StateChangeListener)l).stateChanged(event);