package mil.nga.giat.geowave.analytic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import mil.nga.giat.geowave.analytic.clustering.NeighborData;
import mil.nga.giat.geowave.analytic.distance.DistanceFn;
import mil.nga.giat.geowave.core.index.FloatCompareUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.math.util.MathUtils;
import org.apache.commons.math3.geometry.Vector;
import org.apache.commons.math3.geometry.euclidean.twod.Euclidean2D;
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.algorithm.ConvexHull;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.operation.union.UnaryUnionOp;
/**
*
* Set of algorithms to mere hulls and increase the gradient of convexity over
* hulls.
*
*/
public class GeometryHullTool
{
protected static final Logger LOGGER = LoggerFactory.getLogger(GeometryHullTool.class);
DistanceFn<Coordinate> distanceFnForCoordinate;
double concaveThreshold = 1.8;
public void connect(
final List<Geometry> geometries ) {
}
public DistanceFn<Coordinate> getDistanceFnForCoordinate() {
return distanceFnForCoordinate;
}
public void setDistanceFnForCoordinate(
final DistanceFn<Coordinate> distanceFnForCoordinate ) {
this.distanceFnForCoordinate = distanceFnForCoordinate;
}
protected double getConcaveThreshold() {
return concaveThreshold;
}
/*
* Set the threshold for the concave algorithm
*/
protected void setConcaveThreshold(
final double concaveThreshold ) {
this.concaveThreshold = concaveThreshold;
}
protected static class Edge implements
Comparable<Edge>
{
Coordinate start;
Coordinate end;
double distance;
Edge next, last;
private TreeSet<NeighborData<Coordinate>> points = null;
public Edge(
final Coordinate start,
final Coordinate end,
final double distance ) {
super();
this.start = start;
this.end = end;
this.distance = distance;
}
public TreeSet<NeighborData<Coordinate>> getPoints() {
if (points == null) {
points = new TreeSet<NeighborData<Coordinate>>();
}
return points;
}
@Override
public int compareTo(
final Edge edge ) {
return (distance - edge.distance) > 0 ? 1 : -1;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = (prime * result) + ((end == null) ? 0 : end.hashCode());
result = (prime * result) + ((start == null) ? 0 : start.hashCode());
return result;
}
public void connectLast(
final Edge last ) {
this.last = last;
last.next = this;
}
@Override
public boolean equals(
final Object obj ) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Edge other = (Edge) obj;
if (end == null) {
if (other.end != null) {
return false;
}
}
else if (!end.equals(other.end)) {
return false;
}
if (start == null) {
if (other.start != null) {
return false;
}
}
else if (!start.equals(other.start)) {
return false;
}
return true;
}
@Override
public String toString() {
return "Edge [start=" + start + ", end=" + end + ", distance=" + distance + "]";
}
}
private Edge createEdgeWithSideEffects(
final Coordinate start,
final Coordinate end,
final Set<Coordinate> innerPoints,
final TreeSet<Edge> edges ) {
final Edge newEdge = new Edge(
start,
end,
distanceFnForCoordinate.measure(
start,
end));
innerPoints.remove(newEdge.start);
innerPoints.remove(newEdge.end);
edges.add(newEdge);
return newEdge;
}
/*
* Generate a concave hull, if possible, given a geometry and a set of
* additional points.
*
* @param fast expedite processing allowing for some outliers.
*/
public Geometry createHullFromGeometry(
Geometry clusterGeometry,
Collection<Coordinate> additionalPoints,
boolean fast ) {
if (additionalPoints.isEmpty()) return clusterGeometry;
final Set<Coordinate> batchCoords = new HashSet<Coordinate>();
if (clusterGeometry != null) {
for (final Coordinate coordinate : clusterGeometry.getCoordinates()) {
batchCoords.add(coordinate);
}
}
for (final Coordinate coordinate : additionalPoints) {
batchCoords.add(coordinate);
}
GeometryFactory factory = clusterGeometry == null ? new GeometryFactory() : clusterGeometry.getFactory();
final Coordinate[] actualCoords = batchCoords.toArray(new Coordinate[batchCoords.size()]);
if (batchCoords.size() == 2) {
return factory.createLineString(actualCoords);
}
final ConvexHull convexHull = new ConvexHull(
actualCoords,
factory);
final Geometry convexHullGeo = convexHull.getConvexHull();
try {
// does this shape benefit from concave hulling?
// it cannot be a line string
if (batchCoords.size() > 5 && convexHullGeo.getArea() > 0.0) {
final Geometry concaveHull = fast ? concaveHull(
convexHullGeo,
batchCoords) : this.concaveHullParkOhMethod(
convexHullGeo,
batchCoords);
if (fast && !concaveHull.isSimple()) {
LOGGER.warn(
"Produced non simple hull",
concaveHull.toText());
return this.concaveHullParkOhMethod(
convexHullGeo,
batchCoords);
}
return concaveHull;
}
else {
return convexHullGeo;
}
}
catch (final Exception ex) {
/*
* Geometry[] points = new Geometry[actualCoords.length + 1]; for
* (int i = 0; i < actualCoords.length; i++) points[i] =
* hull.getFactory().createPoint( actualCoords[i]);
* points[points.length - 1] = hull; try { ShapefileTool.writeShape(
* "test_perf_xh", new File( "./targettest_perf_xh"), points); }
* catch (IOException e) { e.printStackTrace(); }
*/
LOGGER.error(
"Failed to compute hull",
ex);
return convexHullGeo;
}
}
/**
*
* Gift unwrapping (e.g. dig) concept, taking a convex hull and a set of
* inner points, add inner points to the hull without violating hull
* invariants--all points must reside on the hull or inside the hull. Based
* on: Jin-Seo Park and Se-Jong Oh.
* "A New Concave Algorithm and Concaveness Measure for n-dimensional Datasets"
* . Department of Nanobiomedical Science. Dankook University". 2010.
*
* Per the paper, N = concaveThreshold
*
* @param geometry
* @param providedInnerPoints
* @return
*/
public Geometry concaveHullParkOhMethod(
final Geometry geometry,
final Collection<Coordinate> providedInnerPoints ) {
final Set<Coordinate> innerPoints = new HashSet<Coordinate>(
providedInnerPoints);
final TreeSet<Edge> edges = new TreeSet<Edge>();
final Coordinate[] geoCoordinateList = geometry.getCoordinates();
final int s = geoCoordinateList.length - 1;
final Edge firstEdge = createEdgeWithSideEffects(
geoCoordinateList[0],
geoCoordinateList[1],
innerPoints,
edges);
Edge lastEdge = firstEdge;
for (int i = 1; i < s; i++) {
final Edge newEdge = createEdgeWithSideEffects(
geoCoordinateList[i],
geoCoordinateList[i + 1],
innerPoints,
edges);
newEdge.connectLast(lastEdge);
lastEdge = newEdge;
}
firstEdge.connectLast(lastEdge);
while (!edges.isEmpty() && !innerPoints.isEmpty()) {
final Edge edge = edges.pollLast();
lastEdge = edge;
double score = Double.MAX_VALUE;
Coordinate selectedCandidate = null;
for (final Coordinate candidate : innerPoints) {
final double dist = calcDistance(
edge.start,
edge.end,
candidate);
// on the hull
if (MathUtils.equals(
dist,
0.0,
0.000000001)) {
score = 0.0;
selectedCandidate = candidate;
break;
}
if ((dist > 0) && (dist < score)) {
score = dist;
selectedCandidate = candidate;
}
}
if (selectedCandidate == null) {
continue;
}
// if one a line segment of the hull, then remove candidate
if (FloatCompareUtils.checkDoublesEqual(
score,
0.0)) {
innerPoints.remove(selectedCandidate);
edges.add(edge);
continue;
}
// Park and Oh look only at the neighbor edges
// but this fails in some cases.
if (isCandidateCloserToAnotherEdge(
score,
edge,
edges,
selectedCandidate)) {
continue;
}
innerPoints.remove(selectedCandidate);
final double eh = edge.distance;
final double startToCandidate = distanceFnForCoordinate.measure(
edge.start,
selectedCandidate);
final double endToCandidate = distanceFnForCoordinate.measure(
edge.end,
selectedCandidate);
final double min = Math.min(
startToCandidate,
endToCandidate);
// protected against duplicates
if ((eh / min) > concaveThreshold) {
final Edge newEdge1 = new Edge(
edge.start,
selectedCandidate,
startToCandidate);
final Edge newEdge2 = new Edge(
selectedCandidate,
edge.end,
endToCandidate);
// need to replace this with something more intelligent. This
// occurs in cases of sharp angles. An angular approach may also
// work
// look for an angle to flip in the reverse direction.
if (!intersectAnotherEdge(
newEdge1,
edge) && !intersectAnotherEdge(
newEdge2,
edge) && !intersectAnotherEdge(
newEdge1,
edge.last) && !intersectAnotherEdge(
newEdge2,
edge.next)) {
edges.add(newEdge2);
edges.add(newEdge1);
newEdge1.connectLast(edge.last);
newEdge2.connectLast(newEdge1);
edge.next.connectLast(newEdge2);
lastEdge = newEdge1;
}
}
}
return geometry.getFactory().createPolygon(
reassemble(lastEdge));
}
/**
*
* Gift unwrapping (e.g. dig) concept, taking a convex hull and a set of
* inner points, add inner points to the hull without violating hull
* invariants--all points must reside on the hull or inside the hull. Based
* on: Jin-Seo Park and Se-Jong Oh.
* "A New Concave Algorithm and Concaveness Measure for n-dimensional Datasets"
* . Department of Nanobiomedical Science. Dankook University". 2010.
*
* Per the paper, N = concaveThreshold.
*
* This algorithm evaluates remarkably faster than Park and Oh, but the
* quality of the result is marginally less. If it is acceptable to have
* some small number of points fall outside of the hull and speed is
* critical, use this method. The measure of error is difficult to calculate
* since it is not directly calculated based on the number of inner points.
* Rather, the measure is based on some number of points in proximity the
* optimal concave hull.
*
*
* @param geometry
* @param providedInnerPoints
* @return
*/
public Geometry concaveHull(
final Geometry geometry,
final Collection<Coordinate> providedInnerPoints ) {
final Set<Coordinate> innerPoints = (providedInnerPoints instanceof Set) ? (Set<Coordinate>) providedInnerPoints
: new HashSet<Coordinate>(
providedInnerPoints);
final TreeSet<Edge> edges = new TreeSet<Edge>();
final Coordinate[] geoCoordinateList = geometry.getCoordinates();
final int s = geoCoordinateList.length - 1;
final Edge firstEdge = createEdgeWithSideEffects(
geoCoordinateList[0],
geoCoordinateList[1],
innerPoints,
edges);
Edge lastEdge = firstEdge;
for (int i = 1; i < s; i++) {
final Edge newEdge = createEdgeWithSideEffects(
geoCoordinateList[i],
geoCoordinateList[i + 1],
innerPoints,
edges);
newEdge.connectLast(lastEdge);
lastEdge = newEdge;
}
firstEdge.connectLast(lastEdge);
for (final Coordinate candidate : innerPoints) {
double min = Double.MAX_VALUE;
Edge bestEdge = null;
for (final Edge edge : edges) {
final double dist = calcDistance(
edge.start,
edge.end,
candidate);
if ((dist > 0) && (dist < min)) {
min = dist;
bestEdge = edge;
}
}
if (bestEdge != null) {
bestEdge.getPoints().add(
new NeighborData<Coordinate>(
candidate,
null,
min));
}
}
while (!edges.isEmpty()) {
final Edge edge = edges.pollLast();
lastEdge = edge;
NeighborData<Coordinate> candidate = edge.getPoints().pollFirst();
while (candidate != null) {
if (!MathUtils.equals(
candidate.getDistance(),
0.0,
0.000000001)) {
final Coordinate selectedCandidate = candidate.getElement();
final double eh = edge.distance;
final double startToCandidate = distanceFnForCoordinate.measure(
edge.start,
selectedCandidate);
final double endToCandidate = distanceFnForCoordinate.measure(
edge.end,
selectedCandidate);
final double min = Math.min(
startToCandidate,
endToCandidate);
// protected against duplicates
if ((eh / min) > concaveThreshold) {
final Edge newEdge1 = new Edge(
edge.start,
selectedCandidate,
startToCandidate);
final Edge newEdge2 = new Edge(
selectedCandidate,
edge.end,
endToCandidate);
edges.add(newEdge2);
edges.add(newEdge1);
newEdge1.connectLast(edge.last);
newEdge2.connectLast(newEdge1);
edge.next.connectLast(newEdge2);
lastEdge = newEdge1;
for (final NeighborData<Coordinate> otherPoint : edge.getPoints()) {
final double[] distProfile1 = calcDistanceSegment(
newEdge1.start,
newEdge1.end,
otherPoint.getElement());
final double[] distProfile2 = calcDistanceSegment(
newEdge2.start,
newEdge2.end,
otherPoint.getElement());
if (distProfile1[0] >= 0.0 && distProfile1[0] <= 1.0) {
if (distProfile1[0] < 0.0 || distProfile1[0] > 1.0 || distProfile2[1] > distProfile1[1]) {
otherPoint.setDistance(distProfile1[1]);
newEdge1.getPoints().add(
otherPoint);
}
else {
otherPoint.setDistance(distProfile2[1]);
newEdge2.getPoints().add(
otherPoint);
}
}
else if (distProfile2[0] >= 0.0 && distProfile2[0] <= 1.0) {
otherPoint.setDistance(distProfile2[1]);
newEdge2.getPoints().add(
otherPoint);
}
}
edge.getPoints().clear(); // forces this loop to end
}
}
candidate = edge.getPoints().pollFirst();
}
}
return geometry.getFactory().createPolygon(
reassemble(lastEdge));
}
public static boolean intersectAnotherEdge(
final Edge newEdge,
final Edge edgeToReplace ) {
Edge nextEdge = edgeToReplace.next.next;
final Edge stopEdge = edgeToReplace.last;
while (nextEdge != stopEdge) {
if (edgesIntersect(
newEdge,
nextEdge)) {
return true;
}
nextEdge = nextEdge.next;
}
return false;
}
public static boolean edgesIntersect(
final Edge e1,
final Edge e2 ) {
return CGAlgorithms.distanceLineLine(
e1.start,
e1.end,
e2.start,
e2.end) <= 0.0;
}
private static boolean isCandidateCloserToAnotherEdge(
final double distanceToBeat,
final Edge selectedEdgeToBeat,
final Collection<Edge> edges,
final Coordinate selectedCandidate ) {
for (final Edge edge : edges) {
if (selectedEdgeToBeat.equals(edge)) {
continue;
}
final double dist = calcDistance(
edge.start,
edge.end,
selectedCandidate);
if ((dist >= 0.0) && (dist < distanceToBeat)) {
return true;
}
}
return false;
}
private static Coordinate[] reassemble(
final Edge lastEdge ) {
final List<Coordinate> coordinates = new ArrayList<Coordinate>();
coordinates.add(lastEdge.start);
Edge nextEdge = lastEdge.next;
while (nextEdge != lastEdge) {
coordinates.add(nextEdge.start);
nextEdge = nextEdge.next;
}
coordinates.add(lastEdge.start);
return coordinates.toArray(new Coordinate[coordinates.size()]);
}
protected boolean isInside(
final Coordinate coor,
final Coordinate[] hullCoordinates ) {
double maxAngle = 0;
for (int i = 1; i < hullCoordinates.length; i++) {
final Coordinate hullCoordinate = hullCoordinates[i];
maxAngle = Math.max(
calcAngle(
hullCoordinates[0],
coor,
hullCoordinate),
maxAngle);
}
// return 360 == Math.abs(maxAngle);
return (Math.abs(maxAngle) >= 359.999 && Math.abs(maxAngle) <= 360.0001);
}
/**
* Forms create edges between two shapes maintaining convexity.
*
* Does not currently work if the shapes intersect
*
* @param shape1
* @param shape2
* @return
*/
public Geometry connect(
final Geometry shape1,
final Geometry shape2 ) {
try {
if (shape1 instanceof Polygon && shape2 instanceof Polygon && !shape1.intersects(shape2)) return connect(
shape1,
shape2,
getClosestPoints(
shape1,
shape2,
distanceFnForCoordinate));
return UnaryUnionOp.union(Arrays.asList(
shape1,
shape2));
}
catch (Exception ex) {
LOGGER.warn(
"Exception caught in connect method",
ex);
}
return createHullFromGeometry(
shape1,
Arrays.asList(shape2.getCoordinates()),
false);
}
protected Geometry connect(
final Geometry shape1,
final Geometry shape2,
final Pair<Integer, Integer> closestCoordinates ) {
Coordinate[] leftCoords = shape1.getCoordinates(), rightCoords = shape2.getCoordinates();
int startLeft, startRight;
if ((leftCoords[closestCoordinates.getLeft()].x < rightCoords[closestCoordinates.getRight()].x)) {
startLeft = closestCoordinates.getLeft();
startRight = closestCoordinates.getRight();
}
else {
leftCoords = shape2.getCoordinates();
rightCoords = shape1.getCoordinates();
startLeft = closestCoordinates.getRight();
startRight = closestCoordinates.getLeft();
}
final HashSet<Coordinate> visitedSet = new HashSet<Coordinate>();
visitedSet.add(leftCoords[startLeft]);
visitedSet.add(rightCoords[startRight]);
final boolean leftClockwise = clockwise(leftCoords);
final boolean rightClockwise = clockwise(rightCoords);
final Pair<Integer, Integer> upperCoords = walk(
visitedSet,
leftCoords,
rightCoords,
startLeft,
startRight,
new DirectionFactory() {
@Override
public Direction createLeftFootDirection(
final int start,
final int max ) {
return leftClockwise ? new IncreaseDirection(
start,
max,
true) : new DecreaseDirection(
start,
max,
true);
}
@Override
public Direction createRightFootDirection(
final int start,
final int max ) {
return rightClockwise ? new DecreaseDirection(
start,
max,
false) : new IncreaseDirection(
start,
max,
false);
}
});
final Pair<Integer, Integer> lowerCoords = walk(
visitedSet,
leftCoords,
rightCoords,
startLeft,
startRight,
new DirectionFactory() {
@Override
public Direction createLeftFootDirection(
final int start,
final int max ) {
return leftClockwise ? new DecreaseDirection(
start,
max,
false) : new IncreaseDirection(
start,
max,
false);
}
@Override
public Direction createRightFootDirection(
final int start,
final int max ) {
return rightClockwise ? new IncreaseDirection(
start,
max,
true) : new DecreaseDirection(
start,
max,
true);
}
});
final List<Coordinate> newCoordinateSet = new ArrayList<Coordinate>();
final Direction leftSet = leftClockwise ? new IncreaseDirection(
upperCoords.getLeft(),
lowerCoords.getLeft() + 1,
leftCoords.length) : new DecreaseDirection(
upperCoords.getLeft(),
lowerCoords.getLeft() - 1,
leftCoords.length);
newCoordinateSet.add(leftCoords[upperCoords.getLeft()]);
while (leftSet.hasNext()) {
newCoordinateSet.add(leftCoords[leftSet.next()]);
}
final Direction rightSet = rightClockwise ? new IncreaseDirection(
lowerCoords.getRight(),
upperCoords.getRight() + 1,
rightCoords.length) : new DecreaseDirection(
lowerCoords.getRight(),
upperCoords.getRight() - 1,
rightCoords.length);
newCoordinateSet.add(rightCoords[lowerCoords.getRight()]);
while (rightSet.hasNext()) {
newCoordinateSet.add(rightCoords[rightSet.next()]);
}
newCoordinateSet.add(leftCoords[upperCoords.getLeft()]);
return shape1.getFactory().createPolygon(
newCoordinateSet.toArray(new Coordinate[newCoordinateSet.size()]));
}
private Pair<Integer, Integer> walk(
final Set<Coordinate> visited,
final Coordinate[] shape1Coords,
final Coordinate[] shape2Coords,
final int start1,
final int start2,
final DirectionFactory factory ) {
final int upPos = takeBiggestStep(
visited,
shape2Coords[start2],
shape1Coords,
factory.createLeftFootDirection(
start1,
shape1Coords.length));
// even if the left foot was stationary, try to move the right foot
final int downPos = takeBiggestStep(
visited,
shape1Coords[upPos],
shape2Coords,
factory.createRightFootDirection(
start2,
shape2Coords.length));
// if the right step moved, then see if another l/r step can be taken
if (downPos != start2) {
return walk(
visited,
shape1Coords,
shape2Coords,
upPos,
downPos,
factory);
}
return Pair.of(
upPos,
start2);
}
/**
* Determine if the polygon is defined clockwise
*
* @param set
* @return
*/
public static boolean clockwise(
final Coordinate[] set ) {
double sum = 0.0;
for (int i = 1; i < set.length; i++) {
sum += (set[i].x - set[i - 1].x) / (set[i].y + set[i - 1].y);
}
return sum > 0.0;
}
public static double calcSmallestAngle(
final Coordinate one,
final Coordinate vertex,
final Coordinate two ) {
final double angle = Math.abs(calcAngle(
one,
vertex,
two));
return (angle > 180.0) ? angle - 180.0 : angle;
}
/**
* Calculate the angle between two points and a given vertex
*
* @param one
* @param vertex
* @param two
* @return
*/
public static double calcAngle(
final Coordinate one,
final Coordinate vertex,
final Coordinate two ) {
final double p1x = one.x - vertex.x;
final double p1y = one.y - vertex.y;
final double p2x = two.x - vertex.x;
final double p2y = two.y - vertex.y;
final double angle1 = Math.toDegrees(Math.atan2(
p1y,
p1x));
final double angle2 = Math.toDegrees(Math.atan2(
p2y,
p2x));
return angle2 - angle1;
}
/**
* Calculate the distance between two points and a given vertex
*
* @param one
* @param vertex
* @param two
* @return array if doubles double[0] = length of the projection from start
* on the line containing the segment(start to end) double[1] =
* distance to the segment double[2] = distance to the line
* containing the segment(start to end)
*/
public static double[] calcDistanceSegment(
final Coordinate start,
final Coordinate end,
final Coordinate point ) {
final Vector<Euclidean2D> vOne = new Vector2D(
start.x,
start.y);
final Vector<Euclidean2D> vTwo = new Vector2D(
end.x,
end.y);
final Vector<Euclidean2D> vVertex = new Vector2D(
point.x,
point.y);
final Vector<Euclidean2D> E1 = vTwo.subtract(vOne);
final Vector<Euclidean2D> E2 = vVertex.subtract(vOne);
final double distOneTwo = E2.dotProduct(E1);
final double lengthVOneSq = E1.getNormSq();
final double projectionLength = distOneTwo / lengthVOneSq;
final Vector<Euclidean2D> projection = E1.scalarMultiply(
projectionLength).add(
vOne);
final double o = ((projectionLength < 0.0) ? vOne.distance(vVertex) : ((projectionLength > 1.0) ? vTwo
.distance(vVertex) : vVertex.distance(projection)));
return new double[] {
projectionLength,
o,
vVertex.distance(projection)
};
}
public static double calcDistance(
final Coordinate start,
final Coordinate end,
final Coordinate point ) {
double[] p = calcDistanceSegment(
start,
end,
point);
return (p[0] < 0.0 || p[0] > 1.0) ? -1 : p[1];
}
public static Pair<Integer, Integer> getClosestPoints(
final Geometry shape1,
final Geometry shape2,
final DistanceFn<Coordinate> distanceFnForCoordinate ) {
int bestShape1Position = 0;
int bestShape2Position = 0;
double minDist = Double.MAX_VALUE;
int pos1 = 0, pos2 = 0;
for (final Coordinate coord1 : shape1.getCoordinates()) {
pos2 = 0;
for (final Coordinate coord2 : shape2.getCoordinates()) {
final double dist = (distanceFnForCoordinate.measure(
coord1,
coord2));
if (dist < minDist) {
bestShape1Position = pos1;
bestShape2Position = pos2;
minDist = dist;
}
pos2++;
}
pos1++;
}
return Pair.of(
bestShape1Position,
bestShape2Position);
}
private int takeBiggestStep(
final Set<Coordinate> visited,
final Coordinate station,
final Coordinate[] shapeCoords,
final Direction legIncrement ) {
double angle = 0.0;
final Coordinate startPoint = shapeCoords[legIncrement.getStart()];
int last = legIncrement.getStart();
Coordinate lastCoordinate = shapeCoords[last];
while (legIncrement.hasNext()) {
final int pos = legIncrement.next();
// skip over duplicate (a ring or polygon has one duplicate)
if (shapeCoords[pos].equals(lastCoordinate)) {
continue;
}
lastCoordinate = shapeCoords[pos];
if (visited.contains(lastCoordinate)) {
break;
}
double currentAngle = legIncrement.angleChange(calcAngle(
startPoint,
station,
lastCoordinate));
currentAngle = currentAngle < -180 ? currentAngle + 360 : currentAngle;
if ((currentAngle >= angle) && (currentAngle < 180.0)) {
angle = currentAngle;
last = pos;
visited.add(shapeCoords[pos]);
}
else {
return last;
}
}
return last;
}
private interface DirectionFactory
{
Direction createLeftFootDirection(
int start,
int max );
Direction createRightFootDirection(
int start,
int max );
}
private interface Direction extends
Iterator<Integer>
{
public int getStart();
public double angleChange(
double angle );
}
private class IncreaseDirection implements
Direction
{
final int max;
final int start;
final int stop;
int current = 0;
final boolean angleIsNegative;
@Override
public int getStart() {
return start;
}
public IncreaseDirection(
final int start,
final int max,
final boolean angleIsNegative ) {
super();
this.max = max;
current = getNext(start);
stop = start;
this.start = start;
this.angleIsNegative = angleIsNegative;
}
public IncreaseDirection(
final int start,
final int stop,
final int max ) {
super();
this.max = max;
current = getNext(start);
this.stop = stop;
this.start = start;
angleIsNegative = true;
}
@Override
public Integer next() {
final int n = current;
current = getNext(current);
return n;
}
@Override
public boolean hasNext() {
return current != stop;
}
protected int getNext(
final int n ) {
return (n + 1) % max;
}
@Override
public void remove() {}
@Override
public double angleChange(
final double angle ) {
return angleIsNegative ? -angle : angle;
}
}
private class DecreaseDirection extends
IncreaseDirection implements
Direction
{
public DecreaseDirection(
final int start,
final int max,
final boolean angleIsNegative ) {
super(
start,
max,
angleIsNegative);
}
public DecreaseDirection(
final int start,
final int stop,
final int max ) {
super(
start,
stop,
max);
}
@Override
protected int getNext(
final int n ) {
return (n == 0) ? max - 1 : n - 1;
}
}
}