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