View Javadoc
1   package org.opentrafficsim.swing.gui;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Color;
5   import java.awt.Component;
6   import java.awt.Container;
7   import java.awt.Dimension;
8   import java.awt.Font;
9   import java.awt.Graphics;
10  import java.awt.Insets;
11  import java.awt.event.ActionEvent;
12  import java.awt.event.ActionListener;
13  import java.awt.event.MouseAdapter;
14  import java.awt.event.MouseEvent;
15  import java.awt.event.MouseListener;
16  import java.awt.geom.Point2D;
17  import java.awt.geom.Rectangle2D;
18  import java.awt.geom.RectangularShape;
19  import java.rmi.RemoteException;
20  import java.util.ArrayList;
21  import java.util.LinkedHashMap;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Optional;
25  import java.util.OptionalInt;
26  import java.util.Properties;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import javax.swing.AbstractAction;
31  import javax.swing.Box;
32  import javax.swing.BoxLayout;
33  import javax.swing.Icon;
34  import javax.swing.JButton;
35  import javax.swing.JCheckBox;
36  import javax.swing.JLabel;
37  import javax.swing.JPanel;
38  import javax.swing.JToggleButton;
39  import javax.swing.SwingConstants;
40  import javax.swing.UIManager;
41  import javax.swing.border.EmptyBorder;
42  import javax.swing.event.ChangeEvent;
43  import javax.swing.event.ChangeListener;
44  
45  import org.djutils.draw.bounds.Bounds2d;
46  import org.djutils.draw.point.Point;
47  import org.djutils.draw.point.Point2d;
48  import org.djutils.event.Event;
49  import org.djutils.event.EventListener;
50  import org.djutils.event.TimedEvent;
51  import org.djutils.exceptions.Throw;
52  import org.opentrafficsim.animation.ColorInterpolator;
53  import org.opentrafficsim.animation.data.AnimationGtuData;
54  import org.opentrafficsim.animation.data.gtu.GtuColorerManager;
55  import org.opentrafficsim.animation.data.util.IconUtil;
56  import org.opentrafficsim.base.logger.Logger;
57  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
58  import org.opentrafficsim.core.gtu.Gtu;
59  import org.opentrafficsim.core.network.Network;
60  import org.opentrafficsim.road.gtu.LaneBasedGtu;
61  
62  import nl.tudelft.simulation.dsol.animation.Locatable;
63  import nl.tudelft.simulation.dsol.animation.d2.Renderable2dInterface;
64  import nl.tudelft.simulation.dsol.animation.gis.GisMapInterface;
65  import nl.tudelft.simulation.dsol.animation.gis.GisRenderable2d;
66  import nl.tudelft.simulation.dsol.experiment.Replication;
67  import nl.tudelft.simulation.dsol.swing.animation.d2.AnimationPanel;
68  import nl.tudelft.simulation.dsol.swing.gui.ConsoleOutput;
69  import nl.tudelft.simulation.dsol.swing.gui.TabbedContentPane;
70  import nl.tudelft.simulation.language.DsolException;
71  
72  /**
73   * Simulation panel with various controls and animation.
74   * <p>
75   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
76   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
77   * </p>
78   * @author Alexander Verbraeck
79   * @author Peter Knoppers
80   * @author Wouter Schakel
81   */
82  public class OtsSimulationPanel extends JPanel implements ActionListener, EventListener
83  {
84      /** Serialization version UID. */
85      private static final long serialVersionUID = 20150617L;
86  
87      static
88      {
89          // use narrow border for TabbedPane, which cannot be changed afterwards
90          UIManager.put("TabbedPane.contentBorderInsets", new Insets(1, 0, 1, 0));
91      }
92  
93      /** Properties. */
94      public static final PropertiesStore PROPERTIES =
95              new PropertiesStore(new Properties(), "simulation", "simulation user settings");
96  
97      /** Simulator. */
98      private final OtsSimulatorInterface simulator;
99  
100     /** Control panel to control start/stop, speed of the simulation. */
101     private final OtsSimulationControlPanel otsSimulationControlPanel;
102 
103     /** Tabbed pane that contains the different (default) screens. */
104     private final TabbedContentPane tabbedPane;
105 
106     /** Pattern to split string by upper case, with lower case adjacent, without disregarding the match itself. */
107     private static final Pattern UPPER_PATTERN = Pattern.compile("(?=\\p{Lu})(?<=\\p{Ll})|(?=\\p{Lu}\\p{Ll})");
108 
109     /** Format for the world coordinates. */
110     private static final String COORD_FORMAT = "%09.2f";
111 
112     /** Pattern to split leading zeros from the rest of a number. */
113     private static final Pattern LEADING_ZEROS = Pattern.compile("^([+-]?)(0*)((:?0|[1-9]\\d*)(:?[\\.,]\\d+)?)$");
114 
115     /** Collapsed icon. */
116     private static final Icon COLLAPSED_ICON = IconUtil.of("Collapsed24.png").imageSize(12, 12).get();
117 
118     /** Expanded icon. */
119     private static final Icon EXPANDED_ICON = IconUtil.of("Expanded24.png").imageSize(12, 12).get();
120 
121     /** OTS search panel. */
122     private final OtsSearchPanel otsSearchPanel;
123 
124     /** Animation panel on tab position 0. */
125     private final OtsAnimationPanel otsAnimationPanel;
126 
127     /** Toggle panel with which animation features can be shown/hidden. */
128     private final JPanel togglePanel;
129 
130     /** Demo panel. */
131     private JPanel demoPanel = null;
132 
133     /** Whether the current toggle section is visible. */
134     private boolean toggleSectionVisible = true;
135 
136     /** Toggle section data (name and default visible). */
137     private Map<AppearanceJToggleButton, ToggleSectionData> toggleSectionData = new LinkedHashMap<>();
138 
139     /** Map of toggle names to toggle animation classes. */
140     private Map<String, Class<? extends Locatable>> toggleLocatableMap = new LinkedHashMap<>();
141 
142     /** Set of animation classes to toggle buttons. */
143     private Map<Class<? extends Locatable>, JToggleButton> toggleButtons = new LinkedHashMap<>();
144 
145     /** Set of GIS layer names to toggle GIS layers . */
146     private Map<String, GisMapInterface> toggleGisMap = new LinkedHashMap<>();
147 
148     /** Set of GIS layer names to toggle buttons. */
149     private Map<String, JToggleButton> toggleGisButtons = new LinkedHashMap<>();
150 
151     /** GTU color panel. */
152     private OtsGtuColorPanel gtuColorPanel = null;
153 
154     /** Coordinates of the cursor. */
155     private final JLabel coordinateField;
156 
157     /** GTU count field. */
158     private final JLabel gtuCountField;
159 
160     /** GTU count. */
161     private int gtuCount = 0;
162 
163     /** Id of object to auto pan to. */
164     private String autoPanId = null;
165 
166     /** Type of object to auto pan to. */
167     private OtsSearchPanel.ObjectKind<?> autoPanKind = null;
168 
169     /** Track auto pan object continuously? */
170     private boolean autoPanTrack = false;
171 
172     /** Track auto on the next paint (if this is true but autoPanTrack is false, only a one-shot auto pan). */
173     private boolean autoPanOnNextPaintComponent = false;
174 
175     /**
176      * Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
177      * @param network network
178      * @throws RemoteException when notification of the animation panel fails
179      * @throws DsolException when simulator does not implement AnimatorInterface
180      */
181     public OtsSimulationPanel(final Network network) throws RemoteException, DsolException
182     {
183         this(network.getExtent(), network);
184     }
185 
186     /**
187      * Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
188      * @param extent bottom left corner, length and width of the area (world) to animate
189      * @param network network
190      * @throws RemoteException when notification of the animation panel fails
191      * @throws DsolException when simulator does not implement AnimatorInterface
192      */
193     public OtsSimulationPanel(final Rectangle2D extent, final Network network) throws RemoteException, DsolException
194     {
195         this(extent, network, new OtsSimulationPanelDecorator()
196         {
197         });
198     }
199 
200     /**
201      * Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
202      * @param network network
203      * @param decorator decorator for the animation panel
204      * @throws RemoteException when notification of the animation panel fails
205      * @throws DsolException when simulator does not implement AnimatorInterface
206      * @throws NullPointerException when any input is {@code null}
207      */
208     public OtsSimulationPanel(final Network network, final OtsSimulationPanelDecorator decorator)
209             throws RemoteException, DsolException
210     {
211         this(network.getExtent(), network, decorator);
212     }
213 
214     /**
215      * Construct a panel that looks like the DSOLPanel for quick building of OTS applications.
216      * @param extent bottom left corner, length and width of the area (world) to animate
217      * @param network network
218      * @param decorator decorator for the animation panel
219      * @throws RemoteException when notification of the animation panel fails
220      * @throws DsolException when simulator does not implement AnimatorInterface
221      * @throws NullPointerException when any input is {@code null}
222      */
223     public OtsSimulationPanel(final Rectangle2D extent, final Network network, final OtsSimulationPanelDecorator decorator)
224             throws RemoteException, DsolException
225     {
226         Throw.whenNull(network, "network");
227         Throw.whenNull(decorator, "decorator");
228 
229         AppearanceApplication.setDefaultFont();
230 
231         this.simulator = network.getSimulator();
232 
233         /*-
234          * .-- OtsSimulationApplication -------------------------------------------------------------------------.
235          * | OTS | The Open Traffic Simulator | {model name}                                               _ # X |
236          * |o== OtsSimulationPanel =============================================================================o|
237          * ||+-- topPanel -------------------------------------------------------------------------------------+||
238          * |||o-- OtsSimulationControlPanel ----------------------------------o-- OtsSearchPanel -------------o|||
239          * ||||  >  >  >                                  00:00:00.000  0.00x |  GTU|v| |Id...    | track     ||||
240          * |||o---------------------------------------------------------------o-------------------------------o|||
241          * ||o-- AppearanceControlTabbedContentPane -----------------------------------------------------------o||
242          * |||+-- borderLayoutPanel --------------------------------------------------------------------------+|||
243          * ||||+-- animationTopBarPanel ---------------------------------------------------------------------+||||
244          * |||||o-- OtsGtuColorPanel -----------------------------------o               +-- infoTextPanel --+|||||
245          * ||||||  Blue     |v|                                         |    Home Grid  | 0 GTU's           ||||||
246          * |||||o-------------------------------------------------------o               +-------------------+|||||
247          * ||||+-- toggle --o-- OtsAnimationPanel -----------------------------------------------------------o||||
248          * |||||   -Panel   |                                                                                |||||
249          * |||||            |                                                                                |||||
250          * |||||            |                        +-- demoPanel -----------------+                        |||||
251          * |||||            |                        |                              |                        |||||
252          * |||||            |                        | (top, bottom, left or right) |                        |||||
253          * |||||            |                        |                              |                        |||||
254          * |||||            |                        +------------------------------+                        |||||
255          * |||||            |                                                                                |||||
256          * |||||            |                                                                                |||||
257          * ||||+------------o--------------------------------------------------------------------------------o||||
258          * |||+-----------------------------------------------------------------------------------------------+|||
259          * ||| animation /                                                                                     |||
260          * ||o-------------------------------------------------------------------------------------------------o||
261          * |o---------------------------------------------------------------------------------------------------o|
262          * '-----------------------------------------------------------------------------------------------------'
263          *
264          * Legend:   +-- lowerCaseName -------+    o-- UpperCaseName -------o
265          *           |     Regular JPanel     |    |     Specific class     |
266          *           +------------------------+    o------------------------o
267          */
268 
269         setLayout(new BorderLayout());
270 
271         // topPanel > OtsSimulationControlPanel, OtsSearchPanel
272         JPanel topPanel = new JPanel();
273         topPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
274         topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.X_AXIS));
275         this.otsSimulationControlPanel = new OtsSimulationControlPanel(this.simulator, this);
276         this.otsSearchPanel = new OtsSearchPanel(this);
277         topPanel.add(this.otsSimulationControlPanel);
278         topPanel.add(Box.createHorizontalGlue());
279         topPanel.add(this.otsSearchPanel);
280         add(topPanel, BorderLayout.NORTH);
281 
282         // tabbedPane with borderLayoutPanel in animation tab
283         this.tabbedPane = new AppearanceControlTabbedContentPane(SwingConstants.BOTTOM);
284         JPanel borderLayoutPanel = new JPanel(new BorderLayout());
285         this.tabbedPane.addTab(0, "animation", borderLayoutPanel);
286         add(this.tabbedPane, BorderLayout.CENTER);
287 
288         // borderLayoutPanel > animationTopBarPanel, togglePanel, OtsAnimationPanel
289         JPanel animationTopBarPanel = new JPanel();
290         animationTopBarPanel.setLayout(new BoxLayout(animationTopBarPanel, BoxLayout.X_AXIS));
291         borderLayoutPanel.add(animationTopBarPanel, BorderLayout.NORTH);
292         this.togglePanel = new JPanel();
293         this.togglePanel.setLayout(new BoxLayout(this.togglePanel, BoxLayout.Y_AXIS));
294         JPanel resetBox = new JPanel();
295         resetBox.setLayout(new BoxLayout(resetBox, BoxLayout.X_AXIS));
296         resetBox.setAlignmentX(Component.LEFT_ALIGNMENT);
297         this.togglePanel.add(resetBox);
298         Dimension dim = new Dimension(16, 16);
299         resetBox.add(Box.createHorizontalGlue());
300         createToggleSectionResetButton("Expand", EXPANDED_ICON, true, dim, resetBox);
301         createToggleSectionResetButton("Collapse", COLLAPSED_ICON, false, dim, resetBox);
302         createToggleSectionResetButton("Reset", IconUtil.of("Restore24.png").imageSize(12, 12).get(), null, dim, resetBox);
303         resetBox.add(Box.createHorizontalGlue());
304 
305         borderLayoutPanel.add(this.togglePanel, BorderLayout.WEST);
306         this.otsAnimationPanel = new OtsAnimationPanel(extent, this.simulator, network);
307         this.otsAnimationPanel.showGrid(PROPERTIES.getOptionalBoolean("grid").orElse(false));
308         borderLayoutPanel.add(this.otsAnimationPanel, BorderLayout.CENTER);
309 
310         // animationTopBarPanel > OtsGtuColorPanel, buttons, infoTextPanel
311         this.gtuColorPanel = new OtsGtuColorPanel();
312         decorator.getGtuColorers().forEach((colorer) -> this.gtuColorPanel.addGtuColorer(colorer));
313         animationTopBarPanel.add(this.gtuColorPanel);
314         animationTopBarPanel.add(Box.createHorizontalStrut(10));
315         animationTopBarPanel.add(makeButton("yZoomButton", "UpDown24.png", "Reset Y-zoom", "Reset Y-zoom", true));
316         animationTopBarPanel.add(makeButton("allButton", "ZoomAll24.png", "ZoomAll", "Zoom whole network", true));
317         animationTopBarPanel.add(makeButton("homeButton", "Home24.png", "Home", "Zoom to original extent", true));
318         animationTopBarPanel.add(makeButton("gridButton", "Grid24.png", "Grid", "Toggle grid on/off", true));
319         animationTopBarPanel.add(Box.createHorizontalStrut(10));
320         JPanel infoTextPanel = new JPanel();
321         animationTopBarPanel.add(infoTextPanel);
322         infoTextPanel.setMinimumSize(new Dimension(250, 30));
323         infoTextPanel.setPreferredSize(new Dimension(250, 30));
324         infoTextPanel.setMaximumSize(new Dimension(250, 30));
325         infoTextPanel.setLayout(new BoxLayout(infoTextPanel, BoxLayout.Y_AXIS));
326 
327         // infoTextPanel contents
328         this.coordinateField = new JLabel();
329         this.coordinateField.setMinimumSize(new Dimension(150, 15));
330         this.coordinateField.setPreferredSize(new Dimension(150, 15));
331         this.coordinateField.setMaximumSize(new Dimension(150, 15));
332         this.coordinateField.setFont(new Font("Consolas", Font.PLAIN, 12));
333         infoTextPanel.add(this.coordinateField);
334         this.gtuCountField = new JLabel();
335         this.gtuCountField.setMinimumSize(new Dimension(150, 15));
336         this.gtuCountField.setPreferredSize(new Dimension(150, 15));
337         this.gtuCountField.setMaximumSize(new Dimension(150, 15));
338         this.gtuCount = null == network ? 0 : network.getGTUs().size();
339         infoTextPanel.add(this.gtuCountField);
340         setGtuCountText();
341 
342         // only show OtsSearchPanel when the animation tab is selected
343         this.tabbedPane.addChangeListener(new ChangeListener()
344         {
345             @Override
346             public void stateChanged(final ChangeEvent e)
347             {
348                 int index = OtsSimulationPanel.this.tabbedPane.getSelectedIndex();
349                 Component component = OtsSimulationPanel.this.tabbedPane.getComponentAt(index);
350                 OtsSimulationPanel.this.otsSearchPanel.setVisible(borderLayoutPanel.equals(component));
351             }
352         });
353 
354         // listen to update GTU count
355         if (null != network)
356         {
357             network.addListener(this, Network.GTU_ADD_EVENT);
358             network.addListener(this, Network.GTU_REMOVE_EVENT);
359         }
360 
361         // fake start event to draw static objects before the simulation is started (this will all be cleared on real start)
362         this.otsAnimationPanel
363                 .notify(new TimedEvent<>(Replication.START_REPLICATION_EVENT, null, getSimulator().getSimulatorTime()));
364 
365         // switch off the X and Y coordinates in a tool-tip
366         this.otsAnimationPanel.setShowToolTip(false);
367 
368         // decorate
369         decorator.decorate(this, network);
370     }
371 
372     /**
373      * Creates toggle section reset button.
374      * @param action action, is prepended to tooltip text
375      * @param icon icon
376      * @param selected selected state of toggle section toggles when this button is clicked
377      * @param dim dimension
378      * @param resetBox panel to add the button to
379      */
380     private void createToggleSectionResetButton(final String action, final Icon icon, final Boolean selected,
381             final Dimension dim, final JPanel resetBox)
382     {
383         JButton expand = new JButton(new AbstractAction("", icon)
384         {
385             private static final long serialVersionUID = 20260507L;
386 
387             @Override
388             public void actionPerformed(final ActionEvent e)
389             {
390                 setToggleSectionButtonsState(selected);
391             }
392         });
393         expand.setToolTipText(action + " all animation toggle sections");
394         expand.setContentAreaFilled(false);
395         expand.setBorder(null);
396         expand.setMinimumSize(dim);
397         expand.setPreferredSize(dim);
398         expand.setMaximumSize(dim);
399         resetBox.add(expand);
400     }
401 
402     /**
403      * Sets all toggle section buttons to the given selected state.
404      * @param selected {@code true} is selected, {@code false} is not selected, {@code null} is as per default
405      */
406     private void setToggleSectionButtonsState(final Boolean selected)
407     {
408         for (Component component : this.togglePanel.getComponents())
409         {
410             if (component instanceof AppearanceJToggleButton button)
411             {
412                 ToggleSectionData sectionData = this.toggleSectionData.get(button);
413                 boolean state = selected == null ? sectionData.defaultVisible() : selected;
414                 button.setSelected(state);
415                 toggleSectionToggleChanged(sectionData.name(), button, getToggleSectionKey(sectionData.name()));
416             }
417         }
418     }
419 
420     /**
421      * Adds the console tab.
422      */
423     public void addConsoleTab()
424     {
425         ConsoleOutput console = new ConsoleOutput();
426         console.setBorder(null);
427         this.tabbedPane.addTab("console", console);
428     }
429 
430     /**
431      * Return tabbed pane.
432      * @return tabbed pane
433      */
434     public TabbedContentPane getTabbedPane()
435     {
436         return this.tabbedPane;
437     }
438 
439     /**
440      * Return simulator.
441      * @return simulator
442      */
443     public OtsSimulatorInterface getSimulator()
444     {
445         return this.simulator;
446     }
447 
448     /**
449      * Enable the simulation or animation buttons in the GUI. This method HAS TO BE CALLED in order for the buttons to be
450      * enabled, because the initial state is DISABLED. Typically, this is done after all tabs, statistics, and other user
451      * interface and model components have been constructed and initialized.
452      */
453     public void enableSimulationControlButtons()
454     {
455         this.otsSimulationControlPanel.setSimulationControlButtons(true);
456     }
457 
458     /**
459      * Disable the simulation or animation buttons in the GUI.
460      */
461     public void disableSimulationControlButtons()
462     {
463         this.otsSimulationControlPanel.setSimulationControlButtons(false);
464     }
465 
466     /**
467      * Change auto pan target.
468      * @param newAutoPanId id of object to track
469      * @param newAutoPanKind kind of object to track
470      * @param newAutoPanTrack if true; tracking is continuously; if false; tracking is once
471      */
472     public void setAutoPan(final String newAutoPanId, final OtsSearchPanel.ObjectKind<?> newAutoPanKind,
473             final boolean newAutoPanTrack)
474     {
475         this.autoPanId = newAutoPanId;
476         this.autoPanKind = newAutoPanKind;
477         this.autoPanTrack = newAutoPanTrack;
478         this.autoPanOnNextPaintComponent = true;
479         Logger.ots().trace("AutoPan id=" + newAutoPanId + ", kind=" + newAutoPanKind + ", track=" + newAutoPanTrack);
480         if (null != this.autoPanId && null != OtsSimulationPanel.this.otsAnimationPanel && this.autoPanId.length() > 0
481                 && null != this.autoPanKind)
482         {
483             OtsSimulationPanel.this.otsAnimationPanel.repaint();
484         }
485     }
486 
487     /**
488      * Create a button.
489      * @param name name of the button
490      * @param iconFile name of the icon file
491      * @param actionCommand the action command
492      * @param toolTipText the hint to show when the mouse hovers over the button
493      * @param enabled true if the new button must initially be enable; false if it must initially be disabled
494      * @return button
495      */
496     private JButton makeButton(final String name, final String iconFile, final String actionCommand, final String toolTipText,
497             final boolean enabled)
498     {
499         JButton result = new JButton(IconUtil.of(iconFile).get());
500         result.setMinimumSize(new Dimension(34, 32));
501         result.setPreferredSize(new Dimension(34, 32));
502         result.setMaximumSize(new Dimension(34, 32));
503         result.setName(name);
504         result.setEnabled(enabled);
505         result.setActionCommand(actionCommand);
506         result.setToolTipText(toolTipText);
507         result.addActionListener(this);
508         return result;
509     }
510 
511     /**
512      * Adds a button that can hide all toggles in a section. The section is defined as laying between two section buttons.
513      * @param name name of the section that can be hidden
514      * @param visibleDefault whether the section is visible by default
515      */
516     public void startToggleSection(final String name, final boolean visibleDefault)
517     {
518         AppearanceJToggleButton toggle = new AppearanceJToggleButton();
519         toggle.setSelectedIcon(EXPANDED_ICON);
520         toggle.setIcon(COLLAPSED_ICON);
521         String key = getToggleSectionKey(name);
522         boolean toggleOn = PROPERTIES.getOptionalBoolean(key).orElse(true);
523         toggle.setSelected(toggleOn);
524         PROPERTIES.setBoolean(key, toggleOn);
525         this.toggleSectionVisible = toggleOn;
526         toggle.setText(toggleOn ? null : name);
527         toggle.setContentAreaFilled(false);
528         toggle.setBorder(null);
529         Dimension dim = new Dimension(64, 14);
530         toggle.setMinimumSize(dim);
531         toggle.setPreferredSize(dim);
532         toggle.setMaximumSize(dim);
533         toggle.addActionListener(new ActionListener()
534         {
535             @Override
536             public void actionPerformed(final ActionEvent e)
537             {
538                 toggleSectionToggleChanged(name, toggle, key);
539             }
540         });
541         this.togglePanel.add(toggle);
542         this.toggleSectionData.put(toggle, new ToggleSectionData(name, visibleDefault));
543     }
544 
545     /**
546      * Returns a toggle section key for inside {@code PROPERTIES}.
547      * @param name name of the section that can be hidden
548      * @return toggle section key
549      */
550     private String getToggleSectionKey(final String name)
551     {
552         return "toggle.section." + PropertiesStore.key(name);
553     }
554 
555     /**
556      * Remembers the state of a toggled toggle section in properties and shows or hides the section animation toggles.
557      * @param name section name
558      * @param sectionToggle section toggle
559      * @param key section key
560      */
561     private void toggleSectionToggleChanged(final String name, final AppearanceJToggleButton sectionToggle, final String key)
562     {
563         PROPERTIES.setBoolean(key, sectionToggle.isSelected());
564         sectionToggle.setText(sectionToggle.isSelected() ? null : name);
565         boolean inSection = false;
566         for (Component component : OtsSimulationPanel.this.togglePanel.getComponents())
567         {
568             if (component.equals(sectionToggle))
569             {
570                 inSection = true;
571             }
572             else if (inSection && component instanceof AppearanceJToggleButton)
573             {
574                 return;
575             }
576             else if (inSection)
577             {
578                 component.setVisible(sectionToggle.isSelected());
579             }
580         }
581     }
582 
583     /**
584      * Add a button for toggling an animation class on or off. Button icons for which 'nextToPrevious' is true will be placed to
585      * the right of the previous button, which should be the corresponding button for id buttons. An example is an icon for
586      * showing/hiding the class 'Lane' followed by the button to show/hide the Lane ids. Other buttons can be placed next to the
587      * previous too.
588      * @param name the name of the button
589      * @param locatableClass the class for which the button holds (e.g., GTU.class)
590      * @param iconPath the path to the 24x24 icon to display
591      * @param toolTipText the tool tip text to show when hovering over the button
592      * @param initiallyVisible whether the class is initially shown or not
593      * @param nextToPrevious button that needs to be placed next to the previous button
594      */
595     public void addToggleAnimationButtonIcon(final String name, final Class<? extends Locatable> locatableClass,
596             final String iconPath, final String toolTipText, final boolean initiallyVisible, final boolean nextToPrevious)
597     {
598         JToggleButton button;
599         Icon icon = IconUtil.of(iconPath).get();
600         Icon unIcon = IconUtil.of(iconPath).gray().get();
601         button = new JCheckBox();
602         button.setSelectedIcon(icon);
603         button.setIcon(unIcon);
604         button.setPreferredSize(new Dimension(32, 28));
605         button.setName(name);
606         String key = "toggle." + PropertiesStore.key(name);
607         boolean toggleOn = PROPERTIES.getOptionalBoolean(key).orElse(initiallyVisible);
608         button.setSelected(toggleOn);
609         PROPERTIES.setBoolean(key, toggleOn);
610         button.setActionCommand(name);
611         button.setToolTipText(toolTipText);
612         button.addActionListener(this);
613 
614         // place button to the right of the previous content button?
615         if (nextToPrevious && this.togglePanel.getComponentCount() > 0)
616         {
617             JPanel lastToggleBox = (JPanel) this.togglePanel.getComponent(this.togglePanel.getComponentCount() - 1);
618             button.setVisible(true);
619             lastToggleBox.add(button);
620         }
621         else
622         {
623             JPanel toggleBox = new JPanel();
624             toggleBox.setVisible(this.toggleSectionVisible);
625             button.setVisible(true);
626             toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
627             toggleBox.add(button);
628             this.togglePanel.add(toggleBox);
629             toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
630         }
631 
632         if (toggleOn)
633         {
634             this.otsAnimationPanel.showClass(locatableClass);
635         }
636         else
637         {
638             this.otsAnimationPanel.hideClass(locatableClass);
639         }
640         this.toggleLocatableMap.put(name, locatableClass);
641         this.toggleButtons.put(locatableClass, button);
642     }
643 
644     /**
645      * Add a button for toggling an animation class on or off.
646      * @param name the name of the button
647      * @param locatableClass the class for which the button holds (e.g., {@code GTU.class})
648      * @param toolTipText the tool tip text to show when hovering over the button
649      * @param initiallyVisible whether the class is initially shown or not
650      */
651     public void addToggleAnimationButtonText(final String name, final Class<? extends Locatable> locatableClass,
652             final String toolTipText, final boolean initiallyVisible)
653     {
654         JToggleButton button;
655         button = new JCheckBox(name);
656         button.setText(separatedName(name));
657         button.setVisible(this.toggleSectionVisible);
658         String key = "toggle." + PropertiesStore.key(name);
659         boolean toggleOn = PROPERTIES.getOptionalBoolean(key).orElse(initiallyVisible);
660         button.setSelected(toggleOn);
661         PROPERTIES.setBoolean(key, toggleOn);
662         button.setActionCommand(name);
663         button.setToolTipText(toolTipText);
664         button.addActionListener(this);
665         button.setPreferredSize(new Dimension(113, 19)); // Can just fit "Generator Q" at largest Appearance Control font size
666         button.setMaximumSize(new Dimension(113, 19));
667 
668         this.togglePanel.add(button);
669 
670         if (toggleOn)
671         {
672             this.otsAnimationPanel.showClass(locatableClass);
673         }
674         else
675         {
676             this.otsAnimationPanel.hideClass(locatableClass);
677         }
678         this.toggleLocatableMap.put(name, locatableClass);
679         this.toggleButtons.put(locatableClass, button);
680     }
681 
682     /**
683      * Add a text to explain animation classes.
684      * @param text the text to show
685      */
686     public void addToggleText(final String text)
687     {
688         JPanel textBox = new JPanel();
689         textBox.setVisible(this.toggleSectionVisible);
690         textBox.setLayout(new BoxLayout(textBox, BoxLayout.X_AXIS));
691         textBox.add(new JLabel(text));
692         this.togglePanel.add(textBox);
693         textBox.setAlignmentX(Component.LEFT_ALIGNMENT);
694     }
695 
696     /**
697      * Add buttons for toggling all GIS layers on or off.
698      * @param header the name of the group of layers
699      * @param gisMap the GIS map for which the toggles have to be added
700      * @param toolTipText the tool tip text to show when hovering over the button
701      */
702     public void addAllToggleGisButtonText(final String header, final GisRenderable2d gisMap, final String toolTipText)
703     {
704         addToggleText(" ");
705         addToggleText(header);
706         for (String layerName : gisMap.getMap().getLayerMap().keySet())
707         {
708             addToggleGisButtonText(layerName, layerName, gisMap, toolTipText, true);
709         }
710     }
711 
712     /**
713      * Add a button to toggle a GIS Layer on or off.
714      * @param layerName the name of the layer
715      * @param displayName the name to display next to the tick box
716      * @param gisMap the map
717      * @param toolTipText the tool tip text
718      * @param initiallyVisible whether the layer is initially shown or not
719      */
720     public void addToggleGisButtonText(final String layerName, final String displayName, final GisRenderable2d gisMap,
721             final String toolTipText, final boolean initiallyVisible)
722     {
723         JToggleButton button;
724         button = new JCheckBox(displayName);
725         button.setName(layerName);
726         button.setVisible(this.toggleSectionVisible);
727         String key = "toggle.gis." + PropertiesStore.key(layerName);
728         boolean toggleOn = PROPERTIES.getOptionalBoolean(key).orElse(initiallyVisible);
729         button.setSelected(toggleOn);
730         PROPERTIES.setBoolean(key, toggleOn);
731         button.setSelected(toggleOn);
732         button.setActionCommand(layerName);
733         button.setToolTipText(toolTipText);
734         button.addActionListener(this);
735 
736         JPanel toggleBox = new JPanel();
737         toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
738         toggleBox.add(button);
739         this.togglePanel.add(toggleBox);
740         toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
741 
742         this.toggleGisMap.put(layerName, gisMap.getMap());
743         this.toggleGisButtons.put(layerName, button);
744     }
745 
746     /**
747      * Set a GIS layer to be shown in the animation to true.
748      * @param layerName the name of the GIS-layer that has to be shown
749      */
750     public void showGisLayer(final String layerName)
751     {
752         GisMapInterface gisMap = this.toggleGisMap.get(layerName);
753         if (gisMap != null && !gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
754         {
755             toggleGisLayer(layerName);
756         }
757     }
758 
759     /**
760      * Set a GIS layer to be hidden in the animation to true.
761      * @param layerName the name of the GIS-layer that has to be hidden
762      */
763     public void hideGisLayer(final String layerName)
764     {
765         GisMapInterface gisMap = this.toggleGisMap.get(layerName);
766         if (gisMap != null && gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName)))
767         {
768             toggleGisLayer(layerName);
769         }
770     }
771 
772     /**
773      * Toggle a GIS layer to be displayed in the animation to its reverse value.
774      * @param layerName the name of the GIS-layer that has to be turned off or vice versa
775      */
776     public void toggleGisLayer(final String layerName)
777     {
778         GisMapInterface gisMap = this.toggleGisMap.get(layerName);
779         if (gisMap != null)
780         {
781             boolean show = !gisMap.getVisibleLayers().contains(gisMap.getLayerMap().get(layerName));
782             String key = "toggle.gis." + PropertiesStore.key(layerName);
783             PROPERTIES.setBoolean(key, show);
784             if (show)
785             {
786                 gisMap.showLayer(layerName);
787                 this.toggleGisButtons.get(layerName).setSelected(true);
788             }
789             else
790             {
791                 gisMap.hideLayer(layerName);
792                 this.toggleGisButtons.get(layerName).setSelected(false);
793             }
794             this.otsAnimationPanel.repaint();
795         }
796     }
797 
798     @Override
799     public void actionPerformed(final ActionEvent actionEvent)
800     {
801         String actionCommand = actionEvent.getActionCommand();
802         Logger.ots().trace("Action command is " + actionCommand);
803         try
804         {
805             if (actionCommand.equals("Reset Y-zoom"))
806             {
807                 this.otsAnimationPanel.resetZoomY();
808             }
809             else if (actionCommand.equals("Home"))
810             {
811                 this.otsAnimationPanel.resetZoomY();
812                 this.otsAnimationPanel.home();
813             }
814             else if (actionCommand.equals("ZoomAll"))
815             {
816                 this.otsAnimationPanel.resetZoomY();
817                 this.otsAnimationPanel.zoomAll();
818             }
819             else if (actionCommand.equals("Grid"))
820             {
821                 this.otsAnimationPanel.showGrid(!this.otsAnimationPanel.isShowGrid());
822                 PROPERTIES.setBoolean("grid", this.otsAnimationPanel.isShowGrid());
823             }
824             else if (this.toggleLocatableMap.containsKey(actionCommand))
825             {
826                 Class<? extends Locatable> locatableClass = this.toggleLocatableMap.get(actionCommand);
827                 this.otsAnimationPanel.toggleClass(locatableClass);
828                 String key = "toggle." + PropertiesStore.key(actionCommand);
829                 PROPERTIES.setBoolean(key, this.otsAnimationPanel.isShowClass(locatableClass));
830                 this.togglePanel.repaint();
831             }
832             else if (this.toggleGisMap.containsKey(actionCommand))
833             {
834                 this.toggleGisLayer(actionCommand);
835                 this.togglePanel.repaint();
836             }
837         }
838         catch (Exception exception)
839         {
840             exception.printStackTrace();
841         }
842     }
843 
844     /**
845      * Easy access to the AnimationPanel.
846      * @return animation panel
847      */
848     public AnimationPanel getAnimationPanel()
849     {
850         return this.otsAnimationPanel;
851     }
852 
853     /**
854      * Creates a demo panel within the animation area.
855      * @param position position within the animation panel
856      * @throws IllegalStateException if the panel was already created
857      */
858     public void createDemoPanel(final DemoPanelPosition position)
859     {
860         Throw.when(this.demoPanel != null, IllegalStateException.class,
861                 "Attempt to create demo panel, but it's already created");
862         Throw.whenNull(position, "Position may not be null.");
863         Container parent = this.otsAnimationPanel.getParent();
864         parent.remove(this.otsAnimationPanel);
865 
866         JPanel splitPanel = new JPanel(new BorderLayout());
867         parent.add(splitPanel);
868         splitPanel.add(this.otsAnimationPanel, BorderLayout.CENTER);
869 
870         this.demoPanel = new JPanel();
871         this.demoPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
872         splitPanel.add(this.demoPanel, position.getBorderLayoutPosition());
873     }
874 
875     /**
876      * Return a panel for on-screen demo controls. The panel is created on the right of the screen on first call, unless
877      * {@link #createDemoPanel} was already called.
878      * @return demo panel
879      */
880     public JPanel getDemoPanel()
881     {
882         if (this.demoPanel == null)
883         {
884             createDemoPanel(DemoPanelPosition.RIGHT);
885         }
886         return this.demoPanel;
887     }
888 
889     /**
890      * Update the check-mark related to a programmatically changed animation state.
891      * @param locatableClass class to show the check-mark for
892      */
893     public void updateAnimationClassCheckBox(final Class<? extends Locatable> locatableClass)
894     {
895         JToggleButton button = this.toggleButtons.get(locatableClass);
896         if (button == null)
897         {
898             return;
899         }
900         button.setSelected(getAnimationPanel().isShowClass(locatableClass));
901     }
902 
903     /**
904      * Display the latest world coordinate based on the mouse position on the screen.
905      */
906     private void updateWorldCoordinate()
907     {
908         String x = String.format(COORD_FORMAT, this.otsAnimationPanel.getWorldCoordinate().getX());
909         String y = String.format(COORD_FORMAT, this.otsAnimationPanel.getWorldCoordinate().getY());
910         String worldPoint = "<html>(x=" + fadeLeadingZeros(x) + "; y=" + fadeLeadingZeros(y) + ")</html>";
911         this.coordinateField.setText(worldPoint);
912         String worldPointNoHtml = "(x=" + x + "; y=" + y + ")";
913         if (this.coordinateField.getGraphics() == null)
914         {
915             // window is in a deleted state, but the mouse listener is still causing this event
916             return;
917         }
918         // add 10px margin for if the window is dragged to another monitor and Swing finds a few more pixels required there due
919         // to the monitor being set to e.g. 125% instead of 100%
920         int requiredWidth = this.coordinateField.getGraphics().getFontMetrics().stringWidth(worldPointNoHtml) + 10;
921         if (this.coordinateField.getPreferredSize().width < requiredWidth)
922         {
923             Dimension requiredSize = new Dimension(requiredWidth, this.coordinateField.getPreferredSize().height);
924             this.coordinateField.setMinimumSize(requiredSize);
925             this.coordinateField.setPreferredSize(requiredSize);
926             this.coordinateField.setMaximumSize(requiredSize);
927             Container parent = this.coordinateField.getParent();
928             requiredSize = new Dimension(requiredWidth, parent.getPreferredSize().height);
929             parent.setMinimumSize(requiredSize);
930             parent.setPreferredSize(requiredSize);
931             parent.setMaximumSize(requiredSize);
932             Logger.ots().trace("Increased minimum width to " + requiredSize.width);
933             parent.revalidate();
934         }
935         this.coordinateField.repaint();
936     }
937 
938     /**
939      * Gives the leading zeros a faded color using HTML.
940      * @param formatted formatted number
941      * @return formatted number with leading zeros faded
942      */
943     private String fadeLeadingZeros(final String formatted)
944     {
945         Matcher m = LEADING_ZEROS.matcher(formatted);
946         if (!m.matches())
947         {
948             return formatted;
949         }
950         String sign = m.group(1) == null ? "" : m.group(1);
951         String zeros = m.group(2) == null ? "" : m.group(2);
952         String digits = m.group(3);
953         Color faded = ColorInterpolator.interpolateColor(this.coordinateField.getBackground(),
954                 this.gtuCountField.getForeground(), 0.2);
955         String zerosColor = String.format("#%02x%02x%02x", faded.getRed(), faded.getGreen(), faded.getBlue());
956         return sign + "<span style='color:" + zerosColor + ";'>" + zeros + "</span>" + digits;
957     }
958 
959     /**
960      * GTU colorer manager from the GTU color panel.
961      * @return GTU colorer manager from the GTU color panel
962      */
963     public GtuColorerManager getGtuColorerManager()
964     {
965         return this.gtuColorPanel.getGtuColorerManager();
966     }
967 
968     @Override
969     public void notify(final Event event)
970     {
971         if (event.getType().equals(Network.GTU_ADD_EVENT))
972         {
973             this.gtuCount++;
974             setGtuCountText();
975         }
976         else if (event.getType().equals(Network.GTU_REMOVE_EVENT))
977         {
978             this.gtuCount--;
979             setGtuCountText();
980         }
981     }
982 
983     /**
984      * Updates the text of the GTU counter.
985      */
986     private void setGtuCountText()
987     {
988         String text = this.gtuCount + " GTU's";
989         this.gtuCountField.setText(text);
990     }
991 
992     /**
993      * Adds a thin space before each capital character in a {@code String}, except the first.
994      * @param name name of node
995      * @return input string but with a thin space before each capital character, except the first
996      */
997     public static String separatedName(final String name)
998     {
999         String[] parts = UPPER_PATTERN.split(name);
1000         if (parts.length == 1)
1001         {
1002             return parts[0];
1003         }
1004         String separator = "";
1005         StringBuilder stringBuilder = new StringBuilder();
1006         for (String part : parts)
1007         {
1008             stringBuilder.append(separator).append(part);
1009             separator = "\u2009"; // thin space
1010         }
1011         return stringBuilder.toString();
1012     }
1013 
1014     /**
1015      * Toggle button with appearance control.
1016      */
1017     private final class AppearanceJToggleButton extends JCheckBox implements AppearanceControl
1018     {
1019         /** Serialization version UID. */
1020         private static final long serialVersionUID = 1L;
1021 
1022         @Override
1023         public boolean isForeground()
1024         {
1025             return true;
1026         }
1027 
1028         @Override
1029         public OptionalInt getFontSize()
1030         {
1031             return OptionalInt.empty();
1032         }
1033     }
1034 
1035     /**
1036      * Toggle section data.
1037      * @param name section name
1038      * @param defaultVisible whether the section is visible by default
1039      */
1040     private record ToggleSectionData(String name, boolean defaultVisible)
1041     {
1042     }
1043 
1044     /**
1045      * Animation panel that adds auto-pan functionality.
1046      */
1047     private class OtsAnimationPanel extends AnimationPanel
1048     {
1049 
1050         /** Serialization version UID. */
1051         private static final long serialVersionUID = 20180430L;
1052 
1053         /** Network. */
1054         private final Network network;
1055 
1056         /**
1057          * Constructor.
1058          * @param extent home extent
1059          * @param simulator simulator
1060          * @param network network
1061          * @throws RemoteException on remote animation error
1062          * @throws DsolException when simulator does not implement AnimatorInterface
1063          */
1064         OtsAnimationPanel(final Rectangle2D extent, final OtsSimulatorInterface simulator, final Network network)
1065                 throws RemoteException, DsolException
1066         {
1067             super(new Bounds2d(extent.getMinX(), extent.getMaxX(), extent.getMinY(), extent.getMaxY()), simulator);
1068             setPreferredSize(new Dimension(800, 600));
1069             this.network = network;
1070             MouseListener[] listeners = getMouseListeners();
1071             for (MouseListener listener : listeners)
1072             {
1073                 removeMouseListener(listener);
1074             }
1075             this.addMouseListener(new MouseAdapter()
1076             {
1077                 @Override
1078                 public void mouseClicked(final MouseEvent e)
1079                 {
1080                     if (e.isControlDown())
1081                     {
1082                         Gtu gtu = getSelectedGtu(e.getPoint());
1083                         if (gtu != null)
1084                         {
1085                             OtsSimulationPanel.this.otsSearchPanel.selectAndTrackObject("GTU", gtu.getId(), true);
1086                         }
1087                     }
1088                     e.consume();
1089                 }
1090             });
1091             for (MouseListener listener : listeners)
1092             {
1093                 addMouseListener(listener);
1094             }
1095         }
1096 
1097         /**
1098          * Set the world coordinates based on a mouse move.
1099          * @param point the x,y world coordinates
1100          */
1101         @Override
1102         public synchronized void setWorldCoordinate(final Point2d point)
1103         {
1104             super.setWorldCoordinate(point);
1105             updateWorldCoordinate();
1106         }
1107 
1108         /**
1109          * returns the list of selected objects at a certain mousePoint.
1110          * @param mousePoint the mousePoint
1111          * @return the selected objects
1112          */
1113         private Gtu getSelectedGtu(final Point2D mousePoint)
1114         {
1115             List<LaneBasedGtu> targets = new ArrayList<>();
1116             Point2d point = getRenderableScale().getWorldCoordinates(mousePoint, getExtent(), getSize());
1117             for (Renderable2dInterface<?> renderable : getElements())
1118             {
1119                 if (isShowElement(renderable) && renderable.contains(point, getExtent()))
1120                 {
1121                     if (renderable.getSource() instanceof AnimationGtuData animData)
1122                     {
1123                         targets.add(animData.getObject());
1124                     }
1125                 }
1126             }
1127             if (targets.size() == 1)
1128             {
1129                 return targets.get(0);
1130             }
1131             return null;
1132         }
1133 
1134         @Override
1135         public void paintComponent(final Graphics g)
1136         {
1137             final OtsSearchPanel.ObjectKind<?> panKind = OtsSimulationPanel.this.autoPanKind;
1138             final String panId = OtsSimulationPanel.this.autoPanId;
1139             final boolean doPan = OtsSimulationPanel.this.autoPanOnNextPaintComponent;
1140             OtsSimulationPanel.this.autoPanOnNextPaintComponent = OtsSimulationPanel.this.autoPanTrack;
1141             if (doPan && panKind != null && panId != null)
1142             {
1143                 Optional<? extends Locatable> locatable = panKind.searchNetwork(this.network, panId);
1144                 if (locatable.isPresent())
1145                 {
1146                     Point<?> point = locatable.get().getLocation();
1147                     if (point != null) // Center extent around point
1148                     {
1149                         double w = getExtent().getDeltaX();
1150                         double h = getExtent().getDeltaY();
1151                         setExtent(new Bounds2d(point.getX() - w / 2, point.getX() + w / 2, point.getY() - h / 2,
1152                                 point.getY() + h / 2));
1153                     }
1154                 }
1155             }
1156             super.paintComponent(g);
1157         }
1158 
1159         @Override
1160         public void setBackground(final Color bg)
1161         {
1162             int threshold = 64;
1163             int alternative = 96;
1164             if (bg.getRed() <= threshold && bg.getGreen() <= threshold && bg.getBlue() <= threshold)
1165             {
1166                 setGridColor(new Color(alternative, alternative, alternative));
1167             }
1168             else
1169             {
1170                 setGridColor(Color.BLACK);
1171             }
1172             super.setBackground(bg);
1173         }
1174 
1175         // Overridden because there are rounding and vertical mod errors in the super implementation.
1176         // See https://github.com/averbraeck/dsol/issues/116.
1177         @Override
1178         protected synchronized void drawGrid(final Graphics g)
1179         {
1180             // we prepare the graphics object for the grid
1181             g.setFont(g.getFont().deriveFont(11.0f));
1182             g.setColor(this.getGridColor());
1183             double scaleX = this.getRenderableScale().getXScale(this.getExtent(), this.getSize());
1184             double scaleY = this.getRenderableScale().getYScale(this.getExtent(), this.getSize());
1185 
1186             int count = 0;
1187             double gridSizePixelsX = this.gridSizeX / scaleX;
1188             while (gridSizePixelsX < 40)
1189             {
1190                 this.gridSizeX = 10 * this.gridSizeX;
1191                 int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
1192                 this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
1193                 gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
1194                 if (count++ > 10)
1195                 {
1196                     break;
1197                 }
1198             }
1199 
1200             count = 0;
1201             while (gridSizePixelsX > 10 * 40)
1202             {
1203                 int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
1204                 this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
1205                 this.gridSizeX = this.gridSizeX / 10;
1206                 gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
1207                 if (count++ > 10)
1208                 {
1209                     break;
1210                 }
1211             }
1212 
1213             double gridSizePixelsY = this.gridSizeY / scaleY;
1214             while (gridSizePixelsY < 40)
1215             {
1216                 this.gridSizeY = 10 * this.gridSizeY;
1217                 int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
1218                 this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
1219                 gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
1220                 if (count++ > 10)
1221                 {
1222                     break;
1223                 }
1224             }
1225 
1226             count = 0;
1227             while (gridSizePixelsY > 10 * 40)
1228             {
1229                 int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
1230                 this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
1231                 this.gridSizeY = this.gridSizeY / 10;
1232                 gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
1233                 if (count++ > 10)
1234                 {
1235                     break;
1236                 }
1237             }
1238 
1239             // Let's draw the vertical lines
1240             double mod = this.getExtent().getMinX() % this.gridSizeX;
1241             double x = -mod / scaleX;
1242             while (x < this.getWidth())
1243             {
1244                 Point2d point = this.getRenderableScale().getWorldCoordinates(new Point2D.Double(x, 0), this.getExtent(),
1245                         this.getSize());
1246                 if (point != null)
1247                 {
1248                     String label = this.formatter.format(Math.round(point.getX() / this.gridSizeX) * this.gridSizeX);
1249                     double labelWidth = this.getFontMetrics(this.getFont()).getStringBounds(label, g).getWidth();
1250                     if (x > labelWidth + 4)
1251                     {
1252                         int xInt = (int) Math.round(x);
1253                         g.drawLine(xInt, 15, xInt, this.getHeight());
1254                         g.drawString(label, (int) Math.round(x - 0.5 * labelWidth), 11);
1255                     }
1256                 }
1257                 x = x + gridSizePixelsX;
1258             }
1259 
1260             // Let's draw the horizontal lines
1261             mod = this.getExtent().getMinY() % this.gridSizeY;
1262             double y = this.getSize().getHeight() + (mod / scaleY);
1263             while (y > 15)
1264             {
1265                 Point2d point = this.getRenderableScale().getWorldCoordinates(new Point2D.Double(0, y), this.getExtent(),
1266                         this.getSize());
1267                 if (point != null)
1268                 {
1269                     String label = this.formatter.format(Math.round(point.getY() / this.gridSizeY) * this.gridSizeY);
1270                     RectangularShape labelBounds = this.getFontMetrics(this.getFont()).getStringBounds(label, g);
1271                     int yInt = (int) Math.round(y);
1272                     g.drawLine((int) Math.round(labelBounds.getWidth() + 4), yInt, this.getWidth(), yInt);
1273                     g.drawString(label, 2, (int) Math.round(y + labelBounds.getHeight() * 0.3));
1274                 }
1275                 y = y - gridSizePixelsY;
1276             }
1277         }
1278 
1279         @Override
1280         public String toString()
1281         {
1282             return "OtsAnimationPanel [network=" + this.network + "]";
1283         }
1284     }
1285 
1286     /**
1287      * Enum for demo panel position. Each value contains a field representing the position correlating to the
1288      * {@link BorderLayout} class.
1289      */
1290     public enum DemoPanelPosition
1291     {
1292 
1293         /** Top. */
1294         TOP("First"),
1295 
1296         /** Bottom. */
1297         BOTTOM("Last"),
1298 
1299         /** Left. */
1300         LEFT("Before"),
1301 
1302         /** Right. */
1303         RIGHT("After");
1304 
1305         /** Value used in {@link BorderLayout}. */
1306         private final String direction;
1307 
1308         /**
1309          * Constructor.
1310          * @param direction value used in {@link BorderLayout}
1311          */
1312         DemoPanelPosition(final String direction)
1313         {
1314             this.direction = direction;
1315         }
1316 
1317         /**
1318          * Return border layout position.
1319          * @return value used in {@link BorderLayout}
1320          */
1321         public String getBorderLayoutPosition()
1322         {
1323             return this.direction;
1324         }
1325 
1326     }
1327 
1328 }