1   package org.djutils.draw.curve;
2   
3   import java.util.ArrayList;
4   import java.util.HashSet;
5   import java.util.LinkedHashMap;
6   import java.util.List;
7   import java.util.Map;
8   import java.util.NavigableMap;
9   import java.util.Set;
10  import java.util.TreeMap;
11  
12  import org.djutils.draw.Direction3d;
13  import org.djutils.draw.line.PolyLine3d;
14  import org.djutils.draw.point.Point3d;
15  import org.djutils.exceptions.Throw;
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  public interface Flattener3d extends Flattener<Flattener3d, Curve3d, PolyLine3d, Point3d, Direction3d>
28  {
29      
30  
31  
32  
33  
34  
35      PolyLine3d flatten(Curve3d curve);
36  
37      @Override
38      default boolean checkInflectionPoint(final FlattableCurve<Point3d, Direction3d> curve, final double prevT,
39              final double medianT, final double nextT, final Point3d prevPoint, final Point3d nextPoint)
40      {
41          Point3d oneQuarter = curve.getPoint((prevT + medianT) / 2);
42          Direction3d oneQDir = oneQuarter.directionTo(oneQuarter.closestPointOnSegment(prevPoint, nextPoint));
43          Point3d threeQuarter = curve.getPoint((nextT + medianT) / 2);
44          Direction3d threeQDir = threeQuarter.directionTo(threeQuarter.closestPointOnSegment(prevPoint, nextPoint));
45          Double angle = oneQDir.directionDifference(threeQDir);
46          return angle > Math.PI / 2; 
47      }
48  
49      @Override
50      default boolean checkLoopBack(final Direction3d prevDirection, final Direction3d nextDirection)
51      {
52          return prevDirection.directionDifference(nextDirection) > Math.PI / 2;
53      }
54  
55      @Override
56      default boolean checkDirectionError(final Direction3d segmentDirection, final Direction3d curveDirectionAtStart,
57              final Direction3d curveDirectionAtEnd, final double maxDirectionDeviation)
58      {
59          return (segmentDirection.directionDifference(curveDirectionAtStart) > maxDirectionDeviation
60                  || segmentDirection.directionDifference(curveDirectionAtEnd) > maxDirectionDeviation);
61      }
62  
63      
64  
65  
66      class NumSegments implements Flattener3d
67      {
68          
69          private final int numSegments;
70  
71          
72  
73  
74  
75  
76          public NumSegments(final int numSegments)
77          {
78              Throw.when(numSegments < 1, IllegalArgumentException.class, "Number of segments must be at least 1");
79              this.numSegments = numSegments;
80          }
81  
82          @Override
83          public PolyLine3d flatten(final Curve3d curve) throws NullPointerException
84          {
85              Throw.whenNull(curve, "curve");
86              List<Point3d> points = new ArrayList<>(this.numSegments + 1);
87              for (int i = 0; i <= this.numSegments; i++)
88              {
89                  double fraction = ((double) i) / this.numSegments;
90                  points.add(curve.getPoint(fraction));
91              }
92              return new PolyLine3d(points);
93          }
94      }
95  
96      
97  
98  
99      class MaxDeviation implements Flattener3d
100     {
101         
102         private final double maxDeviation;
103 
104         
105 
106 
107 
108 
109 
110         public MaxDeviation(final double maxDeviation)
111         {
112             Throw.whenNaN(maxDeviation, "maxDeviation");
113             Throw.when(maxDeviation <= 0.0, IllegalArgumentException.class, "Maximum deviation must be above 0.0 and finite");
114             this.maxDeviation = maxDeviation;
115         }
116 
117         @Override
118         public PolyLine3d flatten(final Curve3d curve)
119         {
120             Throw.whenNull(curve, "curve");
121             NavigableMap<Double, Point3d> result = new TreeMap<>();
122             loadKnots(result, curve);
123 
124             
125             double prevT = result.firstKey();
126             Point3d prevPoint = result.get(prevT);
127             Map.Entry<Double, Point3d> entry;
128             while ((entry = result.higherEntry(prevT)) != null)
129             {
130                 double nextT = entry.getKey();
131                 Point3d nextPoint = entry.getValue();
132                 double medianT = (prevT + nextT) / 2;
133                 Point3d medianPoint = curve.getPoint(medianT);
134                 
135                 if (checkPositionError(medianPoint, prevPoint, nextPoint, this.maxDeviation))
136                 {
137                     
138                     result.put(medianT, medianPoint);
139                     continue;
140                 }
141                 if (prevPoint.distance(nextPoint) > this.maxDeviation
142                         && checkInflectionPoint(curve, prevT, medianT, nextT, prevPoint, nextPoint))
143                 {
144                     
145                     result.put(medianT, medianPoint);
146                     continue;
147                 }
148                 if (checkLoopBack(curve.getDirection(prevT), curve.getDirection(nextT)))
149                 {
150                     
151                     
152                     result.put(medianT, medianPoint);
153                     continue;
154                 }
155                 prevT = nextT;
156                 prevPoint = nextPoint;
157             }
158             return new PolyLine3d(result.values().iterator());
159         }
160     }
161 
162     
163 
164 
165 
166     class MaxDeviationAndAngle implements Flattener3d
167     {
168         
169         private final double maxDeviation;
170 
171         
172         private final double maxAngle;
173 
174         
175 
176 
177 
178 
179 
180 
181 
182         public MaxDeviationAndAngle(final double maxDeviation, final double maxAngle)
183         {
184             Throw.whenNaN(maxDeviation, "maxDeviation");
185             Throw.whenNaN(maxAngle, "maxAngle");
186             Throw.when(maxDeviation <= 0.0, IllegalArgumentException.class, "Maximum deviation must be above 0.0 and finite");
187             Throw.when(maxAngle <= 0.0, IllegalArgumentException.class, "Maximum angle must be above 0.0");
188             this.maxDeviation = maxDeviation;
189             this.maxAngle = maxAngle;
190         }
191 
192         @Override
193         public PolyLine3d flatten(final Curve3d curve) throws NullPointerException
194         {
195             NavigableMap<Double, Point3d> result = new TreeMap<>();
196             loadKnots(result, curve);
197             Map<Double, Direction3d> directions = new LinkedHashMap<>();
198             Set<Double> knots = new HashSet<>();
199             for (double fraction : result.keySet())
200             {
201                 directions.put(fraction, curve.getDirection(fraction));
202                 knots.add(fraction);
203             }
204 
205             
206             double prevT = result.firstKey();
207             Point3d prevPoint = result.get(prevT);
208             Map.Entry<Double, Point3d> entry;
209             int iterationsAtSinglePoint = 0;
210             while ((entry = result.higherEntry(prevT)) != null)
211             {
212                 double nextT = entry.getKey();
213                 Point3d nextPoint = entry.getValue();
214                 double medianT = (prevT + nextT) / 2;
215                 Point3d medianPoint = curve.getPoint(medianT);
216                 
217                 if (checkPositionError(medianPoint, prevPoint, nextPoint, this.maxDeviation))
218                 {
219                     
220                     result.put(medianT, medianPoint);
221                     directions.put(medianT, curve.getDirection(medianT));
222                     continue;
223                 }
224                 
225                 if (checkDirectionError(prevPoint.directionTo(nextPoint), directions.get(prevT), directions.get(nextT),
226                         this.maxAngle))
227                 {
228                     
229                     result.put(medianT, medianPoint);
230                     directions.put(medianT, curve.getDirection(medianT));
231                     iterationsAtSinglePoint++;
232                     Throw.when(iterationsAtSinglePoint == 50, IllegalArgumentException.class,
233                             "Required a new point 50 times "
234                                     + "around the same point (t=%f). Likely there is an (unreported) knot in the Curve3d.",
235                             medianT);
236                     continue;
237                 }
238                 iterationsAtSinglePoint = 0;
239                 if (prevPoint.distance(nextPoint) > this.maxDeviation
240                         && checkInflectionPoint(curve, prevT, medianT, nextT, prevPoint, nextPoint))
241                 {
242                     
243                     result.put(medianT, medianPoint);
244                     directions.put(medianT, curve.getDirection(medianT));
245                     continue;
246                 }
247                 prevT = nextT;
248                 prevPoint = nextPoint;
249                 if (knots.contains(prevT))
250                 {
251                     directions.put(prevT, curve.getDirection(prevT + Math.ulp(prevT)));
252                 }
253             }
254             return new PolyLine3d(result.values().iterator());
255         }
256     }
257 
258     
259 
260 
261     class MaxAngle implements Flattener3d
262     {
263         
264         private final double maxAngle;
265 
266         
267 
268 
269 
270 
271 
272         public MaxAngle(final double maxAngle)
273         {
274             Throw.whenNaN(maxAngle, "maxAngle");
275             Throw.when(maxAngle <= 0.0, IllegalArgumentException.class, "Maximum angle must be above 0.0");
276             this.maxAngle = maxAngle;
277         }
278 
279         @Override
280         public PolyLine3d flatten(final Curve3d curve)
281         {
282             NavigableMap<Double, Point3d> result = new TreeMap<>();
283             loadKnots(result, curve);
284             Map<Double, Direction3d> directions = new LinkedHashMap<>();
285             directions.put(0.0, curve.getDirection(0.0)); 
286             Set<Double> knots = new HashSet<>();
287             for (double knot : result.keySet())
288             {
289                 if (knot > 0)
290                 {
291                     directions.put(knot, curve.getDirection(knot - Math.ulp(knot)));
292                 }
293                 if (knot != 0.0 && knot != 1.0)
294                 {
295                     knots.add(knot);
296                 }
297             }
298 
299             
300             double prevT = result.firstKey();
301             Point3d prevPoint = result.get(prevT);
302             Map.Entry<Double, Point3d> entry;
303             int iterationsAtSinglePoint = 0;
304             while ((entry = result.higherEntry(prevT)) != null)
305             {
306                 double nextT = entry.getKey();
307                 Point3d nextPoint = entry.getValue();
308                 double medianT = (prevT + nextT) / 2;
309 
310                 
311                 if (checkDirectionError(prevPoint.directionTo(nextPoint), directions.get(prevT), directions.get(nextT),
312                         this.maxAngle))
313                 {
314                     
315                     Point3d medianPoint = curve.getPoint(medianT);
316                     result.put(medianT, medianPoint);
317                     directions.put(medianT, curve.getDirection(medianT));
318                     iterationsAtSinglePoint++;
319                     Throw.when(iterationsAtSinglePoint == 50, IllegalArgumentException.class,
320                             "Required a new point 50 times "
321                                     + "around the same point (t=%f). Likely there is an (unreported) knot in the Curve3d.",
322                             medianT);
323                     continue;
324                 }
325                 iterationsAtSinglePoint = 0;
326                 if (checkInflectionPoint(curve, prevT, medianT, nextT, prevPoint, nextPoint))
327                 {
328                     
329                     Point3d medianPoint = curve.getPoint(medianT);
330                     result.put(medianT, medianPoint);
331                     directions.put(medianT, curve.getDirection(medianT));
332                     continue;
333                 }
334                 prevT = nextT;
335                 prevPoint = nextPoint;
336                 if (knots.contains(prevT))
337                 {
338                     directions.put(prevT, curve.getDirection(prevT + Math.ulp(prevT)));
339                 }
340             }
341             return new PolyLine3d(result.values().iterator());
342         }
343     }
344 
345 }