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