View Javadoc
1   package org.opentrafficsim.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.event.ActionEvent;
8   import java.awt.event.ActionListener;
9   import java.awt.event.WindowEvent;
10  import java.awt.event.WindowListener;
11  import java.awt.geom.Rectangle2D;
12  import java.rmi.RemoteException;
13  import java.text.NumberFormat;
14  import java.util.ArrayList;
15  import java.util.HashMap;
16  import java.util.Map;
17  
18  import javax.swing.BoxLayout;
19  import javax.swing.Icon;
20  import javax.swing.JButton;
21  import javax.swing.JCheckBox;
22  import javax.swing.JFrame;
23  import javax.swing.JLabel;
24  import javax.swing.JPanel;
25  import javax.swing.JToggleButton;
26  
27  import org.opentrafficsim.base.modelproperties.PropertyException;
28  import org.opentrafficsim.core.gtu.animation.GTUColorer;
29  import org.opentrafficsim.simulationengine.SimpleAnimator;
30  import org.opentrafficsim.simulationengine.WrappableAnimation;
31  
32  import nl.javel.gisbeans.map.MapInterface;
33  import nl.tudelft.simulation.dsol.animation.Locatable;
34  import nl.tudelft.simulation.dsol.animation.D2.AnimationPanel;
35  import nl.tudelft.simulation.dsol.animation.D2.GisRenderable2D;
36  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
37  import nl.tudelft.simulation.event.Event;
38  
39  /**
40   * Animation panel with various controls.
41   * <p>
42   * Copyright (c) 2013-2017 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
43   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
44   * <p>
45   * $LastChangedDate: 2017-04-29 12:51:08 +0200 (Sat, 29 Apr 2017) $, @version $Revision: 3570 $, by $Author: averbraeck $,
46   * initial version Jun 18, 2015 <br>
47   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
48   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
49   */
50  public class OTSAnimationPanel extends OTSSimulationPanel implements ActionListener, WindowListener
51  {
52      /** */
53      private static final long serialVersionUID = 20150617L;
54  
55      /** The animation panel on tab position 0. */
56      private final AnimationPanel animationPanel;
57  
58      /** Border panel in which the animation is shown. */
59      private final JPanel borderPanel;
60  
61      /** Toggle panel with which animation features can be shown/hidden. */
62      private final JPanel togglePanel;
63  
64      /** Map of toggle names to toggle animation classes. */
65      private Map<String, Class<? extends Locatable>> toggleLocatableMap = new HashMap<>();
66  
67      /** Set of GIS layer names to toggle GIS layers . */
68      private Map<String, MapInterface> toggleGISMap = new HashMap<>();
69  
70      /** Set of GIS layer names to toggle buttons. */
71      private Map<String, JToggleButton> toggleGISButtons = new HashMap<>();
72  
73      /** The switchableGTUColorer used to color the GTUs. */
74      private GTUColorer gtuColorer = null;
75  
76      /** The ColorControlPanel that allows the user to operate the SwitchableGTUColorer. */
77      private ColorControlPanel colorControlPanel = null;
78  
79      /** The coordinates of the cursor. */
80      private final JLabel coordinateField;
81  
82      /** The animation buttons. */
83      private final ArrayList<JButton> buttons = new ArrayList<JButton>();
84  
85      /** The formatter for the world coordinates. */
86      private static final NumberFormat FORMATTER = NumberFormat.getInstance();
87  
88      /** Has the window close handler been registered? */
89      @SuppressWarnings("checkstyle:visibilitymodifier")
90      protected boolean closeHandlerRegistered = false;
91  
92      /** Indicate the window has been closed and the timer thread can stop. */
93      @SuppressWarnings("checkstyle:visibilitymodifier")
94      protected boolean windowExited = false;
95  
96      /** Initialize the formatter. */
97      static
98      {
99          FORMATTER.setMaximumFractionDigits(3);
100     }
101 
102     /**
103      * Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
104      * @param extent Rectangle2D; bottom left corner, length and width of the area (world) to animate.
105      * @param size the size to be used for the animation.
106      * @param simulator the simulator or animator of the model.
107      * @param wrappableAnimation the builder and rebuilder of the simulation, based on properties.
108      * @param gtuColorer the colorer to use for the GTUs.
109      * @throws RemoteException when notification of the animation panel fails
110      * @throws PropertyException when one of the user modified properties has the empty string as key
111      */
112     public OTSAnimationPanel(final Rectangle2D extent, final Dimension size, final SimpleAnimator simulator,
113             final WrappableAnimation wrappableAnimation, final GTUColorer gtuColorer) throws RemoteException, PropertyException
114     {
115         super(simulator, wrappableAnimation);
116 
117         // Add the animation panel as a tab.
118         this.animationPanel = new AnimationPanel(extent, size, simulator);
119         this.borderPanel = new JPanel(new BorderLayout());
120         this.borderPanel.add(this.animationPanel, BorderLayout.CENTER);
121         getTabbedPane().addTab(0, "animation", this.borderPanel);
122         getTabbedPane().setSelectedIndex(0); // Show the animation panel as the default tab
123 
124         // Include the GTU colorer control panel NORTH of the animation.
125         this.gtuColorer = gtuColorer;
126         this.colorControlPanel = new ColorControlPanel(this.gtuColorer);
127         JPanel buttonPanel = new JPanel();
128         buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
129         this.borderPanel.add(buttonPanel, BorderLayout.NORTH);
130         buttonPanel.add(this.colorControlPanel);
131 
132         // Include the TogglePanel WEST of the animation.
133         this.togglePanel = new JPanel();
134         this.togglePanel.setLayout(new BoxLayout(this.togglePanel, BoxLayout.Y_AXIS));
135         this.borderPanel.add(this.togglePanel, BorderLayout.WEST);
136 
137         // add the buttons for home, zoom all, grid, and mouse coordinates
138         buttonPanel.add(new JLabel("   "));
139         buttonPanel.add(makeButton("allButton", "/Expand.png", "ZoomAll", "Zoom whole network", true));
140         buttonPanel.add(makeButton("homeButton", "/Home.png", "Home", "Zoom to original extent", true));
141         buttonPanel.add(makeButton("gridButton", "/Grid.png", "Grid", "Toggle grid on/off", true));
142         buttonPanel.add(new JLabel("   "));
143         this.coordinateField = new JLabel("Mouse: ");
144         this.coordinateField.setMinimumSize(new Dimension(250, 10));
145         this.coordinateField.setPreferredSize(new Dimension(250, 10));
146         buttonPanel.add(this.coordinateField);
147 
148         // Tell the animation to build the list of animation objects.
149         this.animationPanel.notify(new Event(SimulatorInterface.START_REPLICATION_EVENT, simulator, null));
150 
151         // switch off the X and Y coordinates in a tooltip.
152         this.animationPanel.setShowToolTip(false);
153 
154         // run the update task for the mouse coordinate panel
155         new UpdateTimer().start();
156 
157         // make sure the thread gets killed when the window closes.
158         installWindowCloseHandler();
159     }
160 
161     /**
162      * Create a button.
163      * @param name String; name of the button
164      * @param iconPath String; path to the resource
165      * @param actionCommand String; the action command
166      * @param toolTipText String; the hint to show when the mouse hovers over the button
167      * @param enabled boolean; true if the new button must initially be enable; false if it must initially be disabled
168      * @return JButton
169      */
170     private JButton makeButton(final String name, final String iconPath, final String actionCommand, final String toolTipText,
171             final boolean enabled)
172     {
173         // JButton result = new JButton(new ImageIcon(this.getClass().getResource(iconPath)));
174         JButton result = new JButton(OTSControlPanel.loadIcon(iconPath));
175         result.setPreferredSize(new Dimension(34, 32));
176         result.setName(name);
177         result.setEnabled(enabled);
178         result.setActionCommand(actionCommand);
179         result.setToolTipText(toolTipText);
180         result.addActionListener(this);
181         this.buttons.add(result);
182         return result;
183     }
184 
185     /**
186      * Add a button for toggling an animatable class on or off. Button icons for which 'idButton' is true will be placed to the
187      * right of the previous button, which should be the corresponding button without the id. An example is an icon for
188      * showing/hiding the class 'Lane' followed by the button to show/hide the Lane ids.
189      * @param name the name of the button
190      * @param locatableClass the class for which the button holds (e.g., GTU.class)
191      * @param iconPath the path to the 24x24 icon to display
192      * @param toolTipText the tool tip text to show when hovering over the button
193      * @param initiallyVisible whether the class is initially shown or not
194      * @param idButton id button that needs to be placed next to the previous button
195      */
196     public final void addToggleAnimationButtonIcon(final String name, final Class<? extends Locatable> locatableClass,
197             final String iconPath, final String toolTipText, final boolean initiallyVisible, final boolean idButton)
198     {
199         JToggleButton button;
200         Icon icon = OTSControlPanel.loadIcon(iconPath);
201         button = new JCheckBox(icon);
202         button.setPreferredSize(new Dimension(32, 28));
203         button.setName(name);
204         button.setEnabled(true);
205         button.setSelected(initiallyVisible);
206         button.setActionCommand(name);
207         button.setToolTipText(toolTipText);
208         button.addActionListener(this);
209 
210         // place an Id button to the right of the corresponding content button
211         if (idButton && this.togglePanel.getComponentCount() > 0)
212         {
213             JPanel lastToggleBox = (JPanel) this.togglePanel.getComponent(this.togglePanel.getComponentCount() - 1);
214             lastToggleBox.add(button);
215         }
216         else
217         {
218             JPanel toggleBox = new JPanel();
219             toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
220             toggleBox.add(button);
221             this.togglePanel.add(toggleBox);
222             toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
223         }
224 
225         if (initiallyVisible)
226         {
227             this.animationPanel.showClass(locatableClass);
228         }
229         else
230         {
231             this.animationPanel.hideClass(locatableClass);
232         }
233         this.toggleLocatableMap.put(name, locatableClass);
234     }
235 
236     /**
237      * Add a button for toggling an animatable class on or off.
238      * @param name the name of the button
239      * @param locatableClass the class for which the button holds (e.g., GTU.class)
240      * @param toolTipText the tool tip text to show when hovering over the button
241      * @param initiallyVisible whether the class is initially shown or not
242      */
243     public final void addToggleAnimationButtonText(final String name, final Class<? extends Locatable> locatableClass,
244             final String toolTipText, final boolean initiallyVisible)
245     {
246         JToggleButton button;
247         button = new JCheckBox(name);
248         button.setName(name);
249         button.setEnabled(true);
250         button.setSelected(initiallyVisible);
251         button.setActionCommand(name);
252         button.setToolTipText(toolTipText);
253         button.addActionListener(this);
254 
255         JPanel toggleBox = new JPanel();
256         toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
257         toggleBox.add(button);
258         this.togglePanel.add(toggleBox);
259         toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
260 
261         if (initiallyVisible)
262         {
263             this.animationPanel.showClass(locatableClass);
264         }
265         else
266         {
267             this.animationPanel.hideClass(locatableClass);
268         }
269         this.toggleLocatableMap.put(name, locatableClass);
270     }
271 
272     /**
273      * Add a text to explain animatable classes.
274      * @param text the text to show
275      */
276     public final void addToggleText(final String text)
277     {
278         JPanel textBox = new JPanel();
279         textBox.setLayout(new BoxLayout(textBox, BoxLayout.X_AXIS));
280         textBox.add(new JLabel(text));
281         this.togglePanel.add(textBox);
282         textBox.setAlignmentX(Component.LEFT_ALIGNMENT);
283     }
284 
285     /**
286      * Add a button to toggle a GIS Layer on or off.
287      * @param layerName the name of the layer
288      * @param displayName the name to display next to the tick box
289      * @param gisMap the map
290      * @param toolTipText the tool tip text
291      */
292     public final void addToggleGISButtonText(final String layerName, final String displayName, final GisRenderable2D gisMap,
293             final String toolTipText)
294     {
295         JToggleButton button;
296         button = new JCheckBox(displayName);
297         button.setName(layerName);
298         button.setEnabled(true);
299         button.setSelected(true);
300         button.setActionCommand(layerName);
301         button.setToolTipText(toolTipText);
302         button.addActionListener(this);
303 
304         JPanel toggleBox = new JPanel();
305         toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
306         toggleBox.add(button);
307         this.togglePanel.add(toggleBox);
308         toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
309 
310         this.toggleGISMap.put(layerName, gisMap.getMap());
311         this.toggleGISButtons.put(layerName, button);
312     }
313 
314     /**
315      * Set a GIS layer to be shown in the animation to true.
316      * @param layerName the name of the GIS-layer that has to be shown.
317      */
318     public final void showGISLayer(final String layerName)
319     {
320         MapInterface gisMap = this.toggleGISMap.get(layerName);
321         if (gisMap != null)
322         {
323             try
324             {
325                 gisMap.showLayer(layerName);
326                 this.toggleGISButtons.get(layerName).setSelected(true);
327                 this.animationPanel.repaint();
328             }
329             catch (RemoteException exception)
330             {
331                 exception.printStackTrace();
332             }
333         }
334     }
335 
336     /**
337      * Set a GIS layer to be hidden in the animation to true.
338      * @param layerName the name of the GIS-layer that has to be hidden.
339      */
340     public final void hideGISLayer(final String layerName)
341     {
342         MapInterface gisMap = this.toggleGISMap.get(layerName);
343         if (gisMap != null)
344         {
345             try
346             {
347                 gisMap.hideLayer(layerName);
348                 this.toggleGISButtons.get(layerName).setSelected(false);
349                 this.animationPanel.repaint();
350             }
351             catch (RemoteException exception)
352             {
353                 exception.printStackTrace();
354             }
355         }
356     }
357 
358     /**
359      * Toggle a GIS layer to be displayed in the animation to its reverse value.
360      * @param layerName the name of the GIS-layer that has to be turned off or vice versa.
361      */
362     public final void toggleGISLayer(final String layerName)
363     {
364         MapInterface gisMap = this.toggleGISMap.get(layerName);
365         if (gisMap != null)
366         {
367             try
368             {
369                 if (gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
370                 {
371                     gisMap.hideLayer(layerName);
372                     this.toggleGISButtons.get(layerName).setSelected(false);
373                 }
374                 else
375                 {
376                     gisMap.showLayer(layerName);
377                     this.toggleGISButtons.get(layerName).setSelected(true);
378                 }
379                 this.animationPanel.repaint();
380             }
381             catch (RemoteException exception)
382             {
383                 exception.printStackTrace();
384             }
385         }
386     }
387 
388     /** {@inheritDoc} */
389     @Override
390     public final void actionPerformed(final ActionEvent actionEvent)
391     {
392         String actionCommand = actionEvent.getActionCommand();
393         try
394         {
395             if (actionCommand.equals("Home"))
396             {
397                 this.animationPanel.home();
398             }
399             if (actionCommand.equals("ZoomAll"))
400             {
401                 this.animationPanel.zoomAll();
402             }
403             if (actionCommand.equals("Grid"))
404             {
405                 this.animationPanel.showGrid(!this.animationPanel.isShowGrid());
406             }
407 
408             if (this.toggleLocatableMap.containsKey(actionCommand))
409             {
410                 Class<? extends Locatable> locatableClass = this.toggleLocatableMap.get(actionCommand);
411                 this.animationPanel.toggleClass(locatableClass);
412                 this.togglePanel.repaint();
413             }
414 
415             if (this.toggleGISMap.containsKey(actionCommand))
416             {
417                 this.toggleGISLayer(actionCommand);
418                 this.togglePanel.repaint();
419             }
420         }
421         catch (Exception exception)
422         {
423             exception.printStackTrace();
424         }
425     }
426 
427     /**
428      * Easy access to the AnimationPanel.
429      * @return AnimationPanel
430      */
431     public final AnimationPanel getAnimationPanel()
432     {
433         return this.animationPanel;
434     }
435 
436     /**
437      * Display the latest world coordinate based on the mouse position on the screen.
438      */
439     protected final void updateWorldCoordinate()
440     {
441         String worldPoint = "(x=" + FORMATTER.format(this.animationPanel.getWorldCoordinate().getX()) + " ; y="
442                 + FORMATTER.format(this.animationPanel.getWorldCoordinate().getY()) + ")";
443         this.coordinateField.setText("Mouse: " + worldPoint);
444         this.coordinateField.repaint();
445     }
446 
447     /**
448      * Access the GTUColorer of this animation ControlPanel.
449      * @return GTUColorer the colorer used. If it is a SwitchableGTUColorer, the wrapper with the list will be returned, not the
450      *         actual colorer in use.
451      */
452     public final GTUColorer getGTUColorer()
453     {
454         return this.gtuColorer;
455     }
456 
457     /**
458      * Access the ColorControlPanel of this ControlPanel. If the simulator is not a SimpleAnimator, no ColorControlPanel was
459      * constructed and this method will return null.
460      * @return ColorControlPanel
461      */
462     public final ColorControlPanel getColorControlPanel()
463     {
464         return this.colorControlPanel;
465     }
466 
467     /**
468      * Install a handler for the window closed event that stops the simulator (if it is running).
469      */
470     public final void installWindowCloseHandler()
471     {
472         if (this.closeHandlerRegistered)
473         {
474             return;
475         }
476 
477         // make sure the root frame gets disposed of when the closing X icon is pressed.
478         new DisposeOnCloseThread(this).start();
479     }
480 
481     /** Install the dispose on close when the OTSControlPanel is registered as part of a frame. */
482     protected class DisposeOnCloseThread extends Thread
483     {
484         /** The current container. */
485         private OTSAnimationPanel panel;
486 
487         /**
488          * @param panel the OTSControlpanel container.
489          */
490         public DisposeOnCloseThread(final OTSAnimationPanel panel)
491         {
492             super();
493             this.panel = panel;
494         }
495 
496         /** {@inheritDoc} */
497         @Override
498         public final void run()
499         {
500             Container root = this.panel;
501             while (!(root instanceof JFrame))
502             {
503                 try
504                 {
505                     Thread.sleep(10);
506                 }
507                 catch (InterruptedException exception)
508                 {
509                     // nothing to do
510                 }
511 
512                 // Search towards the root of the Swing components until we find a JFrame
513                 root = this.panel;
514                 while (null != root.getParent() && !(root instanceof JFrame))
515                 {
516                     root = root.getParent();
517                 }
518             }
519             JFrame frame = (JFrame) root;
520             frame.addWindowListener(this.panel);
521             this.panel.closeHandlerRegistered = true;
522         }
523 
524         /** {@inheritDoc} */
525         @Override
526         public final String toString()
527         {
528             return "DisposeOnCloseThread of OTSAnimationPanel [panel=" + this.panel + "]";
529         }
530     }
531 
532     /** {@inheritDoc} */
533     @Override
534     public void windowOpened(final WindowEvent e)
535     {
536         // No action
537     }
538 
539     /** {@inheritDoc} */
540     @Override
541     public final void windowClosing(final WindowEvent e)
542     {
543         // No action
544     }
545 
546     /** {@inheritDoc} */
547     @Override
548     public final void windowClosed(final WindowEvent e)
549     {
550         this.windowExited = true;
551     }
552 
553     /** {@inheritDoc} */
554     @Override
555     public final void windowIconified(final WindowEvent e)
556     {
557         // No action
558     }
559 
560     /** {@inheritDoc} */
561     @Override
562     public final void windowDeiconified(final WindowEvent e)
563     {
564         // No action
565     }
566 
567     /** {@inheritDoc} */
568     @Override
569     public final void windowActivated(final WindowEvent e)
570     {
571         // No action
572     }
573 
574     /** {@inheritDoc} */
575     @Override
576     public final void windowDeactivated(final WindowEvent e)
577     {
578         // No action
579     }
580 
581     /**
582      * UpdateTimer class to update the coordinate on the screen.
583      */
584     protected class UpdateTimer extends Thread
585     {
586         /** {@inheritDoc} */
587         @Override
588         public final void run()
589         {
590             while (!OTSAnimationPanel.this.windowExited)
591             {
592                 if (OTSAnimationPanel.this.isShowing())
593                 {
594                     OTSAnimationPanel.this.updateWorldCoordinate();
595                 }
596                 try
597                 {
598                     Thread.sleep(50); // 20 times per second
599                 }
600                 catch (InterruptedException exception)
601                 {
602                     // do nothing
603                 }
604             }
605         }
606 
607         /** {@inheritDoc} */
608         @Override
609         public final String toString()
610         {
611             return "UpdateTimer thread for OTSAnimationPanel";
612         }
613 
614     }
615 }