OtsLine2d.java

  1. package org.opentrafficsim.core.geometry;

  2. import java.awt.geom.Line2D;
  3. import java.awt.geom.Path2D;
  4. import java.awt.geom.PathIterator;
  5. import java.awt.geom.Point2D;
  6. import java.io.Serializable;
  7. import java.util.ArrayList;
  8. import java.util.Arrays;
  9. import java.util.List;

  10. import org.djunits.unit.DirectionUnit;
  11. import org.djunits.value.vdouble.scalar.Direction;
  12. import org.djunits.value.vdouble.scalar.Length;
  13. import org.djutils.draw.bounds.Bounds2d;
  14. import org.djutils.draw.line.PolyLine2d;
  15. import org.djutils.draw.line.Ray2d;
  16. import org.djutils.draw.point.OrientedPoint2d;
  17. import org.djutils.draw.point.Point2d;
  18. import org.djutils.exceptions.Throw;
  19. import org.djutils.exceptions.Try;

  20. import nl.tudelft.simulation.dsol.animation.Locatable;

  21. /**
  22.  * Line with underlying PolyLine2d, a cached length indexed line, a cached length, and a cached centroid (all calculated on
  23.  * first use). This class supports fractional projection.
  24.  * <p>
  25.  * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  26.  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  27.  * </p>
  28.  * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  29.  * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  30.  * @author <a href="https://www.citg.tudelft.nl">Guus Tamminga</a>
  31.  * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  32.  */
  33. public class OtsLine2d implements Locatable, Serializable
  34. {
  35.     /** */
  36.     private static final long serialVersionUID = 20150722L;

  37.     /** The 2d line. */
  38.     private PolyLine2d line2d;

  39.     /** The cumulative length of the line at point 'i'. */
  40.     private double[] lengthIndexedLine = null;

  41.     /** The cached length; will be calculated at time of construction. */
  42.     private Length length;

  43.     /** The cached centroid; will be calculated when needed for the first time. */
  44.     private Point2d centroid = null;

  45.     /** The cached bounds; will be calculated when needed for the first time. */
  46.     private Bounds2d bounds = null;

  47.     /** The cached helper points for fractional projection; will be calculated when needed for the first time. */
  48.     private Point2d[] fractionalHelperCenters = null;

  49.     /** The cached helper directions for fractional projection; will be calculated when needed for the first time. */
  50.     private Point2D.Double[] fractionalHelperDirections = null;

  51.     /** Intersection of unit offset lines of first two segments. */
  52.     private Point2d firstOffsetIntersection;

  53.     /** Intersection of unit offset lines of last two segments. */
  54.     private Point2d lastOffsetIntersection;

  55.     /** Precision for fractional projection algorithm. */
  56.     private static final double FRAC_PROJ_PRECISION = 2e-5 /* PK too fine 1e-6 */;

  57.     /** Radius at each vertex. */
  58.     private Length[] vertexRadii;

  59.     /**
  60.      * Construct a new OtsLine2d.
  61.      * @param points Point2d...; the array of points to construct this OtsLine2d from.
  62.      */
  63.     public OtsLine2d(final Point2d... points)
  64.     {
  65.         this(new PolyLine2d(points));
  66.     }

  67.     /**
  68.      * Creates a new OtsLine2d based on 2d information. Elevation will be 0.
  69.      * @param line2d PolyLine2d; 2d information.
  70.      */
  71.     public OtsLine2d(final PolyLine2d line2d)
  72.     {
  73.         init(line2d);
  74.     }

  75.     /**
  76.      * Construct a new OtsLine2d, and immediately make the length-indexed line.
  77.      * @param line2d PolyLine2d; the 2d line.
  78.      */
  79.     private void init(final PolyLine2d line2d)
  80.     {
  81.         this.lengthIndexedLine = new double[line2d.size()];
  82.         this.lengthIndexedLine[0] = 0.0;
  83.         for (int i = 1; i < line2d.size(); i++)
  84.         {
  85.             this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + line2d.get(i - 1).distance(line2d.get(i));
  86.         }
  87.         this.line2d = line2d;
  88.         this.length = Length.instantiateSI(this.lengthIndexedLine[this.lengthIndexedLine.length - 1]);
  89.     }

  90.     /**
  91.      * Construct parallel line.<br>
  92.      * @param offset double; offset distance from the reference line; positive is LEFT, negative is RIGHT
  93.      * @return OtsLine2d; the line that has the specified offset from the reference line
  94.      */
  95.     public final OtsLine2d offsetLine(final double offset)
  96.     {
  97.         return new OtsLine2d(this.line2d.offsetLine(offset));
  98.     }

  99.     /**
  100.      * Clean up a list of points that describe a polyLine by removing points that lie within epsilon distance of a more
  101.      * straightened version of the line. <br>
  102.      * @param epsilon double; maximal deviation
  103.      * @param useHorizontalDistance boolean; if true; the horizontal distance is used; if false; the 3D distance is used
  104.      * @return OtsLine2d; a new OtsLine2d containing all the remaining points
  105.      */
  106.     @Deprecated
  107.     public final OtsLine2d noiseFilterRamerDouglasPeucker(final double epsilon, final boolean useHorizontalDistance)
  108.     {
  109.         // Apply the Ramer-Douglas-Peucker algorithm to the buffered points.
  110.         // Adapted from https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
  111.         double maxDeviation = 0;
  112.         int splitIndex = -1;
  113.         int pointCount = size();
  114.         // Find the point with largest deviation from the straight line from start point to end point
  115.         for (int i = 1; i < pointCount - 1; i++)
  116.         {
  117.             Point2d point = this.line2d.get(i);
  118.             Point2d closest = point.closestPointOnLine(this.line2d.get(0), this.line2d.get(pointCount - 1));
  119.             double deviation = useHorizontalDistance ? closest.distance(point) : closest.distance(point);
  120.             if (deviation > maxDeviation)
  121.             {
  122.                 splitIndex = i;
  123.                 maxDeviation = deviation;
  124.             }
  125.         }
  126.         if (maxDeviation <= epsilon)
  127.         {
  128.             // All intermediate points can be dropped. Return a new list containing only the first and last point.
  129.             return new OtsLine2d(this.line2d.get(0), this.line2d.get(pointCount - 1));
  130.         }
  131.         // The largest deviation is larger than epsilon.
  132.         // Split the polyLine at the point with the maximum deviation. Process each sub list recursively and concatenate the
  133.         // results
  134.         List<Point2d> points = this.line2d.getPointList();
  135.         OtsLine2d first = new OtsLine2d(points.subList(0, splitIndex + 1).toArray(new Point2d[splitIndex + 1]))
  136.                 .noiseFilterRamerDouglasPeucker(epsilon, useHorizontalDistance);
  137.         OtsLine2d second = new OtsLine2d(
  138.                 points.subList(splitIndex, this.line2d.size()).toArray(new Point2d[this.line2d.size() - splitIndex]))
  139.                         .noiseFilterRamerDouglasPeucker(epsilon, useHorizontalDistance);
  140.         return concatenate(epsilon, first, second);
  141.     }

  142.     /**
  143.      * Returns a 2d representation of this line.
  144.      * @return PolyLine2d; Returns a 2d representation of this line.
  145.      */
  146.     public PolyLine2d getLine2d()
  147.     {
  148.         return this.line2d;
  149.     }

  150.     /**
  151.      * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
  152.      * start of the reference line to its final offset value at the end of the reference line.
  153.      * @param offsetAtStart double; offset at the start of the reference line (positive value is Left, negative value is Right)
  154.      * @param offsetAtEnd double; offset at the end of the reference line (positive value is Left, negative value is Right)
  155.      * @return OtsLine2d; the OtsLine2d of the line at linearly changing offset of the reference line
  156.      */
  157.     public final OtsLine2d offsetLine(final double offsetAtStart, final double offsetAtEnd)
  158.     {
  159.         return new OtsLine2d(this.line2d.offsetLine(offsetAtStart, offsetAtEnd));
  160.     }

  161.     /**
  162.      * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
  163.      * start of the reference line via a number of intermediate offsets at intermediate positions to its final offset value at
  164.      * the end of the reference line.
  165.      * @param relativeFractions double[]; positional fractions for which the offsets have to be generated
  166.      * @param offsets double[]; offsets at the relative positions (positive value is Left, negative value is Right)
  167.      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
  168.      * @throws OtsGeometryException when this method fails to create the offset line
  169.      */
  170.     public final OtsLine2d offsetLine(final double[] relativeFractions, final double[] offsets) throws OtsGeometryException
  171.     {
  172.         return new OtsLine2d(OtsGeometryUtil.offsetLine(this.line2d, relativeFractions, offsets));
  173.     }

  174.     /**
  175.      * Concatenate several OtsLine2d instances.
  176.      * @param lines OtsLine2d...; OtsLine2d... one or more OtsLine2d. The last point of the first
  177.      *            &lt;strong&gt;must&lt;/strong&gt; match the first of the second, etc.
  178.      * @return OtsLine2d
  179.      */
  180.     public static OtsLine2d concatenate(final OtsLine2d... lines)
  181.     {
  182.         return concatenate(0.0, lines);
  183.     }

  184.     /**
  185.      * Concatenate two OtsLine2d instances. This method is separate for efficiency reasons.
  186.      * @param toleranceSI double; the tolerance between the end point of a line and the first point of the next line
  187.      * @param line1 OtsLine2d; first line
  188.      * @param line2 OtsLine2d; second line
  189.      * @return OtsLine2d
  190.      */
  191.     public static OtsLine2d concatenate(final double toleranceSI, final OtsLine2d line1, final OtsLine2d line2)
  192.     {
  193.         return new OtsLine2d(PolyLine2d.concatenate(toleranceSI, line1.line2d, line2.line2d));
  194.     }

  195.     /**
  196.      * Concatenate several OtsLine2d instances.
  197.      * @param toleranceSI double; the tolerance between the end point of a line and the first point of the next line
  198.      * @param lines OtsLine2d...; OtsLine2d... one or more OtsLine2d. The last point of the first
  199.      *            &lt;strong&gt;must&lt;/strong&gt; match the first of the second, etc.
  200.      * @return OtsLine2d
  201.      */
  202.     public static OtsLine2d concatenate(final double toleranceSI, final OtsLine2d... lines)
  203.     {
  204.         List<PolyLine2d> lines2d = new ArrayList<>();
  205.         for (OtsLine2d line : lines)
  206.         {
  207.             lines2d.add(line.line2d);
  208.         }
  209.         return new OtsLine2d(PolyLine2d.concatenate(toleranceSI, lines2d.toArray(new PolyLine2d[lines.length])));
  210.     }

  211.     /**
  212.      * Construct a new OtsLine2d with all points of this OtsLine2d in reverse order.
  213.      * @return OtsLine2d; the new OtsLine2d
  214.      */
  215.     public final OtsLine2d reverse()
  216.     {
  217.         return new OtsLine2d(this.line2d.reverse());
  218.     }

  219.     /**
  220.      * Construct a new OtsLine2d covering the indicated fraction of this OtsLine2d.
  221.      * @param start double; starting point, valid range [0..<cite>end</cite>)
  222.      * @param end double; ending point, valid range (<cite>start</cite>..1]
  223.      * @return OtsLine2d; the new OtsLine2d
  224.      */
  225.     public final OtsLine2d extractFractional(final double start, final double end)
  226.     {
  227.         return extract(start * this.length.si, end * this.length.si);
  228.     }

  229.     /**
  230.      * Create a new OtsLine2d that covers a sub-section of this OtsLine2d.
  231.      * @param start Length; the length along this OtsLine2d where the sub-section starts, valid range [0..<cite>end</cite>)
  232.      * @param end Length; length along this OtsLine2d where the sub-section ends, valid range
  233.      *            (<cite>start</cite>..<cite>length</cite> (length is the length of this OtsLine2d)
  234.      * @return OtsLine2d; the selected sub-section
  235.      */
  236.     public final OtsLine2d extract(final Length start, final Length end)
  237.     {
  238.         return extract(start.si, end.si);
  239.     }

  240.     /**
  241.      * Create a new OtsLine2d that covers a sub-section of this OtsLine2d.
  242.      * @param start double; length along this OtsLine2d where the sub-section starts, valid range [0..<cite>end</cite>)
  243.      * @param end double; length along this OtsLine2d where the sub-section ends, valid range
  244.      *            (<cite>start</cite>..<cite>length</cite> (length is the length of this OtsLine2d)
  245.      * @return OtsLine2d; the selected sub-section
  246.      */
  247.     public final OtsLine2d extract(final double start, final double end)
  248.     {
  249.         return new OtsLine2d(this.line2d.extract(start, end));
  250.     }

  251.     /**
  252.      * Create an OtsLine2d, while cleaning repeating successive points.
  253.      * @param points Point2d...; the coordinates of the line as OtsPoint3d
  254.      * @return the line
  255.      * @throws OtsGeometryException when number of points &lt; 2
  256.      */
  257.     public static OtsLine2d createAndCleanOtsLine2d(final Point2d... points) throws OtsGeometryException
  258.     {
  259.         if (points.length < 2)
  260.         {
  261.             throw new OtsGeometryException(
  262.                     "Degenerate OtsLine2d; has " + points.length + " point" + (points.length != 1 ? "s" : ""));
  263.         }
  264.         return createAndCleanOtsLine2d(new ArrayList<>(Arrays.asList(points)));
  265.     }

  266.     /**
  267.      * Create an OtsLine2d, while cleaning repeating successive points.
  268.      * @param pointList List&lt;Point2d&gt;; list of the coordinates of the line as OtsPoint3d; any duplicate points in this
  269.      *            list are removed (this method may modify the provided list)
  270.      * @return OtsLine2d; the line
  271.      * @throws OtsGeometryException when number of non-equal points &lt; 2
  272.      */
  273.     public static OtsLine2d createAndCleanOtsLine2d(final List<Point2d> pointList) throws OtsGeometryException
  274.     {
  275.         return new OtsLine2d(new PolyLine2d(true, pointList));
  276.     }

  277.     /**
  278.      * Construct a new OtsLine2d from a List&lt;OtsPoint3d&gt;.
  279.      * @param pointList List&lt;OtsPoint3d&gt;; the list of points to construct this OtsLine2d from.
  280.      * @throws OtsGeometryException when the provided points do not constitute a valid line (too few points or identical
  281.      *             adjacent points)
  282.      */
  283.     public OtsLine2d(final List<Point2d> pointList) throws OtsGeometryException
  284.     {
  285.         this(pointList.toArray(new Point2d[pointList.size()]));
  286.     }

  287.     /**
  288.      * Construct a new OtsShape (closed shape) from a Path2D. Elevation will be 0.
  289.      * @param path Path2D; the Path2D to construct this OtsLine2d from.
  290.      * @throws OtsGeometryException when the provided points do not constitute a valid line (too few points or identical
  291.      *             adjacent points)
  292.      */
  293.     public OtsLine2d(final Path2D path) throws OtsGeometryException
  294.     {
  295.         List<Point2d> pl = new ArrayList<>();
  296.         for (PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next())
  297.         {
  298.             double[] p = new double[6];
  299.             int segType = pi.currentSegment(p);
  300.             if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
  301.             {
  302.                 pl.add(new Point2d(p[0], p[1]));
  303.             }
  304.             else if (segType == PathIterator.SEG_CLOSE)
  305.             {
  306.                 if (!pl.get(0).equals(pl.get(pl.size() - 1)))
  307.                 {
  308.                     pl.add(new Point2d(pl.get(0).x, pl.get(0).y));
  309.                 }
  310.                 break;
  311.             }
  312.         }
  313.         init(new PolyLine2d(pl.toArray(new Point2d[pl.size() - 1])));
  314.     }

  315.     /**
  316.      * Return the number of points in this OtsLine2d. This is the number of points in horizontal plane.
  317.      * @return the number of points on the line
  318.      */
  319.     public final int size()
  320.     {
  321.         return this.line2d.size();
  322.     }

  323.     /**
  324.      * Return the first point of this OtsLine2d.
  325.      * @return the first point on the line
  326.      */
  327.     public final Point2d getFirst()
  328.     {
  329.         return this.line2d.getFirst();
  330.     }

  331.     /**
  332.      * Return the last point of this OtsLine2d.
  333.      * @return the last point on the line
  334.      */
  335.     public final Point2d getLast()
  336.     {
  337.         return this.line2d.getLast();
  338.     }

  339.     /**
  340.      * Return one point of this OtsLine2d.
  341.      * @param i int; the index of the point to retrieve
  342.      * @return Point2d; the i-th point of the line
  343.      * @throws OtsGeometryException when i &lt; 0 or i &gt; the number of points
  344.      */
  345.     public final Point2d get(final int i) throws OtsGeometryException
  346.     {
  347.         if (i < 0 || i > size() - 1)
  348.         {
  349.             throw new OtsGeometryException("OtsLine2d.get(i=" + i + "); i<0 or i>=size(), which is " + size());
  350.         }
  351.         return this.line2d.get(i);
  352.     }

  353.     /**
  354.      * Return the length of this OtsLine2d in meters. (Assuming that the coordinates of the points constituting this line are
  355.      * expressed in meters.)
  356.      * @return the length of the line
  357.      */
  358.     public final Length getLength()
  359.     {
  360.         return this.length;
  361.     }

  362.     /**
  363.      * Return an array of OtsPoint3d that represents this OtsLine2d.
  364.      * @return the points of this line
  365.      */
  366.     public final Point2d[] getPoints()
  367.     {
  368.         return this.line2d.getPointList().toArray(new Point2d[this.line2d.size()]);
  369.     }

  370.     /**
  371.      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
  372.      * that case, the position will be extrapolated in the direction of the line at its start or end.
  373.      * @param position Length; the position on the line for which to calculate the point on, before, of after the line
  374.      * @return a directed point
  375.      */
  376.     public final OrientedPoint2d getLocationExtended(final Length position)
  377.     {
  378.         return getLocationExtendedSI(position.getSI());
  379.     }

  380.     /**
  381.      * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
  382.      * that case, the position will be extrapolated in the direction of the line at its start or end.
  383.      * @param positionSI double; the position on the line for which to calculate the point on, before, of after the line, in SI
  384.      *            units
  385.      * @return a directed point
  386.      */
  387.     public final synchronized OrientedPoint2d getLocationExtendedSI(final double positionSI)
  388.     {
  389.         Ray2d ray = this.line2d.getLocationExtended(positionSI);
  390.         return new OrientedPoint2d(ray.x, ray.y, ray.phi);
  391.     }

  392.     /**
  393.      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
  394.      * @param fraction double; the fraction for which to calculate the point on the line
  395.      * @return a directed point
  396.      * @throws OtsGeometryException when fraction less than 0.0 or more than 1.0.
  397.      */
  398.     public final OrientedPoint2d getLocationFraction(final double fraction) throws OtsGeometryException
  399.     {
  400.         if (fraction < 0.0 || fraction > 1.0)
  401.         {
  402.             throw new OtsGeometryException("getLocationFraction for line: fraction < 0.0 or > 1.0. fraction = " + fraction);
  403.         }
  404.         return getLocationSI(fraction * this.length.si);
  405.     }

  406.     /**
  407.      * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
  408.      * @param fraction double; the fraction for which to calculate the point on the line
  409.      * @param tolerance double; the delta from 0.0 and 1.0 that will be forgiven
  410.      * @return a directed point
  411.      * @throws OtsGeometryException when fraction less than 0.0 or more than 1.0.
  412.      */
  413.     public final OrientedPoint2d getLocationFraction(final double fraction, final double tolerance) throws OtsGeometryException
  414.     {
  415.         if (fraction < -tolerance || fraction > 1.0 + tolerance)
  416.         {
  417.             throw new OtsGeometryException(
  418.                     "getLocationFraction for line: fraction < 0.0 - tolerance or > 1.0 + tolerance; fraction = " + fraction);
  419.         }
  420.         double f = fraction < 0 ? 0.0 : fraction > 1.0 ? 1.0 : fraction;
  421.         return getLocationSI(f * this.length.si);
  422.     }

  423.     /**
  424.      * Get the location at a fraction of the line (or outside the line), with its direction.
  425.      * @param fraction double; the fraction for which to calculate the point on the line
  426.      * @return a directed point
  427.      */
  428.     public final OrientedPoint2d getLocationFractionExtended(final double fraction)
  429.     {
  430.         return getLocationExtendedSI(fraction * this.length.si);
  431.     }

  432.     /**
  433.      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
  434.      * @param position Length; the position on the line for which to calculate the point on the line
  435.      * @return a directed point
  436.      * @throws OtsGeometryException when position less than 0.0 or more than line length.
  437.      */
  438.     public final OrientedPoint2d getLocation(final Length position) throws OtsGeometryException
  439.     {
  440.         return getLocationSI(position.getSI());
  441.     }

  442.     /**
  443.      * Binary search for a position on the line.
  444.      * @param pos double; the position to look for.
  445.      * @return the index below the position; the position is between points[index] and points[index+1]
  446.      * @throws OtsGeometryException when index could not be found
  447.      */
  448.     private int find(final double pos) throws OtsGeometryException
  449.     {
  450.         if (pos == 0)
  451.         {
  452.             return 0;
  453.         }

  454.         int lo = 0;
  455.         int hi = this.lengthIndexedLine.length - 1;
  456.         while (lo <= hi)
  457.         {
  458.             if (hi == lo)
  459.             {
  460.                 return lo;
  461.             }
  462.             int mid = lo + (hi - lo) / 2;
  463.             if (pos < this.lengthIndexedLine[mid])
  464.             {
  465.                 hi = mid - 1;
  466.             }
  467.             else if (pos > this.lengthIndexedLine[mid + 1])
  468.             {
  469.                 lo = mid + 1;
  470.             }
  471.             else
  472.             {
  473.                 return mid;
  474.             }
  475.         }
  476.         throw new OtsGeometryException(
  477.                 "Could not find position " + pos + " on line with length indexes: " + Arrays.toString(this.lengthIndexedLine));
  478.     }

  479.     /**
  480.      * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
  481.      * @param positionSI double; the position on the line for which to calculate the point on the line
  482.      * @return a directed point
  483.      * @throws OtsGeometryException when position less than 0.0 or more than line length.
  484.      */
  485.     public final OrientedPoint2d getLocationSI(final double positionSI) throws OtsGeometryException
  486.     {
  487.         Ray2d ray = Try.assign(() -> this.line2d.getLocation(positionSI), OtsGeometryException.class, "Position not on line.");
  488.         return new OrientedPoint2d(ray.x, ray.y, ray.phi);
  489.     }

  490.     /**
  491.      * Truncate a line at the given length (less than the length of the line, and larger than zero) and return a new line.
  492.      * @param lengthSI double; the location where to truncate the line
  493.      * @return a new OtsLine2d truncated at the exact position where line.getLength() == lengthSI
  494.      * @throws OtsGeometryException when position less than 0.0 or more than line length.
  495.      */
  496.     public final OtsLine2d truncate(final double lengthSI) throws OtsGeometryException
  497.     {
  498.         return new OtsLine2d(this.line2d.truncate(lengthSI));
  499.     }

  500.     /**
  501.      * Returns the fractional position along this line of the orthogonal projection of point (x, y) on this line. If the point
  502.      * is not orthogonal to the closest line segment, the nearest point is selected.
  503.      * @param x double; x-coordinate of point to project
  504.      * @param y double; y-coordinate of point to project
  505.      * @return fractional position along this line of the orthogonal projection on this line of a point
  506.      */
  507.     public final double projectOrthogonal(final double x, final double y)
  508.     {
  509.         Point2d closest = this.line2d.closestPointOnPolyLine(new Point2d(x, y));
  510.         return this.line2d.projectOrthogonalFractionalExtended(closest);
  511.     }

  512.     /**
  513.      * Returns the fractional projection of a point to a line. The projection works by taking slices in space per line segment
  514.      * as shown below. A point is always projected to the nearest segment, but not necessarily to the closest point on that
  515.      * segment. The slices in space are analogous to a Voronoi diagram, but for the line segments instead of points. If
  516.      * fractional projection fails, the orthogonal projection is returned.<br>
  517.      * <br>
  518.      * The point 'A' is projected to point 'B' on the 3rd segment of line 'C-D'. The line from 'A' to 'B' extends towards point
  519.      * 'E', which is the intersection of lines 'E-F' and 'E-G'. Line 'E-F' cuts the first bend of the 3rd segment (at point 'H')
  520.      * in half, while the line 'E-G' cuts the second bend of the 3rd segment (at point 'I') in half.
  521.      *
  522.      * <pre>
  523.      *            ____________________________     G                   .
  524.      * .         |                            |    .                 .
  525.      *   .       |  . . . .  helper lines     |    .               .
  526.      *     .     |  _.._.._  projection line  |   I.             .
  527.      *       .   |____________________________|  _.'._         .       L
  528.      *        F.                              _.'  .  '-.    .
  529.      *          ..                       B _.'     .     '-.
  530.      *           . .                    _.\        .     .  D
  531.      *            .  .               _.'   :       .   .
  532.      *     J       .   .          _.'      \       . .
  533.      *             ..    .     _.'          :      .                M
  534.      *            .  .     ..-'             \      .
  535.      *           .    .    /H.               A     .
  536.      *          .      .  /    .                   .
  537.      *        C _________/       .                 .
  538.      *        .          .         .               .
  539.      *   K   .            .          .             .
  540.      *      .              .           .           .
  541.      *     .                .            .         .           N
  542.      *    .                  .             .       .
  543.      *   .                    .              .     .
  544.      *  .                      .               .   .
  545.      * .                        .                . .
  546.      *                           .                 .E
  547.      *                            .                  .
  548.      *                             .                   .
  549.      *                              .                    .
  550.      * </pre>
  551.      *
  552.      * Fractional projection may fail in three cases.
  553.      * <ol>
  554.      * <li>Numerical difficulties at slight bend, orthogonal projection returns the correct point.</li>
  555.      * <li>Fractional projection is possible only to segments that aren't the nearest segment(s).</li>
  556.      * <li>Fractional projection is possible for no segment.</li>
  557.      * </ol>
  558.      * In the latter two cases the projection is undefined and a orthogonal projection is returned if
  559.      * {@code orthoFallback = true}, or {@code NaN} if {@code orthoFallback = false}.
  560.      * @param start Direction; direction in first point
  561.      * @param end Direction; direction in last point
  562.      * @param x double; x-coordinate of point to project
  563.      * @param y double; y-coordinate of point to project
  564.      * @param fallback FractionalFallback; fallback method for when fractional projection fails
  565.      * @return fractional position along this line of the fractional projection on that line of a point
  566.      */
  567.     public final synchronized double projectFractional(final Direction start, final Direction end, final double x,
  568.             final double y, final FractionalFallback fallback)
  569.     {

  570.         // prepare
  571.         double minDistance = Double.POSITIVE_INFINITY;
  572.         double minSegmentFraction = 0;
  573.         int minSegment = -1;
  574.         Point2d point = new Point2d(x, y);

  575.         // determine helpers (centers and directions)
  576.         determineFractionalHelpers(start, end);

  577.         // get distance of point to each segment
  578.         double[] d = new double[size() - 1];
  579.         double minD = Double.POSITIVE_INFINITY;
  580.         for (int i = 0; i < size() - 1; i++)
  581.         {
  582.             d[i] = Line2D.ptSegDist(this.line2d.get(i).x, this.line2d.get(i).y, this.line2d.get(i + 1).x,
  583.                     this.line2d.get(i + 1).y, x, y);
  584.             minD = d[i] < minD ? d[i] : minD;
  585.         }

  586.         // loop over segments for projection
  587.         double distance;
  588.         for (int i = 0; i < size() - 1; i++)
  589.         {
  590.             // skip if not the closest segment, note that often two segments are equally close in their shared end point
  591.             if (d[i] > minD + FRAC_PROJ_PRECISION)
  592.             {
  593.                 continue;
  594.             }
  595.             Point2d center = this.fractionalHelperCenters[i];
  596.             Point2d p;
  597.             if (center != null)
  598.             {
  599.                 // get intersection of line "center - (x, y)" and the segment
  600.                 p = intersectionOfLines(center, point, this.line2d.get(i), this.line2d.get(i + 1));
  601.                 if (p == null || (x < center.x + FRAC_PROJ_PRECISION && center.x + FRAC_PROJ_PRECISION < p.x)
  602.                         || (x > center.x - FRAC_PROJ_PRECISION && center.x - FRAC_PROJ_PRECISION > p.x)
  603.                         || (y < center.y + FRAC_PROJ_PRECISION && center.y + FRAC_PROJ_PRECISION < p.y)
  604.                         || (y > center.y - FRAC_PROJ_PRECISION && center.y - FRAC_PROJ_PRECISION > p.y))
  605.                 {
  606.                     // projected point may not be 'beyond' segment center (i.e. center may not be between (x, y) and (p.x, p.y)
  607.                     continue;
  608.                 }
  609.             }
  610.             else
  611.             {
  612.                 // parallel helper lines, project along direction
  613.                 Point2d offsetPoint =
  614.                         new Point2d(x + this.fractionalHelperDirections[i].x, y + this.fractionalHelperDirections[i].y);
  615.                 p = intersectionOfLines(point, offsetPoint, this.line2d.get(i), this.line2d.get(i + 1));
  616.             }
  617.             double segLength = this.line2d.get(i).distance(this.line2d.get(i + 1)) + FRAC_PROJ_PRECISION;
  618.             if (p == null || this.line2d.get(i).distance(p) > segLength || this.line2d.get(i + 1).distance(p) > segLength)
  619.             {
  620.                 // intersection must be on the segment
  621.                 // in case of p == null, the length of the fractional helper direction falls away due to precision
  622.                 continue;
  623.             }
  624.             // distance from (x, y) to intersection on segment
  625.             double dx = x - p.x;
  626.             double dy = y - p.y;
  627.             distance = Math.hypot(dx, dy);
  628.             // distance from start of segment to point on segment
  629.             if (distance < minDistance)
  630.             {
  631.                 dx = p.x - this.line2d.get(i).x;
  632.                 dy = p.y - this.line2d.get(i).y;
  633.                 double dFrac = Math.hypot(dx, dy);
  634.                 // fraction to point on segment
  635.                 minDistance = distance;
  636.                 minSegmentFraction = dFrac / (this.lengthIndexedLine[i + 1] - this.lengthIndexedLine[i]);
  637.                 minSegment = i;
  638.             }
  639.         }

  640.         // return
  641.         if (minSegment == -1)

  642.         {
  643.             /*
  644.              * If fractional projection fails (x, y) is either outside of the applicable area for fractional projection, or is
  645.              * inside an area where numerical difficulties arise (i.e. far away outside of very slight bend which is considered
  646.              * parallel).
  647.              */
  648.             // CategoryLogger.info(Cat.CORE, "projectFractional failed to project " + point + " on " + this
  649.             // + "; using fallback approach");
  650.             return fallback.getFraction(this, x, y);
  651.         }

  652.         double segLen = this.lengthIndexedLine[minSegment + 1] - this.lengthIndexedLine[minSegment];
  653.         return (this.lengthIndexedLine[minSegment] + segLen * minSegmentFraction) / this.length.si;

  654.     }

  655.     /**
  656.      * Fallback method for when fractional projection fails as the point is beyond the line or from numerical limitations.
  657.      * <p>
  658.      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  659.      * <br>
  660.      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  661.      * </p>
  662.      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
  663.      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  664.      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  665.      */
  666.     public enum FractionalFallback
  667.     {
  668.         /** Orthogonal projection. */
  669.         ORTHOGONAL
  670.         {
  671.             @Override
  672.             double getFraction(final OtsLine2d line, final double x, final double y)
  673.             {
  674.                 return line.projectOrthogonal(x, y);
  675.             }
  676.         },

  677.         /** Distance to nearest end point. */
  678.         ENDPOINT
  679.         {
  680.             @Override
  681.             double getFraction(final OtsLine2d line, final double x, final double y)
  682.             {
  683.                 Point2d point = new Point2d(x, y);
  684.                 double dStart = point.distance(line.getFirst());
  685.                 double dEnd = point.distance(line.getLast());
  686.                 if (dStart < dEnd)
  687.                 {
  688.                     return -dStart / line.length.si;
  689.                 }
  690.                 else
  691.                 {
  692.                     return (dEnd + line.length.si) / line.length.si;
  693.                 }
  694.             }
  695.         },

  696.         /** NaN value. */
  697.         NaN
  698.         {
  699.             @Override
  700.             double getFraction(final OtsLine2d line, final double x, final double y)
  701.             {
  702.                 return Double.NaN;
  703.             }
  704.         };

  705.         /**
  706.          * Returns fraction for when fractional projection fails as the point is beyond the line or from numerical limitations.
  707.          * @param line OtsLine2d; line
  708.          * @param x double; x coordinate of point
  709.          * @param y double; y coordinate of point
  710.          * @return double; fraction for when fractional projection fails
  711.          */
  712.         abstract double getFraction(OtsLine2d line, double x, double y);

  713.     }

  714.     /**
  715.      * Determines all helpers (points and/or directions) for fractional projection and stores fixed information in properties
  716.      * while returning the first and last center points (.
  717.      * @param start Direction; direction in first point
  718.      * @param end Direction; direction in last point
  719.      */
  720.     private synchronized void determineFractionalHelpers(final Direction start, final Direction end)
  721.     {

  722.         final int n = size() - 1;

  723.         // calculate fixed helpers if not done yet
  724.         if (this.fractionalHelperCenters == null)
  725.         {
  726.             this.fractionalHelperCenters = new Point2d[n];
  727.             this.fractionalHelperDirections = new Point2D.Double[n];
  728.             if (size() > 2)
  729.             {
  730.                 // intersection of parallel lines of first and second segment
  731.                 PolyLine2d prevOfsSeg = unitOffsetSegment(0);
  732.                 PolyLine2d nextOfsSeg = unitOffsetSegment(1);
  733.                 Point2d parStartPoint;
  734.                 parStartPoint = intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0), nextOfsSeg.get(1));
  735.                 if (parStartPoint == null || prevOfsSeg.get(1).distance(nextOfsSeg.get(0)) < Math
  736.                         .min(prevOfsSeg.get(1).distance(parStartPoint), nextOfsSeg.get(0).distance(parStartPoint)))
  737.                 {
  738.                     parStartPoint = new Point2d((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
  739.                             (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
  740.                 }
  741.                 // remember the intersection of the first two unit offset segments
  742.                 this.firstOffsetIntersection = parStartPoint;
  743.                 // loop segments
  744.                 for (int i = 1; i < size() - 2; i++)
  745.                 {
  746.                     prevOfsSeg = nextOfsSeg;
  747.                     nextOfsSeg = unitOffsetSegment(i + 1);
  748.                     Point2d parEndPoint;
  749.                     parEndPoint =
  750.                             intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0), nextOfsSeg.get(1));
  751.                     if (parEndPoint == null || prevOfsSeg.get(1).distance(nextOfsSeg.get(0)) < Math
  752.                             .min(prevOfsSeg.get(1).distance(parEndPoint), nextOfsSeg.get(0).distance(parEndPoint)))
  753.                     {
  754.                         parEndPoint = new Point2d((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
  755.                                 (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
  756.                     }
  757.                     // center = intersections of helper lines
  758.                     this.fractionalHelperCenters[i] =
  759.                             intersectionOfLines(this.line2d.get(i), parStartPoint, this.line2d.get(i + 1), parEndPoint);
  760.                     if (this.fractionalHelperCenters[i] == null)
  761.                     {
  762.                         // parallel helper lines, parallel segments or /\/ cause parallel helper lines, use direction
  763.                         this.fractionalHelperDirections[i] = new Point2D.Double(parStartPoint.x - this.line2d.get(i).x,
  764.                                 parStartPoint.y - this.line2d.get(i).y);
  765.                     }
  766.                     parStartPoint = parEndPoint;
  767.                 }
  768.                 // remember the intersection of the last two unit offset segments
  769.                 this.lastOffsetIntersection = parStartPoint;
  770.             }
  771.         }

  772.         // use directions at start and end to get unit offset points to the left at a distance of 1
  773.         double ang = (start == null
  774.                 ? Math.atan2(this.line2d.get(1).y - this.line2d.get(0).y, this.line2d.get(1).x - this.line2d.get(0).x)
  775.                 : start.getInUnit(DirectionUnit.DEFAULT)) + Math.PI / 2; // start.si + Math.PI / 2;
  776.         Point2d p1 = new Point2d(this.line2d.get(0).x + Math.cos(ang), this.line2d.get(0).y + Math.sin(ang));
  777.         ang = (end == null
  778.                 ? Math.atan2(this.line2d.get(n).y - this.line2d.get(n - 1).y, this.line2d.get(n).x - this.line2d.get(n - 1).x)
  779.                 : end.getInUnit(DirectionUnit.DEFAULT)) + Math.PI / 2; // end.si + Math.PI / 2;
  780.         Point2d p2 = new Point2d(this.line2d.get(n).x + Math.cos(ang), this.line2d.get(n).y + Math.sin(ang));

  781.         // calculate first and last center (i.e. intersection of unit offset segments), which depend on inputs 'start' and 'end'
  782.         if (size() > 2)
  783.         {
  784.             this.fractionalHelperCenters[0] =
  785.                     intersectionOfLines(this.line2d.get(0), p1, this.line2d.get(1), this.firstOffsetIntersection);
  786.             this.fractionalHelperCenters[n - 1] =
  787.                     intersectionOfLines(this.line2d.get(n - 1), this.lastOffsetIntersection, this.line2d.get(n), p2);
  788.             if (this.fractionalHelperCenters[n - 1] == null)
  789.             {
  790.                 // parallel helper lines, use direction for projection
  791.                 this.fractionalHelperDirections[n - 1] =
  792.                         new Point2D.Double(p2.x - this.line2d.get(n).x, p2.y - this.line2d.get(n).y);
  793.             }
  794.         }
  795.         else
  796.         {
  797.             // only a single segment
  798.             this.fractionalHelperCenters[0] = intersectionOfLines(this.line2d.get(0), p1, this.line2d.get(1), p2);
  799.         }
  800.         if (this.fractionalHelperCenters[0] == null)
  801.         {
  802.             // parallel helper lines, use direction for projection
  803.             this.fractionalHelperDirections[0] = new Point2D.Double(p1.x - this.line2d.get(0).x, p1.y - this.line2d.get(0).y);
  804.         }

  805.     }

  806.     /**
  807.      * This method is used, rather than {@code Point2d.intersectionOfLines()} because this method will return {@code null} if
  808.      * the determinant &lt; 0.0000001, rather than determinant &eq; 0.0. The benefit of this is that intersections are not so
  809.      * far away, that any calculations with them cause underflow or overflow issues.
  810.      * @param line1P1 Point2d; point 1 of line 1.
  811.      * @param line1P2 Point2d; point 2 of line 1.
  812.      * @param line2P1 Point2d; point 1 of line 2.
  813.      * @param line2P2 Point2d; point 2 of line 2.
  814.      * @return Point2d; intersection of lines, or {@code null} for (nearly) parallel lines.
  815.      */
  816.     private Point2d intersectionOfLines(final Point2d line1P1, final Point2d line1P2, final Point2d line2P1,
  817.             final Point2d line2P2)
  818.     {
  819.         double l1p1x = line1P1.x;
  820.         double l1p1y = line1P1.y;
  821.         double l1p2x = line1P2.x - l1p1x;
  822.         double l1p2y = line1P2.y - l1p1y;
  823.         double l2p1x = line2P1.x - l1p1x;
  824.         double l2p1y = line2P1.y - l1p1y;
  825.         double l2p2x = line2P2.x - l1p1x;
  826.         double l2p2y = line2P2.y - l1p1y;
  827.         double determinant = (0 - l1p2x) * (l2p1y - l2p2y) - (0 - l1p2y) * (l2p1x - l2p2x);
  828.         if (Math.abs(determinant) < 0.0000001)
  829.         {
  830.             return null;
  831.         }
  832.         return new Point2d(l1p1x + (l1p2x * (l2p1x * l2p2y - l2p1y * l2p2x)) / determinant,
  833.                 l1p1y + (l1p2y * (l2p1x * l2p2y - l2p1y * l2p2x)) / determinant);
  834.     }

  835.     /**
  836.      * Helper method for fractional projection which returns an offset line to the left of a segment at a distance of 1.
  837.      * @param segment int; segment number
  838.      * @return parallel line to the left of a segment at a distance of 1
  839.      */
  840.     private synchronized PolyLine2d unitOffsetSegment(final int segment)
  841.     {
  842.         return new PolyLine2d(this.line2d.get(segment), this.line2d.get(segment + 1)).offsetLine(1.0);
  843.     }

  844.     /**
  845.      * Returns the projected directional radius of the line at a given fraction. Negative values reflect right-hand curvature in
  846.      * the design-line direction. The radius is taken as the minimum of the radii at the vertices before and after the given
  847.      * fraction. The radius at a vertex is calculated as the radius of a circle that is equidistant from both edges connected to
  848.      * the vertex. The circle center is on a line perpendicular to the shortest edge, crossing through the middle of the
  849.      * shortest edge. This method ignores Z components.
  850.      * @param fraction double; fraction along the line, between 0.0 and 1.0 (both inclusive)
  851.      * @return Length; radius; the local radius; or si field set to Double.NaN if line is totally straight
  852.      * @throws OtsGeometryException fraction out of bounds
  853.      */
  854.     // TODO: move to djutils?
  855.     public synchronized Length getProjectedRadius(final double fraction) throws OtsGeometryException
  856.     {
  857.         Throw.when(fraction < 0.0 || fraction > 1.0, OtsGeometryException.class, "Fraction %f is out of bounds [0.0 ... 1.0]",
  858.                 fraction);
  859.         if (this.vertexRadii == null)
  860.         {
  861.             this.vertexRadii = new Length[size() - 1];
  862.         }
  863.         int index = find(fraction * getLength().si);
  864.         if (index > 0 && this.vertexRadii[index] == null)
  865.         {
  866.             this.vertexRadii[index] = getProjectedVertexRadius(index);
  867.         }
  868.         if (index < size() - 2 && this.vertexRadii[index + 1] == null)
  869.         {
  870.             this.vertexRadii[index + 1] = getProjectedVertexRadius(index + 1);
  871.         }
  872.         if (index == 0)
  873.         {
  874.             if (this.vertexRadii.length < 2)
  875.             {
  876.                 return Length.instantiateSI(Double.NaN);
  877.             }
  878.             return this.vertexRadii[1];
  879.         }
  880.         if (index == size() - 2)
  881.         {
  882.             return this.vertexRadii[size() - 2];
  883.         }
  884.         return Math.abs(this.vertexRadii[index].si) < Math.abs(this.vertexRadii[index + 1].si) ? this.vertexRadii[index]
  885.                 : this.vertexRadii[index + 1];
  886.     }

  887.     /**
  888.      * Calculates the directional radius at a vertex. Negative values reflect right-hand curvature in the design-line direction.
  889.      * The radius at a vertex is calculated as the radius of a circle that is equidistant from both edges connected to the
  890.      * vertex. The circle center is on a line perpendicular to the shortest edge, crossing through the middle of the shortest
  891.      * edge. This function ignores Z components.
  892.      * @param index int; index of the vertex in range [1 ... size() - 2]
  893.      * @return Length; radius at the vertex
  894.      * @throws OtsGeometryException if the index is out of bounds
  895.      */
  896.     // TODO: move to djutils? Note, uses fractionalHelperCenters
  897.     public synchronized Length getProjectedVertexRadius(final int index) throws OtsGeometryException
  898.     {
  899.         Throw.when(index < 1 || index > size() - 2, OtsGeometryException.class, "Index %d is out of bounds [1 ... size() - 2].",
  900.                 index);
  901.         determineFractionalHelpers(null, null);
  902.         double length1 = this.lengthIndexedLine[index] - this.lengthIndexedLine[index - 1];
  903.         double length2 = this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index];
  904.         int shortIndex = length1 < length2 ? index : index + 1;
  905.         // center of shortest edge
  906.         Point2d p1 = new Point2d(.5 * (this.line2d.get(shortIndex - 1).x + this.line2d.get(shortIndex).x),
  907.                 .5 * (this.line2d.get(shortIndex - 1).y + this.line2d.get(shortIndex).y));
  908.         // perpendicular to shortest edge, line crossing p1
  909.         Point2d p2 = new Point2d(p1.x + (this.line2d.get(shortIndex).y - this.line2d.get(shortIndex - 1).y),
  910.                 p1.y - (this.line2d.get(shortIndex).x - this.line2d.get(shortIndex - 1).x));
  911.         // vertex
  912.         Point2d p3 = this.line2d.get(index);
  913.         // point on line that splits angle between edges at vertex 50-50
  914.         Point2d p4 = this.fractionalHelperCenters[index];
  915.         if (p4 == null)
  916.         {
  917.             // parallel helper lines
  918.             p4 = new Point2d(p3.x + this.fractionalHelperDirections[index].x, p3.y + this.fractionalHelperDirections[index].y);
  919.         }
  920.         Point2d intersection = intersectionOfLines(p1, p2, p3, p4);
  921.         if (null == intersection)
  922.         {
  923.             return Length.instantiateSI(Double.NaN);
  924.         }
  925.         // determine left or right
  926.         double refLength = length1 < length2 ? length1 : length2;
  927.         double radius = intersection.distance(p1);
  928.         double i2p2 = intersection.distance(p2);
  929.         if (radius < i2p2 && i2p2 > refLength)
  930.         {
  931.             // left as p1 is closer than p2 (which was placed to the right) and not on the perpendicular line
  932.             return Length.instantiateSI(radius);
  933.         }
  934.         // right as not left
  935.         return Length.instantiateSI(-radius);
  936.     }

  937.     /**
  938.      * Returns the length fraction at the vertex.
  939.      * @param index int; index of vertex [0 ... size() - 1]
  940.      * @return double; length fraction at the vertex
  941.      * @throws OtsGeometryException if the index is out of bounds
  942.      */
  943.     public double getVertexFraction(final int index) throws OtsGeometryException
  944.     {
  945.         Throw.when(index < 0 || index > size() - 1, OtsGeometryException.class, "Index %d is out of bounds [0 %d].", index,
  946.                 size() - 1);
  947.         return this.lengthIndexedLine[index] / this.length.si;
  948.     }

  949.     /**
  950.      * Retrieve the centroid of this OtsLine2d.
  951.      * @return OtsPoint3d; the centroid of this OtsLine2d
  952.      */
  953.     public final Point2d getCentroid()
  954.     {
  955.         if (this.centroid == null)
  956.         {
  957.             this.centroid = this.line2d.getBounds().midPoint();
  958.         }
  959.         return this.centroid;
  960.     }

  961.     /**
  962.      * Get the bounding rectangle of this OtsLine2d.
  963.      * @return Rectangle2D; the bounding rectangle of this OtsLine2d
  964.      */
  965.     public final Bounds2d getEnvelope()
  966.     {
  967.         return this.line2d.getBounds();
  968.     }

  969.     /** {@inheritDoc} */
  970.     @Override
  971.     @SuppressWarnings("checkstyle:designforextension")
  972.     public Point2d getLocation()
  973.     {
  974.         return getCentroid();
  975.     }

  976.     /** {@inheritDoc} */
  977.     @Override
  978.     @SuppressWarnings("checkstyle:designforextension")
  979.     public Bounds2d getBounds()
  980.     {
  981.         if (this.bounds == null)
  982.         {
  983.             Bounds2d envelope = getEnvelope();
  984.             this.bounds = new Bounds2d(envelope.getDeltaX(), envelope.getDeltaY());
  985.         }
  986.         return this.bounds;
  987.     }

  988.     /** {@inheritDoc} */
  989.     @Override
  990.     @SuppressWarnings("checkstyle:designforextension")
  991.     public String toString()
  992.     {
  993.         return this.line2d.toString();
  994.     }

  995.     /** {@inheritDoc} */
  996.     @Override
  997.     @SuppressWarnings("checkstyle:designforextension")
  998.     public int hashCode()
  999.     {
  1000.         return this.line2d.hashCode();
  1001.     }

  1002.     /** {@inheritDoc} */
  1003.     @Override
  1004.     @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
  1005.     public boolean equals(final Object obj)
  1006.     {
  1007.         if (!(obj instanceof OtsLine2d))
  1008.         {
  1009.             return false;
  1010.         }
  1011.         return this.line2d.equals(((OtsLine2d) obj).line2d);
  1012.     }

  1013.     /**
  1014.      * Convert the 2D projection of this OtsLine2d to something that MS-Excel can plot.
  1015.      * @return excel XY plottable output
  1016.      */
  1017.     public final String toExcel()
  1018.     {
  1019.         return this.line2d.toExcel();
  1020.     }

  1021.     /**
  1022.      * Convert the 2D projection of this OtsLine2d to Peter's plot format.
  1023.      * @return Peter's format plot output
  1024.      */
  1025.     public final String toPlot()
  1026.     {
  1027.         return this.line2d.toPlot();
  1028.     }

  1029. }