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