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