* Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License.
*
* $LastChangedDate: 2018-10-11 22:54:04 +0200 (Thu, 11 Oct 2018) $, @version $Revision: 4696 $, by $Author: averbraeck $,
* initial version 11 dec. 2014
* @author Alexander Verbraeck
* @author Peter Knoppers
*/
public class OTSControlPanel extends JPanel
implements ActionListener, PropertyChangeListener, WindowListener, EventListenerInterface
{
/** */
private static final long serialVersionUID = 20150617L;
/** The simulator. */
private DEVSSimulatorInterface.TimeDoubleUnit simulator;
/** The model, needed for its properties. */
private final OTSModelInterface model;
/** The clock. */
private final ClockLabel clockPanel;
/** The time warp control. */
private final TimeWarpPanel timeWarpPanel;
/** The control buttons. */
private final ArrayList buttons = new ArrayList<>();
/** Font used to display the clock and the stop time. */
private final Font timeFont = new Font("SansSerif", Font.BOLD, 18);
/** The TimeEdit that lets the user set a time when the simulation will be stopped. */
private final TimeEdit timeEdit;
/** The OTS search panel. */
private final OTSSearchPanel otsSearchPanel;
/** The currently registered stop at event. */
private SimEvent stopAtEvent = null;
/** The current enabled state of the buttons. */
private boolean buttonsEnabled = false;
/** Has the window close handler been registered? */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected boolean closeHandlerRegistered = false;
/** Has cleanup taken place? */
private boolean isCleanUp = false;
/**
* Decorate a SimpleSimulator with a different set of control buttons.
* @param simulator DEVSSimulatorInterface.TimeDoubleUnit; the simulator
* @param model OTSModelInterface; if non-null, the restart button should work
* @param otsAnimationPanel OTSAnimationPanel; the OTS animation panel
* @throws RemoteException when simulator cannot be accessed for listener attachment
*/
public OTSControlPanel(final DEVSSimulatorInterface.TimeDoubleUnit simulator, final OTSModelInterface model,
final OTSAnimationPanel otsAnimationPanel) throws RemoteException
{
this.simulator = simulator;
this.model = model;
this.setLayout(new FlowLayout(FlowLayout.LEFT));
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
buttonPanel.add(makeButton("stepButton", "/Last_recor.png", "Step", "Execute one event", true));
buttonPanel.add(makeButton("nextTimeButton", "/NextTrack.png", "NextTime",
"Execute all events scheduled for the current time", true));
buttonPanel.add(makeButton("runPauseButton", "/Play.png", "RunPause", "XXX", true));
this.timeWarpPanel = new TimeWarpPanel(0.1, 1000, 1, 3, simulator);
buttonPanel.add(this.timeWarpPanel);
// buttonPanel.add(makeButton("resetButton", "/Undo.png", "Reset", "Reset the simulation", false));
/** Label with appearance control. */
class AppearanceControlLabel extends JLabel implements AppearanceControl
{
/** */
private static final long serialVersionUID = 20180207L;
/** {@inheritDoc} */
@Override
public boolean isForeground()
{
return true;
}
/** {@inheritDoc} */
@Override
public boolean isBackground()
{
return true;
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "AppearanceControlLabel []";
}
}
JLabel speedLabel = new AppearanceControlLabel();
this.clockPanel = new ClockLabel(speedLabel);
this.clockPanel.setMaximumSize(new Dimension(133, 35));
buttonPanel.add(this.clockPanel);
speedLabel.setMaximumSize(new Dimension(66, 35));
buttonPanel.add(speedLabel);
this.timeEdit = new TimeEdit(new Time(0, TimeUnit.DEFAULT));
this.timeEdit.setMaximumSize(new Dimension(133, 35));
this.timeEdit.addPropertyChangeListener("value", this);
buttonPanel.add(this.timeEdit);
this.add(buttonPanel);
this.otsSearchPanel = new OTSSearchPanel(otsAnimationPanel);
this.add(this.otsSearchPanel, BorderLayout.SOUTH);
fixButtons();
installWindowCloseHandler();
this.simulator.addListener(this, ReplicationInterface.END_REPLICATION_EVENT);
this.simulator.addListener(this, SimulatorInterface.START_EVENT);
this.simulator.addListener(this, SimulatorInterface.STOP_EVENT);
this.simulator.addListener(this, DEVSRealTimeAnimator.CHANGE_SPEED_FACTOR_EVENT);
}
/**
* Change the enabled/disabled state of the various simulation control buttons.
* @param newState boolean; true if the buttons should become enabled; false if the buttons should become disabled
*/
public void setSimulationControlButtons(final boolean newState)
{
this.buttonsEnabled = newState;
fixButtons();
}
/**
* Provide access to the search panel.
* @return OTSSearchPanel; the OTS search panel
*/
public OTSSearchPanel getOtsSearchPanel()
{
return this.otsSearchPanel;
}
/**
* Create a button.
* @param name String; name of the button
* @param iconPath String; path to the resource
* @param actionCommand String; the action command
* @param toolTipText String; the hint to show when the mouse hovers over the button
* @param enabled boolean; true if the new button must initially be enable; false if it must initially be disabled
* @return JButton
*/
private JButton makeButton(final String name, final String iconPath, final String actionCommand, final String toolTipText,
final boolean enabled)
{
/** Button with appearance control. */
class AppearanceControlButton extends JButton implements AppearanceControl
{
/** */
private static final long serialVersionUID = 20180206L;
/**
* @param loadIcon Icon; icon
*/
AppearanceControlButton(final Icon loadIcon)
{
super(loadIcon);
}
/** {@inheritDoc} */
@Override
public boolean isFont()
{
return true;
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "AppearanceControlButton []";
}
}
JButton result = new AppearanceControlButton(loadIcon(iconPath));
result.setName(name);
result.setEnabled(enabled);
result.setActionCommand(actionCommand);
result.setToolTipText(toolTipText);
result.addActionListener(this);
this.buttons.add(result);
return result;
}
/**
* Attempt to load and return an icon.
* @param iconPath String; the path that is used to load the icon
* @return Icon; or null if loading failed
*/
public static final Icon loadIcon(final String iconPath)
{
try
{
return new ImageIcon(ImageIO.read(Resource.getResourceAsStream(iconPath)));
}
catch (NullPointerException | IOException npe)
{
System.err.println("Could not load icon from path " + iconPath);
return null;
}
}
/**
* Attempt to load and return an icon, which will be made gray-scale.
* @param iconPath String; the path that is used to load the icon
* @return Icon; or null if loading failed
*/
public static final Icon loadGrayscaleIcon(final String iconPath)
{
try
{
return new ImageIcon(GrayFilter.createDisabledImage(ImageIO.read(Resource.getResourceAsStream(iconPath))));
}
catch (NullPointerException | IOException e)
{
System.err.println("Could not load icon from path " + iconPath);
return null;
}
}
/**
* Construct and schedule a SimEvent using a Time to specify the execution time.
* @param executionTime Time; the time at which the event must happen
* @param priority short; should be between SimEventInterface.MAX_PRIORITY and
* SimEventInterface.MIN_PRIORITY; most normal events should use
* SimEventInterface.NORMAL_PRIORITY
* @param source Object; the object that creates/schedules the event
* @param eventTarget Object; the object that must execute the event
* @param method String; the name of the method of target that must execute the event
* @param args Object[]; the arguments of the method that must execute the event
* @return SimEvent<SimTimeDoubleUnit>; the event that was scheduled (the caller should save this if a need to cancel
* the event may arise later)
* @throws SimRuntimeException when the executionTime is in the past
*/
private SimEvent scheduleEvent(final Time executionTime, final short priority, final Object source,
final Object eventTarget, final String method, final Object[] args) throws SimRuntimeException
{
SimEvent simEvent =
new SimEvent<>(new SimTimeDoubleUnit(new Time(executionTime.getSI(), TimeUnit.DEFAULT)), priority, source,
eventTarget, method, args);
this.simulator.scheduleEvent(simEvent);
return simEvent;
}
/**
* Install a handler for the window closed event that stops the simulator (if it is running).
*/
public final void installWindowCloseHandler()
{
if (this.closeHandlerRegistered)
{
return;
}
// make sure the root frame gets disposed of when the closing X icon is pressed.
new DisposeOnCloseThread(this).start();
}
/** Install the dispose on close when the OTSControlPanel is registered as part of a frame. */
protected class DisposeOnCloseThread extends Thread
{
/** The current container. */
private OTSControlPanel panel;
/**
* @param panel OTSControlPanel; the OTSControlpanel container.
*/
public DisposeOnCloseThread(final OTSControlPanel panel)
{
this.panel = panel;
}
/** {@inheritDoc} */
@Override
public final void run()
{
Container root = this.panel;
while (!(root instanceof JFrame))
{
try
{
Thread.sleep(10);
}
catch (InterruptedException exception)
{
// nothing to do
}
// Search towards the root of the Swing components until we find a JFrame
root = this.panel;
while (null != root.getParent() && !(root instanceof JFrame))
{
root = root.getParent();
}
}
JFrame frame = (JFrame) root;
frame.addWindowListener(this.panel);
this.panel.closeHandlerRegistered = true;
// frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
}
/** {@inheritDoc} */
@Override
public final String toString()
{
return "DisposeOnCloseThread [panel=" + this.panel + "]";
}
}
/** {@inheritDoc} */
@Override
public final void actionPerformed(final ActionEvent actionEvent)
{
String actionCommand = actionEvent.getActionCommand();
// System.out.println("actionCommand: " + actionCommand);
try
{
if (actionCommand.equals("Step"))
{
if (getSimulator().isStartingOrRunning())
{
getSimulator().stop();
}
this.simulator.step();
}
if (actionCommand.equals("RunPause"))
{
if (this.simulator.isStartingOrRunning())
{
// System.out.println("RunPause: Stopping simulator");
this.simulator.stop();
}
else if (getSimulator().getEventList().size() > 0)
{
// System.out.println("RunPause: Starting simulator");
this.simulator.start();
}
}
if (actionCommand.equals("NextTime"))
{
if (getSimulator().isStartingOrRunning())
{
// System.out.println("NextTime: Stopping simulator");
getSimulator().stop();
}
double now = getSimulator().getSimulatorTime().getSI();
// System.out.println("now is " + now);
try
{
this.stopAtEvent = scheduleEvent(new Time(now, TimeUnit.DEFAULT), SimEventInterface.MIN_PRIORITY, this,
this, "autoPauseSimulator", null);
}
catch (SimRuntimeException exception)
{
this.simulator.getLogger().always()
.error("Caught an exception while trying to schedule an autoPauseSimulator event "
+ "at the current simulator time");
}
// System.out.println("NextTime: Starting simulator");
this.simulator.start();
}
if (actionCommand.equals("Reset"))
{
if (getSimulator().isStartingOrRunning())
{
getSimulator().stop();
}
if (null == OTSControlPanel.this.model)
{
throw new RuntimeException("Do not know how to restart this simulation");
}
// find the JFrame position and dimensions
Container root = OTSControlPanel.this;
while (!(root instanceof JFrame))
{
root = root.getParent();
}
JFrame frame = (JFrame) root;
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.dispose();
OTSControlPanel.this.cleanup();
// TODO: maybe rebuild model...
}
fixButtons();
}
catch (Exception exception)
{
exception.printStackTrace();
}
}
/**
* clean up timers, contexts, threads, etc. that could prevent garbage collection.
*/
private void cleanup()
{
if (!this.isCleanUp)
{
this.isCleanUp = true;
try
{
if (this.simulator != null)
{
if (this.simulator.isStartingOrRunning())
{
this.simulator.stop();
}
// unbind the old animation and statistics
// TODO: change getExperiment().removeFromContext() so it works properly...
// Now: ConcurrentModificationException...
if (getSimulator().getReplication().getContext().hasKey("animation"))
{
getSimulator().getReplication().getContext().destroySubcontext("animation");
}
if (getSimulator().getReplication().getContext().hasKey("statistics"))
{
getSimulator().getReplication().getContext().destroySubcontext("statistics");
}
if (getSimulator().getReplication().getContext().hasKey("statistics"))
{
getSimulator().getReplication().getContext().destroySubcontext("statistics");
}
// TODO: this is implemented completely different in latest DSOL versions
getSimulator().cleanUp();
}
if (this.clockPanel != null)
{
this.clockPanel.cancelTimer(); // cancel the timer on the clock panel.
}
// TODO: are there timers or threads we need to stop?
}
catch (Throwable exception)
{
exception.printStackTrace();
}
}
}
/**
* Update the enabled state of all the buttons.
*/
protected final void fixButtons()
{
// System.out.println("FixButtons entered");
final boolean moreWorkToDo = getSimulator().getEventList().size() > 0;
for (JButton button : this.buttons)
{
final String actionCommand = button.getActionCommand();
if (actionCommand.equals("Step"))
{
button.setEnabled(moreWorkToDo && this.buttonsEnabled);
}
else if (actionCommand.equals("RunPause"))
{
button.setEnabled(moreWorkToDo && this.buttonsEnabled);
if (this.simulator.isStartingOrRunning())
{
button.setToolTipText("Pause the simulation");
button.setIcon(OTSControlPanel.loadIcon("/Pause.png"));
}
else
{
button.setToolTipText("Run the simulation at the indicated speed");
button.setIcon(loadIcon("/Play.png"));
}
button.setEnabled(moreWorkToDo && this.buttonsEnabled);
}
else if (actionCommand.equals("NextTime"))
{
button.setEnabled(moreWorkToDo && this.buttonsEnabled);
}
// else if (actionCommand.equals("Reset"))
// {
// button.setEnabled(true); // FIXME: should be disabled when the simulator was just reset or initialized
// }
else
{
this.simulator.getLogger().always().error(new Exception("Unknown button?"));
}
}
// System.out.println("FixButtons finishing");
}
/**
* Pause the simulator.
*/
public final void autoPauseSimulator()
{
// System.out.println("OTSControlPanel.autoPauseSimulator entered");
if (getSimulator().isStartingOrRunning())
{
try
{
// System.out.println("AutoPauseSimulator: stopping simulator");
getSimulator().stop();
}
catch (SimRuntimeException exception1)
{
exception1.printStackTrace();
}
double currentTick = getSimulator().getSimulatorTime().getSI();
double nextTick = getSimulator().getEventList().first().getAbsoluteExecutionTime().get().getSI();
// System.out.println("currentTick is " + currentTick);
// System.out.println("nextTick is " + nextTick);
if (nextTick > currentTick)
{
// The clock is now just beyond where it was when the user requested the NextTime operation
// Insert another autoPauseSimulator event just before what is now the time of the next event
// and let the simulator time increment to that time
// System.out.println("Re-Scheduling at " + nextTick);
try
{
this.stopAtEvent = scheduleEvent(new Time(nextTick, TimeUnit.DEFAULT), SimEventInterface.MAX_PRIORITY, this,
this, "autoPauseSimulator", null);
// System.out.println("AutoPauseSimulator: starting simulator");
getSimulator().start();
}
catch (SimRuntimeException exception)
{
this.simulator.getLogger().always()
.error("Caught an exception while trying to re-schedule an autoPauseEvent at the next real event");
}
}
else
{
// System.out.println("Not re-scheduling");
if (SwingUtilities.isEventDispatchThread())
{
// System.out.println("Already on EventDispatchThread");
fixButtons();
}
else
{
try
{
// System.out.println("Current thread is NOT EventDispatchThread: " + Thread.currentThread());
SwingUtilities.invokeAndWait(new Runnable()
{
@Override
public void run()
{
// System.out.println("Runnable started");
fixButtons();
// System.out.println("Runnable finishing");
}
});
}
catch (Exception e)
{
if (e instanceof InterruptedException)
{
System.out.println("Caught " + e);
// e.printStackTrace();
}
else
{
e.printStackTrace();
}
}
}
}
}
// System.out.println("OTSControlPanel.autoPauseSimulator finished");
}
/** {@inheritDoc} */
@Override
public final void propertyChange(final PropertyChangeEvent evt)
{
// System.out.println("PropertyChanged: " + evt);
if (null != this.stopAtEvent)
{
getSimulator().cancelEvent(this.stopAtEvent); // silently ignore false result
this.stopAtEvent = null;
}
String newValue = (String) evt.getNewValue();
String[] fields = newValue.split("[:\\.]");
int hours = Integer.parseInt(fields[0]);
int minutes = Integer.parseInt(fields[1]);
int seconds = Integer.parseInt(fields[2]);
int fraction = Integer.parseInt(fields[3]);
double stopTime = hours * 3600 + minutes * 60 + seconds + fraction / 1000d;
if (stopTime < getSimulator().getSimulatorTime().getSI())
{
return;
}
else
{
try
{
this.stopAtEvent = scheduleEvent(new Time(stopTime, TimeUnit.DEFAULT), SimEventInterface.MAX_PRIORITY, this,
this, "autoPauseSimulator", null);
}
catch (SimRuntimeException exception)
{
this.simulator.getLogger().always()
.error("Caught an exception while trying to schedule an autoPauseSimulator event");
}
}
}
/**
* @return simulator.
*/
@SuppressWarnings("unchecked")
public final DEVSSimulator