1 package org.opentrafficsim.base.geometry;
2
3 import java.util.Optional;
4
5 import org.djunits.value.vdouble.scalar.Length;
6 import org.djutils.draw.point.Point2d;
7 import org.djutils.exceptions.Throw;
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 public final class RadiusCalculator2d
28 {
29
30
31 private static final double INTERSECTION_EPS = 1e-7;
32
33
34 private final OtsLine2d line;
35
36
37 private final FractionalProjectionHelper fracHelper;
38
39
40
41
42 private Length[] vertexRadii;
43
44
45
46
47
48
49 public RadiusCalculator2d(final OtsLine2d line, final FractionalProjectionHelper fracHelper)
50 {
51 this.line = line;
52 this.fracHelper = fracHelper != null ? fracHelper : new FractionalProjectionHelper(line);
53 this.vertexRadii = new Length[Math.max(0, line.size())];
54 }
55
56
57
58
59
60
61
62
63 public synchronized Optional<Length> radiusAtFraction(final double fraction) throws IllegalArgumentException
64 {
65 Throw.when(fraction < 0.0 || fraction > 1.0, IllegalArgumentException.class,
66 "Fraction %s is out of bounds [0.0 .. 1.0]", fraction);
67
68 final int n = this.line.size() - 1;
69 if (n < 2)
70 {
71
72 return Optional.empty();
73 }
74
75 final double totalLen = this.line.lengthAtIndex(this.line.size() - 1);
76 final double absS = fraction * totalLen;
77
78 final int segIndex = segmentIndexAt(absS);
79
80 if (segIndex > 0 && this.vertexRadii[segIndex] == null)
81 {
82 this.vertexRadii[segIndex] = computeProjectedVertexRadius(segIndex);
83 }
84 if (segIndex < n - 1 && this.vertexRadii[segIndex + 1] == null)
85 {
86 this.vertexRadii[segIndex + 1] = computeProjectedVertexRadius(segIndex + 1);
87 }
88
89 if (segIndex == 0)
90 {
91
92 return Optional.ofNullable(n >= 2 ? this.vertexRadii[1] : null);
93 }
94 if (segIndex == n - 1)
95 {
96
97 return Optional.of(this.vertexRadii[n - 1]);
98 }
99
100 final Length left = this.vertexRadii[segIndex];
101 final Length right = this.vertexRadii[segIndex + 1];
102 if (left == null && right == null)
103 {
104 return Optional.empty();
105 }
106 else if (left == null)
107 {
108 return Optional.of(right);
109 }
110 else if (right == null)
111 {
112 return Optional.of(left);
113 }
114 return Optional.of(Math.abs(left.si) <= Math.abs(right.si) ? left : right);
115 }
116
117
118
119
120
121
122
123
124 public synchronized Optional<Length> radiusAtVertex(final int index) throws IndexOutOfBoundsException
125 {
126 Throw.when(index < 1 || index > this.line.size() - 2, IndexOutOfBoundsException.class,
127 "Index %s is out of bounds [1 .. %s]", index, this.line.size() - 2);
128 if (this.vertexRadii[index] == null)
129 {
130 this.vertexRadii[index] = computeProjectedVertexRadius(index);
131 }
132 return Optional.ofNullable(this.vertexRadii[index]);
133 }
134
135
136
137
138
139
140 private Length computeProjectedVertexRadius(final int index)
141 {
142
143
144 final double lenPrev = this.line.lengthAtIndex(index) - this.line.lengthAtIndex(index - 1);
145 final double lenNext = this.line.lengthAtIndex(index + 1) - this.line.lengthAtIndex(index);
146 final int shortIndex = lenPrev <= lenNext ? index : index + 1;
147
148
149 final Point2d aS = this.line.get(shortIndex - 1);
150 final Point2d bS = this.line.get(shortIndex);
151 final Point2d mid = new Point2d(0.5 * (aS.x + bS.x), 0.5 * (aS.y + bS.y));
152
153
154 final double ex = bS.x - aS.x;
155 final double ey = bS.y - aS.y;
156 final Point2d midPerpEnd = new Point2d(mid.x + ey, mid.y - ex);
157
158
159 final Point2d vertex = this.line.get(index);
160 final FractionalProjectionHelper.Helper h = this.fracHelper.helperAtVertex(index, null, null);
161
162 final Point2d rayEnd;
163 if (h.hasCenter())
164 {
165 rayEnd = new Point2d(h.cx(), h.cy());
166 }
167 else
168 {
169
170 rayEnd = new Point2d(vertex.x + h.dx(), vertex.y + h.dy());
171 }
172
173
174 Point2d inter = intersectionOrNull(mid, midPerpEnd, vertex, rayEnd);
175 if (inter == null)
176 {
177 return null;
178 }
179
180 final double radius = inter.distance(mid);
181 final double i2p2 = inter.distance(midPerpEnd);
182 final double refLen = Math.min(lenPrev, lenNext);
183 final boolean isLeft = (radius < i2p2 && i2p2 > refLen);
184 return Length.ofSI(isLeft ? radius : -radius);
185 }
186
187
188
189
190
191
192
193
194
195
196 private static Point2d intersectionOrNull(final Point2d p1, final Point2d p2, final Point2d p3, final Point2d p4)
197 {
198 final double x1 = p1.x, y1 = p1.y;
199 final double x2 = p2.x, y2 = p2.y;
200 final double x3 = p3.x, y3 = p3.y;
201 final double x4 = p4.x, y4 = p4.y;
202
203 final double dx1 = x2 - x1, dy1 = y2 - y1;
204 final double dx2 = x4 - x3, dy2 = y4 - y3;
205
206 final double denom = dx1 * dy2 - dy1 * dx2;
207 if (Math.abs(denom) < INTERSECTION_EPS)
208 {
209 return null;
210 }
211 final double t = ((x3 - x1) * dy2 - (y3 - y1) * dx2) / denom;
212 return new Point2d(x1 + t * dx1, y1 + t * dy1);
213 }
214
215
216
217
218
219
220 private int segmentIndexAt(final double absS)
221 {
222 final int n = this.line.size();
223 for (int i = 0; i < n - 1; i++)
224 {
225 final double s1 = this.line.lengthAtIndex(i + 1);
226 if (absS <= s1)
227 {
228 return i;
229 }
230 }
231 return n - 2;
232 }
233 }