1 package org.opentrafficsim.core.geometry;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.Iterator;
6 import java.util.List;
7 import java.util.Map.Entry;
8 import java.util.NavigableMap;
9 import java.util.NavigableSet;
10 import java.util.Set;
11 import java.util.SortedSet;
12 import java.util.TreeMap;
13 import java.util.TreeSet;
14
15 import org.djutils.draw.line.PolyLine2d;
16 import org.djutils.draw.point.OrientedPoint2d;
17 import org.djutils.draw.point.Point2d;
18 import org.djutils.exceptions.Throw;
19
20
21
22
23
24
25
26
27
28
29
30
31 public class ContinuousBezierCubic extends ContinuousBezier implements ContinuousLine
32 {
33
34
35 private static final double STRAIGHT = Math.PI / 36000;
36
37
38 private final OrientedPoint2d startPoint;
39
40
41 private final OrientedPoint2d endPoint;
42
43
44 private final double length;
45
46
47
48
49
50
51
52
53 public ContinuousBezierCubic(final Point2d point1, final Point2d point2, final Point2d point3, final Point2d point4)
54 {
55 super(point1, point2, point3, point4);
56 this.startPoint = new OrientedPoint2d(point1.x, point1.y, Math.atan2(point2.y - point1.y, point2.x - point1.x));
57 this.endPoint = new OrientedPoint2d(point4.x, point4.y, Math.atan2(point4.y - point3.y, point4.x - point3.x));
58 this.length = length();
59 }
60
61 @Override
62 public OrientedPoint2d getStartPoint()
63 {
64 return this.startPoint;
65 }
66
67 @Override
68 public OrientedPoint2d getEndPoint()
69 {
70 return this.endPoint;
71 }
72
73 @Override
74 public double getStartCurvature()
75 {
76 return curvature(0.0);
77 }
78
79 @Override
80 public double getEndCurvature()
81 {
82 return curvature(1.0);
83 }
84
85
86
87
88
89 private SortedSet<Double> getRoots()
90 {
91
92 double ax = 3.0 * (-this.points[0].x + 3.0 * this.points[1].x - 3.0 * this.points[2].x + this.points[3].x);
93 double ay = 3.0 * (-this.points[0].y + 3.0 * this.points[1].y - 3.0 * this.points[2].y + this.points[3].y);
94 double bx = 6.0 * (this.points[0].x - 2.0 * this.points[1].x + this.points[2].x);
95 double by = 6.0 * (this.points[0].y - 2.0 * this.points[1].y + this.points[2].y);
96 double cx = 3.0 * (this.points[1].x - this.points[0].x);
97 double cy = 3.0 * (this.points[1].y - this.points[0].y);
98
99
100 TreeSet<Double> roots = new TreeSet<>();
101 double g = bx * bx - 4.0 * ax * cx;
102 if (g > 0)
103 {
104 double sqrtg = Math.sqrt(g);
105 double ax2 = 2.0 * ax;
106 roots.add((-bx + sqrtg) / ax2);
107 roots.add((-bx - sqrtg) / ax2);
108 }
109 g = by * by - 4.0 * ay * cy;
110 if (g > 0)
111 {
112 double sqrtg = Math.sqrt(g);
113 double ay2 = 2.0 * ay;
114 roots.add((-by + sqrtg) / ay2);
115 roots.add((-by - sqrtg) / ay2);
116 }
117
118
119 return roots.subSet(0.0, false, 1.0, false);
120 }
121
122
123
124
125
126 private SortedSet<Double> getInflections()
127 {
128
129 Point2d[] aligned = new Point2d[4];
130 double ang = -Math.atan2(this.points[3].y - this.points[0].y, this.points[3].x - this.points[0].x);
131 double cosAng = Math.cos(ang);
132 double sinAng = Math.sin(ang);
133 for (int i = 0; i < 4; i++)
134 {
135 aligned[i] =
136 new Point2d(cosAng * (this.points[i].x - this.points[0].x) - sinAng * (this.points[i].y - this.points[0].y),
137 sinAng * (this.points[i].x - this.points[0].x) + cosAng * (this.points[i].y - this.points[0].y));
138 }
139
140
141
142
143 double a = aligned[2].x * aligned[1].y;
144 double b = aligned[3].x * aligned[1].y;
145 double c = aligned[1].x * aligned[2].y;
146 double d = aligned[3].x * aligned[2].y;
147
148 double x = -3.0 * a + 2.0 * b + 3.0 * c - d;
149 double y = 3.0 * a - b - 3.0 * c;
150 double z = c - a;
151
152
153 TreeSet<Double> inflections = new TreeSet<>();
154 if (Math.abs(x) < 1.0e-6)
155 {
156 if (Math.abs(y) >= 1.0e-12)
157 {
158 inflections.add(-z / y);
159 }
160 }
161 else
162 {
163 double det = y * y - 4.0 * x * z;
164 double sq = Math.sqrt(det);
165 double d2 = 2 * x;
166 if (det >= 0.0 && Math.abs(d2) >= 1e-12)
167 {
168 inflections.add(-(y + sq) / d2);
169 inflections.add((sq - y) / d2);
170 }
171 }
172
173
174 return inflections.subSet(0.0, false, 1.0, false);
175 }
176
177
178
179
180
181
182 private SortedSet<Double> getOffsetT(final Set<Double> fractions)
183 {
184 TreeSet<Double> crossSections = new TreeSet<>();
185 double lenTot = length();
186 for (Double f : fractions)
187 {
188 if (f > 0.0 && f < 1.0)
189 {
190 crossSections.add(getT(f * lenTot));
191 }
192 }
193 return crossSections;
194 }
195
196
197
198
199
200
201
202 public double getT(final double len)
203 {
204
205 double t0 = 0.0;
206 double t2 = 1.0;
207 double t1 = 0.5;
208 while (t2 > t0 + 1.0e-6)
209 {
210 t1 = (t2 + t0) / 2.0;
211 ContinuousBezierCubic[] parts = split(t1);
212 double len1 = parts[0].length();
213 if (len1 < len)
214 {
215 t0 = t1;
216 }
217 else
218 {
219 t2 = t1;
220 }
221 }
222 return t1;
223 }
224
225
226
227
228
229
230 public ContinuousBezierCubic[] split(final double t)
231 {
232 Throw.when(t < 0.0 || t > 1.0, IllegalArgumentException.class, "t value should be in the range [0.0 ... 1.0].");
233 List<Point2d> p1 = new ArrayList<>();
234 List<Point2d> p2 = new ArrayList<>();
235 split0(t, List.of(this.points), p1, p2);
236 return new ContinuousBezierCubic[] {new ContinuousBezierCubic(p1.get(0), p1.get(1), p1.get(2), p1.get(3)),
237 new ContinuousBezierCubic(p2.get(3), p2.get(2), p2.get(1), p2.get(0))};
238 }
239
240
241
242
243
244
245
246
247 private void split0(final double t, final List<Point2d> p, final List<Point2d> p1, final List<Point2d> p2)
248 {
249 if (p.size() == 1)
250 {
251 p1.add(p.get(0));
252 p2.add(p.get(0));
253 }
254 else
255 {
256 List<Point2d> pNew = new ArrayList<>();
257 for (int i = 0; i < p.size() - 1; i++)
258 {
259 if (i == 0)
260 {
261 p1.add(p.get(i));
262 }
263 if (i == p.size() - 2)
264 {
265 p2.add(p.get(i + 1));
266 }
267 double t1 = 1.0 - t;
268 pNew.add(new Point2d(t1 * p.get(i).x + t * p.get(i + 1).x, t1 * p.get(i).y + t * p.get(i + 1).y));
269 }
270 split0(t, pNew, p1, p2);
271 }
272 }
273
274 @Override
275 public PolyLine2d flatten(final Flattener flattener)
276 {
277 Throw.whenNull(flattener, "Flattener may not be null.");
278 return flattener.flatten(new FlattableLine()
279 {
280 @Override
281 public Point2d get(final double fraction)
282 {
283 return at(fraction);
284 }
285
286 @Override
287 public double getDirection(final double fraction)
288 {
289 Point2d derivative = derivative().at(fraction);
290 return Math.atan2(derivative.y, derivative.x);
291 }
292 });
293 }
294
295 @Override
296 public PolyLine2d flattenOffset(final ContinuousDoubleFunction offset, final Flattener flattener)
297 {
298 Throw.whenNull(offset, "Offsets may not be null.");
299 Throw.whenNull(flattener, "Flattener may not be null.");
300
301
302 double ang1 = Math.atan2(this.points[1].y - this.points[0].y, this.points[1].x - this.points[0].x);
303 double ang2 = Math.atan2(this.points[3].y - this.points[1].y, this.points[3].x - this.points[1].x);
304 double ang3 = Math.atan2(this.points[3].y - this.points[2].y, this.points[3].x - this.points[2].x);
305 boolean straight =
306 Math.abs(ang1 - ang2) < STRAIGHT && Math.abs(ang2 - ang3) < STRAIGHT && Math.abs(ang3 - ang1) < STRAIGHT;
307
308
309
310
311
312
313
314
315 NavigableMap<Double, ContinuousBezierCubic> segments = new TreeMap<>();
316
317
318 NavigableMap<Double, Integer> splits0 = new TreeMap<>();
319 if (!straight)
320 {
321 getRoots().forEach((t) -> splits0.put(t, 1));
322 getInflections().forEach((t) -> splits0.put(t, 2));
323 }
324 NavigableSet<Double> knots = new TreeSet<>();
325 for (double f : offset.getKnots())
326 {
327 knots.add(f);
328 }
329 getOffsetT(knots).forEach((t) -> splits0.put(t, 3));
330 NavigableMap<Double, Integer> splits = splits0.subMap(1e-6, false, 1.0 - 1e-6, false);
331
332
333
334 NavigableSet<Double> fCrossSectionRemain = knots.tailSet(0.0, false);
335 double lengthTotal = length();
336 ContinuousBezierCubic currentBezier = this;
337 double lengthSoFar = 0.0;
338 boolean first = true;
339
340 double sig = Math.signum((this.points[1].y - this.points[0].y) * (this.points[2].x - this.points[0].x)
341 - (this.points[1].x - this.points[0].x) * (this.points[2].y - this.points[0].y));
342
343 Iterator<Double> typeIterator = splits.navigableKeySet().iterator();
344 double tStart = 0.0;
345 if (splits.isEmpty())
346 {
347 segments.put(tStart, currentBezier.offset(offset, lengthSoFar, lengthTotal, sig, first, true));
348 }
349 while (typeIterator.hasNext())
350 {
351
352 double tInFull = typeIterator.next();
353 int type = splits.get(tInFull);
354 boolean isRoot = type == 1;
355 boolean isInflection = type == 2;
356
357 double t;
358
359
360 if (isRoot)
361 {
362 t = currentBezier.getRoots().first();
363 }
364 else if (isInflection)
365 {
366 t = currentBezier.getInflections().first();
367 }
368 else
369 {
370 NavigableSet<Double> fCrossSection = new TreeSet<>();
371 double fSoFar = lengthSoFar / lengthTotal;
372 double fFirst = fCrossSectionRemain.pollFirst();
373 fCrossSection.add((fFirst - fSoFar) / (1.0 - fSoFar));
374 t = currentBezier.getOffsetT(fCrossSection).first();
375 }
376
377
378 ContinuousBezierCubic[] parts = currentBezier.split(t);
379 segments.put(tStart, parts[0].offset(offset, lengthSoFar, lengthTotal, sig, first, false));
380
381
382 first = false;
383 lengthSoFar += parts[0].getLength();
384 if (isInflection)
385 {
386 sig = -sig;
387 }
388 tStart = tInFull;
389
390
391 if (!typeIterator.hasNext())
392 {
393 segments.put(tStart, parts[1].offset(offset, lengthSoFar, lengthTotal, sig, first, true));
394 }
395 else
396 {
397 currentBezier = parts[1];
398 }
399 }
400 segments.put(1.0, null);
401
402
403 return flattener.flatten(new FlattableLine()
404 {
405 @Override
406 public Point2d get(final double fraction)
407 {
408 Entry<Double, ContinuousBezierCubic> entry;
409 double nextT;
410 if (fraction == 1.0)
411 {
412 entry = segments.lowerEntry(fraction);
413 nextT = fraction;
414 }
415 else
416 {
417 entry = segments.floorEntry(fraction);
418 nextT = segments.higherKey(fraction);
419 }
420 double t = (fraction - entry.getKey()) / (nextT - entry.getKey());
421 return entry.getValue().at(t);
422 }
423
424 @Override
425 public double getDirection(final double fraction)
426 {
427 Entry<Double, ContinuousBezierCubic> entry = segments.floorEntry(fraction);
428 if (entry.getValue() == null)
429 {
430
431 entry = segments.lowerEntry(fraction);
432 Point2d derivative = entry.getValue().derivative().at(1.0);
433 return Math.atan2(derivative.y, derivative.x);
434 }
435 Double nextT = segments.higherKey(fraction);
436 if (nextT == null)
437 {
438 nextT = 1.0;
439 }
440 double t = (fraction - entry.getKey()) / (nextT - entry.getKey());
441 Point2d derivative = entry.getValue().derivative().at(t);
442 return Math.atan2(derivative.y, derivative.x);
443 }
444 });
445 }
446
447
448
449
450
451
452
453
454
455
456
457 private ContinuousBezierCubic offset(final ContinuousDoubleFunction offset, final double lengthSoFar, final double lengthTotal,
458 final double sig, final boolean first, final boolean last)
459 {
460 double offsetStart = sig * offset.apply(lengthSoFar / lengthTotal);
461 double offsetEnd = sig * offset.apply((lengthSoFar + getLength()) / lengthTotal);
462
463 Point2d p1 = new Point2d(this.points[0].x - (this.points[1].y - this.points[0].y),
464 this.points[0].y + (this.points[1].x - this.points[0].x));
465 Point2d p2 = new Point2d(this.points[3].x - (this.points[2].y - this.points[3].y),
466 this.points[3].y + (this.points[2].x - this.points[3].x));
467 Point2d center = Point2d.intersectionOfLines(this.points[0], p1, p2, this.points[3]);
468
469 if (center == null)
470 {
471
472 Point2d[] newBezierPoints = new Point2d[4];
473 double ang = Math.atan2(p1.y - this.points[0].y, p1.x - this.points[0].x);
474 double dxStart = Math.cos(ang) * offsetStart;
475 double dyStart = -Math.sin(ang) * offsetStart;
476 newBezierPoints[0] = new Point2d(this.points[0].x + dxStart, this.points[0].y + dyStart);
477 newBezierPoints[1] = new Point2d(this.points[1].x + dxStart, this.points[1].y + dyStart);
478 double dxEnd = Math.cos(ang) * offsetEnd;
479 double dyEnd = -Math.sin(ang) * offsetEnd;
480 newBezierPoints[2] = new Point2d(this.points[2].x + dxEnd, this.points[2].y + dyEnd);
481 newBezierPoints[3] = new Point2d(this.points[3].x + dxEnd, this.points[3].y + dyEnd);
482 return new ContinuousBezierCubic(newBezierPoints[0], newBezierPoints[1], newBezierPoints[2], newBezierPoints[3]);
483 }
484
485
486 Point2d[] newBezierPoints = new Point2d[4];
487 double off = offsetStart;
488 for (int i = 0; i < 4; i = i + 3)
489 {
490 double dy = this.points[i].y - center.y;
491 double dx = this.points[i].x - center.x;
492 double ang = Math.atan2(dy, dx);
493 double len = Math.hypot(dx, dy) + off;
494 newBezierPoints[i] = new Point2d(center.x + len * Math.cos(ang), center.y + len * Math.sin(ang));
495 off = offsetEnd;
496 }
497
498
499 double ang = sig * Math.atan((offsetEnd - offsetStart) / getLength());
500 double cosAng = Math.cos(ang);
501 double sinAng = Math.sin(ang);
502 double dx = this.points[1].x - this.points[0].x;
503 double dy = this.points[1].y - this.points[0].y;
504 double dx1;
505 double dy1;
506 if (first)
507 {
508
509 dx1 = dx;
510 dy1 = dy;
511 }
512 else
513 {
514
515 dx1 = cosAng * dx - sinAng * dy;
516 dy1 = sinAng * dx + cosAng * dy;
517 }
518 dx = this.points[2].x - this.points[3].x;
519 dy = this.points[2].y - this.points[3].y;
520 double dx2;
521 double dy2;
522 if (last)
523 {
524
525 dx2 = dx;
526 dy2 = dy;
527 }
528 else
529 {
530
531 dx2 = cosAng * dx - sinAng * dy;
532 dy2 = sinAng * dx + cosAng * dy;
533 }
534
535
536
537 Point2d cp2 = new Point2d(newBezierPoints[0].x + dx1, newBezierPoints[0].y + dy1);
538 newBezierPoints[1] = Point2d.intersectionOfLines(newBezierPoints[0], cp2, center, this.points[1]);
539 Point2d cp3 = new Point2d(newBezierPoints[3].x + dx2, newBezierPoints[3].y + dy2);
540 newBezierPoints[2] = Point2d.intersectionOfLines(newBezierPoints[3], cp3, center, this.points[2]);
541
542
543 return new ContinuousBezierCubic(newBezierPoints[0], newBezierPoints[1], newBezierPoints[2], newBezierPoints[3]);
544 }
545
546 @Override
547 public double getLength()
548 {
549 return this.length;
550 }
551
552 @Override
553 public String toString()
554 {
555 return "ContinuousBezierCubic [points=" + Arrays.toString(this.points) + "]";
556 }
557
558 }