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