View Javadoc
1   package org.opentrafficsim.swing.gui;
2   
3   import java.awt.Color;
4   import java.awt.Dimension;
5   import java.awt.Font;
6   import java.awt.event.ActionEvent;
7   import java.awt.event.ActionListener;
8   import java.awt.event.FocusAdapter;
9   import java.awt.event.FocusEvent;
10  import java.awt.event.KeyAdapter;
11  import java.awt.event.KeyEvent;
12  import java.awt.event.MouseAdapter;
13  import java.awt.event.MouseEvent;
14  import java.awt.event.WindowAdapter;
15  import java.awt.event.WindowEvent;
16  import java.beans.PropertyChangeEvent;
17  import java.beans.PropertyChangeListener;
18  import java.rmi.RemoteException;
19  import java.text.DecimalFormat;
20  import java.text.NumberFormat;
21  import java.text.ParseException;
22  import java.util.ArrayList;
23  import java.util.Hashtable;
24  import java.util.LinkedHashMap;
25  import java.util.Map;
26  import java.util.OptionalInt;
27  import java.util.Timer;
28  import java.util.TimerTask;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  import javax.swing.Box;
33  import javax.swing.BoxLayout;
34  import javax.swing.Icon;
35  import javax.swing.JButton;
36  import javax.swing.JFormattedTextField;
37  import javax.swing.JFrame;
38  import javax.swing.JLabel;
39  import javax.swing.JPanel;
40  import javax.swing.JSlider;
41  import javax.swing.SwingConstants;
42  import javax.swing.SwingUtilities;
43  import javax.swing.event.ChangeEvent;
44  import javax.swing.event.ChangeListener;
45  import javax.swing.text.DefaultFormatter;
46  
47  import org.djunits.unit.TimeUnit;
48  import org.djunits.value.vdouble.scalar.Duration;
49  import org.djunits.value.vdouble.scalar.Time;
50  import org.djutils.event.Event;
51  import org.djutils.event.EventListener;
52  import org.djutils.exceptions.Throw;
53  import org.opentrafficsim.animation.data.util.IconUtil;
54  import org.opentrafficsim.base.OtsRuntimeException;
55  import org.opentrafficsim.base.logger.Logger;
56  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
57  
58  import nl.tudelft.simulation.dsol.SimRuntimeException;
59  import nl.tudelft.simulation.dsol.experiment.Replication;
60  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.Executable;
61  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
62  import nl.tudelft.simulation.dsol.simulators.DevsRealTimeAnimator;
63  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
64  
65  /**
66   * Peter's and Wouter's improved simulation control panel.
67   * <p>
68   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
69   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
70   * </p>
71   * @author Alexander Verbraeck
72   * @author Peter Knoppers
73   * @author Wouter Schakel
74   */
75  public class OtsSimulationControlPanel extends JPanel implements ActionListener, PropertyChangeListener, EventListener
76  {
77  
78      /** Serialization version UID. */
79      private static final long serialVersionUID = 20150617L;
80  
81      /** Pause icon. */
82      private static final Icon PAUSE_ICON = IconUtil.of("Pause24.png").get();
83  
84      /** Play icon. */
85      private static final Icon PLAY_ICON = IconUtil.of("Play24.png").get();
86  
87      /** Simulator. */
88      private final OtsSimulatorInterface simulator;
89  
90      /** Control buttons. */
91      private final ArrayList<JButton> buttons = new ArrayList<>();
92  
93      /** Decimal separator. */
94      private final String decimalSeparator;
95  
96      /** Slider for simulation speed. */
97      private final JSlider speedSlider;
98  
99      /** Ratios used in each decade of the speed slider. */
100     private final int[] simulationSpeedRatios;
101 
102     /** Tick values of the speed slider. */
103     private final Map<Integer, Double> tickValues = new LinkedHashMap<>();
104 
105     /** Font used to display the clock and the stop time. */
106     private final Font timeFont = new Font("SansSerif", Font.BOLD, 18);
107 
108     /** Clock label. */
109     private final ClockLabel clockLabel;
110 
111     /** Simulate until edit field. */
112     private final TimeEdit timeEdit;
113 
114     /** Current stop event to simulate until. */
115     private SimEventInterface<Duration> stopAtEvent = null;
116 
117     /** Overruled buttons enabled state (for when simulation is controlled otherwise). */
118     private boolean buttonsEnabled = false;
119 
120     /** Has cleanup taken place? */
121     private boolean isCleanUp = false;
122 
123     /**
124      * Decorate a SimpleSimulator with a different set of control buttons.
125      * @param simulator the simulator
126      * @param otsAnimationPanel the OTS animation panel
127      * @throws RemoteException when simulator cannot be accessed for listener attachment
128      */
129     public OtsSimulationControlPanel(final OtsSimulatorInterface simulator, final OtsSimulationPanel otsAnimationPanel)
130             throws RemoteException
131     {
132         this.simulator = simulator;
133         this.decimalSeparator =
134                 "" + ((DecimalFormat) NumberFormat.getInstance()).getDecimalFormatSymbols().getDecimalSeparator();
135 
136         setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
137 
138         // buttons
139         add(makeButton("stepButton", "Next24.png", "Step", "Execute one event", true));
140         add(makeButton("nextTimeButton", "Step24.png", "NextTime", "Execute all events scheduled for the current time", true));
141         add(makeButton("runPauseButton", "Play24.png", "RunPause", "XXX", true));
142         add(Box.createHorizontalStrut(5));
143 
144         // simulation speed slider
145         this.simulationSpeedRatios = new int[] {1, 2, 5};
146         this.speedSlider = setupSlider(0.1, 1000, 1, simulator);
147         add(this.speedSlider);
148         add(Box.createHorizontalStrut(5));
149 
150         /** Label with appearance control. */
151         class AppearanceControlLabel extends JLabel implements AppearanceControl
152         {
153             /** Serialization version UID. */
154             private static final long serialVersionUID = 20180207L;
155 
156             @Override
157             public boolean isForeground()
158             {
159                 return true;
160             }
161 
162             @Override
163             public boolean isBackground()
164             {
165                 return true;
166             }
167 
168             @Override
169             public OptionalInt getFontSize()
170             {
171                 return OptionalInt.empty();
172             }
173 
174             @Override
175             public String toString()
176             {
177                 return "AppearanceControlLabel []";
178             }
179         }
180 
181         // clock/timeEdit (exchanged) and speed label
182         JLabel speedLabel = new AppearanceControlLabel();
183         speedLabel.setMinimumSize(new Dimension(85, 25));
184         speedLabel.setPreferredSize(new Dimension(85, 25));
185         speedLabel.setMaximumSize(new Dimension(85, 25));
186         this.clockLabel = new ClockLabel(speedLabel);
187         this.clockLabel.setMinimumSize(new Dimension(130, 25));
188         this.clockLabel.setPreferredSize(new Dimension(130, 25));
189         this.clockLabel.setMaximumSize(new Dimension(130, 25));
190         this.timeEdit = new TimeEdit(new Time(0, TimeUnit.DEFAULT));
191         this.timeEdit.setMinimumSize(new Dimension(130, 25));
192         this.timeEdit.setPreferredSize(new Dimension(130, 25));
193         this.timeEdit.setMaximumSize(new Dimension(130, 25));
194         this.timeEdit.addPropertyChangeListener("value", this); // adds event at 'simulate until' time
195         add(this.clockLabel);
196         add(this.timeEdit);
197         add(speedLabel);
198 
199         setButtonsEnabledState();
200         prepareCleanup();
201         this.simulator.addListener(this, Replication.END_REPLICATION_EVENT);
202         this.simulator.addListener(this, SimulatorInterface.START_EVENT);
203         this.simulator.addListener(this, SimulatorInterface.STOP_EVENT);
204         this.simulator.addListener(this, DevsRealTimeAnimator.CHANGE_SPEED_FACTOR_EVENT);
205     }
206 
207     /**
208      * Create a button.
209      * @param name name of the button
210      * @param iconFile name of the icon file
211      * @param actionCommand the action command
212      * @param toolTipText the hint to show when the mouse hovers over the button
213      * @param enabled true if the new button must initially be enable; false if it must initially be disabled
214      * @return created button
215      */
216     private JButton makeButton(final String name, final String iconFile, final String actionCommand, final String toolTipText,
217             final boolean enabled)
218     {
219         JButton result = new AppearanceControlButton(IconUtil.of(iconFile).get());
220         result.setName(name);
221         result.setEnabled(enabled);
222         result.setActionCommand(actionCommand);
223         result.setToolTipText(toolTipText);
224         result.addActionListener(this);
225         Dimension dimension = new Dimension(50, 30);
226         result.setMinimumSize(dimension);
227         result.setPreferredSize(dimension);
228         result.setMaximumSize(dimension);
229         this.buttons.add(result);
230         return result;
231     }
232 
233     /**
234      * Setup the slider.
235      * @param minimum the minimum value on the scale (the displayed scale may extend a little further than this value)
236      * @param maximum the maximum value on the scale (the displayed scale may extend a little further than this value)
237      * @param initialValue the initially selected value on the scale
238      * @param sim the simulator to change the speed of
239      * @return slider
240      */
241     private JSlider setupSlider(final double minimum, final double maximum, final double initialValue,
242             final OtsSimulatorInterface sim)
243     {
244         Throw.when(minimum <= 0 || minimum > initialValue || initialValue > maximum || maximum > 9999,
245                 OtsRuntimeException.class, "Bad (combination of) minimum, maximum and initialValue; "
246                         + "(restrictions: 0 < minimum <= initialValue <= maximum <= 9999)");
247 
248         Hashtable<Integer, JLabel> labels = new Hashtable<>();
249         int maximumTick = -1;
250         int minimumTick = 0;
251         int ratioIndex = 0;
252         int scale = 0;
253         while (this.simulationSpeedRatios[ratioIndex] * Math.pow(10, scale) <= maximum)
254         {
255             maximumTick++;
256             this.tickValues.put(maximumTick, this.simulationSpeedRatios[ratioIndex] * Math.pow(10, scale));
257             StringBuilder text = new StringBuilder();
258             text.append(this.simulationSpeedRatios[ratioIndex]);
259             for (int i = 0; i < scale; i++)
260             {
261                 text.append("0");
262             }
263             labels.put(maximumTick, new JLabel(text.toString().replace("000", "K")));
264             ratioIndex++;
265             if (ratioIndex == this.simulationSpeedRatios.length)
266             {
267                 ratioIndex = 0;
268                 scale += 1;
269             }
270         }
271         ratioIndex = this.simulationSpeedRatios.length - 1;
272         scale = 1;
273         while (this.simulationSpeedRatios[ratioIndex] * Math.pow(0.1, scale) >= minimum)
274         {
275             minimumTick--;
276             this.tickValues.put(minimumTick, this.simulationSpeedRatios[ratioIndex] * Math.pow(0.1, scale));
277             StringBuilder text = new StringBuilder("0").append(OtsSimulationControlPanel.this.decimalSeparator);
278             for (int i = 1; i < scale; i++)
279             {
280                 text.append("0");
281             }
282             text.append(this.simulationSpeedRatios[ratioIndex]);
283             labels.put(minimumTick, new JLabel(text.toString()));
284             ratioIndex--;
285             if (ratioIndex < 0)
286             {
287                 ratioIndex = this.simulationSpeedRatios.length - 1;
288                 scale += 1;
289             }
290         }
291         JSlider slider = new JSlider(SwingConstants.HORIZONTAL, minimumTick, maximumTick + 1, 0);
292         slider.setMinimumSize(new Dimension(350, 45));
293         slider.setPreferredSize(new Dimension(350, 45));
294         slider.setMaximumSize(new Dimension(350, 45));
295         labels.put(maximumTick + 1, new JLabel("\u221E"));
296         this.tickValues.put(maximumTick + 1, 1E9);
297         slider.setLabelTable(labels);
298         slider.setPaintLabels(true);
299         slider.setPaintTicks(true);
300         slider.setMajorTickSpacing(1);
301         /*- Uncomment to verify the stepToFactor method.
302         for (int i = slider.getMinimum(); i <= slider.getMaximum(); i++)
303         {
304             System.out.println("pos=" + i + " value is " + stepToFactor(i));
305         }
306          */
307 
308         // initial value of simulation speed
309         if (sim instanceof DevsRealTimeAnimator)
310         {
311             @SuppressWarnings("unchecked")
312             DevsRealTimeAnimator<Duration> clock = (DevsRealTimeAnimator<Duration>) sim;
313             clock.setSpeedFactor(this.tickValues.get(slider.getValue()));
314         }
315 
316         // adjust the simulation speed
317         slider.addChangeListener(new ChangeListener()
318         {
319             @Override
320             public void stateChanged(final ChangeEvent ce)
321             {
322                 JSlider source = (JSlider) ce.getSource();
323                 if (!source.getValueIsAdjusting() && sim instanceof DevsRealTimeAnimator)
324                 {
325                     @SuppressWarnings("unchecked")
326                     DevsRealTimeAnimator<Duration> clock = (DevsRealTimeAnimator<Duration>) sim;
327                     clock.setSpeedFactor(OtsSimulationControlPanel.this.tickValues.get(source.getValue()));
328                 }
329             }
330         });
331 
332         return slider;
333     }
334 
335     /**
336      * Prepare the clean-up; stopping the simulator when going to close, and calling {@link #cleanup} when closed.
337      */
338     private void prepareCleanup()
339     {
340         // The JFrame is not yet created, as panels are prepared as an argument to it, so run a Thread until we find it
341         new Thread("OtsSimulationControlPanel cleanup preparation")
342         {
343             @Override
344             public void run()
345             {
346                 JFrame root = null;
347                 int n = 0;
348                 while (root == null && n++ < 500) // should wait for about 5s max
349                 {
350                     try
351                     {
352                         Thread.sleep(10);
353                     }
354                     catch (InterruptedException exception)
355                     {
356                         // nothing to do
357                     }
358                     root = (JFrame) SwingUtilities.getRoot(OtsSimulationControlPanel.this);
359                 }
360                 root.addWindowListener(new WindowAdapter()
361                 {
362                     @Override
363                     public void windowClosing(final WindowEvent e)
364                     {
365                         if (OtsSimulationControlPanel.this.simulator != null)
366                         {
367                             try
368                             {
369                                 if (OtsSimulationControlPanel.this.simulator.isStartingOrRunning())
370                                 {
371                                     OtsSimulationControlPanel.this.simulator.stop();
372                                 }
373                             }
374                             catch (SimRuntimeException exception)
375                             {
376                                 exception.printStackTrace();
377                             }
378                         }
379                     }
380 
381                     @Override
382                     public void windowClosed(final WindowEvent e)
383                     {
384                         cleanup();
385                     }
386                 });
387             }
388         }.start();
389     }
390 
391     /**
392      * Set the time warp factor to the best possible approximation of a given value.
393      * @param factor the requested speed factor
394      */
395     public void setSpeedFactor(final double factor)
396     {
397         int bestStep = -1;
398         double bestError = Double.MAX_VALUE;
399         double logOfFactor = Math.log(factor);
400         for (int step = this.speedSlider.getMinimum(); step <= this.speedSlider.getMaximum(); step++)
401         {
402             double ratio = this.tickValues.get(step); // stepToFactor(step);
403             double logError = Math.abs(logOfFactor - Math.log(ratio));
404             if (logError < bestError)
405             {
406                 bestStep = step;
407                 bestError = logError;
408             }
409         }
410         Logger.ots().trace("setSpeedfactor: factor is {}, best slider value is {} current value is {}", factor, bestStep,
411                 this.speedSlider.getValue());
412         if (this.speedSlider.getValue() != bestStep)
413         {
414             this.speedSlider.setValue(bestStep);
415         }
416     }
417 
418     /**
419      * Change the enabled/disabled state of the various simulation control buttons.
420      * @param newState true if the buttons should become enabled; false if the buttons should become disabled
421      */
422     public void setSimulationControlButtons(final boolean newState)
423     {
424         this.buttonsEnabled = newState;
425         setButtonsEnabledState();
426     }
427 
428     /**
429      * Construct and schedule a SimEvent using a Time to specify the execution time.
430      * @param executionTime the time at which the event must happen
431      * @param priority should be between {@code SimEventInterface.MAX_PRIORITY} and {@code SimEventInterface.MIN_PRIORITY}; most
432      *            normal events should use {@code SimEventInterface.NORMAL_PRIORITY}
433      * @param executable executable
434      * @return the event that was scheduled (the caller should save this if a need to cancel the event may arise later)
435      * @throws SimRuntimeException when the {@code executionTime} is in the past
436      */
437     private SimEventInterface<Duration> scheduleEvent(final Duration executionTime, final short priority,
438             final Executable executable) throws SimRuntimeException
439     {
440         return this.simulator.scheduleEventAbs(executionTime, priority, executable);
441     }
442 
443     @Override
444     public void actionPerformed(final ActionEvent actionEvent)
445     {
446         String actionCommand = actionEvent.getActionCommand();
447         Logger.ots().trace("actionCommand: " + actionCommand);
448         try
449         {
450             if (actionCommand.equals("Step"))
451             {
452                 if (getSimulator().isStartingOrRunning())
453                 {
454                     getSimulator().stop();
455                 }
456                 this.simulator.step();
457             }
458             if (actionCommand.equals("RunPause"))
459             {
460                 if (this.simulator.isStartingOrRunning())
461                 {
462                     Logger.ots().trace("RunPause: Stopping simulator");
463                     this.simulator.stop();
464                 }
465                 else if (getSimulator().getEventList().size() > 0)
466                 {
467                     Logger.ots().trace("RunPause: Starting simulator");
468                     this.simulator.start();
469                 }
470             }
471             if (actionCommand.equals("NextTime"))
472             {
473                 if (getSimulator().isStartingOrRunning())
474                 {
475                     Logger.ots().trace("NextTime: Stopping simulator");
476                     getSimulator().stop();
477                 }
478                 try
479                 {
480                     this.stopAtEvent = scheduleEvent(getSimulator().getSimulatorTime(), SimEventInterface.MIN_PRIORITY,
481                             () -> autoPauseSimulator());
482                 }
483                 catch (SimRuntimeException exception)
484                 {
485                     Logger.ots().error("Caught an exception while trying to schedule an autoPauseSimulator event "
486                             + "at the current simulator time");
487                 }
488                 Logger.ots().trace("NextTime: Starting simulator");
489                 this.simulator.start();
490             }
491             setButtonsEnabledState();
492         }
493         catch (Exception exception)
494         {
495             exception.printStackTrace();
496         }
497     }
498 
499     /**
500      * Clean up timers, contexts, threads, etc. that could prevent garbage collection.
501      */
502     private void cleanup()
503     {
504         if (!this.isCleanUp)
505         {
506             this.isCleanUp = true;
507             try
508             {
509                 if (this.simulator != null)
510                 {
511                     if (this.simulator.isStartingOrRunning())
512                     {
513                         System.out.println("Clean-up: stopping simulator.");
514                         this.simulator.stop();
515                     }
516                     getSimulator().cleanUp();
517                 }
518 
519                 System.out.println("Clock timer cancelled.");
520                 if (this.clockLabel != null)
521                 {
522                     this.clockLabel.cancelTimer(); // cancel the timer on the clock panel.
523                 }
524             }
525             catch (Throwable exception)
526             {
527                 exception.printStackTrace();
528             }
529         }
530     }
531 
532     /**
533      * Update the enabled state of all the buttons.
534      */
535     private void setButtonsEnabledState()
536     {
537         Logger.ots().trace("FixButtons entered");
538         final boolean moreWorkToDo = getSimulator().getEventList().size() > 0;
539         for (JButton button : this.buttons)
540         {
541             final String actionCommand = button.getActionCommand();
542             if (actionCommand.equals("Step"))
543             {
544                 button.setEnabled(moreWorkToDo && this.buttonsEnabled);
545             }
546             else if (actionCommand.equals("RunPause"))
547             {
548                 button.setEnabled(moreWorkToDo && this.buttonsEnabled);
549                 if (this.simulator.isStartingOrRunning())
550                 {
551                     button.setToolTipText("Pause the simulation");
552                     button.setIcon(PAUSE_ICON);
553                 }
554                 else
555                 {
556                     button.setToolTipText("Run the simulation at the indicated speed");
557                     button.setIcon(PLAY_ICON);
558                 }
559                 button.setEnabled(moreWorkToDo && this.buttonsEnabled);
560             }
561             else if (actionCommand.equals("NextTime"))
562             {
563                 button.setEnabled(moreWorkToDo && this.buttonsEnabled);
564             }
565             else
566             {
567                 Logger.ots().error(new Exception("Unknown button?"));
568             }
569         }
570         this.speedSlider.setEnabled(this.buttonsEnabled);
571         Logger.ots().trace("FixButtons finishing");
572     }
573 
574     /**
575      * Pause the simulator.
576      */
577     public void autoPauseSimulator()
578     {
579         Logger.ots().trace("OtsControlPanel.autoPauseSimulator entered");
580         if (getSimulator().isStartingOrRunning())
581         {
582             Duration currentTick = getSimulator().getSimulatorTime();
583             Duration nextTick = getSimulator().getEventList().first().getAbsoluteExecutionTime();
584             Logger.ots().trace("currentTick is {}", currentTick);
585             Logger.ots().trace("nextTick is {}", nextTick);
586             if (nextTick.gt(currentTick))
587             {
588                 // The clock is now just beyond where it was when the user requested the NextTime operation
589                 // Insert another autoPauseSimulator event just before what is now the time of the next event
590                 // and let the simulator time increment to that time
591                 Logger.ots().trace("Re-Scheduling at " + nextTick);
592                 try
593                 {
594                     this.stopAtEvent = scheduleEvent(nextTick, SimEventInterface.MAX_PRIORITY, () -> autoPauseSimulator());
595                     Logger.ots().trace("AutoPauseSimulator: starting simulator");
596                 }
597                 catch (SimRuntimeException exception)
598                 {
599                     Logger.ots()
600                             .error("Caught an exception while trying to re-schedule an autoPauseEvent at the next real event");
601                 }
602             }
603             else
604             {
605                 try
606                 {
607                     Logger.ots().trace("AutoPauseSimulator: stopping simulator");
608                     getSimulator().stop();
609                 }
610                 catch (SimRuntimeException exception1)
611                 {
612                     exception1.printStackTrace();
613                 }
614                 Logger.ots().trace("Not re-scheduling");
615                 if (SwingUtilities.isEventDispatchThread())
616                 {
617                     Logger.ots().trace("Already on EventDispatchThread");
618                     setButtonsEnabledState();
619                 }
620                 else
621                 {
622                     try
623                     {
624                         Logger.ots().trace("Current thread is NOT EventDispatchThread: " + Thread.currentThread());
625                         SwingUtilities.invokeAndWait(new Runnable()
626                         {
627                             @Override
628                             public void run()
629                             {
630                                 Logger.ots().trace("Runnable started");
631                                 setButtonsEnabledState();
632                                 Logger.ots().trace("Runnable finishing");
633                             }
634                         });
635                     }
636                     catch (Exception e)
637                     {
638                         if (e instanceof InterruptedException)
639                         {
640                             Logger.ots().error(e);
641                             // e.printStackTrace();
642                         }
643                         else
644                         {
645                             e.printStackTrace();
646                         }
647                     }
648                 }
649             }
650         }
651         Logger.ots().trace("OtsControlPanel.autoPauseSimulator finished");
652     }
653 
654     @Override
655     public void propertyChange(final PropertyChangeEvent evt)
656     {
657         // timeEdit value changed, schedule stop event (and cancel possible previous event)
658         Logger.ots().trace("PropertyChanged: " + evt);
659         if (null != this.stopAtEvent)
660         {
661             getSimulator().cancelEvent(this.stopAtEvent); // silently ignore false result
662             this.stopAtEvent = null;
663         }
664         String newValue = (String) evt.getNewValue();
665         String[] fields = newValue.split("[:\\" + this.decimalSeparator + "]");
666         int hours = Integer.parseInt(fields[0]);
667         int minutes = Integer.parseInt(fields[1]);
668         int seconds = Integer.parseInt(fields[2]);
669         int fraction = Integer.parseInt(fields[3]);
670         double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
671         if (stopTime < getSimulator().getSimulatorTime().getSI())
672         {
673             return;
674         }
675         else
676         {
677             try
678             {
679                 this.stopAtEvent =
680                         scheduleEvent(Duration.ofSI(stopTime), SimEventInterface.MAX_PRIORITY, () -> autoPauseSimulator());
681             }
682             catch (SimRuntimeException exception)
683             {
684                 Logger.ots().error("Caught an exception while trying to schedule an autoPauseSimulator event");
685             }
686         }
687     }
688 
689     /**
690      * Return simulator.
691      * @return simulator
692      */
693     public OtsSimulatorInterface getSimulator()
694     {
695         return this.simulator;
696     }
697 
698     /**
699      * Return time font.
700      * @return font for the time display
701      */
702     public Font getTimeFont()
703     {
704         return this.timeFont;
705     }
706 
707     @Override
708     public void notify(final Event event)
709     {
710         if (event.getType().equals(Replication.END_REPLICATION_EVENT) || event.getType().equals(SimulatorInterface.START_EVENT)
711                 || event.getType().equals(SimulatorInterface.STOP_EVENT)
712                 || event.getType().equals(DevsRealTimeAnimator.CHANGE_SPEED_FACTOR_EVENT))
713         {
714             Logger.ots().trace("OtsControlPanel receive event " + event);
715             if (event.getType().equals(DevsRealTimeAnimator.CHANGE_SPEED_FACTOR_EVENT))
716             {
717                 setSpeedFactor((Double) event.getContent());
718                 return;
719             }
720             else if (event.getType().equals(Replication.END_REPLICATION_EVENT))
721             {
722                 this.buttonsEnabled = false;
723             }
724             setButtonsEnabledState();
725         }
726     }
727 
728     @Override
729     public String toString()
730     {
731         return "OtsControlPanel [simulatorTime=" + this.simulator.getSimulatorTime() + "]";
732     }
733 
734     /**
735      * {@link JLabel} that displays the simulation time.
736      */
737     private final class ClockLabel extends JLabel implements AppearanceControl
738     {
739 
740         /** Serialization version UID. */
741         private static final long serialVersionUID = 20141211L;
742 
743         /** The JLabel that displays the time. */
744         private final JLabel speedLabel;
745 
746         /** The timer (so we can cancel it). */
747         private Timer timer;
748 
749         /** Timer update interval in msec. */
750         private static final long UPDATEINTERVAL = 1000;
751 
752         /** Simulation time time. */
753         private double prevSimTime = 0;
754 
755         /**
756          * Construct a clock panel.
757          * @param speedLabel speed label
758          */
759         private ClockLabel(final JLabel speedLabel)
760         {
761             super("00:00:00" + OtsSimulationControlPanel.this.decimalSeparator + "000");
762             this.speedLabel = speedLabel;
763             speedLabel.setFont(getTimeFont());
764             setFont(getTimeFont());
765             setHorizontalAlignment(SwingConstants.RIGHT);
766             setOpaque(true);
767             this.timer = new Timer();
768             this.timer.scheduleAtFixedRate(new TimeUpdateTask(), 0, ClockLabel.UPDATEINTERVAL);
769             addMouseListener(new MouseAdapter()
770             {
771                 @Override
772                 public void mouseClicked(final MouseEvent e)
773                 {
774                     if (!OtsSimulationControlPanel.this.buttonsEnabled)
775                     {
776                         return;
777                     }
778                     setVisible(false);
779                     OtsSimulationControlPanel.this.timeEdit.setVisible(true);
780                     OtsSimulationControlPanel.this.timeEdit.requestFocus();
781                     getParent().invalidate();
782                 }
783             });
784         }
785 
786         /**
787          * Cancel the timer task.
788          */
789         public void cancelTimer()
790         {
791             if (this.timer != null)
792             {
793                 this.timer.cancel();
794             }
795             this.timer = null;
796         }
797 
798         /** Updater for the clock panel. */
799         private class TimeUpdateTask extends TimerTask
800         {
801             /**
802              * Constructor.
803              */
804             TimeUpdateTask()
805             {
806             }
807 
808             @Override
809             public void run()
810             {
811                 double now = Math.round(getSimulator().getSimulatorTime().getSI() * 1000) / 1000d;
812                 int seconds = (int) Math.floor(now);
813                 int h = (int) seconds / 3600;
814                 int m = (int) (seconds - h * 3600) / 60;
815                 double s = now - h * 3600 - m * 60;
816                 ClockLabel.this.setText(String.format("  %02d:%02d:%06.3f ", h, m, s));
817                 ClockLabel.this.repaint();
818                 double speed = getSpeed(now);
819                 if (Double.isNaN(speed))
820                 {
821                     getSpeedLabel().setText("");
822                 }
823                 else
824                 {
825                     getSpeedLabel().setText(String.format("% 5.2fx  ", speed));
826                 }
827                 getSpeedLabel().repaint();
828             }
829 
830             @Override
831             public final String toString()
832             {
833                 return "TimeUpdateTask of ClockPanel";
834             }
835         }
836 
837         /**
838          * Return speed label.
839          * @return speed label
840          */
841         private JLabel getSpeedLabel()
842         {
843             return this.speedLabel;
844         }
845 
846         /**
847          * Returns the simulation speed.
848          * @param t simulation time
849          * @return simulation speed
850          */
851         private double getSpeed(final double t)
852         {
853             double speed = (t - this.prevSimTime) / (0.001 * UPDATEINTERVAL);
854             this.prevSimTime = t;
855             return speed;
856         }
857 
858         @Override
859         public boolean isForeground()
860         {
861             return true;
862         }
863 
864         @Override
865         public boolean isBackground()
866         {
867             return true;
868         }
869 
870         @Override
871         public void setBackground(final Color color)
872         {
873             double f = 0.92;
874             super.setBackground(
875                     new Color((int) (color.getRed() * f), (int) (color.getGreen() * f), (int) (color.getBlue() * f)));
876         }
877 
878         @Override
879         public OptionalInt getFontSize()
880         {
881             return OptionalInt.empty();
882         }
883 
884         @Override
885         public String toString()
886         {
887             return "ClockPanel";
888         }
889 
890     }
891 
892     /** Entry field for simulate until time. */
893     private final class TimeEdit extends JFormattedTextField implements AppearanceControl
894     {
895 
896         /** Serialization version UID. */
897         private static final long serialVersionUID = 20141212L;
898 
899         /** Last caret position in the time editor. USed to know whether to skip left or right around ':', '.' or ','. */
900         private int lastCaretPosition = -1;
901 
902         /**
903          * Construct a new TimeEdit.
904          * @param initialValue the initial value for the TimeEdit
905          */
906         private TimeEdit(final Time initialValue)
907         {
908             super(new RegexFormatter(
909                     "\\d{2,}:[0-5]\\d:[0-5]\\d\\" + OtsSimulationControlPanel.this.decimalSeparator + "\\d\\d\\d"));
910             addKeyListener(new KeyAdapter()
911             {
912                 @Override
913                 public void keyPressed(final KeyEvent e)
914                 {
915                     String value = getText();
916                     int caretPosition = getCaretPosition();
917                     ((RegexFormatter) getFormatter()).setOverwriteMode(caretPosition > value.indexOf(':') - 2);
918                 }
919             });
920             addCaretListener((e) ->
921             {
922                 String value = getText();
923                 int caretPosition = getCaretPosition();
924                 if (value.length() - 1 > caretPosition && (value.charAt(caretPosition) == ':'
925                         || value.charAt(caretPosition) == '.' || value.charAt(caretPosition) == ','))
926                 {
927                     caretPosition = caretPosition + (this.lastCaretPosition <= caretPosition ? 1 : -1);
928                     this.lastCaretPosition = caretPosition;
929                     this.setCaretPosition(caretPosition);
930                 }
931                 else if (e.getDot() != e.getMark())
932                 {
933                     this.lastCaretPosition = caretPosition;
934                     this.setCaretPosition(caretPosition);
935                 }
936             });
937             addFocusListener(new FocusAdapter()
938             {
939                 @Override
940                 public void focusLost(final FocusEvent e)
941                 {
942                     OtsSimulationControlPanel.this.clockLabel.setVisible(true);
943                     setVisible(false);
944                     getParent().invalidate();
945                 }
946             });
947             OtsSimulationControlPanel.this.addMouseListener(new MouseAdapter()
948             {
949                 @Override
950                 public void mouseClicked(final MouseEvent e)
951                 {
952                     if (OtsSimulationControlPanel.this.timeEdit.hasFocus())
953                     {
954                         // removes focus from time edit when the user clicks anywhere on the control panel
955                         TimeEdit.this.setFocusable(false);
956                         TimeEdit.this.setFocusable(true);
957                     }
958                     // this listener prevents events from reaching the main panel with pop-up menu, make it appear from here
959                     JPanel mainPanel = (JPanel) ((AppearanceApplication) SwingUtilities
960                             .getAncestorOfClass(AppearanceApplication.class, OtsSimulationControlPanel.this)).getContentPane();
961                     if (e.getButton() == MouseEvent.BUTTON3 && e.getClickCount() == 1
962                             && mainPanel.getComponentPopupMenu() != null)
963                     {
964                         mainPanel.getComponentPopupMenu().show(mainPanel, e.getX(), e.getY());
965                     }
966                 }
967             });
968             RegexFormatter formatter = (RegexFormatter) getFormatter();
969             formatter.setAllowsInvalid(false);
970             formatter.setCommitsOnValidEdit(true);
971             formatter.setOverwriteMode(true);
972             setTime(initialValue);
973             setFont(getTimeFont());
974             setHorizontalAlignment(SwingConstants.RIGHT);
975             setVisible(false);
976         }
977 
978         /**
979          * Set or update the time shown in this TimeEdit.
980          * @param newValue the (new) value to set/show in this TimeEdit
981          */
982         public void setTime(final Time newValue)
983         {
984             double v = newValue.getSI();
985             int seconds = (int) Math.floor(v);
986             int h = (int) seconds / 3600;
987             int m = (int) (seconds - h * 3600) / 60;
988             double s = v - h * 3600 - m * 60;
989             this.setText(String.format("%02d:%02d:%06.3f", h, m, s));
990         }
991 
992         @Override
993         public OptionalInt getFontSize()
994         {
995             return OptionalInt.empty();
996         }
997 
998         @Override
999         public String toString()
1000         {
1001             return "TimeEdit [time=" + getText() + "]";
1002         }
1003 
1004     }
1005 
1006     /**
1007      * Extension of a {@link DefaultFormatter} that uses a regular expression. For use in the simulate until time edit. <br>
1008      * Derived from <a href="http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm">
1009      * http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm</a>
1010      * @author Peter Knoppers
1011      */
1012     private static final class RegexFormatter extends DefaultFormatter
1013     {
1014 
1015         /** Serialization version UID. */
1016         private static final long serialVersionUID = 20141212L;
1017 
1018         /** The regular expression pattern. */
1019         private Pattern pattern;
1020 
1021         /**
1022          * Create a new RegexFormatter.
1023          * @param pattern regular expression pattern that defines what this RexexFormatter will accept
1024          */
1025         private RegexFormatter(final String pattern)
1026         {
1027             this.pattern = Pattern.compile(pattern);
1028         }
1029 
1030         @Override
1031         public Object stringToValue(final String text) throws ParseException
1032         {
1033             Matcher matcher = this.pattern.matcher(text);
1034             if (matcher.matches())
1035             {
1036                 Logger.ots().trace("String \"" + text + "\" matches");
1037                 return super.stringToValue(text);
1038             }
1039             Logger.ots().trace("String \"" + text + "\" does not match");
1040             throw new ParseException("Pattern did not match", 0);
1041         }
1042 
1043         @Override
1044         public String toString()
1045         {
1046             return "RegexFormatter [pattern=" + this.pattern + "]";
1047         }
1048 
1049     }
1050 
1051 }