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
74
75
76
77
78
79
80
81 public class MapLinkData extends MapData implements LinkData, EventListener, EventProducer
82 {
83
84
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
89 private final EventListenerMap eventListenerMap = new EventListenerMap();
90
91
92 private final ShapeListener shapeListener = new ShapeListener();
93
94
95 private String id = "";
96
97
98 private XsdTreeNode nodeStart;
99
100
101 private XsdTreeNode nodeEnd;
102
103
104 private Direction directionStart = Direction.ZERO;
105
106
107 private Direction directionEnd = Direction.ZERO;
108
109
110 private Length offsetStart;
111
112
113 private Length offsetEnd;
114
115
116 private Point2d from;
117
118
119 private Point2d to;
120
121
122 private OffsetCurve2d designLine = null;
123
124
125 private PolyLine2d flattenedDesignLine = null;
126
127
128 private DirectedPoint2d location;
129
130
131 private Polygon2d absoluteContour;
132
133
134 private Polygon2d relativeContour;
135
136
137 private XsdTreeNode roadLayoutNode;
138
139
140 private XsdTreeNode definedRoadLayoutNode;
141
142
143 private RoadLayoutListener roadLayoutListener;
144
145
146 private FlattenerListener flattenerListener;
147
148
149 private Set<Renderable2d<?>> crossSectionElements = new LinkedHashSet<>();
150
151
152 private java.util.Map<String, MapLaneData> laneData = new LinkedHashMap<>();
153
154
155 private java.util.Map<String, MapStripeData> stripeData = new LinkedHashMap<>();
156
157
158 private PriorityAnimation priorityAnimation;
159
160
161
162
163
164
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
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
185 if (getNode().isActive())
186 {
187 SwingUtilities.invokeLater(() ->
188 {
189
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
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
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
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
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
366 buildDesignLine();
367 }
368 return;
369 }
370
371
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
401 }
402 else if ("Direction".equals(attribute))
403 {
404
405 }
406 else
407 {
408
409 return;
410 }
411 buildDesignLine();
412 }
413
414
415
416
417
418
419
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
453
454
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
479
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
494
495
496 public void removeCoordinate(final XsdTreeNode node)
497 {
498
499
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
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
569
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;
579 }
580 }
581 return getMap().getNetworkFlattener();
582 }
583
584
585
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
686
687
688
689
690
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
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
827
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
857
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
868
869
870
871
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
882 return null;
883 }
884 }
885
886
887
888
889
890
891 public MapLaneData getLaneData(final String laneId)
892 {
893 return this.laneData.get(laneId);
894 }
895
896
897
898
899 public class MiddleOffset
900 {
901
902 private double startOffsetMin = Double.POSITIVE_INFINITY;
903
904
905 private double startOffsetMax = Double.NEGATIVE_INFINITY;
906
907
908 private double endOffsetMin = Double.POSITIVE_INFINITY;
909
910
911 private double endOffsetMax = Double.NEGATIVE_INFINITY;
912
913
914
915
916 public MiddleOffset()
917 {
918
919 }
920
921
922
923
924
925 public double getStartOffset()
926 {
927 return .5 * (this.startOffsetMin + this.startOffsetMax);
928 }
929
930
931
932
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
942
943
944 public double getEndOffset()
945 {
946 return .5 * (this.endOffsetMin + this.endOffsetMax);
947 }
948
949
950
951
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
962
963
964
965
966
967
968
969 private final class ShapeListener implements EventListener
970 {
971
972 private XsdTreeNode shapeNode;
973
974
975 private Double shape;
976
977
978 private Boolean weighted;
979
980
981 private LinearDensity startCurvature;
982
983
984 private LinearDensity endCurvature;
985
986
987 private Length length;
988
989
990 private Length a;
991
992
993 private Length radius;
994
995
996 private ArcDirection direction;
997
998
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
1018 for (XsdTreeNode option : node.getChildren())
1019 {
1020 option.addListener(this, XsdTreeNode.VALUE_CHANGED, ReferenceType.WEAK);
1021 }
1022
1023 SwingUtilities.invokeLater(() -> update());
1024 }
1025 else
1026 {
1027
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
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
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
1098 return;
1099 }
1100 buildDesignLine();
1101 }
1102 else if (event.getType().equals(XsdTreeNode.MOVED))
1103 {
1104
1105 update();
1106 buildDesignLine();
1107 }
1108 else if (event.getType().equals(XsdTreeNode.ACTIVATION_CHANGED))
1109 {
1110
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
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
1144
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
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
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
1239
1240
1241 private void setAttribute(final String attribute)
1242 {
1243 if (this.shapeNode.reportInvalidAttributeValue(this.shapeNode.getAttributeIndexByName(attribute)).isEmpty())
1244 {
1245
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
1267 }
1268 }
1269
1270
1271
1272
1273
1274
1275
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
1285
1286
1287
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
1361
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
1374
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
1387
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
1400
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
1413
1414
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 }