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