geodetic computations
[debian/openrocket] / src / net / sf / openrocket / simulation / SimulationOptions.java
1 package net.sf.openrocket.simulation;
2
3 import java.util.ArrayList;
4 import java.util.List;
5 import java.util.Random;
6
7 import javax.swing.event.ChangeEvent;
8 import javax.swing.event.ChangeListener;
9
10 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
11 import net.sf.openrocket.masscalc.BasicMassCalculator;
12 import net.sf.openrocket.models.atmosphere.AtmosphericModel;
13 import net.sf.openrocket.models.atmosphere.ExtendedISAModel;
14 import net.sf.openrocket.models.gravity.GravityModel;
15 import net.sf.openrocket.models.gravity.WGSGravityModel;
16 import net.sf.openrocket.models.wind.PinkNoiseWindModel;
17 import net.sf.openrocket.rocketcomponent.Rocket;
18 import net.sf.openrocket.util.BugException;
19 import net.sf.openrocket.util.ChangeSource;
20 import net.sf.openrocket.util.GeodeticComputationStrategy;
21 import net.sf.openrocket.util.MathUtil;
22 import net.sf.openrocket.util.Utils;
23 import net.sf.openrocket.util.WorldCoordinate;
24
25 /**
26  * A class holding simulation options in basic parameter form and which functions
27  * as a ChangeSource.  A SimulationConditions instance is generated from this class
28  * using {@link #toSimulationConditions()}.
29  * 
30  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
31  */
32 public class SimulationOptions implements ChangeSource, Cloneable {
33         
34         public static final double MAX_LAUNCH_ROD_ANGLE = Math.PI / 3;
35         
36         /**
37          * The ISA standard atmosphere.
38          */
39         private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
40         
41
42         private final Rocket rocket;
43         private String motorID = null;
44         
45
46         /*
47          * NOTE:  When adding/modifying parameters, they must also be added to the
48          * equals and copyFrom methods!!
49          */
50
51         // TODO: HIGH: Fetch default values from Prefs!
52         
53         private double launchRodLength = 1;
54         
55         /** Launch rod angle > 0, radians from vertical */
56         private double launchRodAngle = 0;
57         
58         /** Launch rod direction, 0 = upwind, PI = downwind. */
59         private double launchRodDirection = 0;
60         
61
62         private double windAverage = 2.0;
63         private double windTurbulence = 0.1;
64         
65         /*
66          * SimulationOptions maintains the launch site parameters as separate double values,
67          * and converts them into a WorldCoordinate when converting to SimulationConditions.
68          */
69         private double launchAltitude = 0;
70         private double launchLatitude = 45;
71         private double launchLongitude = 0;
72         private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL;
73         
74         private boolean useISA = true;
75         private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE;
76         private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE;
77         
78
79         private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP;
80         private double maximumAngle = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP;
81         
82         private int randomSeed = new Random().nextInt();
83         
84         private boolean calculateExtras = true;
85         
86
87         private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
88         
89         
90
91         public SimulationOptions(Rocket rocket) {
92                 this.rocket = rocket;
93         }
94         
95         
96         public Rocket getRocket() {
97                 return rocket;
98         }
99         
100         
101         public String getMotorConfigurationID() {
102                 return motorID;
103         }
104         
105         /**
106          * Set the motor configuration ID.  This must be a valid motor configuration ID of
107          * the rocket, otherwise the configuration is set to <code>null</code>.
108          * 
109          * @param id    the configuration to set.
110          */
111         public void setMotorConfigurationID(String id) {
112                 if (id != null)
113                         id = id.intern();
114                 if (!rocket.isMotorConfigurationID(id))
115                         id = null;
116                 if (id == motorID)
117                         return;
118                 motorID = id;
119                 fireChangeEvent();
120         }
121         
122         
123         public double getLaunchRodLength() {
124                 return launchRodLength;
125         }
126         
127         public void setLaunchRodLength(double launchRodLength) {
128                 if (MathUtil.equals(this.launchRodLength, launchRodLength))
129                         return;
130                 this.launchRodLength = launchRodLength;
131                 fireChangeEvent();
132         }
133         
134         
135         public double getLaunchRodAngle() {
136                 return launchRodAngle;
137         }
138         
139         public void setLaunchRodAngle(double launchRodAngle) {
140                 launchRodAngle = MathUtil.clamp(launchRodAngle, 0, MAX_LAUNCH_ROD_ANGLE);
141                 if (MathUtil.equals(this.launchRodAngle, launchRodAngle))
142                         return;
143                 this.launchRodAngle = launchRodAngle;
144                 fireChangeEvent();
145         }
146         
147         
148         public double getLaunchRodDirection() {
149                 return launchRodDirection;
150         }
151         
152         public void setLaunchRodDirection(double launchRodDirection) {
153                 launchRodDirection = MathUtil.reduce180(launchRodDirection);
154                 if (MathUtil.equals(this.launchRodDirection, launchRodDirection))
155                         return;
156                 this.launchRodDirection = launchRodDirection;
157                 fireChangeEvent();
158         }
159         
160         
161
162         public double getWindSpeedAverage() {
163                 return windAverage;
164         }
165         
166         public void setWindSpeedAverage(double windAverage) {
167                 if (MathUtil.equals(this.windAverage, windAverage))
168                         return;
169                 this.windAverage = MathUtil.max(windAverage, 0);
170                 fireChangeEvent();
171         }
172         
173         
174         public double getWindSpeedDeviation() {
175                 return windAverage * windTurbulence;
176         }
177         
178         public void setWindSpeedDeviation(double windDeviation) {
179                 if (windAverage < 0.1) {
180                         windAverage = 0.1;
181                 }
182                 setWindTurbulenceIntensity(windDeviation / windAverage);
183         }
184         
185         
186         /**
187          * Return the wind turbulence intensity (standard deviation / average).
188          * 
189          * @return  the turbulence intensity
190          */
191         public double getWindTurbulenceIntensity() {
192                 return windTurbulence;
193         }
194         
195         /**
196          * Set the wind standard deviation to match the given turbulence intensity.
197          * 
198          * @param intensity   the turbulence intensity
199          */
200         public void setWindTurbulenceIntensity(double intensity) {
201                 // Does not check equality so that setWindSpeedDeviation can be sure of event firing
202                 this.windTurbulence = intensity;
203                 fireChangeEvent();
204         }
205         
206         
207
208
209
210         public double getLaunchAltitude() {
211                 return launchAltitude;
212         }
213         
214         public void setLaunchAltitude(double altitude) {
215                 if (MathUtil.equals(this.launchAltitude, altitude))
216                         return;
217                 this.launchAltitude = altitude;
218                 fireChangeEvent();
219         }
220         
221         
222         public double getLaunchLatitude() {
223                 return launchLatitude;
224         }
225         
226         public void setLaunchLatitude(double launchLatitude) {
227                 launchLatitude = MathUtil.clamp(launchLatitude, -90, 90);
228                 if (MathUtil.equals(this.launchLatitude, launchLatitude))
229                         return;
230                 this.launchLatitude = launchLatitude;
231                 fireChangeEvent();
232         }
233         
234         public double getLaunchLongitude() {
235                 return launchLongitude;
236         }
237         
238         public void setLaunchLongitude(double launchLongitude) {
239                 launchLongitude = MathUtil.clamp(launchLongitude, -180, 180);
240                 if (MathUtil.equals(this.launchLongitude, launchLongitude))
241                         return;
242                 this.launchLongitude = launchLongitude;
243                 fireChangeEvent();
244         }
245         
246         
247         public GeodeticComputationStrategy getGeodeticComputation() {
248                 return geodeticComputation;
249         }
250         
251         public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) {
252                 if (this.geodeticComputation == geodeticComputation)
253                         return;
254                 if (geodeticComputation == null) {
255                         throw new IllegalArgumentException("strategy cannot be null");
256                 }
257                 this.geodeticComputation = geodeticComputation;
258                 fireChangeEvent();
259         }
260         
261         
262         public boolean isISAAtmosphere() {
263                 return useISA;
264         }
265         
266         public void setISAAtmosphere(boolean isa) {
267                 if (isa == useISA)
268                         return;
269                 useISA = isa;
270                 fireChangeEvent();
271         }
272         
273         
274         public double getLaunchTemperature() {
275                 return launchTemperature;
276         }
277         
278         
279
280         public void setLaunchTemperature(double launchTemperature) {
281                 if (MathUtil.equals(this.launchTemperature, launchTemperature))
282                         return;
283                 this.launchTemperature = launchTemperature;
284                 fireChangeEvent();
285         }
286         
287         
288
289         public double getLaunchPressure() {
290                 return launchPressure;
291         }
292         
293         
294
295         public void setLaunchPressure(double launchPressure) {
296                 if (MathUtil.equals(this.launchPressure, launchPressure))
297                         return;
298                 this.launchPressure = launchPressure;
299                 fireChangeEvent();
300         }
301         
302         
303         /**
304          * Returns an atmospheric model corresponding to the launch conditions.  The
305          * atmospheric models may be shared between different calls.
306          * 
307          * @return      an AtmosphericModel object.
308          */
309         private AtmosphericModel getAtmosphericModel() {
310                 if (useISA) {
311                         return ISA_ATMOSPHERIC_MODEL;
312                 }
313                 return new ExtendedISAModel(getLaunchAltitude(), launchTemperature, launchPressure);
314         }
315         
316         
317         public double getTimeStep() {
318                 return timeStep;
319         }
320         
321         public void setTimeStep(double timeStep) {
322                 if (MathUtil.equals(this.timeStep, timeStep))
323                         return;
324                 this.timeStep = timeStep;
325                 fireChangeEvent();
326         }
327         
328         public double getMaximumStepAngle() {
329                 return maximumAngle;
330         }
331         
332         public void setMaximumStepAngle(double maximumAngle) {
333                 maximumAngle = MathUtil.clamp(maximumAngle, 1 * Math.PI / 180, 20 * Math.PI / 180);
334                 if (MathUtil.equals(this.maximumAngle, maximumAngle))
335                         return;
336                 this.maximumAngle = maximumAngle;
337                 fireChangeEvent();
338         }
339         
340         
341
342         public boolean getCalculateExtras() {
343                 return calculateExtras;
344         }
345         
346         
347
348         public void setCalculateExtras(boolean calculateExtras) {
349                 if (this.calculateExtras == calculateExtras)
350                         return;
351                 this.calculateExtras = calculateExtras;
352                 fireChangeEvent();
353         }
354         
355         
356
357         public int getRandomSeed() {
358                 return randomSeed;
359         }
360         
361         public void setRandomSeed(int randomSeed) {
362                 if (this.randomSeed == randomSeed) {
363                         return;
364                 }
365                 this.randomSeed = randomSeed;
366                 /*
367                  * This does not fire an event since we don't want to invalidate simulation results
368                  * due to changing the seed value.  This needs to be revisited if the user is ever
369                  * allowed to select the seed value.
370                  */
371                 //              fireChangeEvent();
372         }
373         
374         /**
375          * Randomize the random seed value.
376          */
377         public void randomizeSeed() {
378                 this.randomSeed = new Random().nextInt();
379                 //              fireChangeEvent();
380         }
381         
382         
383
384         @Override
385         public SimulationOptions clone() {
386                 try {
387                         SimulationOptions copy = (SimulationOptions) super.clone();
388                         copy.listeners = new ArrayList<ChangeListener>();
389                         return copy;
390                 } catch (CloneNotSupportedException e) {
391                         throw new BugException(e);
392                 }
393         }
394         
395         
396         public void copyFrom(SimulationOptions src) {
397                 
398                 if (this.rocket == src.rocket) {
399                         
400                         this.motorID = src.motorID;
401                         
402                 } else {
403                         
404                         if (src.rocket.hasMotors(src.motorID)) {
405                                 // Try to find a matching motor ID
406                                 String motorDesc = src.rocket.getMotorConfigurationDescription(src.motorID);
407                                 String matchID = null;
408                                 
409                                 for (String id : this.rocket.getMotorConfigurationIDs()) {
410                                         if (motorDesc.equals(this.rocket.getMotorConfigurationDescription(id))) {
411                                                 matchID = id;
412                                                 break;
413                                         }
414                                 }
415                                 
416                                 this.motorID = matchID;
417                         } else {
418                                 this.motorID = null;
419                         }
420                 }
421                 
422                 this.launchAltitude = src.launchAltitude;
423                 this.launchLatitude = src.launchLatitude;
424                 this.launchLongitude = src.launchLongitude;
425                 this.launchPressure = src.launchPressure;
426                 this.launchRodAngle = src.launchRodAngle;
427                 this.launchRodDirection = src.launchRodDirection;
428                 this.launchRodLength = src.launchRodLength;
429                 this.launchTemperature = src.launchTemperature;
430                 this.maximumAngle = src.maximumAngle;
431                 this.timeStep = src.timeStep;
432                 this.windAverage = src.windAverage;
433                 this.windTurbulence = src.windTurbulence;
434                 this.calculateExtras = src.calculateExtras;
435                 this.randomSeed = src.randomSeed;
436                 
437                 fireChangeEvent();
438         }
439         
440         
441
442         /**
443          * Compares whether the two simulation conditions are equal.  The two are considered
444          * equal if the rocket, motor id and all variables are equal.
445          */
446         @Override
447         public boolean equals(Object other) {
448                 if (!(other instanceof SimulationOptions))
449                         return false;
450                 SimulationOptions o = (SimulationOptions) other;
451                 return ((this.rocket == o.rocket) &&
452                                 Utils.equals(this.motorID, o.motorID) &&
453                                 MathUtil.equals(this.launchAltitude, o.launchAltitude) &&
454                                 MathUtil.equals(this.launchLatitude, o.launchLatitude) &&
455                                 MathUtil.equals(this.launchLongitude, o.launchLongitude) &&
456                                 MathUtil.equals(this.launchPressure, o.launchPressure) &&
457                                 MathUtil.equals(this.launchRodAngle, o.launchRodAngle) &&
458                                 MathUtil.equals(this.launchRodDirection, o.launchRodDirection) &&
459                                 MathUtil.equals(this.launchRodLength, o.launchRodLength) &&
460                                 MathUtil.equals(this.launchTemperature, o.launchTemperature) &&
461                                 MathUtil.equals(this.maximumAngle, o.maximumAngle) &&
462                                 MathUtil.equals(this.timeStep, o.timeStep) &&
463                                 MathUtil.equals(this.windAverage, o.windAverage) &&
464                                 MathUtil.equals(this.windTurbulence, o.windTurbulence) &&
465                                 this.calculateExtras == o.calculateExtras && this.randomSeed == o.randomSeed);
466         }
467         
468         /**
469          * Hashcode method compatible with {@link #equals(Object)}.
470          */
471         @Override
472         public int hashCode() {
473                 if (motorID == null)
474                         return rocket.hashCode();
475                 return rocket.hashCode() + motorID.hashCode();
476         }
477         
478         @Override
479         public void addChangeListener(ChangeListener listener) {
480                 listeners.add(listener);
481         }
482         
483         @Override
484         public void removeChangeListener(ChangeListener listener) {
485                 listeners.remove(listener);
486         }
487         
488         private final ChangeEvent event = new ChangeEvent(this);
489         
490         private void fireChangeEvent() {
491                 ChangeListener[] array = listeners.toArray(new ChangeListener[0]);
492                 
493                 for (int i = array.length - 1; i >= 0; i--) {
494                         array[i].stateChanged(event);
495                 }
496         }
497         
498         
499         // TODO: HIGH: Clean up
500         public SimulationConditions toSimulationConditions() {
501                 SimulationConditions conditions = new SimulationConditions();
502                 
503                 conditions.setRocket((Rocket) getRocket().copy());
504                 conditions.setMotorConfigurationID(getMotorConfigurationID());
505                 conditions.setLaunchRodLength(getLaunchRodLength());
506                 conditions.setLaunchRodAngle(getLaunchRodAngle());
507                 conditions.setLaunchRodDirection(getLaunchRodDirection());
508                 conditions.setLaunchSite(new WorldCoordinate(getLaunchLatitude(), getLaunchLongitude(), getLaunchAltitude()));
509                 conditions.setGeodeticComputation(getGeodeticComputation());
510                 conditions.setRandomSeed(randomSeed);
511                 
512                 PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed);
513                 windModel.setAverage(getWindSpeedAverage());
514                 windModel.setStandardDeviation(getWindSpeedDeviation());
515                 conditions.setWindModel(windModel);
516                 
517                 conditions.setAtmosphericModel(getAtmosphericModel());
518                 
519                 //BasicGravityModel gravityModel = new BasicGravityModel(getLaunchLatitude());
520                 GravityModel gravityModel = new WGSGravityModel();
521                 
522                 conditions.setGravityModel(gravityModel);
523                 
524                 conditions.setAerodynamicCalculator(new BarrowmanCalculator());
525                 conditions.setMassCalculator(new BasicMassCalculator());
526                 
527                 conditions.setTimeStep(getTimeStep());
528                 conditions.setMaximumAngleStep(getMaximumStepAngle());
529                 
530                 conditions.setCalculateExtras(getCalculateExtras());
531                 
532                 return conditions;
533         }
534         
535 }