View Javadoc
1   package org.opentrafficsim.simulationengine;
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.Font;
8   import java.awt.Frame;
9   import java.awt.Rectangle;
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.WindowAdapter;
15  import java.awt.event.WindowEvent;
16  import java.awt.geom.Rectangle2D;
17  import java.io.File;
18  import java.io.FileReader;
19  import java.io.FileWriter;
20  import java.io.IOException;
21  import java.io.Serializable;
22  import java.rmi.RemoteException;
23  import java.util.ArrayList;
24  import java.util.Dictionary;
25  import java.util.Enumeration;
26  import java.util.List;
27  import java.util.Properties;
28  
29  import javax.naming.NamingException;
30  import javax.swing.BoxLayout;
31  import javax.swing.ButtonGroup;
32  import javax.swing.JCheckBoxMenuItem;
33  import javax.swing.JComponent;
34  import javax.swing.JFrame;
35  import javax.swing.JLabel;
36  import javax.swing.JMenu;
37  import javax.swing.JMenuItem;
38  import javax.swing.JPanel;
39  import javax.swing.JPopupMenu;
40  import javax.swing.JSlider;
41  import javax.swing.MenuElement;
42  import javax.swing.MenuSelectionManager;
43  import javax.swing.SwingUtilities;
44  import javax.swing.UIManager;
45  import javax.swing.WindowConstants;
46  import javax.swing.border.EmptyBorder;
47  import javax.swing.event.ChangeEvent;
48  import javax.swing.event.ChangeListener;
49  import javax.vecmath.Point3d;
50  
51  import org.djunits.value.vdouble.scalar.Duration;
52  import org.djunits.value.vdouble.scalar.Time;
53  import org.opentrafficsim.base.modelproperties.Property;
54  import org.opentrafficsim.base.modelproperties.PropertyException;
55  import org.opentrafficsim.core.dsol.OTSModelInterface;
56  import org.opentrafficsim.core.gtu.Try;
57  import org.opentrafficsim.core.gtu.animation.DefaultSwitchableGTUColorer;
58  import org.opentrafficsim.core.gtu.animation.GTUColorer;
59  import org.opentrafficsim.core.network.Link;
60  import org.opentrafficsim.core.network.NetworkException;
61  import org.opentrafficsim.core.network.OTSNetwork;
62  import org.opentrafficsim.gui.Appearance;
63  import org.opentrafficsim.gui.AppearanceControl;
64  import org.opentrafficsim.gui.OTSAnimationPanel;
65  import org.opentrafficsim.gui.SimulatorFrame;
66  
67  import nl.tudelft.simulation.dsol.SimRuntimeException;
68  import nl.tudelft.simulation.dsol.animation.Locatable;
69  import nl.tudelft.simulation.dsol.animation.D2.AnimationPanel;
70  import nl.tudelft.simulation.dsol.animation.D2.GisRenderable2D;
71  import nl.tudelft.simulation.language.d3.BoundingBox;
72  import nl.tudelft.simulation.language.d3.DirectedPoint;
73  
74  /**
75   * <p>
76   * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
77   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
78   * <p>
79   * $LastChangedDate: 2018-09-19 13:55:45 +0200 (Wed, 19 Sep 2018) $, @version $Revision: 4006 $, by $Author: averbraeck $,
80   * initial version Jun 18, 2015 <br>
81   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
82   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
83   */
84  public abstract class AbstractWrappableAnimation implements WrappableAnimation, Serializable
85  {
86      /** */
87      private static final long serialVersionUID = 20150000L;
88  
89      /** The properties exhibited by this simulation. */
90      @SuppressWarnings("checkstyle:visibilitymodifier")
91      protected List<Property<?>> properties = new ArrayList<>();
92  
93      /** The properties after (possible) editing by the user. */
94      @SuppressWarnings("checkstyle:visibilitymodifier")
95      protected List<Property<?>> savedUserModifiedProperties;
96  
97      /** Properties for the frame appearance (not simulation related). */
98      protected Properties frameProperties;
99  
100     /** Use EXIT_ON_CLOSE when true, DISPOSE_ON_CLOSE when false on closing of the window. */
101     @SuppressWarnings("checkstyle:visibilitymodifier")
102     protected boolean exitOnClose;
103 
104     /** The tabbed panel so other tabs can be added by the classes that extend this class. */
105     @SuppressWarnings("checkstyle:visibilitymodifier")
106     protected OTSAnimationPanel panel;
107 
108     /** Save the startTime for restarting the simulation. */
109     private Time savedStartTime;
110 
111     /** Save the startTime for restarting the simulation. */
112     private Duration savedWarmupPeriod;
113 
114     /** Save the runLength for restarting the simulation. */
115     private Duration savedRunLength;
116 
117     /** The model. */
118     private OTSModelInterface model;
119 
120     /** Override the replication number by this value if non-null. */
121     private Integer replication = null;
122 
123     /** Current appearance. */
124     private Appearance appearance = Appearance.GRAY;
125 
126     /** Colorer. */
127     private GTUColorer colorer = new DefaultSwitchableGTUColorer();
128 
129     /**
130      * Build the animator.
131      * @param startTime Time; the start time
132      * @param warmupPeriod Duration; the warm up period
133      * @param runLength Duration; the duration of the simulation / animation
134      * @param otsModel OTSModelInterface; the simulation model
135      * @return SimpleAnimator; a newly constructed animator
136      * @throws SimRuntimeException on ???
137      * @throws NamingException when context for the animation cannot be created
138      * @throws PropertyException when one of the user modified properties has the empty string as key
139      */
140     @SuppressWarnings("checkstyle:designforextension")
141     protected SimpleAnimator buildSimpleAnimator(final Time startTime, final Duration warmupPeriod, final Duration runLength,
142             final OTSModelInterface otsModel) throws SimRuntimeException, NamingException, PropertyException
143     {
144         return new SimpleAnimator(startTime, warmupPeriod, runLength, otsModel);
145     }
146 
147     /**
148      * Build the animator with the specified replication number.
149      * @param startTime Time; the start time
150      * @param warmupPeriod Duration; the warm up period
151      * @param runLength Duration; the duration of the simulation / animation
152      * @param otsModel OTSModelInterface; the simulation model
153      * @param replicationNumber int; the replication number
154      * @return SimpleAnimator; a newly constructed animator
155      * @throws SimRuntimeException on ???
156      * @throws NamingException when context for the animation cannot be created
157      * @throws PropertyException when one of the user modified properties has the empty string as key
158      */
159     @SuppressWarnings("checkstyle:designforextension")
160     protected SimpleAnimator buildSimpleAnimator(final Time startTime, final Duration warmupPeriod, final Duration runLength,
161             final OTSModelInterface otsModel, final int replicationNumber)
162             throws SimRuntimeException, NamingException, PropertyException
163     {
164         return new SimpleAnimator(startTime, warmupPeriod, runLength, otsModel, replicationNumber);
165     }
166 
167     /** {@inheritDoc} */
168     @Override
169     @SuppressWarnings("checkstyle:designforextension")
170     public SimpleAnimator buildAnimator(final Time startTime, final Duration warmupPeriod, final Duration runLength,
171             final List<Property<?>> userModifiedProperties, final Rectangle rect, final boolean eoc)
172             throws SimRuntimeException, NamingException, OTSSimulationException, PropertyException
173     {
174 
175         this.savedUserModifiedProperties = userModifiedProperties;
176         this.exitOnClose = eoc;
177         this.savedStartTime = startTime;
178         this.savedWarmupPeriod = warmupPeriod;
179         this.savedRunLength = runLength;
180         this.model = makeModel();
181         if (null == this.model)
182         {
183             return null; // Happens when the user cancels a file open dialog
184         }
185 
186         // Animator
187         final SimpleAnimator simulator =
188                 null == this.replication ? buildSimpleAnimator(startTime, warmupPeriod, runLength, this.model)
189                         : buildSimpleAnimator(startTime, warmupPeriod, runLength, this.model, this.replication);
190         try
191         {
192             this.panel = new OTSAnimationPanel(makeAnimationRectangle(), new Dimension(1024, 768), simulator, this,
193                     getColorer(), this.model.getNetwork());
194         }
195         catch (RemoteException exception)
196         {
197             throw new SimRuntimeException(exception);
198         }
199 
200         // Case specific GUI elements
201         addAnimationToggles();
202         addTabs(simulator);
203 
204         // Frame
205         SimulatorFrame frame = new SimulatorFrame(shortName(), this.panel);
206         if (rect != null)
207         {
208             frame.setBounds(rect);
209         }
210         else
211         {
212             frame.setExtendedState(Frame.MAXIMIZED_BOTH);
213         }
214         frame.setDefaultCloseOperation(this.exitOnClose ? WindowConstants.EXIT_ON_CLOSE : WindowConstants.DISPOSE_ON_CLOSE);
215 
216         ////////////////////////////////////////
217         ///// Look and Feel and Appearance /////
218         ////////////////////////////////////////
219 
220         // Listener to write frame properties on frame close
221         String sep = System.getProperty("file.separator");
222         String propertiesFile = System.getProperty("user.home") + sep + "OTS" + sep + "properties.ini";
223         frame.addWindowListener(new WindowAdapter()
224         {
225             /** {@inheritDoce} */
226             @Override
227             public void windowClosing(final WindowEvent windowEvent)
228             {
229                 try
230                 {
231                     File f = new File(propertiesFile);
232                     f.getParentFile().mkdirs();
233                     FileWriter writer = new FileWriter(f);
234                     AbstractWrappableAnimation.this.frameProperties.store(writer, "OTS user settings");
235                 }
236                 catch (@SuppressWarnings("unused") IOException exception)
237                 {
238                     System.err.println("Could not store properties at " + propertiesFile + ".");
239                 }
240             }
241         });
242 
243         // Set default frame properties and load properties from file (if any)
244         Properties defaults = new Properties();
245         defaults.setProperty("Appearance", "GRAY");
246         defaults.setProperty("LookAndFeel", "javax.swing.plaf.metal.MetalLookAndFeel");
247         this.frameProperties = new Properties(defaults);
248         try
249         {
250             FileReader reader = new FileReader(propertiesFile);
251             this.frameProperties.load(reader);
252         }
253         catch (@SuppressWarnings("unused") IOException ioe)
254         {
255             // ok, use defaults
256         }
257         this.appearance = Appearance.valueOf(this.frameProperties.getProperty("Appearance").toUpperCase());
258 
259         /** Menu class to only accept the font of an Appearance */
260         class AppearanceControlMenu extends JMenu implements AppearanceControl
261         {
262             /** */
263             private static final long serialVersionUID = 20180206L;
264 
265             /**
266              * Constructor.
267              * @param string String; string
268              */
269             AppearanceControlMenu(final String string)
270             {
271                 super(string);
272             }
273 
274             /** {@inheritDoc} */
275             @Override
276             public boolean isFont()
277             {
278                 return true;
279             }
280 
281             /** {@inheritDoc} */
282             @Override
283             public String toString()
284             {
285                 return "AppearanceControlMenu []";
286             }
287         }
288 
289         // Look and feel menu
290         JMenu laf = new AppearanceControlMenu("Look and feel");
291         laf.addMouseListener(new SubMenuShower(laf));
292         ButtonGroup lafGroup = new ButtonGroup();
293         lafGroup.add(addLookAndFeel(frame, laf, "javax.swing.plaf.metal.MetalLookAndFeel", "Metal"));
294         lafGroup.add(addLookAndFeel(frame, laf, "com.sun.java.swing.plaf.motif.MotifLookAndFeel", "Motif"));
295         lafGroup.add(addLookAndFeel(frame, laf, "javax.swing.plaf.nimbus.NimbusLookAndFeel", "Nimbus"));
296         lafGroup.add(addLookAndFeel(frame, laf, "com.sun.java.swing.plaf.windows.WindowsLookAndFeel", "Windows"));
297         lafGroup.add(
298                 addLookAndFeel(frame, laf, "com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel", "Windows classic"));
299         lafGroup.add(addLookAndFeel(frame, laf, UIManager.getSystemLookAndFeelClassName(), "System default"));
300 
301         // Appearance menu
302         JMenu app = new AppearanceControlMenu("Appearance");
303         app.addMouseListener(new SubMenuShower(app));
304         ButtonGroup appGroup = new ButtonGroup();
305         for (Appearance appearanceValue : Appearance.values())
306         {
307             appGroup.add(addAppearance(app, appearanceValue));
308         }
309 
310         /** PopupMenu class to only accept the font of an Appearance */
311         class AppearanceControlPopupMenu extends JPopupMenu implements AppearanceControl
312         {
313             /** */
314             private static final long serialVersionUID = 20180206L;
315 
316             /** {@inheritDoc} */
317             @Override
318             public boolean isFont()
319             {
320                 return true;
321             }
322 
323             /** {@inheritDoc} */
324             @Override
325             public String toString()
326             {
327                 return "AppearanceControlPopupMenu []";
328             }
329         }
330 
331         // Popup menu to change the Look and Feel or Appearance
332         JPopupMenu popMenu = new AppearanceControlPopupMenu();
333         popMenu.add(laf);
334         popMenu.add(app);
335         this.getPanel().getOtsControlPanel().setComponentPopupMenu(popMenu);
336 
337         // Set the Look and Feel and Appearance as by frame properties
338         setAppearance(getAppearance()); // color elements that were just added
339         Try.execute(() -> UIManager.setLookAndFeel(this.frameProperties.getProperty("LookAndFeel")),
340                 "Could not set look-and-feel %s", laf);
341         SwingUtilities.invokeLater(() -> SwingUtilities.updateComponentTreeUI(frame));
342 
343         // demo
344         this.demoPanel = null;
345         setupDemo(this, this.model.getNetwork());
346 
347         return simulator;
348     }
349 
350     /**
351      * Adds a look-and-feel item.
352      * @param frame JFrame; frame to set the look-and-feel to
353      * @param group JMenu; menu to add item to
354      * @param laf String; full path of LookAndFeel
355      * @param name String; name on menu item
356      * @return JMenuItem; menu item
357      */
358     private JCheckBoxMenuItem addLookAndFeel(final JFrame frame, final JMenu group, final String laf, final String name)
359     {
360         boolean checked = this.frameProperties.getProperty("LookAndFeel").equals(laf);
361         JCheckBoxMenuItem check = new StayOpenCheckBoxMenuItem(name, checked);
362         check.addMouseListener(new MouseAdapter()
363         {
364             /** {@inheritDoc} */
365             @Override
366             public void mouseClicked(final MouseEvent e)
367             {
368                 Try.execute(() -> UIManager.setLookAndFeel(laf), "Could not set look-and-feel %s", laf);
369                 SwingUtilities.updateComponentTreeUI(frame);
370                 AbstractWrappableAnimation.this.frameProperties.setProperty("LookAndFeel", laf);
371             }
372         });
373         group.add(check);
374         return check;
375     }
376 
377     /**
378      * Adds an appearance to the menu.
379      * @param group JMenu; menu to add item to
380      * @param appear Appearance; appearance this item selects
381      * @return JMenuItem; menu item
382      */
383     private JMenuItem addAppearance(final JMenu group, final Appearance appear)
384     {
385         JCheckBoxMenuItem check = new StayOpenCheckBoxMenuItem(appear.getName(), appear.equals(getAppearance()));
386         check.addMouseListener(new MouseAdapter()
387         {
388             /** {@inheritDoc} */
389             @Override
390             public void mouseClicked(final MouseEvent e)
391             {
392                 setAppearance(appear);
393             }
394         });
395         return group.add(check);
396     }
397 
398     /**
399      * Sets an appearance.
400      * @param appearance Appearance; appearance
401      */
402     public void setAppearance(final Appearance appearance)
403     {
404         this.appearance = appearance;
405         setAppearance(this.panel.getParent(), appearance);
406         this.frameProperties.setProperty("Appearance", appearance.toString());
407     }
408 
409     /**
410      * Sets an appearance recursively on components.
411      * @param c Component; visual component
412      * @param appear Appearance; look and feel
413      */
414     private void setAppearance(final Component c, final Appearance appear)
415     {
416         if (c instanceof AppearanceControl)
417         {
418             AppearanceControl ac = (AppearanceControl) c;
419             if (ac.isBackground())
420             {
421                 c.setBackground(appear.getBackground());
422             }
423             if (ac.isForeground())
424             {
425                 c.setForeground(appear.getForeground());
426             }
427             if (ac.isFont())
428             {
429                 changeFont(c, appear.getFont());
430             }
431         }
432         else if (c instanceof AnimationPanel)
433         {
434             // animation backdrop
435             c.setBackground(appear.getBackdrop()); // not background
436             c.setForeground(appear.getForeground());
437             changeFont(c, appear.getFont());
438         }
439         else
440         {
441             // default
442             c.setBackground(appear.getBackground());
443             c.setForeground(appear.getForeground());
444             changeFont(c, appear.getFont());
445         }
446         if (c instanceof JSlider)
447         {
448             // labels of the slider
449             Dictionary<?, ?> dictionary = ((JSlider) c).getLabelTable();
450             Enumeration<?> keys = dictionary.keys();
451             while (keys.hasMoreElements())
452             {
453                 JLabel label = (JLabel) dictionary.get(keys.nextElement());
454                 label.setForeground(appear.getForeground());
455                 label.setBackground(appear.getBackground());
456             }
457         }
458         // children
459         if (c instanceof JComponent)
460         {
461             for (Component child : ((JComponent) c).getComponents())
462             {
463                 setAppearance(child, appear);
464             }
465         }
466     }
467 
468     /**
469      * Change font on component.
470      * @param c Component; component
471      * @param font String; font name
472      */
473     private void changeFont(final Component c, final String font)
474     {
475         Font prev = c.getFont();
476         c.setFont(new Font(font, prev.getStyle(), prev.getSize()));
477     }
478 
479     /**
480      * Returns the appearance.
481      * @return Appearance; appearance
482      */
483     public Appearance getAppearance()
484     {
485         return this.appearance;
486     }
487 
488     /**
489      * Overridable method to return GTU colorer.
490      * @return GTU colorer
491      */
492     @SuppressWarnings("checkstyle:designforextension")
493     public GTUColorer getColorer()
494     {
495         return this.colorer;
496     }
497 
498     /**
499      * Make additional tabs in the main simulation window.
500      * @param simulator SimpleSimulatorInterface; the simulator
501      * @throws OTSSimulationException in case the chart, axes or legend cannot be generated
502      * @throws PropertyException when one of the user modified properties has the empty string as key
503      */
504     protected void addTabs(final SimpleSimulatorInterface simulator) throws OTSSimulationException, PropertyException
505     {
506         // Override this method to add custom tabs
507     }
508 
509     /**
510      * Placeholder method to place animation buttons or to show/hide classes on the animation.
511      */
512     @SuppressWarnings("checkstyle:designforextension")
513     protected void addAnimationToggles()
514     {
515         // overridable placeholder to place animation buttons or to show/hide classes on the animation.
516     }
517 
518     /**
519      * Method that is called when the animation has been created, to add components for a demo.
520      * @param animation AbstractWrappableAnimation; animation
521      * @param net OTSNetwork; network
522      */
523     protected void setupDemo(final AbstractWrappableAnimation animation, final OTSNetwork net)
524     {
525         // overridable placeholderv
526     }
527 
528     /**
529      * Add a button for toggling an animatable class on or off. Button icons for which 'idButton' is true will be placed to the
530      * right of the previous button, which should be the corresponding button without the id. An example is an icon for
531      * showing/hiding the class 'Lane' followed by the button to show/hide the Lane ids.
532      * @param name the name of the button
533      * @param locatableClass the class for which the button holds (e.g., GTU.class)
534      * @param iconPath the path to the 24x24 icon to display
535      * @param toolTipText the tool tip text to show when hovering over the button
536      * @param initiallyVisible whether the class is initially shown or not
537      * @param idButton id button that needs to be placed next to the previous button
538      */
539     public final void addToggleAnimationButtonIcon(final String name, final Class<? extends Locatable> locatableClass,
540             final String iconPath, final String toolTipText, final boolean initiallyVisible, final boolean idButton)
541     {
542         this.panel.addToggleAnimationButtonIcon(name, locatableClass, iconPath, toolTipText, initiallyVisible, idButton);
543     }
544 
545     /**
546      * Add a button for toggling an animatable class on or off.
547      * @param name the name of the button
548      * @param locatableClass the class for which the button holds (e.g., GTU.class)
549      * @param toolTipText the tool tip text to show when hovering over the button
550      * @param initiallyVisible whether the class is initially shown or not
551      */
552     public final void addToggleAnimationButtonText(final String name, final Class<? extends Locatable> locatableClass,
553             final String toolTipText, final boolean initiallyVisible)
554     {
555         this.panel.addToggleAnimationButtonText(name, locatableClass, toolTipText, initiallyVisible);
556     }
557 
558     /**
559      * Set a class to be shown in the animation to true.
560      * @param locatableClass the class for which the animation has to be shown.
561      */
562     public final void showAnimationClass(final Class<? extends Locatable> locatableClass)
563     {
564         this.panel.getAnimationPanel().showClass(locatableClass);
565         this.panel.updateAnimationClassCheckBox(locatableClass);
566     }
567 
568     /**
569      * Set a class to be hidden in the animation to true.
570      * @param locatableClass the class for which the animation has to be hidden.
571      */
572     public final void hideAnimationClass(final Class<? extends Locatable> locatableClass)
573     {
574         this.panel.getAnimationPanel().hideClass(locatableClass);
575         this.panel.updateAnimationClassCheckBox(locatableClass);
576     }
577 
578     /**
579      * Toggle a class to be displayed in the animation to its reverse value.
580      * @param locatableClass the class for which a visible animation has to be turned off or vice versa.
581      */
582     public final void toggleAnimationClass(final Class<? extends Locatable> locatableClass)
583     {
584         this.panel.getAnimationPanel().toggleClass(locatableClass);
585         this.panel.updateAnimationClassCheckBox(locatableClass);
586     }
587 
588     /**
589      * Add a button for toggling a GIS class on or off.
590      * @param header the name of the group of layers
591      * @param gisMap the GIS map for which the toggles have to be added
592      * @param toolTipText the tool tip text to show when hovering over the button
593      */
594     public final void addToggleGISButtonText(final String header, final GisRenderable2D gisMap, final String toolTipText)
595     {
596         this.panel.addToggleText(" ");
597         this.panel.addToggleText(header);
598         try
599         {
600             for (String layerName : gisMap.getMap().getLayerMap().keySet())
601             {
602                 this.panel.addToggleGISButtonText(layerName, layerName, gisMap, toolTipText);
603             }
604         }
605         catch (RemoteException exception)
606         {
607             exception.printStackTrace();
608         }
609     }
610 
611     /**
612      * Set a GIS layer to be shown in the animation to true.
613      * @param layerName the name of the GIS-layer that has to be shown.
614      */
615     public final void showGISLayer(final String layerName)
616     {
617         this.panel.showGISLayer(layerName);
618     }
619 
620     /**
621      * Set a GIS layer to be hidden in the animation to true.
622      * @param layerName the name of the GIS-layer that has to be hidden.
623      */
624     public final void hideGISLayer(final String layerName)
625     {
626         this.panel.hideGISLayer(layerName);
627     }
628 
629     /**
630      * Toggle a GIS layer to be displayed in the animation to its reverse value.
631      * @param layerName the name of the GIS-layer that has to be turned off or vice versa.
632      */
633     public final void toggleGISLayer(final String layerName)
634     {
635         this.panel.toggleGISLayer(layerName);
636     }
637 
638     /**
639      * @return the demo model. Don't forget to keep a local copy.
640      * @throws OTSSimulationException in case the construction of the model fails
641      */
642     protected abstract OTSModelInterface makeModel() throws OTSSimulationException;
643 
644     /**
645      * Return the initial 'home' extent for the animation. The 'Home' button returns to this extent. Override this method when a
646      * smaller or larger part of the infra should be shown. In the default setting, all currently visible objects are shown.
647      * @return the initial and 'home' rectangle for the animation.
648      */
649     @SuppressWarnings("checkstyle:designforextension")
650     protected Rectangle2D makeAnimationRectangle()
651     {
652         double minX = Double.MAX_VALUE;
653         double maxX = -Double.MAX_VALUE;
654         double minY = Double.MAX_VALUE;
655         double maxY = -Double.MAX_VALUE;
656         Point3d p3dL = new Point3d();
657         Point3d p3dU = new Point3d();
658         try
659         {
660             for (Link link : this.model.getNetwork().getLinkMap().values())
661             {
662                 DirectedPoint l = link.getLocation();
663                 BoundingBox b = new BoundingBox(link.getBounds());
664                 b.getLower(p3dL);
665                 b.getUpper(p3dU);
666                 minX = Math.min(minX, l.x + Math.min(p3dL.x, p3dU.x));
667                 minY = Math.min(minY, l.y + Math.min(p3dL.y, p3dU.y));
668                 maxX = Math.max(maxX, l.x + Math.max(p3dL.x, p3dU.x));
669                 maxY = Math.max(maxY, l.y + Math.max(p3dL.y, p3dU.y));
670             }
671         }
672         catch (@SuppressWarnings("unused") Exception e)
673         {
674             // ignore
675         }
676         double relativeMargin = 0.05;
677         double xMargin = relativeMargin * (maxX - minX);
678         double yMargin = relativeMargin * (maxY - minY);
679         minX = minX - xMargin;
680         minY = minY - yMargin;
681         maxX = maxX + xMargin;
682         maxY = maxY + yMargin;
683 
684         return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
685     }
686 
687     /** {@inheritDoc} */
688     @Override
689     public final ArrayList<Property<?>> getProperties()
690     {
691         return new ArrayList<>(this.properties);
692     }
693 
694     /** {@inheritDoc} */
695     @Override
696     public final SimpleSimulatorInterface rebuildSimulator(final Rectangle rect)
697             throws SimRuntimeException, NetworkException, NamingException, OTSSimulationException, PropertyException
698     {
699         return buildAnimator(this.savedStartTime, this.savedWarmupPeriod, this.savedRunLength, this.savedUserModifiedProperties,
700                 rect, this.exitOnClose);
701     }
702 
703     /** {@inheritDoc} */
704     @Override
705     public final List<Property<?>> getUserModifiedProperties()
706     {
707         return this.savedUserModifiedProperties;
708     }
709 
710     /** {@inheritDoc} */
711     @Override
712     @SuppressWarnings("checkstyle:designforextension")
713     public void stopTimersThreads()
714     {
715         if (this.panel != null && this.panel.getStatusBar() != null)
716         {
717             this.panel.getStatusBar().cancelTimer();
718         }
719         this.panel = null;
720     }
721 
722     /**
723      * @return panel
724      */
725     public final OTSAnimationPanel getPanel()
726     {
727         return this.panel;
728     }
729 
730     /**
731      * Add a tab to the simulation window. This method can not be called from constructModel because the TabbedPane has not yet
732      * been constructed at that time; recommended: override addTabs and call this method from there.
733      * @param index int; index of the new tab; use <code>getTabCount()</code> to obtain the valid range
734      * @param caption String; caption of the new tab
735      * @param container Container; content of the new tab
736      */
737     public final void addTab(final int index, final String caption, final Container container)
738     {
739         this.panel.getTabbedPane().addTab(index, caption, container);
740     }
741 
742     /**
743      * Report the current number of tabs in the simulation window. This method can not be called from constructModel because the
744      * TabbedPane has not yet been constructed at that time; recommended: override addTabs and call this method from there.
745      * @return int; the number of tabs in the simulation window
746      */
747     public final int getTabCount()
748     {
749         return this.panel.getTabbedPane().getTabCount();
750     }
751 
752     /** {@inheritDoc} */
753     @Override
754     public final void setNextReplication(final Integer nextReplication)
755     {
756         this.replication = nextReplication;
757     }
758 
759     // Demo panel
760 
761     /** Panel for on-screen demo settings. */
762     private JPanel demoPanel;
763 
764     /**
765      * Return a panel for on-screen demo controls. The panel is create on first call.
766      * @return JPanel; panel
767      */
768     public JPanel getDemoPanel()
769     {
770         if (this.demoPanel == null)
771         {
772             this.demoPanel = new JPanel();
773             this.demoPanel.setLayout(new BoxLayout(this.demoPanel, BoxLayout.Y_AXIS));
774             this.demoPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
775             this.demoPanel.setPreferredSize(new Dimension(300, 300));
776             this.getPanel().getAnimationPanel().getParent().add(this.demoPanel, BorderLayout.EAST);
777             this.demoPanel.addContainerListener(new ContainerListener()
778             {
779                 @Override
780                 public void componentAdded(final ContainerEvent e)
781                 {
782                     try
783                     {
784                         setAppearance(getAppearance());
785                     }
786                     catch (@SuppressWarnings("unused") NullPointerException exception)
787                     {
788                         //
789                     }
790                 }
791 
792                 @Override
793                 public void componentRemoved(final ContainerEvent e)
794                 {
795                     //
796                 }
797             });
798         }
799         return this.demoPanel;
800     }
801 
802     /**
803      * Mouse listener which shows the submenu when the mouse enters the button.
804      * <p>
805      * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
806      * <br>
807      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
808      * <p>
809      * @version $Revision: 4006 $, $LastChangedDate: 2018-09-19 13:55:45 +0200 (Wed, 19 Sep 2018) $, by $Author: averbraeck $,
810      *          initial version 6 feb. 2018 <br>
811      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
812      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
813      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
814      */
815     private class SubMenuShower extends MouseAdapter
816     {
817         /** The menu. */
818         private JMenu menu;
819 
820         /**
821          * Constructor.
822          * @param menu JMenu; menu
823          */
824         SubMenuShower(final JMenu menu)
825         {
826             this.menu = menu;
827         }
828 
829         /** {@inheritDoc} */
830         @Override
831         public void mouseEntered(final MouseEvent e)
832         {
833             MenuSelectionManager.defaultManager().setSelectedPath(
834                     new MenuElement[] { (MenuElement) this.menu.getParent(), this.menu, this.menu.getPopupMenu() });
835         }
836 
837         /** {@inheritDoc} */
838         @Override
839         public String toString()
840         {
841             return "SubMenuShower [menu=" + this.menu + "]";
842         }
843     }
844 
845     /**
846      * Check box item that keeps the popup menu visible after clicking, so the user can click and try some options.
847      * <p>
848      * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
849      * <br>
850      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
851      * <p>
852      * @version $Revision: 4006 $, $LastChangedDate: 2018-09-19 13:55:45 +0200 (Wed, 19 Sep 2018) $, by $Author: averbraeck $,
853      *          initial version 6 feb. 2018 <br>
854      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
855      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
856      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
857      */
858     private static class StayOpenCheckBoxMenuItem extends JCheckBoxMenuItem implements AppearanceControl
859     {
860         /** */
861         private static final long serialVersionUID = 20180206L;
862 
863         /** Stored selection path. */
864         private static MenuElement[] path;
865 
866         {
867             getModel().addChangeListener(new ChangeListener()
868             {
869 
870                 @Override
871                 public void stateChanged(final ChangeEvent e)
872                 {
873                     if (getModel().isArmed() && isShowing())
874                     {
875                         setPath(MenuSelectionManager.defaultManager().getSelectedPath());
876                     }
877                 }
878             });
879         }
880 
881         /**
882          * Sets the path.
883          * @param path MenuElement[]; path
884          */
885         public static void setPath(final MenuElement[] path)
886         {
887             StayOpenCheckBoxMenuItem.path = path;
888         }
889 
890         /**
891          * Constructor.
892          * @param text String; menu item text
893          * @param selected boolean; if the item is selected
894          */
895         StayOpenCheckBoxMenuItem(final String text, final boolean selected)
896         {
897             super(text, selected);
898         }
899 
900         /** {@inheritDoc} */
901         @Override
902         public void doClick(final int pressTime)
903         {
904             super.doClick(pressTime);
905             for (MenuElement element : path)
906             {
907                 if (element instanceof JComponent)
908                 {
909                     ((JComponent) element).setVisible(true);
910                 }
911             }
912             JMenu menu = (JMenu) path[path.length - 3];
913             MenuSelectionManager.defaultManager()
914                     .setSelectedPath(new MenuElement[] { (MenuElement) menu.getParent(), menu, menu.getPopupMenu() });
915         }
916 
917         /** {@inheritDoc} */
918         @Override
919         public boolean isFont()
920         {
921             return true;
922         }
923 
924         /** {@inheritDoc} */
925         @Override
926         public String toString()
927         {
928             return "StayOpenCheckBoxMenuItem []";
929         }
930     }
931 
932 }