View Javadoc
1   package org.opentrafficsim.swing.gui;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Component;
5   import java.awt.Container;
6   import java.awt.Dimension;
7   import java.awt.Graphics;
8   import java.awt.event.ActionEvent;
9   import java.awt.event.ActionListener;
10  import java.awt.event.ContainerEvent;
11  import java.awt.event.ContainerListener;
12  import java.awt.event.MouseAdapter;
13  import java.awt.event.MouseEvent;
14  import java.awt.event.MouseListener;
15  import java.awt.event.MouseWheelEvent;
16  import java.awt.event.MouseWheelListener;
17  import java.awt.event.WindowEvent;
18  import java.awt.event.WindowListener;
19  import java.awt.geom.Point2D;
20  import java.awt.geom.Rectangle2D;
21  import java.rmi.RemoteException;
22  import java.text.NumberFormat;
23  import java.util.ArrayList;
24  import java.util.LinkedHashMap;
25  import java.util.List;
26  import java.util.Map;
27  
28  import javax.swing.BoxLayout;
29  import javax.swing.Icon;
30  import javax.swing.JButton;
31  import javax.swing.JCheckBox;
32  import javax.swing.JFrame;
33  import javax.swing.JLabel;
34  import javax.swing.JPanel;
35  import javax.swing.JToggleButton;
36  import javax.swing.border.EmptyBorder;
37  
38  import org.djutils.draw.bounds.Bounds2d;
39  import org.djutils.draw.point.Point;
40  import org.djutils.draw.point.Point2d;
41  import org.djutils.event.Event;
42  import org.djutils.event.EventListener;
43  import org.djutils.event.TimedEvent;
44  import org.djutils.exceptions.Throw;
45  import org.opentrafficsim.core.animation.gtu.colorer.GtuColorer;
46  import org.opentrafficsim.core.dsol.OtsAnimator;
47  import org.opentrafficsim.core.dsol.OtsModelInterface;
48  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
49  import org.opentrafficsim.core.gtu.Gtu;
50  import org.opentrafficsim.core.network.Network;
51  
52  import nl.tudelft.simulation.dsol.animation.Locatable;
53  import nl.tudelft.simulation.dsol.animation.D2.Renderable2DInterface;
54  import nl.tudelft.simulation.dsol.animation.gis.GisMapInterface;
55  import nl.tudelft.simulation.dsol.animation.gis.GisRenderable2D;
56  import nl.tudelft.simulation.dsol.experiment.ReplicationInterface;
57  import nl.tudelft.simulation.dsol.swing.animation.D2.AnimationPanel;
58  import nl.tudelft.simulation.dsol.swing.animation.D2.GridPanel;
59  import nl.tudelft.simulation.dsol.swing.animation.D2.InputListener;
60  import nl.tudelft.simulation.language.DSOLException;
61  
62  /**
63   * Animation panel with various controls.
64   * <p>
65   * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
66   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
67   * </p>
68   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
69   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
70   */
71  public class OtsAnimationPanel extends OtsSimulationPanel implements ActionListener, WindowListener, EventListener
72  {
73      /** */
74      private static final long serialVersionUID = 20150617L;
75  
76      /** The animation panel on tab position 0. */
77      private final AutoAnimationPanel animationPanel;
78  
79      /** Border panel in which the animation is shown. */
80      private final JPanel borderPanel;
81  
82      /** Toggle panel with which animation features can be shown/hidden. */
83      private final JPanel togglePanel;
84  
85      /** Demo panel. */
86      private JPanel demoPanel = null;
87  
88      /** Map of toggle names to toggle animation classes. */
89      private Map<String, Class<? extends Locatable>> toggleLocatableMap = new LinkedHashMap<>();
90  
91      /** Set of animation classes to toggle buttons. */
92      private Map<Class<? extends Locatable>, JToggleButton> toggleButtons = new LinkedHashMap<>();
93  
94      /** Set of GIS layer names to toggle GIS layers . */
95      private Map<String, GisMapInterface> toggleGISMap = new LinkedHashMap<>();
96  
97      /** Set of GIS layer names to toggle buttons. */
98      private Map<String, JToggleButton> toggleGISButtons = new LinkedHashMap<>();
99  
100     /** The switchableGtuColorer used to color the GTUs. */
101     private GtuColorer gtuColorer = null;
102 
103     /** The ColorControlPanel that allows the user to operate the SwitchableGtuColorer. */
104     private ColorControlPanel colorControlPanel = null;
105 
106     /** The coordinates of the cursor. */
107     private final JLabel coordinateField;
108 
109     /** The GTU count field. */
110     private final JLabel gtuCountField;
111 
112     /** The GTU count. */
113     private int gtuCount = 0;
114 
115     /** The animation buttons. */
116     private final ArrayList<JButton> buttons = new ArrayList<>();
117 
118     /** The formatter for the world coordinates. */
119     private static final NumberFormat FORMATTER = NumberFormat.getInstance();
120 
121     /** Has the window close handler been registered? */
122     @SuppressWarnings("checkstyle:visibilitymodifier")
123     protected boolean closeHandlerRegistered = false;
124 
125     /** Indicate the window has been closed and the timer thread can stop. */
126     @SuppressWarnings("checkstyle:visibilitymodifier")
127     protected boolean windowExited = false;
128 
129     /** Id of object to auto pan to. */
130     private String autoPanId = null;
131 
132     /** Type of object to auto pan to. */
133     private OtsSearchPanel.ObjectKind<?> autoPanKind = null;
134 
135     /** Track auto pan object continuously? */
136     private boolean autoPanTrack = false;
137 
138     /** Track auto on the next paintComponent operation; then copy state from autoPanTrack. */
139     private boolean autoPanOnNextPaintComponent = false;
140 
141     /** Initialize the formatter. */
142     static
143     {
144         FORMATTER.setMaximumFractionDigits(3);
145     }
146 
147     /**
148      * Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
149      * @param extent Rectangle2D; bottom left corner, length and width of the area (world) to animate.
150      * @param size Dimension; the size to be used for the animation.
151      * @param simulator OTSAnimator; the simulator or animator of the model.
152      * @param otsModel OtsModelInterface; the builder and rebuilder of the simulation, based on properties.
153      * @param gtuColorer GtuColorer; the colorer to use for the GTUs.
154      * @param network Network; network
155      * @throws RemoteException when notification of the animation panel fails
156      * @throws DSOLException when simulator does not implement AnimatorInterface
157      */
158     public OtsAnimationPanel(final Rectangle2D extent, final Dimension size, final OtsAnimator simulator,
159             final OtsModelInterface otsModel, final GtuColorer gtuColorer, final Network network)
160             throws RemoteException, DSOLException
161     {
162         super(simulator, otsModel);
163 
164         // Add the animation panel as a tab.
165 
166         this.animationPanel = new AutoAnimationPanel(extent, size, simulator, network);
167         this.animationPanel.showGrid(false);
168         this.borderPanel = new JPanel(new BorderLayout());
169         this.borderPanel.add(this.animationPanel, BorderLayout.CENTER);
170         getTabbedPane().addTab(0, "animation", this.borderPanel);
171         getTabbedPane().setSelectedIndex(0); // Show the animation panel as the default tab
172 
173         // Include the GTU colorer control panel NORTH of the animation.
174         this.gtuColorer = gtuColorer;
175         this.colorControlPanel = new ColorControlPanel(this.gtuColorer);
176         JPanel buttonPanel = new JPanel();
177         buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
178         this.borderPanel.add(buttonPanel, BorderLayout.NORTH);
179         buttonPanel.add(this.colorControlPanel);
180 
181         // Include the TogglePanel WEST of the animation.
182         this.togglePanel = new JPanel();
183         this.togglePanel.setLayout(new BoxLayout(this.togglePanel, BoxLayout.Y_AXIS));
184         this.borderPanel.add(this.togglePanel, BorderLayout.WEST);
185 
186         // add the buttons for home, zoom all, grid, and mouse coordinates
187         buttonPanel.add(new JLabel("   "));
188         buttonPanel.add(makeButton("allButton", "/Expand.png", "ZoomAll", "Zoom whole network", true));
189         buttonPanel.add(makeButton("homeButton", "/Home.png", "Home", "Zoom to original extent", true));
190         buttonPanel.add(makeButton("gridButton", "/Grid.png", "Grid", "Toggle grid on/off", true));
191         buttonPanel.add(new JLabel("   "));
192 
193         // add info labels next to buttons
194         JPanel infoTextPanel = new JPanel();
195         buttonPanel.add(infoTextPanel);
196         infoTextPanel.setMinimumSize(new Dimension(250, 20));
197         infoTextPanel.setPreferredSize(new Dimension(250, 20));
198         infoTextPanel.setLayout(new BoxLayout(infoTextPanel, BoxLayout.Y_AXIS));
199         this.coordinateField = new JLabel("Mouse: ");
200         this.coordinateField.setMinimumSize(new Dimension(250, 10));
201         this.coordinateField.setPreferredSize(new Dimension(250, 10));
202         infoTextPanel.add(this.coordinateField);
203         // gtu fields
204         JPanel gtuPanel = new JPanel();
205         gtuPanel.setAlignmentX(0.0f);
206         gtuPanel.setLayout(new BoxLayout(gtuPanel, BoxLayout.X_AXIS));
207         gtuPanel.setMinimumSize(new Dimension(250, 10));
208         gtuPanel.setPreferredSize(new Dimension(250, 10));
209         infoTextPanel.add(gtuPanel);
210         if (null != network)
211         {
212             network.addListener(this, Network.GTU_ADD_EVENT);
213             network.addListener(this, Network.GTU_REMOVE_EVENT);
214         }
215         // gtu counter
216         this.gtuCountField = new JLabel("0 GTU's");
217         this.gtuCount = null == network ? 0 : network.getGTUs().size();
218         gtuPanel.add(this.gtuCountField);
219         setGtuCountText();
220 
221         // Tell the animation to build the list of animation objects.
222         this.animationPanel.notify(
223                 new TimedEvent<>(ReplicationInterface.START_REPLICATION_EVENT, null, getSimulator().getSimulatorTime()));
224 
225         // switch off the X and Y coordinates in a tooltip.
226         this.animationPanel.setShowToolTip(false);
227 
228         // run the update task for the mouse coordinate panel
229         new UpdateTimer().start();
230 
231         // make sure the thread gets killed when the window closes.
232         installWindowCloseHandler();
233 
234     }
235 
236     /**
237      * Change auto pan target.
238      * @param newAutoPanId String; id of object to track (or
239      * @param newAutoPanKind String; kind of object to track
240      * @param newAutoPanTrack boolean; if true; tracking is continuously; if false; tracking is once
241      */
242     public void setAutoPan(final String newAutoPanId, final OtsSearchPanel.ObjectKind<?> newAutoPanKind,
243             final boolean newAutoPanTrack)
244     {
245         this.autoPanId = newAutoPanId;
246         this.autoPanKind = newAutoPanKind;
247         this.autoPanTrack = newAutoPanTrack;
248         this.autoPanOnNextPaintComponent = true;
249         // System.out.println("AutoPan id=" + newAutoPanId + ", kind=" + newAutoPanKind + ", track=" + newAutoPanTrack);
250         if (null != this.autoPanId && this.autoPanId.length() > 0 && null != this.autoPanKind)
251         {
252             OtsAnimationPanel.this.animationPanel.repaint();
253         }
254     }
255 
256     /**
257      * Create a button.
258      * @param name String; name of the button
259      * @param iconPath String; path to the resource
260      * @param actionCommand String; the action command
261      * @param toolTipText String; the hint to show when the mouse hovers over the button
262      * @param enabled boolean; true if the new button must initially be enable; false if it must initially be disabled
263      * @return JButton
264      */
265     private JButton makeButton(final String name, final String iconPath, final String actionCommand, final String toolTipText,
266             final boolean enabled)
267     {
268         // JButton result = new JButton(new ImageIcon(this.getClass().getResource(iconPath)));
269         JButton result = new JButton(OtsControlPanel.loadIcon(iconPath));
270         result.setPreferredSize(new Dimension(34, 32));
271         result.setName(name);
272         result.setEnabled(enabled);
273         result.setActionCommand(actionCommand);
274         result.setToolTipText(toolTipText);
275         result.addActionListener(this);
276         this.buttons.add(result);
277         return result;
278     }
279 
280     /**
281      * Add a button for toggling an animatable class on or off. Button icons for which 'idButton' is true will be placed to the
282      * right of the previous button, which should be the corresponding button without the id. An example is an icon for
283      * showing/hiding the class 'Lane' followed by the button to show/hide the Lane ids.
284      * @param name String; the name of the button
285      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which the button holds (e.g., GTU.class)
286      * @param iconPath String; the path to the 24x24 icon to display
287      * @param toolTipText String; the tool tip text to show when hovering over the button
288      * @param initiallyVisible boolean; whether the class is initially shown or not
289      * @param idButton boolean; id button that needs to be placed next to the previous button
290      */
291     public final void addToggleAnimationButtonIcon(final String name, final Class<? extends Locatable> locatableClass,
292             final String iconPath, final String toolTipText, final boolean initiallyVisible, final boolean idButton)
293     {
294         JToggleButton button;
295         Icon icon = OtsControlPanel.loadIcon(iconPath);
296         Icon unIcon = OtsControlPanel.loadGrayscaleIcon(iconPath);
297         button = new JCheckBox();
298         button.setSelectedIcon(icon);
299         button.setIcon(unIcon);
300         button.setPreferredSize(new Dimension(32, 28));
301         button.setName(name);
302         button.setEnabled(true);
303         button.setSelected(initiallyVisible);
304         button.setActionCommand(name);
305         button.setToolTipText(toolTipText);
306         button.addActionListener(this);
307 
308         // place an Id button to the right of the corresponding content button
309         if (idButton && this.togglePanel.getComponentCount() > 0)
310         {
311             JPanel lastToggleBox = (JPanel) this.togglePanel.getComponent(this.togglePanel.getComponentCount() - 1);
312             lastToggleBox.add(button);
313         }
314         else
315         {
316             JPanel toggleBox = new JPanel();
317             toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
318             toggleBox.add(button);
319             this.togglePanel.add(toggleBox);
320             toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
321         }
322 
323         if (initiallyVisible)
324         {
325             this.animationPanel.showClass(locatableClass);
326         }
327         else
328         {
329             this.animationPanel.hideClass(locatableClass);
330         }
331         this.toggleLocatableMap.put(name, locatableClass);
332         this.toggleButtons.put(locatableClass, button);
333     }
334 
335     /**
336      * Add a button for toggling an animatable class on or off.
337      * @param name String; the name of the button
338      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which the button holds (e.g., GTU.class)
339      * @param toolTipText String; the tool tip text to show when hovering over the button
340      * @param initiallyVisible boolean; whether the class is initially shown or not
341      */
342     public final void addToggleAnimationButtonText(final String name, final Class<? extends Locatable> locatableClass,
343             final String toolTipText, final boolean initiallyVisible)
344     {
345         JToggleButton button;
346         button = new JCheckBox(name);
347         button.setName(name);
348         button.setEnabled(true);
349         button.setSelected(initiallyVisible);
350         button.setActionCommand(name);
351         button.setToolTipText(toolTipText);
352         button.addActionListener(this);
353 
354         JPanel toggleBox = new JPanel();
355         toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
356         toggleBox.add(button);
357         this.togglePanel.add(toggleBox);
358         toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
359 
360         if (initiallyVisible)
361         {
362             this.animationPanel.showClass(locatableClass);
363         }
364         else
365         {
366             this.animationPanel.hideClass(locatableClass);
367         }
368         this.toggleLocatableMap.put(name, locatableClass);
369         this.toggleButtons.put(locatableClass, button);
370     }
371 
372     /**
373      * Add a text to explain animatable classes.
374      * @param text String; the text to show
375      */
376     public final void addToggleText(final String text)
377     {
378         JPanel textBox = new JPanel();
379         textBox.setLayout(new BoxLayout(textBox, BoxLayout.X_AXIS));
380         textBox.add(new JLabel(text));
381         this.togglePanel.add(textBox);
382         textBox.setAlignmentX(Component.LEFT_ALIGNMENT);
383     }
384 
385     /**
386      * Add buttons for toggling all GIS layers on or off.
387      * @param header String; the name of the group of layers
388      * @param gisMap GisRenderable2D; the GIS map for which the toggles have to be added
389      * @param toolTipText String; the tool tip text to show when hovering over the button
390      */
391     public final void addAllToggleGISButtonText(final String header, final GisRenderable2D gisMap, final String toolTipText)
392     {
393         addToggleText(" ");
394         addToggleText(header);
395         try
396         {
397             for (String layerName : gisMap.getMap().getLayerMap().keySet())
398             {
399                 addToggleGISButtonText(layerName, layerName, gisMap, toolTipText);
400             }
401         }
402         catch (RemoteException exception)
403         {
404             exception.printStackTrace();
405         }
406     }
407 
408     /**
409      * Add a button to toggle a GIS Layer on or off.
410      * @param layerName String; the name of the layer
411      * @param displayName String; the name to display next to the tick box
412      * @param gisMap GisRenderable2D; the map
413      * @param toolTipText String; the tool tip text
414      */
415     public final void addToggleGISButtonText(final String layerName, final String displayName, final GisRenderable2D gisMap,
416             final String toolTipText)
417     {
418         JToggleButton button;
419         button = new JCheckBox(displayName);
420         button.setName(layerName);
421         button.setEnabled(true);
422         button.setSelected(true);
423         button.setActionCommand(layerName);
424         button.setToolTipText(toolTipText);
425         button.addActionListener(this);
426 
427         JPanel toggleBox = new JPanel();
428         toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
429         toggleBox.add(button);
430         this.togglePanel.add(toggleBox);
431         toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
432 
433         this.toggleGISMap.put(layerName, gisMap.getMap());
434         this.toggleGISButtons.put(layerName, button);
435     }
436 
437     /**
438      * Set a GIS layer to be shown in the animation to true.
439      * @param layerName String; the name of the GIS-layer that has to be shown.
440      */
441     public final void showGISLayer(final String layerName)
442     {
443         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
444         if (gisMap != null)
445         {
446             try
447             {
448                 gisMap.showLayer(layerName);
449                 this.toggleGISButtons.get(layerName).setSelected(true);
450                 this.animationPanel.repaint();
451             }
452             catch (RemoteException exception)
453             {
454                 exception.printStackTrace();
455             }
456         }
457     }
458 
459     /**
460      * Set a GIS layer to be hidden in the animation to true.
461      * @param layerName String; the name of the GIS-layer that has to be hidden.
462      */
463     public final void hideGISLayer(final String layerName)
464     {
465         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
466         if (gisMap != null)
467         {
468             try
469             {
470                 gisMap.hideLayer(layerName);
471                 this.toggleGISButtons.get(layerName).setSelected(false);
472                 this.animationPanel.repaint();
473             }
474             catch (RemoteException exception)
475             {
476                 exception.printStackTrace();
477             }
478         }
479     }
480 
481     /**
482      * Toggle a GIS layer to be displayed in the animation to its reverse value.
483      * @param layerName String; the name of the GIS-layer that has to be turned off or vice versa.
484      */
485     public final void toggleGISLayer(final String layerName)
486     {
487         GisMapInterface gisMap = this.toggleGISMap.get(layerName);
488         if (gisMap != null)
489         {
490             try
491             {
492                 if (gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
493                 {
494                     gisMap.hideLayer(layerName);
495                     this.toggleGISButtons.get(layerName).setSelected(false);
496                 }
497                 else
498                 {
499                     gisMap.showLayer(layerName);
500                     this.toggleGISButtons.get(layerName).setSelected(true);
501                 }
502                 this.animationPanel.repaint();
503             }
504             catch (RemoteException exception)
505             {
506                 exception.printStackTrace();
507             }
508         }
509     }
510 
511     /** {@inheritDoc} */
512     @Override
513     public final void actionPerformed(final ActionEvent actionEvent)
514     {
515         String actionCommand = actionEvent.getActionCommand();
516         // System.out.println("Action command is " + actionCommand);
517         try
518         {
519             if (actionCommand.equals("Home"))
520             {
521                 this.animationPanel.home();
522             }
523             if (actionCommand.equals("ZoomAll"))
524             {
525                 this.animationPanel.zoomAll();
526             }
527             if (actionCommand.equals("Grid"))
528             {
529                 this.animationPanel.showGrid(!this.animationPanel.isShowGrid());
530             }
531 
532             if (this.toggleLocatableMap.containsKey(actionCommand))
533             {
534                 Class<? extends Locatable> locatableClass = this.toggleLocatableMap.get(actionCommand);
535                 this.animationPanel.toggleClass(locatableClass);
536                 this.togglePanel.repaint();
537             }
538 
539             if (this.toggleGISMap.containsKey(actionCommand))
540             {
541                 this.toggleGISLayer(actionCommand);
542                 this.togglePanel.repaint();
543             }
544         }
545         catch (Exception exception)
546         {
547             exception.printStackTrace();
548         }
549     }
550 
551     /**
552      * Easy access to the AnimationPanel.
553      * @return AnimationPanel
554      */
555     public final AnimationPanel getAnimationPanel()
556     {
557         return this.animationPanel;
558     }
559 
560     /**
561      * Creates a demo panel within the animation area.
562      * @param position String; any string from BorderLayout indicating the position of the demo panel, except CENTER.
563      * @throws IllegalStateException if the panel was already created
564      */
565     public void createDemoPanel(final DemoPanelPosition position)
566     {
567         Throw.when(this.demoPanel != null, IllegalStateException.class,
568                 "Attempt to create demo panel, but it's already created");
569         Throw.whenNull(position, "Position may not be null.");
570         Container parent = this.animationPanel.getParent();
571         parent.remove(this.animationPanel);
572 
573         JPanel splitPanel = new JPanel(new BorderLayout());
574         parent.add(splitPanel);
575         splitPanel.add(this.animationPanel, BorderLayout.CENTER);
576 
577         this.demoPanel = new JPanel();
578         this.demoPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
579         splitPanel.add(this.demoPanel, position.getBorderLayoutPosition());
580     }
581 
582     /**
583      * Return a panel for on-screen demo controls. The panel is create on first call.
584      * @return JPanel; panel
585      */
586     public JPanel getDemoPanel()
587     {
588         if (this.demoPanel == null)
589         {
590             createDemoPanel(DemoPanelPosition.RIGHT);
591             // this.demoPanel = new JPanel();
592             // this.demoPanel.setLayout(new BoxLayout(this.demoPanel, BoxLayout.Y_AXIS));
593             // this.demoPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
594             // this.demoPanel.setPreferredSize(new Dimension(300, 300));
595             // getAnimationPanel().getParent().add(this.demoPanel, BorderLayout.EAST);
596             this.demoPanel.addContainerListener(new ContainerListener()
597             {
598                 @Override
599                 public void componentAdded(final ContainerEvent e)
600                 {
601                     try
602                     {
603                         // setAppearance(getAppearance());
604                     }
605                     catch (NullPointerException exception)
606                     {
607                         //
608                     }
609                 }
610 
611                 @Override
612                 public void componentRemoved(final ContainerEvent e)
613                 {
614                     //
615                 }
616             });
617         }
618         return this.demoPanel;
619     }
620 
621     /**
622      * Update the checkmark related to a programmatically changed animation state.
623      * @param locatableClass Class&lt;? extends Locatable&gt;; class to show the checkmark for
624      */
625     public final void updateAnimationClassCheckBox(final Class<? extends Locatable> locatableClass)
626     {
627         JToggleButton button = this.toggleButtons.get(locatableClass);
628         if (button == null)
629         {
630             return;
631         }
632         button.setSelected(getAnimationPanel().isShowClass(locatableClass));
633     }
634 
635     /**
636      * Display the latest world coordinate based on the mouse position on the screen.
637      */
638     protected final void updateWorldCoordinate()
639     {
640         String worldPoint = "(x=" + FORMATTER.format(this.animationPanel.getWorldCoordinate().getX()) + " ; y="
641                 + FORMATTER.format(this.animationPanel.getWorldCoordinate().getY()) + ")";
642         this.coordinateField.setText("Mouse: " + worldPoint);
643         int requiredWidth = this.coordinateField.getGraphics().getFontMetrics().stringWidth(this.coordinateField.getText());
644         if (this.coordinateField.getPreferredSize().width < requiredWidth)
645         {
646             Dimension requiredSize = new Dimension(requiredWidth, this.coordinateField.getPreferredSize().height);
647             this.coordinateField.setPreferredSize(requiredSize);
648             this.coordinateField.setMinimumSize(requiredSize);
649             Container parent = this.coordinateField.getParent();
650             parent.setPreferredSize(requiredSize);
651             parent.setMinimumSize(requiredSize);
652             // System.out.println("Increased minimum width to " + requiredSize.width);
653             parent.revalidate();
654         }
655         this.coordinateField.repaint();
656     }
657 
658     /**
659      * Access the GtuColorer of this animation ControlPanel.
660      * @return GtuColorer the colorer used. If it is a SwitchableGtuColorer, the wrapper with the list will be returned, not the
661      *         actual colorer in use.
662      */
663     public final GtuColorer getGtuColorer()
664     {
665         return this.gtuColorer;
666     }
667 
668     /**
669      * Access the ColorControlPanel of this ControlPanel. If the simulator is not a SimpleAnimator, no ColorControlPanel was
670      * constructed and this method will return null.
671      * @return ColorControlPanel
672      */
673     public final ColorControlPanel getColorControlPanel()
674     {
675         return this.colorControlPanel;
676     }
677 
678     /**
679      * Install a handler for the window closed event that stops the simulator (if it is running).
680      */
681     public final void installWindowCloseHandler()
682     {
683         if (this.closeHandlerRegistered)
684         {
685             return;
686         }
687 
688         // make sure the root frame gets disposed of when the closing X icon is pressed.
689         new DisposeOnCloseThread(this).start();
690     }
691 
692     /** Install the dispose on close when the OtsControlPanel is registered as part of a frame. */
693     protected class DisposeOnCloseThread extends Thread
694     {
695         /** The current container. */
696         private OtsAnimationPanel panel;
697 
698         /**
699          * @param panel OtsAnimationPanel; the OTSControlpanel container.
700          */
701         public DisposeOnCloseThread(final OtsAnimationPanel panel)
702         {
703             this.panel = panel;
704         }
705 
706         /** {@inheritDoc} */
707         @Override
708         public final void run()
709         {
710             Container root = this.panel;
711             while (!(root instanceof JFrame))
712             {
713                 try
714                 {
715                     Thread.sleep(10);
716                 }
717                 catch (InterruptedException exception)
718                 {
719                     // nothing to do
720                 }
721 
722                 // Search towards the root of the Swing components until we find a JFrame
723                 root = this.panel;
724                 while (null != root.getParent() && !(root instanceof JFrame))
725                 {
726                     root = root.getParent();
727                 }
728             }
729             JFrame frame = (JFrame) root;
730             frame.addWindowListener(this.panel);
731             this.panel.closeHandlerRegistered = true;
732         }
733 
734         /** {@inheritDoc} */
735         @Override
736         public final String toString()
737         {
738             return "DisposeOnCloseThread of OtsAnimationPanel [panel=" + this.panel + "]";
739         }
740     }
741 
742     /** {@inheritDoc} */
743     @Override
744     public void windowOpened(final WindowEvent e)
745     {
746         // No action
747     }
748 
749     /** {@inheritDoc} */
750     @Override
751     public final void windowClosing(final WindowEvent e)
752     {
753         // No action
754     }
755 
756     /** {@inheritDoc} */
757     @Override
758     public final void windowClosed(final WindowEvent e)
759     {
760         this.windowExited = true;
761     }
762 
763     /** {@inheritDoc} */
764     @Override
765     public final void windowIconified(final WindowEvent e)
766     {
767         // No action
768     }
769 
770     /** {@inheritDoc} */
771     @Override
772     public final void windowDeiconified(final WindowEvent e)
773     {
774         // No action
775     }
776 
777     /** {@inheritDoc} */
778     @Override
779     public final void windowActivated(final WindowEvent e)
780     {
781         // No action
782     }
783 
784     /** {@inheritDoc} */
785     @Override
786     public final void windowDeactivated(final WindowEvent e)
787     {
788         // No action
789     }
790 
791     /** {@inheritDoc} */
792     @Override
793     public void notify(final Event event) throws RemoteException
794     {
795         if (event.getType().equals(Network.GTU_ADD_EVENT))
796         {
797             this.gtuCount++;
798             setGtuCountText();
799         }
800         else if (event.getType().equals(Network.GTU_REMOVE_EVENT))
801         {
802             this.gtuCount--;
803             setGtuCountText();
804         }
805     }
806 
807     /**
808      * Updates the text of the GTU counter.
809      */
810     private void setGtuCountText()
811     {
812         this.gtuCountField.setText(this.gtuCount + " GTU's");
813     }
814 
815     /**
816      * UpdateTimer class to update the coordinate on the screen.
817      */
818     protected class UpdateTimer extends Thread
819     {
820         /** {@inheritDoc} */
821         @Override
822         public final void run()
823         {
824             while (!OtsAnimationPanel.this.windowExited)
825             {
826                 if (OtsAnimationPanel.this.isShowing())
827                 {
828                     OtsAnimationPanel.this.updateWorldCoordinate();
829                 }
830                 try
831                 {
832                     Thread.sleep(50); // 20 times per second
833                 }
834                 catch (InterruptedException exception)
835                 {
836                     // do nothing
837                 }
838             }
839         }
840 
841         /** {@inheritDoc} */
842         @Override
843         public final String toString()
844         {
845             return "UpdateTimer thread for OtsAnimationPanel";
846         }
847 
848     }
849 
850     /**
851      * Animation panel that adds autopan functionality.
852      * <p>
853      * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
854      * <br>
855      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
856      * </p>
857      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
858      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
859      * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
860      */
861     private class AutoAnimationPanel extends AnimationPanel
862     {
863 
864         /** */
865         private static final long serialVersionUID = 20180430L;
866 
867         /** Network. */
868         private final Network network;
869 
870         /** Last GTU that was followed. */
871         private Gtu lastGtu;
872 
873         /**
874          * Constructor.
875          * @param extent Rectangle2D; home extent
876          * @param size Dimension; size
877          * @param simulator SimulatorInterface&lt;?, ?, ?&gt;; simulator
878          * @param network Network; network
879          * @throws RemoteException on remote animation error
880          * @throws DSOLException when simulator does not implement AnimatorInterface
881          */
882         AutoAnimationPanel(final Rectangle2D extent, final Dimension size, final OtsSimulatorInterface simulator,
883                 final Network network) throws RemoteException, DSOLException
884         {
885             super(new Bounds2d(extent.getMinX(), extent.getMaxX(), extent.getMinY(), extent.getMaxY()), simulator);
886             this.network = network;
887             MouseListener[] listeners = getMouseListeners();
888             for (MouseListener listener : listeners)
889             {
890                 removeMouseListener(listener);
891             }
892             this.addMouseListener(new MouseAdapter()
893             {
894                 /** {@inheritDoc} */
895                 @SuppressWarnings("synthetic-access")
896                 @Override
897                 public void mouseClicked(final MouseEvent e)
898                 {
899                     if (e.isControlDown())
900                     {
901                         Gtu gtu = getSelectedGTU(e.getPoint());
902                         if (gtu != null)
903                         {
904                             getOtsControlPanel().getOtsSearchPanel().selectAndTrackObject("GTU", gtu.getId(), true);
905                             e.consume(); // sadly doesn't work to prevent a pop up
906                         }
907                     }
908                     e.consume();
909                 }
910             });
911             for (MouseListener listener : listeners)
912             {
913                 addMouseListener(listener);
914             }
915             // mouse wheel
916             MouseWheelListener[] wheelListeners = getMouseWheelListeners();
917             for (MouseWheelListener wheelListener : wheelListeners)
918             {
919                 removeMouseWheelListener(wheelListener);
920             }
921             this.addMouseWheelListener(new InputListener(this)
922             {
923                 /** {@inheritDoc} */
924                 @Override
925                 public void mouseWheelMoved(final MouseWheelEvent e)
926                 {
927                     if (e.isShiftDown())
928                     {
929                         int amount = e.getUnitsToScroll();
930                         if (amount > 0)
931                         {
932                             zoomVertical(GridPanel.ZOOMFACTOR, e.getX(), e.getY());
933                         }
934                         else
935                         {
936                             zoomVertical(1.0 / GridPanel.ZOOMFACTOR, e.getX(), e.getY());
937                         }
938                     }
939                     else if (e.isAltDown())
940                     {
941                         int amount = e.getUnitsToScroll();
942                         if (amount > 0)
943                         {
944                             zoomHorizontal(GridPanel.ZOOMFACTOR, e.getX(), e.getY());
945                         }
946                         else
947                         {
948                             zoomHorizontal(1.0 / GridPanel.ZOOMFACTOR, e.getX(), e.getY());
949                         }
950                     }
951                     else
952                     {
953                         super.mouseWheelMoved(e);
954                     }
955                 }
956             });
957         }
958 
959         /**
960          * Zoom vertical.
961          * @param factor double; The zoom factor
962          * @param mouseX int; x-position of the mouse around which we zoom
963          * @param mouseY int; y-position of the mouse around which we zoom
964          */
965         final synchronized void zoomVertical(final double factor, final int mouseX, final int mouseY)
966         {
967             // TODO allow vertical and horizontal zoom when DSOL supports it in getScreenCoordinates() and getWorldCoordinates()
968             this.zoom(factor, mouseX, mouseY);
969             // double minX = this.extent.getMinX();
970             // Point2D mwc = Renderable2DInterface.Util.getWorldCoordinates(new Point2D.Double(mouseX, mouseY), this.extent,
971             // this.getSize());
972             // double minY = mwc.getY() - (mwc.getY() - this.extent.getMinY()) * factor;
973             // double w = this.extent.getWidth();
974             // double h = this.extent.getHeight() * factor;
975             //
976             // this.extent.setRect(minX, minY, w, h);
977             // this.repaint();
978         }
979 
980         /**
981          * Zoom horizontal.
982          * @param factor double; The zoom factor
983          * @param mouseX int; x-position of the mouse around which we zoom
984          * @param mouseY int; y-position of the mouse around which we zoom
985          */
986         final synchronized void zoomHorizontal(final double factor, final int mouseX, final int mouseY)
987         {
988             this.zoom(factor, mouseX, mouseY);
989             // double minY = this.extent.getMinY();
990             // Point2D mwc = Renderable2DInterface.Util.getWorldCoordinates(new Point2D.Double(mouseX, mouseY), this.extent,
991             // this.getSize());
992             // double minX = mwc.getX() - (mwc.getX() - this.extent.getMinX()) * factor;
993             // double w = this.extent.getWidth() * factor;
994             // double h = this.extent.getHeight();
995             //
996             // this.extent.setRect(minX, minY, w, h);
997             // this.repaint();
998         }
999 
1000         /**
1001          * returns the list of selected objects at a certain mousePoint.
1002          * @param mousePoint Point2D; the mousePoint
1003          * @return the selected objects
1004          */
1005         @SuppressWarnings("synthetic-access")
1006         protected Gtu getSelectedGTU(final Point2D mousePoint)
1007         {
1008             List<Gtu> targets = new ArrayList<>();
1009             Point2d point = getRenderableScale().getWorldCoordinates(mousePoint, getExtent(), getSize());
1010             for (Renderable2DInterface<?> renderable : getElements())
1011             {
1012                 if (isShowElement(renderable) && renderable.contains(point, getExtent()))
1013                 {
1014                     if (renderable.getSource() instanceof Gtu)
1015                     {
1016                         targets.add((Gtu) renderable.getSource());
1017                     }
1018                 }
1019             }
1020             if (targets.size() == 1)
1021             {
1022                 return targets.get(0);
1023             }
1024             return null;
1025         }
1026 
1027         /** {@inheritDoc} */
1028         @SuppressWarnings("synthetic-access")
1029         @Override
1030         public void paintComponent(final Graphics g)
1031         {
1032             final OtsSearchPanel.ObjectKind<?> panKind = OtsAnimationPanel.this.autoPanKind;
1033             final String panId = OtsAnimationPanel.this.autoPanId;
1034             final boolean doPan = OtsAnimationPanel.this.autoPanOnNextPaintComponent;
1035             OtsAnimationPanel.this.autoPanOnNextPaintComponent = OtsAnimationPanel.this.autoPanTrack;
1036             if (doPan && panKind != null && panId != null)
1037             {
1038                 Locatable locatable = panKind.searchNetwork(this.network, panId);
1039                 if (null != locatable)
1040                 {
1041                     try
1042                     {
1043                         Point<?> point = locatable.getLocation();
1044                         if (point != null) // Center extent around point
1045                         {
1046                             double w = getExtent().getDeltaX();
1047                             double h = getExtent().getDeltaY();
1048                             setExtent(new Bounds2d(point.getX() - w / 2, point.getX() + w / 2, point.getY() - h / 2,
1049                                     point.getY() + h / 2));
1050                         }
1051                     }
1052                     catch (RemoteException exception)
1053                     {
1054                         getSimulator().getLogger().always().warn(
1055                                 "Caught RemoteException trying to locate {} with id {} in network {}.", panKind, panId,
1056                                 this.network.getId());
1057                         return;
1058                     }
1059                 }
1060             }
1061             super.paintComponent(g);
1062         }
1063 
1064         /** {@inheritDoc} */
1065         @Override
1066         public String toString()
1067         {
1068             return "AutoAnimationPanel [network=" + this.network + ", lastGtu=" + this.lastGtu + "]";
1069         }
1070     }
1071 
1072     /**
1073      * Enum for demo panel position. Each value contains a field representing the position correlating to the
1074      * {@code BorderLayout} class.
1075      */
1076     public enum DemoPanelPosition
1077     {
1078         /** Top. */
1079         TOP("First"),
1080 
1081         /** Bottom. */
1082         BOTTOM("Last"),
1083 
1084         /** Left. */
1085         LEFT("Before"),
1086 
1087         /** Right. */
1088         RIGHT("After");
1089 
1090         /** Value used in {@code BorderLayout}. */
1091         private final String direction;
1092 
1093         /**
1094          * @param direction String; value used in {@code BorderLayout}
1095          */
1096         DemoPanelPosition(final String direction)
1097         {
1098             this.direction = direction;
1099         }
1100 
1101         /**
1102          * @return direction String; value used in {@code BorderLayout}
1103          */
1104         public String getBorderLayoutPosition()
1105         {
1106             return this.direction;
1107         }
1108     }
1109 
1110 }