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     /** Map of synchronizable stripes. */
145     private Map<MapStripeData, SynchronizableMapStripe> synStripes = new LinkedHashMap<>();
146     
147     /** Listeners to lane and stripe overrides. */
148     private final Map<XsdTreeNode, ChangeListener<Object>> overrideListeners = new LinkedHashMap<>();
149 
150     /**
151      * Constructor.
152      * @param animator thread for frequent painting.
153      * @param contextualized context provider.
154      * @param editor editor.
155      * @throws RemoteException context binding problem.
156      * @throws NamingException context binding problem.
157      */
158     private EditorMap(final AnimationUpdaterThread animator, final Contextualized contextualized, final OtsEditor editor)
159             throws RemoteException, NamingException
160     {
161         super(new BorderLayout());
162         this.contextualized = contextualized;
163         this.editor = editor;
164         this.animationPanel = new VisualizationPanel(new Bounds2d(500, 500), animator, contextualized.getContext())
165         {
166             /** */
167             private static final long serialVersionUID = 20231016L;
168 
169             @Override
170             public void setExtent(final Bounds2d extent)
171             {
172                 if (EditorMap.this.lastScreen != null)
173                 {
174                     // this prevents zoom being undone when resizing the screen afterwards
175                     EditorMap.this.lastXScale = this.getRenderableScale().getXScale(extent, EditorMap.this.lastScreen);
176                     EditorMap.this.lastYScale = this.getRenderableScale().getYScale(extent, EditorMap.this.lastScreen);
177                 }
178                 super.setExtent(extent);
179             }
180 
181             @Override
182             public synchronized void zoomAll()
183             {
184                 EditorMap.this.ignoreKeepScale = true;
185                 Bounds2d extent = EditorMap.this.animationPanel.fullExtent();
186                 if (Double.isFinite(extent.getMaxX()))
187                 {
188                     super.zoomAll();
189                 }
190                 else if (getSize().height != 0)
191                 {
192                     // there are no objects
193                     super.home();
194                 }
195                 EditorMap.this.ignoreKeepScale = false;
196             }
197 
198             @Override
199             public synchronized void home()
200             {
201                 EditorMap.this.ignoreKeepScale = true;
202                 super.home();
203                 EditorMap.this.ignoreKeepScale = false;
204             }
205         };
206         this.animationPanel.setBackground(Color.GRAY);
207         this.animationPanel.setShowToolTip(false);
208         editor.addListener(this, OtsEditor.NEW_FILE);
209         this.animationPanel.setRenderableScale(new RenderableScale()
210         {
211             @Override
212             public Bounds2d computeVisibleExtent(final Bounds2d extent, final Dimension screen)
213             {
214                 if (EditorMap.this.ignoreKeepScale)
215                 {
216                     return super.computeVisibleExtent(extent, screen);
217                 }
218                 // overridden to preserve zoom scale, otherwise dragging the split screen may pump up the zoom factor
219                 double xScale = getXScale(extent, screen);
220                 double yScale = getYScale(extent, screen);
221                 Bounds2d result;
222                 if (EditorMap.this.lastYScale != null && yScale == EditorMap.this.lastYScale)
223                 {
224                     result = new Bounds2d(extent.midPoint().getX() - 0.5 * screen.getWidth() * yScale,
225                             extent.midPoint().getX() + 0.5 * screen.getWidth() * yScale, extent.getMinY(), extent.getMaxY());
226                     xScale = yScale;
227                 }
228                 else if (EditorMap.this.lastXScale != null && xScale == EditorMap.this.lastXScale)
229                 {
230                     result = new Bounds2d(extent.getMinX(), extent.getMaxX(),
231                             extent.midPoint().getY() - 0.5 * screen.getHeight() * xScale * getYScaleRatio(),
232                             extent.midPoint().getY() + 0.5 * screen.getHeight() * xScale * getYScaleRatio());
233                     yScale = xScale;
234                 }
235                 else
236                 {
237                     double scale = EditorMap.this.lastXScale == null ? Math.min(xScale, yScale)
238                             : EditorMap.this.lastXScale * EditorMap.this.lastScreen.getWidth() / screen.getWidth();
239                     result = new Bounds2d(extent.midPoint().getX() - 0.5 * screen.getWidth() * scale,
240                             extent.midPoint().getX() + 0.5 * screen.getWidth() * scale,
241                             extent.midPoint().getY() - 0.5 * screen.getHeight() * scale * getYScaleRatio(),
242                             extent.midPoint().getY() + 0.5 * screen.getHeight() * scale * getYScaleRatio());
243                     yScale = scale;
244                     xScale = scale;
245                 }
246                 EditorMap.this.lastXScale = xScale;
247                 EditorMap.this.lastYScale = yScale;
248                 EditorMap.this.lastScreen = screen;
249                 return result;
250             }
251         });
252         add(this.animationPanel, BorderLayout.CENTER);
253 
254         this.toolPanel = new JPanel();
255         setupTools();
256 
257         this.togglePanel = new JPanel();
258         this.togglePanel.setBackground(BAR_COLOR);
259         setAnimationToggles();
260         this.togglePanel.setLayout(new BoxLayout(this.togglePanel, BoxLayout.Y_AXIS));
261         add(this.togglePanel, BorderLayout.WEST);
262     }
263 
264     /**
265      * Sets up all the tool in the tool panel on top.
266      */
267     private void setupTools()
268     {
269         this.toolPanel.setBackground(BAR_COLOR);
270         this.toolPanel.setMinimumSize(new Dimension(350, 28));
271         this.toolPanel.setPreferredSize(new Dimension(350, 28));
272         this.toolPanel.setLayout(new BoxLayout(this.toolPanel, BoxLayout.X_AXIS));
273 
274         this.toolPanel.add(Box.createHorizontalStrut(5));
275         this.toolPanel.add(new JLabel("Add tools:"));
276 
277         this.toolPanel.add(Box.createHorizontalStrut(5));
278         // button group that allows no selection when toggling the currently selected
279         ButtonGroup group = new ButtonGroup()
280         {
281             /** */
282             private static final long serialVersionUID = 20240227L;
283 
284             @Override
285             public void setSelected(final ButtonModel model, final boolean selected)
286             {
287                 if (selected)
288                 {
289                     super.setSelected(model, selected);
290                 }
291                 else
292                 {
293                     clearSelection();
294                 }
295             }
296         };
297 
298         JToggleButton nodeButton = new JToggleButton(loadIcon("./OTS_node.png"));
299         nodeButton.setPreferredSize(new Dimension(24, 24));
300         nodeButton.setMinimumSize(new Dimension(24, 24));
301         nodeButton.setMaximumSize(new Dimension(24, 24));
302         nodeButton.setToolTipText("Add node");
303         group.add(nodeButton);
304         this.toolPanel.add(nodeButton);
305 
306         JToggleButton linkButton = new JToggleButton(loadIcon("./OTS_link.png"));
307         linkButton.setPreferredSize(new Dimension(24, 24));
308         linkButton.setMinimumSize(new Dimension(24, 24));
309         linkButton.setMaximumSize(new Dimension(24, 24));
310         linkButton.setToolTipText("Add link");
311         group.add(linkButton);
312         this.toolPanel.add(linkButton);
313 
314         JToggleButton centroidButton = new JToggleButton(loadIcon("./OTS_centroid.png"));
315         centroidButton.setPreferredSize(new Dimension(24, 24));
316         centroidButton.setMinimumSize(new Dimension(24, 24));
317         centroidButton.setMaximumSize(new Dimension(24, 24));
318         centroidButton.setToolTipText("Add centroid");
319         group.add(centroidButton);
320         this.toolPanel.add(centroidButton);
321 
322         JToggleButton connectorButton = new JToggleButton(loadIcon("./OTS_connector.png"));
323         connectorButton.setPreferredSize(new Dimension(24, 24));
324         connectorButton.setMinimumSize(new Dimension(24, 24));
325         connectorButton.setMaximumSize(new Dimension(24, 24));
326         connectorButton.setToolTipText("Add connector");
327         group.add(connectorButton);
328         this.toolPanel.add(connectorButton);
329 
330         this.toolPanel.add(Box.createHorizontalStrut(5));
331         JComboBox<String> shape = new AppearanceControlComboBox<>();
332         shape.setModel(new DefaultComboBoxModel<>(new String[] {"Straight", "Bezier", "Clothoid", "Arc", "PolyLine"}));
333         shape.setMinimumSize(new Dimension(50, 22));
334         shape.setMaximumSize(new Dimension(90, 22));
335         shape.setPreferredSize(new Dimension(90, 22));
336         shape.setToolTipText("Standard shape for new links");
337         this.toolPanel.add(shape);
338 
339         this.toolPanel.add(Box.createHorizontalStrut(5));
340         JComboBox<String> roadLayout = new AppearanceControlComboBox<>();
341         roadLayout.setModel(new DefaultComboBoxModel<>(new String[] {}));
342         roadLayout.setMinimumSize(new Dimension(50, 22));
343         roadLayout.setMaximumSize(new Dimension(125, 22));
344         roadLayout.setPreferredSize(new Dimension(125, 22));
345         roadLayout.setToolTipText("Standard defined road layout for new links");
346         roadLayout.setEnabled(false);
347         this.toolPanel.add(roadLayout);
348 
349         this.toolPanel.add(Box.createHorizontalStrut(5));
350         JComboBox<String> linkType = new AppearanceControlComboBox<>();
351         linkType.setModel(new DefaultComboBoxModel<>(new String[] {}));
352         linkType.setMinimumSize(new Dimension(50, 22));
353         linkType.setMaximumSize(new Dimension(125, 22));
354         linkType.setPreferredSize(new Dimension(125, 22));
355         linkType.setToolTipText("Standard link type for new links and connectors");
356         linkType.setEnabled(false);
357         this.toolPanel.add(linkType);
358 
359         this.toolPanel.add(Box.createHorizontalStrut(5));
360         Dimension minDim = new Dimension(0, 1);
361         Dimension prefDim = new Dimension(0, 1);
362         Dimension maxDim = new Dimension(5000, 1);
363         this.toolPanel.add(new Filler(minDim, prefDim, maxDim)); // pushes further elements right aligned
364 
365         this.toolPanel.add(new JLabel("Show:"));
366 
367         this.toolPanel.add(Box.createHorizontalStrut(5));
368         JButton extent = new JButton(loadIcon("./Expand.png"));
369         extent.setMinimumSize(new Dimension(24, 24));
370         extent.setMaximumSize(new Dimension(24, 24));
371         extent.setPreferredSize(new Dimension(24, 24));
372         extent.setToolTipText("Zoom whole network");
373         extent.addActionListener((e) -> this.animationPanel.zoomAll());
374         this.toolPanel.add(extent);
375 
376         JButton grid = new JButton(loadIcon("./Grid.png"));
377         grid.setMinimumSize(new Dimension(24, 24));
378         grid.setMaximumSize(new Dimension(24, 24));
379         grid.setPreferredSize(new Dimension(24, 24));
380         grid.setToolTipText("Toggle grid on/off");
381         grid.addActionListener((e) -> this.animationPanel.setShowGrid(!this.animationPanel.isShowGrid()));
382         this.toolPanel.add(grid);
383 
384         this.toolPanel.add(Box.createHorizontalStrut(5));
385 
386         add(this.toolPanel, BorderLayout.NORTH);
387     }
388 
389     /**
390      * Loads an icon from the given file. Returns {@code null} if the file can not be found.
391      * @param file file.
392      * @return icon.
393      */
394     private Icon loadIcon(final String file)
395     {
396         try
397         {
398             return OtsEditor.loadIcon(file, 16, 16, -1, -1);
399         }
400         catch (IOException ioe)
401         {
402             // skip loading icon
403             return null;
404         }
405     }
406 
407     /**
408      * Sets the animation toggles as useful for in the editor.
409      */
410     private void setAnimationToggles()
411     {
412         addToggle("Node", NodeData.class, "/icons/Node24.png", "Show/hide nodes", true, false);
413         addToggle("NodeId", NodeAnimation.Text.class, "/icons/Id24.png", "Show/hide node ids", false, true);
414         addToggle("Link", LinkData.class, "/icons/Link24.png", "Show/hide links", true, false);
415         addToggle("LinkId", LinkAnimation.Text.class, "/icons/Id24.png", "Show/hide link ids", false, true);
416         addToggle("Priority", PriorityData.class, "/icons/Priority24.png", "Show/hide link priority", true, false);
417         addToggle("Lane", LaneData.class, "/icons/Lane24.png", "Show/hide lanes", true, false);
418         addToggle("LaneId", LaneAnimation.Text.class, "/icons/Id24.png", "Show/hide lane ids", false, true);
419         addToggle("LaneCenter", CenterLine.class, "/icons/CenterLine24.png", "Show/hide lane center lines", false, false);
420         addToggle("Stripe", StripeData.class, "/icons/Stripe24.png", "Show/hide stripes", true, false);
421         addToggle("Shoulder", ShoulderData.class, "/icons/Shoulder24.png", "Show/hide shoulders", true, false);
422         // TODO: perhaps a specific data type for generators?
423         addToggle("Generator", GtuGeneratorPositionData.class, "/icons/Generator24.png", "Show/hide generators", true, false);
424         addToggle("Sink", SinkData.class, "/icons/Sink24.png", "Show/hide sinks", true, true);
425         addToggle("Detector", LoopDetectorData.class, "/icons/Detector24.png", "Show/hide loop detectors", true, false);
426         addToggle("DetectorId", LoopDetectorData.Text.class, "/icons/Id24.png", "Show/hide loop detector ids", false, true);
427         addToggle("Light", TrafficLightData.class, "/icons/TrafficLight24.png", "Show/hide traffic lights", true, false);
428         addToggle("LightId", TrafficLightAnimation.Text.class, "/icons/Id24.png", "Show/hide traffic light ids", false, true);
429         addToggle("Bus", BusStopData.class, "/icons/BusStop24.png", "Show/hide bus stops", true, false);
430         addToggle("BusId", BusStopAnimation.Text.class, "/icons/Id24.png", "Show/hide bus stop ids", false, true);
431     }
432 
433     /**
434      * Add a button for toggling an animatable class on or off. Button icons for which 'idButton' is true will be placed to the
435      * right of the previous button, which should be the corresponding button without the id. An example is an icon for
436      * showing/hiding the class 'Lane' followed by the button to show/hide the Lane ids.
437      * @param name the name of the button
438      * @param locatableClass the class for which the button holds (e.g., GTU.class)
439      * @param iconPath the path to the 24x24 icon to display
440      * @param toolTipText the tool tip text to show when hovering over the button
441      * @param initiallyVisible whether the class is initially shown or not
442      * @param idButton id button that needs to be placed next to the previous button
443      */
444     public final void addToggle(final String name, final Class<? extends Locatable> locatableClass, final String iconPath,
445             final String toolTipText, final boolean initiallyVisible, final boolean idButton)
446     {
447         JToggleButton button;
448         Icon icon = OtsControlPanel.loadIcon(iconPath);
449         Icon unIcon = OtsControlPanel.loadGrayscaleIcon(iconPath);
450         button = new JCheckBox();
451         button.setSelectedIcon(icon);
452         button.setIcon(unIcon);
453         button.setPreferredSize(new Dimension(32, 28));
454         button.setName(name);
455         button.setEnabled(true);
456         button.setSelected(initiallyVisible);
457         button.setActionCommand(name);
458         button.setToolTipText(toolTipText);
459         button.addActionListener(new ActionListener()
460         {
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 editor.
503      * @return 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             @Override
515             public ContextInterface getContext()
516             {
517                 return context;
518             }
519         };
520         return new EditorMap(animator, contextualized, editor);
521     }
522 
523     @Override
524     public void notify(final Event event) throws RemoteException
525     {
526         if (event.getType().equals(OtsEditor.NEW_FILE))
527         {
528             for (XsdTreeNode node : new LinkedHashSet<>(this.datas.keySet()))
529             {
530                 remove(node);
531             }
532             this.datas.clear();
533             this.links.clear();
534             for (Renderable2d<?> animation : this.animations.values())
535             {
536                 animation.destroy(this.contextualized);
537                 removeAnimation(animation);
538             }
539             this.animations.clear();
540             for (RoadLayoutListener roadLayoutListener : this.roadLayoutListeners.values())
541             {
542                 roadLayoutListener.destroy();
543             }
544             this.roadLayoutListeners.clear();
545             if (this.networkFlattenerListener != null)
546             {
547                 this.networkFlattenerListener.destroy();
548                 this.networkFlattenerListener = null;
549             }
550             XsdTreeNodeRoot root = (XsdTreeNodeRoot) event.getContent();
551             root.addListener(this, XsdTreeNodeRoot.NODE_CREATED);
552             root.addListener(this, XsdTreeNodeRoot.NODE_REMOVED);
553             SwingUtilities.invokeLater(() -> this.animationPanel.zoomAll());
554         }
555         else if (event.getType().equals(XsdTreeNodeRoot.NODE_CREATED))
556         {
557             Object[] content = (Object[]) event.getContent();
558             XsdTreeNode node = (XsdTreeNode) content[0];
559             if (isType(node))
560             {
561                 node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
562                 node.addListener(this, XsdTreeNode.OPTION_CHANGED);
563                 if (node.isActive())
564                 {
565                     add(node);
566                 }
567             }
568             else if (node.getPathString().equals(XsdPaths.POLYLINE_COORDINATE))
569             {
570                 for (MapLinkData linkData : this.links.keySet())
571                 {
572                     linkData.addCoordinate(node);
573                 }
574             }
575             else if (node.getPathString().equals(XsdPaths.DEFINED_ROADLAYOUT))
576             {
577                 addRoadLayout(node);
578             }
579             else if (node.getPathString().equals(XsdPaths.NETWORK + ".Flattener"))
580             {
581                 setNetworkFlattener(node);
582             }
583             else if (node.getPathString().endsWith("LaneOverride") || node.getPathString().endsWith("StripeOverride"))
584             {
585                 ChangeListener<Object> listener = new ChangeListener<>(node, () -> this.editor.getEval())
586                 {
587                     private static final long serialVersionUID = 20241206L;
588 
589                     @Override
590                     public void notify(final Event event) throws RemoteException
591                     {
592                         if (event.getType().equals(ChangeListener.CHANGE_EVENT))
593                         {
594                             MapData data = EditorMap.this.datas.get(getNode().getParent().getParent());
595                             if (data instanceof MapLinkData linkData)
596                             {
597                                 linkData.evalChanged();
598                             }
599                         }
600                         else
601                         {
602                             super.notify(event);
603                         }
604                     }
605 
606                     @Override
607                     Object calculateData()
608                     {
609                         return null; // This change listener represents no data
610                     }
611                 };
612                 this.overrideListeners.put(node, listener);
613                 listener.addListener(listener, ChangeListener.CHANGE_EVENT); // register to self, but for change events
614             }
615         }
616         else if (event.getType().equals(XsdTreeNodeRoot.NODE_REMOVED))
617         {
618             Object[] content = (Object[]) event.getContent();
619             XsdTreeNode node = (XsdTreeNode) content[0];
620             if (this.datas.containsKey(node)) // node.isType does not work as parent is gone, i.e. type is just "Node"
621             {
622                 remove(node);
623             }
624             else if (node.getPathString().equals(XsdPaths.POLYLINE_COORDINATE))
625             {
626                 for (MapLinkData linkData : this.links.keySet())
627                 {
628                     linkData.removeCoordinate(node);
629                 }
630             }
631             else if (node.getPathString().equals(XsdPaths.DEFINED_ROADLAYOUT))
632             {
633                 removeRoadLayout(node);
634             }
635             else if (node.getPathString().equals(XsdPaths.NETWORK + ".Flattener"))
636             {
637                 removeNetworkFlattener();
638             }
639             else if (node.getPathString().endsWith("LaneOverride") || node.getPathString().endsWith("StripeOverride"))
640             {
641                 ChangeListener<Object> listener = this.overrideListeners.remove(node);
642                 if (listener != null)
643                 {
644                     listener.removeListener(listener, ChangeListener.CHANGE_EVENT);
645                     listener.destroy();
646                 }
647             }
648         }
649         else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
650         {
651             Object[] content = (Object[]) event.getContent();
652             XsdTreeNode node = (XsdTreeNode) content[0];
653             if (isType(node))
654             {
655                 if ((boolean) content[1])
656                 {
657                     add(node);
658                 }
659                 else
660                 {
661                     remove(node);
662                 }
663             }
664             else if (node.getPathString().equals(XsdPaths.DEFINED_ROADLAYOUT))
665             {
666                 if ((boolean) content[1])
667                 {
668                     addRoadLayout(node);
669                 }
670                 else
671                 {
672                     removeRoadLayout(node);
673                 }
674             }
675             else if (node.getPathString().equals(XsdPaths.NETWORK + ".Flattener"))
676             {
677                 if ((boolean) content[1])
678                 {
679                     setNetworkFlattener(node);
680                 }
681                 else
682                 {
683                     removeNetworkFlattener();
684                 }
685             }
686         }
687         else if (event.getType().equals(XsdTreeNode.OPTION_CHANGED))
688         {
689             Object[] content = (Object[]) event.getContent();
690             XsdTreeNode node = (XsdTreeNode) content[0];
691             XsdTreeNode selected = (XsdTreeNode) content[1];
692             XsdTreeNode previous = (XsdTreeNode) content[2];
693             if (node.equals(selected))
694             {
695                 if (isType(previous))
696                 {
697                     remove(previous);
698                 }
699                 if (isType(selected) && selected.isActive())
700                 {
701                     add(selected);
702                 }
703             }
704         }
705         else if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED))
706         {
707             for (MapLinkData linkData : this.links.keySet())
708             {
709                 linkData.notifyNodeIdChanged(linkData.getNode());
710             }
711         }
712     }
713 
714     /**
715      * Returns whether the node is any of the visualized types.
716      * @param node node.
717      * @return whether the node is any of the visualized types.
718      */
719     private boolean isType(final XsdTreeNode node)
720     {
721         for (String type : TYPES)
722         {
723             if (node.isType(type))
724             {
725                 return true;
726             }
727         }
728         return false;
729     }
730 
731     /**
732      * Set the data as being valid to draw.
733      * @param data data that is valid to draw.
734      */
735     public void setValid(final MapData data)
736     {
737         XsdTreeNode node = data.getNode();
738         if (this.animations.containsKey(node))
739         {
740             return;
741         }
742         Renderable2d<?> animation;
743         if (node.getPathString().equals(XsdPaths.NODE))
744         {
745             animation = new NodeAnimation((MapNodeData) data, this.contextualized);
746         }
747         else if (node.getPathString().equals(XsdPaths.LINK))
748         {
749             animation = Try.assign(() -> new LinkAnimation((MapLinkData) data, this.contextualized, 0.5f).setDynamic(true), "");
750         }
751         else if (node.getPathString().equals(XsdPaths.TRAFFIC_LIGHT))
752         {
753             animation = Try.assign(() -> new TrafficLightAnimation((MapTrafficLightData) data, this.contextualized), "");
754         }
755         else if (node.getPathString().equals(XsdPaths.SINK))
756         {
757             Function<LaneDetectorAnimation<SinkData, SinkText>, SinkText> textSupplier =
758                     (s) -> Try.assign(() -> new SinkText(s.getSource(),
759                             (float) (s.getSource().getLine().getLength() / 2.0 + 0.2), this.contextualized), "");
760             animation = Try.assign(() -> new LaneDetectorAnimation<SinkData, SinkText>((SinkData) data, this.contextualized,
761                     Color.ORANGE, textSupplier), "");
762         }
763         else if (node.getPathString().equals(XsdPaths.GENERATOR) || node.getPathString().equals(XsdPaths.LIST_GENERATOR))
764         {
765             animation = new GtuGeneratorPositionAnimation((GtuGeneratorPositionData) data, this.contextualized);
766         }
767         else
768         {
769             throw new UnsupportedOperationException("Data cannot be added by the map editor.");
770         }
771         this.animations.put(node, animation);
772     }
773 
774     /**
775      * Set the data as being invalid to draw.
776      * @param data data that is invalid to draw.
777      */
778     // TODO: for some reason, this does not work... because data remains in JVM?
779     public void setInvalid(final MapData data)
780     {
781         //
782     }
783 
784     /**
785      * Adds a data representation of the node. This will not yet be drawn until the data object itself tells the map it is valid
786      * to be drawn.
787      * @param node node of element to draw.
788      * @throws RemoteException context binding problem.
789      */
790     private void add(final XsdTreeNode node) throws RemoteException
791     {
792         MapData data;
793         if (this.datas.containsKey(node))
794         {
795             return; // activated choice
796         }
797         if (node.getPathString().equals(XsdPaths.NODE))
798         {
799             data = new MapNodeData(this, node, this.editor);
800             node.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
801         }
802         else if (node.getPathString().equals(XsdPaths.LINK))
803         {
804             MapLinkData linkData = new MapLinkData(this, node, this.editor);
805             data = linkData;
806             if (this.networkFlattenerListener != null)
807             {
808                 this.networkFlattenerListener.addListener(linkData, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
809             }
810             this.links.put(linkData, null);
811         }
812         else if (node.getPathString().equals(XsdPaths.TRAFFIC_LIGHT))
813         {
814             MapTrafficLightData trafficLightData = new MapTrafficLightData(this, node, this.editor);
815             data = trafficLightData;
816         }
817         else if (node.getPathString().equals(XsdPaths.SINK))
818         {
819             MapSinkData sinkData = new MapSinkData(this, node, this.editor);
820             data = sinkData;
821         }
822         else if (node.getPathString().equals(XsdPaths.GENERATOR) || node.getPathString().equals(XsdPaths.LIST_GENERATOR))
823         {
824             MapGeneratorData generatorData = new MapGeneratorData(this, node, this.editor);
825             data = generatorData;
826         }
827         else
828         {
829             throw new UnsupportedOperationException("Node cannot be added by the map editor.");
830         }
831         this.datas.put(node, data);
832     }
833 
834     /**
835      * Remove the drawing data of pertaining to the node.
836      * @param node node.
837      */
838     private void remove(final XsdTreeNode node)
839     {
840         if (node.getPathString().equals(XsdPaths.NODE))
841         {
842             node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
843         }
844         if (node.getPathString().equals(XsdPaths.LINK))
845         {
846             Iterator<MapLinkData> it = this.links.keySet().iterator();
847             while (it.hasNext())
848             {
849                 MapLinkData link = it.next();
850                 if (link.getNode().equals(node))
851                 {
852                     for (RoadLayoutListener roadLayoutListener : this.roadLayoutListeners.values())
853                     {
854                         roadLayoutListener.removeListener(link, ChangeListener.CHANGE_EVENT);
855                     }
856                     if (this.networkFlattenerListener != null)
857                     {
858                         this.networkFlattenerListener.removeListener(link, ChangeListener.CHANGE_EVENT);
859                     }
860                     it.remove();
861                     break;
862                 }
863             }
864         }
865         MapData data = this.datas.remove(node);
866         if (data != null)
867         {
868             data.destroy();
869         }
870         removeAnimation(this.animations.remove(node));
871     }
872 
873     /**
874      * Reinitialize animation on object who's animator stores static information that depends on something that was changed.
875      * This will create a new animation object. Only data objects that know their animations have static data, should call this.
876      * And only when information changed on which the static data depends.
877      * @param node node.
878      */
879     public void reinitialize(final XsdTreeNode node)
880     {
881         remove(node);
882         Try.execute(() -> add(node), RuntimeException.class, "Unable to bind to context.");
883     }
884 
885     /**
886      * Returns the map data of the given XSD node.
887      * @param node node.
888      * @return map data of the given XSD node, {@code null} if no such data.
889      */
890     public MapData getData(final XsdTreeNode node)
891     {
892         return this.datas.get(node);
893     }
894 
895     /**
896      * Add defined road layout.
897      * @param node node of the defined road layout.
898      */
899     private void addRoadLayout(final XsdTreeNode node)
900     {
901         RoadLayoutListener roadLayoutListener = new RoadLayoutListener(node, () -> this.editor.getEval());
902         for (MapLinkData linkData : this.links.keySet())
903         {
904             roadLayoutListener.addListener(linkData, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
905         }
906         this.roadLayoutListeners.put(node, roadLayoutListener);
907     }
908 
909     /**
910      * Remove defined road layout.
911      * @param node node of the defined road layout.
912      */
913     private void removeRoadLayout(final XsdTreeNode node)
914     {
915         RoadLayoutListener roadLayoutListener = this.roadLayoutListeners.remove(node);
916         roadLayoutListener.destroy();
917     }
918 
919     /**
920      * Sets the network level flattener.
921      * @param node node of network flattener.
922      */
923     private void setNetworkFlattener(final XsdTreeNode node)
924     {
925         this.networkFlattenerListener = new FlattenerListener(node, () -> this.editor.getEval());
926         for (MapLinkData linkData : this.links.keySet())
927         {
928             this.networkFlattenerListener.addListener(linkData, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
929             this.editor.addEvalListener(this.networkFlattenerListener);
930         }
931         node.addListener(this, XsdTreeNode.ACTIVATION_CHANGED, ReferenceType.WEAK);
932     }
933 
934     /**
935      * Removes the network flattener.
936      */
937     private void removeNetworkFlattener()
938     {
939         if (this.networkFlattenerListener != null)
940         {
941             this.editor.removeEvalListener(this.networkFlattenerListener);
942         }
943         this.networkFlattenerListener.destroy();
944         for (MapLinkData linkData : this.links.keySet())
945         {
946             Try.execute(() -> linkData.notify(new Event(ChangeListener.CHANGE_EVENT, this.networkFlattenerListener.getNode())),
947                     "Remove event exception.");
948         }
949         this.networkFlattenerListener = null;
950     }
951 
952     /**
953      * Returns the road layout listener from which a {@code MapLinkData} can obtain offsets.
954      * @param node node of a defined layout.
955      * @return listener, can be used to obtain offsets.
956      */
957     RoadLayoutListener getRoadLayoutListener(final XsdTreeNode node)
958     {
959         return this.roadLayoutListeners.get(node);
960     }
961 
962     /**
963      * Remove animation.
964      * @param animation animation to remove.
965      */
966     void removeAnimation(final Renderable2d<?> animation)
967     {
968         if (animation != null)
969         {
970             this.animationPanel.objectRemoved(animation);
971             animation.destroy(this.contextualized);
972         }
973     }
974 
975     /**
976      * Returns the context.
977      * @return context.
978      */
979     Contextualized getContextualized()
980     {
981         return this.contextualized;
982     }
983 
984     /**
985      * Returns the network level flattener, or a 64 segment flattener of none specified.
986      * @return flattener.
987      */
988     public Flattener getNetworkFlattener()
989     {
990         if (this.networkFlattenerListener != null)
991         {
992             Flattener flattener = this.networkFlattenerListener.getData();
993             if (flattener != null)
994             {
995                 return flattener; // otherwise, return default
996             }
997         }
998         return new NumSegments(64);
999     }
1000 
1001     /**
1002      * Returns the map of synchronizable stripes, not a safe copy.
1003      * @return map of synchronizable stripes
1004      */
1005     public Map<MapStripeData, SynchronizableMapStripe> getSynchronizableStripes()
1006     {
1007         return this.synStripes;
1008     }
1009 
1010 }