View Javadoc
1   package org.opentrafficsim.core.network.lane;
2   
3   import java.rmi.RemoteException;
4   import java.util.ArrayList;
5   import java.util.HashSet;
6   import java.util.List;
7   import java.util.Locale;
8   import java.util.Set;
9   
10  import javax.media.j3d.Bounds;
11  import javax.vecmath.Point3d;
12  
13  import nl.tudelft.simulation.dsol.animation.LocatableInterface;
14  import nl.tudelft.simulation.language.d3.BoundingBox;
15  import nl.tudelft.simulation.language.d3.DirectedPoint;
16  
17  import org.opentrafficsim.core.network.LateralDirectionality;
18  import org.opentrafficsim.core.network.NetworkException;
19  import org.opentrafficsim.core.network.geotools.LinearGeometry;
20  import org.opentrafficsim.core.unit.LengthUnit;
21  import org.opentrafficsim.core.value.vdouble.scalar.DoubleScalar;
22  
23  import com.vividsolutions.jts.geom.Coordinate;
24  import com.vividsolutions.jts.geom.Envelope;
25  import com.vividsolutions.jts.geom.Geometry;
26  import com.vividsolutions.jts.geom.GeometryFactory;
27  import com.vividsolutions.jts.geom.LineString;
28  import com.vividsolutions.jts.linearref.LengthIndexedLine;
29  import com.vividsolutions.jts.operation.buffer.BufferParameters;
30  
31  /**
32   * <p>
33   * Copyright (c) 2013-2014 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights
34   * reserved. <br>
35   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
36   * <p>
37   * @version Aug 19, 2014 <br>
38   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
39   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
40   * @author <a href="http://www.citg.tudelft.nl">Guus Tamminga</a>
41   */
42  public abstract class CrossSectionElement implements LocatableInterface
43  {
44      /** Cross Section Link to which the element belongs. */
45      private final CrossSectionLink<?, ?> parentLink;
46  
47      /** The lateral offset from the design line of the parentLink at the start of the parentLink. */
48      private final DoubleScalar.Rel<LengthUnit> designLineOffsetAtBegin;
49  
50      /** The lateral offset from the design line of the parentLink at the end of the parentLink. */
51      private final DoubleScalar.Rel<LengthUnit> designLineOffsetAtEnd;
52  
53      /** Start width, positioned <i>symmetrically around</i> the lateral start position. */
54      private final DoubleScalar.Rel<LengthUnit> beginWidth;
55  
56      /** End width, positioned <i>symmetrically around</i> the lateral end position. */
57      private final DoubleScalar.Rel<LengthUnit> endWidth;
58  
59      /** geometry matching the contours of the cross section element. */
60      private final Geometry contour;
61  
62      /** The offset line as calculated. */
63      private LineString crossSectionDesignLine;
64  
65      /** The length of the line. Calculated once at the creation. */
66      private final DoubleScalar.Rel<LengthUnit> length;
67  
68      /**
69       * <b>Note:</b> LEFT is seen as a positive lateral direction, RIGHT as a negative lateral direction, with the
70       * direction from the StartNode towards the EndNode as the longitudinal direction.
71       * @param parentLink CrossSectionLink; Link to which the element belongs.
72       * @param lateralOffsetAtBegin DoubleScalar.Rel&lt;LengthUnit&gt;; the lateral offset of the design line of the new
73       *            CrossSectionLink with respect to the design line of the parent Link at the start of the parent Link
74       * @param lateralOffsetAtEnd DoubleScalar.Rel&lt;LengthUnit&gt;; the lateral offset of the design line of the new
75       *            CrossSectionLink with respect to the design line of the parent Link at the end of the parent Link
76       * @param beginWidth DoubleScalar.Rel&lt;LengthUnit&gt;; width at start, positioned <i>symmetrically around</i> the
77       *            design line
78       * @param endWidth DoubleScalar.Rel&lt;LengthUnit&gt;; width at end, positioned <i>symmetrically around</i> the
79       *            design line
80       * @throws NetworkException when creation of the geometry fails
81       */
82      public CrossSectionElement(final CrossSectionLink<?, ?> parentLink,
83              final DoubleScalar.Rel<LengthUnit> lateralOffsetAtBegin,
84              final DoubleScalar.Rel<LengthUnit> lateralOffsetAtEnd, final DoubleScalar.Rel<LengthUnit> beginWidth,
85              final DoubleScalar.Rel<LengthUnit> endWidth) throws NetworkException
86      {
87          super();
88          this.parentLink = parentLink;
89          this.designLineOffsetAtBegin = lateralOffsetAtBegin;
90          this.designLineOffsetAtEnd = lateralOffsetAtEnd;
91          this.beginWidth = beginWidth;
92          this.endWidth = endWidth;
93          this.contour = constructGeometry();
94          // TODO LengthUnit and width might depend on CRS
95          this.length = new DoubleScalar.Rel<LengthUnit>(this.crossSectionDesignLine.getLength(), LengthUnit.METER);
96          this.parentLink.addCrossSectionElement(this);
97      }
98  
99      /** Precision of buffer operations. */
100     private final static int QUADRANTSEGMENTS = 8;
101 
102     // FIXME put in utility class. Also exists in XmlNetworkLaneParser.
103     /**
104      * normalize an angle between 0 and 2 * PI.
105      * @param angle original angle.
106      * @return angle between 0 and 2 * PI.
107      */
108     private double norm(final double angle)
109     {
110         double normalized = angle % (2 * Math.PI);
111         if (normalized < 0.0)
112         {
113             normalized += 2 * Math.PI;
114         }
115         return normalized;
116     }
117 
118     /**
119      * Generate a Geometry that has a fixed offset from a reference Geometry.
120      * @param referenceLine Geometry; the reference line
121      * @param offset double; offset distance from the reference line; positive is Left, negative is Right
122      * @return Geometry; the Geometry of a line that has the specified offset from the reference line
123      * @throws NetworkException on failure
124      */
125     @SuppressWarnings("checkstyle:methodlength")
126     private Geometry offsetGeometry(final Geometry referenceLine, final double offset) throws NetworkException
127     {
128         Coordinate[] referenceCoordinates = referenceLine.getCoordinates();
129         // printCoordinates("reference", referenceCoordinates);
130         double bufferOffset = Math.abs(offset);
131         if (bufferOffset == 0)
132         {
133             // return a copy of the reference line
134             GeometryFactory factory = new GeometryFactory();
135             Geometry result = factory.createLineString(referenceCoordinates);
136             return result;
137         }
138         Coordinate[] bufferCoordinates =
139                 referenceLine.buffer(bufferOffset, CrossSectionElement.QUADRANTSEGMENTS, BufferParameters.CAP_FLAT)
140                         .getCoordinates();
141         // find the coordinate indices closest to the start point and end point, at a distance of approximately the
142         // offset
143         Coordinate sC = referenceCoordinates[0];
144         Coordinate sC1 = referenceCoordinates[1];
145         Coordinate eC = referenceCoordinates[referenceCoordinates.length - 1];
146         Coordinate eC1 = referenceCoordinates[referenceCoordinates.length - 2];
147         Set<Integer> startIndexSet = new HashSet<>();
148         Set<Coordinate> startSet = new HashSet<Coordinate>();
149         Set<Integer> endIndexSet = new HashSet<>();
150         Set<Coordinate> endSet = new HashSet<Coordinate>();
151         final double precision = 0.000001; //
152         for (int i = 0; i < bufferCoordinates.length; i++) // Note: the last coordinate = the first coordinate
153         {
154             Coordinate c = bufferCoordinates[i];
155             if (Math.abs(c.distance(sC) - bufferOffset) < bufferOffset * precision && !startSet.contains(c))
156             {
157                 startIndexSet.add(i);
158                 startSet.add(c);
159             }
160             if (Math.abs(c.distance(eC) - bufferOffset) < bufferOffset * precision && !endSet.contains(c))
161             {
162                 endIndexSet.add(i);
163                 endSet.add(c);
164             }
165         }
166         if (startIndexSet.size() != 2)
167         {
168             throw new NetworkException("offsetGeometry: startIndexSet.size() = " + startIndexSet.size());
169         }
170         if (endIndexSet.size() != 2)
171         {
172             throw new NetworkException("offsetGeometry: endIndexSet.size() = " + endIndexSet.size());
173         }
174 
175         // which point(s) are in the right direction of the start / end?
176         int startIndex = -1;
177         int endIndex = -1;
178         double expectedStartAngle = norm(Math.atan2(sC1.y - sC.y, sC1.x - sC.x) + Math.signum(offset) * Math.PI / 2.0);
179         double expectedEndAngle = norm(Math.atan2(eC.y - eC1.y, eC.x - eC1.x) + Math.signum(offset) * Math.PI / 2.0);
180         for (int ic : startIndexSet)
181         {
182             if (Math.abs(norm(Math.atan2(bufferCoordinates[ic].y - sC.y, bufferCoordinates[ic].x - sC.x)
183                     - expectedStartAngle)) < Math.PI / 4.0
184                     || Math.abs(norm(Math.atan2(bufferCoordinates[ic].y - sC.y, bufferCoordinates[ic].x - sC.x)
185                             - expectedStartAngle)
186                             - 2.0 * Math.PI) < Math.PI / 4.0)
187             {
188                 startIndex = ic;
189             }
190         }
191         for (int ic : endIndexSet)
192         {
193             if (Math.abs(norm(Math.atan2(bufferCoordinates[ic].y - eC.y, bufferCoordinates[ic].x - eC.x)
194                     - expectedEndAngle)) < Math.PI / 4.0
195                     || Math.abs(norm(Math.atan2(bufferCoordinates[ic].y - eC.y, bufferCoordinates[ic].x - eC.x)
196                             - expectedEndAngle)
197                             - 2.0 * Math.PI) < Math.PI / 4.0)
198             {
199                 endIndex = ic;
200             }
201         }
202         if (startIndex == -1 || endIndex == -1)
203         {
204             throw new NetworkException("offsetGeometry: could not find startIndex or endIndex");
205         }
206         startIndexSet.remove(startIndex);
207         endIndexSet.remove(endIndex);
208 
209         // Make two lists, one in each direction; start at "start" and end at "end".
210         List<Coordinate> coordinateList1 = new ArrayList<>();
211         List<Coordinate> coordinateList2 = new ArrayList<>();
212         boolean use1 = true;
213         boolean use2 = true;
214 
215         int i = startIndex;
216         while (i != endIndex)
217         {
218             if (!coordinateList1.contains(bufferCoordinates[i]))
219             {
220                 coordinateList1.add(bufferCoordinates[i]);
221             }
222             i = (i + 1) % bufferCoordinates.length;
223             if (startIndexSet.contains(i) || endIndexSet.contains(i))
224             {
225                 use1 = false;
226             }
227         }
228         if (!coordinateList1.contains(bufferCoordinates[endIndex]))
229         {
230             coordinateList1.add(bufferCoordinates[endIndex]);
231         }
232 
233         i = startIndex;
234         while (i != endIndex)
235         {
236             if (!coordinateList2.contains(bufferCoordinates[i]))
237             {
238                 coordinateList2.add(bufferCoordinates[i]);
239             }
240             i = (i == 0) ? bufferCoordinates.length - 1 : i - 1;
241             if (startIndexSet.contains(i) || endIndexSet.contains(i))
242             {
243                 use2 = false;
244             }
245         }
246         if (!coordinateList2.contains(bufferCoordinates[endIndex]))
247         {
248             coordinateList2.add(bufferCoordinates[endIndex]);
249         }
250 
251         if (!use1 && !use2)
252         {
253             throw new NetworkException("offsetGeometry: could not find path from start to end for offset");
254         }
255         if (use1 && use2)
256         {
257             throw new NetworkException("offsetGeometry: Both paths from start to end for offset were found to be ok");
258         }
259         Coordinate[] coordinates;
260         if (use1)
261         {
262             coordinates = new Coordinate[coordinateList1.size()];
263             coordinateList1.toArray(coordinates);
264         }
265         else
266         {
267             coordinates = new Coordinate[coordinateList2.size()];
268             coordinateList2.toArray(coordinates);
269         }
270         GeometryFactory factory = new GeometryFactory();
271         Geometry result = factory.createLineString(coordinates);
272         return result;
273     }
274 
275     /**
276      * Create the Geometry of a line at offset from a reference line. The offset may change linearly from its initial
277      * value at the start of the reference line to its final offset value at the end of the reference line.
278      * @param referenceLine Geometry; the Geometry of the reference line
279      * @param offsetAtStart double; offset at the start of the reference line (positive value is Left, negative value is
280      *            Right)
281      * @param offsetAtEnd double; offset at the end of the reference line (positive value is Left, negative value is
282      *            Right)
283      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
284      * @throws NetworkException when this method fails to create the offset line
285      */
286     private Geometry offsetLine(final Geometry referenceLine, final double offsetAtStart, final double offsetAtEnd)
287             throws NetworkException
288     {
289         // printCoordinates("referenceLine    ", referenceLine);
290         Geometry offsetLineAtStart = offsetGeometry(referenceLine, offsetAtStart);
291         // System.out.println("offsetAtStart  " + offsetAtStart);
292         // printCoordinates("offsetLineAtStart", offsetLineAtStart);
293         if (offsetAtStart == offsetAtEnd)
294         {
295             return offsetLineAtStart; // offset does not change
296         }
297         Geometry offsetLineAtEnd = offsetGeometry(referenceLine, offsetAtEnd);
298         // System.out.println("offsetAtEnd    " + offsetAtEnd);
299         // printCoordinates("offsetLineAtEnd  ", offsetLineAtEnd);
300         LengthIndexedLine first = new LengthIndexedLine(offsetLineAtStart);
301         double firstLength = offsetLineAtStart.getLength();
302         LengthIndexedLine second = new LengthIndexedLine(offsetLineAtEnd);
303         double secondLength = offsetLineAtEnd.getLength();
304         ArrayList<Coordinate> out = new ArrayList<Coordinate>();
305         Coordinate[] firstCoordinates = offsetLineAtStart.getCoordinates();
306         Coordinate[] secondCoordinates = offsetLineAtEnd.getCoordinates();
307         int firstIndex = 0;
308         int secondIndex = 0;
309         Coordinate prevCoordinate = null;
310         final double tooClose = 0.05; // 5 cm
311         while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
312         {
313             double firstRatio =
314                     firstIndex < firstCoordinates.length ? first.indexOf(firstCoordinates[firstIndex]) / firstLength
315                             : Double.MAX_VALUE;
316             double secondRatio =
317                     secondIndex < secondCoordinates.length ? second.indexOf(secondCoordinates[secondIndex])
318                             / secondLength : Double.MAX_VALUE;
319             double ratio;
320             if (firstRatio < secondRatio)
321             {
322                 ratio = firstRatio;
323                 firstIndex++;
324             }
325             else
326             {
327                 ratio = secondRatio;
328                 secondIndex++;
329             }
330             Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
331             Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
332             Coordinate resultCoordinate =
333                     new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x, (1 - ratio)
334                             * firstCoordinate.y + ratio * secondCoordinate.y);
335             // System.out.println(String.format(Locale.US,
336             // "ratio: %7.5f, first  %8.3f,%8.3f, second: %8.3f,%8.3f -> %8.3f,%8.3f", ratio, firstCoordinate.x,
337             // firstCoordinate.y, secondCoordinate.x, secondCoordinate.y, resultCoordinate.x, resultCoordinate.y));
338             if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
339             {
340                 out.add(resultCoordinate);
341                 prevCoordinate = resultCoordinate;
342             }
343         }
344         Coordinate[] resultCoordinates = new Coordinate[out.size()];
345         for (int index = 0; index < out.size(); index++)
346         {
347             resultCoordinates[index] = out.get(index);
348         }
349         // printCoordinates("resultCoordinates", resultCoordinates);
350         GeometryFactory factory = new GeometryFactory();
351         return factory.createLineString(resultCoordinates);
352     }
353 
354     /**
355      * Construct a buffer geometry by offsetting the linear geometry line with a distance and constructing a so-called
356      * "buffer" around it.
357      * @return the geometry belonging to this CrossSectionElement.
358      * @throws NetworkException when construction of the geometry fails (which should never happen)
359      */
360     private Geometry constructGeometry() throws NetworkException
361     {
362         GeometryFactory factory = new GeometryFactory();
363         LinearGeometry parentGeometry = this.parentLink.getGeometry();
364         if (null == parentGeometry)
365         {
366             return null; // If the Link does not have a Geometry; this CrossSectionElement can't have one either
367         }
368         Coordinate[] referenceCoordinates = parentGeometry.getLineString().getCoordinates();
369         if (referenceCoordinates.length < 2)
370         {
371             throw new NetworkException("Parent Link has bad Geometry");
372         }
373         // printCoordinates("Link design line:", referenceCoordinates);
374         Geometry referenceGeometry = factory.createLineString(referenceCoordinates);
375         Geometry resultLine =
376                 offsetLine(referenceGeometry, this.designLineOffsetAtBegin.getSI(), this.designLineOffsetAtEnd.getSI());
377         // printCoordinates("Lane design line:", resultLine);
378         this.crossSectionDesignLine = factory.createLineString(resultLine.getCoordinates());
379         Coordinate[] rightBoundary =
380                 offsetLine(this.crossSectionDesignLine, -this.beginWidth.getSI() / 2, -this.endWidth.getSI() / 2)
381                         .getCoordinates();
382         // printCoordinates("Right boundary:  ", rightBoundary);
383         Coordinate[] leftBoundary =
384                 offsetLine(this.crossSectionDesignLine, this.beginWidth.getSI() / 2, this.endWidth.getSI() / 2)
385                         .getCoordinates();
386         // printCoordinates("Left boundary:   ", leftBoundary);
387         Coordinate[] result = new Coordinate[rightBoundary.length + leftBoundary.length + 1];
388         int resultIndex = 0;
389         for (int index = 0; index < rightBoundary.length; index++)
390         {
391             result[resultIndex++] = rightBoundary[index];
392         }
393         for (int index = leftBoundary.length; --index >= 0;)
394         {
395             result[resultIndex++] = leftBoundary[index];
396         }
397         result[resultIndex] = rightBoundary[0]; // close the contour
398         // printCoordinates("Lane contour:    ", result);
399         return factory.createLineString(result);
400     }
401 
402     /**
403      * @return parentLink.
404      */
405     public final CrossSectionLink<?, ?> getParentLink()
406     {
407         return this.parentLink;
408     }
409 
410     /**
411      * Retrieve the lateral offset from the Link design line at the specified longitudinal position.
412      * @param fractionalPosition double; fractional longitudinal position on this Lane
413      * @return DoubleScalar.Rel&lt;LengthUnit&gt; the lateralCenterPosition at the specified longitudinal position
414      */
415     public final DoubleScalar.Rel<LengthUnit> getLateralCenterPosition(final double fractionalPosition)
416     {
417         return DoubleScalar.interpolate(this.designLineOffsetAtBegin, this.designLineOffsetAtEnd, fractionalPosition)
418                 .immutable();
419     }
420 
421     /**
422      * Retrieve the lateral offset from the Link design line at the specified longitudinal position.
423      * @param longitudinalPosition DoubleScalar.Rel&lt;LengthUnit&gt;; the longitudinal position on this Lane
424      * @return DoubleScalar.Rel&lt;LengthUnit&gt; the lateralCenterPosition at the specified longitudinal position
425      */
426     public final DoubleScalar<LengthUnit> getLateralCenterPosition(
427             final DoubleScalar.Rel<LengthUnit> longitudinalPosition)
428     {
429         return getLateralCenterPosition(longitudinalPosition.getSI() / getLength().getSI());
430     }
431 
432     /**
433      * Return the width of this CrossSectionElement at a specified longitudinal position.
434      * @param longitudinalPosition DoubleScalar&lt;LengthUnit&gt;; the longitudinal position
435      * @return DoubleScalar.Rel&lt;LengthUnit&gt;; the width of this CrossSectionElement at the specified longitudinal
436      *         position.
437      */
438     public final DoubleScalar.Rel<LengthUnit> getWidth(final DoubleScalar.Rel<LengthUnit> longitudinalPosition)
439     {
440         return getWidth(longitudinalPosition.getSI() / getLength().getSI());
441     }
442 
443     /**
444      * Return the width of this CrossSectionElement at a specified fractional longitudinal position.
445      * @param fractionalPosition double; the fractional longitudinal position
446      * @return DoubleScalar.Rel&lt;LengthUnit&gt;; the width of this CrossSectionElement at the specified fractional
447      *         longitudinal position.
448      */
449     public final DoubleScalar.Rel<LengthUnit> getWidth(final double fractionalPosition)
450     {
451         return DoubleScalar.interpolate(this.beginWidth, this.endWidth, fractionalPosition).immutable();
452     }
453 
454     /** @return the z-value to determine "stacking" for animation. */
455     protected abstract double getZ();
456 
457     /** {@inheritDoc} */
458     @Override
459     public final DirectedPoint getLocation() throws RemoteException
460     {
461         Envelope e = this.contour.getEnvelopeInternal(); // cached, so not expensive
462         return new DirectedPoint(0.5 * (e.getMaxX() - e.getMinX()), 0.5 * (e.getMaxY() - e.getMinY()), getZ());
463     }
464 
465     /** {@inheritDoc} */
466     @Override
467     public final Bounds getBounds() throws RemoteException
468     {
469         Envelope e = this.contour.getEnvelopeInternal(); // cached, so not expensive
470         double dx = 0.5 * (e.getMaxX() - e.getMinX());
471         double dy = 0.5 * (e.getMaxY() - e.getMinY());
472         return new BoundingBox(new Point3d(e.getMinX() - dx, e.getMinY() - dy, 0.0), new Point3d(e.getMinX() + dx,
473                 e.getMinY() + dy, getZ()));
474     }
475 
476     /**
477      * @return the contour of this CrossSectionElement. <br>
478      *         <b>Do not modify the returned object or chaos will ensue.</b>
479      */
480     public final Geometry getContour()
481     {
482         return this.contour;
483     }
484 
485     /**
486      * Retrieve the center line or design line of this CrossSectionElement. <br>
487      * <b>Do not modify the returned object or chaos will ensue.</b>
488      * @return LineString; the design line of this CrossSectionElement (which equals the center line of this
489      *         CrossSectionElement)
490      */
491     public final LineString getCenterLine()
492     {
493         return this.crossSectionDesignLine;
494     }
495 
496     /**
497      * Print one Coordinate on the console.
498      * @param prefix String; text to put before the output
499      * @param coordinate Coordinate; the coordinate to print
500      */
501     public static void printCoordinate(final String prefix, final Coordinate coordinate)
502     {
503         System.out.print(String.format(Locale.US, "%s %8.3f,%8.3f   ", prefix, coordinate.x, coordinate.y));
504     }
505 
506     /**
507      * Print coordinates of a Geometry on the console.
508      * @param prefix String; text to put before the output
509      * @param geometry Geometry; the coordinates to print
510      * @param fromIndex int; index of the first coordinate to print
511      * @param toIndex int; one higher than the index of the last coordinate to print
512      */
513     public static void printCoordinates(final String prefix, final Geometry geometry, final int fromIndex,
514             final int toIndex)
515     {
516         printCoordinates(prefix, geometry.getCoordinates(), fromIndex, toIndex);
517     }
518 
519     /**
520      * Print coordinates of a Geometry on the console.
521      * @param prefix String; text to put before the output
522      * @param geometry Geometry; the coordinates to print
523      */
524     public static void printCoordinates(final String prefix, final Geometry geometry)
525     {
526         printCoordinates(prefix, geometry.getCoordinates());
527     }
528 
529     /**
530      * Print an array of coordinates on the console.
531      * @param prefix String; text to put before the coordinates
532      * @param coordinates Coordinate[]; the coordinates to print
533      */
534     public static void printCoordinates(final String prefix, final Coordinate[] coordinates)
535     {
536         printCoordinates(prefix + "(" + coordinates.length + " pts)", coordinates, 0, coordinates.length);
537     }
538 
539     /**
540      * Print part of an array of coordinates on the console.
541      * @param prefix String; text to put before the output
542      * @param coordinates Coordinate[]; the coordinates to print
543      * @param fromIndex int; index of the first coordinate to print
544      * @param toIndex int; one higher than the index of the last coordinate to print
545      */
546     public static void printCoordinates(final String prefix, final Coordinate[] coordinates, final int fromIndex,
547             final int toIndex)
548     {
549         System.out.print(prefix);
550         String operator = "M"; // Move absolute
551         for (int i = fromIndex; i < toIndex; i++)
552         {
553             printCoordinate(operator, coordinates[i]);
554             operator = "L"; // LineTo Absolute
555         }
556         System.out.println("");
557     }
558 
559     /**
560      * Return the length of this CrossSectionElement as measured along the design line (which equals the center line).
561      * @return DoubleScalar.Rel&lt;LengthUnit&gt;; the length of this CrossSectionElement
562      */
563     public final DoubleScalar.Rel<LengthUnit> getLength()
564     {
565         return this.length;
566     }
567 
568     /** {@inheritDoc} */
569     @Override
570     @SuppressWarnings("checkstyle:designforextension")
571     public String toString()
572     {
573         return String.format("offset %.2fm", this.designLineOffsetAtBegin.getSI());
574     }
575 
576     /**
577      * Return the lateral offset from the design line of the parent Link of the Left or Right boundary of this
578      * CrossSectionElement at the specified fractional longitudinal position.
579      * @param lateralDirection LateralDirectionality; LEFT, or RIGHT
580      * @param fractionalLongitudinalPosition double; ranges from 0.0 (begin of parentLink) to 1.0 (end of parentLink)
581      * @return DoubleScalar.Rel&lt;LengthUnit&gt;
582      */
583     public final DoubleScalar.Rel<LengthUnit> getLateralBoundaryPosition(final LateralDirectionality lateralDirection,
584             final double fractionalLongitudinalPosition)
585     {
586         DoubleScalar.Rel<LengthUnit> designLineOffset =
587                 DoubleScalar.interpolate(this.designLineOffsetAtBegin, this.designLineOffsetAtEnd,
588                         fractionalLongitudinalPosition).immutable();
589         DoubleScalar.Rel<LengthUnit> halfWidth =
590                 (DoubleScalar.Rel<LengthUnit>) DoubleScalar
591                         .interpolate(this.beginWidth, this.endWidth, fractionalLongitudinalPosition).multiply(0.5)
592                         .immutable();
593         switch (lateralDirection)
594         {
595             case LEFT:
596                 return DoubleScalar.minus(designLineOffset, halfWidth).immutable();
597             case RIGHT:
598                 return DoubleScalar.plus(designLineOffset, halfWidth).immutable();
599             default:
600                 throw new Error("Bad switch on LateralDirectionality " + lateralDirection);
601         }
602     }
603 
604     /**
605      * Return the lateral offset from the design line of the parent Link of the Left or Right boundary of this
606      * CrossSectionElement at the specified longitudinal position.
607      * @param lateralDirection LateralDirectionality; LEFT, or RIGHT
608      * @param longitudinalPosition DoubleScalar.Rel&lt;LengthUnit&gt;; the position along the length of this
609      *            CrossSectionElement
610      * @return DoubleScalar.Rel&lt;LengthUnit&gt;
611      */
612     public final DoubleScalar.Rel<LengthUnit> getLateralBoundaryPosition(final LateralDirectionality lateralDirection,
613             final DoubleScalar.Rel<LengthUnit> longitudinalPosition)
614     {
615         return getLateralBoundaryPosition(lateralDirection, longitudinalPosition.getSI() / getLength().getSI());
616     }
617 
618 }