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