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 }