View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import java.util.ArrayList;
4   import java.util.LinkedHashMap;
5   import java.util.List;
6   import java.util.Map;
7   import java.util.NavigableMap;
8   import java.util.TreeMap;
9   
10  import org.djutils.draw.line.PolyLine2d;
11  import org.djutils.draw.point.Point2d;
12  import org.djutils.exceptions.Throw;
13  
14  /**
15   * Flattens a continuous line in to a polyline.
16   * <p>
17   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
18   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
19   * </p>
20   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
21   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
22   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
23   */
24  @FunctionalInterface
25  public interface Flattener
26  {
27  
28      /**
29       * Flatten continuous line in to a polyline.
30       * @param line line function.
31       * @return flattened line.
32       */
33      PolyLine2d flatten(FlattableLine line);
34  
35      /**
36       * Flattener based on number of segments.
37       * <p>
38       * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
39       * <br>
40       * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
41       * </p>
42       * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
43       * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
44       * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
45       */
46      public static class NumSegments implements Flattener
47      {
48          /** Number of segments. */
49          private final int numSegments;
50  
51          /**
52           * Constructor.
53           * @param numSegments number of segments, must be at least 1.
54           */
55          public NumSegments(final int numSegments)
56          {
57              Throw.when(numSegments < 1, IllegalArgumentException.class, "Number of segments must be at least 1.");
58              this.numSegments = numSegments;
59          }
60  
61          @Override
62          public PolyLine2d flatten(final FlattableLine line)
63          {
64              Throw.whenNull(line, "Line function may not be null.");
65              List<Point2d> points = new ArrayList<>(this.numSegments + 1);
66              for (int i = 0; i <= this.numSegments; i++)
67              {
68                  points.add(line.get(((double) i) / this.numSegments));
69              }
70              return new PolyLine2d(points);
71          }
72      }
73  
74      /**
75       * Flattener based on maximum deviation.
76       * <p>
77       * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
78       * <br>
79       * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
80       * </p>
81       * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
82       * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
83       * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
84       */
85      public static class MaxDeviation implements Flattener
86      {
87          /** Maximum deviation. */
88          private final double maxDeviation;
89  
90          /**
91           * Constructor.
92           * @param maxDeviation maximum deviation, must be above 0.0.
93           */
94          public MaxDeviation(final double maxDeviation)
95          {
96              Throw.when(maxDeviation <= 0.0, IllegalArgumentException.class, "Maximum deviation must be above 0.0.");
97              this.maxDeviation = maxDeviation;
98          }
99  
100         @Override
101         public PolyLine2d flatten(final FlattableLine line)
102         {
103             Throw.whenNull(line, "Line function may not be null.");
104             NavigableMap<Double, Point2d> result = new TreeMap<>();
105             result.put(0.0, line.get(0.0));
106             result.put(1.0, line.get(1.0));
107 
108             // Walk along all point pairs and see if additional points need to be inserted
109             double prevT = result.firstKey();
110             Point2d prevPoint = result.get(prevT);
111             Map.Entry<Double, Point2d> entry;
112             while ((entry = result.higherEntry(prevT)) != null)
113             {
114                 double nextT = entry.getKey();
115                 Point2d nextPoint = entry.getValue();
116                 double medianT = (prevT + nextT) / 2;
117                 Point2d medianPoint = line.get(medianT);
118 
119                 // Check max deviation
120                 Point2d projectedPoint = medianPoint.closestPointOnSegment(prevPoint, nextPoint);
121                 double errorPosition = medianPoint.distance(projectedPoint);
122                 if (errorPosition >= this.maxDeviation)
123                 {
124                     // We need to insert another point
125                     result.put(medianT, medianPoint);
126                     continue;
127                 }
128 
129                 if (prevPoint.distance(nextPoint) > this.maxDeviation)
130                 {
131                     // Check for an inflection point by creating additional points at one quarter and three quarters. If these
132                     // are on opposite sides of the line from prevPoint to nextPoint; there must be an inflection point.
133                     // https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line
134                     Point2d quarter = line.get((prevT + medianT) / 2);
135                     int sign1 = (int) Math.signum((nextPoint.x - prevPoint.x) * (quarter.y - prevPoint.y)
136                             - (nextPoint.y - prevPoint.y) * (quarter.x - prevPoint.x));
137                     Point2d threeQuarter = line.get((nextT + medianT) / 2);
138                     int sign2 = (int) Math.signum((nextPoint.x - prevPoint.x) * (threeQuarter.y - prevPoint.y)
139                             - (nextPoint.y - prevPoint.y) * (threeQuarter.x - prevPoint.x));
140                     if (sign1 != sign2)
141                     {
142                         // There is an inflection point, inserting the halfway point should take care of this
143                         result.put(medianT, medianPoint);
144                         continue;
145                     }
146                 }
147                 prevT = nextT;
148                 prevPoint = nextPoint;
149             }
150             return new PolyLine2d(result.values().iterator());
151         }
152     }
153 
154     /**
155      * Flattener based on maximum deviation and maximum angle.
156      * <p>
157      * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
158      * <br>
159      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
160      * </p>
161      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
162      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
163      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
164      */
165     public static class MaxDeviationAndAngle implements Flattener
166     {
167         /** Maximum deviation. */
168         private final double maxDeviation;
169 
170         /** Maximum angle. */
171         private final double maxAngle;
172 
173         /**
174          * Constructor.
175          * @param maxDeviation maximum deviation, must be above 0.0.
176          * @param maxAngle maximum angle, must be above 0.0.
177          */
178         public MaxDeviationAndAngle(final double maxDeviation, final double maxAngle)
179         {
180             Throw.when(maxDeviation <= 0.0, IllegalArgumentException.class, "Maximum deviation must be above 0.0.");
181             Throw.when(maxAngle <= 0.0, IllegalArgumentException.class, "Maximum angle must be above 0.0.");
182             this.maxDeviation = maxDeviation;
183             this.maxAngle = maxAngle;
184         }
185 
186         @Override
187         public PolyLine2d flatten(final FlattableLine line)
188         {
189             NavigableMap<Double, Point2d> result = new TreeMap<>();
190             result.put(0.0, line.get(0.0));
191             result.put(1.0, line.get(1.0));
192             Map<Double, Double> directions = new LinkedHashMap<>();
193             directions.put(0.0, line.getDirection(0.0));
194             directions.put(1.0, line.getDirection(1.0));
195 
196             // Walk along all point pairs and see if additional points need to be inserted
197             double prevT = result.firstKey();
198             Point2d prevPoint = result.get(prevT);
199             Map.Entry<Double, Point2d> entry;
200             int iterationsAtSinglePoint = 0;
201             while ((entry = result.higherEntry(prevT)) != null)
202             {
203                 double nextT = entry.getKey();
204                 Point2d nextPoint = entry.getValue();
205                 double medianT = (prevT + nextT) / 2;
206                 Point2d medianPoint = line.get(medianT);
207 
208                 // Check max deviation
209                 Point2d projectedPoint = medianPoint.closestPointOnSegment(prevPoint, nextPoint);
210                 double errorPosition = medianPoint.distance(projectedPoint);
211                 if (errorPosition >= this.maxDeviation)
212                 {
213                     // We need to insert another point
214                     result.put(medianT, medianPoint);
215                     directions.put(medianT, line.getDirection(medianT)); // for angle checks
216                     continue;
217                 }
218 
219                 // Check max angle
220                 double angle = prevPoint.directionTo(nextPoint) - directions.get(prevT);
221                 while (angle < -Math.PI)
222                 {
223                     angle += 2 * Math.PI;
224                 }
225                 while (angle > Math.PI)
226                 {
227                     angle -= 2 * Math.PI;
228                 }
229                 if (Math.abs(angle) >= this.maxAngle)
230                 {
231                     // We need to insert another point
232                     result.put(medianT, medianPoint);
233                     directions.put(medianT, line.getDirection(medianT));
234                     iterationsAtSinglePoint++;
235                     Throw.when(iterationsAtSinglePoint == 50, RuntimeException.class,
236                             "Required a new point 50 times at the same point. Likely the reported direction of the point does "
237                                     + "not match further points produced. Consider using the numerical approach in the "
238                                     + "default getDirection(fraction) method of the FlattableLine.");
239                     continue;
240                 }
241                 iterationsAtSinglePoint = 0;
242 
243                 // Check for an inflection point by creating additional points at one quarter and three quarters. If these
244                 // are on opposite sides of the line from prevPoint to nextPoint; there must be an inflection point.
245                 // https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line
246                 Point2d quarter = line.get((prevT + medianT) / 2);
247                 int sign1 = (int) Math.signum((nextPoint.x - prevPoint.x) * (quarter.y - prevPoint.y)
248                         - (nextPoint.y - prevPoint.y) * (quarter.x - prevPoint.x));
249                 Point2d threeQuarter = line.get((nextT + medianT) / 2);
250                 int sign2 = (int) Math.signum((nextPoint.x - prevPoint.x) * (threeQuarter.y - prevPoint.y)
251                         - (nextPoint.y - prevPoint.y) * (threeQuarter.x - prevPoint.x));
252                 if (sign1 != sign2)
253                 {
254                     // There is an inflection point, inserting the halfway point should take care of this
255                     result.put(medianT, medianPoint);
256                     directions.put(medianT, line.getDirection(medianT));
257                     continue;
258                 }
259                 prevT = nextT;
260                 prevPoint = nextPoint;
261             }
262             return new PolyLine2d(result.values().iterator());
263         }
264     }
265 
266     /**
267      * Flattener based on maximum angle.
268      * <p>
269      * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
270      * <br>
271      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
272      * </p>
273      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
274      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
275      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
276      */
277     public static class MaxAngle implements Flattener
278     {
279         /** Maximum angle. */
280         private final double maxAngle;
281 
282         /**
283          * Constructor.
284          * @param maxAngle maximum angle.
285          */
286         public MaxAngle(final double maxAngle)
287         {
288             Throw.when(maxAngle <= 0.0, IllegalArgumentException.class, "Maximum angle must be above 0.0.");
289             this.maxAngle = maxAngle;
290         }
291 
292         @Override
293         public PolyLine2d flatten(final FlattableLine line)
294         {
295             NavigableMap<Double, Point2d> result = new TreeMap<>();
296             result.put(0.0, line.get(0.0));
297             result.put(1.0, line.get(1.0));
298             Map<Double, Double> directions = new LinkedHashMap<>();
299             directions.put(0.0, line.getDirection(0.0));
300             directions.put(1.0, line.getDirection(1.0));
301 
302             // Walk along all point pairs and see if additional points need to be inserted
303             double prevT = result.firstKey();
304             Point2d prevPoint = result.get(prevT);
305             Map.Entry<Double, Point2d> entry;
306             int iterationsAtSinglePoint = 0;
307             while ((entry = result.higherEntry(prevT)) != null)
308             {
309                 double nextT = entry.getKey();
310                 Point2d nextPoint = entry.getValue();
311                 double medianT = (prevT + nextT) / 2;
312 
313                 // Check max angle
314                 double angle = prevPoint.directionTo(nextPoint) - directions.get(prevT);
315                 while (angle < -Math.PI)
316                 {
317                     angle += 2 * Math.PI;
318                 }
319                 while (angle > Math.PI)
320                 {
321                     angle -= 2 * Math.PI;
322                 }
323                 if (Math.abs(angle) >= this.maxAngle)
324                 {
325                     // We need to insert another point
326                     Point2d medianPoint = line.get(medianT);
327                     result.put(medianT, medianPoint);
328                     directions.put(medianT, line.getDirection(medianT));
329                     iterationsAtSinglePoint++;
330                     Throw.when(iterationsAtSinglePoint == 50, RuntimeException.class,
331                             "Required a new point 50 times at the same point. Likely the reported direction of the point does "
332                                     + "not match further points produced. Consider using the numerical approach in the "
333                                     + "default getDirection(fraction) method of the FlattableLine.");
334                     continue;
335                 }
336                 iterationsAtSinglePoint = 0;
337 
338                 // Check for an inflection point by creating additional points at one quarter and three quarters. If these
339                 // are on opposite sides of the line from prevPoint to nextPoint; there must be an inflection point.
340                 // https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line
341                 Point2d quarter = line.get((prevT + medianT) / 2);
342                 int sign1 = (int) Math.signum((nextPoint.x - prevPoint.x) * (quarter.y - prevPoint.y)
343                         - (nextPoint.y - prevPoint.y) * (quarter.x - prevPoint.x));
344                 Point2d threeQuarter = line.get((nextT + medianT) / 2);
345                 int sign2 = (int) Math.signum((nextPoint.x - prevPoint.x) * (threeQuarter.y - prevPoint.y)
346                         - (nextPoint.y - prevPoint.y) * (threeQuarter.x - prevPoint.x));
347                 if (sign1 != sign2)
348                 {
349                     // There is an inflection point, inserting the halfway point should take care of this
350                     Point2d medianPoint = line.get(medianT);
351                     result.put(medianT, medianPoint);
352                     directions.put(medianT, line.getDirection(medianT));
353                     continue;
354                 }
355                 prevT = nextT;
356                 prevPoint = nextPoint;
357             }
358             return new PolyLine2d(result.values().iterator());
359         }
360     }
361 
362 }