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