View Javadoc
1   package nl.tudelft.simulation.dsol.web.animation.D2;
2   
3   import java.awt.Color;
4   import java.awt.geom.AffineTransform;
5   import java.rmi.RemoteException;
6   import java.util.ArrayList;
7   import java.util.LinkedHashMap;
8   import java.util.LinkedHashSet;
9   import java.util.List;
10  import java.util.Map;
11  import java.util.Set;
12  import java.util.SortedSet;
13  import java.util.TreeSet;
14  
15  import org.djutils.draw.bounds.Bounds;
16  import org.djutils.draw.bounds.Bounds2d;
17  import org.djutils.draw.point.Point;
18  import org.djutils.event.EventInterface;
19  import org.djutils.event.EventListenerInterface;
20  import org.opentrafficsim.core.animation.gtu.colorer.GTUColorer;
21  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
22  
23  import nl.tudelft.simulation.dsol.animation.Locatable;
24  import nl.tudelft.simulation.dsol.animation.D2.Renderable2DComparator;
25  import nl.tudelft.simulation.dsol.animation.D2.Renderable2DInterface;
26  import nl.tudelft.simulation.dsol.animation.gis.GisMapInterface;
27  import nl.tudelft.simulation.dsol.animation.gis.GisRenderable2D;
28  import nl.tudelft.simulation.dsol.experiment.ReplicationInterface;
29  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
30  import nl.tudelft.simulation.dsol.web.animation.HTMLGraphics2D;
31  import nl.tudelft.simulation.naming.context.ContextInterface;
32  import nl.tudelft.simulation.naming.context.util.ContextUtil;
33  
34  /**
35   * The AnimationPanel to display animated (Locatable) objects. Added the possibility to witch layers on and off. By default all
36   * layers will be drawn, so no changes to existing software need to be made.
37   * <p>
38   * Copyright (c) 2003-2022 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
39   * BSD-style license. See <a href="https://opentrafficsim.org/docs/v2/license.html">OpenTrafficSim License</a>.
40   * </p>
41   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
42   */
43  public class HTMLAnimationPanel extends HTMLGridPanel implements EventListenerInterface
44  {
45      /** */
46      private static final long serialVersionUID = 1L;
47  
48      /** the elements of this panel. */
49      private SortedSet<Renderable2DInterface<? extends Locatable>> elements =
50              new TreeSet<Renderable2DInterface<? extends Locatable>>(new Renderable2DComparator());
51  
52      /** filter for types to be shown or not. */
53      private Map<Class<? extends Locatable>, Boolean> visibilityMap = new LinkedHashMap<>();
54  
55      /** cache of the classes that are hidden. */
56      private Set<Class<? extends Locatable>> hiddenClasses = new LinkedHashSet<>();
57  
58      /** cache of the classes that are shown. */
59      private Set<Class<? extends Locatable>> shownClasses = new LinkedHashSet<>();
60  
61      /** the simulator. */
62      private OTSSimulatorInterface simulator;
63  
64      /** the eventContext. */
65      private ContextInterface context = null;
66  
67      /** a line that helps the user to see where s/he is dragging. */
68      private int[] dragLine = new int[4];
69  
70      /** enable drag line. */
71      private boolean dragLineEnabled = false;
72  
73      /** List of drawable objects. */
74      private List<Renderable2DInterface<? extends Locatable>> elementList = new ArrayList<>();
75  
76      /** dirty flag for the list. */
77      private boolean dirtyElements = false;
78  
79      /** Map of toggle names to toggle animation classes. */
80      private Map<String, Class<? extends Locatable>> toggleLocatableMap = new LinkedHashMap<>();
81  
82      /** Set of animation classes to toggle buttons. */
83      private Map<Class<? extends Locatable>, ToggleButtonInfo> toggleButtonMap = new LinkedHashMap<>();
84  
85      /** Set of GIS layer names to toggle GIS layers . */
86      private Map<String, GisMapInterface> toggleGISMap = new LinkedHashMap<>();
87  
88      /** Set of GIS layer names to toggle buttons. */
89      private Map<String, ToggleButtonInfo> toggleGISButtonMap = new LinkedHashMap<>();
90  
91      /** List of buttons in the right order. */
92      private List<ToggleButtonInfo> toggleButtons = new ArrayList<>();
93  
94      /** The switchableGTUColorer used to color the GTUs. */
95      private GTUColorer gtuColorer = null;
96  
97      /** the margin factor 'around' the extent. */
98      public static final double EXTENT_MARGIN_FACTOR = 0.05;
99  
100     /**
101      * constructs a new AnimationPanel.
102      * @param homeExtent Bounds2d; the extent of the panel
103      * @param simulator SimulatorInterface&lt;?,?,?&gt;; the simulator of which we want to know the events for animation
104      * @throws RemoteException on network error for one of the listeners
105      */
106     public HTMLAnimationPanel(final Bounds2d homeExtent, final OTSSimulatorInterface simulator) throws RemoteException
107     {
108         super(homeExtent);
109         super.showGrid = true;
110         this.simulator = simulator;
111         simulator.addListener(this, ReplicationInterface.START_REPLICATION_EVENT);
112     }
113 
114     /** {@inheritDoc} */
115     @Override
116     public void paintComponent(final HTMLGraphics2D g2)
117     {
118         // draw the grid.
119         super.paintComponent(g2);
120 
121         // update drawable elements when necessary
122         if (this.dirtyElements)
123         {
124             synchronized (this.elementList)
125             {
126                 this.elementList.clear();
127                 this.elementList.addAll(this.elements);
128                 this.dirtyElements = false;
129             }
130         }
131 
132         // draw the animation elements.
133         for (Renderable2DInterface<? extends Locatable> element : this.elementList)
134         {
135             // destroy has been called?
136             if (element.getSource() == null)
137             {
138                 objectRemoved(element);
139             }
140             else if (isShowElement(element))
141             {
142                 AffineTransform at = (AffineTransform) g2.getTransform().clone();
143                 element.paintComponent(g2, this.getExtent(), this.getSize(), this.renderableScale, this);
144                 g2.setTransform(at);
145             }
146         }
147 
148         // draw drag line if enabled.
149         if (this.dragLineEnabled)
150         {
151             g2.setColor(Color.BLACK);
152             g2.drawLine(this.dragLine[0], this.dragLine[1], this.dragLine[2], this.dragLine[3]);
153             this.dragLineEnabled = false;
154         }
155     }
156 
157     /**
158      * Test whether the element needs to be shown on the screen or not.
159      * @param element Renderable2DInterface&lt;? extends Locatable&gt;; the renderable element to test
160      * @return whether the element needs to be shown or not
161      */
162     public boolean isShowElement(final Renderable2DInterface<? extends Locatable> element)
163     {
164         return element.getSource() == null ? false : isShowClass(element.getSource().getClass());
165     }
166 
167     /**
168      * Test whether a certain class needs to be shown on the screen or not. The class needs to implement Locatable, otherwise it
169      * cannot be shown at all.
170      * @param locatableClass Class&lt;? extends Locatable&gt;; the class to test
171      * @return whether the class needs to be shown or not
172      */
173     public boolean isShowClass(final Class<? extends Locatable> locatableClass)
174     {
175         if (this.hiddenClasses.contains(locatableClass))
176         {
177             return false;
178         }
179         else
180         {
181             boolean show = true;
182             if (!this.shownClasses.contains(locatableClass))
183             {
184                 for (Class<? extends Locatable> lc : this.visibilityMap.keySet())
185                 {
186                     if (lc.isAssignableFrom(locatableClass))
187                     {
188                         if (!this.visibilityMap.get(lc))
189                         {
190                             show = false;
191                         }
192                     }
193                 }
194                 // add to the right cache
195                 if (show)
196                 {
197                     this.shownClasses.add(locatableClass);
198                 }
199                 else
200                 {
201                     this.hiddenClasses.add(locatableClass);
202                 }
203             }
204             return show;
205         }
206     }
207 
208     /** {@inheritDoc} */
209     @SuppressWarnings("unchecked")
210     @Override
211     public void notify(final EventInterface event) throws RemoteException
212     {
213         if (event.getType().equals(ContextInterface.OBJECT_ADDED_EVENT))
214         {
215             objectAdded((Renderable2DInterface<? extends Locatable>) ((Object[]) event.getContent())[2]);
216         }
217 
218         else if (event.getType().equals(ContextInterface.OBJECT_REMOVED_EVENT))
219         {
220             objectRemoved((Renderable2DInterface<? extends Locatable>) ((Object[]) event.getContent())[2]);
221         }
222 
223         else if // (this.simulator.getSourceId().equals(event.getSourceId()) &&
224         (event.getType().equals(ReplicationInterface.START_REPLICATION_EVENT))
225         {
226             synchronized (this.elementList)
227             {
228                 this.elements.clear();
229                 try
230                 {
231                     if (this.context != null)
232                     {
233                         this.context.removeListener(this, ContextInterface.OBJECT_ADDED_EVENT);
234                         this.context.removeListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
235                     }
236 
237                     this.context =
238                             ContextUtil.lookupOrCreateSubContext(this.simulator.getReplication().getContext(), "animation/2D");
239                     this.context.addListener(this, ContextInterface.OBJECT_ADDED_EVENT);
240                     this.context.addListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
241                     for (Object element : this.context.values())
242                     {
243                         objectAdded((Renderable2DInterface<? extends Locatable>) element);
244                     }
245                     this.repaint();
246                 }
247                 catch (Exception exception)
248                 {
249                     this.simulator.getLogger().always().warn(exception, "notify");
250                 }
251             }
252         }
253     }
254 
255     /**
256      * Add a locatable object to the animation.
257      * @param element Renderable2DInterface&lt;? extends Locatable&gt;; the element to add to the animation
258      */
259     public void objectAdded(final Renderable2DInterface<? extends Locatable> element)
260     {
261         synchronized (this.elementList)
262         {
263             this.elements.add(element);
264             this.dirtyElements = true;
265         }
266     }
267 
268     /**
269      * Remove a locatable object from the animation.
270      * @param element Renderable2DInterface&lt;? extends Locatable&gt;; the element to add to the animation
271      */
272     public void objectRemoved(final Renderable2DInterface<? extends Locatable> element)
273     {
274         synchronized (this.elementList)
275         {
276             this.elements.remove(element);
277             this.dirtyElements = true;
278         }
279     }
280 
281     /**
282      * Calculate the full extent based on the current positions of the objects.
283      * @return Bounds2d; the full extent of the animation.
284      */
285     public synchronized Bounds2d fullExtent()
286     {
287         double minX = Double.MAX_VALUE;
288         double maxX = -Double.MAX_VALUE;
289         double minY = Double.MAX_VALUE;
290         double maxY = -Double.MAX_VALUE;
291         try
292         {
293             for (Renderable2DInterface<? extends Locatable> renderable : this.elementList)
294             {
295                 if (renderable.getSource() == null)
296                 {
297                     continue;
298                 }
299                 Point<?> l = renderable.getSource().getLocation();
300                 if (l != null)
301                 {
302                     Bounds<?, ?, ?> b = renderable.getSource().getBounds();
303                     minX = Math.min(minX, l.getX() + b.getMinX());
304                     minY = Math.min(minY, l.getY() + b.getMinY());
305                     maxX = Math.max(maxX, l.getX() + b.getMaxX());
306                     maxY = Math.max(maxY, l.getY() + b.getMaxY());
307                 }
308             }
309         }
310         catch (Exception e)
311         {
312             // ignore
313         }
314 
315         minX -= EXTENT_MARGIN_FACTOR * Math.abs(maxX - minX);
316         minY -= EXTENT_MARGIN_FACTOR * Math.abs(maxY - minY);
317         maxX += EXTENT_MARGIN_FACTOR * Math.abs(maxX - minX);
318         maxY += EXTENT_MARGIN_FACTOR * Math.abs(maxY - minY);
319 
320         return new Bounds2d(minX, maxX, minY, maxY);
321     }
322 
323     /**
324      * resets the panel to its an extent that covers all displayed objects.
325      */
326     public synchronized void zoomAll()
327     {
328         setExtent(getRenderableScale().computeVisibleExtent(fullExtent(), this.getSize()));
329         this.repaint();
330     }
331 
332     /**
333      * Set a class to be shown in the animation to true.
334      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which the animation has to be shown.
335      */
336     public void showClass(final Class<? extends Locatable> locatableClass)
337     {
338         this.visibilityMap.put(locatableClass, true);
339         this.shownClasses.clear();
340         this.hiddenClasses.clear();
341         this.repaint();
342     }
343 
344     /**
345      * Set a class to be hidden in the animation to true.
346      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which the animation has to be hidden.
347      */
348     public void hideClass(final Class<? extends Locatable> locatableClass)
349     {
350         this.visibilityMap.put(locatableClass, false);
351         this.shownClasses.clear();
352         this.hiddenClasses.clear();
353         this.repaint();
354     }
355 
356     /**
357      * Toggle a class to be displayed in the animation to its reverse value.
358      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which a visible animation has to be turned off or
359      *            vice versa.
360      */
361     public void toggleClass(final Class<? extends Locatable> locatableClass)
362     {
363         if (!this.visibilityMap.containsKey(locatableClass))
364         {
365             showClass(locatableClass);
366         }
367         this.visibilityMap.put(locatableClass, !this.visibilityMap.get(locatableClass));
368         this.shownClasses.clear();
369         this.hiddenClasses.clear();
370         this.repaint();
371     }
372 
373     /**
374      * @return the set of animation elements.
375      */
376     public final SortedSet<Renderable2DInterface<? extends Locatable>> getElements()
377     {
378         return this.elements;
379     }
380 
381     /**
382      * @return returns the dragLine.
383      */
384     public final int[] getDragLine()
385     {
386         return this.dragLine;
387     }
388 
389     /**
390      * @return returns the dragLineEnabled.
391      */
392     public final boolean isDragLineEnabled()
393     {
394         return this.dragLineEnabled;
395     }
396 
397     /**
398      * @param dragLineEnabled boolean; the dragLineEnabled to set.
399      */
400     public final void setDragLineEnabled(final boolean dragLineEnabled)
401     {
402         this.dragLineEnabled = dragLineEnabled;
403     }
404 
405     /**********************************************************************************************************/
406     /******************************************* TOGGLES ******************************************************/
407     /**********************************************************************************************************/
408 
409     /**
410      * Add a button for toggling an animatable class on or off.
411      * @param name String; the name of the button
412      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which the button holds (e.g., GTU.class)
413      * @param toolTipText String; the tool tip text to show when hovering over the button
414      * @param initiallyVisible boolean; whether the class is initially shown or not
415      */
416     public final void addToggleAnimationButtonText(final String name, final Class<? extends Locatable> locatableClass,
417             final String toolTipText, final boolean initiallyVisible)
418     {
419         ToggleButtonInfo.LocatableClass buttonInfo =
420                 new ToggleButtonInfo.LocatableClass(name, locatableClass, toolTipText, initiallyVisible);
421         if (initiallyVisible)
422         {
423             showClass(locatableClass);
424         }
425         else
426         {
427             hideClass(locatableClass);
428         }
429         this.toggleButtons.add(buttonInfo);
430         this.toggleLocatableMap.put(name, locatableClass);
431         this.toggleButtonMap.put(locatableClass, buttonInfo);
432     }
433 
434     /**
435      * Show a Locatable class based on the name.
436      * @param name String; the name of the class to show
437      */
438     public final void showClass(final String name)
439     {
440         showClass(this.toggleLocatableMap.get(name));
441     }
442 
443     /**
444      * Hide a Locatable class based on the name.
445      * @param name String; the name of the class to hide
446      */
447     public final void hideClass(final String name)
448     {
449         hideClass(this.toggleLocatableMap.get(name));
450     }
451 
452     /**
453      * Add a text to explain animatable classes.
454      * @param text String; the text to show
455      */
456     public final void addToggleText(final String text)
457     {
458         this.toggleButtons.add(new ToggleButtonInfo.Text(text, true));
459     }
460 
461     /**
462      * Add buttons for toggling all GIS layers on or off.
463      * @param header String; the name of the group of layers
464      * @param gisMap GisRenderable2D; the GIS map for which the toggles have to be added
465      * @param toolTipText String; the tool tip text to show when hovering over the button
466      */
467     public final void addAllToggleGISButtonText(final String header, final GisRenderable2D gisMap, final String toolTipText)
468     {
469         addToggleText(" ");
470         addToggleText(header);
471         try
472         {
473             for (String layerName : gisMap.getMap().getLayerMap().keySet())
474             {
475                 addToggleGISButtonText(layerName, layerName, gisMap, toolTipText);
476             }
477         }
478         catch (RemoteException exception)
479         {
480             exception.printStackTrace();
481         }
482     }
483 
484     /**
485      * Add a button to toggle a GIS Layer on or off.
486      * @param layerName String; the name of the layer
487      * @param displayName String; the name to display next to the tick box
488      * @param gisMap GisRenderable2D; the map
489      * @param toolTipText String; the tool tip text
490      */
491     public final void addToggleGISButtonText(final String layerName, final String displayName, final GisRenderable2D gisMap,
492             final String toolTipText)
493     {
494         ToggleButtonInfo.Gis buttonInfo = new ToggleButtonInfo.Gis(displayName, layerName, toolTipText, true);
495         this.toggleButtons.add(buttonInfo);
496         this.toggleGISMap.put(layerName, gisMap.getMap());
497         this.toggleGISButtonMap.put(layerName, buttonInfo);
498     }
499 
500     /**
501      * Set a GIS layer to be shown in the animation to true.
502      * @param layerName String; the name of the GIS-layer that has to be shown.
503      */
504     public final void showGISLayer(final String layerName)
505     {
506         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
507         if (gisMap != null)
508         {
509             try
510             {
511                 gisMap.showLayer(layerName);
512                 this.toggleGISButtonMap.get(layerName).setVisible(true);
513             }
514             catch (RemoteException exception)
515             {
516                 exception.printStackTrace();
517             }
518         }
519     }
520 
521     /**
522      * Set a GIS layer to be hidden in the animation to true.
523      * @param layerName String; the name of the GIS-layer that has to be hidden.
524      */
525     public final void hideGISLayer(final String layerName)
526     {
527         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
528         if (gisMap != null)
529         {
530             try
531             {
532                 gisMap.hideLayer(layerName);
533                 this.toggleGISButtonMap.get(layerName).setVisible(false);
534             }
535             catch (RemoteException exception)
536             {
537                 exception.printStackTrace();
538             }
539         }
540     }
541 
542     /**
543      * Toggle a GIS layer to be displayed in the animation to its reverse value.
544      * @param layerName String; the name of the GIS-layer that has to be turned off or vice versa.
545      */
546     public final void toggleGISLayer(final String layerName)
547     {
548         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
549         if (gisMap != null)
550         {
551             try
552             {
553                 if (gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
554                 {
555                     gisMap.hideLayer(layerName);
556                     this.toggleGISButtonMap.get(layerName).setVisible(false);
557                 }
558                 else
559                 {
560                     gisMap.showLayer(layerName);
561                     this.toggleGISButtonMap.get(layerName).setVisible(true);
562                 }
563             }
564             catch (RemoteException exception)
565             {
566                 exception.printStackTrace();
567             }
568         }
569     }
570 
571     /**
572      * @return toggleButtons
573      */
574     public final List<ToggleButtonInfo> getToggleButtons()
575     {
576         return this.toggleButtons;
577     }
578 
579 }