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