View Javadoc
1   package org.opentrafficsim.simulationengine;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Font;
5   import java.awt.event.ActionEvent;
6   import java.awt.event.ActionListener;
7   import java.beans.PropertyChangeEvent;
8   import java.beans.PropertyChangeListener;
9   import java.text.ParseException;
10  import java.util.ArrayList;
11  import java.util.Timer;
12  import java.util.TimerTask;
13  import java.util.logging.Level;
14  import java.util.logging.Logger;
15  import java.util.regex.Matcher;
16  import java.util.regex.Pattern;
17  
18  import javax.swing.ImageIcon;
19  import javax.swing.JButton;
20  import javax.swing.JFormattedTextField;
21  import javax.swing.JLabel;
22  import javax.swing.JPanel;
23  import javax.swing.SwingUtilities;
24  import javax.swing.text.DefaultFormatter;
25  import javax.swing.text.MaskFormatter;
26  
27  import nl.tudelft.simulation.dsol.SimRuntimeException;
28  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
29  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
30  import nl.tudelft.simulation.dsol.gui.swing.DSOLPanel;
31  import nl.tudelft.simulation.dsol.gui.swing.SimulatorControlPanel;
32  import nl.tudelft.simulation.dsol.simulators.DEVSSimulator;
33  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
34  import nl.tudelft.simulation.language.io.URLResource;
35  
36  import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
37  import org.opentrafficsim.core.unit.TimeUnit;
38  import org.opentrafficsim.core.value.vdouble.scalar.DoubleScalar;
39  
40  /**
41   * Peter's improved simulation control panel.
42   * <p>
43   * Copyright (c) 2013-2014 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
44   * reserved. <br>
45   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
46   * <p>
47   * @version 11 dec. 2014 <br>
48   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
49   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
50   */
51  public class ControlPanel implements ActionListener, PropertyChangeListener
52  {
53      /** The simulator. */
54      private final DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble> simulator;
55  
56      /** The SimulatorInterface that is controlled by the buttons. */
57      private final SimulatorInterface<?, ?, ?> target;
58  
59      /** Logger. */
60      private final Logger logger;
61  
62      /** The clock. */
63      private final ClockPanel clockPanel;
64  
65      /** The control buttons. */
66      private final ArrayList<JButton> buttons = new ArrayList<JButton>();
67  
68      /** Font used to display the clock and the stop time. */
69      private final Font timeFont = new Font("SansSerif", Font.BOLD, 18);
70  
71      /** The TimeEdit that lets the user set a time when the simulation will be stopped. */
72      private final TimeEdit timeEdit;
73  
74      /** The currently registered stop at event. */
75      private SimEvent<OTSSimTimeDouble> stopAtEvent = null;
76  
77      /**
78       * Decorate a SimpleSimulator with a different set of control buttons.
79       * @param simulator SimpleSimulator; the simulator.
80       */
81      public ControlPanel(final SimpleSimulator simulator)
82      {
83          this.simulator = simulator.getSimulator();
84          this.target = simulator.getSimulator();
85          this.logger = Logger.getLogger("nl.tudelft.opentrafficsim");
86  
87          DSOLPanel<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble> panel =
88                  simulator.getPanel();
89          SimulatorControlPanel controlPanel =
90                  (SimulatorControlPanel) ((BorderLayout) panel.getLayout()).getLayoutComponent(BorderLayout.NORTH);
91          JPanel buttonPanel = (JPanel) controlPanel.getComponent(0);
92          buttonPanel.removeAll();
93          buttonPanel.add(makeButton("stepButton", "/Last_recor.png", "Step", "Execute one event", true));
94          buttonPanel.add(makeButton("nextTimeButton", "/NextTrack.png", "NextTime",
95                  "Execute all events scheduled for the current time", true));
96          buttonPanel.add(makeButton("runButton", "/Play.png", "Run", "Run the simulation at maximum speed", true));
97          buttonPanel.add(makeButton("pauseButton", "/Pause.png", "Pause", "Pause the simulator", false));
98          buttonPanel.add(makeButton("resetButton", "/Undo.png", "Reset", null, false));
99          this.clockPanel = new ClockPanel();
100         buttonPanel.add(this.clockPanel);
101         this.timeEdit = new TimeEdit(new DoubleScalar.Abs<TimeUnit>(0, TimeUnit.SECOND));
102         this.timeEdit.addPropertyChangeListener("value", this);
103         buttonPanel.add(this.timeEdit);
104     }
105 
106     /**
107      * Create a button.
108      * @param name String; name of the button
109      * @param iconPath String; path to the resource
110      * @param actionCommand String; the action command
111      * @param toolTipText String; the hint to show when the mouse hovers over the button
112      * @param enabled boolean; true if the new button must initially be enable; false if it must initially be disabled
113      * @return JButton
114      */
115     private JButton makeButton(final String name, final String iconPath, final String actionCommand,
116             final String toolTipText, final boolean enabled)
117     {
118         // JButton result = new JButton(new ImageIcon(this.getClass().getResource(iconPath)));
119         JButton result = new JButton(new ImageIcon(URLResource.getResource(iconPath)));
120         result.setName(name);
121         result.setEnabled(enabled);
122         result.setActionCommand(actionCommand);
123         result.setToolTipText(toolTipText);
124         result.addActionListener(this);
125         this.buttons.add(result);
126         return result;
127     }
128 
129     /** {@inheritDoc} */
130     @Override
131     public final void actionPerformed(final ActionEvent actionEvent)
132     {
133         String actionCommand = actionEvent.getActionCommand();
134         // System.out.println("actionCommand: " + actionCommand);
135         try
136         {
137             if (actionCommand.equals("Step"))
138             {
139                 if (this.simulator.isRunning())
140                 {
141                     this.simulator.stop();
142                 }
143                 this.target.step();
144             }
145             if (actionCommand.equals("Run"))
146             {
147                 this.target.start();
148             }
149             if (actionCommand.equals("NextTime"))
150             {
151                 if (this.simulator.isRunning())
152                 {
153                     this.simulator.stop();
154                 }
155                 double now = this.simulator.getSimulatorTime().get().getSI();
156                 // System.out.println("now is " + now);
157                 this.stopAtEvent =
158                         new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new DoubleScalar.Abs<TimeUnit>(now,
159                                 TimeUnit.SECOND)), SimEventInterface.MIN_PRIORITY, this, this, "autoPauseSimulator",
160                                 null);
161                 try
162                 {
163                     this.simulator.scheduleEvent(this.stopAtEvent);
164                 }
165                 catch (SimRuntimeException exception)
166                 {
167                     this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator", "Caught an exception "
168                             + "while trying to schedule an autoPauseSimulator event at the current simulator time");
169                 }
170                 this.target.start();
171             }
172             if (actionCommand.equals("Pause"))
173             {
174                 this.target.stop();
175             }
176             if (actionCommand.equals("Reset"))
177             {
178                 if (this.simulator.isRunning())
179                 {
180                     this.simulator.stop();
181                 }
182                 // Should this create a new replication?
183             }
184             fixButtons();
185         }
186         catch (Exception exception)
187         {
188             this.logger.logp(Level.SEVERE, "ControlPanel", "actionPerformed", "", exception);
189         }
190     }
191 
192     /**
193      * Update the enabled state of all the buttons.
194      */
195     protected final void fixButtons()
196     {
197         System.out.println("FixButtons entered");
198         final boolean moreWorkToDo = this.simulator.getEventList().size() > 0;
199         for (JButton button : this.buttons)
200         {
201             final String actionCommand = button.getActionCommand();
202             if (actionCommand.equals("Step"))
203             {
204                 button.setEnabled(moreWorkToDo);
205             }
206             else if (actionCommand.equals("Run"))
207             {
208                 button.setEnabled(moreWorkToDo && !this.simulator.isRunning());
209             }
210             else if (actionCommand.equals("NextTime"))
211             {
212                 button.setEnabled(moreWorkToDo);
213             }
214             else if (actionCommand.equals("Pause"))
215             {
216                 button.setEnabled(this.simulator.isRunning());
217             }
218             else if (actionCommand.equals("Reset"))
219             {
220                 button.setEnabled(true); // FIXME: should be disabled when the simulator was just reset or initialized
221             }
222             else
223             {
224                 this.logger.logp(Level.SEVERE, "ControlPanel", "fixButtons", "", new Exception("Unknown button?"));
225             }
226         }
227         System.out.println("FixButtons finishing");
228     }
229 
230     /**
231      * Pause the simulator.
232      */
233     public final void autoPauseSimulator()
234     {
235         if (this.simulator.isRunning())
236         {
237             this.simulator.stop();
238             double currentTick = this.simulator.getSimulatorTime().get().getSI();
239             double nextTick = this.simulator.getEventList().first().getAbsoluteExecutionTime().get().getSI();
240             // System.out.println("currentTick is " + currentTick);
241             // System.out.println("nextTick is " + nextTick);
242             if (nextTick > currentTick)
243             {
244                 // The clock is now just beyond where it was when the user requested the NextTime operation
245                 // Insert another autoPauseSimulator event just before what is now the time of the next event
246                 // and let the simulator time increment to that time
247                 this.stopAtEvent =
248                         new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new DoubleScalar.Abs<TimeUnit>(nextTick,
249                                 TimeUnit.SECOND)), SimEventInterface.MAX_PRIORITY, this, this, "autoPauseSimulator",
250                                 null);
251                 // System.out.println("Re-Scheduling at " + nextTick);
252                 try
253                 {
254                     this.simulator.scheduleEvent(this.stopAtEvent);
255                     this.simulator.start();
256                 }
257                 catch (SimRuntimeException exception)
258                 {
259                     this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator",
260                             "Caught an exception while trying to re-schedule an autoPauseEvent at the next real event");
261                 }
262             }
263             else
264             {
265                 // System.out.println("Not re-scheduling");
266                 if (SwingUtilities.isEventDispatchThread())
267                 {
268                     System.out.println("Already on EventDispatchThread");
269                     fixButtons();
270                 }
271                 else
272                 {
273                     try
274                     {
275                         System.out.println("Current thread is NOT EventDispatchThread: " + Thread.currentThread());
276                         SwingUtilities.invokeAndWait(new Runnable()
277                         {
278                             @Override
279                             public void run()
280                             {
281                                 System.out.println("Runnable started");
282                                 fixButtons();
283                                 System.out.println("Runnable finishing");
284                             }
285                         });
286                     }
287                     catch (Exception e)
288                     {
289                         if (e instanceof InterruptedException)
290                         {
291                             System.out.println("Caught " + e);
292                             e.printStackTrace();
293                         }
294                         else
295                         {
296                             e.printStackTrace();
297                         }
298                     }
299                 }
300             }
301         }
302     }
303 
304     /** {@inheritDoc} */
305     @Override
306     public final void propertyChange(final PropertyChangeEvent evt)
307     {
308         // System.out.println("PropertyChanged: " + evt);
309         if (null != this.stopAtEvent)
310         {
311             this.simulator.cancelEvent(this.stopAtEvent); // silently ignore false result
312             this.stopAtEvent = null;
313         }
314         String newValue = (String) evt.getNewValue();
315         String[] fields = newValue.split("[:\\.]");
316         int hours = Integer.parseInt(fields[0]);
317         int minutes = Integer.parseInt(fields[1]);
318         int seconds = Integer.parseInt(fields[2]);
319         int fraction = Integer.parseInt(fields[3]);
320         double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
321         if (stopTime < this.simulator.getSimulatorTime().get().getSI())
322         {
323             return;
324         }
325         else
326         {
327             this.stopAtEvent =
328                     new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new DoubleScalar.Abs<TimeUnit>(stopTime,
329                             TimeUnit.SECOND)), SimEventInterface.MAX_PRIORITY, this, this, "autoPauseSimulator", null);
330             try
331             {
332                 this.simulator.scheduleEvent(this.stopAtEvent);
333             }
334             catch (SimRuntimeException exception)
335             {
336                 this.logger.logp(Level.SEVERE, "ControlPanel", "propertyChange",
337                         "Caught an exception while trying to schedule an autoPauseSimulator event");
338             }
339         }
340 
341     }
342 
343     /**
344      * @return simulator.
345      */
346     public final DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble> getSimulator()
347     {
348         return this.simulator;
349     }
350 
351     /**
352      * @return timeFont.
353      */
354     public final Font getTimeFont()
355     {
356         return this.timeFont;
357     }
358 
359     /** JLabel that displays the simulation time. */
360     class ClockPanel extends JLabel
361     {
362         /** */
363         private static final long serialVersionUID = 20141211L;
364 
365         /** The JLabel that displays the time. */
366         private final JLabel clockLabel;
367 
368         /** timer update in msec. */
369         private final static long UPDATEINTERVAL = 1000;
370 
371         /** Construct a clock panel. */
372         ClockPanel()
373         {
374             super("00:00:00.000");
375             this.clockLabel = this;
376             this.setFont(getTimeFont());
377             Timer timer = new Timer();
378             timer.scheduleAtFixedRate(new TimeUpdateTask(), 0, ClockPanel.UPDATEINTERVAL);
379 
380         }
381 
382         /** Updater for the clock panel. */
383         private class TimeUpdateTask extends TimerTask
384         {
385             /**
386              * Create a TimeUpdateTask.
387              */
388             public TimeUpdateTask()
389             {
390             }
391 
392             /** {@inheritDoc} */
393             @Override
394             public void run()
395             {
396                 double now = Math.round(getSimulator().getSimulatorTime().get().getSI() * 1000) / 1000d;
397                 int seconds = (int) Math.floor(now);
398                 int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
399                 getClockLabel().setText(
400                         String.format("  %02d:%02d:%02d.%03d  ", seconds / 3600, seconds / 60 % 60, seconds % 60,
401                                 fractionalSeconds));
402                 getClockLabel().repaint();
403             }
404         }
405 
406         /**
407          * @return clockLabel.
408          */
409         protected JLabel getClockLabel()
410         {
411             return this.clockLabel;
412         }
413 
414     }
415 
416     /** Entry field for time. */
417     class TimeEdit extends JFormattedTextField
418     {
419         /** */
420         private static final long serialVersionUID = 20141212L;
421 
422         /**
423          * Construct a new TimeEdit.
424          * @param initialValue DoubleScalar.Abs&lt;TimeUnit&gt;; the initial value for the TimeEdit
425          */
426         TimeEdit(final DoubleScalar.Abs<TimeUnit> initialValue)
427         {
428             super(new RegexFormatter("\\d\\d\\d\\d:[0-5]\\d:[0-5]\\d\\.\\d\\d\\d"));
429             MaskFormatter mf = null;
430             try
431             {
432                 mf = new MaskFormatter("####:##:##.###");
433                 mf.setPlaceholderCharacter('0');
434                 mf.setAllowsInvalid(false);
435                 mf.setCommitsOnValidEdit(true);
436                 mf.setOverwriteMode(true);
437                 mf.install(this);
438             }
439             catch (ParseException exception)
440             {
441                 exception.printStackTrace();
442             }
443             setTime(initialValue);
444             setFont(getTimeFont());
445         }
446 
447         /**
448          * Set or update the time shown in this TimeEdit.
449          * @param newValue DoubleScalar.Abs&lt;TimeUnit&gt;; the (new) value to set/show in this TimeEdit
450          */
451         public void setTime(final DoubleScalar.Abs<TimeUnit> newValue)
452         {
453             double v = newValue.getSI();
454             int integerPart = (int) Math.floor(v);
455             int fraction = (int) Math.floor((v - integerPart) * 1000);
456             String text =
457                     String.format("%04d:%02d:%02d.%03d", integerPart / 3600, integerPart / 60 % 60, integerPart % 60,
458                             fraction);
459             this.setText(text);
460         }
461     }
462 
463     /**
464      * Extension of a DefaultFormatter that uses a regular expression. <br />
465      * Derived from <a
466      * href="http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm">
467      * http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm</a>;
468      * <p>
469      * Copyright (c) 2013-2014 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
470      * reserved. <br>
471      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
472      * <p>
473      * @version 12 dec. 2014 <br>
474      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
475      */
476     static class RegexFormatter extends DefaultFormatter
477     {
478         /** */
479         private static final long serialVersionUID = 20141212L;
480 
481         /** The regular expression pattern. */
482         private Pattern pattern;
483 
484         /**
485          * Create a new RegexFormatter.
486          * @param pattern String; regular expression pattern that defines what this RexexFormatter will accept
487          */
488         public RegexFormatter(final String pattern)
489         {
490             this.pattern = Pattern.compile(pattern);
491         }
492 
493         @Override
494         public Object stringToValue(final String text) throws ParseException
495         {
496             Matcher matcher = this.pattern.matcher(text);
497             if (matcher.matches())
498             {
499                 // System.out.println("String \"" + text + "\" matches");
500                 return super.stringToValue(text);
501             }
502             // System.out.println("String \"" + text + "\" does not match");
503             throw new ParseException("Pattern did not match", 0);
504         }
505     }
506 
507 }