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