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