View Javadoc
1   package org.opentrafficsim.editor.extensions.map;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Color;
5   import java.awt.Component;
6   import java.awt.Dimension;
7   import java.awt.event.ActionEvent;
8   import java.awt.event.ActionListener;
9   import java.io.IOException;
10  import java.rmi.RemoteException;
11  import java.util.Iterator;
12  import java.util.LinkedHashMap;
13  import java.util.LinkedHashSet;
14  import java.util.Map;
15  import java.util.Set;
16  import java.util.WeakHashMap;
17  import java.util.function.Function;
18  
19  import javax.naming.NamingException;
20  import javax.swing.Box;
21  import javax.swing.Box.Filler;
22  import javax.swing.BoxLayout;
23  import javax.swing.ButtonGroup;
24  import javax.swing.ButtonModel;
25  import javax.swing.DefaultComboBoxModel;
26  import javax.swing.Icon;
27  import javax.swing.JButton;
28  import javax.swing.JCheckBox;
29  import javax.swing.JComboBox;
30  import javax.swing.JLabel;
31  import javax.swing.JPanel;
32  import javax.swing.JToggleButton;
33  import javax.swing.SwingUtilities;
34  
35  import org.djutils.draw.bounds.Bounds2d;
36  import org.djutils.event.Event;
37  import org.djutils.event.EventListener;
38  import org.djutils.event.reference.ReferenceType;
39  import org.djutils.exceptions.Try;
40  import org.opentrafficsim.core.geometry.Flattener;
41  import org.opentrafficsim.core.geometry.Flattener.NumSegments;
42  import org.opentrafficsim.draw.network.LinkAnimation;
43  import org.opentrafficsim.draw.network.LinkAnimation.LinkData;
44  import org.opentrafficsim.draw.network.NodeAnimation;
45  import org.opentrafficsim.draw.network.NodeAnimation.NodeData;
46  import org.opentrafficsim.draw.road.BusStopAnimation;
47  import org.opentrafficsim.draw.road.BusStopAnimation.BusStopData;
48  import org.opentrafficsim.draw.road.CrossSectionElementAnimation.ShoulderData;
49  import org.opentrafficsim.draw.road.GtuGeneratorPositionAnimation;
50  import org.opentrafficsim.draw.road.GtuGeneratorPositionAnimation.GtuGeneratorPositionData;
51  import org.opentrafficsim.draw.road.LaneAnimation;
52  import org.opentrafficsim.draw.road.LaneAnimation.CenterLine;
53  import org.opentrafficsim.draw.road.LaneAnimation.LaneData;
54  import org.opentrafficsim.draw.road.LaneDetectorAnimation;
55  import org.opentrafficsim.draw.road.LaneDetectorAnimation.LoopDetectorData;
56  import org.opentrafficsim.draw.road.LaneDetectorAnimation.SinkData;
57  import org.opentrafficsim.draw.road.LaneDetectorAnimation.SinkData.SinkText;
58  import org.opentrafficsim.draw.road.PriorityAnimation.PriorityData;
59  import org.opentrafficsim.draw.road.StripeAnimation.StripeData;
60  import org.opentrafficsim.draw.road.TrafficLightAnimation;
61  import org.opentrafficsim.draw.road.TrafficLightAnimation.TrafficLightData;
62  import org.opentrafficsim.editor.OtsEditor;
63  import org.opentrafficsim.editor.XsdPaths;
64  import org.opentrafficsim.editor.XsdTreeNode;
65  import org.opentrafficsim.editor.XsdTreeNodeRoot;
66  import org.opentrafficsim.swing.gui.AppearanceControlComboBox;
67  import org.opentrafficsim.swing.gui.OtsControlPanel;
68  
69  import nl.tudelft.simulation.dsol.animation.Locatable;
70  import nl.tudelft.simulation.dsol.animation.d2.Renderable2d;
71  import nl.tudelft.simulation.dsol.animation.d2.RenderableScale;
72  import nl.tudelft.simulation.dsol.swing.animation.d2.AnimationUpdaterThread;
73  import nl.tudelft.simulation.dsol.swing.animation.d2.VisualizationPanel;
74  import nl.tudelft.simulation.naming.context.ContextInterface;
75  import nl.tudelft.simulation.naming.context.Contextualized;
76  import nl.tudelft.simulation.naming.context.JvmContext;
77  
78  /**
79   * Editor map.
80   * <p>
81   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
82   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
83   * </p>
84   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
85   */
86  public class EditorMap extends JPanel implements EventListener
87  {
88  
89      /** */
90      private static final long serialVersionUID = 20231010L;
91  
92      /** Color for toolbar and toggle bar. */
93      private static final Color BAR_COLOR = Color.LIGHT_GRAY;
94  
95      /** All types that are valid to show in the map. */
96      private static final Set<String> TYPES = Set.of(XsdPaths.NODE, XsdPaths.LINK, XsdPaths.TRAFFIC_LIGHT, XsdPaths.SINK,
97              XsdPaths.GENERATOR, XsdPaths.LIST_GENERATOR);
98  
99      /** Context provider. */
100     private final Contextualized contextualized;
101 
102     /** Editor. */
103     private final OtsEditor editor;
104 
105     /** Panel to draw in. */
106     private final VisualizationPanel animationPanel;
107 
108     /** Panel with tools. */
109     private final JPanel toolPanel;
110 
111     /** Panel with toggles. */
112     private final JPanel togglePanel;
113 
114     /** All map data's drawn (or hidden as they are invalid). */
115     private final LinkedHashMap<XsdTreeNode, MapData> datas = new LinkedHashMap<>();
116 
117     /** Weak references to all created link data's. */
118     private final WeakHashMap<MapLinkData, Object> links = new WeakHashMap<>();
119 
120     /** Listeners to road layouts. */
121     private final LinkedHashMap<XsdTreeNode, RoadLayoutListener> roadLayoutListeners = new LinkedHashMap<>();
122 
123     /** Listener to flattener at network level. */
124     private FlattenerListener networkFlattenerListener;
125 
126     /** Animation objects of all data's drawn. */
127     private final LinkedHashMap<XsdTreeNode, Renderable2d<?>> animations = new LinkedHashMap<>();
128 
129     /** Whether we can ignore maintaining the scale. */
130     private boolean ignoreKeepScale = false;
131 
132     /** Last x-scale. */
133     private Double lastXScale = null;
134 
135     /** Last y-scale. */
136     private Double lastYScale = null;
137 
138     /** Last screen size. */
139     private Dimension lastScreen = null;
140 
141     /** Map of toggle names to toggle animation classes. */
142     private Map<String, Class<? extends Locatable>> toggleLocatableMap = new LinkedHashMap<>();
143 
144     /**
145      * Constructor.
146      * @param animator AnimationUpdaterThread; thread for frequent painting.
147      * @param contextualized Contextualized; context provider.
148      * @param editor OtsEditor; editor.
149      * @throws RemoteException context binding problem.
150      * @throws NamingException context binding problem.
151      */
152     private EditorMap(final AnimationUpdaterThread animator, final Contextualized contextualized, final OtsEditor editor)
153             throws RemoteException, NamingException
154     {
155         super(new BorderLayout());
156         this.contextualized = contextualized;
157         this.editor = editor;
158         this.animationPanel = new VisualizationPanel(new Bounds2d(500, 500), animator, contextualized.getContext())
159         {
160             /** */
161             private static final long serialVersionUID = 20231016L;
162 
163             /** {@inheritDoc} */
164             @Override
165             public void setExtent(final Bounds2d extent)
166             {
167                 if (EditorMap.this.lastScreen != null)
168                 {
169                     // this prevents zoom being undone when resizing the screen afterwards
170                     EditorMap.this.lastXScale = this.getRenderableScale().getXScale(extent, EditorMap.this.lastScreen);
171                     EditorMap.this.lastYScale = this.getRenderableScale().getYScale(extent, EditorMap.this.lastScreen);
172                 }
173                 super.setExtent(extent);
174             }
175 
176             /** {@inheritDoc} */
177             @Override
178             public synchronized void zoomAll()
179             {
180                 EditorMap.this.ignoreKeepScale = true;
181                 Bounds2d extent = EditorMap.this.animationPanel.fullExtent();
182                 if (Double.isFinite(extent.getMaxX()))
183                 {
184                     super.zoomAll();
185                 }
186                 else if (getSize().height != 0)
187                 {
188                     // there are no objects
189                     super.home();
190                 }
191                 EditorMap.this.ignoreKeepScale = false;
192             }
193 
194             /** {@inheritDoc} */
195             @Override
196             public synchronized void home()
197             {
198                 EditorMap.this.ignoreKeepScale = true;
199                 super.home();
200                 EditorMap.this.ignoreKeepScale = false;
201             }
202         };
203         this.animationPanel.setBackground(Color.GRAY);
204         this.animationPanel.setShowToolTip(false);
205         editor.addListener(this, OtsEditor.NEW_FILE);
206         this.animationPanel.setRenderableScale(new RenderableScale()
207         {
208             /** {@inheritDoc} */
209             @Override
210             public Bounds2d computeVisibleExtent(final Bounds2d extent, final Dimension screen)
211             {
212                 if (EditorMap.this.ignoreKeepScale)
213                 {
214                     return super.computeVisibleExtent(extent, screen);
215                 }
216                 // overridden to preserve zoom scale, otherwise dragging the split screen may pump up the zoom factor
217                 double xScale = getXScale(extent, screen);
218                 double yScale = getYScale(extent, screen);
219                 Bounds2d result;
220                 if (EditorMap.this.lastYScale != null && yScale == EditorMap.this.lastYScale)
221                 {
222                     result = new Bounds2d(extent.midPoint().getX() - 0.5 * screen.getWidth() * yScale,
223                             extent.midPoint().getX() + 0.5 * screen.getWidth() * yScale, extent.getMinY(), extent.getMaxY());
224                     xScale = yScale;
225                 }
226                 else if (EditorMap.this.lastXScale != null && xScale == EditorMap.this.lastXScale)
227                 {
228                     result = new Bounds2d(extent.getMinX(), extent.getMaxX(),
229                             extent.midPoint().getY() - 0.5 * screen.getHeight() * xScale * getYScaleRatio(),
230                             extent.midPoint().getY() + 0.5 * screen.getHeight() * xScale * getYScaleRatio());
231                     yScale = xScale;
232                 }
233                 else
234                 {
235                     double scale = EditorMap.this.lastXScale == null ? Math.min(xScale, yScale)
236                             : EditorMap.this.lastXScale * EditorMap.this.lastScreen.getWidth() / screen.getWidth();
237                     result = new Bounds2d(extent.midPoint().getX() - 0.5 * screen.getWidth() * scale,
238                             extent.midPoint().getX() + 0.5 * screen.getWidth() * scale,
239                             extent.midPoint().getY() - 0.5 * screen.getHeight() * scale * getYScaleRatio(),
240                             extent.midPoint().getY() + 0.5 * screen.getHeight() * scale * getYScaleRatio());
241                     yScale = scale;
242                     xScale = scale;
243                 }
244                 EditorMap.this.lastXScale = xScale;
245                 EditorMap.this.lastYScale = yScale;
246                 EditorMap.this.lastScreen = screen;
247                 return result;
248             }
249         });
250         add(this.animationPanel, BorderLayout.CENTER);
251 
252         this.toolPanel = new JPanel();
253         setupTools();
254 
255         this.togglePanel = new JPanel();
256         this.togglePanel.setBackground(BAR_COLOR);
257         setAnimationToggles();
258         this.togglePanel.setLayout(new BoxLayout(this.togglePanel, BoxLayout.Y_AXIS));
259         add(this.togglePanel, BorderLayout.WEST);
260     }
261 
262     /**
263      * Sets up all the tool in the tool panel on top.
264      */
265     private void setupTools()
266     {
267         this.toolPanel.setBackground(BAR_COLOR);
268         this.toolPanel.setMinimumSize(new Dimension(350, 28));
269         this.toolPanel.setPreferredSize(new Dimension(350, 28));
270         this.toolPanel.setLayout(new BoxLayout(this.toolPanel, BoxLayout.X_AXIS));
271 
272         this.toolPanel.add(Box.createHorizontalStrut(5));
273         this.toolPanel.add(new JLabel("Add tools:"));
274 
275         this.toolPanel.add(Box.createHorizontalStrut(5));
276         // button group that allows no selection when toggling the currently selected
277         ButtonGroup group = new ButtonGroup()
278         {
279             /** */
280             private static final long serialVersionUID = 20240227L;
281 
282             /** {@inheritDoc} */
283             @Override
284             public void setSelected(final ButtonModel model, final boolean selected)
285             {
286                 if (selected)
287                 {
288                     super.setSelected(model, selected);
289                 }
290                 else
291                 {
292                     clearSelection();
293                 }
294             }
295         };
296 
297         JToggleButton nodeButton = new JToggleButton(loadIcon("./OTS_node.png"));
298         nodeButton.setPreferredSize(new Dimension(24, 24));
299         nodeButton.setMinimumSize(new Dimension(24, 24));
300         nodeButton.setMaximumSize(new Dimension(24, 24));
301         nodeButton.setToolTipText("Add node");
302         group.add(nodeButton);
303         this.toolPanel.add(nodeButton);
304 
305         JToggleButton linkButton = new JToggleButton(loadIcon("./OTS_link.png"));
306         linkButton.setPreferredSize(new Dimension(24, 24));
307         linkButton.setMinimumSize(new Dimension(24, 24));
308         linkButton.setMaximumSize(new Dimension(24, 24));
309         linkButton.setToolTipText("Add link");
310         group.add(linkButton);
311         this.toolPanel.add(linkButton);
312 
313         JToggleButton centroidButton = new JToggleButton(loadIcon("./OTS_centroid.png"));
314         centroidButton.setPreferredSize(new Dimension(24, 24));
315         centroidButton.setMinimumSize(new Dimension(24, 24));
316         centroidButton.setMaximumSize(new Dimension(24, 24));
317         centroidButton.setToolTipText("Add centroid");
318         group.add(centroidButton);
319         this.toolPanel.add(centroidButton);
320 
321         JToggleButton connectorButton = new JToggleButton(loadIcon("./OTS_connector.png"));
322         connectorButton.setPreferredSize(new Dimension(24, 24));
323         connectorButton.setMinimumSize(new Dimension(24, 24));
324         connectorButton.setMaximumSize(new Dimension(24, 24));
325         connectorButton.setToolTipText("Add connector");
326         group.add(connectorButton);
327         this.toolPanel.add(connectorButton);
328 
329         this.toolPanel.add(Box.createHorizontalStrut(5));
330         JComboBox<String> shape = new AppearanceControlComboBox<>();
331         shape.setModel(new DefaultComboBoxModel<>(new String[] {"Straight", "Bezier", "Clothoid", "Arc", "PolyLine"}));
332         shape.setMinimumSize(new Dimension(50, 22));
333         shape.setMaximumSize(new Dimension(90, 22));
334         shape.setPreferredSize(new Dimension(90, 22));
335         shape.setToolTipText("Standard shape for new links");
336         this.toolPanel.add(shape);
337 
338         this.toolPanel.add(Box.createHorizontalStrut(5));
339         JComboBox<String> roadLayout = new AppearanceControlComboBox<>();
340         roadLayout.setModel(new DefaultComboBoxModel<>(new String[] {}));
341         roadLayout.setMinimumSize(new Dimension(50, 22));
342         roadLayout.setMaximumSize(new Dimension(125, 22));
343         roadLayout.setPreferredSize(new Dimension(125, 22));
344         roadLayout.setToolTipText("Standard defined road layout for new links");
345         roadLayout.setEnabled(false);
346         this.toolPanel.add(roadLayout);
347 
348         this.toolPanel.add(Box.createHorizontalStrut(5));
349         JComboBox<String> linkType = new AppearanceControlComboBox<>();
350         linkType.setModel(new DefaultComboBoxModel<>(new String[] {}));
351         linkType.setMinimumSize(new Dimension(50, 22));
352         linkType.setMaximumSize(new Dimension(125, 22));
353         linkType.setPreferredSize(new Dimension(125, 22));
354         linkType.setToolTipText("Standard link type for new links and connectors");
355         linkType.setEnabled(false);
356         this.toolPanel.add(linkType);
357 
358         this.toolPanel.add(Box.createHorizontalStrut(5));
359         Dimension minDim = new Dimension(0, 1);
360         Dimension prefDim = new Dimension(0, 1);
361         Dimension maxDim = new Dimension(5000, 1);
362         this.toolPanel.add(new Filler(minDim, prefDim, maxDim)); // pushes further elements right aligned
363 
364         this.toolPanel.add(new JLabel("Show:"));
365 
366         this.toolPanel.add(Box.createHorizontalStrut(5));
367         JButton extent = new JButton(loadIcon("./Expand.png"));
368         extent.setMinimumSize(new Dimension(24, 24));
369         extent.setMaximumSize(new Dimension(24, 24));
370         extent.setPreferredSize(new Dimension(24, 24));
371         extent.setToolTipText("Zoom whole network");
372         extent.addActionListener((e) -> this.animationPanel.zoomAll());
373         this.toolPanel.add(extent);
374 
375         JButton grid = new JButton(loadIcon("./Grid.png"));
376         grid.setMinimumSize(new Dimension(24, 24));
377         grid.setMaximumSize(new Dimension(24, 24));
378         grid.setPreferredSize(new Dimension(24, 24));
379         grid.setToolTipText("Toggle grid on/off");
380         grid.addActionListener((e) -> this.animationPanel.setShowGrid(!this.animationPanel.isShowGrid()));
381         this.toolPanel.add(grid);
382 
383         this.toolPanel.add(Box.createHorizontalStrut(5));
384 
385         add(this.toolPanel, BorderLayout.NORTH);
386     }
387 
388     /**
389      * Loads an icon from the given file. Returns {@code null} if the file can not be found.
390      * @param file String; file.
391      * @return Icon; icon.
392      */
393     private Icon loadIcon(final String file)
394     {
395         try
396         {
397             return OtsEditor.loadIcon(file, 16, 16, -1, -1);
398         }
399         catch (IOException ioe)
400         {
401             // skip loading icon
402             return null;
403         }
404     }
405 
406     /**
407      * Sets the animation toggles as useful for in the editor.
408      */
409     private void setAnimationToggles()
410     {
411         addToggle("Node", NodeData.class, "/icons/Node24.png", "Show/hide nodes", true, false);
412         addToggle("NodeId", NodeAnimation.Text.class, "/icons/Id24.png", "Show/hide node ids", false, true);
413         addToggle("Link", LinkData.class, "/icons/Link24.png", "Show/hide links", true, false);
414         addToggle("LinkId", LinkAnimation.Text.class, "/icons/Id24.png", "Show/hide link ids", false, true);
415         addToggle("Priority", PriorityData.class, "/icons/Priority24.png", "Show/hide link priority", true, false);
416         addToggle("Lane", LaneData.class, "/icons/Lane24.png", "Show/hide lanes", true, false);
417         addToggle("LaneId", LaneAnimation.Text.class, "/icons/Id24.png", "Show/hide lane ids", false, true);
418         addToggle("LaneCenter", CenterLine.class, "/icons/CenterLine24.png", "Show/hide lane center lines", false, false);
419         addToggle("Stripe", StripeData.class, "/icons/Stripe24.png", "Show/hide stripes", true, false);
420         addToggle("Shoulder", ShoulderData.class, "/icons/Shoulder24.png", "Show/hide shoulders", true, false);
421         // TODO: perhaps a specific data type for generators?
422         addToggle("Generator", GtuGeneratorPositionData.class, "/icons/Generator24.png", "Show/hide generators", true, false);
423         addToggle("Sink", SinkData.class, "/icons/Sink24.png", "Show/hide sinks", true, true);
424         addToggle("Detector", LoopDetectorData.class, "/icons/Detector24.png", "Show/hide loop detectors", true, false);
425         addToggle("DetectorId", LoopDetectorData.Text.class, "/icons/Id24.png", "Show/hide loop detector ids", false, true);
426         addToggle("Light", TrafficLightData.class, "/icons/TrafficLight24.png", "Show/hide traffic lights", true, false);
427         addToggle("LightId", TrafficLightAnimation.Text.class, "/icons/Id24.png", "Show/hide traffic light ids", false, true);
428         addToggle("Bus", BusStopData.class, "/icons/BusStop24.png", "Show/hide bus stops", true, false);
429         addToggle("BusId", BusStopAnimation.Text.class, "/icons/Id24.png", "Show/hide bus stop ids", false, true);
430     }
431 
432     /**
433      * Add a button for toggling an animatable class on or off. Button icons for which 'idButton' is true will be placed to the
434      * right of the previous button, which should be the corresponding button without the id. An example is an icon for
435      * showing/hiding the class 'Lane' followed by the button to show/hide the Lane ids.
436      * @param name String; the name of the button
437      * @param locatableClass Class&lt;? extends Locatable&gt;; the class for which the button holds (e.g., GTU.class)
438      * @param iconPath String; the path to the 24x24 icon to display
439      * @param toolTipText String; the tool tip text to show when hovering over the button
440      * @param initiallyVisible boolean; whether the class is initially shown or not
441      * @param idButton boolean; id button that needs to be placed next to the previous button
442      */
443     public final void addToggle(final String name, final Class<? extends Locatable> locatableClass, final String iconPath,
444             final String toolTipText, final boolean initiallyVisible, final boolean idButton)
445     {
446         JToggleButton button;
447         Icon icon = OtsControlPanel.loadIcon(iconPath);
448         Icon unIcon = OtsControlPanel.loadGrayscaleIcon(iconPath);
449         button = new JCheckBox();
450         button.setSelectedIcon(icon);
451         button.setIcon(unIcon);
452         button.setPreferredSize(new Dimension(32, 28));
453         button.setName(name);
454         button.setEnabled(true);
455         button.setSelected(initiallyVisible);
456         button.setActionCommand(name);
457         button.setToolTipText(toolTipText);
458         button.addActionListener(new ActionListener()
459         {
460             /** {@inheritDoc} */
461             @Override
462             public void actionPerformed(final ActionEvent e)
463             {
464                 String actionCommand = e.getActionCommand();
465                 if (EditorMap.this.toggleLocatableMap.containsKey(actionCommand))
466                 {
467                     Class<? extends Locatable> locatableClass = EditorMap.this.toggleLocatableMap.get(actionCommand);
468                     EditorMap.this.animationPanel.toggleClass(locatableClass);
469                     EditorMap.this.togglePanel.repaint();
470                 }
471             }
472         });
473 
474         // place an Id button to the right of the corresponding content button
475         if (idButton && this.togglePanel.getComponentCount() > 0)
476         {
477             JPanel lastToggleBox = (JPanel) this.togglePanel.getComponent(this.togglePanel.getComponentCount() - 1);
478             lastToggleBox.add(button);
479         }
480         else
481         {
482             JPanel toggleBox = new JPanel();
483             toggleBox.setLayout(new BoxLayout(toggleBox, BoxLayout.X_AXIS));
484             toggleBox.add(button);
485             this.togglePanel.add(toggleBox);
486             toggleBox.setAlignmentX(Component.LEFT_ALIGNMENT);
487         }
488 
489         if (initiallyVisible)
490         {
491             this.animationPanel.showClass(locatableClass);
492         }
493         else
494         {
495             this.animationPanel.hideClass(locatableClass);
496         }
497         this.toggleLocatableMap.put(name, locatableClass);
498     }
499 
500     /**
501      * Builds a map panel with an animator and context.
502      * @param editor OtsEditor; editor.
503      * @return Map; map.
504      * @throws RemoteException context binding problem.
505      * @throws NamingException context binding problem.
506      */
507     public static EditorMap build(final OtsEditor editor) throws RemoteException, NamingException
508     {
509         AnimationUpdaterThread animator = new AnimationUpdaterThread();
510         animator.start();
511         ContextInterface context = new JvmContext("ots-context");
512         Contextualized contextualized = new Contextualized()
513         {
514             /** {@inheritDoc} */
515             @Override
516             public ContextInterface getContext()
517             {
518                 return context;
519             }
520         };
521         return new EditorMap(animator, contextualized, editor);
522     }
523 
524     /** {@inheritDoc} */
525     @Override
526     public void notify(final Event event) throws RemoteException
527     {
528         if (event.getType().equals(OtsEditor.NEW_FILE))
529         {
530             for (XsdTreeNode node : new LinkedHashSet<>(this.datas.keySet()))
531             {
532                 remove(node);
533             }
534             this.datas.clear();
535             this.links.clear();
536             for (Renderable2d<?> animation : this.animations.values())
537             {
538                 animation.destroy(this.contextualized);
539                 removeAnimation(animation);
540             }
541             this.animations.clear();
542             for (RoadLayoutListener roadLayoutListener : this.roadLayoutListeners.values())
543             {
544                 roadLayoutListener.destroy();
545             }
546             this.roadLayoutListeners.clear();
547             if (this.networkFlattenerListener != null)
548             {
549                 this.networkFlattenerListener.destroy();
550                 this.networkFlattenerListener = null;
551             }
552             XsdTreeNodeRoot root = (XsdTreeNodeRoot) event.getContent();
553             root.addListener(this, XsdTreeNodeRoot.NODE_CREATED);
554             root.addListener(this, XsdTreeNodeRoot.NODE_REMOVED);
555             SwingUtilities.invokeLater(() -> this.animationPanel.zoomAll());
556         }
557         else if (event.getType().equals(XsdTreeNodeRoot.NODE_CREATED))
558         {
559             Object[] content = (Object[]) event.getContent();
560             XsdTreeNode node = (XsdTreeNode) content[0];
561             if (isType(node))
562             {
563                 node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
564                 node.addListener(this, XsdTreeNode.OPTION_CHANGED);
565                 if (node.isActive())
566                 {
567                     add(node);
568                 }
569             }
570             else if (node.getPathString().equals(XsdPaths.POLYLINE_COORDINATE))
571             {
572                 for (MapLinkData linkData : this.links.keySet())
573                 {
574                     linkData.addCoordinate(node);
575                 }
576             }
577             else if (node.getPathString().equals(XsdPaths.DEFINED_ROADLAYOUT))
578             {
579                 addRoadLayout(node);
580             }
581             else if (node.getPathString().equals(XsdPaths.NETWORK + ".Flattener"))
582             {
583                 setNetworkFlattener(node);
584             }
585         }
586         else if (event.getType().equals(XsdTreeNodeRoot.NODE_REMOVED))
587         {
588             Object[] content = (Object[]) event.getContent();
589             XsdTreeNode node = (XsdTreeNode) content[0];
590             if (this.datas.containsKey(node)) // node.isType does not work as parent is gone, i.e. type is just "Node"
591             {
592                 remove(node);
593             }
594             else if (node.getPathString().equals(XsdPaths.POLYLINE_COORDINATE))
595             {
596                 for (MapLinkData linkData : this.links.keySet())
597                 {
598                     linkData.removeCoordinate(node);
599                 }
600             }
601             else if (node.getPathString().equals(XsdPaths.DEFINED_ROADLAYOUT))
602             {
603                 removeRoadLayout(node);
604             }
605             else if (node.getPathString().equals(XsdPaths.NETWORK + ".Flattener"))
606             {
607                 removeNetworkFlattener();
608             }
609         }
610         else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
611         {
612             Object[] content = (Object[]) event.getContent();
613             XsdTreeNode node = (XsdTreeNode) content[0];
614             if (isType(node))
615             {
616                 if ((boolean) content[1])
617                 {
618                     add(node);
619                 }
620                 else
621                 {
622                     remove(node);
623                 }
624             }
625             else if (node.getPathString().equals(XsdPaths.DEFINED_ROADLAYOUT))
626             {
627                 if ((boolean) content[1])
628                 {
629                     addRoadLayout(node);
630                 }
631                 else
632                 {
633                     removeRoadLayout(node);
634                 }
635             }
636             else if (node.getPathString().equals(XsdPaths.NETWORK + ".Flattener"))
637             {
638                 if ((boolean) content[1])
639                 {
640                     setNetworkFlattener(node);
641                 }
642                 else
643                 {
644                     removeNetworkFlattener();
645                 }
646             }
647         }
648         else if (event.getType().equals(XsdTreeNode.OPTION_CHANGED))
649         {
650             Object[] content = (Object[]) event.getContent();
651             XsdTreeNode node = (XsdTreeNode) content[0];
652             XsdTreeNode selected = (XsdTreeNode) content[1];
653             XsdTreeNode previous = (XsdTreeNode) content[2];
654             if (node.equals(selected))
655             {
656                 if (isType(previous))
657                 {
658                     remove(previous);
659                 }
660                 if (isType(selected) && selected.isActive())
661                 {
662                     add(selected);
663                 }
664             }
665         }
666         else if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED))
667         {
668             for (MapLinkData linkData : this.links.keySet())
669             {
670                 linkData.notifyNodeIdChanged(linkData.getNode());
671             }
672         }
673     }
674 
675     /**
676      * Returns whether the node is any of the visualized types.
677      * @param node XsdTreeNode; node.
678      * @return boolean; whether the node is any of the visualized types.
679      */
680     private boolean isType(final XsdTreeNode node)
681     {
682         for (String type : TYPES)
683         {
684             if (node.isType(type))
685             {
686                 return true;
687             }
688         }
689         return false;
690     }
691 
692     /**
693      * Set the data as being valid to draw.
694      * @param data MapData; data that is valid to draw.
695      */
696     public void setValid(final MapData data)
697     {
698         XsdTreeNode node = data.getNode();
699         if (this.animations.containsKey(node))
700         {
701             return;
702         }
703         Renderable2d<?> animation;
704         if (node.getPathString().equals(XsdPaths.NODE))
705         {
706             animation = Try.assign(() -> new NodeAnimation((MapNodeData) data, this.contextualized), "");
707         }
708         else if (node.getPathString().equals(XsdPaths.LINK))
709         {
710             animation = Try.assign(() -> new LinkAnimation((MapLinkData) data, this.contextualized, 0.5f).setDynamic(true), "");
711         }
712         else if (node.getPathString().equals(XsdPaths.TRAFFIC_LIGHT))
713         {
714             animation = Try.assign(() -> new TrafficLightAnimation((MapTrafficLightData) data, this.contextualized), "");
715         }
716         else if (node.getPathString().equals(XsdPaths.SINK))
717         {
718             Function<LaneDetectorAnimation<SinkData, SinkText>, SinkText> textSupplier = (s) -> Try
719                     .assign(() -> new SinkText(s.getSource(), (float) s.getHalfLength() + 0.2f, this.contextualized), "");
720             animation = Try.assign(() -> new LaneDetectorAnimation<SinkData, SinkText>((SinkData) data, this.contextualized,
721                     Color.ORANGE, textSupplier), "");
722         }
723         else if (node.getPathString().equals(XsdPaths.GENERATOR) || node.getPathString().equals(XsdPaths.LIST_GENERATOR))
724         {
725             animation = Try
726                     .assign(() -> new GtuGeneratorPositionAnimation((GtuGeneratorPositionData) data, this.contextualized), "");
727         }
728         else
729         {
730             throw new UnsupportedOperationException("Data cannot be added by the map editor.");
731         }
732         this.animations.put(node, animation);
733     }
734 
735     /**
736      * Set the data as being invalid to draw.
737      * @param data MapData; data that is invalid to draw.
738      */
739     // TODO: for some reason, this does not work... because data remains in JVM?
740     public void setInvalid(final MapData data)
741     {
742         //
743     }
744 
745     /**
746      * Adds a data representation of the node. This will not yet be drawn until the data object itself tells the map it is valid
747      * to be drawn.
748      * @param node XsdTreeNode; node of element to draw.
749      * @throws RemoteException context binding problem.
750      */
751     private void add(final XsdTreeNode node) throws RemoteException
752     {
753         MapData data;
754         if (this.datas.containsKey(node))
755         {
756             return; // activated choice
757         }
758         if (node.getPathString().equals(XsdPaths.NODE))
759         {
760             data = new MapNodeData(this, node, this.editor);
761             node.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
762         }
763         else if (node.getPathString().equals(XsdPaths.LINK))
764         {
765             MapLinkData linkData = new MapLinkData(this, node, this.editor);
766             data = linkData;
767             for (RoadLayoutListener roadLayoutListener : this.roadLayoutListeners.values())
768             {
769                 roadLayoutListener.addListener(linkData, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
770             }
771             if (this.networkFlattenerListener != null)
772             {
773                 this.networkFlattenerListener.addListener(linkData, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
774             }
775             this.links.put(linkData, null);
776         }
777         else if (node.getPathString().equals(XsdPaths.TRAFFIC_LIGHT))
778         {
779             MapTrafficLightData trafficLightData = new MapTrafficLightData(this, node, this.editor);
780             data = trafficLightData;
781         }
782         else if (node.getPathString().equals(XsdPaths.SINK))
783         {
784             MapSinkData sinkData = new MapSinkData(this, node, this.editor);
785             data = sinkData;
786         }
787         else if (node.getPathString().equals(XsdPaths.GENERATOR) || node.getPathString().equals(XsdPaths.LIST_GENERATOR))
788         {
789             MapGeneratorData generatorData = new MapGeneratorData(this, node, this.editor);
790             data = generatorData;
791         }
792         else
793         {
794             throw new UnsupportedOperationException("Node cannot be added by the map editor.");
795         }
796         this.datas.put(node, data);
797     }
798 
799     /**
800      * Remove the drawing data of pertaining to the node.
801      * @param node XsdTreeNode; node.
802      */
803     private void remove(final XsdTreeNode node)
804     {
805         if (node.getPathString().equals(XsdPaths.NODE))
806         {
807             node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
808         }
809         if (node.getPathString().equals(XsdPaths.LINK))
810         {
811             Iterator<MapLinkData> it = this.links.keySet().iterator();
812             while (it.hasNext())
813             {
814                 MapLinkData link = it.next();
815                 if (link.getNode().equals(node))
816                 {
817                     for (RoadLayoutListener roadLayoutListener : this.roadLayoutListeners.values())
818                     {
819                         roadLayoutListener.removeListener(link, ChangeListener.CHANGE_EVENT);
820                     }
821                     if (this.networkFlattenerListener != null)
822                     {
823                         this.networkFlattenerListener.removeListener(link, ChangeListener.CHANGE_EVENT);
824                     }
825                     it.remove();
826                     break;
827                 }
828             }
829         }
830         MapData data = this.datas.remove(node);
831         if (data != null)
832         {
833             data.destroy();
834         }
835         removeAnimation(this.animations.remove(node));
836     }
837 
838     /**
839      * Reinitialize animation on object who's animator stores static information that depends on something that was changed.
840      * This will create a new animation object. Only data objects that know their animations have static data, should call this.
841      * And only when information changed on which the static data depends.
842      * @param node XsdTreeNode; node.
843      */
844     public void reinitialize(final XsdTreeNode node)
845     {
846         remove(node);
847         Try.execute(() -> add(node), RuntimeException.class, "Unable to bind to context.");
848     }
849 
850     /**
851      * Returns the map data of the given XSD node.
852      * @param node XsdTreeNode; node.
853      * @return MapData; map data of the given XSD node, {@code null} if no such data.
854      */
855     public MapData getData(final XsdTreeNode node)
856     {
857         return this.datas.get(node);
858     }
859 
860     /**
861      * Add defined road layout.
862      * @param node XsdTreeNode; node of the defined road layout.
863      */
864     private void addRoadLayout(final XsdTreeNode node)
865     {
866         RoadLayoutListener roadLayoutListener = new RoadLayoutListener(node, () -> this.editor.getEval());
867         for (MapLinkData linkData : this.links.keySet())
868         {
869             roadLayoutListener.addListener(linkData, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
870         }
871         this.roadLayoutListeners.put(node, roadLayoutListener);
872     }
873 
874     /**
875      * Remove defined road layout.
876      * @param node XsdTreeNode; node of the defined road layout.
877      */
878     private void removeRoadLayout(final XsdTreeNode node)
879     {
880         RoadLayoutListener roadLayoutListener = this.roadLayoutListeners.remove(node);
881         roadLayoutListener.destroy();
882     }
883 
884     /**
885      * Sets the network level flattener.
886      * @param node XsdTreeNode; node of network flattener.
887      */
888     private void setNetworkFlattener(final XsdTreeNode node)
889     {
890         this.networkFlattenerListener = new FlattenerListener(node, () -> this.editor.getEval());
891         for (MapLinkData linkData : this.links.keySet())
892         {
893             this.networkFlattenerListener.addListener(linkData, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
894             this.editor.addEvalListener(this.networkFlattenerListener);
895         }
896         node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED, ReferenceType.WEAK);
897     }
898 
899     /**
900      * Removes the network flattener.
901      */
902     private void removeNetworkFlattener()
903     {
904         if (this.networkFlattenerListener != null)
905         {
906             this.editor.removeEvalListener(this.networkFlattenerListener);
907         }
908         this.networkFlattenerListener.destroy();
909         for (MapLinkData linkData : this.links.keySet())
910         {
911             Try.execute(() -> linkData.notify(new Event(ChangeListener.CHANGE_EVENT, this.networkFlattenerListener.getNode())),
912                     "Remove event exception.");
913         }
914         this.networkFlattenerListener = null;
915     }
916 
917     /**
918      * Returns the road layout listener from which a {@code MapLinkData} can obtain offsets.
919      * @param node XsdTreeNode; node of a defined layout.
920      * @return RoadLayoutListener; listener, can be used to obtain offsets.
921      */
922     RoadLayoutListener getRoadLayoutListener(final XsdTreeNode node)
923     {
924         return this.roadLayoutListeners.get(node);
925     }
926 
927     /**
928      * Remove animation.
929      * @param animation Renderable2d&lt;?&gt;; animation to remove.
930      */
931     void removeAnimation(final Renderable2d<?> animation)
932     {
933         if (animation != null)
934         {
935             this.animationPanel.objectRemoved(animation);
936             animation.destroy(this.contextualized);
937         }
938     }
939 
940     /**
941      * Returns the context.
942      * @return Contextualized; context.
943      */
944     Contextualized getContextualized()
945     {
946         return this.contextualized;
947     }
948 
949     /**
950      * Returns the network level flattener, or a 64 segment flattener of none specified.
951      * @return Flattener; flattener.
952      */
953     public Flattener getNetworkFlattener()
954     {
955         if (this.networkFlattenerListener != null)
956         {
957             Flattener flattener = this.networkFlattenerListener.getData();
958             if (flattener != null)
959             {
960                 return flattener; // otherwise, return default
961             }
962         }
963         return new NumSegments(64);
964     }
965 
966 }