OTSControlPanel.java

  1. package org.opentrafficsim.gui;

  2. import java.awt.Container;
  3. import java.awt.Dimension;
  4. import java.awt.FlowLayout;
  5. import java.awt.Font;
  6. import java.awt.Rectangle;
  7. import java.awt.event.ActionEvent;
  8. import java.awt.event.ActionListener;
  9. import java.awt.event.WindowEvent;
  10. import java.awt.event.WindowListener;
  11. import java.beans.PropertyChangeEvent;
  12. import java.beans.PropertyChangeListener;
  13. import java.io.Serializable;
  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. import javax.swing.BoxLayout;
  29. import javax.swing.Icon;
  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. import nl.tudelft.simulation.dsol.SimRuntimeException;
  45. import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
  46. import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
  47. import nl.tudelft.simulation.dsol.simulators.DEVSRealTimeClock;
  48. import nl.tudelft.simulation.dsol.simulators.DEVSSimulator;
  49. import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
  50. import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
  51. import nl.tudelft.simulation.event.EventInterface;
  52. import nl.tudelft.simulation.event.EventListenerInterface;
  53. import nl.tudelft.simulation.language.io.URLResource;

  54. import org.djunits.unit.TimeUnit;
  55. import org.djunits.value.vdouble.scalar.Duration;
  56. import org.djunits.value.vdouble.scalar.Time;
  57. import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
  58. import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
  59. import org.opentrafficsim.simulationengine.WrappableAnimation;

  60. /**
  61.  * Peter's improved simulation control panel.
  62.  * <p>
  63.  * Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  64.  * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  65.  * <p>
  66.  * $LastChangedDate: 2017-04-17 17:27:27 +0200 (Mon, 17 Apr 2017) $, @version $Revision: 3447 $, by $Author: averbraeck $,
  67.  * initial version 11 dec. 2014 <br>
  68.  * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
  69.  * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  70.  */
  71. public class OTSControlPanel extends JPanel implements ActionListener, PropertyChangeListener, WindowListener,
  72.         EventListenerInterface
  73. {
  74.     /** */
  75.     private static final long serialVersionUID = 20150617L;

  76.     /** The simulator. */
  77.     private OTSDEVSSimulatorInterface simulator;

  78.     /** The WrappableAnimation (needed for restart operation). */
  79.     private final WrappableAnimation wrappableAnimation;

  80.     /** Logger. */
  81.     private final Logger logger;

  82.     /** The clock. */
  83.     private final ClockPanel clockPanel;

  84.     /** The time warp control. */
  85.     private final TimeWarpPanel timeWarpPanel;

  86.     /** The control buttons. */
  87.     private final ArrayList<JButton> buttons = new ArrayList<JButton>();

  88.     /** Font used to display the clock and the stop time. */
  89.     private final Font timeFont = new Font("SansSerif", Font.BOLD, 18);

  90.     /** The TimeEdit that lets the user set a time when the simulation will be stopped. */
  91.     private final TimeEdit timeEdit;

  92.     /** The currently registered stop at event. */
  93.     private SimEvent<OTSSimTimeDouble> stopAtEvent = null;

  94.     /** Has the window close handler been registered? */
  95.     protected boolean closeHandlerRegistered = false;

  96.     /** Has cleanup taken place? */
  97.     private boolean isCleanUp = false;

  98.     /**
  99.      * Decorate a SimpleSimulator with a different set of control buttons.
  100.      * @param simulator SimpleSimulator; the simulator
  101.      * @param wrappableAnimation WrappableAnimation; if non-null, the restart button should work
  102.      * @throws RemoteException when simulator cannot be accessed for listener attachment
  103.      */
  104.     public OTSControlPanel(final OTSDEVSSimulatorInterface simulator, final WrappableAnimation wrappableAnimation)
  105.             throws RemoteException
  106.     {
  107.         this.simulator = simulator;
  108.         this.wrappableAnimation = wrappableAnimation;
  109.         this.logger = Logger.getLogger("nl.tudelft.opentrafficsim");

  110.         this.setLayout(new FlowLayout(FlowLayout.LEFT));
  111.         JPanel buttonPanel = new JPanel();
  112.         buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
  113.         buttonPanel.add(makeButton("stepButton", "/Last_recor.png", "Step", "Execute one event", true));
  114.         buttonPanel.add(makeButton("nextTimeButton", "/NextTrack.png", "NextTime",
  115.                 "Execute all events scheduled for the current time", true));
  116.         buttonPanel.add(makeButton("runPauseButton", "/Play.png", "RunPause", "XXX", true));
  117.         this.timeWarpPanel = new TimeWarpPanel(0.1, 1000, 1, 3, simulator);
  118.         buttonPanel.add(this.timeWarpPanel);
  119.         buttonPanel.add(makeButton("resetButton", "/Undo.png", "Reset", "Reset the simulation", false));
  120.         this.clockPanel = new ClockPanel();
  121.         buttonPanel.add(this.clockPanel);
  122.         this.timeEdit = new TimeEdit(new Time(0, TimeUnit.SECOND));
  123.         this.timeEdit.addPropertyChangeListener("value", this);
  124.         buttonPanel.add(this.timeEdit);
  125.         this.add(buttonPanel);
  126.         fixButtons();
  127.         installWindowCloseHandler();
  128.         this.simulator.addListener(this, SimulatorInterface.END_OF_REPLICATION_EVENT);
  129.         this.simulator.addListener(this, SimulatorInterface.START_EVENT);
  130.         this.simulator.addListener(this, SimulatorInterface.STOP_EVENT);
  131.         this.simulator.addListener(this, DEVSRealTimeClock.CHANGE_SPEED_FACTOR_EVENT);
  132.     }

  133.     /**
  134.      * Create a button.
  135.      * @param name String; name of the button
  136.      * @param iconPath String; path to the resource
  137.      * @param actionCommand String; the action command
  138.      * @param toolTipText String; the hint to show when the mouse hovers over the button
  139.      * @param enabled boolean; true if the new button must initially be enable; false if it must initially be disabled
  140.      * @return JButton
  141.      */
  142.     private JButton makeButton(final String name, final String iconPath, final String actionCommand, final String toolTipText,
  143.             final boolean enabled)
  144.     {
  145.         // JButton result = new JButton(new ImageIcon(this.getClass().getResource(iconPath)));
  146.         JButton result = new JButton(loadIcon(iconPath));
  147.         result.setName(name);
  148.         result.setEnabled(enabled);
  149.         result.setActionCommand(actionCommand);
  150.         result.setToolTipText(toolTipText);
  151.         result.addActionListener(this);
  152.         this.buttons.add(result);
  153.         return result;
  154.     }

  155.     /**
  156.      * Attempt to load and return an icon.
  157.      * @param iconPath String; the path that is used to load the icon
  158.      * @return Icon; or null if loading failed
  159.      */
  160.     public static final Icon loadIcon(final String iconPath)
  161.     {
  162.         try
  163.         {
  164.             return new ImageIcon(URLResource.getResource(iconPath));
  165.         }
  166.         catch (NullPointerException npe)
  167.         {
  168.             System.err.println("Could not load icon from path " + iconPath);
  169.             return null;
  170.         }
  171.     }

  172.     /**
  173.      * Construct and schedule a SimEvent using a Time to specify the execution time.
  174.      * @param executionTime Time; the time at which the event must happen
  175.      * @param priority short; should be between <cite>SimEventInterface.MAX_PRIORITY</cite> and
  176.      *            <cite>SimEventInterface.MIN_PRIORITY</cite>; most normal events should use
  177.      *            <cite>SimEventInterface.NORMAL_PRIORITY</cite>
  178.      * @param source Object; the object that creates/schedules the event
  179.      * @param eventTarget Object; the object that must execute the event
  180.      * @param method String; the name of the method of <code>target</code> that must execute the event
  181.      * @param args Object[]; the arguments of the <code>method</code> that must execute the event
  182.      * @return SimEvent&lt;OTSSimTimeDouble&gt;; the event that was scheduled (the caller should save this if a need to cancel
  183.      *         the event may arise later)
  184.      * @throws SimRuntimeException when the <code>executionTime</code> is in the past
  185.      */
  186.     private SimEvent<OTSSimTimeDouble> scheduleEvent(final Time executionTime, final short priority, final Object source,
  187.             final Object eventTarget, final String method, final Object[] args) throws SimRuntimeException
  188.     {
  189.         SimEvent<OTSSimTimeDouble> simEvent =
  190.                 new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new Time(executionTime.getSI(), TimeUnit.SECOND)),
  191.                         priority, source, eventTarget, method, args);
  192.         this.simulator.scheduleEvent(simEvent);
  193.         return simEvent;
  194.     }

  195.     /**
  196.      * Install a handler for the window closed event that stops the simulator (if it is running).
  197.      */
  198.     public final void installWindowCloseHandler()
  199.     {
  200.         if (this.closeHandlerRegistered)
  201.         {
  202.             return;
  203.         }

  204.         // make sure the root frame gets disposed of when the closing X icon is pressed.
  205.         new DisposeOnCloseThread(this).start();
  206.     }

  207.     /** Install the dispose on close when the OTSControlPanel is registered as part of a frame. */
  208.     protected class DisposeOnCloseThread extends Thread
  209.     {
  210.         /** The current container. */
  211.         private OTSControlPanel panel;

  212.         /**
  213.          * @param panel the OTSControlpanel container.
  214.          */
  215.         public DisposeOnCloseThread(final OTSControlPanel panel)
  216.         {
  217.             super();
  218.             this.panel = panel;
  219.         }

  220.         /** {@inheritDoc} */
  221.         @Override
  222.         public final void run()
  223.         {
  224.             Container root = this.panel;
  225.             while (!(root instanceof JFrame))
  226.             {
  227.                 try
  228.                 {
  229.                     Thread.sleep(10);
  230.                 }
  231.                 catch (InterruptedException exception)
  232.                 {
  233.                     // nothing to do
  234.                 }

  235.                 // Search towards the root of the Swing components until we find a JFrame
  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.             // frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
  246.         }

  247.         /** {@inheritDoc} */
  248.         @Override
  249.         public final String toString()
  250.         {
  251.             return "DisposeOnCloseThread [panel=" + this.panel + "]";
  252.         }
  253.     }

  254.     /** {@inheritDoc} */
  255.     @Override
  256.     public final void actionPerformed(final ActionEvent actionEvent)
  257.     {
  258.         String actionCommand = actionEvent.getActionCommand();
  259.         // System.out.println("actionCommand: " + actionCommand);
  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.                 // System.out.println("now is " + now);
  289.                 try
  290.                 {
  291.                     this.stopAtEvent =
  292.                             scheduleEvent(new Time(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.                 if (null == OTSControlPanel.this.wrappableAnimation)
  309.                 {
  310.                     throw new RuntimeException("Do not know how to restart this simulation");
  311.                 }

  312.                 // find the JFrame position and dimensions
  313.                 Container root = OTSControlPanel.this;
  314.                 while (!(root instanceof JFrame))
  315.                 {
  316.                     root = root.getParent();
  317.                 }
  318.                 JFrame frame = (JFrame) root;
  319.                 Rectangle rect = frame.getBounds();
  320.                 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
  321.                 frame.dispose();
  322.                 OTSControlPanel.this.cleanup();
  323.                 try
  324.                 {
  325.                     OTSControlPanel.this.wrappableAnimation.rebuildSimulator(rect);
  326.                 }
  327.                 catch (Exception exception)
  328.                 {
  329.                     exception.printStackTrace();
  330.                 }
  331.             }
  332.             fixButtons();
  333.         }
  334.         catch (Exception exception)
  335.         {
  336.             exception.printStackTrace();
  337.         }
  338.     }

  339.     /**
  340.      * clean up timers, contexts, threads, etc. that could prevent garbage collection.
  341.      */
  342.     private void cleanup()
  343.     {
  344.         if (!this.isCleanUp)
  345.         {
  346.             this.isCleanUp = true;
  347.             try
  348.             {
  349.                 if (this.simulator != null)
  350.                 {
  351.                     if (this.simulator.isRunning())
  352.                     {
  353.                         this.simulator.stop();
  354.                     }

  355.                     // unbind the old animation and statistics
  356.                     getSimulator().getReplication().getExperiment().removeFromContext(); // clean up the context
  357.                     getSimulator().cleanUp();
  358.                 }

  359.                 if (this.clockPanel != null)
  360.                 {
  361.                     this.clockPanel.cancelTimer(); // cancel the timer on the clock panel.
  362.                 }

  363.                 if (this.wrappableAnimation != null)
  364.                 {
  365.                     this.wrappableAnimation.stopTimersThreads(); // stop the linked timers and other threads
  366.                 }
  367.             }
  368.             catch (Throwable exception)
  369.             {
  370.                 exception.printStackTrace();
  371.             }
  372.         }
  373.     }

  374.     /**
  375.      * Update the enabled state of all the buttons.
  376.      */
  377.     protected final void fixButtons()
  378.     {
  379.         // System.out.println("FixButtons entered");
  380.         final boolean moreWorkToDo = getSimulator().getEventList().size() > 0;
  381.         for (JButton button : this.buttons)
  382.         {
  383.             final String actionCommand = button.getActionCommand();
  384.             if (actionCommand.equals("Step"))
  385.             {
  386.                 button.setEnabled(moreWorkToDo);
  387.             }
  388.             else if (actionCommand.equals("RunPause"))
  389.             {
  390.                 button.setEnabled(moreWorkToDo);
  391.                 if (this.simulator.isRunning())
  392.                 {
  393.                     button.setToolTipText("Pause the simulation");
  394.                     button.setIcon(OTSControlPanel.loadIcon("/Pause.png"));
  395.                 }
  396.                 else
  397.                 {
  398.                     button.setToolTipText("Run the simulation at the indicated speed");
  399.                     button.setIcon(loadIcon("/Play.png"));
  400.                 }
  401.                 button.setEnabled(moreWorkToDo);
  402.             }
  403.             else if (actionCommand.equals("NextTime"))
  404.             {
  405.                 button.setEnabled(moreWorkToDo);
  406.             }
  407.             else if (actionCommand.equals("Reset"))
  408.             {
  409.                 button.setEnabled(true); // FIXME: should be disabled when the simulator was just reset or initialized
  410.             }
  411.             else
  412.             {
  413.                 this.logger.logp(Level.SEVERE, "ControlPanel", "fixButtons", "", new Exception("Unknown button?"));
  414.             }
  415.         }
  416.         // System.out.println("FixButtons finishing");
  417.     }

  418.     /**
  419.      * Pause the simulator.
  420.      */
  421.     public final void autoPauseSimulator()
  422.     {
  423.         if (getSimulator().isRunning())
  424.         {
  425.             getSimulator().stop();
  426.             double currentTick = getSimulator().getSimulatorTime().getTime().getSI();
  427.             double nextTick = getSimulator().getEventList().first().getAbsoluteExecutionTime().get().getSI();
  428.             // System.out.println("currentTick is " + currentTick);
  429.             // System.out.println("nextTick is " + nextTick);
  430.             if (nextTick > currentTick)
  431.             {
  432.                 // The clock is now just beyond where it was when the user requested the NextTime operation
  433.                 // Insert another autoPauseSimulator event just before what is now the time of the next event
  434.                 // and let the simulator time increment to that time
  435.                 // System.out.println("Re-Scheduling at " + nextTick);
  436.                 try
  437.                 {
  438.                     this.stopAtEvent =
  439.                             scheduleEvent(new Time(nextTick, TimeUnit.SI), SimEventInterface.MAX_PRIORITY, this, this,
  440.                                     "autoPauseSimulator", null);
  441.                     getSimulator().start();
  442.                 }
  443.                 catch (SimRuntimeException exception)
  444.                 {
  445.                     this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator",
  446.                             "Caught an exception while trying to re-schedule an autoPauseEvent at the next real event");
  447.                 }
  448.             }
  449.             else
  450.             {
  451.                 // System.out.println("Not re-scheduling");
  452.                 if (SwingUtilities.isEventDispatchThread())
  453.                 {
  454.                     // System.out.println("Already on EventDispatchThread");
  455.                     fixButtons();
  456.                 }
  457.                 else
  458.                 {
  459.                     try
  460.                     {
  461.                         // System.out.println("Current thread is NOT EventDispatchThread: " + Thread.currentThread());
  462.                         SwingUtilities.invokeAndWait(new Runnable()
  463.                         {
  464.                             @Override
  465.                             public void run()
  466.                             {
  467.                                 // System.out.println("Runnable started");
  468.                                 fixButtons();
  469.                                 // System.out.println("Runnable finishing");
  470.                             }
  471.                         });
  472.                     }
  473.                     catch (Exception e)
  474.                     {
  475.                         if (e instanceof InterruptedException)
  476.                         {
  477.                             System.out.println("Caught " + e);
  478.                             // e.printStackTrace();
  479.                         }
  480.                         else
  481.                         {
  482.                             e.printStackTrace();
  483.                         }
  484.                     }
  485.                 }
  486.             }
  487.         }
  488.     }

  489.     /** {@inheritDoc} */
  490.     @Override
  491.     public final void propertyChange(final PropertyChangeEvent evt)
  492.     {
  493.         // System.out.println("PropertyChanged: " + evt);
  494.         if (null != this.stopAtEvent)
  495.         {
  496.             getSimulator().cancelEvent(this.stopAtEvent); // silently ignore false result
  497.             this.stopAtEvent = null;
  498.         }
  499.         String newValue = (String) evt.getNewValue();
  500.         String[] fields = newValue.split("[:\\.]");
  501.         int hours = Integer.parseInt(fields[0]);
  502.         int minutes = Integer.parseInt(fields[1]);
  503.         int seconds = Integer.parseInt(fields[2]);
  504.         int fraction = Integer.parseInt(fields[3]);
  505.         double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
  506.         if (stopTime < getSimulator().getSimulatorTime().getTime().getSI())
  507.         {
  508.             return;
  509.         }
  510.         else
  511.         {
  512.             try
  513.             {
  514.                 this.stopAtEvent =
  515.                         scheduleEvent(new Time(stopTime, TimeUnit.SECOND), SimEventInterface.MAX_PRIORITY, this, this,
  516.                                 "autoPauseSimulator", null);
  517.             }
  518.             catch (SimRuntimeException exception)
  519.             {
  520.                 this.logger.logp(Level.SEVERE, "ControlPanel", "propertyChange",
  521.                         "Caught an exception while trying to schedule an autoPauseSimulator event");
  522.             }
  523.         }
  524.     }

  525.     /**
  526.      * @return simulator.
  527.      */
  528.     @SuppressWarnings("unchecked")
  529.     public final DEVSSimulator<Time, Duration, OTSSimTimeDouble> getSimulator()
  530.     {
  531.         return (DEVSSimulator<Time, Duration, OTSSimTimeDouble>) this.simulator;
  532.     }

  533.     /** {@inheritDoc} */
  534.     @Override
  535.     public void windowOpened(final WindowEvent e)
  536.     {
  537.         // No action
  538.     }

  539.     /** {@inheritDoc} */
  540.     @Override
  541.     public final void windowClosing(final WindowEvent e)
  542.     {
  543.         if (this.simulator != null)
  544.         {
  545.             try
  546.             {
  547.                 if (this.simulator.isRunning())
  548.                 {
  549.                     this.simulator.stop();
  550.                 }
  551.             }
  552.             catch (SimRuntimeException exception)
  553.             {
  554.                 exception.printStackTrace();
  555.             }
  556.         }
  557.     }

  558.     /** {@inheritDoc} */
  559.     @Override
  560.     public final void windowClosed(final WindowEvent e)
  561.     {
  562.         cleanup();
  563.     }

  564.     /** {@inheritDoc} */
  565.     @Override
  566.     public final void windowIconified(final WindowEvent e)
  567.     {
  568.         // No action
  569.     }

  570.     /** {@inheritDoc} */
  571.     @Override
  572.     public final void windowDeiconified(final WindowEvent e)
  573.     {
  574.         // No action
  575.     }

  576.     /** {@inheritDoc} */
  577.     @Override
  578.     public final void windowActivated(final WindowEvent e)
  579.     {
  580.         // No action
  581.     }

  582.     /** {@inheritDoc} */
  583.     @Override
  584.     public final void windowDeactivated(final WindowEvent e)
  585.     {
  586.         // No action
  587.     }

  588.     /**
  589.      * @return timeFont.
  590.      */
  591.     public final Font getTimeFont()
  592.     {
  593.         return this.timeFont;
  594.     }

  595.     /** JPanel that contains a JSider that uses a logarithmic scale. */
  596.     static class TimeWarpPanel extends JPanel
  597.     {
  598.         /** */
  599.         private static final long serialVersionUID = 20150408L;

  600.         /** The JSlider that the user sees. */
  601.         private final JSlider slider;

  602.         /** The ratios used in each decade. */
  603.         private final int[] ratios;

  604.         /** The values at each tick. */
  605.         private Map<Integer, Double> tickValues = new HashMap<>();

  606.         /**
  607.          * Construct a new TimeWarpPanel.
  608.          * @param minimum double; the minimum value on the scale (the displayed scale may extend a little further than this
  609.          *            value)
  610.          * @param maximum double; the maximum value on the scale (the displayed scale may extend a little further than this
  611.          *            value)
  612.          * @param initialValue double; the initially selected value on the scale
  613.          * @param ticksPerDecade int; the number of steps per decade
  614.          * @param simulator SimpleSimulator; the simulator to change the speed of
  615.          */
  616.         TimeWarpPanel(final double minimum, final double maximum, final double initialValue, final int ticksPerDecade,
  617.                 final DEVSSimulatorInterface<?, ?, ?> simulator)
  618.         {
  619.             if (minimum <= 0 || minimum > initialValue || initialValue > maximum)
  620.             {
  621.                 throw new RuntimeException("Bad (combination of) minimum, maximum and initialValue; "
  622.                         + "(restrictions: 0 < minimum <= initialValue <= maximum)");
  623.             }
  624.             switch (ticksPerDecade)
  625.             {
  626.                 case 1:
  627.                     this.ratios = new int[] { 1 };
  628.                     break;
  629.                 case 2:
  630.                     this.ratios = new int[] { 1, 3 };
  631.                     break;
  632.                 case 3:
  633.                     this.ratios = new int[] { 1, 2, 5 };
  634.                     break;
  635.                 default:
  636.                     throw new RuntimeException("Bad ticksPerDecade value (must be 1, 2 or 3)");
  637.             }
  638.             int minimumTick = (int) Math.floor(Math.log10(minimum / initialValue) * ticksPerDecade);
  639.             int maximumTick = (int) Math.ceil(Math.log10(maximum / initialValue) * ticksPerDecade);
  640.             this.slider = new JSlider(SwingConstants.HORIZONTAL, minimumTick, maximumTick + 1, 0);
  641.             this.slider.setPreferredSize(new Dimension(350, 45));
  642.             Hashtable<Integer, JLabel> labels = new Hashtable<Integer, JLabel>();
  643.             for (int step = 0; step <= maximumTick; step++)
  644.             {
  645.                 StringBuilder text = new StringBuilder();
  646.                 text.append(this.ratios[step % this.ratios.length]);
  647.                 for (int decade = 0; decade < step / this.ratios.length; decade++)
  648.                 {
  649.                     text.append("0");
  650.                 }
  651.                 this.tickValues.put(step, Double.parseDouble(text.toString()));
  652.                 labels.put(step, new JLabel(text.toString().replace("000", "K")));
  653.                 // System.out.println("Label " + step + " is \"" + text.toString() + "\"");
  654.             }
  655.             // Figure out the DecimalSymbol
  656.             String decimalSeparator =
  657.                     "" + ((DecimalFormat) NumberFormat.getInstance()).getDecimalFormatSymbols().getDecimalSeparator();
  658.             for (int step = -1; step >= minimumTick; step--)
  659.             {
  660.                 StringBuilder text = new StringBuilder();
  661.                 text.append("0");
  662.                 text.append(decimalSeparator);
  663.                 for (int decade = (step + 1) / this.ratios.length; decade < 0; decade++)
  664.                 {
  665.                     text.append("0");
  666.                 }
  667.                 int index = step % this.ratios.length;
  668.                 if (index < 0)
  669.                 {
  670.                     index += this.ratios.length;
  671.                 }
  672.                 text.append(this.ratios[index]);
  673.                 labels.put(step, new JLabel(text.toString()));
  674.                 this.tickValues.put(step, Double.parseDouble(text.toString()));
  675.                 // System.out.println("Label " + step + " is \"" + text.toString() + "\"");
  676.             }
  677.             labels.put(maximumTick + 1, new JLabel("\u221E"));
  678.             this.tickValues.put(maximumTick + 1, 1E9);
  679.             this.slider.setLabelTable(labels);
  680.             this.slider.setPaintLabels(true);
  681.             this.slider.setPaintTicks(true);
  682.             this.slider.setMajorTickSpacing(1);
  683.             this.add(this.slider);
  684.             /*- Uncomment to verify the stepToFactor method.
  685.             for (int i = this.slider.getMinimum(); i <= this.slider.getMaximum(); i++)
  686.             {
  687.                 System.out.println("pos=" + i + " value is " + stepToFactor(i));
  688.             }
  689.              */

  690.             // initial value of simulation speed
  691.             if (simulator instanceof DEVSRealTimeClock)
  692.             {
  693.                 DEVSRealTimeClock<?, ?, ?> clock = (DEVSRealTimeClock<?, ?, ?>) simulator;
  694.                 clock.setSpeedFactor(TimeWarpPanel.this.tickValues.get(this.slider.getValue()));
  695.             }

  696.             // adjust the simulation speed
  697.             this.slider.addChangeListener(new ChangeListener()
  698.             {
  699.                 public void stateChanged(final ChangeEvent ce)
  700.                 {
  701.                     JSlider source = (JSlider) ce.getSource();
  702.                     if (!source.getValueIsAdjusting() && simulator instanceof DEVSRealTimeClock)
  703.                     {
  704.                         DEVSRealTimeClock<?, ?, ?> clock = (DEVSRealTimeClock<?, ?, ?>) simulator;
  705.                         clock.setSpeedFactor(((TimeWarpPanel) source.getParent()).getTickValues().get(source.getValue()));
  706.                     }
  707.                 }
  708.             });
  709.         }

  710.         /**
  711.          * Access to tickValues map from within the event handler.
  712.          * @return Map&lt;Integer, Double&gt; the tickValues map of this TimeWarpPanel
  713.          */
  714.         protected Map<Integer, Double> getTickValues()
  715.         {
  716.             return this.tickValues;
  717.         }

  718.         /**
  719.          * Convert a position on the slider to a factor.
  720.          * @param step int; the position on the slider
  721.          * @return double; the factor that corresponds to step
  722.          */
  723.         private double stepToFactor(final int step)
  724.         {
  725.             int index = step % this.ratios.length;
  726.             if (index < 0)
  727.             {
  728.                 index += this.ratios.length;
  729.             }
  730.             double result = this.ratios[index];
  731.             // Make positive to avoid trouble with negative values that round towards 0 on division
  732.             int power = (step + 1000 * this.ratios.length) / this.ratios.length - 1000; // This is ugly
  733.             while (power > 0)
  734.             {
  735.                 result *= 10;
  736.                 power--;
  737.             }
  738.             while (power < 0)
  739.             {
  740.                 result /= 10;
  741.                 power++;
  742.             }
  743.             return result;
  744.         }

  745.         /**
  746.          * Retrieve the current TimeWarp factor.
  747.          * @return double; the current TimeWarp factor
  748.          */
  749.         public final double getFactor()
  750.         {
  751.             return stepToFactor(this.slider.getValue());
  752.         }

  753.         /** {@inheritDoc} */
  754.         @Override
  755.         public final String toString()
  756.         {
  757.             return "TimeWarpPanel [timeWarp=" + this.getFactor() + "]";
  758.         }

  759.         /**
  760.          * Set the time warp factor to the best possible approximation of a given value.
  761.          * @param factor double; the requested speed factor
  762.          */
  763.         public void setSpeedFactor(final double factor)
  764.         {
  765.             int bestStep = -1;
  766.             double bestError = Double.MAX_VALUE;
  767.             for (int step = this.slider.getMinimum(); step < this.slider.getMaximum(); step++)
  768.             {
  769.                 double ratio = getTickValues().get(step);// stepToFactor(step);
  770.                 double logError = Math.abs(Math.log(factor / ratio));
  771.                 if (logError < bestError)
  772.                 {
  773.                     bestStep = step;
  774.                     bestError = logError;
  775.                 }
  776.             }
  777.             System.out.println("setSpeedfactor: factor is " + factor + ", best slider value is " + bestStep
  778.                     + " current value is " + this.slider.getValue());
  779.             if (this.slider.getValue() != bestStep && factor < 1.0E6)
  780.             {
  781.                 this.slider.setValue(bestStep);
  782.             }
  783.         }
  784.     }

  785.     /** JLabel that displays the simulation time. */
  786.     class ClockPanel extends JLabel
  787.     {
  788.         /** */
  789.         private static final long serialVersionUID = 20141211L;

  790.         /** The JLabel that displays the time. */
  791.         private final JLabel clockLabel;

  792.         /** The timer (so we can cancel it). */
  793.         private Timer timer;

  794.         /** Timer update interval in msec. */
  795.         private static final long UPDATEINTERVAL = 1000;

  796.         /** Construct a clock panel. */
  797.         ClockPanel()
  798.         {
  799.             super("00:00:00.000");
  800.             this.clockLabel = this;
  801.             this.setFont(getTimeFont());
  802.             this.timer = new Timer();
  803.             this.timer.scheduleAtFixedRate(new TimeUpdateTask(), 0, ClockPanel.UPDATEINTERVAL);
  804.         }

  805.         /**
  806.          * Cancel the timer task.
  807.          */
  808.         public void cancelTimer()
  809.         {
  810.             if (this.timer != null)
  811.             {
  812.                 this.timer.cancel();
  813.             }
  814.             this.timer = null;
  815.         }

  816.         /** Updater for the clock panel. */
  817.         private class TimeUpdateTask extends TimerTask implements Serializable
  818.         {
  819.             /** */
  820.             private static final long serialVersionUID = 20140000L;

  821.             /**
  822.              * Create a TimeUpdateTask.
  823.              */
  824.             TimeUpdateTask()
  825.             {
  826.             }

  827.             /** {@inheritDoc} */
  828.             @Override
  829.             public void run()
  830.             {
  831.                 double now = Math.round(getSimulator().getSimulatorTime().getTime().getSI() * 1000) / 1000d;
  832.                 int seconds = (int) Math.floor(now);
  833.                 int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
  834.                 getClockLabel().setText(
  835.                         String.format("  %02d:%02d:%02d.%03d  ", seconds / 3600, seconds / 60 % 60, seconds % 60,
  836.                                 fractionalSeconds));
  837.                 getClockLabel().repaint();
  838.             }

  839.             /** {@inheritDoc} */
  840.             @Override
  841.             public final String toString()
  842.             {
  843.                 return "TimeUpdateTask of ClockPanel";
  844.             }
  845.         }

  846.         /**
  847.          * @return clockLabel.
  848.          */
  849.         protected JLabel getClockLabel()
  850.         {
  851.             return this.clockLabel;
  852.         }

  853.         /** {@inheritDoc} */
  854.         @Override
  855.         public final String toString()
  856.         {
  857.             return "ClockPanel [clockLabel=" + this.clockLabel + ", time=" + getText() + "]";
  858.         }

  859.     }

  860.     /** Entry field for time. */
  861.     class TimeEdit extends JFormattedTextField
  862.     {
  863.         /** */
  864.         private static final long serialVersionUID = 20141212L;

  865.         /**
  866.          * Construct a new TimeEdit.
  867.          * @param initialValue Time; the initial value for the TimeEdit
  868.          */
  869.         TimeEdit(final Time initialValue)
  870.         {
  871.             super(new RegexFormatter("\\d\\d\\d\\d:[0-5]\\d:[0-5]\\d\\.\\d\\d\\d"));
  872.             MaskFormatter mf = null;
  873.             try
  874.             {
  875.                 mf = new MaskFormatter("####:##:##.###");
  876.                 mf.setPlaceholderCharacter('0');
  877.                 mf.setAllowsInvalid(false);
  878.                 mf.setCommitsOnValidEdit(true);
  879.                 mf.setOverwriteMode(true);
  880.                 mf.install(this);
  881.             }
  882.             catch (ParseException exception)
  883.             {
  884.                 exception.printStackTrace();
  885.             }
  886.             setTime(initialValue);
  887.             setFont(getTimeFont());
  888.         }

  889.         /**
  890.          * Set or update the time shown in this TimeEdit.
  891.          * @param newValue Time; the (new) value to set/show in this TimeEdit
  892.          */
  893.         public void setTime(final Time newValue)
  894.         {
  895.             double v = newValue.getSI();
  896.             int integerPart = (int) Math.floor(v);
  897.             int fraction = (int) Math.floor((v - integerPart) * 1000);
  898.             String text =
  899.                     String.format("%04d:%02d:%02d.%03d", integerPart / 3600, integerPart / 60 % 60, integerPart % 60, fraction);
  900.             this.setText(text);
  901.         }

  902.         /** {@inheritDoc} */
  903.         @Override
  904.         public final String toString()
  905.         {
  906.             return "TimeEdit [time=" + getText() + "]";
  907.         }
  908.     }

  909.     /**
  910.      * Extension of a DefaultFormatter that uses a regular expression. <br>
  911.      * Derived from <a href="http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm">
  912.      * http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm</a>
  913.      * <p>
  914.      * $LastChangedDate: 2017-04-17 17:27:27 +0200 (Mon, 17 Apr 2017) $, @version $Revision: 3447 $, by $Author: averbraeck $,
  915.      * initial version 2 dec. 2014 <br>
  916.      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  917.      */
  918.     static class RegexFormatter extends DefaultFormatter
  919.     {
  920.         /** */
  921.         private static final long serialVersionUID = 20141212L;

  922.         /** The regular expression pattern. */
  923.         private Pattern pattern;

  924.         /**
  925.          * Create a new RegexFormatter.
  926.          * @param pattern String; regular expression pattern that defines what this RexexFormatter will accept
  927.          */
  928.         RegexFormatter(final String pattern)
  929.         {
  930.             this.pattern = Pattern.compile(pattern);
  931.         }

  932.         @Override
  933.         public Object stringToValue(final String text) throws ParseException
  934.         {
  935.             Matcher matcher = this.pattern.matcher(text);
  936.             if (matcher.matches())
  937.             {
  938.                 // System.out.println("String \"" + text + "\" matches");
  939.                 return super.stringToValue(text);
  940.             }
  941.             // System.out.println("String \"" + text + "\" does not match");
  942.             throw new ParseException("Pattern did not match", 0);
  943.         }

  944.         /** {@inheritDoc} */
  945.         @Override
  946.         public final String toString()
  947.         {
  948.             return "RegexFormatter [pattern=" + this.pattern + "]";
  949.         }
  950.     }

  951.     /** {@inheritDoc} */
  952.     @Override
  953.     public final void notify(final EventInterface event) throws RemoteException
  954.     {
  955.         if (event.getType().equals(SimulatorInterface.END_OF_REPLICATION_EVENT)
  956.                 || event.getType().equals(SimulatorInterface.START_EVENT)
  957.                 || event.getType().equals(SimulatorInterface.STOP_EVENT)
  958.                 || event.getType().equals(DEVSRealTimeClock.CHANGE_SPEED_FACTOR_EVENT))
  959.         {
  960.             if (event.getType().equals(DEVSRealTimeClock.CHANGE_SPEED_FACTOR_EVENT))
  961.             {
  962.                 this.timeWarpPanel.setSpeedFactor((Double) event.getContent());
  963.             }
  964.             fixButtons();
  965.         }
  966.     }

  967.     /** {@inheritDoc} */
  968.     @Override
  969.     public final String toString()
  970.     {
  971.         return "OTSControlPanel [simulatorTime=" + this.simulator.getSimulatorTime().getTime() + ", timeWarp="
  972.                 + this.timeWarpPanel.getFactor() + ", stopAtEvent=" + this.stopAtEvent + "]";
  973.     }

  974. }