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