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