moving to core/
[debian/openrocket] / core / src / net / sf / openrocket / gui / main / SimulationRunDialog.java
diff --git a/core/src/net/sf/openrocket/gui/main/SimulationRunDialog.java b/core/src/net/sf/openrocket/gui/main/SimulationRunDialog.java
new file mode 100644 (file)
index 0000000..c108030
--- /dev/null
@@ -0,0 +1,462 @@
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.dialogs.DetailDialog;
+import net.sf.openrocket.gui.util.GUIUtil;
+import net.sf.openrocket.gui.util.SwingPreferences;
+import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationCancelledException;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.exception.SimulationLaunchException;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+import net.sf.openrocket.simulation.listeners.SimulationListener;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class SimulationRunDialog extends JDialog {
+       private static final LogHelper log = Application.getLogger();
+       private static final Translator trans = Application.getTranslator();
+       
+
+       /** Update the dialog status every this many ms */
+       private static final long UPDATE_MS = 200;
+       
+       /** Flight progress at motor burnout */
+       private static final double BURNOUT_PROGRESS = 0.4;
+       
+       /** Flight progress at apogee */
+       private static final double APOGEE_PROGRESS = 0.7;
+       
+
+       /*
+        * The executor service is not static since we want concurrent simulation
+        * dialogs to run in parallel, ie. they both have their own executor service.
+        */
+       private final ExecutorService executor = Executors.newFixedThreadPool(
+                       SwingPreferences.getMaxThreadCount());
+       
+
+       private final JLabel simLabel, timeLabel, altLabel, velLabel;
+       private final JProgressBar progressBar;
+       
+
+       /*
+        * NOTE:  Care must be used when accessing the simulation parameters, since they
+        * are being run in another thread.  Mutexes are used to avoid concurrent usage, which
+        * will result in an exception being thrown!
+        */
+       private final Simulation[] simulations;
+       private final String[] simulationNames;
+       private final SimulationWorker[] simulationWorkers;
+       private final SimulationStatus[] simulationStatuses;
+       private final double[] simulationMaxAltitude;
+       private final double[] simulationMaxVelocity;
+       private final boolean[] simulationDone;
+       
+       public SimulationRunDialog(Window window, Simulation... simulations) {
+               //// Running simulations...
+               super(window, trans.get("SimuRunDlg.title.RunSim"), Dialog.ModalityType.DOCUMENT_MODAL);
+               
+               if (simulations.length == 0) {
+                       throw new IllegalArgumentException("Called with no simulations to run");
+               }
+               
+               this.simulations = simulations;
+               
+
+               // Randomize the simulation random seeds
+               for (Simulation sim : simulations) {
+                       sim.getOptions().randomizeSeed();
+               }
+               
+               // Initialize the simulations
+               int n = simulations.length;
+               simulationNames = new String[n];
+               simulationWorkers = new SimulationWorker[n];
+               simulationStatuses = new SimulationStatus[n];
+               simulationMaxAltitude = new double[n];
+               simulationMaxVelocity = new double[n];
+               simulationDone = new boolean[n];
+               
+               for (int i = 0; i < n; i++) {
+                       simulationNames[i] = simulations[i].getName();
+                       simulationWorkers[i] = new InteractiveSimulationWorker(simulations[i], i);
+                       executor.execute(simulationWorkers[i]);
+               }
+               
+               // Build the dialog
+               JPanel panel = new JPanel(new MigLayout("fill", "[][grow]"));
+               
+               //// Running ...
+               simLabel = new JLabel(trans.get("SimuRunDlg.lbl.Running"));
+               panel.add(simLabel, "spanx, wrap para");
+               //// Simulation time: 
+               panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Simutime") + " "), "gapright para");
+               timeLabel = new JLabel("");
+               panel.add(timeLabel, "growx, wmin 200lp, wrap rel");
+               
+               //// Altitude:
+               panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Altitude") + " "));
+               altLabel = new JLabel("");
+               panel.add(altLabel, "growx, wrap rel");
+               
+               //// Velocity:
+               panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Velocity") + " "));
+               velLabel = new JLabel("");
+               panel.add(velLabel, "growx, wrap para");
+               
+               progressBar = new JProgressBar();
+               panel.add(progressBar, "spanx, growx, wrap para");
+               
+
+               // Add cancel button
+               JButton cancel = new JButton(trans.get("dlg.but.cancel"));
+               cancel.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               cancelSimulations();
+                       }
+               });
+               panel.add(cancel, "spanx, tag cancel");
+               
+
+               // Cancel simulations when user closes the window
+               this.addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosing(WindowEvent e) {
+                               cancelSimulations();
+                       }
+               });
+               
+
+               this.add(panel);
+               this.setMinimumSize(new Dimension(300, 0));
+               this.setLocationByPlatform(true);
+               this.validate();
+               this.pack();
+               
+               GUIUtil.setDisposableDialogOptions(this, null);
+               
+               updateProgress();
+       }
+       
+       
+       /**
+        * Cancel the currently running simulations.  This is equivalent to clicking
+        * the Cancel button on the dialog.
+        */
+       public void cancelSimulations() {
+               executor.shutdownNow();
+               for (SimulationWorker w : simulationWorkers) {
+                       w.cancel(true);
+               }
+       }
+       
+       
+       /**
+        * Static helper method to run simulations.
+        * 
+        * @param parent                the parent Window of the dialog to use.
+        * @param simulations   the simulations to run.
+        */
+       public static void runSimulations(Window parent, Simulation... simulations) {
+               new SimulationRunDialog(parent, simulations).setVisible(true);
+       }
+       
+       
+
+
+       private void updateProgress() {
+               int index;
+               for (index = 0; index < simulations.length; index++) {
+                       if (!simulationDone[index])
+                               break;
+               }
+               
+               if (index >= simulations.length) {
+                       // Everything is done, close the dialog
+                       log.debug("Everything done.");
+                       this.dispose();
+                       return;
+               }
+               
+               // Update the progress bar status
+               int progress = 0;
+               for (SimulationWorker s : simulationWorkers) {
+                       progress += s.getProgress();
+               }
+               progress /= simulationWorkers.length;
+               progressBar.setValue(progress);
+               log.debug("Progressbar value " + progress);
+               
+               // Update the simulation fields
+               simLabel.setText("Running " + simulationNames[index]);
+               if (simulationStatuses[index] == null) {
+                       log.debug("No simulation status data available, setting empty labels");
+                       timeLabel.setText("");
+                       altLabel.setText("");
+                       velLabel.setText("");
+                       return;
+               }
+               
+               Unit u = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit();
+               timeLabel.setText(u.toStringUnit(simulationStatuses[index].getSimulationTime()));
+               
+               u = UnitGroup.UNITS_DISTANCE.getDefaultUnit();
+               altLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketPosition().z) + " (max. " +
+                               u.toStringUnit(simulationMaxAltitude[index]) + ")");
+               
+               u = UnitGroup.UNITS_VELOCITY.getDefaultUnit();
+               velLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketVelocity().z) + " (max. " +
+                               u.toStringUnit(simulationMaxVelocity[index]) + ")");
+       }
+       
+       
+
+       /**
+        * A SwingWorker that performs a flight simulation.  It periodically updates the
+        * simulation statuses of the parent class and calls updateProgress().
+        * The progress of the simulation is stored in the progress property of the
+        * SwingWorker.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       private class InteractiveSimulationWorker extends SimulationWorker {
+               
+               private final int index;
+               private final double burnoutTimeEstimate;
+               private volatile double burnoutVelocity;
+               private volatile double apogeeAltitude;
+               
+               /*
+                * -2 = time from 0 ... burnoutTimeEstimate
+                * -1 = velocity from v(burnoutTimeEstimate) ... 0
+                *  0 ... n = stages from alt(max) ... 0
+                */
+               private volatile int simulationStage = -2;
+               
+               private int progress = 0;
+               
+               
+               public InteractiveSimulationWorker(Simulation sim, int index) {
+                       super(sim);
+                       this.index = index;
+                       
+                       // Calculate estimate of motor burn time
+                       double launchBurn = 0;
+                       double otherBurn = 0;
+                       Configuration config = simulation.getConfiguration();
+                       String id = simulation.getOptions().getMotorConfigurationID();
+                       Iterator<MotorMount> iterator = config.motorIterator();
+                       while (iterator.hasNext()) {
+                               MotorMount m = iterator.next();
+                               if (m.getIgnitionEvent() == IgnitionEvent.LAUNCH)
+                                       launchBurn = MathUtil.max(launchBurn, m.getMotor(id).getBurnTimeEstimate());
+                               else
+                                       otherBurn = otherBurn + m.getMotor(id).getBurnTimeEstimate();
+                       }
+                       burnoutTimeEstimate = Math.max(launchBurn + otherBurn, 0.1);
+                       
+               }
+               
+               
+               /**
+                * Return the extra listeners to use, a progress listener and cancel listener.
+                */
+               @Override
+               protected SimulationListener[] getExtraListeners() {
+                       return new SimulationListener[] { new SimulationProgressListener() };
+               }
+               
+               
+               /**
+                * Processes simulation statuses published by the simulation listener.
+                * The statuses of the parent class and the progress property are updated.
+                */
+               @Override
+               protected void process(List<SimulationStatus> chunks) {
+                       
+                       // Update max. altitude and velocity
+                       for (SimulationStatus s : chunks) {
+                               simulationMaxAltitude[index] = Math.max(simulationMaxAltitude[index],
+                                               s.getRocketPosition().z);
+                               simulationMaxVelocity[index] = Math.max(simulationMaxVelocity[index],
+                                               s.getRocketVelocity().length());
+                       }
+                       
+                       // Calculate the progress
+                       SimulationStatus status = chunks.get(chunks.size() - 1);
+                       simulationStatuses[index] = status;
+                       
+                       // 1. time = 0 ... burnoutTimeEstimate
+                       if (simulationStage == -2 && status.getSimulationTime() < burnoutTimeEstimate) {
+                               log.debug("Method 1:  t=" + status.getSimulationTime() + "  est=" + burnoutTimeEstimate);
+                               setSimulationProgress(MathUtil.map(status.getSimulationTime(), 0, burnoutTimeEstimate,
+                                               0.0, BURNOUT_PROGRESS));
+                               updateProgress();
+                               return;
+                       }
+                       
+                       if (simulationStage == -2) {
+                               simulationStage++;
+                               burnoutVelocity = MathUtil.max(status.getRocketVelocity().z, 0.1);
+                               log.debug("CHANGING to Method 2, vel=" + burnoutVelocity);
+                       }
+                       
+                       // 2. z-velocity from burnout velocity to zero
+                       if (simulationStage == -1 && status.getRocketVelocity().z >= 0) {
+                               log.debug("Method 2:  vel=" + status.getRocketVelocity().z + " burnout=" + burnoutVelocity);
+                               setSimulationProgress(MathUtil.map(status.getRocketVelocity().z, burnoutVelocity, 0,
+                                               BURNOUT_PROGRESS, APOGEE_PROGRESS));
+                               updateProgress();
+                               return;
+                       }
+                       
+                       if (simulationStage == -1 && status.getRocketVelocity().z < 0) {
+                               simulationStage++;
+                               apogeeAltitude = MathUtil.max(status.getRocketPosition().z, 1);
+                               log.debug("CHANGING to Method 3, apogee=" + apogeeAltitude);
+                       }
+                       
+                       // 3. z-position from apogee to zero
+                       // TODO: MEDIUM: several stages
+                       log.debug("Method 3:  alt=" + status.getRocketPosition().z + "  apogee=" + apogeeAltitude);
+                       setSimulationProgress(MathUtil.map(status.getRocketPosition().z,
+                                       apogeeAltitude, 0, APOGEE_PROGRESS, 1.0));
+                       updateProgress();
+               }
+               
+               /**
+                * Marks this simulation as done and calls the progress update.
+                */
+               @Override
+               protected void simulationDone() {
+                       simulationDone[index] = true;
+                       log.debug("Simulation done");
+                       setSimulationProgress(1.0);
+                       updateProgress();
+               }
+               
+               
+               /**
+                * Marks the simulation as done and shows a dialog presenting
+                * the error, unless the simulation was cancelled.
+                */
+               @Override
+               protected void simulationInterrupted(Throwable t) {
+                       
+                       if (t instanceof SimulationCancelledException) {
+                               simulationDone();
+                               return; // Ignore cancellations
+                       }
+                       
+                       // Analyze the exception type
+                       if (t instanceof SimulationLaunchException) {
+                               
+                               DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
+                                               new Object[] {
+                                                               //// Unable to simulate:
+                                                               trans.get("SimuRunDlg.msg.Unabletosim"),
+                                                               t.getMessage()
+                                               },
+                                               null, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+                               
+                       } else if (t instanceof SimulationException) {
+                               
+                               DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
+                                               new Object[] {
+                                                               //// A error occurred during the simulation:
+                                                               trans.get("SimuRunDlg.msg.errorOccurred"),
+                                                               t.getMessage()
+                                               },
+                                               null, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+                               
+                       } else {
+                               
+                               Application.getExceptionHandler().handleErrorCondition("An exception occurred during the simulation", t);
+                               
+                       }
+                       simulationDone();
+               }
+               
+               
+               private void setSimulationProgress(double p) {
+                       int exact = Math.max(progress, (int) (100 * p + 0.5));
+                       progress = MathUtil.clamp(exact, 0, 100);
+                       log.debug("Setting progress to " + progress + " (real " + exact + ")");
+                       super.setProgress(progress);
+               }
+               
+               
+               /**
+                * A simulation listener that regularly updates the progress property of the 
+                * SimulationWorker and publishes the simulation status for the run dialog to process.
+                * 
+                * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+                */
+               private class SimulationProgressListener extends AbstractSimulationListener {
+                       private long time = 0;
+                       
+                       @Override
+                       public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) {
+                               switch (event.getType()) {
+                               case APOGEE:
+                                       simulationStage = 0;
+                                       apogeeAltitude = status.getRocketPosition().z;
+                                       log.debug("APOGEE, setting progress");
+                                       setSimulationProgress(APOGEE_PROGRESS);
+                                       publish(status);
+                                       break;
+                               
+                               case LAUNCH:
+                                       publish(status);
+                                       break;
+                               
+                               case SIMULATION_END:
+                                       log.debug("END, setting progress");
+                                       setSimulationProgress(1.0);
+                                       break;
+                               }
+                               return true;
+                       }
+                       
+                       @Override
+                       public void postStep(SimulationStatus status) {
+                               if (System.currentTimeMillis() >= time + UPDATE_MS) {
+                                       time = System.currentTimeMillis();
+                                       publish(status);
+                               }
+                       }
+               }
+       }
+}