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