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 }