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