package net.sf.openrocket.simulation; import java.util.ArrayList; import java.util.EventListener; import java.util.EventObject; import java.util.List; import java.util.Random; import net.sf.openrocket.aerodynamics.BarrowmanCalculator; import net.sf.openrocket.formatting.MotorDescriptionSubstitutor; import net.sf.openrocket.masscalc.BasicMassCalculator; import net.sf.openrocket.models.atmosphere.AtmosphericModel; import net.sf.openrocket.models.atmosphere.ExtendedISAModel; import net.sf.openrocket.models.gravity.GravityModel; import net.sf.openrocket.models.gravity.WGSGravityModel; import net.sf.openrocket.models.wind.PinkNoiseWindModel; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.ChangeSource; import net.sf.openrocket.util.GeodeticComputationStrategy; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.StateChangeListener; import net.sf.openrocket.util.Utils; import net.sf.openrocket.util.WorldCoordinate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A class holding simulation options in basic parameter form and which functions * as a ChangeSource. A SimulationConditions instance is generated from this class * using {@link #toSimulationConditions()}. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public class SimulationOptions implements ChangeSource, Cloneable { private static final Logger log = LoggerFactory.getLogger(SimulationOptions.class); public static final double MAX_LAUNCH_ROD_ANGLE = Math.PI / 3; /** * The ISA standard atmosphere. */ private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel(); protected final Preferences preferences = Application.getPreferences(); private final Rocket rocket; private String motorID = null; /* * NOTE: When adding/modifying parameters, they must also be added to the * equals and copyFrom methods!! */ private double launchRodLength = preferences.getDouble(Preferences.LAUNCH_ROD_LENGTH, 1); private boolean launchIntoWind = preferences.getBoolean(Preferences.LAUNCH_INTO_WIND, true); private double launchRodAngle = preferences.getDouble(Preferences.LAUNCH_ROD_ANGLE, 0); private double windDirection = preferences.getDouble(Preferences.WIND_DIRECTION, Math.PI / 2); private double launchRodDirection = preferences.getDouble(Preferences.LAUNCH_ROD_DIRECTION, Math.PI / 2); private double windAverage = preferences.getDouble(Preferences.WIND_AVERAGE, 2.0); private double windTurbulence = preferences.getDouble(Preferences.WIND_TURBULANCE, 0.1); /* * SimulationOptions maintains the launch site parameters as separate double values, * and converts them into a WorldCoordinate when converting to SimulationConditions. */ private double launchAltitude = preferences.getDouble(Preferences.LAUNCH_ALTITUDE, 0); private double launchLatitude = preferences.getDouble(Preferences.LAUNCH_LATITUDE, 28.61); private double launchLongitude = preferences.getDouble(Preferences.LAUNCH_LONGITUDE, -80.60); private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL; private boolean useISA = preferences.getBoolean(Preferences.LAUNCH_USE_ISA, true); private double launchTemperature = preferences.getDouble(Preferences.LAUNCH_TEMPERATURE, ExtendedISAModel.STANDARD_TEMPERATURE); private double launchPressure = preferences.getDouble(Preferences.LAUNCH_PRESSURE, ExtendedISAModel.STANDARD_PRESSURE); private double timeStep = preferences.getDouble(Preferences.SIMULATION_TIME_STEP, RK4SimulationStepper.RECOMMENDED_TIME_STEP); private double maximumAngle = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP; private int randomSeed = new Random().nextInt(); private boolean calculateExtras = true; private List<EventListener> listeners = new ArrayList<EventListener>(); public SimulationOptions(Rocket rocket) { this.rocket = rocket; } public Rocket getRocket() { return rocket; } public String getMotorConfigurationID() { return motorID; } /** * Set the motor configuration ID. This must be a valid motor configuration ID of * the rocket, otherwise the configuration is set to <code>null</code>. * * @param id the configuration to set. */ public void setMotorConfigurationID(String id) { if (id != null) id = id.intern(); if (!rocket.isFlightConfigurationID(id)) id = null; if (id == motorID) return; motorID = id; fireChangeEvent(); } public double getLaunchRodLength() { return launchRodLength; } public void setLaunchRodLength(double launchRodLength) { if (MathUtil.equals(this.launchRodLength, launchRodLength)) return; this.launchRodLength = launchRodLength; fireChangeEvent(); } public boolean getLaunchIntoWind() { return launchIntoWind; } public void setLaunchIntoWind(boolean i) { launchIntoWind = i; } public double getLaunchRodAngle() { return launchRodAngle; } public void setLaunchRodAngle(double launchRodAngle) { launchRodAngle = MathUtil.clamp(launchRodAngle, -MAX_LAUNCH_ROD_ANGLE, MAX_LAUNCH_ROD_ANGLE); if (MathUtil.equals(this.launchRodAngle, launchRodAngle)) return; this.launchRodAngle = launchRodAngle; fireChangeEvent(); } public double getLaunchRodDirection() { if (launchIntoWind) { this.setLaunchRodDirection(windDirection); } return launchRodDirection; } public void setLaunchRodDirection(double launchRodDirection) { launchRodDirection = MathUtil.reduce360(launchRodDirection); if (MathUtil.equals(this.launchRodDirection, launchRodDirection)) return; this.launchRodDirection = launchRodDirection; fireChangeEvent(); } public double getWindSpeedAverage() { return windAverage; } public void setWindSpeedAverage(double windAverage) { if (MathUtil.equals(this.windAverage, windAverage)) return; this.windAverage = MathUtil.max(windAverage, 0); fireChangeEvent(); } public double getWindSpeedDeviation() { return windAverage * windTurbulence; } public void setWindSpeedDeviation(double windDeviation) { if (windAverage < 0.1) { windAverage = 0.1; } setWindTurbulenceIntensity(windDeviation / windAverage); } /** * Return the wind turbulence intensity (standard deviation / average). * * @return the turbulence intensity */ public double getWindTurbulenceIntensity() { return windTurbulence; } /** * Set the wind standard deviation to match the given turbulence intensity. * * @param intensity the turbulence intensity */ public void setWindTurbulenceIntensity(double intensity) { // Does not check equality so that setWindSpeedDeviation can be sure of event firing this.windTurbulence = intensity; fireChangeEvent(); } /** * Set the wind direction * * @param direction the wind direction */ public void setWindDirection(double direction) { direction = MathUtil.reduce360(direction); if (launchIntoWind) { this.setLaunchRodDirection(direction); } if (MathUtil.equals(this.windDirection, direction)) return; this.windDirection = direction; fireChangeEvent(); } public double getWindDirection() { return this.windDirection; } public double getLaunchAltitude() { return launchAltitude; } public void setLaunchAltitude(double altitude) { if (MathUtil.equals(this.launchAltitude, altitude)) return; this.launchAltitude = altitude; fireChangeEvent(); } public double getLaunchLatitude() { return launchLatitude; } public void setLaunchLatitude(double launchLatitude) { launchLatitude = MathUtil.clamp(launchLatitude, -90, 90); if (MathUtil.equals(this.launchLatitude, launchLatitude)) return; this.launchLatitude = launchLatitude; fireChangeEvent(); } public double getLaunchLongitude() { return launchLongitude; } public void setLaunchLongitude(double launchLongitude) { launchLongitude = MathUtil.clamp(launchLongitude, -180, 180); if (MathUtil.equals(this.launchLongitude, launchLongitude)) return; this.launchLongitude = launchLongitude; fireChangeEvent(); } public GeodeticComputationStrategy getGeodeticComputation() { return geodeticComputation; } public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) { if (this.geodeticComputation == geodeticComputation) return; if (geodeticComputation == null) { throw new IllegalArgumentException("strategy cannot be null"); } this.geodeticComputation = geodeticComputation; fireChangeEvent(); } public boolean isISAAtmosphere() { return useISA; } public void setISAAtmosphere(boolean isa) { if (isa == useISA) return; useISA = isa; fireChangeEvent(); } public double getLaunchTemperature() { return launchTemperature; } public void setLaunchTemperature(double launchTemperature) { if (MathUtil.equals(this.launchTemperature, launchTemperature)) return; this.launchTemperature = launchTemperature; fireChangeEvent(); } public double getLaunchPressure() { return launchPressure; } public void setLaunchPressure(double launchPressure) { if (MathUtil.equals(this.launchPressure, launchPressure)) return; this.launchPressure = launchPressure; fireChangeEvent(); } /** * Returns an atmospheric model corresponding to the launch conditions. The * atmospheric models may be shared between different calls. * * @return an AtmosphericModel object. */ private AtmosphericModel getAtmosphericModel() { if (useISA) { return ISA_ATMOSPHERIC_MODEL; } return new ExtendedISAModel(getLaunchAltitude(), launchTemperature, launchPressure); } public double getTimeStep() { return timeStep; } public void setTimeStep(double timeStep) { if (MathUtil.equals(this.timeStep, timeStep)) return; this.timeStep = timeStep; fireChangeEvent(); } public double getMaximumStepAngle() { return maximumAngle; } public void setMaximumStepAngle(double maximumAngle) { maximumAngle = MathUtil.clamp(maximumAngle, 1 * Math.PI / 180, 20 * Math.PI / 180); if (MathUtil.equals(this.maximumAngle, maximumAngle)) return; this.maximumAngle = maximumAngle; fireChangeEvent(); } public boolean getCalculateExtras() { return calculateExtras; } public void setCalculateExtras(boolean calculateExtras) { if (this.calculateExtras == calculateExtras) return; this.calculateExtras = calculateExtras; fireChangeEvent(); } public int getRandomSeed() { return randomSeed; } public void setRandomSeed(int randomSeed) { if (this.randomSeed == randomSeed) { return; } this.randomSeed = randomSeed; /* * This does not fire an event since we don't want to invalidate simulation results * due to changing the seed value. This needs to be revisited if the user is ever * allowed to select the seed value. */ // fireChangeEvent(); } /** * Randomize the random seed value. */ public void randomizeSeed() { this.randomSeed = new Random().nextInt(); // fireChangeEvent(); } @Override public SimulationOptions clone() { try { SimulationOptions copy = (SimulationOptions) super.clone(); copy.listeners = new ArrayList<EventListener>(); return copy; } catch (CloneNotSupportedException e) { throw new BugException(e); } } public void copyFrom(SimulationOptions src) { if (this.rocket == src.rocket) { this.motorID = src.motorID; } else { if (src.rocket.hasMotors(src.motorID)) { // First check for exact match: if (this.rocket.isFlightConfigurationID(src.motorID)) { this.motorID = src.motorID; } else { // Try to find a closely matching motor ID MotorDescriptionSubstitutor formatter = Application.getInjector().getInstance(MotorDescriptionSubstitutor.class); String motorDesc = formatter.getMotorConfigurationDescription(src.rocket, src.motorID); String matchID = null; for (String id : this.rocket.getFlightConfigurationIDs()) { String motorDesc2 = formatter.getMotorConfigurationDescription(this.rocket, id); if (motorDesc.equals(motorDesc2)) { matchID = id; break; } } this.motorID = matchID; } } else { this.motorID = null; } } this.launchAltitude = src.launchAltitude; this.launchLatitude = src.launchLatitude; this.launchLongitude = src.launchLongitude; this.launchPressure = src.launchPressure; this.launchRodAngle = src.launchRodAngle; this.launchRodDirection = src.launchRodDirection; this.launchRodLength = src.launchRodLength; this.launchTemperature = src.launchTemperature; this.maximumAngle = src.maximumAngle; this.timeStep = src.timeStep; this.windAverage = src.windAverage; this.windTurbulence = src.windTurbulence; this.windDirection = src.windDirection; this.calculateExtras = src.calculateExtras; this.randomSeed = src.randomSeed; fireChangeEvent(); } public void copyConditionsFrom(SimulationOptions src) { // Be a little smart about triggering the change event. // only do it if one of the "important" (user specified) parameters has really changed. boolean isChanged = false; if (this.launchAltitude != src.launchAltitude) { isChanged = true; this.launchAltitude = src.launchAltitude; } if (this.launchLatitude != src.launchLatitude) { isChanged = true; this.launchLatitude = src.launchLatitude; } if (this.launchLongitude != src.launchLongitude) { isChanged = true; this.launchLongitude = src.launchLongitude; } if (this.launchPressure != src.launchPressure) { isChanged = true; this.launchPressure = src.launchPressure; } if (this.launchRodAngle != src.launchRodAngle) { isChanged = true; this.launchRodAngle = src.launchRodAngle; } if (this.launchRodDirection != src.launchRodDirection) { isChanged = true; this.launchRodDirection = src.launchRodDirection; } if (this.launchRodLength != src.launchRodLength) { isChanged = true; this.launchRodLength = src.launchRodLength; } if (this.launchTemperature != src.launchTemperature) { isChanged = true; this.launchTemperature = src.launchTemperature; } if (this.maximumAngle != src.maximumAngle) { isChanged = true; this.maximumAngle = src.maximumAngle; } this.maximumAngle = src.maximumAngle; if (this.timeStep != src.timeStep) { isChanged = true; this.timeStep = src.timeStep; } if (this.windAverage != src.windAverage) { isChanged = true; this.windAverage = src.windAverage; } if (this.windDirection != src.windDirection) { isChanged = true; this.windDirection = src.windDirection; } if (this.windTurbulence != src.windTurbulence) { isChanged = true; this.windTurbulence = src.windTurbulence; } if (this.calculateExtras != src.calculateExtras) { isChanged = true; this.calculateExtras = src.calculateExtras; } if (isChanged) { // Only copy the randomSeed if something else has changed. // Honestly, I don't really see a need for that. this.randomSeed = src.randomSeed; fireChangeEvent(); } } /** * Compares whether the two simulation conditions are equal. The two are considered * equal if the rocket, motor id and all variables are equal. */ @Override public boolean equals(Object other) { if (!(other instanceof SimulationOptions)) return false; SimulationOptions o = (SimulationOptions) other; return ((this.rocket == o.rocket) && Utils.equals(this.motorID, o.motorID) && MathUtil.equals(this.launchAltitude, o.launchAltitude) && MathUtil.equals(this.launchLatitude, o.launchLatitude) && MathUtil.equals(this.launchLongitude, o.launchLongitude) && MathUtil.equals(this.launchPressure, o.launchPressure) && MathUtil.equals(this.launchRodAngle, o.launchRodAngle) && MathUtil.equals(this.launchRodDirection, o.launchRodDirection) && MathUtil.equals(this.launchRodLength, o.launchRodLength) && MathUtil.equals(this.launchTemperature, o.launchTemperature) && MathUtil.equals(this.maximumAngle, o.maximumAngle) && MathUtil.equals(this.timeStep, o.timeStep) && MathUtil.equals(this.windAverage, o.windAverage) && MathUtil.equals(this.windTurbulence, o.windTurbulence) && MathUtil.equals(this.windDirection, o.windDirection) && this.calculateExtras == o.calculateExtras && this.randomSeed == o.randomSeed); } /** * Hashcode method compatible with {@link #equals(Object)}. */ @Override public int hashCode() { if (motorID == null) return rocket.hashCode(); return rocket.hashCode() + motorID.hashCode(); } @Override public void addChangeListener(StateChangeListener listener) { listeners.add(listener); } @Override public void removeChangeListener(StateChangeListener listener) { listeners.remove(listener); } private final EventObject event = new EventObject(this); private void fireChangeEvent() { // Copy the list before iterating to prevent concurrent modification exceptions. EventListener[] list = listeners.toArray(new EventListener[0]); for (EventListener l : list) { if (l instanceof StateChangeListener) { ((StateChangeListener) l).stateChanged(event); } } } // TODO: HIGH: Clean up public SimulationConditions toSimulationConditions() { SimulationConditions conditions = new SimulationConditions(); conditions.setRocket((Rocket) getRocket().copy()); conditions.setMotorConfigurationID(getMotorConfigurationID()); conditions.setLaunchRodLength(getLaunchRodLength()); conditions.setLaunchRodAngle(getLaunchRodAngle()); conditions.setLaunchRodDirection(getLaunchRodDirection()); conditions.setLaunchSite(new WorldCoordinate(getLaunchLatitude(), getLaunchLongitude(), getLaunchAltitude())); conditions.setGeodeticComputation(getGeodeticComputation()); conditions.setRandomSeed(randomSeed); PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed); windModel.setAverage(getWindSpeedAverage()); windModel.setStandardDeviation(getWindSpeedDeviation()); windModel.setDirection(windDirection); conditions.setWindModel(windModel); conditions.setAtmosphericModel(getAtmosphericModel()); GravityModel gravityModel = new WGSGravityModel(); conditions.setGravityModel(gravityModel); conditions.setAerodynamicCalculator(new BarrowmanCalculator()); conditions.setMassCalculator(new BasicMassCalculator()); conditions.setTimeStep(getTimeStep()); conditions.setMaximumAngleStep(getMaximumStepAngle()); conditions.setCalculateExtras(getCalculateExtras()); return conditions; } }