1 package org.opentrafficsim.base.geometry;
2
3 import java.awt.geom.Line2D;
4
5 import org.djunits.value.vdouble.scalar.Direction;
6 import org.djutils.draw.line.PolyLine2d;
7 import org.djutils.draw.point.Point2d;
8 import org.djutils.exceptions.Throw;
9 import org.djutils.math.AngleUtil;
10
11
12
13
14
15
16
17
18
19
20 public final class FractionalProjectionHelper
21 {
22
23
24 private static final double FRAC_PROJ_PRECISION = 2e-5;
25
26
27 private static final double INTERSECTION_EPS = 1e-7;
28
29
30 private final OtsLine2d line;
31
32
33 private final int n;
34
35
36 private final boolean[] isCenter;
37
38
39 private final double[] centerX;
40
41
42 private final double[] centerY;
43
44
45 private final double[] dirX;
46
47
48 private final double[] dirY;
49
50
51 private Point2d firstOffsetIntersection;
52
53
54 private Point2d lastOffsetIntersection;
55
56
57
58
59
60 private EdgeHelpers edgeNullNull;
61
62
63 private DirectionKey lastKey;
64
65
66 private EdgeHelpers edgeLast;
67
68
69
70
71
72 FractionalProjectionHelper(final OtsLine2d line)
73 {
74 this.line = line;
75 this.n = this.line.size() - 1;
76
77 this.isCenter = new boolean[this.n];
78 this.centerX = new double[this.n];
79 this.centerY = new double[this.n];
80 this.dirX = new double[this.n];
81 this.dirY = new double[this.n];
82
83
84 precomputeFixedHelpers();
85 }
86
87
88
89
90 private void precomputeFixedHelpers()
91 {
92 if (this.n < 2)
93 {
94
95 return;
96 }
97
98
99 PolyLine2d prevOfs = unitOffsetSegment(0);
100 PolyLine2d nextOfs = unitOffsetSegment(1);
101
102 Point2d parStart = intersectionOrFallbackMid(prevOfs.get(0), prevOfs.get(1), nextOfs.get(0), nextOfs.get(1));
103 this.firstOffsetIntersection = parStart;
104
105
106 if (this.n == 2)
107 {
108 this.lastOffsetIntersection = parStart;
109 return;
110 }
111
112
113 for (int i = 1; i <= this.n - 2; i++)
114 {
115 prevOfs = nextOfs;
116 if (i + 1 <= this.n - 1)
117 {
118 nextOfs = unitOffsetSegment(i + 1);
119 }
120 final Point2d parEnd = intersectionOrFallbackMid(prevOfs.get(0), prevOfs.get(1), nextOfs.get(0), nextOfs.get(1));
121
122
123 final Point2d c = intersectionOrNull(this.line.get(i), parStart, this.line.get(i + 1), parEnd);
124 if (c != null)
125 {
126 this.isCenter[i] = true;
127 this.centerX[i] = c.x;
128 this.centerY[i] = c.y;
129 }
130 else
131 {
132 this.isCenter[i] = false;
133 this.dirX[i] = parStart.x - this.line.get(i).x;
134 this.dirY[i] = parStart.y - this.line.get(i).y;
135 }
136
137 parStart = parEnd;
138 if (i == this.n - 2)
139 {
140 this.lastOffsetIntersection = parStart;
141 }
142 }
143 }
144
145
146
147
148
149
150 private PolyLine2d unitOffsetSegment(final int i)
151 {
152 return new PolyLine2d(this.line.get(i), this.line.get(i + 1)).offsetLine(1.0);
153 }
154
155
156
157
158
159
160
161
162
163
164
165
166 public synchronized double projectFractionalAt(final Direction start, final Direction end, final double x, final double y,
167 final FractionalFallback fallback)
168 {
169 Throw.whenNull(fallback, "fallback");
170
171
172 final EdgeHelpers edges = getEdgeHelpers(start, end);
173
174
175 double minD = Double.POSITIVE_INFINITY;
176 final double[] segDist = new double[this.n];
177 for (int i = 0; i < this.n; i++)
178 {
179 Point2d a = this.line.get(i);
180 Point2d b = this.line.get(i + 1);
181 segDist[i] = Line2D.ptSegDist(a.x, a.y, b.x, b.y, x, y);
182 if (segDist[i] < minD)
183 {
184 minD = segDist[i];
185 }
186 }
187
188 double bestDistance = Double.POSITIVE_INFINITY;
189 int bestSeg = -1;
190 double bestSegFrac = 0.0;
191
192 final Point2d ext = new Point2d(x, y);
193
194 for (int i = 0; i < this.n; i++)
195 {
196 if (segDist[i] > minD + FRAC_PROJ_PRECISION)
197 {
198 continue;
199 }
200 final Helper h = helperForSegment(i, edges);
201 final Point2d p = intersectProjection(i, h, ext);
202 if (p == null)
203 {
204 continue;
205 }
206
207
208 Point2d a = this.line.get(i);
209 Point2d b = this.line.get(i + 1);
210 final double segLen = a.distance(b) + FRAC_PROJ_PRECISION;
211 if (p.distance(a) > segLen || p.distance(b) > segLen)
212 {
213 continue;
214 }
215
216
217 final double dist = p.distance(ext);
218 if (dist < bestDistance)
219 {
220 bestDistance = dist;
221
222 final double segActual = a.distance(b);
223 final double along = a.distance(p);
224 bestSegFrac = Math.min(1.0, Math.max(0.0, segActual > 0.0 ? (along / segActual) : 0.0));
225 bestSeg = i;
226 }
227 }
228
229 if (bestSeg < 0)
230 {
231
232 return fallback.getFraction(this, x, y);
233 }
234
235
236 final double segStartLen = this.line.lengthAtIndex(bestSeg);
237 final double segEndLen = this.line.lengthAtIndex(bestSeg + 1);
238 final double abs = segStartLen + bestSegFrac * (segEndLen - segStartLen);
239 final double total = this.line.lengthAtIndex(this.line.size() - 1);
240 return abs / total;
241 }
242
243
244
245
246
247
248
249
250
251 public synchronized Helper helperAtVertex(final int vertexIndex, final Direction start, final Direction end)
252 {
253 Throw.when(vertexIndex < 0 || vertexIndex > this.line.size() - 1, IndexOutOfBoundsException.class,
254 "vertexIndex %s out of bounds [0..%s]", vertexIndex, this.line.size() - 1);
255 if (vertexIndex < 0 || vertexIndex > this.line.size() - 1)
256 {
257 throw new IndexOutOfBoundsException("vertexIndex out of bounds");
258 }
259 final EdgeHelpers edges = getEdgeHelpers(start, end);
260 if (vertexIndex == 0)
261 {
262 return edges.first;
263 }
264 if (vertexIndex >= this.n - 1)
265 {
266 return edges.last;
267 }
268
269 return helperForInteriorSegment(vertexIndex);
270 }
271
272
273
274
275 public enum FractionalFallback
276 {
277
278
279 ORTHOGONAL
280 {
281 @Override
282 double getFraction(final FractionalProjectionHelper helper, final double x, final double y)
283 {
284 return helper.line.projectOrthogonalSnapAt(x, y);
285 }
286 },
287
288
289 ORTHOGONAL_EXTENDED
290 {
291 @Override
292 double getFraction(final FractionalProjectionHelper helper, final double x, final double y)
293 {
294 return helper.line.projectOrthogonalSnapAt(x, y, false);
295 }
296 },
297
298
299 ENDPOINT
300 {
301 @Override
302 double getFraction(final FractionalProjectionHelper helper, final double x, final double y)
303 {
304 final Point2d p = new Point2d(x, y);
305 final Point2d a = helper.line.get(0);
306 final Point2d b = helper.line.get(helper.line.size() - 1);
307 final double dStart = p.distance(a);
308 final double dEnd = p.distance(b);
309 final double total = helper.line.lengthAtIndex(helper.line.size() - 1);
310 return (dStart < dEnd) ? (-dStart / total) : ((total + dEnd) / total);
311 }
312 },
313
314
315 NaN
316 {
317 @Override
318 double getFraction(final FractionalProjectionHelper h, final double x, final double y)
319 {
320 return Double.NaN;
321 }
322 };
323
324
325
326
327
328
329
330
331 abstract double getFraction(FractionalProjectionHelper helper, double x, double y);
332 }
333
334
335
336
337
338
339
340
341
342 public record Helper(boolean hasCenter, double cx, double cy, double dx, double dy)
343 {
344
345
346
347
348
349
350 public static Helper center(final double cx, final double cy)
351 {
352 return new Helper(true, cx, cy, Double.NaN, Double.NaN);
353 }
354
355
356
357
358
359
360
361 public static Helper direction(final double dx, final double dy)
362 {
363 return new Helper(false, Double.NaN, Double.NaN, dx, dy);
364 }
365 }
366
367
368
369
370
371
372 private record EdgeHelpers(Helper first, Helper last)
373 {
374 }
375
376
377
378
379
380
381 public record DirectionKey(Double start, Double end)
382 {
383
384
385
386
387
388
389 public static DirectionKey of(final Direction start, final Direction end)
390 {
391 Double sa = (start == null) ? null : AngleUtil.normalizeAroundZero(start.si);
392 Double ea = (end == null) ? null : AngleUtil.normalizeAroundZero(end.si);
393 return new DirectionKey(sa, ea);
394 }
395 }
396
397
398
399
400
401
402 private Helper helperForInteriorSegment(final int segIndex)
403 {
404 if (this.isCenter[segIndex])
405 {
406 return Helper.center(this.centerX[segIndex], this.centerY[segIndex]);
407 }
408 return Helper.direction(this.dirX[segIndex], this.dirY[segIndex]);
409 }
410
411
412
413
414
415
416
417 private Helper helperForSegment(final int segIndex, final EdgeHelpers edges)
418 {
419 if (segIndex == 0)
420 {
421 return edges.first;
422 }
423 if (segIndex == this.n - 1)
424 {
425 return edges.last;
426 }
427 return helperForInteriorSegment(segIndex);
428 }
429
430
431
432
433
434
435
436 private EdgeHelpers getEdgeHelpers(final Direction start, final Direction end)
437 {
438 if (start == null && end == null)
439 {
440 if (this.edgeNullNull == null)
441 {
442 this.edgeNullNull = computeEdgeHelpers(null, null);
443 }
444 return this.edgeNullNull;
445 }
446 final DirectionKey key = DirectionKey.of(start, end);
447 if (key.equals(this.lastKey) && this.edgeLast != null)
448 {
449 return this.edgeLast;
450 }
451 this.edgeLast = computeEdgeHelpers(start, end);
452 this.lastKey = key;
453 return this.edgeLast;
454 }
455
456
457
458
459
460
461
462 private EdgeHelpers computeEdgeHelpers(final Direction start, final Direction end)
463 {
464
465 final double startAng = (start == null)
466 ? Math.atan2(this.line.get(1).y - this.line.get(0).y, this.line.get(1).x - this.line.get(0).x) : start.si;
467 final double endAng = (end == null) ? Math.atan2(this.line.get(this.n).y - this.line.get(this.n - 1).y,
468 this.line.get(this.n).x - this.line.get(this.n - 1).x) : end.si;
469
470
471 final Point2d p1 = new Point2d(this.line.get(0).x + Math.cos(startAng + Math.PI / 2.0),
472 this.line.get(0).y + Math.sin(startAng + Math.PI / 2.0));
473 final Point2d p2 = new Point2d(this.line.get(this.n).x + Math.cos(endAng + Math.PI / 2.0),
474 this.line.get(this.n).y + Math.sin(endAng + Math.PI / 2.0));
475
476 Helper hFirst, hLast;
477 if (this.n == 1)
478 {
479
480 final Point2d c = intersectionOrNull(this.line.get(0), p1, this.line.get(1), p2);
481 if (c != null)
482 {
483 hFirst = Helper.center(c.x, c.y);
484 }
485 else
486 {
487 hFirst = Helper.direction(p1.x - this.line.get(0).x, p1.y - this.line.get(0).y);
488 }
489
490 hLast = hFirst;
491 }
492 else
493 {
494
495 Point2d cFirst = intersectionOrNull(this.line.get(0), p1, this.line.get(1), this.firstOffsetIntersection);
496 if (cFirst != null)
497 {
498 hFirst = Helper.center(cFirst.x, cFirst.y);
499 }
500 else
501 {
502 hFirst = Helper.direction(p1.x - this.line.get(0).x, p1.y - this.line.get(0).y);
503 }
504
505 Point2d cLast =
506 intersectionOrNull(this.line.get(this.n - 1), this.lastOffsetIntersection, this.line.get(this.n), p2);
507 if (cLast != null)
508 {
509 hLast = Helper.center(cLast.x, cLast.y);
510 }
511 else
512 {
513 hLast = Helper.direction(p2.x - this.line.get(this.n).x, p2.y - this.line.get(this.n).y);
514 }
515 }
516 return new EdgeHelpers(hFirst, hLast);
517 }
518
519
520
521
522
523
524
525
526 private Point2d intersectProjection(final int segIndex, final Helper helper, final Point2d ext)
527 {
528 final Point2d a = this.line.get(segIndex);
529 final Point2d b = this.line.get(segIndex + 1);
530
531 if (helper.hasCenter)
532 {
533
534 final Point2d c = new Point2d(helper.cx, helper.cy);
535 final Point2d p = intersectionOrNull(c, ext, a, b);
536 if (p == null)
537 {
538 return null;
539 }
540
541 final double v1x = p.x - c.x, v1y = p.y - c.y;
542 final double v2x = ext.x - c.x, v2y = ext.y - c.y;
543 final double dot = v1x * v2x + v1y * v2y;
544 if (dot <= FRAC_PROJ_PRECISION)
545 {
546 return null;
547 }
548 return p;
549 }
550
551 final Point2d off = new Point2d(ext.x + helper.dx, ext.y + helper.dy);
552 return intersectionOrNull(ext, off, a, b);
553 }
554
555
556
557
558
559
560
561
562
563
564 private static Point2d intersectionOrNull(final Point2d p1, final Point2d p2, final Point2d p3, final Point2d p4)
565 {
566 final double x1 = p1.x, y1 = p1.y;
567 final double x2 = p2.x, y2 = p2.y;
568 final double x3 = p3.x, y3 = p3.y;
569 final double x4 = p4.x, y4 = p4.y;
570
571 final double dx1 = x2 - x1, dy1 = y2 - y1;
572 final double dx2 = x4 - x3, dy2 = y4 - y3;
573
574 final double denom = dx1 * dy2 - dy1 * dx2;
575 if (Math.abs(denom) < INTERSECTION_EPS)
576 {
577 return null;
578 }
579 final double t = ((x3 - x1) * dy2 - (y3 - y1) * dx2) / denom;
580 return new Point2d(x1 + t * dx1, y1 + t * dy1);
581 }
582
583
584
585
586
587
588
589
590
591 private static Point2d intersectionOrFallbackMid(final Point2d a1, final Point2d a2, final Point2d b1, final Point2d b2)
592 {
593 final Point2d inter = intersectionOrNull(a1, a2, b1, b2);
594 if (inter == null)
595 {
596 return midpoint(a2, b1);
597 }
598 final double dStraight = a2.distance(b1);
599 if (dStraight < Math.min(a2.distance(inter), b1.distance(inter)))
600 {
601 return midpoint(a2, b1);
602 }
603 return inter;
604 }
605
606
607
608
609
610
611
612 private static Point2d midpoint(final Point2d p, final Point2d q)
613 {
614 return new Point2d(0.5 * (p.x + q.x), 0.5 * (p.y + q.y));
615 }
616
617 }