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