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