package net.sf.openrocket.document; import java.util.EventListener; import java.util.EventObject; import java.util.Iterator; import java.util.List; import net.sf.openrocket.aerodynamics.AerodynamicCalculator; import net.sf.openrocket.aerodynamics.BarrowmanCalculator; import net.sf.openrocket.aerodynamics.WarningSet; import net.sf.openrocket.formatting.RocketDescriptor; import net.sf.openrocket.masscalc.BasicMassCalculator; import net.sf.openrocket.masscalc.MassCalculator; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.MotorInstanceConfiguration; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.IgnitionConfiguration; import net.sf.openrocket.rocketcomponent.MotorConfiguration; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.simulation.BasicEventSimulationEngine; import net.sf.openrocket.simulation.DefaultSimulationOptionFactory; import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.RK4SimulationStepper; import net.sf.openrocket.simulation.SimulationConditions; import net.sf.openrocket.simulation.SimulationEngine; import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.SimulationStepper; import net.sf.openrocket.simulation.exception.SimulationException; import net.sf.openrocket.simulation.extension.SimulationExtension; import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.ArrayList; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.ChangeSource; import net.sf.openrocket.util.SafetyMutex; import net.sf.openrocket.util.StateChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A class defining a simulation, its conditions and simulated data. * <p> * This class is not thread-safe and enforces single-threaded access with a * SafetyMutex. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public class Simulation implements ChangeSource, Cloneable { private static final Logger log = LoggerFactory.getLogger(Simulation.class); public static enum Status { /** Up-to-date */ UPTODATE, /** Loaded from file, status probably up-to-date */ LOADED, /** Data outdated */ OUTDATED, /** Imported external data */ EXTERNAL, /** Not yet simulated */ NOT_SIMULATED, /** Can't be simulated, NO_MOTORS **/ CANT_RUN } private RocketDescriptor descriptor = Application.getInjector().getInstance(RocketDescriptor.class); private SafetyMutex mutex = SafetyMutex.newInstance(); private final Rocket rocket; private String name = ""; private Status status = Status.NOT_SIMULATED; /** The conditions to use */ // TODO: HIGH: Change to use actual conditions class?? private SimulationOptions options; private ArrayList<SimulationExtension> simulationExtensions = new ArrayList<SimulationExtension>(); private final Class<? extends SimulationEngine> simulationEngineClass = BasicEventSimulationEngine.class; private Class<? extends SimulationStepper> simulationStepperClass = RK4SimulationStepper.class; private Class<? extends AerodynamicCalculator> aerodynamicCalculatorClass = BarrowmanCalculator.class; @SuppressWarnings("unused") private Class<? extends MassCalculator> massCalculatorClass = BasicMassCalculator.class; /** Listeners for this object */ private List<EventListener> listeners = new ArrayList<EventListener>(); /** The conditions actually used in the previous simulation, or null */ private SimulationOptions simulatedConditions = null; private String simulatedConfiguration = null; private FlightData simulatedData = null; private int simulatedRocketID = -1; /** * Create a new simulation for the rocket. Parent document should also be provided. * The initial motor configuration is taken from the default rocket configuration. * * @param rocket the rocket associated with the simulation. */ public Simulation(Rocket rocket) { this.rocket = rocket; this.status = Status.NOT_SIMULATED; options = new SimulationOptions(rocket); DefaultSimulationOptionFactory f = Application.getInjector().getInstance(DefaultSimulationOptionFactory.class); options.copyConditionsFrom(f.getDefault()); options.setMotorConfigurationID(rocket.getDefaultConfiguration().getFlightConfigurationID()); options.addChangeListener(new ConditionListener()); } public Simulation(Rocket rocket, Status status, String name, SimulationOptions options, List<SimulationExtension> extensions, FlightData data) { if (rocket == null) throw new IllegalArgumentException("rocket cannot be null"); if (status == null) throw new IllegalArgumentException("status cannot be null"); if (name == null) throw new IllegalArgumentException("name cannot be null"); if (options == null) throw new IllegalArgumentException("options cannot be null"); this.rocket = rocket; if (status == Status.UPTODATE) { this.status = Status.LOADED; } else if (data == null) { this.status = Status.NOT_SIMULATED; } else { this.status = status; } this.name = name; this.options = options; options.addChangeListener(new ConditionListener()); if (extensions != null) { this.simulationExtensions.addAll(extensions); } if (data != null && this.status != Status.NOT_SIMULATED) { simulatedData = data; if (this.status == Status.LOADED) { simulatedConditions = options.clone(); simulatedRocketID = rocket.getModID(); } } } /** * Return the rocket associated with this simulation. * * @return the rocket. */ public Rocket getRocket() { mutex.verify(); return rocket; } /** * Return a newly created Configuration for this simulation. The configuration * has the motor ID set and all stages active. * * @return a newly created Configuration of the launch conditions. */ public Configuration getConfiguration() { mutex.verify(); Configuration c = new Configuration(rocket); c.setFlightConfigurationID(options.getMotorConfigurationID()); c.setAllStages(); return c; } /** * Returns the simulation options attached to this simulation. The options * may be modified freely, and the status of the simulation will change to reflect * the changes. * * @return the simulation conditions. */ public SimulationOptions getOptions() { mutex.verify(); return options; } /** * Get the list of simulation extensions. The returned list is the one used by * this object; changes to it will reflect changes in the simulation. * * @return the actual list of simulation extensions. */ public List<SimulationExtension> getSimulationExtensions() { mutex.verify(); return simulationExtensions; } /** * Return the user-defined name of the simulation. * * @return the name for the simulation. */ public String getName() { mutex.verify(); return name; } /** * Set the user-defined name of the simulation. Setting the name to * null yields an empty name. * * @param name the name of the simulation. */ public void setName(String name) { mutex.lock("setName"); try { if (this.name.equals(name)) return; if (name == null) this.name = ""; else this.name = name; fireChangeEvent(); } finally { mutex.unlock("setName"); } } /** * Returns the status of this simulation. This method examines whether the * simulation has been outdated and returns {@link Status#OUTDATED} accordingly. * * @return the status * @see Status */ public Status getStatus() { mutex.verify(); if (status == Status.UPTODATE || status == Status.LOADED) { if (rocket.getFunctionalModID() != simulatedRocketID || !options.equals(simulatedConditions)) { status = Status.OUTDATED; } } //Make sure this simulation has motors. Configuration c = new Configuration(this.getRocket()); MotorInstanceConfiguration motors = new MotorInstanceConfiguration(); c.setFlightConfigurationID(options.getMotorConfigurationID()); final String flightConfigId = c.getFlightConfigurationID(); Iterator<MotorMount> iterator = c.motorIterator(); boolean no_motors = true; while (iterator.hasNext()) { MotorMount mount = iterator.next(); RocketComponent component = (RocketComponent) mount; MotorConfiguration motorConfig = mount.getMotorConfiguration().get(flightConfigId); IgnitionConfiguration ignitionConfig = mount.getIgnitionConfiguration().get(flightConfigId); Motor motor = motorConfig.getMotor(); if (motor != null) no_motors = false; } if (no_motors) status = Status.CANT_RUN; return status; } /** * Simulate the flight. * * @param additionalListeners additional simulation listeners (those defined by the simulation are used in any case) * @throws SimulationException if a problem occurs during simulation */ public void simulate(SimulationListener... additionalListeners) throws SimulationException { mutex.lock("simulate"); try { if (this.status == Status.EXTERNAL) { throw new SimulationException("Cannot simulate imported simulation."); } SimulationEngine simulator; try { simulator = simulationEngineClass.newInstance(); } catch (InstantiationException e) { throw new IllegalStateException("Cannot instantiate simulator.", e); } catch (IllegalAccessException e) { throw new IllegalStateException("Cannot access simulator instance?! BUG!", e); } SimulationConditions simulationConditions = options.toSimulationConditions(); simulationConditions.setSimulation(this); for (SimulationListener l : additionalListeners) { simulationConditions.getSimulationListenerList().add(l); } for (SimulationExtension extension : simulationExtensions) { extension.initialize(simulationConditions); } long t1, t2; log.debug("Simulation: calling simulator"); t1 = System.currentTimeMillis(); simulatedData = simulator.simulate(simulationConditions); t2 = System.currentTimeMillis(); log.debug("Simulation: returning from simulator, simulation took " + (t2 - t1) + "ms"); // Set simulated info after simulation, will not be set in case of exception simulatedConditions = options.clone(); final Configuration configuration = getConfiguration(); simulatedConfiguration = descriptor.format(configuration.getRocket(), configuration.getFlightConfigurationID()); simulatedRocketID = rocket.getFunctionalModID(); status = Status.UPTODATE; fireChangeEvent(); configuration.release(); } finally { mutex.unlock("simulate"); } } /** * Return the conditions used in the previous simulation, or <code>null</code> * if this simulation has not been run. * * @return the conditions used in the previous simulation, or <code>null</code>. */ public SimulationOptions getSimulatedConditions() { mutex.verify(); return simulatedConditions; } /** * Return the warnings generated in the previous simulation, or * <code>null</code> if this simulation has not been run. This is the same * warning set as contained in the <code>FlightData</code> object. * * @return the warnings during the previous simulation, or <code>null</code>. * @see FlightData#getWarningSet() */ public WarningSet getSimulatedWarnings() { mutex.verify(); if (simulatedData == null) return null; return simulatedData.getWarningSet(); } /** * Return a string describing the motor configuration of the previous simulation, * or <code>null</code> if this simulation has not been run. * * @return a description of the motor configuration of the previous simulation, or * <code>null</code>. */ public String getSimulatedConfigurationDescription() { mutex.verify(); return simulatedConfiguration; } /** * Return the flight data of the previous simulation, or <code>null</code> if * this simulation has not been run. * * @return the flight data of the previous simulation, or <code>null</code>. */ public FlightData getSimulatedData() { mutex.verify(); return simulatedData; } /** * Return true if this simulation contains plotable flight data. * * @return */ public boolean hasSimulationData() { FlightData data = getSimulatedData(); if (data == null) { return false; } if (data.getBranchCount() == 0) { return false; } return true; } /** * Returns a copy of this simulation suitable for cut/copy/paste operations. * The rocket refers to the same instance as the original simulation. * This excludes any simulated data. * * @return a copy of this simulation and its conditions. */ public Simulation copy() { mutex.lock("copy"); try { Simulation copy = (Simulation) super.clone(); copy.mutex = SafetyMutex.newInstance(); copy.status = Status.NOT_SIMULATED; copy.options = this.options.clone(); copy.simulationExtensions = new ArrayList<SimulationExtension>(); for (SimulationExtension c : this.simulationExtensions) { copy.simulationExtensions.add(c.clone()); } copy.listeners = new ArrayList<EventListener>(); copy.simulatedConditions = null; copy.simulatedConfiguration = null; copy.simulatedData = null; copy.simulatedRocketID = -1; return copy; } catch (CloneNotSupportedException e) { throw new BugException("Clone not supported, BUG", e); } finally { mutex.unlock("copy"); } } /** * Create a duplicate of this simulation with the specified rocket. The new * simulation is in non-simulated state. * * @param newRocket the rocket for the new simulation. * @return a new simulation with the same conditions and properties. */ public Simulation duplicateSimulation(Rocket newRocket) { mutex.lock("duplicateSimulation"); try { Simulation copy = new Simulation(newRocket); copy.name = this.name; copy.options.copyFrom(this.options); copy.simulatedConfiguration = this.simulatedConfiguration; for (SimulationExtension c : this.simulationExtensions) { copy.simulationExtensions.add(c.clone()); } copy.simulationStepperClass = this.simulationStepperClass; copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass; return copy; } finally { mutex.unlock("duplicateSimulation"); } } @Override public void addChangeListener(StateChangeListener listener) { mutex.verify(); listeners.add(listener); } @Override public void removeChangeListener(StateChangeListener listener) { mutex.verify(); listeners.remove(listener); } protected void fireChangeEvent() { EventObject e = new EventObject(this); // Copy the list before iterating to prevent concurrent modification exceptions. EventListener[] ls = listeners.toArray(new EventListener[0]); for (EventListener l : ls) { if (l instanceof StateChangeListener) { ((StateChangeListener) l).stateChanged(e); } } } private class ConditionListener implements StateChangeListener { private Status oldStatus = null; @Override public void stateChanged(EventObject e) { if (getStatus() != oldStatus) { oldStatus = getStatus(); fireChangeEvent(); } } } }