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.function.ContinuousPiecewiseLinearFunction;
13 import org.djutils.draw.line.PolyLine2d;
14 import org.djutils.draw.point.Point2d;
15 import org.djutils.exceptions.Throw;
16 import org.djutils.math.AngleUtil;
17
18
19
20
21
22
23
24
25
26
27
28 public interface OffsetFlattener2d extends Flattener<Flattener2d, Curve2d, PolyLine2d, Point2d, Double>
29 {
30
31
32
33
34
35
36 PolyLine2d flatten(OffsetCurve2d curve, ContinuousPiecewiseLinearFunction of);
37
38
39
40
41
42
43
44
45
46
47
48 default void loadKnot(final NavigableMap<Double, Point2d> map, final double knot, final OffsetCurve2d curve,
49 final ContinuousPiecewiseLinearFunction of)
50 {
51 Throw.when(knot < 0.0 || knot > 1.0, IllegalArgumentException.class, "Knots must all be between 0.0 and 1.0, (got %f)",
52 knot);
53 double t = curve.getT(knot * curve.getLength());
54 if (map.containsKey(t))
55 {
56 return;
57 }
58 map.put(t, curve.getPoint(t, of));
59 }
60
61
62
63
64
65
66
67 default void loadKnots(final NavigableMap<Double, Point2d> map, final OffsetCurve2d curve,
68 final ContinuousPiecewiseLinearFunction of)
69 {
70 map.put(0.0, curve.getPoint(0.0, of));
71 Set<Double> knots = curve.getKnots();
72 if (null != knots)
73 {
74 for (double knot : knots)
75 {
76 loadKnot(map, knot, curve, of);
77 }
78 }
79 for (ContinuousPiecewiseLinearFunction.TupleSt knot : of)
80 {
81 loadKnot(map, knot.s(), curve, of);
82 }
83 map.put(1.0, curve.getPoint(1.0, of));
84 }
85
86 @Override
87 default boolean checkLoopBack(final Double prevDirection, final Double nextDirection)
88 {
89 return Math.abs(AngleUtil.normalizeAroundZero(nextDirection - prevDirection)) > Math.PI / 2;
90 }
91
92 @Override
93 default boolean checkDirectionError(final Double segmentDirection, final Double curveDirectionAtStart,
94 final Double curveDirectionAtEnd, final double maxDirectionDeviation)
95 {
96 return (Math.abs(AngleUtil.normalizeAroundZero(segmentDirection - curveDirectionAtStart)) > maxDirectionDeviation)
97 || Math.abs(AngleUtil.normalizeAroundZero(segmentDirection - curveDirectionAtEnd)) >= maxDirectionDeviation;
98 }
99
100
101
102
103
104
105
106 default Flattener.FlattableCurve<Point2d, Double> makeFlattableCurve(final OffsetCurve2d curve,
107 final ContinuousPiecewiseLinearFunction of)
108 {
109 return new Flattener.FlattableCurve<Point2d, Double>()
110 {
111 @Override
112 public Point2d getPoint(final double fraction)
113 {
114 return curve.getPoint(fraction, of);
115 }
116
117 @Override
118 public Double getDirection(final double fraction)
119 {
120 return curve.getDirection(fraction, of);
121 }
122 };
123 }
124
125
126
127
128 class NumSegments implements OffsetFlattener2d
129 {
130
131 private final int numSegments;
132
133
134
135
136
137
138 public NumSegments(final int numSegments)
139 {
140 Throw.when(numSegments < 1, IllegalArgumentException.class, "Number of segments must be at least 1.");
141 this.numSegments = numSegments;
142 }
143
144 @Override
145 public PolyLine2d flatten(final OffsetCurve2d curve, final ContinuousPiecewiseLinearFunction of)
146 {
147 Throw.whenNull(curve, "curve");
148 List<Point2d> points = new ArrayList<>(this.numSegments + 1);
149 for (int i = 0; i <= this.numSegments; i++)
150 {
151 double fraction = ((double) i) / this.numSegments;
152 points.add(curve.getPoint(fraction, of));
153 }
154 return new PolyLine2d(points);
155 }
156 }
157
158
159
160
161 class MaxDeviation implements OffsetFlattener2d
162 {
163
164 private final double maxDeviation;
165
166
167
168
169
170
171
172
173 public MaxDeviation(final double maxDeviation)
174 {
175 Throw.whenNaN(maxDeviation, "maxDeviation");
176 Throw.when(maxDeviation <= 0.0, IllegalArgumentException.class, "Maximum deviation must be above 0.0 and finite");
177 this.maxDeviation = maxDeviation;
178 }
179
180 @Override
181 public PolyLine2d flatten(final OffsetCurve2d curve, final ContinuousPiecewiseLinearFunction of)
182 {
183 Throw.whenNull(curve, "curve");
184 Flattener.FlattableCurve<Point2d, Double> fc = makeFlattableCurve(curve, of);
185 NavigableMap<Double, Point2d> result = new TreeMap<>();
186 loadKnots(result, curve, of);
187
188
189 double prevT = result.firstKey();
190 Point2d prevPoint = result.get(prevT);
191 Map.Entry<Double, Point2d> entry;
192 while ((entry = result.higherEntry(prevT)) != null)
193 {
194 double nextT = entry.getKey();
195 Point2d nextPoint = entry.getValue();
196 double medianT = (prevT + nextT) / 2;
197 Point2d medianPoint = curve.getPoint(medianT, of);
198
199 if (checkPositionError(medianPoint, prevPoint, nextPoint, this.maxDeviation))
200 {
201
202 result.put(medianT, medianPoint);
203 continue;
204 }
205 if (prevPoint.distance(nextPoint) > this.maxDeviation
206 && checkInflectionPoint(fc, prevT, medianT, nextT, prevPoint, nextPoint))
207 {
208
209 result.put(medianT, medianPoint);
210 continue;
211 }
212 if (checkLoopBack(curve.getDirection(prevT), curve.getDirection(nextT)))
213 {
214
215
216 result.put(medianT, medianPoint);
217 continue;
218 }
219 prevT = nextT;
220 prevPoint = nextPoint;
221 }
222 return new PolyLine2d(result.values().iterator());
223 }
224 }
225
226
227
228
229
230 class MaxDeviationAndAngle implements OffsetFlattener2d
231 {
232
233 private final double maxDeviation;
234
235
236 private final double maxAngle;
237
238
239
240
241
242
243
244
245
246 public MaxDeviationAndAngle(final double maxDeviation, final double maxAngle)
247 {
248 Throw.whenNaN(maxDeviation, "maxDeviation");
249 Throw.whenNaN(maxAngle, "maxAngle");
250 Throw.when(maxDeviation <= 0.0, IllegalArgumentException.class, "Maximum deviation must be above 0.0.");
251 Throw.when(maxAngle <= 0.0, IllegalArgumentException.class, "Maximum angle must be above 0.0.");
252 this.maxDeviation = maxDeviation;
253 this.maxAngle = maxAngle;
254 }
255
256 @Override
257 public PolyLine2d flatten(final OffsetCurve2d curve, final ContinuousPiecewiseLinearFunction of)
258 {
259 Throw.whenNull(curve, "curve");
260 Flattener.FlattableCurve<Point2d, Double> fc = makeFlattableCurve(curve, of);
261 NavigableMap<Double, Point2d> result = new TreeMap<>();
262 loadKnots(result, curve, of);
263 Map<Double, Double> directions = new LinkedHashMap<>();
264 directions.put(0.0, curve.getDirection(0.0, of));
265 Set<Double> knots = new HashSet<>();
266 for (double knot : result.keySet())
267 {
268 if (knot > 0)
269 {
270 directions.put(knot, curve.getDirection(knot - Math.ulp(knot), of));
271 }
272 if (knot != 0.0 && knot != 1.0)
273 {
274 knots.add(knot);
275 }
276 }
277
278
279 double prevT = result.firstKey();
280 Point2d prevPoint = result.get(prevT);
281 Map.Entry<Double, Point2d> entry;
282 int iterationsAtSinglePoint = 0;
283 while ((entry = result.higherEntry(prevT)) != null)
284 {
285 double nextT = entry.getKey();
286 Point2d nextPoint = entry.getValue();
287 double medianT = (prevT + nextT) / 2;
288 Point2d medianPoint = curve.getPoint(medianT, of);
289
290 if (checkPositionError(medianPoint, prevPoint, nextPoint, this.maxDeviation))
291 {
292
293 result.put(medianT, medianPoint);
294 directions.put(medianT, curve.getDirection(medianT, of));
295 continue;
296 }
297
298 if (checkDirectionError(prevPoint.directionTo(nextPoint), directions.get(prevT), directions.get(nextT),
299 this.maxAngle))
300 {
301
302 result.put(medianT, medianPoint);
303 directions.put(medianT, curve.getDirection(medianT, of));
304 iterationsAtSinglePoint++;
305 Throw.when(iterationsAtSinglePoint == 50, IllegalArgumentException.class, "Required a new point 50 times "
306 + "around the same point (t=%f). Likely there is an (unreported) knot in the OffsetCurve2d.",
307 medianT);
308 continue;
309 }
310 iterationsAtSinglePoint = 0;
311 if (prevPoint.distance(nextPoint) > this.maxDeviation
312 && checkInflectionPoint(fc, prevT, medianT, nextT, prevPoint, nextPoint))
313 {
314
315 result.put(medianT, medianPoint);
316 directions.put(medianT, curve.getDirection(medianT, of));
317 continue;
318 }
319 prevT = nextT;
320 prevPoint = nextPoint;
321 if (prevT < 1.0 && knots.contains(prevT))
322 {
323 directions.put(prevT, curve.getDirection(prevT + Math.ulp(prevT), of));
324 }
325 }
326 return new PolyLine2d(result.values().iterator());
327 }
328 }
329
330
331
332
333 class MaxAngle implements OffsetFlattener2d
334 {
335
336 private final double maxAngle;
337
338
339
340
341
342
343
344
345 public MaxAngle(final double maxAngle)
346 {
347 Throw.whenNaN(maxAngle, "maxAngle");
348 Throw.when(maxAngle <= 0.0, IllegalArgumentException.class, "Maximum angle must be above 0.0.");
349 this.maxAngle = maxAngle;
350 }
351
352 @Override
353 public PolyLine2d flatten(final OffsetCurve2d curve, final ContinuousPiecewiseLinearFunction of)
354 {
355 Throw.whenNull(curve, "curve");
356 Flattener.FlattableCurve<Point2d, Double> fc = makeFlattableCurve(curve, of);
357 NavigableMap<Double, Point2d> result = new TreeMap<>();
358 loadKnots(result, curve, of);
359 Map<Double, Double> directions = new LinkedHashMap<>();
360 directions.put(0.0, curve.getDirection(0.0, of));
361 Set<Double> knots = new HashSet<>();
362 for (double knot : result.keySet())
363 {
364 if (knot > 0)
365 {
366 directions.put(knot, curve.getDirection(knot - Math.ulp(knot), of));
367 }
368 if (knot != 0.0 && knot != 1.0)
369 {
370 knots.add(knot);
371 }
372 }
373
374
375 double prevT = result.firstKey();
376 Point2d prevPoint = result.get(prevT);
377 Map.Entry<Double, Point2d> entry;
378 int iterationsAtSinglePoint = 0;
379 while ((entry = result.higherEntry(prevT)) != null)
380 {
381 double nextT = entry.getKey();
382 Point2d nextPoint = entry.getValue();
383 double medianT = (prevT + nextT) / 2;
384
385
386 if (checkDirectionError(prevPoint.directionTo(nextPoint), directions.get(prevT), directions.get(nextT),
387 this.maxAngle))
388 {
389
390 Point2d medianPoint = curve.getPoint(medianT, of);
391 result.put(medianT, medianPoint);
392 directions.put(medianT, curve.getDirection(medianT, of));
393 iterationsAtSinglePoint++;
394 Throw.when(iterationsAtSinglePoint == 50, IllegalArgumentException.class, "Required a new point 50 "
395 + "times around the same point (t=%f). Likely there is an (unreported) knot in the OffsetCurve2d.",
396 medianT);
397 continue;
398 }
399 iterationsAtSinglePoint = 0;
400 if (checkInflectionPoint(fc, prevT, medianT, nextT, prevPoint, nextPoint))
401 {
402
403 Point2d medianPoint = curve.getPoint(medianT, of);
404 result.put(medianT, medianPoint);
405 directions.put(medianT, curve.getDirection(medianT, of));
406 continue;
407 }
408 prevT = nextT;
409 prevPoint = nextPoint;
410 if (knots.contains(prevT))
411 {
412 directions.put(prevT, curve.getDirection(prevT + Math.ulp(prevT), of));
413 }
414 }
415 return new PolyLine2d(result.values().iterator());
416 }
417 }
418
419 }