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