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