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  
16  
17  
18  
19  
20  
21  
22  
23  
24  @FunctionalInterface
25  public interface Flattener
26  {
27  
28      
29  
30  
31  
32  
33      PolyLine2d flatten(FlattableLine line);
34  
35      
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46      public static class NumSegments implements Flattener
47      {
48          
49          private final int numSegments;
50  
51          
52  
53  
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  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85      public static class MaxDeviation implements Flattener
86      {
87          
88          private final double maxDeviation;
89  
90          
91  
92  
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             
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                 
120                 Point2d projectedPoint = medianPoint.closestPointOnSegment(prevPoint, nextPoint);
121                 double errorPosition = medianPoint.distance(projectedPoint);
122                 if (errorPosition >= this.maxDeviation)
123                 {
124                     
125                     result.put(medianT, medianPoint);
126                     continue;
127                 }
128 
129                 if (prevPoint.distance(nextPoint) > this.maxDeviation)
130                 {
131                     
132                     
133                     
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                         
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 
156 
157 
158 
159 
160 
161 
162 
163 
164 
165     public static class MaxDeviationAndAngle implements Flattener
166     {
167         
168         private final double maxDeviation;
169 
170         
171         private final double maxAngle;
172 
173         
174 
175 
176 
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             
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                 
209                 Point2d projectedPoint = medianPoint.closestPointOnSegment(prevPoint, nextPoint);
210                 double errorPosition = medianPoint.distance(projectedPoint);
211                 if (errorPosition >= this.maxDeviation)
212                 {
213                     
214                     result.put(medianT, medianPoint);
215                     directions.put(medianT, line.getDirection(medianT)); 
216                     continue;
217                 }
218 
219                 
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                     
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                 
244                 
245                 
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                     
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 
268 
269 
270 
271 
272 
273 
274 
275 
276 
277     public static class MaxAngle implements Flattener
278     {
279         
280         private final double maxAngle;
281 
282         
283 
284 
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             
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                 
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                     
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                 
339                 
340                 
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                     
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 }