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