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