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