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