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