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.WrappableSimulation;
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-03 13:38:01 +0200 (Thu, 03 Sep 2015) $, @version $Revision: 1378 $, 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 WrappableSimulation (needed for restart operation). */
80      private final WrappableSimulation wrappableSimulation;
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 wrappableSimulation WrappableSimulation; if non-null, the restart button should work
113      */
114     public OTSControlPanel(final OTSDEVSSimulatorInterface simulator, final WrappableSimulation wrappableSimulation)
115     {
116         this.simulator = simulator;
117         this.wrappableSimulation = wrappableSimulation;
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      * @throws RemoteException on communications failure
178      */
179     private SimEvent<OTSSimTimeDouble> scheduleEvent(final Time.Abs executionTime, final short priority,
180         final Object source, final Object eventTarget, final String method, final Object[] args) throws SimRuntimeException,
181         RemoteException
182     {
183         SimEvent<OTSSimTimeDouble> simEvent =
184             new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new Time.Abs(executionTime.getSI(), SECOND)), priority,
185                 source, eventTarget, method, args);
186         this.simulator.scheduleEvent(simEvent);
187         return simEvent;
188     }
189 
190     /**
191      * Install a handler for the window closed event that stops the simulator (if it is running).
192      */
193     public final void installWindowCloseHandler()
194     {
195         if (this.closeHandlerRegistered)
196         {
197             return;
198         }
199 
200         // make sure the root frame gets disposed of when the closing X icon is pressed.
201         new DisposeOnCloseThread(this).start();
202     }
203 
204     /** Install the dispose on close when the OTSControlPanel is registered as part of a frame. */
205     protected class DisposeOnCloseThread extends Thread
206     {
207         /** the current container. */
208         private OTSControlPanel panel;
209 
210         /**
211          * @param panel the OTSControlpanel container.
212          */
213         public DisposeOnCloseThread(final OTSControlPanel panel)
214         {
215             super();
216             this.panel = panel;
217         }
218 
219         /** {@inheritDoc} */
220         @Override
221         public final void run()
222         {
223             Container root = this.panel;
224             while (!(root instanceof JFrame))
225             {
226                 try
227                 {
228                     Thread.sleep(10);
229                 }
230                 catch (InterruptedException exception)
231                 {
232                     // nothing to do
233                 }
234 
235                 // Search towards the root of the Swing components until we find a JFrame
236                 root = this.panel;
237                 while (null != root.getParent() && !(root instanceof JFrame))
238                 {
239                     root = root.getParent();
240                 }
241             }
242             JFrame frame = (JFrame) root;
243             frame.addWindowListener(this.panel);
244             this.panel.closeHandlerRegistered = true;
245             // frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
246         }
247     }
248 
249     /** {@inheritDoc} */
250     @Override
251     public final void actionPerformed(final ActionEvent actionEvent)
252     {
253         String actionCommand = actionEvent.getActionCommand();
254         // System.out.println("actionCommand: " + actionCommand);
255         try
256         {
257             if (actionCommand.equals("Step"))
258             {
259                 if (getSimulator().isRunning())
260                 {
261                     getSimulator().stop();
262                 }
263                 this.simulator.step();
264             }
265             if (actionCommand.equals("Run"))
266             {
267                 this.simulator.start();
268             }
269             if (actionCommand.equals("NextTime"))
270             {
271                 if (getSimulator().isRunning())
272                 {
273                     getSimulator().stop();
274                 }
275                 double now = getSimulator().getSimulatorTime().getTime().getSI();
276                 // System.out.println("now is " + now);
277                 try
278                 {
279                     this.stopAtEvent =
280                         scheduleEvent(new Time.Abs(now, TimeUnit.SI), SimEventInterface.MIN_PRIORITY, this, this,
281                             "autoPauseSimulator", null);
282                 }
283                 catch (SimRuntimeException exception)
284                 {
285                     this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator", "Caught an exception "
286                         + "while trying to schedule an autoPauseSimulator event at the current simulator time");
287                 }
288                 this.simulator.start();
289             }
290             if (actionCommand.equals("Pause"))
291             {
292                 this.simulator.stop();
293             }
294             if (actionCommand.equals("Reset"))
295             {
296                 if (getSimulator().isRunning())
297                 {
298                     getSimulator().stop();
299                 }
300 
301                 if (null == OTSControlPanel.this.wrappableSimulation)
302                 {
303                     throw new Error("Do not know how to restart this simulation");
304                 }
305 
306                 // find the JFrame position and dimensions
307                 Container root = OTSControlPanel.this;
308                 while (!(root instanceof JFrame))
309                 {
310                     root = root.getParent();
311                 }
312                 JFrame frame = (JFrame) root;
313                 Rectangle rect = frame.getBounds();
314                 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
315                 frame.dispose();
316                 OTSControlPanel.this.cleanup();
317                 try
318                 {
319                     OTSControlPanel.this.wrappableSimulation.rebuildSimulator(rect);
320                 }
321                 catch (Exception exception)
322                 {
323                     exception.printStackTrace();
324                 }
325             }
326             fixButtons();
327         }
328         catch (Exception exception)
329         {
330             exception.printStackTrace();
331         }
332     }
333 
334     /**
335      * clean up timers, contexts, threads, etc. that could prevent garbage collection.
336      */
337     private void cleanup()
338     {
339         if (!this.isCleanUp)
340         {
341             this.isCleanUp = true;
342             try
343             {
344                 if (this.simulator != null)
345                 {
346                     if (this.simulator.isRunning())
347                     {
348                         this.simulator.stop();
349                     }
350 
351                     // unbind the old animation and statistics
352                     getSimulator().getReplication().getExperiment().removeFromContext(); // clean up the context
353                     getSimulator().cleanUp();
354                 }
355 
356                 if (this.clockPanel != null)
357                 {
358                     this.clockPanel.cancelTimer(); // cancel the timer on the clock panel.
359                 }
360 
361                 if (this.wrappableSimulation != null)
362                 {
363                     this.wrappableSimulation.stopTimersThreads(); // stop the linked timers and other threads
364                 }
365             }
366             catch (Throwable exception)
367             {
368                 exception.printStackTrace();
369             }
370         }
371     }
372 
373     /**
374      * Update the enabled state of all the buttons.
375      */
376     protected final void fixButtons()
377     {
378         // System.out.println("FixButtons entered");
379         final boolean moreWorkToDo = getSimulator().getEventList().size() > 0;
380         for (JButton button : this.buttons)
381         {
382             final String actionCommand = button.getActionCommand();
383             if (actionCommand.equals("Step"))
384             {
385                 button.setEnabled(moreWorkToDo);
386             }
387             else if (actionCommand.equals("Run"))
388             {
389                 button.setEnabled(moreWorkToDo && !getSimulator().isRunning());
390             }
391             else if (actionCommand.equals("NextTime"))
392             {
393                 button.setEnabled(moreWorkToDo);
394             }
395             else if (actionCommand.equals("Pause"))
396             {
397                 button.setEnabled(getSimulator().isRunning());
398             }
399             else if (actionCommand.equals("Reset"))
400             {
401                 button.setEnabled(true); // FIXME: should be disabled when the simulator was just reset or initialized
402             }
403             else
404             {
405                 this.logger.logp(Level.SEVERE, "ControlPanel", "fixButtons", "", new Exception("Unknown button?"));
406             }
407         }
408         // System.out.println("FixButtons finishing");
409     }
410 
411     /**
412      * Pause the simulator.
413      */
414     public final void autoPauseSimulator()
415     {
416         if (getSimulator().isRunning())
417         {
418             getSimulator().stop();
419             double currentTick = getSimulator().getSimulatorTime().getTime().getSI();
420             double nextTick = getSimulator().getEventList().first().getAbsoluteExecutionTime().get().getSI();
421             // System.out.println("currentTick is " + currentTick);
422             // System.out.println("nextTick is " + nextTick);
423             if (nextTick > currentTick)
424             {
425                 // The clock is now just beyond where it was when the user requested the NextTime operation
426                 // Insert another autoPauseSimulator event just before what is now the time of the next event
427                 // and let the simulator time increment to that time
428                 // System.out.println("Re-Scheduling at " + nextTick);
429                 try
430                 {
431                     this.stopAtEvent =
432                         scheduleEvent(new Time.Abs(nextTick, TimeUnit.SI), SimEventInterface.MAX_PRIORITY, this, this,
433                             "autoPauseSimulator", null);
434                     getSimulator().start();
435                 }
436                 catch (SimRuntimeException | RemoteException exception)
437                 {
438                     this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator",
439                         "Caught an exception while trying to re-schedule an autoPauseEvent at the next real event");
440                 }
441             }
442             else
443             {
444                 // System.out.println("Not re-scheduling");
445                 if (SwingUtilities.isEventDispatchThread())
446                 {
447                     // System.out.println("Already on EventDispatchThread");
448                     fixButtons();
449                 }
450                 else
451                 {
452                     try
453                     {
454                         // System.out.println("Current thread is NOT EventDispatchThread: " + Thread.currentThread());
455                         SwingUtilities.invokeAndWait(new Runnable()
456                         {
457                             @Override
458                             public void run()
459                             {
460                                 // System.out.println("Runnable started");
461                                 fixButtons();
462                                 // System.out.println("Runnable finishing");
463                             }
464                         });
465                     }
466                     catch (Exception e)
467                     {
468                         if (e instanceof InterruptedException)
469                         {
470                             System.out.println("Caught " + e);
471                             // e.printStackTrace();
472                         }
473                         else
474                         {
475                             e.printStackTrace();
476                         }
477                     }
478                 }
479             }
480         }
481     }
482 
483     /** {@inheritDoc} */
484     @Override
485     public final void propertyChange(final PropertyChangeEvent evt)
486     {
487         // System.out.println("PropertyChanged: " + evt);
488         if (null != this.stopAtEvent)
489         {
490             getSimulator().cancelEvent(this.stopAtEvent); // silently ignore false result
491             this.stopAtEvent = null;
492         }
493         String newValue = (String) evt.getNewValue();
494         String[] fields = newValue.split("[:\\.]");
495         int hours = Integer.parseInt(fields[0]);
496         int minutes = Integer.parseInt(fields[1]);
497         int seconds = Integer.parseInt(fields[2]);
498         int fraction = Integer.parseInt(fields[3]);
499         double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
500         if (stopTime < getSimulator().getSimulatorTime().getTime().getSI())
501         {
502             return;
503         }
504         else
505         {
506             try
507             {
508                 this.stopAtEvent =
509                     scheduleEvent(new Time.Abs(stopTime, SECOND), SimEventInterface.MAX_PRIORITY, this, this,
510                         "autoPauseSimulator", null);
511             }
512             catch (SimRuntimeException | RemoteException exception)
513             {
514                 this.logger.logp(Level.SEVERE, "ControlPanel", "propertyChange",
515                     "Caught an exception while trying to schedule an autoPauseSimulator event");
516             }
517         }
518 
519     }
520 
521     /**
522      * @return simulator.
523      */
524     @SuppressWarnings("unchecked")
525     public final DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble> getSimulator()
526     {
527         return (DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble>) this.simulator;
528     }
529 
530     /** {@inheritDoc} */
531     @Override
532     public void windowOpened(final WindowEvent e)
533     {
534         // No action
535     }
536 
537     /** {@inheritDoc} */
538     @Override
539     public final void windowClosing(final WindowEvent e)
540     {
541         if (this.simulator != null)
542         {
543             try
544             {
545                 if (this.simulator.isRunning())
546                 {
547                     this.simulator.stop();
548                 }
549             }
550             catch (RemoteException | SimRuntimeException exception)
551             {
552                 exception.printStackTrace();
553             }
554         }
555     }
556 
557     /** {@inheritDoc} */
558     @Override
559     public final void windowClosed(final WindowEvent e)
560     {
561         cleanup();
562     }
563 
564     /** {@inheritDoc} */
565     @Override
566     public final void windowIconified(final WindowEvent e)
567     {
568         // No action
569     }
570 
571     /** {@inheritDoc} */
572     @Override
573     public final void windowDeiconified(final WindowEvent e)
574     {
575         // No action
576     }
577 
578     /** {@inheritDoc} */
579     @Override
580     public final void windowActivated(final WindowEvent e)
581     {
582         // No action
583     }
584 
585     /** {@inheritDoc} */
586     @Override
587     public final void windowDeactivated(final WindowEvent e)
588     {
589         // No action
590     }
591 
592     /**
593      * @return timeFont.
594      */
595     public final Font getTimeFont()
596     {
597         return this.timeFont;
598     }
599 
600     /** JPanel that contains a JSider that uses a logarithmic scale. */
601     static class TimeWarpPanel extends JPanel
602     {
603         /** */
604         private static final long serialVersionUID = 20150408L;
605 
606         /** The JSlider that the user sees. */
607         private final JSlider slider;
608 
609         /** The ratios used in each decade. */
610         private final int[] ratios;
611 
612         /** The values at each tick. */
613         private Map<Integer, Double> tickValues = new HashMap<>();
614 
615         /**
616          * Construct a new TimeWarpPanel.
617          * @param minimum double; the minimum value on the scale (the displayed scale may extend a little further than this
618          *            value)
619          * @param maximum double; the maximum value on the scale (the displayed scale may extend a little further than this
620          *            value)
621          * @param initialValue double; the initially selected value on the scale
622          * @param ticksPerDecade int; the number of steps per decade
623          * @param simulator SimpleSimulator; the simulator to change the speed of
624          */
625         public TimeWarpPanel(final double minimum, final double maximum, final double initialValue,
626             final int ticksPerDecade, final DEVSSimulatorInterface<?, ?, ?> simulator)
627         {
628             if (minimum <= 0 || minimum > initialValue || initialValue > maximum)
629             {
630                 throw new Error("Bad (combination of) minimum, maximum and initialValue; "
631                     + "(restrictions: 0 < minimum <= initialValue <= maximum)");
632             }
633             switch (ticksPerDecade)
634             {
635                 case 1:
636                     this.ratios = new int[]{1};
637                     break;
638                 case 2:
639                     this.ratios = new int[]{1, 3};
640                     break;
641                 case 3:
642                     this.ratios = new int[]{1, 2, 5};
643                     break;
644                 default:
645                     throw new Error("Bad ticksPerDecade value (must be 1, 2 or 3)");
646             }
647             int minimumTick = (int) Math.floor(Math.log10(minimum / initialValue) * ticksPerDecade);
648             int maximumTick = (int) Math.ceil(Math.log10(maximum / initialValue) * ticksPerDecade);
649             this.slider = new JSlider(SwingConstants.HORIZONTAL, minimumTick, maximumTick, 0);
650             this.slider.setPreferredSize(new Dimension(300, 45));
651             Hashtable<Integer, JLabel> labels = new Hashtable<Integer, JLabel>();
652             for (int step = 0; step <= maximumTick; step++)
653             {
654                 StringBuilder text = new StringBuilder();
655                 text.append(this.ratios[step % this.ratios.length]);
656                 for (int decade = 0; decade < step / this.ratios.length; decade++)
657                 {
658                     text.append("0");
659                 }
660                 this.tickValues.put(step, Double.parseDouble(text.toString()));
661                 labels.put(step, new JLabel(text.toString().replace("000", "K")));
662                 // System.out.println("Label " + step + " is \"" + text.toString() + "\"");
663             }
664             // Figure out the DecimalSymbol
665             String decimalSeparator =
666                 "" + ((DecimalFormat) NumberFormat.getInstance()).getDecimalFormatSymbols().getDecimalSeparator();
667             for (int step = -1; step >= minimumTick; step--)
668             {
669                 StringBuilder text = new StringBuilder();
670                 text.append("0");
671                 text.append(decimalSeparator);
672                 for (int decade = (step + 1) / this.ratios.length; decade < 0; decade++)
673                 {
674                     text.append("0");
675                 }
676                 int index = step % this.ratios.length;
677                 if (index < 0)
678                 {
679                     index += this.ratios.length;
680                 }
681                 text.append(this.ratios[index]);
682                 labels.put(step, new JLabel(text.toString()));
683                 this.tickValues.put(step, Double.parseDouble(text.toString()));
684                 // System.out.println("Label " + step + " is \"" + text.toString() + "\"");
685             }
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-03 13:38:01 +0200 (Thu, 03 Sep 2015) $, @version $Revision: 1378 $, 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 }