pointList = new ArrayList<>();
while (start > cumulativeLength)
{
Point2d fromPoint = get(index);
index++;
Point2d toPoint = get(index);
segmentLength = fromPoint.distance(toPoint);
cumulativeLength = nextCumulativeLength;
nextCumulativeLength = cumulativeLength + segmentLength;
if (nextCumulativeLength >= start)
{
break;
}
}
if (start == nextCumulativeLength)
{
pointList.add(get(index));
}
else
{
pointList.add(get(index - 1).interpolate(get(index), (start - cumulativeLength) / segmentLength));
if (end > nextCumulativeLength)
{
pointList.add(get(index));
}
}
while (end > nextCumulativeLength)
{
Point2d fromPoint = get(index);
index++;
if (index >= size())
{
break; // rounding error
}
Point2d toPoint = get(index);
segmentLength = fromPoint.distance(toPoint);
cumulativeLength = nextCumulativeLength;
nextCumulativeLength = cumulativeLength + segmentLength;
if (nextCumulativeLength >= end)
{
break;
}
pointList.add(toPoint);
}
if (end == nextCumulativeLength)
{
pointList.add(get(index));
}
else
{
Point2d point = get(index - 1).interpolate(get(index), (end - cumulativeLength) / segmentLength);
// can be the same due to rounding
if (!point.equals(pointList.get(pointList.size() - 1)))
{
pointList.add(point);
}
}
try
{
return instantiate(pointList);
}
catch (DrawRuntimeException exception)
{
CategoryLogger.always().error(exception, "interval " + start + ".." + end + " too short");
throw new DrawException("interval " + start + ".." + end + "too short");
}
}
/** {@inheritDoc} */
@Override
public PolyLine2d truncate(final double position) throws DrawException
{
if (position <= 0.0 || position > getLength())
{
throw new DrawException("truncate for line: position <= 0.0 or > line length. Position = " + position
+ ". Length = " + getLength() + " m.");
}
// handle special case: position == length
if (position == getLength())
{
return this;
}
// find the index of the line segment
int index = find(position);
double remainder = position - lengthAtIndex(index);
double fraction = remainder / (lengthAtIndex(index + 1) - lengthAtIndex(index));
Point2d p1 = get(index);
Point2d lastPoint;
if (0.0 == fraction)
{
index--;
lastPoint = p1;
}
else
{
Point2d p2 = get(index + 1);
lastPoint = p1.interpolate(p2, fraction);
}
// FIXME: Cannot create a P[]; will have to do it with a List
List coords = new ArrayList<>(index + 2);
for (int i = 0; i <= index; i++)
{
coords.add(get(i));
}
coords.add(lastPoint);
return instantiate(coords);
}
/** Default precision of approximation of arcs in the offsetLine method. */
public static final double DEFAULT_CIRCLE_PRECISION = 0.001;
/** By default, noise in the reference line of the offsetLine method less than this value is always filtered. */
public static final double DEFAULT_OFFSET_MINIMUM_FILTER_VALUE = 0.001;
/** By default, noise in the reference line of the offsetLineMethod greater than this value is never filtered. */
public static final double DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE = 0.1;
/**
* By default, noise in the reference line of the offsetLineMethod less than offset / offsetFilterRatio is
* filtered except when the resulting value exceeds offsetMaximumFilterValue.
*/
public static final double DEFAULT_OFFSET_FILTER_RATIO = 10;
/** By default, the offsetLineMethod uses this offset precision. */
public static final double DEFAULT_OFFSET_PRECISION = 0.00001;
/**
* Construct an offset line. This is similar to what geographical specialists call buffering, except that this method only
* construct a new line on one side of the reference line and does not add half disks around the end points. This method
* tries to strike a delicate balance between generating too few and too many points to approximate arcs. Noise in
* this (the reference line) can cause major artifacts in the offset line. This method calls the underlying
* method with default values for circlePrecision (DEFAULT_OFFSET), offsetMinimumFilterValue
* (DEFAULT_OFFSET_MINIMUM_FILTER_VALUE), offsetMaximumFilterValue
* (DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE), offsetFilterRatio (DEFAULT_OFFSET_FILTER_RATIO),
* minimumOffset (DEFAULT_OFFSET_PRECISION).
* @param offset double; the offset; positive values indicate left of the reference line, negative values indicate right of
* the reference line
* @return PolyLine2d; a line at the specified offset from the reference line
*/
public PolyLine2d offsetLine(final double offset)
{
return offsetLine(offset, DEFAULT_CIRCLE_PRECISION, DEFAULT_OFFSET_MINIMUM_FILTER_VALUE,
DEFAULT_OFFSET_MAXIMUM_FILTER_VALUE, DEFAULT_OFFSET_FILTER_RATIO, DEFAULT_OFFSET_PRECISION);
}
/**
* Construct an offset line. This is similar to what geographical specialists call buffering, except that this method only
* construct a new line on one side of the reference line and does not add half disks around the end points. This method
* tries to strike a delicate balance between generating too few and too many points to approximate arcs. Noise in
* this (the reference line) can cause major artifacts in the offset line.
* @param offset double; the offset; positive values indicate left of the reference line, negative values indicate right of
* the reference line
* @param circlePrecision double; precision of approximation of arcs; the line segments that are used to approximate an arc
* will not deviate from the exact arc by more than this value
* @param offsetMinimumFilterValue double; noise in the reference line less than this value is always filtered
* @param offsetMaximumFilterValue double; noise in the reference line greater than this value is never filtered
* @param offsetFilterRatio double; noise in the reference line less than offset / offsetFilterRatio is
* filtered except when the resulting value exceeds offsetMaximumFilterValue
* @param minimumOffset double; an offset value less than this value is treated as 0.0
* @return PolyLine2d; a line at the specified offset from the reference line
* @throws IllegalArgumentException when offset is NaN, or circlePrecision, offsetMinimumFilterValue,
* offsetMaximumfilterValue, offsetFilterRatio, or minimumOffset is not positive, or NaN, or
* offsetMinimumFilterValue >= offsetMaximumFilterValue
*/
@SuppressWarnings("checkstyle:methodlength")
public PolyLine2d offsetLine(final double offset, final double circlePrecision, final double offsetMinimumFilterValue,
final double offsetMaximumFilterValue, final double offsetFilterRatio, final double minimumOffset)
throws IllegalArgumentException
{
Throw.when(Double.isNaN(offset), IllegalArgumentException.class, "Offset may not be NaN");
Throw.when(Double.isNaN(circlePrecision) || circlePrecision <= 0, IllegalArgumentException.class,
"bad circlePrecision");
Throw.when(Double.isNaN(offsetMinimumFilterValue) || offsetMinimumFilterValue <= 0, IllegalArgumentException.class,
"bad offsetMinimumFilterValue");
Throw.when(Double.isNaN(offsetMaximumFilterValue) || offsetMaximumFilterValue <= 0, IllegalArgumentException.class,
"bad offsetMaximumFilterValue");
Throw.when(Double.isNaN(offsetFilterRatio) || offsetFilterRatio <= 0, IllegalArgumentException.class,
"bad offsetFilterRatio");
Throw.when(Double.isNaN(minimumOffset) || minimumOffset <= 0, IllegalArgumentException.class, "bad minimumOffset");
Throw.when(offsetMinimumFilterValue >= offsetMaximumFilterValue, IllegalArgumentException.class,
"bad offset filter values; minimum must be less than maximum");
double bufferOffset = Math.abs(offset);
if (bufferOffset < minimumOffset)
{
return this;
}
PolyLine2d filteredReferenceLine = noiseFilteredLine(
Math.max(offsetMinimumFilterValue, Math.min(bufferOffset / offsetFilterRatio, offsetMaximumFilterValue)));
List tempPoints = new ArrayList<>();
// Make good use of the fact that an OTSLine3D cannot have consecutive duplicate points and has > 1 points
Point2d prevPoint = filteredReferenceLine.get(0);
Double prevAngle = null;
for (int index = 0; index < filteredReferenceLine.size() - 1; index++)
{
Point2d nextPoint = filteredReferenceLine.get(index + 1);
double angle = Math.atan2(nextPoint.y - prevPoint.y, nextPoint.x - prevPoint.x);
Point2d segmentFrom =
new Point2d(prevPoint.x - Math.sin(angle) * offset, prevPoint.y + Math.cos(angle) * offset);
Point2d segmentTo =
new Point2d(nextPoint.x - Math.sin(angle) * offset, nextPoint.y + Math.cos(angle) * offset);
boolean addSegment = true;
if (index > 0)
{
double deltaAngle = angle - prevAngle;
if (Math.abs(deltaAngle) > Math.PI)
{
deltaAngle -= Math.signum(deltaAngle) * 2 * Math.PI;
}
if (deltaAngle * offset <= 0)
{
// Outside of curve of reference line
// Approximate an arc using straight segments.
// Determine how many segments are needed.
int numSegments = 1;
if (Math.abs(deltaAngle) > Math.PI / 2)
{
numSegments = 2;
}
while (true)
{
double maxError = bufferOffset * (1 - Math.abs(Math.cos(deltaAngle / numSegments / 2)));
if (maxError < circlePrecision)
{
break; // required precision reached
}
numSegments *= 2;
}
Point2d prevArcPoint = tempPoints.get(tempPoints.size() - 1);
// Generate the intermediate points
for (int additionalPoint = 1; additionalPoint < numSegments; additionalPoint++)
{
double intermediateAngle =
(additionalPoint * angle + (numSegments - additionalPoint) * prevAngle) / numSegments;
if (prevAngle * angle < 0 && Math.abs(prevAngle) > Math.PI / 2 && Math.abs(angle) > Math.PI / 2)
{
intermediateAngle += Math.PI;
}
Point2d intermediatePoint = new Point2d(prevPoint.x - Math.sin(intermediateAngle) * offset,
prevPoint.y + Math.cos(intermediateAngle) * offset);
// Find any intersection points of the new segment and all previous segments
Point2d prevSegFrom = null;
int stopAt = tempPoints.size();
for (int i = 0; i < stopAt; i++)
{
Point2d prevSegTo = tempPoints.get(i);
if (null != prevSegFrom)
{
Point2d prevSegIntersection = Point2d.intersectionOfLineSegments(prevArcPoint,
intermediatePoint, prevSegFrom, prevSegTo);
if (null != prevSegIntersection && prevSegIntersection.distance(prevArcPoint) > circlePrecision
&& prevSegIntersection.distance(prevSegFrom) > circlePrecision
&& prevSegIntersection.distance(prevSegTo) > circlePrecision)
{
tempPoints.add(prevSegIntersection);
// System.out.println(new OTSLine3D(tempPoints).toPlot());
}
}
prevSegFrom = prevSegTo;
}
Point2d nextSegmentIntersection =
Point2d.intersectionOfLineSegments(prevSegFrom, intermediatePoint, segmentFrom, segmentTo);
if (null != nextSegmentIntersection)
{
tempPoints.add(nextSegmentIntersection);
// System.out.println(new OTSLine3D(tempPoints).toPlot());
}
tempPoints.add(intermediatePoint);
// System.out.println(new OTSLine3D(tempPoints).toPlot());
prevArcPoint = intermediatePoint;
}
}
// Inside of curve of reference line.
// Add the intersection point of each previous segment and the next segment
Point2d pPoint = null;
int currentSize = tempPoints.size(); // PK DO NOT use the "dynamic" limit
for (int i = 0; i < currentSize /* tempPoints.size() */; i++)
{
Point2d p = tempPoints.get(i);
if (null != pPoint)
{
double pAngle = Math.atan2(p.y - pPoint.y, p.x - pPoint.x);
double angleDifference = angle - pAngle;
if (Math.abs(angleDifference) > Math.PI)
{
angleDifference -= Math.signum(angleDifference) * 2 * Math.PI;
}
if (Math.abs(angleDifference) > 0)// 0.01)
{
Point2d intersection = Point2d.intersectionOfLineSegments(pPoint, p, segmentFrom, segmentTo);
if (null != intersection)
{
if (tempPoints.size() - 1 == i)
{
tempPoints.remove(tempPoints.size() - 1);
segmentFrom = intersection;
}
else
{
tempPoints.add(intersection);
}
}
}
else
{
// This is where things went very wrong in the TestGeometry demo.
if (i == tempPoints.size() - 1)
{
tempPoints.remove(tempPoints.size() - 1);
segmentFrom = tempPoints.get(tempPoints.size() - 1);
tempPoints.remove(tempPoints.size() - 1);
}
}
}
pPoint = p;
}
}
if (addSegment)
{
tempPoints.add(segmentFrom);
tempPoints.add(segmentTo);
prevPoint = nextPoint;
prevAngle = angle;
}
}
// Remove points that are closer than the specified offset
for (int index = 1; index < tempPoints.size() - 1; index++)
{
Point2d checkPoint = tempPoints.get(index);
prevPoint = null;
boolean tooClose = false;
boolean somewhereAtCorrectDistance = false;
for (int i = 0; i < filteredReferenceLine.size(); i++)
{
Point2d p = filteredReferenceLine.get(i);
if (null != prevPoint)
{
Point2d closestPoint = checkPoint.closestPointOnSegment(prevPoint, p);
double distance = closestPoint.distance(checkPoint);
if (distance < bufferOffset - circlePrecision)
{
tooClose = true;
break;
}
else if (distance < bufferOffset + minimumOffset)
{
somewhereAtCorrectDistance = true;
}
}
prevPoint = p;
}
if (tooClose || !somewhereAtCorrectDistance)
{
tempPoints.remove(index);
index--;
}
}
try
{
return PolyLine2d.createAndCleanPolyLine2d(tempPoints);
}
catch (DrawException exception)
{
exception.printStackTrace();
}
return null;
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "PolyLine2d [points=" + Arrays.toString(this.points) + "]";
}
/**
* Convert this PolyLine2d to something that MS-Excel can plot.
* @return excel XY plottable output
*/
public final String toExcel()
{
StringBuffer s = new StringBuffer();
for (Point2d p : this.points)
{
s.append(p.x + "\t" + p.y + "\n");
}
return s.toString();
}
/**
* Convert this PolyLine3D to Peter's plot format.
* @return Peter's format plot output
*/
public final String toPlot()
{
StringBuffer result = new StringBuffer();
for (Point2d p : this.points)
{
result.append(String.format(Locale.US, "%s%.3f,%.3f", 0 == result.length() ? "M" : " L", p.x, p.y));
}
result.append("\n");
return result.toString();
}
/** {@inheritDoc} */
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(this.points);
return result;
}
/** {@inheritDoc} */
@SuppressWarnings({"checkstyle:designforextension", "checkstyle:needbraces"})
@Override
public boolean equals(final Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PolyLine2d other = (PolyLine2d) obj;
if (!Arrays.equals(this.points, other.points))
return false;
return true;
}
}