View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import java.util.ArrayList;
4   import java.util.HashSet;
5   import java.util.List;
6   import java.util.Set;
7   
8   import com.vividsolutions.jts.geom.Coordinate;
9   import com.vividsolutions.jts.geom.Geometry;
10  import com.vividsolutions.jts.linearref.LengthIndexedLine;
11  import com.vividsolutions.jts.operation.buffer.BufferParameters;
12  
13  /**
14   * <p>
15   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
16   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
17   * <p>
18   * $LastChangedDate: 2015-07-16 10:20:53 +0200 (Thu, 16 Jul 2015) $, @version $Revision: 1124 $, by $Author: pknoppers $,
19   * initial version Jul 22, 2015 <br>
20   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
21   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
22   */
23  public final class OTSBuffering
24  {
25      /** Precision of buffer operations. */
26      private static final int QUADRANTSEGMENTS = 16;
27  
28      /**
29       * 
30       */
31      private OTSBuffering()
32      {
33          // cannot be instantiated.
34      }
35  
36      /**
37       * normalize an angle between 0 and 2 * PI.
38       * @param angle original angle.
39       * @return angle between 0 and 2 * PI.
40       */
41      private static double norm(final double angle)
42      {
43          double normalized = angle % (2 * Math.PI);
44          if (normalized < 0.0)
45          {
46              normalized += 2 * Math.PI;
47          }
48          return normalized;
49      }
50  
51      /**
52       * @param c1 first coordinate
53       * @param c2 second coordinate
54       * @return the normalized angle of the line between c1 and c2
55       */
56      private static double angle(final Coordinate c1, final Coordinate c2)
57      {
58          return norm(Math.atan2(c2.y - c1.y, c2.x - c1.x));
59      }
60  
61      /**
62       * Generate a Geometry that has a fixed offset from a reference Geometry.
63       * @param referenceLine Geometry; the reference line
64       * @param offset double; offset distance from the reference line; positive is LEFT, negative is RIGHT
65       * @return Geometry; the Geometry of a line that has the specified offset from the reference line
66       * @throws OTSGeometryException on failure
67       */
68      @SuppressWarnings("checkstyle:methodlength")
69      public static OTSLine3D offsetGeometry(final OTSLine3D referenceLine, final double offset) throws OTSGeometryException
70      {
71          Coordinate[] referenceCoordinates = referenceLine.getCoordinates();
72          // printCoordinates("reference", referenceCoordinates);
73          double bufferOffset = Math.abs(offset);
74          final double precision = 0.00001;
75          if (bufferOffset < precision) // if this is not added, and offset = 1E-16: CRASH
76          {
77              // return a copy of the reference line
78              return new OTSLine3D(referenceCoordinates);
79          }
80          Geometry geometryLine = referenceLine.getLineString();
81          Coordinate[] bufferCoordinates =
82              geometryLine.buffer(bufferOffset, QUADRANTSEGMENTS, BufferParameters.CAP_FLAT).getCoordinates();
83          // find the coordinate indices closest to the start point and end point, at a distance of approximately the
84          // offset
85          Coordinate sC0 = referenceCoordinates[0];
86          Coordinate sC1 = referenceCoordinates[1];
87          Coordinate eCm1 = referenceCoordinates[referenceCoordinates.length - 1];
88          Coordinate eCm2 = referenceCoordinates[referenceCoordinates.length - 2];
89          Set<Integer> startIndexSet = new HashSet<>();
90          Set<Coordinate> startSet = new HashSet<Coordinate>();
91          Set<Integer> endIndexSet = new HashSet<>();
92          Set<Coordinate> endSet = new HashSet<Coordinate>();
93          for (int i = 0; i < bufferCoordinates.length; i++) // Note: the last coordinate = the first coordinate
94          {
95              Coordinate c = bufferCoordinates[i];
96              if (Math.abs(c.distance(sC0) - bufferOffset) < bufferOffset * precision && !startSet.contains(c))
97              {
98                  startIndexSet.add(i);
99                  startSet.add(c);
100             }
101             if (Math.abs(c.distance(eCm1) - bufferOffset) < bufferOffset * precision && !endSet.contains(c))
102             {
103                 endIndexSet.add(i);
104                 endSet.add(c);
105             }
106         }
107         if (startIndexSet.size() != 2)
108         {
109             throw new OTSGeometryException("offsetGeometry: startIndexSet.size() = " + startIndexSet.size());
110         }
111         if (endIndexSet.size() != 2)
112         {
113             throw new OTSGeometryException("offsetGeometry: endIndexSet.size() = " + endIndexSet.size());
114         }
115 
116         // which point(s) are in the right direction of the start / end?
117         int startIndex = -1;
118         int endIndex = -1;
119         double expectedStartAngle = norm(angle(sC0, sC1) + Math.signum(offset) * Math.PI / 2.0);
120         double expectedEndAngle = norm(angle(eCm2, eCm1) + Math.signum(offset) * Math.PI / 2.0);
121         for (int ic : startIndexSet)
122         {
123             if (norm(expectedStartAngle - angle(sC0, bufferCoordinates[ic])) < Math.PI / 4.0
124                 || norm(angle(sC0, bufferCoordinates[ic]) - expectedStartAngle) < Math.PI / 4.0)
125             {
126                 startIndex = ic;
127             }
128         }
129         for (int ic : endIndexSet)
130         {
131             if (norm(expectedEndAngle - angle(eCm1, bufferCoordinates[ic])) < Math.PI / 4.0
132                 || norm(angle(eCm1, bufferCoordinates[ic]) - expectedEndAngle) < Math.PI / 4.0)
133             {
134                 endIndex = ic;
135             }
136         }
137         if (startIndex == -1 || endIndex == -1)
138         {
139             throw new OTSGeometryException("offsetGeometry: could not find startIndex or endIndex");
140         }
141         startIndexSet.remove(startIndex);
142         endIndexSet.remove(endIndex);
143 
144         // Make two lists, one in each direction; start at "start" and end at "end".
145         List<Coordinate> coordinateList1 = new ArrayList<>();
146         List<Coordinate> coordinateList2 = new ArrayList<>();
147         boolean use1 = true;
148         boolean use2 = true;
149 
150         int i = startIndex;
151         while (i != endIndex)
152         {
153             if (!coordinateList1.contains(bufferCoordinates[i]))
154             {
155                 coordinateList1.add(bufferCoordinates[i]);
156             }
157             i = (i + 1) % bufferCoordinates.length;
158             if (startIndexSet.contains(i) || endIndexSet.contains(i))
159             {
160                 use1 = false;
161             }
162         }
163         if (!coordinateList1.contains(bufferCoordinates[endIndex]))
164         {
165             coordinateList1.add(bufferCoordinates[endIndex]);
166         }
167 
168         i = startIndex;
169         while (i != endIndex)
170         {
171             if (!coordinateList2.contains(bufferCoordinates[i]))
172             {
173                 coordinateList2.add(bufferCoordinates[i]);
174             }
175             i = (i == 0) ? bufferCoordinates.length - 1 : i - 1;
176             if (startIndexSet.contains(i) || endIndexSet.contains(i))
177             {
178                 use2 = false;
179             }
180         }
181         if (!coordinateList2.contains(bufferCoordinates[endIndex]))
182         {
183             coordinateList2.add(bufferCoordinates[endIndex]);
184         }
185 
186         if (!use1 && !use2)
187         {
188             throw new OTSGeometryException("offsetGeometry: could not find path from start to end for offset");
189         }
190         if (use1 && use2)
191         {
192             throw new OTSGeometryException("offsetGeometry: Both paths from start to end for offset were found to be ok");
193         }
194         Coordinate[] coordinates;
195         if (use1)
196         {
197             coordinates = new Coordinate[coordinateList1.size()];
198             coordinateList1.toArray(coordinates);
199         }
200         else
201         {
202             coordinates = new Coordinate[coordinateList2.size()];
203             coordinateList2.toArray(coordinates);
204         }
205         return new OTSLine3D(coordinates);
206     }
207 
208     /**
209      * Create the Geometry of a line at offset from a reference line. The offset may change linearly from its initial value at
210      * the start of the reference line to its final offset value at the end of the reference line.
211      * @param referenceLine Geometry; the Geometry of the reference line
212      * @param offsetAtStart double; offset at the start of the reference line (positive value is Left, negative value is Right)
213      * @param offsetAtEnd double; offset at the end of the reference line (positive value is Left, negative value is Right)
214      * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
215      * @throws OTSGeometryException when this method fails to create the offset line
216      */
217     public static OTSLine3D offsetLine(final OTSLine3D referenceLine, final double offsetAtStart, final double offsetAtEnd)
218         throws OTSGeometryException
219     {
220         OTSLine3D offsetLineAtStart = offsetGeometry(referenceLine, offsetAtStart);
221         if (offsetAtStart == offsetAtEnd)
222         {
223             return offsetLineAtStart; // offset does not change
224         }
225         OTSLine3D offsetLineAtEnd = offsetGeometry(referenceLine, offsetAtEnd);
226         Geometry startGeometry = offsetLineAtStart.getLineString();
227         Geometry endGeometry = offsetLineAtEnd.getLineString();
228         LengthIndexedLine first = new LengthIndexedLine(startGeometry);
229         double firstLength = startGeometry.getLength();
230         LengthIndexedLine second = new LengthIndexedLine(endGeometry);
231         double secondLength = endGeometry.getLength();
232         ArrayList<Coordinate> out = new ArrayList<Coordinate>();
233         Coordinate[] firstCoordinates = startGeometry.getCoordinates();
234         Coordinate[] secondCoordinates = endGeometry.getCoordinates();
235         int firstIndex = 0;
236         int secondIndex = 0;
237         Coordinate prevCoordinate = null;
238         final double tooClose = 0.05; // 5 cm
239         while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
240         {
241             double firstRatio =
242                 firstIndex < firstCoordinates.length ? first.indexOf(firstCoordinates[firstIndex]) / firstLength
243                     : Double.MAX_VALUE;
244             double secondRatio =
245                 secondIndex < secondCoordinates.length ? second.indexOf(secondCoordinates[secondIndex]) / secondLength
246                     : Double.MAX_VALUE;
247             double ratio;
248             if (firstRatio < secondRatio)
249             {
250                 ratio = firstRatio;
251                 firstIndex++;
252             }
253             else
254             {
255                 ratio = secondRatio;
256                 secondIndex++;
257             }
258             Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
259             Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
260             Coordinate resultCoordinate =
261                 new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x, (1 - ratio) * firstCoordinate.y
262                     + ratio * secondCoordinate.y);
263             if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
264             {
265                 out.add(resultCoordinate);
266                 prevCoordinate = resultCoordinate;
267             }
268         }
269         Coordinate[] resultCoordinates = new Coordinate[out.size()];
270         for (int index = 0; index < out.size(); index++)
271         {
272             resultCoordinates[index] = out.get(index);
273         }
274         return new OTSLine3D(resultCoordinates);
275     }
276 }