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