View Javadoc
1   package org.opentrafficsim.gui;
2   
3   import java.awt.Container;
4   import java.awt.Dimension;
5   import java.awt.FlowLayout;
6   import java.awt.Font;
7   import java.awt.Rectangle;
8   import java.awt.event.ActionEvent;
9   import java.awt.event.ActionListener;
10  import java.awt.event.WindowEvent;
11  import java.awt.event.WindowListener;
12  import java.beans.PropertyChangeEvent;
13  import java.beans.PropertyChangeListener;
14  import java.rmi.RemoteException;
15  import java.text.DecimalFormat;
16  import java.text.NumberFormat;
17  import java.text.ParseException;
18  import java.util.ArrayList;
19  import java.util.HashMap;
20  import java.util.Hashtable;
21  import java.util.Map;
22  import java.util.Timer;
23  import java.util.TimerTask;
24  import java.util.logging.Level;
25  import java.util.logging.Logger;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import javax.swing.BoxLayout;
30  import javax.swing.ImageIcon;
31  import javax.swing.JButton;
32  import javax.swing.JFormattedTextField;
33  import javax.swing.JFrame;
34  import javax.swing.JLabel;
35  import javax.swing.JPanel;
36  import javax.swing.JSlider;
37  import javax.swing.SwingConstants;
38  import javax.swing.SwingUtilities;
39  import javax.swing.WindowConstants;
40  import javax.swing.event.ChangeEvent;
41  import javax.swing.event.ChangeListener;
42  import javax.swing.text.DefaultFormatter;
43  import javax.swing.text.MaskFormatter;
44  
45  import nl.tudelft.simulation.dsol.SimRuntimeException;
46  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
47  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
48  import nl.tudelft.simulation.dsol.simulators.DEVSRealTimeClock;
49  import nl.tudelft.simulation.dsol.simulators.DEVSSimulator;
50  import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
51  import nl.tudelft.simulation.language.io.URLResource;
52  
53  import org.djunits.unit.TimeUnit;
54  import org.djunits.value.vdouble.scalar.DoubleScalar;
55  import org.opentrafficsim.core.OTS_SCALAR;
56  import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
57  import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
58  import org.opentrafficsim.simulationengine.WrappableAnimation;
59  
60  /**
61   * Peter's improved simulation control panel.
62   * <p>
63   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
64   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
65   * <p>
66   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
67   * initial version 11 dec. 2014 <br>
68   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
69   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
70   */
71  public class OTSControlPanel extends JPanel implements ActionListener, PropertyChangeListener, WindowListener, OTS_SCALAR
72  {
73      /** */
74      private static final long serialVersionUID = 20150617L;
75  
76      /** The simulator. */
77      private OTSDEVSSimulatorInterface simulator;
78  
79      /** The WrappableAnimation (needed for restart operation). */
80      private final WrappableAnimation wrappableAnimation;
81  
82      /** Logger. */
83      private final Logger logger;
84  
85      /** The clock. */
86      private final ClockPanel clockPanel;
87  
88      /** The time warp control. */
89      private final TimeWarpPanel timeWarpPanel;
90  
91      /** The control buttons. */
92      private final ArrayList<JButton> buttons = new ArrayList<JButton>();
93  
94      /** Font used to display the clock and the stop time. */
95      private final Font timeFont = new Font("SansSerif", Font.BOLD, 18);
96  
97      /** The TimeEdit that lets the user set a time when the simulation will be stopped. */
98      private final TimeEdit timeEdit;
99  
100     /** The currently registered stop at event. */
101     private SimEvent<OTSSimTimeDouble> stopAtEvent = null;
102 
103     /** Has the window close handler been registered? */
104     protected boolean closeHandlerRegistered = false;
105 
106     /** has cleanup taken place? */
107     private boolean isCleanUp = false;
108 
109     /**
110      * Decorate a SimpleSimulator with a different set of control buttons.
111      * @param simulator SimpleSimulator; the simulator
112      * @param wrappableAnimation WrappableAnimation; if non-null, the restart button should work
113      */
114     public OTSControlPanel(final OTSDEVSSimulatorInterface simulator, final WrappableAnimation wrappableAnimation)
115     {
116         this.simulator = simulator;
117         this.wrappableAnimation = wrappableAnimation;
118         this.logger = Logger.getLogger("nl.tudelft.opentrafficsim");
119 
120         this.setLayout(new FlowLayout(FlowLayout.LEFT));
121         JPanel buttonPanel = new JPanel();
122         buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
123         buttonPanel.add(makeButton("stepButton", "/Last_recor.png", "Step", "Execute one event", true));
124         buttonPanel.add(makeButton("nextTimeButton", "/NextTrack.png", "NextTime",
125             "Execute all events scheduled for the current time", true));
126         buttonPanel.add(makeButton("runButton", "/Play.png", "Run", "Run the simulation at maximum speed", true));
127         buttonPanel.add(makeButton("pauseButton", "/Pause.png", "Pause", "Pause the simulator", false));
128         this.timeWarpPanel = new TimeWarpPanel(0.1, 1000, 1, 3, simulator);
129         buttonPanel.add(this.timeWarpPanel);
130         buttonPanel.add(makeButton("resetButton", "/Undo.png", "Reset", null, false));
131         this.clockPanel = new ClockPanel();
132         buttonPanel.add(this.clockPanel);
133         this.timeEdit = new TimeEdit(new Time.Abs(0, SECOND));
134         this.timeEdit.addPropertyChangeListener("value", this);
135         buttonPanel.add(this.timeEdit);
136         this.add(buttonPanel);
137 
138         installWindowCloseHandler();
139     }
140 
141     /**
142      * Create a button.
143      * @param name String; name of the button
144      * @param iconPath String; path to the resource
145      * @param actionCommand String; the action command
146      * @param toolTipText String; the hint to show when the mouse hovers over the button
147      * @param enabled boolean; true if the new button must initially be enable; false if it must initially be disabled
148      * @return JButton
149      */
150     private JButton makeButton(final String name, final String iconPath, final String actionCommand,
151         final String toolTipText, final boolean enabled)
152     {
153         // JButton result = new JButton(new ImageIcon(this.getClass().getResource(iconPath)));
154         JButton result = new JButton(new ImageIcon(URLResource.getResource(iconPath)));
155         result.setName(name);
156         result.setEnabled(enabled);
157         result.setActionCommand(actionCommand);
158         result.setToolTipText(toolTipText);
159         result.addActionListener(this);
160         this.buttons.add(result);
161         return result;
162     }
163 
164     /**
165      * Construct and schedule a SimEvent using a DoubleScalar.Abs&lt;TimeUnit&gt; to specify the execution time.
166      * @param executionTime DoubleScalar.Abs&lt;TimeUnit&gt;; the time at which the event must happen
167      * @param priority short; should be between <cite>SimEventInterface.MAX_PRIORITY</cite> and
168      *            <cite>SimEventInterface.MIN_PRIORITY</cite>; most normal events should use
169      *            <cite>SimEventInterface.NORMAL_PRIORITY</cite>
170      * @param source Object; the object that creates/schedules the event
171      * @param eventTarget Object; the object that must execute the event
172      * @param method String; the name of the method of <code>target</code> that must execute the event
173      * @param args Object[]; the arguments of the <code>method</code> that must execute the event
174      * @return SimEvent&lt;OTSSimTimeDouble&gt;; the event that was scheduled (the caller should save this if a need to cancel
175      *         the event may arise later)
176      * @throws SimRuntimeException when the <code>executionTime</code> is in the past
177      */
178     private SimEvent<OTSSimTimeDouble> scheduleEvent(final Time.Abs executionTime, final short priority,
179         final Object source, final Object eventTarget, final String method, final Object[] args) throws SimRuntimeException
180     {
181         SimEvent<OTSSimTimeDouble> simEvent =
182             new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new Time.Abs(executionTime.getSI(), SECOND)), priority,
183                 source, eventTarget, method, args);
184         this.simulator.scheduleEvent(simEvent);
185         return simEvent;
186     }
187 
188     /**
189      * Install a handler for the window closed event that stops the simulator (if it is running).
190      */
191     public final void installWindowCloseHandler()
192     {
193         if (this.closeHandlerRegistered)
194         {
195             return;
196         }
197 
198         // make sure the root frame gets disposed of when the closing X icon is pressed.
199         new DisposeOnCloseThread(this).start();
200     }
201 
202     /** Install the dispose on close when the OTSControlPanel is registered as part of a frame. */
203     protected class DisposeOnCloseThread extends Thread
204     {
205         /** the current container. */
206         private OTSControlPanel panel;
207 
208         /**
209          * @param panel the OTSControlpanel container.
210          */
211         public DisposeOnCloseThread(final OTSControlPanel panel)
212         {
213             super();
214             this.panel = panel;
215         }
216 
217         /** {@inheritDoc} */
218         @Override
219         public final void run()
220         {
221             Container root = this.panel;
222             while (!(root instanceof JFrame))
223             {
224                 try
225                 {
226                     Thread.sleep(10);
227                 }
228                 catch (InterruptedException exception)
229                 {
230                     // nothing to do
231                 }
232 
233                 // Search towards the root of the Swing components until we find a JFrame
234                 root = this.panel;
235                 while (null != root.getParent() && !(root instanceof JFrame))
236                 {
237                     root = root.getParent();
238                 }
239             }
240             JFrame frame = (JFrame) root;
241             frame.addWindowListener(this.panel);
242             this.panel.closeHandlerRegistered = true;
243             // frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
244         }
245     }
246 
247     /** {@inheritDoc} */
248     @Override
249     public final void actionPerformed(final ActionEvent actionEvent)
250     {
251         String actionCommand = actionEvent.getActionCommand();
252         // System.out.println("actionCommand: " + actionCommand);
253         try
254         {
255             if (actionCommand.equals("Step"))
256             {
257                 if (getSimulator().isRunning())
258                 {
259                     getSimulator().stop();
260                 }
261                 this.simulator.step();
262             }
263             if (actionCommand.equals("Run"))
264             {
265                 this.simulator.start();
266             }
267             if (actionCommand.equals("NextTime"))
268             {
269                 if (getSimulator().isRunning())
270                 {
271                     getSimulator().stop();
272                 }
273                 double now = getSimulator().getSimulatorTime().getTime().getSI();
274                 // System.out.println("now is " + now);
275                 try
276                 {
277                     this.stopAtEvent =
278                         scheduleEvent(new Time.Abs(now, TimeUnit.SI), SimEventInterface.MIN_PRIORITY, this, this,
279                             "autoPauseSimulator", null);
280                 }
281                 catch (SimRuntimeException exception)
282                 {
283                     this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator", "Caught an exception "
284                         + "while trying to schedule an autoPauseSimulator event at the current simulator time");
285                 }
286                 this.simulator.start();
287             }
288             if (actionCommand.equals("Pause"))
289             {
290                 this.simulator.stop();
291             }
292             if (actionCommand.equals("Reset"))
293             {
294                 if (getSimulator().isRunning())
295                 {
296                     getSimulator().stop();
297                 }
298 
299                 if (null == OTSControlPanel.this.wrappableAnimation)
300                 {
301                     throw new Error("Do not know how to restart this simulation");
302                 }
303 
304                 // find the JFrame position and dimensions
305                 Container root = OTSControlPanel.this;
306                 while (!(root instanceof JFrame))
307                 {
308                     root = root.getParent();
309                 }
310                 JFrame frame = (JFrame) root;
311                 Rectangle rect = frame.getBounds();
312                 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
313                 frame.dispose();
314                 OTSControlPanel.this.cleanup();
315                 try
316                 {
317                     OTSControlPanel.this.wrappableAnimation.rebuildSimulator(rect);
318                 }
319                 catch (Exception exception)
320                 {
321                     exception.printStackTrace();
322                 }
323             }
324             fixButtons();
325         }
326         catch (Exception exception)
327         {
328             exception.printStackTrace();
329         }
330     }
331 
332     /**
333      * clean up timers, contexts, threads, etc. that could prevent garbage collection.
334      */
335     private void cleanup()
336     {
337         if (!this.isCleanUp)
338         {
339             this.isCleanUp = true;
340             try
341             {
342                 if (this.simulator != null)
343                 {
344                     if (this.simulator.isRunning())
345                     {
346                         this.simulator.stop();
347                     }
348 
349                     // unbind the old animation and statistics
350                     getSimulator().getReplication().getExperiment().removeFromContext(); // clean up the context
351                     getSimulator().cleanUp();
352                 }
353 
354                 if (this.clockPanel != null)
355                 {
356                     this.clockPanel.cancelTimer(); // cancel the timer on the clock panel.
357                 }
358 
359                 if (this.wrappableAnimation != null)
360                 {
361                     this.wrappableAnimation.stopTimersThreads(); // stop the linked timers and other threads
362                 }
363             }
364             catch (Throwable exception)
365             {
366                 exception.printStackTrace();
367             }
368         }
369     }
370 
371     /**
372      * Update the enabled state of all the buttons.
373      */
374     protected final void fixButtons()
375     {
376         // System.out.println("FixButtons entered");
377         final boolean moreWorkToDo = getSimulator().getEventList().size() > 0;
378         for (JButton button : this.buttons)
379         {
380             final String actionCommand = button.getActionCommand();
381             if (actionCommand.equals("Step"))
382             {
383                 button.setEnabled(moreWorkToDo);
384             }
385             else if (actionCommand.equals("Run"))
386             {
387                 button.setEnabled(moreWorkToDo && !getSimulator().isRunning());
388             }
389             else if (actionCommand.equals("NextTime"))
390             {
391                 button.setEnabled(moreWorkToDo);
392             }
393             else if (actionCommand.equals("Pause"))
394             {
395                 button.setEnabled(getSimulator().isRunning());
396             }
397             else if (actionCommand.equals("Reset"))
398             {
399                 button.setEnabled(true); // FIXME: should be disabled when the simulator was just reset or initialized
400             }
401             else
402             {
403                 this.logger.logp(Level.SEVERE, "ControlPanel", "fixButtons", "", new Exception("Unknown button?"));
404             }
405         }
406         // System.out.println("FixButtons finishing");
407     }
408 
409     /**
410      * Pause the simulator.
411      */
412     public final void autoPauseSimulator()
413     {
414         if (getSimulator().isRunning())
415         {
416             getSimulator().stop();
417             double currentTick = getSimulator().getSimulatorTime().getTime().getSI();
418             double nextTick = getSimulator().getEventList().first().getAbsoluteExecutionTime().get().getSI();
419             // System.out.println("currentTick is " + currentTick);
420             // System.out.println("nextTick is " + nextTick);
421             if (nextTick > currentTick)
422             {
423                 // The clock is now just beyond where it was when the user requested the NextTime operation
424                 // Insert another autoPauseSimulator event just before what is now the time of the next event
425                 // and let the simulator time increment to that time
426                 // System.out.println("Re-Scheduling at " + nextTick);
427                 try
428                 {
429                     this.stopAtEvent =
430                         scheduleEvent(new Time.Abs(nextTick, TimeUnit.SI), SimEventInterface.MAX_PRIORITY, this, this,
431                             "autoPauseSimulator", null);
432                     getSimulator().start();
433                 }
434                 catch (SimRuntimeException exception)
435                 {
436                     this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator",
437                         "Caught an exception while trying to re-schedule an autoPauseEvent at the next real event");
438                 }
439             }
440             else
441             {
442                 // System.out.println("Not re-scheduling");
443                 if (SwingUtilities.isEventDispatchThread())
444                 {
445                     // System.out.println("Already on EventDispatchThread");
446                     fixButtons();
447                 }
448                 else
449                 {
450                     try
451                     {
452                         // System.out.println("Current thread is NOT EventDispatchThread: " + Thread.currentThread());
453                         SwingUtilities.invokeAndWait(new Runnable()
454                         {
455                             @Override
456                             public void run()
457                             {
458                                 // System.out.println("Runnable started");
459                                 fixButtons();
460                                 // System.out.println("Runnable finishing");
461                             }
462                         });
463                     }
464                     catch (Exception e)
465                     {
466                         if (e instanceof InterruptedException)
467                         {
468                             System.out.println("Caught " + e);
469                             // e.printStackTrace();
470                         }
471                         else
472                         {
473                             e.printStackTrace();
474                         }
475                     }
476                 }
477             }
478         }
479     }
480 
481     /** {@inheritDoc} */
482     @Override
483     public final void propertyChange(final PropertyChangeEvent evt)
484     {
485         // System.out.println("PropertyChanged: " + evt);
486         if (null != this.stopAtEvent)
487         {
488             getSimulator().cancelEvent(this.stopAtEvent); // silently ignore false result
489             this.stopAtEvent = null;
490         }
491         String newValue = (String) evt.getNewValue();
492         String[] fields = newValue.split("[:\\.]");
493         int hours = Integer.parseInt(fields[0]);
494         int minutes = Integer.parseInt(fields[1]);
495         int seconds = Integer.parseInt(fields[2]);
496         int fraction = Integer.parseInt(fields[3]);
497         double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
498         if (stopTime < getSimulator().getSimulatorTime().getTime().getSI())
499         {
500             return;
501         }
502         else
503         {
504             try
505             {
506                 this.stopAtEvent =
507                     scheduleEvent(new Time.Abs(stopTime, SECOND), SimEventInterface.MAX_PRIORITY, this, this,
508                         "autoPauseSimulator", null);
509             }
510             catch (SimRuntimeException exception)
511             {
512                 this.logger.logp(Level.SEVERE, "ControlPanel", "propertyChange",
513                     "Caught an exception while trying to schedule an autoPauseSimulator event");
514             }
515         }
516 
517     }
518 
519     /**
520      * @return simulator.
521      */
522     @SuppressWarnings("unchecked")
523     public final DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble> getSimulator()
524     {
525         return (DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble>) this.simulator;
526     }
527 
528     /** {@inheritDoc} */
529     @Override
530     public void windowOpened(final WindowEvent e)
531     {
532         // No action
533     }
534 
535     /** {@inheritDoc} */
536     @Override
537     public final void windowClosing(final WindowEvent e)
538     {
539         if (this.simulator != null)
540         {
541             try
542             {
543                 if (this.simulator.isRunning())
544                 {
545                     this.simulator.stop();
546                 }
547             }
548             catch (SimRuntimeException exception)
549             {
550                 exception.printStackTrace();
551             }
552         }
553     }
554 
555     /** {@inheritDoc} */
556     @Override
557     public final void windowClosed(final WindowEvent e)
558     {
559         cleanup();
560     }
561 
562     /** {@inheritDoc} */
563     @Override
564     public final void windowIconified(final WindowEvent e)
565     {
566         // No action
567     }
568 
569     /** {@inheritDoc} */
570     @Override
571     public final void windowDeiconified(final WindowEvent e)
572     {
573         // No action
574     }
575 
576     /** {@inheritDoc} */
577     @Override
578     public final void windowActivated(final WindowEvent e)
579     {
580         // No action
581     }
582 
583     /** {@inheritDoc} */
584     @Override
585     public final void windowDeactivated(final WindowEvent e)
586     {
587         // No action
588     }
589 
590     /**
591      * @return timeFont.
592      */
593     public final Font getTimeFont()
594     {
595         return this.timeFont;
596     }
597 
598     /** JPanel that contains a JSider that uses a logarithmic scale. */
599     static class TimeWarpPanel extends JPanel
600     {
601         /** */
602         private static final long serialVersionUID = 20150408L;
603 
604         /** The JSlider that the user sees. */
605         private final JSlider slider;
606 
607         /** The ratios used in each decade. */
608         private final int[] ratios;
609 
610         /** The values at each tick. */
611         private Map<Integer, Double> tickValues = new HashMap<>();
612 
613         /**
614          * Construct a new TimeWarpPanel.
615          * @param minimum double; the minimum value on the scale (the displayed scale may extend a little further than this
616          *            value)
617          * @param maximum double; the maximum value on the scale (the displayed scale may extend a little further than this
618          *            value)
619          * @param initialValue double; the initially selected value on the scale
620          * @param ticksPerDecade int; the number of steps per decade
621          * @param simulator SimpleSimulator; the simulator to change the speed of
622          */
623         public TimeWarpPanel(final double minimum, final double maximum, final double initialValue,
624             final int ticksPerDecade, final DEVSSimulatorInterface<?, ?, ?> simulator)
625         {
626             if (minimum <= 0 || minimum > initialValue || initialValue > maximum)
627             {
628                 throw new Error("Bad (combination of) minimum, maximum and initialValue; "
629                     + "(restrictions: 0 < minimum <= initialValue <= maximum)");
630             }
631             switch (ticksPerDecade)
632             {
633                 case 1:
634                     this.ratios = new int[]{1};
635                     break;
636                 case 2:
637                     this.ratios = new int[]{1, 3};
638                     break;
639                 case 3:
640                     this.ratios = new int[]{1, 2, 5};
641                     break;
642                 default:
643                     throw new Error("Bad ticksPerDecade value (must be 1, 2 or 3)");
644             }
645             int minimumTick = (int) Math.floor(Math.log10(minimum / initialValue) * ticksPerDecade);
646             int maximumTick = (int) Math.ceil(Math.log10(maximum / initialValue) * ticksPerDecade);
647             this.slider = new JSlider(SwingConstants.HORIZONTAL, minimumTick, maximumTick + 1, 0);
648             this.slider.setPreferredSize(new Dimension(350, 45));
649             Hashtable<Integer, JLabel> labels = new Hashtable<Integer, JLabel>();
650             for (int step = 0; step <= maximumTick; step++)
651             {
652                 StringBuilder text = new StringBuilder();
653                 text.append(this.ratios[step % this.ratios.length]);
654                 for (int decade = 0; decade < step / this.ratios.length; decade++)
655                 {
656                     text.append("0");
657                 }
658                 this.tickValues.put(step, Double.parseDouble(text.toString()));
659                 labels.put(step, new JLabel(text.toString().replace("000", "K")));
660                 // System.out.println("Label " + step + " is \"" + text.toString() + "\"");
661             }
662             // Figure out the DecimalSymbol
663             String decimalSeparator =
664                 "" + ((DecimalFormat) NumberFormat.getInstance()).getDecimalFormatSymbols().getDecimalSeparator();
665             for (int step = -1; step >= minimumTick; step--)
666             {
667                 StringBuilder text = new StringBuilder();
668                 text.append("0");
669                 text.append(decimalSeparator);
670                 for (int decade = (step + 1) / this.ratios.length; decade < 0; decade++)
671                 {
672                     text.append("0");
673                 }
674                 int index = step % this.ratios.length;
675                 if (index < 0)
676                 {
677                     index += this.ratios.length;
678                 }
679                 text.append(this.ratios[index]);
680                 labels.put(step, new JLabel(text.toString()));
681                 this.tickValues.put(step, Double.parseDouble(text.toString()));
682                 // System.out.println("Label " + step + " is \"" + text.toString() + "\"");
683             }
684             labels.put(maximumTick + 1, new JLabel("\u221E"));
685             this.tickValues.put(maximumTick + 1, 1E9);
686             this.slider.setLabelTable(labels);
687             this.slider.setPaintLabels(true);
688             this.slider.setPaintTicks(true);
689             this.slider.setMajorTickSpacing(1);
690             this.add(this.slider);
691             /*- Uncomment to verify the stepToFactor method.
692             for (int i = this.slider.getMinimum(); i <= this.slider.getMaximum(); i++)
693             {
694                 System.out.println("pos=" + i + " value is " + stepToFactor(i));
695             }
696              */
697 
698             // initial value of simulation speed
699             if (simulator instanceof DEVSRealTimeClock)
700             {
701                 DEVSRealTimeClock<?, ?, ?> clock = (DEVSRealTimeClock<?, ?, ?>) simulator;
702                 clock.setSpeedFactor(TimeWarpPanel.this.tickValues.get(this.slider.getValue()));
703             }
704 
705             // adjust the simulation speed
706             this.slider.addChangeListener(new ChangeListener()
707             {
708                 public void stateChanged(final ChangeEvent ce)
709                 {
710                     JSlider source = (JSlider) ce.getSource();
711                     if (!source.getValueIsAdjusting() && simulator instanceof DEVSRealTimeClock)
712                     {
713                         DEVSRealTimeClock<?, ?, ?> clock = (DEVSRealTimeClock<?, ?, ?>) simulator;
714                         clock.setSpeedFactor(((TimeWarpPanel) source.getParent()).getTickValues().get(source.getValue()));
715                     }
716                 }
717             });
718         }
719 
720         /**
721          * Access to tickValues map from within the event handler.
722          * @return Map&lt;Integer, Double&gt; the tickValues map of this TimeWarpPanel
723          */
724         protected Map<Integer, Double> getTickValues()
725         {
726             return this.tickValues;
727         }
728 
729         /**
730          * Convert a position on the slider to a factor.
731          * @param step int; the position on the slider
732          * @return double; the factor that corresponds to step
733          */
734         private double stepToFactor(final int step)
735         {
736             int index = step % this.ratios.length;
737             if (index < 0)
738             {
739                 index += this.ratios.length;
740             }
741             double result = this.ratios[index];
742             // Make positive to avoid trouble with negative values that round towards 0 on division
743             int power = (step + 1000 * this.ratios.length) / this.ratios.length - 1000; // This is ugly
744             while (power > 0)
745             {
746                 result *= 10;
747                 power--;
748             }
749             while (power < 0)
750             {
751                 result /= 10;
752                 power++;
753             }
754             return result;
755         }
756 
757         /**
758          * Retrieve the current TimeWarp factor.
759          * @return double; the current TimeWarp factor
760          */
761         public final double getFactor()
762         {
763             return stepToFactor(this.slider.getValue());
764         }
765     }
766 
767     /** JLabel that displays the simulation time. */
768     class ClockPanel extends JLabel
769     {
770         /** */
771         private static final long serialVersionUID = 20141211L;
772 
773         /** The JLabel that displays the time. */
774         private final JLabel clockLabel;
775 
776         /** the timer so we can cancel it. */
777         private Timer timer;
778 
779         /** timer update in msec. */
780         private static final long UPDATEINTERVAL = 1000;
781 
782         /** Construct a clock panel. */
783         ClockPanel()
784         {
785             super("00:00:00.000");
786             this.clockLabel = this;
787             this.setFont(getTimeFont());
788             this.timer = new Timer();
789             this.timer.scheduleAtFixedRate(new TimeUpdateTask(), 0, ClockPanel.UPDATEINTERVAL);
790         }
791 
792         /**
793          * Cancel the timer task.
794          */
795         public void cancelTimer()
796         {
797             if (this.timer != null)
798             {
799                 this.timer.cancel();
800             }
801             this.timer = null;
802         }
803 
804         /** Updater for the clock panel. */
805         private class TimeUpdateTask extends TimerTask
806         {
807             /**
808              * Create a TimeUpdateTask.
809              */
810             public TimeUpdateTask()
811             {
812             }
813 
814             /** {@inheritDoc} */
815             @Override
816             public void run()
817             {
818                 double now = Math.round(getSimulator().getSimulatorTime().getTime().getSI() * 1000) / 1000d;
819                 int seconds = (int) Math.floor(now);
820                 int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
821                 getClockLabel().setText(
822                     String.format("  %02d:%02d:%02d.%03d  ", seconds / 3600, seconds / 60 % 60, seconds % 60,
823                         fractionalSeconds));
824                 getClockLabel().repaint();
825             }
826         }
827 
828         /**
829          * @return clockLabel.
830          */
831         protected JLabel getClockLabel()
832         {
833             return this.clockLabel;
834         }
835 
836     }
837 
838     /** Entry field for time. */
839     class TimeEdit extends JFormattedTextField
840     {
841         /** */
842         private static final long serialVersionUID = 20141212L;
843 
844         /**
845          * Construct a new TimeEdit.
846          * @param initialValue DoubleScalar.Abs&lt;TimeUnit&gt;; the initial value for the TimeEdit
847          */
848         TimeEdit(final Time.Abs initialValue)
849         {
850             super(new RegexFormatter("\\d\\d\\d\\d:[0-5]\\d:[0-5]\\d\\.\\d\\d\\d"));
851             MaskFormatter mf = null;
852             try
853             {
854                 mf = new MaskFormatter("####:##:##.###");
855                 mf.setPlaceholderCharacter('0');
856                 mf.setAllowsInvalid(false);
857                 mf.setCommitsOnValidEdit(true);
858                 mf.setOverwriteMode(true);
859                 mf.install(this);
860             }
861             catch (ParseException exception)
862             {
863                 exception.printStackTrace();
864             }
865             setTime(initialValue);
866             setFont(getTimeFont());
867         }
868 
869         /**
870          * Set or update the time shown in this TimeEdit.
871          * @param newValue DoubleScalar.Abs&lt;TimeUnit&gt;; the (new) value to set/show in this TimeEdit
872          */
873         public void setTime(final Time.Abs newValue)
874         {
875             double v = newValue.getSI();
876             int integerPart = (int) Math.floor(v);
877             int fraction = (int) Math.floor((v - integerPart) * 1000);
878             String text =
879                 String.format("%04d:%02d:%02d.%03d", integerPart / 3600, integerPart / 60 % 60, integerPart % 60, fraction);
880             this.setText(text);
881         }
882     }
883 
884     /**
885      * Extension of a DefaultFormatter that uses a regular expression. <br>
886      * Derived from <a href="http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm">
887      * http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm</a>;
888      * <p>
889      * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
890      * initial version 2 dec. 2014 <br>
891      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
892      */
893     static class RegexFormatter extends DefaultFormatter
894     {
895         /** */
896         private static final long serialVersionUID = 20141212L;
897 
898         /** The regular expression pattern. */
899         private Pattern pattern;
900 
901         /**
902          * Create a new RegexFormatter.
903          * @param pattern String; regular expression pattern that defines what this RexexFormatter will accept
904          */
905         public RegexFormatter(final String pattern)
906         {
907             this.pattern = Pattern.compile(pattern);
908         }
909 
910         @Override
911         public Object stringToValue(final String text) throws ParseException
912         {
913             Matcher matcher = this.pattern.matcher(text);
914             if (matcher.matches())
915             {
916                 // System.out.println("String \"" + text + "\" matches");
917                 return super.stringToValue(text);
918             }
919             // System.out.println("String \"" + text + "\" does not match");
920             throw new ParseException("Pattern did not match", 0);
921         }
922     }
923 
924 }