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