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.WrappableSimulation;
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 WrappableSimulation wrappableSimulation;
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 WrappableSimulation wrappableSimulation)
115 {
116 this.simulator = simulator;
117 this.wrappableSimulation = wrappableSimulation;
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
179 private SimEvent<OTSSimTimeDouble> scheduleEvent(final Time.Abs executionTime, final short priority,
180 final Object source, final Object eventTarget, final String method, final Object[] args) throws SimRuntimeException,
181 RemoteException
182 {
183 SimEvent<OTSSimTimeDouble> simEvent =
184 new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new Time.Abs(executionTime.getSI(), SECOND)), priority,
185 source, eventTarget, method, args);
186 this.simulator.scheduleEvent(simEvent);
187 return simEvent;
188 }
189
190
191
192
193 public final void installWindowCloseHandler()
194 {
195 if (this.closeHandlerRegistered)
196 {
197 return;
198 }
199
200
201 new DisposeOnCloseThread(this).start();
202 }
203
204
205 protected class DisposeOnCloseThread extends Thread
206 {
207
208 private OTSControlPanel panel;
209
210
211
212
213 public DisposeOnCloseThread(final OTSControlPanel panel)
214 {
215 super();
216 this.panel = panel;
217 }
218
219
220 @Override
221 public final void run()
222 {
223 Container root = this.panel;
224 while (!(root instanceof JFrame))
225 {
226 try
227 {
228 Thread.sleep(10);
229 }
230 catch (InterruptedException exception)
231 {
232
233 }
234
235
236 root = this.panel;
237 while (null != root.getParent() && !(root instanceof JFrame))
238 {
239 root = root.getParent();
240 }
241 }
242 JFrame frame = (JFrame) root;
243 frame.addWindowListener(this.panel);
244 this.panel.closeHandlerRegistered = true;
245
246 }
247 }
248
249
250 @Override
251 public final void actionPerformed(final ActionEvent actionEvent)
252 {
253 String actionCommand = actionEvent.getActionCommand();
254
255 try
256 {
257 if (actionCommand.equals("Step"))
258 {
259 if (getSimulator().isRunning())
260 {
261 getSimulator().stop();
262 }
263 this.simulator.step();
264 }
265 if (actionCommand.equals("Run"))
266 {
267 this.simulator.start();
268 }
269 if (actionCommand.equals("NextTime"))
270 {
271 if (getSimulator().isRunning())
272 {
273 getSimulator().stop();
274 }
275 double now = getSimulator().getSimulatorTime().getTime().getSI();
276
277 try
278 {
279 this.stopAtEvent =
280 scheduleEvent(new Time.Abs(now, TimeUnit.SI), SimEventInterface.MIN_PRIORITY, this, this,
281 "autoPauseSimulator", null);
282 }
283 catch (SimRuntimeException exception)
284 {
285 this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator", "Caught an exception "
286 + "while trying to schedule an autoPauseSimulator event at the current simulator time");
287 }
288 this.simulator.start();
289 }
290 if (actionCommand.equals("Pause"))
291 {
292 this.simulator.stop();
293 }
294 if (actionCommand.equals("Reset"))
295 {
296 if (getSimulator().isRunning())
297 {
298 getSimulator().stop();
299 }
300
301 if (null == OTSControlPanel.this.wrappableSimulation)
302 {
303 throw new Error("Do not know how to restart this simulation");
304 }
305
306
307 Container root = OTSControlPanel.this;
308 while (!(root instanceof JFrame))
309 {
310 root = root.getParent();
311 }
312 JFrame frame = (JFrame) root;
313 Rectangle rect = frame.getBounds();
314 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
315 frame.dispose();
316 OTSControlPanel.this.cleanup();
317 try
318 {
319 OTSControlPanel.this.wrappableSimulation.rebuildSimulator(rect);
320 }
321 catch (Exception exception)
322 {
323 exception.printStackTrace();
324 }
325 }
326 fixButtons();
327 }
328 catch (Exception exception)
329 {
330 exception.printStackTrace();
331 }
332 }
333
334
335
336
337 private void cleanup()
338 {
339 if (!this.isCleanUp)
340 {
341 this.isCleanUp = true;
342 try
343 {
344 if (this.simulator != null)
345 {
346 if (this.simulator.isRunning())
347 {
348 this.simulator.stop();
349 }
350
351
352 getSimulator().getReplication().getExperiment().removeFromContext();
353 getSimulator().cleanUp();
354 }
355
356 if (this.clockPanel != null)
357 {
358 this.clockPanel.cancelTimer();
359 }
360
361 if (this.wrappableSimulation != null)
362 {
363 this.wrappableSimulation.stopTimersThreads();
364 }
365 }
366 catch (Throwable exception)
367 {
368 exception.printStackTrace();
369 }
370 }
371 }
372
373
374
375
376 protected final void fixButtons()
377 {
378
379 final boolean moreWorkToDo = getSimulator().getEventList().size() > 0;
380 for (JButton button : this.buttons)
381 {
382 final String actionCommand = button.getActionCommand();
383 if (actionCommand.equals("Step"))
384 {
385 button.setEnabled(moreWorkToDo);
386 }
387 else if (actionCommand.equals("Run"))
388 {
389 button.setEnabled(moreWorkToDo && !getSimulator().isRunning());
390 }
391 else if (actionCommand.equals("NextTime"))
392 {
393 button.setEnabled(moreWorkToDo);
394 }
395 else if (actionCommand.equals("Pause"))
396 {
397 button.setEnabled(getSimulator().isRunning());
398 }
399 else if (actionCommand.equals("Reset"))
400 {
401 button.setEnabled(true);
402 }
403 else
404 {
405 this.logger.logp(Level.SEVERE, "ControlPanel", "fixButtons", "", new Exception("Unknown button?"));
406 }
407 }
408
409 }
410
411
412
413
414 public final void autoPauseSimulator()
415 {
416 if (getSimulator().isRunning())
417 {
418 getSimulator().stop();
419 double currentTick = getSimulator().getSimulatorTime().getTime().getSI();
420 double nextTick = getSimulator().getEventList().first().getAbsoluteExecutionTime().get().getSI();
421
422
423 if (nextTick > currentTick)
424 {
425
426
427
428
429 try
430 {
431 this.stopAtEvent =
432 scheduleEvent(new Time.Abs(nextTick, TimeUnit.SI), SimEventInterface.MAX_PRIORITY, this, this,
433 "autoPauseSimulator", null);
434 getSimulator().start();
435 }
436 catch (SimRuntimeException | RemoteException exception)
437 {
438 this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator",
439 "Caught an exception while trying to re-schedule an autoPauseEvent at the next real event");
440 }
441 }
442 else
443 {
444
445 if (SwingUtilities.isEventDispatchThread())
446 {
447
448 fixButtons();
449 }
450 else
451 {
452 try
453 {
454
455 SwingUtilities.invokeAndWait(new Runnable()
456 {
457 @Override
458 public void run()
459 {
460
461 fixButtons();
462
463 }
464 });
465 }
466 catch (Exception e)
467 {
468 if (e instanceof InterruptedException)
469 {
470 System.out.println("Caught " + e);
471
472 }
473 else
474 {
475 e.printStackTrace();
476 }
477 }
478 }
479 }
480 }
481 }
482
483
484 @Override
485 public final void propertyChange(final PropertyChangeEvent evt)
486 {
487
488 if (null != this.stopAtEvent)
489 {
490 getSimulator().cancelEvent(this.stopAtEvent);
491 this.stopAtEvent = null;
492 }
493 String newValue = (String) evt.getNewValue();
494 String[] fields = newValue.split("[:\\.]");
495 int hours = Integer.parseInt(fields[0]);
496 int minutes = Integer.parseInt(fields[1]);
497 int seconds = Integer.parseInt(fields[2]);
498 int fraction = Integer.parseInt(fields[3]);
499 double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
500 if (stopTime < getSimulator().getSimulatorTime().getTime().getSI())
501 {
502 return;
503 }
504 else
505 {
506 try
507 {
508 this.stopAtEvent =
509 scheduleEvent(new Time.Abs(stopTime, SECOND), SimEventInterface.MAX_PRIORITY, this, this,
510 "autoPauseSimulator", null);
511 }
512 catch (SimRuntimeException | RemoteException exception)
513 {
514 this.logger.logp(Level.SEVERE, "ControlPanel", "propertyChange",
515 "Caught an exception while trying to schedule an autoPauseSimulator event");
516 }
517 }
518
519 }
520
521
522
523
524 @SuppressWarnings("unchecked")
525 public final DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble> getSimulator()
526 {
527 return (DEVSSimulator<DoubleScalar.Abs<TimeUnit>, DoubleScalar.Rel<TimeUnit>, OTSSimTimeDouble>) this.simulator;
528 }
529
530
531 @Override
532 public void windowOpened(final WindowEvent e)
533 {
534
535 }
536
537
538 @Override
539 public final void windowClosing(final WindowEvent e)
540 {
541 if (this.simulator != null)
542 {
543 try
544 {
545 if (this.simulator.isRunning())
546 {
547 this.simulator.stop();
548 }
549 }
550 catch (RemoteException | SimRuntimeException exception)
551 {
552 exception.printStackTrace();
553 }
554 }
555 }
556
557
558 @Override
559 public final void windowClosed(final WindowEvent e)
560 {
561 cleanup();
562 }
563
564
565 @Override
566 public final void windowIconified(final WindowEvent e)
567 {
568
569 }
570
571
572 @Override
573 public final void windowDeiconified(final WindowEvent e)
574 {
575
576 }
577
578
579 @Override
580 public final void windowActivated(final WindowEvent e)
581 {
582
583 }
584
585
586 @Override
587 public final void windowDeactivated(final WindowEvent e)
588 {
589
590 }
591
592
593
594
595 public final Font getTimeFont()
596 {
597 return this.timeFont;
598 }
599
600
601 static class TimeWarpPanel extends JPanel
602 {
603
604 private static final long serialVersionUID = 20150408L;
605
606
607 private final JSlider slider;
608
609
610 private final int[] ratios;
611
612
613 private Map<Integer, Double> tickValues = new HashMap<>();
614
615
616
617
618
619
620
621
622
623
624
625 public TimeWarpPanel(final double minimum, final double maximum, final double initialValue,
626 final int ticksPerDecade, final DEVSSimulatorInterface<?, ?, ?> simulator)
627 {
628 if (minimum <= 0 || minimum > initialValue || initialValue > maximum)
629 {
630 throw new Error("Bad (combination of) minimum, maximum and initialValue; "
631 + "(restrictions: 0 < minimum <= initialValue <= maximum)");
632 }
633 switch (ticksPerDecade)
634 {
635 case 1:
636 this.ratios = new int[]{1};
637 break;
638 case 2:
639 this.ratios = new int[]{1, 3};
640 break;
641 case 3:
642 this.ratios = new int[]{1, 2, 5};
643 break;
644 default:
645 throw new Error("Bad ticksPerDecade value (must be 1, 2 or 3)");
646 }
647 int minimumTick = (int) Math.floor(Math.log10(minimum / initialValue) * ticksPerDecade);
648 int maximumTick = (int) Math.ceil(Math.log10(maximum / initialValue) * ticksPerDecade);
649 this.slider = new JSlider(SwingConstants.HORIZONTAL, minimumTick, maximumTick, 0);
650 this.slider.setPreferredSize(new Dimension(300, 45));
651 Hashtable<Integer, JLabel> labels = new Hashtable<Integer, JLabel>();
652 for (int step = 0; step <= maximumTick; step++)
653 {
654 StringBuilder text = new StringBuilder();
655 text.append(this.ratios[step % this.ratios.length]);
656 for (int decade = 0; decade < step / this.ratios.length; decade++)
657 {
658 text.append("0");
659 }
660 this.tickValues.put(step, Double.parseDouble(text.toString()));
661 labels.put(step, new JLabel(text.toString().replace("000", "K")));
662
663 }
664
665 String decimalSeparator =
666 "" + ((DecimalFormat) NumberFormat.getInstance()).getDecimalFormatSymbols().getDecimalSeparator();
667 for (int step = -1; step >= minimumTick; step--)
668 {
669 StringBuilder text = new StringBuilder();
670 text.append("0");
671 text.append(decimalSeparator);
672 for (int decade = (step + 1) / this.ratios.length; decade < 0; decade++)
673 {
674 text.append("0");
675 }
676 int index = step % this.ratios.length;
677 if (index < 0)
678 {
679 index += this.ratios.length;
680 }
681 text.append(this.ratios[index]);
682 labels.put(step, new JLabel(text.toString()));
683 this.tickValues.put(step, Double.parseDouble(text.toString()));
684
685 }
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 }