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.GtuColorerManager;
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      /** GTU colorer manager. */
94      private final GtuColorerManager gtuColorerManager = new GtuColorerManager(Color.WHITE);
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 the extent of the panel
102      * @param simulator 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     /**
114      * Returns the GTU colorer manager.
115      * @return GTU colorer manager
116      */
117     public GtuColorerManager getGtuColorerManager()
118     {
119         return this.gtuColorerManager;
120     }
121 
122     @Override
123     public void paintComponent(final HtmlGraphics2d g2)
124     {
125         // draw the grid.
126         super.paintComponent(g2);
127 
128         // update drawable elements when necessary
129         if (this.dirtyElements)
130         {
131             synchronized (this.elementList)
132             {
133                 this.elementList.clear();
134                 this.elementList.addAll(this.elements);
135                 this.dirtyElements = false;
136             }
137         }
138 
139         // draw the animation elements.
140         for (Renderable2dInterface<? extends Locatable> element : this.elementList)
141         {
142             // destroy has been called?
143             if (element.getSource() == null)
144             {
145                 objectRemoved(element);
146             }
147             else if (isShowElement(element))
148             {
149                 AffineTransform at = (AffineTransform) g2.getTransform().clone();
150                 element.paintComponent(g2, this.getExtent(), this.getSize(), this.renderableScale, this);
151                 g2.setTransform(at);
152             }
153         }
154 
155         // draw drag line if enabled.
156         if (this.dragLineEnabled)
157         {
158             g2.setColor(Color.BLACK);
159             g2.drawLine(this.dragLine[0], this.dragLine[1], this.dragLine[2], this.dragLine[3]);
160             this.dragLineEnabled = false;
161         }
162     }
163 
164     /**
165      * Test whether the element needs to be shown on the screen or not.
166      * @param element the renderable element to test
167      * @return whether the element needs to be shown or not
168      */
169     public boolean isShowElement(final Renderable2dInterface<? extends Locatable> element)
170     {
171         return element.getSource() == null ? false : isShowClass(element.getSource().getClass());
172     }
173 
174     /**
175      * Test whether a certain class needs to be shown on the screen or not. The class needs to implement Locatable, otherwise it
176      * cannot be shown at all.
177      * @param locatableClass the class to test
178      * @return whether the class needs to be shown or not
179      */
180     public boolean isShowClass(final Class<? extends Locatable> locatableClass)
181     {
182         if (this.hiddenClasses.contains(locatableClass))
183         {
184             return false;
185         }
186         else
187         {
188             boolean show = true;
189             if (!this.shownClasses.contains(locatableClass))
190             {
191                 for (Class<? extends Locatable> lc : this.visibilityMap.keySet())
192                 {
193                     if (lc.isAssignableFrom(locatableClass))
194                     {
195                         if (!this.visibilityMap.get(lc))
196                         {
197                             show = false;
198                         }
199                     }
200                 }
201                 // add to the right cache
202                 if (show)
203                 {
204                     this.shownClasses.add(locatableClass);
205                 }
206                 else
207                 {
208                     this.hiddenClasses.add(locatableClass);
209                 }
210             }
211             return show;
212         }
213     }
214 
215     @SuppressWarnings("unchecked")
216     @Override
217     public void notify(final Event event) throws RemoteException
218     {
219         if (event.getType().equals(ContextInterface.OBJECT_ADDED_EVENT))
220         {
221             objectAdded((Renderable2dInterface<? extends Locatable>) ((Object[]) event.getContent())[2]);
222         }
223 
224         else if (event.getType().equals(ContextInterface.OBJECT_REMOVED_EVENT))
225         {
226             objectRemoved((Renderable2dInterface<? extends Locatable>) ((Object[]) event.getContent())[2]);
227         }
228 
229         else if // (this.simulator.getSourceId().equals(event.getSourceId()) &&
230         (event.getType().equals(Replication.START_REPLICATION_EVENT))
231         {
232             synchronized (this.elementList)
233             {
234                 this.elements.clear();
235                 try
236                 {
237                     if (this.context != null)
238                     {
239                         this.context.removeListener(this, ContextInterface.OBJECT_ADDED_EVENT);
240                         this.context.removeListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
241                     }
242 
243                     this.context =
244                             ContextUtil.lookupOrCreateSubContext(this.simulator.getReplication().getContext(), "animation/2D");
245                     this.context.addListener(this, ContextInterface.OBJECT_ADDED_EVENT);
246                     this.context.addListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
247                     for (Object element : this.context.values())
248                     {
249                         objectAdded((Renderable2dInterface<? extends Locatable>) element);
250                     }
251                     this.repaint();
252                 }
253                 catch (Exception exception)
254                 {
255                     this.simulator.getLogger().always().warn(exception, "notify");
256                 }
257             }
258         }
259     }
260 
261     /**
262      * Add a locatable object to the animation.
263      * @param element the element to add to the animation
264      */
265     public void objectAdded(final Renderable2dInterface<? extends Locatable> element)
266     {
267         synchronized (this.elementList)
268         {
269             this.elements.add(element);
270             this.dirtyElements = true;
271         }
272     }
273 
274     /**
275      * Remove a locatable object from the animation.
276      * @param element the element to add to the animation
277      */
278     public void objectRemoved(final Renderable2dInterface<? extends Locatable> element)
279     {
280         synchronized (this.elementList)
281         {
282             this.elements.remove(element);
283             this.dirtyElements = true;
284         }
285     }
286 
287     /**
288      * Calculate the full extent based on the current positions of the objects.
289      * @return the full extent of the animation.
290      */
291     public synchronized Bounds2d fullExtent()
292     {
293         double minX = Double.MAX_VALUE;
294         double maxX = -Double.MAX_VALUE;
295         double minY = Double.MAX_VALUE;
296         double maxY = -Double.MAX_VALUE;
297         try
298         {
299             for (Renderable2dInterface<? extends Locatable> renderable : this.elementList)
300             {
301                 if (renderable.getSource() == null)
302                 {
303                     continue;
304                 }
305                 Point<?> l = renderable.getSource().getLocation();
306                 if (l != null)
307                 {
308                     Bounds<?, ?, ?> b = renderable.getSource().getBounds();
309                     minX = Math.min(minX, l.getX() + b.getMinX());
310                     minY = Math.min(minY, l.getY() + b.getMinY());
311                     maxX = Math.max(maxX, l.getX() + b.getMaxX());
312                     maxY = Math.max(maxY, l.getY() + b.getMaxY());
313                 }
314             }
315         }
316         catch (Exception e)
317         {
318             // ignore
319         }
320 
321         minX -= EXTENT_MARGIN_FACTOR * Math.abs(maxX - minX);
322         minY -= EXTENT_MARGIN_FACTOR * Math.abs(maxY - minY);
323         maxX += EXTENT_MARGIN_FACTOR * Math.abs(maxX - minX);
324         maxY += EXTENT_MARGIN_FACTOR * Math.abs(maxY - minY);
325 
326         return new Bounds2d(minX, maxX, minY, maxY);
327     }
328 
329     /**
330      * resets the panel to its an extent that covers all displayed objects.
331      */
332     public synchronized void zoomAll()
333     {
334         setExtent(getRenderableScale().computeVisibleExtent(fullExtent(), this.getSize()));
335         this.repaint();
336     }
337 
338     /**
339      * Set a class to be shown in the animation to true.
340      * @param locatableClass the class for which the animation has to be shown.
341      */
342     public void showClass(final Class<? extends Locatable> locatableClass)
343     {
344         this.visibilityMap.put(locatableClass, true);
345         this.shownClasses.clear();
346         this.hiddenClasses.clear();
347         this.repaint();
348     }
349 
350     /**
351      * Set a class to be hidden in the animation to true.
352      * @param locatableClass the class for which the animation has to be hidden.
353      */
354     public void hideClass(final Class<? extends Locatable> locatableClass)
355     {
356         this.visibilityMap.put(locatableClass, false);
357         this.shownClasses.clear();
358         this.hiddenClasses.clear();
359         this.repaint();
360     }
361 
362     /**
363      * Toggle a class to be displayed in the animation to its reverse value.
364      * @param locatableClass the class for which a visible animation has to be turned off or vice versa.
365      */
366     public void toggleClass(final Class<? extends Locatable> locatableClass)
367     {
368         if (!this.visibilityMap.containsKey(locatableClass))
369         {
370             showClass(locatableClass);
371         }
372         this.visibilityMap.put(locatableClass, !this.visibilityMap.get(locatableClass));
373         this.shownClasses.clear();
374         this.hiddenClasses.clear();
375         this.repaint();
376     }
377 
378     /**
379      * @return the set of animation elements.
380      */
381     public final SortedSet<Renderable2dInterface<? extends Locatable>> getElements()
382     {
383         return this.elements;
384     }
385 
386     /**
387      * @return returns the dragLine.
388      */
389     public final int[] getDragLine()
390     {
391         return this.dragLine;
392     }
393 
394     /**
395      * @return returns the dragLineEnabled.
396      */
397     public final boolean isDragLineEnabled()
398     {
399         return this.dragLineEnabled;
400     }
401 
402     /**
403      * @param dragLineEnabled the dragLineEnabled to set.
404      */
405     public final void setDragLineEnabled(final boolean dragLineEnabled)
406     {
407         this.dragLineEnabled = dragLineEnabled;
408     }
409 
410     /**********************************************************************************************************/
411     /******************************************* TOGGLES ******************************************************/
412     /**********************************************************************************************************/
413 
414     /**
415      * Add a button for toggling an animatable class on or off.
416      * @param name the name of the button
417      * @param locatableClass the class for which the button holds (e.g., GTU.class)
418      * @param toolTipText the tool tip text to show when hovering over the button
419      * @param initiallyVisible whether the class is initially shown or not
420      */
421     public final void addToggleAnimationButtonText(final String name, final Class<? extends Locatable> locatableClass,
422             final String toolTipText, final boolean initiallyVisible)
423     {
424         ToggleButtonInfo.LocatableClass buttonInfo =
425                 new ToggleButtonInfo.LocatableClass(name, locatableClass, toolTipText, initiallyVisible);
426         if (initiallyVisible)
427         {
428             showClass(locatableClass);
429         }
430         else
431         {
432             hideClass(locatableClass);
433         }
434         this.toggleButtons.add(buttonInfo);
435         this.toggleLocatableMap.put(name, locatableClass);
436         this.toggleButtonMap.put(locatableClass, buttonInfo);
437     }
438 
439     /**
440      * Show a Locatable class based on the name.
441      * @param name the name of the class to show
442      */
443     public final void showClass(final String name)
444     {
445         showClass(this.toggleLocatableMap.get(name));
446     }
447 
448     /**
449      * Hide a Locatable class based on the name.
450      * @param name the name of the class to hide
451      */
452     public final void hideClass(final String name)
453     {
454         hideClass(this.toggleLocatableMap.get(name));
455     }
456 
457     /**
458      * Add a text to explain animatable classes.
459      * @param text the text to show
460      */
461     public final void addToggleText(final String text)
462     {
463         this.toggleButtons.add(new ToggleButtonInfo.Text(text, true));
464     }
465 
466     /**
467      * Add buttons for toggling all GIS layers on or off.
468      * @param header the name of the group of layers
469      * @param gisMap the GIS map for which the toggles have to be added
470      * @param toolTipText the tool tip text to show when hovering over the button
471      */
472     public final void addAllToggleGISButtonText(final String header, final GisRenderable2d gisMap, final String toolTipText)
473     {
474         addToggleText(" ");
475         addToggleText(header);
476         try
477         {
478             for (String layerName : gisMap.getMap().getLayerMap().keySet())
479             {
480                 addToggleGISButtonText(layerName, layerName, gisMap, toolTipText);
481             }
482         }
483         catch (RemoteException exception)
484         {
485             exception.printStackTrace();
486         }
487     }
488 
489     /**
490      * Add a button to toggle a GIS Layer on or off.
491      * @param layerName the name of the layer
492      * @param displayName the name to display next to the tick box
493      * @param gisMap the map
494      * @param toolTipText the tool tip text
495      */
496     public final void addToggleGISButtonText(final String layerName, final String displayName, final GisRenderable2d gisMap,
497             final String toolTipText)
498     {
499         ToggleButtonInfo.Gis buttonInfo = new ToggleButtonInfo.Gis(displayName, layerName, toolTipText, true);
500         this.toggleButtons.add(buttonInfo);
501         this.toggleGISMap.put(layerName, gisMap.getMap());
502         this.toggleGISButtonMap.put(layerName, buttonInfo);
503     }
504 
505     /**
506      * Set a GIS layer to be shown in the animation to true.
507      * @param layerName the name of the GIS-layer that has to be shown.
508      */
509     public final void showGISLayer(final String layerName)
510     {
511         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
512         if (gisMap != null)
513         {
514             try
515             {
516                 gisMap.showLayer(layerName);
517                 this.toggleGISButtonMap.get(layerName).setVisible(true);
518             }
519             catch (RemoteException exception)
520             {
521                 exception.printStackTrace();
522             }
523         }
524     }
525 
526     /**
527      * Set a GIS layer to be hidden in the animation to true.
528      * @param layerName the name of the GIS-layer that has to be hidden.
529      */
530     public final void hideGISLayer(final String layerName)
531     {
532         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
533         if (gisMap != null)
534         {
535             try
536             {
537                 gisMap.hideLayer(layerName);
538                 this.toggleGISButtonMap.get(layerName).setVisible(false);
539             }
540             catch (RemoteException exception)
541             {
542                 exception.printStackTrace();
543             }
544         }
545     }
546 
547     /**
548      * Toggle a GIS layer to be displayed in the animation to its reverse value.
549      * @param layerName the name of the GIS-layer that has to be turned off or vice versa.
550      */
551     public final void toggleGISLayer(final String layerName)
552     {
553         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
554         if (gisMap != null)
555         {
556             try
557             {
558                 if (gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
559                 {
560                     gisMap.hideLayer(layerName);
561                     this.toggleGISButtonMap.get(layerName).setVisible(false);
562                 }
563                 else
564                 {
565                     gisMap.showLayer(layerName);
566                     this.toggleGISButtonMap.get(layerName).setVisible(true);
567                 }
568             }
569             catch (RemoteException exception)
570             {
571                 exception.printStackTrace();
572             }
573         }
574     }
575 
576     /**
577      * @return toggleButtons
578      */
579     public final List<ToggleButtonInfo> getToggleButtons()
580     {
581         return this.toggleButtons;
582     }
583 
584 }