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