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