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