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