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