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