View Javadoc
1   package nl.tudelft.simulation.dsol.swing.animation.d2;
2   
3   import java.awt.Color;
4   import java.awt.Dimension;
5   import java.awt.Graphics;
6   import java.awt.Graphics2D;
7   import java.awt.event.MouseEvent;
8   import java.awt.geom.Point2D;
9   import java.awt.geom.RectangularShape;
10  import java.rmi.RemoteException;
11  import java.text.NumberFormat;
12  import java.util.ArrayList;
13  import java.util.Collections;
14  import java.util.Iterator;
15  import java.util.LinkedHashMap;
16  import java.util.LinkedHashSet;
17  import java.util.List;
18  import java.util.Map;
19  import java.util.Set;
20  import java.util.SortedSet;
21  import java.util.TreeSet;
22  
23  import javax.naming.NamingException;
24  import javax.swing.JPanel;
25  import javax.swing.JPopupMenu;
26  import javax.swing.JSeparator;
27  
28  import org.djutils.draw.bounds.Bounds;
29  import org.djutils.draw.bounds.Bounds2d;
30  import org.djutils.draw.point.Point;
31  import org.djutils.draw.point.Point2d;
32  import org.djutils.event.Event;
33  import org.djutils.event.EventListener;
34  import org.djutils.event.EventListenerMap;
35  import org.djutils.event.EventProducer;
36  import org.djutils.event.EventType;
37  import org.djutils.event.LocalEventProducer;
38  import org.djutils.event.reference.ReferenceType;
39  import org.djutils.logger.CategoryLogger;
40  import org.djutils.metadata.MetaData;
41  import org.djutils.metadata.ObjectDescriptor;
42  
43  import nl.tudelft.simulation.dsol.animation.Locatable;
44  import nl.tudelft.simulation.dsol.animation.d2.Renderable2dComparator;
45  import nl.tudelft.simulation.dsol.animation.d2.Renderable2dInterface;
46  import nl.tudelft.simulation.dsol.animation.d2.RenderableScale;
47  import nl.tudelft.simulation.dsol.simulators.AnimatorInterface;
48  import nl.tudelft.simulation.dsol.swing.animation.d2.actions.IntrospectionAction;
49  import nl.tudelft.simulation.naming.context.ContextInterface;
50  
51  /**
52   * The VisualizationPanel implements the basic functions to visualize Locatable objects on the screen. It also allows for
53   * toggling a grid on/off, zooming, panning, translation between world coordinates and screen coordinates, and changing the
54   * displayed extent such as the home extent.
55   * <p>
56   * The screen has the possibility to witch layers on and off. <br>
57   * <br>
58   * <b>Asynchronous and synchronous calls:</b><br>
59   * The internal functions of the AnimationPanel are handled in a synchronous way inside the animation panel, possibly through
60   * (mouse or keyboard) listeners and handlers that implement the functions.There are several exceptions, though:
61   * </p>
62   * <ul>
63   * <li><i>Clicking on one or more objects:</i> what has to happen is very much dependent on the implementation. Therefore, the
64   * click on an object will lead to firing of an event, where the listener(s), if any, can decide what to do. This can be
65   * dependent on whether CTRL, SHIFT, or ALT were pressed at the same time as the mouse button. Example behaviors could be:
66   * pop-up with properties of the object; showing properties in a special pane; highlighting the object; or setting the auto-pan
67   * on the clicked object. The event to use is the ANIMATION_MOUSE_CLICK_EVENT.</li>
68   * <li><i>Right click on one or more objects:</i> what has to happen is very much dependent on the implementation. Therefore,
69   * the click on an object will lead to firing of an event, where the listener(s), if any, can decide what to do. The event to
70   * use is the ANIMATION_MOUSE_POPUP_EVENT.</li>
71   * </ul>
72   * Furthermore, the AnimationPanel is an event listener, and listens, e.g., to the event of a searched object: the
73   * ANIMATION_SEARCH_OBJECT_EVENT to highlight the object, or, in case of an AutoPanAnimationPanel, to keep the object in the
74   * middle of the screen.
75   * <p>
76   * Copyright (c) 2002-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
77   * for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
78   * project is distributed under a three-clause BSD-style license, which can be found at
79   * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
80   * https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
81   * </p>
82   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
83   * @author Niels Lang
84   * @author <a href="http://www.peter-jacobs.com">Peter Jacobs </a>
85   */
86  public class VisualizationPanel extends JPanel implements EventProducer, EventListener
87  {
88      /** */
89      private static final long serialVersionUID = 20230305L;
90  
91      /** the UP directions for moving/zooming. */
92      public static final int UP = 1;
93  
94      /** the DOWN directions for moving/zooming. */
95      public static final int DOWN = 2;
96  
97      /** the LEFT directions for moving/zooming. */
98      public static final int LEFT = 3;
99  
100     /** the RIGHT directions for moving/zooming. */
101     public static final int RIGHT = 4;
102 
103     /** the ZOOM factor. */
104     public static final double ZOOMFACTOR = 1.2;
105 
106     /** gridColor. */
107     protected static final Color GRIDCOLOR = Color.BLACK;
108 
109     /** the extent of this panel. */
110     @SuppressWarnings("checkstyle:visibilitymodifier")
111     private Bounds2d extent = null;
112 
113     /** the initial and default extent of this panel. */
114     @SuppressWarnings("checkstyle:visibilitymodifier")
115     private Bounds2d homeExtent = null;
116 
117     /** show the grid. */
118     @SuppressWarnings("checkstyle:visibilitymodifier")
119     protected boolean showGrid = true;
120 
121     /** the gridSize for the X-direction in world Units. */
122     @SuppressWarnings("checkstyle:visibilitymodifier")
123     protected double gridSizeX = 100.0;
124 
125     /** the gridSize for the Y-direction in world Units. */
126     @SuppressWarnings("checkstyle:visibilitymodifier")
127     protected double gridSizeY = 100.0;
128 
129     /** the formatter to use. */
130     @SuppressWarnings("checkstyle:visibilitymodifier")
131     protected NumberFormat formatter = NumberFormat.getInstance();
132 
133     /** the last computed Dimension. */
134     @SuppressWarnings("checkstyle:visibilitymodifier")
135     protected Dimension lastDimension = null;
136 
137     /** the last known world coordinate of the mouse. */
138     @SuppressWarnings("checkstyle:visibilitymodifier")
139     protected Point2d worldCoordinate = new Point2d(0.0, 0.0);
140 
141     /** whether to show a tooltip with the coordinates or not. */
142     @SuppressWarnings("checkstyle:visibilitymodifier")
143     protected boolean showToolTip = true;
144 
145     /** the renderable scale (X/Y ratio) to use. */
146     private RenderableScale renderableScale;
147 
148     /** the elements of this panel. */
149     @SuppressWarnings("checkstyle:visibilitymodifier")
150     protected SortedSet<Renderable2dInterface<? extends Locatable>> elements =
151             new TreeSet<Renderable2dInterface<? extends Locatable>>(new Renderable2dComparator());
152 
153     /** filter for types to be shown or not. */
154     @SuppressWarnings("checkstyle:visibilitymodifier")
155     protected Map<Class<? extends Locatable>, Boolean> visibilityMap = Collections.synchronizedMap(new LinkedHashMap<>());
156 
157     /** cache of the classes that are hidden. */
158     @SuppressWarnings("checkstyle:visibilitymodifier")
159     protected Set<Class<? extends Locatable>> hiddenClasses = new LinkedHashSet<>();
160 
161     /** cache of the classes that are shown. */
162     @SuppressWarnings("checkstyle:visibilitymodifier")
163     protected Set<Class<? extends Locatable>> shownClasses = new LinkedHashSet<>();
164 
165     /** the context with the path /experiment/replication/animation/2D. */
166     @SuppressWarnings("checkstyle:visibilitymodifier")
167     protected ContextInterface context = null;
168 
169     /** a line that helps the user to see where she/he is dragging. */
170     private int[] dragLine = new int[4];
171 
172     /** enable drag line. */
173     private boolean dragLineEnabled = false;
174 
175     /** List of drawable objects. */
176     @SuppressWarnings("checkstyle:visibilitymodifier")
177     protected List<Renderable2dInterface<? extends Locatable>> elementList = new ArrayList<>();
178 
179     /** dirty flag for the list. */
180     private boolean dirty = false;
181 
182     /** delegate class to do handle event producing. */
183     private final AnimationEventProducer animationEventProducer;
184 
185     /** the margin factor 'around' the extent. */
186     public static final double EXTENT_MARGIN_FACTOR = 0.05;
187 
188     /** the event when the user clicked ith the left mouse button, possibly on one or more objects. */
189     public static final EventType ANIMATION_MOUSE_CLICK_EVENT = new EventType(new MetaData("ANIMATION_MOUSE_CLICK_EVENT",
190             "ANIMATION_MOUSE_CLICK_EVENT",
191             new ObjectDescriptor("worldCoordinate", "x and y position in world coordinates", Point2d.class),
192             new ObjectDescriptor("screenCoordinate", "x and y position in screen coordinates", java.awt.Point.class),
193             new ObjectDescriptor("shiftCtrlAlt", "shift[0], ctrl[1], and/or alt[2] pressed", boolean[].class),
194             new ObjectDescriptor("objectList", "List of objects whose bounding box includes the coordinate", List.class)));
195 
196     /** the event when the user clicked with the right mouse button, selecting from on one or more objects. */
197     public static final EventType ANIMATION_MOUSE_POPUP_EVENT = new EventType(new MetaData("ANIMATION_MOUSE_POPUP_EVENT",
198             "ANIMATION_MOUSE_POPUP_EVENT",
199             new ObjectDescriptor("worldCoordinate", "x and y position in world coordinates", Point2d.class),
200             new ObjectDescriptor("screenCoordinate", "x and y position in screen coordinates", java.awt.Point.class),
201             new ObjectDescriptor("shiftCtrlAlt", "shift[0], ctrl[1], and/or alt[2] pressed", boolean[].class),
202             new ObjectDescriptor("object", "Selected object whose bounding box includes the coordinate", Object.class)));
203 
204     /**
205      * constructs a new VisualizationPanel.
206      * @param homeExtent Bounds2d; the initial extent.
207      * @param producer EventProducer; the object firing animation update events
208      * @throws RemoteException on error when remote panel and producer cannot connect
209      */
210     public VisualizationPanel(final Bounds2d homeExtent, final EventProducer producer) throws RemoteException
211     {
212         setPreferredSize(new Dimension(1024, 768));
213         this.animationEventProducer = new AnimationEventProducer();
214         this.showGrid = true;
215         InputListener listener = new InputListener(this);
216         this.addMouseListener(listener);
217         this.addMouseMotionListener(listener);
218         this.addMouseWheelListener(listener);
219         this.addKeyListener(listener);
220         this.renderableScale = new RenderableScale();
221         this.homeExtent = homeExtent;
222         this.setBackground(Color.WHITE);
223         this.lastDimension = this.getSize();
224         setExtent(homeExtent);
225         producer.addListener(this, AnimatorInterface.UPDATE_ANIMATION_EVENT);
226     }
227 
228     /**
229      * constructs a new VisualizationPanel with a context, so it can start drawing right away.
230      * @param homeExtent Bounds2d; the initial extent.
231      * @param producer EventProducer; the object firing animation update events
232      * @param context ContextInterface; the context that contains the drawing objects
233      * @throws RemoteException on error when remote panel and producer cannot connect
234      * @throws NamingException on context error
235      */
236     public VisualizationPanel(final Bounds2d homeExtent, final EventProducer producer, final ContextInterface context)
237             throws RemoteException, NamingException
238     {
239         this(homeExtent, producer);
240         this.context = context.createSubcontext("animation/2D");
241         subscribeToContext();
242     }
243 
244     /** {@inheritDoc} */
245     @Override
246     public void paintComponent(final Graphics g)
247     {
248         Graphics2D g2 = (Graphics2D) g;
249         super.paintComponent(g);
250 
251         // draw the grid.
252         if (!this.getSize().equals(this.lastDimension))
253         {
254             this.lastDimension = this.getSize();
255             setExtent(this.renderableScale.computeVisibleExtent(this.extent, this.getSize()));
256         }
257         if (this.showGrid)
258         {
259             this.drawGrid(g);
260         }
261 
262         // update drawable elements when necessary
263         if (this.dirty)
264         {
265             synchronized (this.elementList)
266             {
267                 this.elementList.clear();
268                 this.elementList.addAll(this.elements);
269                 this.dirty = false;
270             }
271         }
272 
273         // draw the animation elements
274         for (Renderable2dInterface<? extends Locatable> element : this.elementList)
275         {
276             // destroy has been called?
277             if (element.getSource() == null)
278             {
279                 objectRemoved(element);
280             }
281             else if (isShowElement(element))
282             {
283                 element.paintComponent(g2, this.getExtent(), this.getSize(), getRenderableScale(), this);
284             }
285         }
286 
287         // draw drag line if enabled.
288         if (this.dragLineEnabled)
289         {
290             g.setColor(Color.BLACK);
291             g.drawLine(this.dragLine[0], this.dragLine[1], this.dragLine[2], this.dragLine[3]);
292             this.dragLineEnabled = false;
293         }
294     }
295 
296     /**
297      * Test whether the element needs to be shown on the screen or not.
298      * @param element Renderable2dInterface&lt;? extends Locatable&gt;; the renderable element to test
299      * @return whether the element needs to be shown or not
300      */
301     public boolean isShowElement(final Renderable2dInterface<? extends Locatable> element)
302     {
303         return element.getSource() == null ? false : isShowClass(element.getSource().getClass());
304     }
305 
306     /**
307      * Test whether a certain class needs to be shown on the screen or not. The class needs to implement Locatable, otherwise it
308      * cannot be shown at all.
309      * @param locatableClass Class&lt;? extends Locatable&gt;; the class to test
310      * @return whether the class needs to be shown or not
311      */
312     public boolean isShowClass(final Class<? extends Locatable> locatableClass)
313     {
314         if (this.hiddenClasses.contains(locatableClass))
315         {
316             return false;
317         }
318         else
319         {
320             boolean show = true;
321             if (!this.shownClasses.contains(locatableClass))
322             {
323                 synchronized (this.visibilityMap)
324                 {
325                     for (Class<? extends Locatable> lc : this.visibilityMap.keySet())
326                     {
327                         if (lc.isAssignableFrom(locatableClass))
328                         {
329                             if (!this.visibilityMap.get(lc))
330                             {
331                                 show = false;
332                             }
333                         }
334                     }
335                     // add to the right cache
336                     if (show)
337                     {
338                         this.shownClasses.add(locatableClass);
339                     }
340                     else
341                     {
342                         this.hiddenClasses.add(locatableClass);
343                     }
344                 }
345             }
346             return show;
347         }
348     }
349 
350     /**
351      * Subscribe to the context to receive object added and object removed events.
352      * @throws RemoteException on network error
353      */
354     @SuppressWarnings("unchecked")
355     protected void subscribeToContext() throws RemoteException
356     {
357         this.context.addListener(this, ContextInterface.OBJECT_ADDED_EVENT);
358         this.context.addListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
359         for (Object element : this.context.values())
360         {
361             if (element instanceof Renderable2dInterface)
362             {
363                 objectAdded((Renderable2dInterface<? extends Locatable>) element);
364             }
365             else
366             {
367                 System.err.println("odd object in context: " + element);
368             }
369         }
370         this.repaint();
371     }
372 
373     /** {@inheritDoc} */
374     @SuppressWarnings("unchecked")
375     @Override
376     public void notify(final Event event) throws RemoteException
377     {
378         if (event.getType().equals(AnimatorInterface.UPDATE_ANIMATION_EVENT) && this.isShowing())
379         {
380             if (this.getWidth() > 0 || this.getHeight() > 0)
381             {
382                 this.repaint();
383             }
384             return;
385         }
386 
387         else if (event.getType().equals(ContextInterface.OBJECT_ADDED_EVENT))
388         {
389             objectAdded((Renderable2dInterface<? extends Locatable>) ((Object[]) event.getContent())[2]);
390         }
391 
392         else if (event.getType().equals(ContextInterface.OBJECT_REMOVED_EVENT))
393         {
394             objectRemoved((Renderable2dInterface<? extends Locatable>) ((Object[]) event.getContent())[2]);
395         }
396     }
397 
398     /**
399      * returns the extent of this panel.
400      * @return Bounds2d
401      */
402     public Bounds2d getExtent()
403     {
404         return this.extent;
405     }
406 
407     /**
408      * set a new extent for this panel.
409      * @param extent Bounds2d; set a new extent
410      */
411     public void setExtent(final Bounds2d extent)
412     {
413         this.extent = extent;
414         this.repaint();
415     }
416 
417     /**
418      * show the grid?
419      * @param bool boolean; true/false
420      */
421     public synchronized void showGrid(final boolean bool)
422     {
423         this.showGrid = bool;
424         this.repaint();
425     }
426 
427     /**
428      * Set the world coordinates based on a mouse move.
429      * @param point Point2D; the x,y world coordinates
430      */
431     public synchronized void setWorldCoordinate(final Point2d point)
432     {
433         this.worldCoordinate = point;
434     }
435 
436     /**
437      * @return worldCoordinate
438      */
439     public synchronized Point2d getWorldCoordinate()
440     {
441         return this.worldCoordinate;
442     }
443 
444     /**
445      * Display a tooltip with the last known world coordinates of the mouse, in case the tooltip should be displayed.
446      */
447     public synchronized void displayWorldCoordinateToolTip()
448     {
449         if (this.showToolTip)
450         {
451             String worldPoint = "(x=" + this.formatter.format(this.worldCoordinate.getX()) + " ; y="
452                     + this.formatter.format(this.worldCoordinate.getY()) + ")";
453             setToolTipText(worldPoint);
454         }
455     }
456 
457     /**
458      * @return showToolTip
459      */
460     public synchronized boolean isShowToolTip()
461     {
462         return this.showToolTip;
463     }
464 
465     /**
466      * @param showToolTip boolean; set showToolTip
467      */
468     public synchronized void setShowToolTip(final boolean showToolTip)
469     {
470         this.showToolTip = showToolTip;
471     }
472 
473     /**
474      * pans the panel in a specified direction.
475      * @param direction int; the direction
476      * @param percentage double; the percentage
477      */
478     public synchronized void pan(final int direction, final double percentage)
479     {
480         if (percentage <= 0 || percentage > 1.0)
481         {
482             throw new IllegalArgumentException("percentage<=0 || >1.0");
483         }
484         switch (direction)
485         {
486             case LEFT:
487                 setExtent(new Bounds2d(this.extent.getMinX() - percentage * this.extent.getDeltaX(),
488                         this.extent.getMaxX() - percentage * this.extent.getDeltaX(), this.extent.getMinY(),
489                         this.extent.getMaxY()));
490                 break;
491             case RIGHT:
492                 setExtent(new Bounds2d(this.extent.getMinX() + percentage * this.extent.getDeltaX(),
493                         this.extent.getMaxX() + percentage * this.extent.getDeltaX(), this.extent.getMinY(),
494                         this.extent.getMaxY()));
495                 break;
496             case UP:
497                 setExtent(new Bounds2d(this.extent.getMinX(), this.extent.getMaxX(),
498                         this.extent.getMinY() + percentage * this.extent.getDeltaY(),
499                         this.extent.getMaxY() + percentage * this.extent.getDeltaY()));
500                 break;
501             case DOWN:
502                 setExtent(new Bounds2d(this.extent.getMinX(), this.extent.getMaxX(),
503                         this.extent.getMinY() - percentage * this.extent.getDeltaY(),
504                         this.extent.getMaxY() - percentage * this.extent.getDeltaY()));
505                 break;
506             default:
507                 throw new IllegalArgumentException("direction unkown");
508         }
509     }
510 
511     /**
512      * resets the panel to its original extent.
513      */
514     public synchronized void home()
515     {
516         setExtent(this.renderableScale.computeVisibleExtent(this.homeExtent, this.getSize()));
517     }
518 
519     /**
520      * @return Returns the showGrid.
521      */
522     public boolean isShowGrid()
523     {
524         return this.showGrid;
525     }
526 
527     /**
528      * @param showGrid boolean; The showGrid to set.
529      */
530     public void setShowGrid(final boolean showGrid)
531     {
532         this.showGrid = showGrid;
533     }
534 
535     /**
536      * zooms in/out.
537      * @param factor double; The zoom factor
538      */
539     public synchronized void zoom(final double factor)
540     {
541         zoom(factor, (int) (this.getWidth() / 2.0), (int) (this.getHeight() / 2.0));
542     }
543 
544     /**
545      * zooms in/out.
546      * @param factor double; The zoom factor
547      * @param mouseX int; x-position of the mouse around which we zoom
548      * @param mouseY int; y-position of the mouse around which we zoom
549      */
550     public synchronized void zoom(final double factor, final int mouseX, final int mouseY)
551     {
552         Point2d mwc = this.renderableScale.getWorldCoordinates(new Point2D.Double(mouseX, mouseY), this.extent, this.getSize());
553         double minX = mwc.getX() - (mwc.getX() - this.extent.getMinX()) * factor;
554         double minY = mwc.getY() - (mwc.getY() - this.extent.getMinY()) * factor;
555         double w = this.extent.getDeltaX() * factor;
556         double h = this.extent.getDeltaY() * factor;
557 
558         setExtent(new Bounds2d(minX, minX + w, minY, minY + h));
559     }
560 
561     /**
562      * Added to make sure the recursive render-call calls THIS render method instead of a potential super-class defined
563      * 'paintComponent' render method.
564      * @param g Graphics; the graphics object
565      */
566     protected synchronized void drawGrid(final Graphics g)
567     {
568         // we prepare the graphics object for the grid
569         g.setFont(g.getFont().deriveFont(11.0f));
570         g.setColor(GRIDCOLOR);
571         double scaleX = this.renderableScale.getXScale(this.extent, this.getSize());
572         double scaleY = this.renderableScale.getYScale(this.extent, this.getSize());
573 
574         int count = 0;
575         int gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
576         while (gridSizePixelsX < 40)
577         {
578             this.gridSizeX = 10 * this.gridSizeX;
579             int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
580             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
581             gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
582             if (count++ > 10)
583             {
584                 break;
585             }
586         }
587 
588         count = 0;
589         while (gridSizePixelsX > 10 * 40)
590         {
591             int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
592             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
593             this.gridSizeX = this.gridSizeX / 10;
594             gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
595             if (count++ > 10)
596             {
597                 break;
598             }
599         }
600 
601         int gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
602         while (gridSizePixelsY < 40)
603         {
604             this.gridSizeY = 10 * this.gridSizeY;
605             int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
606             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
607             gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
608             if (count++ > 10)
609             {
610                 break;
611             }
612         }
613 
614         count = 0;
615         while (gridSizePixelsY > 10 * 40)
616         {
617             int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
618             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
619             this.gridSizeY = this.gridSizeY / 10;
620             gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
621             if (count++ > 10)
622             {
623                 break;
624             }
625         }
626 
627         // Let's draw the vertical lines
628         double mod = this.extent.getMinX() % this.gridSizeX;
629         int x = (int) -Math.round(mod / scaleX);
630         while (x < this.getWidth())
631         {
632             Point2d point = this.renderableScale.getWorldCoordinates(new Point2D.Double(x, 0), this.extent, this.getSize());
633             if (point != null)
634             {
635                 String label = this.formatter.format(Math.round(point.getX() / this.gridSizeX) * this.gridSizeX);
636                 double labelWidth = this.getFontMetrics(this.getFont()).getStringBounds(label, g).getWidth();
637                 if (x > labelWidth + 4)
638                 {
639                     g.drawLine(x, 15, x, this.getHeight());
640                     g.drawString(label, (int) Math.round(x - 0.5 * labelWidth), 11);
641                 }
642             }
643             x = x + gridSizePixelsX;
644         }
645 
646         // Let's draw the horizontal lines
647         mod = Math.abs(this.extent.getMinY()) % this.gridSizeY;
648         int y = (int) Math.round(this.getSize().getHeight() - (mod / scaleY));
649         while (y > 15)
650         {
651             Point2d point = this.renderableScale.getWorldCoordinates(new Point2D.Double(0, y), this.extent, this.getSize());
652             if (point != null)
653             {
654                 String label = this.formatter.format(Math.round(point.getY() / this.gridSizeY) * this.gridSizeY);
655                 RectangularShape labelBounds = this.getFontMetrics(this.getFont()).getStringBounds(label, g);
656                 g.drawLine((int) Math.round(labelBounds.getWidth() + 4), y, this.getWidth(), y);
657                 g.drawString(label, 2, (int) Math.round(y + labelBounds.getHeight() * 0.3));
658             }
659             y = y - gridSizePixelsY;
660         }
661     }
662 
663     /**
664      * @return renderableScale
665      */
666     public RenderableScale getRenderableScale()
667     {
668         return this.renderableScale;
669     }
670 
671     /**
672      * @param renderableScale set renderableScale
673      */
674     public void setRenderableScale(final RenderableScale renderableScale)
675     {
676         this.renderableScale = renderableScale;
677     }
678 
679     /**
680      * Add a locatable object to the animation.
681      * @param element Renderable2dInterface&lt;? extends Locatable&gt;; the element to add to the animation
682      */
683     public void objectAdded(final Renderable2dInterface<? extends Locatable> element)
684     {
685         synchronized (this.elementList)
686         {
687             this.elements.add(element);
688             this.dirty = true;
689         }
690     }
691 
692     /**
693      * Remove a locatable object from the animation.
694      * @param element Renderable2dInterface&lt;? extends Locatable&gt;; the element to add to the animation
695      */
696     public void objectRemoved(final Renderable2dInterface<? extends Locatable> element)
697     {
698         synchronized (this.elementList)
699         {
700             this.elements.remove(element);
701             this.dirty = true;
702         }
703     }
704 
705     /**
706      * Calculate the full extent based on the current positions of the objects.
707      * @return Bounds2d; the full extent of the animation.
708      */
709     public synchronized Bounds2d fullExtent()
710     {
711         double minX = Double.MAX_VALUE;
712         double maxX = -Double.MAX_VALUE;
713         double minY = Double.MAX_VALUE;
714         double maxY = -Double.MAX_VALUE;
715         try
716         {
717             for (Renderable2dInterface<? extends Locatable> renderable : this.elementList)
718             {
719                 if (renderable.getSource() == null)
720                 {
721                     continue;
722                 }
723                 Point<?> l = renderable.getSource().getLocation();
724                 if (l != null)
725                 {
726                     Bounds<?, ?, ?> b = renderable.getSource().getBounds();
727                     minX = Math.min(minX, l.getX() + b.getMinX());
728                     minY = Math.min(minY, l.getY() + b.getMinY());
729                     maxX = Math.max(maxX, l.getX() + b.getMaxX());
730                     maxY = Math.max(maxY, l.getY() + b.getMaxY());
731                 }
732             }
733         }
734         catch (Exception e)
735         {
736             // ignore
737         }
738 
739         minX -= EXTENT_MARGIN_FACTOR * Math.abs(maxX - minX);
740         minY -= EXTENT_MARGIN_FACTOR * Math.abs(maxY - minY);
741         maxX += EXTENT_MARGIN_FACTOR * Math.abs(maxX - minX);
742         maxY += EXTENT_MARGIN_FACTOR * Math.abs(maxY - minY);
743 
744         return new Bounds2d(minX, maxX, minY, maxY);
745     }
746 
747     /**
748      * resets the panel to its an extent that covers all displayed objects.
749      */
750     public synchronized void zoomAll()
751     {
752         setExtent(getRenderableScale().computeVisibleExtent(fullExtent(), this.getSize()));
753     }
754 
755     /**
756      * Set a class to be shown in the animation to true.
757      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which the animation has to be shown.
758      */
759     public void showClass(final Class<? extends Locatable> locatableClass)
760     {
761         synchronized (this.visibilityMap)
762         {
763             this.visibilityMap.put(locatableClass, true);
764         }
765         this.shownClasses.clear();
766         this.hiddenClasses.clear();
767         this.repaint();
768     }
769 
770     /**
771      * Set a class to be hidden in the animation to true.
772      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which the animation has to be hidden.
773      */
774     public void hideClass(final Class<? extends Locatable> locatableClass)
775     {
776         synchronized (this.visibilityMap)
777         {
778             this.visibilityMap.put(locatableClass, false);
779         }
780         this.shownClasses.clear();
781         this.hiddenClasses.clear();
782         this.repaint();
783     }
784 
785     /**
786      * Toggle a class to be displayed in the animation to its reverse value.
787      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which a visible animation has to be turned off or
788      *            vice versa.
789      */
790     public void toggleClass(final Class<? extends Locatable> locatableClass)
791     {
792         synchronized (this.visibilityMap)
793         {
794             if (!this.visibilityMap.containsKey(locatableClass))
795             {
796                 showClass(locatableClass);
797             }
798             this.visibilityMap.put(locatableClass, !this.visibilityMap.get(locatableClass));
799         }
800         this.shownClasses.clear();
801         this.hiddenClasses.clear();
802         this.repaint();
803     }
804 
805     /**
806      * Handle the movement of the mouse.
807      * @param point Point; the location of the mouse relative to the AnimationPanel
808      */
809     public void mouseMoved(final java.awt.Point point)
810     {
811         Point2d world = getRenderableScale().getWorldCoordinates(point, getExtent(), getSize());
812         setWorldCoordinate(world);
813         displayWorldCoordinateToolTip();
814     }
815 
816     // public static final EventType ANIMATION_MOUSE_CLICK_EVENT = new EventType(new MetaData("ANIMATION_MOUSE_CLICK_EVENT",
817     // "ANIMATION_MOUSE_CLICK_EVENT",
818     // new ObjectDescriptor("worldCoordinate", "x and y position in world coordinates", Point2d.class),
819     // new ObjectDescriptor("screenCoordinate", "x and y position in screen coordinates", java.awt.Point.class),
820     // new ObjectDescriptor("shiftCtrlAlt", "shift[0], ctrl[1], and/or alt[2] pressed", boolean[].class),
821     // new ObjectDescriptor("objectList", "List of objects whose bounding box includes the coordinate", List.class)));
822     //
823     // public static final EventType ANIMATION_MOUSE_POPUP_EVENT = new EventType(new MetaData("ANIMATION_MOUSE_POPUP_EVENT",
824     // "ANIMATION_MOUSE_POPUP_EVENT",
825     // new ObjectDescriptor("worldCoordinate", "x and y position in world coordinates", Point2d.class),
826     // new ObjectDescriptor("screenCoordinate", "x and y position in screen coordinates", java.awt.Point.class),
827     // new ObjectDescriptor("shiftCtrlAlt", "shift[0], ctrl[1], and/or alt[2] pressed", boolean[].class),
828     // new ObjectDescriptor("object", "Selected object whose bounding box includes the coordinate", Object.class)));
829 
830     /**
831      * What to do if the left mouse button was released after a drag.
832      * @param mouseClickedPoint Point2D; the point where the mouse was clicked
833      * @param mouseReleasedPoint Point2D; the point where the mouse was released
834      */
835     protected void pan(final Point2D mouseClickedPoint, final Point2D mouseReleasedPoint)
836     {
837         // Drag extend to new location
838         double dx = mouseReleasedPoint.getX() - mouseClickedPoint.getX();
839         double dy = mouseReleasedPoint.getY() - mouseClickedPoint.getY();
840         double scaleX = getRenderableScale().getXScale(getExtent(), getSize());
841         double scaleY = getRenderableScale().getYScale(getExtent(), getSize());
842         Bounds2d extent = getExtent();
843         setExtent(new Bounds2d(extent.getMinX() - dx * scaleX, extent.getMinX() - dx * scaleX + extent.getDeltaX(),
844                 extent.getMinY() + dy * scaleY, extent.getMinY() + dy * scaleY + extent.getDeltaY()));
845     }
846 
847     /**
848      * returns the list of selected objects at a certain mousePoint.
849      * @param mousePoint Point2D; the mousePoint
850      * @return the selected objects
851      */
852     protected List<Locatable> getSelectedObjects(final Point2D mousePoint)
853     {
854         List<Locatable> targets = new ArrayList<Locatable>();
855         try
856         {
857             Point2d point = getRenderableScale().getWorldCoordinates(mousePoint, getExtent(), getSize());
858             for (Renderable2dInterface<?> renderable : getElements())
859             {
860                 if (isShowElement(renderable) && renderable.contains(point, getExtent()))
861                 {
862                     targets.add(renderable.getSource());
863                 }
864             }
865         }
866         catch (Exception exception)
867         {
868             CategoryLogger.always().warn(exception, "getSelectedObjects");
869         }
870         return targets;
871     }
872 
873     /**
874      * popup on a mouseEvent.
875      * @param e MouseEvent; the mouseEvent
876      */
877     protected void popup(final MouseEvent e)
878     {
879         List<Locatable> targets = this.getSelectedObjects(e.getPoint());
880         if (targets.size() > 0)
881         {
882             JPopupMenu popupMenu = new JPopupMenu();
883             popupMenu.add("Introspect");
884             popupMenu.add(new JSeparator());
885             for (Iterator<Locatable> i = targets.iterator(); i.hasNext();)
886             {
887                 popupMenu.add(new IntrospectionAction(i.next()));
888             }
889             popupMenu.show(this, e.getX(), e.getY());
890         }
891     }
892 
893     /**
894      * Returns the clicked Renderable2d with the highest z-value.
895      * @param targets List&lt;Locatable&gt;; which are selected by the mouse.
896      * @return the selected Object (e.g. the one with the highest zValue).
897      */
898     protected Object getSelectedObject(final List<Locatable> targets)
899     {
900         Object selectedObject = null;
901         try
902         {
903             double zValue = -Double.MAX_VALUE;
904             for (Locatable next : targets)
905             {
906                 double z = next.getZ();
907                 if (z > zValue)
908                 {
909                     zValue = z;
910                     selectedObject = next;
911                 }
912             }
913         }
914         catch (RemoteException exception)
915         {
916             CategoryLogger.always().warn(exception, "getSelectedObject");
917         }
918         return selectedObject;
919     }
920 
921     /**
922      * set the drag line: a line that shows where the user is dragging.
923      * @param mousePosition Point2D; the position of the mouse pointer
924      * @param mouseClicked Point2D; the position where the mouse was clicked before dragging
925      */
926     protected void setDragLine(final Point2D mousePosition, final Point2D mouseClicked)
927     {
928         if ((mousePosition != null) && (mouseClicked != null))
929         {
930             setDragLineEnabled(false); // to avoid problems with concurrency
931             this.dragLine = new int[4];
932             this.dragLine[0] = (int) mousePosition.getX();
933             this.dragLine[1] = (int) mousePosition.getY();
934             this.dragLine[2] = (int) mouseClicked.getX();
935             this.dragLine[3] = (int) mouseClicked.getY();
936             setDragLineEnabled(true);
937         }
938     }
939 
940     /**
941      * @return returns the dragLine.
942      */
943     public int[] getDragLine()
944     {
945         return this.dragLine;
946     }
947 
948     /**
949      * @return returns the dragLineEnabled.
950      */
951     public boolean isDragLineEnabled()
952     {
953         return this.dragLineEnabled;
954     }
955 
956     /**
957      * @param dragLineEnabled boolean; the dragLineEnabled to set.
958      */
959     public void setDragLineEnabled(final boolean dragLineEnabled)
960     {
961         this.dragLineEnabled = dragLineEnabled;
962     }
963 
964     /**
965      * @return the set of animation elements.
966      */
967     public SortedSet<Renderable2dInterface<? extends Locatable>> getElements()
968     {
969         return this.elements;
970     }
971 
972     /**
973      * EventProducer to which to delegate the event producing methods.
974      * <p>
975      * Copyright (c) 2021-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
976      * See for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The
977      * DSOL project is distributed under a three-clause BSD-style license, which can be found at
978      * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
979      * </p>
980      * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
981      */
982     class AnimationEventProducer extends LocalEventProducer
983     {
984         /** */
985         private static final long serialVersionUID = 20210213L;
986 
987         /** {@inheritDoc} */
988         @Override
989         public void fireEvent(final Event event)
990         {
991             super.fireEvent(event);
992         }
993     }
994 
995     /**
996      * Return the delegate event producer.
997      * @return AnimationEventProducer; the delegate event producer
998      */
999     public AnimationEventProducer getAnimationEventProducer()
1000     {
1001         return this.animationEventProducer;
1002     }
1003 
1004     /** {@inheritDoc} */
1005     @Override
1006     public boolean addListener(final EventListener listener, final EventType eventType)
1007     {
1008         return this.animationEventProducer.addListener(listener, eventType);
1009     }
1010 
1011     /** {@inheritDoc} */
1012     @Override
1013     public boolean addListener(final EventListener listener, final EventType eventType, final ReferenceType referenceType)
1014     {
1015         return this.animationEventProducer.addListener(listener, eventType, referenceType);
1016     }
1017 
1018     /** {@inheritDoc} */
1019     @Override
1020     public boolean addListener(final EventListener listener, final EventType eventType, final int position)
1021     {
1022         return this.animationEventProducer.addListener(listener, eventType, position);
1023     }
1024 
1025     /** {@inheritDoc} */
1026     @Override
1027     public boolean addListener(final EventListener listener, final EventType eventType, final int position,
1028             final ReferenceType referenceType)
1029     {
1030         return this.animationEventProducer.addListener(listener, eventType, position, referenceType);
1031     }
1032 
1033     /** {@inheritDoc} */
1034     @Override
1035     public boolean removeListener(final EventListener listener, final EventType eventType)
1036     {
1037         return this.animationEventProducer.removeListener(listener, eventType);
1038     }
1039 
1040     /** {@inheritDoc} */
1041     @Override
1042     public int removeAllListeners()
1043     {
1044         return this.animationEventProducer.removeAllListeners();
1045     }
1046 
1047     /** {@inheritDoc} */
1048     @Override
1049     public EventListenerMap getEventListenerMap() throws RemoteException
1050     {
1051         return this.animationEventProducer.getEventListenerMap();
1052     }
1053 
1054 }