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