OTSOffsetLinePK.java
package org.opentrafficsim.core.geometry;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Peter Knoppers' attempt to implement offsetLine.
* <p>
* Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
* <p>
* @version $Revision$, $LastChangedDate$, by $Author$, initial version Dec 1, 2015 <br>
* @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
*/
public final class OTSOffsetLinePK
{
/** This class should never be instantiated. */
private OTSOffsetLinePK()
{
// Cannot be instantiated.
}
/** Debugging flag. */
static boolean debugOffsetLine = false;
/** Precision of approximation of arcs in the offsetLine method. */
static double circlePrecision = 0.001;
/** Noise in the reference line less than this value is always filtered. */
static double offsetMinimumFilterValue = 0.001;
/** Noise in the reference line greater than this value is never filtered. */
static double offsetMaximumFilterValue = 0.1;
/**
* Noise in the reference line less than <cite>offset / offsetFilterRatio</cite> is filtered except when the resulting value
* exceeds <cite>offsetMaximumFilterValue</cite>.
*/
static double offsetFilterRatio = 10;
/**
* Construct an offset line.
* @param referenceLine OTSLine3D; the reference line
* @param offset double; the offset; positive values indicate left of the reference line, negative values indicate right of
* the reference line
* @return OTSLine3D; a line at the specified offset from the reference line
* @throws OTSGeometryException when this method runs into major trouble and cannot produce a decent result
*/
public static OTSLine3D offsetLine(final OTSLine3D referenceLine, final double offset) throws OTSGeometryException
{
// if (referenceLine.size() > 1 && referenceLine.getFirst().horizontalDistanceSI(new OTSPoint3D(-200.376, -111.999)) <
// 0.1
// && referenceLine.get(1).horizontalDistanceSI(new OTSPoint3D(-204.098, -100.180)) < 0.1 && Math.abs(offset) > 1)
// if (referenceLine.size() > 1 && referenceLine.getFirst().horizontalDistanceSI(new OTSPoint3D(-177.580, -169.726)) <
// 0.1
// && referenceLine.get(1).horizontalDistanceSI(new OTSPoint3D(-179.028, -166.084)) < 0.1 && Math.abs(offset) > 1
// && referenceLine.size() == 0)
// {
// debugOffsetLine = true;
// for (int i = 0; i < referenceLine.size(); i++)
// {
// System.out.println(String.format(
// Locale.US,
// "point %2d: %20s,%20s%s",
// i,
// referenceLine.get(i).x,
// referenceLine.get(i).y,
// (i == 0 ? "" : " ("
// + Math.toDegrees(Math.atan2(referenceLine.get(i).y - referenceLine.get(i - 1).y,
// referenceLine.get(i).x - referenceLine.get(i - 1).x)) + ")")));
// }
// System.out.println("# offset is " + offset);
// System.out.println(OTSGeometry.printCoordinates("#reference:\nc0,0,0\n#", referenceLine, "\n "));
// }
// else
// {
// debugOffsetLine = false;
// }
double bufferOffset = Math.abs(offset);
final double precision = 0.00001;
if (bufferOffset < precision)
{
// squash the Z-coordinate
List<OTSPoint3D> coordinates = new ArrayList<>(referenceLine.size());
for (int i = 0; i < referenceLine.size(); i++)
{
OTSPoint3D coordinate = referenceLine.get(i);
coordinates.add(new OTSPoint3D(coordinate.x, coordinate.y));
}
return OTSLine3D.createAndCleanOTSLine3D(coordinates);
}
OTSLine3D filteredReferenceLine = referenceLine.noiseFilteredLine(
Math.max(offsetMinimumFilterValue, Math.min(bufferOffset / offsetFilterRatio, offsetMaximumFilterValue)));
if (debugOffsetLine)
{
System.out.println("#filtered reference line ");
System.out.println(filteredReferenceLine.toPlot());
}
List<OTSPoint3D> tempPoints = new ArrayList<>();
// Make good use of the fact that an OTSLine3D cannot have consecutive duplicate points and has > 1 points
OTSPoint3D prevPoint = filteredReferenceLine.get(0);
Double prevAngle = null;
for (int index = 0; index < filteredReferenceLine.size() - 1; index++)
{
OTSPoint3D nextPoint = filteredReferenceLine.get(index + 1);
double angle = Math.atan2(nextPoint.y - prevPoint.y, nextPoint.x - prevPoint.x);
if (debugOffsetLine)
{
System.out.println("#reference segment " + index + " from " + prevPoint + " to " + nextPoint + " angle "
+ Math.toDegrees(angle));
}
OTSPoint3D segmentFrom =
new OTSPoint3D(prevPoint.x - Math.sin(angle) * offset, prevPoint.y + Math.cos(angle) * offset);
OTSPoint3D segmentTo =
new OTSPoint3D(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;
}
OTSPoint3D 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;
}
OTSPoint3D intermediatePoint = new OTSPoint3D(prevPoint.x - Math.sin(intermediateAngle) * offset,
prevPoint.y + Math.cos(intermediateAngle) * offset);
// Find any intersection points of the new segment and all previous segments
OTSPoint3D prevSegFrom = null;
int stopAt = tempPoints.size();
for (int i = 0; i < stopAt; i++)
{
OTSPoint3D prevSegTo = tempPoints.get(i);
if (null != prevSegFrom)
{
OTSPoint3D prevSegIntersection = OTSPoint3D.intersectionOfLineSegments(prevArcPoint,
intermediatePoint, prevSegFrom, prevSegTo);
if (null != prevSegIntersection
&& prevSegIntersection.horizontalDistanceSI(prevArcPoint) > circlePrecision
&& prevSegIntersection.horizontalDistanceSI(prevSegFrom) > circlePrecision
&& prevSegIntersection.horizontalDistanceSI(prevSegTo) > circlePrecision)
{
if (debugOffsetLine)
{
System.out.println("#inserting intersection in arc segment " + prevSegIntersection);
}
tempPoints.add(prevSegIntersection);
// System.out.println(new OTSLine3D(tempPoints).toPlot());
}
}
prevSegFrom = prevSegTo;
}
OTSPoint3D nextSegmentIntersection =
OTSPoint3D.intersectionOfLineSegments(prevSegFrom, intermediatePoint, segmentFrom, segmentTo);
if (null != nextSegmentIntersection)
{
if (debugOffsetLine)
{
System.out.println(
"#inserting intersection of arc segment with next segment " + nextSegmentIntersection);
}
tempPoints.add(nextSegmentIntersection);
// System.out.println(new OTSLine3D(tempPoints).toPlot());
}
if (debugOffsetLine)
{
System.out.println("#inserting arc point " + intermediatePoint + " for angle "
+ Math.toDegrees(intermediateAngle));
}
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
OTSPoint3D pPoint = null;
int currentSize = tempPoints.size(); // PK DO NOT use the "dynamic" limit
for (int i = 0; i < currentSize /* tempPoints.size() */; i++)
{
OTSPoint3D 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 (debugOffsetLine)
{
System.out.println("#preceding segment " + pPoint + " to " + p + " angle " + Math.toDegrees(pAngle)
+ ", next segment " + segmentFrom + " to " + segmentTo + " angle " + Math.toDegrees(angle)
+ " angleDifference " + Math.toDegrees(angleDifference));
}
if (Math.abs(angleDifference) > 0)// 0.01)
{
OTSPoint3D intersection = OTSPoint3D.intersectionOfLineSegments(pPoint, p, segmentFrom, segmentTo);
if (null != intersection)
{
if (tempPoints.size() - 1 == i)
{
if (debugOffsetLine)
{
System.out.println("#Replacing last point of preceding segment and "
+ "first point of next segment by their intersection " + intersection);
}
tempPoints.remove(tempPoints.size() - 1);
segmentFrom = intersection;
}
else
{
if (debugOffsetLine)
{
if (tempPoints.size() > 17)
{
System.out.println("#not good");
System.out.println(new OTSLine3D(tempPoints).toPlot());
}
System.out.println("#Adding intersection of preceding segment and " + "next segment "
+ intersection);
}
tempPoints.add(intersection);
}
// tempPoints.set(tempPoints.size() - 1, intermediatePoint);
if (tempPoints.size() > 1)
{
// System.out.println(new OTSLine3D(tempPoints).toPlot());
}
}
}
else
{
if (debugOffsetLine)
{
System.out.println("#Not adding intersection of preceding segment and this segment "
+ "(angle too small)");
}
if (i == tempPoints.size() - 1)
{
if (debugOffsetLine)
{
System.out.println("#Not adding segment");
}
addSegment = false;
}
}
}
pPoint = p;
}
}
if (addSegment)
{
tempPoints.add(segmentFrom);
tempPoints.add(segmentTo);
if (debugOffsetLine)
{
System.out.println("#Added segmentFrom " + segmentFrom + " and segmentTo " + segmentTo);
System.out.println(new OTSLine3D(tempPoints).toPlot());
}
prevPoint = nextPoint;
prevAngle = angle;
}
}
if (debugOffsetLine)
{
System.out.println("# before cleanup: \nc1,0,0\n");
if (tempPoints.size() > 0)
{
OTSPoint3D p = tempPoints.get(0);
System.out.println(String.format(Locale.US, "M %.3f,%.3f", p.x, p.y));
for (int i = 1; i < tempPoints.size(); i++)
{
p = tempPoints.get(i);
System.out.println(String.format(Locale.US, "L %.3f,%.3f", p.x, p.y));
}
}
}
// Remove points that are closer than the specified offset
for (int index = 1; index < tempPoints.size() - 1; index++)
{
OTSPoint3D checkPoint = tempPoints.get(index);
prevPoint = null;
boolean tooClose = false;
boolean somewhereAtCorrectDistance = false;
for (int i = 0; i < filteredReferenceLine.size(); i++)
{
OTSPoint3D p = filteredReferenceLine.get(i);
if (null != prevPoint)
{
OTSPoint3D closestPoint = checkPoint.closestPointOnSegment(prevPoint, p);
double distance = closestPoint.horizontalDistanceSI(checkPoint);
if (distance < bufferOffset - circlePrecision)
{
if (debugOffsetLine)
{
System.out.print("#point " + checkPoint + " inside buffer (distance is " + distance + ") ");
}
tooClose = true;
break;
}
else if (distance < bufferOffset + precision)
{
somewhereAtCorrectDistance = true;
}
}
prevPoint = p;
}
if (tooClose || !somewhereAtCorrectDistance)
{
if (debugOffsetLine)
{
System.out.println("#Removing " + checkPoint);
}
tempPoints.remove(index);
index--;
}
}
if (debugOffsetLine)
{
System.out.println("#after cleanup " + tempPoints.size() + " points left");
System.out.println(new OTSLine3D(tempPoints).toPlot());
}
// Fix the z-coordinate of all points that were added as intersections of segments.
for (int index = 0; index < tempPoints.size(); index++)
{
OTSPoint3D p = tempPoints.get(index);
if (Double.isNaN(p.z))
{
tempPoints.set(index, new OTSPoint3D(p.x, p.y, 0));
}
}
try
{
return OTSLine3D.createAndCleanOTSLine3D(tempPoints);
}
catch (OTSGeometryException exception)
{
exception.printStackTrace();
}
return null;
}
}