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