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