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.NavigableMap;
15 import java.util.Set;
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.math.AngleUtil;
43 import org.djutils.metadata.MetaData;
44 import org.djutils.metadata.ObjectDescriptor;
45 import org.opentrafficsim.animation.network.LinkAnimation.LinkData;
46 import org.opentrafficsim.animation.road.CrossSectionElementAnimation;
47 import org.opentrafficsim.animation.road.LaneAnimation;
48 import org.opentrafficsim.animation.road.PriorityAnimation;
49 import org.opentrafficsim.animation.road.StripeAnimation;
50 import org.opentrafficsim.base.OtsRuntimeException;
51 import org.opentrafficsim.base.StripeElement;
52 import org.opentrafficsim.base.StripeElement.StripeLateralSync;
53 import org.opentrafficsim.base.geometry.OtsGeometryUtil;
54 import org.opentrafficsim.base.geometry.OtsShape;
55 import org.opentrafficsim.base.logger.Logger;
56 import org.opentrafficsim.core.geometry.CurveFlattener;
57 import org.opentrafficsim.core.geometry.PolyLineCurve2d;
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.CrossSectionGeometry;
64 import org.opentrafficsim.road.network.StripeData.StripePhaseSync;
65 import org.opentrafficsim.road.network.factory.xml.utils.RoadLayoutOffsets.CseData;
66 import org.opentrafficsim.road.network.factory.xml.utils.StripeSynchronization;
67 import org.opentrafficsim.xml.bindings.ExpressionAdapter;
68 import org.opentrafficsim.xml.bindings.types.ArcDirectionType.ArcDirection;
69 import org.opentrafficsim.xml.generated.Link.Arc;
70 import org.opentrafficsim.xml.generated.Link.Bezier;
71 import org.opentrafficsim.xml.generated.Link.Clothoid;
72 import org.opentrafficsim.xml.generated.Link.Polyline;
73
74 import nl.tudelft.simulation.dsol.animation.d2.Renderable2d;
75
76
77
78
79
80
81
82
83
84
85 public class MapLinkData extends MapData implements LinkData, EventListener, EventProducer
86 {
87
88
89 public static final EventType LAYOUT_REBUILT = new EventType("LAYOUTREBUILT", new MetaData("LAYOUT", "Layout is rebuilt.",
90 new ObjectDescriptor("LinkData", "Map link data object.", MapLinkData.class)));
91
92
93 private final EventListenerMap eventListenerMap = new EventListenerMap();
94
95
96 private final ShapeListener shapeListener = new ShapeListener();
97
98
99 private String id = "";
100
101
102 private XsdTreeNode nodeStart;
103
104
105 private XsdTreeNode nodeEnd;
106
107
108 private Direction directionStart = Direction.ZERO;
109
110
111 private Direction directionEnd = Direction.ZERO;
112
113
114 private Length offsetStart;
115
116
117 private Length offsetEnd;
118
119
120 private Point2d from;
121
122
123 private Point2d to;
124
125
126 private OffsetCurve2d designLine = null;
127
128
129 private PolyLine2d flattenedDesignLine = null;
130
131
132 private DirectedPoint2d location;
133
134
135 private Polygon2d absoluteContour;
136
137
138 private Polygon2d relativeContour;
139
140
141 private XsdTreeNode roadLayoutNode;
142
143
144 private XsdTreeNode definedRoadLayoutNode;
145
146
147 private RoadLayoutListener roadLayoutListener;
148
149
150 private FlattenerListener flattenerListener;
151
152
153 private Set<Renderable2d<?>> crossSectionElements = new LinkedHashSet<>();
154
155
156 private java.util.Map<String, MapLaneData> laneData = new LinkedHashMap<>();
157
158
159 private java.util.Map<String, MapStripeData> stripeData = new LinkedHashMap<>();
160
161
162 private PriorityAnimation priorityAnimation;
163
164
165 private final boolean isConnector;
166
167
168 private XsdTreeNode centroid;
169
170
171 private XsdTreeNode connectorNode;
172
173
174 private boolean dirtyGeometry = true;
175
176
177 private boolean dirtyLayout = true;
178
179
180
181
182
183
184
185 public MapLinkData(final EditorMap map, final XsdTreeNode linkNode, final OtsEditor editor)
186 {
187 super(map, linkNode, editor);
188
189 linkNode.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
190 this.isConnector = "Connector".equals(linkNode.getNodeName());
191 if (this.isConnector)
192 {
193 SwingUtilities.invokeLater(() ->
194 {
195
196
197 notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "Id", null}));
198 this.centroid = replaceNode(this.centroid, linkNode.getCoupledNodeAttribute("Centroid").orElse(null));
199 this.connectorNode = replaceNode(this.connectorNode, linkNode.getCoupledNodeAttribute("Node").orElse(null));
200 setDirtyEndCheckValidity();
201 });
202 return;
203 }
204 if (Set.of("Straight", "PolyLine", "Bezier", "Clothoid", "Arc").contains(linkNode.getChild(0).getNodeName()))
205 {
206 linkNode.getChild(0).addListener(this.shapeListener, XsdTreeNode.OPTION_CHANGED, ReferenceType.WEAK);
207 this.shapeListener.shapeNode = linkNode.getChild(0);
208 }
209
210
211 SwingUtilities.invokeLater(() ->
212 {
213 XsdTreeNode layout = linkNode.getChild(1);
214 layout.addListener(this, XsdTreeNode.OPTION_CHANGED, ReferenceType.WEAK);
215 if (layout.getOption().equals(layout))
216 {
217 notify(new Event(XsdTreeNode.OPTION_CHANGED, new Object[] {layout, layout, layout}));
218 }
219 });
220
221
222 if (getNode().isActive())
223 {
224 SwingUtilities.invokeLater(() ->
225 {
226
227
228 this.shapeListener.shapeNode = linkNode.getChild(0);
229 linkNode.getChild(0).addListener(this.shapeListener, XsdTreeNode.OPTION_CHANGED, ReferenceType.WEAK);
230
231 notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "Id", null}));
232 this.nodeStart = replaceNode(this.nodeStart, linkNode.getCoupledNodeAttribute("NodeStart").orElse(null));
233 this.nodeEnd = replaceNode(this.nodeEnd, linkNode.getCoupledNodeAttribute("NodeEnd").orElse(null));
234 notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "OffsetStart", null}));
235 notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "OffsetEnd", null}));
236 XsdTreeNode shape = linkNode.getChild(0);
237 this.shapeListener.notify(new Event(XsdTreeNode.OPTION_CHANGED, new Object[] {shape, shape, shape}));
238
239 setDirtyEndCheckValidity();
240 });
241 }
242 }
243
244 @Override
245 public void destroy()
246 {
247 super.destroy();
248 for (Renderable2d<?> renderable : this.crossSectionElements)
249 {
250 getMap().removeAnimation(renderable);
251 }
252 this.crossSectionElements.clear();
253 if (this.roadLayoutListener != null)
254 {
255 this.roadLayoutListener.destroy();
256 this.roadLayoutListener = null;
257 }
258 if (this.definedRoadLayoutNode != null)
259 {
260 this.definedRoadLayoutNode.removeListener(this, XsdTreeNode.VALUE_CHANGED);
261 this.definedRoadLayoutNode = null;
262 }
263 if (this.flattenerListener != null)
264 {
265 this.flattenerListener.destroy();
266 this.flattenerListener = null;
267 }
268 if (this.priorityAnimation != null)
269 {
270 getMap().removeAnimation(this.priorityAnimation);
271 }
272 if (this.nodeStart != null)
273 {
274 this.nodeStart.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
275 }
276 if (this.nodeEnd != null)
277 {
278 this.nodeEnd.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
279 }
280 this.roadLayoutNode = null;
281 }
282
283
284
285
286 private void checkDirtyGeometry()
287 {
288 if (this.dirtyGeometry)
289 {
290 buildDesignLine();
291 }
292 else if (this.dirtyLayout)
293 {
294 buildLayout();
295 }
296 }
297
298 @Override
299 public DirectedPoint2d getLocation()
300 {
301 checkDirtyGeometry();
302 return this.location;
303 }
304
305 @Override
306 public Polygon2d getAbsoluteContour()
307 {
308 checkDirtyGeometry();
309 return this.absoluteContour;
310 }
311
312 @Override
313 public Polygon2d getRelativeContour()
314 {
315 checkDirtyGeometry();
316 return this.relativeContour;
317 }
318
319 @Override
320 public String getId()
321 {
322 return this.id;
323 }
324
325 @Override
326 public boolean isConnector()
327 {
328 return this.isConnector;
329 }
330
331 @Override
332 public PolyLine2d getCenterLine()
333 {
334 checkDirtyGeometry();
335 return this.flattenedDesignLine;
336 }
337
338 @Override
339 public PolyLine2d getLine()
340 {
341 return OtsShape.transformLine(getCenterLine(), getLocation());
342 }
343
344 @Override
345 public void notify(final Event event)
346 {
347 if (event.getType().equals(XsdTreeNode.OPTION_CHANGED))
348 {
349 Object[] content = (Object[]) event.getContent();
350 XsdTreeNode selected = (XsdTreeNode) content[1];
351 if (selected.getNodeName().equals("RoadLayout") || (selected.getNodeName().equals("xsd:sequence")
352 && selected.getChildCount() > 0 && selected.getChild(0).getNodeName().equals("DefinedLayout")))
353 {
354
355 if (this.roadLayoutListener != null)
356 {
357 this.roadLayoutListener.destroy();
358 }
359 else if (this.roadLayoutNode != null)
360 {
361 getMap().getRoadLayoutListener(this.roadLayoutNode).removeListener(this, ChangeListener.CHANGE_EVENT);
362 }
363 if (this.definedRoadLayoutNode != null)
364 {
365 this.definedRoadLayoutNode.removeListener(this, XsdTreeNode.VALUE_CHANGED);
366 }
367 if (selected.getNodeName().equals("RoadLayout"))
368 {
369 this.roadLayoutNode = selected;
370 this.definedRoadLayoutNode = null;
371 this.roadLayoutListener = new RoadLayoutListener(selected, this::getEval);
372 this.roadLayoutListener.addListener(this, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
373 }
374 else
375 {
376 this.definedRoadLayoutNode = selected.getChild(0);
377 this.definedRoadLayoutNode.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
378 this.roadLayoutNode = this.definedRoadLayoutNode.getCoupledNodeValue().orElse(null);
379 if (this.roadLayoutNode != null)
380 {
381 getMap().getRoadLayoutListener(this.roadLayoutNode).addListener(this, ChangeListener.CHANGE_EVENT,
382 ReferenceType.WEAK);
383 }
384 this.roadLayoutListener = null;
385 }
386 }
387 else
388 {
389
390 if (this.flattenerListener != null)
391 {
392 this.flattenerListener.destroy();
393 }
394 this.flattenerListener = new FlattenerListener(selected, this::getEval);
395 }
396 this.dirtyLayout = true;
397 return;
398 }
399 else if (event.getType().equals(XsdTreeNode.VALUE_CHANGED))
400 {
401
402 if (this.roadLayoutNode != null)
403 {
404 getMap().getRoadLayoutListener(this.roadLayoutNode).removeListener(this, ChangeListener.CHANGE_EVENT);
405 }
406 this.roadLayoutNode = this.definedRoadLayoutNode.getCoupledNodeValue().orElse(null);
407 if (this.roadLayoutNode != null)
408 {
409 getMap().getRoadLayoutListener(this.roadLayoutNode).addListener(this, ChangeListener.CHANGE_EVENT,
410 ReferenceType.WEAK);
411 }
412 this.dirtyLayout = true;
413 return;
414 }
415 else if (event.getType().equals(ChangeListener.CHANGE_EVENT))
416 {
417 XsdTreeNode node = (XsdTreeNode) event.getContent();
418 if (node.getNodeName().equals("RoadLayout") || node.getNodeName().equals("DefinedLayout"))
419 {
420 if (node.isIdentifiable() && this.definedRoadLayoutNode != null)
421 {
422
423 this.roadLayoutNode = this.definedRoadLayoutNode.getCoupledNodeValue().orElse(null);
424 }
425 if (node.equals(this.roadLayoutNode) && node.reportInvalidId().isEmpty())
426 {
427 this.dirtyLayout = true;
428 }
429 }
430 else
431 {
432
433 setDirtyEndCheckValidity();
434 }
435 return;
436 }
437
438
439 Object[] content = (Object[]) event.getContent();
440 XsdTreeNode node = (XsdTreeNode) content[0];
441 String attribute = (String) content[1];
442 String value = node.getAttributeValueOrDefault(attribute);
443
444 if ("Id".equals(attribute))
445 {
446 this.id = value == null ? "" : value;
447 return;
448 }
449 else if ("NodeStart".equals(attribute))
450 {
451 this.nodeStart = replaceNode(this.nodeStart, getNode().getCoupledNodeAttribute("NodeStart").orElse(null));
452 }
453 else if ("NodeEnd".equals(attribute))
454 {
455 this.nodeEnd = replaceNode(this.nodeEnd, getNode().getCoupledNodeAttribute("NodeEnd").orElse(null));
456 }
457 else if ("Centroid".equals(attribute))
458 {
459 this.centroid = replaceNode(this.centroid, getNode().getCoupledNodeAttribute("Centroid").orElse(null));
460 }
461 else if ("Node".equals(attribute))
462 {
463 this.connectorNode = replaceNode(this.connectorNode, getNode().getCoupledNodeAttribute("Node").orElse(null));
464 }
465 else if ("OffsetStart".equals(attribute))
466 {
467 setValue((v) -> this.offsetStart = v, Adapters.get(Length.class), getNode(), attribute);
468 }
469 else if ("OffsetEnd".equals(attribute))
470 {
471 setValue((v) -> this.offsetEnd = v, Adapters.get(Length.class), getNode(), attribute);
472 }
473 else if (!"Coordinate".equals(attribute) && !"Direction".equals(attribute))
474 {
475
476
477 return;
478 }
479 setDirtyEndCheckValidity();
480 }
481
482
483
484
485
486
487
488
489 private XsdTreeNode replaceNode(final XsdTreeNode oldNode, final XsdTreeNode newNode)
490 {
491 if (oldNode != null)
492 {
493 oldNode.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
494 if (oldNode.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETER_STRING))
495 {
496 XsdTreeNode node = getInputNode(newNode);
497 if (node != null)
498 {
499 node.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
500 }
501 }
502 }
503 if (newNode != null)
504 {
505 newNode.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
506 if (newNode.getPathString().equals(XsdPaths.DEFAULT_INPUT_PARAMETER_STRING))
507 {
508 XsdTreeNode node = getInputNode(newNode);
509 if (node != null)
510 {
511 node.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
512 }
513 return node;
514 }
515 }
516 return newNode;
517 }
518
519
520
521
522
523
524 private XsdTreeNode getInputNode(final XsdTreeNode inputParameter)
525 {
526 String inputId = inputParameter.getId();
527 String nodeId;
528 try
529 {
530 nodeId = (String) getEval().evaluate(inputId.substring(1, inputId.length() - 1));
531 }
532 catch (IllegalArgumentException ex)
533 {
534
535 return null;
536 }
537 XsdTreeNode ots = inputParameter.getRoot();
538 for (XsdTreeNode child : ots.getChildren())
539 {
540 if (child.getPathString().equals(XsdPaths.NETWORK))
541 {
542 for (XsdTreeNode networkElement : child.getChildren())
543 {
544 if (networkElement.isType("Node") && nodeId != null && nodeId.equals(networkElement.getId()))
545 {
546 return networkElement;
547 }
548 }
549 }
550 }
551 return null;
552 }
553
554
555
556
557
558 public void addCoordinate(final XsdTreeNode node)
559 {
560 if (this.shapeListener.shapeNode != null && this.shapeListener.shapeNode.equals(node.getParent()))
561 {
562 this.shapeListener.coordinates.put(node, orNull(node.getValueOrDefault(), Adapters.get(Point2d.class)));
563 setDirtyEndCheckValidity();
564 node.addListener(this.shapeListener, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
565 node.addListener(this.shapeListener, XsdTreeNode.MOVED, ReferenceType.WEAK);
566 }
567 }
568
569
570
571
572
573 public void removeCoordinate(final XsdTreeNode node)
574 {
575
576
577 Iterator<XsdTreeNode> it = this.shapeListener.coordinates.keySet().iterator();
578 while (it.hasNext())
579 {
580 XsdTreeNode key = it.next();
581 if (node.equals(key))
582 {
583 it.remove();
584 setDirtyEndCheckValidity();
585 node.removeListener(this.shapeListener, XsdTreeNode.VALUE_CHANGED);
586 node.removeListener(this.shapeListener, XsdTreeNode.MOVED);
587 return;
588 }
589 }
590 }
591
592
593
594
595
596 private void setDirtyEndCheckValidity()
597 {
598 this.dirtyGeometry = true;
599
600
601 if (this.isConnector)
602 {
603 if (this.centroid == null || this.connectorNode == null)
604 {
605 setInvalid();
606 return;
607 }
608 setValid();
609 return;
610 }
611
612
613 if (this.nodeStart == null || this.nodeEnd == null || this.nodeStart.equals(this.nodeEnd)
614 || this.shapeListener.shapeNode == null)
615 {
616 setInvalid();
617 return;
618 }
619 setValue((v) -> this.from = v, Adapters.get(Point2d.class), this.nodeStart, "Coordinate");
620 setValue((v) -> this.to = v, Adapters.get(Point2d.class), this.nodeEnd, "Coordinate");
621 if (this.from == null || this.to == null)
622 {
623 setInvalid();
624 return;
625 }
626
627 setValid();
628 }
629
630
631
632
633 private void buildDesignLine()
634 {
635
636
637
638 if (this.isConnector)
639 {
640 setValue((v) -> this.from = v, Adapters.get(Point2d.class), this.centroid, "Coordinate");
641 setValue((v) -> this.to = v, Adapters.get(Point2d.class), this.connectorNode, "Coordinate");
642 DirectedPoint2d fromPoint = new DirectedPoint2d(this.from, this.from.directionTo(this.to));
643 this.designLine = new Straight2d(fromPoint, this.from.distance(this.to));
644 setGeometry();
645 return;
646 }
647
648
649 setValue((v) -> this.directionStart = v, Adapters.get(Direction.class), this.nodeStart, "Direction");
650 double dirStart = this.directionStart == null ? 0.0 : this.directionStart.si;
651 DirectedPoint2d fromPoint = new DirectedPoint2d(this.from, dirStart);
652 setValue((v) -> this.directionEnd = v, Adapters.get(Direction.class), this.nodeEnd, "Direction");
653 double dirEnd = this.directionEnd == null ? 0.0 : this.directionEnd.si;
654 DirectedPoint2d toPoint = new DirectedPoint2d(this.to, dirEnd);
655 if (this.offsetStart != null)
656 {
657 fromPoint = OtsGeometryUtil.offsetPoint(fromPoint, this.offsetStart.si);
658 }
659 if (this.offsetEnd != null)
660 {
661 toPoint = OtsGeometryUtil.offsetPoint(toPoint, this.offsetEnd.si);
662 }
663
664
665 this.designLine = this.shapeListener.getContiuousLine(fromPoint, toPoint);
666 if (this.designLine == null)
667 {
668 this.designLine = new Straight2d(fromPoint, fromPoint.distance(toPoint));
669 }
670 setGeometry();
671 if (this.priorityAnimation != null)
672 {
673 getMap().removeAnimation(this.priorityAnimation);
674 }
675 this.priorityAnimation = new PriorityAnimation(new MapPriorityData(this), getMap().getContextualized());
676 buildLayout();
677 }
678
679
680
681
682 private void setGeometry()
683 {
684 this.flattenedDesignLine = this.designLine.toPolyLine(getFlattener());
685 DirectedPoint2d point = this.flattenedDesignLine.getLocationFractionExtended(0.5);
686 this.location = new DirectedPoint2d(point.x, point.y, point.dirZ);
687 this.absoluteContour =
688 new Polygon2d(PolyLine2d.concatenate(this.flattenedDesignLine, this.flattenedDesignLine.reverse()).iterator());
689 this.relativeContour =
690 new Polygon2d(0.0, OtsShape.toRelativeTransform(this.location).transform(this.absoluteContour.iterator()));
691 this.dirtyGeometry = false;
692 }
693
694
695
696
697
698 private CurveFlattener getFlattener()
699 {
700 if (this.flattenerListener != null)
701 {
702 CurveFlattener flattener = this.flattenerListener.getData();
703 if (flattener != null)
704 {
705 return flattener;
706 }
707 }
708 return getMap().getNetworkFlattener();
709 }
710
711
712
713
714 private void buildLayout()
715 {
716 if (this.designLine == null)
717 {
718 return;
719 }
720 for (Renderable2d<?> renderable : this.crossSectionElements)
721 {
722 getMap().removeAnimation(renderable);
723 }
724 for (Entry<String, MapStripeData> entry : this.stripeData.entrySet())
725 {
726 getMap().getSynchronizableStripes().remove(entry.getValue());
727 }
728 this.crossSectionElements.clear();
729 this.laneData.clear();
730 this.stripeData.clear();
731 if (this.roadLayoutNode != null)
732 {
733 java.util.Map<XsdTreeNode, CseData> cseDataMap = this.roadLayoutListener != null ? this.roadLayoutListener.getData()
734 : getMap().getRoadLayoutListener(this.roadLayoutNode).getData();
735 MiddleOffset middleOffset = new MiddleOffset();
736 Map<String, XsdTreeNode> laneOverrides = new LinkedHashMap<>();
737 Map<String, XsdTreeNode> stripeOverrides = new LinkedHashMap<>();
738 if (new ChildNodeFinder(getNode()).hasActiveChild("DefinedLayout"))
739 {
740 for (XsdTreeNode child : getNode().getChildren())
741 {
742 if (child.getNodeName().equals("xsd:sequence"))
743 {
744 for (XsdTreeNode override : child.getChildren())
745 {
746 if (override.getNodeName().equals("LaneOverride") && override.isActive())
747 {
748 laneOverrides.put(override.getAttributeValueOrDefault("Lane"), override);
749 }
750 else if (override.getNodeName().equals("StripeOverride") && override.isActive())
751 {
752 stripeOverrides.put(override.getAttributeValueOrDefault("Stripe"), override);
753 }
754 }
755 }
756 }
757 }
758
759 for (Entry<XsdTreeNode, CseData> entry : cseDataMap.entrySet())
760 {
761 XsdTreeNode node = entry.getKey();
762 CseData cseData = entry.getValue();
763 ContinuousPiecewiseLinearFunction offsetFunc = ContinuousPiecewiseLinearFunction.of(0.0,
764 cseData.centerOffsetStart.si, 1.0, cseData.centerOffsetEnd.si);
765 if (node.getNodeName().equals("Stripe"))
766 {
767 StripeAnimation stripe = createStripe(node, offsetFunc, middleOffset, stripeOverrides);
768 if (stripe == null)
769 {
770 continue;
771 }
772 this.crossSectionElements.add(stripe);
773 }
774 else
775 {
776 ContinuousPiecewiseLinearFunction widthFunc =
777 ContinuousPiecewiseLinearFunction.of(0.0, cseData.widthStart.si, 1.0, cseData.widthEnd.si);
778 CrossSectionGeometry geometry =
779 CrossSectionGeometry.of(this.designLine, getFlattener(), offsetFunc, widthFunc);
780 if (node.getNodeName().equals("Lane"))
781 {
782 MapLaneData mapLaneData = new MapLaneData(node.getId(), getNode(), geometry);
783 LaneAnimation lane =
784 new LaneAnimation(mapLaneData, getMap().getContextualized(), Color.GRAY.brighter());
785 this.crossSectionElements.add(lane);
786 this.laneData.put(node.getId(), mapLaneData);
787 }
788 else if (node.getNodeName().equals("Shoulder"))
789 {
790 CrossSectionElementAnimation<?> shoulder = new CrossSectionElementAnimation<>(
791 new MapShoulderData(getNode(), geometry), getMap().getContextualized(), Color.DARK_GRAY);
792 this.crossSectionElements.add(shoulder);
793 }
794 else if (node.getNodeName().equals("NoTrafficLane"))
795 {
796 CrossSectionElementAnimation<?> noTrafficLane = new CrossSectionElementAnimation<>(
797 new MapCrossSectionData(getNode(), geometry), getMap().getContextualized(), Color.DARK_GRAY);
798 this.crossSectionElements.add(noTrafficLane);
799 }
800 else
801 {
802 throw new OtsRuntimeException(
803 "Element " + node.getNodeName() + " is not a supported cross-section element.");
804 }
805 }
806 }
807 }
808 StripeSynchronization.synchronize(new LinkedHashMap<>(getMap().getSynchronizableStripes()));
809 this.dirtyLayout = false;
810 fireEvent(LAYOUT_REBUILT, this);
811 }
812
813
814
815
816
817
818
819
820
821 private StripeAnimation createStripe(final XsdTreeNode node, final ContinuousPiecewiseLinearFunction offsetFunc,
822 final MiddleOffset middleOffset, final Map<String, XsdTreeNode> stripeOverrides)
823 {
824 StripePhaseSync phaseSync = StripePhaseSync.NONE;
825 Length dashOffset = Length.ZERO;
826 StripeLateralSync lateralSync = StripeLateralSync.NONE;
827 List<StripeElement> elements = new ArrayList<>();
828 XsdTreeNode stripeNode;
829 ChildNodeFinder stripeRefFinder = new ChildNodeFinder(node);
830 if (stripeRefFinder.hasActiveChild("DefinedStripe"))
831 {
832 stripeNode = stripeRefFinder.get().getCoupledNodeValue().orElse(null);
833 }
834 else if (stripeRefFinder.hasActiveChild("Custom"))
835 {
836 stripeNode = stripeRefFinder.get();
837 }
838 else
839 {
840 return null;
841 }
842 ChildNodeFinder stripeFinder = new ChildNodeFinder(stripeNode);
843 ChildNodeFinder overrideFinder =
844 stripeOverrides.containsKey(node.getId()) ? new ChildNodeFinder(stripeOverrides.get(node.getId())) : null;
845 if ((overrideFinder != null && overrideFinder.hasActiveChild("DashOffset"))
846 || stripeFinder.hasActiveChild("DashOffset"))
847 {
848 try
849 {
850 XsdTreeNode dashOffsetNode = overrideFinder != null && overrideFinder.hasActiveChild("DashOffset")
851 ? overrideFinder.get().getChild(0) : stripeFinder.get().getChild(0);
852
853 switch (dashOffsetNode.getNodeName())
854 {
855 case "SyncUpstream":
856 {
857 phaseSync = StripePhaseSync.UPSTREAM;
858 break;
859 }
860 case "SyncDownstream":
861 {
862 phaseSync = StripePhaseSync.DOWNSTREAM;
863 break;
864 }
865 case "Fixed":
866 {
867 phaseSync = StripePhaseSync.NONE;
868 dashOffset = Adapters.get(Length.class).unmarshal(dashOffsetNode.getAttributeValueOrDefault("Offset"))
869 .get(getEval());
870 break;
871 }
872 default:
873 {
874 Logger.ots().warn("Dash synchronization " + dashOffsetNode.getNodeName() + " is unknown.");
875 }
876 }
877 }
878 catch (Exception e)
879 {
880
881 }
882 }
883 if ((overrideFinder != null && overrideFinder.hasActiveChild("LateralSync"))
884 || stripeFinder.hasActiveChild("LateralSync"))
885 {
886 String latSyncName = overrideFinder != null && overrideFinder.hasActiveChild("DashOffset")
887 ? overrideFinder.get().getValueOrDefault() : stripeFinder.get().getValueOrDefault();
888 lateralSync = latSyncName == null ? StripeLateralSync.NONE
889 : Adapters.get(StripeLateralSync.class).unmarshal(latSyncName).get(getEval());
890 }
891 Length width = Length.ZERO;
892 List<XsdTreeNode> elementNodes;
893 if (overrideFinder != null && overrideFinder.hasActiveChild("Elements"))
894 {
895 elementNodes = overrideFinder.get().getChildren();
896 }
897 else if (stripeFinder.hasActiveChild("Elements"))
898 {
899 elementNodes = stripeFinder.get().getChildren();
900 }
901 else
902 {
903 elementNodes = Collections.emptyList();
904 }
905 for (XsdTreeNode elementNode : elementNodes)
906 {
907 if (elementNode.isValid())
908 {
909 Length w = Adapters.get(Length.class).unmarshal(elementNode.getAttributeValueOrDefault("Width")).get(getEval());
910 width = width.plus(w);
911 if (elementNode.getNodeName().equals("Line"))
912 {
913 String colorName = elementNode.getAttributeValueOrDefault("Color");
914 Color color = Adapters.get(Color.class).unmarshal(colorName).get(getEval());
915 if (elementNode.getChild(0).getNodeName().equals("Continuous"))
916 {
917 elements.add(StripeElement.continuous(w, color));
918 }
919 else
920 {
921 List<Double> gapsAndDashes = new ArrayList<>();
922 for (XsdTreeNode gapDash : elementNode.getChild(0).getChildren())
923 {
924 gapsAndDashes.add(Adapters.get(Length.class).unmarshal(gapDash.getChild(0).getValueOrDefault())
925 .get(getEval()).si);
926 gapsAndDashes.add(Adapters.get(Length.class).unmarshal(gapDash.getChild(1).getValueOrDefault())
927 .get(getEval()).si);
928 }
929 elements.add(StripeElement.dashed(w, color,
930 new LengthVector(gapsAndDashes.stream().mapToDouble(v -> v).toArray())));
931 }
932 }
933 else
934 {
935 elements.add(StripeElement.gap(w));
936 }
937 }
938 }
939 ContinuousPiecewiseLinearFunction widthFunc = ContinuousPiecewiseLinearFunction.of(0.0, width.si, 1.0, width.si);
940 CrossSectionGeometry geometry = CrossSectionGeometry.of(this.designLine, getFlattener(), offsetFunc, widthFunc);
941 middleOffset.addStartOffset(geometry.offset().get(0.0));
942 middleOffset.addEndOffset(geometry.offset().get(1.0));
943 MapStripeData data = new MapStripeData(dashOffset, getNode(), geometry, elements, lateralSync, this.flattenedDesignLine,
944 middleOffset, this.directionStart, this.directionEnd);
945 getMap().getSynchronizableStripes().put(data, new SynchronizableMapStripe(this, data, phaseSync));
946 this.stripeData.put(node.getId(), data);
947 return new StripeAnimation(data, getMap().getContextualized());
948 }
949
950
951
952
953
954 protected Collection<MapStripeData> getStripeData()
955 {
956 return this.stripeData.values();
957 }
958
959 @Override
960 public EventListenerMap getEventListenerMap()
961 {
962 return this.eventListenerMap;
963 }
964
965 @Override
966 public void evalChanged()
967 {
968 if (getNode().isActive())
969 {
970 this.id = getNode().getId() == null ? "" : getNode().getId();
971 this.nodeStart = replaceNode(this.nodeStart, getNode().getCoupledNodeAttribute("NodeStart").orElse(null));
972 this.nodeEnd = replaceNode(this.nodeEnd, getNode().getCoupledNodeAttribute("NodeEnd").orElse(null));
973 XsdTreeNode node = getNode();
974 if (node != null && node.getPathString().equals(XsdPaths.LINK))
975 {
976 setValue((v) -> this.offsetStart = v, Adapters.get(Length.class), node, "OffsetStart");
977 setValue((v) -> this.offsetEnd = v, Adapters.get(Length.class), node, "OffsetEnd");
978 }
979 this.shapeListener.updateShape();
980 setDirtyEndCheckValidity();
981 }
982 }
983
984
985
986
987 public void notifyNodeIdChanged()
988 {
989 this.nodeStart = replaceNode(this.nodeStart, getNode().getCoupledNodeAttribute("NodeStart").orElse(null));
990 this.nodeEnd = replaceNode(this.nodeEnd, getNode().getCoupledNodeAttribute("NodeEnd").orElse(null));
991 setDirtyEndCheckValidity();
992 }
993
994
995
996
997
998 public void notifyNodeRemoved(final XsdTreeNode node)
999 {
1000 if (node.equals(this.nodeStart))
1001 {
1002 this.nodeStart = null;
1003 this.directionStart = null;
1004 setDirtyEndCheckValidity();
1005 }
1006 else if (node.equals(this.nodeEnd))
1007 {
1008 this.nodeEnd = null;
1009 this.directionEnd = null;
1010 setDirtyEndCheckValidity();
1011 }
1012 }
1013
1014
1015
1016
1017
1018
1019
1020
1021 private <T> T orNull(final String value, final ExpressionAdapter<T, ?> adapter)
1022 {
1023 try
1024 {
1025 return value == null ? null : adapter.unmarshal(value).get(getEval());
1026 }
1027 catch (IllegalArgumentException ex)
1028 {
1029
1030 return null;
1031 }
1032 }
1033
1034
1035
1036
1037
1038
1039 public MapLaneData getLaneData(final String laneId)
1040 {
1041 return this.laneData.get(laneId);
1042 }
1043
1044
1045
1046
1047 public class MiddleOffset
1048 {
1049
1050 private double startOffsetMin = Double.POSITIVE_INFINITY;
1051
1052
1053 private double startOffsetMax = Double.NEGATIVE_INFINITY;
1054
1055
1056 private double endOffsetMin = Double.POSITIVE_INFINITY;
1057
1058
1059 private double endOffsetMax = Double.NEGATIVE_INFINITY;
1060
1061
1062
1063
1064 public MiddleOffset()
1065 {
1066
1067 }
1068
1069
1070
1071
1072
1073 public double getStartOffset()
1074 {
1075 return .5 * (this.startOffsetMin + this.startOffsetMax);
1076 }
1077
1078
1079
1080
1081
1082 public void addStartOffset(final double startOffset)
1083 {
1084 this.startOffsetMin = Math.min(this.startOffsetMin, startOffset);
1085 this.startOffsetMax = Math.max(this.startOffsetMax, startOffset);
1086 }
1087
1088
1089
1090
1091
1092 public double getEndOffset()
1093 {
1094 return .5 * (this.endOffsetMin + this.endOffsetMax);
1095 }
1096
1097
1098
1099
1100
1101 public void addEndOffset(final double endOffset)
1102 {
1103 this.endOffsetMin = Math.min(this.endOffsetMin, endOffset);
1104 this.endOffsetMax = Math.max(this.endOffsetMax, endOffset);
1105 }
1106 }
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117 private final class ShapeListener implements EventListener
1118 {
1119
1120 private XsdTreeNode shapeNode;
1121
1122
1123 private Double shape;
1124
1125
1126 private Boolean weighted;
1127
1128
1129 private LinearDensity startCurvature;
1130
1131
1132 private LinearDensity endCurvature;
1133
1134
1135 private Length length;
1136
1137
1138 private Length a;
1139
1140
1141 private Length radius;
1142
1143
1144 private ArcDirection direction;
1145
1146
1147 private NavigableMap<XsdTreeNode, Point2d> coordinates = new TreeMap<>(new Comparator<>()
1148 {
1149 @Override
1150 public int compare(final XsdTreeNode o1, final XsdTreeNode o2)
1151 {
1152 List<XsdTreeNode> list = ShapeListener.this.shapeNode.getChildren();
1153 return Integer.compare(list.indexOf(o1), list.indexOf(o2));
1154 }
1155 });
1156
1157 @Override
1158 public void notify(final Event event)
1159 {
1160 if (event.getType().equals(XsdTreeNode.OPTION_CHANGED))
1161 {
1162 XsdTreeNode node = (XsdTreeNode) ((Object[]) event.getContent())[1];
1163 if (node.getParent().equals(this.shapeNode))
1164 {
1165
1166 for (XsdTreeNode option : node.getChildren())
1167 {
1168 option.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
1169 }
1170
1171 SwingUtilities.invokeLater(() -> updateShape());
1172 }
1173 else
1174 {
1175
1176 if (this.shapeNode != null)
1177 {
1178 if (this.shapeNode.getChildCount() > 0)
1179 {
1180 this.shapeNode.getChild(0).removeListener(this, XsdTreeNode.OPTION_CHANGED);
1181 }
1182 this.shapeNode.removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
1183 }
1184 this.shapeNode = node;
1185 this.shapeNode.addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
1186 if (this.shapeNode.getNodeName().equals(Polyline.class.getSimpleName()))
1187 {
1188 for (XsdTreeNode option : this.shapeNode.getChildren())
1189 {
1190 option.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
1191 option.addListener(this, XsdTreeNode.MOVED, ReferenceType.WEAK);
1192 }
1193 }
1194 else if (this.shapeNode.getNodeName().equals(Clothoid.class.getSimpleName()))
1195 {
1196
1197 SwingUtilities.invokeLater(() ->
1198 {
1199 this.shapeNode.getChild(0).addListener(this, XsdTreeNode.OPTION_CHANGED, ReferenceType.WEAK);
1200 for (XsdTreeNode option : this.shapeNode.getChild(0).getChildren())
1201 {
1202 option.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
1203 }
1204 });
1205 }
1206 if (this.shapeNode.getNodeName().equals(Clothoid.class.getSimpleName())
1207 || this.shapeNode.getNodeName().equals(Arc.class.getSimpleName())
1208 || this.shapeNode.getNodeName().equals(Bezier.class.getSimpleName()))
1209 {
1210
1211 SwingUtilities.invokeLater(() -> setFlattenerListener());
1212 }
1213
1214 SwingUtilities.invokeLater(() -> updateShape());
1215 }
1216 setDirtyEndCheckValidity();
1217 }
1218 else if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED))
1219 {
1220 setAttribute((String) ((Object[]) event.getContent())[1]);
1221 setDirtyEndCheckValidity();
1222 }
1223 else if (event.getType().equals(XsdTreeNode.VALUE_CHANGED))
1224 {
1225
1226 Object[] content = (Object[]) event.getContent();
1227 XsdTreeNode node = (XsdTreeNode) content[0];
1228 try
1229 {
1230 switch (node.getNodeName())
1231 {
1232 case "Coordinate":
1233 this.coordinates.put(node, orNull(node.getValueOrDefault(), Adapters.get(Point2d.class)));
1234 break;
1235 case "StartCurvature":
1236 this.startCurvature = orNull(node.getValueOrDefault(), Adapters.get(LinearDensity.class));
1237 break;
1238 case "EndCurvature":
1239 this.endCurvature = orNull(node.getValueOrDefault(), Adapters.get(LinearDensity.class));
1240 break;
1241 case "Length":
1242 this.length = orNull(node.getValueOrDefault(), Adapters.get(Length.class));
1243 break;
1244 case "A":
1245 this.a = orNull(node.getValueOrDefault(), Adapters.get(Length.class));
1246 break;
1247 default:
1248 Logger.ots().warn(node.getNodeName()
1249 + " is unknown node name of which the value is assumed to affect link center line.");
1250 }
1251 }
1252 catch (Exception ex)
1253 {
1254
1255 return;
1256 }
1257 setDirtyEndCheckValidity();
1258 }
1259 else if (event.getType().equals(XsdTreeNode.MOVED))
1260 {
1261
1262 updateShape();
1263 setDirtyEndCheckValidity();
1264 }
1265 else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
1266 {
1267
1268 boolean activated = (boolean) ((Object[]) event.getContent())[1];
1269 if (activated)
1270 {
1271 setFlattenerListener();
1272 }
1273 else
1274 {
1275 if (MapLinkData.this.flattenerListener != null)
1276 {
1277 MapLinkData.this.flattenerListener.destroy();
1278 }
1279 MapLinkData.this.flattenerListener = null;
1280 }
1281 setDirtyEndCheckValidity();
1282 }
1283 }
1284
1285
1286
1287
1288 private void setFlattenerListener()
1289 {
1290 if (MapLinkData.this.flattenerListener != null)
1291 {
1292 MapLinkData.this.flattenerListener.destroy();
1293 }
1294 MapLinkData.this.flattenerListener = new FlattenerListener(this.shapeNode.getChild(0), () -> getEval());
1295 MapLinkData.this.flattenerListener.addListener(MapLinkData.this, ChangeListener.CHANGE_EVENT, ReferenceType.WEAK);
1296 this.shapeNode.getChild(0).addListener(this, XsdTreeNode.ACTIVATION_CHANGED);
1297 }
1298
1299
1300
1301
1302
1303 private void updateShape()
1304 {
1305 this.shape = null;
1306 this.weighted = null;
1307 this.startCurvature = null;
1308 this.endCurvature = null;
1309 this.length = null;
1310 this.a = null;
1311 this.radius = null;
1312 this.direction = null;
1313 this.coordinates.clear();
1314 if (this.shapeNode == null)
1315 {
1316 return;
1317 }
1318 switch (this.shapeNode.getNodeName())
1319 {
1320 case "Straight":
1321 setDirtyEndCheckValidity();
1322 break;
1323 case "Polyline":
1324 for (XsdTreeNode child : this.shapeNode.getChildren())
1325 {
1326 try
1327 {
1328 this.coordinates.put(child, orNull(child.getValueOrDefault(), Adapters.get(Point2d.class)));
1329 }
1330 catch (Exception ex)
1331 {
1332 orNull(child.getValueOrDefault(), Adapters.get(Point2d.class));
1333 throw new OtsRuntimeException(
1334 "Expression adapter could not unmarshal value for polyline coordinate.");
1335 }
1336 }
1337 setDirtyEndCheckValidity();
1338 break;
1339 case "Bezier":
1340 setAttribute("Shape");
1341 setAttribute("Weighted");
1342 setDirtyEndCheckValidity();
1343 setFlattenerListener();
1344 break;
1345 case "Clothoid":
1346 if (this.shapeNode.getChildCount() > 0 && this.shapeNode.getChild(0).getChildCount() > 0)
1347 {
1348
1349 for (int childIndex = 0; childIndex < this.shapeNode.getChild(0).getChildCount(); childIndex++)
1350 {
1351 XsdTreeNode child = this.shapeNode.getChild(0).getChild(childIndex);
1352 try
1353 {
1354 switch (child.getNodeName())
1355 {
1356 case "StartCurvature":
1357 this.startCurvature =
1358 orNull(child.getValueOrDefault(), Adapters.get(LinearDensity.class));
1359 break;
1360 case "EndCurvature":
1361 this.endCurvature =
1362 orNull(child.getValueOrDefault(), Adapters.get(LinearDensity.class));
1363 break;
1364 case "Length":
1365 this.length = orNull(child.getValueOrDefault(), Adapters.get(Length.class));
1366 break;
1367 case "A":
1368 this.a = orNull(child.getValueOrDefault(), Adapters.get(Length.class));
1369 break;
1370 default:
1371 throw new OtsRuntimeException(
1372 "Clothoid child " + child.getNodeName() + " not supported.");
1373 }
1374 }
1375 catch (Exception ex)
1376 {
1377 throw new OtsRuntimeException("Expression adapter could not unmarshal value for Clothoid child "
1378 + child.getNodeName());
1379 }
1380 }
1381 }
1382 setDirtyEndCheckValidity();
1383 setFlattenerListener();
1384 break;
1385 case "Arc":
1386 setAttribute("Radius");
1387 setAttribute("Direction");
1388 setDirtyEndCheckValidity();
1389 setFlattenerListener();
1390 break;
1391 case "xsd:choice":
1392
1393 setDirtyEndCheckValidity();
1394 break;
1395 default:
1396 throw new OtsRuntimeException(
1397 "Drawing of shape node " + this.shapeNode.getNodeName() + " is not supported.");
1398 }
1399 }
1400
1401
1402
1403
1404
1405 private void setAttribute(final String attribute)
1406 {
1407 if (!this.shapeNode.reportInvalidAttributeValue(this.shapeNode.getAttributeIndexByName(attribute)).isEmpty())
1408 {
1409
1410 return;
1411 }
1412 switch (attribute)
1413 {
1414 case "Shape":
1415 this.shape = getOrNull(attribute, Adapters.DOUBLE_POSITIVE_ADAPTER);
1416 break;
1417 case "Weighted":
1418 this.weighted = getOrNull(attribute, Adapters.get(Boolean.class));
1419 break;
1420 case "Length":
1421 this.length = getOrNull(attribute, Adapters.get(Length.class));
1422 break;
1423 case "Radius":
1424 this.radius = getOrNull(attribute, Adapters.get(Length.class));
1425 break;
1426 case "Direction":
1427 this.direction = getOrNull(attribute, Adapters.get(ArcDirection.class));
1428 break;
1429 default:
1430
1431 }
1432 }
1433
1434
1435
1436
1437
1438
1439
1440
1441 private <T> T getOrNull(final String attribute, final ExpressionAdapter<T, ?> adapter)
1442 {
1443 String value = this.shapeNode.getAttributeValueOrDefault(attribute);
1444 return orNull(value, adapter);
1445 }
1446
1447
1448
1449
1450
1451
1452
1453 public OffsetCurve2d getContiuousLine(final DirectedPoint2d fromPoint, final DirectedPoint2d toPoint)
1454 {
1455 switch (this.shapeNode.getNodeName())
1456 {
1457 case "Straight":
1458 return new Straight2d(fromPoint, fromPoint.distance(toPoint));
1459 case "Polyline":
1460 List<Point2d> list = new ArrayList<>();
1461 list.add(fromPoint);
1462 for (Entry<XsdTreeNode, Point2d> entry : this.coordinates.entrySet())
1463 {
1464 list.add(entry.getValue());
1465 }
1466 list.add(toPoint);
1467 if (list.contains(null))
1468 {
1469 if (list.size() == 3)
1470 {
1471
1472 list.remove(1);
1473 }
1474 else
1475 {
1476 return null;
1477 }
1478 }
1479 return new PolyLineCurve2d(new PolyLine2d(0.0, list), fromPoint.dirZ, toPoint.dirZ);
1480 case "Bezier":
1481 return new BezierCubic2d(new Ray2d(fromPoint), new Ray2d(toPoint), this.shape == null ? 1.0 : this.shape,
1482 this.weighted == null ? false : this.weighted);
1483 case "Clothoid":
1484 if (this.shapeNode.getChildCount() == 0 || this.shapeNode.getChild(0).getChildCount() == 0
1485 || this.shapeNode.getChild(0).getChild(0).getNodeName().equals("Interpolated"))
1486 {
1487 return new Clothoid2d(fromPoint, toPoint);
1488 }
1489 else if (this.shapeNode.getChild(0).getChild(0).getNodeName().equals("Length"))
1490 {
1491 if (this.length == null || this.startCurvature == null || this.endCurvature == null)
1492 {
1493 return null;
1494 }
1495 return Clothoid2d.withLength(fromPoint, this.length.si, this.startCurvature.si, this.endCurvature.si);
1496 }
1497 else
1498 {
1499 if (this.a == null || this.startCurvature == null || this.endCurvature == null)
1500 {
1501 return null;
1502 }
1503 return new Clothoid2d(fromPoint, this.a.si, this.startCurvature.si, this.endCurvature.si);
1504 }
1505 case "Arc":
1506 if (this.direction == null || this.radius == null)
1507 {
1508 return null;
1509 }
1510 boolean left = this.direction.equals(ArcDirection.LEFT);
1511 double endHeading = toPoint.dirZ;
1512 while (left && endHeading < fromPoint.dirZ)
1513 {
1514 endHeading += 2.0 * Math.PI;
1515 }
1516 while (!left && endHeading > fromPoint.dirZ)
1517 {
1518 endHeading -= 2.0 * Math.PI;
1519 }
1520 Angle angle = Angle.ofSI(
1521 AngleUtil.normalizeAroundPi(left ? endHeading - fromPoint.dirZ : fromPoint.dirZ - endHeading));
1522 return new Arc2d(fromPoint, this.radius.si, left, angle.si);
1523 default:
1524 throw new OtsRuntimeException(
1525 "Drawing of shape node " + this.shapeNode.getNodeName() + " is not supported.");
1526 }
1527 }
1528 }
1529
1530
1531
1532
1533
1534 public OffsetCurve2d getDesignLine()
1535 {
1536 checkDirtyGeometry();
1537 return this.designLine;
1538 }
1539
1540
1541
1542
1543
1544
1545
1546 public boolean isBezier()
1547 {
1548 return isShape(Bezier.class);
1549 }
1550
1551
1552
1553
1554
1555 public double getBezierShape()
1556 {
1557 return this.shapeListener.shape == null ? 1.0 : this.shapeListener.shape;
1558 }
1559
1560
1561
1562
1563
1564 public boolean isWeightedBezier()
1565 {
1566 return this.shapeListener.weighted == null ? false : this.shapeListener.weighted;
1567 }
1568
1569
1570
1571
1572
1573
1574
1575 public boolean isPolyline()
1576 {
1577 return isShape(Polyline.class);
1578 }
1579
1580
1581
1582
1583
1584 public NavigableMap<XsdTreeNode, Point2d> getPolylineCoordinates()
1585 {
1586 return new TreeMap<>(this.shapeListener.coordinates);
1587 }
1588
1589
1590
1591
1592
1593
1594
1595 public boolean isClothoid()
1596 {
1597 return isShape(Clothoid.class);
1598 }
1599
1600
1601
1602
1603
1604 public LinearDensity getClothoidStartCurvature()
1605 {
1606 if (this.designLine != null && this.designLine instanceof Clothoid2d)
1607 {
1608 return LinearDensity.ofSI(((Clothoid2d) this.designLine).getStartCurvature());
1609 }
1610 return null;
1611 }
1612
1613
1614
1615
1616
1617 public LinearDensity getClothoidEndCurvature()
1618 {
1619 if (this.designLine != null && this.designLine instanceof Clothoid2d)
1620 {
1621 return LinearDensity.ofSI(((Clothoid2d) this.designLine).getEndCurvature());
1622 }
1623 return null;
1624 }
1625
1626
1627
1628
1629
1630 public Length getClothoidLength()
1631 {
1632 if (this.designLine != null && this.designLine instanceof Clothoid2d)
1633 {
1634 return Length.ofSI(((Clothoid2d) this.designLine).getLength());
1635 }
1636 return null;
1637 }
1638
1639
1640
1641
1642
1643 public Length getClothoidA()
1644 {
1645 if (this.designLine != null && this.designLine instanceof Clothoid2d)
1646 {
1647 return Length.ofSI(((Clothoid2d) this.designLine).getA());
1648 }
1649 return null;
1650 }
1651
1652
1653
1654
1655
1656
1657 public String getClothoidAppliedShape()
1658 {
1659 if (this.designLine != null && this.designLine instanceof Clothoid2d)
1660 {
1661 return ((Clothoid2d) this.designLine).getAppliedShape();
1662 }
1663 return null;
1664 }
1665
1666
1667
1668
1669
1670
1671 private boolean isShape(final Class<?> clazz)
1672 {
1673 return this.shapeListener.shapeNode.getNodeName().equals(clazz.getSimpleName());
1674 }
1675
1676 @Override
1677 public String toString()
1678 {
1679 return (this.isConnector ? "Connector" : "Link ") + this.id;
1680 }
1681
1682 }