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.ImageIcon;
  30. import javax.swing.JButton;
  31. import javax.swing.JFormattedTextField;
  32. import javax.swing.JFrame;
  33. import javax.swing.JLabel;
  34. import javax.swing.JPanel;
  35. import javax.swing.JSlider;
  36. import javax.swing.SwingConstants;
  37. import javax.swing.SwingUtilities;
  38. import javax.swing.WindowConstants;
  39. import javax.swing.event.ChangeEvent;
  40. import javax.swing.event.ChangeListener;
  41. import javax.swing.text.DefaultFormatter;
  42. import javax.swing.text.MaskFormatter;

  43. import org.djunits.unit.TimeUnit;
  44. import org.djunits.value.vdouble.scalar.Duration;
  45. import org.djunits.value.vdouble.scalar.Time;
  46. import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
  47. import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
  48. import org.opentrafficsim.simulationengine.WrappableAnimation;

  49. import nl.tudelft.simulation.dsol.SimRuntimeException;
  50. import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEvent;
  51. import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
  52. import nl.tudelft.simulation.dsol.simulators.DEVSRealTimeClock;
  53. import nl.tudelft.simulation.dsol.simulators.DEVSSimulator;
  54. import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
  55. import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
  56. import nl.tudelft.simulation.event.EventInterface;
  57. import nl.tudelft.simulation.event.EventListenerInterface;
  58. import nl.tudelft.simulation.language.io.URLResource;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  154.     /**
  155.      * Construct and schedule a SimEvent using a Time to specify the execution time.
  156.      * @param executionTime Time; the time at which the event must happen
  157.      * @param priority short; should be between <cite>SimEventInterface.MAX_PRIORITY</cite> and
  158.      *            <cite>SimEventInterface.MIN_PRIORITY</cite>; most normal events should use
  159.      *            <cite>SimEventInterface.NORMAL_PRIORITY</cite>
  160.      * @param source Object; the object that creates/schedules the event
  161.      * @param eventTarget Object; the object that must execute the event
  162.      * @param method String; the name of the method of <code>target</code> that must execute the event
  163.      * @param args Object[]; the arguments of the <code>method</code> that must execute the event
  164.      * @return SimEvent&lt;OTSSimTimeDouble&gt;; the event that was scheduled (the caller should save this if a need to cancel
  165.      *         the event may arise later)
  166.      * @throws SimRuntimeException when the <code>executionTime</code> is in the past
  167.      */
  168.     private SimEvent<OTSSimTimeDouble> scheduleEvent(final Time executionTime, final short priority, final Object source,
  169.             final Object eventTarget, final String method, final Object[] args) throws SimRuntimeException
  170.     {
  171.         SimEvent<OTSSimTimeDouble> simEvent =
  172.                 new SimEvent<OTSSimTimeDouble>(new OTSSimTimeDouble(new Time(executionTime.getSI(), TimeUnit.SECOND)),
  173.                         priority, source, eventTarget, method, args);
  174.         this.simulator.scheduleEvent(simEvent);
  175.         return simEvent;
  176.     }

  177.     /**
  178.      * Install a handler for the window closed event that stops the simulator (if it is running).
  179.      */
  180.     public final void installWindowCloseHandler()
  181.     {
  182.         if (this.closeHandlerRegistered)
  183.         {
  184.             return;
  185.         }

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

  189.     /** Install the dispose on close when the OTSControlPanel is registered as part of a frame. */
  190.     protected class DisposeOnCloseThread extends Thread
  191.     {
  192.         /** The current container. */
  193.         private OTSControlPanel panel;

  194.         /**
  195.          * @param panel the OTSControlpanel container.
  196.          */
  197.         public DisposeOnCloseThread(final OTSControlPanel panel)
  198.         {
  199.             super();
  200.             this.panel = panel;
  201.         }

  202.         /** {@inheritDoc} */
  203.         @Override
  204.         public final void run()
  205.         {
  206.             Container root = this.panel;
  207.             while (!(root instanceof JFrame))
  208.             {
  209.                 try
  210.                 {
  211.                     Thread.sleep(10);
  212.                 }
  213.                 catch (InterruptedException exception)
  214.                 {
  215.                     // nothing to do
  216.                 }

  217.                 // Search towards the root of the Swing components until we find a JFrame
  218.                 root = this.panel;
  219.                 while (null != root.getParent() && !(root instanceof JFrame))
  220.                 {
  221.                     root = root.getParent();
  222.                 }
  223.             }
  224.             JFrame frame = (JFrame) root;
  225.             frame.addWindowListener(this.panel);
  226.             this.panel.closeHandlerRegistered = true;
  227.             // frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
  228.         }

  229.         /** {@inheritDoc} */
  230.         @Override
  231.         public final String toString()
  232.         {
  233.             return "DisposeOnCloseThread [panel=" + this.panel + "]";
  234.         }
  235.     }

  236.     /** {@inheritDoc} */
  237.     @Override
  238.     public final void actionPerformed(final ActionEvent actionEvent)
  239.     {
  240.         String actionCommand = actionEvent.getActionCommand();
  241.         // System.out.println("actionCommand: " + actionCommand);
  242.         try
  243.         {
  244.             if (actionCommand.equals("Step"))
  245.             {
  246.                 if (getSimulator().isRunning())
  247.                 {
  248.                     getSimulator().stop();
  249.                 }
  250.                 this.simulator.step();
  251.             }
  252.             if (actionCommand.equals("RunPause"))
  253.             {
  254.                 if (this.simulator.isRunning())
  255.                 {
  256.                     this.simulator.stop();
  257.                 }
  258.                 else if (getSimulator().getEventList().size() > 0)
  259.                 {
  260.                     this.simulator.start();
  261.                 }
  262.             }
  263.             if (actionCommand.equals("NextTime"))
  264.             {
  265.                 if (getSimulator().isRunning())
  266.                 {
  267.                     getSimulator().stop();
  268.                 }
  269.                 double now = getSimulator().getSimulatorTime().getTime().getSI();
  270.                 // System.out.println("now is " + now);
  271.                 try
  272.                 {
  273.                     this.stopAtEvent =
  274.                             scheduleEvent(new Time(now, TimeUnit.SI), SimEventInterface.MIN_PRIORITY, this, this,
  275.                                     "autoPauseSimulator", null);
  276.                 }
  277.                 catch (SimRuntimeException exception)
  278.                 {
  279.                     this.logger.logp(Level.SEVERE, "ControlPanel", "autoPauseSimulator", "Caught an exception "
  280.                             + "while trying to schedule an autoPauseSimulator event at the current simulator time");
  281.                 }
  282.                 this.simulator.start();
  283.             }
  284.             if (actionCommand.equals("Reset"))
  285.             {
  286.                 if (getSimulator().isRunning())
  287.                 {
  288.                     getSimulator().stop();
  289.                 }

  290.                 if (null == OTSControlPanel.this.wrappableAnimation)
  291.                 {
  292.                     throw new RuntimeException("Do not know how to restart this simulation");
  293.                 }

  294.                 // find the JFrame position and dimensions
  295.                 Container root = OTSControlPanel.this;
  296.                 while (!(root instanceof JFrame))
  297.                 {
  298.                     root = root.getParent();
  299.                 }
  300.                 JFrame frame = (JFrame) root;
  301.                 Rectangle rect = frame.getBounds();
  302.                 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
  303.                 frame.dispose();
  304.                 OTSControlPanel.this.cleanup();
  305.                 try
  306.                 {
  307.                     OTSControlPanel.this.wrappableAnimation.rebuildSimulator(rect);
  308.                 }
  309.                 catch (Exception exception)
  310.                 {
  311.                     exception.printStackTrace();
  312.                 }
  313.             }
  314.             fixButtons();
  315.         }
  316.         catch (Exception exception)
  317.         {
  318.             exception.printStackTrace();
  319.         }
  320.     }

  321.     /**
  322.      * clean up timers, contexts, threads, etc. that could prevent garbage collection.
  323.      */
  324.     private void cleanup()
  325.     {
  326.         if (!this.isCleanUp)
  327.         {
  328.             this.isCleanUp = true;
  329.             try
  330.             {
  331.                 if (this.simulator != null)
  332.                 {
  333.                     if (this.simulator.isRunning())
  334.                     {
  335.                         this.simulator.stop();
  336.                     }

  337.                     // unbind the old animation and statistics
  338.                     getSimulator().getReplication().getExperiment().removeFromContext(); // clean up the context
  339.                     getSimulator().cleanUp();
  340.                 }

  341.                 if (this.clockPanel != null)
  342.                 {
  343.                     this.clockPanel.cancelTimer(); // cancel the timer on the clock panel.
  344.                 }

  345.                 if (this.wrappableAnimation != null)
  346.                 {
  347.                     this.wrappableAnimation.stopTimersThreads(); // stop the linked timers and other threads
  348.                 }
  349.             }
  350.             catch (Throwable exception)
  351.             {
  352.                 exception.printStackTrace();
  353.             }
  354.         }
  355.     }

  356.     /**
  357.      * Update the enabled state of all the buttons.
  358.      */
  359.     protected final void fixButtons()
  360.     {
  361.         // System.out.println("FixButtons entered");
  362.         final boolean moreWorkToDo = getSimulator().getEventList().size() > 0;
  363.         for (JButton button : this.buttons)
  364.         {
  365.             final String actionCommand = button.getActionCommand();
  366.             if (actionCommand.equals("Step"))
  367.             {
  368.                 button.setEnabled(moreWorkToDo);
  369.             }
  370.             else if (actionCommand.equals("RunPause"))
  371.             {
  372.                 button.setEnabled(moreWorkToDo);
  373.                 if (this.simulator.isRunning())
  374.                 {
  375.                     button.setToolTipText("Pause the simulation");
  376.                     button.setIcon(new ImageIcon(URLResource.getResource("/Pause.png")));
  377.                 }
  378.                 else
  379.                 {
  380.                     button.setToolTipText("Run the simulation at the indicated speed");
  381.                     button.setIcon(new ImageIcon(URLResource.getResource("/Play.png")));
  382.                 }
  383.                 button.setEnabled(moreWorkToDo);
  384.             }
  385.             else if (actionCommand.equals("NextTime"))
  386.             {
  387.                 button.setEnabled(moreWorkToDo);
  388.             }
  389.             else if (actionCommand.equals("Reset"))
  390.             {
  391.                 button.setEnabled(true); // FIXME: should be disabled when the simulator was just reset or initialized
  392.             }
  393.             else
  394.             {
  395.                 this.logger.logp(Level.SEVERE, "ControlPanel", "fixButtons", "", new Exception("Unknown button?"));
  396.             }
  397.         }
  398.         // System.out.println("FixButtons finishing");
  399.     }

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

  471.     /** {@inheritDoc} */
  472.     @Override
  473.     public final void propertyChange(final PropertyChangeEvent evt)
  474.     {
  475.         // System.out.println("PropertyChanged: " + evt);
  476.         if (null != this.stopAtEvent)
  477.         {
  478.             getSimulator().cancelEvent(this.stopAtEvent); // silently ignore false result
  479.             this.stopAtEvent = null;
  480.         }
  481.         String newValue = (String) evt.getNewValue();
  482.         String[] fields = newValue.split("[:\\.]");
  483.         int hours = Integer.parseInt(fields[0]);
  484.         int minutes = Integer.parseInt(fields[1]);
  485.         int seconds = Integer.parseInt(fields[2]);
  486.         int fraction = Integer.parseInt(fields[3]);
  487.         double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
  488.         if (stopTime < getSimulator().getSimulatorTime().getTime().getSI())
  489.         {
  490.             return;
  491.         }
  492.         else
  493.         {
  494.             try
  495.             {
  496.                 this.stopAtEvent =
  497.                         scheduleEvent(new Time(stopTime, TimeUnit.SECOND), SimEventInterface.MAX_PRIORITY, this, this,
  498.                                 "autoPauseSimulator", null);
  499.             }
  500.             catch (SimRuntimeException exception)
  501.             {
  502.                 this.logger.logp(Level.SEVERE, "ControlPanel", "propertyChange",
  503.                         "Caught an exception while trying to schedule an autoPauseSimulator event");
  504.             }
  505.         }
  506.     }

  507.     /**
  508.      * @return simulator.
  509.      */
  510.     @SuppressWarnings("unchecked")
  511.     public final DEVSSimulator<Time, Duration, OTSSimTimeDouble> getSimulator()
  512.     {
  513.         return (DEVSSimulator<Time, Duration, OTSSimTimeDouble>) this.simulator;
  514.     }

  515.     /** {@inheritDoc} */
  516.     @Override
  517.     public void windowOpened(final WindowEvent e)
  518.     {
  519.         // No action
  520.     }

  521.     /** {@inheritDoc} */
  522.     @Override
  523.     public final void windowClosing(final WindowEvent e)
  524.     {
  525.         if (this.simulator != null)
  526.         {
  527.             try
  528.             {
  529.                 if (this.simulator.isRunning())
  530.                 {
  531.                     this.simulator.stop();
  532.                 }
  533.             }
  534.             catch (SimRuntimeException exception)
  535.             {
  536.                 exception.printStackTrace();
  537.             }
  538.         }
  539.     }

  540.     /** {@inheritDoc} */
  541.     @Override
  542.     public final void windowClosed(final WindowEvent e)
  543.     {
  544.         cleanup();
  545.     }

  546.     /** {@inheritDoc} */
  547.     @Override
  548.     public final void windowIconified(final WindowEvent e)
  549.     {
  550.         // No action
  551.     }

  552.     /** {@inheritDoc} */
  553.     @Override
  554.     public final void windowDeiconified(final WindowEvent e)
  555.     {
  556.         // No action
  557.     }

  558.     /** {@inheritDoc} */
  559.     @Override
  560.     public final void windowActivated(final WindowEvent e)
  561.     {
  562.         // No action
  563.     }

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

  570.     /**
  571.      * @return timeFont.
  572.      */
  573.     public final Font getTimeFont()
  574.     {
  575.         return this.timeFont;
  576.     }

  577.     /** JPanel that contains a JSider that uses a logarithmic scale. */
  578.     static class TimeWarpPanel extends JPanel
  579.     {
  580.         /** */
  581.         private static final long serialVersionUID = 20150408L;

  582.         /** The JSlider that the user sees. */
  583.         private final JSlider slider;

  584.         /** The ratios used in each decade. */
  585.         private final int[] ratios;

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

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

  672.             // initial value of simulation speed
  673.             if (simulator instanceof DEVSRealTimeClock)
  674.             {
  675.                 DEVSRealTimeClock<?, ?, ?> clock = (DEVSRealTimeClock<?, ?, ?>) simulator;
  676.                 clock.setSpeedFactor(TimeWarpPanel.this.tickValues.get(this.slider.getValue()));
  677.             }

  678.             // adjust the simulation speed
  679.             this.slider.addChangeListener(new ChangeListener()
  680.             {
  681.                 public void stateChanged(final ChangeEvent ce)
  682.                 {
  683.                     JSlider source = (JSlider) ce.getSource();
  684.                     if (!source.getValueIsAdjusting() && simulator instanceof DEVSRealTimeClock)
  685.                     {
  686.                         DEVSRealTimeClock<?, ?, ?> clock = (DEVSRealTimeClock<?, ?, ?>) simulator;
  687.                         clock.setSpeedFactor(((TimeWarpPanel) source.getParent()).getTickValues().get(source.getValue()));
  688.                     }
  689.                 }
  690.             });
  691.         }

  692.         /**
  693.          * Access to tickValues map from within the event handler.
  694.          * @return Map&lt;Integer, Double&gt; the tickValues map of this TimeWarpPanel
  695.          */
  696.         protected Map<Integer, Double> getTickValues()
  697.         {
  698.             return this.tickValues;
  699.         }

  700.         /**
  701.          * Convert a position on the slider to a factor.
  702.          * @param step int; the position on the slider
  703.          * @return double; the factor that corresponds to step
  704.          */
  705.         private double stepToFactor(final int step)
  706.         {
  707.             int index = step % this.ratios.length;
  708.             if (index < 0)
  709.             {
  710.                 index += this.ratios.length;
  711.             }
  712.             double result = this.ratios[index];
  713.             // Make positive to avoid trouble with negative values that round towards 0 on division
  714.             int power = (step + 1000 * this.ratios.length) / this.ratios.length - 1000; // This is ugly
  715.             while (power > 0)
  716.             {
  717.                 result *= 10;
  718.                 power--;
  719.             }
  720.             while (power < 0)
  721.             {
  722.                 result /= 10;
  723.                 power++;
  724.             }
  725.             return result;
  726.         }

  727.         /**
  728.          * Retrieve the current TimeWarp factor.
  729.          * @return double; the current TimeWarp factor
  730.          */
  731.         public final double getFactor()
  732.         {
  733.             return stepToFactor(this.slider.getValue());
  734.         }

  735.         /** {@inheritDoc} */
  736.         @Override
  737.         public final String toString()
  738.         {
  739.             return "TimeWarpPanel [timeWarp=" + this.getFactor() + "]";
  740.         }

  741.         /**
  742.          * Set the time warp factor to the best possible approximation of a given value.
  743.          * @param factor double; the requested speed factor
  744.          */
  745.         public void setSpeedFactor(final double factor)
  746.         {
  747.             int bestStep = -1;
  748.             double bestError = Double.MAX_VALUE;
  749.             for (int step = this.slider.getMinimum(); step < this.slider.getMaximum(); step++)
  750.             {
  751.                 double ratio = getTickValues().get(step);// stepToFactor(step);
  752.                 double logError = Math.abs(Math.log(factor / ratio));
  753.                 if (logError < bestError)
  754.                 {
  755.                     bestStep = step;
  756.                     bestError = logError;
  757.                 }
  758.             }
  759.             System.out.println("setSpeedfactor: factor is " + factor + ", best slider value is " + bestStep
  760.                     + " current value is " + this.slider.getValue());
  761.             if (this.slider.getValue() != bestStep)
  762.             {
  763.                 this.slider.setValue(bestStep);
  764.             }
  765.         }
  766.     }

  767.     /** JLabel that displays the simulation time. */
  768.     class ClockPanel extends JLabel
  769.     {
  770.         /** */
  771.         private static final long serialVersionUID = 20141211L;

  772.         /** The JLabel that displays the time. */
  773.         private final JLabel clockLabel;

  774.         /** The timer (so we can cancel it). */
  775.         private Timer timer;

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

  778.         /** Construct a clock panel. */
  779.         ClockPanel()
  780.         {
  781.             super("00:00:00.000");
  782.             this.clockLabel = this;
  783.             this.setFont(getTimeFont());
  784.             this.timer = new Timer();
  785.             this.timer.scheduleAtFixedRate(new TimeUpdateTask(), 0, ClockPanel.UPDATEINTERVAL);
  786.         }

  787.         /**
  788.          * Cancel the timer task.
  789.          */
  790.         public void cancelTimer()
  791.         {
  792.             if (this.timer != null)
  793.             {
  794.                 this.timer.cancel();
  795.             }
  796.             this.timer = null;
  797.         }

  798.         /** Updater for the clock panel. */
  799.         private class TimeUpdateTask extends TimerTask implements Serializable
  800.         {
  801.             /** */
  802.             private static final long serialVersionUID = 20140000L;

  803.             /**
  804.              * Create a TimeUpdateTask.
  805.              */
  806.             TimeUpdateTask()
  807.             {
  808.             }

  809.             /** {@inheritDoc} */
  810.             @Override
  811.             public void run()
  812.             {
  813.                 double now = Math.round(getSimulator().getSimulatorTime().getTime().getSI() * 1000) / 1000d;
  814.                 int seconds = (int) Math.floor(now);
  815.                 int fractionalSeconds = (int) Math.floor(1000 * (now - seconds));
  816.                 getClockLabel().setText(
  817.                         String.format("  %02d:%02d:%02d.%03d  ", seconds / 3600, seconds / 60 % 60, seconds % 60,
  818.                                 fractionalSeconds));
  819.                 getClockLabel().repaint();
  820.             }

  821.             /** {@inheritDoc} */
  822.             @Override
  823.             public final String toString()
  824.             {
  825.                 return "TimeUpdateTask of ClockPanel";
  826.             }
  827.         }

  828.         /**
  829.          * @return clockLabel.
  830.          */
  831.         protected JLabel getClockLabel()
  832.         {
  833.             return this.clockLabel;
  834.         }

  835.         /** {@inheritDoc} */
  836.         @Override
  837.         public final String toString()
  838.         {
  839.             return "ClockPanel [clockLabel=" + this.clockLabel + ", time=" + getText() + "]";
  840.         }

  841.     }

  842.     /** Entry field for time. */
  843.     class TimeEdit extends JFormattedTextField
  844.     {
  845.         /** */
  846.         private static final long serialVersionUID = 20141212L;

  847.         /**
  848.          * Construct a new TimeEdit.
  849.          * @param initialValue Time; the initial value for the TimeEdit
  850.          */
  851.         TimeEdit(final Time initialValue)
  852.         {
  853.             super(new RegexFormatter("\\d\\d\\d\\d:[0-5]\\d:[0-5]\\d\\.\\d\\d\\d"));
  854.             MaskFormatter mf = null;
  855.             try
  856.             {
  857.                 mf = new MaskFormatter("####:##:##.###");
  858.                 mf.setPlaceholderCharacter('0');
  859.                 mf.setAllowsInvalid(false);
  860.                 mf.setCommitsOnValidEdit(true);
  861.                 mf.setOverwriteMode(true);
  862.                 mf.install(this);
  863.             }
  864.             catch (ParseException exception)
  865.             {
  866.                 exception.printStackTrace();
  867.             }
  868.             setTime(initialValue);
  869.             setFont(getTimeFont());
  870.         }

  871.         /**
  872.          * Set or update the time shown in this TimeEdit.
  873.          * @param newValue Time; the (new) value to set/show in this TimeEdit
  874.          */
  875.         public void setTime(final Time newValue)
  876.         {
  877.             double v = newValue.getSI();
  878.             int integerPart = (int) Math.floor(v);
  879.             int fraction = (int) Math.floor((v - integerPart) * 1000);
  880.             String text =
  881.                     String.format("%04d:%02d:%02d.%03d", integerPart / 3600, integerPart / 60 % 60, integerPart % 60, fraction);
  882.             this.setText(text);
  883.         }

  884.         /** {@inheritDoc} */
  885.         @Override
  886.         public final String toString()
  887.         {
  888.             return "TimeEdit [time=" + getText() + "]";
  889.         }
  890.     }

  891.     /**
  892.      * Extension of a DefaultFormatter that uses a regular expression. <br>
  893.      * Derived from <a href="http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm">
  894.      * http://www.java2s.com/Tutorial/Java/0240__Swing/RegexFormatterwithaJFormattedTextField.htm</a>
  895.      * <p>
  896.      * $LastChangedDate: 2016-10-16 14:55:54 +0200 (Sun, 16 Oct 2016) $, @version $Revision: 2386 $, by $Author: averbraeck $,
  897.      * initial version 2 dec. 2014 <br>
  898.      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  899.      */
  900.     static class RegexFormatter extends DefaultFormatter
  901.     {
  902.         /** */
  903.         private static final long serialVersionUID = 20141212L;

  904.         /** The regular expression pattern. */
  905.         private Pattern pattern;

  906.         /**
  907.          * Create a new RegexFormatter.
  908.          * @param pattern String; regular expression pattern that defines what this RexexFormatter will accept
  909.          */
  910.         RegexFormatter(final String pattern)
  911.         {
  912.             this.pattern = Pattern.compile(pattern);
  913.         }

  914.         @Override
  915.         public Object stringToValue(final String text) throws ParseException
  916.         {
  917.             Matcher matcher = this.pattern.matcher(text);
  918.             if (matcher.matches())
  919.             {
  920.                 // System.out.println("String \"" + text + "\" matches");
  921.                 return super.stringToValue(text);
  922.             }
  923.             // System.out.println("String \"" + text + "\" does not match");
  924.             throw new ParseException("Pattern did not match", 0);
  925.         }

  926.         /** {@inheritDoc} */
  927.         @Override
  928.         public final String toString()
  929.         {
  930.             return "RegexFormatter [pattern=" + this.pattern + "]";
  931.         }
  932.     }

  933.     /** {@inheritDoc} */
  934.     @Override
  935.     public final void notify(final EventInterface event) throws RemoteException
  936.     {
  937.         if (event.getType().equals(SimulatorInterface.END_OF_REPLICATION_EVENT)
  938.                 || event.getType().equals(SimulatorInterface.START_EVENT)
  939.                 || event.getType().equals(SimulatorInterface.STOP_EVENT)
  940.                 || event.getType().equals(DEVSRealTimeClock.CHANGE_SPEED_FACTOR_EVENT))
  941.         {
  942.             if (event.getType().equals(DEVSRealTimeClock.CHANGE_SPEED_FACTOR_EVENT))
  943.             {
  944.                 this.timeWarpPanel.setSpeedFactor((Double) event.getContent());
  945.             }
  946.             fixButtons();
  947.         }
  948.     }

  949.     /** {@inheritDoc} */
  950.     @Override
  951.     public final String toString()
  952.     {
  953.         return "OTSControlPanel [simulatorTime=" + this.simulator.getSimulatorTime().getTime() + ", timeWarp="
  954.                 + this.timeWarpPanel.getFactor() + ", stopAtEvent=" + this.stopAtEvent + "]";
  955.     }

  956. }