View Javadoc
1   package org.opentrafficsim.editor.extensions.map;
2   
3   import java.util.Locale;
4   import java.util.Optional;
5   
6   import javax.swing.SwingUtilities;
7   
8   import org.djunits.value.vdouble.scalar.Length;
9   import org.djutils.draw.bounds.Bounds2d;
10  import org.djutils.draw.line.PolyLine2d;
11  import org.djutils.draw.line.Polygon2d;
12  import org.djutils.draw.point.DirectedPoint2d;
13  import org.djutils.event.Event;
14  import org.djutils.event.EventListener;
15  import org.djutils.event.reference.ReferenceType;
16  import org.opentrafficsim.animation.road.AbstractLineAnimation.LaneBasedObjectData;
17  import org.opentrafficsim.base.geometry.OtsShape;
18  import org.opentrafficsim.editor.OtsEditor;
19  import org.opentrafficsim.editor.XsdTreeNode;
20  import org.opentrafficsim.editor.extensions.Adapters;
21  import org.opentrafficsim.road.network.factory.xml.utils.ParseUtil;
22  import org.opentrafficsim.xml.bindings.types.LengthBeginEndType.LengthBeginEnd;
23  
24  /**
25   * Data class for objects that are drawn at a lane position. Implementations must call setLinkNode() in their constructor or by
26   * some other dynamic means, or the XSD node must have a Link attribute that points to the XSD node of a link by a keyref. This
27   * class will listen to attributes Id, Link, Lane and Position, and update visualization as needed. Attributes Id and Link are
28   * optional.
29   * <p>
30   * Copyright (c) 2023-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
31   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
32   * </p>
33   * @author Wouter Schakel
34   */
35  public abstract class MapLaneBasedObjectData extends MapData implements LaneBasedObjectData, EventListener
36  {
37  
38      /** Id. */
39      private String id = "";
40  
41      /** Lane. */
42      private String lane;
43  
44      /** Position as entered, e.g. END-20m. */
45      private LengthBeginEnd position;
46  
47      /** Position from start. */
48      private Length positionFromStart;
49  
50      /** Lane width. */
51      private Length laneWidth;
52  
53      /** Location. */
54      private DirectedPoint2d location;
55  
56      /** Bounds. */
57      private Bounds2d bounds;
58  
59      /** Absolute contour. */
60      private Polygon2d absoluteContour;
61  
62      /** Relative contour. */
63      private Polygon2d relativeContour;
64  
65      /** Line on lane. */
66      private PolyLine2d line;
67  
68      /** Node of link. */
69      private XsdTreeNode lastLinkNode;
70  
71      /**
72       * Constructor.
73       * @param map map.
74       * @param node node.
75       * @param editor editor.
76       */
77      public MapLaneBasedObjectData(final EditorMap map, final XsdTreeNode node, final OtsEditor editor)
78      {
79          super(map, node, editor);
80          getNode().addListener(this, XsdTreeNode.ATTRIBUTE_CHANGED, ReferenceType.WEAK);
81          if (getNode().isActive())
82          {
83              if (getNode().isIdentifiable())
84              {
85                  notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "Id", null}));
86              }
87              if (getNode().hasAttribute("Link"))
88              {
89                  notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "Link", null}));
90              }
91              notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "Lane", null}));
92              notify(new Event(XsdTreeNode.ATTRIBUTE_CHANGED, new Object[] {getNode(), "Position", null}));
93          }
94      }
95  
96      /**
97       * Sets a node as link. Sub-classes may call this in their constructor if it is a fixed node. This class will listen to
98       * changes in the Link attribute, and set a coupled node as link node if it exists.
99       * @param linkNode link node.
100      */
101     protected void setLinkNode(final XsdTreeNode linkNode)
102     {
103         if (this.lastLinkNode != null)
104         {
105             Optional<MapData> data = getMap().getData(linkNode);
106             if (data.isPresent())
107             {
108                 ((MapLinkData) data.get()).removeListener(this, MapLinkData.LAYOUT_REBUILT);
109             }
110         }
111         this.lastLinkNode = linkNode;
112         Optional<MapData> data = getMap().getData(linkNode);
113         if (data.isPresent())
114         {
115             ((MapLinkData) data.get()).addListener(this, MapLinkData.LAYOUT_REBUILT, ReferenceType.WEAK);
116         }
117     }
118 
119     @Override
120     public void destroy()
121     {
122         super.destroy();
123         getNode().removeListener(this, XsdTreeNode.ATTRIBUTE_CHANGED);
124         if (this.lastLinkNode != null)
125         {
126             this.lastLinkNode.removeListener(this, MapLinkData.LAYOUT_REBUILT);
127         }
128     }
129 
130     @Override
131     public Length getLaneWidth()
132     {
133         return this.laneWidth;
134     }
135 
136     @Override
137     public DirectedPoint2d getLocation()
138     {
139         return this.location;
140     }
141 
142     @Override
143     public Bounds2d getRelativeBounds()
144     {
145         return this.bounds;
146     }
147 
148     @Override
149     public Polygon2d getAbsoluteContour()
150     {
151         return this.absoluteContour;
152     }
153 
154     @Override
155     public Polygon2d getRelativeContour()
156     {
157         return this.relativeContour;
158     }
159 
160     @Override
161     public PolyLine2d getLine()
162     {
163         return this.line;
164     }
165 
166     @Override
167     public String getId()
168     {
169         return this.id;
170     }
171 
172     /**
173      * Returns an id in the form {linkId}.{laneId}.{id} or {linkId}.{laneId}@{position} if the id is empty.
174      * @return id in link/lane/position form.
175      */
176     protected String getLinkLanePositionId()
177     {
178         StringBuilder str = new StringBuilder();
179         String sep = "";
180         if (this.lastLinkNode != null)
181         {
182             str.append(this.lastLinkNode.getId());
183             sep = ".";
184         }
185         if (this.lane != null)
186         {
187             str.append(sep).append(this.lane);
188         }
189         if (this.positionFromStart != null)
190         {
191             str.append(String.format(Locale.US, "@%.3fm", this.positionFromStart.si));
192         }
193         return str.toString();
194     }
195 
196     @Override
197     public void evalChanged()
198     {
199         if (getNode().isIdentifiable())
200         {
201             this.id = getNode().getId() == null ? "" : getNode().getId();
202         }
203         if (getNode().hasAttribute("Link"))
204         {
205             XsdTreeNode linkNode = getNode().getCoupledNodeAttribute("Link").orElse(null);
206             setLinkNode(linkNode);
207         }
208         setValue((v) -> this.lane = v, Adapters.get(String.class), getNode(), "Lane");
209         setValue((v) -> this.position = v, Adapters.get(LengthBeginEnd.class), getNode(), "Position");
210         setLocation();
211     }
212 
213     @Override
214     public void notify(final Event event)
215     {
216         if (event.getType().equals(XsdTreeNode.ATTRIBUTE_CHANGED))
217         {
218             String attribute = (String) ((Object[]) event.getContent())[1];
219             String value = getNode().getAttributeValueOrDefault(attribute);
220             if ("Id".equals(attribute))
221             {
222                 this.id = value == null ? "" : value;
223             }
224             else if ("Link".equals(attribute))
225             {
226                 XsdTreeNode linkNode = getNode().getCoupledNodeAttribute("Link").orElse(null);
227                 setLinkNode(linkNode);
228                 setLocation();
229             }
230             else if ("Lane".equals(attribute))
231             {
232                 setValue((v) -> this.lane = v, Adapters.get(String.class), getNode(), "Lane");
233                 setLocation();
234             }
235             else if ("Position".equals(attribute))
236             {
237                 setValue((v) -> this.position = v, Adapters.get(LengthBeginEnd.class), getNode(), "Position");
238                 setLocation();
239             }
240         }
241         else
242         {
243             // MapLinkData.LAYOUT_REBUILT
244             setLocation();
245         }
246         SwingUtilities.invokeLater(() -> getMap().update());
247     }
248 
249     /**
250      * Return center line of the coupled lane.
251      * @return center line of the coupled lane
252      */
253     public Optional<PolyLine2d> getLaneCenterLine()
254     {
255         if (this.lane == null || this.lastLinkNode == null)
256         {
257             return Optional.empty();
258         }
259         Optional<MapData> linkDataOptional = getMap().getData(this.lastLinkNode);
260         if (linkDataOptional.isEmpty())
261         {
262             return Optional.empty();
263         }
264         MapLinkData linkData = (MapLinkData) linkDataOptional.get();
265         MapLaneData laneData = linkData.getLaneData(this.lane);
266         if (laneData == null)
267         {
268             return Optional.empty();
269         }
270         return Optional.of(laneData.getCenterLine());
271     }
272 
273     /**
274      * Set the location from the coordinate and direction. Notify when invalid or valid.
275      */
276     private void setLocation()
277     {
278         if (this.lane == null || this.position == null || this.lastLinkNode == null)
279         {
280             setInvalid();
281             return;
282         }
283         Optional<MapData> linkDataOptional = getMap().getData(this.lastLinkNode);
284         if (linkDataOptional.isEmpty())
285         {
286             setInvalid();
287             return;
288         }
289         MapLinkData linkData = (MapLinkData) linkDataOptional.get();
290         MapLaneData laneData = linkData.getLaneData(this.lane);
291         if (laneData == null)
292         {
293             setInvalid();
294             return;
295         }
296         this.positionFromStart =
297                 ParseUtil.parseLengthBeginEnd(this.position, Length.ofSI(laneData.getCenterLine().getLength()));
298 
299         this.laneWidth = laneData.getWidth(this.positionFromStart);
300         double w45 = 0.45 * getLaneWidth().si;
301         DirectedPoint2d point = laneData.getCenterLine().getLocationExtended(this.positionFromStart.si);
302         this.location = new DirectedPoint2d(point.x, point.y, point.dirZ);
303         this.line = new PolyLine2d(new double[] {0.0, 0.0}, new double[] {-w45, w45});
304         this.relativeContour = new Polygon2d(PolyLine2d.concatenate(this.line, this.line.reverse()).getPointList());
305         this.bounds = LaneBasedObjectData.super.getRelativeBounds();
306         this.absoluteContour =
307                 new Polygon2d(0.0, OtsShape.toAbsoluteTransform(this.location).transform(this.relativeContour.iterator()));
308         setValid();
309     }
310 
311 }