package nl.tudelft.simulation.dsol.web.animation.D2;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.djutils.draw.bounds.Bounds;
import org.djutils.draw.bounds.Bounds2d;
import org.djutils.draw.point.Point;
import org.djutils.event.EventInterface;
import org.djutils.event.EventListenerInterface;
import org.opentrafficsim.core.animation.gtu.colorer.GTUColorer;
import nl.tudelft.simulation.dsol.animation.Locatable;
import nl.tudelft.simulation.dsol.animation.D2.Renderable2DComparator;
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.ReplicationInterface;
import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
import nl.tudelft.simulation.dsol.web.animation.HTMLGraphics2D;
import nl.tudelft.simulation.naming.context.ContextInterface;
import nl.tudelft.simulation.naming.context.util.ContextUtil;
/**
* The AnimationPanel to display animated (Locatable) objects. Added the possibility to witch layers on and off. By default all
* layers will be drawn, so no changes to existing software need to be made.
*
* Copyright (c) 2003-2022 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License.
*
* @author Alexander Verbraeck
*/
public class HTMLAnimationPanel extends HTMLGridPanel implements EventListenerInterface
{
/** */
private static final long serialVersionUID = 1L;
/** the elements of this panel. */
private SortedSet> elements =
new TreeSet>(new Renderable2DComparator());
/** filter for types to be shown or not. */
private Map, Boolean> visibilityMap = new LinkedHashMap<>();
/** cache of the classes that are hidden. */
private Set> hiddenClasses = new LinkedHashSet<>();
/** cache of the classes that are shown. */
private Set> shownClasses = new LinkedHashSet<>();
/** the simulator. */
private SimulatorInterface, ?, ?> simulator;
/** the eventContext. */
private ContextInterface context = null;
/** a line that helps the user to see where s/he is dragging. */
private int[] dragLine = new int[4];
/** enable drag line. */
private boolean dragLineEnabled = false;
/** List of drawable objects. */
private List> elementList = new ArrayList<>();
/** dirty flag for the list. */
private boolean dirtyElements = false;
/** Map of toggle names to toggle animation classes. */
private Map> toggleLocatableMap = new LinkedHashMap<>();
/** Set of animation classes to toggle buttons. */
private Map, ToggleButtonInfo> toggleButtonMap = new LinkedHashMap<>();
/** Set of GIS layer names to toggle GIS layers . */
private Map toggleGISMap = new LinkedHashMap<>();
/** Set of GIS layer names to toggle buttons. */
private Map toggleGISButtonMap = new LinkedHashMap<>();
/** List of buttons in the right order. */
private List toggleButtons = new ArrayList<>();
/** The switchableGTUColorer used to color the GTUs. */
private GTUColorer gtuColorer = null;
/** the margin factor 'around' the extent. */
public static final double EXTENT_MARGIN_FACTOR = 0.05;
/**
* constructs a new AnimationPanel.
* @param homeExtent Bounds2d; the extent of the panel
* @param simulator SimulatorInterface<?,?,?>; the simulator of which we want to know the events for animation
* @throws RemoteException on network error for one of the listeners
*/
public HTMLAnimationPanel(final Bounds2d homeExtent, final SimulatorInterface, ?, ?> simulator) throws RemoteException
{
super(homeExtent);
super.showGrid = true;
this.simulator = simulator;
simulator.addListener(this, ReplicationInterface.START_REPLICATION_EVENT);
}
/** {@inheritDoc} */
@Override
public void paintComponent(final HTMLGraphics2D g2)
{
// draw the grid.
super.paintComponent(g2);
// update drawable elements when necessary
if (this.dirtyElements)
{
synchronized (this.elementList)
{
this.elementList.clear();
this.elementList.addAll(this.elements);
this.dirtyElements = false;
}
}
// draw the animation elements.
for (Renderable2DInterface extends Locatable> element : this.elementList)
{
// destroy has been called?
if (element.getSource() == null)
{
objectRemoved(element);
}
else if (isShowElement(element))
{
AffineTransform at = (AffineTransform) g2.getTransform().clone();
element.paintComponent(g2, this.getExtent(), this.getSize(), this.renderableScale, this);
g2.setTransform(at);
}
}
// draw drag line if enabled.
if (this.dragLineEnabled)
{
g2.setColor(Color.BLACK);
g2.drawLine(this.dragLine[0], this.dragLine[1], this.dragLine[2], this.dragLine[3]);
this.dragLineEnabled = false;
}
}
/**
* Test whether the element needs to be shown on the screen or not.
* @param element Renderable2DInterface<? extends Locatable>; the renderable element to test
* @return whether the element needs to be shown or not
*/
public boolean isShowElement(final Renderable2DInterface extends Locatable> element)
{
return element.getSource() == null ? false : isShowClass(element.getSource().getClass());
}
/**
* Test whether a certain class needs to be shown on the screen or not. The class needs to implement Locatable, otherwise it
* cannot be shown at all.
* @param locatableClass Class<? extends Locatable>; the class to test
* @return whether the class needs to be shown or not
*/
public boolean isShowClass(final Class extends Locatable> locatableClass)
{
if (this.hiddenClasses.contains(locatableClass))
{
return false;
}
else
{
boolean show = true;
if (!this.shownClasses.contains(locatableClass))
{
for (Class extends Locatable> lc : this.visibilityMap.keySet())
{
if (lc.isAssignableFrom(locatableClass))
{
if (!this.visibilityMap.get(lc))
{
show = false;
}
}
}
// add to the right cache
if (show)
{
this.shownClasses.add(locatableClass);
}
else
{
this.hiddenClasses.add(locatableClass);
}
}
return show;
}
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override
public void notify(final EventInterface event) throws RemoteException
{
if (event.getType().equals(ContextInterface.OBJECT_ADDED_EVENT))
{
objectAdded((Renderable2DInterface extends Locatable>) ((Object[]) event.getContent())[2]);
}
else if (event.getType().equals(ContextInterface.OBJECT_REMOVED_EVENT))
{
objectRemoved((Renderable2DInterface extends Locatable>) ((Object[]) event.getContent())[2]);
}
else if // (this.simulator.getSourceId().equals(event.getSourceId()) &&
(event.getType().equals(ReplicationInterface.START_REPLICATION_EVENT))
{
synchronized (this.elementList)
{
this.elements.clear();
try
{
if (this.context != null)
{
this.context.removeListener(this, ContextInterface.OBJECT_ADDED_EVENT);
this.context.removeListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
}
this.context =
ContextUtil.lookupOrCreateSubContext(this.simulator.getReplication().getContext(), "animation/2D");
this.context.addListener(this, ContextInterface.OBJECT_ADDED_EVENT);
this.context.addListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
for (Object element : this.context.values())
{
objectAdded((Renderable2DInterface extends Locatable>) element);
}
this.repaint();
}
catch (Exception exception)
{
this.simulator.getLogger().always().warn(exception, "notify");
}
}
}
}
/**
* Add a locatable object to the animation.
* @param element Renderable2DInterface<? extends Locatable>; the element to add to the animation
*/
public void objectAdded(final Renderable2DInterface extends Locatable> element)
{
synchronized (this.elementList)
{
this.elements.add(element);
this.dirtyElements = true;
}
}
/**
* Remove a locatable object from the animation.
* @param element Renderable2DInterface<? extends Locatable>; the element to add to the animation
*/
public void objectRemoved(final Renderable2DInterface extends Locatable> element)
{
synchronized (this.elementList)
{
this.elements.remove(element);
this.dirtyElements = true;
}
}
/**
* Calculate the full extent based on the current positions of the objects.
* @return Bounds2d; the full extent of the animation.
*/
public synchronized Bounds2d fullExtent()
{
double minX = Double.MAX_VALUE;
double maxX = -Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
try
{
for (Renderable2DInterface extends Locatable> renderable : this.elementList)
{
if (renderable.getSource() == null)
{
continue;
}
Point> l = renderable.getSource().getLocation();
if (l != null)
{
Bounds, ?, ?> b = renderable.getSource().getBounds();
minX = Math.min(minX, l.getX() + b.getMinX());
minY = Math.min(minY, l.getY() + b.getMinY());
maxX = Math.max(maxX, l.getX() + b.getMaxX());
maxY = Math.max(maxY, l.getY() + b.getMaxY());
}
}
}
catch (Exception e)
{
// ignore
}
minX -= EXTENT_MARGIN_FACTOR * Math.abs(maxX - minX);
minY -= EXTENT_MARGIN_FACTOR * Math.abs(maxY - minY);
maxX += EXTENT_MARGIN_FACTOR * Math.abs(maxX - minX);
maxY += EXTENT_MARGIN_FACTOR * Math.abs(maxY - minY);
return new Bounds2d(minX, maxX, minY, maxY);
}
/**
* resets the panel to its an extent that covers all displayed objects.
*/
public synchronized void zoomAll()
{
setExtent(getRenderableScale().computeVisibleExtent(fullExtent(), this.getSize()));
this.repaint();
}
/**
* Set a class to be shown in the animation to true.
* @param locatableClass Class<? extends Locatable>; the class for which the animation has to be shown.
*/
public void showClass(final Class extends Locatable> locatableClass)
{
this.visibilityMap.put(locatableClass, true);
this.shownClasses.clear();
this.hiddenClasses.clear();
this.repaint();
}
/**
* Set a class to be hidden in the animation to true.
* @param locatableClass Class<? extends Locatable>; the class for which the animation has to be hidden.
*/
public void hideClass(final Class extends Locatable> locatableClass)
{
this.visibilityMap.put(locatableClass, false);
this.shownClasses.clear();
this.hiddenClasses.clear();
this.repaint();
}
/**
* Toggle a class to be displayed in the animation to its reverse value.
* @param locatableClass Class<? extends Locatable>; the class for which a visible animation has to be turned off or
* vice versa.
*/
public void toggleClass(final Class extends Locatable> locatableClass)
{
if (!this.visibilityMap.containsKey(locatableClass))
{
showClass(locatableClass);
}
this.visibilityMap.put(locatableClass, !this.visibilityMap.get(locatableClass));
this.shownClasses.clear();
this.hiddenClasses.clear();
this.repaint();
}
/**
* @return the set of animation elements.
*/
public final SortedSet> getElements()
{
return this.elements;
}
/**
* @return returns the dragLine.
*/
public final int[] getDragLine()
{
return this.dragLine;
}
/**
* @return returns the dragLineEnabled.
*/
public final boolean isDragLineEnabled()
{
return this.dragLineEnabled;
}
/**
* @param dragLineEnabled boolean; the dragLineEnabled to set.
*/
public final void setDragLineEnabled(final boolean dragLineEnabled)
{
this.dragLineEnabled = dragLineEnabled;
}
/**********************************************************************************************************/
/******************************************* TOGGLES ******************************************************/
/**********************************************************************************************************/
/**
* Add a button for toggling an animatable class on or off.
* @param name String; the name of the button
* @param locatableClass Class<? extends Locatable>; the class for which the button holds (e.g., GTU.class)
* @param toolTipText String; the tool tip text to show when hovering over the button
* @param initiallyVisible boolean; whether the class is initially shown or not
*/
public final void addToggleAnimationButtonText(final String name, final Class extends Locatable> locatableClass,
final String toolTipText, final boolean initiallyVisible)
{
ToggleButtonInfo.LocatableClass buttonInfo =
new ToggleButtonInfo.LocatableClass(name, locatableClass, toolTipText, initiallyVisible);
if (initiallyVisible)
{
showClass(locatableClass);
}
else
{
hideClass(locatableClass);
}
this.toggleButtons.add(buttonInfo);
this.toggleLocatableMap.put(name, locatableClass);
this.toggleButtonMap.put(locatableClass, buttonInfo);
}
/**
* Show a Locatable class based on the name.
* @param name String; the name of the class to show
*/
public final void showClass(final String name)
{
showClass(this.toggleLocatableMap.get(name));
}
/**
* Hide a Locatable class based on the name.
* @param name String; the name of the class to hide
*/
public final void hideClass(final String name)
{
hideClass(this.toggleLocatableMap.get(name));
}
/**
* Add a text to explain animatable classes.
* @param text String; the text to show
*/
public final void addToggleText(final String text)
{
this.toggleButtons.add(new ToggleButtonInfo.Text(text, true));
}
/**
* Add buttons for toggling all GIS layers on or off.
* @param header String; the name of the group of layers
* @param gisMap GisRenderable2D; the GIS map for which the toggles have to be added
* @param toolTipText String; the tool tip text to show when hovering over the button
*/
public final void addAllToggleGISButtonText(final String header, final GisRenderable2D gisMap, final String toolTipText)
{
addToggleText(" ");
addToggleText(header);
try
{
for (String layerName : gisMap.getMap().getLayerMap().keySet())
{
addToggleGISButtonText(layerName, layerName, gisMap, toolTipText);
}
}
catch (RemoteException exception)
{
exception.printStackTrace();
}
}
/**
* Add a button to toggle a GIS Layer on or off.
* @param layerName String; the name of the layer
* @param displayName String; the name to display next to the tick box
* @param gisMap GisRenderable2D; the map
* @param toolTipText String; the tool tip text
*/
public final void addToggleGISButtonText(final String layerName, final String displayName, final GisRenderable2D gisMap,
final String toolTipText)
{
ToggleButtonInfo.Gis buttonInfo = new ToggleButtonInfo.Gis(displayName, layerName, toolTipText, true);
this.toggleButtons.add(buttonInfo);
this.toggleGISMap.put(layerName, gisMap.getMap());
this.toggleGISButtonMap.put(layerName, buttonInfo);
}
/**
* Set a GIS layer to be shown in the animation to true.
* @param layerName String; the name of the GIS-layer that has to be shown.
*/
public final void showGISLayer(final String layerName)
{
GisMapInterface gisMap = this.toggleGISMap.get(layerName);
if (gisMap != null)
{
try
{
gisMap.showLayer(layerName);
this.toggleGISButtonMap.get(layerName).setVisible(true);
}
catch (RemoteException exception)
{
exception.printStackTrace();
}
}
}
/**
* Set a GIS layer to be hidden in the animation to true.
* @param layerName String; the name of the GIS-layer that has to be hidden.
*/
public final void hideGISLayer(final String layerName)
{
GisMapInterface gisMap = this.toggleGISMap.get(layerName);
if (gisMap != null)
{
try
{
gisMap.hideLayer(layerName);
this.toggleGISButtonMap.get(layerName).setVisible(false);
}
catch (RemoteException exception)
{
exception.printStackTrace();
}
}
}
/**
* Toggle a GIS layer to be displayed in the animation to its reverse value.
* @param layerName String; the name of the GIS-layer that has to be turned off or vice versa.
*/
public final void toggleGISLayer(final String layerName)
{
GisMapInterface gisMap = this.toggleGISMap.get(layerName);
if (gisMap != null)
{
try
{
if (gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
{
gisMap.hideLayer(layerName);
this.toggleGISButtonMap.get(layerName).setVisible(false);
}
else
{
gisMap.showLayer(layerName);
this.toggleGISButtonMap.get(layerName).setVisible(true);
}
}
catch (RemoteException exception)
{
exception.printStackTrace();
}
}
}
/**
* @return toggleButtons
*/
public final List getToggleButtons()
{
return this.toggleButtons;
}
}