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