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