View Javadoc
1   package org.opentrafficsim.editor.extensions.map;
2   
3   import java.awt.Color;
4   import java.rmi.RemoteException;
5   import java.util.ArrayList;
6   import java.util.Comparator;
7   import java.util.Iterator;
8   import java.util.LinkedHashMap;
9   import java.util.LinkedHashSet;
10  import java.util.List;
11  import java.util.Map.Entry;
12  import java.util.Set;
13  import java.util.SortedMap;
14  import java.util.TreeMap;
15  
16  import javax.naming.NamingException;
17  import javax.swing.SwingUtilities;
18  
19  import org.djunits.value.vdouble.scalar.Angle;
20  import org.djunits.value.vdouble.scalar.Direction;
21  import org.djunits.value.vdouble.scalar.Length;
22  import org.djunits.value.vdouble.scalar.LinearDensity;
23  import org.djutils.draw.DrawRuntimeException;
24  import org.djutils.draw.line.PolyLine2d;
25  import org.djutils.draw.line.Polygon2d;
26  import org.djutils.draw.line.Ray2d;
27  import org.djutils.draw.point.OrientedPoint2d;
28  import org.djutils.draw.point.Point2d;
29  import org.djutils.event.Event;
30  import org.djutils.event.EventListener;
31  import org.djutils.event.EventListenerMap;
32  import org.djutils.event.EventProducer;
33  import org.djutils.event.EventType;
34  import org.djutils.event.reference.ReferenceType;
35  import org.djutils.exceptions.Try;
36  import org.djutils.metadata.MetaData;
37  import org.djutils.metadata.ObjectDescriptor;
38  import org.opentrafficsim.base.geometry.BoundingPolygon;
39  import org.opentrafficsim.base.geometry.ClickableBounds;
40  import org.opentrafficsim.base.geometry.OtsBounds2d;
41  import org.opentrafficsim.core.geometry.Bezier;
42  import org.opentrafficsim.core.geometry.ContinuousArc;
43  import org.opentrafficsim.core.geometry.ContinuousBezierCubic;
44  import org.opentrafficsim.core.geometry.ContinuousClothoid;
45  import org.opentrafficsim.core.geometry.ContinuousLine;
46  import org.opentrafficsim.core.geometry.ContinuousPolyLine;
47  import org.opentrafficsim.core.geometry.ContinuousStraight;
48  import org.opentrafficsim.core.geometry.Flattener;
49  import org.opentrafficsim.core.geometry.OtsGeometryUtil;
50  import org.opentrafficsim.draw.network.LinkAnimation.LinkData;
51  import org.opentrafficsim.draw.road.CrossSectionElementAnimation;
52  import org.opentrafficsim.draw.road.LaneAnimation;
53  import org.opentrafficsim.draw.road.PriorityAnimation;
54  import org.opentrafficsim.draw.road.StripeAnimation;
55  import org.opentrafficsim.draw.road.StripeAnimation.StripeData;
56  import org.opentrafficsim.editor.OtsEditor;
57  import org.opentrafficsim.editor.XsdPaths;
58  import org.opentrafficsim.editor.XsdTreeNode;
59  import org.opentrafficsim.editor.extensions.Adapters;
60  import org.opentrafficsim.road.network.factory.xml.utils.RoadLayoutOffsets.CseData;
61  import org.opentrafficsim.road.network.lane.CrossSectionSlice;
62  import org.opentrafficsim.road.network.lane.LaneGeometryUtil;
63  import org.opentrafficsim.road.network.lane.SliceInfo;
64  import org.opentrafficsim.road.network.lane.Stripe;
65  import org.opentrafficsim.road.network.lane.Stripe.Type;
66  import org.opentrafficsim.xml.bindings.ExpressionAdapter;
67  import org.opentrafficsim.xml.bindings.types.ArcDirectionType.ArcDirection;
68  
69  import nl.tudelft.simulation.dsol.animation.d2.Renderable2d;
70  
71  /**
72   * LinkData for the editor Map. This class will also listen to any changes that may affect the link shape, maintain the drawn
73   * layout, and maintain the priority animation.
74   * <p>
75   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
76   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
77   * </p>
78   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
79   */
80  public class MapLinkData extends MapData implements LinkData, EventListener, EventProducer
81  {
82  
83      /** */
84      private static final long serialVersionUID = 20231003L;
85  
86      /** Event when layout is rebuilt. */
87      public static final EventType LAYOUT_REBUILT = new EventType("LAYOUTREBUILT", new MetaData("LAYOUT", "Layout is rebuilt.",
88              new ObjectDescriptor("LinkData", "Map link data object.", MapLinkData.class)));
89  
90      /** Event listeners. */
91      private final EventListenerMap eventListenerMap = new EventListenerMap();
92  
93      /** Listener to changes in shape. */
94      private final ShapeListener shapeListener = new ShapeListener();
95  
96      /** String attribute. */
97      private String id = "";
98  
99      /** Tree node of start. */
100     private XsdTreeNode nodeStart;
101 
102     /** Tree node of end. */
103     private XsdTreeNode nodeEnd;
104 
105     /** Start direction. */
106     private Direction directionStart = Direction.ZERO;
107 
108     /** End direction. */
109     private Direction directionEnd = Direction.ZERO;
110 
111     /** Start offset. */
112     private Length offsetStart;
113 
114     /** End offset. */
115     private Length offsetEnd;
116 
117     /** From point. */
118     private Point2d from;
119 
120     /** To point. */
121     private Point2d to;
122 
123     /** Continuous design line. */
124     private ContinuousLine designLine = null;
125 
126     /** Flattened design line. */
127     private PolyLine2d flattenedDesignLine = null;
128 
129     /** Bounds around the flattened design line. */
130     private BoundingPolygon bounds;
131 
132     /** Location. */
133     private OrientedPoint2d location;
134 
135     /** Node describing the road layout. */
136     private XsdTreeNode roadLayoutNode;
137 
138     /** Node linking defined road layout id. */
139     private XsdTreeNode definedRoadLayoutNode;
140 
141     /** Listener to road layout, if locally defined. */
142     private RoadLayoutListener roadLayoutListener;
143 
144     /** Listener to flattener, if locally defined. */
145     private FlattenerListener flattenerListener;
146 
147     /** Set of drawable cross-section elements. */
148     private Set<Renderable2d<?>> crossSectionElements = new LinkedHashSet<>();
149 
150     /** Lane data. */
151     private java.util.Map<String, MapLaneData> laneData = new LinkedHashMap<>();
152 
153     /** Priority animation. */
154     private PriorityAnimation priorityAnimation;
155 
156     /**
157      * Constructor.
158      * @param map Map; map.
159      * @param linkNode XsdTreeNode; node Ots.Network.Link.
160      * @param editor OtsEditor; editor.
161      */
162     public MapLinkData(final EditorMap map, final XsdTreeNode linkNode, final OtsEditor editor)
163     {
164         super(map, linkNode, editor);
165         linkNode.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
166         linkNode.getChild(0).addListener(this.shapeListener, XsdTreeNode.OPTION_CHANGED, ReferenceType.WEAK);
167         linkNode.getChild(1).addListener(this, XsdTreeNode.OPTION_CHANGED, ReferenceType.WEAK);
168         this.shapeListener.shapeNode = linkNode.getChild(0);
169 
170         // as RoadLayout is the default, a setOption() never triggers this (for DefinedRoadLayout this is not required)
171         SwingUtilities.invokeLater(() ->
172         {
173             XsdTreeNode layout = linkNode.getChild(1);
174             if (layout.getOption().equals(layout))
175             {
176                 try
177                 {
178                     notify(new Event(XsdTreeNode.OPTION_CHANGED, new Object[] {layout, layout, layout}));
179                 }
180                 catch (RemoteException e)
181                 {
182                     throw new RuntimeException(e);
183                 }
184             }
185         });
186 
187         // for when node is duplicated, set data immediately if (getNode().isActive())
188         if (getNode().isActive())
189         {
190             SwingUtilities.invokeLater(() ->
191             {
192                 try
193                 {
194                     // this is for when delete is undone, as some children are recovered later, including the shape node
195                     this.shapeListener.shapeNode = linkNode.getChild(0);
196                     linkNode.getChild(0).addListener(this.shapeListener, XsdTreeNode.OPTION_CHANGED, ReferenceType.WEAK);
197 
198                     notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "Id", null}));
199                     this.nodeStart = replaceNode(this.nodeStart, linkNode.getCoupledKeyrefNodeAttribute("NodeStart"));
200                     this.nodeEnd = replaceNode(this.nodeEnd, linkNode.getCoupledKeyrefNodeAttribute("NodeEnd"));
201                     notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "OffsetStart", null}));
202                     notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "OffsetEnd", null}));
203                     XsdTreeNode shape = linkNode.getChild(0);
204                     this.shapeListener.notify(new Event(XsdTreeNode.OPTION_CHANGED, new Object[] {shape, shape, shape}));
205                 }
206                 catch (RemoteException e)
207                 {
208                     throw new RuntimeException(e);
209                 }
210             });
211         }
212     }
213 
214     /** {@inheritDoc} */
215     @Override
216     public void destroy()
217     {
218         super.destroy();
219         for (Renderable2d<?> renderable : this.crossSectionElements)
220         {
221             getMap().removeAnimation(renderable);
222         }
223         this.crossSectionElements.clear();
224         if (this.roadLayoutListener != null)
225         {
226             this.roadLayoutListener.destroy();
227             this.roadLayoutListener = null;
228         }
229         if (this.definedRoadLayoutNode != null)
230         {
231             this.definedRoadLayoutNode.removeListener(this, XsdTreeNode.VALUE_CHANGED);
232             this.definedRoadLayoutNode = null;
233         }
234         if (this.flattenerListener != null)
235         {
236             this.flattenerListener.destroy();
237             this.flattenerListener = null;
238         }
239         if (this.priorityAnimation != null)
240         {
241             this.priorityAnimation.destroy(getMap().getContextualized());
242         }
243     }
244 
245     /** {@inheritDoc} */
246     @Override
247     public OtsBounds2d getBounds()
248     {
249         return this.bounds;
250     }
251 
252     /** {@inheritDoc} */
253     @Override
254     public String getId()
255     {
256         return this.id;
257     }
258 
259     /** {@inheritDoc} */
260     @Override
261     public OrientedPoint2d getLocation()
262     {
263         return this.location;
264     }
265 
266     /** {@inheritDoc} */
267     @Override
268     public boolean isConnector()
269     {
270         return false;
271     }
272 
273     /** {@inheritDoc} */
274     @Override
275     public PolyLine2d getDesignLine()
276     {
277         return this.flattenedDesignLine;
278     }
279 
280     /** {@inheritDoc} */
281     @Override
282     public void notify(final Event event) throws RemoteException
283     {
284         if (event.getType().equals(XsdTreeNode.OPTION_CHANGED))
285         {
286             Object[] content = (Object[]) event.getContent();
287             XsdTreeNode selected = (XsdTreeNode) content[1];
288             if (selected.getNodeName().equals("RoadLayout") || (selected.getNodeName().equals("xsd:sequence")
289                     && selected.getChildCount() > 0 && selected.getChild(0).getNodeName().equals("DefinedLayout")))
290             {
291                 // road layout
292                 if (this.roadLayoutListener != null)
293                 {
294                     this.roadLayoutListener.destroy();
295                 }
296                 if (this.definedRoadLayoutNode != null)
297                 {
298                     this.definedRoadLayoutNode.removeListener(this, XsdTreeNode.VALUE_CHANGED);
299                 }
300                 if (selected.getNodeName().equals("RoadLayout"))
301                 {
302                     this.roadLayoutNode = selected;
303                     this.definedRoadLayoutNode = null;
304                     this.roadLayoutListener = new RoadLayoutListener(selected, () -> getEval());
305                     this.roadLayoutListener.addListener(this, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
306                 }
307                 else
308                 {
309                     this.definedRoadLayoutNode = selected.getChild(0);
310                     this.definedRoadLayoutNode.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
311                     this.roadLayoutNode = this.definedRoadLayoutNode.getCoupledKeyrefNodeValue();
312                     this.roadLayoutListener = null;
313                 }
314             }
315             else
316             {
317                 // flattener
318                 if (this.flattenerListener != null)
319                 {
320                     this.flattenerListener.destroy();
321                 }
322                 this.flattenerListener = new FlattenerListener(selected, () -> getEval());
323             }
324             buildLayout();
325             return;
326         }
327         else if (event.getType().equals(XsdTreeNode.VALUE_CHANGED))
328         {
329             // defined road layout value changed
330             this.roadLayoutNode = this.definedRoadLayoutNode.getCoupledKeyrefNodeValue();
331             buildLayout();
332             return;
333         }
334         else if (event.getType().equals(ChangeListener.CHANGE_EVENT))
335         {
336             XsdTreeNode node = (XsdTreeNode) event.getContent();
337             if (node.getNodeName().equals("RoadLayout") || node.getNodeName().equals("DefinedLayout"))
338             {
339                 if (node.isIdentifiable() && this.definedRoadLayoutNode != null)
340                 {
341                     // for if the id in the road layout has changed
342                     this.roadLayoutNode = this.definedRoadLayoutNode.getCoupledKeyrefNodeValue();
343                 }
344                 if (node.equals(this.roadLayoutNode) && node.reportInvalidId() == null)
345                 {
346                     buildLayout();
347                 }
348             }
349             else
350             {
351                 // change in flattener
352                 buildDesignLine();
353             }
354             return;
355         }
356 
357         // any attribute of the link node, or of either of the connected nodes
358         Object[] content = (Object[]) event.getContent();
359         XsdTreeNode node = (XsdTreeNode) content[0];
360         String attribute = (String) content[1];
361         String value = node.getAttributeValue(attribute);
362 
363         if ("Id".equals(attribute))
364         {
365             this.id = value == null ? "" : value;
366             return;
367         }
368         else if ("NodeStart".equals(attribute))
369         {
370             this.nodeStart = replaceNode(this.nodeStart, getNode().getCoupledKeyrefNodeAttribute("NodeStart"));
371         }
372         else if ("NodeEnd".equals(attribute))
373         {
374             this.nodeEnd = replaceNode(this.nodeEnd, getNode().getCoupledKeyrefNodeAttribute("NodeEnd"));
375         }
376         else if ("OffsetStart".equals(attribute))
377         {
378             setValue((v) -> this.offsetStart = v, Adapters.get(Length.class), getNode(), attribute);
379         }
380         else if ("OffsetEnd".equals(attribute))
381         {
382             setValue((v) -> this.offsetEnd = v, Adapters.get(Length.class), getNode(), attribute);
383         }
384         else if ("Coordinate".equals(attribute))
385         {
386             // this pertains to either of the nodes, to which this class also listens
387         }
388         else if ("Direction".equals(attribute))
389         {
390             // this pertains to either of the nodes, to which this class also listens
391         }
392         else
393         {
394             // other attribute, not important
395             return;
396         }
397         buildDesignLine();
398     }
399 
400     /**
401      * Replaces the old node with the new node, adding and removing this as a listener as required. If a node refers to a
402      * default input parameter node, the node that the input parameter id refers to is found instead.
403      * @param oldNode XsdTreeNode; former node.
404      * @param newNode XsdTreeNode; new node.
405      * @return XsdTreeNode; the actual new node (Ots.Network.Node).
406      */
407     private XsdTreeNode replaceNode(final XsdTreeNode oldNode, final XsdTreeNode newNode)
408     {
409         if (oldNode != null)
410         {
411             oldNode.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
412             if (oldNode.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETER_STRING))
413             {
414                 XsdTreeNode node = getInputNode(newNode);
415                 if (node != null)
416                 {
417                     node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
418                 }
419             }
420         }
421         if (newNode != null)
422         {
423             newNode.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
424             if (newNode.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETER_STRING))
425             {
426                 XsdTreeNode node = getInputNode(newNode);
427                 if (node != null)
428                 {
429                     node.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
430                 }
431                 return node;
432             }
433         }
434         return newNode;
435     }
436 
437     /**
438      * Finds the network node that the value of an input parameter node refers to.
439      * @param inputParameter XsdTreeNode; input parameter node (default).
440      * @return XsdTreeNode; the actual node (Ots.Network.Node).
441      */
442     private XsdTreeNode getInputNode(final XsdTreeNode inputParameter)
443     {
444         String inputId = inputParameter.getId();
445         String nodeId = (String) getEval().evaluate(inputId.substring(1, inputId.length() - 1));
446         XsdTreeNode ots = inputParameter.getRoot();
447         for (XsdTreeNode child : ots.getChildren())
448         {
449             if (child.getPathString().equals(XsdPaths.NETWORK))
450             {
451                 for (XsdTreeNode networkElement : child.getChildren())
452                 {
453                     if (networkElement.isType("Node") && networkElement.getId().equals(nodeId))
454                     {
455                         return networkElement;
456                     }
457                 }
458             }
459         }
460         return null;
461     }
462 
463     /**
464      * The map was notified a new coordinate node was added. The node may or may not be part of this link.
465      * @param node XsdTreeNode; added coordinate node.
466      */
467     public void addCoordinate(final XsdTreeNode node)
468     {
469         if (this.shapeListener.shapeNode.equals(node.getParent()))
470         {
471             this.shapeListener.coordinates.put(node, orNull(node.getValue(), Adapters.get(Point2d.class)));
472             buildDesignLine();
473             node.addListener(this.shapeListener, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
474             node.addListener(this.shapeListener, XsdTreeNode.MOVED, ReferenceType.WEAK);
475         }
476     }
477 
478     /**
479      * The map was notified a coordinate node was removed. The node may or may not be part of this link.
480      * @param node XsdTreeNode; removed coordinate node.
481      */
482     public void removeCoordinate(final XsdTreeNode node)
483     {
484         // this.shapeListener.shapeNode.equals(node.getParent()) does not work as the parent is null after being removed
485         // this.shapeListener.coordinates.containsKey() id. as the ordering is incomplete as the node is removed from the parent
486         Iterator<XsdTreeNode> it = this.shapeListener.coordinates.keySet().iterator();
487         while (it.hasNext())
488         {
489             XsdTreeNode key = it.next();
490             if (node.equals(key))
491             {
492                 it.remove();
493                 buildDesignLine();
494                 node.removeListener(this.shapeListener, XsdTreeNode.VALUE_CHANGED);
495                 node.removeListener(this.shapeListener, XsdTreeNode.MOVED);
496                 return;
497             }
498         }
499     }
500 
501     /**
502      * Builds the design line.
503      */
504     private void buildDesignLine()
505     {
506         if (this.nodeStart == null || this.nodeEnd == null || this.nodeStart.equals(this.nodeEnd))
507         {
508             setInvalid();
509             return;
510         }
511         setValue((v) -> this.from = v, Adapters.get(Point2d.class), this.nodeStart, "Coordinate");
512         setValue((v) -> this.to = v, Adapters.get(Point2d.class), this.nodeEnd, "Coordinate");
513         if (this.from == null || this.to == null)
514         {
515             setInvalid();
516             return;
517         }
518         setValue((v) -> this.directionStart = v, Adapters.get(Direction.class), this.nodeStart, "Direction");
519         double dirStart = this.directionStart == null ? 0.0 : this.directionStart.si;
520         OrientedPoint2d from = new OrientedPoint2d(this.from, dirStart);
521         setValue((v) -> this.directionEnd = v, Adapters.get(Direction.class), this.nodeEnd, "Direction");
522         double dirEnd = this.directionEnd == null ? 0.0 : this.directionEnd.si;
523         OrientedPoint2d to = new OrientedPoint2d(this.to, dirEnd);
524         if (this.offsetStart != null)
525         {
526             from = OtsGeometryUtil.offsetPoint(from, this.offsetStart.si);
527         }
528         if (this.offsetEnd != null)
529         {
530             to = OtsGeometryUtil.offsetPoint(to, this.offsetEnd.si);
531         }
532         this.designLine = this.shapeListener.getContiuousLine(from, to);
533         if (this.designLine == null)
534         {
535             return;
536         }
537         this.flattenedDesignLine = this.designLine.flatten(getFlattener());
538         Ray2d ray = this.flattenedDesignLine.getLocationFractionExtended(0.5);
539         this.location = new OrientedPoint2d(ray.x, ray.y, ray.phi);
540         this.bounds =
541                 BoundingPolygon.geometryToBounds(this.location, ClickableBounds.get(this.flattenedDesignLine).asPolygon());
542         if (this.priorityAnimation != null)
543         {
544             getMap().removeAnimation(this.priorityAnimation);
545         }
546         this.priorityAnimation = new PriorityAnimation(new MapPriorityData(this), getMap().getContextualized());
547         buildLayout();
548         setValid();
549     }
550 
551     /**
552      * Returns the flattener to use, which is either a flattener defined at link level, or at network level.
553      * @return Flattener, flattener to use.
554      */
555     private Flattener getFlattener()
556     {
557         if (this.flattenerListener != null)
558         {
559             Flattener flattener = this.flattenerListener.getData();
560             if (flattener != null)
561             {
562                 return flattener; // otherwise not valid, return network level flattener
563             }
564         }
565         return getMap().getNetworkFlattener();
566     }
567 
568     /**
569      * Builds all animation objects for stripes, lanes, shoulders, no-traffic lanes, and their center lines and id's.
570      */
571     private void buildLayout()
572     {
573         if (this.designLine == null)
574         {
575             return;
576         }
577         for (Renderable2d<?> renderable : this.crossSectionElements)
578         {
579             getMap().removeAnimation(renderable);
580         }
581         this.crossSectionElements.clear();
582         this.laneData.clear();
583         if (this.roadLayoutNode != null)
584         {
585             java.util.Map<XsdTreeNode, CseData> cseDataMap = this.roadLayoutListener != null ? this.roadLayoutListener.getData()
586                     : getMap().getRoadLayoutListener(this.roadLayoutNode).getData();
587             for (Entry<XsdTreeNode, CseData> entry : cseDataMap.entrySet())
588             {
589                 XsdTreeNode node = entry.getKey();
590                 CseData cseData = entry.getValue();
591                 try
592                 {
593                     List<CrossSectionSlice> slices;
594                     Type type = null;
595                     Length width = null;
596                     if (node.getNodeName().equals("Stripe"))
597                     {
598                         type = Adapters.get(Stripe.Type.class).unmarshal(node.getAttributeValue("Type")).get(getEval());
599                         width = node.getChild(1).isActive() // child 1 is DrawingWidth
600                                 ? Adapters.get(Length.class).unmarshal(node.getChild(1).getValue()).get(getEval())
601                                 : type.defaultWidth();
602                         slices = LaneGeometryUtil.getSlices(this.designLine, cseData.centerOffsetStart, cseData.centerOffsetEnd,
603                                 width, width);
604                     }
605                     else
606                     {
607                         slices = LaneGeometryUtil.getSlices(this.designLine, cseData.centerOffsetStart, cseData.centerOffsetEnd,
608                                 cseData.widthStart, cseData.widthEnd);
609                     }
610                     SliceInfo sliceInfo = new SliceInfo(slices, Length.instantiateSI(this.designLine.getLength()));
611 
612                     Flattener flattener = getFlattener();
613                     PolyLine2d centerLine = this.designLine
614                             .flattenOffset(LaneGeometryUtil.getCenterOffsets(this.designLine, slices), flattener);
615                     PolyLine2d leftEdge = this.designLine
616                             .flattenOffset(LaneGeometryUtil.getLeftEdgeOffsets(this.designLine, slices), flattener);
617                     PolyLine2d rightEdge = this.designLine
618                             .flattenOffset(LaneGeometryUtil.getRightEdgeOffsets(this.designLine, slices), flattener);
619                     Polygon2d contour = LaneGeometryUtil.getContour(leftEdge, rightEdge);
620 
621                     if (node.getNodeName().equals("Stripe"))
622                     {
623                         StripeAnimation stripe =
624                                 new StripeAnimation(
625                                         new MapStripeData(StripeData.Type.valueOf(type.name()), width,
626                                                 slices.get(0).getOffset(), getNode(), centerLine, contour, sliceInfo),
627                                         getMap().getContextualized());
628                         this.crossSectionElements.add(stripe);
629                     }
630                     else
631                     {
632                         if (node.getNodeName().equals("Lane"))
633                         {
634                             MapLaneData laneData = new MapLaneData(node.getId(), getNode(), centerLine, contour, sliceInfo);
635                             LaneAnimation lane =
636                                     new LaneAnimation(laneData, getMap().getContextualized(), Color.GRAY.brighter());
637                             this.crossSectionElements.add(lane);
638                             this.laneData.put(node.getId(), laneData);
639                         }
640                         else if (node.getNodeName().equals("Shoulder"))
641                         {
642                             CrossSectionElementAnimation<?> shoulder = new CrossSectionElementAnimation<>(
643                                     new MapShoulderData(slices.get(0).getOffset(), getNode(), centerLine, contour, sliceInfo),
644                                     getMap().getContextualized(), Color.DARK_GRAY);
645                             this.crossSectionElements.add(shoulder);
646                         }
647                         else if (node.getNodeName().equals("NoTrafficLane"))
648                         {
649                             CrossSectionElementAnimation<?> noTrafficLane = new CrossSectionElementAnimation<>(
650                                     new MapCrossSectionData(getNode(), centerLine, contour, sliceInfo),
651                                     getMap().getContextualized(), Color.DARK_GRAY);
652                             this.crossSectionElements.add(noTrafficLane);
653                         }
654                         else
655                         {
656                             throw new RuntimeException(
657                                     "Element " + node.getNodeName() + " is not a supported cross-section element.");
658                         }
659                     }
660                 }
661                 catch (RemoteException | NamingException exception)
662                 {
663                     throw new RuntimeException("Exception while building layout.");
664                 }
665             }
666         }
667         Try.execute(() -> this.fireEvent(LAYOUT_REBUILT, this), "Unable to fire LAYOUT event.");
668     }
669 
670     /** {@inheritDoc} */
671     @Override
672     public EventListenerMap getEventListenerMap() throws RemoteException
673     {
674         return this.eventListenerMap;
675     }
676 
677     /** {@inheritDoc} */
678     @Override
679     public void evalChanged()
680     {
681         if (getNode().isActive())
682         {
683             this.id = getNode().getId() == null ? "" : getNode().getId();
684             this.nodeStart = replaceNode(this.nodeStart, getNode().getCoupledKeyrefNodeAttribute("NodeStart"));
685             this.nodeEnd = replaceNode(this.nodeEnd, getNode().getCoupledKeyrefNodeAttribute("NodeEnd"));
686             setValue((v) -> this.offsetStart = v, Adapters.get(Length.class), getNode(), "OffsetStart");
687             setValue((v) -> this.offsetEnd = v, Adapters.get(Length.class), getNode(), "OffsetEnd");
688             this.shapeListener.update();
689             buildDesignLine();
690         }
691     }
692 
693     /**
694      * Notification from the Map that a node (Ots.Network.Node) id was changed.
695      * @param node XsdTreeNode; node.
696      */
697     public void notifyNodeIdChanged(final XsdTreeNode node)
698     {
699         this.nodeStart = replaceNode(this.nodeStart, getNode().getCoupledKeyrefNodeAttribute("NodeStart"));
700         this.nodeEnd = replaceNode(this.nodeEnd, getNode().getCoupledKeyrefNodeAttribute("NodeEnd"));
701         buildDesignLine();
702     }
703 
704     /**
705      * Returns the value with appropriate adapter, or {@code null} if the value is {@code null}.
706      * @param <T> type of the value after unmarshaling.
707      * @param value String; value.
708      * @param adapter ExpressionAdapter&lt;T, ?&gt;; adapter for values of type T.
709      * @return T; unmarshaled value.
710      */
711     private <T> T orNull(final String value, final ExpressionAdapter<T, ?> adapter)
712     {
713         try
714         {
715             return value == null ? null : adapter.unmarshal(value).get(getEval());
716         }
717         catch (IllegalArgumentException ex)
718         {
719             // illegal value for adapter
720             return null;
721         }
722     }
723 
724     /**
725      * Returns the editor lane data for the lane of given id.
726      * @param laneId String; id.
727      * @return EditorLaneData; editor lane data for the lane of given id.
728      */
729     public MapLaneData getLaneData(final String laneId)
730     {
731         return this.laneData.get(laneId);
732     }
733 
734     /**
735      * Listener to events that affect the shape. This class can also deliver the resulting line.
736      * <p>
737      * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
738      * <br>
739      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
740      * </p>
741      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
742      */
743     private class ShapeListener implements EventListener
744     {
745         /** */
746         private static final long serialVersionUID = 20231020L;
747 
748         /** Node of the shape. */
749         private XsdTreeNode shapeNode;
750 
751         /** Bezier shape. */
752         private Double shape;
753 
754         /** Bezier weighted or not. */
755         private Boolean weighted;
756 
757         /** Clothoid start curvature. */
758         private LinearDensity startCurvature;
759 
760         /** Clothoid end curvature. */
761         private LinearDensity endCurvature;
762 
763         /** Clothoid length. */
764         private Length length;
765 
766         /** Clothoid a-value. */
767         private Length a;
768 
769         /** Arc radius. */
770         private Length radius;
771 
772         /** Arc direction. */
773         private ArcDirection direction;
774 
775         /** Polyline coordinates. */
776         public SortedMap<XsdTreeNode, Point2d> coordinates = new TreeMap<>(new Comparator<>()
777         {
778             /** {@inheritDoc} */
779             @Override
780             public int compare(final XsdTreeNode o1, final XsdTreeNode o2)
781             {
782                 List<XsdTreeNode> list = ShapeListener.this.shapeNode.getChildren();
783                 return Integer.compare(list.indexOf(o1), list.indexOf(o2));
784             }
785         });
786 
787         /** {@inheritDoc} */
788         @Override
789         public void notify(final Event event) throws RemoteException
790         {
791             if (event.getType().equals(XsdTreeNode.OPTION_CHANGED))
792             {
793                 XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[1];
794                 if (node.getParent().equals(this.shapeNode))
795                 {
796                     // clothoid option changed
797                     for (XsdTreeNode option : node.getChildren())
798                     {
799                         option.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
800                     }
801                     // later as values may not be loaded yet during loading
802                     SwingUtilities.invokeLater(() -> update());
803                 }
804                 else
805                 {
806                     // shape node changed
807                     if (this.shapeNode != null)
808                     {
809                         if (this.shapeNode.getChildCount() > 0)
810                         {
811                             this.shapeNode.getChild(0).removeListener(this, XsdTreeNode.OPTION_CHANGED);
812                         }
813                         this.shapeNode.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
814                     }
815                     this.shapeNode = node;
816                     this.shapeNode.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
817                     if (this.shapeNode.getNodeName().equals("Polyline"))
818                     {
819                         for (XsdTreeNode option : this.shapeNode.getChildren())
820                         {
821                             option.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
822                             option.addListener(this, XsdTreeNode.MOVED, ReferenceType.WEAK);
823                         }
824                     }
825                     else if (this.shapeNode.getNodeName().equals("Clothoid"))
826                     {
827                         this.shapeNode.getChild(0).addListener(this, XsdTreeNode.OPTION_CHANGED, ReferenceType.WEAK);
828                         for (XsdTreeNode option : this.shapeNode.getChild(0).getChildren())
829                         {
830                             option.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
831                         }
832                     }
833                     if (this.shapeNode.getNodeName().equals("Clothoid") || this.shapeNode.getNodeName().equals("Arc")
834                             || this.shapeNode.getNodeName().equals("Bezier"))
835                     {
836                         setFlattenerListener();
837                     }
838                     // later as values/coordinates may not be loaded yet during loading
839                     SwingUtilities.invokeLater(() -> update());
840                 }
841                 buildDesignLine();
842             }
843             else if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED))
844             {
845                 setAttribute((String) ((Object[]) event.getContent())[1]);
846                 buildDesignLine();
847             }
848             else if (event.getType().equals(XsdTreeNode.VALUE_CHANGED))
849             {
850                 // clothoid option specification or polyline coordinate
851                 Object[] content = (Object[]) event.getContent();
852                 XsdTreeNode node = (XsdTreeNode) content[0];
853                 try
854                 {
855                     switch (node.getNodeName())
856                     {
857                         case "Coordinate":
858                             this.coordinates.put(node, orNull(node.getValue(), Adapters.get(Point2d.class)));
859                             break;
860                         case "StartCurvature":
861                             this.startCurvature = orNull(node.getValue(), Adapters.get(LinearDensity.class));
862                             break;
863                         case "EndCurvature":
864                             this.endCurvature = orNull(node.getValue(), Adapters.get(LinearDensity.class));
865                             break;
866                         case "Length":
867                             this.length = orNull(node.getValue(), Adapters.get(Length.class));
868                             break;
869                         case "A":
870                             this.a = orNull(node.getValue(), Adapters.get(Length.class));
871                             break;
872                     }
873                 }
874                 catch (Exception ex)
875                 {
876                     // leave line as is, new value is not a valid value
877                     return;
878                 }
879                 buildDesignLine();
880             }
881             else if (event.getType().equals(XsdTreeNode.MOVED))
882             {
883                 // order of coordinates changed
884                 update();
885                 buildDesignLine();
886             }
887             else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
888             {
889                 // flattener node
890                 boolean activated = (boolean) ((Object[]) event.getContent())[1];
891                 if (activated)
892                 {
893                     setFlattenerListener();
894                 }
895                 else
896                 {
897                     if (MapLinkData.this.flattenerListener != null)
898                     {
899                         MapLinkData.this.flattenerListener.destroy();
900                     }
901                     MapLinkData.this.flattenerListener = null;
902                 }
903                 buildDesignLine();
904             }
905         }
906 
907         /**
908          * Sets the flattener listener in the link.
909          */
910         private void setFlattenerListener()
911         {
912             if (MapLinkData.this.flattenerListener != null)
913             {
914                 MapLinkData.this.flattenerListener.destroy();
915             }
916             MapLinkData.this.flattenerListener = new FlattenerListener(this.shapeNode.getChild(0), () -> getEval());
917             MapLinkData.this.flattenerListener.addListener(MapLinkData.this, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
918             this.shapeNode.getChild(0).addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
919         }
920 
921         /**
922          * Update the line, clearing all fields, and setting any already available attributes (as the shape node was previously
923          * selected and edited).
924          */
925         private void update()
926         {
927             this.shape = null;
928             this.weighted = null;
929             this.startCurvature = null;
930             this.endCurvature = null;
931             this.length = null;
932             this.a = null;
933             this.radius = null;
934             this.direction = null;
935             this.coordinates.clear();
936             switch (this.shapeNode.getNodeName())
937             {
938                 case "Straight":
939                     buildDesignLine();
940                     break;
941                 case "Polyline":
942                     for (XsdTreeNode child : this.shapeNode.getChildren())
943                     {
944                         try
945                         {
946                             this.coordinates.put(child, orNull(child.getValue(), Adapters.get(Point2d.class)));
947                         }
948                         catch (Exception ex)
949                         {
950                             throw new RuntimeException("Expression adapter could not unmarshal value for polyline coordinate.");
951                         }
952                     }
953                     buildDesignLine();
954                     break;
955                 case "Bezier":
956                     setAttribute("Shape");
957                     setAttribute("Weighted");
958                     buildDesignLine();
959                     setFlattenerListener();
960                     break;
961                 case "Clothoid":
962                     if (this.shapeNode.getChildCount() > 0 && this.shapeNode.getChild(0).getChildCount() > 0)
963                     {
964                         // child is an xsd:sequence, take children from that
965                         for (int childIndex = 0; childIndex < this.shapeNode.getChild(0).getChildCount(); childIndex++)
966                         {
967                             XsdTreeNode child = this.shapeNode.getChild(0).getChild(childIndex);
968                             try
969                             {
970                                 switch (child.getNodeName())
971                                 {
972                                     case "StartCurvature":
973                                         this.startCurvature = orNull(child.getValue(), Adapters.get(LinearDensity.class));
974                                         break;
975                                     case "EndCurvature":
976                                         this.endCurvature = orNull(child.getValue(), Adapters.get(LinearDensity.class));
977                                         break;
978                                     case "Length":
979                                         this.length = orNull(child.getValue(), Adapters.get(Length.class));
980                                         break;
981                                     case "A":
982                                         this.a = orNull(child.getValue(), Adapters.get(Length.class));
983                                         break;
984                                     default:
985                                         throw new RuntimeException("Clothoid child " + child.getNodeName() + " not supported.");
986                                 }
987                             }
988                             catch (Exception ex)
989                             {
990                                 throw new RuntimeException("Expression adapter could not unmarshal value for Clothoid child "
991                                         + child.getNodeName());
992                             }
993                         }
994                     }
995                     buildDesignLine();
996                     setFlattenerListener();
997                     break;
998                 case "Arc":
999                     setAttribute("Radius");
1000                     setAttribute("Direction");
1001                     buildDesignLine();
1002                     setFlattenerListener();
1003                     break;
1004                 default:
1005                     throw new RuntimeException("Drawing of shape node " + this.shapeNode.getNodeName() + " is not supported.");
1006             }
1007         }
1008 
1009         /**
1010          * Set the given attribute from the shape node.
1011          * @param attribute String; attribute name.
1012          */
1013         private void setAttribute(final String attribute)
1014         {
1015             if (this.shapeNode.reportInvalidAttributeValue(this.shapeNode.getAttributeIndexByName(attribute)) != null)
1016             {
1017                 // invalid value, do nothing
1018                 return;
1019             }
1020             switch (attribute)
1021             {
1022                 case "Shape":
1023                     this.shape = getOrNull(attribute, Adapters.get(Double.class));
1024                     break;
1025                 case "Weighted":
1026                     this.weighted = getOrNull(attribute, Adapters.get(Boolean.class));
1027                     break;
1028                 case "Length":
1029                     this.length = getOrNull(attribute, Adapters.get(Length.class));
1030                     break;
1031                 case "Radius":
1032                     this.radius = getOrNull(attribute, Adapters.get(Length.class));
1033                     break;
1034                 case "Direction":
1035                     this.direction = getOrNull(attribute, Adapters.get(ArcDirection.class));
1036                     break;
1037                 default:
1038                     // an attribute was changed that does not change the shape
1039             }
1040         }
1041 
1042         /**
1043          * Returns the attribute value with appropriate adapter, or {@code null} if the attribute is not given.
1044          * @param <T> type of the attribute value after unmarshaling.
1045          * @param attribute String; attribute.
1046          * @param adapter ExpressionAdapter&lt;T, ?&gt;; adapter for values of type T.
1047          * @return T; unmarshaled value.
1048          */
1049         private <T> T getOrNull(final String attribute, final ExpressionAdapter<T, ?> adapter)
1050         {
1051             String value = this.shapeNode.getAttributeValue(attribute);
1052             return orNull(value, adapter);
1053         }
1054 
1055         /**
1056          * Returns the continuous line.
1057          * @param from OrientedPoint2d; possibly offset start point.
1058          * @param to OrientedPoint2d; possibly offset end point.
1059          * @return PolyLine2d; line from the shape and attributes.
1060          */
1061         public ContinuousLine getContiuousLine(final OrientedPoint2d from, final OrientedPoint2d to)
1062         {
1063             try
1064             {
1065                 switch (this.shapeNode.getNodeName())
1066                 {
1067                     case "Straight":
1068                         double length = from.distance(to);
1069                         return new ContinuousStraight(from, length);
1070                     case "Polyline":
1071                         List<Point2d> list = new ArrayList<>();
1072                         list.add(from);
1073                         for (Entry<XsdTreeNode, Point2d> entry : this.coordinates.entrySet())
1074                         {
1075                             list.add(entry.getValue());
1076                         }
1077                         list.add(to);
1078                         if (list.contains(null))
1079                         {
1080                             return null;
1081                         }
1082                         return new ContinuousPolyLine(new PolyLine2d(list), from, to);
1083                     case "Bezier":
1084                         double shape = this.shape == null ? 1.0 : this.shape;
1085                         boolean weighted = this.weighted == null ? false : this.weighted;
1086                         Point2d[] points = Bezier.cubicControlPoints(from, to, shape, weighted);
1087                         return new ContinuousBezierCubic(points[0], points[1], points[2], points[3]);
1088                     case "Clothoid":
1089                         if (this.shapeNode.getChildCount() == 0 || this.shapeNode.getChild(0).getChildCount() == 0
1090                                 || this.shapeNode.getChild(0).getChild(0).getNodeName().equals("Interpolated"))
1091                         {
1092                             return new ContinuousClothoid(from, to);
1093                         }
1094                         else if (this.shapeNode.getChild(0).getChild(0).getNodeName().equals("Length"))
1095                         {
1096                             if (this.length == null || this.startCurvature == null || this.endCurvature == null)
1097                             {
1098                                 return null;
1099                             }
1100                             return ContinuousClothoid.withLength(from, this.length.si, this.startCurvature.si,
1101                                     this.endCurvature.si);
1102                         }
1103                         else
1104                         {
1105                             if (this.a == null || this.startCurvature == null || this.endCurvature == null)
1106                             {
1107                                 return null;
1108                             }
1109                             return new ContinuousClothoid(from, this.a.si, this.startCurvature.si, this.endCurvature.si);
1110                         }
1111                     case "Arc":
1112                         if (this.direction == null || this.radius == null)
1113                         {
1114                             return null;
1115                         }
1116                         boolean left = this.direction.equals(ArcDirection.LEFT);
1117                         double endHeading = to.dirZ;
1118                         while (left && endHeading < from.dirZ)
1119                         {
1120                             endHeading += 2.0 * Math.PI;
1121                         }
1122                         while (!left && endHeading > from.dirZ)
1123                         {
1124                             endHeading -= 2.0 * Math.PI;
1125                         }
1126                         Angle angle = Angle.instantiateSI(left ? endHeading - from.dirZ : from.dirZ - endHeading);
1127                         return new ContinuousArc(from, this.radius.si, left, angle);
1128                     default:
1129                         throw new RuntimeException(
1130                                 "Drawing of shape node " + this.shapeNode.getNodeName() + " is not supported.");
1131                 }
1132             }
1133             catch (DrawRuntimeException exception)
1134             {
1135                 // Probably a degenerate line as nodes are at the same location
1136                 return null;
1137             }
1138         }
1139     }
1140 
1141     /**
1142      * Returns the start curvature from the clothoid.
1143      * @return Length; start curvature from the clothoid.
1144      */
1145     public LinearDensity getClothoidStartCurvature()
1146     {
1147         if (this.designLine != null && this.designLine instanceof ContinuousClothoid)
1148         {
1149             return LinearDensity.instantiateSI(((ContinuousClothoid) this.designLine).getStartCurvature());
1150         }
1151         return null;
1152     }
1153 
1154     /**
1155      * Returns the end curvature from the clothoid.
1156      * @return Length; end curvature from the clothoid.
1157      */
1158     public LinearDensity getClothoidEndCurvature()
1159     {
1160         if (this.designLine != null && this.designLine instanceof ContinuousClothoid)
1161         {
1162             return LinearDensity.instantiateSI(((ContinuousClothoid) this.designLine).getEndCurvature());
1163         }
1164         return null;
1165     }
1166 
1167     /**
1168      * Returns the length from the clothoid.
1169      * @return Length; length from the clothoid.
1170      */
1171     public Length getClothoidLength()
1172     {
1173         if (this.designLine != null && this.designLine instanceof ContinuousClothoid)
1174         {
1175             return Length.instantiateSI(((ContinuousClothoid) this.designLine).getLength());
1176         }
1177         return null;
1178     }
1179 
1180     /**
1181      * Returns the A value from the clothoid.
1182      * @return Length; A value from the clothoid.
1183      */
1184     public Length getClothoidA()
1185     {
1186         if (this.designLine != null && this.designLine instanceof ContinuousClothoid)
1187         {
1188             return Length.instantiateSI(((ContinuousClothoid) this.designLine).getA());
1189         }
1190         return null;
1191     }
1192 
1193     /**
1194      * Returns whether the shape was applied as a Clothoid, an Arc, or as a Straight, depending on start and end position and
1195      * direction.
1196      * @return String; "Clothoid", "Arc" or "Straight".
1197      */
1198     public String getClothoidAppliedShape()
1199     {
1200         if (this.designLine != null && this.designLine instanceof ContinuousClothoid)
1201         {
1202             return ((ContinuousClothoid) this.designLine).getAppliedShape();
1203         }
1204         return null;
1205     }
1206 
1207     /** {@inheritDoc} */
1208     @Override
1209     public String toString()
1210     {
1211         return "Link " + this.id;
1212     }
1213 
1214 }