OtsSimulationPanel.java
package org.opentrafficsim.swing.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.djutils.draw.bounds.Bounds2d;
import org.djutils.draw.point.Point;
import org.djutils.draw.point.Point2d;
import org.djutils.event.Event;
import org.djutils.event.EventListener;
import org.djutils.event.TimedEvent;
import org.djutils.exceptions.Throw;
import org.opentrafficsim.animation.ColorInterpolator;
import org.opentrafficsim.animation.data.AnimationGtuData;
import org.opentrafficsim.animation.data.gtu.GtuColorerManager;
import org.opentrafficsim.animation.data.util.IconUtil;
import org.opentrafficsim.base.logger.Logger;
import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
import org.opentrafficsim.core.gtu.Gtu;
import org.opentrafficsim.core.network.Network;
import org.opentrafficsim.road.gtu.LaneBasedGtu;
import nl.tudelft.simulation.dsol.animation.Locatable;
import nl.tudelft.simulation.dsol.animation.d2.Renderable2dInterface;
import nl.tudelft.simulation.dsol.animation.gis.GisMapInterface;
import nl.tudelft.simulation.dsol.animation.gis.GisRenderable2d;
import nl.tudelft.simulation.dsol.experiment.Replication;
import nl.tudelft.simulation.dsol.swing.animation.d2.AnimationPanel;
import nl.tudelft.simulation.dsol.swing.gui.ConsoleOutput;
import nl.tudelft.simulation.dsol.swing.gui.TabbedContentPane;
import nl.tudelft.simulation.language.DsolException;
/**
* Simulation panel with various controls and animation.
* <p>
* Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author Alexander Verbraeck
* @author Peter Knoppers
* @author Wouter Schakel
*/
public class OtsSimulationPanel extends JPanel implements ActionListener, EventListener
{
/** Serialization version UID. */
private static final long serialVersionUID = 20150617L;
static
{
// use narrow border for TabbedPane, which cannot be changed afterwards
UIManager.put("TabbedPane.contentBorderInsets", new Insets(1, 0, 1, 0));
}
/** Properties. */
public static final PropertiesStore PROPERTIES =
new PropertiesStore(new Properties(), "simulation", "simulation user settings");
/** Simulator. */
private final OtsSimulatorInterface simulator;
/** Control panel to control start/stop, speed of the simulation. */
private final OtsSimulationControlPanel otsSimulationControlPanel;
/** Tabbed pane that contains the different (default) screens. */
private final TabbedContentPane tabbedPane;
/** Pattern to split string by upper case, with lower case adjacent, without disregarding the match itself. */
private static final Pattern UPPER_PATTERN = Pattern.compile("(?=\\p{Lu})(?<=\\p{Ll})|(?=\\p{Lu}\\p{Ll})");
/** Format for the world coordinates. */
private static final String COORD_FORMAT = "%09.2f";
/** Pattern to split leading zeros from the rest of a number. */
private static final Pattern LEADING_ZEROS = Pattern.compile("^([+-]?)(0*)((:?0|[1-9]\\d*)(:?[\\.,]\\d+)?)$");
/** Collapsed icon. */
private static final Icon COLLAPSED_ICON = IconUtil.of("Collapsed24.png").imageSize(12, 12).get();
/** Expanded icon. */
private static final Icon EXPANDED_ICON = IconUtil.of("Expanded24.png").imageSize(12, 12).get();
/** OTS search panel. */
private final OtsSearchPanel otsSearchPanel;
/** Animation panel on tab position 0. */
private final OtsAnimationPanel otsAnimationPanel;
/** Toggle panel with which animation features can be shown/hidden. */
private final JPanel togglePanel;
/** Demo panel. */
private JPanel demoPanel = null;
/** Whether the current toggle section is visible. */
private boolean toggleSectionVisible = true;
/** Toggle section data (name and default visible). */
private Map<AppearanceJToggleButton, ToggleSectionData> toggleSectionData = new LinkedHashMap<>();
/** Map of toggle names to toggle animation classes. */
private Map<String, Class<? extends Locatable>> toggleLocatableMap = new LinkedHashMap<>();
/** Set of animation classes to toggle buttons. */
private Map<Class<? extends Locatable>, JToggleButton> toggleButtons = new LinkedHashMap<>();
/** Set of GIS layer names to toggle GIS layers . */
private Map<String, GisMapInterface> toggleGisMap = new LinkedHashMap<>();
/** Set of GIS layer names to toggle buttons. */
private Map<String, JToggleButton> toggleGisButtons = new LinkedHashMap<>();
/** GTU color panel. */
private OtsGtuColorPanel gtuColorPanel = null;
/** Coordinates of the cursor. */
private final JLabel coordinateField;
/** GTU count field. */
private final JLabel gtuCountField;
/** GTU count. */
private int gtuCount = 0;
/** Id of object to auto pan to. */
private String autoPanId = null;
/** Type of object to auto pan to. */
private OtsSearchPanel.ObjectKind<?> autoPanKind = null;
/** Track auto pan object continuously? */
private boolean autoPanTrack = false;
/** Track auto on the next paint (if this is true but autoPanTrack is false, only a one-shot auto pan). */
private boolean autoPanOnNextPaintComponent = false;
/**
* Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
* @param network network
* @throws RemoteException when notification of the animation panel fails
* @throws DsolException when simulator does not implement AnimatorInterface
*/
public OtsSimulationPanel(final Network network) throws RemoteException, DsolException
{
this(network.getExtent(), network);
}
/**
* Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
* @param extent bottom left corner, length and width of the area (world) to animate
* @param network network
* @throws RemoteException when notification of the animation panel fails
* @throws DsolException when simulator does not implement AnimatorInterface
*/
public OtsSimulationPanel(final Rectangle2D extent, final Network network) throws RemoteException, DsolException
{
this(extent, network, new OtsSimulationPanelDecorator()
{
});
}
/**
* Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
* @param network network
* @param decorator decorator for the animation panel
* @throws RemoteException when notification of the animation panel fails
* @throws DsolException when simulator does not implement AnimatorInterface
* @throws NullPointerException when any input is {@code null}
*/
public OtsSimulationPanel(final Network network, final OtsSimulationPanelDecorator decorator)
throws RemoteException, DsolException
{
this(network.getExtent(), network, decorator);
}
/**
* Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
* @param extent bottom left corner, length and width of the area (world) to animate
* @param network network
* @param decorator decorator for the animation panel
* @throws RemoteException when notification of the animation panel fails
* @throws DsolException when simulator does not implement AnimatorInterface
* @throws NullPointerException when any input is {@code null}
*/
public OtsSimulationPanel(final Rectangle2D extent, final Network network, final OtsSimulationPanelDecorator decorator)
throws RemoteException, DsolException
{
Throw.whenNull(network, "network");
Throw.whenNull(decorator, "decorator");
AppearanceApplication.setDefaultFont();
this.simulator = network.getSimulator();
/*-
* .-- OtsSimulationApplication -------------------------------------------------------------------------.
* | OTS | The Open Traffic Simulator | {model name} _ # X |
* |o== OtsSimulationPanel =============================================================================o|
* ||+-- topPanel -------------------------------------------------------------------------------------+||
* |||o-- OtsSimulationControlPanel ----------------------------------o-- OtsSearchPanel -------------o|||
* |||| > > > 00:00:00.000 0.00x | GTU|v| |Id... | track ||||
* |||o---------------------------------------------------------------o-------------------------------o|||
* ||o-- AppearanceControlTabbedContentPane -----------------------------------------------------------o||
* |||+-- borderLayoutPanel --------------------------------------------------------------------------+|||
* ||||+-- animationTopBarPanel ---------------------------------------------------------------------+||||
* |||||o-- OtsGtuColorPanel -----------------------------------o +-- infoTextPanel --+|||||
* |||||| Blue |v| | Home Grid | 0 GTU's ||||||
* |||||o-------------------------------------------------------o +-------------------+|||||
* ||||+-- toggle --o-- OtsAnimationPanel -----------------------------------------------------------o||||
* ||||| -Panel | |||||
* ||||| | |||||
* ||||| | +-- demoPanel -----------------+ |||||
* ||||| | | | |||||
* ||||| | | (top, bottom, left or right) | |||||
* ||||| | | | |||||
* ||||| | +------------------------------+ |||||
* ||||| | |||||
* ||||| | |||||
* ||||+------------o--------------------------------------------------------------------------------o||||
* |||+-----------------------------------------------------------------------------------------------+|||
* ||| animation / |||
* ||o-------------------------------------------------------------------------------------------------o||
* |o---------------------------------------------------------------------------------------------------o|
* '-----------------------------------------------------------------------------------------------------'
*
* Legend: +-- lowerCaseName -------+ o-- UpperCaseName -------o
* | Regular JPanel | | Specific class |
* +------------------------+ o------------------------o
*/
setLayout(new BorderLayout());
// topPanel > OtsSimulationControlPanel, OtsSearchPanel
JPanel topPanel = new JPanel();
topPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.X_AXIS));
this.otsSimulationControlPanel = new OtsSimulationControlPanel(this.simulator, this);
this.otsSearchPanel = new OtsSearchPanel(this);
topPanel.add(this.otsSimulationControlPanel);
topPanel.add(Box.createHorizontalGlue());
topPanel.add(this.otsSearchPanel);
add(topPanel, BorderLayout.NORTH);
// tabbedPane with borderLayoutPanel in animation tab
this.tabbedPane = new AppearanceControlTabbedContentPane(SwingConstants.BOTTOM);
JPanel borderLayoutPanel = new JPanel(new BorderLayout());
this.tabbedPane.addTab(0, "animation", borderLayoutPanel);
add(this.tabbedPane, BorderLayout.CENTER);
// borderLayoutPanel > animationTopBarPanel, togglePanel, OtsAnimationPanel
JPanel animationTopBarPanel = new JPanel();
animationTopBarPanel.setLayout(new BoxLayout(animationTopBarPanel, BoxLayout.X_AXIS));
borderLayoutPanel.add(animationTopBarPanel, BorderLayout.NORTH);
this.togglePanel = new JPanel();
this.togglePanel.setLayout(new BoxLayout(this.togglePanel, BoxLayout.Y_AXIS));
JPanel resetBox = new JPanel();
resetBox.setLayout(new BoxLayout(resetBox, BoxLayout.X_AXIS));
resetBox.setAlignmentX(Component.LEFT_ALIGNMENT);
this.togglePanel.add(resetBox);
Dimension dim = new Dimension(16, 16);
resetBox.add(Box.createHorizontalGlue());
createToggleSectionResetButton("Expand", EXPANDED_ICON, true, dim, resetBox);
createToggleSectionResetButton("Collapse", COLLAPSED_ICON, false, dim, resetBox);
createToggleSectionResetButton("Reset", IconUtil.of("Restore24.png").imageSize(12, 12).get(), null, dim, resetBox);
resetBox.add(Box.createHorizontalGlue());
borderLayoutPanel.add(this.togglePanel, BorderLayout.WEST);
this.otsAnimationPanel = new OtsAnimationPanel(extent, this.simulator, network);
this.otsAnimationPanel.showGrid(PROPERTIES.getOptionalBoolean("grid").orElse(false));
borderLayoutPanel.add(this.otsAnimationPanel, BorderLayout.CENTER);
// animationTopBarPanel > OtsGtuColorPanel, buttons, infoTextPanel
this.gtuColorPanel = new OtsGtuColorPanel();
decorator.getGtuColorers().forEach((colorer) -> this.gtuColorPanel.addGtuColorer(colorer));
animationTopBarPanel.add(this.gtuColorPanel);
animationTopBarPanel.add(Box.createHorizontalStrut(10));
animationTopBarPanel.add(makeButton("yZoomButton", "UpDown24.png", "Reset Y-zoom", "Reset Y-zoom", true));
animationTopBarPanel.add(makeButton("allButton", "ZoomAll24.png", "ZoomAll", "Zoom whole network", true));
animationTopBarPanel.add(makeButton("homeButton", "Home24.png", "Home", "Zoom to original extent", true));
animationTopBarPanel.add(makeButton("gridButton", "Grid24.png", "Grid", "Toggle grid on/off", true));
animationTopBarPanel.add(Box.createHorizontalStrut(10));
JPanel infoTextPanel = new JPanel();
animationTopBarPanel.add(infoTextPanel);
infoTextPanel.setMinimumSize(new Dimension(250, 30));
infoTextPanel.setPreferredSize(new Dimension(250, 30));
infoTextPanel.setMaximumSize(new Dimension(250, 30));
infoTextPanel.setLayout(new BoxLayout(infoTextPanel, BoxLayout.Y_AXIS));
// infoTextPanel contents
this.coordinateField = new JLabel();
this.coordinateField.setMinimumSize(new Dimension(150, 15));
this.coordinateField.setPreferredSize(new Dimension(150, 15));
this.coordinateField.setMaximumSize(new Dimension(150, 15));
this.coordinateField.setFont(new Font("Consolas", Font.PLAIN, 12));
infoTextPanel.add(this.coordinateField);
this.gtuCountField = new JLabel();
this.gtuCountField.setMinimumSize(new Dimension(150, 15));
this.gtuCountField.setPreferredSize(new Dimension(150, 15));
this.gtuCountField.setMaximumSize(new Dimension(150, 15));
this.gtuCount = null == network ? 0 : network.getGTUs().size();
infoTextPanel.add(this.gtuCountField);
setGtuCountText();
// only show OtsSearchPanel when the animation tab is selected
this.tabbedPane.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(final ChangeEvent e)
{
int index = OtsSimulationPanel.this.tabbedPane.getSelectedIndex();
Component component = OtsSimulationPanel.this.tabbedPane.getComponentAt(index);
OtsSimulationPanel.this.otsSearchPanel.setVisible(borderLayoutPanel.equals(component));
}
});
// listen to update GTU count
if (null != network)
{
network.addListener(this, Network.GTU_ADD_EVENT);
network.addListener(this, Network.GTU_REMOVE_EVENT);
}
// fake start event to draw static objects before the simulation is started (this will all be cleared on real start)
this.otsAnimationPanel
.notify(new TimedEvent<>(Replication.START_REPLICATION_EVENT, null, getSimulator().getSimulatorTime()));
// switch off the X and Y coordinates in a tool-tip
this.otsAnimationPanel.setShowToolTip(false);
// decorate
decorator.decorate(this, network);
}
/**
* Creates toggle section reset button.
* @param action action, is prepended to tooltip text
* @param icon icon
* @param selected selected state of toggle section toggles when this button is clicked
* @param dim dimension
* @param resetBox panel to add the button to
*/
private void createToggleSectionResetButton(final String action, final Icon icon, final Boolean selected,
final Dimension dim, final JPanel resetBox)
{
JButton expand = new JButton(new AbstractAction("", icon)
{
private static final long serialVersionUID = 20260507L;
@Override
public void actionPerformed(final ActionEvent e)
{
setToggleSectionButtonsState(selected);
}
});
expand.setToolTipText(action + " all animation toggle sections");
expand.setContentAreaFilled(false);
expand.setBorder(null);
expand.setMinimumSize(dim);
expand.setPreferredSize(dim);
expand.setMaximumSize(dim);
resetBox.add(expand);
}
/**
* Sets all toggle section buttons to the given selected state.
* @param selected {@code true} is selected, {@code false} is not selected, {@code null} is as per default
*/
private void setToggleSectionButtonsState(final Boolean selected)
{
for (Component component : this.togglePanel.getComponents())
{
if (component instanceof AppearanceJToggleButton button)
{
ToggleSectionData sectionData = this.toggleSectionData.get(button);
boolean state = selected == null ? sectionData.defaultVisible() : selected;
button.setSelected(state);
toggleSectionToggleChanged(sectionData.name(), button, getToggleSectionKey(sectionData.name()));
}
}
}
/**
* Adds the console tab.
*/
public void addConsoleTab()
{
ConsoleOutput console = new ConsoleOutput();
console.setBorder(null);
this.tabbedPane.addTab("console", console);
}
/**
* Return tabbed pane.
* @return tabbed pane
*/
public TabbedContentPane getTabbedPane()
{
return this.tabbedPane;
}
/**
* Return simulator.
* @return simulator
*/
public OtsSimulatorInterface getSimulator()
{
return this.simulator;
}
/**
* Enable the simulation or animation buttons in the GUI. This method HAS TO BE CALLED in order for the buttons to be
* enabled, because the initial state is DISABLED. Typically, this is done after all tabs, statistics, and other user
* interface and model components have been constructed and initialized.
*/
public void enableSimulationControlButtons()
{
this.otsSimulationControlPanel.setSimulationControlButtons(true);
}
/**
* Disable the simulation or animation buttons in the GUI.
*/
public void disableSimulationControlButtons()
{
this.otsSimulationControlPanel.setSimulationControlButtons(false);
}
/**
* Change auto pan target.
* @param newAutoPanId id of object to track
* @param newAutoPanKind kind of object to track
* @param newAutoPanTrack if true; tracking is continuously; if false; tracking is once
*/
public void setAutoPan(final String newAutoPanId, final OtsSearchPanel.ObjectKind<?> newAutoPanKind,
final boolean newAutoPanTrack)
{
this.autoPanId = newAutoPanId;
this.autoPanKind = newAutoPanKind;
this.autoPanTrack = newAutoPanTrack;
this.autoPanOnNextPaintComponent = true;
Logger.ots().trace("AutoPan id=" + newAutoPanId + ", kind=" + newAutoPanKind + ", track=" + newAutoPanTrack);
if (null != this.autoPanId && null != OtsSimulationPanel.this.otsAnimationPanel && this.autoPanId.length() > 0
&& null != this.autoPanKind)
{
OtsSimulationPanel.this.otsAnimationPanel.repaint();
}
}
/**
* Create a button.
* @param name name of the button
* @param iconFile name of the icon file
* @param actionCommand the action command
* @param toolTipText the hint to show when the mouse hovers over the button
* @param enabled true if the new button must initially be enable; false if it must initially be disabled
* @return button
*/
private JButton makeButton(final String name, final String iconFile, final String actionCommand, final String toolTipText,
final boolean enabled)
{
JButton result = new JButton(IconUtil.of(iconFile).get());
result.setMinimumSize(new Dimension(34, 32));
result.setPreferredSize(new Dimension(34, 32));
result.setMaximumSize(new Dimension(34, 32));
result.setName(name);
result.setEnabled(enabled);
result.setActionCommand(actionCommand);
result.setToolTipText(toolTipText);
result.addActionListener(this);
return result;
}
/**
* Adds a button that can hide all toggles in a section. The section is defined as laying between two section buttons.
* @param name name of the section that can be hidden
* @param visibleDefault whether the section is visible by default
*/
public void startToggleSection(final String name, final boolean visibleDefault)
{
AppearanceJToggleButton toggle = new AppearanceJToggleButton();
toggle.setSelectedIcon(EXPANDED_ICON);
toggle.setIcon(COLLAPSED_ICON);
String key = getToggleSectionKey(name);
boolean toggleOn = PROPERTIES.getOptionalBoolean(key).orElse(true);
toggle.setSelected(toggleOn);
PROPERTIES.setBoolean(key, toggleOn);
this.toggleSectionVisible = toggleOn;
toggle.setText(toggleOn ? null : name);
toggle.setContentAreaFilled(false);
toggle.setBorder(null);
Dimension dim = new Dimension(64, 14);
toggle.setMinimumSize(dim);
toggle.setPreferredSize(dim);
toggle.setMaximumSize(dim);
toggle.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(final ActionEvent e)
{
toggleSectionToggleChanged(name, toggle, key);
}
});
this.togglePanel.add(toggle);
this.toggleSectionData.put(toggle, new ToggleSectionData(name, visibleDefault));
}
/**
* Returns a toggle section key for inside {@code PROPERTIES}.
* @param name name of the section that can be hidden
* @return toggle section key
*/
private String getToggleSectionKey(final String name)
{
return "toggle.section." + PropertiesStore.key(name);
}
/**
* Remembers the state of a toggled toggle section in properties and shows or hides the section animation toggles.
* @param name section name
* @param sectionToggle section toggle
* @param key section key
*/
private void toggleSectionToggleChanged(final String name, final AppearanceJToggleButton sectionToggle, final String key)
{
PROPERTIES.setBoolean(key, sectionToggle.isSelected());
sectionToggle.setText(sectionToggle.isSelected() ? null : name);
boolean inSection = false;
for (Component component : OtsSimulationPanel.this.togglePanel.getComponents())
{
if (component.equals(sectionToggle))
{
inSection = true;
}
else if (inSection && component instanceof AppearanceJToggleButton)
{
return;
}
else if (inSection)
{
component.setVisible(sectionToggle.isSelected());
}
}
}
/**
* Add a button for toggling an animation class on or off. Button icons for which 'nextToPrevious' is true will be placed to
* the right of the previous button, which should be the corresponding button for id buttons. An example is an icon for
* showing/hiding the class 'Lane' followed by the button to show/hide the Lane ids. Other buttons can be placed next to the
* previous too.
* @param name the name of the button
* @param locatableClass the class for which the button holds (e.g., GTU.class)
* @param iconPath the path to the 24x24 icon to display
* @param toolTipText the tool tip text to show when hovering over the button
* @param initiallyVisible whether the class is initially shown or not
* @param nextToPrevious button that needs to be placed next to the previous button
*/
public void addToggleAnimationButtonIcon(final String name, final Class<? extends Locatable> locatableClass,
final String iconPath, final String toolTipText, final boolean initiallyVisible, final boolean nextToPrevious)
{
JToggleButton button;
Icon icon = IconUtil.of(iconPath).get();
Icon unIcon = IconUtil.of(iconPath).gray().get();
button = new JCheckBox();
button.setSelectedIcon(icon);
button.setIcon(unIcon);
button.setPreferredSize(new Dimension(32, 28));
button.setName(name);
String key = "toggle." + PropertiesStore.key(name);
boolean toggleOn = PROPERTIES.getOptionalBoolean(key).orElse(initiallyVisible);
button.setSelected(toggleOn);
PROPERTIES.setBoolean(key, toggleOn);
button.setActionCommand(name);
button.setToolTipText(toolTipText);
button.addActionListener(this);
// place button to the right of the previous content button?
if (nextToPrevious && this.togglePanel.getComponentCount() > 0)
{
JPanel lastToggleBox = (JPanel) this.togglePanel.getComponent(this.togglePanel.getComponentCount() - 1);
button.setVisible(true);
lastToggleBox.add(button);
}
else
{
JPanel toggleBox = new JPanel();
toggleBox.setVisible(this.toggleSectionVisible);
button.setVisible(true);
toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
toggleBox.add(button);
this.togglePanel.add(toggleBox);
toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
}
if (toggleOn)
{
this.otsAnimationPanel.showClass(locatableClass);
}
else
{
this.otsAnimationPanel.hideClass(locatableClass);
}
this.toggleLocatableMap.put(name, locatableClass);
this.toggleButtons.put(locatableClass, button);
}
/**
* Add a button for toggling an animation class on or off.
* @param name the name of the button
* @param locatableClass the class for which the button holds (e.g., {@code GTU.class})
* @param toolTipText the tool tip text to show when hovering over the button
* @param initiallyVisible whether the class is initially shown or not
*/
public void addToggleAnimationButtonText(final String name, final Class<? extends Locatable> locatableClass,
final String toolTipText, final boolean initiallyVisible)
{
JToggleButton button;
button = new JCheckBox(name);
button.setText(separatedName(name));
button.setVisible(this.toggleSectionVisible);
String key = "toggle." + PropertiesStore.key(name);
boolean toggleOn = PROPERTIES.getOptionalBoolean(key).orElse(initiallyVisible);
button.setSelected(toggleOn);
PROPERTIES.setBoolean(key, toggleOn);
button.setActionCommand(name);
button.setToolTipText(toolTipText);
button.addActionListener(this);
button.setPreferredSize(new Dimension(113, 19)); // Can just fit "Generator Q" at largest Appearance Control font size
button.setMaximumSize(new Dimension(113, 19));
this.togglePanel.add(button);
if (toggleOn)
{
this.otsAnimationPanel.showClass(locatableClass);
}
else
{
this.otsAnimationPanel.hideClass(locatableClass);
}
this.toggleLocatableMap.put(name, locatableClass);
this.toggleButtons.put(locatableClass, button);
}
/**
* Add a text to explain animation classes.
* @param text the text to show
*/
public void addToggleText(final String text)
{
JPanel textBox = new JPanel();
textBox.setVisible(this.toggleSectionVisible);
textBox.setLayout(new BoxLayout(textBox, BoxLayout.X_AXIS));
textBox.add(new JLabel(text));
this.togglePanel.add(textBox);
textBox.setAlignmentX(Component.LEFT_ALIGNMENT);
}
/**
* Add buttons for toggling all GIS layers on or off.
* @param header the name of the group of layers
* @param gisMap the GIS map for which the toggles have to be added
* @param toolTipText the tool tip text to show when hovering over the button
*/
public void addAllToggleGisButtonText(final String header, final GisRenderable2d gisMap, final String toolTipText)
{
addToggleText(" ");
addToggleText(header);
for (String layerName : gisMap.getMap().getLayerMap().keySet())
{
addToggleGisButtonText(layerName, layerName, gisMap, toolTipText, true);
}
}
/**
* Add a button to toggle a GIS Layer on or off.
* @param layerName the name of the layer
* @param displayName the name to display next to the tick box
* @param gisMap the map
* @param toolTipText the tool tip text
* @param initiallyVisible whether the layer is initially shown or not
*/
public void addToggleGisButtonText(final String layerName, final String displayName, final GisRenderable2d gisMap,
final String toolTipText, final boolean initiallyVisible)
{
JToggleButton button;
button = new JCheckBox(displayName);
button.setName(layerName);
button.setVisible(this.toggleSectionVisible);
String key = "toggle.gis." + PropertiesStore.key(layerName);
boolean toggleOn = PROPERTIES.getOptionalBoolean(key).orElse(initiallyVisible);
button.setSelected(toggleOn);
PROPERTIES.setBoolean(key, toggleOn);
button.setSelected(toggleOn);
button.setActionCommand(layerName);
button.setToolTipText(toolTipText);
button.addActionListener(this);
JPanel toggleBox = new JPanel();
toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
toggleBox.add(button);
this.togglePanel.add(toggleBox);
toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
this.toggleGisMap.put(layerName, gisMap.getMap());
this.toggleGisButtons.put(layerName, button);
}
/**
* Set a GIS layer to be shown in the animation to true.
* @param layerName the name of the GIS-layer that has to be shown
*/
public void showGisLayer(final String layerName)
{
GisMapInterface gisMap = this.toggleGisMap.get(layerName);
if (gisMap != null && !gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
{
toggleGisLayer(layerName);
}
}
/**
* Set a GIS layer to be hidden in the animation to true.
* @param layerName the name of the GIS-layer that has to be hidden
*/
public void hideGisLayer(final String layerName)
{
GisMapInterface gisMap = this.toggleGisMap.get(layerName);
if (gisMap != null && gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
{
toggleGisLayer(layerName);
}
}
/**
* Toggle a GIS layer to be displayed in the animation to its reverse value.
* @param layerName the name of the GIS-layer that has to be turned off or vice versa
*/
public void toggleGisLayer(final String layerName)
{
GisMapInterface gisMap = this.toggleGisMap.get(layerName);
if (gisMap != null)
{
boolean show = !gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName));
String key = "toggle.gis." + PropertiesStore.key(layerName);
PROPERTIES.setBoolean(key, show);
if (show)
{
gisMap.showLayer(layerName);
this.toggleGisButtons.get(layerName).setSelected(true);
}
else
{
gisMap.hideLayer(layerName);
this.toggleGisButtons.get(layerName).setSelected(false);
}
this.otsAnimationPanel.repaint();
}
}
@Override
public void actionPerformed(final ActionEvent actionEvent)
{
String actionCommand = actionEvent.getActionCommand();
Logger.ots().trace("Action command is " + actionCommand);
try
{
if (actionCommand.equals("Reset Y-zoom"))
{
this.otsAnimationPanel.resetZoomY();
}
else if (actionCommand.equals("Home"))
{
this.otsAnimationPanel.resetZoomY();
this.otsAnimationPanel.home();
}
else if (actionCommand.equals("ZoomAll"))
{
this.otsAnimationPanel.resetZoomY();
this.otsAnimationPanel.zoomAll();
}
else if (actionCommand.equals("Grid"))
{
this.otsAnimationPanel.showGrid(!this.otsAnimationPanel.isShowGrid());
PROPERTIES.setBoolean("grid", this.otsAnimationPanel.isShowGrid());
}
else if (this.toggleLocatableMap.containsKey(actionCommand))
{
Class<? extends Locatable> locatableClass = this.toggleLocatableMap.get(actionCommand);
this.otsAnimationPanel.toggleClass(locatableClass);
String key = "toggle." + PropertiesStore.key(actionCommand);
PROPERTIES.setBoolean(key, this.otsAnimationPanel.isShowClass(locatableClass));
this.togglePanel.repaint();
}
else if (this.toggleGisMap.containsKey(actionCommand))
{
this.toggleGisLayer(actionCommand);
this.togglePanel.repaint();
}
}
catch (Exception exception)
{
exception.printStackTrace();
}
}
/**
* Easy access to the AnimationPanel.
* @return animation panel
*/
public AnimationPanel getAnimationPanel()
{
return this.otsAnimationPanel;
}
/**
* Creates a demo panel within the animation area.
* @param position position within the animation panel
* @throws IllegalStateException if the panel was already created
*/
public void createDemoPanel(final DemoPanelPosition position)
{
Throw.when(this.demoPanel != null, IllegalStateException.class,
"Attempt to create demo panel, but it's already created");
Throw.whenNull(position, "Position may not be null.");
Container parent = this.otsAnimationPanel.getParent();
parent.remove(this.otsAnimationPanel);
JPanel splitPanel = new JPanel(new BorderLayout());
parent.add(splitPanel);
splitPanel.add(this.otsAnimationPanel, BorderLayout.CENTER);
this.demoPanel = new JPanel();
this.demoPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
splitPanel.add(this.demoPanel, position.getBorderLayoutPosition());
}
/**
* Return a panel for on-screen demo controls. The panel is created on the right of the screen on first call, unless
* {@link #createDemoPanel} was already called.
* @return demo panel
*/
public JPanel getDemoPanel()
{
if (this.demoPanel == null)
{
createDemoPanel(DemoPanelPosition.RIGHT);
}
return this.demoPanel;
}
/**
* Update the check-mark related to a programmatically changed animation state.
* @param locatableClass class to show the check-mark for
*/
public void updateAnimationClassCheckBox(final Class<? extends Locatable> locatableClass)
{
JToggleButton button = this.toggleButtons.get(locatableClass);
if (button == null)
{
return;
}
button.setSelected(getAnimationPanel().isShowClass(locatableClass));
}
/**
* Display the latest world coordinate based on the mouse position on the screen.
*/
private void updateWorldCoordinate()
{
String x = String.format(COORD_FORMAT, this.otsAnimationPanel.getWorldCoordinate().getX());
String y = String.format(COORD_FORMAT, this.otsAnimationPanel.getWorldCoordinate().getY());
String worldPoint = "<html>(x=" + fadeLeadingZeros(x) + "; y=" + fadeLeadingZeros(y) + ")</html>";
this.coordinateField.setText(worldPoint);
String worldPointNoHtml = "(x=" + x + "; y=" + y + ")";
if (this.coordinateField.getGraphics() == null)
{
// window is in a deleted state, but the mouse listener is still causing this event
return;
}
// add 10px margin for if the window is dragged to another monitor and Swing finds a few more pixels required there due
// to the monitor being set to e.g. 125% instead of 100%
int requiredWidth = this.coordinateField.getGraphics().getFontMetrics().stringWidth(worldPointNoHtml) + 10;
if (this.coordinateField.getPreferredSize().width < requiredWidth)
{
Dimension requiredSize = new Dimension(requiredWidth, this.coordinateField.getPreferredSize().height);
this.coordinateField.setMinimumSize(requiredSize);
this.coordinateField.setPreferredSize(requiredSize);
this.coordinateField.setMaximumSize(requiredSize);
Container parent = this.coordinateField.getParent();
requiredSize = new Dimension(requiredWidth, parent.getPreferredSize().height);
parent.setMinimumSize(requiredSize);
parent.setPreferredSize(requiredSize);
parent.setMaximumSize(requiredSize);
Logger.ots().trace("Increased minimum width to " + requiredSize.width);
parent.revalidate();
}
this.coordinateField.repaint();
}
/**
* Gives the leading zeros a faded color using HTML.
* @param formatted formatted number
* @return formatted number with leading zeros faded
*/
private String fadeLeadingZeros(final String formatted)
{
Matcher m = LEADING_ZEROS.matcher(formatted);
if (!m.matches())
{
return formatted;
}
String sign = m.group(1) == null ? "" : m.group(1);
String zeros = m.group(2) == null ? "" : m.group(2);
String digits = m.group(3);
Color faded = ColorInterpolator.interpolateColor(this.coordinateField.getBackground(),
this.gtuCountField.getForeground(), 0.2);
String zerosColor = String.format("#%02x%02x%02x", faded.getRed(), faded.getGreen(), faded.getBlue());
return sign + "<span style='color:" + zerosColor + ";'>" + zeros + "</span>" + digits;
}
/**
* GTU colorer manager from the GTU color panel.
* @return GTU colorer manager from the GTU color panel
*/
public GtuColorerManager getGtuColorerManager()
{
return this.gtuColorPanel.getGtuColorerManager();
}
@Override
public void notify(final Event event)
{
if (event.getType().equals(Network.GTU_ADD_EVENT))
{
this.gtuCount++;
setGtuCountText();
}
else if (event.getType().equals(Network.GTU_REMOVE_EVENT))
{
this.gtuCount--;
setGtuCountText();
}
}
/**
* Updates the text of the GTU counter.
*/
private void setGtuCountText()
{
String text = this.gtuCount + " GTU's";
this.gtuCountField.setText(text);
}
/**
* Adds a thin space before each capital character in a {@code String}, except the first.
* @param name name of node
* @return input string but with a thin space before each capital character, except the first
*/
public static String separatedName(final String name)
{
String[] parts = UPPER_PATTERN.split(name);
if (parts.length == 1)
{
return parts[0];
}
String separator = "";
StringBuilder stringBuilder = new StringBuilder();
for (String part : parts)
{
stringBuilder.append(separator).append(part);
separator = "\u2009"; // thin space
}
return stringBuilder.toString();
}
/**
* Toggle button with appearance control.
*/
private final class AppearanceJToggleButton extends JCheckBox implements AppearanceControl
{
/** Serialization version UID. */
private static final long serialVersionUID = 1L;
@Override
public boolean isForeground()
{
return true;
}
@Override
public OptionalInt getFontSize()
{
return OptionalInt.empty();
}
}
/**
* Toggle section data.
* @param name section name
* @param defaultVisible whether the section is visible by default
*/
private record ToggleSectionData(String name, boolean defaultVisible)
{
}
/**
* Animation panel that adds auto-pan functionality.
*/
private class OtsAnimationPanel extends AnimationPanel
{
/** Serialization version UID. */
private static final long serialVersionUID = 20180430L;
/** Network. */
private final Network network;
/**
* Constructor.
* @param extent home extent
* @param simulator simulator
* @param network network
* @throws RemoteException on remote animation error
* @throws DsolException when simulator does not implement AnimatorInterface
*/
OtsAnimationPanel(final Rectangle2D extent, final OtsSimulatorInterface simulator, final Network network)
throws RemoteException, DsolException
{
super(new Bounds2d(extent.getMinX(), extent.getMaxX(), extent.getMinY(), extent.getMaxY()), simulator);
setPreferredSize(new Dimension(800, 600));
this.network = network;
MouseListener[] listeners = getMouseListeners();
for (MouseListener listener : listeners)
{
removeMouseListener(listener);
}
this.addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(final MouseEvent e)
{
if (e.isControlDown())
{
Gtu gtu = getSelectedGtu(e.getPoint());
if (gtu != null)
{
OtsSimulationPanel.this.otsSearchPanel.selectAndTrackObject("GTU", gtu.getId(), true);
}
}
e.consume();
}
});
for (MouseListener listener : listeners)
{
addMouseListener(listener);
}
}
/**
* Set the world coordinates based on a mouse move.
* @param point the x,y world coordinates
*/
@Override
public synchronized void setWorldCoordinate(final Point2d point)
{
super.setWorldCoordinate(point);
updateWorldCoordinate();
}
/**
* returns the list of selected objects at a certain mousePoint.
* @param mousePoint the mousePoint
* @return the selected objects
*/
private Gtu getSelectedGtu(final Point2D mousePoint)
{
List<LaneBasedGtu> targets = new ArrayList<>();
Point2d point = getRenderableScale().getWorldCoordinates(mousePoint, getExtent(), getSize());
for (Renderable2dInterface<?> renderable : getElements())
{
if (isShowElement(renderable) && renderable.contains(point, getExtent()))
{
if (renderable.getSource() instanceof AnimationGtuData animData)
{
targets.add(animData.getObject());
}
}
}
if (targets.size() == 1)
{
return targets.get(0);
}
return null;
}
@Override
public void paintComponent(final Graphics g)
{
final OtsSearchPanel.ObjectKind<?> panKind = OtsSimulationPanel.this.autoPanKind;
final String panId = OtsSimulationPanel.this.autoPanId;
final boolean doPan = OtsSimulationPanel.this.autoPanOnNextPaintComponent;
OtsSimulationPanel.this.autoPanOnNextPaintComponent = OtsSimulationPanel.this.autoPanTrack;
if (doPan && panKind != null && panId != null)
{
Optional<? extends Locatable> locatable = panKind.searchNetwork(this.network, panId);
if (locatable.isPresent())
{
Point<?> point = locatable.get().getLocation();
if (point != null) // Center extent around point
{
double w = getExtent().getDeltaX();
double h = getExtent().getDeltaY();
setExtent(new Bounds2d(point.getX() - w / 2, point.getX() + w / 2, point.getY() - h / 2,
point.getY() + h / 2));
}
}
}
super.paintComponent(g);
}
@Override
public void setBackground(final Color bg)
{
int threshold = 64;
int alternative = 96;
if (bg.getRed() <= threshold && bg.getGreen() <= threshold && bg.getBlue() <= threshold)
{
setGridColor(new Color(alternative, alternative, alternative));
}
else
{
setGridColor(Color.BLACK);
}
super.setBackground(bg);
}
// Overridden because there are rounding and vertical mod errors in the super implementation.
// See https://github.com/averbraeck/dsol/issues/116.
@Override
protected synchronized void drawGrid(final Graphics g)
{
// we prepare the graphics object for the grid
g.setFont(g.getFont().deriveFont(11.0f));
g.setColor(this.getGridColor());
double scaleX = this.getRenderableScale().getXScale(this.getExtent(), this.getSize());
double scaleY = this.getRenderableScale().getYScale(this.getExtent(), this.getSize());
int count = 0;
double gridSizePixelsX = this.gridSizeX / scaleX;
while (gridSizePixelsX < 40)
{
this.gridSizeX = 10 * this.gridSizeX;
int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
if (count++ > 10)
{
break;
}
}
count = 0;
while (gridSizePixelsX > 10 * 40)
{
int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
this.gridSizeX = this.gridSizeX / 10;
gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
if (count++ > 10)
{
break;
}
}
double gridSizePixelsY = this.gridSizeY / scaleY;
while (gridSizePixelsY < 40)
{
this.gridSizeY = 10 * this.gridSizeY;
int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
if (count++ > 10)
{
break;
}
}
count = 0;
while (gridSizePixelsY > 10 * 40)
{
int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
this.gridSizeY = this.gridSizeY / 10;
gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
if (count++ > 10)
{
break;
}
}
// Let's draw the vertical lines
double mod = this.getExtent().getMinX() % this.gridSizeX;
double x = -mod / scaleX;
while (x < this.getWidth())
{
Point2d point = this.getRenderableScale().getWorldCoordinates(new Point2D.Double(x, 0), this.getExtent(),
this.getSize());
if (point != null)
{
String label = this.formatter.format(Math.round(point.getX() / this.gridSizeX) * this.gridSizeX);
double labelWidth = this.getFontMetrics(this.getFont()).getStringBounds(label, g).getWidth();
if (x > labelWidth + 4)
{
int xInt = (int) Math.round(x);
g.drawLine(xInt, 15, xInt, this.getHeight());
g.drawString(label, (int) Math.round(x - 0.5 * labelWidth), 11);
}
}
x = x + gridSizePixelsX;
}
// Let's draw the horizontal lines
mod = this.getExtent().getMinY() % this.gridSizeY;
double y = this.getSize().getHeight() + (mod / scaleY);
while (y > 15)
{
Point2d point = this.getRenderableScale().getWorldCoordinates(new Point2D.Double(0, y), this.getExtent(),
this.getSize());
if (point != null)
{
String label = this.formatter.format(Math.round(point.getY() / this.gridSizeY) * this.gridSizeY);
RectangularShape labelBounds = this.getFontMetrics(this.getFont()).getStringBounds(label, g);
int yInt = (int) Math.round(y);
g.drawLine((int) Math.round(labelBounds.getWidth() + 4), yInt, this.getWidth(), yInt);
g.drawString(label, 2, (int) Math.round(y + labelBounds.getHeight() * 0.3));
}
y = y - gridSizePixelsY;
}
}
@Override
public String toString()
{
return "OtsAnimationPanel [network=" + this.network + "]";
}
}
/**
* Enum for demo panel position. Each value contains a field representing the position correlating to the
* {@link BorderLayout} class.
*/
public enum DemoPanelPosition
{
/** Top. */
TOP("First"),
/** Bottom. */
BOTTOM("Last"),
/** Left. */
LEFT("Before"),
/** Right. */
RIGHT("After");
/** Value used in {@link BorderLayout}. */
private final String direction;
/**
* Constructor.
* @param direction value used in {@link BorderLayout}
*/
DemoPanelPosition(final String direction)
{
this.direction = direction;
}
/**
* Return border layout position.
* @return value used in {@link BorderLayout}
*/
public String getBorderLayoutPosition()
{
return this.direction;
}
}
}