org.apache.lucene.spatial3d.geom.GeoPolygonFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lucene-spatial3d Show documentation
Show all versions of lucene-spatial3d Show documentation
Apache Lucene (module: spatial3d)
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.spatial3d.geom;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
/**
* Class which constructs a GeoMembershipShape representing an arbitrary polygon.
*
* @lucene.experimental
*/
public class GeoPolygonFactory {
private GeoPolygonFactory() {
}
private static final int SMALL_POLYGON_CUTOFF_EDGES = 100;
/** Create a GeoConcavePolygon using the specified points. The polygon must have
* a maximum extent larger than PI. The siding of the polygon is chosen so that any
* adjacent point to a segment provides an exterior measurement and therefore,
* the polygon is a truly concave polygon. Note that this method should only be used when there is certainty
* that we are dealing with a concave polygon, e.g. the polygon has been serialized.
* If there is not such certainty, please refer to @{@link GeoPolygonFactory#makeGeoPolygon(PlanetModel, List)}.
* @param pointList is a list of the GeoPoints to build an arbitrary polygon out of.
* @return a GeoPolygon corresponding to what was specified.
*/
public static GeoPolygon makeGeoConcavePolygon(final PlanetModel planetModel,
final List pointList) {
return new GeoConcavePolygon(planetModel, pointList);
}
/** Create a GeoConvexPolygon using the specified points. The polygon must have
* a maximum extent no larger than PI. The siding of the polygon is chosen so that any adjacent
* point to a segment provides an interior measurement and therefore
* the polygon is a truly convex polygon. Note that this method should only be used when
* there is certainty that we are dealing with a convex polygon, e.g. the polygon has been serialized.
* If there is not such certainty, please refer to @{@link GeoPolygonFactory#makeGeoPolygon(PlanetModel, List)}.
* @param pointList is a list of the GeoPoints to build an arbitrary polygon out of.
* @return a GeoPolygon corresponding to what was specified.
*/
public static GeoPolygon makeGeoConvexPolygon(final PlanetModel planetModel,
final List pointList) {
return new GeoConvexPolygon(planetModel, pointList);
}
/** Create a GeoConcavePolygon using the specified points and holes. The polygon must have
* a maximum extent larger than PI. The siding of the polygon is chosen so that any adjacent
* point to a segment provides an exterior measurement and therefore
* the polygon is a truly concave polygon. Note that this method should only be used when
* there is certainty that we are dealing with a concave polygon, e.g. the polygon has been serialized.
* If there is not such certainty, please refer to {@link GeoPolygonFactory#makeGeoPolygon(PlanetModel, List, List)}.
* @param pointList is a list of the GeoPoints to build an arbitrary polygon out of.
* @param holes is a list of polygons representing "holes" in the outside polygon. Holes describe the area outside
* each hole as being "in set". Null == none.
* @return a GeoPolygon corresponding to what was specified.
*/
public static GeoPolygon makeGeoConcavePolygon(final PlanetModel planetModel,
final List pointList,
final List holes) {
return new GeoConcavePolygon(planetModel,pointList, holes);
}
/** Create a GeoConvexPolygon using the specified points and holes. The polygon must have
* a maximum extent no larger than PI. The siding of the polygon is chosen so that any adjacent
* point to a segment provides an interior measurement and therefore
* the polygon is a truly convex polygon. Note that this method should only be used when
* there is certainty that we are dealing with a convex polygon, e.g. the polygon has been serialized.
* If there is not such certainty, please refer to {@link GeoPolygonFactory#makeGeoPolygon(PlanetModel, List, List)}.
* @param pointList is a list of the GeoPoints to build an arbitrary polygon out of.
* @param holes is a list of polygons representing "holes" in the outside polygon. Holes describe the area outside
* each hole as being "in set". Null == none.
* @return a GeoPolygon corresponding to what was specified.
*/
public static GeoPolygon makeGeoConvexPolygon(final PlanetModel planetModel,
final List pointList,
final List holes) {
return new GeoConvexPolygon(planetModel,pointList, holes);
}
/** Use this class to specify a polygon with associated holes.
*/
public static class PolygonDescription {
/** The list of points */
public final List extends GeoPoint> points;
/** The list of holes */
public final List extends PolygonDescription> holes;
/** Instantiate the polygon description.
* @param points is the list of points.
*/
public PolygonDescription(final List extends GeoPoint> points) {
this(points, new ArrayList<>());
}
/** Instantiate the polygon description.
* @param points is the list of points.
* @param holes is the list of holes.
*/
public PolygonDescription(final List extends GeoPoint> points, final List extends PolygonDescription> holes) {
this.points = points;
this.holes = holes;
}
}
/** Create a GeoPolygon using the specified points and holes, using order to determine
* siding of the polygon. Much like ESRI, this method uses clockwise to indicate the space
* on the same side of the shape as being inside, and counter-clockwise to indicate the
* space on the opposite side as being inside.
* @param description describes the polygon and its associated holes. If points go
* clockwise from a given pole, then that pole should be within the polygon. If points go
* counter-clockwise, then that pole should be outside the polygon.
* @return a GeoPolygon corresponding to what was specified, or null if a valid polygon cannot be generated
* from this input.
*/
public static GeoPolygon makeGeoPolygon(final PlanetModel planetModel,
final PolygonDescription description) {
return makeGeoPolygon(planetModel, description, 0.0);
}
/** Create a GeoPolygon using the specified points and holes, using order to determine
* siding of the polygon. Much like ESRI, this method uses clockwise to indicate the space
* on the same side of the shape as being inside, and counter-clockwise to indicate the
* space on the opposite side as being inside.
* @param description describes the polygon and its associated holes. If points go
* clockwise from a given pole, then that pole should be within the polygon. If points go
* counter-clockwise, then that pole should be outside the polygon.
* @param leniencyValue is the maximum distance (in units) that a point can be from the plane and still be considered as
* belonging to the plane. Any value greater than zero may cause some of the provided points that are in fact outside
* the strict definition of co-planarity, but are within this distance, to be discarded for the purposes of creating a
* "safe" polygon.
* @return a GeoPolygon corresponding to what was specified, or null if a valid polygon cannot be generated
* from this input.
*/
public static GeoPolygon makeGeoPolygon(final PlanetModel planetModel,
final PolygonDescription description,
final double leniencyValue) {
// First, convert the holes to polygons in their own right.
final List holes;
if (description.holes != null && description.holes.size() > 0) {
holes = new ArrayList<>(description.holes.size());
for (final PolygonDescription holeDescription : description.holes) {
final GeoPolygon gp = makeGeoPolygon(planetModel, holeDescription, leniencyValue);
if (gp == null) {
return null;
}
holes.add(gp);
}
} else {
holes = null;
}
if (description.points.size() <= SMALL_POLYGON_CUTOFF_EDGES) {
// First, exercise a sanity filter on the provided pointList, and remove identical points, linear points, and backtracks
//System.err.println(" filtering "+pointList.size()+" points...");
//final long startTime = System.currentTimeMillis();
final List firstFilteredPointList = filterPoints(description.points);
if (firstFilteredPointList == null) {
return null;
}
final List filteredPointList = filterEdges(firstFilteredPointList, leniencyValue);
//System.err.println(" ...done in "+(System.currentTimeMillis()-startTime)+"ms ("+((filteredPointList==null)?"degenerate":(filteredPointList.size()+" points"))+")");
if (filteredPointList == null) {
return null;
}
try {
//First approximation to find a point
final GeoPoint centerOfMass = getCenterOfMass(planetModel, filteredPointList);
final Boolean isCenterOfMassInside = isInsidePolygon(centerOfMass, filteredPointList);
if (isCenterOfMassInside != null) {
return generateGeoPolygon(planetModel, filteredPointList, holes, centerOfMass, isCenterOfMassInside);
}
//System.err.println("points="+pointList);
// Create a random number generator. Effectively this furnishes us with a repeatable sequence
// of points to use for poles.
final Random generator = new Random(1234);
for (int counter = 0; counter < 1000000; counter++) {
//counter++;
// Pick the next random pole
final GeoPoint pole = pickPole(generator, planetModel, filteredPointList);
// Is it inside or outside?
final Boolean isPoleInside = isInsidePolygon(pole, filteredPointList);
if (isPoleInside != null) {
// Legal pole
//System.out.println("Took "+counter+" iterations to find pole");
//System.out.println("Pole = "+pole+"; isInside="+isPoleInside+"; pointList = "+pointList);
return generateGeoPolygon(planetModel, filteredPointList, holes, pole, isPoleInside);
}
// If pole choice was illegal, try another one
}
throw new IllegalArgumentException("cannot find a point that is inside the polygon "+filteredPointList);
} catch (TileException e) {
// Couldn't tile the polygon; use GeoComplexPolygon instead, if we can.
}
}
// Fallback: create large geo polygon, using complex polygon logic.
final List pd = new ArrayList<>(1);
pd.add(description);
return makeLargeGeoPolygon(planetModel, pd);
}
/** Create a GeoPolygon using the specified points and holes, using order to determine
* siding of the polygon. Much like ESRI, this method uses clockwise to indicate the space
* on the same side of the shape as being inside, and counter-clockwise to indicate the
* space on the opposite side as being inside.
* @param pointList is a list of the GeoPoints to build an arbitrary polygon out of. If points go
* clockwise from a given pole, then that pole should be within the polygon. If points go
* counter-clockwise, then that pole should be outside the polygon.
* @return a GeoPolygon corresponding to what was specified.
*/
public static GeoPolygon makeGeoPolygon(final PlanetModel planetModel,
final List pointList) {
return makeGeoPolygon(planetModel, pointList, null);
}
/** Create a GeoPolygon using the specified points and holes, using order to determine
* siding of the polygon. Much like ESRI, this method uses clockwise to indicate the space
* on the same side of the shape as being inside, and counter-clockwise to indicate the
* space on the opposite side as being inside.
* @param pointList is a list of the GeoPoints to build an arbitrary polygon out of. If points go
* clockwise from a given pole, then that pole should be within the polygon. If points go
* counter-clockwise, then that pole should be outside the polygon.
* @param holes is a list of polygons representing "holes" in the outside polygon. Holes describe the area outside
* each hole as being "in set". Null == none.
* @return a GeoPolygon corresponding to what was specified, or null if a valid polygon cannot be generated
* from this input.
*/
public static GeoPolygon makeGeoPolygon(final PlanetModel planetModel,
final List pointList,
final List holes) {
return makeGeoPolygon(planetModel, pointList, holes, 0.0);
}
/** Create a GeoPolygon using the specified points and holes, using order to determine
* siding of the polygon. Much like ESRI, this method uses clockwise to indicate the space
* on the same side of the shape as being inside, and counter-clockwise to indicate the
* space on the opposite side as being inside.
* @param pointList is a list of the GeoPoints to build an arbitrary polygon out of. If points go
* clockwise from a given pole, then that pole should be within the polygon. If points go
* counter-clockwise, then that pole should be outside the polygon.
* @param holes is a list of polygons representing "holes" in the outside polygon. Holes describe the area outside
* each hole as being "in set". Null == none.
* @param leniencyValue is the maximum distance (in units) that a point can be from the plane and still be considered as
* belonging to the plane. Any value greater than zero may cause some of the provided points that are in fact outside
* the strict definition of co-planarity, but are within this distance, to be discarded for the purposes of creating a
* "safe" polygon.
* @return a GeoPolygon corresponding to what was specified, or null if a valid polygon cannot be generated
* from this input.
*/
public static GeoPolygon makeGeoPolygon(final PlanetModel planetModel,
final List pointList,
final List holes,
final double leniencyValue) {
// First, exercise a sanity filter on the provided pointList, and remove identical points, linear points, and backtracks
//System.err.println(" filtering "+pointList.size()+" points...");
//final long startTime = System.currentTimeMillis();
final List firstFilteredPointList = filterPoints(pointList);
if (firstFilteredPointList == null) {
return null;
}
final List filteredPointList = filterEdges(firstFilteredPointList, leniencyValue);
//System.err.println(" ...done in "+(System.currentTimeMillis()-startTime)+"ms ("+((filteredPointList==null)?"degenerate":(filteredPointList.size()+" points"))+")");
if (filteredPointList == null) {
return null;
}
try {
//First approximation to find a point
final GeoPoint centerOfMass = getCenterOfMass(planetModel, filteredPointList);
final Boolean isCenterOfMassInside = isInsidePolygon(centerOfMass, filteredPointList);
if (isCenterOfMassInside != null) {
return generateGeoPolygon(planetModel, filteredPointList, holes, centerOfMass, isCenterOfMassInside);
}
//System.err.println("points="+pointList);
// Create a random number generator. Effectively this furnishes us with a repeatable sequence
// of points to use for poles.
final Random generator = new Random(1234);
for (int counter = 0; counter < 1000000; counter++) {
//counter++;
// Pick the next random pole
final GeoPoint pole = pickPole(generator, planetModel, filteredPointList);
// Is it inside or outside?
final Boolean isPoleInside = isInsidePolygon(pole, filteredPointList);
if (isPoleInside != null) {
// Legal pole
//System.out.println("Took "+counter+" iterations to find pole");
//System.out.println("Pole = "+pole+"; isInside="+isPoleInside+"; pointList = "+pointList);
return generateGeoPolygon(planetModel, filteredPointList, holes, pole, isPoleInside);
}
// If pole choice was illegal, try another one
}
throw new IllegalArgumentException("cannot find a point that is inside the polygon "+filteredPointList);
} catch (TileException e) {
// Couldn't tile the polygon; use GeoComplexPolygon instead, if we can.
if (holes != null && holes.size() > 0) {
// We currently cannot get the list of points that went into making a hole back out, so don't allow this case.
// In order to support it, we really need to change the API contract, which is a bigger deal.
throw new IllegalArgumentException(e.getMessage());
}
final List description = new ArrayList<>(1);
description.add(new PolygonDescription(pointList));
return makeLargeGeoPolygon(planetModel, description);
}
}
/** Generate a point at the center of mass of a list of points.
*/
private static GeoPoint getCenterOfMass(final PlanetModel planetModel, final List points) {
double x = 0;
double y = 0;
double z = 0;
//get center of mass
for (final GeoPoint point : points) {
x += point.x;
y += point.y;
z += point.z;
}
// Normalization is not needed because createSurfacePoint does the scaling anyway.
return planetModel.createSurfacePoint(x, y, z);
}
/** Create a large GeoPolygon. This is one which has more than 100 sides and/or may have resolution problems
* with very closely spaced points, which often occurs when the polygon was constructed to approximate curves. No tiling
* is done, and intersections and membership are optimized for having large numbers of sides.
*
* This method does very little checking for legality. It expects the incoming shapes to not intersect
* each other. The shapes can be disjoint or nested. If the shapes listed are nested, then we are describing holes.
* There is no limit to the depth of holes. However, if a shape is nested within another it must be explicitly
* described as being a child of the other shape.
*
* Membership in any given shape is described by the clockwise/counterclockwise direction of the points. The
* clockwise direction indicates that a point inside is "in-set", while a counter-clockwise direction implies that
* a point inside is "out-of-set".
*
* @param planetModel is the planet model.
* @param shapesList is the list of polygons we should be making.
* @return the GeoPolygon, or null if it cannot be constructed.
*/
public static GeoPolygon makeLargeGeoPolygon(final PlanetModel planetModel,
final List shapesList) {
// We're going to be building a single-level list of shapes in the end, with a single point that we know to be inside/outside, which is
// not on an edge.
final List> pointsList = new ArrayList<>();
BestShape testPointShape = null;
for (final PolygonDescription shape : shapesList) {
// Convert this shape and its holes to a general list of shapes. We also need to identify exactly one
// legal, non-degenerate shape with no children that we can use to find a test point. We also optimize
// to choose as small as possible a polygon for determining the in-set-ness of the test point.
testPointShape = convertPolygon(pointsList, shape, testPointShape, true);
}
// If there's no polygon we can use to determine a test point, we throw up.
if (testPointShape == null) {
throw new IllegalArgumentException("couldn't find a non-degenerate polygon for in-set determination");
}
final GeoPoint centerOfMass = getCenterOfMass(planetModel, testPointShape.points);
final GeoComplexPolygon comRval = testPointShape.createGeoComplexPolygon(planetModel, pointsList, centerOfMass);
if (comRval != null) {
return comRval;
}
// Center of mass didn't work.
// Create a random number generator. Effectively this furnishes us with a repeatable sequence
// of points to use for poles.
final Random generator = new Random(1234);
for (int counter = 0; counter < 1000000; counter++) {
// Pick the next random pole
final GeoPoint pole = pickPole(generator, planetModel, testPointShape.points);
final GeoComplexPolygon rval = testPointShape.createGeoComplexPolygon(planetModel, pointsList, pole);
if (rval != null) {
return rval;
}
// If pole choice was illegal, try another one
}
throw new IllegalArgumentException("cannot find a point that is inside the polygon "+testPointShape);
}
/** Convert a polygon description to a list of shapes. Also locate an optimal shape for evaluating a test point.
* @param pointsList is the structure to add new polygons to.
* @param shape is the current polygon description.
* @param testPointShape is the current best choice for a low-level polygon to evaluate.
* @return an updated best-choice for a test point polygon, and update the points list.
*/
private static BestShape convertPolygon(final List> pointsList, final PolygonDescription shape, BestShape testPointShape, final boolean mustBeInside) {
// First, remove duplicate points. If degenerate, just ignore the shape.
final List filteredPoints = filterPoints(shape.points);
if (filteredPoints == null) {
return testPointShape;
}
// Non-degenerate. Check if this is a candidate for in-set determination.
if (shape.holes.size() == 0) {
// This shape is a candidate for a test point.
if (testPointShape == null || testPointShape.points.size() > filteredPoints.size()) {
testPointShape = new BestShape(filteredPoints, mustBeInside);
}
}
pointsList.add(filteredPoints);
// Now, do all holes too
for (final PolygonDescription hole : shape.holes) {
testPointShape = convertPolygon(pointsList, hole, testPointShape, !mustBeInside);
}
// Done; return the updated test point shape.
return testPointShape;
}
/**
* Class for tracking the best shape for finding a pole, and whether or not the pole
* must be inside or outside of the shape.
*/
private static class BestShape {
public final List points;
public boolean poleMustBeInside;
public BestShape(final List points, final boolean poleMustBeInside) {
this.points = points;
this.poleMustBeInside = poleMustBeInside;
}
public GeoComplexPolygon createGeoComplexPolygon(final PlanetModel planetModel,
final List> pointsList, final GeoPoint testPoint) {
// Is it inside or outside?
final Boolean isTestPointInside = isInsidePolygon(testPoint, points);
if (isTestPointInside != null) {
try {
// Legal pole
if (isTestPointInside == poleMustBeInside) {
return new GeoComplexPolygon(planetModel, pointsList, testPoint, isTestPointInside);
} else {
return new GeoComplexPolygon(planetModel, pointsList, new GeoPoint(-testPoint.x, -testPoint.y, -testPoint.z), !isTestPointInside);
}
} catch (IllegalArgumentException e) {
// Probably bad choice of test point.
return null;
}
}
// If pole choice was illegal, try another one
return null;
}
}
/**
* Create a GeoPolygon using the specified points and holes and a test point.
*
* @param filteredPointList is a filtered list of the GeoPoints to build an arbitrary polygon out of.
* @param holes is a list of polygons representing "holes" in the outside polygon. Null == none.
* @param testPoint is a test point that is either known to be within the polygon area, or not.
* @param testPointInside is true if the test point is within the area, false otherwise.
* @return a GeoPolygon corresponding to what was specified, or null if what was specified
* cannot be turned into a valid non-degenerate polygon.
*/
static GeoPolygon generateGeoPolygon(final PlanetModel planetModel,
final List filteredPointList,
final List holes,
final GeoPoint testPoint,
final boolean testPointInside) throws TileException {
// We will be trying twice to find the right GeoPolygon, using alternate siding choices for the first polygon
// side. While this looks like it might be 2x as expensive as it could be, there's really no other choice I can
// find.
final SidedPlane initialPlane = new SidedPlane(testPoint, filteredPointList.get(0), filteredPointList.get(1));
// We don't know if this is the correct siding choice. We will only know as we build the complex polygon.
// So we need to be prepared to try both possibilities.
GeoCompositePolygon rval = new GeoCompositePolygon(planetModel);
MutableBoolean seenConcave = new MutableBoolean();
if (buildPolygonShape(rval, seenConcave, planetModel, filteredPointList, new BitSet(), 0, 1, initialPlane, holes, testPoint) == false) {
// The testPoint was within the shape. Was that intended?
if (testPointInside) {
// Yes: build it for real
rval = new GeoCompositePolygon(planetModel);
seenConcave = new MutableBoolean();
buildPolygonShape(rval, seenConcave, planetModel, filteredPointList, new BitSet(), 0, 1, initialPlane, holes, null);
return rval;
}
// No: do the complement and return that.
rval = new GeoCompositePolygon(planetModel);
seenConcave = new MutableBoolean();
buildPolygonShape(rval, seenConcave, planetModel, filteredPointList, new BitSet(), 0, 1, new SidedPlane(initialPlane), holes, null);
return rval;
} else {
// The testPoint was outside the shape. Was that intended?
if (!testPointInside) {
// Yes: return what we just built
return rval;
}
// No: return the complement
rval = new GeoCompositePolygon(planetModel);
seenConcave = new MutableBoolean();
buildPolygonShape(rval, seenConcave, planetModel, filteredPointList, new BitSet(), 0, 1, new SidedPlane(initialPlane), holes, null);
return rval;
}
}
/** Filter duplicate points.
* @param input with input list of points
* @return the filtered list, or null if we can't get a legit polygon from the input.
*/
static List filterPoints(final List extends GeoPoint> input) {
final List noIdenticalPoints = new ArrayList<>(input.size());
// Backtrack to find something different from the first point
int startIndex = -1;
final GeoPoint comparePoint = input.get(0);
for (int i = 0; i < input.size()-1; i++) {
final GeoPoint thePoint = input.get(getLegalIndex(- i - 1, input.size()));
if (!thePoint.isNumericallyIdentical(comparePoint)) {
startIndex = getLegalIndex(-i, input.size());
break;
}
}
if (startIndex == -1) {
return null;
}
// Now we can start the process of walking around, removing duplicate points.
int currentIndex = startIndex;
while (true) {
final GeoPoint currentPoint = input.get(currentIndex);
noIdenticalPoints.add(currentPoint);
while (true) {
currentIndex = getLegalIndex(currentIndex + 1, input.size());
if (currentIndex == startIndex) {
break;
}
final GeoPoint nextNonIdenticalPoint = input.get(currentIndex);
if (!nextNonIdenticalPoint.isNumericallyIdentical(currentPoint)) {
break;
}
}
if (currentIndex == startIndex) {
break;
}
}
if (noIdenticalPoints.size() < 3) {
return null;
}
return noIdenticalPoints;
}
/** Filter coplanar points.
* @param noIdenticalPoints with input list of points
* @param leniencyValue is the allowed distance of a point from the plane for cleanup of overly detailed polygons
* @return the filtered list, or null if we can't get a legit polygon from the input.
*/
static List filterEdges(final List noIdenticalPoints, final double leniencyValue) {
// Now, do the search needed to find a path that has no coplanarities in it.
// It is important to check coplanarities using the points that are further away so the
// plane is more precise.
for (int i = 0; i < noIdenticalPoints.size(); i++) {
//Search starting for current index.
final SafePath resultPath = findSafePath(noIdenticalPoints, i, leniencyValue);
if (resultPath != null && resultPath.previous != null) {
// Read out result, maintaining ordering
final List rval = new ArrayList<>(noIdenticalPoints.size());
resultPath.fillInList(rval);
return rval;
}
}
// No path found. This means that everything was coplanar.
return null;
}
/** Iterative path search through ordered list of points. The method merges together
* all consecutive coplanar points and builds the plane using the first and the last point.
* It does not converge if the starting point is coplanar with the last and next point of the path.
*
* @param points is the ordered raw list of points under consideration.
* @param startIndex is index of the point that starts the current path, so that we can know when we are done.
* @param leniencyValue is the allowed distance of a point from the plane to be considered coplanar.
* @return null if the starting point is coplanar with the last and next point of the path.
*/
private static SafePath findSafePath(final List points, final int startIndex, final double leniencyValue) {
SafePath safePath = null;
for (int i = startIndex; i < startIndex + points.size(); i++) {
//get start point, always the same for an iteration
final int startPointIndex = getLegalIndex(i -1, points.size());
final GeoPoint startPoint = points.get(startPointIndex);
//get end point, can be coplanar and therefore change
int endPointIndex = getLegalIndex(i, points.size());
GeoPoint endPoint = points.get(endPointIndex);
if (startPoint.isNumericallyIdentical(endPoint)) {
//go to next if identical
continue;
}
//Check if nextPoints are co-planar, if so advance to next point.
//if we go over the start index then we have no succeed.
while (true) {
int nextPointIndex = getLegalIndex(endPointIndex + 1, points.size());
final GeoPoint nextPoint = points.get(nextPointIndex);
if (startPoint.isNumericallyIdentical(nextPoint)) {
//all coplanar
return null;
}
if (!Plane.arePointsCoplanar(startPoint, endPoint, nextPoint)) {
//no coplanar.
break;
}
if (endPointIndex == startIndex) {
//we are over the path, we fail.
return null;
}
//advance
endPointIndex = nextPointIndex;
endPoint = nextPoint;
i++;
}
if (safePath != null && endPointIndex == startIndex) {
//We are already at the start, current point is coplanar with
//start point, no need to add this node.
break;
}
//Create node and move to next one
Plane currentPlane = new Plane(startPoint, endPoint);
safePath = new SafePath(safePath, endPoint, endPointIndex, currentPlane);
}
return safePath;
}
/** Pick a random pole that has a good chance of being inside the polygon described by the points.
* @param generator is the random number generator to use.
* @param planetModel is the planet model to use.
* @param points is the list of points available.
* @return the randomly-determined pole selection.
*/
private static GeoPoint pickPole(final Random generator, final PlanetModel planetModel, final List points) {
final int pointIndex = generator.nextInt(points.size());
final GeoPoint closePoint = points.get(pointIndex);
// We pick a random angle and random arc distance, then generate a point based on closePoint
final double angle = generator.nextDouble() * Math.PI * 2.0 - Math.PI;
double maxArcDistance = points.get(0).arcDistance(points.get(1));
double trialArcDistance = points.get(0).arcDistance(points.get(2));
if (trialArcDistance > maxArcDistance) {
maxArcDistance = trialArcDistance;
}
final double arcDistance = maxArcDistance - generator.nextDouble() * maxArcDistance;
// We come up with a unit circle (x,y,z) coordinate given the random angle and arc distance. The point is centered around the positive x axis.
final double x = Math.cos(arcDistance);
final double sinArcDistance = Math.sin(arcDistance);
final double y = Math.cos(angle) * sinArcDistance;
final double z = Math.sin(angle) * sinArcDistance;
// Now, use closePoint for a rotation pole
final double sinLatitude = Math.sin(closePoint.getLatitude());
final double cosLatitude = Math.cos(closePoint.getLatitude());
final double sinLongitude = Math.sin(closePoint.getLongitude());
final double cosLongitude = Math.cos(closePoint.getLongitude());
// This transformation should take the point (1,0,0) and transform it to the closepoint's actual (x,y,z) coordinates.
// Coordinate rotation formula:
// x1 = x0 cos T - y0 sin T
// y1 = x0 sin T + y0 cos T
// We're in essence undoing the following transformation (from GeoPolygonFactory):
// x1 = x0 cos az + y0 sin az
// y1 = - x0 sin az + y0 cos az
// z1 = z0
// x2 = x1 cos al + z1 sin al
// y2 = y1
// z2 = - x1 sin al + z1 cos al
// So, we reverse the order of the transformations, AND we transform backwards.
// Transforming backwards means using these identities: sin(-angle) = -sin(angle), cos(-angle) = cos(angle)
// So:
// x1 = x0 cos al - z0 sin al
// y1 = y0
// z1 = x0 sin al + z0 cos al
// x2 = x1 cos az - y1 sin az
// y2 = x1 sin az + y1 cos az
// z2 = z1
final double x1 = x * cosLatitude - z * sinLatitude;
final double y1 = y;
final double z1 = x * sinLatitude + z * cosLatitude;
final double x2 = x1 * cosLongitude - y1 * sinLongitude;
final double y2 = x1 * sinLongitude + y1 * cosLongitude;
final double z2 = z1;
// Finally, scale to put the point on the surface
return planetModel.createSurfacePoint(x2, y2, z2);
}
/** For a specified point and a list of poly points, determine based on point order whether the
* point should be considered in or out of the polygon.
* @param point is the point to check.
* @param polyPoints is the list of points comprising the polygon.
* @return null if the point is illegal, otherwise false if the point is inside and true if the point is outside
* of the polygon.
*/
private static Boolean isInsidePolygon(final GeoPoint point, final List polyPoints) {
// First, compute sine and cosine of pole point latitude and longitude
final double latitude = point.getLatitude();
final double longitude = point.getLongitude();
final double sinLatitude = Math.sin(latitude);
final double cosLatitude = Math.cos(latitude);
final double sinLongitude = Math.sin(longitude);
final double cosLongitude = Math.cos(longitude);
// Now, compute the incremental arc distance around the points of the polygon
double arcDistance = 0.0;
Double prevAngle = null;
//System.out.println("Computing angles:");
for (final GeoPoint polyPoint : polyPoints) {
final Double angle = computeAngle(polyPoint, sinLatitude, cosLatitude, sinLongitude, cosLongitude);
if (angle == null) {
return null;
}
//System.out.println("Computed angle: "+angle);
if (prevAngle != null) {
// Figure out delta between prevAngle and current angle, and add it to arcDistance
double angleDelta = angle - prevAngle;
if (angleDelta < -Math.PI) {
angleDelta += Math.PI * 2.0;
}
if (angleDelta > Math.PI) {
angleDelta -= Math.PI * 2.0;
}
if (Math.abs(angleDelta - Math.PI) < Vector.MINIMUM_ANGULAR_RESOLUTION) {
return null;
}
//System.out.println(" angle delta = "+angleDelta);
arcDistance += angleDelta;
//System.out.println(" For point "+polyPoint+" angle is "+angle+"; delta is "+angleDelta+"; arcDistance is "+arcDistance);
}
prevAngle = angle;
}
if (prevAngle != null) {
final Double lastAngle = computeAngle(polyPoints.get(0), sinLatitude, cosLatitude, sinLongitude, cosLongitude);
if (lastAngle == null) {
return null;
}
//System.out.println("Computed last angle: "+lastAngle);
// Figure out delta and add it
double angleDelta = lastAngle - prevAngle;
if (angleDelta < -Math.PI) {
angleDelta += Math.PI * 2.0;
}
if (angleDelta > Math.PI) {
angleDelta -= Math.PI * 2.0;
}
if (Math.abs(angleDelta - Math.PI) < Vector.MINIMUM_ANGULAR_RESOLUTION) {
return null;
}
//System.out.println(" angle delta = "+angleDelta);
arcDistance += angleDelta;
//System.out.println(" For point "+polyPoints.get(0)+" angle is "+lastAngle+"; delta is "+angleDelta+"; arcDistance is "+arcDistance);
}
// Clockwise == inside == negative
//System.out.println("Arcdistance = "+arcDistance);
if (Math.abs(arcDistance) < Vector.MINIMUM_ANGULAR_RESOLUTION) {
// No idea what direction, so try another pole.
return null;
}
return arcDistance > 0.0;
}
/** Compute the angle for a point given rotation information.
* @param point is the point to assess
* @param sinLatitude the sine of the latitude
* @param cosLatitude the cosine of the latitude
* @param sinLongitude the sine of the longitude
* @param cosLongitude the cosine of the longitude
* @return the angle of rotation, or null if not computable
*/
private static Double computeAngle(final GeoPoint point,
final double sinLatitude,
final double cosLatitude,
final double sinLongitude,
final double cosLongitude) {
// Coordinate rotation formula:
// x1 = x0 cos T - y0 sin T
// y1 = x0 sin T + y0 cos T
// We need to rotate the point in question into the coordinate frame specified by
// the lat and lon trig functions.
// To do this we need to do two rotations on it. First rotation is in x/y. Second rotation is in x/z.
// And we rotate in the negative direction.
// So:
// x1 = x0 cos az + y0 sin az
// y1 = - x0 sin az + y0 cos az
// z1 = z0
// x2 = x1 cos al + z1 sin al
// y2 = y1
// z2 = - x1 sin al + z1 cos al
final double x1 = point.x * cosLongitude + point.y * sinLongitude;
final double y1 = - point.x * sinLongitude + point.y * cosLongitude;
final double z1 = point.z;
// final double x2 = x1 * cosLatitude + z1 * sinLatitude;
final double y2 = y1;
final double z2 = - x1 * sinLatitude + z1 * cosLatitude;
// Now we should be looking down the X axis; the original point has rotated coordinates (N, 0, 0).
// So we can just compute the angle using y2 and z2. (If Math.sqrt(y2*y2 + z2 * z2) is 0.0, then the point is on the pole and we need another one).
if (Math.sqrt(y2*y2 + z2*z2) < Vector.MINIMUM_RESOLUTION) {
return null;
}
return Math.atan2(z2, y2);
}
/** Build a GeoPolygon out of one concave part and multiple convex parts given points, starting edge, and whether starting edge is internal or not.
* @param rval is the composite polygon to add to.
* @param seenConcave is true if a concave polygon has been seen in this generation yet.
* @param planetModel is the planet model.
* @param pointsList is a list of the GeoPoints to build an arbitrary polygon out of.
* @param internalEdges specifies which edges are internal.
* @param startPointIndex is the first of the points, constituting the starting edge.
* @param startingEdge is the plane describing the starting edge.
* @param holes is the list of holes in the polygon, or null if none.
* @param testPoint is an (optional) test point, which will be used to determine if we are generating
* a shape with the proper sidedness. It is passed in only when the test point is supposed to be outside
* of the generated polygon. In this case, if the generated polygon is found to contain the point, the
* method exits early with a null return value.
* This only makes sense in the context of evaluating both possible choices and using logic to determine
* which result to use. If the test point is supposed to be within the shape, then it must be outside of the
* complement shape. If the test point is supposed to be outside the shape, then it must be outside of the
* original shape. Either way, we can figure out the right thing to use.
* @return false if what was specified
* was inconsistent with what we generated. Specifically, if we specify an exterior point that is
* found in the interior of the shape we create here we return false, which is a signal that we chose
* our initial plane sidedness backwards.
*/
static boolean buildPolygonShape(
final GeoCompositePolygon rval,
final MutableBoolean seenConcave,
final PlanetModel planetModel,
final List pointsList,
final BitSet internalEdges,
final int startPointIndex,
final int endPointIndex,
final SidedPlane startingEdge,
final List holes,
final GeoPoint testPoint) throws TileException {
// It could be the case that we need a concave polygon. So we need to try and look for that case
// as part of the general code for constructing complex polygons.
// Note that there can be only one concave polygon. This code will enforce that condition and will return
// false if it is violated.
// The code here must keep track of two lists of sided planes. The first list contains the planes consistent with
// a concave polygon. This list will grow and shrink. The second list is built starting at the current edge that
// was last consistent with the concave polygon, and contains all edges consistent with a convex polygon.
// When that sequence of edges is done, then an internal edge is created and the identified points are converted to a
// convex polygon. That internal edge is used to extend the list of edges in the concave polygon edge list.
// The edge buffer.
final EdgeBuffer edgeBuffer = new EdgeBuffer(pointsList, internalEdges, startPointIndex, endPointIndex, startingEdge);
/*
// Verify that the polygon does not self-intersect
// Now, look for non-adjacent edges that cross.
System.err.println("Looking for intersections...");
System.err.println("Starting edge is: "+startingEdge);
final Iterator edgeIterator = edgeBuffer.iterator();
while (edgeIterator.hasNext()) {
final Edge edge = edgeIterator.next();
final Set excludedEdges = new HashSet<>();
excludedEdges.add(edge);
Edge oneBoundary = edgeBuffer.getPrevious(edge);
while (oneBoundary.plane.isNumericallyIdentical(edge.plane)) {
excludedEdges.add(oneBoundary);
oneBoundary = edgeBuffer.getPrevious(oneBoundary);
}
excludedEdges.add(oneBoundary);
Edge otherBoundary = edgeBuffer.getNext(edge);
while (otherBoundary.plane.isNumericallyIdentical(edge.plane)) {
excludedEdges.add(otherBoundary);
otherBoundary = edgeBuffer.getNext(otherBoundary);
}
excludedEdges.add(otherBoundary);
// Now go through all other edges and rule out any intersections
final Iterator compareIterator = edgeBuffer.iterator();
while (compareIterator.hasNext()) {
final Edge compareEdge = compareIterator.next();
if (!excludedEdges.contains(compareEdge)) {
// Found an edge we can compare with!
//System.err.println("Found a compare edge...");
boolean nonOverlapping = true;
// We need the other boundaries though.
Edge oneCompareBoundary = edgeBuffer.getPrevious(compareEdge);
while (oneCompareBoundary.plane.isNumericallyIdentical(compareEdge.plane)) {
if (excludedEdges.contains(oneCompareBoundary)) {
//System.err.println(" excluded because oneCompareBoundary found to be in set");
nonOverlapping = false;
break;
}
oneCompareBoundary = edgeBuffer.getPrevious(oneCompareBoundary);
}
Edge otherCompareBoundary = edgeBuffer.getNext(compareEdge);
while (otherCompareBoundary.plane.isNumericallyIdentical(compareEdge.plane)) {
if (excludedEdges.contains(otherCompareBoundary)) {
//System.err.println(" excluded because otherCompareBoundary found to be in set");
nonOverlapping = false;
break;
}
otherCompareBoundary = edgeBuffer.getNext(otherCompareBoundary);
}
if (nonOverlapping) {
//System.err.println("Preparing to call findIntersections...");
// Finally do an intersection test
if (edge.plane.findIntersections(planetModel, compareEdge.plane, oneBoundary.plane, otherBoundary.plane, oneCompareBoundary.plane, otherCompareBoundary.plane).length > 0) {
throw new IllegalArgumentException("polygon has intersecting edges");
}
}
}
}
}
*/
// Starting state:
// The stopping point
Edge stoppingPoint = edgeBuffer.pickOne();
// The current edge
Edge currentEdge = stoppingPoint;
// Progressively look for convex sections. If we find one, we emit it and replace it.
// Keep going until we have been around once and nothing needed to change, and then
// do the concave polygon, if necessary.
while (true) {
if (currentEdge == null) {
// We're done!
break;
}
// Find convexity around the current edge, if any
final Boolean foundIt = findConvexPolygon(planetModel, currentEdge, rval, edgeBuffer, holes, testPoint);
if (foundIt == null) {
return false;
}
if (foundIt) {
// New start point
stoppingPoint = edgeBuffer.pickOne();
currentEdge = stoppingPoint;
// back around
continue;
}
// Otherwise, go on to the next
currentEdge = edgeBuffer.getNext(currentEdge);
if (currentEdge == stoppingPoint) {
break;
}
}
// Look for any reason that the concave polygon cannot be created.
// This test is really the converse of the one for a convex polygon.
// Points on the edge of a convex polygon MUST be inside all the other
// edges. For a concave polygon, this check is still the same, except we have
// to look at the reverse sided planes, not the forward ones.
// If we find a point that is outside of the complementary edges, it means that
// the point is in fact able to form a convex polygon with the edge it is
// offending.
// If what is left has any plane/point pair that is on the wrong side, we have to split using one of the plane endpoints and the
// point in question. This is best structured as a recursion, if detected.
// Note: Any edge that fails means (I think!!) that there's another edge that will also fail.
// This is because each point is included in two edges.
// So, when we look for a non-conforming edge, and we can find one (but can't use it), we
// also can find another edge that we might be able to use instead.
// If this is true, it means we should continue when we find a bad edge we can't use --
// but we need to keep track of this, and fail hard if we don't find a place to split.
boolean foundBadEdge = false;
final Iterator checkIterator = edgeBuffer.iterator();
while (checkIterator.hasNext()) {
final Edge checkEdge = checkIterator.next();
final SidedPlane flippedPlane = new SidedPlane(checkEdge.plane);
// Now walk around again looking for points that fail.
final Iterator confirmIterator = edgeBuffer.iterator();
while (confirmIterator.hasNext()) {
final Edge confirmEdge = confirmIterator.next();
if (confirmEdge == checkEdge) {
continue;
}
// Look for a point that is on the wrong side of the check edge. This means that we can't build the polygon.
final GeoPoint thePoint;
if (checkEdge.startPoint != confirmEdge.startPoint && checkEdge.endPoint != confirmEdge.startPoint && !flippedPlane.isWithin(confirmEdge.startPoint)) {
thePoint = confirmEdge.startPoint;
} else if (checkEdge.startPoint != confirmEdge.endPoint && checkEdge.endPoint != confirmEdge.endPoint && !flippedPlane.isWithin(confirmEdge.endPoint)) {
thePoint = confirmEdge.endPoint;
} else {
thePoint = null;
}
if (thePoint != null) {
// Note that we found a problem.
foundBadEdge = true;
// thePoint is on the wrong side of the complementary plane. That means we cannot build a concave polygon, because the complement would not
// be a legal convex polygon.
// But we can take advantage of the fact that the distance between the edge and thePoint is less than 180 degrees, and so we can split the
// would-be concave polygon into three segments. The first segment includes the edge and thePoint, and uses the sense of the edge to determine the sense
// of the polygon.
// This should be the only problematic part of the polygon.
// We know that thePoint is on the "wrong" side of the edge -- that is, it's on the side that the
// edge is pointing at.
// The proposed tiling generates two new edges -- one from thePoint to the start point of the edge we found, and the other from thePoint
// to the end point of the edge. We generate that as a triangle convex polygon, and tile the two remaining pieces.
if (Plane.arePointsCoplanar(checkEdge.startPoint, checkEdge.endPoint, thePoint)) {
// Can't build this particular tile because of colinearity, so advance to another that maybe we can build.
continue;
}
final List thirdPartPoints = new ArrayList<>(3);
final BitSet thirdPartInternal = new BitSet();
thirdPartPoints.add(checkEdge.startPoint);
thirdPartInternal.set(0, checkEdge.isInternal);
thirdPartPoints.add(checkEdge.endPoint);
thirdPartInternal.set(1, true);
thirdPartPoints.add(thePoint);
assert checkEdge.plane.isWithin(thePoint) : "Point was on wrong side of complementary plane, so must be on the right side of the non-complementary plane!";
// Check for illegal argument using try/catch rather than pre-emptive check, since it cuts down on building objects for a rare case
final GeoPolygon convexPart = new GeoConvexPolygon(planetModel, thirdPartPoints, holes, thirdPartInternal, true);
//System.out.println("convex part = "+convexPart);
rval.addShape(convexPart);
// The part preceding the bad edge, back to thePoint, needs to be recursively
// processed. So, assemble what we need, which is basically a list of edges.
Edge loopEdge = edgeBuffer.getPrevious(checkEdge);
final List firstPartPoints = new ArrayList<>();
final BitSet firstPartInternal = new BitSet();
int i = 0;
while (true) {
firstPartPoints.add(loopEdge.endPoint);
if (loopEdge.endPoint == thePoint) {
break;
}
firstPartInternal.set(i++, loopEdge.isInternal);
loopEdge = edgeBuffer.getPrevious(loopEdge);
}
firstPartInternal.set(i, true);
//System.out.println("Doing first part...");
if (buildPolygonShape(rval,
seenConcave,
planetModel,
firstPartPoints,
firstPartInternal,
firstPartPoints.size()-1,
0,
new SidedPlane(checkEdge.endPoint, false, checkEdge.startPoint, thePoint),
holes,
testPoint) == false) {
return false;
}
//System.out.println("...done first part.");
final List secondPartPoints = new ArrayList<>();
final BitSet secondPartInternal = new BitSet();
loopEdge = edgeBuffer.getNext(checkEdge);
i = 0;
while (true) {
secondPartPoints.add(loopEdge.startPoint);
if (loopEdge.startPoint == thePoint) {
break;
}
secondPartInternal.set(i++, loopEdge.isInternal);
loopEdge = edgeBuffer.getNext(loopEdge);
}
secondPartInternal.set(i, true);
//System.out.println("Doing second part...");
if (buildPolygonShape(rval,
seenConcave,
planetModel,
secondPartPoints,
secondPartInternal,
secondPartPoints.size()-1,
0,
new SidedPlane(checkEdge.startPoint, false, checkEdge.endPoint, thePoint),
holes,
testPoint) == false) {
return false;
}
//System.out.println("... done second part");
return true;
}
}
}
if (foundBadEdge) {
// Unaddressed bad edge
throw new TileException("Could not tile polygon; found a pathological coplanarity that couldn't be addressed");
}
// No violations found: we know it's a legal concave polygon.
// If there's anything left in the edge buffer, convert to concave polygon.
//System.out.println("adding concave part");
if (makeConcavePolygon(planetModel, rval, seenConcave, edgeBuffer, holes, testPoint) == false) {
return false;
}
return true;
}
/** Look for a concave polygon in the remainder of the edgebuffer.
* By this point, if there are any edges in the edgebuffer, they represent a concave polygon.
* @param planetModel is the planet model.
* @param rval is the composite polygon we're building.
* @param seenConcave is true if we've already seen a concave polygon.
* @param edgeBuffer is the edge buffer.
* @param holes is the optional list of holes.
* @param testPoint is the optional test point.
* @return true unless the testPoint caused failure.
*/
private static boolean makeConcavePolygon(final PlanetModel planetModel,
final GeoCompositePolygon rval,
final MutableBoolean seenConcave,
final EdgeBuffer edgeBuffer,
final List holes,
final GeoPoint testPoint) throws TileException {
if (edgeBuffer.size() == 0) {
return true;
}
if (seenConcave.value) {
throw new IllegalArgumentException("Illegal polygon; polygon edges intersect each other");
}
seenConcave.value = true;
// If there are less than three edges, something got messed up somehow. Don't know how this
// can happen but check.
if (edgeBuffer.size() < 3) {
// Linear...
// Here we can emit GeoWorld, but probably this means we had a broken poly to start with.
throw new IllegalArgumentException("Illegal polygon; polygon edges intersect each other");
}
// Create the list of points
final List points = new ArrayList(edgeBuffer.size());
final BitSet internalEdges = new BitSet(edgeBuffer.size()-1);
//System.out.println("Concave polygon points:");
Edge edge = edgeBuffer.pickOne();
boolean isInternal = false;
for (int i = 0; i < edgeBuffer.size(); i++) {
//System.out.println(" "+edge.plane+": "+edge.startPoint+"->"+edge.endPoint+"; previous? "+(edge.plane.isWithin(edgeBuffer.getPrevious(edge).startPoint)?"in":"out")+" next? "+(edge.plane.isWithin(edgeBuffer.getNext(edge).endPoint)?"in":"out"));
points.add(edge.startPoint);
if (i < edgeBuffer.size() - 1) {
internalEdges.set(i, edge.isInternal);
} else {
isInternal = edge.isInternal;
}
edge = edgeBuffer.getNext(edge);
}
try {
if (testPoint != null && holes != null && holes.size() > 0) {
// No holes, for test
final GeoPolygon testPolygon = new GeoConcavePolygon(planetModel, points, null, internalEdges, isInternal);
if (testPolygon.isWithin(testPoint)) {
return false;
}
}
final GeoPolygon realPolygon = new GeoConcavePolygon(planetModel, points, holes, internalEdges, isInternal);
if (testPoint != null && (holes == null || holes.size() == 0)) {
if (realPolygon.isWithin(testPoint)) {
return false;
}
}
rval.addShape(realPolygon);
return true;
} catch (IllegalArgumentException e) {
throw new TileException(e.getMessage());
}
}
/** Look for a convex polygon at the specified edge. If we find it, create one and adjust the edge buffer.
* @param planetModel is the planet model.
* @param currentEdge is the current edge to use starting the search.
* @param rval is the composite polygon to build.
* @param edgeBuffer is the edge buffer.
* @param holes is the optional list of holes.
* @param testPoint is the optional test point.
* @return null if the testPoint is within any polygon detected, otherwise true if a convex polygon was created.
*/
private static Boolean findConvexPolygon(final PlanetModel planetModel,
final Edge currentEdge,
final GeoCompositePolygon rval,
final EdgeBuffer edgeBuffer,
final List holes,
final GeoPoint testPoint) throws TileException {
//System.out.println("Looking at edge "+currentEdge+" with startpoint "+currentEdge.startPoint+" endpoint "+currentEdge.endPoint);
// Initialize the structure.
// We don't keep track of order here; we just care about membership.
// The only exception is the head and tail pointers.
final Set includedEdges = new HashSet<>();
includedEdges.add(currentEdge);
Edge firstEdge = currentEdge;
Edge lastEdge = currentEdge;
// First, walk towards the end until we need to stop
while (true) {
if (firstEdge.startPoint == lastEdge.endPoint) {
break;
}
final Edge newLastEdge = edgeBuffer.getNext(lastEdge);
if (Plane.arePointsCoplanar(lastEdge.startPoint, lastEdge.endPoint, newLastEdge.endPoint)) {
break;
}
// Planes that are almost identical cannot be properly handled by the standard polygon logic. Detect this case and, if found,
// give up on the tiling -- we'll need to create a large poly instead.
if (lastEdge.plane.isFunctionallyIdentical(newLastEdge.plane)) {
throw new TileException("Two adjacent edge planes are effectively parallel despite filtering; give up on tiling");
}
if (isWithin(newLastEdge.endPoint, includedEdges)) {
//System.out.println(" maybe can extend to next edge");
// Found a candidate for extension. But do some other checks first. Basically, we need to know if we construct a polygon
// here will overlap with other remaining points?
final SidedPlane returnBoundary;
if (firstEdge.startPoint != newLastEdge.endPoint) {
if (Plane.arePointsCoplanar(firstEdge.endPoint, firstEdge.startPoint, newLastEdge.endPoint) ||
Plane.arePointsCoplanar(firstEdge.startPoint, newLastEdge.endPoint, newLastEdge.startPoint)) {
break;
}
returnBoundary = new SidedPlane(firstEdge.endPoint, firstEdge.startPoint, newLastEdge.endPoint);
} else {
returnBoundary = null;
}
// The complete set of sided planes for the tentative new polygon include the ones in includedEdges, plus the one from newLastEdge,
// plus the new tentative return boundary. We have to make sure there are no points from elsewhere within the tentative convex polygon.
boolean foundPointInside = false;
final Iterator edgeIterator = edgeBuffer.iterator();
while (edgeIterator.hasNext()) {
final Edge edge = edgeIterator.next();
if (!includedEdges.contains(edge) && edge != newLastEdge) {
// This edge has a point to check
if (edge.startPoint != newLastEdge.endPoint) {
// look at edge.startPoint
if (isWithin(edge.startPoint, includedEdges, newLastEdge, returnBoundary)) {
//System.out.println(" nope; point within found: "+edge.startPoint);
foundPointInside = true;
break;
}
}
if (edge.endPoint != firstEdge.startPoint) {
// look at edge.endPoint
if (isWithin(edge.endPoint, includedEdges, newLastEdge, returnBoundary)) {
//System.out.println(" nope; point within found: "+edge.endPoint);
foundPointInside = true;
break;
}
}
}
}
if (!foundPointInside) {
//System.out.println(" extending!");
// Extend the polygon by the new last edge
includedEdges.add(newLastEdge);
lastEdge = newLastEdge;
// continue extending in this direction
continue;
}
}
// We can't extend any more in this direction, so break from the loop.
break;
}
// Now, walk towards the beginning until we need to stop
while (true) {
if (firstEdge.startPoint == lastEdge.endPoint) {
break;
}
final Edge newFirstEdge = edgeBuffer.getPrevious(firstEdge);
if (Plane.arePointsCoplanar(newFirstEdge.startPoint, newFirstEdge.endPoint, firstEdge.endPoint)) {
break;
}
// Planes that are almost identical cannot be properly handled by the standard polygon logic. Detect this case and, if found,
// give up on the tiling -- we'll need to create a large poly instead.
if (firstEdge.plane.isFunctionallyIdentical(newFirstEdge.plane)) {
throw new TileException("Two adjacent edge planes are effectively parallel despite filtering; give up on tiling");
}
if (isWithin(newFirstEdge.startPoint, includedEdges)) {
//System.out.println(" maybe can extend to previous edge");
// Found a candidate for extension. But do some other checks first. Basically, we need to know if we construct a polygon
// here will overlap with other remaining points?
final SidedPlane returnBoundary;
if (newFirstEdge.startPoint != lastEdge.endPoint) {
if(Plane.arePointsCoplanar(lastEdge.startPoint, lastEdge.endPoint, newFirstEdge.startPoint) ||
Plane.arePointsCoplanar(lastEdge.endPoint, newFirstEdge.startPoint, newFirstEdge.endPoint)) {
break;
}
returnBoundary = new SidedPlane(lastEdge.startPoint, lastEdge.endPoint, newFirstEdge.startPoint);
} else {
returnBoundary = null;
}
// The complete set of sided planes for the tentative new polygon include the ones in includedEdges, plus the one from newLastEdge,
// plus the new tentative return boundary. We have to make sure there are no points from elsewhere within the tentative convex polygon.
boolean foundPointInside = false;
final Iterator edgeIterator = edgeBuffer.iterator();
while (edgeIterator.hasNext()) {
final Edge edge = edgeIterator.next();
if (!includedEdges.contains(edge) && edge != newFirstEdge) {
// This edge has a point to check
if (edge.startPoint != lastEdge.endPoint) {
// look at edge.startPoint
if (isWithin(edge.startPoint, includedEdges, newFirstEdge, returnBoundary)) {
//System.out.println(" nope; point within found: "+edge.startPoint);
foundPointInside = true;
break;
}
}
if (edge.endPoint != newFirstEdge.startPoint) {
// look at edge.endPoint
if (isWithin(edge.endPoint, includedEdges, newFirstEdge, returnBoundary)) {
//System.out.println(" nope; point within found: "+edge.endPoint);
foundPointInside = true;
break;
}
}
}
}
if (!foundPointInside) {
//System.out.println(" extending!");
// Extend the polygon by the new last edge
includedEdges.add(newFirstEdge);
firstEdge = newFirstEdge;
// continue extending in this direction
continue;
}
}
// We can't extend any more in this direction, so break from the loop.
break;
}
// Ok, figure out what we've accumulated. If it is enough for a polygon, build it.
if (includedEdges.size() < 2) {
//System.out.println("Done edge "+currentEdge+": no poly found");
return false;
}
// It's enough to build a convex polygon
//System.out.println("Edge "+currentEdge+": Found complex poly");
// Create the point list and edge list, starting with the first edge and going to the last. The return edge will be between
// the start point of the first edge and the end point of the last edge. If the first edge start point is the same as the last edge end point,
// it's a degenerate case and we want to just clean out the edge buffer entirely.
final List points = new ArrayList(includedEdges.size()+1);
final BitSet internalEdges = new BitSet(includedEdges.size());
final boolean returnIsInternal;
if (firstEdge.startPoint == lastEdge.endPoint) {
// Degenerate case!! There is no return edge -- or rather, we already have it.
if (includedEdges.size() < 3) {
// This means we found a degenerate cycle of edges. If we emit a polygon at this point it
// has no contents, so we generate no polygon.
return false;
}
if (firstEdge.plane.isFunctionallyIdentical(lastEdge.plane)) {
throw new TileException("Two adjacent edge planes are effectively parallel despite filtering; give up on tiling");
}
// Now look for completely planar points. This too is a degeneracy condition that we should
// return "false" for.
Edge edge = firstEdge;
points.add(edge.startPoint);
int k = 0;
while (true) {
if (edge == lastEdge) {
break;
}
points.add(edge.endPoint);
internalEdges.set(k++, edge.isInternal);
edge = edgeBuffer.getNext(edge);
}
returnIsInternal = lastEdge.isInternal;
edgeBuffer.clear();
} else {
// Build the return edge (internal, of course)
final SidedPlane returnSidedPlane = new SidedPlane(firstEdge.endPoint, false, firstEdge.startPoint, lastEdge.endPoint);
final Edge returnEdge = new Edge(firstEdge.startPoint, lastEdge.endPoint, returnSidedPlane, true);
if (returnEdge.plane.isFunctionallyIdentical(lastEdge.plane) ||
returnEdge.plane.isFunctionallyIdentical(firstEdge.plane)) {
throw new TileException("Two adjacent edge planes are effectively parallel despite filtering; give up on tiling");
}
// Build point list and edge list
final List edges = new ArrayList(includedEdges.size());
returnIsInternal = true;
// Now look for completely planar points. This too is a degeneracy condition that we should
// return "false" for.
Edge edge = firstEdge;
points.add(edge.startPoint);
int k = 0;
while (true) {
points.add(edge.endPoint);
internalEdges.set(k++, edge.isInternal);
edges.add(edge);
if (edge == lastEdge) {
break;
}
edge = edgeBuffer.getNext(edge);
}
// Modify the edge buffer
edgeBuffer.replace(edges, returnEdge);
}
// Now, construct the polygon
// Failures in construction mean we have a polygon that is too large (>180 degrees)
try {
if (testPoint != null && holes != null && holes.size() > 0) {
// No holes, for test
final GeoPolygon testPolygon = new GeoConvexPolygon(planetModel, points, null, internalEdges, returnIsInternal);
if (testPolygon.isWithin(testPoint)) {
return null;
}
}
final GeoPolygon realPolygon = new GeoConvexPolygon(planetModel, points, holes, internalEdges, returnIsInternal);
if (testPoint != null && (holes == null || holes.size() == 0)) {
if (realPolygon.isWithin(testPoint)) {
return null;
}
}
rval.addShape(realPolygon);
return true;
} catch (IllegalArgumentException e) {
throw new TileException(e.getMessage());
}
}
/** Check if a point is within a set of edges.
* @param point is the point
* @param edgeSet is the set of edges
* @param extension is the new edge
* @param returnBoundary is the return edge
* @return true if within
*/
private static boolean isWithin(final GeoPoint point, final Set edgeSet, final Edge extension, final SidedPlane returnBoundary) {
if (!extension.plane.isWithin(point)) {
return false;
}
if (returnBoundary != null && !returnBoundary.isWithin(point)) {
return false;
}
return isWithin(point, edgeSet);
}
/** Check if a point is within a set of edges.
* @param point is the point
* @param edgeSet is the set of edges
* @return true if within
*/
private static boolean isWithin(final GeoPoint point, final Set edgeSet) {
for (final Edge edge : edgeSet) {
if (!edge.plane.isWithin(point)) {
return false;
}
}
return true;
}
/** Convert raw point index into valid array position.
*@param index is the array index.
*@param size is the array size.
*@return an updated index.
*/
private static int getLegalIndex(int index, int size) {
while (index < 0) {
index += size;
}
while (index >= size) {
index -= size;
}
return index;
}
/** Class representing a single (unused) edge.
*/
private static class Edge {
/** Plane */
public final SidedPlane plane;
/** Start point */
public final GeoPoint startPoint;
/** End point */
public final GeoPoint endPoint;
/** Internal edge flag */
public final boolean isInternal;
/** Constructor.
* @param startPoint the edge start point
* @param endPoint the edge end point
* @param plane the edge plane
* @param isInternal true if internal edge
*/
public Edge(final GeoPoint startPoint, final GeoPoint endPoint, final SidedPlane plane, final boolean isInternal) {
this.startPoint = startPoint;
this.endPoint = endPoint;
this.plane = plane;
this.isInternal = isInternal;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(final Object o) {
return o == this;
}
}
/** Class representing an iterator over an EdgeBuffer.
*/
private static class EdgeBufferIterator implements Iterator {
/** Edge buffer */
protected final EdgeBuffer edgeBuffer;
/** First edge */
protected final Edge firstEdge;
/** Current edge */
protected Edge currentEdge;
/** Constructor.
* @param edgeBuffer the edge buffer
*/
public EdgeBufferIterator(final EdgeBuffer edgeBuffer) {
this.edgeBuffer = edgeBuffer;
this.currentEdge = edgeBuffer.pickOne();
this.firstEdge = currentEdge;
}
@Override
public boolean hasNext() {
return currentEdge != null;
}
@Override
public Edge next() {
final Edge rval = currentEdge;
if (currentEdge != null) {
currentEdge = edgeBuffer.getNext(currentEdge);
if (currentEdge == firstEdge) {
currentEdge = null;
}
}
return rval;
}
@Override
public void remove() {
throw new RuntimeException("Unsupported operation");
}
}
/** Class representing a pool of unused edges, all linked together by vertices.
*/
private static class EdgeBuffer {
/** Starting edge */
protected Edge oneEdge;
/** Full set of edges */
protected final Set edges = new HashSet<>();
/** Map to previous edge */
protected final Map previousEdges = new HashMap<>();
/** Map to next edge */
protected final Map nextEdges = new HashMap<>();
/** Constructor.
* @param pointList is the list of points.
* @param internalEdges is the list of edges that are internal (includes return edge)
* @param startPlaneStartIndex is the index of the startPlane's starting point
* @param startPlaneEndIndex is the index of the startPlane's ending point
* @param startPlane is the starting plane
*/
public EdgeBuffer(final List pointList, final BitSet internalEdges, final int startPlaneStartIndex, final int startPlaneEndIndex, final SidedPlane startPlane) {
/*
System.out.println("Start plane index: "+startPlaneStartIndex+" End plane index: "+startPlaneEndIndex+" Initial points:");
for (final GeoPoint p : pointList) {
System.out.println(" "+p);
}
*/
final Edge startEdge = new Edge(pointList.get(startPlaneStartIndex), pointList.get(startPlaneEndIndex), startPlane, internalEdges.get(startPlaneStartIndex));
// Fill in the EdgeBuffer by walking around creating more stuff
Edge currentEdge = startEdge;
int startIndex = startPlaneStartIndex;
int endIndex = startPlaneEndIndex;
while (true) {
/*
System.out.println("For plane "+currentEdge.plane+", the following points are in/out:");
for (final GeoPoint p: pointList) {
System.out.println(" "+p+" is: "+(currentEdge.plane.isWithin(p)?"in":"out"));
}
*/
// Check termination condition
if (currentEdge.endPoint == startEdge.startPoint) {
// We finish here. Link the current edge to the start edge, and exit
previousEdges.put(startEdge, currentEdge);
nextEdges.put(currentEdge, startEdge);
edges.add(startEdge);
break;
}
// Compute the next edge
startIndex = endIndex;
endIndex++;
if (endIndex >= pointList.size()) {
endIndex -= pointList.size();
}
// Get the next point
final GeoPoint newPoint = pointList.get(endIndex);
// Build the new edge
// We need to know the sidedness of the new plane. The point we're going to be presenting to it has
// a certain relationship with the sided plane we already have for the current edge. If the current edge
// is colinear with the new edge, then we want to maintain the same relationship. If the new edge
// is not colinear, then we can use the new point's relationship with the current edge as our guide.
final boolean isNewPointWithin = currentEdge.plane.isWithin(newPoint);
final GeoPoint pointToPresent = currentEdge.startPoint;
final SidedPlane newPlane = new SidedPlane(pointToPresent, isNewPointWithin, pointList.get(startIndex), newPoint);
final Edge newEdge = new Edge(pointList.get(startIndex), pointList.get(endIndex), newPlane, internalEdges.get(startIndex));
// Link it in
previousEdges.put(newEdge, currentEdge);
nextEdges.put(currentEdge, newEdge);
edges.add(newEdge);
currentEdge = newEdge;
}
oneEdge = startEdge;
// Verify the structure.
//verify();
}
/*
protected void verify() {
if (edges.size() != previousEdges.size() || edges.size() != nextEdges.size()) {
throw new IllegalStateException("broken structure");
}
// Confirm each edge
for (final Edge e : edges) {
final Edge previousEdge = getPrevious(e);
final Edge nextEdge = getNext(e);
if (e.endPoint != nextEdge.startPoint) {
throw new IllegalStateException("broken structure");
}
if (e.startPoint != previousEdge.endPoint) {
throw new IllegalStateException("broken structure");
}
if (getNext(previousEdge) != e) {
throw new IllegalStateException("broken structure");
}
if (getPrevious(nextEdge) != e) {
throw new IllegalStateException("broken structure");
}
}
if (oneEdge != null && !edges.contains(oneEdge)) {
throw new IllegalStateException("broken structure");
}
if (oneEdge == null && edges.size() > 0) {
throw new IllegalStateException("broken structure");
}
}
*/
/** Get the previous edge.
* @param currentEdge is the current edge.
* @return the previous edge, if found.
*/
public Edge getPrevious(final Edge currentEdge) {
return previousEdges.get(currentEdge);
}
/** Get the next edge.
* @param currentEdge is the current edge.
* @return the next edge, if found.
*/
public Edge getNext(final Edge currentEdge) {
return nextEdges.get(currentEdge);
}
/** Replace a list of edges with a new edge.
* @param removeList is the list of edges to remove.
* @param newEdge is the edge to add.
*/
public void replace(final List removeList, final Edge newEdge) {
/*
System.out.println("Replacing: ");
for (final Edge e : removeList) {
System.out.println(" "+e.startPoint+"-->"+e.endPoint);
}
System.out.println("...with: "+newEdge.startPoint+"-->"+newEdge.endPoint);
*/
final Edge previous = previousEdges.get(removeList.get(0));
final Edge next = nextEdges.get(removeList.get(removeList.size()-1));
edges.add(newEdge);
previousEdges.put(newEdge, previous);
nextEdges.put(previous, newEdge);
previousEdges.put(next, newEdge);
nextEdges.put(newEdge, next);
for (final Edge edge : removeList) {
if (edge == oneEdge) {
oneEdge = newEdge;
}
edges.remove(edge);
previousEdges.remove(edge);
nextEdges.remove(edge);
}
//verify();
}
/** Clear all edges.
*/
public void clear() {
edges.clear();
previousEdges.clear();
nextEdges.clear();
oneEdge = null;
}
/** Get the size of the edge buffer.
* @return the size.
*/
public int size() {
return edges.size();
}
/** Get an iterator to iterate over edges.
* @return the iterator.
*/
public Iterator iterator() {
return new EdgeBufferIterator(this);
}
/** Return a first edge.
* @return the edge.
*/
public Edge pickOne() {
return oneEdge;
}
}
/** An instance of this class represents a known-good
* path of nodes that contains no coplanar points , no matter
* how assessed. It's used in the depth-first search that
* must be executed to find a valid complete polygon without
* coplanarities.
*/
private static class SafePath {
public final GeoPoint lastPoint;
public final int lastPointIndex;
public final Plane lastPlane;
public final SafePath previous;
/** Create a new safe end point.
*/
public SafePath(final SafePath previous, final GeoPoint lastPoint, final int lastPointIndex, final Plane lastPlane) {
this.lastPoint = lastPoint;
this.lastPointIndex = lastPointIndex;
this.lastPlane = lastPlane;
this.previous = previous;
}
/** Fill in a list, in order, of safe points.
*/
public void fillInList(final List pointList) {
//we don't use recursion because it can be problematic
//for polygons with many points.
SafePath safePath = this;
while (safePath.previous != null) {
pointList.add(safePath.lastPoint);
safePath = safePath.previous;
}
pointList.add(safePath.lastPoint);
Collections.reverse(pointList);
}
}
static class MutableBoolean {
public boolean value = false;
}
/** Exception we throw when we can't tile a polygon due to numerical precision issues.
*/
private static class TileException extends Exception {
public TileException(final String msg) {
super(msg);
}
}
}