1 package org.opentrafficsim.core.geometry;
2
3 import java.awt.geom.Line2D;
4 import java.awt.geom.Path2D;
5 import java.awt.geom.PathIterator;
6 import java.awt.geom.Point2D;
7 import java.io.Serializable;
8 import java.util.ArrayList;
9 import java.util.Arrays;
10 import java.util.List;
11 import java.util.Locale;
12 import java.util.stream.Collectors;
13 import java.util.stream.DoubleStream;
14
15 import org.djunits.unit.DirectionUnit;
16 import org.djunits.value.vdouble.scalar.Direction;
17 import org.djunits.value.vdouble.scalar.Length;
18 import org.djutils.draw.point.Point3d;
19 import org.djutils.exceptions.Throw;
20 import org.djutils.logger.CategoryLogger;
21 import org.locationtech.jts.geom.Coordinate;
22 import org.locationtech.jts.geom.CoordinateSequence;
23 import org.locationtech.jts.geom.Envelope;
24 import org.locationtech.jts.geom.Geometry;
25 import org.locationtech.jts.geom.GeometryFactory;
26 import org.locationtech.jts.geom.LineString;
27 import org.locationtech.jts.linearref.LengthIndexedLine;
28
29 import nl.tudelft.simulation.dsol.animation.Locatable;
30
31 /**
32 * Line with OTSPoint3D points, a cached length indexed line, a cached length, and a cached centroid (all calculated on first
33 * use).
34 * <p>
35 * Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
36 * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
37 * <p>
38 * $LastChangedDate: 2015-07-16 10:20:53 +0200 (Thu, 16 Jul 2015) $, @version $Revision: 1124 $, by $Author: pknoppers $,
39 * initial version Jul 22, 2015 <br>
40 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
41 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
42 * @author <a href="http://www.citg.tudelft.nl">Guus Tamminga</a>
43 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
44 */
45 public class OTSLine3D implements Locatable, Serializable // XXX: DJ
46 {
47 /** */
48 private static final long serialVersionUID = 20150722L;
49
50 /** The points of the line. */
51 private OTSPoint3D[] points;
52
53 /** The cumulative length of the line at point 'i'. */
54 private double[] lengthIndexedLine = null;
55
56 /** The cached length; will be calculated at time of construction. */
57 private Length length; // XXX: DJ uses double
58
59 /** The cached centroid; will be calculated when needed for the first time. */
60 private OTSPoint3D centroid = null;
61
62 /** The cached bounds; will be calculated when needed for the first time. */
63 private Bounds bounds = null; // XXX: DJ
64
65 /** The cached helper points for fractional projection; will be calculated when needed for the first time. */
66 private OTSPoint3D[] fractionalHelperCenters = null;
67
68 /** The cached helper directions for fractional projection; will be calculated when needed for the first time. */
69 private Point2D.Double[] fractionalHelperDirections = null;
70
71 /** Intersection of unit offset lines of first two segments. */
72 private OTSPoint3D firstOffsetIntersection;
73
74 /** Intersection of unit offset lines of last two segments. */
75 private OTSPoint3D lastOffsetIntersection;
76
77 /** Precision for fractional projection algorithm. */
78 private static final double FRAC_PROJ_PRECISION = 2e-5 /* PK too fine 1e-6 */;
79
80 /** Radius at each vertex. */
81 private Length[] vertexRadii; // XXX: DJ
82
83 /** Bounding of this OTSLine3D. */
84 private Envelope envelope;
85
86 /**
87 * Construct a new OTSLine3D.
88 * @param points OTSPoint3D...; the array of points to construct this OTSLine3D from.
89 * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
90 * adjacent points)
91 */
92 public OTSLine3D(final OTSPoint3D... points) throws OTSGeometryException
93 {
94 init(points);
95 }
96
97 /**
98 * Construct a new OTSLine3D, and immediately make the length-indexed line.
99 * @param pts OTSPoint3D...; the array of points to construct this OTSLine3D from.
100 * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
101 * adjacent points)
102 */
103 private void init(final OTSPoint3D... pts) throws OTSGeometryException
104 {
105 if (pts.length < 2)
106 {
107 throw new OTSGeometryException("Degenerate OTSLine3D; has " + pts.length + " point" + (pts.length != 1 ? "s" : ""));
108 }
109 this.lengthIndexedLine = new double[pts.length];
110 this.lengthIndexedLine[0] = 0.0;
111 for (int i = 1; i < pts.length; i++)
112 {
113 if (pts[i - 1].x == pts[i].x && pts[i - 1].y == pts[i].y && pts[i - 1].z == pts[i].z)
114 {
115 throw new OTSGeometryException(
116 "Degenerate OTSLine3D; point " + (i - 1) + " has the same x, y and z as point " + i);
117 }
118 this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + pts[i - 1].distanceSI(pts[i]);
119 }
120 this.points = pts;
121 this.length = Length.instantiateSI(this.lengthIndexedLine[this.lengthIndexedLine.length - 1]); // XXX: DJ double
122 }
123
124 /** Which offsetLine method to use... */
125 public enum OffsetMethod
126 {
127 /** Via JTS buffer. */
128 JTS,
129
130 /** Peter Knoppers. */
131 PK;
132 };
133
134 /** Which offset line method to use... */
135 public static final OffsetMethod OFFSETMETHOD = OffsetMethod.PK;
136
137 /**
138 * Construct parallel line.<br>
139 * TODO Let the Z-component of the result follow the Z-values of the reference line.
140 * @param offset double; offset distance from the reference line; positive is LEFT, negative is RIGHT
141 * @return OTSLine3D; the line that has the specified offset from the reference line
142 */
143 public final OTSLine3D offsetLine(final double offset)
144 {
145 try
146 {
147 switch (OFFSETMETHOD)
148 {
149 case PK:
150 return OTSOffsetLinePK.offsetLine(this, offset);
151
152 case JTS:
153 return OTSBufferingJTS.offsetLine(this, offset);
154
155 default:
156 return null;
157 }
158 }
159 catch (OTSGeometryException exception)
160 {
161 CategoryLogger.always().error(exception);
162 return null;
163 }
164 }
165
166 /**
167 * Construct a line that is equal to this line except for segments that are shorter than the <cite>noiseLevel</cite>. The
168 * result is guaranteed to start with the first point of this line and end with the last point of this line.
169 * @param noiseLevel double; the minimum segment length that is <b>not</b> removed
170 * @return OTSLine3D; the filtered line
171 */
172 public final OTSLine3D noiseFilteredLine(final double noiseLevel)
173 {
174 if (this.size() <= 2)
175 {
176 return this; // Except for some cached fields; an OTSLine3D is immutable; so safe to return
177 }
178 OTSPoint3D prevPoint = null;
179 List<OTSPoint3D> list = null;
180 for (int index = 0; index < this.size(); index++)
181 {
182 OTSPoint3D currentPoint = this.points[index];
183 if (null != prevPoint && prevPoint.distanceSI(currentPoint) < noiseLevel)
184 {
185 if (null == list)
186 {
187 // Found something to filter; copy this up to (and including) prevPoint
188 list = new ArrayList<>();
189 for (int i = 0; i < index; i++)
190 {
191 list.add(this.points[i]);
192 }
193 }
194 if (index == this.size() - 1)
195 {
196 if (list.size() > 1)
197 {
198 // Replace the last point of the result by the last point of this OTSLine3D
199 list.set(list.size() - 1, currentPoint);
200 }
201 else
202 {
203 // Append the last point of this even though it is close to the first point than the noise value to
204 // comply with the requirement that first and last point of this are ALWAYS included in the result.
205 list.add(currentPoint);
206 }
207 }
208 continue; // Do not replace prevPoint by currentPoint
209 }
210 else if (null != list)
211 {
212 list.add(currentPoint);
213 }
214 prevPoint = currentPoint;
215 }
216 if (null == list)
217 {
218 return this;
219 }
220 if (list.size() == 2 && list.get(0).equals(list.get(1)))
221 {
222 // CategoryLogger.always().debug("Fixing up degenerate noiseFilteredLine by inserting an intermediate point");
223 // Find something to insert along the way
224 for (int index = 1; index < this.size() - 1; index++)
225 {
226 if (!this.points[index].equals(list.get(0)))
227 {
228 list.add(1, this.points[index]);
229 break;
230 }
231 }
232 }
233 try
234 {
235 return new OTSLine3D(list);
236 }
237 catch (OTSGeometryException exception)
238 {
239 CategoryLogger.always().error(exception);
240 throw new Error(exception);
241 }
242 }
243
244 /**
245 * Clean up a list of points that describe a polyLine by removing points that lie within epsilon distance of a more
246 * straightened version of the line. <br>
247 * TODO Test this code (currently untested).
248 * @param epsilon double; maximal deviation
249 * @param useHorizontalDistance boolean; if true; the horizontal distance is used; if false; the 3D distance is used
250 * @return OTSLine3D; a new OTSLine3D containing all the remaining points
251 */
252 public final OTSLine3D noiseFilterRamerDouglasPeucker(final double epsilon, final boolean useHorizontalDistance) // XXX: DJ
253 {
254 try
255 {
256 // Apply the Ramer-Douglas-Peucker algorithm to the buffered points.
257 // Adapted from https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
258 double maxDeviation = 0;
259 int splitIndex = -1;
260 int pointCount = size();
261 OTSLine3DOTSLine3D.html#OTSLine3D">OTSLine3D straight = new OTSLine3D(get(0), get(pointCount - 1));
262 // Find the point with largest deviation from the straight line from start point to end point
263 for (int i = 1; i < pointCount - 1; i++)
264 {
265 OTSPoint3D point = get(i);
266 OTSPoint3D closest =
267 useHorizontalDistance ? point.closestPointOnLine2D(straight) : point.closestPointOnLine(straight);
268 double deviation = useHorizontalDistance ? closest.horizontalDistanceSI(point) : closest.distanceSI(point);
269 if (deviation > maxDeviation)
270 {
271 splitIndex = i;
272 maxDeviation = deviation;
273 }
274 }
275 if (maxDeviation <= epsilon)
276 {
277 // All intermediate points can be dropped. Return a new list containing only the first and last point.
278 return straight;
279 }
280 // The largest deviation is larger than epsilon.
281 // Split the polyLine at the point with the maximum deviation. Process each sub list recursively and concatenate the
282 // results
283 OTSLine3Dry/OTSLine3D.html#OTSLine3D">OTSLine3D first = new OTSLine3D(Arrays.copyOfRange(this.points, 0, splitIndex + 1))
284 .noiseFilterRamerDouglasPeucker(epsilon, useHorizontalDistance);
285 OTSLine3Dy/OTSLine3D.html#OTSLine3D">OTSLine3D second = new OTSLine3D(Arrays.copyOfRange(this.points, splitIndex, this.points.length))
286 .noiseFilterRamerDouglasPeucker(epsilon, useHorizontalDistance);
287 return concatenate(epsilon, first, second);
288 }
289 catch (OTSGeometryException exception)
290 {
291 CategoryLogger.always().error(exception); // Peter thinks this cannot happen ...
292 return null;
293 }
294 }
295
296 /**
297 * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
298 * start of the reference line to its final offset value at the end of the reference line.
299 * @param offsetAtStart double; offset at the start of the reference line (positive value is Left, negative value is Right)
300 * @param offsetAtEnd double; offset at the end of the reference line (positive value is Left, negative value is Right)
301 * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
302 * @throws OTSGeometryException when this method fails to create the offset line
303 */
304 public final OTSLine3D offsetLine(final double offsetAtStart, final double offsetAtEnd) throws OTSGeometryException // XXX:
305 // DJ
306 {
307 // CategoryLogger.trace(Cat.CORE, OTSGeometry.printCoordinates("#referenceLine: \nc1,0,0\n# offset at start is "
308 // + offsetAtStart + " at end is " + offsetAtEnd + "\n#", referenceLine, "\n "));
309
310 OTSLine3D offsetLineAtStart = offsetLine(offsetAtStart);
311 if (offsetAtStart == offsetAtEnd)
312 {
313 return offsetLineAtStart; // offset does not change
314 }
315 // CategoryLogger.trace(Cat.CORE, OTSGeometry.printCoordinates("#offset line at start: \nc0,0,0\n#",
316 // offsetLineAtStart, "\n "));
317 OTSLine3D offsetLineAtEnd = offsetLine(offsetAtEnd);
318 // CategoryLogger.trace(Cat.CORE, OTSGeometry.printCoordinates("#offset line at end: \nc0.7,0.7,0.7\n#",
319 // offsetLineAtEnd, "\n "));
320 Geometry startGeometry = offsetLineAtStart.getLineString();
321 Geometry endGeometry = offsetLineAtEnd.getLineString();
322 LengthIndexedLine first = new LengthIndexedLine(startGeometry);
323 double firstLength = startGeometry.getLength();
324 LengthIndexedLine second = new LengthIndexedLine(endGeometry);
325 double secondLength = endGeometry.getLength();
326 ArrayList<Coordinate> out = new ArrayList<>();
327 Coordinate[] firstCoordinates = startGeometry.getCoordinates();
328 Coordinate[] secondCoordinates = endGeometry.getCoordinates();
329 int firstIndex = 0;
330 int secondIndex = 0;
331 Coordinate prevCoordinate = null;
332 final double tooClose = 0.05; // 5 cm
333 while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
334 {
335 double firstRatio = firstIndex < firstCoordinates.length ? first.indexOf(firstCoordinates[firstIndex]) / firstLength
336 : Double.MAX_VALUE;
337 double secondRatio = secondIndex < secondCoordinates.length
338 ? second.indexOf(secondCoordinates[secondIndex]) / secondLength : Double.MAX_VALUE;
339 double ratio;
340 if (firstRatio < secondRatio)
341 {
342 ratio = firstRatio;
343 firstIndex++;
344 }
345 else
346 {
347 ratio = secondRatio;
348 secondIndex++;
349 }
350 Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
351 Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
352 Coordinate resultCoordinate = new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x,
353 (1 - ratio) * firstCoordinate.y + ratio * secondCoordinate.y);
354 if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
355 {
356 out.add(resultCoordinate);
357 prevCoordinate = resultCoordinate;
358 }
359 }
360 Coordinate[] resultCoordinates = new Coordinate[out.size()];
361 for (int index = 0; index < out.size(); index++)
362 {
363 resultCoordinates[index] = out.get(index);
364 }
365 return new OTSLine3D(resultCoordinates);
366 }
367
368 /**
369 * Create a line at linearly varying offset from this line. The offset may change linearly from its initial value at the
370 * start of the reference line via a number of intermediate offsets at intermediate positions to its final offset value at
371 * the end of the reference line.
372 * @param relativeFractions double[]; positional fractions for which the offsets have to be generated
373 * @param offsets double[]; offsets at the relative positions (positive value is Left, negative value is Right)
374 * @return Geometry; the Geometry of the line at linearly changing offset of the reference line
375 * @throws OTSGeometryException when this method fails to create the offset line
376 */
377 public final OTSLine3D offsetLine(final double[] relativeFractions, final double[] offsets) throws OTSGeometryException // XXX:
378 // DJ
379 {
380 Throw.whenNull(relativeFractions, "relativeFraction may not be null");
381 Throw.whenNull(offsets, "offsets may not be null");
382 Throw.when(relativeFractions.length < 2, OTSGeometryException.class, "size of relativeFractions must be >= 2");
383 Throw.when(relativeFractions.length != offsets.length, OTSGeometryException.class,
384 "size of relativeFractions must be equal to size of offsets");
385 Throw.when(relativeFractions[0] < 0, OTSGeometryException.class, "relativeFractions may not start before 0");
386 Throw.when(relativeFractions[relativeFractions.length - 1] > 1, OTSGeometryException.class,
387 "relativeFractions may not end beyond 1");
388 List<Double> fractionsList = DoubleStream.of(relativeFractions).boxed().collect(Collectors.toList());
389 List<Double> offsetsList = DoubleStream.of(offsets).boxed().collect(Collectors.toList());
390 if (relativeFractions[0] != 0)
391 {
392 fractionsList.add(0, 0.0);
393 offsetsList.add(0, 0.0);
394 }
395 if (relativeFractions[relativeFractions.length - 1] < 1.0)
396 {
397 fractionsList.add(1.0);
398 offsetsList.add(0.0);
399 }
400 OTSLine3Dine3D.html#OTSLine3D">OTSLine3D[] offsetLine = new OTSLine3D[fractionsList.size()];
401 for (int i = 0; i < fractionsList.size(); i++)
402 {
403 offsetLine[i] = offsetLine(offsetsList.get(i));
404 System.out.println("# offset is " + offsetsList.get(i));
405 System.out.println(offsetLine[i].toPlot());
406 }
407 List<Coordinate> out = new ArrayList<>();
408 Coordinate prevCoordinate = null;
409 final double tooClose = 0.05; // 5 cm
410 // TODO make tooClose a parameter of this method.
411 for (int i = 0; i < offsetsList.size() - 1; i++)
412 {
413 Throw.when(fractionsList.get(i + 1) <= fractionsList.get(i), OTSGeometryException.class,
414 "fractions must be in ascending order");
415 Geometry startGeometry =
416 offsetLine[i].extractFractional(fractionsList.get(i), fractionsList.get(i + 1)).getLineString();
417 Geometry endGeometry =
418 offsetLine[i + 1].extractFractional(fractionsList.get(i), fractionsList.get(i + 1)).getLineString();
419 LengthIndexedLine first = new LengthIndexedLine(startGeometry);
420 double firstLength = startGeometry.getLength();
421 LengthIndexedLine second = new LengthIndexedLine(endGeometry);
422 double secondLength = endGeometry.getLength();
423 Coordinate[] firstCoordinates = startGeometry.getCoordinates();
424 Coordinate[] secondCoordinates = endGeometry.getCoordinates();
425 int firstIndex = 0;
426 int secondIndex = 0;
427 while (firstIndex < firstCoordinates.length && secondIndex < secondCoordinates.length)
428 {
429 double firstRatio = firstIndex < firstCoordinates.length
430 ? first.indexOf(firstCoordinates[firstIndex]) / firstLength : Double.MAX_VALUE;
431 double secondRatio = secondIndex < secondCoordinates.length
432 ? second.indexOf(secondCoordinates[secondIndex]) / secondLength : Double.MAX_VALUE;
433 double ratio;
434 if (firstRatio < secondRatio)
435 {
436 ratio = firstRatio;
437 firstIndex++;
438 }
439 else
440 {
441 ratio = secondRatio;
442 secondIndex++;
443 }
444 Coordinate firstCoordinate = first.extractPoint(ratio * firstLength);
445 Coordinate secondCoordinate = second.extractPoint(ratio * secondLength);
446 Coordinate resultCoordinate = new Coordinate((1 - ratio) * firstCoordinate.x + ratio * secondCoordinate.x,
447 (1 - ratio) * firstCoordinate.y + ratio * secondCoordinate.y);
448 if (null == prevCoordinate || resultCoordinate.distance(prevCoordinate) > tooClose)
449 {
450 out.add(resultCoordinate);
451 prevCoordinate = resultCoordinate;
452 }
453 }
454 }
455 Coordinate[] resultCoordinates = new Coordinate[out.size()];
456 for (int index = 0; index < out.size(); index++)
457 {
458 resultCoordinates[index] = out.get(index);
459 }
460 return new OTSLine3D(resultCoordinates);
461 }
462
463 /**
464 * Concatenate several OTSLine3D instances.
465 * @param lines OTSLine3D...; OTSLine3D... one or more OTSLine3D. The last point of the first
466 * <strong>must</strong> match the first of the second, etc.
467 * @return OTSLine3D
468 * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
469 */
470 public static OTSLine3D concatenate(final OTSLine3D... lines) throws OTSGeometryException
471 {
472 return concatenate(0.0, lines);
473 }
474
475 /**
476 * Concatenate two OTSLine3D instances. This method is separate for efficiency reasons.
477 * @param toleranceSI double; the tolerance between the end point of a line and the first point of the next line
478 * @param line1 OTSLine3D; first line
479 * @param line2 OTSLine3D; second line
480 * @return OTSLine3D
481 * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
482 */
483 public static OTSLine3D concatenate(finalOTSLine3Dy/OTSLine3D.html#OTSLine3D">OTSLine3Dclass="jxr_keyword">double toleranceSI, final OTSLine3Dy/OTSLine3D.html#OTSLine3D">OTSLine3D line1, final OTSLine3D line2)
484 throws OTSGeometryException
485 {
486 if (line1.getLast().distance(line2.getFirst()).si > toleranceSI)
487 {
488 throw new OTSGeometryException("Lines are not connected: " + line1.getLast() + " to " + line2.getFirst()
489 + " distance is " + line1.getLast().distance(line2.getFirst()).si + " > " + toleranceSI);
490 }
491 int size = line1.size() + line2.size() - 1;
492 OTSPoint3DTSPoint3D.html#OTSPoint3D">OTSPoint3D[] points = new OTSPoint3D[size];
493 int nextIndex = 0;
494 for (int j = 0; j < line1.size(); j++)
495 {
496 points[nextIndex++] = line1.get(j);
497 }
498 for (int j = 1; j < line2.size(); j++)
499 {
500 points[nextIndex++] = line2.get(j);
501 }
502 return new OTSLine3D(points);
503 }
504
505 /**
506 * Concatenate several OTSLine3D instances.
507 * @param toleranceSI double; the tolerance between the end point of a line and the first point of the next line
508 * @param lines OTSLine3D...; OTSLine3D... one or more OTSLine3D. The last point of the first
509 * <strong>must</strong> match the first of the second, etc.
510 * @return OTSLine3D
511 * @throws OTSGeometryException if zero lines are given, or when there is a gap between consecutive lines
512 */
513 public static OTSLine3D concatenate(final double toleranceSI, final OTSLine3D... lines) throws OTSGeometryException
514 {
515 // CategoryLogger.trace(Cat.CORE, "Concatenating " + lines.length + " lines.");
516 if (0 == lines.length)
517 {
518 throw new OTSGeometryException("Empty argument list");
519 }
520 else if (1 == lines.length)
521 {
522 return lines[0];
523 }
524 int size = lines[0].size();
525 for (int i = 1; i < lines.length; i++)
526 {
527 if (lines[i - 1].getLast().distance(lines[i].getFirst()).si > toleranceSI)
528 {
529 throw new OTSGeometryException(
530 "Lines are not connected: " + lines[i - 1].getLast() + " to " + lines[i].getFirst() + " distance is "
531 + lines[i - 1].getLast().distance(lines[i].getFirst()).si + " > " + toleranceSI);
532 }
533 size += lines[i].size() - 1;
534 }
535 OTSPoint3DTSPoint3D.html#OTSPoint3D">OTSPoint3D[] points = new OTSPoint3D[size];
536 int nextIndex = 0;
537 for (int i = 0; i < lines.length; i++)
538 {
539 OTSLine3D line = lines[i];
540 for (int j = 0 == i ? 0 : 1; j < line.size(); j++)
541 {
542 points[nextIndex++] = line.get(j);
543 }
544 }
545 return new OTSLine3D(points);
546 }
547
548 /**
549 * Construct a new OTSLine3D with all points of this OTSLine3D in reverse order.
550 * @return OTSLine3D; the new OTSLine3D
551 */
552 public final OTSLine3D reverse()
553 {
554 OTSPoint3Dt3D.html#OTSPoint3D">OTSPoint3D[] resultPoints = new OTSPoint3D[size()];
555 int nextIndex = size();
556 for (OTSPoint3D p : getPoints())
557 {
558 resultPoints[--nextIndex] = p;
559 }
560 try
561 {
562 return new OTSLine3D(resultPoints);
563 }
564 catch (OTSGeometryException exception)
565 {
566 // Cannot happen
567 throw new RuntimeException(exception);
568 }
569 }
570
571 /**
572 * Construct a new OTSLine3D covering the indicated fraction of this OTSLine3D.
573 * @param start double; starting point, valid range [0..<cite>end</cite>)
574 * @param end double; ending point, valid range (<cite>start</cite>..1]
575 * @return OTSLine3D; the new OTSLine3D
576 * @throws OTSGeometryException when start >= end, or start < 0, or end > 1
577 */
578 public final OTSLine3D extractFractional(final double start, final double end) throws OTSGeometryException
579 {
580 if (start < 0 || start >= end || end > 1)
581 {
582 throw new OTSGeometryException(
583 "Bad interval (start=" + start + ", end=" + end + ", this is " + this.toString() + ")");
584 }
585 return extract(start * this.length.si, end * this.length.si);
586 }
587
588 /**
589 * Create a new OTSLine3D that covers a sub-section of this OTSLine3D.
590 * @param start Length; the length along this OTSLine3D where the sub-section starts, valid range [0..<cite>end</cite>)
591 * @param end Length; length along this OTSLine3D where the sub-section ends, valid range
592 * (<cite>start</cite>..<cite>length</cite> (length is the length of this OTSLine3D)
593 * @return OTSLine3D; the selected sub-section
594 * @throws OTSGeometryException when start >= end, or start < 0, or end > length
595 */
596 public final OTSLine3D extract(final Length start, final Length end) throws OTSGeometryException // XXX: DJ
597 {
598 return extract(start.si, end.si);
599 }
600
601 /**
602 * Create a new OTSLine3D that covers a sub-section of this OTSLine3D.
603 * @param start double; length along this OTSLine3D where the sub-section starts, valid range [0..<cite>end</cite>)
604 * @param end double; length along this OTSLine3D where the sub-section ends, valid range
605 * (<cite>start</cite>..<cite>length</cite> (length is the length of this OTSLine3D)
606 * @return OTSLine3D; the selected sub-section
607 * @throws OTSGeometryException when start >= end, or start < 0, or end > length
608 */
609 public final OTSLine3D extract(final double start, final double end) throws OTSGeometryException
610 {
611 if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || start >= end || end > getLengthSI())
612 {
613 throw new OTSGeometryException(
614 "Bad interval (" + start + ".." + end + "; length of this OTSLine3D is " + this.getLengthSI() + ")");
615 }
616 double cumulativeLength = 0;
617 double nextCumulativeLength = 0;
618 double segmentLength = 0;
619 int index = 0;
620 List<OTSPoint3D> pointList = new ArrayList<>();
621 // CategoryLogger.trace(Cat.CORE, "interval " + start + ".." + end);
622 while (start > cumulativeLength)
623 {
624 OTSPoint3D fromPoint = this.points[index];
625 index++;
626 OTSPoint3D toPoint = this.points[index];
627 segmentLength = fromPoint.distanceSI(toPoint);
628 cumulativeLength = nextCumulativeLength;
629 nextCumulativeLength = cumulativeLength + segmentLength;
630 if (nextCumulativeLength >= start)
631 {
632 break;
633 }
634 }
635 if (start == nextCumulativeLength)
636 {
637 pointList.add(this.points[index]);
638 }
639 else
640 {
641 pointList.add(OTSPoint3D.interpolate((start - cumulativeLength) / segmentLength, this.points[index - 1],
642 this.points[index]));
643 if (end > nextCumulativeLength)
644 {
645 pointList.add(this.points[index]);
646 }
647 }
648 while (end > nextCumulativeLength)
649 {
650 OTSPoint3D fromPoint = this.points[index];
651 index++;
652 if (index >= this.points.length)
653 {
654 break; // rounding error
655 }
656 OTSPoint3D toPoint = this.points[index];
657 segmentLength = fromPoint.distanceSI(toPoint);
658 cumulativeLength = nextCumulativeLength;
659 nextCumulativeLength = cumulativeLength + segmentLength;
660 if (nextCumulativeLength >= end)
661 {
662 break;
663 }
664 pointList.add(toPoint);
665 }
666 if (end == nextCumulativeLength)
667 {
668 pointList.add(this.points[index]);
669 }
670 else
671 {
672 OTSPoint3D point = OTSPoint3D.interpolate((end - cumulativeLength) / segmentLength, this.points[index - 1],
673 this.points[index]);
674 // can be the same due to rounding
675 if (!point.equals(pointList.get(pointList.size() - 1)))
676 {
677 pointList.add(point);
678 }
679 }
680 try
681 {
682 return new OTSLine3D(pointList);
683 }
684 catch (OTSGeometryException exception)
685 {
686 CategoryLogger.always().error(exception, "interval " + start + ".." + end + " too short");
687 throw new OTSGeometryException("interval " + start + ".." + end + "too short");
688 }
689 }
690
691 /**
692 * Build an array of OTSPoint3D from an array of Coordinate.
693 * @param coordinates Coordinate[]; the coordinates
694 * @return OTSPoint3D[]
695 */
696 private static OTSPoint3D[] coordinatesToOTSPoint3D(final Coordinate[] coordinates) // XXX: DJ
697 {
698 OTSPoint3DTSPoint3D.html#OTSPoint3D">OTSPoint3D[] result = new OTSPoint3D[coordinates.length];
699 for (int i = 0; i < coordinates.length; i++)
700 {
701 result[i] = new OTSPoint3D(coordinates[i]);
702 }
703 return result;
704 }
705
706 /**
707 * Create an OTSLine3D, while cleaning repeating successive points.
708 * @param points OTSPoint3D...; the coordinates of the line as OTSPoint3D
709 * @return the line
710 * @throws OTSGeometryException when number of points < 2
711 */
712 public static OTSLine3D createAndCleanOTSLine3D(final OTSPoint3D... points) throws OTSGeometryException
713 {
714 if (points.length < 2)
715 {
716 throw new OTSGeometryException(
717 "Degenerate OTSLine3D; has " + points.length + " point" + (points.length != 1 ? "s" : ""));
718 }
719 return createAndCleanOTSLine3D(new ArrayList<>(Arrays.asList(points)));
720 }
721
722 /**
723 * Create an OTSLine3D, while cleaning repeating successive points.
724 * @param pointList List<OTSPoint3D>; list of the coordinates of the line as OTSPoint3D; any duplicate points in this
725 * list are removed (this method may modify the provided list)
726 * @return OTSLine3D; the line
727 * @throws OTSGeometryException when number of non-equal points < 2
728 */
729 public static OTSLine3D createAndCleanOTSLine3D(final List<OTSPoint3D> pointList) throws OTSGeometryException
730 {
731 // clean successive equal points
732 int i = 1;
733 while (i < pointList.size())
734 {
735 if (pointList.get(i - 1).equals(pointList.get(i)))
736 {
737 pointList.remove(i);
738 }
739 else
740 {
741 i++;
742 }
743 }
744 return new OTSLine3D(pointList);
745 }
746
747 /**
748 * Construct a new OTSLine3D from an array of Coordinate.
749 * @param coordinates Coordinate[]; the array of coordinates to construct this OTSLine3D from
750 * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
751 * adjacent points)
752 */
753 public OTSLine3D(final Coordinate[] coordinates) throws OTSGeometryException // XXX: DJ
754 {
755 this(coordinatesToOTSPoint3D(coordinates));
756 }
757
758 /**
759 * Construct a new OTSLine3D from a LineString.
760 * @param lineString LineString; the lineString to construct this OTSLine3D from.
761 * @throws OTSGeometryException when the provided LineString does not constitute a valid line (too few points or identical
762 * adjacent points)
763 */
764 public OTSLine3D(final LineString lineString) throws OTSGeometryException // XXX: DJ
765 {
766 this(lineString.getCoordinates());
767 }
768
769 /**
770 * Construct a new OTSLine3D from a Geometry.
771 * @param geometry Geometry; the geometry to construct this OTSLine3D from
772 * @throws OTSGeometryException when the provided Geometry do not constitute a valid line (too few points or identical
773 * adjacent points)
774 */
775 public OTSLine3D(final Geometry geometry) throws OTSGeometryException // XXX: DJ
776 {
777 this(geometry.getCoordinates());
778 }
779
780 /**
781 * Construct a new OTSLine3D from a List<OTSPoint3D>.
782 * @param pointList List<OTSPoint3D>; the list of points to construct this OTSLine3D from.
783 * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
784 * adjacent points)
785 */
786 public OTSLine3D(final List<OTSPoint3D> pointList) throws OTSGeometryException
787 {
788 this(pointList.toArray(new OTSPoint3D[pointList.size()]));
789 }
790
791 /**
792 * Construct a new OTSShape (closed shape) from a Path2D.
793 * @param path Path2D; the Path2D to construct this OTSLine3D from.
794 * @throws OTSGeometryException when the provided points do not constitute a valid line (too few points or identical
795 * adjacent points)
796 */
797 public OTSLine3D(final Path2D path) throws OTSGeometryException
798 {
799 List<OTSPoint3D> pl = new ArrayList<>();
800 for (PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next())
801 {
802 double[] p = new double[6];
803 int segType = pi.currentSegment(p);
804 if (segType == PathIterator.SEG_MOVETO || segType == PathIterator.SEG_LINETO)
805 {
806 pl.add(new OTSPoint3D(p[0], p[1]));
807 }
808 else if (segType == PathIterator.SEG_CLOSE)
809 {
810 if (!pl.get(0).equals(pl.get(pl.size() - 1)))
811 {
812 pl.add(new OTSPoint3D(pl.get(0).x, pl.get(0).y));
813 }
814 break;
815 }
816 }
817 init(pl.toArray(new OTSPoint3D[pl.size() - 1]));
818 }
819
820 /**
821 * Construct a Coordinate array and fill it with the points of this OTSLine3D.
822 * @return an array of Coordinates corresponding to this OTSLine
823 */
824 public final Coordinate[] getCoordinates() // XXX: DJ
825 {
826 Coordinate[] result = new Coordinate[size()];
827 for (int i = 0; i < size(); i++)
828 {
829 result[i] = this.points[i].getCoordinate();
830 }
831 return result;
832 }
833
834 /**
835 * Construct a LineString from this OTSLine3D.
836 * @return a LineString corresponding to this OTSLine
837 */
838 public final LineString getLineString() // XXX: DJ
839 {
840 GeometryFactory factory = new GeometryFactory();
841 Coordinate[] coordinates = getCoordinates();
842 CoordinateSequence cs = factory.getCoordinateSequenceFactory().create(coordinates);
843 return new LineString(cs, factory);
844 }
845
846 /**
847 * Return the number of points in this OTSLine3D.
848 * @return the number of points on the line
849 */
850 public final int size()
851 {
852 return this.points.length;
853 }
854
855 /**
856 * Return the first point of this OTSLine3D.
857 * @return the first point on the line
858 */
859 public final OTSPoint3D getFirst()
860 {
861 return this.points[0];
862 }
863
864 /**
865 * Return the last point of this OTSLine3D.
866 * @return the last point on the line
867 */
868 public final OTSPoint3D getLast()
869 {
870 return this.points[size() - 1];
871 }
872
873 /**
874 * Return one point of this OTSLine3D.
875 * @param i int; the index of the point to retrieve
876 * @return OTSPoint3d; the i-th point of the line
877 * @throws OTSGeometryException when i < 0 or i > the number of points
878 */
879 public final OTSPoint3D get(final int i) throws OTSGeometryException
880 {
881 if (i < 0 || i > size() - 1)
882 {
883 throw new OTSGeometryException("OTSLine3D.get(i=" + i + "); i<0 or i>=size(), which is " + size());
884 }
885 return this.points[i];
886 }
887
888 /**
889 * Return the length of this OTSLine3D as a double value in SI units. (Assumes that the coordinates of the points
890 * constituting this line are expressed in meters.)
891 * @return the length of the line in SI units
892 */
893 public final double getLengthSI()
894 {
895 return this.length.si;
896 }
897
898 /**
899 * Return the length of this OTSLine3D in meters. (Assuming that the coordinates of the points constituting this line are
900 * expressed in meters.)
901 * @return the length of the line
902 */
903 public final Length getLength() // XXX: DJ
904 {
905 return this.length;
906 }
907
908 /**
909 * Return an array of OTSPoint3D that represents this OTSLine3D. <strong>Do not modify the result.</strong>
910 * @return the points of this line
911 */
912 public final OTSPoint3D[] getPoints()
913 {
914 return this.points;
915 }
916
917 /**
918 * Make the length indexed line if it does not exist yet, and cache it.
919 */
920 private synchronized void makeLengthIndexedLine()
921 {
922 if (this.lengthIndexedLine == null)
923 {
924 this.lengthIndexedLine = new double[this.points.length];
925 this.lengthIndexedLine[0] = 0.0;
926 for (int i = 1; i < this.points.length; i++)
927 {
928 this.lengthIndexedLine[i] = this.lengthIndexedLine[i - 1] + this.points[i - 1].distanceSI(this.points[i]);
929 }
930 }
931 }
932
933 /**
934 * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
935 * that case, the position will be extrapolated in the direction of the line at its start or end.
936 * @param position Length; the position on the line for which to calculate the point on, before, of after the line
937 * @return a directed point
938 */
939 public final DirectedPoint getLocationExtended(final Length position) // XXX: DJ
940 {
941 return getLocationExtendedSI(position.getSI());
942 }
943
944 /**
945 * Get the location at a position on the line, with its direction. Position can be below 0 or more than the line length. In
946 * that case, the position will be extrapolated in the direction of the line at its start or end.
947 * @param positionSI double; the position on the line for which to calculate the point on, before, of after the line, in SI
948 * units
949 * @return a directed point
950 */
951 public final synchronized DirectedPoint getLocationExtendedSI(final double positionSI) // XXX: DJ name + super(p)
952 {
953 makeLengthIndexedLine();
954 if (positionSI >= 0.0 && positionSI <= getLengthSI())
955 {
956 try
957 {
958 return getLocationSI(positionSI);
959 }
960 catch (OTSGeometryException exception)
961 {
962 // cannot happen
963 }
964 }
965
966 // position before start point -- extrapolate
967 if (positionSI < 0.0)
968 {
969 double len = positionSI;
970 double fraction = len / (this.lengthIndexedLine[1] - this.lengthIndexedLine[0]);
971 OTSPoint3D p1 = this.points[0];
972 OTSPoint3D p2 = this.points[1];
973 return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
974 p1.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
975 }
976
977 // position beyond end point -- extrapolate
978 int n1 = this.lengthIndexedLine.length - 1;
979 int n2 = this.lengthIndexedLine.length - 2;
980 double len = positionSI - getLengthSI();
981 double fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
982 while (Double.isInfinite(fraction))
983 {
984 if (--n2 < 0)
985 {
986 CategoryLogger.always().error("lengthIndexedLine of {} is invalid", this);
987 OTSPoint3D p = this.points[n1];
988 return new DirectedPoint(p.x, p.y, p.z, 0.0, 0.0, 0.0); // Bogus direction
989 }
990 fraction = len / (this.lengthIndexedLine[n1] - this.lengthIndexedLine[n2]);
991 }
992 OTSPoint3D p1 = this.points[n2];
993 OTSPoint3D p2 = this.points[n1];
994 return new DirectedPoint(p2.x + fraction * (p2.x - p1.x), p2.y + fraction * (p2.y - p1.y),
995 p2.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
996 }
997
998 /**
999 * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
1000 * @param fraction double; the fraction for which to calculate the point on the line
1001 * @return a directed point
1002 * @throws OTSGeometryException when fraction less than 0.0 or more than 1.0.
1003 */
1004 public final DirectedPoint getLocationFraction(final double fraction) throws OTSGeometryException
1005 {
1006 if (fraction < 0.0 || fraction > 1.0)
1007 {
1008 throw new OTSGeometryException("getLocationFraction for line: fraction < 0.0 or > 1.0. fraction = " + fraction);
1009 }
1010 return getLocationSI(fraction * getLengthSI());
1011 }
1012
1013 /**
1014 * Get the location at a fraction of the line, with its direction. Fraction should be between 0.0 and 1.0.
1015 * @param fraction double; the fraction for which to calculate the point on the line
1016 * @param tolerance double; the delta from 0.0 and 1.0 that will be forgiven
1017 * @return a directed point
1018 * @throws OTSGeometryException when fraction less than 0.0 or more than 1.0.
1019 */
1020 public final DirectedPoint getLocationFraction(final double fraction, final double tolerance) throws OTSGeometryException
1021 {
1022 if (fraction < -tolerance || fraction > 1.0 + tolerance)
1023 {
1024 throw new OTSGeometryException(
1025 "getLocationFraction for line: fraction < 0.0 - tolerance or > 1.0 + tolerance; fraction = " + fraction);
1026 }
1027 double f = fraction < 0 ? 0.0 : fraction > 1.0 ? 1.0 : fraction;
1028 return getLocationSI(f * getLengthSI());
1029 }
1030
1031 /**
1032 * Get the location at a fraction of the line (or outside the line), with its direction.
1033 * @param fraction double; the fraction for which to calculate the point on the line
1034 * @return a directed point
1035 */
1036 public final DirectedPoint getLocationFractionExtended(final double fraction)
1037 {
1038 return getLocationExtendedSI(fraction * getLengthSI());
1039 }
1040
1041 /**
1042 * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
1043 * @param position Length; the position on the line for which to calculate the point on the line
1044 * @return a directed point
1045 * @throws OTSGeometryException when position less than 0.0 or more than line length.
1046 */
1047 public final DirectedPoint getLocation(final Length position) throws OTSGeometryException // XXX: DJ
1048 {
1049 return getLocationSI(position.getSI());
1050 }
1051
1052 /**
1053 * Binary search for a position on the line.
1054 * @param pos double; the position to look for.
1055 * @return the index below the position; the position is between points[index] and points[index+1]
1056 * @throws OTSGeometryException when index could not be found
1057 */
1058 private int find(final double pos) throws OTSGeometryException
1059 {
1060 if (pos == 0)
1061 {
1062 return 0;
1063 }
1064
1065 int lo = 0;
1066 int hi = this.lengthIndexedLine.length - 1;
1067 while (lo <= hi)
1068 {
1069 if (hi == lo)
1070 {
1071 return lo;
1072 }
1073 int mid = lo + (hi - lo) / 2;
1074 if (pos < this.lengthIndexedLine[mid])
1075 {
1076 hi = mid - 1;
1077 }
1078 else if (pos > this.lengthIndexedLine[mid + 1])
1079 {
1080 lo = mid + 1;
1081 }
1082 else
1083 {
1084 return mid;
1085 }
1086 }
1087 throw new OTSGeometryException(
1088 "Could not find position " + pos + " on line with length indexes: " + Arrays.toString(this.lengthIndexedLine));
1089 }
1090
1091 /**
1092 * Get the location at a position on the line, with its direction. Position should be between 0.0 and line length.
1093 * @param positionSI double; the position on the line for which to calculate the point on the line
1094 * @return a directed point
1095 * @throws OTSGeometryException when position less than 0.0 or more than line length.
1096 */
1097 public final synchronized DirectedPoint getLocationSI(final double positionSI) throws OTSGeometryException // XXX: DJ
1098 // without SI
1099 {
1100 makeLengthIndexedLine();
1101 if (positionSI < 0.0 || positionSI > getLengthSI())
1102 {
1103 throw new OTSGeometryException("getLocationSI for line: position < 0.0 or > line length. Position = " + positionSI
1104 + " m. Length = " + getLengthSI() + " m.");
1105 }
1106
1107 // handle special cases: position == 0.0, or position == length
1108 if (positionSI == 0.0)
1109 {
1110 OTSPoint3D p1 = this.points[0];
1111 OTSPoint3D p2 = this.points[1];
1112 return new DirectedPoint(p1.x, p1.y, p1.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1113 }
1114 if (positionSI == getLengthSI())
1115 {
1116 OTSPoint3D p1 = this.points[this.points.length - 2];
1117 OTSPoint3D p2 = this.points[this.points.length - 1];
1118 return new DirectedPoint(p2.x, p2.y, p2.z, 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1119 }
1120
1121 // find the index of the line segment, use binary search
1122 int index = find(positionSI);
1123 double remainder = positionSI - this.lengthIndexedLine[index];
1124 double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
1125 OTSPoint3D p1 = this.points[index];
1126 OTSPoint3D p2 = this.points[index + 1];
1127 return new DirectedPoint(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
1128 p1.z + fraction * (p2.z - p1.z), 0.0, 0.0, Math.atan2(p2.y - p1.y, p2.x - p1.x));
1129 }
1130
1131 /**
1132 * Truncate a line at the given length (less than the length of the line, and larger than zero) and return a new line.
1133 * @param lengthSI double; the location where to truncate the line
1134 * @return a new OTSLine3D truncated at the exact position where line.getLength() == lengthSI
1135 * @throws OTSGeometryException when position less than 0.0 or more than line length.
1136 */
1137 public final synchronized OTSLine3D truncate(final double lengthSI) throws OTSGeometryException
1138 {
1139 makeLengthIndexedLine();
1140 if (lengthSI <= 0.0 || lengthSI > getLengthSI())
1141 {
1142 throw new OTSGeometryException("truncate for line: position <= 0.0 or > line length. Position = " + lengthSI
1143 + " m. Length = " + getLengthSI() + " m.");
1144 }
1145
1146 // handle special case: position == length
1147 if (lengthSI == getLengthSI())
1148 {
1149 return new OTSLine3D(getPoints());
1150 }
1151
1152 // find the index of the line segment
1153 int index = find(lengthSI);
1154 double remainder = lengthSI - this.lengthIndexedLine[index];
1155 double fraction = remainder / (this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index]);
1156 OTSPoint3D p1 = this.points[index];
1157 OTSPoint3D lastPoint;
1158 if (0.0 == fraction)
1159 {
1160 index--;
1161 lastPoint = p1;
1162 }
1163 else
1164 {
1165 OTSPoint3D p2 = this.points[index + 1];
1166 lastPoint = new OTSPoint3D(p1.x + fraction * (p2.x - p1.x), p1.y + fraction * (p2.y - p1.y),
1167 p1.z + fraction * (p2.z - p1.z));
1168
1169 }
1170 OTSPoint3DTSPoint3D.html#OTSPoint3D">OTSPoint3D[] coords = new OTSPoint3D[index + 2];
1171 for (int i = 0; i <= index; i++)
1172 {
1173 coords[i] = this.points[i];
1174 }
1175 coords[index + 1] = lastPoint;
1176 return new OTSLine3D(coords);
1177 }
1178
1179 /*-
1180 * TODO finish this method if it is needed; remove otherwise.
1181 * Calculate the first point on this line that intersects somewhere with the provided line, or NaN if no intersection was
1182 * found.
1183 * @param line the line to test the intersection with
1184 * @return the fraction of the first intersection point
1185 *
1186 public final double firstIntersectionFraction(final OTSLine3D line)
1187 {
1188 List<Line2D.Double> segs = new ArrayList<>();
1189 for (int j = 1; j < line.getPoints().length; j++)
1190 {
1191 Line2D.Double seg =
1192 new Line2D.Double(this.points[j - 1].x, this.points[j - 1].y, this.points[j].x, this.points[j].y);
1193 segs.add(seg);
1194
1195 }
1196 for (int i = 1; i < this.points.length; i++)
1197 {
1198 Line2D.Double thisSeg =
1199 new Line2D.Double(this.points[i - 1].x, this.points[i - 1].y, this.points[i].x, this.points[i].y);
1200 for (Line2D.Double seg : segs)
1201 {
1202 if (thisSeg.intersectsLine(seg))
1203 {
1204 // Point2D.Double intersectionPoint = thisSeg.
1205
1206 }
1207 }
1208 }
1209 return Double.NaN;
1210 }
1211 */
1212
1213 /**
1214 * Returns the fractional position along this line of the orthogonal projection of point (x, y) on this line. If the point
1215 * is not orthogonal to the closest line segment, the nearest point is selected.
1216 * @param x double; x-coordinate of point to project
1217 * @param y double; y-coordinate of point to project
1218 * @return fractional position along this line of the orthogonal projection on this line of a point
1219 */
1220 public final synchronized double projectOrthogonal(final double x, final double y)
1221 {
1222
1223 // prepare
1224 makeLengthIndexedLine();
1225 double minDistance = Double.POSITIVE_INFINITY;
1226 double minSegmentFraction = 0;
1227 int minSegment = -1;
1228
1229 // code based on Line2D.ptSegDistSq(...)
1230 for (int i = 0; i < size() - 1; i++)
1231 {
1232 double dx = this.points[i + 1].x - this.points[i].x;
1233 double dy = this.points[i + 1].y - this.points[i].y;
1234 // vector relative to (x(i), y(i))
1235 double px = x - this.points[i].x;
1236 double py = y - this.points[i].y;
1237 // dot product
1238 double dot1 = px * dx + py * dy;
1239 double f;
1240 double distance;
1241 if (dot1 > 0)
1242 {
1243 // vector relative to (x(i+1), y(i+1))
1244 px = dx - px;
1245 py = dy - py;
1246 // dot product
1247 double dot2 = px * dx + py * dy;
1248 if (dot2 > 0)
1249 {
1250 // projection on line segment
1251 double len2 = dx * dx + dy * dy;
1252 double proj = dot2 * dot2 / len2;
1253 f = dot1 / len2;
1254 distance = px * px + py * py - proj;
1255 }
1256 else
1257 {
1258 // dot<=0 projection 'after' line segment
1259 f = 1;
1260 distance = px * px + py * py;
1261 }
1262 }
1263 else
1264 {
1265 // dot<=0 projection 'before' line segment
1266 f = 0;
1267 distance = px * px + py * py;
1268 }
1269 // check if closer than previous
1270 if (distance < minDistance)
1271 {
1272 minDistance = distance;
1273 minSegmentFraction = f;
1274 minSegment = i;
1275 }
1276 }
1277
1278 // return
1279 double segLen = this.lengthIndexedLine[minSegment + 1] - this.lengthIndexedLine[minSegment];
1280 return (this.lengthIndexedLine[minSegment] + segLen * minSegmentFraction) / getLengthSI();
1281
1282 }
1283
1284 /**
1285 * Returns the fractional projection of a point to a line. The projection works by taking slices in space per line segment
1286 * as shown below. A point is always projected to the nearest segment, but not necessarily to the closest point on that
1287 * segment. The slices in space are analogous to a Voronoi diagram, but for the line segments instead of points. If
1288 * fractional projection fails, the orthogonal projection is returned.<br>
1289 * <br>
1290 * The point 'A' is projected to point 'B' on the 3rd segment of line 'C-D'. The line from 'A' to 'B' extends towards point
1291 * 'E', which is the intersection of lines 'E-F' and 'E-G'. Line 'E-F' cuts the first bend of the 3rd segment (at point 'H')
1292 * in half, while the line 'E-G' cuts the second bend of the 3rd segment (at point 'I') in half.
1293 *
1294 * <pre>
1295 * ____________________________ G .
1296 * . | | . .
1297 * . | . . . . helper lines | . .
1298 * . | _.._.._ projection line | I. .
1299 * . |____________________________| _.'._ . L
1300 * F. _.' . '-. .
1301 * .. B _.' . '-.
1302 * . . _.\ . . D
1303 * . . _.' : . .
1304 * J . . _.' \ . .
1305 * .. . _.' : . M
1306 * . . ..-' \ .
1307 * . . /H. A .
1308 * . . / . .
1309 * C _________/ . .
1310 * . . . .
1311 * K . . . .
1312 * . . . .
1313 * . . . . N
1314 * . . . .
1315 * . . . .
1316 * . . . .
1317 * . . . .
1318 * . .E
1319 * . .
1320 * . .
1321 * . .
1322 * </pre>
1323 *
1324 * Fractional projection may fail in three cases.
1325 * <ol>
1326 * <li>Numerical difficulties at slight bend, orthogonal projection returns the correct point.</li>
1327 * <li>Fractional projection is possible only to segments that aren't the nearest segment(s).</li>
1328 * <li>Fractional projection is possible for no segment.</li>
1329 * </ol>
1330 * In the latter two cases the projection is undefined and a orthogonal projection is returned if
1331 * {@code orthoFallback = true}, or {@code NaN} if {@code orthoFallback = false}.
1332 * @param start Direction; direction in first point
1333 * @param end Direction; direction in last point
1334 * @param x double; x-coordinate of point to project
1335 * @param y double; y-coordinate of point to project
1336 * @param fallback FractionalFallback; fallback method for when fractional projection fails
1337 * @return fractional position along this line of the fractional projection on that line of a point
1338 */
1339 public final synchronized double projectFractional(final Direction start, final Direction end, final double x,
1340 final double y, final FractionalFallback fallback) // XXX: DJ
1341 {
1342
1343 // prepare
1344 makeLengthIndexedLine();
1345 double minDistance = Double.POSITIVE_INFINITY;
1346 double minSegmentFraction = 0;
1347 int minSegment = -1;
1348 OTSPoint3Dy/OTSPoint3D.html#OTSPoint3D">OTSPoint3D point = new OTSPoint3D(x, y);
1349
1350 // determine helpers (centers and directions)
1351 determineFractionalHelpers(start, end);
1352
1353 // get distance of point to each segment
1354 double[] d = new double[this.points.length - 1];
1355 double minD = Double.POSITIVE_INFINITY;
1356 for (int i = 0; i < this.points.length - 1; i++)
1357 {
1358 d[i] = Line2D.ptSegDist(this.points[i].x, this.points[i].y, this.points[i + 1].x, this.points[i + 1].y, x, y);
1359 minD = d[i] < minD ? d[i] : minD;
1360 }
1361
1362 // loop over segments for projection
1363 double distance;
1364 for (int i = 0; i < this.points.length - 1; i++)
1365 {
1366 // skip if not the closest segment, note that often two segments are equally close in their shared end point
1367 if (d[i] > minD + FRAC_PROJ_PRECISION)
1368 {
1369 continue;
1370 }
1371 OTSPoint3D center = this.fractionalHelperCenters[i];
1372 OTSPoint3D p;
1373 if (center != null)
1374 {
1375 // get intersection of line "center - (x, y)" and the segment
1376 p = OTSPoint3D.intersectionOfLines(center, point, this.points[i], this.points[i + 1]);
1377 if (p == null || (x < center.x + FRAC_PROJ_PRECISION && center.x + FRAC_PROJ_PRECISION < p.x)
1378 || (x > center.x - FRAC_PROJ_PRECISION && center.x - FRAC_PROJ_PRECISION > p.x)
1379 || (y < center.y + FRAC_PROJ_PRECISION && center.y + FRAC_PROJ_PRECISION < p.y)
1380 || (y > center.y - FRAC_PROJ_PRECISION && center.y - FRAC_PROJ_PRECISION > p.y))
1381 {
1382 // projected point may not be 'beyond' segment center (i.e. center may not be between (x, y) and (p.x, p.y)
1383 continue;
1384 }
1385 }
1386 else
1387 {
1388 // parallel helper lines, project along direction
1389 OTSPoint3D offsetPoint =
1390 new OTSPoint3D(x + this.fractionalHelperDirections[i].x, y + this.fractionalHelperDirections[i].y);
1391 p = OTSPoint3D.intersectionOfLines(point, offsetPoint, this.points[i], this.points[i + 1]);
1392 }
1393 double segLength = this.points[i].distance(this.points[i + 1]).si + FRAC_PROJ_PRECISION;
1394 if (p == null || this.points[i].distance(p).si > segLength || this.points[i + 1].distance(p).si > segLength)
1395 {
1396 // intersection must be on the segment
1397 // in case of p == null, the length of the fractional helper direction falls away due to precision
1398 continue;
1399 }
1400 // distance from (x, y) to intersection on segment
1401 double dx = x - p.x;
1402 double dy = y - p.y;
1403 distance = Math.sqrt(dx * dx + dy * dy);
1404 // distance from start of segment to point on segment
1405 if (distance < minDistance)
1406 {
1407 dx = p.x - this.points[i].x;
1408 dy = p.y - this.points[i].y;
1409 double dFrac = Math.sqrt(dx * dx + dy * dy);
1410 // fraction to point on segment
1411 minDistance = distance;
1412 minSegmentFraction = dFrac / (this.lengthIndexedLine[i + 1] - this.lengthIndexedLine[i]);
1413 minSegment = i;
1414 }
1415 }
1416
1417 // return
1418 if (minSegment == -1)
1419
1420 {
1421 /*
1422 * If fractional projection fails (x, y) is either outside of the applicable area for fractional projection, or is
1423 * inside an area where numerical difficulties arise (i.e. far away outside of very slight bend which is considered
1424 * parallel).
1425 */
1426 // CategoryLogger.info(Cat.CORE, "projectFractional failed to project " + point + " on " + this
1427 // + "; using fallback approach");
1428 return fallback.getFraction(this, x, y);
1429 }
1430
1431 double segLen = this.lengthIndexedLine[minSegment + 1] - this.lengthIndexedLine[minSegment];
1432 return (this.lengthIndexedLine[minSegment] + segLen * minSegmentFraction) / getLengthSI();
1433
1434 }
1435
1436 /**
1437 * Fallback method for when fractional projection fails as the point is beyond the line or from numerical limitations.
1438 * <p>
1439 * Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
1440 * <br>
1441 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
1442 * <p>
1443 * @version $Revision$, $LastChangedDate$, by $Author$, initial version 18 apr. 2018 <br>
1444 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
1445 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
1446 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
1447 */
1448 public enum FractionalFallback // XXX: DJ
1449 {
1450 /** Orthogonal projection. */
1451 ORTHOGONAL
1452 {
1453 @Override
1454 double getFraction(final OTSLine3D line, final double x, final double y)
1455 {
1456 return line.projectOrthogonal(x, y);
1457 }
1458 },
1459
1460 /** Distance to nearest end point. */
1461 ENDPOINT
1462 {
1463 @Override
1464 double getFraction(final OTSLine3D line, final double x, final double y)
1465 {
1466 OTSPoint3Dy/OTSPoint3D.html#OTSPoint3D">OTSPoint3D point = new OTSPoint3D(x, y);
1467 double dStart = point.distanceSI(line.getFirst());
1468 double dEnd = point.distanceSI(line.getLast());
1469 if (dStart < dEnd)
1470 {
1471 return -dStart / line.getLengthSI();
1472 }
1473 else
1474 {
1475 return (dEnd + line.getLengthSI()) / line.getLengthSI();
1476 }
1477 }
1478 },
1479
1480 /** NaN value. */
1481 NaN
1482 {
1483 @Override
1484 double getFraction(final OTSLine3D line, final double x, final double y)
1485 {
1486 return Double.NaN;
1487 }
1488 };
1489
1490 /**
1491 * Returns fraction for when fractional projection fails as the point is beyond the line or from numerical limitations.
1492 * @param line OTSLine3D; line
1493 * @param x double; x coordinate of point
1494 * @param y double; y coordinate of point
1495 * @return double; fraction for when fractional projection fails
1496 */
1497 abstract double getFraction(OTSLine3D line, double x, double y);
1498
1499 }
1500
1501 /**
1502 * Determines all helpers (points and/or directions) for fractional projection and stores fixed information in properties
1503 * while returning the first and last center points (.
1504 * @param start Direction; direction in first point
1505 * @param end Direction; direction in last point
1506 */
1507 private synchronized void determineFractionalHelpers(final Direction start, final Direction end) // XXX: DJ
1508 {
1509
1510 final int n = this.points.length - 1;
1511
1512 // calculate fixed helpers if not done yet
1513 if (this.fractionalHelperCenters == null)
1514 {
1515 this.fractionalHelperCenters = new OTSPoint3D[n];
1516 this.fractionalHelperDirections = new Point2D.Double[n];
1517 if (this.points.length > 2)
1518 {
1519 // intersection of parallel lines of first and second segment
1520 OTSLine3D prevOfsSeg = unitOffsetSegment(0);
1521 OTSLine3D nextOfsSeg = unitOffsetSegment(1);
1522 OTSPoint3D parStartPoint;
1523 try
1524 {
1525 parStartPoint = OTSPoint3D.intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0),
1526 nextOfsSeg.get(1));
1527 if (parStartPoint == null || prevOfsSeg.get(1).distanceSI(nextOfsSeg.get(0)) < Math
1528 .min(prevOfsSeg.get(1).distanceSI(parStartPoint), nextOfsSeg.get(0).distanceSI(parStartPoint)))
1529 {
1530 parStartPoint = new OTSPoint3D((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
1531 (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
1532 }
1533 }
1534 catch (OTSGeometryException oge)
1535 {
1536 // cannot happen as only the first and second point (which are always present) are requested
1537 throw new RuntimeException(oge);
1538 }
1539 // remember the intersection of the first two unit offset segments
1540 this.firstOffsetIntersection = parStartPoint;
1541 // loop segments
1542 for (int i = 1; i < this.points.length - 2; i++)
1543 {
1544 prevOfsSeg = nextOfsSeg;
1545 nextOfsSeg = unitOffsetSegment(i + 1);
1546 OTSPoint3D parEndPoint;
1547 try
1548 {
1549 parEndPoint = OTSPoint3D.intersectionOfLines(prevOfsSeg.get(0), prevOfsSeg.get(1), nextOfsSeg.get(0),
1550 nextOfsSeg.get(1));
1551 if (parEndPoint == null || prevOfsSeg.get(1).distanceSI(nextOfsSeg.get(0)) < Math
1552 .min(prevOfsSeg.get(1).distanceSI(parEndPoint), nextOfsSeg.get(0).distanceSI(parEndPoint)))
1553 {
1554 parEndPoint = new OTSPoint3D((prevOfsSeg.get(1).x + nextOfsSeg.get(0).x) / 2,
1555 (prevOfsSeg.get(1).y + nextOfsSeg.get(0).y) / 2);
1556 }
1557 }
1558 catch (OTSGeometryException oge)
1559 {
1560 // cannot happen as only the first and second point (which are always present) are requested
1561 throw new RuntimeException(oge);
1562 }
1563 // center = intersections of helper lines
1564 this.fractionalHelperCenters[i] =
1565 OTSPoint3D.intersectionOfLines(this.points[i], parStartPoint, this.points[i + 1], parEndPoint);
1566 if (this.fractionalHelperCenters[i] == null)
1567 {
1568 // parallel helper lines, parallel segments or /\/ cause parallel helper lines, use direction
1569 this.fractionalHelperDirections[i] =
1570 new Point2D.Double(parStartPoint.x - this.points[i].x, parStartPoint.y - this.points[i].y);
1571 }
1572 parStartPoint = parEndPoint;
1573 }
1574 // remember the intersection of the last two unit offset segments
1575 this.lastOffsetIntersection = parStartPoint;
1576 }
1577 }
1578
1579 // use directions at start and end to get unit offset points to the left at a distance of 1
1580 double ang = (start == null ? Math.atan2(this.points[1].y - this.points[0].y, this.points[1].x - this.points[0].x)
1581 : start.getInUnit(DirectionUnit.DEFAULT)) + Math.PI / 2; // start.si + Math.PI / 2;
1582 OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D p1 = new OTSPoint3D(this.points[0].x + Math.cos(ang), this.points[0].y + Math.sin(ang));
1583 ang = (end == null ? Math.atan2(this.points[n].y - this.points[n - 1].y, this.points[n].x - this.points[n - 1].x)
1584 : end.getInUnit(DirectionUnit.DEFAULT)) + Math.PI / 2; // end.si + Math.PI / 2;
1585 OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D p2 = new OTSPoint3D(this.points[n].x + Math.cos(ang), this.points[n].y + Math.sin(ang));
1586
1587 // calculate first and last center (i.e. intersection of unit offset segments), which depend on inputs 'start' and 'end'
1588 if (this.points.length > 2)
1589 {
1590 this.fractionalHelperCenters[0] =
1591 OTSPoint3D.intersectionOfLines(this.points[0], p1, this.points[1], this.firstOffsetIntersection);
1592 this.fractionalHelperCenters[n - 1] =
1593 OTSPoint3D.intersectionOfLines(this.points[n - 1], this.lastOffsetIntersection, this.points[n], p2);
1594 if (this.fractionalHelperCenters[n - 1] == null)
1595 {
1596 // parallel helper lines, use direction for projection
1597 this.fractionalHelperDirections[n - 1] = new Point2D.Double(p2.x - this.points[n].x, p2.y - this.points[n].y);
1598 }
1599 }
1600 else
1601 {
1602 // only a single segment
1603 this.fractionalHelperCenters[0] = OTSPoint3D.intersectionOfLines(this.points[0], p1, this.points[1], p2);
1604 }
1605 if (this.fractionalHelperCenters[0] == null)
1606 {
1607 // parallel helper lines, use direction for projection
1608 this.fractionalHelperDirections[0] = new Point2D.Double(p1.x - this.points[0].x, p1.y - this.points[0].y);
1609 }
1610
1611 }
1612
1613 /**
1614 * Helper method for fractional projection which returns an offset line to the left of a segment at a distance of 1.
1615 * @param segment int; segment number
1616 * @return parallel line to the left of a segment at a distance of 1
1617 */
1618 private synchronized OTSLine3D unitOffsetSegment(final int segment) // XXX: DJ
1619 {
1620
1621 // double angle = Math.atan2(this.points[segment + 1].y - this.points[segment].y,
1622 // this.points[segment + 1].x - this.points[segment].x) + Math.PI / 2;
1623 // while (angle > Math.PI)
1624 // {
1625 // angle -= Math.PI;
1626 // }
1627 // while (angle < -Math.PI)
1628 // {
1629 // angle += Math.PI;
1630 // }
1631 // OTSPoint3D from = new OTSPoint3D(this.points[segment].x + Math.cos(angle), this.points[segment].y + Math.sin(angle));
1632 // OTSPoint3D to =
1633 // new OTSPoint3D(this.points[segment + 1].x + Math.cos(angle), this.points[segment + 1].y + Math.sin(angle));
1634 // try
1635 // {
1636 // return new OTSLine3D(from, to);
1637 // }
1638 // catch (OTSGeometryException oge)
1639 // {
1640 // // cannot happen as points are from this OTSLine3D which performed the same checks and 2 points are given
1641 // throw new RuntimeException(oge);
1642 // }
1643 OTSPoint3Dry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D from = new OTSPoint3D(this.points[segment].x, this.points[segment].y);
1644 OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D to = new OTSPoint3D(this.points[segment + 1].x, this.points[segment + 1].y);
1645 try
1646 {
1647 OTSLine3Dtry/OTSLine3D.html#OTSLine3D">OTSLine3D line = new OTSLine3D(from, to);
1648 return line.offsetLine(1.0);
1649 }
1650 catch (OTSGeometryException oge)
1651 {
1652 // Cannot happen as points are from this OTSLine3D which performed the same checks and 2 points are given
1653 throw new RuntimeException(oge);
1654 }
1655 }
1656
1657 /**
1658 * Returns the projected directional radius of the line at a given fraction. Negative values reflect right-hand curvature in
1659 * the design-line direction. The radius is taken as the minimum of the radii at the vertices before and after the given
1660 * fraction. The radius at a vertex is calculated as the radius of a circle that is equidistant from both edges connected to
1661 * the vertex. The circle center is on a line perpendicular to the shortest edge, crossing through the middle of the
1662 * shortest edge. This method ignores Z components.
1663 * @param fraction double; fraction along the line, between 0.0 and 1.0 (both inclusive)
1664 * @return Length; radius; the local radius; or si field set to Double.NaN if line is totally straight
1665 * @throws OTSGeometryException fraction out of bounds
1666 */
1667 public synchronized Length getProjectedRadius(final double fraction) throws OTSGeometryException // XXX: DJ
1668 {
1669 Throw.when(fraction < 0.0 || fraction > 1.0, OTSGeometryException.class, "Fraction %f is out of bounds [0.0 ... 1.0]",
1670 fraction);
1671 if (this.vertexRadii == null)
1672 {
1673 this.vertexRadii = new Length[size() - 1];
1674 }
1675 int index = find(fraction * getLength().si);
1676 if (index > 0 && this.vertexRadii[index] == null)
1677 {
1678 this.vertexRadii[index] = getProjectedVertexRadius(index);
1679 }
1680 if (index < size() - 2 && this.vertexRadii[index + 1] == null)
1681 {
1682 this.vertexRadii[index + 1] = getProjectedVertexRadius(index + 1);
1683 }
1684 if (index == 0)
1685 {
1686 if (this.vertexRadii.length < 2)
1687 {
1688 return Length.instantiateSI(Double.NaN);
1689 }
1690 return this.vertexRadii[1];
1691 }
1692 if (index == size() - 2)
1693 {
1694 return this.vertexRadii[size() - 2];
1695 }
1696 return Math.abs(this.vertexRadii[index].si) < Math.abs(this.vertexRadii[index + 1].si) ? this.vertexRadii[index]
1697 : this.vertexRadii[index + 1];
1698 }
1699
1700 /**
1701 * Calculates the directional radius at a vertex. Negative values reflect right-hand curvature in the design-line direction.
1702 * The radius at a vertex is calculated as the radius of a circle that is equidistant from both edges connected to the
1703 * vertex. The circle center is on a line perpendicular to the shortest edge, crossing through the middle of the shortest
1704 * edge. This function ignores Z components.
1705 * @param index int; index of the vertex in range [1 ... size() - 2]
1706 * @return Length; radius at the vertex
1707 * @throws OTSGeometryException if the index is out of bounds
1708 */
1709 public synchronized Length getProjectedVertexRadius(final int index) throws OTSGeometryException // XXX: DJ
1710 {
1711 Throw.when(index < 1 || index > size() - 2, OTSGeometryException.class, "Index %d is out of bounds [1 ... size() - 2].",
1712 index);
1713 makeLengthIndexedLine();
1714 determineFractionalHelpers(null, null);
1715 double length1 = this.lengthIndexedLine[index] - this.lengthIndexedLine[index - 1];
1716 double length2 = this.lengthIndexedLine[index + 1] - this.lengthIndexedLine[index];
1717 int shortIndex = length1 < length2 ? index : index + 1;
1718 // center of shortest edge
1719 OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D p1 = new OTSPoint3D(.5 * (this.points[shortIndex - 1].x + this.points[shortIndex].x),
1720 .5 * (this.points[shortIndex - 1].y + this.points[shortIndex].y),
1721 .5 * (this.points[shortIndex - 1].z + this.points[shortIndex].z));
1722 // perpendicular to shortest edge, line crossing p1
1723 OTSPoint3Detry/OTSPoint3D.html#OTSPoint3D">OTSPoint3D p2 = new OTSPoint3D(p1.x + (this.points[shortIndex].y - this.points[shortIndex - 1].y),
1724 p1.y - (this.points[shortIndex].x - this.points[shortIndex - 1].x), p1.z);
1725 // vertex
1726 OTSPoint3D p3 = this.points[index];
1727 // point on line that splits angle between edges at vertex 50-50
1728 OTSPoint3D p4 = this.fractionalHelperCenters[index];
1729 if (p4 == null)
1730 {
1731 // parallel helper lines
1732 p4 = new OTSPoint3D(p3.x + this.fractionalHelperDirections[index].x,
1733 p3.y + this.fractionalHelperDirections[index].y);
1734 }
1735 OTSPoint3D intersection = OTSPoint3D.intersectionOfLines(p1, p2, p3, p4);
1736 if (null == intersection)
1737 {
1738 return Length.instantiateSI(Double.NaN);
1739 }
1740 // determine left or right
1741 double refLength = length1 < length2 ? length1 : length2;
1742 Length radius = intersection.horizontalDistance(p1);
1743 Length i2p2 = intersection.horizontalDistance(p2);
1744 if (radius.si < i2p2.si && i2p2.si > refLength)
1745 {
1746 // left as p1 is closer than p2 (which was placed to the right) and not on the perpendicular line
1747 return radius;
1748 }
1749 // right as not left
1750 return radius.neg();
1751 }
1752
1753 /**
1754 * Returns the length fraction at the vertex.
1755 * @param index int; index of vertex [0 ... size() - 1]
1756 * @return double; length fraction at the vertex
1757 * @throws OTSGeometryException if the index is out of bounds
1758 */
1759 public synchronized double getVertexFraction(final int index) throws OTSGeometryException // XXX: DJ
1760 {
1761 Throw.when(index < 0 || index > size() - 1, OTSGeometryException.class, "Index %d is out of bounds [0 %d].", index,
1762 size() - 1);
1763 makeLengthIndexedLine();
1764 return this.lengthIndexedLine[index] / getLengthSI();
1765 }
1766
1767 /**
1768 * Calculate the centroid of this line, and the bounds, and cache for later use.
1769 */
1770 private synchronized void calcCentroidBounds()
1771 {
1772 double minX = Double.POSITIVE_INFINITY;
1773 double minY = Double.POSITIVE_INFINITY;
1774 double minZ = Double.POSITIVE_INFINITY;
1775 double maxX = Double.NEGATIVE_INFINITY;
1776 double maxY = Double.NEGATIVE_INFINITY;
1777 double maxZ = Double.NEGATIVE_INFINITY;
1778 for (OTSPoint3D p : this.points)
1779 {
1780 minX = Math.min(minX, p.x);
1781 minY = Math.min(minY, p.y);
1782 minZ = Math.min(minZ, p.z);
1783 maxX = Math.max(maxX, p.x);
1784 maxY = Math.max(maxY, p.y);
1785 maxZ = Math.max(maxZ, p.z);
1786 }
1787 this.centroid = new OTSPoint3D((maxX + minX) / 2, (maxY + minY) / 2, (maxZ + minZ) / 2);
1788 double deltaX = maxX - minX;
1789 double deltaY = maxY - minY;
1790 double deltaZ = maxZ - minZ;
1791 this.bounds = new Bounds(new Point3d(-deltaX / 2.0, -deltaY / 2.0, -deltaZ / 2.0),
1792 new Point3d(deltaX / 2, deltaY / 2, deltaZ / 2));
1793 this.envelope = new Envelope(minX, maxX, minY, maxY);
1794 }
1795
1796 /**
1797 * Retrieve the centroid of this OTSLine3D.
1798 * @return OTSPoint3D; the centroid of this OTSLine3D
1799 */
1800 public final synchronized OTSPoint3D getCentroid()
1801 {
1802 if (this.centroid == null)
1803 {
1804 calcCentroidBounds();
1805 }
1806 return this.centroid;
1807 }
1808
1809 /**
1810 * Get the bounding rectangle of this OTSLine3D.
1811 * @return Rectangle2D; the bounding rectangle of this OTSLine3D
1812 */
1813 public final synchronized Envelope getEnvelope() // XXX: DJ uses BoundingRectangle
1814 {
1815 if (this.envelope == null)
1816 {
1817 calcCentroidBounds();
1818 }
1819 return this.envelope;
1820 }
1821
1822 /** {@inheritDoc} */
1823 @Override
1824 @SuppressWarnings("checkstyle:designforextension")
1825 public synchronized DirectedPoint getLocation() // XXX: DJ
1826 {
1827 if (this.centroid == null)
1828 {
1829 calcCentroidBounds();
1830 }
1831 return this.centroid.getDirectedPoint();
1832 }
1833
1834 /** {@inheritDoc} */
1835 @Override
1836 @SuppressWarnings("checkstyle:designforextension")
1837 public synchronized Bounds getBounds() // XXX: DJ
1838 {
1839 if (this.bounds == null)
1840 {
1841 calcCentroidBounds();
1842 }
1843 return this.bounds;
1844 }
1845
1846 /** {@inheritDoc} */
1847 @Override
1848 @SuppressWarnings("checkstyle:designforextension")
1849 public String toString()
1850 {
1851 return Arrays.toString(this.points);
1852 }
1853
1854 /** {@inheritDoc} */
1855 @Override
1856 @SuppressWarnings("checkstyle:designforextension")
1857 public int hashCode()
1858 {
1859 final int prime = 31;
1860 int result = 1;
1861 result = prime * result + Arrays.hashCode(this.points);
1862 return result;
1863 }
1864
1865 /** {@inheritDoc} */
1866 @Override
1867 @SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
1868 public boolean equals(final Object obj)
1869 {
1870 if (this == obj)
1871 return true;
1872 if (obj == null)
1873 return false;
1874 if (getClass() != obj.getClass())
1875 return false;
1876 OTSLine3D/../../../org/opentrafficsim/core/geometry/OTSLine3D.html#OTSLine3D">OTSLine3D other = (OTSLine3D) obj;
1877 if (!Arrays.equals(this.points, other.points))
1878 return false;
1879 return true;
1880 }
1881
1882 /**
1883 * Convert the 2D projection of this OTSLine3D to something that MS-Excel can plot.
1884 * @return excel XY plottable output
1885 */
1886 public final String toExcel()
1887 {
1888 StringBuffer s = new StringBuffer();
1889 for (OTSPoint3D p : this.points)
1890 {
1891 s.append(p.x + "\t" + p.y + "\n");
1892 }
1893 return s.toString();
1894 }
1895
1896 /**
1897 * Convert the 2D projection of this OTSLine3D to Peter's plot format.
1898 * @return Peter's format plot output
1899 */
1900 public final String toPlot() // XXX: DJ
1901 {
1902 StringBuffer result = new StringBuffer();
1903 for (OTSPoint3D p : this.points)
1904 {
1905 result.append(String.format(Locale.US, "%s%.3f,%.3f", 0 == result.length() ? "M" : " L", p.x, p.y));
1906 }
1907 result.append("\n");
1908 return result.toString();
1909 }
1910
1911 }