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