View Javadoc
1   package org.opentrafficsim.base.geometry;
2   
3   import java.util.ArrayList;
4   import java.util.List;
5   import java.util.Locale;
6   import java.util.stream.Collectors;
7   import java.util.stream.DoubleStream;
8   
9   import org.djunits.value.vdouble.scalar.Angle;
10  import org.djutils.draw.line.PolyLine2d;
11  import org.djutils.draw.point.DirectedPoint2d;
12  import org.djutils.draw.point.Point2d;
13  import org.djutils.exceptions.Throw;
14  
15  /**
16   * Utility class for OTS geometry.
17   * <p>
18   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
19   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
20   * </p>
21   * @author Alexander Verbraeck
22   * @author Peter Knoppers
23   * @author Wouter Schakel
24   */
25  public final class OtsGeometryUtil
26  {
27      /** */
28      private OtsGeometryUtil()
29      {
30          // do not instantiate this class.
31      }
32  
33      /**
34       * Print one Point2d on the console.
35       * @param prefix text to put before the output
36       * @param point the coordinate to print
37       * @return String
38       */
39      public static String printCoordinate(final String prefix, final Point2d point)
40      {
41          return String.format(Locale.US, "%s %8.3f,%8.3f   ", prefix, point.x, point.y);
42      }
43  
44      /**
45       * Returns the number of segments to use for a given maximum spatial error, and radius.
46       * @param maxSpatialError maximum spatial error.
47       * @param angle angle of arc at radius.
48       * @param r critical radius (largest radius).
49       * @return number of segments to use for a given maximum spatial error, and radius.
50       */
51      public static int getNumSegmentsForRadius(final double maxSpatialError, final Angle angle, final double r)
52      {
53          /*-
54           * Geometric derivation from a right-angled half pizza slice:
55           * b = adjacent side of triangle = line from center of circle to middle of straight line arc segment
56           * r = radius = hypotenuse
57           * a = maxDeviation;
58           * r = a + b (middle of straight line segment has largest deviation)
59           * phi = |endAng - startAng| / 2n = angle at center of circle in right-angled half pizza slice = half angle of slice
60           * n = number of segments
61           *
62           * r - a = b = r * cos(phi)
63           * => 1 - (a / r) = cos(phi)
64           * => phi = acos(1 - (a / r)) = |endAng - startAng| / 2n
65           * => n = |endAng - startAng| / 2 * acos(1 - (a / r))
66           */
67          return (int) Math.ceil(angle.si / (2.0 * Math.acos(1.0 - maxSpatialError / r)));
68      }
69  
70      /**
71       * Returns a point on a line through the given point, perpendicular to the given direction, at the offset distance. A
72       * negative offset is towards the right hand side relative to the direction.
73       * @param point point.
74       * @param offset offset, negative values are to the right.
75       * @return offset point.
76       */
77      public static DirectedPoint2d offsetPoint(final DirectedPoint2d point, final double offset)
78      {
79          return new DirectedPoint2d(point.x - Math.sin(point.dirZ) * offset, point.y + Math.cos(point.dirZ) * offset,
80                  point.dirZ);
81      }
82  
83      /**
84       * Translates a directed point a given distance in its direction.
85       * @param point point
86       * @param distance distance
87       * @return translated point by distance in its direction
88       */
89      // TODO: remove method and replace usage after https://github.com/averbraeck/djutils/issues/126 is solved
90      public static DirectedPoint2d translatePoint(final DirectedPoint2d point, final double distance)
91      {
92          return new DirectedPoint2d(point.x + distance * Math.cos(point.dirZ), point.y + distance * Math.sin(point.dirZ),
93                  point.dirZ);
94      }
95  
96      /**
97       * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
98       * start of the reference line via a number of intermediate offsets at intermediate positions to its final offset value at
99       * the end of the reference line.
100      * @param line reference line.
101      * @param relativeFractions positional fractions for which the offsets have to be generated
102      * @param offsets offsets at the relative positions (positive value is Left, negative value is Right)
103      * @return the PolyLine2d of the line at multi-linearly changing offset of the reference line
104      * @throws OtsGeometryException when this method fails to create the offset line
105      */
106     public static PolyLine2d offsetLine(final PolyLine2d line, final double[] relativeFractions, final double[] offsets)
107             throws OtsGeometryException
108     {
109         Throw.whenNull(relativeFractions, "relativeFraction may not be null");
110         Throw.whenNull(offsets, "offsets may not be null");
111         Throw.when(relativeFractions.length < 2, OtsGeometryException.class, "size of relativeFractions must be >= 2");
112         Throw.when(relativeFractions.length != offsets.length, OtsGeometryException.class,
113                 "size of relativeFractions must be equal to size of offsets");
114         Throw.when(relativeFractions[0] < 0, OtsGeometryException.class, "relativeFractions may not start before 0");
115         Throw.when(relativeFractions[relativeFractions.length - 1] > 1, OtsGeometryException.class,
116                 "relativeFractions may not end beyond 1");
117         List<Double> fractionsList = DoubleStream.of(relativeFractions).boxed().collect(Collectors.toList());
118         List<Double> offsetsList = DoubleStream.of(offsets).boxed().collect(Collectors.toList());
119         if (relativeFractions[0] != 0)
120         {
121             fractionsList.add(0, 0.0);
122             offsetsList.add(0, 0.0);
123         }
124         if (relativeFractions[relativeFractions.length - 1] < 1.0)
125         {
126             fractionsList.add(1.0);
127             offsetsList.add(0.0);
128         }
129         PolyLine2d[] offsetLine = new PolyLine2d[fractionsList.size()];
130         for (int i = 0; i < fractionsList.size(); i++)
131         {
132             offsetLine[i] = line.offsetLine(offsetsList.get(i));
133         }
134         List<Point2d> out = new ArrayList<>();
135         Point2d prevCoordinate = null;
136         final double tooClose = 0.05; // 5 cm
137         for (int i = 0; i < offsetsList.size() - 1; i++)
138         {
139             Throw.when(fractionsList.get(i + 1) <= fractionsList.get(i), OtsGeometryException.class,
140                     "fractions must be in ascending order");
141             PolyLine2d startGeometry = offsetLine[i].extractFractional(fractionsList.get(i), fractionsList.get(i + 1));
142             PolyLine2d endGeometry = offsetLine[i + 1].extractFractional(fractionsList.get(i), fractionsList.get(i + 1));
143             double firstLength = startGeometry.getLength();
144             double secondLength = endGeometry.getLength();
145             int firstIndex = 0;
146             int secondIndex = 0;
147             while (firstIndex < startGeometry.size() && secondIndex < endGeometry.size())
148             {
149                 double firstRatio = firstIndex < startGeometry.size() ? startGeometry.lengthAtIndex(firstIndex) / firstLength
150                         : Double.MAX_VALUE;
151                 double secondRatio = secondIndex < endGeometry.size() ? endGeometry.lengthAtIndex(secondIndex) / secondLength
152                         : Double.MAX_VALUE;
153                 double ratio;
154                 if (firstRatio < secondRatio)
155                 {
156                     ratio = firstRatio;
157                     firstIndex++;
158                 }
159                 else
160                 {
161                     ratio = secondRatio;
162                     secondIndex++;
163                 }
164                 Point2d firstCoordinate = startGeometry.getLocation(ratio * firstLength);
165                 Point2d secondCoordinate = endGeometry.getLocation(ratio * secondLength);
166                 Point2d resultCoordinate = new Point2d((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x,
167                         (1 - ratio) * firstCoordinate.y + ratio * secondCoordinate.y);
168                 if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
169                 {
170                     out.add(resultCoordinate);
171                     prevCoordinate = resultCoordinate;
172                 }
173             }
174         }
175         return new PolyLine2d(0.0, out.toArray(new Point2d[out.size()]));
176     }
177 
178     /**
179      * Compute the 2D intersection of two lines. Both lines are defined by two points (that should be distinct).
180      * @param line1P1X x-coordinate of start point of line 1
181      * @param line1P1Y y-coordinate of start point of line 1
182      * @param line1P2X x-coordinate of end point of line 1
183      * @param line1P2Y y-coordinate of end point of line 1
184      * @param lowLimitLine1 if {@code true}; the intersection may not lie before the start point of line 1
185      * @param highLimitLine1 if {@code true}; the intersection may not lie beyond the end point of line 1
186      * @param line2P1X x-coordinate of start point of line 2
187      * @param line2P1Y y-coordinate of start point of line 2
188      * @param line2P2X x-coordinate of end point of line 2
189      * @param line2P2Y y-coordinate of end point of line 2
190      * @param lowLimitLine2 if {@code true}; the intersection may not lie before the start point of line 2
191      * @param highLimitLine2 if {@code true}; the intersection may not lie beyond the end point of line 2
192      * @param eps tolerance (conservative to find intersections)
193      * @return the intersection of the two lines, or {@code null} if the lines are (almost) parallel, or the intersection point
194      *         lies outside the permitted range
195      * @throws ArithmeticException when any of the parameters is {@code NaN}
196      */
197     @SuppressWarnings("checkstyle:parameternumber")
198     public static Point2d intersectionOfLinesEps(final double line1P1X, final double line1P1Y, final double line1P2X,
199             final double line1P2Y, final boolean lowLimitLine1, final boolean highLimitLine1, final double line2P1X,
200             final double line2P1Y, final double line2P2X, final double line2P2Y, final boolean lowLimitLine2,
201             final boolean highLimitLine2, final double eps)
202     {
203         Throw.when(eps < 0.0, IllegalArgumentException.class, "eps may not be negative");
204         double line1DX = line1P2X - line1P1X;
205         double line1DY = line1P2Y - line1P1Y;
206         double l2p1x = line2P1X - line1P1X;
207         double l2p1y = line2P1Y - line1P1Y;
208         double l2p2x = line2P2X - line1P1X;
209         double l2p2y = line2P2Y - line1P1Y;
210         double denominator = (l2p2y - l2p1y) * line1DX - (l2p2x - l2p1x) * line1DY;
211         Throw.whenNaN(denominator, "none of the parameters may be NaN");
212         if (Math.abs(denominator) < eps)
213         {
214             return null; // lines are parallel (they might even be on top of each other, but we don't check that)
215         }
216         double uA = ((l2p2x - l2p1x) * (-l2p1y) - (l2p2y - l2p1y) * (-l2p1x)) / denominator;
217         // System.out.println("uA is " + uA);
218         if (uA < -eps && lowLimitLine1 || uA > 1.0 + eps && highLimitLine1)
219         {
220             return null; // intersection outside line 1
221         }
222         double uB = (line1DY * l2p1x - line1DX * l2p1y) / denominator;
223         // System.out.println("uB is " + uB);
224         if (uB < -eps && lowLimitLine2 || uB > 1.0 + eps && highLimitLine2)
225         {
226             return null; // intersection outside line 2
227         }
228         if (Math.abs(uA - 1.0) < eps) // maximize precision
229         {
230             return new Point2d(line1P2X, line1P2Y);
231         }
232         if (Math.abs(uB) < eps)
233         {
234             return new Point2d(line2P1X, line2P1Y);
235         }
236         if (Math.abs(uB - 1.0) < eps)
237         {
238             return new Point2d(line2P2X, line2P2Y);
239         }
240         return new Point2d(line1P1X + uA * line1DX, line1P1Y + uA * line1DY);
241     }
242 
243 }