View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import java.util.ArrayList;
4   import java.util.List;
5   import java.util.Locale;
6   
7   /**
8    * Peter Knoppers' attempt to implement offsetLine.
9    * <p>
10   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
11   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
12   * <p>
13   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Dec 1, 2015 <br>
14   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
15   */
16  public final class OTSOffsetLinePK
17  {
18      /** This class should never be instantiated. */
19      private OTSOffsetLinePK()
20      {
21          // Cannot be instantiated.
22      }
23      
24      /** Debugging flag. */
25      static boolean debugOffsetLine = false;
26  
27      /** Precision of approximation of arcs in the offsetLine method. */
28      static double circlePrecision = 0.001;
29  
30      /** Noise in the reference line less than this value is always filtered. */
31      static double offsetMinimumFilterValue = 0.001;
32  
33      /** Noise in the reference line greater than this value is never filtered. */
34      static double offsetMaximumFilterValue = 0.1;
35  
36      /**
37       * Noise in the reference line less than <cite>offset / offsetFilterRatio</cite> is filtered except when the resulting value
38       * exceeds <cite>offsetMaximumFilterValue</cite>.
39       */
40      static double offsetFilterRatio = 10;
41  
42      /**
43       * Construct an offset line.
44       * @param referenceLine OTSLine3D; the reference line
45       * @param offset double; the offset; positive values indicate left of the reference line, negative values indicate right of
46       *            the reference line
47       * @return OTSLine3D; a line at the specified offset from the reference line
48       * @throws OTSGeometryException when this method runs into major trouble and cannot produce a decent result
49       */
50      public static OTSLine3D offsetLine(final OTSLine3D referenceLine, final double offset) throws OTSGeometryException
51      {
52          // if (referenceLine.size() > 1 && referenceLine.getFirst().horizontalDistanceSI(new OTSPoint3D(-200.376, -111.999)) <
53          // 0.1
54          // && referenceLine.get(1).horizontalDistanceSI(new OTSPoint3D(-204.098, -100.180)) < 0.1 && Math.abs(offset) > 1)
55  
56          // if (referenceLine.size() > 1 && referenceLine.getFirst().horizontalDistanceSI(new OTSPoint3D(-177.580, -169.726)) <
57          // 0.1
58          // && referenceLine.get(1).horizontalDistanceSI(new OTSPoint3D(-179.028, -166.084)) < 0.1 && Math.abs(offset) > 1
59          // && referenceLine.size() == 0)
60          // {
61          // debugOffsetLine = true;
62          // for (int i = 0; i < referenceLine.size(); i++)
63          // {
64          // System.out.println(String.format(
65          // Locale.US,
66          // "point %2d: %20s,%20s%s",
67          // i,
68          // referenceLine.get(i).x,
69          // referenceLine.get(i).y,
70          // (i == 0 ? "" : " ("
71          // + Math.toDegrees(Math.atan2(referenceLine.get(i).y - referenceLine.get(i - 1).y,
72          // referenceLine.get(i).x - referenceLine.get(i - 1).x)) + ")")));
73          // }
74          // System.out.println("# offset is " + offset);
75          // System.out.println(OTSGeometry.printCoordinates("#reference:\nc0,0,0\n#", referenceLine, "\n    "));
76          // }
77          // else
78          // {
79          // debugOffsetLine = false;
80          // }
81          double bufferOffset = Math.abs(offset);
82          final double precision = 0.00001;
83          if (bufferOffset < precision)
84          {
85              return referenceLine; // "OTSLine3D" is immutable (except for some cached fields); so we can safely return it.
86          }
87  
88          OTSLine3D filteredReferenceLine =
89                  referenceLine.noiseFilteredLine(Math.max(offsetMinimumFilterValue,
90                          Math.min(bufferOffset / 10, offsetMaximumFilterValue)));
91          List<OTSPoint3D> tempPoints = new ArrayList<>();
92          // Make good use of the fact that an OTSLine3D cannot have consecutive duplicate points and has > 1 points
93          OTSPoint3D prevPoint = filteredReferenceLine.get(0);
94          Double prevAngle = null;
95          for (int index = 0; index < filteredReferenceLine.size() - 1; index++)
96          {
97              OTSPoint3D nextPoint = filteredReferenceLine.get(index + 1);
98              double angle = Math.atan2(nextPoint.y - prevPoint.y, nextPoint.x - prevPoint.x);
99              if (debugOffsetLine)
100             {
101                 System.out.println("#reference segment " + prevPoint + " to " + nextPoint + " angle " + Math.toDegrees(angle));
102             }
103             OTSPoint3D segmentFrom =
104                     new OTSPoint3D(prevPoint.x - Math.sin(angle) * offset, prevPoint.y + Math.cos(angle) * offset);
105             OTSPoint3D segmentTo =
106                     new OTSPoint3D(nextPoint.x - Math.sin(angle) * offset, nextPoint.y + Math.cos(angle) * offset);
107             boolean addSegment = true;
108             if (index > 0)
109             {
110                 double deltaAngle = angle - prevAngle;
111                 if (Math.abs(deltaAngle) > Math.PI)
112                 {
113                     deltaAngle -= Math.signum(deltaAngle) * 2 * Math.PI;
114                 }
115                 if (deltaAngle * offset <= 0)
116                 {
117                     // Outside of curve of reference line
118                     // Approximate an arc using straight segments.
119                     // Determine how many segments are needed.
120                     int numSegments = 1;
121                     if (Math.abs(deltaAngle) > Math.PI / 2)
122                     {
123                         numSegments = 2;
124                     }
125                     while (true)
126                     {
127                         double maxError = bufferOffset * (1 - Math.abs(Math.cos(deltaAngle / numSegments / 2)));
128                         if (maxError < circlePrecision)
129                         {
130                             break; // required precision reached
131                         }
132                         numSegments *= 2;
133                     }
134                     OTSPoint3D prevArcPoint = tempPoints.get(tempPoints.size() - 1);
135                     // Generate the intermediate points
136                     for (int additionalPoint = 1; additionalPoint < numSegments; additionalPoint++)
137                     {
138                         double intermediateAngle =
139                                 (additionalPoint * angle + (numSegments - additionalPoint) * prevAngle) / numSegments;
140                         if (prevAngle * angle < 0 && Math.abs(prevAngle) > Math.PI / 2 && Math.abs(angle) > Math.PI / 2)
141                         {
142                             intermediateAngle += Math.PI;
143                         }
144                         OTSPoint3D intermediatePoint =
145                                 new OTSPoint3D(prevPoint.x - Math.sin(intermediateAngle) * offset, prevPoint.y
146                                         + Math.cos(intermediateAngle) * offset);
147                         // Find any intersection points of the new segment and all previous segments
148                         OTSPoint3D prevSegFrom = null;
149                         int stopAt = tempPoints.size();
150                         for (int i = 0; i < stopAt; i++)
151                         {
152                             OTSPoint3D prevSegTo = tempPoints.get(i);
153                             if (null != prevSegFrom)
154                             {
155                                 OTSPoint3D prevSegIntersection =
156                                         OTSPoint3D.intersectionOfLineSegments(prevArcPoint, intermediatePoint, prevSegFrom,
157                                                 prevSegTo);
158                                 if (null != prevSegIntersection
159                                         && prevSegIntersection.horizontalDistanceSI(prevArcPoint) > circlePrecision
160                                         && prevSegIntersection.horizontalDistanceSI(prevSegFrom) > circlePrecision
161                                         && prevSegIntersection.horizontalDistanceSI(prevSegTo) > circlePrecision)
162                                 {
163                                     if (debugOffsetLine)
164                                     {
165                                         System.out.println("#inserting intersection in arc segment " + prevSegIntersection);
166                                     }
167                                     tempPoints.add(prevSegIntersection);
168                                 }
169                             }
170                             prevSegFrom = prevSegTo;
171                         }
172                         OTSPoint3D nextSegmentIntersection =
173                                 OTSPoint3D.intersectionOfLineSegments(prevSegFrom, intermediatePoint, segmentFrom, segmentTo);
174                         if (null != nextSegmentIntersection)
175                         {
176                             if (debugOffsetLine)
177                             {
178                                 System.out.println("#inserting intersection of arc segment with next segment "
179                                         + nextSegmentIntersection);
180                             }
181                             tempPoints.add(nextSegmentIntersection);
182                         }
183                         if (debugOffsetLine)
184                         {
185                             System.out.println("#inserting arc point " + intermediatePoint + " for angle "
186                                     + Math.toDegrees(intermediateAngle));
187                         }
188                         tempPoints.add(intermediatePoint);
189                         prevArcPoint = intermediatePoint;
190                     }
191                 }
192                 // Inside of curve of reference line.
193                 // Add the intersection point of each previous segment and the next segment
194                 OTSPoint3D pPoint = null;
195                 for (int i = 0; i < tempPoints.size(); i++)
196                 {
197                     OTSPoint3D p = tempPoints.get(i);
198                     if (null != pPoint)
199                     {
200                         double pAngle = Math.atan2(p.y - pPoint.y, p.x - pPoint.x);
201                         double angleDifference = angle - pAngle;
202                         if (Math.abs(angleDifference) > Math.PI)
203                         {
204                             angleDifference += Math.signum(angleDifference) * 2 * Math.PI;
205                         }
206                         if (debugOffsetLine)
207                         {
208                             System.out.println("#preceding segment " + pPoint + " to " + p + ", next segment " + segmentFrom
209                                     + " to " + segmentTo + " angleDifference " + Math.toDegrees(angleDifference));
210                         }
211                         if (Math.abs(angleDifference) > 0)// 0.01)
212                         {
213                             OTSPoint3D intersection = OTSPoint3D.intersectionOfLineSegments(pPoint, p, segmentFrom, segmentTo);
214                             if (null != intersection)
215                             {
216                                 if (tempPoints.size() - 1 == i)
217                                 {
218                                     if (debugOffsetLine)
219                                     {
220                                         System.out.println("#Replacing last point of preceding segment and "
221                                                 + "first point of next segment by their intersection " + intersection);
222                                     }
223                                     tempPoints.remove(tempPoints.size() - 1);
224                                     segmentFrom = intersection;
225                                 }
226                                 else
227                                 {
228                                     if (debugOffsetLine)
229                                     {
230                                         System.out.println("#Adding intersection of preceding segment and " + "next segment "
231                                                 + intersection);
232                                     }
233                                     tempPoints.add(intersection);
234                                 }
235                                 // tempPoints.set(tempPoints.size() - 1, intermediatePoint);
236                             }
237                         }
238                         else
239                         {
240                             if (debugOffsetLine)
241                             {
242                                 System.out.println("#Not adding intersection of preceding segment and this segment "
243                                         + "(angle too small)");
244                             }
245                             if (i == tempPoints.size() - 1)
246                             {
247                                 if (debugOffsetLine)
248                                 {
249                                     System.out.println("#Not adding segment");
250                                 }
251                                 addSegment = false;
252                             }
253                         }
254                     }
255                     pPoint = p;
256                 }
257             }
258             if (addSegment)
259             {
260                 if (debugOffsetLine)
261                 {
262                     System.out.println("#Adding segmentFrom " + segmentFrom);
263                 }
264                 tempPoints.add(segmentFrom);
265                 if (debugOffsetLine)
266                 {
267                     System.out.println("#Adding segmentTo " + segmentTo);
268                 }
269                 tempPoints.add(segmentTo);
270                 prevPoint = nextPoint;
271                 prevAngle = angle;
272             }
273         }
274         if (debugOffsetLine)
275         {
276             System.out.println("# before cleanup: \nc1,0,0\n");
277             if (tempPoints.size() > 0)
278             {
279                 OTSPoint3D p = tempPoints.get(0);
280                 System.out.println(String.format(Locale.US, "M %.3f,%.3f", p.x, p.y));
281                 for (int i = 1; i < tempPoints.size(); i++)
282                 {
283                     p = tempPoints.get(i);
284                     System.out.println(String.format(Locale.US, "L %.3f,%.3f", p.x, p.y));
285                 }
286             }
287         }
288         // Remove points that are closer than the specified offset
289         for (int index = 1; index < tempPoints.size() - 1; index++)
290         {
291             OTSPoint3D checkPoint = tempPoints.get(index);
292             prevPoint = null;
293             boolean tooClose = false;
294             boolean somewhereAtCorrectDistance = false;
295             for (int i = 0; i < filteredReferenceLine.size(); i++)
296             {
297                 OTSPoint3D p = filteredReferenceLine.get(i);
298                 if (null != prevPoint)
299                 {
300                     OTSPoint3D closestPoint = checkPoint.closestPointOnSegment(prevPoint, p);
301                     double distance = closestPoint.horizontalDistanceSI(checkPoint);
302                     if (distance < bufferOffset - circlePrecision)
303                     {
304                         if (debugOffsetLine)
305                         {
306                             System.out.print("#point " + checkPoint + " inside buffer (distance is " + distance + ") ");
307                         }
308                         tooClose = true;
309                         break;
310                     }
311                     else if (distance < bufferOffset + precision)
312                     {
313                         somewhereAtCorrectDistance = true;
314                     }
315                 }
316                 prevPoint = p;
317             }
318             if (tooClose || !somewhereAtCorrectDistance)
319             {
320                 if (debugOffsetLine)
321                 {
322                     System.out.println("#Removing " + checkPoint);
323                 }
324                 tempPoints.remove(index);
325                 index--;
326             }
327         }
328         if (debugOffsetLine)
329         {
330             System.out.println("#after cleanup " + tempPoints.size() + " points left");
331         }
332         // Fix the z-coordinate of all points that were added as intersections of segments.
333         for (int index = 0; index < tempPoints.size(); index++)
334         {
335             OTSPoint3D p = tempPoints.get(index);
336             if (Double.isNaN(p.z))
337             {
338                 tempPoints.set(index, new OTSPoint3D(p.x, p.y, 0));
339             }
340         }
341         try
342         {
343             return OTSLine3D.createAndCleanOTSLine3D(tempPoints);
344         }
345         catch (OTSGeometryException exception)
346         {
347             exception.printStackTrace();
348         }
349         return null;
350     }
351 }