/*
 * 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.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

/**
 * 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<GeoPoint> 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<GeoPoint> 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<GeoPoint> pointList, final List<GeoPolygon> 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<GeoPoint> pointList, final List<GeoPolygon> 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<GeoPolygon> 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<GeoPoint> firstFilteredPointList = filterPoints(description.points);
      if (firstFilteredPointList == null) {
        return null;
      }
      final List<GeoPoint> 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 (
          @SuppressWarnings("unused")
          TileException e) {
        // Couldn't tile the polygon; use GeoComplexPolygon instead, if we can.
      }
    }
    // Fallback: create large geo polygon, using complex polygon logic.
    final List<PolygonDescription> 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<GeoPoint> 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<GeoPoint> pointList, final List<GeoPolygon> 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<GeoPoint> pointList,
      final List<GeoPolygon> 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<GeoPoint> firstFilteredPointList = filterPoints(pointList);
    if (firstFilteredPointList == null) {
      return null;
    }
    final List<GeoPoint> 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<PolygonDescription> 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<GeoPoint> 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.
   *
   * <p>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.
   *
   * <p>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<PolygonDescription> 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<List<GeoPoint>> 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<List<GeoPoint>> pointsList,
      final PolygonDescription shape,
      BestShape testPointShape,
      final boolean mustBeInside) {
    // First, remove duplicate points.  If degenerate, just ignore the shape.
    final List<GeoPoint> 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<GeoPoint> points;
    public boolean poleMustBeInside;

    public BestShape(final List<GeoPoint> points, final boolean poleMustBeInside) {
      this.points = points;
      this.poleMustBeInside = poleMustBeInside;
    }

    public GeoComplexPolygon createGeoComplexPolygon(
        final PlanetModel planetModel,
        final List<List<GeoPoint>> 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 (
            @SuppressWarnings("unused")
            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<GeoPoint> filteredPointList,
      final List<GeoPolygon> 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<GeoPoint> filterPoints(final List<? extends GeoPoint> input) {

    final List<GeoPoint> 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<GeoPoint> filterEdges(
      final List<GeoPoint> 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<GeoPoint> 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<GeoPoint> 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<GeoPoint> 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<GeoPoint> 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<GeoPoint> pointsList,
      final BitSet internalEdges,
      final int startPointIndex,
      final int endPointIndex,
      final SidedPlane startingEdge,
      final List<GeoPolygon> 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<Edge> edgeIterator = edgeBuffer.iterator();
    while (edgeIterator.hasNext()) {
      final Edge edge = edgeIterator.next();
      final Set<Edge> 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<Edge> 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<Edge> 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<Edge> 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<GeoPoint> 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<GeoPoint> 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<GeoPoint> 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<GeoPolygon> 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<GeoPoint> points = new ArrayList<GeoPoint>(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<GeoPolygon> 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<Edge> 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<Edge> 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<Edge> 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<GeoPoint> points = new ArrayList<GeoPoint>(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<Edge> edges = new ArrayList<Edge>(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<Edge> 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<Edge> 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> {
    /** 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<Edge> edges = new HashSet<>();
    /** Map to previous edge */
    protected final Map<Edge, Edge> previousEdges = new HashMap<>();
    /** Map to next edge */
    protected final Map<Edge, Edge> 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<GeoPoint> 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<Edge> 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<Edge> 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 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.previous = previous;
    }

    /** Fill in a list, in order, of safe points. */
    public void fillInList(final List<GeoPoint> 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);
    }
  }
}
