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