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