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