Fixed leaking threads in simulations
[debian/openrocket] / core / src / net / sf / openrocket / gui / main / SimulationRunDialog.java
1 package net.sf.openrocket.gui.main;
2
3
4 import java.awt.Dialog;
5 import java.awt.Dimension;
6 import java.awt.Window;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.event.WindowAdapter;
10 import java.awt.event.WindowEvent;
11 import java.util.Iterator;
12 import java.util.List;
13 import java.util.concurrent.ExecutorService;
14 import java.util.concurrent.Executors;
15
16 import javax.swing.JButton;
17 import javax.swing.JDialog;
18 import javax.swing.JLabel;
19 import javax.swing.JOptionPane;
20 import javax.swing.JPanel;
21 import javax.swing.JProgressBar;
22
23 import net.miginfocom.swing.MigLayout;
24 import net.sf.openrocket.document.Simulation;
25 import net.sf.openrocket.gui.dialogs.DetailDialog;
26 import net.sf.openrocket.gui.util.GUIUtil;
27 import net.sf.openrocket.gui.util.SwingPreferences;
28 import net.sf.openrocket.l10n.Translator;
29 import net.sf.openrocket.logging.LogHelper;
30 import net.sf.openrocket.rocketcomponent.Configuration;
31 import net.sf.openrocket.rocketcomponent.MotorMount;
32 import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
33 import net.sf.openrocket.simulation.FlightEvent;
34 import net.sf.openrocket.simulation.SimulationStatus;
35 import net.sf.openrocket.simulation.exception.SimulationCancelledException;
36 import net.sf.openrocket.simulation.exception.SimulationException;
37 import net.sf.openrocket.simulation.exception.SimulationLaunchException;
38 import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
39 import net.sf.openrocket.simulation.listeners.SimulationListener;
40 import net.sf.openrocket.startup.Application;
41 import net.sf.openrocket.unit.Unit;
42 import net.sf.openrocket.unit.UnitGroup;
43 import net.sf.openrocket.util.MathUtil;
44
45
46 public class SimulationRunDialog extends JDialog {
47         private static final LogHelper log = Application.getLogger();
48         private static final Translator trans = Application.getTranslator();
49         
50         
51         /** Update the dialog status every this many ms */
52         private static final long UPDATE_MS = 200;
53         
54         /** Flight progress at motor burnout */
55         private static final double BURNOUT_PROGRESS = 0.4;
56         
57         /** Flight progress at apogee */
58         private static final double APOGEE_PROGRESS = 0.7;
59         
60         
61         private final static ExecutorService executor = Executors.newFixedThreadPool(
62                         SwingPreferences.getMaxThreadCount());
63         
64         
65         private final JLabel simLabel, timeLabel, altLabel, velLabel;
66         private final JProgressBar progressBar;
67         
68         
69         /*
70          * NOTE:  Care must be used when accessing the simulation parameters, since they
71          * are being run in another thread.  Mutexes are used to avoid concurrent usage, which
72          * will result in an exception being thrown!
73          */
74         private final Simulation[] simulations;
75         private final String[] simulationNames;
76         private final SimulationWorker[] simulationWorkers;
77         private final SimulationStatus[] simulationStatuses;
78         private final double[] simulationMaxAltitude;
79         private final double[] simulationMaxVelocity;
80         private final boolean[] simulationDone;
81         
82         public SimulationRunDialog(Window window, Simulation... simulations) {
83                 //// Running simulations...
84                 super(window, trans.get("SimuRunDlg.title.RunSim"), Dialog.ModalityType.APPLICATION_MODAL);
85                 
86                 if (simulations.length == 0) {
87                         throw new IllegalArgumentException("Called with no simulations to run");
88                 }
89                 
90                 this.simulations = simulations;
91                 
92                 
93                 // Randomize the simulation random seeds
94                 for (Simulation sim : simulations) {
95                         sim.getOptions().randomizeSeed();
96                 }
97                 
98                 // Initialize the simulations
99                 int n = simulations.length;
100                 simulationNames = new String[n];
101                 simulationWorkers = new SimulationWorker[n];
102                 simulationStatuses = new SimulationStatus[n];
103                 simulationMaxAltitude = new double[n];
104                 simulationMaxVelocity = new double[n];
105                 simulationDone = new boolean[n];
106                 
107                 for (int i = 0; i < n; i++) {
108                         simulationNames[i] = simulations[i].getName();
109                         simulationWorkers[i] = new InteractiveSimulationWorker(simulations[i], i);
110                         executor.execute(simulationWorkers[i]);
111                 }
112                 
113                 // Build the dialog
114                 JPanel panel = new JPanel(new MigLayout("fill", "[][grow]"));
115                 
116                 //// Running ...
117                 simLabel = new JLabel(trans.get("SimuRunDlg.lbl.Running"));
118                 panel.add(simLabel, "spanx, wrap para");
119                 //// Simulation time: 
120                 panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Simutime") + " "), "gapright para");
121                 timeLabel = new JLabel("");
122                 panel.add(timeLabel, "growx, wmin 200lp, wrap rel");
123                 
124                 //// Altitude:
125                 panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Altitude") + " "));
126                 altLabel = new JLabel("");
127                 panel.add(altLabel, "growx, wrap rel");
128                 
129                 //// Velocity:
130                 panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Velocity") + " "));
131                 velLabel = new JLabel("");
132                 panel.add(velLabel, "growx, wrap para");
133                 
134                 progressBar = new JProgressBar();
135                 panel.add(progressBar, "spanx, growx, wrap para");
136                 
137                 
138                 // Add cancel button
139                 JButton cancel = new JButton(trans.get("dlg.but.cancel"));
140                 cancel.addActionListener(new ActionListener() {
141                         @Override
142                         public void actionPerformed(ActionEvent e) {
143                                 cancelSimulations();
144                         }
145                 });
146                 panel.add(cancel, "spanx, tag cancel");
147                 
148                 
149                 // Cancel simulations when user closes the window
150                 this.addWindowListener(new WindowAdapter() {
151                         @Override
152                         public void windowClosing(WindowEvent e) {
153                                 cancelSimulations();
154                         }
155                 });
156                 
157                 
158                 this.add(panel);
159                 this.setMinimumSize(new Dimension(300, 0));
160                 this.setLocationByPlatform(true);
161                 this.validate();
162                 this.pack();
163                 
164                 GUIUtil.setDisposableDialogOptions(this, null);
165                 
166                 updateProgress();
167         }
168         
169         
170         /**
171          * Cancel the currently running simulations.  This is equivalent to clicking
172          * the Cancel button on the dialog.
173          */
174         public void cancelSimulations() {
175                 executor.shutdownNow();
176                 for (SimulationWorker w : simulationWorkers) {
177                         w.cancel(true);
178                 }
179         }
180         
181         
182         /**
183          * Static helper method to run simulations.
184          * 
185          * @param parent                the parent Window of the dialog to use.
186          * @param simulations   the simulations to run.
187          */
188         public static void runSimulations(Window parent, Simulation... simulations) {
189                 new SimulationRunDialog(parent, simulations).setVisible(true);
190         }
191         
192         
193         
194         
195         private void updateProgress() {
196                 int index;
197                 for (index = 0; index < simulations.length; index++) {
198                         if (!simulationDone[index])
199                                 break;
200                 }
201                 
202                 if (index >= simulations.length) {
203                         // Everything is done, close the dialog
204                         log.debug("Everything done.");
205                         this.dispose();
206                         return;
207                 }
208                 
209                 // Update the progress bar status
210                 int progress = 0;
211                 for (SimulationWorker s : simulationWorkers) {
212                         progress += s.getProgress();
213                 }
214                 progress /= simulationWorkers.length;
215                 progressBar.setValue(progress);
216                 log.debug("Progressbar value " + progress);
217                 
218                 // Update the simulation fields
219                 simLabel.setText("Running " + simulationNames[index]);
220                 if (simulationStatuses[index] == null) {
221                         log.debug("No simulation status data available, setting empty labels");
222                         timeLabel.setText("");
223                         altLabel.setText("");
224                         velLabel.setText("");
225                         return;
226                 }
227                 
228                 Unit u = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit();
229                 timeLabel.setText(u.toStringUnit(simulationStatuses[index].getSimulationTime()));
230                 
231                 u = UnitGroup.UNITS_DISTANCE.getDefaultUnit();
232                 altLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketPosition().z) + " (max. " +
233                                 u.toStringUnit(simulationMaxAltitude[index]) + ")");
234                 
235                 u = UnitGroup.UNITS_VELOCITY.getDefaultUnit();
236                 velLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketVelocity().z) + " (max. " +
237                                 u.toStringUnit(simulationMaxVelocity[index]) + ")");
238         }
239         
240         
241         
242         /**
243          * A SwingWorker that performs a flight simulation.  It periodically updates the
244          * simulation statuses of the parent class and calls updateProgress().
245          * The progress of the simulation is stored in the progress property of the
246          * SwingWorker.
247          * 
248          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
249          */
250         private class InteractiveSimulationWorker extends SimulationWorker {
251                 
252                 private final int index;
253                 private final double burnoutTimeEstimate;
254                 private volatile double burnoutVelocity;
255                 private volatile double apogeeAltitude;
256                 
257                 /*
258                  * -2 = time from 0 ... burnoutTimeEstimate
259                  * -1 = velocity from v(burnoutTimeEstimate) ... 0
260                  *  0 ... n = stages from alt(max) ... 0
261                  */
262                 private volatile int simulationStage = -2;
263                 
264                 private int progress = 0;
265                 
266                 
267                 public InteractiveSimulationWorker(Simulation sim, int index) {
268                         super(sim);
269                         this.index = index;
270                         
271                         // Calculate estimate of motor burn time
272                         double launchBurn = 0;
273                         double otherBurn = 0;
274                         Configuration config = simulation.getConfiguration();
275                         String id = simulation.getOptions().getMotorConfigurationID();
276                         Iterator<MotorMount> iterator = config.motorIterator();
277                         while (iterator.hasNext()) {
278                                 MotorMount m = iterator.next();
279                                 if (m.getIgnitionEvent() == IgnitionEvent.LAUNCH)
280                                         launchBurn = MathUtil.max(launchBurn, m.getMotor(id).getBurnTimeEstimate());
281                                 else
282                                         otherBurn = otherBurn + m.getMotor(id).getBurnTimeEstimate();
283                         }
284                         burnoutTimeEstimate = Math.max(launchBurn + otherBurn, 0.1);
285                         
286                 }
287                 
288                 
289                 /**
290                  * Return the extra listeners to use, a progress listener and cancel listener.
291                  */
292                 @Override
293                 protected SimulationListener[] getExtraListeners() {
294                         return new SimulationListener[] { new SimulationProgressListener() };
295                 }
296                 
297                 
298                 /**
299                  * Processes simulation statuses published by the simulation listener.
300                  * The statuses of the parent class and the progress property are updated.
301                  */
302                 @Override
303                 protected void process(List<SimulationStatus> chunks) {
304                         
305                         // Update max. altitude and velocity
306                         for (SimulationStatus s : chunks) {
307                                 simulationMaxAltitude[index] = Math.max(simulationMaxAltitude[index],
308                                                 s.getRocketPosition().z);
309                                 simulationMaxVelocity[index] = Math.max(simulationMaxVelocity[index],
310                                                 s.getRocketVelocity().length());
311                         }
312                         
313                         // Calculate the progress
314                         SimulationStatus status = chunks.get(chunks.size() - 1);
315                         simulationStatuses[index] = status;
316                         
317                         // 1. time = 0 ... burnoutTimeEstimate
318                         if (simulationStage == -2 && status.getSimulationTime() < burnoutTimeEstimate) {
319                                 log.debug("Method 1:  t=" + status.getSimulationTime() + "  est=" + burnoutTimeEstimate);
320                                 setSimulationProgress(MathUtil.map(status.getSimulationTime(), 0, burnoutTimeEstimate,
321                                                 0.0, BURNOUT_PROGRESS));
322                                 updateProgress();
323                                 return;
324                         }
325                         
326                         if (simulationStage == -2) {
327                                 simulationStage++;
328                                 burnoutVelocity = MathUtil.max(status.getRocketVelocity().z, 0.1);
329                                 log.debug("CHANGING to Method 2, vel=" + burnoutVelocity);
330                         }
331                         
332                         // 2. z-velocity from burnout velocity to zero
333                         if (simulationStage == -1 && status.getRocketVelocity().z >= 0) {
334                                 log.debug("Method 2:  vel=" + status.getRocketVelocity().z + " burnout=" + burnoutVelocity);
335                                 setSimulationProgress(MathUtil.map(status.getRocketVelocity().z, burnoutVelocity, 0,
336                                                 BURNOUT_PROGRESS, APOGEE_PROGRESS));
337                                 updateProgress();
338                                 return;
339                         }
340                         
341                         if (simulationStage == -1 && status.getRocketVelocity().z < 0) {
342                                 simulationStage++;
343                                 apogeeAltitude = MathUtil.max(status.getRocketPosition().z, 1);
344                                 log.debug("CHANGING to Method 3, apogee=" + apogeeAltitude);
345                         }
346                         
347                         // 3. z-position from apogee to zero
348                         // TODO: MEDIUM: several stages
349                         log.debug("Method 3:  alt=" + status.getRocketPosition().z + "  apogee=" + apogeeAltitude);
350                         setSimulationProgress(MathUtil.map(status.getRocketPosition().z,
351                                         apogeeAltitude, 0, APOGEE_PROGRESS, 1.0));
352                         updateProgress();
353                 }
354                 
355                 /**
356                  * Marks this simulation as done and calls the progress update.
357                  */
358                 @Override
359                 protected void simulationDone() {
360                         simulationDone[index] = true;
361                         log.debug("Simulation done");
362                         setSimulationProgress(1.0);
363                         updateProgress();
364                 }
365                 
366                 
367                 /**
368                  * Marks the simulation as done and shows a dialog presenting
369                  * the error, unless the simulation was cancelled.
370                  */
371                 @Override
372                 protected void simulationInterrupted(Throwable t) {
373                         
374                         if (t instanceof SimulationCancelledException) {
375                                 simulationDone();
376                                 return; // Ignore cancellations
377                         }
378                         
379                         // Analyze the exception type
380                         if (t instanceof SimulationLaunchException) {
381                                 
382                                 DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
383                                                 new Object[] {
384                                                                 //// Unable to simulate:
385                                                                 trans.get("SimuRunDlg.msg.Unabletosim"),
386                                                                 t.getMessage()
387                                                 },
388                                                 null, simulation.getName(), JOptionPane.ERROR_MESSAGE);
389                                 
390                         } else if (t instanceof SimulationException) {
391                                 
392                                 DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
393                                                 new Object[] {
394                                                                 //// A error occurred during the simulation:
395                                                                 trans.get("SimuRunDlg.msg.errorOccurred"),
396                                                                 t.getMessage()
397                                                 },
398                                                 null, simulation.getName(), JOptionPane.ERROR_MESSAGE);
399                                 
400                         } else {
401                                 
402                                 Application.getExceptionHandler().handleErrorCondition("An exception occurred during the simulation", t);
403                                 
404                         }
405                         simulationDone();
406                 }
407                 
408                 
409                 private void setSimulationProgress(double p) {
410                         int exact = Math.max(progress, (int) (100 * p + 0.5));
411                         progress = MathUtil.clamp(exact, 0, 100);
412                         log.debug("Setting progress to " + progress + " (real " + exact + ")");
413                         super.setProgress(progress);
414                 }
415                 
416                 
417                 /**
418                  * A simulation listener that regularly updates the progress property of the 
419                  * SimulationWorker and publishes the simulation status for the run dialog to process.
420                  * 
421                  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
422                  */
423                 private class SimulationProgressListener extends AbstractSimulationListener {
424                         private long time = 0;
425                         
426                         @Override
427                         public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) {
428                                 switch (event.getType()) {
429                                 case APOGEE:
430                                         simulationStage = 0;
431                                         apogeeAltitude = status.getRocketPosition().z;
432                                         log.debug("APOGEE, setting progress");
433                                         setSimulationProgress(APOGEE_PROGRESS);
434                                         publish(status);
435                                         break;
436                                 
437                                 case LAUNCH:
438                                         publish(status);
439                                         break;
440                                 
441                                 case SIMULATION_END:
442                                         log.debug("END, setting progress");
443                                         setSimulationProgress(1.0);
444                                         break;
445                                 }
446                                 return true;
447                         }
448                         
449                         @Override
450                         public void postStep(SimulationStatus status) {
451                                 if (System.currentTimeMillis() >= time + UPDATE_MS) {
452                                         time = System.currentTimeMillis();
453                                         publish(status);
454                                 }
455                         }
456                 }
457         }
458 }