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