1 package org.opentrafficsim.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.rmi.RemoteException;
15 import java.text.DecimalFormat;
16 import java.text.NumberFormat;
17 import java.text.ParseException;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.Hashtable;
21 import java.util.Map;
22 import java.util.Timer;
23 import java.util.TimerTask;
24 import java.util.logging.Level;
25 import java.util.logging.Logger;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import javax.swing.BoxLayout;
30 import javax.swing.ImageIcon;
31 import javax.swing.JButton;
32 import javax.swing.JFormattedTextField;
33 import javax.swing.JFrame;
34 import javax.swing.JLabel;
35 import javax.swing.JPanel;
36 import javax.swing.JSlider;
37 import javax.swing.SwingConstants;
38 import javax.swing.SwingUtilities;
39 import javax.swing.WindowConstants;
40 import javax.swing.event.ChangeEvent;
41 import javax.swing.event.ChangeListener;
42 import javax.swing.text.DefaultFormatter;
43 import javax.swing.text.MaskFormatter;
44
45 import nl.tudelft.simulation.dsol.SimRuntimeException;
46 import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
47 import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
48 import nl.tudelft.simulation.dsol.simulators.DEVSRealTimeClock;
49 import nl.tudelft.simulation.dsol.simulators.DEVSSimulator;
50 import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
51 import nl.tudelft.simulation.language.io.URLResource;
52
53 import org.djunits.unit.TimeUnit;
54 import org.djunits.value.vdouble.scalar.DoubleScalar;
55 import org.opentrafficsim.core.OTS_SCALAR;
56 import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
57 import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
58 import org.opentrafficsim.simulationengine.WrappableAnimation;
59
60
61
62
63
64
65
66
67
68
69
70
71 public class OTSControlPanel extends JPanel implements ActionListener, PropertyChangeListener, WindowListener, OTS_SCALAR
72 {
73
74 private static final long serialVersionUID = 20150617L;
75
76
77 private OTSDEVSSimulatorInterface simulator;
78
79
80 private final WrappableAnimation wrappableAnimation;
81
82
83 private final Logger logger;
84
85
86 private final ClockPanel clockPanel;
87
88
89 private final TimeWarpPanel timeWarpPanel;
90
91
92 private final ArrayList<JButton> buttons = new ArrayList<JButton>();
93
94
95 private final Font timeFont = new Font("SansSerif", Font.BOLD, 18);
96
97
98 private final TimeEdit timeEdit;
99
100
101 private SimEvent<OTSSimTimeDouble> stopAtEvent = null;
102
103
104 protected boolean closeHandlerRegistered = false;
105
106
107 private boolean isCleanUp = false;
108
109
110
111
112
113
114 public OTSControlPanel(final OTSDEVSSimulatorInterface simulator, final WrappableAnimation wrappableAnimation)
115 {
116 this.simulator = simulator;
117 this.wrappableAnimation = wrappableAnimation;
118 this.logger = Logger.getLogger("nl.tudelft.opentrafficsim");
119
120 this.setLayout(new FlowLayout(FlowLayout.LEFT));
121 JPanel buttonPanel = new JPanel();
122 buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
123 buttonPanel.add(makeButton("stepButton", "/Last_recor.png", "Step", "Execute one event", true));
124 buttonPanel.add(makeButton("nextTimeButton", "/NextTrack.png", "NextTime",
125 "Execute all events scheduled for the current time", true));
126 buttonPanel.add(makeButton("runButton", "/Play.png", "Run", "Run the simulation at maximum speed", true));
127 buttonPanel.add(makeButton("pauseButton", "/Pause.png", "Pause", "Pause the simulator", false));
128 this.timeWarpPanel = new TimeWarpPanel(0.1, 1000, 1, 3, simulator);
129 buttonPanel.add(this.timeWarpPanel);
130 buttonPanel.add(makeButton("resetButton", "/Undo.png", "Reset", null, false));
131 this.clockPanel = new ClockPanel();
132 buttonPanel.add(this.clockPanel);
133 this.timeEdit = new TimeEdit(new Time.Abs(0, SECOND));
134 this.timeEdit.addPropertyChangeListener("value", this);
135 buttonPanel.add(this.timeEdit);
136 this.add(buttonPanel);
137
138 installWindowCloseHandler();
139 }
140
141
142
143
144
145
146
147
148
149
150 private JButton makeButton(final String name, final String iconPath, final String actionCommand,
151 final String toolTipText, final boolean enabled)
152 {
153
154 JButton result = new JButton(new ImageIcon(URLResource.getResource(iconPath)));
155 result.setName(name);
156 result.setEnabled(enabled);
157 result.setActionCommand(actionCommand);
158 result.setToolTipText(toolTipText);
159 result.addActionListener(this);
160 this.buttons.add(result);
161 return result;
162 }
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178 private SimEvent<OTSSimTimeDouble> scheduleEvent(final Time.Abs executionTime, final short priority,
179 final Object source, final Object eventTarget, final String method, final Object[] args) throws SimRuntimeException
180 {
181 SimEvent<OTSSimTimeDouble> simEvent =
182 new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new Time.Abs(executionTime.getSI(), SECOND)), priority,
183 source, eventTarget, method, args);
184 this.simulator.scheduleEvent(simEvent);
185 return simEvent;
186 }
187
188
189
190
191 public final void installWindowCloseHandler()
192 {
193 if (this.closeHandlerRegistered)
194 {
195 return;
196 }
197
198
199 new DisposeOnCloseThread(this).start();
200 }
201
202
203 protected class DisposeOnCloseThread extends Thread
204 {
205
206 private OTSControlPanel panel;
207
208
209
210
211 public DisposeOnCloseThread(final OTSControlPanel panel)
212 {
213 super();
214 this.panel = panel;
215 }
216
217
218 @Override
219 public final void run()
220 {
221 Container root = this.panel;
222 while (!(root instanceof JFrame))
223 {
224 try
225 {
226 Thread.sleep(10);
227 }
228 catch (InterruptedException exception)
229 {
230
231 }
232
233
234 root = this.panel;
235 while (null != root.getParent() && !(root instanceof JFrame))
236 {
237 root = root.getParent();
238 }
239 }
240 JFrame frame = (JFrame) root;
241 frame.addWindowListener(this.panel);
242 this.panel.closeHandlerRegistered = true;
243
244 }
245 }
246
247
248 @Override
249 public final void actionPerformed(final ActionEvent actionEvent)
250 {
251 String actionCommand = actionEvent.getActionCommand();
252
253 try
254 {
255 if (actionCommand.equals("Step"))
256 {
257 if (getSimulator().isRunning())
258 {
259 getSimulator().stop();
260 }
261 this.simulator.step();
262 }
263 if (actionCommand.equals("Run"))
264 {
265 this.simulator.start();
266 }
267 if (actionCommand.equals("NextTime"))
268 {
269 if (getSimulator().isRunning())
270 {
271 getSimulator().stop();
272 }
273 double now = getSimulator().getSimulatorTime().getTime().getSI();
274
275 try
276 {
277 this.stopAtEvent =
278 scheduleEvent(new Time.Abs(now, TimeUnit.SI), SimEventInterface.MIN_PRIORITY, this, this,
279 "autoPauseSimulator", null);
280 }
281 catch (SimRuntimeException exception)
282 {
283 this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator", "Caught an exception "
284 + "while trying to schedule an autoPauseSimulator event at the current simulator time");
285 }
286 this.simulator.start();
287 }
288 if (actionCommand.equals("Pause"))
289 {
290 this.simulator.stop();
291 }
292 if (actionCommand.equals("Reset"))
293 {
294 if (getSimulator().isRunning())
295 {
296 getSimulator().stop();
297 }
298
299 if (null == OTSControlPanel.this.wrappableAnimation)
300 {
301 throw new Error("Do not know how to restart this simulation");
302 }
303
304
305 Container root = OTSControlPanel.this;
306 while (!(root instanceof JFrame))
307 {
308 root = root.getParent();
309 }
310 JFrame frame = (JFrame) root;
311 Rectangle rect = frame.getBounds();
312 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
313 frame.dispose();
314 OTSControlPanel.this.cleanup();
315 try
316 {
317 OTSControlPanel.this.wrappableAnimation.rebuildSimulator(rect);
318 }
319 catch (Exception exception)
320 {
321 exception.printStackTrace();
322 }
323 }
324 fixButtons();
325 }
326 catch (Exception exception)
327 {
328 exception.printStackTrace();
329 }
330 }
331
332
333
334
335 private void cleanup()
336 {
337 if (!this.isCleanUp)
338 {
339 this.isCleanUp = true;
340 try
341 {
342 if (this.simulator != null)
343 {
344 if (this.simulator.isRunning())
345 {
346 this.simulator.stop();
347 }
348
349
350 getSimulator().getReplication().getExperiment().removeFromContext();
351 getSimulator().cleanUp();
352 }
353
354 if (this.clockPanel != null)
355 {
356 this.clockPanel.cancelTimer();
357 }
358
359 if (this.wrappableAnimation != null)
360 {
361 this.wrappableAnimation.stopTimersThreads();
362 }
363 }
364 catch (Throwable exception)
365 {
366 exception.printStackTrace();
367 }
368 }
369 }
370
371
372
373
374 protected final void fixButtons()
375 {
376
377 final boolean moreWorkToDo = getSimulator().getEventList().size() > 0;
378 for (JButton button : this.buttons)
379 {
380 final String actionCommand = button.getActionCommand();
381 if (actionCommand.equals("Step"))
382 {
383 button.setEnabled(moreWorkToDo);
384 }
385 else if (actionCommand.equals("Run"))
386 {
387 button.setEnabled(moreWorkToDo && !getSimulator().isRunning());
388 }
389 else if (actionCommand.equals("NextTime"))
390 {
391 button.setEnabled(moreWorkToDo);
392 }
393 else if (actionCommand.equals("Pause"))
394 {
395 button.setEnabled(getSimulator().isRunning());
396 }
397 else if (actionCommand.equals("Reset"))
398 {
399 button.setEnabled(true);
400 }
401 else
402 {
403 this.logger.logp(Level.SEVERE, "ControlPanel", "fixButtons", "", new Exception("Unknown button?"));
404 }
405 }
406
407 }
408
409
410
411
412 public final void autoPauseSimulator()
413 {
414 if (getSimulator().isRunning())
415 {
416 getSimulator().stop();
417 double currentTick = getSimulator().getSimulatorTime().getTime().getSI();
418 double nextTick = getSimulator().getEventList().first().getAbsoluteExecutionTime().get().getSI();
419
420
421 if (nextTick > currentTick)
422 {
423
424
425
426
427 try
428 {
429 this.stopAtEvent =
430 scheduleEvent(new Time.Abs(nextTick, TimeUnit.SI), SimEventInterface.MAX_PRIORITY, this, this,
431 "autoPauseSimulator", null);
432 getSimulator().start();
433 }
434 catch (SimRuntimeException exception)
435 {
436 this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator",
437 "Caught an exception while trying to re-schedule an autoPauseEvent at the next real event");
438 }
439 }
440 else
441 {
442
443 if (SwingUtilities.isEventDispatchThread())
444 {
445
446 fixButtons();
447 }
448 else
449 {
450 try
451 {
452
453 SwingUtilities.invokeAndWait(new Runnable()
454 {
455 @Override
456 public void run()
457 {
458
459 fixButtons();
460
461 }
462 });
463 }
464 catch (Exception e)
465 {
466 if (e instanceof InterruptedException)
467 {
468 System.out.println("Caught " + e);
469
470 }
471 else
472 {
473 e.printStackTrace();
474 }
475 }
476 }
477 }
478 }
479 }
480
481
482 @Override
483 public final void propertyChange(final PropertyChangeEvent evt)
484 {
485
486 if (null != this.stopAtEvent)
487 {
488 getSimulator().cancelEvent(this.stopAtEvent);
489 this.stopAtEvent = null;
490 }
491 String newValue = (String) evt.getNewValue();
492 String[] fields = newValue.split("[:\\.]");
493 int hours = Integer.parseInt(fields[0]);
494 int minutes = Integer.parseInt(fields[1]);
495 int seconds = Integer.parseInt(fields[2]);
496 int fraction = Integer.parseInt(fields[3]);
497 double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
498 if (stopTime < getSimulator().getSimulatorTime().getTime().getSI())
499 {
500 return;
501 }
502 else
503 {
504 try
505 {
506 this.stopAtEvent =
507 scheduleEvent(new Time.Abs(stopTime, SECOND), SimEventInterface.MAX_PRIORITY, this, this,
508 "autoPauseSimulator", null);
509 }
510 catch (SimRuntimeException exception)
511 {
512 this.logger.logp(Level.SEVERE, "ControlPanel", "propertyChange",
513 "Caught an exception while trying to schedule an autoPauseSimulator event");
514 }
515 }
516
517 }
518
519
520
521
522 @SuppressWarnings("unchecked")
523 public final DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble> getSimulator()
524 {
525 return (DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble>) this.simulator;
526 }
527
528
529 @Override
530 public void windowOpened(final WindowEvent e)
531 {
532
533 }
534
535
536 @Override
537 public final void windowClosing(final WindowEvent e)
538 {
539 if (this.simulator != null)
540 {
541 try
542 {
543 if (this.simulator.isRunning())
544 {
545 this.simulator.stop();
546 }
547 }
548 catch (SimRuntimeException exception)
549 {
550 exception.printStackTrace();
551 }
552 }
553 }
554
555
556 @Override
557 public final void windowClosed(final WindowEvent e)
558 {
559 cleanup();
560 }
561
562
563 @Override
564 public final void windowIconified(final WindowEvent e)
565 {
566
567 }
568
569
570 @Override
571 public final void windowDeiconified(final WindowEvent e)
572 {
573
574 }
575
576
577 @Override
578 public final void windowActivated(final WindowEvent e)
579 {
580
581 }
582
583
584 @Override
585 public final void windowDeactivated(final WindowEvent e)
586 {
587
588 }
589
590
591
592
593 public final Font getTimeFont()
594 {
595 return this.timeFont;
596 }
597
598
599 static class TimeWarpPanel extends JPanel
600 {
601
602 private static final long serialVersionUID = 20150408L;
603
604
605 private final JSlider slider;
606
607
608 private final int[] ratios;
609
610
611 private Map<Integer, Double> tickValues = new HashMap<>();
612
613
614
615
616
617
618
619
620
621
622
623 public TimeWarpPanel(final double minimum, final double maximum, final double initialValue,
624 final int ticksPerDecade, final DEVSSimulatorInterface<?, ?, ?> simulator)
625 {
626 if (minimum <= 0 || minimum > initialValue || initialValue > maximum)
627 {
628 throw new Error("Bad (combination of) minimum, maximum and initialValue; "
629 + "(restrictions: 0 < minimum <= initialValue <= maximum)");
630 }
631 switch (ticksPerDecade)
632 {
633 case 1:
634 this.ratios = new int[]{1};
635 break;
636 case 2:
637 this.ratios = new int[]{1, 3};
638 break;
639 case 3:
640 this.ratios = new int[]{1, 2, 5};
641 break;
642 default:
643 throw new Error("Bad ticksPerDecade value (must be 1, 2 or 3)");
644 }
645 int minimumTick = (int) Math.floor(Math.log10(minimum / initialValue) * ticksPerDecade);
646 int maximumTick = (int) Math.ceil(Math.log10(maximum / initialValue) * ticksPerDecade);
647 this.slider = new JSlider(SwingConstants.HORIZONTAL, minimumTick, maximumTick + 1, 0);
648 this.slider.setPreferredSize(new Dimension(350, 45));
649 Hashtable<Integer, JLabel> labels = new Hashtable<Integer, JLabel>();
650 for (int step = 0; step <= maximumTick; step++)
651 {
652 StringBuilder text = new StringBuilder();
653 text.append(this.ratios[step % this.ratios.length]);
654 for (int decade = 0; decade < step / this.ratios.length; decade++)
655 {
656 text.append("0");
657 }
658 this.tickValues.put(step, Double.parseDouble(text.toString()));
659 labels.put(step, new JLabel(text.toString().replace("000", "K")));
660
661 }
662
663 String decimalSeparator =
664 "" + ((DecimalFormat) NumberFormat.getInstance()).getDecimalFormatSymbols().getDecimalSeparator();
665 for (int step = -1; step >= minimumTick; step--)
666 {
667 StringBuilder text = new StringBuilder();
668 text.append("0");
669 text.append(decimalSeparator);
670 for (int decade = (step + 1) / this.ratios.length; decade < 0; decade++)
671 {
672 text.append("0");
673 }
674 int index = step % this.ratios.length;
675 if (index < 0)
676 {
677 index += this.ratios.length;
678 }
679 text.append(this.ratios[index]);
680 labels.put(step, new JLabel(text.toString()));
681 this.tickValues.put(step, Double.parseDouble(text.toString()));
682
683 }
684 labels.put(maximumTick + 1, new JLabel("\u221E"));
685 this.tickValues.put(maximumTick + 1, 1E9);
686 this.slider.setLabelTable(labels);
687 this.slider.setPaintLabels(true);
688 this.slider.setPaintTicks(true);
689 this.slider.setMajorTickSpacing(1);
690 this.add(this.slider);
691
692
693
694
695
696
697
698
699 if (simulator instanceof DEVSRealTimeClock)
700 {
701 DEVSRealTimeClock<?, ?, ?> clock = (DEVSRealTimeClock<?, ?, ?>) simulator;
702 clock.setSpeedFactor(TimeWarpPanel.this.tickValues.get(this.slider.getValue()));
703 }
704
705
706 this.slider.addChangeListener(new ChangeListener()
707 {
708 public void stateChanged(final ChangeEvent ce)
709 {
710 JSlider source = (JSlider) ce.getSource();
711 if (!source.getValueIsAdjusting() && simulator instanceof DEVSRealTimeClock)
712 {
713 DEVSRealTimeClock<?, ?, ?> clock = (DEVSRealTimeClock<?, ?, ?>) simulator;
714 clock.setSpeedFactor(((TimeWarpPanel) source.getParent()).getTickValues().get(source.getValue()));
715 }
716 }
717 });
718 }
719
720
721
722
723
724 protected Map<Integer, Double> getTickValues()
725 {
726 return this.tickValues;
727 }
728
729
730
731
732
733
734 private double stepToFactor(final int step)
735 {
736 int index = step % this.ratios.length;
737 if (index < 0)
738 {
739 index += this.ratios.length;
740 }
741 double result = this.ratios[index];
742
743 int power = (step + 1000 * this.ratios.length) / this.ratios.length - 1000;
744 while (power > 0)
745 {
746 result *= 10;
747 power--;
748 }
749 while (power < 0)
750 {
751 result /= 10;
752 power++;
753 }
754 return result;
755 }
756
757
758
759
760
761 public final double getFactor()
762 {
763 return stepToFactor(this.slider.getValue());
764 }
765 }
766
767
768 class ClockPanel extends JLabel
769 {
770
771 private static final long serialVersionUID = 20141211L;
772
773
774 private final JLabel clockLabel;
775
776
777 private Timer timer;
778
779
780 private static final long UPDATEINTERVAL = 1000;
781
782
783 ClockPanel()
784 {
785 super("00:00:00.000");
786 this.clockLabel = this;
787 this.setFont(getTimeFont());
788 this.timer = new Timer();
789 this.timer.scheduleAtFixedRate(new TimeUpdateTask(), 0, ClockPanel.UPDATEINTERVAL);
790 }
791
792
793
794
795 public void cancelTimer()
796 {
797 if (this.timer != null)
798 {
799 this.timer.cancel();
800 }
801 this.timer = null;
802 }
803
804
805 private class TimeUpdateTask extends TimerTask
806 {
807
808
809
810 public TimeUpdateTask()
811 {
812 }
813
814
815 @Override
816 public void run()
817 {
818 double now = Math.round(getSimulator().getSimulatorTime().getTime().getSI() * 1000) / 1000d;
819 int seconds = (int) Math.floor(now);
820 int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
821 getClockLabel().setText(
822 String.format(" %02d:%02d:%02d.%03d ", seconds / 3600, seconds / 60 % 60, seconds % 60,
823 fractionalSeconds));
824 getClockLabel().repaint();
825 }
826 }
827
828
829
830
831 protected JLabel getClockLabel()
832 {
833 return this.clockLabel;
834 }
835
836 }
837
838
839 class TimeEdit extends JFormattedTextField
840 {
841
842 private static final long serialVersionUID = 20141212L;
843
844
845
846
847
848 TimeEdit(final Time.Abs initialValue)
849 {
850 super(new RegexFormatter("\\d\\d\\d\\d:[0-5]\\d:[0-5]\\d\\.\\d\\d\\d"));
851 MaskFormatter mf = null;
852 try
853 {
854 mf = new MaskFormatter("####:##:##.###");
855 mf.setPlaceholderCharacter('0');
856 mf.setAllowsInvalid(false);
857 mf.setCommitsOnValidEdit(true);
858 mf.setOverwriteMode(true);
859 mf.install(this);
860 }
861 catch (ParseException exception)
862 {
863 exception.printStackTrace();
864 }
865 setTime(initialValue);
866 setFont(getTimeFont());
867 }
868
869
870
871
872
873 public void setTime(final Time.Abs newValue)
874 {
875 double v = newValue.getSI();
876 int integerPart = (int) Math.floor(v);
877 int fraction = (int) Math.floor((v - integerPart) * 1000);
878 String text =
879 String.format("%04d:%02d:%02d.%03d", integerPart / 3600, integerPart / 60 % 60, integerPart % 60, fraction);
880 this.setText(text);
881 }
882 }
883
884
885
886
887
888
889
890
891
892
893 static class RegexFormatter extends DefaultFormatter
894 {
895
896 private static final long serialVersionUID = 20141212L;
897
898
899 private Pattern pattern;
900
901
902
903
904
905 public RegexFormatter(final String pattern)
906 {
907 this.pattern = Pattern.compile(pattern);
908 }
909
910 @Override
911 public Object stringToValue(final String text) throws ParseException
912 {
913 Matcher matcher = this.pattern.matcher(text);
914 if (matcher.matches())
915 {
916
917 return super.stringToValue(text);
918 }
919
920 throw new ParseException("Pattern did not match", 0);
921 }
922 }
923
924 }